Java 泛型详解

本文深入探讨Java泛型的概念、用途及其实现原理。包括类型推断、类型参数、协变和逆变等核心概念,以及如何在类、接口和方法中使用泛型。此外,还讨论了泛型在Java中的实现机制,如类型擦除及其带来的限制。

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

概念


泛型

泛型在编程语言中出现的最初的目的是希望类或方法具有最广泛的表达能力。通过解耦类或者方法与所使用的类型之间的约束来实现。

通常一般的类和方法,只能应用于具体的类型,基本类型或者自定义的类,若要编写应用于多种类型的代码,这种限制会对代码的束缚很大。在Java语言处于还未出现泛型的版本时,只能通过 Object 是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。下面是一个容器代码实现部分代码。

public class Container {
    private Object obj;

    /**
     * @return the obj
     */
    public Object getObj() {
        return obj;
    }

    /**
     * @param obj the obj to set
     */
    public void setObj(Object obj) {
        this.obj = obj;
    }
}

虽然上述容器会达到预期效果,但就我们的目的而言,它并不是最合适的解决方案。它不是类型安全的,并且要求在检索封装对象时使用显式类型转换,因此有可能引发异常。通过泛型可以很好的解决这些问题。

Java 泛型(generics)是 JDK 1.5 中引入的一个新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,在实例化时为所使用的容器分配一个类型,这样就可以创建一个对象来存储所分配类型的对象。
所分配的类型将用于限制容器内使用的值,这样就无需进行类型转换,还可以在编译时提供更强的类型检查。类型参数的魅力在于让程序具有更好的可读性和安全性。
这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类泛型接口泛型方法

类型推断

官方给出的定义是:

Type inference is a Java compiler’s ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation applicable. The inference algorithm determines the types of the arguments and, if available, the type that the result is being assigned, or returned. Finally, the inference algorithm tries to find the most specific type that works with all of the arguments.

翻译过来便是:类型推断是 Java 编译器查看每一个方法调用和相关声明,以确定类型参数(或参数),使调用可用。推理算法确定参数类型,如果类型推断成功,那么方法返回的值就是那个类型的。最后,推理算法试图找到与所有变量的最具体类型。

观察下面的代码:

static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());

编译器可以从以上代码中推导出 pick 的两个参数都是 Serializable 类型。

类型参数

类型参数(又称类型变量)用作占位符,指示在运行时为类分配类型。根据需要,可能有一个或多个类型参数,并且可以用于整个类。根据惯例,类型参数是单个大写字母,该字母用于指示所定义的参数类型。下面列出推荐的标准类型参数:

  • E:元素
  • K:键
  • N:数字
  • T:类型
  • V:值
  • S、U、V 等:多参数情况中的第 2、3、4 个类型

协变和逆变

维基给出的形式化定义如下:

在一门程序设计语言的类型系统中,一个类型规则或者类型构造器是:

  • 协变(covariant),如果它保持了这样的序关系,该序关系是:子类型 ≦ 基类型。
  • 逆变(contravariant),该序关系是:基类型 ≦ 子类型。
  • 不变(invariant),如果上述两种均不适用。

先看下面的类相关定义。

class Fruit {}
class Apple extends Fruit{} 
class Banana extends Fruit{}
class RedFuji extends Apple{}

协变就是符合我们正常逻辑的一种转换关系。如苹果是水果的一种,我们可以称苹果为水果。

Fruit [] f = new Apple[10];

上述在Java中是完全可行的。可见数组是协变的。

// Compile Error
List<Fruit> f = new ArrayList<Apple>();

上述可见,泛型没有内建的协变类型。Apple 的 List 在类型上不等价于 Fruit 的 List,即使 Apple 是一种 Fruit 类型。
泛型中利用通配符实现的协变和逆变:

// 协变
List<? extends Fruit> flist = new ArrayList<Apple>();
// 逆变
List<? super Apple> alist = new ArrayList<Fruit>();

上述协变和逆变在泛型中是完全可行的。后面会解释为什么可行及编译器会对这样的对象进行什么样的限制

边界

边界使得我们可以在泛型的参数类型上设置限制条件,这可以让我们按照边界的类型来调用方法。

通配符

通配符指在泛型表达式中的问号 ‘?’。

  • < ? extends T> : 可以接收 T 类型或者 T 的子类型。规定了上界
  • < ? super T> :可以接收 T 类型或者 T 的父类型。规定了下界
无界
  • < ?> 看起来意味着 “任何类型”,可等价于使用原生类型。

< ?> 增加了可读性,可解读为作者想使用泛型来编写这段代码,并不是想用原生类型,虽然这个时候泛型参数可以持有任何类型,只是我们不知道这个类型是什么。

用法


类型参数广泛应用在容器相关的类、接口和方法中。下面以几个案例介绍下类型参数的使用。

泛型类

一个泛型类(generic class)就是具有一个或多个类型变量的类。在类名后,用尖括号(<>)括起来,并将类型变量写在里面,可有多个类型变量。
ArrayList 和 HashMap 的类泛型定义如下:

// ArrayList 泛型类的定义
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    ...
}

// HashMap 泛型类定义
public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{
    ....
}

下面使用泛型实现一个链栈,定义了一个内部类 Node

/**
 * Created by manuu on 17-7-25.
 */
public class LinkedStack<T> {
    private class Node<U> {
        U item;
        Node<U> next;

        Node() {
            item = null;
            next = null;
        }

        Node(U item, Node<U> next) {
            this.item = item;
            this.next = next;
        }

        boolean end() {
            return item == null && next == null;
        }
    }
    // 设置一个哨兵
    private Node<T> top = new Node<T>();

    public void push(T item) {
        top = new Node<T>(item, top);
    }
    // 返回 T 类型
    public T pop() {
        if (!top.end()) {
            T tmp = top.item;
            top = top.next;
            return tmp;
        }
        return null;
    }

    public T peek() {
        if (!top.end()) {
            return top.item;
        }
        return null;
    }

    public static void main(String[] args) {
        // 设置 String 为类型
        LinkedStack<String> stack = new LinkedStack<>();
        stack.push("A");
        stack.push("B");
        stack.push("C");
        String s;
        while ((s = stack.pop()) != null) {
            System.out.print(s);
        }
    }
}
/* Output:
CBA
*/

泛型接口

泛型接口的定义和泛型类相似。List 和 Map 接口的泛型定义如下:

// List 接口的泛型定义
public interface List<E> extends Collection<E> {
    ...
}
// map 接口的泛型定义
public interface Map<K,V> {
    ...
}

泛型方法

泛型方法的类型变量是放在修饰符后面,返回类型的前面。泛型方法可以定义在普通类中,也可以定义在泛型类中。下面是一个泛型方法的例子:

public static <T> T addAndReturn(T element, Collection<T> collection){
    collection.add(element);
    return element;
}

在调用泛型方法时候,可以显式的设定类型,也可以让编译器通过类型推断来判定

泛型限定

有时,类、接口或方法需要对类型变量加以约束。

考虑这样的情况,需要对类型参数声明的变量进行方法调用,如果只使用类型参数T,这意味着可以是任何一个类的对象。这个时候需要指定泛型类型,但希望控制可以指定的类型,而非不加限制。有界类型,在类型参数部分指定 extends 或 super 关键字,分别用上限或下限限制类型,从而限制泛型类型的边界。
使用的时候需要注意以下事项:
1. 不管该限定是类还是接口,统一都使用关键字 extends。
2. 可以使用 ‘&’ 符号给出多个限定。
3. 如果限定既有接口也有类,那么类必须只有一个,并且放在首位置。

通配符

泛型限定过程中利用通配符进行类型转换的时候,需要注意的事项。

协变

先看下面代码:

public class GenericsAndCovariance {
    public static void main(String [] args) {
        List<? extends Fruit> flist = new ArrayList<Apple>();
        // Compile Error : can't add any type of object
        // flist.add(new Apple());
        // flist.add(new Fruit());
        // flist.add(new Object());
        flist.add(null);// 编译器允许,但无意义。
        Fruit f = flist.get(0);
    }
}

从上述代码中可知,通过通配符实现了协变,虽然通配符继承自Fruit, 并不意味着这个 List 可以持有任何类型的 Fruit,在某种意义上可以看成 flist 引用没有指定具体类型。原来这个 List 持有 Apple 这样的指定的类型,但是为了向上转型为 flist,这个类型原来是什么并没那么重要了。
< ? extends Fruit> 意味着从这个列表里读出一个 Fruit 是安全的,这个列表里的所有对象至少是一个 Fruit,并且可能是从 Fruit 里导出的某种对象。

在上述代码指定了 ArrayList< ? extends Fruit>时,add() 的参数也变成了 “? extends Fruit”,这意味它可以是任何事物,这个时候编译器并不知道需要的 Fruit 是哪一个子类型,因此它不会接受任何类型的 Fruit,因为编译器无法验证 “任何事物”的类型安全性。

一旦执行了向上转型,将丢失掉向其中传递任何对象的能力,甚至 Object 也不行。

逆变

看下面的代码:

public class GenericsAndContravariant {
    static void writeTo(List<? super Apple> apples) {
        apples.add(new Apple());
        apples.add(new RedFuji());
        //  apples.add(new Fruit()); // Error
    }
}

从上述代码可知,apples 是Apple的某种基类型的List,Apple 作为下界,所以我们向里面传递 Apple 及 Apple 的导出的任何对象是安全的。但是我们不能向内部添加 Fruit 类型,这是不安全的。

PECS

用法可以总结成:PECS ( Producer Extends,Consumer Super )。

假设你有一个 List 相关的容器,现在你想灵活的对此容器进行操作。
1. 如果你是想遍历 List,并对每一项元素操作时,此时这个容器是 Producer(生产元素),应该使用 List< ? extends Thing>。
2. 如果你是想添加元素到 List,那么此时容器是 Consumer(消费元素)List< ? super Thing>。

实现原理


先看下面的代码:

public class ErasedTypeEquivalence {
    public void main(string [] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.print(c1 == c2);
    }
}
/* Output
true
*/

由上述结果可知,ArrayList 和 ArrayList 在运行时事实上是相同的类型。这是由于这两个泛型在编译后都替换成了原始的类型。

实现原理:无论何时定义一个泛型类型,都自动提供了一个相应的原始的类型,原始类型就是删去类型参数后的泛型类型名。擦除类型变量,并替换成限定类型(无则为 Object)。
要明确一点的是,在泛型代码内部,无法获得任何有关泛型参数类型的信息。

类型擦除

在使用泛型时,任何具体的泛型信息都被擦除。这点上和 C++ 有很大的区别。C++ 为每个模板的实例化产生不同的类型。
在类型擦除的过程中,若有多个边界,此时将会擦除到它的第一个边界。

擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为”迁移兼容性“。

采用擦除的原因

泛型是在 JDK1.5后才加入的,为了实现迁移兼容性,Java 的设计者们采用了类型擦除的方案。通过允许非泛型代码和泛型代码共存,擦除使这个向泛型的迁移成为了可能。然而擦除减少了泛型的泛化性。这是 Java 型实现的一种折中。

在基于擦除的实现泛型方案中,泛型类型被当作第二类类型处理,既不能在某些重要的上下文环境中使用的类型。泛型类型只有在静态类型检查期间才能出现,在此之后所有的泛型类型都会被擦除,替换成它们的非泛型上界。

注意事项


大多数限制都是由采用擦除方案引起的。

任何基本类型都不能作为类型参数

这是因为擦除后数据类型变为了 Object,而 Object 并不能表示基本数据类型。然而 Java 提供了基本数据类型的包装器。

泛型不能应用于运行时类型查询操作

不能将泛型进行转型,instanceof 操作和 new 表达式。因为所有有关参数的类型信息都丢失了。

关于 instanceof 不能使用的解决方案可以在泛型内部设置一个类型标签。然后动态的调用isInstance()。如下

public class ClassType<T> {
    Class<T> kind;
    ...

    public boolean isInstance(Object arg) {
        return kind.isInstance(arg);
    }
}

对于下面代码编译是行不通的。

T t = new T()

行不通的部分原因是因为擦除,还有部分因为编译器不能验证 T 是否具有默认的构造器。这种操作在 C++ 中很自然安全,它是在编译期检查的。
这个解决方式可以使用工厂模式,最便利的工厂对象就是Class对象,可以使用newInstance() 来创建这个类型的新对象。如下所示:

class ClassAsFactory<T> {
    T x;
    public ClassAsFactory(Class<T> kind) {
        try {
            x = kind.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

这个方法,可能会导致运行期异常,针对这个可以编写显式的工厂来获得编译期检查。

泛型数组

不能实例化泛型数组。
通常的解决方案是通过使用ArrayList,这样可以获得数组的行为,以及由泛型提供的编译期的类型安全。举例如下:

public class ListOfGenerics<T> {
    private List<T> array = new ArrayList<T>();
    public void add(T item) { array.add(item); }
    public T get(int index) { return array.get(index); }
    ...
}

重载

下面的程序是不能编译的:

public class UseList<W, T> {
    void f(List<W> v){}
    void f(List<T> v){}
}

由于擦除的原因,重载的方法将产生相同的类型签名。
当被擦除的参数不能产生唯一的参数列表时,必须提供明显区别的方法名。

多态与泛型

先看以下代码:

class DateTest extends pair<Date> {
    public void setFisrt(Date fisrt) {
        if (fisrt.compareTo(getFisrt()) >= 0) {
            super.setFisrt(fisrt);
        }
    }
}
class pair<T> {
    private T fisrt;
    public pair(T fisrt)  {
        fisrt = fisrt;
    }
    public void setFisrt(T newValue) {
        fisrt = newValue;
    }
    public T getFisrt() {
        return fisrt;
    }
}

DateTest 继承自泛型类 pair,并且实现了 setFisrt 方法。按照擦除的方案,所有的参数类型都被替换成原始类型了。当我们用父类对象指向子类对象,并且调用 setFisrt 方法时,这个时候会成功调用子类的此方法,完成多态的特性。这是因为编译器会在 DateTest 中生成一个桥方法 ( bridge method )。
通过javap -c DateTest 获得以下代码:

public void setFisrt(java.util.Date);
    Code:
       0: aload_1       
       1: aload_0       
       2: invokevirtual #2                  // Method getFisrt:()Ljava/lang/Object;
       5: checkcast     #3                  // class java/util/Date
       8: invokevirtual #4                  // Method java/util/Date.compareTo:(Ljava/util/Date;)I
      11: iflt          19
      14: aload_0       
      15: aload_1       
      16: invokespecial #5                  // Method pair.setFisrt:(Ljava/lang/Object;)V
      19: return        

  public void setFisrt(java.lang.Object);
    Code:
       0: aload_0       
       1: aload_1       
       2: checkcast     #3                  // class java/util/Date
       5: invokevirtual #6                  // Method setFisrt:(Ljava/util/Date;)V
       8: return        
}

从上述可知,在编译器生成的桥方法 setFisrt(java.lang.Object) 中调用了子类中的 setFisrt 方法。
桥方法还应用在重写方法中,当一个方法覆盖另一个方法时候,可以指定一个更严格的返回类型时。( 具有协变的返回类型 )

实现泛型接口

一个类不能实现同一个泛型接口的两种变体。由于擦除的存在,这两种变体会变成相同的接口。

类型变量在静态上下文无效

不能在静态域或方法中的引用类型变量。例如下面会编译不通过:

public class Singleton<T> {
    private static T instance; //ERROR
    public static T getInstance() // ERROR
    {
        return instance;
    }
}

由于擦除,以及静态域和非静态域的工作方式,导致禁止使用带有类型变量的静态域和方法。静态域的成员是独立于对象的,而类型变量须在对象声明的时候进行绑定,所以这是不可取的。
然而这个需要和静态泛型方法有所区分。泛型方法中的泛型指的是方法中的参数变量,而不是泛型类中的参数变量。所以静态泛型方法是可取的。

不能抛出或捕获泛型类的实例

既不能抛出也不能捕获泛型类的对象。实际上,甚至泛型类扩展Throwable都是不合法的。
下面的代码不能正常编译:

public class Problem<T> extends Exception { ... } //ERROR

catch 子句中不能使用类型变量。

public static <T extends Throwable> void doWork(Class<T> t){  
    try {  
        ...  
    } catch(T e){ //编译错误  
        ...  
    }  
}  

由于 擦除会替换为Throwable,后面会和捕获的子类会发生冲突,Java为了避免这种冲突,直接禁止在 catch 中使用类型变量。

不过可以在异常规范中使用类型变量。以下的方法是合法的:

public static<T extends Throwable> void doWork(T t) throws T{  
    try{  
        ...  
    }catch(Throwable realCause){  
        t.initCause(realCause);  
        throw t;   
    }
}

参考资料


《Java 编程思想 第四版》
《深入理解 Java 虚拟机》
《Java 核心技术卷一 第九版》
https://zh.wikipedia.org/wiki/协变与逆变

<think>嗯,用户想了解Java详解。首先,我需要回忆一下Java的基本概念和应用场景。Java 5引入的特性,主要用于增强类型安全,减少类型转换的麻烦,以及提高代码的可重用性。用户可能是在学习过程中遇到了相关的困惑,或者在实际开发中需要更深入的理解。 接下来,用户的需求可能包括了解的基本语法、类型擦除、通配符、方法、类与接口,以及使用限制等。需要分步骤讲解,确保结构清晰。可能需要从为什么需要开始,然后逐步展开各个部分。 要注意的是,用户可能对类型擦除这一Java的实现机制不太清楚,需要详细解释。同时,通配符部分,尤其是上下界通配符的区别和使用场景,可能是容易混淆的地方,需要举例说明。此外,在集合框架中的应用是一个常见的例子,可以用来帮助用户理解。 还需要考虑用户可能的误区,比如在运行时类型信息被擦除的问题,导致无法进行instanceof判断或者创建数组。这些限制需要明确说明,避免用户在实际使用中犯错。 另外,用户可能对方法与类的区别不太清楚,需要分别举例说明。比如,方法可以在普通类中定义,而类则是整个类作用于类型参数。 最后,要确保语言通俗易懂,避免过于技术性的术语,同时结合代码示例,让用户能够通过实例理解抽象的概念。检查是否有遗漏的重要知识点,比如继承、通配符捕获等,但可能根据用户的提问深度决定是否展开。</think>以下是对Java的系统化详解,分步骤解析核心概念和用法: --- ### 一、解决的问题 1. **类型安全** 在集合等场景中,编译时检测类型错误,避免运行时`ClassCastException` ```java // 非写法(存在隐患) List list = new ArrayList(); list.add("abc"); Integer num = (Integer) list.get(0); // 运行时异常 // 写法(编译时报错) List<String> list = new ArrayList<>(); list.add(123); // 编译错误 ``` 2. **消除强制类型转换** 明确容器存储类型,减少冗余代码 --- ### 二、基础语法 #### 1. 类 ```java public class Box<T> { private T content; public void setContent(T content) { this.content = content; } public T getContent() { return content; } } ``` - `T`为类型参数(可任意命名,常用`T`, `E`, `K`, `V`等) - 实例化时指定具体类型: ```java Box<Integer> intBox = new Box<>(); ``` #### 2. 接口 ```java public interface Comparator<T> { int compare(T o1, T o2); } ``` #### 3. 方法 ```java public <T> void printArray(T[] array) { for (T element : array) { System.out.println(element); } } ``` - 类型参数声明在方法返回值前 --- ### 三、类型擦除(关键机制) 1. **编译时类型检查** 编译器检查类型合法性 2. **运行时类型擦除** 生成的字节码中不包含类型信息,替换为原始类型 ```java List<String> list = new ArrayList<>(); // 编译后等效于 List list = new ArrayList(); ``` 3. **桥方法生成** 用于保持多态性(如继承类时) --- ### 四、通配符 #### 1. 上界通配符 `<? extends T>` ```java void process(List<? extends Number> list) { // 允许读取为Number类型 Number num = list.get(0); // 不允许添加元素(编译器无法确定具体子类型) } ``` #### 2. 下界通配符 `<? super T>` ```java void addNumbers(List<? super Integer> list) { list.add(42); // 允许添加Integer及其父类元素 Object obj = list.get(0); // 读取只能作为Object } ``` #### 3. 无限定通配符 `<?>` ```java void printList(List<?> list) { for (Object elem : list) { System.out.println(elem); } } ``` --- ### 五、重要限制 1. **无法实例化类型参数** ```java T obj = new T(); // 编译错误 ``` 2. **不能创建数组** ```java T[] array = new T[10]; // 编译错误 ``` 3. **静态上下文中不可用** ```java public class Test<T> { private static T instance; // 错误 } ``` 4. **instanceof检测无效** ```java if (list instanceof ArrayList<String>) { ... } // 编译错误 ``` --- ### 六、高级技巧 1. **类型参数多重限定** ```java <T extends Comparable & Serializable> ``` 2. **与反射** ```java Type type = new TypeToken<Map<String, Integer>>(){}.getType(); ``` 3. **PECS原则** Producer-Extends, Consumer-Super (适合选择通配符边界的场景) --- ### 七、典应用场景 1. 集合框架(`ArrayList<E>`, `HashMap<K,V>`) 2. 工具类方法(`Collections.sort()`) 3. 回调接口设计(`Comparator<T>`) 4. 自定义通用数据结构 --- 通过掌握这些核心概念,可以写出类型更安全、复用性更强的Java代码。建议结合具体项目实践加深理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值