- 「MoreThanJava」 宣扬的是 「学习,不止 CODE」,本系列 Java 基础教程是自己在结合各方面的知识之后,对 Java 基础的一个总回顾,旨在 「帮助新朋友快速高质量的学习」。
- 当然 不论新老朋友 我相信您都可以 从中获益。如果觉得 「不错」 的朋友,欢迎 「关注 + 留言 + 分享」,文末有完整的获取链接,您的支持是我前进的最大的动力!
特性总览
以下是 Java 8 中的引入的部分新特性。关于 Java 8 新特性更详细的介绍可参考这里。
- 接口默认方法和静态方法
- Lambda 表达式
- 函数式接口
- 方法引用
- Stream
- Optional
- Date/Time API
- 重复注解
- 扩展注解的支持
- Base64
- JavaFX
- 其它
- JDBC 4.2 规范
- 更好的类型推测机制
- HashMap 性能提升
- IO/NIO 的改进
- JavaScript 引擎 Nashorn
- 并发(Concurrency)
- 类依赖分析器 jdeps
- JVM 的 PermGen 空间被移除
一. 接口默认方法和静态方法
接口默认方法
在 Java 8 中,允许为接口方法提供一个默认的实现。必须用 default
修饰符标记这样一个方法,例如 JDK 中的 Iterator
接口:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationExceition("remove"); }
}
这将非常有用!如果你要实现一个迭代器,就需要提供 hasNext()
和 next()
方法。这些方法没有默认实现——它们依赖于你要遍历访问的数据结构。不过,如果你的迭代器是 只读 的,那么就不用操心实现 remove()
方法。
默认方法也可以调用其他方法,例如,我们可以改造 Collection
接口,定义一个方便的 isEmpty()
方法:
public interface Collection {
int size(); // an abstract method
default boolean isEmpty() {
return size() == 0; }
}
这样,实现 Collection
的程序员就不用再操心实现 isEmpty()
方法了。
在 JVM 中,默认方法的实现是非常高效的,并且通过字节码指令为方法调用提供了支持。默认方法允许继续使用现有的 Java 接口,而同时能够保障正常的编译过程。这方面好的例子是大量的方法被添加到java.util.Collection
接口中去:stream()
,parallelStream()
,forEach()
,removeIf()
等。尽管默认方法非常强大,但是在使用默认方法时我们需要小心注意一个地方:在声明一个默认方法前,请仔细思考是不是真的有必要使用默认方法。
解决默认方法冲突
如果先在一个接口中将一个方法定义为默认方法,然后又在类或另一个接口中定义同样的方法,会发生什么?
// 测试接口 1
public interface TestInterface1 {
default void sameMethod() {
System.out.println("Invoke TestInterface1 method!"); }
}
// 测试接口 2
public interface TestInterface2 {
default void sameMethod() {
System.out.println("Invoke TestInterface2 method!"); }
}
// 继承两个接口的测试类
public class TestObject implements TestInterface1, TestInterface2 {
@Override
public void sameMethod() {
// 这里也可以选择两个接口中的一个默认实现
// 如: TestInterface1.super.sameMethod();
System.out.println("Invoke Object method!");
}
}
// 测试类
public class Tester {
public static void main(String[] args) {
TestObject testObject = new TestObject();
testObject.sameMethod();
}
}
测试输出:
Invoke Object method!
➡️ 对于 Scale
或者 C++
这些语言来说,解决这种具有 二义性 的情况规则会很复杂,Java
的规则则简单得多:
- 类优先。如果本类中提供了一个具体方法符合签名,则同名且具有相同参数列表的接口中的默认方法会被忽略;
- 接口冲突。如果一个接口提供了一个默认方法,另一个接口提供了一个同名且参数列表相同的方法 (顺序和类型都相同) ,则必须覆盖这个方法来解决冲突 (就是👆代码的情况,不覆盖编译器不会编译…);
Java 设计者更强调一致性,让程序员自己来解决这样的二义性似乎也显得很合理。如果至少有一个接口提供了一个实现,编译器就会报告错误,程序员就必须解决这个二义性。(如果两个接口都没有为共享方法提供默认实现,则不存在冲突,要么实现,要么不实现…)
➡️ 我们只讨论了两个接口的命名冲突。现在来考虑另一种情况,一个类继承自一个类,同时实现了一个接口,从父类继承的方法和接口拥有同样的方法签名,又将怎么办呢?
// 测试接口
public interface TestInterface {
default void sameMethod() {
System.out.println("Invoke TestInterface Method!"); }
}
// 父类
public class Father {
void sameMethod() {
System.out.println("Invoke Father Method!"); }
}
// 子类
public class Son extends Father implements TestInterface {
@Override
public void sameMethod() {
System.out.println("Invoke Son Method!");
}
}
// 测试类
public class Tester {
public static void main(String[] args) {
new Son().sameMethod(); }
}
程序输出:
COPYInvoke Son Method!
还记得我们说过的方法调用的过程吗 (先找本类的方法找不到再从父类找)?加上这里提到的 “类优先” 原则 (本类中有方法则直接调用),这很容易理解!
千万不要让一个默认方法重新定义
Object
类中的某个方法。例如,不能为toString()
或equals()
定义默认方法,尽管对于 List 之类的接口这可能很有吸引力,但由于 类优先原则,这样的方法绝对无法超越Object.toString()
或者Object.equals()
。
接口静态方法
在 Java 8 中,允许在接口中增加静态方法 (允许不构建对象而直接使用的具体方法)。理论上讲,没有任何理由认为这是不合法的,只是这有违将接口作为抽象规范的初衷。
例子:
public interface StaticInterface {
static void method() {
System.out.println("这是Java8接口中的静态方法!");
}
}
调用:
public class Main {
public static void main(String[] args) {
StaticInterface.method(); // 输出 这是Java8接口中的静态方法!
}
}
目前为止,通常的做法都是将静态方法放在 伴随类 (可以理解为操作继承接口的实用工具类) 中。在标准库中,你可以看到成对出现的接口和实用工具类,如 Collection/ Collections
或 Path/ Paths
。
在 Java 11 中,Path
接口就提供了一个与之工具类 Paths.get()
等价的方法 (该方法用于将一个 URI 或者字符串序列构造成一个文件或目录的路径):
COPYpublic interface Path {
public static Path of(String first, String... more) {
... }
public static Path of(URI uri) {
... }
}
这样一来,Paths
类就不再是必要的了。类似地,如果实现你自己的接口时,没有理由再额外提供一个带有实用方法的工具类。
➡️ 另外,在 Java 9 中,接口中的方法可以是 private
。private
方法可以是静态方法或实例方法。由于私有方法只能在接口本身的方法中使用,所以它们的用法很有限,只能作为接口中其他方法的辅助方法。
二. Lambda 表达式
Lambda
表达式 (也称为闭包) 是整个 Java 8 发行版中最受期待的在 Java 语言层面上的改变,Lambda
允许把函数作为一个方法的参数,即 行为参数化,函数作为参数传递进方法中。
什么是 Lambda 表达式
我们知道,对于一个 Java 变量,我们可以赋给一个 「值」。
如果你想把 「一块代码」 赋给一个 Java 变量,应该怎么做呢?
比如,我想把右边的代码块,赋值给一个叫做 blockOfCode
的 Java 变量:
在 Java 8 之前,这个是做不到的,但是 Java 8 问世之后,利用 Lambda 特性,就可以做到了。
当然,这个并不是一个很简洁的写法,所以为了让这个赋值操作变得更加优雅,我们可以移除一些没有必要的声明。
这样,我们就成功的非常优雅的把「一块代码」赋给了一个变量。而「这块代码」,或者说「这个被赋给一个变量的函数」,就是一个 Lambda 表达式。
但是这里仍然有一个问题,就是变量 blockOfCode
的类型应该是什么?
在 Java 8 里面,**所有的 Lambda 的类型都是一个接口,而 Lambda 表达式本身,也就是「那段代码」,需要是这个接口的实现。**这是理解 Lambda 的一个关键所在,简而言之就是,Lambda 表达式本身就是一个接口的实现。直接这样说可能还是有点让人困扰,我们继续看看例子。我们给上面的 blockOfCode
加上一个类型:
这种只有一个接口函数需要被实现的接口类型,我们叫它「函数式接口」。
为了避免后来的人在这个接口中增加接口函数导致其有多个接口函数需要被实现,变成「非函数接口」,我们可以在这个上面加上一个声明 @FunctionalInterface
, 这样别人就无法在里面添加新的接口函数了:
这样,我们就得到了一个完整的 Lambda 表达式声明:
Lambda 表达式的作用
Lambda 最直观的作用就是使代码变得整洁.。
我们可以对比一下 Lambda 表达式和传统的 Java 对同一个接口的实现:
这两种写法本质上是等价的。但是显然,Java 8 中的写法更加优雅简洁。并且,由于 Lambda 可以直接赋值给一个变量,我们就可以直接把 Lambda 作为参数传给函数, 而传统的 Java 必须有明确的接口实现的定义,初始化才行。
有些情况下,这个接口实现只需要用到一次。传统的 Java 7 必须要求你定义一个“污染环境”的接口实现 MyInterfaceImpl
,而相较之下 Java 8 的 Lambda, 就显得干净很多。
三. 函数式接口
上面我们说到,只有一个接口函数需要被实现的接口类型,我们叫它「函数式接口」。Lambda 表达式配合函数式接口能让我们代码变得干净许多。
Java 8 API 包含了很多内建的函数式接口,在老 Java 中常用到的比如Comparator
或者Runnable
接口,这些接口都增加了@FunctionalInterface
注解以便能用在Lambda
上。
Java 8 API 同样还提供了很多全新的函数式接口来让工作更加方便,有一些接口是来自 Google Guava 库里的,即便你对这些很熟悉了,还是有必要看看这些是如何扩展到 Lambda 上使用的。
1 - Comparator(比较器接口)
Comparator
是老Java中的经典接口, Java 8 在此之上添加了多种默认方法。源代码及使用示例如下:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);
Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");
comparator.compare(p1, p2); // > 0
comparator.reversed().compare(p1, p2); // < 0
2 - Consumer(消费型接口)
Consumer
接口表示执行在单个参数上的操作。源代码及使用示例如下:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
更多的Consumer接口
BiConsumer:void accept(T t, U u);
: 接受两个参数的二元函数DoubleConsumer:void accept(double value);
: 接受一个double参数的一元函数IntConsumer:void accept(int value);
: 接受一个int参数的一元函数LongConsumer:void accept(long value);
: 接受一个long参数的一元函数ObjDoubleConsumer:void accept(T t, double value);
: 接受一个泛型参数一个double参数的二元函数ObjIntConsumer:void accept(T t, int value);
: 接受一个泛型参数一个int参数的二元函数ObjLongConsumer:void accept(T t, long value);
: 接受一个泛型参数一个long参数的二元函数
3 - Supplier(供应型接口)
Supplier
接口是不需要参数并返回一个任意范型的值。其简洁的声明,会让人以为不是函数。这个抽象方法的声明,同 Consumer
相反,是一个只声明了返回值,不需要参数的函数。也就是说 Supplier
其实表达的不是从一个参数空间到结果空间的映射能力,而是表达一种生成能力,因为我们常见的场景中不止是要consume(Consumer)或者是简单的map(Function),还包括了 new
这个动作。而 Supplier
就表达了这种能力。源代码及使用示例如下:
@FunctionalInterface
public interface Supplier<T>