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的向后兼容,同时提供编译时的类型检查和安全性。
相关推荐
2401_857439692 小时前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
SoraLuna2 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹3 小时前
基于java的改良版超级玛丽小游戏
java
Dream_Snowar3 小时前
速通Python 第三节
开发语言·python
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫4 小时前
泛型(2)
java
超爱吃士力架4 小时前
邀请逻辑
java·linux·后端
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石4 小时前
12/21java基础
java