我学习学习编程技术最大的体会,就是感觉像是在吃热豆腐,慢慢来,是豆腐总会凉凉的。
为什么要有反射?
最直接的原因是,在java之后出现了动态的语言,如python,js,这种动态的语言的类型的检查是在运行的时候做的,程序在运行的时候,能够改变程序结构或是变量类型,java学习别的语言的动态性,引入了反射机制,使得java成为了一门准动态语言,使得java在编程的时候有更大的灵活性,如spring的依赖反转(ioc),便是依赖反射机制的。
应用范围:java 的反射在框架中有着大量的应用。比如开发人员日常接触到的IDE,(集成开发环境)便运用了反射,每当我们敲入点号的时候,IDE变根据点号之前的内容,动态展示可以访问的字段或者是方法;再比如java的调试器,能够在调试过程中枚举某一个对象所有字段的值。
java在没有反射之前,因为java是强类型的语言,所有的参数,变量,返回值,属性都必须在编译期间就确定他的类型(所有的静态的语言的特点就是在编译时候,就必须确定变量等的类型),而对于弱类型的语言不需要确定其类型,比如PHP,声明一个变量的时候不需要指定该变量是什么类型的,在运行的时候会自动根据值来确定该变量的类型。
如果我们想在运行期间才确定变量,或者是返回值的类型,我们需要使用到反射机制。
反射的作用:
(1)在运行期间,确定某个类型,动态的创建一个对象
(2)在运行期间,获取某个对象的类型的全部信息
(3)在运行期间,动态的为某个对象的某个属性赋值,获取或者某个属性值
(4)在运行期间,动态的调用某个对象的某个方法
反射的根源(java.lang.Class)
首先,我们来看看java8的API:
public final class Class<T>
extends Object
implements Serializable, GenericDeclaration, Type, AnnotatedElement
Instances of the class
Class
represent classes and interfaces in a running Java application。Class类的实例(对象)代表着,正在java应用程序中运行的类或者是接口。
我们知道所有的类在加载到JVM之前,都需要被编译成字节码文件,即.class文件,在这个.class文件中有这个类的相关的属性,构造,方法等信息。而这些类的信息被JVM加载之后,将会以类的Class对象存在的。这有点抽象,我们可以把.class文件比作是一个箱子,而把类的这些信息比作是一些玩具,这些玩具装入了这个箱子里,等到这个箱子被JVM加载到内存的时候,JVM根据这个箱子,或者是这个箱子的信息,产生一个这个箱子的编号,从此之后用这个编号就可以获得这个箱子。这个箱子我们人可以认为就是一个该类的Class对象。可能真实的情况不是这样的,但是这将有助于你理解Class类的对象。
由此,可以看出,每一个被加载到JVM中的类的存在形式,表面是类文件(即字节码文件)实际上是该类的Class类对象。接着API:
An enum is a kind of class and an annotation is a kind of interface. Every array also belongs to a class that is reflected as a
Class
object that is shared by all arrays with the same element type and number of dimensions. The primitive Java types (boolean
,byte
,char
,short
,int
,long
,float
, anddouble
), and the keywordvoid
are also represented asClass
objects.
总结一下:
Java中任意类型,在运行期间都对应一个唯一的Class类对象。
Java的类型:
8种基本数据类型和void
引用数据类型:类(class,enum)、接口(interface,@interface)、数组([])
也就是说,所有类运行时(字节码加载到JVM),都对应着唯一的一个Class类对象(在方法区中)。接着:
Class
has no public constructor. InsteadClass
objects are constructed automatically by the Java Virtual Machine as classes are loaded and by calls to thedefineClass
method in the class loader.Class类没有公共的构造方法,Class类对象是在类加载时有JVM及通过调用调用类加载器中的defineClass方法自动构造的。
这句话,说明我们无法new 一个 Class类的对象,只能通过某种方式去获取这个Class对象。因为一个类型只有一个Class类的对象,而且在加载到JVM时候必须有且仅有一个Class对象,那么JVM自动给我们创建以此来确保唯一也帮我们省事,如果我们需要使用这个类的Class对象,java提供获取方式给我们,接下来我们来认识一个获取Class类对象的方法。一共有四种方法,我们依次来说:
类名/型.class
适用于所有的类型。
上文我们说到加载类时,JVM自动帮我们创建了该类Class对象。那么JVM总得找一个地方放,放在哪里?放在了class里。我们可以class想象成一个静态的属性,但是其实他不是,因为你翻遍任何一个类的源代码,都找不到这样的一个属性。我们获得了class,也就获得了该类的信息,还记得上文说到的箱子的编号么?获得class,就获得了该类的Class对象,即获得了“箱子编号”,获得了箱子,箱子里有类的信息。(一旦有类的信息,我们留能够创建该类的实例了)
我们来使用类名.class来获取Class对象玩玩:
package com.isea.java;
import java.io.Serializable;
import java.lang.annotation.ElementType;
public class TestClass {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println("void的类对象:" + void.class);//void的类对象:void
System.out.println("Object的类对象:" + Object.class);//Object的类对象:class java.lang.Object
System.out.println(int[].class);//class [I
System.out.println(int[][].class);//class [[I
System.out.println(Override.class);//interface java.lang.Override
System.out.println(ElementType.class);//class java.lang.annotation.ElementType
System.out.println(Serializable.class);//interface java.io.Serializable
System.out.println(TestClass.class);//class com.isea.java.TestClass
}
}
对象名.getClass()
适用于引用数据类型,必须有对象对象才能用,如类,接口,枚举,数据等的对象都可以得到,它对应的运行时的类型。
package com.isea.java;
public class TestClass {
public static void main(String[] args) {
Object obj = new String();
Class<? extends Object> c1 = obj.getClass();
System.out.println(c1);//class java.lang.String
}
}
该方法,是Object下的方法:放回调用this的类对象
public final native Class<?> getClass();
Class.forName("类型的全名称")
适用于类,接口,枚举,注解,基本数据类型,数组是不能使用这个方法得到的。
package com.isea.java;
public class TestClass {
public static void main(String[] args) throws ClassNotFoundException {
Class c1 = String.class;
Class c2 = new String().getClass();
Class c3 = Class.forName("java.lang.String");
System.out.println(c1 == c2);//true
System.out.println(c1 == c3);//true
System.out.println(c2 == c3);//true 证明一个类对应的Class对象是唯一的
}
}
类加载器对象.loadClass("类型的全名称")
适用于类,接口,枚举,注解。所谓的类加载器,我们可以简单的理解为用来加载类的东西。这种方式,我们首先要得到类加载器的对象,介绍其中一种方式:
ClassLoarder.getSystemClassLoader()
这种方法返回系统类加载器,就是默认的类加载器对象。源码如下:
//return The system <tt>ClassLoader</tt> for delegation or <tt>null</tt> if none
@CallerSensitive
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
例子:
package com.isea.java;
public class TestClass {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class<?> c1 = loader.loadClass("java.lang.Object");
System.out.println(c1);//class java.lang.Object
}
}
至此,我们介绍完了所有的获取Class类对象的方式,获取Class类对象是使用反射的第一步。获取到Class对象之后,我们便可以创建这个对象,使用Class类下面的这个方法能够获得该类的一个实例,:
//return a newly allocated instance of the class represented by this object
public T newInstance() throws InstantiationException, IllegalAccessException{
//....省略
}
注意:
调用该方法的对象对应的类要含有public类型的无参构造
反射应用一:运行期间动态创建任意类型对象
我们先来看看,如果不使用反射,假如在编译期间,我们就已经知道类型:
package com.isea.java;
public class Student {
}
package com.isea.java;
public class TestClass {
public static void main(String[] args){
Student student = new Student();
}
}
以上的例子中,我们已经知道有了Student类,我们直接创建该对象,这样做虽然非常的简洁,但是Student类和我们的TestClass直接发生了联系(耦合)一旦我们修改了Student类,我们就得修改TestClass中的代码。
再来看另外的一种场景,假如我们在编译期间不确定某个类型,只知道这个某个接口的实现类,但是我们能够在配置文件中获取类名。(广泛使用在web和框架中)来看下面的例子:
配置文件dao.properties:
dao = com.isea.dao.StudentDaoImp
接口:
package com.isea.java;
public interface IStudentDao {
void test();
}
测试代码:
public class TestClass {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
properties.load(new FileInputStream("dao.properties"));//加载的是项目于根目录src(目录下)文件下
String name = properties.getProperty("dao");
Class c = Class.forName(name);
IStudentDao iStudentDao = (IStudentDao) c.newInstance();
iStudentDao.test();
}
}
以上代码,在编译期间是不会报错的,但是在运行的时候回报如下的异常:
Exception in thread "main" java.lang.ClassNotFoundException:
com.isea.dao.StudentDaoImp
找不到该类!以上的这段代码利用了反射通过编译,但是并不表示运行期间这个类型可以不存在。代码中我们并没有出现我们要创建的类型(即从配置文件读取的文件类型)。假如我们创建这个类:
package com.isea.dao;
import com.isea.java.IStudentDao;
public class StudentDaoImp implements IStudentDao {
@Override
public void test() {
System.out.println("Just for test...");
}
}
这样代码就能够正常的运行。这个时候,我们编写的测试类没有直接和StudentDao发生任何的关系(解耦合)。实际上,这个例子中的获得Class对象,并获得这个对象的实例都将由一些框架完成,如javaWeb中Servelet对象都是TomCat创建的,一些接口的实现类都交由Spring等框架来做,可以理解为框架帮助我们管理写在配置文件中的那些类,获取类的信息,创建该类实例等。
本例子模拟了框架中对于反射的应用,值得把玩,框架提前在不知道要创建什么类的情况下通过反射,获取相关要创建类的信息(其实没有,但是能够通过编译)在运行之前,我们需要编写配置文件,将要创建的类告诉框架;框架并能够帮我们创建我们写在配置文件中的类的对象。
这个过程容易发生
ClassNotFoundException
排错:
配置文件中(包.类名)是否写错了;对应的路径下是否存在.class文件
这是第一种方式,我们通过在获得了class对象之后,直接通过class对象newInstance,创建该类的实例。还有一种创建对象的方式,先获取该类的构造器对象,然后在创建构造器对象,newInstance。下面来演示一下(在获取了class对象之后,上文中提到的 c)获取构造器对象的方法有多个:
c.getConstructors() 获得所有的公众的构造器
c.getDeclaredConstructors() 获得所有的声明的构造器,包括私有的
c.getConstructor(parameterType) 获得一个指定类型的公众的构造器
c.getDeclaredConstructor(parameterType) 获得一个指定类型的公众的构造器、参数不写都不写,得到的是无参构造;
返回值是Constructor类型;参数的类型是,class;
得到构造器对象之后,如果使用该构造器对象创建该类的对象:
Constructor constructor = c.getDeclaredConstructor();
IStudentDao iStudentDao = (IStudentDao) constructor.newInstance();
如果得到的构造器对象不是public的,在创建对象时,会发生如下的一异常:
Exception in thread "main" java.lang.IllegalAccessException:
Class com.isea.java.TestClass can not access a member of class com.isea.dao.StudentDaoImp with modifiers "private"
在创建对象之前,我们需要设置访问权限:
constructor.setAccessible(true);
如此一来,我们便能够根据这个构造器对象创建对象。另外,我们在写代码的时候,所有的类尽量的显示化一个无参的构造函数
反射应用二:动态的设置/访问某个对象的属性
先来看一下使用的场景:我们需要动态的获取Student对象的属性
package com.isea.java;
public class Student {
private int id;
private String name;
private char gender;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public Student() {
}
}
第一步:获取class对象;第二步根据class对象,创建该类的对象。第三步是我们需要看的:
c.getFields() 获取公共的属性
c.getDeclaredFields() 获取所有声明的属性(包括私有)
c.getField(name) 获取指定的公共的属性
c.getDeclaredField(name) 获取指定的声明的属性(包括私有)
根据属性名区分,获取哪个属性;返回的类型是Field类型,
注意,我们这里是使用 c(class类对象)来获得属性的,而不是使用该类的对象。
我们通过以上的几种方式获得了属性,便可以进行get和set,如果该类的属性是私有的,在set和get之前,我们需要设置他的访问权限
package com.isea.java;
import java.lang.reflect.Field;
public class TestClass {
public static void main(String[] args) throws Exception {
Class c = Class.forName("com.isea.java.Student");
Student student = (Student) c.newInstance();
Field field = c.getDeclaredField("name");
field.setAccessible(true);
field.set(student,"isea_you");
Object value = field.get(student);
System.out.println(value);//isea_you
}
}
关于Filed类型的组成?周知,一个类的属性,有访问权限,返回值,属性名等组成,可推测Field也是如此。
总结一下:
- 获取Class类对象
- 创建实例对象
- 获取属性值Filed
- 如果非公属性,setAccessible(true)
- Fielfd对象.set() ,Field对象.get()
反射应用三:动态的调用方法
和前面的步骤一样,获取Class对象,创建该类的对象;第三步:
c..getMethods() /获取所有的公共方法
c.getDeclaredMethods() 获取所有声明的方法
c.getMethord(name,paramaterType) 获取指定的公共的方法
c.getDeclaredMethod(name,paramaterType) 获取指定的声明的方法
返回值类型是:Methord类型。同一个类中有方法名相同的方法,因此,需要传入方法的形参列表进行区别。
举一个例子:
package com.isea.java;
public class Num {
public int getMax(int x,int y){
return x > y ? x : y;
}
public double getMax(double x,double y){
return x > y ? x : y;
}
}
package com.isea.java;
import java.lang.reflect.Method;
public class TestClass {
public static void main(String[] args) throws Exception {
Class c = Class.forName("com.isea.java.Num");
Num instance = (Num) c.newInstance();
Method method = c.getDeclaredMethod("getMax", int.class, int.class);
Object n = method.invoke(instance,3,9);
System.out.println(n);//9
}
}
为什么,我们在得到了该类的对象之后,不适用对象 “.” 的方式去调用方法,而是使用反射?因为我们并不知道项目中需要什么类,以至于这个类我们还没有创建,我们只是知道这个类全路径,和这个类应该有的方法。我们能够通过反射拿到该类的方法,并调用该类的方法,这在编译期间,这个类实际上并不存在。
反射应用四, 动态获取类型信息
首先,我们新建一个用来测试的User类,我们从配置文件中读取这个类的全路径
package com.isea.java;
import java.io.Serializable;
public class User implements Serializable,Comparable {
private static final long serialVersion = 1l;
private String username;
private String password;
private String email;
public User(String username, String password, String email) {
this.username = username;
this.password = password;
this.email = email;
}
public static long getSerialVersion() {
return serialVersion;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public int compareTo(Object o) {
return 0;
}
}
配置配置信息:
type = com.isea.java.User
获取关于这个类的相关信息:
package com.isea.java;
import java.io.FileInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Properties;
public class TestClass {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
properties.load(new FileInputStream("bean.properties"));
Class c = Class.forName(properties.getProperty("type"));
// User instance = (User) c.newInstance();
// 获取包名类型信息
Package p = c.getPackage();
System.out.println("包名:" + p.getName());
String name = c.getName();
System.out.println("类名:" + name);
int modify = c.getModifiers();
System.out.println("修饰符:" + modify);
System.out.println(Modifier.toString(modify));
// 获取父类信息
Class superclass = c.getSuperclass();
System.out.println("父类:" + superclass.getName());
Class[] interfaces = c.getInterfaces();
System.out.println("该类接口:");
for (Class inter :
interfaces) {
System.out.println("\t" + inter.getName());
}
// 该类的属性
Field[] fields = c.getDeclaredFields();
System.out.println("该类的属性");
for (Field field:
fields ) {
System.out.println("\t修饰符:" + Modifier.toString(field.getModifiers()));
System.out.println("\t数据类型:" + field.getType().getName());
System.out.println("\t属性名:" + field.getName());
System.out.println();
}
// 该类的构造器
Constructor[] constructors = c.getDeclaredConstructors();
System.out.println("该类的构造器:");
for (Constructor constructor :
constructors) {
System.out.println("\t修饰符:" + Modifier.toString(constructor.getModifiers()));
System.out.println("\t构造器名:" + constructor.getName());
System.out.println("\t构造器的形参列表:");
Class[] paramType = constructor.getParameterTypes();
for (Class param :
paramType) {
System.out.println("\t\t" + param);
}
}
Method[] methods = c.getDeclaredMethods();
for (Method method :
methods) {
System.out.println("\t修饰符:" + Modifier.toString(method.getModifiers()));
System.out.println("\t返回值类型:" + method.getReturnType().getName());
System.out.println("\t方法名:" + method.getName());
System.out.println("\t方法的形参列表:");
Class[] parameterTypes = method.getParameterTypes();
for (Class param :
parameterTypes) {
System.out.println("\t\t" + param);
System.out.println();
}
}
}
}
最后所有的类型打印:
包名:com.isea.java
类名:com.isea.java.User
修饰符:1
public
父类:java.lang.Object
该类接口:
java.io.Serializable
java.lang.Comparable
该类的属性
修饰符:private static final
数据类型:long
属性名:serialVersion
修饰符:private
数据类型:java.lang.String
属性名:username
修饰符:private
数据类型:java.lang.String
属性名:password
修饰符:private
数据类型:java.lang.String
属性名:email
该类的构造器:
修饰符:public
构造器名:com.isea.java.User
构造器的形参列表:
class java.lang.String
class java.lang.String
class java.lang.String
修饰符:public
返回值类型:int
方法名:compareTo
方法的形参列表:
class java.lang.Object
修饰符:public
返回值类型:java.lang.String
方法名:getPassword
方法的形参列表:
修饰符:public
返回值类型:java.lang.String
方法名:getUsername
方法的形参列表:
修饰符:public
返回值类型:java.lang.String
方法名:getEmail
方法的形参列表:
修饰符:public
返回值类型:void
方法名:setEmail
方法的形参列表:
class java.lang.String
修饰符:public
返回值类型:void
方法名:setUsername
方法的形参列表:
class java.lang.String
修饰符:public static
返回值类型:long
方法名:getSerialVersion
方法的形参列表:
修饰符:public
返回值类型:void
方法名:setPassword
方法的形参列表:
class java.lang.String
演示完毕。此外,我们也能通过上面的方式获得泛型的信息,这里就不在演示了。
曾经有一个朋友总是说,人生苦短,及时行乐。可是我知道他并不快乐,或许这就是人生。