Java 程序员必须掌握的 5 个注解!

来源:codeceo
www.codeceo.com/5-annotations-every-java-developer-should-know.htmlhtml

划重点java

自 JDK5 推出以来,注解已成为Java生态系统不可缺乏的一部分。虽然开发者为Java框架(例如Spring的@Autowired)开发了无数的自定义注解,但编译器承认的一些注解很是重要。面试

在本文中,咱们将看到5个Java编译器支持的注解,并了解其指望用途。顺便,咱们将探索其建立背后的基本原理,围绕其用途的一些特质,以及正确应用的一些例子。虽然其中有些注解比其余注解更为常见,但非初学Java开发人员都应该消化了解每一个注解。后端

  • @Override数组

  • @FunctionalInterface安全

  • @SuppressWarnings多线程

  • @SafeVarargs架构

  • @Deprecated框架

首先,咱们将深刻研究Java中最经常使用的注解之一:@Override。ide

@Override

覆盖方法的实现或为抽象方法提供实现的能力是任何面向对象(OO)语言的核心。因为Java是OO语言,具备许多常见的面向对象的抽象机制,因此在非终极超类定义的非最终方法或接口中的任何方法(接口方法不能是最终的)均可以被子类覆盖。点击这里阅读 Java 10 新特性实战教程。

虽然开始时覆盖方法看起来很简单,可是若是执行不正确,则可能会引入许多微小的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 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章节。

@FunctionalInterface

随着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);    // 10
consumer.consumeBar(() -> 20);    // 20

重点要注意的是,抽象类,即便它们只包含一个抽象方法,也不是函数式接口。更多信息,请参阅首席Java语言架构师Brian Goetz编写的《Allow lambdas to implement abstract classes》。与@Override注解相似,Java编译器提供了@FunctionalInterface注解以确保接口确实是函数式接口。例如,咱们能够将此注解添加到上面建立的接口中:

@FunctionalInterface
public interface Foo {
	public int doSomething();
}

@Functional
Interfacepublic interface Bar {
	public int doSomething();
	public default int doSomethingElse() {
		return 1;
	}
}

若是咱们错误地将接口定义为非函数接口并用@FunctionalInterface注解了错误的接口,则Java编译器会发出错误。例如,咱们能够定义如下带注解的非函数式接口:

@FunctionalInterface
public interface Foo {
	public int doSomething();
	public int doSomethingElse();
}

若是咱们试图编译这个接口,则会收到如下错误:

$ javac Foo.java
Foo.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 新特性实战教程。

@SuppressWarnings

警告是全部编译器的重要组成部分,为开发人员提供的反馈——可能危险的行为或在将来的编译器版本中可能会出现的错误。例如,在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
    }
}

@SafeVarargs

可变参数在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 的类型,但存储引用到实际为List 类型的值。这个无效的引用被称为堆污染。因为直到运行时才能肯定此错误,所以它会在编译时显示为警告,并在运行时出现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 Java》第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 新特性实战教程。

@Deprecated

在开发代码时,有时候代码会变得过期和不该该再被使用。在这些状况下,一般会有个替补的更适合手头的任务,且虽然现存的对过期代码的调用可能会保留,可是全部新的调用都应该使用替换方法。这个过期的代码被称为不推荐使用的代码。在某些紧急状况下,不建议使用的代码可能会被删除,应该在将来的框架或库版本从其代码库中删除弃用的代码以前当即转换为替换代码。

为了支持不推荐使用的代码的文档,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.

一般,每当使用@Deprecated注解的元素时,都会引起警告,除了用于如下五种状况:

  • 声明自己就被声明为是弃用的(即递归调用)。

  • 声明被注解禁止弃用警告(即@SuppressWarnings(“deprecation”)注解,如上所述,应用于使用弃用元素的上下文。

  • 使用和声明都在同一个最外面的类中(即,若是类调用其自己的弃用方法)。

  • 用在import声明中,该声明导入一般不同意使用的类型或构件(即,在将已弃用的类导入另外一个类时)。

  • exports或opens指令内。

正如前面所说的,在某些状况下,当不推荐使用的元素将被删除,则调用代码应当即删除不推荐使用的元素(称为terminally deprecated code)。在这种状况下,可使用forRemoval参数提供的@Deprecated注解,以下所示:

@Deprecated(forRemoval = true)public class Foo {}

使用此最终弃用代码会致使一系列更严格的警告:

$ javac Main.java
Main.java:7: warning: \[removal\] Foo in com.foo has been deprecated and marked for removal
                Foo foo = new Foo();
                ^
Main.java:7: warning: \[removal\] Foo in com.foo has been deprecated and marked for removal
                Foo foo = new Foo();
                              ^2 warnings

除了标准@Deprcated注解所描述的相同异常以外,老是会发出最终弃用的警告。咱们还能够经过为注解提供since变量来添加文档到@Deprecated注解中:

@Deprecated(since = "1.0.5", forRemoval = true)public class Foo {}

可使用@deprecated JavaDoc元素(注意小写字母d)进一步文档化已弃用的元素,如如下代码片断所示:

/**
 * Some test class.
 *
 * @deprecated Replaced by {@link com.foo.NewerFoo}.
 *
 * @author Justin Albano
 */@Deprecated(since = "1.0.5", forRemoval = true)public class Foo {}

JavaDoc工具将生成如下文档:

有关@Deprecated注解的更多信息,请参阅@Deprecated文档和《JLS》9.6.4.6章节。

结尾

自JDK 5引入注解以来,注解一直是Java不可缺乏的一部分。虽然有些注解比其余注解更受欢迎,但本文中介绍的这5种注解是新手级别以上的开发人员都应该理解和掌握的:

  • @Override

  • @FunctionalInterface

  • @SuppressWarnings

  • @SafeVarargs

  • @Deprecated

虽然每种方法都有其独特的用途,但全部这些注解使得Java应用程序更具可读性,并容许编译器对咱们的代码执行一些其余隐含的假设。随着Java语言的不断发展,这些通过实践验证的注解可能服务多年,帮助确保更多的应用程序按开发人员的意图行事。

推荐去个人博客阅读更多:

1.Java JVM、集合、多线程、新特性系列教程

2.Spring MVC、Spring Boot、Spring Cloud 系列教程

3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

4.Java、后端、架构、阿里巴巴等大厂最新面试题

以为不错,别忘了点赞+转发哦!