黑马程序员——Java基础—反射机制

时间:2023-02-16 22:55:17


Java基础之反射技术



反射的基石——Class类

Java程序中的各个Java类属于同一类事物,描述这类事物的Java类名就是Class。例如:众多的人用Person类来表示,而众多的Java类就用Class类来表示,Person类的实例对象就如张三、李四这样一个个具体的人,而Class类代表java类,它的各个实例对象对应的是什么呢?对应的是各个类在内存中的字节码,例如Person类的字节码,Demo类的字节码。一个类被加载器加载到内存中,占用一片存储空间,这个空间里面的内容就是类的字节码,不同的类的字节码是不同的,所以它们在内存中的内容是不同的,这一个个的空间可以分别用一个个的对象来表示,这些对象显然具有相同的类型,就是Class类型。

想要获取字节码文件对象中的成员,要先获取字节码文件对象。如何得到各个字节码对应的实例对象(Class类型)呢?

有三种方式,这三种方式得到的是同一个字节码实例对象。:

1.类型名称.class,例如:System.class。 好处:不用new对象,但仍需要使用具体的类型。

2.对象.getClass,例如:new Demo.getClass()。 弊端:必须要创建指定类的对象才可以调用该方法。

3.Class.forName("类名"),例如:Class.forName("java.util.Date")。

Class.forName("类名")的作用:返回字节码,有两种方式:

1.该字节码已经被加载到JVM里面了,那么就直接返回

2.JVM里面还没有这份字节码,则会用类加载器去加载,把加载进来的字节码缓存到JVM里面,以后就不用再加载了。 

String  getName():获取此Class对象的名称。 
boolean isArray():判断此Class对象是否表示一个数组类。
boolean isInterface():判断此Class对象是否表示一个接口类型。
boolean isPrimitive():判断指定的Class对象是否表示一个基本类型。

System.out.println(int.class.isPrimitive()); //判断是否为基本类型。true
System.out.println(int.class == Integer.class);
System.out.println(int.class == Integer.TYPE); //判断是否为int类型。 true
System.out.println(int[].class.isPrimitive()); //判断是否为基本类型。false
System.out.println(int[].class.isArray()); //判断是否为数组类型。true

总之:只要是在源程序中出现的类型,都有各自的Class实例对象。例如:int[] ,void 。

反射技术:

动态的获取指定的类以及动态的调用类中的内容。反射就是把java类中的各种成分映射成相应的java类。

例如:一个java类中用一个Class类的对象来表示,一个类中的组成部分包括:成员变量、方法、构造方法、包等等信息也用一个个的Java类来表示,就像汽车是一个类,汽车中的发动机、变速箱等等也是一个个的类。表示java类的Class类显然要提供一系列的方法,来获得其中的变量、方法、构造方法、修饰符、包等信息,这些信息就是用相应类的实例对象来表示,它们是Field、Method、Contructor、Package等等。

反射的应用场景:

当应用程序已经写好了,后期出现的接口子类无法直接在该应用程序中用new创建对象。这时,既然子类不确定,可以通过对外提供配置文件的形式,将不确定的信息存储到配置文件中即可。而程序要提前写好如何读取配置文件信息。根据配置文件中具体的类名找到该类,并进行加载和对象的创建。这些动作要在前期定义软件时写好。没有类之前就将创建对象的动作提前做完了,这就是动态的获取指定的类,并使用类中的功能,这就是反射技术。

反射的好处:大大的提高了程序的扩展性。

一个类中的每个成员都可以用相应的反射API类的一个实例对象来表示,通过调用Class类的方法可以得到这些实例对象后,得到这些实例对象后有什么用呢?怎么用呢?这正是学习和应用反射的要点。

构造方法的反射: 

Constructor类代表某个类中的一个构造方法。

通过Class类中的getConstructor方法获取一个构造方法对象,获取构造方法对象时,要指定构造方法参数类型。示例:Constructor con = cls.getConstructor(StringBuffer.class)。 获得的构造方法对象调用newInstance方法时,参数中接收的实例对象要和上面的类型相同。示例:String str = (String)con.newInstance(new StringBuffer("abc"));如果是获取空参数的构造方法对象时,可以直接使用Class类中的newInstance方法得到,省去了再找Constructor的过程。

Class clazz = Class.forName("className"):通过给定的类名,加载对应的字节码文件,并封装成字节码文件对象Class。
Object obj = clazz.newInstance():通过new创建给定类的实例。 调用该类的构造函数。 

在反射中经常见到的异常:
1.InstantiationException:实例初始化异常。表示没有对应的构造函数。
2.IllegalAccessException:无效的访问异常。表示有提供,但是权限不够。

如果通过指定的构造函数初始化对象:

1.先获取字节码文件对象。

2.再获取给定的构造函数。

3.通过该构造函数初始化对象。

getConstructors():得到某个类所有的构造方法。 

getConstructor(Class<?>... parameterTypes):得到某一个构造方法。

newInstance():创建此Class对象所表示的类的一个新实例。

newInstance(Object... initargs):创建该构造方法的声明类的新实例。

getDeclaringClass():得到此Constructor对象表示的构造方法所属的类。表示哪个类的构造方法。

构造方法的反射代码示例:

public class ReflectTest{
public static void main(String[] args) throws Exception {
//获取String类型的字节码实例对象。
Class cls = String.class;
//通过Class类中的getConstructor方法获取一个构造方法对象。要指定构造方法的类型。
Constructor con = cls.getConstructor(StringBuffer.class);

//获得的构造方法对象调用newInstance方法时,参数中接收的实例对象要和上面的类型相同。
String str = (String)con.newInstance(new StringBuffer("abc"));

//String str = (String)cls.newInstance(); 如果是空参数的构造方法的话,可以直接用字节码对象调用newInstance方法。
System.out.println(str);
}
}

成员变量的反射:

在Class类中通过getField(String name)、getDeclaredField(String name) 方法获取Field对象。该Field对象是对应到类上面的成员变量,也就是字节码上的成员变量,并不是对象上的成员变量。Filed类代表某个类中的一个成员变量。再通过Field对象的get(对象)方法获取某个对象上对应该成员变量(字段)所表示的值,示例:fieY.get(pt1)。setAccessible(boolean flag):将此对象的accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。值为 false 则指示反射的对象应该实施 Java 语言访问检查。 set(Object obj, Object value):将指定对象变量上此 Field 对象表示的字段设置为指定的新值。代码示例:

import java.lang.reflect.Field;

public class Test{
public static void main(String[] args) throws Exception {
ReflectPoint pt = new ReflectPoint(6,9);

//通过Class类中的getField方法获取Field对象,该对象是类上的成员变量,也就是字节码上的成员变量。
Field fieldY = pt.getClass().getField("y");

//通过Field对象的get(对象)方法获取某个对象上对应该成员变量(字段)所表示的值。
System.out.println(fieldY.get(pt));

//如果此已声明的字段是私有的,则使用getDeclaredField方法获取Field对象。
Field fieldX = pt.getClass().getDeclaredField("x");

//设置访问检查。值为 true 则指示反射的对象在使用时应该取消 Java语言访问检查。
fieldX.setAccessible(true);
System.out.println(fieldX.get(pt));
}
}

成员方法的反射:

Method类代表某个类中的一个成员方法。

getMethod(String name, Class<?>... parameterTypes):获得类中的某一个方法,第一个参数是方法的名称,第二个参数是参数列表(参数类型和个数)。示例:Method charAt = str.getClass().getMethod("charAt",int.class)。

调用方法:通常方式:str.charAt(2)。 

反射方式:invoke(Object obj, Object... args):第一个参数是对象,第二个参数也是参数列表,和getMethod的第二个参数是对应的。示例:charAt.invoke(str,2)。 如果传递给Method对象的invoke方法的第一个参数为null,说明该Method对象对应的是一个静态方法。 代码示例:

public class Test{
public static void main(String[] args) throws Exception {

String s1 = "aliluyaamen";

//获得类上面的成员方法对象。
Method replace = String.class.getMethod("replace", char.class,char.class);

//让得到的成员方法对象作用在一个对象上面,并按照参数列表传递实际的参数。
String s2 = (String)replace.invoke(s1, 'a','m');
System.out.println(s2);
}
}

数组的反射:

具有相同维数和元素类型的数组属于同一个类型,即具有相同的Class实例对象。

代表数组的Class实例对象的getSuperclass方法返回的父类为Object类对应的Class。

基本类型的一维数组可以被当做Object类型使用,不能当做Object[]类型使用,非基本类型的一维数组既可以当做Object类型使用,也可以当做Object[]类型使用。

Class类中的getSuperclass方法可以获取该字节码对象的父类的Class。通过验证发现int[]类型的父类是Object,而String[]类型的父类也是Object。

Arrays.asList方法处理int[]和String[]时的差异:数组的工具类Arrays中的方法asList()可以将数组转变成List集合,如果是一个基本类型的数组,那么会将这个数组作为元素转变成List集合,如果是一个Object类型的数组,那么会将数组中的元素转变成集合中的元素存在。

java.lang.reflect包中的Array类,提供了动态创建和访问 Java 数组的方法。该类中的方法都是静态的,因此要用类名调用。get(Object array, int index):返回指定数组对象中索引组件的值。示例:int arr[x] = Array.get(arr,x);

需求:通过反射的方式打印任意对象中的元素,如果该对象是数组,就打印数组中的每个元素。如果不是数组就直接打印该对象。代码示例如下:

import java.lang.reflect.Array;

public class Test{
public static void main(String[] args) throws Exception {
String str = "abcdefg";
printObject(str);

int[] arr = {1,5,6,9,2,3,7};
printObject(arr);
}
public static void printObject(Object obj){
//先获取到该对象的字节码对象。
Class cla = obj.getClass();

//判断该Class对象是否是数组类型的。
if(cla.isArray()){
//通过反射获取数组的长度。
int len = Array.getLength(obj);
for(int x=0; x<len; x++){
//通过反射获取数组元素的值。
System.out.println(Array.get(obj,x));
}
}
else{
System.out.println(obj);
}
}
}
反射在程序中的作用——我们学习反射主要是用来做框架。
框架与工具类有区别,工具类被用户的类调用,而框架则是调用用户提供的类。框架的核心问题:因为在写程序时无法知道要被调用的类名,所以在程序中无法直接new某个类的实例对象了,而要用反射方式来做。通过读取配置文件,动态的获取类并使用类中的功能,这种方式大大的提高了程序的扩展性。