[Advanced Java] 3 泛型

文章介绍了Java泛型的概念、产生原因、泛型类和方法、通配符、边界以及泛型与继承的关系。泛型的主要目的是保证类型安全,消除烦琐的类型转换,并增强代码的表达能力。通配符和边界允许更灵活的类型约束,而泛型擦除则是为了保持与早期Java版本的兼容性。然而,擦除也带来了一些限制,如无法实例化类型参数、不能用instanceof判断、不能抛出或捕获泛型异常等。
摘要由CSDN通过智能技术生成

3 泛型

泛型是Java的一种机制。

但设计者为了向下兼容,出现了一些新的问题。

3.1 泛型概述

泛型利用参数化类型(参数化类型:将所操作的数据类型被指定为一个参数。例如定义方法时用形参,调用方法时用实参)使得编写的代码适用于广泛的类型。同时,在使用参数类型的时候,可以根据需要指定它使用的类型。

3.1.1 泛型的产生原因

1. 泛型保证程序的类型安全并消除了一些烦琐的类型转换

在引入泛型之前,一般的程序都使用多态和继承来提高代码的灵活性和重用性。

List list1 = new ArrayList();
List list2 = new ArrayList();

list1.add(new Integer());
list2.add(new String("字符串"));

Integer i = (Integer)list1.get(0);
String s = (String)list2.get(0);
  • 问题1:很多时候明明确定List中存放的类型,却仍然需要显示转换。

    List integerList = new ArrayList();
    integerList.add(new Integer(1));
    integerList.add(new Integer(1));
    ...
    integerList.add(new Integer(1));
    for(Objct i:integerList){
        System.out.println((Integer)i);
    }
    
  • 问题2:只允许存放Integer的List中添加字符串类型时,编译器并不会报错,这样会导致运行时从List中取出的元素无法转化为Integer类型并抛出异常。

    List integerList = new ArrayList();
    integerList.add(new Integer(1));
    ...
    integerList.add(new String("失误"));
    integerList.add(new Integer(1));
    for(Objct i:integerList){
        System.out.println((Integer)i);
    }
    

泛型机制的设计很好地解决了上面这些问题。

2. 泛型使得Java编写的代码具有更加广泛的表达能力

Java语言是一种强类型语言。强类型语言通常是指在编译或运行时,数据的类型有较为严格的区分,不同数据类型的对象在转型时需要进行严格的兼容性检查。

Integer i;
String s = "i";
age = s;//在Java中是不合法的;但在Python中合法。
  • 问题:强类型的语言对提高程序的健壮性,提高开发效率有利,但强类型语言导致一个问题:数据类型与算法在编译时绑定,这意味着必须为不同的数据类型编写相同的逻辑代码。

    public Integer add(Integer a1, Integer a2) {
        return a1+a2;
    }
    public Float add(Float a1, Float a2) {
    	return a1+a2;
    }
    public Double add(Double a1, Double a2) {
        return a1+a2;
    }
    

继承是解决这个问题的方法之一:为适用这一算法的多种数据类型抽象一个基类(或者接口),针对这个基类(或者接口)编写算法。但在实际程序设计过程中,专门为特定算法修改数据类型的设计,这不是个好习惯。另外,如果过使用已经设计好的数据类型,问题就无法解决。

泛型是解决这一问题的有效方法之一。泛型的最大价值在于:在保证类型安全的前提下,把算法与数据类型解耦。

3.2 泛型类型

在介绍之前,需要了解泛型字母的含义:

字母意义
E-Element在集合中的元素
T-TypeJava类
K-Key
V-Value
N-Number数值类型
3.2.1 泛型类

定义方法

修饰符 class 类名<代表泛型的变量> {}

//单个
public class Person<T> {}
//多个,类里面不包含Person中的T
public class Teacher<V, S> extends Person {}
//多个,类里面包含Person中的T
public class Teacher<T, S> extends Person<T> {}

使用方法

Person<Integer> p = new Person<>(1);
3.2.2 泛型方法

定义

修饰符 <泛型> 返回值类型方法名(参数列表(使用泛型)){方法体;}

//单个
public <T> void method1(T t) {}
//多个
public <K,V> void method2(K k, V v) {}
//静态
public static <T> void method3(T t) {}

使用

method1("1");
method3("1");
method2("1", 1);
3.2.3 泛型接口

定义

修饰符 interface 接口名<代表泛型的变量> {}

public interface GenericInterface<E> {
    public abstract void add(E e);
    public abstract E getE();
}

使用

//方法一
public class GenericInterfaceImpl01 implements GenericInterface<String>{
    @Override
    public void add(String s) {}
    @Override
    public String getE() {return null;}
}
//方法二
public class GenericInterfaceImpl02<E> implements GenericInterface<E> {
    @Override
    public void add(E e) {}
    @Override
    public E getE() {return null;}
}

3.3 通配符

3.3.1 通配符的产生原因

泛型的作用之一在于:使得Java编写的代码具有更加广泛的表达能力。意思就是,对于同样的算法逻辑,我仅仅编写一个方法即可。

但是存在一个问题,若方法体内传入的参数是泛型类,则这个作用就无法得到实现。

public static void printArray(ArrayList<Number> list) {}

public static void testArray(){
    ArrayList<Number> list1 = new ArrayList<>();
    ArrayList<Integer> list2 = new ArrayList<>();
    ArrayList<String> list3 = new ArrayList<>();
    printArray(list1);
    printArray(list2);//不合法
    printArray(list3);//不合法
}

Java设计者为此添加了通配符类型。它可以表示任何类型,通配符类型的符号是“?”。

3.3.2 通配符的使用方法
public static void printArray(ArrayList<?> list) {}

public static void testArray(){
    ArrayList<Number> list1 = new ArrayList<>();
    ArrayList<Integer> list2 = new ArrayList<>();
    ArrayList<String> list3 = new ArrayList<>();
    printArray(list1);
    printArray(list2);
    printArray(list3);
}

3.4 泛型边界

边界是指为某一区域划定一个界限,在界限内是允许的,超出了界限就不合法。Java的泛型边界是指为泛型参数指定范围,在范围内可以合法访问,超出这个边界就是非法访问。

Java泛型系统允许使用类型通配符上限(extends)和通配符下限(super)关键字设置边界。extends仅允许泛型值为其本身或其子类;super仅允许泛型值为其本身或其父类。(实际上类似于判断放入的类是否满足规定,True就允许,然后将类型隐式转化为规定类;False就警告)

3.4.1 含边界的泛型类

设置含边界的泛型类是为了该类为某些特定的类使用。

public class NumberFactory<T extends Number> {
    public void printNumberClass(Class<T> clazz) {}
}
public class Test {
    public static void main(String[] args) {
        NumberFactory<Integer> integerFactory = new NumberFactory<>();
        integerFactory.printNumberClass(Integer.class);
        
        NumberFactory<String> stringFactory = new NumberFactory<>();//不合法
    }
}
3.4.2 含边界的泛型方法

设置含边界泛型方法是为了该方法为某些特定的类使用。与上同理,这里不再举例。

3.4.3 通配符与边界的使用

通配符与边界使用表示传入的参数类型需要满足特定的规范。

public static  void  getElement1(Collection<? extends  Number> coll) {}
public  static  void  getElement2(Collection<? super  Number> collection) {}

Collection<Integer> list1 = new ArrayList<Integer>();
Collection<String> list2 = new ArrayList<String>();
Collection<Number> list3 = new ArrayList<Number>();
Collection<Object> list4 = new ArrayList<Object>();

/*Collection<? extends  Number> coll*/
getElement1(list1);
getElement1(list2);//不合法
getElement1(list3);
getElement1(list4);//不合法

/*Collection<? super  Number> collection*/
getElement2(list1);//不合法
getElement2(list2);//不合法
getElement2(list3);
getElement2(list4);

3.5 泛型与继承

警告:泛型与继承存在误区。泛型的类型设定不满足继承关系,而放入类型满足继承关系。

List<Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
numberList = integerList;//不合法
numberList.add(new Integer(1));//合法

//通配符和上下界结合作用:
List<? extends Number> numberList = new ArrayList<Number>();
List<? extends Number> integerList = new ArrayList<Integer>();
numberList = integerList;//合法

3.6 泛型擦除

泛型擦除是指泛型代码在编译后,都会被擦除成原生类型。

List<Integer> integerList = new ArrayList<>();
List<String> stringList = new ArrayList<>();
integerList.getClass().equals(stringList.getClass());//true

虽然是同样的编译结果,但是Java的编译器在类型参数擦除之前对泛型的类型进行了安全验证,以确保泛型的类型安全。例如:

List<Integer> integerList = new ArrayList<>();
integerList.add("str");//不合法
3.6.1 为何要擦除

擦除并不是一种语言特性,而是Java泛型实现的一种折中办法。因为泛型在JDK5之后才是Java的组成部分,因此这种折中是必需的。擦除的核心动机是使得泛化的代码可以使用非泛化的类库,反之亦然,这称为“迁移性兼容”。

因此Java泛型必须支持向后兼容性,即现有的代码和类库依然合法,而且与在没有泛型的JDK上运行效果一致。为了让非泛型代码和泛型代码共存,擦除成为一种可靠实用的方法。

3.6.2 如何擦除

擦除的实质是在编译过程中:

  1. 将泛型类转化为原生类型。
  2. 将原有的类型参数换成即非泛化的上界,若未指定上界则为Object。

例如:

public class Zoo<T extends Animal> {
    private T t;
    public Zoo(T t) {
        this.t = t;
    }
    public T pop() {
        return this.t;
    }
}

经过编译后:

public class Zoo {//Zoo<T extends Animal>
    public Zoo(Animal t) {//T
        this.t = t;
    }
    public Animal pop() {//T
        return t;
    }
    
    private Animal t;//T
}
3.6.3 多边界擦除

对于单边界的泛型,擦除时使用其上界替换参数类型。对于多边界的泛型擦除,则与边界声明的顺序有关,Java编译器将选择排在前面的边界进行参数替换。例如:

public class Zoo<T extends Flyable&Speakable> {
    private T t;
    public Zoo(T t) {
        this.t = t;
    }
    public T pop() {
        return this.t;
    }
}

经编译后:

public class Zoo {
    public Zoo(Flyable t) {
        this.t = t;
    }
    public Flyable pop() {
        return t;
    }
    
    private Flyable t;
}

交换Flyable和Speakable后编译代码为:

public class Zoo {
    public Zoo(Speakable t) {
        this.t = t;
    }
    public Speakable pop() {
        return t;
    }
    
    private Speakable t;
}

3.7 泛型的限制问题

3.7.1 擦除限制

擦除机制使得泛型类在运行时丢失了泛型信息,因此一些被认为时理所当然的功能,在Java泛型系统中并得不到支持。

1. 类型参数的实例化

public class Zoo<T> {
    private T t = new T();//不合法
}

为了保证类型安全,Java并不允许使用类型参数类实例化对象。因为运行时参数类型信息被擦除,无法确定类型参数T所代表的具体类型,拥有无参的构造函数,甚至T所代表的具体类型可能不能被实例化,例如抽象类。

2. instanceof判断类型

Zoo<Bird> birdZoo = new Zoo<bird>();
if(birdZoo instanceof Zoo<Bird>) {}

代码看上去没有问题,但是JVM总是提示Cannot perform instanceof check against parameterized tpye Z, Use instead its raw form Zoo since generic tpye information will be erased at runtime错误,说明instanceof不能用于参数化类型的判断上,建议使用原生类型,因为类型信息将在运行时被擦除。

对于一个泛型类来说,即使其参数类型有多种不同,但在运行时他们都共享着一个原生对象。

3. 抛出或捕获参数类型信息

在Java异常系统中,泛型类对象时不能被抛出或捕获的,因为泛型类是不能继承实现Throwable接口及其子类的。

public class GenericException<T> extends Exception {}//不合法
3.7.2 擦除冲突

泛型的擦除有可能导致与多态发生冲突。例如:

public class Animal<T> {
    public void set(T t) {}
}
public class Bird extends Animal<String> {
    public void set(String name) {
        super.set(name);
    }
}
public class GenericTest {
    public static void main(String[] args) {
        Bird bird = new Bird();
        Animal<String> animal = bird;
        animal.set("Bird");
    }
}

由于Java的方法调用采用的时动态绑定的方式,所以呈现出多样性。但擦除导致了一个问题:由于擦除的原因,泛型方法set(T t)被擦除成set(Object t),而在子类中存在方法set(String name),我们本意时让子类的set方法覆盖父类的set方法,但擦除致使他们成了两个不同的方法,类型擦除与多态产生了冲突。

为了解决这个问题,Java编译器会在子类中生成一个桥的方法。例如,上面的Bird类经过编译后:

public class Bird extends Animal {
    public Bird() {}
    public void set(String name) {
        super.set(name);
    }
    public volatile void set(Object obj) {
        set((String) obj);
    }
}

在父类引用animal调用set方法时,因为父类的set被重写为桥方法,JVM会首先调用桥方法。然后由桥方法调用子类Bird的set(String name)方法。除了传入参数的多态冲突,同时还存在另外一种获取参数的多态冲突:

public class Animal<T> {
    public T get() {return null;}
}
public class Bird extends Animal<String> {
    public String get() {return null;}
}
public class GenericTest {
    public static void main(String[] args) {
        Bird bird = new Bird();
        Animal<String> animal = bird;
        animal.get();
    }
}

Bird类编译后的代码为:

public class Bird extends Animal {
    public Bird() {}
    public String get() {return null;}
   	public volatile Object get() {
        return get();
    }
}
3.7.3 类型安全和转换

因为擦除会导致运行时类型信息的丧失,JVM为了运行Java时保证类型安全和取消不必要的类型转换,把类型安全控制和类型转换放到了编译时进行。例如:

List<Integer> list = new ArrayList<>();
list.add(3);
Integer i = list.get(0);

编译后:

List list = new ArrayList();
list.add(Integer.valueOf(3));
Integer i = (Integer) list.get(0);

经过翻译后,从list取出元素时,编译器自动增加了转型代码,这就是在使用list时,无需手动转型的原因。可以推断在编译的时候,编译器也做了其他泛型操作以确保类型安全。

3.7.4 泛型数组

Java中不能声明泛型类数组,例如:

List<Integer>[] list = new ArrayList<Integer>[2];

该代码编译时时无法通过的,因为擦除后List会失去泛型的特性变成List[]。

3.7.5 擦除总结

排除使Java程序在运行时丧失了类型变量的具体信息,因此在使用泛型时要牢记一下内容:

  1. 虚拟机中没有泛型。
  2. 所有的类型参数都将被擦除成边界。
  3. 为确保多态,必要时合成了桥方法。
  4. 类型安全检查和类型转换实在编译时运行的,必要时插入额外代码。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值