什么是注解?
一句话理解:注解就是给代码”贴标签”——告诉编译器或框架一些额外信息,但不影响代码本身的逻辑
比喻:代码是一件衣服,注解是衣服上的洗涤标签——衣服照样穿,但洗衣机(框架)会根据标签决定怎么处理
注解本身什么都不做,只提供元数据(metadata)
真正干活的是读取注解的代码(编译器或框架),它们根据注解信息决定行为
注解和注释的区别:
注释(//)是给人看的,编译后就没了
注解(@)是给程序看的,编译器和框架可以读取并处理
一、内置注解:Java自带的标签
@Override:我在重写父类方法
贴在方法上,告诉编译器”我是故意重写的,帮我检查一下有没有写错”
如果方法签名和父类不匹配,编译器直接报错
1 |
|
不加 @Override 也能重写,但强烈建议加——防止手滑写错方法名导致”重写变新方法”的bug
@Deprecated:我过时了,别用了
贴在类、方法或字段上,编译器会给调用者一个警告(划删除线)
1 |
|
@SuppressWarnings:闭嘴,我知道我在干什么
告诉编译器”这个警告我知道了,别烦我”
常见参数:"unchecked"(泛型相关)、"deprecation"(使用过时API)、"all"(所有警告)
@SuppressWarnings("unchecked") 最常用
@FunctionalInterface:我是函数式接口
标记接口只能有一个抽象方法,否则编译报错
配合Lambda表达式使用
1 |
|
内置注解速查表
| 注解 | 贴在哪 | 作用 |
|---|---|---|
@Override |
方法 | 编译器检查是否正确重写 |
@Deprecated |
类/方法/字段 | 标记过时,编译器给警告 |
@SuppressWarnings |
类/方法/变量 | 抑制编译器警告 |
@FunctionalInterface |
接口 | 确保只有一个抽象方法 |
二、元注解:给注解贴的注解
元注解是”注解的注解”——用来定义自定义注解的行为规则
@Target:这个注解能贴在哪?
| 值 | 能贴在哪 |
|---|---|
ElementType.TYPE |
类、接口、枚举 |
ElementType.METHOD |
方法 |
ElementType.FIELD |
字段 |
ElementType.PARAMETER |
方法参数 |
ElementType.CONSTRUCTOR |
构造器 |
ElementType.LOCAL_VARIABLE |
局部变量 |
ElementType.ANNOTATION_TYPE |
注解(元注解自己就用这个) |
ElementType.PACKAGE |
包 |
@Retention:这个注解活到什么时候?(最重要的元注解)
| 值 | 生命周期 | 说明 |
|---|---|---|
RetentionPolicy.SOURCE |
只在源码里 | 编译后就没了。如 @Override |
RetentionPolicy.CLASS |
存到class文件 | 但运行时读不到(默认值) |
RetentionPolicy.RUNTIME |
运行时还在 | 可以用 反射机制 读取。框架用的都是这个 |
口诀:想要运行时用反射读注解,必须设成 RUNTIME
@Documented
生成JavaDoc时把注解信息也包含进去
不常用,知道有这么个东西就行
@Inherited
父类上的这个注解会被子类继承
注意:只对类有效,接口和方法上的注解不会被继承
三、自定义注解
用 @interface 关键字定义,语法长得像接口但完全是两码事
基本语法
1 | // 定义一个自定义注解 |
注解属性的规则
属性类型只能是:基本类型、String、Class、枚举、注解、以及它们的数组
属性用”方法”的语法声明,但实际上是属性
default 指定默认值,没有默认值的属性使用时必须填
如果只有一个属性且名叫 value,使用时可以省略 value=
1 |
|
四、注解处理:反射读取注解
注解本身不干活,真正的魔法在于读取注解并执行逻辑
这就需要用到 反射机制 中的 getAnnotation 系列方法
核心API
| 方法 | 说明 |
|---|---|
getAnnotation(注解类.class) |
获取指定注解,没有返回null |
getAnnotations() |
获取所有注解(包括继承的) |
getDeclaredAnnotations() |
获取所有直接声明的注解 |
isAnnotationPresent(注解类.class) |
判断是否有某个注解 |
实战:自己写一个简易测试框架
1 |
|
注意:只有 @Retention(RetentionPolicy.RUNTIME) 的注解才能被反射读取
@Override 是 SOURCE 级别的,编译后就消失了,反射读不到
框架用的注解(@Autowired、@RequestMapping等)都是 RUNTIME 级别
五、实际框架中的注解原理(简述)
Spring的 @Autowired 怎么工作的?
Spring启动时扫描所有类(通过 反射机制 )
找到所有带
@Component、@Service、@Controller注解的类通过反射创建这些类的对象(调用构造器)
扫描对象的所有字段,找到带
@Autowired的通过反射
field.setAccessible(true)+field.set()注入依赖
所以 @Autowired 的private字段能被赋值,就是因为Spring用了暴力反射
Spring MVC的 @RequestMapping 怎么工作的?
扫描所有
@Controller类找到每个方法上的
@RequestMapping注解读取注解的value属性(URL路径)和method属性(GET/POST)
建立 URL → Method 的映射表
请求来了 → 查映射表 → 反射调用对应方法
MyBatis的 @Select 怎么工作的?
扫描Mapper接口的所有方法
读取
@Select("SQL语句")注解方法被调用时 → 执行注解里的SQL → 结果通过反射映射到返回类型
小结:框架 = 注解(标记) + 反射(执行)
注解负责”声明意图”(我想注入、我想处理这个URL、我想执行这个SQL)
反射负责”实现意图”(创建对象、调用方法、赋值字段)
六、常见坑
坑1:注解@Retention没设成RUNTIME
自定义注解默认是 CLASS 级别,运行时反射读不到
99%的情况你想用 RUNTIME,记得加 @Retention(RetentionPolicy.RUNTIME)
坑2:注解属性类型限制
不能用包装类型(Integer),只能用基本类型(int)
不能用任意对象,只能用 String、Class、枚举、注解和它们的数组
原因:注解的值在编译时就确定了,必须是常量
坑3:@Inherited只对类有效
接口上的注解不会被实现类继承
方法上的注解不会被重写方法继承
只有类上的注解会被子类继承
坑4:注解属性是数组时的写法
只有一个元素可以省略花括号:@Target(ElementType.METHOD)
多个元素必须用花括号:@Target({ElementType.METHOD, ElementType.TYPE})
坑5:忘了注解和注释的区别
// 这是注释 → 给人看的
@Override → 给程序看的
面试时问到”注解是什么”,千万别说成注释
七、对比表格
四种内置注解对比
| 注解 | 级别 | 目的 | 不用会怎样 |
|---|---|---|---|
@Override |
SOURCE | 编译检查重写 | 方法名写错不会报错 |
@Deprecated |
RUNTIME | 标记过时API | 调用者不知道该用新方法 |
@SuppressWarnings |
SOURCE | 抑制警告 | IDE一堆黄色警告看着烦 |
@FunctionalInterface |
RUNTIME | 确保函数式接口 | 多写了抽象方法不会报错 |
@Retention三种级别对比
| 级别 | 存在时间 | 能用反射读吗 | 典型代表 |
|---|---|---|---|
| SOURCE | 编译时就没了 | ❌ | @Override |
| CLASS | 存到class文件 | ❌ | 默认值(很少用) |
| RUNTIME | 运行时还在 | ✅ | @Autowired、@RequestMapping |
八、练习题
练习1(基础):定义一个 @Author 注解,包含 name 和 date 两个属性,贴在类上,用反射读取并打印
练习2(进阶):模仿Spring的 @Autowired,写一个简单的自动注入:定义 @Inject 注解,写一个方法扫描对象的所有字段,如果有 @Inject 注解就自动创建并注入对象
练习3(综合):写一个 @Validate 注解(属性:min、max),贴在字段上,写一个校验方法检查对象的所有 @Validate 字段是否在范围内。这就是 Bean Validation(如 @Size、@Min)的简化版原理
| 上一章 | 目录 | 下一章 |
|---|---|---|
| 反射机制 | java基础 | 枚举类型 |