Java零基础入门到精通22 枚举和反射以及注解

枚举学习目标

  • 了解枚举的概念
  • 掌握枚举的格式
  • 掌握枚举的应用场景
  • 掌握枚举的使用

枚举的概述

枚举是 Java 中一种特殊的类,它可以定义固定数量的枚举实例,例如: 性别、交通信号灯、季节等等。

为什么要使用枚举

假设我们要定义一个人类,人类中包含姓名和性别。通常会将性别定义成字符串类型,效果如下:

public class Person {
    private String name;
    private String sex;

    public Person() {
    }

    public Person(String name, String sex) {
        this.name = name;
        this.sex = sex;
    }
	
    // 省略get/set/toString方法
}
public class Demo01 {
    public static void main(String[] args) {
        Person p1 = new Person("张三""男");
        Person p2 = new Person("张三""abc"); // 因为性别是字符串,所以我们可以传入任意字符串
    }
}

不使用枚举存在的问题:可以给性别传入任意的字符串,导致性别是非法的数据,不安全。

枚举作用

一个方法接收的参数是固定范围之内的时候,那么即可使用枚举类型

枚举格式

枚举类使用enum关键字定义,语法格式如下:

enum 枚举名 {
    第一行都是罗列枚举实例,这些枚举实例直接写大写名字即可。
}

比如,我们定义一个性别的枚举类。我们定义性别里面只有男和女两种性别。代码如下:

package com.zhangdapeng520;

public enum Gender {
    MALE("男")// 前面的枚举对象以逗号结尾
    FEMALE("女"); // 最后一个枚举对象以分号结尾

    private String tag; // 标签

    Gender(String tag) {
        this.tag = tag;
    }

    public String getTag() {
        return tag;
    }
}

枚举入门案例

定义枚举:MALE表示男,FEMALE表示女。枚举类的主要作用是约束,比如说性别,只要男和女,如果我们定义为一个char类型,那么用户想要输入什么就输入什么,极容易造成错误。使用枚举就不一样,一旦性别被定义为枚举类,那么用户输入的时候,只能选择枚举的其中一种类型。

比如,下面这个案例中,性别只能是男或者女,那么用户在输入的时候,只能选择是男或者女。示例代码如下:

package com.zhangdapeng520;

public enum Gender {
    MALE("男")// 前面的枚举对象以逗号结尾
    FEMALE("女"); // 最后一个枚举对象以分号结尾

    private String tag; // 标签

    // 重写了枚举的构造方法,创建枚举对象的时候,则必须遵从这种创建方式
    // 比如:MALE("男")
    Gender(String tag) {
        this.tag = tag;
    }

    // 获取枚举对象的标签
    public String getTag() {
        return tag;
    }
}

创建一个Perosn类,有姓名和性别。其中的性别有String类型改为Gender枚举类型。这样做的好处是,用户之后在传性别的时候,只能选择性别枚举类中枚举的其中一种类型。也就是性别要么是男,要么是女,结果是确定的,不会存在其他数据,这样就避免了数据不一致的错误问题。

示例代码如下:

package com.zhangdapeng520;

public class Person {
    private String name;
    private Gender gender; // 性别是一个枚举类

    public Person() {
    }

    public Person(String name, Gender gender) {
        this.name = name;
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", gender=" + gender +
                '}';
    }

    public String getName() {
        return name;
    }

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

    public Gender getGender() {
        return gender;
    }

    public void setGender(Gender gender) {
        this.gender = gender;
    }
}

然后,我们创建一个测试类,创建一个Person进行测试。我们创建Person的时候,性别只能传入枚举中的固定值。示例代码如下:

package com.zhangdapeng520;

public class EnumTest {
    public static void main(String[] args) {
        // 创建对象,性别使用枚举
        Person person = new Person("张三"Gender.MALE);
        System.out.println(person);

        //我们怎么才能获取到用户的性别的标记
        System.out.println(Gender.MALE.getTag());
    }
}

交通信号灯案例

交通信号灯有红,黄,绿三种颜色,特别适合用枚举类定义。示例代码如下:

package com.zhangdapeng520;


public enum Color {
    RED("红")GREEN("绿")YELLOW("黄");

    private String colorTag;

    Color(String colorTag) {
        this.colorTag = colorTag;
    }

    public String getColorTag() {
        return colorTag;
    }
}

我们可以创建一个测试类进行测试,示例代码如下:

package com.zhangdapeng520;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className ColorTest.java
 * @description
 * @createTime 2022-07-03 16:40:00
 */
public class ColorTest {
    public static void main(String[] args) {
        System.out.println("交通信号灯的颜色:");
        System.out.println(Color.RED.getColorTag());
        System.out.println(Color.GREEN.getColorTag());
        System.out.println(Color.YELLOW.getColorTag());
    }
}

反射学习目标

  • 了解类的加载过程
  • 理解类初始化过程
  • 了解类加载器
  • 掌握获取Class对象的四种方式
  • 能够运用反射获取类型的详细信息
  • 能够运用反射动态创建对象
  • 能够运用反射动态获取成员变量并使用
  • 能够运用反射动态获取成员方法并使用
  • 能够运用反射获取泛型父类的类型参数

类加载(了解)

类在内存中的生命周期:加载–>使用–>卸载

类的加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化,如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。

类的加载又分为三个阶段:

  • 加载:load。就是指将类型的class字节码数据读入内存 。

  • 连接:link

    • 验证:校验合法性等

    • 准备:准备对应的内存(方法区),创建Class对象,为类变量赋默认值,为静态常量赋初始值。

    • 解析:把字节码中的符号引用替换为对应的直接地址引用

  • 初始化:initialize(类初始化)即执行<clinit>类初始化方法,会给类的静态变量赋初始值

类初始化

哪些操作会导致类的初始化?

  • 运行主方法所在的类,要先完成类初始化,再执行main方法

  • 第一次使用某个类型就是在new它的对象,此时这个类没有初始化的话,先完成类初始化再做实例初始化

  • 调用某个类的静态成员(类变量和类方法),此时这个类没有初始化的话,先完成类初始化

  • 子类初始化时,发现它的父类还没有初始化的话,那么先初始化父类

  • 通过反射操作某个类时,如果这个类没有初始化,也会导致该类先初始化

哪些使用类的操作,不会导致类的初始化?

  • 使用某个类的静态的常量(static final)

  • 通过子类调用父类的静态变量,静态方法,只会导致父类初始化,不会导致子类初始化,即只有声明静态成员的类才会初始化

  • 用某个类型声明数组并创建数组对象时,不会导致这个类初始化

类加载器

很多开发人员都遇到过java.lang.ClassNotFoundException或java.lang.NoClassDefError,想要更好的解决这类问题,或者在一些特殊的应用场景,比如需要支持类的动态加载或需要对编译后的字节码文件进行加密解密操作,那么需要你自定义类加载器,因此了解类加载器及其类加载机制也就成了每一个Java开发人员的必备技能之一。

类加载器分为:

  • 引导类加载器(Bootstrap Classloader)又称为根类加载器。它负责加载jre/lib中的核心库,它本身不是Java代码实现的,也不是ClassLoader的子类,获取它的对象时往往返回null。

  • 扩展类加载器(Extension ClassLoader)。它负责加载jre/lib/ext扩展库,它是ClassLoader的子类。

  • 应用程序类加载器(Application Classloader)。它负责加载项目的classpath路径下的类,它是ClassLoader的子类。

  • 自定义类加载器。当你的程序需要加载“特定”目录下的类,可以自定义类加载器。当你的程序的字节码文件需要加密时,那么往往会提供一个自定义类加载器对其进行解码。后面会见到的自定义类加载器:tomcat中。

Java系统类加载器的双亲委托模式

简单描述:下一级的类加载器,如果接到任务时,会先搜索是否加载过,如果没有,会先把任务往上传,如果都没有加载过,一直到根加载器,如果根加载器在它负责的路径下没有找到,会往回传,如果一路回传到最后一级都没有找到,那么会报ClassNotFoundException或NoClassDefError,如果在某一级找到了,就直接返回Class对象。

应用程序类加载器, 把扩展类加载器视为父加载器。扩展类加载器把引导类加载器视为父加载器。不是继承关系,是组合的方式实现的。

java.lang.Class类

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。

要想解剖一个类,必须先要获取到该类的Class对象。而剖析一个类或用反射解决具体的问题就是使用相关API。所以,Class对象是反射的根源。

哪些类型可以获取Class对象

所有Java类型都可以获取到Class对象,示例代码如下:

package com.zhangdapeng520;

import java.lang.annotation.ElementType;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className Demo02GetClass.java
 * @description
 * @createTime 2022-07-03 17:17:00
 */
public class Demo02GetClass {
    public static void main(String[] args) {
        // 基本数据类型和void获取Class对象
        Class<Integer> c1 = int.class;
        System.out.println(c1);

        Class<Void> c2 = void.class;
        System.out.println(c2);

        // 类和接口获取Class对象
        Class<Demo02GetClass> c3 = Demo02GetClass.class;
        System.out.println(c3);

        Class<Comparable> c4 = Comparable.class;
        System.out.println(c4);

        // 枚举类型获取class
        Class<ElementType> c5 = ElementType.class;
        System.out.println(c5);

        // 注解类型获取class
        Class<Override> c6 = Override.class;
        System.out.println(c6);

        // 数组获取class
        Class<int[]> c7 = int[].class;
        System.out.println(c7);
    }
}

获取Class对象的四种方式

通过以下四种方式,都可以获取到Class对象

  • 类型名.class。要求编译期间已知类型。

  • 对象.getClass(),获取对象的运行时类型。

  • Class.forName(类型全名称),通常需要配置文件配置配合使用,可以获取编译期间未知的类型。

  • ClassLoader的类加载器对象.loadClass(类型全名称)。可以用系统类加载对象或自定义加载器对象加载指定路径下的类型。

方法1、通过以下代码获取一个Class对象:

Class<Demo01GetClass> c1 = Demo01GetClass.class;

方法2、通过以下代码获取一个Class对象:

Demo01GetClass d1 = new Demo01GetClass();
Class<? extends Demo01GetClass> c2 = d1.getClass();

方法3、通过以下代码获取一个Class对象:

Class<?> c3 = Class.forName("com.zhangdapeng520.Demo01GetClass");

方法4、通过以下代码获取一个Class对象:

ClassLoader cl = ClassLoader.getSystemClassLoader();
Class<?> c4 = cl.loadClass("com.zhangdapeng520.Demo01GetClass");

完整代码如下:

package com.zhangdapeng520;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className Demo01GetClass.java
 * @description
 * @createTime 2022-07-03 17:10:00
 */
public class Demo01GetClass {
    public static void main(String[] args) throws ClassNotFoundException {
        // 方式1
        Class<Demo01GetClass> c1 = Demo01GetClass.class;
        System.out.println(c1);

        // 方式2
        Demo01GetClass d1 = new Demo01GetClass();
        Class<? extends Demo01GetClass> c2 = d1.getClass();
        System.out.println(c2);

        // 方式3
        Class<?> c3 = Class.forName("com.zhangdapeng520.Demo01GetClass");
        System.out.println(c3);

        // 方式4
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        Class<?> c4 = cl.loadClass("com.zhangdapeng520.Demo01GetClass");
        System.out.println(c4);
    }
}

反射的概念

反射是一种机制/功能,利用该机制/功能可以在程序运行过程中对类进行解剖并操作类中的构造方法,成员方法,成员属性。

反射的应用场景

各种框架的设计(主要场景)

各大框架的内部实现也大量使用到了反射机制,所以要想学好这些框架,则必须要求了解反射机制

反射的应用

获取类型的详细信息

可以获取:包、修饰符、类型名、父类(包括泛型父类)、父接口(包括泛型父接口)、成员(属性、构造器、方法)、注解(类上的、方法上的、属性上的)

获取包信息

Package pkg = clazz.getPackage();

获取修饰符

int mod = clazz.getModifiers();

修饰符定义在Modifier类中,该类里面有很多常量值,每一个常量对应一种修饰符

获取类名

String name = clazz.getName();

获取父类的字节码对象

Class superclass = clazz.getSuperclass();

获取该类实现的所有接口

Class[] interfaces = clazz.getInterfaces();

获取该类的所有属性

Field[] declaredFields = clazz.getDeclaredFields();

获取该类的所有构造函数

Method[] declaredMethods = clazz.getDeclaredMethods();

获取该类的所有方法

Method[] declaredMethods = clazz.getDeclaredMethods();

创建任意引用类型的对象

假设我们有User和Person两个类,能不能实现一个方法,既可以创建User的对象,也可以创建Person的对象呢?答案当然是可以的。使用反射,我们可以轻松的获取到对象的class,然后调用clazz.newInstance()方法创建实例。

创建一个Person类,示例代码如下:

package com.zhangdapeng520;

public class Person implements Hello {
    private int age = 10;
    private String name = "张三";
    public String address = "深圳";

    public Person() {
        System.out.println("执行了无参构造");
    }

    public Person(String name, String address) {
        this.name = name;
        this.address = address;
    }


    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public Person(int age, String name, String address) {
        this.age = age;
        this.name = name;
        this.address = address;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }

    @Override
    public void sayHello() {
        System.out.println("hello world");
    }

    private void study(String course, int day) {
        System.out.println("努力学习:" + course + ",学习" + day + "天");
    }
}

创建一个User类,示例代码如下:

package com.zhangdapeng520;


public class User {
    private String username = "aobama";

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                '}';
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

接着,创建最终的测试类,BeanFactory类。示例代码如下:

package com.zhangdapeng520;

public class BeanFactory {
    public static Object createBean(String className) throws Exception {

        Class clazz = Class.forName(className);
        return clazz.newInstance();
    }

    public static Object createBean(Class clazz) throws Exception {
        return clazz.newInstance();
    }

    public static void main(String[] args) throws Exception {
        // 目标1:创建User
        User user = (User) BeanFactory.createBean("com.zhangdapeng520.User");
        System.out.println(user);

        // 目标2: 创建Person
        Person person = (Person) BeanFactory.createBean("com.zhangdapeng520.Person");
        System.out.println(person);

        // 目标3: 传入字节码对象,创建Person
        Person person2 = (Person) BeanFactory.createBean(Person.class);
        System.out.println(person2);
    }
}

根据Class创建不同对象

在上面的案例中,我们使用全路径的方式创建了一个对象,示例代码如下:

User user = (User) BeanFactory.createBean("com.zhangdapeng520.User");
        System.out.println(user);

还可以通过Class对象,来创建对象,示例代码如下:

public static Object createBean(Class clazz) throws Exception {
        return clazz.newInstance();
    }

我们可以写个案例感受一下。

创建Obj接口,作为User和Person的共同接口。

package com.zhangdapeng520.create_object;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className Obj.java
 * @description
 * @createTime 2022-07-03 19:36:00
 */
public interface Obj {
    void show();
}

创建Person类,实现Obj接口。

package com.zhangdapeng520.create_object;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className Person.java
 * @description
 * @createTime 2022-07-03 19:37:00
 */
public class Person implements Obj {
    @Override
    public void show() {
        System.out.println("Person类对象");
    }
}

创建User类,实现Obj接口。

package com.zhangdapeng520.create_object;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className User.java
 * @description
 * @createTime 2022-07-03 19:36:00
 */
public class User implements Obj {

    @Override
    public void show() {
        System.out.println("User类对象");
    }
}

创建Main测试类,创建获取对象的方法并测试。

package com.zhangdapeng520.create_object;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className Main.java
 * @description
 * @createTime 2022-07-03 19:37:00
 */
public class Main {
    static Obj getObject(Class clazz) throws InstantiationExceptionIllegalAccessException {
        Obj obj = (Obj) clazz.newInstance();
        return obj;
    }

    public static void main(String[] args) throws InstantiationExceptionIllegalAccessException {
        // 获取用户对象
        Obj user = getObject(User.class);
        user.show();

        // 获取Person对象
        Obj person = getObject(Person.class);
        person.show();
    }
}

无参构造方法创建对象

我们调用clazz.newInstance()方法创建对象,本质上是调用类的无参构造方法在创建对象。如果这个类没有无参构造方法,那么会创建失败。

为了验证这个知识点,我们可以写个示例感受一下。我们只需要有一个类,在类的无参构造方法加上一些输出,只需要在newInstance()创建对象的时候,这些输出会生效,即可证明。

创建一个User类,示例代码如下:

package com.zhangdapeng520.newinstance;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className User.java
 * @description
 * @createTime 2022-07-03 19:46:00
 */
public class User {
    public User() {
        System.out.println("调用newinstance时,自动执行了User的无参构造方法");
    }
}

创建一个Main测试类,示例代码如下:

package com.zhangdapeng520.newinstance;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className Main.java
 * @description
 * @createTime 2022-07-03 19:47:00
 */
public class Main {
    public static void main(String[] args) throws InstantiationExceptionIllegalAccessException {
        Class<User> userClass = User.class;
        User user = userClass.newInstance();
        System.out.println(user);
    }
}

有参构造方法创建对象

使用无参构造方法创建对象,往往只是比较理想的情况。更多的时候,我们可能需要使用有参构造方法来创建对象。这在Java中,当然也是很容易实现的。

通过以下方法,我们可以获取到类的构造器:

Constructor<User> userConstructor = userClass.getDeclaredConstructor(String.class);

通过以下方法,我们可以使用构造器对象创建类对象:

User zdp = userConstructor.newInstance("张大鹏");

创建一个User类,示例代码如下:

package com.zhangdapeng520.constructor;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className User.java
 * @description
 * @createTime 2022-07-03 19:50:00
 */
public class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    public void show() {
        System.out.println("我的名字是:" + name);
    }
}

创建一个Main测试类,示例代码如下:

package com.zhangdapeng520.constructor;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className Main.java
 * @description
 * @createTime 2022-07-03 19:51:00
 */
public class Main {
    public static void main(String[] args) throws NoSuchMethodExceptionInvocationTargetExceptionInstantiationExceptionIllegalAccessException {
        Class<User> userClass = User.class;

        // 获取构造器
        Constructor<User> userConstructor = userClass.getDeclaredConstructor(String.class);

        // 使用构造器创建对象
        User zdp = userConstructor.newInstance("张大鹏");

        // 调用对象方法
        zdp.show();
    }
}

操作任意类型的属性

获取该类型的Class对象

Class clazz = Class.forName("com.zhangdapeng520.bean.User");

获取属性对象

Field field = clazz.getDeclaredField("username");

设置属性可访问

field.setAccessible(true);

创建实例对象:如果操作的是非静态属性,需要创建实例对象

Object obj = clazz.newInstance();

设置属性值

field.set(obj,"chai");

获取属性值

Object value = field.get(obj);

如果操作静态变量,那么实例对象可以省略,用null表示,当然一般不会使用反射操作静态变量

我们可以通过反射,创建一个User对象并设置属性,调用方法。示例代码:

package com.zhangdapeng520.property;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className User.java
 * @description
 * @createTime 2022-07-03 19:57:00
 */
public class User {
    private String name;
    private int age;

    public void show() {
        System.out.println("用户名:" + name + ", 年龄:" + age);
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

接着,我们创建一个Main测试类,测试通过反射获取属性和方法,示例代码如下:

package com.zhangdapeng520.property;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className Main.java
 * @description
 * @createTime 2022-07-03 19:58:00
 */
public class Main {
    public static void main(String[] args) throws NoSuchFieldExceptionIllegalAccessExceptionNoSuchMethodExceptionInvocationTargetExceptionInstantiationException {
        // 创建对象
        Class<User> userClass = User.class;
        User user = userClass.newInstance();

        // 获取属性
        Field name = userClass.getDeclaredField("name");

        // 设置属性的可见性
        name.setAccessible(true);

        // 修改属性的值
        name.set(user, "张大鹏");

        // 获取方法
        //Method setName = userClass.getMethod("setName", String.class);
        Method setAge = userClass.getMethod("setAge"int.class);

        // 设置姓名
        //setName.invoke(user, "张大鹏");

        // 设置年龄
        setAge.invoke(user, 22);

        // 获取方法
        Method show = userClass.getMethod("show");

        // 调用方法
        show.invoke(user);
    }
}

调用任意类型的方法

获取该类型的Class对象

Class clazz = Class.forName("com.zhangdapeng520.service.UserService");

获取方法对象

Method method = clazz.getDeclaredMethod("login"String.classString.class);

创建实例对象

Object obj = clazz.newInstance();

调用方法

Object result = method.invoke(obj,"chai","123);

如果方法的权限修饰符修饰的范围不可见,也可以调用setAccessible(true)。如果方法是静态方法,实例对象也可以省略,用null代替。

我们创建一个User类,提供一个静态方法。示例代码如下:

package com.zhangdapeng520.mstatic;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className User.java
 * @description
 * @createTime 2022-07-03 20:09:00
 */
public class User {
    public static void show() {
        System.out.println("User的静态方法调用了。。。");
    }
}

然后,创建一个Main测试类,测试通过反射获取静态方法并调用。示例代码如下:

package com.zhangdapeng520.mstatic;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className Main.java
 * @description
 * @createTime 2022-07-03 20:12:00
 */
public class Main {
    public static void main(String[] args) throws NoSuchMethodExceptionInvocationTargetExceptionIllegalAccessException {
        Class<User> userClass = User.class;

        // 反射静态方法
        Method show = userClass.getMethod("show");
        show.invoke(null); // 直接传null就行
    }
}

Type接口的介绍

java.lang.reflect.Type接口及其相关接口用于描述java中用到的所有类型,是Java的反射中很重要的组成部分。Type 是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。

使用反射获取Type

有很多场景下我们可以获得Type,比如:

  • 当我们拿到一个Class,用Class.getGenericInterfaces()方法得到Type[],也就是这个类实现接口的Type类型列表。

  • 当我们拿到一个Class,用Class.getDeclaredFields()方法得到Field[],也就是类的属性列表,然后用Field。getGenericType()方法得到这个属性的Type类型。

  • 当我们拿到一个Method,用Method.getGenericParameterTypes()方法获得Type[],也就是方法的参数类型列表。

  • 当我们拿到一个Class,用clazz.getGenericSuperclass()这样就可以获取父类的泛型实参列表

Type的分类

Type接口包含了一个实现类(Class)和四个实现接口(TypeVariable, ParameterizedType, GenericArrayType, WildcardType),这四个接口都有自己的实现类,但这些实现类开发都不能直接使用,只能用接口。

当需要描述的类型是普通Java类、数组、自定义类、 8种java基本类型 的时候, java会选择Class来作为这个Type的实现类,我们甚至可以直接把这个Type强行转换类型为Class。这些类基本都有一个特点:基本和泛型无关,其他4种Type的类型,基本都是泛型的各种形态。

当需要描述的类是泛型类时,比如List,Map等,不论代码里写没写具体的泛型,java会选择ParameterizedType接口做为Type的实现。ParameterizedType接口有getActualTypeArguments()方法,用于得到泛型的Type类型数组。

当需要描述的类型是泛型类的数组时,比如比如List[],Map[],type用GenericArrayType接口作为Type的实现。GenericArrayType接口有getGenericComponentType()方法,得到数组的组件类型的Type对象。

当需要描述的类型是泛型类,而且泛型类中的泛型被定义为(? extends xxx)或者(? super xxx)这种类型,比如List<? extends TestReflect>,这个类型首先将由ParameterizedType实现,当调用ParameterizedType的getActualTypeArguments()方法后得到的Type就由WildcardType实现。

获取泛型父类信息

我们创建一个父类和一个子类,用于测试:

// 泛型形参:<T,U>
class Father<TU> {

}

// 泛型实参:<String,Integer>
class Son extends Father<StringInteger> {

}

以下的代码,可以实现通过子类的字节码对象,获取父类的泛型:

Type type = clazz.getGenericSuperclass();

以下的代码,可以获取父类的泛型:

// 强转
ParameterizedType parameterizedType = (ParameterizedType) type;

// 获取类型的泛型
Type[] typeArguments = parameterizedType.getActualTypeArguments();
for (Type typeArgument : typeArguments) {
    System.out.println(typeArgument);
}

完整示例代码如下:

package com.zhangdapeng520;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;


public class TypeTest {
    public static void main(String[] args) {
        // 获取Son类的字节码对象
        Class clazz = Son.class;

        // 通过子类的字节码对象获取父类的泛型
        Type type = clazz.getGenericSuperclass();

        // 强转
        ParameterizedType parameterizedType = (ParameterizedType) type;

        // 获取类型的泛型
        Type[] typeArguments = parameterizedType.getActualTypeArguments();
        for (Type typeArgument : typeArguments) {
            System.out.println(typeArgument);
        }

    }

    // 泛型形参:<T,U>
    class Father<TU> {

    }

    // 泛型实参:<String,Integer>
    class Son extends Father<StringInteger> {

    }
}

动态创建和操作任意类型的数组

在java.lang.reflect包下还提供了一个Array类,Array对象可以代表所有的数组。程序可以通过使用Array类来动态的创建数组,操作数组元素等。

Array类提供了如下几个方法:

public static Object newInstance(Class<?> componentType, int..。dimensions)

创建一个具有指定的组件类型和维度的新数组。

public static void setXxx(Object array,int index,xxx value)

将array数组中[index]元素的值修改为value。此处的Xxx对应8种基本数据类型,如果该属性的类型是引用数据类型,则直接使用set(Object array,int index, Object value)方法。

public static xxx getXxx(Object array,int index,xxx value)

将array数组中[index]元素的值返回。此处的Xxx对应8种基本数据类型,如果该属性的类型是引用数据类型,则直接使用get(Object array,int index)方法。

完整示例代码如下:

package com.zhangdapeng520;

import java.lang.reflect.Array;


public class ArrayTest {
    public static void main(String[] args) {
        // 使用反射操作数组
        // 1。使用反射创建一个String类型的数组,长度是5
        Object array = Array.newInstance(String.class5);

        // 2。往数组中存入数据
        for (int i = 0; i < 5; i++) {
            Array.set(array, i, "value" + i);
        }

        // 使用Array获取数组中的元素
        for (int i = 0; i < 5; i++) {
            System.out.println(Array.get(array, i));
        }
    }
}

注解学习目标

  • 了解注解的概念
  • 了解JDK提供的三种基本注解
  • 掌握自定义注解
  • 掌握元注解
  • 掌握注解解析

什么是注解

注解英文是annotation,是一种代码级别的说明,和类接口平级关系。

相当于一种标记,在程序中加入注解就等于为程序打上某种标记,以后,javac编译器、开发工具和其他程序可以通过反射来了解你的类及各种元素上有无标记,看你的程序有什么标记,就去干相应的事,标记可以加在包、类,属性、方法,方法的参数以及局部变量上定义。

注解的作用

注解主要有三个作用:

  • 1、执行编译期检查。比如@Override,可以检查重写的方法是否符合规范。

  • 2、分析代码。主要用途是替代配置文件。

  • 3、注解开发。在Spring框架里面,以及现在的其他很多框架里面,基本都是基于注解开发的。

JDK提供的三个基本的注解

  1. @Override:描述方法的重写.

  2. @SuppressWarnings:压制警告.

  3. @Deprecated:标记过时

自定义注解语法

语法: @interface 注解名{}

定义一个简单的注解,示例代码如下:

package com.zhangdapeng520;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className Annotation01.java
 * @description
 * @createTime 2022-07-03 23:13:00
 */
public @interface Annotation01 {
}

使用自定义注解

我们编写了自定义注解以后,就需要使用。我们先通过一个简单的案例感受一下自定义注解是如何使用。

我们先创建一个自定义注解MyAnnotation,代码如下:

package com.zhangdapeng520.demo1;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className MyAnnotation.java
 * @description
 * @createTime 2022-07-03 23:20:00
 */
public @interface MyAnnotation {
}

然后,我们创建一个UseAnnotation类,使用我们的自定义注解。代码如下:

package com.zhangdapeng520.demo1;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className UseAnnotation.java
 * @description 使用注解
 * @createTime 2022-07-03 23:20:00
 */
@MyAnnotation // 使用自定义的注解
public class UseAnnotation {
}

注解属性

注解属性可以让注解具备携带存储数据的功能。注解属性主要有以下类型:

  • 基本类型
  • String
  • 枚举类型
  • 注解类型
  • Class类型
  • 以上类型的一维数组类型

**注意:**一旦注解有属性了,使用注解的时候,属性必须有值 。

创建一个Color枚举类型:

package com.zhangdapeng520.demo2;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className Color.java
 * @description
 * @createTime 2022-07-03 23:25:00
 */
public enum Color {
}

创建一个MyAnnotation1注解类型:

package com.zhangdapeng520.demo2;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className MyAnnotation1.java
 * @description
 * @createTime 2022-07-03 23:24:00
 */
public @interface MyAnnotation1 {
}

然后,我们创建一个MyAnnotation注解,使用各种属性类型。示例代码如下:

package com.zhangdapeng520.demo2;


/**
 * @author 张大鹏
 * @version 1.0.0
 * @className MyAnnotation.java
 * @description
 * @createTime 2022-07-03 23:22:00
 */
public @interface MyAnnotation {
    String value(); // 注解的属性写法很特殊,要加括号“()”

    int a();//基本类型

    String b();//String

    Color c();//枚举类型

    MyAnnotation1 d();//注解类型

    Class e();//Class类型  

    String[] f();//一维数组类型
}

使用注解时给属性赋值

格式

@注解名(属性名=值,属性名2=值2)  
@MyAnnotation3(i = 0,s="23")

我们编写代码感受一下如何给注解属性赋值。先创建一个带属性的注解,示例代码如下:

package com.zhangdapeng520.demo3;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className MyAnnotation.java
 * @description
 * @createTime 2022-07-03 23:29:00
 */
public @interface MyAnnotation {
    String value();
}

然后我们创建一个User类,使用该注解并给属性赋值。示例代码如下:

package com.zhangdapeng520.demo3;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className User.java
 * @description
 * @createTime 2022-07-03 23:30:00
 */
@MyAnnotation(value = "user")
public class User {
}

属性赋值的特殊情况

若属性类型的一维数组的时候,当数组的值只有一个的时候可以省略{}

@MyAnnotation4(ss = { "a" })
@MyAnnotation4(ss = "a")

注解属性可以有默认值

属性类型 属性名() default 默认值;

若属性名为value的时候,且只有这一个属性需要赋值的时候可以省略value【重点】

注解属性是一维数组

注解属性是一维数组,但是赋值的时候只有一个元素,那么可以省略数组的大括号。我们写代码感受一下。

首先,我们创建GetController注解,代码如下:

package com.zhangdapeng520.demo4;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className User.java
 * @description
 * @createTime 2022-07-03 23:32:00
 */
@GetController(methods = "GET")
public class User {
}

然后,我们创建UserController,使用该注解。示例代码如下:

package com.zhangdapeng520.demo4;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className User.java
 * @description
 * @createTime 2022-07-03 23:32:00
 */
@GetController(methods = "GET")
public class UserController {
}

注解默认值和value属性

注解的属性可以定义默认值。如果注解属性名叫做value,赋值的时候可以省略。这两个特性,我们也写一段代码感受一下。

首先,我们创建GetController注解,代码如下:

package com.zhangdapeng520.demo4;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className GetController.java
 * @description
 * @createTime 2022-07-03 23:32:00
 */
public @interface GetController {
    String value(); // 注解名字叫value
    
    // 使用了默认值
    String[] methods() default "GET";
}

然后,创建一个UserController,使用该注解:

package com.zhangdapeng520.demo4;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className User.java
 * @description
 * @createTime 2022-07-03 23:32:00
 */

// 属性叫value,可以省略不写
// methods有默认值,可以不赋值
@GetController("user")
public class UserController {
}

元注解

元注解是使用在自定义的注解上,为自定义的注解提供支持的。

常用的元注解:

  • @Target:定义该注解作用在什么上面(位置),默认注解可以在任何位置。值为:ElementType的枚举值

    • METHOD:方法

    • TYPE:类 接口

    • FIELD:字段

    • CONSTRUCTOR:构造方法声明

  • @Retention:定义该注解保留到那个代码阶段, 值为:RetentionPolicy类型,默认只在源码阶段保留

    • SOURCE:只在源码上保留(默认)
    • CLASS:在源码和字节码上保留
    • RUNTIME:在所有的阶段都保留

Java中最开始是SOURCE源码阶段,编译以后是CLASS字节码阶段,加载到内存中运行以后,是RUNTIME运行阶段。

我们可以优化之前的注解GetController,添加上元注解。示例代码如下:

package com.zhangdapeng520.demo4;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className GetController.java
 * @description
 * @createTime 2022-07-03 23:32:00
 */

@Target(value = {ElementType.TYPE}) // 表示能够用在类上
@Retention(RetentionPolicy.RUNTIME) // 在所有阶段都保留
public @interface GetController {
    String value(); // 注解名字叫value

    // 使用了默认值
    String[] methods() default "GET";
}

反射与注解

java.lang.reflect.AnnotatedElement提供了对注解的反射。常用的接口方法如下。

T getAnnotation(Class<T>annotationType)

得到指定类型的注解引用。没有返回null。

boolean isAnnotationPresent(Class<?extends Annotation> annotationType)

判断指定的注解有没有。

Class、Method、Field、Constructor等实现了AnnotatedElement接口。

Annotation[] getAnnotations()

得到所有的注解,包含从父类继承下来的。

Annotation[] getDeclaredAnnotations()

得到自己身上的注解。

我们还是写个案例感受一下反射注解的基本用法。首先,我们创建GetController注解:

package com.zhangdapeng520.demo6;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className GetController.java
 * @description
 * @createTime 2022-07-03 23:32:00
 */
@Retention(RetentionPolicy.RUNTIME)
public @interface GetController {
    String value(); // 注解名字叫value

    // 使用了默认值
    String[] methods() default "GET";
}

然后,创建UserController类,使用注解:

package com.zhangdapeng520.demo6;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className User.java
 * @description
 * @createTime 2022-07-03 23:32:00
 */

// 属性叫value,可以省略不写
// methods有默认值,可以不赋值
@GetController("user")
public class UserController {
}

最后再创建一个Main测试类,通过反射获取UserController的注解,并进行操作。

package com.zhangdapeng520.demo6;

import java.util.Arrays;

/**
 * @author 张大鹏
 * @version 1.0.0
 * @className Main.java
 * @description
 * @createTime 2022-07-03 23:47:00
 */
public class Main {
    public static void main(String[] args) {
        // 获取UserController的字节码对象
        Class<UserController> ucc = UserController.class;

        // 得到UserController的注解对象
        if (ucc.isAnnotationPresent(GetController.class)){
            GetController annotation = ucc.getAnnotation(GetController.class);

            // 获取注解内容
            System.out.println("value值是:" + annotation.value());
            System.out.println("methods值是:" + Arrays.toString(annotation.methods()));
        }
    }
}

资源下载地址

PDF版本电子书下载:

链接:https://pan.baidu.com/s/1htbL8LwTf2oVUi6cld7IjQ
提取码:49vd

源码下载:

链接:https://pan.baidu.com/s/1o5GL_AG6nNPalzhZPu_Xsg
提取码:lxux

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Python私教

创业不易,请打赏支持我一点吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值