Java的泛型

本文最后更新于 2025年4月18日

一、引言

泛型(Generics)和面向对象、函数式编程一样,也是一种程序设计的范式,泛型允许程序员在定义类、接口和方法时使用引用类型的类型形参代表一些以后才能确定下来的类型,在声明变量、创建对象、调用方法时像调用函数传参一样将具体类型作为实参传入来动态指明类型。

Java的泛型,是在jdk1.5中引入的一个特性,最主要应用是在jdk1.5的新集合框架中。作为Java语法层面的东西,本博客原本不打算介绍,但考虑到泛型理解和使用起来有一定的难度,应用的还很普遍,再加上自己工作多年好像也没有能够完全理解和灵活的运用泛型,因此还是决定看一些相关的书籍中与泛型有关的内容,并用一些篇幅总结下学习成果,介绍下我理解的泛型。

二、创建泛型

泛型,用一对菱形<>声明,<>中是类型形参列表,如有多个类型形参,使用英文逗号,隔开。

2.1 创建泛型类

下面程序定义了一个带有泛型声明的类Fruit,有一个类型形参T(Type),声明了类的泛型参数后,就可以在类内部使用此泛型参数,创建泛型接口同理

public class Fruit<T> {

    private T obj;

    public Fruit() {
        
    }

    public Fruit(T o) {
        this.obj = o;
    }

    public void set(T o) {
        this.obj = o;
    }

    public T get() {
        return this.obj;
    }

}

⚠️ 需要注意的是,Java规定:异常类(java.lang.Throwable)不得带有泛型!

public class MyException<T> extends Exception { //编译出错❌,Generic class may not extend 'java.lang.Throwable'
    T msg;
}
public class MyException<T> extends RuntimeException { //编译出错❌,Generic class may not extend 'java.lang.Throwable'
    T msg;
}
public class MyException<T> extends Throwable { //编译出错❌,Generic class may not extend 'java.lang.Throwable'
    T msg;
}

2.2 创建泛型方法

有时候,在类和接口上不需定义类型形参,只是方法的类型不确定,需要在方法上面定义类型形参,这个也是支持的,jdk1.5提供了对于泛型方法的支持。

声明泛型方法时,在返回值前指明泛型的类型形参<T>,仅作用于方法内,类型形参可以出现在参数和返回值中,调用方法时指定具体类型。泛型方法可以根据需要声明为静态。

任何类中都可以存在泛型方法,而不是只有泛型类中才能声明泛型方法。

💡在返回值前面指明泛型的类型形参列表<>是泛型方法的特征,没有这个特征的都不是泛型方法,泛型类中使用类<>里面声明的类型作为方法参数或返回值类型的方法,不属于泛型方法,例如2.1中Fruit类中的任何方法都不是泛型方法。

public class Demo {

    public <T> E fun1(T e) {
        return null;
    }

    public <T> void fun2(T e) {
        
    }

    public static <T> List<T> copyArray(T[] arr) {
        List<T> list = new ArrayList<>();
        
        for (int i = 0; i < arr.length; i++) {
            list.add(arr[i]);
        }
        
        return list;
    }

}

三、使用泛型

使用泛型就是在声明变量、创建对象、调用方法时将具体类型作为实参传入来动态指明类型,传进去的类型必须是引用类型。

3.1 实例化泛型类

使用该类创建对象时就可以为类型形参T传入具体类型,就可以生成类似Fruit<String>Fruit<Double>的类型

public static void main(String[] args) {
    // 构造器T形参是String,只能用String初始化
    Fruit<String> fruit = new Fruit<>("apple");

    // 构造器T形参是Double,只能用Double初始化
    Fruit<Double> fruit2 = new Fruit<>(3.8);
}

如不指定默认为Object类型,因为所有引用类型都能被Object代表,int、double、char等基本数据类型不能被Object代表,这就是类型实参必须是引用类型的原因

public static void main(String[] args) {
    Fruit fruit = new Fruit("apple");
    fruit = new Fruit(12);
    fruit = new Fruit(new Date());
}

3.2 派生泛型类

派生该类时,需要指定类型实参

public class Apple extends Fruit<Integer> {

    public static void main(String[] args) {
        Apple apple = new Apple();
        apple.set(32); 
        apple.set(""); //编译出错❌
    }

}

如不指定类型实参,一样转换为Object类型

public class Apple extends Fruit {

    public static void main(String[] args) {
        Apple apple = new Apple();
        apple.set(32); 
        apple.set(""); 
    }
}

还可以子类和父类声明同一个类型形参,子类中也不确定具体的类型,需要子类被实例化时将类型间接传递给父类,同时子类还可以一同定义自己的泛型

public class Apple<T> extends Fruit<T> {

    
}
public class Apple<T, E> extends Fruit<T> {

    
}

为父类泛型确定类型,子类又有自己的泛型类是允许的

public class Apple<E> extends Fruit<Integer> {

}

使用泛型又不指定类型的写法是错误的

public class Apple extends Fruit<T> { //编译出错❌

}

四、不存在的泛型类

即使加了不同泛型,运行时仍然是同一种类,并不会因为类型参数的不同,产生新的类

public static void main(String[] args) {
    Fruit<String> fruit = new Fruit<>("apple");
    Fruit<Double> fruit2 = new Fruit<>(3.8);
    System.out.println(fruit2.getClass() == fruit.getClass()); //true
}

因此在泛型类中的静态的代码块、静态变量和静态方法上,不能使用类型形参

public class Demo<T> {
    
    public static T st; //编译出错❌
    
    static {
        T a = st;  //编译出错❌
    }
    
    public static void fun1(T obj) { //编译出错❌
        st = obj;
    }


}

由于系统中并不存在真正的泛型类,因此instanceof关键字后不能接泛型类

if (new ArrayList<>() instanceof List<String>) {  //编译出错❌
            
}

if (new ArrayList<>() instanceof List) { //正确写法✅
            
}

五、类型通配符

5.1 ?通配符

有时候,在使用泛型类时,传递的实参不能确定,例如要实现一个遍历打印list的方法,list中是哪一种元素都有可能,于是我们将泛型实参指定为Object类型,看似解决了问题,但是调用时却会编译报错:无法将List<Object>用于List<String>

public class Demo {

    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        test(strings); //编译出错❌
    }

    public static void test(List<Object> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }


}

在Java中,两个类通过继承和实现接口可以具有父子关系,但不能认为使用了父子类型的两个泛型类具有父子关系,例如上面程序出现了编译错误,说明List<String>不能被当成List<Object>的子类来用。

💡泛型与数组不同,如果是两个有父子关系的类各声明一个数组,例如:Object[]String[]String[]Object[]的子类型,是可以将String[]类型的变量赋值给Object[]的,这是一种Java语言早期不安全的设计,操作不当会引发ArrayStoreException,因此jdk1.5设计泛型时避免了这种设计。

为了兼容各种泛型的List,可以采用类型通配符?代表元素类型可以是任何类型

public static void test(List<?> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

但是,如果调用add()方法向其中添加非null元素,又会发现编译出错

public static void test(List<?> list) {    
    list.add(new Date()); //编译出错❌
    list.add(null); //能通过编译✅
}

通过分析List的get()add()两个方法的源码,可知get()方法参数是下标,返回为EE类型虽然不确定,但肯定是一个Object类型,而add()方法需要一个E类型或其子类的对象,而传进来的?不能确定是什么类型,甚至不知道是不是引用类型,假如传进来的List是个List<String>,在方法中又add(new Date())就导致类型混乱了,因此无法处理,但是null除外,它是任何引用类型的实例。

E get(int index);

boolean add(E e);

说白了,Java的泛型系统是类型安全优先的,不确定类型的泛型可读不可写。

5.2 有限制条件的通配符

⚠️ 本小节是整个泛型中难度最大,最不容易理解的

有时候,如果不希望List<?>可以存放任意一种类型,只希望存放某一类具体的类型,在设置类型通配符时,可以添加extendssuper限制条件,extends代表可以是某种类型及子类,super代表可以是某种类型及父类。

假如有这样的一些类

/**
 * 动物
 */
public class Animal {

}
/**
 * 猫
 */
public class Cat extends Animal {

}
/**
 * 狗
 */
public class Dog extends Animal {

}
/**
 * 英短猫
 */
public class YingDuan extends Cat {
    
}
/**
 * 布偶猫
 */
public class BuOu extends Cat {
    
}

首先看extends,在使用时,如果只希望List存放Animal及其子类,List的类型形参就可以设置为List<? extends Animal>extends修饰通配符的泛型,可读,读出为父类类型Animal,但不可写,例如传进来的是个List<Dog>,方法中又去add(new Cat())会导致类型混乱。例如传进来的是个List<Dog>,方法中又去add(new Animal())也会导致类型混乱,所以extends修饰的泛型禁止写入任何一个非null实例,因为只要允许写入就有类型混乱的风险

public class Demo {

    public static void test(List<? extends Animal> list) {

        Animal animal = list.get(1); //获取返回值时,由父类Animal接收

        list.add(new Cat()); //编译出错❌
        
        list.add(new YingDuan()); //编译出错❌
        
        list.add(new Dog()); //编译出错❌

        list.add(new Animal()); //同样编译出错❌
    }

}

再来看supersuperextends的情况会略有不同,super代表传入某种类型及父类,super修饰的泛型可读,但只能读出为Object,写也只能写入TT的子类,例如:

public class Demo{

    public static void test(List<? super Cat> list) {

        Object object = list.get(1);

        list.add(new Animal()); //编译出错❌

        list.add(new Cat()); //编译通过✅

        list.add(new YingDuan()); //编译通过✅

        list.add(new BuOu()); //编译通过✅

        list.add(null); //编译通过✅

        list.add(new Dog()); //编译出错❌
    }
}

举例来讲的话,传进来的类型不是“猫”就是“动物”,所以首先不能添加“动物”进去,因为有可能传进来的是“猫”,只有“动物”包含“猫”,没有“猫”包含“动物”。同理,不能添加“狗”进去,因为“狗”属于“动物”但不属于“猫”(废话)。所以只有“猫”和“英短”以及“布偶”能添加进去,因为无论传进来的是“猫”还是“动物”,“英短”和“布偶”既直接是“猫”,也间接是“动物”,而“猫”本身就能添加进去,所以只有T和T的子类(猫、英短、布偶)能写是因为这样符合程序里面类的父子关系,不会导致泛型中类型混乱。说白了就是,存放“动物”的容器,存一只“猫”进去也行,存放“猫”的容器,存进去“英短”以及“布偶”逻辑上都是正确的。

可直接记住结论:
? extends T → “只能读,不能写”
? super T → “能写(T和T的子类),读出来只能是 Object”

Java不仅能在使用泛型时设置通配符限制条件,还能在定义泛型时设置通配符限制条件,如下例定义了一个Bill类,成员变量num的类型只能是Number和相关子类

public class Bill<T extends Number> {

}

还可以设置多个

public class Bill<T extends Number & Comparable<T>> {

}

六、泛型擦除和转换


Java的泛型
https://blog.liuzijian.com/post/java-generics.html
作者
Liu Zijian
发布于
2022年9月21日
更新于
2025年4月18日
许可协议