Java反射机制小结和实际操作
一、什么是反射
1、编译和运行
在了解什么是Java反射机制前,先聊聊Java的编译和运行。
还记得第一次使用记事本编写第一个Java程序的场景吗?通过命令窗口,使用javac命令编译一个.java文件,生成一个.class文件,再使用java命令运行.class文件。
在编译期,jvm会去检查.java文件中使用的类、类的属性和方法、类的对象等信息,如果都能找到对应的信息,则编译通过,否则会编译报错或抛出异常信息。
在运行期,会根据.class文件信息,处理其中的代码逻辑,如果遇到程序运行错误会抛出异常信息。
下面我们使用一个小例子来说明编译和运行。新建一个Animal类,根据传入的参数不同,创建不同的对象,执行不同的方法。
public class Animal{
public static void main(String[] args) {
if("Cat".equals(args[0])){
Cat cat = new Cat();
cat.eat();
}
if("Dog".equals(args[0])){
Dog dog = new Dog();
dog.eat();
}
}
}
使用cmd命令编译时,会报错,找不到Cat类和Dog类。
E:\Java项目实战>javac Animal.java
Animal.java:6: 错误: 找不到符号
Cat cat = new Cat();
^
符号: 类 Cat
位置: 类 Animal
Animal.java:6: 错误: 找不到符号
Cat cat = new Cat();
^
符号: 类 Cat
位置: 类 Animal
Animal.java:11: 错误: 找不到符号
Dog dog = new Dog();
^
符号: 类 Dog
位置: 类 Animal
Animal.java:11: 错误: 找不到符号
Dog dog = new Dog();
^
符号: 类 Dog
位置: 类 Animal
4 个错误
这时我们新建一个Cat类,编译Cat类后,再次编译Animal类,又报了两个错误,找不到Dog类。
Animal.java:11: 错误: 找不到符号
Dog dog = new Dog();
^
符号: 类 Dog
位置: 类 Animal
Animal.java:11: 错误: 找不到符号
Dog dog = new Dog();
^
符号: 类 Dog
位置: 类 Animal
如果我们想要使用猫类,而不管狗类是否存在,那该怎么做?那就是绕过编译期,在运行期动态执行猫类的方法。怎样绕过编译期呢?这就需要使用Java的反射机制了。
2、反射的概念
反射是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法。这种动态获取信息以及动态调用对象方法的功能,我们称之为Java语言的反射机制。
继续上面遗留的问题,在不新建一个Dog.java文件时,怎样调到Cat类的eat方法?
首先,我们要根据传入的参数动态加载类,然后创建类的实例对象,最后执行类的方法。因为你无法确定传入的参数是猫类还是狗类,就需要定义一个超类或接口,让猫类继承超类或实现接口,重载或重写eat()方法,在创建类实例时,只用创建超类或接口的实例即可。
public class AnimalBetter {
public static void main(String[] args) {
try {
Class c1 = Class.forName(args[0]);
IAnimal animal = (IAnimal)c1.newInstance();
animal.eat();
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里我们选用的是接口
public interface IAnimal {
public void eat();
}
猫类的实现
public class Cat implements IAnimal {
public void eat() {
System.out.println("Cat is eatting");
}
}
二、反射的优缺点
使用反射,可以在代码改动最小的情况下有效扩展功能。就像上面的小例子,如果哪天我们想使用老虎类进行eat动作,我们还需要改动AnimalBetter类的代码吗?答案是不需要,只需要新建一个老虎类,实现IAnimal的接口,重写eat方法,就可以了,对其他既有代码无需做任何改动。
使用反射,可以动态的创建类实例和使用类的方法,可以分析类文件,可以访问类的私有属性或方法,对于一些需要在运行时处理的事情都可以交给反射实现。
既然反射这么好,为什么不在项目中大范围的使用?其实,反射也是有弊端的。反射包括了一些动态类型,jvm无法对这些代码进行优化,实际操作效率低于非反射操作,在那些经常被执行的代码或对性能要求较高的模块尽量少使用反射。反射在程序有安全限制的环境中是无法使用的。反射可以访问私有的属性或方法,可能会破坏封装性,产生意料之外的结果。
三、如何使用反射
1、使用过程
首先要获取操作的类的Class对象,然后通过Class类中的方法获取和查看该类中的方法和属性,最后使用反射API来操作这些信息。
2、获取要操作的类的Class对象
有三种方式获取类的Class对象
(1)已经知道该类的类名
Class c2 = Animal.class;
实际上,任何一个类都有一个隐含的静态成员变量class,并且任何一个类都是Class类的实例对象,这个对象我们称为该类的类类型。
(2)已经知道该类的对象,通过getClass方法获取
Animal animal = new Animal();
Class c3 = animal.getClass();
不管是上面的c2还是这里的c3,都是Animal类的类类型,一个类只可能是Class类的一个实例对象或一个类类型。
(3)使用Class类中的forName()静态方法
Class c4 = Class.forName("Animal"); //forName(String str)中参数为类的全路径(包名+类名)
3、获取类的属性和方法
我们将其编写为一个工具类,如下:
/**
* 获取类的属性和方法
* @author 小川94
* @date 2018年6月24日
*/
public class ClassUtil {
/**
* 获取类的成员变量信息
* @param obj 要操作的类
*/
public static void getClassFieldInfo(Object obj){
// 获取要操作的类的Class对象
Class c = obj.getClass();
// 获取该类的成员变量信息
// field类是java.lang.reflect包下的类,封装了关于类成员变量的操作
Field[] fs = c.getFields(); // 获取的是所有的public的成员变量的信息
Field[] fs2 = c.getDeclaredFields(); // 获取的是该类自己声明的成员变量的信息
// 使用foreach遍历成员变量
for (Field field : fs2) {
// 得到成员变量的类型的类类型
Class fieldType = field.getType();
// 获取类类型的名称
String typeName = fieldType.getName();
// 获取成员变量的名称
String fieldName = field.getName();
System.out.println(typeName + " " + fieldName);
}
}
/**
* 获取类的方法信息
* @param obj 要操作的类
*/
public static void getClassMethodInfo(Object obj){
// 获取要操作的类的Class对象
Class c = obj.getClass();
// 获取要操作的类的名称
System.out.println("类的名称是:" + c.getName());
// 获取该类的方法信息
// Method类是java.lang.reflect包下的类,封装了关于类方法的操作
Method[] ms = c.getMethods(); //获取的是所有的public的函数,包括从父类继承而来的
Method[] ms2 = c.getDeclaredMethods(); // 获取的是所有该类自己声明的方法,不限访问权限
// 遍历方法
for (Method method : ms2) {
// 得到方法的返回值类型的类类型
Class returnType = method.getReturnType();
System.out.print(returnType.getName() + " ");
// 获取方法名
System.out.print(method.getName() + " (");
// 获取参数类型,得到的是参数列表的类型的类类型
Class[] para = method.getParameterTypes();
for (Class class1 : para) {
System.out.print(class1.getName() + ",");
}
System.out.println(")");
}
}
/**
* 获取类的构造方法
* @param obj 要操作的类
*/
public static void getClassConstructorMethodInfo(Object obj){
// 获取要操作的类的Class对象
Class c = obj.getClass();
// 获取该类的构造方法信息
// constructor类是java.lang.reflect包下的类,封装了关于类的构造方法的操作
Constructor[] cs = c.getConstructors(); // 获取所有的public的构造函数
Constructor[] cs2 = c.getDeclaredConstructors(); //得到所有的构造函数
for (Constructor constructor : cs2) {
System.out.print(constructor.getName()+"(");
// 获取构造函数的参数列表,得到的是参数列表的类类型
Class[] paramTypes = constructor.getParameterTypes();
for (Class class1 : paramTypes) {
System.out.print(class1.getName()+",");
}
System.out.println(")");
}
}
}
编写测试类,如下:
public class Test {
public static void main(String[] args) {
System.out.println("----- 成员变量信息 -----");
ClassUtil.getClassFieldInfo(new Integer(2));
System.out.println("----- 构造方法信息 -----");
ClassUtil.getClassConstructorMethodInfo(new Integer(2));
System.out.println("----- 方法信息 -----");
ClassUtil.getClassMethodInfo(new Integer(2));
}
}
测试结果如下,大家可以将测试结果与Integer这个类做比较。
----- 成员变量信息 -----
int MIN_VALUE
int MAX_VALUE
java.lang.Class TYPE
[C digits
[C DigitTens
[C DigitOnes
[I sizeTable
int value
int SIZE
int BYTES
long serialVersionUID
----- 构造方法信息 -----
java.lang.Integer(int,)
java.lang.Integer(java.lang.String,)
----- 方法信息 -----
类的名称是:java.lang.Integer
int numberOfLeadingZeros (int,)
int numberOfTrailingZeros (int,)
int bitCount (int,)
boolean equals (java.lang.Object,)
java.lang.String toString (int,int,)
java.lang.String toString ()
java.lang.String toString (int,)
int hashCode (int,)
int hashCode ()
int min (int,int,)
int max (int,int,)
int reverseBytes (int,)
int compareTo (java.lang.Integer,)
int compareTo (java.lang.Object,)
byte byteValue ()
short shortValue ()
int intValue ()
long longValue ()
float floatValue ()
double doubleValue ()
java.lang.Integer valueOf (java.lang.String,int,)
java.lang.Integer valueOf (int,)
java.lang.Integer valueOf (java.lang.String,)
java.lang.String toHexString (int,)
int compare (int,int,)
java.lang.Integer decode (java.lang.String,)
void getChars (int,int,[C,)
int reverse (int,)
int stringSize (int,)
int sum (int,int,)
int parseInt (java.lang.String,)
int parseInt (java.lang.String,int,)
long toUnsignedLong (int,)
int compareUnsigned (int,int,)
int divideUnsigned (int,int,)
int formatUnsignedInt (int,int,[C,int,int,)
java.lang.Integer getInteger (java.lang.String,java.lang.Integer,)
java.lang.Integer getInteger (java.lang.String,int,)
java.lang.Integer getInteger (java.lang.String,)
int highestOneBit (int,)
int lowestOneBit (int,)
int parseUnsignedInt (java.lang.String,)
int parseUnsignedInt (java.lang.String,int,)
int remainderUnsigned (int,int,)
int rotateLeft (int,int,)
int rotateRight (int,int,)
int signum (int,)
java.lang.String toBinaryString (int,)
java.lang.String toOctalString (int,)
java.lang.String toUnsignedString (int,)
java.lang.String toUnsignedString (int,int,)
java.lang.String toUnsignedString0 (int,int,)
4、使用反射API来操作类
public class MethodDemo {
public static void main(String[] args) {
// 第一种写法
try {
// 获取要操作的类的Class对象
Class c = Class.forName("sessionone.Foo");
// 获取print方法
Method m = c.getMethod("print", String.class);
// 第二种写法,new Class[]{}里面可以接多个参数,也可以为空
Method m2 = c.getMethod("print", new Class[]{String.class});
// 第三种写法,获取该类自己声明的方法
Method m3 = c.getDeclaredMethod("print", String.class);
// 执行方法
m3.invoke(c.newInstance(), "小川94");
} catch (Exception e) {
e.printStackTrace();
}
// 第二种写法
Foo foo = new Foo();
foo.print("小川94");
// 第三种写法
try {
Class c2 = Class.forName("sessionone.Foo");
Foo foo2 = (Foo) c2.newInstance();
foo2.print("小川94");
} catch (Exception e) {
e.printStackTrace();
}
// 第四种写法
try {
Foo foo3 = new Foo();
Class c3 = Class.forName("sessionone.Foo");
Method method = c3.getDeclaredMethod("print", String.class);
method.invoke(foo3, "小川94");
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Foo{
public void print(String name){
System.out.println("Hello," + name);
}
}
四、反射的实际应用
1、在JDBC中的使用
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, user, password);
在装载驱动时,使用了反射,在getConnection的源代码中,使用了反射,整个JDBC都和反射有关联。
2、在集合泛型中的使用
ArrayList list = new ArrayList();
ArrayList<String> list2 = new ArrayList<String>();
System.out.println(list.equals(list2)); //true
Class c = list.getClass();
System.out.println("list的类类型名称:"+c.getName());
Class c2 = list2.getClass();
System.out.println("list2的类类型名称:"+c2.getName());
System.out.println(c == c2); //true
// c1==c2结果返回true,说明编译之后集合的泛型是去泛型化的
// Java中集合的泛型,是防止错误输入的,只在编译阶段有效,绕过编译就无效了
// list2.add(20); 这样写,是会编译报错
try {
System.out.println("list2中有"+list2.size()+"个元素");
Method m = c2.getMethod("add", Object.class);
m.invoke(list2, 20);
System.out.println("list2中有"+list2.size()+"个元素");
System.out.println(list2.toString()); //[20],已经将类型是整型的数添加到list2中
} catch (Exception e) {
e.printStackTrace();
}
集合中的泛型只是为了防止编译错误,在运行时会将泛型全部清除。
3、在Servlet中的使用
在之前的servlet系列文章中,使用了反射来完成了一个Servlet处理多个请求,具体可以看那篇文章的详细描述。
4、在框架中的使用
在主流框架中都有反射的影子,通过配置、注解完成一些需要静态加载类的方法转为使用反射来动态加载类,具体的会在讲解到框架时细讲。