简单讲讲Java泛型

154 阅读16分钟

写在最前面:
因为本人能力的原因,感觉泛型写的不是很好🙇‍♂️
主要体现在:
一、排版上:目录、标题、顺序。自我感觉不是很顺畅... 我这里是参考《Thinking in Java》来排的...
二、对于通配符的讲解...很基础...甚至感觉没用...🙇‍♂️大佬们可以不用看,最好别看了😖
三、对于《Thinking in Java》中的一些细小知识点想要写出来却碍于不知道要穿插在哪一部分比较合适而无从下笔...(也是有写上几点的,但就是这几点让我想了好久... 不知道放哪里...不知道如何组织...)
四、独自一个人在啃《Thinking in Java》这本书,自我感觉真的很难啃...特别是难得部分。本人在写这篇得时候已经要疯了🙄
最后,希望大佬能指点我下。跪求了😭

一、泛型

一般的类和方法,只能使用具体的类型;要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。

泛型的概念:泛型实现了参数化类型的概念,使代码可以应用于多种类型。泛型又有适用于许许多多的类型的意思。

起初引入泛型的目的:希望类或方法能够具备最广泛的表达能力。
现在泛型的目的是:用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。

泛型的优点(作用):

  1. 泛型会在你创建参数化类型的一个实例时,让编译器为你进行转型操作,并且保证类型的正确性 ==> 这使得泛型的用处大大提升了一个档次
  2. 能够在编译时而不是运行时检测出错误
  3. 提高代码的可读性
  4. 提高代码的可复用性

二、泛型的作用

泛型有四大作用/优点:类型安全、自动转换、性能提升、可复用性。

在编译的时候检查类型安全,将所有的强制转换都自动 or 隐式的进行,同时提高代码的可复用性

1. 类型安全

在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换。如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。

public class Test {
    public static void main(String[] args) {
        // 编译正常通过,但是使用的时候可能会出现问题
        ArrayList arr = new ArrayList();
        arr.add(0);
        arr.add("a");
        arr.add(2);
        System.out.println(arr);
        // 报类型转换错误:java.lang.String cannot be cast to java.lang.Integer
        int a = (int) arr.get(0);
        System.out.println(a);
    }
}

image.png


image.png

从以上内容就可以看出泛型是多么贴心了~ 这你不好好学习泛型?🤔

2. 类型自动转换,消除强转

消除源代码中的强制类型转换,这样代码的可读性更强,且减少了转换类型出错的可能性。

public class Test {
    public static void main(String[] args) {
        // 没引入泛型之前
        ArrayList arr = new ArrayList();
        arr.add(0);
        arr.add(1);
        System.out.println(arr);
        int a = (int) arr.get(1); // 需要进行强装
        System.out.println(a);
    }
}

image.png

3. 避免装箱、拆箱,提高性能

在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。

泛型变量固定了类型,使用的时候就已经知道是基本数据类型还是引用数据类型了,避免了不必要的装箱、拆箱操作。

object a = 1; // 由于是object类型,会自动进行装箱操作。
 
int b = (int)a; // 强制转换,拆箱操作。这样一去一来,当次数多了以后会影响程序的运行效率。

// 使用泛型之后
public static T GetValue<T>(T a) {
  return a;
}
 
public static void Main(){
  int b = GetValue<int>(1); // 使用这个方法的时候已经指定了类型是int,所以不会有装箱和拆箱的操作。
}

4. 提升程序可复用性

适用于多种数据类型执行相同的代码(代码复用)

如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法:

private static int add(int a, int b) {
   System.out.println(a + " + " + b + " = " + (a + b)); 
   return a + b;
}

private static float add(float a, float b) {
   System.out.println(a + " + " + b + " = " + (a + b)); 
   return a + b;
}

private static double add(double a, double b) {
    System.out.println(a + " + " + b + " = " + (a + b));
    return a + b;
}

通过泛型,我们可以复用为一个方法:

private static <T extends Number> double add(T a, T b) {
    System.out.println(a + " + " + b + " = " + (a.doubleValue() + b.doubleValue()));
    return a.doubleValue() + b.doubleValue();
}

三、简单泛型

先了解一下泛型。PS:这真的是简单的泛型!

// 堆栈类
public class LinkedStack<T> {
    private static class Node<U>{
        U item;
        Node<U> next;
        Node() {
            item = null;
            next = null;
        }
        public Node(U item, Node<U> next) {
            this.item = item;
            this.next = next;
        }
        boolean end(){ // 判断是否为“末端哨兵”(即是否为最后一个结点)
            return item == null && next == null;
        }
    }

    private Node<T> top = new Node<>();
    public void push(T item){
        top = new Node<T>(item, top);
    }
    public T pop(){
        T result = top.item;
        if (!top.end()) // 判断该结点是否是最后一个结点
            top = top.next;
        return result;
    }

    public static void main(String[] args) {
        LinkedStack<String> lss = new LinkedStack<>();
        for (String s : "Phasers on stun!".split(" "))
            lss.push(s);
        String s;
        while ((s = lss.pop()) != null) // 判断栈内是否还有元素存在~
            System.out.println(s);
    }
}

四、泛型的使用方式

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

类型参数(即 < T >)的意义是告诉编译器这个集合中要存放什么样类型的数据,从而在添加其他类型的数据时给出提示。从而在编译期就做到了类型安全上的保证。避免了在使用数据的时候,进行强转的操作。

这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法

1. 泛型类

尖括号<>中的T被称作类型参数,可以是任何类型,当然也可以用E、V等符号来代替T,这种形式的类就叫做泛型类.

public class GenericStack<E> { // ①
    GenericStack(){ // ②

    }

    private ArrayList<E> list = new ArrayList<>();
    
    public int getSize(){
        return list.size();
    }
    
    public E peek(){
        return list.get(getSize() - 1);
    }
    
    public void push(E o){
        list.add(o);
    }
    
    public E pop(){
        E o = list.get(getSize() - 1);
        list.remove(getSize() - 1);
        return o;
    }
    
    public boolean isEmpty(){
        return list.isEmpty();
    }

    @Override
    public String toString() {
        return "stack:" + list.toString();
    }
}

小结:
① 在定义一个泛型类时,将泛型类的类型参数放置在类名之后
② 即使是泛型类,它的构造方法还是与之前一样~

2. 泛型接口

泛型接口于泛型类的定义方式大同小异

// 定义一个泛型接口
public interface Generator<T> { // ①
    public T next();
}

① 可以看出,定义泛型接口与定义泛型类的方式都是一样的😁

需要额外注意的是:
② 当泛型接口的实现类,未传入泛型实参时(是泛型时):在声明类的时候,需将泛型的声明也一起加到类中

public class FruitGenerator<T> implements Generator<T>{ // ②
    @Override
    public T next() {
        return null;
    }
}

image.png

③ 当泛型接口的实现类,传入泛型实参时(不是泛型时):所有使用泛型的地方都要替换成传入的实参类型
如:泛型接口(Generator)的 public T next()方法中的T都要替换成传入的String类型。

public class FruitGenerator implements Generator<String> {
    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() { // ③
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

3. 泛型方法

首先先问几个问题:
1. 类中的成员方法内部使用了泛型方法,那么该成员方法是泛型方法嘛?
很明显不是嘛🙄
2. 泛型类中的成员方法使用了泛型作为返回值,那么该成员方法是泛型方法嘛?
很显然这也不是啊👹

什么是泛型方法?如何声明泛型方法?

① public与返回值中间的< T >才能够声明此方法为泛型方法
② 在定义一个泛型方法时,将泛型方法的类型参数放置在返回值类型之前的就是泛型方法
③ 类中成员方法里,使用了泛型类的方法并不是泛型方法
④ 泛型类中使用类型参数作为返回值类型的成员方法并不是泛型方法
⑤ 泛型类中使用类型参数作为形参的类型的方法不是泛型方法
⑥ 类中的成员方法使用泛型类(具体的)作为形参的类型的方法不是泛型方法

public class GenericMethodDemo {
    public static void main(String[] args) {
        Integer[] integers = {1, 2, 3, 4, 5};
        String[] strings = {"London", "Paris", "New York", "Austin"};
        GenericMethodDemo.print(integers); // 因为编译器已经知道你存储的是什么类型的数据了(类型参数推断),所以可以不用写
        GenericMethodDemo.<String>print(strings); // 但写了可读性更好(让别人知道你这个是一个泛型方法)
        printList();
        System.out.println("------ 内部类测试 ------");
        GenericMethodDemo.GenericMethod g = new GenericMethodDemo().new GenericMethod<Integer>(666);
        GenericMethodDemo.showKeyValue(g);
    }

    public static <E> void print(E[] list){ // ①、② 静态泛型方法
        for (E e : list)
            System.out.print(e + " ");
        System.out.println();
    }

    public static void printList(){ // ③
        ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); // ArrayList<Integer> 实例化泛型
        System.out.println(list);
    }
    
    // PS:不能在泛型类中引用其他没有声明过的类型参数(即 在泛型类中声明了什么样的类型参数,就用什么样的类型参数)
    class GenericMethod<T>{
        private T key;

        public GenericMethod() {
        }

        public GenericMethod(T key) { // ⑤
            this.key = key;
        }

        // 在方法中使用了泛型,但是这并不是一个泛型方法。这只是类中一个普通的成员方法,只不过他的返回值类型与泛型类类型一致罢了
        public T getKey() { // ④ 
            return key;
        }

        // 这也不是泛型方法,这只是一个普通的方法。将泛型类的类型作为形参key的类型罢了
        public void setKey(T key) { // ⑤ 
            this.key = key;
        }

    }

    // 这也不是一个泛型方法,这就是一个普通的方法,只是使用了GenericMethod<Number>这个泛型类做形参而已
    public static void showKeyValue(GenericMethod<Number> obj){ // ⑥
        System.out.println("泛型测试: key value is " + obj.getKey());
    }
}
Output:
    1 2 3 4 5 
    London Paris New York Austin 
    [1, 2, 3, 4, 5]
    ------ 内部类测试 ------
    泛型测试: key value is 666

讲这么多其实也就是:public与返回值中间的类型参数(< T >)可以确定该方法是否为泛型方法

补充:是否拥有泛型方法,与其所在的类是否是泛型没有关系。泛型方法使得该方法能够独立于类而产生变化。 ==> 如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白)
对于一个static(静态)的方法而言,无法访问泛型类的类型参数。所以如果static方法需要使用泛型能力的话,就必须让其成为泛型方法。

4. 引出的小知识点

1. 类型参数推断

当使用泛型类时,必须在创建对象的时候指定类型参数的值。使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型,这称为 “类型参数推断”

在《Java编程思想第4版》中有这句话:如果你将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行类型推断。(即 编译器认为:调用泛型方法后,其返回值被赋给一个Object类型的变量。 ==> 这时需要我们人为的去强制类型转化)。但经过我的测试,编译器却神奇的知道泛型方法做返回值时是什么类型的...🙄

2. 显式的类型说明

在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内。

image.png

public class test {
    public static void main(String[] args) {
        List<String> list1 = new ArrayList<>(); // 类型参数推断
        List<String> list2 = new ArrayList<String>(); // 显式类型说明
    }
}

五、泛型的类型擦除

当编译器对带有泛型的java代码进行编译时,它会去执行类型检查类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的 Java 虚拟机接收并执行,这被称为 类型擦除(type erasure) 简得来说:擦除就是在代码运行过程中将具体的类型都抹除成原生类型(究极父类Object)
这将造成一种现象:泛型擦除使得在运行时无法明确知道其实际类型。你唯一知道的只有:“你正在使用一个对象”
看一下以下代码:

public class GenericType {
    public static void main(String[] args) {
        ArrayList<String> arrayString = new ArrayList<String>();
        ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
        System.out.println(arrayString.getClass() == arrayInteger.getClass()); // 在编译期进行了泛型擦除了
    }
}
Output:
    true

为什么我们定义了两种不同的类型,编译器却返回true呢?
因为,在编译期间所有的泛型信息都会被擦除,List< Integer >和List< String >类型,在编译后都会变成List类型(原始类型)。Java中的泛型基本上都是在编译器这个层次来实现的,这也是Java的泛型被称为“伪泛型”的原因。

image.png

1. 擦除的问题

泛型不能用于显式地引用运行时类型的操作之中,例如:转型、instanceof操作和new表达式。因为所有关于参数的类型信息都丢失了

public class GenericInstanceof {
    public static void main(String[] args) {
        ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
        System.out.println(arrayInteger instanceof ArrayList<? extends Number>); // 泛型不能用于显式地引用运行时类型的操作之中
    }
}
在编译期就报错了:Illegal generic type for instanceof

image.png

由于 泛型擦除(泛型类型参数将擦除到它的第一个边界) 的原因,所有的泛型类型参数都丢失了。以至于在运行时,JVM无法正确识别谁是谁。

边界:使得你可以在用于泛型的参数类型上设置限制条件。

因此在编写泛型的时候,你必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息罢了

2. 边界处的动作

当进行泛型擦除的时候,该泛型类型参数的边界又会发生什么事情呢?

首先必须明确一点:泛型 ==> 可以表示没有任何意义的事物。

public class ArrayMaker<T> {
    private Class<T> kind;

    public ArrayMaker(Class<T> kind){
        this.kind = kind;
    }

    T[] create(int size){
        return (T[]) Array.newInstance(kind, size); // 初始化数组
    }

    public static void main(String[] args) {
        ArrayMaker<String> stringMaker = new ArrayMaker<>(String.class);
        String[] stringArray = stringMaker.create(9);
        System.out.println(Arrays.toString(stringArray));
    }
}
Output:
    [null, null, null, null, null, null, null, null, null]

image.png

public class ListMaker<T> {
    List<T> create(){
        return new ArrayList();
    }

    public static void main(String[] args) {
        ListMaker<String> stringMaker = new ListMaker<>();
        List<String> stringList = stringMaker.create();
        System.out.println(stringList);
    }
}

image.png

但是,类型参数< T >真的是毫无意义的嘛?

public class FilledListMaker<T> {
    List<T> create(T t,int n){
        List<T> result = new ArrayList<>();
        for (int i = 0; i < n; i++){
            result.add(t);
        }
        return result;
    }

    public static void main(String[] args) {
        FilledListMaker<String> stringMaker = new FilledListMaker<>();
        List<String> list = stringMaker.create("Hello", 4);
        System.out.println(list);
    }
}
Output:
    [Hello, Hello, Hello, Hello]

即使编译器无法知道有关create()中的T的任何信息,但是它仍旧可以在编译期确保你放置到result中的对象具有T类型(编译器的警告),使其适合ArrayList< T >。
因此,即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。(这就是为什么泛型可以保证在编译期间就可以查得出问题所在的原因)

因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地点

image.png

image.png

从上述代码可以看出:类型擦除会减少泛型的泛化性。它使泛型更具体了

3. 擦除补偿

由于泛型不能用于显式的引用运行时类型的操作之中(类型信息会被擦除),但有时候我们又必须使用泛型来创建对象,这该怎么办呢?
我们可以显式的传递你的类型的Class对象,以便你可以在类型表达式中使用它(用它来代替instanceof)

class Building {
}

class House extends Building {
}

public class ClassTypeCapture<T> {
    Class<T> kind; // 使用Class对象调用其isInstance()方法,来代替instanceof

    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }

    public boolean f(Object arg) {
/*
    1、 isInstance()方法用于检查给定对象是否是该类表示的对象的实例。
    2、 isInstance()方法是一种非静态方法,只能通过类对象访问,如果尝试使用类名称访问该方法,则会收到错误消息。
 */
        return kind.isInstance(arg);
    }

    public static void main(String[] args) {
        ClassTypeCapture<Building> ctt1 =
                new ClassTypeCapture<Building>(Building.class);
        System.out.println(ctt1.f(new Building()));
        System.out.println(ctt1.f(new House()));
        ClassTypeCapture<House> ctt2 =
                new ClassTypeCapture<House>(House.class);
        System.out.println(ctt2.f(new Building()));
        System.out.println(ctt2.f(new House()));
    }
}
Output:
    true
    true
    false
    true

擦除补偿使我们能够创建泛型对象的实例。但这需要使用Class对象来间接创建。

六、通配符

有时候希望传入的类型有一个指定的范围,从而可以进行一些特定的操作,这时就引入了通配符

Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法, 主要有以下三类:

  • < ? > 无界通配符
  • < ? extends E >(基类型通配符/协变泛型):extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
  • < ? super E >(超类型通配符/逆变泛型):super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

1. 无界通配符< T >

无界通配符< ? >看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。

public class Test{
    public static void printList(List<?> list) {
        for (Object elem : list)
            System.out.print(elem + "");
        System.out.println();
    }
}

PS:List< Object >与List< ? >并不等同,List< Object >是List< ? >的子类。还有不能往List< ? > list里添加任意对象,除了null。

2. 协变泛型/基类型通配符

在引入协变泛型与逆变泛型之前,先解释一下什么是协变、逆变:
协变子类向父类转换 如:Animal a1 = new Cat();
逆变父类向子类转换(需要强装) 如:Cat a2 = (Cat)a1;

image.png

public class ExtendsTest {
    public static void printIntValue(List<? extends Number> list) {
        for (Number number : list) {
            System.out.print(number.intValue() + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>();
        integerList.add(2);
        integerList.add(2);
        printIntValue(integerList);
        List<Float> floatList = new ArrayList<>();
        floatList.add(3.3f);
        floatList.add(0.3f);
        printIntValue(floatList);
    }
}
Output:
    [2, 2]
    [3.3, 0.3]

3. 超类型通配符/逆变泛型

image.png

public class SuperTest {
    public static void main(String[] args) {
        List<Number> list = new ArrayList<Number>();
        list.add(1);
        fillNumberList(list);
        System.out.println(list);
    }
    private static void fillNumberList(List<? super Integer> list) {
        list.add(0);
    }
}
Output:
    [1, 0]

七、泛型的限制

1. 不能使用new E()
不能使用泛型类型参数来创建实例。
出错的原因:运行时执行的是new E(),但是运行时泛型类型E时不可用的(被擦除了)

2. 不能使用new E[capacity]
不能使用泛型类型参数创建数组。
可以通过创建一个Object类型的数组,然后将它的类型转换为E[]来规避这个限制。

3. 在静态上下文中不允许类的参数是泛型类型
由于泛型类的所有实例都有相同的运行时类,所以泛型类的静态变量和方法是被它的所有实例所共享的。
因此,在静态方法、数据域或者初始化语句中,为类引用泛型参数是非法的。

4. 异常类不能是泛型
泛型类不能扩展java.long.Throwable(即 不能继承于异常类)
如果继承了异常类,则子类需要添加catch子句,且运行时要捕获,但这个于“运行时,泛型信息不可得”相冲突了,所有,不能继承异常类。

public class Limit<E> {
    // 限制1: 不能使用new E()
    E object1 = new E();

    E Object2 = (E) new Object();

    // 限制2: 不能使用new E[capacity]
    E[] elements1 = new E[5];

    E[] elements2 = (E[]) new Object[5];

    // 限制3: 在静态上下文中不允许类的参数是泛型类型
    public static E o1;
    public static void m(E o1){}
    static {
        E o2;
    }

}
// 限制4: 异常类不能是泛型
class MyEXception<T> extends Exception{

}

image.png

八、Reference

  1. Java核心知识:泛型机制详解
  2. java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
  3. 秒懂Java泛型
  4. Java泛型深入理解
  5. Java总结篇系列:Java泛型
  6. 秒懂Java泛型 - 知乎
  7. 《Java编程思想》
  8. 《Java语言程序设计与数据结构》