Java:泛型(深入解析,一文读懂)

基本概念和原理


为什么使用泛型:

在没有使用泛型之前,一旦把一个对象“丢进”Java集合中,集合就会忘记对象的类型,把所有的对象当成Object类型处理。当程序从集合中取出对象后,就需要进行强制类型转换,这种强制类型转换不仅是代码臃肿,而且容易引起ClassCastException异常。

标题的基本概念:

所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。

使用泛型的好处:
  1. 更好的安全性
    通过使用泛型 ,开发环境和编译器能确保不会用错类型,为程序多设置一道安全防护网。

  2. 更好的可读性
    使用泛型,可以省去繁琐的强制类型转换,再加上明确的类型信息,代码可读性也会更好。

泛型的使用:
public class Test<T> {
    T first;
    T second;
    public Test(T first, T second) {
        this.first = first;
        this.second = second;
    }
    public T getFirst() {
        return first;
    }
    public T getSecond() {
        return second;
    }
}

Test是一个泛型类,与普通类的区别:

  • 类名后多了一个<T>;
  • first和second的类型都是T;

T表示类型参数,泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。

泛型的原理:

Java有Java编译器和Java虚拟机,编译器将Java源代码转换为.class文件,虚拟机加载并运行.class文件。对于泛型类,Java编译器会将泛型代码转换为普通的非泛型代码,就像上面的普通Test类代码及其使用代码一样,将类型参数T擦除,替换Object,插入必要的强制类型转换。Java虚拟机实际执行的时候,它是不知道泛型这回事的,只知道普通的类及代码。

泛型擦除:

Java泛型是通过擦除实现额,类定义中的类型参数如T会被替换为Object,在程序运行过程中,不知道泛型的实际类型参数,比如Test<Integer>,运行中只知道Test,而不知道Integer。

深入泛型

//定义接口时指定了一个泛型形参,该形参名为E
public interface Test<E> {
    
    //在接口方法里,E可作为类型使用
    //下面方法可以使用E作为类型参数
    void add(E x);
    Iterable<E> iterator();
    
	//在接口里,E完全可以作为类型使用
    E next();
}
public interface Map<K, V> {

    //在接口里K、V完全可以作为类型使用
    Set<K> keySet();
    V put(K key, V value);
}

解释:
允许在定义接口、类时声明泛型形参,泛型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通方法类型的地方都可以使用这种泛型形参。

注:
当创建泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。 例如,为Test<T>类定义构造器,其构造器名依然是Test,而不是Test<T>;!调用该构造器时却可以使用Test<T>的形式,当然应该为T形参传入实际的类型参数。Java7提供了“菱形”语法,允许省略<>中的类型实参。

从泛型类派生子类:

方法中的形参代表变量、常量、表达式等数据,本文把它们直接称为形参,或者称为数据形参。定义方式时可以声明数据形参,调用方法(使用方法)时必须为这些数据形参传入实际的数据;于此类似的是,定义类、接口、方法时可以使用声明泛型形参,使用类、接口、方法时应该为泛型形参传入实际的类型。

//定义类A继承Apple类,Apple类不能跟泛型形参
public class A extends Apple<T> {}   //错误

//使用Apple类时为T形参传入String类型
public class A extends Apple<String> //正确

调用方法时必须为所有的数据形参传入参数值, 与调用方法不同的是,使用类、接口时也可以不为泛型形参传入实际的类型参数,即下面代码也是正确的。

public class A extends Apple  //正确

像这种使用Apple类时省略泛型的形式被称为原始类型(raw type)。
如果使用Apple类时没有传入实际的类型(即使用原始类型),Java编译器可能发出警告:使用了未经检查或不安全的操作 - - 就是泛型检查的警告。

并不存在泛型类:

看如下代码:

List<String> list = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
//调用getClass()方法来比较list和list2的类是否相等
System.out.println(list.getClass() == list2.getClass());

运行上面的代码片段,可能有读者认为应该输出false,但实际输出true。因为不管泛型的实际类型参数是什么,它们在运行时总有同样的类(Class)。
不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类来处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量(它们都是类相关的)的声明和初始化中不允许使用泛型形参。

public class Test<T> {

    //下面代码错误,不能在静态变量声明中使用泛型形参
    static T info;
    
    //正确
    T age;
    public void foo(T msg) { }

    //下面代码错误,不能再静态方法声明中使用使用泛型形参
    public static void bar(T msg) {}
}

由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。

java.util.Collection<String> cs = new java.util.ArrayList<String>();
//下面代码编译时引起错误:instanceof运算符后不能使用泛型
if(cs instanceof java.util.ArrayList<String>) {}
使用类型通配符:

为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?>(意思是元素类型未知的List)。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。
看如下代码:

public void test(List<?> c) { }
设定类型通配符的上限(协变):

指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型或其子类),不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)。

设定类型通配符的下限(逆变):

除可以指定通配符的上限之外,Java也允许指定通配符的下限,通配符的下限用<? super 类型>的方式类指定,通配符下限的作用与通配符上限的作用恰好相反。
Foo是Bar的子类,当程序需要一个A<? super Foo>变量时,程序可以将A<Bar>、A<Object>赋值给A<? super Foo>类型的变量,这种方式称为逆变。
对于逆变的泛型来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从集合中取元素时只能被当成Object类型处理(编译器无法确定取出的到底是哪个父类的对象)。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值