Java Generics泛型 看这一篇就够了

本文详细介绍了Java泛型的使用,包括泛型的基本概念、泛型接口、擦拭法实现、泛型的局限、通配符的运用,如extends和super通配符,以及PECS原则。通过实例展示了如何在实际编程中安全有效地使用泛型,避免类型转换异常,提高代码的类型安全性。
摘要由CSDN通过智能技术生成

Java使用擦拭法实现泛型,编译器内部永远把所有类型T视为Object处理

使用泛型

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

  • 泛型就是编写模板代码来适应任意类型

  • 泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查

  • 注意泛型的继承关系:可以把ArrayList向上转型为ListT不能变!),但不能把List向下转型为ArrayListT不能变成父类)

ArrayList和ArrayList两者完全没有继承关系

泛型接口

Arrays.sort(Object[]) 可以对任意数组进行排序,但待排序的元素必须实现Comparable这个泛型接口:

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

String等内置类型已经实现了Comparable接口

如果换成我们自定义的Person类型,需要让Person实现Comparable接口,否则会报告ClassCastException的错误:

// sort
import java.util.Arrays;

public class Main {
    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));
    }
}

//实现泛型接口,指定类型:
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);
    }
    public String toString() {
        return this.name + "," + this.score;
    }
}

实现泛型接口,不指定类型:
class Person<T> implements Comparable<T> {
}    

常用的通配符为: T,E,K,V,?

  • ? 表示不确定的 java 类型
  • T (type) 表示具体的一个 java 类型
  • K V (key value) 分别代表 java 键值中的 Key Value
  • E (element) 代表 Element

总结

  • 使用泛型时,把泛型参数T替换为需要的class类型,例如:ArrayList<String>ArrayList<Number>

  • 可以省略编译器能自动推断出的类型,例如:List<String> list = new ArrayList<>();

  • 不指定泛型参数类型时,编译器会给出警告,且只能将T视为Object类型

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

编写泛型

编写泛型

通常来说,泛型类一般用在集合类中,例如ArrayList,我们很少需要编写泛型类

  • 编写泛型时,需要定义泛型类型<T>

  • 静态方法不能引用泛型类型<T>,必须定义其他类型(例如<K>)来实现静态泛型方法

  • 泛型可以同时定义多种类型,例如Map<K, V>

如何自定义一个泛型类:

  • 先按照某种类型,例如:String,来编写类

  • 再把特定类型String替换为T,并在类头部申明<T>

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;
    }
}

静态方法

编写泛型类时,要特别注意,泛型类型<T>不能用于静态方法

对于静态方法,我们可以单独改写为“泛型”方法,只需要使用另一个类型即可。这样将静态方法的泛型类型和实例类型的泛型类型区分开。

    // 静态泛型方法应该使用其他类型区分:
    public static <K> Pair<K> create(K first, K last) {
        return new Pair<K>(first, last);
    }

多个泛型类型

泛型还可以定义多种类型。例如,我们希望Pair不总是存储两个类型一样的对象,就可以使用类型<T, K>

擦拭法

Java语言的泛型实现方式是擦拭法(Type Erasure),即虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

例如上例是编译器看到的代码,而虚拟机根本不知道泛型。这是虚拟机执行的代码:

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

因此,Java使用擦拭法实现泛型,编译器内部永远把所有类型T视为Object处理,导致了:

  • 编译器把类型<T>视为Object
  • 编译器根据<T>实现安全的强制转型

例如如下非安全代码:

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

list.add(12);
//这里直接添加会报错
list.add("a");

//但是通过反射添加,是可以的
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
add.invoke(list, "kl");

System.out.println(list);
Java泛型的局限

局限一:<T>不能是基本类型,例如int,因为实际类型是ObjectObject类型无法持有基本类型:

Pair<int> p = new Pair<>(1, 2); // compile error!

局限二:无法取得带泛型的Class

Pair<String> p1 = new Pair<>("Hello", "world");
Pair<Integer> p2 = new Pair<>(123, 456);
Class c1 = p1.getClass();
Class c2 = p2.getClass();
System.out.println(c1==c2); // true
System.out.println(c1==Pair.class); // true

所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是Pair<Object>

局限三:无法判断带泛型的Class

Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>.class) {
}

原因和前面一样,并不存在Pair.class,而是只有唯一的Pair.class

局限四:不能实例化T类型:

public class Pair<T> {
    private T first;
    private T last;
    public Pair() {
        // Compile error:
        first = new T();
        last = new T();
    }
}

上述代码无法通过编译,因为构造方法的两行语句:

first = new T();
last = new T();

擦拭后实际上变成了:

first = new Object();
last = new Object();

这样一来,创建new Pair()和创建new Pair()就全部成了Object,显然编译器要阻止这种类型不对的代码。

要实例化T类型,我们必须借助额外的Class参数:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(Class<T> clazz) {
        first = clazz.newInstance();
        last = clazz.newInstance();
    }
}

上述代码借助Class参数并通过反射来实例化T类型,使用的时候,也必须传入Class。例如:

Pair<String> pair = new Pair<>(String.class);
不恰当的覆写方法
public class Pair<T> {
    public boolean equals(T t) {
        return this == t;
    }
}

定义的equals(T t)方法实际上会被擦拭成equals(Object t),而这个方法是继承自Object的,编译器会阻止一个实际上会变成覆写的泛型方法定义。

此时换个方法名即可。

泛型继承

子类可以获取父类的泛型类型<T>

public class IntPair extends Pair<Integer> {
}

在父类是泛型类型的情况下,编译器就必须把类型T(对IntPair来说,也就是Integer类型)保存到子类的class文件中,不然编译器就不知道IntPair只能存取Integer这种类型。

在继承了泛型类型的情况下,子类可以获取父类的泛型类型。例如:IntPair可以获取到父类的泛型类型Integer

Class<IntPair> clazz = IntPair.class;
Type t = clazz.getGenericSuperclass();
if (t instanceof ParameterizedType) {
   ParameterizedType pt = (ParameterizedType) t;
   Type[] types = pt.getActualTypeArguments(); // 可能有多个泛型类型
   Type firstType = types[0]; // 取第一个泛型类型
   Class<?> typeClass = (Class<?>) firstType;
   System.out.println(typeClass); // Integer
}
                      ┌────┐
                      │Type│
                      └────┘
                         ▲
                         │
   ┌────────────┬────────┴─────────┬───────────────┐
   │            │                  │               │
┌─────┐┌─────────────────┐┌────────────────┐┌────────────┐
│Class││ParameterizedType││GenericArrayType││WildcardType│
└─────┘└─────────────────┘└────────────────┘└────────────┘

extends通配符

因为Pair<Integer>不是Pair<Number>的子类,因此,add(Pair<Number>)不接受参数类型Pair<Integer>

使用Pair<? extends Number>使得方法接收所有泛型类型为NumberNumber子类的Pair类型。

static int add(Pair<? extends Number> p) {
    Number first = p.getFirst();
    Number last = p.getLast();
    return first.intValue() + last.intValue();
}

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

重要限制

泛型内部的方法参数签名setFirst(? extends Number)无法传递任何Number类型给setFirst(? extends Number)

原因还在于擦拭法。如果我们传入的pPair<Double>,显然它满足参数定义Pair<? extends Number>,然而,Pair<Double>setFirst()显然无法接受Integer类型。

extends通配符的作用
  • 允许调用get()方法获取Integer的引用;
  • 不允许调用set(? extends Integer)方法并传入任何Integer的引用(null除外)。
int sumOfList(List<? extends Integer> list) {
    int sum = 0;
    for (int i=0; i<list.size(); i++) {
        Integer n = list.get(i);
        sum = sum + n;
    }
    return sum;
}

因此,方法参数类型List表明了该方法内部只会读取List的元素,不会修改List的元素(因为无法调用add(? extends Integer)remove(? extends Integer)这些方法。

即使用extends通配符表示可以读,不能写。(恶意调用set(null)除外)。

使用extends限定T类型

在定义泛型类型Pair的时候,也可以使用extends通配符来限定T的类型:

public class Pair<T extends Number> { ... }

super通配符

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

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

即使用super通配符表示只能写不能读。唯一例外是可以获取Object的引用:Object o = p.getFirst()

使用extendssuper通配符要遵循PECS原则。

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

public class Main {
    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.getFirst() + ", " + p1.getLast());
        System.out.println(p2.getFirst() + ", " + p2.getLast());
    }

    static void setSame(Pair<? super Integer> p, Integer n) {
        p.setFirst(n);
        p.setLast(n);
    }
}
extends vs 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);
        }
    }
}
PECS原则

Producer Extends Consumer Super。

如果需要返回T,它是生产者(Producer),要使用extends通配符;

如果需要写入T,它是消费者(Consumer),要使用super通配符。

无限定通配符
void sample(Pair<?> p) {
}

既不能读,也不能写,那只能做一些null判断

<?>通配符有一个独特的特点,就是:Pair<?>是所有Pair<T>的超类:

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<?>的子类,可以安全地向上转型。

泛型与反射

  • 可以声明带泛型的数组,但不能直接创建带泛型的数组,必须强制转型。因为泛型数组T[]擦拭后代码变为Object[]

  • 可以通过Array.newInstance(Class, int)创建T[]数组,需要强制转型

Java的部分反射API也是泛型。例如:Class就是泛型,构造方法Constructor也是泛型:

Class<Integer> clazz = Integer.class;
Constructor<Integer> cons = clazz.getConstructor(int.class);
Integer i = cons.newInstance(123);

我们可以声明带泛型的数组,但不能用new操作符创建带泛型的数组:

Pair<String>[] ps = null; // ok
Pair<String>[] ps = new Pair<String>[2]; // compile error!

使用泛型数组要特别小心,因为数组实际上在运行期没有泛型,编译器可以强制检查变量ps,因为它的类型是泛型数组,但编译器不会检查变量arr,因为它不是泛型数组。

因为这两个变量实际上指向同一个数组,所以,操作arr可能导致从ps获取元素时报错,例如,以下代码演示了不安全地使用带泛型的数组:

Pair[] arr = new Pair[2];
Pair<String>[] ps = (Pair<String>[]) arr;

ps[0] = new Pair<String>("a", "b");
arr[1] = new Pair<Integer>(1, 2);

// ClassCastException:
Pair<String> p = ps[1];
String s = p.getFirst();

要安全地使用泛型数组,必须扔掉arr的引用。必须通过强制转型实现带泛型的数组:

@SuppressWarnings("unchecked")
Pair<String>[] ps = (Pair<String>[]) new Pair[2];

内部必须借助Class来创建泛型数组:

T[] createArray(Class<T> cls) {
    return (T[]) Array.newInstance(cls, 5);
}

我们还可以利用可变参数创建泛型数组T[]

public class ArrayHelper {
    @SafeVarargs
    static <T> T[] asArray(T... objs) {
        return objs;
    }
}

String[] ss = ArrayHelper.asArray("a", "b", "c");
Integer[] ns = ArrayHelper.asArray(1, 2, 3);

如果在方法内部创建了泛型数组,最好不要将它返回给外部使用

参考:
廖雪峰等

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

郎涯技术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值