《On Java》阅读笔记-03

第 10 章 接口

本章介绍了抽象类和接口的定义，使用，以及区别和意义。这章进一步阐释了 “使用与功能实现分离” 的优秀设计，还顺带介绍了一些设计模式。图灵导读目标如图所示：

接口，到底代表了什么？

接口描述了一个类应该是什么样的和可以做什么，但是不包括怎么做。它实际上是定义了一种类之间的 “协议”。我个人认为接口是一种更高阶的抽象：在代码中对一些相同行为的类进行了进一步的抽象，这带来了代码复用和很强的扩展性，但也带来了额外的复杂性。从这里可以看出，要保持合理设计，就要适当封装，将抽象控制在合理的层次，否则这是一种过度设计！

接口的默认方法

接口本来是不能实现任何方法的，它仅仅代表 “协议”，需要交给子类实现（implements）。但JDK 8依然为借口提供了默认方法。当一个接口有默认方法时，子类可以不用重写这个方法，以作为默认实现。一种说法是默认方法增强了代码的兼容性和灵活性。《On Java》认为：这允许向现有接口中添加方法，而不会破坏已经在使用该接口的所有代码。默认方法应该是为JDK 8引入流的解决方案。

抽象类与接口的区别

特性 | 接口 | 抽象类
组合 | 可以在新类中组合多个接口 | 只能继承一个抽象类
状态（字段） | 不能包含字段（静态字段除外，但它们不支持对象状态） | 可以包含字段，非抽象方法可以引用这些字段
默认方法与抽象方法 | 默认方法不用被子类实现，它只能引用接口内的方法（字段不行） | 抽象方法必须在子类实现
构造器 | 不能有构造器 | 可以有构造器
访问权限限制 | 隐式的 public | 可以为protected或包访问权限

使用准则：“在合理的范围内尽可能抽象”。所以一般是优先用接口，等需要使用抽象类的时候你自然会知道。但除非必要，否则两个都不要使用。在后面会讲到，这是一种过早优化。接口和抽象类可以作为我们重构代码的强力工具。

完全解耦

比起抽象类，接口更受人喜欢些，毕竟如果你调用的方法传入抽象类及其子类，你将只能使用基类及其子类，不能跳出其继承体系。而使用接口不受继承体系限制，能编写更多可复用的代码。

浅谈策略设计模式

首先，有关策略设计模式，这篇文章非常好，后续如果有设计模式相关问题我也会引用。

策略设计模式就是创建一个方法，它能根据传递的参数对象来表现出不同的行为。这使得上下文没有必要知道它这个策略是怎么实现的，我直接用就行了，后续如果有新策略，不用更改上下文，使得方法更加灵活，通用，可复用性也更高。也就是 “我这个方法可以操作任何对象，只要这个对象遵循我的接口”。这也是 “将变化的事物与不变的事物分开” 的设计体现。

这里写上Java的简单的实现，后续可以展开讲讲：

Copy
import java.util.Random;

enum concreteStrategy {
// 加法
addition,
// 减法
subtraction,
// 乘法
multiplication

}

/**
* 策略接口声明了某个算法各个不同版本间所共有的操作。上下文会使用该接口来
* 调用有具体策略定义的算法。
*/
interface Strategy {
/**
* 该方法可实现某种二元运算，并返回值。
*
* @param num1 运算数 1
* @param num2 运算数 2
* @return double 进行运算后的结果
* @author BlackFlame33
*/
double execute(int num1, int num2);
}

/**
* 具体策略会在遵循策略基础接口的情况下实现算法。该接口实现了它们在上下文
* 中的互换性。
*/
class ConcreteStrategyAdd implements Strategy {

@Override
public double execute(int num1, int num2) {
return num1 + num2;
}
}

class ConcreteStrategySubtract implements Strategy {
@Override
public double execute(int num1, int num2) {
return num1 - num2;
}
}

class ConcreteStrategyMultiply implements Strategy {

@Override
public double execute(int num1, int num2) {
return num1 * num2;
}
}

/**
* 上下文定义了客户端关注的接口。
*/
class Context {
/**
* 上下文会维护指向某个策略对象的引用。上下文不知晓策略的具体类。上下
* 文必须通过策略接口来与所有策略进行交互。
*/
private Strategy strategy;

public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}

/**
* 上下文会将一些工作委派给策略对象，而不是自行实现不同版本的算法。
*
* @param num1 运算数 1
* @param num2 运算数 2
* @return double 运算结果
*/
public double executeStrategy(int num1, int num2) {
return strategy.execute(num1, num2);
}
}

/**
* 客户端代码会选择具体策略并将其传递给上下文。客户端必须知晓策略之间的差
* 异，才能做出正确的选择。
*
* @author BlackFlame33
*/
public class ExampleApplication {

public static void main(String[] args) {
// 创建上下文对象。
Context context = new Context();
// 读取第一个数。
int num1 = new Random().nextInt(10);
// 读取最后一个数。
int num2 = new Random().nextInt(10);
// 应用会知道应该执行的具体策略
concreteStrategy action = randomStrategy();
if (action == concreteStrategy.addition) {
context.setStrategy(new ConcreteStrategyAdd());
}
if (action == concreteStrategy.subtraction) {
context.setStrategy(new ConcreteStrategySubtract());
}
if (action == concreteStrategy.multiplication) {
context.setStrategy(new ConcreteStrategyMultiply());
}
double result = context.executeStrategy(num1, num2);
// 打印结果
System.out.println(num1 + " 与 " + num2 + " 的 " + action + " 等于 " + result);
}

private static concreteStrategy randomStrategy() {
return concreteStrategy.values()[new Random().nextInt(concreteStrategy.values().length)];
}
}

组合接口时的名称冲突

将接口组合在一起时，在不同的接口中使用相同的方法名称通常会导致代码可读性变差。（不要这样命名。。。）

适配接口

适配器模式

一样，看这篇文章就行。

个人理解：适配器就好像在不兼容的对象之间的一层兼容层一样。有时候不兼容的代码我们没有权限修改，这时候我们通过适配器内的方法，将参数适配到合适的格式， 然后调用定向到其封装对象中的一个或多个方法。

如此以来，一个把接口作为参数的方法，几乎可以让任何类适应它，只要实现接口的方法就行。这让我深刻感受到接口的威力。

接口的字段

抽象类与接口的区别我们了解到接口只有静态字段。其静态字段存储在接口的静态存储区中。

嵌套接口

浅谈工厂模式

工厂模式将对创建对象的代码与使用对象的代码分离，成为其间接层，使得我们在添加新种类的对象时可以直接添加新种类的类，而不用修改客户端代码。通常发生在复杂的创建对象的情况时。

更多信息还是看这篇文章。

接口，用起来？

指导方针：“优先使用类而不是接口”。从类开始设计，如果明显感觉接口是必要，那么就重构吧！

第 11 章 内部类

定义在另一个类中的类称为内部类。

除了内部类的相关语法外，匿名内部类在后续会非常实用，应该熟练掌握。

内部类到外部类的链接

内部类会隐含一个指向外部类对象的引用。因此内部类拥有访问外部类所有元素的访问权。也是因此，创建内部类时，为了让内部类持有外部类对象的引用，必须先创建外部类对象，再通过外部类对象创建内部类。

匿名内部类

Copy
public Contents contents() {
return new Contents() {
int i = 11;
};
}

这种声明类的方式初看起来比较怪：我正写到return的时候，要返回一个Contents对象了，哦～等等，让我先定义一个Contents的子类。但当类仅需要使用一次或极少重复使用时，也没有必要专门为它写一个类。

内部类的价值

内部类的价值体现为：每个内部类可以独立地继承类或者实现接口，这完善了多重继承问题的解决方案。

内部类还可以实现闭包。它能保留它被创建时所处作用域的信息。

第 12 章 集合

如果对象的数量是固定的，而且这些对象的生命周期都是已知的，那么这样的程序是相当简单的。

数组非常实用，但是其大小长度是已经固定了的，这会带来很大的局限性，因为通常情况下我们并不能准确知道需要多少个对象。

Iterator

迭代器可以实现向任意类型的集合的正向遍历。它让程序员不用关心所处理的集合的类型（我们说不用关心，其实就是在说，我们不必考虑传入集合的类型而被迫做不同的处理），它统一了对集合的访问。

糟糕的设计 ——Stack 类

尽管Java提供了Stack类，但这个类的设计非常糟糕（这很可能与其不合理的继承关系有关），所以JDK 6加入了ArrayDeque，提供了直接使用栈的方法。
警钟长鸣，谨慎考虑继承关系！

面向接口

通过面向接口而不是面向实现来编写代码，可以让我们的代码可以应用于更多对象类型。

总结

Java提供了很多持有对象的方式。最重要的四大类：List、Map、Set，Queue:

Collection保存单个元素，Map保存键值对；集合的大小可以自动调节；通过泛型，我们可以约束可存放的类型，且取出时也不用强制类型转换；集合只能保存引用类型的数据，基本类型的数据可以通过包装类自动装箱存入。

List线性表是一种有序集合。某种意义上，它将数字索引与对象关联起来。

在List中，ArrayList在随机访问上效率很高；而LinkedList在插入删除上效率很高。

Map将对象与其他对象关联起来。

在Map中，HashMap可以快速访问元素；TreeMap将它的键以有序方式保存；LinkedHashMap 按照元素的插入顺序保存，但也通过哈希实现快速访问。

Set中，相同的元素只能保存一个，Set的底层实现其实就是Map，所以它的子实现的特性与Map的子实现的特性很相似。

HashSet可以快速访问元素；TreeSet 以有序方式保存元素；LinkedHashSet按照元素的插入顺序保存。

集合类中的部分不常使用，且设计不合理的类被称为 “遗留类”。

Hashtable、Vector和Stack，它们都是线程安全的，但不要在新代码中使用它们。

关于集合还有很多可总结的，二次笔记时可以以集合为主题展开。

第 13 章 函数式编程

函数式编程语言处理代码片段就像处理数据一样简单。

就是有点费头发

函数式编程基于这样一种想法：使用代码以某种方式操纵其他代码。我们并非从零开始构建代码，而是从现有的、可靠的、经过测试的小片代码开始组合在一起，创建新的代码。

可以这样理解函数式编程：面对对象编程抽象数据，函数式编程抽象行为。

由于函数式编程规定所有的数据必须是不可变的：设置一次，永不改变。这非常适合在并行编程场景中使用。

lambda表达式

建议lambda表达式的行数控制还 3 行内，如果超过 3 行，考虑使用方法引用。

函数式接口

当一个接口只包含一个抽象方法时，这种接口也叫 “功能接口”。

从抽象的层面，可以理解为这就是将方法作为参数或者返回值。但从JVM实现的角度，类和对象才是Java的一等公民，方法是依附于对象或类的，无法独立存在，所以Java选择将其与功能接口绑定在一起。

使用函数式接口时，名字并不重要，重要的只有参数类型和返回类型。当然对功能接口而言，其命名模式能帮助我们快速理解其作用，例如：

只处理对象的接口，命名通常为Function, Consumer和Predicate；

只接受一个基本类型参数的接口，命名通常使用第一部分表示其基本类型，如LongConsumber, DoubleFunction, IntPredicate；

返回类型为基本类型的接口，命名通常使用 'To' 来表示，如ToLongFunction<T>和IntToLongFunction；

参数类型与返回值类型相同的接口，命名使用Operator。UnaryOperator表示一个参数，BinaryOperator表示两个参数；

接受一个参数并返回boolean类型的接口，命名使用Predicate；

接受两个参数的接口，命名通常使用BiXxx，如BiPredicate 表示接受两个参数，返回类型为boolean。

闭包

在内部类章节我们也讲到闭包。闭包可以作为函数对象或者匿名函数，持有上下文数据，可以传递或保存。Java要求其变量是 == 最终 == 不可变的。

第 14 章 流

集合优化了对象的存储。而流（stream）与对象的成批处理有关。

我们平常说流，一般都是指I/O流，而用stream流指代本章所讲的流。大多数时候，我们将对象存储在一个集合中是为了处理它们，所以你会发现，自己编程的重点会从集合转向流。流使我们的程序更小，可读性更高，配合lambda表达式和方法引用，流大大提升了JDK 8的吸引力。

声明式编程

声明式编程是一种有别于命令式编程的编程风格，我们声明 “What to do”（做什么），而不是像命令式编程那样指明 “How to do”（怎么做）。

内部迭代

我们平常显式使用for循环遍历的迭代被称为 “外部迭代”，而流使用 “内部迭代”：你不知道它具体是如何执行迭代的。然而，通过放宽对迭代方式的掌控，我们可以将其交给某种并行机制 —— 流的内部迭代效率更高。

惰性求值

惰性求值意味着流只有在必要时才会被求值。你可以认为在 “被求值” 之前，流就像没有被调用的方法一样，只是声明了逻辑；又或者流被声明塑造成了能如此求值的模型，当真正需要 “被求值” 时，模型开始运行。

JDK 8对流的支持

接口的默认方法也是在JDK 8新增的，它与流的添加或许有着千丝万缕的联系。

在创造了流这个概念后，Java的设计者们遇到了一个难题：如何将流整合进库中，而又不影响已经使用了库的代码？因为，如果向接口加入新方法，会破坏每一个实现了该接口，但还没有实现这个新方法的类。。。无所谓，默认方法会出手默认方法使得我们可以向接口添加默认实现，实现子类而不必重写这些方法。

Optional类型

设计Optional类型的初衷是为了避免流中发生异常而中断。如果流中没有元素，在执行某些流操作时会返回Optional对象。

reduce(BinaryOperator)

形参命名（acc, i）

由于reduce(BinaryOperator)传入的lambda表达式中的第一个参数是上次调用时的结果，第二个参数是来自流中的新值，最好将第一个参数命名为acc、第二个参数命名为i用以见名之意。

第 15 章 异常

Java的基本哲学是‘写得不好的代码无法运行’。

缺陷：异常缺失

异常使用不当可能会导致异常被 “吞掉” 的情况。

异常说明

“异常说明” 可以缩小，但不可以扩大，这与类在继承过程中的规则相反

Java的缺点

除了内存的清理，其他清理都不会自动发生，必须告知客户程序员，让他们自己处理。

使用异常的准则

尽可能使用try-with-resources。

要在恰当的层次处理问题。除非你知道该如何处理，否则不要捕捉异常。