面向对象的程序设计

一、Java 语言

1. Java 的特点

  • 面向对象

    • 两个基本概念:类、对象
    • 三大特性:封装、继承、多态
  • 健壮性

    吸收了 C/C++ 语言的优点,但是去掉了指针、内存的申请和释放等影响程序健壮性的部分,提供了一个相对安全的的内存管理和访问机制

  • 跨平台性

    Write once, run anywhere. 由 JVM 来负责 Java 程序在不同系统上的运行。

2. Java 虚拟机

  • JVM 是一个虚拟的计算机,具有指令集并使用不同的存储区域,负责执行指令,管理数据、内存、寄存器。
  • 不同平台对应的 JVM 也不同。只要某平台提供了对应的 Java 虚拟机,Java 程序就可以在此平台运行。
  • Java 虚拟机机制屏蔽了底层运行平台的差异。

3. 垃圾回收机制

  • Java 提供了一种系统级线程跟踪存储空间分配情况的手段,在 JVM 空闲时,检查并释放那些可被释放的内存空间。
  • 垃圾回收在 Java 程序运行过程中自动进行,程序员无法精确控制和干预。
  • 即便如此,Java 程序仍可能会出现内存泄漏和溢出的问题

4. JRE 和 JDK

  • Java Runtime Environment( JRE,Java 运行环境)= JVM + Java SE 标准类库
  • Java Development Kit( JDK,Java 开发工具包) = JRE + 开发工具集(如 javac 编译工具等)

5. Java 关键字

类别 关键字
true false null this super
运算 new instanceof
类型 boolean byte char short int long float double void class interface enum
控制 if else switch case default for do while continue break return
修饰 private protected public abstract static final synchronized strictfp native transient volatile
声明 package import extends implements
异常 try catch finally throws throw
调试 assert
保留 const goto

二、类和对象

1. 类

  • 类是一个模板。类定义了一类事物的状态和行为。
  • 类是对现实世界的抽象,类是一种抽象的复合数据类型。

2. 类和对象的关系

  • 类和对象之间是抽象和具体的关系。类是创建对象的模板,对象是类的具体实例。
  • class 是总称,object 是个体。object 也叫 instance(实例)。

3. 消息

  • 对象提供的服务是由对象的函数来实现的。发送消息实际上就是调用一个对象的函数。

4. 成员

  • 类的成员变量可以是基本类型或数组,也可以是类的对象。
  • 类的成员函数用于处理该类的数据,是用户和对象之间或对象之间的交互接口。

5. 类之间的关系

  • 关联(Association)

    关联指的是类之间的特定对应关系。

    • 一对一关联
    • 一对多关联
    • 多对多关联
  • 依赖(Dependency)

    依赖指的是类之间的调用关系。如果类 A 访问类 B 的成员,或者 A 负责实例化 B,那么可以说类 A 依赖类 B。

  • 聚集(Aggression)

    聚集指的是整体与部分之间的关系。

  • 泛化(Generalization)

    泛化指的是类之间的继承关系。

  • 实现(Realization)

    实现指的是类与接口之间的关系。

三、封装

1. 数据类型

2. 变量

  • 概念

    • 内存中的一个存储区域,该区域拥有自己的名称和类型
    • Java 是强类型语言,变量必须先定义、后使用。
    • 变量是通过变量名访问该区域的。
    • 变量的作用域:一对 { } 之间有效。
  • 定义

    1
    Type name [= initValue];
  • 分类

    • 按照被定义的位置划分
      • 成员变量:函数外部、类内部
      • 局部变量:函数内部或语句块内部
    • 按照所属的数据类型划分
      • 基本数据类型变量(栈)
      • 引用数据类型变量(堆)

3. 类的定义

  • 一个基本 Java 程序的组成

    • 包的声明:指定类所在的位置
    • 类的导入:用到其他位置的类
    • 类的定义:核心部分

    Java 允许在一个源文件中编写多个类,但至多只能有一个使用public修饰。

  • 类的定义格式

    1
    2
    3
    4
    [类修饰符] class Name [extends 父类] [implements 接口列表] {
    [成员变量]
    [成员函数]
    }
    • 类修饰符分为访问控制符和类型说明符

      • 类的访问控制符有两个,一个是public,另一个是默认(没有)

        公共类表示能被其他所有类访问和引用。

        一个源文件只能有一个公共类,其中一般含有 main 函数。

        默认访问属性的类,只能被同一个包中定义的类访问和引用。

      • 类的类型说明符主要有finalabstract

  • 成员变量的定义格式

    1
    [修饰符] Type name [= initValue];
    • 修饰符主要有static public private protected final 默认
  • 成员函数的定义格式

    1
    2
    3
    4
    [修饰符] Type name([形参列表]) [throws 异常列表] {
    [局部变量定义]
    [语句]
    }
    • 修饰符主要有static public private protected final 默认
  • this关键字

    • Java 每个类都默认具有null this super三个域。null表示空,用在定义一个对象但尚未为之开辟内存空间时;thissuper是指代本类对象和父类对象的关键字。
    • this是一个变量,其值是当前对象的引用。使用this可以解决函数中成员变量和局部变量重名的问题。
  • 参数传递

    • 基本类型作为参数传递时,其实只是将值复制了一份传给函数的局部变量。(深拷贝)
    • 引用变量作为参数传递时,其实是将其所保存的堆上的内存地址(引用)复制了一份传给函数的局部变量。(浅拷贝)

    引用变量只保存对象在堆上的内存地址(引用)。引用变量是对象的管理者。引用变量的赋值是指向同一个对象(同一片内存空间)的过程。引用变量用==判断是否相等,实际是判断两个变量是否管理同一个对象。

  • 函数重载

    • 函数重载指的是一个类中可以定义多个同名、但参数不同的函数。
    • 重载的函数必须满足
      • 函数名相同
      • 形参的类型、个数、顺序至少有一项不同
    • 重载的函数的返回类型可以不同。
    • 重载的函数的修饰符可以不同。
    • 调用重载函数时,根据参数的类型和数量决定调用哪个版本。

4. 对象的生成和清除

  • 构造函数(constructor)

    • 特点

      • 与类同名
      • 无返回值
      • 多数情况下需要重载
    • new的使用

      1
      ClassName objectName = new ClassName(args);

      new的作用是:(1)为对象分配堆上的内存空间;(2)引起对象对应构造函数的调用;(3)返回对象的引用。

    • 每个类至少有一个构造函数。如果没有定义构造函数,编译器会自动创建无参数、函数体为空的默认构造函数。如果定义了有参的构造函数,默认构造函数就不会被自动创建,必须手动重载

    • 调用默认构造函数时,成员变量的值被赋予了数据类型的隐含初值。

  • 内存管理(垃圾自动回收机制)

    • Java 虚拟机后台线程负责内存的回收。
    • 垃圾强制回收函数System.gc()Runtime.gc()用来强制立即回收垃圾,但是系统并不会保证立即回收。
    • 判断一个存储存储单元是否是垃圾的依据:该存储单元对应的对象是否仍被程序所用,也即是否有引用指向该对象
  • 匿名对象

    • Student stu = new Student();创建了一个Student类型的名为stu的对象,new Student();则创建了一个Student类型的匿名对象。
    • 匿名对象用完之后就变成垃圾被回收了,因为在其定义的时候并没有栈内存的引用变量指向它。
    • 使用场景
      • 一个对象只需要进行一次函数调用
      • 作为实参传递给一个函数

5. static

  • 静态成员变量(类变量)和静态成员函数(类函数)

    • 静态成员在类加载的时候就已经被装载和分配,它们不依赖于对象而存在。
    • 即使没有创建该类的对象,static的成员变量也会存在,故可以通过类名.静态成员变量名对象名.静态成员变量名访问。
    • 即使没有创建该类的对象,static的成员函数也可以调用,故可以通过类名.静态成员函数名对象名.静态成员函数名调用。
    • 静态成员函数只能访问静态成员,而不能访问非静态成员。非静态成员函数既可以访问静态成员也可以访问非静态成员。
  • 静态代码块

    • 用 { } 包裹起来的语句体称为代码块。其中静态代码块的格式为

      1
      2
      3
      static {
      // statements
      }
    • 静态代码块只能定义在类中,它独立于任何函数,不能定义在函数里面。

    • 静态代码块中声明的变量都是局部变量,只在本块内有效。静态代码块一般用于初始化类的静态成员变量。静态代码块不能访问类的非静态成员。

    • 一个类允许定义多个静态代码块,按照定义的顺序执行

    • new一个对象的语句执行顺序

      第一次new该类对象时

      • 初始化有显式初始化的static成员变量
      • 按顺序执行static语句块
      • 初始化有显式初始化的非static成员变量
      • 按顺序执行非static语句块
      • 调用构造函数

      之后再new该类对象时

      • 初始化有显式初始化的非static成员变量
      • 按照顺序执行非static语句块
      • 调用构造函数

6. 访问控制修饰符

  • 访问权限的四种级别
    • 公开级别:用public修饰,对外公开。
    • 受保护级别:用protected修饰,向子类及同一个包中的类公开。
    • 默认级别:default/没有访问控制修饰符,向同一个包中的类公开。
    • 私有级别:用private修饰,只有类本身可以访问,不对外公开。
修饰符 同一个类 同一个包 子类 整体
private 可见 不可见 不可见 不可见
default/没有 可见 可见 不可见 不可见
protected 可见 可见 可见 不可见
public 可见 可见 可见 可见
  • protected详解

    • protected成员包内可见。

    • protected成员对子类可见。

      • 如果子类和父类在同一个包中,自然可见父类所有protected成员。子类可以直接访问该成员,也可以通过自己的对象访问该成员,还可以通过父类对象访问该成员。
      • 如果子类和父类不在一个包中,仍可见父类所有protected成员。子类可以直接访问该成员,也可通过自己的对象访问该成员,但是不能通过父类对象访问。
      • 同一个父类的不同子类间,相互之间不能通过对方的对象访问从父类那里得到的protected成员。
    • protectedstatic成员对所有子类可见。

      子类可以通过直接访问、自身对象访问、父类对象访问、同父类的其他子类对象访问其父类的static成员。

  • 单例模式

    • 对于单例模式的类,它在内存中只有一个实例存在。对于(1)需要使用一个单独的共享的资源;(2)需要频繁创建和销毁的对象;(3)创建时开销过大但又需要经常用到的对象;(4)工具类对象;(5)频繁访问的数据库或文件对象等,应考虑单例模式。

    • “饿汉式”

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class Singleton {
      /**********************************************************
      ** 多线程安全,没有加锁,执行效率会提高;但在最开始就初始化,浪费内存
      ***********************************************************/
      private static Singleton instance = new Singleton(); // 类加载时创建
      private Singleton() {}
      public static Singleton() getInstance() {
      return instance;
      }
      }
    • “懒汉式”

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      public class Singleton {
      /**********************
      ** 多线程不安全,除非加锁
      ***********************/
      private static Singleton instance = null;
      private Singleton() {}
      public static Singleton getInstance() {
      if (instance == null) instance = new Singleton(); // 第一次使用时创建
      return instance;
      }
      public static Singleton realGetInstance() {
      if (instance == null) {
      synchronized (Singleton.class) {
      if (instance == null) instance = new Singleton();
      }
      }
      return instance;
      }
      }

四、继承

1. Java 的继承

  • Java 不支持类的多继承,但支持接口的多继承。

  • Java 继承的实现

    1
    2
    3
    [修饰符] class SubClass extends SuperClass {
    // body
    }

2. 子类的构造函数

  • 父类的构造函数不能被子类继承。
  • 子类可以通过super();显式调用父类的默认构造,也可以隐式调用;如果要调用父类的有参构造,必须明确地写super(args);。不论何种情况,必须在子类构造函数的第一句调用父类的构造。
  • new出子类对象时,总会按照层次结构从上到下调用所有父类的构造。
  • 总结
    • super用于
      • 引用父类的成员变量super.age = 10;
      • 调用父类的成员函数super.show();
      • 调用父类的构造函数super("Jack", 24);
    • this用于
      • 引用自身的成员变量this.age = 10;
      • 调用自身的成员函数this.show();
      • 调用自身的构造函数this("Tom", 26, Jobs.MANAGER);

3. 继承中的变量隐藏

  • 如果子类新增的成员变量和父类原有的成员变量同名,则子类隐藏了父类的成员变量。
  • 在子类中访问其隐藏的父类成员变量,应写super.变量名

4. 函数的重写(Override)

  • 重写/覆盖就是子类重新书写了父类的某个函数。构成覆盖的条件是
    • 子类的返回值类型必须与父类的相同或是其子类
    • 子类和父类的函数名和参数列表完全相同
    • 访问权限不能更低,只能相同或更高
    • 子类中抛出异常的范围不能更大
  • 重写时的注意事项
    • 私有函数不能继承,故不能覆盖
    • 构造函数不能继承,故不能覆盖
    • 静态函数不存在覆盖
    • final声明的成员函数是最终函数,不能被子类重写

五、多态(Polymorphism)

1. 多态性

  • Java 的引用类型分为两种——编译时类型运行时类型。前者由定义该变量的类型决定,后者由实际赋给给变量的对象的类型决定。
  • 多态性指的是“拥有多种形态”,具体指用相同的名称来表示不同的含义。
  • 多态可以分为静多态动多态

2. 静多态

  • 静多态即在编译时决定调用哪个函数,也称为编译时多态静态联编静绑定

  • 静多态一般指函数重载和隐藏

  • 静多态与是否发生继承没有必然联系。

    注意

    • 函数重载时的返回值类型和访问权限修饰符可以相同也可以不同,不能以此为是否重载的判断条件。
    • 如果一个类中有两个同名、参数列表完全相同的函数,仅返回值类型不同,编译时会出错。(因为编译器不知道这两个函数未来会怎样被调用,例如,一个有返回值的函数其返回值也可以不被使用)

3. 动多态

  • 函数的覆盖指的是子类的成员函数重写了父类的成员函数,重写的目的很大程度是为了实现多态。

  • 动多态即在运行时才能确定调用哪个函数,也称为运行时多态动态联编动绑定

  • 实现动多态必须具备三个条件

    • 继承:必须要存在继承的情况
    • 覆盖:子类必须重写父类的函数
    • 向上造型(Upcasting):必须用父类的引用管理子类的对象,并通过此引用调用那个被重写的函数

    注意

    • 子类的成员变量和父类同名,会出现变量隐藏的情况;子类“重写”父类的privatestatic函数,会出现函数隐藏的情况。用父类引用访问那个被隐藏的变量,只会访问父类的那个变量;用父类引用调用那个被隐藏的函数,只会调用父类的那个函数。

    • 向上造型可以通过父类的引用动态调用子类重写的函数,但不能访问子类新增的成员。此时可以通过强制转型(向下造型)访问子类新增的成员,但有风险存在

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      class SuperClass {
      void f() { System.out.println("SuperClass::f()"); }
      }
      class SubClass extends SuperClass {
      void f() { System.out.println("SubClass::f()"); }
      void g() { System.out.println("SubClass::g()"); }
      }
      public class Test {
      public static void main(String[] args) {
      SuperClass superClass = new SubClass();
      superClass.f(); // 正确,输出 SubClass::f()
      superClass.g(); // 错误
      SubClass subClass = (SubClass)superClass;
      subClass.g(); // 正确,但有风险,输出 SubClass::g()
      }
      }

4. 抽象(Abstract)

  • 关键字abstract可用来修饰类和函数,表示“尚未实现”的含义。

  • 抽象类

    • 抽象类的声明

      1
      2
      3
      public abstract class ClassName {
      // body
      }
    • 一个普通的类也可以用abstract修饰,只是没有含义。但是,含有抽象函数的类,必须用abstract修饰。

  • 抽象函数

    1
    public abstract void funcName();	
    • 抽象函数没有函数体。
    • 抽象函数必须在抽象类里。
    • 抽象函数必须在子类中被实现,除非子类是抽象类。

    注意

    无函数体和函数体为空是两个不同的概念。

  • 更多的细节

    • 抽象类一般用在父类对象没有机会被访问调用、父类的函数无法明确实现的情况下。例如飞机和民航客机、民用运输机、战斗机等的关系——飞机具有一些公共属性,但飞机只是一个抽象的概念,其实现必须依赖更具体的子类。对于飞机制造者,让他“造一架飞机”,他也不清楚要造什么样的飞机;对于看到某种飞机的普通人,他会说“这是一架飞机”。这体现了抽象和具体并存的重要性。
    • 不能实例化抽象类。但是,可以通过抽象类的引用管理其子类,实现动态联编。
    • 含有抽象函数的类必须是抽象类。
      • 如果一个类继承自某个抽象父类,而没有实现或只是部分实现了父类的抽象函数,则这个子类也得声明为抽象类。
      • 当一个类声明实现了某个接口,但并没有实现或只是部分实现了该接口的函数,则这个类必须声明为抽象类。
      • 从抽象到具体是一个逐步实现的过程,一个抽象类中也可以书写成员变量和函数,包括静态的类变量和类函数。
    • 抽象函数不能被privatefinalstatic修饰。因为这类函数均无法被重写,即不能被子类实现

六、接口(Interface)

1. 接口的概念

  • 概念性的接口,指的是一个类的所有能被外界访问的函数
  • interface关键字定义的接口类型。Java 接口是一系列函数特征的集合,但不具有函数的实现。这些函数将在不同的地方被不同的类实现,于是这些类将具有不同的功能。接口是类的行为的规范。

2. 接口的实现

  • 接口是一种特殊的抽象类,是一种复合数据类型,定义一个接口的格式为

    1
    2
    3
    4
    [public] interface InterfaceName [extends 父接口] {
    [常量]
    [函数]
    }
  • 接口中所有函数都默认、也必须是public abstract的,并且没有函数体,以分号结尾。

  • 接口中所有变量都默认、也必须是public static final的。

  • 接口没有构造函数。

  • 接口中的函数可以用 Java 书写,也可以用其他语言书写。对于后者,函数需要用native修饰。

    注意

    • 一个类在实现接口的函数时,应采用完全相同的函数头,否则只是重载。
    • 接口的抽象函数访问权限是public,类在实现时,必须显式使用public修饰符。

3. 接口回调

1
2
3
4
// InterfaceEg.java
public interface InterfaceEg {
void f();
}
1
2
3
4
5
6
// MyClass.java
public class MyClass implements InterfaceEg {
public void f() {
// do something
}
}
1
2
3
4
5
6
7
// Test.java
public class Test {
public static void main(String[] args) {
InterfaceEg interfaceEg = new MyClass();
interfaceEg.f();
}
}
  • 用一个接口类型的引用去管理实现了该接口的类的对象,则可以通过这个引用去调用接口中的那些函数。
  • 接口也可以作为函数形参,但实际传入的参数是实现了该接口的类的对象的引用,从而在函数中实现接口回调。

4. 接口的继承

  • 接口可以像普通类那样用extends继承父接口,从而得到父接口中的所有成员。

  • 接口中的函数在某一个实现该接口的类中,只能被书写一次。

    1
    2
    3
    4
    // In1.java
    public interface In1 {
    void f();
    }
    1
    2
    3
    4
    // In2.java
    public interface In2 extends In1 {
    void f();
    }
    1
    2
    3
    4
    // AnotherIn.java
    public interface AnotherIn {
    void f();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // MyClass.java
    public class MyClass implements In1, In2, AnotherIn {
    @Override
    public void f() { System.out.println("hello"); }

    public static void main(String[] args) {
    MyClass test = new MyClass();
    test.f(); // OK,输出 hello

    In1 in1 = test;
    in1.f(); // OK,输出 hello

    In2 in2 = test;
    in2.f(); // OK,输出 hello

    AnotherIn in = test;
    in.f(); // OK,输出 hello
    }
    }
  • 不同接口(包括父子接口)中同名的变量相互隐藏,使用时应用接口名.变量名的形式明确指出。

    • Case 1

      1
      2
      3
      4
      5
      6
      // In.java
      public interface In {
      int a = 1;
      int b = 2;
      int c = 3;
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // MyClass.java
      public class MyClass implements In {
      int b = 22;
      public static int c = 33;
      void f() {
      System.out.println(a); // OK,但不推荐,输出 1
      System.out.println(In.a); // 正确的
      System.out.println(b); // 输出 22
      System.out.println(In.b); // 输出 2
      System.out.println(c); // 输出 33
      System.out.println(In.c); // 输出 3
      }
      }
    • Case 2

      1
      2
      3
      4
      5
      // In1.java
      public interface In1 {
      int a = 1;
      int b = 11;
      }
      1
      2
      3
      4
      5
      // In2.java
      public interface In2 {
      int a = 2;
      int b = 22;
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // MyClass.java
      public class MyClass implements In1, In2 {
      int b = 999;
      void f() {
      System.out.println(a); // 错误,存在冲突
      System.out.println(In1.a); // OK,输出 1
      System.out.println(In2.a); // OK,输出 2

      System.out.println(b); // OK,输出 999
      System.out.println(In1.b); // OK,输出 11
      System.out.println(In2.b); // OK,输出 22
      }
      }

5. instanceof

  • 向上造型总是安全的。这一手段可以实现动多态代价是无法通过父类的引用访问子类新增的成员

  • 向下造型是有风险的。如果不可以转型,会发生运行时错误,这是编译时刻无法确定的。

  • instanceof是一个二元运算符,用于判断一个引用类型所管理的对象是不是一个类的实例。其左侧的算子是一个引用类型变量(可以是null),右侧算子是一个类名或接口名。此运算符的结果是boolean类型的truefalse

  • instanceof的细节

    • 表达式A instanceof B的值为true的情况包括

      • 引用A管理类B的对象
      • BA的直接或间接父类
      • BA实现的接口
    • 强制转型,有可能发生编译错误,也有可能抛出ClassCastException

      • 编译错误的可能情形

        1
        2
        3
        // 毫不相关的类之间强制转型
        Random random = new Random();
        String s = (String)random;
        1
        2
        3
        // 用接口管理未实现该接口的 final 类
        Integer integer = new Integer(10);
        List list = (List)integer;
        1
        2
        3
        // 用一个 final 类管理其未实现的接口
        List list = new ArrayList<Integer>();
        String s = (String)list;
      • 抛出异常的可能情形

        1
        2
        3
        // 用子类管理父类(哪怕子类是 final 的)
        Vector<Integer> vector = new Vector<Integer>();
        Stack<Integer> stack = (Stack<Integer>)vector;
        1
        2
        3
        // 用接口管理未实现该接口的普通类
        Random random = new Random();
        List list = (List)random;
        1
        2
        3
        // 用未实现某个接口的普通类管理该接口
        List list = new ArrayList<String>();
        Random random = (Random)list;
        1
        2
        3
        // 用一个接口管理另一个接口
        List list = new ArrayList<String>();
        Runnable runnable = (Runnable)list;
    • 当强制转型不能通过编译时,instanceof也不能通过编译;当强制转型会抛出异常时,instanceof返回false

七、类的进阶设计

(一) OOP 的若干基本原则

1. UML 图

  • 统一建模语言(UML,Unified Modeling Language),又称标准建模语言,是用来对软件密集系统进行可视化建模的一种语言。

2. 面向抽象原则:面向接口的编程

  • 含义

    所有具有相同功能的组件都应该实现某个接口,或从某个抽象类继承。

    客户代码只应该和接口通讯。

    • 降低代码耦合性,提升代码可维护性
    • 修改一方不会影响另一方
    • 提升代码复用性
  • 设计一个类时,应面向抽象类或接口,即类中的重要数据应该是抽象类或接口定义的变量,而不是该类本身定义的变量;类中的重要函数应该是抽象类或接口声明的函数,而不是该类本身定义的函数。

    Example

    • 传统做法

      1
      2
      3
      4
      5
      6
      // Circle.java
      public class Circle {
      double r;
      public Circle(double r) { if (r > 0) this.r = r > 0 ? r : 0.0; }
      public double getArea() { return Math.PI * r * r; }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // Pillar.java
      public class Pillar {
      Circle bottom;
      double height;
      public Pillar(Circle circ, double h) {
      this.bottom = circ;
      this.height = h > 0 ? h : 0.0;
      }
      public double getVolume() { return bottom.getArea * height; }
      }
    • 面向接口的做法

      1
      2
      3
      4
      // AreaGettable.java
      public interface AreaGettable {
      double getArea();
      }
      1
      2
      3
      4
      5
      6
      // Circle.java
      public class Circle implements AreaGettable {
      double r;
      public Circle(double r) { if (r > 0) this.r = r > 0 ? r : 0.0; }
      public double getArea() { return Math.PI * r * r; }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // Pillar.java
      public class Pillar {
      AreaGettable bottom;
      double height;
      public Pillar(AreaGettable bottom, double h) {
      this.bottom = bottom;
      this.height = h > 0 ? h : 0.0;
      }
      public double getVolume() { return bottom.getArea * height; }
      }

3. 优先使用组合少用继承原则

  • 函数复用的两种常用技术:类继承和对象组合。
  • 继承的缺点
    • 继承破坏了封装。通过继承复用也称“白盒”复用,父类的细节对子类是可见的。
    • 子类和父类是强耦合关系,父类的改变必然引起子类的改变。
    • 子类从父类继承来的函数在编译时就确定了,无法在运行期间改变从父类继承的函数的行为
  • 对象组合的优点
    • 新的更复杂的功能可以通过组合对象来获得。
    • 对象组合要求对象定义良好的接口,这种复用方式也被称为“黑箱”复用,即被组合的对象的内部细节是不可见的。
    • 对象与其所包含的对象是弱耦合关系,修改一个类所包含的类的代码,不必修改此类本身的代码。
    • 当前对象可以在运行时刻动态指定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Animal.java
public abstract class Animal {
String getMessage();
}
class Animal1 extends Animal {
public String getMessage() { System.out.println("I'm tiger."); }
}
class Animal3 extends Animal {
public String getMessage() { System.out.println("I'm panda."); }
}
class Animal4 extends Animal {
public String getMessage() { System.out.println("I'm lion."); }
}
class Animal8 extends Animal {
public String getMessage() { System.out.println("I'm giraffe."); }
}
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
// Zoo.java
public class Zoo {
Animal animal = null;
public void setAnimal(Animal animal) { this.animal = animal; }
public void show() {
if (animal == null) System.out.println("No animal in the zoo.");
else System.out.println("The current animal says: " + animal.getMessage());
}
public static void main(String[] args) {
Zoo zoo = new Zoo();
int idx = 1;
while (true) {
try {
zoo.show();
Thread.sleep(2000);
Class<?> cs = Class.forName("Animal" + i);
Animal a = (Animal)cs.getDeclaredConstructor().newInstance();
// 换动物,出错抛异常
zoo.setAnimal(a);
idx++;
} catch(Exception e) {
idx++;
}
if (idx > 10) idx = 1; // 最多十种动物
}
}
}
/*
运行 main 后,可继续编写其他 Animal 的子类。编译后,程序输出将发生变化。
*/

4. 更多的原则

软件设计七大原则

原则 内容 目的
开-闭原则 对扩展开放,对修改关闭 降低维护带来的新风险
依赖倒置原则 抽象不依赖具体,面向接口编程 便于代码结构层次的升级和拓展
单一职责原则 一个类只干一件事 便于组织和理解,提升代码可读性
接口隔离原则 一个接口只规定一件事 实现高内聚、低耦合
迪米特法则 一个类应保持对其他类最少的了解 降低耦合,避免代码臃肿
里氏替换原则 减少子类对父类的重写 减少程序出错,防止继承泛滥
合成复用原则 多用组合,少用继承 降低代码耦合
  • 开-闭原则

    • 设计应当对扩展开放、对修改关闭,即在增加一个新的模块时,不需要修改现有的模块。

      上例中计算柱体体积的程序,由于AreaGettable接口的存在,系统中对于新的柱体底面的添加是开放的,对体积计算是关闭的。

  • 高内聚-低耦合原则

    • 高内聚:内聚描述一个模块自身的特性。模块内部的元素关联性越强,则内聚程度越高、模块单一性越强。

      例如,系统存在 A 和 B 两个模块进行交互,如果修改 A 不影响 B 的工作,比如 B 只调用了 A 的 getter 和 setter 等,那么可以认为 A 模块具有足够的内聚。

    • 低耦合:耦合描述模块之间的依赖程度。模块之间的依赖程度越低,则一个模块的改动对其他模块的影响程度越小。

      例如,如果模块 A 直接修改了模块 B 的数据,则视为高耦合;如果 A 只是通过发消息与 B 交互,则视为低耦合。

    关于里氏替换原则的例子

    计算不同鸟类飞行一段距离所需时间,特别,几维鸟(Kiwi)没有翅膀,所以不会飞。

    • 错误案例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // Bird.java
      public class Bird {
      protected String type;
      protected double speed;
      protected void setType(String type) { this.type = type; }
      public void setSpeed(double speed) { this.speed = speed; }
      public double getFlyingTime(double distance) { return distance / speed; }
      }
      class Swallow extends Bird {
      public Swallow() { setType("Swallow"); }
      }
      class Kiwi extends Bird {
      public Kiwi() { setType("Kiwi"); }
      public void setSpeed(double speed) { this.speed = 0; }
      // 重写以保证几维鸟飞行速度为 0
      // 对于几维鸟,调用 getFlyingTime 将出现除零错误
      }
    • 正确案例

      1
      2
      3
      4
      5
      // Flyable.java
      public interface Flyable {
      void setSpeed(double speed);
      double getFlyingTime(double distance);
      }
      1
      2
      3
      4
      5
      // Animal.java
      public abstract class Animal {
      protected String type;
      protected void setType(String type) { this.type = type; }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      // Bird.java
      public class Bird extends Animal implements Flyable {
      protected double speed; // 子类只添加成员,而不修改父类
      public void setSpeed(double speed) { this.speed = speed; }
      public double getFlyingTime(double distance) { return distance / speed; }
      }
      class Swallow extends Bird {
      public Swallow() { setType("Swallow"); }
      }
      1
      2
      3
      4
      5
      // Kiwi.java
      public class Kiwi extends Animal { // 更合理的关系
      public Kiwi() { setType("Kiwi"); }
      public void fly() { System.out.println("I can't fly."); }
      }

(二) 几个重要的设计模式

设计模式(pattern)是针对某一类问题的最佳解决方案,程序设计过程中,应基于业务来选择设计模式。技术永远为业务服务,技术只是满足业务需要的一个工具。

设计模式的具体分类

  • 创建型模式:涉及对象的实例化问题,避免用户直接使用new创建对象。

    • 单例模式 Singleton

      某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例。可拓展为有限多例模式。

    • 原型模式 Prototype

      将一个对象作为原型,通过对其复制而克隆出多个和原型类似的对象。

    • 工厂模式 Factory

      定义一个用于创建产品的接口,由其实现类决定生产什么产品。

    • 抽象工厂模式 AbstractFactory

      提供一个用于创建产品族的接口,其每个实现类可以生产一系列相关产品。

    • 建造者模式 Builder

      将一个复杂对象分解成若干相对简单的部分,根据需要分别创建,最后组装。

  • 结构型模式:涉及类和对象的结构组织问题。

    • 代理模式 Proxy

      客户通过代理间接地访问对象。

    • 适配器模式 Adapter

      将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的类能一起工作。

    • 桥接模式 Bridge

      用组合代替继承,使得抽象与实现分离,可以独立变化。

    • 装饰模式 Decorator

      动态地给对象增加职责,添加额外的功能。

    • 外观模式 Facade

      为多个复杂的子系统提供一个一致的接口,使之更容易被访问。

    • 享元模式 Flyweight

      运用共享技术,实现大量细粒度对象的复用。

    • 组合模式 Composite

      将对象组合成树状层次结构,使客户对单个对象和组合对象的访问具有一致性。

  • 行为型模式:涉及对象间的交互通信以及为对象分配职责的问题。

    • 模板方法模式 TemplateMethod

      定义一个操作中的算法骨架,将其中的一些步骤延迟到子类中实现。

    • 策略模式 Strategy

      定义一系列算法并封装。

    • 命令模式 Command

      将一个请求封装为一个对象,使请求的发出和执行分离。

    • 责任链模式 ChainOfResponsibility

      把请求从链中的一个对象传到下一个对象,直到请求被响应。

    • 状态模式 State

      允许一个对象在内部状态变化时改变其行为能力。

    • 观察者模式 Observer

      把一个对象的改变通知给其他对象,从而影响其他对象的行为。

    • 中介者模式 Mediator

      定义一个中介对象来简化原有对象间的交互,降低耦合度。

    • 迭代器模式 Iterator

      提供一种顺序访问聚合对象的方法,不暴露其内部细节。

    • 访问者模式 Visitor

      为集合中的每个元素提供多种访问方式。

    • 备忘录模式 Memento

      在不破坏封装的前提下保存一个对象的内部状态,以便后续恢复它。

    • 解释器模式 Interpreter

      提供定义语言文法、解释句子的解释器。

1. 原型模式

当某些类的初始化需要消耗非常多资源、new一个对象需要非常繁琐的数据准备和访问权限、一个对象需要在多个场合被访问且访问者可能修改其值时,可以考虑原型模式。

  • 原型模式是内存中的二进制流的拷贝,比new性能高得多。
  • 由原型生成对象避免了构造函数的约束,优缺点共存。

使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Test.java
class MyClass implements Cloneable {
MyClass() { System.out.println("Create prototype success."); }

@Override
public Object clone() throws CloneNotSupportedException {
Object o = super.clone();
System.out.println("Clone prototype success.");
return (MyClass)o;
}
}

public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
MyClass c1 = new MyClass();
MyClass c2 = (MyClass)c1.clone();
// test
System.out.println(c1 == c2);
}
}

2. 策略模式

当程序的主要类不希望暴露复杂而具体的数据结构和算法实现时,可以考虑策略模式。

  • 上下文和具体策略是松耦合关系
  • 策略模式遵循“开-闭原则”,增加新的策略时不需要改动原有代码。
  • 策略模式采用组合结构,具有可扩展性和复用性
  • 策略:一个接口

    策略接口定义了若干个算法标识,即声明了若干抽象函数。

  • 上下文:依赖于策略接口的类

    上下文包含有用策略声明的变量。上下文提供一个函数,该函数委托策略变量调用具体策略所实现的策略接口中的函数。

  • 具体策略:实现策略接口的类

    具体策略类中实现了若干具体的算法。

举例应用:多个裁判给选手打分,用不同的方案计算最终得分。

1
2
3
4
// Strategy.java 策略
public interface Strategy {
double calcScore(double[] scores);
}
1
2
3
4
5
6
7
8
9
// AverageScore.java 上下文
public class AverageScore {
private Strategy s;
public void setStrategy(Strategy s) { this.s = s; }
public double getScore(double[] scores) {
if (s == null) return -1;
else return s.calcScore(scores);
}
}
1
2
3
4
5
6
7
8
// StrategyA.java  具体策略 A
public class StrategyA implements Strategy {
public double calcScore(double[] scores) {
double sum = 0;
for (double score : scores) sum += score;
return sum / scores.length;
}
}
1
2
3
4
5
6
7
8
9
10
11
// StrategyB.java  具体策略 B
import java.util.Arrays;
public class StrategyB implements Strategy {
public double calcScore(double[] scores) {
if (scores.length <= 2) return new StrategyA().calcScore(scores);
double sum = 0;
Arrays.sort(scores);
for (int i = 1; i < scores.length - 1; i++) sum += scores[i];
return sum / (scores.length - 2);
}
}
1
2
3
4
5
6
7
8
9
10
11
// Test.java
public class Test {
public static void main(String[] args) {
double[] scores = {92.5, 93, 89, 90.5, 96, 88.5};
AverageScore as = new AverageScore();
as.setStrategy(new StrategyA());
System.out.println(as.getScore(scores));
as.setStrategy(new StrategyB());
System.out.println(as.getScore(scores));
}
}

3. 访问者模式

当需要对集合中的元素进行很多不同且不相关的操作时,可以考虑访问者模式。

  • 在不改变集合元素的类的情况下,可以添加新的施加于该元素的操作。
  • 具有一定的可扩展性。
  • 此模式包含四个角色
    • 抽象元素:被访问的元素,该元素必须提供允许访问者访问它的接口。
    • 具体元素:被访问的对象,即抽象元素的实现类。
    • 抽象访问者:规定访问时的操作。
    • 具体访问者:实现访问时的操作。

举例应用:抄录一个电表,分别按照家用电标准(6000 度以下 0.6 元/度;以上 1.05 元/度)和企业用电标准(15000 度以下 1.52 元/度;以上 2.78 元/度)计费。

1
2
3
4
5
6
7
8
9
10
11
12
// Visitor.java 抽象访问者
public interface Visitor {
int HOME_STANDARD_AMOUNT = 6000;
double HOME_STANDARD_LEVEL_1 = 0.6;
double HOME_STANDARD_LEVEL_2 = 1.05;

int INDUSTRY_STANDARD_AMOUNT = 15000;
double INDUSTRY_STANDARD_LEVEL_1 = 1.52;
double INDUSTRY_STANDARD_LEVEL_2 = 2.78;

public double visit(Element e);
}
1
2
3
4
5
6
7
8
9
10
11
12
// HomeVisitor.java 具体访问者 1
public class HomeVisitor implements Visitor {
public double visit(Element e) { // 一种访问操作
double fee = 0;
double amt = e.showAmount();
if (amt <= HOME_STANDARD_AMOUNT) fee = amt * HOME_STANDARD_LEVEL_1;
else
fee = HOME_STANDARD_AMOUNT * HOME_STANDARD_LEVEL_1
+ (amt - HOME_STANDARD_AMOUNT) * HOME_STANDARD_LEVEL_2;
return fee;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// IndustryVisitor.java 具体访问者 2
public class IndustryVisitor implements Visitor {
public double visit(Element e) { // 另一种访问操作
double fee = 0;
double amt = e.showAmount();
if (amt <= INDUSTRY_STANDARD_AMOUNT)
fee = amt * INDUSTRY_STANDARD_LEVEL_1;
else
fee = INDUSTRY_STANDARD_AMOUNT * INDUSTRY_STANDARD_LEVEL_1
+ (amt - INDUSTRY_STANDARD_AMOUNT) * INDUSTRY_STANDARD_LEVEL_2;
return fee;
}
}
1
2
3
4
5
6
// Element.java 抽象元素
public interface Element {
void accept(Visitor v);
double showAmount();
void setAmount(double amt);
}
1
2
3
4
5
6
7
8
9
10
// Ammeter.java 具体元素
public class Ammeter implements Element {
private double amount;
public void accept(Visitor v) { // 接收访问者
double fee = v.visit(this); // 让访问者访问自己
System.out.printf("Electric fee: %.2f\n", fee);
}
public void setAmount(double amt) { amount = amt; }
public double showAmount() { return amount; }
}
1
2
3
4
5
6
7
8
9
10
11
// Test.java
public class Test {
public static void main(String[] args) {
Ammeter a = new Ammeter(); a.setAmount(6723);

Visitor v = new HomeVisitor();
a.accept(v);
v = new IndustryVisitor();
a.accept(v);
}
}

4. 装饰模式

当希望动态地增强某个类的某个对象的功能,同时不影响该类其他对象的时候,可以考虑装饰模式。

  • 被装饰者和装饰者是松耦合关系
  • 装饰者不知道自己要装饰哪个类的哪个对象,相比于用继承生成子类更加灵活
  • 装饰模式使得程序的弹性和可扩展性更优,不影响系统的扩展和维护。
  • 此模式包含四个角色
    • 抽象组件:声明了需要装饰的函数,即“被装饰者”的抽象。
    • 具体组件:实际装饰的对象,是抽象组建的实现类。
    • 装饰:抽象组建的子类,用于进行装饰。
    • 具体装饰:装饰的非抽象子类,用于定义装饰的内容。

举例应用:给 C919 加装油箱,使其航程从 5500 km 提升至 8000 km。

1
2
3
4
// Flyer.java
public interface Flyer {
int fly(); // 需要装饰的函数
}
1
2
3
4
5
// C919.java
public class C919 implements Flyer {
public final int AIR_RANGE = 5500;
public int fly() { return AIR_RANGE; } // 装饰前的函数
}
1
2
3
4
5
6
// Decorator.java
public abstract class Decorator implements Flyer {
protected Flyer f; // 抽象的装饰包办被装饰对象的引用
public Decorator(Flyer f) { this.f = f; }
public abstract int fly();
}
1
2
3
4
5
6
7
8
9
// FuelTankDecorator.java
public class FuelTankDecorator extends Decorator {
public final int AIR_RANGE_ADDED = 2500;
public FuelTankDecorator(Flyer f) { super(f); }
public int fly() { // 用于装饰的函数
return f.fly() + flyWithMoreFuel(); // 进行具体的装饰过程
}
private int flyWithMoreFuel() { return AIR_RANGE_ADDED; }
}
1
2
3
4
5
6
7
8
9
10
11
// Test.java
public class Test {
public static void main(String[] args) {
Flyer c919 = new C919();
System.out.println
("C919 can fly " + c919.fly() + " km without added fuel.");
c919 = new FuelTankDecorator(c919); // 进行装饰
System.out.println
("C919 can fly " + c919.fly() + " km with added fuel.");
}
}

5. 适配器模式

当想要使用一个已经存在的类,但是该类不符合现有接口规范而无法直接访问,可以通过适配器间接访问;当设计出一个新类并希望它将来可以在多种场合下被重用,可以为之创建一系列适配器。

  • 将目标与被适配者解耦,无需重构被适配者。
  • 将业务封装在适配器中,提高了透明度和复用性
  • 遵循“开闭原则”,灵活性和可扩展性好。
  • 此模式包含三种角色
    • 目标:用户想使用的接口。
    • 被适配者:已经存在的、需要适配的接口或抽象类。
    • 适配器:此类实现了目标,并包含被适配者的引用,用于进行适配。
  • 适配器的适配程度
    • 完全适配:目标中的函数数目等于被适配者中的函数数目。
    • 不完全适配:目标中的函数数目小于被适配者中的函数数目。
    • 剩余适配:目标中的函数数目大于被适配者中的函数数目,这种情况下用户必须给出多出的那些函数的实现。

举例应用:用户家里有台使用交流电的洗衣机,现在用户买了一台只能用直流电的录音机,需要进行转化。

1
2
3
4
// DC.java 目标
public interface DC {
String giveDC();
}
1
2
3
4
// AC.java 被适配者
public interface AC {
String giveAC();
}
1
2
3
4
5
6
7
8
9
10
11
12
// Adapter.java 适配器
public class Adapter implements DC {
private AC ac;
Adapter(AC ac) { this.ac = ac; }
public String giveDC() {
String in = ac.giveAC();
StringBuffer out = new StringBuffer(in);
for (int i = 0; i < out.length(); i++)
if (out.charAt(i) == '0') out.setCharAt(i, '1');
return new String(out);
}
}
1
2
3
4
// PowerProvider.java 被适配者的具体实现类
public class PowerProvider implements AC {
public String giveAC() { return "1010101010101010..."; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Application.java
class WashingMachine {
public void work(AC ac) {
System.out.println("Turn on the washing machine " + ac.giveAC());
System.out.println("Start washing...");
}
}
class Recorder {
public void work(DC dc) {
System.out.println("Turn on the recorder " + dc.giveDC());
System.out.println("Start recording...");
}
}
public class Application {
public static void main(String[] args) {
AC ac = new PowerProvider();
WashingMachine wm = new WashingMachine();
wm.work(ac);

DC dc = new Adapter(ac);
Recorder r = new Recorder();
r.work(dc);
}
}

6. 责任链模式

当有多个类用于处理用户请求并希望动态制定处理请求的逻辑时,可以考虑责任链模式。

  • 将命令发出者和执行者解耦
  • 可灵活地为不同处理者分配职责。
  • 此模式包含两种角色
    • 处理者
    • 具体处理者

举例应用:模拟一个学生请假的情形。假设天数小于等于 2 天,班主任可以批准;小于等于 7 天,系主任可以批准;小于等于 10 天,院长可以批准;更多的天数原则上不再批准。

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
// Test.java
abstract class Handler { // 抽象处理者
private Handler next;
public void setNext(Handler next) { this.next = next; }
public Handler getNext() { return next; }
public abstract void handle(int days);
}
class ClassAdviser extends Handler { // 处理者 1
public void handle(int days) {
if (days <= 2)
System.out.println("The class adviser allows you "
+ days + " days.");
else if (getNext() != null)
getNext().handle(days);
else
System.out.println("Request not allowed.");
}
}
class DepartmentHead extends Handler { // 处理者 2
public void handle(int days) {
if (days <= 7)
System.out.println("The department head allows you "
+ days + " days.");
else if (getNext() != null)
getNext().handle(days);
else
System.out.println("Request not allowed.");
}
}
class Dean extends Handler { // 处理者 3
public void handle(int days) {
if (days <= 10)
System.out.println("The dean allows you "
+ days + " days.");
else if (getNext() != null)
getNext().handle(days);
else
System.out.println("Request not allowed.");
}
}
public class Test {
public static void main(String[] args) {
// ----------- 组装责任链 -----------
Handler h1 = new ClassAdviser();
Handler h2 = new DepartmentHead();
Handler h3 = new Dean();
h1.setNext(h2);
h2.setNext(h3);
// --------------------------------

h1.handle(8);
}
}

7. 外观模式(门面模式)

当需要为多个子系统提供一个一致的接口便于外界访问时,可以考虑外观模式。

外观模式是迪米特法则的典型应用,其优点是

  • 对客户屏蔽了子系统组件,降低耦合度,并使得客户的使用更加容易。
  • 编译一个子系统不影响其他子系统,也不影响外观类,降低了大型软件系统的编译依赖性,简化了系统在不同平台间的移植过程

外观模式大量使用组合,其缺点是

  • 对子系统的修改或增加新的子系统可能需要修改外观类或客户端,违背了“开-闭原则”
  • 不能很好地限制用户直接使用子系统类,易带来未知风险
  • 此模式包含三种角色
    • 外观类:提供给用户的统一接口
    • 子系统:一系列具体的实现任务的类
    • 客户端:外观的调用者

举例应用:家庭影院的模拟。家庭影院包含播放器、音响、屏幕、投影仪四部分。用户希望通过遥控器一键启动、暂停、结束等,而非逐个部件地操作。

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
// HomeTheater.java
class Player { // 子系统 1:播放器
private static Player ins = new Player();
private Player() {}
public static Player getInstance() { return ins; }
public void on() { System.out.println("播放器打开了"); }
public void off() { System.out.println("播放器关闭了"); }
public void play() { System.out.println("播放器正在播放"); }
public void pause() { System.out.println("播放器暂停"); }
}
class Stereo { // 子系统 2:音响
private static Stereo ins = new Stereo();
private Stereo() {}
public static Stereo getInstance() { return ins; }
public void on() { System.out.println("音响打开了"); }
public void off() { System.out.println("音响关闭了"); }
public void play() { System.out.println("音响正在播放"); }
public void pause() { System.out.println("音响暂停"); }
public void adjust() { System.out.println("调节音量中"); }
}
class Screen { // 子系统 3:屏幕
private static Screen ins = new Screen();
private Screen() {}
public static Screen getInstance() { return ins; }
public void up() { System.out.println("屏幕上升"); }
public void down() { System.out.println("屏幕下降"); }
}
class Projector { // 子系统 4:投影仪
private static Projector ins = new Projector();
private Projector() {}
public static Projector getInstance() { return ins; }
public void on() { System.out.println("投影仪打开了"); }
public void off() { System.out.println("投影仪关闭了"); }
}

public class HomeTheater { // 外观类:家庭影院
private Player player = Player.getInstance();
private Stereo stereo = Stereo.getInstance();
private Screen screen = Screen.getInstance();
private Projector projector = Projector.getInstance();
// 提供服务
public void ready() {
System.out.println("=== READY ===");
screen.down(); player.on(); stereo.on(); projector.on(); stereo.adjust();
}
public void play() {
System.out.println("=== PLAY ===");
player.play(); stereo.play();
}
public void pause() {
System.out.println("=== PAUSE ===");
player.pause(); stereo.pause();
}
public void end() {
System.out.println("=== END ===");
player.off(); stereo.off(); projector.off(); screen.up();
}
}
1
2
3
4
5
6
7
// Client.java
public class Client {
public static void main(String[] args) {
HomeTheater ht = new HomeTheater();
ht.ready(); ht.play(); ht.pause(); ht.end();
}
}

8. 工厂模式

  • 简单工厂:为产品创建工厂类,客户调用工厂的函数获得产品。

    举例应用:模拟女娲造人。传入参数 M 或 m 创造男人,传入 W 或 w 创造女人。其他种类暂时无法创造。

    1
    2
    3
    4
    //Person.java
    public interface Person {
    void create();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // Nvwa.java
    class Man implements Person {
    Man() { create(); }
    public void create() { System.out.println("Create a man."); }
    }
    class Woman implements Person {
    Woman() { create(); }
    public void create() { System.out.println("Create a woman."); }
    }
    public class Nvwa {
    public static Person create(String p) {
    if (p.equalsIgnoreCase("M")) return new Man();
    else if (p.equalsIgnoreCase("W")) return new Woman();
    else {
    System.out.println("Sorry, I can't create " + p + ".");
    return null;
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    // SimpleFactoryTest.java
    public class SimpleFactoryTest {
    public static void main(String[] args) {
    Person p1 = Nvwa.create("M");
    Person p2 = Nvwa.create("W");
    Person p3 = Nvwa.create("abc");
    }
    }
  • 工厂方法
    • 此模式包含四种角色
      • 抽象产品
      • 具体产品
      • 抽象生产者
      • 具体生产者

    举例应用:生产黑色和红色的笔芯。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // PenCore.java
    public abstract class PenCore {
    protected String color;
    public void writeDown(String s) {
    System.out.println("Write down " + color + " words: " + s);
    }
    }

    class BlackPenCore extends PenCore {
    public BlackPenCore() { color = "black"; }
    }
    class RedPenCore extends PenCore {
    public RedPenCore() { color = "red"; }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // PenCoreCreator.java
    public abstract class PenCoreCreator {
    public abstract PenCore getPenCore();
    }

    class BlackPenCoreCreator extends PenCoreCreator {
    public PenCore getPenCore() { return new BlackPenCore(); }
    }
    class RedPenCoreCreator extends PenCoreCreator {
    public PenCore getPenCore() { return new RedPenCore(); }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // FactoryMethodTest.java
    class BallPen {
    PenCore pc;
    public void setPenCore(PenCore pc) { this.pc = pc; }
    public void write(String s) { pc.writeDown(s); }
    }

    public class FactoryMethodTest {
    public static void main(String[] args) {
    PenCoreCreator pcc;
    BallPen pen = new BallPen();

    pcc = new BlackPenCoreCreator();
    pen.setPenCore(pcc.getPenCore());
    pen.write("Hello");
    pcc = new RedPenCoreCreator();
    pen.setPenCore(pcc.getPenCore());
    pen.write("World");
    }
    }
  • 抽象工厂:更高层次抽象的工厂方法
    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
    /*
    上例可以用工厂方法解决,其特性是一个工厂、多种产品。可以为之赋予如下实际含义:
    ---------------------------
    [品牌] [产品]
    晨光 圆珠笔(黑/红)
    ---------------------------
    对应的类的结构为
    [abstract class] PenCore <-实现- [class] BlackPenCore & RedPenCore
    [abstract class] PenCoreCreator <-实现- [class] BPCC & RPCC
    */
    /*
    考虑另一种情形。假设有多个工厂(品牌),生产多种产品,则需要对工厂进一步抽象。例如:
    ---------------------------------------------
    [品牌] [产品1] [产品2]
    晨光 圆珠笔(黑/红) 笔记本(横线/网格)
    Muji 圆珠笔(黑/红) 笔记本(横线/网格)
    ---------------------------------------------
    对应的结构应设置为
    [abstract class] AbstractCreator
    - createBallPen(color)
    - createNotebook(type)
    [class] CreatorNamedChenguang extends AbstractCreator
    [class] CreatorNamedMuji extends AbstractCreator

    [abstract class] BallPen
    - color
    - writeDown()
    [class] BallPenOfChenguang extends BallPen
    [class] BallPenOfMuji extends BallPen

    [abstract class] Notebook
    - type
    - beWritten()
    [class] NotebookOfChenguang extends Notebook
    [class] NotebookOfMuji extends Notebook
    */

9. 观察者模式(监听模式)

当程序中的对象需要通知其他对象,希望建立一种链式触发机制时,可以考虑观察者模式。

此模式的优点

  • 一个对象的改变将导致其他一个或多个对象的改变,但是这个发生改变的对象无需关心其他这些对象的细节,也不用知道他们是谁。
  • 观察者和被观察者仅在抽象层次上耦合

此模式的缺点

  • 如果一个被观察者有很多直接和间接的观察者,则全部通知需要花费很多时间
  • 如果被观察者和观察者存在循环依赖的话,可能会触发循环通知,导致系统崩溃。
  • 此模式包含四种角色
    • 抽象主题:抽象的被观察者,声明了增加、删除、通知观察者的函数。
    • 具体主题:抽象主题的实现类。
    • 抽象观察者:声明了更新自己的函数。
    • 具体观察者:抽象观察者的实现类。

举例应用:模拟微信公众号更新推送,通知所有用户的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Subject.java
import java.util.ArrayList;
import java.util.List;

public interface Subject { // 抽象主题
void attach(Observer obs); // 增加订阅者
void detach(Observer obs); // 删除订阅者
void notify(String msg); // 通知订阅者
}
class SubscriptionSubject implements Subject { // 具体主题
private List<Observer> userList = new ArrayList<Observer>();
public void attach(Observer obs) { userList.add(obs); }
public void detach(Observer obs) { userList.remove(obs); }
public void notify(String msg) {
for (Observer obs : userList)
obs.update(msg);
}
}
1
2
3
4
5
6
7
8
9
// Observer.java
public interface Observer { // 抽象观察者
void update(String msg);
}
class WeChatUser implements Observer { // 具体观察者
private String name;
public WeChatUser(String name) { this.name = name; }
public void update(String msg) { System.out.println(name + " \"" + msg + "\""); }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Test.java
public class Test {
public static void main(String[] args) {
Subject s = new SubscriptionSubject();

WeChatUser user1 = new WeChatUser("Tom");
WeChatUser user2 = new WeChatUser("John");
WeChatUser user3 = new WeChatUser("Kate");

s.attach(user1);
s.attach(user2);
s.attach(user3);

s.notify("Update");
}
}

八、集合框架

1. 集合框架概述

  • 含义

    集合框架是为表示和操作集合而规定的一种统一的、标准的体系结构。

  • 集合框架的三大组成部分

    • 接口:表示集合的抽象数据类型
    • 实现:接口的具体实现,即一系列数据结构
    • 算法:含有查找、排序等算法的工具包

    说明

    • 所有集合类都位于java.util包下。
    • 集合框架可以细化为四个部分
      • 数据结构:列表List、队列Queue、双端队列Deque、集合Set和映射Map
      • 比较器:比较器Comparator和排序接口Comparable
      • 算法:常用算法类Collections和静态数组的排序、查找类Arrays
      • 迭代器:通用迭代器IteratorList特化迭代器ListIterator
  • 集合类只容纳对象。

2. Collection接口

  • Collection接口是ListSetQueue接口的父接口,是集合框架的根接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // Collection 接口中定义的常用函数
    public interface Collection<E> extends Iterable<E> {
    boolean add(E e);
    boolean addAll(Collection<? extends E> c);
    void clear();
    boolean remove(E e);
    boolean removeAll(Collection<?> c);
    boolean contains(Object obj);
    boolean containsAll(Collection<?> c);
    boolean isEmpty();
    int size();
    Object[] toArray();
    Iterator iterator();
    // ...
    }
  • List接口

    • List的特征是其元素有确定的顺序并且可以重复,可以通过下标访问元素

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // List 接口中新增的函数
      public interface List<E> extends Collection<E> {
      E get(int index);
      E set(int index, E element);
      void add(int index, E element);
      E remove(int index);
      int indexOf(Object o);
      int lastIndexOf(Object o);
      boolean addAll(int index, Collection<? extends E> c);
      ListIterator<E> listIterator();
      ListIterator<E> listIterator(int index);
      List<E> subList(int fromIndet, int toIndex);
      // ...
      }
    • List的实现类有ArrayListLinkedList

      • ArrayList实现了在内存中连续储存的可变长数组遍历和随机访问元素的效率较高。

      • LinkedList实现了链表插入和删除的效率较高。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        // LinkedList 的特殊函数
        {
        void addFirst(E element);
        void addLast(E element);
        E getFirst();
        E getLast();
        E removeFirst();
        E removeLast();
        }
  • Set接口

    • Set中的元素必须唯一。
    • 添加到Set中的元素必须定义equals函数,从而保证元素的唯一性。
    • 实现Set接口的类有HashSetTreeSet等。

3. Map接口

  • Map接口专门处理键值对映射数据的存储,其常用的成员有

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public interface Map<K, V> {
    int size();
    boolean isEmpty();
    boolean containsKey(Object key);
    boolean containsValue(Object value);
    V get(Object key);
    V getOrDefault(Object key, V defaultValue);
    // 若找到 key 返回其对应的 value,否则返回 defaultVaule
    V put(K key, V value);
    V remove(Object key);
    void putAll(Map<? extends K, ? extends V> m);
    void clear();
    Set<K> keySet();
    Collection<V> values();
    Set<Map.Entry<K, V>> entrySet();

    interface Entry<K, V> {
    K getKey();
    V getValue();
    V setValue();
    // ...
    }

    //...
    }

    Map的注意事项

    • Map是接口,不能直接实例化。通常选用其子类TreeMapHashMap

    • Map中的key是唯一的,value可以重复。分离key时,将其储存到Set中;分离value时,将其储存到Collection的可包含重复元素的子类中。

    • Map中的key不能修改value可以修改。如果想修改key只能先删除后重新插入。

    • Map.Entry —— Map中的每一个元素,即将键值对打包后的类型

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // 使用 Map.Entry 遍历 Map
      import java.util.*;

      public class Test {
      public static void main(String[] args) {
      Map<Integer, String> map = new TreeMap<Integer, String>();
      map.put(10, "hello"); map.put(20, "abc"); map.put(66, "yes");

      for (Map.Entry<Integer, String> entry : map.entrySet()) {
      System.out.println("key: " + entry.getKey()
      + " value: " + entry.getValue());
      }
      }
      }
    • 另一种遍历的有效手段

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // 使用 forEach 遍历 Map
      import java.util.*;

      public class Test {
      public static void main(String[] args) {
      Map<Integer, String> map = new TreeMap<Integer, String>();
      map.put(10, "hello"); map.put(20, "abc"); map.put(66, "yes");

      map.forEach((key, value) -> {
      System.out.println("key: " + key + " value: " + value);
      })
      }
      }
Map实现类 TreeMap HashMap
底层结构 红黑树 哈希桶
增删查时间复杂度 $O(log_2N)$ $O(1)$
是否有序 关于key有序 无序
线程安全性 不安全 不安全
增删查区别 需要进行元素比较 通过哈希函数计算哈希地址
注意事项 key必须能够比较,否则抛ClassCastException 自定义类型必须重写equalshashCode函数
应用场景 需要key有序的情形 关心更高的时间性能
  • 使用TreeMap时,如果key是自定义类型,必须定义比较大小的方法。

    • Method 1
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // Test1.java
    import java.util.*;

    class Person implements Comparable {
    private String name;
    private int age;
    public Person(String name, int age) { this.name = name; this.age = age; }

    @Override
    public int compareTo(Object o) {
    Person p = (Person)o;
    if (this.age == p.age) return this.name.compareTo(p.name);
    else return (this.age < p.age) ? -1 : 1;
    }
    }
    public class Test1 {
    public static void main(String[] args) {
    Map<Person, String> map = new TreeMap<Person, String>();
    map.put(new Person("Tom", 20), "hello");
    map.put(new Person("Jana", 18), "world");
    }
    }
    • Method 2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // Test2.java
    import java.util.*;

    class Person implements Comparable {
    private String name;
    private int age;
    public Person(String name, int age) { this.name = name; this.age = age; }
    }
    public class Test2 {
    public static void main(String[] args) {
    Map<Person, String> map = new TreeMap<Person, String>
    (new Comparator<Person>() {
    @Override
    public int compare(Person p1, Person p2) {
    if (p1.age == p2.age) return p1.name.compareTo(p2.name);
    else return (p1.age < p2.age) ? -1 : 1;
    }
    });
    map.put(new Person("Tom", 20), "hello");
    map.put(new Person("Jana", 18), "world");
    }
    }
  • 更多的细节

    • VectorArrayList实现原理相同、功能相同,多数情况可以互用。区别为(1)Vector线程安全,ArrayList重速度、线程不安全;(2)长度需增加时,Vector默认增长一倍,ArrayList默认增长50%。
    • HashtableHashMap实现原理相同、功能相同,多数情况可以互用。区别为(1)Hashtable继承自Dictionary类,HashMap实现Map接口;(2)Hashtable线程安全,HashMap线程不安全;(3)Hashtable不允许null值。
    • 关于null
      • 各集合中均可以放null值。
      • Set的元素和Mapkey中,null最多有一个。特别,TreeMapkey不能是null

4. Iterator接口

1
2
3
4
5
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 遍历一个 List 的四种方式
import java.util.*;

public class TraverseList<E> {
public void traverseByFor(List<E> list) {
for (int i = 0; i < list.size(); i++)
System.out.println(list.get(i));
}
public void traverseByPoweredFor(List<E> list) {
for (E each : list)
System.out.println(each);
}
public void traverseByIterator(List<E> list) {
Iterator<E> it = list.iterator();
while (it.hasNext())
System.out.println(it.next());
}
public void traverseByForEach(List<E> list) {
list.forEach((e) -> {
System.out.println(e);
});
}
}

九、异常(Exception)

1. 异常及其处理

  • 异常是一个可以正确运行的程序运行中可能发生的错误;异常处理就是将错误按类型和差别区分,对无法预知的错误进行捕获,并为之编写错误处理代码。

  • 异常的特点

    • 偶然性
    • 可预见性
    • 严重性
  • Java 异常类

    • ThrowableObject的直接子类,是所有异常类的父类,它位于java.lang包中。
    • Exception继承自Throwable,它本身及其子类都是异常。
    • Error也是Throwable的子类,它本身及其子类都由 Java 虚拟机生成并抛出,程序不做处理
  • 异常的分类

    • 受检异常:RuntimeException异常和用户自己定义的未继承自RuntimeException的异常。此类异常必须用try-catch处理,或者该函数throws该异常。

      1
      2
      3
      4
      5
      6
      7
      java.lang.ClassNotFoundException
      java.lang.CloneNotSupportedException
      java.lang.IllegalAccessException
      java.lang.NoSuchMethodException
      java.io.IOException
      java.io.FileNotFoundException
      ...
    • 非受检异常:由系统检测,用户程序可不做处理。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      java.lang.ArithmeticException
      java.lang.ArrayStoreException
      java.lang.ClassCastException
      java.lang.IllegalArgumentException
      java.lang.NumberFormatException
      java.lang.IndexOutOfBoundsException
      java.lang.ArrayIndexOutOfBoundsException
      java.lang.StringIndexOutOfBoundsException
      java.lang.NullPointerException
      ...

    错误Error也是非受检的。

    1
    2
    3
    4
    5
    6
    7
    java.lang.LinkageError
    java.lang.NoClassDefFoundError
    java.lang.OutOfMemoryError
    java.lang.StackOverflowError
    java.lang.UnknownError
    java.lang.VirtualMachineError
    ...

2. 异常处理的抓抛模型

  • 自定义异常 Exceptions.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class ExpA extends Exception {      // 自定义异常可以继承自 Exception

    }
    class ExpB extends Throwable { // 也可以继承自 Throwable
    // 对于继承自 Throwable 的异常,不能用 Exception 捕获。
    // 因为 Exception 是 Throwable 的子类。
    }
    class ExpC extends RuntimeException {
    // 自定义的非受检异常
    }
  • 编写可能抛出异常的类 MyClass.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
    import java.io.IOException;
    import java.util.Random;

    public class MyClass {
    void throwMyException() throws ExpA { // 声明函数抛出异常
    throw new ExpA();
    }
    void throwMoreExceptions() throws ExpB, IOException, Exception {
    // 声明函数抛出一系列异常
    Random r = new Random();
    int x = r.nextInt(-1, 2); // return {-1, 0, 1} randomly
    if (x > 0)
    throw new ExpB();
    else if (x == 0)
    throw new IOException();
    else
    throw new Exception();
    }
    void throwUncheckedException(int idx) {
    int[] arr = new int[10];
    arr[idx] = 10;
    }
    void throwSelfDefUncheckedException() { // 运行时异常不声明也不报错
    throw new ExpC();
    }
    }
  • 编写测试类,用try-catch(-finally)处理异常 Test.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
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    import java.io.IOException;

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

    try {
    mc.throwMyException();
    } catch (ExpA e) {
    e.printStackTrace(System.out);
    System.out.println("catch ExpA");
    }

    try {
    mc.throwMoreExceptions();
    } catch (ExpB e) {
    e.printStackTrace(System.out);
    System.out.println("catch ExpB");
    } catch (IOException e) {
    e.printStackTrace(System.out);
    System.out.println("catch IOException");
    } catch (Exception e) {
    // 更上层的异常类应放 catch 的后面,因为父类异常可以捕获子类异常
    e.printStackTrace(System.out);
    System.out.println("catch Exception");
    }

    try {
    mc.throwUncheckedException(3);
    } catch (Exception e) {
    // 用父类 Exception 捕获子类 ArrayIndexOfOutBoundsException
    System.out.println("in catch");
    } finally {
    // 无论是否抛出并捕获,finally 都会执行
    System.out.println("in finally");
    }

    try {
    mc.throwUncheckedException(100);
    } catch (Throwable t) {
    // 用父类 Throwable 捕获子类 ArrayIndexOfOutBoundsException
    System.out.println("in catch");
    } finally {
    // 无论是否抛出并捕获,finally 都会执行
    System.out.println("in finally");
    }

    mc.throwSelfDefUncheckedException(); // 非受检异常,编译不报错
    }
    }
  • 注意事项

    • 一个函数可以不处理它产生的异常,而是通过throws关键字声明异常,从而使异常沿着调用层次向上传递,由调用它的函数处理。当传递到主函数main而没有处理异常,即public static void main(String[] args) throws Exception { },则异常将交给系统处理

    • 子类重写父类的函数时,不能抛出比父类函数抛出范围更大的异常。

      说明

      使用动多态时,通过父类的引用动态调用子类重写的函数。在调用处,由于使用了父类引用,故只会用try-catch处理父类抛出的异常类型,而无法预知子类可能产生的更多的异常类型,则会出错。

    • finally细节:try中抛出异常 - catchreturn - finally中修改返回值

      • 此类情况下,程序会自动添加一个返回值类型的、名为var2的变量。在catch中的return x;语句会被换成var2 = x;。真正的return发生在finally执行之后,并且是return var2;
      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
      public class Test {
      public static void main(String[] args) {
      System.out.println("test1: in main, i = " + test1());
      System.out.println();
      System.out.println("test2: in main, s = " + test2());
      System.out.println();
      System.out.println("test3: in main, sb = " + test3());
      }
      public static int test1() {
      int i = 1;
      // int var2;
      try {
      throw new Exception();
      } catch (Exception e) {
      return i; // var2 = i; (var2 == i == 1)
      } finally {
      i++; // i == 2;
      System.out.println("test1: in finally, i = " + i);
      }
      // return var2;
      }
      public static String test2() {
      String s = "Hello";
      // String var2;
      try {
      throw new Exception();
      } catch (Exception e) {
      return s; // var2 = s;
      } finally { // 字符串的 + 会 new 出一个新字符串
      s += " World"; // var2 -> "Hello"; s -> "Hello World"
      System.out.println("test2: in finally, s = " + s);
      }
      // return var2;
      }
      public static StringBuilder test3() {
      StringBuilder sb = new StringBuilder("Good");
      // StringBuilder var2;
      try {
      throw new Exception();
      } catch (Exception e) {
      return sb; // var2 = s;
      } finally {
      sb.append(" Morning"); // 不 new 新对象
      // sb -> "Good Morning" <- var2
      System.out.println("test3: in finally, sb = " + sb);
      }
      // return var2;
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 输出
      test1: in finally, i = 2
      test1: in main, i = 1

      test2: in finally, s = Hello World
      test2: in main, s = Hello

      test3: in finally, sb = Good Morning
      test3: in main, sb = Good Morning

十、Java IO

(一) 基本输入输出

1. 命令行参数

1
2
3
4
5
6
public class CommandLineArgsTest {
public static void main(String[] args) {
for (String arg : args)
System.out.println(arg);
}
}
1
2
3
4
5
6
7
>>> javac CommandLineArgsTest.java
>>> java CommandLineArgsTest arg0 arg1 arg2 ...
arg0
arg1
arg2
...
>>>

args[0]从第一个有效参数开始,不包括程序名称。

2. 标准输入输出

  • 三个标准 I/O 对象

    • 标准输入System.in
    • 标准输出System.out
    • 标准错误System.err
  • 两个标准输出对象都重载有printlnprint函数,支持基本类型和类类型的输出。打印类类型时,调用其toString函数。

  • java.util.Scanner的使用

    • Scanner通过分隔符模式将输入分解为标记(默认情况下为空白符匹配),然后可以使用不同的next函数获取其值。在获取前,一般需要判断是否还有要输入的数据。
    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
    import java.util.*;

    public class Test {
    static Scanner in = new Scanner(System.in);
    public static ArrayList<String> readWord() {
    ArrayList<String> ret = new ArrayList<String>();
    while (in.hasNext()) { // 判断是否还有数据
    ret.add(in.next()); // 读取数据
    }
    return ret;
    }
    public static ArrayList<String> readLine() {
    ArrayList<String> ret = new ArrayList<String>();
    while (in.hasNextLine()) {
    ret.add(in.nextLine());
    }
    return ret;
    }
    public static void readAndShow() {
    int i = 0;
    double d = 0;
    String s = null;
    if (in.hasNextInt()) i = in.nextInt();
    if (in.hasNextDouble()) d = in.nextDouble();
    if (in.hasNext()) s = in.next();
    System.out.println(String.format("i = %d, d = %.2f, s = %s", i, d, s));
    }
    }

(二) 文件与数据流

1. 输入输出流

  • 概念:一组有顺序的、有起点和终点的字节集合。

    • Java 程序不能直接操纵 I/O 设备,而是在程序和设备之间加入一个中间介质,建立起数据通道传输数据,这就是流。
    • 流是数据传输的抽象表达,和具体设备无关。程序一旦建立起流,就可以不用关心起点或终点是何种设备。
  • 分类

    • 输入数据流:只读不写
      • 字节流:InputStream的子类
      • 字符流:Reader的子类
    • 输出数据流:只写不读
      • 字节流:OutputStream的子类
      • 字符流:Writer的子类

2. 文件类File

  • File类代表与平台无关的文件或目录。

  • File类不涉及文件的具体内容,一般用于获得文件属性、新建和删除、重命名等。

  • File类常用操作

    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
    // 四种构造
    File(File parent, String child);
    File(String pathname);
    File(String parent, String child);
    File(URI uri);

    // 文件属性
    boolean canRead();
    boolean canWrite();
    boolean exists();
    boolean isFile();
    boolean isDirectory();
    long length();
    boolean setReadOnly();


    // 文件的创建、删除、重命名等
    String getName();
    String getPath();
    String getAbsolutePath();
    String getParent();
    File getParentFile();
    boolean createNewFile();
    boolean mkdir();
    boolean mkdirs();
    boolean delete();
    boolean renameTo(File f);

3. I/O 流

InputStream

  • FileInputStream用于从本地文件读取数据

  • PipedInputStream用于从管道读取数据

  • FilterInputStream的子类

    • PushBackInputStream可以将读出的数据回退到缓冲区中
    • BufferedInputStream可指定缓冲区的大小进行构造
    • DataInputStream可以读取基本数据类型
    • ByteArrayInputStream可以将指定的byte[]作为缓冲区
    • SequencedInputStream可以把多个InputStream对象转化为单个
    • ObjectInputStream用于读取对象
    • ……
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int read();
    int read(byte[] buffer);
    int read(byte[] buffer, int offset, int length);

    void close();
    int available();
    long skip(long n);
    boolean markSupported();
    void mark(int readlimit);
    void reset();

OutputStream

  • 基本成员与InputStream类似。

  • 子类PrintStream支持多种格式的打印函数。

    1
    2
    3
    4
    5
    6
    void write(int c);
    void write(byte[] buffer);
    void write(byte[] buffer, int offset, int length);

    void close();
    void flush();

Reader

  • 结构

    1
    2
    3
    4
    5
    6
    7
    8
              BufferedReader <- LineNumberReader
    CharArrayReader
    StringReader
    Reader <-
    InputStreamReader <- FileReader
    PipedReader
    FilterReader <- PushbackReader
    ...
  • 常用函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int read();
    int read(char[] cbuf);
    int read(char[] cbuf, int offset, int length);
    String readLine();

    void close();
    boolean ready();
    long skip(long n);
    boolean markSupported();
    void mark(int readAheadLimit);
    void reset();

Writer

  • 结构

    1
    2
    3
    4
    5
    6
    7
    8
              BufferedWriter
    CharArrayWriter
    StringWriter
    Writer <- OutputStreamWriter <- FileWriter
    PrintWriter
    PipedWriter
    FilterWriter
    ...
  • 常用函数

    1
    2
    3
    4
    5
    6
    7
    8
    void write(int c);
    void write(char[] cbuf);
    void write(char[] cbuf,l int offset, int length);
    void write(String string);
    void write(String string, int offset, int length);

    void close();
    void flush();
  • 过滤流

    • FilterInputStreamFilterOutputStream是两个抽象类,都属于过滤流类。

    • 过滤流具有将流连接在一起的能力。

    • 过滤流实现了对象的同步机制,使得任一时刻只有一个线程访问它。

    • 过滤流及其子类的构造通过可以接受一个 I/O 流对象作为参数,从而为之加上一层过滤器

      举例说明:带缓冲的字节输入流BufferedInputStream

      • 此类接收一个InputStream对象作为自己的成员,从而使原输入流对象带上了缓冲,并提升了读取速度。
      • 此类自带缓冲区,也可以调用重载的构造指定缓冲区大小,一般以本设备的内存页或磁盘块大小的整数倍为宜。
      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
      // Example.java
      import java.io.BufferedInputStream;
      import java.io.File;
      import java.io.FileInputStream;
      import java.io.InputStream;
      import java.util.Date;

      public class Example {
      public static void main(String[] args) {
      File f = new File("./a.jpeg"); // 一张很大的图片
      int c, cnt1 = 0, cnt2 = 0;
      if (!f.exists()) System.exit(-1); // 判断文件是否存在

      Date d1 = new Date(); // 第一个时间点

      try {
      InputStream is = new FileInputStream(f);
      while ((c = is.read()) != -1) cnt1++; // 逐字节遍历
      is.close();
      } catch (Exception e) {
      e.printStackTrace();
      System.exit(-1);
      }

      Date d2 = new Date(); // 第二个时间点

      try {
      BufferedInputStream bis
      = new BufferedInputStream(new FileInputStream(f));
      while ((c = bis.read()) != -1) cnt2++; // 逐字节遍历
      bis.close();
      } catch (Exception e) {
      e.printStackTrace();
      System.exit(-1);
      }

      Date d3 = new Date(); // 第三个时间点

      System.out.println
      ("InputStream: read " + cnt1 + " bytes, using " +
      (d2.getTime() - d1.getTime()) + " ms.");
      System.out.println
      ("BufferedInputStream: read " + cnt2 + " bytes, using " +
      (d3.getTime() - d2.getTime()) + " ms.");
      }
      }
  • 数据流

    使用举例

    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
    import java.io.*;

    public class Example {
    public static void main(String[] args) {
    byte[] b = {23, 90, -1, 48, -29};
    File out = new File("./date.dat");
    try {
    out.createNewFile();

    DataOutputStream dos
    = new DataOutputStream(
    new BufferedOutputStream(
    new FileOutputStream(out)));

    dos.write(b); // 写入五个字节
    dos.writeBoolean(true); // 写入一个布尔值
    dos.writeDouble(3.14); // 写入一个双精度浮点数
    dos.writeUTF("你好,世界"); // 写入 UTF 编码的字符串
    dos.writeChar('s'); // 写入一个字符
    dos.writeUTF("hello 数字123"); // 写入 UTF 编码的字符串
    dos.close();

    File in = out;
    DataInputStream dis
    = new DataInputStream(
    new BufferedInputStream(
    new FileInputStream(in)));

    System.out.println("available: " + dis.available());
    // 可读字节数
    System.out.println("mark supported? " + dis.markSupported());
    // 是否可标记
    dis.skip(3);
    // 跳过三个字节
    System.out.println("=== skip 3 bytes, read ===\n" +
    dis.readByte() + ", " + dis.readByte());
    // 读取剩下两个字节的整数 48, -29
    System.out.println(dis.readBoolean());
    // 读取布尔类型 true
    System.out.printf("%.2f\n", dis.readDouble());
    // 读取双精度浮点类型 3.14
    dis.mark(1);
    // 在当前位置做标记
    System.out.println(dis.readUTF()); // 读取字符串 “你好,世界”
    System.out.println(dis.readChar()); // 读取字符 'c'
    System.out.println(dis.readUTF()); // 读取字符串 “hello 数字123”
    dis.reset(); // 回到标记位置
    System.out.println(dis.readUTF()); // 读取字符串 “你好,世界”
    } catch (Exception e) {
    e.printStackTrace();
    System.exit(-1);
    }
    }
    }
  • 回压流

    • 一些情况下需要先读入一个或几个字节,判断输入流的属性。在正式读取时,再将全部内容读出。回压字节流PushbackInputStream提供的unread函数可以把读过的一个或几个字节回压到流中,也可以回压别的字节数据

    • 构造

      1
      2
      public PushbackInputStream(InputStream in);
      public PushbackInputStream(InputStream in, int size); // size - 缓冲区大小
    • unread函数

      1
      2
      3
      public void unread(int b);
      public void unread(byte[] b);
      public void unread(byte[] b, int off, int len);
  • 打印流

    • PrintStream的对象建构在其他输出流的对象的基础上,所以其构造需要传入其他OutputStream的子类的对象作为参数。
    • 标准输出System.out和标准错误System.err就是PrintStream实例的引用。
    • PrintStream提供了重载write print println函数用于输出。
    • PrintStream的成员函数一般不抛异常,且当输出回车换行时,会自动强制刷缓存(flush)
  • 管道流

    • 管道数据流一定是成对出现、相互连接的。管道主要用于线程间的通信
    • 管道输入流用管道输出流对象构造,管道输出流用管道输入流对象构造。如果连接了已经和别的管道连接的管道,会抛IOException
    • 管道流是InputStreamOutputStream的子类,因而可以连接到别的流。

4. try with resource

  • 打开的流越多,try-catch-finally嵌套得越深。一般地,finally中进行流的关闭,这一功能可以用try with resource语句实现。

  • 凡是实现了java.lang.AutoCloseablejava.io.Closeable接口的类,都可以作为资源(resource)。在try结束后,不论是否抛异常,资源都会被关闭,关闭的顺序与资源初始化的顺序相反

    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
    class ResourceA implements AutoCloseable {
    public void f() {
    System.out.println("ResourceA");
    }
    @Override
    public void close() throws Exception {
    System.out.println("close ResourceA");
    }
    }
    class ResourceB implements AutoCloseable {
    public void f() {
    System.out.println("ResourceB");
    }
    @Override
    public void close() throws Exception {
    System.out.println("close ResourceB");
    }
    }
    public class Test {
    public static void main(String[] args) {
    try (ResourceA a = new ResourceA(); // 多个资源的初始化,用分号隔开
    ResourceB b = new ResourceB()) {
    a.f();
    b.f();
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }

5. 对象流和串行化

  • 使用对象流ObjectInputStreamObjectOutputStream将从文件中读取某个类的对象或把某个类的对象存到文件中。
  • 能够进行串行化的类必须实现串行化接口java.io.Serializable此接口中没有定义任何内容,仅用于告诉编译器这个类可以串行化。串行化(Serialize)指的是把自身转化为一系列字节以记录状态。
  • 有些类不能进行串行化,如Thread对象、FileInputStream对象等,因为其状态是暂时的对不希望串行化的对象应使用transient关键字修饰。
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
import java.io.*;

class MyClass implements Serializable {
int i;
double d;
transient String s1; // 不被串行化的成员
boolean b;
String s2;
MyClass(int i, double d, String s1, boolean b, String s2) {
this.i = i; this.d = d; this.s1 = s1; this.b = b; this.s2 = s2;
}
@Override
public String toString() {
return new String("i = " + i + ", d = " + d +
", s1 = " + s1 + ", b = " +
b + ", s2 = " + s2);
}
}

public class Test {
public static void main(String[] args) {
MyClass mc = new MyClass(10, 3.14, "hello", true, "world");
File f = new File("./class.dat");
try {
f.createNewFile();

ObjectOutputStream oos
= new ObjectOutputStream(
new FileOutputStream(f)); // 创建对象输出流
oos.writeObject(mc); // 将对象 mc 写入文件
oos.close();

ObjectInputStream ois
= new ObjectInputStream(
new FileInputStream(f)); // 创建对象输入流
MyClass mc1 = (MyClass)ois.readObject();
// 从文件读取对象(readObject 返回 Object)
ois.close();

System.out.println(mc1); // s1 未被串行化,故会输出默认值 s1 = null
} catch (Exception e) {
e.printStackTrace();
}
}
}