Loading...

Java核心(二)——this/反射/注解

  iwehdio的博客园:https://www.cnblogs.com/iwehdio/

学习自:

相关:

1、对象与this

  • 对象的本质理解为“多个相关数据的统一载体”。

  • 在Java中对象是在堆空间中生成的,数据会在堆空间占据一定内存开销。而方法只有一份。

  • 多个个体,属性可能不同,但技能(method)都是共通的,没必要和属性数据一样单独在堆空间各存一份,所以被抽取出来存放。

  • 时,方法相当于一套指令模板,谁都可以传入数据交给它执行,然后得到执行后的结果返回。

  • 但是,方法如何知道是谁调用了它?

    • 换句话说:共性的方法如何处理特定的数据?
    • Java的this其实就是解决这个问题的。
    • 可以理解为对象内部持有一个引用,当你调用某个方法时,必须传递这个对象引用,然后方法根据这个引用就知道当前这套指令是对哪个对象的数据进行操作了。

    image-20210116090927889

  • static修饰的属性或方法其实都是属于类的,是所有对象共享的。之所以一个变量或者方法要声明为static,是因为

    • static变量:大家共有的,大家都一样,不是特定的差异化数据。
    • static方法:这个方法不处理差异化数据。
    • 即,static注定与差异化数据无关,即与具体对象的数据无关。
  • 静态方法的其实就是调用时不会传入this:

    • 静态方法无法访问非静态数据(实例字段)和非静态方法(实例方法)。
    • 因为Java不会在调用静态方法时传递this,静态方法内没有this当然无法处理实例相关的一切。

2、反射

  • JVM是如何构建一个实例的:

    image-20210116091953245

  • 类加载器:

    • ClassLoader类负责加载类,它的核心方法是loadClass(),传入需要加载的类名,它会帮你加载:

      protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
          synchronized (getClassLoadingLock(name)) {
              // 首先,检查是否已经加载该类
              Class<?> c = findLoadedClass(name);
              if (c == null) {
                  long t0 = System.nanoTime();
                  try {
                      // 如果尚未加载,则遵循父优先的等级加载机制(所谓双亲委派机制)
                      if (parent != null) {
                          c = parent.loadClass(name, false);
                      } else {
                          c = findBootstrapClassOrNull(name);
                      }
                  } catch (ClassNotFoundException e) {
                      // ClassNotFoundException thrown if class not found
                      // from the non-null parent class loader
                  }
      
                  if (c == null) {
                      // 模板方法模式:如果还是没有加载成功,调用findClass()
                      long t1 = System.nanoTime();
                      c = findClass(name);
      
                      // this is the defining class loader; record the stats
                      sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                      sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                      sun.misc.PerfCounter.getFindClasses().increment();
                  }
              }
              if (resolve) {
                  resolveClass(c);
              }
              return c;
          }
      }
      
      // 子类应该重写该方法
      protected Class<?> findClass(String name) throws ClassNotFoundException {
          throw new ClassNotFoundException(name);
      }
      
    • 加载.class文件大致可以分为3个步骤:

      1. 检查是否已经加载,有就直接返回,避免重复加载
      2. 当前缓存中确实没有该类,那么遵循父优先加载机制(双亲委派机制),加载.class文件
      3. 上面两步都失败了,调用findClass()方法加载
    • ClassLoader类本身是抽象类,而抽象类是无法通过new创建对象的,所以它并没有实现最核心的findClass()方法,直接抛了异常。此处又是应用了模板方法模式,具体的findClass()方法丢给子类实现。

    • 正确的做法是,子类重写覆盖findClass(),在里面写自定义的加载逻辑:

      @Override
      public Class<?> findClass(String name) throws ClassNotFoundException {
      	try {
      		/*自己另外写一个getClassData()
                        通过IO流从指定位置读取xxx.class文件得到字节数组*/
      		byte[] datas = getClassData(name);
      		if(datas == null) {
      			throw new ClassNotFoundException("类没有找到:" + name);
      		}
      		//调用类加载器本身的defineClass()方法,由字节码得到Class对象
      		return defineClass(name, datas, 0, datas.length);
      	} catch (IOException e) {
      		e.printStackTrace();
      		throw new ClassNotFoundException("类找不到:" + name);
      	}
      }
      
    • defineClass()是父类ClassLoader里定义的方法,目的是根据.class文件的字节数组byte[] b造出一个对应的Class对象。我们无法得知具体是如何实现的,因为最终它会调用一个native方法:

      image-20210116092935736

    • 总的流程:

      image-20210116093120112

  • Class对象:

    • 字段、方法、构造器对象:

      image-20210116093609596

    • 注解和泛型:

      image-20210116093635959

    • 而且,针对字段、方法、构造器,因为这三个部分信息量太大了,JDK单独写了三个类:Field、Method、Constructor。以Method类为例:

      image-20210116093745347

      image-20210116093816544

  • Class类的方法:

    • 构造器:声明为final无法被继承,构造器私有无法手动new,只能由JVM创建。JVM在构造Class对象时,需要传入一个类加载器,然后才有我们上面分析的一连串加载、创建过程。

      image-20210116093917924

    • Class.forName()方法:本质还是通过类加载器,返回Class对象。

      image-20210116094052882

    • newInstance()方法:底层就是调用无参构造对象的newInstance()。

      image-20210116094626698

    • 所以,本质上Class对象要想创建实例,其实都是通过构造器对象。如果没有空参构造对象,就无法使用clazz.newInstance(),必须要获取其他有参的构造对象然后调用对应的Constructor#newInstance()。

  • 反射API:

    • 开发中反射最终目的主要两个:

      • 创建实例
      • 反射调用方法
    • clazz.newInstance()底层还是调用Contructor对象的newInstance(),所以,要想调用clazz.newInstance(),必须保证编写类的时候有个无参构造,否则就要先获取对应的Constructor。

    • Class、Field、Method、Constructor四个对象的关系:

      image-20210116095209792

    • 为什么根据Class对象获取Method时,需要传入方法名+参数的Class类型?

      image-20210116095311183

      • 因为.class文件中通常有不止一个方法,比如:

      image-20210116095354895

      • 所以必须传入name,以明确本次需要调用的方法,得到该方法对应的Method对象。
    • 参数parameterTypes为什么要用Class类型,像调用普通方法那样直接传变量名不行吗?

      • JVM判定方法重载的依据是参数列表,包括参数类型及参数个数,但不包括变量名。仅仅是变量名不同不叫重载,本质是同一个方法。
    • 调用Class对象的getMethod()方法时,内部会循环遍历所有Method,然后根据方法名methodName和参数类型parameterType匹配唯一的Method返回:

      image-20210116095852735

    • 调用method.invoke(obj, args)时为什么要传入一个目标对象?

      • 对象和this中说到,Java中每次对象调用方法时,都会隐性传递当前调用该方法的对象参数this,方法可以根据this知道当前调用本方法的是哪个对象。
      • 同样的,在反射调用方法时,本质还是希望方法处理数据,所以必须告诉它去处理哪个对象的数据。

      image-20210116100207039

      • 所以,反射调用方法(绿线)时,也需要告诉是去处理那个对象的数据。
      • 最好把Method理解为方法执行指令吧,它更像是一个方法执行器,必须告诉它要执行的对象(数据)。
      • 当然,如果是invoke一个静态方法,不需要传入具体的对象,因为静态方法是属于类的。
  • 反射的原理和使用:Java SE入门(二十一)——反射和注解

  • 反射获取父类的泛型:

    • 通过子类B的Class对象获得父类A的Class泛型对象。

    • 然后向下强转为参数化类型实现类ParameterizedTypeImpl。

    • 使用指定的方法获取泛型数组。

      public class JpaTest {
          public static void main(String[] args) {
              new B();
          }
      }
      
      class A<T> {
          public A() {
              Class<? extends A> subClass = this.getClass();
              // 得到泛型父类
              Type genericSuperclass = subClass.getGenericSuperclass();
      
              // 本质是ParameterizedTypeImpl,可以向下强转
              ParameterizedType parameterizedTypeSuperclass = (ParameterizedType) genericSuperclass;
      
              // 强转后可用的方法变多了,比如getActualTypeArguments()可以获取Class A<String>的泛型的实际类型参数
              Type[] actualTypeArguments = parameterizedTypeSuperclass.getActualTypeArguments();
      
              // 由于A类只有一个泛型,这里可以直接通过actualTypeArguments[0]得到子类传递的实际类型参数
              Class actualTypeArgument = (Class) actualTypeArguments[0];
              System.out.println(actualTypeArgument);
              System.out.println(subClass.getName());
          }
      }
      
      class B extends A<String> {
      }
      
      class C extends A<Integer> {
      }
      

3、注解

  • 注解的作用:像一个标签,贴在一个类、一个方法或者字段上。它的目的是为当前读取该注解的程序提供判断依据及少量附加信息。

  • 只要用到注解,必然有三角关系:

    • 定义注解
    • 使用注解
    • 读取注解

    image-20210116101856418

  • 注解的保留策略有三种:SOURCE/ClASS/RUNTIME:

    image-20210116102223994

    image-20210116102230570

    • 一般来说,普通开发者使用注解的时机都是运行时,比如反射读取注解(也有类似Lombok这类编译期注解)。
    • 既然反射是运行时调用,那就要求注解的信息必须保留到虚拟机将.class文件加载到内存为止。
  • 反射读取注解:

    • 自定义注解:需要注意的是,注解的指定的数据类型,需要与对应的方法的返回值类型一致。

      @Retention(RetentionPolicy.RUNTIME)
      public @interface MyAnno {
          String getAttr() default "no description";
      }
      
    • 使用注解:

      @MyAnno(getAttr = "annotation on class")
      public class testAnno {
      
          @MyAnno(getAttr = "annotation on field")
          public String name;
      
          @MyAnno(getAttr = "annotation on method")
          public void hello() {}
      
          @MyAnno() // 故意不指定
          public void defaultMethod() {}
      }
      
    • 读取注解:

      public class Test2 {
          public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
              // 获取类上的注解
              Class<TestAnno> clazz = TestAnno.class;
              MyAnno annotationOnClass = clazz.getAnnotation(MyAnno.class);
              System.out.println(annotationOnClass.getAttr());
      
              // 获取成员变量上的注解
              Field name = clazz.getField("name");
              MyAnno annotationOnField = name.getAnnotation(MyAnno.class);
              System.out.println(annotationOnField.getAttr());
      
              // 获取hello方法上的注解
              Method hello = clazz.getMethod("hello", (Class<?>[]) null);
              MyAnno annotationOnMethod = hello.getAnnotation(MyAnno.class);
              System.out.println(annotationOnMethod.getAttr());
      
              // 获取defaultMethod方法上的注解
              Method defaultMethod = clazz.getMethod("defaultMethod", (Class<?>[]) null);
              MyAnno annotationOnDefaultMethod = defaultMethod.getAnnotation(MyAnno.class);
              System.out.println(annotationOnDefaultMethod.getAttr());
      
          }
      }
      
  • 注解的读取并不只有反射一种途径。比如@Override,它由编译器读取(写完代码ctrl+s时就编译了),而编译器只是检查语法错误,此时程序尚未运行。

    image-20210116103451948

  • 保留策略为SOURCE,仅仅是源码阶段,编译成.class文件后就消失:

    image-20210116103556540

  • 注解属性的数据类型:

    • 八种基本数据类型
    • String
    • 枚举
    • Class
    • 注解类型
    • 以上类型的一维数组
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotation {
       // 8种基本数据类型
        int intValue();
        long longValue();
        // ...其他类型省略
    
        // String
        String name();
        // 枚举
        CityEnum cityName();
        // Class类型
        Class<?> clazz();
        // 注解类型
        MyAnnotation2 annotation2();
    
        // 以上几种类型的数组类型
        int[] intValueArray();
        String[] names();
        // ...其他类型省略
    }
    
    @interface MyAnnotation2 {
    }
    
    enum CityEnum {
        BEIJING,
        HANGZHOU,
        SHANGHAI;
    }
    
  • 如果注解的属性只有一个,且叫value,那么使用该注解时,可以不用指定属性名,因为默认就是给value赋值。

  • 但是注解的属性如果有多个,无论是否叫value,都必须写明属性的对应关系。

  • 对于数组属性,如果数组的元素只有一个,可以省略花括号{}。

  • 如果你希望为注解的属性提供统一的几个可选值,可以使用常量类:

    image-20210116104140570


iwehdio的博客园:https://www.cnblogs.com/iwehdio/
posted @ 2021-01-16 11:14  iwehdio  阅读(254)  评论(0编辑  收藏  举报