这篇文章我们一起来学习java8最重要的新特性之一,强大的StreamAPI,在本文中的代码示例中我们会大量使用lambda表达式,如果对lambda表达式还不太熟悉,可以看一下上一篇文章。
java8中Java8中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则是 Stream API(java.util.stream.*)。Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。
使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
一、首先我们来思考一个问题,什么是Stream(流)?
流是一个数据渠道,用于操作数据源(集合,数组等)所生成的元素序列,总之,集合主要是数据,流主要是计算。
二、Stream操作的三个步骤:
通过一个数据源获取一个流,例如,List中的stream()方法可以直接返回一个Stream对象。
我们需要对流中的数据进行的操作,比如循环处理(map),过滤(filter)等
流都是惰性求值的,这个我们在后面会讲到,需要进行一个终止操作,这样才会返回中间操作后的数据。
如下图:
注意:
Stream只是一个计算通道,自己不会存储元素;
Stream不会改变源对象,相反,他们会返回一个新的Stream对象。
Stream操作是延时的,只有在执行终止操作时才会执行。
三、创建Steam的方式:
1、由Collection子类(List,Set)来创建流:
java8扩展了Collection接口,提供了stream(返回顺序流)和parallelStream(返回并行流)两个方法。并行流我们后面在详细讨论,示例代码:
@Test
public void test(){
List list = Arrays.asList("a","b","c");
Stream stram = list.stream();
Stream parallelSteam = list.parallelStream();
}
2、由数据来创建流:
数组可以通过Arrays工具类的stream方法来获得一个Steam对象,示例代码:
public void test2(){
String[] strArr = new String[]{"a","b","c"};
Stream stram = Arrays.stream(strArr);
//还有许多重载形式的方法,可以返回带类型的Stream,例如:
IntStream stram2 = Arrays.stream(new int []{1,2,3,4,5});
}
3、通过具体值来创建流:
通过Stream的静态方法Stream.of(T...values)可以创建一个流,它可以接受任意个值,代码示例:
@Test
public void test3() {
Stream s = Stream.of("1","2",3,new String[]{"a", "b", "c"});
}
4、通过函数来创建流(无限流)
通过Stream.iterate()和Stream.generate()方法可以创建无限流,什么是无限流,我们来看一下代码示例就明白了:
@Test
public void test4() {
//Stream.iterate方法第一个方法表示开始值得,第二个参数需要提供一个一元操作函数,我们用lambda表达式传递给它
Stream stream1 = Stream.iterate(0, (x) -> x + 2);
stream1.forEach(System.out::println); //输出的是0,2,4,6,8,10....将会一直循环下去,永远不停息
//Stream.generate需要一个供给型函数接口
Stream stream2 = Stream.generate(() -> 1);
stream2.forEach(System.out::println); //输出无数个1,将会一直循环下去,永远不停息
}
备注:实际运用中,我们肯定不会生成一个无限流,除非你想要死循环,我们会结合Stream的终止操作,如limit来获取有指定个数元素的流:
@Test
public void test5(){
//我们从0开始获取前50个偶数
Stream stream1 = Stream.iterate(0, (x) -> x + 2).limit(50);
stream1.forEach(System.out::println);//输出0,2,4,6,8。。。。98
}
四、Stream的中间操作:
Stream可以进行一系列的流水线式的中间操作,除非流水线上触发终止操作,否则,这些中间操作不会进行任何处理,而在终止操作时一次性处理,这个我们叫做Stream的惰性求值。
记住,中间操作不管做多少次,都不会改变原来的流,只会返回一个新的流;
Stream的中间操作可以分为以下几类:
方法 描述 filter(Predicate d) 接受一个断言型函数,对Stream流中的元素进行处理,过滤掉不满足条件的元素 distinct 筛选元素,通过Stream元素中的hasCode和equals方法来去除重复元素 limit(long maxSize) 截断流,使元素不超过manSize指定的数量 skip(Long n) 跳过元素,返回一个扔掉了前n个元素的流,若流中的元素不足n个,则会返回一个空流
方法 描述 map(Function f) 接受一个函数型接口作为参数,该函数会对流中的每个元素进行处理,返回处理后的流 mapToDouble(ToDoubleFunction f) 接口一个函数型接口作为参数,该函数会对流中的每个元素进行处理,并返回一个Double值,最终得到一个Stream
方法 描述 sorted 返回一个新流,流中的元素按照自然排序进行排序 sorted(Comparator comp) 返回一个新流,并且Comparator指定的排序方式进行排序
中间操作练习:
/**
* Stream的中间操作练习
*/
public class StreamTest2 {
List emps = Arrays.asList(
new Employee("张三", 18, 6666.66),
new Employee("李四", 20, 7777.77),
new Employee("王五", 36, 8888.88),
new Employee("田七", 55, 11111.11),
new Employee("赵六", 55, 9999.99),
new Employee("赵六", 45, 12222.22)
);
/**
* 筛选与切片 filter, distinct limit skip
*/
@Test
public void test1() {
//1.过滤掉年龄小于25的员工
emps.stream().filter((e) -> e.getAge() > 25).forEach(System.out::println);
//2.过滤掉姓名重复的员工
emps.stream().distinct().forEach(System.out::println);
//3.获取前三名员工
emps.stream().limit(3).forEach(System.out::println);
//4.获取第三名以后的员工
emps.stream().skip(3).forEach(System.out::println);
//5.先获取前3名员工,再获取其中年龄大于25的员工。(中间操作可以任意次)
emps.stream().limit(3).filter(e -> e.getAge() > 25);
}
/**
* 映射操作 map, mapToDouble, mapToInt, mapToLong, flatMap
*/
@Test
public void test2() {
//1. 获取所有员工的姓名
emps.stream().map(e -> e.getName()).forEach(System.out::println);
//2. 获取所有员工的工资,这里工资是Double类型,我们可以用mapToDouble方法
emps.stream().mapToDouble(e -> e.getSalary()).forEach(System.out::println);
//3. 获取所有员工的年龄,用mapToInt方法
emps.stream().mapToInt(e -> e.getAge()).forEach(System.out::println);
//4. 将员工年龄转换成Long型并输出,PS:这里就算不用longValue系统也会自动将getAge转换成Long类型
emps.stream().mapToLong(e -> e.getAge().longValue()).forEach(System.out::println);
/**
* 5.将所有员工的 姓名,年龄,工资转换成一个流并返回
* 首先我们用map方法来处理,这种方式返回的是六个Stream对象,数据结构可以类似于:
* [{"张三",18, 6666.66},{"李四",20, 7777.77}, {"王五", 36, 8888.88}, {"赵六", 24, 9999.99}, {"田七", 55, 11111.11}, {"赵六", 45, 12222.22}]
* 然后我们用flatMap方法来处理,返回的是一个Stream对象,将所有元素连接到了一起,数据结构类似于:
* ["张三",18, 6666.66,"李四",20, 7777.77, "王五", 36, 8888.88, "赵六", 24, 9999.99, "田七", 55, 11111.11, "赵六", 45, 12222.22]
*/
emps.stream().map(e -> {
return Stream.of(e.getName(), e.getAge(), e.getSalary());
}).forEach(System.out::println);
emps.stream().flatMap(e -> {
return Stream.of(e.getName(), e.getAge(), e.getSalary());
}).forEach(System.out::println);
}
/**
* 排序操作 sorted
*/
@Test
public void test3() {
//1.按照自然排序,注意,需要进行自然排序则对象必须实现Comparable接口
emps.stream().sorted().forEach(System.out::println);
//2.按照给定规则进行排序,(按照工资高低进行排序)
emps.stream().sorted((x,y) -> Double.compare(x.getSalary(),y.getSalary())).forEach(System.out::println);
}
}
五、Stream的终止操作:
Stream的终止操作用来获取一系列流水线操作的最终结果,这个结果可以是任何值,例如boolean,List,Integer甚至可以是void,终止操作也分为以下几大类:
方法 描述 allMatch(Predicate p) 传入一个断言型函数,对流中所有的元素进行判断,如果都满足返回true,否则返回false。 anyMatch(Predicate p) 传入一个断言型函数,对流中所有的元素进行判断,只要有一个满足条件就返回true,都不满足返回false。 noneMatch(Predicate p) 所有条件都不满足,返回true,否则返回false。 findFirst() 返回流中的第一个元素。 findAny() 返回流中的任意一个元素。 count() 返回流中元素的个数。 max(Comparator c) 按照给定的排序规则进行排序后,返回流中最大值的元素 min(Comparator c) 按照给定的排序规则进行排序后,返回流中最小值的元素 forEach(Consumer c) 内部迭代。
方法 描述 reduce(T iden, BinaryOperator bo) 可以将流中的元素反复结合起来,得到一个值,返回T reduce(BinaryOperator bo) 可以将流中的元素反复结合起来,得到一个值,返回Optional。(Optional我们后面再聊)
方法 描述 collect(Collector c) 将流中的元素转换成其他形式,接受一个Collector接口的实现,用于处理Stream流中的元素,将流转换成其他形式的对象。
collector接口中方法的实现决定了如何对流执行收集操作(如收集到List,Set,Map)。java8中的Collectors类提供了很多静态方法,可以非常方便的创建常用的收集器实例,具体方法与实例:
方法 | 返回类型 | 作用 |
---|---|---|
toList | List |
将流中的元素收集到List中,例如:list.stream.collect(Collectors.toList()); |
toSet | Set |
将流中的元素收集到set中:Set |
toCollection | Collection |
将流中的元素收集到创建的集合:list.stream().collect(Collectors.toCollection(ArrayList::new)) |
counting | Integer | 计算流中元素的个数:long count = list.stream().collect(Collectors.counting()); |
summingInt | Long | 对流中的整数属性进行求和:inttotal=list.stream().collect(Collectors.summingInt(Employee::getSalary)); |
averagingInt | Double | 计算流中元素的平均值:doubleavg= list.stream().collect(Collectors.averagingInt(Employee::getSalary)); |
summarizingInt | IntSummaryStatistics | 返回一个IntSummaryStatistics,可以通过这个对象获取统计值,如平均值:IntSummaryStatistics iss=list.stream().collect(Collectors.summarizingInt(Employee::getSalary)) |
joining | String | 连接流中的每个字符串:String str= list.stream().map(Employee::getName).collect(Collectors.joining()); |
maxBy | Optional |
根据比较器选择最大值:Optional |
minBy | Optional |
根据比较器选择最小值:Optional |
reducing | 规约产生的类型 | 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值。Integer totalAge = emps.stream().collect(Collectors.reducing(0, Employee::getAge, Integer::sum)); |
colectingAndThen | 转换函数返回的类型 | 包裹一个收集器,对其结果进行转换:int inthow= emps.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size)); |
groupingBy | Map |
根据某属性对结果进行分组,属性为K,结果为V:Map |
partitioningBy | Map |
根据true或false进行分区:Map |
终止操作练习:
/**
* Stream的终止操作练习
*/
public class StreamTest3 {
List emps = Arrays.asList(
new Employee("张三", 17, 6666.66),
new Employee("李四", 20, 7777.77),
new Employee("王五", 36, 8888.88),
new Employee("田七", 55, 11111.11),
new Employee("赵六", 55, 9999.99),
new Employee("赵六", 45, 12222.22)
);
/**
* 终止操作:查找与匹配 allMatch, anyMatch, noneMatch, findFirst, findAny, count, max, min ,forEach
*/
@Test
public void test1() {
//1.查看是否有员工年龄是否都大于18
boolean flag1 = emps.stream().allMatch(e -> e.getAge() > 18);
System.out.println(flag1); // false
//2.先去掉张三,然后再判断所有员工年龄是否都大于18
boolean flag2 = emps.stream().filter(e -> !"张三".equals(e.getName())).allMatch(e -> e.getAge() > 18);
System.out.println(flag2); //true
//3.是否有员工年龄大于50
boolean flag3 = emps.stream().filter(e -> !"张三".equals(e.getName())).anyMatch(e -> e.getAge() > 50);
System.out.println(flag3); //true
//4.没有员工的年龄大于50?
boolean flag4 = emps.stream().filter(e -> !"张三".equals(e.getName())).noneMatch(e -> e.getAge() > 50);
System.out.println(flag4); //false
//5.先按照年龄进行排序,然后返回第一个员工。optional是java8用来包装可能出现空指针的对象的对象
Optional op1 = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge())).findFirst();
System.out.println(op1.get()); //Employee{name='张三', age=17, salary=6666.66}
//6. 查找任意一名员工的姓名,当使用顺序流时,返回的是第一个对象,当使用并行流时,会随机返回一名员工的姓名
Optional op2 = emps.parallelStream().map(e -> e.getName()).findAny();
System.out.println(op2.get()); //会随机获取一名员工
//7. 查询员工人数
Long count = emps.stream().count();
System.out.println(count);
//8.查询员工工资最大的员工信息。PS: 这个也可以通过先按照工资排序,然后取第一个元素来实现
Optional maxSalary = emps.stream().max((x, y) -> Double.compare(x.getSalary(), y.getSalary()));
System.out.println(maxSalary.get());
//9.查询员工最小年龄
Optional minAge = emps.stream().max((x, y) -> -Integer.compare(x.getAge(), y.getAge()));
System.out.println(minAge.get());
//10.循环输出所有员工的信息
emps.stream().forEach(System.out::println);
}
/**
* 终止操作:规约 reduce
* 规约操作两个重载方法的区别是:
* 一个未指定起始值,有可能返回null,所以返回的是一个Optional对象
* 一个指定了起始值,不可能返回null,所以返回值可以确定是一个Employee
*/
@Test
public void test2() {
//1.将所有员工的名字加上 -> 下一个员工的名字: 如 张三 -> 李四
Optional op1 = emps.stream().reduce((x,y) -> {x.setName(x.getName() + "->" + y.getName()); return x;});
System.out.println(op1.get().getName()); //张三->李四->王五->田七->赵六->赵六
//2.将所有员工的名字加上 -> 下一个员工的名字,并且开始以王八开始;
// PS:测试时,请将例子1注释掉,不然会影响emps对象
Employee emp = emps.stream()
.reduce(new Employee("王八", 65, 8888.88)
, (x,y) -> {
x.setName(x.getName() + "->" + y.getName());
return x;
});
System.out.println(emp.getName()); //王八->张三->李四->王五->田七->赵六->赵六
}
/**
* 终止操作:收集
*/
@Test
public void test3(){
//1.按年龄排序后收集成一个list并返回
List list = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge()))
.collect(Collectors.toList());
list.forEach(System.out::println);
//2.按工资高低排序后收集成一个Set返回
Set set = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge()))
.collect(Collectors.toSet());
set.forEach(System.out::println);
//3.按工资排序后收集到指定的集合中,这里指定LinkedList
LinkedList linkedList = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge()))
.collect(Collectors.toCollection(LinkedList::new));
linkedList.forEach(System.out::println);
//4.计算流中元素的个数:
long count = emps.stream().collect(Collectors.counting());
System.out.println(count);
//5.对所有员工的年龄求和:
int inttotal = emps.stream().collect(Collectors.summingInt(Employee::getAge));
System.out.println(inttotal);
//6.计算所有员工工资的平均值:
Double doubleavg= emps.stream().collect(Collectors.averagingDouble(Employee::getSalary));
System.out.println(doubleavg);
//7.返回一个IntSummaryStatistics,可以通过这个对象获取统计值,如平均值:
IntSummaryStatistics iss =emps.stream().collect(Collectors.summarizingInt(Employee::getAge));
System.out.println(iss.getAverage());
System.out.println(iss.getMax());
System.out.println(iss.getMin());
//8.连接所有员工的名字:
String str= emps.stream().map(Employee::getName).collect(Collectors.joining());
System.out.println(str);
//9.相当于先按照工资进行排序,再取出排在第一位的员工
Optional min = emps.stream().collect(Collectors.minBy((x ,y) -> Double.compare(x.getSalary(), y.getSalary())));
System.out.println(min);
//10.相当于先按照工资进行排序,再取出排在最后一位的员工
Optional max = emps.stream().collect(Collectors.maxBy((x ,y) -> Double.compare(x.getSalary(), y.getSalary())));
System.out.println(max);
//11.从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值。
Integer totalAge = emps.stream().collect(Collectors.reducing(0, Employee::getAge, Integer::sum));
System.out.println(totalAge);
//12.包裹一个收集器,对其结果进行转换:
int inthow = emps.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
System.out.println(inthow);
//13.根据某属性对结果进行分组,属性为K,结果为V:
Map> kv = emps.stream().collect(Collectors.groupingBy(Employee::getName));
System.out.println(emps);
//14.根据true或false进行分区,年龄大于30的分在true区,小于30的分在false区
Map> vd = emps.stream().collect(Collectors.partitioningBy(e -> e.getAge() > 30));
System.out.println(vd);
}
}
总结:
Stream的总结其实就是一句话,记住Stream操作的三个步骤,创建流 -> 一系列中间操作 -> 终止操作拿到返回结果。
PS:上面练习的代码放在了我的git仓库中,感兴趣的可以down下来再练练:https://github.com/caishi13202/jdk8.git