探索Java8——流(Stream)


集合是Java中使用最多的API。要是没有集合,还能做什么呢?几乎每个Java应用程序都会制造和处理集合。集合对于很多编程任务来说都是非常基本的:它们可以让你把数据分组并加以处理。

什么是流

流是Java API的新成员,它允许你以声明性方式处理数据集合
就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码了!

比较一下,一个是用Java 7写的,另一个是用Java 8的流写的。
(举例为:筛选卡路里低于400的菜肴并排序)

List<Dish> lowCaloricDishes = new ArrayList<>(); 
for(Dish d: menu){ 
	//用累加器筛选元素
 	if(d.getCalories() < 400){ 
 		lowCaloricDishes.add(d); 
 	} 
} 
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
	//用匿名类进行排序
 	public int compare(Dish d1, Dish d2){ 
 		return Integer.compare(d1.getCalories(), d2.getCalories()); 
 	} 
}); 
List<String> lowCaloricDishesName = new ArrayList<>(); 
for(Dish d: lowCaloricDishes){ 
	//处理排序后的结果
 	lowCaloricDishesName.add(d.getName()); 
} 

在这段代码中,你用了一个“垃圾变量”lowCaloricDishes。它唯一的作用就是作为一次性的中间容器。在Java 8中,实现的细节被放在它本该归属的库里了。

import static java.util.Comparator.comparing; 
import static java.util.stream.Collectors.toList; 
List<String> lowCaloricDishesName = 
 	menu.stream() 
 		.filter(d -> d.getCalories() < 400) 
 		.sorted(comparing(Dish::getCalories))
 		.map(Dish::getName) 
 		.collect(toList()); 

为了利用多核架构并行执行这段代码,你只需要把stream()换成parallelStream()

代码是以声明性方式写的:说明想要完成什么(筛选热量低的菜肴)而不是说明如何实现一个操作(利用循环和if条件等控制流语句)。

因为filter、sorted、map和collect等操作是与具体线程模型无关的高层次构件,所以它们的内部实现可以是单线程的,也可能透明地充分利用你的多核架构。
在这里插入图片描述
为了接下来介绍方便,我们把Dish定义为:

public class Dish {
    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private final Type type;

    public Dish(String name, boolean vegetarian, int calories, Type type) {
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    public int getCalories() {
        return calories;
    }

    public Type getType() {
        return type;
    }

    @Override
    public String toString() {
        return name;
    }

    public enum Type {MEAT, FISH, OTHER}
}

我们会使用这样一个例子:一个menu,它只是一张菜肴列表:

List<Dish> menu = Arrays.asList( 
 new Dish("pork", false, 800, Dish.Type.MEAT), 
 new Dish("beef", false, 700, Dish.Type.MEAT), 
 new Dish("chicken", false, 400, Dish.Type.MEAT), 
 new Dish("french fries", true, 530, Dish.Type.OTHER), 
 new Dish("rice", true, 350, Dish.Type.OTHER), 
 new Dish("season fruit", true, 120, Dish.Type.OTHER), 
 new Dish("pizza", true, 550, Dish.Type.OTHER), 
 new Dish("prawns", false, 300, Dish.Type.FISH), 
 new Dish("salmon", false, 450, Dish.Type.FISH) ); 

现在就来仔细探讨一下怎么使用Stream API。我们会用流与集合做类比,做点儿铺垫。

流简介

。Java 8中的集合支持一个新的stream方法,它会返回一个流(接口定义在java.util.stream.Stream里)。

那么,流到底是什么呢?简短的定义就是“从支持数据处理操作的源生成的元素序列”。

  • 元素序列:就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList 与 LinkedList)。但流的目的在于表达计算,比如你前面见到的filter、sorted和map。集合讲的是数据,流讲的是计算。
  • 源:流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
  • 数据处理操作:流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。
  • 流水线:很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。
  • 内部迭代:与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。

比如下面的操作:

List<String> threeHighCaloricDishNames =
        menu.stream()
        .filter(d -> d.getCalories() > 300)
        .map(Dish::getName)
        .limit(3)
        .collect(toList());
//结果是[pork, beef, chicken]
System.out.println(threeHighCaloricDishNames);

在这里插入图片描述

流与集合

粗略地说,集合与流之间的差异就在于什么时候进行计算。可以简单的以Scala中学到的理论来解释:Spark一路火花带闪电——Spark底层原理介绍

只能遍历一次

和迭代器类似,流只能遍历一次,遍历完后,这个流已经被消费掉了。
考虑下面的代码:

public class IteratorLambda {
    public static void main(String[] args) {
        List<Integer>  integers= Arrays.asList(1,2,5,4,2,1);
        Stream<Integer> integerStream=integers.stream();
        integerStream.forEach(System.out::println);
        integerStream.forEach(System.out::println);
    }
}

其输出结果是:

1
2
5
4
2
1
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
	at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
	at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
	at IteratorLambda.main(IteratorLambda.java:15)

stream has already been operated upon or closed说明:流已经操作或关闭。

外部迭代与内部迭代

使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。 相反,Streams库使用内部迭代。

怎么理解这两种迭代呢?还是来看看代码。

集合:用for-each循环外部迭代

List<String> names = new ArrayList<>(); 
for(Dish d: menu){ 
 	names.add(d.getName()); 
}

集合:用背后的迭代器做外部迭代

List<String> names = new ArrayList<>(); 
Iterator<String> iterator = menu.iterator(); 
while(iterator.hasNext()) { 
 	Dish d = iterator.next(); 
 	names.add(d.getName()); 
} 

流:内部迭代

List<String> names = menu.stream() 
 	.map(Dish::getName)
 	.collect(toList()); 

那么,内部迭代有什么好处吗?
内部迭代时,项目可以透明地并行处理,或者用更优化的顺序进行处理。

流操作

同Scala一样,Steam的操作可以分为两大类:
可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作

在这里插入图片描述

中间操作

最简单的理解,返回流的操作就是中间操作

为了搞清楚流水线中到底发生了什么,我们把代码改一改,让每个Lambda都打印出当前处理的菜肴。

List<String> threeHighCaloricDishNames =
        menu.stream()
        .filter(d -> {
        System.out.println("filter:"+d.getName());
        return d.getCalories() > 300;
        })
        .map(d->{
        System.out.println("map:"+d.getName());
        return d.getName();
        })
        .limit(3)
        .collect(toList());
System.out.println(threeHighCaloricDishNames);

它的输出如下:

filter:pork
map:pork
filter:beef
map:beef
filter:chicken
map:chicken

可以很明显的看到,并不是filter了全部的种类,然后再map操作。也就是说,尽管filter和map是两个独立的操作,但它们合并到同一次遍历中了

我们把上面的技术叫做循环合并

终端操作

终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如List、Integer,甚至void。

比如:void类型
menu.stream().forEach(System.out::println);
或者Long类型
menu.stream().count();

使用流

总而言之,流的使用一般包括三件事:

  • 一个数据源(如集合)来执行一个查询;
  • 一个中间操作链,形成一条流的流水线;
  • 一个终端操作,执行流水线,并能生成结果。

在这里插入图片描述

收集器简介

用指令式风格对交易按照货币分组:

Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>(); 
for (Transaction transaction : transactions) {
 	Currency currency = transaction.getCurrency(); 
 	List<Transaction> transactionsForCurrency = 
 transactionsByCurrencies.get(currency); 
 	if (transactionsForCurrency == null) { 
 		transactionsForCurrency = new ArrayList<>(); 
 		transactionsByCurrencies 
 			.put(currency, transactionsForCurrency); 
 	} 
 	transactionsForCurrency.add(transaction); 
} 

如果你是一位经验丰富的Java程序员,写这种东西可能挺顺手的,不过你必须承认,做这么简单的一件事就得写很多代码。

用Stream中collect方法的一个更通用的Collector参数,你就可以用一句话实现完全相同的结果。

Map<Currency, List<Transaction>> transactionsByCurrencies = 
 	transactions.stream().collect(groupingBy(Transaction::getCurrency)); 

前一个例子清楚地展示了函数式编程相对于指令式编程的一个主要优势:你只需指出希望的结果——“做什么”,而不用操心执行的步骤——“如何做”。
传递给collect方法的参数是Collector接口的一个实现,也就是给Stream中元素做汇总的方法。groupingBy说的是“生成一个Map,它的键是(货币)桶,值则是桶中那些元素的列表”。

Collectors静态工厂方法:
在这里插入图片描述
在这里插入图片描述

并行流

并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
这样一来,你就可以自动把给定操作的工作负荷分配给多核处理器的所有内核,让它们都忙起来。

让我们用一个简单的例子来试验一下这个思想。
假设你需要写一个方法,接受数字n作为参数,并返回从1到给定参数的所有数字的和。
一个直接的方法如下所示:

public static long sequentialSum(long n) { 
 	return Stream.iterate(1L, i -> i + 1) 
 		.limit(n) 
 		.reduce(0L, Long::sum);
} 

将顺序流转换为并行流

你可以把流转换成并行流,从而让前面的函数归约过程(也就是求和)并行运行——对顺序流调用parallel方法:

public static long sequentialSum(long n) { 
 	return Stream.iterate(1L, i -> i + 1) 
 		.limit(n) 
 		.parallel()
 		.reduce(0L, Long::sum);
} 

Stream在内部分成了几块。因此可以对不同的块独立并行进行归纳操作。同一个归纳操作会将各个子流的部分归纳结果合并起来,得到整个原始流的归纳结果。
在这里插入图片描述
如果不想使用并行流parallel,可以使用顺序流sequential。但最后一次parallel或sequential调用会影响整个流水线。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值