JAVA致力于让并发编程更容易,出错更少,JAVA5添加了线城池和并发集合,JAVA7添加了fork、join框架
1. JAVA8中有哪些新的设计
- Stream API,支持处理数据的并行操作,避免用synchronized编写代码
- 向方法传递代码,行为参数化,函数式编程
- 接口中的默认方法
流处理:流处理实际上是一个序列,但是不同加工过程一般是并行的,基于这一思想,JAVA8中在java.util.stream中添加了Stream API;Stream<T>可以看做比较花哨的迭代器。JAVA8可以透明的把输入的不相关的部分拿到几个CPU内核上去执行操作流水线。
行为参数化:即把你的代码传递给函数。
并行与共享的可变数据:如果使用流进行并行操作,那么就意味着写代码时不能访问共享的可变数据(因为并行操作会有不确定性),这些函数成为“纯函数”,“无副作用函数”或“无状态函数”。
没有共享的可变数据,即将代码传递给其他方法的能力是函数式编程的基石。
2. Java中的函数
2.1 将函数作为值
函数一般指方法,尤其是静态方法。在JAVA中值是一等公民,类和方法是二等公民,JAVA8致力于将二等公民方法变为一等公民值。
方法引用
与对象引用类似,下面例子是找出目录下的隐藏文件:
File hiddenFiles = new File(".").listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isHidden();
}
});//JAVA8以前还要new 一个对象,实现方法。
JAVA8中
File hiddenFiles = new File(".").listFiles(File::isHidden);
因为isHidden()方法已经实现过了,所以我们可以直接通过引用的方法使用。只要方法中有代码(方法中的可执行部分),用方法引用就可以传递代码。
Lambda--匿名函数
顾名思义就是没有名字的函数,简单的例如(int x) -> x+1,表示“调用时给定参数x,就返回x+1值的函数”。
传递代码的例子
从一个例子,我们来学习JAVA8如何帮我们更好的写程序:
如果我们有一个苹果Apple列表用inventory表示,我们希望得到一个返回苹果颜色的红色的列表,那么应该这样写,
public static List<Apple> filterRedApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if("red".equals(apple.getColor())){
result.add(apple);
}
}
return result;
}
那如果我们还想筛选出重量大于150g的苹果该怎么办呢,按照JAVA8以前的写法,我们会粘贴复制filterRedApples()的代码,然后修改条件。显然这样不符合代码的高可用性。所以我们理想的是编写一个筛选的模板,只需要把不同的条件传递进去就可以。也就是前面我么提到过的将代码作为参数传递给方法。
public static boolean isRedApple(Apple apple){ //颜色的条件代码
return "red".equals(apple.getColor);
}
public static boolean isHeavyApple(Apple apple){ //重量的条件代码
return apple.getWeight() > 150;
}
public interface Predicate<T>{
boolean test(T t);
}
public 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::isRedApple);
filterApples(inventory, Apple::isHeavyApple);
即使这样提高了代码的复用性,但是我们还是会感觉到,因为一行条件代码还需要去创建一个方法去传递它,有没有更见的方法去传递代码呢?JAVA8还引入了一套新记法去解决这个问题,那就是匿名函数或Lambda。你可以简单地把需要传递的代码直接写进去。
filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
filterApples(inventory, (Apple apple) -> apple.getWeight() > 150);
2.2 流
Stream API
流主要是针对集合的处理而设计,因为几乎每一个JAVA应用都会制造和处理集合,但集合用起来总不是那么理想,比如如果你想在一个列表中筛选出金额比较高的交易,然后按货币分组,则需要写一大堆代码,如下所示:
Map<Currency, List<Transaction>> tracsactionsByCurrencies = new HashMap<>();
for(Transaction transaction: transactions) {
if(transaction.getPrice() > 1000) { //筛选较高金额的交易
Currency currency = transaction.getCurrency();
List<Transaction> transactionsByCurrency = tracsactionsByCurrencies.get(currency); //获取当前货币的所有高金额列表
if(transactionsByCurrency == null){ //如果该货币没有高金额交易,新建列表,并添加至集合。
transactionsByCurrency = new ArrayList();
transactionsByCurrencies.put(currency, transactionsByCurrency);
}
transactionsByCurrency.add(transaction); //将该交易添加到列表中去
}
}
显然这是十分复杂的,那么通过JAVA8的Stream API,我们可以这样解决问题了。
import java.util.stream.Collectors.toList;
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream()
.filter((Tracsantion tracsaction) -> transaction.getPrice() > 1000)
.collect(groupBy(Transaction::getCurrency));
看起来十分简单,先通过filter筛选出金额较高的交易,然后按货币分组,Stream API提供了许多对开发者友好的API。它与Collection API十分不同,Collection API是得自己去迭代使用for-each,我们称之为外部迭代,而Stream API是内部迭代,使用者不用操心循环的事情,完全是在内库进行的。到这里,我们直说了新的Stream API与现有的JAVA集合API行为差不多,它们都能访问数据的序列,但是Collection主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算。
JAVA中的多线程
通过多线程来实现并行并非易事,JAVA8基于Stream的并行提倡尽量少使用synchronized的函数式编程风格,它关注数据块而不是协调访问。JAVA8主要用Stream API解决了两个问题:
1. 集合处理时的套路与晦涩,以及难以利用多核;
2. 让一些操作可以并行化,例如filter;
再回到刚才我们筛选苹果的例子中,我么将体验一下利用Stream和Lamba表达式顺序或并行从一个列表中筛选比较重的苹果。
顺序处理:
List<Apple> result = inventory.stream()
.filter((Apple apple) -> apple.getWeight() > 150)
.collect(toList());
并行处理:
List<Apple> result = inventory.parallelStream()
.filter((Apple apple) -> apple.getWeight() > 150)
.collect(toList());
2.3 默认方法
默认方法其实是为了支持库数据师,让他们能够写出更容易改进的接口。我们举一个简单地例子来说明:
我们之前使用了List.stream()和List.parallelStream(),但是JAVA8以前的List<T>以及Collection<T>接口里面没有这个方法,那么就需要把这些方法加到Collection<T>接口,但这对使用Collection接口的开发者无疑是噩梦,因为所有实现Collection<T>接口的类都必须提供一个实现,所以如何保证改变已发布的接口而不破坏已有的实现呢?JAVA8的解决办法是添加默认方法,即接口可以包含实现类没有提供实现的方法签名了。
这给接口设计者提供了一个扩充接口的方式,而且不会破坏现有的代码,JAVA8使用新的default关键字来表示。例如,你可以直接使用List.sort(),而不必使用之前的Collection.sort(list, comparator)。
default void sort(Comparator<? super E> c){
Collection.sort(this, c);
}
不过有一个问题需要思考,一个类可以实现多个接口,如果在好几个接口理实现多个默认实现,那是不是就会有某种程度上的多重继承?JAVA8中会有限制来避免类似于C++中臭名昭著的菱形继承问题。有兴趣的可以自行了解。
2.4 函数编程的其他好思想
Optional<T>类
它是一个容器类,包含的内容可以是空的也可以是一个值。如果你能一致的使用它,它会帮你避免NullPointerException。
(结构)模式匹配