@@ 在面向对象编程语言中,多态算是一种泛化机制。
@@ 多态具备更好的灵活性。但是,考虑到除了 final 类不能扩展,其他任何类都可以
被扩展,所以这种灵活性大多数时候也会有一些性能损耗。
@@ 泛型实现了参数化类型的概念,使代码可以应用于多种类型。
@@ 在你创建一个参数化类型的实例时,编译器会为你负责转型操作,并且保证类型的
正确性。
》》与C++的比较
@@ 了解 C++ 模板的某些方面,有助于你理解泛型的基础。
@@ 可以了解 Java 泛型的局限是什么,以及为什么会有这些限制。最终目的是帮助你理解,
Java 泛型的边界在哪里。
》》简单泛型
@@ 泛型出现最引人注目的原因是:为了创造容器类。
《
------容器:存放要使用的对象的地方。
------事实上,所有的程序,在运行时都要求你持有一大推对象,所以,容器类算得上
最具重用性的类库之一。
》
@@ 通常而言,我们只会使用容器来存储一种类型的对象。
@@ 泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证
类型的正确性。
@@ 例如:
public class Holder3<T>{ // 不指定类型
private T a ;
public Holder3( T a){
this.a = a ;
}
public void set (T a ){
this.a = a ;
}
public T get(){
return a;
}
public static void main(String[] args){
Holder3<Automobile> h3 = // 调用的时候,传入具体的类型
new Holder3<Automobile>( new Automobile() ) ;
Automobile a = h3.get();
}
}
说明:(1)、不指定类型,而是稍后再决定具体使用什么类型。要达到这个目的,需要使用
类型参数,用尖括号括住,放在类名后面。然后在使用这个类的时候,再用实际的类
型替换此类型。
@@ Java 泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节。
@@ 一般而言,你可以认为泛型与其他的类型差不多,只不过它们碰巧有类型参数罢了。
在使用泛型时,我们只需要指定它们的名称以及类型参数列表即可。
### 一个元组类库
@@ 仅一次方法调用就能返回多个对象。
@@ 元组:它是将一组对象直接打包存储于其中的一个单一对象。 这个容器对象允许读取
其中元素,但是不允许向其中存放新的对象(这个概念也称为数据传送对象,或者信使)。
@@ 通常,元组可以具有任意长度,同时,元组中的对象可以是任意不同的类型。
@@ 例如:
public class TwoTuple<A , B>{ // 尖括号里面的内容即为一个元组
public final A first ;
public final B second ;
}
@@ 我们可以利用继承机制实现长度更长的元组。
例如:
public class TreeTuple<A , B , C > extends TwoTuple <A , B > {
}
@@ 为了使用元组,你只需定义一个长度适合的元组,将其作为方法的返回值,然后在
return 语句中创建该元组,并返回即可。
@@ 由于使用了泛型,你可以很容易地创建元组,令其返回一组任意类型的对象。而你
所要做的,只是编写表达式而已。
### 一个堆栈类
@@ 传统的下推堆栈。这个堆栈是作为 net.mindview.util.Stack 类,用一个 LinkedList
实现的。
@@ 使用末端哨兵(end sentinel)来判断堆栈何时为空。
### RandomList
》》泛型接口
@@ 泛型也可以应用于接口。例如生成器,这是一种专门负责创建对象的类。实际上,这是
工厂方法设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,
而工厂方法一般需要参数。也就是说,生成器无需额外的信息就知道如何创建新对象。
@@ 一般而言,一个生成器只定义一个方法,该方法用以产生新的对象。例如,下面的代码,
next() 方法。
public interface Generator <T>{
T next () ;
}
@@ Java 泛型一个局限性:基本类型无法作为类型参数。
不过,Java SE5 具备了自动打包和自动拆包的功能,可以很方便地在基本类型和其相应的
包装器类型之间进行转换。
@@ 适配器设计模式。
》》泛型方法
@@ 是否拥有泛型方法,与其所在的类是否是泛型没有关系。
@@ 泛型方法使得该方法能够独立于类而产生变化。以下是一个基本的指导原则:
(1)、无论何时,只要你能做到,你就应该尽量使用泛型方法。
(2)、如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,
因为它可以使事情更清楚明白。
(3)、对于一个 static 的 方法而言,无法访问泛型类的类型参数,所以,如果
static 方法需要使用泛型能力,就必须将其成为泛型方法。
@@ 要定义泛型方法,只需将泛型参数列表置于返回值之前。例如下面的代码:
public class GenericMethods {
public <T> void f ( T x) {
System.out.println( x.getClass().getName() ) ;
}
}
注意:
(1)、当使用泛型类时,必须在创建对象的时候指定类型参数的值。
(2)、使用泛型方法时,通常不必指明参数类型,因为编译器会为我们找出具体的类型。
这称为类型参数推断(type argument inference)。因此,我们可以像调用普通
方法一样调用泛型方法。
(3)、如果调用泛型方法传入基本类型作为参数,自动打包机制就会介入其中,将基本
类型的值包装为对应的对象。
### 杠杆利用类型参数推断
@@ 在泛型方法中,类型参数推断可以为我们简化一部分工作。
@@ 类型参数推断避免了重复的泛型参数列表。
@@ 类型推断只对赋值操作有效,其他时候并不起作用。如果你将一个泛型方法调用的结果
作为参数,传递给另一个方法,这时编译器并不会执行类型推断。在这种情况下,编译器
认为:调用泛型方法后,其返回值被赋给一个 Object 类型的变量。
@@ 显式的类型说明:
-----在泛型方法中,可以显式地指明类型,不过这种语法很少使用。
-----要显式地指明类型,必须在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内。
-----如果是在定义该方法的类的内部,必须在点操作符之前使用 this 关键字。
----- 如果是使用 static 关键字,必须在点操作符之前加上类名。
例如:
public class ExplicitTypeSpecification{
static void f ( Map<Person , List<Pet>> petPeople ){
}
public static void main(String[] args){
f ( New.<Person , List<Pet>>map() ) ;
}
}
### 可变参数与泛型方法
@@ 泛型方法与可变参数列表能够很好地共存。
### 用于 Generator 的泛型方法
### 一个通用的 Generator
### 简化元组的使用
### 一个 Set 实用工具
》》匿名内部类
@@ 泛型可以应用于内部类以及匿名内部类。
》》构建复杂模型
@@ 泛型的一个重要好处是能够简单而安全地创建复杂的模型。
》》擦除的神秘之处
@@ 在泛型代码内部,无法获得任何有关泛型参数类型的信息。
@@ Java 泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型
信息都被擦除了,你唯一知道的就是你在使用一个对象。因此 List<String> 和
List<Integer> 在运行时事实上是相同的类型。这两种形式都被擦除成它们的
“ 原生 ”类型,即 List 。理解擦除以及应该如何处理它,是你在学习 Java 泛型时
面临的最大障碍。
### C++ 的方式
@@ 给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。使用 extends
关键字:
例如:
class Mainpulator2< T extends HasF> {
private T obj ;
public Mainpulator2( T x ) {
obj = x ;
}
}
说明:(1)、边界 <T extends HasF> 声明 T 必须具有类型 HasF 或者从 HasF 导出
的类型。
@@ 当你希望代码能够跨多个类工作时,使用泛型才有所帮助。因此,类型参数和它们在有
用的泛型代码中的应用,通常比简单的类替换要更复杂。
@@ 例如:如果某个类有一个返回 T 的方法,那么泛型就有所帮助,因为它们之后将返回
确切的类型。
class ReturnGenericType<T extends HasF>{
private T obj ;
public ReturnGenericType( T x ) {
obj = x ;
}
public T get( ){
return obj;
}
}
### 迁移兼容性
@@ 擦除减少了泛型的泛型化。
@@ 在基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的
上下文环境中使用的类型。
泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将
被擦除,替换为它们的非泛型上界。
例如, List<T> 这样的类型注释将被擦除为 List , 而普通的类型变量在未指定
边界的情况下将被擦除为 Object 。
@@ 擦除的核心动机是:它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,
这经常被称为 “ 迁移兼容性 ” 。
@@ 因此 Java 不仅支持向后兼容性,即现有的代码和类文件仍旧合法,并且继续保持
其之前的含义;而且还要支持迁移兼容性,使得类库按照它们自己的步调变为泛型
的,并且当某个类变为泛型时,不会破坏依赖于它的代码和应用程序。
-----------------》 使用 擦除 可以解决上面的目标。(允许非泛型代码与泛型代码共存)。
@@ 擦除是否是最佳的或者唯一的迁移途径,还需要时间来证明。
### 擦除的问题
@@ 擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类
库的情况下,将泛型融入 Java 语言。
@@ 擦除使得现有的非泛型客户端代码能够在不改变的情况下继续使用,直至客户端准备
好用泛型重写这些代码。
@@ 擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作之中,例如转型 、
instanceof 操作 和 new 表达式。因为所有关于参数的类型信息都丢失了,无论何时,
当你在编写泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型
信息而已。
@@ class GenericBase<T> {
private T element ;
public void set ( T arg ){
element = args;
}
public T get( ){
return element ;
}
}
class Derived2 extends GenericBase {
}
public class Era {
@SuppressWarnings("unchecked")
public static void main( String[] args ){
Derived2 d2 = new Derived2() ;
Object obj = d2.get() ;
d2.set( obj ) ; // 执行这一行代码时会发出警告
}
}
说明:(1)、Derived2 继承自 GenericBase ,但是没有任何泛型参数,而编译器
不会发出任何警告。警告是在 set () 被调用时才会出现。
(2)、为了关闭警告, Java 提供了一个注解:
@SuppressWarnings("unchecked")
注意:这个注解被放置在可以产生这类警告的方法之上,而不是整个类上。
当你要关闭警告时,最好是尽量地“ 聚焦 ” ,这样不会因为过于宽泛地关闭
警告,而导致意外地遮蔽掉真正的问题。
@@ 当你希望将类型参数不要仅仅当作 Object 处理时,就需要付出额外努力来管理
边界。
### 边界处的动作
@@ 在泛型中创建数组,使用 Array.newInstance() 是推荐的方式。
@@ 即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在
方法或类中使用的类型的内部一致性。
@@ 因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:即对象
进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码
的地点。
@@ 在泛型中的所有动作都发生在边界处--------对传递进来的值进行额外的编译期检查,
并插入对传递出去的值的转型。
这有助于澄清对擦除的混淆,记住,“ 边界就是发生动作的地方 ” 。
》》擦除的补偿
@@ 擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道确切类型
信息的操纵都将无法工作。
@@ 有时必须通过引入类型标签来对擦除进行补偿。这意味着你需要显式地传递你的
类型的 class 对象,以便你可以在类型表达式中使用它。
@@ 如果引入类型标签,就可以转而使用动态的 isInstance( ) 。
@@ 编译器将确保类型标签可以匹配泛型参数。
###创建类型实例
@@ 在 .java 源码中对创建一个 new T( ) 的尝试将无法实现,部分原因是因为擦除,而
另一部分原因是因为编译器不能验证 T 具有默认(无惨)构造器。
《
java 中的解决方案是:(1)、传递一个工厂对象,并使用它来创建新的实例。
(2)、另一种方式:模板方法设计模式
》
@@ 最便利的工厂 对象就是 Class 对象 ,因此如果使用类型标签,那么你就可以使用
newInstance( ) 来创建这个类型的新对象:
class ClassAsFactory<T>{
T x ;
public ClassAsFactory( Class<T> kind ){
try{
x = kind.newInstance() ; // 使用内建的工厂对象
}catch( Exception e){
throw new RuntimeException(e) ;
}
}
}
@@ 建议使用显式的工厂,并将限制其类型,使得只能接受实现了这个工厂的类。
补充: Class<T> 使用的是内建的工厂对象
### 泛型数组
@@ 在 .java 源码中不能创建泛型数组。
一般的解决方案是:在任何想要创建泛型数组的地方都使用 ArrayList 。
(ArrayList 内部使用的是数组 )
@@ 数组将跟踪它们的实际类型,而这个类型是在数组被创建时确定的。
@@ 成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其
转型。
@@ 因为有了擦除,数组的运行时类型就只能是 Object[ ] 。如果我们立即将其转型
为 T[ ] , 那么在编译期该数组的实际类型就将丢失,而编译器可能会错过某些潜在
的错误检查。正是因为这样,最好是在集合内部使用 Object[ ] ,然后当你使用数组
元素时,添加一个对 T 的转型。
@@ 没有任何方式可以推翻底层的数组类型,它只能是 Object[ ] 。
@@ 即使在Java 类库源代码中出现了某些惯用法,也不能表示这就是正确地解决之道。
当查看类库代码时,你不能认为它就是应该在自己的代码中遵循的示例。
》》边界
@@ 边界使得你可以在用于泛型的参数类型上设置限制条件。尽管这使得你可以强制规
定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界
类型来调用方法。
@@ 因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只是那些可以用
Object 调用的方法。但是,如果能够将这个参数限制为某个类型子集,那么你就可
以用这些类型子集来调用方法。为了执行这种限制,Java 泛型重用了 extends 关键字。
对你来说有一点很重要,即要理解 extends 关键字在泛型边界上下文环境中和在普通
情况下具有的意义是完全不同的。
@@ 通配符被限制为单一边界。例如:
List< ? extends SuperHearing >
》》通配符
@@ 数组的一种特殊行为:可以向导出类型的数组赋予基类型的数组引用。
Fruit[ ] fruit = new Apple[ 10 ] ;
@@ 与数组不同,泛型没有内建的协变类型。
数组在语言中是完全定义的,因为可以内建了编译期和运行时的检查,但是在使用
泛型时,编译器和运行时系统都不知道你想用类型做什么,以及应该采用什么样的
规则 。
@@ 你想要在两个类型之前建立某种类型的向上转型关系,这正是通配符所允许的。
@@ 例如:
List< ? extends Fruit > flist = new ArrayList<Apple> ( ) ;
说明: (1)、< ? extends Fruit > 可以将其读作 “具有任何从 Fruit 继承的类型的
列表 ” 。但是,这实际上并不意味着这个 List 将持有任何类型的 Fruit 。通
配符引用的是明确的类型,因此它意味着 “某种 flist 引用没有指定的具体类型”。
### 编译器有多聪明
@@ 如果创建一个 Holder<Apple> ,不能将其向上转型为 Holder<Fruit> ,但是可以将其向上
转型为 Holder< ? extends Fruit > 。
### 逆变
@@ 超类型通配符,可以声明通配符是由某个特定类的任何基类来界定的,方法时指定
< ? super MyClass> , 还可以使用类型参数 < ? super T>
不能使用:< T super MyClass >
使得你可以安全地传递一个类型对象到泛型类型中。
@@ 例如:
public class SuperTypeWildcards {
static void writeTo ( List< ? super Apple > apples) {
apples.add( new Apple() ) ;
}
}
说明:(1)、List< ? super Apple > ,可以向其中加入 Apple 或者 Apple
的子类型
### 无界通配符
@@ 无界通配符 < ? > 看起来意味着 “ 任何事物 ” , 因此使用无界通配符好像等价于
使用原生类型。
@@ 无界通配符的一个重要应用:当你在处理多个泛型参数时,有时允许一个参数可以
是任何类型,同时为其他参数确定某种特定类型。
@@ 当你拥有的全部都是无界通配符时,就像在 Map< ? , ? > 中看到的那样,编译器
看起来就无法将其与原生 Map 区分开了。
@@ List 实际上表示 “ 持有任何 Object 类型的原生 List ”
List< ? > 表示 “具有某种特定类型的非原生 List , 只是我们不知道那种类型是什么”。
@@ 无论何时,只要使用了原生类型,都会放弃编译期检查。
@@ 如果向接受 “ 确切 ”泛型类型(没有通配符)的方法传递一个原生 Holder 引用,就
会得到一个警告,因为确切的参数期望得到在原生类型中并不存在的信息。
@@ 使用确切类型来替代通配符类型的好处是,可以用泛型参数来做更多的事,但是使
用通配符使得你必须接受范围更宽的参数化类型作为参数。因此,必须逐个情况地
权衡利弊,找到更适合你的需求的方法。
### 捕获转换
@@ 如果向一个使用< ? > 的方法传递原生类型,那么对编译器来说,可能会推断出
实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法。
------这种技术称为 “捕获转换 ” ,因为未指定的通配符类型被捕获,并被转换为确切
类型。
@@ 例如:
public class CaptureConversion {
static <T> void f1 ( Holder<T> holder){
}
static void f2 ( Holder<?> holder ){
f1 ( holder ) ;
}
}
说明:在 f2( ) 中,f1( ) 被调用,而 f1( ) 需要一个已知参数。这里所发生的是:
参数类型在调用 f2( ) 的过程中被捕获,因此它可以在对 f1( ) 的调用中被使用。
@@ 捕获转换技术:要求要在传递 Holder<?> 时同时传递一个具体类型。
@@ 捕获转换技术只有在这样的情况下可以工作:即在方法内部,你需要使用
确切的类型。
》》问题
### 任何基本类型都不能作为类型参数
@@ 解决之道是使用基本类型的包装器类以及 Java SE5 的自动包装机制。
如果创建一个 ArrayList<Integer> , 并将基本类型 int 应用于这个容器,那么
你将发现自动包装机制将自动地实现 int 到 Integer 的双向转换。
@@ 如果性能成为了问题,就需要使用专门适配基本类型的容器版本。
Org.apache.commons.collections.primitives 就是一种开源的这类版本。
@@ 注意,自动包装机制解决了一些问题,但并不是解决了所有问题。
@@ 自动包装机制不能应用于数组。
### 实现参数化接口
@@ 一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会
成为相同的接口。
### 转型和警告
@@ 使用带有泛型类型参数的转型或 instanceof 不会有任何效果。
@@ 当你被强制要求转型,但是又被告知不应该转型。解决这个问题,必须使用
Java SE5 中引入的新的转型形式,既通过泛型类来转型。
例如:
public class ClassCasting {
public void f ( String[] args) throws Exception {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(args[0]) ) ;
List<Widget> lw2 = List.class.cast( in.readObject( ) ) ;
}
}
补充:上面的不能声明为:
List<Widget>.class.cast( in.readObject( ) ) ;
也不能声明为:
( List<Widget> ) List.class.cast( in.readObject( ) ) ;
### 重载
### 基类劫持了接口
@@ 例如:
public class ComparablePet implements Comparable<ComparablePet>{
public int compareTo( ComparablePet arg ){
return 0 ;
}
}
// 下面的 comparaTo( Cat arg) 是不能工作的。一旦为 Comparable 确定了
ComparablePet 参数,那么其他任何实现类都不能与 ComparablePet 之外的任何
对象比较
class Cat extends ComparablePet implements Comparable<Cat>{
public int comparaTo( Cat arg){
return 0 ;
}
}
// 下面的 Hamster 说明再次实现 ComparablePet 中的相同接口是可能的,只要
它们精确地相同,包括参数在内。但是,这只是与覆盖基类中的方法相同
class Hamster extends ComparablePet implements Comparable<ComparablePet>{
public int compareTo( ComparablePet arg ){
return 0 ;
}
}
》》自限定的类型
@@ 在 Java 泛型中,有一个好像是经常性出现的惯用法。如下:
class SelfBounded< T extends SelfBounded<T>> {
}
说明:它强调的是当 extends 关键字用于边界与用来创建子类明显是不同的。
### 古怪的循环泛型(CRG)
@@ 例如:
class GenericType <T> {
}
public class CuriouslyRecurringGeneric extends
GenericType<CuriouslyRecurringGeneric> {
}
其含义: “ 古怪的循环” ---》我在创建一个新类,它继承自一个泛型基类,这个泛型类型
接受我的类的名字作为其参数。
@@ 当给出导出类的名字时, 泛型基类能够实现什么呢?
(1)、能够产生使用导出类作为其参数和返回类型的基类。
(2)、还能导出类型作用域类型,甚至那些将被擦除为 Object 的类型
@@ CRG 的本质: 基类用导出类替代其参数。
意味着泛型基类变成了一种其所有导出类的公共功能的模板,但是这些功能对于其所有
参数和返回值,都将使用导出类型。也就是说,在所产生的类中将使用确切类型而不是
基类型。
### 自限定
@@ 自限定所做的,就是要求在继承关系中,像下面这样使用这个类:
class A extends SelfBounded<A> {
}
这会强制要求将正在定义的类当作参数传递给基类。
@@ 自限定的参数有何意义呢?
它可以保证类型参数必须与正在被定义的类相同。
@@ 自限定惯用法不是可强制执行的。如果它确实很重要,可以要求一个外部
工具来确保不会使用原生类型来替代参数化类型。
@@ 注意,可以移除自限定这个限制。(泛型参数中不使用 extends 关键字)。
@@ 自限定只能强制作用于继承关系。如果使用自限定,就应该了解这个类所用
类型参数将与使用这个参数的类具有相同的基类型。这会强制要求使用这个类的
每个人都要遵循这种形式。
@@ 可以将自限定用于泛型方法:
public class SelfBoundingMethods {
static <T extends SelfBounded<T> > T f ( T arg ) {
return arg.set(arg).get() ;
}
public static void main( String[] args){
A a = f ( new A () ) ;
}
}
这可以防止这个方法被应用于除上述形式的自限定参数之外的任何事物上。
### 参数协变
@@ 自限定类型的价值在于它们可以产生协变参数类型-------方法参数类型会随
子类而变换。
@@ 在使用自限定类型时,在导出类中只有一个方法,并且这个方法接受
导出类型而不接受基类型为参数。
例如:
interface SelfBoundSetter < T extends SelfBoundSetter<T> > {
void set ( T arg ) ;
}
interface Setter extends SelfBoundSetter<Setter> {
}
public class SelfBoundingAndCovarantArguments {
void testA ( Setter s1 , Setter s2 , SelfBoundSetter sbs) {
s1.set( s2 ) ; // 可以执行
s1.set( sbs ) ; // 报错
}
}
说明:编译器不能识别将基类型当作参数传递给 set( ) 的尝试,因为没有任何
方法具有这样的签名。实际上,这个参数已经被覆盖了。
@@ 如果不使用自限定,将重载参数类型。如果使用自限定,只能获得某个方法
的一个版本,它将接受确切的类型参数。
》》动态类型安全
@@ Java SE5 的 java.util.Collections 中的一组便利工具,可以解决类型检查问题,它们是:
----- static checkedCollection()
----- static checkedList()
----- static checkedMap()
----- static checkedSet()
----- static checkedSortedMap()
----- static checkedSortedSet()
上面的方法每一个都会将你希望动态检查的容器当作第一个参数接受,并将你希望强制要求
的类型作为第二个参数。
@@ 受检查的容器在你试图插入类型不正确的对象时抛出 ClassCastException ,这与泛型
之前的(原生)容器形成了对比,对于后者来说,当你将对象从容器中取出时,才会通知你
出现了问题。在后一种情况中,你知道存在问题,但是不知道罪魁祸首在哪里,如果使用受
检查的容器,就可以发现谁在试图插入不良对象。
@@ 可以将导出类型的对象放置到将要检查基类型的受检查的容器中。
》》异常
@@ 由于擦除的原因,将泛型应用于异常是非常受限的。 catch 语句不能捕获泛型类型的
异常,因为在编译期和运行期都必须知道异常的确切类型。泛型类不能直接或间接继承自
Throwable 。
@@ 类型参数可能会在 一个方法的 throws 子句中用到。这使得你可以编写随检查型异常
的类型而发生变化的泛型代码。
----------------------------------------------------------
例如:
interface Processor< T , E extends Exception>{
void process( List<T> resultCollector ) throws E ;
}
如果不能参数化所抛出的异常,那么由于检查型异常的缘故,将不能编写出这种泛化的代码。
---------------------------------------------------------------
》》混型
@@ 最基本的概念:混合多个类的能力,以产生一个可以表示混型中所有类型的类。它将 使组
装多个类变得简单易行。
@@ 混型的价值之一 是它们可以将特性和行为一致地 应用于多个类之上。如果想在混型类中
修改某些东西,作为一种意外地好处,这些修改将会应用于混型所应用的所有类型之上。
正由于此,混型有一点面向方面编程(AOP)的味道,而方面 经常被建议用来 解决混型问
题。
### C++ 中的混型
@@ 在C++ 中,使用多重继承的最大理由,就是为了使用混型。但是,对于混型来说,更有趣 、
更优雅的方式是使用参数化类型,因为混型就是继承自其类型参数的类。
@@ 在 C++ 中,可以很容易的创建混型,因为 C++ 能够记住其模板参数的类型。
@@ 可以将混型看作是一种功能,它可以将现有类映射到新的子类上。注意,使用这种技术来
创建一个混型是多么地轻而易举。
### 与接口混合(实现多个接口)
@@ Java 中使用接口来实现产生混型效果。
@@ 理解与使用代理。
### 使用装饰器模式
@@ 装饰器模式使用分层对象来动态透明地向单个对象中添加责任。
@@ 装饰器指定包装在最初的对象周围的所有对象都具有相同的基本接口。
@@ 某些事物是可装饰的,可以通过其他类包装在这个可装饰对象的四周,来将功能分层。
使得对装饰器的使用是透明的------无论对象是否被装饰,你都拥有一个可以向对象发送
的公共消息集。
@@ 装饰类也可以添加新方法,但是正如你所见,这将是受限的。
@@ 装饰器的例子:
class Basic {
private String value ;
public void set (String val ){
value = val ;
}
public String get( ){
return value ;
}
}
class Decorater extends Basic {
protected Basic basic ;
public Decorater ( Basic basic ) {
this.basic = basic ;
}
public void set ( String val ){
basic.set ( val );
}
public String get ( ){
return basic.get( ) ;
}
}
补充:(1)、使用装饰器所产生的对象类型是最后被装饰的类型。也就是说,
尽管可以添加多个层,但是最后一层才是实际的类型,因此只有最后一
层的方法是可视的。
(2)、装饰器是通过使用组合和形式化结构(可装饰物 / 装饰器层次结构)
来实现的。
@@ 装饰器模式与混型之间的区别:
(1)、装饰器是通过组合和形式化结构(可装饰物 / 装饰器层次结构)来实现
的,而混型是基于继承的。因此可以将基于参数化类型的混型当作是一种泛型
装饰器机制,这种机制不需要装饰器设计模式的继承结构。
(2)、装饰器模式只有最后一层的方法是可视的,而混型的类型是所有被混合
到一起的类型。因此对于装饰器来说,其明显的缺陷是它只能有效地工作于装
饰中的一层(最后一层),而混型方法显然更自然一些。
### 与动态代理混合
@@ 可以使用动态代理来创建一种比装饰器更贴近混型模型的机制。通过使用动态代理,
所产生的动态类型将会是已经混入的组合类型。
@@ 由于动态代理的限制,每个被混入的类都必须是某个接口的实现。
@@ 为了让 Java 支持混型,人们已经做了大量的工作朝着这个目标努力,包括创建了至少
一种附加语言(Jam 语言),它是专门用来支持混型的。
》》潜在类型机制
@@ 在某种情况下,你最终可能会使用普通类或普通接口,因为限定边界的泛型可能会和
指定类或接口没有任何区别。
------- 某种编程语言提供的一种解决方案称为:潜在类型机制或结构化类型机制。还有更古怪
的术语称为鸭子类型机制。
@@ 泛型代码典型地将在泛型类型上调用少量方法,而具有潜在类型机制的语言只要求实现
某个方法子集,而不是某个特定类或接口,从而放松了这种限制(并且可以产生更加泛化的
代码)。正由于此,潜在类型机制使得你可以横跨类继承结构,调用不属于某个公共接口的
方法。
@@ 潜在类型机制是一种代码组织和复用机制。有了它编写出的代码相对于没有它编写出的
代码,能够更容易地复用。
代码组织和复用是所有计算机编程的基本手段:编写一次,多次使用,并在一个位置
保存代码。
因为我并未被要求去命名我的代码要操作于其上的确切接口,所以,有了潜在类型机制,
我就可以编写更少的代码,并更容易地将其应用于多个地方。
@@ 两种支持潜在类型机制的语言: Python 和 C++ 。
---------- Python 是动态类型语言(事实上所有的类型检查都发生在运行时)
---------- C++ 是静态类型语言(类型检查发生在编译期)
因此,潜在类型机制不要求静态或动态类型检查。
补充:
尽管它们是在不同时期实现的潜在类型机制,C++ 在编译期,而 Python 在
运行时,但是这两种语言都可以确保类型不会被误用,因此被认为是强类型的。潜在类型
机制没有损害强类型机制。
@@ 补充: Ruby 和 Smalltalk 语言也支持潜在类型机制。
》》对缺乏潜在类型机制的补偿
@@ 尽管 Java 不支持潜在类型机制,但是这并不意味着有界泛型代码不能在不同的类型
层次结构之间应用。也就是说,我们仍旧可以创建真正的泛型代码,但是需要付出一定额外
的努力。
### 反射
@@ 使用 Class 对象
### 将一个方法应用于序列
@@ Iterable 接口是由 Java 容器类使用的内建接口。
@@ 反射将所有的类型检查都转移到了运行时,因此在许多情况下并不是我们所希望的。
如果能够实现编译期类型检查,这通常会更符合要求。
@@ 确保编译期检查的常见建议是定义一个工厂接口,它有一个生成对象的方法。
@@ 注意 ,类型标记技术是 Java 文献提供的技术。
@@ 我们必须知道使用反射可能比非反射的实现要慢一些,因为有太多的动作都是在运行
时发生的。
### 当你并未碰巧拥有正确的接口时
### 用适配器仿真潜在类型机制
@@ 潜在类型机制创建了一个包含所需方法的隐式接口。因此它遵循这样的规则:如果
我们手工编写必需的接口(因为 Java 并没有为我们做这些事),那么它就应该能够解决
问题。
@@ 从我们拥有的接口中编写代码来产生我们需要的接口,这是适配器设计模式的一个
典型示例。我们可以使用适配器来适配已有的接口,以产生想要的接口。
》》将函数对象作用策略
@@ 策略设计模式:
--------- 将 “ 变化的事物 ” 完全隔离到一个函数对象中。
--------- 函数对象就是在某种程度上行为像函数的对象-----一般地,会有一个相关的方法
(在支持操作符重载的语言中,可以创建对这个方法的调用,而这个调用看起来就
和普通的方法调用一样)
-------- 函数对象的价值就在于,与普通方法的不同,它们可以传递出去,并且还可以拥有
多个调用之间持久化的状态。当然,可以用类中的任何方法来 实现与此相似的操作,
但是(与使用任何设计模式一样)函数对象主要是由其目的来区别的。
@@ 在 C++ 中,潜在类型机制将在你调用函数时负责协调各个操作,但是在 Java 中,
需要编写函数对象来将泛型方法适配为我们特定的需求。
》》总结:转型真的如此之糟吗?
@@ 论点:使用泛型类型机制的最吸引人的地方,就是在使用容器类的地方,这些类包括诸如
各种 List 、 各种 Set 、 各种 Map
@@ 在 Java SE5 之前,当你将一个对象放置到容器中时,这个对象就会被向上转型为
Object ,因此你会丢失类型信息。当你想要将这个对象从容器中取回,用它去执行某些
操作时,必须将其向下转型为正确的类型。
@@ 如果没有 Java SE5 的泛型版本的容器,你放到容器里的和从容器中取回的,都是
Object 。
@@ 注意,因为泛型是后来添加到 Java 中,而不是从一开始就设计到这种语言中的,
所以某些容器无法达到它们应该具备的健壮性。
@@ 某些语言,特别是 Nice (这种语言可以产生 Java 字节码,并可以工作于现有的
Java 类库之上) 和 NextGen 已经融入了更简洁、影响更小的方式,来实现参数化类型。