Проблемы в Java 8

Проблемы в Java 8

1. обзор

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

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

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

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

Stream использует стандартный, общийForkJoinPool - разбивает последовательности на более мелкие части и выполняет операции с использованием нескольких потоков.

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

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

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

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

Стоит отметить, что есть еще один потенциальный улов:“this technique of submitting a task to a fork-join pool, to run the parallel stream in that pool is an implementation ‘trick' and is not guaranteed to work”, по словам Стюарта Маркса, разработчика Java и OpenJDK из Oracle. Важный нюанс, который нужно учитывать при использовании этой техники.

3. Сниженная отлаживаемость

The new coding style simplifies our source code, yetcan cause headaches while debugging it.

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

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, и посмотрим, что произойдет, когда будет передан пустойString:

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.

4. Методы ВозвратNull илиOptional

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

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

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

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

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

  • Consumer - представляет собой операцию, которая принимает некоторые аргументы и не возвращает результата

  • Function - представляет функцию, которая принимает некоторые аргументы и выдает результат

  • Operator - представляет операцию с некоторыми аргументами типа и возвращает результат того же типа, что и операнды

  • Predicate - представляет собой предикат (boolean-значную функцию) некоторых аргументов

  • Supplier - представляет поставщика, который не принимает аргументов и возвращает результаты

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

  • IntConsumer

  • IntFunction

  • IntPredicate

  • IntSupplier

  • IntToDoubleFunction

  • IntToLongFunction

  • … И те же альтернативы дляLongs иDoubles

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

  • BiConsumer

  • BiPredicate

  • BinaryOperator

  • BiFunction

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

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

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

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

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

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

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

Это будет работать Однако выдачаRuntimeException противоречит цели проверяемого исключения и превращает весь код в оболочку шаблонного кода, который мы пытаемся уменьшить, используя лямбда-выражения. Одно из хакерских решений -to rely on the sneaky-throws hack.

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

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

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

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

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

8. Заключение

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

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