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

Posted Y_ZhiWen

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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之外的任何对象比较

以上是关于Java编程思想 -- 泛型概括总结的主要内容,如果未能解决你的问题,请参考以下文章

Java编程思想学习 泛型

Java中的泛型 --- Java 编程思想

Java 中的泛型详解-Java编程思想

《JAVA编程思想》学习笔记——第十五章 泛型

Java编程思想(第十一章持有对象)

泛型方法