深入理解 Java 泛型

本文深入探讨了Java泛型的引入背景、作用及其带来的好处,例如编译时类型检查和减少类型转换。通过示例展示了泛型的使用,包括泛型类和泛型方法的定义及调用。此外,文章详细解释了泛型的核心术语,如泛型类型、类型参数、类型参数化、原始类型等,并介绍了有界类型参数、通配符的概念,包括无界、上界和下界通配符,以及PECS原则。最后,讨论了泛型在继承和子类中的行为以及常见的语法混淆点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

没有泛型是怎样的

了解点 Java 历史的都知道,泛型是从 JDK 1.5 版本添加的特性,在 JDK1.5 之前,Java 很多特性都是没有的例如:泛型、注解、自动装箱和拆箱、可变参数。
在介绍泛型之前,我们先来看看,如果没有泛型的世界是怎么样的。

假设有一个 List,我只想把 String 类型的元素放入 List,如:

List list = new ArrayList();
list.add("hello");
list.add("hello2");
list.add("hello3");

但是谁也不能保证 list 里面都是 String 类型的元素,因为开发者可能这样写:

list.add(100);

这就和我们的预期不一致了,程序很可能就会出问题。例如我们想从 list 中取第一个元素:

List strlist =  new ArrayList();
strlist.add("hello");
strlist.add("hello2");
strlist.add(18);   // 不小心放入了一个整型数据
String data = strlist.get(0); // 编译器报错

可以看到使用 String 类型类接收 list 中的第一个元素报错了,这是因为 list.get 返回的 Object 类型,所以需要强转:

List list =  new ArrayList();
list.add("hello");
list.add("hello2");
String data = (String)list.get(0);

但是,如果第一个元素不是 String 呢?强转就会抛出 ClassCastException

为什么要有泛型

那如何使用泛型将上面的代码改造下:

List<String> strlist = new ArrayList<>();
strlist.add("hello");
strlist.add("hello2");
strlist.add(18);   // 编译报错
String data = strlist.get(0);  // 编译器正常

可以看出有了泛型之后,往 strList 里添加整型数据编译器会报错,也就是说会进行类型检查,将潜在的错误提前暴露出来。
而且获取数据也不用进行强转了。

使用泛型的好处:

  • 编译器类型检查。可以有效的将潜在的问题,在编译器提前暴漏出来,否则在了运行时再报错,往往就不好排查错误。
  • 消除了类型转换
  • 帮助开发者更好的实现通用算法。例如实现计算器。

泛型初体验

上面泛型示例我们使用的是 JDK 中内置的 List,接下来看下定义一个泛型类的语法:

class name<T1, T2, ..., Tn> { /* ... */ }

也就是在定义类时,在类名后面使用尖括号,尖括号里面的,称之为类型参数(type parameters 或 type variables),如 T1, T2, …, Tn

基于上面的语法,定义了一个容器类,提供了存数据和取数据的功能:

// 在定义类时,在类名后面使用<T>,为类添加参数类型(parameter type)
public class Container<T> {
	// 上面定义了参数类型,那么在类里面就可以使用该参数类型
    // 在字段上使用参数类型
    private T data;
    
	// 在成员方法上使用参数类型
    public void set(T t) {
        data = t;
    }
    
	// 在成员方法上使用参数类型
    public T get() {
        return data;
    }
}

上面我们将类型参数命名为 T,也可以定义成其他名字,但是最好遵循类型参数的命名约定:

  • E 表示元素(Element),例如集合里的元素
  • K 表示 key
  • V 表示 value
  • N 表示数字(Number)
  • T 表示类型(Type)

当然,你也可以命名成一个单词,例如 Param,Result 等:

public abstract class AsyncTask<Params, Progress, Result>

定义好了泛型类(generic class),那如何使用呢?,很简单,就是使用泛型类的时候,将类型参数 type parameter 换成具体的类型即可,这也称之为 调用泛型类型(Invoking a Generic Type):

public static void main(String[] args) {
    // 调用泛型类型,将泛型参数 T 换成具体的类型 String
    // 从 JDK 1.7 开始,new Container后面可以省略参数类型
    // Container<String> container = new Container<String>();
    Container<String> container = new Container<>();
    container.set("hello");
    // container.set(120); // 编译器报错 类型检查
    String value = container.get(); // 无需强转
    System.out.println(value);
}

除了可以定义泛型类(generic class),还可以定义泛型方法(generic method),语法为:
在方法的返回类型前面定义 type parameter,然后就可以使用 type parameter 了,这个type parameter 可以作为方法的返回类型,也可以作为方法的参数:

public final <T> T findViewById(int id) {
    //...
}

Android View 里有一个方法叫做 findViewById,在 Android 老版本是需要强转的,例如:

ImageView ivHeader1 = (ImageView) headerView.findViewById(R.id.ivHeader1);

在后面的 Android 版本中在 findViewById 使用了泛型,就可以不用强转了:

ImageView ivHeader1 = headerView.findViewById(R.id.ivHeader1);

小结:不管是泛型类还是泛型方法,要想使用 type parameter(一般记作T),首先要定义 type parameter,泛型类是在类名后面定义 type parameter,泛型方法是在方法的返回类型前面定义 type parameter。

泛型的核心术语

为什么要了解泛型相关的术语?

  1. 帮助我们更好的阅读关于泛型的源码注释和官方文档
  2. 帮我我们更好的掌握关于泛型的反射相关的知识

关于第一点:帮助我们更好的阅读关于泛型的源码注释和官方文档

我们在阅读关于泛型官方文档的时候,经常会碰到诸如:generic type、type parameter、type variable、type argument、parameterized type、raw type 等等术语。

  • Generic Type,例如代码:
class Foo<T>{}

其中 Foo<T>generic type

  • Type Parameter,例如代码:
class Foo<T>{}

其中 Ttype parameter

  • Type Argument 例如代码:
Foo<String> foo = new Foo<>();

其中 String就是 type argument

  • Parameterized Type 一般将泛型类的调用称之为 parameterized type,例如代码:
Foo<String> foo;

其中 Foo<String>就是 parameterized type

  • Raw Type 例如代码:
class Foo<T>{}

// 那么 Foo 就是泛型 Foo<T> 的 Raw Type
Foo foo = new Foo();

注意:非泛型的 class 或 interface ,则不存在所谓的 Raw Type

其实上面的术语也不用硬记,我们可以这样来理解下。

上面的术语 Type 指的是 Generic Type,因为这里的 Type 肯定不是普通的 Type,因为普通的 Type,不存在所谓的 parameter、argument、raw

首先 Generic Type 强调的是类型(Type),所以,Generic Type 所表示的是一个类型。Generic 是用来修饰这个 Type 的,例如定义一个普通类 class Foo,class 关键字后面的 Foo 就是一个普通的 Normal Type,要想达到 Generic Type 的效果,在 Type 名字后面加上类型参数即可,如 class Foo<T>,class 关键字后面的 Foo<T> 就变成了 Generic Type,即 Foo<T> 是一个 Generic Type.

class Foo {} // Foo 是一个普通类型 Normal Type
class Foo<T> {} // Foo<T> 是一个 Generic Type

再来看 Type Parameter ,它强调的是 Parameter,这里的 Type 指的是 Generic Type,上面提到 class Foo<T>中的 Foo<T> 是一个 Generic Type,那 Foo<T> 中的 T 叫什么,这个 T 就是 Type Parameter,也可以称之为 Type variable。
在计算机术语中,我们一般把 parameter 称之为形参,argument 称之为实参,例如:

// 方法定义处
// num1 和 num2 是形参 parameter
private int add(int num1, int num2){
	return num1 + num2;
}

public static void main(String[] args){
    // 方法调用处
	// 这里的 1,2 就是实参 argument
    int result = add(1,2);
}

既然有 type parameter,那应该也有 type argument.

下面就来看下 Type Argument ,这里的 Type 指的是 Generic Type,上面提到 argument 是调用时传递的参数,那么 type argument 就是调用泛型类型(Invoking a Generic Type)的时候传入的参数:

// Foo<String> 就是在调用泛型类型
// String 就是 type argument
Foo<String> foo = new Foo<>();


再来看下 Parameterized Type ,这里强调的是 Type(类型)。 Type 指的是 Generic Type。那什么是 Parameterized(它是 Parameterize 的过去式,Parameterize 是“用参数表示;确定……的参数”意思),Parameterized Type 可以理解为“type parameter 是具体类型(确定的)的 Generic Type”:

class Foo<T> {}

// 调用 GenericType Foo<T>
Foo<String> foo = new Foo<>(); // Foo<String> 是一个 Parameterized Type

最后来看下 Raw Type,这里强调的是 Type,Type 表示的 Generic Type,完整就是 Raw Generic Type,表示“未加工的泛型类型”。例如 Foo<T> 是一个 Generic Type,未加工的 Foo<T>,就是 Foo

// Foo<T> 是一个 Generic Type
// Raw Foo<T> 就是 Foo
class Foo<T> {}

关于第二点:帮我我们更好的掌握泛型的反射相关的知识

掌握了上面泛型的术语,泛型相关的反射 API 也就很容易懂了,举个例子:

public class ParameterizedTypeTest {
    Map<String, Integer> map;
    public static void main(String[] args) throws Exception {
        Field f = ParameterizedTypeTest.class.getDeclaredField("map");
		// java.util.Map<java.lang.String, java.lang.String>
        System.out.println(f.getGenericType());
        ParameterizedType pType = (ParameterizedType) f.getGenericType();
         // interface java.util.Map
        System.out.println(pType.getRawType());
        // class java.lang.String
        // class java.lang.Integer
        for (Type type : pType.getActualTypeArguments()) {
            System.out.println(type);
        }
    }
}

除了上面 ParameterizedType 类,泛型反射相关的类还有:

  • TypeVariable
  • GenericArrayType
  • WildcardType

有界类型参数

有界类型参数(Bounded Type Parameter)用来对 Argument Parameter 进行限制的,例如上面的例子:

public final <T> T findViewById(int id) {
    //...
}

通过这个方法名字我们知道,该方法返回一个 View 类型的对象的,但是我们在调用这个方法的时候,可以用任意类型的变量来接受这个方法的返回值:

int a = findViewById(R.id.test)

那有没有一种方法做到只能使用 View 类型的变量来接收 findViewById 的返回值,这个时候就可以使用 Bounded Type Parameter,对上面的方法进行如下改在:

public final <T extends View> T findViewById(int id) {
    //...
}

这样的话只能使用 View 或者 View 的子类类型的变量才能接收 findViewById 方法的返回值,这样就可以避免一些低级错误的出现。
有界类型参数是通过 extends 关键子来实现的,也可以称之为上界(Upper Bounded)。关于 extends 和 Upper Bounded 下面还会介绍到。

上面的代码只有一个限定条件,其实可以有多个限定条件,例如:

<T extends B1 & B2 & B3>

多限定类型参数,要求 Argument type 是所有限定类型的子类,如果某个限定类型是 class 类型,那么它必须放在类型参数列表的第一个:

class ClassBound {
}

interface IBound {
}

interface IBound2 {
}

// 继承或实现所有的限定类型
class ArgumentP extends ClassBound implements IBound,IBound2 {
}
// 定义多限定参数类型
class MultipleBoundTypeParameter<T extends ClassBound & IBound & IBound2> {
}

// 使用多限定参数类型
MultipleBoundTypeParameter<ArgumentP> p2 = new MultipleBoundTypeParameter<>();

泛型中的继承和子类

我们来定义如下类:

class Box<T> {
    public void add(T t) { }
}

使用 Number 作为类型参数(Type Argument):

Box<Number> box = new Box<>();
// Box.add 的参数是 Number 类型,如 public void add(Number t)
box.add(1);   // 因为 Integer extends Number
box.add(1.1);  // 因为 Double extends Number

需要注意的是 Type Argument 之间的继承关系,不会延伸到 Parameterized Type,什么意思呢?例如 Box<Number> 的 Type Argument 是 Number,它和 Box<Integer> 的 Type Argument Integer,它们有继承关系,但是不会导致 Box<Number>Box<Integer> 有继承关系:

private static void test(Box<Number> box){
   // ...
}

Box<Integer> boxInteger = new Box<>();
// 下面代码编译报错,因为 Box<Number> 不是 Box<Integer> 的 父类
// 他们不是 is-a 关系
// test(boxInteger); 

我们声明一个 Box 的子类 BoxSubtype,然后调用 test 方法:

class BoxSubtype<T> extends Box<T>{
}

BoxSubtype<Number> boxSubtype = new BoxSubtype<>();
// 编译不报错
test(boxSubtype);         

BoxSubtype<Integer> boxSubtype2 = new BoxSubtype<>();
// 编译报错
test(boxSubtype2);

可以看出 BoxSubtype<Number>Box<Number> 的子类型,但是 BoxSubtype<Integer>不是 Box<Number> 的子类型。

Box<Integer> is not Box<Number>
BoxSubtype<Integer> not is a Box<Number>

BoxSubtype<Integer> is a Box<Integer>

同理,List<Integer> 也不是 List<Number>子类型,但是 List<String>Collection<String> 的子类型,如下图所示:

我们也可以把这种 List<Integer> 不是 List<Number>子类型的现象称之为泛型的不变性(invariance)。泛型除了不变性还有协变性和逆变性,后面我们会介绍到。

根据泛型的不变性,我们无法将一个 List<Integer> 的变量赋值给 List<Number> 的变量。但是有的时候我们需要达到类似的效果,例如:

// 打印数字
public static void printNumbers(List<Number> list){
	//...
}

public static void main(String[] args) {
    List<Integer> list= new ArrayList<>();
    list.add(1);
    printNumbers(list); // 编译器报错
}

要想编译器不报错,该怎么改造呢?这需要介绍到泛型协变(Java中也称之为下界通配符),在介绍下界通配符之前,我们先来看看什么事通配符。

通配符

在 Java 中,通配符表示一种未知类型,使用问号(?) 表示。例如:

public static void printList(List<?> list){
   //...     
}

泛型通配符一般用于泛型类的调用,例如 List<?> list
但是通配符不能用作调用泛型方法的类型参数(Argument Type)、泛型类的实例创建、superType,如:

// 不能用于创建泛型类对象
new ArrayList<?>(); 

// 不能用于调用泛型方法
genericMethod(?);  

// 不能用于 super class
interface MyList<T> extends List<?> 
interface MyList<T> implements List<? extends T>

无界通配符(Unbounded Wildcards)

上面的 printList(List<?> list) 函数里的问号就是无界通配符。通配符表示一种未知类型,上面的 printList(List<?> list) 方法可以打印元素为任意类型的 List 集合,既然是打印任意元素类型的 List,那我们是不是也可以改成 printList(List<Obejct> list),使用 Object 代替通配符。
其实 printList(List<?> list) 和 printList(List<Obejct> list) 还是不同的,先看如下代码:

printList(Arrays.asList("String1","String2")); //List<String>
printList(Arrays.asList(1,2,3,4)); //List<Integer>

可以看出调用 printList(List<?> list) 方法可以接收 List和 List等任意 List 类型的参数. 我们在定义一个参数为 List printList2 方法:

public static void printList2(List<Object> list){
   //...     
}

我们再传递任意类型的List参数给 printList2 方法:

printList2(Arrays.asList("String1","String2")); // 编译报错
printList2(Arrays.asList(1,2,3,4)); //List<Integer>  // 编译报错

为什么调用 printList2 会报错呢?因为 List<String>List<Integer> 不是 List<Object> 的子类。但是 List<String>List<Integer>List<?> 的子类,所以调用 printList 没有问题。

那什么时候使用无界通配符呢?官方文档给了两种情形:

  • 如果实现的方法中,只需要用到 Object 中的定义的方法。通配符在编译器看来就是 Object 类型。
  • 如果在泛型类中不依赖类型参数(Type Parameter)。例如 Class<T>,用它的时候绝大多数都是 Class<?>,因为大部分情况不关心传递进来的 Type Argument 是什么类型。

对于上面的两种情形可以归结为一种情形:因为任何类型都是 Object 的子类,所以如果在实现方法的时候只用到了 Object 方法,说白了也就是不关心实际的类型是什么,这跟第二种情况本质上是一样的。
例如我们编写了一个方法叫做 getClassFromRouters,用于从路由表里获取路由对应页面的 Class:

public static Class getClassFromRouters(String router){
    // ...
}

此时编译器会报 Warning:Raw use of parameterized class ‘Class’,怎么消除这个 Warning:

public static Class<?> getClassFromRouters(String router){
    // ...
}

Object 中的 getClass 方法也是使用了通配符"?"

上界通配符(Upper Bounded Wildcards )

上界通配符也称之为泛型的协变(covariance=co+variance),不同的编程语言不同的叫法,本质上是一样的。

以上面泛型不变性的代码为例:

// 打印数字
public static void printNumbers(List<Number> list){
	//...
}

public static void main(String[] args) {
    List<Integer> list= new ArrayList<>();
    list.add(1);
    printNumbers(list); // 编译器报错
}

为了能让 printNumbers 打印 List<Integer>,我们可以使用上界通配符进行改造.
上界通配符的语法:在通配符 “?”后面加上 extends 关键字,然后紧接着 Type 类型:

// 使用上界通配符
public static void printNumbers(List<? extends Number> list){
    //...
}
public static void main(String[] args) {
    List<Integer> list= new ArrayList<>();
    list.add(1);
    printNumbers(list); // ok
}

只要 List 里元素类型是继承自 Number 的都可以将该 list 传递给 printNumbers

为了更好的说明上界通配符(协变性)和下界通配符(逆变性),就不使用 Number 和 Integer 了,因为它们的继承关系有限。
我们自定义几个Bean,它们的继承关系如下:

public class Thing {}                   // 物
public class Food extends Thing {}      // 食物
public class Fruit extends Food {}      // 水果
public class Apple extends Fruit {}     // 苹果
public class FujiApple extends Apple {} // 红富士

演示泛型不变性:

// 打包
public static void pack(List<Fruit> list){
    //...
}

public static void main(String[] args) {
    List<Apple> list= new ArrayList<>();
    list.add(new Apple());
    pack(list); // 编译器报错
}

演示泛型协变性(上界通配符):

// 打包
public static void pack(List<? extends Fruit> list){
    //...
}

public static void main(String[] args) {
    List<Apple> list= new ArrayList<>();
    list.add(new Apple());
    pack(list); // ok
    
    List<FujiApple> list2= new ArrayList<>();
    list2.add(new FujiApple());
    pack(list2); // ok
}

为什么称之为上界呢?这个不用硬记,看下面一张图,就明白了:
image.png

下界通配符(Lower Bounded Wildcards)

下界通配符也称之为泛型的逆变性(contravariance=contra+variance),不同的编程语言不同的叫法,本质上是一样的。
下界通配符和上界通配符语法类似,将 extends 关键字改成 super 即可:<? super Type>
下界通配符限定的未知类型只能是 Type 或者 Type 的父类。例如:

// 打包
public static void pack(List<? super Fruit> list){
    //...
}

public static void main(String[] args) {
    List<Apple> list= new ArrayList<>();
    list.add(new Apple());
    pack(list); // 编译器报错

    List<FujiApple> list2= new ArrayList<>();
    list2.add(new FujiApple());
    pack(list2); // 编译器报错
}

将 List 的类型参数改成 Fruit 或 Fruit 的父类即可:

public static void main(String[] args) {
    List<Thing> list= new ArrayList<>();
    list.add(new Thing());
    pack(list); // ok

    List<Food> list2= new ArrayList<>();
    list2.add(new Food());
    pack(list2); // ok
}

为什么称之为下界呢?这个不用硬记,看下面一张图,就明白了:
image.png

PECS(Producer Extends, Consumer Super)

讲完了上界通配符(协变)和下界通配符(逆变),那我们怎么使用呢?
再讲怎么使用原则之前,我们继续看下上界通配符(协变)和下界通配符(逆变)的特点。

上界通配符特点:只能读、不能改

// 打包
public static void pack(List<? extends Fruit> list){
    // 如果超过10个,则赠送一个水果
    if (list.size() > 10) {
        list.add(new Fruit());     // 编译报错    
    }
    Fruit fruit = list.get(0);     // ok
    // ...
}

如果我想修改怎么办?
下界通配符特点:只能改,不能读

    // 打包
    public static void pack(List<? super Fruit> list){
        // 如果超过10个,则赠送一个水果
        if (list.size() > 10) {
            list.add(new Fruit());     // ok
        }
        Fruit fruit = list.get(0);     // 编译报错
        // ...
    }

所以可以得出它们的使用原则:

  • 上界通配符,只读,不能写
  • 下界通配符,只写,不能读

我们可以看下 JDK 源码中是怎么使用的:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}

Collections.copy 的功能是将一个集合(src)的元素拷贝到另一个集合中(dest)。那么目标集合 dest 是需要修改的,但是不要获取里面的元素;而源集合 src 它需要获取里面的元素,但是不能修改。Collections.copy 方法天然适合使用泛型通配符来实现。如果使用普通泛型则达不到这样的效果。

上面的使用原则可能比较难记,业界有些人将其提炼为:PECS(Producer Extends, Consumer Super)

容易混淆的语法

至此,泛型通配符我们就介绍完毕了,但是在实际开发中我们很容易将泛型的几个概念混淆:

  1. <T> VS <?>
    <T><?> 是不是很相像,但是他们是不同的概念,<T> 是一个 type parameter (type variable),<T> 用于定义泛型类或泛型方法。而 <?> 是一个泛型通配符,它不能用于定义泛型类或泛型方法。通常用于泛型类的调用,如 Class<?> klass

  2. <T extends Type> VS <? extends Type>
    <T extends Type><? extends Type> 也很相像。<T extends Type> 是有界类型参数(Bounded Type Parameter),用于限定 Type Argument 的。<? extends Type> 用于限定未知类型必须是 Type 或者继承自 Type。它们使用场景也不一样,<T extends Type> 用于定义泛型类或泛型方法。泛型通配符 <? extends Type> 一般用于泛型类的调用,如 List<? extends String> list

reference

https://docs.oracle.com/javase/tutorial/java/generics/types.html

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Chiclaim

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值