一文带你了解Java8之Stream

Posted 风在哪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文带你了解Java8之Stream相关的知识,希望对你有一定的参考价值。

Java8 Stream流式编程

简介

Java8中stream是用于对集合迭代器的增强,使之能供完成更高效的聚合操作(例如过滤、排序、统计分组等)或者大批量数据操作。此外,stream与lambda表达式结合后编码效率将会大大提高,并且可以提高可读性。

首先来看一个简单的场景,准备工作如下,编写了一个person类:

public class Person 
    /**
     * 姓
     */
    private String lastname;
    /**
     * 名
     */
    private String name;
    private Integer age;
    private String sex;
    private String address;

   	// 省略了构造函数、getter和setter方法以及toString()方法,读者可使用idea生成

接下来看看我们的一些需求:

  1. 假如我们要寻找以"王"为姓的人,那么我们该如何做呢?传统方法就是for循环遍历判断,然后加入结果集,但是这里如果用到stream的话,可以一行代码搞定(详细代码稍后列出)
  2. 假如我们要根据性别统计用户的平均年龄,那么我们该如何做呢?传统方法:首先就是根据不同的性别进行分组,然乎对两个分组求平均值,stream也可以一行代码搞定。

说的这么神奇,来看看具体的代码吧:

public class LearnStream 
    private static List<Person> persons = new ArrayList<>();

    static 
        persons.add(new Person("王", "大", 23, "男", "湖北省武汉市"));
        persons.add(new Person("王", "二", 22, "女", "河南省郑州市"));
        persons.add(new Person("刘", "三", 24, "女", "北京市"));
        persons.add(new Person("朱", "子", 21, "男", "天津市"));
    

    public static void main(String[] args) 
        LearnStream learnStream = new LearnStream();
        List<Person> ps = learnStream.searchLastname("王");
        ps.forEach(person -> System.out.println(person.toString()));
        List<Person> personList = learnStream.searchByParams(person -> person.getLastname().equals("王") && person.getAge() > 22);
        personList.forEach(p -> System.out.println(p.toString()));
        Map<String, Double> average = learnStream.average();
        average.forEach((k, v) -> System.out.println(k + ":" + v));
    

    /**
     * 查找以lastname为姓的人
     * @param lastname
     */
    public List<Person> searchLastname(String lastname) 
        List<Person> results = persons.stream()
                .filter(person -> person.getLastname().equals(lastname))
                .collect(Collectors.toList());
        return results;
    

    /**
     * 自定义lambda表达式参数
     * @param predicate
     * @return
     */
    public List<Person> searchByParams(Predicate<? super Person> predicate) 
        List<Person> collect = persons.stream().filter(predicate).collect(Collectors.toList());
        return collect;
    

    /**
     * 统计男性或女性的平均年龄
     * @return
     */
    public Map<String, Double> average() 
        // groupingBy按照某个属性分组
        Map<String, List<Person>> collect = persons.stream().collect(Collectors.groupingBy(person -> person.getSex()));
        // 先分组,然后在分组内按照某个属性计算其平均值
        Map<String, Double> res = persons.stream()
                .collect(Collectors.groupingBy(person -> person.getSex(),           // 基于性别分组
                        Collectors.averagingDouble(persons -> persons.getAge())));  // 基于年龄求分组平均值
        return res;
    

代码的运行结果如下:

通过上面的案例是不是可以发现,stream的代码其实很简单,一行搞定我们的需求,比传统的方法要高效很多。

stream出现背景

像这种遍历的方式,我们有没有更简单的实现方法呢?答案肯定是有的,通常情况下,我们的数据是存储在数据库中,当我们使用SQL语句查询数据的时候直接做好处理比在Java程序中写这些代码要简单的多。上面的需求可以通过如下SQL语句实现:

# 获取所有姓王的用户
select * from person where lastname='王';
# 根据性别统计平均年龄
select p.sex, average(p.age) from person as p group by sex;

通过一个sql语句就能实现我们上面Java代码中的功能,而且简单易懂,效率也比较高。

在传统的JavaEE项目中数据源单一且集中,像上面的需求我们可以通过SQL语句进行计算。但是现在互联网项目的数据源多样化,包括:关系数据库、NoSQL、Redis、mongodb、ElasticSerach等。此时,我们需要从各种数据源中聚集数据并进行统计,在stream出现之前,使用传统的for循环遍历非常繁琐,stream出现之后,这种局面就改变了。

Stream大大简化了我们的开发,结合lambda表达式更是效率神器。

lambda表达式

lambda表达式也可以称为闭包,它是推动Java8发布的最重要新特性,lambda允许把函数作为一个方法参数传递给方法。

在Java8之前,如果我们新创建一个线程对象,需要使用匿名内部类传递我们要执行的任务,在Java8我们可以使用lambda简化这种操作,例如:

public static void main(String[] args) 
    	// 匿名内部类
        Thread thread = new Thread(new Runnable() 
            @Override
            public void run() 
                System.out.println("使用匿名内部类");
            
        ).start();
		// lambda表达式
        new Thread(() -> System.out.println("使用lambda")).start();

从上面的代码中,我们可以看出lambda的简洁,比匿名内部类少写了很多代码(其实它的底层原理还是匿名内部类)。可以看看编译后的字节码(只包含lambda表达式的代码),从中可以发现它底层还是使用了匿名内部类:

函数式接口

上述的这种直接使用lambda是如何实现的呢?我们来看看Runnable的源码:

它是一个接口,而且它被注解@FunctionalInterface标注,这证明它是一个函数式接口,通过函数式接口我们可以使用lambda表达式来进行编码。

那么什么是函数式接口呢?它必须满足如下条件:

  1. 函数式接口只能包含一个方法(想想也对,我们使用lambda表达式不会指定实现哪个方法,所以只能有一个方法,然后lambda表达式就是对这个方法的实现)
  2. 可以包含多个默认方法(默认方法相当于已经实现的方法,默认方法不会影响lambda表达式对接口方法的实现)
  3. Object类下的方法不计算在内,例如:toString()、equals()、hashCode()等方法。

满足上述条件的接口就是函数式接口,那么@FunctionalInterface注解的作用是什么呢?

其实只要满足上述条件,没有@FunctionalInterface注解标注也是函数式接口,该注解的作用就是帮我们辨别一个接口是否是函数式接口,如果不是的话,在编译阶段就会报错。

例如:

上面这个接口就不是函数式接口,idea会很智能的提示我们编码错误。

当我们为部分方法提供默认实现,让接口中的方法只有一个时,这个函数式接口就成立了。

函数式接口是lambda表达式的前提,我们需要先编写函数式接口才能继续使用lambda表达式。

编写lambda表达式

在编写lambda之前,我们需要先编写一个函数式接口,如下:

@FunctionalInterface
public interface Function 
    /**
     * 输出姓名和年龄
     * @param name
     * @param age
     * @return
     */
    void print(String name, int age);


然后根据函数式接口传递参数,并且使用lambda表达式实现函数式接口:

public class MyLambda 

    public static void main(String[] args) 
        print((name, age) -> System.out.println(String.format("name: %s, age: %s", name, age)));
    

    public static void print(Function function) 
        function.print("王大", 23);
    


来看看运行结果:

这里的lambda表达式就相当于函数式接口的实现类,我们对函数式接口的方法调用的最终实现就是这个lambda表达式的内容,这里就是将我们传入的姓名和年龄转换为一个固定的格式,然后输出。

当lambda表达式没有参数时,可以直接以print(() -> System.out.println())这种形式书写,以空括号开始即可;如果lambda表达式只有一个参数的话,可以省略括号,例如print(name -> System.out.println(name)),这里的参数可以带类型,也可以不带,看个人习惯;而且使用lambda表达式也可以省略return语句。

lambda表达式的特性:

  • 单行表达式,如果有返回值可以省略return,如上面的代码所示
  • 代码块
		print((name, age) -> 
            String format = String.format("name: %s, age: %s", name, age);
            System.out.println(format);
        );

在多行表达式中,如果有返回值不能省略return

  • 方法引用,我们可以将lambda表达式的实现逻辑封装成一个方法,然后直接在lambda表达式函数中调用封装好的方法,称为方法引用,方法引用包括静态方法引用和动态方法引用
public class MyLambda 

    public static void main(String[] args) 
        // 静态方法引用
        print(MyLambda::format);
        // 普通方法引用
        print(new MyLambda()::f);
    

    public static void format(String name, int age) 
        System.out.println(String.format("name: %s, age: %s", name, age));
    

    public void f(String name, int age) 
        System.out.println(String.format("name: %s, age: %s", name, age));
    

    public static void print(Function function) 
        function.print("王大", 23);
    


Stream执行机制

Stream的执行机制就是首先通过数据源生成Stream,然后对这个Stream进行我们需要的操作,例如分组、排序、过滤等,然后采集结果输出最终的数据。

Stream特性

流的特性有哪些呢?

  • stream不存储数据
  • stream不改变源数据
  • stream不可重复使用

首先我们来看看流的产生方式

persons.stream();		// List<Person> persons = new ArrayList<>();
Arrays.stream(new int[] 1, 2, 3, 4, 5, 6);
Stream.of(1, 2, 3, 4);

接下来可以看看stream是不可重复使用的,当我们使用一次流之后,再重复使用时会报错,例如:

public class StreamFeatures 
    private static List<Person> persons = new ArrayList<>();

    static 
        persons.add(new Person("王", "大", 23, "男", "湖北省武汉市"));
        persons.add(new Person("王", "二", 22, "女", "河南省郑州市"));
        persons.add(new Person("刘", "三", 24, "女", "北京市"));
        persons.add(new Person("朱", "子", 21, "男", "天津市"));
    

    public static void main(String[] args) 
        Stream<Person> stream = persons.stream();
        Stream<Person> first = stream.filter(person -> person.getLastname().equals("王"));
        Stream<Person> second = stream.filter(person -> person.getAge() > 21);
    

运行结果如下:

当我们重复使用stream这个流时,就会报IllegalStateException异常,提示我们stream已经被操作过。

为什么stream流不能重复使用呢?因为流是串联起来的,从Stream的执行机制我们可以发现,stream是经过管道操作,刚开始产生的stream会作为输入进行下一次的操作,stream只能顺序执行,不能对同一stream进行不同的操作。所以当我们对stream进行两次不同的操作时会发生异常。

从最初的stream执行机制图中,我们可以发现,stream流在执行的过程中包括中间节点和终值节点;这些不同的节点就代表了不同的操作,对同一stream的所有操作组合在一起就变成了管道,管道中包含如下操作:

  • Intermediate(中间操作):调用中间操作方法会产生一个新的stream流,通过连续执行多个操作就组成了Stream中的执行管道,需要注意的是这些管道被添加后并不会真正执行,只有等到调用终值操作后才会执行
  • Terminal(终值操作):在调用该方法后会执行之前所有的中间操作,获取返回结果,并结束对流的使用
  • Short-circuiting:对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream;对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果

此外,数据源在stream流中的传递过程是一个一个进行的,就是从第一个数据源开始传递,经过不同的操作,产生不同的结果,例如:

public class StreamFeatures     
	private static List<Person> persons = new ArrayList<>();    
	static         
		persons.add(new Person("王", "大", 23, "男", "湖北省武汉市"));        
		persons.add(new Person("王", "二", 22, "女", "河南省郑州市"));        
		persons.add(new Person("刘", "三", 24, "女", "北京市"));        
		persons.add(new Person("朱", "子", 21, "男", "天津市"));    
	    
	public static void main(String[] args)         
		// 上一个节点的操作可以影响下一个节点        
		persons.stream()                
		.peek(person -> System.out.println(person.getName()))	// peek可以执行一个函数,相当于中间节点                
		.peek(person -> System.out.println(person.getAge()))                
		.toArray();    
	

上述代码的执行结果就是名字和年龄交替打印,这就证明了数据源里面的数据是一条条的传递的:

stream流在执行过程中,上一个节点的操作会影响下一个节点,影响方式包括:

  • 过滤
  • 转换:当我们使用map后,会将person类转变成person类对应的属性或者其他的值等,而且转换后会影响后续操作

  • 去重

首先看看未去重的版本:

加上distinct函数对stream流进行去重的结果:

我们可以对其进行调试,看看IDEA强大的调试功能:

从上面的调试过程可以看出对Stream流的每一步操作。

Stream实践

常见的流操作的分类如下:

Intermediate
map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

Terminal
forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

Short-circuiting
anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

当我们对Stream进行操作时,往往会产生中间结果,这些中间结果就是Optional包装类,所以我们先来了解一下Optional的具体作用。

Optional

Optional通过泛型来包装不同的值,这个值可能为null,也可能不为null,它相当于一个可选值的包装类。

在实际开发中,我们如果使用Optional来包装一个可能的null值的话,可以帮助我们减少大量的if else判断,首先来看看Optional包含哪些方法。

public final class Optional<T> 
	// 创造一个包含空值的Optional
    private static final Optional<?> EMPTY = new Optional<>();
	// Optional包装的值
    private final T value;
	// 私有构造函数,不对外公开,构造一个包含空值的Optional
    private Optional() 
        this.value = null;
    
	// 暴露给用户使用的静态方法,用于构造一个包含空值的Optional
    public static<T> Optional<T> empty() 
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    
	// 私有构造函数,构造一个包含非空值的Optional
    private Optional(T value) 
        this.value = Objects.requireNonNull(value);
    
	// 静态方法,构造一个包含非空值的Optional
    public static <T> Optional<T> of(T value) 
        return new Optional<>(value);
    
	// 静态方法,根据传入的参数是否为空选择创建不同的Optional
    public static <T> Optional<T> ofNullable(T value) 
        return value == null ? empty() : of(value);
    
	// 获取Optional包装的值
    public T get() 
        if (value == null) 
            throw new NoSuchElementException("No value present");
        
        return value;
    
	// 判断Optional包装的值是否为空
    public boolean isPresent() 
        return value != null;
    
	// 如果Optional包装的值不为空,对其进行操作
    public void i

以上是关于一文带你了解Java8之Stream的主要内容,如果未能解决你的问题,请参考以下文章

一文带你了解Java8之Stream

Java8 Stream流如何操作集合,一文带你了解!

Java8 Stream流如何操作集合,一文带你了解!

一文带你入门Java Stream流,太强了

都 Java19 了,还不了解 Java 8 ? 一文带你深入了解 Java 8 新特性

一文带你了解大数据技术之Hadoop