在了解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
相比传统的实现,策略模式实现更加灵活扩展,但这种方式也有不爽的地方,每次定义一个过滤策略的时候,我们都要单独创建一个过滤类。那么使用匿名内部类是不是能够优化我们书写的代码呢?
接下来,我们就使用匿名内部类来实现对员工信息的过滤。
先来看过滤年龄大于或者等于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
这就是本文的主角,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);
太简洁了!
Stream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。Stream可以由数组或集合创建,对流的操作分为两种:
另外,Stream有几个特性:
集合和流二者的主要区别有两点:
1.集合是一个内存中的数据结构,每个元素都需要先算出来才能添加到集合中,是可以增删改的。
而流是概念上固定的数据结构,不能增删改,其元素是按需计算的。流就像一个延迟创建的集合,只有在消费时才会计算。
2.遍历数据的迭代方式不一样,集合需要用户去做迭代,但流则使用内部迭代,用户无需只要细节,只要给出一个函数想干什么就行了。
本质上,集合讲的是数据,流讲的是计算。
Collection
下的 stream()
和 parallelStream()
方法List list = new ArrayList<>();
Stream stream = list.stream(); //获取一个顺序流
Stream parallelStream = list.parallelStream(); //获取一个并行流
stream()
方法,将数组转成流Integer[] nums = new Integer[10];
Stream stream = Arrays.stream(nums);
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);
BufferedReader.lines()
方法,将每行内容转成流BufferedReader reader = new BufferedReader(new FileReader("F:\\test_stream.txt"));
Stream lineStream = reader.lines();
lineStream.forEach(System.out::println);
Pattern.splitAsStream()
方法,将字符串分隔成流Pattern pattern = Pattern.compile(",");
Stream stringStream = pattern.splitAsStream("a,b,c,d");
stringStream.forEach(System.out::println);
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);
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
:第一次执行时,accumulator函数的第一个参数为流中的第一个元素,第二个参数为流中元素的第二个元素;第二次执行时,第一个参数为第一次函数执行的结果,第二个参数为流中的第三个元素;依次类推。
T reduce(T identity, BinaryOperator
:流程跟上面一样,只是第一次执行时,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个抽象方法:
Supplier supplier()
:创建一个结果容器A
BiConsumer accumulator()
:消费型接口,第一个参数为容器A,第二个参数为流中元素T。
BinaryOperator combiner()
:函数接口,该参数的作用跟上一个方法(reduce)中的combiner参数一样,将并行流中各个子进程的运行结果(accumulator函数操作后的容器A)进行合并。
Function finisher()
:函数式接口,参数为:容器A,返回类型为:collect方法最终想要的结果R。
Set
:返回一个不可变的Set集合,用来表明该Collector的特征。有以下三个特征:
CONCURRENT
:表示此收集器支持并发。(官方文档还有其他描述,暂时没去探索,故不作过多翻译)
UNORDERED
:表示该收集操作不会保留流中元素原有的顺序。
IDENTITY_FINISH
:表示finisher参数只是标识而已,可忽略。
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
最后来一个领域综合应用例子,已知交易员信息及其交易信息,需要求:
代码示例:
定义交易员和交易值对象
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;
}
}