Java 注解定义与元注解
一、注解定义基础
-
语法 :使用
@interface
关键字定义注解,格式如下:javapublic @interface 注解名 { 参数类型 参数名() default 默认值; // 参数可带默认值(推荐) }
- 参数规则 :参数类似无参数方法,最常用参数建议命名为
value
。
- 参数规则 :参数类似无参数方法,最常用参数建议命名为
二、元注解(修饰注解的注解)
-
@Target
-
作用:指定注解可应用的源码位置(类型、字段、方法等)。
-
参数 :
ElementType
枚举值,可单值或数组(如{ TYPE, METHOD }
)。 -
示例:
java@Target(ElementType.METHOD) // 注解仅用于方法
-
-
@Retention
-
作用:定义注解的生命周期(源码期、编译期、运行期)。
-
参数 :
RetentionPolicy
枚举值:SOURCE
:仅源码有效(编译后丢弃)。CLASS
:编译到 class 文件(默认值)。RUNTIME
:运行期可用(自定义注解常用,需显式声明)。
-
示例:
java@Retention(RetentionPolicy.RUNTIME) // 运行期保留注解
-
-
@Repeatable
-
作用:允许注解在同一位置重复使用。
-
使用:需定义包含注解数组的容器注解,示例:
java@Repeatable(Reports.class) // 允许重复@Report注解 @interface Report { ... } @interface Reports { Report[] value(); }
-
-
@Inherited
-
作用 :子类自动继承父类的类型注解(仅针对
@Target(TYPE)
的类注解,接口无效)。 -
示例:
java@Inherited // 子类继承@Report注解 @Target(ElementType.TYPE) @interface Report { ... }
-
三、定义注解步骤
-
声明注解 :使用
@interface
定义基础结构。 -
添加参数 :定义参数及默认值(推荐为所有参数设置默认值,核心参数命名为
value
)。 -
配置元注解:
- 必须设置
@Target
(指定应用范围)和@Retention(RetentionPolicy.RUNTIME)
(运行期可用)。 - 可选
@Repeatable
(重复注解)和@Inherited
(继承性,按需使用)。
- 必须设置
处理注解
一、注解的保留策略
- SOURCE :注解仅存在于源代码中,编译期被丢弃,由编译器使用(如
@Override
),无需手动编写。 - CLASS:注解保存到 class 文件中,但 JVM 加载时不读取,主要由底层工具库使用,开发中较少涉及。
- RUNTIME :注解在运行期被加载到 JVM,可通过反射读取,是自定义业务逻辑的核心,需手动编写和使用(必须声明
@Retention(RetentionPolicy.RUNTIME)
)。
二、反射 API 读取注解
1. 判断注解是否存在
通过Class/Field/Method/Constructor.isAnnotationPresent(Class<? extends Annotation> annotationClass)
判断目标对象是否存在指定注解。
java
Person.class.isAnnotationPresent(Report.class); // 判断类是否有@Report注解
2. 获取注解实例
通过Class/Field/Method/Constructor.getAnnotation(Class<? extends Annotation> annotationClass)
获取注解实例并读取属性。
java
Report report = Person.class.getAnnotation(Report.class);
int type = report.type(); // 获取注解属性
3. 处理方法参数注解
方法参数注解以二维数组形式存储,通过Method.getParameterAnnotations()
获取,遍历处理每个参数的注解。
java
Annotation[][] annos = method.getParameterAnnotations();
for (int i = 0; i < annos.length; i++) {
for (Annotation anno : annos[i]) {
// 判断注解类型并处理
}
}
三、实战示例:@Range 注解校验 JavaBean 字段
1. 定义 @Range 注解(RUNTIME 保留策略)
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD) // 限定用于字段
public @interface Range {
int min() default 0; // 最小允许值
int max() default 255; // 最大允许值
}
2. 在 JavaBean 中应用注解
java
public class Person {
@Range(min = 1, max = 20) // 字符串长度需在1-20之间
public String name;
@Range(max = 10) // 整数需≤10
public int age;
}
3. 编写校验逻辑(反射实现)
- 遍历对象字段,获取
@Range
注解并校验值(字符串长度 / 整数范围)。 - 处理私有字段时需调用
field.setAccessible(true)
突破访问限制。
java
void check(Person person) throws ReflectiveOperationException {
for (Field field : person.getClass().getFields()) {
Range range = field.getAnnotation(Range.class);
if (range != null) {
Object value = field.get(person);
if (value instanceof String s) {
if (s.length() < range.min() || s.length() > range.max()) {
throw new IllegalArgumentException("Invalid field: " + field.getName());
}
} else if (value instanceof Integer i) {
if (i < range.min() || i > range.max()) {
throw new IllegalArgumentException("Invalid field: " + field.getName());
}
}
}
}
}
四、核心要点与注意事项
-
注解生效前提 :必须声明
@Retention(RUNTIME)
,否则运行时无法读取。 -
反射关键作用 :通过
Class
、Field
等反射对象的 API 动态获取注解,实现自定义逻辑(如参数校验、框架功能扩展)。 -
应用场景:
- 自定义参数校验(如示例中的字段范围检查);
- 框架集成(如 JUnit 通过
@Test
识别测试方法,Spring 通过@Autowired
实现依赖注入)。
-
注意事项:
- 注解本身不影响代码逻辑,需手动编写处理逻辑;
- 高频调用场景建议缓存注解信息以提升性能。
Java 泛型
一、核心概念
泛型是一种编写模板代码的机制,通过定义类型参数(如T
),使类、接口或方法能够适应任意类型,实现代码的通用性和类型安全。
二、传统集合类的问题(以ArrayList
为例)
-
无泛型时的缺陷
- 内部使用
Object[]
数组,存入 / 取出需强制转型(如(String) list.get(0)
)。 - 编译期不检查类型,运行时可能抛出
ClassCastException
(如误将Integer
存入String
类型的集合)。
- 内部使用
-
重复代码问题
- 为不同类型(如
String
、Integer
、Person
)单独编写类,导致代码冗余(如StringArrayList
、IntegerArrayList
)。
- 为不同类型(如
三、泛型的解决方案
-
定义泛型类
- 使用类型参数
T
作为模板,例如:
javapublic class ArrayList<T> { private T[] array; public void add(T e) { ... } public T get(int index) { ... } }
- 使用类型参数
-
实例化时指定类型
- 创建具体类型的泛型实例,编译器自动检查类型:
javaArrayList<String> strList = new ArrayList<String>(); // 仅允许存入String strList.add("Hello"); // 合法 strList.add(123); // 编译错误(不允许非String类型)
- 无需强制转型,直接获取指定类型:
javaString first = strList.get(0); // 直接得到String,无需转型
四、泛型的核心优势
-
类型安全
- 编译期严格检查存入 / 取出的类型,避免运行时类型错误。
-
代码复用
- 编写一次泛型类,可适配所有类型,无需为每种类型单独实现。
-
消除强制转型
- 直接通过类型参数获取目标类型,提升代码简洁性。
五、泛型的继承规则(关键注意点)
-
允许向上转型为泛型接口
ArrayList<T>
可向上转型为List<T>
(因为ArrayList<T>
实现了List<T>
接口):
javaList<String> list = new ArrayList<String>(); // 合法
-
不允许泛型类型的父类转型
- 禁止 将
ArrayList<Integer>
转型为ArrayList<Number>
或List<Number>
,即使Integer
是Number
的子类:
javaArrayList<Integer> intList = new ArrayList<>(); ArrayList<Number> numList = intList; // 编译错误!
- 原因 :若允许转型,通过
numList
存入Float
(Number
的子类)会导致intList
中出现非法类型,运行时获取时抛出ClassCastException
。 - 结论 :泛型类型
T
必须严格匹配,不存在继承关系(如ArrayList<Integer>
与ArrayList<Number>
无任何父子关系)。
- 禁止 将
Java 泛型使用要点
一、泛型基础概念
-
类型安全增强
- 未定义泛型时,
ArrayList
默认类型为Object
,需强制转型且存在类型安全隐患(如运行时ClassCastException
)。 - 定义泛型后(如
List<String>
),编译器确保类型一致,无需强制转型,提升代码安全性和可读性。
- 未定义泛型时,
-
类型推断(Type Inference)
- 编译器可自动推断泛型类型,简化代码书写。
- 例:
List<Number> list = new ArrayList<>();
,省略右侧<Number>
,由左侧声明推断类型。
二、泛型使用场景
-
集合类应用
- 定义强类型集合 :通过
<T>
指定元素类型,如List<String>
、List<Number>
,确保添加元素类型一致。 - 避免编译器警告 :未指定泛型时(如
List list = new ArrayList();
),编译器提示未经检查的操作警告。
- 定义强类型集合 :通过
-
泛型接口实现
-
Comparable<T>
接口 :定义对象比较逻辑,实现该接口的类可用于排序(如Arrays.sort()
)。- 例:自定义
Person
类实现Comparable<Person>
,重写compareTo
方法,支持按name
或score
排序。
- 例:自定义
-
实现泛型接口时需明确具体类型(如
Comparable<Person>
),否则导致类型不匹配异常(如ClassCastException
)。
-
三、核心代码示例
-
泛型集合基础用法
java// 无泛型:需强制转型,存在类型风险 List list = new ArrayList(); list.add("Hello"); String first = (String) list.get(0); // 强制转型 // 强类型泛型:类型安全,无需转型 List<String> list = new ArrayList<>(); // 类型推断省略右侧泛型 list.add("Hello"); String first = list.get(0); // 直接获取强类型元素
-
泛型接口实现示例
javaclass Person implements Comparable<Person> { String name; int score; // 按 name 排序 public int compareTo(Person other) { return this.name.compareTo(other.name); } } // 使用 Arrays.sort 排序 Person 数组 Person[] ps = { ... }; Arrays.sort(ps); // 基于 compareTo 逻辑排序
编写泛型
一、核心内容概述
本文介绍如何编写 Java 泛型类,包括基础步骤、静态方法处理、多泛型类型定义等关键知识点,适用于需要自定义泛型类的场景(如集合类扩展)。
二、编写泛型类的步骤
-
从具体类型开始
先按特定类型(如
String
)编写类,定义字段、构造方法和方法。javapublic class Pair { private String first; private String last; // 构造方法、getter等 }
-
替换为泛型类型
<T>
将所有特定类型替换为泛型类型
T
,并在类名后声明<T>
。javapublic class Pair<T> { private T first; private T last; public Pair(T first, T last) { ... } public T getFirst() { ... } // 其他方法使用T作为返回/参数类型 }
三、静态方法与泛型
-
禁止直接使用类泛型
<T>
静态方法属于类级别,无法引用实例级泛型
T
,直接使用会导致编译错误。
错误示例:javapublic static Pair<T> create(T first, T last) { ... } // 编译错误
-
定义独立泛型方法
在静态方法前单独声明泛型类型(如
<K>
),与类泛型区分。
正确示例:javapublic static <K> Pair<K> create(K first, K last) { return new Pair<K>(first, last); }
四、多个泛型类型
-
定义多泛型类
通过
<T, K>
声明多种泛型类型,用于存储不同类型的对象。javapublic class Pair<T, K> { private T first; private K last; public Pair(T first, K last) { ... } public T getFirst() { ... } public K getLast() { ... } }
-
使用场景
类似 Java 标准库
Map<K, V>
,分别定义键(K)和值(V)的类型。
示例:javaPair<String, Integer> p = new Pair<>("test", 123);
Java 泛型擦拭法(Type Erasure)详解
一、擦拭法核心概念
-
定义 :Java 泛型的实现方式,编译器在编译时处理泛型,虚拟机运行时 "看不到" 泛型,所有泛型类型
T
会被擦除为Object
(或其边界类型)。 -
本质:
- 编译器将泛型类 / 方法中的
T
视为Object
,并在必要时插入安全强制转型。 - 运行时,泛型类型信息消失,仅保留原始类型(如
Pair<T>
编译后为Pair
,字段和方法参数为Object
)。
- 编译器将泛型类 / 方法中的
二、擦拭法的具体表现
-
编译前后对比
- 编译器视角 :泛型类
Pair<T>
包含类型参数T
,如T first
、T getFirst()
。 - 虚拟机视角 :
T
被擦除为Object
,实际代码为Object first
、Object getFirst()
,使用时自动转型(如(String)p.getFirst()
)。
- 编译器视角 :泛型类
-
示例代码
java// 编译器代码(泛型) Pair<String> p = new Pair<>("Hello", "World"); String first = p.getFirst(); // 自动转型 // 虚拟机执行代码(无泛型) Pair p = new Pair("Hello", "World"); String first = (String) p.getFirst(); // 手动转型(编译器隐式插入)
三、擦拭法的局限
-
T
不能是基本类型- 原因:
T
被擦除为Object
,而Object
无法直接存储基本类型(如int
)。 - 示例:
Pair<int>
编译错误,需使用包装类型Pair<Integer>
。
- 原因:
-
无法获取带泛型的
Class
-
所有泛型实例的
getClass()
返回同一个原始类型的Class
。 -
示例:
javaPair<String> p1 = new Pair<>("a", "b"); Pair<Integer> p2 = new Pair<>(1, 2); System.out.println(p1.getClass() == p2.getClass()); // true(均为Pair.class)
-
-
无法判断泛型类型实例
- 编译时禁止使用
instanceof
判断泛型类型(如if (p instanceof Pair<String>)
),因运行时无泛型信息。
- 编译时禁止使用
-
不能直接实例化
T
类型-
禁止
new T()
,因擦除后为new Object()
,类型不安全。 -
解决方案:通过反射传入
Class<T>
参数实现实例化:javapublic Pair(Class<T> clazz) throws InstantiationException, IllegalAccessException { first = clazz.newInstance(); // 借助Class实例创建T对象 }
-
四、泛型方法与覆写问题
-
方法覆写冲突 :若泛型方法擦除后与
Object
方法签名冲突(如equals(T t)
擦除为equals(Object t)
),编译器会报错。-
错误示例:
javapublic class Pair<T> { public boolean equals(T t) { ... } // 编译错误,与Object.equals(Object)冲突 }
-
解决:改用不同方法名(如
same(T t)
)。
-
五、泛型继承与类型获取
-
子类继承泛型父类 :子类可固定父类的泛型类型(如
IntPair extends Pair<Integer>
),此时子类中T
被具体化(如Integer
)。 -
通过反射获取父类泛型类型:
-
使用
ParameterizedType
接口获取父类的实际类型参数:javaType superClass = IntPair.class.getGenericSuperclass(); if (superClass instanceof ParameterizedType) { Type[] typeArgs = ((ParameterizedType) superClass).getActualTypeArguments(); Class<?> type = (Class<?>) typeArgs[0]; // 获取父类泛型类型(如Integer) }
-
extends 通配符
一、核心概念:上界通配符(Upper Bounds Wildcards)
- 定义 :使用
<? extends 父类>
形式的泛型通配符,限定泛型类型为指定父类或其子类(包括父类本身)。 - 作用 :解决泛型类型的继承问题。例如,
Pair<Integer>
不是Pair<Number>
的子类,无法直接传递给参数为Pair<Number>
的方法,通过Pair<? extends Number>
可允许所有Number
及其子类的Pair
类型传入。
二、核心特性与规则
-
只读不写原则
- 允许读操作 :通过
<? extends 父类>
获取的对象,其方法返回值可安全赋值给父类引用(如Number x = p.getFirst();
),因为编译器确保返回值是父类或其子类。 - 禁止写操作 :无法向
<? extends 父类>
类型的对象写入任何非null
值(除null
外)。例如,p.setFirst(new Integer(1));
会编译错误,因为实际类型可能是Double
等其他子类,写入Integer
会破坏类型安全。
- 允许读操作 :通过
-
与泛型类定义的区别
- 方法参数通配符 (
<? extends Number>
):临时限定方法接收的泛型类型,不改变原有泛型类的定义,仅在方法作用域内生效。 - 泛型类限定 (
<T extends Number>
):定义泛型类时直接限定T
的类型范围,实例化时只能使用Number
及其子类(如Pair<Integer>
合法,Pair<String>
非法)。
- 方法参数通配符 (
三、典型场景与示例
-
方法参数应用
-
错误场景 :直接使用
Pair<Number>
作为参数时,无法接收Pair<Integer>
等子类实例。 -
正确写法 :使用
Pair<? extends Number>
允许子类传入,同时确保读取安全:java
javascriptstatic int add(Pair<? extends Number> p) { Number first = p.getFirst(); // 合法,返回值为Number或其子类 // p.setFirst(new Integer(1)); 编译错误,禁止写入 return first.intValue() + p.getLast().intValue(); }
-
-
Java 集合中的应用
-
当方法只需读取集合元素(如计算总和),不修改集合时,使用
List<? extends 类型>
:java
sqlint sumOfList(List<? extends Integer> list) { int sum = 0; for (Integer n : list) sum += n; // 合法,读取元素 // list.add(new Integer(1)); 编译错误,禁止添加 return sum; }
-
四、关键限制与原因
- 擦拭法影响 :泛型在编译期会被擦拭,
<? extends Number>
实际类型在运行时无法确定具体子类,因此写入操作可能导致类型不匹配(如向Pair<Double>
写入Integer
)。 - 唯一例外 :允许写入
null
,但会导致后续读取时抛出NullPointerException
。
Java 泛型中的super
通配符详解
一、核心概念:super
通配符的作用
-
定义 :
<? super T>
表示泛型类型为T
或T
的父类(包括直接父类和祖先类)。 -
核心作用 :允许向泛型对象中写入
T
类型的数据,但限制读取操作(仅能读取为Object
类型)。 -
示例:
java
javascriptvoid setSame(Pair<? super Integer> p, Integer n) { p.setFirst(n); // 允许写入Integer类型 Object obj = p.getFirst(); // 仅能读取为Object }
二、super
与extends
通配符对比
特性 | <? extends T> (上界通配符) |
<? super T> (下界通配符) |
---|---|---|
读取操作 | 允许读取T 类型(生产者) |
仅允许读取Object 类型 |
写入操作 | 仅允许写入null |
允许写入T 及其子类类型(消费者) |
典型场景 | 从集合中读取元素(如src.get() ) |
向集合中写入元素(如dest.add() ) |
三、PECS 原则(Producer Extends Consumer Super)
-
生产者(Producer) :当需要从泛型对象中读取数据(返回
T
)时,使用extends
通配符(确保类型安全)。
示例 :List<? extends T> src
(从src
读取T
)。 -
消费者(Consumer) :当需要向泛型对象中写入数据(接受
T
)时,使用super
通配符(允许父类类型接收子类数据)。
示例 :List<? super T> dest
(向dest
写入T
)。 -
应用案例 :Java 标准库
Collections.copy()
方法:java
typescriptpublic static <T> void copy(List<? super T> dest, List<? extends T> src) { // dest是消费者(写入T),src是生产者(读取T) }
四、无限定通配符<?>
-
定义 :
Pair<?>
表示未知类型的泛型,是所有Pair<T>
的超类。 -
限制:
- 不能写入非
null
数据(类型不确定)。 - 只能读取为
Object
类型。
- 不能写入非
-
示例:
java
javascriptvoid sample(Pair<?> p) { Object first = p.getFirst(); // 允许 p.setFirst(123); // 编译错误(类型不明确) }
五、关键规则与最佳实践
-
读写分离原则:
- 只读场景用
extends
,只写场景用super
。 - 禁止在同一泛型对象上同时进行读写(避免类型安全问题)。
- 只读场景用
-
类型擦除影响:
- 编译后泛型信息会被擦除,
super
通配符通过编译器静态检查保证类型安全。
- 编译后泛型信息会被擦除,
-
优先使用具体泛型:
- 无限定通配符
<?>
很少使用,多数场景可通过定义泛型参数<T>
替代。
- 无限定通配符