Java8 Stream实战

1、引子

在了解Stream之前,我们先来看一个需求:已知一个公司的员工信息,获取当前公司中员工年龄大于30 岁的员工信息。首先,我们创建一个员工实体类。

public class Employee implements Serializable {
    private Integer id;
    private String name;
    private Integer age;
    private Double salary;

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }

    public Double getSalary() {
        return salary;
    }

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

    public void setAge(Integer age) {
        this.age = age;
    }

    public void setSalary(Double salary) {
        this.salary = salary;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public String toString()
    {
        return id.toString() + " " +name + " " + age.toString() + " " + salary.toString();
    }
}

我们用一个List对象构建一组员工数据。

    private static final String[] name = {"alice","tom","john","steven","susan","jerry"};
    private static Double[] salary = {10000.66,2222.22,4000.15,6500.99,7777.77,5000.25};
    private static Integer[] age = {10,20,30,40,50,60};

    public static List employeeList = new ArrayList();
    static {
        Random random = new Random(10);
        for(int i=0;i

接下来,最容易想到是我们使用常规遍历集合的方式来查找年龄大于等于30 的员工信息

    public List filterByAge(List list){
        List employees = new ArrayList<>();
        for(Employee e : list){
            if( e.getAge() >= 30){
                employees.add(e);
            }
        }
        return employees;
    }

很快,业务需求发生了变化,要求”获取当前公司中员工工资大于或者等于5000 的员工信息“。于是,我们不得不再次创建一个按照工资过滤的方法。

 public List filterBySalary(List list){
        List employees = new ArrayList<>();
        for(Employee e : list){
            if( e.getSalary() >=5000){
                employees.add(e);
            }
        }
        return employees;
    }

对比filterByAge()方法和filterBySalar() 方法后,我们发现,大部分的代码是相同的,只是for 循环中对于条件的判断不同。

如果此时我们再来一个需求,查找当前公司中年龄小于或者等于20并且工资大于5000 的员工信息,那我们又要创建一个过滤方法了。那该如何优化呢?可以考虑策略模式。

我们先定义一个泛型接口FilterEmployeInf,对传递过来的数据进行过滤,符合规则返回true,不
符合规则返回false。

public interface FilterEmployeeInf {
    boolean filter(T t);
}

接下来,我们创建FilterEmployeInf接口的实现类FilterEmployeeByAge来过滤年龄大于或者等于

30 的员工信息。

public class FilterEmployeeByAge implements FilterEmployee {
    @Override
    public boolean filter(Employee employee) {
        return employee.getAge()>=30;
    }
}

然后定义一个过滤员工信息的方法,此时传递的参数不仅有员工的信息集合,同时还有一个我
们定义的接口实例,在遍历员工集合时将符合过滤条件的员工信息返回。

    public List filterEmployee(List list,FilterEmployeeInf filter){
        List employees = new ArrayList<>();
        for(Employee e : list){
            if(filter.filter(e)){
                employees.add(e);
            }
        }
        return employees;
    }

做个简单测试调用

FilterEmployeeByAge filterEmployeeByAge = new FilterEmployeeByAge();

System.out.println("strategy age: " + 

filterEmployee(employeeList,filterEmployeeByAge).toString());

如果我们继续获取当前公司中工资大于或者等于5000 的员工信息,此时,我们只需要创建一个
FilterEmployeeBySalary 类实现MyPredicate 接口。

public class FilterEmployeeBySalary implements FilterEmployee{
    @Override
    public boolean filter(Employee employee) {
        return employee.getSalary()>=5000;
    }
}

再来做个简单测试调用

FilterEmployeeBySalary filterEmployeeBySalary = new FilterEmployeeBySalary();
System.out.println("strategy salary: " + 

filterEmployee(employeeList,filterEmployeeBySalary).toString());

可以看到,使用设计模式对代码进行优化后,无论过滤员工信息的需求如何变化,我们只需要
创建FilterEmployeInf接口的实现类来实现具体的过滤逻辑,然后在测试方法中调用filterEmployee(List list, FilterEmployeInf filter)方法将员工集合和过滤规则传入即可。

相比传统的实现,策略模式实现更加灵活扩展,但这种方式也有不爽的地方,每次定义一个过滤策略的时候,我们都要单独创建一个过滤类。那么使用匿名内部类是不是能够优化我们书写的代码呢?

接下来,我们就使用匿名内部类来实现对员工信息的过滤。

先来看过滤年龄大于或者等于30 的员工信息。

List employees = filterEmployee(employeeList, new FilterEmployeeInf() {
            @Override
            public boolean filter(Employee employee) {
                return employee.getAge()>=30;
            }
        });

再实现过滤工资大于或者等于5000 的员工信息。

List employees2 = streamApi.filterEmployee(employeeList, new FilterEmployee() {
            @Override
            public boolean filter(Employee employee) {
                return employee.getSalary()>=5000;
            }
        });

匿名内部类看起来比常规遍历集合的方法要简单些,并且将使用设计模式优化代码时,每次创
建一个类来实现过滤规则写到了匿名内部类中,使得代码进一步简化了。
但是,使用匿名内部类代码的可读性不高,并且冗余代码也比较多的问题依然存在。

好在Java8引入了lambda表达式,可以进一步简化我们的代码。

filterEmployee(employeeList,(e)->e.getAge()>=30).forEach(System.out::println);
filterEmployee(employeeList,(e)->e.getSalary()>=5000).forEach(System.out::println)

非常简洁,一行代码就搞定。注意,在使用Lambda 表达式时,我们还是要调用之前写的 filterEmployee(List list,FilterEmployeeInf filter)方法。还能不能进一步优化?

这就是本文的主角,Java8提供的Stream接口,使用Lambda 表达式结合Stream API,只要给出相应的集合,我们就可以完成对集合的各种过滤并输出结果信息。

对应前面的需求,来看看代码实现:

employeeList.stream().filter((e)->e.getAge()>=30).forEach(System.out::println);
employeeList.stream().filter((e)->e.getSalary()>=5000).forEach(System.out::println);

太简洁了!

2、Java8 Stream

Stream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。Stream可以由数组或集合创建,对流的操作分为两种:

  1. 中间操作,每次返回一个新的流,可以有多个。
  2. 终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。

另外,Stream有几个特性:

  1. stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。
  2. stream不会改变数据源,通常情况下会产生一个新的集合或一个值。
  3. stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。

集合和流二者的主要区别有两点:

1.集合是一个内存中的数据结构,每个元素都需要先算出来才能添加到集合中,是可以增删改的。
而流是概念上固定的数据结构,不能增删改,其元素是按需计算的。流就像一个延迟创建的集合,只有在消费时才会计算。

2.遍历数据的迭代方式不一样,集合需要用户去做迭代,但流则使用内部迭代,用户无需只要细节,只要给出一个函数想干什么就行了。

本质上,集合讲的是数据,流讲的是计算。

2.1  流的常用创建方法

1)使用Collection下的 stream() 和 parallelStream() 方法

List list = new ArrayList<>();
Stream stream = list.stream(); //获取一个顺序流
Stream parallelStream = list.parallelStream(); //获取一个并行流

2)使用Arrays 中的stream()方法,将数组转成流

Integer[] nums = new Integer[10];
Stream stream = Arrays.stream(nums);

3)使用Stream中的静态方法:of()iterate()generate()

Stream stream = Stream.of(1,2,3,4,5,6); 
Stream stream2 = Stream.iterate(0, (x) -> x + 2).limit(6);
stream2.forEach(System.out::println); // 0 2 4 6 8 10 
Stream stream3 = Stream.generate(Math::random).limit(2);
stream3.forEach(System.out::println);

4)使用 BufferedReader.lines() 方法,将每行内容转成流

BufferedReader reader = new BufferedReader(new FileReader("F:\\test_stream.txt"));
Stream lineStream = reader.lines();
lineStream.forEach(System.out::println);

5)使用 Pattern.splitAsStream() 方法,将字符串分隔成流

Pattern pattern = Pattern.compile(",");
Stream stringStream = pattern.splitAsStream("a,b,c,d");
stringStream.forEach(System.out::println);

2.2  流的中间操作

1)筛选与切片

  • filter:过滤流中的某些元素

  • limit(n):获取n个元素

  • skip(n):跳过n元素,配合limit(n)可实现分页

  • distinct:通过流中元素的 hashCode() 和 equals() 去除重复元素

List datalist = Arrays.asList(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14); 
Stream newStream = dataList.stream().filter(s -> s > 5) //6 6 7 9 8 10 12 14 14
        .distinct() //6 7 9 8 10 12 14
        .skip(2) //9 8 10 12 14
        .limit(2); //9 8
newStream.forEach(System.out::println);

2) 映射

  • map: 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。

  • flatMap: 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。

List list = Arrays.asList("a,b,c", "1,2,3");
 
//将每个元素转成一个新的且不带逗号的元素
Stream s1 = list.stream().map(s -> s.replaceAll(",", ""));
s1.forEach(System.out::println); // abc  123
 
Stream s3 = list.stream().flatMap(s -> {
    //将每个元素转换成一个stream
    String[] split = s.split(",");
    Stream s2 = Arrays.stream(split);
    return s2;
});
s3.forEach(System.out::println); // a b c 1 2 3

3) 排序

  • sorted():自然排序,流中元素需实现Comparable接口

  • sorted(Comparator com):定制排序,自定义Comparator排序器

List list = Arrays.asList("aa", "ff", "dd");
//String 类自身已实现Compareable接口
list.stream().sorted().forEach(System.out::println);// aa dd ff
 
Student s1 = new Student("aa", 10);
Student s2 = new Student("bb", 20);
Student s3 = new Student("aa", 30);
Student s4 = new Student("dd", 40);
List studentList = Arrays.asList(s1, s2, s3, s4);
 
//自定义排序:先按姓名升序,姓名相同则按年龄升序
studentList.stream().sorted(
        (o1, o2) -> {
            if (o1.getName().equals(o2.getName())) {
                return o1.getAge() - o2.getAge();
            } else {
                return o1.getName().compareTo(o2.getName());
            }
        }
).forEach(System.out::println);

3. 流的终止操作

1) 匹配、聚合操作

  • allMatch:接收一个 Predicate 函数,当流中每个元素都符合该断言时才返回true,否则返回false.

  • noneMatch:接收一个 Predicate 函数,当流中每个元素都不符合该断言时才返回true,否则返回false

  • anyMatch:接收一个 Predicate 函数,只要流中有一个元素满足该断言则返回true,否则返回false

  • findFirst:返回流中第一个元素

  • findAny:返回流中的任意元素

  • count:返回流中元素的总个数

  • max:返回流中元素最大值

  • min:返回流中元素最小值

List list = Arrays.asList(1, 2, 3, 4, 5);
 
boolean allMatch = list.stream().allMatch(e -> e > 10); //false
boolean noneMatch = list.stream().noneMatch(e -> e > 10); //true
boolean anyMatch = list.stream().anyMatch(e -> e > 4);  //true
 
Integer findFirst = list.stream().findFirst().get(); //1
Integer findAny = list.stream().findAny().get(); //1
 
long count = list.stream().count(); //5
Integer max = list.stream().max(Integer::compareTo).get(); //5
Integer min = list.stream().min(Integer::compareTo).get(); //1

2) 规约操作

归约,也称缩减,顾名思义,是把一个流缩减成一个值,能实现对集合求和、求乘积和求最值操作。

  • Optional reduce(BinaryOperator accumulator):第一次执行时,accumulator函数的第一个参数为流中的第一个元素,第二个参数为流中元素的第二个元素;第二次执行时,第一个参数为第一次函数执行的结果,第二个参数为流中的第三个元素;依次类推。

  • T reduce(T identity, BinaryOperator accumulator):流程跟上面一样,只是第一次执行时,accumulator函数的第一个参数为identity,而第二个参数为流中的第一个元素。

  • U reduce(U identity,BiFunction accumulator,BinaryOperator combiner):在串行流(stream)中,该方法跟第二个方法一样,即第三个参数combiner不会起作用。在并行流(parallelStream)中,我们知道流被fork join出多个线程进行执行,此时每个线程的执行流程就跟第二个方法reduce(identity,accumulator)一样,而第三个参数combiner函数,则是将每个线程的执行结果当成一个新的流,然后使用第一个方法reduce(accumulator)流程进行规约。

//经过测试,当元素个数小于24时,并行时线程数等于元素个数,当大于等于24时,并行时线程数为16
List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24); 
Integer v = list.stream().reduce((x1, x2) -> x1 + x2).get();
System.out.println(v);   // 300 
Integer v1 = list.stream().reduce(10, (x1, x2) -> x1 + x2);
System.out.println(v1);  //310 
Integer v2 = list.stream().reduce(0,
        (x1, x2) -> {
            System.out.println("stream accumulator: x1:" + x1 + "  x2:" + x2);
            return x1 - x2;
        },
        (x1, x2) -> {
            System.out.println("stream combiner: x1:" + x1 + "  x2:" + x2);
            return x1 * x2;
        });
System.out.println(v2); // -300 
Integer v3 = list.parallelStream().reduce(0,
        (x1, x2) -> {
            System.out.println("parallelStream accumulator: x1:" + x1 + "  x2:" + x2);
            return x1 - x2;
        },
        (x1, x2) -> {
            System.out.println("parallelStream combiner: x1:" + x1 + "  x2:" + x2);
            return x1 * x2;
        });
System.out.println(v3); //197474048

3)收集操作

因为流不存储数据,那么在流中的数据完成处理后,需要将流中的数据重新归集到新的集合里。toList、toSet和toMap比较常用,另外还有toCollection、toConcurrentMap等复杂一些的用法。

collect:接收一个Collector实例,将流中元素收集成另外一个数据结构。

Collector 是一个接口,有以下5个抽象方法:

Student s1 = new Student("aa", 10,1);
Student s2 = new Student("bb", 20,2);
Student s3 = new Student("cc", 10,3);
List list = Arrays.asList(s1, s2, s3);
 
//装成list
List ageList = list.stream().map(Student::getAge).collect(Collectors.toList()); // [10, 20, 10]
 
//转成set
Set ageSet = list.stream().map(Student::getAge).collect(Collectors.toSet()); // [20, 10]
 
//转成map,注:key不能相同,否则报错
Map studentMap = list.stream().collect(Collectors.toMap(Student::getName, Student::getAge)); // {cc=10, bb=20, aa=10}
 
//字符串分隔符连接
String joinName = list.stream().map(Student::getName).collect(Collectors.joining(",", "(", ")")); // (aa,bb,cc)
 
//聚合操作
//1.学生总数
Long count = list.stream().collect(Collectors.counting()); // 3
//2.最大年龄 (最小的minBy同理)
Integer maxAge = list.stream().map(Student::getAge).collect(Collectors.maxBy(Integer::compare)).get(); // 20
//3.所有人的年龄
Integer sumAge = list.stream().collect(Collectors.summingInt(Student::getAge)); // 40
//4.平均年龄
Double averageAge = list.stream().collect(Collectors.averagingDouble(Student::getAge)); // 13.333333333333334
// 带上以上所有方法
DoubleSummaryStatistics statistics = list.stream().collect(Collectors.summarizingDouble(Student::getAge));
System.out.println("count:" + statistics.getCount() + ",max:" + statistics.getMax() + ",sum:" + statistics.getSum() + ",average:" + statistics.getAverage());
 
//分组
Map> ageMap = list.stream().collect(Collectors.groupingBy(Student::getAge));
//多重分组,先根据类型分再根据年龄分
Map>> typeAgeMap = list.stream().collect(Collectors.groupingBy(Student::getType, Collectors.groupingBy(Student::getAge)));
 
//分区
//分成两部分,一部分大于10岁,一部分小于等于10岁
Map> partMap = list.stream().collect(Collectors.partitioningBy(v -> v.getAge() > 10));
 
//规约
Integer allAge = list.stream().map(Student::getAge).collect(Collectors.reducing(Integer::sum)).get(); //40

3 综合实例

最后来一个领域综合应用例子,已知交易员信息及其交易信息,需要求:

  • 找出2021年发生的所有交易,并按交易额排序
  • 交易员都在哪些不同的城市工作过
  • 查找来自某个城市的交易员,并按姓名排序
  • 返回所有交易员的姓名,按字母顺序排序
  • 查询某个城市的交易员的所有交易额
  • 所有交易中,最高的交易额是多少
  • 找到交易额的最小交易
  • 查询所有交易员在某年的交易额总和,并取前3名

代码示例:

定义交易员和交易值对象

public class Trader {
    private final String name;
    private final String city;

    public Trader(String name,String city){
        this.name = name;
        this.city = city;
    }

    public String getName() {
        return this.name;
    }

    public String getCity() {
        return this.city;
    }
}
public class Transaction {
    private final Trader trader;
    private final int year;
    private final int value;

    public Transaction(Trader trader,int year,int value){
        this.trader = trader;
        this.year = year;
        this.value = value;
    }

    public Trader getTrader() {
        return trader;
    }

    public int getYear() {
        return year;
    }

    public int getValue() {
        return value;
    }
}

求解问题代码如下:

public class StreamCompute {

        private Trader jack = new Trader("jack","hangzhou");
        private Trader jeff = new Trader("jeff","shenzhen");
        private Trader steven = new Trader("steven","shanghai");
        private Trader rose = new Trader("rose","beijing");
        private Trader akka = new Trader("akka","guangzhou");
        private Trader pony = new Trader("pony","shenzhen");
        private Trader walson = new Trader("walson","shanghai");


        private List transactions = Arrays.asList(
                new Transaction(jeff,2021,500),
                new Transaction(steven,2021,6000),
                new Transaction(steven,2022,700),
                new Transaction(rose,2020,300),
                new Transaction(rose,2021,900),
                new Transaction(rose,2022,5000),
                new Transaction(akka,2019,100),
                new Transaction(akka,2020,2200),
                new Transaction(akka,2021,3300),
                new Transaction(jack,2019,7400),
                new Transaction(jack,2020,15000),
                new Transaction(pony,2021,20000),
                new Transaction(pony,2020,3200),
                new Transaction(walson,2019,8500),
                new Transaction(walson,2021,9900),
                new Transaction(akka,2022,10000)
        );

        //找出2021年发生的所有交易,并按交易额排序
        public List tradeValue(){
            List sortList = transactions.stream()
                    .filter(transaction -> transaction.getYear()==2021)
                    .sorted(comparing(Transaction::getValue))
                    .collect(Collectors.toList());
            return sortList;
        }

        //交易员都在哪些不同的城市工作过
        public List workCity(){
                List cityList = transactions.stream()
                        .map(transaction -> transaction.getTrader().getCity())
                        .distinct()
                        .collect(Collectors.toList());
                return cityList;
        }

        //查找来自某个城市的交易员,并按姓名排序
        public List findTraderByCity(String city){
                List traderList = transactions.stream()
                        .map(Transaction::getTrader)
                        .filter(trader -> trader.getCity().equals(city))
                        .sorted(comparing(Trader::getName))
                        .collect(Collectors.toList());
                return traderList;
        }

        //返回所有交易员的姓名,按字母顺序排序
        public List traderName(){
                List traderName = transactions.stream()
                        .map(transaction -> transaction.getTrader().getName())
                        .distinct()
                        .sorted()
                        .collect(Collectors.toList());
                return traderName;
        }

        //查询某个城市的交易员的所有交易额
        public List findTraderValueByCity(String city){
                List traderValueList = transactions.stream()
                        .filter(transaction -> transaction.getTrader().getCity().equals(city))
                        .map(Transaction::getValue)
                        .collect(Collectors.toList());
                return traderValueList;
        }

        //所有交易中,最高的交易额是多少
        public Integer getMaxTradeValue(){
                Integer maxValue = transactions.stream()
                        .map(transaction -> transaction.getValue())
                        .reduce(Integer::max)
                        .get();
                return maxValue;
        }

        //找到交易额的最小交易,当然这里也可以用min方法
        public Transaction findMinValueTrans(){
                Transaction sortList = transactions.stream()
                        .reduce((t1,t2)->t1.getValue() findTraderValueTopN(int year){
                Map topMap = transactions.stream()
                        .filter(transaction -> transaction.getYear() == year)
                        .collect(Collectors.groupingBy(transaction -> transaction.getTrader().getName(),Collectors.summingInt(Transaction::getValue)))
                        .entrySet().stream()
                        .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
                        .limit(3)
                             .collect(Collectors.toMap(Map.Entry::getKey,Map.Entry::getValue));
                return topMap;
        }
}

你可能感兴趣的:(开发基础技术,java)