Java 8

The main changes of the Java 8 release were these:

·        Lambda Expression and Stream API

·        Method Reference

·        Default Methods

·        Type Annotations

·        Repeating Annotations

·        Method Parameter Reflection

Java 9

Java 9 introduced these main features:

·        Java Module System

·        Try-with-resources

·        Diamond Syntax with Inner Anonymous Classes

·        Private Interface Methods

Java 10

Local Variable Type Inference

Implicit Typing with var

Java 11

Local Variable Type in Lambda Expressions

Java 14

Switch Expressions

Switch expressions allowed us to omit break calls inside every case block. 

Using Switch Expressions

days = switch (month) {

            case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> 31;

            case FEBRUARY -> 28;

            case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30;

            default -> throw new IllegalStateException();

        };

 

The yield Keyword

days = switch (month) {

            case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> {

                System.out.println(month);

                yield 31;

            }

Java 15

Text Blocks “””

Java 16

Pattern Matching of instanceof

defining variable Car c and Bicycle b

 

public class PatternMatching {

    public static double price(Vehicle v) {

        if (v instanceof Car c) {

            return 10000 - c.kilomenters * 0.01 -

                    (Calendar.getInstance().get(Calendar.YEAR) -

                            c.year) * 100;

        } else if (v instanceof Bicycle b) {

            return 1000 + b.wheelSize * 10;

        } else throw new IllegalArgumentException();

    }

}

 

Records

 

public record VehicleRecord(String code, String engineType) {}

we cannot extend a record class

 

 

 

Java 17

Sealed Classes

 

public sealed class Vehicle permits Bicycle, Car {...}

The final modifier on a class doesn’t allow anyone to extend it

What about when we want to extend a class but only allow it for some classes?

       

public sealed class Vehicle permits Bicycle, Car {...}

 

A Java functional interface is an interface that contains exactly one abstract method. 

@FunctionalInterface annotation can be used to explicitly mark an interface as functional
it serves as a compile-time check, preventing the addition of more abstract methods and ensuring the interface adheres to the functional interface contract.

 

Function Interfaces

·        Function<T, R> - Takes one argument of type T and returns type R

·        BiFunction<T, U, R> - Takes two arguments and returns a result

·        UnaryOperator<T> - Special case of Function where input and output are the same type

·        BinaryOperator<T> - Special case of BiFunction where both inputs and output are the same type

Consumer Interfaces

·        Consumer<T> - Takes one argument and returns nothing (void)

·        BiConsumer<T, U> - Takes two arguments and returns nothing

Supplier Interfaces

·        Supplier<T> - Takes no arguments and returns a result

Predicate Interfaces

·        Predicate<T> - Takes one argument and returns boolean

·        BiPredicate<T, U> - Takes two arguments and returns boolean

 


@FunctionalInterface

public interface Calculator {

    double calculate(double x, double y);

}

 

  Lambda expressions: list.forEach(item -> System.out.println(item))

  Method references: list.forEach(System.out::println)

  Stream operations: stream.map(String::toUpperCase).filter(s -> s.length() > 3)

// Function: Convert String to Integer

Function<String, Integer> stringToInt = s -> Integer.parseInt(s);

Integer num = stringToInt.apply("123"); // returns 123

 

// Consumer: Print a message

Consumer<String> printer = s -> System.out.println(s);

printer.accept("Hello, World!"); // prints "Hello, World!"

 

// Supplier: Get a random number

Supplier<Double> randomSupplier = () -> Math.random();

Double rand = randomSupplier.get(); // returns a random double

 

// Predicate: Check if a string is empty

Predicate<String> isEmpty = s -> s.isEmpty();

boolean result = isEmpty.test(""); // returns true

 

List<List<String>> listOfLists = List.of(

        List.of("a", "b", "c"),

        List.of("d", "e"),

        List.of("f", "g", "h")

);

 

// Goal: Convert to ["a", "b", "c", "d", "e", "f", "g", "h"]

 

List<String> singleList = listOfLists.stream()

        .flatMap(oneList -> oneList.stream()) // Function: List<String> -> Stream<String>

        .toList();

 

// Input: 3 elements (lists) -> Output: 8 elements (strings)

 

List<String> lines = List.of("Hello world", "Java streams", "flatMap example");

 

// Goal: Get a list of all individual words: ["Hello", "world", "Java", "streams", "flatMap", "example"]

 

// Using map (WRONG - gives us streams of words, not words)

List<Stream<String>> wrongResult = lines.stream()

        .map(line -> Arrays.stream(line.split(" ")))

        .toList(); // A list of 3 streams! Not what we want.

 

// Using flatMap (CORRECT - flattens all the streams of words into one stream)

List<String> allWords = lines.stream()

        .flatMap(line -> Arrays.stream(line.split(" "))) // Split line into a Stream<String>

        .toList();

 

System.out.println(allWords);

// Output: [Hello, world, Java, streams, flatMap, example]

 

Optional<String> optionalValue = Optional.of("hello");

 

// Let's say we have a function that returns an Optional

Optional<String> toUpperCaseOptional(String s) {

    return Optional.of(s.toUpperCase());

}

 

// Using map (results in Optional<Optional<String>> - BAD!)

Optional<Optional<String>> badResult = optionalValue.map(v -> toUpperCaseOptional(v));

 

// Using flatMap (results in Optional<String> - GOOD!)

Optional<String> goodResult = optionalValue.flatMap(v -> toUpperCaseOptional(v));

 

System.out.println(goodResult); // Output: Optional[HELLO]