Fork me on GitHub

Java的艺术-浅谈反射机制

此处输入图片的描述
在运行时发现和使用类的信息 - Java反射

什么是反射

通俗的解释是:JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

动态语言

动态语言,是指程序在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等在结构上的变化。

反射的作用

参考
假设我们有很多食材需要处理,猪肉,牛肉,羊肉;每一样食材对应一个类,这个类里面有处理食材的方法。

1
2
3
4
5
6
7
8
9
10
11
12
public class Pork{     //猪肉
public void cook(){
System.out.println("cook pork");
}
public class Beef{ //牛肉
public void cook(){
System.out.println("cook beef");
}
public class Mutton{ //羊肉
public void cook(){
System.out.println("cook mutton");
}

当我们在程序中需要根据食材的不同来实例化对应类的对象,再调用对应的方法要怎么做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class test{
public static void main(String[] args){
String food=null;
if(food.equals("Pork")){
Pork pork=new Pork();
pork.cook();
}
else if(food.equals("Beef")){
Beef beef=new Beef();
beef.cook();
}
else if(food.equals("Mutton")){
Mutton mutton=new Mutton();
mutton.cook();
}
}
}

以上的做法是很容易想到的,很直观,但是当我们的食材有几十种甚至上百种时,我们要怎么写上百个if else 来判断食材,再实例化对应的类,调用对应的方法。

有没有更简单的方法?答案是肯定的,我们可以利用反射机制来完成这一工作,在程序运行时根据类名实例化对应的对象,并调用对应的方法

步骤如下:

1、首先我们需要用到接口:

1
2
3
public interface IFood {
public void cook();
}

2、所有的食材类实现这一接口

1
2
3
4
5
6
7
8
9
10
11
12
public class Pork implements IFood{     //猪肉
public void cook(){
System.out.println("cook pork");
}
public class Beef implements IFood{ //牛肉
public void cook(){
System.out.println("cook beef");
}
public class Mutton implements IFood{ //羊肉
public void cook(){
System.out.println("cook mutton");
}

3、接下来我们看看怎么使程序更为简单

1
2
3
4
5
6
7
8
9
public class test{
public static void main(String[] args){
String food="Pork"; //字符串food代表食物的类
Class c=Class.forName(food); //根据字符串food来载入类对象
Object object=c.newInstance(); //生成对象
IFood Ifood=(IFood)object; //强制转型
Ifood.cook(); //调用方法
}
}

如此一来我们不需要写冗杂的if else 嵌套了

只需要利用 Class.forName()方法根据字符串来实例化对应的类,并调用其方法即可。

当我们需要处理其他的食材的时候只需要添加对应的类即可,而主程序中可以不改变一行代码。

反射的应用

Class对象的获取

加载完类之后, 在堆内存中会产生一个Class类型的对象(一个类只有一个Class对象), 这个对象包含了完整的类的结构信息,而且这个Class对象就像一面镜子,透过这个镜子看到类的结构,所以被称之为:反射。

1
2
java.lang.Object
--java.lang.Class<T>

获取Class对象的方法一般有三种方法:

调用对象的getClass()方法;

1
2
StringBuffer sb = new StringBuffer("test");
Class<?> class1 = sb.getClass();

类名的.class(最安全/性能最好)属性,或者TYPE属性(继承自Object类);

1
2
3
Class<?> class1 = int.class;
Class<?> class2 = Integer.class;
Class<?> class3 = Integer.TYPE;

运用Class.forName(String className)动态加载类,className需要是类的全限定名(最常用)

1
Class<?> class1 = Class.forName("Integer");

创建实例化对象

得到了对应java.lang.Class对象,有两种方法通过反射生成对象:[都需要抛出异常]

使用Class对象的newInstance()方法来创建该Class对象对应类的实例(这种方式要求该Class对象的对应类有默认构造器);相当于无参构造

1
2
Class<?> class1 = String.class;
Object str = c.newInstance();

先使用Class对象获取指定的Constructor对象, 再调用Constructor对象的newInstance()方法来创建该Class对象对应类的实例(通过这种方式可以选择指定的构造器来创建实例).适用于无参和有参的构造方法

1
Constructor<T> getConstructor(Class<?>... parameterTypes)

1
2
3
4
5
6
//获取String所对应的Class对象
Class<?> c = String.class;
//获取String类带一个String参数的构造器
Constructor constructor = c.getConstructor(String.class);
//根据构造器创建实例
Object obj = constructor.newInstance("test");

另外可以用getConstructors()方法返回Constructor对象的一个数组。这些对象反映此 Class 对象所表示的类的所有公共构造方法。如果该类没有公共构造方法,或者该类是一个数组类,或者该类反映一个基本类型或 void,则返回一个长度为 0 的数组。 注意,此方法返回 Constructor 对象的数组(即取自此类构造方法的数组)时,此方法的返回类型是 Constructor<?>[],不是预期的 Constructor[]。此少量信息的返回类型是必需的,因为从此方法返回之后,该数组可能被修改以保存不同类的 Constructor 对象,而这将违反 Constructor[] 的类型保证。

1
2
3
4
5
Class<?> c = String.class;//获取Class对象
Constructor<?> cons[] = c.getConstructors();
for (Constructor<?> constructor : cons) {
System.out.println(constructor);
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public java.lang.String(byte[],int,int)
public java.lang.String(byte[],java.nio.charset.Charset)
public java.lang.String(byte[],java.lang.String) throws java.io.UnsupportedEncodingException
public java.lang.String(byte[],int,int,java.nio.charset.Charset)
public java.lang.String(byte[],int,int,java.lang.String) throws java.io.UnsupportedEncodingException
public java.lang.String(java.lang.StringBuilder)
public java.lang.String(java.lang.StringBuffer)
public java.lang.String(byte[])
public java.lang.String(int[],int,int)
public java.lang.String()
public java.lang.String(char[])
public java.lang.String(java.lang.String)
public java.lang.String(char[],int,int)
public java.lang.String(byte[],int)
public java.lang.String(byte[],int,int,int)

此外,还可以用getDeclaredConstructors()方法获取所有的构造方法。

getConstructor()和getDeclaredConstructor()区别:

getDeclaredConstructor(Class<?>… parameterTypes)
这个方法会返回制定参数类型的所有构造器,包括public的和非public的,当然也包括private的。
getDeclaredConstructors()的返回结果就没有参数类型的过滤了。

getConstructor(Class<?>… parameterTypes)
这个方法返回的是getDeclaredConstructor()方法返回结果的子集,只返回制定参数类型访问权限是public的构造器。
getConstructors()的返回结果同样也没有参数类型的过滤。

获取方法

获取某个Class对象的方法集合,主要有以下几种方法:

getDeclaredMethods方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。

1
public Method[] getDeclaredMethods() throws SecurityException

getMethods方法返回某个类的所有公用(public)方法,包括其继承类的公用方法。

1
public Method[] getMethods() throws SecurityException

getMethod 方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象
注意:如果不带方法参数对应的class对象,则无法根据方法名获取对应的方法

1
public Method getMethod(String name, Class<?>... parameterTypes)

调用方法

当我们从类中获取了一个方法后,我们就可以用 invoke() 方法来调用这个方法。invoke 方法的原型为:

1
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException

invoke回调流程示例:

  • 由Class对象动态构造对应类型对象;

  • 利用Class对象的getMethod()、getMethods()、getDeclaredMethod()、getDeclaredMethods()等方法构造method对象;

  • 类型对象,使其执行对应形参的方法(也就是需要传入一个动态执行方法的对象,以及对应方法的形参)。

举例:(利用反射调用String对象的toString()方法)

1
2
3
4
Class test = String.class;
Object ob = new String("abc");
Method me = test.getDeclaredMethod("toString");
System.out.println(me.invoke(ob,null));

invoke()的缺点:

  • invoke的参数和返回值必需时Object类型的,这意味着必须进行多次的类型转换(特别是基本数据类型),而这将导致编译器错过检查代码的机会,有类型安全的风险,只有到了测试阶段才会发现这些错误,此时找到并改正他们将会更加困难。

  • 使用反射获得方法指针的代码要比仅仅直接调用方法明显慢一些。

  • 因此仅在必要时才使用Method对象,而最好使用接口和内部类,不建议Java开发者使用Method对象的回调功能,使用接口进行回调不仅会使代码的运行速度更快,还更易于维护。

访问成员变量

通过Class对象的的getField()方法可以获取该类所包含的全部或指定的成员变量Field,Filed提供了如下两组方法来读取和设置成员变量值:
getFiled:访问公有的成员变量
getDeclaredField:所有已声明的成员变量,但不能得到其父类的成员变量

getXxx(Object obj): 获取obj对象的该成员变量的值, 此处的Xxx对应8中基本类型,如果该成员变量的类型是引用类型, 则取消get后面的Xxx;
setXxx(Object obj, Xxx val): 将obj对象的该成员变量值设置成val值.此处的Xxx对应8种基本类型, 如果该成员类型是引用类型, 则取消set后面的Xxx;

注: getDeclaredXxx方法可以获取所有的成员变量,无论private/public;

反射与Spring框架设计

反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。

Spring内部采用了很多反射机制。
很多web框架的前端,实现URI请求映射到Action方法,也可以简单的通过反射来做。
这部分内容后面再单独补充。

反射存在的问题

  1. 由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。
  2. 另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题
-------------本文结束感谢阅读-------------