Java 8中的挑战
1. 概述
Java 8 引入了一些新特性,这些特性主要围绕 lambda 表达式的使用。在这篇快速文章中,我们将看看其中一些的缺点。 而且,虽然这不是一个完整的列表,但它是对 Java 8 中的新特性最常见和最流行的抱怨的主观集合。
2. Java 8 流和线程池
首先,并行流旨在使序列的简单并行处理成为可能,并且对于简单的场景来说效果很好。 Stream 使用默认的通用*ForkJoinPool * – 将序列拆分为更小的块并使用多个线程执行操作。 但是,有一个问题。没有很好的方法来指定使用哪个ForkJoinPool,因此,如果一个线程卡住了所有其他线程,使用共享池,将不得不等待长时间运行的任务完成。
幸运的是,有一个解决方法:
ForkJoinPool forkJoinPool = new ForkJoinPool(2);
forkJoinPool.submit(() -> /*some parallel stream pipeline */)
.get();
这将创建一个新的、单独的ForkJoinPool并且并行流生成的所有任务都将使用指定的池,而不是共享的默认池。 值得注意的是,还有另一个潜在的问题:“这种将任务提交到 fork-join 池中以在该池中运行并行流的技术是一种实现‘技巧’,并且不能保证工作”,Stuart Marks 说– 来自 Oracle 的 Java 和 OpenJDK 开发人员。使用此技术时要牢记一个重要的细微差别。
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 重写相同的代码,看看当一个空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的方法
Java 8 中引入了Optional 以提供一种表示可选性的类型安全的方式。 Optional,明确表示返回值可能不存在。因此,调用一个方法可能会返回一个值,而Optional用于将这个值包装在里面——结果证明这很方便。 不幸的是,由于 Java 的向后兼容性,我们有时会发现 Java API 混合了两种不同的约定。在同一个类中,我们可以找到返回 null 的方法以及返回Optionals 的方法。
5. 太多的功能接口
在*java.util.function *包中,我们有一组 lambda 表达式的目标类型。我们可以将它们区分和分组为:
- Consumer——代表一个接受一些参数并且不返回结果的操作
- Function – 表示一个接受一些参数并产生结果的函数
- Operator——表示对某些类型参数的操作,并返回与操作数相同类型的结果
- Predicate – 表示一些参数的谓词(布尔值函数)
- Supplier——代表不带参数并返回结果的供应商
此外,我们还有其他类型用于处理原语:
- IntConsumer
- IntFunction
- IntPredicate
- IntSupplier
- IntToDoubleFunction
- IntToLongFunction
- ……以及Longs和Doubles的相同选择
此外,arity 为 2 的函数的特殊类型:
- BiConsumer
- BiPredicate
- BinaryOperator
- BiFunction
结果,整个包包含 44 种功能类型,这肯定会让人感到困惑。
6. 检查异常和 Lambda 表达式
在 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与检查异常的目的相矛盾,并使整个代码都被样板代码包裹,我们试图通过利用 lambda 表达式来减少这种情况。hacky 解决方案之一是依靠偷偷摸摸的 throws hack 。 另一种解决方案是编写一个消费者功能接口——它可以抛出异常:
@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 表达式中的异常 。