当前位置: 首页 > 科技观察

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

时间:2023-03-12 17:11:55 科技观察

计划重点自JDK5推出以来,注解已经成为Java生态系统中不可或缺的一部分。虽然开发者为Java框架开发了无数的自定义注解(比如Spring的@Autowired),但有些被编译器识别的注解是非常重要的。在本文中,我们将看到Java编译器支持的5个注释并了解它们的预期用途。在此过程中,我们将探讨其创建背后的基本原理、围绕其使用的一些特性以及正确应用的一些示例。虽然其中一些注解比其他注解更常见,但每个非初级Java开发人员都应该消化每一个注解。@Override@FunctionalInterface@SuppressWarnings@SafeVarargs@Deprecated首先,我们将深入研究Java中最常用的注解之一:@Override。@Override方法的实现或为抽象方法提供实现的能力是任何面向对象(OO)语言的核心。由于Java是一门面向对象的语言,具有许多常见的面向对象抽象机制,非***超类中定义的非final方法或接口中的任何方法(接口方法不能是final)都可以被子类覆盖。单击此处阅读有关Java10新功能的实践教程。虽然重写方法乍一看似乎很简单,但如果操作不当,可能会引入许多小错误。例如,使用覆盖类类型的单个参数覆盖Object#equals方法是一个常见的错误:Foo类重写了Object#equals方法,因此可以测试Foo是否与Java中的任何其他对象相等。虽然我们的意图是正确的,但我们的实施却不正确。事实上,我们的实现根本没有覆盖Object#equals方法。相反,我们提供了方法的重载:我们没有替换Object类提供的equals方法的实现,而是提供了第二种方法,它专门接受Foo对象,而不是Object对象。我们的错误可以通过一个简单的实现来举例说明,该实现对所有相等性检查都返回true,但是当提供的对象被视为对象时(Java将执行的操作,例如在Java集合框架中,即JCF),那么它永远不会被调用:publicclassFoo{publicbooleanequals(Foofoo){returntrue;}}Objectfoo=newFoo();ObjectidenticalFoo=newFoo();System.out.println(foo.equals(identicalFoo));//false这是一个非常微妙但常见的错误被编译器捕获。我们的意图是覆盖Object#equals方法,但是因为我们指定了Foo类型的参数而不是Object,所以我们实际上提供了重载的Object#equals方法而不是覆盖它。为了捕获此类错误,我们引入了@Override注解,它指示编译器检查是否实际执行了覆盖。如果未执行有效覆盖,则会抛出错误。因此我们可以按如下方式更新Foo类:Override^1error本质上,我们已经将我们已经覆盖该方法的这种隐含假设转变为编译器的显式验证。如果我们的意图实现不正确,Java编译器将发出错误-不允许我们错误实现的代码被成功编译。通常,如果以下任何条件不成立,Java编译器将对使用@Override注释的方法发出错误(引用自Override注释文档):该方法确实覆盖或实现了在超类中声明的方法。此方法的签名与Object中声明的任何公共方法(即equals或hashCode方法)的签名等效。因此,我们也可以使用这个注释来确保子类方法实际上也覆盖超类中的非最终具体或抽象方法:;}@OverridepublicintdoSomethingElse(){return20;}}Foobar=newBar();System.out.println(bar.doSomething());//10System.out.println(bar.doSomethingElse());//20@重写注解不限于超类中的具体或抽象方法,还可以用于确保接口的方法也被重写(自JDK6起):}}Foobar=newBar();System.out.println(bar.doSomething());//10一般重写非final类方法、抽象超类方法、接口方法的方法都可以用@Override注解.有关有效覆盖的更多信息,请参阅《Overriding and Hiding》文档和《Java Language Specification (JLS)》的第9.6.4.4节。@FunctionalInterface随着JDK8中lambda表达式的引入,函数式接口在Java中变得越来越流行。这些特殊类型的接口可以被lambda表达式、方法引用或构造函数引用所取代。根据@FunctionalInterface文档,函数式接口定义如下:一个函数式接口只有一个抽象方法。由于默认方法有一个实现,它们不是抽象的。例如,以下接口被认为是功能接口:publicinterfaceFoo{publicintdoSomething();}publicinterfaceBar{publicintdoSomething();publicdefaultintdoSomethingElse(){return1;}}因此,以下每一项都可以用lambda表达式替换,如下所示:(bar.doSomething());}}FunctionalConsumerconsumer=newFunctionalConsumer();consumer.consumeFoo(()->10);//10consumer.consumeBar(()->20);//20需要注意的重要一点是抽象类,即使它们只包含一个抽象方法,也不是功能接口。有关详细信息,请参阅《Allow lambdas to implement abstract classes》由***Java语言架构师BrianGoetz撰写。与@Override注解类似,Java编译器提供了@FunctionalInterface注解来保证接口确实是函数式接口。例如,我们可以在上面创建的接口中添加这个注解:@FunctionalInterfacepublicinterfaceFoo{publicintdoSomething();}@FunctionalInterfacepublicinterfaceBar{publicintdoSomething();publicdefaultintdoSomethingElse(){return1;}}如果我们错误地将接口定义为非功能接口并且使用@FunctionalInterface注解了错误的接口,Java编译器会发出错误。例如,我们可以定义以下带注释的非功能接口:@FunctionalInterfacepublicinterfaceFoo{publicintdoSomething();publicintdoSomethingElse();}如果我们尝试编译这个接口,我们将收到以下错误:$javacFoo.javaFoo.java:1:error:Unexpected@FunctionalInterfaceannotation@FunctionalInterface^Fooisnotfunctionalinterfacemultiplenon-overridingabstractmethodsfoundininterfaceFoo1error使用这个注释,我们可以确保我们不会错误地创建旨在成为功能接口的非功能接口。重要的是要注意,即使没有@FunctionalInterface注释,该接口也可以用作功能接口(可以用lambda、方法引用和构造函数引用代替),正如我们在前面的示例中看到的那样。这类似于@Override注解,即一个方法即使不包含@Override注解也可以被覆盖。在这两种情况下,注释都是可选的技术,允许编译器强制执行所需的意图。有关@FunctionalInterface注释的更多信息,请参阅@FunctionalInterface文档和《JLS》的第4.6.4.9节。单击此处阅读有关Java10新功能的实践教程。@SuppressWarnings警告是所有编译器的重要组成部分,它向开发人员提供有关未来编译器版本中可能出现的潜在危险行为或错误的反馈。例如,在Java中使用没有其关联的正式泛型参数(称为原始类型)的泛型会导致警告,使用已弃用的代码也会产生警告(请参阅下面的@Deprecated部分)。尽管这些警告很重要,但它们可能并不总是适用,甚至可能并不正确。比如可能会出现不安全的类型转换会出现警告的情况,但是根据使用的上下文,我们可以保证它是安全的。为了在某??些上下文中忽略特定的警告,JDK5中引入了@SuppressWarnings注释。该注释接受一个或多个字符串参数——描述要忽略的警告的名称。虽然这些警告的名称通常因编译器实现而异,但有3个警告在Java语言中是标准化的(因此在所有Java编译器实现中都是通用的):类型转换是安全的),这可能是由于访问原始类型的成员(见《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部分)。移除:指示使用最终弃用的方法、类、类型等的警告(请参阅《JLS》9.6.4.6和下面的@Deprecated部分)。要忽略特定警告,请将@SuppressedWarning注释和一个或多个抑制警告的名称(以字符串数组的形式提供)添加到发生警告的上下文中:publicclassFoo{publicvoiddoSomething(@SuppressWarnings("rawtypes")ListmyList){//DosomethingwithmyList}}@SuppressWarnings注解可以用在以下任何一种情况下:typefieldmethodparameterconstructorlocalvariablemodule通常,@SuppressWarnings注解应该应用于最直接的警告范围。例如,如果方法中的局部变量应该忽略警告,@SuppressWarnings注释应该应用于局部变量,而不是包含局部变量的方法或类:publicclassFoo{publicvoiddoSomething(){@SuppressWarnings("rawtypes")ListmyList=newArrayList();//DosomethingwithmyList}}@SafeVarargsVarargs在Java中是一种有用的技术,但当与泛型参数一起使用时,它们也会导致一些严重的问题。由于泛型在Java中是不明确的,因此无法在运行时确定具有泛型类型的变量的实际(已实现)类型。由于无法做出此确定,因此变量可能会存储对不是其实际类型的类型的引用,如以下代码片段所示(取自《Java Generics FAQs》):Listln=newArrayList();ln.add(1);Listls=ln;//uncheckedwarningStrings=ls.get(0);//ClassCastException将ln赋值给ls后,堆中有一个变量ls,其类型为List,但存储的是referenceto它实际上是一个List类型的值。这种无效引用称为堆污染。由于此错误直到运行时才能确定,因此它在编译时显示为警告,在运行时显示为ClassCastException。当泛型参数与可变参数组合时,这个问题可能会加剧:在调用站点内部存储可变数量的参数,但T的类型未实现,因此在运行时丢失。本质上,doSomething的参数实际上是Object[]类型。如果您依赖T的运行时类型,这可能会导致严重的问题,如以下代码片段所示:objects[0];}}Foofoo=newFoo();foo.doSomething(1,2);如果执行此代码片段,将导致ClassCastException,因为在调用站点传递的第一个Number参数无法转换为String(类似于独立堆污染示例中抛出的ClassCastException)。通常,编译器没有足够的信息来正确确定泛型可变参数的确切类型,这可能导致堆污染,堆污染可以通过允许内部可变参数数组从方法中转义来传播,如下所示来自《Effective Java》3rdEditionpp.147:publicstaticT[]toArray(T...args){returnargs;}在某些情况下,我们知道方法实际上是类型安全的,不会造成堆污染。如果可以保证做出此决定,那么我们可以使用@SafeVarargs注释对该方法进行注释,从而抑制与可能的堆污染相关的警告。然而,这引出了一个问题:泛型可变参数方法何时被认为是类型安全的?JoshBloch基于《Effective Java》第3版p.提供了一个很好的解决方案。147-基于方法与用于存储其可变参数的内部创建数组的交互:如果方法未将任何内容存储到数组中(这将覆盖参数)并且不允许转义对数组的引用(这将允许不受信任的代码访问数组),那么它是安全的。换句话说,如果可变参数数组仅用于从调用者向方法传递可变数量的参数——毕竟,可变参数就是为了这个——那么该方法是安全的。因此,如果我们创建以下方法(来自pp.149同上),那么我们可以使用@SafeVarags注释正确地注释我们的方法:@SafeVarargsstaticListflatten(List…lists){Listresult=newArrayList<>();for(Listlist:lists){result.addAll(list);}returnresult;}@SafeVarargs注解的更多信息,参见@SafeVarargsDocumentation,Section《JLS》9.6.4.7和版本3中的Item32。单击此处阅读有关Java10新功能的实践教程。@Deprecated随着代码的开发,有时代码会过时,不应再使用。在这些情况下,通常会有更适合手头任务的回退,虽然对过时代码的现有调用可能会保留,但所有新调用都应使用替换方法。这种过时的代码被称为弃用代码。在某些紧急情况下,不推荐使用的代码可能会被删除,并且应该在未来的框架或库版本从其代码库中删除不推荐使用的代码之前立即转换为替换代码。为了支持已弃用代码的文档,Java包含@Deprecated注释,它将一些构造函数、字段、局部变量、方法、包、模块、参数或类型标记为已弃用。如果使用过时的元素(构造函数、字段、局部变量等),编译器会发出警告。例如,我们可以创建一个弃用类并按如下方式使用它:@DeprecatedpublicclassFoo{}Foofoo=newFoo();如果我们编译此代码(在名为Main.java的文件中),我们会收到以下警告:$javacMain.javaNote:Main.javausesoroverridesadeprecatedAPI.Note:Recompilewith-Xlint:deprecationfordetails。一般而言,只要使用@Deprecated注释的元素,就会发出警告,以下五种情况除外:声明本身被声明为yesDeprecated(即递归调用)。该声明被注释以禁止弃用警告(即@SuppressWarnings(“deprecation”)注释,如上所述,应用于使用已弃用元素的上下文。使用和声明都在同一个最外层类中(即如果该类自己调用其弃用的方法。在导入通常弃用的类型或工件的导入语句中使用(即,将弃用的类导入另一个类时)。在导出或打开指令中。如前所述,在某些情况下,当弃用元素将被删除时,调用代码应立即删除弃用元素(称为最终弃用代码)。在这种情况下,您可以使用提供@Deprecated注解的forRemoval参数,如下所示:@Deprecated(forRemoval=true)publicclassFoo{}使用这个最终弃用的代码将导致一系列更严格的警告:$javacMain.javaMain.java:7:warning:[removal]Fooincom.foohasbeendeprecatedandmarkedforremovalFoofoo=newFoo();^Main.java:7:warning:[removal]Fooincom.foohasbeendeprecatedandmarkedforremovalFoofoo=newFoo();^2warnings除了标准@Deprcated注释描述的相同异常之外,始终警告最终弃用。我们还可以通过向注释提供since变量来向@Deprecated注释添加文档:@Deprecated(since="1.0.5",forRemoval=true)publicclassFoo{}可以使用@deprecatedJavaDoc元素进一步记录(注意小写d)不推荐使用的元素,如以下代码片段所示:=true)publicclassFoo{}JavaDoc工具将生成以下文档:有关@Deprecated注释的更多信息,请参阅@Deprecated文档和《JLS》9.6.4.6。结束语自从在JDK5中引入注解以来,注解一直是Java不可或缺的一部分。虽然有些注解比其他注解更受欢迎,但本文中介绍的这5个注解应该被新手及以上级别的开发人员理解和掌握:@Override@FunctionalInterface@SuppressWarnings@SafeVarargs@Deprecated尽管每个方法都有其独特的目的,但是所有这些注释使Java应用程序更具可读性,并允许编译器对我们的代码强制执行其他一些隐式假设。随着Java语言的不断发展,这些经过验证的注释可能会使用很多年,有助于确保更多应用程序的行为符合开发人员的预期。