Java作为一种强大的面向对象编程语言,提供了多种高级特性来提升开发效率和灵活性。这些特性包括单元测试、反射、注解和动态代理,它们在现代Java开发中扮演着重要角色。
1. 单元测试
单元测试是软件开发中的关键实践,用于**验证代码的单个单元(如方法或类)**是否按预期工作。在Java中,JUnit是最流行的单元测试框架,它简化了测试编写和执行过程。
- 为什么重要:单元测试能及早发现错误,提高代码质量,并支持重构。测试覆盖率可以用数学公式表示。
- 核心元素 :JUnit使用注解(如
@Test)标记测试方法,并提供断言方法(如assertEquals)来验证结果。 - 示例代码:以下是一个简单的JUnit测试示例,测试一个计算器类的加法方法。
java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CalculatorTest {
@Test
public void testAdd() {
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
assertEquals(5, result); // 验证结果是否等于5
}
}
class Calculator {
public int add(int a, int b) {
return a + b;
}
}
在这个示例中:
@Test注解标记了测试方法。assertEquals用于比较预期值和实际值。- 运行测试时,如果
add方法返回5,测试通过;否则失败。
示例
• 某个系统,有多个业务方法,请使用Junit单元测试框架,编写测试代码,完成对这些方法的正确性测试。
具体步骤
① 将Junit框架的jar包导入到项目中 (注意:IDEA集成了Junit框架,不需要我们自己手工导入了)
② 为需要测试的业务类,定义对应的测试类,并为每个业务方法,编写对应的测试方法**(必须:公共、无参、无返回值)**
③ 测试方法上必须声明@Test注解 ,然后在测试方法中,编写代码调用被测试的业务方法进行测试;
④ 开始测试:选中测试方法,右键选择"JUnit运行",如果测试通过则是绿色;如果测试失败,则是红色
2. 反射
反射(Reflection)是Java的一种机制,允许程序在运行时检查和修改类、对象、方法和字段的结构。它通过java.lang.reflect包实现,常用于框架开发(如Spring)和动态加载类。
(一) 认识反射(Reflection)
反射是指在程序运行时,能够动态地获取、检查和操作类、对象、方法、属性等程序自身结构的能力。它允许程序在运行时"观察"和修改自身的状态和行为,而无需在编译时知道具体的类型信息。简单来说,反射就是程序在运行时"认识自己"的能力。
核心思想是:通过类的元数据(Metadata),在运行时动态地获取类的信息,并对其成员进行操作。
理解:反射就是加载类并允许以编程的方式解剖类中的各种成分(成员变量,方法,构造器等)
- 为什么重要:反射提供了动态性,例如在未知类名时创建对象或调用方法。但需注意性能开销,因为反射操作通常比直接调用慢(时间复杂度约为 O(n),其中 n 是反射调用的复杂度)。
- 核心类 :主要类包括
Class(表示类)、Method(表示方法)、Field(表示字段)等。 - 示例代码:以下代码展示如何使用反射获取类的信息并调用方法。
java
import java.lang.reflect.Method;
public class ReflectionExample {
public static void main(String[] args) throws Exception {
// 获取String类的Class对象
Class<?> stringClass = String.class;
// 获取toUpperCase方法并调用
Method toUpperCaseMethod = stringClass.getMethod("toUpperCase");
String str = "hello";
String result = (String) toUpperCaseMethod.invoke(str);
System.out.println(result); // 输出 "HELLO"
}
}
在这个示例中:
Class.forName()或类名.class获取Class对象。getMethod获取方法,invoke调用方法。- 反射常用于动态代理和注解处理。
(二) 获取类及其成分并操作
(1) 获取类 (Class)
通常可以通过以下方式获取一个类的 Clas 对象:
- 使用
类名.class(如:String.class) - 通过对象调用
getClass()方法 (如:"hello".getClass()) - 使用
Class.forName("全限定类名")(如:Class.forName("java.lang.String"))
java
// 示例:获取String类的Class对象
Class<?> stringClass = Class.forName("java.lang.String");
(2) 获取类中的成分
一旦获取了 Class 对象,就可以进一步获取其内部的成员:
-
字段 (Field)
java// 获取所有公共字段 Field[] publicFields = stringClass.getFields(); // 获取所有字段(包括私有) Field[] allFields = stringClass.getDeclaredFields(); // 获取特定字段 Field specificField = stringClass.getDeclaredField("value"); -
方法 (Method)
java// 获取所有公共方法 Method[] publicMethods = stringClass.getMethods(); // 获取所有方法(包括私有) Method[] allMethods = stringClass.getDeclaredMethods(); // 获取特定方法 Method charAtMethod = stringClass.getMethod("charAt", int.class); -
构造器 (Constructor)
java// 获取所有公共构造器 Constructor<?>[] publicConstructors = stringClass.getConstructors(); // 获取所有构造器 Constructor<?>[] allConstructors = stringClass.getDeclaredConstructors(); // 获取特定构造器(如String的byte[]构造器) Constructor<?> byteConstructor = stringClass.getConstructor(byte[].class);
(3) 操作类成分
获取到这些成员后,可以对其进行操作:
-
创建对象实例
java// 使用无参构造器创建String实例 Object strInstance = stringClass.newInstance(); // 使用带参构造器 Constructor<?> constructor = stringClass.getConstructor(byte[].class); String strFromBytes = (String) constructor.newInstance(new byte[]{65, 66, 67}); -
调用方法
java// 调用charAt方法 Method charAt = stringClass.getMethod("charAt", int.class); char result = (char) charAt.invoke("ABC", 1); // 返回 'B' -
访问/修改字段值
java// 获取并修改私有字段(需要设置可访问性) Field valueField = stringClass.getDeclaredField("value"); valueField.setAccessible(true); // 突破封装访问限制 char[] oldValue = (char[]) valueField.get("Hello"); valueField.set("Hello", new char[]{'W', 'o', 'r', 'l', 'd'}); -
注解处理
java// 获取类上的注解 Annotation[] annotations = stringClass.getAnnotations();
具体示例
java
package demo1reflect;
import lombok.Data;
@Data
public class Dog {
private String name;
private int age;
private String hobby;
private Dog() {
}
public Dog(String name) {
this.name = name;
}
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
private void eat() {
System.out.println("吃吃吃吃吃");
}
public String eat(String food){
System.out.println("吃吃吃吃吃:" + food);
return "yammy";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getHobby() {
return hobby;
}
public void setHobby(String hobby) {
this.hobby = hobby;
}
}
java
package demo1reflect;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Objects;
public class ReflectDome2 {
//1.获取Class对象
@Test
public void getClassInfo(){
Class c1 = Dog.class;
System.out.println(c1.getClasses());//类名的全类名
System.out.println(c1.getSimpleName());//类名
}
//2.获取类的构造方法对象并对其进行操作
@Test
public void getConstructorInfo() throws Exception {
//获取类的构造方法对象
//1、获取Class对象
Class c1 = Dog.class;
//2、获取构造方法对象
Constructor[] constructor = c1.getDeclaredConstructors();
for (Constructor c : constructor) {
System.out.println(c.getName() + "(" + c.getParameterCount() + ")");
}
//3. 获取单个构造方法对象
Constructor c2 = c1.getDeclaredConstructor();//获取无参构造方法对象
System.out.println(c2.getName() + "(" + c2.getParameterCount() + ")");
Constructor c3 = c1.getDeclaredConstructor(String.class, int.class);//获取有参构造方法对象
System.out.println(c3.getName() + "(" + c3.getParameterCount() + ")");
//4. 获取的作用:创建对象
//创建无参对象:私有的-暴力反射!
c2.setAccessible(true);//暴力反射,绕过访问权限修饰符
Dog d1 = (Dog) c2.newInstance();
System.out.println(d1);
//创建有参对象
Dog d2 = (Dog) c3.newInstance("小花", 5);
System.out.println(d2);
}
//3. 获取类的成员变量对象并对其进行操作
@Test
public void test03() throws Exception {
//1. 获取类对象
Class c1 = Dog.class;
//2. 获取成员变量对象
Field[] fields= c1.getDeclaredFields();
for (Field field : fields){
System.out.println(field.getName() + "(" + field.getType().getName() + ")");
}
//3.获取单个成员变量对象
Field field = c1.getDeclaredField("hobby");
System.out.println(field.getName() + "(" + field.getType().getName() + ")");
Field field1 = c1.getDeclaredField("age");
System.out.println(field1.getName() + "(" + field1.getType().getName() + ")");
//4. 获取类的成员方法对象的目的依然是取值和设置值
Dog d = new Dog("旺财", 5);
field.setAccessible( true);//暴力反射,hobby属性是私有的,所以需要设置
field.set(d, "看家");
System.out.println(d);
String hobby = (String) field.get(d);//强转的原因: get方法返回的是Object类型
System.out.println(hobby);
}
//4.获取类的成员方法对象并对其进行操作
@Test
public void test4() throws Exception {
//1.获取类本身
Class c1 = Dog.class;
//2.获取方法对象
Method[] methods = c1.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName() + "(" + method.getParameterTypes() + ")");
}
//3.获取单个方法对象
Method m = c1.getDeclaredMethod("eat");//获取无参数eat方法对象
System.out.println(m.getName() + "(" + m.getParameterTypes() + ")");
Method m2 = c1.getDeclaredMethod("eat", String.class);//获取有参数eat方法对象
System.out.println(m2.getName() + "(" + m2.getParameterTypes() + ")");
//4.调用方法对象的作用依然是执行
Dog d1 = new Dog("金毛",3);
m.setAccessible( true);
m.invoke(d1);//调用无参数方法对象,相当于d1.eat();
Object obj = m2.invoke(d1,"骨头");//调用有参数方法对象,相当于d1.eat("骨头");
System.out.println(obj);
}
}
(三) 反射的作用和应用场景
基本作用:可以得到一个类的全部成分然后操作;可以破坏封装性;可以绕过泛型的约束!
最重要的用途:适合做Java的框架,基本上,主流的框架都会基于反射设计出一些通用的功能。
1、作用
- 动态性:程序可以在运行时动态加载类、创建对象、调用方法,提高灵活性。
- 框架开发:是各种框架(如Spring、Hibernate)实现依赖注入、AOP等高级特性的基础。
- 通用代码:编写可处理任意类型的通用工具或库(如序列化/反序列化库)。
- 测试工具:单元测试框架(如JUnit)利用反射查找并执行测试方法。
2、应用场景
-
IoC (控制反转) / DI (依赖注入) 容器 框架通过反射扫描类路径,读取注解(如
@Component,@Autowired),动态创建Bean并注入依赖。java// 伪代码:根据配置创建实例 Class<?> beanClass = Class.forName(beanClassName); Object beanInstance = beanClass.newInstance(); // 查找并注入依赖字段 for (Field field : beanClass.getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { field.setAccessible(true); Object dependency = ...; // 获取依赖对象 field.set(beanInstance, dependency); } } -
ORM (对象关系映射) 框架 如Hibernate,通过反射获取实体类的字段信息,动态生成SQL语句,并将查询结果映射回对象。
java// 伪代码:将结果集映射到对象 ResultSet rs = ...; User user = new User(); for (Field field : User.class.getDeclaredFields()) { field.setAccessible(true); field.set(user, rs.getObject(field.getName())); } -
动态代理 (Proxy) 在运行时创建实现特定接口的代理类,用于实现AOP(面向切面编程),如日志记录、事务管理。
java// Java动态代理示例 InvocationHandler handler = (proxy, method, args) -> { System.out.println("调用方法: " + method.getName()); return method.invoke(target, args); }; MyInterface proxy = (MyInterface) Proxy.newProxyInstance( MyInterface.class.getClassLoader(), new Class[]{MyInterface.class}, handler); -
配置文件驱动的对象创建 根据配置文件中的类名动态创建对象并调用方法。
java// 读取配置文件 String className = config.getProperty("processor.class"); String methodName = config.getProperty("processor.method"); // 反射创建并调用 Class<?> processorClass = Class.forName(className); Object processor = processorClass.newInstance(); Method method = processorClass.getMethod(methodName); method.invoke(processor);
插件系统 允许程序在运行时加载并执行未知的插件模块。
java
// 加载插件JAR,获取入口类并执行
URLClassLoader loader = new URLClassLoader(new URL[]{new File("plugin.jar").toURI().toURL()});
Class<?> pluginClass = loader.loadClass("com.example.Plugin");
PluginInterface plugin = (PluginInterface) pluginClass.newInstance();
plugin.execute();
绕过泛型类型约束
在 Java 中,反射(Reflection) 确实可以绕过泛型(Generics)的类型约束,这主要是由于 Java 泛型的实现机制------类型擦除(Type Erasure)。
一、类型擦除机制
Java 泛型在编译期进行类型检查,但在运行时会将泛型类型信息擦除,替换为原始类型(通常是
Object或边界类型)。例如:
javaList<String> list = new ArrayList<>();编译后,
List<String>会被擦除为List<Object>(实际是原始类型List)。因此运行时无法直接获取泛型的具体类型参数(如String)。
二、反射绕过泛型约束的原理
由于类型擦除,运行时集合中存储的实际是
Object类型。通过反射操作集合时,可以绕过编译期的泛型检查,向集合插入非泛型指定类型的对象。例如:
javaList<String> stringList = new ArrayList<>(); stringList.add("Hello"); // 反射获取 add 方法 Method addMethod = ArrayList.class.getMethod("add", Object.class); addMethod.invoke(stringList, 123); // 插入 Integer 类型此时,
stringList中既包含String类型,也包含Integer类型,绕过了泛型约束。
三、具体操作步骤
获取集合的 Class 对象
通过
getClass()方法获取运行时类信息:
javaClass<?> listClass = stringList.getClass();获取目标方法
泛型擦除后,
add方法的参数类型实际为Object:
javaMethod addMethod = listClass.getMethod("add", Object.class);通过反射调用方法
直接插入非泛型指定类型的对象:
javaaddMethod.invoke(stringList, 123); // 插入整数
四、风险与问题
运行时异常
后续按泛型类型操作集合时可能抛出
ClassCastException:
javafor (String s : stringList) { // 遍历到 Integer 时会抛出异常 }破坏类型安全
泛型的核心目的是保证类型安全,反射绕过约束会破坏这一机制。
五、实际应用场景
虽然不推荐,但在某些特殊场景下可能有用,例如:
- 框架开发:如 JSON 序列化库需动态处理不同类型。
- 兼容旧代码:适配遗留的非泛型代码。
总结
反射绕过泛型是 Java 类型擦除机制的副作用。尽管技术上可行,但会破坏类型安全,需谨慎使用。正确做法是:
- 优先使用泛型保证类型安全;
- 如需动态类型,改用
List<Object>或设计多态结构。
3、注意事项
- 性能开销:反射操作通常比直接代码调用慢,因为涉及动态解析和类型检查。
- 安全限制:可能突破封装性(访问私有成员),需谨慎使用并注意安全管理器设置。
- 代码可读性:过度使用反射可能导致代码难以理解和维护。
总之,反射是一个强大的工具,它为程序提供了高度的灵活性和动态能力,尤其在框架开发和需要处理未知类型的场景中不可或缺。但需权衡其带来的性能损失和复杂性,在适当场景下使用。
3. 注解
(一) 注解概述
注解(Annotation)是 Java 提供的一种元数据 机制,用于为程序元素(如类、方法、变量、参数等)添加额外的说明信息。这些信息本身不会直接影响程序的逻辑,但可以被编译器、开发工具或其他程序读取并利用。
- 核心作用: 提供关于程序代码的元数据描述。
- 编译时处理: 编译器可以利用注解信息进行编译检查(如
@Override)或生成辅助代码/文件(如APT)。 - 运行时处理: 通过反射机制读取注解信息,实现运行时动态行为(如Spring框架的依赖注入)。
- 文档化:
@Deprecated等注解有助于生成文档。
- 为什么重要 :注解简化了配置和元数据管理,例如在JUnit中标记测试方法,或在Spring中依赖注入。常见的内置注解包括
@Override、@Deprecated和@SuppressWarnings。 - 自定义注解 :开发者可以定义自己的注解,通过
@interface关键字。 - 示例代码:以下展示如何定义和使用一个自定义注解。
java
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 ImportantMethod {
}
// 使用注解的类
public class AnnotationExample {
@ImportantMethod
public void criticalOperation() {
System.out.println("执行重要操作");
}
public static void main(String[] args) {
AnnotationExample example = new AnnotationExample();
example.criticalOperation(); // 输出 "执行重要操作"
}
}
在这个示例中:
@ImportantMethod是一个自定义注解。@Retention指定注解保留到运行时,以便反射处理。- 注解可以被反射API读取,用于动态行为。
(二) 元注解
元注解(Meta-Annotation)是用于注解其他注解 的注解。它们定义了被注解的注解的行为和适用范围。Java 提供了几个核心的元注解(位于java.lang.annotation包):
@Retention: 指定被注解的注解的保留策略 (即生命周期)。RetentionPolicy.SOURCE: 仅存在于源代码中,编译时被丢弃(如@Override,@SuppressWarnings)。RetentionPolicy.CLASS: 保留在 class 文件中,但不会被加载到 JVM(默认策略,较少用)。RetentionPolicy.RUNTIME: 保留在 class 文件中,并被加载到 JVM,可在运行时通过反射读取(框架常用)。[即一直保留到运行阶段!]
@Target: 指定被注解的注解可以应用于哪些程序元素 。- 常用元素类型:
ElementType.TYPE(类/接口/枚举),ElementType.FIELD(字段),ElementType.METHOD(方法),ElementType.PARAMETER(参数),ElementType.CONSTRUCTOR(构造器),ElementType.LOCAL_VARIABLE(局部变量),ElementType.ANNOTATION_TYPE(注解类型),ElementType.PACKAGE(包),ElementType.TYPE_PARAMETER(类型参数-JDK8),ElementType.TYPE_USE(类型使用-JDK8)。
- 常用元素类型:
@Documented: 指明被注解的注解应该被包含在 Javadoc 文档中。@Inherited: 指明被注解的注解具有继承性。如果父类使用了该注解,子类默认也会继承这个注解(仅对类注解有效)。@Repeatable(JDK8+): 指明被注解的注解可以在同一个元素上重复使用。
(三) 自定义注解
开发者可以根据需要定义自己的注解类型。定义格式类似于接口,但使用 @interface 关键字:
java
@Retention(RetentionPolicy.RUNTIME) // 元注解:运行时保留
@Target(ElementType.METHOD) // 元注解:只能注解方法
public @interface MyCustomAnnotation {
// 定义注解的属性(成员变量)
String value() default "defaultValue"; // 带默认值的属性
int priority() default 0; // 带默认值的属性
String[] tags() default {}; // 数组类型属性
}
- 注解的属性声明类似于接口的方法,但没有参数,可以有默认值 (
default ...)。 - 属性的类型有限制:基本类型、
String、Class、枚举类型、注解类型,以及这些类型的数组。 - 使用注解时,通过
@AnnotationName(propertyName=value)格式指定属性值。如果注解只有一个名为value的属性且需要赋值时,可以省略value=直接写值。如果所有属性都有默认值,可以不指定任何属性值 (@AnnotationName)。
(四) 注解的解析
注解本身只是元数据,其功能需要通过解析器来实现。解析方式主要有两种:
- 编译时解析 (APT - Annotation Processing Tool):
- 在编译阶段,由特定的注解处理器 (
javax.annotation.processing.Processor) 处理源代码或 class 文件中的注解。 - 处理器可以读取注解信息,生成新的源代码、资源文件或编译错误/警告。
- 处理过程在 Javac 编译时自动触发。处理器生成的代码会参与后续的编译。
- 特点: 不修改原有代码,生成新代码。常用于生成样板代码(如 Builder 模式)、验证约束(如检查是否实现了接口)、生成配置文件等。例如 Lombok 库的核心原理。
- 在编译阶段,由特定的注解处理器 (
- 运行时解析 (Reflection):
- 在程序运行时,通过 Java 的反射 API (
java.lang.reflect) 获取注解信息。 - 核心类:
AnnotatedElement接口(Class,Method,Field,Constructor等都实现了它),提供getAnnotation(Class),isAnnotationPresent(Class),getAnnotations()等方法。 - 特点: 需要注解保留策略为
RUNTIME。常用于框架的动态行为,如 Spring 的依赖注入 (@Autowired)、事务管理 (@Transactional)、Web 路由 (@RequestMapping) 等。
- 在程序运行时,通过 Java 的反射 API (
AnnotatedElement 接口解析
AnnotatedElement 是 Java 反射 API 中的核心接口,用于处理注解(Annotation)。它定义了一套标准方法来访问和操作类、方法、字段等元素上的注解。许多核心类如
Class、Method、Field和Constructor都实现了这个接口,使得开发者可以统一地获取和检查注解信息。1. AnnotatedElement 接口的作用
AnnotatedElement 接口的主要目的是提供一种通用的方式来访问元素的注解。在 Java 中,注解用于添加元数据到代码中,例如标记方法为测试用例或指定字段的序列化规则。通过实现这个接口,任何支持注解的元素(如类、方法等)都能通过反射机制被查询和处理。这简化了注解的解析过程,无需为每种元素类型编写重复代码。
2. 核心方法解析
AnnotatedElement 接口定义了多个方法,用于获取和检查注解。以下是主要方法的详细说明:
getAnnotation(Class<T> annotationClass):这个方法用于获取指定类型的注解实例。参数
annotationClass是注解的 Class 对象。如果元素上存在该注解,则返回注解实例;否则返回null。例如,检查一个方法是否使用了@Override注解:
javaMethod method = ... // 获取方法的实例 Override overrideAnnotation = method.getAnnotation(Override.class); if (overrideAnnotation != null) { System.out.println("该方法被 @Override 注解标记"); }
isAnnotationPresent(Class<? extends Annotation> annotationClass):这个方法检查元素上是否存在指定类型的注解。它返回一个布尔值:
true表示注解存在,false表示不存在。这个方法比getAnnotation更轻量级,适合快速检查:
javaField field = ... // 获取字段的实例 if (field.isAnnotationPresent(Deprecated.class)) { System.out.println("该字段已废弃,请勿使用"); }
getAnnotations():这个方法返回元素上的所有注解(包括继承的注解,如果有)。返回值是一个
Annotation[]数组。如果元素没有注解,则返回空数组。这适用于需要遍历所有注解的场景:
javaClass<?> clazz = ... // 获取类的实例 Annotation[] annotations = clazz.getAnnotations(); for (Annotation ann : annotations) { System.out.println("注解类型: " + ann.annotationType()); }其他相关方法:
getDeclaredAnnotations():返回直接声明在元素上的注解(不包括继承的注解)。getAnnotationsByType(Class<T> annotationClass):用于获取重复注解(Java 8 引入),返回指定类型的所有注解实例。getDeclaredAnnotationsByType(Class<T> annotationClass):类似,但只包括直接声明的注解。3. 实现类和实际应用
AnnotatedElement 接口由多个 Java 核心类实现,包括:
Class:用于访问类或接口上的注解。Method:用于访问方法上的注解。Field:用于访问字段上的注解。Constructor:用于访问构造器上的注解。Package:用于访问包上的注解。这些实现使得注解解析具有一致性。例如,在自定义注解处理器中,你可以通过反射遍历一个类的所有元素:
javaimport java.lang.annotation.*; import java.lang.reflect.*; // 定义一个自定义注解 @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) public @interface MyTest { String value(); double height() default 180.2; String[] address(); } // 使用注解的示例类 @MyTest(value = "wjk", address = "beijing") public class Demo { @MyTest(value = "wy", address = {"beijing","boston"}) public void go(){ } } // 解析注解的主类 public class AnnotationDome2 { //解析注解 @Test public void praseClass() throws Exception { //获取类对象 Class c = Demo.class; //判断类对象上是否有MyTest注解 if (c.isAnnotationPresent(MyTest.class)) { //获取类对象上的MyTest注解 MyTest myTest = (MyTest) c.getDeclaredAnnotation(MyTest.class); //获取注解的属性值 System.out.println(myTest.value()); System.out.println(myTest.height()); System.out.println(Arrays.toString(myTest.address())); } } @Test public void praseMethod() throws Exception{ Class c = Demo.class;//获取Demo类对象 //获取Demo类对象中的方法对象 Method method = c.getMethod("go"); //判断方法上是否有MyTest注解 if (method.isAnnotationPresent(MyTest.class)){ //获取方法上的MyTest注解 MyTest myTest = method.getDeclaredAnnotation(MyTest.class); //获取注解的属性值 System.out.println(myTest.value()); System.out.println(myTest.height()); System.out.println(Arrays.toString(myTest.address())); } } }运行上述代码,输出可能为:
javawjk 180.2 [beijing] wy 180.2 [beijing, boston]4. 注意事项和最佳实践
- 性能考虑:反射操作(如获取注解)可能较慢,应在必要时使用,例如在框架初始化时缓存结果。
- 注解保留策略 :确保注解的
@Retention设置为RetentionPolicy.RUNTIME,否则反射无法访问。- 继承性 :默认情况下,注解不会被继承;如果需要,使用
@Inherited元注解。- 错误处理 :调用这些方法时,如果元素不可访问(如私有字段),可能抛出
SecurityException,需适当处理。
(五) 注解的作用与应用场景
- 编译检查:
@Override,@FunctionalInterface帮助编译器检查代码的正确性。 - 代码生成: APT 用于自动生成代码,减少重复劳动(如 Dagger, Lombok, MapStruct)。
- 框架配置: 简化配置,代替 XML 配置文件(如 Spring Boot 的
@SpringBootApplication,@RestController,@Bean)。 - 运行时行为控制: 框架根据运行时解析的注解实现 AOP、事务、权限控制等(如 Spring, Hibernate)。
- 文档生成:
@Deprecated,@see,@param,@return等用于 Javadoc。 - 代码分析: 静态代码分析工具(如 FindBugs, SonarQube)可以利用注解信息。
- 测试: JUnit 的
@Test,@Before,@After等标记测试方法。 - 序列化/反序列化: Jackson 的
@JsonProperty,@JsonIgnore控制 JSON 处理。 - 依赖注入: Guice, Spring 的
@Inject,@Autowired。 - 标记接口替代: 比标记接口更灵活(如
@Entity代替Entity接口)。 - 约束校验: Bean Validation (JSR 303/349/380) 的
@NotNull,@Size,@Pattern等。
总结: Java 注解是一种强大的元数据机制,通过元注解定义其行为,允许开发者创建自定义注解。注解的生命周期(@Retention)和适用范围(@Target)由元注解控制。注解的功能依赖于编译时(APT)或运行时(反射)的解析器。
4. 动态代理
动态代理(Dynamic Proxy)是Java反射机制的一部分,允许在运行时创建代理对象,用于拦截方法调用。它常用于AOP(面向切面编程)和日志记录等场景。
- 为什么重要:动态代理实现了间接调用,增强了代码的灵活性和可维护性。例如,在代理模式中,时间复杂度可能为 O(1) 如果代理直接委托,但添加逻辑会增加开销。
- 核心类 :
java.lang.reflect.Proxy类用于创建代理对象,InvocationHandler接口处理调用。 - 示例代码:以下代码展示如何创建一个动态代理来记录方法调用。
java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
// 定义接口
public interface StarServer {
void sing(String name);
String dance();
}
// 实现接口的类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Star implements StarServer{
private String name;
@Override
public void sing(String name) {
System.out.println(this.name+"正在唱歌"+name);
}
@Override
public String dance() {
System.out.println(this.name+"正在跳舞...");
return "Thanks";
}
}
// InvocationHandler实现-代理工厂:创建代理并实现方法
//代理工具类:负责创建代理
public class ProxyUtil {
//创建一个Proxy对象,代理
public static StarServer createProxy(Star s){
/**
* 参数一:用于执行用哪个类加载器去加载生成的代理类
* 参数二:用于指定代理类需要实现的接口:明星类实现了接口,所以代理类必须实现这个接口
* 参数三:用于指定代理类需要如何去代理(代理要做的事情)
*/
StarServer proxy = (StarServer) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),
//new Class[]{StarServer.class},
s.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//声明代理对象要干的事情
//参数一:代理对象
//参数二:正在被代理的方法
//参数三:被代理的方法的参数
String name = method.getName();
if (name.equals("sing")){
System.out.println("准备话筒...");
}else if (name.equals("dance")){
System.out.println("准备场地...");
}
//真正干活,找Star对象来执行被代理的行为:method.invoke(star,args);
Object result = method.invoke(s, args);
return result;
}
});
return proxy;
}
}
//测试
public class Test {
static void main(String[] args) {
//创建一个Star对象
Star star = new Star("yyqx");
//为star创建一个代理对象
StarServer proxy =ProxyUtil.createProxy(star);
proxy.sing("Relax");
System.out.println(proxy.dance());
}
}
综合应用
这些技术常结合使用。例如:
- 在单元测试中,使用注解标记测试方法。
- 反射用于读取注解信息或动态创建对象。
- 动态代理在测试中模拟对象行为(如Mockito框架)。 整体上,它们提升了Java的灵活性和可扩展性。
总结:单元测试确保代码质量,反射提供运行时自省能力,注解简化元数据管理,动态代理实现方法拦截。掌握这些高级特性是Java开发者的关键技能,能显著提高开发效率和代码健壮性。实践中,建议优先使用框架(如JUnit、Spring)来简化实现。
Javase完结,后面会接着写Javaweb相关。