1. Class类
在上一篇博客《反射1:概述》中,我们简单介绍了反射的概念、应用背景、以及通过反射技术实现应用程序功能扩展的步骤。从中我们了解到,反射技术的作用就是获取到指定类的各种成员信息。那么在介绍获取成员信息的方法以前,我们先介绍一个非常关键的类——Class类。
1.2 Class类简介
根据面向对象的思想,万事万物都是对象。因此我们可以将现实生活中的事物向上抽取,对其进行抽象描述,这一抽象描述的在Java语言中的表现形式就是类,一种事物对应一个类。Java语言中除了对一般的事物进行描述以外,还定义了对字节码文件这类事物进行描述的类——Class。可能有朋友会问,Java中已经通过File类对一般的文件进行过描述了,那么字节码文件同样是文件,为什么还要再进行重复描述呢?因为字节码文件比较特殊,是Java的运行文件,因此需要单独对其进行描述和解析。
按照对一般事物的抽象过程,首先要找到字节码文件的共性特征。比如,都具有类名、构造方法、成员变量以及成员方法等内容。将这些共性内容向上抽取以后,就定义了用于描述字节码文件这一类事物的类——Class。创建某个类对应Class实例对象,那么此对象中则封装了该类的所有信息,比如类名、包名、父类信息、方法、字段、构造方法等等。通过Class类对外提供的各种get方法即可获取到这些信息。那么对于构造方法和方法来说,不仅可以获取到相关描述,还可以对它们进行调用。因此反射技术实现的基础就是依靠Class类实例对象来完成的,只要获取到Class类实例对象即可获取对应类的所有信息。
1.3 Class类实例对象
既然Class表示一种类,那么该如何去理解Class的实例对象呢?API文档中对Class类的描述为:Class类的实例表示正在运行的Java应用程序的类和接口。Class没有公共构造方法。Class对象是在加载类时由Java虚拟机以及通过调用类加载器中的difineClass方法自动构造的。由此我们明确了,Class的实例对象是不能通过“new”关键字创建出来的,而是在加载某个类的过程中由Java虚拟机创建的。实际上,当Java虚拟机加载某个类的过程中,会将硬盘中对应的“.class”转换为字节码存储到内存中,那么每一份字节码就是一个Class的实例对象。若需要的类字节码已经存在于内存中,则直接获取即可,否则虚拟机就会将硬盘中的类字节码文件加载到内存中,获得类的字节码,以后在需要相同字节码时,就不再需要加载了。关于类加载器的内容,我们将在后面的博客中做介绍,这里大家只要知道类加载器的作用就是将硬盘中某个类的“.class”文件,通过字节读取流的方式读取到内存中来就可以了。
大家需要区分的是,Class类的实例对象并非是某个类的实例对象。比如通过“People p = new People();”语句创建了一个People对象,这是People类的实例对象。而当我们通过某种方法(这种方法我们下面就会说到),获取到表示People.class文件在内存中的字节码时,这依旧是一个Class对象,而不是People对象,只不过其中存储的是People类的信息。换句话说,每加载一个类并获取其字节码时,都会创建一个Class对象,而不同Class对象之间的区别就是存储了不同类的信息。
下面我们就分别来介绍获取Class对象,或者说获取类字节码的三种方式。
1.4 获取Class对象的三种方法
为了演示获取某个类Class对象的方法,我们首先自定义一个Person类,代码如下,
代码1:
public class Person {
//定义字段
private int age;
private String name;
//含参构造方法
public Person(String name, int age){
super();
this.age = age;
this.name = name;
System.out.println(this.name+"...Person paramConstructorrun..."+this.age);
}
//无参构造方法
public Person(){
super();
System.out.println("Personconstructor run.");
}
//一般无参方法
public void show(){
System.out.println(name+"...show()..."+age);
}
public void method(){
System.out.println("...method()...");
}
//私有方法
private void privateMethod(){
System.out.println("...privateMethod()...");
}
//含参方法
public void paramMethod(String str, int num){
System.out.println(str+"...paramMethod()..."+num);
}
//静态方法
public static void staticMethod(){
System.out.println("...staticMethod()...");
}
}
下面我们就以上述Person类为例演示获取Class对象的方法。
(1) 通过Object类的getClass()方法获取
public finalClass<?> getClass():返回此Object的运行时类。该方法的返回值类型即为Class类型的对象。一个类的实例对象一定是通过,该类的字节码创建出来的,那么该方法的作用就是返回创建对象的那个字节码。
演示代码如下:
代码2:
public static voidgetClassObject1(){
Person p = new Person();
Class clazz = p.getClass();
Person p1 = new Person();
Class clazz1 = p1.getClass();
System.out.println(clazz == clazz1);
}
在主函数调用该方法的执行结果为:
Person constructor run.
Person constructor run.
true
上述代码中创建了两个Person对象,分别获取了对应的Class对象,由于它们封装的是同一个字节码,因此两个Class类型引用其实指向的是同一个Class对象。通过上述方法虽然可以获取到Class对象,但是前提是必须要在源代码中明确具体的类,并创建该类的实例对象,因此不满足前述通过反射技术实现应用程序功能扩展的要求。
(2) 通过class静态成员变量获取Class对象
每一个数据类型(包括基本数据类型和引用数据类型),都具备一个公有静态成员——class。通过调用类的这一变量即可获取到对应的Class对象。演示代码如下:
代码3:
public static voidgetClassObject2(){
Class clazz = Person.class;
System.out.println(clazz);
Class clazz1 = Person.class;
System.out.println(clazz1);
System.out.println(clazz == clazz1);
}
在主函数调用以上方法的执行结果为:
class test.Person
class test.Person
true
方法二相比于方法一的区别就是不需要创建实例对象,直接通过类名即可获取Class对象,它们的使用场景是不一样的。但是方法二的使用依旧要定义在源代码中,因此它也不是实现反射技术的主要方法。
此外由于基本数据类型不能创建对象,因此这一类型的字节码对象只能通过方法二和方法三获取。通过方法二获取基本数据类型字节码对象的方式与引用类型时相同的,比如,
“Class clazz = int.class;”就获取到了整型类型的字节码对象。
(3) 通过字符串形式的类名获取对应的Class对象
方法三需要调用Class类的一个静态方法——forName(String className)。
public staticClass<?> forName(String className) throws ClassNotFoundException:返回带有给定字符串名的类或接口相关的Class对象。参数列表中的className代表的就是某个类的包括包名在内的全类名。当未能找到指定类名对应的字节码文件时,将抛出ClassNotFoundExcetpion,此时应该检查指定类名是否有拼写错误,或者未指定包名。演示代码如下:
代码4:
public static voidgetClassObject3() throws ClassNotFoundException {
String className = "test.Person";
Class clazz = Class.forName(className);
System.out.println(clazz);
}
在主函数中调用以上方法的结果为:
class test.Person
通过以上方法同样也可以获取到指定类对应的Class对象。可能有朋友会问,这种方法也需要在源代码中指定类名,才可以获取到Class对象,与方法一/二有什么区别呢?关键就在于方法三中指定的类名的形式是字符串,而不是代码。如果能够以字符串的形式指定类名,那么就可以从源代码的外部“输入”类名及信息了——比如通过配置文件。从配置文件读取到字符串形式的全类名以后,通过一个字符串类型的变量记录住,然后传递给forName方法即可。
对于一种特定的类型来说,无论使用以上三种方法中的哪一种获取字节码对象,它们都是指向了同一个对象,可以通过以下代码的执行结果进行验证,
代码5:
class ReflectTest {
public static void main(String[] args) throws Exception {
/*
通过三种不同的方式获取String类型的字节码对象
*/
Class clazz1 = "Hello World!".getClass();
Class clazz2 = String.class;
Class clazz3 =Class.forName("java.lang.String");
System.out.println(clazz1 == clazz2);
System.out.println(clazz2 == clazz3);
}
}
执行结果为:
true
true
需要强调的是,指定的字符串类型的类名一定是全类名。也就是说一定要指定在定义类时在代码开头指定的包名,否则将抛出ClassNotFoundException。可能有朋友认为,如果Person类的包与主函数所在类的包不一致,可以使用“import”关键字对其进行导入操作,那么就不必再指定包名。导包导入的字节码形式的类,但是方法三种需要的是字符串形式的全类名,因此这是两码事。
由以上三种方法的对比可知,方法三具备一定的扩展性,因为它可以接受来自程序外部的指定类名。因此反射技术的实现都是首先通过方法三获取指定类的Class对象的。
1.5 基本数据类型的Class对象
我们除了可以获取到引用数据类型的字节码以外,Java还为九种基本数据类型预定义了字节码对象,包括void。也就是说,只要是数据类型,无论是基本类型还是引用类型,都可以获取到对应的字节码。这里我们以整型int为例进行说明,阅读下面的代码,
代码6:
public class ReflectTest2 {
public static void main(String[] args) throws Exception {
Class intClass = int.class;//整型字节码对象
Class integerClass = Integer.class;//整型包装类字节码对象
Class stringClass = String.class;//字符串类型字节码对象
/*
分别判断以上这三种类型的字节码对象是否是基本数据类型
*/
System.out.println(intClass.isPrimitive());
System.out.println(IntegerClass.isPrimitive());
System.out.println(stringClass.isPrimitive());
//判断整型与其包装类的字节码对象是否相同
System.out.println(intClass== IntegerClass);
//获取包装类对应的基本数据类型字节码对象
System.out.println(intClass== Integer.TYPE);
}
}
以上代码的执行结果为:
true
false
false
false
true
isPrimitive方法的作用就是判断某个字节码对象是否是基本数据类型,显然以上三种类型中只有整型int是基本数据类型的。
此外,基本数据类型和对应的包装类的字节码对象是两种不同的对象,因此两者相等比较运算的结果为false,这一点需要大家注意。
注意到最后一行代码的输出结果为true。这是因为,包装类的TYPE静态字段的作用就是返回这一包装类所对应基本数据类型的字节码对象。
1.6 数组类型的Class对象
数组作为一种单独的类型,Java中也为其定义了字节码对象,同样可以通过class静态字段获取。演示代码如下,
代码7:
public class ReflectTest3 {
public static void main(String[] args) throws Exception {
Class intArrayClass = int[].class;
System.out.println(intArrayClass.getName());//获取数组类型字节码对象的名称
System.out.println(intArrayClass.isPrimitive());//判断是否是基本数据类型
System.out.println(intArrayClass.isArray());//判断是否是数组类型
}
}
以上代码的执行结果为:
[I
false
true
从执行结果为来看,数组类型的字节码对象名称形式为“[”+“类型名”:“[”表示这是一个数组类型,“I”表示这是一个存储有整型类型的数组。
显然数组类型并不是基本数据类型因此调用isPrimitive方法的返回值为false,但可以调用isArray方法进行判断。
2. 通过Class对象创建对应类的实例对象
我们曾经在《面向对象》系列博客中简单介绍过,常规的通过“new”关键字创建对象的过程是:首先通过指定的类名(包括包名),找到对应的字节码文件,并将其加载进内存,同时创建Class对象(字节码对象)。然后调用构造函数,在堆内存中开辟一块空间,用于创建对象。
而实际上,无论是通过传统方法还是反射的方法,上述创建对象的步骤是一样的只是代码表现形式不同。那么如何通过Class对象创建对应类的实例对象呢?需要用到Class类的newInstance()方法。
public T newInstance() throws InstantiationException, IllegalAccessException:创建此Class对象所表示的类的一个新实例。如同用一个带有空参数列表的new表达式实例化该类。如果该类。如果,该Class对象对应的类中没有定义空参数构造方法,将抛出InstantiantionException异常,表示初始化失败。如果定义了空参数构造方法,但是被定义为了私有,那么将抛出IllegalAccessException异常,表示无法访问私有空参数构造方法。
演示代码如下:
代码5:
public static voidcreateNewObject() throws Exception {
String className = "test.Person";
Class<?> clazz = Class.forName(className);
Person person = (Person)clazz.newInstance();
}
在主函数中调用以上方法的结果为:
Person constructor run.
代码5中的前两行代码的作用实际就是,通过类名将对应的字节码文件加载进内存,并创建Class对象,为创建对应类的实例对象做好了准备工作。而第三行代码,就是默认调用类的空参数构造方法,创建实例对象。相比较传统对象创建过程,代码5更为繁琐和复杂,但相应的,其扩展性是非常强大的。
一方面,类名的指定不需要在源代码中写死,只需要定义在配置配置文件中即可;另一方面,通过类名加载对应字节码文件,以及创建字节码文件对应类的实例对象的代码均可预先定义在应用程序中。基于以上两点,通过反射机制创建对象,并调用方法的方式更具扩展性。
当然,创建对象时并不总是通过空参数构造方法完成,很多时候需要调用含参构造方法,为其指定初始化信息。那么这个时候,单单依靠默认调用空参构造方法的newInstance()方法就无法实现了,而需要获取到Class对象对应类的含参构造方法,并对其进行调用。下面我们就来介绍如何获取构造方法,以及其他成员信息的方法。
3. 通过Class对象获取类的成员
Java语言中为类的各个成员,以及其他信息定义了相对应的类,比如构造方法对应的类称为Constructor,代表字段的类是Field,而Method类表示的就是各个成员方法,而这些类的实例对象代表的就是类成员,以及类的各个信息。下面我们就逐一介绍如何获取类的这些信息。
3.1 构造方法
Class中对外提供了以下用于获取构造函数的方法:
public Constructor<T> getConstructor(Class<?>… parameterTypes) throws NoSuchMethodException, SecurityException:该方法返回具有指定参数列表的公有构造方法。由于一个类中通常会定义若干重载构造方法,而区分这些构造方法的唯一方式就是定义不同的参数类表,因此需要指定构造方法所具有的各参数类型的Class(字节码)对象数组。
public Constructor<?>[] getConstructors() throws SecurityException:返回此Class对象对应类的所有公有构造方法。
public Constructor<T> getDeclaredConstructor(Class<?>… parameterTypes) throws NoSuchMethodException, SecurityException:返回具有指定参数列表的构造函数,该构造函数可以是任意访问权限。
public Constructor<?>[] getDeclaredConstructors() throws SecurityException:返回此类对象中的所有构造函数,无论是什么样的访问权限。
以上方法中,如果需要获取到具有指定参数列表的构造函数,那么就要向getConstructor方法中传递所有这些参数类型的Class对象。比如,Person类中定义了含参构造方法“Person(Stirng name, int age)”,若要获取这一构造方法,调用getConstructor方法的形式为“getConstructor(String.class,int.class)”。
注意到以上这些方法的返回值类型为Constructor,顾名思义就是构造器。这是按照面相对象的思想将构造函数这一类事物向上抽取而得的类。不仅构造方法有对应的类,成员变量和成员方法均有对应的类进行描述,在下面的内容中会进行介绍。通过构造方法对象,可以获取此构造方法的名称、修饰符、参数列表以及其他各种信息,而最重要的是提供了通过此构造方法创建所属类实例对象的方法——newInstance(Object… initargs)。
public T newInstance(Object… initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException:使用此Constructor对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例。注意到该方法的参数列表为Object类型的可变参数列表,若传递参数为基本数据类型时,将会进行自动装箱,并向上转型为Object对象。如果想要调用空参数构造方法创建对象时,只需向newInstance方法中传递“null”即可,这实际也是Class类中newInstance方法的底层实现原理。
通过构造函数对象创建对应类对象的演示代码:
代码6:
public static void createNewObject2() throws Exception {
String className = "Person";
Class clazz = Class.forName(className);
//获取具有指定参数列表的构造函数对象
Constructor cons = clazz.getConstructor(String.class,int.class);
//创建构造方法对象所属类的对象,并指定初始化参数
Person person = (Person)cons.newInstance("David", 26);
}
以上代码的执行结果为:
David...PersonparamConstructor run...26
通过以上方法即可调用指定类的含参构造方法创建对象,并为其使用指定的参数进行初始化。那么在定义相应的配置文件时,除了要指定全类名以外,还需要指定向构造函数传递的各个参数的值。需要强调的是,获取某个构造方法对象时,向getConstructor方法传递参数列表中各参数的Class对象,而利用这一构造方法创建对应类的实例对象时,则要传递同一参数列表中各类型的实例对象或基本数据类型值,因此getConstructor方法和newInstance方法,两者的类型需要一一对应才能通过编译并成功运行。由于我们并没有为Constructor对象定义泛型,因此newInstance方法的返回值类型为Object,需要进行强制类型转换,才能得到Person对象。
实际上,当我们调用Class对象的getConstructor方法,传递指定参数列表对应的字节码对象,并返回一个Constructor对象时,编译器并不知道该构造器对象具体是Person类的哪一个构造方法。也就是说,在编写代码是,即使向newInstance方法传递错误的参数,编译器也不会报出任何错误。只有在运行时虚拟机才能判断出这一构造方法所需的参数列表,并与实际传递的参数进行比较,如果不符将抛出运行时异常IllegalArgumentException,并提示传递参数的个数或类型错误。
同样在编译时期,编译器并不知道某个构造器对象是属于哪个类的,因此当没有定义泛型时,调用其newInstance方法的返回值类型为Object。
以下代码演示获取某个类的所有构造方法,在遍历这些构造方法的同时,打印每个构造方法的信息,包括修饰符、构造方法名。以及参数列表,
代码7:
public static void showPersonConstructors() throws Exception {
Class personClass = Class.forName("test.Person");
//获取Person类中所有的公有构造方法
Constructor[] constructors = personClass.getConstructors();
//创建StringBuilder,用于缓存构造方法的信息
StringBuilder sb = new StringBuilder();
//遍历所有构造方法
for(Constructor constructor : constructors){
//获取构造方法字符串形式的修饰符
String modifier = Modifier.toString(constructor.getModifiers());
sb.append(modifier+" ");
//获取构造方法的名称
String constructorName = constructor.getName();
sb.append(constructorName+"(");
//获取每个构造方法的参数列表,并将这些这些参数转换为字符串存储起来
Class[] parameterTypes =constructor.getParameterTypes();
for(int i=0; i<parameterTypes.length; i++){
if(i != parameterTypes.length-1)
sb.append(parameterTypes[i].getSimpleName()+",");
else
sb.append(parameterTypes[i].getSimpleName());
}
sb.append(")");
System.out.println(sb.toString());
//每打印一个构造方法信息以后,清空StringBuilder
sb.replace(0, sb.length(), "");
}
}
执行结果为:
public Person(String,int)
public Person()
3.2 字段
Class类用于获取对应类字段的方法如下:
public Field getField(String name) throws NoSuchFieldException, SecurityException:返回一个Field对象,它反映此Class对象所表示的类或接口的指定公共成员字段。字符串类型参数name表示的就是字段名称。
public Field[] getFields() throws SecurityException:返回一个包含某些Field对象的数组,这些对象反映此Class对象所表示的类或接口的所有可访问公共字段。
public Field getDeclaredField(String name) throws NoSuchFieldException, SecurityException:返回一个Field对象,该对象反映此Class对象所表示的类或接口的指定已声明字段。name参数是一个字符串,它指定所需字段的简称。该方法所返回的成员可以是任意访问权限,但不包括继承的字段。
public Field[] getDeclaredFields() throws SecurityException:返回一个Field对象的数组,这些对象反映此Class对象所表示的类或接口所声明的所有字段。包括公共、保护、默认(包)方位和私有访问,但不包括继承的字段。
以上这些方法的返回值类型为Filed,它是对字段这一类事物进行向上抽取所得的类。该类对象中封装有包括字段名称、类型,以及字段对应的值在内的各种信息,通过Field类提供的方法即可获得这些信息。
Field类中提供了用于获取和设置字段值的方法get(Object obj):
public Object get(Object obj) throws IllegalArgumentException, IllegalAccessException:返回指定对象上此Field表示的字段的值。如果该值是一个基本类型值,则自动将其包装在一个对象中。该方法的参数是封装有此字段的对象,换句话说,调用该方法时需要指定获取哪个对象的字段,这其实和操作通过“new”关键字创建的对象的字段是一样的,都要声明字段所属的对象。
public void set(Object obj, Object value) throwsIllegalArgument, IllegalAccessException:将指定对象变量上此Field对象表示的字段设置为指定的新值。如果底层字段的类型为基本类型,则对新值进行自动解包。第一个参数表示该字段所属的对象,第二个参数代表,设置的新值。
那么我们就尝试使用该方法来获取Person对象的“age”字段的值,演示代码如下:
代码8:
public static voidgetFieldDemo() throws Exception {
String className = "test.Person";
Class clazz = Class.forName(className);
Constructor cons = clazz.getConstructor(String.class,int.class);
Person person = (Person)cons.newInstance("David",26);
//调用getDeclaredField方法,以获取私有字段
Field field = clazz.getDeclaredField("age");
//取消私有字段的访问限制
field.setAccessible(true);
field.set(person, 31);
System.out.println(field+" : "+field.get(obj));
}
以上代码的执行结果为:
David...PersonparamConstructor run...26
private inttest.Person.age : 31
通过以上代码获取到了Person对象的私有成员变量age的值。
代码说明:
(1) 由于age是Person类的私有成员变量,因此使用getField(String name)是无法获得的,该方法只能获得公有字段;
( 2) setAccessible(boolean flag)是继承自父类AccessibleObject的方法,它的作用是可以取消此成员的访问权限限制,使其成为公有成员。由于age成员变量是私有字段,因此即使获取该字段对应的Field对象,也不能对其进行取值或者设置操作(尝试获取/设置时会抛出IllegalAccessException异常)。因此需要提升该字段的访问权限,才能操作该字段。
我们并与建议大家不要随意使用setAccessible方法修改权限,因为该字段既然被定义为了私有,肯定是有原因的。
(3) 这里要提醒大家的是,获取到私有字段对应的Field对象(getDeclaredField方法),和访问Field对象获取字段值(get方法),是两个不同的操作。并不会因为获取到了私有字段Field对象,就代表可以获取私有字段的值了,还是需要修改访问权限的。
(4) Field对象代表的仅仅是某个类中的成员,因为该对象是从某个类的Class对象中获得的,而不同对象的类虽然一样,具有相同的字段,但是不同对象中的字段值可能是不同的,因此Field对象中并没有存储任何值。只有当我们调用其get方法,指定某个对象时,才能返回指定对象中存储的值。
3.3 方法
Class同样提供了4个用于获取对应类的成员方法的函数,它们分别是:
public Method getMethod(String name, Class<?>… parameterTypes) throws NoSuchMethodException, SecurityException:返回一个Method对象,它反映此Class对象所表示的类或接口的指定公共成员方法。name参数是一个String,用于指定所需方法的简称。这一点是与Constructor所不同的,因为构造方法的方法名就是类名,因此不需要指定。不过与构造方法类似的是,为了区分重载方法,需要传递参数列表中参数类型的Class对象数组。parameterTypes参数就是按声明顺序标识该方法形参类型Class对象的一个数组。如果是空参数方法,则传递null即可。
public Method[] getMethods() throws SecurityException:返回一个包含某些Method对象的数组,这些对象反映此Class对象所表示的类或接口的公共方法(包括继承所得方法)。
public Method getDeclaredMethod(String name, Class<?>… parameterTypes) throws NoSuchMethodException, SecurityExceptin:返回一个Method对象,该对象反映此Class对象所表示的类或接口的指定已声明方法(因此不包括继承所得方法),该方法可以具有任意访问权限。
public Method[] getDeclaredMethods() throws SecurityException:返回一个Method对象的一个数组,这些对象反映此Class对象表示的类或接口声明的所有方法,这些方法访问权限任意,但不包括继承所得方法。
以上方法的返回值类型为Method,它是对方法这类事物进行抽象描述所得的类,可以通过该类提供的各种方法,获取对应函数的各种信息,比如修饰符、返回值类型、方法名,以及参数列表等等,大家可以自行查阅Method类的API文档,这里不再赘述。而我们重点要强调的方法是invoke:
public Object invoke(Object obj, Object… args) throws IllegalAccessException, IllegalArgumentException, InvocationException:对带有指定参数的指定对象调用此Method对象表示的底层方法。该方法的作用就是对此Method对象对应的方法进行调用。第一个参数是该方法所属类的实例对象,表示必须要指定调用哪个对象的方法;而第二参数——对象数组代表的就是调用方法所需的参数。若要调用一个静态方法,则第一个参数传递null即可——因为静态方法不需要通过对象即可调用。
对于invoke方法的理解是这样的:Method对象代表的仅仅是一个方法,其内部并没有任何对象信息,要调用这个方法,必须要指定由谁去完成这个方法,因此我们需要在调用invoke方法的同时指定被调用方法的那个对象。
小知识点1:
这里我们再复习一下面向对象的思想。比如,人关门。“关闭”方法应该定义在人身上,还是定义在门身上呢?实际上在人关门这个事件中,人的作用仅仅是通过推的动作下达了一个指令,而真正完成关门动作的是门——它转动门轴,并且碰到门框,并把门锁住了。因此“关闭”动作应该定义在门这个事物上。那么invoke方法就相当于是人发出关闭的指令,但要明确接收指令的对象是谁,换句话说,需要明确需要由谁去完成这个动作。
小知识点2:
JDK1.4版本中由于没有加入可变参数列表,因此在该版本中invoke方法定义如下:
public Object invoke(Object obj, Object[] args):传递参数时,需要定义一个Object类型的数组,存有所需参数。由于参数类型各异,因此定义Object类型数组,可以存储任何任意类型的参数——包括引用数据类型和基本数据类型。
下面我们来演示,获取Person类中所有已声明方法,并显示这些方法一些信息,代码如下:
代码9:
public static void getMethodDemo() throws Exception {
Class clazz = Class.forName("test.Person");
//获取所有已声明方法对象的数组
Method[] methods = clazz.getDeclaredMethods();
for(Method method : methods){
String modifier = Modifier.toString(method.getModifiers());//修饰符
String returnType = method.getReturnType().getSimpleName();//返回值类型
String methodName = method.getName();//方法名
Class[] parameterTypes = method.getParameterTypes();//参数类型数组
StringBuilder sb = new StringBuilder();
for(int x=0; x<parameterTypes.length; x++){
if(x != parameterTypes.length-1)
sb.append(parameterTypes[x].getSimpleName()+",");
else
sb.append(parameterTypes[x].getSimpleName());
}
//将以上信息转换为数组打印
System.out.println(modifier+" "+returnType +" "+methodName+"("+sb.toString()+")");
}
}
以上方法的执行结果为:
private voidprivateMethod()
public voidparamMethod(String, int)
public static void staticMethod()
public void show()
public void method()
以上代码列出了包括私有方法在内的所有已声明方法,及其方法信息。
以下代码演示了,通过Method对象分别调用,空参数方法、含参方法以及私有方法:
代码10:
public static void getMethodDemo2() throws Exception {
Class clazz = Class.forName("test.Person");
Constructor cons = clazz.getConstructor(String.class,int.class);
Person person = (Person)cons.newInstance("David",26);
//空参数方法调用
Method show = clazz.getMethod("show", null);
System.out.println(show.getName()+" : ");
show.invoke(person, null);
//私有方法调用
Method privateMethod = clazz.getDeclaredMethod("privateMethod", null);
privateMethod.setAccessible(true);
System.out.println(privateMethod.getName()+" : ");
privateMethod.invoke(person, null);
//带参数方法调用
Method paramMethod = clazz.getMethod("paramMethod", String.class,int.class);
System.out.println(paramMethod.getName()+" : ");
paramMethod.invoke(person, "Peter", 43);
}
以上方法的执行结果为:
David...PersonparamConstructor run...26
show :
David...show()...26
privateMethod :
...privateMethod()...
paramMethod :
Peter...paramMethod()...43
代码说明:
(1) 通过invoke方法调用Method对象对应方法,需要传递方法所属类的实例对象和实参。这与调用通过“new”关键创建对象的方法是一样的,必须指定调用哪个对象的方法,同时指定实际参数。
(2) 对于私有方法的调用,同样需要通过setAccessible方法取消其访问权限限制,否则会抛出IllegalAccessException异常。
以上内容我们分别演示了通过Class对象的方法获取到对应类的构造方法、字段以及成员方法的方式。除此以外,Class类还提供了用于获取类的其他信息的方法,比如,getInterfaces方法能够返回某个类实现的接口;通过getModifiers方法可以返回类中成员的修饰符;getPackage方法可以返回此类所在的包名等等,而这些方法的返回值也都是代表一类事物的类,并以其实例对象的形式返回。例如,包都是以Package对象的形式返回,Package类就是对包这一类事物的描述;还有专门用于描述修饰符的Modifier类,可以通过该类内部的静态字段获取到代表不同修饰符的整型值。对于这些表示这些信息的类及其获取他们实例对象的方式大家可以自行参考API文档。
4. 数组的反射
4.1 数组类型
在Class类API文档中对于数组的描述为:每个数组属于被映射为Class对象的一个类,所有具有相同元素类型和维数的数组都共享该Class对象。也就是说,元素类型相同,并且维度相同的所有数组实例,它们都共享同一个Class对象。大家要注意的是,这里指的维度并非是数组长度,比如“int[]”是一维整型数组,“double[][]”是二维双精度浮点型数组,等等。我们首先通过下面的代码,帮助大家进一步理解数组类型。
代码11:
public class ReflectTest4{
public static void main(String[] args) {
int[] arr1 = new int[12];
int[] arr2 = new int[1024];
int[][] arr3 = new int[3][5];
double[] arr4 = new double[8];
Class arr1Class = arr1.getClass();
Class arr2Class = arr2.getClass();
Class arr3Class = arr3.getClass();
Class arr4Class = arr4.getClass();
//分别打印以上四个数组对应Class对象的名称
System.out.println(arr1Class);
System.out.println(arr2Class);
System.out.println(arr3Class);
System.out.println(arr4Class);
//分别比较四个Class对象的是否相同
System.out.println(arr1Class == arr2Class);
System.out.println(arr1Class == arr3Class);
System.out.println(arr1Class == arr4Class);
}
}
以上代码的执行结果为:
class [I
class [I
class [[I
class [Ljava.lang.String;
true
false
false
代码说明:
(1) 数组类型Class对象的命名规则,Class类getName方法的API文档对其进行了如下规定:数组类的名称的内部形式为,表示该数组嵌套深度(即维度)的一个或多个“[”字符加元素类型名。元素类型名的编码如下:
Element Type Encoding
boolean Z
byte B
char C
class or interface Lclassname
double D
float F
int I
long J
short S
由此可知,“[I”表示一维整型数组,“[[I”表示二维整形数组,“[Ljava.lang.String”表示一维字符串数组。
(2) Class对象比较结果除了第一个以外,均为false,表明只要类型或维度不同,数组实例的Class对象就是不相同的。
4.2 数组的父类
Class类中对外提供了用于获取某个类父类的方法——getSuperclass,其API文档描述为:
public Class<? super T> getSuperclass():返回表示此Class所表示的实体(类、接口、基本类型或void)的超类的Class。如果此Class表示Object类、一个接口、一个基本类型或void,则返回null。如果此对象表示一个数组类,则返回表示该类Object类的Class对象。由此可知,数组的父类是Object,我们可以通过下面的代码进行验证。
代码12:
public class ReflectTest5 {
public static void main(String[] args) {
int[] arr1 = new int[12];
int[] arr2 = new int[1024];
int[][] arr3 = new int[3][5];
String[] arr4 = new String[8];
Class arr1Class = arr1.getClass();
Class arr2Class = arr2.getClass();
Class arr3Class = arr3.getClass();
Class arr4Class = arr4.getClass();
System.out.println(arr1Class.getSuperclass());
System.out.println(arr3Class.getSuperclass());
System.out.println(arr4Class.getSuperclass());
}
}
代码执行结果为:
class java.lang.Object
class java.lang.Object
class java.lang.Object
由代码执行结果可知,任意类型/维度数组的父类,均为Object。既然如此,就可以多态地使用一个Object引用型变量指向一个数组实例,如下代码所示,
代码13:
public class ReflectTest6 {
public static void main(String[] args) {
int[] arr1 = new int[12];
int[] arr2 = new int[1024];
int[][] arr3 = new int[3][5];
String[] arr4 =new String[8];
Object ojb1 = arr1;
Object obj2 = arr3;
Object obj3 = arr4;
//Object[] obj4 = arr1;
Object[] obj5 = arr3;
Object[] obj6 = arr4;
}
}
将代码13中的第11行注释掉以后,以上代码才能够通过编译,并正常执行。
代码说明:
(1) 如前文所述,任一数组类型的父类均为Object,因此使用Object引用型变量指向一个数组实例,因此8、9、10行代码是能够通过编译,并执行的。
(2) 第11行代码的含义为,将一个整型数组的元素(整型值),向上转型为Object类型对象,并存储到一个Object类型数组中。这显然是行不通的,因为8种基本数据类型是没有父类的,因此不存在向上转型一说。
(3) 可以将二维整型数组理解为,存储元素类型为整型数组的一个数组,因此第12行代码的含义为,将二维整型数组中的整形数组元素,向上转型为Object对象,然后将这些Object对象存储到一个Object类型数组中。
(4) 第13行代码的含义为,将一个字符串类型数组中的元素,向上转型为Object类型对象,并存储到一个Object类型数组中,也是可以通过编译并执行的。
4.3 打印数组中的内容
通常我们可以使用工具类Arrays的静态方法toString,即可按照一定的格式将数组中的元素列出。当然我们也可以手动通过循环打印数组中的元素。
这里我们想提供第三种思路:集合类复写了Object类的toString方法,可以打印集合中的元素。因此我们可以利用Arrays类静态方法asList,向其中传递一个数组,然后将数组拆包,并将其中的元素打包为一个集合返回,并打印。
asList的API文档描述为:
public static <T> List<T> asList(T… a):返回一个受指定数组支持的固定大小的列表。此方法还提供了一个创建固定长度的列表的便捷方法,该列表被初始化为包含多个元素。该方法的作用就是将传入asList方法中的多个参数,整合为一个集合返回。
演示代码如下:
代码14:
import java.util.Arrays;
public class ReflectTest7 {
public static void main(String[] args) {
int[] arr1 = new int[12];
String[] arr4 = new String[]{"Hello!", "Hey!","Hi!"};
System.out.println(Arrays.asList(arr1));
System.out.println(Arrays.asList(arr4));
}
}
执行结果为:
[[I@8c436b]
[Hello!, Hey!, Hi!]
从执行结果来看,整型数组并未被拆包,而是将数组作为一个整体,打包为一个集合了,也就是说这个集合中只存储了一个整型数组。而字符串数组被自定拆包,并将其中的元素打包为一个集合返回,并按照集合定义的toString方法,打印了其中的内容。
那么造成这一现象的原因其实可以使用4.2节中的结论进行解释。JDK1.5为了兼容JDK1.4的语法,当向可变参数列表中传入一个数组的时候,就会将该数组作为一个Object类型数组接收,以代码14为例,就相当于:
Object[] a1 = newint[12];
Object[] a2 = new String[]{"Hello!","Hey!", "Hi!"};
按照前文所述,第二行代码是可以执行的,其原理就是将字符串数组拆包,并将其中的元素打包为一个Object类型数组,因为String类型对象可以向上转型为Object对象。而第一行代码是不能执行的,因为基本数据类型不能转型为Object对象,因此不能按照JDK1.4的语法进行处理,此时该整型数组就直接被向上转型为了一个Object对象,作为可变参数列表中的唯一参数,并被打包为了一个集合,那么该集合中存储元素的类型就是Obejct。那么直接打印出来的结果自然就相当于调用了Object对象,也即数组的hashCode方法。
4.4 数组的反射
Java标准类库中提供了用于实现数组反射的类——Array,该类的API文档描述为:Array类提供了动态创建和方位Java数组的方法。我们通过下面的例子来演示,如何对数组进行反射操作。
需求:定义一个方法,参数列表为一个Object对象。如果该Object对象是一个类类型对象则直接调用其toString方法打印;如果该Object对象是一个数组,则打印其中的元素。
代码15:
import java.lang.reflect.Array;
import java.util.Arrays;
public class ReflectTest8 {
public static void main(String[] args) {
int[] arr1 = new int[12];
int[] arr2 = new int[5];
int[][] arr3 = new int[3][5];
String[] arr4 = new String[]{"Hello!","Hey!", "Hi!"};
printObject(arr1);
System.out.println();
printObject(arr2);
System.out.println();
printObject(arr3);
System.out.println();
printObject(arr4);
}
private static void printObject(Object obj){
Class clazz = obj.getClass();
if(clazz.isArray()){//判断是否是数组
int len = Array.getLength(obj);//获取数组长度
System.out.print("[");
for(int i=0; i<len; i++){//遍历数组
if(i != len-1){
Object obj1 = Array.get(obj, i);
if(obj1.getClass().isArray())//获取数组中元素继续判断是否是数组
//若数组中元素本身就是数组,及递归调用本方法
printObject(obj1);
else
//若数组中元素并非数组,则直接打印该元素
System.out.print(obj1);
System.out.print(",");
}else{
Object obj1 = Array.get(obj, i);
if(obj1.getClass().isArray())
printObject(obj1);
else
System.out.print(obj1);
System.out.print("]");
}
}
}else//如果传入参数不是数组则直接打印该参数
System.out.println(obj);
}
}
执行结果为:
[0,0,0,0,0,0,0,0,0,0,0,0]
[0,0,0,0,0]
[[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]]
[Hello!,Hey!,Hi!]
代码说明:
(1) 首先获取到参数Object对象的Class对象,并判断其是否是数组类型。如果不是数组,则直接打印该对象。
(2) 如果Object对象是数组类型,则循环打印其中的元素。通过Array类的静态方法获取数组长度。
(3) 若想获取数组中某个角标下的元素,则调用Array类的静态方法get,同时传递数组对象和目标角标值即可将元素以Object对象的形式返回。
(4) 以上代码中为了打印多维数组,会判断获取到的元素是否是数组,如果是数组则递归调用方法本身,继续循环打印数组中的元素。
以上代码演示了如何利用反射,动态地操作数组。之所以说是动态地,是因为在编译阶段编译器并不知道被操作的Object对象具体是什么类型,只有在运行时才能判断它是数组实例。然后通过Array类提供的各种静态方法,即可实现对数组的操作。
5. 反射练习
需求1:模拟计算机功能的扩展。最核心是主板的正常运行,并在此基础上添加网卡或声卡等设备,以扩展上网和听音乐的功能。
思路:
以前的做法是,定义一个PCI接口,该接口中定义了调用两个抽象方法,分别是开启和关闭设备。那么后期需要扩展类只要实现此接口,即可被主板所调用。
但是按照以上方法实现功能扩展,还是要事先知道新设备的名称,并且在源代码中就要事先定义好,而这是不符合现实情况的。而对于能够运行的应用程序,我们又无法修改其源代码,也就不能向其添加新设备了。
解决以上问题的方法就是可以利用反射技术。将新设备的类名写入到一个配置文件,程序通过读取这一文件,获取到新设备名称后,利用反射技术创建新设备对象,并调用他们的开启和关闭功能即可。当然这些新设备类还是要实现PCI接口,这样我们才能多态地创建对象。而读取配置、获取新设备类名、创建新设备对象,并调用他们的共有方法等代码可以事先定义好,而不需要在后期修改。
代码11:
/*
定义PCI接口
新设备类只要实现这一接口即可被主板所调用
*/
public interface PCI {
abstract public void open();
abstract public void close();
}
/*
定义两个分别实现了PCI接口的新设备类
*/
public class SoundCard implements PCI {
public void open(){
System.out.println("Sound Open.");
}
public void close(){
System.out.println("Sound close.");
}
}
public class NetCard implements PCI {
public void open() {
System.out.println("net open.");
}
public void close() {
System.out.println("net close.");
}
}
//定义主板类
public class Mainboard {
public void run(){
System.out.println("Mainboard run.");
}
/*
可以多态地接受任意PCI接口的实现类实例对象
并调用它们开启和关闭功能
*/
public void usePCI(PCI pci){
if(pci != null){
pci.open();
pci.close();
}
}
}
/*
定义配置文件——pcis.properties
使用Properties对象,向文件中写入类名键值对,格式如下:
pci1=设备名
pci2=设备名
…
*/
pci1=SoundCard
pci2=NetCard
//测试代码
import java.io.*;
import java.util.Properties;
public class ReflectTest9 {
public static void main(String[] args) throws Exception {
Mainboard mb = new Mainboard();
mb.run();
//将配置文件封装为File对象
File configFile = new File("pcis.properties");
//判断配置文件是否存在
if(!configFile.exists())
configFile.createNewFile();
Properties prop = new Properties();
FileInputStream fis = new FileInputStream(configFile);
//将配置文件中的内容加载到Properties集合中
prop.load(fis);
for(int i=0; i<prop.size(); i++){
//获取配置文件中的每个新设备类名
String pciName = prop.getProperty("pci"+(i+1));
//通过反射创建新设备对象
Class clazz = Class.forName(pciName);
//要保证每个新设备类内部定义了空参构造函数
PCI pci = (PCI)clazz.newInstance();
//主板多态地使用新设备对象
mb.usePCI(pci);
}
fis.close();
}
}
执行测试类ReflectTest后的结果为:
Mainboard run.
Sound Open.
Sound close.
net open.
net close.
通过以上代码,我们就可以在不修改源代码的情况下,创建新类对象,并调用其方法,实现功能的应用程序功能的扩展。
代码说明:
实际开发中配置文件通常是“,xml”格式的,除了可以定义新类类名以外,还可以对其进行更为详细的描述。由于我们还没学习xml语法,因此这里以键值对的形式存储,键的命名具有一定的规律,均已pci开头,后接从1开始的连续数字,这样就可以使用Properties集合读取这些键值对。
需求2:通过反射的方法,创建集合对象,并向集合中存储对象。
思路:
同样还是通过一个配置文件,将需要用到的集合类的类名写入到配置文件中,那么事先定义好的类,比如框架类,只需要读取该配置文件,获取到用户指定的类名,即可创建该类的实例对象,并调用其方法。这样一来,用户无需修改框架类,只需要改动配置文件即可。
代码12:
//配置文件
//文件名:config.properties
//文件内容
className=java.util.ArrayList
//测试类代码
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Properties;
public classReflectTest10 {
public static void main(String[] args) throws Exception {
//创建一个文件字节读取流,并与配置文件关联起来
InputStream inStream = new FileInputStream("config.properties");
//创建Properties对象,并加载配置文件中的键值对
Properties prop = new Properties();
prop.load(inStream);
//获取className键对应的值,也即需要动态调用的类名
String className = prop.getProperty("className");
//反射地创建对象,并调用对象的方法
Class clazz = Class.forName(className);
Collection collection = (Collection)clazz.newInstance();
Person p1 = new Person("David",26);
Person p2 = new Person("Kate",31);
Person p3 = new Person("Peter",19);
Person p4 = new Person("Kate",31);
collection.add(p1);
collection.add(p2);
collection.add(p3);
collection.add(p4);
System.out.println(collection.size());
inStream.close();
}
}
当存储在配置文件中的类名为ArrayList时,执行结果为:
4
若将类名修改为HashSet,执行结果就是:
3
可见修改起来,非常方便,无需修改源代码,即可调用不同类的不同功能。
小知识点2:
以上两个需求中,都用到了配置文件,作为现有类与新类之间进行沟通的桥梁。代码中均使用了相对路径,将一个字节读取流与配置文件进行关联。当然在程序开发阶段这样做是行得通的,但是,当我们开发完成,并将项目发送给用户时,并非把开发程序时所使用的项目文件夹全部发送给对方,而仅仅将能够执行的“.class”文件,以及附带的配置文件发送给用户。而配置文件按照需要,可能会与“.class”文件一同打包到jar中,也可能存放到jar包的外部。但无论配置文件在jar包外还是包内,配置文件与所属项目之间的相对路径通常是固定的,而最终软件的安装目录(相当于项目的根路径)是由用于决定的,并且这一绝对路径相对开发时很可能会发生变化,因此配置文件的绝对路径路径是不能硬编码写入到源代码中的。
对此有两种解决方法:
方法一:JavaWeb框架中提供了一种API,它可以返回该JavaWeb项目所在路径,也就是用户安装软件的根路径,然后只需将该方法返回的路径与配置文件相对该项目的相对路径合并起来,就是配置文件的绝对路径。这样无论用户将程序安装到了什么位置,都可以准确地找到指定的配置文件。而使用这种方法的优点是,不仅可以读取配置文件,而且还能够对其进行修改。
方法二:还可以使用类加载器——ClassLoader——对配置文件进行加载。这种方法我们将在详细介绍类加载器时,进行说明。那么这种方法就只能读取配置文件的内容了。
需求3:将任意一个对象中的所有String类型的成员变量所对应的字符串内容的“b”改成“a”。
思路:
(1) 首先获取到某对象对应类中的所有公有字段,也即公有字段数组;
(2) 在遍历这一字段数组的同时,判断该字段类型是否是字符串;
(3) 如果是字符串,就获取到此对象中这一字段的字符串值;
(4) 通过调用字符串值的replaceAll方法,将所有字母b替换为字母a,包括大小写;
(5) 最后将替换后的字符串字段打印到控制台;
(6) 对声明字段,也即非公有字段进行重复操作,尽可能将该对象中所有字符串字段都进行替换。
代码13:
private static void changeStringValue(Object obj) throws Exception {
Field[] fields = obj.getClass().getFields();
Field field;
String str;
for(int i=0; i<fields.length; i++){
field = fields[i];
if(field.getType()== String.class) {
//由于此时已经知道是String类型,因此可以进行强制类型转换
str = (String)field.get(obj);
//打印修改前的字符串字段
System.out.println("before: "+str);
//将大小写字母b替换为a
field.set(obj, str.replaceAll("b","a").replaceAll("B", "A"));
//打印修改后的字符串字段
System.out.println("after:"+(String)field.get(obj));
}
}
System.out.println();
//对非公有字段进行重复操作
Field[] declaredFields = obj.getClass().getDeclaredFields();
String str1;
Field declaredField;
for(int i=0; i<declaredFields.length; i++){
declaredField = declaredFields[i];
if(declaredField.getType() == String.class){
declaredField.setAccessible(true);
str1 = (String)declaredField.get(obj);
System.out.println("before: "+str1);
declaredField.set(obj,str1.replaceAll("b", "a").replaceAll("B","A"));
System.out.println("after:"+(String)declaredField.get(obj));
}
}
}
大家可以自行尝试以上方法的执行效果。
需要说明的是,在判断字段类型是否是字符串类型时,我们使用了“==”比较运算符,而不是equals方法。这是因为,String类的Class对象在内存中永远只有一份,因此只要字段的Class对象不指向这唯一的对象,那么它肯定不是字符串类型的字段。虽然equals方法也能起到相同的作用,但是表达的语义是不准确的。
需求4:写一个程序,这个程序能够根据用户提供的类名,去执行该类中的main方法。
思路:如果使用一般的方式的话,就是在测试类主函数中,通过类名调用目标类的主函数,如下代码所示,
代码14:
class Demo {
public static void main(String[] args) {
for(String arg : args){
System.out.println(arg);
}
}
}
public class ReflectTest2{
public static void main(String[] args) {
Demo.main(newString[]{"Hello World!", "Hi~", "Hey!"});
}
}
将ReflectTest2作为主类,将其主函数作为程序入口执行,执行结果为:
Hello World!
Hi~
Hey!
以上代码中,调用Demo类主函数的同时,向其中传递了一个字符串数组作为其参数,那么打印结果就是字符串数组中的字符串。虽然以上方法可以用于某个类主函数的调用,但是需要在源代码中就定义好要调用的那个类,因此不具有扩展性——因为代码定义初期我们并不知道,后期新定义的类的名称。
那么我们可以将被调用类的字符串形式类名作为,测试类主函数的参数(字符串数组的元素)。如果使用命令行,可以在通过java命令启动某个类的同时,后接向主函数传递的字符串参数即可,例如“java ReflectTest2 Demo”。但若使用eclipse,则需要在软件中进行参数设定。在测试类代码视图中单击鼠标右键,依次点击“Run As”——>“Run Configurations…”,在打开的“Run Configurations”对话框右侧,选择“Arguments”选项卡,在“Program arguments”文本框中输入被调用类的全类名(包括包名),然后分别单击“Apply”和“Run”即可运行。代码如下,
代码15:
public class ReflectTest3 {
public static void main(String[] args) throws Exception {
String startingClassName = args[0];
Method mainMethod = Class.forName(startingClassName).getMethod("main",String[].class);
//mainMethod.invoke(null, new Object[]{new String[]{"HelloWorld!", "Hi~", "Hey!"}});//方法一
mainMethod.invoke(null, (Object)new String[]{"Hello World!","Hi~", "Hey!"});//方法二
}
}
无论使用方法一还是方法二,执行结果均为:
Hello World!
Hi~
Hey!
代码说明:
(1) 通过字符串数组获取到被调用类的类名以后,通过反射在测试类主函数中调用了目标类的主函数。由于主函数是静态方法,因此invoke方法的第一个参数为null;
(2) 主函数的参数为一个字符串数组,因此invoke方法的第二个参数按理传递一个字符串参数即可。但是实际的运行结果抛出了IllegalArgumentException异常,异常提示为:wrong number of arguments。这是因为JDK1.5为了兼容JDK1.4的语法,如果向定义有可变参数列表的方法传递一个数组,就会自动将该数组拆包,并将其中的元素作为多个参数看待,这一过程其实就是可变参数列表的实现原理。但是这样一来,原来只能接受一个数组参数的位置,却相当于被传递了多个字符串,因此参数数量不符。
对此有两种解决方法:方法一,将含有多个元素的字符串数组,存储到一个Object类型的数组中,这样即使拆包,也只有一个参数——字符串数组;方法二,直接将字符串数组强制类型转换为Object对象,这样就不会进行拆包动作了,因为原来的字符串组数已经不再被作为数组,直接作为单独的一个Object对象。