一般情况下,我们用到某个类时都是直接实例化出一个类对象,而这个类我们都知道的,然后对这个类对象进行一系列操作。如果想在运行期间获得某个类的结构、成员变量、方法等信息,那么就要用到反射了,也用来实例化出一个对象,对其进行一系列对象。
反射基本概念
JAVA反射机制是在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称之为反射机制。
反射机制主要的功能
- 在运行时构造任意类对象
- 在运行时,获取包信息、属性和方法等
- 在运行时,调用类的方法或修改成员变量值等
在面向对象的世界里,万事万物皆对象。在java语言中,静态的成员、普通数据类型是不是对象呢?类是谁的对象呢?类是对象,类是java.lang.Class类的实例对象。在Java 中使用 Class 这个类来表示所有的类。反射的入口的第一步是首先获取类的 Class 对象。
Class类
Class 类用于描述一个类的所有信息,例如构造方法,成员变量,成员方法等,类中每一个元素都有一个对应的类来表示。只列举一些函数表达
- 使用Constructor表示构造函数
- 使用Method表示成员方法
- 使用Field表示成员属性
Class类的常用方法
-
getName()
一个Class对象描述了一个特定类的属性,Class类中最常用的方法getName以String的形式返回此Class对象所表示的实体(类、接口、数组类、基本类型或void名称。
-
getSimpleName()
获取不包含包名的类的名称。
-
newInstance()
Class还有一个有用的方法可以为类创建一个实例对象,这个方法叫做newInstance()。例如:x.getClass.newInstance(),创建了一个同x一样类型的新实例。newInstance()方法调用默认构造器(无参数构造器)初始化新建实例对象。
注意:前提是需要一个无参的构造方法
-
getClassLoader()
返回该类的类加载器。
-
getComponentType()
返回表示数组组件类型的Class。
-
getSuperclass()
返回表示此 Class 所表示的实体(类、接口、基本类型或 void)的超类的Class。
-
isArray()
判定此 Class 对象是否表示一个数组类。
-
getModifiers()
使用Modifier 表示解码 Class 修饰符。使用 Class.getModifiers() 获得调用类的修饰符的二进制值,然后使用 Modifier.toString(int modifiers) 将获取到的二进制值转换为字符串。
获取一个类的Class对象的三种方式:
-
通过类名.class获取
当执行
类名.class
时,JVM会先检查Class对象是否装入内存,如果没有装入内存,则将Class对象装入内存,然后返回Class对象,如果装入内存,则直接返回Class对象。在加载Class对象后,不会对Class对象进行初始化。 -
通过对象.getClass()获取
getClass()方法
的方法是在通过的类的实例调用的,即已经创建了类的实例。 -
通过Class.forName(全类名)获取
当执行
Class.forName()
时,JVM也会先检查Class对象是否装入内存,如果没有装入内存,则将Class对象装入内存,然后返回Class对象,如果装入内存,则直接返回Class对象。在加载Class对象后,会对类进行初始化,即执行类的静态代码块。forName()
方法中的参数是类名字符串,类名字符串 = 包名 + 类名。Class.forName()
的一个很常见的用法是在加载数据库驱动的时候。不仅表示了类的类类型,还代表了动态加载类。注意:编译时刻加载类是静态加载类、运行时刻加载类是动态加载类。
//获取对象 一般直接new出一个对象 Person p = new Person(); //Class类的实例对象,不能直接new出来。任何一个类都是Class的实例对象,这个实例对象有三种表达方 式。 //每个类型,包括基本和引用,都会赋予这个类型的一个静态的属性,属性的名字就叫class。 //实际在告诉我们任何一个类都是有一个隐含的静态成员变量class。 Class c= Person.class; //调用person类的父类的方法getclass,即知道该类的对象通过getClass方法获得 Class c = p.getClass(); //forName 还不知道具体的类 //此处需要注意的是引号里面需要填写完整的类名包括包名 Class c= Class.forName("com.example.reflect.Person"); //通过类的类类型创建该类的对象实例 //Person person = (Person) c.newInstance();//(提前是需要有无参数的构造方法)
Class类的基本的数据类型
Class c1 = int.class;//int的类类型
Class c2 = String.class;
Class c3 = double.class;
Class c4 = Double.class;
Class c5 = void.class;
System.out.println("c1 getName "+c1.getName());//打印类类型的名称
System.out.println("c2 getName "+c2.getName());//打印类的全称
System.out.println("c2 getSimpleName "+c2.getSimpleName());//不包含包名的类的名称
System.out.println("c3 getName "+c3.getName());//打印类类型的名称
System.out.println("c4 getName "+c4.getName());//打印类的全称
System.out.println("c5 getName "+c5.getName());//打印类的全称
//Class.getModifiers() 使用Modifier 表示解码 Class 修饰符。使用 Class.getModifiers() 获得调用类的修饰符的二进制值,然后使用 Modifier.toString(int modifiers) 将获取到的二进制值转换为字符串。
//打印结果
c1 getName int
c2 getName java.lang.String
c2 getSimpleName String
c3 getName double
c4 getName java.lang.Double
c5 getName void
注意:void关键字都存在类类型
Constructor构造器
获取Constructor构造器对象:
-
getConstructor(Class... parameterType) 获取具有指定参数的公共构造器对象
-
getConstructors() 获取所有公共构造器对象
-
getDeclaredConstructor(Class... parameterType)获取具有指定参数的构造器对象
-
getDdclaredConstructors() 获取所有构造器对象
Class c = Person.class; Constructor cons = c.getConstructor(); Person p = (Person) cons.newInstance();//无参构造(提前是需要有无参数的构造方法) p.print(); Constructor cons1 = c.getConstructor(int.class,String.class); Person p1 = (Person) cons1.newInstance(18,"张三");//有参构造 p1.print(); Constructor cons2 = c.getConstructor(int.class,String.class,String.class); Person p2 = (Person) cons2.newInstance(20,"李四","男");//有参构造 p2.print(); //打印结果 {姓名:null,性别:null,年龄:0} {姓名:张三,性别:null,年龄:18} {姓名:李四,性别:男,年龄:20}
Field字段
获取Field字段对象:
- getField(String name) 获取具有指定名称的公共字段对象
- getFields()获取具有所有公共字段对象
- getDeclaredField(String name)获取具有指定名称的字段对象
- getDeclaredFields()获取所有字段对象
Field类主要方法
-
Object get(Object obj) 获取obj对象的此Field对象代表的字段的值
-
void set(Object obj , Object value)设置obj对象的此Field对象代表的字段的值
Constructor cons2 = c.getConstructor(int.class,String.class,String.class); Person p2 = (Person) cons2.newInstance(20,"李四","男"); p2.print();//打印 Field field = c.getDeclaredField("name");//获取name字段 field.setAccessible(true); //开启访问权限 field.set(p2,"小二");////给Person对象的该字段设置值 p2.print();//打印 //打印出的结果 {姓名:李四,性别:男,年龄:20} {姓名:小二,性别:男,年龄:20}
Method对象
获取Method对象:
- getMethod(String name,Class...parameterType)获取具有指定名称和参数的公共方法对象
- getMethods()获取所有公共方法对象,包括父类继承而来的
- getDeclaredMethod(String name,Class...parameterType)获取的是所有该 类自己声明的方法对象
- getDeclaredMethods()获取的是所有该 类自己声明的方法对象,不问访问权限
Person p = (Person) c.newInstance();
Method method = c.getDeclaredMethod("printMethod",String.class);//获取名为"printMethod",参数为string的method对象
method.invoke(p,"我通过反射调用printMethod方法");//调用Person的printMethod方法,并打印
//打印结果
我通过反射调用printMethod方法
在JDK1.4 和 JDk1.5 版本中 Method#invoke 方法的区别
JDK1.5
public Object invoke(Object obj,Object...args)
JDK1.4
public Object invoke(Object obj,Object[] args)
如果是以一个字符串数组传入给 invoke 方法,那么 javac 会按照哪种语法给处理呢?
因为 JDK1.5 需要兼容 JDk1.4 因此会按照 JDK1.4 的语法来执行。由于invoke方法的实现是在native中就不过多的详实了。
@RecentlyNullable
public native Object invoke(@RecentlyNullable Object var1, @RecentlyNullable Object... var2) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
通过Class、Method来认清泛型的本质
ArrayList list1 = new ArrayList();//指任意类型
ArrayList<String> list2 = new ArrayList<>();//指定String类型
list2.add("hello");
//list2.add(100);//错误
Class c1 = list1.getClass();
Class c2 = list2.getClass();
System.out.println(c1==c2);//打印结果为true
c1==c2结果为true,说明编译之后的集合泛型是去泛型化的。java中集合的泛型,是防止错误输入的(list2.add(100)会提示这样添加是错误的
),只在编译阶段有效,绕过了编译就无效了。怎么证明只在编译阶段有效,绕过了编译就无效了是正确的呢?
我们可以通过方法的反射来操作,绕过编译验证。代码如下:
Method m = c2.getDeclaredMethod("add",Object.class);
m.invoke(list2,100);//绕过编译操作就绕过了泛型
//看看是否添加一个int值进入list2集合中
System.out.println(list2);//打印结果为[hello, 100]
打印结果为[hello, 100],说明这个int 100能添加到ArrayList<String>集合中,所以就证明了java中集合
的泛型,是防止错误输入的,只在编译阶段有效,绕过了编译就无效了。所以需要区分编译和运行。
注意: 反射后就不能for(String s : list2)这样遍历了,因为list集合中100是int类型,则会报类异常。
反射中为什么调用方法Method.invoke(Object obj,Object arr),既然传了对象给invoke方法,为什么不直接调用对应的方法?
Method对象是在运行期通过字节码对象获取的,对于编码期间,并不知道具体的对象是什么类型,所以需要用invoke方法调用。Method.invoke(...)中传入了具体对象和方法参数,以及方法对象。在JVM中会获取具体对象的class字节码对象,又有了方法对象和方法参数,就可以动态调用Method.invoke()方法。
动态加载与静态加载
此处所说的加载是针对编译
的,将xxx.java转化成xxx.class,而不是运行的加载字节码。Java创建对象的常用方式是使用new 关键字,如 Person p = new Person(); 这种是静态加载
,即在编译期就已经获取Person类的有关信息,如果Person类不存在或有错误,编译会报错。动态加载
就是用上面的Class.forName("包名.Person");来加载Person类,如果Person类不存在或是有错误,在编译时是不会报错的,因为根本没去加载Person类。只有当程序运行到该处,JVM才会去加载Person,而动态加载
则是依赖反射实现的。
网友评论