打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
带你快速看完9.8分神作《Effective Java》—— Lambda 和 Stream篇(小王工作里用的很多)

🍊 Java学习:Java从入门到精通总结

🍊 Spring系列推荐:Spring源码解析

📆 最近更新:2021年12月16日

🍊 个人简介:通信工程本硕💪、阿里新晋猿同学🌕。我的故事充满机遇、挑战与翻盘,欢迎关注作者来共饮一杯鸡汤

🍊 点赞 👍 收藏 ⭐留言 📝 都是我最大的动力!

豆瓣评分9.8的图书《Effective Java》,是当今世界顶尖高手Josh Bloch的著作,在我之前的文章里我也提到过,编程就像练武,既需要外在的武功招式(编程语言、工具、中间件等等),也需要修炼心法(设计模式、源码等等)学霸、学神OR开挂

我也始终有一个观点:看视频跟着敲代码永远只是入门,从书籍里学到了多少东西才决定了你的上限。

我个人在Java领域也已经学习了近5年,在修炼“内功”的方面也通过各种途径接触到了一些编程规约,例如阿里巴巴的泰山版规约,在此基础下读这本书的时候仍是让我受到了很大的冲激,学习到了很多约定背后的细节问题,还有一些让我欣赏此书的点是,书中对于编程规约的解释让我感到十分受用,并愿意将他们应用在我的工作中,也提醒了我要把阅读JDK源码的任务提上日程。

最后想分享一下我个人目前的看法,内功修炼不像学习一个新的工具那么简单,其主旨在于踏实,深入探索底层原理的过程很缓慢并且是艰辛的,但一旦开悟,修为一定会突破瓶颈,达到更高的境界,这远远不是我通过一两篇博客就能学到的东西。

接下来就针对此书列举一下我的收获与思考。

不过还是要吐槽一下的是翻译版属实让人一言难尽,有些地方会有误导的效果,你比如java语言里extends是继承的关键字,书本中全部翻译成了扩展 就完全不是原来的意思了。所以建议有问题的地方对照英文原版进行语义上的理解。

没有时间读原作的同学可以参考我这篇文章。


文章目录

42 Lambda优先于匿名类

自从JDK 1.1于1997年发布以来,创建函数对象的主要手段就是匿名类。下面代码是按照字符串⻓度顺序对列表进行排序,使用匿名类创建排序的比较方法:

Collections.sort(words, new Comparator<String>() {
	public int compare(String s1, String s2) {
		return Integer.compare(s1.length(), s2.length());
	}
});

匿名类适用于需要函数对象的经典面向对象设计模式,特别是策略模式Comparator接口代表一种排序的抽象策略;上面的匿名类是排序字符串的具体策略

在Java 8中,“带有单个抽象方法的接口”是特殊的,他们被称作函数式接口,Java允许利用Lambda表达式创建这些接口的实例。

Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

Lambda的类型是Comparator <String>,其参数s1,s2的类型是String,返回值类型int全都没有出现在代码里

编译器使用一个叫类型推断的过程从上下文中推断出这些类型。

如果编译器产生错误消息,无法推断出Lambda参数的类型,那就手动指定它。


如果用Lambda表达式代替Comparator的构造方法,代码会更加简练:

Collections.sort(words, comparingInt(String::length));

如果用Java 8中List接口里的sort方法,代码还可以更简洁:

words.sort(comparingInt(String::length));

Java中增加了Lambda之后,使得之前不能使用函数对象的地方现在也能用了。例如,以34条里的Operation枚举类型为例:

public enum Operation {
    PLUS("+") {
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*") {
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE("/") {
        public double apply(double x, double y) {
            return x / y;
        }
    };

    private final String symbol;

    Operation(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }

    public abstract double apply(double x, double y);
}

使用Lambda改造的话,只要给每个枚举常量的构造器传递一个实现其行为的Lambda即可:

public enum Operation {
    PLUS("+", (x, y) -> x + y),
    MINUS("-", (x, y) -> x - y),
    TIMES("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y);

    private final String symbol;
    private final DoubleBinaryOperator op;

    Operation(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

    @Override
    public String toString() {
        return symbol;
    }

    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    }
}

构造方法将Lambda存储在实例属性中,apply 方法将调用转发给Lambda,代码量更少,逻辑也更清晰。


使用Lambda需要注意的地方是:Lambda没有名称和文档;如果计算比较复杂,或者代码量超过几行,就不要用Lambda了

Lambda表达式一行是最好的,三行是极限!不能再多了

传递给枚举构造方法的参数是在静态环境中计算的。因此,枚举构造方法中的Lambda表达式不能访问枚举的实例成员。如果枚举类型具有难以理解的特殊方法,使用原先的实现方式仍是首选。


匿名类在Lambda时代并未过时,可以依据以下几点来进行选择:
1. 如果想创建抽象类的实例,可以用匿名类来完成

2. 如果一个接口有多个抽象方法,创建实例要用匿名类

3. Lambda无法获得对自身的引用
在Lambda中,关键字this指向外围实例,这通常是我们想要的


不要序列化一个 Lambda 或 匿名类实例,如果想要可序列化的函数对象,如Comparator,就使用私有静态嵌套类的实例。


43 方法引用优先于Lambda

如果方法引用看起来更简短更清晰,就用方法引用;否则还是用Lambda


Java提供了一种生成函数对象的方法,比lambda还要简洁:方法引用(method references),就是常说的::运算符


下面代码是用来保持从任意键到Integer的映射:

map.merge(key, 1, (count, incr) -> count + incr);

代码使用了Java 8中的 Map 接口中的merge方法,如果没有给定key的映射,就插入默认值(上面代码里的1);如果映射已经存在,则将函数应用于当前值和指定值,并用结果覆盖当前值。这里的函数是将现有值count递增incr


Integer 类(和所有其他包装数字基本类型)提供了一个静态方法sum,只传入这个方法的引用也行:

map.merge(key, 1, Integer::sum);

使用了方法引用的代码更简洁


但有时候Lambda会比方法引用更简洁,大多数情况是方法与lambda相同的类中,例如下面的代码发生在GoshThisClassNameIsHumongous类里:

- 方法引用

service.execute(GoshThisClassNameIsHumongous::action);

- Lambda

service.execute(() -> action());

类似的还有 Function 接口,它用一个静态工厂方法返回 id 函数 Function.identity()。如果使用等效的lambda内联代码:

x -> x

这样会更简洁


许多方法引用是指静态方法,但有4种方法没有引用静态方法:

方法引用类型范例Lambda等式
静态Integer::parseIntstr -> Integer.parseInt(str)
有限制Instant.now()::isAfterInstant then = Instant.now();
t-> then.isAfter(t)
无限制String::toLowerCasestr -> str.toLowerCase()
类构造器TreeMap<K, V>::new() -> new TreeMap<K, V>
数组构造器int[]::newlen -> new int[len]

无限制的引用经常用在流管道(Stream pipeline)中作为映射和过滤函数;构造器引用是充当工厂对象


44 优先使用标准的函数式接口

如果标准函数接口能满足要求,应该优先使用它,而不是专⻔自己创建新的函数接口


LinkedHashMap为例,可以通过重写其protected removeEldestEntry方法将此类用作缓存,每次将新的key值加入到map时都会调用该方法。以下代码重写允许map最多保存100个条目,然后在每次添加新key值时删除最老的条目:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
	return size() > 100;
}

用Lambda可以做得更好,自己定义一个函数接口如下:

@FunctionalInterface
interface EldestEntryRemovalFunction<K,V>{
	boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}

这个接口可以正常工作,但是没必要,因为java.util.function包提供了大量标准函数式接口以供使用。

许多标准函数式接口都提供了有用的默认方法。如Predicate接口提供了组合判断的方法。标准的BiPredicate<Map<K,V>, Map.Entry<K,V>> 接口应优先于自定义的EldestEntryRemovalFunction接口的使用。


EldestEntryRemovalFunction 接口使用@FunctionalInterface注解进行标注的。这个注解类型本质上与@Override一样,有三个目的:

  1. 告诉读者这个接口是针对Lambda设计的
  2. 这个接口不会进行编译,除非他只有一个抽象方法
  3. 避免后续维护人员不小心给该接口添加抽象方法

始终使用@FunctionalInterface1注解标注自己写的函数式接口


接口方法示例
UnaryOperatorT apply(T t)String::toLowerCase
BinaryOperatorT apply(T t1, T t2)BigInteger::add
Predicateboolean test(T t)Collection::isEmpty
Function<T,R>R apply(T t)Arrays::asList
SupplierT get()Instant::now
Consumervoid accept(T t)System.out::println
  • 这六个基础接口各自还有3种变体(int、long、double),例如predicate的变体IntPredicate
  • Function 接口还有9种变体,LongToIntFunctionDoubleToObjFunction
  • 这三种基础函数接口还有带两个参数的版本,BiPredicate <T,U>BiFunction <T,U,R>BiConsumer <T,U>
  • 还有BiFunction变体用于返回三个相关的基本类型:ToIntBiFunction<T,U>ToLongBiFunction<T,U>ToDoubleBiFunction <T,U>
  • Consumer接口也有带两个参数的变体版本,带一个对象和一个基本类型:ObjDoubleConsumer <T>ObjIntConsumer <T>ObjLongConsumer <T>
  • 还有一个 BooleanSupplier 接口,它是 Supplier 的一个变体,返回boolean

注意:不要用带包装类型的基础函数接口来代替基本函数接口。使用装箱基本类型进行批量操作处理,后果可能是致命的。


什么时候应该自己编写接口呢?
答案是:如果没有一个标准的函数接口能够满足需求时


Comparator <T>为例,它的结构与ToIntBiFunction <T, T>接口相同。Comparator有自己的接口有以下几个原因:

  1. 每当在API中使用时,其名称提供了良好的文档信息
  2. Comparator接口对于如何构成一个有效的实例,有着严格的条件限制
  3. 这个接口配置了大量好用的default方法,可以对Comparator进行转换和合并

如果所需要的函数接口与Comparator一样具有以下特征,就需要自己编写专用的函数接口了:

  1. 通用,并且将受益于描述性的名称
  2. 具有与其关联的严格的契约
  3. 将受益于定制的缺省方法

45 谨慎使用Stream

在Java 8中添加了Stream API,以简化串(并)行执行批量操作的任务。

Stream表示有限或无限的数据元素序列,Stream pipeline,表示对这些元素的多级计算。Stream中的元素可以来自集合、数组、文件、正则表达式模式匹配器、伪随机数生成器和其他Stream。数据可以是对象引用或基本类型(int、long、double)。

一个Stream pipeline包含一个 源Stream,几个中间操作,1个终止操作。每个中间操作都以某种方式转换Stream,比如过滤操作。终止操作会对Stream执行一个最终计算,比如返回一个List,打印所有元素等。

  • Stream pipeline是lazy的:直到调用终止操作时才会开始计算

没有终止操作的的Stream pipeline是静默的,所以终止操作千万不能忘

  • Stream API是fluent的:所有包含pipeline的调用可以链接成一个表达式

介绍完Stream之后,肯定就会有小伙伴们开始思考了,我们应该在什么时候用呢?

其实并没有任何硬性的规定,但可以从以下例子中得到启发:

例一:
读取字典中的单词,打印出单词出现次数大于某值的所有“换位词”

换位词:包含相同字母,但顺序不同的单词
如果换位词一样,这里就认为是同一个单词

public class Anagrams {
    public static void main(String[] args) throws IOException {
        File dictionary = new File(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);
        Map<String, Set<String>> groups = new HashMap<>();
        try (Scanner s = new Scanner(dictionary)) {
            while (s.hasNext()) {
                String word = s.next();
                groups.computeIfAbsent(alphabetize(word),
                        (unused) -> new TreeSet<>()).add(word);

            }
        }

        for (Set<String> group : groups.values())
            if (group.size() >= minGroupSize)
                System.out.println(group.size() + ": " + group);
    }

    private static String alphabetize(String s) {
        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
}

将每个单词插入到map中中使用了computeIfAbsent方法,computeIfAbsent 方法对 hashMap 中指定 key 的值进行重新计算,如果不存在这个 key,则添加到 hashMap 中。

语法为:

hashmap.computeIfAbsent(K key, Function remappingFunction)

参数说明:

  • key - 键
  • remappingFunction - 重新映射函数,用于重新计算value

例二:
这个例子大量使用了Stream

public class Anagrams {
public static void main(String[] args) throws IOException {
	Path dictionary = Paths.get(args[0]);
	int minGroupSize = Integer.parseInt(args[1]);
	try (Stream<String> words = Files.lines(dictionary)) {
		words.collect(
				groupingBy(word -> word.chars().sorted()
					.collect(StringBuilder::new,
					(sb, c) -> sb.append((char) c),
					StringBuilder::append).toString()))
			.values().stream()
			.filter(group -> group.size() >= minGroupSize)
			.map(group -> group.size() + ": " + group)
			.forEach(System.out::println);
		}
	}
}

如果你发现这段代码难以阅读,别担心,我也难看懂吗,在工作里面也是不提倡的,所以滥用Stream会使得程序代码难以读懂和维护


例三:
下面的代码和例二的逻辑相同,它没有过度使用Stream,代码可读性很强:

public class Anagrams {

    public static void main(String[] args) throws IOException {
        Path dictionary = Paths.get(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);

        try (Stream<String> words = Files.lines(dictionary)) {
            words.collect(groupingBy(word -> alphabetize(word)))
                    .values().stream()
                    .filter(group -> group.size() >= minGroupSize)
                    .forEach(g -> System.out.println(g.size() + ": " + g));
        }
    }

    // alphabetize method is the same as in original version
    private static String alphabetize(String s) {
        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
}

它在一个try-with-resources块中打开文件,获得一个由文件中的所有代码的Stream。Stream中的pipeline没有中间操作,终止操作是将所有单词集合到一个映射中,按照它们的换位词对单词进行分组

values().stream()

打开了一个新的Stream<List<String>>,这个Stream里的元素都是换位词,filter进行了过滤,忽略大小小于minGroupSize的所有组,最后由终结操作forEach打印剩下的同位词组。


提高Stream代码的可读性有两个要求:

  • 在没有显式类型的情况下,认真命名Lambda参数
  • 使用辅助方法(上面的alphabetize),因为pipeline缺少显式类型信息和命名临时变量

需要提醒一点,使用Stream处理char类型的数据有风险:

例四:

"Hello world!".chars().forEach(System.out::print);

发现它打印 721011081081113211911111410810033。这是因为“Hello world!”.chars()返回的Stream的元素不是char值,而是int,修改方法是加一个强制类型转换:

"Hello world!".chars().forEach(x -> System.out.print((char) x));

所以应该避免使用Stream来处理char值


综上所述,Stream适合完成下面这些工作:

  • 统一转换元素序列
  • 过滤元素序列
  • 使用单个操作组合元素序列(例如添加、连接或计算最小值)
  • 将元素序列累积到一个集合中,可能通过一些公共属性将它们分组
  • 在元素序列中搜索满足某些条件的元素

假设Card是一个不变值类,用于封装Rank和Suit,下面代码求他们的笛卡尔积:

private static List<Card> newDeck() {
	List<Card> result = new ArrayList<>();
	
	for (Suit suit : Suit.values())
		for (Rank rank : Rank.values())
			result.add(new Card(suit, rank));
	
	return result;
}

基于Stream实现的代码如下:

private static List<Card> newDeck() {
	return Stream.of(Suit.values())
		.flatMap(suit ->
			Stream.of(Rank.values())
				.map(rank -> new Card(suit, rank)))
		.collect(toList());
}

其中用到了 flatMap 方法:这个操作将一个Stream中的每个元素都映射到一个Stream中,然后将这些新的Stream全部合并到一个Stream(或展平它们)。

newDeck的两个版本中到底哪一个更好?这就是仁者见仁智者见智的问题了,取决于你的个人喜好 : )


46 优先选择Stream中无副作用的函数

Stream最重要的是把将计算结构构造成一系列变型,其中每个阶段的结果尽可能接近前一阶段结果的纯函数(pure function)。纯函数的结果仅取决于其输入的函数:它不依赖于任何可变状态,也不更新任何状态。为了实现这一点,Stream操作的任何中间操作和终结操作都应该是没有副作用的。


如下代码,将单词出现的频率打印出来:

Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
	words.forEach(word -> {
		freq.merge(word.toLowerCase(), 1L, Long::sum);
	});
}

实际上这根本不是Stream代码,只不过是伪装成Stream的迭代代码。可读性也差,forEach里面逻辑太多了,正确的应该是这么写:

Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
	freq = words
		.collect(groupingBy(String::toLowerCase, counting()));
}

所以forEach 操作应仅用于报告Stream计算的结果,而不是进行计算


对于初学者来说,可以忽略Collector接口,将其看做是黑盒对象即可,这个黑盒可以将Stream的元素合并到单个集合里。

有三个这样的CollectortoList()toSet()toCollection(collectionFactory)。基于此,我们可以从频率表中提取排名前10的单词列表:

List<String> topTen = freq.keySet().stream()
	 .sorted(comparing(freq::get).reversed())
	 .limit(10)
	 .collect(toList());

注意上述代码用的是.collect(toList()),而不是.collect(Collectors.toList()),这是因为静态导入了Collectors所有成员,也是一种提高代码可读性的手段


接下来介绍Collector中比较重要的三个方法:

1. toMap(keyMapper、valueMapper)

它接受两个函数:一个将Stream元素映射到键,另一个将它映射到值。例如下面将枚举的字符串形式映射到枚举本身:

private static final Map<String, Operation> stringToEnum =
	Stream.of(values()).collect(toMap(Object::toString, e -> e));

还有带三个参数的toMap,假设有一个Stream代表不同艺术家(artists)的专辑(albums),可以得到每个歌唱家最畅销的那一张专辑,用map来存储:

Map<Artist, Album> topHits = albums.collect(toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));

比较器使用静态工厂方法maxBy,它是从BinaryOperator import进来的。此方法将Comparator<T> 转换为BinaryOperator<T>,用于计算指定比较器产生的最大值。


对于`toMap`,阿里巴巴开发规约也专门做了要求:




2. groupingBy

该方法返回Collector,基于分类函数(classifier function)将元素分类,返回值是一个map,value是存储了每个类别的所有元素的List

words.collect(groupingBy(word -> alphabetize(word)))

上面代码就返回的是collect,key是alphabetize(word),value是word列表


还有传入两个参数的groupingBy,传入counting()作为下游收集器,这样会生成一个映射,将每个类别与该类别中的元素数量关联起来

Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));

3. groupingByConcurrent

该方法提供了提供了groupingBy所有三种重载的变体,支持并发安全性,最终返回的也是ConcurrentHashMap实例


4. joining

它仅对 CharSequence 实例(如字符串)的Stream进行操作。可以传入一个参数CharSequence delimiter作为分界符。如果传入一个逗号作为分隔符,Collector就会返回一个用逗号隔开的字符串


47 Stream要优先用Collection作为返回类型

先说结论:

在编写会返回一系列元素的方法时,某些程序员可能希望将它们作为 Stream 处理,其他程序员可能希望使用迭代方式(Iterable)。

如何做到兼顾呢?

  • 如果可以返回集合,就返回集合
  • 如果集合中已经有元素,或者元素数量不多,就返回一个标准集合,比如 ArrayList
  • 否则,就需要自定义集合,如下文将提到的幂集
  • 如果不能返回集合,则返回Stream或Iterable

如果想要用for-each循环遍历返回序列的话,必须将方法引用转换成合适的Iterable类型:

for (ProcessHandle ph : (Iterable<ProcessHandle>)ProcessHandle.allProcesses()::iterator)

但是上面的代码在实际使用时过于杂乱、不清晰。解决方案就是写一个适配器:

public static <E> Iterable<E> iterableOf(Stream<E> stream) {
	return stream::iterator;
}

有了这个适配器,就可以使用 for-each 语句迭代任何Sream了:

for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
	// Process the process
}

想要利用Stream pipeline处理序列的程序员,如果API只提供了Iterable的话,我们需要手动将IterableStream

public static <E> Stream<E> streamOf(Iterable<E> iterable) {
	return StreamSupport.stream(iterable.spliterator(), false);
}

Collection接口是Iterable的子类型,有一个stream方法,因此提供了迭代和stream访问。所以Collection或适当的子类型通常是公共序列返回方法的最佳返回类型


如果返回的序列很大,可以考虑实现一个专用的集合。例如想要返回一个指定集合的幂集

例如{a,b,c} 的幂集为 {{},{a},{b},{c},{a,b},{a,c},{b,c},{a,b, c}}

技巧是,使用幂集中每个元素的索引作为位向量(bit vector),在索引中的第 n 位,表示源集合中是否存在第 n 位元素。

public class PowerSet {
	public static final <E> Collection<Set<E>> of(Set<E> s) {
		List<E> src = new ArrayList<>(s);
		
		if (src.size() > 30)
			throw new IllegalArgumentException("Set too big " + s);
		
		return new AbstractList<Set<E>>() {
			@Override
			public int size() {
				return 1 << src.size(); // 2 to the power srcSize
			}
			
			@Override
			public boolean contains(Object o) {
				return o instanceof Set && src.containsAll((Set)o);
			}
			
			@Override
			public Set<E> get(int index) {
				Set<E> result = new HashSet<>();
				for (int i = 0; index != 0; i++, index >>= 1)
					if ((index & 1) == 1)
						result.add(src.get(i));
				return result;
			}
		};
	}
}

为了在 AbstractCollection 上编写 Collection 实现,除了 Iterable 所需的方法之外,只需要实现两种方法:containssize


48 谨慎使用Stream并行

下面的代码逻辑是求所有2 ^ p - 1数字里为素数的数字

public static void main(String[] args) {
	primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
		.filter(mersenne -> mersenne.isProbablePrime(50))
		.limit(20)
		.forEach(System.out::println);
 }

static Stream<BigInteger> primes() {
	return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}

TWO.pow(p.intValueExact()).subtract(ONE))表示2 ^ p - 1
mersenne.isProbablePrime(50)表示mersenne是否可能是素数,传入的数字表示可能性阈值

如果添加一个parallel()的话,原意是想要提速,但实际结果是根本不打印任何内容,CPU使用率却很高。

原因是:Stream不知道如何去并行这个pipeline。如果源头是Stream.iterate,或者使用了中间操作limit,并行则不太可能提高其性能

默认的并行策略在处理limit的不可预知性时,每查找到一个素数时,所花费的时间都等于计算所有之前元素总和的时间,所以不要任意地并行Stream pipeline


并行能带来性能收益的应用场景在于ArrayListHashMapHashSetConcurrentHashMap、数组、int 范围和long 范围

这些数据结构有共同的特点:
1. 可以精确、轻松地分割成任意大小的子范围
Stream类库里用来执行分工任务的是spliterator,由 spliterator 方法在StreamIterable 上返回。

2. 在顺序处理时提供了较好的引用局部性(localityof reference)

时间局部性是指,被引用一次的储存器位置,在接下来的时间会经常被引用
空间局部性是指,被引用一次的储存器位置,在接下来的时间,他旁边的储存位置也会被引用

引用局部性非常重要:没有它,线程会出现闲置,需要等待数据从内存转移到处理器的缓存中。


并行pipeline效率会受限的场景: 终止操作里做了复杂的运算

并行的最佳终止操作是用Stream的reduce方法,将所有pipeline产生的元素合并到一起,或者事先打包minmaxcountsum这类方法。短路操作anyMatchallMatchnoneMatch 也可以支持并行。由Stream的collect 方法执行的操作,不适合并行性,因为组合集合的开销非常大。

如果是编写自己的Stream、Iterable 或Collection,并且希望获得良好的并行性能,则必须重写 spliterator 方法并广泛测试性能。编写高质量的 spliterator 很困难,不建议这么做!


并行Stream不仅可能降低性能,它会导致不正确的结果和不可预知的行为


对于一个高效的可拆分的源Stream、一个可并行化或简单的终止操作、互不干扰干扰的函数对象,由于他们本身就性能很好地,所以也无法从并行化中获得良好的加速效果。

切记: 并行化Stream是严格的性能优化,必须在更改前后进行测试性能

通常,程序中的所有并行Stream pipeline都在公共fork-join池中运行。只要有一个pipeline不正常,都会损害到系统里其他不相关部分的性能。


当然,在某些条件下,给Stream pipeline添加parallel调用,确实可以基于CPU核实现性能倍增,典型的就是机器学习和数据处理领域。

比如下面这个数学计算的代码就能通过并行来提速:

static long pi(long n) {
	return LongStream.rangeClosed(2, n)
		.parallel()
		.mapToObj(BigInteger::valueOf)
		.filter(i -> i.isProbablePrime(50))
		.count();
}
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
Java 8新特性终极指南
JDK各个版本的新特性jdk1.5-jdk1.8
java8的新特性以及用法简介,再不学习,代码都看不懂了
JAVA学习线路:day13-JDK8新特性
【读书笔记】《写给大忙人看的Java SE 8》
Lambda表达式
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服