Java基础之泛型

自JDK1.5后,Java引入了泛型。泛型,即“参数化类型”。将类型由原来具体的类型参数化,类似于方法中的变量参数,此时参数也定义成参数形式(可以称之为类型形参),然后在使用时传入具体的类型(类型实参)。

为什么需要泛型

在泛型出现之前,集合中可以存放任意非基本类型,取出来的时候要强转,因此使用的时候可能会出现ClassCastException错误。例如,在下面的代码中,List中存放了Integer类型、String类型以及Boolean类型,在使用的时候强转必然会抛异常。

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {
		List list = new ArrayList();
		list.add(1);
		list.add("11");
		list.add(true);
		for(int i=0;i<list.size();i++){
			System.out.println((String) list.get(i));
		}
    }
}
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
	at Main.main(Main.java:13)

泛型出现之后,集合中只能存在指定类型的数据,以上提到的问题在编译期编译器就会报错而不会等到运行时才出错。

可以看到List在添加Integer类型和Bollean类型的时候编译器已经报错了,并且在for循环中String强转也变成灰色,即不需要强转了。另外,使用泛型也利于代码的复用。

泛型的使用

泛型可以使用在类、接口以及方法的定义中,使用了泛型的类、接口和方法,分别称作泛型类、泛型接口和泛型方法。

  1. 泛型类:在类名后加上泛型参数即可,也可以添加多个参数,例如<T,K>,<T,K,V>。这里的T,K,V都是一种表现形式而已,并没有什么特殊的意义,你愿意的话也可以定义为<Q>、<W>、<E>、<R>。
class GenericClass<T>{
    T item;
    
    public void setItem(T t) {
    	this.item=t;
    }
    
    public T getItem() {
    	return this.item;
    }
}

     2. 泛型接口:在接口名后加上泛型参数,与泛型类类似。

public interface GenericInterface<T> {
    T sing();
}

    3. 泛型方法:在方法返回值前加上泛型参数列表。下面代码中,只是用了泛型参数的方法(例如getItem()和setItem())不是泛型方法,必须要是定义了泛型参数列表的才是泛型方法。即使泛型方法中的泛型与泛型类的泛型声明都为T,该方法也是泛型方法,且这两个T之间没有任何关系。

class GenericClass<T>{
    T item;
    
    public void setItem(T t) {
    	this.item=t;
    }
  
    public T getItem() {
    	return this.item;
    }
    
    //泛型方法
    public <E> void sing(E e) {
    	System.out.println(e);
    }
}

对于泛型类和泛型接口,在创建对象时就需要指定参数类型;而对于泛型方法,在使用时再指定参数类型即可。

不可协变

在介绍泛型的不可协变之前,我们先来了解一下数组的协变性。数组的协变性指的是如果类a是类b的子类,那么a[]也是b[]的子类。类似的,不可协变是说如果类a是类b的子类,但使用a和b的泛型类之间没有任何关系,例如ArrayList<a>与ArrayList<b>没有任何关系。

举个例子,现在有水果类及其子类如下:

public class Fruit{
    
    protected String name;
    protected String color;

    public String getName(){
        return this.name;
    }

    public void setName(String name){
        this.name = name;
    }

    public String getColor(){
        return color;
    }

    public void setColor(){
        this.color = color;
    }
}

public class Apple extends Fruit{
    
    public Apple(){
        name = "apple";
        color = "green";
    }
}

public class Banana extends Fruit{
    
    public Banana(){
        name = "Banana";
        color = "yellow";
    }
}

然而在像下面这样使用时会有编译错误,印象中的向上转型在这好像不起作用了?

事实上,这是由泛型的不可协变导致的。泛型的不可协变是指Apple是Fruit的子类,但List<Apple>和List<Fruit>没有父子关系。与之相反的,数组具有协变性,指Apple是Fruit的子类,那么Apple[]也是Fruit的子类。

如上,编译可以通过。但数组的协变性也会导致Exception in thread "main" java.lang.ArrayStoreException: Banana
    at Main.main(Main.java:8),试图将错误类型的对象存储到一个对象数组时抛出的异常。

通配符

为了让泛型参数可以传入一定范围的类型,泛型中引入了通配符?来表示一种未知类型,并使用以下通配符:

  1. 上界通配符<? extents T>:只能保存T及它的子类。无法写数据,因为只允许保存一种T,不是每种T都可以保存,但可以读取数据,因为其上界为T。
  2. 下界通配符<? super T>:只能存放T及其T的基类类型的数据。可以向里边写T及其派生类的数据,但读出来的数据类型只能是Object的。
  3. 无限定通配符<?>:只读,不能增加修改。

PECS原则:上界通配符不能往里存,只能往外取,适合频繁往外面读取内容的场景;下界通配符不影响往里存,但取出来只能放在Object对象里,适合经常往里插入数据的场景。

类型擦除

泛型只在编译期起作用,在运行时泛型为了兼容JDK1.5之前的旧代码使用了“类型擦除”机制,编译器在编译过程中会移除参数的类型信息,泛型参数会被擦除到它的第一个边界,如果没有指明边界,将会被擦除到Object。

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {
		List<Integer> integerList = new ArrayList<>();
		List<String> stringList = new ArrayList<>();
		System.out.println(integerList.getClass().equals(stringList.getClass()));
    }
}

由于类型擦除机制,以上代码的运行结果为true,因为运行时传入的Integer和String被丢掉了,原始类型都变为第一个边界Object。对于List<? extends Comparable>来说,类型擦除后的原始类型变为Comparable。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值