找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3662

积分

0

好友

486

主题
发表于 1 小时前 | 查看: 4| 回复: 0

Java 泛型(generics)是 JDK 5 引入的一个特性,它提供了编译时类型安全检测机制,允许开发者在编译期就发现非法的类型操作。泛型的本质就是参数化类型,也就是说,所操作的数据类型被指定为一个参数。若想了解更多 Java 泛型的底层细节与最佳实践,可以在云栈社区的技术专区深入交流。

泛型带来的好处

在没有泛型的时代,通常用 Object 引用来实现参数的“任意化”。但“任意化”的代价是必须进行显式的强制类型转换,而这要求开发者能提前预知实际参数的类型。一旦强制转换错误,编译器可能不会报错,到运行时才抛异常,这本身就是一个安全隐患。

泛型的优势就在于:编译时就能检查类型安全,并且所有的强制转换都是自动、隐式的。

public class GlmapperGeneric<T> {
    private T t;
    public void set(T t) { this.t = t; }
    public T get() { return t; }

    public static void main(String[] args) {
        // do nothing
    }

    /**
     * 不指定类型
     */
    public void noSpecifyType(){
        GlmapperGeneric glmapperGeneric = new GlmapperGeneric();
        glmapperGeneric.set("test");
        // 需要强制类型转换
        String test = (String) glmapperGeneric.get();
        System.out.println(test);
    }

    /**
     * 指定类型
     */
    public void specifyType(){
        GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric();
        glmapperGeneric.set("test");
        // 不需要强制类型转换
        String test = glmapperGeneric.get();
        System.out.println(test);
    }
}

上面的 specifyType 方法省略了强制转换,可以在编译期检查类型安全,这种机制可应用于类、方法、接口。

泛型中通配符

在定义泛型类、泛型方法、泛型接口时,经常会见到形如 TEKV 等不同的通配符。它们到底有什么含义呢?

常用的 T、E、K、V、?

本质上,这些只是通配符的约定俗成,并没有本质区别。比如上面代码中的 T,可以换成 A‑Z 之间任何一个字母,程序都能正常运行,只是换成别的字母可读性会变弱。通常的约定如下:

  • ? 表示不确定的 Java 类型  
  • T (type) 表示具体的一个 Java 类型  
  • K V (key value) 分别代表 Java 键值中的 Key 与 Value  
  • E (element) 代表 Element(元素)

? 无界通配符

先从一个小例子看起。

我有一个父类 Animal 和几个子类,如狗、猫等。现在需要一个动物的列表,我的第一想法是这样:

List<Animal> listAnimals

不过,更推荐的做法是:

List<? extends Animal> listAnimals

为什么要用通配符而不是简单的泛型?其实,单纯声明局部变量时通配符没什么意义,但当你在方法参数中使用时,它就非常重要了。

static int countLegs (List<? extends Animal > animals ) {
    int retVal = 0;
    for ( Animal animal : animals ) {
        retVal += animal.countLegs();
    }
    return retVal;
}

static int countLegs1 (List< Animal > animals ) {
    int retVal = 0;
    for ( Animal animal : animals ) {
        retVal += animal.countLegs();
    }
    return retVal;
}

public static void main(String[] args) {
    List<Dog> dogs = new ArrayList<>();
    // 不会报错
    countLegs( dogs );
    // 报错
    countLegs1(dogs);
}

调用 countLegs1 时会直接飘红,报错如下:

泛型类型不匹配编译错误提示

因此,对于不确定或者不关心实际要操作的类型,可以使用无界通配符(尖括号里一个问号,即 <?>),表示可以持有任何类型。像 countLegs 方法,限定了上界 extends Animal,但不关心具体是什么子类,所有的 Animal 子类都能正常传入。而 countLegs1 就没法做到这一点。

上界通配符 <? extends E>

上界:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是该类型的子类。

使用 <? extends E> 在类型参数里有两大好处:

  • 如果传入的类型不是 EE 的子类,编译直接失败  
  • 泛型内部可以直接使用 E 的方法,不用再强转
private <K extends A, E extends B> E test (K arg1, E arg2) {
    E result = arg2;
    arg2.compareTo(arg1);
    //.....
    return result;
}

类型参数列表中如果有多个上限,用逗号分开。

下界通配符 <? super E>

下界:用 super 声明,表示参数化的类型可能是所指定的类型,或者是该类型的父类型,最远可以至 Object

在类型参数中使用 super,意味着这个泛型参数必须是 EE 的父类。

private <T> void test (List<? super T> dst, List<T> src) {
    for (T t : src) {
        dst.add(t);
    }
}

public static void main(String[] args) {
    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = new ArrayList<>();
    new Test3().test(animals,dogs);
}
// Dog 是 Animal 的子类
class Dog extends Animal {

}

dst 的类型 “大于等于” src 的类型(这里的 “大于等于” 指 dst 代表的类型范围更广),因此能装下 src 的容器自然也能装下 dst

?和 T 的区别

泛型列表声明:List&lt;T&gt;与List&lt;?&gt;

?T 都表示不确定的类型,但区别在于我们可以对 T 进行操作,而对 ? 不行。比如:

// 可以
T t = operate();

// 不可以
?car = operate();

简单总结:

T 是一个 确定的 类型,通常用在泛型类和泛型方法的定义中;? 是一个 不确定 的类型,通常用于泛型方法的调用和形参,不能用来定义类和泛型方法。

区别1:通过 T 来确保泛型参数的一致性

// 通过 T 来确保泛型参数的一致性
public <T extends Number> void test(List<T> dest, List<T> src)

// 通配符是不确定的,所以此方法不能保证两个 List 具有相同的元素类型
public void test(List<? extends Number> dest, List<? extends Number> src)

下面的代码,因为 String 不是 Number 的子类,所以直接飘红报错。

泛型参数类型不一致导致的编译错误

而如果两个 List 的元素类型不一致的情况,如下:

GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric<>();
List<String> dest = new ArrayList<>();
List<Number> src = new ArrayList<>();
glmapperGeneric.testNon(dest,src);

上面这段代码在编译器不会报错,但进入 testNon 方法内部操作时(比如赋值),对于 destsrc 就还是需要手动进行类型转换。

区别2:类型参数可以多重限定而通配符不行

使用&设定多重边界的泛型示例代码

使用 & 符号可以设定多重边界(Multi Bounds),指定泛型类型 T 必须同时是 MultiLimitInterfaceAMultiLimitInterfaceB 的共有子类型,此时变量 t 就拥有了所有限定的方法和属性。通配符因为不是确定的类型,所以无法进行多重限定。

区别3:通配符可以使用超类限定而类型参数不行

类型参数 T 只具有 一种 类型限定方式:

T extends A

但通配符 ? 可以进行 两种限定

? extends A
? super A

Class<T>Class<?> 的区别

前面介绍了 ?T 的区别,那么 Class<T>Class<?> 又有什么不同呢?

反射场景中的使用尤为常见,这里用一段反射代码来说明。

// 通过反射的方式生成 multiLimit
// 对象,这里比较明显的是,我们需要使用强制类型转换
MultiLimit multiLimit = (MultiLimit)
Class.forName("com.glmapper.bridge.boot.generic.MultiLimit").newInstance();

对于上述代码,在运行期如果反射出来的类型不是 MultiLimit 类,就会抛出 java.lang.ClassCastException 异常。

更好的写法是让编译器提前检查类型问题:

使用Class&lt;T&gt;进行反射生成实例

Class<T> 在实例化时,T 必须替换成具体的类。而 Class<?> 是一个通配泛型,? 可以代表任意类型,所以主要用于声明时的限制。比如:

// 可以
public Class<?> clazz;
// 不可以,因为 T 需要指定类型
public Class<T> clazzT;

那如果也想使用 public Class<T> clazzT; 这样的声明,就必须让所在的类也指定 T

public class Test3<T> {
    public Class<?> clazz;
    // 不会报错
    public Class<T> clazzT;
}

当你不确定要声明什么类型的 Class 时,可以定义一个 Class<?>

Class&lt;?&gt;通配符声明示例

小结

本文零碎整理了 Java 泛型中的一些要点,难免有不全之处,仅供参考。若发现不当之处,欢迎指正。
如果想要了解更多 Java 技术干货,欢迎访问 云栈社区 与众多开发者一起交流成长。




上一篇:三步构建职场盟友关系:自然破冰、雪中送炭与利益绑定
下一篇:CVE-2026-31431 Copy Fail漏洞分析:732字节脚本实现Linux提权及容器逃逸
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-7 07:06 , Processed in 0.828210 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表