Java从头到尾都是被设计成为一门面向对象的语言,所以时间长了,它就积累了很多的有用的库。从头开始,他就拥有多线程处理的能力。最重要的是Java里面有两个非常强大非常超前的两个概念:jvm和Java字节码。
Java虚拟机( JVM)及其字节码可能会变得比Java语言本身更重要,而且对于某些应用来说, Java可能会被同样运行在JVM上的竞争对手语言(如Scala或Groovy)取代 。
但是,编程语言生态系统的气候正在变化。程序员越来越多地要处理所谓的大数据(数百万兆甚至更多字节的数据集),并希望利用多核计算机或计算集群来有效地处理。意味着需要使用并行处理——Java以前对此并不支持。
Java 8对于程序员的主要好处在于它提供了更多的编程工具和概念,能以更快,更重要的是能以更为简洁、更易于维护的方式解决新的或现有的编程问题。
语言需要不断改进以跟进硬件的更新或满足程序员的期待 。
要坚持下去, Java必须通过增加新功能来改进,而且只有新功能被人使用,变化才有意义。所以,使用Java 8,你就是在保护你作为Java程序员的职业生涯。
在未使用函数式编程的时候,我们从一个篮子中取出来一个绿色的苹果,需要编写这样的一个函数:
public static List filterGreenApples(List inventory){
List result = new ArrayList<>();
for (Apple apple: inventory){
if ("green".equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
当我们要再筛选出重量大于50g的苹果的时候,需要重新写一个方法:
public static List filterHeavyApples(List inventory){
List result = new ArrayList<>();
for (Apple apple: inventory){
if (apple.getWeight() > 150) {
result.add(apple);
}
}
return result;
}
这样子比较麻烦,所以Java8新增了函数式编程的语法,再遇到这种函数的时候,我们可以这样写:
public static boolean isGreenApple(Apple apple) {
return "green".equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
static List filterApples(List inventory, Predicate p) {
List result = new ArrayList<>();
for (Apple apple: inventory){
if (p.test(apple)) {
result.add(apple);
}
}
return result;
}
要使用这个函数,我们可以这么调用它:
filterApples(inventory, Apple::isGreenApple);
//或者
filterApples(inventory, Apple::isHeavyApple);
我们发现,在传递方法作为参数的时候,需要先定义一个谓词(方法),这样的话,也是比较麻烦的一件事情。所以Java8的新语法引入了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()) );
//再甚至,省去定义filterApples方法,直接使用库方法filter
filter(inventory, (Apple a) -> a.getWeight() > 150 );
通过这种写法,我们的代码优雅(少)了很多,看起来也很直观。但是,当传递的方法比较复杂,代码比较多时,我们尽量还是定义一个名字比较直观的方法作为参数传进来的比较好。(ps:作为一个优秀的攻城狮,要保证我们的代码质量)
你可能以为对Java8的函数编程特性就此结束了,但是,很遗憾,最优秀的还在最后面:流Streams。通过函数式编程结合Streams的使用,我们可以充分利用好CPU的多核特性,完成对资源的充分利用,它有一套函数式程序员熟悉的、类似于filter的操作,比如map、 reduce,还有我们接下来要讨论的在Collections和Streams之间做转换的方法。
Java8中的Streams可以帮助我们更好的对集合进行处理。比如我们要从一个列表中筛选金额较高的交易,然后按照货币进行分组。在Java8之前,我们可能需要这么写:
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);
}
}
这段代码有很多套路的写法,好几个if判断导致我们没有办法直观的理解这段代码是做什么的。如果使用streams后,我们可以这样写:
Map<Currency, List<Transaction>> transactionsByCurrencies =
transactions.stream()
.filter((Transaction t) -> t.getPrice() > 1000)
.collect(groupingBy(Transaction::getCurrency));
在理解了streams的用法以后,这段代码的意思就会非常容易读懂。当然,streams并不只是改变了代码的呈现方式,在底层,streams api处理数据的方式与传统的单线程代码并不相同。在之前的代码中,我们会用到for-each来对集合进行迭代,这种方法也叫外部迭代;而在有了streams api之后,根本用不着操心循环的事情,数据处理完全是在库内部进行的,这种方法也叫内部迭代。
在进行内部迭代的时候,我们需要知道,streams api会自动利用cpu的多核特性,充分利用CPU的计算资源进行计算。要使用多核,那就涉及到了并行编程。在以往的代码中,并行编程是一件不容易的事情,因为线程间会涉及到共享变量的同时访问和更新,如果没有协调好,数据可能会被意外改变。代码如下:
public class AddTest implements Runnable {
private static int i = 0;
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
AddTest test = new AddTest();
for (int i = 0; i < 10; i++) {
exec.execute(test);
}
exec.shutdown();
}
@Override
public void run() {
i++;
Thread.yield();
System.out.println(i);
}
}
Java 8也用Stream API( java.util.stream)解决了这两个问题:集合处理时的套路和晦涩,以及难以利用多核。这样设计的第一个原因是,有许多反复出现的数据处理模式,类似于前一节所说的filterApples或SQL等数据库查询语言里熟悉的操作,如果在库中有这些就会很方便:根据标准筛选数据(比如较重的苹果), 提取数据(例如抽取列表中每个苹果的重量字段),或给数据分组(例如,将一个数字列表分组,奇数和偶数分别列表)等。第二个原因是,这类操作常常可以并行化。例如,如图1-6所示,在两个CPU上筛选列表,可以让一个CPU处理列表的前一半,第二个CPU处理后一半,这称为分支步骤(1)。 CPU随后对各自的半个列表做筛选(2)。最后(3),一个CPU会把两个结果合并起来( Google搜索这么快就与此紧密相关,当然他们用的CPU远远不止两个了)。 -<<Java8实战>>
下面我们体验一下如何利用Stream和Lambda表达式顺序或并行的从一个列表里筛选比较重的苹果:
//顺序处理:
List<Apple> heavyApples = inventory
.stream()
.filter((Apple a) -> a.getWeight() > 150)
.collect(toList());
//并行处理:
List<Apple> heavyApples = inventory
.parallelStream()
.filter((Apple a) -> a.getWeight() > 150)
.collect(toList());
可以看到,使用stream可以大大的减少代码量,增加代码的可读性,方便的控制并发编程。好处多多。
Java 8中加入默认方法主要是为了支持库设计师,让他们能够写出更容易改进的接口。 我们都知道,Java8之前,如果在接口里面定义了一个方法,那么实现这个接口的方法就必须实现该接口的所有方法,否则,编译就不会通过。而要实现上面的List中的stream和parallelStream方法,就需要在List或者它的父类Conllection中实现这些方法。如果按照之前的需要在实现类中实现接口的所有方法,那么这将会成为我们这些程序员的噩梦。
让我们放心的是,Java8引入了默认方法:以后在接口中可以直接写出来方法体作为默认的方法了,实现这个接口的类不用必须实现已经实现默认方法的方法了。例如: 在Java 8里,你现在可以直接对List调用sort方法。它是用Java 8 List接口中如下所示的默认方法实现的,它会调用Collections.sort静态方法:
default void sort(Comparator<? super E> c) {
Collections.sort(this, c);
}
这意味着List的任何实体类都不需要显式实现sort,而在以前的Java版本中,除非提供了sort的实现,否则这些实体类在重新编译时都会失败。 因为这会导致多重继承的问题,所以java8中才用了一些限制来避免出现类似于C++中的菱形继承问题。
与Java运行在jvm虚拟机上一样,Scala语言也是运行在jvm上的,它在有些方面有赶超java的趋势。
现在我们知道了java8中新引进来的思想有:将方法和lambda作为一等值;以及在没有可变共享状态时,函数或方法可以有效、安全的并行执行。这两个思想在Stream里面都用到了。
另外,对null的处理,Java8中引入了optional类来进行处理。另外Java8也引入了Scala中的模式匹配的概念。
总结一下:
1、 请记住语言生态系统的思想,以及语言面临的“要么改变,要么衰亡”的压力
2、 函数是一等值;记得方法如何作为函数式值来传递,还有Lambda是怎样写的
3、 Java 8中Streams的概念使得Collections的许多方面得以推广,让代码更为易读,并允许并行处理流元素
4、 可以在接口中使用默认方法,在实现类没有实现方法时提供方法内容
5、 其他来自函数式编程的有趣思想,包括处理null和使用模式匹配