Java编程思想总结篇——第十五章

第十五章 泛型

一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。

无论何时,只要能做到,就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那就应该只使用泛型方法。

1.与C++比较

2.简单泛型

应用于容器类:只需指定它们的名称以及类型参数列表即可。

元组:将一组对象打包存储于一个单一对象。

3.泛型接口

4.泛型方法

除了将泛型应用于整个类,还可以在类中包含泛型化方法,而这个方法所在的类可以是泛型类也可以不是泛型类。

泛型方法使得该方法能够独立于类而产生变化 。

对于一个static方法无法访问泛型类的类型参数,所以如果static方法需要使用泛型能力就必须使其成为泛型方法。

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

当使用泛型类时必须在创建对象的时候指定类型参数的值,而使用泛型方法的时通常不必指明参数类型,因为编译器会为我们找出具体的类型,这称为类型参数推断。

杠杆利用类型参数判断:

使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数推断

类型推断只对赋值操作有效。

如果将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行类型推断。

 

通过以下代码优化重复的泛型参数列表:

Mapa=New.map()

Listb=New.list()

LinkedListc=New.lList()

Setd=New.set()

Queuee=New.queue()

显示的类型说明:

在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内,即显式的类型说明。如果是在定义该方法的类的内部,必须在点操作符之前使用this关键字,如果是使用static方法,必须在点操作符之前加上类名。

可变参数与范型方法:

泛型与可变长参数可以结合使用:

makeList(T ... args)

5 匿名内部类

6 构建复杂模型

7 擦除神秘之处

Class c1 = new ArrayList().getClass();
Class c2 = new ArrayList().getClass();
System.out.println(c1 == c2);

输出的结果是true,表明编译器认为ArrayList和ArrayList是相同的类型。

对一个带有泛型参数的对象使用getClass().getTypeParameters()看到返回的只是用作参数占位符的标识符。

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

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

我们可以给定泛型的边界,以此告诉编译器只能接受遵循这个边界的类型,可以使用extends关键字。

声明T必须具有类型HasF或者HasF导出的类型。

我们说泛型类型参数将擦除到它的第一个边界(它可能会有多个边界),我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例一样。T 擦除到HasF,就好像在类的声明中用 HasF 替换T 一样。

泛型类型参数将擦除到它的第一个边界(它可能会有多个边界)。

只有当你希望使用的类型参数比某个具体类型(以及它的所有子类型)更加泛化时,也就是说,当你希望代码能够跨多个类工作时,使用泛型才有所帮助。

 

根据JDK文档的描述,Class.getTypeParameters()将“返回一个TypeVariable对象数组,表示有泛型声明的类型参数…..”,这好像是在暗示你可能发现参数类型的信息,但是,正如你从输出中看到,你能够发现的只是用作参数占位符的标识符,这并非有用的信息。因此,残酷的现实是:在泛型代码内部,无法获得任何有关泛型参数类型的信息。

因此,你可以知道诸如泛型参数标识符和泛型类型边界这类信息——你却无法知道创建某个特定实例的实际的类型参数。在使用Java泛型工作时它是必须处理的最基本的问题。

Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此 List 和 List 在运行时事实上是相同的类型。这两种形式都被擦除成它们的“原生类型,即 List。

迁移兼容性:

擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为迁移兼容性。

因此,擦除主要的正当理由是从非泛化的代码到泛化的代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言。擦除的代码是显著的。

擦除的问题:

如果编写了下面这样的代码:

class Foo{ T var; }

那么看起来当你在创建Foo的实例时:

Foo f = new Foo<>();

class Foo中的代码应该知道现在工作于Cat之上,而泛型语法也强烈暗示:在整个类中的各个地方,类型T都在被替换。但是事实上并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,它只是一个Object。”
擦除和迁移兼容性意味着,使用泛型并不是强制的。

class GenericBase{}
class Derived1 extends GenericBase{}
class Derived2 extends GenericBase{} // No warning

边界的动作:

即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性

因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查插入转型代码的地点。

在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。这有助于澄清对擦除的混淆,记住,“边界就是发生动作的地方。”

8 擦除的补偿 

有时必须通过引入类型标签来对擦除进行补偿。这意味着你需要显示地传递你的类型的Class对象,以便你可以在类型表达式中使用它。 

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

public class Erased {
  private final int SIZE = 100;
  public static void f(Object arg){
    if(arg instanceof T) {      错误
       T var = new T();         错误
       T[] array = new T[SIZE]; 错误
       T[] array = (T)new Object[SIZE]; 正确但有警告
    }
}

如果想实现上述错误代码的功能可以显示传递类型的Class对象:

public Erased {
  
  public Class kind;
 
  public Erased(Class kind) {
    this.kind = kind;
  }

  public static void f(Object arg) {
    if(kind.isInstance(arg){
 
    }
  }
}

new T() 创建对象是无法实现的,部分原因是因为擦除,而另一部分原因是因为编译器不能验证T具有无参构造函数。

解决方案是传递一个工厂对象,并使用它来创建新的实例。最便利的工厂对象是Class对象,使用newInstance来创建这个类型的对象。但是有些没有无参构造函数的类无法使用这种方法来创建,并且这个错误无法再编译期被捕获,所以Java的创建者不赞成这种方式,他们建议使用显示的工厂,并将其限制类型。

interface Factory {
    T create();
}

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

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

class Foo2 {
    private T x;
    public >

    Foo2(F factory) {
        x = factory.create();
    }
}

泛型数组:

可以声明一个泛型数组的引用:

class Generic {

}

public class ArrayOfGeneric {
    static Generic[] gia;
}

但是却不能创建确切泛型类型的数组:gia = new Generic[10],这种写法是错误的。如果想把Object[]转型为Generic运行时会抛出ClassCastException。这个问题在于数组将跟踪它们的实际类型,而这个类型是在数组被创建时确定的,因此即使gia已经被转型为Generic[],但是这个信息只存在于编译期。在运行时,它仍旧是Object数组,而这将引发问题。

成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。

public class GenericArray {
    private T[] array;

    public GenericArray(int sz) {
        array = (T[]) new Object[sz];
    }

    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 gai = new GenericArray(10);
        Integer[] ia = gai.rep();
    }
}

 当程序运行到Integer[] ia = gai.rep();语句时会抛出ClassCastException。因为泛型擦除,在实际运行时数组array的类型是Object[],而要将其转型为Integer[]就会产生异常。

创建泛型数组的最好方法是持有一个泛型类型标识(Class类的对象),然后使用(T[])Array.newInstance(Class, size);创建数组。

9 边界 

Java泛型重用了extends关键字,它在泛型边界上下文环境中和在普通情况下所具有的意义是完全不同的。

它要点是上界可以指定单独的类或者方法,也可以指定一个类和(一个或者多个)方法,用&符号分隔,此时类必须在最前面。

边界使得你可以在用于泛型的参数类型上设置限制条件。尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法

因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。

但是,如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法。

通配符被限制为单一边界

10 通配符

 

首先创建几个用于例子的类,注意它们的继承体系。

class Fruit {

}

class Apple extends Fruit {

}

class Jonathan extends Apple {

}

class Orange extends Fruit{

}

 

数组的一种特殊行为 :

以将子类型的数组赋给基类型的数组引用。然后编译期数组元素可以放置基类型及其子类型的元素,即编译时不报错,但运行时的数组机制知道实际的数组类型是子类,因此会在运行时检查放置的类型是否是实际类型及其再导出的子类型,不是则抛出java.lang.ArrayStoreException异常。

List list = new ArrayList() ;//报错

实际上向上转型不适合在这里。数组的行为应该是它可以持有其他对象。

 对于数组,可以将导出类型的数组赋予基类型的数组引用。

class CovariantArrays {
  public static void main(String[] args) {
    Fruit[] fruit = new Apple[10];
    fruit[0] = new Apple();
    fruit[1] = new Jonathan();
    fruit[2] = new Fruit(); //会抛出ArrayStoreException
    fruit[3] = new Orange(); //会抛出ArrayStoreException
  }
}

 

与数组不同,泛型没有内建的协变类型。即协变性对泛型不起作用。

相对于数组,泛型容器不允许这样的向上转型:

List flist = new ArrayList();

由于泛型不知道类型信息,因此它拒绝向上转型。与数组不同,泛型没有内建的协变类型。数组在语言中完全定义的,因此可以内建了编译期和运行时的检查,但是在使用泛型时,编译器和运行时系统都不知道你想用类型做什么,以及应该采用什么样的规则。

如果想要在两个类型之间建立某种类型的向上转型关系,可以使用通配符:

List flist = new ArrayList();
flist.add(new Apple()); //编译错误
flist.add(new Fruit()); //编译错误
flist.add(new Object());//编译错误
flist.add(null);        //正确

引用的类型是List可以将其视为“具有任何从Fruit继承的类型的列表”。但是这实际上并不意味着这个List将 持有任何类型的List。通配符引用的是明确的类型,因此它意味着”flist引用持有某种没有指定具体的类型“。

可以看到一旦执行了上例子中的向上转型,就会丢失传递任何对象的能力,甚至是Object也不行,只能传入null。

对一个泛型类,如果使用了通配符的向上转型,对于那些带有Object类型参数的方法是可以正常工作的。 

 编译器有多聪明:

对于 Listset() 方法不能工作于 AppleFruit,因为 set() 的参数也是 ? extends Furit,这意味着它可以是任何事物,而编译器无法验证“任何事物”的类型安全性。

但是,equals() 方法工作良好,因为它将接受Object类型而并非T类型的参数。因此,编译器只关注传递进来和要返回的对象类型,它并不会分析代码,以查看是否执行了任何实际的写入和读取操作。

逆变:

使用超类型通配符。声明通配符是由某个特定类的任何基类界定的,方法是指定,甚至或者使用类型参数:。这使得你可以安全地传递一个类型对象到泛型类型中。

超类型通配符使得可以向泛型容器写入。超类型边界放松了在可以向方法传递的参数上所作的限制。

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

package job;

import java.util.*;

public class SuperTypeWildcards {
    static void write(List apples) {
        apples.add(new Apple());
        apples.add(new Jonathan());
        //apples.add(new Fruit());    // Error
    }
}

无界通配符:

原生泛型HolderHolder

原生Holder将持有任何类型的组合,而Holder将持有具有某种具体类型同构集合,因此不能只是向其中传递Object。

捕获转换:

以下示例,被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型。参数类型在调用f2()的过程中被捕获,因此它可以在对f1()的调用中被使用。

package job;
public class CaptureConversion {
    static  void f1(Holder holder) {
        T t = holder.get();
        System.out.println(t.getClass().getSimpleName());
    }

    static void f2(Holder holder) {
        f1(holder);     // Call with captured type
    }

    public static void main(String[] args) {
        Holder raw = new Holder(1);
        f1(raw);
        f2(raw);
        Holder rawBasic = new Holder();
        rawBasic.set(new Object());
        f2(rawBasic);
        Holder wildcarded = new Holder(1.0);
        f2(wildcarded);
    }
}

11 问题 

任何基本类型不能作为类型参数。

一个类不能实现同一个泛型接口的两种变体

下面的例子是产生冲突的情况:

interface Payable {
}

class Employee implements Payable {
}

class Hourly extends  Employee implements Payable {
}

 Hourly不能编译,因为擦除会将Payable和Payable简化为相同的类Payable。

通过泛型来重载方法将产生相同的签名,编译出错,不能实现重载

带泛型的参数类型无法作为重载的依据:

public class UseList {
  void f(List v){}
  void f(LIst v){}
}

基类劫持了接口

12 自限定类型

class SelfBounded>

这种写法的主要意义是保证类型参数必须与被定义的类相同。强制要求正在定义的类当作参数传递给基类。

从Java 1.5开始加入了返回类型的协变(继承类函数的返回类型可以是基类该函数返回类型的子类),但是没有方法实现函数参数的协变,运用这种自限定写法可以实现函数参数的协变。

13 动态类型安全

Collections类中提供了一组静态方法checkedCollection()、checkedList()、checkedMap()、checkedSet()、checkedSortedMap()和checkedSortedSet()返回受检查的容器。每个方法的第一个参数接受容器,第二个参数接受容器的泛型类型。当试图向受检查的容器插入不正确的对象时会抛出ClassCastException。

14 异常

带泛型参数的类不能直接或间接继承自Throwable。

catch语句中不能捕获泛型类型的异常。

但是泛型类型参数可以应用到throws子句中:

interface ProcessRunner {
  void process(List resultCollector) throws E;
}

15 混型

混型最基本的概念是混合多个类的能力,以产生一个可以表示混型中所有类型的类。混型的价值之一是它们可以将特性和行为一致的应用于多个类之上。如果想在混型类中修改某些东西,这些修改将应用于混型所应用的所有类型之上。混型有一点AOP的味道。

Java有三种方法:

(1)使用组合(代理)

(2)使用装饰器模式

(3)使用动态代理

16 潜在类型机制

Java没有

17 对缺乏潜在机制的补偿

(1)使用反射可以实现类似潜在类型机制的功能;

(2)使用适配器模式。

18 将函数对象用作策略

19 总结

泛型的通用语言特性(并非必须是其在Java中的特定实现)的目的在于可表达性,而不仅仅是为了创建类型安全的容器。类型安全的容器是能够创建更通用代码这一能力所带来的副作用。
泛型正如其名称所暗示的:它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型有更少的限制,因此单个的代码段能够应用到更多的类型上。

你可能感兴趣的:(JAVA)