Java编程思想 -- 泛型概括总结(一)

在面向对象编程语言中,多态算是一种泛化机制

例如你可以将方法的参数类型设为基类,那么该方法就可以接受从这个类中导出的任何类作为参数。但是,考虑到除了final类不能扩展,这种灵活性大大降低。

如果方法的参数是一个接口,而不是一个类,这种限制就放松很多,可是有时候,使用了接口,对程序的约束也还是太强了。因为一旦指明了接口,它就要求你的代码必须使用特定的接口。

Java SE5的重大变化之一就是:泛型的概念。泛型实现了参数化类型的概念,使代码可以应用于多种类型。

简单泛型

直接看例子

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<String> holder = new Holder<String>("String");
        String s = holder.get();

        // Error
        // holder.set(1);
        // holder.set(1.1);
    }
}

在这个例子中可以看出,Holder是持有对象T的类(Holder< T>),在构造器,方法参数,成员变量以及返回值都可以使用对象T的类型。

class I<T> {

        class K<T>{

        }

        class KK{
            private T t;
        }

        // Error
        // public static class KKK<T>{ }

        // public static class KKK{
        //    private static T t;
        // }
}

这里可以看到,内部类可以持有对象T的类型,而嵌套类不可以。

泛型接口

直接看例子

public interface GenericsInstance<T> {
        T next();

        void set(T t);

        class InnerClass<T>{

        }

        public static T t;
}

这里可以看到,GenericsInstance是持有对象T的接口,在其返回值,方法参数,成员变量已经嵌套类都可以使用对象T的类型

泛型方法

可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是。

泛型方法使得该方法能够独立于类而产生变化。以下是一个基本的指导原则:无论何时,只要你能做到,就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白

定义泛型方法,只需将泛型参数列表置于返回值之前

public class GenericMethods {

    // 泛型方法
    public <T> void f(T t){
        System.out.println(t.getClass().getName());
    }

    public static void main(String[] args){
        GenericMethods gm = new GenericMethods();

        gm.f("");
        gm.f(1);
        gm.f(1.2);      
    }
}

这个例子中,只有方法f()拥有类型参数T。

可以看下面例子

class T{

        public <T> T get(T t){
            T tt = null;

            return tt;
        }

        // 可变参数与泛型方法
        public static <E> void f(E... es){
            for (E e:es){
                // do something
            }
        }
}

注意,当在使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,比如前面的gm.f(1);。因为编译器会为我们找出具体的类型。这称为类型参数推断

匿名内部类

泛型还可以应用于内部类以及匿名内部类

内部类前面例子已经简单描述,这里看一下匿名内部类的例子

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

class Customer{
    private static long counter = 1;
    private final long id = counter++;

    private Customer() { }

    public String toString() { return "Customer " + id; }

    // 匿名内部类与泛型
    public static Generator<Customer> generator(){
        return new Generator<Customer>(){
            public Customer next(){
                return new Customer();
            }
        }
    }
}

擦除的神秘之处

例如,可以声明ArrayList.class,但是不能声明ArrayList< Integer>.class。

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 == c2);
    }
}
/*Output:
true
*/

上面的c1和c2被认为相同的类型

在泛型代码内部,无法获得任何有关泛型参数类型的信息

Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体类型信息都被擦除了,你唯一知道的就是你在使用一个对象。

因此,List< String>和List< Integer>在运行时事实上是相同类型,这两种形式都被擦除成为它们的“原生”类型List

下面看个例子:

public class HasF{
        public void f(){

        }
}

class Manipulator<T>{
    private T obj;
        public Manipulator(T t){
            this.obj = t;
        }

        public void manipulate(){
            // Error
            //obj.f();
        }
}

由于有了擦除,所以上面obj.f()方法不能调用。

为了调用f(),我们必须协助泛型类,给定泛型类的边界,这里使用extends关键字:

// 协助泛型类,给定泛型类的边界
class Manipulator<T extends HasF>{
        private T obj;
        public Manipulator(T t){
            this.obj = t;
        }

        public void manipulate(){
            obj.f();
        }
}

边界< T extends HasF>声明T必须具有类型HasF或者从HasF导出的类型。这样就可以安全调用f()

擦除的问题

擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作之中,例如转型、instanceof操作和new表达式

另外,使用泛型并不是强制的

class GenericBase<T>{
    private T element;
    public void set(T arg) { element = arg; }
    public T get() { return element; }
}

class Derived1<T> extends GenericBase<T> { }

class Derived2 extends GenericBase { } // No warning

// class Derived3 extends GenericBase<?> { } // Error
// class or interface without bounds

public class ErasureAndInheritance{

    @SuppressWarnings("unchecked")
    public static void main(String[] args){
        Derived2 d2 = new Derived2();
        Object obj = d2.get();
        d2.set(obj); // Warning here    
    }
}

边界处的动作

public class ArrayMaker<T> {

    private Class<T> kind;

    public ArrayMaker(Class<T> kind){
        this.kind = kind;
    }

    @SuppressWarnings("unchecked")
    T[] create(int size){
        // Type safety: Unchecked cast from Object to T[]
        return (T[]) Array.newInstance(kind, size);
    }

    public static void main(String[] args) {
        ArrayMaker<String> stringMaker = new ArrayMaker<String>(String.class);

        String[] stringArray = stringMaker.create(5);

        System.out.println(stringArray);
    }
}
/*Output:
[null, null, null, null, null]
*/

即使kind被存储为Class< T>,擦除也意味着它实际类型将被存储为Class,没有任何参数,因此,在创建数组时,这不会产生具体的结果,所以必须转型,这将产生一条警告。

注意,对于在泛型中创建数组,使用Array.newInstance()是推荐方式

泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型

擦除的补偿

由于擦除,一下操作将无法操纵

public class Erased<T>{
    private final int SIZE = 10;
    public static void f(Object arg){

        if( arg instanceof T ){ } // Error
        T var = new T();// Error
        T[] array = new T[SIZE];// Error

        T[] array = (T) new Object[SIZE];// Warning
    }
}

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

class Building { }
class House extends Building { } 

public static class ClassTypeCapture<T>{
    Class<T> kind;
    public ClassTypeCapture(Class<T> kind){
        this.kind = kind;
    }

    public boolean f(Object arg){
        return kind.isInstance(arg);
    }


    public static void main(String[] args) {

        ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);
        System.out.println(ctt1.f(new Building()));
        System.out.println(ctt1.f(new House()));

        ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);
        System.out.println(ctt2.f(new Building()));
        System.out.println(ctt2.f(new House()));

    }
}
/*Output:
true
true
false
true
*/

创建类型实例

new T()无法实现,部分原因是因为擦除,另一部分原因是因为编译器不能验证T具有默认(无参)构造器,但是C++却可以实现,因为它是在编译期受到检查

Java中的解决方案是传递一个工厂对象

class ClassAsFactory<T>{
    T x;
    public ClassAsFactory(Class<T> kind){
        try{
            x = kind.newInstance();
        }catch(Exception e){
            throw new RuntimeException(e);
        }
    }
}

class Employee { }

public class InstantiateGenericType{
    public static void main(string[] args){
        ClassAsFactory<Employee> fe = new ClassAsFactory<Employee>(Employee.class);

        // Exception
        ClassAsFactory<Integer> fi = new ClassAsFactory<Integer>(Integer.class);
    }
}

第一个可以创建,但是ClassAsFactory<Integer>失败,因为Integer没有默认的构造器,而是应该使用显示工厂

interface Factory<T>{
    T create();
}

class Foo<T>{
    private T x;
    // 构造器:public <F extends Factory<T>> Foo(f factory) 
    public <F extends Factory<T>> Foo(f factory) { 
        x = factory.create();
    }
    // ...
}

class IntegerFactory implements Factory<Integer>{
    public Integer create(){
        return new Integer(0);
    }
}

class Widget{
    public static class Factory implements Factory<Widget>{
        public Widget create(){
            return new Widget();
        }
    }
}

public class FactoryConstraint{
    public static void main(String[] args){
        new Foo<Integer>(new IntegerFactory);
        new Foo<Widget>(new WidgetFactory);
    }
}

另一种方式是模板方法设计模式

abstract class GenericWithCreate<T>{
    final T element;
    GenericWithCreate() { element = create(); }
    abstract T create();
}

class X { }

class Creator extends GenericWithCreate<X>{
    X create() { return new X(); }
    void f(){
        System.out.println(element.getClass().getSimpleName());
    }
}

public class GreatorGeneric{
    public static void main(String[] args){
        Creator c = new Creator();
        c.f();
    }
}

泛型数组

看一下例子

class Generic<T> { }

public class ArrayOfGenericReference{
    // 泛型数组
    static Generic<Integer>[] gia;
}

编译器将接受这个程序,而不会产生警告,但是,永远都不能创建这个确切类型的数组(包括类型参数)这一点令人困惑

既然所以数组无论它们持有的类型如何,都具有相同的结构,那么看起来应该能够创建一个Object数组,并将其转型为所希望的数组类型,事实上这可编译,但是不能运行:

publicclassArrayOfGeneric{
    static Generic<Integer>[] gia;
    public static void main(String[] args){
        // ClassCaseException
        // gia = (Generic<Integer>[])new Object[100];

        // 成功创建泛型数组
        gia = (Generic<Integer>[])new Generic[100];
        System.out.println(gia.getClass().getSimpleName());
        gia[0] = new Generic<Integer>();

        //gia[1] = new Object();
        //gia[2] = new Generic<Double>();
    }
}

成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型
即:gia = (Generic<Integer>[])new Generic[100];

public class GenericArray<T>{
    private T[] array;

    @SuppressWarnings("unchecked")
    public GenericArray(int size){
        array = (T[])new Object[size];
    }

    public void put(int index, T item){
        array[index] = item;
    }

    public T get(int index){
        return array[index];
    }

    public T[] rep() { return array; }

    public static void main(String[] args){
        GenericArray<Integer> gai = new GenericArray<Integer>(10);

        // ClassCastException
        // Integer[] ia = gai.rep();

        Object[] oa = gai.rep();
    }
}

rep()方法返回T[],并将结果作为Integer[]引用来捕获,会产生ClassCastException,这是因为实际运行时类型是Object[]。

因为有了擦除,数组的运行时类型就只能是Object[],如果我们立即将其转型为T[],那么在编译期该数组的实际类型就将丢失,而编译器可能错过某些潜在的错误检查。正因为这样,最好是在集合内部使用Object[],然后当你使用数组元素时,添加一个对T的转型。这一点在集合框架的源码可以见到。

示例:

public class GenericArray<T>{
    private Object[] array;
    public GenericArray(int size){
        array = new Object[size];
    }

    @SuppressWarnings("unchecked")
    public T get(int index){ return (T)array[index]; }

    @SuppressWarnings("unchecked")
    public T[] rep(){
        return (T[])array;
    }

    public static void main(String[] args){
        GenericArray<Integer> gai = new GenericArray<Integer>(10);

        for(int i = 0; i < 10; i++)
            gai.put(i,i);

        for( int i = 0; i < 10; i ++)
            System.out.println(gai.get(i)+"");

        // ClassCastException
        // Integer[] ia = gai.rep();
    }
}

然而,如果你调用rep(),它还是尝试着将Object[]转型为T[],这仍旧是不正确的,将在编译期产生警告,运行时产生异常。

public class GenericArrayWithTypeToken{
    private T[] array;

    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class<T> type, int sz){
        array = (T[]) Array.newInstance(type,sz);
    }

    public void put(int index, T item){
        array[index] = item;
    }

    public void get(int index) { return array[index]; }

    public T[] rep() { return array; }

    public static void main(String[] args){
        GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<Integer>(Integer.class,10);

        // work
        Integer[] ia = gai.rep();
    }
}

通过传递类型标记Class<T>到构造器中,以便从擦除中恢复,使得我们可以创建需要的实际类型的数组。

边界

因为擦除移除了类型信息,所以可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。但是如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法

interface HasColor { int getColor(); }

class Colored<T extends HasColor>{
    T item;
    Colored(T item){ this,item = item; }
    T getItem() { return item; }
    // 泛型边界
    int color() { return item.getColor(); }
}

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

//T extends Dimension & HasColor:类在前,接口在后
class ColoredDimension<T extends Dimension & HasColor>{
    T item;
    ColoredDimension(T item) { this.item = item; }

    T getItem() { return item; }
    // 泛型边界
    int color() { return item.getColor(); }
    int getX() { return item.x; }
    int getY() { return item.y; }
    int getZ() { return item.z; }
}

interface Weight { int weight(); }

//T extends Dimension & HasColor & Weight:类在前,接口在后
class Solid<T extends Dimension & HasColor & Weight>{
    T item;
    ColoredDimension(T item) { this.item = item; }

    T getItem() { return item; }
    // 泛型边界
    int color() { return item.getColor(); }
    int getX() { return item.x; }
    int getY() { return item.y; }
    int getZ() { return item.z; }
    int weight() { return item.weight(); }
}

class Bounded extends Dimension implements HasColor, Weight{
    public int getColor() { return null; }
    public int weight() { return 0; }
}

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

可以看到使用<T extends Dimension & HasColor & Weight>实现多边界,而且类在前,接口在后。

更多层次的情况在书中。

通配符

通配符被限制为单一边界,所以List<? extends A & B>会报错

看一下例子:数组向导出类型的数组赋予基类型的数组引用

class Fruit{ }
class Apple extends Fruit{ }
class Jonathan extends Apple{ }
class Orange extends Fruit{ }

public class GovariantArrays{
    public static void main(String[] args){
        // 
        Fruit[] fruit = new Apple[10];
        fruit[0] = new Apple(); // OK
        fruit[1] = new Jonahan(); // OK

        //  编译器不会报错,运行期报错
        // fruit[0] = new Fruit(); // ArrayStoreException
        // fruit[1] = new Fruit(); // ArrayStoreException
    }
}

实际数组类型是Apple[],应该只能放置Apple和Apple的子类型,这在编译期和运行时都可以工作。

但是编译器允许你将Fruit放置到这个数组中,因为它有一个Fruit[]引用,但是运行时数组机制知道它处理的是Apple[],因此会抛出异常

泛型的主要目标之一就是将这种错误检测移入到编译期

因此使用泛型容器来代替数组

public class NonCovariantGenerics{
    // 编译错误
    List<Fruit> flist = new ArrayList<Apple>();
}

这里要明确一点:Apple的List不是Fruit的List,尽管Apple是一种Fruit类型

那怎么解决这问题呢?

这时候需要在两种类型之间建立某种类型的向上转型关系

public class GenericsAndCovariance{
    public static void main(String[] args){
        List<? extends Fruit> flist = new ArrayList<Apple>();

        // 编译错误
        flist.add(new Apple());
        flist.add(new Fruit());
        flist.add(new Object());

        // 可以运行
        flist.add(null);
        Fruit f = flist.get(0);
        flist.contains(new Apple()); // 参数类型是Object
        flist.indexOf(new Apple()); // 参数类型是Object
    }
}

flist类型现在是List<? extends Fruit>,可以将其读作“具有任何从Fruit继承的类型的列表”。但是,这实际上并不意味着这个List将持有任何类型的Fruit。

通配符引用的是明确的类型,因此它意味着“某种flist引用没有指定的具体类型”

因此当你指定一个ArrayList<? extends Fruit>时,add的参数也变成? extends Fruit。编译器不能了解这里需要Fruit哪个具体子类型,因此它不会接受任何类型的Fruit

但是,在使用contains和indexOf时,类型参数是Object,因此不涉及任何通配符,而编译器也将允许这个调用。这一点在ArrayList等源码可以看到

那应该怎么解决呢??

另一条路:超类型通配符

这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定<? super MyClass>,甚至或者使用类型参数:<? super T>(尽管你不能对泛型参数给出一个超类型边界;即不能声明<T super MyClass>

因此:

public class SuperTypeWildcards{
    static void writeTo(List<? super Apple> apples){
        apples.add(new Apple());
        apples.add(new Jonathan());
        // apples.add(new Fruit()); // Error
    }
}

参数Apple是Apple的某种基类型的List,这样你就知道向其中添加Apple或Apple的子类型是安全的

超类型边界放松了在可以先方法传递的参数上所作的限制,再看一个例子:

public class GenericWriting{
    static List<Apple> apples = new ArrayList<Apple>();
    static List<Fruit> fruit = new ArrayList<Fruit>();

    static <T> void writeExact(List<T> list,T item){
        list.add(item);
    }

    static void f1(){
        writeExact(apples, new Apple());
        // Error
        // writeExact(fruit, new Apple());
    }

    static <T> void writeExactWildcard(List<? super T> list, T item){
        list.add(item);
    }

    static void f2(){
        writeExactWildcard(apples, new Apple());
        writeExactWildcard(fruit, new Apple()); // OK
    }

    public static void main(String[] args){
        f1();
        f2();
    }
}

继续一个例子:

public class GenericReading{
    static List<Apple> apples = new ArrayList<Apple>();
    static List<Fruit> fruit = new ArrayList<Fruit>();

    static <T> T readExact(List<T> list){
        return list.get(0);
    }

    static void f1(){
        Apple a = readExact(apples);
        Fruit f = readExact(fruit);
        f = readExact(apples);
    }

    // 通过泛型类读取
    static class Reader<T> { 
        T readExact(List<T> list) { reutrn list.get(0); }
    }

    static void f2(){
        Reader<Fruit> fruitReader = new Reader<Fruit>();
        Fruit f = fruitReader.readExact(fruit);
        // Error
        // Fruit f = fruitReader.readExact(apples);
    }

    // 改进
    static class CovariantReader<T>{
        T readCovariant(List<? extends T> list){
            return list.get(0);
        }
    }

    static void f3(){
        CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
        Fruit f = fruitReader.readExact(fruit);
        // OK
        Fruit a = fruitReader.readExact(apples);
    }

    public static void main(String[] args){
        f1(); f2(); f3();
    }
}

上面这例子说明,List<? extends T>列表中所有对象至少是一个T,并且可能是从T导出的某种对象

问题

实现参数化接口

一个类不能实现同一个泛型接口的两个变体,由于擦除的原因,这两个变体会成为相同的接口

interface Payable<T>{ }

class Employee implements Payable<Employee>{ }
class Hourly extends Employee implements Payable<Hourly>{ } // Error

Hourly不能编译,因为擦除会将Payable<Employee>Payable<Hourly>简化为相同的类Payable,这样,上面的代码就意味着在重复两次实现相同的接口。

十分有趣的是,如果从Payable的两种用法都移除掉泛型参数(就像编译器在擦除阶段所作的那样),这段代码就可以编译。

重载

下面程序是不能编译的

public class UserList<W,T>{
    void f(List<T> v) {}
    void f(List<W> w) {}    
}

由于擦除的原因,重载方法将产生相同类型的签名

因此可以改成

public class UserList<W,T>{
    void f1(List<T> v) {}
    void f2(List<W> w) {}   
}

基类劫持了接口

class ComparablePet implements Comparable<ComparablePet>{
    public int compareTo(ComparablePet arg) { return 0; }
}

//Error
class Cat extends ComparablePet implements Comparable<Cat>{ }

class Hamster extends ComparablePet implements Comparable<ComparablePet>{
    public int compareTo(ComparablePet arg) { return 0; }
}

可以看到,一旦为Comparable确定了ComparablePet参数,那么其他任何实现类都不能与ComparablePet之外的任何对象比较

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值