Java编程思想(第四版)Thinking in Java 4th 读书笔记

目录

  • 前言
    • 配书代码使用指南
  • 第1章 对象导论
    • 1.2接口
    • 1.4 被隐藏的具体实现
    • 1.5 复用具体实现
    • 1.7 多态
    • 1.8 单根继承
  • 第2章 一切都是对象 object
    • 2.2 必须由你创建所有对象
      • 2.2.1 五个地方存储数据
      • 2.2.2 特例:基本类型
    • 2.4 类
      • 2.4.1 字段和方法
    • 2.6 构建一个Java程序
      • 2.6.1 名字可见性
    • 2.7 你的第一个Java程序
    • 2.8 注释和嵌入式文档
  • 第3章 操作符 operators
    • 3.1 更简单的打印语句
    • 3.4 赋值
    • 3.6 自动递增和递减
    • 3.7 关系操作符
      • 3.7.1 测试对象的等价性
  • 第4章 控制执行流程 control
      • 4.3.3 逗号操作符
    • 4.7 臭名昭著的goto
  • 第5章 初始化与清理 initialization
    • 5.2 方法重载
      • 5.2.1 区分重载方法
      • 5.2.3 以返回值区分重载方法
    • 5.3 默认构造器
    • 5.4 this关键字
    • 5.5 清理:终结处理和垃圾回收
      • 5.5.1 finalize()
      • 5.5.4 垃圾回收器如何工作
    • 5.7 构造器初始化
      • 5.7.2 静态数据的初始化
    • 5.8 数组初始化
      • 5.8.1 可变参数列表
  • 第6章 访问控制权限 access
    • 6.1 包:库单元
      • 6.1.2 创建独一无二的包名
      • 6.1.5 对使用包的忠告
    • 6.2 Java访问权限修饰词
    • 6.4 类的访问权限
  • 第7章 复用类 reusing
    • 7.2 继承语法
      • 7.2.1 初始化基类
    • 7.3 代理
    • 7.4 结合使用组合和继承
      • 7.4.1 确保正确清理
    • 7.5 在组合与继承之间选择
    • 7.7 向上转型
    • 7.7 final
      • 7.7.1 final数据
        • 空白final
        • final参数
      • 7.8.2 final方法
      • 7.8.3 final类
    • 7.9 初始化及类的加载
  • 第8章 多态 polymorphism
    • 8.2 转机
      • 8.2.1 方法调用绑定
      • 8.2.5 缺陷:域与静态方法
    • 8.3 构造器和多态
      • 8.3.2 继承与清理
      • 8.3.3 构造器内部的多态方法的行为
    • 8.4 协变返回类型
    • 8.5 用继承进行设计
      • 继承和组合该如何选择?
      • 8.5.2 向下转型与运行时类型识别(RTTI)
  • 第9章 接口 interfaces
    • 9.1 抽象类和抽象方法
    • 9.2 接口
    • 9.3 完全解耦
      • 策略模式
      • 适配器模式
    • 9.4 Java中的多重继承
    • 9.5 通过继承来扩展接口
      • 9.5.1 组合接口时的名字冲突
    • 9.6 适配接口
    • 9.7 接口中的域
    • 9.8 嵌套接口
    • 9.9 接口与工厂
    • 9.10 总结
  • 第10章 内部类 innerclasses
  • 第11章 持有对象 holding
  • 第12章 异常 exceptions
  • 第13章 字符串 strings
  • 第14章 类型信息 typeinfo
  • 第15章 泛型 generics
  • 第16章 数组 arrays
  • 第17章 容器 containers
  • 第18章 Java I/O系统 io
  • 第19章 枚举类型 enumerated
  • 第20章 注解 annotations
  • 第21章 并发 concurrency
  • 第22章 图形化用户界面 gui


前言

《Java 编程思想》第四版基于 JAVA 5 版本;《On Java 8》 基于 JAVA 8 版本。都是由作者 [美] Bruce Eckel所著,建议直接看新书《On Java 8》,它是是事实上的 《Java 编程思想》第五版。本博文我只记录1-9章的读书笔记,后面章节我将直接看《On Java 8》并另开博文记录。

配书代码使用指南

参考:Java编程思想配书代码使用指南
我的补充,以mac为例

  • 配置java环境变量
cd ~
vim .bash_profile
esc   
:wq
source .bash_profile
  • 依赖jar包存放目录:/Users/chenxu/Documents/code/jars
    只依赖两个jar包:javassist.jar和net.jar
  • 适配非JavaSE5环境
    由于作者使用的是JavaSE5,而我用的JavaSE8,所以需要在所有的build.xml(总工程和每章都有一个)中,注释掉下面一行,然后就可以顺利ant build(构建所有当前目录下的java文件),再ant run 运行了,可以单独运行一个类,例如ant run ForEachInt
<fail message="J2SE5 required" unless="version1.5"/>


第1章 对象导论

1.2接口

接口确定了对某一特定对象所能发出的请求。
UML(Unified Modelling Language,统一建模语言)图

1.4 被隐藏的具体实现

访问权限
protected:继承的类可以访问
默认:包访问权限,同一个包中的类可以访问

1.5 复用具体实现

has-a关系:组合(composition)或者聚合(aggregation)

1.7 多态

后期绑定来实现多态:当向对象发送消息时,被调用的代码直到运行时才能确定。
Java中,动态绑定是默认行为。
C++中,动态绑定不是默认行为,用关键字virtual来实现。
向上转型(upcasting)将子类看成父类。

1.8 单根继承

保证所有对象都具备某些功能,因此可以更容易对所有对象进行某些操作。比如:堆上创建,参数传递,垃圾回收器的实现,异常处理。

第2章 一切都是对象 object

2.2 必须由你创建所有对象

2.2.1 五个地方存储数据

  • 寄存器。最快,位于处理器内部,很小,你不能直接控制(C和C++可以)
  • 堆栈stack。次快,位于通用RAM(随机访问存储器)中,通过堆栈指针上下移动来释放和分配内存。因为创建程序时,Java系统必须知道存储在堆栈内的所有项的确切生命周期,以便上下移动堆栈指针,所以不够灵活。用于存储对象引用,不存储对象。
  • 堆heap。速度比栈慢,位于通用RAM,存放所有Java对象,比堆栈灵活,编译器不需要知道存储的数据在堆上存活多长时间。
  • 常量存储。常量值通常存放在程序代码内部,在嵌入性系统中,也可以放在ROM(只读存取器)中。
  • 非RAM存储。数据存活于程序之外,如流对象和持久化对象。JDBC和Hibernate支持对数据库的存储和读取。

2.2.2 特例:基本类型

  • 基本类型不用new来创建变量,而是创建一个并非是引用的“自动”变量,这个变量直接存储“值”,并置于堆栈中,更高效。

  • 基本类型所占存储空间大小确定,不随机器硬件架构的变化而变化,因此比其他语言更具可移植性。
    Java编程思想(第四版)Thinking in Java 4th 读书笔记_第1张图片

  • 高精度数字。两个包装器类型。
    BigInteger:支持任意精度的整数。
    BigDecimal:支持任意精度的定点数。

2.4 类

2.4.1 字段和方法

  • 类的某个成员如果是基本数据类型,即使没有初始化也会自动获得一个默认值。这种情况不适用于局部变量(即并非某个类的字段)。
    Java编程思想(第四版)Thinking in Java 4th 读书笔记_第2张图片

2.6 构建一个Java程序

2.6.1 名字可见性

C++引入了名字空间。
Java希望用自己的域名反转来保证类库的命名独一无二。如域名是MindView.net,类类库名为net.mindview.utility。

2.7 你的第一个Java程序

有一个特定类会自动导入到每一个Java文件中:java.lang。
查找sun公司文档

2.8 注释和嵌入式文档

用到时再看

第3章 操作符 operators

3.1 更简单的打印语句

作者自己搞了一个类库,简化了打印语句:net.mindview.util.Print

3.4 赋值

基本数据类型赋值:内容传递
对象赋值:引用传递
对象作为方法的实参:引用传递

3.6 自动递增和递减

  • 前缀式:++a,先执行运算,再生成值,++a=a+1
  • 后缀式:a++,先生成值,再执行运算,a++=a

3.7 关系操作符

3.7.1 测试对象的等价性

  • ==和!=比较的是对象的引用
  • 比较对象的实际内容需要重写对象的equals()方法,此方法默认行为是比较引用,大多数Java类库都实现了equals()方法来比较对象的内容。
  • equals()方法不适用于基本数据类型

第4章 控制执行流程 control

4.3.3 逗号操作符

Java里唯一用到逗号操作符的地方就是for循环的控制表达式,在初始化和步进控制部分,可以在for语句内定义多个变量,但它们必须是相同类型。

4.7 臭名昭著的goto

  • Java的标签在功能上类似于goto
  • 标签是后面跟有冒号的标识符
    label1:
  • Java里使用标签的唯一理由是:当有嵌套循环存在时,需要从多层嵌套中break和continue,比如从内层循环直接跳出或中断外层循环。
  • 标签只能放在迭代语句之前,而且在标签和迭代之间不能插入任何语句。

第5章 初始化与清理 initialization

5.2 方法重载

5.2.1 区分重载方法

名字和参数类型列表

5.2.3 以返回值区分重载方法

不行,因为有时候调用方法只是为了副作用而不关心其返回值,比如以下调用就无法区分到底该调用哪个方法
f()

void f(){}
int f(){return 1;}

5.3 默认构造器

如果你已经定义了一个构造器,编译器就不会再帮你自动创建默认构造器

5.4 this关键字

  • this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。
  • 只有当需要明确指出对当前对象的引用时,才需要使用this关键字。
public class Leaf {
  int i = 0;
  Leaf increment() {
    i++;
    return this;
  }
}
  • this关键字对于将当前对象传递给其他类的方法时也很有用。
class Person {
  public void eat(Apple apple) {
    Apple peeled = apple.getPeeled();
    System.out.println("Yummy");
  }
}

class Peeler {
  static Apple peel(Apple apple) {
    // ... remove peel
    return apple; // Peeled
  }
}

class Apple {
  Apple getPeeled() { return Peeler.peel(this); }
}

public class PassingThis {
  public static void main(String[] args) {
    new Person().eat(new Apple());
  }
}
  • 在构造器中调用另一个构造器也需要this关键字。
    可以用this调用一个构造器,但却不能调用两个。
    必须将构造器调用置于最起始处,否则编译器会报错。
  • 当参数s的名称和数据成员s的名称相同时,为了避免歧义,可以用this.s来表示对数据成员s的调用。
  • static方法就是没有this的方法。在static方法内部不能调用非静态方法。
  • static方法不是面向对象的,如果代码中出现大量static方法,就该重新考虑自己的设计了。

5.5 清理:终结处理和垃圾回收

5.5.1 finalize()

  • 当垃圾回收器准备回收对象时,会首先调用其finalize()方法。
  • 不同于C++的析构函数(C++中销毁对象必须用到这个函数),在C++中,对象一定会被销毁。
  • Java中对象不一定会销毁,因为垃圾回收不一定会运行,即使运行也不一定会销毁你的对象。
  • 垃圾回收只与内存有关。
  • 用途之一:在finalize()中用本地方法释放非Java代码创建的对象。
  • 用途之二:对象终结条件的验证。

5.5.4 垃圾回收器如何工作

  • C++的堆像一个院子,Java的堆像一个传送带。相率相当。
  • 引用计数的缺陷:无法解决循环引用问题。
  • Java虚拟机采用自适应垃圾回收技术:在停止-复制(stop-and-copy)和标记-清扫(mark-and-sweep)两种模式之间自动切换。
  • 停止-复制:得有两个堆,当垃圾较多时用,效率高。
  • 标记-清扫:剩下的堆空间不连续,当垃圾较少时用。
  • 早期虚拟机版本中,两种模式都需要暂停程序,都不是在后台运行的。
  • 垃圾回收器如何处理循环引用

5.7 构造器初始化

顺序:先成员再方法,先静态再非静态

5.7.2 静态数据的初始化

总结一下对象的创建过程
Java编程思想(第四版)Thinking in Java 4th 读书笔记_第3张图片

5.8 数组初始化

可以用new来创建并初始化一个不确定元素个数的数组。数组元素中的基本数据类型会自动初始化为空值。(0,false)

Random rand = new Random(47);
int[] a = new int[rand.nextInt(20)];

5.8.1 可变参数列表

只能传入数组,Java SE5之前的写法

void printArray(Object[] args) {}
printArray(new Object[]{"one", "two", "three" });

传入数组和非数组都行,方便很多,Java SE5之后的新特性。
将0个参数传递给可变参数列表是可行的。

void printArray(Object... args) {} 
printArray(new Object[]{"one", "two", "three" });
printArray("one", "two", "three");
printArray(); // Empty list is OK

重载方法时,最好只让最多一个方法使用可变参数列表,否则很容易出问题。

第6章 访问控制权限 access

6.1 包:库单元

相同目录下的所有不具有明确package声明的文件,都被视为是该目录下默认包的一部分。

6.1.2 创建独一无二的包名

Java解析器的运行过程如下
Java编程思想(第四版)Thinking in Java 4th 读书笔记_第4张图片

6.1.5 对使用包的忠告

  • 包必须位于其名称所指定的目录之中,而该目录必须是在以CLASSPATH开始的目录中可以查询到的。
  • 如果在运行时告知无法找到特定的类的错误,可以尝试注释掉package语句试一下,如果又能运行了,说明可能是包放错了目录。

6.2 Java访问权限修饰词

  • 放权权限大小:public>protected(子类可以访问、也有包访问权限)>包访问权限(默认)>private
  • 每个文件只能有一个public class,且类名必须与文件名相同

6.4 类的访问权限

  • 尽量将域设为private,只公开方法
  • 单例设计模式
class Soup2 {
  private Soup2() {} // 私有化构造方法,无法从这里创建对象
  private static Soup2 ps1 = new Soup2(); // 静态实例ps1是唯一的
  public static Soup2 access() { // 尽量将域设为private,只公开方法
    return ps1; // 只能通过这个静态方法,创建唯一一个静态实例ps1
  }
  public void f() {}
}

第7章 复用类 reusing

7.2 继承语法

  • 在每个类都设置一个main()方法可使每个类的单元测试都变得简便易行。
  • 即使一个程序中多个类都有main()方法,也只有命令行调用的那个类的main()方法会被调用。
  • 即使一个类只具有包访问权限,其public main()仍然是可以访问的。
  • 为了继承,一般的规则是将所有的数据成员都指定为private,将所有方法都指定为public。

7.2.1 初始化基类

  • 当创建一个导出类的对象时,该对象包含了一个基类的子对象,调用基类构造器可以对子对象进行初始化。这个子对象与你用基类直接创建的对象是一样的。二者的区别在于,后者来自于外部,而基类的子对象被包装在导出类对象内部。
  • Java会自动在导出类的构造器中插入对基类构造器的调用。
  • 如果不使用默认的构造器,或者想使用一个带参数的构造器,那么必须用关键字super显示地调用基类构造器,且这个调用必须是导出类构造器中要做的第一件事,否则会报错。

7.3 代理

代理是继承与组合之间的中庸之道,因为我们将一个成员对象置于所要构造的类中(就像组合),但与此同时我们在新类中暴露了该成员对象的所有方法(就像继承)。

7.4 结合使用组合和继承

虽然编译器强制你去初始化基类,但是它并不监督你必须将基类的成员对象也初始化,因此在这一点上你自己必须时刻注意。

7.4.1 确保正确清理

  • 必须将清理动作置于finally子句之中,以防异常的出现。
  • 首先,执行类的所有特定的清理动作,其顺序与生成顺序相反;然后调用基类的清理方法。
  • 如果需要进行清理,最好编写自己的清理方法,不要使用finalize()。

7.5 在组合与继承之间选择

  • 组合:通常用于想在新类中使用现有类的功能而非它的接口这种情形。
  • 继承:使用某个现有类,并开发一个它的特殊版本。

7.7 向上转型

  • 向上转型:由导出类转型为基类,用于实现多态特性。
  • 继承不太常用,如果必须向上转型才使用继承,否则一般不考虑继承。

7.7 final

7.7.1 final数据

一个永不改变的编译时常量

  private final int valueOne = 9;
  private static final int VALUE_TWO = 99;
  public static final int VALUE_THREE = 39;

一个在运行时被初始化的值,而你不希望它被改变

  private final int i4 = rand.nextInt(20);
  static final int INT_5 = rand.nextInt(20);
  • 对于基本类型,final使数值恒定不变
  • 对于对象引用,final使引用恒定不变,一旦引用初始化指向一个对象,就不能再改为指向另一个对象。然而,对象其自身却是可以被修改的。这一限制同样适用于数组,它也是对象。
空白final
  • 指被声明为final但又未给定初始值的域
  • 可以做到域根据对象而有所不同,却又保持其恒定不变的特性
  • 在构造器中初始化,因为final域在使用前必须被初始化
final参数
  • 如果参数为对象引用,则你无法在方法中更改参数引用所指向的对象
  • 如果参数为基本类型,则参数为只读,这一特性主要用来向匿名内部类传递数据

7.8.2 final方法

  • 唯一用途:把方法锁定,以防任何继承类重写它
  • 类中所有的private方法都隐式地指定为是final的,因此对private方法添加final修饰词是多此一举的
  • 如果在子类中写了一个与基类的private方法或final方法同名的方法,则该方法是一个新方法,而非对基类方法的重写。

7.8.3 final类

  • 无法继承该类
  • 该类的域可以选择是或不是final域
  • 该类的所有方法都隐式地指定为是final的

7.9 初始化及类的加载

  1. 按照声明顺序初始化父类中的静态变量
  2. 按照声明顺序初始化子类中的静态变量
  3. 将为对象分配的内存设为二进制零值,因此对象的所有基本类型成员都会被设为默认值,对象引用成员会被设为null
  4. 按照声明顺序初始化父类中的实例变量
  5. 父类的构造器被调用
  6. 按照声明顺序初始化子类中的实例变量
  7. 子类的构造器被调用

示例代码

//: reusing/Beetle.java
// The full process of initialization.
import static net.mindview.util.Print.*;

class Insect {
  private int i = 9;
  protected int j;
  Insect() {
    print("i = " + i + ", j = " + j);
    j = 39;
  }
  private static int x1 =
    printInit("static Insect.x1 initialized");
  static int printInit(String s) {
    print(s);
    return 47;
  }
}

public class Beetle extends Insect {
  private int k = printInit("Beetle.k initialized");
  public Beetle() {
    print("k = " + k);
    print("j = " + j);
  }
  private static int x2 =
    printInit("static Beetle.x2 initialized");
  public static void main(String[] args) {
    print("Beetle constructor");
    Beetle b = new Beetle();
  }
} /* Output:
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
*///:~

第8章 多态 polymorphism

  • oop有三大特征:封装,继承,多态。
  • 多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。
  • 多态可以创建可扩展的程序。
  • 多态可以消除类型之间的耦合关系。

8.2 转机

8.2.1 方法调用绑定

  • 将一个方法调用同一个方法主体关联起来被称为绑定。
  • 若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定。
  • 后期绑定,也叫做动态绑定或运行时绑定。
  • Java中除了static方法和final方法(private方法属于final方法)之外,其他所有方法都是后期绑定。Java用后期绑定来实现多态。

8.2.5 缺陷:域与静态方法

  • 只有普通方法的调用可以是多态的
  • 域的访问将在编译器进行解析,无法实现多态;而且你通常会将所有的域都设置为private,只通过方法来访问它们;另外,一般不会对基类中的域和子类中的域取相同的名字。
  • 静态方法的调用也不是多态的,因为静态方法与类,而不是与对象相关联。

8.3 构造器和多态

8.3.2 继承与清理

当某个成员对象被这个类的多个对象共享时,如果要对此成员对象进行必要的清理工作,就需要使用引用计数来跟踪仍旧访问着此共享对象的对象数量了。

//: polymorphism/ReferenceCounting.java
// Cleaning up shared member objects.
import static net.mindview.util.Print.*;

class Shared {
  private int refcount = 0;
  private static long counter = 0;
  private final long id = counter++;
  public Shared() {
    print("Creating " + this);
  }
  public void addRef() { refcount++; }
  protected void dispose() {
    if(--refcount == 0)
      print("Disposing " + this);
  }
  public String toString() { return "Shared " + id; }
}

class Composing {
  private Shared shared;
  private static long counter = 0;
  private final long id = counter++;
  public Composing(Shared shared) {
    print("Creating " + this);
    this.shared = shared;
    this.shared.addRef();
  }
  protected void dispose() {
    print("disposing " + this);
    shared.dispose();
  }
  public String toString() { return "Composing " + id; }
}

public class ReferenceCounting {
  public static void main(String[] args) {
    Shared shared = new Shared();
    Composing[] composing = { new Composing(shared),
      new Composing(shared), new Composing(shared),
      new Composing(shared), new Composing(shared) };
    for(Composing c : composing)
      c.dispose();
  }
} /* Output:
Creating Shared 0
Creating Composing 0
Creating Composing 1
Creating Composing 2
Creating Composing 3
Creating Composing 4
disposing Composing 0
disposing Composing 1
disposing Composing 2
disposing Composing 3
disposing Composing 4
Disposing Shared 0
*///:~

8.3.3 构造器内部的多态方法的行为

  • 动态绑定的调用是在运行时才决定的,因为对象无法知道它是属于方法所在的那个类,还是属于那个类的子类。
  • 如果在父类构造器内部调用它的一个动态绑定方法(已被子类重写),则实际调用的是被重写的子类方法,而非父类的方法(虽然很奇怪,但事实如此),而此时正在初始化父类对象,子类对象还未初始化,因此子类的非静态成员也未初始化,如果这个子类方法又操纵了还未初始化的成员就肯定会导致难以发现的错误,代码示例如下:
//: polymorphism/PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect.
import static net.mindview.util.Print.*;

class Glyph {
  void draw() { print("Glyph.draw()"); }
  Glyph() {
    print("Glyph() before draw()");
    draw();
    print("Glyph() after draw()");
  }
}	

class RoundGlyph extends Glyph {
  private int radius = 1;
  RoundGlyph(int r) {
    radius = r;
    print("RoundGlyph.RoundGlyph(), radius = " + radius);
  }
  void draw() {
    print("RoundGlyph.draw(), radius = " + radius);
  }
}	

public class PolyConstructors {
  public static void main(String[] args) {
    new RoundGlyph(5);
  }
} /* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*///:~

  • 因此,编写构造器有一条准则:“用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其他方法”
  • 在构造器中唯一能够安全调用的方法是final方法(也适用于private方法,它们自动属于final方法),因为final方法无法被重写,就不会导致上面的错误。

8.4 协变返回类型

Java5中添加了协变返回类型,即子类的重写方法的返回值类型可以是父类方法的返回值类型的子类型

//: polymorphism/CovariantReturn.java

class Grain {
  public String toString() { return "Grain"; }
}

class Wheat extends Grain {
  public String toString() { return "Wheat"; }
}

class Mill {
  Grain process() { return new Grain(); }
}

class WheatMill extends Mill {
  Wheat process() { return new Wheat(); }
}

public class CovariantReturn {
  public static void main(String[] args) {
    Mill m = new Mill();
    Grain g = m.process();
    System.out.println(g);
    m = new WheatMill();
    g = m.process();
    System.out.println(g);
  }
} /* Output:
Grain
Wheat
*///:~

8.5 用继承进行设计

继承和组合该如何选择?

  • 一条通用的准则是:“用继承表达行为间的差异,并用组合表达状态上的变化(通过改变成员变量的值或者引用)”。
  • 在下面的例子中,两者都用到了:通过继承得到两个不同的类,用于表达act()方法的差异;而Stage通过运用组合使自己的状态发生变化。在这种情况下,这种状态的改变也就产生了行为的改变。这也称作状态模式
//: polymorphism/Transmogrify.java
// Dynamically changing the behavior of an object
// via composition (the "State" design pattern).
import static net.mindview.util.Print.*;

class Actor {
  public void act() {}
}

class HappyActor extends Actor {
  public void act() { print("HappyActor"); }
}

class SadActor extends Actor {
  public void act() { print("SadActor"); }
}

class Stage {
  private Actor actor = new HappyActor();
  public void change() { actor = new SadActor(); }
  public void performPlay() { actor.act(); }
}

public class Transmogrify {
  public static void main(String[] args) {
    Stage stage = new Stage();
    stage.performPlay();
    stage.change();
    stage.performPlay();
  }
} /* Output:
HappyActor
SadActor
*///:~

8.5.2 向下转型与运行时类型识别(RTTI)

在Java中,所有转型都会在运行时得到检查,如果不是我们希望的类型,就会返回一个ClassCastException(类转型异常)。

第9章 接口 interfaces

9.1 抽象类和抽象方法

  • 抽象方法:用abstract关键字修饰,仅有声明而没有方法体。
  • 抽象类:用abstract关键字修饰,不能创建对象,可以有抽象方法也可以没有抽象方法。

9.2 接口

  • 接口用interface关键字修饰,是一个完全抽象的类,没有任何具体实现。
  • 接口有public和包访问两种可视性。
  • 接口还弥补了Java单继承的不足,实现多个接口提供了类似多重继承变种的特性。
  • 接口也可以包含域,但是这些域隐式地是static和final的。
  • 接口中的方法默认为public(不需要显示声明),所以实现中的方法也应为public(需要显示声明),否则就降低了方法的可访问权限,而这是Java编译器不允许的。

9.3 完全解耦

策略模式

  • 创建一个能够根据所传递的参数对象的不同而具有不同行为的方法,被称为策略模式。
  • 这类方法包含要执行的算法中固定不变的部分,而策略包含变化的部分,策略就是传递进去的参数对象,它包含要执行的代码。
  • 在下例中,Processor对象就是一个策略,在main()中可以看到三种不同类型的策略应用到了String类型的s对象上。
//: interfaces/classprocessor/Apply.java
package interfaces.classprocessor;
import java.util.*;
import static net.mindview.util.Print.*;

class Processor {
  public String name() {
    return getClass().getSimpleName();
  }
  Object process(Object input) { return input; }
}	

class Upcase extends Processor {
  String process(Object input) { // Covariant return
    return ((String)input).toUpperCase();
  }
}

class Downcase extends Processor {
  String process(Object input) {
    return ((String)input).toLowerCase();
  }
}

class Splitter extends Processor {
  String process(Object input) {
    // The split() argument divides a String into pieces:
    return Arrays.toString(((String)input).split(" "));
  }
}	

public class Apply {
  public static void process(Processor p, Object s) {
    print("Using Processor " + p.name());
    print(p.process(s));
  }
  public static String s =
    "Disagreement with beliefs is by definition incorrect";
  public static void main(String[] args) {
    process(new Upcase(), s);
    process(new Downcase(), s);
    process(new Splitter(), s);
  }
} /* Output:
Using Processor Upcase
DISAGREEMENT WITH BELIEFS IS BY DEFINITION INCORRECT
Using Processor Downcase
disagreement with beliefs is by definition incorrect
Using Processor Splitter
[Disagreement, with, beliefs, is, by, definition, incorrect]
*///:~

适配器模式

  • 适用场景:你拥有的类需要实现某个接口,而你无法修改这个类,比如它是某个第三方类库的类。这时,你可以用适配器模式来进行接口转换或者适配。
  • 适配器接受你拥有的类或者接口,并产生你需要的接口(通过实现这个接口)
  • 下面示例中,适配器FilterAdapter的构造器接受你所拥有的接口Filter,然后生成具有你所有需要的Processor接口的对象。
//: interfaces/interfaceprocessor/FilterProcessor.java
package interfaces.interfaceprocessor;
import interfaces.filters.*;

class FilterAdapter implements Processor {
  Filter filter;
  public FilterAdapter(Filter filter) {
    this.filter = filter;
  }
  public String name() { return filter.name(); }
  public Waveform process(Object input) {
    return filter.process((Waveform)input);
  }
}	

public class FilterProcessor {
  public static void main(String[] args) {
    Waveform w = new Waveform();
    Apply.process(new FilterAdapter(new LowPass(1.0)), w);
    Apply.process(new FilterAdapter(new HighPass(2.0)), w);
    Apply.process(
      new FilterAdapter(new BandPass(3.0, 4.0)), w);
  }
} /* Output:
Using Processor LowPass
Waveform 0
Using Processor HighPass
Waveform 0
Using Processor BandPass
Waveform 0
*///:~

9.4 Java中的多重继承

Java编程思想(第四版)Thinking in Java 4th 读书笔记_第5张图片

  • 可以继承任意多个接口,并可以向上转型为每个接口。
  • 使用接口的核心原因:为了能够向上转型为多个基类型。
  • 使用接口的第二个原因与使用抽象基类相同:防止客户端程序员创建该类的对象。
  • 如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。

9.5 通过继承来扩展接口

一个子类只能继承一个父类,但一个子接口可以继承多个父接口

interface Vampire extends DangerousMonster, Lethal {
  void drinkBlood();
}

9.5.1 组合接口时的名字冲突

在打算组合的不同接口中使用相同的方法名通常会造成代码可读性的混乱,请尽量避免这种情况。

//: interfaces/InterfaceCollision.java
package interfaces;

interface I1 { void f(); }
interface I2 { int f(int i); }
interface I3 { int f(); }
class C { public int f() { return 1; } }

class C2 implements I1, I2 {
  public void f() {}
  public int f(int i) { return 1; } // overloaded
}

class C3 extends C implements I2 {
  public int f(int i) { return 1; } // overloaded
}

class C4 extends C implements I3 {
  // Identical, no problem:
  public int f() { return 1; }
}

// Methods differ only by return type:
//! class C5 extends C implements I1 {}
//! interface I4 extends I1, I3 {} ///:~

9.6 适配接口

  • 接口的一种常见用法就是前面提到的策略设计模式,此时你编写一个执行某些操作的方法,而该方法将接受一个同样是你指定的接口。例如,Java的Scanner类的构造器接受的就是一个Readable接口。通过这种方式,Scanner可以作用于更多的类型。
  • 用适配器模式可以在任何现有类之上添加新的接口,所以这意味着让方法接受接口类型,是一种让任何类都可以对该方法进行适配的方式。这就是使用接口而不是类的强大之处。

9.7 接口中的域

  • 接口中的域都自动是static、final和public的,因此可以很方便的用来创建常量。
  • Java SE5之前用接口创建常量组来实现enum功能,之后已被enum关键字替代。
  • 这些域不是接口的一部分,它们的值被存储在该接口的静态存储区域内。

9.8 嵌套接口

接口可以嵌套在类或其他接口中。这个特性还没发现有什么具体用途,知道有这个特性就行,不用了解细节。

9.9 接口与工厂

9.10 总结

恰当的原则应该是优先选择类而不是接口。从类开始,如果接口的必要性变得非常明确,那么就进行重构。

第10章 内部类 innerclasses

第11章 持有对象 holding

第12章 异常 exceptions

第13章 字符串 strings

第14章 类型信息 typeinfo

第15章 泛型 generics

第16章 数组 arrays

第17章 容器 containers

第18章 Java I/O系统 io

第19章 枚举类型 enumerated

第20章 注解 annotations

第21章 并发 concurrency

第22章 图形化用户界面 gui

你可能感兴趣的:(读书笔记,Java,java,学习,读书笔记)