Java8两大新特征,一是支持 lambda 表达式,二是 Stream API。在使用 Stream API 之前,最好有 lambda表达式的基础,如果不太清楚,可以看我的另一篇博客lambda表达式。
在编写代码的过程中,我们经常要对集合或数组数据进行操作,而有了 Stream API,我们能够非常轻松的对集合、数据进行映射、过滤、查找等操作,使得我们能够像操作数据库一样的操作集合。
Stream 本身并不存储元素,它并不改变源数据,每次操作都会形成一个新的流,并且只有执行了Stream的终止操作,中间的过滤、查找等操作才会执行。
集合注重存储,Stream注重计算。集合是基于内存层面的,而Stream是基于CPU的。
Stream 操作的步骤- 创建
- 中间操作
- 终止操作
只有执行了终止操作,中间操作才会执行。这也是 Stream 的延迟执行的体现。
Stream 的创建 通过集合创建default Stream stream() {} 返回一个顺序流
@Test
public void test() {
List list = new ArrayList<>();
list.add("H");
list.add("E");
list.add("l");
list.add("l");
list.add("O");
Stream stream = list.stream();
}
通过数组创建
通过 Arrays 的静态方法进行创建
public static Stream stream(T[] array) {}
@Test
public void test() {
Integer[] arr = {1,2,3,4,5,6,7,8,9};
Stream stream = Arrays.stream(arr);
}
顺便一提,在 Arrays 中,帮我们重载了此方法,根据数组类型不同,返回不同的类型的流。
- public static IntStream stream(int[] array)
- public static LongStream stream(long[] array)
- public static DoubleStream stream(double[] array)
@Test
public void test() {
int[] ints = {1,2,3};
IntStream intStream = Arrays.stream(ints);
double[] doubles = {1.1,1.2,1.3};
DoubleStream doubleStream = Arrays.stream(doubles);
long[] longs = {1L,2L,3L};
LongStream stream = Arrays.stream(longs);
}
使用 of 方法创建
使用 Stream 的静态方法进行创建
public static < T > Stream< T > of(T… values) {}
@Test
public void test() {
Stream integerStream = Stream.of(1, 2, 3, 4, 5, 6);
}
创建无限流
可以使用 Stream 的静态方法 Stream.iterate() 和 Stream.generate(),创建无限流。
- public static Stream iterate(final T seed, final UnaryOperator< T > f) {} 此方法用于迭代
- public static Stream generate(Supplier< T > s) {} 此方法用于生成
为什么说是无限流呢? 因为在创建无限流时,如果没有使用终止操作,那么这个流的中间操作,会一直执行。
generate(Supplier< T > s) 方法用于生成元素,方法传入一个提供者接口,是一个函数式接口,我们可以用 lambda 和方法引用来简化代码。
举例:无限生成随机数
@Test
public void test() {
// generate
Stream generate = Stream.generate(Math::random);
generate.forEach(System.out::println);
}
iterate(final T seed, final UnaryOperator< T > f) 方法用于迭代元素,第一个参数是一个种子数,也就是元素的起始值。第二个参数是一个功能型函数式接口,我们也可以使用 lambda 和方法引用来简化代码。如不太清楚功能型函数式接口,请移步到 另一篇博客。
举例:无限迭代生成的元素
@Test
public void test() {
// iterator
Stream iterate = Stream.iterate(0, ele -> ele + 2);
iterate.forEach(System.out::println);
}
想要让无限流停止,我们可以对流进行一些中间操作,使其停止即可。
Stream 的中间操作 筛选与切片filter(Predicate p) ,接收断言型函数式接口,对流中的元素进行过滤。
@Test
public void test() {
List list = new ArrayList<>();
list.add("春天");
list.add("春风");
list.add("春色");
list.add("春意");
list.add("秋天");
list.stream().filter(e-> e.contains("天")).forEach(System.out::println);
}
distinct(),对流进行去重。
@Test
public void test() {
List list = new ArrayList<>();
list.add("春天");
list.add("春天");
list.add("春色");
list.add("春意");
list.add("秋天");
list.stream().distinct().forEach(System.out::println);
}
limit(long maxSize) ,限制流中的元素数量。可以使用 limit方法来终止无限流。
@Test
public void test() {
Stream generate = Stream.generate(Math::random);
generate.limit(10).forEach(System.out::println);
}
skip(long n) 跳过流中的 n 个元素。
@Test
public void test() {
List list = new ArrayList<>();
list.add("春天");
list.add("春天");
list.add("春色");
list.add("春意");
list.add("秋天");
list.stream().skip(2L).forEach(System.out::println);
}
映射
map(Function f) ; 接收一个功能型函数式接口的实现类,该函数式接口中的抽象方法会被用到流中的每一个元素上。通常使用 map(Function f) 方法对流中的数据进行处理、提取、转换成其他对象等操作。
举例:使用 map 将流中的字符串映射成大写的,然后通过 filter 过滤字符串长度小于5的进行遍历输出。
@Test
public void test() {
List list = Arrays.asList("hello", "world", "ni", "hao", "shi", "jie", "!");
list.stream()
.map(String::toUpperCase)
.filter(e -> e.length() < 5)
.forEach(System.out::println);
}
flatMap(Function f); flatMap 的功能和 map 类似,都是将方法应用到流中的每个元素上。但是有一点区别,当流中的元素还是一个流时,map 会将流中的流看做一个对象处理。而flatMap会将流中的流拆开,将方法也应用到流中的流的各个元素上。
类比: List 的 add() 和 addAll() 方法
@Test
public void test() {
List list1 = new ArrayList();
list1.add(1);
list1.add(2);
list1.add(3);
List list2 = new ArrayList();
list2.add(4);
list2.add(5);
list2.add(6);
list1.add(list2);
System.out.println(list1.size()); // 4
list1.addAll(list2);
System.out.println(list1.size()); // 7
}
add方法如果添加的还是一个集合时,则会将集合当做一个对象添加进去。相当于 map。
addAll方法如果添加的还是一个集合时,则会将集合拆开把元素一一加到 list 中。相当于 flatMap。
举个例子。在映射时,将流中的字符串全部转换成字符流。如果是用 map 映射,则将映射后的元素看做一个整体,然后执行后序流程。如果是用 flatMap 映射,则流中的元素如果还是流,那么会将流拆开,再执行后序操作。
@Test
public void test() {
List list = new ArrayList<>();
list.add("hello");
list.add(" hi");
System.out.println("*****直接map*****");
Stream> streamStream1 = list.stream().map(this::strToCharacter);
streamStream1.forEach(System.out::println);
System.out.println("*****两个forEach等同于 flatMap *****");
Stream> streamStream2 = list.stream().map(this::strToCharacter);
streamStream2.forEach(strStream -> {
strStream.forEach(System.out::print);
});
System.out.println();
System.out.println("*****直接 flatMap *****");
Stream characterStream = list.stream().flatMap(this::strToCharacter);
characterStream.forEach(System.out::print);
}
public Stream strToCharacter(String str) {
ArrayList list = new ArrayList<>();
for (Character c : str.toCharArray()) {
list.add(c);
}
return list.stream();
}
结果入下:
流的排序就比较简单了。在 Java 中,涉及到排序,无外乎就两种,一种自然排序,实现 Comparable 接口,一种定制排序,实现 Comparator 接口。
- Stream< T > sorted(); 使用自然排序
- Stream< T > sorted(Comparator super T> comparator); 使用定制排序
这里就以定制排序举例了。
先整写测试数据:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private int id;
private String name;
private int age;
private double salary;
}
//---------------------------------
public class EmployeeData {
public static List getEmployees(){
List list = new ArrayList<>();
list.add(new Employee(1001, "马化腾", 34, 6000.38));
list.add(new Employee(1001, "朱化腾", 34, 7000.38));
list.add(new Employee(1002, "马云", 12, 9876.12));
list.add(new Employee(1002, "郝云", 15, 9999.12));
list.add(new Employee(1003, "刘强东", 33, 3000.82));
list.add(new Employee(1004, "雷军", 26, 7657.37));
list.add(new Employee(1005, "李彦宏", 65, 5555.32));
list.add(new Employee(1006, "比尔盖茨", 42, 9500.43));
list.add(new Employee(1007, "任正非", 26, 4333.32));
list.add(new Employee(1008, "扎克伯格", 35, 2500.32));
return list;
}
}
先根据 id 排序,如果 id 相同,再根据工资排序。
@Test
public void test() {
List employees = EmployeeData.getEmployees();
employees.stream().sorted((e1, e2) -> {
int compare = Integer.compare(e1.getId(), e2.getId());
if (compare != 0) {
return compare;
} else {
return Double.compare(e1.getSalary(), e2.getSalary());
}
}).forEach(System.out::println);
}
Stream 的终止操作
匹配与查找
| 方法 | 描述 |
|---|---|
| allMatch(Predicate p) | 检查流中所有元素是否匹配自定义的条件 |
| anyMatch(Predicate p) | 检查是否至少匹配一个元素 |
| noneMatch(Predicate p) | 返回第一个元素 |
| count() | 返回流中元素总数 |
| max(Comparator c) | 返回流中最小值 |
| forEach(Consumer c) | 内部迭代 |
匹配与查找比较简单,这里就举第一个例子。
@Test
public void test() {
List stringList = Arrays.asList("hello","world","hi","word");
boolean b = stringList.stream().allMatch(str -> str.length() > 5);
System.out.println(b); // false
}
归约
| 方法 | 描述 |
|---|---|
| reduce(T iden, BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 T,第一个参数是初始值 |
| reduce(BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 Optional |
reduce 会帮我们遍历流中的元素。
@Test
public void test() {
List stringList = Arrays.asList("hello","world","hi","word");
String reduce1 = stringList.stream().reduce("", (s1, s2) -> String.valueOf(s1.length() + s2.length()));
System.out.println(reduce1);
List integerList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Optional 是 Java8 新增的防止空指针的类,通过 get() 方法可以获取到包装的值
Optional reduce2 = integerList.stream().reduce((i1, i2) -> {
return i1 + i2;
});
System.out.println(reduce2.get());
// 上述代码等同于下面代码
Optional reduce3 = integerList.stream().reduce(Integer::sum);
System.out.println(reduce3.get());
}
这里 String 流输出的值是5。 因为字符串默认值为 “”,那么第一次就是0+5,那么 s1 就变成了 “5”; 第二次去就是 “5”.length() + “world”.length(),再String.valueOf()一下,就变成了 “6”,那么永远就是 1 + 下一个字符串的长度了。最终输出 5。
收集在开发中也是非常实用的终止操作。常常用来对流进行中间操作最后收集生成集合。
| 方法 | 描述 |
|---|---|
| collect(Collector c) | 将流转换为其他形式。参数一个 Collector接口的实现,用于给Stream中元素做汇总的方法。 |
Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。
在开发中我们往往不去手写 Collector 的实现类,而用 Java 8 新增的 Collectors 中的静态方法来创建 Collector 的实现类。Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法如下表所示:
| 方法 | 返回类型 | 作用 | 举例 |
|---|---|---|---|
| toList | List | 把流中元素收集到List | List emps = list.stream().collect(Collectors.toList()); |
| toSet | Set | 把流中元素收集到Set | Set emps= list.stream().collect(Collectors.toSet()); |
| toCollection | Collection | 把流中元素收集到创建的集合 | Collection emps = list.stream().collect(Collectors.toCollection(ArrayList::new)); |
| counting | Long | 计算流中元素的个数 | long count = list.stream().collect(Collectors.counting()); |
| summingInt | Integer | 对流中元素的整数属性求和 | int total=list.stream().collect(Collectors.summingInt(Employee::getSalary)); |
| averagingInt | Double | 计算流中元素Integer属性的平均值 | double avg = list.stream().collect(Collectors.averagingInt(Employee::getSalary)); |
| summarizingInt | IntSummaryStatistics | 收集流中Integer属性的统计值。 | int SummaryStatisticsiss= list.stream().collect(Collectors.summarizingInt(Employee::getSalary)); |
| joining | String | 连接流中每个字符串 | String str= list.stream().map(Employee::getName).collect(Collectors.joining()); |
| maxBy | Optional< T > | 根据比较器选择最大值 | Optional< Employee >max= list.stream().collect(Collectors.maxBy(comparingInt(Employee::getSalary))); |
| minBy | Optional< T > | 根据比较器选择最小值 | Optional min = list.stream().collect(Collectors.minBy(comparingInt(Employee::getSalary))); |
| reducing | 归约产生的类型 | 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值 | int total=list.stream().collect(Collectors.reducing(0, Employee::getSalar, Integer::sum)); |
| collectingAndThen | 转换函数返回的类型 | 包裹另一个收集器,对其结果转换函数 | int how= list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size)); |
| groupingBy | Map | 根据某属性值对流分组,属性为K,结果为V | Map |
| partitioningBy | Map | 根据true或false进行分区 | Map |
这里就拿 groupingBy() 方法举例。
假如我要将员工根据 id 进行分组,保存到 map 中,key 是员工的 id,value 就是这些员工,那么就可以这么操作。
@Test
public void test() {
List employees = EmployeeData.getEmployees();
Map> collect = employees.stream().collect(Collectors.groupingBy(Employee::getId));
System.out.println(collect);
}
总结
Stream API 还是很实用的,最好能够将其掌握,不仅开发效率高,还装x。



