简介
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<String> list = new ArrayList<>();
list.add("H");
list.add("E");
list.add("l");
list.add("l");
list.add("O");
Stream<String> 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<Integer> 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<Integer> 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<Double> 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<Integer> iterate = Stream.iterate(0, ele -> ele + 2);
iterate.forEach(System.out::println);
}
想要让无限流停止,我们可以对流进行一些中间操作,使其停止即可。
Stream 的中间操作
筛选与切片
filter(Predicate p) ,接收断言型函数式接口,对流中的元素进行过滤。
@Test
public void test() {
List<String> 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<String> 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<Double> generate = Stream.generate(Math::random);
generate.limit(10).forEach(System.out::println);
}
skip(long n) 跳过流中的 n 个元素。
@Test
public void test() {
List<String> 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<String> 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<String> list = new ArrayList<>();
list.add("hello");
list.add(" hi");
System.out.println("*****直接map*****");
Stream<Stream<Character>> streamStream1 = list.stream().map(this::strToCharacter);
streamStream1.forEach(System.out::println);
System.out.println("*****两个forEach等同于 flatMap *****");
Stream<Stream<Character>> streamStream2 = list.stream().map(this::strToCharacter);
streamStream2.forEach(strStream -> {
strStream.forEach(System.out::print);
});
System.out.println();
System.out.println("*****直接 flatMap *****");
Stream<Character> characterStream = list.stream().flatMap(this::strToCharacter);
characterStream.forEach(System.out::print);
}
/**
* 将字符串转换成字符流
* @param str 输入的字符串
* @return 字符流
*/
public Stream<Character> strToCharacter(String str) {
ArrayList<Character> 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<Employee> getEmployees(){
List<Employee> 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<Employee> 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);
}
再举一个例子,比如我现在有两个集合,其中两个集合里面的元素相同,但是顺序不同。现在要让一个集合中元素的顺序和另一个集合保持一致,也可以使用排序完成。
public class SortTest {
static List<Integer> orderIdList = new ArrayList<>();
static List<Integer> randomIdList = new ArrayList<>();
static {
orderIdList.add(7);
orderIdList.add(8);
orderIdList.add(9);
orderIdList.add(4);
orderIdList.add(5);
orderIdList.add(6);
orderIdList.add(1);
orderIdList.add(2);
orderIdList.add(3);
randomIdList.add(1);
randomIdList.add(4);
randomIdList.add(7);
randomIdList.add(2);
randomIdList.add(5);
randomIdList.add(8);
randomIdList.add(3);
randomIdList.add(6);
randomIdList.add(9);
}
@Test
public void testSortCollection() {
List<Integer> collect = randomIdList.stream().sorted((e1, e2) -> {
Integer orderIndex1 = orderIdList.indexOf(e1);
Integer orderIndex2 = orderIdList.indexOf(e2);
return orderIndex1 - orderIndex2;
}).collect(Collectors.toList());
System.out.println(collect);
}
}
可以看到, randomIdList 集合中元素的顺序和 orderIdList 保持一致了。
如果想要倒序,则在 orderIndex1 - orderIndex2 前一个负号就行,这里就不演示了。
不过上述流的操作有点臃肿,其实一行代码就可以搞定。
// 这里使用 Comparator.comparingInt 传入的 int 值是 randomIdList 中元素在 orderIdList 中的下标
// 这样 Comparator 会根据 randomIdList 中元素在 orderIdList 中的下标进行排序,这样就实现了和 orderIdList 顺序一致
// 再说细一点,我现在 randomIdList 中 1 4 7 2 5 8 3 6 9 元素在 orderIdList 中的下标分别是 6 3 0 7 4 1 8 5 2
// 这样 Comparator 就会根据这些下标排序,这样元素顺序又会和 orderIdList 一致了
// 升序下标对应就是 0 1 2 3 4 5 6 7 8 9,元素的值对应就是 7 8 9 4 5 6 1 2 3
// 降序下标对应就是 9 8 7 6 5 4 3 2 1,元素的值对应就是 3 2 1 6 5 4 9 8 7
// 打印输出验证下
// 根据下标升序
System.out.println(randomIdList.stream().sorted(Comparator.comparingInt(t -> orderIdList.indexOf(t))).collect(Collectors.toList()));
// 根据下标降序
System.out.println(randomIdList.stream().sorted(Comparator.comparingInt(t -> -orderIdList.indexOf(t))).collect(Collectors.toList()));
Stream 的终止操作
匹配与查找
方法 | 描述 |
---|---|
allMatch(Predicate p) | 检查流中所有元素是否匹配自定义的条件 |
anyMatch(Predicate p) | 检查是否至少匹配一个元素 |
noneMatch(Predicate p) | 返回第一个元素 |
count() | 返回流中元素总数 |
max(Comparator c) | 返回流中最小值 |
forEach(Consumer c) | 内部迭代 |
匹配与查找比较简单,这里就举第一个例子。
@Test
public void test() {
List<String> 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<String> 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<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Optional 是 Java8 新增的防止空指针的类,通过 get() 方法可以获取到包装的值
Optional<Integer> reduce2 = integerList.stream().reduce((i1, i2) -> {
return i1 + i2;
});
System.out.println(reduce2.get());
// 上述代码等同于下面代码
Optional<Integer> 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, List< T >> | 根据某属性值对流分组,属性为K,结果为V | Map<Emp.Status, List< Employee >> map= list.stream().collect(Collectors.groupingBy(Employee::getStatus)); |
partitioningBy | Map<Boolean, List< T >> | 根据true或false进行分区 | Map<Boolean,List> vd = list.stream().collect(Collectors.partitioningBy(Employee::getManage)); |
这里就拿 groupingBy() 方法举例。
假如我要将员工根据 id 进行分组,保存到 map 中,key 是员工的 id,value 就是这些员工,那么就可以这么操作。
@Test
public void test() {
List<Employee> employees = EmployeeData.getEmployees();
Map<Integer, List<Employee>> collect = employees.stream().collect(Collectors.groupingBy(Employee::getId));
System.out.println(collect);
}
总结
Stream API 还是很实用的,最好能够将其掌握,不仅开发效率高,还装x。