动态语言
静态语言
反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。
正常情况下,如果我们要调用一个对象的方法,或者访问一个对象的字段,通常会传入对象实例:
// Main.java import com.itranswarp.learnjava.Person; public class Main { String getFullName(Person p) { return p.getFirstName() + " " + p.getLastName(); } }
但是,如果不能获得 Person
类,只有一个 Object
实例,比如这样:
String getFullName(Object obj) { return ??? }
怎么办?有童鞋会说:强制转型啊!
String getFullName(Object obj) { Person p = (Person) obj; return p.getFirstName() + " " + p.getLastName(); }
强制转型的时候,你会发现一个问题:编译上面的代码,仍然需要引用 Person
类。不然,去掉 import
语句,你看能不能编译通过?
所以,反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
<img src="https://tva1.sinaimg.cn/large/007S8ZIlgy1gges29pxjoj30tw0t2q34.jpg" alt="image-20200205095809050" style="zoom:50%;float:left" />
String类、Dog类在方法区中的类文件代码中,存了属性、构造方法、方法等信息,其实在Class中也就定义了Field、Constructor、Method这些信息,相当于定义一个对象的接口
<img src="https://tva1.sinaimg.cn/large/007S8ZIlgy1ggerrn9w2xj30u00coaaq.jpg" alt="image-20200205095809050" style="zoom:80%;float:left" />
除了 int
等基本类型外,Java的其他类型全部都是 class
(包括 interface
)。例如:
String Object Runnable Exception
仔细思考,我们可以得出结论: class
(包括 interface
)的本质是数据类型( Type
)。无继承关系的数据类型无法赋值:
Number n = new Double(123.456); // OK String s = new Double(123.456); // compile error!
而 class
是由JVM在执行过程中动态加载的。JVM在第一次读取到一种 class
类型时,将其加载进内存。
每加载一种 class
,JVM就为其创建一个 Class
类型的实例,并关联起来。注意:这里的 Class
类型是一个名叫 Class
的 class
。它长这样:
public final class Class { private Class() {} }
以 String
类为例,当JVM加载 String
类时,它首先读取 String.class
文件到内存,然后,为 String
类创建一个 Class
实例并关联起来:
Class cls = new Class(String);
这个 Class
实例是JVM内部创建的,如果我们查看JDK源码,可以发现 Class
类的构造方法是 private
,只有JVM能创建 Class
实例,我们自己的Java程序是无法创建 Class
实例的。
所以,JVM持有的每个 Class
实例都指向一个数据类型( class
或 interface
):
┌───────────────────────────┐ │ Class Instance │──────> String ├───────────────────────────┤ │name = "java.lang.String" │ └───────────────────────────┘ ┌───────────────────────────┐ │ Class Instance │──────> Random ├───────────────────────────┤ │name = "java.util.Random" │ └───────────────────────────┘ ┌───────────────────────────┐ │ Class Instance │──────> Runnable ├───────────────────────────┤ │name = "java.lang.Runnable"│ └───────────────────────────┘
一个 Class
实例包含了该 class
的所有完整信息:
┌───────────────────────────┐ │ Class Instance │──────> String ├───────────────────────────┤ │name = "java.lang.String" │ ├───────────────────────────┤ │package = "java.lang" │ ├───────────────────────────┤ │super = "java.lang.Object" │ ├───────────────────────────┤ │interface = CharSequence...│ ├───────────────────────────┤ │field = value[],hash,... │ ├───────────────────────────┤ │method = indexOf()... │ └───────────────────────────┘
由于JVM为每个加载的 class
创建了对应的 Class
实例,并在实例中保存了该 class
的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个 Class
实例,我们就可以通过这个 Class
实例获取到该实例对应的 class
的所有信息。
这种通过 Class
实例获取 class
信息的方法称为反射(Reflection)。
如何获取一个 class
的 Class
实例?有三个方法:
直接通过一个 class
的静态变量 class
获取:
Class cls = String.class;
通过该实例变量提供的 getClass()
方法
String s = "Hello"; Class cls = s.getClass();
通过静态方法 Class.forName()
Class cls = Class.forName("java.lang.String");
因为 Class
实例在JVM中是唯一的,所以,上述方法获取的 Class
实例是同一个实例。可以用 ==
比较两个 Class
实例:
Class cls1 = String.class; String s = "Hello"; Class cls2 = s.getClass(); boolean sameClass = cls1 == cls2; // true
Integer n = new Integer(123); boolean b1 = n instanceof Integer; // true,因为n是Integer类型 boolean b2 = n instanceof Number; // true,因为n是Number类型的子类 boolean b3 = n.getClass() == Integer.class; // true,因为n.getClass()返回Integer.class boolean b4 = n.getClass() == Number.class; // false,因为Integer.class!=Number.class
用 instanceof
不但匹配指定类型,还匹配指定类型的子类。而用 ==
判断 class
实例可以精确地判断数据类型,但不能作子类型比较。
通常情况下,我们应该用 instanceof
判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。只有在需要精确判断一个类型是不是某个 class
的时候,我们才使用 ==
判断 class
实例。
public class Main { public static void main(String[] args) { printClassInfo("".getClass()); printClassInfo(Runnable.class); printClassInfo(java.time.Month.class); printClassInfo(String[].class); printClassInfo(int.class); } static void printClassInfo(Class cls) { System.out.println("Class name: " + cls.getName()); System.out.println("Simple name: " + cls.getSimpleName()); if (cls.getPackage() != null) { System.out.println("Package name: " + cls.getPackage().getName()); } System.out.println("is interface: " + cls.isInterface()); System.out.println("is enum: " + cls.isEnum()); System.out.println("is array: " + cls.isArray()); System.out.println("is primitive: " + cls.isPrimitive()); } }
注意到数组(例如 String[]
)也是一种 Class
,而且不同于 String.class
,它的类名是 [Ljava.lang.String
。此外,JVM为每一种基本类型如int也创建了 Class
,通过 int.class
访问。
如果获取到了一个 Class
实例,我们就可以通过该 Class
实例来创建对应类型的实例:
// 获取String的Class实例: Class cls = String.class; // 创建一个String实例: String s = (String) cls.newInstance();
上述代码相当于 new String()
。通过 Class.newInstance()
可以创建类实例,它的局限是:只能调用 public
的无参数构造方法。带参数的构造方法,或者非 public
的构造方法都无法通过 Class.newInstance()
被调用。
JVM在执行Java程序的时候,并不是一次性把所有用到的class全部加载到内存,而是第一次需要用到class时才加载。例如:
// Main.java public class Main { public static void main(String[] args) { if (args.length > 0) { create(args[0]); } } static void create(String name) { Person p = new Person(name); } }
当执行 Main.java
时,由于用到了 Main
,因此,JVM首先会把 Main.class
加载到内存。然而,并不会加载 Person.class
,除非程序执行到 create()
方法,JVM发现需要加载 Person
类时,才会首次加载 Person.class
。如果没有执行 create()
方法,那么 Person.class
根本就不会被加载。
这就是JVM动态加载 class
的特性。
动态加载 class
的特性对于Java程序非常重要。利用JVM动态加载 class
的特性,我们才能在运行期根据条件加载不同的实现类。例如,Commons Logging总是优先使用Log4j,只有当Log4j不存在时,才使用JDK的logging。利用JVM动态加载特性,大致的实现代码如下:
反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
Java 反射主要提供以下功能:
我们先看看如何通过 Class
实例获取字段信息。 Class
类提供了以下几个方法来获取字段:
public class Main { public static void main(String[] args) throws Exception { Class stdClass = Student.class; // 获取public字段"score": System.out.println(stdClass.getField("score")); // 获取继承的public字段"name": System.out.println(stdClass.getField("name")); // 获取private字段"grade": System.out.println(stdClass.getDeclaredField("grade")); } } class Student extends Person { public int score; private int grade; } class Person { public String name; }
上述代码首先获取 Student
的 Class
实例,然后,分别获取 public
字段、继承的 public
字段以及 private
字段,打印出的 Field
类似:
public int Student.score public java.lang.String Person.name private int Student.grade
一个 Field
对象包含了一个字段的所有信息:
getName()
:返回字段名称,例如, "name"
; getType()
:返回字段类型,也是一个 Class
实例,例如, String.class
; getModifiers()
:返回字段的修饰符,它是一个 int
,不同的bit表示不同的含义。
以 String
类的 value
字段为例,它的定义是:
public final class String { private final byte[] value; }
我们用反射获取该字段的信息,代码如下:
Field f = String.class.getDeclaredField("value"); f.getName(); // "value" f.getType(); // class [B 表示byte[]类型 int m = f.getModifiers(); Modifier.isFinal(m); // true Modifier.isPublic(m); // false Modifier.isProtected(m); // false Modifier.isPrivate(m); // true Modifier.isStatic(m); // false
利用反射拿到字段的一个 Field
实例只是第一步,我们还可以拿到一个实例对应的该字段的值。
例如,对于一个 Person
实例,我们可以先拿到 name
字段对应的 Field
,再获取这个实例的 name
字段的值:
public class Main { public static void main(String[] args) throws Exception { Object p = new Person("Xiao Ming"); Class c = p.getClass(); Field f = c.getDeclaredField("name"); Object value = f.get(p); System.out.println(value); // "Xiao Ming" } } class Person { private String name; public Person(String name) { this.name = name; } }
上述代码先获取 Class
实例,再获取 Field
实例,然后,用 Field.get(Object)
获取指定实例的指定字段的值。
运行代码,如果不出意外,会得到一个 IllegalAccessException
,这是因为 name
被定义为一个 private
字段,正常情况下, Main
类无法访问 Person
类的 private
字段。要修复错误,可以将 private
改为 public
,或者,在调用 Object value = f.get(p);
前,先写一句:
f.setAccessible(true);
调用 Field.setAccessible(true)
的意思是,别管这个字段是不是 public
,一律允许访问。
可以试着加上上述语句,再运行代码,就可以打印出 private
字段的值。
有童鞋会问:如果使用反射可以获取 private
字段的值,那么类的封装还有什么意义?
答案是正常情况下,我们总是通过 p.name
来访问 Person
的 name
字段,编译器会根据 public
、 protected
和 private
决定是否允许访问字段,这样就达到了数据封装的目的。
而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。
此外, setAccessible(true)
可能会失败。如果JVM运行期存在 SecurityManager
,那么它会根据规则进行检查,有可能阻止 setAccessible(true)
。例如,某个 SecurityManager
可能不允许对 java
和 javax
开头的 package
的类调用 setAccessible(true)
,这样可以保证JVM核心库的安全。
通过Field实例既然可以获取到指定实例的字段值,自然也可以设置字段的值。
设置字段值是通过 Field.set(Object, Object)
实现的,其中第一个 Object
参数是指定的实例,第二个 Object
参数是待修改的值。示例代码如下:
public class Main { public static void main(String[] args) throws Exception { Person p = new Person("Xiao Ming"); System.out.println(p.getName()); // "Xiao Ming" Class c = p.getClass(); Field f = c.getDeclaredField("name"); f.setAccessible(true); f.set(p, "Xiao Hong"); System.out.println(p.getName()); // "Xiao Hong" } } class Person { private String name; public Person(String name) { this.name = name; } public String getName() { return this.name; } }
我们已经能通过 Class
实例获取所有 Field
对象,同样的,可以通过 Class
实例获取所有 Method
信息。 Class
类提供了以下几个方法来获取 Method
:
Method getMethod(name, Class...)
:获取某个 public
的 Method
(包括父类) Method getDeclaredMethod(name, Class...)
:获取当前类的某个 Method
(不包括父类) Method[] getMethods()
:获取所有 public
的 Method
(包括父类) Method[] getDeclaredMethods()
:获取当前类的所有 Method
(不包括父类) public class Main { public static void main(String[] args) throws Exception { Class stdClass = Student.class; // 获取public方法getScore,参数为String: System.out.println(stdClass.getMethod("getScore", String.class)); // 获取继承的public方法getName,无参数: System.out.println(stdClass.getMethod("getName")); // 获取private方法getGrade,参数为int: System.out.println(stdClass.getDeclaredMethod("getGrade", int.class)); } } class Student extends Person { public int getScore(String type) { return 99; } private int getGrade(int year) { return 1; } } class Person { public String getName() { return "Person"; } }
上述代码首先获取 Student
的 Class
实例,然后,分别获取 public
方法、继承的 public
方法以及 private
方法,打印出的 Method
类似:
public int Student.getScore(java.lang.String) public java.lang.String Person.getName() private int Student.getGrade(int)
一个 Method
对象包含一个方法的所有信息:
getName()
:返回方法名称,例如: "getScore"
; getReturnType()
:返回方法返回值类型,也是一个Class实例,例如: String.class
; getParameterTypes()
:返回方法的参数类型,是一个Class数组,例如: {String.class, int.class}
; getModifiers()
:返回方法的修饰符,它是一个 int
,不同的bit表示不同的含义。
当我们获取到一个 Method
对象时,就可以对它进行调用。我们以下面的代码为例:
String s = "Hello world"; String r = s.substring(6); // "world"
如果用反射来调用 substring
方法,需要以下代码:
public class Main { public static void main(String[] args) throws Exception { // String对象: String s = "Hello world"; // 获取String substring(int)方法,参数为int: Method m = String.class.getMethod("substring", int.class); // 在s对象上调用该方法并获取结果: String r = (String) m.invoke(s, 6); // 打印调用结果: System.out.println(r); } }
注意到 substring()
有两个重载方法,我们获取的是 String substring(int)
这个方法。思考一下如何获取 String substring(int, int)
方法。
对 Method
实例调用 invoke
就相当于调用该方法, invoke
的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错
如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以 invoke
方法传入的第一个参数永远为 null
。我们以 Integer.parseInt(String)
为例:
public class Main { public static void main(String[] args) throws Exception { // 获取Integer.parseInt(String)方法,参数为String: Method m = Integer.class.getMethod("parseInt", String.class); // 调用该静态方法并获取结果: Integer n = (Integer) m.invoke(null, "12345"); // 打印调用结果: System.out.println(n); } }
和Field类似,对于非public方法,我们虽然可以通过 Class.getDeclaredMethod()
获取该方法实例,但直接对其调用将得到一个 IllegalAccessException
。为了调用非public方法,我们通过 Method.setAccessible(true)
允许其调用:
此外, setAccessible(true)
可能会失败。如果JVM运行期存在 SecurityManager
,那么它会根据规则进行检查,有可能阻止 setAccessible(true)
。例如,某个 SecurityManager
可能不允许对 java
和 javax
开头的 package
的类调用 setAccessible(true)
,这样可以保证JVM核心库的安全。
一个 Person
类定义了 hello()
方法,并且它的子类 Student
也覆写了 hello()
方法,那么,从 Person.class
获取的 Method
,作用于 Student
实例时,调用的方法到底是哪个?
public class Main { public static void main(String[] args) throws Exception { // 获取Person的hello方法: Method h = Person.class.getMethod("hello"); // 对Student实例调用hello方法: h.invoke(new Student()); } } class Person { public void hello() { System.out.println("Person:hello"); } } class Student extends Person { public void hello() { System.out.println("Student:hello"); } }
运行上述代码,发现打印出的是 Student:hello
,因此,使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。上述的反射代码:
Method m = Person.class.getMethod("hello"); m.invoke(new Student());
实际上相当于:
Person p = new Student(); p.hello();
我们通常使用 new
操作符创建新的实例:
Person p = new Person();
如果通过反射来创建新的实例,可以调用Class提供的newInstance()方法:
Person p = Person.class.newInstance();
调用Class.newInstance()的局限是,它只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。
为了调用任意的构造方法,Java的反射API提供了Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例。Constructor对象和Method非常类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例:
public class Main { public static void main(String[] args) throws Exception { // 获取构造方法Integer(int): Constructor cons1 = Integer.class.getConstructor(int.class); // 调用构造方法: Integer n1 = (Integer) cons1.newInstance(123); System.out.println(n1); // 获取构造方法Integer(String) Constructor cons2 = Integer.class.getConstructor(String.class); Integer n2 = (Integer) cons2.newInstance("456"); System.out.println(n2); } }
通过Class实例获取Constructor的方法如下:
getConstructor(Class...)
:获取某个 public
的 Constructor
; getDeclaredConstructor(Class...)
:获取某个 Constructor
; getConstructors()
:获取所有 public
的 Constructor
; getDeclaredConstructors()
:获取所有 Constructor
。
注意 Constructor
总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
调用非 public
的 Constructor
时,必须首先通过 setAccessible(true)
设置允许访问。 setAccessible(true)
可能会失败。
由于一个类可能实现一个或多个接口,通过 Class
我们就可以查询到实现的接口类型。例如,查询 Integer
实现的接口:
public class Main { public static void main(String[] args) throws Exception { Class s = Integer.class; Class[] is = s.getInterfaces(); for (Class i : is) { System.out.println(i); } } }
运行上述代码可知, Integer
实现的接口有:
要特别注意: getInterfaces()
只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型:
public class Main { public static void main(String[] args) throws Exception { Class s = Integer.class.getSuperclass(); Class[] is = s.getInterfaces(); for (Class i : is) { System.out.println(i); } } }
Integer
的父类是 Number
, Number
实现的接口是 java.io.Serializable
。
此外,对所有 interface
的 Class
调用 getSuperclass()
返回的是 null
,获取接口的父接口要用 getInterfaces()
:
System.out.println(java.io.DataInputStream.class.getSuperclass()); // java.io.FilterInputStream,因为DataInputStream继承自FilterInputStream System.out.println(java.io.Closeable.class.getSuperclass()); // null,对接口调用getSuperclass()总是返回null,获取接口的父接口要用getInterfaces()
当我们判断一个实例是否是某个类型时,正常情况下,使用 instanceof
操作符:
Object n = Integer.valueOf(123); boolean isDouble = n instanceof Double; // false boolean isInteger = n instanceof Integer; // true boolean isNumber = n instanceof Number; // true boolean isSerializable = n instanceof java.io.Serializable; // true
如果是两个 Class
实例,要判断一个向上转型是否成立,可以调用 isAssignableFrom()
:
// Integer i = ? Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer // Number n = ? Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number // Object o = ? Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object // Integer i = ? Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer
3.1优点
反射机制是构建框架技术的基础所在,使用反射可以避免将代码写死在框架中。正是反射有以上的特征,所以它能动态编译和创建对象,极大的激发了编程语言的灵活性,强化了多态的特性,进一步提升了面向对象编程的抽象能力,因而受到编程界的青睐。
3.2缺点
尽管反射机制带来了极大的灵活性及方便性,但反射也有缺点。反射机制的功能非常强大,但不能滥用。在能不使用反射完成时,尽量不要使用,原因有以下几点:
性能问题。
Java反射机制中包含了一些动态类型,所以Java虚拟机不能够对这些动态代码进行优化。因此,反射操作的效率要比正常操作效率低很多。我们应该避免在对性能要求很高的程序或经常被执行的代码中使用反射。而且,如何使用反射决定了性能的高低。如果它作为程序中较少运行的部分,性能将不会成为一个问题。
安全限制
使用反射通常需要程序的运行没有安全方面的限制。如果一个程序对安全性提出要求,则最好不要使用反射。
程序健壮性
反射允许代码执行一些通常不被允许的操作,所以使用反射有可能会导致意想不到的后果。反射代码破坏了Java程序结构的抽象性,所以当程序运行的平台发生变化的时候,由于抽象的逻辑结构不能被识别,代码产生的效果与之前会产生差异。
IOC:即“控制反转”,不是什么技术,而是一种思想。使用IOC意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。在Spring的配置文件中,经常看到如下配置:
<bean id="userService" class="com.tim.wang.sourcecode.reflection.springioc.UserServiceImpl"></bean>
那么通过这样配置,Spring是怎么帮我们实例化对象,并且放到容器中去了了,就是通过反射!!!
//解析<bean .../>元素的id属性得到该字符串值为“courseDao” String idStr = "courseDao"; //解析<bean .../>元素的class属性得到该字符串值为“com.qcjy.learning.Dao.impl.CourseDaoImpl” String classStr = "com.qcjy.learning.Dao.impl.CourseDaoImpl"; //利用反射知识,通过classStr获取Class类对象 Class<?> cls = Class.forName(classStr); //实例化对象 Object obj = cls.newInstance(); //container表示Spring容器 container.put(idStr, obj);
当一个类里面需要应用另一类的对象时,Spring的配置如下所示:
<bean id="courseService" class="com.qcjy.learning.service.impl.CourseServiceImpl"> <!-- 控制调用setCourseDao()方法,将容器中的courseDao bean作为传入参数 --> <property name="courseDao" ref="courseDao"></property> </bean>
继续用伪代码的形式来模拟实现一下Spring底层处理原理:
//解析<property .../>元素的name属性得到该字符串值为“courseDao” String nameStr = "courseDao"; //解析<property .../>元素的ref属性得到该字符串值为“courseDao” String refStr = "courseDao"; //生成将要调用setter方法名 String setterName = "set" + nameStr.substring(0, 1).toUpperCase() + nameStr.substring(1); //获取spring容器中名为refStr的Bean,该Bean将会作为传入参数 Object paramBean = container.get(refStr); //获取setter方法的Method类,此处的cls是刚才反射代码得到的Class对象 Method setter = cls.getMethod(setterName, paramBean.getClass()); //调用invoke()方法,此处的obj是刚才反射代码得到的Object对象 setter.invoke(obj, paramBean);