Skip to content

Getter-Setter你真的用对了吗

C语言是我学的第一门语言,但是职业生涯是以Java开始的,许多面向对象开发人员(包括我自己)总是习惯写下面例子的代码(Java为例),或许是因为从接触OOP开始, getter-setter 模式似乎就好像天经地义一样了,以致于一直忽视了一个明显的问题:可变性和状态。

java
public class Person {
  private String name;
  private int age;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
  }
}

可变性和状态

可变性和状态是代码复杂且难以推理的原因。在查找错误或审查 PR 时,这通常是最难理解的。getter 和 Setter 乍一看只是访问和修改对象属性的常见方式,但它们有几个问题:

  • 引入不必要的可变性:允许对象状态随时被修改,使代码难以推理(难以预测 name 或 age 何时被更改)。
  • 破坏封装:传统 OOP 认为对象应该隐藏实现细节,但 Getter-Setter 让对象的内部状态暴露给外部代码,违背封装原则。
  • 不适用于并发环境:在多线程环境下,如果一个对象的状态可以被随意更改,可能会导致 竞争条件(Race Condition),比如:
java
Person p = new Person();
Thread t1 = new Thread(() -> p.setAge(30));
Thread t2 = new Thread(() -> p.setAge(40));
t1.start();
t2.start();
System.out.println(p.getAge());  // 30? 40? 难以预测

因此,在设计类时,我们应该问自己这个简单的问题:我可以让它不改变吗?

不变性

不变性是对象状态在创建后不能被修改的属性。因此,让我们将其应用到上面的代码片段。

java
public class Person {
  private final String name;
  private final int age;

  public Person(final String name, final int age) {
    this.name = name;
    this.age = age;
  }

  public String getName() {
    return name;
  }

  public int getAge() {
    return age;
  }
}

这样:

  • name 和 age 一旦赋值,就不会再被修改,代码更安全。
  • 线程安全(多个线程访问 Person 实例不会出问题)。
  • 逻辑更清晰,避免了 “某个对象的状态是否被修改” 这种难以追踪的问题。

with 模式 vs. Builder 模式

有时候,我们仍然希望修改对象的一些属性,而又不希望改变原始对象。这时候,可以用 with 模式:

java
public class Person {
  private final String name;
  private final int age;

  public Person(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public Person withName(String newName) {
    return new Person(newName, this.age);
  }

  public Person withAge(int newAge) {
    return new Person(this.name, newAge);
  }
}

或者使用 Builder 模式:

java
public class Person {
  private final String name;
  private final int age;

  private Person(Builder builder) {
    this.name = builder.name;
    this.age = builder.age;
  }

  public String name() {
    return name;
  }

  public int age() {
    return age;
  }

  public Person withName(String newName) {
    return new Person(new Builder().name(newName).age(this.age));
  }

  public Person withAge(int newAge) {
    return new Person(new Builder().name(this.name).age(newAge));
  }

  public static Builder builder() {
    return new Builder();
  }

  public static class Builder {
    private String name;
    private int age;

    public Builder name(String name) {
      this.name = name;
      return this;
    }

    public Builder age(int age) {
      this.age = age;
      return this;
    }

    public Person build() {
      return new Person(this);
    }
  }
}

这样,每次修改都会返回一个新的 Person 实例,而不会改变原来的数据。

进一步理解不可变对象

如果想更进一步理解不可变对象的设计模式:

  • 可以研究 Java 记录类型(Record) record 是 Java 14+ 提供的简洁方式来创建不可变对象:
java
public record Person(String name, int age) {}

这比手写 final 变量和 with 方法更方便。

  • 了解 Java Streams 和 FP 编程 不可变对象常用于 函数式编程(FP),结合 Java Streams 可以减少副作用:
java
List<String> names = persons.stream()
    .map(Person::name)
    .collect(Collectors.toList());

这避免了对象状态的变化,提高了代码可预测性。

放下包袱

书本上学到的知识在实际场景中有着不同的表现,唯有实践才能知道是否真正合适。正如常用的 Getter-Setter 模式,很多时候不管有没有用,似乎都得写上,因为这是 OOP 的“标准”。但实际开发中,我们可能需要考虑更多的因素,比如:Getter-Setter 可能会引入 不必要的可变性、破坏封装、影响并发安全。因此,在设计类时,我们应该问自己这个简单的问题:我是否应该让它不改变?

Released under the MIT License.