远见与创新

2006-08-04

Think in Java 笔记 - 7/8章

最近在学Java,总的感觉不错。《Think in Java》这本很好的的书,到第七章开始产生难度,需要做些笔记才不至于看完又忘。下面是第七章-多形和第八章-数组的笔记。

第7章:多形

多形性将接口从具体的实施细节中分离出来。通过继承实现。

Java中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final。

把一个方法声明成final能防止其他人覆盖那个方法。更重要的是,它可"关闭"动态绑定,清楚地告诉编译器不需要进行动态绑定。这样一来,编译器就可为final方法调用生成效率更高的代码。

上溯造型

可将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得一个对象句柄,将其作为基础类型句柄使用的行为就叫作"上溯造型"。

static void tune(Instrument3 i) {
i.play();
}

抽象方法

"抽象方法"属于一种不完整的方法,只含有一个声明,没有方法主体。抽象方法声明时采用语法: abstract void X();

包含了抽象方法的一个类叫作"抽象类"。如果一个类里包含了一个或多个抽象方法,类就必须指定成abstract(抽象)。否则,编译器会向我们报告出错消息。

接口

"interface"(接口)关键字使抽象的概念更深入了一层。它防止客户程序员制作这个类的一个对象,规定它仅仅是一个接口。

接口也可以包含基本数据类型的数据成员,但它们都默认为static和final。static内部类也可以成为接口的一部分。

接口也可以通过extends关键字继承。利用继承技术,可方便地为一个接口添加新的方法声明,也可以将几个接口合并成一个新接口,即"多重继承"。

Java的"多重继承"通过接口实现:将所有接口名置于implements关键字的后面,并用逗号分隔它们。这是接口最关键的作用,也是使用接口最重要的一个原因:可根据需要使用多个接口,而且每个接口都会成为一个独立的类型,可对其进行上溯造型。能上溯造型至多个基础类。

由于置入一个接口的所有字段都自动具有static和final属性,所以接口是对常数值进行分组的一个好工具,它具有与C或C++的enum非常相似的效果。注意根据Java命名规则,拥有固定标识符的static final基本数据类型(亦即编译期常数)都全部采用大写字母(用下划线分隔单个标识符里的多个单词)。

接口中的字段会自动具备public属性,所以没必要专门指定。

内部类

利用private内部类,类设计人员可将具体的实施细节完全隐藏起来。除此以外,因为他们不能被访问,使Java编译器也有机会生成效率更高的代码。而普通(非内部)类不可设为private或protected--只允许设为public或者"友好的"。

若试图定义一个匿名内部类,并想使用在匿名内部类外部定义的一个对象,则编译器要求外部对象为final属性。此外,通过Java 1.1的实例初始化,我们可以有效地为一个匿名内部类创建一个构建器。

一个重要的事实是,创建自己的内部类时,那个类的对象同时拥有指向封装对象的一个链接,所以它们能访问那个封装对象的成员--毋需取得任何资格。它们拥有对封装类所有元素的访问权限。它们可以访问封装类的方法与字段,就象已经拥有了它们一样。这一特征对我们来说是非常方便的。若想生成外部类对象的句柄,就要用一个点号以及一个this来命名外部类。

为告诉创建某个内部类的一个对象,需要在new表达式中提供指向外部类对象的一个句柄,然后利用这个句柄生成内部类的对象,比如:Parcel11 p = new Parcel11(); Parcel11.Contents c = p.new Contents(); 这是由于内部类的对象已同创建它的外部类的对象"默默"地连接到一起。然而,如果生成一个static内部类,就不需要指向外部类对象的一个句柄。

从内部类继承:由于内部类构建器必须同封装类对象的一个句柄联系到一起,所以从一个内部类继承的时候,封装类的"秘密"句柄必须获得初始化,而且在衍生类中不再有一个默认的对象可以连接。需要采用一种特殊的语法,明确建立这种关联。

public class InheritInner
extends WithInner.Inner {
InheritInner(WithInner wi) {
wi.super();
}

当我们从外部类继承的时候,没有任何额外的内部类继承下去。如果需要继承,需要"明确"地从内部类继承:public class Yolk extends Egg2.Yolk

内部类编译为class的时候,它的名字遵守一种严格的形式:先是封装类的名字,再跟随一个$,再跟随内部类的名字。如果内部类是匿名的,那么编译器会简单地生成数字,把它们作为内部类标识符使用。

用内部类的一大好处是为了实现控制框架。"控制框架"属于应用程序框架的一种特殊类型,受到对事件响应的需要的支配。控制框架的设计宗旨是将不同的代码方便地隔离开。比如Java 1.1 AWT就是属于一种控制框架,它通过内部类完美地解决了GUI的问题。

我们通过创建不同的Event内部子类,从而表达出不同的行动。这里正是内部类大显身手的地方。它们允许我们做两件事情:
(1) 在单独一个类里表达一个控制框架应用的全部实施细节,从而完整地封装与那个实施有关的所有东西。
(2) 内部类使我们具体的实施变得更加巧妙,因为它能方便地访问外部类的任何成员。

一条常规的设计准则是:用继承表达行为间的差异,并用成员变量表达状态的变化。

构建器

基础类的构建器肯定会在它的衍生类的构建器中被调用,而且逐渐向上链接,使每个基础类使用的构建器都能得到调用。之所以要这样做,是由于构建器负有一项特殊任务:检查对象是否得到了正确的构建。由于一个衍生类只能访问它自己的成员,不能访问基础类的成员(这些成员通常都具有private属性),所以只有基础类的构建器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。所以,必须令所有构建器都得到调用。

构建器的调用遵照下面的顺序:
前提:在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。
(1) 调用基础类构建器。这个步骤会不断重复下去,首先得到构建的是分级结构的根部,然后是下一个衍生类,等等。直到抵达最深一层的衍生类。
(2) 按声明顺序调用成员初始化模块。
(3) 调用衍生构建器的主体。

设计构建器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构建器内唯一能够安全调用的是在基础类中具有final属性的那些方法(也适用于private方法,它们自动具有final属性)。这些方法不能被覆盖,所以不会出现潜在的调用继承类方法的问题。

消除

若认为自己的句柄可以被消除,可以将其设为null,使垃圾收集器能够正常地清除它们。

如果需要自己写finalize,那么在覆盖衍生类的finalize()时,务必记住调用finalize()的基础类版本。

收尾的顺序与初始化顺序正好相反。我们首先执行衍生类的收尾,再是基础类的收尾。这是由于衍生类的收尾可能调用基础类中相同的方法,要求基础类组件仍然处于活动状态。


第8章 对象的容纳

数组

在Java中,无论使用的是数组还是集合,都会进行范围检查--若超过边界,就会获得一个RuntimeException(运行期违例)错误。

创建一个数组时,可令其容纳一种特定的类型。这意味着可进行编译期类型检查,预防自己设置了错误的类型,或者错误指定了准备提取的类型。

在Java 1.1中,可动态创建想作为参数传递的数组,如 hide(new Weeble[] {new Weeble(), new Weeble() });

假如容纳的是基本数据类型,必须采用数组。

集合

四种类型的"集合类":Vector(矢量)、BitSet(位集)、Stack(堆栈)以及Hashtable(散列表)。

所有集合类都能自动改变自身的大小。

(1) 将一个对象句柄置入集合时,类型信息会被抛弃,所以任何类型的对象都可进入集合--即便特别指示它只能容纳特定类型的对象。举个例子来说,虽然指示它只能容纳猫,但事实上任何人都可以把一条狗扔进来。
(2) 由于类型信息不复存在,所以集合能肯定的唯一事情就是自己容纳的是指向一个对象的句柄。正式使用它之前,必须对其进行造型,使其具有正确的类型。

Vector

Vector使用是非常简单:先创建一个,Vector cats = new Vector(); 再用addElement()置入对象,以后用elementAt()取得那些对象(注意Vector有一个size()方法)。v.elements()返回它的枚举。addElement()和elementAt()都具有final属性。

参数化类型:在编译期间拥有特定的类型信息,在Java仍不支持。

Enumeration:
(1) 用一个名为elements()的方法要求集合为我们提供一个Enumeration。我们首次调用它的nextElement()时,这个Enumeration会返回序列中的第一个元素。
(2) 用nextElement()获得下一个对象。
(3) 用hasMoreElements()检查序列中是否还有更多的对象。

BitSet

BitSet的最小长度是一个长整数(Long)的长度:64位。

BitSet bb = new BitSet(); bb.set(i); bb.clear(i); bb.size(); bb.get(j)

Stack

Stack实现一个LIFO(后入先出)序列。

Stack stk = new Stack(); stk.push("entry"); stk.addElement("The last line"); stk.empty();stk.elementAt(5)); stk.pop();

Hashtable

Hashtable是一种"关联数组",允许我们将任何对象关联起来。

size();isEmpty();put(Object key, Object value);get(Object key);remove(Object Key)。还可以使用枚举技术:keys()产生对键的一个枚举(Enumeration);elements()产生对所有值的一个枚举。

所有对象都有一个散列码,通过根类Object方法hashCode()获得,然后用它快速查找键。性能大幅提升。

Hashtable ht = new Hashtable(); ht.containsKey(r));ht.get(r));ht.put(r, new Counter());

Hashtable toString()方法能遍历所有键-值对,并为每一对都调用toString()。

因为不可以将任何基本数据类型置入集合里,所以需要封装类。比如Integer。然而,对Java封装类能做的唯一事情就是将其初始化成一个特定的值,然后读取那个值。也就是说,一旦封装类对象创建,就没有办法改变它。这使得我们有时不得不创建一个新类,用它来填充Hashtable。

创建Hash Key类

为了在散列表中将自己的类作为键使用,必须同时覆盖hashCode()和equals()。

public boolean equals(Object o) {
return (o instanceof Groundhog2)
&& (ghNumber == ((Groundhog2)o).ghNumber);
}

系统 Hashtable类型 Properties(属性)
Properties p = System.getProperties();
p.list(System.out);
list()可将内容发给我们选择的任何流式输出。还有一个save()方法可将属性列表写入一个文件,以便日后用load()方法读取。

构建自己的排序算法

interface Compare {
boolean lessThan(Object lhs, Object rhs);
boolean lessThanOrEqual(Object lhs, Object rhs);
}

public class SortVector extends Vector {
private Compare compare; // To hold the callback
public SortVector(Compare comp) {
compare = comp;
}

private void quickSort(int left, int right) {
if(right > left) {
Object o1 = elementAt(right);
int i = left - 1;
int j = right;
while(true) {
while(compare.lessThan(
elementAt(++i), o1))
;
while(j > 0)
if(compare.lessThanOrEqual(
elementAt(--j), o1))
break; // out of while
if(i >= j) break;
swap(i, j);
}
swap(i , right);
quickSort(left, i-1);
quickSort(i+1, right);
}
}

JGL

ObjectSpace公司设计了Java版本的"通用集合库"(从前叫作"Java通用库",即JGL;JGL实现了许多功能,可满足对一个集合库的大多数常规需求,它与C++的模板机制非常相似。JGL包括相互链接起来的列表、设置、队列、映射、堆栈、序列以及反复器,它们的功能比Enumeration(枚举)强多了。同时提供了一套完整的算法,如检索和排序等。在某些方面,ObjectSpace的设计也显得比Sun的库设计方案"智能"一些。举个例子来说,JGL集合中的方法不会进入final状态,所以很容易继承和改写那些方法。JGL已包括到一些厂商发行的Java套件中,而且ObjectSpace公司自己也允许所有用户免费使用JGL,包括商业性的使用。详细情况和软件下载可访问http://www.ObjectSpace.com。与JGL配套提供的联机文档做得非常好,可作为自己的一个绝佳起点使用。

新的Java 1.2集合库

有些名字进行了修改,更接近于通俗:用"反复器"(Inerator)代替了"枚"(Enumeration)。

新的集合库将其分割成两个明确的概念:
(1) 集合(Collection):一组单独的元素,通常应用了某种规则。List(列表)必须按特定的顺序容纳元素,而Set(集)不可包含任何重复的元素。
(2) 映射(Map):一系列"键-值"对(这已在散列表身上得到了充分的体现)。可以方便地查看Map的某个部分:只需创建一个集合,然后用它表示那一部分即可。这样一来,Map就可以返回自己键的一个Set、一个包含自己值的List或者包含自己"键-值"对的一个List。和数组相似,Map可方便扩充到多个"维",毋需涉及任何新概念。只需简单地在一个Map里包含其他Map(后者又可以包含更多的Map,以此类推)。

Collection c = new ArrayList(); c.add(Integer.toString(i));

Iterator it = c.iterator(); it.hasNext(); it.next(); it.remove() - 新方法,可删除由Iterator生成的上一个元素。所以每次调用next()的时候,只需调用remove()一次。

List, ArrayList, LinkedList, ListIterator

ArrayList(以及Vector)是一个数组;LinkedList是常规的双重链接列表,每个对象都包含了数据以及指向列表内前后元素的句柄。假如想在一个列表中部进行大量插入和删除操作,那么LinkedList无疑是最恰当的选择(LinkedList还有一些额外的功能,建立于AbstractSequentialList中)。若进行随机访问(即get())以及循环反复,就情愿选择ArrayList,它的速度要快一些。我们最好的做法也许是先选择一个ArrayList作为自己的默认起点。以后若发现由于大量的插入和删除造成了性能的降低,再考虑换成LinkedList不迟。

Set, HashSet, TreeSet

作为另一个例子,Set既可作为一个ArraySet实现,亦可作为HashSet实现。ArraySet是由一个ArrayList后推得到的,设计成只支持少量元素,特别适合要求创建和删除大量Set对象的场合使用。然而,一旦需要在自己的Set中容纳大量元素,ArraySet的性能就会大打折扣。写一个需要Set的程序时,应默认选择HashSet。而且只有在某些特殊情况下(对性能的提升有迫切的需求),才应切换到ArraySet。 进行add()以及contains()操作时,HashSet显然要比ArraySet出色得多,而且性能明显与元素的多寡关系不大。一般编写程序的时候,几乎永远用不着使用ArraySet。

Map, HashMap, TreeMap

当我们使用Map时,首要的选择应该是HashMap。只有在极少数情况下才需要考虑其他方法。

使用TreeMap通常并不作为Map使用,而是作为创建顺序列表的一种途径。树的本质在于它总是顺序排列的,不必特别进行排序。一旦填充了一个TreeMap,就可以调用keySet()来获得键的一个Set"景象"。然后用toArray()产生包含了那些键的一个数组。随后,可用static方法Array.binarySearch()快速查找排好序的数组中的内容。

TreeMap的创建速度比其他两种类型明显快得多,所以如果需要创建大量Map,而且只有在以后才需要涉及大量检索操作,那么最佳的策略就是:创建和填充TreeMap;以后检索量增大的时候,再将重要的TreeMap转换成HashMap--使用HashMap(Map)构建器。同样地,只有在事实证明确实存在性能瓶颈后,才应关心这些方面的问题--先用起来,再根据需要加快速度。

排序

Random.nextBytes()用随机选择的字节填充数组自变量(没有对应的Random方法用于创建其他基本数据类型的数组)。获得一个数组后,便可执行sort()或者binarySearch()。一个重要的警告是:若在执行binarySearch()之前不调用sort(),便会发生不可预测的行为,其中甚至包括无限循环。

对String的排序以及搜索是相似的,但在运行程序的时候,我们会注意到一个有趣的现象:排序遵守的是字典顺序,亦即大写字母在字符集中位于小写字母的前面。因此,所有大写字母都位于列表的最前面,后面再跟上小写字母--Z居然位于a的前面。似乎连电话簿也是这样排序的。

针对Object数组,可在binarySearch()和sort()中加入另一个参数:实现Comparator接口(新集合库的一部分)的一个对象,并用它的单个compare()方法进行比较。注意:若用自己的Comparator来进行一次sort(),那么在使用binarySearch()时必须使用那个相同的Comparator。

Arrays类提供了另一种sort()方法,不需加入参数,它要求Object实现Comparable接口及接口的compareTo()方法。

排序和搜索列表(List)与排序搜索数组形式相同,只是用于排序和搜索列表的静态方法包含在类Collections中。

只读版本

通常,创建Collection或Map的一个"只读"版本显得更有利一些。Collections类允许我们达到这个目标,方法是将原始容器传递进入一个方法,并令其传回一个只读版本。这个方法共有四种变化形式,分别用于Collection(如果不想把集合当作一种更特殊的类型对待)List、Set以及Map。e.g. c = Collections.unmodifiableCollection(c);

同步版本

为了支持"多线程",Collections类提供了对整个容器进行自动同步的一种途径。Map m = Collections.synchronizedMap(new HashMap()); 这种直接传递新容器的方法可避免不慎暴露出未同步的版本。同步提供了防止多个进程同时修改一个容器内容的机制。它集成了一套解决机制,若探测到有其他方面也准备修改容器,便会立即产生一个ConcurrentModificationException(并发修改违例)。这一机制被称为"立即失败"--它并不用更复杂的算法在"以后"侦测问题,而是"立即"产生违例。

System.currentTimeMillis();

Technorati :

标签:

1 Comments:

  • Think in Java 笔记 - 7/8章

    nice..

    By Anonymous 匿名, at 9:53 上午  

发表评论

<< Home