Stream是Java8中处理集合的关键抽象概念,你可以指定对集合的操作,但是真正执行时间由具体实现决定。
stream遵循的是“做什么”,而不是“怎么做”。使用迭代循环一开始就需要指定如何计算,失去了优化的机会。
2.1 从迭代器到stream操作
一个基本的操作:
List<String> list=new ArrayList<>();
list.add("asgjasiljgoa");
list.add("agsd");
list.add("agarhgadfhadfhahafhga");
long n=list.stream().filter(s->s.length()>10).count();
System.out.println(n);
集合调用stream方法得到Stream对象,filter方法进行元素选择,返回新的流对象,count方法是执行和终止流操作,返回元素个数。
Stream与集合的区别:
a,stream自己不存储元素,元素可能存储在底层集合中,或者根据需要产生。
b,stream操作符不改变源对象,返回一个新的stream。
c,操作符可能是延迟执行的,需要结果的时候才执行。
将上面的stream方法改变一下,即可使用并行流:
long n=list.parallelStream().filter(s->s.length()>10).count();
stream的使用:
a,创建一个Stream
b,指定将Stream转换到新流的操作
c,指定终止操作产生一个结果。该操作使前面的延迟操作立即执行。
2.2 创建Stream
对于集合来说,调用stream方法即可。
对于数组,使用静态方法Stream.of()或Arrays.stream()
int[] a =new int[]{1,2,3,4,5};
long x=Stream.of(a).count();
long y=Arrays.stream(a, 0, a.length).count();
of接收可变参数,可以一个个输入多个值,以上x=1,y=5。第二个方法将数组的一部分转换为stream
要创建一个空的Stream,
Stream<String> s=Stream.empty();
要创建无限流:
Stream.generate(()->"hehe");
Stream.iterate(1, e->e+1);
以上创建的是一个常量流和一个1,2,3,4...序列。
如想按照正则匹配切割一个字符串:返回切割后的流
Pattern.compile(regex).splitAsStream(content);
要将文件所有行转换为一个流:
try {
Files.lines(Paths.get(url));
} catch (IOException e1) {
e1.printStackTrace();
}
2.3 filter,map和flatMap方法
在前面代码中已经提到过filter方法,它接收Predicate类型参数,即一种过滤规则,根据测试条件返回true or false
map:对Stream中的元素执行某种映射操作,接收一个Function类型参数。如:
list.stream().map(String::toUpperCase)
它将里面的字母变成大写后返回新的流。
flatMap:和map类似,不过它会合并结果,即Stream<T>,如果T也是一个Stream,那么它会展开里面的stream然后合并成一个stream。如下:
static Stream<Character> fun(String s){
List<Character> list=new ArrayList<>();
for(Character c:s.toCharArray())
list.add(c);
return list.stream();
}
Stream<Stream<Character>> s1=list.stream().map(s->fun(s));
Stream<Character> s2=list.stream().flatMap(s->fun(s));
2.4 提取子流和组合流
以下产生一个流,使用limit,skip,concat,peek等方法对流进行操作:
static void fun2(double d) {
System.out.println(d);
}
Stream.generate(Math::random).limit(10).forEach(e -> fun2(e));
Stream.generate(Math::random).limit(10).skip(5).forEach(e -> fun2(e));
Stream.concat(Stream.generate(Math::random).limit(5),Stream.generate(Math::random)
.limit(5)).forEach(e->fun2(e));
Stream.iterate(1,e->e*2).peek(e->System.out.println(e)).limit(10).toArray();
limit:返回一个限制元素个数的新流
skip:返回一个忽略流中前面若干元素的新流
concat:静态方法,将2个流拼接。注意第一个流不要是无限流,否则永远不会返回。
peek:返回和原始流相同的新流,但是每次获取一个元素时,都会调用其中的方法,这样可以测试无限流是否被延迟处理。在上面的代码中,如果不使用limit方法,那么会一直输出0。如果你通过流执行某些操作,但你还想继续进行其他操作,使用peek是正确的做法。
2.5 有状态的转换
所谓有状态,就是指流的操作对于后面的元素进行操作时,需要考虑前面的元素,无状态则跟前面的元素无关。比如去重操作,必须知道前面是否出现过相同的元素。
distinct方法去除流中重复的元素到1个为止
Stream.of("hehe","haha","hehe").distinct().peek(System.out::println).count();
以上输出hehe,haha。
sotred方法对流进行排序,Collections.sort方法排序是会改变原来的集合的,而流的操作都是返回新流。
Stream.of("hehe","haha","heihei").sorted().peek(System.out::println).count();
Stream.of("hehe1","haha","hehe11").sorted(Comparator.comparing(String::length)
.reversed()).peek(System.out::println).count();
以上也可以提供自定义比较器进行比较。
2.6 简单的聚合方法
前面提到过count方法,它会返回流中元素总数(long类型)。
还有max,min方法,分别返回流中最大,最小值(Optional类型)。
List<String> list = new ArrayList<>();
list.add("asgjasiljgoa");
list.add("agsd");
list.add("agarhgadfhadfhahafhga");
Optional<String> op1=list.stream().max(String::compareToIgnoreCase);
Optional<String> op2=list.stream().min(String::compareToIgnoreCase);
System.out.println(op1.get()+" "+op2.get());
在java8中,Optional是一种更好的表示缺少返回值的情况。即使没有返回一个元素给它,也不会抛出异常。
findFirst:返回流中第一个元素。通常与filter方法结合使用
String s=Stream.of("hehe","haha","heihei").filter(e->e.startsWith("ha"))
.findFirst().get();
System.out.println(s);//haha
findAny:返回所有元素。
anyMatch:接收参数同filter,即Predicate类型。返回类型boolean。判断是否有符合条件的元素存在。
boolean b=list.parallelStream().anyMatch(e->e.startsWith("h"));
System.out.println(b);//true
allMatch:如果所有元素都满足规则,返回true。
noneMatch:没有一个元素满足规则时返回true。
以上带有any,all,none的方法都可以并行执行。
2.7 Optional类型
Optional<T>或者是一个对T类型对象的封装,或者表示不是任何对象。它比一般指向T类型的引用更加安全,因为它不会返回null
如果直接调用它的get方法,如果不存在值时,会抛出NoSuchElementException异常
当然你可以这么使用:
Optional<String> op1=list.stream().max(String::compareToIgnoreCase);
if(op1.isPresent())
System.out.println(op1.get());
但这不会比非null判断更简单。
2.7.1 使用Optional值
ifPresent方法另一种形式是接收一个函数,如果存在值,将值传递给函数,否则不进行任何操作。
上面代码可以变成这样:
Optional<String> op1=list.stream().max(String::compareToIgnoreCase);
op1.ifPresent(System.out::println);
op1.ifPresent(list::add);
如果想对结果进行处理,可以使用map方法:
Optional<Boolean> option= op1.map(list::add);
option可能有3种值:封装true或者false的Optional,或者一个空的可选值。
当然,有存在时则进行操作的方法,也有不存在时进行操作的方法orElse,orElseGet,orElseThrow:
Optional<String> op1=list.stream().max(String::compareToIgnoreCase);
String s=op1.orElse("");
op1.orElseGet(()->System.getProperty("driver.className"));
op1.orElseThrow(NoSuchElementException::new);
以上第一个方法提供默认值,第2,3个方法Supplier类型参数,即返回相应类型的方法。
2.7.2 创建可选值
使用Optional.of或者Optional.empty创建一个Optional对象,还可以使用Optional.ofNullable创建可选值。如果值存在,跟调用of一样,不存在跟empty一样。
2.7.3 使用flatMap组合可选函数
根据前面的内容知道,如果函数返回的类型与调用者持有的类型不同,使用map是不恰当的,特别是返回与调用者相同类型的时候,此时使用flatMap可以将中间结果展开然后合并。
假设一个函数f返回Optional<T>,类型T里面有另一个函数g返回Optional<U>,如下:
如果调用s.f().g() 得到的是Optional<Optional<U>>
package chapter2;
import java.util.Optional;
public class Demo2 {
static Optional<Double> inverse(double x) {
return x == 0 ? Optional.empty() : Optional.of(1.0 / x);
}
static Optional<Double> sqrt(double x) {
return x < 0 ? Optional.empty() : Optional.of(Math.sqrt(x));
}
public static void main(String[] args) {
Optional<Double> op1=inverse(-4.0).flatMap(Demo2::sqrt);
Optional<Double> op = Optional.of(-4.0).flatMap(Demo2::inverse).flatMap(Demo2::sqrt);
op.ifPresent(System.out::println);
}
}
以上实现了返回类型为Optional的函数的组合。
2.8 聚合操作
如下,通过reduce方法求一个整数流中所有元素的和。其中第二种方式提供了初始sum值,即使流为空也会返回Integer类型值
Stream<Integer> stream=Stream.iterate(1, e->2+e).limit(100);
stream.reduce((x,y)->x+y).ifPresent(System.out::println);
Integer i=stream.reduce(0,(x,y)->x+y);
System.out.println(i);
如果想对String类型的流求所有字符串的长度总和,不能使用上面的简单形式,因为你用lambda表达式求出2个字符串的长度和为int类型,并不是String类型。所以要通过另一种方式:
Stream<String> stream3=Stream.of("ajgag","agafgag","asdfgageh");
//参数:初始值+函数1+函数2
//函数要求 an associative, non-interfering, statelessfunction
//第一个单词意思是满足结合律
//第2,3个单词是互不干扰的和无状态的意思。说的都是无状态性
//函数1:某线程对流部分的计算结果
//函数2:将各线程结果合并
int l=stream3.reduce(0,(total,word)->total+word.length(),(t1,t2)->t1+t2);
int l2=stream3.mapToInt(String::length).sum();
System.out.println(l);
上面第二种方式是映射到数字流,更为简单。
2.9 收集结果
可以使用Iterator方法返回一个迭代器,也可以使用toArray方法返回Object数组,如果想得到带类型的数组,还是可以使用数组构造器引用方式:
String[] str=stream3.toArray(String[]::new);
要将结果收集起来,使用collect方法。使用方式:
a,一个能创建目标类型实例的方法。如HashSet的构造函数
b,一个将元素添加到目标中的方法。
c,一个将2个目标对象合并的方法。如HashSet的addAll方法
HashSet<String> set1=stream3.collect(HashSet::new,HashSet::add,HashSet::addAll);
其实Collectors提供了这3个方法。所以可以这么做:
List<String> list=stream3.collect(Collectors.toList());//toSet
//特定类型
Set<String> set2=stream3.collect(Collectors.toCollection(TreeSet::new));
//将字符串全部拼接
String result=stream3.collect(Collectors.joining());
//使用,进行拼接
String result2=stream3.collect(Collectors.joining(","));
//元素不是String时
String result3=stream3.map(Object::toString).collect(Collectors.joining(","));
对于一些统计操作,我们可以生成一个统计工具来实现一些统计方法:
IntSummaryStatistics intSummary=stream3.collect(Collectors.summarizingInt(String::length));
intSummary.getSum();
intSummary.getAverage();
intSummary.getMax();
intSummary.getMin();
这样可以获取最大最小值,平均值,总和等信息。
2.10 将结果收集到map中
假设有一个Person类,现在想将Stream<Person>收集到map中,如下:
List<Person> list = new ArrayList<>();
list.add(new Person(1,"heihei"));
list.add(new Person(2,"hehe"));
list.add(new Person(3,"haha"));
Stream<Person> s=list.stream();
Map<Integer, String> map1=s.collect(Collectors.toMap(Person::getId, Person::getName));
Map<Integer, Person> map2=s.collect(Collectors.toMap(Person::getId, Function.identity()));
以上第二种方式使用Person自身作为value。
下面是地区语言和国家信息构成的map。
Stream<Locale> locales=Stream.of(Locale.getAvailableLocales());
//第3个参数解决key冲突,可选,不选有冲突时抛出异常
//第4个参数提供具体类型,可选,不提供使用hashmap
Map<String,String> map3=locales.collect(Collectors.toMap(
l->l.getDisplayLanguage(),
l->l.getDisplayLanguage(l),
(oldvalue,newvalue)->oldvalue,
TreeMap::new));
//key冲突时,添加新的语言到原来的集合中
Map<String,Set<String>> map4=locales.collect(Collectors.toMap(
l->l.getDisplayCountry(),
l->Collections.singleton(l.getDisplayLanguage()),
(a,b)->{
Set<String> set=new HashSet<>(a);
set.addAll(b);
return set;
}
));
另外,每个toMap对应并发情况都有一个toConcurrentMap方法。
2.11 分组和分片
使用groupingBy和partitioningBy可以简化上面操作,实现数据分类
partitioningBy将数据二分,要么是符合条件,要么不符合
Map<String, List<Locale>> map5 = locales.collect(Collectors.groupingBy(Locale::getCountry));
Map<Boolean, List<Locale>> map6 = locales.collect(Collectors.partitioningBy(l -> l.getLanguage().equals("en")));
groupingBy方法还可以提供第二个参数,类型为Collector。
以下第二个参数使用的是Collectors的静态方法,使用了静态导入。
Map<String, Set<Locale>> map7 = locales.collect(Collectors.groupingBy(
Locale::getCountry, toSet()));
//每个国家的语言(Locale)数
Map<String, Long> map8 =
locales.collect(Collectors.groupingBy(Locale::getCountry, counting()));
除了counting,还有summing(Int/Double/Long),maxBy/minBy
//每个州下所有城市的总人数
Map<String,Integer> map9=cities.collect(Collectors.groupingBy(
City::getState,
summingInt(City::getPopulation)));
//每个州人口最多的城市
Map<String,City> map10=cities.collect(Collectors.groupingBy(
City::getState,
maxBy(Comparator.comparing(City::getPopulation))));
mapping方法将一个函数应用到Collector结果上,并且需要另一个收集器来处理结果。
//根据城市所属州分组,对城市名字长度取最大返回
Map<String,Optional<String>> map11=cities.collect(Collectors.groupingBy(
City::getState,
mapping(
City::getName,
maxBy(Comparator.comparing(String::length))
)
));
有了mapping后我们可以更简单的获取每个国家对应的语言:
Map<String,Set<String>> map12=locales.collect(groupingBy(
l->l.getDisplayCountry(),
mapping(l->l.getDisplayLanguage(),
toSet())
));
如果grouping或mapping函数返回int,double或long类型,可以将元素收集到一个summary statistics对象中
Map<String,IntSummaryStatistics> map13=cities.collect(groupingBy(
City::getState,
summarizingInt(City::getPopulation)
));
以下通过2种方式将流中字符串分类后拼接:
Map<String,String> map14=cities.collect(groupingBy(
City::getState,
reducing("",City::getName,(s,t)->s.length()==0?t:s+","+t)
));
Map<String,String> map15=cities.collect(groupingBy(
City::getState,
mapping(City::getName,
joining(","))
));
以上reducing有3种格式:一个参数(二元运算),2个参数(加一个identity),3个参数(再加一个mapper)。有了Stream的reduce方法,Collectors的reducing方法很少会用到了。
2.12 原始类型流
为了高效,不可能老是将基本类型包装,所以提供了IntStream等,它们直接存储基本类型值,不进行包装。boolean,byte,char,short,int使用IntStream,float类型使用DoubleStream
IntStream i1=IntStream.of(1, 2, 3, 4, 5);
IntStream i2=Arrays.stream(new int[]{1,2,3,4,5},0,3);
IntStream.range(0, 100);//0~99
IntStream.rangeClosed(0, 100);//0~100
Random rand=new Random();
IntStream i3=rand.ints();
以上是IntStream的创建,其他类似。也可以将其转换成包装类的Stream:
Stream<Integer> s1=IntStream.range(0, 100).boxed();
前面提到过,Stream<String>类型可以使用如下转换成IntStream:
s.mapToInt(String::length);
原始类型流和对象流上调用方法的区别:
a,toArray方法返回一个原始类型数组
b,产生Optional结果的方法会返回OptionalInt/Long/Double等,他们没有get方法而是getAsInt等方法
c,方法sum,average,max,min可以直接使用
d,summaryStatistics方法会产生一个IntSummaryStatistics
2.13 并行流
parallel方法可以将一个Stream转换成一个并行流。
当不考虑有序时,一些操作会得到优化。
调用Stream.unordered方法可以不关心顺序。对于distinct、limit等可以加快执行顺序。
Collectors.groupingByConcurrent天然是无序的,因为需要考虑效率。
2.14 函数式接口
就一个图。