Java编程思想之泛型(上)

一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
多态算是一种泛化机制。我们可以将方法的参数类型设为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数。同样的,也可以将方法的参数设为接口,任何实现了该接口的类都能够满足该方法。但是,如果我们希望达到的目的是编写更通用的代码,要使代码能够应用于“某种不具体的类型”,而不是一个具体的接口或类呢?

泛型实现了参数化类型的概念,使代码可以应用于多种类型。在你创建参数化类型的一个实例时,编译器会为你负责转型操作,并且保证类型的正确性。

1 简单泛型

泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。
使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类的时候,再用实际的类型替换此类型参数。在下面的这个例子中,T就是类型参数:

public class Holder<T>{
    private T a;
    public Holder(T a){
        this.a = a;
    }
    public void set(T a){
        this.a =a;
    }
    public T get(){
        return a;
    }
    public static void main(String[] args){
        Holder<Integer> h = new Holder<Integer>(2);
        Integer a = h.get();    //not cast needed
        //h.set("2");   不兼容的类型: String无法转换为Integer
        h.set(2);
    }
}

当你创建Holder对象时,必须指明想持有什么样类型的对象,将其置于尖括号内,然后,你就只能在Holder中存入该类型 (或其子类,因为多态与泛型不冲突)的对象了。并且,在你从Holder中取出它持有的对象时,自然地就是正确的类型。
这就是Java泛型的核心概念:* 告诉编译器想使用什么类型,然后编译器帮你处理一切细节。*
在使用泛型时,我们只需指定它们的名称以及类型参数列表即可。

元组

仅一次方法调用就能返回多个对象,可是return 语句只允许返回单个对象,我们可以创建一个对象,用它来持有想要返回的多个对象。这个概念称为元组(tuple),它是将一组对象直接打包存储于其中的一个单一对象。这个容器对象允许读取其中元素,但是不允许像其中存放新的对象。下面是一个二维元组,它能够持有两个对象:

public class TwoTuple<A,B>{
    public final A first;
    public final B second;  //如果想要使用具有不同元素的元组,就必须另外新创建一个TwoTuple对象
    public TwoTuple(A a,B b){
        first = a;
        second = b;
    }
    public String toString(){
        return "("+first+", "+second+")";
    }
}

元组隐含地保持了其中元素的次序。我们可以利用继承机制来实现长度更长的元组。

2 泛型接口

public interface Generator<T>{
    T next();
}

接口使用泛型和类使用泛型没什么区别。

3 泛型方法

同样可以在类中包含参数化方法。是否拥有泛型方法,与其所在的类是否是泛型没有关系。另外,对于一个 static 的方法而言,无法访问泛型类的类型参数,所以如果static方法需要使用泛型能力,就必须使其成为泛型方法。
要定义泛型方法,只需将泛型参数列表置于返回值之前,就想下面这样:

public class GenericMethods{
    public <T> void f(T x){
        System.out.println(x.getClass().getName());
    }
    public static void main(String[] args){
        GenericMethods gm = new GenericMethods();
        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
GenericMethods
*/

注意,当使用泛型类时,必须在创建对象的时候指定类型参数的值,而在使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型,这称为* 类型参数推断*
类型推断只对赋值操作有效,其他时候并不起作用。如果你将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行类型推断。这种情况下:编译器认为:调用泛型方法后,其返回值被赋给一个Object类型的变量。不过可以使用显示指明类型来解决。
在泛型方法中,可以显示地指明类型。必须在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内。如果是在定义该方法的类的内部,必须在点操作符之前使用this 关键字,如果是使用static 的方法,必须在点操作符之间加上类名。
例如:f(New.<Person>list());\\list()作为New类的一个static方法

4 擦除

在泛型代码内部,无法获得任何有关泛型参数类型的信息。你可以知道类型参数标识符和泛型类型边界。
Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此List< String >和List < Integer > 在运行时事实上是相同的类型。这两种形式都被擦除成它们的“原生”类型,即 List 。

为了调用参数类型中的方法,我们必须协助泛型类,给定泛型边界,以此告知编译器只能接受遵循这个边界的类型。例如:上面的方法f(),如果想将一个Person对象传入,并且我们想在 f ( ) 方法中调用Person对象的get()方法,直接使用x.get()嘛?你怎么能知道get()方法是为类型参数T而存在的呢?在这里我们可以使用边界来告知编译器,更改f()方法的签名如下:public <T extends Person> void f(T x )

* 泛型类型参数将擦除到它的第一个边界,编译器实际上会把类型参数替换为它的擦除。*就像上面的示例一样,T擦除到了Person,就好像在声明中用Person替换了T一样。
即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转型。

5 擦除的补偿

擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道确切类型信息的操作都将无法工作:

if(arg instanceof T){}  //Error
T var = new T();    //Error
T[] array = new T[size];    //Error
T[] array = (T)new Object[size];    //Unchecked warning

可以通过引入类型标签来对擦除进行补偿。这意味着你需要显式地传递你的类型的Class对象,以便你可以在类型表达式中使用它。
如果引入类型标签,就可以转而使用动态的isInstance():

class Building{}
class House extends Building{}
public class ClassType<T>{
    Class<T> kind;
    public ClassType(Class<T> kind){
        this.kind = kind;
    }
    public boolean f(Object arg){
        return kind.isInstance(arg);  //it`s ok
    }
    public static void main(String[] args){
        ClassType<Building> ct1 = new ClassType<Building>(Building.class);
        System.out.println(ct1.f(new Building()));
        System.out.println(ct1.f(new House()));
        ClassType<House> ct2 = new ClassType<House>(House.class);
        System.out.println(ct2.f(new Building()));
        System.out.println(ct2.f(new House()));
    }
}
/*
运行结果为:
true
true
false
true
*/

编译器将确保类型标签可以匹配泛型参数。同样的,可以使用类型便签来创建新的实例:
Class<T> kind; kind.newInstance(); //必须具有默认的构造器

泛型数组

不能创建泛型数组,一般的解决方案是在任何想要创建泛型数组的地方都使用ArrayList:

 public class ListOfGenerics<T>{
     private List<T> array = new ArrayList<T>();
     public void add(T item){array.add(item);}
     public T get(int index){return arrag.get(index);}
 }

你将获得数组的行为,以及由泛型提供的编译期的类型安全。

...
private Object[] array;
public T[] rep(){
    return (T[])array;  //warning:unchecked cast
}
...

rep()方法将在编译时产生警告,运行时产生异常。问题在于数组将跟踪它们的实际类型,而这个类型是在数组被创建时确定的,因此即使array被转型为特定的类型数组(T[]),这个信息也只存在于编译期(并且如果没有@SuppressWarnings注解,将得到这个转型的警告)。因此没有任何方式可以推翻底层的数组类型,它只能是Object[]。仍然可以通过类型标记来从擦除中恢复信息,使得我们可以创建需要的实际类型的数组。

private T[] array;
@SuppressWarnings("Unchecked")
public GenericArrayWithTypeToken(Class<T> type,int sz){
    array = (T[])Array.newInstance(type,sz);
}
public T[] rep(){return array;} //一旦我们获得了实际类型,就可以返回它。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值