你应该知道的java中的5个注解

前言

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

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

首先,我们将深入研究Java中最常用的注解之一:@Override。

@Override

覆盖方法的实现或为抽象方法提供实现的能力是任何面向对象(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注解不仅不限于超类中的具体或抽象方法,而且还可用于确保接口的方法也被覆盖:

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();
}
@FunctionalInterface
public 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 Foo
1 error

使用这个注解,我们可以确保我们不会错误地创建原本打算用作函数式接口的非函数式接口。需要注意的是,即使在@FunctionalInterface注解不存在的情况下,接口也可以用作函数式接口(可以替代为lambdas,方法引用和构造函数引用),正如我们前面的示例中所见的那样。这类似于@Override注解,即一个方法是可以被覆盖的,即使它不包含@Override注解。在这两种情况下,注解都是允许编译器执行期望意图的可选技术。

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

@SuppressWarnings

警告是所有编译器的重要组成部分,为开发人员提供的反馈——可能危险的行为或在未来的编译器版本中可能会出现的错误。例如,在Java中使用泛型类型而没有其关联的正式泛型参数(称为原始类型)会导致警告,就像使用不推荐使用的代码一样(请参阅下面的@Deprecated部分)。虽然这些警告很重要,但它们可能并不总是适用甚至并不总是正确的。例如,可能会有对不安全的类型转换发生警告的情况,但是基于使用它的上下文,我们可以保证它是安全的。

为了忽略某些上下文中的特定警告,JDK 5中引入了@SuppressWarnings注解。此注解接受一个或多个字符串参数——描述要忽略的警告名称。虽然这些警告的名称通常在编译器实现之间有所不同,但有3种警告在Java语言中是标准化的(因此在所有Java编译器实现中都很常见):

  • unchecked:表示类型转换未经检查的警告(编译器无法保证类型转换是安全的),导致发生的可能原因有访问原始类型的成员、窄参考转换或不安全的向下转换、未经检查的类型转换使用带有可变参数的泛型参数、使用无效的协变返回类型不确定的参数评估,未经检查的方法引用类型的转换或未经检查的lambda类型的对话)。
  • deprecation:表示使用了已弃用的方法、类、类型等的警告)。
  • removal:表示使用了最终废弃的方法、类、类型等的警告

为了忽略特定的警告,可以将@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<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 Java》第3版第147页的基础上提供了一个完善的解决方案——基于方法与内部创建的用于存储其可变参数的数组的交互:

如果方法没有存储任何东西到数组(这会覆盖参数)且不允许对数组的引用进行转义(这会使得不受信任的代码可以访问数组),那么它是安全的。换句话说,如果可变参数数组仅用于从调用者向方法传递可变数量的参数——毕竟,这是可变参数的目的——那么该方法是安全的。

因此,如果我们创建了以下方法(来自pp.149同上),那么我们可以用@SafeVarags注解来合理地注解我们的方法:

@SafeVarargs
static <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文档

@Deprecated

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

为了支持不推荐使用的代码的文档,Java包含@Deprecated注解,它会将一些构造函数、域、局部变量、方法、软件包、模块、参数或类型标记为已弃用。如果弃用的元素(构造函数,域,局部变量等)被使用了,则编译器发出警告。例如,我们可以创建一个弃用的类并按如下所示使用它:

@Deprecated
public class Foo {}
Foo foo = new Foo();

如果我们编译此代码(在命名为Main.java的文件中),我们会收到以下警告:

$ javac Main.java
Note: 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工具将生成以下文档:

结尾

自JDK 5引入注解以来,注解一直是Java不可缺少的一部分。虽然有些注解比其他注解更受欢迎,但本文中介绍的这5种注解是新手级别以上的开发人员都应该理解和掌握的:@Override,@FunctionalInterface,@SuppressWarnings,@SafeVarargs,和@Deprecated。虽然每种方法都有其独特的用途,但所有这些注解使得Java应用程序更具可读性,并允许编译器对我们的代码执行一些其他隐含的假设。随着Java语言的不断发展,这些经过实践验证的注解可能服务多年,帮助确保更多的应用程序按开发人员的意图行事。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java注解与反射原理说明

    一 点睛 注解若想发挥更大作用,还需借助反射机制之力.通过反射,可以取得一个方法上声明的注解的全部内容. 一般有两种需求: 1 取得方法中全部的注解,通过调用getAnnotations来实现. 2 判断操作是否是指定注解,通过调用getAnnotation来实现. 下面从源码角度来说明怎样获取这些注解信息. 二 源码导读--取得方法中全部的注解 public class AccessibleObject implements AnnotatedElement { ... //取得全部Annot

  • 深入理解Java高级特性——注解

    博主在初学注解的时候看到网上的介绍大部分都是直接介绍用法或者功能,没有实际的应用场景,篇幅又很长导致学习的时候难以理解其意图,而且学完就忘QAQ.本篇文章中我将结合实际的应用场景尽可能由浅入深,平缓的介绍java注解. java注解是jdk1.5以后新出的特性,对于它的应用非常广泛,我们首先来看一下注解的应用,百度百科上这样说: 我们可以看到,注解的作用有三方面: 编写doc文档:这个就我们很常用的 @return 以及 @author,加了这些注解以后,就可以用jdk帮我们自动生成对应的API

  • 全面剖析java中的注解(Annotation)

    1.什么是注解 用一个词就可以描述注解,那就是元数据,即一种描述数据的数据.所以,可以说注解就是源代码的元数据.比如,下面这段代码: @Override public String toString() { return "This is String Representation of current object."; } 上面的代码中,我重写了toString()方法并使用了@Override注解.但是,即使我不使用@Override注解标记代码,程序也能够正常执行.那么,该注解

  • 5分钟搞懂java注解@Annotation的具体使用

    首先一句话结论:注解就是一种通过在类.方法.或者属性等上使用类似@xxx的方式进行"打标签",然后可以通过反射机制对标签的内容进行解析并进行相应处理的手段. 注解是java中的一个重要知识点,从java5后开始引入,尤其在spring框架中大量使用.比较常用的有@controller.@service等等各种,本文将从注解的实现原理出发,通过一些demo代码的实现,进行分析. 一. 注解定义方式 直接上代码,看看spring中@Service注解的定义就知道了: @Target({El

  • 使用java的注解(用在java类的方法上的注解)方法

    场景:根据方法上的注解,通过java反射方式找到需要执行的的方法. 1.注解类 /**注解作用在方法上*/ @Target({ElementType.METHOD}) /**注解的生命周期一直程序运行时都存在VM运行期间保留注解,可以通过反射机制读取注解信息*/ @Retention(RetentionPolicy.RUNTIME) /**注解包含在Javadoc中*/ @Documented public @interface Item { String value(); } 2.在类的方法上

  • 你应该知道的java中的5个注解

    前言 自 JDK5 推出以来,注解已成为Java生态系统不可缺少的一部分.虽然开发者为Java框架(例如Spring的@Autowired)开发了无数的自定义注解,但编译器认可的一些注解非常重要. 在本文中,我们将看到5个Java编译器支持的注解,并了解其期望用途.顺便,我们将探索其创建背后的基本原理,围绕其用途的一些特质,以及正确应用的一些例子.虽然其中有些注解比其他注解更为常见,但非初学Java开发人员都应该消化了解每个注解. 首先,我们将深入研究Java中最常用的注解之一:@Overrid

  • Java中lombok的@Builder注解的解析与简单使用详解

    Lombok中@Builder用法 1.建造者模式简介:Builder 使用创建者模式又叫建造者模式.简单来说,就是一步步创建一个对象,它对用户屏蔽了里面构建的细节,但却可以精细地控制对象的构造过程. 2.注解类Builder.java注释: * The builder annotation creates a so-called 'builder' aspect to the class that is annotated or the class  * that contains a mem

  • Java中三种简单注解介绍和代码实例

    简单Java注解 JDK5提供的简单注解类型只有3个. 这三个都是用来预防错误或者进行提醒的,分别是: 1.Override 2.Deprecated 3.Suppresswarnings 需要注意,JDK5(另一个说法,Tiger)实际上并没有许多内置注解;相反,它允许核心Java支持注解特性的能力. JSR-175中严格规定它用来定义元数据功能. 需要由程序员编写自定义的注解类型,其他JSR标准也编写了一系列标准注解类型. 下面将用实例来深入讲解这三个简单注解. Override 注解 Ov

  • Java 中的注解详解及示例代码

    在Java中,注解(Annotation)引入始于Java5,用来描述Java代码的元信息,通常情况下注解不会直接影响代码的执行,尽管有些注解可以用来做到影响代码执行. 注解可以做什么 Java中的注解通常扮演以下角色 编译器指令 构建时指令 运行时指令 其中 Java内置了三种编译器指令,本文后面部分会重点介绍 Java注解可以应用在构建时,即当你构建你的项目时.构建过程包括生成源码,编译源码,生成xml文件,打包编译的源码和文件到JAR包等.软件的构建通常使用诸如Apache Ant和Mav

  • 详解Java中自定义注解的使用

    目录 什么是注解 注解的注意事项 注解的本质 自定义注解使用 使用方式 1 使用方式 2 什么是注解 在早期的工作的时候 ,自定义注解写的比较多,可大多都只是因为 这样看起来 不会存在一堆代码耦合在一起的情况,所以使用了自定义注解,这样看起来清晰些, Annontation是Java5开始引入的新特征,中文名称叫注解. 它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类.方法.成员变量等)进行关联.为程序的元素(类.方法.成员变量)加上更直观.更明了的说

  • 你应该知道的21个Java核心技术

    写这篇文章的目的是想总结一下自己这么多年来使用java的一些心得体会,主要是和一些java基础知识点相关的,所以也希望能分享给刚刚入门的Java程序员和打算入Java开发这个行当的准新手们,希望可以给大家一些经验,能让大家更好学习和使用Java. 这次介绍的主要内容是和J2SE相关的部分,另外,会在以后再介绍些J2EE相关的.和Java中各个框架相关的内容. 经过这么多年的Java开发,以及结合平时面试Java开发者的一些经验,我觉得对于J2SE方面主要就是要掌握以下的一些内容. 1. JVM相

  • 关于Java中你所不知道的Integer详解

    前言 本文主要给大家介绍了关于Java中Integer的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 实参形参 前些天看到朋友圈分享了一片文章<Java函数的传参机制--你真的了解吗?> 有些触发,之前也研究过Java的Integer,所以写下本文,希望对你有所帮助. 交换 首先来看一个示例. 请用Java完成swap函数,交换两个整数类型的值. public static void test() throws Exception { Integer a = 1,

  • PHP小白必须要知道的php基础知识(超实用)

    很多人看到PHP就以为是程序员,就以为钱很多(虽然是事实),但是也要考虑下自己是不是适合这一行,知道PHP是什么吗?PHP都有什么样的功能,都能用来干嘛? PHP是什么? •PHP(PHP: Hypertext Preprocessor,超文本预处理器的缩写),是一 种被广泛应用的开放源代码的.基于服务器端的用于产生动态网页 的.可嵌入HTML中的脚本程序语言,尤其适合 WEB 开发. •当客户端向服务器的程序提出请求时,web服务器根据请求晌应对应 的页面,当页面中含有php脚本时,服务器会交

  • Java中的字符编码问题处理心得总结

    当面对一串字节流的时候,如果不指定它的编码,其实际意义是无法知道的. 这句话应该也是我们面对"字符转字节,字节转字符"问题时候时刻记在脑子里的.否则乱码问题可能就接踵而至. 其实乱码问题的本质就是Encoding和Decoding用的不是一个编码,明白了这个道理就很好解决乱码问题了. Java中常见的时候有如下: 1. String类使用byte[]的构造函数 String(byte[] bytes),String类同时提供了两个重载 (1)String(byte[] bytes, C

  • 详解Java中Checked Exception与Runtime Exception 的区别

    详解Java中Checked Exception与Runtime Exception 的区别 Java里有个很重要的特色是Exception ,也就是说允许程序产生例外状况.而在学Java 的时候,我们也只知道Exception 的写法,却未必真能了解不同种类的Exception 的区别. 首先,您应该知道的是Java 提供了两种Exception 的模式,一种是执行的时候所产生的Exception (Runtime Exception),另外一种则是受控制的Exception (Checked

随机推荐