【搞定Java基础】第13篇:Java 类型信息(Class对象)与反射机制

本文转载自:https://blog.csdn.net/javazejian/article/details/70768369

本文目录:

1、深入理解 Class 对象

1.1、RRTI 的概念以及 Class 对象作用

1.2、Class 对象的加载及其获取方式

1.2.1  Class 对象的加载

1.2.2  Class.forName 方法

1.2.3  Class 字面常量

1.2.4  理解泛化的 Class 对象引用

1.2.5  关于类型转换的问题

1.2.6  instanceof 关键字与 isInstance 方法

2、理解反射技术

2.1、Constructor 类及其用法

2.2、Field 类及其用法

2.3、Method类及其用法

2.4、反射包中的 Array 类


1、深入理解 Class 对象

1.1、RRTI 的概念以及 Class 对象作用

认识 Class 对象之前,先来了解一个概念,RTTI(Run-Time Type Identification)运行时类型识别,对于这个词一直是 C++ 中的概念,至于 Java 中出现 RRTI 的说法则是源于《Thinking in Java》一书,其作用是在运行时识别一个对象的类型和类的信息,这里分两种:传统的”RRTI”,它假定我们在编译期已知道了所有类型(在没有反射机制创建和使用类对象时,一般都是编译期已确定其类型,如 new 对象时该类必须已定义好),另外一种是反射机制,它允许我们在运行时发现和使用类型的信息。在 Java 中用来表示运行时类型信息的对应类就是 Class 类,Class 类也是一个实实在在的类,存在于 JDK 的 java.lang 包中,其部分源码如下:

public final class Class<T> implements java.io.Serializable, GenericDeclaration, Type, AnnotatedElement {
	private static final int ANNOTATION = 0x00002000;
	private static final int ENUM       = 0x00004000;
	private static final int SYNTHETIC  = 0x00001000;

	private static native void registerNatives();
	static {
		registerNatives();
	}

	// 私有构造,只能由JVM创建该类
	private Class(ClassLoader loader) {
		
		classLoader = loader;
	}

	// ...
}

Class 类被创建后的对象就是 Class 对象,注意,Class 对象表示的是自己手动编写类的类型信息,比如创建一个Shapes 类,那么,JVM 就会创建一个 Shapes 对应 Class 类的 Class 对象,该 Class 对象保存了 Shapes 类相关的类型信息。实际上在 Java 中每个类都有一个 Class 对象,每当我们编写并且编译一个新创建的类就会产生一个对应 Class 对象并且这个 Class 对象会被保存在同名.class文件里(编译后的字节码文件保存的就是 Class 对象),那为什么需要这样一个 Class 对象呢?是这样的,当我们 new 一个新对象或者引用静态成员变量时,Java 虚拟机(JVM)中的类加载器子系统会将对应 Class 对象加载到 JVM 中,然后 JVM 再根据这个类型信息相关的 Class 对象创建我们需要实例对象或者提供静态变量的引用值。需要特别注意的是,手动编写的每个 Class 类,无论创建多少个实例对象,在 JVM 中都只有一个 Class 对象,即在内存中每个类有且只有一个相对应的 Class 对象,挺拗口,通过下图理解(内存中的简易现象图):

到这我们也就可以得出以下几点信息:

1、Class 类也是类的一种,与 Class 关键字是不一样的。

2、手动编写的类被编译后会产生一个 Class 对象,其表示的是创建的类的类型信息,而且这个 Class 对象保存在同名.class 的文件中(字节码文件),比如:创建一个 Shapes 类,编译 Shapes 类后就会创建其包含 Shapes 类相关类型信息的 Class 对象,并保存在 Shapes.class 字节码文件中。

3、每个通过关键字 Class 标识的类,在内存中有且只有一个与之对应的 Class 对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个 Class 对象。

4、Class 类只存在私有构造函数,因此对应 Class 对象只能有 JVM 创建和加载。

5、Class 类的对象作用是:运行时提供或获得某个对象的类型信息,这点对于反射技术很重要(关于反射稍后分析)。

1.2、Class 对象的加载及其获取方式

1.2.1  Class 对象的加载

前面我们已提到过,Class 对象是由 JVM 加载的,那么其加载时机是什么?

实际上所有的类都是在对其第一次使用时,动态加载到 JVM 中的,当程序创建第一个对类的静态成员引用时,就会加载这个被使用的类(实际上加载的就是这个类的字节码文件),注意,使用 new 操作符创建类的新实例对象也会被当作对类的静态成员的引用(构造函数也是类的静态方法),由此看来 Java 程序在它们开始运行之前并非被完全加载到内存的,其各个部分是按需加载,所以在使用该类时,类加载器首先会检查这个类的 Class 对象是否已被加载(类的实例对象创建时依据 Class 对象中类型信息完成的),如果还没有加载,默认的类加载器就会先根据类名查找 .class 文件(编译后 Class 对象被保存在同名的 .class 文件中),在这个类的字节码文件被加载时,它们必须接受相关验证,以确保其没有被破坏并且不包含不良的 Java 代码(这是 Java 的安全机制检测),完全没有问题后就会被动态加载到内存中,此时相当于 Class 对象也就被载入内存了(毕竟 .class 字节码文件保存的就是 Class 对象),同时也就可以被用来创建这个类的所有实例对象。下面通过一个简单例子来说明 Class 对象被加载的时机问题(例子引用自 Thinking in Java):

class Candy {
  static {   System.out.println("Loading Candy"); }
}

class Gum {
  static {   System.out.println("Loading Gum"); }
}

class Cookie {
  static {   System.out.println("Loading Cookie"); }
}

public class SweetShop {
  public static void print(Object obj) {
	System.out.println(obj);
  }

  public static void main(String[] args) {  
	print("inside main");
	new Candy();

	print("After creating Candy");

	try {
	  Class.forName("com.zju.Gum");
	} catch(ClassNotFoundException e) {
	  print("Couldn't find Gum");
	}

	print("After Class.forName(\"com.zju.Gum\")");

	new Cookie();
	print("After creating Cookie");
  }
}

在上述代码中,每个类 Candy、Gum、Cookie 都存在一个 static 语句,这个语句会在类第一次被加载时执行,这个语句的作用就是告诉我们该类在什么时候被加载,执行结果:

inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("com.zju.Gum")
Loading Cookie
After creating Cookie

从结果来看,new 一个 Candy 对象和 Cookie 对象,构造函数将被调用,属于静态方法的引用,Candy 类的 Class 对象和 Cookie 的 Class 对象肯定会被加载,毕竟 Candy 实例对象的创建依据其 Class 对象。比较有意思的是:

Class.forName("com.zju.Gum");

其中 forName 方法是 Class 类的一个 static 成员方法,记住所有的 Class 对象都源于这个 Class 类,因此 Class 类中定义的方法将适应所有 Class 对象。这里通过 forName 方法,我们可以获取到 Gum 类对应的 Class 对象引用。从打印结果来看,调用 forName 方法将会导致 Gum 类被加载(前提是 Gum 类从来没有被加载过)。

1.2.2  Class.forName 方法

通过上述的案例,我们也就知道 Class.forName() 方法的调用将会返回一个对应类的 Class 对象因此如果我们想获取一个类的运行时类型信息并加以使用时,可以调用 Class.forName() 方法获取 Class 对象的引用,这样做的好处是无需通过持有该类的实例对象引用而去获取 Class 对象。如下的第 2 种方式是通过一个实例对象获取一个类的 Class 对象,其中的getClass() 是从顶级类 Object 继承而来的,它将返回表示该对象的实际类型的 Class 对象引用。

public static void main(String[] args) {

	try{
		// 通过Class.forName获取Gum类的Class对象
		Class clazz = Class.forName("com.zju.Gum");
		System.out.println("forName=clazz:" + clazz.getName());
	}catch (ClassNotFoundException e){
		e.printStackTrace();
	}

	// 通过实例对象获取Gum的Class对象
	Gum gum = new Gum();
	Class clazz2 = gum.getClass();
	System.out.println("new = clazz2:" + clazz2.getName());
}

注意调用 forName 方法时需要捕获一个名称为 ClassNotFoundException 的异常,因为 forName 方法在编译器是无法检测到其传递的字符串对应的类是否存在的,只能在程序运行时进行检查,如果不存在就会抛出 ClassNotFoundException 异常。

1.2.3  Class 字面常量

在 Java 中存在另一种方式来生成 Class 对象的引用,它就是 Class 字面常量,如下:

// 字面常量的方式获取Class对象
Class clazz = Gum.class;

这种方式相对前面两种方法更加简单,更安全。因为它在编译期就会受到编译器的检查,同时由于无需调用 forName 方法效率也会更高,因为通过字面量的方法获取 Class 对象的引用不会自动初始化该类。更加有趣的是字面常量的获取 Class对象引用方式不仅可以应用于普通的类,也可以应用于:接口,数组以及基本数据类型,这点在反射技术应用传递参数时很有帮助,关于反射技术稍后会分析,由于基本数据类型还有对应的基本包装类型,其包装类型有一个标准字段 TYPE,而这个TYPE 就是一个引用,指向基本数据类型的 Class 对象,其等价转换如下,一般情况下更倾向使用 .class 的形式,这样可以保持与普通类的形式统一。

boolean.class = Boolean.TYPE;
char.class = Character.TYPE;
byte.class = Byte.TYPE;
short.class = Short.TYPE;
int.class = Integer.TYPE;
long.class = Long.TYPE;
float.class = Float.TYPE;
double.class = Double.TYPE;
void.class = Void.TYPE;

前面提到过,使用字面常量的方式获取 Class 对象的引用不会触发类的初始化,这里我们可能需要简单了解一下类加载的过程,如下:

1、加载:类加载过程的第一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个 Class 对象;

2、链接:验证字节码的安全性和完整性,准备阶段正式为静态域分配存储空间,注意:此时只是分配静态成员变量的存储空间,不包含实例成员变量,如果必要的话,解析这个类创建的对其他类的所有引用

3、初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量。

由此可知,我们获取字面常量的 Class 引用时,触发的应该是加载阶段,因为在这个阶段 Class 对象已创建完成,获取其引用并不困难,而无需触发类的最后阶段初始化。下面通过小例子来验证这个过程:

import java.util.*;

class Initable {
	// 编译期静态常量
	static final int staticFinal = 47;
	// 非编期静态常量
	static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);
	static {
		System.out.println("Initializing Initable");
	}
}

class Initable2 {
	// 静态成员变量
	static int staticNonFinal = 147;
	static {
		System.out.println("Initializing Initable2");
	}
}

class Initable3 {
	// 静态成员变量
	static int staticNonFinal = 74;
	static {
		System.out.println("Initializing Initable3");
	}
}

public class ClassInitialization {
	public static Random rand = new Random(47);
	public static void main(String[] args) throws Exception {
	// 字面常量获取方式获取Class对象
	Class initable = Initable.class;
	System.out.println("After creating Initable ref");
	// 不触发类初始化
	System.out.println(Initable.staticFinal);
	// 会触发类初始化
	System.out.println(Initable.staticFinal2);
	// 会触发类初始化
	System.out.println(Initable2.staticNonFinal);
	// forName方法获取Class对象
	Class initable3 = Class.forName("Initable3");
	System.out.println("After creating Initable3 ref");
	System.out.println(Initable3.staticNonFinal);
	}
}

执行结果:

After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74

从输出结果来看,可以发现,通过字面常量获取方式获取 Initable 类的 Class 对象并没有触发 Initable 类的初始化,这点也验证了前面的分析。

同时发现调用 Initable.staticFinal 变量时也没有触发初始化,这是因为 staticFinal 属于编译期静态常量,在编译阶段通过常量传播优化的方式将 Initable 类的常量 staticFinal 存储到了一个称为 NotInitialization 类的常量池中,在以后对 Initable 类常量 staticFinal 的引用实际都转化为对 NotInitialization 类对自身常量池的引用,所以在编译期后,对编译期常量的引用都将在 NotInitialization 类的常量池获取,这也就是引用编译期静态常量不会触发 Initable 类初始化的重要原因。

但在之后调用了 Initable.staticFinal2 变量后就触发了 Initable 类的初始化,注意 staticFinal2 虽然被 static 和 final 修饰,但其值在编译期并不能确定,因此 staticFinal2 并不是编译期常量,使用该变量必须先初始化 Initable 类。Initable2 和 Initable3 类中都是静态成员变量并非编译期常量,引用都会触发初始化。至于 forName 方法获取 Class 对象,肯定会触发初始化,这点在前面已分析过。到这几种获取 Class 对象的方式也都分析完,ok~,到此这里可以得出小结论:

1、获取 Class 对象引用的方式 3 种,通过继承自 Object 类的 getClass 方法,Class 类的静态方法 forName 以及字面常量的方式”.class”。

2、其中实例类的 getClass 方法和 Class 类的静态方法 forName 都将会触发类的初始化阶段,而字面常量获取 Class 对象的方式则不会触发初始化。

3、初始化是类加载的最后一个阶段,也就是说完成这个阶段后,类也就加载到内存中(Class 对象在加载阶段已被创建),此时可以对类进行各种必要的操作了(如 new 对象,调用静态成员等),注意在这个阶段,才真正开始执行类中定义的 Java 程序代码或者字节码。

关于类加载的初始化阶段,在虚拟机规范严格规定了有且只有 5 种场景必须对类进行初始化:

1、使用 new 关键字实例化对象时、读取或者设置一个类的静态字段(不包含编译期常量)以及调用静态方法的时候,必须触发类加载的初始化过程(类加载过程最终阶段)。

2、使用反射包(java.lang.reflect)的方法对类进行反射调用时,如果类还没有被初始化,则需先进行初始化,这点对反射很重要。

3、当初始化一个类的时候,如果其父类还没进行初始化则需先触发其父类的初始化。

4、当 Java 虚拟机启动时,用户需要指定一个要执行的主类(包含 main 方法的类),虚拟机会先初始化这个主类。

5、当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后解析结果为:REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄对应类没有初始化时,必须触发其初始化(这点看不懂就算了,这是 JDK1.7 的新增的动态语言支持,其关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,这是一个比较大点的话题,这里暂且打住)。

1.2.4  理解泛化的 Class 对象引用

由于 Class 的引用总是指向某个类的 Class 对象,利用 Class 对象可以创建实例类,这也就足以说明 Class 对象的引用指向的对象确切的类型。在 Java SE5 引入泛型后,使用我们可以利用泛型来表示 Class 对象更具体的类型,即使在运行期间会被擦除,但编译期足以确保我们使用正确的对象类型。如下:

public class ClazzDemo {

	public static void main(String[] args){
		// 没有泛型
		Class intClass = int.class;

		// 带泛型的Class对象
		Class<Integer> integerClass = int.class;

		integerClass = Integer.class;

		// 没有泛型的约束,可以随意赋值
		intClass= double.class;

		// 编译期错误,无法编译通过
		// integerClass = double.class
	}
}

从代码可以看出,声明普通的 Class 对象,在编译器并不会检查 Class 对象的确切类型是否符合要求,如果存在错误只有在运行时才得以暴露出来。但是通过泛型声明指明类型的 Class 对象,编译器在编译期将对带泛型的类进行额外的类型检查,确保在编译期就能保证类型的正确性,实际上 Integer.class 就是一个 Class<Integer> 类的对象。面对下述语句,确实可能令人困惑,但该语句确实是无法编译通过的。

// 编译无法通过
Class<Number> numberClass = Integer.class;

我们或许会想 Integer 不就是 Number 的子类吗?然而事实并非这般简单,毕竟 Integer 的 Class 对象并非 Number 的Class 对象的子类,前面提到过,所有的 Class 对象都只来源于 Class 类,看来事实确实如此。当然我们可以利用通配符“?”来解决问题:

Class<?> intClass = int.class;
intClass = double.class;

这样的语句并没有什么问题,毕竟通配符指明所有类型都适用,那么为什么不直接使用 Class 还要使用 Class<?> 呢?这样做的好处是告诉编译器,我们是确实是采用任意类型的泛型,而非忘记使用泛型约束,因此 Class<?> 总是优于直接使用Class,至少前者在编译器检查时不会产生警告信息。当然我们还可以使用 extends 关键字告诉编译器接收某个类型的子类,如解决前面 Number 与 Integer 的问题:

// 编译通过!
Class<? extends Number> clazz = Integer.class;
// 赋予其他类型
clazz = double.class;
clazz = Number.class;

上述的代码是行得通的,extends 关键字的作用是告诉编译器,只要是 Number 的子类都可以赋值。这点与前面直接使用 Class<Number> 是不一样的。实际上,应该时刻记住向 Class 引用添加泛型约束仅仅是为了提供编译期类型的检查从而避免将错误延续到运行时期。

1.2.5  关于类型转换的问题

在许多需要强制类型转换的场景,我们更多的做法是直接强制转换类型:

public class ClassCast {

 public void cast(){

     Animal animal= new Dog();
     // 强制转换
     Dog dog = (Dog) animal;
 }
}

interface Animal{ }

class Dog implements Animal{ }

之所可以强制转换,这得归功于 RRTI,要知道在 Java 中,所有类型转换都是在运行时进行正确性检查的利用 RRTI 进行判断类型是否正确从而确保强制转换的完成,如果类型转换失败,将会抛出类型转换异常。除了强制转换外,在Java SE5 中新增一种使用 Class 对象进行类型转换的方式,如下:

Animal animal = new Dog();
// 这两句等同于Dog dog = (Dog) animal;
Class<Dog> dogType = Dog.class;
Dog dog = dogType.cast(animal)

利用 Class 对象的 cast 方法,其参数接收一个参数对象并将其转换为 Class 引用的类型。这种方式似乎比之前的强制转换更麻烦些,确实如此,而且当类型不能正确转换时,仍然会抛出 ClassCastException 异常。源码如下:

public T cast(Object obj) {
    if (obj != null && !isInstance(obj))
         throw new ClassCastException(cannotCastMsg(obj));
     return (T) obj;
}

1.2.6  instanceof 关键字与 isInstance 方法

关于 instanceof 关键字,它返回一个 boolean 类型的值,意在告诉我们对象是不是某个特定的类型实例。如下,在强制转换前利用 instanceof 检测 obj 是不是 Animal 类型的实例对象,如果返回 true 再进行类型转换,这样可以避免抛出类型转换的异常(ClassCastException):

public void cast2(Object obj){
    if(obj instanceof Animal){
          Animal animal= (Animal) obj;
    }
}

而 isInstance 方法则是 Class 类中的一个 Native 方法,也是用于判断对象类型的,看个简单例子:

public void cast2(Object obj){
	// instanceof关键字
	if(obj instanceof Animal){
		Animal animal= (Animal) obj;
	}

	// isInstance方法
	if(Animal.class.isInstance(obj)){
		Animal animal= (Animal) obj;
	}
}

事实上 instanceOf 与 isInstance 方法产生的结果是相同的。对于 instanceOf 是关键字只被用于对象引用变量,检查左边对象是不是右边类或接口的实例化。如果被测对象是 null 值,则测试结果总是 false。一般形式:

// 判断这个对象是不是这种类型
obj.instanceof(class)

而 isInstance 方法则是 Class 类的 Native 方法,其中 obj 是被测试的对象或者变量,如果 obj 是调用这个方法的 class或接口的实例,则返回 true。如果被检测的对象是 null 或者基本类型,那么返回值是 false。一般形式如下:

// 判断这个对象能不能被转化为这个类
class.inInstance(obj)

最后这里给出一个简单实例,验证 isInstance 方法与 instanceof 等价性:

class A {}

class B extends A {}

public class C {
	
	static void test(Object x) {
		print("Testing x of type " + x.getClass());
		print("x instanceof A " + (x instanceof A));
		print("x instanceof B "+ (x instanceof B));
		print("A.isInstance(x) "+ A.class.isInstance(x));
		print("B.isInstance(x) " + B.class.isInstance(x));
		print("x.getClass() == A.class " + (x.getClass() == A.class));
		print("x.getClass() == B.class " + (x.getClass() == B.class));
		print("x.getClass().equals(A.class)) "+ (x.getClass().equals(A.class)));
		print("x.getClass().equals(B.class)) " + (x.getClass().equals(B.class)));
	}

	public static void main(String[] args) {
		test(new A());
		test(new B());
	} 
}

执行结果:

Testing x of type class com.zju.A
x instanceof A true
x instanceof B false  // 父类不一定是子类的某个类型
A.isInstance(x) true
B.isInstance(x) false
x.getClass() == A.class true
x.getClass() == B.class false
x.getClass().equals(A.class)) true
x.getClass().equals(B.class)) false
---------------------------------------------
Testing x of type class com.zju.B
x instanceof A true
x instanceof B true
A.isInstance(x) true
B.isInstance(x) true
x.getClass() == A.class false
x.getClass() == B.class true
x.getClass().equals(A.class)) false
x.getClass().equals(B.class)) true

到此关于 Class 对象相关的知识点都分析完了,下面将结合 Class 对象的知识点分析反射技术。


2、理解反射技术

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

一直以来反射技术都是 Java 中的闪亮点,这也是目前大部分框架(如 Spring / Mybatis 等)得以实现的支柱。在 Java中,Class 类与 java.lang.reflect 类库一起对反射技术进行了全力的支持。在反射包中,我们常用的类主要有 Constructor 类表示的是 Class 对象所表示的类的构造方法,利用它可以在运行时动态创建对象、Field 表示 Class 对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含 private)、Method 表示 Class 对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含 private)。下面将对这几个重要类进行分别说明。

2.1、Constructor 类及其用法

Constructor 类存在于反射包(java.lang.reflect)中,反映的是 Class 对象所表示的类的构造方法。获取 Constructor 对象是通过 Class 类中的方法获取的,Class 类与 Constructor 相关的主要方法如下:

下面看一个简单例子来了解 Constructor 对象的使用:

  • User 实体类

包含三个构造函数,其中有一个是私有的。

package com.zju.reflect;

public class User {

	private int age;
	private String name;
	
	public User(){
		super();
	}
	
	public User(String name){
		super();
		this.name = name;
	}
	
	// 私有的构造函数
	private User(int age, String name){
		super();
		this.age = age;
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String toString() {
		return "User [age=" + age + ", name=" + name + "]";
	}
	
}
  • 测试类
package com.zju.reflect;

import java.io.Serializable;
import java.lang.reflect.Constructor;

public class ReflectDemo implements Serializable {

	public static void main(String[] args) throws Exception {
		
		Class<?> clazz = null;
		
		// 获取Class对象的引用
		clazz = Class.forName("com.zju.reflect.User");
		
		// 第一种方法:实例化默认构造器
		User user1 = (User) clazz.newInstance();
		user1.setAge(20);
		user1.setName("Tom");
		System.out.println("user1:" +user1.toString());
		
		System.out.println("-----------------------------------------");
		
		// 第二种方法:获取带String参数的public函数
		// String.class指的是参数类型
		Constructor cs1 = clazz.getConstructor(String.class);
		// 创建User,指定User的name
		User user2 = (User) cs1.newInstance("Jerry");
		user2.setAge(30);
		System.out.println("user2:" + user2.toString());
		
		System.out.println("-----------------------------------------");
		
		// 第三种方法:取得指定带int和String参数的构造函数,该方法是私有的构造函数private
		Constructor cs2 = clazz.getDeclaredConstructor(int.class, String.class);
		// 由于是private,所以必须先设置可访问
		cs2.setAccessible(true);
		// 创建User对象
		User user3 = (User) cs2.newInstance(40, "David");
		System.out.println("user3:" + user3.toString());
		
		System.out.println("-----------------------------------------");
		
		// 获取所有的构造函数对象,包括private
		Constructor<?> cons[] = clazz.getDeclaredConstructors();
		// 查看每个构造函数方法需要的参数
		for(int i = 0; i < cons.length; i++){
			// 获取构造函数参数类型
			Class<?> clazzs[] = cons[i].getParameterTypes();
			
			System.out.println("构造函数["+i+"]:" + cons[i].toString());
			System.out.print("参数类型["+i+"]:(");
			
			for (int j = 0; j < clazzs.length; j++) {
				if(j == clazzs.length - 1){
					System.out.print(clazzs[j].getName());
				}else{
					System.out.print(clazzs[j].getName() + ",");
				}
			}
			System.out.println(")");
		}
	}
}
  • 运行结果

关于 Constructor 类本身一些常用方法如下(仅部分,其他可查API):

代码演示如下:

Constructor cs3 = clazz.getDeclaredConstructor(int.class, String.class);

System.out.println("-----getDeclaringClass-----");
Class uclazz = cs3.getDeclaringClass();
// Constructor对象表示的构造方法的类
System.out.println("构造方法的类:" + uclazz.getName());

System.out.println("-----getGenericParameterTypes-----");
// 表示此 Constructor 对象所表示的方法的形参类型
Type[] tps=cs3.getGenericParameterTypes();
for (Type tp : tps) {
	System.out.println("参数名称tp:" + tp);
}

System.out.println("-----getParameterTypes-----");
// 获取构造函数参数类型
Class<?> clazzs[] = cs3.getParameterTypes();
for (Class claz:clazzs) {
	System.out.println("参数名称:" + claz.getName());
}
System.out.println("-----getName-----");
// 以字符串形式返回此构造方法的名称
System.out.println("getName:" + cs3.getName());

System.out.println("-----getoGenericString-----");
// 返回描述此 Constructor 的字符串,其中包括类型参数。
System.out.println("getoGenericString():" + cs3.toGenericString());

/**
 输出结果:
 -----getDeclaringClass-----
 构造方法的类:reflect.User
 -----getGenericParameterTypes-----
 参数名称tp:int
 参数名称tp:class java.lang.String
 -----getParameterTypes-----
 参数名称:int
 参数名称:java.lang.String
 -----getName-----
 getName:reflect.User
 -----getoGenericString-----
 getoGenericString():private reflect.User(int,java.lang.String)
 */

其中关于 Type 类型这里简单说明一下,Type 是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。getGenericParameterTypes 与 getParameterTypes 都是获取构成函数的参数类型,前者返回的是 Type 类型,后者返回的是 Class 类型,由于 Type 顶级接口,Class 也实现了该接口,因此 Class 类是Type 的子类,Type 表示的全部类型而每个 Class 对象表示一个具体类型的实例,如 String.class 仅代表 String 类型。由此看来 Type 与 Class 表示类型几乎是相同的,只不过 Type 表示的范围比 Class 要广得多而已。当然 Type 还有其他子类,如:

TypeVariable:表示类型参数,可以有上界,比如:T extends Number

ParameterizedType:表示参数化的类型,有原始类型和具体的类型参数,比如:List<String>

WildcardType:表示通配符类型,比如:?, ? extends Number, ? super Integer

  • Type 接口源码
package java.lang.reflect;

public interface Type {
}

通过以上的分析,对于 Constructor 类已有比较清晰的理解,利用好 Class 类和 Constructor 类,我们可以在运行时动态创建任意对象,从而突破必须在编译期知道确切类型的障碍。

2.2、Field 类及其用法

Field 提供有关类或接口的单个字段的信息,以及对它的动态访问权限。反射的字段可能是一个类(静态)字段或实例字段。同样的道理,我们可以通过 Class 类的提供的方法来获取代表字段信息的 Field 对象,Class 类与 Field 对象相关方法如下:

下面的代码演示了上述方法的使用过程:

package com.zju.reflect;

import java.lang.reflect.Field;

public class ReflectField {

	public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, SecurityException {
		
		// 获取Student的类对象
		Class<?> clazz = Class.forName("com.zju.reflect.Student");
		
		// 获取指定字段名称的Filed类,注意字段修饰符必须是public,且该字段存在
		Field field = clazz.getField("age");
		System.out.println("field:" + field);
		
		// 获取所有修饰符为public的字段,包含父类字段,注意修饰符为public才会获取
		Field fields[] = clazz.getFields();
		for (Field f : fields) {
			System.out.println("f:" + f.getDeclaringClass());
		}
		
		System.out.println("================getDeclaredFields====================");
		
		// 获取当前类所有字段(包括private字段,但是不包含父类字段)
		Field fields2[] = clazz.getDeclaredFields();
		for(Field f: fields2){
			System.out.println("f2:" + f.getDeclaringClass());
		}
		
		// 获取指定字段名称的Field类,可以是任意修饰符,但不包含父类字段
		Field field3 = clazz.getDeclaredField("desc");
		System.out.println("field3:" + field3);
	}
}

class Person{
	
	public int age;
	public String name;
	
	public int getAge() {
		return age;
	}
	
	public void setAge(int age) {
		this.age = age;
	}
	
	public String getName() {
		return name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
}

class Student extends Person{
	
	public String desc;
	private int score;
	
	public String getDesc() {
		return desc;
	}
	
	public void setDesc(String desc) {
		this.desc = desc;
	}
	
	public int getScore() {
		return score;
	}
	
	public void setScore(int score) {
		this.score = score;
	}
}

运行结果:

上述方法需要注意的是,如果我们不期望获取其父类的字段,则需使用 Class 类的 getDeclaredField / getDeclaredFields 方法来获取字段即可,倘若需要连带获取到父类的字段,那么请使用 Class 类的 getField / getFields,但是也只能获取到 public 修饰的的字段,无法获取父类的私有字段。下面将通过 Field 类本身的方法对指定类属性赋值,代码演示如下:

// 获取Class对象引用
Class<?> clazz = Class.forName("reflect.Student");

Student st= (Student) clazz.newInstance();
// 获取父类public字段并赋值
Field ageField = clazz.getField("age");
ageField.set(st,18);
Field nameField = clazz.getField("name");
nameField.set(st,"Lily");

// 只获取当前类的字段,不获取父类的字段
Field descField = clazz.getDeclaredField("desc");
descField.set(st,"I am student");
Field scoreField = clazz.getDeclaredField("score");
// 设置可访问,score是private的
scoreField.setAccessible(true);
scoreField.set(st,88);
System.out.println(st.toString());

// 输出结果:Student{age=18, name='Lily ,desc='I am student', score=88} 

// 获取字段值
System.out.println(scoreField.get(st));  // 88

其中的 set(Object obj, Object value) 方法是 Field 类本身的方法,用于设置字段的值,而 get(Object obj) 则是获取字段的值,当然关于 Field 类还有其他常用的方法如下:

上述方法可能是较为常用的,事实上在设置值的方法上,Field 类还提供了专门针对基本数据类型的方法,如setInt()/getInt()、setBoolean()/getBoolean、setChar()/getChar()等等方法,这里就不全部列出了,需要时查API文档即可。需要特别注意的是被 final 关键字修饰的 Field 字段是安全的,在运行时可以接收任何修改,但最终其实际值是不会发生改变的。

2.3、Method类及其用法

Method 提供关于类或接口上单独某个方法(以及如何访问该方法)的信息,所反映的方法可能是类方法或实例方法(包括抽象方法)。下面是 Class 类获取 Method 对象相关的方法:

同样通过案例演示上述方法:

package com.zju.reflect;

import java.lang.reflect.Method;

public class ReflectMethod {

	public static void main(String[] args) throws Exception {
		
		// 获取Circle的Class类对象
		Class clazz = Class.forName("com.zju.reflect.Circle");
		
		// 根据参数获取public的Method,包含继承自父类的方法
		Method method = clazz.getMethod("draw", int.class, String.class);
		System.out.println("method:" + method);

		// 获取所有的public方法
		Method[] methods = clazz.getMethods();
		for(Method m1 : methods){
			System.out.println("m1:" + m1);
		}
		
		System.out.println("==========================================");
		
		// 获取当前类的方法包含private,该方法无法获取继承自父类的method
		Method method1 = clazz.getDeclaredMethod("drawCircle");
		System.out.println("method1:" + method1);
		
		// 获取当前类的所有方法包含private,该方法无法获取继承自父类的method
		Method[] methods2 = clazz.getDeclaredMethods();
		for(Method m2 : methods2){
			System.out.println("m2:" + m2);
		}
	}
}

class Shape{
	
	public void draw(){
		System.out.println("draw");
	}
	
	public void draw(int count, String name){
		System.out.println("draw:" + name + ", count = " + count);
	}
}

class Circle extends Shape{
	
	private void drawCircle(){
		System.out.println("drawCircle");
	}
	
	public int getAllCount(){
		return 100;
	}
}

运行结果:

在通过 getMethods 方法获取Method对象时,会把父类的方法也获取到,如上的输出结果,把 Object 类的方法都打印出来了。而 getDeclaredMethod/getDeclaredMethods 方法都只能获取当前类的方法。我们在使用时根据情况选择即可。下面将演示通过 Method 对象调用指定类的方法:

Class clazz = Class.forName("reflect.Circle");
// 创建对象
Circle circle = (Circle) clazz.newInstance();

// 获取指定参数的方法对象Method
Method method = clazz.getMethod("draw",int.class,String.class);

// 通过Method对象的invoke(Object obj,Object... args)方法调用
method.invoke(circle,15,"圈圈");

// 对私有无参方法的操作
Method method1 = clazz.getDeclaredMethod("drawCircle");
// 修改私有方法的访问标识
method1.setAccessible(true);
method1.invoke(circle);

// 对有返回值得方法操作
Method method2 =clazz.getDeclaredMethod("getAllCount");
Integer count = (Integer) method2.invoke(circle);
System.out.println("count:"+count);

/**
	输出结果:
	draw 圈圈,count=15
	drawCircle
	count:100
*/

在上述代码中调用方法,使用了Method类的 invoke(Object obj,Object... args) 第一个参数代表调用的对象,第二个参数传递的调用方法的参数。这样就完成了类方法的动态调用。

getReturnType 方法 /getGenericReturnType 方法都是获取 Method 对象表示的方法的返回类型,只不过前者返回的Class 类型后者返回的 Type (前面已分析过),Type 就是一个接口而已,在 Java8 中新增一个默认的方法实现,返回的就参数类型信息:

public interface Type {
    // 1.8新增
    default String getTypeName() {
        return toString();
    }
}

而getParameterTypes/getGenericParameterTypes也是同样的道理,都是获取Method对象所表示的方法的参数类型,其他方法与前面的Field和Constructor是类似的。

2.4、反射包中的 Array 类

在 Java 的 java.lang.reflect 包中存在着一个可以动态操作数组的类,Array,它提供了动态创建和访问 Java 数组的方法。Array 允许在执行 get 或 set 操作进行取值和赋值。在 Class 类中与数组关联的方法是:

java.lang.reflect.Array 中的常用静态方法如下:

下面通过一个简单例子来演示这些方法:

package reflect;

import java.lang.reflect.Array;

public class ReflectArray {

	public static void main(String[] args) throws ClassNotFoundException {
		int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
		// 获取数组类型的Class 即int.class
		Class<?> clazz = array.getClass().getComponentType();
		// 创建一个具有指定的组件类型和长度的新数组。
		// 第一个参数:数组的类型,第二个参数:数组的长度
		Object newArr = Array.newInstance(clazz, 15);
		// 获取原数组的长度
		int co = Array.getLength(array);
		// 赋值原数组到新数组
		System.arraycopy(array, 0, newArr, 0, co);
		for (int i:(int[]) newArr) {
			System.out.print(i+",");
		}

		// 创建了一个长度为10的字符串数组,
		// 接着把索引位置为6的元素设为"hello world!",然后再读取索引位置为6 的元素的值
		Class clazz2 = Class.forName("java.lang.String");

		// 创建一个长度为10的字符串数组,在Java中数组也可以作为Object对象
		Object array2 = Array.newInstance(clazz2, 10);

		// 把字符串数组对象的索引位置为6的元素设置为"hello"
		Array.set(array2, 6, "hello world!");

		// 获得字符串数组对象的索引位置为5的元素的值
		String str = (String)Array.get(array2, 6);
		System.out.println();
		System.out.println(str);//hello
	}
	/**
	 输出结果:
	 1,2,3,4,5,6,7,8,9,0,0,0,0,0,0,
	 hello world!
	 */
}

通过上述代码演示,确实可以利用 Array 类和反射相结合动态创建数组,也可以在运行时动态获取和设置数组中元素的值,其实除了上的 set/get 外 Array 还专门为 8 种基本数据类型提供特有的方法,如 setInt/getInt、setBoolean/getBoolean,其他依次类推,需要使用是可以查看 API 文档即可。除了上述动态修改数组长度或者动态创建数组或动态获取值或设置值外,可以利用泛型动态创建泛型数组如下:

/**
  * 接收一个泛型数组,然后创建一个长度与接收的数组长度一样的泛型数组,
  * 并把接收的数组的元素复制到新创建的数组中,
  * 最后找出新数组中的最小元素,并打印出来
  */
 public  <T extends Comparable<T>> void min(T[] a) {
	 // 通过反射创建相同类型的数组
	 T[] b = (T[]) Array.newInstance(a.getClass().getComponentType(), a.length);
	 for (int i = 0; i < a.length; i++) {
		 b[i] = a[i];
	 }
	 T min = null;
	 boolean flag = true;
	 for (int i = 0; i < b.length; i++) {
		 if (flag) {
			 min = b[i];
			 flag = false;
		 }
		 if (b[i].compareTo(min) < 0) {
			 min = b[i];
		 }
	 }
	 System.out.println(min);
}

毕竟我们无法直接创建泛型数组,有了 Array 的动态创建数组的方式这个问题也就迎刃而解了。

// 无效语句,编译不通
T[] a = new T[];

ok~,到这反射中几个重要并且常用的类我们都基本介绍完了,但更重要是,我们应该认识到反射机制并没有什么神奇之处。当通过反射与一个未知类型的对象打交道时,JVM 只会简单地检查这个对象,判断该对象属于那种类型,同时也应该知道,在使用反射机制创建对象前,必须确保已加载了这个类的 Class 对象,当然这点完全不必由我们操作,毕竟只能 JVM 加载,但必须确保该类的 ”.class” 文件已存在并且 JVM 能够正确找到。关于 Class 类的方法在前面我们只是分析了主要的一些方法,其实 Class 类的 API 方法挺多的,建议查看一下 API 文档,浏览一遍,有个印象也是不错的选择,这里仅列出前面没有介绍过又可能用到的 API:

/** 
  *    修饰符、父类、实现的接口、注解相关 
  */

// 获取修饰符,返回值可通过Modifier类进行解读
public native int getModifiers();
// 获取父类,如果为Object,父类为null
public native Class<? super T> getSuperclass();
// 对于类,为自己声明实现的所有接口,对于接口,为直接扩展的接口,不包括通过父类间接继承来的
public native Class<?>[] getInterfaces();
// 自己声明的注解
public Annotation[] getDeclaredAnnotations();
// 所有的注解,包括继承得到的
public Annotation[] getAnnotations();
// 获取或检查指定类型的注解,包括继承得到的
public <A extends Annotation> A getAnnotation(Class<A> annotationClass);
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass);

/** 
  *   内部类相关
  */
// 获取所有的public的内部类和接口,包括从父类继承得到的
public Class<?>[] getClasses();
// 获取自己声明的所有的内部类和接口
public Class<?>[] getDeclaredClasses();
// 如果当前Class为内部类,获取声明该类的最外部的Class对象
public Class<?> getDeclaringClass();
// 如果当前Class为内部类,获取直接包含该类的类
public Class<?> getEnclosingClass();
// 如果当前Class为本地类或匿名内部类,返回包含它的方法
public Method getEnclosingMethod();

/** 
  *    Class对象类型判断相关
  */
// 是否是数组
public native boolean isArray();  
// 是否是基本类型
public native boolean isPrimitive();
// 是否是接口
public native boolean isInterface();
// 是否是枚举
public boolean isEnum();
// 是否是注解
public boolean isAnnotation();
// 是否是匿名内部类
public boolean isAnonymousClass();
// 是否是成员类
public boolean isMemberClass();
// 是否是本地类
public boolean isLocalClass(); 

猜你喜欢

转载自blog.csdn.net/pcwl1206/article/details/86556091