Нововведения Java 8

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

Лямбды, SAM и ссылки на методы

List<String> normalized = names.stream()
        .filter(name -> !name.trim().isEmpty())
        .map(String::trim)
        .collect(Collectors.toList());

Лямбда реализует Функциональные интерфейсы, а ссылка String::trim — более короткая запись совместимого поведения. Тип лямбды определяется контекстом.

Java 8 добавила java.util.function: Predicate, Function, Consumer, Supplier, операторы и primitive-specializations.

Stream API

Stream описывает одноразовый ленивый конвейер:

long count = orders.stream()
        .filter(Order::paid)
        .map(Order::customerId)
        .distinct()
        .count();

Промежуточные операции ленивы; терминальная запускает обработку. Stream не является коллекцией и сам по себе не хранит элементы. Parallel stream использует инфраструктуру Fork/Join и требует ассоциативных, независимых операций — он не гарантирует ускорение.

Default и static-методы интерфейсов

interface Identified {
    long id();
 
    default String externalId() {
        return "id-" + id();
    }
 
    static boolean valid(long id) {
        return id > 0;
    }
}

Default-методы позволили добавлять поведение в интерфейс, не ломая все существующие реализации. При конфликте:

  1. метод класса имеет приоритет над default интерфейса;
  2. более специфичный интерфейс имеет приоритет;
  3. иначе класс должен явно разрешить конфликт и может вызвать InterfaceName.super.method().

Optional

Optional<T> выражает возможное отсутствие результата:

Optional<User> user = repository.findById(id);
 
String name = user
        .filter(User::active)
        .map(User::name)
        .orElse("unknown");

Это прежде всего тип результата API, а не замена каждой nullable-переменной.

java.time

Новый date/time API разделяет разные понятия:

  • Instant — точка на UTC-временной шкале;
  • LocalDate — дата без времени и зоны;
  • LocalDateTime — дата и время без зоны;
  • ZonedDateTime — дата-время с часовым поясом;
  • Duration — длительность по секундам/наносекундам;
  • Period — календарный период;
  • DateTimeFormatter — неизменяемое потокобезопасное форматирование.
Instant expiresAt = clock.instant().plus(Duration.ofMinutes(15));

Выбор типа должен отражать смысл данных; LocalDateTime не является моментом времени без зоны/offset.

CompletableFuture

CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> loadUser(id), executor);
 
CompletableFuture<Profile> profileFuture =
        userFuture.thenCompose(user -> loadProfile(user.id()));
  • thenApply преобразует готовое значение;
  • thenCompose разворачивает вложенную асинхронную операцию;
  • thenCombine объединяет независимые результаты;
  • exceptionally, handle, whenComplete по-разному работают с ошибкой.

Без явного executor async-методы часто используют общий пул; для блокирующего I/O это может быть неподходящим решением.

Изменения библиотек

Java 8 также принесла:

  • Map.computeIfAbsent, compute, merge, replaceAll;
  • Collection.removeIf, forEach, spliterator;
  • Arrays.parallelSort;
  • стандартный Base64;
  • повторяемые аннотации и аннотации применительно к использованию типов;
  • LongAdder и другие concurrent-инструменты;
  • параметры методов в reflection при компиляции с -parameters.

В HotSpot постоянное поколение PermGen было удалено; метаданные классов переместились в Metaspace, управляемый иначе. Это изменение JVM, а не новая возможность языка.

Ловушки

  • Считать Stream хранилищем и пытаться использовать его повторно.
  • Включать parallel() без измерения и проверки безопасности операций.
  • Путать thenApply и thenCompose.
  • Использовать LocalDateTime как абсолютный момент.
  • Выполнять блокирующую работу в общем async-пуле без контроля.
  • Считать default-метод полноценным множественным наследованием состояния.

Вопросы на собеседовании

  1. Какие возможности Java 8 сильнее всего изменили стиль кода?
  2. Чем Stream отличается от коллекции?
  3. Как разрешается конфликт default-методов?
  4. Чем thenApply отличается от thenCompose?
  5. Почему LocalDateTime не определяет момент времени?
  6. Что произошло с PermGen?

Связанные заметки

Лямбда выражения · Функциональные интерфейсы · ФИ стандартной библиотеки Java · Stream API · Класс Optional · Интерфейсы Java