Java Stream详解!深度解析Lambda表达式和Stream表达式的使用原理

Lambda表达式

  • JVM内部是通过invokedynamic指令来实现Lambda表达式的
  • Lambda中允许将一个函数作为方法的参数,即函数作为参数传递进方法中
  • 使用Lambda表达式可以使代码更加简洁

变量作用域

  • Lambda表达式只能引用标记了final的外层局部变量.即不能在Lambda表达式内部修改定义在作用域外的局部变量,否则会导致报错
  • Lambda表达式中可以直接访问外层的局部变量
  • Lambda表达式中外层局部变量可以不用声明为final, 但是必须不可被后面的代码修改,即隐性地具有final的语义
  • Lambda表达式中不允许声明一个与外层局部变量同名的参数或者局部变量

使用示例

匿名内部类

  • 匿名内部类: 匿名内部类仍然是一个类,不需要指定类名,编译器会自动为该类取名

    • Java中的匿名内部类:
    public class MainAnonymousClass {
      public static void main(String[] args) {
          new Thread(new Runnable(){
              @Override
              public void run(){
                  System.out.println("Anonymous Class Thread run()");
              }
          }).start();;
      }
    }
    • 使用Lambda表达式实现匿名内部类:
    public class MainLambda {
      public static void main(String[] args) {
          new Thread(
                  () -> System.out.println("Lambda Thread run()")
              ).start();;
      }
    }

带参函数

  • 带参函数的简写:
List<String> list = Arrays.asList("I", "love", "you", "too");
Collections.sort(list, new Comparator<String>() { // 接口名
    @Override
    public int compare(String s1, String s2) { // 方法名
        if(s1 == null)
            return -1;
        if(s2 == null)
            return 1;
        return s1.length() - s2.length();             
    }
});
  • 上述代码通过内部类重载了Comparator接口的compare() 方法来实现比较逻辑. 采用Lambda表达式可简写如下:
List<String> list = Arrays.asList("I", "love", "you", "too");
Collections.sort(list, (s1, s2) -> { // 省略参数表类型
    if (s1 == null)
        return -1;
    if (s2 == null)
        return 1;
    return s1.length() - s2.length();
});
  • 上述代码根内部类的作用一样
  • 除了省略了接口名和方法名,代码中的参数类型也可以省略
  • 因为javac类型推断机制,编译器能够根据上下文信息推断出参数的类型

Collection

forEach

  • 增强型for循环:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(String str : list) {
    if (str.length() > 3)
        System.out.println(str);
}
  • 使用forEach() 方法结合匿名内部类实现:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach(new Consumer<String>(){
    @Override
    public void accept(String str) {
        if (str.length() > 3) {
            System.out.println(str);
        }
    }
});
  • 使用Lambda表达式实现如下:
// 使用forEach()结合Lambda表达式迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach(str -> {
    if (str.length() > 3) {
        Systemm.out.println(str);
    }        
});

上述代码给forEach() 方法传入一个Lambda表达式,不需要知道accept() 方法,也不需要知道Consumer接口,类型推导已经完成了这些

removeIf

  • 该方法签名: boolean removeIf(Predicate<? super E> filter);

    • 删除容器中所有满足filter指定条件的元素

      • Predicate是一个函数接口,里面有一个待实现的方法boolean test(T t)
  • 如果需要在迭代过程中对容器进行删除操作必须使用迭代器, 否则会抛出ConcurrentModificationException.
  • 使用迭代器删除列表元素:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().length > 3) {
        it.remove();
    }
}
  • 使用removeIf() 方法结合匿名内部类实现:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(new Predicate<String>(){
    @Override
    public boolean test(String str) {
        return str.length() > 3;
    }
});
  • 使用removeIf结合Lambda表达式实现:
Array<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(str -> str.length() > 3);

使用Lambda表达式不需要记忆Predicate接口名,也不需要记忆test() 方法名,只需要此处需要一个返回布尔类型的Lambda表达式

replaceAll

  • 该方法签名: void replaceAll(UnaryOperator operator);

    • 对每个元素执行operator指定的操作,并用操作结果来替换原来的元素

      • UnaryOperator是一个函数接口,里面有待实现的方法T apply(T t)
  • 使用下标实现元素替换:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for (int i = 0; i < list.size(); i ++) {
    String str = list.get(i)
    if (str.length() > 3) {
        list.set(i, str.toUpperCase());
    }
}
  • 使用replaceAll结合匿名内部类实现:
ArrayList<String> list =new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(new UnaryOperator<>(String){
    @Override
    public String apply(String str) {
        if (str.length() > 3) {
            return str.toUpperCase();
        }
        return str;
    }
});

代码调用replaceAll() 方法,并使用匿名内部类实现UnaryOperator接口

  • 使用Lambda表达式实现:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(str -> {
    if (str.length > 3) {
        return str.toUpperCase();
    }
    return str;
});

sort

  • 该方法定义在List接口中,方法签名: void sort(Comparator<? super E> c);

    • 根据c指定的比较规则对容器进行排序

      • Comparator接口中需要实现接口int compare(T o1, T o2)
  • 使用Collectionssort() 方法:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String str1, String str2) {
        return str1.length() - str2.length();
    }
});
  • 直接使用List.sort() 方法,结合Lambda表达式:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.sort((str1, str2) -> str1.length() - str2.length());

spliterator

  • 该方法签名: Spliterator spliterator();

    • Spliterator既可以像Iterator那样逐个迭代,也可以批量迭代,批量迭代可以降低迭代的开销
    • Spliterator是可拆分的,一个Spliterator可以通过调用Spliterator trySplit() 方法来尝试分成两个.一个是this, 一个是新返回的元素.这两个迭代器代表的元素没有重叠
    • 可通过多次调用Spliterator.trySplit() 方法来分解负载,以便于多线程处理

stream和parallStream

  • Stream()parallStream() 分别返回该容器的Stream视图表示
  • parallStream() 返回并行的Stream
  • StreamJava函数式编程的核心类

Map

forEach

  • 该方法签名: void forEach(BiConsumer<? super K,? super V> action);

    • Map中的每个映射执行action操作

      • BiConsumer是一个函数接口,里面有一个待实现方法 void accept(T t, U u);
  • 使用Java 7之前的方式输出Map中所有的对应关系:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
    system.out.println(entry.getKey() + "=" + entry.getValue());
}
  • 使用MapforEach() 方法,结合匿名内部类:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach(new BiConsumer<Integer, String>() {
    @Override
    public void accept(Integer k, String v) {
        System.out.println(k + "=" + v);
    }
});
  • 使用Lambda表达式:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach((k, v) -> System.out.println(k + "=" + v));

getOrDefault

  • 该方法签名: V getOrDefault(Object key, V defaultValue);

    • 按照给定的key查询Map中对应的value, 如果没有找到则返回defaultValue
  • 查询Map中指定键所对应的值,如果不存在则返回NoValue:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
System.out.println(map.getOrDefault(4,"NoValue"));

putIfAbsent

  • 该方法签名: V putIfAbsent(K key, V value);

    • 只有在不存在key值的映射或映射值为null时,才将value指定的值放入到Map中,否则不对Map做修改
    • 该方法将判断和赋值合二为一,使用起来更加方便

remove

  • 该方法签名: remove(Object key);

    • 根据指定的key值删除Map中映射关系
  • 该方法签名: remove(Object key, Object value);

    • 只有在当前Mapkey正好映射到value时才删除该映射

replace

  • 该方法签名: replace(K key, V value);

    • 只有在当前Mapkey的映射存在时才用value去替换原来的值
  • 该方法签名: replace(K key, V oldValue, V newValue);

    • 只有在当前Mapkey的映射存在且等于oldValue时,才用newValue去替换原来的值,否则不做任何操作

replaceAll

  • 该方法签名: replaceAll(BiFunction<? super K, ? super V, ? extends V> function);

    • Map中的每个映射执行function操作,并用function的执行结果替换原来的value
    • 其中BiFunction是一个函数接口,里面有一个待实现的方法R apply(T t, U u)
  • 使用Java 7以前的方式将Map中的映射关系的单词都转换成大写:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
    entry.setValue(entry.getValue().toUpperCase());
}
  • 使用replaceAll方法结合匿名内部类:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll(new BiFunction<Integer, String, String>(){
    @Override
    public String apply(Integer k, String v) {
        return v.toUpperCase();
    }
});
  • 使用Lambda表达式实现:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll(<k, v> -> v.toUpperCase());

merge

  • 该方法签名: merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction);

    • 如果Map中的key对应的映射不存在或者为null, 则将value, value不可能为null关联到key
    • 否则执行remappingFunction, 如果执行结果非null, 则用该结果与key关联,否则在Map中删除key的映射
    • 其中BiFunction是一个函数接口,里面有一个待实现方法R apply(T t, U u)
  • merge()方法语义复杂,但使用的方式明确,经典的使用场景: 将新的错误信息拼接到原来的信息上:
map.merge(key, newMsg, (v1, v2) -> v1 + v2);

compute

  • 该方法签名: compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction);

    • remappingFunction计算的结果关联到key上,如果计算结果为null, 则在Map中删除key的映射
  • 使用compute实现将新的错误信息拼接到原来的信息上:
map.compute(key, (k, v) -> v == null ? newMsg : v.concat(newMsg));

computeIfAbsent

  • 该方法签名: V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction);

    • 只有在当前Map中不存在key值的映射或映射值为null时,才调用mappingFunction, 并在mappingFunction执行结果非null时,将结果跟key关联
    • Function是一个函数接口,里面有待实现方法R apply(T t)
  • computeIfAbsent() 常用来对Map的某个key值建立初始化映射.比如在实现一个多值映射时 ,Map的定义可能是Map< K, Set< V > >, 要向Map中插入新值:
Map<Integer, Set<String>> map = new HashMap<>();
if (map.containsKey(1)) {
    map.get(1).add("one");
} else {
    Set<String> valueSet = new HashSet<String>();
    valueSet.add("one");
    map.put(1, valueSet);
}
  • 使用Lambda表达式实现:
Map<Integer, Set<String>> map = new HashMap<>();
map.computeIfAbsent(1, v -> new HashSet<String>()).add("one");

computeIfPresent

  • 该方法签名: V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction);

    • 作用与computeIfAbsent() 相反
    • 只有当前Map中存在key值的映射且非null时,才调用remappingFunction, 如果remappingFunction执行结果为null, 则删除key的映射,否则使用该结果替换key原来的映射
  • Java7之前的等效代码:
if (map.get(key) != null) {
    V oldValue = map.get(key);
    V newValue = remappingFunction.apply(key, oldValue);
    if (newValue !=null) {
        map.put(key, newValue);
    } else {
        map.remove(key);
    }
    return newValue;
}
return null;

Streams API

  • stream:

    • Java函数式编程主角
    • stream不是某种数据结构,只是一种数据源视图
    • 这里的数据源可以是:

      • 数组
      • Java容器
      • I/O channel
  • stream是一个数据源视图,需要调用对应的工具方法创建一个stream:

    • 调用Collection.stream() 方法
    • 调用Collection.parallelStream() 方法
    • 调用Arrays.stream(T[] array) 方法
  • stream接口继承关系:

Java Stream详解!深度解析Lambda表达式和Stream表达式的使用原理

  • 图中4stream接口继承自BaseStream:

    • IntStream,LongStream,DoubleStream对应三种基本类型int, long, double. 不是对应相应的包装类型
    • Stream对应所有剩余类型的stream视图
  • 为不同的数据类型设置不同的stream接口:

    • 提高性能
    • 增加特定接口函数
  • 尽管stream是容器调用Collection.stream()方法得到的. stream和collections有以下不同点:

    • 无存储: stream不是一种数据结构,只是数据源的一个视图.数据源可以是数组,Java容器或I/O channel
    • 函数式编程:stream的修改都不会修改背后的数据源:比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含过滤元素的新stream
    • 惰式执行: stream上的操作不会立即执行,只有等到真正需要stream执行的结果时才会执行
    • 可消费性: stream只能被消费一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成
  • 对stream的操作分为两类:

    • 中间操作: intermediate operations. 中间操作总会惰式执行. 调用中间操作只会生成一个标记了该操作的新stream
    • 结束操作: terminal operations. 结束操作会触发实际运算. 计算发生时,会把中间积攒的操作以pipeline的方式执行,这样可以减少迭代操作.计算完成之后stream就会失效
  • Stream接口常用方法:

    • 中间操作:

      • concat()
      • distinct()
      • filter()
      • flatMap()
      • limit()
      • map()
      • peek()
      • skip()
      • sorted()
      • parallel()
      • sequential()
      • unordered()
    • 结束操作:

      • allMatch()
      • anyMatch()
      • collect()
      • count()
      • findAny()
      • findFirst()
      • forEach()
      • forEachOrdered()
      • max()
      • min()
      • noneMatch()
      • reduce()
      • toArray()
  • 区分中间操作和结束操作就是看方法的返回值:

    • 返回值为stream的大都是中间操作
    • 否则是结束操作
  • Stream方法使用:

    • stream与函数接口关系非常紧密,没有函数接口stream就无法操作

      • 函数接口是指内部只有一个抽象方法的接口
      • 函数接口出现的地方都可以使用Lambda表达式

forEach

  • 该方法签名: void forEach(Consumer<? super E> action);

    • 对容器中的每个元素执行action指定的操作,即对元素进行遍历
/* 
 * 使用Stream.forEach()进行迭代
 */
 Stream<String> stream = Stream.of("I", "love", "you", "too");
 stream.forEach(str -> System.out.println(str)); 
  • 由于forEach() 是结束方法,所以上述方法会立即执行,输出所有字符串

filter

Java Stream详解!深度解析Lambda表达式和Stream表达式的使用原理

  • 该函数原型: Stream< T > filter(Predicate<? super T> predicate);

    • 返回一个只包含满足predicate条件元素的stream
/*
 * 保留长度等于3的字符串
 */
 Stream<String> stream = Stream.of("I", "love", "you", "too");
 stream.filter(str -> str.length() == 3)
        .forEach(str -> System.out.println(str));
  • 输出长度等于3的字符串you和too
  • 由于filter() 是个中间操作, 如果只调用filter() 不会有实际计算,因此不会输出任何信息

distinct

Java Stream详解!深度解析Lambda表达式和Stream表达式的使用原理

  • 该函数原型: Stream< T > distinct();

    • 返回一个去除重复元素之后的Stream
Stream<String> stream = Stream.of("I", "love", "you", "too", "too");
stream.distinct()
      .forEach(str -> System.out.println(str));
  • 输出去掉一个too之后的其余字符串

sorted

  • 排序函数有两个:

    • 自然顺序排序: Stream< T > sorted();
    • 使用自定义比较器排序: Stream < T > sorted(Comparator<? super T> comparator);
Stream<String> stream = Stream.of("I", "love", "you", "too");
stream.sorted((str1, str2) -> str1.length() - str2.length())
      .forEach(str -> System.out.println(str));
  • 输出按照长度升序排序后的字符串

map

Java Stream详解!深度解析Lambda表达式和Stream表达式的使用原理

  • 该函数原型: < R > Stream< R > map(Function<? super T, ? extends R> mapper);

    • 返回一个对当前所有元素执行mapper之后的结果组成的Stream
    • 就是对每个元素按照某种操作进行转换 ,转换前后Stream中元素的个数不会改变,但是元素的类型取决于转换之后的类型
Stream<String> stream = Stream.of("I", "love", "you", "too");
stream.map(str -> str.toUpperCase())
      .forEach(str -> System.out.println(str));
  • 输出原字符串的大写形式

flatMap

Java Stream详解!深度解析Lambda表达式和Stream表达式的使用原理

  • 该函数原型: < R > Stream< R > flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

    • 对每一个元素执行mapper的指定操作,并用所有mapper返回的Stream中的元素作为一个新的Stream作为最终返回结果
    • 就相当于将原Stream中的所有元素都"摊平"之后组成的新Stream. 转换前后元素的个数和类型都可能会改变
Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3,4,5));
stream.flatMap(list -> list.stream())
      .forEach(i -> System.out.println(i)); 
  • 原来的stream中有两个元素,分别是两个List< Integer >, 执行flatMap() 之后,将每一个List都"摊平"成一个个数字,所以会产生一个由5个数字组成的Stream. 所以最终将输出1~5这5个数字

Stream API 高级

  • 归约操作: reduction operation

    • 又称作折叠操作fold
    • 通过某个连接动作将所有元素汇总成一个汇总结果的过程
    • 元素求和,求最大值最小值,求元素总个数,将所有元素转换成一个列表或集合,都属于归约操作
  • Stream类库中两个通用的规约操作:

    • reduce()
    • collect()
  • 也有一些为了简化书写而设计的专用归约操作 : sum(), max(), min(), count()

reduce

  • 实现从一组元素中生成一个值
  • sum(),max(),min(),count()等都是reduce操作,将这些单独设为函数是因为经常使用
  • reduce()的方法定义有三种重写形式:

    • Optional< T > reduce(BinaryOperator< T > accumulator);
    • T reduce(T identity, BinaryOperator< T > accumulator);
    • < U > U reduce(U identity, BiFunction< U, ? super T,U> accumulator, BinaryOperator< U > combiner);
    • 虽然函数的定义越来越长,但是语义不变.多的参数是为了指明初始值(identity), 或者是指定并行执行时多个部分结果的合并方式(combiner)
  • 从一组单词中找出最长的单词.这里"大"的含义就是"长":

    /*
     * 找出最长的单词
     */
    Stream<String> stream = Stream.of("I", "love", "you", "too");
    Optional<String> longets = stream.reduce((s1, s2) -> s1.length() >= s2.length() ? s1 : s2);
    // Optional<String> longest = stream.max((s1, s2) -> s1.length() - s2.length());
    System.out.println(longest.get());
    • 选出最长的单词love
    • 其中Optional是只有一个值的容器,使用Optional可以避免null
  • 求出一组单词长度之和. 这是个求和操作,操作对象输入类型是String,结果类型是Integer:

Java Stream详解!深度解析Lambda表达式和Stream表达式的使用原理

/*
 * 求单词长度之和
 */
 Stream<String> strean = Stream.of("I", "love", "you", "too");
 Integer lengthSum = stream.reduce(0,        // 初始值 (1)
                                   (sum, str) -> sum +str.length(),        // 累加器 (2)
                                   (a, b) -> a + b);        // 部分和拼接,并行执行时会用到 (3)
// int lengthSun = stream.mapToInt(str -> str.length()).sum();
System.out.println(lengthSum);
  • 上述代码 (2) 处的累加器:

    • 字符串映射成长度
    • 并和当前累加和相加
    • 使用reduce() 函数将这两步合二为一,更有助于提升性能
  • 同样也可以使用map()sum() 组合也可以达到目的

collect

  • reduce() 的优点的是生成一个值,但是如果想要从Stream中生成一个集合或者Map等复杂对象时,就要用到collect()
  • 示例:
/* 
 * 将Stream转换成容器或者Map
 */
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
// 将Stream转换成Map
Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length));
  • 上述分别将Stream转换成List,Set,Map
  • 需要注意的有:

    • Function.identity()
    • String::length
    • Collectors

接口的静态方法和默认方法

  • Function是一个接口 ,Function.identity() 含义有两个方面:

    • Java 8允许在接口中加入具体方法. 接口中的具体方法有两种:

      • static: 静态方法,identity()就是Function接口的一个静态方法
      • default: 默认方法
    • Function.identity(): 返回一个输出和输入一样的Lambda表达式对象,等价于 t -> t 形式的Lambda表达式
在Java 7之前要想在定义好的接口中加入新的抽象方法是很困难甚至不可能的,因为会所有实现了该接口的类都要重新实现.Java 8中的default方法就是用来解决这个问题,直接在接口中实现新加入的方法,引进了default方法之后,可以继续加入static方法来避免专门的工具类
方法引用
  • 形如String::length的语法格式叫作方法引用(method reference),这种语法用来替代某些特定形式的Lambda表达式
  • 如果Lambda表达式的全部内容就是调用一个已有的方法,就可以用方法引用来代替Lambda表达式
  • 方法引用可以分为四类:

    • 引用静态方法: Integer :: sum
    • 引用某个对象的方法: list :: add
    • 引用某个类的方法: String :: length
    • 引用构造方法: HashMap :: new

Collector

Java Stream详解!深度解析Lambda表达式和Stream表达式的使用原理

  • 收集器Collector是为Stream.collect方法打造的工具类
  • 将一个Stream转换成一个容器或者Map至少需要考虑两个方面:

    • 目标容器是什么: ArrayList,HashSet还是TreeMap
    • 新元素如何添加到目标容器中: List.add()还是Map.put()
    • 如果是并行进行规约,还要使得collect()如何做到将多个部分结果合并成一个
  • collect() 方法定义: < R > R collect(Supplier< R > supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);

    • 三个参数一次对应着上面的三条分析
    • 每次调用collect() 都要传入这三个参数很麻烦,所以使用收集器Collector对三个参数进行简单的封装
    • 所以collect() 另一个方法定义: < R, A> R collect(Collecor<? super T, A, R> collector);
  • Collectors工具类可以通过静态方法生成各种常用的Collector
  • 这样,将Stream规约成List可以通过如下两种方式:
/*
 * 将Stream规约成List
 */
 Stream<String> stream = Stream.of("I", "love", "you", "too");
 
 List<String> list1 = stream.collect(ArrayList :: new, ArrayList :: add, ArrayList :: addAll);
 System.out.println(list1);

 List<String> list2 = stream.collect(Collectors.toList());
 System.out.println(list2);
  • 通常情况下不需要手动指定collect() 的三个参数,而是调用collect(Collector<? super T, A, R> collector) 方法,并且参数中的Collector对象大都是直接通过Collectors工具类获得
  • 实际传入的收集器的行为决定collect()的行为

使用collect()生成Collection

  • 通过collect() 方法将Stream转换成容器的方法中将Stream转换成ListSet是最常见的操作
  • 在Collectors工具类中已经提供了对应的收集器:
/*
 * 将Stream转换成List或者Set
 */
 Stream<String> stream = Stream.of("I", "love", "you", "too");
 
 List<String> list = stream.collect(Collectors.toString());
 Set<String> set = stream.collect(Collectors.toSet());
  • 由于返回结果是接口类型,所以并不清楚类库实际选择的容器类型什么
  • 有时需要人为指定容器的实际类型,这个需求可以通过Collectors.toCollection(Supplier< C > collectionFactory) 完成:
/*
 * 使用toCollection指定规约容器的类型
 */
 ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList :: new));
 HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet :: new));
  • 分别指定规约结果为ArrayListHashSet

使用collect()生成Map

  • Stream依赖某种数据源,数据源可以是数组,容器等,但不能是Map
  • 但是可以从Stream生成Map,要做的是确定好Map的key和value分别代表什么,这个在于要想清楚到底要干什么
  • 通常在三种情况下collect()的结果会是Map:

    • 使用Collectors.toMap() 生成的收集器: 用户需要指定如何生成Mapkeyvalue
    • 使用Collectors.partitioningBy() 生成的收集器: 对元素进行二分区操作时用到
    • 使用Collectors.groupingBy() 生成的收集器: 对元素做group操作时用到
  • 使用toMap()生成的收集器:

    • 这个是和Collectors.toCollection() 并列的方法
    • 示例: 将学生列表转换成由<学生, GPA>组成的Map
/*
 * 使用toMap()统计学生的GPA
 */
 Map<Student, Double> studentToGPA = student.stream().collect(Collectors.toMap(Function.identity(),    // 如何生成key
                                                                                 student -> computeGPA(student)));    // 如何生成value                                                                 
  • 使用partitioningBy()生成的收集器:

    • 适用于将Stream中的元素依据某个二值逻辑(Boolean 满足,不满足)分成互补相交的两部分
    • 示例: 将学生分成成绩及格和不及格的两部分
/*
 * 将学生成绩分为及格不及格两部分
 */
 Map<Boolean, List<Student>> passingFailing = students.stream().collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
  • 使用groupingBy()生成的收集器:

    • 这是比较灵活的一种,与SQL中的group by语句类似
    • 这里的groupingBy也是按照某个属性对数据进行分组,属性相同的元素会被对应到Map的同一个key
    • 示例: 将员工按照部门进行分组
/*
 * 将员工按照部门进行分组
 */
 Map<Department, List<Employee>> byDept = employees.stream().collect(Collectors.groupingBy(Employee :: getDepartment));
  • 有时候仅仅分组是无法满足要求的.在SQL中使用group by是为了方便更高级的查询:

    • 先将员工按照部门分组
    • 然后统计每个部门员工的人数
    • 增强版的groupingBy()能够满足这种需求:

      • 增强版的groupingBy()允许先对元素分组之后再执行某种运算,比如求和,计数,平均值,类型转换等
      • 这种先将元素分组的收集器叫作上游收集器
      • 然后执行分组后的运算的收集器叫作下游收集器
/*
 * 使用下游收集器统计每个部门的人数
 */
 Map<Department, Integer> totalByDept = employees.stream()
                                                  .collect(Collectors.groupingBy(Employee :: getDepartment,
                                                                                  Collectors.counting()));

这个groupingBySQL相似,也是高度非结构化

  • 下游收集器还可以包含更下游的收集器:

    • 将员工按照部门分组
    • 得到每个员工的名字字符串,而不是一个个Employee对象
/*
 * 按照部门对员工进行分组,并且只保留员工的名字
 */
 Map<Department, List<String>> byDept = employees.stream()
                                                  .collect(Collectors.groupingBy(Employee :: getDepartment,
                                                   Collectors.mapping(Employee :: getName,
                                                                      Collectors.toList())));

使用collect()做字符串join

  • 字符串拼接时使用Collectors.joining() 生成的收集器,代替for循环拼接
  • Collectors.joining() 方法有三种重写形式,分别对应三种不同的拼接方式:
/*
 * 使用Collectors.joining()拼接字符串
 */
 Stream<String> stream = Stream.of("I", "love", "you");

String joined = stream.collect(Collectors.joining());    // Iloveyou
String joined = stream.collect(Collectors.joining(","));    // I,love,you
String joined = stream.collect(Collectors.joining(",", "{", "}"));    // {I,love,you}
  • 除了可以使用Collectors工具类已经封装好的收集器,还可以自定义收集器.或者直接调用collect(Supplier< R > supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner) 方法,收集需要的任何形式的信息

Stream Pipelines

  • 通过使用Stream API中引起的疑问:

    • 如此强大的Stream API是如何实现的?
    • Pipeline是怎么执行的,每次调用都会迭代一次吗?
    • 自动并行又是怎么做到的,线程个数是多少?
  • 容器执行Lambda表达式的方式 - 以ArrayList.forEach()方法为例:
/*
 * ArrayList.forEach()
 */
 public void forEach(Consumer<? super E> action) {
     ...
     for (int i = 0; modCount == expectedModCount && i < size; i ++) {
         // 回调方法
         action.accept(elementData[i]);
     }
     ...
 }
  • ArrayList.forEach() 方法的主要逻辑就是一个for循环,在该for循环里不断调用action.accept() 回调方法完成对元素的遍历
  • 回调方法在Java GUI的监听器中广泛使用,Lambda表达式的作用就是相当于一个回调方法
  • Stream API中大量使用Lambda表达式作为回调方法. 但想要理解Stream, 关键的是:

    • 流水线
    • 自动并行
int longestStringLengthStaringWithA = strings.stream().filter(s -> s.startsWith("A"))
                                                      .mapToInt(String :: length)
                                                      .max();
  • 上述代码用来求出以字母 "A" 开头的字符串的最大长度:

    • 一种直白的方式就是为每一次函数调用都执行一次迭代. 尽管这样做能够实现功能,但效率上是无法接受的
    • 类库的实现是使用Stream Pipeline的方式巧妙地避免了多次迭代.基本思想就是在一次迭代中尽可能多的执行用户指定的操作
  • Stream中的相关操作:

    • 中间操作: Intermediate operations

      • 无状态: Stateless

        • unordered()
        • filter()
        • map()
        • mapToInt()
        • mapToLong()
        • mapToDouble()
        • flatMap()
        • flatMapToInt()
        • flatMapToLong()
        • flatMapToDouble()
        • peek()
      • 有状态: Stateful

        • distinct()
        • sorted()
        • limit()
        • skip()
    • 结束操作: Terminal operations

      • 短路操作: short-circuiting

        • anyMatch()
        • allMatch()
        • noneMatch()
        • findFirst()
        • findAny()
      • 非短路操作:

        • forEach()
        • forEachOrdered()
        • toArray()
        • reduce()
        • collect()
        • max()
        • min()
        • count()
  • Stream上的所有操作分为两类: 因为Stream底层对每一种情况的处理方式不同,所以要进行精细的划分

    • 中间操作: 中间操作只是一种标记

      • 无状态: 指元素的处理不受前面元素的影响,处理完一个元素就能立即知道结果
      • 有状态: 指元素的处理受到别的元素的影响,必须等到所有元素处理之后才能知道结果
    • 结束操作: 只有结束操作才会触发实际的计算

      • 短路操作: 指不用处理全部元素就可以返回结果
      • 非短路操作: 指对所有的元素处理后才可以返回结果

Stream Pipeline实现方案

  • 一种直白的Stream Pipeline实现方案:

Java Stream详解!深度解析Lambda表达式和Stream表达式的使用原理

  • 求最长字符串的长度:

    • 一种直白的实现方式是为每一次函数调用都执行一次迭代,并将处理中间结果发明放到某种数据结构中,比如数组,容器等

      • 就是调用filter() 方法后立即执行
      • 选出所有以A开头的字符串并放到一个列表list1
      • 然后让list1传递给mapToInt() 方法并立即执行
      • 生成的结果放到list2
      • 最后遍历list2, 找出最大的数字作为最终的结果
    • 这种实现方法实现简单直观,但存在两个明显的缺陷:

      • 迭代次数多: 迭代次数和函数的调用次数相等
      • 频繁产生中间结果: 每次函数调用都产生一次中间结果,存储开销大
  • 不使用Stream API在一次迭代中实现求最长字符串长度的方式:
int longest = 0;
for (String str : strings) {
    if (str.startsWith("A")) {    // 类似filter(),保留以A开头的字符串
        int len = str.length();    // 类似mapToInt(),得到字符串的长度
        longest = Math.max( );
    }
}
  • 采用这种方法不但减少了迭代次数,也避免了存储中间结果,这就是Stream Pipeline.将三个操作放在了一次迭代中

    • 只要事先知道意图,总是能够采取上述方式实现与Stream API等价的功能

Stream Pipeline解决方法

  • 由于Stream类库的设计者不知道用户意图,所以如何在无法假设用户行为的前提下,是类库的设计者要考虑的问题?
  • 关于这个解决方法,可以采用某种方式记录用户每一步的操作,当用户调用结束操作时将之前记录的操作叠加到一起在一次迭代中全部执行完成. 关于这种解决方法,需要解决以下四个问题:

    • 用户的操作如何记录?
    • 操作如何叠加?
    • 叠加之后的操作如何执行?
    • 执行后的结果在哪里?

操作如何记录?

  • 这里的操作指的是Stream中间操作
  • 很多Stream的操作会需要一个回调函数 - Lambda表达式,因此一个完整的操作应该是一个三元数组:

    • <数据来源, 操作, 回调函数>
  • Stream中使用Stage的概念来描述一个完整的操作,并用某种实例化后的PipelineHelper来代表Stage, 将具有先后顺序的各个Stage连到一起,就构成了整个Stream Pipeline
  • Stream相关类和接口的继承关系图:

Java Stream详解!深度解析Lambda表达式和Stream表达式的使用原理

  • IntPipeline.LongPipeline,DoublePipeline三个类是专门为三种基本类型而不是包装类型定制的,与ReferencePipeline是并列关系
  • 图中Head用于表示第一个Stage, 即调用诸如Collection.stream() 方法产生的Stage, 很显然这个Stage中不包含任何操作
  • StatelessOpStatefulOp分别表示无状态和有状态的Stage, 对应于无状态和有状态的操作
  • Stream Pipeline组织结构示意图:

Java Stream详解!深度解析Lambda表达式和Stream表达式的使用原理

  • 通过Collection.stream() 方法得到Head,Stage0, 紧接着调用一系列中间操作,不断产生新的Stream
  • 这些Stream对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage都记录了前一个Stage和本次操作的回调函数,依靠这种数据结构就能建立起对所有数据源的操作. 这就是Stream记录操作的方式

操作如何叠加?

  • 通过上面的方法解决了操作记录问题,要想让Stream Pipeline起到应有的作用需要一种将所有操作叠加到一起的方案
  • 因为只有当前Stage本身才知道该如何执行自己包含的操作.前面的Stage并不知道后面的Stage到底执行了哪种操作,以及回调函数是哪种形式,所以不能够从Stream PipelineHead开始依次执行每一步操作与回调函数来实现操作叠加
  • 为了解决以上问题,就需要某种协议来协调相邻Stage之间的调用关系, 这种协议由Sink接口完成 ,Sink接口包含的方法如下所示:
方法名 作用
void begin(long size) 开始遍历元素之前调用该方法,通知Sink做好准备
void end() 所有元素遍历完成之后调用,通知Sink没有更多的元素了
boolean cancellationRequested() 是否可以结束操作,可以让短路操作尽早结束
void accept(T t) 遍历元素时调用,接收一个待处理元素并对元素进行处理.
Stage将自己包含的操作和回调方法封装到该方法里,
前一个Stage只需要调用当前Stage.accept(T t)方法就可以
  • 通过Sink协议,可以方便地进行相邻的Stage调用,每个Stage都会将自己的操作封装到一个Sink里,前一个Stage只需要调用最后一个Stageaccept() 方法即可,并不需要知道Stage的内部是如何处理的
  • 对于有状态的操作,Sinkbegin()end() 方法是必须实现的:

    • 比如Stream.sorted() 是一个有状态的中间操作
    • 对应的Sink.begin() 方法可能创建一个存放结果的容器
    • accept() 方法负责将元素添加到容器中
    • 最后end() 负责对容器进行排序
  • 对于短路操作,Sink.cancellationRequest() 是必须实现的:

    • 比如Stream.findFirst() 是一个短路操作
    • 只要找到一个元素 ,cancellationRequested() 就应该返回true, 以便调用者尽快结束查找
  • Sink的四个接口方法互相协作,共同完成计算任务. 实际上Stream API内部实现的本质,就是如何重载Sink的这四个接口方法
  • 根据Sink对操作的包装,就解决了Stage之间的调用问题,执行时只需要从流水线的head开始对数据源依次调用每个Stage对应的Sink.{begin(), accept(), cancellationRequested(), end()} 方法
  • 示例: 一种可能的Sink.accept()方法流程
void accept(U u) {
    1. 使用当前Sink包装的回调函数处理u
    2. 将处理结果传递给Pipeline下游的Sink
}
  • Sink接口的方法都是按照 [处理 -> 转发] 的模型实现
  • 示例: Stream的中间操作是如何将自身的操作包装成Sink以及Sink如何将结果转发给下一个Sink的,Stream.map()方法如下
/*
 * Stream.map(),调用该方法将产生一个新的Stream
 */
public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
    ...
    return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
        @override
        /*
         * opWripSink()方法返回由回调函数包装成的Sink
         */
         Sink<P_OUT> opWrapSink(int flags, Sink<R> downStream) {
             return new Sink.ChainedReference<P_OUT, R>(downStream) {
                 @Override
                 public void accept(P_OUT u) {
                     // 使用当前Sink包装的回调函数mapper处理u
                     R r = mapper.apply(u)
                     // 将处理结果传递给流水线下游的Sink
                     downstream.accept(r);
                 }
             };
         }
    };
}
  • 将回调函数mapper包装到一个Sink中:

    • Stream.map() 是一个无状态的中间操作,所以map() 方法返回了一个StatelessOp内部类对象,一个新的Stream
    • 调用这个新StreamopWripSink() 方法将得到一个包装了当前回调函数的Sink
  • 示例:

    • Stream.sorted() 方法将对Stream中的元素进行排序
    • 这是一个有状态的中间操作,因为在读取所有元素之前是无法获得最终顺序的
    • sorted()方法封装的Sink如下:
/*
 * Stream.sort()中的Sink实现
 */
 class RefSortingSink<T> extends AbstractRefSortingSink<T> {
    // 存放用于排序的元素
     private ArrayList<T> list;

    RefSortingSink(Sink<? super T> downstream, Comparator<? super T> comparator) {
        super(downstream, comparator);
    }

    @Override
    public void begin(long size) {
        ...
        // 创建一个存放排序元素的列表
        list = (size > 0) ? new ArrayList<T>((int)size) : new ArrayList<T>();
    }

    @Override
    public void end() {
        // 只有全部元素结束接收之后才能开始排序
        list.sort(comparator);
        downstream.begin(list.size());
        
        if (!cacellationWasRequested()) {
            // 如果下游Sink不包含短路操作,将处理结果传递给流水线下游的Sink
            list.forEach(downstream :: accept);
        } else {
            /*
             * 如果下游Sink包含短路操作:
             *     每次都调用cancellationRequested()询问是否可以结束处理
             */
             for (T t : list) {
                 if (down.cancellationWasRequested()) {
                     break;
                 }
                 // 将处理结果传递给流水线下游的Sink
                 downstream.accept();
             }
        }
        downstream.end();
        list = null;
    }

    @Override
    public void accept(T t) {
        /*
         * 使用当前Sink包装动作处理:
         *     将元素添加到中间列表中
         */
        list.add(t);
    }
 }
  • Sink中的四个接口方法的协作方式:

    • 首先begin() 方法获取参与排序的元素个数传递给Sink. 方便确定中间结果容器的大小
    • 然后通过accept() 方法将元素添加到中间结果中,最终执行时调用者会不断调用该方法,直到遍历所有元素
    • 最后end() 方法返回给Sink所有元素遍历完毕,启动排序步骤,排序完成后将结果传递给下游的Sink
    • 如果下游的Sink是短路操作,将结果传递给下游时不断询问下游cancellationRequested() 是否可以结束处理

叠加操作如何执行?

Java Stream详解!深度解析Lambda表达式和Stream表达式的使用原理

  • Sink封装了Stream的每一步操作,并使用 [处理 -> 转发] 的模式来叠加操作.一旦调用某个结束操作, 就会触发整个流水线的执行

    • 结束操作之后不会有别的操作,所以结束操作不会创建新的流水线阶段Stage. 流水线的链表不会再往后延伸
    • 结束操作会创建一个包装了自己操作的Sink, 这是最后一个Sink, 不会有下游的Sink. 所以这个Sink只需要处理数据而不需要将结果传递给下游的Sink
    • 对于Sink[处理 -> 转发] 模型,结束操作的Sink就是调用链的出口
  • 上游Sink如何找到下游Sink:

    • 一种方案是在PipelineHelper中设置一个Sink字段,在流水线中找到下游Stage并访问Sink字段即可
    • Stream中,设置了一个SinkAbstractPipeline.opWrapSink(int flag, Sink downstream) 方法来得到Sink. 该方法的作用:

      • 返回一个新的包含了当前Stage代表的操作以及能够将结果传递给downstreamSink对象
  • 使用一个新的Sink对象而不是返回一个Sink字段:

    • 因为使用opWrapSink() 可以将当前操作与下游Sinkdownstream参数结合成新的Sink
    • 这样只要从流水线的最后一个Stage开始,不断调用上一个StageopWrapSink() 方法直到最开始(不包括stage0, 因为stage0代表数据源,不包含操作),就可以得到一个代表了流水线上所有操作的Sink
/**
 * AbstractPipeline.wrapStack():
 * 从下游向上游不断包装Sink,如果最初传入的Sink代表结束操作,函数返回时就可以得到一个代表了流水线上所有操作的Sink
 */
final <P_IN> Sink<P_IN> wrapSink() {
    ...
    for (AbstractPipeline p = AbstractPipeline.this; p.depth > 0; p = p.previousStage) {
        sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
    }
    return (Sink<P_IN>) sink;
}
  • 流水线Stage上从开始到结束的所有操作都被包装到一个Sink里,执行这个Sink就相当于执行整个流水线:
/*
 * AbstractPipeline.copyInto():
 *     对spliterator代表的数据执行wrappedSink代表的操作
 */
 final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
     ...
     if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags)) {
         // 通知开始遍历履历
         wrappedSink.begin(spliterator.getExactSizeIfKnown());
         // 迭代
         spliterator.forEachRemaining(wrappedSink);
         // 通知遍历结束
         wrappedSink.end();
     }
     ...
 }

上述代码首先调用wrappedSink.begin() 方法告诉Sink数据即将到来,然后调用Spliterator迭代器的spliterator.forEachRemaining() 方法对数据进行迭代,最后调用wrappedSink.end() 方法通知Sink数据处理结束

执行后的结果位置

  • 首先不是所有的Stream结束操作都需要返回结果,有些操作只是为了使用副作用Side-effects :

    • 比如Stream.forEach() 方法将结果打印出来就是常见的副作用场景
    • 事实上,除了打印之外的场景都应该避免使用副作用
    • 副作用不能被滥用,因为使用的正确性和效率都无法保证,因为Stream会并行执行
    • 大多数使用副作用的地方都可以使用归约操作来更安全和有效地完成
// ======================== 错误的收集方式 ========================
ArrayList<String> results = new ArrayList<>();
stream.filter(s -> pattern.matcher(s).matches()).forEach(s -> results.add(s));

// ======================== 正确的错误收集方式 ====================
List<String> results = stream.filter(s -> pattern.matcher(s).matches()).collect(Collectors.toList());
  • 根据不同的Stream结束操作,需要返回结果的流水线结果存储在不同的位置:

Java Stream详解!深度解析Lambda表达式和Stream表达式的使用原理

  • 对于表中返回boolean或者Optional(存放一个值的容器)的操作,由于返回一个值,只需要在对应的Sink中记录这个值,等到执行结束时返回
  • 对于规约操作,最终结果存放在用户调用时指定的容器中,容器类型通过收集器指定. collect(), reduce(), max(), min() 都是规约操作.尽管max()min() 也是返回一个Optional, 但事实上底层是通过reduce() 方法实现的
  • 对于返回数组的情况,结果放在数组中. 但是在最终返回数组之前,结果存储在Node的数据结构中:

    • Node是一种多叉树结构,元素存储在树的叶子中,并且一个叶子节点可以存放多个元素.这样执行起来方便

总结

  • 能够使用Lambda表达式的依据是必须有响应的函数接口(内部只有一个抽象方法的接口)
  • Lambda表达式主要用来定义行内执行的方法类型接口
  • Lambda表达式免去了使用匿名方法和匿名内部类的麻烦,使Java有了简单强大的函数化编程能力
上一篇:关于阿里云DRDS 5.3升级 详细介绍 性能提升300%


下一篇:【移动开发】Android中不用图片资源也能做出好看的界面