03 | 档案类:怎么精简地表达不可变数据?

你好,我是范学雷。今天,我们聊一聊Java的档案类。

档案类这个特性,首先在JDK 14中以预览版的形式发布。在JDK 15中,改进的档案类再次以预览版的形式发布。最后,档案类在JDK 16正式发布

那么,什么是档案类呢?档案类的英文,使用的词汇是“record”。官方的说法,Java档案类是用来表示不可变数据的透明载体。这样的表述,有两个关键词,一个是不可变的数据,另一个是透明的载体。

该怎么理解“不可变的数据”和“透明的载体”呢?我们还是通过案例和代码,一步一步地来拆解、理解这些概念。

阅读案例

在面向对象的编程语言中,研究表示形状的类是一个常用的教学案例。今天的评审案例,我们从形状的子类圆形开始,来看一看面向对象编程实践中,这个类的设计和演化。

下面的这段代码,就是一个简单的、典型的圆形类的定义。这个抽象类的名字是Circle。它有一个私有的变量radius,用来表示圆的半径。有一个构造方法,用来生成圆形的实例。有一个设置半径的方法setRadius,一个读取半径的方法getRadius。还有一个重写的方法getArea,用来计算圆形的面积。

package co.ivi.jus.record.former;

public final class Circle implements Shape {
    private double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
    
    public double getRadius() {
        return radius;
    }
    
    public void setRadius(double radius) {
        this.radius = radius;
    }
}

这个圆形类之所以典型,是因为它交代了面向对象设计的关键思想,包括面向对象编程的三大支柱性原则:封装、继承和多态。

封装的原则是隐藏具体实现细节,实现的修改不会影响接口的使用。Circle类中,表示半径的变量被定义成私有的变量。我们可以改变半径这个变量的名字,或者不使用半径而是使用直径来表示圆形。这样的实现细节的变化,并不会影响公开方法的调用。

由于需要隐藏内部实现细节,所以需要设计公开接口来访问类的相关特征,比如例子中的圆形的半径。所以上面的例子中,设置半径的方法setRadius和读取半径的方法getRadius,就显得显而易见,并且顺理成章。在面向对象编程的教科书里,以及Java的标准类库里,我们可以看到很多类似的设计。

可是,这样的设计有哪些严重的缺陷呢?花点时间想想你能找到的问题,然后我们接下来再继续分析。

案例分析

上面这个例子,最重要的问题,就是它的接口不是多线程安全的。如果在一个多线程的环境中,有些线程调用了setRadius方法,有些线程调用getRadius方法,这些调用的最终结果是难以预料的。这也就是我们常说的多线程安全问题。

在现代计算机架构下,大多数的应用需要多线程的环境。所以,我们通常需要考虑多线程安全的问题。 该怎么解决上面例子中的多线程安全问题呢?如果上述例子的实现源代码不能更改,那么就需要在调用这些接口的程序中,增加线程同步的措施。

synchronized (circleObject) {
    double radius = circleObject.getRadius();
    // do something with the radius.
}

遗憾的是,在调用层面解决线程同步问题的办法,并不总是显而易见的。不论多么资深的程序员,都有可能疏漏、忘记或者没有正确地解决好线程同步的问题。

所以,通常地,为了更皮实的接口设计,在接口规范设计的时候,就应该考虑解决掉线程同步的问题。比如说,我们可以把上面案例中的代码改成线程安全的代码。对于Circle类,只需要把它的公开方法都设置成同步方法,那么这个类就是多线程安全的了。具体的实现,请参考下面的代码。

package co.ivi.jus.record.former;

public final class Circle implements Shape {
    private double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    @Override
    public synchronized double getArea() {
        return Math.PI * radius * radius;
    }
    
    public synchronized double getRadius() {
        return radius;
    }
    
    public synchronized void setRadius(double radius) {
        this.radius = radius;
    }
}

可是,线程同步并不是免费的午餐。代价有多大呢?我做了一个简单的性能基准测试,哪怕最简单的同步,比如上面代码里同步的getRadius方法,它的吞吐量损失也有十数倍。这相当于说,如果没有同步的应用需要一台机器支持的话,加了同步的应用就需要十多台机器来支撑相同的业务量。

这样的代价就有点大了,我们需要寻找更好的办法来解决多线程安全的问题。最有效的办法,就是在接口设计的时候,争取做到即使不使用线程同步,也能做到多线程安全。这说起来还是有点难以理解的,我们还是来看看代码吧。

下面的代码,是一个修改过的Circle类实现。在这个实现里,圆形的对象一旦实例化,就不能再修改它的半径了。相应地,我们删除了设置半径的方法。也就是说,这个对象是一个只读的对象,不支持修改。通常地,我们称这样的对象为不可变对象。

package co.ivi.jus.record.immute;

public final class Circle implements Shape {
    public final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

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

对于只读的圆形类的设计,我们可以看到两个好处。

第一个好处,就是天生的多线程安全。因为这个类的对象,一旦实例化就不能再修改,所以即便在多线程环境下使用,也不需要同步。而不可变对象所承载的数据,比如上面例子中圆形的半径,就是我们前面所说的不可变的数据。这个不可变,是有一个界定范围的。这个界定范围,就是它所在对象的生命周期。如果跳出了对象的生命周期,我们可以重新生成新对象,从而实现数据的变化。

第二个好处,就是简化的代码。只读对象的设计,使得我们可以重新考虑代码的设计,这是代码简化的来源。你可能已经注意到了,在这个实现里,我们还删除了读取半径的方法。取而代之的,是公开的半径这个变量。这就是一个最直接的简化。

应用程序可以直接读取这个变量,而不是通过一个类似于getRadius的方法。由于半径这个变量被声明为final变量,所以它只可以被读取,不能被修改。这并没有破坏对象的只读性。

不过,乍看之下,这样的设计似乎破坏了面向对象编程的封装原则。公开半径变量radius,相当于公开的实现细节。如果我们改变主意,想使用直径来表示一个圆形,那么实现的修改就会显得很丑陋。

可是,如果我们认真思考一下几个简单的问题,对于封装的顾虑可能就降低很多了。比如说,使用直径来表示一个圆,这是一个真实的需求吗? 这是一个必需的表达方式吗?未来的圆,会不会变得没法使用半径来表达?其实不是的,未来的圆,还是可以用半径来表达的。使用其他的办法,比如直径,来表达一个圆,其实并没有必要。

所以,公开半径这个只读变量,并没有带来违反封装原则的实质性后果。而且,从另外一个角度来看,我们可以把读取这个只读变量的操作,看成是等价的读取方法的调用。不过,虽然很多人,包括我自己,倾向于这样解读,但是这总归是一个有争议的形式。

进一步的简化

还有没有进一步简化的空间呢?我们再来看看不可变的正方形Square类的设计。具体的实现,请参考下面的代码。

package co.ivi.jus.record.immute;

public final class Square implements Shape {
    public final double side;

    public Square(double side) {
        this.side = side;
    }

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

如果比较一下不可变的圆形Circle类和正方形Square类的源代码,你有没有发现这两个类的代码有惊人的相似点?

第一个相似的地方,就是使用公开的只读变量(使用final修饰符来声明只读变量)。Circle类的变量radius,和Square类的变量side,都是公开的只读的变量。这样的声明,是为了公开变量的只读性。

第二个相似的地方,就是公开的只读变量,需要在构造方法中赋值,而且只在构造方法中赋值,且这样的构造方法还是公开的方法。Circle类的构造方法给radius变量赋值,Square类的构造方法给side变量赋值。这样的构造方法,解决了对象的初始化问题。

第三个相似的地方,就是没有了读取的方法;公开的只读变量,替换了掉了公开的读取方法。这样的变化,使得代码量总体变少了。

这么多相似的地方,相似的代码,能不能进一步地简化呢?我知道,你可能已经开始思考这样的问题了。

对于这个问题,Java的答案,就是使用档案类。

怎么声明档案类

我们前面说过,Java档案类是用来表示不可变数据的透明载体。那么,怎么使用档案类来表示不可变数据呢?

我们还是一起先来看看代码吧。咱们试着把上面不可变的圆形Circle普通的类改成档案类,来感受下档案类到底是什么模样的。

package co.ivi.jus.record.modern;

public record Circle(double radius) implements Shape {
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

看到这样的代码,是不是有点出乎意料?你可以对比一下不可变的Circle类的代码,感受一下这两者之间的差异。

首先,最常见的class关键字不见了,取而代之的是record关键字。record关键字是class关键字的一种特殊表现形式,用来标识档案类。record关键字可以使用和class关键字差不多一样的类修饰符(比如public、static等;但是也有一些例外,我们后面再说)。

然后,类标识符Circle后面,有用小括号括起来的参数。类标识符和参数一起看,就像是一个构造方法。事实上,这样的表现方式,的确可以看成是构造方法。而且,这种形式,还就是当作构造方法使用的。比如下面的代码,就是使用构造方法的形式来生成Circle档案类实例的。

Circle circle = new Circle(10.0);

最后,在大括号里,也就是档案类的实现代码里,变量的声明没有了,构造方法也没有了。前面我们已经知道怎么生成一个档案类实例了,但还有一个问题是,我们能读取这个圆形档案类的半径吗?

其实,类标识符声明后面的小括号里的参数,就是等价的不可变变量。在档案类里,这样的不可变变量是私有的变量,我们不可以直接使用它们。但是我们可以通过等价的方法来调用它们。变量的标识符就是等价方法的标识符。比如下面的代码,就是一个读取上面圆形档案类半径的代码。

double radius = circle.radius();

是的,在档案类里,方法调用的形式又回来了。我们前面讨论过打破封装原则的顾虑,你可能还是没有足够的信心去接受不完整的封装形式。那么现在,档案类的调用形式依然保持着良好的封装形式。打破封装原则的顾虑也就不复存在了。

需要注意的是,由于档案类表示的是不可变数据,除了构造方法之外,并没有给不可变变量赋值的方法。

意料之外的改进

上面,通过传统Circle类和档案Circle类代码的对比,我们可以感受到档案类在简化代码、提高生产力方面的努力。如果说,上面这些简化,还在我的预料之内的话;下面的简化,我刚看到的时候,是很惊喜的:“哇,这真是太奇妙了!”

我们还是通过代码来体验一下这种感受。如果我们生成两个半径为10厘米的圆形的实例,这两个实例是相等的吗?下面的代码,就是用来验证我们猜想的。你可以试着运行一下,看看和你猜想的结果是不是一样的。

package co.ivi.jus.record;

import co.ivi.jus.record.immute.Circle;

public class ImmuteUseCases {
    public static void main(String[] args) {
        Circle c1 = new Circle(10.0);
        Circle c2 = new Circle(10.0);

        System.out.println("Equals? " + c1.equals(c2));
    }
}

上面的代码里,使用了我们开篇案例分析中的传统Circle类。运行结果告诉我们,两个半径为10厘米的圆形的实例,并不是相等的实例。我想这应该在你的预料之内。

如果需要比较两个实例是不是相等,我们需要重写equals方法和hashCode方法。如果需要把实例转换成肉眼可以阅读的信息,我们需要重写toString方法。我们上面案例分析的代码中,这些方法都没有重写,因此对应的操作结果也是不可预测的。

当然,如果没有遗忘,我们可以添加这三个方法的重写实现。然而,这三个方法的重写,尤其是equals方法和hashCode方法的重写实现,一直是代码安全的重灾区。即便是经验丰富的程序员,也可能忘记重写这三个方法;就算没有遗忘,equals方法和hashCode方法也可能没有正确实现,从而带来各种各样的问题。这实在难以让人满意,但是一直以来,我们也没有更好的办法。

档案类会不一样吗?

我们再来看看使用档案类的代码,结果会不会不一样呢? 下面的这段代码,Circle的实现使用的是档案类。这段代码运行的结果告诉我们,两个半径为10厘米的圆形的档案类实例,是相等的实例。

package co.ivi.jus.record;

import co.ivi.jus.record.modern.Circle;

public class ModernUseCases {
    public static void main(String[] args) {
        Circle c1 = new Circle(10.0);
        Circle c2 = new Circle(10.0);

        System.out.println("Equals? " + c1.equals(c2));
    }
}

看到这里,你是不是感觉到:哇! 这真的是太棒了!我们并没有重写这三个方法,它们居然可以使用。

为什么会这样呢?

这是因为,档案类内置了缺省的equals方法、hashCode方法以及toString方法的实现。一般情况下,我们就再也不用担心这三个方法的重写问题了。这不仅减少了代码数量,提高了编码的效率;还减少了编码错误,提高了产品的质量。

不可变的数据

讨论到这里,我们可以回头再看看Java档案类的定义了:Java档案类是用来表示不可变数据的透明载体。“不可变的数据”和“透明的载体”是两个最重要的关键词。

我们前面讨论了不可变的数据。如果一个Java类一旦实例化就不能再修改,那么用它表述的数据就是不可变数据。Java档案类就是表述不可变数据的。为了强化“不可变”这一原则,避免面向对象设计的陷阱,Java档案类还做了以下的限制:

  1. Java档案类不支持扩展子句,用户不能定制它的父类。隐含的,它的父类是java.lang.Record。父类不能定制,也就意味着我们不能通过修改父类来影响Java档案的行为。
  2. Java档案类是个终极(final)类,不支持子类,也不能是抽象类。没有子类,也就意味着我们不能通过修改子类来改变Java档案的行为。
  3. Java档案类声明的变量是不可变的变量。这就是我们前面反复强调的,一旦实例化就不能再修改的关键所在。
  4. Java档案类不能声明可变的变量,也不能支持实例初始化的方法。这就保证了,我们只能使用档案类形式的构造方法,避免额外的初始化对可变性的影响。
  5. Java档案类不能声明本地(native)方法。如果允许了本地方法,也就意味着打开了修改不可变变量的后门。

通常地,我们把Java档案类看成是一种特殊形式的Java类。除了上述的限制,Java档案类和普通类的用法是一样的。

透明的载体

好了,聊完“不可变的数据”,接下来该聊聊“透明的载体”了。

陆陆续续地,我们在前面提到过,档案类内置了下面的这些方法缺省实现:

  • 构造方法
  • equals方法
  • hashCode方法
  • toString方法
  • 不可变数据的读取方法

如果你注意到的话,我们使用了“缺省”这样的字眼。换一种说法,我们可以使用缺省的实现,也可以替换掉缺省的实现。下面的代码,就是我们试图替换掉缺省实现的尝试。请注意,除了构造方法,其他的替换方法都可以使用Override注解来标注(如果你读过《代码精进之路》,你就会倾向于总是使用Override注解的)。

package co.ivi.jus.record.explicit;

import java.util.Objects;

public record Circle(double radius) implements Shape {
    public Circle(double radius) {
        this.radius = radius;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
    
        if (o instanceof Circle other) {
            return other.radius == this.radius;
        }

        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(radius);
    }

    @Override
    public String toString() {
        return String.format("Circle[radius=%f]", radius);
    }

    @Override
    public double radius() {
        return this.radius;
    }
}

到这里,你应该明白了“透明的载体”的意思了。透明载体的意思,通俗地说,就是档案类承载有缺省实现的方法,这些方法可以直接使用,也可以替换掉。

不过,像上面这样的替换,除了徒增烦恼,是没有实际意义的。那我们什么时候需要替换掉缺省实现呢?

重写构造方法

最常见的替换,是要在构造方法里对档案类声明的变量添加必要的检查。比如说,我们现实生活中看到的各种各样的圆形,它的半径都不会是负数。如果在这样的场景里来讨论圆形,那么表示圆形的类的半径就不应该是负数。

你应该已经意识到了,我们上面的代码,在实例化的时候,都没有检查半径的数值,包括档案类缺省的构造方法。那么这时候,我们就要替换掉缺省的构造方法。下面的代码,就是一种替换的方法。如果,构造实例的时候,半径的数值为负,构造就会抛出运行时异常IllegalArgumentException

package co.ivi.jus.record.improved;

public record Circle(double radius) implements Shape {
    public Circle {
        if (radius < 0) {
            throw new IllegalArgumentException(
                "The radius of a circle cannot be negative [" + radius + "]");
        }
    }

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

如果你阅读了上面的代码,应该已经注意到了一点不太常规的形式。构造方法的声明没有参数,也没有给实例变量赋值的语句。这并不是说,构造方法就没有参数,或者实例变量不需要赋值。实际上,为了简化代码,Java编译的时候,已经替我们把这些东西加上去了。所以,不论哪一种编码形式,构造方法的调用都是没有区别的。

在上一个例子中,我们已经看到了构造方法的常规形式。在下面这张表里,我列出了两种构造方法形式上的差异,你可以看看它们的差异。

图片

重写equals方法

还有一类常见的替换,如果缺省的equals方法或者hashCode方法不能正常工作或者存在安全的问题,就需要替换掉缺省的方法。

如果声明的不可变变量没有重写equals方法和hashCode方法,那么这个档案类的equals方法和hashCode方法的行为就可能不是可以预测的。比如,如果不可变的变量是一个数组,通过下面的例子,我们来看看它的equals方法能不能正常工作。

jshell> record Password(byte[] password) {};
|  modified record Password

jshell> Password pA = new Password("123456".getBytes());
pA ==> Password[password=[B@2ef1e4fa]

jshell> Password pB = new Password("123456".getBytes());
pB ==> Password[password=[B@b81eda8]

jshell> pA.equals(pB);
$16 ==> false

这个例子里,我们设计了一个口令的档案类,其中的口令使用字节数组来存放。我们使用同样的口令,生成了两个不同的实例。然后,我们调用equals方法,来比较这两个实例。

运算的结果显示,这两个实例并不相等。这不是我们期望的结果。其中的原因,就是因为数组这个变量的equals方法并不能正常工作(或者换个说法,数组变量没有重写equals方法)。

如果把变量的类型换成重写了equals方法的字符串String,我们就能看到预期的结果了。

jshell> record Password(String password) {};
|  created record Password

jshell> Password pA = new Password("123456");
pA ==> Password[password=123456]

jshell> Password pB = new Password("123456");
pB ==> Password[password=123456]

jshell> pA.equals(pB);
$5 ==> true

一般情况下,equals方法和hashCode方法是成双成对的,实现逻辑上需要匹配。所以,当我们重写equals方法的时候,一般也需要重写hashCode方法;反之亦然。

不推荐的重写

为了更个性化的显示,我们有时候也需要重写toString方法。但是,我们通常不建议重写不可变数据的读取方法。因为,这样的重写往往意味着需要变更缺省的不可变数值,从而打破实例的状态,进而造成许多无法预料的、让人费解的后果。

比如说,我们设想定义一个数,如果是负值的话,我们希望读取的是它的相反数。下面的例子,就是一个味道很坏的示范。

jshell> record Number(int x) {
   ...>     public int x() {
   ...>         return x > 0 ? x : (-1) * x;
   ...>     }
   ...> }
|  created record Number

jshell> Number n = new Number(-1);
n ==> Number[x=-1]

jshell> n.x();
$9 ==> 1

jshell> Number m = new Number(n.x());
m ==> Number[x=1]

jshell> m.equals(n);
$11 ==> false

在这个例子里,我们重写了读取的方法。如果一个数是负数,重写的读取就返回它的相反数。读取出来的数据,并不是实例化的时候赋于的数据。这让代码变得难以理解,很容易出错。

更严重的问题是,这样的重写不再能够支持实例的拷贝。比如说,我们把实例n拷贝到另一个实例m。这两个实例按照道理来说应该相等。而由于重写了读取的方法,实际的结果,这两个实例是不相等的。这样的结果,也可能会使代码容易出错,而且难以调试。

总结

好,今天就到这里,我来做个小结。从前面的讨论中,我们了解到,Java档案类是用来表示不可变数据的透明载体,用来简化不可变数据的表达,提高编码效率,降低编码错误。同时,我们也讨论了使用档案类的几个容易忽略的陷阱。

在我们日常的接口设计和编码实践中,为了最大化的性能,我们应该优先考虑使用不可变的对象(数据);如果一个类是用来表述不可变的对象(数据),我们应该优先使用Java档案类。

如果要丰富你的代码评审清单,有了封闭类后,你可以加入下面这一条:

一个类,如果是用来表述不可变的数据,能不能使用Java档案类?

另外,通过今天的讨论,我拎出几个技术要点,这些都可能在你们面试中出现哦,通过学习,你应该能够:

  • 知道Java支持档案类,并且能够有意识地使用档案类,提高编码效率,降低编码错误;
    • 面试问题:你知道档案类吗?会不会使用它?
  • 了解档案类的原理和它要解决的问题,知道使用不可变的对象优势;
    • 面试问题:什么情况下可以使用档案类,什么情况下不能使用档案类?
  • 了解档案类的缺省方法,掌握缺省方法的好处和不足,知道什么时候要重写这些方法。
    • 面试问题:使用档案类应该注意什么问题?

如果你能够有意识地使用不可变的对象以及档案类,并且有能力规避掉其中的陷阱,你应该能够大幅度提高编码的效率和质量。毫无疑问,在面试的时候,这也是一个能够让你脱颖而出的知识点。

思考题

在重写equals方法这一小节里,我们讨论了数组类型的不可变数据。我们已经知道了,这样的数据类型,需要重写equals方法和hashCode方法。其实,toString()的方法也需要重写。今天的思考题,就是请你实现这些方法的重写。

方便起见,我们假设这个数组是字节数组,用来表示社会保障号。我们都知道,社会保障号是高度敏感的信息,不能被泄漏,也不能被盗取。你来想一想,有哪些方法需要重写?为什么?代码看起来是什么样子的?有难以克服的困难吗?

我开个头,写一个空白的档案类,你来把你想添加的代码补齐。

record SocialSecurityNumber(byte[] ssn) {
  // Here is your code.
}

欢迎你在留言区留言、讨论,分享你的阅读体验以及对这些问题的思考。

注:本文使用的完整的代码可以从GitHub下载,你可以通过修改GitHubreview template代码,完成这次的思考题。如果你想要分享你的修改或者想听听评审的意见,请提交一个 GitHub的拉取请求(Pull Request),并把拉取请求的地址贴到留言里。这一小节的拉取请求代码,请在档案类专用的代码评审目录下,建一个以你的名字命名的子目录,代码放到你专有的子目录里。比如,我的代码,就放在record/review/xuelei的目录下面。

精选留言

  • Jxin

    2021-11-21 00:40:00

    1.这不是kt吗。看来java也在吸收派生语言的基因啊,不论优秀与否。
    2.这玩意,很遗憾,节省不了太多代码工作。虽然我处理不了这个bug,但我写了自动检测和修复数据的外挂,无需为bug产生的问题花费时间。jdk8我们虽然没有语言层面的支持,但我们有idea和第三方工具包呀。我没办法用 record, 但我可以一键生成我要的不可变类,包括需要的缺省实现。(代码会比较冗余?其实也还好,因为像缺省实现这种都是可以做编译期代码增强的,在类上其实就是挂个注解)
    3.能处理bug总是比挂个自动修复来得要好。所以语言既然支持了,当然是好事。就是不知道我还能肆无忌惮的往属性上挂注解不,毕竟我的校验可是跟对象走的。后面可以试试。
    4.核心内容可能3分钟就讲完,但这篇讲了22分钟。不确定对新手同学的体验是否会比较好,可能调研下用户体验再看是否要调整行文风格会好些。毕竟专栏的关键是听众听的逻辑,而不是作者写的逻辑。精细是好事,但要当心越描越黑。脱裤子就一句话,大家能懂。但如果拆解成,双手举到腰间,握紧皮带,推至膝盖,重心微微右移,缓缓弯曲左脚。关联上反而费劲了。
    课后题
    1.社会保障🐎如果直接用数组表示就属于基础类型偏执,是坏味道,所以这里封装了一个类。但活学活用好像也没用,record 的声明不能直接解决问题,毕竟数组上的元素变动不改变数组对象本身的引用。还是得自己加代码,看是禁止写操作还是写时复制。
    2.不能直接继承也是个问题,如果要想实现自动扩容,不能直接继承list的实现子类。得自个重写。
    作者回复

    谢谢大侠订阅我写的小专栏,还码了这么多字,不胜惶恐。

    1. 不是。
    2. 长见识了。
    3. 又长见识了。
    4. 多谢言传身教。

    2021-11-21 13:02:38

  • LeaveStyle

    2021-11-23 15:42:58

    在没有record之前,我们在工作中一般使用lombok来帮助我们定义model,在lombok的config文件中禁用掉了setter相关,让model为只读状态。如果想修改model中的字段值,我们一般两种方式:
    1. @With 修改单值 -> 返回一个新对象
    2. @Builder(toBuilder = true) 修改多个值 -> 返回一个新对象

    现在有了record能进一步简化一些代码,继续使用@With和@Builder来修改数据值。
    作者回复

    今天我学到新东西了,谢谢!

    2021-11-23 23:38:26

  • ABC

    2021-11-19 23:02:44

    java.time整个包都被设计成了不可变类,而且代码编码方式和本文中的不可变类设计如出一辙,想请问老师,预测一下,OpenJDK后面会有计划用封闭类重构java.time包吗?
    作者回复

    是这样的,但是重构的话,短期看可能性不太大,主要是太忙了。估计要到JDK 21或者25的时候,Java语言会重上一个新台阶。以后,可能才会考虑重构很多标准类库。

    2021-11-20 00:38:42

  • 2021-11-19 16:05:53

    多属性的时候怎么用呢?比如有十几个甚至几十个属性,定义时写在名称后的小括号里会很长,不直观,初始化的时候通过构造方法的参数赋值,也会很长不直观
    作者回复

    如果属性很多,可能就要考虑是不是需要拆分了。

    2021-11-20 00:31:21

  • ABC

    2021-11-19 22:59:31

    在想一个问题: Java 8在写java.time包的时候,要是知道会增加封闭类这个特性.会不会简化不少代码?
    作者回复

    我想这是确定的。不仅仅是java.time,很多包的设计都会不同,代码也会减少很多。

    2021-11-20 00:35:50

  • 雷霹雳的爸爸

    2021-11-19 11:37:43

    对数组和集合field得保护性复制,或者对集合做不可修改得处理,是不是还得考虑复写读取方法
    作者回复

    是的,这是两个容易出问题的数据结构。

    2021-11-19 16:33:12

  • 拉欧

    2022-05-17 11:48:47

    这不就是scala里面的case class吗,看来scala就是给java探路用的吧
    作者回复

    哈哈,scala里有很多好玩的简化。

    2022-05-18 06:23:15

  • 码农Kevin亮

    2021-11-23 09:00:57

    如果类有多个构造方法,档案类是不是不支持呢?
    作者回复

    档案类用来简洁地表达不可变类,目前还没有看到支持多个构造方法的必要性。

    2021-11-23 17:52:37

  • TableBear

    2021-11-20 14:58:37

    看来要下载个JDK17才能完成课后思考题了😂
    作者回复

    建议使用JDK 17, 并且使用预览版模式。

    2021-11-20 23:07:46

  • bigben

    2021-11-20 02:05:11

    1,很想知道lombok的@Builder能总在档案类上吗?
    2,jackson的@JsonProperty等注解加在哪里?
    3,方法名不符合javabean的规范,转json等能正常支持吗?
    作者回复

    你说的这些词汇我几乎是都不懂。小伙伴们,一起来帮帮忙吧!

    2021-11-20 16:16:19

  • meanless

    2021-11-20 00:03:57

    似乎和kotlin的data class几乎一模一样
  • ifelse

    2022-10-07 16:44:23

    学习打卡
  • leaf

    2021-12-18 13:35:06

    请问这个档案类在jvm里面是怎么实现的, 可以作为值类型吗?还是只是语法糖?
    作者回复

    都不是

    2021-12-20 11:57:20

  • 王硕🤖

    2021-11-25 10:02:16

    这个重载(Overload)是不是说的不对啊。应该是重写(Override)吧。
    作者回复

    嗯, 我一直傻傻分不清楚常用的翻译是什么。 谢谢指正, 我看看能不能改过来。

    2021-11-25 14:16:52

  • Craig

    2021-11-24 18:14:29

    目前来看java越来越scala了。这个和scala的 case class差不多吧。
    作者回复

    还有人说像Kotlin

    2021-11-25 00:25:00

  • 时光勿念

    2021-11-22 10:08:28

    能自定义缺省的toString()方法嘛,希望以json的形式打印toString()。
    作者回复

    可以, 覆盖缺省的方法就行。

    2021-11-22 16:15:43

  • fatme

    2021-11-21 19:02:04

    将来会增加返回新对象的 setter 方法吗?语义类似这样:
    public class Circle {
    private final int radius;

    public Circle(int radius) {
    this.radius = radius;
    }

    public int radius() {
    return this.radius;
    }

    public Circle setRadius(int radius) {
    return new Circle(radius);
    }
    }
    作者回复

    setter方法破坏了对象的不可变性,而使用不可变对象,是目前优先的选择。上面的代码,并没有破坏不可变性,不过现有的设计已经能够表达这种需要了,直接使用“new Circle(radius)”就好了。

    2021-11-22 10:02:24

  • bigben

    2021-11-20 02:11:50

    限制有点多啊,适用范围有限。
    作者回复

    哦,其实这些限制,都是好的限制,有助于写出更好的代码。我想,大部分的代码都不需要去突破这些限制。

    2021-11-20 16:17:17

  • 往事,优雅而已

    2021-11-19 23:17:34

    减少了不少代码量😇 编译后应该也是生成和原本类似的代码 有时间我去试下
    作者回复

    嗯,试试看,回来更新一下你的发现。

    2021-11-20 01:34:06

  • 念头通达

    2021-11-19 21:45:38

    这玩意给我一种工具类归档的感觉。
    xxutils xxkit xxtool 变更为 record
    作者回复

    可以这么用,但是不限于工具类归档。

    2021-11-20 00:34:36