Java- 反射机制
参考文章:重要
参考文章
什么是反射机制
简单来说,反射可以帮助我们在动态运行的时候,对于任意一个类,可以获得其所有的方法(包括 public protected private 默认状态的),所有的变量 (包括 public protected private 默认状态的)。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。
反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
Java 反射主要提供以下功能:
- 在运行时判断任意一个对象所属的类;
- 在运行时构造任意一个类的对象;
- 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
- 在运行时调用任意一个对象的方法
重点:是运行时而不是编译时
反射的主要用途
很多人都认为反射在实际的 Java 开发应用中并不广泛,其实不然。当我们在使用 IDE(如 Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。
反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。
- 静态编译:在编译时确定类型,绑定对象
- 动态编译:运行时确定类型,绑定对象
两者的区别在于,动态编译可以最大程度地支持多态,而多态最大的意义在于降低类的耦合性,因此反射的优点就很明显了:解耦以及提高代码的灵活性。
因此,反射的优势和劣势分别在于:
- 优势
- 劣势
- 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多
反射的基本运用
1、获得class对象
Class对象
在Java中有两种对象:Class对象和实例对象,实例对象是类的实例,通常是通过 new
关键字构建的。Class对象是JVM生成用来保存对象的类的信息的。Java程序执行之前需要经过编译、加载、链接和初始化这几个阶段,编译阶段会将源码文件编译为 .class
字节码文件,编译器同时会在 .class
文件中生成Class对象,加载阶段通过JVM内部的类加载机制,将Class对象加载到内存中。在创建对象实例之前,JVM会先检查Class对象是否在内存中存在,如果不存在,则加载Class对象,然后再创建对象实例,如果存在,则直接根据Class对象创建对象实例。JVM中只有一个Class对象,但可以根据Class对象生成多个对象实例。
Class对象的获得
1、类名.class
当执行 类名.class
时,JVM会先检查Class对象是否装入内存,如果没有装入内存,则将Class对象装入内存,然后返回Class对象,如果装入内存,则直接返回Class对象。在加载Class对象后,不会对Class对象进行初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class Test { static { System.out.println("Run static initialization block."); } { System.out.println("Run nonstatic initialization block."); } }
public class ClassTest {
public static void main(String[] args) { Class t = Test.class; } }
|
2、 Class.forName()
当执行 Class.forName()
时,JVM也会先检查Class对象是否装入内存,如果没有装入内存,则将Class对象装入内存,然后返回Class对象,如果装入内存,则直接返回Class对象。在加载Class对象后,会对类进行初始化,即执行类的静态代码块。forName()
方法中的参数是类名字符串,类名字符串 = 包名 + 类名。Class.forName()
的一个很常见的用法是在加载数据库驱动的时候。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| package com.tyan.test;
public class Test { static { System.out.println("Run static initialization block."); } { System.out.println("Run nonstatic initialization block."); } }
package com.tyan.test;
public class ClassTest {
public static void main(String[] args) { try { Class t = Class.forName("com.tyan.test.Test"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
|
1
| Run static initialization block
|
3、getClass()
getClass()
方法的方法是在通过的类的实例调用的,即已经创建了类的实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class Test { static { System.out.println("Run static initialization block."); } { System.out.println("Run nonstatic initialization block."); } }
public class ClassTest { public static void main(String[] args) { Test t = new Test(); Class test = t.getClass(); } }
|
1 2
| Run static initialization block. Run nonstatic initialization block.
|
Class类的常用方法
一个Class对象描述了一个特定类的属性,Class类中最常用的方法getName以String的形式返回此Class对象所表示的实体(类、接口、数组类、基本类型或void名称。
Class还有一个有用的方法可以为类创建一个实例,这个方法叫做newInstance()。例如:x.getClass.newInstance()
,创建了一个同 x
一样类型的新实例。newInstance()
方法调用默认构造器(无参数构造器)初始化新建对象。
返回该类的类加载器。
返回表示数组组件类型的Class。
返回表示此 Class 所表示的实体(类、接口、基本类型或 void)的超类的Class。
判定此 Class 对象是否表示一个数组类。
2、判断是否为某个类的实例
一般地,我们用 instanceof
关键字来判断是否为某个类的实例。同时我们也可以借助反射中 Class 对象的 isInstance()
方法来判断是否为某个类的实例,它是一个 native 方法:
1
| public native boolean isInstance(Object obj);
|
3、创建实例
通过反射来生成对象主要有两种方式。
- 使用Class对象的newInstance()方法来创建Class对象对应类的实例。
1
| Class<?> c = String.class;Object str = c.newInstance();
|
- 先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例。
1 2 3 4 5 6 7
| Class<?> c = String.class;
Constructor constructor = c.getConstructor(String.class);
Object obj = constructor.newInstance("23333"); System.out.println(obj);
|
4、获取方法
获取某个Class对象的方法集合,主要有以下几个方法:
getDeclaredMethods
方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
1
| public Method[] getDeclaredMethods() throws SecurityException
|
getMethods
方法返回某个类的所有公用(public)方法,包括其继承类的公用方法。
1
| public Method[] getMethods() throws SecurityException
|
getMethod
方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象。
1
| public Method getMethod(String name, Class<?>... parameterTypes)
|
只是这样描述的话可能难以理解,我们用例子来理解这三个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| package org.ScZyhSoft.common; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class test1 { public static void test() throws IllegalAccessException,InstantiationException, NoSuchMethodException, InvocationTargetException { Class<?> c = methodClass.class; Object object = c.newInstance(); Method[] methods = c.getMethods(); Method[] declaredMethods = c.getDeclaredMethods(); Method method = c.getMethod("add", int.class, int.class); System.out.println("getMethods获取的方法:"); for(Method m:methods) System.out.println(m); System.out.println("getDeclaredMethods获取的方法:"); for(Method m:declaredMethods) System.out.println(m); } } class methodClass { public final int fuck = 3; public int add(int a,int b) { return a+b; } public int sub(int a,int b) { return a+b; } }
|
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| getMethods获取的方法: public int org.ScZyhSoft.common.methodClass.add(int,int) public int org.ScZyhSoft.common.methodClass.sub(int,int) public final void java.lang.Object.wait() throws java.lang.InterruptedException public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException public boolean java.lang.Object.equals(java.lang.Object) public java.lang.String java.lang.Object.toString() public native int java.lang.Object.hashCode() public final native java.lang.Class java.lang.Object.getClass() public final native void java.lang.Object.notify() public final native void java.lang.Object.notifyAll() getDeclaredMethods获取的方法: public int org.ScZyhSoft.common.methodClass.add(int,int) public int org.ScZyhSoft.common.methodClass.sub(int,int)
|
可以看到,通过 getMethods()
获取的方法可以获取到父类的方法,比如 java.lang.Object 下定义的各个方法。
5、获取构造器信息
获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:
1
| public T newInstance(Object ... initargs)
|
此方法可以根据传入的参数来调用对应的Constructor创建对象实例。
6、获取类的成员变量(字段)信息
主要是这几个方法,在此不再赘述:
getFiled
:访问公有的成员变量
getDeclaredField
:所有已声明的成员变量,但不能得到其父类的成员变量
getFileds
和 getDeclaredFields
方法用法同上(参照 Method)。
7、调用方法
当我们从类中获取了一个方法后,我们就可以用 invoke()
方法来调用这个方法。invoke
方法的原型为:
1 2
| public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
|
下面是一个实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class test1 { public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { Class<?> klass = methodClass.class; Object obj = klass.newInstance(); Method method = klass.getMethod("add",int.class,int.class); Object result = method.invoke(obj,1,4); System.out.println(result); } } class methodClass { public final int fuck = 3; public int add(int a,int b) { return a+b; } public int sub(int a,int b) { return a+b; } }
|
8、利用反射创建数组
数组在Java里是比较特殊的一种类型,它可以赋值给一个Object Reference。下面我们看一看利用反射创建数组的例子:
1 2 3 4 5 6 7 8 9 10 11 12
| public static void testArray() throws ClassNotFoundException { Class<?> cls = Class.forName("java.lang.String"); Object array = Array.newInstance(cls,25);
Array.set(array,0,"hello"); Array.set(array,1,"Java"); Array.set(array,2,"fuck"); Array.set(array,3,"Scala"); Array.set(array,4,"Clojure");
System.out.println(Array.get(array,3)); }
|
其中的Array类为java.lang.reflect.Array类。我们通过Array.newInstance()创建数组对象,它的原型是:
1 2 3 4
| public static Object newInstance(Class<?> componentType, int length) throws NegativeArraySizeException { return newArray(componentType, length); }
|
而 newArray
方法是一个 native 方法,它在 HotSpot JVM 里的具体实现我们后边再研究,这里先把源码贴出来:
1
| private static native Object newArray(Class<?> componentType, int length) throws NegativeArraySizeException;
|
源码目录:openjdk\hotspot\src\share\vm\runtime\reflection.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| arrayOop Reflection::reflect_new_array(oop element_mirror, jint length, TRAPS) { if (element_mirror == NULL) { THROW_0(vmSymbols::java_lang_NullPointerException()); } if (length < 0) { THROW_0(vmSymbols::java_lang_NegativeArraySizeException()); } if (java_lang_Class::is_primitive(element_mirror)) { Klass* tak = basic_type_mirror_to_arrayklass(element_mirror, CHECK_NULL); return TypeArrayKlass::cast(tak)->allocate(length, THREAD); } else { Klass* k = java_lang_Class::as_Klass(element_mirror); if (k->oop_is_array() && ArrayKlass::cast(k)->dimension() >= MAX_DIM) { THROW_0(vmSymbols::java_lang_IllegalArgumentException()); } return oopFactory::new_objArray(k, length, THREAD); } }
|
另外,Array 类的 set
和 get
方法都为 native 方法,在 HotSpot JVM 里分别对应 Reflection::array_set
和 Reflection::array_get
方法,这里就不详细解析了。
反射的应用场景
JDBC 的数据库的连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class ConnectionJDBC {
public static final String DBDRIVER = "com.mysql.jdbc.Driver"; public static final String DBURL = "jdbc:mysql://localhost:3306/test"; public static final String DBUSER = "root"; public static final String DBPASS = ""; public static void main(String[] args) throws Exception { Connection con = null; Class.forName(DBDRIVER); con = DriverManager.getConnection(DBURL,DBUSER,DBPASS); System.out.println(con); con.close(); }
|
spring框架的使用
Spring 通过 XML 配置模式装载 Bean 的过程:
- 将程序内所有 XML 或 Properties 配置文件加载入内存中
- Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
- 使用反射机制,根据这个字符串获得某个类的Class实例
- 动态配置实例的属性
Spring这样做的好处是:
- 不用每一次都要在代码里面去new或者做其他的事情
- 以后要改的话直接改配置文件,代码维护起来就很方便了
- 有时为了适应某些需求,Java类里面不一定能直接调用另外的方法,可以通过反射机制来实现
模拟 Spring 加载 XML 配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class BeanFactory { private Map<String, Object> beanMap = new HashMap<String, Object>();
public void init(String xml) { try { SAXReader reader = new SAXReader(); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); InputStream ins = classLoader.getResourceAsStream(xml); Document doc = reader.read(ins); Element root = doc.getRootElement(); Element foo; for (Iterator i = root.elementIterator("bean"); i.hasNext();) { foo = (Element) i.next(); Attribute id = foo.attribute("id"); Attribute cls = foo.attribute("class"); Class bean = Class.forName(cls.getText()); java.beans.BeanInfo info = java.beans.Introspecto
|
反射的一些注意事项
由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。
另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。