Lec1-入门与项目管理

课程简介

  • Java语言及其初步使用技巧
  • 基础的层次化设计思维
  • 讲练结合,每周一个小作业
  • 使用OO平台,但略去互测
  • 7次作业,逐步迭代
  • 目标:一周以内开发200行有效代码的Java程序

快乐体验:8次课,7次丝滑的迭代作业

image-20230908140729535

常见错误

image-20230908151546218 image-20230908151828850

什么是面向对象

面向对象:以对象为中心来构建程序逻辑的方法

  • 一切皆对象
  • 程序逻辑:数据及其关系、行为及其关系

这个概念来抽象化“对象”,一个类可示例化为任意数目的对象

  • 类:数据和行为的综合体
  • 类关系:表征数据之间的关系或行为间关系

提供了控制复杂性的重要机制

C语言写结构体和函数是分离的,面向对象将其结合在一起

将数据和行为都封装在一起了

三个基本特征

  1. 封装:让内部复杂性外部不可见

    • 封装了数据和方法,通过可见性来控制外部对内部数据和方法的访问
    • 外部不用了解内部的情况,内部可进行改变而不影响外部
  2. 继承:通过抽象层次来协同降低复杂性

    • 已有的东西不重复造轮子,相似的东西提炼出来
    • 一个类从另一个类中获得属性和方法
      • 建立了上下层关系:子类和父类
    • 子类可复用父类的设计和实现,也可以进行扩展
      • 可以增加新的属性和方法
      • 也可以重写父类已经实现的方法
  3. 多态:通过多种形态来解耦复杂性

    • 针对同一个指令,一个类有多种响应状态
      • 本质上是有多种同名方法
      • 要么在不同类实现,要不有不同的参数
    • 针对同一个指令的多种处理逻辑,OO语言提供了根据对象和输入参数来匹配的方法:分派机制

class

编译和执行的基本单位:类(class

类由属性和方法组成

  • 属性:定义数据结构
  • 方法:定义对数据结构的操作函数

每个类都有一种构造方法,用于实例化对象

把结构体和操作函数封装在了一起

接口interface

接口(interface)和类处于同一层次

  • 接口中只有方法,没有属性
  • 接口中的方法没有实现体

任何类都可以实现一个接口,即实现接口中所定义的所有方法

  • 一旦实现了一个接口,就可以使用该接口类型来引用对应的对象

关系

类关系:继承、关联

类和接口之间:实现、观念

接口间:集成

访问

访问权限:public,protected,private

访问:obj.attr,obj.func()

关键词

static

image-20230908150254070

this

image-20230908150612514

Object

相当于万能指针

image-20230908150604286

版本控制

  • 多人协同开发:控制多人修改对项目的修改所带来的影响

    • 了解哪些改动,以及改动带来的影响
  • 版本管理工具建立不同的分支

    • 分支互不影响

使用 gitlab:基于文件提交十分繁琐,将项目文件提交到gitlab,评测机自动从gitlab拉取。

Lec2-编写类与单元测试

Java程序的类

类为编程单位,而不是C语言中的函数。规模控制、逻辑控制,形成层次化结构。

类是一种自定义数据类型,包含数据和操作数据的方法

  • 相当于对结构体和操作结构体的函数的封装

  • public class ClassName {
        //属性定义
        //方法定义
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    - 属性和方法再类中的位置是自由的,处于同一层次

    - 方法中定义的变量是局部变量,不是类的属性


    ### 主类

    一个java程序只能有一个类拥有main方法,为程序入口,称为主类

    - ```java
    public static void main(String[] args){...}
  • 一般不设置属性

  • 常见流程

    • 构建业务类对象
    • 获取输入
    • 调用业务类对象处理获得的输入
      • 把任务交给对象,再main中只做顶层处理
    • 打印输出处理结果

类成员的可见性

类方法中能否访问另一个对象或类中成员的规则

  • public:任意外部对象都能访问
  • protected:任意本类或子类对象都能访问
  • private:只有本类对象才能访问

构造方法

构造方法用来初始化对象属性,java默认为每个类“配”一个无参数的构造方法,所有属性都按照默认值来初始化。一旦用户自己定义了一个,原有的那个就失效。

用来初始化类中的属性

重名的方法

java允许在一个类中定义多个重名的方法

  • 前提:在参数列表必须有差异:列表序列和个数
    • 返回值不可作区分:某种程度上返回值是不重要的

面向对象重载:

  • 多个方法重名,说明具有相近的功能
  • 一个方法有多个相近但有差异的功能
  • 提供多个构造方法是常见的,用于不同的对象的初始化

创建对象

设计好类后,就可以创建对象,有些全为private的类不一定能实例化:只能在类内部

对象引用与方法调用

使用类声明的一个变量,相当于一个指针,通过对象引用可以访问所引用对象的属性和方法

1
2
3
Object ba= new String("sss");
Object ca= new bankAccount(500);
//Object 相当于万能类型

方法调用的参数传递

java中除了基础类型,所有使用类所声明的变量都是引用,使用任意类型声明的数组变量也是引用

参数调用时:

  • 基础类型直接传递值(拷贝)
  • 引用类型传递医用本身(地址)
  • 本质都是值传递,不过引用类拷贝地址

容器

java提供了一套容器解决方案:管理数量不定且动态变化,有灵活的遍历和插入方法

1
2
3
4
5
6
class Adventurer {
private int id;
private String name;
private ArrayList<Bottle> bottleArray;
private HashMap<Integer, Bottle> bottleMap;
}

容器中的元素实际上是对象(也就是说,容器其中的元素类型不能是int、char等这样的基本类型),一些常见的基本类型可以使用它的包装类。

基本类型对应的包装类表如下:

基本类型 引用类型
boolean Boolean
int Integer
char Character
float Float
double Double

ArrayList

ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制

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
44
45
46
47
/* ArrayList 类位于 java.util 包中,使用前需要引入它,语法格式如下:*/
import java.util.ArrayList; // 引入 ArrayList 类

public class ArrayListSample {
public void sample() {
/* 1. 创建ArrayList */
/* ArrayList<类名ClassName> 数组名ArrayName = new ArrayList<> */
ArrayList<Bottle> bottles = new ArrayList<>();

Bottle bottle1 = new Bottle(/*parameters*/);
Bottle bottle2 = new Bottle(/*parameters*/);

/* 2. 向ArrayList内加入一个元素(此外, 还可以向任意位置插入元素, 在add方法中新增参数即可) */
/* 数组名ArrayName.add(元素名elementaryName) */
bottles.add(bottle1);
bottles.add(bottle2);

/* 3. 访问ArrayList中下标为i的元素 */
/* 数组名ArrayName.get(i) */
Bottle bottle = bottles.get(0); // == bottle1

/* 4. 判断元素是否在容器内 */
if (bottles.contains(bottle)) { // true
System.out.println("We have such a bottle!");
}

/* 5. ArrayList大小 */
/* 数组名ArrayName.size() */
int size = bottles.size();

/* 6. 遍历ArrayList中的所有元素 */
for (Bottle item : bottles) {
System.out.println(item.getName()); // getName方法是Bottle类中用于获取其name属性的方法
}

for (int i = 0; i < bottles.size(); i++) {
System.out.println(bottles.get(i).getName());
}

/* 7. 删除一个元素 */
/* 数组名ArrayName.remove(对象名) */
bottles.remove(bottle1);
/* 数组名ArrayName.remove(i) */
bottles.remove(0); // 删除了bottle2

}
}

hashmap

是散列表,提供了key-value映射,keyvalue都是对象引用

arraylist中对象引用有序,hashmap无序,不会记录插入的顺序,通过key对象映射到相应的value对象

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/* HashMap 类位于 java.util 包中,使用前需要引入它,语法格式如下:*/
import java.util.HashMap; // 引入 HashMap 类

public class HashMapSample {
public void sample () {

/* 1. 创建HashMap */
/* HashMap<引用类型Key,引用类型Value> 哈希表名mapName = new HashMap<> */
HashMap<Integer, Bottle> bottles = new HashMap<>(); // bottle's id => bottle

Bottle bottle1 = new Bottle(/*parameters*/);
Bottle bottle2 = new Bottle(/*parameters*/);

/* 2. 向HashMap内加入一个元素 */
/* 数组名mapName.put(引用对象key,引用对象value) */
bottles.put(12345,bottle1);
bottles.put(bottle2.getId(),bottle2);

/* 3. 访问HashMap中key值对应的value */
/* 哈希表名mapName.get(key) */
Bottle bottle = bottles.get(12345); // == bottle1

/* 4. 检查HashMap中是否存在指定的 key 对应的映射关系。 */
/* mapName.containsKey(key) */
if (bottles.containsKey(12345)) { // true
System.out.println("We have such a bottle!");
}

/* 5. 检查HashMap中是否存在指定的 value 对应的映射关系。*/
/* mapName.containsValue(value) */
if (bottles.containsValue(bottle2)) {
System.out.println("We have such a bottle!");
}

/* 6. HashMap大小, 即键值对数目 */
/* 数组名mapName.size() */
int size = bottles.size();

/* 7. 遍历HashMap中的所有元素 */
for (int key : bottles.keySet()) { // keySet可以获取HashMap容器中所有 key 组成的对象集合
System.out.println("bottle's function is " + bottles.get(key).getName());
}

for (Bottle value : bottles.values()) { // values可以获取HashMap容器中所有 value 组成的对象集合
System.out.println("bottle's function is " + value.getName());
}

/* 8. 删除一个映射关系 */
/* mapName.remove(key) */
// bottles.containsKey(12345) == true
bottles.remove(12345); // true
// bottles.containsKey(12345) == false


/* 9. 删除一个键值对
/* mapName.remove(key, value) 键值对能被删除的条件为:当且仅当HashMap存在该 key-value 键值*/
// bottles.containsKey(bottle2.getid()) == true
bottles.remove(bottle2.getid(), bottle1); // false
// bottles.containsKey(bottle2.getid()) == true 此处仍然为真!
bottles.remove(bottle2.getid(), bottle2); // true
// bottles.containsKey(bottle2.getid()) == false;
}
}

Java程序的单元测试

一个类在语法上必须完整,否则无法编译

单元测试

测试的目的是发现bug,调式是定位和修复bug。原则上每个类都应该单元测试

使用junit来进行测试:

  1. 在测试前,在test文件夹中生成测试文件并进行编写

  2. 在test文件中,可以相对独立地运行测试代码中的一部分而不影响main主类中的数据

    • 需要使用assert断言来判断是否运行得到了正确的运行结果

    • 相当于自己编写数据进行测试

    • // 需要import对应的junit包
      import org.junit.Test;
      public class ChildTest {
      
          @Test
          public void subMoney() {
              Child child = new Child(20);
              child.subMoney(5);
              assert (child.getMoney() == 15);
          }
      
          @Test
          public void addOneFruit() {
              Child child = new Child(20);
              child.addOneFruit("apple");
              assert (child.getAppleCount() == 1);
      
              child.addOneFruit("banana");
              assert (child.getBananaCount() == 1);
          }
      }
      
      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

      #### assert语句

      和C语言中的assert类似,对于`assert(condition)`,当条件为真时跳过,否则会报错并停止程序运行

      #### 测试的覆盖率

      在测试中,很重要的一个衡量标准就是覆盖率:究竟覆盖了多少代码的范围,究竟有没有测到真正可能出现bug的地方

      - 函数覆盖率:定义的函数中有多少被调用
      - 语句覆盖率:程序中的语句有多少被执行
      - 分支覆盖率:有多少控制结构的分支(例如if语句)被执行
      - 条件覆盖率:有多少布尔子表达式被测试为真值和假值
      - 行覆盖率:有多少行的源代码被测试过
      - 类覆盖率:有多少类被测试过
      - 方法覆盖率:这个类有多少方法被测试过
      - 行覆盖率:有多少行的源代码被测试过

      # Lec3-如何管理对象

      ## 对象和引用

      引用:程序中声明的变量

      对象:程序通过new产生的**实例**

      - 堆内存中的一个空间,按照类的属性填充了相应的数据,包含**属性数据和指向类的指针**,指向类的指针用于找到对应的指针

      - 方法定义在类而不是对象中,调用类的方法时,从类中找到指向类的指针,调用其中的方法

      - 使用不同的变量去引用一个对象时,不改变对象本身的数据
      - ![image-20230922142044932](https://pigkiller-011955-1319328397.cos.ap-beijing.myqcloud.com/img/202309221420283.png)

      - Java的对象引用相当于C的指针变量,指向一片内存

      - Java通过new初始化对象空间,C通过malloc等分配指针指向的内存

      - ```java
      Adventurer adventurer = new Adventurer(); //Java
      Adventurer *adventurer = (Adventurer *) malloc(sizeof(Adventurer)); //C

对象和引用的区别:

  • 对象有实际存在的内存空间,引用是一个标识符,指向对象所在的地址
  • 引用对象内容的唯一方式是通过一个对象引用去访问其方法或数据
  • 对象引用的内容改变可以随时通过变量赋值来实现

常见容器

image-20230922143049773

ArrayListLinkedList

  • ArrayList相当于数组,LinkedList相当于链表,在使用方式上一致,在底层构建上有差异,因而在查询增删上有区别
  • 实际存储的不是对象本身,而是指向对象的指针
    • 可以先得到查询对象的指针,再remove对象,接着按照指针继续访问对象

HashMap

基于哈希表的键值对应关系,允许使用键来快速查找和访问值。

没有固定的值,键不能重复

HashSet

  • 本质是只存了值的HashMap

  • 保存了内部的互异性,无重复,可以实现自动去重的功能

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
/* HashSet 类位于 java.util 包中,使用前需要引入它,语法格式如下:*/
import java.util.HashSet; // 引入 HashSet 类

public class HashSetSample {
public void sample () {

/* 1. 创建HashSet */
/* HashMap<引用类型> <setName> = new HashSet<> */
HashSet<Bottle> bottles = new HashSet<>();

Bottle bottle1 = new Bottle(/*parameters*/);
Bottle bottle2 = new Bottle(/*parameters*/);

/* 2. 向HashSet内加入一个元素 */
/* 集合名<setName>.add(对象) */
bottles.add(bottle1);
bottles.add(bottle2);
bottles.add(bottle1); // 重复添加的元素不会被加入HahsSet中

/* 3. 检查HashSet中是否存在指定的元素 */
/* setName.contains(obj) */
if (bottles.contains(bottle1)) { // true
System.out.println("We have such a bottle!");
}

/* 4. HashSet大小, 即元素个数 */
/* setName.size() */
int size = bottles.size();

/* 5. 遍历HashSet中的所有元素 */
for (Bottle obj : bottles) {
System.out.println("bottle's function is " + obj.getName());
}

/* 6. 删除HashSet中的指定元素
/* setName.remove(obj) */
bottles.remove(bottle2);
}
}

其他容器

image-20230922150329252

层次化管理对象

使用容器管理一组对象

一个对象可以用内置容器来管理下一组对象

组合关系

组合:由多个对象组合成一个更多的对象,整体和部分的关系。以对象引用作为属性变量,形成了对象之间的整体和部分结构

聚合关系

聚合:弱关联关系,以对象引用作为其成员变量,相应的对象引用可以是NULL

属性

属性关系:

  • 类的职责确认

  • 组合:顶层对象之间不会有共享对象,一个对象只会出现在一个容器中

  • 聚合:顶层对象之间会有共享对象,一个对象有可能在多个容器中

  • 容器只储存指针,不储存值

Lec5-Java程序的bug与调试

Java 的内存情况

JVM 的内存划分为 5 个部分:虚拟机栈、堆区、元空间、本地方法栈、程序计数器。其中前三个比较重要,后两个和 JVM(java virtual machine)的运行调度有关。

  • 栈(stack):中存放方法中的局部变量,方法的运行一定要在栈中进行。栈内存的数据用完就释放。 这里简单称为栈,但实际上是 JVM 栈,Java 虚拟机栈
  • 堆(Heap):是 Java 虚拟机中最大的一块内存区域,用于存储对象实例。通俗的理解,就是我们 new出来的 所有对象和数组,都是存放在堆中的。
  • 元空间(MetaSpace):虚拟机加载的类信息、常量、各类方法的信息。

jdk1.8之前元空间又作方法区(Method Area),相比元空间,方法区额外存储 .class 二进制文件,虚拟机加载的静态变量等数据。

jdk1.8及之后,类的元信息被存储在元空间中。相比方法区,元空间使用与堆不相连的本地内存区域。所以,理论上不会出现永久代存在时的内存溢出问题。

具体实现

  • 首先,程序会将 main 方法压入调用栈中。
  • main 方法中声明了一个局部变量 Dog dog ,该变量同样被放置在调用栈中。接下来,我们使用 new 关键字创建了一个小狗对象,并将其存储在堆中的一块内存空间中,然后将堆中对应的地址存储在调用栈中。这个堆中的内存用于存储小狗的属性,而堆中的成员方法只保存一个地址,指向元空间中实际内容的位置。
  • 同样的操作也适用于 Person person = new Person(…) , 创建一个 person 对象并将其地址存储在调用栈中。
  • 接下来,调用 Person 类的 buy 方法并将 dog 作为参数传入,实际传入的内容就是调用栈中dog 变量所存内容,即dog在堆中的地址。
    • buy 方法被压入调用栈中。
    • buy 方法中,首先调用了 Dog 类的 getPrice() 方法。
    • getPrice() 方法被压入调用栈中。
    • 通过 dog 在堆中的地址,找到对应的属性 price 并将其作为结果返回。
    • getPrice() 方法结束,弹栈。
    • 然后调用了 Dog 类的 setBought() 方法。
    • setBought() 方法被压入调用栈中。
    • 通过 dog 在堆中的地址,找到对应的成员方法,并在元空间中执行该方法对应的程序代码,即将 dog 在堆中的属性 isBought 修改为 ture
    • setBought() 方法结束,弹栈。
    • 最后,将 dog 作为参数继续传递给 Ming 在堆中的属性 pet,即将 dog 在堆中的地址存储在 pet 中。
    • buy 方法结束,弹栈。
  • main 方法中的所有内容执行完毕,会从调用栈中弹出 main 方法,程序结束运行。

深克隆和浅克隆

执行 dog2 = dog1 时,我们传递的给 dog2 的,是 dog1 在堆内存中的内存地址,所以 dog2 和 dog1 指向了相同的堆内存。通俗来讲,本质上 dog1 和 dog2 就是同一条狗。所以不论是修改哪条狗的参数,结果均是执行/修改 0x1234 这块内存的内容。

知识补充-基础数据类型&引用类型的传递方式:基础数据类型为值传递,引用数据类型为引用传递。

基础数据类型(int、boolean)在作为参数传递时,传递的是真实的数据,形参的改变,不影响实参。

引用数据类型(类、数组、接口)作为参数传递时,传递的是堆内存中的地址,形参改变,实参也改变。

而上述这种克隆过程,在 Java 术语中可以解释为,我们在程序中创建了一个 Dog 实例 ,使用 dog1 引用 了这个实例。之后声明的 dog2 变量 ,重新引用了 dog1 引用的实例。因此在整个程序运行过程中,仅有一个实例。故当我们使用这两个引用中任意一个引用来对其实例进行修改时,会在使用另一种引用访问该实例中体现出来。

这种只克隆引用的克隆过程,称为 浅克隆 (Shallow copy)。如果希望创造出一个“完整”的克隆,我们不仅要在编码时创建一个新的引用,还要创建一个新的实例,即,创建出另一块堆内存,使得二者互不干涉,互不影响。

常见bug

==与equals

当要求的是两个对象是同一个对象时,使用==是正确的。当要求的两个对象不必为同一个,只需要某些属性相等时(这些属性可以在 equal 是方法中自由定义)

迭代器删除

遍历容器的过程中,可能用到的是 ArrayList的索引,当然也有同学使用的是 hashmap 这种没有“第几个”说法的容器,就可能会用到for增强循环:

1
for (Adventure adventure:adventure)

其实它的本质是用迭代器遍历

1
2
3
4
5
Iterator<Dog>iterator= arrayList.iterator();
while (iterator.hasNext()){
Dog dog=iterator.next();
//....
}

使用ArrayList的删除,也就是容器本身的删除方法时,确实能修改容器的状态,但是它不能修改迭代器的状态。这导致迭代器的状态和当前 ArrayList 的状态不相同,而在每次 next() 的时候会判断这个状态是否相同,导致报错。

应该使用迭代器本身提供的删除一个方法:

1
2
3
4
5
6
Iterator<Dog>iterator= arrayList.iterator();
while (iterator.hasNext()){
Dog dog=iterator.next();
iterator.remove();
System.out.println(dog.name);
}
  • 遇到删除的不使用 for 循环,而使用迭代器遍历,删除的时候使用迭代器的删除方法
  • 将删除和遍历分开,比如把要删除的对象用一个容器先存起来,遍历后统一删除。

Lec6-继承与接口的复用

继承的概念

继承相当于在原先类上的扩展,如果几个类之间只有几处不同,大量的属性和方法可以共享,可以使用继承。

在实际的开发中, 如果细化的类较多,会出现重复的属性定义和操作代码,无法统一管理各种细化类的实例化对象,在代码维护过程中容易出现修改不一致,难以debug

  • 继承是类之间的一种抽象层次关系
  • 继承让 子类将获得父类的属性和方法,实现 复用和扩展
  • 继承 可以把多个类中的重复内容提取出来形成父类,减少冗
    余和增强可维护性

extends

利用extends关键字来定义继承关系

  • 子类可以访问publicprotected成员,private无法被直接访问,调用父类的方法来进行访问修改
  • 父类可以概括子类:子类对象可以被父类对象所引用

子类和父类之间有指针的关系,通过指针进行属性和方法的访问。如果子类中没有,则向上在父类中访问

super

在子类中,使用super. attribute可以引用父类中定义的非私有属性

如果子类 重写 了父类的某个方法,那么子类通过super.methodName()可以调用父类所实现的那个方法

(this.) methodName()调用的是子类实现的那个方法

在子类构造方法中,一般使用super(arguments)来调用父类的构造方法,从而完成对父类所定义属性的初始化

重写方法

@Override注解可选的注解,用于标记方法是重写的父类方法。非强制,可提高代码的可读性和可维护性

访问权限:子类方法的访问权限必须大于等于父类方法的访问权限

  • 如果父类方法为public,子类方法只能为public
  • 返回类型:子类方法的返回类型必须与父类方法的返回类型一致,或者是其子类型(协变返回类型)
    • 若返回类型是基本类型,则只能相同
  • 方法名和参数列表:子类方法的方法名和参数列表必须与父类方法完全相同
    • 参数个数和参数顺序、参数类型

继承的使用

继承的优势

代码重用:子类无需复实现父类的属性和方法,提高了重用度

扩展性:

  • 子类可以添加新的属性和方法实现自己独有的功能
  • 实现了对父类的增量式扩展(保持父类不变)

继承层次:

  • 形成抽象层次结构,任何子类对象都可以使用父类型来统一管理和引用

  • 实现了对各种变化的统一处理能力

接口

接口( Interface )是一种定义方法和常量的抽象类型,不提供方法实现

接口统一并规范定义一组类的行为。一个接口可以由N个类实现,一个类可以实现N个接口

接口的实现

使用implements关键字

抽象方法的声明不需要public或者 private 等关键字的修饰规定方法名,参数及返回类型即可

接口也是一种类型,可以用 a instanceof B 判断对象 a 是否实现了接口 B ,即是否类型 B 的实例

接口无法被实例化(用 new ),只能被实现

接口和继承的区别

image-20231020152005887

装备 、三种药水瓶、食物和冒险者都有price(价格)属性,因而可以将它们都看作是价值体Commodity,且他们都有 getPrice 函数来对外表征自己的价值

  • 向上提炼
  • Bottle Equipment Food 和 Adventurer 这四种价值体的数据内涵和业务功能差别很大
    • 核心是围绕其价值( price )建立统一的对象管理手段
    • 把 Commodity定义为interface,让Bottle 、Equipment 、Food 和Adventurer实现Commodity接口
    • 三种具体的Bottle仍然继承自Bottle
    • 建立了实现行为抽象层次

image-20231020151938387

指导书相关

继承中的转型

向上转型

在建立了继承关系之后,可以使用父类型去引用通过子类型创建的对象。这里涉及两个重要的概念,对象与对象引用。一般而言,对象是一个类的实例化结果,对应内存中的一个数据结构。对象引用则是使用一个变量来指向内存中的这个数据结构(即对象)。

如我们可以使用上面的 Dog 类来构造一个对象:new Dog() ,这条语句返回一个创建的对象。我们同时需要声明一个对象引用来指向返回的对象,否则可能就找不到这个对象了。所以,一般代码都会这么写:Dog bernese = new Dog()

在建立了继承关系之后,我们也可以使用 Animal 类来声明一个对象引用,并指向类型为 Dog 的对象:Animal pet = new Dog(...)。从程序类型的角度,这个表达方式称为向上的类型转换,简称向上转型 (up cast)。

相关例子在继承中的方法调用给出,同学们可以仿照案例课下再做尝试

向下转型

Java 语言提供了一个特殊的关键词 instanceof 用来判断一个对象引用所指向的对象的创建类型是否为特定的某个类,一般写为 obj instanceof A,其中 obj 为一个对象引用,A 为一个类型(类或接口),这个表达式的取值结果为布尔型,如果 obj 的创建类型为 A,则结果为 true,否则为 false。在这个表达式取值为 true 的情况下,可以使用向下转型 (down cast) 来使用一个 A 类型的对象来引用obj: A ao = (A)obj 。注意,实际上 obj 所指向对象的创建类型永远不会发生变化,转型的只是对象引用类型。下面例子给出了相应的向下转型场景:

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
import java.util.Random;

public class Main {
public static void main(String[] args) {
Animal pet;
if (new Random().nextInt() > 0) { // 随机一个整数
// 若大于零 则生成一只伯恩山小狗
pet = new Dog("Bernese Mountain", 18000);
} else {
// 若小于零 则声称一只缅因猫
pet = new Cat("Maine Coon", 8000);
}

/* 值得注意的是,
* 在 `instanceof` 返回真的时候使用向下转型,才能保证向下转型的安全性,否则运行时会触发错误*/

if (pet instanceof Dog) {
System.out.println("this is a dog!");
Dog bernese = (Dog) pet;
bernese.information();
} else if (pet instanceof Cat) {
System.out.println("this is a cat!");
Cat maine = (Cat) pet;
maine.information();
} else {
System.out.println("this is an unknown species");
}
}
}

接口

如果说继承是一种类和类之间的共性抽取,那么接口可以认为是行为的规范标准。相较于继承是对类迭代属性,接口更多的是覆写方法。 我们仍然看回宠物商店,宠物店在管理宠物的过程中衍生了三个部门,一个是洗澡部,一个是干饭部,一个是购买处。

我们观察这三个部门

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
44
45
46
47
48
49
50
public class Bath {
public void sendIn(String origin) {
System.out.println("A pet is sent from " + origin);
}

public void sendOut(String target) {
System.out.println("Bathing department sends a pet to " + target);
}

public void operation(Animal animal) {
System.out.println("Bathing starts");
animal.enhanceHealthCondition(30);
System.out.println("Bathing finishes");
}
}

public class Feed {
public void sendIn(String origin) {
System.out.println("A pet is sent from " + origin);
}

public void sendOut(String target) {
System.out.println("Feeding department sends a pet to " + target);
}

public void operation(Animal animal) {
System.out.println("Feeding starts");
animal.enhanceHealthCondition(10);
System.out.println("Feeding finishes");
}
}

public class Purchase {
public void sendIn(String origin) {
System.out.println("A pet is sent from " + origin);
}

public void sendOut(String target) {
System.out.println("Purchasing department sends a pet to " + target);
}

public void operation(Animal animal) {
System.out.println("Negotiating price....");
if (animal instanceof Dog) {
System.out.println("A dog sells for " + animal.getPrice() + "¥");
} else if (animal instanceof Cat) {
System.out.println("A cat sells for " + animal.getPrice() + "¥");
}
}
}

发现这三个工作部门的工作模式非常相近,都有统一的三个行为:接受宠物,处理宠物,将宠物置位。 就像我们对小猫和小狗进行的共性抽取,此时我们对他的行为模式规范进行提取。

接口的概念

接口 是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能)。

需要注意的是,接口提供了行为的抽象机制。在上面的例子中,Bath 、Feed 、Purchase 的共性在于其行为操作,因而使用接口是合适的。对于其他一些情况,多个类之间可能即有共性的行为,也有共性的数据属性,此时使用类建立抽象层次更加合适。

接口的格式

设置接口

1
2
3
public interface NameOfInterface {
public [返回值] nameOfFunction (args..);
}

接口中的方法默认被public static abstract修饰,设置实现类

1
2
3
4
5
6
public class A implements NameOfInterface {
/* 属性们 */

/* @Override
* ... */
}

在本案中,我们根据BathFeedPurchase 的共性操作,设置一个接口叫做 Department

1
2
3
4
5
public interface Department {
public void sendIn(String origin);
public void sendOut(String target);
public void operation(Animal animal);
}

然后声明BathFeedPurchase 类来实现 (implements) 这个接口: 需要注意的是,当类实现接口的时候,类要实现接口中所有的方法。否则,类必须声明为抽象的类。 在实现接口的时候,也要注意一些规则:

  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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class Bath implements Department {
@Override
public void sendIn(String origin) {
System.out.println("A pet is sent from " + origin);
}
@Override
public void sendOut(String target) {
System.out.println("Bathing department sends a pet to " + target);
}
@Override
public void operation(Animal animal) {
System.out.println("Bathing starts");
animal.enhanceHealthCondition(30);
System.out.println("Bathing finishes");
}
}

public class Feed implements Department{
@Override
public void sendIn(String origin) {
System.out.println("A pet is sent from " + origin);
}
@Override
public void sendOut(String target) {
System.out.println("Feeding department sends a pet to " + target);
}
@Override
public void operation(Animal animal) {
System.out.println("Feeding starts");
animal.enhanceHealthCondition(10);
System.out.println("Feeding finishes");
}
}

public class Purchase implements Department{
@Override
public void sendIn(String origin) {
System.out.println("A pet is sent from " + origin);
}
@Override
public void sendOut(String target) {
System.out.println("Feeding department sends a pet to " + target);
}
@Override
public void operation(Animal animal) {
System.out.println("Negotiating price....");
if (animal instanceof Dog) {
System.out.println("A dog sells for " + animal.getPrice() + "¥");
} else if (animal instanceof Cat) {
System.out.println("A cat sells for " + animal.getPrice() + "¥");
}
}
}

接口中的属性的访问

接口不能包含属性(即实例变量),但是在 Java 8 及之后的版本中,接口可以定义常量(使用public static final修饰符),其他类可以直接通过接口名访问这些常量。

格式: public static final 数据类型 常量名称 = 数据值; 接口中的常量必须进行赋值,同时一经赋值便不可改变。这里可以被理解成,接口本身就是一个规格,一个模范,他的属性也必须是一个标准化的常量。

接口通过多态的形式实现实例化

不能用接口类型来实例化一个对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {
public static void main(String[] args) {
//Department是一个接口,由接口传入对应的变量进行工作
Department bath = new Bath();
Department feed = new Feed();
Department purchase = new Purchase();

Animal bernese = new Dog("Bernese Mountain", 18000);
bath.sendIn("Factory");
bath.operation(bernese);
bath.sendOut("Feeding Department");
System.out.println("");
feed.sendIn("Bathing Department");
feed.operation(bernese);
feed.sendOut("Purchase Department");
System.out.println("");
purchase.sendIn("Feeding Department");
purchase.operation(bernese);
purchase.sendOut("Customer");
}
}

实现接口类的管理

对于宠物商店,需要集中管理者三个部门,即实现了Department接口的三个类。 当我们需要管理实现接口的类时,可以使接口作为泛型,由此达到容器存储时容器泛型的统一书写。

我们举一个例子,每到月底,宠物商店都需要给这三个部门发钱…… 首先我们在Department接口中新增一个方法

1
public void getPaid(int wage);

然后分别在实现接口的三个类中,重写发工资方法

1
2
3
4
5
6
7
8
/* 剩下两个类同理 */
public class Bath implements Department {
/*...*/
@Override
public void getPaid(int wage) {
System.out.println("Bathing department gets " + wage);
}
}

最后, PayDay!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) {
Department bath = new Bath();
Department feed = new Feed();
Department purchase = new Purchase();

HashMap<String ,Department> departments = new HashMap<>();
departments.put("Bath", bath);
departments.put("Feed", feed);
departments.put("Purchase", purchase);

for (Department department : departments.values()) {
department.getPaid(new Random().nextInt(8000,10000));
}
}
}

输出

1
2
3
Purchasing department gets 9245
Bathing department gets 8865
Feeding Department gets 9221

Lec7-设计模式入门

封装 :内部复杂性外部不可见

  • 状态和行为封装在类中,隐藏内部实现细节
  • 对象之间通过方法进行交互

抽象 通过抽象层次来协同降低复杂性

  • 利用继承或接口实现机制,建立数据或行为抽象层次
  • 形成代码重用和层次化设计,避免冗余

多态 :通过多种形态来解 藕处理 行为 的内在复杂性

  • 按照处理行为的内在差异,设计为多个名字相同但处理逻辑不同的方法
  • 重载和重写

多态

父类定义的共性行为可由子类根据自身的具体情况来定义

设计模式

模式:针对某种具有一定特征的问题所形成的一般性解决方案

  • 可以适用于多种不同的具体应用,关键是识别其中的问题特征

模式组成

  • 名称
  • 动机,问题及其特征
  • 解决方案
  • 参与者和协作者
  • 效果
  • 实现

单例模式

单例模式是确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

知识回顾-实例 实例指的是通过使用类创建的具体对象。它占用内存并持有对象的数据和方法。实例是真正存在的,可以执行各种操作和访问其属性。

1.如何保证有且仅有一个实例?

该类的构造方法一定是 private,即不可以被外界进行实例化。且这个实例是属于当前类的静态成员变量

1
2
3
4
5
6
7
8
9
public class Singleton {
// 注意static
private static Singleton singleton;

// 注意private
private Singleton(/*parameters*/) {
/* ... */
}
}

2.如何向整个系统提供这个实例?

该类应提供一个静态方法,能够向外界/系统提供当前类的实例

1
2
3
public static Singleton getInstance()  {
return singleton;
}

3.在什么时机进行该类单例的实例化?

  • 饿汉式:在类加载时就进行实例化
1
2
3
public class Singleton {
private static Singleton singleton = new Singleton(/* paramters */);
}
  • 懒汉式:在第一次使用时进行实例化
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private static Singleton singleton;

/* ... */

public static Singleton getInstance() {
// 如果是第一次使用:
if (singleton == null) {
singleton = new Singleton(/* paramters */);
}
return singleton;
}
}

事实上,getInstance() 方法还需要注意多线程的同步问题

策略模式

顶层归纳类的共性行为职责,允许每个类采取不同的实现策略

  • 类有确定的行为职责,其实现与其所管理的数据紧密相关(独立性)
  • 一组类的行为职责在概念上相同或相似,只是实现有差异
  • 把差异实现为策略(独立的类),上层类通过聚合来引用策略
  • Client code 看不到策略差异,能够进行统一管理

优点

  • 对策略进行封装 ,且相互独立
  • 可扩展性好,增加新的实现策略不影响 client code 和已有实现 策略

观察者模式

当一个对象的状态发生改变时(发生了所关注的事件),所有依赖于它的对象都将得到通知

寻找变化,封装和抽象化处理

  • 不同类型 的观察者对象:有一系列对象需要在状态发生变化时获得通知
    • 这些对象往往属于不同的类
  • 不同的通知接口: 不同的类有不同的接口/方法来获得通知
  • 不同的通知方式 谁来确定使用那种通知方式

image-20231027145748795

高可扩展性

  • Observer 与 Subject 之间松耦合
  • 添加新的 Observer 类不影响 Subject 类的业务逻辑
  • 多种通知方式,应由 Observer 实现类来选择
  • 通知方式需要通信机制的支撑
  • 进一步把变化抽象为事件

面向事件进行通知的业务场景

  • 用户和客服不期望收到快递到达中转站的通知,而配送员需要
  • 根据 Event 与 Observer 之间的映射表,来动态确定需要实际被通知的
    observer 对象 hashmap

事件处理需要的信息

  • 不同的事件、不同的观察者需要有不同的信息来处理相应的状态变化(即通知)
  • 快递发出事件:承运商、包裹 ID 、始发地等
  • 快递到达事件:包裹 ID 、存放地点、取件码等
  • 可用 Strategy 模式来对这些信息进行封装,通过 update 方法进行传递

观察者模式解析

观察者–多

被观察者–1

我们举一个例子,同学们经常在B站看up主们发的视频,当我们关注的up主更新视频时,我们会收到他的更新通知……

1.创建up主和用户接口
1
2
3
4
5
6
7
8
public interface Uploader {
public void addFollower(User follower); // 增加关注
public void notifyFollowers(); // 通知所有关注用户
}

public interface User {
public void watch();
}
2.实现up主和用户接口
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
import java.util.ArrayList;

public class Up implements Uploader {
// 管理观众们
public ArrayList<User> followers = new ArrayList<>();
// 更新状态 TRUE - 更新了 :) FALSE - 没更新 :(
public boolean updatingStatus;

@Override
public void addFollower(User follower) {
followers.add(follower);
}

@Override
public void notifyFollowers() {
followers.forEach(follower -> follower.watch(this));
/* <==>
for (User follower : followers) {
follower.watch(this);
}
*/
}
}

public class Follower implements User{
private String name;

public Follower(String name) {
this.name = name;
}

@Override
public void watch() {
System.out.println(name + " is watching " + up.name + "'s channel");
}
}
3.被观察者状态改变 → 通知观察者!
1
2
3
4
5
6
7
8
9
10
11
12
public class MainClass {
public static void main(String[] args) {
Uploader classmateHe = new Up("何同学");
classmateHe.addFollower(new Follower("AAA"));
classmateHe.addFollower(new Follower("BBB"));

// 何同学更新了!
classmateHe.notifyFollowers();
}
}
AAA is watching 何同学's channel
BBB is watching 何同学's channel

工厂模式

工厂方法模式 (Factory Method Pattern) 又称为工厂模式,也叫虚拟构造器 (Virtual Constructor) 模式或者多态工厂 (Polymorphic Factory) 模式,它属于类创建型模式。

用于创建对象的模式,提供了灵活而具有扩展性的解决方案

  • 如果对象创建只是简单 new XXX(…) XXX(…),需要这个模式吗
  • 将复杂的对象创建工作隐藏起来 ,仅暴露接 口供客户使用
  • 在不同情况下需要创建不同类型的对象(但 client code 不关心具体使用哪些类型来创建)
  • 需要为对象创建准备复杂的内容

简单工厂模式

简单工厂模式 (Simple Factory Pattern):又称为静态工厂方法 (Static Factory Method) 模式,它属于类创建型模式。 在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义了一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

1. 产品和其共同的父类

这个父类通常都是 抽象类 ,就像我们在生产一个手机的时候,他不能只是一个手机,他一定是iPhone、Samsung、华为、小米……其中的一种。

知识补充-抽象类 抽象类可以被理解成一个可以包含普通方法和成员变量的接口,在被子类继承时可以选择性的Override抽象类的方法。 深入内容同学们可以课下自行了解。

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
public abstract class Product {
private int price;

public Product(int price) {
this.price = price;
}

public abstract void use();

public void information() {
System.out.println(this.price);
}
}

public class ProductA extends Product{
public ProductA(int price) {
super(price);
}

@Override
public void use() {
System.out.println("AAAA!");
}
}

public class ProductB extends Product {
public ProductB(int price) {
super(price);
}

@Override
public void use() {
System.out.println("BBBB");
}
}
2.定义工厂来负责创建其他类的实例
1
2
3
4
5
6
7
8
9
10
11
12
public class Factory {
public static Product create(String type) {
if (type.equalsIgnoreCase("A")) {
return new ProductA(100);
} else if (type.equalsIgnoreCase("B")) {
return new ProductB(200);
} else {
System.out.println("Wrong kind!");
return null;
}
}
}
3.生产产品
1
2
3
4
5
6
7
8
public class MainClass {
public static void main(String[] args) {
Product productA = Factory.create("A");
Product productB = Factory.create("B");
Product productC = Factory.create("C");
/* ... */
}
}

工厂模式

工厂模式(Factory Pattern)中,我们定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。

工厂模式和我们之前介绍过的简单工厂模式都属于面向对象设计方法中的创建型模式,它们的目标都是解决对象的创建过程。他们都用于创建对象,将对象的实例化与客户端代码分离。 但二者仍然有不同之处。

首先,二者的抽象程度不同:

  • 工厂模式更抽象,它通过定义一个抽象工厂接口和多个实现工厂子类来生产不同类型的产品;
  • 而简单工厂模式是由一个具体的工厂类负责创建所有的产品对象。

此外,二者在扩展性上也展示出不同特点。当新增一种产品时,工厂模式只需新增一个对应的工厂子类即可,而简单工厂模式在新增产品时需要修改原有的工厂类,这就违反了开闭原则。

知识补充-*开闭原则* 开闭原则(Open-Closed Principle, OCP)是指一个软件实体如类、模块和函数应该对扩展开放, 对修改关闭。通俗解释就是,添加一个新的功能,应该通过在已有代码(模块、类、方法)的基础上进行扩展来实现,而不是修改已有代码。 深入内容同学们可以课下自行了解。

最后,就是工厂被调用的方式不同:工厂模式调用工厂接口来创建具体的产品对象;而简单工厂模式直接调用工厂类的静态方法,并传入一个参数来指定创建的具体产品类型。

通俗来讲,就是简单工厂模式是用一个厂生产多个产品,而工厂模式则是多个厂生产不同的产品

1.定义一个用于创建对象的接口
1
2
3
public interface Factory {
public Product creat(int price);
}
2.让子类决定实例化哪个类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FactoryA implements Factory{
@Override
public Product creat(int price) {
return new ProductA(price);
}
}

public class FactoryB implements Factory{
@Override
public Product creat(int price) {
return new ProductB(price);
}
}

public class MainClass {
public static void main(String[] args) {
Factory factoryA = new FactoryA();
Factory factoryB = new FactoryB();
factoryA.creat(100).use();
factoryB.creat(200).use();
}
}

抽象工厂模式

抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,它是一种对象创建型模式。

如果说工厂模式只能构建同一品类/等级的产品,例如手机(iPhone HUAWEI Samsung…)。那么抽象工厂模式则支持了多种品类,例如手机、笔记本电脑、电视……它打破了工厂和产品一对一的关系,他满足一个具体的工厂类可以生产多个大类的产品

1. 产品类

手机类产品:

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
public abstract class Phone {
private int price;

public Phone(int price) {
this.price = price;
}

public abstract void use();
}

public class iPhone extends Phone{
public iPhone(int price) {
super(price);
}

@Override
public void use() {
System.out.println("APPLE iPhone!!!");
}
}

public class Honor extends Phone{
public Honor(int price) {
super(price);
}

@Override
public void use() {
System.out.println("HUAWEI Honor!!!");
}
}

笔记本电脑类产品:

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
public abstract class Laptop {
private int size;

public Laptop(int size) {
this.size = size;
}

public abstract void use();
}
public class iMac extends Laptop{
public iMac(int size) {
super(size);
}

@Override
public void use() {
System.out.println("APPLE iMac!!");
}
}
public class MateBook extends Laptop{
public MateBook(int size) {
super(size);
}

@Override
public void use() {
System.out.println("HUAWEI MateBook!!");
}
}
2. 工厂类
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
public interface Factory {
public Phone creatPhone(int price);
public Laptop creatLaptop(int size);
}

public class BJFactory implements Factory {
@Override
public Phone creatPhone(int price) {
return new Honor(price);
}

@Override
public Laptop creatLaptop(int size) {
return new MateBook(size);
}
}

public class LAFactory implements Factory {
@Override
public Phone creatPhone(int price) {
return new iPhone(price);
}

@Override
public Laptop creatLaptop(int size) {
return new iMac(size);
}
}
3. 客户端调取
1
2
3
4
5
6
7
8
9
10
public class MainClass {
public static void main(String[] args) {
Factory Beijing = new BJFactory();
Factory LosAngeles = new LAFactory();
Beijing.creatPhone(8000).use();
LosAngeles.creatPhone(11000).use();
Beijing.creatLaptop(15).use();
LosAngeles.creatLaptop(13).use();
}
}

输出

1
2
3
4
HUAWEI Honor!!!
APPLE iPhone!!!
HUAWEI MateBook!!
APPLE iMac!!

Lec8-代码风格与代码重构