跳转至

Java中的Junit、类加载时机与机制、反射、注解及枚举

约 5343 个字 496 行代码 5 张图片 预计阅读时间 24 分钟

Junit

Junit介绍与使用

在Java中,Junit是一个单元测试框架,可以代替main方法去执行其他的方法,其可以单独执行一个方法,测试该方法是否能跑通

因为Junit是一个第三方工具包,与Lombok一样需要先导入对应的jar包并进行解压,不同的是,Junit有两个包,并且相互依赖,所以都需要进行解压。这里可以使用一个文件夹名为lib,将两个解压包放在lib文件下,再对lib文件夹进行整体Add as Library

使用时,在需要进行测试的方法上方写上@Test注解,此时就会在对应方法所在行左侧显示运行按钮,点击即可运行对应的方法而不会执行其他方法

例如下面的代码:

Java
1
2
3
4
5
6
public class Test01 {
    @Test
    public void method() {
        System.out.println("测试Junit");
    }
}

一个类中可以有多个有@Test方法:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Test01 {
    @Test
    public void method() {
        System.out.println("测试Junit");
    }

    @Test
    public void method01() {
        System.out.println("测试Junit-2");
    }
}

如果有多个@Test方法,想要运行所有的@Test方法,就可以点击类名所在行的运行按钮

Junit注意事项

  1. @Test不能修饰静态方法
  2. @Test不能修饰带参数的方法
  3. @Test不能修饰带返回值的方法

例如:

Java
1
2
3
4
5
6
7
8
public class Test01 {
    @Test
    public static void method02() {
        System.out.println("测试Junit-3");
    }
}

// 报错:Method 'method02' annotated with '@Test' should be non-static 

Junit其他注解

  1. @Before:被@Before修饰的方法会在每一个@Test方法执行前执行,但是本身不可以单独执行。特点:有多个@Test修饰的方法时会执行多次
  2. @After:被@Before修饰的方法会在每一个@Test方法执行后执行,但是本身不可以单独执行。特点:有多个@Test修饰的方法时会执行多次
  3. @BeforeClass:被@BeforeClass修饰的方法会在所有@Test方法执行前执行,但是本身不可以单独执行。特点:不论是否有多个@Test修饰的方法,该方法始终只会执行一次,并且被修饰的方法必须为静态方法
  4. @AfterClass:被@AfterClass修饰的方法会在所有@Test方法执行后执行,但是本身不可以单独执行。特点:不论是否有多个@Test修饰的方法,该方法始终只会执行一次,并且被修饰的方法必须为静态方法

例如:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Test01 {
    @Test
    public void method() {
        System.out.println("测试Junit");
    }

    @Test
    public void method01() {
        System.out.println("测试Junit-2");
    }

    @Before
    public void before() {
        System.out.println("before");
    }

    @After
    public void after() {
        System.out.println("after");
    }

    @BeforeClass
    public static void beforeClass() {
        System.out.println("beforeClass");
    }

    @AfterClass
    public static void afterClass() {
        System.out.println("afterClass");
    }
}

输出结果
beforeClass
before
测试Junit
after
before
测试Junit-2
after
afterClass

类加载时机与机制

类加载时机

在Java中,以下五种情况中的一种情况出现,JVM就会将class文件加载进入内存:

  1. 实例化对象
  2. 实例化子类对象或者接口实现类对象(此时创建子类对象会初始化父类)
  3. 执行main方法
  4. 直接使用类名调用静态成员
  5. 使用反射创建class对象

因为class也属于文件,所以JVM也需要使用IO流中的操作将class文件读取到内存,但并不是JVM直接加载,而是使用一个名为ClassLoader的类加载器进行加载

类加载器介绍

Note

下面的类加载器将基于JDK8分析

JVM会使用ClassLoader的类加载器加载类,在Java中,类加载器有以下三种:

  1. BootStrapClassLoader:根类加载器,也称为引导类加载器,主要加载Java中的核心类,例如System类、String类等(在路径jre/lib/rt.jar下)
  2. ExtClassLoader:扩展类加载器,负责jre的扩展目录中的jar包的加载(在路径jdk\jre\lib\ext下)
  3. AppClassLoader:系统类加载器,负责在JVM启动时加载来自Java命令的class文件(可以理解为自定义类),以及classPath环境变量所指定的jar包(可以理解为第三方jar包)

在Java中,三种不同的类加载器会加载不同的类,而其三者的关系「从类加载机制层面」如下:

Text Only
1
2
AppClassLoader`的父类加载器是`ExtClassLoader
ExtClassLoader`的父类加载器是`BootStrapClassLoader

Note

需要注意,他们从代码级别上来看,没有子父类继承关系,但是他们都有一个共同的父类,即ClassLoader

并且BootStrapClassLoader是无法直接获取到的,因为其底层是C语言编写的

Java
1
2
3
4
5
6
7
8
// AppClassLoader类声明
public class AppletClassLoader extends URLClassLoader
// ExtClassLoader类声明
static class ExtClassLoader extends URLClassLoader

// 拥有共同的父类
public class URLClassLoader extends SecureClassLoader implements Closeable
public class SecureClassLoader extends ClassLoader

获取类加载器对象

在Java代码中,可以通过下面的两个方法获取类加载器对象:

  1. Class对象中的方法:getClassLoader(),使用时:类名.class.getClassLoader(),作用是获取某一个使用的类加载器
  2. ClassLoader类中的方法:getParent(),使用时:ClassLoader对象.getParent(),作用是获取指定类加载器的父类加载器
Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Test {
    public static void main(String[] args) {
        // 自定义类
        ClassLoader classLoader = Test.class.getClassLoader();
        System.out.println(classLoader);
        // 第三方类
        ClassLoader classLoader1 = IOUtil.class.getClassLoader();
        System.out.println(classLoader1);
        // 获取二者的父类
        System.out.println(classLoader.getParent());
        if (classLoader1 != null) {
            System.out.println(classLoader1.getParent());
        } else {
            System.out.println("父类加载器无法获取");
        }
        // 系统类
        ClassLoader classLoader2 = Scanner.class.getClassLoader();
        System.out.println(classLoader2);
    }
}

输出结果
sun.misc.Launcher$AppClassLoader@18b4aac2
null
sun.misc.Launcher$ExtClassLoader@4554617c
父类加载器无法获取
null

双亲委派机制和缓存机制

双亲委派机制,也称为全盘负责委托机制,表示类加载器在加载类时,首先不会自己去尝试加载这个类,而是把请求交给父类加载器来完成,每一层的类加载器都会遵循这样的规则。

类加载器的缓存机制:一个类加载到内存之后,缓存中也会保存一份儿,后面如果再使用此类,如果缓存中保存了这个类,就直接返回他;如果没有才加载这个类,下一次如果有其他类在使用的时候就不会重新加载了,直接去缓存中拿

类加载器加载类的过程图如下:

图解析:

一个类准备加载进内存时,首先遇到的类加载器就是AppClassLoader,如果AppClassLoader中的缓存已经存在该类,则直接返回该类,否则向上找类加载器ExtClassLoader,同样,如果ExtClassLoader的缓存已经存在该类,则直接返回,否则向上找类加载器BootStrapClassLoader,如果BootStrapClassLoader缓存依旧没有该类,则判断类是否是该类加载需要加载的,不是继续向下直到遇到属于加载该类的类加载器

所以,类加载器的双亲委派和缓存机制共同造就了加载类的特点:保证了类在内存中的唯一性

反射

在Java中,反射的作用是获取类对象,通过这个类对象获取对应类中的成员属性(重新赋值)、成员方法(调用方法)和构造方法(实例化对象)

类对象:在Java中,一切皆是对象,而class文件加载进内存就会生成对应的对象,该对象就被称为class对象

Class类:描述Class对象就是Class

同样,对于成员变量、成员方法和构造方法来说也有对应的对象和类:

  1. 成员变量:对应的成员变量对象为Field对象,描述Field对象的类称为Field
  2. 成员方法:对应的成员方法对象为Method对象,描述Method对象的类称为Method
  3. 构造方法:对应的构造方法对象为Constructor对象,描述Constructor对象的类称为Constructor

获取类对象

获取类对象是反射成立的第一步,而一共有三种方式获取类对象:

  1. 调用Object中的方法:Class <?> getClass(),该方法返回一个类对象
  2. 通过class成员获取类对象:基本数据类型/引用数据类型.class
  3. Class类中的静态方法:static Class<?> forName(String className),该方法的参数为类的全限定名,该方法返回一个类对象

Note

类的全限定名即为类所在的包名,即package后面的内容+指定类名

例如,下面的代码:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Test {
    public static void main(String[] args) throws Exception{
        // 1. 调用Object中的方法:Class <?> getClass(),该方法返回一个类对象
        Scanner scanner = new Scanner(System.in);
        Class<? extends Scanner> aClass = scanner.getClass();
        System.out.println(aClass);
        // 2. 通过class成员获取类对象:基本数据类型/引用数据类型.class
        Class<Scanner> scannerClass = Scanner.class;
        System.out.println(scannerClass);
        // 3. Class类中的静态方法:static Class<?> forName(String className),该方法的参数为类的全限定名,该方法返回一个类对象
        Class<?> aClass1 = Class.forName("com.epsda.advanced.test_reflect.Test");
        System.out.println(aClass1);
    }
}

输出结果
class java.util.Scanner
class java.util.Scanner
class com.epsda.advanced.test_reflect.Test

在IDEA中获取类的全限定名:

  1. 右键需要全限定名的类->选择Copy Path/Reference...->选择Copy Reference
  2. forName方法中输入需要全限定名的类名,按下Tab或者Enter

Note

如果按住 Ctrl +鼠标左键点击类的全限定名可以跳转到指定类时,说明全限定名正确

在实际开发中最常用的获取类对象的方式是第二种,但是最通用的方式是第三种,因为第三种的参数是String类型,后面可以结合配置文件xxx.propertiesProperties集合中的load方法加载类的全限定名

获取类对象的构造方法

获取类对象的构造方法一共有四种方法:

  1. 获取类对象中所有public构造方法:使用Class类中的方法:Constructor<?>[] getConstructors(),该方法返回一个构造方法类的对象
  2. 获取类对象中指定的public构造方法:Class类中的方法:Constructor<T> getConstructor (Class<?>... parameterTypes),该方法的参数为指定的public构造方法参数类型对应的类对象,返回一个构造方法类的对象。如果指定的public构造方法没有参数,则可以不传递任何内容
  3. 获取类对象中所有构造方法(包括publicprivate):Constructor<?>[] getDeclaredConstructors(),该方法返回一个构造方法类的对象
  4. 获取类对象中指定的构造方法(包括publicprivate):Constructor<T> getDeclaredConstructor (Class<?>... parameterTypes),该方法的参数为指定的构造方法的参数,返回一个构造方法类的对象。如果指定的构造方法没有参数,则可以不传递任何内容

下面示例使用的类:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    private String name;
    public Integer age;

    private Person(String name) {
        this.name = name;
    }
}

使用如下:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test01 {
    public static void main(String[] args) throws Exception{
        Class<Person> personClass = Person.class;

        // 获取所有public构造方法
        Constructor<?>[] constructors = personClass.getConstructors();
        for (Constructor<?> constructor : constructors) {
            System.out.println(constructor);
        }

        // 获取指定public构造方法
        Constructor<Person> constructor = personClass.getConstructor(String.class, Integer.class);
        System.out.println(constructor);

        // 获取所有构造方法
        Constructor<?>[] declaredConstructors = personClass.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println(declaredConstructor);
        }

        // 获取指定的构造方法
        Constructor<Person> declaredConstructor = personClass.getDeclaredConstructor(String.class);
        System.out.println(declaredConstructor);
    }
}

使用反射获取的构造方法创建对象

使用Constructor类中的方法: T newInstance(Object...initargs),参数传递对应对象初始值,如果获取到的是无参构造,则参数不传递,否则传递对应的值,该方法返回一个对应类的对象

Java
1
2
3
4
5
6
7
8
public class Test02 {
    public static void main(String[] args) throws Exception{
        Class<Person> personClass = Person.class;
        Constructor<Person> declaredConstructor = personClass.getDeclaredConstructor(String.class, Integer.class);
        Person person = declaredConstructor.newInstance("张三", 16);
        System.out.println(person);
    }
}

如果获取到的是无参构造,则可以直接使用类对象.newInstance(),简写为如下的代码:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Test03 {
    public static void main(String[] args) throws Exception{
        // 无参构造默认反射方式
        Class<Person> personClass = Person.class;
        Constructor<Person> constructor = personClass.getConstructor();
        Person person = constructor.newInstance();
        System.out.println(person);

        // 简写为
        Person person1 = personClass.newInstance();
        System.out.println(person);
    }
}

Note

但是,上面的简写形式已经被弃用(修饰为@Deprecated),不过依旧可以使用

如果获取到的是私有构造方法,则需要使用Constructor的父类AccessibleObject中的方法void setAccessible(boolean flag)将私有构造方法的权限修改为public,参数有两个值:true代表修改,false表示不修改

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Test04 {
    public static void main(String[] args) throws Exception{
        Class<Person> personClass = Person.class;
        Constructor<Person> personConstructor = personClass.getDeclaredConstructor(String.class);
        // 修改权限
        personConstructor.setAccessible(true);
        Person person = personConstructor.newInstance("张三");
        System.out.println(person);
    }
}

上面获取私有构造方法并创建对象也被称为暴力反射

获取类对象的成员方法

获取类对象的成员方法一共有四种方法:

  1. 获取类对象中所有public成员方法:使用Class类中的方法:Method[] getMethods(),该方法返回一个成员方法类的对象
  2. 获取类对象中指定的public成员方法:Class类中的方法:Method getMethod (String name, Class<?>... parameterTypes),该方法的第一个参数为指定的public成员方法名,第二个参数为指定的public成员方法参数类型对应的类对象,返回一个成员方法类的对象。如果指定的public成员方法没有参数,则第二个参数可以不传递任何内容
  3. 获取类对象中所有成员方法(包括publicprivateprotected):Method[] getDeclaredMethods(),该方法返回一个成员方法类的对象
  4. 获取类对象中指定的成员方法(包括publicprivate):Method getDeclaredMethod (String name, Class<?>... parameterTypes),该方法的参数为指定的成员方法的参数,返回一个成员方法类的对象。如果指定的public成员方法没有参数,则第二个参数可以不传递任何内容

Note

需要注意,getMethods方法会获取到本类和其父类的所有public方法,但是getDeclaredMethods方法只会获取到本类中的privatepublicprotected方法

例如下面的代码:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test05 {
    public static void main(String[] args) throws Exception{
        Class<Person> personClass = Person.class;
        // 获取所有public方法
        Method[] methods = personClass.getMethods();
        for (Method method : methods) {
            System.out.println(method);
        }

        // 获取指定的public方法
        Method method = personClass.getMethod("setName", String.class);
        System.out.println(method);

        // 获取所有的方法
        Method[] declaredMethods = personClass.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            System.out.println(declaredMethod);
        }

        // 获取指定的方法
        Method walk = personClass.getDeclaredMethod("walk");
        System.out.println(walk);
    }
}

使用反射获取的成员方法

使用成员方法类对象调用Object类中的方法Object invoke(Object obj, Object... args)可以使用获取到的成员方法,第一个参数传递成员方法所在类的对象,第二个参数传递获取到的成员方法参数对应的值,该方法返回一个Object对象。该方法的返回值根据调用对象对应的方法是否有返回值决定,如果调用对象对应的方法有返回值,则与调用对象对应的方法的值相同,否则为null

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Test06 {
    public static void main(String[] args) throws Exception{
        Class<Person> personClass = Person.class;
        Person person = personClass.newInstance();
        Method setName = personClass.getDeclaredMethod("setName", String.class);
        Method getName = personClass.getDeclaredMethod("getName");
        Object set = setName.invoke(person, "张三");
        Object get = getName.invoke(person);
        System.out.println(get);
    }
}

如果需要操作类对象中的私有方法,与私有构造方法一样,需要使用AccessibleObject中的方法void setAccessible(boolean flag)将私有构造方法的权限修改为public

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Test06 {
    public static void main(String[] args) throws Exception{
        Class<Person> personClass = Person.class;
        Person person = personClass.newInstance();

        Method declaredMethod = personClass.getDeclaredMethod("walk");
        declaredMethod.setAccessible(true);
        declaredMethod.invoke(person);
    }
}

获取类对象的成员属性

获取类对象的构造方法一共有四种方法:

  1. 获取类对象中所有public成员属性:使用Class类中的方法:Field[] getFields(),该方法返回一个成员属性类的对象
  2. 获取类对象中指定的public成员属性:Class类中的方法:Field getField(String name),该方法的参数为指定的public成员属性名,返回一个成员属性类的对象
  3. 获取类对象中所有成员属性(包括publicprivate):Field[] getDeclaredFields(),该方法返回一个成员属性类的对象
  4. 获取类对象中指定的成员属性(包括publicprivateprotected):Field getDeclaredField(String name),该方法的参数为指定的public成员属性名,返回一个成员属性类的对象

例如:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test07 {
    public static void main(String[] args) throws Exception{
        Class<Person> personClass = Person.class;

        // 获取所有public成员属性
        Field[] fields = personClass.getFields();
        for (Field field : fields) {
            System.out.println(field);
        }

        // 获取指定的public成员属性
        Field age = personClass.getField("age");
        System.out.println(age);

        // 获取所有成员属性
        Field[] declaredFields = personClass.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            System.out.println(declaredField);
        }

        // 获取指定成员属性
        Field declaredField = personClass.getDeclaredField("name");
        System.out.println(declaredField);
    }
}

使用反射获取的成员属性

使用成员属性类对象调用Object中的方法:void set(Object obj, Object value)为获取到的成员属性赋值,第一个参数传递成员属性所在类的对象,第二个参数传递属性值

使用成员属性类对象调用Object中的方法:Object get(Object obj)获取指定成员属性的值,参数传递成员属性所在类的对象,该方法返回一个Object对象,该对象中的值即为成员属性的值

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Test08 {
    public static void main(String[] args) throws Exception{
        Class<Person> personClass = Person.class;
        Person person = personClass.newInstance();
        Field age = personClass.getField("age");
        age.set(person, 18);
        Object o = age.get(person);
        System.out.println(o);
    }
}

同样,如果是私有成员,则需要使用AccessibleObject中的方法void setAccessible(boolean flag)将私有构造方法的权限修改为public

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Test08 {
    public static void main(String[] args) throws Exception{
        Class<Person> personClass = Person.class;
        Person person = personClass.newInstance();

        Field declaredField = personClass.getDeclaredField("name");
        declaredField.setAccessible(true);
        declaredField.set(person, "张三");
        Object o1 = declaredField.get(person);
        System.out.println(o1);
    }
}

反射实用案例

在配置文件中,配置类的全限定名,以及配置一个方法名,通过解析配置文件,让配置好的方法执行起来,配置文件的内容如下:

Properties
1
2
className=包名.Person
methodName=walk

步骤:

  1. 创建properties配置文件,配置信息,需要注意,这个配置文件不能直接放到模块或者项目下,否则在out文件夹中不存在该文件,最常见的做法是在指定目录下创建一个名为resources文件夹,并将这个文件夹标记为Resources Root,然后将配置文件放在这个文件夹内部
  2. 读取配置文件,解析配置文件。读取配置文件可以使用properties集合中的load方法,解析配置文件时不建议直接在创建IO流对象时传递properties文件的地址,这样导致该地址为死地址,而且out文件夹下不会存在resources文件夹。推荐方法:因为配置文件也属于文件,在Java中加载该文件时会产生对应的对象,使用ClassLoader获取当前类的类加载器对象,再使用该对象调用getResourceAsStream ("配置文件名")方法获取InputStream对象。这种方式会自动扫描resources下的文件,可以简单理解为扫描out路径下的配置文件
  3. 根据解析出来的className,创建Class对象
  4. 根据解析出来的methodName,获取对应的方法
  5. 执行方法

IDEA中「将这个文件夹标记为Resources Root」的步骤图:

如果上面的方式中没有显示Resources Root,则可以考虑下面的步骤:

参考代码如下:

Properties
1
2
3
// 配置文件
className=com.epsda.advanced.test_reflect_exercise.Person
methodName=walk

测试:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 自定义类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    private String name;
    private Integer age;

    public void walk() {
        System.out.println("人在行走");
    }
}

// 测试
package com.epsda.advanced.test_reflect_exercise;

import org.junit.Test;

import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Properties;

/**
 * ClassName: Test09
 * Description: 测试
 *
 * @author 憨八嘎
 * @version 1.0
 */
public class Test09 {
    @Test
    public void method () throws Exception{
        ClassLoader classLoader = Test09.class.getClassLoader();
        InputStream resourceAsStream = classLoader.getResourceAsStream("test.properties");
        // 读取配置文件
        Properties properties = new Properties();
        properties.load(resourceAsStream);
        // 根据key取出其中的值
        String methodName = properties.getProperty("methodName");
        String className = properties.getProperty("className");
        // 创建Class对象
        Class<?> aClass = Class.forName(className);
        Object o = aClass.newInstance();
        Method declaredMethod = aClass.getDeclaredMethod(methodName);
        declaredMethod.invoke((Person)o);
    }
}

输出结果
人在行走

目录结构:

注解

介绍

在Java中,注解也是一种引用数据类型,与类、接口、枚举和Record类同层次

注解常见的作用如下:

  1. 说明:对代码进行说明,生成doc文档(API文档)
  2. 检查:检查代码是否符合条件,例如:@Override@FunctionalInterface
  3. 分析:对代码进行分析,起到了代替配置文件的作用

JDK中常见的注解:

  1. @Override:检测此方法是否为重写方法
    • JDK5版本,支持父类的方法重写
    • JDK6版本,支持接口的方法重写
  2. @Deprecated:表示方法已经过时,不推荐使用,但是依旧可以使用
  3. @SuppressWarnings:消除警告,例如消除所有警告:@SuppressWarnings("all")

在IDEA中,一般被警告的方法默认会有黄色底色,例如下图:

定义注解和属性

在Java中,可以使用下面的格式定义注解:

Java
1
2
3
public @Interface 注解名 {
    // 属性
}

在注解体内的属性,本质是抽象方法,但是在使用时,与成员属性相同,使用成员属性名=值的方式

属性的定义有两种方式:

  1. 数据类型 属性名():定义一个没有默认值的属性,使用注解时就必须赋值
  2. 数据类型 属性名() default 值:定义一个有默认值的属性,使用注解时可以不需要赋值

可以作为属性的类型:

  1. 所有基本数据类型
  2. String类型
  3. 枚举类型
  4. 注解类型
  5. Class类型
  6. 上面所有类型的一维数组(不可以是二维数组)

例如:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public @interface Book {
    //书名
    String bookName();
    //作者
    String[] author();
    //价格
    int price();
    //数量
    int count() default 10;
}

注解的使用

使用注解本质就是为每一个属性(抽象方法)赋值,一般使用位置有下面几种:

  1. 类名上
  2. 方法上
  3. 成员变量上
  4. 局部变量上
  5. 参数位置

使用格式如下:

  1. 普通属性:@注解名(属性名 = 值, 属性名 = 值...)
  2. 属性中有数组:@注解名(属性名 = {元素1,元素2...})

注解使用时需要注意:

  1. 空注解(注解中没有任何的属性)可以直接使用
  2. 不同的位置可以使用一样的注解,但是同样的位置不能使用一样的注解
  3. 使用注解时,如果此注解中有属性没有默认值,则注解中对应的属性一定要赋值。如果有多个属性,用,隔开;如果注解中的属性值有默认值,那么不用显示写,也不用重新赋值
  4. 如果注解中的属性有数组,使用{}
  5. 如果注解中只有一个属性,并且属性名叫value,那么使用注解的时候,属性名不用写,直接写值(包括普通类型和数组)

例如:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 自定义注解
public @interface Book {
    //书名
    String bookName();
    //作者
    String[] author();
    //价格
    int price();
    //数量
    int count() default 10;
}

// 测试
@Book(bookName = "寓言故事",author = {"张三","李四"},price = 10,count = 20)
public class BookShelf {
}

解析注解

解析注解即为取出注解对应属性的值,注解涉及的接口是:AnnotatedElement接口,实现类有: AccessibleObjectClassConstructorExecutableFieldMethodPackageParameter

解析思路如下:

  1. 判断指定位置上有没有使用指定的注解:使用方法:boolean isAnnotationPresent(Class<? extends Annotation> annotationClass),如果存在注解则返回为true,否则返回false
  2. 如果有,则获取指定的注解:使用方法:getAnnotation(Class<T> annotationClass)
  3. 通过注解获取指定的值

例如:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 自定义注解
public @interface Book {
    //书名
    String bookName();
    //作者
    String[] author();
    //价格
    int price();
    //数量
    int count() default 10;
}

// 自定义类
@Book(bookName = "寓言故事",author = {"张三","李四"},price = 10,count = 20)
public class BookShelf {
}

// 测试
public class Test01 {
    public static void main(String[] args) {
        //1.获取BookShelf的class对象
        Class<BookShelf> bookShelfClass = BookShelf.class;
        //2.判断bookShelf上有没有Book注解
        boolean b = bookShelfClass.isAnnotationPresent(Book.class);
        //3.判断,如果b为true就获取
        if (b) {
            Book book = bookShelfClass.getAnnotation(Book.class);
            System.out.println(book.bookName());
            System.out.println(Arrays.toString(book.author()));
            System.out.println(book.price());
            System.out.println(book.count());
        }
    }
}

上面的代码没有在控制台中打印运行结果,原因是注解并没有在内存中出现,而class文件在内存中运行,所以导致方法无法获取到对应的注解

元注解

元注解也是注解,这个注解用来管理其他注解,一般管理下面的方面:

  1. 控制注解的使用位置
    1. 控制注解是否能在类上使用
    2. 控制注解是否能在方法上使用
    3. 控制注解是否能在构造上使用等
  2. 控制注解的生命周期(加载位置)
    • 控制注解是否能在源码中出现
    • 控制注解是否能在class文件中出现
    • 控制注解是否能在内存中出现

使用元注解:

  1. 注解@Target:控制注解的使用位置,其属性是个枚举数组ElementType[] value();,枚举的成员可以类名直接调用。常见的成员有:
    • TYPE:控制注解能使用在类上
    • FIELD:控制注解能使用在属性上
    • METHOD:控制注解能使用在方法上
    • PARAMETER:控制注解能使用在参数上
    • CONSTRUCTOR:控制注解能使用在构造上
    • LOCAL_VARIABLE:控制注解能使用在局部变量上
  2. 注解@Retention:控制注解的生命周期(加载位置),其属性是一个枚举对象RetentionPolicy,常见的成员有:
    • SOURCE:控制注解能在源码中出现(默认)
    • CLASS:控制注解能在class文件中出现
    • RUNTIME:控制注解能在内存中出现

Note

需要注意,@Target如果不指定属性,默认是全部可用

使用元注解就可以解决前面解析注解部分无法读取到注解的问题:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Book {
    //书名
    String bookName();
    //作者
    String[] author();
    //价格
    int price();
    //数量
    int count() default 10;
}

// 测试
public class Test01 {
    public static void main(String[] args) {
        //1.获取BookShelf的class对象
        Class<BookShelf> bookShelfClass = BookShelf.class;
        //2.判断bookShelf上有没有Book注解
        boolean b = bookShelfClass.isAnnotationPresent(Book.class);
        //3.判断,如果b为true,就获取
        if (b){
            Book book = bookShelfClass.getAnnotation(Book.class);
            System.out.println(book.bookName());
            System.out.println(Arrays.toString(book.author()));
            System.out.println(book.price());
            System.out.println(book.count());
        }
    }
}

输出结果
寓言故事
[张三, 李四]
10
20

枚举

基本使用

枚举属于五大引用数据类型中的一种:类、数组、接口、注解、枚举

定义枚举格式如下:

Java
1
2
3
public enum 枚举类名{

}

在Java中,所有枚举的父类都是Enum

枚举的特点如下:

  1. 每一个枚举都是static final,但是定义枚举是不能显示写出
  2. 每一个枚举由逗号分隔
  3. 写完所有的枚举值之后,最后一个枚举后方需要加;
  4. 枚举值名字最好大写
  5. 使用时使用枚举类名直接调用枚举成员
  6. 枚举中的成员都是当前枚举类类型的对象
  7. 枚举类中的构造方法都是private修饰

基本使用如下:

Java
1
2
3
4
5
public enum Status {
    RUNNING,
    WAITING,
    STOPPED;
}

如果想为每一个枚举赋值,可以在枚举类中定义成员和构造方法,并对外提供获取方法就可以获取到每一个枚举值对应的值

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public enum Status {
    RUNNING("运行"),
    WAITING("等待"),
    STOPPED("暂停");

    private String name;

    private Status(String name) {
        this.name = name;
    }
}

测试如下:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Test {
    public static void main(String[] args) {
        System.out.println(Status.RUNNING);
        System.out.println(Status.RUNNING.getName());
    }
}

输出结果
RUNNING
运行

枚举中的常用方法

方法名 说明
String toString() 返回枚举值的名字
values() 返回所有与的枚举值
valueOf(String str) 将一个字符串转成枚举类型

例如下面的代码:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 枚举
public enum Status {
    RUNNING("运行"),
    WAITING("等待"),
    STOPPED("暂停");

    private String name;

    private Status(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

// 测试
public class Test01 {
    public static void main(String[] args) {
        Status status = Status.RUNNING;

        System.out.println(status.toString());
        System.out.println(status);

        Status[] values = Status.values();
        for (Status value : values) {
            System.out.println(value);
        }

        Status status1 = Status.valueOf("RUNNING");
        System.out.println(status1);
    }
}