一、反射介绍
1.0 动静语言和动态语言
动静语言
- 是一类在运行是能够扭转其构造的语言:例如新的函数,对象,甚至代码能够被引进,已有的函数能够被删除或者是其余构造上的变动。艰深点就是说能够在运行时代码能够依据某些条件扭转本身构造
- 次要动静语言:Object-C、JavaScript、PHP、Python等
动态语言
- 与动静语言向对应的,运行时构造不可变的语言就是动态语言。如Java、C、C++
- Java尽管不是动静语言,但Java能够称之为“准动静语言”。即Java有肯定的动态性,咱们能够利用反射机制取得相似动静语言的个性。
1.1 什么是反射
反射就是Reflection,Java的反射是指程序在运行期能够拿到一个对象的所有信息。
失常状况下,如果咱们要调用一个对象的办法,或者拜访一个对象的字段,通常会传入对象实例:
// Main.javaimport 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
语句,你看能不能编译通过?
所以,反射是为了解决在运行期,对某个实例无所不知的状况下,如何调用其办法。
1.2 对象编译在JVM散布状况
<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" />
1.3 Class类
除了int
等根本类型外,Java的其余类型全部都是class
(包含interface
)。例如:
String
Object
Runnable
Exception
- ...
认真思考,咱们能够得出结论:class
(包含interface
)的实质是数据类型(Type
)。无继承关系的数据类型无奈赋值:
Number n = new Double(123.456); // OKString 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)。
1.4 如何获取一个Class实例
如何获取一个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
1.5 Class实例比拟和instanceof区别
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.classboolean b4 = n.getClass() == Number.class; // false,因为Integer.class!=Number.class
用instanceof
岂但匹配指定类型,还匹配指定类型的子类。而用==
判断class
实例能够准确地判断数据类型,但不能作子类型比拟。
通常状况下,咱们应该用instanceof
判断数据类型,因为面向形象编程的时候,咱们不关怀具体的子类型。只有在须要准确判断一个类型是不是某个class
的时候,咱们才应用==
判断class
实例。
1.6 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()
被调用。
1.7 动静加载
JVM在执行Java程序的时候,并不是一次性把所有用到的class全副加载到内存,而是第一次须要用到class时才加载。例如:
// Main.javapublic 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 反射次要提供以下性能:
- 在运行时判断任意一个对象所属的类;
- 在运行时结构任意一个类的对象;
- 在运行时判断任意一个类所具备的成员变量和办法(通过反射甚至能够调用private办法);
- 在运行时调用任意一个对象的办法
2.1 拜访字段
咱们先看看如何通过Class
实例获取字段信息。Class
类提供了以下几个办法来获取字段:
- Field getField(name):依据字段名获取某个public的field(包含父类)
- Field getDeclaredField(name):依据字段名获取以后类的某个field(不包含父类)
- Field[] getFields():获取所有public的field(包含父类)
- Field[] getDeclaredFields():获取以后类的所有field(不包含父类)
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.scorepublic java.lang.String Person.nameprivate 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); // trueModifier.isPublic(m); // falseModifier.isProtected(m); // falseModifier.isPrivate(m); // trueModifier.isStatic(m); // false
2.1.1 获取字段值
利用反射拿到字段的一个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外围库的平安。
2.1.2 设置字段值
通过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; }}
2.2 调用办法
咱们曾经能通过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示意不同的含意。
2.2.1 调用办法
当咱们获取到一个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
的第一个参数是对象实例,即在哪个实例上调用该办法,前面的可变参数要与办法参数统一,否则将报错
2.2.2 调用静态方法
如果获取到的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); }}
2.2.3 调用非public办法
和Field相似,对于非public办法,咱们尽管能够通过Class.getDeclaredMethod()
获取该办法实例,但间接对其调用将失去一个IllegalAccessException
。为了调用非public办法,咱们通过Method.setAccessible(true)
容许其调用:
此外,setAccessible(true)
可能会失败。如果JVM运行期存在SecurityManager
,那么它会依据规定进行查看,有可能阻止setAccessible(true)
。例如,某个SecurityManager
可能不容许对java
和javax
结尾的package
的类调用setAccessible(true)
,这样能够保障JVM外围库的平安。
2.2.4 多态办法
一个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();
2.3 调用构造方法
咱们通常应用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)
可能会失败。
2.4 获取继承关系
2.4.1 获取interface
因为一个类可能实现一个或多个接口,通过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
实现的接口有:
- java.lang.Comparable
- java.lang.constant.Constable
- java.lang.constant.ConstantDesc
要特地留神: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继承自FilterInputStreamSystem.out.println(java.io.Closeable.class.getSuperclass()); // null,对接口调用getSuperclass()总是返回null,获取接口的父接口要用getInterfaces()
2.4.2 继承关系
当咱们判断一个实例是否是某个类型时,失常状况下,应用instanceof
操作符:
Object n = Integer.valueOf(123);boolean isDouble = n instanceof Double; // falseboolean isInteger = n instanceof Integer; // trueboolean isNumber = n instanceof Number; // trueboolean 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程序结构的抽象性,所以当程序运行的平台发生变化的时候,因为形象的逻辑构造不能被辨认,代码产生的成果与之前会产生差别。
四、反射理论利用场景
4.1 Spring IOC
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);