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

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

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

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

@Override

@FunctionalInterface

@SuppressWarnings

@SafeVarargs

@Deprecated

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

@Override

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

虽然开始时覆盖方法看起来很简单,但是如果执行不正确,则可能会引入许多微小的bug。例如,用覆盖类类型的单个参数覆盖Object#equals方法就是一种常见的错误:

publicclassFoo{publicbooleanequals(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中)时,就永远不会调用它:

publicclassFoo{publicbooleanequals(Foo foo){returntrue;    }}Object foo =newFoo();Object identicalFoo =newFoo();System.out.println(foo.equals(identicalFoo));// false

这是一个非常微妙但常见的错误,可以被编译器捕获。我们的意图是覆盖Object#equals方法,但因为我们指定了一个类型为Foo而不是Object类型的参数,所以我们实际上提供了重载的Object#equals方法,而不是覆盖它。为了捕获这种错误,我们引入@Override注解,它指示编译器检查覆盖实际有没有执行。如果没有执行有效的覆盖,则会抛出错误。因此,我们可以更新Foo类,如下所示:

publicclassFoo{@Overridepublicbooleanequals(Foo foo){returntrue;}}

如果我们尝试编译这个类,我们现在收到以下错误:

$ javac Foo.javaFoo.java:3:error:method doesnotoverrideorimplement a method from a supertype        @Override        ^1error

实质上,我们已经将我们已经覆盖方法的这一隐含的假设转变为由编译器进行的显性验证。如果我们的意图被错误地实现,那么Java编译器会发出一个错误——不允许我们不正确实现的代码被成功编译。通常,如果以下任一条件不满足,则Java编译器将针对使用@Override注解的方法发出错误(引用自Override注解文档):

该方法确实会覆盖或实现在超类中声明的方法。

该方法的签名与在Object中声明的任何公共方法(即equals或hashCode方法)的签名覆盖等价(override-equivalent)。

因此,我们也可以使用此注解来确保子类方法实际上也覆盖超类中的非最终具体方法或抽象方法:

publicabstractclassFoo{publicintdoSomething(){return1;    }publicabstractintdoSomethingElse();}publicclassBarextendsFoo{@OverridepublicintdoSomething(){return10;    }@OverridepublicintdoSomethingElse(){return20;    }}Foo bar =newBar();System.out.println(bar.doSomething());// 10System.out.println(bar.doSomethingElse());// 20

@Override注解不仅不限于超类中的具体或抽象方法,而且还可用于确保接口的方法也被覆盖(从JDK 6开始):

publicinterfaceFoo{publicintdoSomething();}publicclassBarimplementsFoo{@OverridepublicintdoSomething(){return10;    }}Foo bar =newBar();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文档,函数式接口的定义如下:

一个函数式接口只有一个抽象方法。由于默认方法有一个实现,所以它们不是抽象的。

例如,以下接口被视为函数式接口:

publicinterfaceFoo{publicintdoSomething();}publicinterfaceBar{publicintdoSomething();publicdefaultintdoSomethingElse(){return1;    }}

因此,下面的每一个都可以用lambda表达式代替,如下所示:

publicclassFunctionalConsumer{publicvoidconsumeFoo(Foo foo){        System.out.println(foo.doSomething());    }publicvoidconsumeBar(Bar bar){        System.out.println(bar.doSomething());    }}FunctionalConsumer consumer =newFunctionalConsumer();consumer.consumeFoo(() ->10);// 10consumer.consumeBar(() ->20);// 20

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

@FunctionalInterfacepublicinterfaceFoo{publicintdoSomething();}@FunctionalInterfacepublicinterfaceBar{publicintdoSomething();publicdefaultintdoSomethingElse(){return1;    }}

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

@FunctionalInterfacepublicinterfaceFoo{publicintdoSomething();publicintdoSomethingElse();}

如果我们试图编译这个接口,则会收到以下错误:

$ javac Foo.javaFoo.java:1: error: Unexpected @FunctionalInterface annotation@FunctionalInterface^  Fooisnota functional interface    multiple non-overriding abstract methods foundininterface Foo1error

使用这个注解,我们可以确保我们不会错误地创建原本打算用作函数式接口的非函数式接口。需要注意的是,即使在@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注解与抑制警告(以字符串数组的形式提供)的一个或多个名字添加到发生警告的上下文中:

publicclassFoo{publicvoiddoSomething(@SuppressWarnings("rawtypes")List myList){// Do something with myList}}

@SuppressWarnings注解可用于以下任何一种情况:

类型

方法

参数

构造函数

局部变量

模块

一般来说,@SuppressWarnings注解应该应用于最直接的警告范围。例如,如果方法中的局部变量应忽略警告,则应将@SuppressWarnings注解应用于局部变量,而不是包含局部变量的方法或类:

publicclassFoo{publicvoiddoSomething(){@SuppressWarnings("rawtypes")        List myList =newArrayList();// Do something with myList}}

@SafeVarargs

可变参数在Java中是一种很有用的技术手段,但在与泛型参数一起使用时,它们也可能会导致一些严重的问题。由于泛型在Java中是非特定的,所以具有泛型类型的变量的实际(实现)类型不能在运行时被断定。由于无法做出此判断,因此变量可能会存储非其实际类型的引用到类型,如以下代码片段所示(摘自《Java Generics FAQs》):

List ln =newArrayList();ln.add(1);List ls = ln;// unchecked warning Strings = ls.get(0);// ClassCastException

在将ln分配给ls后,堆中存在变量ls,该变量具有List的类型,但存储引用到实际为List类型的值。这个无效的引用被称为堆污染。由于直到运行时才能确定此错误,因此它会在编译时显示为警告,并在运行时出现ClassCastException。当泛型参数与可变参数组合时,可能会加剧此问题:

publicclassFoo{publicvoiddoSomething(T... args){// ...}}

在这种情况下,Java编译器会在调用站点内部创建一个数组来存储可变数量的参数,但是T的类型并未实现,因此在运行时会丢失。实质上,到doSomething的参数实际上是Object[]类型。如果依赖T的运行时类型,那么这会导致严重的问题,如下面的代码片段所示:

publicclassFoo{    public voiddoSomething(T... args) {Object[] objects = args;Stringstring = (String) objects[0];    }}Foo foo =newFoo();foo.doSomething(1,2);

如果执行此代码片段,那么将导致ClassCastException,因为在调用站点传递的第一个Number参数不能转换为String(类似于独立堆污染示例中抛出的ClassCastException)。通常,可能会出现以下情况:编译器没有足够的信息来正确确定通用可变参数的确切类型,这会导致堆污染,这种污染可以通过允许内部可变参数数组从方法中转义来传播,如下面摘自《Effective Java》第3版 pp.147的例子:

publicstaticT[]toArray(T... args){returnargs;}

在某些情况下,我们知道方法实际上是类型安全的,不会造成堆污染。如果可以在保证的情况下做出这个决定,那么我们可以使用@SafeVarargs注解来注解该方法,从而抑制与可能的堆污染相关的警告。但是,这引出了一个问题:什么时候通用可变参数方法会被认为是类型安全的?Josh Bloch在《Effective Java》第3版第147页的基础上提供了一个完善的解决方案——基于方法与内部创建的用于存储其可变参数的数组的交互:

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

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

@SafeVarargsstaticList flatten(List... lists) {List result =newArrayList<>();for(Listlist: lists) {        result.addAll(list);    }returnresult;}

有关@SafeVarargs注解的更多信息,请参阅@SafeVarargs文档,《JLS》9.6.4.7章节以及《Effective Java》第3版中的Item32。点击这里阅读 Java 10 新特性实战教程。

@Deprecated

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

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

@DeprecatedpublicclassFoo{}Foo foo =newFoo();

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

$ javac Main.javaNote:Main.java usesoroverrides a deprecated API.Note:Recompile with -Xlint:deprecationfordetails.

通常,每当使用@Deprecated注解的元素时,都会引发警告,除了用于以下五种情况:

声明本身就被声明为是弃用的(即递归调用)。

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

使用和声明都在同一个最外面的类中(即,如果类调用其本身的弃用方法)。

用在import声明中,该声明导入通常不赞成使用的类型或构件(即,在将已弃用的类导入另一个类时)。

exports或opens指令内。

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

@Deprecated(forRemoval =true)publicclassFoo{}

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

$ javac Main.javaMain.java:7: warning: [removal] Fooincom.foo has been deprecatedandmarkedforremoval                Foo foo =newFoo();                ^Main.java:7: warning: [removal] Fooincom.foo has been deprecatedandmarkedforremoval                Foo foo =newFoo();                              ^2warnings

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

@Deprecated(since ="1.0.5", forRemoval =true)publicclassFoo{}

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

/** * Some test class. *  *@deprecatedReplaced by {@linkcom.foo.NewerFoo}. *  *@authorJustin Albano */@Deprecated(since ="1.0.5", forRemoval =true)publicclassFoo{}

JavaDoc工具将生成以下文档:

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

结尾

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

@Override

@FunctionalInterface

@SuppressWarnings

@SafeVarargs

@Deprecated

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

欢迎工作一到五年的Java程序员朋友们加入Java架构开发:744677563

本群提供免费的学习指导 架构资料 以及免费的解答

不懂得问题都可以在本群提出来 之后还会有职业生涯规划以及面试指导

相关文章
|
17天前
|
XML Java 编译器
Java注解的底层源码剖析与技术认识
Java注解(Annotation)是Java 5引入的一种新特性,它提供了一种在代码中添加元数据(Metadata)的方式。注解本身并不是代码的一部分,它们不会直接影响代码的执行,但可以在编译、类加载和运行时被读取和处理。注解为开发者提供了一种以非侵入性的方式为代码提供额外信息的手段,这些信息可以用于生成文档、编译时检查、运行时处理等。
53 7
|
1月前
|
Java 程序员
JAVA程序员的进阶之路:掌握URL与URLConnection,轻松玩转网络资源!
在Java编程中,网络资源的获取与处理至关重要。本文介绍了如何使用URL与URLConnection高效、准确地获取网络资源。首先,通过`java.net.URL`类定位网络资源;其次,利用`URLConnection`类实现资源的读取与写入。文章还提供了最佳实践,包括异常处理、连接池、超时设置和请求头与响应头的合理配置,帮助Java程序员提升技能,应对复杂网络编程场景。
64 9
|
2月前
|
XML Java 编译器
Java学习十六—掌握注解:让编程更简单
Java 注解(Annotation)是一种特殊的语法结构,可以在代码中嵌入元数据。它们不直接影响代码的运行,但可以通过工具和框架提供额外的信息,帮助在编译、部署或运行时进行处理。
98 43
Java学习十六—掌握注解:让编程更简单
|
22天前
|
Java 编译器 数据库
Java 中的注解(Annotations):代码中的 “元数据” 魔法
Java注解是代码中的“元数据”标签,不直接参与业务逻辑,但在编译或运行时提供重要信息。本文介绍了注解的基础语法、内置注解的应用场景,以及如何自定义注解和结合AOP技术实现方法执行日志记录,展示了注解在提升代码质量、简化开发流程和增强程序功能方面的强大作用。
60 5
|
1月前
|
SQL 存储 Java
面向 Java 程序员的 SQLite 替代品
SQLite 是轻量级数据库,适用于小微型应用,但其对外部数据源支持较弱、无存储过程等问题影响了开发效率。esProc SPL 是一个纯 Java 开发的免费开源工具,支持标准 JDBC 接口,提供丰富的数据源访问、强大的流程控制和高效的数据处理能力,尤其适合 Java 和安卓开发。SPL 代码简洁易懂,支持热切换,可大幅提高开发效率。
|
1月前
|
Java 开发者 Spring
[Java]自定义注解
本文介绍了Java中的四个元注解(@Target、@Retention、@Documented、@Inherited)及其使用方法,并详细讲解了自定义注解的定义和使用细节。文章还提到了Spring框架中的@AliasFor注解,通过示例帮助读者更好地理解和应用这些注解。文中强调了注解的生命周期、继承性和文档化特性,适合初学者和进阶开发者参考。
57 14
|
1月前
|
前端开发 Java
[Java]讲解@CallerSensitive注解
本文介绍了 `@CallerSensitive` 注解及其作用,通过 `Reflection.getCallerClass()` 方法返回调用方的 Class 对象。文章还详细解释了如何通过配置 VM Options 使自定义类被启动类加载器加载,以识别该注解。涉及的 VM Options 包括 `-Xbootclasspath`、`-Xbootclasspath/a` 和 `-Xbootclasspath/p`。最后,推荐了几篇关于 ClassLoader 的详细文章,供读者进一步学习。
37 12
|
1月前
|
SQL Java 程序员
倍增 Java 程序员的开发效率
应用计算困境:Java 作为主流开发语言,在数据处理方面存在复杂度高的问题,而 SQL 虽然简洁但受限于数据库架构。SPL(Structured Process Language)是一种纯 Java 开发的数据处理语言,结合了 Java 的架构灵活性和 SQL 的简洁性。SPL 提供简洁的语法、完善的计算能力、高效的 IDE、大数据支持、与 Java 应用无缝集成以及开放性和热切换特性,能够大幅提升开发效率和性能。
|
2月前
|
IDE Java 程序员
C++ 程序员的 Java 指南
一个 C++ 程序员自己总结的 Java 学习中应该注意的点。
25 5
|
1月前
|
Java 编译器
Java进阶之标准注解
Java进阶之标准注解
34 0