zoukankan      html  css  js  c++  java
  • 理解Android中的注解与反射

    反射

    Java反射(Reflection)定义

    Java反射机制是指在运行状态中

    对于任意一个类,都能知道这个类的所有属性和方法;
    对于任何一个对象,都能够调用它的任何一个方法和属性;

    这样动态获取新的以及动态调用对象方法的功能就叫做反射。

    比如像下面:

    //获取类
    Class c = Class.forName("java.lang.String");
    // 获取所有的属性
    Field[] fields = c.getDeclaredFields();
    StringBuffer sb = new StringBuffer();
    sb.append(Modifier.toString(c.getModifiers()) + " class " + c.getSimpleName() + "{
    ");
    // 遍历每一个属性
    for (Field field : fields) {
         sb.append("	");// 空格
         sb.append(Modifier.toString(field.getModifiers()) + " ");// 获得属性的修饰符,例如public,static等等
         sb.append(field.getType().getSimpleName() + " ");// 属性的类型的名字
         sb.append(field.getName() + ";
    ");// 属性的名字+回车
    }
    sb.append("}
    ");
    System.out.println(sb);
    

    就可以获得 String ,这个我们常用类的所有属性:


    string_property

    再比如:

    //获取类
    Class c = Class.forName("java.lang.String");
    // 获取所有的方法
    Method[] ms = c.getDeclaredMethods();
     //遍历输出所有方法
    for (Method method : ms) {
       //获取方法所有参数
       Parameter[] parameters = method.getParameters();
       String params = "";
       if (parameters.length > 0) {
            StringBuffer stringBuffer = new StringBuffer();
            for (Parameter parameter : parameters) {
                 stringBuffer.append(parameter.getType().getSimpleName() + " " + parameter.getName() + ",");
            }
            //去掉最后一个逗号
           params = stringBuffer.substring(0, stringBuffer.length() - 1);
    }
    System.err.println(Modifier.toString(method.getModifiers())
                        + " " + method.getReturnType().getSimpleName()
                        + " " + method.getName()
                        + " (" +params  + ")");
    }
    

    可以获得String 类的所有方法(图片只截取了部分方法,实际有很多就不占篇幅了):


    string_method

    Java反射机制API

    主要的几个类

    Java中有关反射的类有以下这几个:

    用途
    java.lang.Class 编译后的class文件的对象
    java.lang.reflect.Constructor 构造方法
    java.lang.reflect.Field 类的成员变量(属性)
    java.lang.reflect.Method 类的成员方法
    java.lang.reflect.Modifier 判断方法类型
    java.lang.annotation.Annotation 类的注解

    具体实现

    为了方便描述,这里我们创建一个类 TestClass

    public class TestClass {
        private String address;
        private String port;
        private int number;
    
       public void printInfo() {
            System.out.println("info is " + address + ":" + port);
        }        
        private void myMethod(int number,String sex) {
    
        }
    
        public String getPort() {
            return port;
        }
    
        public void setPort(String port) {
            this.port = port;
        }
    
        public String getAddress() {
            return address;
        }
    
        public void setAddress(String address) {
            this.address = address;
        }
    
        public int getNumber() {
            return number;
        }
    
        public void setNumber(int number) {
            this.number = number;
        }
    
    
    }
    

    这个类很简单,包含三个成员变量address,port和number,以及它们各自的get,set方法。
    两个自定义的方法printInfo()和myMethod()。

    下面我们就看一下如何通过反射,获取这个TestClass的所有“信息”

    • 1.获取Class
      关于Class的获取有三种写法:

      

    //获取类的三种方法:
    Class c = Class.forName("java.lang.String");  //这里一定要用完整的包名
    Class c1=String.class;
    String str = new String();
    Class c2=str.getClass();
    

     

    • 这里获取的c,c1以及c2都是相等的。一般在反射中会用第一种写法。

    • 2.获取类的属性(成员变量)

    Field[] fields = c.getDeclaredFields();
    

    这里返回的是一个数组 ,包含所有的属性。获取到的每一个属性Filed,包含一系列的方法可以获取及修改他的内容。
    如下所示:

    遍历每一个属性
    for (Field field : fields) {
       sb.append("	");// 空格
       sb.append(Modifier.toString(field.getModifiers()) + " ");// 获得属性的修饰符,例如public,static等等
       sb.append(field.getType().getSimpleName() + " ");// 属性的类型的名字
       sb.append(field.getName() + ";
    ");// 属性的名字+回车
    }
    

    这里我们可以得到TestClass的所有属性:


     
      • 3.获取类的方法
    // 获取所有的方法
    Method[] ms = c.getDeclaredMethods();
    
    • 和属性类似,我们依然可以通过一系列的方法获取到方法的返回值类型,名称以及参数。下面的表格中总结了一些关键方法:

    reflection

    类似的获取到TestClass的所有方法:


    test_method

    这里可以看到,获取的TestClass的属性和方法同我们定义的是完全一致的。

    这里我们顺便调用一下TestClass的printInfo方法:

    new TestClass().printInfo();
    

    用于所有属性没有做初始化,所以得到如下输出:


    null

    可以看到,利用反射我们可以很方便的去“反编译”一个class。那么我们用反射这么做的意义是什么呢?不要着急,下面我们先来了解一下注解

    Java 注解(Annotation)

    什么是注解

    关于注解的定义网上有很多说法,就不再赘述。这里我们就说两点

    Annotation(注解)就是Java提供了一种源程序中的元素关联任何信息或者任何元数据(metadata)的途径和方法。

    Annotation是被动的元数据,永远不会有主动行为

    既然是被动数据,对于那些已经存在的注解,比如Override,我们只能看看而已,并不知道它具体的工作机制是什么;所以想要理解注解,就直接从自定义注解开始。

    自定义注解

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD})
    @Documented
    @Inherited
    public @interface Bind {
        int value() default 1;
        boolean canBeNull() default false;
    }
    

    这就是自定义注解的形式,我们用@interface 表明这是一个注解,Annotation只有成员变量,没有方法。Annotation的成员变量在Annotation定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。比如上面的value和canBeNull。

    元注解

    可以看到自定义注解里也会有注解存在,给自定义注解使用的注解就是元注解。

    @Rentention Rentention

    @Rentention Rentention用来标记自定义注解的有效范围,他的取值有以下三种:

    RetentionPolicy.SOURCE: 只在源代码中保留 一般都是用来增加代码的理解性或者帮助代码检查之类的,比如我们的Override;

    RetentionPolicy.CLASS: 默认的选择,能把注解保留到编译后的字节码class文件中,仅仅到字节码文件中,运行时是无法得到的;

    RetentionPolicy.RUNTIME: ,注解不仅 能保留到class字节码文件中,还能在运行通过反射获取到,这也是我们最常用的。

    @Target

    @Target指定Annotation用于修饰哪些程序元素。
    @Target也包含一个名为”value“的成员变量,该value成员变量类型为ElementType[ ],ElementType为枚举类型,值有如下几个:

    • ElementType.TYPE:能修饰类、接口或枚举类型
    • ElementType.FIELD:能修饰成员变量
    • ElementType.METHOD:能修饰方法
    • ElementType.PARAMETER:能修饰参数
    • ElementType.CONSTRUCTOR:能修饰构造器
    • ElementType.LOCAL_VARIABLE:能修饰局部变量
    • ElementType.ANNOTATION_TYPE:能修饰注解
    • ElementType.PACKAGE:能修饰包

    使用了@Documented的可以在javadoc中找到
    使用了@Interited表示注解里的内容可以被子类继承,比如父类中某个成员使用了上述@From(value),From中的value能给子类使用到。

    好了,关于注解就说这么多。

    反射&注解的使用

    属性值使用注解

    下面我们首先自定义两个注解:BindPort 和 BindAddress

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface BindPort {
        String value() default "8080";
    }
    

    指定BindPort 可以保留到运行时,并且可以修饰成员变量,包含一个成员变量默认值为”8080“。

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface BindAddress {
        String value() default "127.0.0.0";
    }
    

    这个和上面类似,只是默认值为"127.0.0.0"。

    同时,我们修改之前的TestClass

    public class TestClass {
        @BindAddress()
        String address;
        @BindPort()
        private String port;
    
        private int number;
    
        public void printInfo() {
            System.out.println("info is " + address + ":" + port);
        }
    
       ........
    
    
    }
    

    这里我们将原先的address 和 port 两个变量分别用这里定义的注解进行修饰,由于我们在定义注解时有默认值,所以这里的注解可以不写参数。

    使用反射获取注解信息

    前面已经说了,Annotation是被动的元数据,永远不会有主动行为,所以我们需要通过使用反射,才能让我们的注解产生意义。

    通过反射可以获取Class的所有属性和方法,因此获取注解信息也不在话下。我们看代码:

    //获取类
    Class c = Class.forName(className);
    //实例化一个TestClass对象
    TestClass tc= (TestClass) c.newInstance();
    
    // 获取所有的属性
    Field[] fields = c.getDeclaredFields();
    
    for (Field field : fields) {
       if(field.isAnnotationPresent(BindPort.class)){
             BindPort port = field.getAnnotation(BindPort.class);
             field.setAccessible(true);
             field.set(tc,port.value());
       }
    
       if (field.isAnnotationPresent(BindAddress.class)) {
             BindAddress address = field.getAnnotation(BindAddress.class);
             field.setAccessible(true);        
             field.set(tc,address.value());
       }
    
    }
    
    tc.printInfo();
    

    我们运行程序得到如下输出:


    output

    我们对tc 对象并没有做任何的set及初始化工作,输出结果却依然不再是null了,这就是反射与注解的功劳。

    上面代码的逻辑很简单:

    首先遍历循环所有的属性,如果当前属性被指定的注解所修饰,那么就将当前属性的值修改为注解中成员变量的值。

    上面的代码中,找到被BindPort修饰的属性,然后将BindPort中value的值赋给该属性。

    这里setAccessible(true)的使用时因为,我们在声明port变量时,其类型为private,为了确保可以访问这个变量,防止程序出现异常。

    理论上来说,这样做是不安全的,不符合面向对象的思想,这里只是为了说明注解和反射举例。

    但是,你也会发现,反射给我们提供了一种在运行时改变对象的方法。

    好了,下面我们继续修改TestClass

    public class TestClass {
        @BindAddress("http://www.google.com.cn")
        String address;
        @BindPort("8888")
        private String port;
    
        private int number;
    
        public void printInfo() {
            System.out.println("info is " + address + ":" + port);
        }
        .......
    }
    

    我们为注解设定了参数,再次运行,相信你已经猜到结果了。


    output1

    这时候由于我们在给成员变量设定注解时,写了参数,反射时也取到了相应的值。

    方法使用注解

    上面对于类属性(成员变量)设定注解,可能还不能让你感受到注解&反射的优势,我们再来看一下类的方法使用注解会怎样。

    我们还是先定义一个注解

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface BindGet {
        String value() default "";
    }
    

    有效范围至运行时,适用于方法。

    再次修改TestClass 如下:

    public class TestClass {
        @BindAddress("http://www.google.com.cn")
        String address;
        @BindPort("8888")
        private String port;
    
        private int number;
    
        @BindGet("mike")
        void getHttp(String param){
            String url="http://www.baidu.com/?username"+param;
            System.err.println("get------->"+url);
        }
    
        ...........
    }
    

    我们添加了一个名为getHttp的方法,而且这个方法由@BindGet注解。

    然后看反射的使用:

    //获取类
    Class c = Class.forName(className);
    TestClass tc= (TestClass) c.newInstance();
    
    // 获取所有的方法
    Method[] ms = c.getDeclaredMethods();
    
    for (Method method : ms) {
      if(method.isAnnotationPresent(BindGet.class)){
         BindGet bindGet = method.getAnnotation(BindGet.class);
         String param=bindGet.value();
         method.invoke(tc, param);
      }
    }
    

    这里的逻辑和对属性的解析相似,依旧是判断当前方法是否被指定的注解(BindGet)所修饰,
    如果是的话,就使用注解中的参数作为当前方法的参数去调用他自己。

    这样,我们在运行程序时,通过反射就回去主动调用getHttp方法,得到如下输出:


    output2

    这里我们就可以通过注解动态的实现username参数的修改,甚至getHttp方法整个http url地址的修改。
    (假设我们这里的getHttp方法是做网络请求)

    到这里,你应该已经明白了如何使用反射获取注解的信息,但你一定会困惑这么做有什么用呢?

    ”动态“”动态“”动态“

    这就是使用注解和反射最大的意义,我们可以动态的访问对象。

    说了这么多,下面我们看看,在Android开发中,我们遇到的注解和反射。

    Android 中的注解&反射

    Butterknife

    如果你是一个Android开发者,相信在使用Butterknife插件之前,你一定写了无数次的findViewById。

    然而,如果使用了Butterknife 插件,我们就可以很方便的完成findViewById的工作,甚至是setOnClickListener 的工作。

    public class ButtferknifeDemoActivity extends AppCompatActivity {
        @BindView(R.id.textView)
        TextView textView;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_buttferknife);
            ButterKnife.bind(this);
            textView.setText("I'm not null");
    
        }
    }
    

    上面的代码,应该不陌生。试想如果你的activity_bufferknife 布局文件中有很多控件时,这样做不知道可以省多少时间了

    我们看一下BindView的注解定义:

    @Retention(CLASS) @Target(FIELD)
    public @interface BindView {
      /** View ID to which the field will be bound. */
      @IdRes int value();
    }
    

    这个注解用于修饰变量,有效范围也是限定到了CLASS(即编译阶段),并没有到运行时。
    我们在Butterknife(8.4.0)的部分源码中可以看到:

    /** Simpler version of {@link View#findViewById(int)} which infers the target type. */
      @SuppressWarnings({ "unchecked", "UnusedDeclaration" }) // Checked by runtime cast. Public API.
      @CheckResult
      public static <T extends View> T findById(@NonNull View view, @IdRes int id) {
        return (T) view.findViewById(id);
      }
    

    我们可以猜到的,编译时最终的实现必然是到这里,实现view.findViewById(id)。

  • 相关阅读:
    计算机硬件基础
    元类
    内置函数
    单例模式的三种实现方式
    字符编码
    odoo权限
    odoo api介绍
    odoo 二次开发小记不定时更新
    js与jQuery区别
    Cookie, LocalStorage 与 SessionStorage说明
  • 原文地址:https://www.cnblogs.com/foxy/p/7879460.html
Copyright © 2011-2022 走看看