【Java基础】之深入讲解泛型

泛型的介绍


☕️ 什么是泛型?

泛型,即"参数化类型",顾名思义,就是将类型由原来的具体的类型参数化。类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在调用的时候传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

☕️ 为什么要使用泛型?

我们来举一个例子:

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);

for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
    Log.d("泛型测试","item = " + item);
}

毫无疑问,程序的运行结果会以崩溃结束:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

ArrayList 可以存放任意类型,例子中添加了一个 String 类型,添加了一个 Integer 类型,再使用时都以 String 的方式使用,因此程序崩溃了。所以,为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。

我们将第一行声明初始化 list 的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题:

List<String> arrayList = new ArrayList<String>();
arrayList.add("aaaa");
arrayList.add(100);		// 编译的时候会报错

所以通过使用泛型,可以把类型明确的工作推迟到创建对象或调用方法的时候,让编译器帮我们检查类型问题:只要在编译时期没有出现警告,那么运行时期就不会出现ClassCastException异常

☕️ 类型擦除

什么是类型擦除?

擦除的意思是从泛型类型中清除类型参数的相关信息,并且在必要的时候添加类型检查和类型转换的方法。我们可以把类型擦除简单的理解为将泛型 java 代码转换为普通 java 代码。泛型擦除的主要过程如下:

  • 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换
  • 移除所有的类型参数

我们来看一下这个例子:

public static void main(String[] args) {  
    Map<String, String> map = new HashMap<String, String>();  
    map.put("name", "chenHao");  
    map.put("age", "23");  
    System.out.println(map.get("name"));  
    System.out.println(map.get("age"));  
} 

反编译后的代码为:

public static void main(String[] args) {  
    Map map = new HashMap();  
    map.put("name", "chenHao");  
    map.put("age", "23");  
    System.out.println((String)map.get("name"));  
    System.out.println((String)map.get("age"));  
} 

我们可以发现,擦除之后,泛型都不见了,程序变成了 Java 泛型出现之前的写法。

下面我们通过例子来看一下泛型的擦除过程:

public interface Comparable<T> {
	public int compareTo(T that);
}

public final class NumericValue implements Comparable<NumericValue> {
    private byte value;

    public NumericValue(byte value) {
        this.value = value;
    }

    public byte getValue() {
        return this.value;
    }

    public int compareTo(NumericValue that) {
        return this.value - that.value;
    }
}

反编译之后如下:

interface Comparable {
  public int compareTo( Object that);
} 

public final class NumericValue implements Comparable {
	private byte value;

    public NumericValue(byte value)
    {
        this.value = value;
    }
    public byte getValue()
    {
        return value;
    }
    public int compareTo(NumericValue that)
    {
        return value - that.value;
    }

    // 编译器提供的桥接方法
    public volatile int compareTo(Object obj)
    {
        return compareTo((NumericValue)obj);
    }  
}

我们可以看到泛型类 Comparable 的泛型参数 <T> 擦除后,T 被替换为最左边界 Object。但由于 Comparable 的类型参数 NumericValue 被擦除后,直接导致类 NumericValue 没有实现接口 Comparable 的 compareTo(Object that) 方法,于是编译器充当好人,添加了一个桥接方法。

也就是说,如果没有限定类型参数的边界,则擦除后的类型就会被替换为最左边界 Object,下面是提供类型参数边界的例子:

public class Collections {
    public static <T extends Comparable<T>> T max(Collection<T> xs) {
        Iterator<T> xi = xs.iterator();
        T w = xi.next();
        while (xi.hasNext()) {
            T x = xi.next();
            if (w.compareTo(x) < 0)
                w = x;
        }
        return w;
    }
}

反编译后:

public class Collections {
    public static Comparable max(Collection xs) {
        Iterator xi = xs.iterator();
        Comparable w = (Comparable)xi.next();
        while (xi.hasNext()) {
            Comparable x = (Comparable)xi.next();
            if (w.compareTo(x) < 0)
                w = x;
        }
        return w;
    }
}

从中我们可以看到,由于限定了类型参数的边界 <T extends Comparable<T>>,T 必须为 Comparable<T> 的子类,所以,按照类型擦除的过程,先将类型参数 T 替换为最左边界 Comparable<T>,然后去掉类型参数 <T>,得到上面的最终结果。


泛型的使用


泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

📝 泛型类

在类的定义中使用了泛型类型就被称为泛型类,最典型的就是各种容器类,如:List、Set、Map。

泛型类的基本写法如下:

// T 表示泛型标识,可以是任意标识,比如:T、E、K、V
public class Generic<T> {
	// 成员变量的类型为 T,T的类型由外部指定
	private T key;

	// 泛型构造方法形参 key 的类型为 T
	public Generic(T key) {
		this.key = key;
	}

	// 方法返回值类型为 T
	public T getKey() {
		return key;
	}

	public static void main(String[] args) {
		// 实例化对象时需要传入具体的类型,不能是基本类型
        Generic<Integer> intGeneric = new Generic<Integer>(1234);
        Generic<String> strGeneric = new Generic<String>("Hello");

        System.out.println(intGeneric.getKey() + strGeneric.getKey());
    }
}

>>>>>
1234 Hello

这里需要注意的是:

  • 泛型的类型参数只能是类类型,不能是基本类型
  • 不能对确切的泛型类型使用 instanceof 操作。比如:if(testNum instanceof Generic){ } 的操作是非法的,编译时会出错

📝 泛型接口

泛型接口与泛型类的定义及使用基本相同,泛型接口常被用在各种类的生产器中,下面我们看一个例子:

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

// 实现接口的类如果没有传入具体泛型实参,则需要把泛型的的声明添加到类中
public class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

// 实现接口的类如果传入具体泛型实参,则不需要把泛型的的声明添加到类中
// 并且需要把类里面的所有泛型标识符 T 替换为具体的类型
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)];
    }
}

📝 泛型方法

泛型类,是在实例化类的时候指明泛型的具体类型,而泛型方法,是在调用方法的时候指明泛型的具体类型。

来看一下泛型方法的基本写法:

public <T> T genericMethod(Class<T> tClass)
		throws InstantiationException , IllegalAccessException{

        T instance = tClass.newInstance();
        return instance;
}

例子中,我们需要注意几点:

  • public 与返回值 T 之间的 <T> 表示声明此方法为泛型方法
  • 只有声明了 <T>,该方法才是泛型方法,哪怕方法中使用了泛型成员变量也不是泛型方法
  • <T> 表明该方法将使用泛型类型 T,此时才可以在方法中使用泛型类型 T
  • 与泛型类的定义一样,此处 T 可以随便写为任意标识

📝 泛型方法与可变参数

下面是泛型方法可变参数的例子:

public <T> void pringMsg(T... args) {
	for (T t : args) {
		System.out.println("t is : " + t);
	}
}

pringMsg("aaa", "bbb", 111);

>>>>>
t is : aaa
t is : bbb
t is : 111

📝 静态方法与泛型

如果静态方法要使用泛型参数或者泛型返回值的话,必须将静态方法也定义成泛型方法

public static <T> void show(T t) {
	System.out.println(t);
}

注意,即使静态方法声明为了泛型,但仍然不能使用类中其它非静态的泛型方法!


📝 泛型通配符

我们知道 Integer 是 Number 的一个子类,那么,在使用 Generic 作为形参的方法中,能否使用 Generic 的实例传入呢?在逻辑上类似于 Generic 和 Generic 是否可以看成具有父子关系的泛型类型?

我们使用一个例子来看一下:

// 定义一个参数为 Generic<Number> 的方法
public void showKeyValue(Generic<Number> obj){
    System.out.println("泛型测试:key value is " + obj.getKey());
}

// 创建 Generic<Number> 和 Generic<Integer> 的实例
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

// 调用方法
showKeyValue(gNumber)

>>>>>
编译器报错:
Generic<java.lang.Integer> cannot be applied to Generic<java.lang.Number>

通过提示信息我们可以看到 Generic 不能被看作为 Generic 的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

类型统配符就是为了解决这个问题而产生的,类型通配符可以一个在逻辑上可以同时表示是 Generic 和 Generic 父类的引用类型。

我们把方法改成统配符的情况:

// 定义一个参数为 Generic<Number> 的方法
public void showKeyValue(Generic<?> obj){
    System.out.println("泛型测试:key value is " + obj.getKey());
}

这样就可以解决问题了,类型通配符一般使用 “?” 代表具体的类型实参,注意:通配符 ? 表示的是类型实参,而不是类型形参!意思就是,通配符 ? 和 Number、String、Integer 一样都是一种实际的类型,可以把 ? 看成所有类型的父类,是一种真实的类型。

所以:

  • 当具体类型不确定的时候,可以使用通配符 ? 代替
  • 当操作类型而不需要使用类型的具体功能时,可以使用通配符 ? 来表示位置类型

📝 泛型上下边界

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

上边界

上边界即传入的类型实参必须是指定类型的子类型,使用的是 extends 关键字,比如:

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

这里传进来的类型 T 必须继承于 Number,即 T 是 Number 的子类。

下边界

下边界即传入的类型实参必须是指定类型的父类型,使用的是 super 关键字,比如:

public class Generic <T super Integer> {
	...
}

这里传进来的类型 T 必须是 Number 的父类。

我们可以这么理解,父为上,子为下,所以上限限定的是父类,下限限定的是子类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值