Новые функции Java8, вы должны это знать!

Java задняя часть API SQL
Новые функции Java8, вы должны это знать!

Оригинальная статья, краткое изложение опыта и жизненные перипетии на всем пути от набора в школу до фабрики А

Нажмите, чтобы узнать подробностиwww.codercc.com

Для Java-разработчиков версия Java8, безусловно, является важной вехой, содержащей множество интересных новых функций.Если мы сможем эффективно использовать эти новые функции, эффективность нашей разработки может быть значительно повышена. Функциональное программирование Java 8 позволяет значительно сократить объем кода и облегчить обслуживание, в то же время есть некоторые функции, связанные с параллелизмом. Новые функции, обычно используемые в разработке, следующие:

1. Методы по умолчанию и статические методы интерфейсов

До Java 8 в интерфейсеТолькоСодержит абстрактные методы. Итак, каковы недостатки этого? Например, если вы хотите добавить абстрактный метод разделителя в интерфейс Collection, это означает, что все классы реализации, которые раньше реализовывали интерфейс Collection, должны повторно реализовать метод spliterator. Метод интерфейса по умолчаниюЧтобы решить проблему несовместимости модификации интерфейса с классом реализации интерфейса, как метод прямой совместимости кода.

Итак, как определить метод по умолчанию в интерфейсе? Давайте посмотрим, как определяется метод spliterator в Collection в JDK:

default Spliterator<E> spliterator() {
    return Spliterators.spliterator(this, 0);
}

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

public interface IAnimal {
    default void breath(){
        System.out.println("breath!");
    };
}


public class DefaultMethodTest implements IAnimal {
    public static void main(String[] args) {
        DefaultMethodTest defaultMethod = new DefaultMethodTest();
        defaultMethod.breath();
    }

}


输出结果为:breath!

Как можно видетьIAnimalПосле того, как интерфейс имеет метод по умолчанию, определенный по умолчанию, его класс реализации DefaultMethodTest также может иметь методы экземпляра.breath. Но если класс наследует несколько интерфейсов, будут конфликты одних и тех же методов в нескольких интерфейсах, как это решить? Фактически, улучшение методов по умолчанию позволяет классам Java иметь возможность множественного наследования, то есть экземпляр объекта будет иметь методы экземпляра нескольких интерфейсов, и, естественно, возникнет проблема повторения методов и конфликтов.

Вот пример:

public interface IDonkey{
    default void run() {
        System.out.println("IDonkey run");
    }
}

public interface IHorse {

    default void run(){
        System.out.println("Horse run");
    }

}

public class DefaultMethodTest implements IDonkey,IHorse {
    public static void main(String[] args) {
        DefaultMethodTest defaultMethod = new DefaultMethodTest();
        defaultMethod.breath();
    }



}

Определите два интерфейса: IDonkey и IHorse, оба из которых имеют один и тот же метод запуска. DefaultMethodTest реализует эти два интерфейса. Поскольку эти два интерфейса имеют один и тот же метод, будут конфликты. Я не знаю, какой метод запуска интерфейса будет преобладать, и компиляция завершится ошибкой:inherits unrelated defaults for run.....

Решение

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

public class DefaultMethodTest implements IAnimal,IDonkey,IHorse {
    public static void main(String[] args) {
        DefaultMethodTest defaultMethod = new DefaultMethodTest();
        defaultMethod.run();
    }

    @Override
    public void run() {
        IHorse.super.run();
    }
}

DefaultMethodTest переопределяет метод запуска и передаетIHorse.super.run();Спецификация основана на методе run в IHorse.

статический метод

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

public interface IAnimal {
    default void breath(){
        System.out.println("breath!");
    }
    static void run(){}
}

2. Функциональный интерфейс FunctionInterface и лямбда-выражения

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

Самым большим изменением в Java 8 является введениеФункциональное мышление, то есть функция может быть использована в качестве аргумента для другой функции.. Функциональный интерфейс, который требует интерфейсаимеет ровно один абстрактный метод, поэтому часто используемые интерфейсы Runnable и Callable являются типичными функциональными интерфейсами. можно использовать@FunctionalInterfaceаннотация, объявляющая, что интерфейс является функциональным интерфейсом. Если интерфейс удовлетворяет определению функционального интерфейса, он по умолчанию преобразуется в функциональный интерфейс. Однако лучше использовать@FunctionalInterfaceАннотация объявлена ​​явно. Это связано с тем, что функциональные интерфейсы хрупки. Если разработчики непреднамеренно добавят другие методы, они нарушат требования функциональных интерфейсов. Если используются аннотации@FunctionalInterface, разработчик будет знать, что текущий интерфейс является функциональным интерфейсом, и не нарушит его непреднамеренно. Вот пример:

@java.lang.FunctionalInterface
public interface FunctionalInterface {
    void handle();
}

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

@java.lang.FunctionalInterface
public interface FunctionalInterface {
    void handle();

    default void run() {
        System.out.println("run");
    }
}

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

лямбда-выражение

Лямбда-выражение — это ядро ​​функционального программирования.Лямбда-выражение — это анонимная функция, представляющая собой тело функции без имени функции, которое может быть напрямую передано в качестве параметра соответствующему вызывающему объекту. Лямбда-выражения значительно повышают выразительность языка Java. Синтаксическая структура лямбды:

(parameters) -> expression
或
(parameters) ->{ statements; }
  • необязательное объявление типа: нет необходимости объявлять тип параметра, компилятор может единообразно идентифицировать значение параметра.

  • скобки для необязательных параметров: Один параметр не должен определять круглые скобки, но несколько параметров должны определять круглые скобки.

  • необязательные фигурные скобки: Если тело содержит оператор, фигурные скобки не требуются.

  • необязательное возвращаемое ключевое слово: если тело имеет только одно возвращаемое значение выражения, компилятор автоматически вернет значение, а фигурные скобки должны указать, что выражение возвращает значение.

Полный пример (взято изновобранец)

public class Java8Tester {
   public static void main(String args[]){
      Java8Tester tester = new Java8Tester();
        
      // 类型声明
      MathOperation addition = (int a, int b) -> a + b;
        
      // 不用类型声明
      MathOperation subtraction = (a, b) -> a - b;
        
      // 大括号中的返回语句
      MathOperation multiplication = (int a, int b) -> { return a * b; };
        
      // 没有大括号及返回语句
      MathOperation division = (int a, int b) -> a / b;
        
      System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
      System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
      System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
      System.out.println("10 / 5 = " + tester.operate(10, 5, division));
        
      // 不用括号
      GreetingService greetService1 = message ->
      System.out.println("Hello " + message);
        
      // 用括号
      GreetingService greetService2 = (message) ->
      System.out.println("Hello " + message);
        
      greetService1.sayMessage("Runoob");
      greetService2.sayMessage("Google");
   }
    
   interface MathOperation {
      int operation(int a, int b);
   }
    
   interface GreetingService {
      void sayMessage(String message);
   }
    
   private int operate(int a, int b, MathOperation mathOperation){
      return mathOperation.operation(a, b);
   }
}

Кроме того, лямбда-выражения также могут обращаться к внешним локальным переменным, как показано в следующем примере:

int adder = 5;
Arrays.asList(1, 2, 3, 4, 5).forEach(e -> System.out.println(e + adder));

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

final int adder = 5;
Arrays.asList(1, 2, 3, 4, 5).forEach(e -> System.out.println(e + adder));

3. Ссылка на метод

Ссылка на метод предназначена для дальнейшего упрощения лямбда-выражения через классИмя или комбинация имени экземпляра и имени метода для прямого доступа к существующему методу или конструктору класса или экземпляра.. Ссылки на методы используют **::определять,Первая половина ::** представляет имя класса или имя экземпляра, а вторая половина представляет имя метода.Если это конструктор, используйтеNEWПредставлять.

Ссылки на методы в Java 8 достаточно гибкие, в общем, существуют следующие формы:

  • Ссылка на статический метод: ClassName::methodName;
  • Ссылка на метод экземпляра для экземпляра: instanceName::methodName;
  • Ссылка на метод экземпляра суперкласса: supper::methodName;
  • Ссылка на метод экземпляра класса: ClassName:methodName;
  • Ссылка на конструктор Класс: новый;
  • Справочник по конструктору массива::TypeName[]::new

Вот пример:

public class MethodReferenceTest {

    public static void main(String[] args) {
        ArrayList<Car> cars = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Car car = Car.create(Car::new);
            cars.add(car);
        }
        cars.forEach(Car::showCar);

    }

    @FunctionalInterface
    interface Factory<T> {
        T create();
    }

    static class Car {
        public void showCar() {
            System.out.println(this.toString());
        }

        public static Car create(Factory<Car> factory) {
            return factory.create();
        }
    }
}


输出结果:

learn.MethodReferenceTest$Car@769c9116
learn.MethodReferenceTest$Car@6aceb1a5
learn.MethodReferenceTest$Car@2d6d8735
learn.MethodReferenceTest$Car@ba4d54
learn.MethodReferenceTest$Car@12bc6874

используется в приведенном выше примереCar::new, что еще больше упрощает лямбда-выражение, создавая ссылку на метод метода,Car::showCar, который представляет ссылку на метод экземпляра.

4. Stream

В Java 8 появился новый метод обработки данных, то есть Stream, который может обрабатывать данные более кратко и эффективно с помощью лямбда-выражений. Stream использует интуитивно понятный способ запроса данных из базы данных, аналогичный операторам SQL, и выполняет различные операции с данными, такие как фильтрация, сортировка и агрегирование.

4.1 Что такое поток

Stream представляет собой очередь элементов из источника данных и поддерживает операции агрегации.Это больше похоже на более высокую версию Iterator, оригинальную версию Iterator, которая может только обходить элементы один за другим и выполнять соответствующие операции. В Stream вам нужно только указать операцию, такую ​​как «фильтрация строк длиной более 10», и Stream пройдёт и завершит указанную операцию внутри себя.

После того, как элементы в потоке обработаны промежуточной операцией в конвейере, конечный результат, наконец, получен терминальной операцией.

  • Источник данных: это источник потока, который может быть преобразован потоком из коллекции, массива, канала ввода-вывода и т. д.;
  • Основные операции: операции, аналогичные операторам SQL, такие как фильтрация, сопоставление, сокращение, поиск, сопоставление, сортировка и другие операции.

Когда мы работаем с потоком, он фактически включает в себя такой процесс выполнения:

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

4.2 Как создать поток

Существует несколько способов создания потока:

  1. Из интерфейса Коллекция и Массивы:

    • Collection.stream();
    • Collection.parallelStream(); //По сравнению с последовательными потоками, параллельные потоки могут значительно повысить эффективность выполнения
    • Arrays.stream(T array);
  2. Статический метод в потоке:

    • Поток();
    • generate(Supplier s);
    • iterate(T seed, UnaryOperator f);
    • empty();
  3. Другие методы

    • Random.ints()
    • BitSet.stream()
    • Pattern.splitAsStream(java.lang.CharSequence)
    • JarFile.stream()
    • BufferedReader.lines()

Ниже приведены примеры двух распространенных методов, описанных выше:

public class StreamTest {


    public static void main(String[] args) {
        //1.使用Collection中的方法和Arrays
        String[] strArr = new String[]{"a", "b", "c"};
        List<String> list = Arrays.asList(strArr);
        Stream<String> stream = list.stream();
        Stream<String> stream1 = Arrays.stream(strArr);

        //2. 使用Stream中提供的静态方法
        Stream<String> stream2 = Stream.of(strArr);
        Stream<Double> stream3 = Stream.generate(Math::random);
        Stream<Object> stream4 = Stream.empty();
        Stream.iterate(1, i -> i++);

    }
}	

4.3 Работа потока

Общие операции потока следующие:

  1. Промежуточная (промежуточная операция): промежуточная операция относится к соответствующему преобразованию или операции над элементами данных в потоке и по-прежнему возвращается как поток потока, который все еще может использоваться для следующей операции потока. Обычно используются: карта (mapToInt, flatMap и т. д.), фильтр, отдельный, отсортированный, просмотр, ограничение, пропуск.
  2. Терминал (конечная операция): относится к конечной операции агрегирования в потоке и к результату вывода.

Промежуточная операция

filter: элементы фильтра в Stream

Отфильтровать строки с пустыми элементами:

long count = stream.filter(str -> str.isEmpty()).count();

map: Сопоставьте элементы в потоке с другим элементом в соответствии с указанными правилами.

Добавьте строку «_map» к каждому элементу

stream.map(str -> str + "_map").forEach(System.out::println);

Метод карты представляет собой отношение «один к одному», и каждый элемент в потоке преобразуется в другой элемент в соответствии с правилами отображения, и если это отношение «один ко многим», необходимо использовать метод плоской карты.

concat: операции слияния потоков

Метод concat объединяет два потока вместе, чтобы сформировать один поток. Если оба входных потока отсортированы, новый поток также сортируется; если какой-либо из входных потоков является параллельным, новый поток также является параллельным; если новый поток закрыт, исходные два входных потока будут выполнять процесс отключения.

Stream.concat(Stream.of(1, 2, 3), Stream.of(4, 5, 6)).
	forEach(System.out::println);

отличный: дедуплицирует поток

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

Stream<String> stream = Stream.of("a", "a", "b", "c");
        stream.distinct().forEach(System.out::println);

输出结果:
a
b
c

limit: ограничить количество элементов в потоке

Перехватите первые два элемента в потоке:

Stream<String> stream = Stream.of("a", "a", "b", "c");
        stream.limit(2).forEach(System.out::println);

输出结果:
a
a

skip: пропустить первые несколько элементов в потоке

Отбросьте первые два элемента в потоке:

Stream<String> stream = Stream.of("a", "a", "b", "c");
        stream.skip(2).forEach(System.out::println);
输出结果:
b
c

peek: работать с каждым элементом в потоке по очереди, аналогично операции forEach.

Пример, приведенный в JDK:

Stream.of("one", "two", "three", "four")
            .filter(e -> e.length() > 3)
            .peek(e -> System.out.println("Filtered value: " + e))
            .map(String::toUpperCase)
            .peek(e -> System.out.println("Mapped value: " + e))
            .collect(Collectors.toList());
输出结果:
Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR

sorted: сортирует элементы в потоке, вы можете настроить правила сравнения через sorted(Comparator super T> comparator)

Stream<Integer> stream = Stream.of(3, 2, 1);
        stream.sorted(Integer::compareTo).forEach(System.out::println);
输出结果:
1
2
3

match: Проверяет, соответствуют ли элементы в потоке указанным правилам сопоставления.

С точки зрения семантики Stream имеет три метода сопоставления:

  • allMatch: все элементы в потоке соответствуют входящему предикату, возвращают true;
  • anyMatch: возвращает значение true, если в потоке есть хотя бы один элемент, соответствующий входящему предикату;
  • noneMatch: Ни один из элементов в потоке не соответствует переданному предикату, возвращает true.

Например, проверить, больше ли каждый элемент в потоке 5:

Stream<Integer> stream = Stream.of(3, 2, 1);
boolean match = stream.allMatch(integer -> integer > 5);
System.out.println(match);
输出结果:
false

окончание операции

Краткое изложение распространенных методов редукции в Collectors

Рефакторинг и пользовательские сборщики Сборщики

count: подсчитать количество элементов в потоке

long count = stream.filter(str -> str.isEmpty()).count();

max/min: найти самый большой или самый маленький элемент в потоке

Stream<Integer> stream = Stream.of(3, 2, 1);
    System.out.println(stream.max(Integer::compareTo).get());

输出结果:
3

forEach

Метод forEach использовался много раз ранее, он используется для обхода всех элементов в потоке, избегая использования циклов for, делая код более кратким, а логику более ясной.

Пример:

Stream.of(5, 4, 3, 2, 1)
    .sorted()
    .forEach(System.out::println);
    // 打印结果
    // 1,2,3,4,5

reduce

Объяснение сокращения в потоке

5. Optional

Чтобы разрешить исключение нулевого указателя, необходимо использовать оператор, такой как if-else, для предотвращения исключения нулевого указателя до Java8, но в Java8 для его решения можно использовать необязательный. Необязательный можно понимать как контейнер данных и даже инкапсулировать null, и если значение существует, метод isPresent() вернет true. Уметь понимать Факультативный. Сначала рассмотрим пример:

public class OptionalTest {


    private String getUserName(User user) {
        return user.getUserName();
    }

    class User {
        private String userName;

        public User(String userName) {
            this.userName = userName;
        }

        public String getUserName() {
            return userName;
        }
    }
}

На самом деле метод getUserName не оценивает, является ли входной параметр нулевым, поэтому этот метод небезопасен. Если вы хотите избежать возможных исключений нулевого указателя до Java 8, вам нужно использоватьif-elseДля логической обработки getUserName изменится следующим образом:

private String getUserName(User user) {
    if (user != null) {
        return user.getUserName();
    }
    return null;
}

Это очень громоздкий фрагмент кода. А если использовать Optional, то будет намного проще:

private String getUserName(User user) {
    Optional<User> userOptional = Optional.ofNullable(user);
    return userOptional.map(User::getUserName).orElse(null);
}

Логическое суждение if-else перед Java8 — это императивный метод программирования, в то время как использование Optional больше похоже на функциональное программирование, ориентированное на конечный результат, а промежуточная обработка передается JDK для внутренней реализации.

До сих пор можно было интуитивно понять, что необязательное очень эффективно помогает избежать исключений нулевого указателя.Ниже кратко изложен API необязательного:

Создать необязательный

  1. Optional.empty(): создание пустого объекта Optional с помощью статического фабричного методаOptional.empty;
  2. Необязательно из (значение T): если значение равно null, немедленно выдать исключение NullPointerException;
  3. Необязательный ofNullable(значение T): Используя статический фабричный метод Optional.ofNullable, вы можете создать необязательный объект, который допускает нулевые значения.

Пример кода:

//创建Optional
Optional<Object> optional = Optional.empty();
Optional<Object> optional1 = Optional.ofNullable(null);
Optional<String> optional2 = Optional.of(null);

Общий метод

1.	boolean equals(Object obj):判断其他对象是否等于 Optional;
2. Optional<T> filter(Predicate<? super <T> predicate):如果值存在,并且这个值匹配给定的 predicate,返回一个Optional用以描述这个值,否则返回一个空的Optional;
3. <U> Optional<U> flatMap(Function<? super T,Optional<U>> mapper):如果值存在,返回基于Optional包含的映射方法的值,否则返回一个空的Optional;
4. T get():如果在这个Optional中包含这个值,返回值,否则抛出异常:NoSuchElementException;
5. int hashCode():返回存在值的哈希码,如果值不存在 返回 0;
6. void ifPresent(Consumer<? super T> consumer):如果值存在则使用该值调用 consumer , 否则不做任何事情;
7. boolean isPresent():如果值存在则方法会返回true,否则返回 false;
8. <U>Optional<U> map(Function<? super T,? extends U> mapper):如果存在该值,提供的映射方法,如果返回非null,返回一个Optional描述结果;
9. T orElse(T other):如果存在该值,返回值, 否则返回 other;
10. T orElseGet(Supplier<? extends T> other):如果存在该值,返回值, 否则触发 other,并返回 other 调用的结果;
11. <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier):如果存在该值,返回包含的值,否则抛出由 Supplier 继承的异常;
12. String toString():返回一个Optional的非空字符串,用来调试

6. Улучшения API даты/времени

В версиях до Java 8 у API даты и времени было много проблем, таких как:

  • Проблемы безопасности потоков: java.util.Date не является потокобезопасным, все классы даты изменяемы;
  • Плохой дизайн: в пакетах java.util и java.sql есть классы даты, и, кроме того, классы для форматирования и синтаксического анализа определены в пакете java.text. И неразумно объединять его вместе для каждого пакета;
  • Проблемы с обработкой часового пояса: классы Date не обеспечивают интернационализацию и не поддерживают часовой пояс, поэтому в Java представлены классы java.util.Calendar и Java.util.TimeZone;

В ответ на эти проблемы в Java 8 был переработан API, связанный с датой и временем, а Java 8 еще больше улучшила обработку даты и времени, выпустив новый API даты и времени (JSR 310). Несколько классов, обычно используемых в пакете java.util.time:

  • Он может получить текущий момент, дату и время, указав часовой пояс. Часы могут заменить System.currentTimeMillis() и TimeZone.getDefault().
  • Instant: мгновенный объект представляет момент времени на временной шкале, а метод Instant.now() возвращает текущую мгновенную точку (среднее время по Гринвичу);
  • Продолжительность: используется для представления количества времени между двумя мгновенными точками;
  • LocalDate: дата с годом, месяцем и днем, которую можно создать с помощью статического метода сейчас или метода;
  • LocalTime: представляет определенное время дня, которое также может быть создано с использованием сейчас и из;
  • LocalDateTime: дата и время;
  • ZonedDateTime: создайте время с часовым поясом, установив идентификатор времени;
  • DateTimeFormatter: класс форматирования даты, который предоставляет множество предопределенных стандартных форматов;

Пример кода выглядит следующим образом:

public class TimeTest {
    public static void main(String[] args) {
        Clock clock = Clock.systemUTC();
        Instant instant = clock.instant();
        System.out.println(instant.toString());

        LocalDate localDate = LocalDate.now();
        System.out.println(localDate.toString());

        LocalTime localTime = LocalTime.now();
        System.out.println(localTime.toString());

        LocalDateTime localDateTime = LocalDateTime.now();
        System.out.println(localDateTime.toString());

        ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
        System.out.println(zonedDateTime.toString());
    }
}
输出结果为:
2018-04-14T12:50:27.437Z
2018-04-14
20:50:27.646
2018-04-14T20:50:27.646
2018-04-14T20:50:27.647+08:00[Asia/Shanghai]

7. Другие улучшения

Java8 также внесла изменения в другие детали, которые можно резюмировать следующим образом:

  1. В предыдущей версии аннотации можно было объявлять только один раз в одном и том же месте, тогда как аннотация @Repeatable предоставляется в версии Java8 для реализации повторяющихся аннотаций;
  2. Метод соединения предоставляется в классе String для завершения объединения строк;
  3. Предоставляет способ распараллелить обработку массивов в Arrays.Например, параллельную сортировку можно выполнить с помощью parallelSort в классе Arrays;
  4. В Java8 мы также приложили большие усилия на уровне параллельных приложений: (1) Обеспечить более мощное будущее: CompletableFuture; (2) StampedLock можно использовать для замены ReadWriteLock; (3) Атомарные классы с большей производительностью: LongAdder, LongAccumulator и DoubleAdder и DoubleAccumulator;
  5. Компилятор добавляет некоторые новые функции и предоставляет некоторые новые инструменты Java.

использованная литература

Ссылки на поток:

Объяснение потокового API

Stream объясняет серию статей

Объяснение Stream очень подробно

Stream API новых функций Java 8

Дополнительные ссылки:

Подробно объясняется API необязательного

Объяснение Необязательной части неплохое, на него стоит обратить внимание

Знакомство с новыми функциями Java 8:

Руководство по новым функциям Java8, очень хорошая информация

Идти в ногу с Java 8 — новые функции, которые вы упустили из виду

Серия руководств по изучению новых возможностей Java8