[Java泛型]泛型介绍以及对上下界通配符的说明

457 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情

泛型

1. 泛型介绍

1.1. 什么是泛型?

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

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

编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Persion> persons = new ArrayList<Persion>() 这行代码就指明了该 ArrayList 对象只能传入 Persion 对象,如果传入其他类型的对象就会报错。

ArrayList<E> extends AbstractList<E>

并且,原生 List 返回类型是 Object ,需要手动转换类型才能使用,使用泛型后编译器自动转换。

1.2. 泛型的使用方式

泛型一般有三种使用方式:

  • 泛型类
  • 泛型接口
  • 泛型方法

1.3. java 中泛型标记符

先来说明一下我们的泛型标记符

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

注意:

?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ? 不行。

T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。


下面有几个特别的

1. ?无界通配符

对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。

public static void printUnbound(List<?> items) {
    items.forEach(item ->
            System.out.println(item));
}

2. 上界通配符 < ? extends E>

上届:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类

public static <K extends TargetObject, E extends TargetObject> void printUpperBound(K k, E e) {
​
}

在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:

  • 如果传入的类型不是 E 或者 E 的子类,编译不成功
  • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用

注:类型参数列表中如果有多个类型参数上限,用逗号分开,但是形参中只能有一个对应类型的泛型标记符

使用 &符号设定多重边界(Multi Bounds)

public static <T extends TargetObject & InterfaceA> void multiBound(T t){
​
}

指定泛型类型 T 必须是 extends 后的类的共有子类型,此时变量 t 就具有了所有限定的方法和属性。

注:extends 后面必须有接口类或者abstract修饰的类

3.下界通配符 < ? super E>

下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型直至 Object

public static <T> void printUpperBound(List<? super T> dst, List<T> src) {
    for (T t : src) {
        dst.add(t);
    }
}

在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。

2. 泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分

/**
 * 泛型类
 * 此处 T 可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
 * 在实例化泛型类时,必须指定T的具体类型
 */
public class Generic<T> {
    private T key;
    public Generic(T key) {
        this.key = key;
    }
​
    public T getKey(){
        return key;
    }
}

实例化泛型类:

Generic<String> stringGeneric = new Generic<>("abc");
Generic<Integer> integerGeneric = new Generic<>(123);

3. 泛型接口

public interface Generator<T> {
    public T method();
}

实现泛型接口,不指定类型:

class GeneratorImpl<T> implements Generator<T>{
    @Override
    public T method() {
        return null;
    }
}

实现泛型接口,指定类型:

class GeneratorImpl<T> implements Generator<String>{
    @Override
    public String method() {
        return "hello";
    }
}

这里注意一下指定类型实现泛型接口,是在泛型接口后面的<>内写上你要指定的类型!!

4. 泛型方法

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

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

  • 所有泛型方法声明都有一个类型参数声明部分(<>) ,该类型参数声明部分在方法返回类型之前(在下面例子中的 <E>)。
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。
public static <E> void printArray(E[] inputArray) {
    for (E element : inputArray) {
        System.out.printf("%s ", element);
    }
    System.out.println();
}

使用:

// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray  );
printArray( stringArray  );

注意: public static < E > void printArray( E[] inputArray ) 一般被称为静态泛型方法;

在 java 中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的 <E>

有界的类型参数:

可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围。例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例。这就是有界类型参数的目的。

要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。

下面的例子演示了"extends"如何使用在一般意义上的意思"extends"(类)或者"implements"(接口)。该例子中的泛型方法返回三个可比较对象的最大值。

public class MaximumDemo {
    // 比较三个值并返回最大值
    public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
        T max = x; // 假设x是初始最大值
        if (y.compareTo(max) > 0) {
            max = y; //y 更大
        }
        if (z.compareTo(max) > 0) {
            max = z; // 现在 z 更大
        }
        return max; // 返回最大对象
    }

    public static void main(String args[]) {
        System.out.printf("%d, %d 和 %d 中最大的数为 %d\n\n",
                3, 4, 5, maximum(3, 4, 5));

        System.out.printf("%.1f, %.1f 和 %.1f 中最大的数为 %.1f\n\n",
                6.6, 8.8, 7.7, maximum(6.6, 8.8, 7.7));

        System.out.printf("%s, %s 和 %s 中最大的数为 %s\n", "pear",
                "apple", "orange", maximum("pear", "apple", "orange"));
    }
}

\