JAVA24-泛型与注解

泛型(Generics)是一种允许在定义类、接口或方法时使用类型参数的机制。泛型的主要目的是提供类型安全性,并减少类型转换的需要。通过泛型,可以编写更通用、更灵活的代码,同时避免运行时类型转换错误。

为什么需要泛型

主要是为了实现类型安全和使用时的一点点方便(不需要再转换类型),元素拿出来就可以直接用

什么是泛型

规定这个容器只能装什么类型的数据(元素)

1
List<String> list = new ArrayList<>(); // 只能装String类型的容器

泛型擦除

Java的泛型是在编译时期规定这个容器只能装什么类型的数据的,在运行时对于JVM来说,是没有泛型这个区别的,因为你在编译时期添加元素的时候,已经限定了它们都是同一个类型的,因此在运行时,是不存在类型安全问题的,所以在运行时JVM会擦除掉泛型限定,这个叫做泛型的擦除

擦除带来的问题

Java的泛型是假泛型,它只在编译期有效,运行时期完全不保留,因此可以通过泛型的擦除来绕过编译器的检查,如下代码所示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Main {
    public static void main(String[] args) {
        /*
        本来list只能装String类型,但通过泛型擦除绕过了编译器的检查
        添加了其他类型的数据,这是很危险的,这就是泛型擦除带来的问题
         */
        ArrayList<String> list = new ArrayList<>();
        ArrayList raw = list;
        raw.add(1);
        raw.add(new Object());
        ArrayList<String> newList = list;
        System.out.println(list.get(0));
    }
}

ArrayList 不是 ArrayList的子类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Main {
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("Hello");
        print(arrayList); // 报错
    }
    public static void print(ArrayList<Object> arrayList) {
        for (Object o : arrayList) {
            System.out.println(o.toString());
        }
    }
}

当你使用一个裸类型时,编译器会警告

1
ArrayList list = new ArrayList();

泛型的绑定

 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
public class Main {
    public static void main(String[] args) {
        System.out.println(max(1, 2));
        System.out.println(max(1.0, 2.0));
        System.out.println(max("AAA", "BBB"));
    }
    
    // 将参数,返回值,等改成泛型参数,在未来动态决定是什么类型,这种操作我们称为泛型绑定
    public static <T> T max(T a, T b) {
        return a;
    }

    // <T extends Comparable> 就是泛型限定符,它限制了T类型必须是Comparable的子类或实现了Comparable接口的类
    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) >= 0 ? a : b;
    }
    
    // 泛型类型可以不同,例如这里的T和E它代表两个不同的类型,泛型限定符可以绑定多个,多个限定符用逗号隔开
    public static <T extends Comparable<T>, E extends Comparable<T>> T max(T a, E b) {
        return a;
    }

    /*
    public static T max (T a, T b) {
        return a > b ? a : b; // 我们想把它泛型化,但这里依然不能比较,因为它不知道你这个T是什么玩意
    }
    */

    // 没有泛型的年代,就只能如下方式处理,非常啰嗦
    /*
    public static Integer max(Integer a, Integer b) {
        return a > b ? a : b;
    }

    public static Long max(Long a, Long b) {
        return a > b ? a : b;
    }

    public static Double max(Double a, Double b) {
        return a > b ? a : b;
    }
    */
}

泛型的限定符

? extends 要求泛型是某种类型及其子类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Main {
    public static void main(String[] args) {
        System.out.println(max(1, 2));
        System.out.println(max(1.0, 2.0));
        System.out.println(max("AAA", "BBB"));
    }
    
    // <T extends Comparable> 就是泛型限定符,它限制了T类型必须是Comparable的子类或实现了Comparable接口的类
    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) >= 0 ? a : b;
    }
}

? super 要求泛型是某种类型及其父类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import java.util.Comparator;
import java.util.List;

public class Main {
    // 接受2个泛型参数,Comparator<? super T>表示传进来的这个Comparator必须是T的父类型或T类型
    public static <T> void sort(List<T> list, Comparator<? super T> c) {
        list.sort(c);
    }

    // 接受1个泛型参数,T必须继承Comparable,而Comparable必须是T类型的父类
    public static <T extends Comparable<? super T>> void sort(List<T> list) {
        list.sort(null);
    }
}

泛型练习

 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
public class Main {
    static class IntBinaryTreeNode {
        int value;
        IntBinaryTreeNode left;
        IntBinaryTreeNode right;
    }

    static class StringBinaryTreeNode {
        String value;
        StringBinaryTreeNode left;
        StringBinaryTreeNode right;
    }

    static class DoubleBinaryTreeNode {
        double value;
        DoubleBinaryTreeNode left;
        DoubleBinaryTreeNode right;
    }

    // 你看,上面三种"二叉树节点"结构相似,内容重复,请将其泛型化,以节省代码
    static class BinaryTreeNode<T> {
        T value;
        BinaryTreeNode<T> left;
        BinaryTreeNode<T> right;
    }

    // 泛型化之后,请再编写一个算法,对二叉树进行中序遍历,返回中序遍历的结果
    public static <T> List<T> inorderTraversal(BinaryTreeNode<T> root) {
        List<T> list = new ArrayList<>();
        if (root.left != null) {
            list.addAll(inorderTraversal(root.left));
        }
        list.add(root.value);
        if (root.right != null) {
            list.addAll(inorderTraversal(root.right));
        }
        return list;
    }
}

Java中的注解

我们之前说了,Class对象是类的说明书,而注解就是说明书中的一小段说明信息
注解只是一小段信息,至于如何处理,和注解没有半毛钱关系,如何处理它是由使用这个注解的对象决定的

注解的创建

注解的创建和普通的类没有什么太大区别,关键字是@interface

1
2
public @interface MyAnnotation {
}

元注解

元注解就是可以放在注解上面的注解

Retention 指定这个注解在编译后的保留等级

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

Target 指定这个注解可以被标注在什么东西上面

 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
public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}
  • Documented 不常用,用@Documented注解修饰的注解类会被Javadoc工具提取成文档
  • Inherited 不常用,指定这个注解能否被子类所看到
  • Repeatable 不常用,指定这个注解是否可以被重构

注解的属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Target(ElementType.METHOD)
public @interface MyAnnotation {
    // 属性类型,属性名,默认值
    String name() default "Hello Annotation";
}

public class Main {
    @MyAnnotation(name = "Jerry")
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

注解属性的类型可以有哪些?

基本数据类型+String+类以及它们的数组

value属性

当只有一个value属性时,调用时可以不指定属性名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Target(ElementType.METHOD)
public @interface MyAnnotation {
    // 属性类型,属性名,默认值
    String value() default "Hello Annotation";
}

public class Main {
    @MyAnnotation("Jerry")
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

一些JDK自带的注解

  • @Override 编译器会检查你是否覆盖了父类的方法
  • @Deprecated 编译器会标记这个方法已经过时
  • @SuppressWarnings 抑制一些警告
  • @FunctionalInterface 标记这是一个函数式接口

注解实战,实现基于注解的缓存装饰器

我们知道注解本身是不具备任何功能的,它只是一段说明,要想给注解赋予真正的功能,
我们需要通过反射在运行时动态的赋予它各种能力(为什么只能在运行时动态赋予呢?)
因为它本身就不具备什么功能嘛,源码层面上没有体现,这就需要在运行时动态生成代码

  • Cache 注解
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.github.hcsp.annotation;

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

@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
    // 标记缓存的时长(秒),默认60s
    int cacheSeconds() default 60;
}
  • CacheService
 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
package com.github.hcsp.annotation;

import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class DataService {
    /**
     * 根据数据ID查询一列数据,有缓存。
     *
     * @param id 数据ID
     * @return 查询到的数据列表
     */
    @Cache
    public List<Object> queryData(int id) {
        // 模拟一个查询操作
        Random random = new Random();
        int size = random.nextInt(10) + 10;
        return IntStream.range(0, size)
                .mapToObj(i -> random.nextInt(10))
                .collect(Collectors.toList());
    }

    /**
     * 根据数据ID查询一列数据,无缓存。
     *
     * @param id 数据ID
     * @return 查询到的数据列表
     */
    public List<Object> queryDataWithoutCache(int id) {
        // 模拟一个查询操作
        Random random = new Random();
        int size = random.nextInt(10) + 1;
        return IntStream.range(0, size)
                .mapToObj(i -> random.nextBoolean())
                .collect(Collectors.toList());
    }
}
  • CacheClassDecorator 实现@Cache注解
 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.github.hcsp.annotation;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;
import net.bytebuddy.matcher.ElementMatchers;

import java.lang.reflect.Method;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;

public class CacheClassDecorator {
    // 将传入的服务类Class进行增强
    // 使得返回一个具有如下功能的Class:
    // 如果某个方法标注了@Cache注解,则返回值能够被自动缓存注解所指定的时长
    // 这意味着,在短时间内调用同一个服务的同一个@Cache方法两次
    // 它实际上只被调用一次,第二次的结果直接从缓存中获取
    // 注意,缓存的实现需要是线程安全的
    @SuppressWarnings("unchecked")
    public static <T> Class<T> decorate(Class<T> klass) {
        return (Class<T>) new ByteBuddy()
                .subclass(klass) // 动态创建一个klass的子类,因为你要扩展klass嘛,因此你肯定需要一个子类
//                .method(methodDescription -> klass.getAnnotation(Cache.class) != null) // 过滤出所有带有@Cache注解的方法
                .method(ElementMatchers.isAnnotatedWith(Cache.class)) // 这句和上面的意思一样
                .intercept(MethodDelegation.to(Target.class)) // 拦截这些方法并委托给谁处理
                .make() // 创建出这个Class对象
                .load(klass.getClassLoader())  // 让ClassLoader加载它
                .getLoaded(); // 返回加载后的Class对象
    }

    public static class Target {
        private static final ConcurrentHashMap<CacheK, CacheV> cache = new ConcurrentHashMap<>();

        @RuntimeType // 这个注解指定当发生委托时,要调用这个方法,如果你不想加这个注解,那必须保证方法的返回值和被拦截的方法返回值一致(目前我看到的是这样)
        public static Object intercept(
                @SuperCall Callable<Object> supperCall, // 既然我们这个Class是继承的klass,那么我们通过这个参数就能调用父类的方法
                @Origin Method method, // 当委托发生的时候,可以拿到当前被调用的方法(换句话说就是,调用哪个方法的时候发生了这个委托)
                @This Object thisObject, // 哪个对象调用了这个方法
                @AllArguments Object[] allArguments // 可以拿到所有的参数
        ) throws Exception {
            CacheK key = new CacheK(thisObject, method.getName(), allArguments);
            CacheV cached = cache.get(key);
            if (cached != null) {
                if (System.currentTimeMillis() - cached.getTime() > method.getAnnotation(Cache.class).cacheSeconds() * 1000L) {
                    return cacheValue(supperCall, key);
                }
                return cached.getValue();
            }
            return cacheValue(supperCall, key);
        }

        private static Object cacheValue(Callable<Object> supperCall, CacheK key) throws Exception {
            CacheV value = new CacheV(supperCall.call(), System.currentTimeMillis());
            cache.put(key, value);
            return value.getValue();
        }
    }

    public static void main(String[] args) throws Exception {
        DataService dataService = decorate(DataService.class).getConstructor().newInstance();

        // 有缓存的查询:只有第一次执行了真正的查询操作,第二次从缓存中获取
        System.out.println(dataService.queryData(1));
        Thread.sleep(1 * 1000);
        System.out.println(dataService.queryData(1));

        // 无缓存的查询:两次都执行了真正的查询操作
        System.out.println(dataService.queryDataWithoutCache(1));
        Thread.sleep(1 * 1000);
        System.out.println(dataService.queryDataWithoutCache(1));
    }
}
  • CacheK 保存缓存的K
 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
package com.github.hcsp.annotation;

import java.util.Arrays;
import java.util.Objects;

public class CacheK {
    private Object object;
    private String methodName;
    private Object[] allArguments;

    public CacheK(Object object, String methodName, Object[] allArguments) {
        this.object = object;
        this.methodName = methodName;
        this.allArguments = allArguments;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        CacheK cacheK = (CacheK) o;
        return Objects.equals(object, cacheK.object) && Objects.equals(methodName, cacheK.methodName) && Arrays.equals(allArguments, cacheK.allArguments);
    }

    @Override
    public int hashCode() {
        int result = Objects.hash(object, methodName);
        result = 31 * result + Arrays.hashCode(allArguments);
        return result;
    }
}
  • CacheV 保存缓存的值和缓存时间
 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
package com.github.hcsp.annotation;

public class CacheV {
    private Object value;
    private long time;

    public CacheV(Object value, long time) {
        this.value = value;
        this.time = time;
    }

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }

    public long getTime() {
        return time;
    }

    public void setTime(long time) {
        this.time = time;
    }
}
updatedupdated2025-03-012025-03-01