[toc]
Java SE8 流
综述
如果我们要迭代的话,一般是进行这样的操作:
ArrayList<Integer> contents = new ArrayList<Integer>(){{for(int i = 0; i < 100; i++){ add(i * i);}}};
//foreach迭代,进行计数操作
int count = 0;
for (int s : contents)
if (s % 3 == 0)
count++;
System.out.println("能被3整除的数字有" + count + "个");
在java SE8之后,我们可以这样:
ArrayList<Integer> contents = new ArrayList<Integer>(){{for(int i = 0; i < 100; i++){ add(i * i);}}};
//流操作
int count = (int) contents.stream().parallel().mapToInt(s -> s).filter(s -> s % 3 == 0).count();
System.out.println("能被3整除的数字有" + count + "个");
上面代码只是说明了流与for循环的不同,看起来似乎流的逼格更高,但流真正好处体现在哪里?
现在假设我们要设计一个学生管理系统,有一个需求:假设现在年纪里要评选奖学金,我们需要把全年级的学生根据综测排名,然后筛选出时长大于20的人,然后根据奖学金的数量选出人数,根据综测排名我们先分析一下步骤的实现:
- 输入所有学生信息
- 查重,将重复的学生信息删除
- 按绩点排序
- 选出前三个(假设奖学金只有三个)
- 按班分组
可以想象用for循环来实现需要多少代码和逻辑,但是用流操作就很简单
下面是代码实现:
Map<Integer, List<Student>> map = students.stream()
.parallel()
.distinct()
.sorted(Comparator.comparing(Student::getGPA).reversed())
.filter((n) -> n.getVolunteerTime() > 20)
.limit(3)
.collect(Collectors.groupingBy(Student::getClassID));
ps:我们可以发现流的版本比循环的版本更简洁,可读性更高。因为我们不必扫描整个代码去查找过滤和计数操作。方法名就可以直接告诉我们其意欲何为。而且,循环需要非常详细的指定操作的顺序,而流就可以以其想要的任何代码来调度这些操作。
- 流的操作不会修改其数据源,所有的操作都是生成一个新的流
- 流的操作是尽可能的惰性执行,这意味着直至需要其结果时,操作才会执行。
总的来说,流的创建分为三步:
- 创建一个流:
contents.stream()
- 将初始流转换为其他流的中间过程,可能包含很多步骤
- 终止操作。
//还有其他很多终止操作
.count();
第一步 转换流的常用方法
- 将集合转化成流
就像上面的代码,可以使用Connection接口的stream方法将任何集合转换成一个流
ArrayList<Integer> contents =...;
Stream<Integer> stream = contents.stream();
- 将数组转化成流
使用静态的Stream.of()方法:这个方法接受一个数组,当然也支持可变参数,所以我们可以直接传值:
Stream<Integer> stream = Stream.of(1,2,3,4,5,6);
- 创建一个空流
Stream<Integer> stream = Stream.empty();
- 创建一个无限流
- 使用静态方法generate(),下面是这个方法的定义
public static<T> Stream<T> generate(Supplier<T> s)
- 这个方法的参数是一个函数式接口,也就意味着我们可以用lambda表达式, 这段代码会生成一个无限流,流的内容全部都是1。
Stream<Integer> stream = Stream.generate(() -> 1);
- 或者下面这段代码,会生成一个无限随即数的流:
Stream<Double> stream = Stream.generate(Math::random);
- 生成自定义的无限流
使用iterate()方法:
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
第一个参数是种子,第二个参数的lambda表达式会对种子进行不断迭代:
Stream<Integer> stream = Stream.iterate(1, integer -> ++integer);
上面这段代码会生成1.2.3.4.5.6....的无限流。
中间的转换流操作
流的转换会产生一个新的流,它的元素派生自另一个流中的元素
filter(筛选)
filter()转换会产生一个流,它的元素于提供的条件相匹配。
Stream<Integer> stream = Stream.iterate(1, integer -> ++integer);
stream.filter((n) -> n % 2 == 0);
筛选之后会产生一个偶数无限流。++ps:有问题++
map(转变)
使用map()可以按照我们提供的方式转变流中的所有值,例如:
Stream<Integer> stream = Stream.iterate(1, integer -> ++integer);
stream.filter((n) -> n % 2 == 0).map((a) -> a + 1).forEach(System.out::println);
我们使用了map操作符使每一个元素+1。
flatMap(平摊)
map是指一一对应,而flatMap就是一对多:
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
先看下面这段代码:
/**
* flatMap
* 接受一个字符串,返回这个字符串的字符流。
*/
public static Stream<String> letters(String s) {
List<String> result = new ArrayList<>();
for (int i = 0;i < s.length();i++)
result.add(s.substring(i,i+1));
return result.stream();
}
然后我们就可以这么使用:
/**
*
*/
ArrayList<String> words = new ArrayList<String>() {{
add("Hello");
add("World");
}};
words.stream().flatMap(TextFileTEst::letters).forEach(System.out::println);
他将我们words集合内的每一个单词都解析成字母集合的流,然后将这些字母流依次汇聚成有一个流
map与flatMap的使用区别:map的lambda返回一个值,flatMap的lambda返回一个流
总的来说,flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
截取流
limit(),截取前n个数据
Stream<T> limit(long maxSize);
截取流中的的前maxSize个数据
Stream<Integer> stream = Stream.iterate(1, integer -> ++integer).limit(100);
上面这段代码会产生1-100的整数
skip(),跳过前n个数据
Stream<T> skip(long n);
与limit操作符相反,skip的作用是跳过前面的n个数据
Stream<Integer> stream = Stream.iterate(1, integer -> ++integer).limit(100).skip(20);
上面这段代码会产生20-100的整数
concat(),拼接两个流
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
Stream类的静态方法,参数接受两个流,拼接在一起
//拼接成一个流:{H,e,l,l,o,W,o,r,l,d}
Stream<String> stringStream = Stream.concat(letters("Hello"),letters("World"));
其他转化流
distinct(),查重
产生一个流,流中不包含重复的元素,也就是剔除原先流中重复的元素
sorted(),排序
如果流中的数据类实现了Comparable接口,我们可以直接使用sorted()进行排序(这个排序是按字符串的字典顺序进行排序)
//字典顺序进行排序
Stream<String> stringStream = Stream.concat(letters("Hello"),letters("World"))
.sorted();
也可以自定义排序规则(按照字符串的长度排序):
Stream<String> stringStream = Stream.concat(letters("Hello"),letters("World"))
.sorted((a1,a2) -> Integer.compare(a1.length(), a2.length()));
或者说可以更精简一些,使用Compator的静态方法:
Stream<String> stringStream = Stream.concat(letters("Hello"),letters("World"))
.sorted(Comparator.comparingInt(String::length));
peek(),遍历
peek()会产生一个新的流,元素与原先的流中的元素相同,但是在每次获取一个元素时,都会调用一个函。类似于前面使用的foreach,但是又有不同。
Stream<String> stringStream = Stream.concat(letters("Hello"),letters("World"))
.sorted(Comparator.comparingInt(String::length))
.peek(System.out::println);
上面的代码会对每一个数据进行输出
然后看一下这两个方法:
Stream<T> peek(Consumer<? super T> action);
void forEach(Consumer<? super T> action);
- foreach返回值是一个void,而peek还是返回一个流
- foreach是终结操作,foreach处理之后将不能对流进行其他处理
- peek通常用来对流进行调试
终结操作
约简
将流转换成可以在程序中使用的非流值
之前见过的count()就是约简,它会返回集合中的数据个数
类似的还有max,min等等。
Stream<Integer> stream = Stream.iterate(1, integer -> ++integer).limit(100)
.skip(20)
.filter((n) -> n % 2 == 0)
.map((a) -> a + 1);
Optional<Integer> optional = stream.max(Integer::compareTo);
System.out.println(optional.get());
其中Optional是对结果的包装类
下面有几个例子
- 找到单词列表中的第一个以q开头的单词
Optional<String> startWithQ = words.stream().filter(n -> n.startsWith("q")).findFirst();
- 是否所有单词都是q开头
boolean startWithQ = words.stream().allMatch(n -> n.startsWith("q"));
Optional
使用Optional
作用:避免产生null
这是一个包装了约简结果的类
这个类要么包装了类型T的对象(也就是值存在),要么没有包装任何对象(值不存在),
然后我们可以使用这个类的方法:
//找到q开头的单词,就返回这个单词,否则返回空字符串
String result = otionalString.orElse(" ");
//找到q开头的单词,就返回这个单词,否则抛出异常
String result = optionalString.orElseThrow(IllegalStateException::new);
//找到q开头的单词,就返回这个单词,否则不做任何事情
optionalString.ifPresent(System.out::print)
创建Optional
创建optional的两个常用方法:
Optional.of()
Optional.empty()
下面是创建一个结果Optional 的安全的inverse方法
public static Optional<Double> inverse(Double x) {
return x == 0 ? Optional.empty() : Optional.of(x);
}
ps:Optional不只是可以用于流的简约操作。自己写代码的过程也可以用,来防止出现空指针异常
收集
以下方法都是终结操作
foreach
当我们处理完流之后,通常会想要查看其元素,此时可以调用iterater方法,或者直接使用foreach:
Stream<Integer> stream = Stream.iterate(1, integer -> ++integer).limit(100)
.skip(20)
.filter((n) -> n % 2 == 0)
.map((a) -> a + 1);
stream.forEach(System.out::println);
foreach会迭代每一个元素并进行相应操作。
foreach是终结操作,这个方法返回void,意味着对流的操作终止。
toArray,转成数组
我们也可以将流数据转换成一个数组:
Integer[] a = (Integer[])stream.toArray();
这个方法会返回一个对象数组,需在进行一次强转,所以大多数情况下我们不使用空参数,而是传入数组的构造方法:
Integer[] a = stream.toArray(Integer[]::new);
collect
如果要将流转换成集合或其他目标,可以使用collect操作符,这个方法接受一个Collector接口的实例,而Collectors提供了我们使用的工厂方法,不需要让我们自己实现。
将流收集到列表中
List<Integer> n = stream.collect(Collectors.toList());
将流收集到集中
List<Integer> n = stream.collect(Collectors.toSet());
收集到特定种类的集合中
在toCollection方法中传入特定集合的构造器
ArrayList<Integer> n = stream.collect(Collectors.toCollection(ArrayList::new));
收集到map中
这里假设我们有一个employees集合,在对他进行流操作与筛选后,我们要把数据存到Map中,是我们能够通过ID找到Employee,那么可以用toMap,这个方法接受两个参数,也就是两个函数引元,分别产生Map的键和值,其中Function.identity()相当于lambda表达式:
n -> n
ArrayList<Employee> employees = new ArrayList<Employee>() {{
add(new Employee(1,"chen"));
add(new Employee(2,"Li"));
add(new Employee(3,"Hon"));
add(new Employee(4,"Fen"));
}};
Map<Integer,Employee> map = employees.stream().collect(Collectors.toMap(Employee::getID,Function.identity()));
如果键重复的话,会抛出异常
joining(),将所有元素连接成字符串
/*
输出"HelloWorld"
*/
ArrayList<String> words = new ArrayList<String>() {{
add("Hello");
add("World");
}};
String a = words.stream().sorted().collect(Collectors.joining());
System.out.println(a);
连接所有字符换,用分割符分隔
/*
输出"Hello,Hello,World,World"
*/
ArrayList<String> words = new ArrayList<String>() {{
add("Hello");
add("World");
add("Hello");
add("World");
}};
String a = words.stream().sorted().collect(Collectors.joining(","));
System.out.println(a);
对结果的数约简操作
如果想要将对结果进行数 的约简操作:总和,平均值,最大值,最小值,可以使用summarizing
ArrayList<String> words = new ArrayList<String>() {{
add("Hello");
add("World");
add("Hello");
add("World");
}};
IntSummaryStatistics intSummaryStatistics = words.stream()
.collect(Collectors.summarizingInt(String::length));
System.out.println("最大值:" + intSummaryStatistics.getMax() + "平均值:" + intSummaryStatistics.getAverage());
分组,groupingBy
ArrayList<Employee> words = new ArrayList<Employee>() {{
add(new Employee(1,"1组","chen"));
add(new Employee(2,"2组","Li"));
add(new Employee(3,"2组","Hon"));
add(new Employee(4,"3组","Fen"));
add(new Employee(1,"3组","chen"));
add(new Employee(2,"3组","Li"));
add(new Employee(3,"4组","Hon"));
add(new Employee(4,"4组","Fen"));
}};
//通过group分组
Map<String,List<Employee>> map = words.stream().collect(Collectors.groupingBy(Employee::getGroup));
System.out.println(map);
其中Employee::getGroup是分组函数
上面代码会被分成四组,下面是打印结果:
{
4组=[text_file.Employee@2f4d3709, text_file.Employee@4e50df2e],
3组=[text_file.Employee@1d81eb93, text_file.Employee@7291c18f, text_file.Employee@34a245ab],
2组=[text_file.Employee@7cc355be, text_file.Employee@6e8cf4c6],
1组=[text_file.Employee@12edcd21]
}
上面是根据getGroup方法的返回String进行分类,我们也可以根据返回boolean类型分成两类,例如按照ID是否大于2来分类:
Map<Boolean,List<Employee>>map = words.stream().collect(Collectors.groupingBy(n -> n.getID() > 2));
上面的代码会按照ID是否大于2把employees分成两组。
ps:当处理这种返回boolean的分组函数时,建议使用partitioningBy代替groupBy,这样会更加高效。
基本数据类型流
之前我们在操作整型流是,是这样使用的:
Stream<Integer> stream = numbers.tream();
很明显,将每个整数都包装到包装器对象中是很低效的,尤其是当我们的数据量很大时,对于其他基本类型来说也是一样,jdk中内置的基本类型流库:IntStream、Longtream、DoubleStream。用来直接储存基本类型值。
创建基本数据流
IntStream intStream = IntStream.of(1,2,3,4,5);
int[] array = ...;
IntStream intStream = Arrays.stream(array,0,4);
//开区间,0-99
IntStream intStream = IntStrema.range(0.100);
//闭区间,0-100
IntStream intStream = IntStrema.rangeClosed(0.100);
{
13061803=[Student{ID=2018214021, GPA=2.27, volunteerTime=30, classID=13061803}, Student{ID=2018214053, GPA=3.01, volunteerTime=25, classID=13061803}],
13061801=[Student{ID=2018214002, GPA=3.1, volunteerTime=36, classID=13061801}]
}
其他的用法就和上面介绍的一样了
网友评论