引言
在java编程中,注解(Annotation)是一种元数据,它提供了关于程序代码的额外信息。注解不直接影响程序的执行,但可以在运行时提供有关程序的信息,或者让编译器执行额外的检查。
下面笔者通过循序渐进的方式一步步介绍注解的相关内容,帮助大家消化吸收知识点。
一、何谓java注解
Java注解又称Java标注,是在 JDK5 时引入的新特性,注解(也被称为元数据)。
Java注解它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。
Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。
二、java注解分类
1. 内置注解(Built-in Annotations):
这些注解是Java标准库(java.lang包或相关包)中预先定义的,用于特定的编程目的。例如:
- @Override:表示方法重写父类的方法。
- @Deprecated:标记过时的方法或类,编译器会发出警告。
- @SuppressWarnings:抑制特定的编译器警告。
- @FunctionalInterface:标识一个函数式接口,即只有一个抽象方法的接口。
- @SafeVarargs:表示方法的安全可变参数列表,避免泛型警告。
@Override示例
/**
* override注解
* @author hulei
* @date 2024/5/10 13:52
*/
public class OverrideAnnotations {
static class BaseUser implements UserInterface {
@Override
public void method() {
new BaseUser().method();
}
}
interface UserInterface {
void method();
}
}
这个注解没什么好说的,一般用在子类覆写父类的方法上,比较简单基础的注解。上图代码表示的是:BaseUser 类实现了 UserInterface 接口,并重写了method()方法,方法上标识了 @Override注解。不一定非要是实现一个接口,继承一个普通类或者抽象类,重写父类的方法也可。
在Java中,如果子类重写父类的方法但不使用@Override注解,会有以下几点需要注意:
-
编译器提示:
如果你没有使用@Override,但实际上是重写了父类方法,某些IDE(如Eclipse, IntelliJ IDEA)会在方法上显示警告,提示你可能遗漏了@Override注解。虽然这不是强制性的,但添加它有助于提高代码的可读性和清晰度。 -
编译错误:
如果方法签名(包括方法名、参数列表和返回类型)与父类方法不完全匹配,编译器不会报错,因为你实际上并没有重写方法。这可能导致意外的行为,因为你可能以为你在调用子类的方法,但实际上调用了父类的方法。 -
方法覆盖的确认:
使用@Override可以确保编译器在编译时检查你是否真正重写了父类的方法。如果签名不匹配,编译器会报错,防止因意外的非重写而导致的问题。 -
代码可读性:
添加@Override注解使代码更易读,因为它清楚地表明该方法是用于重写父类方法的。 -
未来修改的保护:
如果父类的签名在未来发生变化,而你没有更新子类的方法签名,没有@Override的子类方法将不再重写父类方法。而如果有@Override,编译器会报错,提醒你需要更新子类的方法。
因此,尽管不是必须的,但推荐在重写父类方法时使用@Override注解,以确保代码的正确性和一致性。
@Deprecated示例
package com.datastructures;
/**
* Deprecated注解
* @author hulei
* @date 2024/5/10 14:06
*/
public class DeprecatedAnnotation {
public static void main(String[] args) {
DeprecatedAnnotation deprecatedAnnotation = new DeprecatedAnnotation();
deprecatedAnnotation.method();
}
@Deprecated
public void method() {
System.out.println("DeprecatedAnnotation.method");
}
}
调用一个过时的方法,大部分编译器比如IntelliJ IDEA会给出警告信息,不推荐使用。像我们在开发过程中使用很多的第三方库或者框架包括jdk自身的大量类库时,可能早期提供的方法或函数有缺陷,但是又被大量的开发者使用,所以不能删除,这些第三方库的作者就在过时的方法加上这个注解,api调用者在调用这个过时方法就会收到提示,从而查看源码,根据作者的注释指引调用新的更加安全的方法。
@SuppressWarnings示例
package com.datastructures;
import java.util.ArrayList;
import java.util.List;
/**
* @author hulei
* @date 2024/5/10 14:31
*/
public class SuppressWarningsAnnotation {
@SuppressWarnings("all")
public static void addItems(String item){
List items = new ArrayList();
items.add(item);
}
public static void main(String[] args) {
addItems("item");
}
}
如果不加@SuppressWarnings注解,则会出现如下提示
看着很不舒服,都是一些无关紧要的提示,比如类型检查操作的警告,装箱、拆箱操作时候的警告等等。
加了 @SuppressWarnings(“all”) 这个注解,告警信息就没有了,抑制类所有类型的告警信息,清清爽爽,这对强迫症患者极为友好。
就算是加了过时注解的方法,加了@SuppressWarnings(“all”),也会把过时告警信息隐蔽掉。
我们一般常用的是如下三种
- @SuppressWarnings(“unchecked”) :抑制单类型的警告
- @SuppressWarnings(value={“unchecked”, “rawtypes”}) :抑制多类型的警告
- @SuppressWarnings(“all”) :抑制所有类型的警告
抑制警告的关键字对照表
关键字 | 用途 | 描述 |
---|---|---|
all | to suppress all warnings | 抑制所有警告 |
boxing | to suppress warnings relative to boxing/unboxing operations | 抑制装箱、拆箱操作时候的警告 |
cast | to suppress warnings relative to cast operations | 抑制映射相关的警告 |
dep-ann | to suppress warnings relative to deprecated annotation | 抑制启用注释的警告 |
deprecation | to suppress warnings relative to deprecation | 抑制过期方法警告 |
fallthrough | to suppress warnings relative to missing breaks in switch statements | 抑制确在switch中缺失breaks的警告 |
finally | to suppress warnings relative to finally block that don’t return | 抑制finally模块没有返回的警告 |
hiding | to suppress warnings relative to locals that hide variable | 抑制相对于隐藏变量的局部的警告 |
incomplete-switch | to suppress warnings relative to missing entries in a switch statement (enum case) | 忽略没有完整的switch语句 |
nls | to suppress warnings relative to non-nls string literals | 忽略非nls格式的字符 |
null | to suppress warnings relative to null analysis | 忽略对null的操作 |
rawtypes | to suppress warnings relative to un-specific types when using generics on class params | 使用generics时忽略没有指定相应的类型 |
restriction | to suppress warnings relative to usage of discouraged or forbidden references | 抑制禁止引用的使用相关的警告 |
serial | to suppress warnings relative to missing serialVersionUID field for a serializable class | 忽略在serializable类中没有声明serialVersionUID变量 |
static-access | to suppress warnings relative to incorrect static access | 抑制不正确的静态访问方式警告 |
synthetic-access | to suppress warnings relative to unoptimized access from inner classes | 抑制子类没有按最优方法访问内部类的警告 |
unchecked | to suppress warnings relative to unchecked operations | 抑制没有进行类型检查操作的警告 |
unqualified-field-access | to suppress warnings relative to field access unqualified | 抑制没有权限访问的域的警告 |
unused | to suppress warnings relative to unused code | 抑制没被使用过的代码的警告 |
@FunctionalInterface示例
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
这里就拿JDK官方的Function函数式接口为例,注释被我删除了
打上@FunctionalInterface注解的接口,就可以使用java8提供的lamda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的)。
如下代码调用实例
表示给定一个入参,经过一定的逻辑处理,返回一个出参结果
还有BiFunction,给定两个入参,返回一个出参结果,
也可以自定义,多个入参,比如笔者自定义的 ThreeBiFunction就是三个入参,一个出参
@SafeVarargs示例
package com.datastructures;
import java.util.List;
import java.util.Optional;
/**
* 注解:SafeVarargs示例
*/
public class SafeVarargsAnnotations {
@SafeVarargs
static void function(List<String>... stringLists) {
}
abstract static class BaseUser implements UserInterface {
@SafeVarargs
final <T> void gamma(T... ts) {
}
@Override
@SafeVarargs
public final void method(Optional<Object>... optionals) {
UserInterface.super.method(optionals);
}
}
interface UserInterface {
default void method(Optional<Object>... optionals) {
}
@SafeVarargs
static <T> void gamma(Class<T>... classes) {
}
void method();
}
}
方法的参数包含可变参数列表时,不加这个@SafeVarargs注解就会有告警信息,比如上面的代码,method方法有可变参数列表,没有加注解,产生类型安全和泛型相关提示
2.元注解(Meta-Annotations):
元注解是用于注解其他注解的注解,是所有其他注解的基础,它们定义了注解的行为和生命周期。主要包括:
- @Retention:定义注解的保留策略,可以是SOURCE(只存在于源码中)、CLASS(编译时丢弃,存在于字节码中但不运行时可用)或RUNTIME(运行时可通过反射访问)。
- @Target:指定注解可以应用于哪些程序元素,如类、方法、字段等。
- @Documented:指示是否将注解包含在生成的Javadoc中。
- @Inherited:允许子类继承父类的注解(仅适用于类,不适用于方法或字段)。
@Retention示例
上面的@SuppressWarnings注解源码,就只有一个 @Retention注解
打上@Retention注解的其他注解,有三个保留策略,上面已经说明。
@Target示例
如果一个注解上有@Target注解,则@Target注解声明了这个注解可以使用的地方
比如这个自定义注解,就只能在方法上使用,ElementType.METHOD枚举就是方法声明限制,关于ElementType枚举,可以自行查看里面的枚举信息
当然,后面可以写多个使用场景的枚举声明
还有的注解,没有加@Target注解,比如上面的@SuppressWarnings注解。一个注解上没有加使用范围的注解@Targe,那这个注解可以使用在任何能够使用注解的地方。所以 @SuppressWarnings 不包含自己的 @Target 注解,意味着它理论上可以应用于 Java 规范中任何允许注解的地方。然而,它实际上的使用受到限制,尤其是不能在表达式上下文中使用,这是因为其设计目的和 Java 语言规范的限制。
-
设计目的:@SuppressWarnings 的设计初衷是为了告诉编译器在特定的范围(如类、方法、字段等)内忽略特定类型的警告。它是为了简化开发过程,允许开发者在明知某些代码可能引起编译器警告,但确认这些警告不影响程序正确性的情况下,有选择地忽略这些警告。因此,它主要应用于编译单位的较大结构上。
-
表达式上下文限制:表达式上下文通常涉及更细粒度的操作,如赋值、方法调用、算术运算等。在这些上下文中使用 @SuppressWarnings 不符合其设计逻辑,因为这些地方通常不涉及整体性的类型或结构警告,而是更具体的、即时的操作。如果允许在表达式中使用,不仅会增加语言的复杂性,还可能引发滥用,使得代码难以理解和维护。
-
类型注解与普通注解的区别:类型注解(自 Java 8 引入)专门设计用于标注类型声明,包括泛型类型参数、返回类型、参数类型等,而 @SuppressWarnings 并不属于这一类别。类型注解可以在某种程度上改变编译器对类型的理解,而 @SuppressWarnings 仅用于指示编译器如何处理警告信息,不改变代码的类型系统或结构。
-
Java 语言规范限制:即使 @SuppressWarnings 没有限定其 @Target,Java 语言规范和编译器实现也决定了哪些注解可以用在哪些上下文中。表达式上下文通常不接受注解,特别是像 @SuppressWarnings 这样旨在影响编译器警告处理的注解,因为这不符合语言的语义和设计哲学。
举例如下:
综上所述,@SuppressWarnings 不能在表达式上下文中使用,主要是由于其设计意图、语言规范的限制以及为了保持语言的清晰度和简洁性。
@Documented示例
这个注解不重要,表示是否将注解包含在生成的Javadoc中。加不加完全在于我们自己,只要知道的用途就行了
@Inherited示例
这个注解还是比较重要的,允许子类继承父类的注解(仅适用于类,不适用于方法或字段)。什么意思呢?
一个注解上有@Inherited注解,那么当我们把这个注解打在一个类上时,如果这个类有子类,那么这个子类继承父类的这个注解
package com.datastructures;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* @author hulei
* @date 2024/5/10 17:01
*/
public class InheritedAnnotation {
/**
* 自定义注解
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface InnerAnnotation{
String value();
}
/**
* 父类,使用了自定义注解
*/
@InnerAnnotation(value = "父类注解")
public static class ParentClass {
}
/**
* 子类继承父类
*/
public static class ChildClass extends ParentClass {
}
public static void main(String[] args) {
Class<?> childClass = ChildClass.class;
if (childClass.isAnnotationPresent(InnerAnnotation.class)) {
InnerAnnotation annotation = childClass.getAnnotation(InnerAnnotation.class);
System.out.println("Value from InnerAnnotation: " + annotation.value());
} else {
System.out.println("No InnerAnnotation found.");
}
}
}
运行可以看到,子类也获取到了这个注解
那我们改造下,把注解上的@Inherited注解去掉,再执行看看
可以看到子类没有获取到父类的注解了,即没有从父类继承
3.自定义注解(Custom Annotations):
开发者可以使用**@interface**关键字创建自己的注解,根据需求定义注解的行为和用途。自定义注解可以结合元注解来定义其行为,例如通过@Retention和@Target来控制自定义注解的生命周期和应用范围。下面给出几个示例:
1. 用于记录日志的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface LogExecution {
String message() default "";
}
这个注解可以应用于方法,表示在执行该方法前/后需要记录日志。
切面类aop
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Around("@annotation(LogExecution)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getSignature().getDeclaringTypeName();
String message = joinPoint.getSignature().getAnnotation(LogExecution.class).message();
long start = System.currentTimeMillis();
logger.info("Starting method: {}.{} with message: {}", className, methodName, message);
Object result = joinPoint.proceed(); // 继续执行目标方法
long elapsedTime = System.currentTimeMillis() - start;
logger.info("Completed method: {}.{} in {}ms", className, methodName, elapsedTime);
return result;
}
}
实际代码调用
@Service
public class SomeService {
@LogExecution(message = "Executing business logic")
public String performTask() {
// 示例业务逻辑
try {
Thread.sleep(100); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
return "Task completed";
}
}
后面的注解,笔者就不再提供aop相关代码了,和日志注解类似,比较简单
2.用于数据验证的注解
比如邮箱格式验证
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Constraint(validatedBy = EmailValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EmailVaild{
String message() default "邮箱格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
3.用于事务管理的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Transactional {
boolean readOnly() default false;
}
这个注解用于标记一个方法需要在数据库事务中执行,readOnly 参数表示是否为只读事务。
4.用于权限控制的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiresRole {
String[] roles() default {};
}
这个注解用于标记一个方法或类需要特定的角色才能访问,roles 参数是角色的数组。
5.用于缓存结果的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CacheResult {
long cacheTime() default 60; // 缓存60秒
}
这个注解用于标记一个方法的结果应该被缓存一定时间,cacheTime 参数表示缓存的秒数。
在实际使用中,这些注解通常会与AOP(面向切面编程)框架结合,如Spring AOP,以便在运行时动态地处理注解的逻辑。
三、注解中的属性省略问题
这一节是笔者在学习时遇到的疑问,这里作为记录
这个注解有两个属性,value和logical ,@HasPermissions(“system:user:query”)
这种写法会把属性值默认给value,注意必须要有名为value的属性,并且其他属性都有默认值才可以
否则得显示给属性赋值
@HasPermissions(value = {"om:deviceCascade:edit","om:deviceCascade:add"},logical = Logical.OR)
总结如下
-
如果注解只有一个属性,那么肯定是赋值给该属性。
-
如果注解有多个属性,而且前提是这多个属性都有默认值,那么你不写注解名赋值,会赋值给名字为“value”这属性或者赋值给多个属性中唯一没有默认值的属性。
-
如果注解有多个属性,其中有没有设置默认值的属性,那么当你不写属性名进行赋值的时候,是会报错的。