1. Обзор
Java 8 представила некоторые новые функции, которые в основном вращались вокруг использования лямбда-выражений. В этой краткой статье мы рассмотрим недостатки некоторых из них.
И, хотя это не полный список, это субъективная коллекция наиболее распространенных и популярных жалоб на новые функции в Java 8.
2. Пул потоков и потоков Java 8
Прежде всего, параллельные потоки предназначены для облегчения параллельной обработки последовательностей, и это вполне подходит для простых сценариев.
Поток использует стандартный, общий ForkJoinPool – разбивает последовательности на более мелкие фрагменты и выполняет операции с использованием нескольких потоков.
Однако есть одна загвоздка. Нет хорошего способа указать, какой ForkJoinPool использовать , и поэтому, если один из потоков застрянет, всем остальным, использующим общий пул, придется ждать завершения длительных задач.
К счастью, для этого есть обходной путь:
ForkJoinPool forkJoinPool = new ForkJoinPool(2); forkJoinPool.submit(() -> /*some parallel stream pipeline */) .get();
Это создаст новый отдельный ForkJoinPool , и все задачи, созданные параллельным потоком, будут использовать указанный пул, а не общий пул по умолчанию.
Стоит отметить, что есть еще один потенциальный подвох: “этот метод отправки задачи в пул с вилочным соединением для запуска параллельного потока в этом пуле является” трюком “реализации и не гарантирует , что он будет работать” , согласно Стюарту Марксу- разработчику 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-код, который не требует пояснений.
Если мы передадим пустую строку в качестве входных данных – в результате код выдаст исключение, и в консоли отладки мы увидим:
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)
Это цена, которую мы платим за использование нескольких уровней абстракции в нашем коде. Однако Ideas уже разработали надежные инструменты для отладки потоков Java.
4. Методы, возвращающие значение Null или Необязательные
Необязательный был введен в Java 8, чтобы обеспечить типобезопасный способ выражения необязательности.
Необязательно , явно указывает, что возвращаемое значение может отсутствовать. Следовательно, вызов метода может возвращать значение, и Необязательно используется для переноса этого значения внутрь, что оказалось удобным.
К сожалению, из-за обратной совместимости Java мы иногда сталкивались с тем, что API Java смешивали два разных соглашения. В том же классе мы можем найти методы, возвращающие значения null, а также методы, возвращающие Необязательно.
5. Слишком Много Функциональных Интерфейсов
В пакете java.util.function у нас есть коллекция целевых типов для лямбда-выражений. Мы можем различать и группировать их как:
- Потребитель – представляет операцию, которая принимает некоторые аргументы и не возвращает результата
- Функция – представляет функцию, которая принимает некоторые аргументы и выдает результат
- Оператор – представляет операцию над некоторыми аргументами типа и возвращает результат того же типа, что и операнды
- Предикат – представляет предикат ( логическую -значную функцию) некоторых аргументов
- Поставщик – представляет поставщика, который не принимает аргументы и возвращает результаты
Кроме того, у нас есть дополнительные типы для работы с примитивами:
- IntConsumer
- Функция Int
- Интпредикат
- IntSupplier
- IntToDoubleFunction
- Функция IntToLongFunction
- … и те же альтернативы для Длинных и Двойных
Кроме того, специальные типы для функций с арностью 2:
- БиКонсумер
- Двупредикатный
- Двоичный оператор
- бифункциональный
В результате весь пакет содержит 44 функциональных типа, что, безусловно, может привести к путанице.
6. Проверенные исключения и лямбда-выражения
Проверенные исключения уже были проблематичной и спорной проблемой до Java 8. С появлением Java 8 возникла новая проблема.
Проверенные исключения должны быть либо немедленно перехвачены, либо объявлены. Поскольку java.util.function функциональные интерфейсы не объявляют исключения, код, который создает проверенное исключение, завершится ошибкой во время компиляции:
static void writeToFile(Integer integer) throws IOException { // logic to write to file which throws IOException }
Listintegers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> writeToFile(i));
Один из способов преодолеть эту проблему-обернуть проверенное исключение в try-catch блок и перестроить Исключение RuntimeException :
Listintegers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> { try { writeToFile(i); } catch (IOException e) { throw new RuntimeException(e); } });
Это сработает. Однако выбрасывание RuntimeException противоречит цели проверяемого исключения и делает весь код завернутым в стандартный код, который мы пытаемся уменьшить, используя лямбда-выражения. Одним из хакерских решений является полагаться на взлом sneakythrows.
Другим решением является написание пользовательского функционального интерфейса, который может выдавать исключение:
@FunctionalInterface public interface ThrowingConsumer{ void accept(T t) throws E; }
staticConsumer throwingConsumerWrapper( ThrowingConsumer throwingConsumer) { return i -> { try { throwingConsumer.accept(i); } catch (Exception ex) { throw new RuntimeException(ex); } }; }
К сожалению, мы все еще заключаем проверенное исключение в исключение времени выполнения.
Наконец, для более глубокого решения и объяснения проблемы мы можем изучить следующее глубокое погружение: Исключения в лямбда-выражениях Java 8 .
8. Заключение
В этом кратком обзоре мы обсудили некоторые недостатки Java 8.
Хотя некоторые из них были преднамеренным выбором дизайна, сделанным архитекторами языка Java, и во многих случаях существует обходной путь или альтернативное решение; нам действительно нужно знать об их возможных проблемах и ограничениях.