java–Stream
JDK8-特性-Stream
Stream是以一种类似于SQL语句的方式直接的对Java进行运算和表达;
将要处理的元素视为一种流,在管道中运输,可以在管道的节点上进行处理。
Stream of elements----->|filter±–>|sorted±—>|map±->|collect–>|
上述操作转为Java代码为:
List<Integer> transactionsIds = widgets.stream().filter(b->b.getColor()==RED)
.sorted((x,y)->x.getWegiht()-y.getWegiht())
.mapToInt(Widget::getWegiht)
.sum();
一、Stream流的一些定义
Stream流属于一种操作数据的手段,
Pipelining:中间操作都会返回流对象本身,这样多个操作可以串联称为管道,
内部迭代:过去使用Iterator或者for-each的方式,都是显式的在集合外部迭代,而Stream提供了内部迭代的方式,通过访问者模式实现。
1.生成流
JDK8采用两种方式:
1.Stream()-为集合创建串行流
2.parallelStream()-为集合创建并行流
如:
List<String> strings= Arrays.asList("abc","ab","abd","bv");
List<String> filtered = strings.stream().filter(String->!string.isEmpty()).collect(Collectors.toList);
2.forEach
stream提供了内部的forEach来迭代流中的每个数据,以下代码片段使用forEach输出了10个随机数
Random random = new Random();
random.ints().limit(10).forEach(System.out::println);
3.map
用于映射每个元素到对应的结果,下列diamagnetic输出了元素对应的平方数
List<Integer> numbers = Arrays.asList(3,2,2,7,3,5);
List<Integer> squareList = numbers.stream().map(i->i*i).distinct().collect(Collector.toList);
4.filter
用于设置条件过滤,下列代码过滤出字符串
List<String> strings = Arrays.asList("ad","asList","sss","efg");
long count = Strings.stream().filter(string -> string.isEmpty()).count();
5.limit
获取指定数量的流,
Random random = new Random();
random.ints().limit(10).forEach(System.out::println);
6.并行–parallel程序
List<string> strings = Arrays.asList("abc","","b","efg");
long count = strings.parallelStream().filter(string->string.isEmpty()).count();
7.Collector
List<string> strings = Arrays.asList("abc","","b","efg");
//过滤空字符
List<String> filtered = strings.stream().filter(string->!string.isEmpty()).collect(Collectors.toList());
//合并字符串
String merged String = strings.stream().filter(string->!string.isEmpty()).collect(Collectors.joining(","));
8.统计
用于int、double、long等基本类型,可以用来产生统计结果
List<Integer> numbers =Arrays.asList(3,2,3,1,5,6);
IntSummaryStatistics stas = numbers.stream().mapToInt((x)->x).summaryStatistics();
System.out.println(stas.getMax());
System.out.println(stas.getMin());
System.out.println(stas.getSum());
二、谨慎的使用Stream
一个Stream pipeline中包含了一个源Stream,接着是0个或者多个中间操作和一个终止操作,
Stream pipeline通常是lazy的,直到调用种植操作时才会开始计算,对于完成终止操作不需要的数据元素,
将永不会被计算,
注意: 没有终止操作的Stream pipeline将是一个静默的无操作指令,因此不能忘记终止操作;
Stream API是流式fluent:所有包含pipeline的调用可以连接成一个表达式
以下程序为例,从词典文件中中读取单词,并打印出单词长度符合用户指定的最低值的所有换位词;
两个单词包含相同字母,但字母顺序不同的两个词,称为换位词–anagram。
该程序会从用户指定的词典文件中读取每个词,并将符合条件的单词放入一个映射中。
public class Anagrams {
public static void main(String[] args) throws IOException {
File dictionary = new File("D:\\learning\\dictionary.txt");
int minGroupSize = Integer.parseInt("5");
Map<String, Set<String>> groups = new HashMap<>();
try(Scanner s = new Scanner(dictionary)){
while(s.hasNext()){
String word = s.next();
groups.computeIfAbsent(alphbetize(word),(unused)->new TreeSet<>()).add(word);
}
}
for(Set<String> group:groups.values()){
if(group.size()>=minGroupSize){
System.out.println(group.size()+":"+group);
}
}
}
private static String alphbetize(String s){
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
computeIfAbsent函数会在映射中查找一个键,如果这个键存在,只会返回与之关联的值,如果不存在,该方法就会对该键运用指定的函数对象算出一个值,将这个值和键关联起来,并返回计算得到的值。
如果我们滥用Stream上述代码就会变成:
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get("D:\\learning\\dictionary.txt");
int minGroupSize = Integer.parseInt("0");
try(Stream<String> words = Files.lines(dictionary)){
words.collect(
groupingBy(word -> word.chars().sorted().collect(StringBuilder::new,
(sb,c)->sb.append((char) c),StringBuilder::append).toString())).values().stream()
.filter(group->group.size()>=minGroupSize).map(group->group.size()+"+"+group)
.forEach(System.out::println);
}
}
上述代码极其难懂;我们可以采用更易懂的代码方式;
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get("D:\\learning\\dictionary.txt");
int minGroupSize = Integer.parseInt("0");
try(Stream<String> words = Files.lines(dictionary)){
words.collect(groupingBy(word->alphbetize(word))).values().stream()
.filter(group->group.size()>=minGroupSize)
.forEach(g-> System.out.println(g.size()+":"+g));
}
}
这里的alphbetize用的是和第一个版本相同的代码。
需要注意的是在没有显示类型的情况下,仔细命名Lambda参数,对于可读性非常重要,
且上述代码中字母排序是在单独的方法中完成的,给操作命名,减少主程序中的细节,增强了可读性。
使用stream对char进行操作,难以正确编写,这是因为Java几乎不支持char类型的stream;stream处理char是非常危险的;
“hello world”.chars().forEach(System.out::printl);
你可能以为它会输出Hello world,但是运行之后,输出的是721011081081113211911111410810033,
这是因为"hello world".chars()返回的是Stream中的元素,不是char值,而是int值,因此调用了print的int覆盖。
名为chars的方法却返回int值的Stream,修正方法是利用转换强制调用正确的覆盖:
“Hello world”.chars().forEach(x->System.out.print((char)x));
最好避免用Stream处理char类型
1.适用于Stream的情况
- 1.统一转换元素的序列
- 2.过滤元素的序列
- 3.利用单个操作-如添加、连接或计算最小值,合并元素的顺序;
- 4.将元素的序列存放到一个集合中,比如根据某些公共属性进行分组
- 5.搜索满足某些条件的元素的序列
2.不适用于Stream的情况
- 1.从代码块可以读取或者修改范围内的任意局部变量,从Lambda表达式只能读取final或者有效的final变量,并不能修改任何local变量
- 2.从代码块中,可以从外围方法中return、break、或continue外围循环,或者抛出该方法声明要抛出的热河受检异常,从Lambda完全无法完成。
注意:Stream很难同时从一个pipeline的多个阶段访问同一个元素,一旦将一个值映射,原来的值就丢了。
3.flatMap操作:
private static List<Card> newDeck(){
List<Card> result = new ArrayList<>();
for(Suit suit:Suit.values){
for(Rank rank:Rank.values()){
result.add(new Card(suit,rank));
}
}
return result;
}
private static List<Card> newDeck(){
return Stream.of(Suit.values()).
flatMap(suit -> Stream.of(Rank.values())
.map(rank ->new Card(suit,rank)))
.collect(toList());
}
三、 优先使用Stream中无副作用的函数
Stream并不是一个API,是一种基于函数编程的模型,为了获取Stream带来的描述性和速度,有时还有并行性,必须采用泛型还有API。
Stream泛型最重要的部分是把计算构造成一些列变形,每一级结果都尽可能靠近上一级结果的纯函数,
纯函数指结果只取决于输入的函数:不依赖于任何可边的状态,也不更新任何状态。
Map<String,Long> freq = new HashMap<>();
try(Stream<String> words=new Scanner(file).tokens()){
words.forEach(word->{
freq.merge(word.toLowerCase(),1L,Long::sum);
});
}
上述代码使用了Stream、Lambda和方法引用,并且得出了正确的答案,但这并不是Stream代码,是迭代式代码;
可读性较差且更长了,且使用了一个改变外部状态的Lambda,完成了在终止操作中的forEach所有工作。
Map<String,Long> freq = new HashMap<>();
try(Stream<String> words=new Scanner(file).tokens()){
freq = words.collect(groupingBy(String::toLowerCase,counting()));
}
在这个代码中正确使用了StreamAPI,变得更加简洁。forEach操作是终止操作中最没有威力的,也是对Stream最不友好的,
它是显示迭代的,因此不适合并行。forEach操作应该只用于报告Stream计算的结果,而不是执行计算。
四、CollectorsAPI
将上述代码进行改进,使用collector,CollectorsAPI有39种方法,其中有些方法还带有5个类型参数,
我们不必完全搞懂这些API,将其视为一个黑盒->收集stream中元素的收集器即可。
toList()、toSet()、toCollection(collectionFactory)分别返回一个列表、一个集合、和程序员指定的集合类型。
上述代码可以优化为:
List<String> topTen = freq.keySet().Stream()
.sorted(comparing(freq::get).reversed())
.limit(10)
.collect(toList());
传入sorted的比较器comparing(freq::get).reversed();comparing方法是一个比较器构造方法,带有一个键提取函数。
函数读取一个单词,“提取”实际上是一个表查找,有限制的方法引用freq::get在频率表中查找单词,并返回该单词在文件中出现的次数。
CollectorsAPI中的大多数是为了将一个Stream集合到映射中,每个Stream元素都有一个关联的键和值,多个Stream元素可以关联到同一个键;
1.toMap
toMap(keyMapper,valueMapper),
它有两个映射函数,keyMapper负责将Stream元素映射到键,valueMapper负责将它映射到值;
private static final Map<String,Operation> stringToEnum =
Stream.of(values()).collect(toMap(Object::toString,e->e));
上述代码中,如果Stream中的每个元素都映射到一个唯一的键,那么这个toMap是很完美的,如果多个Stream元素映射到同一个键
pipeline就会抛出一个IllegalState-Exception;
toMap更复杂的形式以及groupingBy方法,提供了更多处理这类冲突的策略;其中还提供了合并函数merge function
merge是一个BinaryOperator,这里的V是映射的值类型。合并函数将与键关联的任何其他值和现有值合并起来,
因此,假如合并函数是乘法,得到的值就是与该值映射的键关联的所有值的积。
Map<Artist,Album> toHits =
ablums.collect(toMap(Ablum::artist,a->a,maxBy(comparing(Album::sales))));
带有3个参数的toMap形式,对于完成从键到键关联的元素的映射也是非常有用的,上述代码表示从一个歌唱家到
最畅销的唱片之间的映射。
注意:这个比较器使用了静态工厂方法-maxBy,这是从BinaryOperator静态导入的,用于计算指定比较器产生的最大值,
比较器是由比较构造器comparing返回的,有一个键提取函数Ablum::sales。
带有3个参数的toMap还有另一种用途,即生成一个收集器,当有冲突时强制保留最后更新。对于许多Stream而言,结果是不确定的,
但如果与映射函数的键关联的所有值都相同,或许都可接受的。
toMap(keyMapper,valueMapper,(oldVal,newVal)->newVal)
keyMapper生成键,valueMapper生成值,
第三个参数用在key值冲突的情况下:如果新元素产生的key在Map中已经出现过了,第三个参数就会定义解决的办法
2.groupingBy
返回收集器以生成映射,根据分类函数将元素分门别类。
分类函数带有一个元素,并返回其所属的类别。这个类别就是元素的映射键。
groupingBy最简单的版本只有一个分类器,并返回一个映射,映射值为每个类别中所有元素的列表。
words.collect(groupingBy(word->alohabetize(word)));
如果要让groupingBy返回一个收集器,用其生成一个值而不是列表的映射,出了分类器之外,
还可以指定一个下游收集器-downstream collector;包含某个类别中所有元素的Stream中生成一个值;
这个参数最简单的用法是传入toSet();结果生成一个映射,这个映射值为元素集合而非列表。
Map<String,Long> freq = words.collect(groupingBy(String::toLowerCase,counting()));
上述代码是将元素数量和元素联合起来。
Collectors中,不包含集合,前两个minBy和maxBy有一个比较器,并返回由比较器确定的Stream中最少或最多的元素,
最后一个方法是joining,只在CharSequence实例的Stream中操作,如字符串,以参数的形式返回一个简单的合并元素的收集器,
其中一个参数形式带有一个名为delimiter-分节符的CharSequence参数,返回一个连接Stream元素并在相邻元素之间
插入分隔符的收集器。如果传入一个逗号作为分隔符,就会返回一个用逗号隔开的值字符串。