Java 8 - DmitryGontarenko/usefultricks GitHub Wiki

Contents

Static and Default Methods in Interfaces

В Java 8 появилась возможность реализовывать методы в интерфейсе. Такие методы называются default и static.

Default method

Методы по умолчанию (default) - создаются как и обычные методы, но с ключевым словом default:

interface Developer {
    default void write() {
        System.out.println("Java developer writes Java code");
    }
}

Вызвать такой метод можно с помощью создания экземпляра:

        Developer developer = new Developer() {};
        developer.write(); // Java developer writes Java code

При реализации интерфейса, методы по умолчанию можно также переопределять, но это не обязательно:

interface Developer {
    default void write() {
        System.out.println("Java developer writes Java code");
    }
}

class JavaDeveloper implements Developer {

}

class PythonDeveloper implements Developer {
    @Override
    public void write() {
        System.out.println("Python developer writes Python code");
    }
}


public class DefaultAndStaticApp {
    public static void main(String[] args) {
        JavaDeveloper javaDeveloper = new JavaDeveloper();
        PythonDeveloper pythonDeveloper = new PythonDeveloper();

        javaDeveloper.write(); // Java developer writes Java code
        pythonDeveloper.write(); // Python developer writes Python code
    }
}

Но есть один нюанс, если класс реализует два и более интерфейса с одинаковыми именами default-методов - мы увидим ошибку компиляции.

Static method

Статические методы (static) - создаются аналогично обычным статическим методам:

interface Developer {
    static void write() {
        System.out.println("Java developer writes Java code");
    }
}

И вызываются по имени объекта:

        Developer.write(); // Java developer writes Java code

Переопределить или вызвать статический метод в классе наследнике нельзя:

interface Developer {
    static void write() {
        System.out.println("Java developer writes Java code");
    }
}

class JavaDeveloper implements Developer {
    // No methods to implement have been found
}

public class DefaultAndStaticApp {
    public static void main(String[] args) {
        JavaDeveloper developer = new JavaDeveloper();
        developer.write(); // Static method may be invoked on containing interface class only

    }
}

Lambda

Лямбда-выражение - это набор инструкций, более удобная замена анонимным классам. Лямбда-выражение не выполняется само по себе, а реализует абстрактный метод, определенный в функциональном интерфейсе.
В том случае, если метод функционального интерфейса имеет какой-либо тип возвращаемого значения, мы можем запись лямбда-выражение в переменную, а затем использовать в программе любое количество раз.
В лямбда-выражениях используется лямбда-оператор ->, левая часть лямбда-выражения определяет параметры ввода (метода), а правая часть содержит набор инструкций (тело). Например - x -> x * 5.

Мы можем использовать поля на уровне класса в теле лямбда-выражений. Но для использования локальных переменных на уровне метода в теле лямбда-выражений, как и в обычных анонимных классах, мы должны финализировать такие переменные ключевым словом final. В Java 8 стало необязательно использование ключевого слова final в таких случаях, но при попытке изменение переменной - будет выведена ошибка.

Рассмотрим следующий пример, в котором будет показано, в каких случаях можно использовать лямбда-выражение:

/**
 * Данный интерфейс является функциональным
 * и описывает только один метод - 
 * выполнение работы сотрудника
 */
interface Employee {
    void work();
}

/**
 * Перечень конкретных Сотрудников.
 */
class Developer implements Employee {
    @Override
    public void work() {
        System.out.println("Developers writes code");
    }
}

class Analyst implements Employee {
    @Override
    public void work() {
        System.out.println("Analyst creates tasks");
    }
}

/**
 * Класс Company реализует Компанию.
 * Содержит коллекцию Сотрудников и методы для
 * работы с ними.
 * Реализация класс схожа с паттерном Composite.
 */
class Company {
    private List<Employee> employees = new ArrayList<>();

    public void addEmployee(Employee employee) {
        employees.add(employee);
    }

    // Вызываем метод work() у каждого Сотрудника из коллекции
    public void startWork() {
        System.out.println("Разработка проекта началась");

        for (Employee employee : employees) {
            employee.work();
        }
    }
}

/**
 * Клиентский код
 */
public class LambdaApplication {
    public static void main(String[] args) {
        Company company = new Company();
        Employee developer = new Developer();
        Employee analyst = new Analyst();

        company.addEmployee(developer);
        company.addEmployee(analyst);

        company.startWork();
    }
}

// Output:
//        Разработка проекта началась
//        Developers writes code
//        Analyst creates tasks

А теперь представим, что нам нужно добавить еще одного сотрудника. Если действовать по аналогии с выше приведенном кодом, то нам пришлось бы создать новый класс, создать экземпляр этого класса и передать его в метод addEmployee().
Для того, что бы упростить эту процедуру создания нового класса, мы можем воспользоваться анонимным классом:

        company.addEmployee(new Employee() {
            @Override
            public void work() {
                System.out.println("QA Engineer tests code");
            }
        });

У анонимного класса не указан тип или имя экземпляра, потому что он не будет использоваться где то еще. Вместо этого указывается только имя интерфейса (в данном случае Employee), который будет реализован этим анонимным классом.
Но мы не просто создаем класс, мы создаем объект этого анонимного класса, поэтому в конце имени интерфейса должны быть круглые скобки (в данном случае - Employee()), которые говорят о вызове конструктора.
Затем идет описание структуры нашего анонимного класса. Т.к. мы заявили, что данный анонимный класс реализует интерфейс, в структуре будет описана реализация методов данного интерфейса.

Но конструкция абстрактного класса все равно является достаточно громоздкой, давайте попробуем это исправить.
Особенность нашего интерфейса Employee в том, что у него есть только один единственный метод, а это значит, когда мы хотим этот метод реализовать, нет необходимости указывать имя этого метода. Такие интерфейсы называют функциональными.
Функциональные интерфейсы - это интерфейсы, которые имеют ровно один абстрактный метод. В Java 8 такие интерфейсы помечаются одноименной аннотацией @FunctionalInterface, если такая аннотация установлена, компилятор будет выкидывать ошибку при попытке добавить еще один метод в интерфейс.
Если в качестве параметра нашего метода addEmployee() выступает функциональный интерфейс, то запись можно сократить:

  1. Нет необходимости в явном указании интерфейса Employee, т.к. компилятор сам может узнать, какой параметр должен быть подставлен для метода addEmployee();
  2. Поскольку интерфейс является функциональным и состоит из одного единственного метода, то нет необходимости указывать имя метода, и так понятно, что речь идет только о нем;
  3. Тип возвращаемого значения и модификатор доступа можно опустить по той же причине, что метод только один;

Для того, что бы разделить набор параметров, которые указываются в круглых скобках и тело самого метода, применяется специальный символ ->, после чего идет тело метода. Вся получившиеся конструкция и называется лямбда-выражением:

        company.addEmployee(() -> {
            System.out.println("QA Engineer tests code");
        });
  1. Если в нашем методе, которые используется в лямбда-выражении, были бы какие то параметры, то тип этих параметров можно было бы не писать, т.к. компилятор узнал бы их аналогично имени метода в интерфейсе;
  2. Круглые скобки указываются только тогда, когда параметры отсутствуют или их больше одного, при наличии одного параметра в методе круглые скобки можно опустить;
  3. Если тело метода состоит из единственного оператора (строки), то фигурные скобки также можно опустить;
  4. Если метод имеет какое-то возвращающее значение и состоит из одного оператора, то ключевое слово return можно опустить.

Теперь применим всю полученную информацию к нашему лямбда-выражению и получим финальный результат:

        company.addEmployee(() -> System.out.println("QA Engineer tests code"));

Таким образом, мы получили полностью рабочий, легко читаемый и компактный код.

Functional Interface

Функциональные интерфейсы - это интерфейсы, которые имеют ровно один абстрактный метод. Каждое лямбда-выражение этого типа будет сопоставлено объявленному методу. Также, поскольку методы по умолчанию (default) не являются абстрактными, мы можем добавлять в функциональных интерфейс сколько угодно таких методов.
Мы можем использовать любые интерфейсы для лямбда-выражений, содержащие ровно один абстрактный метод. Для того, что бы гарантировать, что интерфейс отвечает этому требованию, используется аннотация @FunctionalInterface. Компилятор осведомлен об этой аннотации, и выдаст ошибку компиляции, если добавить второй абстрактный метод в функциональный интерфейс.
Рассмотрим несколько примеров:

@FunctionalInterface
interface Converter<F, T> {
    T convert(F from);
}
@FunctionalInterface
interface ConvertUpper {
    String make(String text);
}

/**
 * Клиентский код
 */
public class FuncInterfaceApplication {
    public static void main(String[] args) {
        Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
        Integer convert = converter.convert("123");
        System.out.println(convert); // 123

        ConvertUpper convertUpper = (text) -> text.toUpperCase();
        String hello = convertUpper.make("hello");
        System.out.println(hello); // HELLO
    }
}

Встроенные функциональные интерфейсы

В Java 8 была добавлены несколько обобщенных функциональных интерфейсов, которые мы можем использовать по необходимости. Основные из них, это - Predicate, Consumer, Function, Supplier, UnaryOperator и BinaryOperator.

Predicate

Predicate - с помощью метода test() функционального интерфейса Predicat, выполняется проверка какого-либо условия, если условие выполняется, возвращается значение true.
Например, реализуем проверку значений на 0:

        Predicate<Integer> isZero = value -> value == 0;
        System.out.println(isZero.test(0)); // true
        System.out.println(isZero.test(1)); // false

Consumer

Consumer - с помощью метода accept() функционального интерфейса Consumer, выполняется какое-либо действия над объектом типа T, при этом ничего не возвращая.
Например, выведем в консоль текст:

        Consumer<String> print = string -> System.out.println(string);
        print.accept("Hello World!"); // Hello World!

Function

Function - с помощью метода apply() функционального интерфейса Function, выполняется переход от объекта типа T к объекту типа R.
Например, преобразуем значение типа Integer в значение типа Double:

        Function<Integer, Double> converter = value -> Double.valueOf(value);
        System.out.println(converter.apply(1)); // 1.0

Supplier

Supplier - метод get() функционального интерфейса Supplier, не принимает никаких аргументов, но возвращает объект типа T.
Например, выведем в консоль введенных текст:

        Supplier<String> text = () -> {
            Scanner scanner = new Scanner(System.in);
            System.out.print("Enter text: ");
            return scanner.nextLine();
        };
        System.out.println(text.get());

UnaryOperator

UnaryOperator - метод apply() функционального интерфейса UnaryOperator, позволяет выполнять унарные операции над объектом типа T.
Унарной операцией называется операция, имеющая только один операнд (возведение в квадрат, инверсия, инкремент, декремент и т.д.).
Например, вычислим корень числа:

        UnaryOperator<Double> sqrt = value -> Math.sqrt(value);
        System.out.println(sqrt.apply(4.0)); // 2.0

BinaryOperator

BinaryOperator - метод apply() функционального интерфейса BinaryOperator, позволяет выполнять бинарные операции над двумя объектами типа T и U.
Например, возведем число в степень:

        BinaryOperator<Double> pow = (value1, value2) -> Math.pow(value1, value2);
        System.out.println(pow.apply(2.0, 5.0)); // 32

Method Reference

Ссылки на метод позволяют нам в лямбда-выражении обращаться к существующим методам по имени.
Условием реализации ссылки на метод является идентичность параметров, которые приходят на вход лямбда-выражению и парамтеров в методе, на который мы хотим ссылаться.

Рассмотрим на примере:

@FunctionalInterface
interface Printer {
    void print(String text);
}

public class Application {
    private static void print(String text) {
        System.out.println(text);
    }

    public static void main(String[] args) {
        Printer printer = (text) -> print(text);
        printer.print("Hello!"); // Hello!
    }
}

Обратите внимание на то, что тип передаваемого значения и набор параметров у статического метода print(), вызов которого реализует лямбда-выражение, в точности совпадает с методом print() нашего функционального интерфейса.
Т.е. вызывается лямбда выражение с параметром типа String, которое в свою очередь просто вызывает метод с таким же параметром.
В этой ситуации запись можно сократить, указан что все лямбда-выражение реализуется сразу статическим методом print(). Записывается это таким образом:

...

public class Application {
    private static void print(String text) {
        System.out.println(text);
    }

    public static void main(String[] args) {
        Printer printer = Application::print;
        printer.print("Hello!"); // Hello!
    }
}

Почему после используется символ двоеточия ::? В Java допускается создания поля и метода с одинаковым именем, т.е. если бы после имени объекта Application мы поставили точку, компилятор мог бы не понять, что мы имеем ввиду.
Так что оператор :: показывает, что речь идет именно о методе print, а не о поле.

В том случае, если метод не статический, то слева от оператора :: мы указываем не сам класс, а ссылку на объект этого класса.

...

public class Application {
    private void print(String text) {
        System.out.println(text);
    }

    public static void main(String[] args) {
        Application app = new Application();

        Printer printer = app::print;
        printer.print("Hello!"); // Hello!
    }
}

Еще один пример, ссылка на метод работает и с такими методами, как System.out.println():

public class Application {
    public static void main(String[] args) {
        Printer printer = System.out::println;
        printer.print("Hello!"); // Hello!
    }
}

Можно также передавать и ссылки на коснутрктор:

@AllArgsConstructor
@ToString
class User {
    private String name;
    private int age;
}

@FunctionalInterface
interface Database {
    User create(String name, int age);
}

public class Application {
    public static void main(String[] args) {
        Database database = User::new;
        User user = database.create("John", 23);
        System.out.println(user); // User(name=John, age=23)
    }
}

Stream

Java Stream API - это новый способ взаимодействия с данными, представляя их в виде потока данных.

Создание стримов

collection.stream() - создание стрима из коллекции:

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        Stream<Integer> stream = list.stream();

Stream.of() - создание стрима из значений или объектов:

        Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);

Arrays.stream() - создание стрима из массива:

        String[] array = {"One", "Two", "Three"};
        Stream<String> stream = Arrays.stream(array);

Files.lines() - создание стрима из файла (каждая строка в файле будет считаться отдельным элементом стрима):

        Stream<String> lines = Files.lines(Paths.get("test.txt"));

Stream.builder() - создание стрима с помощью билдера:

        Stream<String> builder = Stream.<String>builder()
                .add("One")
                .add("Two")
                .build();

Создание стримов из примитивов

Примитивы не являются объектами и не могут использоваться в дженериках стримов. Поэтому для них созданы специальные фабричные методы:

        IntStream intStream = IntStream.of(1, 2, 3, 4, 5);
        DoubleStream doubleStream = DoubleStream.of(1.2, 3.8, 5.7);
        LongStream longStream = LongStream.of(1, 2, 3, 4);

При создании стрима можно задавать диапозон значений, который он будет содержать:

        IntStream intStream = IntStream.range(1, 10); // от 1 до 9
        LongStream longStream = LongStream.range(1, 10); // от 1 до 9

        IntStream intStream = IntStream.rangeClosed(1, 10); // от 1 до 10
        LongStream longStream = LongStream.rangeClosed(1, 10); // от 1 до 10

Преобразование примитивных стримов

IntStream можно преобразовать в LongStream или в DoubleStream.
LongStream только в DoubleStream.
DoubleStream преобразовать нельзя:

        DoubleStream stream = IntStream.range(1, 10)
                .asLongStream()
                .asDoubleStream();

Примитивные стримы можно преобразовать в массивы примитивных типов:

        int[] ints = IntStream.of(1, 2).toArray();
        double[] doubles = DoubleStream.of(1, 2).toArray();
        long[] longs = LongStream.of(1, 2).toArray();

Методы работы со стримами

Stream API предлагает два вида методов:

  1. Конвейерные — возвращают другой stream, то есть работают как builder;
  2. Терминальные — возвращают другой объект, такой как коллекция, примитивы, объекты, Optional и т.д. Такие методы "закрывают" использование стрима.

Конвейерные операции

skip

skip() - позволяет пропустить N первых элементов:

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        list.stream()
                .skip(3)
                .forEach(e -> System.out.println(e));  // 4, 5

limit

limit() - позволяет ограничить выборку определенным количеством первых элементов:

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        list.stream()
                .limit(3)
                .forEach((e) -> System.out.println(e)); // 1, 2, 3

distinct

distinct() - Возвращает стрим без дубликатов:

        List<Integer> list = Arrays.asList(1, 1, 1, 2, 3);
        list.stream()
                .distinct()
                .forEach(e -> System.out.println(e)); // 1, 2, 3

map

map() - метод, который принимает лямбда-выражение известное как функция (Function) и преобразует каждый элемент стрима:

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        list.stream()
                .map(e -> e * 2)
                .forEach(e -> System.out.println(e)); // 2, 4, 6, 8, 10

sorted

sorted() - позволяет сортировать значения либо в натуральном порядке, либо задавая Comparator*:

        List<Integer> list = Arrays.asList(4, 5, 3, 1, 2);
        list.stream()
                .sorted()
                .forEach((e) -> System.out.println(e)); // 1, 2, 3, 4, 5

Сортировка объекта по полям age и name:

        List<Model> list = new ArrayList<Model>() {{
            add(new Model((short) 12, "Jame"));
            add(new Model((short) 5, "Kate"));
            add(new Model((short) 4, "Kate"));
            add(new Model((short) 30, "Lex"));
        }};

        // создаем свой компаратор
        Comparator<Model> compare = Comparator
                .comparingInt(Model::getAge)
                .thenComparing(Model::getName);

        // передаем созданный компаратор и сортируем
        List<Model> collect = list.stream()
                .sorted(compare)
                .collect(Collectors.toList());

        /*
        Output:
          4 Kate
          5 Kate
          12 Jame
          30 Lex
         */

filter

filter() - это метод, который позволяет фильтровать данные по каком-либо значению, в качестве аргумента он принимает лямбда-выражение, известное как Предикат.

Вывести список четных чисел коллекции:

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        list.stream()
                .filter(integer -> integer % 2 == 0)
                .forEach(integer -> System.out.println(integer)); // 2, 4

К стриму может быть применено несколько фильтров одновременно:

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        list.stream()
                .filter(integer -> integer % 2 == 0)
                .filter(integer -> integer != 2)
                .forEach(integer -> System.out.println(integer)); // 4

Терминальные операции

count

count() - возвращает количество элементов в стриме:

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

        long count = list.stream()
                .count();
        System.out.println(count); // 5

reduce

reduce() - этот метод принимает начальное значение (identity) и лямбда выражение (аккумулятор), с помощью котрого можно задать метод "аккумулирования" значений.

        List<Integer> listOfNumbers = Arrays.asList(1, 2, 3, 4);
        Optional<Integer> result = listOfNumbers.stream().reduce((x, y) -> x + y); // 1 + 2 + 3 + 4
        System.out.println(result); // 10

        int sum = listOfNumbers.parallelStream().reduce(5, Integer::sum); // (5 + 1) + (5 + 2) + (5 + 3) + (5 + 4)
        System.out.println(sum); // 30

        Integer reduce = listOfNumbers.stream().reduce(5, (x, y) -> x + y); // identity как начальное значение, 5 + 1 + 2 + 3 + 4
        System.out.println(reduce); // 15

anyMatch

anyMatch() - этот метод принимает Предикат и возвращает true, если условие выполняется хотя бы для одного элемента:

        List<Integer> list = Arrays.asList(1, 2, 3);

        boolean b = list.stream()
                .anyMatch(e -> e % 2 == 0);
        System.out.println(b); // true

noneMatch

noneMatch() - этот метод принимает Предикат и возвращает true, если условие не выполняется ни для одного элемента:

        List<Integer> list = Arrays.asList(1, 3, 5);

        boolean b = list.stream()
                .noneMatch((e) -> e % 2 == 0);
        System.out.println(b); // true

allMatch

allMatch() - этот метод принимает Предикат и возвращает true, если условие выполняется для всех элементов:

        List<Integer> list = Arrays.asList(2, 4, 6);

        boolean b = list.stream()
                .allMatch((e) -> e % 2 == 0);
        System.out.println(b); // true

findAny

findAny() - возвращает первый попавшийся элемент из стрима, в виде обертки Optional.
Но если использовать метод orElse(), возвращенный элемент будет того же типа, что и коллекция:

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

        Integer integer = list.stream()
                .findAny()
                .orElse(0);
        System.out.println(integer); // 1

findFirst

findFirst() - возвращает первый элемент из стрима, в виде обертки Optional.
Но если использовать метод orElse(), возвращенный элемент будет того же типа, что и коллекция:

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

        Integer integer = list.stream()
                .filter((e) -> e > 3)
                .findFirst()
                .orElse(0);
        System.out.println(integer); // 4

forEach

forEach() - терминальный метод, который предназначен для перебора коллекции и выполнения определенного действия над наждым элементом.
Перебор коллекции типа List:

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        list.forEach(number -> System.out.println(number)); // 1, 2, 3, 4, 5

Перебор коллекции типа Set:

        Set<Integer> set = new HashSet<>(Arrays.asList(1, 1, 2, 3, 4));
        set.forEach(number -> System.out.println(number)); // 1, 2, 3, 4

Перебор коллекции типа Map:

        Map<Integer, String> map = new HashMap<Integer, String>() {{
            put(1, "John");
            put(2, "Paul");
            put(3, "Sarah");
        }};
        map.forEach((key, value) -> System.out.println("id: " + key + ", value: " + value)); // id: 1, value: John, etc.

Так как метод forEach() принимает в качестве аргумента лямбда-выражение, мы можем задавать разные дополнительные условия:

        List<Integer> list = Arrays.asList(100, 200, 300);
        list.forEach(number -> {
            if (number > 150) {
                System.out.println(number); // 200, 300
            }
        });

collect

collect() - это метод, позволяющий приминимать лямбда-выражения известное как коллектор (Collector)*, которое собирает данные в необходимую структуру данных. В Java 8 уже существует класс Collectors с необходимыми нам методами, например:

Collectors.toList() - вернет нам ArrayList:

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

        List<Integer> collect = list.stream()
                .filter((e) -> e % 2 == 0)
                .collect(Collectors.toList());
        System.out.println(collect); // [2, 4]

Collectors.toSet() - вернет нам HashSet:

        List<Integer> list = Arrays.asList(1, 1, 1, 2, 3);

        Set<Integer> collect = list.stream()
                .collect(Collectors.toSet());
        System.out.println(collect); // 1, 2, 3

Collectors.toCollection() - принимает лямбда-выражение типа поставщик (Supplier), которое должно вернуть коллекцию, в которую мы хотим сохранить данные. В данном случае возвращается коллекция типа LinkedList:

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

        LinkedList<Integer> collect = list.stream()
                .collect(Collectors.toCollection(() -> new LinkedList<>()));
        System.out.println(collect); // 1, 2, 3, 4, 5

sum

sum() - терминальная операция для примитивных стримов, которая возвращает сумму всех элементов стрима:

        int intSum = IntStream.of(1, 2, 3).sum(); // 6
        double doubleSum = DoubleStream.of(1.3, 4.7, 8.2).sum(); // 14.2
        long longSum = LongStream.of(1, 2, 3).sum(); // 6

average

average() - терминальная операция для примитивных стримов, которая возвращает среднее значение из всех элементов стрима. Этот метод возвращает тип OptionalDouble, обертку над double:

        OptionalDouble intAverage = IntStream.of(1, 2, 3).average(); // 2.0
        OptionalDouble doubleAverage = DoubleStream.of(1.3, 4.7, 8.2).average(); // 4.7
        OptionalDouble LongAverage = LongStream.of(1, 2, 5).average(); // 2.6

max

max() - терминальная операция, которая возвращает максимальных элемент потока на основе представленного компаратора.

        // Получить максимальное значение
        List<Integer> list = Arrays.asList(-9, -18, 0, 25, 4);
        Integer i = list.stream().max(Integer::compare).orElse(null); // 25

        // Получить наименьшее значение (но логичнее было бы использовать функцию min)
        List<Integer> list = Arrays.asList(-9, -18, 0, 25, 4);
        Integer i = list.stream().max(Comparator.reverseOrder()).orElse(null); // -18

Вернуть объект с самой последней датой:

        List<Model> modelList = Arrays.asList(
                new Model(new Date()),
                new Model(new GregorianCalendar(2024, Calendar.FEBRUARY, 11, 15, 30).getTime())
        );
        Model result = modelList.stream()
                .max(Comparator.comparing(Model::getDate))
                .orElse(null);

min

min() - терминальная операция, которая возвращает минимальный элемент потока на основе представленного компаратора.

        // Получить минимальное значение
        List<Integer> list = Arrays.asList(-9, -18, 0, 25, 4);
        Integer i = list.stream().min(Integer::compare).orElse(null); // -18

orElse

orElse() - возвращает значение по дефолту.
Например, если коллекция пуста - возвращаем 0:

        List<Integer> list = Arrays.asList(); // пустая коллекция

        Integer integer = list.stream()
                .findFirst()
                .orElse(0);
        System.out.println(integer); // 0

Map methods

В Java 8 для Map было добавлено несколько новых методов.

pulIfAbsent

pulIfAbsent() - позволяет добавить пару в Map, если ее там нет:
До Java 8 нам пришлось бы проверять эту условие вручную:

        Map<String, String> map = new HashMap<>();
        if (map.get("name") == null) {
            map.put("name", "John");
        }

После Java 8:

        Map<String, String> map = new HashMap<>();
        map.putIfAbsent("name", "John");

computeIfPresent

computeIfPresent() - позволяет произвести какое-либо действие со значением в Map, если оно там есть:

        Map<String, String> map = new HashMap<>();
        map.put("name", "John");

        map.computeIfPresent("name", (key, value) -> key + ": " + value);
        System.out.println(map.get("name")); // name: John

В данном случае мы объединили ключ и значение Map.

computeIfAbsent

computeIfPresent() - позволяет произвести какое-либо действие со значением в Map, если его там нет:

        Map<String, String> map = new HashMap<>();

        map.computeIfAbsent("key", key -> key + ": " + "new value");
        System.out.println(map.get("key")); // key: new value

В том случае, если значение есть, то оно как обычно выведется по ключу:

        Map<String, String> map = new HashMap<>();
        map.put("key", "value");

        map.computeIfAbsent("key", key -> key + ": " + "new value");
        System.out.println(map.get("key")); // value

remove

remove() - позволяет удалить пару из Map, если переданные для этого в метод ключ и значения полностью совпадают:

        Map<String, String> map = new HashMap<>();
        map.put("name", "John");

        map.remove("name", "John");
        System.out.println(map.get("name")); // null

getOrDefault

getOrDefault() - позволяет вернуть значение по умолчанию, если значения с переданным ключем нет:

        Map<String, String> map = new HashMap<>();
        map.put("name", "John");

        String value = map.getOrDefault("some key", "default value");
        System.out.println(value); // default value

merge

merge() - позволяет по ключу объединить в Map одно значение с другим:

        Map<String, String> map = new HashMap<>();
        map.put("name", "John");

        map.merge("name", " Wick", (oldValue, newValue) -> oldValue + newValue);
        System.out.println(map.get("name")); // John Wick

Если такой пары в Map нет, она будет создана:

        Map<String, String> map = new HashMap<>();

        map.merge("name", " Wick", (oldValue, newValue) -> oldValue + newValue);
        System.out.println(map.get("name")); // Wick

DataTimeAPI

Java 8 содержит новое API для работы с датой и временем. В нем имеются неизменные, потокобезопасные классы, находящиеся в пакете java.time:
LocalDate - дата без времени и временных зон;
LocalTime - время без даты и временных зон;
LocalDateTime - дата и время без временных зон;
ZonedDateTime - дата и время с временной зоной;
DateTimeFormatter - формитирует даты в строки и наборот, только для классов java.time;
Instant - колличество секунд с Unix Epoch Time (с 00.00 01.01.1970);
Duration - продолжительность в секундах и наносекундах;
Period - период времени в годах, месяцах и днях;
TemporalAdjuster - корректировщих дат.

Получить текущую дату/время.

Получение текущий даты/времени осуществляется с помощью статического метода now() для классов LocalDate, LocalTime и LocalDateTime:

        System.out.println(LocalDate.now()); // 2020-05-25
        System.out.println(LocalTime.now()); // 17:01:40.762
        System.out.println(LocalDateTime.now()); // 2020-05-25T17:01:40.762

Задать дату/время.

Для установки нужной даты/времени используется статический метод of() для классов LocalDatem LocalTime и LocalDateTime. Значение передаваемых параметров отличаются и зависят от перечисленных классов:

        System.out.println(LocalDate.of(2014, 1, 1)); // 2014-01-01
        System.out.println(LocalTime.of(12, 10)); // 12:10
        System.out.println(LocalDateTime.of(2003, Month.JUNE, 6, 12, 10)); // 2003-06-06T12:10

Добавить или отнять дату/время.

LocalDate

Для добавления в классе LocalDate существуют методы plus(), plusDays(), plusWeeks(), plusMonth() и plusYears().

        System.out.println(LocalDate.now()); // 2020-05-25 (для сравнения)

        System.out.println(LocalDate.now().plusDays(1)); // 2020-05-26
        System.out.println(LocalDate.now().plusWeeks(2)); // 2020-06-08
        System.out.println(LocalDate.now().plusMonths(3)); // 2020-08-25
        System.out.println(LocalDate.now().plusYears(4)); // 2024-05-25

        System.out.println(LocalDate.now().plus(Period.ofDays(5))); // 2020-05-30
        System.out.println(LocalDate.now().plus(1, ChronoUnit.DECADES)); // 2030-05-25

Метод plus() принимает классы Period или Duration, либо количество того, что мы хотим добавить и ChronoUnit (Enum, который содержит значения от NANOS до FOREVER). Для LocalDate валидными из ChronoUnit являются только значения от DAYS до ERAS (т.е. только те, которые относятся к датам).

Что бы отнять значения в LocalDate существуют схожие методы - minus(), minusDays(), minusWeeks(), minusMonths() и minusYears(). Передача параметров для метода minus() действует по тем же правилам, что для и plus().

Мы также можем прибавлять и отнимать отрицательные значения:

        System.out.println(LocalDate.now()); // 2020-05-25 (для сравнения)

        System.out.println(LocalDate.now().plusDays(-5)); // 2020-05-20
        System.out.println(LocalDate.now().minusDays(-5)); // 2020-05-30

LocalTime

Для добавления в классе LocalTime существуют методы plus(), plusNanos(), plusSeconds(), plusMinutes() и plusHours().

        System.out.println(LocalTime.now()); // 17:56:21.484

        System.out.println(LocalTime.now().plusNanos(100_000)); // 17:56:21.484100
        System.out.println(LocalTime.now().plusSeconds(2500)); // 18:38:01.484
        System.out.println(LocalTime.now().plusMinutes(120)); // 19:56:21.484
        System.out.println(LocalTime.now().plusHours(12)); // 05:56:21.484
        System.out.println(LocalTime.now().plus(Duration.ofHours(5))); // 22:56:21.484
        System.out.println(LocalTime.now().plus(1, ChronoUnit.HALF_DAYS)); // 05:56:21.485

Значения ChronoUnit в данном случае считаются валидными от NANOS до HALF_DAYS (т.е. только значения времени).

Что бы отнять значения в LocalTime существуют схожие методы - minus(), minusNanos(), minusSeconds(), minusMinutes() и minusHours(). Передача параметров для метода minus() действует по тем же правилам, что для и plus().

Прибавлять и отнимать отрицательные числа мы все также можем.

LocalDateTime

Поскольку LocalDateTime это дата и время, то и методы для того что бы прибавлять/отнимать значения просто делегирются методам из LocalDate и LocalTime.

Сравнение дат

Для сравнения даты и времени в классах LocalDate, LocalTime и LocalDateTime используются метода isAfter() и isBefore().

        LocalDate today = LocalDate.now();
        LocalDate _2018 = LocalDate.of(2017, 1, 1);
        System.out.println(today.isAfter(_2018)); // true
        System.out.println(today.isBefore(_2018)); // false

        LocalTime now = LocalTime.now();
        LocalTime hoursLater = now.minusHours(1);
        System.out.println(now.isAfter(hoursLater)); // true
        System.out.println(now.isBefore(hoursLater)); // false

        LocalDateTime dateTimeNow = LocalDateTime.now();
        LocalDateTime yearAgo = dateTimeNow.plusYears(1);
        System.out.println(dateTimeNow.isAfter(yearAgo)); // false
        System.out.println(dateTimeNow.isBefore(yearAgo)); // true

При сравнении используется метод compareTo(), который можно вызвать вручную. Он переопределен в каждом из классов (сравнивать их друг с другом нельзя). Сравнение идет от большей единицы к меньшей, в LocalTime от наносекунды до часа, в LocalDate от года до дня, а в LocalDateTime от даты до времени.

        LocalDate today = LocalDate.now();
        LocalDate tomorrow = LocalDate.now().plusDays(1);
        System.out.println(today.compareTo(tomorrow)); // -1 (больше)

        LocalTime timeNow = LocalTime.now();
        LocalTime hourLater = LocalTime.now().minusHours(1);
        System.out.println(timeNow.compareTo(hourLater)); // 1 (меньше)

        LocalDateTime localDateTime = LocalDateTime.now();
        LocalDateTime localDateTime2 = LocalDateTime.now();
        System.out.println(localDateTime.compareTo(localDateTime2)); // 0 (равно)

Стоит напомнить, что метода compareTo() возвращает тип int с результатом 1 - если второе число меньше, 0 - если равно и -1 - если больше.

Форматирование дат

При получении даты и времени мы можем задать собственный формат отображения.
Для этого используется метод format(), который принимает класс DateTimeFormatter.
Класс DateTimeFormatter имеет множество уже готовых форматов, но мы всегда можем задать свой собственный - используя символы даты и времени (пример).

        LocalDate localDate = LocalDate.now();
        System.out.println(localDate.format(DateTimeFormatter.BASIC_ISO_DATE)); // 20200526
        System.out.println(localDate.format(DateTimeFormatter.ISO_DATE)); // 2020-05-26
        System.out.println(localDate.format(DateTimeFormatter.ofPattern("dd-MMM-yyyy"))); // 26-мая-2020
        System.out.println(localDate.format(DateTimeFormatter.ofPattern("dd-MMM-yyyy", Locale.FRANCE))); // 26-mai-2020

        LocalTime localTime = LocalTime.now();
        System.out.println(localTime.format(DateTimeFormatter.ofPattern("hh:mm:ss"))); // 10:09:06

        LocalDateTime localDateTime = LocalDateTime.now();
        System.out.println(localDateTime.format(DateTimeFormatter.ofPattern("E, dd-MM-yy hh:mm:ss"))); // Вт, 26-05-20 10:09:06

ZonedDateTime

Для работы с временными зонами существует класс ZonedDateTime.
Он создается так же, как и остальные классы даты/времени, а внутри себя просто содержит LocalDateTime и ZoneId. Т.е. в нем уже существует возможность прибавить и отнять дату, сравнить дату/время или отформатировать вывод.

        ZonedDateTime zonedDateTime = ZonedDateTime.now();
        System.out.println(zonedDateTime); // 2020-05-26T10:56:34.240+03:00[Europe/Moscow]

        ZonedDateTime newYorkTime = ZonedDateTime.of(
                LocalDate.of(2015, 1, 1),
                LocalTime.of(15, 40),
                ZoneId.of("America/New_York")
        );
        System.out.println(newYorkTime); // 2015-01-01T15:40-05:00[America/New_York]

Получить список id всех временных зон (без указания смещения по времени) можно с помощью цикла:

        List<String> zones = new ArrayList<>(ZoneId.getAvailableZoneIds());
        zones.forEach(System.out::println);

Duration

Класс Duration позволяет измерить разницу во времени.

        Duration time = Duration.between(
                LocalTime.now(), 
                LocalTime.now().plusHours(1)
        );
        System.out.println(time.toHours()); // 1

Но существуют способы, с помощью которых можно сравнить и даты:

        LocalDate now = LocalDate.now(); // 2020-05-26
        LocalDate nextBirthday = LocalDate.of(2020, 8, 2); // 2020-08-02

        Duration duration = Duration.between(now.atStartOfDay(), nextBirthday.atStartOfDay());
        System.out.println(duration.toDays()); // 68

        long daysLeft = ChronoUnit.DAYS.between(now, nextBirthday);
        System.out.println(daysLeft); // 68

Period

Класс Period позволяет получить временной промежуток между двумя датами. Но разница это будет получена в буквальном смысле, например:

        LocalDate now = LocalDate.now(); // 2020-05-26
        LocalDate nextBirthday = LocalDate.of(2020, 8, 2); // 2020-08-02

        Period period = Period.between(now, nextBirthday);
        System.out.println(period.getDays()); // 7

Что бы получить "развернутый" результат временного промежутка, небоходимо будет поочередно вывести методы period.getDays(), period.getMonths() и period.getYears().

        Period period = Period.between(now, nextBirthday);
        System.out.println(
                period.getDays() + "  days, " +
                period.getMonths() + " months and " +
                period.getYears() + " years left before the birthday"
        ); // 7  days, 2 months and 0 years left before the birthday

TemporalAdjusters

Класс TemporalAdjusters позволяет получить какую-либо конкретную дату. Например, следующий понедельник месяца или последний день месяца.

        LocalDate localDate = LocalDate.of(2020, Month.JULY, 2);
        TemporalAdjuster fourthFriday = TemporalAdjusters.dayOfWeekInMonth(4, DayOfWeek.FRIDAY);
        TemporalAdjuster nextMonday = TemporalAdjusters.next(DayOfWeek.MONDAY);
        TemporalAdjuster nextTuesday = TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY);
        TemporalAdjuster previousMonday = TemporalAdjusters.previous(DayOfWeek.MONDAY);
        TemporalAdjuster lastDayOfYear = TemporalAdjusters.lastDayOfYear();

        System.out.println(localDate.with(fourthFriday)); // 2020-07-24
        System.out.println(localDate.with(nextMonday)); // 2020-07-06
        System.out.println(localDate.with(nextTuesday)); // 2020-07-07
        System.out.println(localDate.with(previousMonday)); // 2020-06-29
        System.out.println(localDate.with(lastDayOfYear)); // 2020-12-31

В данном примере рассматривается лишь малая часть из возможных методов.
Метод dayOfWeekInMonth() возвращает 4-ю пятницу месяца;
Метод next() возвращает следующий понедельник;
Метод nextOrSame() возвращает следующий вторник, но если условие совпадает с текущий датой, то вернется сегодяшняя дата;
Метод previous() возвращает предыдущий понедельник;
Метод lastDayOfYear() вернет дату последнего дня в году.

Sources

  1. YouTube. Лямбда-выражения в Java 8
  2. Metanit. Лямбда-выражения
  3. YouTube. Java Lambdas
  4. Vertex. Java 8 Учебник: теория и примеры
  5. Habr. Новое в Java 8
⚠️ **GitHub.com Fallback** ⚠️