《Java编程思想》第四版读书笔记 第十五章 泛型

15.4 泛型方法java

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

做者推荐的一个基本指导原则:不管什么时候,只要能作到,就应该尽可能使用泛型方法。也就是说,若是使用泛型方法能够取代将整个类泛型化,那就应该只使用泛型方法。编程

对于一个static方法没法访问泛型类的类型参数,因此若是static方法须要使用泛型能力就必须使其成为泛型方法。设计模式

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

当使用泛型类时必须在建立对象的时候指定类型参数的值,而使用泛型方法的时一般没必要指明参数类型,由于编译器会为咱们找出具体的类型,这称为类型参数推断。安全

15.4.1小节做者讨论了添加泛型参数后形成代码增多:app

Map> petPeople = new HashMap>();编程语言

做者使用了泛型方法来消除后面的泛型声明。函数

在1.7中加入了类型推断的新特性解决了上面的问题:ui

Map> petPeople = new HashMap<>();

在使用泛型方法时能够显示指明类型,在点操做符与方法名之间插入尖括号,而后把类型置于尖括号内。若是是在定义该方法的类的内部,必须在点操做符以前使用this关键字,若是是使用static方法,必须在点操做符以前加上类名:

New.>map();

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

makeList(T ... args);

做者在15.4.5小节例子写到:

public class Tuple {
    public static  Twopule tuple(A a, B b) {
       return new TwoTuple(a, b);
    }
}

public class TupleTest2 {
    static TwoTuple f() {
       return tuple("hi", 47);
    }
    static TwoTuple f2() {
       return tuple("hi", 47);
    }
}

方法f()返回一个参数化的TwoTuple对象,而f2()返回的是非参数化的TwoTuple对象。在这个例子中,编译器并无关于f2()的警告信息,由于咱们并无将其返回值做为参数化对象使用。在某种意义上它被向上转型为一个非参数化的TwoTuple。然而,若是试图将f2()的返回值转型为参数化的TwoTuple,编译器就会发出警告。

对于这段话我不理解,而且通过我本身coding,f2()函数是会被编译器警告的。有没有大虾能解释一下?

在15.4.6小节的例子中做者使用了EnumSet类,它是1.5新加入的,用来从enum直接建立Set,它的range()静态方法传入enum某个范围的第一个元素和最后一个元素,而后返回一个Set。

15.7 擦除

能够声明ArrayList.class,可是不能声明ArrayList.class;经过coding:

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导出的类型。

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

这种边界方式有时没有给代码带来任何好处。因此做者提出:只有当你但愿使用的类型参数比某个具体类型(以及它的全部子类型)更加泛化时,也就是说,当你但愿代码可以跨多个类工做时,使用泛型才有所帮助。

练习20提醒了我,在给泛型参数指定边界时,边界也能够是接口,而且依然使用extends关键字而不是implements。

在15.7.4小节中做者经过对比了一个普通类和一个泛型类的字节码,发现二者的字节码是相同的(由于泛型擦除,运行时根本不知道泛型参数的具体类型)。在get()方法取值处普通类须要咱们手动转型为String,字节码随之有了类型检查一行,而泛型了编译器替咱们添加了这段代码。

15.8 擦除的补偿

因为擦除丢失了在泛型代码中执行某些操做的能力,任何在运行时须要知道确切类型信息的操做都没法完成:

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();
	}
}

另外一种方式使模板方法设计模式,在下面的例子中get()时模板方法,而create()是在子类中定义的、用来产生子类类型的对象:

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

class X {
	
}

class Creator extends GenericWithCreate {
	X create() {
		return new X();
	}
}

练习24翻译有误,应该翻译成修改练习21,使得Map中持有的是工厂对象而不是Class。

二、泛型数组

能够声明一个泛型数组的引用:

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[]就会产生异常。因此在上个例子中,较好的方式是将array引用的类型声明为Object[],由于在运行时T[]和Object[]是相同的,而Object[]不会引发误会。固然调用Integer[] ia = gai.rep();依旧会出错的。

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

15.9 边界

Java泛型重用了extends关键字,它在泛型边界上下文环境中和在普通状况下所具备的意义是彻底不一样的。

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

 

15.10 通配符

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

class Fruit {

}

class Apple extends Fruit {

}

class Jonathan extends Apple {

}

class Orange extends Fruit{

}

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

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
  }
}

第一行建立了一个Apple数组,并将其赋值给一个Fruit数组引用。这是有意义的,由于Apple也是一种Fruit,所以Apple数组应该也是一个Fruit数组。可是这里实际的数组类型是Apple[],因此只能在其中放置Apple或Apple的子类型,这在编译器和运行时能够工做。可是,编译器容许将Fruit放置到这个数组中,而运行时的数组机制知道它处理的是Apple[],所以会在向数组中放置异构类型时抛出异常。

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

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

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类型参数的方法是能够正常工做的。

二、超类型通配符

能够声明通配符是由某个特定类的任何基类来界定的,方法是指定,也可使用类型参数,但不能对泛型参数给出一个超类型 边界,即不能声明(个人理解,超类型通配符是在引用类型中使用的,不能再定义中使用)。这使得能够安全的传递一个类型对象到泛型类型中。有了超类型通配符,能够向Collection写入对象了:

class SuperTypeWildcards {
  static void writeTo(List apples) {
    apples.add(new Apple());
    apples.add(new Jonathan());
    //apples.add(new Fruit()); 编译错误
  }
}

上面这个例子有些绕,须要好好理解。参数apples是Apple的某种基类型的List,这样就能够知道向其中添加Apple或Apple的子类型是 安全的。可是,Apple是下界,那么能够知道向这样的List中添加Fruit是不安全的。

能够根据若是可以像一个泛型类型“写入”(传递给一个方法),以及如何可以从一个泛型类型中“读取”(从一个方法中返回),来思考子类型和超类型边界。

下面这个例子表现出超类型边界放松了在能够向方法传递的参数上所作的限制:

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

static List apples = new ArrayList<>();
static List fruits = new ArrayList<>();

static void f1() {
  writeExact(apples, new Apple());
  //writeExact(fruits, new Apple()); 编译错误
}

static void writeWithWildcard(List list, T item) {
  list.add(item);
}

static void f2(){
  writeWithWildcard(apples, new Apple());
  writeWithWildcard(fruite, new Apple());
}

第一方法使用了确切的泛型参数类型(无通配符),它不容许将Apple放置到List中,即便知道这是能够的。第二个方法泛型参数时? super T,所以这个List将持有从T导出的某种具体类型,这样就能够安全的将一个T类型的对象或者从T导出的任何对象做为参数传递给List方法。

三、无界通配符<?>

static List list1;
static List<?> list2;
static List<? extends Object> list3;

static void assign1(List list){
  list1 = list;
  list2 = list;
  list3 = list; //有警告:unchecked conversion
}

static void assign2(List<?> list){
  list1 = list;
  list2 = list;
  list3 = list;
}

static void assign3(List<? exnteds Object list){
  list1 = list;
  list2 = list;
  list3 = list;
}

看似List<?>与List相同,可是从上例子中能够看出仍是有区别的。实际上它是在声明:“我是想用Java的泛型来编写这段代码,我在这里并 不是要用原生类型,可是在当前这种状况下,泛型参数能够持有任何类型”。

当在处理多个泛型参数时,容许一个参数能够是任何类型,同时为其余参数肯定某种特定类型有时很重要。

15.11泛型的问题

一、泛型不能持有基本类型,但可使用对应的包装器类,可是这会在必定程度上影响效率。org.apache.commons.collections.primitives是适配基本类型的容器版本。另外自动包装机制不能用于数组。

二、一个类不能实现同一个泛型接口的两种变体,下面的例子是产生冲突的状况:

interface Payable<T> {
}

class Employee implements Payable<Employee> {
}

class Hourly extends  Employee implements Payable<Hourly> {
}

Hourly不能编译,由于擦除会将Payable<Employee>和Payable<Hourly>简化为相同的类Payable。搞笑的是把两个Payable的泛型参数都去掉Hourly能够编译经过。

三、带泛型的参数类型没法做为重载的依据:

public class UseList<W, T> {
  void f(List<T> v){}
  void f(LIst<W> v){}
}

以上这种形式不是重载,编译器会报错。

15.12 自限定的类型

常常会出现以下的泛型写法:

class SelfBounded<T extends SelfBounded<T>>

这种写法的主要意义是保证类型参数必须与被定义的类相同。

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

15.13 动态类型安全

由于能够向Java 1.5以前的代码传递带泛型参数的容器,因此旧代码会破坏容器的正确性。Collections类中提供了一组静态方法checkedCollection()、checkedList()、checkedMap()、checkedSet()、checkedSortedMap()和checkedSortedSet()返回受检查的容器。每一个方法的第一个参数接受容器,第二个参数接受容器的泛型类型。当试图向受检查的容器插入不正确的对象时会抛出ClassCastException。

通过coding验证产生的受检查容器和传入的容器不是一个引用。

15.14 异常

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

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

可是泛型类型参数能够应用到throws子句中:

interface ProcessRunner<T, E extends Exception> {
  void process(List<T> resultCollector) throws E;
}

15.15 混型

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

因为参数Java没法像C++这样来实现混型class TimeStamped : public T。做者 讨论了几种替代方式:

(1)使用组合(代理)

(2)使用装饰器模式

(3)使用动态代理

15.16 潜在类型机制

一些编程语言提供一种机制称为潜在类型机制或结构化类型机制,还有叫鸭子类型机制,即“若是它走起来像鸭子,而且叫起来像鸭子,那么就能够把它当鸭子来对待“。C++ 和 Python提供这种机制,而Java泛型由于擦除并不能提供。

15.17 对缺少潜在类型机制的补偿

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

(2)使用适配器模式。

相关文章
相关标签/搜索