java1.8实战学习(一)——总结:流处理、行为参数化、并行与共享

笔者这段时间在学习java8的新特性,发现有好多新的特点,特写此博客用于梳理记录学习,不用每次都抱着pdf《java8实战》去看,也供大家参考

下一篇:java1.8实战学习(二)

知识点概括

总结了Java的主要变化(Lambda表达式、方法引用、流和默认方法),并为学习后面的内容做好准备。

  •  流处理

 第一个编程概念是流处理。介绍一下,流是一系列数据项,一次只生成一项。程序可以从输入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。一个程序的输出流很可能是另一个程序的输入流。

一个实际的例子是在Unix或Linux中,很多程序都从标准输入(Unix和C中的stdin,Java中的System.in)读取数据,然后把结果写入标准输出(Unix和C中的stdout,Java中的System.out)。首先我们来看一点点背景:Unix的cat命令会把两个文件连接起来创建一个流,tr会转换流中的字符,sort会对流中的行进行排序,而tail -3则给出流的最后三行。Unix命令行允许这些程序通过管道(|)连接在一起,比如

cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3 

会(假设file1和file2中每行都只有一个词)先把字母转换成小写字母,然后打印出按照词典排序出现在最后的三个单词。我们说sort把一个行流①作为输入,产生了另一个行流(进行排序)作为输出,如图1-2所示。请注意在Unix中,命令(cat、tr、sort和tail)是同时执行的,这样sort就可以在cat或tr完成前先处理头几行。就像汽车组装流水线一样,汽车排队进入加工站,每个加工站会接收、修改汽车,然后将之传递给下一站做进一步的处理。尽管流水线实际上是一个序列,但不同加工站的运行一般是并行的。

 

基于这一思想,Java 8在java.util.stream中添加了一个Stream API;Stream<T>就是一系列T类型的项目。

推动这种做法的关键在于,现在你可以在一个更高的抽象层次上写Java 8程序了:思路变成了把这样的流变成那样的流(就像写数据库查询语句时的那种思路),而不是一次只处理一个项目。另一个好处是,Java 8可以透明地把输入的不相关部分拿到几个CPU内核上去分别执行你的Stream操作流水线——这是几乎免费的并行,用不着去费劲搞Thread了。

  • 用行为参数化把代码传递给方法

Java 8中增加的另一个编程概念是通过API来传递代码的能力。

在Unix的例子里,你可能想告诉sort命令使用自定义排序。虽然sort命令支持通过命令行参数来执行各种预定义类型的排序,比如倒序,但这毕竟是有限的。

比方说,你有一堆发票代码,格式类似于2013UK0001、2014US0002……前四位数代表年份,接下来两个字母代表国家,最后四位是客户的代码。你可能想按照年份、客户代码,甚至国家来对发票进行排序。你真正想要的是,能够给sort命令一个参数让用户定义顺序:给sort命令传递一段独立代码。

那么,直接套在Java上,你是要让sort方法利用自定义的顺序进行比较。你可以写一个compareUsingCustomerId来比较两张发票的代码,但是在Java 8之前,你没法把这个方法传给另一个方法。Java 8增加了把方法(你的代码)作为参数传递给另一个方法的能力。图1-3是基于图1-2画出的,它描绘了这种思路。我们把这一概念称为行为参数化。Stream API就是构建在通过传递代码使操作行为实现参数化的思想上的,当把compareUsingCustomerId传进去,你就把sort的行为参数化了。
 

 

  • 并行与共享的可变数据

Java 8的流实现并行比Java现有的线程API更容易,因此,尽管可以使用synchronized来打破“不能有共享的可变数据”这一规则,但这相当于是在和整个体系作对,因为它使所有围绕这一规则做出的优化都失去意义了。在多个处理器内核之间使用synchronized,其代价往往比你预期的要大得多,因为同步迫使代码按照顺序执行,而这与并行处理的宗旨相悖。这两个要点(没有共享的可变数据,将方法和函数即代码传递给其他方法的能力)是我们平常所说的函数式编程范式的基石。

  •  一个传递代码的例子

假设你有一个Apple类,它有一个getColor方法,还有一个变量inventory保存着一个Apples的列表。你可能想要选出所有的绿苹果,并返回一个列表。通常我们用筛选(filter)一词来表达这个概念。在Java 8之前,你可能会写这样一个方法filterGreenApples:

public static List<Apple> filterGreenApples(List<Apple> inventory){ 
 List<Apple> result = new ArrayList<>(); 
 for (Apple apple: inventory){ 
 if ("green".equals(apple.getColor())) {
 result.add(apple); 
 } 
 } 
 return result; 
}

但是接下来,有人可能想要选出重的苹果,比如超过150克,于是你心情沉重地写了下面这个方法,甚至用了复制粘贴:
 

public static List<Apple> filterHeavyApples(List<Apple> inventory){ 
 List<Apple> result = new ArrayList<>(); 
 for (Apple apple: inventory){ 
 if (apple.getWeight() > 150) {  //仅仅此句不同
 result.add(apple); 
 } 
 } 
 return result; 
} 

我们都知道软件工程中复制粘贴的危险——给一个做了更新和修正,却忘了另一个。这两个方法只有一行不同:if里面高亮的那行条件。如果这两个高亮的方法之间的差异仅仅是接受的重量范围不同,那么你只要把接受的重量上下限作为参数传递给filter就行了,比如指定(150, 1000)来选出重的苹果(超过150克),或者指定(0, 80)来选出轻的苹果(低于80克)。但是,我们前面提过了,Java 8会把条件代码作为参数传递进去,这样可以避免filter方法出现重复的代码。现在你可以写:

 

public static boolean isGreenApple(Apple apple) { 
 return "green".equals(apple.getColor());
} 
public static boolean isHeavyApple(Apple apple) { 
 return apple.getWeight() > 150;
} 
public interface Predicate<T>{ 
boolean test(T t); 
} 
static List<Apple> filterApples(List<Apple> inventory, 
 Predicate<Apple> p) {
 List<Apple> result = new ArrayList<>(); 
 for (Apple apple: inventory){ 
 if (p.test(apple)) { 
 result.add(apple); 
 } 
 } 
 return result; 
} 

要用它的话,你可以写:

filterApples(inventory, Apple::isGreenApple);
// 或者
filterApples(inventory, Apple::isHeavyApple);  
  • 从传递方法到 Lambda

把方法作为值来传递显然很有用,但要是为类似于isHeavyApple和isGreenApple这种可能只用一两次的短方法写一堆定义有点儿烦人。不过Java 8也解决了这个问题,它引入了一套新记法(匿名函数或Lambda),让你可以写

filterApples(inventory, (Apple a) -> "green".equals(a.getColor()) ); 
//或者
filterApples(inventory, (Apple a) -> a.getWeight() > 150 ); 
//甚至
filterApples(inventory, (Apple a) -> a.getWeight() < 80 || 
 "brown".equals(a.getColor()) ); 

所以,你甚至都不需要为只用一次的方法写定义;代码更干净、更清晰,因为你用不着去找自己到底传递了什么代码。但要是Lambda的长度多于几行(它的行为也不是一目了然)的话,那你还是应该用方法引用来指向一个有描述性名称的方法,而不是使用匿名的Lambda。你应该以代码的清晰度为准绳。

  • 关于流的一个例子

几乎每个Java应用都会制造和处理集合。但集合用起来并不总是那么理想。比方说,你需要从一个列表中筛选金额较高的交易,然后按货币分组。你需要写一大堆套路化的代码来实现这个数据处理命令,如下所示:

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

有了Stream API,你现在可以这样解决这个问题了:

import static java.util.stream.Collectors.toList; 

Map<Currency, List<Transaction>> transactionsByCurrencies = 
 transactions.stream() 
 .filter((Transaction t) -> t.getPrice() > 1000) 
 .collect(groupingBy(Transaction::getCurrency)); 

现在值得注意的是,和Collection API相比,Stream API处理数据的方式非常不同。用集合的话,你得自己去做迭代的过程。你得用for-each循环一个个去迭代元素,然后再处理元素。我们把这种数据迭代的方法称为外部迭代。相反,有了Stream API,你根本用不着操心循环的事情。数据处理完全是在库内部进行的。我们把这种思想叫作内部迭代

使用集合的另一个头疼的地方是,想想看,要是你的交易量非常庞大,你要怎么处理这个巨大的列表呢?单个CPU根本搞不定这么大量的数据,但你很可能已经有了一台多核电脑。理想的情况下,你可能想让这些CPU内核共同分担处理工作,以缩短处理时间。理论上来说,要是你有八个核,那并行起来,处理数据的速度应该是单核的八倍。

多线程并非易事

问题在于,通过多线程代码来利用并行(使用先前Java版本中的Thread API)并非易事。你得换一种思路:线程可能会同时访问并更新共享变量。因此,如果没有协调好,数据可能会被意外改变。

Java 8也用Stream API(java.util.stream)解决了这两个问题:集合处理时的套路和晦涩,以及难以利用多核。这样设计的第一个原因是,有许多反复出现的数据处理模式,类似于前一节所说的filterApples或SQL等数据库查询语言里熟悉的操作,如果在库中有这些就会很方便:

根据标准筛选数据(比如较重的苹果),提取数据(例如抽取列表中每个苹果的重量字段),或给数据分组(例如,将一个数字列表分组,奇数和偶数分别列表)等。

第二个原因是,这类操作常常可以并行化。例如,如图1-6所示,在两个CPU上筛选列表,可以让一个CPU处理列表的前一半,第二个CPU处理后一半,这称为分支步骤(1)。CPU随后对各自的半个列表做筛选(2)。最后(3),一个CPU会把两个结果合并起来

到这里,我们只是说新的Stream API和Java现有的集合API的行为差不多:它们都能够访问数据项目的序列。不过,现在最好记得,Collection主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算。这里的关键点在于,Stream允许并提倡并行处理一个Stream中的元素。虽然可能乍看上去有点儿怪,但筛选一个Collection(将上一节的filterApples应用在一个List上)的最快方法常常是将其转换为Stream,进行并行处理,然后再转换回List,下面举的串行和并行的例子都是如此。我们这里还只是说“几乎免费的并行”,让你稍微体验一下,如何利用Stream和Lambda表达式顺序或并行地从一个列表里筛选比较重的苹果。

//顺序处理
import static java.util.stream.Collectors.toList; 
List<Apple> heavyApples = 
 inventory.stream().filter((Apple a) -> a.getWeight() > 150) .collect(toList()); 
//并行处理:
import static java.util.stream.Collectors.toList; 
List<Apple> heavyApples = 
 inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150) .collect(toList()); 

Java中的并行与无共享可变状态

大家都说Java里面并行很难,而且和synchronized相关的玩意儿都容易出问题。那Java 8里面有什么“灵丹妙药”呢?事实上有两个。首先,库会负责分块,即把大的流分成几个小的流,以便并行处理。其次,流提供的这个几乎免费的并行,只有在传递给filter之类的库方法的方法不会互动(比方说有可变的共享对象)时才能工作。但是其实这个限制对于程序员来说挺自然的,举个例子,我们的Apple::isGreenApple就是这样。确实,虽然函数式编程中的函数的主要意思是“把函数作为一等值”,不过它也常常隐含着第二层意思,即“执行时在元素之间无互动”。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值