【学习笔记】java泛型

一、什么是泛型

1.1 泛型简介

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

java 中泛型标记符:

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

1.2 用一个例子认识泛型

举个例子来体会下上面说的功能:我们的需求是,设计一个“可变长度”的数组,可以存取任何类型的数据。

方案一:使用Object[]型数组

我们可以使用一个Object[]数组,配合存储一个当前分配的长度,就可以充当“可变数组”,用来存储“任何类型”的数据了:

public class MyArrayList {
    private Object[] array;
    private int size;
    public void add(Object e) {...}
    public void remove(int index) {...}
    public Object get(int index) {...}
}

当我们想存储 String 类型的数据时:

MyArrayList list = new MyArrayList();
list.add("Hello");
// 获取到Object,必须强制转型为String:
String value = (String) list.get(0);

当我们想存储 Integer 类型的数据时:

MyArrayList list = new MyArrayList();
list.add(new Integer(123));
// 获取到Object,必须强制转型为Integer:
Integer value = (Integer) list.get(0);

这样能满足需求,但存在一些缺陷:

  • 需要强制转型
  • 不方便,易出现“误转型”,报ClassCastException

例如,我们容易一不留神就出现这样问题:

MyArrayList list = new MyArrayList();
// 将 String 和 Integer 当作Object 混合存储
list.add("Hello");
list.add(new Integer(123));

String value = (String) list.get(0);
// ERROR: ClassCastException:
String value = (String) list.get(1);

方案二:为每一种类型数据单独编写一种ArrayList

要解决方案一的缺陷,我们可以为每一种类型数据单独编写一种ArrayList:

为String单独编写一种StringArrayList:

public class StringArrayList {
    private String[] array;
    private int size;
    public void add(String e) {...}
    public void remove(int index) {...}
    public String get(int index) {...}
}

这样一来,存入的必须是String,取出的也一定是String,不需要强制转型,因为编译器会强制检查放入的类型:

StringArrayList list = new StringArrayList();
list.add("Hello");
String first = list.get(0);
// 编译错误: 不允许放入非String类型:
list.add(new Integer(123));

同样的,再为 Integer 类型编写个 IntegerArrayList,为Long类型编写个 LongArrayList,为 Double类型编写个 DoubleArrayList······
JDK的class就有千百个,这还不包含其他人写的,这个路子看样子也是行不通了。

方案三:使用泛型

我们把ArrayList变成一种模板:ArrayList<T>,代码如下:

public class ArrayList<T> {
    private T[] array;
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}

T可以是任何class。上面的ArrayList就是Java标准库提供的ArrayList定义。

这样一来,我们就实现了:编写一次模版,可以创建任意类型的ArrayList:

// 创建可以存储String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 创建可以存储Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 创建可以存储Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();

泛型就是定义了一种模板,例如ArrayList<T>,然后在代码中为用到的类创建对应的ArrayList<类型>:

ArrayList<String> strList = new ArrayList<String>();

由编译器针对类型作检查:

strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!

这样一来,既实现了编写一次,万能匹配,又通过编译器保证了类型安全:这就是泛型。

二、使用泛型

2.1 使用泛型类:以ArrayList< T>为例

泛型类一般用在集合类中,例如ArrayList<T>,下面以其为例,看看泛型的一般使用方式。

2.1.1 要定义泛型类型

使用ArrayList时,如果不定义泛型类型时,泛型类型实际上就是Object,相当于只把<T>当作Object使用,没有发挥泛型的优势。

// 编译器警告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);

在这里插入图片描述
当我们定义泛型类型<String>后,List<T>的泛型接口变为强类型List<String>:

// 无编译器警告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 无强制转型:
String first = list.get(0);
String second = list.get(1);

2.1.2 可省略编译器能推断出的泛型类型

编译器如果能自动推断出泛型类型,就可以省略后面的泛型类型。例如,对于下面的代码:

List<String> list = new ArrayList<String>();

编译器看到泛型类型List<Number>就可以自动推断出后面的ArrayList<T>的泛型类型必须是ArrayList<Number>,因此,可以把代码简写为:

// 可以省略后面的String,编译器可以自动推断泛型类型:
List<String> list = new ArrayList<>();

2.2 使用泛型接口:以Comparable< T>为例

除了ArrayList<T>使用了泛型,还可以在接口中使用泛型。例如,Arrays.sort(Object[])可以对任意数组进行排序,但待排序的元素必须实现Comparable<T>这个泛型接口

public interface Comparable<T> {
    /**
     * 返回负数: 当前实例比参数o小
     * 返回0: 当前实例与参数o相等
     * 返回正数: 当前实例比参数o大
     */
    int compareTo(T o);
}

可以直接对String数组进行排序:

public class Main {
    public static void main(String[] args) {
        String[] ss = new String[] { "Orange", "Apple", "Pear" };
        Arrays.sort(ss);
        System.out.println(Arrays.toString(ss));
    }
}

//输出结果:
[Apple, Orange, Pear]

这是因为String本身已经实现了Comparable<String>接口:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {......}

如果换成我们自定义的Person类型试试:

public class Person  {
    String name;
    int score;
    Person(String name, int score) {
        this.name = name;
        this.score = score;
    }
    @Override
    public String toString() {
        return this.name + "," + this.score;
    }
    
    public static void main(String[] args) {
        Person[] ps = new Person[] {
            new Person("Bob", 61),
            new Person("Alice", 88),
            new Person("Lily", 75),
        };
        Arrays.sort(ps);
        System.out.println(Arrays.toString(ps));
    }
}

运行程序,我们会得到ClassCastException:

Exception in thread "main" java.lang.ClassCastException: genericsDemo.Person cannot be cast to java.lang.Comparable
	at java.util.ComparableTimSort.countRunAndMakeAscending(ComparableTimSort.java:320)
	at java.util.ComparableTimSort.sort(ComparableTimSort.java:188)
	at java.util.Arrays.sort(Arrays.java:1246)
	at genericsDemo.Person.main(Person.java:33)

即无法将Person转型为Comparable。

我们修改代码,让Person实现Comparable<T>接口:

package genericsDemo;

import java.util.Arrays;

/**
 * @author xiongkunworkmac
 * @date 2022/03/13
 */
public class Person implements Comparable<Person> {
    String name;
    int score;
    Person(String name, int score) {
        this.name = name;
        this.score = score;
    }
    @Override
    public int compareTo(Person other) {
        return this.name.compareTo(other.name);
    }
    @Override
    public String toString() {
        return this.name + "," + this.score;
    }

    public static void main(String[] args) {
        Person[] ps = new Person[] {
            new Person("Bob", 61),
            new Person("Alice", 88),
            new Person("Lily", 75),
        };
        Arrays.sort(ps);
        System.out.println(Arrays.toString(ps));
    }
}

运行得到结果:

[Alice,88, Bob,61, Lily,75]

所以,我们可以在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。

三、编写泛型

3.1 编写泛型方法

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

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

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

我们举个例子,来验证上面各种规则:

public class GenericsMethodDemo {
    /**
     * 单类型参数 无返回值方法
     * 功能:能打印各种类型的数组
     * @param inputArray 输入数组
     * @param <E> 类型参数声明
     */
    public static <E> void printAnyTypeArray(E[] inputArray){
        for(E e : inputArray){
            System.out.print(e + " ");
        }
        System.out.println();
    }

    /**
     * 单类型参数 有回值方法,返回 int
     * 功能:返回输入数组的长度
     * @param inputArray
     * @param <E>
     * @return
     */
    public static <E> int getArrayLength(E[] inputArray){
        return inputArray.length;
    }

    /**
     *  单类型参数 有回值方法,返回 E
     * 功能:打印数组最后的一个元素
     * @param inputArray
     * @param <E>
     * @return
     */
    public static <E> E getLastElement(E[] inputArray){
        return inputArray[inputArray.length-1];
    }

    /**
     * 两个类型参数 无返回值方法
     * 打印两个数组信息
     * @param inputArray1
     * @param inputArray2
     * @param <E>
     * @param <T>
     */
    public static <E,T> void twoArrayInfo(E[] inputArray1, T[] inputArray2){
        System.out.println("第一个数组的类型是: " + inputArray1.getClass().getName() );
        System.out.println("第二个数组的类型是: " + inputArray2.getClass().getName() );
    }


    public static void main(String[] args){
        String[] strings = {"hello","world","!"};
        Integer[] integers = {1,2,3,4,5,6};
        Double[] doubles = {1.1, 2.2, 3.3,4.4,5.5,6.6};

        System.out.println("***********验证 单个类型参数 相关方法*********");
        System.out.println("打印 String 型数组:");
        GenericsMethodDemo.printAnyTypeArray(strings);
        System.out.println("数组长度是:" + GenericsMethodDemo.getArrayLength(strings));
        String stringResult =  GenericsMethodDemo.getLastElement(strings);
        System.out.println("String 型数组的最后一个元素是: " + stringResult);

        System.out.println("\n打印 Integer 型数组:");
        GenericsMethodDemo.printAnyTypeArray(integers);
        System.out.println("数组长度是:" + GenericsMethodDemo.getArrayLength(integers));
        Integer integerResult =  GenericsMethodDemo.getLastElement(integers);
        System.out.println("String 型数组的最后一个元素是: " + integerResult);

        System.out.println("\n打印 Double 型数组:");
        GenericsMethodDemo.printAnyTypeArray(doubles);
        System.out.println("数组长度是:" + GenericsMethodDemo.getArrayLength(doubles));
        Double doubleResult =  GenericsMethodDemo.getLastElement(doubles);
        System.out.println("String 型数组的最后一个元素是: " + doubleResult);

        System.out.println("***********验证 两个类型参数 相关方法*********");
        System.out.println("\n打印 两个 数组信息:");
        GenericsMethodDemo.twoArrayInfo(strings,doubles);
    }
}

输出结果:

***********验证 单个类型参数 相关方法*********
打印 String 型数组:
hello world ! 
数组长度是:3
String 型数组的最后一个元素是: !

打印 Integer 型数组:
1 2 3 4 5 6 
数组长度是:6
String 型数组的最后一个元素是: 6

打印 Double 型数组:
1.1 2.2 3.3 4.4 5.5 6.6 
数组长度是:6
String 型数组的最后一个元素是: 6.6

***********验证 两个类型参数 相关方法*********

打印 两个 数组信息:
第一个数组的类型是: [Ljava.lang.String;
第二个数组的类型是: [Ljava.lang.Double;

3.2 编写泛型类

通常来说,泛型类一般用在集合类中,例如ArrayList<T>,我们很少需要编写泛型类。但如果我们确实需要编写一个泛型类,那么,也应该知道如何编写它。

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

还是用例子来说明:

public class Box<T, V> {
    private T obj1;
    private V obj2;

    public Box(T o1,V o2) {
        obj1 = o1;
        obj2 = o2;
    }
    public void showTypes() {
        System.out.println("Type of T is " + obj1.getClass().getName());
        System.out.println("Type of V is " + obj2.getClass().getName());
    }
    public T getObj1() {
        return obj1;
    }
    public V getObj2() {
        return obj2;
    }


    public static void main(String[] args) {
        //创建对象时必须为Box传递两个类型参数,这里Integer替换T,String替换V。
        Box<Integer,String> testObj1 = new Box<>(123,"bigBear");
        testObj1.showTypes();
        int t1 = testObj1.getObj1();
        System.out.println("value: " + t1);
        String v1 = testObj1.getObj2();
        System.out.println("value: " + v1);
        System.out.println("-----------------------------");

        //在这个例子中,尽管两个类型参数是不同的,但是可以将两个类型参数设置为相同的类型。这T,V都是String类型
        Box<String,String> testObj2 = new Box<>("bigBear","happyBird");
        testObj2.showTypes();
        String t2 = testObj2.getObj1();
        System.out.println("value: " + t2);
        String v2 = testObj2.getObj2();
        System.out.println("value: " + v2);
    }
}

运行结果如下:

ype of T is java.lang.Integer
Type of V is java.lang.String
value: 123
value: bigBear
-----------------------------
Type of T is java.lang.String
Type of V is java.lang.String
value: bigBear
value: happyBird

四、通配符extends、super、?

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

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

4.1 上界通配符extends

通常在方法参数中会用到上界通配符extends,举个例子:

我们有个 add 方法的参数,希望接受Pair<Number>及Pair<Integer>、Pair<Long>等,即方法接收所有泛型类型为Number或Number子类的Pair类型。我们可以这样做:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }

	//希望能支持所有Number或Number子类的参数,使用 <? extends Number> 做参数
    static int add(Pair<? extends Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        return first.intValue() + last.intValue();
    }

    public static void main(String[] args) {
        Pair<Integer> p1 = new Pair<>(123, 456);
        int result1 = add(p1);
        System.out.println("Integer Pair 的结果是:" + result1);

        Pair<Double> p2 = new Pair<>(123.1, 456.1);
        int result2 = add(p2);
        System.out.println("\nInteger Pair 的结果是:" + result2);

        Pair<Long> p3 = new Pair<>(123L, 456L);
        int result3 = add(p3);
        System.out.println("\nLong Pair 的结果是:" + result3);
    }
}

输出结果:

Integer Pair 的结果是:579

Integer Pair 的结果是:579

Long Pair 的结果是:579

这里add方法参数使用了 Pair<? extends Number> ,使得方法接收所有泛型类型为Number或Number子类的Pair类型。这样一来,给方法传入Pair<Integer>类型时,它符合参数Pair<? extends Number>类型。除了可以传入Pair<Integer>类型,我们还可以传入Pair<Double>类型,Pair<BigDecimal>类型等等,因为Double和BigDecimal都是Number的子类.

这种使用<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。

关于 extends ,有些结论:使用类似<? extends Number>通配符作为方法参数时表示:

  • 方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();;
  • 方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);。

即一句话总结:使用extends通配符表示可以读,不能写。具体分析见 文档

4.2 下界通配符super

和extends通配符相反,这次,我们希望方法参数接受Pair<Integer>类型,以及Pair<Number>、Pair<Object>,即方法接收所有泛型类型为Integer或Integer的父类的Pair类型(Number和Object是Integer的父类)。

我们可以这样做:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }

    static void setSame(Pair<? super Integer> p, Integer n) {
        p.setFirst(n);
        p.setLast(n);
    }

    public static void main(String[] args) {
        Pair<Number> p1 = new Pair<>(12.3, 4.56);
        Pair<Integer> p2 = new Pair<>(123, 456);
        setSame(p1, 100);
        setSame(p2, 200);
        System.out.println("p1 result: " + p1.getFirst() + ", " + p1.getLast());
        System.out.println("p2 result: " + p2.getFirst() + ", " + p2.getLast());
    }
}

注意到Pair<? super Integer>表示,方法参数接受所有泛型类型为Integer或Integer父类的Pair类型。

使用类似<? super Integer>通配符作为方法参数时表示:

  • 方法内部可以调用传入Integer引用的方法,例如:obj.setFirst(Integer n);;
  • 方法内部无法调用获取Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();。

**即一句话总结:使用super通配符表示只能写不能读。**具体分析见 文档

4.3 对比extends和super通配符

我们再回顾一下extends通配符。作为方法参数,<? extends T>类型和<? super T>类型的区别在于:

  • <? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
  • <? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。

一个是允许读不允许写,另一个是允许写不允许读。

我们来看Java标准库的Collections类定义的copy()方法:

public class Collections {
    // 把src的每个元素复制到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i);
            dest.add(t);
        }
    }
}

它的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是List<? super T>,表示目标List,第二个参数List<? extends T>,表示要复制的List。我们可以简单地用for循环实现复制。在for循环中,我们可以看到,对于类型<? extends T>的变量src,我们可以安全地获取类型T的引用,而对于类型<? super T>的变量dest,我们可以安全地传入T的引用。

这个copy()方法的定义就完美地展示了extends和super的意图:

  • copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用;
  • copy()方法内部也不会修改src,因为不能调用src.add(T)。

这是由编译器检查来实现的。如果在方法代码中意外修改了src,或者意外读取了dest,就会导致一个编译错误:

public class Collections {
    // 把src的每个元素复制到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        ...
        T t = dest.get(0); // compile error!
        src.add(t); // compile error!
    }
}

这个copy()方法的另一个好处是可以安全地把一个List<Integer>添加到List<Number>,但是无法反过来添加:

// copy List<Integer> to List<Number> ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);

// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);

而这些都是通过super和extends通配符,并由编译器强制检查来实现的。

4.4 使用extends和super的PECS原则

何时使用extends,何时使用super?为了便于记忆,我们可以用PECS原则:Producer Extends Consumer Super。

即:如果需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它是消费者(Consumer),要使用super通配符。

还是以Collections的copy()方法为例:

public class Collections {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i); // src是producer
            dest.add(t); // dest是consumer
        }
    }
}

需要返回T的src是生产者,因此声明为List<? extends T>,需要写入T的dest是消费者,因此声明为List<? super T>。

4.5 无限定通配符: ?

我们已经讨论了<? extends T>和<? super T>作为方法参数的作用。实际上,Java的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个?。

无限定通配符<?>很少使用,可以用<T>替换,同时它是所有<T>类型的超类。

例如:

void sample(Pair<?> p) {
}

因为<?>通配符既没有extends,也没有super,因此:

  • 不允许调用set(T)方法并传入引用(null除外);
  • 不允许调用T get()方法并获取T引用(只能获取Object引用)。
    换句话说,既不能读,也不能写,那只能做一些null判断:
static boolean isNull(Pair<?> p) {
    return p.getFirst() == null || p.getLast() == null;
}

大多数情况下,可以引入泛型参数<T>消除<?>通配符:

static <T> boolean isNull(Pair<T> p) {
    return p.getFirst() == null || p.getLast() == null;
}
<?>通配符有一个独特的特点,就是:Pair<?>是所有Pair< T>的超类:
class Pair<T> {
    private T first;
    private T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
    
	public static void main(String[] args) {
        Pair<Integer> p = new Pair<>(123, 456);
        Pair<?> p2 = p; // 安全地向上转型
        System.out.println(p2.getFirst() + ", " + p2.getLast());
    }
}

上述代码是可以正常编译运行的,因为Pair<Integer>是Pair<?>的子类,可以安全地向上转型。

五、参考文档

ref1. runoob.com: Java 泛型
ref2. 廖雪峰的官方网站:java泛型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值