Java泛型程序设计

 

之前学Java时对泛型没有好好学习和理解,但是很多源码都涉及到泛型程序设计,所以借着书在整理记录下。

以下内容借鉴于  《Java核心技术 卷Ⅰ》

目录

1.为什么使用泛型程序设计

1.1 类型参数的好处

1.2 谁想成为泛型程序员

2. 定义简单泛型类

3.泛型方法

4.类型变量的限定

5. 泛型代码和虚拟机

5.1 类型擦除

5.2 翻译泛型表达式

5.3 翻译类型方法

5.4 调用遗留代码

6. 约束与局限性

6.1 不能用基本类型实例化类型参数

6.2 运行时类型查询只适用于原始查询

6.3 不能创建参数化类型的数组

6.4 Varags 警告


 

 

 

 

1.为什么使用泛型程序设计

泛型程序设计(Gernric Programing)意味着编写的代码可以被很多不同类型的对象所重用。

例如,我们并不希望为聚集String和File对象分别设计不同的类。 实际上,用ArrayList就可以聚集任何类型的对象,因为ArrayList就是一个泛型程序设计的实例。

1.1 类型参数的好处

    public class ArryList{
        private Object[] elementData;
        ...
        public Object get(int i){...}
        public void add(Object o){...}
    }

上面 是Java还没增加泛型前的ArrayList实现,通过维护一个Object数组来实现功能。

如果要储存一串String时,每次都要通过强制类型转换然后进行赋值,这样很不方便而且也不安全,经常没办法转换成功而报错。

泛型提供了一个更好的解决方案:类型参数(type parameter)。例如:

ArrayList<String> files = new ArrayList<>();

上面的代码具有更好的可读性,一看就知道是String数组。

而且使用get方法时不需要进行类型转换,编译器知道返回String。使用add方法时,同样如此,而且编译器会检查是否插入了错误的类型对象,会无法通过编译的。相比于之前的方法,编译错误比运行时出现转换异常要安全得多。

类型参数的魅力在于:使得程序具有更好的可读性和安全性。

 

1.2 谁想成为泛型程序员

实现一个泛型类并没有那么容易,对于类型参数,使用这段代码的程序员可能想内置(plug in)所有的类。他们希望在没有过多的限制以及混乱的错误消息的状态下,做所有的事情。

因此,一个泛型程序员的任务就是预测出所用类的未来可能有的所有用途。

这个任务是相当困难的,为此Java语言设计者发明了一个具有独创性的新概念,通配符类型(wildcard type),它们能让库的构建者编写出尽可能灵活的方法。

所以,泛型能够带给我们的:重用性、更方便、更安全

后面会介绍更多的书中知识。

 

2. 定义简单泛型类

一个泛型类(generic class)就是具有一个或多个类型变量的类。使用一个简单的Pair类作为例子。(只关注泛型)

public class Pair<T> {
    private T first;
    private T second;

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

    public Pair(T first, T second) { this.first = first;this.second = second; }

    public T getFirst() { return first; }

    public T getSecond() { return second; }

    public void setFirst(T newValue) { first = newValue; }

    public void setSecond(T newValue) { second = newValue; }
}

Pair类引入了一个类型变量T,用尖括号(<>)扩起来,并放在类名的后面。泛型类可以有多个类型变量。

例如可以定义Pair类,第一个域与第二个域不同:

public class Pair<T,U> {...}

在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型,T表示“任意类型”。

用具体的类型替换类型变量就可以实例化泛型类型。例如:

Pair<String>()
Pair<String>(String,String)

String getFirst()
void setFirst(String)
...

换句话说,泛型类可看作普通类的工厂。

 

3.泛型方法

除了上述定义的泛型类方法,实际上还可以定义一个带有类型参数的简单方法。

public class ArrayAlg {
    public static <T> T getMiddle(T... a){
        return a[a.length/2];
    }
}

这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法,可以从尖括号和类型变量看出一点。

注意,类型变量放在修饰符(这里是public static)的后面,返回类型的前面。

泛型方法可以定义在普通类中,也可以定义在泛型类中。

当调用一个泛型方法是,在方法名前的尖括号中放入具体的类型:

String middle = ArrayAlg.<String>getMiddle("John","Q.","Public");

上述情况下,方法调用中可以省略<String>类型参数,编译器通过后面的String[]可以推测T一定为String。

String middle = ArrayAlg.getMiddle("John","Q.","Public");

当然也有一些情况,编译器也会报错,比如下面这种:

double middle = ArrayAlg.getMiddle(3.14,110,0);

上面这种就没办法确定是什么具体类型参数,因为后两个参数是Integer类型,除了写上<Double>外,还可以将后面两个参数改成doble值。

 

4.类型变量的限定

有时,类或方法需要对类型变量加以约束。比如对泛型数组进行最小比较时,需要确定数组中的类型都实现了Comparable接口。

    public static <T> T min(T[] a){
        if (a==null||a.length==0)
            return null;
        T smallest=a[0];
        for (int i=1;i<a.length;i++){
            if (smallest.compareTo(a[i])>0)
                smallest=a[i];
        }
        return smallest;
    }

上面的代码就需要将T限制为实现了Comparable接口(其实Comparable本身也是一个泛型类型),可以对T设置限定(bound)实现这一点:

public static <T extends Comparable> T min(T[] a){...}

这里可能会有困惑的是为什么写extends,不写成implements,毕竟Comparable是个接口。<T extends BoundingType>表示T应该是绑定类型的子类型(subtype),选择extends是因为更接近子类的概念,与其他无关,而且Java设计者们也不打算在语言中再添加新的关键字。比较重要的一点,一个类型变量或通配符可以有多个限定,例如:

T extends Comparable & Serializable

 

5. 泛型代码和虚拟机

虚拟机没有泛型类型对象——所有对象都属于普通类。

5.1 类型擦除

无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型泪姓名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object)。比如说之前写了的Pair对象:

public class Pair{
    private Object first;
    private Object second;

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

    public Pair(Object first, Object second) { this.first = first;this.second = second; }

    public Object getFirst() { return first; }

    public Object getSecond() { return second; }

    public void setFirst(Object newValue) { first = newValue; }

    public void setSecond(Object newValue) { second = newValue; }
}

因为T是一个无限定的变量,所以直接用Object替换。

如果有限定类型变量,则选择第一个限定的类型变量来替换,例如:

public class Interval<T extends Comparable & Serializable> implements Serializable{
    private T lower;
    private T upper;
    //...
    public Interval(T first,T upper){
        //...
    }
}

则原始类型Interval如下:

public class Interval implements Serializable{
    private Comparable lower;
    private Comparable upper;
    //...
    public Interval(Comparable first,Comparable upper){
        //...
    }
}

5.2 翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如:

Pair<Employee> buddies = ...
Employee buddy = buddies.getFirst();

编译器会把这个方法翻译成两条虚拟机指令

  • 对原始方法Pair.getFirst调用
  • 将返回的Object类型强制转换为Employee

当存取泛型域时也要插入强制类型转换,例如:

Employee buddy = biddies.first;

5.3 翻译类型方法

这部分会有点绕。

方法的擦除带来了两个复杂问题。看下面的示例:

class DateInterval extends Pair<LocalDate>{
    public void setSecond(LocalDate second){
        if(second.compareTo(getFist())>=0)
            super.setSecond(second);
    }
//...
}

一个日期区间是一对LocalDate对象,并且需要覆盖这个方法来确保第二个值永远不小于第一个值。这个类擦除后:

class DateInterval extends Pair{
    public void setSecond(LocalDate second){
        //.....
    }
//...
}

这时候还存在另一个从Pair继承的setSecond(Object second)方法。

这时候希望调用最合适的方法,编译器就需要在DateInterval中生成一个桥方法(bridge method):

public void setSecond(Object second){
    setSecond((Date) second);
}

第二个问题是桥方法有时候会变得很奇怪。例如DateInterval中有个函数是getSecond():

class DateInterval extends Pair<LocalDate>{
    public localDate getSecond(){
        return (Date) super.getSecond().clone();
    }
}

这时候也会产生另一个函数 Object getSecond(),这时候具有相同参数的两个方法,这样编写java代码是不合法的(虽然他们的返回类型不一样)。但是,在虚拟机中,用参数类型和返回类型确定一个方法,因此,虚拟机可能产生仅两个返回类型不同的字节码。

总之,需要记住有关Java泛型转换的事实:

  • 虚拟机中没有泛型,只有普通的类和方法
  • 所有的类型参数都用他们的限定类型替换
  • 桥方法被合成来保持多态
  • 为保持类型安全性,必要时插入强制类型转换

5.4 调用遗留代码

设计Java泛型类型时,主要目标是允许泛型代码和遗留代码之间能够互操作。

 

6. 约束与局限性

下面将会阐述使用Java泛型时需要考虑的一些限制。大多数限制都是由类型擦除引起的。

6.1 不能用基本类型实例化类型参数

如小标题所说,所以没有Pair<double>,只有Pair<Double>。原因是类型擦除,擦出之后,Pair类含有Object类型的域,而Object不能存储double值。

6.2 运行时类型查询只适用于原始查询

虚拟机中的对象总有一个特定的非泛型类型。因此所有的类型查询只产生原始类型。例如:

if(a instanceof Pair<String>) //Error

if(a instanceof Pair<T>) //Error

同样的道理,getClass方法总是返回原始类型。例如:

Pair<String> stringPair = ... ;
Pair<Employee> employeePair = ... ;
if(stringPair.getClass()==employee.getClass()) //they are eqaul

上面的两次调用getClass都将返回Pair.class。

6.3 不能创建参数化类型的数组

不能实例化参数化类型的数组,例如:

Pair<String>[] table = new Pair<String>[10];//ERROR

擦除后table的类型是Pair[],可以把它转换为Object[]。数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStoreException异常。

需要说明的是,只是不允许创建这些数组,而生命类型为Pair<String>[]的变量鞥是合法的。不过不能用new Pair<String>[10]初始化这个变量。

可以声明统配类型的数组,然后进行类型转换:

Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];

但是上面的方法不安全,如果table[0]存储一个Pair<Employee>,然后对table[0].getFirst() 调用一个String方法,会得到一个ClassCastException异常。

如果需要收集参数化类型对象,只有一种安全而有效的方法:使用ArrayList:ArrayList<Pair<String>>

6.4 Varags 警告

上一节我们已经了解到,Java不支持泛型类型的数组

这节讨论一个问题:像参数个数可变的方法传递一个泛型类型的实例。

public static <T> void addAll(Collection<T> coll,T... ts){
    for(t : ts) coll.add(t);
}
Collection<Pair<String>> table=...;
Pair<String> pair1=...;
Pair<String> pair2=...;
addAll(table,pair1,pair2);

为了调用这个方法,Java虚拟机必须建立一个Pair<String>数组,这样违反了之前的规则,不过这种情况,规则有所放松,你会得到一个警告而不是错误。可以对调用了addAll的方法加注解@SuppressWarnings("unchecked")或者对addAll加上注解@SafeVarargs

6.5 不能实例化类型变量

不能使用像new T(...),new T[...]或T.class这样的表达式中的类型变量。

Java SE 8之后,最好的解决办法是让调用者提供一个构造器表达式。例如:

Pair<String> p=Pair.makePair(String::new);

上面涉及到1.8新特性lambda表达式,makePair方法接收一个Supplier<T>,这是个函数式接口,表示一个无参数而且返回类型为T的函数:

public static <T> Pair<T> makePair(Supplier<T> constr)
{
    return new Pair<>(constr.get(),constr.get());
}

也可以像下面这样:

public static <T> Pair<T> makePair(Class<T> c1){
   try{
       return new Pair<>(c1.newInstance(),c1.newInstance());
   }catch (Exception e){
       return null;
   }
}

调用的话:Pair<Stirng> p = Pair.makePair(String.class);

注意,Class本身是泛型。例如String.class是一个Class<String>的实例,因此makePair能判断出pair的类型。

6.6 不能构造泛型数组

 

 

 

 

//持续更新中........

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值