java 程序员必须掌握的 5 个注解
虽然开始时覆盖方法看起来很简单,但是如果执行不正确,则可能会引入许多微小的 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.javaFoo.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()); ? ? ? ? // 10System.out.println(bar.doSomethingElse()); ? ? // 20
@Override 注解不仅不限于超类中的具体或抽象方法,而且还可用于确保接口的方法也被覆盖(从 JDK 6 开始):
public interface 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 进行注解。有关有效覆盖的更多信息,请参阅《Overriding and Hiding》文档 以及《Java Language Specification (JLS)》的第 9.6.4.4 章节。
随着 JDK 8 中 lambda 表达式的引入,函数式接口在 Java 中变得越来越流行。这些特殊类型的接口可以用 lambda 表达式、方法引用或构造函数引用代替。根据 @FunctionalInterface 文档,函数式接口的定义如下:
一个函数式接口只有一个抽象方法。由于默认方法有一个实现,所以它们不是抽象的。
例如,以下接口被视为函数式接口:
public interface Foo { ? ?public int doSomething();}public interface 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); ? ?// 10consumer.consumeBar(() -> 20); ? ?// 20
重点要注意的是,抽象类,即使它们只包含一个抽象方法,也不是函数式接口。更多信息,请参阅首席 Java 语言架构师 Brian Goetz 编写的《Allow lambdas to implement abstract classes》。与 @Override 注解类似,Java 编译器提供了 @FunctionalInterface 注解以确保接口确实是函数式接口。例如,我们可以将此注解添加到上面创建的接口中:
@FunctionalInterfacepublic interface Foo { ? ?public int doSomething();}@FunctionalInterfacepublic interface Bar { ? ?public int doSomething(); ? ?public default int doSomethingElse() { ? ? ? ?return 1; ? ?}}
如果我们错误地将接口定义为非函数接口并用 @FunctionalInterface 注解了错误的接口,则 Java 编译器会发出错误。例如,我们可以定义以下带注解的非函数式接口:
@FunctionalInterfacepublic interface Foo { ? ?public int doSomething(); ? ?public int doSomethingElse();}
如果我们试图编译这个接口,则会收到以下错误:
$ javac Foo.javaFoo.java:1: error: Unexpected @FunctionalInterface annotation@FunctionalInterface^ ?Foo is not a functional interface ? ?multiple non-overriding abstract methods found in interface Foo1 error
使用这个注解,我们可以确保我们不会错误地创建原本打算用作函数式接口的非函数式接口。需要注意的是,即使在 @FunctionalInterface 注解不存在的情况下,接口也可以用作函数式接口(可以替代为 lambdas,方法引用和构造函数引用),正如我们前面的示例中所见的那样。这类似于 @Override 注解,即一个方法是可以被覆盖的,即使它不包含 @Override 注解。在这两种情况下,注解都是允许编译器执行期望意图的可选技术。
有关 @FunctionalInterface 注解的更多信息,请参阅 @FunctionalInterface 文档和《JLS》的第 4.6.4.9 章节。点击这里阅读 Java 10?新特性实战教程。
警告是所有编译器的重要组成部分,为开发人员提供的反馈——可能危险的行为或在未来的编译器版本中可能会出现的错误。例如,在 Java 中使用泛型类型而没有其关联的正式泛型参数(称为原始类型)会导致警告,就像使用不推荐使用的代码一样(请参阅下面的 @Deprecated 部分)。虽然这些警告很重要,但它们可能并不总是适用甚至并不总是正确的。例如,可能会有对不安全的类型转换发生警告的情况,但是基于使用它的上下文,我们可以保证它是安全的。
为了忽略某些上下文中的特定警告,JDK 5 中引入了 @SuppressWarnings 注解。此注解接受一个或多个字符串参数——描述要忽略的警告名称。虽然这些警告的名称通常在编译器实现之间有所不同,但有 3 种警告在 Java 语言中是标准化的(因此在所有 Java 编译器实现中都很常见):
unchecked:表示类型转换未经检查的警告(编译器无法保证类型转换是安全的),导致发生的可能原因有访问原始类型的成员(参见《JLS》4.8 章节)、窄参考转换或不安全的向下转换(参见《JLS》5.1.6 章节)、未经检查的类型转换(参见《JLS》5.1.9 章节)、使用带有可变参数的泛型参数(参见《JLS》8.4.1 章节和下面的 @SafeVarargs 部分)、使用无效的协变返回类型(参见《JLS》8.4.8.3 章节)、不确定的参数评估(参见《JLS》15.12.4.2 章节),未经检查的方法引用类型的转换(参见《JLS》15.13.2 章节)、或未经检查的 lambda 类型的对话(参见《JLS》15.27.3 章节)。
deprecation:表示使用了已弃用的方法、类、类型等的警告(参见《JLS》9.6.4.6 章节和下面的 @Deprecated 部分)。
removal:表示使用了最终废弃的方法、类、类型等的警告(参见《JLS》9.6.4.6 章节和下面的 @Deprecated 部分)。
为了忽略特定的警告,可以将 @SuppressedWarning 注解与抑制警告(以字符串数组的形式提供)的一个或多个名字添加到发生警告的上下文中:
public class Foo { ? ?public void doSomething(@SuppressWarnings("rawtypes") List myList) { ? ? ? ?// Do something with myList ? ?}}
@SuppressWarnings 注解可用于以下任何一种情况:
类型
域
方法
参数
构造函数
局部变量
模块
一般来说,@SuppressWarnings 注解应该应用于最直接的警告范围。例如,如果方法中的局部变量应忽略警告,则应将 @SuppressWarnings 注解应用于局部变量,而不是包含局部变量的方法或类:
public class Foo { ? ?public void doSomething() { ? ? ? ?@SuppressWarnings("rawtypes") ? ? ? ?List myList = new ArrayList(); ? ? ? ?// Do something with myList ? ?}}
可变参数在 Java 中是一种很有用的技术手段,但在与泛型参数一起使用时,它们也可能会导致一些严重的问题。由于泛型在 Java 中是非特定的,所以具有泛型类型的变量的实际(实现)类型不能在运行时被断定。由于无法做出此判断,因此变量可能会存储非其实际类型的引用到类型,如以下代码片段所示(摘自《Java Generics FAQs》):
List ln = new ArrayList<Number>();ln.add(1);List<String> ls = ln; ? ? ? ? ? ? ? ? // unchecked warning String s = ls.get(0); ? ? ? ? ? ? ? ? // ClassCastException
在将 ln 分配给 ls 后,堆中存在变量 ls,该变量具有 List<String>的类型,但存储引用到实际为 List<Number>类型的值。这个无效的引用被称为堆污染。由于直到运行时才能确定此错误,因此它会在编译时显示为警告,并在运行时出现 ClassCastException。当泛型参数与可变参数组合时,可能会加剧此问题:
public class Foo { ? ?public <T> void doSomething(T... args) { ? ? ? ?// ... ? ?}}
在这种情况下,Java 编译器会在调用站点内部创建一个数组来存储可变数量的参数,但是 T 的类型并未实现,因此在运行时会丢失。实质上,到 doSomething 的参数实际上是 Object[]类型。如果依赖 T 的运行时类型,那么这会导致严重的问题,如下面的代码片段所示:
public class Foo { ? ?public <T> void doSomething(T... args) { ? ? ? ?Object[] objects = args; ? ? ? ?String string = (String) objects[0]; ? ?}}Foo foo = new Foo();foo.<Number>doSomething(1, 2);
如果执行此代码片段,那么将导致 ClassCastException,因为在调用站点传递的第一个 Number 参数不能转换为 String(类似于独立堆污染示例中抛出的 ClassCastException)。通常,可能会出现以下情况:编译器没有足够的信息来正确确定通用可变参数的确切类型,这会导致堆污染,这种污染可以通过允许内部可变参数数组从方法中转义来传播,如下面摘自《Effective Java》第 3 版 pp.147 的例子:
public static <T> T[] toArray(T... args) { ? ?return args;}
在某些情况下,我们知道方法实际上是类型安全的,不会造成堆污染。如果可以在保证的情况下做出这个决定,那么我们可以使用 @SafeVarargs 注解来注解该方法,从而抑制与可能的堆污染相关的警告。但是,这引出了一个问题:什么时候通用可变参数方法会被认为是类型安全的?Josh Bloch 在《Effective Ja
va》第 3 版第 147 页的基础上提供了一个完善的解决方案——基于方法与内部创建的用于存储其可变参数的数组的交互:
如果方法没有存储任何东西到数组(这会覆盖参数)且不允许对数组的引用进行转义(这会使得不受信任的代码可以访问数组),那么它是安全的。换句话说,如果可变参数数组仅用于从调用者向方法传递可变数量的参数——毕竟,这是可变参数的目的——那么该方法是安全的。
因此,如果我们创建了以下方法(来自 pp.149 同上),那么我们可以用 @SafeVarags 注解来合理地注解我们的方法:
@SafeVarargsstatic <T> List<T> flatten(List<? extends T>... lists) { ? ?List<T> result = new ArrayList<>(); ? ?for (List<? extends T> list : lists) { ? ? ? ?result.addAll(list); ? ?} ? ?return result;}
有关 @SafeVarargs 注解的更多信息,请参阅 @SafeVarargs 文档,《JLS》9.6.4.7 章节以及《Effective Java》第 3 版中的 Item32。点击这里阅读 Java 10?新特性实战教程。
在开发代码时,有时候代码会变得过时和不应该再被使用。在这些情况下,通常会有个替补的更适合手头的任务,且虽然现存的对过时代码的调用可能会保留,但是所有新的调用都应该使用替换方法。这个过时的代码被称为不推荐使用的代码。在某些紧急情况下,不建议使用的代码可能会被删除,应该在未来的框架或库版本从其代码库中删除弃用的代码之前立即转换为替换代码。
为了支持不推荐使用的代码的文档,Java 包含 @Deprecated 注解,它会将一些构造函数、域、局部变量、方法、软件包、模块、参数或类型标记为已弃用。如果弃用的元素(构造函数,域,局部变量等)被使用了,则编译器发出警告。例如,我们可以创建一个弃用的类并按如下所示使用它:
@Deprecatedpublic class Foo {}Foo foo = new Foo();
如果我们编译此代码(在命名为 Main.java 的文件中),我们会收到以下警告:
$ javac Main.javaNote: Main.java uses or overrides a deprecated API.Note: Recompile with -Xlint:deprecation for details.
评论