初识Class类与反射
文章目录
一、Class类概述
Class类的原码中有这样一段注释:“Instances of the class {@code Class} represent classes and interfaces in a running Java application.”。一个Class类的实例代表着正在运行的java程序中的类和接口。也就是说Class类是用来描述每个类运行时的运行信息的类,当我们编写一个类并编译完成后就会产生一个Class对象用来表示这个类的类型信息。每一个类都有一个Class对象。
二、class对象的获取
Class是没有公有构造函数的,这意味着我们无法使用new来创建一个Class对象。Class对象是在类加载的过程中由JVM通过类加载器构造的。
Class对象的获取有三种方式:
2.1、通过getClass()方法
getClass方法并不是Class对象的方法,而是内置于Object中的方法,用于返回该类所对应的Class对象
public class Test {
public static void main(String[] args) {
String str = new String("str1");
Class c1 = str.getClass();
System.out.println(c1);
}
}
class java.lang.String
2.2、通过类的类字面常量class获取
调用方式:xx.class
class Obj1{
static {
System.out.println("加载类1");
}
}
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Class c1 = Obj1.class;
System.out.println(c1);
}
}
class classTest.Obj1
2.3、通过forName(String className)方法
forName方法是Class类的静态方法,通过传入的类的全限定名(全限定名只绝对路径,即从最上层的包向下逐层引用到类,例如java.lang.String)获得类的Class对象,在forName的执行过程中,若发现被指定的类还没有被加载,JVM就会调用类加载器加载该类,并返回加载后的Class对象。例如在下面这段代码中,我们并没有在main中实例化一个Obj1,因此类加载器没有加载Obj1,那么我们调用forName方法就会加载Obj1,使得Obj1中的静态代码块被执行,而Obj2则有一个实例,在创建这个实例之前,首先要加载Obj2,因此Obj2的静态代码块在调用构造函数之前执行了,当我们再调用forName获取Obj2 的Class对象时就不会在执行。如果没要找打指定的类就会抛出java.lang.ClassNotFoundException异常
class Obj1 {
static {
System.out.println("初始化1");
}
}
class Obj2{
static {
System.out.println("初始化2");
}
public Obj2(){
System.out.println("创建了一个对象2");
}
}
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Class c1 = Class.forName("classTest.Obj1");
System.out.println(c1);
Obj2 obj2 = new Obj2();
Class c2 = Class.forName("classTest.Obj2");
System.out.println(c2);
}
}
初始化类1
class classTest.Obj1
初始化类2
创建了一个对象2
class classTest.Obj2
2.4、三种方法的比较
-
getClass方法并不是静态方法,这意味着需要创建一个类的实例才能调用,即如果我们使用getClass方法获得一个类的Class对象,我们需要先实例化这个对象,而forName方法和获取类的类字面常量
-
类字面常量和forName方法都不用创建类的实例,但类字面常量相较于forName方法更为安全,其在编译时就会接受检查因此不必置于try代码块中
-
getClass方法需要创建一个类的实例,显然不能用于获取接口的Class对象,而类字面常量和forName均可以用于获取接口的Class对象
-
forName不可以用于获取数组的Class对象,而getClass和类字面常量可以
-
forName和getClass不可以用于获取基本数据类型的Class对象,而类字面常量可以
-
调用类字面常量时并不会自动的初始化Class对象,而forName方法会(注意是初始化而不是加载,类的加载分为加载链接和初始化,forName和类字面常量都会检查类有没有加载,但类字面常量不会自动初始化)
三、Class类与反射
Class对象有什么用呢?为什么要记录类的运行时信息呢?在一个类中肯那个会存在私有的属性或变量,如果我们想要访问这些属性和变量,就需要借助于Class对象与反射机制。
Java的反射机制是指:在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。反射可以对一个类进行分解,将类的组成部分映射成一个个对象。很显然Class就是反射机制实现的媒介。
3.1、获取类的构造函数
获取所有的构造函数:
例如下面的代码,首先获得Rectangle类的Class对象,定义一个Constructor数组来存放所有的构造函数(Constructor类存放了构造函数的相关信息),调用Class的getDeclaredConstructor()方法,该方法会返回一个Constructor数组,然后遍历这个数组,通过Constructor的getModifiers()方法获取构造函数的访问修饰符,通过getParameterTypes()方法获得参数的类型。
class Rectangle{
private Integer a;
private Integer b;
public Rectangle(){
System.out.println("无参构造函数");
}
private Rectangle(Integer a, Integer b) {
System.out.println("多参构造函数");
this.a = a;
this.b = b;
}
private Integer getArea(){
return a*b;
}
}
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Class rectClass = Rectangle.class;
Constructor[] constructors = rectClass.getDeclaredConstructors();
for (Constructor c:constructors){
System.out.println("访问修饰符:"+Modifier.toString(c.getModifiers()));
System.out.print("参数类型:");
Class[] paramterTypes = c.getParameterTypes();
for(Class p:paramterTypes){
System.out.print(p+" ");
}
System.out.println();
}
}
}
运行结果:
访问修饰符:public
参数类型:
访问修饰符:private
参数类型:class java.lang.Integer class java.lang.Integer
获取指定构造函数
直接在调用Class对象的getDeclaredConstructor()方法时传入参数类型的Class对象即可获得指定的构造函数
public class Test {
public static void main(String[] args) throws NoSuchMethodException {
Class rectClass = Rectangle.class;
Constructor constructor1 = rectClass.getDeclaredConstructor();
Constructor constructor2 = rectClass.getDeclaredConstructor(Integer.class,Integer.class);
System.out.println(Modifier.toString(constructor1.getModifiers()));
System.out.println(Modifier.toString(constructor2.getModifiers()));
}
}
运行结果:
public
private
构造函数的执行
共有构造函数的调用十分简单,只需要通过Constructor实例调用newInstance()即可,如果是有参的构造函数,在调用newInstance是传入相应的参数即可
Constructor constructor1 = rectClass.getDeclaredConstructor();
constructor1.newInstance();
/*运行结果:
无参构造函数
*/
如果需要调用私有的构造函数,我们需要先调用setAccessible(true)方法
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class rectClass = Rectangle.class;
Constructor constructor1 = rectClass.getDeclaredConstructor();
Constructor constructor2 = rectClass.getDeclaredConstructor(Integer.class,Integer.class);
constructor1.newInstance();
constructor2.setAccessible(true);
constructor2.newInstance(2,3);
}
无参构造函数
多参构造函数
那么如果想要通过反射调用私有构造函数创建一个对象我们只需要将在调用Constructor.newInstance()时进行类型转换即可,即:
Rectangle rectangle = (Rectangle) constructor2.newInstance(2,3);
此外也可以通过Class对象.newInstance()获得类的实例,但这种方法只能调用公有无参构造函数。
3.2、获取类的私有方法
首先通过Class.getDeclaredMethod(String name, Class<?>… parameterTypes)方法获取方法的实例,其中那么是需要获取的方法的名称,parameterTypes是方法的参数类型的Class对象列表。得到Method的实例后调用setAccessible(true)方法修改可见性,最后调用invoke(Object obj, Object… args)方法,其中obj是调用该方法的类的实例,args是方法的参数列表
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class rectClass = Rectangle.class;
Constructor constructor1 = rectClass.getDeclaredConstructor();
Constructor constructor2 = rectClass.getDeclaredConstructor(Integer.class,Integer.class);
constructor1.newInstance();
constructor2.setAccessible(true);
Rectangle rectangle = (Rectangle) constructor2.newInstance(2,3);
Method getArea = rectClass.getDeclaredMethod("getArea");
getArea.setAccessible(true);
System.out.println("面积:"+getArea.invoke(rectangle));
}
无参构造函数
多参构造函数
面积:6
3.3、获取类的私有属性
获取私有属性的方法和上面很类似,首先调用Class.getDeclaredField(String name)获取Filed实例,其中name是属性的名字,然后调用setAccessible(true),然后可以用Field.get(Object obj)获取属性的值,调用Field.set(Object obj, Object value)修改属性的值
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Class rectClass = Rectangle.class;
Constructor constructor = rectClass.getDeclaredConstructor(Integer.class,Integer.class);
constructor.setAccessible(true);
Rectangle rectangle = (Rectangle) constructor.newInstance(2,3);
Field fielda = rectClass.getDeclaredField("a");
Field fieldb = rectClass.getDeclaredField("b");
fielda.setAccessible(true);
fieldb.setAccessible(true);
System.out.println("修改前的边长:"+fielda.get(rectangle)+","+fieldb.get(rectangle));
fielda.set(rectangle,5);
fieldb.set(rectangle,6);
System.out.println("修改后的边长:"+fielda.get(rectangle)+","+fieldb.get(rectangle));
Method getArea = rectClass.getDeclaredMethod("getArea");
getArea.setAccessible(true);
System.out.println("面积:"+getArea.invoke(rectangle));
}
多参构造函数
修改前的边长:2,3
修改后的边长:5,6
面积:30
四、总结
1、反射的优点:
-
提高了Java程序的灵活性和扩展性,降低了耦合性,提高自适应性。
-
它允许程序创建和控制任何类的对象,无需提前硬编码目标类。
2、反射的缺点:
- 性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时远慢于直接代码。因此Java反射机制主要应用在对灵活性和扩展性要求很高的框架下,普通程序不建议使用
- 使用反射会模糊程序内部逻辑:程序员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术,因而会带来维护问题,反射代码比相应的直接代码更复杂