Java高级——枚举、注解与反射

前言

枚举、注解、反射是三个Java中最重要,也是最容易被忽视的三个技术。很多人只知道利用框架机械性地使用它们,但是对原理掌握不扎实,下面一起从根本来理解它们。

一、枚举

1.1 枚举简介

JDK1.5引入了一个新的类型——枚举

在JDK1.5之前,我们定义常量都是:
public static final xxx
很难去管理。

而枚举,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。

枚举:用于定义有限数量的一组同类常量,例如:

  • 错误级别:
    低、中、高、紧急

  • 一年四季:
    春、夏、秋、冬

  • 商品的类型:
    美妆、手机、电脑、男装、女装…

在枚举类型中定义的常量是该枚举类型的实例

1.2 枚举定义格式

在JDK1.5之前, 我们定义一个类型的常量的方式:

//JDK1.5之前描述级别的方式:
public class Level {
    private int levelValue;                  //私有属性值
    private Level(int levelValue){           //封装构造方法,让外部无法直接利用构造方法创建对象
        this.levelValue = levelValue;
    }

    //接下来使用公开静态常量属性的方式,来创建对象
    //用户可以通过类名.常量名获取这些常量,获取的常量就是一个Level类型
    //然后通过.getLevelValue()方法获取它的值
    public static final Level LOW = new Level(1);
    public static final Level MIDDLE = new Level(2);
    public static final Level HIGH = new Level(3);

    public int getLevelValue() {
        return levelValue;
    }

    public void setLevelValue(int levelValue) {
        this.levelValue = levelValue;
    }
}

以上的方式,显然代码量有点冗余,而且并不好提供一些常量对比方法。

枚举定义方式

权限修饰符 enum 枚举类型名称{
		实例1,实例2,实例3....;
}

例如:

public enum Level {
    //描述枚举类型(创建对象)
    LOW(1),
    MIDDLE(2),
    HIGH(3);

    private int levelValue;  //正常私有属性

    private Level2(int levelValue){  //正常私有构造方法
        this.levelValue = levelValue;
    }

    public int getLevelValue() {
        return levelValue;
    }

    public void setLevelValue(int levelValue) {
        this.levelValue = levelValue;
    }
}

此时的代码量就比较简洁了,甚至还可以这样:

//不赋值,直接通过名字从字面意义上判断大小
public enum Level {
    //描述类型的对象(后面可以跟括号,也可以不跟)
    LOW,MIDDLE,HIGH;
    //无私有属性
    //私有无参构造(可省略)
    //private Level3(){}

}

可以通过字面意思直接判断Level类型的状态,也可以通过自定义的方法来实现比较规则等。

1.3 枚举类的主要方法

Enum抽象类常见方法

Enum是所有Java枚举类型的公共基本类(注意Enum是抽象类),以下是它的常见方法:

返回类型 方法名称 方法说明
int compareTo(E e) 比较此枚举与指定对象的顺序
boolean equals(Object o) 当指定对象等于此枚举类型时,返回true
Class getDeclaringClass() 返回此枚举常量的枚举类型对应的Class对象
String name() 返回此枚举常量的名称,在其枚举声明中对其进行声明
int ordinal() 返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零)
String toString() 返回枚举常量的名称,它包含在声明中
static T> static valueOf(Class enumType, String name) 返回带有指定名称的指定枚举类型的枚举常量

comareTo

以下方枚举为例:

public enum Level3 {
    LOW, MIDDLE, HIGH;
}

此方法按照枚举对象定义的顺序来作为比较的依据,与值无关

int a = Level3.LOW.compareTo(Level3.MIDDLE); //a = -1
int b = Level3.LOW.compareTo(Level3.HIGH);   //b = -2
int c = Level3.HIGH.compareTo(Level3.LOW);   //c = 2

valueOf

Level3 level = Enum.valueOf(Level3.class,"LOW");

此时的 level 就是 Level3 的 LOW 对象

1.4 实现接口的枚举类

所有的枚举都是继承自java.lang.Enum类,由于Java不支持多继承,所以枚举对象不能再继承其他类。

每一个枚举对象,都可以实现自己的抽象方法

示例接口:

public interface LShow{
    void show();
}
public enum Level3 implements LShow {
    LOW(){
        @Override
        public void show() {
            System.out.println("我是LOW");
        }
    }, MIDDLE, HIGH;
    
    @Override
    public void show() {
        System.out.println("我是公共方法");
    }
}

当LOW对象调用show时输出“我是LOW”,而其他没有实现自己的抽象方法的对象调用show方法,输出的则是“我是公共方法”。

:对象后的小括号指的是调用构造方法,如果对象调用的是无参构造方法,那么可以省略小括号,如果对象调用有参构造方法,那么括号和参数都必须要有。

1.5 注意事项

  • 一旦定义了枚举,最好不要妄图修改里面的值,除非修改是必要的
  • 枚举类默认继承的是java.lang.Enum类而不是Object类
  • 枚举类不能有子类,因为默认被final修饰,也不能有其他父类,因为Java不能多继承,但是可以实现接口。
  • 构造方法只能是 private
  • switch中使用枚举时,直接使用常量名(对象名),不用携带类名
  • 不能定义name属性,因为自带name属性
  • 不要为枚举类中的属性提供set方法,这不符合枚举最初设计的初衷

二、注解

2.1 注解简介

Java 注解 (Annotation)又称Java标注,是JDK5.0引入的一种注释机制。

Java语言中的类、方法、属性、参数和包等都可以被标注,和注释不同,Java标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java虚拟机可以保留标注内容,在运行时可以获取到标准内容。当然它也支持自定义Java注解。

注解主要用于:

  • 编译格式检查
  • 反射中解析
  • 生成帮助文档
  • 跟踪代码依赖
  • 等…

2.2 学习重点

理解 Annotation 的关键,是理解 Annotation 的语法和用法。

学习步骤:

  1. 概念
  2. 怎么使用内置注解
  3. 怎么自定义注解
  4. 反射中怎么获取注解内容

2.3 内置注解

  • @Override: 重写
    用于标注重写了父类的方法

  • @Deprecated: 废弃
    用于标注该类/接口/方法已被废弃,不建议使用,一般用于项目中代码的扩展

  • @SafeVarargs(一般不怎么使用)
    Java 7 开始支持,忽视任何使用参数为泛变量的方法或构造函数调用产生的警告

  • @FunctionalInterface: 函数式接口
    Java8开始支持,标注一个匿名函数或函数式接口

  • @Repeatable: 标注某注解可以在同一个声明上使用多次

  • @SuppressWarning: 抑制编译时的警告信息

2.4 元注解

2.4.1 元注解简介

作用于其他注解(自定义注解)的注解

2.4.2 四大元注解

  • @Retention:表示这个注解怎么保存,是只在代码中,还是编入.class文件中,还是在运行时可以通过反射访问
  • @Documented:表示该注解可以包含在用户文档中
  • @Target:表示这个注解可以作用的范围,类/变量/方法等
  • @Inherited:表示这个注解可以被子类自动继承

注意:

  1. 子类会继承父类的被@Inherited标记的注解
  2. 接口继承关系中,子接口不会继承父接口中的任何注解,不管父接口中使用的注解有没有被@Inherited标记
  3. 类实现接口时不会继承接口中的任何注解

2.5 自定义注解

2.5.1 注解架构

Java高级——枚举、注解与反射_第1张图片

01)Annotation 与 RetentionPolicy 与 ElementType

每一个 Annotation 对象,都会有唯一的 RetentionPolicy 属性;至于ElementType,则有1-n个。

02)ElementType(注解的用途类型)

每一个 Annotation 都与 1-n 个 ElementType 关联。当 Annotation 与某个 ElementType 关联时,就意味着:Annotation有了某种和用途。例如,若一个 Annotation 对象的 ElementType是METHOD,那么该Annotation只能用来修饰方法。

public enum ElementType {

    TYPE,            //类、接口(包括注解)或枚举的声明
    
    FIELD,           //字段声明(包括枚举常量)
 
    METHOD,          //方法声明

    PARAMETER,       //参数声明

    CONSTRUCTOR,     //构造方法声明
    
    LOCAL_VARIABLE,  //局部变量声明

    ANNOTATION_TYPE, //注解类型声明

    PACKAGE,         //包声
}

03)RetentionPolicy(注解作用域策略)

“每一个 Annotation 都与一个 RententionPolicy 关联”

  • 若Annotation的类型为SOURSE,则意味着:Annotation仅存在于编译器处理期间,编译器处理完之后,这个Annotation就没用了。例如"Override",当它修饰一个方法时,就意味着该方法重写了父类的方法;并且在编译期间会进行语法检查。编译器处理完后,"Override"就没有作用了
  • 若Annotation的类型为CLASS,则意味着:编译器将Annotation储存于类对应的.class文件中,它是Annotation的默认行为
  • 若Annotation的类型为RUNTIME,则意味着:编译器将Annotation储存于.class文件中,并且可以由JVM读入。
public enum RetentionPolicy{
	SOURCE,    //Annotation信息仅存在于编译器处理期间,编译器处理完之后就没用了
	CLASS,     //编译器将Annotation储存于类对应的.class文件,默认行为
	RUNTIME    //编译器将Annotation储存于类对应的.class文件中,并且可以由JVM读取
}

2.5.2 注解的定义格式

public @Interface 注解名{}

2.5.3 自定义注解注意事项

  1. 定义的注解,自动继承了Annotation 接口
  2. 注解中的每一个方法,实际上都是声明的注解配置参数
    方法的名称就是参数名称
    方法的返回值类型就是参数的类型。只能是:基本类型/Class/String/enum
  3. 可以通过default来声明参数的默认值
  4. 如果只有一个参数成员,一般参数名为value
  5. 注解元素必须要有值,我们定义注解元素时,经常使用空字符串、0作为默认值

2.5.4 案例

//元注解
@Documented                                    //表示此注解可以在文档中生成
@Target({ElementType.TYPE,ElementType.METHOD}) //表示此注解的作用范围:可以用在类、接口、枚举、注解、方法上
@Retention(RetentionPolicy.RUNTIME)            //持久策略(JVM可读取)
@Inherited                                     //表示该注解可以被子类继承
@interface MyAnnotation{
    String value() default "";
    int num() default 1;
}

上面是自定义的一个 Annotation ,它上方的四个元注解以及@interface含义分别是:

  1. @interface
        使用@interface定义注解时,意味着它实现了Annotation接口,即该注解就是一个Annotation
        自定义Annotation 时,@interface是必须的
        注意:它和我们通常的implemented实现接口的方法不同。Annotation接口的实现细节全部有编译器完。通过@interface定义注解后,该注解不能继承其他的注解或接口。

  2. @Documented
        类和方法的Annotation在缺省的情况下是不不出现在javadoc中的。如果使用@Documented修饰该Annotation,则表示它可以出现在javadoc中。
        定义Annotation时,@Document可有可无

  3. @Target(ElementType.TYPE)
        之前说过,ElementType 是Annotation 的类型属性。而@Target的作用,就是来指定Annotation的类型属性
        @Target(ElementType.TYPE) 的意思就是指定该 Annotation 的类型是 ElementType.TYPE。这就意味着,MyAnnotation1 是来修饰"类、接口(包括注释类型)或枚举声明"的注解
        定义 Annotation 时,@Target 可有可无。若有 @Target,则该 Annotation 只能用于它所指定的地方;若没有 @Target,则该 Annotation 可以用于任何地方。

  4. @Retention(RetentionPolicy.RUNTIME)
        之前说过,RententionPolicy是 Annotation 的保存策略属性,而@Retention的作用,就是指定Annotation的保存策略。
        @Retention(RetentionPolicy.RUNTIME) 的意思就是指定该 Annotation 的策略是RetentionPolicy.RUNTIME。这就意味着,编译器会将该 Annotation 信息保留在 .class 文件中,并且能被JVM读取。
        定义 Annotation 时,@Retention 可有可无。若没有 @Retention,则默认是RetentionPolicy.CLASS。

三、反射

3.1 反射概述

Java反射机制是在运行状态中(Runtime),可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。
这种运行状态动态获取信息以及动态调用对象的功能被称为Java语言的反射机制。

3.2 类加载器

Java类加载器(Java ClassLoader) 是Java运行时环境JRE(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。

Java默认有三种类加载器:BootStrapClassLoader、ExtensionClassLoader、AppClassLoader。

BootStrapClassLoader(引导启动类加载器):
嵌在JVM内核中的加载器,该加载器是用C++语言写的,主要负责加载JAVA_HOME/lib下的类库,引导启动类加载器无法被应用程序直接使用。

ExtensionClassLoader(扩展类加载器):
是用Java语言编写的,主要加载JAVA_HOME/lib/ext目录下的类库,它的父类加载器是BootStrapClassLoader

AppClassLoader(应用类加载器):
应用程序类加载器,负责加载应用程序classpath目录下的所有jar和class文件,它的父类加载器为ExtensionClassLoader

Java高级——枚举、注解与反射_第2张图片

类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。学习类加载器时,掌握Java类派很重要。

双亲委派模型:如果一个类加载器收到了一个类加载请求,它不会自己去尝试加载这个类,而是把这个请求转交给父类加载器去完成。每一个层次的类加载器都是如此。因此所有的类加载请求都应该传递到最顶层的启动类加载器中,只有父类加载器反馈自己无法完成这个加载请求(在他的搜索范围内没有找到这个类)时,子类加载器才会尝试自己去加载,委派的好处就是避免有些类被重复加载

3.2.1 加载配置文件

  • 给项目添加resource root 目录
    Java高级——枚举、注解与反射_第3张图片

  • 通过类加载器获取配置文件的输入流,从而加载文件信息
    Java高级——枚举、注解与反射_第4张图片
    注意:默认加载的是src路径下的文件,但是当项目存在resource root目录时,就变成了加载resource root下的文件了

3.3 所有类型的Class对象

想要了解一个类,必须先要获取到该类的字节码文件对象,在Java中,每一个.class字节码文件被加载到内存后,其在内存中的表现形式就是Class类的一个对象

3.4 得到Class的几种方式

  1. 如果在编写代码时,知道类的名称,且类已经存在,可以使用:
    包名.类名.class来得到该类的字节码文件对象
  2. 如果拥有类的对象,可以通过:
    对象名.getClass()来得到该类的字节码文件对象
  3. 如果编写代码时,类并不存在,但是知道类名,可以通过:
    Class.forName("包名+类名")在运行时得到该类的字节码文件对象

上述三种方式在调用时,如果类在内存中不存在,则会先通过类加载器的双清委派模型将类加载进内存。如果在内存中已经存在,则直接获取该类的字节码文件对象,不会重复加载,只会重复利用

特殊类的对象

  • 基本数据类型的类的对象
    基本数据类型.class:int.class
    包装类.type:Integer.TYPE
  • 基本数据类型包赚给对象
    包装类.class:Integer.class
//第一种方式:通过包名.类名.class加载类(获取类的字节码文件的对象)
        Class<Person> c1 = Person.class;
        System.out.println(c1);      

        //第二种方式:通过类的对象获取类的信息,此时类信息肯定已经加载进内存了,所以不存在加载类,只是通过对象获取类的信息
        Person person = new Person();
        Class<Person> c2 =  (Class<Person>)person.getClass();
        System.out.println(c2);

        //第三种方式:通过类的位置字符串来获取类的对象(重点)
        Class<?> c3 = Class.forName("Demo.Person");
        System.out.println(c3);

        System.out.println(c1 == c2 && c1 ==c3); //true 证明类只加载了一次。三者指向的都是同一个对象

3.5 获取Constructor(构造方法)

3.5.1 通过class对象 获取一个类的构造方法

  1. 通过指定的参数类型,获取指定的单个构造方法
    getConstructor(参数类型的class或数组)

  2. 获取构造方法数组
    getContructors()

  3. 获取所有权限的单个构造方法
    getDeclaredConstructor(参数类型的class或数组)

  4. 获取所有权限的构造方法数组
    getDeclaredConstructors()

3.5.2 利用 Constructor 创建对象

常用方法:
newInstance(Object param)
作用:调用这个构造方法,传入对应的参数,把对象创建处理
参数:是一个Object类型的可变参数,传递的参数顺序必须匹配构造方法中形参列表的顺序!!!

setAccessible(boolean flag)
作用:设置是否忽视权限检查,true代表忽视权限检查。

3.5.2 代码

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //1.通过类名获取到该类的字节码文件对象
        Class<?> c = Class.forName("Demo.Person");
        //2.从该类的字节码对象中获取该类的无参构造方法
        Constructor<?> c1 = c.getConstructor();
        //3.通过无参构造方法创建对象
        Object o = c1.newInstance();

        //找到全参构造方法
        Constructor<?> c2 = c.getConstructor(String.class, int.class);
        //传参,并创建对象
        Object o1 = c2.newInstance("李四", 1);

        //获取私有的构造方法
        Constructor<?> c3 = c.getDeclaredConstructor(String.class);
        //设置忽略权限检查
        c3.setAccessible(true);
        //传参并创建对象
        Object o2 = c3.newInstance("王五");
    }

3.6 获取Method(方法)

3.6.1 通过class对象 获取一个来的方法

  1. getMethod(String methodName, 参数类型.class)
    根据方法名和参数列表的类型,得到一个方法对象(public 修饰)

  2. getMethods()
    得到一个类的所有方法

  3. getDeclaredMethod(String methodName, 参数类型.class)
    根据方法名和参数列表的类型,得到一个方法对象(除继承以外的所有权限的方法)

  4. getDeclaredMethods()
    得到一个类的所有方法(除了继承以外的所有权限的方法)

3.6.2 Method 执行方法

invoke(Object o,Object param)
执行方法的方法
参数1:要调用该方法的对象
参数2:要传递的参数列表

getName()
获取方法的方法名称

setAccessible(boolean flag)
如果flag为true 则表示忽略权限检查

3.6.3 代码

public static void main(String[] args) throws Exception{
        //加载类
        Class<?> cla = Class.forName("Demo.Person");
        //获取类的构造方法
        Constructor<?> c = cla.getConstructor();
        //创建对象
        Object o = c.newInstance();

        //获取类的方法(public修饰的方法)
        Method setName = cla.getMethod("setName", String.class);
        //private修饰的方法
        Method setAge = cla.getDeclaredMethod("setAge", int.class);
        setAge.setAccessible(true); //忽略权限检查
        //参数1:哪个对象要执行setName方法,
        //参数2:调用方法时传递的参数 0-n个
        setName.invoke(o,"张三");
        setAge.invoke(o,18);
    }

3.7 获取Field(属性)

3.7.1 通过class对象 获取一个类的属性

  1. getField(String filedName)
    根据属性的名称, 获取一个属性对象 (public属性)
  2. getFields()
    获取所有属性 (public)
  3. getDeclaredField(String filedName)
    根据属性的名称, 获取一个属性对象 (所有属性)
  4. getDeclaredFields()
    获取所有属性

3.7.2 Field的常用方法

常用方法:

  1. get(Object o)
    参数:要获取属性的对象
    获取指定对象的此属性

  2. set(Object o, Object value)
    参数1:要设置属性的对象
    参数2:要设置的值
    设置指定对象的属性的值

  3. getName()
    获取属性的名称

  4. setAccessible(boolean flag)
    如果flag为true 则表示忽略访问权限检查!

3.7.3 代码

public static void main(String[] args) throws Exception{
        //加载类
        Class<?> cla = Class.forName("Demo.Person");
        //获取无参构造方法
        Constructor<?> c = cla.getConstructor();
        //创建对象
        Object o = c.newInstance();

		//通过类获取方法(public)
        Field phone = cla.getField("phone");
        phone.set(o,"123456");
		//通过类获取方法(private)
        Field name = cla.getDeclaredField("name");
        name.setAccessible(true);
        name.set(o,"李四");
    }

3.8 获取注解信息

3.8.1 获取类/属性/方法的全部注解对象

Annotation[] annotations = Class/Field/Method.getAnnotations();
for (Annotation annotation : annotations01){
 System.out.println(annotation);
 }

3.8.2 根据类型获取类/属性/方法的注解对象

注解类型 对象名 = c.getAnnotation(注解类型.class);

3.8.3 代码实例

自定义注解:表注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
public @interface TableAnnotation {
    /**
     * 标注类对应的表格名称
     * @return
     */
    String value();
}

自定义注解:字段注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface ColumnAnnotation {
    /**
     * 描述列名
     * @return
     */
    String columnName();

    /**
     * 描述类型
     * @return
     */
    String type();

    /**
     * 描述长度
     * @return
     */
    String length();
}

JavaBean:Book类

@TableAnnotation("test_Book")
public class Book {
    @ColumnAnnotation(columnName = "name",type = "varchar",length = "50")
    private String name;
    @ColumnAnnotation(columnName = "info",type = "varchar",length = "1000`")
    private String info;
    @ColumnAnnotation(columnName = "id",type = "int",length = "11")
    private int id;

    public Book() {
    }

    public Book(String name, String info, int id) {
        this.name = name;
        this.info = info;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", info='" + info + '\'' +
                ", id=" + id +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return id == book.id && Objects.equals(name, book.name) && Objects.equals(info, book.info);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, info, id);
    }
}

测试类

public class Demo {
    public static void main(String[] args) throws Exception{
    	//加载类
        Class<?> c = Class.forName("Demo2.Book");
        //通过Book类的字节码文件对象获取类上的注解对象
        TableAnnotation ta = c.getAnnotation(TableAnnotation.class);
        //通过注解对象获取其注解中设置的属性值
        String value = ta.value();
		
		//根据Book类的字节码文件对象获取Book类的所有权限的属性
        Field[] fields = c.getDeclaredFields();
        for (Field field : fields) {
        	//忽略权限检查
            field.setAccessible(true);
            //通过属性对象获取其注解对象
            ColumnAnnotation ca = field.getAnnotation(ColumnAnnotation.class);
            //通过注解对象获取对应的属性值
            String length = ca.length();
            String type = ca.type();
            System.out.println(field.getName()+"属性,对应数据库中的字段:"+ca.columnName()+"数据类型:"+type+"长度:"+length);
        }
    }
}

你可能感兴趣的