一、概述
Java 反射机制是在运行状态中,对于程序中的任意一个类,通过反射机制都能够知道这个类的所有属性和方法,包括共有、包含、默认和私有。对于任意的一个对象,通过反射机制都可以去调用它的每一个方法,这种机制就称为Java的反射机制。一般的操作都在java.lang.reflect
包中,常用到的类有Constructor
,Field
和Method
三种。既然是对Java类的反射,当然也有个比不可少的类Class
,这也是一个类,作为Java
中类文件的一个类,获取类的类一般有三种方式:
String s = "abc";
// 通过类直接获取
Class<?> c1 = String.class;
// 通过变量的getClass()方法获取
Class<?> c2 = s.getClass();
// 通过Class的静态方法forName(String)根据类的全名获取
Class<?> c3 = Class.forName("java.lang.String");
那么Java的反射机制有什么用处呢?一般的程序开发时,我们都是对使用的类直接操作,如创建等,但是如果遇到不能直接操作时怎么办呢,这里是反射的一个用处,如可是根据一个类名称的字符串来获取类的实例,这也是一种用法,程序可以根据配置文件内的类名去创建不同的类。还有一些情况如,类中有一些私有方法,我们想要访问或者修改,正常的做法是行不通的,通过反射则可以达到目的,这也成为暴力反射。
二、Class
类
第一次见到这个类可能会有些疑惑,Java中的类有很多,用过的也有很多,但是第一次见到这个描述类的类,多少还是有点好奇的,这也更加验证了一点,所有事物都可以被描述成对象,既然类这么常见,那当然也不会例外。首先我们应该对类文件有一些初步了解,我们在编写代码之后使用javac
进行编译,便可以看到目录下由.java
文件生成的.class
文件,那么这些.class
文件便是类文件。执行这些文件时,如java x.class
,类文件便会被加载到内存中,以便使用,类文件中使用到的其他类也会随之加载,通过下面的例子结果可以说明一个问题,便是类文件在内存中是唯一的。
String a1 = "abc";
System.out.println(a1.getClass() == String.class);
System.out.println(a1.getClass() == Class.forName("java.lang.String"));
// 执行结果为
true
true
对于基本数据类型boolean,char,byte,short,int,long,float,double
共8个和一个void
都有其对应的类,如int.class
,注意int.class
和Integer.class
不是同一个类。对于数组也有其对应的类,如int[].class
,int[][].class
,类型相同,维数相同的数组类才是同一个类,即int[].class != int[][].class
,其他类似,数组类的类名有一定的规则,如int[][].class.getName()
返回的是[[I
,其中[
的个数表示维数,int
对应的是I
,对应关系如下:
类型名 | 对应的内容 |
---|---|
boolean | Z |
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
class or interface | Lclassname |
数组类也是继承自Object
的,如可以用Object
对象接收int[]
,如Object obj = new int[]{1, 2, 3};
,但是注意一点Object[] obj = new int[]{1, 2, 3};
这种写法是不正确的,因为基本数据类型不能转成Object
对象。
Class
的常用方法
getConstructor(Class<?>... parameterTypes)
此方法用于获取类的构造方法,其中的参数是可变参数,用于指定获取的构造方法是那个,如获取String
类的String(StringBuilder stringBuilder)
构造方法,可以用Constructor cons String.class.getConstructor(StringBuilder.class);
方式来获取。这个方法类似的另一种获取多构造方法的是getConstructors()
,可以获取所有的构造方法,返回一个Constructor
的数组。newInstance()
方法用于实例化一个对象,即用这个类创建一个对象。示例代码如下:
import java.lang.reflect.*;
class Main {
public static void main(String[] args) throws Exception {
// 获取String的一个构造方法
Constructor cons = String.class.getConstructor(StringBuilder.class);
System.out.println(cons);
// 创建一个空的字符串
String s = String.class.newInstance();
System.out.println(s);
}
}
// 执行结果为
public java.lang.String(java.lang.StringBuilder)
[空串]
String getName()
用于获取类名(包含包名),如String.class.getName()
的返回值为"java.lang.String"
。Package getPackage()
用于返回包名。Field getField(String name)
获取类的成员变量。Field[] getFields()
获取类发所有可访问的成员变量。Field getDeclaredField(String name)
根据名称获取类声明的一个成员变量。这里可以获取private
修饰的成员变量,同样的还有Field[] getDeclaredFields()
用于获取所有类声明的成员变量。Method getMethod(String name, Class<?>... parameterTypes)
获取方法,和获取构造方法类似,同样也有获取所有方法和获取声明的方法等等。
三、Constructor
Class<T> getDeclaringClass()
返回 Class 对象,该对象表示声明由此 Constructor 对象表示的构造方法的类。int getModifiers()
返回以整数形式返回此 Constructor 对象所表示构造方法的 Java 语言修饰符。T newInstance(Object... initargs)
使用此 Constructor 对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例。
import java.lang.reflect.*;
class Main {
public static void main(String[] args) throws Exception {
// 获取String的一个构造方法
Constructor cons = String.class.getConstructor(StringBuilder.class);
// 使用获取的构造函数示例化一个对象
String s = (String)cons.newInstance(new StringBuilder("abc"));
// 输出这个对象
System.out.println(s);
// 获取构造函数对应的类
Class<?> c = cons.getDeclaringClass();
// 输出类名
System.out.println(c.getName());
// 构造方法的类型
System.out.println(Member.DECLARED == cons.getModifiers());
}
}
四、Field
描述一个类的成员,一般功能有获取这个成员的值,设置这个成员的值,获取这个成员的数据类型等,下面通过一个实例演示一下Field
的相关操作,实例内容为将一个自定义对象中的成员(String
类型)中出现的叠词替换为[double]
,首先是获取该自定义对象的类对象,然会获取所有声明的成员,然会获取这些成员的值,最后再将替换过的值设置回去。
import java.lang.reflect.*;
/**
* 将一个自定义类中的成员变量(String类型)中出现的连续两
* 个相同的字符替换成[double]。如aa变成[double]。
*/
class Main {
public static void main(String[] args) throws Exception {
Demo demo = new Demo();
// 获取demo对象中的声明的成员
Field[] fields = demo.getClass().getDeclaredFields();
for(Field field : fields) {
// 如果不是可直接访问的,则将其修改为可直接访问型
if(!field.isAccessible()) {
field.setAccessible(true);
}
// 获取原始字符串内容
String oldString = (String)field.get(demo);
// 将叠词替换为[double]
String newString = oldString.replaceAll("(.)\\1", "[double]");
// 最后将新的字符串设置回去
field.set(demo, newString);
// 输出新的变量内容
System.out.println(field);
}
System.out.println(demo.toString());
}
}
class Demo {
// 修改后应为ac[double]de
public String a = "abccde";
// 修改后应为he[double]oawdcs
private String b = "helloawdcs";
// 修改后应为[double]sdn[double]asdnjasd[double]adas
protected String c = "aasdnjjasdnjasdbbadas";
public String toString() {
return a + "/" + b + "/" + c;
}
}
五、Method
描述一个类的方法,一般常用功能, Class<?> getReturnType()
返回一个 Class
对象,该对象描述了此 Method
对象所表示的方法的正式返回类型。Object invoke(Object obj, Object... args)
对带有指定参数的指定对象调用由此 Method
对象表示的底层方法。 下面通过一个实例,内容是通过反射功能获取一个String
对象的第一个位置的字符,即调用charAt()
方法。
import java.lang.reflect.*;
class Main {
public static void main(String[] args) throws Exception {
String s = "abc";
// 获取String的类对象
Class<?> c = String.class;
// 获取String类的charAt方法
Method method = c.getMethod("charAt", int.class);
// 通过方法的反射获取第1个位置的字符
char ch = (char)method.invoke(s, 1);
System.out.println(ch); // 结果为b
}
}
六、数组的反射
使用反射传递数组参数时,需要注意其可能会被系统自动拆分为对应的可变参数类型,如main
方法的String[]
会变成String...
,所以传递时一般有两种解决方式,一是将其再次封装成一个数组,如new Object[]{new String[]{"abc", "cba", "bac"}}
;二是将其强转为父类(Object)new String[]{"abc", "cba", "bac"}
,两种方式都可。
import java.lang.reflect.*;
/**
* 通过反射调用另一个类的main方法,传递去一个数组参数
*/
class Main {
public static void main(String[] args) throws Exception {
// 首先获取对应类的Class对象
Class clazz = Demo.class;
// 获取Demo的main方法
Method method = clazz.getMethod("main", String[].class);
// 定义待传递的参数
String[] strs = new String[]{"Hello", "Hi", "Bye"};
/*调用静态方法时,不需要传递变量
*注意:因为JDK的版本,如果直接传递strs会被拆分成
*可变类型String...,不符合main的参数要求,所以
*这里将其转为一个整体,Object类型
*/
method.invoke(null, (Object)strs);
}
}
class Demo {
public static void main(String[] args) {
// 将传递进来的参数输出
for(String s : args) {
System.out.println("参数:" + s);
}
}
}
// 执行结果
参数:Hello
参数:Hi
参数:Bye
前面已经说过数组也是一个类,如int[].class
,这种类和其他类有一些区别,Java提供了一个用于操作数组类的类Array
,Array 类提供了动态创建和访问 Java 数组的方法。Array 允许在执行 get 或 set 操作期间进行扩展转换,但如果发生收缩转换,则抛出 IllegalArgumentException
。示例代码如下:
import java.util.*;
import java.lang.reflect.*;
class Main {
public static void main(String[] args) throws Exception {
// 获取String[]对应的类
Class<?> clazz = String[].class;
// 定义一个String数组并初始化
String[] strs = new String[]{"Hello", "Hi", "Bye"};
// 如果这个类是数组类
if(clazz.isArray()) {
// 获取数组的长度
int len = Array.getLength(strs);
// 变量这个数组
for(int i=0; i<len; i++) {
// 反射获取strs的第i位置的值
String s = (String)Array.get(strs, i);
// 输出获取的值
System.out.println(i+":"+s);
// 如果这个值为Hi,则将其改为Good
if(s.equals("Hi")) {
Array.set(strs, i, "Good");
}
}
// 将整个数组输出
System.out.println(Arrays.toString(strs));
}
}
}
// 执行结果为
0:Hello
1:Hi
2:Bye
[Hello, Good, Bye]
七、反射的应用
在实际开发中,反射的应用一般是用于开发框架,框架是一些功能的核心抽取,涵盖了一个系统的整体,但是没有完成细节的东西。
下面是一个框架的实例,程序可以根据用户配置的文件,执行中使用不同的类,在当前目录建立一个文件名为config.properties
在其内写入className=java.util.ArrayList
,这是程序中的集合使用的是ArrayList
,最后的结果是集合大小为4,然后将className
的值修改为java.util.HashSet
,最后的结果则变为了3,如此一来,程序可以根据不同的配置使用不同的类,框架便是如此,在使用的类不确定,或者没有现成的类可使用时,不防使用反射来实现程序功能。代码如下:
import java.io.*;
import java.util.*;
import java.lang.reflect.*;
class Main {
public static void main(String[] args) throws Exception {
// 配置
Properties properties = new Properties();
// 通过类加载器获取输入流
InputStream in = Main.class.getResourceAsStream("config.properties");
// 从流中加载配置
properties.load(in);
// 关闭流
in.close();
// 获取类名
String className = properties.getProperty("className");
// 根据类名获取类
Class clazz = Class.forName(className);
// 根据clazz类,创建一个集合对象
Collection collection = (Collection) clazz.newInstance();
// 向集合中添加一些字符串
collection.add(new String("Hello"));
collection.add(new String("Hi"));
collection.add(new String("Bye"));
collection.add(new String("Bye"));
// 输出集合的大小
System.out.println(collection.size());
}
}