Java 泛型机制详解

706 阅读4分钟

tip:作为程序员一定学习编程之道,一定要对代码的编写有追求,不能实现就完事了。我们应该让自己写的代码更加优雅,即使这会费时费力。

推荐:体系化学习Java(Java面试专题) - 掘金 (juejin.cn)

一、Java 中为什么会引入泛型?

Java 引入泛型的主要目的是为了提高代码的类型安全性和可读性。在 Java 5 之前,集合框架中的容器可以存储任意类型的对象,这就使得程序员需要在运行时进行类型转换,容易引发类型转换异常。而引入泛型后,集合框架中的容器可以限定存储的元素类型,使得程序员可以在编译时进行类型检查,避免了类型转换异常的发生。此外,泛型还可以提高代码的可读性和可维护性,使得代码更易于理解和修改。泛型的引入使得 Java 语言更加类型安全,更加适合大规模软件开发。

接下来我们举个例子说明为什么: 假设有一个需求,需要编写一个方法,用于比较两个对象是否相等。最初的实现可能是这样的。

public static boolean compareObject(String s1, String s2) {
    return s1.equals(s2);
}

但是入参不一定是 String,也有可能是 int、long 等,那么该怎么写呢?难道每个都写一遍?

public static boolean compareObject(String s1, String s2) {
    return s1.equals(s2);
}

public static boolean compareObject(Integer s1, Integer s2) {
    return s1.equals(s2);
}

这样的代码可读性,可维护性都很差,于是我们用泛型进行改进, 一个方法就搞定了。

public static <T> boolean compareObjects(T o1, T o2) {
    return o1.equals(o2);
}

二、泛型的应用

泛型是 Java 中的一个重要特性,它可以让我们编写更加通用、可复用的代码。

package com.pany.camp.base;

public class Genericity<T, U> {

    private T first;
    private U second;

    public Genericity(T first, U second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public U getSecond() {
        return second;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public void setSecond(U second) {
        this.second = second;
    }

    @Override
    public String toString() {
        return "(" + first + ", " + second + ")";
    }
}

这个示例中定义了一个泛型类 Genericity,它有两个类型参数 T 和 U,分别表示 Genericity 中的两个元素的类型。Genericity类有一个构造函数,用于初始化 Genericity对象的两个元素。Genericity 类还有一些方法,用于获取和设置 Genericity 对象的元素,以及将 Genericity 对象转换成字符串表示。

package com.pany.camp.base;

public class GenericityMain {

    public static void main(String[] args) {
        Genericity<String, Integer> pair = new Genericity<>("Hello", 123);

        // 输出:(Hello, 123)
        System.out.println(pair);
    }
}

在这里插入图片描述

它的第一个元素是一个字符串,第二个元素是一个整数。使用泛型可以让我们编写更加通用、可复用的代码,可以在不同的场景中使用相同的类来处理不同类型的数据。

三、泛型接口

package com.pany.camp.base;

public interface Pair<T, U> {

    T getFirst();

    U getSecond();
}

我们定义了一个泛型接口 Pair,它有两个类型参数 T 和 U,分别表示 Pair 中的两个元素的类型。Pair 接口有两个方法,getFirst() 和 getSecond(),用于获取 Pair 对象的两个元素。

package com.pany.camp.base;

public class StringIntegerPair implements Pair<String, Integer> {

    private String first;
    private Integer second;

    public StringIntegerPair(String first, Integer second) {
        this.first = first;
        this.second = second;
    }

    @Override
    public String getFirst() {
        return first;
    }

    @Override
    public Integer getSecond() {
        return second;
    }
}

然后我们创建了一个 StringIntegerPair 类,它实现了 Pair 接口,并指定了 T 和 U 的具体类型为 String 和 Integer。StringIntegerPair 类有一个构造函数,用于初始化 StringIntegerPair 对象的两个元素。StringIntegerPair 类还实现了 Pair 接口的两个方法,用于获取 StringIntegerPair 对象的两个元素。

四、泛型方法

泛型方法可以让我们编写更加通用、可复用的代码,可以在不同的场景中使用相同的方法来处理不同类型的数据。

例子如下:

public static <T> int countOccurrences(T[] array, T element) {
    int count = 0;
    for (T value : array) {
        if (value.equals(element)) {
            count++;
        }
    }
    return count;
}

我们定义了一个泛型方法 countOccurrences,它有两个参数,一个是泛型类型数组 array,另一个是泛型类型元素 element。countOccurrences 方法的作用是统计数组中与 element 相等的元素个数。

String[] strings = {"foo", "bar", "baz", "foo"};
int count1 = countOccurrences(strings, "foo"); // 返回 2
int count2 = countOccurrences(strings, "qux"); // 返回 0

Integer[] integers = {1, 2, 3, 2, 1};
int count3 = countOccurrences(integers, 2); // 返回 2
int count4 = countOccurrences(integers, 4); // 返回 0

我们分别使用了字符串类型数组和整数类型数组来调用 countOccurrences 方法,传入不同的元素来统计数组中相等的元素个数。

五、泛型的上下限

泛型的上下限是指在使用泛型时,可以限制泛型参数的类型范围,从而提高代码的类型安全性。上限限制了泛型参数的类型必须是某个类或其子类,下限限制了泛型参数的类型必须是某个类或其父类。 使用泛型的上下限可以避免类型转换错误,提高代码的类型安全性。

在 Java 中,可以使用 extends 关键字来指定泛型的上限,使用 super 关键字来指定泛型的下限。例如:

public class Box<T> {
    private T item;
    public void setItem(T item) {
        this.item = item;
    }
    public T getItem() {
        return item;
    }
}

// 使用 extends 指定泛型的上限
public class NumberBox<T extends Number> {
    private T item;
    public void setItem(T item) {
        this.item = item;
    }
    public T getItem() {
        return item;
    }
}

// 使用 super 指定泛型的下限
public class IntegerBox<T super Integer> {
    private T item;
    public void setItem(T item) {
        this.item = item;
    }
    public T getItem() {
        return item;
    }
}

NumberBox 类使用 extends 指定泛型参数的上限为 Number 类型及其子类,因此只能使用 Number 类型及其子类作为泛型参数。而 IntegerBox 类使用 super 指定泛型参数的下限为 Integer 类型及其父类,因此只能使用 Integer 类型及其父类作为泛型参数。