Перейти к основному содержимому

Проблемы в Java 8

· 5 мин. чтения

1. Обзор

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

И хотя это не полный список, это субъективная коллекция наиболее распространенных и популярных жалоб на новые функции в Java 8.

2. Поток Java 8 и пул потоков

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

Поток использует общий ForkJoinPool по умолчанию — разбивает последовательности на более мелкие фрагменты и выполняет операции с использованием нескольких потоков.

Однако есть одна загвоздка. Нет хорошего способа указать, какой ForkJoinPool использовать , и поэтому, если один из потоков застрянет, всем остальным, использующим общий пул, придется ждать завершения длительных задач.

К счастью, для этого есть обходной путь:

ForkJoinPool forkJoinPool = new ForkJoinPool(2);
forkJoinPool.submit(() -> /*some parallel stream pipeline */)
.get();

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

Стоит отметить, что есть еще одна потенциальная ловушка: «этот метод отправки задачи в пул fork-join для запуска параллельного потока в этом пуле является «хитростью» реализации и не гарантирует работу» , по словам Стюарта Маркса. — Разработчик Java и OpenJDK из Oracle. Важный нюанс, о котором следует помнить при использовании этой методики.

3. Снижение возможности отладки

Новый стиль кодирования упрощает наш исходный код, но может вызвать головную боль при его отладке .

Прежде всего, давайте посмотрим на этот простой пример:

public static int getLength(String input) {
if (StringUtils.isEmpty(input) {
throw new IllegalArgumentException();
}
return input.length();
}

List lengths = new ArrayList();

for (String name : Arrays.asList(args)) {
lengths.add(getLength(name));
}

Это стандартный императивный код Java, который говорит сам за себя.

Если мы передадим на вход пустую String — в результате — код выкинет исключение, и в консоли отладки мы увидим:

at LmbdaMain.getLength(LmbdaMain.java:19)
at LmbdaMain.main(LmbdaMain.java:34)

Теперь давайте перепишем тот же код, используя Stream API, и посмотрим, что произойдет, когда будет передана пустая строка :

Stream lengths = names.stream()
.map(name -> getLength(name));

Стек вызовов будет выглядеть так:

at LmbdaMain.getLength(LmbdaMain.java:19)
at LmbdaMain.lambda$0(LmbdaMain.java:37)
at LmbdaMain$$Lambda$1/821270929.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.LongPipeline.reduce(LongPipeline.java:438)
at java.util.stream.LongPipeline.sum(LongPipeline.java:396)
at java.util.stream.ReferencePipeline.count(ReferencePipeline.java:526)
at LmbdaMain.main(LmbdaMain.java:39)

Это цена, которую мы платим за использование нескольких уровней абстракции в нашем коде. Однако IDE уже разработали надежные инструменты для отладки Java Streams.

4. Методы, возвращающие null или необязательное значение

Необязательный был введен в Java 8, чтобы обеспечить типобезопасный способ выражения необязательности.

Необязательный , явно указывает, что возвращаемое значение может отсутствовать. Следовательно, вызов метода может вернуть значение, а Optional используется для переноса этого значения внутрь, что оказалось удобным.

К сожалению, из-за обратной совместимости Java мы иногда сталкивались с Java API, смешивающими два разных соглашения. В том же классе мы можем найти методы, возвращающие null, а также методы, возвращающие Options.

5. Слишком много функциональных интерфейсов

В пакете java.util.function у нас есть набор целевых типов для лямбда-выражений. Мы можем различать и группировать их как:

  • Потребитель - представляет собой операцию, которая принимает некоторые аргументы и не возвращает результата.
  • Функция — представляет собой функцию, которая принимает некоторые аргументы и возвращает результат.
  • Оператор - представляет операцию над аргументами некоторого типа и возвращает результат того же типа, что и операнды.
  • Предикат — представляет собой предикат ( логическую функцию) некоторых аргументов.
  • Поставщик — представляет поставщика, который не принимает аргументов и возвращает результаты.

Кроме того, у нас есть дополнительные типы для работы с примитивами:

  • IntConsumer
  • IntFunction
  • IntPredicate
  • IntSupplier
  • Инттодаублефункция
  • Инттолонгфункция
  • … и те же альтернативы для длинных и двойных

Кроме того, специальные типы для функций с арностью 2:

  • BiConsumer
  • Бипредикат
  • Двоичный оператор
  • бифункция

В результате весь пакет содержит 44 функциональных типа, что, безусловно, может сбивать с толку.

6. Проверяемые исключения и лямбда-выражения

Проверенные исключения были проблематичной и противоречивой проблемой еще до Java 8. С появлением Java 8 возникла новая проблема.

Проверенные исключения должны быть либо перехвачены немедленно, либо объявлены. Поскольку функциональные интерфейсы java.util.function не объявляют генерирующие исключения, код, генерирующий проверенное исключение, не будет работать во время компиляции:

static void writeToFile(Integer integer) throws IOException {
// logic to write to file which throws IOException
}
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> writeToFile(i));

Один из способов решить эту проблему — поместить проверенное исключение в блок try-catch и повторно сгенерировать RuntimeException :

List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
try {
writeToFile(i);
} catch (IOException e) {
throw new RuntimeException(e);
}
});

Это сработает. Однако создание RuntimeException противоречит цели проверенного исключения и делает весь код завернутым в шаблонный код, который мы пытаемся сократить, используя лямбда-выражения. Одно из хакерских решений — полагаться на хак скрытных бросков.

Другое решение — написать потребительский функциональный интерфейс, который может генерировать исключение:

@FunctionalInterface
public interface ThrowingConsumer<T, E extends Exception> {
void accept(T t) throws E;
}
static <T> Consumer<T> throwingConsumerWrapper(
ThrowingConsumer<T, Exception> throwingConsumer) {

return i -> {
try {
throwingConsumer.accept(i);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
};
}

К сожалению, мы по-прежнему оборачиваем проверенное исключение в исключение времени выполнения.

Наконец, для более глубокого решения и объяснения проблемы мы можем изучить следующее глубокое погружение: Исключения в Java 8 Lambda Expressions .

8 . Вывод

В этой быстрой статье мы обсудили некоторые недостатки Java 8.

Хотя некоторые из них были преднамеренно выбраны архитекторами языка Java, и во многих случаях существует обходной путь или альтернативное решение; нам нужно знать об их возможных проблемах и ограничениях.