UNSW 2511 23T2 note
Finished
面向对象程序中的继承性
• 继承(inheritance)是软件重用的一种形式,在这种形式中,新的类是由现有的类吸收其属性和行为而创建的。通过吸收它们的属性和行为,从现有的类中创建新的类
• 程序员可以指定新类继承现有类(称为 “超类(superclass)“)的属性和行为,而不是定义完全(独立)的新类。超类)。这个新类被称为**子类(subclass)**。
• 程序员可以为子类添加更多的属性和行为,因此,通常子类比它们的超类(superclass)有更多的功能。
继承关系形成树状的层次结构:

is-a 继承关系
• 在 “is-a“关系中,一个子类的对象也可以被当作超类的一个对象。
• 举例:
undergraduateStudent 同样可被视作 student
• 应该使用继承来模拟 “is-a“关系
is-a 继承关系注意事项
除非所有或大部分继承的属性和方法都有意义,否则不要使用继承。
举例:在数学上,圆是一种椭圆,但是圆类不应该从椭圆类继承而来 -> 一个椭圆类应该有一个方法设置宽度,另一个用来设置高度
Has-a 关联关系(Association relationship)
• 在 “has-a“关系中,一个类的对象有另一个类的对象来存储它的状态或做它的工作,即它 “有一个“对该其他对象的引用。
• 举例:
矩形并不是一条线,但是我们可以使用线来画出矩形
• has-a 关系 与 is-a 关系有很大的区别
• “Has-a“关系是通过现有类的组合来创建新类的例子(与扩展类 extending classes相反)
has-a 继承关系注意事项
• 在最终确定层次结构之前,应该考虑类的所有可能的未来用途
• 明显的解决方案有可能对某些应用不起作用
设计一个类 Designing a class
- 仔细思考一个类应该提供的功能/方法
- 始终努力保持数据的私密性/Private->(local)
- 创建一个对象可能需要不同的操作,如初始化
- 总是初始化数据
- 如果某个对象不再使用,则释放所有相关的资源
- 将有太多任务的类分散成多个小的类
- 类往往是密切相关的。 把共同的属性和行为 “分解”出来,并把它们放在一个类中。然后在类之间使用合适的关系(例如 “is-a “或 “has-a”)
类和对象的介绍
• 类是数据和对该数据进行操作的方法(程序)的集合
• 举例:
一个圆可以用它的中心的x、y位置和它的半径来描述。
• 可以定义一些关于圆的一些有用的方法(程序),如计算周长/面积等
• 通过定义圆 这个 类,即可创建一个新的 数据类型(data type)
Class Circle
在以下代码中,省略了 getter 和 setter
1 | public class Circle { |
对象是类的实例(instance)
在JAVA中, 对象是通过实例化一个类实现
举例:
1 | Circle c; |
访问对象的数据
举例:
1 | Circle c = new Circle(); |
使用对象的方法 methods
访问对象的方法类似于访问对象数据的语法
1 | Circle c = new Circle(); |
子类和继承 Subclasses and inheritance
first approach
创建一个新的独立的类 GraphicalCircle 并 重写已经存在于类 Circle 中的代码。
1 | // the class of graphical circles |
这是最差的解决办法
second approach
这个方法使用了 “has-a“关系
意味着:一个GraphicalCircle 有一个 (数学上的) Circle
它使用 类 Circle 中的方法(area 和 circumference)去定义一些新的方法
这种方法也被称作方法转发 method forwarding
1 | public class GraphicalCircle { |
third Approach
这个方法使用了 “is-a“关系
将 GraphicalCircle 定义为 Circle 的 扩展(extension)或子类(subclass)
子类(subclass) GraphicalCircle 继承 其超类(superclass) Circle 中所有的变量和方法
1 | public class GraphicalCircle extends Circle { |
我们可以将一个GraphicCircle
的实体赋值给一个 Circle
类的变量:
1 | GraphicCircle gc = new GraphicCircle(); |
important:
- 假设有一个Circle类的变量 c
- 则只能访问在Circle类内部的属性和方法
- 不能对 c 使用 draw 方法
超类、对象和类的层次结构 class hierarchy
- 每个类都存在一个超类 superclass
- 如果不定义超类, 则默认为 object 类.
Object 类
- 唯一一个没有superclass的类
- 由 object 定义的方法可以被任何 java 对象(实例instance)调用
- 通常需要重写 (override) 以下方法:
- toString()
- equals()
- hasCode()
抽象类 abstract classes
- 可以声明 仅定义部分实现(define only part of an implementation) 的类
- 使用扩展类(extended classes)来提供部分或全部方法的具体实现
使用的优点
- 可以声明(declare)方法,从而知道某个对象的接口定义(the interface definition)
- 在抽象类的不同子类中,方法可以以不同的方式实现
规则
- 抽象类是一个被声明为抽象的类 declared abstact
- 如果类包含抽象方法,则这个类必须被声明为抽象的 abstract method in absract class
- 抽象类不能被实例化 abstract class cannot be instantiated
- 如果一个抽象类的子类重写(override)其超类的全部抽象方法,并为这些方法提供了实现,则这个抽象类的子类就可以被实例化 override & implement all abstract methods -> instantiatable
- 若一个抽象类的子类没有实现其继承的所有抽象方法,则这个子类仍应该是抽象的 doesn’t implement all -> still abstract
举例

1 | // abstract class |
1 | // one subclass -> Circle |
1 | public class Rectangle extends Shape { |
注意事项:
- shape为一个抽象类,所以不能被实例化
- Circle 和 Rectangle 的实例可以被分配给 Shape 的变量,且不需要casting, 即可理解为:Shape 的子类可以在不用casting的情况下被分配给 Shape 数组中的元素
- 可以对 Shape类 调用area() 和 circumference()
- 不需要casting的原因:
当使用父类引用变量访问子类对象时,编译器会自动识别并调用相应的子类方法(如果存在)。在这种情况下,数组 shapes 中的每个元素都是 Shape 类型的引用变量,但它们实际上引用的是 Circle、Rectangle 和 GraphicalCircle 对象。在循环中,通过 shapes[i].area() 调用了 Shape 类中的 area() 方法。由于多态性的存在,编译器会根据实际的对象类型(Circle、Rectangle 或 GraphicalCircle)来确定应该调用哪个子类的 area() 方法。 这样,无需显式进行类型转换(casting),编译器会根据对象的实际类型自动调用相应的方法。
1 | // create an array to hold shapes |
单一继承和多重继承的比较 single inheritance vs multiple inheritance
- Java中,一个新的类只可以扩展一个超类 ->单一继承
- 某些面向对象的编程语言支持多重继承,即一个新的类可以扩展两个或多个超类
- 对于多重继承,可能会出现超类中的一个行为被以多种形式继承(实现成了不同方法)
- Java中使用 interface 来实现多重继承
Casting 类型转换
定义:类型转换是将一个数据类型转换成另一个数据类型的过程,java中可以分为 隐式转换(iplicit casting)和 显式转换(explicit casting)
隐式转换(iplicit casting)
在编译过程中自动进行的类型转换。它是安全的,不会导致数据丢失或溢出。隐式转换通常发生在以下情况下:将一个较小的数据类型赋值给一个较大的数据类型。
将字面量常量赋值给变量。
显式转换(explicit casting)
在编译过程中需要手动指定的类型转换。它用于将一个较大的数据类型转换为较小的数据类型。由于较大的类型可能无法完全容纳较小的类型的值,因此 可能会导致数据丢失或溢出。因此,在进行显式转换时,需要进行数据的精确性和边界检查。显式转换使用括号将要转换的数据类型括起来,并在前面加上目标类型的名称。
接口 interface
- 接口interface 类似于抽象类 abstract classes,但有几个重要的区别
- 在一个接口中定义的所有方法都是有隐藏的抽象的属性(并不需要使用abstract修饰符,但为了清楚明了仍建议使用)
- 在接口中声明的变量必须是 static/final 的,即均为常量
- 类似于一个类可以拓展其超类一样, 它也可以选择实现一个接口
- 为了实现一个接口,一个类必须首先在一个 implements 字句中声明这个接口, 然后必须实现接口所有的抽象方法
- 一个类可以实现多于1个接口
- 一个接口可以同时继承多个接口 (Interface can extend multiple interfaces)
举例:

定义:
1 | // declare the interface |
使用:
当一个类实现了一个接口, 则这个类的实例也可以被分配给变量类型为接口类型的变量:
1 | Shape[] shapes = new Shape[3]; |
实现多个接口 implement multiple interfaces
一个类何以实现多于1个的接口

1 | public class DrawableScalableRectangle |
拓展接口 extend interfaces
- 接口可以有子接口(sub-interfaces), 就像类可以有子类一样
- 一个子接口继承了其 父接口(super-interface) 的所有抽象方法和常量,并且可以定义新的抽象方法和常量
- 接口可以同时拓展多个接口 can extend more than one interface at a time
1
2public interface Transformable
extends Scalable, Rotable, Reflectable {}
方法转发 Method Forwarding

- 假设 类C 扩展了 类A, 并且实现了 接口X
- 由于 接口X 定义的所有方法都是抽象的,所以 类C 需要实现所有的方法
- 然而, X 有三种实现方式 (分别是 P/Q/R)
- 在 类C 中,我们可能想使用这些实现中的一个 -> 想使用 P/Q/R中实现的部分或者全部方法
- 比如,如果想使用 P 中的方法, 可以通过在 类C 中创建一个类型为 P 的对象来实现, 并通过这个对象访问 P 中实现的所有方法
- 在 类C 中,我们需要为 接口X 中的所有方法提供必要的存根(stub), 在编写方法时, 可以简单地通过 类P 的对象调用 类P 的方法
- 以上即为 方法转发 Method Forwarding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40// 定义接口 -> interface X
interface Service {
void call();
}
// 实现代理类 -> Class C
class ServiceProxy implements Service {
private Service service;
public ServiceProxy(Service service) {
this.service = service;
}
public void call() {
// 在C的接口实现中调用P中的方法
System.out.println("Before call");
service.call();
System.out.println("After call");
}
}
// 实现被转发的接口 -> Class P
class RealService implements Service {
public void call() {
System.out.println("RealService called");
}
}
// 调用代理类
public class Main {
public static void main(String[] args) {
Service realService = new RealService();
Service proxyService = new ServiceProxy(realService);
proxyService.call();
}
}
方法重写(多态性) Method Overriding(Polymorphism)
- 当一个类定义了一个使用相同名称,返回类型的方法,并且其参数的数量,类型和位置于其超类中的方法完全相同时,这个类中的方法会覆盖掉超类的方法,即为override。
- 如果这个方法被该类的一个对象调用,则被调用的是这个方法的新的定义,而不是超类中的旧定义
多态性 polymorphism
- 一个对象根据其在继承层次中的位置,决定对自己使用何种方法的能力,通常被成为多态性
方法重写举例:
在下面的例子中:
* 若p为 类B 的实例,则 p.f() 为 类B 中的 f()
- 若p为 类A 的实例,则 p.f() 为 类A 中的 f()
这个例子还将展示如何使用 super 关键字来引用被覆盖掉的方法
1 | class A { |
假设 类C 为 类B 的 子类, 且 类B 为 类A 的 子类
类A 和 类C 都定义了 方法f()
对于 类C,我们可以通过以下方法调用被覆盖掉的方法 f() :
1 | super.f() |
但是:
- 如果三个类都定义了 f(), 则在 类C 中调用 super.f() 将会调用 类B 中定义的方法
- 尤其重要的是,在上述的情况下,没有办法在 类C 中使用 A.f()
- super.super.f() 在java语法中是不被允许的
方法重载 method Overloading
- 定义具有相同名称,不同参数或者返回类型的方法被称作 方法重载
- 在 JAVA 中, 通过区分方法的名称,返回类型,以及它参数的数量/类型/位置 来区分方法
1
2
3
4
5
6double add(int, int)
double add(int, double)
double add(float, int)
double add(int, int, int)
double add(int, double, int)
// 均为不同的方法 -> 返回类型相同但是参数的类型/数量和位置不完全相同
数据隐藏和封装 Data Hiding and Encapsulation
- 可以将数据隐藏在类中,并且只能通过类的方法使其可用
- 可以帮助保持对象数据的一致性 -> state of an object
可见性修改器 visibility modifiers
JAVA提供物种访问修改器 access modifiers -> 对 variable/method/class
1 | public // visible to the world |

构建器 constructors
良好的做法是为所有的类定义所需要的构造函数
如果一个类没有定义构造函数,则:无参数(no argument)构造函数被隐式插入
这个无参数构造函数会调用超类的无参数构造函数
如果超类没有一个可见的(visible)无参数构造函数,则会导致编译错误
如果构造函数的第一条语句不是对 super() 或者 this() 的调用,则会隐含地插入对 super() 的调用
如果一个构造函数被定义为有一个或者多个参数,则无参数构造函数不会被插入到这个类里面
一个类可以有多种具有不同签名(signatures)的多个构造函数
this 这个词可以用来调用同一个类中的另一个构造函数
举例
1 | public class MyClass { |
钻石型的继承问题 Diamond inheritance Problem
在JAVA中使用单一继承(single inheritance)

1 | class W {} |
通过以下方式实现:
- 在 类Z 中,使用定义于 类X 和 类W 中的方法和变量
- 在 类Z 中,如果想使用 类Y 中实现的方法, 可以通过使用方法转发 method forwarding,即,在 类Z 中创建一个 类型为 类Y 的对象,然后在 类Z 中可以通过这个对象访问在 类Y 中定义的 方法
- Z 和 Y类 的对象可以分配给 IY 类型的变量,而不是 Y类型 的变量 (即使用 IY 作为变量类型,使用casting实现)
Java中的Exceptions
- Exception 是一个事件,它发生在一个程序的执行过程中,扰乱了程序指令的正常流程
- 当错误发生时,一个异常对象被创建并交给实时运行的系统,此为 抛出一个异常(throw an exception)
- 运行时系统在调用stack中搜索一个方法,该方法包含一个可以处理该异常的代码块
- 所选择的异常处理程序被称为捕获异常(catch the exception)
checked/unchecked exceptions
有三种Exceptions:
- Checked exception (IOException/ SQLException…)
- Error (VirtualMachineError, OutOfMemoryError…)
- Runtime exception(ArrayIndexOutOfBoundsExceptions/ ArithmeticException…)
- Exception 的类型决定了他是 checked/unchecked
- 所有属于 RuntimeException (通常由程序代码中的缺陷引起)/ Error (通常是”系统”问题) 子类的都是 unchecked exception
- 所有继承自 Exception类 但不直接或间接继承自 RuntimeException类 的类都被认为是checked Exception
JAVA中exception的层次结构 Hierarchy of Java exceptions

样例
1 | public void writeList() { |
在上述代码中,首先会尝试运行try部分, 如果有任何异常则终止运行并抛出exception,进入catch部分, 如果抛出的exception类型和catch中的类型相对应,则进入对应的catch模块, 最后,无论是否引发exception,都会执行finally部分
用户定义的Exceptions
- exceptions可以被自定义
- 所有的exceptions都必须是 Throwable的子类
- 一个 checked exception需要扩展 Exception类,但不能直接或间接地从RuntimeException类中获取
- 一个unchecked exception(例如runtime exception)需要从runtimeException类中扩展出来
通常情况下。我们通过扩展 Exception类 来定义一个checked Exception
1 | class MyException extends Exception { |
1 | try { |
继承中的Exceptions Exceptions in Inheritance
- 如果一个子类方法覆盖(override)一个超类方法,子类的throws子句可以包含超类 throws字句的一个子集
但是不能抛出更多的exceptions1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26class Parent {
void method() throws IOException, InterruptedException {
// ...
}
}
class Child extends Parent {
// 合法的覆盖
void method() throws InterruptedException {
// ...
}
// 不合法的覆盖,抛出了未声明的异常
// @Override
// void method() throws IOException, InterruptedException, SQLException {
// // ...
// }
// 合法的覆盖,抛出超类方法声明异常的子类型异常
void method() throws InterruptedException, FileNotFoundException {
// ...
}
}
JAVA中的Assertions
- Assertion是java的一个语句,可以使用其测试对程序的假设是否成立
- Assertion在检查下列情况时很有用:
前置条件(preconditions)
后置条件(post-condition)
类不变量(invariant -> 根据design by contract)
内部不变量 (internal invariants)
控制流不变量(control-flow invariants) - 不应用于检查下列情况:
用于public methods的参数检查
做任何程序需要正确操作的工作 - 评估assertion不应导致副作用(side effects)的产生
样例:
1 | /** |
Exception总结
- 在设计过程(design process)中应该考虑异常处理和错误恢复策略
- 有些情况下可以通过先验证数据来防止异常的发生
- 如果一个Exception可以在一个方法中得到有意义的处理,那么这个方法应该可以捕获(catch)这个exception,而不是单纯的声明它(should catch rather than declare)
- 如果一个子类方法覆盖(override)一个超类方法,那么子类throw的字句可以包含超类throw字句的一个子集,但是绝对不能抛出更多的异常
- 程序员应该处理 checked exceptions
- 如果预期中会出现 unchecked exception, 必须仔细处理这些exceptions
- 只有第一个匹配的catch才会被执行,所以要仔细确定catch的类
- exception是API文档和契约的一部分
- assertion 可以用来检查前置条件/后置条件和不变量
java中的泛型和集合 Generics and Collections in JAVA
java中的泛型 Generics in java
泛型(Generics) 使类型types(类和接口) 在定义以下时可以成为参数:
- 类 classes
- 接口 interfaces
- 方法 methods
优点:
- 移除casting并在编译时提供更强的类型检查(type check)
- 允许通用算法(Generic algorithms)的实现,这些算法适用于不同类型的集合,可以被定制,并且类型是安全的
- 通过使更多的错误在编译时被发现,增加了代码的稳定性
1 | List list = new ArrayList(); |
1 | List<String> listG = new ArrayList<String>(); |
Generic Types 通用类型/泛型
- 泛型是一个泛型类或接口,他在类型上有参数化(parameterized)
- 通用类(generic class)的定义格式:
1
class name<T1,T2,...,Tn> {/*...*/}
- 最常用的类型参数名称为:
E - element(被java collection framework广泛使用)
K - key
N - Number
T - Type
V - value
S,U,V etc. - 2nd,3rd,4th types
- normal version
1
2
3
4
5
6
7
8
9public class Box {
private Object object;
public void set(Object object) {
this.object - object;
}
public Object get() {
return object;
}
}
- Generic version
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
* Generic version of the Box class
* @param <T> the type of the value being boxed
*/
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
- 创建变量
1
2
3
4
5Box<integer> intergerBox = new Box<integer>();
OR
Box<integer> integerBox = new Box<>();
多类型参数 Multiple Type Parameters
- 一个泛型类(generic class) 可以有多个类型参数
- 例如,通用的OrderedPair类,有通用的配对接口
1
2
3
4public intergace Pair<K, V> {
public K getKey();
public V getValue();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
} - 使用样例:
1
2
3
4
5
6
7Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world");
... ...
OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String> p2 = new OrderedPair<>("hello", "world");
... ...
PrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
通用方法 Generic Methods
- 通用方法是指 引入自己的类型参数的方法 -> introduce own type
1
2
3
4
5public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue());
}
} - 调用上述方法的完整语法为:
1
2
3Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
java中的集合 collections in java
集合框架(colelctions framework)是一个统一的架构, 用于表示和操作集合。 集合只是一个将多个元素组合成一个单元的对象 collection is an object
所有集合框架都包含以下内容:
接口:允许独立于其表示的细节来操作集合
实现:集合接口的具体实现
算法:对实现集合接口的对象进行有用的计算方法,如搜索/排序这些算法被认为是多态的(polymorphic): 同一个方法可以用在适当的集合接口的许多不同的实现上
核心集合接口 Core Collection interfaces
- 核心集合接口封装了不同类型的集合
- 这些接口允许对集合的操作独立于他们的表示细节

集合接口 the collection interface
- 一个集合(collection)表示一组被称为其元素的对象
- 集合接口(collection interface)被用来在需要最大通用性的地方传递对象的集合
- 例如,按照惯例,所用通用的集合实现都有一个构造函数,其需要一个Collection 参数
- 集合接口包含执行基本操作的方法,例如:
1
2
3
4
5
6
7int size()
boolean isEmpty()
boolean contains(Object element)
boolean add(E element)
boolean remove(Object element)
Iterator<E> iterator()
etc...
集合的实现 collection implementation
下表概述了通用的实现方式:

JUnit Testing
软件测试 software testing
- 不同类型的测试:
面向对象的设计文件描述了类和方法的职责(API) -> unit testing 单元测试
系统设计文件(System design document) -> integration testing 集成测试
需求分析文件(Requirements Analysis Document) -> System Testing 系统测试
客户期望(Client Expectation) -> Acceptance Testing 验收测试 - 单元测试对于重构(refactoring)任务很有用
JUnit
- JUnit是一个流行的单元测试框架,用于测试java程序
- 大多数流行的IDE都能方便地集成JUnit
- 基本的JUnit术语:
测试案例 Test cases - 包含测试方法的 Java class
测试方法 Test methods - 在测试案例中执行测试代码的方法,用 @Test 注释
Asserts - Assert/Assert statement检查预期结果与实际结果的对比
测试套件 Test Suites - 多个测试案例的集合
JUnit Example
- assertEquals
1
assertEquals("COMP2511", forum.getName());
- assertTrue
1
assertTrue(Arrays.equals(new String[] {"coding", "hobbies"}, , funThread.getTags().toAray()));
- Exception
1
2
3assertThrows(UNSWNoSuchFileException.class, () -> {
fs.cd("/usr/bin/cool-stuff");
});1
2
3
4assertDoesNotThrow(() -> {
fs.mkdir("/usr/bin/cool-stuff", true, false);
fs.cd("/usr/bin/cool-stuff");
});
Java Lambda 表达式
- Lambda表达式允许我们:
轻松定义匿名方法
Anonymous methods
视代码为数据code as data
将功能作为方法参数functionality as argument
- 只有一个方法的匿名内部类可以用lambda表达式代替
- Lambda表达式可被用来实现 只有一个抽象方法的接口, 这种接口被称为 功能接口
Functional interfaces
- Lambda表达式将函数作为对象提供
functions as objects
- Lambda表达式更简洁,灵活度更高
lambda 表达式的语法 Syntax
一个lambda表达式包含以下内容:
- 用括号括起来,以逗号分隔的正式参数列表。 无需提供数据类型。 若只有一个参数则可以省略括号
- 箭头符号
->
- 主体,由单个表达式或语句块构成
1 | // 定义三个接口 |
方法引用 Method References
我们可以将现有方法视为功能接口的实例
使用::
操作符可以引用方法:
- 静态方法Static method: (
ClassName :: methodName
) - 特定对象的实例方法: (
instanceRef :: methodName
) - 类的构造函数: (
ClassName :: new
)
Java中的功能接口 Function interface
- java.util.function 包中的功能接口为 lambda 表达式和方法引用提供了预定义的目标类型
- 每个功能接口都有一个抽象方法,称为该功能接口的功能方法,lambda表达式的参数返回类型与该抽象方法相匹配或适应
- 功能接口可以在多种上下文中提供目标类型,如赋值,方法调用等
1
2
3
4
5Predicate<String> p = String :: isEmpty;
// collect empty strings
List<String> strEmptyList1 = strList.stream()
.filter(p)
.collect(Collectors.toList());
1 | // collect strings with length less than six |
有几种基础的功能形状 function shapes:
- Function (从
T
映射到R
)包含一个名为
apply
的抽象方法,该方法接受一个类型为T
的参数,返回一个类型为R
的结果。 - Consumer (接受输入类型
T
,没有返回值)它包含一个名为
accept
的抽象方法,该方法接受一个类型为T
的参数,执行相应的操作 - Predicate (接受输入类型
T
,返回布尔值)它包含一个名为
test
的抽象方法,该方法接受一个类型为T
的参数,返回一个布尔值表示断言的结果。 - Supplier (没有输入值,返回输出类型
R
)它包含一个名为
get
的抽象方法,该方法不接受参数,返回一个类型为R
的结果。
1 | // Function |
使用Lambda功能的比较器 Comparator
1 | //Using an anonymous inner class |
Pipeline & Streams
- Pipeline 是一系列集合操作(
aggregate operation
) - 以下实例打印了集合名册中包含的男性成员,该pipeline包括聚合操作
filter
和forEach
1
2
3
4
5
6
7
8
9
10
11// pipeline and aggregate operations:
roster.stream()
.filter(e -> e.getGender() == Person.Sex.MALE)
.forEach(e -> System.out.println(e.getName()));
// traditional approach-> for loop
for (Person p : roster) {
if (p.getGender() == Person.Sex.MALE) {
System.out.println(p.getName());
}
} - 在pipeline中,各个操作是
loosely coupled
,其只依赖于输入的数据流,这些操作可以很容易地重新排列/替换成其他合适的操作 - 上述语法中的
.
符号与用于实例或类的.
符号的意义完全不同 - 一个pipeline包含以下组件:
一个源
Source
:可以是一个集合collection
,数组array
, 发生器函数generator function
, IO通道I/O channel
.
零个或多个中间操作,中间操作如fliter
会产生一个新的数据流stream
- 数据流
Stream
是一个元素序列(sequence of elements),stream方法
可以从一个集合中创建一个流 - 过滤操作
filter
会返回一个新的数据流,其中包含与其谓词相匹配的元素。 - 终端操作(如
forEach
)会产生一个非流结果,如一个原始值(如double
值)、一个集合collection
,或者在某些情况下,根本不会产生任何值。
设计模式 Design Pattern
- 设计模式是针对经常出现的问题的一种尝试性解决方案
- 每个模式都有:
简短的名称
背景描述
问题描述
解决方案 - 在软件工程中,设计模式是针对软件设计中经常出现的问题的一般可重复的解决方案
- 设计模式是:
代表如何解决问题的模板
捕捉设计方面的专业知识,并使这些知识得以传递和重复使用
提供共享词汇表,改善沟通,简化实施
不是最终的解决方案,而是设计问题的一般解决方案 - 设计模式的类别:
行为模式
Behavioural Patterns
结构模式Structural Patterns
创造模式Creational Patterns
设计模式的分类
创造模式 Creational Patterns
行为模式 Behavioral Patterns
结构模式 Structural Pattern
策略模式StrategyPattern
- 允许将一系列算法封装
encapsulate
起来 - 允许在运行过程中改变行为(更改策略)
- This pattern defines a family of algorithms, encapsulates each one -> 策略模式定义了一系列算法并将它们封装起来
- 策略模式是一种行为设计模式
Behavioural pattern
,可使算法独立于使用它的类而变化
实现:

策略模式的适用性,优点和缺点 Applicability/ benefits/ drawbacks
- 适用性(在以下情况下时适用)
Applicability
许多相关类别的行为各不相同
一个 类 可以从算法的不同变体中受益
一个类定义了许多行为,这些行为以多个条件语句(如 if 或 switch)的形式出现。运用策略模式可以将每个条件分支移到各自具体的策略类内部中 - 优点
Benefits
使用组合(
composition
)而非继承(inheritance
),从而使行为和使用行为的上下文类之间更好地解耦 (Decoupling
) - 缺点
Drawbacks
增加对象数量
客户必须了解不同的策略
举例 Examples
- 对列表排序(
quicksort
/bubble sort
/merge sort
)将每种排序算法封装为一个 具体策略类
类在运行时决定 需要哪种排序行为 - 搜索 (
binary search
/BFS
/DFS
)
状态模式StatePattern
有限状态机 Finite-state Machine
- 有限状态机(FSM)是一种抽象机器,它在任何给定时间内都能处于有限状态中的一种状态。
有限状态机可以根据某些外部输入从一种状态转换到另一种状态
从一种状态到另一种状态的变化称为过渡transition
- 有限状态机的定义是
一个状态列表
每个过渡的条件
其初始状态
举例:投币式旋转门

状态机中的术语 Terminology
- 状态
State
: 对等待执行转换的系统状态的描述 - 过渡
Transition
: 在满足条件或收到事件时要执行的一组操作 - 根据当前的状态不同,相同的行为可能会触发不同的行为
- 通常来说,以下内容也与状态相关:
进入动作
Entry action
: 进入状态时执行
退出动作Exit action
: 退出状态时执行
表现形式 Representation

实例分析 Gumball machine
bad design

- 对于 gumball machine,有四种状态:
- No quarter
- Has quarter
- gumball sold
- out of gumball
- 创建一个变量保存当前状态,并为每个状态定义值:
1
2
3
4final static int SOLD_OUT = 0;
final static int NO_QUARTER = 1;
final static int HAS_QUARTER = 2;
final static int SOLD = 3; - 整合系统中所有可能发生的操作:
- insert quarter
- turns crack
- eject quarter
- dispense
- 现在创建一个类来代表状态机,对于每种操作,创建一个方法并适用条件判断来根据不同的状态执行行为,例如以下:
1
2
3
4
5
6
7
8
9
10
11
12public void insertQuarter() {
if (state == HAS_QUARTER) {
System.out.println("You can't insert another quarter");
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;
System.out.println("You inserted a quarter");
} else if (state == SOLD_OUT) {
System.out.println("You can't insert a quarter, the machine is sold out");
} else if (state == SOLD) {
System.out.println("Please wait, we're already giving you a gumball");
}
} - 对于以上的设计,每当有一个新的状态添加进来,都需要更改全部的操作的代码,所以我们需要重新设计整个系统
better design

对于之前的设计,我们将重新设计它,将不同的状态对象封装在其自身的类中,并在发生操作时委托给当前的状态
步骤:
- 定义一个状态的接口,其中应包含gumball machine中每个操作的方法
- 对于每个状态,定义并实现一个属于其自己的类,当机器处于对应的状态时,这些类将负责机器的行为
- 去掉动作中的所有的条件代码,转而委托给状态类来实现不同的行为对于不同的状态,都会有针对不同行为的方法,举例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// old design
final static int SOLD_OUT = 0;
final static int NO_QUARTER = 1;
final static int HAS_QUARTER = 2;
final static int SOLD = 3;
int state = SOLD_OUT;
int count = 0;
// new design
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;
State state;
int count = 0;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// has quarter:
public void insertQuarter() {
System.out.println("You can't insert another quarter");
}
// no quarter:
public void insertQuarter() {
System.out.println("You inserted a quarter");
gumballMachine.setState(gumballMachine.getHasQuarterState()); // change state
}
// sold out:
public void insertQuarter() {
System.out.println("You can't insert a quarter, the machine is sold out");
}
// sold:
public void insertQuarter() {
System.out.println("Please wait, we're already giving you a gumball");
}
要点
- 状态模式允许一个对象根据其内部状态做出许多不同的行为
- 与过程式状态机不同,状态模式将状态表示为一个完整的类
- 类通过委托给与其组成的当前状态对象来获取行为
- 通过将每个状态封装到一个类中,我们可以本地化任何需要进行的更改
- 状态模式和策略模式的类图相同,但意图不同
- 策略模式通常用一种行为或算法配置上下文类
- 状态模式允许上下文在状态发生变化时改变其行为
- 状态转换可由状态类或上下文类控制
- 使用状态模式通常会增加设计中的类的数量
- 状态类可在上下文实例之间共享
与策略模式的区别
在状态模式中,特定状态知道其他所有状态的存在,且能触发从一个状态到另一个状态的转换
策略则几乎完全不知道其他策略的存在。
观察者模式ObserverPattern
- 观察者模式用于在 “事件驱动 “编程中实现分布式事件处理系统
- 在观察者模式中:
一个被称为主体
subject
(或可观察对象observable
或发布者publisher
)的对象,会维护其被称为观察者observers
(或订阅者subscribers
)的隶属者列表
主体会 自动 将其的任何状态变化 通知 观察者,通常是通过调用观察者的方法之一 - 观察者模式定义了对象之间一对多的依赖关系,因此当一个对象(主体)改变状态时,其所有依赖对象(观察者)都会 自动得到通知和更新
- 观察者模式的目的应为:
在对象之间定义一对多的依赖关系,而不使对象紧密耦合
当主体改变状态时,自动 通知/更新 数量不限 的观察者 (从属对象)
能够 动态 添加和删除观察者
可能的解决方案 Possible solution
- 定义 “主体
Subject
“和 “观察者Observer
“接口,当主体改变状态时,所有注册的观察者都会收到通知并自动更新 - 主体 的职责是维护一个观察者列表,并通过调用观察者的
update()
操作将状态变化通知观察者 - 观察者 的职责是在主体上注册(和取消注册)自己(以获得状态变化的通知),并在收到通知时更新自己的状态。
- 这样的方式令 主体和观察者之间保持着
loosely coupled
的关系 - 可在运行时独立 添加 和 移除 观察者

多观察者和主体

传递/更新数据
主体需要传递(更改)数据,同时向观察者发出更改通知,有两种可能的选择
- Push data
主体将更改后的数据传递给其观察者
update(data1, data2...)
所有观察者都必须实现上述更新方法 - Pull data
主体将自身的参照传递给观察者,观察者需要从主体获取(提取)所需的数据
update(this)
主体需要为其观察者提供所需的访问方法public double getTemperature()
观察者模式的结构

总结
优势
- 避免主体与观察者之间的紧密耦合 (avoid high coupling)
- 令主体和观察者就可以在系统中处于不同的抽象层次。
- 松散耦合对象更易于维护和重复使用
- 允许动态注册和注销注册
需要注意的
- 主体的变化可能导致对其观察者的一连串更新,并进而导致对其从属对象的更新,从而产生复杂的更新行为
- 需要妥善管理此类依赖关系
要点
- 观察者模式定义了对象之间的单对多关系
- 主体或我们所熟知的观测对象,使用一个通用接口更新观察者的数据
- 观察者是松耦合的,因为可观察对象对它们一无所知,只知道它们实现了观察者接口
- 在使用模式时,您可以从可观察对象中推送或提取数据(pull被认为是更正确的)
pull允许观察者在需要的时候获取数据,避免了频繁的推送和可能的资源浪费。
5.不要依赖于Observer的特定通知顺序 - 在需要的情况下,可以自行创建Observable类并实现
复合模式CompositePattern
- 在 OO 程序设计中,复合对象是由一个或多个类似对象(具有类似功能)组成的对象
- 目的是让我们能够像操作一组对象一样操作单个对象实例,例如
调整一组形状大小的操作应与调整单个形状大小的操作相同
计算文件的大小应与计算目录的大小相同 - 单一(叶)对象与复合(组)对象之间没有区别
如果我们区分单个对象和一组对象,代码就会变得更加复杂,因此也更容易出错
样例
计算单个零件或完整子组件(由多个零件组成)的总价,而不必区分零件和子组件

可能的解决方案 Possible solution

- 为叶子
Leaf
(单个/部分)对象和组合Composite
(组/整体)对象定义统一的组件界面 - 组合对象中存储了一系列子组合(
Leaf
/Composite
) - 客户端可以忽略对象组合与单个对象之间的差异,这大大简化了复杂层次结构的客户端,使其更易于实施、更改、测试和重用
- 树形结构通常用于表示部分-整体层次结构。 多向树形结构在每个节点(下面的子节点)上都存储了一个组件集合,用于存储叶子对象和复合(子树)对象
- 叶
Leaf
型的对象直接对对象本身进行操作 - 复合对象(
COmposite
)对其子对象执行操作,并在需要时收集返回值并得出所需的答案
实现问题: 统一性Uniformity
和 类型安全Type Safety

- 类型安全设计:只在复合类中定义与子类相关的操作
- 统一性设计:在组件界面中包含所有与子代相关的操作
统一性
- 在组件接口中包含所有与子代相关的操作,这意味着叶子类需要以 “什么也不做 “或 “抛出异常 “的方式来实现这些方法
- 客户端可以统一处理叶子对象和复合对象
- 我们会失去类型安全性,因为叶子类型和复合类型并没有干净地分开
- 适用于子节点类型动态变化(从叶子节点到复合节点,反之亦然)的动态结构,客户端需要定期执行与子节点相关的操作。例如,文档编辑器应用程序
- 更适合节点类型经常反复变化的情况
类型安全
- 只在复合类中定义与子类相关的操作
- 类型系统会强制执行类型限制,因此客户端无法对 Leaf 对象执行与子对象相关的操作
- 客户端需要区别对待叶子对象和复合对象
- 适用于静态结构,在这种结构中,客户端不需要对 “未知” 的组件类型对象执行与子对象相关的操作
总结
- 复合模式提供了一种结构,既可容纳单个对象,也可容纳复合对象
- 复合模式允许客户统一处理复合材料和单个对象
- 组件
Component
是复合结构中的任何对象,组件可以是其他 复合节点Composite node
或叶节点Leaf node
- 在实施 Composite 的过程中,有许多设计上的权衡。 您需要在透明度/统一性和类型安全性与您的需求之间取得平衡
创造模式
创建模式提供了各种对象创建机制,提高了现有代码的灵活性和重用性
- 工厂模式
Factory Method
提供了在超类中创建对象的接口、 但允许子类改变创建对象的类型
- 抽象工厂模式
Abstract Factory method
让用户在不指定具体类别的情况下生成相关对象
- 构建者
Builder
让用户逐步构建复杂的对象。该模式允许用户使用相同的构造代码生成不同类型和表现形式的对象
- 单例模式
Singleton
让用户确保一个类只有一个实例, 同时为该实例提供一个全局访问点
工厂模式FactoryMethod
- 工厂方法是一种创建设计模式,它使用工厂方法来处理创建对象的问题,而无需指定将要创建的对象的确切类别
- 问题
直接在需要(使用)对象的类中创建对象缺乏灵活性
它将类与特定对象绑定
使实例化无法独立于(无需改变)类而改变 - 可能的解决方法
为创建对象定义一个单独的操作(工厂方法)
通过调用工厂方法创建对象
这样就可以编写子类,改变创建对象的方式(重新定义实例化哪个类)
结构 structure

Product
声明了创建者(Creator
)及其子类可生成的所有对象所共有的接口- 具体产品是产品接口(
Product interface
)的不同实现方式 - 创建者(
Creator
)类声明了返回新产品对象的工厂方法createProduct()
- 具体创建者会覆盖基本工厂方法
createProduct()
,从而返回不同类型的产品
样例 Example

- 如图,
Dialog
类作为factory会在工厂方法createButton()
中返回不同类型的Button
,具体的返回类型取决于子类的类型
抽象工厂模式AbstractFactoryPattern
- 抽象工厂是一种创建设计模式,它能让你创建相关对象的家族,而无需指定它们的具体类
问题 problem
想象一下,您正在创建一个家具店模拟器,您的代码包含以下内容
> 相关产品系列,例如 椅子 + 沙发 + 咖啡桌
> 该系列的几个变种
> 例如,椅子 + 沙发 + 咖啡桌产品有以下几种款式
>> 艺术类
>> 维多利亚式
>> 现代式
解决方法 solution
- 首先, 抽象工厂模式建议为系列中的每件产品明确声明接口 (例如
椅子
、沙发
或咖啡桌
)。 然后, 确保所有产品变体都继承这些接口。 例如, 所有风格的椅子
都实现椅子接口
; 所有风格的咖啡桌
都实现咖啡桌接口
, 以此类推

- 接下来, 我们需要声明抽象工厂(
abstract factory
)——包含系列中所有产品构造方法的接口。 例如createChair()
创建椅子 、 createSofa()
创建沙发和createCoffeeTable()
创建咖啡桌 。 这些方法必须 返回抽象产品类型 , 即我们之前抽取的那些接口: 椅子,沙发和咖啡桌等等

- 对于系列产品的每个变体,我们都将基于 抽象工厂接口(
abstract factory interface
)创建不同的工厂类。每个工厂类都只能返回特定类别的产品, 例如,现代家具工厂ModernFurnitureFactory
只能创建 现代椅子ModernChair
、 现代沙发ModernSofa
和 现代咖啡桌ModernCoffeeTable
对象 - 客户端代码可以通过相应的
抽象接口调用工厂和产品类
。 你无需修改实际客户端代码, 就能更改传递给客户端的工厂类, 也能更改客户端代码接收的产品变体。 客户端无需了解工厂类
结构 Structure

- 抽象产品(
abstract product
)为组成产品系列的 一系列不同但相关的 产品声明接口 - 具体产品(
concrete product
)是抽象产品的各种实现,按变体分组。每个抽象产品(椅子/沙发)必须在所有给定的变体(维多利亚式/现代式)中实现 - 抽象工厂(
abstract factory
)接口 声明 了一套方法,用于创建每个抽象产品 - 具体工厂(
concrete factory
) 实现 抽象工厂的创建方法。每个具体工厂对应一个特定的产品变体,并 只创建这些产品变体 - 客户端可以与任何具体的工厂/产品变体合作,只要它通过抽象接口与它们的对象通信即可
与工厂模式的区别 difference with factory pattern
- 目的和用途
工厂模式旨在通过将对象的创建封装到一个共同的接口中,以便根据需求创建不同类型的对象,同时隐藏对象的实例化细节
抽象工厂模式旨在提供一个接口,用于 创建相关或依赖对象的系列 ,而无需指定具体的类。它允许客户端创建一组相关对象,而不必关心具体的类名 - 结构和关系
工厂模式通常包含一个工厂接口(或抽象类),以及具体的工厂类,每个具体工厂类负责创建特定类型的对象
抽象工厂模式包含一个抽象工厂接口(或抽象类),定义了一系列可以 创建不同类型对象的方法,每个具体工厂类实现了这个接口,用于创建特定系列的对象 - 对象创建的区别
工厂模式关注于对单个对象的创建
抽象工厂模式关注于对一组相关对象的创建,这些对象之间可能存在关联 - 扩展性
工厂模式相对较容易添加新的具体工厂类,以支持创建新的产品类型
抽象工厂模式相对较容易添加新的具体工厂类,以支持创建新的产品系列,但不太容易添加新的产品类型
装饰器模式DecoratorPattern
- 装饰器设计模式允许我们在运行时根据需求有选择地为对象(而不是类)添加功能
- 初始的类的行为不变(开放-封闭原则)
- 继承会在编译时扩展行为,附加功能会在该类的所有实例的生命周期中绑定
- 与继承相比,装饰器设计模式更倾向于组合(
composition
)。它是一种结构模式(structural pattern
),为现有类提供了一个包装器(wrapper
) - 由于这种设计模式涉及递归,因此可以按不同顺序多次装饰对象
- 无需在单一(复杂)类中实现所有可能的功能
结构 structure

- 部件
Component
声明封装其和被封装对象的公用接口 - 具体部件
Concrete Component
作为被封装对象所属的类,它同时定义了部件的基础行为execute
,但是装饰类可以更改此类行为 - 基础装饰
Base Decorator
拥有一个指向被封装对象的引用成员变量(wrappee
), 该变量的类型为通用部件接口(Component
),通过这样的声明,可以引用具体的部件和装饰。 装饰基类会将所有操作委派给被封装的对象 - 具体装饰类
Concrete Decorator
定义了可动态添加到部件的额外行为。 具体装饰类会**重写装饰基类的方法extcute
**, 并在调用父类方法之前或之后进行额外的行为
Java中的泛型 Generic Types
有界类型参数 Bounded Type Parameters
- 有时,您可能需要限制参数化类型中可用作类型参数的类型
- 例如,对数字进行操作的方法可能只接受 Number 或其子类的实例
1
2
3
4
5
6
7public <U extends Number> void inspect(U u) {
System.out.println("U:" + u.getClass().getName());
}
OR
public class NaturalNumber<T extends Number> {}
多重限制 multiple bounds
- 一个类型参数可以有多个边界
< T extends B1 & B2 & B3 >
- 具有多个边界的类型变量是边界中列出的所有类型的子类型
- 注意,上面的 B1、B2、B3 等指的是接口或类。最多只能有一个类(单一继承),其余(或全部)都是接口
- 如果其中一个边界是一个类,则必须首先指定该类
错误样例
1 | public static <T> int countGreaterThan(T[] anArray, T elem) { |
正确样例
1 | public interface Comparable<T> { |
泛型,继承和子类 Generics, inheritance and subtypes
- 给定以下方法
public void boxTest( Box<Number> n ) { /* ... */ }
- 其支持传入的变量既不能是
Box<Integer>
,也不能是Box<Double>
- 因为
Box<Integer>
和Box<Double>
都不是Box<Number>
的子类 - 想让方法能接受
Box<Integer>
和Box<Double>
,可以将方法改为:
public void boxTest( Box<? extends Number> n ) { /* ... */ }
- 通过使用通配符类型
? extends Number
,您可以接受任何类型为 Number 或其子类的 Box 对象作为参数
通用类和子类 Generic classes and subtyping
- 可以通过扩展或实现泛型类或接口来对其进行子类型化
- 一个类或接口的类型参数与另一个类或接口的类型参数之间的关系由
extends
和implements
子句决定 - 举例:
ArrayList<E> implements List<E>
List<E> extends Collection<E>
- 因此,
ArrayList<String>
是List<String>
的子类型,而List<String>
是Collection<String>
的子类型。 - 只要不改变类型参数、 类型之间的子类型关系就会得到保留
通用配符 上界 wildcards: upper bounded
- 在通用代码中,问号 (
?
) 被称为通配符,代表未知类型 - 通配符可以在多种情况下使用:作为 参数、字段或局部变量的类型 ;有时作为返回类型
- 上界通配符
<? extends Foo>
(其中Foo
是任意类型)匹配Foo
和Foo的任意子类型
。 - 可以指定通配符的上限(upper bounded),也可以指定下限(lower bounded),但不能同时指定
1
2
3
4
5
6
7
8
9
10
11
12
13public static void process(List<? extends Foo> list) {
for (Foo elem : list) {
// do something
}
}
//////////////////////////////////////////////////////
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list) {
s += n.doubleValue();
}
return s;
}
通用配符 无界 wildcards: unbounded
- 无限制通配符类型使用通配符 (?) 指定
- 例如,List< ? >. 这称为未知类型的列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public static void printList(List<Object> list) {
for (Object elem : list) {
System.out.println(elem + " ");
}
System.out.println();
// this prints only a list of Object Instances
}
//////////////////////////////////////////////////
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.println(elem + " ");
}
System.out.println();
// 对于这个写法,主要的目标是为了让方法接受不同类型的列表,而无需关心具体的列表类型。通配符类型 List<?> 表示可以接受任何类型的列表,
}
通用配符 下界 wildcards: lower bounded
- 上界通配符将未知类型限制为特定类型或该类型的子类型,并使用
extends
关键字表示 - 下限通配符使用通配符(
?
)表示,后面是super
关键字,后面是其下限:< ? super A >
- 要编写适用于
Integer
列表和Integer
的超级类型(如Integer
、Number
和Object
)的方法,您需要指定List<? super Integer>
方法 List<Integer>
比List<? super Integer>
更具限制性1
2
3
4
5public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
通用配符和子类型 wildcards and subtyping
- 虽然
Integer
是Number
的子类型,但List<Integer>
并不是List<Number>
的子类型,这两种类型并不相关 List<Number>
和List<Integer>
的共同父类是List<?>
。
测试设计 Test design
软件测试:测试推动开发 Test-Driven Development (TDD)
- 软件开发过程中的每次迭代之前都必须制定计划,以适当验证(测试)所开发的软件是否满足要求(即后置条件)
- 软件开发人员不能在开发出软件后才考虑如何对其进行测试
- 测试是开发软件解决方案不可或缺的重要组成部分。绝不能事后才考虑测试
- 在每次迭代过程中,必须根据测试套件对增量开发进行测试。每次代码修改和/或重构后,都必须使用预定义的测试套件进行适当测试。
- 在开始实施解决方案之前,必须根据需求规格设置测试。
- 软件的开发顺序应为
- 要求/规格分析
- 接口 (方法签名/前置&后置条件)
- 测试集合 (单元测试/集成测试)
- 方法实现
软件测试:输入空间覆盖 Input Space Coverage
- 不能通过试错的方式随意进行测试。
- 测试必须系统地进行,并制定周密的测试计划。
- 目标应是考虑可能的输入空间,并尽可能覆盖它。
- 通常的做法是将输入空间划分为 “等价组
equivalence groups
“,并从每个等价组中选择一个有代表性的输入。这里的假设是:在同一等价组中,程序在每个输入上的行为都是相似的。 - 考虑边缘情况
borderline
,通常称为边界测试boundary testing
- 对于多个输入值,考虑可能的输入组合,对其进行优先排序,并在时间和资源允许的情况下考虑尽可能多的输入组合。同样,将可能的组合划分为同质子集,并选择具有代表性的组合
软件测试:代码覆盖率 Code Coverage
- 代码覆盖率是一个有用的指标,可以帮助你评估测试套件的质量。
- 代码覆盖率通过确定测试套件成功验证的代码行数,衡量软件通过测试套件验证的程度。
- 大多数覆盖率报告中的常见指标包括
- 函数覆盖率
function coverage
:有多少已定义的函数被调用。 - 语句覆盖率
statement coverage
:程序中有多少语句被执行。 - 分支覆盖率
branches coverage
:执行了多少个控制结构分支(例如 if 语句)。 - 条件覆盖率
condition coverage
:测试了多少布尔子表达式的真假值。 - 行覆盖率
line coverage
:测试了多少行源代码。
- 函数覆盖率
软件测试和模拟中的随机性 Randomness in Software Testing and Simulation
- 软件测试:
- 随机数据通常被视为无偏数据
- 提供平均性能(如在排序算法中)
- 用随机数据对组件进行压力测试
- 随机数据通常被视为无偏数据
- 软件模拟:
- 生成随机行为/移动。
例如,可能希望玩家/敌人以随机模式移动。
可能的方法:随机生成一个 0 到 3 之间的数字、- 0 表示向前移动,1 表示向左移动,2 表示向后移动,3 表示向右移动。
- 地牢的布局可以随机生成
- 可能希望引入不可预测性
- 生成随机行为/移动。
随机数 Random Numbers
软件只能生成伪随机数
在java中生成随机数
使用 Random
类
- 需要import
java.util.Random
类 - 选项1:创建一个新的随机数生成器
- Random rand = new Random();
- 选项2:使用单个长种子创建新的随机数生成器。
- 重要:每次使用相同种子运行程序时,都会得到完全相同的 “随机 “数序列。
Random rand = new Random(long seed);
- 重要:每次使用相同种子运行程序时,都会得到完全相同的 “随机 “数序列。
- 为了改变输出,我们可以给随机播种器一个随时间变化的起点。
例如,起点(种子)是当前时间
基本测试模板 Basic Test Template
- 设定先决条件
Precondition
(@BeforeEach
等) - 实施 (调用方法)
- 验证后置条件
Postcondition
(@AfterEach
/Asserts
等)
- 通常情况下,每个测试都应独立运行,执行顺序并不重要
参数化测试 Parameterized Tests
- 参数化测试
Parameterized Tests
使用不同的输入值反复执行相同的测试,并根据相应的预期结果测试输出 - 数据源
data source
可用于检索输入值和预期结果的数据。 - 如果要在每个测试用例之前执行某些语句(如前置条件),可使用
@Before
注解。 - 如果要在每个测试用例后执行某些语句,如重置变量、删除临时文件和变量等,则可使用
@After
注解。
测试类型 Types of tests
单元测试 Unit Test
- 测试单一功能
- 理想情况下,应进行隔离测试–采用科学方法,控制所有其他变量
- 尽量减少对其他功能的依赖
- 因此,很难编写黑盒单元测试
- 我们可以通过尽可能减少依赖关系的数量来使我们的测试类似于单元测试
- 可以使用模拟测试和模拟对象,但这需要了解方法依赖于哪些功能
- 在不调用另一个方法的情况下,很难说测试单个方法是否发生了变化
集成测试 Integration test
- 测试依赖关系网(耦合),捕捉单元测试没有发现的 “潜伏在缝隙中 “的错误
- 每个失败的集成测试都应能写成一个失败的单元测试
- 测试软件组件之间的交互(耦合)
系统测试 System Test
- 对整个系统进行黑盒测试
- 可以在不同的抽象层次进行测试
可用性测试/验收测试 Usability tests / acceptance tests
- 测试其在前端是否有效
- 功能是否达到预期目标
- 是否可用
基于属性的测试 Property-based test
- 测试代码的个别属性,而不是直接测试输出
创建测试集换 Creating a test plan
需要正确设计测试方法,确保:
- 高覆盖率
- 混合不同类型的测试
需要注意的:
- 编写过多的测试是 不好 的–如果你对同一件事进行单元测试、集成测试和系统测试,那么测试套件就会变得 紧密耦合 ,难以维护。
- 需要取得 平衡 ->一种方法是用单元测试来测试所有内容,但只用集成测试/系统测试来测试程序的主要流程/用例–对于项目来说,这将是一个团队决定,并记录在测试计划中。
编写测试代码的原则 Principles of writing test code
- 就像适用于普通代码的要求同样适用于测试代码,DRY、KISS
- 可以在测试代码中使用设计模式
- 不过,在编写测试代码时还需要考虑其他一些事情:
- 测试代码越简单越好,否则,最终得到的东西会比您首先要测试的软件更复杂(也更容易出错)
- 应尽量减少条件、循环和任何控制流,以降低测试的复杂性
- 工厂模式通常在测试设计中非常有用,可以编写一个工厂来生产测试用的假对象
单例模式SingletonPattern
单例是一种创建设计模式,可确保一个类只有一个实例,同时为该实例提供全局访问点
- 问题
客户希望- 确保一个类只有一个实例,并
- 为该实例提供全局访问点
- 解决方法
所有单例的实现都有这两个共同步骤:- 将 默认构造函数 设置为私有
private
,以防止其他对象在单例类中使用new
操作符。 - 创建一个作为构造函数的静态创建方法
static creation method
。在内部,该方法调用私有构造函数创建对象,并将其保存在静态字段中。接下来对该方法的所有调用都会 返回缓存对象 。 - 如果你的代码能访问
Singleton
类,那么它就能调用Singleton
的静态方法。 - 无论何时调用
Singleton
的静态方法,返回的总是同一个对象。
- 将 默认构造函数 设置为私有
结构 structure

- 单例类声明了静态方法
getInstance()
,该方法返回其自身类的相同实例 - 注意:如果程序支持多线程,则需要在
getInstance()
处添加线程锁synchronized
- 客户代码应隐藏
Singleton
的构造函数 - 调用
getInstance()
方法应该是获取单例对象的唯一方法
如何实现 how to implement
- 在类中添加一个私有静态字段
private static field
,用于存储单例实例 - 声明一个公共静态创建方法
public static creation method
,用于获取单例实例 - 在静态方法中实现 “懒初始化”
- 首次调用时应创建一个新对象,并将其放入静态字段
- 该方法应在所有后续调用中始终返回该实例
- 将类的构造函数设置为私有
constructor of the class private
。- 类的静态方法仍能调用构造函数,但不能调用其他对象
在客户端中,调用单例的静态创建方法来访问对象
- 类的静态方法仍能调用构造函数,但不能调用其他对象
样例 Example
1 | public class MySingleton { |
并发性入门 Introduction to concurrency
- 包括 Java 在内的多种现代语言都允许多线程并发执行
- 为了充分利用当今的多核硬件,我们必须创建采用多线程的应用程序
- 因此,从根本上了解并发性至关重要
- 线程安全:多个线程可以访问相同的资源,而不会暴露不正确的行为或导致不可预测的结果
- 遗憾的是,许多 Java 库都缺乏线程安全。例如,ArrayList、StringBuilde
线程安全 Thread Safety
- Java 中的时间切分
Time slicing
是指为线程分配时间的过程 - 线程运行的顺序是不确定的。一个线程有多少语句在另一个线程的某些语句运行之前运行也是不可预测的
- 修改同一对象(数据)的两个线程可以并行运行
可行的解决办法:Synchronized
- 同步方法在开始时获取对象或类的锁,执行方法,然后在结束时释放锁
- 使用同步关键词只允许一个线程执行方法,避免了并发问题
- 为了最有效地利用可用的多个 CPU,必须尽量减少访问共享资源的部分代码(在同步下)
- 我们也可以同步一组语句,但同步一个方法才是好的做法
- Java 提供了线程安全的集合封装器,使用静态方法对集合进行线程安全封装。例如,
Collections.synchronizedList(list)
Java.util.concurrent包
包含适合多线程使用并经过优化的集合。
需要避免的 Need to avoid
- 当两个或多个线程无限期地互相等待时,这种情况被称为死锁
deadlock
- 当两个或多个线程陷入相互反应的无休止循环时,这种情况被称为活锁
livelock
- 当一个或多个线程因另一个 “贪婪 “的线程而无法继续运行时,就会出现
starvation
模板模式TemplatePattern
- 模板方法定义了行为的骨架(结构)(
通过实现不变部分invariant parts
) - 模板方法调用原始操作(
primitive operation
),可由子类实现,或在抽象超类中默认实现 - 子类只可以重新定义行为的某些部分,而不改变其他部分或行为的结构
- 子类不能控制父类的行为,父类调用子类的操作
关于控制反转
使用库
library
(可重用类)时,我们调用想要重用的代码
使用框架framework
(如模板模式)时,我们编写子类并实现框架调用的变体代码 - 模板模式只需实现一次行为的通用(不变)部分,”而由子类来实现可变化的行为
- 不变行为属于一个类中(localized)
结构 Structure

- 抽象类定义了一个
templateMethod()
来实现不变结构(行为) templateMethod()
调用抽象类(抽象或具体)中定义的方法–如primitive1
、primitive2
等- 可通过提供具体方法在抽象类中实现默认行为
- 重要的是,子类可以更改原始方法来实现不同的行为(
variant behaviour
) - 子类编写者必须了解哪些操作是为了覆盖而设计的
- 基本操作
Primitive operations
:有默认实现或必须由子类实现的操作 - 最终操作
final operations
:子类不能重写的具体操作 - 钩子操作
hook operations
:默认情况下什么都不做的具体操作,必要时子类可以重新定义。这样,子类就可以根据自己的需要在不同的地方 “挂钩 “算法;子类也可以自由地忽略挂钩
样例 Example


样板模式 vs 策略模式
- 模板方法在类(
class level
)一级工作,因此是静态的 - 策略模式作用于对象层面(
object level
),能在运行时切换行为 - **模板方法基于继承
inheritance
**:它允许你通过在子类中扩展算法的某些部分来改变这些部分 - 策略模式以组合
composition
为基础:通过在运行时为对象提供与该行为相对应的不同策略,可以改变对象的部分行为
迭代器模式IteratorPattern
迭代器模式是一种行为设计模式, 让你能在不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素
迭代器 Iterator
- 迭代器是一种能让程序员遍历容器的对象
- 允许我们访问数据结构的内容,同时抽象出其底层表示形式
- 在 Java 中,for 循环是对迭代器的抽象
- 迭代器可以告诉我们
- 我们还有剩余的元素吗?
- 下一个元素是什么?
遍历数据结构 Traversing a data structure
- 聚合实体(容器)
- 堆栈、队列、列表、树、图、循环
- 如何遍历聚合实体而不暴露其底层表示?
- 保持抽象性和封装性
- 最初的解决方案–接口中的方法
- 如果我们需要多种方法来遍历容器,该怎么办?
解决方法

迭代器模式的主要思想是将集合的遍历行为抽取为单独的迭代器对象
除实现自身算法外, 迭代器还封装了遍历操作的所有细节, 例如当前位置和末尾剩余元素的数量。 因此, 多个迭代器可以在相互独立的情况下同时访问集合。
迭代器通常会提供一个获取集合元素的基本方法。 客户端可不断调用该方法直至它不返回任何内容, 这意味着迭代器已经遍历了所有元素。
所有迭代器必须实现相同的接口。 这样一来, 只要有合适的迭代器, 客户端代码就能兼容任何类型的集合或遍历算法。 如果你需要采用特殊方式来遍历集合, 只需创建一个新的迭代器类即可, 无需对集合或客户端进行修改。
结构 structure

迭代器和可迭代对象 Iterator vs Iterables
- 可迭代对象是可以被迭代的对象
An iterable is an object that can be iterated over
- 所有迭代器都是可迭代对象,但并非所有可迭代对象都是迭代器
All iterators are iterable, but not all iterables are iterators
- For 循环只需给定可遍历对象
访问者模式VisitorPattern
- 访问者是模式一种行为设计模式(
behavioral design pattern
),它在不修改现有对象的情况下为其添加新的操作/行为 - 访问者设计模式是一种将算法从其操作对象结构中 分离 出来的方法
- 这是遵循开放/封闭原则(
open-close principle
)的一种方法 - 创建一个访问者类,实现虚拟操作/方法的所有适当特殊化
- 访问者将实例引用
instance reference
作为输入,并实现目标(额外的行为) - 访问者模式可添加到API接口中,使其客户端无需修改源代码即可对类执行操作
- 一个应用案例:网购的购物车 + 结算
存在的问题 problem
想在不更改底层代码的情况下实现基于底层数据的新功能
解决方法 Solution
- 访问者模式将新行为放入一个名为访问者
Visitor
的单独类separate class
中,而不是试图将其集成到现有类中 - 必须执行行为的原始对象现在作为参数
as an argument
传递给访问者的一个方法,使该方法可以访问对象中包含的所有必要数据 - 访问者类
visitor class
需要定义一组方法,每种类型一个方法。例如,一个城市、一个景点、一个行业等
访问者模式使用一种称为 “双重调度double dispatch
“的技术,在给定对象(不同类型)上执行合适的方法- 一个对象 “接受 “一个访问者,并告诉它应该执行什么访问方法
- 一个附加方法允许我们在不进一步修改代码的情况下添加更多行为
结构 Structure

Visitor
接口声明了一组访问方法,这些方法可以将对象结构的具体元素作为参数- 每个 “具体访问者
Concrete Visitor
“都会针对不同的具体元素类别,实现多个版本的相同行为 - 元素接口
Element interface
声明了一种 “接受”访问者的方法。该方法应有一个参数,该参数应与访客接口Visitor Interface
的类型一致。 - 具体元素(
Concrete Element
)必须实现接收方法。 该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。请注意,即使元素基类实现了该方法, 所有子类都必须对其进行重写并调用访问者对象中的合适方法。
访问者模式的适用性和限制 Applicability and limitation
- 适用性
在以下情况下,将操作移至访问者类是有益的- 需要对对象结构进行许多不相关的操作、
- 组成对象结构的类是已知的,预计不会改变、
- 需要经常添加新的操作
- 算法涉及对象结构的多个类,但希望在一个位置进行管理、
- 算法需要跨越多个独立的类层次结构
- 限制
- 由于新的类通常需要为每个访问者添加新的访问方法,因此类层次结构的扩展会更加困难
适配器模式AdapterPattern
- 适配器模式是一种结构型设计模式,它能使接口不兼容的对象能够相互合作
- 允许将现有类的接口用作另一个接口,适用于客户类
- 适配器模式通常用于使现有类(API)与客户端类协同工作,而无需修改其源代码
- 适配器类
Adapter class
映射/连接两种不同类型/接口的功能 - 适配器模式器为现有的有用类提供了一个包装,使客户类可以使用现有类的功能
- 适配器模式不提供额外功能
结构 Structure

- 适配器包含它所封装类的一个实例(
adaptee
) - 适配器会调用封装对象的实例中的方法以达到映射/链接功能
样例 Example
1 | class LightningToMicroUsbAdapter implements MicroUsbPhone { |
生成器模式BuilderPattern
- 生成器模式是一种创建型设计模式, 使你能够分步骤
step by step
创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象
问题
- 一个复杂的对象需要费力地一步步初始化/构造许多字段和嵌套对象
- 这种初始化/构造代码通常埋藏在一个带有大量参数的庞大构造函数中。
- 甚至更糟:散落在客户端代码的各个角落
解决办法
- 构建器模式将对象构建代码从其自身的类中提取出来,并将其移至称为构建器
builder
的独立对象中 - 构建器模式允许你逐步构建复杂的对象
- 构建器不允许其他对象在构建过程中访问产品
- 导演类
Director
定义了构建步骤的执行顺序,而构建器Builder
则提供了这些步骤的实现。
结构

- 构建器接口
Builder interface
声明了所有类型构建器通用的产品构建步骤 - 实际的构建器
Concrete Builders
提供不同的施工步骤。实际的构建器Concrete Builders
生产的产品可能不遵循通用接口 - 产品
Product
是结果对象。不同构建器构建的产品不必属于相同的类层次结构或接口 Director
类定义了调用构造步骤的顺序,因此您可以创建并重复使用特定的产品配置- 客户端必须将其中一个构建器对象
Builder object
与Director
关联起来1
2
3b = new ConcreteBuilder1();
d = new Director(b); // 将builder和director关联起来
// 这样director可以调用对应的builder去创建对应的产品
适合的应用场景
- 构造函数具有多个可选参数时
- 希望使用代码创建不同形式的产品 (例如石头或木头房屋) 时
- 使用生成器构造组合树或其他复杂对象
和其他pattern的关联
- 许多设计都是从使用工厂方法
Factory pattern
(不太复杂,可通过子类进行自定义)开始,然后发展到抽象工厂Abstract Factory
或生成器Builder
(更灵活,但更复杂) - 构建器
Builder
侧重于逐步构建复杂对象 - 抽象工厂
Abstract Factory
专门创建相关对象族Families of related objects
- 抽象工厂
Abstract Factory
会立即返回产品,而 生成器Builder
会让你在获取产品前执行一些额外的构造步骤 - 构建器
Builder
何时返回生成的产品取决于director/client
Finished