拜读Bruce大作 —— 《Java编程思想》(第一遍)

Java编程思想


所有优秀的作者,包括那些编写软件的程序员,都清楚其著作的某些部分直至重新创作的时候才变得完美,有时甚至要反复重写多次

第一章 对象导论

对象是一个通过对问题域(即上下文)中的元素进行建模,是一个其封装了以数据成员或属性表示的内部状态和以成员方法与接口表示的行为的抽象概念

  1. 对象始于抽象 -> 抽象始于问题建模【问题域即上下文】;
  2. 每个对象都有一个接口,接口体现服务,每个对象都提供服务,将对象看作“服务的提供者”有助于提高对象的内聚性;
  3. 使用面向对象的好处:便于对问题进行表示和交流;可应用设计良好的继承机制;拥有比如多态(可替代性(substitutability):某一特定类型的所有对象都可以接收同样的消息)等高级开发技巧;
  • 继承关系探讨,“是一个” 与 “像是一个”,【书:有时必须在导出类型中添加新的接口元素,这个新的类型仍然可以替代基类,但是这种替代并不完美,因为基类无法访问新添加的方法。这种情况我们可以描述为is-like-a关系。】
  • 多态的可互换对象,【书:为了执行动态绑定,Java使用一小段特殊代码来替代绝对地址调用;它忽略类型的具体细节,仅仅和基类交互。这段代码和具体类型信息是分离的…把将导出类看做是它的基类的过程称为向上转型(upcasting)…这里要表达的是,如果你是一个Shape,我知道你可以 erase() 和 draw() 你自己,那么去做吧…】
  • 单根继承结构的好处:1.简化参数传递,保证所有对象都具有某些功能,执行基本操作;2.使得垃圾回收器的实现变得容易多;3.为异常处理等系统级操作带来灵活性;
  • 异常处理:异常是一种对象;有助于编写出更健壮程序的机制;

第二章 一切都是对象

一切都是引用,引用操纵对象;
对象存储到什么地方:new 对象存储在堆(寄存器、堆栈、堆、常量存储区、非RAM存储区[流对象和持久化对象-即磁盘等]);
基本类型存储到什么地方:变量直接存储“值”,并置于堆栈中,更加高效;
java中的数组,【书:当创建一个数组对象时,实际上就是创建一个引用数组。并且每个引用都会被初始化为一个特定值,该值拥有自己的关键字null】;

  • 永远不需要销毁对象?!
  • static 关键字:static 方法可以创建或使用与其类型相同的被命名对象;因此常常拿来做“牧羊人”的角色,负责看护与其隶属统一类型的实例群;

第三章 操作符


第四章 控制执行流程


第五章 初始化与清理

初始化和清理是涉及安全的两个问题。

  • 用构造器确保初始化,【书:在Java中,“初始化”和“创建”捆绑在一起,两者不可分离】;
  • 方法重载:每个重载的方法都必须有一个独一无二的参数类型列表(尽管参数顺序不同也是被允许的,但会带来代码的混乱使得程序难以维护);另外,只要编译器可以根据语境判断出语义,那么就可以使用返回值区分重载方法(但通常不采用此做法);
  • 在构造器中调用构造器:尽管可以用this调用一个构造器,但却不能调用两个;此外,必须将构造器调用置于最起始处,否则编译器会报错。
  • static 的含义,static 方法就是没有this 的方法。
  • 清理, 终结处理和垃圾回收:在 java 中,对象可能不被垃圾回收;垃圾回收并不等于“析构”。看来之所以要有finalize(),是由于在分配内存时可能采用了类似C语言中的做法,而非 java 中的通常做法。这种情况主要发生在使用“本地方法”的情况下
  • 只要对象中存在没有被适当清理的部分,程序就存在很隐晦的缺陷。(近来,在某些情况下,单例模式也被认为有此问题。)

class文件在JVM中的视图

JVM - 类加载器

  1. bootstrap class loader(引导类加载器):是其他类加载器的父类,它用于加载Java核心库,并且是唯一一个用本地代码编写的类加载器。
  2. extension class loader(扩展类加载器):是bootstrap class loader加载器的子类,用于加载扩展库。
  3. system class loader(系统类加载器):是extension class loader加载器的子类,用于加载在classpath中的应用程序的类文件。
  4. user-defined class loader(用户定义的类加载器):是系统类加载器或其他用户定义的类加载器的子类。

Java GC系列(1):Java垃圾回收简介
在JVM体系结构中,与垃圾回收相关的两个主要组件是堆内存和垃圾回收器。堆内存用来保存运行时的对象实例。
堆内存有以下三个主要区域:

  1. 新生代(Young Generation)
    Eden空间(Eden space,任何实例都通过Eden空间进入运行时内存区域)
    S0 Survivor空间(S0 Survivor space,存在时间长的实例将会从Eden空间移动到S0 Survivor空间)
    S1 Survivor空间 (存在时间更长的实例将会从S0 Survivor空间移动到S1 Survivor空间)
  2. 老年代(Old Generation)实例将从S1提升到Tenured(终身代)
  3. 永久代(Permanent Generation)包含类、方法等细节的元信息。(在Java SE8特性中已经被移除)

垃圾回收器如何工作?

  1. 当它工作时,将一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”可以很容易移动到更靠近“传送带”的开始处,也就尽量避免了页面错误。
  2. 引用计数是一种简单但速度很慢的垃圾回收技术。这种方法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用计数却不为零”的情况。对垃圾回收器来说,定位这样的交互自引用的对象组所需的工作量极大。
  3. 在一些更快的模式中,垃圾回收器并非基于引用计数。它们依据的思想是:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。在这种方式下,Java虚拟机将采用一种自适应的垃圾回收技术。至于如何处理找到的存活对象,取决于不同的Java虚拟机实现。有一种做法名为stop-and-copy。显然,这意味着,先暂停程序的运行(所以它不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部都是垃圾。复制式回收器仍然会将所有内存自一处复制到另一处,这很浪费。为了避免这种情形,一些虚拟机会进行检查:钥匙没有新垃圾产生,就会转换到另一种工作模式(即“自适应”)。这种模式称为标记-清扫(mark-and-sweep),只有全部标记工作完成的时候,清理工作才会开始。没有标记的对象将被释放,不会发生任复制动作。所以省下的堆空间是不连续的。
  4. 内存分配以较大的“块”为单位。如果对象较大,它会单占用单独的块。每个块都用相应的代数来记录它是否还存活。通常,如果块在某处被引用,其代数会增加;大型对象仍然不会被复制(只是其代数会增加)。
    5. Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到“标记-清扫”方式;同样,Java虚拟机会跟踪“标记-清扫”的效果,要是堆空间出现很多碎片,就会切换回“停止-复制”方式。这就是“自适应”技术。
  • 构造器初始化:初始化顺序(在类的内部,变量定义的先后顺序决定了初始化的顺序,即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化,先是静态对象,然后是“非静态”对象)。

对象的创建过程(假设有个名为Dog的类):

  1. 即使没有显式地使用static关键字,构造器实际上也是静态方法(或者可看成静态方法)。因此,当首次创建类型为Dog的对象时,或者Dog类的静态方法/静态域首次被访问时,Java解释器必须查找类路径,以定位Dog.class文件;
  2. 然后载入Dog.class,有关静态初始化的所有动作都会执行。因此,静态初始化只在Class对象首次加载的时候进行一次;
  3. 当用new创建对象的时候,首先将在堆上位Dog对象分配足够的存储空间;
  4. 这块存储空间会被清零,这就自动地将Dog对象中的所有基本类型数据都设置成了默认值,而引用则被设置成了null;
  5. 执行所有出现于字段定义处的初始化动作;
  6. 执行构造器;

第六章 访问权限控制

  • Java 访问权限修饰词:包访问权限(无关键字,默认);public(接口访问权限);private(私有);protected(继承访问权限,同时也具有包访问权限);
  • 访问权限控制可以确保不会有任何客户端程序员依赖于某个类的底层实现的任何部分;另外,严格地遵循访问权限控制并不一定是最佳选择;

第七章 复用类

  • 初始化基类:【书:当创建一个导出类的对象时,该对象包含了一个基类的子对象…Java会自动在导出类的构造器中插入对基类构造器的调用。】,(super 关键字)
  • 对接口进行编程,优先考虑组合;
  • 空白final:可做到根据对象而有所不同,却又保持其恒定不变的特性;
1
2
3
4
5
6
7
public class BlankFinal {
private final int i = 0;
private final int j; // blank final
private fianl Poppet p; // blank final reference
public BlankFinal() { j = 1; p = new Poppet(1); }
public BlankFinal(int x) { j = x; p = new Poppet(x); }
}
  • final参数:无法在方法中更改参数引用所指向的对象;
  • 类中所有的 private() 方法都隐式地指定为是final。

第八章 多态

“封装”通过合并特征和行为来创建新的类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。而多态的作用是消除类型之间的耦合关系。
多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。

  • 书:在这里,Stage对象包含一个对Actor的引用,而Actor被初始化为HappyActor对象。这意味着performPlay()会产生某种特殊行为。既然引用在运行时可以与另一个不同的对象重新绑定起来,所以SadActor对象的引用可以在actor中被替代,然后由performPlay()产生的行为也随之改变。这样一来,程序在运行期间获得了动态灵活性(这也称作状态模式)
  • 纯继承与扩展,【书:因为只要开始考虑,就会转向,并发现扩展接口才是解决特定问题的完美方案…一旦向上转型,就不能调用那些新方法。】
  • 向下转型与运行时类型识别(RTTI),如果所转类型是正确的类型,那么转型成功,否则就会返回一个ClassCastException异常。

第九章 接口

  • 抽象类和抽象方法:为它的所有导出类创建一个通用接口;抽象类还是很有用的重构工具,因为它们使得我们可以很容易地将公共方法沿着继承层次结构向上移动
  • 完全解耦:【书:只要一个方法操作的是类而非接口,那么你就只能使用这个类及其子类。如果你想要将这个方法应用于不在此继承结构中的某个类…则接口可以在很大程度上放宽这种限制,因此,它使得我们可编写可复用性更好的代码。】
  • 策略设计模式:创建一个能够根据所传递的参数对象的不同而具有不同行为的方法。这类方法包含所要执行的算法中固定不变的部分,而“策略”包含变化的部分;策略就是传递进去的参数对象。下面,Processor对象就是一个策略;可以看到有两种策略应用到了String类型的s对象上。(P175)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Processor {
public String name() { return getClass().getSimpleName(); }
Object process(Object input) { return input; }
}
class Upcase extends Processor {
String process(Object input) { return ((String)input).toUpperCase(); }
}
class Downcase extends Processor {
String process(Object input) { return ((String)input).toLowerCse(); }
}
public Class Apply {
public static void process(Processor p, Object s) { // 太耦合
...
}
public static String s = "Disagreement with ...";
public static void main(String[] args) {
process(new Upcase(), s);
process(new Downcase(), s);
}
}
  • 通过继承来扩展接口,并尽量避免命名的冲突;
  • 适配接口,通过使用interface关键字提供的伪多重继承机制,我们既可以是A类又可以是B类的新类;
  • 接口中的域,放入接口中的任何域都自动是static和final的;
  • 嵌套接口?
  • 接口与工厂,【书:我们在工厂对象上调用的是创建方法,而该工厂对象将生成接口的某个实现的对象。】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Service {
void method1();
void method2();
}
interface ServiceFactory { Service getService(); }
class Implementation1 implements Service {
Implementation1();
public void method1();
public void method2();
}
class Implementation1Factory implements ServiceFactory {
public Service getService() {
return new Implementation1();
}
}

第十章 内部类

  • 内部类看起来就像一种代码隐藏机制,除此,内部类还能了解外围类,并与之通信;
  • 链接到外部类,可以使用外部类的名字后面紧跟圆点和this;
  • .new语法
1
2
3
4
5
6
7
public class DotNew {
public class Inner() {}
public static void main(String args) {
DotNew dn = new DotNew();
DotNew.Inner dni = dn.new Inner();
}
}
  • 内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Parcel4 {
private class PContents implements Contents {
private int i = 11;
public int value() { return i; }
}
protected class PDestination implements Destination {
private String label;
private PDestination(String whereTo) { label = whereTo }
public String readLabel() { return label; }
}
public Destination destination(String s) { return new PDestination(s); }
public Contents contents() { return new PContents(); }
}
public class TestParcel {
public static void main(String[] args) {
Parcel4 p = new Parcel4();
Contents c = p.contents();
Destination d = p.destination("Tasmania");
}
}
  • 匿名内部类与工厂方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Service {
void method1();
void method2();
}
interface ServiceFactory { Service getService(); }
class Implementation1 implements Service {
private Implementation1();
public void method1() { print("Implementation1 method1"); }
public void method2() { print("Implementation1 method2"); }
public static ServiceFactory factory =
new ServiceFactory() {
public ServiceFactory() {
return new Implementation1();
}
}
}
  • 接口内部的类

为什么需要内部类?
如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整。
接口解决了部分问题,而内部类有效地实现了“多重继承”。即内部类允许继承多个非接口类型(类或抽象类)

  • 闭包与回调: 通过内部类提供闭包的功能是优良的解决方案,它比指针更灵活、更安全。

内部类与控制框架(一类特殊的应用程序框架,用来解决响应事件的需求。主要用来响应事件的系统被称作事件驱动系统。)
请考虑这样一个控制框架,它的工作就是在事件“就绪”的时候执行事件。虽然“就绪”可以指任何事,但在本例中是指基于时间触发的事件。控制框架并不包含任何具体的信息。那些信息在实现算法的action()部分时,通过继承来提供:
首先,接口描述了要控制的事件。因为其默认的行为是基于时间去执行控制,所以使用抽象类代替实际的接口。
控制框架的完整实现是由单个的类创建的,从而使得实现的细节被封装起来;内部类用来表示解决问题所必需的各种不同的action();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class Event {
private long eventTime;
protected final long delayTime;
public Event(long delayTime) {
this.delayTime = delayTime;
start();
}
public void start() { eventTime = System.nanoTime() + delayTime; }
public boolean ready() { return System.nanoTime() >= eventTime; }
public abstract void action();
}
public class GreenhouseControls extends Controller {
...
public class LightOn extends Event {
...
}
public class LightOff extends Event {
...
}
}
  • 内部类的继承
  • 内部类可以被覆盖吗?(实际上,“覆盖”后的内部类是完全独立的两个实体,各自在自己的命名空间内。)
  • 局部内部类(典型的方式是在方法体里面创建)
  • 内部类标识(OuterClass$InnerClass)

第十一章 持有对象

Java容器类库的用途是“保存对象”。
新程序中不应该使用过时的Vector、Hashtable和Stack。

Java容器全息图

第十二章 通过异常处理错误

异常使得我们可以将每件事都当作一个事务来考虑。

  • 举一个抛出异常的简单例子。对于对象引用t,传给你的时候可能尚未被初始化。所以在使用这个对象引用调用其方法之前,会先对引用进行检查。可以创建一个代表错误信息的对象,并且将它从当前环境中“抛出”,这样就把错误信息传播到了“更大”的环境中。这被称为抛出一个异常。
  • 捕获异常 -> 监控区域(try-catch):一段可能产生异常的代码,并且后面跟着处理这些异常的代码。
  • 终止与恢复:长久以来,尽管程序员们使用的操作系统支持恢复模型的异常处理,但他们最终还是转向使用类似“终止模型”的代码,并且忽略恢复行为。所以虽然恢复模型开始显得很吸引人,但不是很实用。其中的主要原因可能是它所导致的耦合。
  • 异常说明:使用关键字throws,后面接一个所有潜在异常类型的列表。
  • 不过还是有个能“作弊”的地方:可以声明方法将抛出异常,实际上却不抛出。编译器相信了这个声明,并强制此方法的用户像针的抛出异常那样使用这个方法。这样做的好处是,为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽象基类和接口时,这种能力很重要。
  • 异常链:在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称为异常链。有趣的是,在Throwable的子类中,只有三种基本的异常类提供了带cause参数的构造器。它们是Error、Exception以及RuntimeException。如果要把其他类型的异常链接起来,应该使用initCause()方法而不是构造器。
  • 用finally子句进行清理。
  • 对于在构造阶段可能会抛出异常,并且要求清理的类,最安全的使用方式是使用嵌套的try子句。
  • 抛出异常的时候,异常处理系统会按照代码的书写顺序找出“最近”的处理程序。找到匹配的处理程序之后,它就认为异常将得到处理,然后就不再继续查找。

异常使用指南

  1. 在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常)
  2. 解决问题并且重新调用产生异常的方法。
  3. 进行少许修补,然后绕过异常发生的地方继续执行。
  4. 用别的数据进行计算,以代替方法预计会返回的值。
  5. 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。
  6. 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。
  7. 终止程序。
  8. 进行简化,让类库和程序更安全。

第十三章 字符串


第十四章 类型信息

迄今为止,已知的RTTI形式包括:

  1. 传统的类型转换;
  2. 代表对象的类型的Class对象。通过查询Class对象可获取运行时所需的信息;
  3. 关键字instanceof。
  • 反射:运行时的类信息;
  • 代理
  • 空对象
  • 接口与类型信息:interface关键字的一种重要目标就是允许程序员隔离构件,进而降低耦合性。但是通过类型信息,这种耦合性还是会传播出去,接口并非是对解耦的一种无懈可击的保障。
1
2
3
4
5
6
7
8
class C implements A {
public void f() { ... }
}
public class HiddenC {
public static A makeA() {
return new C();
}
}

第十五章 泛型