下载贤集网APP入驻自媒体
在本文中,我们将看到5个Java编译器支持的注解,并了解其期望用途。顺便,我们将探索其创建背后的基本原理,围绕其用途的一些特质,以及正确应用的一些例子。虽然其中有些注解比其他注解更为常见,但非初学Java开发人员都应该消化了解每个注解。
覆盖方法的实现或为抽象方法提供实现的能力是任何面向对象(OO)语言的核心。由于Java是OO语言,具有许多常见的面向对象的抽象机制,所以在非终极超类定义的非最终方法或接口中的任何方法(接口方法不能是最终的)都可以被子类覆盖。虽然开始时覆盖方法看起来很简单,但是如果执行不正确,则可能会引入许多微小的bug。例如,用覆盖类类型的单个参数覆盖Object#equals方法就是一种常见的错误:
public class Foo {
public boolean equals(Foo foo) {
// Check if the supplied object is equal to this object
}
}
由于所有类都隐式地从Object类继承,Foo类的目的是覆盖Object#equals方法,因此Foo可被测试是否与Java中的任何其他对象相等。虽然我们的意图是正确的,但我们的实现则并非如此。实际上,我们的实现根本不覆盖Object#equals方法。相反,我们提供了方法的重载:我们不是替换Object类提供的equals方法的实现,而是提供第二个方法来专门接受Foo对象,而不是Object对象。我们的错误可以用简单实现来举例说明,该实现对所有的相等检查都返回true,但当提供的对象被视为Object(Java将执行的操作,例如在Java Collections Framework即JCF中)时,就永远不会调用它:
public class Foo {
public boolean equals(Foo foo) {
return true;
}
}
Object foo = new Foo();
Object identicalFoo = new Foo();
System.out.println(foo.equals(identicalFoo)); // false
这是一个非常微妙但常见的错误,可以被编译器捕获。我们的意图是覆盖Object#equals方法,但因为我们指定了一个类型为Foo而不是Object类型的参数,所以我们实际上提供了重载的Object#equals方法,而不是覆盖它。为了捕获这种错误,我们引入@Override注解,它指示编译器检查覆盖实际有没有执行。如果没有执行有效的覆盖,则会抛出错误。因此,我们可以更新Foo类,如下所示:
public class Foo {
@Override
public boolean equals(Foo foo) {
return true;
}
}
如果我们尝试编译这个类,我们现在收到以下错误:
$ javac Foo.java
Foo.java:3: error: method does not override or implement a method from a supertype
@Override
^
1 error
实质上,我们已经将我们已经覆盖方法的这一隐含的假设转变为由编译器进行的显性验证。如果我们的意图被错误地实现,那么Java编译器会发出一个错误——不允许我们不正确实现的代码被成功编译。通常,如果以下任一条件不满足,则Java编译器将针对使用@Override注解的方法发出错误(引用自Override注解文档):
该方法确实会覆盖或实现在超类中声明的方法。
该方法的签名与在Object中声明的任何公共方法(即equals或hashCode方法)的签名覆盖等价(override-equivalent)。
因此,我们也可以使用此注解来确保子类方法实际上也覆盖超类中的非最终具体方法或抽象方法:
public abstract class Foo {
public int doSomething() {
return 1;
}
public abstract int doSomethingElse();
}
public class Bar extends Foo {
@Override
public int doSomething() {
return 10;
}
@Override
public int doSomethingElse() {
return 20;
}
}
Foo bar = new Bar();
System.out.println(bar.doSomething()); // 10
System.out.println(bar.doSomethingElse()); // 20
@Override注解不仅不限于超类中的具体或抽象方法,而且还可用于确保接口的方法也被覆盖(从JDK 6开始):
public inter Foo {
public int doSomething();
}
public class Bar implements Foo {
@Override
public int doSomething() {
return 10;
}
}
Foo bar = new Bar();
System.out.println(bar.doSomething()); // 10
通常,覆盖非final类方法、抽象超类方法或接口方法的任何方法都可以使用@Override进行注解。
@FunctionalInter
随着JDK 8中lambda表达式的引入,函数式接口在Java中变得越来越流行。这些特殊类型的接口可以用lambda表达式、方法引用或构造函数引用代替。根据@FunctionalInter文档,函数式接口的定义如下:
一个函数式接口只有一个抽象方法。由于默认方法有一个实现,所以它们不是抽象的。
例如,以下接口被视为函数式接口:
public inter Foo {
public int doSomething();
}
public inter Bar {
public int doSomething();
public default int doSomethingElse() {
return 1;
}
}
因此,下面的每一个都可以用lambda表达式代替,如下所示:
public class FunctionalConsumer {
public void consumeFoo(Foo foo) {
System.out.println(foo.doSomething());
}
public void consumeBar(Bar bar) {
System.out.println(bar.doSomething());
}
}
FunctionalConsumer consumer = new FunctionalConsumer();
consumer.consumeFoo(() -> 10); // 10
consumer.consumeBar(() -> 20); // 20
重点要注意的是,抽象类,即使它们只包含一个抽象方法,也不是函数式接口。更多信息,请参阅首席Java语言架构师Brian Goetz编写的《Allow lambdas to implement abstract classes》。与@Override注解类似,Java编译器提供了@FunctionalInter注解以确保接口确实是函数式接口。例如,我们可以将此注解添加到上面创建的接口中:
@FunctionalInter
public inter Foo {
public int doSomething();
}
@FunctionalInter
public inter Bar {
public int doSomething();
public default int doSomethingElse() {
return 1;
}
}
如果我们错误地将接口定义为非函数接口并用@FunctionalInter注解了错误的接口,则Java编译器会发出错误。例如,我们可以定义以下带注解的非函数式接口:
@FunctionalInter
public inter Foo {
public int doSomething();
public int doSomethingElse();
}
如果我们试图编译这个接口,则会收到以下错误:
$ javac Foo.java
Foo.java:1: error: Unexpected @FunctionalInter annotation
@FunctionalInter
^
Foo is not a functional inter
multiple non-overriding abstract methods found in inter Foo
1 error
使用这个注解,我们可以确保我们不会错误地创建原本打算用作函数式接口的非函数式接口。需要注意的是,即使在@FunctionalInter注解不存在的情况下,接口也可以用作函数式接口(可以替代为lambdas,方法引用和构造函数引用),正如我们前面的示例中所见的那样。这类似于@Override注解,即一个方法是可以被覆盖的,即使它不包含@Override注解。在这两种情况下,注解都是允许编译器执行期望意图的可选技术。