java8中的流式写法的一点思考

最近学习了java8实战和effective java这两本书关于流式写法的知识,在看完java8之后就在想流式写法肯定也会有它的缺点,它优化了外部迭代就肯定舍弃了什么,当学习到effective java 的时候我知道了它的缺点到底是什么。
::Stream流式表达的优缺点是什么::

  • 优点

简洁
容易进行并行计算,添加一个parallel就行
有可能是未来编程的趋势
递归运行非常快,对其有专门的优化

  • 缺点

如果不使用并行计算,代码的效率并没有普通迭代的快(大多数情况)
没有办法进行return、continue、break、对异常进行处理等操作
不容易调试

  • 相较于普通迭代的优点

无存储
函数式风格,不会修改数据源
惰性执行
代码简洁
::Stream和iterator迭代的效率比较::
这里给出两篇博客,其中测试了Stream、parallel stream、iterator的执行效率
Java 8 Stream的性能到底如何? - 某熊的全栈之路 - SegmentFault 思否
JDK8 Stream 数据流效率分析_Al_assad的博客-CSDN博客_stream 效率
在这里直接给出相应的结论:

  • 数据量小的时候(size<=1000)的时候iterator效率要比Stream高,不过都低于毫秒所以可以牺牲这一部分性能来追求Stream的可读性。
  • 在大数据量(szie>10000)时,Stream的处理效率要高一些,如果并行流使用得当效率还能更高。
  • 并行流使用的时候受cpu的影响大,当没分配到多个cpu核心时,加上引用 forkJoinPool 的开销,运行效率可能还不如普通的 Stream。
    ::如何用好Stream表达式::
  1. 注意装箱拆箱
List<Integer> list = new ArrayList<>();
for (int i = 1 ;i<10000 ;i++){
    list.add(i);
}

long s = System.currentTimeMillis();
List<Integer> collect = list.stream().map(x -> ++x).collect(Collectors.toList());
long es = System.currentTimeMillis();
System.out.println(es - s);

long l = System.currentTimeMillis();
List<Integer> result = list.stream()
        .mapToInt(x -> ++x)
        .boxed()
        .collect(Collectors.toList());
long el = System.currentTimeMillis();
System.out.println(el-l);

我将10000个数值放入list中,以引用的形式来存储,在第一段代码中直接使用map进行操作自加1,最后又装箱成为List,这里隐含了一个拆箱和装箱的步骤。
第二段代码中我将原始类型特化,将Stream 特化为IntStream省去拆箱的时间,再使用boxed方法将流转化为Stream最后使用收集为一个list,boxed函数的作用是将IntStream转化为Stream,使流可以被收集为一个List。
数据量10000级别的大小,上面两段代码在我的机器上执行速度分别为70和7毫秒左右,比值大约是10:1。

  1. 不要因为强行使用Stream表达式,而抛弃了之前的迭代方法,这样可能更难理解,有效的结合起来使用更好
    首先是effective java中第7章中第45条谨慎使用Stream中它给出了一个例子
    从词典文件中读取单词,并打印出单词数量大于用户指定的最低值的所有换位词,包含相同字母、字母顺序不同的两个词被称为换位词。比如”staple”和”petals”键都是”aelpst”,它们就是换位词。
  • 不用Stream的代码:
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);
				① computeIfAbsent是java8中map的新方法,当一个key不存在就设置一个值并返回值,如果存在就直接返回值
        }
    }
    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);
}

代码很长,但是仔细看能看懂,这时候就会想能不能用Stream进行优化

  • 大量使用Stream的代码
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的版本
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));
    }
}

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

这样就舒服了很多,使用的alphabetize的方法是和第一个图中一样的方法,正因为这个方法让Stream变的比上面的代码易读很多
①按照word的字母组成进行分组,获取映射值的流
②进行过滤,按照最小分组长度筛选出比其长的分组
③打印

  1. 其次lambda表达式中的foreach不要随意使用
    下面来看effective java中第7章第46条中的一个例子
import static java.util.stream.Collectors.*;
Map<String, Long> freq = new HashMap<>();
ArrayList<String> words = new ArrayList<String>();
words.forEach(word -> {
        freq.merge(word.toLowerCase(), 1L, Long::sum);
 });

这段代码是用来统计单词出现的次数,产生一个单词和频率的映射。看上去这么写也没问题,但是这段代码它对比普通迭代没有任何优势,它还是显式迭代,可读性反而变差了,而且比普通迭代更难维护。
我试了一下迭代版本和流式表达版本的效率差别,随机生成5000000条数据,执行代码,流式表达式Stream.foreach的执行时间是300以上,普通迭代是200以上不到300。
虽然这个测试不标准、正常情况下不会有这么大的数据量,当数据量不多的时候两者性能差别也不大,但是能看出来Stream.foreach和正常迭代相比在这种情况下没有执行速度上的优势。

import static java.util.stream.Collectors.*;
Map<String, Long> freq = new HashMap<>();
ArrayList<String> words = new ArrayList<>();
for (int i=1;i<5000000;i++){
    int random = (int)Math.random()*100;
    words.add(String.valueOf(random));
}
long st = System.currentTimeMillis();
words.forEach(word -> {
    freq.merge(word.toLowerCase(), 1L, Long::sum);
});
long et = System.currentTimeMillis();
System.out.println(et-st);
long st1 = System.currentTimeMillis();
for (String word : words) {
    freq.merge(word.toLowerCase(),1L,Long::sum);
}
long et1 = System.currentTimeMillis();
System.out.println(et1-st1);

effective java中给出了另外一种流式写法来执行相应的功能

import static java.util.stream.Collectors.*;
Map<String, Long> freq = new HashMap<>();
ArrayList<String> words = new ArrayList<String>();
freq = words
        .collect(groupingBy(String::toLowerCase, counting()));

这种写法是隐式迭代,正确的使用了lambda表达式。foreach操作应该只用于打印结果,而不是执行计算,有时候也可以将Stream计算的结果添加到之前已经存在的集合中。如果想要使用foreach循环进行操作,首先看是否可以转为Collection中的方法,Collection比foreach对流式表达更加友好,如果不行看看是否可以直接使用普通的迭代进行操作。

  1. 谨慎使用Stream并行操作
    使用并行化的流式操作的时候一定要进行完善的测试,能确保其性能比串行要好,如果对Stream进行不恰当的并行操作,一定要确保代码正确,不会产生没有预料到的问题。

  2. Collection中方法小技巧
    Collection中有很多方法,可以用其来简化很多之前的代码

  • toMap方法
Map<String, String> collect = testList.stream().collect(toMap(Object::toString, t -> t));

toMap中的两个参数第一个是映射的键,第二个是映射的值,但是如果多个Stream映射了同一个键就会报错。
解决方法:

Map<String, String> collect = testList.stream().collect(toMap(Object::toString, t -> t,(old,newVal)-> old+newVal));

使用三个参数的toMap方法,前两个参数与上面的toMap方法相同,第三个参数是如果产生键冲突如何处理,这里的处理方式是使用旧的值和新的值相加添加到映射值上。你也可以自定义,让其使用新的值进行更新或者乘除某个值等等。

  • groupingBy方法
freq = words.stream()
        .collect(groupingBy(String::toLowerCase, counting()));

这样生成的Map的值是该元素出现的次数。
正确的使用Stream必须要了解收集器,最重要的收集器就是toList,toMap,toSet,groupingBy,joining等。
::总结::

  1. 普通迭代和流式表达式要结合起来使用,有更好的可读性,使用普通迭代也不会让你的性能下降。
  2. 写一个好的流操作很重要,其中涉及到装箱拆箱以及收集器的使用,如果使用不好性能会大大降低。
  3. 谨慎使用并行流。

你可能感兴趣的:(java)