JAVA8-Stream的使用

java–Stream

本文章内容来自于菜鸟教程和effectiveJava

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元素并在相邻元素之间
插入分隔符的收集器。如果传入一个逗号作为分隔符,就会返回一个用逗号隔开的值字符串。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值