【学习经验】OOP复习

1 java基础 23

1.1 关键字和保留字

1.2 浅拷贝、深拷贝、引用

  • 基本数据类型(如 int long long boolean )都是深拷贝:复制内容
  • 的拷贝默认是浅拷贝(拷贝引用),需要深拷贝可以使用 clone() 方法、序列化机制或手动创建新的对象。

    【输出】5

    【输出】10

1.3 构造与垃圾回收

构造函数
  • 特点:与类同名,无返回值(也不是 void)、多数情况要重载
  • new 的作用:分配空间;调用构造;返回引用
不带参数构造函数/默认构造函数
  • 如果类的定义者没有显式的定义任何构造方法,系统将自动提供一个默认的构造方法(无参无方法);如果定义了构造函数,则不会创建默认构造方法。
  • 无参构造函数创建对象时,成员变量的值被赋予了数据类型的隐含初值
带参数构造函数
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 Teacher6 {
private String name; // 教员姓名
private int age; // 年龄
private String education; // 学历
private String position; // 职位
// 带参数的构造方法
public Teacher6(String pName,int pAge,String pEducation,String pPosition) {
name = pName;
age = pAge; // 可以增加对age等属性的存取限制条件
education = pEducation;
position = pPosition;
}
public String introduction() {
return "大家好!我是" + name + ",我今年" + age + "岁,学历" + education + ",目前职位是"+position;
}
}

public class Teacher6Test {
public static void main(String[] args) {
Teacher6 teacher = new Teacher6("Mary", 23, "本科", "咨询师");
System.out.println(teacher.introduction());
}
}
垃圾内存自动回收机制
  • 垃圾自动回收机制(Garbage Collection):Java 虚拟机后台线程负责
  • System.gc() 和 Runtime.gc()
  • 判断存储单元是否为垃圾的依据:引用计数为 0

1.4 匿名对象

  • 匿名对象:无管理者(无栈内存引用指向它)
  • 使用场景:只需要进行一次方法调用 / 作为参数传递给函数
1
2
3
Student stu = new Student();//stu是对象,名字是stu
new Student();//这个也是一个对象,但是没有名字,称为匿名对象
new Student().show(); //匿名对象方法调用

1.5 类的定义

类的定义
  • 类的定义格式
  • 访问控制符:public或默认(即没有访问控制符)
    public类一般含有main方法
  • 类型说明符:finalabstract
成员变量定义
  • 定义格式:[修饰符] 变量的数据类型 变量名[=初始值]
  • 常用的修饰符:thisstaticpublicprivateprotected、默认
成员方法的定义
  • 定义格式
    1
    2
    3
    4
     [修饰符] 返回值类型 方法名([形参说明])[thorws 例外名1,例外名2...]{
    局部变量声明;
    执行语句组;
    }
  • 常用的修饰符:publicprivateprotectedstaticfinal
成员变量vs局部变量
  • 初始化不同:自动初始化只用于成员变量;方法体中的局部变量不能被自动初始化,必须赋值后才能使用。
  • 定义的位置不同:定义在类中的变量是成员变量;定义在方法中或者{}语句里面的变量是局部变量。
  • 在内存中的位置不同:成员变量存储在内存的对象中;局部变量存储在内存的方法中。
  • 声明周期不同:成员变量随着对象的出现而出现在堆中,随着对象的消失而从堆中消失;局部变量随着方法的运行而出现在栈中,随着方法的弹栈而消失。
方法的重载
  1. 方法的重载是指一个类中可以定义有相同的名字,但参数不同的多个方法,调用时会根据不同的参数表选择对应的方法。
  2. 重载方法必须满足以下条件:
    • 方法名相同。
    • 方法的参数类型、个数、顺序至少有一项不相同。
    • 方法的返回类型可以不相同。
    • 方法的修饰符可以不相同。
  3. 调用重载方法时,Java使用参数的类型和数量决定实际调用重载方法的哪个版本。

1.6 toString()方法

  • 在java中,所有对象都有默认的toString()这个方法
  • 创建类时没有定义toString()方法,输出对象时会输出对象的哈希码值(对象的内存地址
  • 它通常只是为了方便输出,比如System.out.println(xx),(xx是对象),括号里面的”xx”如果不是String类型的话,就自动调用xx的toString()方法
  • toString()的定义格式
1
2
3
public String toString(){
return 字符串; // 方法体
}

1.7 静态属性和静态方法

  • static 修饰

  • 不创建具体对象也存在,此时可以通过 类名.类变量类名.类方法

  • 静态方法与非静态方法的区别

    • 静态方法是在类中使用staitc修饰的方法,在类定义的时候已经被装载和分配(早加载)。而非静态方法是不加static关键字的方法,在类定义时没有占用内存,只有在类被实例化成对象时,对象调用该方法才被分配内存(晚加载)。
    • 静态方法只能直接调用静态成员或者方法,不能直接调用非静态方法或者非静态成员(非静态方法要被实例化才能被静态方法调用),而非静态方法既可以调用静态成员或者方法又可以调用其他的非静态成员或者方法。
  • 静态代码块

    • 静态代码块只能定义在类里面,它独立于任何方法,不能定义在方法里面
    • 静态代码块里面声明的变量都是局部变量,只在本块内有效
    • 静态代码块会在类被加载时自动执行,而无论加载者是JVM还是其他的类。
    • 一个类中允许定义多个静态代码块,执行的顺序根据定义的顺序进行
    • 静态代码块只能访问类的静态成员,而不允许访问实例成员。
  • 静态代码块与非静态代码块的不同点:

    • 静态代码块在非静态代码块之前执行:静态代码块—>非静态代码块—>构造方法
    • 静态代码块只在第一次new执行一次,之后不再执行,而非静态代码块在每new一次就执行一次
例:静态方法
1
2
3
4
5
6
7
8
9
class Test{
public int sum(int a, int b){ // 非静态方法
return a+b;
}
public static void main(String[] args){
int result = (sum1, 2); // 静态方法调用非静态方法,报错
System.out.println("result="+result);
}
}
例:静态代码块
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
public class PuTong{
public PuTong(){
System.out.println("默认构造方法!-->");
}
// 非静态代码块
{
System.out.println("非静态代码块!-->");
}
// 静态代码块
static{
System.out.println("静态代码块!-->");
}
// 静态成员方法
public static void test(){
System.out.println("普通方法中的代码块!-->");
}
public static void main(String[] args){
PuTong c1 = new PuTong();
c1.test();
PuTong c2 = new PuTong();
c2.test();
}
}

/*
【输出】
静态代码块!-->
非静态代码块!-->
默认构造方法!-->
普通方法中的代码块!-->
非静态代码块!-->
默认构造方法!-->
普通方法中的代码块!-->
*/

1.8 equals==

  • 比较对象的equals和==是等价的,判断是不是引用的同一个对象
  • Stringequals只看字符串内容是否相等,而==还得看是不是同一个对象。(覆盖了Object类的equals()方法,java.io.File、java.util.Date、包装类(如java.lang.Integer和java.lang.Double类等)同理)

Integer a = 1;
Integer b = 1;
System.out.println(a == b); // 输出 true
这行输出 true 是因为 Integer 有一个缓存机制,对于 -128 到 127 之间的整数,Integer 会缓存这些值的实例。当你使用 Integer a = 1; 和 Integer b = 1; 这样的方式创建对象时,a 和 b 实际上指向了缓存中的同一个 Integer 实例。因此,a == b 比较的是同一个实例的引用,结果为 true

  1. Byte:由于Byte的值范围在-128到127之间,所以所有的Byte值都被缓存。
  2. ShortInteger:这两个类的缓存机制类似,都有默认的缓存范围,通常是-128到127。但是这个范围是可以调整的,通过JVM参数可以设置缓存的最大值。
  3. Long:Long类型也有缓存机制,但是默认的缓存范围比较小,通常是-128到127。同样,这个范围也可以通过JVM参数进行调整。
  4. Character:Character类型缓存了ASCII字符,即0到127的字符。
  5. Boolean:Boolean类型比较特殊,它只有两个值true和false,这两个值都是缓存好的。
  6. FloatDouble:这两个类型没有缓存机制,因为浮点数的取值范围非常大,缓存所有的值是不现实的

2 封装 4

2.1 封装的含义

  1. 一层含义是把对象的属性和行为看成为一个密不可分的整体,将这两者封装在一个不可分割的独立单位(即对象)中。
  2. 另一层含义指信息隐藏,把不需要让外界知道的信息隐藏起来,有些对象的属性及行为允许外界用户知道或使用,但不允许更改,而另一些属性和行为则不允许外界知晓或只允许使用对象的功能,而尽可能隐藏对象的功能实现细节。

2.2 信息隐藏的必要性

成员变量封装加上 private,对外提供公开的用于设置对象属性的 public 方法,并在方法中加上逻辑判断,过滤掉非法数据,从而:

  • 隐藏了类的具体实现
  • 操作简单
  • 提高对象数据的安全性
  • 减少了冗余代码,数据校验等写在方法里,可以复用

2.3 访问控制修饰符

访问控制分四种类别:

  • 公开 public 对外公开。
  • 受保护 protected子类以及同一个包中的类公开。
  • 默认 向同一个包中的类公开。
  • 私有 private 只有类本身可以访问,不对外公开
修饰符 同一个类 同一个包 子类 整体
private yeah
default yeah yeah
protected yeah yeah yeah
public yeah yeah yeah yeah

2.3.1 protected

1 包内可见
2 子类可见(子类和父类在同一个包:通过自己访问、通过父类访问。在不同包:仅可通过自己访问。)

若子类与父类不在同一包中,那么在子类中

  • 子类实例可以访问其从父类继承而来的protected方法
  • 不能访问父类实例protected方法
  • 不能通过另一个子类引用访问共同基类的protected方法。
1
2
3
4
5
6
7
8
9
// 父类
package com.protectedaccess.parentpackage;

public class Parent{
protected String protect = "protect field";
protected void getMessages(){
System.out.println("i am parent");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 不同包下,可以访问其从父类继承而来的`protected`方法
package com.protectedaccess.parentpackage.sonpackage1;
import com.protectedaccess.parentpackage.Parent;
public class son1 extends Parent {
public static void main(String[] args) {
Son1 son1 = new Son1();
son1.getMessage(); // 输出:i am parent
}
private void message() {
getMessage(); // 如果子类重写了该方法,则输出重写方法中的内容
super.getMessage(); // 输出父类该方法中的内容
}
}
1
2
3
4
5
6
7
8
9
10
11
// 不同包下,不能访问**父类实例**的`protected`方法
package com.protectedaccess.parentpackage.sonpackage1;
import com.protectedaccess.parentpackage.Parent;
public class son1 extends Parent {
public static void main(String[] args) {
Parent parent1 = new Parent();
// parent1.getMessage(); // 错误
Parent parent2 = new Parent();
// parent2.getMessage(); // 错误
}
}
1
2
3
4
5
6
// 不同包下,不能通过**另一个子类引用**访问共同基类的`protected`方法。
package com.protectedaccess.parentpackage.sonpackage2;
import com.protectedaccess.parentpackage.Parent;
public class son2 extends Parent {

}
1
2
3
4
5
6
7
8
9
package com.protectedaccess.parentpackage.sonpackage1;  
import com.protectedaccess.parentpackage.Parent;
import com.protectedaccess.parentpackage.sonpackage2.Son2;
public class son1 extends Parent {
public static void main(String[] args) {
Son2 son2 = new Son2();
// son2.getMessage(); // 错误
}
}
3 protected 的 static 成员对所有子类可见。

对于protected修饰的静态变量,无论是否同一个包,在子类中均可直接访问;在不同包的非子类中则不可访问。

1
2
3
4
5
6
7
8
9
// 父类
package com.protectedaccess.parentpackage;

public class Parent{
protected String protect = "protect field";
protected static void getMessages(){
System.out.println("i am parent");
}
}
1
2
3
4
5
6
7
8
9
// 无论是否同一个包,在子类中均可直接访问
package com.protectedaccess.parentpackage.sonpackage1;
import com.protectedaccess.parentpackage.Parent;

public class son3 extends Parent {
public static void main(String[] args) {
Parent.getMessage(); // 输出:i am parent
}
}
1
2
3
4
5
6
7
8
9
// 在不同包的非子类中则不可访问
package com.protectedaccess.parentpackage.sonpackage1;
import com.protectedaccess.parentpackage.Parent;

public class son4 {
public static void main(String[] args) {
// Parent.getMessage(); // 错误
}
}

3 继承 5

  • 定义: 继承是允许创建新的类(子类)来继承现有类(父类)的属性和行为的能力。
  • 目的:
    • 代码复用: 子类可以继承父类的所有属性和方法,无需重复编写代码,提高代码的可重用性。
    • 扩展功能: 子类可以添加新的属性和方法,或重写父类的方法,扩展父类的功能。
    • 层次结构: 继承可以创建类层次结构,清晰地表达类之间的关系,使代码更易于理解和维护。
  • 类型:
    • 单继承: 一个子类只有一个父类。
    • 多继承: 一个子类可以有多个父类(Java 不支持多继承)。
    • 接口继承: 子接口可以继承父接口的方法,并添加新的方法。


4 多态 6

  • 定义: 多态是指同一个行为具有多个不同表现形式或形态的能力。
  • 目的:
    • 灵活性: 允许以统一的方式处理不同类型的对象,提高代码的灵活性和可扩展性。
    • 抽象性: 可以隐藏对象的实际类型,只关注其共同的行为,提高代码的抽象性。
  • 实现方式:
    • 方法重载: 在同一个类中,可以有多个同名但参数类型或数量不同的方法。
    • 方法重写: 子类可以重写父类的方法,提供不同的实现。
    • 接口: 接口定义了一组方法,不同的类可以实现同一个接口,并提供不同的实现。

4.1 多态

  • 多态:用相同的名称来表示不同的含义

  • 静多态:在编译时决定调用哪个方法;方法重载、方法隐藏

    • 方法重载(Overloading)
      • 方法名相同,参数个数参数类型参数顺序至少有一个不同
      • 返回值类型与访问权限修饰符可以相同也可以不同
    • 方法隐藏:子类定义了一个与父类同名同参数列表静态/私有方法(相当于一个新方法)
  • 动多态:在运行时才能确定调用哪个方法;方法覆盖

    • 3个条件:继承、覆盖、向上转型(必须由父类的引用指向派生类的实例,并且通过父类的引用调用被覆盖的方法)
    • 方法覆盖(Override)
      • 方法名、参数个数、参数类型及参数顺序必须一致
      • 异常抛出范围:子类 $\le$ 父类
      • 访问权限:子类 $\ge$ 父类
      • 私有方法、静态方法不能被覆盖,如果在子类出现了同签名的方法,那是方法隐藏;
  • 多态中成员变量编译运行看左边,多态中成员方法编译看左边,运行看右边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// **多态中成员变量编译运行看左边**
class Parent {
int value = 10;
}

class Child extends Parent {
int value = 20;
}

public class Test {
public static void main(String[] args) {
Parent p = new Child();
System.out.println(p.value); // 输出 10,因为编译和运行时都看左边(Parent类)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// **多态中成员方法编译看左边,运行看右边**
class Parent {
void display() {
System.out.println("Parent display");
}
}

class Child extends Parent {
@Override
void display() {
System.out.println("Child display");
}
}

public class Test {
public static void main(String[] args) {
Parent p = new Child();
p.display(); // 编译时看左边(Parent类),运行时看右边(Child类),输出 "Child display"
}
}
  • 抽象类
    • abstract 语义为“尚未实现”
    • 如果一个类继承自某个抽象父类,而没有具体实现抽象父类中的抽象方法,则必须定义为抽象类
    • 抽象类引用:虽然不能实例化抽象类,但可以创建它的引用。因为Java支持多态性,允许通过父类引用来引用子类的对象。
    • 如果一个类里有抽象的方法,则这个类就必须声明成抽象的。但一个抽象类中却可以没有抽象方法
  • 抽象方法
    • 无函数体
    • 必须在抽象类中
    • 必须在子类中实现,除非子类也是抽象的
    • 不能被private、final或static修饰。

4.2 接口

4.2.1 定义

  • 接口:不相关类的功能继承。
    • 只包含常量(所有变量默认public static final)和方法(默认 public abstract)的定义,没有方法的实现。(但一般不包含变量)
    • 没有构造方法
    • 一个类可以实现多个接口;如果类没有实现接口的全部方法。需要被定义成 abstract
    • 接口的方法体还可以由其他语言写,此时接口方法需要用 native 修饰
    • 接口可以继承,而且可以多重继承
      • 同一个函数只能实现一次
      • 不同接口的同名变量相互隐藏
      • 接口变量和类中成员同名时,存在作用域问题
  • 关键词 interface implements
1
2
3
4
5
6
7
8
9
10
11
12
[public] [interface] 接口名称 [extends 父接口名列表]{
// 静态常量
[public][static][final]数据类型 变量名=常量名;
//抽象方法
[public][abstract][native]返回值类型 方法名(参数列表);
}

[修饰符] class类名 [extends父类名] [implements接口A,接口B,…]{
类的成员变量和成员方法;
为接口A中的所有方法编写方法体,实现接口A;
为接口B中的所有方法编写方法体,实现接口B;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Flyer{
public void takeOff();
public void land();
public void fly();
}

public class Bird extends Animal implements Flyer {
public void takeOff() { /* take- off implementation */ }
public void land() { /* landing implementation */ }
public void fly() { /* fly implementation */ }
public void buildNest() { /* nest building behavior */ }
public void layEggs() { /* egg laying behavior */ }
public void eat() { /* override eating behavior */ }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 不同接口的同名变量相互隐藏
interface Animal {
int legs = 0; // 假设动物默认没有腿
}

interface 昆虫 {
int legs = 6; // 昆虫有6条腿
}

class Spider implements Animal,昆虫 {
public void printLegs() {
System.out.println("Animal legs: " + Animal.legs); // 调用Animal接口中的legs变量
System.out.println("昆虫 legs: " +昆虫.legs); // 调用昆虫接口中的legs变量
}
}

public class Main {
public static void main(String[] args) {
Spider spider = new Spider();
spider.printLegs();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 接口变量和类中成员同名时,存在作用域问题
interface Animal {
String name = "Animal";
}

class Dog implements Animal {
String name = "Dog"; // Dog类中定义了与接口同名的变量

public void printName() {
System.out.println(name); // 这将打印"Dog"
System.out.println(Animal.name); // 这将打印"Animal"
}
}

4.2.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
29
30
31
32
33
34
35
36
37
38
interface Runner {
//接口1
public void run();
}

interface Swimmer {
//接口2
public void swim();
}

abstract class Animal {
public abstract void eat();
}

class Person extends Animal implements Runner,Swimmer { //继承类,实现接口
public void run() {
System.out.println("我是飞毛腿,跑步速度极快!");
}
public void swim(){
System.out.println("我游泳技术很好,会蛙泳、自由泳、仰泳、蝶泳...");
}
public void eat(){
System.out.println("我牙好胃好,吃啥都香!");
}
}

public class InterfaceTest{
public void m1(Runner r) { r.run(); } //接口作参数
public void m2(Swimmer s) {s.swim();} //接口作参数
public void m3(Animal a) {a.eat();} //抽象类引用
public static void main(String args[]){
InterfaceTest t = new InterfaceTest();
Person p = new Person();
t.m1(p); //接口回调
t.m2(p); //接口回调
t.m3(p); //接口回调
}
}

4.2.4 抽象类与接口

  • 区别
    • 接口中的成员变量和方法只能是 public 类型的,而抽象类中的成员变量和方法可以处于各种访问级别。
    • 接口中的成员变量只能是 publicstaticfinal 类型的,而在抽象类中可以定义各种类型的实例变量和静态变量。
    • 接口中没有构造方法,抽象类中有构造方法。接口中所有方法都是抽象方法,抽象类中可以有,也可以没有抽象方法。抽象类比接口包含了更多的实现细节。
    • 抽象类是某一类事物的一种抽象,而接口不是类,它只定义了某些行为
      例如,“生物”类虽然抽象,但有“狗”类的雏形,接口中的run方法可以由狗类实现,也可以由汽车实现。
    • 在语义上,接口表示更高层次的抽象,声明系统对外提供的服务。而抽象类则是各种具体类型的抽象。

4.2.5 Native关键字

  • Native用来声明一个方法是由机器相关的语言(如C/C++语言)实现的。通常,native方法用于一些比较消耗资源的方法,该方法用c或其他语言编写,可以提高速度。
  • native 定义符说明该方法是一个使用本地其他语言编写的非java类库的方法,它是调用的本地(也就是当前操作系统的方法或动态连接库)。最常见的就是c/c++封装的DLL里面的方法,这是java的 JNI技术。它在类中的声明和抽象方法一样没有方法体。

4.3 upcasting 和 downcasting

4.3.1 向上转型 upcasting

  • 向上转型:当有子类对象赋值给一个父类引用时,便是向上转型,多态本身就是向上转型的过程。

  • 使用格式:
    父类类型 变量名 = new 子类类型();
    如:Person p = new Student();

  • 上转型对象的使用(父类有的就能访问,没有的不能访问,且儿子的优先级更高)

    • 上转型对象可以访问子类继承或隐藏的成员变量,也可以调用子类继承的方法或子类重写的实例方法
    • 如果子类重写了父类的某个实例方法后,当用上转型对象调用这个实例方法时一定是调用了子类重写的实例方法
    • 上转型对象不能操作子类新增的成员变量;不能调用子类新增的方法。

4.3.2 向下转型 downcasting

  • 向下转型(映射):一个已经向上转型的子类对象可以使用强制类型转换的格式,将父类引用转为子类引用,这个过程是向下转型。
  • 使用格式:
    子类类型 变量名 = (子类类型) 父类类型的变量;
    如:Person p = new Student();
    Student stu = (Student) p
  • 如果是直接创建父类对象,是无法向下转型的 ,能过编译,但运行时会产生异常
    如:Person p = new Peron();
    Student stu = (Student) p
instanceof 操作符
  • instanceof 操作符用于判断一个引用类型所引用的对象是否是一个类的实例instanceof 运算符是Java独有的双目运算符
  • instanceof 操作符左边的操作元是一个引用类型的对象(可以是null),右边的操作元是一个类名或接口名。
  • 形式如下:obj instanceof ClassName 或者 obj instanceof InterfaceName
  • a instanceof X,当 X 是 A类/A类的直接或间接父类/A类实现的接口时,表达式的值为true

5 Object类、最终类、内部类、匿名类 10

5.1 Object类

  • 基于多态的特性,该类可以用来代表任何一个类,因此允许把任何类型的对象赋给 Object类型的变量,也可以作为方法的参数、方法的返回值
public final Class<?> getClass(){}
  • 该方法用于获取对象运行时的字节码类型,得到该对象的运行时的真实类型
  • 通常用于判断两个引用中实际存储对象类型是否一致。
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
public class MyClass {
// 类的成员变量和方法
}

public class AnotherClass {
// 另一个类的成员变量和方法
}

public class TypeComparisonExample {
public static void main(String[] args) {
MyClass obj1 = new MyClass();
MyClass obj2 = new MyClass();
AnotherClass obj3 = new AnotherClass();

// 比较obj1和obj2是否指向相同类型的对象
if (obj1.getClass() == obj2.getClass()) {
System.out.println("obj1和obj2指向相同类型的对象");
} else {
System.out.println("obj1和obj2指向不同类型的对象");
}

// 比较obj1和obj3是否指向相同类型的对象
if (obj1.getClass() == obj3.getClass()) {
System.out.println("obj1和obj3指向相同类型的对象");
} else {
System.out.println("obj1和obj3指向不同类型的对象");
}
}
}

// obj1和obj2指向相同类型的对象
// obj1和obj3指向不同类型的对象
  • 最主要应用:该方法属于Java的反射机制,其返回值是Class类型,例如 Class c = obj.getClass();。通过对象c,
    • 获取所有成员方法每个成员方法都是一个Method对象Method[] methods = cls.getDeclaredMethods();
    • 获取所有成员变量每个成员变量都是一个Field对象Field[] fields = cls.getDeclaredFields();
    • 获取所有构造函数构造函数则是一个Constructor对象Constructor<?>[] constructors = cls.getDeclaredConstructors();
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
// 用getClass来获得该对象的类名、所有成员方法、所有成员变量、所有构造函数
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class MyClass {
private int myField;
public MyClass() {
// 默认构造函数
}
public MyClass(int value) {
// 带参数的构造函数
myField = value;
}
public void myMethod() {
// 成员方法
}
}

public class ReflectionExample {
public static void main(String[] args) {
MyClass obj = new MyClass();

// 获取Class对象
Class<?> cls = obj.getClass();

// 获取类名
String className = cls.getName();
System.out.println("类名: " + className);

// 获取所有成员方法
Method[] methods = cls.getDeclaredMethods();
System.out.println("成员方法:");
for (Method method : methods) {
System.out.println(method.getName());
}

// 获取所有成员变量
Field[] fields = cls.getDeclaredFields();
System.out.println("成员变量:");
for (Field field : fields) {
System.out.println(field.getName());
}

// 获取所有构造函数
Constructor<?>[] constructors = cls.getDeclaredConstructors();
System.out.println("构造函数:");
for (Constructor<?> constructor : constructors) {
System.out.println(constructor.getName());
}
}
}
public int hashCode(){}
  • 返回该对象的哈希码值。哈希值为根据对象的地址或字符串或数字使用hash算法计算出来的int类型的数值。
  • 在Object类中,hashCode的默认实现通常会返回对象的内存地址的某种形式(不能完全将哈希值等价于地址),具体的实现依赖JVM。
  • 提高具有哈希结构的容器的效率。
  • 如果两个对象相等(即equals返回true),那么它们的hashCode值也必须相等
public boolean equals(Object obj)
  • 比较两个对象是否相等。仅当被比较的两个引用变量指向同一对象时(即两个对象地址相同,也即hashCode值相同),equals()方法返回true
  • 可进行覆盖比较两个对象的内容是否相同
equals()hashCode

equals为true与hashCode相同的关系?

  • 如果两个对象的equals()结果为true,那么这两个对象的hashCode()一定相同
  • 两个对象的hashCode()结果相同,并不能代表两个对象的equals()一定为true(Hash散列值有冲突的情况,虽然概率很低,只能够说明这两个对象在一个散列存储结构中)

为什么要重写hashCode和equals?

  • equals() ⽅法⽤于⽐较两个对象的内容是否相等。在Java中,默认实现是⽐较对象的引⽤,即⽐较两个对象是否指向内存中的相同位置。但通常,我们希望⽐较对象的内容是否相等
  • 鉴于这种情况,Object类中 equals() 方法的默认实现是没有实⽤价值的,所以通常都要重写
  • 而由于hashCode()与equals()具有联动关系(如果两个对象相等,则它们必须有相同的哈希码),所以equals()方法重写时,通常也要将hashCode()进⾏重写,使得这两个方法始终保持⼀致性。

重写equals一定要重写hashCode吗?

  • 如果仅仅是为了比较两个对象是否相等只重写equals就可以;
  • 如果你使用了hashSet、hashMap等容器,为了避免加入重复元素,或者查找元素,就一定要同时重写两个方法。
  • 如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals 。
public String toString(){}

默认的 toString() 输出包名类名堆上的首地址

finalize()方法

不存在对该对象的其他引用时,由对象的垃圾器调用此方法

线程中常用的方法
  • public final void wait(): 多线程中等待功能
  • public final native void notify() : 多线程中唤醒功能
  • public final native void notifyAll(): 多线程中唤醒所有等待线程的功能


ObjectsObject 区别
  • Objects 【public final class Objects extends Object】是Object 的工具类,位于java.util包。它从jdk1.7开始才出现,被final修饰不能被继承,拥有私有的构造函数。此类包含static实用程序方法,用于操作对象或在操作前检查某些条件
    • null 或 null方法 ;
    • 用于计算一堆对象的混合哈希代码
    • 返回对象的字符串(会对null进行处理);
    • 比较两个对象,以及检查索引或子范围值是否超出范围

5.2 最终类、最终方法、常量

5.2.1 最终类、最终方法

  • 最终类:如果一个类没有必要再派生子类,通常可以用final关键字修饰,表明它是一个最终类

  • 最终方法:用关键字final修饰的方法称为最终方法。最终方法既不能被覆盖,也不能被重载,它是一个最终方法,其方法的定义永远不能改变

  • final类中的方法可以不声明为final方法,但实际上final类中的方法都是隐式的final方法

  • final修饰的方法不一定要存在于final类中。

  • 定义类头时,abstract和final不能同时使用

  • 访问权限为private的方法默认为final

5.2.2 常量

  • Java中的常量使用关键字 final 修饰。

  • final既可以修饰简单数据类型,也可以修饰复合数据类型。

    • 简单数据类型:值不能再变
    • 符合数据类型:引用不能再变,值可以改变
  • final常量可以在声明的同时赋初值,也可以在构造函数

  • 常量既可以是局部常量,也可以是类常量和实例常量。如果是类常量,在数据类型前加static修饰(由所有对象共享)。如果是实例常量,就不加static修饰

  • 常量名一般大写,多个单词之间用下划线连接。

局部常量、类常量、实例常量
  1. 局部常量(Local Constant): 局部常量是在方法、构造器或代码块内部定义的常量。它们只在定义它们的代码块内部有效,一旦代码块执行完毕,局部常量就不再存在。局部常量通常使用 final 关键字来声明,表示其值在初始化后不能被改变。
1
2
3
4
5
public void myMethod() {
final int LOCAL_CONSTANT = 10; // 局部常量
// 这里可以使用 LOCAL_CONSTANT
}
// 这里不能使用 LOCAL_CONSTANT,因为它只在 myMethod 方法内部有效
  1. 类常量(Class Constant): 类常量是在类的静态初始化块或静态成员变量中定义的常量。它们属于类本身,而不是类的实例。类常量也通常使用 final 关键字来声明,并且是 static 的,这意味着它们是类的所有实例共享的。
1
2
3
4
public class MyClass {
public static final int CLASS_CONSTANT = 20; // 类常量
// 这里可以使用 CLASS_CONSTANT
}
  1. 实例常量(Instance Constant): 实例常量是在类的非静态成员变量中定义的常量。它们属于类的每个实例,每个实例都有自己的实例常量副本。实例常量同样使用 final 关键字来声明,表示一旦被初始化,其值就不能改变。
1
2
3
4
public class MyClass {
public final int INSTANCE_CONSTANT = 30; // 实例常量
// 这里可以使用 INSTANCE_CONSTANT
}





输出

1
2
3
PersonNNNNNNNNNNNNNNNN
StudentTTTTTTTTTTTTTTTTTTTTT
PersonNNNNNNNNNNNNNNNN

解释
p.priFinalMethod() 和 p2.priFinalMethod() 调用的是 Parent 类的 priFinalMethod 方法,因为 priFinalMethod 是 Parent 类的私有方法,无法在 Parent 类的外部通过 Parent 类的引用调用 Child 类的 priFinalMethod 方法

5.3 内部类

5.3.1 内部类的基本语法

内部类的分类

实例内部类
创建实例内部类的实例

在创建实例内部类的实例时,外部类的实例必须已经存在,例如要创建InnerTool类的实例,必须先创建Outer外部类的实例
两种语法:

  • Outer.InnerTool tool=new Outer().new InnerTool();
  • Outer outer=new Outer();
    Outer.InnerTool tool =outer.new InnerTool();
    以下代码会导致编译错误:Outer.InnerTool tool=new Outer.InnerTool();
实例内部类访问外部类的成员
  • 在内部类中,可以直接访问外部类的所有成员,包括成员变量和成员方法。
  • 实例内部类的实例自动持有外部类的实例的引用
静态内部类
  • 静态内部类的实例不会自动持有外部类的特定实例的引用
  • 在创建内部类的实例时,不必创建外部类的实例
  • 客户类可以通过完整的类名直接访问静态内部类的静态成员
局部内部类
  • 局部内部类只能在当前方法中使用。
  • 局部内部类和实例内部类一样,可以访问外部类的所有成员
  • 此外,局部内部类还可以访问函数中的最终变量或参数(final)

5.3.2 内部类的用途

  • 封装类型:如果一个类只能由系统中的某一个类访问,可以定义为该类的内部类。
  • 直接访问外部类的成员
  • 回调外部类的方法
5.3.2.1 内部类封装类型
  • 顶层类只能处于public和默认访问级别
  • 而成员内部类可以处于public、protected、默认和private四个访问级别。
  • 此外,如果一个内部类仅仅为特定的方法提供服务,那么可以把这个内部类定义在方法之内
  • 虽然 InnerTool 是 Test 的私有内部类,但它仍然可以在 Test 类的内部被访问。在 main 方法中,new Test().new InnerTool() 是在 Test 类的内部创建 InnerTool 的实例,因此这是允许的。
  • 在客户类中不能访问Outer.InnerTool类,但是可以通过Outer类的getTool()方法获得InnerTool的实例
5.3.2.2 内部类访问外部类的成员
  • 内部类的一个特点是能够访问外部类的各种访问级别的成员
  • 假定有类A和类B,类B的reset()方法负责重新设置类A的实例变量count的值。一种实现方式是把类A和类B都定义为外部类
  • 假如需求中要求类A的count属性不允许被除类B以外的其他类读取或设置,那么以上实现方式就不能满足这一需求。
  • 在这种情况下,把类B定义为内部类就可以解决这一问题,而且会使程序代码更加简洁
5.3.2.3 回调



故考虑使用回调方法

回调实质上是指一个类(Sub)尽管实际上实现了某种功能(调节温度),但是没有直接提供相应的接口,客户类可以通过这个类的内部类(Closure)的接口(Adjustable)来获得这种功能。而这个内部类本身并没有提供真正的实现,仅仅调用外部类的实现(adjustTemperature)
可见,回调充分发挥了内部类具有访问外部类的实现细节的优势。

5.3.3 内部类的文件命名

对于每个内部类,Java编译器会生成独立的.class文件。这些类文件的命名规则如下:

  • 成员内部类:外部类的名字$内部类的名字
  • 局部内部类:外部类的名字$数字和内部类的名字
  • 匿名类:外部类的名字$数字

5.4 匿名类

  • 匿名类就是没有名字的类,是将类和类的方法定义在一个表达式范围里。
  • 匿名类本身没有构造方法,但是会调用父类的构造方法
  • 匿名内部类将内部类的定义与生成实例的语句合在一起,并省去了类名以及关键字“class”,”extends”和“implements”等
  • 匿名类必须继承自一个具体的或实现一个接口

其他 9 11-14

异常处理 9

1 异常概述

  • 3类错误
    • 编译错误
    • 逻辑错误
    • 运行时错误:在程序运行过程中如果发生了一个不可能执行的操作,就会出现运行时错误。
  • 异常:一个可以正确运行的程序在运行中可能发生的错误。
  • 异常特点:偶然性、可预见性、严重性
  • 异常处理 ( Exception Handling ):提出或者是研究一种机制,能够较好的处理程序不能正常运行的问题。

2 java异常类/异常的层次结构

java异常类

  • Throwable:所有异常类的父类,是Object的直接子类。
  • Error:由Java虚拟机生成并抛出,Java程序不做处理
  • Exception:所有的Throwable类的子孙类所产生的对象都是异常
    • Runtime Exception:编译时不可监测的异常,由系统检测, 用户的Java程序可不做处理,系统将它们交给缺省的异常处理程序。
    • 非Runtime Exception编译时可以监测的异常,Java编译器要求Java程序必须捕获或声明所有的非运行时异常,可以通过try-catch或throws处理。
  • throw:用户自己产生异常。
常见的异常
  1. ArithmeticException
  2. ArrayIndexOutOfBoundsException
  3. ArrayStoreException
  4. IOException
  5. FileNotFoundException
  6. NullPointerException
  7. MalformedURLException
  8. NumberFormatException
  9. OutOfMemoryException
异常分类
  • 非受检异常 ( unchecked exception ) :Runtime Exception 及其子类、Error 及其子类。
    • 只能在程序执行时被检测到,不能在编译时被检测到
    • 程序可不处理,交由系统处理。
  • 受检异常 ( checked exception ):除了非受检异常之外的异常(即其他的异常类都是可检测的类)
    • 这些异常在编译时就能被java编译器所检测到异常。
    • 必须采用 throws 语句或者 try-catch 方式处理异常

3 java异常处理机制

  • 抓抛模型
    • 抛出(throw)异常:Java程序在正常的执行过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象,并将此对象抛出,且其后代码就不再执行
      • 关于异常对象的产生
        • 系统自动生成异常对象
        • 手动生成一个异常对象,并抛出(throw
    • 捕获(catch)异常:可以理解为异常处理方式。
      • try-catch-finally
      • throws
  • Java语言按照面向对象的思想来处理异常:
    • 把各种不同类型的异常情况进行分类,用Java类来表示异常情况,这种类被称为异常类。
    • 用throws语句在方法声明处声明抛出特定异常。(只抛出不处理,交给调用该方法的方法进行处理,若一直不处理,则交给系统处理
    • 用try-catch语句来捕获并处理异常。(处理
    • 用throw语句在方法中抛出具体的异常。(自定义异常

4 try-catch-finally

finally:无条件执行的语句
  • try{} 中执行 returnfinally 语句仍然执行,在 return 前执行
  • try{} 中执行 exist(0)finally 语句不执行
try-catch-finally 语句格式
  • 一个try一个catch可以,一个try一堆catch也可以,try-catch-finally也可以,try-fianlly也可以

  • try-catch-finally 语句的语法格式

    • 一般 finally释放资源的部分(打开水龙头后无论水龙头能不能使最后都要关上水龙头)
    • 如果一个异常类和其子类都出现在catch子句中,应把子类放在前面,否则将永远不会到达子类。
1
2
3
4
5
6
7
8
9
10
11
12
try {
// 接受监视的程序块,在此区域内发生的异常,由catch中指定的程序处理;
}
catch (ExceptionType1 e) {
// 抛出ExceptionType1异常时要执行的代码
}
catch (ExceptionType2 e) {
// 抛出ExceptionType2异常时要执行的代码
}……
[finally {
// 无条件执行的语句
}]
  • try-finally
1
2
3
4
5
6
7
//try-finally
try{
// 对文件进行处理的程序
}
finally{
// 不论是否发生异常,都关闭文件
}
  • catch(异常名1|异常名2|异常名3 变量)
1
2
3
4
5
try{

}catch(异常名1 |异常名2 |异常名3… 变量){

}

方法虽简洁,但是也不是特别完美

  • 上述异常必须是同级关系;
  • 处理方式是一样的(针对同类型的问题,给出同一个处理)
try-with-resourse
  • 资源:所有实现Closeable的类,如流操作,socket操作,httpclient等
  • 打开的资源越多,finally中嵌套的将会越深,所以引入了 Try-with-resourse
  • 带资源的try语句(try-with-resource)的最简形式为:
1
2
3
try(Resource res = xxx){ // 可指定多个资源
work with res
}
  • 处理规则
    • 凡是实现了AutoCloseable接口的类,在try()里声明该类实例的时候,在try结束后,close方法都会被调用,这个动作会早于finally里调用的方法
    • 不管是否出现异常,try()里的实例都会被调用;
    • close方法越晚声明的对象,会越早被close掉。
例1

先声明的后关闭

1
2
3
4
do something
do other things
other resource is closed
some resource is closed

5 Throws

用于声明异常

  • 声明异常:一个方法不处理它产生的异常,而是沿着调用层次向上传递,由调用它的方法来处理这些异常,叫声明异常。若最终方法也没有处理异常,异常将交给系统处理

  • Throws语句用来表明一个方法可能抛出的各种异常,并说明该方法会抛出但不捕获异常

  • 声明异常的格式

1
<访问权限修饰符><返回值类型><方法名>(参数列表) throws 异常列表{}
  • 当父类中的方法没有throws,则子类重写此方法时也不可以throws。若重写方法中出异常,必须采用try结构处理。
  • 重写方法不能抛出比被重写方法范围更大的异常类型,子类重写方法也可以不抛出异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 方法**不处理**它产生的异常,而是**沿着调用层次向上传递**,由调用它的方法来处理这些异常
class ThrowsTest4 {
// 声明异常,但不处理异常
static void method() throws IllegalAccessException {
System.out.println("\n在 method 中抛出一个异常");
throw new IllegalAccessException();
}
public static void main(String[] args) {
try {
method();
} catch (IllegalAccessException e) {
System.out.println("在 main 中捕获异常:" + e);
}
}
}
//
// 在 method 中抛出一个异常
// 在 main 中捕获异常:java.lang.IllegalAccessException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 若最终方法也没有处理异常,异常将交给系统处理
import java.io.FileInputStream;
import java.io.IOException;

public class Test {
public static void main(String[] args) throws IOException {
FileInputStream in = new FileInputStream("myfile.txt");
int b;
while ((b = in.read()) != -1) {
System.out.print((char) b);
}
in.close();
}
}
// Exception in thread "main" java.io.FileNotFoundException: myfile.txt (系统找不到指定的文件。)
// at java.base/java.io.FileInputStream.open0(Native Method)
// at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
// at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
// at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106)
// at Test.main(Test.java:6)

6 throw与创建自定义异常类

  • throw抛出用户自定义异常
  • 用户定义的异常必须由用户自己抛出 <throw><异常对象> throw new MyException
  • 程序会在throw语句处立即终止,转向 try…catch 寻找异常处理方法。
  • 语句格式
1
2
3
4
5
<class><自定义异常名>extends<Exception>{
public String to string(){
return "myException";
}
}



图形界面 11

看PPT去

Java IO 12

scanner
  • Scanner的作用:通过分隔符模式将输入分解为标记,默认情况下该分隔符模式与空白匹配。

  • 通过Scanner 类的 next()nextLine() 方法获取输入的字符串,在读取前我们一般需要使用 hasNexthasNextLine 判断是否还有输入的数据

  • Scanner

1
2
3
4
Scanner scanner = new Scanner(System.in);  
while(scanner.hasNextDouble()){
double x = scanner.nextDouble();
}
  • 转换大小写
1
2
3
String original = "Hello, World!";
String uppercase = original.toUpperCase();
String lowercase = original.toLowerCase();
  • 替换字符串
1
2
String original = "Hello, World!";
String replaced = original.replace("World", "Java");
  • 分割字符串
1
2
String original = "one,two,three";
String[] parts = original.split(",");
  • 连接字符串
1
2
3
String part1 = "Hello, ";
String part2 = "World!";
String combined = part1 + part2;
  • 格式化字符串
1
2
int number = 42;
String formatted = String.format("The answer is %d", number);
  • 输入流:输入数据流只能读,不能写
    • 字节流:Java中的输入数据流(字节流)都是抽象类InputStream的子类;
    • 字符流:Java中的输入数据流(字符流)都是抽象类Reader的子类;
  • 输出流:输出数据流只能写,不能读
    • 字节流:java中的输出数据流(字节流)都是抽象类OutputStream的子类;
    • 字符流:java中的输出数据流(字符流)都是抽象类Writer的子类;
  • 字节流可以操作所有类型的文件;
  • 字符流只能操作纯文本文件




其他 IO NIO NIO2

  • IO流(同步、阻塞)
  • NIO(同步、非阻塞):NIO(NEW IO)用到块,效率比IO高很多
    三个组件:
    • Channels(通道):流是单向的,Channel是双向的,既可以读又可以写,Channel可以进行异步的读写,对Channel的读写必须通过buffer对象
    • Buffer(缓冲区)
    • Selector(选择器):Selector是一个对象,它可以注册到很多个Channel上,监听各个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了
  • NIO2(异步、非阻塞):AIO(Asynchronous IO)
同步与异步
  • 同步:是一种可靠的有序运行机制,进行同步操作时,后续的任务须等待当前调用返回,才会进行下一步;
  • 异步:后续任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系;
阻塞与非阻塞
  • 阻塞:在进行读写操作时,当前线程会处于阻塞状态,无法从事其他任务。只有当条件就绪才能继续;
  • 非阻塞:不管IO操作是否结束,直接返回,相应操作在后台继续处理

字节流

InputStream


简介

  • InputStream是抽象类,所以不能通过“new InputStream()”的方法构造InputStream的实例。但它声明了输入流的基本操作,包括读取数据(read)、关闭输入流(close)、获取流中可读的字节数(available)、移动读取指针(skip)、标记流中的位置(mark)和重置读取指针(reset) 等,它的子类一般都重写这些方法。
  • 通过构造InputStream子类的实例方式可以获得InputStream类型的实例。

相关函数介绍

  • 方法read()提供了三种从流中读数据的方法.
    • int read():一次只能读一个字节,抽象方法。
    • int read(byte b[]):一次读多个字节到数组中
    • int read(byte[],int off,int len);
  • 一般available和read()混合使用,这样在读操作前可以知道有多少字符需要读入。
  • mark通常与reset()方法配合使用,可重复读取输入流所指定的字节数据。

分类

  • FileInputStream:用于从本地文件中读出数据。
  • ObjectInputStream:用来读取对象;要保证对象是串行化(Serializable)的(指对象通过把自己转化为一系列字节,记录字节的状态数据,以便再次利用的这个过程;对不希望串行化的对象要用关键字transient修饰
  • PipedIntputStream:用于管道输入/输出时从管道中读取数据;管道数据流的两个类一定是成对的,同时使用并相互连接的,这样才形成一个数据通信管道;管道数据流主要用于线程间的通信
  • SequencedInputStream:用来把两个或更多的InputStream输入流对象转换为单个inputStream输入流对象使用。
  • FilterInputStream:提供将流连接在一起的能力;某一时刻只能一个线程访问它;其子类PushbackInputStream(读过的一个或几个字节数据退回到输入流中/回压别的字节数据)和BufferedInputStream读取数据时可以对数据进行缓冲,这样可以提高效率增加特殊功能
  • DataInputStream实现了java.io包中的DataInput接口,读取数据的同时,可以对数据进行格式处理。因此,能用来读取java中的基本数据类型
  • ByteArrayInputStream:包含一个内存缓冲区,用于从内存中读取数据。
  • AudioInputStream:Audio的输入输出
OutputStream

简介
OutputStream是抽象类,所以不能通过“newOutputStream()”的方法构造OutputStream的实例。但它声明了输出流的基本操作,包括输出数据(write)、关闭输出流(close)、清空缓冲区(flush)等

分类

  • FileOutputStream:用于向本地文件中写入数据。
  • PipedOutputStream:用于管道输入/输出时把数据向管道输出
  • DataOutputStream:提供了对java的基本数据类型的支持
  • PrintStream:提供了向屏幕输出有格式数据的很多方法;System.out
    为什么PrintStream适合做打印流?
    • 它提供了更多的输出成员方法,输出的数据不必先转换成字符串类型或其它类型
    • PrintStream的成员方法一般不会抛出异常
    • PrintStream具有自动强制输出(flush)功能,即当输出回车换行时,在缓存中的数据会全部自动写入指定的文件或在标准输出窗口中显示

字符流


  • Reader类和Writer类中的大部分方法与InputStream类和OutputStream类中的对应方法名称相同,只是读取或写入的数据是字符、字符数组和字符串等。
  • 如果程序读到的数据是不同国家的语言,其编码不同,那么程序应使用Reader和Writer流。
InputStreamReader类和OutputStreamWriter类
  • InputStreamReader类继承自Reader类,通过其read方法从字节输入流中读取一个或多个字节数据转换为字符数据,它不是一个缓冲流,因此其转换的效率并不高
  • OutputStreamWriter类继承自Writer类,其作用是转变字符输出流为字节流输出
  • InputStreamReader类和OutputStreamWriter类都可以接一个缓冲流来提高效率
FileReader和FileWriter

FileReader和FileWriter类分别是Reader和Writer子类,他们分别用来从字符文件读取字符向字符文件输出字符数据

多线程 13


1 进程的概念

  • 程序(program):静态的代码

  • 进程(process)是程序的一次执行过程

  • 程序是静态的,进程是动态的。

  • 不同进程所占用的系统资源相对独立

  • 属于同一进程的所有线程共享该进程的系统资源

  • 线程本身既没有入口,也没有出口,其自身也不能独立运行,完成其任务后,自动终止,也可以由进程使之强制终止。

当多线程程序执行时具有并发执行的多个线程

为什么用多线程?
  • 速度快:线程之间共享相同的内存单元(代码和数据),因此在线程间切换,不需要很大的系统开销,所以线程之间的切换速度远远比进程之间快,线程之间的通信也比进程通信快的多。
  • CPU利用率高:多个线程轮流抢占CPU资源而运行时,从微观上讲,一个时间里只能有一个作业被执行,在宏观上可使多个作业被同时执行,即等同于要让多台计算机同时工作,使系统资源特别是CPU的利用率得到提高,从而可以提高整个程序的执行效率

2 线程的运行

每个线程都有一个独立的程序计数器和方法调用栈(method invocation stack):

  • 栈存简单局部变量,堆存类对象
  • 线程运行中需要的资源:CPU、方法区的代码、堆区的数据、栈区的方法调用栈

3 线程的调度

线程的调度
  • 在Java中,线程调度通常是抢占式(即哪一个线程先抢到CPU资源则先运行),而不是分时间片式
  • 一旦一个线程获得执行权,这个线程将持续运行下去,直到它运行结束或因为某种原因而阻塞,或者有另一个高优先级线程就绪(这种情况称为低优先级线程被高优先级线程所抢占)。
  • 所有被阻塞的线程按次序排列,组成一个阻塞队列
  • 所有就绪但没有运行的线程则根据其优先级排入一个就绪队列
  • 当CPU空闲时,如果就绪队列不空,就绪队列中第一个具有最高优先级的线程将运行。
  • 当一个线程被抢占而停止运行时,它的运行态被改变并放到就绪队列的队尾;
  • 一个被阻塞(可能因为睡眠或等待I/O设备)的线程就绪后通常也放到就绪队列的队尾
优先级

线程的调度是按:

  1. 其优先级的高低顺序执行的;
  2. 同样优先级的线程遵循“先到先执行的原则”

线程优先级

  • 范围 1~10 (10 级)。数值越大,级别越高
  • Thread 类定义的 3 个常数:
    • MIN_PRIORITY 最低(小)优先级(值为1)
    • MAX_PRIORITY 最高(大)优先级(值为10)
    • NORM_PRIORITY 默认优先级(值为5)
  • 线程创建时,继承父线程的优先级
  • 常用方法:
    • getPriority( ):获得线程的优先级
    • setPriority( ):设置线程的优先级
主线程
  • main( ) 方法:每当用java命令启动一个Java虚拟机进程( Application 应用程序),Java虚拟机就会创建一个主线程,该线程从程序入口main()方法开始执行
  • 当在主线程中创建 Thread 类或其子类对象时,就创建了一个线程对象。主线程就是上述创建线程的父线程
  • Programmer可以控制线程的启动、挂起与终止。
线程的状态

新建、就绪、运行、阻塞、终止

  • 新建:当一个 Thread 类或其子类对象被创建时,新产生的线程处于新建状态,此时它已经有了相应的内存空间和其他资源
  • 就绪:调用 start( ) 方法来启动处于新建状态的线程后,将进入线程队列排队等待 CPU 服务,此时它已经具备了运行的条件,一旦轮到它来享用 CPU 资源时,就可以脱离创建它的主线程,开始自己的生命周期
  • 运行:当就绪状态的线程被调度并获得处理器资源时,便进入运行状态。
    • 每一个 Thread 类及其子类的对象都有一个重要的 run( ) 方法,当线程对象被调用执行时,它将自动调用本对象的 run( )方法,从第一句开始顺序执行。
    • run( ) 方法定义了这个线程的操作和功能。
  • 阻塞:一个正在执行的线程暂停自己的执行而进入的状态。引起线程由运行状态进入阻塞状态的可能情况:
    • 该线程正在等待 I/O 操作的完成:等待 I/O 操作完成或回到就绪状态
    • 网络操作
    • 为了获取锁而进入阻塞操作
    • 调用了该线程的 sleep( ) 方法:等待其指定的休眠事件结束后,自动脱离阻塞状态,回到就绪状态
    • 调用了 wait( ) 方法:调用 notify( )或 notifyAll( ) 方法;
    • 让处于运行状态的线程调用另一个线程的join()方法
  • 终止:
    • 自然终止:线程完成了自己的全部工作
    • 强制终止:在线程执行完之前,调用stop( ) 或 destroy( ) 方法终止线程

4 创建和启动线程

Java中实现多线程有三种方法:

  • 一种是继承Thread类;
  • 第二种是实现Runnable接口;
  • 第三种是实现Callable接口;
Thread构造方法

  • 一个线程的创建肯定是由另一个线程完成的;
  • 被创建线程的父线程是创建它的线程;
  • main线程由JVM创建,而main线程又可以成为其他线程的父线程;
  • 如果一个线程创建的时候没有指定ThreadGroup,那么将会和父线程同一个ThreadGroup。main线程所在的ThreadGroup称为main;
Thread常用方法



tips

因为Java线程的调度不是分时的,所以你必须确保你的代码中的线程会不时地给另外一个线程运行的机会。有三种方法可以做到一点:

  • 让处于运行状态的线程调用 Thread.sleep() 方法。
  • 让处于运行状态的线程调用 Thread.yield() 方法。
  • 让处于运行状态的线程调用另一个线程的 join() 方法。
sleep与yield
  • 这两个方法都是静态的实例方法。
  • sleep()会有中断异常抛出,而yiled()不抛出任何异常。
  • sleep()方法具有更好的可移植性,因为yield()的实现还取决于底层的操作系统对线程的调度策略。
  • 对于yield()的主要用途是在测试阶段人为的提高程序的并发性能,以帮助发现一些隐藏的并发错误,当程序正常运行时,则不能依靠yield方法提高程序的并发行能。
wait与sleep
  • 所以,wait,notify和notifyAll都是与同步相关联的方法,只有在synchronized方法中才可以用。在不同步的方法或代码中则使用sleep()方法使线程暂时停止运行
join

作用:使当前正在运行的线程暂停下来,等待指定的时间后等待调用该方法的线程结束后,再恢复运行

应用线程类Thread创建线程
  • 将一个类定义为Thread的子类,那么这个类就可以用来创建线程。
  • 这个类中有一个至关重要的方法——public void run,这个方法称为线程体,它是整个线程的核心,线程所要完成任务的代码都定义在线程体中,实际上不同功能的线程之间的区别就在于它们线程体的不同
应用Runnable接口创建线程
  • Runnable是Java中用以实现线程的接口,从根本上讲,任何实现线程功能的类都必须实现该接口。
    • Thread(Runnable target);
    • Thread(Runnable target, String name);
  • Runnable接口中只定义了一个方法就是run()方法,也就是线程体
适用于采用实现Runnable接口方法的情况
  • 避免单继承的局限:因为Java只允许单继承,如果一个类已经继承了Thread,就不能再继承其他类。
  • 特别是在除了run()方法以外,并不打算重写Thread类的其它方法的情况下,以实现Runnable接口的方式生成新线程就显得更加合理了。
  • 涉及到数据共享的时候;
终止线程
  • 当线程执行完run()方法,它将自然终止运行。
  • Thread有一个stop()方法,可以强制结束线程,但这种方法是不安全的。因此,在stop()方法已经被废弃。
  • 实际编程中,一般是定义一个标志变量,然后通过程序来改变标志变量的值,从而控制线程从run()方法中自然退出
总结:创建用户多线程的步骤

法1

法2

法3

  • 在程序开发中只要是多线程尽量以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类有如下好处:
    • 避免单继承的局限,一个类可以实现多个接口。
    • 适合于资源的共享
  • Runnable的局限性
    • run() 方法的返回值是void
    • 不允许抛出任何已检查的异常(编译时捕获的异常)
Callable接口
实现callable接口的步骤


callable接口的特点

  • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
线程池

日常开发中,推荐使用线程池的方式来使用。最开始创建一堆线程放在池子里,用的时候拿出来用,不用就放回去,能够减少线程的启动和灭亡

实际工作中如何选择
  • 取舍的基本原则就是需不需要返回值,如果不需要返回值,那直接就选 Runnable。如果有返回值的话,使用Callable。
  • 另外一点就是是否需要抛出异常, Runnable是不接受抛出异常的,Callable可以抛出异常。
  • Runnable适合那种纯异步的处理逻辑。比如每天定时计算报表,将报表存储到数据库或者其他地方,只是要计算,不需要马上展示,展示内容是在其他的方法中单独获取的。(比如那些非核心的功能,当核心流程执行完毕后,非核心功能就自己去执行)
  • Callable适用于那些需要返回值或者需要抛出checked exception的情况,比如对某个任务的计算结果进行处理。在Java中,常常使用callable来实现异步任务的处理,以提高系统的吞吐量和响应速度

网络编程 14

计算机网络工作模式
  • 客户机/服务器模式(Client/Server C/S)
    一共两种
    • 数据库服务器端,客户端通过数据库连接访问服务器端的数据
    • (本讲内容)Socket服务器端,服务器端的程序通过Socket与客户端的程序通信。另,socket服务器端为“传输层”,BS模式为“应用层”
  • 浏览器/服务器模式(Browser/Server)
网络通信协议与接口
  • 网络通信协议:计算机网络中实现通信必须有一些约定
  • 网络通信接口:为了使两个结点之间能进行对话,必须在他们之间建立通信工具(即接口),使彼此之间能进行信息交换,接口包括两部分:
    • 硬件装置:实现结点之间的信息传递。
    • 软件装置:规定双方进行通信的约定协议。
URI 包含 URL 和 URN

URI(Uniform Resource Identifier,统一资源标识符)用于唯一地标识资源,无论是通过名称、位置还是两者兼有。
URL(Uniform Resource Locator,统一资源定位符)是URI的一个子集,它提供了资源的定位信息,即如何访问资源,但不直接提供资源的名称。
URN(Uniform Resource Name)是URI的另一个子集,它提供了资源的名称,但不提供如何定位或访问资源的信息。URN是持久的、与位置无关的标识符。

TCP/IP

TCP和UDP




设计原则(7个) 7

SOLID合成复用

  1. S Single Responsibility Principle (单一职责原则) :每个类只干一件事
  2. O Open/Closed Principle (开闭原则) :用抽象类和接口而不是if-else
  3. L Liskov Substitution Principle (里氏代换原则) :子类不能改变父类的方法
  4. I Interface Segregation Principle (接口隔离原则) :把总接口拆分成多个接口(防止有的类不需要某个功能但被迫实现)
  5. D Dependency Inversion Principle (依赖倒转原则) :细节(更具体的东西,如email通信)实现抽象(更抽象的东西,如通信)而不是抽象拥有细节
  6. 迪米特法则:一个软件实体尽量少的与其他实体发生相互作用(找中介)
  7. 合成复用原则:少用继承,用组合/聚合代替继承



开闭

在Java中,开闭原则可以通过抽象类和接口来实现,这样可以在不修改现有代码的情况下扩展功能。以下是一个简单的Java例子,展示了如何遵循开闭原则。
不符合开闭原则的例子:

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
public class Rectangle {
public void draw() {
System.out.println("Drawing a rectangle");
}
}
public class Circle {
public void draw() {
System.out.println("Drawing a circle");
}
}
public class GraphicEditor {
public void drawShape(Object shape) {
if (shape instanceof Rectangle) {
((Rectangle) shape).draw();
} else if (shape instanceof Circle) {
((Circle) shape).draw();
}
// 如果要添加新的图形,比如三角形,需要修改这个方法
}
}
public class Main {
public static void main(String[] args) {
GraphicEditor editor = new GraphicEditor();
editor.drawShape(new Rectangle());
editor.drawShape(new Circle());
}
}

在这个例子中,GraphicEditor 类的 drawShape 方法依赖于具体的图形类。如果我们要添加一个新的图形类,比如三角形,我们需要修改 GraphicEditor 类,这违反了开闭原则。
符合开闭原则的例子:

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
public interface Shape {
void draw();
}
public class Rectangle implements Shape {
public void draw() {
System.out.println("Drawing a rectangle");
}
}
public class Circle implements Shape {
public void draw() {
System.out.println("Drawing a circle");
}
}
public class GraphicEditor {
public void drawShape(Shape shape) {
shape.draw();
}
}
public class Triangle implements Shape {
public void draw() {
System.out.println("Drawing a triangle");
}
}
public class Main {
public static void main(String[] args) {
GraphicEditor editor = new GraphicEditor();
editor.drawShape(new Rectangle());
editor.drawShape(new Circle());
// 添加新的图形时,不需要修改GraphicEditor类
editor.drawShape(new Triangle());
}
}

在这个改进的例子中,我们定义了一个 Shape 接口,所有的图形类都实现这个接口。GraphicEditor 类的 drawShape 方法现在接受一个 Shape 接口类型的参数,而不是具体的图形类。这样,当我们需要添加新的图形类(如 Triangle)时,我们只需要创建一个新的类实现 Shape 接口,而不需要修改 GraphicEditor 类。这符合开闭原则,因为我们对扩展是开放的,对修改是关闭的。

合成复用1

当然可以。下面是一个更简单的例子,用于说明合成复用原则:
场景:我们有一个表示汽车的类,汽车可以有不同的引擎。而不是通过继承来创建不同类型的汽车,我们使用组合来复用引擎的行为。
不使用合成复用原则的例子(使用继承):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Engine {
public void start() {
System.out.println("Engine starts");
}
}
class CarWithPetrolEngine extends Engine {
// 使用汽油引擎的汽车
}
class CarWithDieselEngine extends Engine {
// 使用柴油引擎的汽车
}
public class Main {
public static void main(String[] args) {
CarWithPetrolEngine petrolCar = new CarWithPetrolEngine();
petrolCar.start(); // Engine starts
CarWithDieselEngine dieselCar = new CarWithDieselEngine();
dieselCar.start(); // Engine starts
}
}

在这个例子中,我们通过继承来创建不同类型的汽车,但这可能导致不必要的复杂性,尤其是当引擎类型增多时。
使用合成复用原则的例子(使用组合):

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
interface Engine {
void start();
}
class PetrolEngine implements Engine {
public void start() {
System.out.println("Petrol engine starts");
}
}
class DieselEngine implements Engine {
public void start() {
System.out.println("Diesel engine starts");
}
}
class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
}
}
public class Main {
public static void main(String[] args) {
Car petrolCar = new Car(new PetrolEngine());
petrolCar.start(); // Petrol engine starts
Car dieselCar = new Car(new DieselEngine());
dieselCar.start(); // Diesel engine starts
}
}

在这个改进的例子中,我们定义了一个 Engine 接口和两个实现类 PetrolEngineDieselEngineCar 类有一个 Engine 类型的成员变量,并在构造函数中注入具体的引擎。这样,我们可以通过组合不同的引擎来创建不同类型的汽车,而不是通过继承。
这种方法的好处是,如果将来我们有新的引擎类型(例如电动引擎),我们只需要添加一个新的实现类,而不需要修改 Car 类或其子类。这提高了代码的复用性、灵活性和可维护性。

合成复用2

合成复用原则(Composite Reuse Principle)是面向对象设计的原则之一,它建议在设计中要尽量使用对象组合,而不是继承来达到复用的目的。该原则强调通过组合不同的对象来获得新的功能,而不是通过继承来扩展类的功能。这样可以减少系统的复杂性,提高灵活性和可维护性。
以下是一个Java例子,展示了如何应用合成复用原则:
不使用合成复用原则的例子(使用继承):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Bird {
public void fly() {
System.out.println("Bird is flying");
}
}
class Sparrow extends Bird {
// Sparrow继承了Bird的fly方法
}
class Penguin extends Bird {
// Penguin继承了Bird的fly方法,但企鹅不会飞,这导致了不合理的设计
@Override
public void fly() {
throw new UnsupportedOperationException("Penguin cannot fly");
}
}
public class Main {
public static void main(String[] args) {
Sparrow sparrow = new Sparrow();
sparrow.fly(); // 合理
Penguin penguin = new Penguin();
penguin.fly(); // 不合理,抛出异常
}
}

在这个例子中,Penguin 类继承了 Bird 类,但企鹅不会飞,所以继承导致了不合理的设计。我们需要重写 fly 方法来抛出异常,这违反了合成复用原则。
使用合成复用原则的例子(使用组合):

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
interface Flyable {
void fly();
}
class Bird {
private Flyable flyable;
public Bird(Flyable flyable) {
this.flyable = flyable;
}
public void performFly() {
flyable.fly();
}
}
class Sparrow extends Bird {
public Sparrow() {
super(new Flyable() {
public void fly() {
System.out.println("Sparrow is flying");
}
});
}
}
class Penguin extends Bird {
public Penguin() {
super(new Flyable() {
public void fly() {
System.out.println("Penguin cannot fly");
}
});
}
}
class FlyWithWings implements Flyable {
public void fly() {
System.out.println("Flying with wings");
}
}
class NoFly implements Flyable {
public void fly() {
System.out.println("Cannot fly");
}
}
public class Main {
public static void main(String[] args) {
Sparrow sparrow = new Sparrow();
sparrow.performFly(); // 合理
Penguin penguin = new Penguin();
penguin.performFly(); // 合理,输出"Penguin cannot fly"
}
}

在这个改进的例子中,我们定义了一个 Flyable 接口,表示飞行的能力。Bird 类有一个 Flyable 类型的成员变量,并在构造函数中注入飞行行为。SparrowPenguin 类通过构造函数分别注入了不同的飞行行为。这样,我们通过组合而不是继承来实现了复用,符合合成复用原则。
通过这种方式,我们可以更容易地添加新的飞行行为或者修改现有的行为,而无需修改 Bird 类或其子类的代码,从而提高了系统的灵活性和可维护性。

依赖倒转

依赖倒转原则(Dependency Inversion Principle,DIP)是面向对象设计原则之一,也是SOLID原则中的“D”。它主张:

  1. 高层模块不应该依赖低层模块,两者都应该依赖抽象。
  2. 抽象不应该依赖细节,细节应该依赖抽象。
    简单例子:假设我们有一个通知系统,最初只通过邮件发送通知。
    违反依赖倒转原则的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class EmailService {
public void sendEmail(String message, String recipient) {
// 发送邮件的实现
}
}
class NotificationService {
private EmailService emailService;
public NotificationService() {
this.emailService = new EmailService();
}
public void notify(String message, String recipient) {
emailService.sendEmail(message, recipient);
}
}

在这个例子中,NotificationService 直接依赖于 EmailService 的具体实现,这违反了依赖倒转原则。
遵循依赖倒转原则的改进代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface NotificationService {
void notify(String message, String recipient);
}
class EmailService implements NotificationService {
public void notify(String message, String recipient) {
// 发送邮件的实现
}
}
class SMSService implements NotificationService {
public void notify(String message, String recipient) {
// 发送短信的实现
}
}
class NotificationController {
private NotificationService notificationService;
public NotificationController(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void sendNotification(String message, String recipient) {
notificationService.notify(message, recipient);
}
}

在这个改进的例子中,我们引入了 NotificationService 接口,作为抽象。EmailServiceSMSService 都实现了这个接口。NotificationController 类依赖于 NotificationService 接口,而不是具体的实现。
这样,如果将来我们需要添加新的通知方式(如微信、推送通知等),我们只需要创建新的实现类即可,而不需要修改 NotificationController。这提高了代码的灵活性和可维护性。
总结:依赖倒转原则通过依赖抽象而不是具体实现,降低了模块间的耦合度,使得系统更易于扩展和维护。

接口隔离原则

假设我们有一个用于打印文档的接口 Printer,它包含以下方法:

  • printDocument()
  • scanDocument()
  • faxDocument()
  • printPhoto()
    现在,我们有几个不同的类实现了这个接口:
  1. SimplePrinter:一个基本的打印机,只能打印文档。
  2. MultiFunctionPrinter:一个多功能打印机,可以打印、扫描、传真和打印照片。

根据接口隔离原则,SimplePrinter 类不应该被迫实现 scanDocument()faxDocument() 和 printPhoto() 这些它不需要的方法。这样做会导致 SimplePrinter 类包含冗余的、不相关的代码。

应用接口隔离原则后的改进:
我们可以将 Printer 接口拆分成更小的接口:

  • PrintDocumentInterface:包含 printDocument() 方法。
  • ScanDocumentInterface:包含 scanDocument() 方法。
  • FaxDocumentInterface:包含 faxDocument() 方法。
  • PrintPhotoInterface:包含 printPhoto() 方法。
    然后,我们的类可以按需实现这些接口:
  1. SimplePrinter:实现 PrintDocumentInterface
  2. MultiFunctionPrinter:实现 PrintDocumentInterfaceScanDocumentInterfaceFaxDocumentInterface 和 PrintPhotoInterface
迪米特法则

在Java中,迪米特法则(Law of Demeter)同样强调减少类之间的直接交互,以降低耦合度。以下是一个简单的Java例子,用于说明如何应用迪米特法则。
场景: 假设我们有一个订单处理系统,其中包含Order(订单)、Customer(客户)和Payment(支付)等类。
不符合迪米特法则的设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Order {
private Customer customer;
private Payment payment;
public void process() {
// Order类直接与Customer和Payment类交互
customer.verify();
payment.pay();
}
}
class Customer {
public void verify() {
// 客户验证逻辑
}
}
class Payment {
public void pay() {
// 支付逻辑
}
}

在这个例子中,Order 类直接调用了 CustomerPayment 类的方法,这意味着 Order 类需要了解 CustomerPayment 类的内部实现。这违反了迪米特法则。
符合迪米特法则的设计:
为了遵守迪米特法则,我们可以引入一个中介者类,例如OrderProcessor,来处理订单处理的逻辑:
我们还可以进一步封装 Order 类,使其不直接暴露 CustomerPayment 对象。通过这种方式,我们进一步限制了 Order 类与其他类的直接交互,使得类之间的关系更加清晰,符合迪米特法则。

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
class Order {
private Customer customer;
private Payment payment;
public void process(OrderProcessor processor) {
processor.process(this);
}
// 私有方法,用于OrderProcessor访问
private Customer getCustomer() {
return customer;
}
// 私有方法,用于OrderProcessor访问
private Payment getPayment() {
return payment;
}
}
class Customer {
public void verify() {
// 客户验证逻辑
}
}
class Payment {
public void pay() {
// 支付逻辑
}
}
class OrderProcessor {
public void process(Order order) {
// OrderProcessor负责处理订单,与Customer和Payment类交互
order.getCustomer().verify();
order.getPayment().pay();
}
}

在这个改进后的设计中,Order 类不再直接与 CustomerPayment 类交互,而是通过 OrderProcessor 类来进行。这样,Order 类不需要了解 CustomerPayment 类的内部实现,从而减少了类之间的耦合。
迪米特法则的应用有助于创建松耦合、高内聚的类设计,从而提高代码的可维护性和可扩展性。然而,也需要注意不要过度应用,以免导致代码过于复杂或难以理解。

里氏代换原则

里氏代换原则(Liskov Substitution Principle,LSP)是面向对象设计中的五大原则之一,由芭芭拉·利斯科夫(Barbara Liskov)在1987年提出。该原则的核心思想是:子类对象应该能够替换其父类对象,而不会导致程序的业务逻辑出现异常。
里氏代换原则强调的是子类和父类之间的兼容性,即子类应该继承父类的所有属性和行为,并且可以在此基础上进行扩展,但不能改变父类原有的行为。这样,在程序中,我们可以放心地使用父类对象的地方替换为子类对象,而不会影响程序的正确性。
以下是一个Java例子,用于说明里氏代换原则:
场景: 假设我们有一个表示鸟的基类Bird,以及两个子类 Sparrow(麻雀)和Ostrich(鸵鸟)。
不符合里氏代换原则的设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Bird {
public void fly() {
System.out.println("This bird can fly.");
}
}
class Sparrow extends Bird {
// Sparrow继承了Bird的fly方法,可以飞行
}
class Ostrich extends Bird {
// Ostrich也继承了Bird的fly方法,但鸵鸟实际上不会飞
// 这违反了里氏代换原则,因为Ostrich不能替换Bird而不改变程序的预期行为
@Override
public void fly() {
throw new UnsupportedOperationException("Ostrich cannot fly.");
}
}
public class Main {
public static void main(String[] args) {
Bird bird = new Sparrow();
bird.fly(); // 正常运行
bird = new Ostrich();
bird.fly(); // 这里会抛出异常,违反了里氏代换原则
}
}

在这个例子中,Ostrich 类继承了 Bird 类的 fly 方法,但实际上鸵鸟是不会飞的。当我们尝试用 Ostrich 对象替换 Bird 对象时,调用 fly 方法会抛出异常,这违反了里氏代换原则。
符合里氏代换原则的设计:
为了遵守里氏代换原则,我们不应该让 Ostrich 继承 Birdfly 方法。我们可以通过提取接口或使用组合的方式来解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Flyable {
void fly();
}
class Bird {
// Bird类不再包含fly方法
}
class Sparrow extends Bird implements Flyable {
@Override
public void fly() {
System.out.println("Sparrow can fly.");
}
}
class Ostrich extends Bird {
// Ostrich不再继承fly方法,因此不会违反里氏代换原则
}
public class Main {
public static void main(String[] args) {
Flyable flyableBird = new Sparrow();
flyableBird.fly(); // 正常运行
Bird bird = new Ostrich();
// 鸵鸟没有fly方法,但我们也没有期望它能够飞行
// 这符合里氏代换原则,因为Ostrich可以替换Bird而不改变程序的预期行为
}
}

在这个改进后的设计中,我们引入了 Flyable 接口,只有会飞的鸟(如 Sparrow)才实现这个接口。Ostrich 类不再继承 fly 方法,因此不会违反里氏代换原则。这样,我们可以确保在程序中替换父类对象为子类对象时,不会影响程序的正确性。
里氏代换原则是面向对象设计中的重要原则,它有助于我们设计出更加灵活、可扩展和可维护的代码。通过遵守这个原则,我们可以确保子类和父类之间的兼容性,避免在程序运行时出现意外行为。

设计模式 7


工厂方法模式的核心是把类的实例化延迟到其子类
被造的东西有个接口、工厂有个接口,被造的东西和工厂分别实现这两个接口,然后工厂类 public Vehicle createVehicle() { return new Car();}
适配器模式的核心是将一个类的接口转换成客户希望的另外一个接口
两个接口:原来的东西和新加的东西。类来实现新加的东西的接口。Adapter实现原来的东西的接口,同时拥有新加的东西的接口。
装饰模式的核心是动态地给对象添加一些额外的职责。
具体组件继承抽象组件;抽象装饰继承抽象组件,同时拥有抽象组件;具体装饰继承抽象装饰,有装饰函数;调用时 Bird bird = new Sparrow; bird = new birdDecorator(bird)
外观模式的核心是通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。
外观角色拥有子系统123;客户角色依赖外观角色。
策略模式的核心是定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
上下文拥有抽象策略,同时在方法体内调用策略的算法;具体策略实现抽象策略
访问者模式的核心是在不改变各个元素的类的前提下定义作用于这些元素的新操作
元素、访问者接口;具体元素实现元素接口;具体访问者实现访问者接口;具体元素和集体访问者相互关联
责任链模式的关键是将用户的请求分派给许多对象
处理者接口规定具体处理者处理用户的请求的方法以及具体处理者设置后继对象的方法;具体处理者实现处理者接口;使用时先设置后继对象在调用第一个处理者
观察者模式的核心是当一个对象改变状态时,所有依赖于它的对象都会得到通知并自动更新。
被观察者存了一个list表示观察者,观察者存了自己观察的对象。当被观察者发生变化时,通知观察者,观察者更新数据并展示出来

单例模式

  • 饿汉式:类加载时就创建实例,像是一个饥饿的人急于吃东西。
  • 懒汉式:使用时才创建实例,像是一个懒惰的人等到需要时才行动。

饿汉式
1
2
3
4
5
6
7
8
9
10
public class Singleton {  
// 静态的。保留自身的引用,类加载时就初始化
private static Singleton test = new Singleton();
// 必须是私有的构造函数
private Singleton() {}
// 公共的静态方法
public static Singleton getInstance() {
return test;
}
}
懒汉式
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {  
// 静态的。保留自身的引用
private static Singleton test = null;
// 必须是私有的构造函数
private Singleton() {}
// 公共的静态方法
public static Singleton getInstance() {
if(test == null) {
test = new Singleton();
}
return test;
}
}
多线程安全的懒汉式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 修改上述代码避免多线程中的安全问题
public class Singleton {
private static Singleton test = null;
private Singleton() {}
public static Singleton getInstance() {
if(test == null) {
synchronized(Singleton.class){
if(test == null){
test = new Singleton();
}
}
}
return test;
}
}

工厂模式

简单工厂:一个工厂类,由一个工厂类根据传入的参数决定创建哪一种产品类的实例。
工厂方法:一个工厂接口,一堆工厂类(每种产品都有一个类),一条产品线(产出不同种类的东西)。工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法模式让类的实例化推迟到子类。
抽象工厂:一个工厂接口,一堆工厂类(每种产品都有一个类),一堆产品线(每条产品线能产出不同种类的东西),提供了一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。这种模式通常用于系统中有多个产品族,且每个产品族都有多个产品等级的情况。

工厂模式(Factory Pattern)是Java中最常用的设计模式之一。这种模式提供了一种创建对象的最佳方式,通过使用工厂模式,我们可以将对象的创建逻辑与使用逻辑分离,使得客户端代码不依赖于具体类的实现,而是依赖于抽象接口或类。这样,当需要更换或增加新的产品类时,不需要修改客户端代码,提高了代码的可扩展性和可维护性。
工厂模式主要有三种形式:

  1. 简单工厂模式(Simple Factory Pattern)
  2. 工厂方法模式(Factory Method Pattern)
  3. 抽象工厂模式(Abstract Factory Pattern)
    下面以简单工厂模式和工厂方法模式为例,介绍工厂模式在Java中的实现。
简单工厂模式


示例: 假设我们需要创建不同类型的交通工具,如汽车和自行车。

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
// 交通工具接口
interface Vehicle {
void drive();
}
// 汽车类
class Car implements Vehicle {
@Override
public void drive() {
System.out.println("Driving a car");
}
}
// 自行车类
class Bicycle implements Vehicle {
@Override
public void drive() {
System.out.println("Riding a bicycle");
}
}
// 简单工厂类
class VehicleFactory {
public static Vehicle createVehicle(String type) {
if ("car".equals(type)) {
return new Car();
} else if ("bicycle".equals(type)) {
return new Bicycle();
}
throw new IllegalArgumentException("Unknown vehicle type");
}
}
// 客户端代码
public class SimpleFactoryDemo {
public static void main(String[] args) {
Vehicle car = VehicleFactory.createVehicle("car");
car.drive();
Vehicle bicycle = VehicleFactory.createVehicle("bicycle");
bicycle.drive();
}
}
工厂方法模式


示例: 继续使用交通工具的例子,但这次我们将工厂类抽象化,并为每种交通工具提供一个具体的工厂类。

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
// 交通工具接口
interface Vehicle {
void drive();
}
// 汽车类
class Car implements Vehicle {
@Override
public void drive() {
System.out.println("Driving a car");
}
}
// 自行车类
class Bicycle implements Vehicle {
@Override
public void drive() {
System.out.println("Riding a bicycle");
}
}
// 工厂接口
interface VehicleFactory {
Vehicle createVehicle();
}
// 汽车工厂类
class CarFactory implements VehicleFactory {
@Override
public Vehicle createVehicle() {
return new Car();
}
}
// 自行车工厂类
class BicycleFactory implements VehicleFactory {
@Override
public Vehicle createVehicle() {
return new Bicycle();
}
}
// 客户端代码
public class FactoryMethodDemo {
public static void main(String[] args) {
VehicleFactory carFactory = new CarFactory();
Vehicle car = carFactory.createVehicle();
car.drive();
VehicleFactory bicycleFactory = new BicycleFactory();
Vehicle bicycle = bicycleFactory.createVehicle();
bicycle.drive();
}
}

在这个例子中,我们定义了一个 VehicleFactory 接口和两个实现该接口的工厂类 CarFactoryBicycleFactory。每个工厂类负责创建一种类型的交通工具。客户端代码通过具体的工厂类来创建对象,这样当需要添加新的交通工具时,只需要添加新的工厂类和产品类,而不需要修改现有的代码。
工厂模式在Java中的应用非常广泛,它可以帮助我们更好地组织代码,实现解耦和灵活的对象创建。通过使用工厂模式,我们可以提高代码的可扩展性、可维护性和可测试性。

抽象工厂


要将上述代码改写成抽象工厂模式,我们需要定义一个抽象工厂接口,该接口不仅负责创建交通工具,还可能负责创建与交通工具相关的其他产品,比如轮胎(Tire)或引擎(Engine)。这样,每个具体工厂就能创建一个产品家族,而不仅仅是一个产品。
以下是一个简单的示例,展示如何将代码改写成抽象工厂模式:

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 交通工具接口
interface Vehicle {
void drive();
}
// 汽车类
class Car implements Vehicle {
@Override
public void drive() {
System.out.println("Driving a car");
}
}
// 自行车类
class Bicycle implements Vehicle {
@Override
public void drive() {
System.out.println("Riding a bicycle");
}
}
// 轮胎接口
interface Tire {
void roll();
}
// 汽车轮胎类
class CarTire implements Tire {
@Override
public void roll() {
System.out.println("Car tire rolling");
}
}
// 自行车轮胎类
class BicycleTire implements Tire {
@Override
public void roll() {
System.out.println("Bicycle tire rolling");
}
}
// 抽象工厂接口
interface VehicleFactory {
Vehicle createVehicle();
Tire createTire();
}
// 汽车工厂类
class CarFactory implements VehicleFactory {
@Override
public Vehicle createVehicle() {
return new Car();
}
@Override
public Tire createTire() {
return new CarTire();
}
}
// 自行车工厂类
class BicycleFactory implements VehicleFactory {
@Override
public Vehicle createVehicle() {
return new Bicycle();
}
@Override
public Tire createTire() {
return new BicycleTire();
}
}
// 客户端代码
public class AbstractFactoryDemo {
public static void main(String[] args) {
VehicleFactory carFactory = new CarFactory();
Vehicle car = carFactory.createVehicle();
Tire carTire = carFactory.createTire();
car.drive();
carTire.roll();
VehicleFactory bicycleFactory = new BicycleFactory();
Vehicle bicycle = bicycleFactory.createVehicle();
Tire bicycleTire = bicycleFactory.createTire();
bicycle.drive();
bicycleTire.roll();
}
}

在这个改写后的例子中,VehicleFactory 接口现在有两个方法:createVehiclecreateTire。每个具体工厂(CarFactoryBicycleFactory)都实现了这两个方法,分别用于创建交通工具和对应的轮胎。这样,每个工厂都能创建一个产品家族,而客户端代码可以通过抽象工厂接口来获取这些相关产品的实例。
抽象工厂模式的关键在于提供一个接口,用于创建多个相关或依赖对象的家族,而不需要明确指定具体类。这样,客户端代码就可以与具体类的实现细节解耦。

原型模式

原型模式(Prototype Pattern) 在Java中通常用于创建对象的一个副本,而不是通过构造函数重新创建。
主要角色:

  1. Prototype(原型接口):声明一个克隆自己的方法。
  2. ConcretePrototype(具体原型类):实现原型接口,实现克隆方法。
  3. Client(客户端):使用原型实例来创建新的对象。
示例:文档编辑器中的文档复制

假设我们有一个文档编辑器,用户可以创建文档,并希望能够复制现有的文档以创建新的文档。这里,文档对象就是一个原型。

  1. 定义原型接口
1
2
3
4
5
public interface DocumentPrototype extends Cloneable {
DocumentPrototype clone();
void setContent(String content);
String getContent();
}
  1. 实现具体原型类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TextDocument implements DocumentPrototype {
private String content;
public TextDocument(String content) {
this.content = content;
}
@Override
public DocumentPrototype clone() {
try {
return (TextDocument) super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
@Override
public void setContent(String content) {
this.content = content;
}
@Override
public String getContent() {
return content;
}
}
  1. 客户端代码使用原型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DocumentEditor {
public static void main(String[] args) {
// 创建一个初始文档
TextDocument originalDocument = new TextDocument("Hello, World!");
System.out.println("Original Document Content: " + originalDocument.getContent());
// 复制文档
TextDocument copiedDocument = (TextDocument) originalDocument.clone();
System.out.println("Copied Document Content: " + copiedDocument.getContent());
// 修改复制后的文档内容
copiedDocument.setContent("Hello, Prototype Pattern!");
System.out.println("Modified Copied Document Content: " + copiedDocument.getContent());
// 原始文档内容保持不变
System.out.println("Original Document Content after copy modification: " + originalDocument.getContent());
}
}
输出:
1
2
3
4
Original Document Content: Hello, World!
Copied Document Content: Hello, World!
Modified Copied Document Content: Hello, Prototype Pattern!
Original Document Content after copy modification: Hello, World!
说明:
  • DocumentPrototype 接口定义了克隆方法,所有具体的文档类都需要实现这个接口。
  • TextDocument 类实现了 DocumentPrototype 接口,并提供了具体的克隆实现。这里使用了Java的 clone() 方法,它执行的是深拷贝。
  • DocumentEditor 类中,我们创建了一个文档对象,并使用原型模式复制了这个对象。修改复制后的文档不会影响原始文档。
    这个例子展示了如何使用原型模式来复制对象,从而避免了通过构造函数重新创建对象的成本。

适配器模式

  • 目标(Target):目标是一个接口,该接口是客户想使用的接口
  • 被适配者(Adaptee):被适配者是一个已经存在的接口或抽象类,这个接口或抽象类需要适配。
  • 适配器(Adapter):适配器是一个类,该类实现了目标接口并包含有被适配者的引用,即适配器的职责是对被适配者接口(抽象类)与目标接口进行适配

例1

在Java中,适配器模式通常用于将一个类的接口转换成客户期望的另一个接口,使原本接口不兼容的类可以合作无间。下面通过一个具体的例子来介绍适配器模式的应用。

场景描述

假设我们有一个MediaPlayer接口,它有一个play方法,用于播放音乐文件。目前它只能播放mp3格式的文件。现在我们需要扩展功能,使其能够播放mp4vlc格式的文件。但是,我们已经有了一些可以播放这些格式的类(Mp4PlayerVlcPlayer),它们的接口与MediaPlayer不兼容。这时,我们可以使用适配器模式来解决这个问题。

  • 目标: MediaPlayer
  • 被适配者: AdvancedMediaPlayer
  • 适配器: MediaAdapter
类图
1
2
3
4
5
6
7
8
9
+-------------------+       +-------------------+       +-------------------+
| MediaPlayer | | MediaAdapter | | AdvancedMediaPlayer|
+-------------------+ +-------------------+ +-------------------+
| +play(String, |<>---->| -advancedMusicPlayer |<>---->| +playVlc(String) |
| String) | | | | +playMp4(String) |
+-------------------+ +-------------------+ +-------------------+
| +play(String, |
| String) |
+-------------------+
代码实现
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
64
// MediaPlayer.java  
interface MediaPlayer { // 目标
void play(String audioType, String fileName);
}
// AdvancedMediaPlayer.java
interface AdvancedMediaPlayer { // 被适配者
void playVlc(String fileName);
void playMp4(String fileName);
}
// Mp4Player.java
class Mp4Player implements AdvancedMediaPlayer { // 被适配者的具体类
@Override
public void playVlc(String fileName) {
// Do nothing
}
@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file. Name: " + fileName);
}
}
// VlcPlayer.java
class VlcPlayer implements AdvancedMediaPlayer { // 被适配者的具体类
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file. Name: " + fileName);
}
@Override
public void playMp4(String fileName) {
// Do nothing
}
}
// MediaAdapter.java
class MediaAdapter implements MediaPlayer { // 适配器
AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter() {}
@Override
public void play(String audioType, String fileName) {
// Inbuilt support to play mp3 music files
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing mp3 file. Name: " + fileName);
}
// MediaAdapter is providing support to play other file formats
else if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer = new VlcPlayer();
advancedMusicPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer = new Mp4Player();
advancedMusicPlayer.playMp4(fileName);
} else {
System.out.println("Invalid media. " + audioType + " format not supported");
}
}
}

// Main.java
public class Main {
public static void main(String[] args) {
MediaAdapter audioPlayer = new MediaAdapter();
audioPlayer.play("mp3", "beyond the horizon.mp3");
audioPlayer.play("mp4", "alone.mp4");
audioPlayer.play("vlc", "far far away.vlc");
audioPlayer.play("avi", "mind me.avi");
}
}
输出
1
2
3
4
Playing mp3 file. Name: beyond the horizon.mp3
Playing mp4 file. Name: alone.mp4
Playing vlc file. Name: far far away.vlc
Invalid media. avi format not supported

在这个例子中,AudioPlayer类实现了MediaPlayer接口,可以播放mp3文件。对于mp4vlc文件,它使用了一个MediaAdapter来适配AdvancedMediaPlayer接口,从而实现了播放不同格式文件的功能。这样,我们就通过适配器模式实现了接口的转换,使得原本不兼容的类可以一起工作。

装饰模式

  • 抽象组件(Component):抽象组件(是抽象类)定义了需要进行装饰的方法。抽象组件就是“被装饰者”角色
  • 具体组件(ConcreteComponent):具体组件是抽象组件的一个子类。
  • 装饰(Decorator):该角色是抽象组件的一个子类,是“装饰者”角色,其作用是装饰具体组件。Decorator角色需要包含抽象组件的引用
  • 具体装饰(ConcreteDecotator):具体装饰是Decorator角色的一个非抽象子类

抽象组件

具体组件

装饰

具体装饰

模式的使用


最后的bird

外观模式(门面模式)

  • 外观(Facade)模式又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。
  • 该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,降低其与子系统的耦合,提高了程序的可维护性。
  • 是“迪米特法则”的典型应用
    迪米特法则: 一个软件实体应当尽可能少地与其他实体发生相互作用


策略模式

  • 策略(Strategy):策略是一个接口,该接口定义若干个算法标识,即定义了若干个抽象方法核心就是将类中经常需要变化的部分分割出来,并将每种可能的变化对应地交给抽象类的一个子类或实现接口的一个类去负责,从而让类的设计者不去关心具体实现,避免所设计的类依赖于具体的实现。
  • 上下文(Context):上下文是依赖于策略接口的(是面向策略设计的类),即上下文包含有用策略声明的变量。上下文中提供一个方法,该方法委托策略变量调用具体策略所实现的策略接口中的方法
  • 具体策略(ConcreteStrategy):具体策略是实现策略接口的类。具体策略实现策略接口所定义的抽象方法,即给出算法标识的具体算法

问题:在多个裁判负责打分的比赛中,每位裁判给选手一个得分,选手的最后得分是根据全体裁判的得分计算出来的。请给出几种计算选手得分的评分方案(策略),对于某次比赛,可以从你的方案中选择一种方案作为本次比赛的评分方案。

  • 在这里我们把策略接口命名为:Strategy。在具体应用中,这个角色的名字可以根据具体问题来命名。

  • 在本问题中将上下文命名为 AverageScore,即让 AverageScore 类依赖于 Strategy 接口。

  • 每个具体策略负责一系列算法中的一个。

  • 策略( Strategy

  • 上下文( Context

  • 具体策略StrategyA.java

  • 具体策略StrategyB.java

  • 模式的使用


访问者模式

  • 模式优点:在不改变一个集合中的元素的类的情况下,可以增加新的施加于该元素上的新操作。保持一定的扩展性。
  • 使用场景:需要对集合中的对象进行很多不同的并且不相关的操作,而我们又不想修改对象的类,就可以使用访问者模式。访问者模式可以在Visitor类中集中定义一些关于集合中对象的操作。

  • 抽象元素(Element):一个抽象类,该类定义了接收访问者的accept操作。

  • 具体元素(Concrete Element):Element的子类。

  • 抽象访问者(Visitor):一个接口,该接口定义操作具体元素的方法。

  • 具体访问者(Concrete Visitor):实现Visitor接口的类。

  • 门诊部是一个类似于访问者的对象,它可以访问不同类型的病人对象,例如普通病人、急诊病人、儿科病人等。

  • 不同类型的病人对象可以有不同的处理方法,例如看病、输液、检查等。

  • 门诊部可以对不同类型的病人对象进行不同的操作,而不需要改变病人对象的类层次结构。

  • 抽象访问者

  • 具体访问者

  • 抽象元素

  • 具体元素

  • 结构对象

  • 测试案例

责任链模式

责任链模式是使用多个对象处理用户请求的成熟模式,责任链模式的关键是将用户的请求分派给许多对象。

  • 处理者(Handler):处理者是一个接口,负责规定具体处理者处理用户的请求的方法以及具体处理者设置后继对象的方法。
  • 具体处理者(ConcreteHandler):具体处理者是实现处理者接口的类的实例。具体处理者通过调用处理者接口规定的方法处理用户的请求,即在接到用户的请求后,处理者将调用接口规定的方法,在执行该方法的过程中,如果发现能处理用户的请求,就处理有关数据,否则就反馈无法处理的信息给用户,然后将用户的请求传递给自己的后继对象


抽象处理者:领导类

具体处理者1:班主任类

具体处理者2:系主任类

具体处理者:院长类

测试类

dlc:具体处理者4:教务处长类

观察者模式

被观察者存了一个list表示观察者,观察者存了自己观察的对象
当被观察者发生变化时,通知观察者,观察者更新数据并展示出来

观察者模式(Observer Pattern)是一种行为设计模式,它定义了对象之间的一对多依赖关系,当一个对象改变状态时,所有依赖于它的对象都会得到通知并自动更新。这种模式在Java中经常用于实现事件处理系统、消息订阅和发布等场景。

场景描述

假设我们有一个天气数据类(WeatherData),它包含了温度、湿度等天气信息。我们希望当天气数据更新时,能够通知多个显示天气信息的界面(如当前天气状况显示、天气统计信息显示等)进行更新。这里可以使用观察者模式来实现。

类图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+-------------------+       +-------------------+       +-------------------+
| Observer | | Subject | | ConcreteObserver |
+-------------------+ +-------------------+ +-------------------+
| +update() |<>---->| +registerObserver()| | +update() |
+-------------------+ | +removeObserver() | +-------------------+
| +notifyObservers() |
+-------------------+
^
|
+-------------------+ +-------------------+
| ConcreteObserver | | WeatherData |
+-------------------+ +-------------------+
| +update() | | -observers |
+-------------------+ | -temperature |
| -humidity |
| -pressure |
| + measurementsChanged()|
| + registerObserver() |
| + removeObserver() |
| + notifyObservers() |
+-------------------+
代码实现
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
64
65
66
67
68
69
70
71
72
73
// Observer.java 观察者
public interface Observer {
void update(float temperature, float humidity, float pressure);
}
// Subject.java 被观察者
public interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
// DisplayElement.java
public interface DisplayElement {
void display();
}
// WeatherData.java 被观察者——天气
public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<>();
}
public void registerObserver(Observer o) {
observers.add(o);
}
public void removeObserver(Observer o) {
observers.remove(o);
}
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}
public void measurementsChanged() {
notifyObservers();
}
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
// Other WeatherData methods here.
}
// CurrentConditionsDisplay.java 观察者,实现展示功能
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private Subject weatherData;
public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
public void display() {
System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
}
}
// Main.java
public class Main {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
}
}
输出
1
2
3
Current conditions: 80.0F degrees and 65.0% humidity
Current conditions: 82.0F degrees and 70.0% humidity
Current conditions: 78.0F degrees and 90.0% humidity

在这个例子中,WeatherData类实现了Subject接口,它有一个观察者列表,用于注册、移除和通知观察者。CurrentConditionsDisplay类实现了ObserverDisplayElement接口,它注册为WeatherData的观察者,并在数据更新时接收通知并显示当前天气状况。
WeatherDatasetMeasurements方法被调用时,它会更新天气数据并调用measurementsChanged方法,后者会通知所有注册的观察者。观察者接收到通知后,会调用它们的update方法来获取新的数据并更新显示。
这样,我们就通过观察者模式实现了当天气数据变化时,自动通知

UML图 7


Java集合框架 8


普通数组的定义int[] a = new int[10];

ArrayList:无序,可重复,长度可变,遍历元素和随机访问元素效率较高

数组大小: site.size()

LinkedList:无序,可重复,FIFO,插入删除元素效率较高


HashSet:无序,不可重复

HashMap:无序,键(Key)不能重复,值(Value)可以重复

重写排序
如果 a 是 list: Collection.sort(a)
如果 a 是普通数组: Arrays.sort(a)


【学习经验】OOP复习
http://example.com/2025/01/10/LE-OOP1/
Author
John Doe
Posted on
January 10, 2025
Licensed under