所有知识体系文章,GitHub已收录,欢迎Star!再次感谢,愿你早日进入大厂!

GitHub地址: https://github.com/Ziphtracks/JavaLearningmanual

Java注解

一、Java注解概述

二、注解的作用分类

编写文档

代码分析

编译检查

三、jdk的内置注解

3.1 内置注解分类

3.2 @Override注解

这里解释一下@Override注解,在我们的Object基类中有一个方法是toString方法,我们通常在实体类中去重写此方法来达到打印对象信息的效果,这时候也会发现重写的toString方法上方就有一个@Override注解。如下所示:

于是,我们试图去改变重写后的toString方法名称,将方法名改为toStrings。你会发现在编译期就报错了!如下所示:

那么这说明什么呢?这就说明该方法不是我们重写其父类(Object)的方法。这就是@Override注解的作用。

3.3 @Deprecated注解

我们解释@Deprecated注解就需要模拟一种场景了。假设我们公司的产品,目前是V1.0版本,它为用户提供了show1方法的功能。这时候我们为产品的show1方法的功能又进行了扩展,打算发布V2.0版本。但是,我们V1.0版本的产品需要抛弃吗?也就是说我们V1.0的产品功能还继续让用户使用吗?答案肯定是不能抛弃的,因为有一部分用户是一直用V1.0版本的。如果抛弃了该版本会损失很多的用户量,所以我们不能抛弃该版本。这时候,我们对功能进行了扩展后,发布了V2.0版本,我们给予用户的通知就可以了,也就是告知用户我们在V2.0版本中为功能进行了扩展。可以让用户自行选择版本。

但是,除了发布告知用户版本情况之外,我们还需要在原来版本的功能上给予提示,在上面的模拟场景中我们需要在show1方法上方加@Deprecated注解给予提示。通过这种方式也告知用户“这是旧版本时候的功能了,我们不建议再继续使用旧版本的功能”,这句话的意思也就正是给用户做了提示。用户也会这么想“奥,这版本的这个功能不好用了,肯定有新版本,又更好用的功能。我要去官网查一下下载新版本”,还会有用户这么想“我明白了,又更新出更好的功能了,但是这个版本的功能我已经够用了,不需要重新下载新版本了”。

那么我们怎么查看我上述所说的在功能上给予的提示呢?这时候我需要去创建一个方法,然后去调用show1方法,并查看调用时它是如何提示的。

图已经贴出来了,你是否发现的新旧版本功能的异同点呢?很明显,在方法中的提示是在调用的方法名上加了一道横线把该方法划掉了。这就体现了show1方法过时了,已经不建议使用了,我们为你提供了更好的。

回想起来,在我们的api中也会有方法是过时的,比如我们的Date日期类中的方法有很多都已经过时了。如下图:

如你所见,是不是有很多方法都过时了呢?那它的方法上是加了@Deprecated注解吗?来跟着我的脚步,我带你们看一下。

我们已经知道的Date类中的这些方法已经是过时的了,如果我们使用该方法并执行该程序的话。执行的过程中就会提示该方法已过时的内容,但是只是提示,并不影响你使用该方法。如下:

OK!这也就是@Deprecated注解的作用了。

3.4 @SuppressWarnings注解

压制警告注解,顾名思义就是压制警告的出现。我们都知道,在Java代码的编写过程中,是有很多黄色警告出现的。但是我不知道你的导师是否教过你,程序员只需要处理红色的error,不需要理会黄色的warning。如果你的导师说过此问题,那是有原因的。因为在你学习阶段,我们认清处理红色的error即可,这样可以减轻你学习阶段在脑部的记忆内容。如果你刚刚加入学习Java的队列中,需要大脑记忆的东西就有太多了,也就是我们目前不需要额外记忆其他的东西,只记忆重点即可。至于黄色warning嘛,在你的学习过程中慢慢就会有所了解的,而不是死记硬背的。

那为了解释@SuppressWarnings注解,我们还使用上一个例子,因为在那个例子中就有黄色的warning出现。

而每一个黄色的warning都会有警告信息的。比如,这一个图中的警告信息,就告知你show2()方法没有被使用,简单来说,你创建的show2方法,但是你在代码中并没有调用过此方法。以后你便会遇到各种各样黄色的warning。然后, 我们就可以使用不同的注解参数来压制不同的注解。但是在该注解的参数中,提供了一个all参数可以压制全部类型的警告。而这个注解是需要加到类的上方,并赋予all参数,即可压制所有警告。如下:

我们加入注解并赋予all参数后,你会发现use方法和show2方法的警告没有了,实际上导Date包的警告还在,因为我们Date包导入到了该类中,但是我们并没有创建Date对象,也就是并没有写入Date在代码中,你也会发现那一行是灰色的,也就证明了我们没有去使用导入这个包的任何信息的说法,出现这种情况我们就需要把这个没有用的导包内容删除掉,使用Ctrl + X删除导入没有用到的包即可。还有一种办法就是在包的上方修饰压制警告注解,但是我认为在一个没有用的包上加压制注解是毫无意义的,所以,我们直接删除就好。

然后,我们还见到上图,注解那一行出现了警告信息提示。这一行的意思是冗余的警告压制。这就是说我们压制以下的警告并没有什么意义而造成的冗余,但是如果我们使用了该类并做了点什么的话,压制注解的冗余警告就会消失,毕竟我们使用了该类,此时就不会早场冗余了。

上述解释@SuppressWarnings注解也差不多就这些了。OK,继续向下看吧。持续为大家讲解。

3.5 @Repeatable注解

首先,我们先创建一个可以重复使用的注解。

package com.mylifes1110.anno;

import java.lang.annotation.Repeatable;

@Repeatable(Hour.class)
public @interface Hours {
    double[] hours() default 0;
}

你会发现注解要求传入的值是一个类对象,此类对象就需要传入另外一个注解,这里也就是另外一个注解容器的类对象。我们去创建一下。

package com.mylifes1110.anno;

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

//容器
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Hour {
    Hours[] value();
}

其实,这两个注解的套用,就是将一个普通的注解封装了一个可重复使用的注解,来达到注解的复用性。最后,我们创建一下测试类,随后带你去看一下源码。

package com.mylifes1110.java;

import com.mylifes1110.anno.Hours;

@Hours(hours = 4)
@Hours(hours = 4.5)
@Hours(hours = 2)
public class Worker {
    public static void main(String[] args) {
        //通过Hours注解类型来获取Worker中的值数组对象
        Hours[] hours = Worker.class.getAnnotationsByType(Hours.class);
        //遍历数组
        for (Hours h : hours) {
            System.out.println(h);
        }
    }
}

测试类,是一个工人测试类,该工人使用注解记录早中晚的工作时间。测试结果如下:

然后我们进入到源码一探究竟。

我们发现进入到源码后,就只看见一个返回值为类对象的抽象方法。这也就验证了该注解只是一个可实现重复性注解的语法糖而已。

四、注解分类

4.1 注解分类

4.2 标记注解

4.3 单值注解

4.4 完整注解

五、自定义注解

5.1 自定义注解格式

如下,这就是一个注解。但是它与jdk自定义注解有点区别,jdk自定义注解的上方还有注解来修饰该注解,而那注解就叫做元注解。元注解我会在后面详细的说到。

这里我们的确不知道@Interface是什么,那我们就把自定义的这个注解反编译一下,看一下反编译信息。反编译操作如下:

反编译后的反编译内容如下:

public interface com.mylifes1110.anno.MyAnno extends java.lang.annotation.Annotation {
}

首先,看过反编译内容后,我们可以直观的得知他是一个接口,因为它的public修饰符后面的关键字是interface。

其次,我们发现MyAnno这个接口是继承了java.lang.annotation包下的Annotation接口。

所以,我们可以得知注解的本质就是一个接口,该接口默认继承了Annotation接口。

既然,是继承的Annotation接口,那我们就去进入到这个接口中,看它定义了什么。以下是我抽取出来的接口内容。我们发现它看似很常见,其实它们不是很常用,作为了解即可。

public interface Annotation {
    boolean equals(Object obj);
    int hashCode();
    String toString();
    Class<? extends Annotation> annotationType();
}

最后,我们的注解中也是可以写有属性的,它的属性不同于普通的属性,它的属性是抽象方法。既然注解也是一个接口,那么我们可以说接口体中可以定义什么,它同样也可以定义,而它的修饰符与接口一样,也是默认被public abstract修饰。

5.2 自定义注解属性的返回值

属性返回值既然有以上几种,那么我就在这里写出这几种演示一下是如何写的。

首先,定义一个枚举类和另外一个注解备用。

package com.mylifes1110.enums;

public enum Lamp {
    RED, GREEN, YELLOW
}
package com.mylifes1110.anno;

public @interface MyAnno2 {
}

其次,我们来定义上述几种类型,如下:

package com.mylifes1110.anno;

import com.mylifes1110.enums.Lamp;

public @interface MyAnno {
    //基本数据类型
    int num();

    //String类型
    String value();

    //枚举类型
    Lamp lamp();

    //注解类型
    MyAnno2 myAnno2();

    //以上类型的数组
    String[] values();
    Lamp[] lamps();
    MyAnno2[] myAnno2s();
    int[] nums();
}

5.3 自定义注解的属性赋值

这里我们演示一下,首先,我们使用该注解来进行演示。

package com.mylifes1110.anno;

public @interface MyAnno {
    //基本数据类型
    int num();

    //String类型
    String value();
}

随后创建一个测试类,在类的上方写上注解,你会发现,注解的参数中会让你写这两个参数(int、String)。

此时,传参是这样来做的。格式为:名称 = 返回值类型参数。如下:

上述所说,如果使用default关键字给属性默认初始化值,就不需要为其参数赋值,如果赋值的话,就把默认初始化的值覆盖掉了。

当然还有一个规则,如果只有一个属性需要赋值,并且属性的名称为value,则赋值时value可以省略,可以直接定义值。那么,我们的num已经有了默认值,就可以不为它传值。我们发现,注解中定义的属性就剩下了一个value属性值,那么我们就可以来演示这个规则了。

这里,我并没有写属性名称value,而是直接为value赋值。如果我将num的default关键字修饰去掉呢,那意思也就是说在使用该注解时必须为num赋值,这样可以省略value吗?那我们看一下。

结果,就是我们所想的,它报错了,必须让我们给num赋值。其实想想这个规则也是很容易懂的,定义一个为value的值,就可以省略其value名称。如果定义多个值,它们可以省略名称就无法区分定义的是那个值了,关键是还有数组,数组内定义的是多个值呢,对吧。

5.4 自定义注解的多种返回值类型赋值

这里我们演示一下,上述的多种返回值类型是如何赋值的。这里我们定义这几个参数来看一下,是如何为属性赋值的。

num是一个int基本数据类型,即num = 1

value是一个String类型,即value = "str"

lamp是一个枚举类型,即lamp = Lamp.RED

myAnno2是一个注解类型,即myAnno2 = @MyAnno2

values是一个String类型数组,即values = {"s1", "s2", "s3"}

values是一个String类型数组,其数组中只有一个值,即values = "s4"

注意: 值与值之间是,隔开的;数组是用{}来存储值的,如果数组中只有一个值可以省略{};枚举类型是枚举名.枚举值

六、元注解

6.1 元注解分类

而在jdk的中java.lang.annotation包中定义了四个元注解,如下:

6.2 @Target

由此可见,该注解体内只有一个value属性值,但是它的类型是一个ElementType数组。那我们进入到这个数组中继续查看。

进入到该数组中,你会发现他是一个枚举类,其中定义了上述表格中的各个属性。

了解了@Target的作用和属性值后,我们来使用一下该注解。首先,我们要先用该注解来修饰一个自定义注解,定义该注解的指定作用在类上。如下:

而你观察如下测试类,我们把注解作用在类上时是没有错误的。而当我们的注解作用在其他地方就会报错。这也就说明了,我们@Target的属性起了作用。

注意: 如果我们定义多个作用范围时,也是可以省略该参数名称了,因为该类型是一个数组,虽然能省略名称但是,我们还需要用{}来存储。

6.3 @Retention

注意: 我们常用的定义即是RetentionPolicy.RUNTIME,因为我们使用反射来实现的时候是需要从JVM中获取class类对象并操作类对象的。

首先,我们要了解反射的三个生命周期阶段,这部分内容我在Java反射机制中也是做了非常详细的说明,有兴趣的小伙伴可以去看看我写的Java反射机制,相信你在其中也会有所收获。

这里我再次强调一下这三个生命周期是源码阶段 - > class类对象阶段 - > Runtime运行时阶段

那我们进入到源码,看看@Retention注解中是否有这些参数。

我们看到该注解中的属性只有一个value,而它的类型是一个RetentionPolicy类型,我们进入到该类型中看看有什么参数,是否与表格中的参数相同呢?

至于该注解怎么使用,其实是相同的,用法如下:

这就证明了我们的注解可以保留到Runtime运行阶段,而我们在反射中大多数是定义到Runtime运行时阶段的,因为我们需要从JVM中获取class类对象并操作类对象。

6.4 @Documented

@Documented注解是比较好理解的,它是一个标记注解。被该标记注解标记的注解,生成doc文档时,注解是可以被加载到文档中显示的。

还拿api中过时的Date中的方法来说,在api中显示Date中的getYear方法是这样的。

正如你看到的,注解在api中显示了出来,证明该注解是@Documented注解修饰并文档化的。那我们就看看这个注解是否被@Documented修饰吧。

然后,我们发现该注解的确是被文档化了。所以在api中才会显示该注解的。如果不信,你可以自己使用javadoc命令来生成一下doc文档,看看被该注解修饰的注解是否存在。

至于Javadoc文档生成,我在javadoc文档生成一文中有过详细记载,大家可以进行参考,生成doc文档查看。

6.5 @Inherited

首先进入到源码中,我们也可以清楚的知道,该注解也是一个标记注解。而且它也是被文档化的注解。

其次,我们去在自定义注解中,标注上@Inherited注解。

演示@Inherited注解,我需要创建两个类,同时两个类中有一层的继承关系。如下:

我们在Person类中标记了@MyAnno注解,由于该注解被@Inherited注解修饰,我们就可以得出继承于Person类的Student类也同样被@MyAnno注解标记了,如果你要获取该注解的值的话,肯定获取的也是父类上注解值的那个"1"。

七、使用反射机制解析注解

自定义注解

package com.mylifes1110.anno;

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

/**
 * @InterfaceName Sign
 * @Description 描述需要执行的类名和方法名
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sign {
    String methodName();

    String className();
}

Cat

package com.mylifes1110.java;

/**
 * @ClassName Cat
 * @Description 描述一只猫的类
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

public class Cat {
    /*
     * @Description 描述一只猫吃鱼的方法 
     * @Author Ziph
     * @Date 2020/6/6
     * @Param []
     * @return void
     */

    public void eat() {
        System.out.println("猫吃鱼");
    }
}

准备好,上述代码后,我们就可以开始编写使用反射技术来解析注解的测试类。如下:

首先,我们先通过反射来获取注解中的methodName和className参数。

package com.mylifes1110.java;

import com.mylifes1110.anno.Sign;

/**
 * @ClassName SignTest
 * @Description 要求创建cat对象并执行其类中eat方法
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

@Sign(className = "com.mylifes1110.java.Cat", methodName = "eat")
public class SignTest {
    public static void main(String[] args) {
        //获取该类的类对象
        Class<SignTest> signTestClass = SignTest.class;
        //获取类对象中的注解对象
        //原理实际上是在内存中生成了一个注解接口的子类实现对象
        Sign sign = signTestClass.getAnnotation(Sign.class);
        //调用注解对象中定义的抽象方法(注解中的属性)来获取返回值
        String className = sign.className();
        String methodName = sign.methodName();
        System.out.println(className);
        System.out.println(methodName);
    }
}

此时的打印结果证明我们已经成功获取到了该注解的两个参数。

注意: 获取类对象中的注解对象时,其原理实际上是在内存中生成了一个注解接口的子类实现对象并返回的字符串内容。如下:

public class SignImpl implements Sign {
    public String methodName() {
        return "eat";
    }

    public String className() {
        return "com.mylifes1110.java.Cat";
    }
}

继续编写我们后面的代码,代码完整版如下:

完整版代码

package com.mylifes1110.java;

import com.mylifes1110.anno.Sign;

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

/**
 * @ClassName SignTest
 * @Description 要求创建cat对象并执行其类中eat方法
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

@Sign(className = "com.mylifes1110.java.Cat", methodName = "eat")
public class SignTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //获取该类的类对象
        Class<SignTest> signTestClass = SignTest.class;
        //获取类对象中的注解对象
        //原理实际上是在内存中生成了一个注解接口的子类实现对象
        Sign sign = signTestClass.getAnnotation(Sign.class);
        //调用注解对象中定义的抽象方法(注解中的属性)来获取返回值
        String className = sign.className();
        String methodName = sign.methodName();
        //获取className名称的类对象
        Class<?> clazz = Class.forName(className);
        //创建对象
        Object o = clazz.newInstance();
        //获取methodName名称的方法对象
        Method method = clazz.getMethod(methodName);
        //执行该方法
        method.invoke(o);
    }
}

执行结果

执行后成功的调用了eat方法,并打印了猫吃鱼的结果,如下:

八、自定义注解改变JDBC工具类

自定义注解
package com.mylifes1110.java.anno;

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

/**
 * @InterfaceName DBInfo
 * @Description 给予注解声明周期为运行时并限定注解只能用在类上
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBInfo {
    String driver() default "com.mysql.jdbc.Driver";

    String url() default "jdbc:mysql://localhost:3306/temp?useUnicode=true&characterEncoding=utf8";

    String username() default "root"
;

    String password() default "123456";
}
数据库连接工具类
package com.mylifes1110.java.utils;

import com.mylifes1110.java.anno.DBInfo;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

/**
 * @ClassName DBUtils
 * @Description 数据库连接工具类
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

@DBInfo()
public class DBUtils {
    private static final Properties PROPERTIES = new Properties();
    private static String driver;
    private static String url;
    private static String username;
    private static String password;

    static {
        Class<DBUtils> dbUtilsClass = DBUtils.class;
        boolean annotationPresent = dbUtilsClass.isAnnotationPresent(DBInfo.class);
        if (annotationPresent) {
            /**
             * DBUilts类上有DBInfo注解,并获取该注解
             */

            DBInfo dbInfo = dbUtilsClass.getAnnotation(DBInfo.class);
//            System.out.println(dbInfo);
            driver = dbInfo.driver();
            url = dbInfo.url();
            username = dbInfo.username();
            password = dbInfo.password();
        } else {
            InputStream inputStream = DBUtils.class.getResourceAsStream("db.properties");
            try {
                PROPERTIES.load(inputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            Class.forName(driver);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection() {
        try {
            return DriverManager.getConnection(url, username, password);
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        return null;
    }

    public static void closeAll(Connection connection, Statement statement, ResultSet resultSet) {
        try {
            if (resultSet != null) {
                resultSet.close();
                resultSet = null;
            }

            if (statement != null) {
                statement.close();
                statement = null;
            }
            if (connection != null) {
                connection.close();
                connection = null;
            }
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}
测试类
package com.mylifes1110.java.test;

import com.mylifes1110.java.utils.DBUtils;

import java.sql.Connection;

/**
 * @ClassName GetConnectionDemo
 * @Description 测试连接是否可以获取到
 * @Author Ziph
 * @Date 2020/6/6
 * @Since 1.8
 */

public class GetConnectionDemo {
    public static void main(String[] args) {
        Connection connection = DBUtils.getConnection();
        System.out.println(connection);
    }
}
测试结果

九、自定义@MyTest注解实现单元测试

06-07 07:55