===========

 

Chap1 对象简介

 

       1. 抽象的过程

       Alan Kay 总结了 Smalltalk 的五项基本特征。这些特征代表了纯的面向对象的编程方法:

       (1). 万物皆对象。将对象想成一种特殊的变量;它存储数据,而且还可以让你“提要求”,命令它进行某些操作。从理论上讲,你可以把所有待解决的问题中的概念性组件(狗,建筑,服务等)都标识成程序里的对象。

       (2). 程序就是一组相互之间传递消息的对象。你只要向那个对象“发一个消息”,就能向它提出要求。更确切的说,你可以这样认为,消息是调用专属某个对象的方法的请求。

       (3). 每个对象都利用别的对象来组建它自己的记忆。换言之,你通过将已有的对象打成一个包,来创建新的对象。由此,你可以将程序的复杂性,隐藏在对象的简单性之下。

       (4). 对象都有类型。任何对象都是某个类的实例 (instance of a class) 。用以区分类的最突出的特点就是“你能传给它什么消息?”

       (5). 所有属于同一类型的对象能接受相同的消息。这种互换性 (substitutability) OOP 最强大的功能之一。

       Booch 还给对象下了个更为简洁的定义:

       对象有状态,行为和标识。

       这就是说,对象可以有内部数据(状态),有方法(因而产生了行为),以及每个对象都能同其它对象区分开来--具体而言,每个对象在内存里都有唯一的地址。

       这句话或许有点太过了。因为对象还能存在于另一台及其上以及不同的内存空间中,此外还能保存在硬盘上。在这种情况下,对象的身份就不能用内存地址,而必须要用别的方法来确定。

      

       2. 可凭借多态性相互替换的对象

       OOP 的编译器的做法称为前绑定 (early binding) 。编译器会产生那个名字的函数的调用,而连接器负责将这个调用解析成须执行的代码的绝对地址。在 OOP 中,不到运行的时候,程序没法确定代码的地址,所以向泛型对象发送一个消息的时候,就要用到一些特别的手段。

       OOP 语言用了后绑定 (late binding) 的概念。当你向某个对象送了一个消息后,不到运行时,系统不能确定到底该调用哪段代码。编译器只保证这个方法存在,并且检查参数和返回值的类型(不这么做的语言属于弱类型 weakly typed ),但是它并不知道具体执行的是哪段代码。

       在有些语言中,你必须明确申明,某个方法要用到后绑定的灵活性( C++ virtual 关键字)。在这些语言中,方法不是默认地动态绑定的。而动态绑定是 Java 的缺省行为,因此无需添加什么额外的关键词就能获得多态性。

       将派生类当作它的基类来用的过程称为上传( upcast ),反之称为下传( downcast )。下传所需的运行时检查会引起程序运行效率的降低,也加重了编程的负担。解决方案就是参数化类型 (parameterized type) 机制,即泛型。

      

       3.Collection 和迭代器

       ArrayList LinkedList ,都是简单的线性序列,具有相同的接口和外部行为。对于 ArrayList ,随机访问是一种时间恒定的操作。然而对于 LinkedList ,随机访问和选取元素的代价会很大。另一方面,如果要在序列中插入元素, LinkedList 的效率会比 ArrayList 的高出许多。

 

 

 

=============

Chap2 万物皆对象

 

       1. 数据存在哪里

       数据可以存储在以下六个地方:

       (1). 寄存器 (registers) 。这是反应最快的存储,因为它处在 CPU 里。但寄存器数量有限,由编译器分配,你不能直接控制。

       (2). (stack) 。位于常规内存区里, CPU 可以通过栈指针对它进行直接访问。栈指针下移就创建新的存储空间,上移就释放内存空间。这是仅次于寄存器的最快、最有效率的分配内存的方法。由于 Java 编译器必须生成能控制栈指针上下移的代码,所以程序编译的时候,那些将被存储在栈中的数据的大小和生命周期必须是已知的。 Java 把对象的 reference 存放在栈里。

       (3). (heap) 。这是一段多用途的内存池,所有 Java 对象都保存在这里。在堆中分配空间时,编译器无需知道该分配多少空间,或数据会在堆里待多长时间。但是其速度比分配栈的慢些。

       (4). 静态存储 (static storage) 。这里“静态”的意思是“在固定的位置” ( 尽管还是在 RAM 里面 ) 。静态存储里面的数据在整个程序运行期间都能访问到。可以用 static 关键词指明对象的某个元素是静态的,但是 Java 对象本身是决不会放到静态存储中去的。

       (5). 固定存储 (constant storage) 。常量值通常直接放在程序里。有时常量还能为自己设置界限,这样在嵌入式系统中,就能选择是不是把它们放到 ROM 里面去。

       (6). 非内存的存储 (Non-RAM storage) 。如果数据完全独立于程序,那么即使程序不运行,它也应该还在。对象被转化成某种能保存在其它介质上的东西,要用的时候,又能在内存里重建。 Java 提供了轻量级 persistence 的支持。

      

       特例: primitive 类型

       primitive( 原始 ) 类型的变量直接保存值,并且存储在栈中。

      

       高精度的数值

       Java 还包括两个能进行高精度算术运算的类: BigInteger BigDecimal

      

       作用域

       int x = 12;

       {

       int x = 100;//illegal

       }

      

       2. 创建新的数据类型:类

       只有在“变量被用作类的成员”时, Java 才能确保它获得默认值。本地变量,没有这种保障。

       不管在哪种情况下, Java 在传递对象的时候,实际上是在传递 reference

      

      

      

============

Chap3 控制程序流程

 

       1. 运算符

       逗号运算符

       Java 里面,唯一一个把逗号当运算符用的地方是 for 循环。

      

       String + 运算符

       加号 (+) 用在 String 上的时候,如果表达式中有 String ,那么 Java 编译器会把其他的操作数都转换成 String

 

       Java 没有 sizeof

       C C++ sizeof() 用于获取数据要占用多少字节的内存,需要 sizeof 的主要原因是为了移植。相同的数据类型在不同的机器上占用的内存长度可能会不一样。

       Java 没有移植的问题,因此不需要 sizeof ,所有数据类型在所有的机器上都是相同的。

 

       运算符的总结

       在进行数学运算或混和赋值的时候, char byte short ,都会先进行提升,运算结果也是 int 。如果要把结果赋给原先那个变量,就必须明确地进行类型转换。

       除了 boolean 之外,所有的 primitive 类型都能被转换成其它的 primitive 类型。

      

       2. 执行控制

       Java 不允许把数字当作 boolean 用,尽管 C C++ 允许这么做(非零值表示 true ,零表示 false )。

      

       Java 里,唯一能放标签的地方,就是在循环语句的外面。而且必须直接放--在循环语句和标签之间不能有任何东西。而这么做的唯一理由就是,你会嵌套多层循环或选择。因为通常情况下 break continue 关键词只会中断当前循环,而用了标签后,就会退到 label 所在的地方。

       label1:

       outer-iteration{

              inner-iteration{

                     break;// 中断内循环,退到外循环

                     continue;// 中断本次内循环,重新移到内循环开始处,执行下次内循环

                     continue label1;// 中断本次外循环,移到外循环开始处,重新执行下次外循环

                     break label1;// 退出外循环,执行循环以后的语句

              }

       }

       如果退出循环或选择的同时,还要退出方法,可以直接使用 return

       continue break 以及 label 的规则:

       (1). 普通的 continue 会退到内部循环的最开始,然后继续执行内部循环。

       (2). 带标签的 continue 会跳转到标签,并且重新进入直接跟在标签后面的循环。

       (3).break 会从循环的“底部溜出去”。

       (4). 带标签的 break 会从由这个标签标识的循环的“底部溜出去”。

       Java 里能使用标签的唯一理由就是,在嵌套循环的同时要用 break continue 退出多层循环。

      

       3.switch

       switch 会根据整数表达式的值(可以是 char )决定应该运行哪些代码。

       找到匹配的值后,就会执行相应的 case 语句,不会再进行比较。通常 case 语句应该以 break 结束。否则会直接执行下一个 case 语句,而不会再次进行匹配。如果没有匹配的 case ,则执行 default 语句。

      

       计算细节

       float double 转换成整数的时候,它总是将后面的小数截去。

       Math.random() 会生成一个 double ,值域是 [0,1)

 

 

 

 

============

Chap4 初始化与清理

 

       1. 用构造函数确保初始化

       构造函数的名字必须与类的名字大小写完全相同。构造函数本身没有返回值,虽然 new 表达式会返回新创建对象的 reference

      

       2. 默认的构造函数

       如果你写了一个没有构造函数的类,编译器会自动创建一个默认的构造函数(不带参数)。但是,只要你定义了构造函数,不管带不带参数,编译器就不会再自动合成默认构造函数了。

      

       3.this 关键字与构造函数

       在类的构造函数中,可以用 this(arg) 调用另一个构造函数,但是不能调用两个构造函数(即 this() 在构造函数中只能出现 0 1 次)。此外,必须在程序代码的最前面调用构造函数。在非构造函数的方法里,不能用 this() 调用同一个类的构造函数。

      

       4.static 的含义

       类的 static 方法只能访问其他的 static 方法和 static 数据成员,不能在 static 方法调用非 static 方法,但是反过来是可以的。

       但是,如果可以传一个对象的 reference static 方法,就可以通过这个 reference 调用非 static 方法和非 static 数据成员。但要达到这个目的,通常应该使用非 static 的方法。

      

       5. 清理: finalize 和垃圾回收

       java 提供 finalize() 方法,垃圾回收器准备释放内存的时候,会先调用 finalize()

       (1). 对象不一定会被回收。

       (2). 垃圾回收不是拆构函数。

       (3). 垃圾回收只与内存有关。

       (4). 垃圾回收和 finalize() 都是靠不住的,只要 JVM 还没有快到耗尽内存的地步,它是不会浪费时间进行垃圾回收的。

      

       6. 成员的初始化

       方法中的局部变量,使用前必须进行初始化,否则编译器会给出错误消息。

       类的成员数据会被自动初始化,对于 primitive 变量,会被清零( char 型也为 0 );对于对象 reference ,会被赋值为 null

      

       7. 指定初始化

       对类成员数据进行初始化时,可以调用方法获取初始值,该方法如果有参数,参数不能是类的其他尚未初始化的数据成员。可以这样:

       class Test{

              int i=f();

              int j=g(i);

       }

       不能这样:

       class Test{

              int j=g(i);//i 尚未初始化

              int i=f();

       }

      

       8. 初始化的顺序

       对类而言,初始化的顺序是由变量在类里定义的顺序决定的,变量的初始化会优先于任何方法,包括构造函数。

       创建对象的过程(小结 , Dog 类为例)

       (1). 第一次创建 Dog 类的对象(构造函数实际上是 static 方法),或者第一次访问 Dog 类的 static 的方法或字段的时候, Java 解释器会搜寻 classpath, 找到 Dog.class.

       (2). 装载了 Dog.class 之后,(创建了 Class 对象之后),会对所有的 static 数据进行初始化。这样第一个装载 Class 对象的时候,会先进行 static 成员的初始化。

       (3). new Dog() 创建新对象的时候, Dog 对象的构建进程会先在堆 (heap) 里为对象分配足够的内存。

       (4). 这块内存先被清零,自动把 Dog 对象的 primitive 类型的成员赋缺省值(对于数字是零,或是相应的 boolean char ),将 reference 设成 null

       (5). 执行定义成员数据时所作的初始化。

       (6). 执行构造函数。可能牵涉到继承关系的很多活动。

      

       9. 非静态的实例初始化

       实例初始化( instance initialization )语句,除了没有 static 关键字,其他与静态初始化没有两样。这种语法为匿名内部类 (anonymous inner class) 的初始化提供了必不可少的支持。

      

       10. 数组的初始化

       Java 可以将一个数组赋给另外一个,实际上是复制 reference.

       可以用花括号括起来的对象列表初始化对象数组。有两种形式:

       Integer[] a = {

              new Integer(1),

              new Integer(2),

       };

       Integer[] b = new Integer[]{

              new Integer(1),

              new Integer(2),

       };

       列表的最后一个逗号是可选的,这个特性能使长列表的维护工作变得简单一些。    

      

      

      

===============    

Chap5 隐藏实现

 

       1.package: 类库的单元

       Java 的源代码文件通常被称为编译单元 (compilation unit) ,必须是一个以 .java 结尾的文件,其中必须有一个与文件名相同的 public 类(大小写也必须相同,但不包括 .java 扩展名)。每个编译单元只能有一个 public 类。

       编译 .java 文件的时候,里面的每个类都会产生输出,文件名是类名,扩展名是 .class

       创建独一无二的 package 名字

       package 语句必须是文件里的第一个非注释行。

       如果使用 JAR 文件打包 package ,必须把文件名放到 CLASSPATH 里面。

      

       冲突

       如果两个 import 语句所引入的类库都包含一个同名的类,只要不写会引起冲突的代码(不使用这个同名的类),就一切 OK ,否则编译器会报错。

       不论哪种对象,只要放进了 String 的表达式,就会被强制转化成该对象的 String 表示形式。如:

       System.out.print(""+100);//Force it to be a String

      

       2.Java 的访问控制符

       Java 的一个访问控制符只管它所定义的这一项,而 C++ 的访问控制符会一直管下去,直到出现另一个。

       private: 除非是用成员所在类的方法,否则一律不得访问。

       package 访问权限( package access ,有时也称为“ friendly ”),是默认的访问权限,没有关键词。同属一个 package 的类都能访问这个成员,但是对于不属于这个 package 的类来说,这个成员就是 private 的。

       protected: 继承的访问权限。 protected 也赋予成员 package 权限--同一个 package 里的其它类也可以访问 protected 元素。除此之外,继承类 ( 不管是否在同一个 package) 也可以访问 protected 成员。

       public: 访问接口的权限,任何类都能访问。

      

       3. 类的访问权限

       类只有两种访问控制权, public package 。( inner class 可以是 private protected

       每个编译单元(文件)只能有一个 public 类,也可以没有 public 类(不常见)。 public 类的名字必须和编译单元文件名大小写完全相同。如果没有 public 类,就可以随意给编译单元文件起名。

       对于 package 权限的类,通常应该将方法也设成 package 权限。

       如果不写类的访问控制符,默认是 package 权限的。 package 中的任何一个类都能创建这个类的对象,但是 package 以外的类就不行了。但是,如果这个类有一个 public static 成员,那么即使客户程序员不能创建这个类的对象,他们也还可以访问这个 static 成员。

      

      

      

==============       

Chap6 复用类

 

       1. 合成所使用的语法

       对象的 reference 作为类的成员时,会被自动初始化为 null 。此外可以在三个时间对类的 reference 成员进行初始化:

       (1) 在定义对象 reference 的时候。这就意味着,在类的构造函数调用前,已经初始化完毕了。

       private String a=new String("hello"), b="world";

       (2) 在类的构造函数。

       (3) 在即将使用对象 reference 之前。这被称为 lazy delayed initialization ,如果创建对象的代价很大,或不是每次都需要创建对象,这种做法就能降低程序开销。

      

       2. 基类的初始化

       当你创建一个派生类对象的时候,这个对象里面还有一个基类的子对象( subobject )。

       对于默认构造函数,即无参构造函数, Java 会让派生类的构造函数自动地调用基类的构造函数。基类会在派生类的构造函数调用它之前进行初始化。

       对于带参数的构造函数,必须用 super 关键字以及合适的参数明确地调用基类构造函数, super() 语句必须是派生类构造函数的第一条语句。

      

       3. 确保进行妥善的清理

       不要依赖垃圾回收器去做任何与内存回收无关的事情。如果要进行清理,一定要自己写清理方法,别去用 finalize()

       清理的顺序:先按照创建对象的相反顺序进行类的清理,然后调用基类的清理方法。

      

       4. 名字的遮盖(重载方法的覆盖)

       如果 Java 的基类里有一个被重载了好几次的方法,那么在派生类里重新定义那个方法,是不会把基类里定义的任何一个给覆盖掉的。(在 C++ 里,就会把基类方法全都隐藏起来)

      

       5. 用合成还是继承

       合成用于新类要使用旧类的功能,而不是其接口的场合。继承则是要对已有的类做一番改造,以获得一个特殊版本,即将一个较为抽象的类改造成能适用于某些特定需求的类。

       合成要表达的是“有( has-a )”关系,继承要表达的是一种“是( is-a )”关系。

       判断该用合成还是继承时,可以问一下是不是会把新类上传给基类。如果必须上传,那么继承就是必须的。

      

       6.final 关键词

       6.1 final 的数据

       常量能用于两种情况:( 1 )编译时的常量( compile-time constant ),这样就再也不能改了;( 2 )运行时初始化的值,这个值你以后不想改了。

       如果是编译时常量,编译器会把常量放到表达式中,可以降低运行时的开销。 Java 中这种常量必须是 primitive 的,要用 final 表示,这种常量的赋值必须在定义的时候进行。

       final 修饰对象的 reference 时,表示 reference 是常量,初始化的时候,一旦将 reference 指向了某个对象,那么它就再也不能指向别的对象了。但是这个对象本身是可以修改的。

       final int v1 = 10;//compile-time constants

       final int v2 = rand.nextInt(100);//

       final Value v3 = new Value();//v3 的数据成员是可变的

       final int[] arr={1,2,3};//arr 的元素是可变的,如 a[i]++;

       空白的 final 数据( Blank finals ),是指声明了 final 成员,却没有在声明时赋值。编译器会强制在构造函数中初始化 final 数据。通过使用带参数的构造函数,根据参数对空白的 final 数据进行初始化,可以在保持 final 数据不变性的同时,提供一定的灵活性。

       可以把方法的参数声明为 final 的。

       6.2 final 方法

       使用 final 方法有两个目的。第一,可以禁止派生类修改方法。第二,效率。对于 final 方法,编译器会把调用转换成“内联 (inline) ”,即用方法本身的拷贝来代替方法的调用。但是,如果方法很大,程序会很快膨胀,于是内联也不会带来什么性能的改善。

       只有是基类接口里的东西才能被覆写。如果基类的方法是 private 的,那它就不属于基类的接口。即使在派生类里创建了一个同名的方法,它同基类中可能同名的 private 方法没有任何联系。

       6.3 final

       把类定义成 final 的,可以禁止继承这个类。 final 类的数据可以是 final 的,也可以不是。 final 类的方法都隐含地变成 final 了。

      

       7. 初始化与类的装载

       在传统的编程语言中,程序启动的时候都是一次装载所有的东西,然后进行初始化,接下来再开始执行。这些语言必须仔细控制初始化的顺序。

       Java 采用了一种新的装载模式。编译之后每个类都保存在它自己的文件里。不到需要的时候,这个文件是不会装载的。即“类的代码会在它们第一次使用的时候装载”,第一次访问 static 成员或创建对象的时候。

       继承情况下的初始化=====

       首次使用类的时候,装载器 (loader) 就会寻找类的 .class 文件,转载过程中,会依次追溯装载基类(不管是否创建基类对象,这个过程都会发生)。下一步,会执行“根基类 (root base class) ”的 static 初始化,然后是下一个派生类的 static 初始化,以此类推。

       所有类都装载结束,就可以创建对象了。首先对象里所有成员数据会被初始化为缺省值,这个过程是一瞬间完成的,对象的内存会被设置成二进制 0 。然后开始构造基类,基类的构造过程及顺序与派生类相同,先对基类的变量按字面顺序进行初始化,再调用基类的构造函数(调用是自动发生的,但你可以用 super 关键字指定要调用基类的哪个构造函数)。之后会对派生类的变量按定义的顺序进行初始化,最后执行派生类构造函数其余代码。

      

      

=============

Chap7 多态性

 

       1. 方法调用的绑定

       将方法的调用连到方法本身被称为“绑定( binding )”。当绑定发生在程序运行之前时(由编译器或连接器负责),被称作“前绑定( early binding )”。

       “后绑定( late binding )”指在程序运行时,根据对象类型决定该绑定哪个方法。后绑定也被称为“动态绑定 (dynamic binding) ”或“运行时绑定( run-time binding )”。

       除了 static final 方法( private 方法隐含有 final 的意思), Java 的所有方法都采用后绑定。

      

       2. 错误:“覆写” private 的方法

       public class Parent{

              private void f(){

              //...

              }

              public static void main(String[] args){

                     Parent pa = new Child();

                     pa.f();// 执行的是 Parent.f()

              }

       }

       class Child extends Parent{

              public void f(){

              //...

              }

       }

       应该避免用基类的 private 的方法名去命名派生类中方法。

       只有非 private 的方法才能被覆写。

      

       3. 继承与清理

       清理的顺序应该与初始化的顺序相反。对数据成员而言,清理顺序就应该与声明的顺序相反。

       在派生类的清理方法(比如 dispose() )的最后,应该调用基类的清理方法。

      

       4. 多态方法在构造函数中的行为

       如果在构造函数里调用了动态绑定的方法,那么它会调用那个覆写后的版本。

       abstract class Glyph{

              abstract void draw();

              Glyph(){

                     System.out.println("Glyph() before draw()");

                     draw();

                     System.out.println("Glyph() after draw()");

              }

       }

       class RoundGlyph extends Glyph{

              private int radius = 1;

              RoundGlyph(int r){

                     radius = r;

                     System.out.println("RoundGlyph.RoundGlyph(),radius = " + radius);

              }

              void draw(){

                     System.out.println("RoundGlyph.draw(),radius = " + radius);

              }

       }

       public class PolyConstructors{

              public static void main(String[] args){

                     new RoundGlyph(5);

              }

       }

       // 输出========

       //Glyph() before draw()

       //RoundGlyph.draw(), radius = 0

       //Glyph() after draw()

       //RoundGlyph.RoundGlyph(), radius = 5

       上面程序中, Glyph 的构造函数调用了 draw() ,而这个调用最后落到了 RoundGlyph.draw() 。而此时 radius 的值还没有被初始化为 1 ,还是 0

      

       真正的对象初始化过程:

       (1) 类加载结束,并且 static 初始化结束后,在进行其它工作之前,分配给这个对象的内存会被初始化为二进制的 0

       (2) 构造基类,调用基类的构造函数。这时会调用被覆写的 draw() 方法(在调用 RoundGlyph 的构造函数之前),受第一步的影响, radius 的值还是 0

       (3) 数据成员按照声明的顺序进行初始化。

       (4) 调用派生类的构造函数的正文。

      

       5. 用继承来进行设计

       一般准则:使用继承来表示不同的行为,使用成员数据(合成)来表示不同的状态。

      

       6. 总结

       人们通常会把多态性同 Java 的那些非面向对象的特性相混淆,比如方法的重载,它常常会被当作面向对象的特性介绍给大家。千万别上当:不是后绑定的,就不是多态性。

      

      

      

===========

Chap8 接口与内部类

 

       1. 接口( interface

       interface 也可以包含数据成员,但是它天生就是 public static final 的。

       interface 默认是 package 权限的,只能用于同一个 package 。也可以加上 public (只有保存在同名文件里的 interface 才可以加)。

       可以把 interface 里的方法声明成 public 的,但是即便不写 public 关键词,这些方法也自动是 public 的。当 implements 一个 interface 的时候,必须把这个 interface 的方法定义成 public 的。

      

       2.Java 的“多重继承”

       由于 interface 不带任何“实现”--也就是说 interface 和内存无关--因此不会有谁去阻挠 interface 之间的结合。

       到底是用 interface ,还是用 abstract 类?只要基类的设计里面可以不包括方法和成员变量的定义,就应该优先使用 interface 。只有在不得不定义方法或成员变量的情况下,才把它改成 abstract 类。

      

       3. 合并接口时的名字冲突

       在要合并的接口里面放上同名的方法,通常会破坏程序的可读性,应该避免。

      

       4. 用继承扩展 interface

       可以用继承往 interface 里添加新的方法,也可以把多个 interface 合并成一个新的 interface 。使用 extends 关键词,多个“基接口 (base interface) ”之间用逗号分割。

       interface Vampire extends IMonster, IUndead{}

      

       5. 常量的分组

       由于 interface 的数据成员自动就是 public static final 的,因此 interface 是一种非常方便的创建一组常量值的工具。这点同 C C++ enum 很相似,但没有 enum 那样的类型安全。

      

       6. 接口的嵌套

       接口既可以嵌套在类里,也可以嵌套在接口里面。

       实现接口的时候,不一定要实现嵌套在里面的接口。同样, private 接口只能在定义它的类里实现。

      

       7. 内部类 (inner class)

       内部类与合成是截然不同的。

       除非是在“宿主类 (outer class) ”的非 static 方法里面,否则无论在哪里创建内部类的对象,都必须用 OuterClassname.InnerClassName 的形式来表示这个对象的类型。

      

       8. 内部类与上传

       内部类可以被定义成 private protected ,非内部类只可能是 public package 权限的。

      

       9. 在方法和作用域里的内部类

       在方法的某个作用域里定义的内部类,比如 if 语句里,并不意味着这个类的创建是有条件的--它会同别的东西一起编译。但是,这个类的访问范围仅限于定义它的那个作用域。除此之外,它同普通的类没有区别。

      

       10. 匿名内部类

       public class Parcel6{

              public Contents cont(){

                     return new Contents(){

                            private int i = 11;

                            public value(){return i;}

                     };//Semicolon required

              }

       }

       上例中 return new Contents(){...} 语句表达的是:创建一个继承 Contents 的匿名类的对象。 new 语句所返回的 reference 对自动上传到 Contents 。这个 return 语句是如下代码的简化形式:

       class MyContents implements Contents{

              private int i = 11;

              public int value(){return i;}

       }

       return new MyContents();

       这个匿名内部类是通过默认构造函数来创建 Contents 的,如果基类需要的是一个带参数的构造函数,可以直接将参数传给基类的构造函数, return new Contents(x){...}

       如果在定义匿名内部类的时候,要用到外面的对象,编译器会要求把这个参数的 reference 声明成 final 的。

       public class Parcel8{

              //Argument must be final to use inside anonymous inner class

              public Destination dest(final String ds){

                     return new Destination(){

                            private String lbl = ds;

                            public String readLabel(){return lbl;}

                     };

              }

       }

       不能在匿名内部类里创建构造函数(因为它根本就没有名字),但是可以通过“实例初始化 (instance initialization) ”(与 static 初始化对应),进行一些类似构造函数的操作。实际上实例初始化过程就是匿名内部类的构造函数。但它的功能是有限的,由于不能重载实例初始化,因此只能有一个构造函数。

       public Base getBase(int i){

              return new Base(i){

                     {

                            System.out.println("Inside instance initializer.");

                     }

                     public void f(){

                            System.out.println("In anonymous f().");    

                     }

              };

       }

      

       11. 与宿主类的关系

       内部类能访问宿主类的所有成员。内部类对象里存在一个隐蔽的指向宿主类对象的 reference ,由编译器处理。

      

       12. 嵌套类 ( 静态内部类 )

       如果不需要这种“内部类对象和宿主类对象之间的”联系,可以把内部类定义成 static 的,通常被称作“嵌套类 (nested class) ”。嵌套类的意思是:

       (1) 无需宿主类的对象就能创建嵌套类的对象。

       (2) 不能在嵌套类的对象里面访问非 static 的宿主类对象。

       此外,嵌套类同普通的内部类还有一点不同。普通的内部类的成员数据和方法只能到类的外围这一层,因此普通的内部类里不能有 static 数据, static 数据成员或嵌套类。但是,这些东西在嵌套类里都可以有。

       嵌套类可以是 interface 的一部分。

       每个类可以带一个供测试的 main() 方法,但编译后,会产生额外的代码。可以考虑在嵌套类里创建供测试的 main() 。编译后会产生单独的名称如 Test$NestedTester.class 的文件,发布的时候可以删除这个文件。

      

       13. 引用宿主类的对象

       在内部类里通过宿主类名字后面加句点再加 this 来表示宿主类对象的 reference 。比如类 Sequence.Selctor 里,可以用 Sequence.this 来获取它所保存的宿主类 Sequence 对象的 reference

       要在其他地方创建内部类的对象,就必须在 new 表达式里面给出宿主类对象的 reference 。如

       Parcel11 p = new Parcel11();

       //Must use instance of outer class to create an instance of the inner class.

       Parcel11.Contents c = p.new Contents();

      

       14. 在多层嵌套的类里向外访问

       内部类的嵌套层次不是问题--它可以透明地访问它的各级宿主类的成员。

       class MNA{

              private void f(){}

              class A{

                     private void g(){}

                     public class B{

                            void h(){

                                   g();

                                   f();

                            }

                     }

              }

       }

      

       public class MultiNestingAccess{

              public static void main(String[] args){

                     MNA mna = new MNA();

                     MNA.A mnaa = mna.new A();

                     MNA.A.B mnaab = mnaa.new B();

                     mnaab.h();

              }

       }

       ".new" 语句指明了正确的作用域,因此无需在调用构造函数的语句里再限定类的名字了。

      

       15. 继承内部类

       class WithInner(){

              class inner{}

       }

       public class InheritInner extends WithInner.Inner{

              //!InheritIner(){}//Won't compile

              InheritIner(WithInner wi){

                     wi.super();

              }

              public static void main(String[] args){

                     WithInner wi = new WithInner();

                     InheritInner ii = new InheritInner(wi);

              }    

       }

       InheritInner 继承的只是内部类,默认的构造函数不能通过编译。必须传递宿主类对象的 reference 。此外,必须在构造函数里面使用这种语法: enclosingClassReference.super(); 这样才能提供那个必须的 reference ,才能编译通过。

      

       16. 内部类可以被覆写吗

       像覆写宿主类的方法那样去“覆写”内部类,是不会有实际效果的。但是,可以在继承类的内部类明确继承基类的内部类。

       public class BigEgg2 extends Egg2{

              public class Yolk extends Egg2.Yolk{

              }

       }

      

       17. 本地内部类 (Local inner classes)

       本地内部类是在代码段,通常是方法的正文部分创建的。本地内部类不能有访问控制符,因为它并不属于宿主类,但它可访问当前代码段的 final 变量,以及宿主类的所有成员。

       由于本地内部类的名字在代码段(通常是一个方法)外面是没法访问的,因此选择本地内部类来代替匿名内部类的一个理由就是,需要一个有名字的构造函数,并要重载这个构造函数,因为匿名内部类只能进行实例初始化。另一个理由是,需要创建多个那种类的对象。

      

       18. 内部类的标识符

       宿主类名字,加上 "$" ,再加上内部类的名字。如果是匿名内部类,编译器会直接用数字来表示。

       如: OuterClass$InnerClass.class OuterClass$2.class

      

       19. 为什么要有内部类?

       每个内部类都可以独立的继承某个“实现 (implementation) ”。内部类能在事实上继承多个实体类或 abstract 类。可以把它当作彻底解决多重继承问题的办法,接口部分地解决了这个问题。

      

      

      

      

========

Chap9     用异常来处理错误

 

       1. 重抛异常

       如果直接重抛当前的异常,则 printStackTrace() 所打印出来的那些保存在异常对象里的信息,还会指向异常发生的地方,不会被指到重抛异常的地点。如果要装载新的栈轨迹信息,你可以调用 fillInStackTrace()

       try{

              f();

       } catch(Exception e){

              e.printStackTrace();

              //throw e;//17

              throw e.fillInStackTrace();//18

       }

      

       也可以抛出一个与捕捉到的异常不同的异常。这么做的效果同使用 fillInstackTrace() 的差不多--异常最初在哪里发生的信息被扔了,现在里面保存的时抛出新异常的地点。

      

       2. 异常链

       异常链可以在捕捉到一个异常并且抛出另一个异常的时候,仍然保存前一个异常的信息。

       Throwable 的子类中,只有三种基本的异常类提供了带 cause 参数的构造函数,它们是 Error Exception RuntimeException 。如果要链接其它异常,就不能用构造函数,而只能用 initCause() 方法了。

      

       3. 错误:丢失的异常

       finally 语句出现异常时,会取代前面 try..catch 语句抛出的异常。这种“第一个异常尚未得到处理,就产生了第二个异常”的现象,属于想当严重的错误。

      

       4. 加在异常上面的限制

       覆写方法的时候,只能抛出这个方法在基类中的版本所声明的异常,或其子异常,也可以不抛出异常。

       即使继承类同时要实现 interface (且该 interface 中方法抛出了不同于基类方法异常),也不能扩大派生类方法的异常说明接口。

       这种异常方面的限制对构造函数不起作用。派生类的构造函数只会根据自己需要抛出异常。然而,派生类的构造函数会调用基类构造函数,所以必须在派生类的构造函数的异常说明中声明,基类构造函数所抛出的异常。注意,派生类的构造函数不能捕获任何由基类构造函数抛出的异常。

       在继承和覆写的过程中,方法的异常说明的接口变小了。

       异常说明本身并不属于方法的特征。

      

       5. 其它方法

       异常处理的一个重要准则就是“如果你不知道该如何处理这个异常,你就别去捕捉它”。

       “所有模型都是错的,但有些是能用的。”

      

       6. 观点

       好的编程语言能帮程序员写出好程序。但是无论哪种语言挡不住你去写坏程序。

      

       7. checked exception 转换成 unchecked exception

       调用一个方法,然后发现不知道怎样处理这个异常,但是又不能把它私吞。可以直接把 checked exception 包进 RuntimeException 里面,如下:

       void throwRuntimeException(){

              try{

                     //to do sth

              } catch(IOException e){

                     throw new RuntimeException(e);

              }

       }

       在异常处理程序中捕获这个包装后的异常,进行处理,还可以用 getCause() 获取包装前的原始异常。如下:

       try{

              throwRuntimeException();

       } catch(RuntimeException re){

              try{

                     throw re.getCause();

              } catch(IOException e){

                     //deal with IOException

              }

       }

      

      

=========

Chap10   检测类型

 

       运行时类型识别( run-time type identification ,缩写为 RTTI

      

       1.Class 对象

       类的常规对象是由 Class 对象创建的。

       程序里的每个类都要有一个 Class 对象。这个对象存储在同名的 .class 文件里。程序运行时,需要创建一个这个类的对象时, JVM 会检查是否装载了那个 Class 对象。如果没有, JVM 就会去找到那个 .class 文件,然后装载。一旦那种类型的 Class 对象被装进了内存,所有那个类的对象就会由它来创建了。

       Class 对象同其它对象一样,也可以用 reference 来操控(装载器会处理)。

       静态方法 Class.forName(String) 会返回 Class 对象的 reference ,该方法可能会抛出异常。

      

       2.Class 常数

       Java 还提供了另一种获取 Class 对象的 reference 的方法,即 class 常数: ClassName.class 。这种方法更安全,因为它是在编译时做检查的,它不会像 Class.forName() 那样抛出异常。此外,由于没有方法调用,它的执行效率也更高一些。

       Class 常数可用于普通类,接口,数组和 primitive 类型。此外,每种 primitive wrapper 类还有一个名为 TYPE 的数据成员,能返回“与这种 wrapper 类相关联的 primitive ”的 Class 对象的 reference 。如下:

       boolean.class  Boolean.TYPE

       char.class              Character.TYPE

       byte.class              Byte.TYPE

       short.class             Short.TYPE

       int.class          Integer.TYPE

       long.class              Long.TYPE

       float.class              Float.TYPE

       double.class    Double.TYPE

       void.class              Void.TYPE

      

       3. 转换之前先作检查

       Java RTTI 的形式有:

       (1). 经典的强制类型转换:如“ (Circle)Shape ”。

       (2). 代表对象类型的 Class 对象。

       (3).instanceof 关键词。

       if(o instanceof Dog)

              ((Dog)o).bark();

       instanceof 的限制很严,只能拿它跟类名,而不是 Class 对象作比较。

      

       4. 动态的 instanceof

       ClassObejct.isInstance() 方法还提供了一种动态调用 instanceof 的方法。

       Dog.class.isInstance(o);

       isInstance() 能完全替代 instanceof

       ObjectName.getClass() 也可获取 Class 对象的 reference

      

       5.Reflection :运行时的类信息

       除了 Class 类,还有一个类库, java.lang.reflect 也支持 reflection 。这个类库里有 Field Method Constructor 类(它们都实现了 Member 接口)。

       使用 reflection 与未知类打交道的时候, JVM 会和普通的 RTTI 一样,先检查这个对象是否属于那个具体类型,然后仍然必须先装载 Class 对象,也就是,不管从本地还是从网络, JVM 必须找到那个 .class 文件。

       RTTI reflection 的真正区别在于, RTTI 时在编译时让编译器打开并检查 .class 文件。而对于 reflection ,编译时得不到 .class 文件,它是在运行时打开并检查那个文件。

      

      

      

      

========

Chap11    对象的集合

 

       1. 数组

       数组与其它容器的区别体现在三个方面:效率,类型识别,以及可以持有 primitives

       数组是 Java 提供的,能随机存储和访问 reference 序列的诸多方法中,最高效的一种。速度的代价是,当一个数组创建后,容量就固定了。

       创建数组的时候,同时指明了数组元素的类型。而泛型容器类如 List Set Map 等,所持有的对象均被上传为 Object

      

       2. 数组是第一流的对象

       数组的标识符实际上是一个“创建在堆( heap )里的实实在在的对象的” reference 。这个对象持有其它对象的 reference ,或直接持有 primitive 类型的值。

      

       3.Arrays

       java.util.Arrays 类,包括了一组可用于数组的 static 方法。其中 asList() 方法,可把数组转成一个 List

       Arrays.fill() 方法,把一个值或对象的 reference 拷贝到数组的各个位置,或指定的范围。

      

       4. 复制一个数组

       相比 for 循环, System.arrayCopy() 能以更快的速度拷贝数组。如果是对象数组,拷贝的是数组中对象的 reference ,对象本身不会被拷贝。这被称为浅拷贝 (Shallow copy)

      

       5. 数组的比较

       Arrays 提供了 equals() 方法。数组是否相等是基于其内容的。

       数组要想完全相等,它们必须有相同数量的元素,且数组的每个元素必须与另一个数组的对应位置上的元素相等。

       元素的相等性,用 equals() 判断。对于 primitive ,会使用其 wrapper 类的 equals()

      

       6. 数组元素的比较

       实现比较功能的一个方法是实现 java.lang.Comparable 接口。这个接口只有一个 compareTo() 方法。

       Arrays.sort() 会把传给它的数组的元素转换成 Comparable 。如果数组元素没有实现 Comparable 接口,就会引发一个 ClassCastException

       实现比较功能的另一个方法使用策略模式 (strategy design pattern) ,即实现 Comparator 接口。

       Arrays.sort() 可接受一个数组和一个 Comparator ,根据 Comparator compare() 方法对数组元素排序。

       Java 标准类库所用的排序算法已经作了优化--对于 primitive ,它用的是快速排序 (Quicksort) ,对于对象,它用的是稳定合并排序 (stable merge sort)

      

       7. 查询有序数组

       一旦对数组进行了排序,就能用 Arrays.binarySearch() 进行快速查询了。但切忌对一个尚未排序的数组使用 binarySearch()

       如果 Arrays.binarySearch() 查找到了,就返回一个大于或等于 0 的值。否则返回负值。这个负值的意思是,如果手动维护这个数组,这个值应该插在哪个位置。这个值是:

       -( 插入点 )-1

       “插入点”就是,在所有比要找的值更大的值中,最小的那个值的下标。如果数组中所有值都比要查找的值小,它就是 a.size()

       如果数组里有重复元素, binarySearch() 不能保证返回哪一个,但也不报错。

       如果排序的时候用到了 Comparator ,那么调用 binarySearch() 的时候,也必须使用同一个 Comparator

      

       8. 数组部分的总结

       如果要持有一组对象,首选,同时效率最高的,应该是数组。如果是要持有一组 primitive ,也只能用数组。

      

       9. 容器简介

       Java2 的容器类要解决“怎样持有对象”,它把这个问题分成两类:

       (1).Collection: 通常是一组有一定规律的独立元素。 List 必须按特定的顺序持有这些元素,而 Set 不能保存重复的元素。

       (2).Map: 一组以“键-值” (key-value) 形式出现的 pair Map 可以返回键 (Key) Set ,值的 Collection ,或者 pair Set

      

       10. 填充容器

       Collection 也有一个辅助类 Collections ,它包含了一些静态的使用工具方法,其中有 fill() fill() 只是把同一个对象的 reference 负值到整个容器,而且只能为 List ,不能为 Set Map 工作。并且这个 fill() 只能替换容器中的值,而不是往 List 加新元素。如:

       List list = new ArrayList();

       for(int i = 0; i<10; i++)

              list.add("");

       Collections.fill(list, "Hello");

      

       11. 容器的缺点:不知道对象的类型

       Java 的容器只持有 Object 。容器对“能往里面加什么类型的对象”没有限制。在使用容器中的对象之前,还必须进行类型转换

      

       12. 迭代器

       迭代器 (iterator) ,又是一个设计模式。 iterator 能让程序员在不知道或不关心他所处理的是什么样的底层序列结构的情况下,在一个对象序列中前后移动,并选取其中的对象。 iterator 是“轻量级”的对象,即创建代价很小的对象。

       不经意的递归 (Unintended recursion)

       public class A{

              public String toString(){

                     return "A address:" + this +"\n";//

              }

              public static void main(String[] args){

                     System.out.println(new A());

              }

       }

       上面的程序会出现无穷无尽的异常。

       "A address:" + this         ,编译器会试着将 this 转换成 String ,要用大 toString() ,于是就变成递归调用了。

       如果想打印对象的地址,应该调用 Object toString() 方法。而不要用 this ,应该写 super.toString()

      

       13.List 的功能

       ArrayList ,一个用数组实现的 List 。能进行快速的随机访问,但是往列表中插入和删除元素比较慢。

       LinkedList ,对顺序访问进行了优化。在 List 中插入和删除元素代价也不高。但是随机访问的速度相对较慢。可以把它当成栈 (Stack) ,队列 (queue) 或双向队列 (deque) 来用。

      

       14.Set 的功能

       加入 Set 的每个元素必须是唯一的。要想加进 Set Object 必须定义 equals() ,才能标明对象的唯一性。

       HashSet ,为优化查询速度而设计的 Set 。要放进 HashSet Object 还要定义 hashCode()

       TreeSet ,一个有序的 Set ,能从中提取一个有序序列。用了红黑树 (red-black tree) 数据结构。

       LinkedHashSet ,使用链表的 Set ,既有 HashSet 的查询速度,又能保存元素的插入顺序。用 Iterator 遍历 Set 的时候,它是按插入顺序进行访问的。

       Set 要有一个判断以什么顺序来存储元素的标准,也就是说必须实现 Comparable 接口,并且定义 compareTo() 方法。

      

       15.SortedSet

       SortedSet( 只有 TreeSet 这一个实现可用 ) 中的元素一定是有序的。 SortedSet 的意思是“根据对象的比较顺序”,而不是“插入顺序”进行排序。

      

       16.Map 的功能

       如果知道 get() 是怎么工作的,就会发觉在 ArrayList 里面找对象是相当慢的。而这正是 HashMap 的强项。 HashMap 利用对象的 hashCode() 来进行快速查找。

       Map keySet() 方法返回一个由 Map 的键组成的 Set values() 返回的是由 Map 的值所组成的 Collection 。由于这些 Collection 的后台都是 map ,因此对这些 Collection 的任何修改都会反映到 Map 上。

      

       17.SortedMap

       SortedMap( 只有 TreeMap 这一个实现 ) 的键肯定是有序的。

      

       18.LinkedHashMap

       为提高速度, LinkedHashMap 对所有东西都作了 hash ,而且遍历的时候,还会按插入顺序返回 pair 。此外,还可通过构造函数进行配置,让它使用基于访问的 LRU(least-recently-used) 算法,这样没被访问过的元素(通常也是要删除的候选对象)就会出现在队列的最前面。

      

       19. 散列算法与 Hash

       要想用自己的类作 HashMap 的键,必须覆写 equals() hashCode() HashMap equals() 来判断查询用的键是否与表里其它键相等。

       Object hashCode() ,在缺省情况下就是返回对象的内存地址。

      

       一个合适的 equals() 必须做到以下五点:

       (1). 反身性:对任何 x x.equals(x) 必须是 true

       (2). 对称性:对任何 x y ,如果 y.equals(x) true 的,那么 x.equals(y) 也必须是 true

       (3). 传递性:对任何 x y z ,如果 x.equals(y) true ,且 y.equals(z) 也是 true ,那么 x.equals(z) 也必须是 true

       (4). 一致性:对任何 x y ,如果对象里面用来判断相等性的信息没有修改过,那么无论调用多少次 x.equals(y) ,它都必须一致地返回 true false

       (5). 对于任何非空的 x x.equals(null) 必须返回 false

      

       默认的 Object.equals() 只是简单地比较两个对象的地址,所以一个 Dog("A") 会不等于另一个 Dog("A")

       下面是覆写 equals() hashCode() 的例子。

       public class Dog{

              public int id;

              public Dog(int x){ id = x; }

              public int hashCode(){ return id; }

              public boolean equals(Object o){

                     return (o instanceof Dog) && (id == ((Dog)o).id)

              }

       }

       equals() 在利用 instanceof 检查参数是不是 Dog 类型的同时,还检查了对象是不是 null ,如果是 null instanceof 会返回 false

      

       20. 理解 hashCode()

       数组是最快的数据结构,所以很容易想到用数组存储 Map 的键的信息(而不是键本身)。 Map 要能存储任意数量的 pair ,而键的数量又被数组的固定大小限制了,所以不能用数组存储键本身。

       要解决定长数组的问题,就得允许多个键生成同一个 hash 数,也就是会有冲突,每个键对象都会对应数组的某个位置。

       查找过程从计算 hash 数开始,算完后用这个数在数组里定位。如果散列函数能确保不产生冲突(如果对象数量是固定的,这是可能的),那么它就被称为“完全散列函数”,这是特例。通常,冲突是由“外部链 (external chaining) ”处理的:数组并不直接指向对象,而是指向一个对象的列表。然后再用 equals() 在这个列表中一个个找。如果散列函数定义得好,每个 hash 数只对应很少的对象,这样,与搜索整个序列相比,能很快跳到这个子序列,比较少量对象,会快许多。

       hash 表的“槽位”常被称为 bucket

      

       21. 影响 HashMap 性能的因素

       Capacity hash 表里 bucket 的数量。

       Initial capacity :创建 hash 表时, bucket 的数量。

       Size :当前 hash 表的记录的数量。

       Load factor size/capacity 。一个负载较轻的表会有较少的冲突,因此插入和查找的速度会比较快,但在用迭代器遍历的时候会比较慢。

       HashMap HashSet 都提供了能指定 load factor 的构造函数,当 load factor 达到这个阀值的时候,容器会自动将 capacity(bucket 的数量 ) 增加大约一倍,然后将现有的对象分配到新的 bucket 里面(这就是所谓的 rehash )。缺省情况下 HashMap 会使用 0.75 load factor

      

       22. 选择实现

       HashTable Vector Stack 属于老版本遗留下来的类,应该避免使用。

       如何挑选 List

       数组的随机访问和顺序访问比任何容器都快。 ArrayList 的随机访问比 LinkedList 快,奇怪的时 LinkedList 的顺序访问居然比 ArrayList 略快。 LinkedList 的插入和删除,特别时删除,比 ArrayList 快很多。 Vector 各方面速度都比 ArrayList 慢,应避免使用。

       如何挑选 Set

       HashSet 各项性能都比 TreeSet 好,只有在需要有序的 Set 时,才应该用 TreeSet

       LinkedHashSet 的插入比 HashSet 稍慢一些,因为要承担维护链表和 hash 容器的双重代价,但是它的遍历速度比较快。

       如何挑选 Map

       首选 HashMap ,只有在需要有序 map 时,才选 TreeMap LinkedHashMap Hashmap 稍慢一些。

      

       23. Collection Map 设成不可修改的

       Collections.unmodifiableCollection() 方法,会把传给它的容器变成只读版返回。这个方法有四种变形, unmodifiableCollection(),unmodifiableList(),unmodifiableSet(),unmodifiableMap()

      

       24.Collection Map 的同步

       Collections 里有一个自动对容器做同步的方法,它的语法与“ unmodifiable ”方法有些相似。 synchronizedCollection(),synchronizedList(),synchronizedSet(),synchronizedMap()

      

       25.Fail fast

       Java 容器类库继承了 fail-fast( 及早报告错误 ) 机制,它能防止多个进程同时修改容器的内容。当它发现有其它进程在修改容器,就会立即返回一个 ConcurrentModificationException

      

       26. 可以不支持的操作

       可以用 Arrays.asList() 把数组改造成 List ,但它只是部分的实现了 Collection List 接口。它支持的都是那些不改变数组容量的操作,不支持 add(),addAll(),clear(),retainAll(),remove(),removeAll() 等。调用不支持的方法会引发一个 UnsupportedOperationException 异常。

       要想创建普通容器,可以把 Arrays.asList() 的结果做为构造函数参数传给 List Set ,这样就能使用它的完整接口了。

      

      

      

      

=============

Chap12   Java I/O 系统

 

       1.File

       File 类有一个极具欺骗性的名字,可以用来表示某个文件的名字,也可以用来表示目录里一组文件的名字。

       File 类的功能不仅限于显示文件或目录。它能创建新的目录,甚至是目录路径。此外还能检查文件的属性,判断 File 对象表示的是文件还是目录,以及删除文件等。

      

       2. 输入与输入

       (Stream) 是一种能生成或接受数据的,代表数据的源和目标的对象。流把 I/O 设备内部的具体操作给隐藏起来了。

       Java I/O 类库分成输入和输出两大部分。

      

       3. 添加属性与适用的接口

       使用“分层对象 (layered objects) ”,为单个对象动态地,透明地添加功能的做法,被称为 Decorator Pattern Decorator 模式要求所有包覆在原始对象之外的对象,都必须具有与之完全相同的接口。无论对象是否被 decorate 过,传给它的消息总是相同的。

       InputStream OutputStream 定义 decorator 类接口的类,分别是 FilterInputStream FilterOutputStream ,它们都继承自 I/O 类库的基类 InputStream OutputStream ,这是 decorator 模式的关键(惟有这样, decorator 类的接口才能与它要服务的对象的完全相同)。

       对于 I/O 类库来说,比较明智的做法是,普遍都做缓冲,把不缓冲当特例。

      

       Reader Writer 类系

       InputStream OutputStream 的某些功能已经淘汰,但仍然提供了很多有价值的,面向 byte I/O 功能。而 Java 1.1 引进的 Reader Writer 则提供了 Unicode 兼容的,面向字符的 I/O 功能。 Java 1.1 还提供了两个适配器 (adapter) 类, InputStreamReader OutputStreamWriter 负载将 InputStream OutputStream 转化成 Reader Writer

       Reader Writer 要解决的,最主要是国际化。原先的 I/O 类库只支持 8 位的字节流,因此不可能很好地处理 16 位的 Unicode 字符流。此外新类库的性能也比旧的好。

      

       4. 数据源和目的

       几乎所有的 Java I/O 流都有与之对应的,专门用来处理 Unicode Reader Writer 。但有时,面向 byte InputStream OutputStream 才是正确的选择,特别是 java.util.zip ,它的类都是面向 byte 的。

       明智的做法是,先用 Reader Writer ,等到必须要用面向 byte 的类库时,你自然会知道,因为程序编译不过去了。

      

       5. 常见的 I/O 流的使用方法

       (1). 对输入文件做缓冲

       BufferedReader in = new BufferedReader( new FileReader("IOStreamDemo.java"));

       String s, s2 = new String();

       while((s = in.readLine())!= null)

              s2 += s + "\n";//readLine() 会把换行符剥掉,所以在这里加上。

       in.close();

       // 读取标准输入

       BufferedReader stdin = new BufferedReader( new InputStreamReader(System.in));

       System.out.print("Enter a line:");

       System.out.println(stdin.readLine());

      

       (2). 读取内存

       StringReader in2 = new StringReader(s2);

       int c;

       while((c = in2.read())!=-1)//read() 会把读出来的 byte 当做 int

              System.out.print((char)c);

      

       (3). 读取格式化内存

       try{

              DataInputStream in3 = new DataInputStream(new ByteArrayInputStream(s2.getBytes()));

              while(true)

                     System.out.print((char)in3.readByte());// 无法根据 readByte() 返回值判断是否结束

       } catch(EOFException e){

              System.err.println("End of stream");

       }

       // 使用 available() 来判断还有多少字符

       DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("TestEOF.java")));

       while(in.available() != 0)

              System.out.print((char)in.readByte());

      

       (4). 读取文件

       try{

              BfferedReader in4 = new BufferedReader(new StringReader(s2));

              PrintWriter out1 = new PrintWriter(new BufferedWriter(new FileWriter(IODemo.out)));

              int lineCount = 1;

              while((s = in4.readLine())!= null)

                     out1.println(lineCount++ +": "+ s);

              out1.close();

       } catch(EOFException e){

              System.err.println(End of stream);

       }

       使用 PrintWriter 去排版,就能得出能够读得懂的,普通的文本文件。

      

       6. 标准 I/O

       标准 I/O Unix 的概念,意思是,一个程序只使用一个信息流。所有输入都是从“标准输入”进来的,输出都从“标准输出”出去,错误消息都送到“标准错误”里。

       Java 遵循标准 I/O 的模型,提供了 Syetem.in System.out ,以及 System.err

      

       System.out 转换成 PrintWriter

       System.out PrintStream ,也就是说它是 OutputStream 。不过可通过 PrintWriter 的构造函数把它改造成 PrintWriter

       PrintWriter out = new PrintWriter(System.out, true);

       out.println("Hello, world");

       为了启动自动清空缓冲区的功能,一定要使用双参数版的构造函数,并把第二个参数设成 true 。这点非常重要,否则就有可能会看不到输出。

      

       标准 I/O 的重定向

       Java System 类提供了几个能重定向标准输入,标准输出和标准错误的静态方法:

       setIn(InputStream),setOut(PrintStream),setErr(PrintStream)

       I/O 重定向处理的不是 character 流,而是 byte 流,因此不能用 Reader Writer ,要用 InputStream OutputStream

      

       7.New I/O

       Java 1.4 java.nio.* 引入了一个新的 I/O 类库,其目的就是提高速度。实际上,旧的 I/O 类库已经用 nio 重写。

       性能的提高源于它用了更贴近操作系统的结构: channel buffer

       java.nio.ByteBuffer 是唯一一个能直接同 channel 打交道的 buffer 。它是一个相当底层的类,存储和提取数据的时候,可以选择是以 byte 形式还是以 primitive 形式,但它不能存储对象。这是为了有效地映射到绝大多数操作系统上。

       I/O 修改了旧 I/O 的三个类,即 FileInputStream FileOutputStream ,以及 RandomAccessFile ,以获取 FileChannel

       // Write a file:

    FileChannel fc = new FileOutputStream("data.txt").getChannel();

    fc.write(ByteBuffer.wrap("Some text ".getBytes()));

    fc.close();

    // Add to the end of the file:

    fc = new RandomAccessFile("data.txt", "rw").getChannel();

    fc.position(fc.size()); // Move to the end

    fc.write(ByteBuffer.wrap("Some more".getBytes()));

    fc.close();

    // Read the file:

    fc = new FileInputStream("data.txt").getChannel();

    ByteBuffer buff = ByteBuffer.allocate(4096);

    fc.read(buff);

    buff.flip();

    while(buff.hasRemaining())

      System.out.print((char)buff.get()); 

       wrap( ) 方法把一个已经拿到手的 byte 数组 " " ByteBuffer 。如果是用这种方法,新创建的 ByteBuffer 是不会去拷贝底层的 (byte) 数组的,相反它直接用那个 byte 数组来当自己的存储空间。所以我们说 ByteBuffer " 后台 " 是数组。

       buffer 中取数据前,要调用 buffer flip() 。往 buffer 中装数据前,要调用 buffer clear()

       FileChannel

      in = new FileInputStream(args[0]).getChannel(),

      out = new FileOutputStream(args[1]).getChannel();

    ByteBuffer buffer = ByteBuffer.allocate(BSIZE);

    while(in.read(buffer) != -1) {

      buffer.flip(); // Prepare for writing

      out.write(buffer);

      buffer.clear();  // Prepare for reading

    }

 

       View Buffers

      

       View Buffer 能让你从特殊的视角,来观察其底层的 ByteBuffer 。对 view 的任何操作都会作用到 ByteBuffer 上。同一个 ByteBuffer ,能读出不同的数据。 ByteBuffer 1 字节区分数据, CharBuffer 2 字节, IntBuffer FloatBuffer 4 字节, LongBuffer DoubleBuffer 8 字节。

       ByteBuffer bb = ByteBuffer.wrap(new byte[]{ 0, 0, 0, 0, 0, 0, 0, 'a' });

    bb.rewind();

    System.out.println("Byte Buffer");

    while(bb.hasRemaining())

      System.out.println(bb.position()+ " -> " + bb.get());

    CharBuffer cb = ((ByteBuffer)bb.rewind()).asCharBuffer();

    System.out.println("Char Buffer");

    while(cb.hasRemaining())

      System.out.println(cb.position()+ " -> " + cb.get());

    FloatBuffer fb = ((ByteBuffer)bb.rewind()).asFloatBuffer();

    System.out.println("Float Buffer");

    while(fb.hasRemaining())

      System.out.println(fb.position()+ " -> " + fb.get());

    IntBuffer ib = ((ByteBuffer)bb.rewind()).asIntBuffer();

    System.out.println("Int Buffer");

    while(ib.hasRemaining())

      System.out.println(ib.position()+ " -> " + ib.get());

     

    Buffer 的细节

    如果使用相对定位的 get() put() 方法, buffer position 会跟着变化。也可以用下标参数调用绝对定位的 get() put() 方法,这时它不会改动 buffer position

    mark() 方法会记录当前 position reset() 会把 position 设置到 mark 的位置。 rewind() position 设置到 buffer 的开头, mark 被擦掉了。 flip() limit 设为 position ,把 position 设为零。当你将数据写入 buffer ,准备读取的时候,必须先调用这个方法。

   

       内存映射文件

       memory-mapped file 能让你创建和修改那些大到无法读入内存的文件(最大 2GB )。

       int length = 0x8FFFFFF; // 128 Mb

       MappedByteBuffer out = new RandomAccessFile("test.dat","rw").getChannel().map(FileChannel.MapMode.READ_WRITE,0,length);

       for(int i = 0; i < length; i++)

              out.put((byte)'x');

       for(int i = length/2;i<length/2+6;i++)

              System.out.print((char)out.get(i));

       MappedByteBuffer ByteBuffer 的派生类。例程创建了一个 128MB 的文件,文件的访问好像只是一瞬间的事,这是因为,真正调入内存的只是其中的一小部分,其余部分则被放在交换文件上。 Java 是调用操作系统的 " 文件映射机制 (file-mapping facility)" 来提升性能的。只有 RandomAccessFile 才能写映射文件。

      

       文件锁

       Java 的文件锁是直接映射操作系统的锁机制的,因此其它进程也能看到文件锁。

       FileOutputStream fos= new FileOutputStream("file.txt");

    FileLock fl = fos.getChannel().tryLock();

    if(fl != null) {

      System.out.println("Locked File");

      Thread.sleep(100);

      fl.release();

      System.out.println("Released Lock");

    }

    fos.close();

    tryLock( ) 是非阻塞的。它会试着去获取这个锁,但是如果得不到 ( 其它进程已经以独占方式得到这个锁了 ) ,那它就直接返回。而 lock( ) 是阻塞的。如果得不到锁,它会在一直处于阻塞状态,除非它得到了锁,或者你打断了调用它 ( lock( ) 方法 ) 的线程,或者关闭了它要 lock( ) channel ,否则它是不会返回的。最后用 FileLock.release( ) 释放锁。

    还可以像这样锁住文件的某一部分,

tryLock(long position, long size, boolean shared)

       或者

lock(long position, long size, boolean shared)

这个方法能锁住文件的某个区域 (size - position) 。其中第三个参数表示锁能不能共享。

       对于带参数的 lock( ) tryLock( ) 方法,如果你锁住了 position position+size 这段范围,而文件的长度又增加了,那么 position+size 后面是不加锁的。而无参数的 lock 方法则会锁定整个文件,不管它变不变长。

 

       8. 压缩

       Java I/O 类库还收录了一些能读写压缩格式流的类,它们是 InputStream OutputStream 的派生类。这是因为压缩算法是针对 byte 而不是字符的。

       GZIP 的接口比较简单,因此如果你只有一个流要压缩的话,用它会比较合适。

       BufferedReader in = new BufferedReader(new FileReader(args[0]));

    BufferedOutputStream out = new BufferedOutputStream(

      new GZIPOutputStream(new FileOutputStream("test.gz")));

    System.out.println("Writing file");

    int c;

    while((c = in.read()) != -1)

      out.write(c);

    in.close();

    out.close();

    System.out.println("Reading file");

    BufferedReader in2 = new BufferedReader(

      new InputStreamReader(new GZIPInputStream(new FileInputStream("test.gz"))));

    String s;

    while((s = in2.readLine()) != null)

      System.out.println(s);

       只要用 GZIPOutputStream ZipOutputStream 把输出流包起来,再用 GZIPInputStream ZipInputStream 把输入流包起来就行了。

      

       Zip 存储多个文件

       FileOutputStream f = new FileOutputStream("test.zip");

    CheckedOutputStream csum = new CheckedOutputStream(f, new Adler32());

    ZipOutputStream zos = new ZipOutputStream(csum);

    BufferedOutputStream out = new BufferedOutputStream(zos);

    zos.setComment("A test of Java Zipping");

    // No corresponding getComment(), though.

    for(int i = 0; i < args.length; i++) {

      System.out.println("Writing file " + args[i]);

      BufferedReader in = new BufferedReader(new FileReader(args[i]));

      zos.putNextEntry(new ZipEntry(args[i]));

      int c;

      while((c = in.read()) != -1)

        out.write(c);

      in.close();

    }

    out.close();

    // Checksum valid only after the file has been closed!

    System.out.println("Checksum: " + csum.getChecksum().getValue());

    // Now extract the files:

    System.out.println("Reading file");

    FileInputStream fi = new FileInputStream("test.zip");

    CheckedInputStream csumi = new CheckedInputStream(fi, new Adler32());

    ZipInputStream in2 = new ZipInputStream(csumi);

    BufferedInputStream bis = new BufferedInputStream(in2);

    ZipEntry ze;

    while((ze = in2.getNextEntry()) != null) {

      System.out.println("Reading file " + ze);

      int x;

      while((x = bis.read()) != -1)

        System.out.write(x);

    }

    System.out.println("Checksum: " + csumi.getChecksum().getValue());

    bis.close();

    // Alternative way to open and read zip files:

    ZipFile zf = new ZipFile("test.zip");

    Enumeration e = zf.entries();

    while(e.hasMoreElements()) {

      ZipEntry ze2 = (ZipEntry)e.nextElement();

      System.out.println("File: " + ze2);

      // ... and extract the data as before

    }

       虽然标准的 Zip 格式是支持口令的,但是 Java Zip 类库却不支持。

      

       Java ARchives (JARs)

       一个 JAR 只有一个文件,包含两个文件,一个是 Zip 文件,另一个是描述 Zip 文件所包含的文件的 "manifest( 清单 )"

       如果 JAR 是用 0( ) 选项创建的,不会进行压缩,那么它就能被列入 CLASSPATH 了。

       不能往已经做好的 JAR 里添加新文件或修改文件。不能在往 JAR 里移文件的同时把原来的文件给删了。不过 JAR 格式是跨平台的,无论 JAR 是在哪个平台上创建的, jar 程序都能将它读出来 (zip 格式有时就会有问题了 )

 

       9. 对象的序列化

       Java " 对象序列化 " 能让你将一个实现了 Serializable 接口的对象转换成一组 byte ,需要的时候,根据 byte 数据重新构建那个对象。这一点甚至在跨网络的环境下也是如此,序列化机制能自动补偿操作系统方面的差异。

       对象序列化不仅能保存对象的副本,而且还会跟着对象里面的 reference ,把它所引用的对象也保存起来,然后再继续跟踪那些对象的 reference ,以此类推。这种情形常被称为 " 单个对象所联结的 ' 对象网 '" 。这个机制所涵盖的范围不仅包括对象的成员数据,而且还包含数组里面的 reference

       Worm w = new Worm(6, 'a');

    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out"));

    out.writeObject("Worm storage\n");

    out.writeObject(w);

    out.close(); // Also flushes output

    ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out"));

    String s = (String)in.readObject();

    Worm w2 = (Worm)in.readObject();

 

       把对象从序列化状态中恢复出来的必要条件是,一定要让 JVM 找到 .class 文件。

      

       控制序列化

       可以让对象去实现 Externalizable 而不是 Serializable 接口,并以此来控制序列化的过程。

       对于 Externalizable 对象, readExternal( ) 要在默认的构造行为会发生之后 ( 包括在定义数据成员时进行的初始化 ) 才启动。

       不但要在 writeExternal( ) 的时候把重要的数据保存起来 ( 默认情况下, Externalizable 对象不会保存任何成员对象 ) ,还得在 readExternal( ) 的时候把它们恢复出来。为了能正确地存取其父类的组件,你还得调用其父类的 writeExternal( ) readExternal( )

      

       transient 关键词

       要想禁止敏感信息的序列化,除了可以实现 Externalizable 外。还可以使用 transient 关键词修饰 Serializable 对象中不想序列化的成员。

       默认情况下, Externalizable 对象不保存任何字段,因此 transient 只能用于 Serializable 对象。

      

       Externalizable 的替代方案

       如果你不喜欢 Externalizable ,还可以选择 Serializable 接口,然后再加入 ( 注意,我没说 " 覆写 " " 实现 ") 序列化和恢复的时候会自动调用的 writeObject( ) readObject( ) 方法。也就是说,如果你写了这两个方法, Java 就会避开默认的序列化机制而去调用这两个方法了。

       两个方法的特征签名如下,(它们都是 private 的,怪异):

private void writeObject(ObjectOutputStream stream) throws IOException;

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException

       如果你决定用默认的序列化机制来存储非 transient 的数据,你就得在 writeObject( ) 里面调用 defaultWriteObject( ) ,不带参数,而且得第一个做。恢复的时候,也要在 readObject( ) 的开头部分调用 defaultReadObject( )

       如果你要序列化 static 数据,就必须亲自动手。

      

       Preferences

       JDK 1.4 所引入的 Preferences API 能自动存储和恢复信息。但是,它只能存取很少几种数据—— primitive String ,而且每个 String 的长度都不能超过 8K

       Preferences 是一组存储在 " 由节点所组成的层次体系 (a hierarchy of nodes)" 里的键值集 ( 很像 Map) Preferences API 是借用操作系统的资源来实现功能的。对于 Windows ,它就放在注册表里。

       // 也可以用 systemNodeForPackage( )

       //"user" 指的是单个用户的 preference ,而 "system" 指整个系统的共用配置

       // 一般用 XXX.class 做节点的标识符

       Preferences prefs = Preferences.userNodeForPackage(PreferencesDemo.class);

    prefs.put("Location", "Oz");

    prefs.putInt("Companions", 4);

    prefs.putBoolean("Are there witches?", true);

   

    10. 正则表达式

    正则表达式是 JDK 1.4 的新功能。由 java.util.regex Pattern Matcher 类实现的。

    Pattern p = Pattern.compile(("\\w+");

       Matcher m = p.matcher(args[0]);

       while(m.find()) {

         System.out.println("Match \"" + m.group() +

           "\" at positions " +

           m.start() + "-" + (m.end() - 1));

       }

    只要字符串里有这个模式, find( ) 就能把它给找出来,但是 matches( ) 成功的前提是正则表达式与字符串完全匹配,而 lookingAt( ) 成功的前提是,字符串的开始部分与正则表达式相匹配。

      

       split()

       所谓分割是指将以正则表达式为界,将字符串分割成 String 数组。

       String[] split(CharSequence charseq)

       String[] split(CharSequence charseq, int limit)// 限定分割的次数

       String input = "This!!unusual use!!of exclamation!!points";

    System.out.println(Arrays.asList(Pattern.compile("!!").split(input)));

 

 

===========

Chap13   并发编程

 

       1. 基本线程

       要想创建线程,最简单的办法就是继承 java.lang.Thread run( ) Thread 最重要的方法,什么时候 run( ) 返回了,线程也就中止了。

       Thread start( ) 方法会先对线程做一些初始化,再调用 run( )

       整个步骤应该是:调用构造函数创建一个 Thread 对象,并且在构造函数里面调用 start( ) 来配置这个线程,然后让线程的执行机制去调用 run( ) 。如果你不调用 start( ) ,那么线程永远也不会启动。

       有时我们创建了 Thread ,但是却没去拿它的 reference 。如果是普通对象,这一点就足以让它成为垃圾,但 Thread 不会。 Thread 都会为它自己 " 注册 " ,所以实际上 reference 还保留在某个地方。除非 run( ) 退出,线程中止,否则垃圾回收器不能动它。

       线程的调度机制是非决定性,即多个线程的执行顺序是不确定的。

      

       yielding

       如果知道 run() 已经告一段落了,你就可以用 yield( ) 形式给线程调度机制作一个暗示。 Java 的线程调度机制是抢占式的 (preemptive) ,只要它认为有必要,它会随时中断当前线程(运行到 yield 之前),并且切换到其它线程。总之, yield( ) 只会在很少的情况下起作用。

      

       Sleeping

       sleep( ) 一定要放在 try 域里,这是因为有可能会出现时间没到 sleep( ) 就被中断的情况。如果有人拿到了线程的 reference ,并且调用了它的 interrupt( ) ,这种事就发生了。 (interrupt( ) 也会影响处于 wait( ) join( ) 状态的线程,所以这两个方法也要放在 try 域里。 ) 如果你准备用 interrupt( ) 唤醒线程,那最好是用 wait( ) 而不是 sleep( ) ,因为这两者的 catch 语句是不一样的。

      

       优先级

       线程往控制台打印的时候是不会被中断的,否则控制台的显示就乱了。

      

       守护线程

       所谓 " 守护线程 (daemon thread)" 是指,只要程序还在运行,它就应该在后台提供某种公共服务的线程,但是守护线程不属于程序的核心部分。因此,当所有非守护线程都运行结束的时候,程序也结束了。

       要想创建守护线程,必须在它启动之前就 setDaemon(true) 。守护线程所创建的线程也自动是守护线程。

      

       连接线程

       线程还能调用另一个线程的 join( ) ,等那个线程结束之后再继续运行。如果线程调用了另一个线程 t t.join( ) ,那么在线程 t 结束之前 ( 判断标准是, t.isAlive( ) 等于 false) ,主叫线程会被挂起。

      

       另一种方式: Runable

       类可能已经继承了别的类,这时就需要实现 Runable 接口了。

       如果要在这个实现了 Runable 的类里做 Thread 对象才有的操作,必须用 Thread.currentThread() 获取其 reference

       除非迫不得已只能用 Runnable ,否则选 Thread

      

       2. 共享有限的资源

       多线程环境的最本质的问题:永远也不会知道线程会在什么时候启动。

       我们不能从线程内部往外面抛异常,因为这只会中止线程而不是程序。

      

       资源访问的冲突

       Semaphore 是一种用于线程间通信的标志对象。如果 semaphore 的值是零,则线程可以获得它所监视的资源,如果不是零,那么线程就必须等待。如果申请到了资源,线程会先对 semaphore 作递增,再使用这个资源。递增和递减是原子操作 (atomic operation ,也就是说不会被打断的操作 ) ,由此 semaphore 就防止两个线程同时使用同一项资源。

      

       解决共享资源的冲突

       一个特定的对象中的所有的 synchronized 方法都会共享一个锁,而这个锁能防止两个或两个以上线程同时读写一块共用内存。当你调用 synchronized 方法时,这个对象就被锁住了。在方法返回并且解锁之前,谁也不能调用同一个对象的其它 synchronized 方法。

       一定要记住:所有访问共享资源的方法都必须是 synchronized 的,否则程序肯定会出错。

       一个线程能多次获得对象的锁。比如,一个 synchronized 方法调用了另一个 synchronized 方法,而后者又调用了另一 synchronized 方法。线程每获一次对象的锁,计数器就加一。当然,只有第一次获得对象锁的线程才能多次获得锁。线程每退出一个 synchronized 方法,计数器就减一。等减到零了,对象也就解锁了。

       此外每个类还有一个锁 ( 它属于类的 Class 对象 ) ,这样当类的 synchronized static 方法读取 static 数据的时候,就不会相互干扰了。

      

       原子操作

       通常所说的原子操作包括对非 long double 型的 primitive 进行赋值,以及返回这两者之外的 primitive 。不过如果你在 long double 前面加了 volatile ,那么它就肯定是原子操作了。最安全的原子操作只有读取和对 primitive 赋值这两种。

       如果你要用 synchronized 修饰类的一个方法,索性把所有的方法全都 synchronize 了。要判断,哪个方法该不该 synchronize ,通常是很难的,而且也没什么把握。

       并发编程的最高法则:绝对不能想当然。

      

       关键段

       有时你只需要防止多个线程同时访问方法中的某一部分,而不是整个方法。这种需要隔离的代码就被称为关键段 (critical section) 。创建关键段需要用到 synchronized 关键词,指明执行下列代码需获得哪个对象的锁。

synchronized(syncObject) {

  // This code can be accessed by only one thread at a time

}

       关键段又被称为 " 同步块 (synchronized block)" 。相比同步整个方法,同步一段代码能显著增加其它线程获得这个对象的机会。

      

       3. 线程的状态

       线程的状态可归纳为以下四种:

       (1).new: 线程对象已经创建完毕,但尚未启动 (start) ,因此还不能运行。

       (2).Runnable: 处在这种状态下的线程,只要分时机制分配给它 CPU 周期,它就能运行。

       (3).Dead: 要想中止线程,正常的做法是退出 run( )

       (4).Blocked: 就线程本身而言,它是可以运行的,但是有什么别的原因在阻止它运行。线程调度机制会直接跳过 blocked 的线程,根本不给它分配 CPU 的时间。除非它重新进入 runnable 状态,否则什么都干不了。

       如果线程被阻塞了,那肯定是出了什么问题。问题可能有以下几种:

       (1). 你用 sleep(milliseconds) 方法叫线程休眠。在此期间,线程是不能运行的。

       (2). 你用 wait( ) 方法把线程挂了起来。除非收到 notify( ) notifyAll( ) 消息,否则线程无法重新进入 runnable 状态。

       (3). 线程在等 I/O 结束。

       (4). 线程要调用另一个对象的 synchronized 方法,但是还没有得到对象的锁。

      

       4. 线程间的协作

       wait notify

       线程 sleep( ) 的时候并不释放对象的锁,但是 wait( ) 的时候却会释放对象的锁。也就是说在线程 wait( ) 期间,别的线程可以调用它的 synchronized 方法。     此外, sleep( ) 属于 Thread wait( ) notify( ) notifyAll( ) 是根 Object 的方法。

       只能在 synchronized 方法里或 synchronized 段里调用 wait( ) notify( ) notifyAll( )

       wait( ) 能让你在等待条件改变的同时让线程休眠,当其他线程调用了对象的 notify( ) notifyAll( ) 的时候,线程自会醒来,然后检查条件是不是改变了。

       安全的做法就是套用下面这个 wait( ) 定式 :

       while(conditionIsNotMet)

              wait( );

             

       用管道进行线程间的 I/O 操作

       在很多情况下,线程也可以利用 I/O 来进行通信。对 Java I/O 类库而言,就是 PipedWriter( 可以让线程往管道里写数据 ) PipedReader( 让另一个线程从这个管道里读数据 )

      

       5. 死锁

       Dijkstra 发现的经典的死锁场景:哲学家吃饭问题。

       只有在下述四个条件同时满足的情况下,死锁才会发生 :

       (1). 互斥:也许线程会用到很多资源,但其中至少要有一项是不能共享的 ( 同一时刻只能被一个线程访问 )

       (2). 至少要有一个进程会在占用一项资源的同时还在等另一项正被其它进程所占用的资源。也就是说,要想让死锁发生,哲学家必须攥着一根筷子等另一根。

       (3).( 调度系统或其他进程 ) 不能从进程里抢资源。所有进程都必须正常的释放资源。我们的哲学家都彬彬有礼,不会从他的邻座手里抢筷子。

       (4). 需要有等待的环。一个进程在等一个已经被另一进程抢占了的资源,而那个进程又在等另一个被第三个进程抢占了的资源,以此类推,直到有个进程正在等被第一个进程抢占了的资源,这样就形成了瘫痪性的阻塞了。这里,由于每个哲学家都是先左后右的拿筷子,所以有可能会造成等待的环。在例程中,我们修改了最后一位哲学家的构造函数,让他先右后左地拿筷子,从而破解了死锁。

       Java 语言没有提供任何能预防死锁的机制。

      

       6. 停止线程的正确的方法

       为了降低死锁的发生几率, Java 2 放弃了 Thread stop( ) suspend( ) resume( ) 方法。

       应该设置一个旗标 (flag) 来告诉线程什么时候该停止。

 

       7. 打断受阻的线程

       有时线程受阻之后就不能再做轮询了,比如在等输入,这时你就不能像前面那样去查询旗标了。碰到这种情况,你可以用 Thread.interrupt( ) 方法打断受阻的线程。最后要把受阻线程的 reference 设成 null

      

       8. 总结

       诺贝尔经济学奖得主 Joseph Stiglitz 有一条人生哲学,就是所谓的承诺升级理论 :

" 延续错误的代价是别人付的,但是承认错误的代价是由你付的。 "

       多线程的主要缺点包括 :

       (1). 等待共享资源的时候,运行速度会慢下来。

       (2). 线程管理需要额外的 CPU 开销。

       (3). 如果设计得不不合理,程序会变得异常复杂。

       (4). 会引发一些不正常的状态,像饥饿 (starving) ,竞争 (racing) ,死锁 (deadlock) ,活锁 (livelock)

       (5). 不同平台上会有一些不一致。

       通常你可以在 run( ) 的主循环里插上 yield( ) ,然后让线程调度机制帮你加快程序的运行。

 

 

==============

Chap14 创建 Windows Applet 程序

      

       设计中一条基本原则:让简单的事情变得容易,让困难的事情变得可行。

       软件工业界的“三次修订”规则:产品在修订三次后才会成熟。

      

       1. 控制布局

       Java 中,组件放置在窗体上的方式可能与其他 GUI 系统都不相同。首先,它完全基于代码,没有用来控制组件布局的“资源”。第二,组件的位置不是通过绝对坐标控制,二十由“布局管理器” (layout manager) 根据组件加入的顺序决定其位置。使用不同的布局管理器,组件的大小、形状和位置将大不相同。此外,布局管理器还可以适应 applet 或视窗的大小,调整组件的布局。

       JApplet,JFrame,JWindow JDialog 都可以通过 getContentPane() 得到一个容器 (Container) ,用来包含和显示组件。容器有 setLayout() 方法,用来设置布局管理器。

      

       2.Swing 事件模型

       Swing 的事件模型中,组件可以触发一个事件。每种事件的类型由单独的类表示。当事件被触发时,它将被一个或多个监听器接收,监听器负责处理事件。

       所谓事件监听器,就是一个“实现了某种类型的监听器接口的”类的对象。程序员要做的就是,先创建一个监听器对象,然后把它注册给触发事件的组件。注册动作是通过该组件的 addXXXListener() 方法完成的。

       所有 Swing 组件都具有 addXXXListener() removeXXXListener() 方法。

 

       3.Swing 组件一览

      

       工具提示 ToolTip

       任何 JComponent 子类对象都可以调用 setToolTipText(String)

      

       Swing 组件上的 HTML

       任何能接受文本的组件都可以接受 HTML 文本,且能根据 HTML 格式化文本。例如,

       JButton b = new JButton("<html><b><font size=+2>Hello<br>Press me");

       必须以 "<html>" 标记开始,但不会强制添加结束标记。

       对于 JApplet ,在除 init() 之外的地方添加新组件后,必须调用容器的 validate() 来强制对组件进行重新布局,才能显示新添加的组件。

      

       4. 选择外观 (Look & Feel)

       “可插拔外观” (Pluggable Look & Feel) 使你的程序能够模仿不同操作系统的外观。

       设置外观的代码要在创建任何可视组件之前调用。 Swing 的跨平台的金属外观是默认外观。

       try{

              UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

       } catch(Exception e){

       }

       catch 子句中什么也不用做,因为缺省情况下,如果设置外观失败, UIManager 将设置成跨平台的外观。

      

       动态绑定事件

       不能保证事件监听器被调用的顺序与它们被添加的顺序相同。

      

       5.Swing 与并发

       始终存在着一个 Swing 事件调度线程,它用来依次对 Swing 的所有事件进行调度。

      

       管理并发

       当你在类的 main() 方法中,或在一个独立线程中,准备修改任何 Swing 组件属性的时候,要注意, Swing 的事件调度线程可能会与你竞争同一资源。

       要解决这个问题,必须确保在任何情况下,只能在事件调度线程里修改 Swing 组件的属性。 Swing 提供了两种机制: SwingUtilities.invokeLater(Runnable) SwingUtilities.invokeAndWait(Runnable) 。它们都接受 runnable 对象作参数,并且在 Swing 的事件处理线程中,只有当事件队列中的任何未处理的事件都被处理完毕之后,它们才会调用 runnable 对象的 run() 方法。

       SwingUtilities.invokeLater(new Runnable(){

              public void run(){

                     txt.setText("ready");

              }

       });

       invokeLater() 是异步方法,会立即返回。 invokeAndWait() 是同步方法,会一直阻塞,直到事件处理完毕才会放回。

      

       6.JavaBean 与同步

       当你创建 Bean 的时候,你必须要假设它可能会在多线程环境下运行。也就是说:

       (1). 尽可能让 Beand 中的所有公共方法都是 synchronized 。这将导致 synchronized 的运行时开销。

       (2). 当一个多路事件触发了一组对该事件感兴趣的监听器时,必须假定,在遍历列表进行通知的同时,监听器可能会被添加或移除。

       public void notifyListeners(){

              ActionEvent a = new ActionEvent(BangBean2.this, ActionEvent.ACTION_PERFORMED, null);

              ArrayList lv = null;

              //Make a shallow copy of the List in case someone adds a listener while we're

              //calling listeners

              synchronized(this){

                     lv = (ArrayList)actinListeners.clone();

              }

              for(int i = 0; i < lv.size(); i++){

                     ((ActionListener)lv.get(i)).actionPerformed(a);

              }

       }

      

      

      

      

 

==============

Chap15 发现问题

 

       1. 单元测试

       //Discover the name of the class this object was created within:

       className = new Throwable().getStackTrace()[1].getClassName();

      

       JUnit

       JUnit 在输出消息中使用 "." 表示每个测试的开始。

       JUnit 为每个测试创建一个测试对象(继承自 TestCase ),以确保在测试运行之间没有不利的影响。所有的测试对象都是同时被创建的,而不是正好在测试方法执行之前才创建。

       setUp 是在每个测试方法运行之前被调用的。

      

       2. 利用断言提高可靠性

       断言语法

       assert boolean-expression;

       assert boolean-expression: information-expression;

      

       JDK 1.4 中,缺省情况下断言是关闭的。为了防止编译时的错误,必须带下面的标志进行编译:

       -source 1.4

       如: javac -source 1.4 Assert1.java

      

       运行程序也必须加上标志 -ea ,全拼是 -enableassertions 。这样才会执行所有的断言语句。

      

       我们也可以基于类名或包名来决定打开或关闭断言。

       还有另一种动态控制断言的方法:通过 ClassLoader 对象的方法 setDefaultAssertionStatus() ,它为所有随后载入的类设置断言的状态。

       public static void main(String[] args){

              ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true);

              //other statements

       }

       这样可以在运行时,不必使用 -ea 标志,但是仍然必须使用 -source 1.4 标志编译。

      

       DBC 使用断言

      

       DBC(Design by Contract) 是由 Bertrand Meyer(Eiffel 编程语言的创建者 ) 所阐明的一个概念,它通过确保对象遵循特定的、不能被编译时的类型检查所验证的规则,来帮助建立健壮的程序。

      

       3. 剖析和优化

       “我们应该忽略较小的效率,在 97% 的时间里我们都应该说:不成熟的优化是万恶之源。”-- Donald Knuth

      

       最优化指南

       避免为性能而牺牲代码的可读性。

       不能孤立地考虑性能。要权衡所需付出的努力与能得到的利益之间的关系。

       性能是大型工程要关心的问题,但通常不是小型工程要考虑的。

       使程序可运转比钻研程序的性能有更高的优先权。仅当性能被确定是一个关键因素的时候,在初始设计开发过程期间才应该予以考虑。

       不要假设瓶颈在什么地方,而应该运行剖析器来获得数据。

       在任何可能的情况下,尽量通过将对象设置为 null ,从而显式地将其销毁。有时这可能是对垃圾回收器的一种很有帮助的提示。

       程序大小的问题。仅当程序是大型的、运行时间长而且速度也是一个问题时,性能优化才有价值。

       static final 变量可以通过 JVM 进行优化以提高程序的速度。

      

       做可以运转的最简单的事物。(极限编程)

      

 

 

================

附录 A :对象的传递与返回

 

       确切地说, Java 有指针。 Java 中(除了基本类型)每个对象的标识符就是一个指针。但是它们受到了限制,有编译器和运行期系统监视着它们。 Java 有指针,但没有指针的相关算法。可以将它们看作“安全的指针”。

       “别名效应”是指,多个引用指向同一个对象。将引用作为方法的参数传递时,它会自动被别名化。

      

       制作局部拷贝

       Java 中所有的参数传递,执行的都是引用传递。当你传递对象时,真正传递的只是一个引用,指向存活于方法外的“对象”。对此引用做的任何修改,都是在修改方法外的对象。此外:

       (1). 别名效应在参数传递时自动发生。

       (2). 方法内没有局部对象,只有局部引用。

       (3). 引用有作用域,对象则没有。

       (4). Java 中,不需要为对象的生命周期操心。

       (5). 没有提供语言级别的支持(例如“常量”)以阻止对象被修改,或者消除别名效应的负面影响。不能简单地使用 final 关键字来修饰参数,它只能阻止你将当前引用指向其他对象。

      

       克隆对象

       如果确实要在方法调用中修改参数,但又不希望修改外部参数,那么就应该在方法内部制作一份参数的副本,以保护原参数。

       Object 类提供了 protected 方法 clone() ,要使用它,必须在子类中以 public 方式重载此方法。例如, ArrayList 就重载了 clone() ArrayList clone() 方法,并不自动克隆容器中包含的每个对象,只是将原 ArrayList 中的对象别名化,即只复制了 ArrayList 中对象的引用。这称为浅拷贝( shallow copy )。

      

       使类具有克隆能力

       虽然在所有类的基类 Object 中定义了克隆方法,但也不是每个类都自动具有克隆能力。

       克隆对象时有两个关键问题:

       (1). 调用 super.clone()

       (2). 将子类的 clone() 方法声明为 public

       基类的 clone() 方法,能“逐位复制 (bitwise copy) ”对象。

      

       实现 Cloneable 接口

       interface Cloneable{}

       这样的空接口称为“标记接口 (tagging interface) ”。

       Cloneable 接口的存在有两个理由。第一,如果某个引用上传为基类后,就不知道它是否能克隆。此时,可以用 instanceof 检查该引用是否指向一个可克隆的对象。

       if(myref instanceof Cloneable)//...

       第二,与克隆能力的设计有关,考虑到也许你不愿意所有类型的对象都是可克隆的。所以 Object.clone() 会检查当前类是否实现了 Cloneable 接口,如果没有,就抛出 CloneNotSupportedException 异常。所以,作为实现克隆能力的一部分,通常必须实现 Cloneable 接口。

      

       == !=

       Java 比较对象相等的等价测试并未深入对象的内部。 == != 只是简单地比较引用。如果引用代表的内存地址相同,则它们指向同一个对象,因此视为相等。所以,该操作符测试的是:不同的引用是否是同一个对象的别名。

      

       Object.clone() 的效果

       克隆过程的第一步通常都是调用 super.clone() 。它制作出完全相同的副本,为克隆操作建立了基础。在此基础上,你可以执行对完成克隆必要的其他操作。

       这里的其他操作是指,对对象中的每个引用,都明确地调用 clone() 。否则,那些引用会被别名化,仍指向原本的对象。

       只要没有向子类中添加需要克隆的引用,那么无论 clone() 定义于继承层次中多深的位置,只需要调用 Object.clone() 一次,就能完成所有必要的复制。

       ArrayList 深层拷贝而言,以下操作是必须的:克隆 ArrayList 之后,必须遍历 ArrayList 中的每个对象,逐一克隆。对 HashMap 做深层拷贝,也必须做类似的操作。

      

       向继承体系的更下层增加克隆能力

       可以向任意层次的子类添加克隆能力,从那层以下的子类,也就都具备了克隆能力。

      

       克隆小结

       如果希望一个类可以被克隆:

       (1). 实现 Cloneable 接口。

       (2). 重载 clone() ,声明为 public

       (3). clone() 中调用 Super.clone()

       (4). clone() 中捕获异常。

      

       只读类

       在只读类中所有数据都是 private 的,并且没有定义会修改对象内部状态的方法。只读类的对象可以有很多别名,也不会造成伤害。例如, Java 标准类库中所有基本类型的包装类。

      

       恒常性 (immutability) 的缺点

       当你需要一个被修改过的此类的对象的时候,必须承受创建新对象的开销,也会更频繁地引发垃圾回收。对于有些类(如 String ),其代价让人不得不禁止这么做。

       解决之道是创建一个可被修改的伴随类( companion class )。

      

 

=============

附录 B Java 编程指南

 

       设计

       1. 优雅设计终将得到回报。精心设计程序的时候生产率不会很高,但欲速则不达。

       2. 先能运行,再求快速。

       3. 分而治之。

       4. 尽量让所有东西自动化。(如测试和构建,先写测试,再编写类)

       5. 尽可能使类原子化。

       建议重新设计类的线索有:

       (1). 复杂的 switch 语句,请考虑使用多态。

       (2). 有许多方法,处理类型极为不同的操作:请考虑划分成不同的类。

       (3). 有许多成员变量,表示类型极为不同的属性:请考虑划分成不同的类。

       (4). 参考《 Refactoring:Improving the Design of Existing Code 》, Martin Fowler 著, (Addison-Wesley 1999)

       6. 将变动的和不变的因素分离。

       7. 在判断应该使用继承还是组合的时候,考虑是否需要上传为基类。

      

       实现

       1. 编写通用性的类时,请遵守标准形式。包括定义 equals() hashCode() toString() clone() (实现 Cloneable 接口,或者选择其它对象复制策略),并实现 Comparable Serialiable 接口。

       2. 在构造器中只做必要的动作:将对象设定为正确的状态。避免在构造器内调用其它方法( final 方法除外),因为这些方法可能会被其他人重载,这就可能在构造期间得到意外的结果。

       3. 优先选择接口而不是抽象类。只有在必须放进方法定义或成员变量时,才把它改为抽象类。接口只和客户希望的动作有关,而类则倾向于实现细节。