Java泛型之一

Bruce说学习泛型的最大障碍是类型擦除,泛型的博文我分成两篇,这篇主要是类型擦除、边界、通配符,使用泛型的约束和局限性以及可能带来的问题放在另外一篇,作者水平有限,如有错误或者不足,欢迎交流讨论。

0 前言

一般的类和方法,要么使用基本类型,要么使用自定义的类,不能编写应用与多种类型的代码,在Java增加泛型之前,泛型程序设计是使用继承实现的,即使用Object类型或者Object数组。采用继承实现会带来安全性和可读性的问题,安全性即每次都要手动进行类型强制转换,可读性即不直观。
采用泛型后,转型由编译器来完成,保证类型的正确性,具有更好的可读性。

1 泛型类与泛型方法

1.1 简单泛型类

定义泛型类,将泛型参数列表放在类名后即可

package generic;
/**
 * 简单泛型类
 * @author 小锅巴
 * @date 2016年4月14日下午6:30:49
 * http://blog.csdn.net/xiaoguobaf
 */
public class Pair<T> {
    private T first;
    private T second;

    public Pair(){
        first = null;
        second = null;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public void setSecond(T second) {
        this.second = second;
    }

}

1.2 泛型方法

同样,定义泛型方法将泛型参数列表放在返回值之前即可。使用泛型方法时不必指定参数类型,编译器会自己找出具体类型,这被称为类型参数推断

package generic;
/**
 * 泛型方法
 * @author 小锅巴
 * @date 2016年4月14日下午6:37:32
 * http://blog.csdn.net/xiaoguobaf
 */
public class GenericMethod {
    public <T> void f(T x){
        System.out.println(x.getClass().getName());
    }

    public static void main(String[] args) {
        GenericMethod gm = new GenericMethod();
        gm.f("");
        gm.f(1);//使用基本类型将会被自动拆箱装箱机制转换为对应的包装类
        gm.f(1.0);
        gm.f(1.0f);
        gm.f('c');
        gm.f(gm);
    }
}
/**
输出:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
generic.GenericMethod
 */

2 类型擦除与边界(限定)

2.1 类型擦除

JVM中所有对象属于普通类,并没有泛型类型对象,那么泛型是如何实现的呢?Java泛型是使用类型擦除来实现的,所谓类型擦除,就是在运行时具体的类型信息被擦除掉了,变成了原生类型(raw type),即去掉类型参数后的泛型类型名,类型参数则被擦除为边界类型(无边界则为Object类型)。

package generic;

import java.util.ArrayList;

/**
 * 运行时类型擦除
 * @author 小锅巴
 * @date 2016年4月14日下午18:56:06
 * http://blog.csdn.net/xiaoguobaf
 */
public class ErasedTypeEquivalence {
    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1);
        System.out.println(c2);
        System.out.println(c1 == c2);
    }
}
/**
输出:
class java.util.ArrayList
class java.util.ArrayList
true
 */

2.2 边界(限定)

重用extends关键字,用来设置泛型参数类型的限制条件。类型参数可以是类、接口或通配符(放在后面),边界中的类最多有一个,还可以有接口,接口可以有多个,如果有多个边界,类必须放在第一位,多个边界类型使用&分隔。有边界后,擦除为第一个类型。

package chapter15;

import java.awt.Color;

/**
 * 边界
 * @author 小锅巴
 * @date 2016年4月12日下午7:36:23
 * http://blog.csdn.net/xiaoguobaf
 */
interface HasColor{
    java.awt.Color getColor();
}

interface Weight{
    int weitht();
}

class Dimension{
    public int x, y, z;
}

class Colored<T extends HasColor>{
    T item;

    Colored(T item){
        this.item = item;
    }

    T getItem() {
        return item;
    }

    //边界允许调用方法
    java.awt.Color color(){
        return item.getColor();
    }
}

class ColoredDimension<T extends Dimension & HasColor>{//使用extends必须先类后接口
    T item;

    ColoredDimension(T item){
        this.item = item;
    }

    T getItem() {
        return item;
    }

    java.awt.Color color(){
        return item.getColor();
    }

    int getX(){
        return item.x;
    }
    int getY(){
        return item.y;
    }
    int getZ(){
        return item.z;
    }
}

class Solid<T extends Dimension & HasColor & Weight>{//只能继承自一个类,但可以有多个接口  
//使用extends关键字更接近子类的概念,如果非要强调是接口的子类型,那么就要增加一个新的关键字
    T item;

    Solid(T item){
        this.item = item;
    }

    T getItem() {
        return item;
    }

    java.awt.Color color(){
        return item.getColor();
    }

    int getX(){
        return item.x;
    }
    int getY(){
        return item.y;
    }
    int getZ(){
        return item.z;
    }

    int weight(){
        return item.weitht();
    }
}

class Bounded extends Dimension implements HasColor, Weight{

    @Override
    public int weitht() {
        // TODO Auto-generated method stub
        return 0;
    }

    @Override
    public Color getColor() {
        // TODO Auto-generated method stub
        return null;
    }

}
public class BasicBounds {

    public static void main(String[] args) {
        Solid<Bounded> solid = new Solid<>(new Bounded());
        solid.color();
        solid.getY();
        solid.weight();
    }
}

3 通配符

通配符包括通配符上限、通配符下限、无界通配符。
1、带有子类型限定的通配符,通配符限制为某一类型的子类型(包括该类型),使用extends关键字,一般用于从泛型对象读取(返回确定的类型),即使用子类型限定的通配符的函数有泛型返回值

2、带有超类型限定的通配符,通配符被限制为某一类型的超类型(包括该类型),使用super关键字,一般用于向泛型对象写入(写入确定的类型),即使用超类型限定的通配符函数不会有泛型返回值有一点可能会让人很费解,对于带有超类型限定的通配符的方法(注意是方法,不能是类),实际类型可以该类型以及其子类型,后来在java核心技术上看到一个例子,这样做是有意义的。

3、无界通配符,表示某种非原生类型的特定类型,但是还不知道是哪种类型,与原生类型看起来很像,但是不一样,原生类型可以传递给各种变体,无界通配符却不可以,它有确定的类型,只是还不确定是哪一种。

Linus Torvalds说过,Talk is cheap,show me the code。光看文字表述真没啥用,还是得看代码。

例子1:

package generic;
/**
 * 通配符
 * @author 小锅巴
 * @date 2016年4月12日下午8:58:02
 * http://blog.csdn.net/xiaoguobaf
 */
class Fruit{}
class Orange extends Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
public class Holder<T> {
    private T value;

    public Holder(){}
    public Holder(T value){
        this.value = value;
    }

    public T get() {
        return value;
    }
    public void set(T value) {
        this.value = value;
    }

    public boolean equals(Object obj){
        return value.equals(obj);
    }

    public static void main(String[] args) {
        Holder<Apple> Apple = new Holder<>(new Apple());
        Apple d = Apple.get();
        Apple.set(d);
//      Holder<Fruit> Furit = Apple;//不能转型,编译器没那么聪明,需要通配符
//      Holder<Fruit> Furit = new Fruit();//不能转型,不是Holder的泛型
//      Holder<Fruit> Furit = new Holder();//由于类型擦除,可以
        Holder<? extends Fruit> fruit = Apple;
        Fruit p = fruit.get();
//      Apple P = fruit.get();//Apple引用不能直接引用fruit,原因很简单,子类型很多,Apple只是其中一种
        d = (Apple)fruit.get();//向下转型为Apple
        try{
            Orange c = (Orange)fruit.get();//通过编译,运行时抛出异常,转型不正确
        }catch(Exception e){
            System.out.println(e);
        }
        System.out.println(fruit.equals(d));//虽然Apple不能直接引用fruit,但是实际上fruit还是Apple类型的
//      fruit.set(new Apple());//因为set的参数是? extends Fruit,可能是继承自Fruit的任何一种,不能保证安全性
//      fruit.set(new Fruit());
    }
}
/**
输出:
java.lang.ClassCastException: chapter15.Apple cannot be cast to chapter15.Orange
true
 */

例子2:

package generic;
/**
 * 原生类型与无界统配符的区别,
 * @author 小锅巴
 * @date 2016年4月14日下午11:51:29
 * http://blog.csdn.net/xiaoguobaf
 */
public class Wildcards1 {
    //使用了原生类型,将放弃编译检查 ,因为泛型是类型擦除实现的,所以实际的holder可以是Holder的各种变体,
    static void rawArgs(Holder holder, Object args){
        holder.set(args);
        //holder是原生类型,holder.value被擦除为Object,故args可以为任何类型,它将被转型为Object
        //但是Holder是泛型的,所以传Object给set是不安全的,这里给出了警告,下面同理
        holder.set(new Wildcards1());

//      T t = holder.get();
        //方法不是泛型的,不能这样,就算是泛型的,修改后会提示:Type mismatch: cannot convert from Object to T,原因还是因为原生类型和类型擦除
        Object obj = holder.get();
    }

    //使用无界通配符,表示holder.value是某种具体类型的非原生Holder,但是具体是哪种,还不知道,下面两个Error都是这个原因,holder.value和arg的类型不匹配
    static void unboundedArg(Holder<?> holder, Object arg){
//      holder.set(arg);//
//      holder.set(new Wildcards1());

//      T t = holder.get();
        Object obj = holder.get();//
    }

    public static void main(String[] args) {
        Holder raw = new Holder<Long>();
        raw = new Holder();
        Holder<Long> qualified = new Holder<Long>();
        Holder<?> unbounded = new Holder<Long>();
        Holder<? extends Long> bounded = new Holder<Long>();
        Long lng = 1L;

        rawArgs(raw, lng);
        rawArgs(qualified, lng);
        rawArgs(unbounded, lng);
        rawArgs(bounded, lng);

        unboundedArg(raw, lng);
        unboundedArg(qualified, lng);
        unboundedArg(unbounded, lng);
        unboundedArg(bounded, lng); 
    }
}

例子3:

package generic;
/**
 * 带子类型限定的通配符与带超类型限定的通配符
 * @author 小锅巴
 * @date 2016年4月15日下午2:28:10
 * http://blog.csdn.net/xiaoguobaf
 */
public class Wildcards3 {
    static <T> T wildSubtype(Holder<? extends T> holder, T arg){
//      holder.set(arg);//丢失向其传递任何对象能力,holder是实际类型的一种子类型,具体哪种就不知道了,所以不能写入,只能读取
//      holder.set(new Object());//甚至是Objct
//      holder.set(new Holder<T>());
//      holder.set(new Holder());
        T t = holder.get();
        return t;//根据实际具体的类型返回
    }

    static <T> void wildSupertype(Holder<? super T> holder, T arg){
        holder.set(arg);
//      T t = holder.get();
        //同样,holder是实际类型的一种确切父类型,具体哪种就不知道了,所以读出只能用Object引用,读出的类型与实际的类型不相符,所以不用来读出,用来写入
        Object obj = holder.get();
    }

    static <T> void Super(Holder<? super T> holder){}

    public static void main(String[] args) {
        Holder raw = new Holder<Long>();
        raw = new Holder();
        Holder<Long> qualified = new Holder<Long>();
        Holder<?> unbounded = new Holder<Long>();
        System.out.println(unbounded.getClass().getSimpleName());//运行显示是原生类型Holder,原因是类型擦除
        Holder<? extends Long> bounded = new Holder<Long>();
        Long lng = 1L;

        Long r9 = wildSubtype(raw, lng);//wildSubtype形参是通配符上限,实参希望得到在原生类型中不存在的信息,故给出警告;由于类型擦除,可以传递原生类型
        Long r10 = wildSubtype(qualified, lng);
//      Long r11 = wildSubtype(unbounded, lng);
        //wildSubtype中的Holder参数必须是继承自T的一种,即子类型的一种,就好比Apple类继承自Fruit类,那么只能是Apple类,而不能是Orange,而unbounded不能有这个保证
        Long r12 = wildSubtype(bounded, lng);  

        Holder<? extends Fruit> Apple = new Holder<Apple>();

        wildSupertype(raw, lng);//类型不确定,是raw引起的
        Super(raw);//警告同上

        wildSupertype(qualified, lng);//qualified是具体的类型,无警告
//      wildSupertype(unbounded, lng);//unbounded不是确定的类型,错误
//      wildSupertype(bounded, lng);//bounded是Long子类型的一种,可能情况很多。这个例子不好,用Fruit更好理解
    }

}

然后再来看看令人费解的带超类型通配符的方法:

package generic;

import java.util.*;

/**
 * 令人费解的带超类型限定通配符
 * @author 小锅巴
 * @date 2016年4月12日下午8:30:10
 * http://blog.csdn.net/xiaoguobaf
 */
public class GenericAndCovariance {

    public static void main(String[] args) {
        List<? extends Fruit> flist = new ArrayList<Apple>();//flist是Fruit的一种子类型,不一定就是Apple的
        //编译器并不知道这一点,向上转型后将丢失掉向其中写入任何对象的能力,甚至是向flist中添加Apple、Object
//      flist.add(new Apple());//Error
//      flist.add(new Fruit());//Error
//      flist.add(new Object());//Error
        flist.add(null);//合法但没什么用
        Fruit f1 = flist.get(0);
//      Apple f2 = flist.get(0);//Error,flist中的元素是Fruit的一种子类型,但是不一定就是Apple,如果是泛型方法,则可以返回正确的实际类型

        List<? extends Fruit> flist_1 = new ArrayList<Fruit>();
        List<? extends Fruit> flist_2 = new ArrayList<Orange>();

        List<? super Apple> flist_3 = new ArrayList<Fruit>();       
//      List<? super Apple> flist_4 = new ArrayList<Jonathan>();//Error,不是父类型       

/**********************************************************************************************************/        
        flist_3.add(new Apple());
        flist_3.add(new Jonathan());
        //不是说好的是父类型(包括自己)么,怎么可以写入子类型?
/**********************************************************************************************************/    

        System.out.println(flist_3.get(0).getClass().getSimpleName());
        Apple apple1 = (Apple)flist_3.get(0);
//      Apple apple2 = flist_3.get(0);//Error,因为flist_3里面的内容可能是Apple的多种父类型中的一种,所以只能是Object
        Object apple3 = flist_3.get(0);
//      flist_3.add(new Object());//Error
//      flist_3.add(new Fruit());//Error
        flist_3.add(null);
    }
}

我的理解,假若类型B继承类型A,若方法是带有超类型的通配符,是包括类型A的,由于B继承自A,类B与类A的关系是is-a关系,B也是类型A,对于针对类型A的方法,类型B也是适用的,所以可以写入类型B。

应用:

package exception;

import java.util.*;

/**
 * 带有超类型限定通配符的应用
 * @author 小锅巴
 * @date 2016年4月15日下午11:41:18
 * http://blog.csdn.net/xiaoguobaf
 */
public class SuperBoundary {
    public static <T extends Comparable<T>> void g(T[] a){}

    public static <T extends Comparable<? super T>> void h(T a){}

    public static void main(String[] args) {
        GregorianCalendar gregorianCalendar = new GregorianCalendar(); 
//      g(gregorianCalendar);//Error
        h(gregorianCalendar);
    }
}

以下是Comparable接口和Calendar的源码截取:

public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {
    public interface Comparable<T> {  
        public int compareTo(T o);
    }
}    
    @Override
    public int compareTo(Calendar anotherCalendar) {
        return compareTo(getMillisOf(anotherCalendar));
    }

Calendar实现了Comparable接口,而GregorianCalendar继承自Calendar,即GregorianCalendar是实现的是Comparable< Calendar >,而不是Comparable< GregorianCalendar>,所以GregorianCalendar类型不适用g方法,但是通过带有超类型限定的通配符,如h方法中所示,GregorianCalendar适用。对于类似这种情况,传入某类型的子类型就很有用。

参考资料:
1. Java编程思想
2. Java核心技术

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值