本文介绍Java中的反射机制。
1. 反射机制及其作用 通过Java语言中的反射机制可以操作(读取和修改)字节码文件(.class文件)。那么操作字节码文件有什么用呢?暂时保留一个疑问,见后续的讲解。
反射机制用到的相关类在java.lang.reflect
包下,主要有以下类:
类名
描述
java.lang.Class
代表字节码文件,即代表一个Java类
java.lang.reflect.Method
代表字节码文件中的普通方法 字节码,即代表类中的方法。
java.lang.reflect.Constructor
代表字节码文件中的构造方法 字节码,即代表类中的构造方法。
java.lang.reflect.Field
代表字节码文件中的属性 字节码,即代表类中的成员(静态变量+成员变量)。
2. 获取类(java.lang.Class) 既然要操作字节码,肯定首先要通过字节码文件获取其中的Java类(一个class文件就代表一个类),即利用java.lang.Class类中的相关方法来获取指定class文件代表的类
。获取类有三种方式:
Class.forName(String className)
static Class<T> forName(String className)
该方法是一个静态方法,参数是一个字符串,字符串是一个完整类名,必须带有包名。
Returns the Class
object associated with the class or interface with the given string name.
即获取指定的类,如 Class c1 = Class.forName("java.lang.String")
,c1类对象表示String类编译后的String.class
字节码文件,或者说c1代表String类。注意会有编译时异常,需要处理。(因为需要获取该class文件表示的类,所以需要将字节码文件加载到方法区内存中,即类加载,可以写一个静态代码块进行测试一下,此时如果只想要静态代码块执行而不需要创建对象,可以用forName()方法。 )
对象.getClass()
Object类中有一个方法:Class<?> getClass()
,Returns the runtime class of this Object,即获取该对象所代表的类(注意多态时的区别,此时是运行时所代表的类)。所以每个类对象也可以调用此方法获取字节码文件。(首先需要有对象,所以JVM肯定要将class文件加载到方法区内存中)
对象.class
Java语言中任何一种类型,包括基本数据类型,都有.class属性,可以通过属性值来获取所代表的类。
类加载实际上就是将class文件加载到JVM中,在方法区内存中只会加载一份 ,所以上述三种方法获取到的类的类对象的内存地址是一样的(注意,这是最基本的情况,但是实际上三者还是有一定的区别的 )。如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class ReflectTest01 { public static void main (String[] args) { Class strs = null ; try { strs = Class.forName("java.lang.String" ); System.out.println(strs); } catch (ClassNotFoundException e) { e.printStackTrace(); } String s1 = "asdf" ; Class strss = s1.getClass(); System.out.println(strss); System.out.println(strs == strss); Class strsss = String.class; System.out.println(strsss); System.out.println(strss == strsss); } }
java.lang.Class
类中常用的方法如下所示:
方法名
描述
static Class<?> forName(String className)
获取指定类名关联的字节码文件
Field[] getFields()
获取该Class对象关联字节码文件类中的public修饰的成员变量
Field[] getDeclaredFields()
获取该类所有的成员变量
Field getDeclaredField(String name)
获取该类中指定name的变量,以Field对象的形式返回
int getModifiers()
获取修饰符列表(编码后的,比如默认defaulted是0,public是1,如果有多个,比如public static final等等,就是三个编码之和,具体的编码见Field中的表格)
String getName()
获取该类的全称,如java.lang.String
String getSimpleName()
获取该类的简称,如String
Class<?>[] getInterfaces()
获取该类实现的所有接口
Class<? super T> getSuperclass()
获取该类的父类
此时我们可以回顾一下,到目前为止,我们所讲到的内容,除了静态变量以及常量等等,其余的操作对象都是在堆内存中,现在我们可以操作方法区内存中对象了,即字节码文件对象了。
而操作字节码文件,首先要操作的就是操作构造方法,创建对象。
3. 反射用例1:通过获取类实例化对象 Class类有一个方法T newInstance()
,Creates a new instance of the class represented by this Class object,创建该类的实例,返回值是泛型,不设置就是Object类型。(这个方法已被其他方法替代了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class ReflectTest02 { public static void main (String[] args) { try { Class aClass = Class.forName("bean.User" ); Object obj = aClass.newInstance(); System.out.println(obj); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } } }
1 2 3 4 5 6 7 8 9 10 public class User { public User () { System.out.println("无参构造方法被执行了" ); } public User (String s) { System.out.println("有参构造方法被执行了" ); } }
可通过设置无参构造方法来进行测试,newInstance()方法调用的是该类的无参构造方法 ,所以在提供有参构造方法后也要提供无参构造方法,否则会报错:实例化异常,找不到类方法。
此时可能会有疑问,虽然可以实例化对象,但是并没有User u = new User();
这种方法更加方便,其实这里实例化对象并没有根本体现出反射的作用。
User u = new User()
这种方法实例化对象比较固定,因为这是Java代码,编译后就不能再改变了。比如后续的JDBC,加入想要更换数据库(mysql、redis等等),或者账号密码等等,此时就需要修改源代码,重新编译,重新部署,这样比较麻烦。
此时,可以通过读取配置文件,将数据库驱动类的全称写在配置文件中,这时就可以通过上述的字符串作为参数传入到Class.forName()
中,然后实例化对象。后续如果想要修改,直接修改配置文件中的字符串即可,不需要重新编译Java文件,实现了动态修改以及创建对象,即通过字符串来创建对象。
案例如下所示:
classInfo.properties文件
java文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class ReflectTest03 { public static void main (String[] args) { try { FileReader fr = new FileReader("src/properties/classInfo.properties" ); Properties pro = new Properties(); pro.load(fr); fr.close(); String className = pro.getProperty("className" ); Class aClass = Class.forName(className); Object o = aClass.newInstance(); System.out.println(o); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } } }
另外,除了采用IO流相关类,也可以采用java.util.ResourceBundle
这个资源绑定器工具类,
只能绑定xxx.properties文件 ,并且这个文件必须在类路径(src目录)下,文件扩展名必须是properties。
在写文件路径的时候,后面的扩展名不能写。
例子如下所示:
1 2 3 4 5 6 7 8 9 10 public class ReflectTest04 { public static void main (String[] args) { ResourceBundle bundle = ResourceBundle.getBundle("properties/classInfo" ); String className = bundle.getString("className" ); System.out.println(className); } }
4. 扩展:类加载器 反射机制涉及到了字节码文件,而字节码文件首先通过类加载器加载到JVM的方法区内存中。这里的类加载器指的是JDK中自带的类加载器,专门负责加载类的命令/工具ClassLoader 。JDK中自带了3个类加载器:
启动类加载器(父加载器)
扩展类加载器(母加载器)
应用类加载器
加载字节码文件的过程如下:
首先通过“启动类加载器”加载,这个类加载器专门加载JAVA安装目录
下的jre/lib/rt.jar
包中的class文件,查看这个文件可以发现,这些都是Java最常用的类,如java.lang.String等等,这些是JDK最核心的类库。
如果通过“启动类加载器”加载不到的时候 ,会通过“扩展类加载器”加载。注意,“扩展类加载器”专门加载jre/lib/ext/*.jar
包中的class文件。
最后,如果扩展类加载器 也没加载到,会通过“应用类加载器”加载。“应用类加载器”专门加载:classpath中jar包中的class文件。
Java为了保证类加载的安全,使用了双亲委派机制:优先从启动类加载器(父)中加载,无法加载到再从扩展类加载器(母)中加载,即双亲委派机制。最后才会考虑从应用类加载器中加载。也就是说,优先选择官方提供的类,如果自己也写了官方重名类,这不会被加载。
5. 反射属性Field(java.lang.reflect.Field) 反射属性指的是通过获取的Class类来获取其中的属性,反射属性需要用到java.util.reflect.Field
类。注意,权限修饰符如public、private在Field类中用数字代替,后续可以采用java.util.reflect.Modifier
进行转换成字符串。
提供学生类:
1 2 3 4 5 6 7 8 public class Student { public int no; private String name; protected int age; boolean sex; }
java.util.reflect.Field
类中常用的方法如下所示:
方法名
描述
int getModifiers()
获得该属性的修饰符列表编码
Class<?> getType()
获得该属性的类型,如int、String等等,Class类型
String getName()
获得该属性的名字
void set(Object obj, Object value)
给指定的obj对象的当前Field对象属性赋值value
Object get(Object obj)
获取指定obj对象的当前Field对象属性的值
java.util.reflect.Modifier
类中常用的方法如下所示:
方法名
描述
static String toString(int mod)
将修饰符编码转换成字符串
部分修饰符编码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static final int PUBLIC = 1 ;public static final int PRIVATE = 2 ;public static final int PROTECTED = 4 ;public static final int STATIC = 8 ;public static final int FINAL = 16 ;public static final int SYNCHRONIZED = 32 ;public static final int VOLATILE = 64 ;public static final int TRANSIENT = 128 ;public static final int NATIVE = 256 ;public static final int INTERFACE = 512 ;public static final int ABSTRACT = 1024 ;public static final int STRICT = 2048 ;static final int BRIDGE = 64 ;static final int VARARGS = 128 ;static final int SYNTHETIC = 4096 ;static final int ANNOTATION = 8192 ;static final int ENUM = 16384 ;static final int MANDATED = 32768 ;private static final int CLASS_MODIFIERS = 3103 ;private static final int INTERFACE_MODIFIERS = 3087 ;private static final int CONSTRUCTOR_MODIFIERS = 7 ;private static final int METHOD_MODIFIERS = 3391 ;private static final int FIELD_MODIFIERS = 223 ;private static final int PARAMETER_MODIFIERS = 16 ;static final int ACCESS_MODIFIERS = 7 ;
5.1 反编译属性 简单地将类的属性反编译一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class ReflectTest06 { public static void main (String[] args) { StringBuffer sb = new StringBuffer(); try { Class studentClass = Class.forName("bean.Student" ); String modifiers = Modifier.toString(studentClass.getModifiers()); sb.append(modifiers + " class " + studentClass.getSimpleName() + "{\n" ); Field[] fields = studentClass.getDeclaredFields(); for (Field f: fields) { String fModifiers = Modifier.toString(f.getModifiers()); String fClass = f.getType().getSimpleName(); String fName = f.getName(); sb.append("\t" + fModifiers + " " + fClass + " " + fName + ";\n" ); } } catch (ClassNotFoundException e) { e.printStackTrace(); } sb.append("}" ); System.out.println(sb); } }
结果如下所示:
简单反编译一下String类(没有获取常量值):
5.2 反射访问对象属性 上面的反编译属性用的不多,重点是通过反射机制访问Java对象的属性,如赋值、获取值等等。因为和JDBC类似,有时候我们需要动态的创建不同类型的对象,设置不同的属性以及值,所以可以采用反射机制。虽然这在代码复杂度上增加了,但是可以达到动态的效果。如下所示:
注意访问控制权限,对于私有的,本质上是不能通过下面直接访问的,但是可以属性对象.setAccessible(true)
打破封装,这也是反射机制的缺点:打破封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class ReflectTest07 { public static void main (String[] args) { try { Class classStudent = Class.forName("bean.Student" ); Object obj = classStudent.newInstance(); Field noField = classStudent.getDeclaredField("no" ); noField.set(obj, 10 ); Object o = noField.get(obj); System.out.println(o); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } } }
6. 反射方法Method(java.lang.reflect.Method) 方法中有修饰符列表、返回值类型、方法名以及参数列表,而参数列表有数据类型和参数名,当然还有抛出的异常类型(这里先不考虑)。java.lang.reflect.Method
中常用的方法如下所示:
方法名
描述
int getModifiers()
获取方法的修饰符列表
String getName()
获取方法名
Class<?>[] getParameterTypes()
获取方法的参数列表的数据类型
Class<?> getReturnType()
获取方法的返回值类型
这里扩展一个知识点,方法中的可变长度参数。
6.1 可变长度参数 有时候,我们调用函数传入的参数可能不确定,这时候就需要采用变长参数。
语法格式如下所示:
1 修饰符列表 返回值类型 函数名(固定形参, 数据类型... 形参名){}
变长参数有以下几个要求:
可变长度参数要求的参数个数是:0~N个。
可变长度参数在参数列表中必须在最后一个位置上,(因为如果不在最后一个位置就不无法对应其余的不可变参数的形参和实参)而且可变长度参数只有一个。
可变长度参数可以当做一个数组来看待。
案例如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class ReflectTest08 { public static void main (String[] args) { m1(); m1(1 ); m1(2 , 3 ); System.out.println("------------------" ); m2(1 ); m2(1 , 2 ); m2(1 , 2 , 3.14 ); m2(1 , 2 , 3.14 , "asdfasdfzxcv" ); m2(1 , 2 , 3.14 , "asdfasdfzxcv" , new Student()); System.out.println("------------------" ); m3(1 , "xcv" ); m3(1 , "xcv" , 2312 ); m3(1 , "xcv" , 2312 , 3.14 ); } public static void m1 (int ... args) { System.out.println("m1方法执行了" ); } public static void m2 (Object... args) { System.out.println("m2方法执行了" ); for (Object obj: args) { System.out.println("[" + obj + "]" ); } } public static void m3 (int i, Object... args) { System.out.println("m3方法执行了" ); } }
6.2 反编译方法 和反编译属性一样,首先反编译一下方法(无方法体),注意,方法参数列表中最重要的是数据类型,而不是形参名。
用到的数据类如下所示:
1 2 3 4 5 6 7 8 9 10 11 public class Login { public void logins (String name, String password) { if ("admin" .equals(name) && "123" .equals(password)){ System.out.println("登录成功" ); } } public void logouts () { System.out.println("退出登录" ); } }
反编译如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class ReflectTest09 { public static void main (String[] args) { StringBuffer sb = new StringBuffer(); try { Class loginClass = Class.forName("bean.Login" ); String classModifiers = Modifier.toString(loginClass.getModifiers()); String simpleName = loginClass.getSimpleName(); sb.append(classModifiers + " class " + simpleName + " {\n" ); Method[] declaredMethods = loginClass.getDeclaredMethods(); for (Method method: declaredMethods) { String modifier = Modifier.toString(method.getModifiers()); String returnType = method.getReturnType().getSimpleName(); String name = method.getName(); Class[] parameterTypes = method.getParameterTypes(); sb.append("\t" + modifier + " " + returnType + " " + name + "(" ); for (Class parameterType: parameterTypes) { sb.append(parameterType.getSimpleName() + "," ); } if ("," .equals(sb.charAt(sb.length() - 1 ) + "" )){ sb.deleteCharAt(sb.length() - 1 ); } sb.append("){}\n" ); } } catch (ClassNotFoundException e) { e.printStackTrace(); } sb.append("}" ); System.out.println(sb); } }
结果如下所示:
6.3 反射访问对象方法(重点) 和反射访问对象属性类似,在动态创建对象之后,既要访问属性,也要调用方法,那么如何通过反射来访问对象的方法呢?
首先需要注意,Java中的方法主要通过下面两点来确定一个方法:
所以,需要指定方法,然后指定对象传入实参。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class ReflectTest10 { public static void main (String[] args) { try { Class loginClass = Class.forName("bean.Login" ); Object obj = loginClass.newInstance(); Method declaredMethod = loginClass.getDeclaredMethod("logins" , String.class, String.class); Object result = declaredMethod.invoke(obj, "admin" , "123" ); System.out.println(result); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } }
可以看到上述程序中需要的参数,均可以以配置文件的形式写出来,注意,String.class
可以直接用Class.forName("java.lang.String")
来代替,即参数均是字符串,达到了动态的效果。
7. 反射构造方法Constructor(java.lang.reflect.Constructor) 构造方法其实和普通方法类似,只是没有返回值类型,方法名和类名一致。java.lang.reflect.Constructor
中常用的方法和上面的类似,查看帮助文档即可,这里不再赘述。
用到的数据类如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class Vip { public int no; protected String name; double height; private double weight; public Vip () { } public Vip (int no) { this .no = no; } public Vip (int no, String name) { this .no = no; this .name = name; } public Vip (int no, String name, double height) { this .no = no; this .name = name; this .height = height; } public Vip (int no, String name, double height, double weight) { this .no = no; this .name = name; this .height = height; this .weight = weight; } @Override public String toString () { return "Vip{" + "no=" + no + ", name='" + name + '\'' + ", height=" + height + ", weight=" + weight + '}' ; } }
7.1 反编译构造方法 代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class ReflectTest11 { public static void main (String[] args) { StringBuffer sb = new StringBuffer(); try { Class vipClass = Class.forName("bean.Vip" ); String modifier = Modifier.toString(vipClass.getModifiers()); sb.append(modifier + " class " + vipClass.getSimpleName() + "{\n" ); Constructor[] declaredConstructors = vipClass.getDeclaredConstructors(); for (Constructor constructor: declaredConstructors) { sb.append("\t" ); String modifierCon = Modifier.toString(constructor.getModifiers()); String name = vipClass.getSimpleName(); Class[] parameterTypes = constructor.getParameterTypes(); sb.append(modifierCon + " " + name + "(" ); for (Class parameter: parameterTypes) { sb.append(parameter.getSimpleName() + "," ); } if ("," .equals(sb.charAt(sb.length() - 1 ) + "" )){ sb.deleteCharAt(sb.length() - 1 ); } sb.append("){}\n" ); } sb.append("}" ); } catch (ClassNotFoundException e) { e.printStackTrace(); } System.out.println(sb); } }
7.2 反射访问构造方法 通过反射机制调用构造方法,和上述访问对象方法类似,只不过构造方法不需要通过对象访问,直接通过类访问即可,然后采用构造方法对象.newInstance()
来创建对象即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class ReflectTest12 { public static void main (String[] args) { try { Class vipClass = Class.forName("bean.Vip" ); Constructor declaredConstructor = vipClass.getDeclaredConstructor(); Object obj = declaredConstructor.newInstance(); System.out.println(obj); declaredConstructor = vipClass.getDeclaredConstructor(int .class, String.class, double .class, double .class); Object zhangSan = declaredConstructor.newInstance(1 , "zhangSan" , 1.76 , 67 ); System.out.println(zhangSan); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } }
8.总结 实际上,反射就是操作字节码文件。字节码文件和Java文件一一对应,
导入的包名;
自身类名(包括权限修饰符列表);
成员变量(权限修饰符列表、数据类型、属性名);
构造方法(权限修饰符列表、方法名);
普通方法(权限修饰符列表、返回值类型、方法名);
通过操作字节码文件可以实例化对象,可以反编译、调用方法 以及访问属性 等等,反射机制使得代码很具有通用性,可变化的内容都是写到了配置文件中,达到了动态的效果 。
9.备注 参考B站《动力节点》。