Challenges in Java 8 – Java 8中的挑战

最后修改: 2017年 10月 31日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Java 8 introduced some new features, which revolved mostly around the use of lambda expressions. In this quick article, we’re going to take a look at downsides of some of them.

Java 8引入了一些新功能,这些功能主要围绕着lambda表达式的使用。在这篇短文中,我们将看看其中一些的缺点。

And, while this is not a full list, it’s a subjective collection of the most common and popular complaints regarding new features in Java 8.

而且,虽然这不是一个完整的列表,但它是关于Java 8中新功能的最常见和最受欢迎的投诉的主观集合。

2. Java 8 Stream and Thread Pool

2.Java 8的流和线程池

First of all, Parallel Streams are meant to make easy parallel processing of sequences possible, and that works quite OK for simple scenarios.

首先,并行流的目的是使序列的并行处理成为可能,这对简单的场景来说是很好的。

The Stream uses the default, common ForkJoinPool – splits sequences into smaller chunks and performs operations using multiple threads.

Stream使用默认的、常见的ForkJoinPool–将序列分割成较小的块,并使用多线程执行操作。

However, there is a catch. There’s no good way to specify which ForkJoinPool to use and therefore, if one of the threads gets stuck all the other ones, using the shared pool, will have to wait for the long-running tasks to complete.

然而,有一个问题。没有好的方法来指定使用哪个ForkJoinPool,因此,如果其中一个线程被卡住,所有其他使用共享池的线程将不得不等待长期运行的任务的完成。

Fortunately, there is a workaround for that:

幸运的是,有一个解决这个问题的办法。

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

This will create a new, separate ForkJoinPool and all tasks generated by the parallel stream will use the specified pool and not in the shared, default one.

这将创建一个新的、独立的ForkJoinPool,所有由并行流产生的任务将使用指定的池,而不是共享的、默认的池。

It’s worth noting that there is another potential catch: “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”, according to Stuart Marks – Java and OpenJDK developer from Oracle. An important nuance to keep in mind when using this technique.

值得注意的是,还有一个潜在的陷阱。“这种将任务提交给分叉连接池,以运行该池中的并行流的技术是一种实施’技巧’,并不保证能够成功”,根据Stuart Marks–来自Oracle的Java和OpenJDK开发者的说法。在使用这种技术时,要记住一个重要的细微差别。

3. Decreased Debuggability

3.可调试性降低

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

新的编码风格简化了我们的源代码,然而 在调试时可能会引起头痛

First of all, let’s look at this simple example:

首先,让我们看一下这个简单的例子。

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));
}

This is a standard imperative Java code that’s self-explanatory.

这是一个标准的命令式Java代码,不言而喻。

If we pass empty String as an input – as a result – the code will throw an exception, and in debug console, we can see:

如果我们传递空的String作为输入–结果–代码将抛出一个异常,在调试控制台,我们可以看到。

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

Now, let’s re-write the same code using Stream API and see what happens when an empty String gets passed:

现在,让我们使用Stream API重写同样的代码,看看当一个空的String被传递时会发生什么。

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

The call stack will look like:

调用堆栈将看起来像。

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)

That’s the price we pay for leveraging multiple abstraction layers in our code. However, IDEs already developed solid tools for debugging Java Streams.

这是我们为在代码中利用多个抽象层所付出的代价。然而,IDE已经为调试Java Streams开发了可靠的工具。

4. Methods Returning Null or Optional

4.返回NullOptional的方法

Optional was introduced in Java 8 to provide a type-safe way of expressing optionality.

Optional是在Java 8中引入的,以提供一种类型安全的方式来表达可选性。

Optional, indicates explicitly that the return value may be not present. Hence, calling a method may return a value, and Optional is used to wrap that value inside – which turned out to be handy.

Optional,明确指出返回值可能不存在。因此,调用一个方法可能会返回一个值,而Optional被用来把这个值包在里面–结果是很方便。

Unfortunately, because of the Java backward compatibility, we sometimes ended up with Java APIs mixing two different conventions. In the same class, we can find methods returning nulls as well as methods returning Optionals.

不幸的是,由于Java的向后兼容性,我们有时会发现Java APIs混合了两种不同的约定。在同一个类中,我们可以找到返回空值的方法以及返回Optionals的方法。

5. Too Many Functional Interfaces

5.太多的功能接口

In the java.util.function package, we have a collection of target types for lambda expressions. We can distinguish and group them as:

java.util.function包中,我们有一个用于lambda表达式的目标类型集合。我们可以对它们进行区分和分组。

  • Consumer – represents an operation that takes some arguments and returns no result
  • Function – represents a function that takes some arguments and produces a result
  • Operator – represents an operation on some type arguments and returns a result of the same type as the operands
  • Predicate – represents a predicate (boolean-valued function) of some arguments
  • Supplier – represents a supplier that takes no arguments and returns results

Additionally, we’ve got additional types for working with primitives:

此外,我们还有额外的类型用于处理基元。

  • IntConsumer
  • IntFunction
  • IntPredicate
  • IntSupplier
  • IntToDoubleFunction
  • IntToLongFunction
  • … and same alternatives for Longs and Doubles

Furthermore, special types for functions with the arity of 2:

此外,还有一些特殊类型的函数,其算数为2。

  • BiConsumer
  • BiPredicate
  • BinaryOperator
  • BiFunction

As a result, the whole package contains 44 functional types, which can certainly start being confusing.

因此,整个软件包包含44种功能类型,这当然会让人开始感到困惑。

6. Checked Exceptions and Lambda Expressions

6.检查的异常和Lambda表达式

Checked exceptions have been a problematic and controversial issue before Java 8 already. Since the arrival of Java 8, the new issue arose.

在Java 8之前,检查异常已经是一个有问题的、有争议的问题了。自从Java 8到来后,新的问题出现了。

Checked exceptions must be either caught immediately or declared. Since java.util.function functional interfaces do not declare throwing exceptions, code that throws checked exception will fail during compilation:

被检查的异常必须被立即捕获或声明。由于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));

One way to overcome this problem is to wrap checked exception in a try-catch block and rethrow RuntimeException:

克服这个问题的一个方法是将检查过的异常包裹在一个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);
    }
});

This will work. However, throwing RuntimeException contradicts the purpose of checked exception and makes the whole code wrapped with boilerplate code, which we’re trying to reduce by leveraging lambda expressions. One of the hacky solutions is to rely on the sneaky-throws hack.

这就可以了。然而,抛出RuntimeException与检查异常的目的相矛盾,并使整个代码被模板代码所包裹,而我们正试图通过利用lambda表达式来减少这种情况。其中一个黑客解决方案是依靠sneaky-throws hack.

Another solution is to write a Consumer Functional Interface – that can throw an exception:

另一个解决方案是编写一个消费者功能接口–它可以抛出一个异常。

@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);
        }
    };
}

Unfortunately, we’re still wrapping the checked exception in a runtime exception.

不幸的是,我们还是把检查过的异常包裹在一个运行时异常中。

Finally, for an in-depth solution and explanation of the problem, we can explore the following deep-dive: Exceptions in Java 8 Lambda Expressions.

最后,为了深入解决和解释这个问题,我们可以探究以下的深层问题。Java 8 Lambda Expressions中的异常

8. Conclusion

8 结语

In this quick write-up, we discussed some of the downsides of Java 8.

在这篇快速的文章中,我们讨论了Java 8的一些弊端。

While some of them were deliberate design choices made by Java language architects and in many cases there is a workaround or alternative solution; we do need to be aware of their possible problems and limitations.

虽然其中一些是Java语言架构师故意做出的设计选择,而且在许多情况下有一个变通或替代解决方案;但我们确实需要意识到它们可能存在的问题和限制。