2511note


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

  1. 仔细思考一个类应该提供的功能/方法
  2. 始终努力保持数据的私密性/Private->(local)
  3. 创建一个对象可能需要不同的操作,如初始化
  4. 总是初始化数据
  5. 如果某个对象不再使用,则释放所有相关的资源
  6. 将有太多任务的类分散成多个小的类
  7. 类往往是密切相关的。 把共同的属性和行为 “分解”出来,并把它们放在一个类中。然后在类之间使用合适的关系(例如 “is-a “或 “has-a”)

类和对象的介绍

• 类是数据和对该数据进行操作的方法(程序)的集合

• 举例:
一个圆可以用它的中心的x、y位置和它的半径来描述。

• 可以定义一些关于圆的一些有用的方法(程序),如计算周长/面积等

• 通过定义 这个 ,即可创建一个新的 数据类型(data type)

Class Circle

在以下代码中,省略了 getter 和 setter

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
public class Circle {

protected static final double pi = 3.14159;
protected int x, y;
protected int r;
// very simple constructor
public Circle() {
this.x = 1;
this.y = 1;
this.r = 1;
}
// another simple constructor
public Circle(int x, int y, int r) {
this.x = x;
this.y = y;
this.r = r;
}
// calculate circumference and area of the circle
public double circumference() {
return 2 * pi * r;
}
public double area() {
return pi * r * r;
}
}

对象是类的实例(instance)

在JAVA中, 对象是通过实例化一个类实现

举例:

1
2
3
4
5
6
Circle c;
c = new Circle();

OR

Circle c = new Circle();

访问对象的数据

举例:

1
2
3
4
5
6
7
Circle c = new Circle();
// Initialize our circle to have centre (2, 5)
// and radius 1.
// Assuming, x, y and r are not private
c.x = 2;
c.y = 5;
c.r = 1;

使用对象的方法 methods

访问对象的方法类似于访问对象数据的语法

1
2
3
4
Circle c = new Circle();
double a;
c.r = 2; // assume r is not private
a = c.area();

子类和继承 Subclasses and inheritance

first approach

创建一个新的独立的类 GraphicalCircle 并 重写已经存在于类 Circle 中的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// the class of graphical circles
public class GraphicalCircle {
int x, y;
int r;
Color outline, fill;

public double circumference() {
return 2 * 3.14159 * r;
}
public double area() {
return 3.14159 * r * r;
}
public void draw(Graphics g) {
g.setColor(outline);
g.drawOval(x-r, y-r, 2*r, 2*r);
g.setColor(fill);
g.fillOval(x-r, y-r, 2*r, 2*r);
}
}

这是最差的解决办法

second approach

这个方法使用了 “has-a“关系

意味着:一个GraphicalCircle 有一个 (数学上的) Circle

它使用 类 Circle 中的方法(areacircumference)去定义一些新的方法

这种方法也被称作方法转发 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
public class GraphicalCircle {
// here is the math circle
Circle c;
// the new graphics variables go here
Color outline, fill;

// very simple constructor
public GraphicalCircle() {
c = new Circle();
this.outline = Color.black;
this.fill = Color.white;
}
// another simple constructor
public GraphicalCircle(int x, int y, int r, Color o, Color f) {
c = new Circle(x, y, r);
this.outline = o;
this.fill = f;
}
// draw method, using object 'c'
public void draw(Graphics g) {
g.setColor(outline);
g.drawOval(c.x - c.r, c.y - c.r, 2 * c.r, 2 * c.r);
g.setColor(fill);
g.fillOval(c.x - c.r, c.y - c.r, 2 * c.r, 2 * c.r);
}
}

third Approach

这个方法使用了 “is-a“关系

GraphicalCircle 定义为 Circle扩展(extension)子类(subclass)

子类(subclass) GraphicalCircle 继承 其超类(superclass) Circle 中所有的变量和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class GraphicalCircle extends Circle {
Color outline, fill;
public GraphicalCircle() {
// 使用super()可以调用父类 Circle 中的
// 构造函数来完成对父类Circle的初始化
super();
this.outline = Color.blalck;
this.fill = Color.white;
}
// another simple constructor
public GraphicalCircle(int x, int y, int r, Color o, Color f) {
// 向super()中传递参数可以向父类传递参数
super(x, y, r);
this.outline = o;
this.fill = f;
}
public void draw(Graphics g) {
g.setColor(outline);
g.drawOval(x - r, y - r, 2 * r, 2 * r);
g.setColor(fill);
g.fillOval(x - r, y - r, 2 * r, 2 * r);
}
}

我们可以将一个GraphicCircle的实体赋值给一个 Circle 类的变量:

1
2
3
4
GraphicCircle gc = new GraphicCircle();
double area = gc.area();
Circle c = gc;
// 我们不能对变量类型为Circle的变量c使用 draw 方法

important:

  1. 假设有一个Circle类的变量 c
  2. 只能访问在Circle类内部的属性和方法
  3. 不能对 c 使用 draw 方法

超类、对象和类的层次结构 class hierarchy

  1. 每个类都存在一个超类 superclass
  2. 如果不定义超类, 则默认为 object 类.

Object 类

  • 唯一一个没有superclass的类
  • 由 object 定义的方法可以被任何 java 对象(实例instance)调用
  • 通常需要重写 (override) 以下方法:
    1. toString()
    2. equals()
    3. 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
2
3
4
5
// abstract class
Public abstract class Shape {
public abstract double area();
public abstract double circumference();
}
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
// one subclass -> Circle
public class Circle extends Shape {
protected static final double pi = 3.14159
protected int x, y;
protrcted int r;

// constructor
public Circle() {
this.x = 1;
this.y = 1;
this.r = 1;
}
public Circle(int x, int y, int r) {
this.x = x;
this.y = y;
this.r = r;
}
// implement the method in the abstract super class
public double circumference() {
return 2 * pi * r;
}
public double area() {
return pi * r * r;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Rectangle extends Shape {
protected double width, height;
public Rectangle() {
width = 1.0;
height = 1.0;
}
public Rectangle(double w, double h) {
this.width = w;
this.height = h;
}
public double area() {
return width * height;
}
public double circumference() {
reeturn 2 * (width + height);
}
}

注意事项:

  • 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
2
3
4
5
6
7
8
9
10
11
// create an array to hold shapes
Shape[] shapes = new Shape[4];
shapes[0] = new Circle(4, 6, 2);
shapes[1] = new Rectangle(1.0, 2.0);
shapes[2] = new Rectangle(4.0, 2.0);
shapes[3] = new GraphicalCircle(1, 1,6, Color.green, Color.yellow);
// 因为GraphicalCircle是由Circle拓展而来,所以也是属于Shape
double total_area = 0;
for (int i = 0; i < shapes.length; i++) {
total_area += shapes[i].area();
}

单一继承和多重继承的比较 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
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
// declare the interface
public interface Drawable {
public void setColor(Color c);
public void setPosition(double x, double y);
public void draw(Graphics g);
}

public class DrawableRectangle
extends Rectangle
implements Drawable {
private Color c;
private double x, y;
...
...
// implementations of the methods in Drawable:
public void setColor(Color c) {
this.c = c;
}
public void setPosition(double x, double y) {
this.x = x;
this.y = y;
}
public void draw(Graphics g) {
g.drawRect(x, y, w, h, c);
}
}

使用:

当一个类实现了一个接口, 则这个类的实例也可以被分配给变量类型为接口类型的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Shape[] shapes = new Shape[3];
Drawable[] drawable = new Drawable[3];

DrawableCircle dc = new DrawableCircle(1, 1);
DrawableSquare ds = new DrawableSquare(2.5);
DrawableRectangle dr = new DrawableRectangle(2.3, 4.5);

// the shape can be assigned to both arrays
shapes[0] = dc; drawable[0] = dc;
shapes[1] = ds; drawable[1] = ds;
shapes[2] = dr; drawable[2] = dr;

// 可以在Drawable和Shapes中调用抽象方法

double total_area = 0;
for(int i = 0; i < shapes.length; i++) {
total_area += shapes[i].area;
drawables[i].setPosition(i * 10.0, i * 10.0);
drawables[i].draw(g);
}

实现多个接口 implement multiple interfaces

一个类何以实现多于1个的接口

1
2
3
4
5
public class DrawableScalableRectangle
extends DrawableRectangle
implements Movable, Scalable {
// here are the methods
}

拓展接口 extend interfaces

  • 接口可以有子接口(sub-interfaces), 就像类可以有子类一样
  • 一个子接口继承了其 父接口(super-interface) 的所有抽象方法和常量,并且可以定义新的抽象方法和常量
  • 接口可以同时拓展多个接口 can extend more than one interface at a time
    1
    2
    public 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;
    }

    @Override
    public void call() {
    // 在C的接口实现中调用P中的方法
    System.out.println("Before call");
    service.call();
    System.out.println("After call");
    }
    }

    // 实现被转发的接口 -> Class P
    class RealService implements Service {
    @Override
    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
2
3
4
5
6
7
8
9
10
11
12
13
class A {
int i = 1;
int f() {
return i;
}
}
class B extends A {
int i; // shadows i from A
int f { // overrides f() from A
i = super.i + 1; // retrives i from A
return super.f() + i; // invokes f() from 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
    6
    double 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
2
3
4
public                  // visible to the world
private // visivle to the class only
protected // visivle to the package and all subclasses
No modifier (default) // visivle to the package

构建器 constructors

良好的做法是为所有的类定义所需要的构造函数
如果一个类没有定义构造函数,则:

无参数(no argument)构造函数被隐式插入
这个无参数构造函数会调用超类的无参数构造函数
如果超类没有一个可见的(visible)无参数构造函数,则会导致编译错误

如果构造函数的第一条语句不是对 super() 或者 this() 的调用,则会隐含地插入对 super() 的调用
如果一个构造函数被定义为有一个或者多个参数,则无参数构造函数不会被插入到这个类里面
一个类可以有多种具有不同签名(signatures)的多个构造函数
this 这个词可以用来调用同一个类中的另一个构造函数

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyClass {
private int x;
private int y;
private String name;

public MyClass() {
this(0, 0, "Default"); // 调用带参数的构造函数,传递默认值
}

public MyClass(int x, int y) {
this(x, y, "Custom"); // 调用带参数的构造函数,传递指定的值和默认名称
}

public MyClass(int x, int y, String name) {
this.x = x;
this.y = y;
this.name = name;
}

// 其他类成员和方法...
}

钻石型的继承问题 Diamond inheritance Problem

在JAVA中使用单一继承(single inheritance)

1
2
3
4
5
class W {}
interface IY {}
class X extends W {}
class Y extends W implements IY {}
class Z extends X implements IY {}

通过以下方式实现:

  • 在 类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…)
checked vs unchecked Exceptions
  • Exception 的类型决定了他是 checked/unchecked
  • 所有属于 RuntimeException (通常由程序代码中的缺陷引起)/ Error (通常是”系统”问题) 子类的都是 unchecked exception
  • 所有继承自 Exception类不直接或间接继承自 RuntimeException类 的类都被认为是checked Exception

JAVA中exception的层次结构 Hierarchy of Java exceptions

样例

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
public void writeList() {
PrintWriter out = null;

// start of try part
try {
System.out.println("Entering" + " try statement");
out = new PrinterWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++) {
out.println("Value at: " + i + " = " + list.get(i));
}
}
// end of try part
// start of catch part
catch (IndexOutOfBoundsException e) {
System.err.println("Caught IndexOutOfBoundsException: " + e.getMessage());
}
catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
}
// end of catch part
// start finally part
finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
}
else {
System.out,println("PrintWriter not open");
}
}
// end of finally part
// try 块用于尝试执行可能引发异常的代码,catch 块用于捕获和处理特定类型的异常,finally 块用于执行必须在方法结束时执行的清理代码。无论是否引发异常,finally 块中的代码都会被执行。
}

在上述代码中,首先会尝试运行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
2
3
4
5
class MyException extends Exception {
public MyException(String message) {
super(message);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try {
out = new PrinterWriter(new FileWriter("myDate.txt"));
for (int i = 0; i < SIZE; i++) {
int idx = i + 5;
if (idx >= SIZE) {
throw new myException("idx is out of index range!");
// 创建自定义的exception,并throw出去
}
out.println(list.get(idx));
}
}
catch (IOException e) {
System.out.println("In writeln...");
}
catch (MyException e) {
System.out.println(e.getMessage());
}
catch (Exception e) {
System.out.println("in Writeln, Exception ...");
}

继承中的Exceptions Exceptions in Inheritance

  • 如果一个子类方法覆盖(override)一个超类方法,子类的throws子句可以包含超类 throws字句的一个子集
    但是不能抛出更多的exceptions
    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
    class Parent {
    void method() throws IOException, InterruptedException {
    // ...
    }
    }

    class Child extends Parent {
    // 合法的覆盖
    @Override
    void method() throws InterruptedException {
    // ...
    }

    // 不合法的覆盖,抛出了未声明的异常
    // @Override
    // void method() throws IOException, InterruptedException, SQLException {
    // // ...
    // }

    // 合法的覆盖,抛出超类方法声明异常的子类型异常
    @Override
    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
2
3
4
5
6
7
8
9
10
11
12
/**
* Sets the refresh interval (which must correspond to a leagal frame rate)
* @param interval refresh interval in milliseconds
*/
private void setRefreshInterval(int interval) {
// confirm adherence to precondition in nonpublic method
assert interval > 0 && interval <= 1000/MAX_REFRESH_RATE : interval;
// 检查 interval是否大于0并且小于等于1000/MAX_REFRESH_RATE,
// 如果不满足条件,则抛出AssertionError
// 并包含指定的表达式interval
... // set the refresh interval
}

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)的实现,这些算法适用于不同类型的集合,可以被定制,并且类型是安全的
  • 通过使更多的错误在编译时被发现,增加了代码的稳定性

Without Generics
1
2
3
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

With Generics
1
2
3
List<String> listG = new ArrayList<String>();
listG.add("hello");
String sg = listG.get(0) // use without casting

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 and Generic version
  • normal version
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public 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
    5
    Box<integer> intergerBox = new Box<integer>();

    OR

    Box<integer> integerBox = new Box<>();

多类型参数 Multiple Type Parameters

  • 一个泛型类(generic class) 可以有多个类型参数
  • 例如,通用的OrderedPair类,有通用的配对接口
    1
    2
    3
    4
    public intergace Pair<K, V> {
    public K getKey();
    public V getValue();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public 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
    7
    Pair<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
    5
    public 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
    3
    Pair<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
    7
    int 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
    3
    assertThrows(UNSWNoSuchFileException.class, () -> {
    fs.cd("/usr/bin/cool-stuff");
    });
    1
    2
    3
    4
    assertDoesNotThrow(() -> {
    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. 用括号括起来,以逗号分隔的正式参数列表。 无需提供数据类型。 若只有一个参数则可以省略括号
  2. 箭头符号 ->
  3. 主体,由单个表达式或语句块构成
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
// 定义三个接口
public interface MyFunctionInterfaceA {
public int myCompute(int x, int y);
}

public interface MyFunctionInterfaceB {
public boolean myCmp(int x, int y);
}

public interface MyFunctionInterfaceC {
public double doSomething(int x);
}
// 使用lambda表达式实现接口
MyFunctionInterfaceA f1 = (x, y) -> x + y; // f1的myCompute为 x + y

MyFunctionInterfaceA f2 = (x, y) -> x - y + 200; // f2的myCompute为 x - y + 200

MyFunctionInterfaceB f3 = (x, y) -> x > y; // f3的myCmp为计算 x > y

MyFunctionInterfaceC f4 = x -> {
double y = 1.5 * x;
return y + 8.0;
}; // f4的doSomething为计算 1.5 * x + 8.0

// 应用实现的方法
System.out.println( f1.myCompute(10, 20) ); // prints 30
System.out.println( f2.myCompute(10, 20) ); // prints 190
System.out.println( f3.myCmp(10, 20) ); // prints false
System.out.println( f4.doSomething(10) ); // prints 23.0

方法引用 Method References

我们可以将现有方法视为功能接口的实例

使用::操作符可以引用方法:

  1. 静态方法Static method: (ClassName :: methodName)
  2. 特定对象的实例方法: (instanceRef :: methodName)
  3. 类的构造函数: (ClassName :: new)

Java中的功能接口 Function interface

  • java.util.function 包中的功能接口为 lambda 表达式和方法引用提供了预定义的目标类型
  • 每个功能接口都有一个抽象方法,称为该功能接口的功能方法,lambda表达式的参数返回类型与该抽象方法相匹配或适应
  • 功能接口可以在多种上下文中提供目标类型,如赋值,方法调用等
    1
    2
    3
    4
    5
    Predicate<String> p = String :: isEmpty;
    // collect empty strings
    List<String> strEmptyList1 = strList.stream()
    .filter(p)
    .collect(Collectors.toList());
1
2
3
4
// collect strings with length less than six
List<String> strEmptyList2 = strList.stream()
.filter(e -> e.length() < 6) // lambda expression
.collect(Collectors.toList());

有几种基础的功能形状 function shapes:

  1. Function (从T映射到R)

    包含一个名为 apply 的抽象方法,该方法接受一个类型为 T 的参数,返回一个类型为 R 的结果。

  2. Consumer (接受输入类型 T,没有返回值)

    它包含一个名为 accept 的抽象方法,该方法接受一个类型为 T 的参数,执行相应的操作

  3. Predicate (接受输入类型T,返回布尔值)

    它包含一个名为 test 的抽象方法,该方法接受一个类型为 T 的参数,返回一个布尔值表示断言的结果。

  4. Supplier (没有输入值,返回输出类型 R)

    它包含一个名为 get 的抽象方法,该方法不接受参数,返回一个类型为 R 的结果。

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
41
42
43
// Function
Function<String, Integer> func = x -> x.length();
Integer answer = func.apply("Sydney");
System.out.println(answer); // prints 6

// --------------------------------------------
// Function
Function<String, Integer> func1 = x -> x.length(); // func1.apply() will return the length of x
Function<Integer, Boolean> func2 = x -> x > 5; // func2.apply() will return if x > 5 or not
Boolean result = func1.andThen(func2).apply("Sydney");
// 先调用func1.apply("Sydney"), 并将结果6 作为func2.apply()的传入值 -> func2.apply(6)

// --------------------------------------------
// Predicate
Predicate<Integer> myPass = mark -> mark >= 50;
List<Integer> listMarks = Arrays.asList(45, 50, 89, 65. 10);
List<Integer> passMarks = listMarks.stream()
.filter(myPass)
.collect(Collectors.toList());

// --------------------------------------------
// Consumer
Consumer<String> print = x -> System.out.println(x);
print.accept("Sydney"); // will print Sydney

// --------------------------------------------
// Consumer
Consumer<List<Integer>> myModifyList = list -> {
// multiply every integer in the list by 5
for (int i = 0; i < list.size(); i++) {
list.set(i, 5 * list.get(i));
}
}
List<Integer> list = Arrays.asList(45, 50, 89, 65. 10);
myModifyList.accept(list); // multiply each entry in list by 5

// --------------------------------------------
// Consumer
Consumer<List<Integer>> myDispList = myList -> {
// display all the numbers in the given list
myList.stream().forEach(e -> System.out.println(e))
}
myDispList.accept(list); // display the list

使用Lambda功能的比较器 Comparator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Using an anonymous inner class
Comparator<Customer> myCmpAnonymous = new Comparator<Customer>() {
@Override
public int compare(Customer o1, Customer o2) {
return o1.getRewardsPoints() - o2.getRewardsPoints() ;
}
} ;
custA.sort(myCmpAnonymous);

//Using Lambda expression - simple example (only one line)
custA.sort((Customer o1, Customer o2)->o1.getRewardsPoints() - o2.getRewardsPoints());

//Using Lambda expression - Another example (with return)
custA.sort( (Customer o1, Customer o2)-> {
// 先根据postCode比较,如果相等则根据RewardsPoints比较
if(o1.getPostcode() != o2.getPostcode()) {
return o1.getPostcode() - o2.getPostcode() ; }
return o1.getRewardsPoints() - o2.getRewardsPoints();
});

Pipeline & Streams

  • Pipeline 是一系列集合操作(aggregate operation)
  • 以下实例打印了集合名册中包含的男性成员,该pipeline包括聚合操作 filterforEach
    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

  1. 工厂模式
  2. 抽象工厂模式
  3. 生成器模式
  4. 单例模式

行为模式 Behavioral Patterns

  1. 迭代器模式
  2. 观察者模式
  3. 状态模式
  4. 策略模式
  5. 模板模式
  6. 访问者模式
  7. 指令模式 Command Pattern

结构模式 Structural Pattern

  1. 适配器模式
  2. 复合模式
  3. 装饰器模式
  4. 立面模式 Facade 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,有四种状态:
    1. No quarter
    2. Has quarter
    3. gumball sold
    4. out of gumball
  • 创建一个变量保存当前状态,并为每个状态定义值:
    1
    2
    3
    4
       final static int SOLD_OUT = 0;
    final static int NO_QUARTER = 1;
    final static int HAS_QUARTER = 2;
    final static int SOLD = 3;
  • 整合系统中所有可能发生的操作:
    1. insert quarter
    2. turns crack
    3. eject quarter
    4. dispense
  • 现在创建一个类来代表状态机,对于每种操作,创建一个方法并适用条件判断来根据不同的状态执行行为,例如以下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
       public 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

对于之前的设计,我们将重新设计它,将不同的状态对象封装在其自身的类中,并在发生操作时委托给当前的状态
步骤

  1. 定义一个状态的接口,其中应包含gumball machine中每个操作的方法
  2. 对于每个状态,定义并实现一个属于其自己的类,当机器处于对应的状态时,这些类将负责机器的行为
  3. 去掉动作中的所有的条件代码,转而委托给状态类来实现不同的行为
    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");
    }

要点

  1. 状态模式允许一个对象根据其内部状态做出许多不同的行为
  2. 与过程式状态机不同,状态模式将状态表示为一个完整的类
  3. 类通过委托给与其组成的当前状态对象来获取行为
  4. 通过将每个状态封装到一个类中,我们可以本地化任何需要进行的更改
  5. 状态模式和策略模式的类图相同,但意图不同
  6. 策略模式通常用一种行为或算法配置上下文类
  7. 状态模式允许上下文在状态发生变化时改变其行为
  8. 状态转换可由状态类或上下文类控制
  9. 使用状态模式通常会增加设计中的类的数量
  10. 状态类可在上下文实例之间共享

与策略模式的区别

在状态模式中,特定状态知道其他所有状态的存在,且能触发从一个状态到另一个状态的转换
策略则几乎完全不知道其他策略的存在。

观察者模式ObserverPattern

  • 观察者模式用于在 “事件驱动 “编程中实现分布式事件处理系统
  • 在观察者模式中:

    一个被称为主体subject(或可观察对象 observable 或发布者 publisher)的对象,会维护其被称为观察者observers(或订阅者 subscribers)的隶属者列表
    主体会 自动 将其的任何状态变化 通知 观察者,通常是通过调用观察者的方法之一

  • 观察者模式定义了对象之间一对多的依赖关系,因此当一个对象(主体)改变状态时,其所有依赖对象(观察者)都会 自动得到通知和更新
  • 观察者模式的目的应为:

    在对象之间定义一对多的依赖关系,而不使对象紧密耦合
    当主体改变状态时,自动 通知/更新 数量不限 的观察者 (从属对象)
    能够 动态 添加和删除观察者

可能的解决方案 Possible solution

  • 定义 “主体Subject“和 “观察者Observer“接口,当主体改变状态时,所有注册的观察者都会收到通知并自动更新
  • 主体 的职责是维护一个观察者列表,并通过调用观察者的 update() 操作将状态变化通知观察者
  • 观察者 的职责是在主体上注册(和取消注册)自己(以获得状态变化的通知),并在收到通知时更新自己的状态。
  • 这样的方式令 主体和观察者之间保持着 loosely coupled 的关系
  • 可在运行时独立 添加移除 观察者

多观察者和主体

传递/更新数据

主体需要传递(更改)数据,同时向观察者发出更改通知,有两种可能的选择

  1. Push data

    主体将更改后的数据传递给其观察者 update(data1, data2...)
    所有观察者都必须实现上述更新方法

  2. Pull data

    主体将自身的参照传递给观察者,观察者需要从主体获取(提取)所需的数据 update(this)
    主体需要为其观察者提供所需的访问方法 public double getTemperature()

观察者模式的结构

总结

优势

  • 避免主体与观察者之间的紧密耦合 (avoid high coupling)
  • 令主体和观察者就可以在系统中处于不同的抽象层次。
  • 松散耦合对象更易于维护和重复使用
  • 允许动态注册和注销注册

需要注意的

  • 主体的变化可能导致对其观察者的一连串更新,并进而导致对其从属对象的更新,从而产生复杂的更新行为
  • 需要妥善管理此类依赖关系

要点

  1. 观察者模式定义了对象之间的单对多关系
  2. 主体或我们所熟知的观测对象,使用一个通用接口更新观察者的数据
  3. 观察者是松耦合的,因为可观察对象对它们一无所知,只知道它们实现了观察者接口
  4. 在使用模式时,您可以从可观察对象中推送或提取数据(pull被认为是更正确的)

    pull允许观察者在需要的时候获取数据,避免了频繁的推送和可能的资源浪费。
    5.不要依赖于Observer的特定通知顺序

  5. 在需要的情况下,可以自行创建Observable类并实现

复合模式CompositePattern

  • 在 OO 程序设计中,复合对象是由一个或多个类似对象(具有类似功能)组成的对象
  • 目的是让我们能够像操作一组对象一样操作单个对象实例,例如

    调整一组形状大小的操作应与调整单个形状大小的操作相同
    计算文件的大小应与计算目录的大小相同

  • 单一(叶)对象与复合(组)对象之间没有区别

    如果我们区分单个对象和一组对象,代码就会变得更加复杂,因此也更容易出错

样例

计算单个零件或完整子组件(由多个零件组成)的总价,而不必区分零件和子组件

可能的解决方案 Possible solution

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

实现问题: 统一性Uniformity和 类型安全Type Safety

  • 类型安全设计:只在复合类中定义与子类相关的操作
  • 统一性设计:在组件界面中包含所有与子代相关的操作

统一性

  • 在组件接口中包含所有与子代相关的操作,这意味着叶子类需要以 “什么也不做 “或 “抛出异常 “的方式来实现这些方法
  • 客户端可以统一处理叶子对象和复合对象
  • 我们会失去类型安全性,因为叶子类型和复合类型并没有干净地分开
  • 适用于子节点类型动态变化(从叶子节点到复合节点,反之亦然)的动态结构,客户端需要定期执行与子节点相关的操作。例如,文档编辑器应用程序
  • 更适合节点类型经常反复变化的情况

类型安全

  • 只在复合类中定义与子类相关的操作
  • 类型系统会强制执行类型限制,因此客户端无法对 Leaf 对象执行与子对象相关的操作
  • 客户端需要区别对待叶子对象和复合对象
  • 适用于静态结构,在这种结构中,客户端不需要对 “未知” 的组件类型对象执行与子对象相关的操作

总结

  • 复合模式提供了一种结构,既可容纳单个对象,也可容纳复合对象
  • 复合模式允许客户统一处理复合材料和单个对象
  • 组件Component是复合结构中的任何对象,组件可以是其他 复合节点Composite node或叶节点Leaf node
  • 在实施 Composite 的过程中,有许多设计上的权衡。 您需要在透明度/统一性和类型安全性与您的需求之间取得平衡

创造模式

创建模式提供了各种对象创建机制,提高了现有代码的灵活性和重用性

  1. 工厂模式 Factory Method

    提供了在超类中创建对象的接口、 但允许子类改变创建对象的类型

  2. 抽象工厂模式 Abstract Factory method

    让用户在不指定具体类别的情况下生成相关对象

  3. 构建者 Builder

    让用户逐步构建复杂的对象。该模式允许用户使用相同的构造代码生成不同类型和表现形式的对象

  4. 单例模式 Singleton

    让用户确保一个类只有一个实例, 同时为该实例提供一个全局访问点

工厂模式FactoryMethod

  • 工厂方法是一种创建设计模式,它使用工厂方法来处理创建对象的问题,而无需指定将要创建的对象的确切类别
  • 问题

    直接在需要(使用)对象的类中创建对象缺乏灵活性
    它将类与特定对象绑定
    使实例化无法独立于(无需改变)类而改变

  • 可能的解决方法

    为创建对象定义一个单独的操作(工厂方法)
    通过调用工厂方法创建对象
    这样就可以编写子类,改变创建对象的方式(重新定义实例化哪个类)

结构 structure

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

样例 Example

  • 如图,Dialog 类作为factory会在工厂方法 createButton() 中返回不同类型的Button,具体的返回类型取决于子类的类型

抽象工厂模式AbstractFactoryPattern

  • 抽象工厂是一种创建设计模式,它能让你创建相关对象的家族,而无需指定它们的具体类

问题 problem

想象一下,您正在创建一个家具店模拟器,您的代码包含以下内容
> 相关产品系列,例如 椅子 + 沙发 + 咖啡桌
> 该系列的几个变种
> 例如,椅子 + 沙发 + 咖啡桌产品有以下几种款式
>> 艺术类
>> 维多利亚式
>> 现代式

解决方法 solution

  1. 首先, 抽象工厂模式建议为系列中的每件产品明确声明接口 (例如椅子沙发咖啡桌)。 然后, 确保所有产品变体都继承这些接口。 例如, 所有风格的椅子都实现 椅子接口; 所有风格的咖啡桌都实现 咖啡桌接口, 以此类推
  1. 接下来, 我们需要声明抽象工厂(abstract factory)——包含系列中所有产品构造方法的接口。 例如 create­Chair()创建椅子 、 ​ create­Sofa() 创建沙发和 create­Coffee­Table() 创建咖啡桌 。 这些方法必须 返回抽象产品类型 , 即我们之前抽取的那些接口: ​ 椅子,沙发和咖啡桌等等
  1. 对于系列产品的每个变体,我们都将基于 抽象工厂接口(abstract factory interface)创建不同的工厂类。每个工厂类都只能返回特定类别的产品, 例如,现代家具工厂Modern­Furniture­Factory只能创建 现代椅子Modern­Chair、​ 现代沙发Modern­Sofa和 现代咖啡桌Modern­Coffee­Table对象
  2. 客户端代码可以通过相应的抽象接口调用工厂和产品类。 你无需修改实际客户端代码, 就能更改传递给客户端的工厂类, 也能更改客户端代码接收的产品变体。
  3. 客户端无需了解工厂类

结构 Structure

  1. 抽象产品(abstract product)为组成产品系列的 一系列不同但相关的 产品声明接口
  2. 具体产品(concrete product)是抽象产品的各种实现,按变体分组。每个抽象产品(椅子/沙发)必须在所有给定的变体(维多利亚式/现代式)中实现
  3. 抽象工厂(abstract factory)接口 声明 了一套方法,用于创建每个抽象产品
  4. 具体工厂(concrete factory) 实现 抽象工厂的创建方法。每个具体工厂对应一个特定的产品变体,并 只创建这些产品变体
  5. 客户端可以与任何具体的工厂/产品变体合作,只要它通过抽象接口与它们的对象通信即可

与工厂模式的区别 difference with factory pattern

  1. 目的和用途

    工厂模式旨在通过将对象的创建封装到一个共同的接口中,以便根据需求创建不同类型的对象,同时隐藏对象的实例化细节
    抽象工厂模式旨在提供一个接口,用于 创建相关或依赖对象的系列 ,而无需指定具体的类。它允许客户端创建一组相关对象,而不必关心具体的类名

  2. 结构和关系

    工厂模式通常包含一个工厂接口(或抽象类),以及具体的工厂类,每个具体工厂类负责创建特定类型的对象
    抽象工厂模式包含一个抽象工厂接口(或抽象类),定义了一系列可以 创建不同类型对象的方法每个具体工厂类实现了这个接口,用于创建特定系列的对象

  3. 对象创建的区别

    工厂模式关注于对单个对象的创建
    抽象工厂模式关注于对一组相关对象的创建,这些对象之间可能存在关联

  4. 扩展性

    工厂模式相对较容易添加新的具体工厂类,以支持创建新的产品类型
    抽象工厂模式相对较容易添加新的具体工厂类,以支持创建新的产品系列,但不太容易添加新的产品类型

装饰器模式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
    7
    public <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
2
3
4
5
6
7
8
9
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray) {
if (e > elem) { // invalid -> 不是所有泛型都支持 '>' 比较大小
++count;
}
}
return count;
}

正确样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Comparable<T> {
public int comapreTo(T o);
}

// let generic type extend comparable -> it can be compared
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray) {
if (e.compareTo(elem) > 0) { // valid
++count;
}
}
return count;
}

泛型,继承和子类 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

  • 可以通过扩展或实现泛型类或接口来对其进行子类型化
  • 一个类或接口的类型参数与另一个类或接口的类型参数之间的关系由 extendsimplements 子句决定
  • 举例:

    ArrayList<E> implements List<E>
    List<E> extends Collection<E>

  • 因此,ArrayList<String>List<String> 的子类型,而 List<String>Collection<String> 的子类型。
  • 只要不改变类型参数、 类型之间的子类型关系就会得到保留

通用配符 上界 wildcards: upper bounded

  • 在通用代码中,问号 (?) 被称为通配符,代表未知类型
  • 通配符可以在多种情况下使用:作为 参数、字段或局部变量的类型 ;有时作为返回类型
  • 上界通配符 <? extends Foo>(其中Foo是任意类型)匹配FooFoo的任意子类型
  • 可以指定通配符的上限(upper bounded),也可以指定下限(lower bounded),但不能同时指定
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public 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
    15
    public 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 的超级类型(如 IntegerNumberObject)的方法,您需要指定 List<? super Integer> 方法
  • List<Integer>List<? super Integer> 更具限制性
    1
    2
    3
    4
    5
    public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
    list.add(i);
    }
    }

通用配符和子类型 wildcards and subtyping

  • 虽然 IntegerNumber 的子类型,但 List<Integer> 并不是 List<Number> 的子类型,这两种类型并不相关
  • List<Number>List<Integer> 的共同父类是List<?>

测试设计 Test design

软件测试:测试推动开发 Test-Driven Development (TDD)

  • 软件开发过程中的每次迭代之前都必须制定计划,以适当验证(测试)所开发的软件是否满足要求(即后置条件)
  • 软件开发人员不能在开发出软件后才考虑如何对其进行测试
  • 测试是开发软件解决方案不可或缺的重要组成部分。绝不能事后才考虑测试
  • 在每次迭代过程中,必须根据测试套件对增量开发进行测试。每次代码修改和/或重构后,都必须使用预定义的测试套件进行适当测试。
  • 在开始实施解决方案之前,必须根据需求规格设置测试。
  • 软件的开发顺序应为
    1. 要求/规格分析
    2. 接口 (方法签名/前置&后置条件)
    3. 测试集合 (单元测试/集成测试)
    4. 方法实现

软件测试:输入空间覆盖 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

  1. 设定先决条件 Precondition (@BeforeEach 等)
  2. 实施 (调用方法)
  3. 验证后置条件 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MySingleton {

private static MySingleton single_instance = null ;
private LocalTime localTime;

private MySingleton() {
localTime = LocalTime.now();
}

// 适用 synchronized避免多线程同时抢占
public static synchronized MySingleton getInstance() {
if (single_instance == null) {

single_instance = new MySingleton();
}
return single_instance;
}

public LocalTime getTime() {
return localTime;
}
}

并发性入门 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() 调用抽象类(抽象或具体)中定义的方法–如 primitive1primitive2
  • 可通过提供具体方法在抽象类中实现默认行为
  • 重要的是,子类可以更改原始方法来实现不同的行为(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

  1. Visitor接口声明了一组访问方法,这些方法可以将对象结构的具体元素作为参数
  2. 每个 “具体访问者Concrete Visitor“都会针对不同的具体元素类别,实现多个版本的相同行为
  3. 元素接口Element interface声明了一种 “接受”访问者的方法。该方法应有一个参数,该参数应与访客接口Visitor Interface的类型一致。
  4. 具体元素(Concrete Element)必须实现接收方法。 该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。请注意,即使元素基类实现了该方法, 所有子类都必须对其进行重写并调用访问者对象中的合适方法

访问者模式的适用性和限制 Applicability and limitation

  • 适用性
    在以下情况下,将操作移至访问者类是有益的
    • 需要对对象结构进行许多不相关的操作
    • 组成对象结构的类是已知的,预计不会改变
    • 需要经常添加新的操作
    • 算法涉及对象结构的多个类,但希望在一个位置进行管理
    • 算法需要跨越多个独立的类层次结构
  • 限制
    • 由于新的类通常需要为每个访问者添加新的访问方法,因此类层次结构的扩展会更加困难

适配器模式AdapterPattern

  • 适配器模式是一种结构型设计模式,它能使接口不兼容的对象能够相互合作
  • 允许将现有类的接口用作另一个接口,适用于客户类
  • 适配器模式通常用于使现有类(API)与客户端类协同工作,而无需修改其源代码
  • 适配器类Adapter class 映射/连接两种不同类型/接口的功能
  • 适配器模式器为现有的有用类提供了一个包装,使客户类可以使用现有类的功能
  • 适配器模式不提供额外功能

结构 Structure

  • 适配器包含它所封装类的一个实例(adaptee)
  • 适配器会调用封装对象的实例中的方法以达到映射/链接功能

样例 Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class LightningToMicroUsbAdapter implements MicroUsbPhone {
private final LightningPhone lightningPhone;

public LightningToMicroUsbAdapter(LightningPhone lightningPhone) {
this.lightningPhone = lightningPhone;
}

@Override
public void useMicroUsb() {
System.out.println("MicroUsb connected");
lightningPhone.useLightning(); // adapt to the original method
}

@Override
public void recharge() {
lightningPhone.recharge(); // adapt to the original method
}
}

// usage

MicroUsbPhone adapted = new LightningToMicroUsbAdapter(new Iphone());
adapted.useMicroUsb(); // will work

生成器模式BuilderPattern

  • 生成器模式是一种创建型设计模式, 使你能够分步骤step by step创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象

问题

  • 一个复杂的对象需要费力地一步步初始化/构造许多字段和嵌套对象
  • 这种初始化/构造代码通常埋藏在一个带有大量参数的庞大构造函数中。
  • 甚至更糟:散落在客户端代码的各个角落

解决办法

  • 构建器模式将对象构建代码从其自身的类中提取出来,并将其移至称为构建器builder的独立对象中
  • 构建器模式允许你逐步构建复杂的对象
  • 构建器不允许其他对象在构建过程中访问产品
  • 导演类Director定义了构建步骤的执行顺序,而构建器Builder则提供了这些步骤的实现。

结构

  • 构建器接口Builder interface声明了所有类型构建器通用的产品构建步骤
  • 实际的构建器Concrete Builders提供不同的施工步骤。实际的构建器Concrete Builders生产的产品可能不遵循通用接口
  • 产品Product是结果对象。不同构建器构建的产品不必属于相同的类层次结构或接口
  • Director类定义了调用构造步骤的顺序,因此您可以创建并重复使用特定的产品配置
  • 客户端必须将其中一个构建器对象Builder objectDirector关联起来
    1
    2
    3
    b = new ConcreteBuilder1();
    d = new Director(b); // 将builder和director关联起来
    // 这样director可以调用对应的builder去创建对应的产品

适合的应用场景

  1. 构造函数具有多个可选参数时
  2. 希望使用代码创建不同形式的产品 (例如石头或木头房屋) 时
  3. 使用生成器构造组合树或其他复杂对象

和其他pattern的关联

  1. 许多设计都是从使用工厂方法Factory pattern(不太复杂,可通过子类进行自定义)开始,然后发展到抽象工厂Abstract Factory或生成器Builder(更灵活,但更复杂)
  2. 构建器Builder侧重于逐步构建复杂对象
  3. 抽象工厂Abstract Factory专门创建相关对象族Families of related objects
  4. 抽象工厂Abstract Factory会立即返回产品,而 生成器Builder会让你在获取产品前执行一些额外的构造步骤
  5. 构建器Builder何时返回生成的产品取决于director/client

Finished