第7章 枚举、注解、泛型的底层原理

枚举、注解、泛型的底层原理

枚举的底层原理

枚举类基础

枚举类enum可以看成是一个普通的class,它们都可以定义一些属性和方法,不同之处是,enum不能使用extends关键字继承其他类,因为enum已经继承了java.lang.Enum(java是单一继承),这个通过反编译枚举类会发现。枚举类型隐性为final类型除非该枚举类中至少含有一个包含类体的枚举常量,此时反编译后的枚举类为abstract类。所以,枚举类是由编译器决定是否是abstract或final,而不能再用abstract和final修饰,否则会报编译错误。

枚举类中所有方法和成员变量全部为static类型,在static静态块中进行初始化值。因为继承Enum类,所以每个方法都继承了该类的values和valueOf方法。枚举类能嵌套使用,默认是static的(编译器却允许你重新在该枚举类中再用static修饰)。枚举类是只有一个实例的,且通过三种机制确保枚举类型只有一个实例存在。

  1. Enum类中的final Object clone()方法确保enum常量永远也不能被克隆。

  2. 枚举类型的反射实例被禁止。这是因为在该枚举类在使用反射实例化时,会调用Constructor#newInstance()方法。里边会判断如果是枚举类,则会抛出异常。

    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
               throw new IllegalArgumentException("Cannot reflectively create enum objects");
  1. 特殊对待序列化机制,确保不能通过反序列化创建多个实例。通过以上三种机制保证一个枚举常量只是一个实例,即是单例的,所以在effective java中推荐使用枚举来实现单例。
   private void readObject(ObjectInputStream in) throws IOException,
           ClassNotFoundException {
           throw new InvalidObjectException("can't deserialize enum");
   }
枚举常量

枚举常量上也可以用注解。也可以有参数,有参数时,在枚举类初始化时,跟随常量的创建被初始化。枚举常量也可以有类体,此时包含了一个匿名内部类。此时匿名内部类中不能包含任何构造方法;因为abstract不能和static连用,所以也不能定义abstract方法。

因为每个枚举常量只有一个实例,当对比两个对象引用时可以用“=”替代equals

枚举体定义

除了枚举常量,也可以包含构造函数,成员定义,静态初始化等。如果构造函数为public或protected,则会出现编译错误,因为默认是private的。

public enum Operation {
    PLUS("+") {//包含类体的枚举类型

        double eval(double x, double y) {
            return x + y;
        }
    },

    MINUS("-") {
        double eval(double x, double y) {
            return x - y;
        }
    },

    TIMES("*") {
        double eval(double x, double y) {
            return x * y;
        }
    },

    DIVIDED_BY("/") {
        double eval(double x, double y) {
            return x / y;
        }
    };

    Operation(String o) {
        this.operator = o;
    }

    private String operator;

    public String getOperator() {
        return operator;
    }

    //抽象方法,每个常量支持一种数学运算
    abstract double eval(double x, double y);


    public static void main(String args[]) {
        double x = 2.0d;
        double y = 4.0d;
        for (Operation op : Operation.values()) {
            System.out.println(x + " " + op + " " + y + " = " + op.eval(x, y));
        }
        Operation op = Operation.DIVIDED_BY;
        System.out.println(x + " " + op + " " + y + " = " + op.eval(x, y));
    }

}

对以上代码反编译分析,从Enum类中我们可以看到,为每个枚举都定义了两个属性,name和ordinal,name表示我们定义的枚举常量的名称,如ADD、SUBTRACT等,而ordinal是一个顺序号,根据定义的顺序分别赋予一个整型值,从0开始。在枚举常量初始化时,会自动为初始化这两个字段,设置相应的值,所以才在构造方法中添加了两个参数。

compareTo(E o)方法则是比较枚举的大小,注意其内部实现是根据每个枚举的ordinal值大小进行比较的。name()方法与toString()几乎是等同的,都是输出变量的字符串形式。valueOf(Class enumType, String name)方法则是根据枚举类的Class对象和枚举名称获取枚举常量。

Enum与单例

在实际生产中,线程池、缓存、日志对象、对话框对象经常被设置为单例模式。单例模式有多种实现方式。常见有饿汉式、懒汉式、懒汉式+同步方法、Dubbo-Check(双重锁)、volatile+Dubbo-Check、Holder方式、枚举方式。

其中,饿汉式虽然能保证在多线程环境下只有唯一个实例,但是如果该实例属性很多,将导致作为instance的类实例在内存中驻留很长时间,出现大对象长时间驻留内存。并且饿汉式也无法进行懒加载。

懒汉式不是类在初始化时创建类实例,而是在使用时创建,但并不能保证单例的唯一性。所以出现了懒汉式+同步方法,但是同步方法在同一时刻只能被一个类访问,性能相对低下(但是经常就这么用)。

而Dubbo-Check看似完美地解决了高效访问,也能懒加载,并且保证实例的唯一性,但是可能会抛出空指针错误。另外一种就是volatile+Dubbo-Check,代码如下。在getSingleton()方法中,进行两次null检查。当线程A进入同步代码块时,其他线程则阻塞在同步块外部。线程A执行完后,其他个别线程还会进入到同步代码块,但是这时第二次进行null检查时发现singleton已经不为null,singleton为volatile类型可理解为它的赋值操作对其他线程是可见的,此时什么也没做就退出了同步块,接下来的所有线程则直接获取singleton即可。提高了执行效率。必须注意的是volatile关键字,该关键字有两层语义。第一层语义是可见性,可见性是指在一个线程中对该变量的修改会马上由线程工作内存写回主内存,所以其它线程会马上读取到已修改的值。volatile的第二层语义是禁止指令重排序优化,由于编译器对代码的优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同,这种乱序在多线程环境中就可能导致严重问题。volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在Java 1.5后才得以实现,因此1.5前的版本的双重锁实现的单例仍然是不安全的,即使使用了volatile关键字。

public class DoubleCheckSingleton {
    private static volatile DoubleCheckSingleton singleton = null;
    private DoubleCheckSingleton(){};

    public static DoubleCheckSingleton getSingleton() {
        if(singleton == null) {
            synchronized (DoubleCheckSingleton.class) {
                if(singleton == null) {
                    singleton = new DoubleCheckSingleton();
                }
            }
        }
        return singleton;
    }
}

Holder方式完美地借用了类加载的特点,将实例放入到静态内部类中,只有当主动引用时候会创建SingletonHolder实例。SingletonHolder实例的创建时在该类编译时收集到了()方法中,该方法是虚拟机保证了同步性,所以可以保证内存的可见性、JVM指令的顺序性和原子性。
JDK8之前,方法区由永久代实现,主要存放类的信息、常量池、方法数据、方法代码等;JDK8之后,取消了永久代,提出了元空间,并且常量池、静态成员变量等迁移到了堆中;元空间不在虚拟机内存中,而是放在本地内存中。静态方法,静态成员变量都保存在方法区。静态内部类需要实例化,保存在堆区。

//不允许被继承
public final class SingletonHolder {
    //实例变量
    private byte[] data = new byte[1024];
    private SingletonHolder(){}

    /**
     * 在静态内部类中持有Singleton的实例,并且可被直接初始化
     */
    private static class Holder {
        private static SingletonHolder instance = new SingletonHolder();
    }

    /**
     * 调用getInstance方法,事实上是获得Holder的instance静态属性
     * @return
     */
    public static SingletonHolder getInstance(){
        return Holder.instance;
    }

}

还有一种方式就是使用枚举类创建单例模式。这种方式也是《Effective Java》力推的一种方式。枚举类不能够被继承,同样由编译器保证线程安全和实例化一次。同样也可以是懒加载。

public class Singleton {
    //实例变量
    private Object object = new Object();
    private Singleton(){}
    private enum EnumInstance{
        INSTANCE;
        private Singleton instance;
        EnumInstance() {
            this.instance = new Singleton();
        }
        private Singleton getSingleton(){
            return instance;
        }

    }

    public static Singleton getInstance() {
        return EnumInstance.INSTANCE.getSingleton();
    }
}

注解底层原理

注解同XML一样,能对既有代码做一些配置或前置校验。在Spring项目中,很多地方都是通过注解这种耦合的方式来完成功能的。JDK1.5中内置了3种标准注解和4种元注解,3种标准注解分别是@Override,@Deprecated,@SuppressWarnings。4种元注解是专门负责新注解的创建的,分别是@Target,@Retention,@Documented,@Inherited。

三种标准注解是Java前端编译器在编译时介入的。它们的作用分别如下。

  1. Override,这个我们经常用于子类覆盖父类的方法。当编译器检测到某个方法使用了该注解,就会检查父类中存在某个方法的签名与该方法签名相同。
  2. Deprecated,这个注解我们经常用于RPC接口中,如果要标识某个接口过时且不推荐使用,就可以使用该注解。
  3. SuppressWarning,关闭不当的编译器警告信息。它有一个value属性,用来表示被压制的警告类型,比如压制一个过时警告可以用@SuppressWarning(value=“deprecated”)。

4种元注解通常用在注解的定义上。一般用于指定某个注解生命周期以及作用目标等信息。

  1. Target,用于指定该注解作用的目标是packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数)。作用目标用枚举ElementType表示。
  2. Retention,用于指明当前注解的生命周期。生命周期枚举类用RetentionPolicy表示。RetentionPolicy.SOURCE只能在源文件保留,编译期可见,编译后被丢弃。RetentionPolicy.CLASS则会被编译器编译进class文件中,如类、方法或字段的属性表。RetentionPolicy.RUNTIME则永久存在,包括在运行时可以通过反射获取。
  3. Documented,当我们执行JavaDoc打包时,将会保存进doc文档。
  4. Inherited,表示注解具有可继承性。如,我们用注解修饰了一个类,而该类的子类将自动继承父类的该注解。

注解的本质是一个继承了Annotation接口的接口。下面举个例子说明注解的底层实现。创建注解时使用了3种元注解。

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Test {
    int id() default 0;
}

通过反编译后字节码如下。从字节码中可以发现Test接口继承Annotation类。并且注解方法id()转化为一个抽象方法。最后在执行时为注解对应的接口生成一个实现该接口的动态代理类。直接点说就是:Java通过动态代理的方式生成了一个实现了"注解对应接口"的实例,该代理类实例实现了"注解成员属性对应的方法",这个步骤类似于"注解成员属性"的赋值过程,这样就可以在程序运行的时候通过反射获取到注解的成员属性(这里注解必须是运行时可见的,也就是使用了@Retention(RetentionPolicy.RUNTIME),另外需要理解JDK原生动态代理和反射相关内容)。

@Retention注解指定了被修饰的注解的生命周期,有三种类型。RetentionPolicy.SOURCE是只能在编译期可见,编译后会被丢弃,RetentionPolicy.CLASS会被编译器编译进 class文件中,无论是类或是方法,乃至字段,他们都是有属性表的,而JAVA虚拟机也定义了几种注解属性表用于存储注解信息,但是这种可见性不能带到方法区,类加载时会予以丢弃,最后RetentionPolicy.RUNTIME则是永久存在的可见性。

public interface Test extends java.lang.annotation.Annotation
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
   #1 = Class              #20            // Test
   #2 = Class              #21            // java/lang/Object
   #3 = Class              #22            // java/lang/annotation/Annotation
   #4 = Utf8               id
   #5 = Utf8               ()I
   #6 = Utf8               AnnotationDefault
   #7 = Integer            0
   #8 = Utf8               SourceFile
   #9 = Utf8               Test.java
  #10 = Utf8               RuntimeVisibleAnnotations
  #11 = Utf8               Ljava/lang/annotation/Retention;
  #12 = Utf8               value
  #13 = Utf8               Ljava/lang/annotation/RetentionPolicy;
  #14 = Utf8               RUNTIME
  #15 = Utf8               Ljava/lang/annotation/Documented;
  #16 = Utf8               Ljava/lang/annotation/Target;
  #17 = Utf8               Ljava/lang/annotation/ElementType;
  #18 = Utf8               METHOD
  #19 = Utf8               TYPE
  #20 = Utf8               Test
  #21 = Utf8               java/lang/Object
  #22 = Utf8               java/lang/annotation/Annotation
{
  public abstract int id();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_ABSTRACT
    AnnotationDefault:
      default_value: I#7}
SourceFile: "Test.java"
RuntimeVisibleAnnotations:
  0: #11(#12=e#13.#14)
  1: #15()
  2: #16(#12=[e#17.#18,e#17.#19])

动态代理实现

注解与反射

我们知道在编译时的注解处理阶段,虚拟机规范定义了一系列和注解相关的属性表,也就是说,无论是字段、方法或是类本身,如果被注解修饰了,该阶段会轮询该类的所有注解,将注解要实现的功能写进字节码文件,是否要写入字节码文件,是由属性表决定,属性表有如下值。

RuntimeVisibleAnnotations:运行时可见的注解
RuntimeInVisibleAnnotations:运行时不可见的注解
RuntimeVisibleParameterAnnotations:运行时可见的方法参数注解
RuntimeInVisibleParameterAnnotations:运行时不可见的方法参数注解
AnnotationDefault:注解类元素的默认值

另外一个前提知识点是Class类,我们知道Class类中有针对注解的处理方法。如getAnnotation用于返回指定的注解、isAnnotationPresent用于判断当前元素是否被注解修饰等。

我们用上面的Test注解类进行测试,首先设置一个虚拟机启动参数,用于捕获 JDK 动态代理类。

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
测试注解

测试代码如下。

@Test( id = 1)
public class TestAnnotation {
    public static void main(String [] args) {
        Test test = TestAnnotation.class.getAnnotation(Test.class);
        System.out.println(test.id());
    }
}

系统为我们生成的动态代理类。代理类实现接口Test并重写其所有方法,包括value方法以及接口Test从Annotation接口继承而来的方法。

public final class $Proxy1 extends Proxy implements Test {
    private static Method m1;
    private static Method m2;
    private static Method m4;
    private static Method m3;
    private static Method m0;

    public $Proxy1(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final Class annotationType() throws  {
        try {
            return (Class)super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    // 通过反射的方式进行处理注解生成的代理类及代理方法
    public final int id() throws  {
        try {
            return (Integer)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m4 = Class.forName("Test").getMethod("annotationType");
            m3 = Class.forName("Test").getMethod("id");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

这几个重写方法都包含了super.h.invoke,即调用的是Proxy中接口类InvocationHandler的invoke方法。而接口的实现类是AnnotationInvocationHandler。

先看下该实现类的构造方法,获取注解类的接口,如果是注解类,则用memberValues保存注解属性名称,它是一个 Map 键值对,键是我们注解属性名称,值就是该属性当初被赋上的值。

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }

下面分析下invoke方法。如果当前调用的方法是toString,equals,hashCode,annotationType的话,AnnotationInvocationHandler实例中已经预定义好了这些方法的实现,直接调用即可。假如var7没有匹配上Annotation中四种方法,说明当前的方法调用的是自定义注解字节声明的方法,例如我们Test注解的id方法。这种情况下,将从我们的注解map中获取这个注解属性对应的值。

public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();//获取方法名称
        Class[] var5 = var2.getParameterTypes();//获取方法参数
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);//如果是Object的equals方法。
        } else if (var5.length != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        } else {
            byte var7 = -1;
            switch(var4.hashCode()) {
            case -1776922004:
                if (var4.equals("toString")) {
                    var7 = 0;//如果是toString方法
                }
                break;
            case 147696667:
                if (var4.equals("hashCode")) {
                    var7 = 1;//如果是hashCode方法
                }
                break;
            case 1444986633:
                if (var4.equals("annotationType")) {
                    var7 = 2;//如果是注解类型方法
                }
            }

            switch(var7) {
            case 0:
                return this.toStringImpl();
            case 1:
                return this.hashCodeImpl();
            case 2:
                return this.type;//返回注解类型
            default:
                Object var6 = this.memberValues.get(var4);//获取注解方法
                if (var6 == null) {
                    throw new IncompleteAnnotationException(this.type, var4);
                } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {//如果是数组类,需要复制数组
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }
                    return var6;
                }
            }
        }
    }

最后我们再总结一下整个反射注解的工作原理:

  1. 我们通过键值对的形式可以为注解属性赋值,像这样:@Test(id= “1”)。
  2. 你用注解修饰某个元素,编译器将在编译期扫描每个类或者方法上的注解,会做一个基本的检查,你的这个注解是否允许作用在当前位置,最后会将注解信息写入元素的属性表。
  3. 当你进行反射的时候,虚拟机将所有生命周期在 RUNTIME 的注解取出来放到一个 map 中,并创建一个 AnnotationInvocationHandler 实例,把这个 map 传递给它。
  4. 虚拟机将采用 JDK 动态代理机制生成一个目标注解的代理类,并初始化好处理器。
    那么这样,一个注解的实例就创建出来了,它本质上就是一个代理类,你应当去理解好 AnnotationInvocationHandler 中 invoke 方法的实现逻辑,这是核心。一句话概括就是,通过方法名返回注解属性值,比如这里的id的值为1。
反射与代理

前面我们有讲解过反射,我们接下来讲解下代理。这是反射最常见的用途。
代理分两类,一类是静态代理,另一类是动态代理,而动态代理有Jdk和Cglib两种实现方式。

静态代理

静态代理会导致要创建不同的代理对象。如下,如果是狗,就需要新建狗代理,否则需要新建猫代理。

if(isDog) {
    Dog dog = new Dog();
    DogProxy dogProxy = new DogProxy(dog);
    dogProxy.action();
} else {
    Cat cat = new Cat();
    Catproxy  catProxy = new CatProxy(cat);
    catProxy.action();
}
动态代理

AOP的拦截功能是由java中的动态代理来实现的,所以我们这里以AOP拦截分析动态代理。AOP的源码中用到了两种动态代理来实现拦截切入功能:jdk动态代理和cglib动态代理。两种方法同时存在,各有优劣。jdk动态代理是由java内部的反射机制来实现的,如上面分析的反射;cglib动态代理底层则是借助asm来实现的。总的来说,反射机制在生成类的过程中比较高效,而asm在生成类之后的相关执行过程中比较高效(可以通过将asm生成的类进行缓存,这样解决asm生成类过程低效问题)。**还有一点必须注意:jdk动态代理的应用前提,必须是目标类基于统一的接口。**如果没有上述前提,jdk动态代理不能应用。由此可以看出,jdk动态代理有一定的局限性,cglib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。

Jdk动态代理的实现

jdk动态代理是jdk通过反射就能支持的一种代理方式,它的实现具体步骤:

  1. 通过实现 InvocationHandler 接口创建自己的调用处理器;
public static interface Hello {
    void hi(String msg);
}
public static class HelloImpl implements Hello {
    @Override
    public void hi(String msg) {
        System.out.println("hello " + msg);
    }
}
/**
 * 代理类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class HelloProxy implements InvocationHandler {
    private Object proxied = null;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("hello proxy");
        return method.invoke(proxied, args);
    }
}
  1. 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
 //创建一个代理对象stuProxy来代理zhangsan,代理对象的每个执行方法都会替换执行Invocation中的invoke方法
public static void main(String[] args) {
    Hello hello = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(), new Class[]{Hello.class}, new HelloProxy(new HelloImpl()));
    hello.hi("world");
}
  1. 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
  2. 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
Cglib方法的动态代理

Cglib与jdk对动态代理的支持不同主要表现在,Jdk动态代理的拦截对象是通过反射的机制来调用被拦截方法的,反射的效率比较低,所以cglib采用了FastClass的机制来实现对被拦截方法的调用。FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法。

  1. 创建目标类
public class Target{
    public void f(){
        System.out.println("Target f()");
    }
    public void g(){
        System.out.println("Target g()");
    }
}
  1. 方法拦截器
public class MyMethodInterceptor implements MethodInterceptor{
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("这里是对目标类进行增强!!!");
        //注意这里的方法调用,不是用反射哦!!!
        Object object = proxy.invokeSuper(obj, args);
        return object;
    }  
}
  1. 测试类

我们在测试类中打印出了反编译的代码。 代理类(Target E n h a n c e r B y C G L I B EnhancerByCGLIB EnhancerByCGLIB788444a0)继承了目标类(Target),至于代理类实现的factory接口与本文无关,残忍无视。例如针对目标类中的每个非private方法,代理类会生成两个方法,以g方法为例:一个是@Override的g方法,一个是CGLIB$g 0 ( C G L I B 0(CGLIB 0CGLIBg$0相当于目标类的g方法)。我们在示例代码中调用目标类的方法t.g()时,实际上调用的是代理类中的g()方法。

当调用invokeSuper方法时,实际上是调用代理类的CGLIB$g 0 方 法 , C G L I B 0方法,CGLIB 0CGLIBg$0直接调用了目标类的g方法。

public class Test {
    public static void main(String[] args) {
    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "F:\\code");
         //实例化一个增强器,也就是cglib中的一个class generator
        Enhancer eh = new Enhancer();
         //设置目标类
        eh.setSuperclass(Target.class);
        // 设置拦截对象
        eh.setCallback(new Interceptor());
        // 生成代理类并返回一个实例
        Target t = (Target) eh.create();
        t.f();
        t.g();
    }
}
对比

经测试,jdk创建对象的速度远大于cglib,这是由于cglib创建对象时需要操作字节码。cglib执行速度略大于jdk,所以比较适合单例模式。另外由于CGLIB的大部分类是直接对Java字节码进行操作,这样生成的类会在Java的永久堆中。如果动态代理操作过多,容易造成永久堆满,触发OutOfMemory异常。spring默认使用jdk动态代理,如果类没有接口,则使用cglib。 cglib动态代理无需实现接口,通过生成子类字节码来实现,比反射快一点,没有性能问题。但是由于cglib会继承被代理类,需要重写被代理方法,所以被代理类不能是final类,被代理方法不能是final。

泛型语法糖与编译

我们知道Sun HotSpot中编译分为Javac前端编译和JIT后端编译。其中前端编译分为解析和填充符号表、注解处理和语义分析和字节码生成三个部分。其中在语义分析中,又包括标注检查、数据和控制流分析和解语法糖。下面通过源码分析Java中怎么解语法糖的过程。

解语法糖源码分析

为了能提高代码编程除出错的机率,提高编码效率和语法的严整性,引入了语法糖。但语法糖让程序员看不到语法糖后面的真实世界。在Javac的源码中(Javac、Javah等工具是由Java语言编写的),解语法糖主要由方法desugar()触发,由TransTypes和Lower两个类来完成解语法糖的功能。

Java中语法糖举例

泛型和类型擦除

先举个例子,从该例子中可以看到。在编译期间,HashMap<String,String> map被替换了HashMap这种原生类型。并且在相应的地方加入强制类型转化。如Java中的HashMap中get()方法返回的Object对象被强制转化为String类型。这样在编译期间将类型转化,就在运行期间减少了ClassCastException异常。保证了代码的健壮性。

public static void main(String [] args) {
        HashMap<String,String> map = new HashMap<>();
        map.put("A","1");
        System.out.println(map.get("A"));
}
//编译后反编译的结果
public static void main(String[] var0) {
        HashMap var1 = new HashMap();//HashMap被替换为原生的类型
        var1.put("A", "1");
        System.out.println((String)var1.get("A"));//编译期间转化为String
}

泛型擦除,将会导致List与List的签名变得一样。将导致在同一个方法内不能重载。但是不同的返回值,将导致方法能重载,因为Java层面的方法特征签名只包含方法名称、参数顺序及参数类型。方法返回值不是参与方法特征签名的元素。所以,两个方法如果有相同的特征签名,但具有不同的返回值,也能共存一个class文件中。

由于Java伪泛型的引入,将导致虚拟机解析、反射下的方法调用都可能对原有的方法产生影响。所以,JCP组织又引入了Signature、LocalVariableTypeTable等新的属性用于解决泛型中的参数识别问题。如Signature就存储了一个方法字节码层面的特征签名,而字节码层面的特征签名是在Java层面的特征签名的基础上加入了方法返回值和异常表。并且从Signature属性我们可以发现,所谓的擦除,仅仅对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段能取得参数化类型的依据。

23: invokevirtual #8                  // Method java/util/HashMap.get:(Ljava/lang/Object;)Ljava/lang/Object;
26: checkcast     #9                  // class java/lang/String

自动装箱、拆箱

先来个实例,该实例中设置到自动装箱和拆箱。读者可以先运行下该实例看看结果。笔者建议初学者不要这样使用来避免装箱拆箱。

    public static void main(String [] args) {
        Integer a = 2;
        Integer b = 125;
        Integer c = 127;
        Integer d = 127;
        Integer e = 128;
        Integer f = 128;
        Long g = 127L;
        System.out.println(c == d);
        System.out.println(e == f);
        System.out.println(c == (a+b));
        System.out.println(c.equals(a + b));
        System.out.println(g == (a+b));
        System.out.println(g.equals(a+b));
}

其中调用拆箱时是调用了Integer.valueOf()方法。我们看下该方法的内部逻辑。通过该代码,我们可以发现。当>-128且<127时,就会在IntegerCache中table中查找。如果超出这个范围,则新建一个Integer对象。所以在示例中的e==f是返回false的。

public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
}

附录

参考简书及Thinking in java.
https://www.jianshu.com/p/501def34d9ed
https://www.jianshu.com/p/a36c3e8014eb
https://www.cnblogs.com/wyq1995/p/10945034.html
https://www.cnblogs.com/cruze/p/3865180.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值