Java泛型

泛型介绍

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

(感觉和c++的模板很像,功能和写法都很类似)

假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?

答案是可以使用 Java 泛型

使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。

▪ 以ArrayList<E>、ArrayList<Integer>为例1. ArrayList<E>定义了一个泛型类型,"E"称为"类型变量"或"类型参数"。

  1. ArrayList<Integer>称为"参数化的类型","Integer"称为"实际类型参数"。

  2. ArrayList称为泛型类型ArrayList<E>的"原始类型(raw type)"。

可以通过泛型对类型的指定,来避免出现给一个数组输入不同类型的情况出现,因为泛型会在编译阶段就不通过编译。

java 复制代码
import java.lang.reflect.InvocationTargetException;
import java.util.*;

public class GenericList {
    public static void main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        //创建一个只想保存字符串的List集合
        List<String> strList = new ArrayList<String>();
        strList.add("One string");
        strList.add("Two string");
        strList.add("Three string");
        //下面代码将引起编译错误
        //strList.add(5);
        //但使用反射可以绕开编译器的语法检查
        //strList.getClass().getMethod("add", Object.class).invoke(strList, 5);
        for (int i = 0; i < strList.size(); i++) {
            //下面代码无需强制类型转换
            String str = strList.get(i);
            System.out.println(strList.get(i));
        }

    }
}

泛型方法

你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

下面是定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔 ),该类型参数声明部分在方法返回类型之前(在下面例子中的**<E>**)。
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型 ,不能是原始类型(像 int、double、char 等)。因此,Pair<double>是错的,只能是Pair<Double>。

可以在普通类或泛型类中定义泛型方法:

java 复制代码
class ArrayAlg {
 public static <T> T getMiddle(T[] a) {
 return a[a.length / 2];
 }
}

使用泛型方法:(下面两个是等价的)

String\[\] names = ... ;

String middle = ArrayAlg.<String>getMiddle(names);
String middle = ArrayAlg.getMiddle(names);

泛型的类型自动推断

假设你有一个泛型方法,如下所示:

java 复制代码
public class GenericMethod {
    // 泛型方法
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

这个方法接受一个类型为T的数组,并打印出数组的每个元素。

当你调用这个方法时,可以直接传入参数,Java编译器会根据你传入的参数类型来推断T的具体类型。例如:

java 复制代码
public class Main {
    public static void main(String[] args) {
        // 创建不同类型的数组
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] strArray = {"Hello", "World"};
        
        // 自动推断类型
        GenericMethod.printArray(intArray); // 输出: 1 2 3 4 5 
        GenericMethod.printArray(strArray); // 输出: Hello World
    }
}

在这个例子中:

  1. 当调用printArray(intArray)时,编译器自动推断出TInteger类型。
  2. 当调用printArray(strArray)时,编译器自动推断出TString类型。

当然你也可以手动指定类型,不过在大多数情况下,Java会自动推断类型,手动指定是多余的。

数目可变的泛型方法参数

泛型方法支持使用"..."定义个数可变的参数,你可以定义一个接受可变数量参数的方法,并将这些参数视为数组。结合泛型,能够处理不同类型的可变参数。

可变参数的语法

可变参数使用三个点...来表示,放在参数类型之后。它允许方法接受零个或多个参数,并将它们视为一个数组。

java 复制代码
public class GenericVarargs {
    // 可变参数的泛型方法
    public static <T> void printElements(T... elements) {
        for (T element : elements) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        // 调用可变参数方法,传入不同类型的参数
        printElements(1, 2, 3, 4, 5); // 输出: 1 2 3 4 5 
        printElements("Hello", "World"); // 输出: Hello World 
        printElements(1.1, 2.2, 3.3); // 输出: 1.1 2.2 3.3 
    }
}
  1. 泛型方法定义

    • public static <T> void printElements(T... elements):这里T是类型参数,elements是一个可变参数,表示T类型的数组。
  2. 调用方法

    • 可以直接传入多个参数,Java会自动将它们转换为数组。这个方法可以接受任意数量的参数(包括零个参数)。
  3. 输出结果

    • main方法中,调用printElements方法传入不同类型的参数,显示了如何处理这些参数。

注意事项

  1. 可变参数只能在方法参数列表的最后一个位置:如果定义了多个参数,必须将可变参数放在最后。

  2. 性能考虑:虽然可变参数提供了灵活性,但在处理大量参数时,可能会产生数组的创建和销毁,因此在性能敏感的场景中要谨慎使用。

泛型类

定义了泛型参数的类成为泛型类,泛型参数可用于定义字段类型、方法参数类型和返回值类型。

泛型类的创建

java 复制代码
//泛型参数为T
public class Pair<T> {
    //用T定义字段类型
    private T first;
    private T second;
    public Pair() {
        first = null;
        second = null;
    }
    //用T定义构造方法的参数类型
    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }
    //用T定义函数返回值类型
    public T getFirst() {
        return first;
    }
    public T getSecond() {
        return second;
    }

    //用T定义方法参数类型
    public void setFirst(T newValue) {
        first = newValue;
    }
    public void setSecond(T newValue) {
        second = newValue;
    }
}

泛型类的实例化

前面定义了泛型类Pair<T>,但在使用时,必须给T指定一个具体的类型(比如String)。"Pair<String>"就是"Pair<T>"的"实例化(instantiate )"。

java 复制代码
class PairTest {

    public static void main(String[] args) {
        String[] strs = new String[]{"a", "b", "c"};
        Pair<String> result = minmax(strs);
        System.out.printf("Min:%1$s  Max:%2$s\n",
                result.getFirst(),
                result.getSecond());

    }

    public static Pair<String> minmax(String[] a) {
        if (a == null || a.length == 0) {
            return null;
        }
        String min = a[0];
        String max = a[0];
        for (int i = 1; i < a.length; i++) {
            if (min.compareTo(a[i]) > 0) {
                min = a[i];
            }
            if (max.compareTo(a[i]) < 0) {
                max = a[i];
            }
        }
        return new Pair<String>(min, max);
    }
}

从泛型类派生子类

从泛型基类派生子类时,应该给其指定一个具体的类型

如果MyClass是泛型类,在定义子类时不指定泛型参数,则 MyClass 的泛型参数默认为 Object 。

泛型使用须知

1.不能定义泛型化数组,如下这个语句是错误的:

Pair<String>\[\] table = new Pair<String>10;

  1. 不能直接创建泛型类型的实例,这个语句也是错误的:

public class Pair<T> {

...

public Pair() {

first = new T();

second = new T();

}

...

}

  1. 泛型类型不能直接或间接继承自Throwable,这个语句还是错误的:

(Throwable 类是 Java 语言中所有错误或异常的超类,是对所有异常进行整合的一个普通类。它的作用就是能够提取保存在堆栈中的错误信息。)

public class Problem<T> extends Exception

  1. 我们无法抛出或捕获泛型类型的异常对象,这个语句是错误的:

catch (T e) {...}

  1. 不能定义静态泛型成员,以下代码将无法通过编译:

class MyClass<T> {

public static T value;

public static T f() { }

}

泛型标记符

当你设计自己的泛型类或泛型方法时,最好遵循以下惯例命名泛型参数:(不强制,属于一种技术规范)

  • E - Element (在集合中使用,因为集合中存放的是元素)
  • T - Type(Java 类)
  • K - Key(键)
  • V - Value(值)
  • N - Number(数值类型)
  • - 表示不确定的 java 类型(类型通配符)

泛型多态

在JDK中定义了大量的泛型类型(包容泛型类和泛型接口),并且这些类型之间存在着复杂的(基类)继承和(接口)实现关系。

▪ 基于泛型接口和抽象泛型类,在JDK中大量出现了基于泛型的"多态"代码,把握"泛型多态"特性,是用好它们的前提。

先回顾一下多态的两个特性:

  1. 父类变量,可以引用子类对象。
  2. 接口变量,可以引用实现了这一接口的类的实例。

JDK中泛型类型间的关联实例:

java 复制代码
import java.util.ArrayList;
import java.util.List;


public class GenericPolymorphism {
    public static void main(String[] args) {
        List<Number> nums = new ArrayList<>();
        nums.add(2);
        nums.add(3.14);
        for (Number number : nums) {
            System.out.println(number.getClass().getName());
        }
    }
}
  • 泛型限制 :指定了List<Number>,这意味着集合只能存储Number及其子类的对象,不能存储其他非数字类型的对象,比如StringBoolean。这一点是通过泛型限制实现的。

  • 多态性IntegerDoubleNumber的子类,通过多态性,Java允许将它们的实例存放在List<Number>中。由于List<Number>接收Number类型的对象,任何继承自Number的子类对象都可以存储进去。

但是这个多态性只是对于泛型所指的类型,而不是使用泛型的类本身。所以下面这个例子的语句是不合法的。

java 复制代码
//定义一个类型为Integer的集合
List<Integer> ints = new ArrayList<Integer>();
ints.add(1);//装箱
ints.add(2);//装箱
//因为Integer派生自Number,所以尝试着将List<Integer>
//变量赋值给List<Number>变量
List<Number> nums = ints; // 出现编译时错误

即使IntegerNumber的子类,但List<Integer>List<Number>之间没有父子关系。换句话说,List<Integer>不是List<Number>的子类,也不能将List<Integer>赋值给List<Number>

泛型约束

泛型约束(Generic Constraints)是在使用泛型时,通过限定泛型类型的范围,确保泛型可以操作特定类型及其子类型。Java中的泛型约束主要通过边界来实现,分为上界和下界。

上界约束

上界约束使用extends关键字来限制泛型参数必须是某个类或者接口的子类或实现类。

java 复制代码
class NumberBox<T extends Number> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

在这个例子中,T必须是Number类的子类(如IntegerDoubleFloat等),因此你不能使用String或其他非Number类的对象来实例化NumberBox

java 复制代码
public class Main {
    public static void main(String[] args) {
        NumberBox<Integer> intBox = new NumberBox<>();
        intBox.setContent(123);
        System.out.println(intBox.getContent()); // 输出: 123

        NumberBox<Double> doubleBox = new NumberBox<>();
        doubleBox.setContent(3.14);
        System.out.println(doubleBox.getContent()); // 输出: 3.14
    }
}
使用上界约束的好处
  • 类型安全:确保泛型只能处理某个类或接口的子类,避免不必要的类型错误。
  • 灵活性:你可以操作具有共同父类的对象,而不需要为每个具体类型单独写逻辑。

需要时,可以给泛型参数可以指定多个约束条件:

T extends Comparable & Serializable

多个约束条件中最多只能有一个是类,并且它必须放在第一位。

下界约束(下界通配符)

实际上,Java没有直接的下界约束 ,这与上界约束(T extends SomeClass)不同。Java中的泛型定义不支持用T super SomeClass这样的方式直接约束一个泛型类型必须是某个类的父类。

因为泛型中的上界约束常用于定义泛型类型的能力(如必须是某个类或接口的子类),而下界约束并没有类似的应用场景。下界的限制通常用于限制写入操作 ,而这在方法调用时已经可以通过下界通配符? super T)来表达。

所以通过下界通配符来实现类似下界约束的效果类型通配符。

类型通配符

通配符约束(?)用于表示未知的泛型类型,结合上界和下界,通配符可以在泛型代码中增加灵活性。必须使用?来表示通配符,不能换成其他字母或符号。

无界通配符 <?>

无界通配符<?>表示任意类型,适用于你不关心泛型参数类型的场景。例如,你可以使用无界通配符来读取或操作一个泛型集合,但无法向其中添加元素。

java 复制代码
public static void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}
java 复制代码
public class Main {
    public static void main(String[] args) {
        List<String> stringList = Arrays.asList("a", "b", "c");
        List<Integer> intList = Arrays.asList(1, 2, 3);
        
        printList(stringList);  // 输出: a b c
        printList(intList);     // 输出: 1 2 3
    }
}

注意事项:

  • 你可以从List<?>中读取元素,但读取出来的元素类型是Object
  • 不能List<?>中添加元素,因为它可以接受任意类型,Java无法确保你添加的元素类型是安全的。
java 复制代码
list.add(1); // 错误,不能添加任何元素

上界通配符 <? extends T>

上界通配符<? extends T>表示某个类型的子类 或该类型本身。它限制了泛型参数的上界,即泛型参数必须是指定类型T或它的子类。

java 复制代码
public static void printNumbers(List<? extends Number> list) {
    for (Number num : list) {
        System.out.println(num);
    }
}

只能读取,不能写入 :虽然你可以从List<? extends T>中读取元素,但是你不能往里面添加元素(除了null)。这是因为泛型参数可以是T的任意子类,Java无法确定你要添加的元素是否与实际的泛型参数类型匹配。

java 复制代码
public class Main {
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3);
        List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
        
        printNumbers(intList);    // 输出: 1 2 3
        printNumbers(doubleList); // 输出: 1.1 2.2 3.3
    }
}
java 复制代码
list.add(new Integer(10)); // 错误,不能添加元素

适用情况:只读取泛型集合中的元素时,适合用上界通配符,因为你只需要知道元素的类型是某个类型的子类。并且读取到的类型是T或其父类(通常是T),这保证了类型安全。

下界通配符<? super T>

下界通配符<? super T>表示某个类型的父类 或该类型本身。它限制了泛型参数的下界,即泛型参数必须是TT的父类。下界通配符允许你向泛型中写入T类型的对象或T的子类对象。

java 复制代码
public static void addNumbers(List<? super Integer> list) {
    list.add(1);  // 允许添加 Integer 类型的对象
    list.add(2);
}

可以写入,有限制地读取 :你可以向List<? super T>中添加T类型或其子类的元素,但读取时只能得到Object类型,因为列表中可能存储的是T的父类对象。

java 复制代码
public class Main {
    public static void main(String[] args) {
        List<Number> numList = new ArrayList<>();
        addNumbers(numList);
        
        List<Object> objList = new ArrayList<>();
        addNumbers(objList);
        
        System.out.println(numList);  // 输出: [1, 2]
        System.out.println(objList);  // 输出: [1, 2]
    }
}

在这里,? super Integer表示list接受Integer类型或它的父类(如NumberObject),这意味着你可以向列表中添加Integer及其子类,但不能向其中添加Double或其他不相关的类型。

java 复制代码
list.add(new Integer(10)); // 正确,允许添加 Integer
Object obj = list.get(0);  // 返回 Object 类型

适用情况:当你需要向泛型集合中添加元素,并且关心集合可以接受某个类型的元素时,可以使用<? super T>。因为它允许你向集合中写入元素,并保证类型安全。

上界通配符和上界约束的区别

上界通配符和上界约束从功能到写法上都很相似。但是两者其实不一样。

上界通配符(? extends T

上界通配符 是指在泛型方法或泛型类中,用? extends T来表示某种未知类型 ,但这个类型必须是TT的子类。

java 复制代码
public static void printList(List<? extends Number> list) {
    for (Number number : list) {
        System.out.println(number);
    }
}

在这个例子中,List<? extends Number>表示可以接受任何Number类型的子类,例如List<Integer>List<Double>。通配符?表示一个未知的类型 ,但我们知道这个类型是Number或其子类。

  • 主要作用 :允许使用T的所有子类,通常用于读取操作,因为我们不知道具体的类型,所以无法向列表中写入。
  • 使用场景 :当我们关心的是泛型的读取行为,而不需要向其中添加元素时,使用上界通配符非常合适。
java 复制代码
List<Integer> integers = Arrays.asList(1, 2, 3);
printList(integers); // 可以接受 List<Integer>

你可以从List<? extends T>中读取元素,并且它们的类型至少是T(比如Number),但你不能 往其中添加新元素(除了null),因为实际的类型可能是T的子类。

上界约束T extends SomeClass

上界约束 是用于定义泛型类型时的约束条件,表示泛型类型参数必须是某个类的子类 或实现某个接口。通过T extends SomeClass来限制泛型类型的范围。

java 复制代码
class Box<T extends Number> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

在这个例子中,T extends Number表示泛型参数T必须是Number类或其子类。这样做的好处是你可以保证T一定具有Number类的特性和方法(如doubleValue()等)。

  • 主要作用 :限制泛型类型只能是某个类及其子类,确保类型安全,可以对泛型进行读写操作。
  • 使用场景:在设计一个泛型类或泛型方法时,想要确保类型参数具有某些特定的属性或行为(比如必须是某个类的子类)时使用上界约束。
java 复制代码
Box<Integer> intBox = new Box<>();
intBox.setContent(123);
System.out.println(intBox.getContent()); // 输出: 123

上界约束用于定义泛型类型参数,而不是像上界通配符那样操作方法的参数或返回值。它明确了泛型参数的范围,并且允许你对这个泛型参数执行读写操作,因为你已经清楚地知道它的类型。

对比:上界通配符 vs 上界约束

特性 上界通配符(? extends T) 上界约束(T extends SomeClass)
定义位置 用于方法参数或泛型类型中,用? extends T表示 用于泛型类型定义时,用T extends SomeClass表示
作用 限制操作的参数类型为TT的子类,但泛型类型未知;主要用于读取操作 限制泛型参数必须是SomeClass的子类,适用于泛型类或泛型方法的定义
典型用法 允许读取 泛型集合中的数据,但不能向其中添加数据(除null 泛型类型的定义,允许对泛型进行读写操作
限制 只能从集合中读取类型为T的元素,不能往集合中添加元素 泛型参数只能是TT的子类,读写操作均可
示例 List<? extends Number> 可以是List<Integer>List<Double> class Box<T extends Number> 限制T只能是Number的子类

使用场景分析

  • 上界通配符 : 主要用于限制泛型的输入范围,在需要处理不确定类型但希望保证它是某个类或接口的子类时使用。它更适合只读的场景。

  • 上界约束 : 适合用在定义类或方法时,当你需要确保泛型类型具有某些能力(比如实现某个接口或继承某个类)时使用。它适合既需要读取 又需要写入的场景,能够在泛型参数上进行更多操作。

    上界通配符是用于泛型方法的参数或返回值中的灵活处理 ,而上界约束则是用于泛型类型定义中的类型限制

泛型擦除

前面的内容都是在代码编写过程的操作,实际在Java文件编译时,会对泛型类型进行"擦除"。

因为Java虚拟机不直接支持泛型。

  • Java泛型的类型擦除 是在编译时将泛型类型参数替换为其上界类型(如果未指定则为Object),并在需要的地方插入类型转换,以确保运行时与非泛型代码的兼容性。
  • 擦除机制使得泛型在编译时是类型安全的,但在运行时类型信息会被移除,因此无法进行某些运行时操作(如类型检查和泛型数组创建)。
  • 类型擦除的设计主要是为了保持Java的向后兼容,同时提供编译时的类型检查和安全性。
相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔1 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨3 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区5 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大6 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai6 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说6 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json