《数据结构与算法分析》读书笔记:第一章 引 论
目录
1.1本书讨论的内容
1.2数学知识复习
1.2.1指数
1.2.2对数
1.2.3级数
1.2.4模运算
1.2.5证明的方法
1.3递归简论
1.4实现泛型构件pre-Java 5
1.4.1使用Object表示泛型
1.4.2基本类型的包装
1.4.3使用接口类型表示泛型
1.4.4数组类型的兼容性
1.5利用Java 5泛型特性实现泛型构件
1.5.1简单的泛型类和接口
1.5.2自动装箱/拆箱
1.5.3菱形运算符
1.5.4带有限制的通配符
1.5.5泛型static方法
1.5.6类型限界
1.5.7类型擦除
1.5.8对于泛型的限制
1.6函数对象
1.1本书讨论的内容
在许多问题当中,一个重要的观念是:写出一个工作程序并不够。如果这个程序在巨大的数据集上运行,那么运行时间就变成了重要的问题。我们将在本书看到对于大量的输入如何估计程序的运行时间,尤其是如何在尚未具体编码的情况下比较两个程序的运行时间。我们还将看到彻底改进程序速度以及确定程序瓶颈的方法。这些方法将使我们能够发现需要我们集中精力努力优化的那些代码段。
1.2数学知识复习
本节列出一些需要记忆或是能够推导出的基本公式,并从推导过程复习基本的证明方法。
1.2.1指数
1.2.2对数
在计算机科学中,除非有特别的声明,否则所有的对数都是以2为底的。
1.2.3级数
1.2.4模运算
如果N整除A-B,那么就说A与B模N同余,记为A≡B( mod N)。直观地看,这意味着无论是A还是B被N去除,所得余数都是相同的。于是,81 ≡61≡1 ( mod 10)。如同等号的情形一样,若A≡B( mod N),则A+C≡B+C( mod N)以及AD≡BD( mod N)。
有许多定理适用模运算,其中有些特别需要用到数论来证明。我们将尽量少使用模运算,这样,前面的一些定理也就足够了。
1.2.5证明的方法
证明数据结构分析中的结论的两种最常用的方法是归纳法证明和反证法证明(偶尔也被迫用到只有教授们才使用的证明)。证明一个定理不成立的最好的方法是举出一个反例。
归纳法证明
由归纳法进行的证明有两个标准的部分。第一步是证明基准情形,就是确定定理对于某个(某些)小的(通常是退化的)值的正确性;这一步几乎总是很简单的。接着,进行归纳假设。一般说来,它指的是假设定理对直到某个有限数k的所有的情况都是成立的。然后使用这个假设证明定理对下一个值(通常是k+1)也是成立的。至此定理得证(在k是有限的情形下)。
1.3递归简论
上面的讨论导致递归的前两个基本法则:
- 基准情形。必须总要有某些基准的情形,它们不用递归就能求解。
- 不断推进。对于那些要递归求解的情形,递归调用必须总能够朝着一个基准情形推进。
在本书中我们将用递归解决一些问题。作为非数学应用的一个例子,考虑一本大词典。词典中的词都是用其他的词定义的。当查一个单词的时候,我们不是总能理解对该词的解释,于是我们不得不再查找解释中的一些词。同样,对这些词中的某些地方我们又不理解,因此还要继续这种查找。因为词典是有限的,所以实际上或者我们最终要查到一处,明白了此处解释中所有的单词(从而理解这里的解释,并按照查找的路径回查其余的解释)或者我们发现这些解释形成一个循环,无法理解其最终含义,或者在解释中需要我们理解的某个单词不在这本词典里。
我们理解这些单词的递归策略如下:如果知道一个单词的含义,那么就算我们成功;否则,就在词典里查找这个单词。如果我们理解对该词解释中的所有的单词,那么又算我们成功;否则,通过递归查找一些我们不认识的单词来“算出”对该单词解释的含义。如果词典编纂得完美无瑕,那么这个过程就能够终止;如果其中一个单词没有查到或是循环定义(解释),那么这个过程则循环不定。
打印输出整数
设有一个正整数n并希望把它打印出来。我们的例程的名字为printout(n)。假设仅有的现成I/O例程将只处理单个数字并将其输出到终端。我们为这种例程命名为printDigit;例如,printDigit(4)将输出4到终端。
递归将为该问题提供一个非常漂亮的解。要打印76234,我们首先需要打印出7623,然后再打印出4。第二步用语句printDigit(n% 10)很容易完成,但是第一步却不比原问题简单多少。它实际上是同一个问题,因此可以用语句printout (n /10 )递归地解决它。
这告诉我们如何去解决一般的问题,不过我们仍然需要确认程序不是循环不定的。由于我们尚未定义一个基准情况,因此很清楚,我们仍然还有些事情要做。如果0≤n<10,那么基准情形就是printDigit(n)。现在,printout(n)已对每一个从0到9的正整数定义,而更大的正整数则用较小的正整数定义。因此,不存在循环的问题。整个方法在图1-4中指出。
我们没有努力去高效地做这件事。我们本可以避免使用mod例程(它是非常耗时的),因为n%10 = n -(n/10)* 10
递归和归纳
让我们多少严格一些地证明上述递归的整数打印程序是可行的。为此,我们将使用归纳法证明。 定理1.4
递归的整数打印算法对n≥0是正确的。
证明(通过对n所含数字的个数,用归纳法证明之):
首先,如果n只有一位数字,那么程序显然是正确的,因为它只是调用一次printDigit。然后,设printout对所有k个或更少位数的数均能正常工作。我们知道k +1位数字的数可以通过其前k位数字后跟一位最低位数字来表示。但是前k位数字形成的数恰好是n/10,由归纳假设它能够被正确地打印出来,而最后的一位数字是n mod 10,因此该程序能够正确打印出任意k+1位数字的数。于是,根据归纳法,所有的数都能被正确地打印出来。
这个证明看起来可能有些奇怪,但它实际上相当于是算法的描述。证明阐述的是在设计递归程序时,同一问题的所有较小实例均可以假设运行正确,递归程序只需要把这些较小问题的解(它们通过递归奇迹般地得到)结合起来形成现行问题的解。其数学根据则是归纳法的证明。由此,我们给出递归的第三个法则:
3.设计法则( design rule)。假设所有的递归调用都能运行。
这是一条重要的法则,因为它意味着,当设计递归程序时一般没有必要知道簿记管理的细节,你不必试图追踪大量的递归调用。追踪具体的递归调用的序列常常是非常困难的。当然,在许多情况下,这正是使用递归好处的体现,因为计算机能够算出复杂的细节。
递归的主要问题是隐含的簿记开销。虽然这些开销几乎总是合理的(因为递归程序不仅简化了算法设计而且也有助于给出更加简洁的代码),但是递归绝不应该作为简单for循环的代替物。我们将在3.6节更仔细地讨论递归涉及的系统开销。
当编写递归例程时,关键是要牢记递归的四条基本法则:
- 基准情形。必须总要有某些基准情形,它无需递归就能解出。
- 不断推进。对于那些需要递归求解的情形,每一次递归调用都必须要使状况朝向一种基准情形推进。
- 设计法则。假设所有的递归调用都能运行。
- 合成效益法则( compound interest rule)。在求解一个问题的同一实例时,切勿在不同的递归调用中做重复性的工作。
第四条法则(连同它的名称一起)将在后面的章节证明是合理的。使用递归计算诸如斐波那契数之类简单数学函数的值的想法一般来说不是一个好主意,其道理正是根据第四条法则。只要在头脑中记住这些法则,递归程序设计就应该是简单明了的。
1.4实现泛型构件pre-Java 5
面向对象的一个重要目标是对代码重用的支持。支持这个目标的一个重要的机制就是泛型机制:如果除去对象的基本类型外,实现方法是相同的,那么我们就可以用泛型实现来描述这种基本的功能。例如,可以编写一个方法,将由一些项组成的数组排序;方法的逻辑关系与被排序的对象的类型无关,此时可以使用泛型方法。
与许多新的语言(例如C++,它使用模板来实现泛型编程)不同,在1.5版以前,Java并不直接支持泛型实现,泛型编程的实现是通过使用继承的一些基本概念来完成的。本节描述在Java中如何使用继承的基本原则来实现一些泛型方法和类。
Sun公司在2001年是把对泛型方法和类的直接支持作为未来的语言增强剂来宣布的。后来,终于在2004年末发表了Java 5并提供了对泛型方法和类的支持。然而,使用泛型类需要理解pre-Java 5对泛型编程的语言特性。因此,对继承如何用来实现泛型程序的理解是根本的关键,甚至在Java 5中仍然如此。
1.4.1使用Object表示泛型
Java中的基本思想就是可以通过使用像Object这样适当的超类来实现泛型类。在下列代码所示的 Memorycell类就是这样一个例子。
当我们使用这种策略时,有两个细节必须要考虑。第一个细节在图1-6中阐释,它描述一个main方法,该方法把串“37”写到MemoryCell对象中,然后又从Memorycell对象读出。为了访问这种对象的一个特定方法,必须要强制转换成正确的类型。(当然,在这个例子中,可以不必进行强制转换,因为在程序的第9行可以调用toString()方法,这种调用对任意对象都是能够做到的)。
第二个重要的细节是不能使用基本类型。只有引用类型能够与Object相容。这个问题的标准工作马上就要讨论。
1.4.2基本类型的包装
当我们实现算法的时候,常常遇到语言定型问题:我们已有一种类型的对象,可是语言的语法却需要一种不同类型的对象。
这种技巧阐释了包装类的基本主题。一种典型的用法是存储一个基本的类型,并添加一些这种基本类型不支持或不能正确支持的操作。
在Java中我们已经看到,虽然每一个引用类型都和Object相容,但是,8种基本类型却不能。于是,Java为这8种基本类型中的每一种都提供了一个包装类。例如,int类型的包装是Integer。每一个包装对象都是不可变的(就是说它的状态绝不能改变),它存储一种当该对象被构建时所设置的原值,并提供一种方法以重新得到该值。包装类也包含不少的静态实用方法。
例如,图1-7说明如何能够使用Memorycel1来存储整数。
1.4.3使用接口类型表示泛型
只有在使用Object类中已有的那些方法能够表示所执行的操作的时候,才能使用Object作为泛型类型来工作。
例如,考虑在由一些项组成的数组中找出最大项的问题。基本的代码是类型无关的,但是它的确需要一种能力来比较任意两个对象,并确定哪个是大的,哪个是小的。因此,我们不能直接找出Object 的数组中的最大元素——-我们需要更多的信息。最简单的想法就是找出Comparable 的数组中的最大元。要确定顺序,可以使用compareTo方法,我们知道,它对所有的Comparable都必然是现成可用的。图1-8中的代码做的就是这项工作,它提供一种main方法,该方法能够找出String或Shape数组中的最大元。
现在,提出几个忠告很重要。首先,只有实现Comparable接口的那些对象才能够作为Comparable数组的元素被传递。仅有compareTo方法但并未宣称实现Comparable接口的对象不是Comparable的,它不具有必需的IS-A关系。因为我们也许会比较两个shape的面积,因此假设shape实现Comparable接口。这个测试程序还告诉我们,circle、square和Rectangle都是Shape的子类。
第二,如果Comparable数组有两个不相容的对象(例如,一个 string和一个Shape),那么CompareTo方法将抛出异常classCastException。这是我们期望的性质。
第三,如前所述,基本类型不能作为Comparable传递,但是包装类则可以,因为它们实现了Comparable接口。
第四,接口究竟是不是标准的库接口倒不是必需的。
最后,这个方案不是总能够行得通,因为有时宣称一个类实现所需的接口是不可能的。例如,一个类可能是库中的类,而接口却是用户定义的接口。如果一个类是final类,那么我们就不可能扩展它以创建一个新的类。1.6节对这个问题提出了另一个解决方案,即function object。这种函数对象也使用一些接口,它或许是我们在Java库中所遇到的核心论题之一。
1.4.4数组类型的兼容性
语言设计中的困难之一是如何处理集合类型的继承问题。设Employee IS-A Person。那么,这是不是也意味着数组Employee[ ] IS-A Person[]呢?换句话说,如果一个例程接受Person[]作为参数,那么我们能不能把Employee[]作为参数来传递呢?
乍一看,该问题不值得一问,似乎 Employee[]就应该是和 Person[]类型兼容的。然而,这个问题却要比想象的复杂。假设除Employee外,我们还有student IS-A Person,并设Employee[]是和Person[]类型兼容的。此时考虑下面两条赋值语句:
两句都编译,而arr[0]实际上是引用一个Employee,可是 student IS- NOT- AEmployee。这样就产生了类型混乱。运行时系统( runtime system ) ( Java 虚拟机―译者注)不能抛出classCastException异常,因为不存在类型转换。
避免这种问题的最容易的方法是指定这些数组不是类型兼容的。可是,在Java 中数组却是类型兼容的。这叫作协变数组类型。每个数组都明了它所允许存储的对象的类型。如果将一个不兼容的类型插入到数组中,那么虚拟机将抛出一个ArraystoreException异常。
在较早版本的Java中是需要数组的协变性的,否则在图1-8的第29行和第30行的调用将编译不了。
1.5利用Java 5泛型特性实现泛型构件
1.5.1简单的泛型类和接口
//MemoryCell类的泛型实现
public class GenericMemoryCell<AnyType>{public AnyType read(){return storedValue;}public void write(AnyType x){storedValue=x;}private AnyType storedValue;
}
上述代码是前面图1-5描述的 Memorycell的泛型版代码。这里,我们把名字改成了GenericMemoryCel1,因为两个类都不在包中,所以名字也就不能相同。
当指定一个泛型类时,类的声明则包含一个或多个类型参数,这些参数被放在类名后面的一对尖括号内。第1行指出,Generic-Memorycell有一个类型参数。在这个例子中,对类型参数没有明显的限制,所以用户可以创建像GenericMemorycell<String >和GenericMemoryCell <Integer >这样的类型,但是不能创建GenericMemoryCell<int>这样的类型。在GenericMemorycell类声明内部,我们可以声明泛型类型的域和使用泛型类型作为参数或返回类型的方法。例如在代码中的第5行,类GenericMemorycell<string >的write方法需要一个string类型的参数。如果传递其他参数那将产生一个编译错误。
也可以声明接口是泛型的。例如,在Java 5以前,comparable接口不是泛型的,而它的compareTo方法需要一个object作为参数。于是,传递到compareTo方法的任何引用变量即使不是一个合理的类型也都会编译,而只是在运行时报告ClassCastException错误。在Java 5 中,Comparable接口是泛型的,
//Java 5 版本的Comparable接口,它是泛型接口
package java.lang;pub1ic interface Comparable<AnyType>{public int compareTo( AnyType ths,AnyType rhs );
}
例如,现在String类实现Comparable < String >并有一个compareTo方法,这个方法以一个String 作为其参数。通过使类变成泛型类,以前只有在运行时才能报告的许多错误如今变成了编译时的错误。
1.5.2自动装箱/拆箱
图1-7中的代码写得很麻烦,因为使用包装类需要在调用write之前创建Integer对象,然后才能使用intvalue方法从Integer中提取int 值。在Java 5以前,这是需要的,因为如果一个int型的量被放到需要 Integer对象的地方,那么编译将会产生一个错误信息,而如果将一个Integer对象的结果赋值给一个int 型的量,则编译也将产生一个错误信息。图1-7中的代码准确地反映出基本类型和引用类型之间的区别,但还没有清楚地表示出程序员把那些int存入集合( collection)的意图。
Java 5矫正了这种情形。如果一个int型量被传递到需要一个Integer对象的地方,那么,编译器将在幕后插人一个对Integer构造方法的调用。这就叫作自动装箱。而如果一个Integer对象被放到需要int 型量的地方,则编译器将在幕后插入一个对intvalue方法的调用,这就叫作自动拆箱。对于其他7对基本类型包装类型,同样会发生类似的情形。下列代码用Java 5描述了自动装箱和自动拆箱的使用。注意,在GenericMemoryCell中引用的那些实体仍然是Integer对象;在GenericMemorycell的实例化中,int不能够代替Integero
//自动装箱和拆箱(Java 5)
class BoxingDemo{public static void main( String [ ] args ){GenericMemoryCe11<Integer> m = new GenericMemoryCe11<Integer>();m.write(37);int val = m.read();System.out.println( "Contents are: " + val );}
}
1.5.3菱形运算符
在上述代码中,第5行有些烦人,因为既然m 是GenericMemorycell <Integer >类型的,显然创建的对象也必须是GenericMemorycell<Integer >类型的,任何其他类型的参数都会产生编译错误。Java 7增加了一种新的语言特性,称为菱形运算符,使得第5行可以改写为
GenericMemorycell<Integer> m = new GenericMemoryCe11<>( );
菱形运算符在不增加开发者负担的情况下简化了代码,我们通篇都会使用它。下列代码给出了带菱形运算符的Java 7版代码。
//自动装箱和拆箱(Java 7,使用菱形运算符)
class BoxingDemo{public static void main( String [ ] args ){GenericMemoryCe11<Integer> m = new GenericMemoryCe11<>();m.write(5);int val = m.read();System.out.println( "Contents are: " + val );}
}
1.5.4带有限制的通配符
//Shape[]的totalArea方法
public static doub1e totalArea( Shape[] arr ){double total = 0;for( Shape s : arr )if( s != nul1 )total += s.area();return total;
}
上述代码显示一个static方法,该方法计算一个Shape数组的总面积(假设Shape是含有area方法的类;而circle和 square则是继承shape 的类)。假设我们想要重写这个计算总面积的方法,使得该方法能够使用Collection <Shape >这样的参数。Collection将在第3章描述。当前,唯一重要的是它能够存储一些项,而且这些项可以用一个增强的for循环来处理。由于是增强的for循环,因此代码是相同的,最后的结果如下列代码所示。如果传递一个Collection <Shape >,那么,程序会正常运行。可是,要是传递一个Collection<square >会发生什么情况呢?答案依赖于是否Collection <Square > IS-A Collection<Shape >。回顾1.4.4节可知,用技术术语来说即是否我们拥有协变性。
/totalArea方法,如果传递一个Collection<Square>,则该方法不能运行
public static double tota1Area( Collection<Shape> arr ){double total = 0;for( Shape s : arr )if( s != nul1 )total += s.area( );return total;
}
我们在1.4.4节提到,Java中的数组是协变的。于是,Square[ ] lS-A Shape[ ]。一方面,这种一致性意味着,如果数组是协变的,那么集合也将是协变的。另一方面,我们在1.4.4节看到,数组的协变性导致代码得以编译,但此后会产生一个运行时异常(一个ArraystoreException)。因为使用泛型的全部原因就在于产生编译器错误而不是类型不匹配的运行时异常,所以,泛型集合不是协变的。因此,我们不能把collection <square >作为参数传递到代码中的方法里去。
现在的问题是,泛型(以及泛型集合)不是协变的(但有意义),而数组是协变的。若无附加的语法,则用户就会避免使用集合( collection),因为失去协变性使得代码缺少灵活性。
Java 5用通配符( wildcard)来弥补这个不足。通配符用来表示参数类型的子类(或超类)。下列代码描述带有限制的通配符的使用,图中编写一个将collection <T >作为参数的方法totalArea,其中T IS-A Shape。因此,Collection <Shape >和Collection<square >都是可以接受的参数。通配符还可以不带限制使用(此时假设为extends object),或不用extends而用super(来表示超类而不是子类);此外还存在一些其他的语法,我们就不在这里讨论了。
//用通配符修正后的totalArea方法,如果传递一个
collection < Square >,则方法能够正常运行public static double tota1Area( Collection<? extends Shape> arr ){double total = 0;for( Shape s : arr )if( s != nu11 )total += s.area( );return total;
}
1.5.5泛型static方法
从某种意义上说,上述代码中的totalArea方法是泛型方法,因为它能够接受不同类型的参数。但是,这里没有特定类型的参数表,正如在GenericMemorycell类的声明中所做的那样。有时候特定类型很重要,这或许因为下列的原因:
- 该特定类型用做返回类型;
- 该类型用在多于一个的参数类型中;
- 该类型用于声明一个局部变量。
如果是这样,那么,必须要声明一种带有若干类型参数的显式泛型方法。
例如,下面代码显示一种泛型static方法,该方法对值x在数组arr中进行一系列查找。通过使用一种泛型方法,代替使用object作为参数的非泛型方法,当在Shape对象的数组中查找Apple对象时我们能够得到编译时错误。
//泛型static方法搜索数组
public static <AnyType> boolean contains( AnyType[] arr,AnyType x ){for( AnyType val : arr )if( x.equals( val ) )return true;return false;
}
泛型方法特别像是泛型类,因为类型参数表使用相同的语法。在泛型方法中的类型参数位于返回类型之前。
1.5.6类型限界
假设我们想要编写一个findMax例程。考虑下列的代码。由于编译器不能证明在第6行上对compareTo 的调用是合法的,因此,程序不能正常运行;只有在AnyType是Comparable的情况下才能保证compareTo存在。我们可以使用类型限界解决这个问题。类型限界在尖括号内指定,它指定参数类型必须具有的性质。一种自然的想法是把性质改写成
public ststic<AnyType extends Comparable>...
//泛型static方法查找一个数组中最大元素,该方法不能正常运行
public static <AnyType> AnyType findMax ( AnyType [ ] arr ){int maxIndex = o;for( int i = l; i < arr.length; i++ )if( arr[ i ].compareTo( arr[ maxIndex ] ) > 0 )maxIndex = i;return arr[ maxIndex ];
)
我们知道,因为Comparable接口如今是泛型的,所以这种做法很自然。虽然这个程序能够被编译,但是更好的做法却是
public static <AnyType extends Comparable<AnyType>> ...
然而,这个做法还是不能令人满意。为了看清这个问题,假设Shape实现 Comparable
<Shape > ,设Square继承 Shape。此时,我们所知道的只是Square实现Comparable<shape >。于是,Square IS-A Comparable < Shape >,但它IS-NOT-A Comparable<square > !
应该说,AnyType IS-A Comparable <T>,其中,T是AnyType的父类。由于我们不需要知道准确的类型T,因此可以使用通配符。最后的结果变成
public static <AnyType extends Comparable<? super AnyTypes>>
下列代码显示findMax的实现。编译器将接受类型T的数组,只是使得T实现Comparable<S>接口,其中T IS-A S。当然,限界声明看起来有些混乱。幸运的是,我们不会再看到任何比这种用语更复杂的用语了。
//在一个数组中找出最大元的泛型static方法。以例说明类型参数的限界
public static <AnyType extends Comparable<? super AnyType>>
AnyType findMax ( AnyType [ ] arr ){int maxIndex = 0;for( int i = 1; i < arr.length; i++ )if( arr[ i ].compareTo( arr[ maxIndex ] ) > 0 )maxIndex = i;return arr[ maxIndex ];
}
1.5.7类型擦除
泛型在很大程度上是Java语言中的成分而不是虚拟机中的结构。泛型类可以由编译器通过所谓的类型擦除过程而转变成非泛型类。这样,编译器就生成一种与泛型类同名的原始类,但是类型参数都被删去了。类型变量由它们的类型限界来代替,当一个具有擦除返回类型的泛型方法被调用的时候,一些特性被自动地插入。如果使用一个泛型类而不带类型参数,那么使用的是原始类。
类型擦除的一个重要推论是,所生成的代码与程序员在泛型之前所写的代码并没有太多的差异,而且事实上运行的也并不快。其显著的优点在于,程序员不必把一些类型转换放到代码中,编译器将进行重要的类型检验。
1.5.8对于泛型的限制
对于泛型类型有许多的限制。由于类型擦除的原因,这里列出的每一个限制都是必须要遵守的。
基本类型
基本类型不能用做类型参数。因此,GenericMemorycell <int >是非法的。我们必须使用包装类。
instanceof检测
instanceof检测和类型转换工作只对原始类型进行。在下列代码中:
GenericMemoryCe11<Integer> ce111 = new GenericMemoryCe11<>( );
ce111.write( 4 );
0bject cell = ce111;
GenericMemoryCe11<String> ce112 = (GenericMemoryCe11<String>) cel1;
String s = ce112.read( );
这里的类型转换在运行时是成功的,因为所有的类型都是GenericMemorycell。但在最后一行,由于对read的调用企图返回一个String对象从而产生一个运行时错误。结果,类型转换将产生一个警告,而对应的instanceof检测是非法的。
static的语境
在一个泛型类中,static方法和static域均不可引用类的类型变量,因为在类型擦除后类型变量就不存在了。另外,由于实际上只存在一个原始的类,因此static域在该类的诸泛型实例之间是共享的。
泛型类型的实例化
不能创建一个泛型类型的实例。如果T是个类型变量,则语句
T obj = new T( );/右边是非法的
是非法的。T由它的限界代替,这可能是object(或甚至是抽象类),因此对new的调用没有意义。
泛型数组对象
也不能创建一个泛型的数组。如果T是一个类型变量,则语句
T [ ] arr = new T[ 10 ];//右边是非法的
是非法的。T将由它的限界代替,这很可能是Object T,于是(由类型擦除产生的)对T[]的类型转换将无法进行,因为object[ ] IS-NOT-A T[ ]。由于我们不能创建泛型对象的数组,因此一般说来我们必须创建一个擦除类型的数组,然后使用类型转换。这种类型转换将产生一个关于未检验的类型转换的编译警告。
参数化类型的数组
参数化类型的数组的实例化是非法的。考虑下列代码:
GenericMemoryCe11<String> [ ] arrl = new GenericMemoryce11<>[10];
GenericMemoryCe11<Double> cell = new GenericMemoryCe11>( ); ce11.write( 4.5 );
0bject [ ] arr2 = arr1;
arr2[ 0 ] = cell;
String s = arr1[ 0 ].read( );
正常情况下,我们认为第4行的赋值会生成一个ArraystoreException,因为赋值的类型有错误。可是,在类型擦除之后,数组的类型为GenericMemoryCell[ ],而加到数组中的对象也是GenericMemoryCell,因此不存在ArrayStoreException异常。于是,该段代码没有类型转换,它最终将在第5行产生一个classCastException异常,这正是泛型应该避免的情况。
1.6函数对象
在1.5节我们指出如何编写泛型算法。例如,图1-16中的泛型方法可以用于找出一个数组中的最大项。
然而,这种泛型方法有一个重要的局限:它只对实现Comparable接口的对象有效,因为它使用compareTo作为所有比较决策的基础。在许多情形下,这种处理方式是不可行的。例如,尽管假设Rectangle类实现Comparable接口有些过分,但即使实现了该接口,它所具有的compareTo方法恐怕还不是我们想要的方法。例如,给定一个2×10的矩形和一个5×5的矩形,哪个是更大的矩形呢?答案恐怕依赖于我们是使用面积还是使用长度来决定。或者,如果我们试图通过一个开口构造该矩形,那么或许较大的矩形就是具有较大最小周长的矩形。作为第二个例子,在一个字符串的数组中如果想要找出最大的串(即字典序排在最后的串),默认的compareTo不忽略字符的大小写,则“ZEBRA”按字典序排在“alligator”之前,这可能不是我们想要的。
上述这些情形的解决方案是重写findMax,使它接受两个参数:一个是对象的数组,另一个是比较函数,该函数解释如何决定两个对象中哪个大哪个小。实际上,这些对象不再知道如何比较它们自己;这些信息从数组的对象中完全去除了。
一种将函数作为参数传递的独创方法是注意到对象既包含数据也包含方法,于是我们可以定义一个没有数据而只有一个方法的类,并传递该类的一个实例。事实上,一个函数通过将其放在一个对象内部而被传递。这样的对象通常叫作函数对象。
图1-18显示函数对象想法的最简单的实现。findMax的第二个参数是Comparator类型的对象。接口Comparator在java. util中指定并包含一个compare方法。这个接口在图1-19中指出。
实现接口Comparator <AnyType >类型的任何类都必须要有一个叫作compare 的方法,该方法有两个泛型类型( AnyType)的参数并返回一个int 型的量,遵守和compareTo相同的一般约定。因此,在图1-18中的第9行对compare的调用可以用来比较数组的项。第4行的带有限制的通配符用来表示如果查找数组中的最大的项,那么该comparator必须知道如何比较这些项,或者这些项的超类型的那些对象。我们可以在第26行看到,为了使用这种版本的findMax,findMax通过传递一个string数组以及一个实现comparator <string >的对象而被调用。这个对象属于CaseInsensitiveCompare类型,它是我们编写的一个类。
在第4章我们将给出关于一个类的例子,这个类需要将它存储的项排序。我们将利用comparable编写大部分的代码,并指出其中需要使用函数对象的改动部分。在本书的其他地方,我们将避免函数对象的细节以使得代码尽可能地简单,我们知道以后将函数对象添加进去并不困难。