跳转至

9.0 Enums, Generic, SOLID

Enums

Concepts

编程当中固定以及有限的值直接用整数表示有缺点,比如 1 到底是周一还是周日:类型不安全、含义不明确、维护困难,因此可以使用枚举

// 定义一个表示星期的枚举类型
enum DayOfWeek {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
// 定义一个表示交通方式的枚举类型
enum Transport {
    BUS, TRAIN, FERRY, TRAM
}
  • 枚举常量名通常使用全大写字母 s
  • 枚举名.常量名 的方式来访问具体的值。注意:不使用 new 关键字
DayOfWeek today = DayOfWeek.MONDAY;
Transport myTransport = Transport.BUS;

这样可读性较强也可以使用 == 比较

  • Java 中的 enum 实际上是一种特殊的类
  • 所有枚举都隐式地继承自 java.lang.Enum
  • 不能再继承其他类,也不能被其他类继承
  • 可以像类一样拥有访问修饰符public, protected, private

以及其和 switch 语句也可以结合的比较好

public static int typicalSpeed(Transport transport) {
    switch (transport) {
        case BUS:
            return 50;
        case TRAIN:
            return 100;
        case FERRY:
            return 20;
        case TRAM:
            return 30;
        default:
            return 0;
    }
}

public static boolean isWeekend(DayOfWeek day) {
    return day == DayOfWeek.Saturday || day == DayOfWeek.Sunday;
}

public static void main(String[] args) {
    DayOfWeek day = DayOfWeek.Monday;
    System.out.println(isWeekend(day));
    System.out.println(isWeekend(DayOfWeek.Saturday));
    System.out.println(typicalSpeed(Transport.BUS));
    System.out.println(typicalSpeed(Transport.TRAIN));
}

Functions

  • values()

枚举含有内置方法,values() 静态方法,返回一个包含该枚举所有常量的数组

for (DayOfWeek day : DayOfWeek.values()) {
    System.out.println(day);
}
  • ordinal(): 返回枚举常量在声明时的序号(从 0 开始)。例如 DayOfWeek.MONDAY.ordinal() 返回 0

  • valueOf(String name): 静态方法,根据给定的名称字符串返回对应的枚举常量

枚举类可以添加自己的方法,也可以添加自己的构造函数以及字段,构造器默认必须 private 可以不写,不能使用 newJVM 会在创建枚举常量实例时自动调用。

enum Transport {
    BUS(50), TRAIN(100), FERRY(20), TRAM(30); // 调用构造器

    private final int typicalSpeed; // 添加字段

    Transport(int typicalSpeed) { // 构造器 (隐式 private)
        this.typicalSpeed = typicalSpeed;
    }

    public int getTypicalSpeed() { // 添加方法
        return typicalSpeed;
    }
}
// 使用: Transport.BUS.getTypicalSpeed() 返回 50

可以通过这种方式直接在内部写好值就不用写 switch 语句

Autoboxing and Unboxing

Java 为每个基本数据类型 (primitive type: int, double, boolean, char 等) 都提供了一个对应的包装类 (wrapper class: Integer, Double, Boolean, Character 等)。这些包装类是对象。

  • 自动装箱 (Autoboxing):

Java 编译器自动地将基本数据类型的值转换为对应的包装类对象。

  • 自动拆箱 (Unboxing):

Java 编译器自动地将包装类对象转换为对应的基本数据类型的值。

Usage

  • 赋值
Integer wrapperInt = 10;       // Autoboxing: int 10 -> Integer(10)
int primitiveInt = wrapperInt; // Unboxing: Integer(10) -> int 10
  • 泛型集合
List<Integer> list = new ArrayList<>();
list.add(1); // Autoboxing: int 1 -> Integer(1)
int first = list.get(0); // Unboxing: Integer(1) -> int 1

ArrayList 这种只能储存对象不能直接储存基本数据类型用这种方法传入

  • 方法调用:类型自动转换
// 方法期望 int,传入 Integer -> 自动拆箱
public static int sum(int a, int b) { return a + b; }
Integer x = 1, y = 2;
int result = sum(x, y);

// 方法期望 Double,传入 double -> 自动装箱
public static Double max(Double a, Double b) { return a > b ? a : b; }
Double maxVal = max(1.0, 2.0);
  • 运算:自动拆箱成基本类型再计算
Integer a = 1;
Integer b = 2;
Integer c = a + b; // a, b 拆箱成 int,相加后结果 3 再装箱成 Integer 赋给 c

Problems

  • reference

自动装箱/拆箱创建的是不同的对象(或从缓存获取)。它不能用来像对象引用那样共享对原始基本类型值的修改。

Integer a = 1;
Integer b = a; // b 指向和 a 相同的 Integer 对象 (对于小数值,通常是缓存的)
a += 2;       // 实际是: a = Integer.valueOf(a.intValue() + 2); a 指向了新的 Integer(3) 对象
System.out.println(a); // 3
System.out.println(b); // 1 (b 仍然指向原来的 Integer(1) 对象)
  • performance

设计到对象的创建和转换,相比直接使用基本数据类型会有一定数据开销:时间与空间

  • NullPointerException

如果一个包装类变量的值是 null,在自动拆箱时会抛出 NullPointerException

Integer i = null;
int j = i; // 尝试拆箱 null,会抛出 NullPointerException

使用包装类/自动装箱

  • 当需要将基本类型值存入只能存储对象的集合类时(如 ArrayList<Integer>)。
  • 当需要使用基本类型值作为泛型类型参数时。
  • 当需要调用需要对象参数的方法时。
  • 当需要使用包装类提供的静态工具方法或常量时。

Wrapper Class Utilities

包装类提供了静态方法与常量

数值类型 (Integer, Long, Double, Float 等):

  • 字符串转换: Integer.parseInt("123"), Double.parseDouble("3.14") (注意可能抛出 NumberFormatException)。
  • 数值范围: Integer.MAX_VALUE, Integer.MIN_VALUE, Double.POSITIVE_INFINITY, Double.NaN 等。
  • 进制转换/位运算: Integer.toBinaryString(10), Integer.highestOneBit(10) 等。

Character:

  • 字符判断: Character.isDigit('1'), Character.isLetter('a'), Character.isUpperCase('A'), Character.isWhitespace(' ') 等。
  • 大小写转换: Character.toUpperCase('a'), Character.toLowerCase('Z')

Generics

想象一下,我们想创建一个栈(Stack)数据结构。如果我们为 String 类型写一个 StringStack ,那么当我们想存 int 或者其他类型时,就需要重写整个类 。这非常低效。

  • 一个方法是使用 Object 类来创建 ObjectStack结合 Autoboxing 以及 Unboxing 特性,甚至可以储存原始类型,但是取出数据的时候需要强制类型转换:int three = (Integer) ss.pop();

  • 如果在 pop 时转换的类型与存入时的类型不匹配,程序不会在编译时报错,而是在运行时抛出 ClassCastException 错误 。这相当于丢失了 Java 强大的类型检查功能 。

而泛型可以更好地解决这个问题:在类名后面加上 <T> 这里的 T 就是类型参数 Type Parameter,他是一个占位符代表某种具体的类型,因此可以用任何合法标识代替 T

public class GenericStack<T> {
    public GenericStack(int capacity) { ... }
    public void push(T s) { ... } // 方法参数使用类型参数 T
    public T pop() { ... }         // 返回值使用类型参数 T
}

那么在实例化的时候需要为其指定一个具体的类型参数如 Integer 或 String

GenericStack<Integer> intStack = new GenericStack<Integer>(5); // T 被指定为 Integer
intStack.push(3); // OK
// intStack.push("Hello"); // 编译时错误!类型不匹配
int value = intStack.pop(); // 不需要强制类型转换
  • 使用 generics 在编译时会检查类型是否匹配,避免了运行时的 ClassCastException
  • 只需写一次 GenericStack<T> 就可以用于任何非原始类型
  • 无需强制转换

类型参数必须是类类型,不能是原始类型(如 int)。但由于自动装箱/拆箱,使用原始类型通常没有障碍 。

Multiple Generics Parameters

public class GenericPair<T, V> {
    public T first;
    public V second;
    public GenericPair(T first, V second) {
        this.first = first;
        this.second = second;
    }
}

泛型类可以有多个类型参数,用逗号分隔。

// 创建一个存储 <Integer, String> 对的 ArrayList
var studentList = new ArrayList<GenericPair<Integer, String>>();
studentList.add(new GenericPair<Integer, String>(10243549, "Alan Turing"));

第二行出现了 var 关键词,是 Java 10 引入的一个关键字

  • 用于局部变量类型推断 Local Variable Type Inference
// 原本需要这样写,类型名称很长:
ArrayList<GenericPair<Integer, String>> studentList = new ArrayList<GenericPair<Integer, String>>();

var替代后编译器可以自动从右边推断出左边减少冗余,只能用于局部变量

Bounded Type Parameters

有时我们希望限制可以用作类型参数的类型范围。使用 extends 关键字,例如 <T extends SuperClass>。这表示 T 必须是 SuperClass 本身或者是它的子类 。SuperClass 就是这个类型参数的上界 (Upper Bound)

abstract class Bird { }
class Emu extends Bird { }
class Hawk extends Bird { }

class BirdPair<T extends Bird> { // T 必须是 Bird 或其子类
    public T first;
    public T second;
    public BirdPair (T first, T second) { ... }
}
var emuPair = new BirdPair<Emu>(new Emu(), new Emu()); // OK
var hawkPair = new BirdPair<Hawk>(new Hawk(), new Hawk()); // OK
var birdPair = new BirdPair<Bird>(new Emu(), new Hawk()); // OK
// var badPair = new BirdPair<String>("Hello", "World"); // 编译时错误!String 不是 Bird 的子类

<T> 实际上等价于 <T extends Object>,因为所有类都继承自 Object

Generic Interfaces

接口也可以是泛型的,拥有类型参数,类型参数可以用在接口的方法签名中。Comparable<T>就是一个泛型接口,它要求实现 compareTo(To) 方法,用于比较当前对象和另一个 T 类型的对象

With bounded type parameters

public class RunningMaximum<T extends Comparable<T>> { // T 必须实现 Comparable<T>
    private T currentMax;
    public RunningMaximum (T initial) { currentMax = initial; }
    public void addNumber (T number) {
        if (number.compareTo(currentMax) > 0) { // 调用 compareTo 方法
            currentMax = number;
        }
    }
    public T getCurrentMax() { return currentMax; }
}
  • 可以将泛型接口用作有界类型参数的边界。
  • 可以用于任何实现了 Comparable 的类型,如 Integer, String
RunningMaximum<Integer> runningMax = new RunningMaximum<>(1); // 使用 Integer
runningMax.addNumber(5);
System.out.println(runningMax.getCurrentMax()); // 输出 5

实现过程中第一行在 Java 7之前你必须这样写:

RunningMaximum<Integer> runningMax = new RunningMaximum<Integer>(1);
// ^^^^^^^^^^^^^^^^^^^             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
// (类型声明)                        (构造函数调用时指定类型)

Java 7 之后允许直接右侧写 <> 减少冗余从左侧推断出范型参数,结合 Java 10 最终简写成

var runningMax = new RunningMaximum<>(1);

Generic Methods

方法也可以独立于类定义自己的类型参数

类型参数列表放在修饰符(如 public static)之后,返回类型之前。

public class GenericMax {
    // 定义了一个类型参数 T,它必须实现 Comparable<T>
    public static <T extends Comparable<T>> T max(T a, T b) {
        if (a.compareTo(b) > 0) {
            return a;
        } else {
            return b;
        }
    }
}

T 的具体含义是跟对象绑定的,而静态方法属于,不属于任何特定的对象。因为静态方法不与任何特定对象关联,所以它无法知道当前上下文中 T 应该代表哪个具体类型。因此当你需要在静态方法中使用泛型时,你不能使用定义的类型参数(如 T)。相反,你需要定义一个泛型静态方法,它有自己独立的类型参数。

Type Erasure

Java 中的泛型主要是编译时的特性。在编译代码时,编译器会检查泛型的类型安全性,编译完成后,所有的泛型类型信息都会被擦除 (erased) 。类型参数(如 T)在字节码中会被替换成它们的边界。编译器会在需要的地方自动插入强制类型转换,以保证运行时逻辑的正确性 。类型擦除是 Java 泛型实现的一种方式,但它会导致一些限制和“陷阱” (sharp edges)。

  • 不能创建类型参数的实例 (如 new T()

  • 不能创建泛型数组 (如 new T[100])

  • 不能对带有参数化类型的对象使用 instanceof 检查 (如 obj instanceof ArrayList<String>)

  • 擦除后方法签名相同会造成歧义

Method Signature:在 Java 中,一个方法的签名主要由它的方法名参数类型列表组成。

public class MyGenericClass<T, V> {

    public void process(T data) {
        System.out.println("Processing T: " + data);
    }

    public void process(V data) { // 看起来和上面那个不同
        System.out.println("Processing V: " + data);
    }
}

擦除后都变成了 process(Objec data) 编译阶段就会报错

SOLID

“Clean Code” by Robert Martin

Robert Martin 给 OOP 代码规范提出了原则,主要是为了让代码容易理解、灵活、可重用、易于维护,让你感觉像是个“真正的”工程师

Single Responsibility Principle - SRP

  • 一个类应该只有一个引起它变化的原因 。这意味着一个类应该只承担一项职责,或者说只做一件事情 。 这使得代码更容易测试和维护 。如果一个类只做一件事,那么思考如何使用和测试它会更容易 。

例如,将文件读取、写入和压缩的功能分散到FileReaderFileWriterFileCompressor三个不同的类中,而不是放在一个FileManager类里,就是遵循SRP的例子 。同样,将迷宫数据的管理(Grid类)和路径查找(MazeSolver类)分开也是SRP的应用 。

Open-Closed Principle - OCP

软件实体(如类、模块、函数等)应该对扩展开放,对修改关闭 。这意味着我们应该能够通过添加新代码(如新类或新方法)来增加新功能,而不是修改现有代码 。

  • 因此鼓励使用接口和抽象类,降低在添加新功能时引入错误的风险 。

通过计算图形面积的例子说明,使用 Shape 接口,并让 CircleSquare 类实现该接口来计算各自的面积,比在一个 AreaCalculator 类中使用 if-else 判断形状类型来计算面积要好 。添加新形状(如三角形)时,只需创建一个新的实现 Shape 接口的类,而无需修改现有代码 。

public enum ShapeType {
    CIRCLE, SQUARE
}

public class Shape {
    public ShapeType type;
    public double radius; // for Circle
    public double side; // for Square
}

public class AreaCalculator {
    public double area(Shape shape) {
        if (shape.type == ShapeType.CIRCLE) {
            return Math.PI * shape.radius * shape.radius;
        } else if (shape.type == ShapeType.SQUARE) {
            return shape.side * shape.side;
        }
        return 0;
    }
}

以上这样的程序一旦出现新的 Shape 就不得不修改 AreaCalculator

public interface Shape {
    double area();
}

class Circle implements Shape {
    private double radius;

    public double area() {
        return Math.PI * radius * radius;
    }
}

class Square implements Shape {
    private double side;

    public double area() {
        return side * side;
    }
}

我们应该做的是利用接口制定准则,这一块是 open 的,但 implementsclose

Liskov Substitution Principle - LSP

里氏替换原则:一个子类应该能装作自己是它的父类,而不会弄坏什么东西。

  • 这鼓励了正确的继承和多态性。

  • 确保新的派生类不会带来意外的行为。

class Bird {
    void fly() {
    }
}

class Penguin extends Bird {
    @Override
    void fly() {
        throw new UnsupportedOperationException("Penguins can't fly");
    }
}

以上这样的程序违反了 LSP因为直接修改了 fly 的方法,这样如果让企鹅替代鸟类使用就会出现错误,企鹅不会飞

abstract class Bird {
    abstract void move();
}

class FlyingBird extends Bird {
    @Override
    void move() {
        fly();
    }

    void fly() { ... }
}

class NonFlyingBird extends Bird {
    @Override
    void move() {
        walk();
    }

    void walk() { ... }
}

因此我们使用更抽象思想派生出会飞与不会飞两个抽象类型

以及用 function 互相调用的方式最小化影响

你觉得电动人造鸭看起来像鸭子抽象为鸭,但是前者需要电池,那就是错误抽象

Interface Segregation Principle - ISP

接口隔离原则

  • 客户端不应该被强迫依赖它们不使用的方法 。也就是说,使用多个专门的小接口通常比使用一个庞大臃肿的接口要好,这样可以提高代码的可维护性和可读性
public class GameEntity {
    public Model3D getModel() { ... }

    public Point3D getPosition() { ... }

    public int getHP() { ... }

    public int getAttack() { ... }
}

class Renderer {
    public void render(GameEntity entity) {
        // We don't care about getHP() and getAttack()
    }
}

比如上述代码中,游戏开发商继承游戏实体后需要开发一个渲染类,这个渲染类不需要血量和攻击力参数,就造成了内存浪费,所以从设计之初就要考虑到使用的小抽象接口:

interface Renderable {
    Model3D getModel();

    Point3D getPosition();
}

interface Fighter {
    int getHP();

    int getAttack();
}

public class GameEntity implements Renderable, Fighter {
    public Model3D getModel() { ... }

    public Point3D getPosition() { ... }

    public int getHP() { ... }

    public int getAttack() { ... }
}

class Renderer {
    public void render(Renderable entity) {
        // Much better
    }
}

用接口实现可渲染、有战士这两个准则

这样 Renderer 类就可以只依赖于 Renderable 接口

Dependency Inversion Principle - DIP

依赖倒置原则

  • 高层模块不应该依赖于低层模块,两者都应该依赖于抽象 。抽象不应该依赖于细节,细节应该依赖于抽象 。简单来说,要面向接口编程,而不是面向实现编程 。

这样使代码更加灵活。如果改变了一个具体类的实现,不需要检查所有依赖它的类 。

class EmailService {
    void sendEmail(String message, String recipient) { ... }
}

class Notification {
    EmailService emailService;

    void sendNotification(String message, String recipient) {
        emailService.sendEmail(message, recipient);
    }
}

可以看到 Notification 直接依赖于 EmailService 而不是接口,正确实现应该是

interface MessageService {
    void sendMessage(String message, String recipient);
}

class EmailService implements MessageService {
    @Override
    public void sendMessage(String message, String recipient) {
        // Not real code
    }
}

public class Notification {
    MessageService messageService;

    void sendNotification(String message, String recipient) {
        // Now we can change the message service whenever we want
        // Depending on an interface is better than depending on a class
        messageService.sendMessage(message, recipient);
    }
}

将来如果需要短信通知服务再实现一个 MessageService 就行,其他类不需要被更改