B站视频链接:https://www.bilibili.com/video/BV1p4411P7V3/?p=
1、反射机制概述
1.1 静态语言&&动态语言
- 动态语言:运行时代码可以根据某些条件改变自身结构,例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除等。动态语言在使用变量前不需要声明数据类型,是在程序运行时确定变量类型的,主要动态语言有:Object-C、C#、JavaScript、PHP、Python等;
- 静态语言:运行时结构不可变的语言就是静态语言,静态语言求在使用变量前必须声明数据类型,在编译时变量的数据类型才被确定,常见的静态语言有Java、C、C++等。
Java不是动态语言,但可以称为“准动态语言”,即Java有一定的动态性,Java可以利用反射机制获得类似动态语言的特性。
1.2 反射的概述
反射是Java被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性和方法。
加载完类之后,在方法区就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息,我们可以通过这个Class对象看到类的结构,这个对象就像一面镜子,透过这个镜子看到类的结构,因此称之为“反射”。
1.3反射提供的功能
- 在运行时判断任意一个对象所属的类;
- 在运行时构造任意一个类的对象;
- 在运行时判断任意一个类所具有的成员变量和方法;
- 在运行时获取泛型信息;
- 在运行时调用任意一个对象的成员变量和方法;
- 在运行时处理注解;
-
1.4 反射的优缺点
优点:
可以实现动态创建对象和编译,体现出很大的灵活性;
缺点:
-
1.5 反射常用API
(1) 获取反射中的Class对象
使用 Class.forName 静态方法
Class clz = Class.forName("java.lang.String");
使用 .class 方法
Class clz = String.class;
使用类对象的 getClass() 方法
String str = new String("Hello");
Class clz = str.getClass();
(2)通过反射创建类的对象
通过 Class 对象的 newInstance() 方法
Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance();
注意这种方式只能调用类的默认构造函数。
通过 Constructor 对象的 newInstance() 方法
Class clz = Apple.class;
Constructor constructor = clz.getConstructor(String.class, int.class);
Apple apple = (Apple)constructor.newInstance("红富士", 15);
通过 Constructor 对象创建类对象可以选择特定构造方法。
(3)通过反射获取类的构造器、成员属性和方法
获取构造器
Class clz = Apple.class;
Constructor constructor = clz.getConstructor(String.class, int.class);
在getConstructor方法里传入构造器的入参,指定获取哪种构造器。
获取成员属性
Class clz = Apple.class;
Field[] fields = clz.getFields();
for (Field field : fields) {
System.out.println(field.getName());
}
getFields方法获取所有非private修饰的属性,获取private修饰的属性用getDeclaredFields()。
获取方法
Method setPriceMethod = clz.getMethod("setPrice", int.class);
getMethod方法,入参第一个为方法名称,后面的参数为方法的入参的class对象。
(4) 通过反射调用对象的方法
setPriceMethod.invoke(appleObj, 14);
利用 invoke 方法调用方法,其中setPriceMethod是通过反射获取到的类的方法对象。
2、理解Class类并获取Class实例
2.1 Class类型的对象
一个类对应唯一一个Class对象,通过这个Class对象可以获取这个类的完整信息,包括构造函数、属性、方法等。
关于Class对象:
- Class本身也是一个类;
- Class对象只能由系统建立对象;
- 一个加载的类在JVM中只会有一个Class实例;
- 一个Class对象对应的是一个加载到JVM中的一个.class文件;
- 每个类的实例都会记得自己是由哪个Class实例所生成的;
- 通过Class实例可以完整地得到一个类中所有被加载的结构;
- Class类是反射的根源,针对任何动态加载、运行的类,唯有先获得相应的Class对象才能进行下一步。
哪些类型可以有Class对象?
- class:外部类、成员内部类、静态内部类、局部内部类、匿名内部类;
- interface:接口;
- []:数组;
- enum:枚举;
- annotation:注解@interface;
- primitive type:基本数据类型
-
2.2 如何获得class对象
使用 Class.forName 静态方法
Class clz = Class.forName("java.lang.String");
使用 .class
Class clz = String.class;
使用实例的 getClass() 方法
String str = new String("Hello"); Class clz = str.getClass();
获取类的父类
Class clz = Class.forName("com.huawei.ChildClass"); Class clz1 = clz.getSuperClass();
3、类的加载与ClassLoader
3.1 Java内存分析
在JDK1.8
之后就没有了方法区的概念,取而代之的是常量池和静态域。栈:存放基本类型的变量、对象的引用和局部变量;
- 堆:存放所有new出来的对象;
- 常量池:存放字符串常量和基本类型常量(public static final);
- 静态域:存放静态成员(static定义的);
3.2 类的加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化:
- 类的加载(Load):将类的class文件读入内存,并为之创建一个java.lang.Class对象,此过程由类加载器完成;
- 类的链接(Link):将类的二进制数据合并到JRE中;
- 验证:确保加载的类的信息符合JVM规范,没有安全方面的问题;
- 准备:正式为类的静态变量分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配;
- 接续:虚拟机常亮池内的符号引用(常量名)替换为直接引用(地址)的过程。
- 类的初始化(Initialize):JVM负责对类进行初始化。
- 执行类构造器
()方法的过程。类构造器 ()方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。 - 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化;
- 虚拟机会保证一个类构造器
()方法在多线程环境中被正确加锁和同步。
- 执行类构造器
3.3 什么时候会发生类的初始化?
- 类的主动引用(一定会发生类的初始化)
- 当虚拟机启动,先初始化main方法所在的类;
- new一个类的对象;
- 调用类的静态成员(除了final常量)和静态方法;
- 使用反射;
- 当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类;
- 类的被动引用(不会发生类的初始化)
即通过反射API在运行时创建类的对象,注意处理调用API可能会抛出异常。
4.1 调用无参构造器创建对象
前提是类必须有无参构造器,或者默认构造器。
public class UserTest {
@Test
public void test() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class c1 = Class.forName("com.Jerry.reflection.User");
// 调用类的无参构造函数
User user = (User) c1.newInstance();
System.out.println(user);
}
}
4.2 调用有参构造器创建对象
public class UserTest {
@Test
public void test() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class c1 = Class.forName("com.Jerry.reflection.User");
// 通过有参构造函数创建对象
Constructor constructor = c1.getDeclaredConstructor(String.class, int.class, long.class);
User user1 = (User) constructor.newInstance("Jerry", 27, 511942);
System.out.println(user1);
}
}
5、获取运行时类的完整结构
Bean:
@Data
public class User {
private String name;
private int age;
private long id;
public User(String name, int age, long id)
{
this.name = name;
this.age = age;
this.id = id;
}
private User() {}
}
5.1 获取类名
Class c1 = Class.forName("com.Jerry.reflection.User");
// 获取包名 + 类名
System.out.println(c1.getName());
// 获取类名
System.out.println(c1.getSimpleName());
5.2 获取属性
// getFields()只能获得public属性
Field[] fields = c1.getFields();
// getDeclaredFields()可以获得所有属性
fields = c1.getDeclaredFields();
// getDeclaredField("属性名")可以获得指定属性
Field name = c1.getDeclaredField("name");
5.3 获取方法
// getMethods()获得本类及其父类的全部public方法
Method[] methods = c1.getMethods();
// getDeclaredMethods()获得本类的所有方法
Method[] methods = c1.getDeclaredMethods();
// getMethod("方法名")获得指定方法
Method getAge = c1.getMethod("getAge");
// 需要传入参数是因为方法可能重载
Method setName = c1.getMethod("setName", String.class);
5.4 获取构造器
// getConstructors获取public构造方法
Constructor[] constructors = c1.getConstructors();
// getDeclaredConstructor获取所有构造方法
Constructor[] constructors = c1.getDeclaredConstructors();
// 获取指定构造方法,传入入参的class对象
Constructor constructor = c1.getDeclaredConstructor(String.class, int.class, int.class);
6、调用运行时类的指定结构
6.1 调用属性
关键API
:set
方法,set
方法的第一个入参是实例对象,第二个入参是赋给属性的值。
public class UserTest {
@Test
public void test() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
Class c1 = Class.forName("com.Jerry.reflection.User");
User user = (User) c1.newInstance();
// 通过反射API获得指定属性
Field name = c1.getDeclaredField("name");
// 通过set方法为这个属性赋值,set方法的第一个入参是实例对象,第二个入参是赋给属性的值
name.set(user, "Jerry");
}
}
6.2 调用方法
关键API
:invoke
方法,invoke
方法第一个入参是实例对象,后面的入参是实例对象调用这个方法需要传入的若干参数。
public class UserTest {
@Test
public void test() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class c1 = Class.forName("com.Jerry.reflection.User");
User user = (User) c1.newInstance();
// 通过反射获取一个方法
Method setName = c1.getDeclaredMethod("setName", String.class);
// 通过invoke API调用方法,invoke方法第一个入参是实例对象,后面的入参是实例对象调用这个方法需要传入的若干参数
setName.invoke(user, "Jerry");
}
}
7、反射操作注解
7.1 什么是ORM?
全称是Object Relationship Mapping,即对象关系映射,该关系映射将Java中的类的实例与数据库中的表结构相对应。
如上图所示,将两个Student类的实例通过ORM映射到数据库中表的两行记录。
7.2 通过反射获得类的注解
Bean:
@Data
@TableJerry("db_user")
public class User {
@FieldJerry(columnName = "db_name", type = "varchar", length = 3)
private String name;
@FieldJerry(columnName = "db_age", type = "int", length = 10)
private int age;
@FieldJerry(columnName = "db_id", type = "int", length = 10)
private long id;
public User(String name, int age, long id)
{
this.name = name;
this.age = age;
this.id = id;
}
public User() {}
}
// 类名的注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface TableJerry{
String value();
}
// 属性的注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@interface FieldJerry{
String columnName();
String type();
int length();
}
Test:
public class UserTest {
@Test
public void test() throws ClassNotFoundException, NoSuchFieldException {
Class c1 = Class.forName("com.Jerry.reflection.User");
// 通过反射获得注解
Annotation[] annotations = c1.getAnnotations();
for (Annotation annotation : annotations)
{
System.out.println(annotation);
}
// 获得注解的value值
Annotation tableJerry = c1.getAnnotation(TableJerry.class);
String value = ((TableJerry) tableJerry).value();
System.out.println(value);
// 获得类指定的注解
Field field = c1.getDeclaredField("name");
FieldJerry annotation = field.getAnnotation(FieldJerry.class);
System.out.println(annotation.columnName());
System.out.println(annotation.type());
System.out.println(annotation.length());
}
}