吃透Java基础六:反射

一:什么是反射

Java 反射机制可以让我们在编译期(Compile Time)之外的运行期(Runtime)检查类,接口,变量以及方法的信息。反射还可以让我们在运行期实例化对象,调用方法,通过调用 get/set 方法获取变量的值。

很多人都认为反射在实际的 Java 开发应用中并不广泛,其实不然。当我们在使用 IDE(如 Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。

反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。

二:反射应用

使用 Java 反射机制可以在运行时期检查 Java 类的信息,检查 Java 类的信息往往是你在使用 Java 反射机制的时候所做的第一件事情,通过获取类的信息你可以获取以下相关的内容:Class对象、类名、修饰符、包信息、父类、实现的接口、构造器、方法、变量、注解等等。

1、Class 对象
检查一个类的信息之前,你首先需要获取类的 Class 对象。Java 中的所有类型包括基本类型(int, long, float等等),即使是数组都有与之关联的 Class 类的对象,获取Class对象有如下三种方式:

Class class1 = Class.forName("MyTest");
Class class2 = new MyTest().getClass();
Class class3 = MyTest.class;

2、类名
可以从 Class 对象中获取两个版本的类名

Class class1 = Class.forName("MyTest");
//获取类的全限定名称(包含包名)
class1.getName();

//获取类的名称(不包含包名)
class1.getSimpleName();

3、修饰符
可以通过 Class 对象来访问一个类的修饰符, 即public,private,static 等等的关键字,你可以使用如下方法来获取类的修饰符:

Class class1 = Class.forName("MyTest");
int flag = class1.getModifiers();

修饰符都被包装成一个int类型的数字,这样每个修饰符都是一个位标识(flag bit),这个位标识可以设置和清除修饰符的类型。 可以使用 java.lang.reflect.Modifier 类中的方法来检查修饰符的类型:

Modifier.isAbstract(int modifiers);
Modifier.isFinal(int modifiers);
Modifier.isInterface(int modifiers);
Modifier.isNative(int modifiers);
Modifier.isPrivate(int modifiers);
Modifier.isProtected(int modifiers);
Modifier.isPublic(int modifiers);
Modifier.isStatic(int modifiers);
Modifier.isStrict(int modifiers);
Modifier.isSynchronized(int modifiers);
Modifier.isTransient(int modifiers);
Modifier.isVolatile(int modifiers);

4、包信息
可以使用 Class 对象通过如下的方式获取包信息:

Class class1 = Class.forName("MyTest");
Package pak = class1.getPackage();

通过 Package 对象你可以获取包的相关信息,比如包名。

5、父类
通过 Class 对象你可以访问类的父类

Class class1 = Class.forName("MyTest");
Class superClass = class1.getSuperclass();

可以得到 superclass 对象其实就是一个 Class 类的实例,所以你可以继续在这个对象上进行反射操作。

6、实现的接口
可以通过如下方式获取指定类所实现的接口集合:

Class class1 = Class.forName("MyTest");
Class[] interfaces = class1.getInterfaces();

由于一个类可以实现多个接口,因此 getInterfaces(); 方法返回一个 Class 数组,在 Java 中接口同样有对应的 Class 对象。 注意:getInterfaces() 方法仅仅只返回当前类所实现的接口。当前类的父类如果实现了接口,这些接口是不会在返回的 Class 集合中的,尽管实际上当前类其实已经实现了父类接口。

7、构造器

//获取每一个声明为公有的(Public)构造方法
Class class1 = Class.forName("MyTest");
Constructor[] constructors = class1.getConstructors();

//获取指定参数类型的构造函数
Constructor constructor = class1.getConstructor(new Class[]{String.class});

//获取构造函数的参数
Constructor constructor = class1.getConstructor(new Class[]{String.class});
Class[] typeParameters = constructor.getParameterTypes();

//利用 Constructor 对象实例化一个类
Constructor constructor = class1.getConstructor(new Class[]{String.class});
MyTest myTest=(MyTest) constructor.newInstance("aa");

8、变量
使用 Java 反射机制你可以运行期检查一个类的变量信息(成员变量)或者获取或者设置变量的值。通过使用 java.lang.reflect.Field 类就可以实现上述功能

//Field 对象数组包含了指定类中声明为公有的(public)的所有变量集合
Class class1 = Class.forName("MyTest");
Field[] fields = class1.getFields();

//获取指定的变量
Field field = class1.getField("device");

//获取变量名称
System.out.println(field.getName());

//变量类型
System.out.println(field.getType());

//获取或设置(get/set)变量值,如果变量是静态变量的话(public static)那么在调用 Field.get()/Field.set()
//方法的时候传入 null 做为参数而不用传递拥有该变量的类的实例。
Class class1 = Class.forName("MyTest");
Field field = class1.getField("device");
MyTest myTest = (MyTest) class1.newInstance();
Object fieldObject = field.get(myTest);
field.set(myTest, fieldObject);

//访问私有变量
//Class.getField(String name)和 Class.getFields()只会返回公有的变量,无法获取私有变量
//要想获取私有变量你可以调用 Class.getDeclaredField(String name)方法或者 Class.getDeclaredFields()方法
Class class1 = Class.forName("MyTest");
Field field = class1.getDeclaredField("privateString");

field.setAccessible(true);

MyTest myTest = (MyTest) class1.newInstance();
String fileText = (String) field.get(myTest);

field.setAccessible(true)这行代码,通过调用 setAccessible()方法会关闭指定类 Field 实例的反射访问检查,这行代码执行之后不论是私有的、受保护的以及包访问的作用域,你都可以在任何地方访问,即使你不在他的访问权限作用域之内。但是你如果你用一般代码来访问这些不在你权限作用域之内的代码依然是不可以的,在编译的时候就会报错。

9、方法

//Method 对象数组包含了指定类中声明为公有的(public)的所有变量集合
Class class1 = Class.forName("MyTest");
Method[] methods = class1.getMethods();

//知道你要调用方法的具体参数类型,你就可以直接通过参数类型来获取指定的方法
Class class1 = Class.forName("MyTest");
Method method = class1.getMethod("doSomething",new Class[]{String.class});

//方法参数以及返回类型
Class[] parameterTypes = method.getParameterTypes();
Class returnType = method.getReturnType();

//通过 Method 对象调用方法
Class class1 = Class.forName("MyTest");
Method method = class1.getMethod("doSomething", new Class[]{String.class});
MyTest myTest = (MyTest) class1.newInstance();
Object returnValue = method.invoke(myTest,"parameter");

//访问私有方法
//访问一个私有方法你需要调用 Class.getDeclaredMethod(String name, Class[] 
//parameterTypes)或者 Class.getDeclaredMethods() 方法。 Class.getMethod(String name, Class[] 
//parameterTypes)和 Class.getMethods()方法,只会返回公有的方法,无法获取私有方法
Class class1 = Class.forName("MyTest");
Method method = class1.getDeclaredMethod("doSomething", null);
method.setAccessible(true);
MyTest myTest = (MyTest) class1.newInstance();
String returnValue = (String) method.invoke(myTest,null);

Method.invoke(Object target, Object … parameters)方法第一个参数是你要调用方法的对象,如果是一个静态方法调用的话则可以用 null 代替指定对象作为 invoke()的参数,在上面这个例子中,如果 doSomething 不是静态方法的话,你就要传入有效的 MyObject 实例而不是 null。 Method.invoke(Object target, Object … parameters)方法的第二个参数是一个可变参数列表,但是你必须要传入与你要调用方法的形参一一对应的实参。就像上个例子那样,方法需要 String 类型的参数,那我们必须要传入一个字符串。

10、注解
注解是 Java 5 的一个新特性。注解是插入你代码中的一种注释或者说是一种元数据(meta data)。这些注解信息可以在编译期使用预编译工具进行处理(pre-compiler tools),也可以在运行期使用 Java 反射机制进行处理,下面定义一个MyAnnotation注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)

public @interface MyAnnotation {
  public String name();
  public String value();
}

在 interface 前面的@符号表名这是一个注解,一旦你定义了一个注解之后你就可以将其应用到你的代码中,就像之前我们的那个例子那样。 在注解定义中的两个指示@Retention(RetentionPolicy.RUNTIME)和@Target(ElementType.TYPE),说明了这个注解该如何使用。 @Retention(RetentionPolicy.RUNTIME)表示这个注解可以在运行期通过反射访问。如果你没有在注解定义的时候使用这个指示那么这个注解的信息不会保留到运行期,这样反射就无法获取它的信息。 @Target(ElementType.TYPE) 表示这个注解只能用在类型上面(比如类跟接口)。你同样可以把Type改为Field或者Method,或者你可以不用这个指示,这样的话你的注解在类,方法和变量上就都可以使用了。

//获取类注解
Class class1 = Class.forName("MyTest");
Annotation annotation = class1.getAnnotation(MyAnnotation.class);

//方法注解
Method method = class1.getMethod("doSomething", null);
Annotation[] annotations = method.getAnnotations();
Annotation annotation1 = method.getAnnotation(MyAnnotation.class);

//参数注解
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Class[] parameterTypes = method.getParameterTypes();
int i=0;
for(Annotation[] annotations : parameterAnnotations){
  Class parameterType = parameterTypes[i++];
  for(Annotation annotation : annotations){
    if(annotation instanceof MyAnnotation){
        MyAnnotation myAnnotation = (MyAnnotation) annotation;
        System.out.println("param: " + parameterType.getName());
        System.out.println("name : " + myAnnotation.name());
        System.out.println("value: " + myAnnotation.value());
    }
  }
}

//变量注解
Annotation[] annotations = field.getDeclaredAnnotations();
Annotation annotation = field.getAnnotation(MyAnnotation.class);

11、泛型
我常常在一些文章以及论坛中读到说 Java 泛型信息在编译期被擦除所以你无法在运行期获得有关泛型的信息。其实这种说法并不完全正确的,在一些情况下是可以在运行期获取到泛型的信息。

Java 在编译时会在字节码里指令集之外的地方保留部分泛型信息,泛型接口、类、方法定义上的所有泛型、成员变量声明处的泛型都会被保留类型信息,其他地方的泛型信息都会被擦除。

Java 的泛型机制虽然在编译期间进行了擦除,但是在编译 Java 源代码成 class 文件中还是保存了泛型相关的信息,这些信息被保存在 class 字节码的常量池中,使用了泛型的代码处会生成一个 signature 签名字段,通过签名 signature 字段指明这个常量池的地址,JDK 提供了方法去读取这些泛型信息的方法,然后再借助反射就可以获得泛型参数的具体类型

所以获取泛型参数类型的实质就是通过 Class 类的 getGenericSuperClass() 方法返回一个 ParameterizedType 对象(对于 Object、接口和原始类型返回 null,对于数组 class 返回 Object.class),ParameterizedType 表示带有泛型参数类型的 Java 类型,JDK1.5 引入泛型后 Java 中所有的 Class 都实现了 Type 接口,ParameterizedType 继承了 Type 接口,所有包含泛型的 Class 类都会自动实现这个接口。

泛型的擦除机制实际上擦除的是除结构化信息外的所有东西(结构化信息指与类结构相关的信息,而不是与程序执行流程有关的,即与类及其字段和方法的类型参数相关的元数据都会被保留下来通过反射获取到)。

public class MyTest {
    public List<String> myList = new ArrayList<>();

    public List<String> getMyList(List<String> myList) {
        return myList;
    }
 }
  • 泛型方法返回类型
    如果你获得了 java.lang.reflect.Method 对象,那么你就可以获取到这个方法的泛型返回类型信息
Class class1 = Class.forName("MyTest");
Method method = class1.getMethod("getMyList", List.class);
Type returnType = method.getGenericReturnType();
if(returnType instanceof ParameterizedType){
   ParameterizedType type = (ParameterizedType) returnType;
   Type[] typeArguments = type.getActualTypeArguments();
   for(Type typeArgument : typeArguments){
      System.out.println((Class) typeArgument);
   }
}

输出:class java.lang.String

  • 泛型方法参数类型
Class class1 = Class.forName("MyTest");
Method method = class1.getMethod("getMyList", List.class);
Type[] genericParameterTypes = method.getGenericParameterTypes();
    for(Type genericParameterType : genericParameterTypes){
       if(genericParameterType instanceof ParameterizedType){
           ParameterizedType aType = (ParameterizedType) genericParameterType;
           Type[] parameterArgTypes = aType.getActualTypeArguments();
           for(Type parameterArgType : parameterArgTypes){
                System.out.println((Class) parameterArgType);
           }
        }
    }

输出:class java.lang.String

  • 泛型变量类型
Class class1 = Class.forName("MyTest");
Field field = class1.getField("myList");
Type genericFieldType = field.getGenericType();
if(genericFieldType instanceof ParameterizedType){
   ParameterizedType aType = (ParameterizedType) genericFieldType;
   Type[] fieldArgTypes = aType.getActualTypeArguments();
   for(Type fieldArgType : fieldArgTypes){
       Class fieldArgClass = (Class) fieldArgType;
       System.out.println((Class) fieldArgType);
   }
}

输出:class java.lang.String

12、数组
Java 反射机制通过 java.lang.reflect.Array 这个类来处理数组。

//创建一个数组
int[] intArray = (int[]) Array.newInstance(int.class, 3);

//访问数组
Array.set(intArray, 0, 123);
Array.set(intArray, 1, 456);
Array.set(intArray, 2, 789);

//获取数组的 Class 对象
Class intArray = Class.forName("[I");

//获取数组的成员类型
Class class1 = Class.forName("[I");
Class classType = class1.getComponentType();
System.out.println(classType);

13、动态代理
利用Java反射机制你可以在运行期动态的创建接口的实现。 java.lang.reflect.Proxy 类就可以实现这一功能。这个类的名字(译者注:Proxy 意思为代理)就是为什么把动态接口实现叫做动态代理。动态的代理的用途十分广泛,比如数据库连接和事物管理(transaction management)还有单元测试时用到的动态 mock 对象以及 AOP 中的方法拦截功能等等都使用到了动态代理。

创建代理
可以通过使用 Proxy.newProxyInstance()方法创建动态代理。 newProxyInstance()方法有三个参数: 1、类加载器(ClassLoader)用来加载动态代理类。 2、一个要实现的接口的数组。 3、一个 InvocationHandler 把所有方法的调用都转到代理上。

public interface InvocationHandler{
  Object invoke(Object proxy, Method method, Object[] args)
         throws Throwable;
}

public class MyInvocationHandler implements InvocationHandler{

  public Object invoke(Object proxy, Method method, Object[] args)
  throws Throwable {
    
  }
}

invoke()方法中的 Method 对象参数代表了被动态代理的接口中要调用的方法,从这个 method 对象中你可以获取到这个方法名字,方法的参数,参数类型等等信息。

Object 数组参数包含了被动态代理的方法需要的方法参数。注意:原生数据类型(如int,long等等)方法参数传入等价的包装对象(如Integer, Long等等)。

InvocationHandler handler = new MyInvocationHandler();
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
                            MyInterface.class.getClassLoader(),
                            new Class[] { MyInterface.class },
                            handler);

执行完这段代码之后,变量 proxy 包含一个 MyInterface 接口的的动态实现。所有对 proxy 的调用都被转向到实现了 InvocationHandler 接口的 handler 上。

发布了66 篇原创文章 · 获赞 138 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/u013277209/article/details/102782208