一文搞懂Java的类型、反射与注解

RTTI(Run-Time Type Identification)运行时类型识别

假设有一个Animal类,那么在声明时,可以将一个Animal的实例对象赋值给Object,因为所有类都继承了Object

Object obj = new Animal();
    
// 假设Animal有这么一个实例方法,这么编译器会报错
obj.run();
复制代码

此时类型会丢失,若Animal类中有一个run方法,是无法使用obj.run()去调用的
因为Java是一个强类型语言,声明了objObject类型的,因此没有run方法

在任何时刻,任何一个对象都清楚的知道自己是什么类型

只需要调用Object.getClass方法,将一个String实例赋值给Object

        Object str = new String("sdkjflsdjfksd");
        Class<?> aClass = str.getClass();
        System.out.println(aClass);
复制代码

此时会打印出一个具体类型的名字class java.lang.String,由此拿到了类的真实信息

类型与Class与JVM的关系

在编写程序时,写的是xx.java文件,称为源代码,想要在JVM中执行,必须先经过一个编译过程(Compile)变成xx.class文件,也叫字节码文件,这是Java跨平台的基石,JVM可以跑xx.class

先看一张JVM(Java Virtual Mechine )的示意图,所有的对象都是在堆上分配的,假设要new一个Animal类,那么在堆上就会多一个Animal的对象。那么Animal是如何被创建出来的?因此我们需要一份类的说明书,所有需要new出来的对象都根据这份说明书去装配。
如果Animal类中有一个静态方法或者静态属性(static),那么这个静态属性或方法是归属于这份类说明书的,所以可以Animal.[静态方法或静态属性名]去使用,而通过new出来的对象不会拥有Class的静态属性和方法 JVM示意图

instanceof

了解了这些知识点后,就可以理解关键字instance的原理了。因为对象任何时刻都获取到对应真实的类说明书,先拿到instanceof关键字前的实例类型是由什么说明书装配的,再拿这份说明去对比instanceof关键字后面的说明书是不是同一份。

    System.out.println(("test" instanceof String));
    // true
复制代码
强制类型转换

这里强制把一个String转成Integer类型,因为这个行为本身是不安全的,所以编译器允许,但是运行时会报错,因为obj知道自己是个String,强行转换时会被JVM给拒绝掉操作,并抛出一个Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer异常

Object obj = new String("abc");
Integer i = (Integer) obj;
// 执行就报错
复制代码

Class对象是怎么来的?

存放Class说明书的地方,在Java7之前叫永久代,Java8之后叫做元空间
说明书从哪里来,这个问题包含了内置类的和我们手动编写的类

一张Class对象的生命周期图奉上

可以了解到一个class对象是怎么被加载到JVM里的 image 答案是被在第一次被使用的时候加载
new Animal()的时候:(Animal可能没有写extends,但会隐式的存在Animal extends Object
在加载AnimalClass之前,由于它的父类是Object,所以得先加载ObjectClass, 如果继承关系多,则依次类推,越上层的Class越早被加载,因为子类需要用到(先有父亲才能有儿子)

做一个小实验去验证

  1. 在任意磁盘目录下创建一个Test.java文件,文件内容如下,为方便演示所以使用静态内部类继承了Test
public class Test{                              
                                                
        public static void main(String[] args){ 
                                                
                Object obj = new Test.Child();  
                                                
        }                                       
                                                
        static class Child extends Test{}       
}                                               
复制代码
  1. 在当前文件所在目录使用javac Test.java编译出Test.class文件
  2. 执行文件并添加一个参数增加输出内容,并将标准输出内容按关键字过滤掉
    java -verbose:class Test | grep Test
复制代码

不出意外的话,会看到下面这么两条信息,我们想去new一个ChildLoader的顺序是先加载Test,再加载Test$Child,加载完后在元空间中就会存在这两份说明书,new操作就可以找到对应的Class对象装配成对象实例。此时还可以看到,这两个类是从系统中哪里来的。
这里还有一个更优先加载的是ObjectClass,只是被过滤掉了,会从环境变量配置的目录中jre文件夹下的lib文件夹中的rt.jar(runtime的缩写)中去找到Object的字节码文件
jar包其实就是zip包,将后缀名改名zip就可以unzip命令解压出来

[Loaded Test from file:/D:/development/test/]
[Loaded Test$Child from file:/D:/development/test/]

  1. javap命令可以查看class文件的内容,把字节码翻译成元空间的那份说明书javap Test.class
Compiled from "Test.java"
public class Test {
  public Test();
  public static void main(java.lang.String[]);
}
复制代码

执行这个加载Class动作的类就是Classloader(类加载器)

  • Classloader负责从外部系统中加载一个类
    1. 这个类对应的Java文件并不一定需要存在,因为JVM跑的是字节码文件,只要能凭空的创造出一个符合JVM规范的字节码文件也能加载
    2. 字节码也不一定需要存在可以动态生成,也叫动态字节码增强,比如可以从网上获取资源,文件的本质是字节流,这种分离提供了JVM中可以执行其他语言的可能
    3. 这是Java世界丰富多彩的基石

Classloader的双亲委派加载模型

Classloader对象都有一个父亲(parent),可以通过getParent获取,还有一个方法叫loadClass,任何一个类加载器被要求加载类时,会调用loadClass方法,但都会先问一遍自己的父亲索要,如果父亲返回了就不会再自己去加载,没有返回才会尝试自己加载,源码可以简单看看了解大概在做什么就行

    @CallerSensitive
    public final ClassLoader getParent() {
        if (parent == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // Check access to the parent class loader
            // If the caller's class loader is same as this class loader,
            // permission check is performed.
            checkClassLoaderPermission(parent, Reflection.getCallerClass());
        }
        return parent;
    }
    

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            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) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }
复制代码

这里编写了一段恶意代码,一个包名相同但构造函数抛出异常的String

package java.lang;

public class String {
    public String() {
        throw new IllegalArgumentException();
    }
}
复制代码

因为有双亲委派机制,在加载这个类的时候,会问他的父亲是否加载过这个类,出于安全考虑,基本类都是由最顶端的启动类加载器进行加载,所以这段有恶意代码的String类不会被加载

虽然叫双亲(parent)委派,但实际是单亲 : )

强大的反射

反射为我们提供了如下特点

  1. 根据参数动态的创建一个对象
  2. 根据参数动态调用一个方法
  3. 根据参数动态获取一个属性(或设置)

根据参数传递的名字,创建一个对应的对象

public class Check {
    public static void main(String[] args) throws ClassNotFoundException,
    NoSuchMethodException, InvocationTargetException, 
    InstantiationException, IllegalAccessException {
        Class klass = Class.forName(args[0]);
        Object obj = klass.getConstructor().newInstance();
    }
}
复制代码

示例中,只要在执行参数中传入一个Class的全限定类名,比如java.lang.String,就能生成一个对应的实例对象(能找到该类的话)

  • Class.forName方法根据名字返回类的对象,对应着上文JVM示例的图,传入Animal的全限定类名,就能找到这份说明书
  • 除了Class.forName以外,实例对象的getClass方法,[Class名].class都可以获取Class对象
  • 调用getConstructor方法时,会返回public的构造器,与之对应的有一个getDeclaredConstructor会返回包括非public的构造器,想要获取的构造器有入参时,则需要在getConstructor(String.class)获取构造器时传入参数类型的Class对象
  • 调用newInstance可以看成new XXX()操作,有入参要求则传入对应参数。甚至可以不通过构造器,直接使用Class.newInstance()实例化一个对象,但这个方法在JDK9中被废弃了,不建议使用
  • getConstructorsgetDeclaredConstructors方法都是获得Constructor数组的作用

根据参数传递的名字,调用对应的方法

  • 调用方法可以使用Methodinvoke方法,此方法需要传入对象的实例和对应的入参(如果有)

要注意getMethodgetDeclaredMethod的区别

  • 前者getMethod只能获取所有权限为public的方法,包括其父类方法
  • 后者getDeclaredField可以获取到包括非public的方法(private),但只限在本类,无法获得继承父类的任何方法。
  • 如果要获取有参数的方法,则在获取方法时,需要传入对应的参数类型Class对象
  • getMethodsgetDeclaredMethods方法都是获得Method数组的作用
public class Animal {
    public void run(String name) {
        System.out.println("run with:" + name);
    }

    public static void main(String[] args) throws Exception {
        Animal animal = new Animal();
        Method run = animal.getClass().getMethod("run",String.class);
        run.invoke(animal,"test");
    }
}
复制代码

方法示例中,通过反射获取了run方法并传入参数调用

public class Animal {
    public void run(String name) {
        System.out.println("run with:" + name);
    }

    private void sleep(){
        System.out.println("sleep");
    }

    public static void main(String[] args) throws Exception {
        Animal animal = new Animal();
        Method run = animal.getClass().getDeclaredMethod("sleep");
        run.invoke(animal);
    }
}
复制代码

方法示例中,通过反射获取了sleep方法并调用

根据参数传递的名字,获取对应的属性(或设置属性)

  • 获取属性调用Fieldget方法,需要传入对象实例
  • 设置属性调用Fieldset方法,需要传入对象实例和即将要赋予的值

要注意getFieldgetDeclaredField的区别

  • 前者getField只能获取所有权限为public的字段,包括其父类字段
  • 后者getDeclaredField可以获取到包括非public的字段(private),但只限在本类,无法获得继承父类的任何字段。获取到private权限属性,此时还无法进行访问,会抛出权限异常,对Field对象调用其setAccessible(true)才能正常访问
  • getFieldsgetDeclaredFields方法都是获得Field数组的作用
public class Animal {
    public String head = "head";

    public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
        Animal animal = new Animal();
        System.out.println(animal.getClass().getField("head").get(animal));
    }
}
复制代码

示例中,在执行参数中传入head,利用反射能获取到对应的属性(get) 还有set方法

public class Animal {
    public String head = "head";
    private Long id = 1L;

    public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
        Animal animal = new Animal();
        Field id = animal.getClass().getDeclaredField("id");
        id.setAccessible(true);
        id.set(animal, 100L);
        System.out.println(id.get(animal));
    }
}
复制代码

示例中,获取到一个私有属性id后,需要先设置访问权限,通过set方法将1改成100,最后输出

注解 Annotation

Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。 Java 语言中的类、方法、变量、参数和包等都可以被标注。Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。

在反射中使用较多的还有getAnnotation等相关获取注解的方法,Class,Method,Field可通过相关方法获取注解信息,注解在Spring世界中应用的非常广泛

声明一个注解的格式,和接口比较类似,在interface前加多一个@符号,在代码中就可以@Test使用此注解

public @interface Test{
}
复制代码

注解和反射是分不开的,注解就是Class说明书中的一小段信息标记,一个类比,Class就是一件衣服,衣服上有个吊牌写着不可水洗,只能干洗,这个吊牌所携带的信息就是注解,除了类以外,方法,字段等都可以使用注解 注解仅仅只是一段信息,它本身无法进行工作,如果没有东西去处理注解,那么注解就没有意义

一些内置的注解:元注解,就是可以用在注解上的注解

  1. @Target,此注解的作用为指定当前注解能作用在哪些地方,当没有@Target时,注解可以用在任何地方,有@Target时,注解只能用于指定的范围内
// 声明注解只能用在字段
@Target(ElementType.FIELD)
public @interface Cache {
}

// 使用注解
public class Animal{
    @Cache
    String name;
}
复制代码

@Target注解可接收ElementType枚举数组,这里传入了ElementType.FIELD枚举,意为此注解只能用在属性字段上 java.lang.annotation.ElementType枚举

  • ElementType.TYPE 可用在类,接口,注解以及枚举类型上
@Cache
public class Animal{
}
复制代码
  • ElementType.FIELD 可用在字段声明上(包括枚举常量),
public enum StatusEnum {
    @Cache OK,FAILD
}
复制代码
  • ElementType.METHOD 可用在方法声明上,不包括构造函数
public class Animal{
    @Cache
    void run(){}
}
复制代码
  • ElementType.PARAMETER 可用在方法参数声明上
public class Animal{
    void run(@Cache String name){}
}
复制代码
  • ElementType.CONSTRUCTOR 可用在构造函数声明上
public class Animal{
    @Cache
    public Animal(){}
}
复制代码
  • ElementType.LOCAL_VARIABLE 可用在方法中的局部变量(无法通过反射获取该注解信息),没什么卵用
public class Animal{
    void run(){
        @Cache
        String address = "where"; 
    }
}
复制代码
  • ElementType.ANNOTATION_TYPE 可用在注解上的注解 : )
@Cache
public @interface Log{
}
复制代码
  • ElementType.PACKAGE 可用在包声明上,在每个包下都可以新建一个package-info.java
@Cache
package main.java.Test;
复制代码
  • ElementType.TYPE_PARAMETER 可用在类型参数上
public class Test<@Cache T>{}
复制代码
  • ElementType.TYPE_PARAMETER 可用在使用类型的任意语句中
String str = new @Cache String("test");
复制代码
  1. @Retention 此注解作用为标注注解以何种策略进行保留

@Retention注解可接收RetentionPolicy枚举数组

  • ElementType.SOURCE 注解会在编译时丢弃,也就是注解只保留在Java源代码中
  • ElementType.CLASS 注解经过编译后会保留在字节码当中,JVM运行时不会保留,默认为此注解
  • ElementType.RUNTIME 注解经过编译后会保留在字节码当中,并且原型是会保留到JVM当中,想要使用反射进行操作,类型必须为ElementType.RUNTIME
  1. @Documented注解,作用是在使用javadoc生成时是否记录,用的不多,因为javadoc用的也不多 : )

介绍俩个JDK自带的注解

  1. @Override 子类重写父类方法的注解,重写时不加也可以工作,但是加了能减少错误的方法,比如重写时方法名写错了,我们以为重写了,但实际没有,此时编译器就能检查到并给出对应的报错
  2. @Deprecated 被标记的地方(比如方法),调用时IDE会提示这是一个被废弃的方法,但仍然可以使用,只是提示使用者不该再使用
  3. @SuppressWarnings 允许选择性的消除某些不想让编译器检查的警告,接受一个String,可传入all,uncheck,unused
  4. @FunctionalInterface 用于标注对应的接口时函数接口

注解的属性

复用上面用过的类比,Class就是一件衣服,衣服上有个吊牌就是Annotation, 可以在这个吊牌标记一些属性,比如传入属性可以水洗可以干洗,根据不同的需求传入不同的参数以此来复用这个吊牌(Annotation
注解可用的属性类型有基本数据类型String,类(Class),以及它们俩的数组,如果不传参数,则使用时()可加可不加

这个注解定义了一个属性为洗涤方式washMethod,类型为String[],并使用"干洗"作为这个属性的默认值

public @interface Clothes {
    String[] washMethod() default "水洗";
}
复制代码

使用时如果不传,这个吊牌的washMethod属性就是水洗,传参则属性为传入的参数

@Clothes(washMethod = {"随便洗","用力洗"})
public class Animal { 
}
复制代码

注解有个特点,如果属性名为value,只传入value时,属性名可以不写,但如果要传入多个参数,则需要写完整的传值;如@Clothes(value=1)

// 定义value
public @interface Clothes {
    String[] washMethod() default "水洗";
    String value() default "";
}

// 只传入value时,可以不写名字(当然想写也可以)
@Clothes("test")
public class Animal {
}

// 传入多个时,则必须写完整
@Clothes(value = "test", washMethod = {"随便洗", "水洗"})
public class Animal {
}
复制代码

如何将注解与反射应用到实践中,可以看看我写的这篇博客 手写一个Spring的IOC容器 ,里面讲解了使用反射与注解,实现了自动装配Bean的过程 : )

猜你喜欢

转载自juejin.im/post/7053985896705556510