一文带你了解Java8之Stream

本文详细介绍了Java8中的Stream流式编程,包括其基本概念、使用场景、操作特性和实战应用。Stream提供了高效的数据处理方式,结合lambda表达式能显著提升代码简洁性和执行效率。内容涵盖Stream的产生、操作、过滤、映射、归约、收集、排序、并行执行等,以及Optional类的使用,帮助开发者更好地理解和运用Java8的这一重要特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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;
    }
}

代码的运行结果如下:

image-20210616153708303

通过上面的案例是不是可以发现,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表达式的代码),从中可以发现它底层还是使用了匿名内部类:

image-20210616162654304

函数式接口

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

image-20210616164827965

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

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

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

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

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

例如:

image-20210616165733528

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

image-20210616170022258

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

函数式接口是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);
    }

}

来看看运行结果:

image-20210616185401606

这里的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进行我们需要的操作,例如分组、排序、过滤等,然后采集结果输出最终的数据。

image-20210616194459109

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, "男", 
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值