Annotation 注解
注解 是在 J5 引入的概念。
同 class 和 interface 一样,注解也属于一种类型
。
注解提供了一系列数据用来装饰程序代码(类、方法、字段等),但是注解并不是所装饰代码的一部分,它对代码的运行效果没有直接影响:
// Autowired 注解本来是用于 Spring 的自动注入,但添加也并不影响代码的运行
public cl.ass AutowiredTest {
@Autowired
private String name;
public static void main(String[] args) {
System.out.println("Not SpringFramework");
}
}
- 有些注解只适用于方法,有些只适用于成员变量,有些只适用于类,有些则都适用。
- 注解的生命周期有 3 种策略,三种策略的生命长度
依次增加
。- SOURCE:在源文件中有效,被编译器丢弃。
- CLASS:在编译器生成的字节码文件中有效,但在运行时会被处理类文件的 JVM 丢弃。
- RUNTIME:在运行时有效。这也是注解生命周期中最常用的一种策略,它允许程序通过反射的方式访问注解,并根据注解的定义执行相应的代码。
- 注解的生命周期在定义该注解时就已经确定了。
// Override 元注解的定义代码。指明了生命周期、适用范围。
package java.lang;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface Override {
}
- 可以自定义注解。
自定义注解 Demo
需求:定义一个字段注解,该注解标注的字段必须被序列化,没有被标注的字段不被序列化。
@Retention(RetentionPolicy.RUNTIME)// 生命周期是 RUNTIME,也就是运行时有效
@Target(ElementType.FIELD) // 注解装饰的目标是 FIELD,也就是针对字段的。
public @interface JsonField { // 指定创建该注解时必须用 @interface 关键字。
public String value() default "";
}
关于 public String value() default "";
的进一步说明:
- JsonField 注解有一个参数,参数名为 value,参数类型为 String,参数默认值为一个空字符
- value 允许注解的使用者提供一个无需指定名字的参数。举个例子,我们可以在一个字段上使用
@JsonField(value = "anthony")
,也可以把 value = 省略,变成@JsonField("value")
。
default
它允许我们在一个字段上直接使用 @JsonField,而无需指定参数的名和值。”我回答说。
- value 允许注解的使用者提供一个无需指定名字的参数。举个例子,我们可以在一个字段上使用
测试:在 Writer 对象中,name 和 bookName 必须被序列化,age 字段不被序列化。
public class Writer {
private int age;
@JsonField("writerName")
private String name;
@JsonField
private String bookName;
public Writer(int age, String name, String bookName) {
this.age = age;
this.name = name;
this.bookName = bookName;
}
// 省略 getter、setter、toString
}
public class JsonSerializer {
public static String serialize(Object object) throws IllegalAccessException {
Class<?> objectClass = object.getClass();
Map<String, String> jsonElements = new HashMap<>();
for (Field field : objectClass.getDeclaredFields()) {
field.setAccessible(true);
if (field.isAnnotationPresent(JsonField.class)) {
jsonElements.put(getSerializedKey(field), (String) field.get(object));
}
}
return toJsonString(jsonElements);
}
private static String getSerializedKey(Field field) {
String annotationValue = field.getAnnotation(JsonField.class).value();
if (annotationValue.isEmpty()) {
return field.getName();
} else {
return annotationValue;
}
}
private static String toJsonString(Map<String, String> jsonMap) {
String elementsString = jsonMap.entrySet()
.stream()
.map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"")
.collect(Collectors.joining(","));
return "{" + elementsString + "}";
}
}
public class JsonFieldTest {
public static void main(String[] args) throws IllegalAccessException {
Writer cmower = new Writer(18,"anthony,"A Jouney,Through Time");
System.out.println(JsonSerializer.serialize(cmower));
}
}
1)serialize() 方法是用来序列化对象的,它接收一个 Object 类型的参数。objectClass.getDeclaredFields() 通过反射的方式获取对象声明的所有字段,然后进行 for 循环遍历。在 for 循环中,先通过 field.setAccessible(true) 将反射对象的可访问性设置为 true,供序列化使用(如果没有这个步骤的话,private 字段是无法获取的,会抛出 IllegalAccessException 异常);再通过 isAnnotationPresent() 判断字段是否装饰了 JsonField 注解,如果是的话,调用 getSerializedKey() 方法,以及获取该对象上由此字段表示的值,并放入 jsonElements 中。
2)getSerializedKey() 方法用来获取字段上注解的值,如果注解的值是空的,则返回字段名。
3)toJsonString() 方法借助 Stream 流的方式返回格式化后的 JSON 字符串。Stream 流后面详细叙述。
输出结果:
{"bookName":"A Jouney,Through Time","writerName":"anthony"}
- age 字段没有装饰 @JsonField 注解,所以没有序列化。
- name 字段装饰了 @JsonField 注解,并且显示指定了字符串“writerName”,所以序列化后变成了 writerName。
- bookName 字段装饰了 @JsonField 注解,但没有显式指定值,所以序列化后仍然是 bookName。
Reflection 反射
-
什么是反射
- Class 对象是一种特殊的 Java 对象,代表了程序中的类和接口。Java 中的每个类型(包括类、接口、数组以及基础类型)在 JVM 中都有一个唯一的 Class 对象与之对应。这个 Class 对象被创建的时机是在 JVM 加载类时,由 JVM 自动完成。
- Class 对象中包含了与类相关的很多信息,如类的名称、类的父类、类实现的接口、类的构造方法、类的方法、类的字段等等。这些信息通常被称为元数据(metadata)。
- 通过 Class 对象来获取类的元数据,甚至动态地创建类的实例、调用类的方法、访问类的字段等,就是Java 的反射机制。
-
反射的缺点
破坏封装
:由于反射允许访问私有字段和私有方法,所以可能会破坏封装而导致安全问题。性能开销
:由于反射涉及到间接调用、动态解析,因此无法执行 JVM 优化,再加上反射的写法的确要复杂得多,所以性能要比“正射”差很多,在一些性能敏感的程序中应该避免使用反射。
-
反射的主要应用场景有:
- 开发通用框架:像 Spring,为了保持通用性,通过配置文件来加载不同的对象,调用不同的方法。
- 动态代理:在面向切面编程中,需要拦截特定的方法,就会选择动态代理的方式,而动态代理的底层技术就是反射。
- 注解:注解本身只是起到一个标记符的作用,它需要利用反射机制,根据标记符去执行特定的行为。
类加载的过程
- 装载(loading):类加载器负责将类的 class 文件读入内存,并创建一个 java.lang.Clas s 对象
- 链接(linking):。
- 验证(Verification):确保类的字节码符合 JVM 规范。
- 准备(Preparation):为类的静态变量分配内存,并初始化为默认值。
- 解析(Resolution):将常量池中的符号引用转换为直接引用(地址),如将方法名解析为具体的方法引用。
- 初始化(initialization):静态变量赋值,静态代码块执行
引子
要想知道什么是反射,就需要先了解什么是‘正射’。
一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。(静态方法除外)
File file = new File("a.txt");
boolean exists = file.exists();
那反射就好理解了:只知道这个类的一些基本信息,但不知道要初始化的类到底是什么,也就没法直接使用 new 关键字创建对象了。
例如警察会问一些目击证人,根据这些证人提供的信息,找专家把犯罪嫌疑人的样貌给画出来——这个过程,就可以称之为反射。
// 获取反射类的 Class 对象
Class<?> clazz = Class.forName("java.io.File");
// 通过 Class 对象获取 File 类的构造器对象
// 进而通过构造器对象初始化反射类对象
Constructor<?> constructor = clazz.getConstructor(String.class);
Object fileInstance = constructor.newInstance("example.txt");
// 获取 exists 方法,并调用该方法
Method existsMethod = clazz.getMethod("exists");
boolean exists = (boolean) existsMethod.invoke(fileInstance);
上面两段代码的执行结果,其实是完全一样的。
但是其思路完全不一样,第一段代码在未运行时就已经确定了要运行的类(File);而第二段代码则是在运行时通过字符串值才得知要运行的类(java.io.File)。
所以说什么是反射?
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
反射总共有四步。
- 获取反射类的 Class 对象
- 通过 Class 对象获取反射类的构造器
- 通过构造器实例化反射类
- 通过第一第二步获取的对象,调用方法/获取字段属性
反射相关类
反射的主要类位于 java.lang.reflect
包中,主要包括以下几个关键类:
Class
:代表一个类或接口,包含了类的结构信息(如名称、构造函数、方法、字段等)。通过 Class对象,可以获取类的元数据并操作类的实例。Constructor
:代表类的构造方法,用于创建类的实例。Method
:代表类的方法,可以通过它调用类的实例方法。Field
:代表类的字段,可以获取或修改字段的值。Modifier
:包含方法、字段和类的访问修饰符(如 public、private 等)
从 Class 对象说起
什么是 Class 对象
- 每一个类,不管它最终生成了多少个对象,这些对象只会对应一个 Class 对象,这个 Class 对象是由 Java 虚拟机生成的,由它来获悉整个类的结构信息。
也就是说,java.lang.Class 是所有反射 API 的入口。 - Class 对象是一种特殊的对象,它代表了程序中的类和接口。
- Java 中的每个类型(包括类、接口、数组以及基础类型)在 JVM 中都有一个唯一的 Class 对象与之对应。这个 Class 对象被创建的时机是:在 JVM 加载类时,由 JVM 自动完成。
- Class 对象中包含了与类相关的很多信息,如类的名称、类的父类、类实现的接口、类的构造方法、类的方法、类的字段等等。这些信息通常被称为元数据(metadata)。
第一步:获取 Class 对象的四种方法
- 使用
Class.forName 静态方法
最常用的方法.。在已经知道类的全限定名
时采用。此方法在运行时动态加载类
,适合在类名在编译时未知的情况下使用。依赖注入
就是采用这种方法。
Class<?> clazz = Class.forName("com.example.MyClass");
- 使用
ClassName.class
常用的方法。如果已有一个类的字面量(类本身),即已知类名,可以用类名调用 class 来获取 Class 对象。适用于静态加载类
。
Class cls = String.class;
- 使用
引用.getClass 实例方法
如果已经有实例,可以通过现有对象来获取其 Class 对象。
MyClass myObject = new MyClass();
Class<?> clazz = myObject.getClass();
- 基本类型和数组的 TYPE 属性
对于基本数据类型,可以通过其包装类的 TYPE 属性来获取 Class 对象。数组类型的 Class 对象也可以通过 ClassName[].class 形式获取。
Class<?> intClass = int.class; // 基本类型
Class<?> integerClass = Integer.TYPE; // 基本类型包装类的 TYPE 属性
Class<?> intArrayClass = int[].class; // 数组类型
Class<?> stringArrayClass = String[].class; // 数组类型
- 通过类加载器
可以通过类加载器来加载类并获取 Class 对象。这适用于一些高级场景,如自定义类加载器。
ClassLoader classLoader = MyClass.class.getClassLoader();
Class<?> clazz = classLoader.loadClass("com.example.MyClass");
第二步:获取构造器对象的四种方法
// 返回反射类的特定 public 构造方法,可以传递参数,参数为构造方法参数对应 Class 对象;缺省的时候返回默认构造方法。
public Constructor<T> getConstructor(Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
// 返回类的所有 public 构造方法。
public Constructor<?>[] getConstructors() throws SecurityException
// 以下两个方法能获取的构造方法,不受访问性影响--即可以6获取 private 的构造器.Delared 即所有声明的都要获取
// 返回指定参数类型的构造器,所传参数不可缺省。
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
// 返回类的所有构造方法。
public Constructor<?>[] getDeclaredConstructors() throws SecurityException
// 获取 File 类的所有构造方法
Class c2 = Class.forName("java.io.File");
Constructor constructor = c2.getConstructor();
Constructor[] constructors1 = String.class.getDeclaredConstructors();
for (Constructor c : constructors1) {
System.out.println(c);
}
第三步:实例化反射类的两种方法
Class.newInstance() // 此方法已经过时.
Constructor.newInstance() // 推荐此方法.因为此方法可以调用任意构造函数实例化反射类
Class c2 = Class.forName("java.io.File");
File file = (File) c1.newInstance();
Constructor constructor = c2.getConstructor();
Object object = constructor.newInstance();
第四步:获取方法并调用
// 获取指定名称和参数类型的公共方法。
public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
// 获取指定名称和参数类型的声明的方法,包括私有方法。
public Method getDeclaredMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
// 返回方法调用的结果。如果方法的返回类型是 void,则返回 null
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
第四步:获取字段
// 先获得字段的 Field 对象,再通过 Field 对象 get 字段的值
// 同时,访问私有字段时,也要设置访问权限.
One More Thing
// Declared 方法可以获取私有方法,但不意味着可以直接调用,还需要设置访问权限
public void setAccessible(boolean flag) throws SecurityException
// 以下代码中,演示了如何通过 setAccessible 方法访问私有的方法、字段
try {
Person person = new Person();
Class<?> personClass = person.getClass(); // 获取 Person 类的 Class 对象
Field nameField = personClass.getDeclaredField("name"); // 获取私有字段 name
nameField.setAccessible(true); // 设置私有字段可访问
// 获取并修改私有字段的值
String name = (String) nameField.get(person);
System.out.println("Original Name: " + name);
nameField.set(person, "Jane Doe");
System.out.println("Updated Name: " + nameField.get(person));
// 获取私有方法 printDetails
Method printDetailsMethod = personClass.getDeclaredMethod("printDetails");
printDetailsMethod.setAccessible(true); // 设置私有方法可访问
printDetailsMethod.invoke(person); // 调用私有方法
} catch (Exception e) {
e.printStackTrace();
}
Parent Delegation Model 双亲委派机制
在 Java 中,类加载器有一个层次结构,通常由三种主要的类加载器组成:
启动类加载器(Bootstrap ClassLoader):负责加载 Java 核心库(例如 rt.jar),是由 JVM 自身实现的类加载器。
扩展类加载器(Extension ClassLoader):负责加载扩展库(例如 lib/ext 目录下的 JAR 包)。
应用程序类加载器(Application ClassLoader):负责加载应用程序类路径(classpath)下的类和库。
双亲委派机制规定,当一个类加载器需要加载一个类时,它首先会把请求委派给它的父类加载器去尝试加载。如果父类加载器无法加载该类,子类加载器才会尝试自己去加载。
工作原理
双亲委派机制的工作过程可以概括为以下几步:
类加载请求:当一个类加载器(如应用程序类加载器)接收到加载类的请求时,它不会直接加载这个类,而是先委派给它的父类加载器(如扩展类加载器)。
父类加载器加载请求:父类加载器接到请求后,也会按同样的机制,继续向上委派,直到请求到达启动类加载器。
启动类加载器尝试加载:启动类加载器尝试加载类,如果找到并成功加载类,则返回加载的类给子类加载器。
逐级返回:如果启动类加载器没有找到该类,则返回给下一级类加载器(扩展类加载器),扩展类加载器继续尝试,依此类推。
子类加载器尝试加载:如果所有父类加载器都没有找到该类,最终由原始请求的类加载器自己尝试加载类。
优点
避免类重复加载:通过双亲委派机制,类只会被加载一次,这避免了重复加载同一个类的问题。
确保核心类的安全性:核心 Java 类库(如 java.lang.*)由启动类加载器加载,避免了自定义类加载器恶意篡改核心类的风险。
层次结构清晰:不同类加载器有明确的职责分工,层次结构清晰,有助于维护和理解类加载过程。
public class ClassLoaderDemo {
public static void main(String[] args) {
// 获取应用程序类加载器
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("Application ClassLoader: " + appClassLoader);
// 获取扩展类加载器
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println("Extension ClassLoader: " + extClassLoader);
// 获取启动类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("Bootstrap ClassLoader: " + bootstrapClassLoader);
try {
// 尝试使用应用程序类加载器加载一个类
Class<?> clazz = appClassLoader.loadClass("java.lang.String");
System.out.println("Class loaded by: " + clazz.getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Application ClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
Extension ClassLoader: sun.misc.Launcher$ExtClassLoader@1540e19d
Bootstrap ClassLoader: null
Class loaded by: null
总结
双亲委派机制通过委派加载请求给父类加载器,实现了类加载器的层次结构,确保了类的唯一性和核心类库的安全性。这一机制是 Java 类加载系统的重要组成部分,有助于维护类加载过程的清晰和稳定。