Java8入门

Java 8发布已经有一段时间了,总体上说Java 8本身是很有诚意的,带来了很多变化。从目前来看,各大Java开源社区和开源软件对Java 8还是比较欢迎的。Spring在Java 8发布后就推出了兼容Java 8的新版本。从开源软件和开发人员的反馈来看,Java 8应该会逐渐替代旧版本成为Java开发的新基石。

Java 8的更新给Java应用带来了很多显著的变化。主要更新包括Lambda表达式接口的默认方法和静态方法方法引用Java库的新特性-Optional,Stream,日期时间API并行运算等等,这些更新解决了Java本身固有的一些缺陷,也带来了更加有效的API。

Lambda表达式

Lambda表达式(闭包)是Java 8中最显著的更新,Lambda的概念是将一段代码作为参数传给方法,在方法中执行,这种概念在脚本语言如Python中应用十分普遍,即使在基于JVM的一些语言也支持Lambda表达式,但是Java程序中只能通过匿名内部类来替代Lambda的作用。

Lambda表达式的格式非常紧凑,最简单的Lambda表达式可以由逗号分隔的参数列表->运算符功能语句块组成。示例如下:

1
Arrays.asList("a", "b", "d").forEach(s -> System.out.println(s));

在参数列表中可以不声明参数类型,编译器会根据上下文判断参数类型,或者也可以显式的指定参数类型。

1
Arrays.asList("a", "b", "d").forEach((String s) -> System.out.println(s));

Lambda表达式中如果语句块过于复杂,也可以通过大括号包起来使结构清晰,Lambda表达式可以引用外部变量,而且Lambda表达式同样可以有返回值,编译器会根据上下文推断返回值的类型,所以我个人理解Lambda表达式其实就是匿名的方法。

如果Lambda表达式仅仅是上面的应用场景,估计很多同学会觉得Lambda表达式的作用就是精简代码,作为Java 8的重大更新未免言过其实。事实上,Lambda表达式的作用是很大的,除了精简代码、并行处理支持CPU多核运算、有利于JIT编译器优化代码外,还有传递行为的特性。

Java死板的接口规范降低了开发人员的入门难度,但很大的副作用是限制了使用场景,不便扩展。就像代码所示:

1
2
3
4
5
6
7
8
9
10
// 计算数组元素之和
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
public int sumAll(List<Integer> numbers) {
int total = 0;
for (int number : numbers) {
total += number;
}
return total;
}

sumAll()方法很简单,计算数组中所有元素的和,然后我们增加了一个需求-计算数组中所有偶数的和:

1
2
3
4
5
6
7
8
9
10
// 计算数组中所有偶数之和
public int sumAllEven(List<Integer> numbers) {
int total = 0;
for (int number : numbers) {
if (number % 2 == 0) {
total += number;
}
}
return total;
}

后来又增加了一个需求,计算数组中所有大于3的元素的和:

1
2
3
4
5
6
7
8
9
10
// 计算数组中所有大于3的元素之和
public int sumMoreThan3(List<Integer> numbers) {
int total = 0;
for (int number : numbers) {
if (number > 3) {
total += number;
}
}
return total;
}

上述需求都很类似,不难发现,三处实现代码中出现了很明显的代码重复现象,三处代码唯一的区别就在if判断的条件不同。如果要处理这种情况,一般方式是通过策略模式重构代码以规避代码重复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 策略模式
**/
interface Strategy {
public boolean test(int num);
}
class SumAllStrategy implements Strategy {
public boolean test(int num) {
return true;
}
}
class SumAllEvenStrategy implements Strategy {
public boolean test(int num) {
return num % 2 == 0;
}
}
public class Solution {
private Strategy strategy;
public Solution(Strategy strategy) {
this.strategy = strategy;
}
public int sumAll(List<Integer> numbers) {
int total = 0;
for (int number : numbers) {
if (strategy.test(number)) {
total += number;
}
}
return total;
}
}
// 调用
Solution solution = new Solution(strategy);
solution.sumAll(numbers);

策略模式成功地解决了代码重复的问题,但通过设计模式来解决这样的小问题未免让人觉得小题大做,不是很方便。现在有了Lambda表达式,我们可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Lambda表达式
public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
int total = 0;
for (int number : numbers) {
if (p.test(number)) {
total += number;
}
}
return total;
}
sumAll(numbers, n -> true);
sumAll(numbers, n -> n % 2 == 0);
sumAll(numbers, n -> n > 3);

策略模式的解决方案就像是传统的Java开发方法,代码冗长复杂;而Lambda的解决方法更像是C++的风格,代码简洁明了,开发起来如沐春风。毫不夸张地说,传递行为的特性给Java的使用带来了更多的可能,从此以后像使用函数语言那样使用Java不再是梦。

函数式接口

因为在Java 8中引入了函数式编程的理念,所以Java 8中增加了函数式接口的概念,函数式接口是指接口中只有一个抽象方法,Java 8有个注解专门表示函数式接口-@FunctionalInterface。函数式接口中的一个抽象方法的限制不针对下文中提到的默认方法和静态方法。

为避免开发人员因为使用Lambda表达式而重复开发函数式接口,Java 8内置了一些通用的函数式接口,包括PredicateFunctionConsumer等。

Predicate<T>:将T作为输入,返回一个布尔值作为输出,该接口包含多种默认方法将Predicate组合成其他复杂的逻辑。

1
2
3
4
5
6
7
8
9
10
Predicate predicate = (s) -> s.length() > 0;
predicate.test("foo"); // true
predicate.negate().test("foo"); // false
Predicate nonNull = Objects::nonNull;
Predicate isNull = Objects::isNull;
Predicate isEmpty = String::isEmpty;
Predicate isNotEmpty = isEmpty.negate();

Function<T, R>:将T作为输入,返回R作为输出,默认方法可被用来将多个函数链接(compose、andThen)

1
2
3
4
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"

Consumer<T>:将T作为输入,不返回任何内容,表示在单个参数上的操作

1
2
3
Consumer greeter = (p) -> System.out.println("Hello, " + p);
greeter.accept("Skywalker");

接口的默认方法和静态方法

在Java 8之前,接口中的方法必须都是没有实现的抽象方法;但是在Java 8中,接口的行为有了很显著的变化。首先接口中支持实现的方法-默认方法,默认方法是已经实现的方法,将默认方法加入接口中需要在方法名前加入default关键字,这样实现接口的子类,可以直接使用默认方法,或者根据需求重写默认方法的实现。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private interface Defaulable {
// Interfaces now allow default methods, the implementer may or
// may not implement (override) them.
default String notRequired() {
return "Default implementation";
}
}
private static class DefaultableImpl implements Defaulable {
}
private static class OverridableImpl implements Defaulable {
@Override
public String notRequired() {
return "Overridden implementation";
}
}

接口Defaultable中声明了一个默认方法notRequired(),实现类DefaultableImpl没有重写默认方法,而OverridableImpl实现类重写了默认方法,这两种方式在Java 8中都是语法正确的。

如果能够理解在接口中增加默认方法的话,那么理解接口中增加已实现的static方法应该也是没有问题的。

1
2
3
4
5
6
private interface DefaulableFactory {
// Interfaces now allow static methods
static Defaulable create(Supplier<Defaulable> supplier) {
return supplier.get();
}
}

官方宣称JVM平台的接口中默认方法实现是很高效的,并且方法调用的字节码指令支持默认方法,默认方法使已存在的接口可以修改而不会影响编译的过程,Java 8的java.util.Collection中增加的额外方法比如:stream(),forEach(),removeIf()等就是很好的例子。尽管默认方法很强大,但是在使用之前一定要考虑到在实际的工业环境中,由于类之间多重的实现和继承关系的情况下,默认方法会不会导致额外的代价。

方法引用

方法引用提供了一种方式直接访问类或者实例已经存在的方法或者构造方法。方法引用结合Lambda表达式可以使语法结构更紧凑简洁。方法引用分为四种方式。

  • 构造方法引用,语法是Class::new,对于泛型则是Class<T>::new
  • 静态方法引用,语法是Class::static_method
  • 类实例方法的引用,语法是Class::method
  • 类的实例引用实例方法,语法是instance::method

方法引用可以取代简单的Lambda表达式,使得代码结构更清晰,提高代码可读性。感兴趣的同学可以参考官方文档

Java库的新特性

除了Lambda表达式之外,Java 8中最让人期待的就是Java库修改及新增的新特性。在这些新增的新特性中,有一些非常好的API,首先是Optional,Optional是Java为解决判断null而加入的;然后是Stream ,Stream是非常强大的扩展,与Lambda结合一起使用会有更好的效果;最后是日期时间API,这个是广大Java开发人员期盼已久的事情。

Optional

NullPointerException是程序开发中最常见的bug,而Optional最早是Google Guava项目中引入以解决空指针异常的一种方式,Guava项目不赞成代码被判断null的代码污染,期望程序员写更整洁的代码,而现在Optional正式成为Java 8的一部分。

Optional是一个容器,可以保存一些类型的变量或者null。它提供了很多有用的方法,将空指针判断的非常优雅,代码实例如下:

1
2
3
4
Optional<String> fullName = Optional.ofNullable(null);
System.out.println("Full Name is set? " + fullName.isPresent());
System.out.println("Full Name: " + fullName.orElseGet(() -> "[none]"));
System.out.println(fullName.map(s -> "Hey " + s + "!").orElse("Hey Stranger!"));

Optional实例有非空的值,方法isPresent()返回true否则就是false。方法orElseGet()提供了回退机制,当Optional实例的值为空时,返回默认值。方法map()转化当前Optional的值并返回新的Optional实例,orElse()方法类似于orElseGet(),但是它不接受方法,只接受一个默认值。

Stream

新增加的Stream API为Java引入了有实际意义的函数式编程,它使得Java程序不再臃肿,大大提高开发效率。Stream只能操作Collection,不支持Map。Stream运算分为中间的(intermediate)、末端的(terminal),中间运算返回Stream自身,末端运算返回具有特定类型的结果。Stream中包括filtersortedmapcountreduce等。接口规范如下:

  • filter,接收一个Predicate来过滤流中所有的元素,为中间运算,可以在过滤结果上调用其他stream运算。
  • sorted,是一个返回流的排序视图的中间运算。如果不传递定制的Comparator,元素将以自然排序方式进行排序。
  • map,将每个元素通过给定的函数转变为其他对象。
  • reduce,使用给定的函数对流的元素进行一个减缩运算。结果是一个保存有减缩值的Optional。
  • parallelStream,返回的也是流对象,不过是并行流,可以更好的利用多核CPU的硬件优势,减少运算时间。

为了说明Stream的用法,通过一些代码实例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Streams {
private enum Status {
OPEN, CLOSED
};
private static final class Task {
private final Status status;
private final Integer points;
Task(final Status status, final Integer points) {
this.status = status;
this.points = points;
}
public Integer getPoints() {
return points;
}
public Status getStatus() {
return status;
}
@Override
public String toString() {
return String.format("[%s, %d]", status, points);
}
}
}

初始化Task数据:

1
2
3
4
5
final Collection<Task> tasks = Arrays.asList(
new Task(Status.OPEN, 5),
new Task(Status.OPEN, 13),
new Task(Status.CLOSED, 8)
);

然后是针对上述代码的提出几个要解决的问题,我会试着使用Stream API解决这些问题,在看到代码的时候,就会知道Stream的强大。

计算所有开放的Task的点数和

1
2
3
4
5
6
7
8
// Calculate total points of all active tasks using sum()
final long totalPointsOfOpenTasks = tasks
.stream()
.filter(task -> task.getStatus() == Status.OPEN)
.mapToInt(Task::getPoints)
.sum();
System.out.println("Total points: " + totalPointsOfOpenTasks);

console的输出:Total points: 18

并行处理计算Task的点数和

1
2
3
4
5
6
7
8
// Calculate total points of all tasks
final double totalPoints = tasks
.stream()
.parallel()
.map(task -> task.getPoints()) // or map(Task::getPoints)
.reduce(0, Integer::sum);
System.out.println("Total points (all tasks): " + totalPoints);

console的输出:Total points (all tasks): 26.0

对Task集合中的元素进行分组

1
2
3
4
5
// Group tasks by their status
final Map<Status, List<Task>> map = tasks
.stream()
.collect(Collectors.groupingBy(Task::getStatus));
System.out.println(map);

console的输出:{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}

计算整个集合中每个Task分数的平均值

1
2
3
4
5
6
7
8
9
10
11
12
// Calculate the weight of each tasks (as percent of total points)
final Collection<String> result = tasks
.stream() // Stream<String>
.mapToInt(Task::getPoints) // IntStream
.asLongStream() // LongStream
.mapToDouble(points -> points / totalPoints) // DoubleStream
.boxed() // Stream<Double>
.mapToLong(weigth -> (long)(weigth * 100)) // LongStream
.mapToObj(percentage -> percentage + "%") // Stream<String>
.collect(Collectors.toList()); // List<String>
System.out.println(result);

console的输出:[19%, 50%, 30%]

时间日期API


在Java 8之前,Java的Date库非常不好用,在开发过程中总是会有一些莫名其妙的问题,这次Java 8中引入了Joda-Time的精华,对原有的接口做了很多调整,也增加了新的一些实用的接口。新增的时间处理类包括:

  • LocalTime,保存ISO-8601日期系统的时间部分,不保存时区信息;
  • LocalDate,保存ISO-8601日期系统的日期部分,且保存时区信息;
  • LocalDateTime,保存ISO-8601日期系统的日期和时间,不保存时区信息;
  • ZonedDateTime,保存ISO-8601日期系统的日期和时间,且保存时区信息;
  • Duration,可以精确计算两个日期之间的时间差。

Java 8这次时间日期API的改动还是很不错的,关于时间API的变动的详细信息请参考官方文档

并行数组

Java 8新增加了很多方法支持并行的数组处理。最明显的是parallelSort()方法,在多核计算机上,parallelSort()方法可以显著的提高排序的速度。

Java 8给开发人员带来了很多可以提高生产力的特性,不过Java 8刚发布不久,现在还不适合将生产环境迁移到Java 8,但是我们可以在开发过程中尽量少的使用与Java 8不兼容的代码和第三方库。Java 8引入了很多Google Guava的特性,如果想要在开发过程中体验Java 8的新特性,建议在开发中引入Google Guava库。

参考