第三章 反射 && 注解 && 依赖注入

文章目录

  • 反射
    • 简介 & 功能 & 应用场景
    • 基本使用
      • 通过Java反射查看类信息
      • 通过Java反射生成并操作对象
    • Java反射机制 与 动态代理
    • Java反射机制 与 泛型
    • 反射在项目中的应用
    • 反射效率低 原因 & 解决
  • 注解(Annotation)
    • 简介
    • 工作机制
    • JDK注解 / 自定义注解
    • 如何解析注解
  • 控制反转(IOC)/依赖注入(DI)
    • ButterKnife用法 & 原理?

反射

简介 & 功能 & 应用场景

  • Java反射机制定义
    Java 反射机制是在运行状态中,对于任意一个类,都能够获得这个类的所有属性和方法;对于任意一个对象都能够调用它的任意一个属性和方法。这种在运行时动态的获取类信息以及动态调用对象的方法的功能称为Java 的反射机制。
  • Java 反射机制的功能
  1. 在运行时判断任意一个对象所属的类。
  2. 在运行时构造任意一个类的对象。
  3. 在运行时判断任意一个类所具有的成员变量和方法。
  4. 在运行时调用任意一个对象的方法。
  5. 生成动态代理。
  • Java 反射机制的应用场景
  1. 逆向代码 ,例如反编译
  2. 与注解相结合的框架 例如Retrofit
  3. 单纯的反射机制应用框架 例如EventBus
  4. 动态生成类框架 例如Gson

基本使用

通过Java反射查看类信息

(1)获取Class类对象——描述.class字节码文件
每个类被加载之后,系统就会为该类生成一个对应的Class对象。通过该Class对象就可以访问到JVM中的这个类。
在Java程序中获得Class对象通常有如下三种方式:

  1. 使用Class类的forName(String clazzName)静态方法。该方法需要传入字符串参数,该字符串参数的值是某个类的全限定名(必须添加完整包名)。
  2. 调用某个类的class属性来获取该类对应的Class对象。
  3. 调用某个对象的getClass()方法。该方法是java.lang.Object类中的一个方法。
//第一种方式 通过Class类的静态方法——forName()来实现
class1 = Class.forName("com.lvr.reflection.Person");
//第二种方式 通过类的class属性
class1 = Person.class;
//第三种方式 通过对象getClass方法
Person person = new Person();
Class<?> class1 = person.getClass();

(2)类成员变量的反射

  1. 获取类成员变量:
  • Field[] getFields():获取所有public 修饰的成员变量
  • Field getField(String name):获取指定名称的public修饰的成员变量
  • Field[] getDeclaredFields():获取所有的成员变量,不考虑修饰符
  • Filed getDeclaredField(String name):获取指定名称的成员变量,不考虑修饰符
  1. Filed:成员变量
  • get(Object object) :获取值
  • void set(Object obj, Object value):设置值
  • setAccessible(true) :忽略访问权限修饰符的安全检查,用于暴力反射,修改私有成员变量的值
Field[] allFields = class1.getDeclaredFields();//获取class对象的所有属性
Field[] publicFields = class1.getFields();//获取class对象的public属性
Field ageField = class1.getDeclaredField("age");//获取class指定属性
Field desField = class1.getField("des");//获取class指定的public属性

(3)类成员方法的反射

  1. 获取类成员方法:
  • Method[] getMethods():获取所有public修饰的成员方法
  • Method getMethod(String name,类… parameterTypes):获取指定的public修饰的成员方法,name 为方法名,parameterTypes为参数列表(重载)
  • Method[] getDeclaredMethods():获取所有成员方法
  • Method getDeclaredMethod(String name,类… parameterTypes):获取指定的成员方法,name 为方法名,parameterTypes为参数列表(重载)
  1. Method
  • invoke(obj … args):执行方法
  • setAccessible(true) :忽略访问权限修饰符的安全检查,用于暴力反射,修改私有成员方法的值
Method[] methods = class1.getDeclaredMethods();//获取class对象的所有声明方法
Method[] allMethods = class1.getMethods();//获取class对象的所有public方法 包括父类的方法
Method method = class1.getMethod("info", String.class);//返回次Class对象对应类的、带指定形参列表的public方法
Method declaredMethod = class1.getDeclaredMethod("info", String.class);//返回次Class对象对应类的、带指定形参列表的方法

(4)类构造方法的反射

  1. 获取类构造方法:
  • Constructor[] getConstructors():获取public修饰的构造方法
  • Constructor getConstructor(类… parameterTypes):获取指定的public修饰的构造方法(构造方法的方法名 = 类名),parameterTypes为参数列表
  • Constructor[] getDeclaredConstructors():获取所有构造方法
  • Constructor getDeclaredConstructor(类… parameterTypes):获取指定的构造方法,name为方法名(构造方法的方法名 = 类名),parameterTypes为参数列表
  1. Constructor:构造方法
  • T.newInstance(Object… init args):创建对象
  • Class.newInstance():如果使用空参数构造方法创建对象,操作可以简化为:Class对象的newInstance方法
  • setAccessible(true) :忽略访问权限修饰符的安全检查,用于暴力反射,修改私有构造方法的值
Constructor<?>[] allConstructors = class1.getDeclaredConstructors();//获取class对象的所有声明构造函数
Constructor<?>[] publicConstructors = class1.getConstructors();//获取class对象public构造函数
Constructor<?> constructor = class1.getDeclaredConstructor(String.class);//获取指定声明构造函数
Constructor publicConstructor = class1.getConstructor(String.class);//获取指定声明的public构造函数

通过Java反射生成并操作对象

  • 生成类的实例对象
//第一种方式 Class对象调用newInstance()方法生成
Object obj = class1.newInstance();
//第二种方式 对象获得对应的Constructor对象,再通过该Constructor对象的newInstance()方法生成
Constructor<?> constructor = class1.getDeclaredConstructor(String.class);//获取指定声明构造函数
obj = constructor.newInstance("hello");
  • 调用类的方法
// 生成新的对象:用newInstance()方法
Object obj = class1.newInstance();
//首先需要获得与该方法对应的Method对象
Method method = class1.getDeclaredMethod("setAge", int.class);
//开启调用该方法的权限
method.setAccessible(true);
//调用指定的函数并传递参数
method.invoke(obj, 28);
  • 访问成员变量值
//生成新的对象:用newInstance()方法 
Object obj = class1.newInstance();
//获取age成员变量
Field field = class1.getField("age");
//将obj对象的age的值设置为10
field.setInt(obj, 10);
//获取obj对象的age的值
field.getInt(obj);

Java反射机制 与 动态代理

第七章 设计模式 —— 动态代理

  • 静态代理
    在程序运行前就已经存在代理类的.class文件,已经确定代理类和委托类的关系。
  • 动态代理
    通过动态代码可实现对不同类、不同方法的代理。动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以不存在代理类的字节码文件(.class)。代理类和委托类的关系在程序运行时确定。
    动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。而且动态代理提高了软件系统的可扩展性,因为Java 反射机制可以生成任意类型的动态代理类。(而每个静态代理只能为一个接口服务)
  • 实现原理 —— 反射
  1. 动态代理类只能代理接口,需创建一个实现接口InvocationHandler的调用处理器,它必须实现invoke方法。invoke方法是调用代理接口所有方法都要调用,返回值是被代理接口的一个实现类(动态代理类)。
public class LogHandler implements InvocationHandler {  
  
    // 目标对象  
    private Object targetObject;  
    //绑定关系,也就是关联到哪个接口(与具体的实现类绑定)的哪些方法将被调用时,执行invoke方法。              
    public Object newProxyInstance(Object targetObject){  
        this.targetObject=targetObject;  
        //该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例    
        //第一个参数指定产生代理对象的类加载器,需要将其指定为和目标对象同一个类加载器  
        //第二个参数要实现和目标对象一样的接口,所以只需要拿到目标对象的实现接口  
        //第三个参数表明这些被拦截的方法在被拦截时需要执行哪个InvocationHandler的invoke方法  
        //根据传入的目标返回一个代理对象  
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),  
                targetObject.getClass().getInterfaces(),this);  
    }  
    @Override  
    //关联的这个实现类的方法被调用时将被执行  
    /*InvocationHandler接口的方法,proxy表示代理,method表示原对象被调用的方法,args表示方法的参数*/  
    public Object invoke(Object proxy, Method method, Object[] args)  
            throws Throwable {  
        System.out.println("start-->>");  
        for(int i=0;i<args.length;i++){  
            System.out.println(args[i]);  
        }  
        Object ret=null;  
        try{  
            /*原对象方法调用前处理日志信息*/  
            System.out.println("satrt-->>");  
              
            //调用目标方法  
            ret=method.invoke(targetObject, args);  
            /*原对象方法调用后处理日志信息*/  
            System.out.println("success-->>");  
        }catch(Exception e){  
            e.printStackTrace();  
            System.out.println("error-->>");  
            throw e;  
        }  
        return ret;  
    }  
}  
  1. 创建接口及具体实现类(委托类)
// 接口
public interface UserManager{
	public void addUser(String userId, String userName);
	public void delUser(String userId) ;
	public String findUser(String userId) ;
	public void modifyUser(String userId, String userName) ;
}
// 实现类(委托类)
public class UserManagerImpl implements UserManager {  
  
    @Override  
    public void addUser(String userId, String userName) {  
        System.out.println("UserManagerImpl.addUser");  
    }  
  
    @Override  
    public void delUser(String userId) {  
        System.out.println("UserManagerImpl.delUser");  
    }  
  
    @Override  
    public String findUser(String userId) {  
        System.out.println("UserManagerImpl.findUser");  
        return "张三";  
    }  
  
    @Override  
    public void modifyUser(String userId, String userName) {  
        System.out.println("UserManagerImpl.modifyUser");  
  
    }  
}  
  1. 通过动态代理类调用Proxy的静态方法newProxyInstance,提供ClassLoader和代理接口类型数组动态创建一个代理类,并通过代理调用方法
public class Client {  
  
    public static void main(String[] args){  
        LogHandler logHandler=new LogHandler();  
        UserManager userManager=(UserManager)logHandler.newProxyInstance(new UserManagerImpl());  
        // 获取动态代理对象
        userManager.addUser("1111", "张三");  // 调用动态代理的addUser方法,该调用会转发到logHandler的invoke上,从而达到动态代理的效果
    }  
}  

可以看到,我们可以通过LogHandler代理不同类型的对象,如果我们把对外的接口都通过动态代理来实现,那么所有的函数调用最终都会经过invoke函数的转发,因此我们就可以在这里做一些自己想做的操作,比如日志系统、事务、拦截器、权限控制等。这也就是AOP(面向切面编程)的基本原理。

AOP(AspectOrientedProgramming):将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码—解耦。

Java反射机制 与 泛型

反射在项目中的应用

应用一:简单工厂创建对象

  • 需求
    通过一个工厂类创建不同类型的实例。
  • 解决方案
  1. 在配置文件中配置需要创建对象的信息
  2. 通过类加载器加载配置文件
  3. 获取类对象,通过类的全限定名创建实例对象
  • 代码
public class BasicFactory {
    private BasicFactory(){}

    private static BasicFactory bf = new BasicFactory();
    private static Properties pro = null;

    static{
        pro = new Properties();
        try{    
            //通过类加载器加载配置文件
            pro.load(new FileReader(BasicFactory.class.getClassLoader().
                    getResource("config.properties").getPath()));
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static BasicFactory getFactory(){
        return bf;
    }

    //使用泛型获得通用的对象
    public  <T> T newInstance(Class<T> clazz){
        String cName = clazz.getSimpleName();   //获得字节码对象的类名
        String clmplName = pro.getProperty(cName);   //根据字节码对象的类名通过配置文件获得类的全限定名

        try{
            return (T)Class.forName(clmplName).newInstance();   //根据类的全限定名创建实例对象
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

应用二:过滤符合输入关键字的数据

  • 需求
    输入关键字检索通过网络请求加载好的列表数据,过滤出符合过滤条件(包含关键字)的数据项。
  • 为何使用反射
    一般解决方案为,遍历存放数据的List,然后每项对比关键字,将过滤后的数据存入一个新的List中,返回给RecyclerView的Adapter进行数据刷新。
    有2个问题:
  1. List获取的数据由于业务不同,其泛型也不同,且泛型没有关联性
  2. 每一个泛型都是一个Bean,要对Bean中的属性尽兴过滤,属性是不确定的
    由于列表数据过滤在项目中很常用,几乎每个列表项都有过滤功能,如果对于每种列表都采用硬编码进行封装,代码的复用性很低,且项目不易维护。
  • 解决方案
    采用反射机制
    主要是getClassInfo获取某个类需要过滤的数据项,并与关键字对比并过滤。
  • 代码
public class ListFilter<T> {

    /**
     * 过滤ArrayList中的关键字数据
     * @param models 网络获取到的数据列表
     * @param query 过滤关键字
     * @param propertyName 泛型的数据过滤项
     * @return 返回过滤后的数据
     */
    public ArrayList<T> filter(ArrayList<T> models, String query, String propertyName) {

        ArrayList<T> filteredModelList = null;
        //实例化这个类赋给o
        try {
            query = query.toLowerCase();
            filteredModelList = new ArrayList<>();
            for (T model : models) {
                final String text = getClassInfo(model,model.getClass().getName(),propertyName).toLowerCase();
                if (text.contains(query)) {
                    filteredModelList.add(model);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return filteredModelList;
    }

    /**
     *  获取类的属性值
     * @param obj 类实例
     * @param classNameString 类名
     * @param propertyNameString 获取的属性名
     * @return  属性值
     */
    private  String getClassInfo(Object obj,String classNameString,String propertyNameString) {
        String returnString="";
        try{
            Class classInfo = Class.forName(classNameString);
            if(!(classInfo.isInstance(obj))){
                L.e("传入的java实例与配置的java对象类型不符!");
                return returnString;
            }
            Field field = classInfo.getDeclaredField(propertyNameString);
            field.setAccessible(true);
            returnString=field.get(obj).toString();
        }catch(Exception e){
            e.printStackTrace();
        }
        return returnString;
    }
}

反射效率低 原因 & 解决

导致反射效率慢的因素及解决:

  1. 获取Class对象时使用Class.forName效率低,耗时。把Class.forName返回的Class对象缓存起来,下一次使用的时候直接从缓存里面获取,这样就极大的提高了获取Class的效率。
  2. 同理,在项目启动时将反射需要的相关配置和数据(Field、Method、Constructor等)加载进内存,在运行时直接从缓存中取这些元数据进行反射操作。
  3. 调用method.setAccessible(true)
    jdk在设置获取字段,调用方法的时候会执行安全访问检查,而此类操作会比较耗时,所以通过setAccessible(true)的方式可以关闭安全检查,从而提升反射效率。
  4. 采用高性能的反射工具包,如ReflectASM。
  5. 使用高版本JDK,提高反射性能。
  6. 反射效率慢,但速度也是可以接受的。所以对于反射不应该因为速度慢而对其"望而却步"。

注解(Annotation)

简介

注解(元数据)是一种代码级别的说明。是JDK 5.0 后引入的新特性。注解作为程序的元数据嵌入到程序中,声明在类、成员变量、成员方法等前面,用来对这些元素进行说明,注释。注解可以被解析工具/编译工具解析。
Annotation的作用可分为3类:

  1. 编写文档:通过代码里标识的注解生成文档
  2. 代码分析:通过反射获取注解信息并对代码进行分析
Class<ReflectTest> reflectTestClass = ReflectTest.class;	// 1.通过反射获取字节码文件对象
Pro an = reflectTestClass.getAnnotation(Pro.class);	// 2.调用getAnnotation(class)获取注解对象
String className = an.className();
String methodName = an.methodName();	// 3.调用注解对象中定义的抽象方法,获取返回值(返回值即注解信息)
  1. 编译检查:如@override 用于检测被该注解标注的方法是否是继承自父类(接口)

工作机制

注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java 运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。

JDK注解 / 自定义注解

  • JDK —— 编译检查
  1. @Override
    用于检测被该注解标注的方法是否是继承自父类(接口)的。
    如果某个方法带有该注解但并没有覆写超类相应的方法,则编译器会生成一条错误信息。如果父类没有这个要覆写的方法,则编译器也会生成一条错误信息。
    @Override可适用元素为方法,仅仅保留在java源文件中。
  2. @Deprecated
    用于标注已经过时的方法。
    用于告知编译器,某一程序元素(比如方法,成员变量)不建议使用了(即过时了)。编译器会出现警告。
    @Deprecated可适合用于除注解类型声明之外的所有元素,保留时长为运行时。
  3. @SuppressWarnnings
    用于通知java编译器禁止特定的编译警告。
    @SuppressWarnings可适合用于除注解类型声明和包名之外的所有元素,仅仅保留在java源文件中。
  • 自定义注解 —— 代码分析/编写文档
  • 自定义注解的格式
元注解
public @interface 注解名称{
	... 属性列表
}

// 举例
@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotataion{
    String name();
    String website() default "hello";
    int revision() default 1;
}

public class AnnotationDemo {
// 当注解中有成员变量时,若没有默认值,需要在使用注解时,指定成员变量的值。
	@MyAnnotataion(name="lvr", website="hello", revision=2)
    public void demo(){
        System.out.println("I am demo method");
    }
}
  • 元注解
    通过元注解注解其他注解。Java提供了4个标准元注解:
    (1)Target:描述注解作用位置,如CONSTRUCTOR、FIELD、METHOD、TYPE等等
    (2)Retention:描述注解保留的阶段,包括SOURCE、CLASS、RUNTIME
    (3)Documented:描述注解是否被抽取到生成的API文档中
    (4)Inherited:描述注解是否被子类继承
  • 属性列表
    接口中的抽象方法被称为注解的属性。每一个抽象方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数类型。

如何解析注解

  • 通过反射技术解析自定义注解
    关于反射类位于包java.lang.reflect,其中有一个接口AnnotatedElement。该接口定义了注释相关的几个核心方法,如下:
    第三章 反射 && 注解 && 依赖注入_第1张图片
    因此,当获取了某个类的Class对象,然后获取其Field,Method等对象,通过上述4个方法提取其中的注解,然后获得注解的详细信息。
public class AnnotationParser {
    public static void main(String[] args) throws SecurityException, ClassNotFoundException {
        String clazz = "com.lvr.annotation.AnnotationDemo";
        Method[]  demoMethod = AnnotationParser.class
                .getClassLoader().loadClass(clazz).getMethods();

        for (Method method : demoMethod) {
            if (method.isAnnotationPresent(MyAnnotataion.class)) {
                 MyAnnotataion annotationInfo = method.getAnnotation(MyAnnotataion.class);
                 System.out.println("method: "+ method);
                 System.out.println("name= "+ annotationInfo.name() +
                         " , website= "+ annotationInfo.website()
                        + " , revision= "+annotationInfo.revision());
            }
        }
    }
}
  • 注解解析案例:实现反射动态加载类
  1. 通过反射获取注解定义位置的对象(Class,Method,Field)
  2. 获取指定的注解:getAnnotation(Class)其实就是在内存中生成了一个该注解接口的子类实现对象
  3. 调用注解中的抽象方法获取配置的属性值

Pro.java


//注解配置文件:描述需要执行的类名和方法名
@Target ({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Pro{
	String className();
	String methodName();
}

ReflectTest.java

public class ReflectTest{
		public static void main(String[] args) throws Exception{
		// 前提:不能改变该类的任何代码,可以创建任意类的对象,可以执行任意方法
		// 1. 解析注解
		// 1.1 获取该类的字节码文件对象
		Class<ReflectTest> reflectTestClass = ReflectTest.class;
		// 2.获取上边的注解对象,起始就是在内存中生成了一个注解接口的子类实现对象
		// public class ProImpl implements Pro{
		//	public String className(){
		//		return "cn.itcast.annotation.Demo1";
		// 	}
		// 	public String methodName(){
		//		return "show";
		//	}
		// }
		Pro an = reflectTestClass.getAnnotation(Pro.class);
		// 3.调用注解对象中定义的抽象方法,获取返回值
		String className = an.className();
		String methodName = an.methodName();
		// 4. 加载该类进内存
		Class cls = Class.forName(className);
		// 5. 创建对象
		Object obj = cls.newInstance();
		// 6. 获取方法对象
		Method method = cls.getMethod(methodName);
		// 7. 执行方法
		method.invoke(obj);
}

控制反转(IOC)/依赖注入(DI)

  • 依赖(Dependency)
    依赖是类与类之间的相互联系,如人(Person)出行可以使用自行车Bike、轿车Car或火车(Train),因此Person类可以依赖Bike类、Car类和Train类。
    每次出行都需要修改Person类代码。
public class Person {

    private Bike mBike;
    private Car mCar;
    private Train mTrain;

    public Person(){
        mBike = new Bike();
        // mCar = new Car();
        // mTrain = new Train();
    }

    public void goOut(){
        System.out.println("出游");
        mBike.drive();
        // mCar.drive();
        // mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person person = new Person();
        person.goOut();
    }
}
  • 依赖反转(IOC)
    IOC对上层模块与底层模块进行了进一步的解耦。控制反转的意思是反转了上层模块对于底层模块的依赖控制。
public class Person {

    private Driveable mDriveable;

    public Person2(Driveable driveable){
        this.mDriveable = driveable;
    }

    public void goOut(){
        System.out.println("出门啦");
        mDriveable.drive();
        // mCar.drive();
        // mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person2 person = new Person2(new Car());
        person.goOut();
    }
}
  • 依赖注入(DI)
    DI是指在外部将依赖实例化并赋值给IOC容器。依赖注入有3种方式:构造函数注入、setter方式注入、接口注入。
public interface DepedencySetter {
    void set(Driveable driveable);
}
public class Person2  implements DepedencySetter {

    //接口方式注入
    @Override
    public void set(Driveable driveable) {
        this.mDriveable = mDriveable;
    }

    private Driveable mDriveable;

    //构造函数注入
    public Person2(Driveable driveable){
        this.mDriveable = driveable;
    }

    //setter 方式注入
    public void setDriveable(Driveable mDriveable) {
        this.mDriveable = mDriveable;
    }

    public void goOut(){
        System.out.println("出门啦");
        mDriveable.drive();
        //mCar.drive();
//        mTrain.drive();
    }

    public static void main(String ... args){
            //TODO:
        Person2 person = new Person2(new Car());
        person.goOut();
    }
}

ButterKnife用法 & 原理?

ButterKnife:是视图注入中相对简单易懂的开源框架,其优势在于:

  1. 强大的View绑定和Click事件处理功能,简单代码,提高开发效率
  2. 方便的处理Adapter和ViewHolder绑定问题
  3. 提高APP运行效率,使用配置方便
  4. 代码清晰,可读性强
@InjectView(R.id.listview)
ListView mListview;
@OnItemClick(R.id.listview)
public void onItemClick(int position){
     Toast.makeText(getBaseContext(), "item"+position, Toast.LENGTH_SHORT).show();
}

你可能感兴趣的:(Java面试之旅)