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, и во многих случаях существует обходной путь или альтернативное решение; нам нужно знать об их возможных проблемах и ограничениях.