JavaSE 8的流库
什么是流?
流是一种比集合更高级的概念,它是指定计算的数据视图。
在我看来,我们处理集合中的数据时,往往需要进行迭代操作,先遍历,再对每个元素进行某项操作。但是流可以优化这个解决办法,只关心需要的是什么,而不关心它的实现过程。
【1】从迭代到流操作
我们先比较一下,迭代和流操作的具体实现。比如,要计算某本书中长单词(大于12个字母就算)的数量。
String contents = new String(Files.readAllBytes(Paths.get("..\\alice30.txt")),StandardCharsets.UTF_8);
List<String> words = Arrays.asList(contents.split("\\PL+"));
先看迭代操作:
long count =0;
for(String w : words){
if(w.length()>12){
count++;
}
}
我们再看看如果使用流操作:
long counts = words.stream().filter(w->w.length()>12).count();
对比来看,流操作比迭代操作要简单,因为它不用关心计数和查找符合条件的单词是怎样实现的,只关心我们要得到的是什么。
还有一个方法叫paralleStream,这个方法是产生并行流,在处理并发时,就可以让流库以并行的方式来过滤和计数:
long count = words.parallelStream().filter(w->w.length()>12).count();
流的特点:
①流不存储数据
②流的操作不会修改其数据源,原本的集合或流中的数据是不会被改变的。
③流是尽可能的惰性执行的,就是说到了非要有结果的时候,它才会执行。
流的操作分为3个步骤:
①创建流
②指定将初始流转换为其他流的中间操作
③终结流
我们看一个简单的例子,用stream方法或者parallelStream方法创建流,用filter方法转换流,用count方法终结流。
package com.zxyy.stream.test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
public class CountLongWordTest {
public static void main(String[] args) throws IOException {
String contents = new String(Files.readAllBytes(Paths.get("C:\\Users\\弓长小月\\Desktop\\Test Forlder\\alice30.txt")),StandardCharsets.UTF_8);
List<String> words = Arrays.asList(contents.split("\\PL+"));
//普通迭代操作
long count = 0;
for(String w : words) {
if(w.length()>12) {
count++;
}
}
System.out.println("Iterator : "+count);
//创建普通流来实现
count = words.stream().filter(w->w.length()>12).count();
System.out.println("stream : "+count);
//创建并行流
count = words.parallelStream().filter(w->w.length()>12).count();
System.out.println("parallelStream : "+count);
}
}
【2】流的创建
Collection接口的stream方法可以将任何集合转换成流。如果有一个数组就使用静态的Stream.of方法。
我们来看看创建不同流的方式:
//将集合转换成流
List<String> words = ...;
Stream<String> s1 = words.stream();
//将数组转换成流
String[] array = ...;
Stream<String> s2 = Stream.of(array);
//of方法还可以选择截取数组来创建流 of(array,from,to):包含from,不包含to
Stream<String> s3 = Stream.of(array,0,5);
同样的还可以创建不包含任何元素的流:
//创建一个不包含任何元素的流
Stream<String> s4 = Stream.empty();
创建无限流有两种方法:
①调用Stream.generate()方法
②调用Stream.iterate()方法
package com.zxyy.stream.test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CreatingStreams {
public static void main(String[] args) throws IOException {
//对于String类型的数组,用Stream.of方法来创建流
Stream<String> words = Stream.of(new String(Files.readAllBytes(Paths.get("C:\\\\Users\\\\弓长小月\\\\Desktop\\\\Test Forlder\\\\alice30.txt")),StandardCharsets.UTF_8).split("\\PL+"));
show("words",words);
Stream<String> song = Stream.of("gently","down","the","stream");
show("song",song);
//创建一个不包含任何元素的流
Stream<String> empty = Stream.empty();
show("empty",empty);
//创建一个String类型的无限流
Stream<String> echos = Stream.generate(()->"Echo");
show("echos",echos);
//创建一个产生随机数的无限流
Stream<Double> randoms = Stream.generate(Math::random);
show("randoms",randoms);
//创建一个无限序列,开始为0,每次加1
//iterate方法可以给出一个种子值,以及一个函数
Stream<Integer> integers = Stream.iterate(0, n->n+1);
show("integers",integers);
//Pattern类有一个splitAsStream方法来按照给出的正则表达式分割一个CharSequence对象
Stream<String> wordsAnotherWay = Pattern.compile("\\PL+").splitAsStream(new String(Files.readAllBytes(Paths.get("C:\\Users\\弓长小月\\Desktop\\Test Forlder\\alice30.txt")),StandardCharsets.UTF_8));
show("wordsAnotherWay",wordsAnotherWay);
//按照alice30.txt的行来创建一个流
Stream<String> lines = Files.lines(Paths.get("C:\\Users\\弓长小月\\Desktop\\Test Forlder\\alice30.txt"));
show("lines",lines);
}
public static <T> void show(String title,Stream<T> stream) {
final int SIZE = 10;
List<T> firstElements = stream.limit(10).collect(Collectors.toList());
System.out.print(title+" : ");
for(int i=0;i<firstElements.size();i++) {
if(i>0) {
System.out.print(",");
}
if(i<SIZE) {
System.out.print(firstElements.get(i));
}else {
System.out.print("...");
}
}
System.out.println();
}
}
【3】filter,map和flatMap方法
在上面我们看到了如何创建流,现在再来看看如何转换流。流的转换是会产生一个新的流,它的元素来自于原来的流中。看看API文档中对这3个方法的介绍:
filter方法转换一个流,它的元素与给定的条件相匹配;
map方法转换一个流,它的作用是按照给定的函数来转换流中的值;
flatMap方法转换一个流,它的作用是将当前流中所有元素产生的结果连接到一起
【4】抽取子流和连接流
抽取子流有两个方法:①limit()方法,②skip()方法
//limit方法获取前n个元素
//例如获取前100个元素的流
Stream<Double> randoms = Stream.generate(Math::random).limit(100);
//skip方法是丢弃前n个元素,保留n之后的
//获取100以后的元素
Stream<Integer> integers = Stream.iterate(0,x->x+1).skip(100);
连接流方法:contact();
//contact方法连接两个流
//假设letters("Hello")的结果是["H","e","l","l","o"]的流
Stream<String> combined = Stream.contact(letters("Hello"),letters("World"));
注意:连接两个流时,前一个流不能是一个无限流。
【5】其他流的转换
方法名 | 方法的作用 | 示例 |
---|---|---|
distinct() | 产生一个没有重复元素的流,并且按照原来的流的顺序排列 | Stream<String> uniqueWords = Stream.of("Mary","Mary","Mary","Jack"); |
sorted() | 产生一个流,它的元素是当前流中的所有元素按照顺序排列,该方法要求元素是实现了Comparable接口的类的实例 | Stream<String> stream = words.stream.sorted(Comparator.comparing(String::length)); |
peek() | 产生一个流,它的流与原来的流中元素相同,但是每次获取一个元素,都会调用一个函数,多作调试用 | Stream<Double> stream = Stream.iterate(1.0,n->n*2).peek(e->System.out.println("Fetching "+e)).limit(20); |
【6】简单约简
我们之前已经看到了创建流和转换流的方法,流的操作还剩下最后最后一步,那就是终结操作。前面看到的count方法是其中一种,下面我们看看其他几种简单的终结操作:
①max和min
//使用min方法终结流,得到流的最小值
Optional<String> smallest = words.min(String::compareToIgnoreCase);
System.out.println("smallest = "+smallest);
//使用max方法终结流,得到最大值
Optional<String> largest = words.max(String::compareToIgnoreCase);
System.out.println("largest = "+largest);
②findFirst和findAny
//findFirst方法返回与之匹配的非空集合的第一个值
//注意,如果没有任何值与之匹配就返回一个空的Optional值
Optional<String> first = words.filter(s->s.startWith("Q").findFirst();
//findAny方法返回任意匹配的任意一个值
//注意,如果没有任何值与之匹配就返回一个空的Optional值
Optional<String> any = words.filter(s->s.startWith("Q").findAny();
③anymatch,allmatch和nonematch
//如果想知道有任意元素与给出条件匹配用anymatch,返回的是boolean值
boolean anymatch = words.parallel().anymatch(s->s.contains("e"));
//查看是否所有元素都与给出条件匹配用allmatch,返回值是boolean值
boolean allmatch = words.parallel().allmatch(s->s.contains("e"));
//查看是否没有任何元素与给出条件匹配,返回值是boolean值
boolean nonematch = words.parallel().nonematch(s->s.contains("e"));
【7】Optional类型
Optional<T>对象是一种包装器对象,要么包装了T类型对象,要么没有包装任何对象。Optional<T>类型被当作一种更安全的方式,用来代替类型T的引用,这种引用要么引用某个对象,要么为null。
(1)如何使用Optional值
Optional在值存在的时候,才会使用这个值;在值不存在时,它会有一个可替代物。我们先看看Optional类的三个简单方法:
//orElse()方法,如果存在值,就用原值,如果不存在值就使用一个值来替代它
//我们这里用空字符串来替代没有值的情况
String result = optionalString.orElse("");
//orElseGet()方法,后面可以接一个lambda表达式,表示如果不存在值的话就用表达式的值来替代它
String result = optionalString.orElseGet(()->Locale.getDefault().getDisplayName());
//orElseThrow()方法,如果不存在值,就在方法中抛出对应的异常
String result = optionalString.orElseThrow(IllegalStateException::new);
还有一个ifPresent()方法,ifPresent(v->Process v)该方法会接收一个函数,如果在值存在的情况下,它会将值传递给该函数,但是如果在值不存在的情况下,它不会做任何事。
//比如我们想要在值存在的情况下,就把值添加到一个集中
optionalValue.ifPresent(v->result.add(v));
或者
optionalValue.ifPresent(result::add);
注意:ifPresent方法并不返回任何值。
如果想要得到结果,就需要使用map方法
Optional<Boolean> added = optionalValue.map(result::add);
//这种情况下,added值可能有三种情况,
//当optionalValue存在的时候可能是true或者false
//当optionalValue不存在的时候,就是一个空的Optional
(2)不适合使用Optional值的方式
不适合使用Optional值的情况有两种:
①需要用到get()方法,因为Optional值在不存在的情况下,使用get方法会抛出NoSuchElementException
Optional<T> optionalValue = ...;
optionalValue.get().somemethod();
//这种方式并不比下面安全
T value = ...;
value.somemethod();
②需要用到isPresent()方法作非空判断时
if(optionalValue.isPresent()){
optionalValue.get().somemethod();
}
//这种方式也并不比下面容易处理
if(value!=null){
value.somemethod();
}
(3)创建Optional值
三种方式创建Optional值:
①empty()方法:创建一个空的Optional值
Optional<String> empty = Optional.empty();
②of(T value)方法:value不能为null,如果为null,会抛出NullPointerException
if(value!=null){
Optional<String> os = Optional.of(value);
}
③ofNullable(T value)方法:这个方法相当于empty方法和of方法的桥梁,在value为null时,它会返回一个空Optional;在value不为null时,它会返回of方法的返回值
Optional<String> ofNullable = Optional.ofNullable(value);
(4)用flatMap来构建Optional值的函数
假设你有一个可以产生Optional<T>对象的方法f,并且目标类型T有一个可以产生Optional<U>的方法g,如果他们都是普通方法,那么你可以使用s.f().g()来调用。但是现在s.f()的返回类型是Optional<T>,而不是类型T,无法调用g方法,这个时候我们就可以利用flatMap方法来组合这两个方法。
Optional<U> result = s.f().flatMap(T::g);
接下来,我们来写个例子,使用一下OptionalAPI中的方法:
package com.zxyy.stream.bean;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
public class OptionalTest {
public static void main(String[] args) throws IOException {
String contents = new String(Files.readAllBytes(Paths.get("C:\\Users\\弓长小月\\Desktop\\Test Forlder\\alice30.txt")),StandardCharsets.UTF_8);
List<String> wordList = Arrays.asList(contents.split("\\PL+"));
Optional<String> optionalValue = wordList.stream().filter(s->s.contains("fred")).findFirst();
System.out.println(optionalValue.orElse("No word")+" contains fred");
Optional<String> optionalString = Optional.empty();
String results = optionalString.orElse("N/A");
System.out.println("results : "+results);
results = optionalValue.orElseGet(()->Locale.getDefault().getDisplayName());
System.out.println("results : "+results);
try {
results = optionalValue.orElseThrow(IllegalStateException::new);
System.out.println("results : "+results);
} catch (Throwable t) {
t.printStackTrace();
}
optionalValue = wordList.stream().filter(s->s.contains("red")).findFirst();
optionalValue.ifPresent(s->System.out.println(s+" contains red"));
Set<String> result = new HashSet<String>();
optionalValue.ifPresent(result::add);
Optional<Boolean> added = optionalValue.map(result::add);
System.out.println(added);
System.out.println(inverse(4.0).flatMap(OptionalTest::squreRoot));
System.out.println(inverse(0.0).flatMap(OptionalTest::squreRoot));
System.out.println(inverse(-1.0).flatMap(OptionalTest::squreRoot));
Optional<Double> result2 = Optional.of(0.0625).flatMap(OptionalTest::inverse).flatMap(OptionalTest::squreRoot);
System.out.println("result2 = "+result2);
}
public static Optional<Double> inverse(Double x){
return x==0?Optional.empty():Optional.of(1/x);
}
public static Optional<Double> squreRoot(Double x){
return x<0?Optional.empty():Optional.of(Math.sqrt(x));
}
}