Java Stream And Method Reference


一、Stream 流

提到 Stream 就容易想起来 Java I/O 中的流,实际上,流不仅仅可以是 IO。在 Java 8 中,得益于 Lambda 所带来的函数式编程,引入了一个 全新的 Stream 概念,用于解决已有集合类既有的弊端。

1、引言

传统集合遍历

几乎所有的集合(如 Collection 接口或 Map 接口等等)都支持直接或间接的遍历操作。当我们需要对集合中的元素进行操作的时候,除了必须的添加、删除、获取外,最典型的就是集合遍历。

例如:

public class Demo01_traverseList {

    public static void main(String[] args) {
        
        // 演示传统的集合遍历
        ArrayList<Integer> list = new ArrayList<>();

        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);
        list.add(7);
        list.add(8);
        list.add(9);

        for (Integer integer : list) {
            System.out.println(integer);
        }
    }
}

这里是一个非常简单的集合遍历输出例子。

循环遍历的弊端

Java 8 的 Lambda 让我们可以更专注于 做什么(What)、而不是 怎么做(How)

看上边的代码,我们可以发现:

  • for 循环的语法就是 “怎么做”
  • for 循环的循环体就是 “做什么”

为什么要使用循环?因为要进行遍历。但是循环就是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是第一个到最后一个顺序处理的循环。前者是目的,后者是方式。

假如我有这样的需求,对一个集合的元素进行筛选,将筛选出的元素再进行处理,这样使用传统的方式来处理是这样的:

public class Demo02_Stream {

    public static void main(String[] args) {

        ArrayList<String> list = new ArrayList<>();

        list.add("张三");
        list.add("李四");
        list.add("王五");
        list.add("赵六");
        list.add("张七");
        list.add("张八");
        list.add("张九");
        
        // 对 list 中集合进行过滤,只要以 张 开头的元素就存储到一个新的集合中
        ArrayList<String> filterList = new ArrayList<>();

        for (String str : list) {
            if (str.startsWith("张")) {
                filterList.add(str);
            }
        }
        
        // 对过滤后元素再进行处理,循环输出
        for (String str : filterList) {
            System.out.println(str);
        }
    }
}

Stream 流的处理

下面使用 Java 8 的 Stram API:

/**
 * 使用 Stream 流的方式,遍历集合,对集合中的数据进行过滤
 * Stream 流是 JDK 1.8 后出现的
 * 它关注的是做什么,而不是怎么做
 */
public class Demo02_Stream {

    public static void main(String[] args) {

        ArrayList<String> list = new ArrayList<>();

        list.add("张三");
        list.add("李四");
        list.add("王五");
        list.add("赵六");
        list.add("张七");
        list.add("张八");
        list.add("张九");
        
        // 使用 Stream 对集合进行加工
        list.stream()
                .filter((e) -> e.startsWith("张"))
                .forEach(e -> System.out.println(e));
    }
}

2、流式思想概述

整体来看,流式思想类似于工厂车间的 生产流水线

当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个 “模型” 步骤方案,然后按照这个方案去执行它。

上面这个流程展示了 过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种 “函数模型”。图中每一个方框都是一个 “流”,调用指定的方法,可以从一个流模型转换到另一个流模型。而最右侧的数字 3 就是最终结果。

这里的 filtermapskip 都是对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法 count 执行的时候,整个模型才会按照指定策略执行操作。而这得益于 Lambda 的延迟执行特性。

备注:”Stream” 流其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)

Stream(流)是一个来自数据源的元素队列

  • 元素是特定类型的对象,形成一个队列。Java 中的 Stream 并不会存储元素,而是按需计算
  • 数据源 :流的来源。可以是集合、数组等等

和以前的 Collection 操作不同,Stream 操作还有两个基础的特征:

  • Pipelining:中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式风格(fluent style)。这样可以对操作进行优化,比如延迟执行(laziness)和 短路(short-circuiting)
  • 内部迭代:以前对集合遍历都是通过 Iterator 或者增强 for 的方式,显式的在集合外部进行迭代,这叫做外部迭代。Stream 提供了内部迭代的方式,流可以直接调用遍历方法。

当使用一个流的时候,通常需要三个步骤:

  1. 获取一个数据源(source)
  2. 数据转换
  3. 执行操作获取想要的结果

每次转换原有的 Stream 流对象不改变,返回一个新的 Stream 流对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。

3、获取流

java.util.stream.Stream<T> 是 Java 8 新加入的最常用的流接口。(注意它不是函数式接口)

获取一个流非常简单,可以有以下几种方式:

  • 所有的 Collection 集合都可以通过 stream 默认方法获取流;
  • Stream 接口的静态方法 of 可以获取数组对应的流

java.util.Collection 接口中加入了 default 方法 stream 来获取流,所以其所有实现类均可获得流

/**
 * 两种方式获取流:
 * Collection : default Stream<E> stream()
 * static <T> Stream<T> of(T... values)
 * static <T> Stream<T> of(T t)
 */
public class Demo03_get_stream {

    public static void main(String[] args) {

        // 集合转化为流
        List<String> list = new ArrayList<>();

        Stream<String> stream1 = list.stream();

        Set<String> set = new HashSet<>();

        Stream<String> stream2 = set.stream();

        Map<String, String> map = new HashMap<>();
        // 获取键
        Set<String> keySet = map.keySet();
        Stream<String> stream3 = keySet.stream();

        // 获取值
        Collection<String> values = map.values();
        Stream<String> stream4 = values.stream();

        // 获取键值对
        Set<Map.Entry<String, String>> entries = map.entrySet();
        Stream<Map.Entry<String, String>> stream5 = entries.stream();

        System.out.println("=========================================");

        // 数组转换为流
        Stream<Integer> stream6 = Stream.of(1, 2, 3, 4, 5);

        int[] arr = new int[]{1, 2, 3, 4, 5};
        Stream stream7 = Stream.of(arr);

        // 单个元素
        Stream<Integer> stream8 = Stream.of(1);
    }
}

4、常用方法

流模型的操作很丰富,这里介绍一些常用的 API。这些方法可以分为两种:

  • 延迟方法:返回值类型仍是 Stream 接口自身类型的方法,支持链式调用(除了终结方法,其余方法均为延迟方法)
  • 终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持链式调用,例如:count()forEach()

更多方法参见 API 文档。

逐一处理:forEach

这里与 for 循环 for-each 不同

void forEach(Consumer<? super T> action);

这里接收一个 Consumer 接口,会将每一个流元素交给该函数进行处理

基本使用:快速打印集合:

public class Demo04_teste {

    public static void main(String[] args) {

        ArrayList<String> list = new ArrayList<>();
        list.add("张三");
        list.add("李四");
        list.add("王五");
        
        list.stream().forEach((elem) -> System.out.println(elem));
    }
}

过滤:filter

可以通过 filter 方法将一个流转换为另一个子集流:

Stream<T> filter(Predicate<? super T> predicate);

该接口接收一个 Predicate 函数式接口参数作为筛选条件。

基本使用:筛选出大于 5 的元素并打印个数

public class Demo04_teste {

    public static void main(String[] args) {

        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);
        list.add(7);
        list.add(8);
        list.add(9);

        long count = list.stream().filter((elem) -> elem > 5).count();
        System.out.println("大于 5 的元素个数: " + count);
    }
}

注意:Stream 流的特点

Stream 流属于管道流,它只能消费一次:

  • 第一个 Stream 流调用完方法,数据会流向下一个 Stream 流
  • 此时第一个 Stream 流已经使用完毕,就关闭了

映射:map

如果需要将流中的元素映射到另一个流中,可以使用 map 方法:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

该方法需要一个 Function 函数式接口参数,可以将当前流的 T 数据转换为另一种 R 类型的流。

基本使用:将字符串集合转换为字符串数组集合:

public class Demo04_teste {

    public static void main(String[] args) {

        ArrayList<String> list = new ArrayList<>();

        list.add("a,b,c");
        list.add("d,e");
        list.add("f");
        
        list.stream()
                .map((elem) -> elem.split(","))
                .forEach(elem -> System.out.println(Arrays.toString(elem)));
    }
}

统计个数:count

正如旧集合 Collection 中的 size 方法一样,流提供 count 方法用来统计其中的元素个数:

long count();

该方法返回一个 long 值代表元素个数(和 Collection 的 size 不同,size 返回的是 int)

注意:count 方法是一个终结方法,使用该方法后不能使用其他流方法了。

取前几个值:limit

limit 方法可以对流进行截取,只取前 n 个

Stream<T> limit(long maxSize);

参数是一个 long 类型,如果集合当前长度大于该参数则可以截取,反之则不进行操作。

基本使用:打印集合前两个元素

public class Demo04_teste {

    public static void main(String[] args) {

        ArrayList<String> list = new ArrayList<>();

        list.add("1");
        list.add("2");
        list.add("3");
        
        list.stream().limit(2).forEach((e) -> System.out.println(e));
    }
}

注意:limit 方法不是终结方法

跳过前几个:skip

如果希望跳过前几个元素,可以使用 skip 方法获取一个截取后的新流:

Stream<T> skip(long n);

如果流当前元素长度大于 n,则跳过前 n 个,否则会得到一个长度为 0 的流。

基本使用:跳过第一个

public class Demo04_teste {

    public static void main(String[] args) {

        ArrayList<String> list = new ArrayList<>();

        list.add("1");
        list.add("2");
        list.add("3");
        
        list.stream().skip(1).forEach((e) -> System.out.println(e));
    }
}

组合:concat

如果有两个流,希望合并为一个流,那么可以使用 Stream 接口的静态方法 concat:

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b);

注意:这个静态方法和 java.long.String 中的 concat 方法是不一样的

基本使用:

public class Demo04_teste {

    public static void main(String[] args) {

        // 单个元素流合并
        Stream<String> stream1 = Stream.of("One");
        Stream<String> stream2 = Stream.of("Two");
        
        Stream.concat(stream1, stream2).forEach(e -> System.out.println(e));
        
        // 集合流合并
        ArrayList<Integer> list1 = new ArrayList<>();
        list1.add(1);
        list1.add(2);
        list1.add(3);
        
        ArrayList<Integer> list2 = new ArrayList<>();
        list2.add(4);
        list2.add(5);
        list2.add(6);
        
        Stream.concat(list1.stream(), list2.stream()).forEach(e -> System.out.println(e));
    }
}

二、方法引用

在使用 Lambda 的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑一种情况:如果我们在 Lambda 中所指定的操作方案,已经有地方存在相同的方案了,那么还有必要再写重复的逻辑了吗?

1、冗余的 Lambda 场景

public class Demo01 {
    
    // 定义一个方法对字符串进行打印
    public static void printString(String str, Printable printable) {
        
        printable.print(str);
    }

    public static void main(String[] args) {
        
        printString("Hello World!", str -> System.out.println(str));
        
        /*
            Lambda 目的:打印传递的参数
                把参数 str 传递给 System.out 对象,调用 out 的方法 println 对字符串进行输出
                1. System.out 对象已经存在
                2. println 方法也已经存在
            所以我们可以使用方法引用来优化 Lambda 表达式
            使用 System.out 直接引用 println 方法
        */
        printString("Java", System.out::println);
    }
}

@FunctionalInterface
interface Printable {
    
    // 打印字符串的抽象方法
    void print(String str);
}

注意其中的双冒号写法,这被称为 “方法引用”,双冒号是一种新的语法。

2、方法引用符

这种双冒号 :: 就是引用运算符,而它所在的表达式被称为 方法引用

如果 Lambda 要表达的函数方案已经存在于某个方法的实现中,那么就可以通过双冒号来引用该方法作为 Lambda 的替代者。

语义分析

上面例子中,System.out 对象中有一个重载的 println(String s) 方法恰好是我们所需要的。那么对于 printString 方法的函数式接口参数,下面两种写法完全等效:

  • Lambda 表达式写法:s - > System.out.println(s);
  • 方法引用写法:System.out::println

第一种语义是指:拿到参数之后经过 Lambda 之手,继而传递给 System.out.println 方法去处理

第二种等效写法的语义是:直接让 System.outprintln 方法来取代 Lambda 。两种写法的执行效果完全一致,第二种写法复用了已有方案,更加简洁。

注:采用方法引用要注意一点,Lambda 传递的参数一定符合要引用方法的参数类型,否则会抛出异常。

推导与省略

如果使用 Lambda,那么可以根据 “可推导就是可省略” 的原则,无需指定参数类型,也无需指定方法重载形式 —— 它们都将被自动推导,如果使用方法引用,也同样可以根据上下文推导。

函数式接口是 Lambda 的基础,而方法引用是 Lambda 的孪生兄弟。

3、通过对象名引用方法成员

这是最常见的一种写法,与上面的例子一样,两个前提:

  • 引用方法的对象必须存在
  • 对象中有我们需要的方法
public class Demo02ObjectRef {
    
    public static void printString(String str, PrintableRe printableRe) {
        
        printableRe.print(str.toUpperCase());
    }

    public static void main(String[] args) {
        
        // 原先的方式
        printString("Hello", str -> {
            MethodRefObject ref = new MethodRefObject();
            ref.printUpperCase(str);
        });
        
        // 使用方法引用进行优化
        // 对象必须存在, 然后才可以引用其中已经存在的方法
        MethodRefObject methodRefObject = new MethodRefObject();
        printString("World", methodRefObject::printUpperCase);
    }
}

class MethodRefObject {
    
    public void printUpperCase(String str) {
        System.out.println(str.toUpperCase());
    }
}

@FunctionalInterface
interface PrintableRe {
    void print(String str);
}

4、通过类名称引用静态方法

由于在 java.lang.Math 类中已经存在了静态方法 abs,所以当我们需要使用 Lambda 来调用该方法时,直接使用方法引用就可以了。

public class Demo03StaticMethodRef {
    
    public static int testAbs(int num, Calcable c) {
        
        return c.calsAbs(num);
    }

    public static void main(String[] args) {
        
        // Lambda 方式
        System.out.println(testAbs(-1, num -> Math.abs(num)));
        
        // 方法引用
        System.out.println(testAbs(-2, Math::abs));
    }
}

@FunctionalInterface
interface Calcable {
    
    int calsAbs(int num);
}

5、通过 super 引用成员方法

如果存在继承体系,当 Lambda 需要出现 super 调用时,也可以使用方法引用进行替换:

public class Demo04SuperMethodRef {

    public static void main(String[] args) {

        Man man = new Man();

        System.out.println("子类的方法: ");
        man.sayHello();

        System.out.println("子类调用父类方法:");
        man.show();
    }
}

@FunctionalInterface
interface Greetable {
    void greet();
}

class Human {
    
    void sayHello() {
        System.out.println("Hello!");
    }
}

class Man extends Human {

    @Override
    void sayHello() {
        System.out.println("Hello, I'm a man.");
    }
    
    // 定义方法传递函数式接口参数
    void method(Greetable g) {
        g.greet();
    }
    
    // 调用父类方法
    void show() {
        method(super::sayHello);
    } 
}

6、通过 this 引用成员方法

this 代表当前对象,如果需要引用的方法是当前类的成员方法,那么可以使用 “this::成员方法“ 的格式来使用方法引用。

public class Demo05ThisMethodRef {

    public static void main(String[] args) {

        Person person = new Person();
        
        person.soHappy();
    }
}

@FunctionalInterface
interface Richable {
    
    void buy();
}

class Person {
    
    private void buyHouse() {
        System.out.println("要买一栋房子。");
    }
    
    // 要结婚了,需要买房子 OvO
    public void marry(Richable r) {
        r.buy();
    }
    
    public void soHappy() {
        
        this.marry(this::buyHouse);
    }
}

7、类的构造器引用

由于类的构造器和类名一样,并不固定。所以构造器引用使用 类名称::new 的格式表示。

public class Demo06ConstructMethodRef {
    
    public static void printName(String name, PeopleBuilder builder) {
        
        System.out.println(builder.builderPeople(name).getName());
    }

    public static void main(String[] args) {
        
        printName("张三", People::new);
    }
}

@FunctionalInterface
interface PeopleBuilder {

    // 定义方法根据传入的姓名,创建一个对象
    People builderPeople(String name);
}

class People {
    
    private String name;

    public People(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

8、数组的构造器引用

数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。

public class Demo07ArrayConstructMethodRef {
    
    public static <T> T[] createArray(int len, ArrayBuilder<T> builder) {
        
        return builder.builderArray(len);
    }

    public static void main(String[] args) {
        
        // 例如创建一个 Integer 型大小为 10 的数组
        Integer[] array = createArray(10, Integer[]::new);

        for (int i = 0; i < 10; i++) {
            array[i] = i;
        }

        Stream.of(array).forEach(System.out::println);
    }
}

@FunctionalInterface
interface ArrayBuilder<T> {
    
    // 创建一个给定长度的数组
    T[] builderArray(int len);
}

Author: NaiveKyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source NaiveKyo !
  TOC