一、背景

以Java语言为例,说到可变的数据,就要提到函数式编程,函数式编程主要有以下概念:

Java作为编程语言的老大哥之一,是在JDK8的时候引入了函数式编程,java是一门面向对象的编程语言,在以前调用函数的时候总是需要依赖于一个对象,经常会写出匿名类这样的代码:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World");
    }
};

JDK8中引入了函数式编程接口和Lambda来简化代码:

Runnable runnable = () -> System.out.println("Hello World");

不可变性是函数式编程推崇的一个重要概念,保证数据的不可变性,从而可以让我们:开发更加简单、可回溯、测试友好,以及减少了任何可能的副作用,从而减少了Bug的出现
但是JDK8对函数式编程的支持还不够完善,比如Collector的toXXX缺少生成不可变的集合,各种集合想要初始化一个不可变的对象也比较繁琐。当然这些问题在JDK11版本中得到了大幅度改进,不仅支持了类型推断,而且还支持了各种不可变对象的初始化,极大的简化了代码,比如:

// JDK7的常规初始化 ---------- 可变集合
List<String> list = new ArrayList<>();
list.add("test01");
list.add("test02");
list.add("test03");

// JDK7的匿名内部类初始化 --- 可变集合
List<String> list = new ArrayList<>() {{
    add("test01");
    add("test02");
    add("test03");
}};

// JDK8的Stream初始化 ------ 可变集合
List<String> list = Stream.of("test01", "test02", "test03").collect(Collectors.toList());

// JDK11的.of初始化 -------- 不可变集合
var list = List.of("test01", "test02", "test03");

// JDK11的stream初始化 ----- 不可变集合
var list = Stream.of("test01", "test02", "test03").collect(Colleactors.toUnmodifiableList());

// 借助工具类:Arrays ------- 不可变集合
List<String> list = new ArrayList<>(Arrays.asList("test01", "test02", "test03"));

// 借助工具类:Collections
List<String> readList = Collections.unmodifiableList(list);

// 借助工具类:Guava
ImmutableList<String> list = ImmutableList.of("test01", "test02", "test03");

通过上面各种集合初始化的对比,相信你也能发现,JDK11对不可变性的支持也日益完善,函数式编程的很多优秀的特性在java语言得到了实现,所以还在用jdk8的小伙伴还是尽早升级,要不然jdk17都要出来了。

二、不可控的可变数据

在第一版java语言的《重构》书中,还没有发现可变数据的影子,也难怪,这本书是在2010年出版的,jdk8是在2012年第一次发布,但是随着函数式编程在这些高级语言中的应用,在2019年第二版js语言的《重构》书中,在代码的坏味道中会发现多了可变数据这一条。

下面介绍两种好用的重构手法,来避免不可控的可变数据为我们带来的麻烦。

1. 移除设置函数(Remove Setting Method)

和读数据相比,修改数据是一项危险的操作,这也就是为什么在并发编程中会有各种复杂的锁机制来保证数据的一致性。对于项目中的Model来说,setter方法就是其对外暴露不可控因素的源头,其实我们完全可以避免使用setter,通过不可变的方式来替代。详情可见3.1的代码样例部分。

2. 编写不可变类

Java中最典型的不可变类就是String类,里面的各种方法,只要涉及到字符串的变化,不会再原字符串上进行修改,而是生成一个新的字符串返回。
想要编写不可变类,也不难,只要做到以下三点:

  • 所有字段只在构造函数中初始化
  • 若发生改变,就返回一个新对象
  • 编程纯函数

三、代码案例,如何避免代码中的可变性

接下来的代码都以jdk11版本的语法为例,部分代码为伪代码只为说明逻辑:

1. 基本数据类型、包装类和String

基本数据类型 :int、long、float、double、byte、short、boolean、char
包装类 :Integer、Long、Float、Double、Byte、Short、Boolean、Character
String 本身是不可变类

// 在使用以上数据类型申请变量时, 应该尽量避免对同一个变量反复赋值
int i = toResult();
....
i = toAnotherResult();
// toAnotherResult()应该重新申请一个变量,不应该对以前的变量进行覆盖,并且不必要的变量应该进行Inline操作

还有一种情况在开发的时候会经常遇到:

// 第一种情况,if中只有一行赋值代码
String s1;
if(isRight(xxx)) {
    s1 = "test_01";
} else {
    s1 = "test_02";
}
use String s1 do something ...

// 第二种情况, if中内嵌了多行代码
String s2;
if(isRight(xxx)) {
    do something ...
    s1 = "test_01";
} else {
    do something ...
    s1 = "test_02";
}
use String s2 do something ...

上面这种情况应该在初始化的时候就给变量赋值。

// 第一种情况可以使用三目运算符来解决:
String s1 = isRight(xxx) ? "test_01" : "test_02";

// 第二种情况可以使用Extract Method(提炼函数)来解决:
String s2 = toStr(xxx);

private String toStr(xxx) {
    //使用卫语句简化if-else结构
    if(isRight(xxx)) {
        do something ...
        return "test_01";
    }
    do something ...
    return "test_02";
}

2. 构建不可变的集合

集合类型 :List、Map、Set, 下面List为例来说明

下列情况应当避免:

// 避免: 初始化可变列表
List<String> list = new ArrayList<>() {{
    add("test01");
    add("test02");
    add("test03");
}};

// 避免: 在一个方法中改变参数列表的长度
public void change(List<String> list) {
    list.add("test04");
}

// 避免: 在一个方法中改变参数列表的内部值
public void fill(List<Model> models) {
    models.foreach(model -> model.setType("new_model"));
}

构建不可变的列表:

// 初始化
var list_01 = List.of("test01", "test02", "test03", "");
var list_02 = List.of("test04", "test05", "test06", "");

// 通过stream来实现列表的合并和过滤,创建一个新的不可变集合
// 两个集合合并
var list_03 = Stream.concat(list_01.stream(), list_02.stream()).collect(Collectors.toUnmodifiableList());
// 多个集合合并
var list_04 = Stream.of(list1.stream(), list2.stream(), list3.stream()).flatMap(Function.identity()).collect(Collectors.toUnmodifiableList());
// 集合过滤
var list_05 = list_04.stream().filter(StringUtils::isNotEmpty).collect(Collectors.toUnmodifiableList());

// 集合改变内部的值生成一个新的集合,这里的model代表一个虚拟的对象
var models = List.of(model1, model2, model3);
var newModels = models.stream().map(model -> model.withType("new_model")).collect(Collectors.toUnmodifiableList());

总之: 集合搭配Stream可以进行任何变化生成新的不可变集合,没有副作用,非常的nice。

3. 构建不可变的model

@Setter是导致model可变的罪魁祸首,其实我们完全可以不使用setter来构建我们的model,可以在lombok中把setter相关的禁用掉。

lombok.setter.flagUsage = error
lombok.data.flagUsage = error

我们可以用以下注释来构建不可见的model,并且通过staticName = "of"让model的构建更加函数式化。

// 声明
@With
@Getter
@Builder(toBuilder = true)
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor(staticName = "of")
public class Model() {
    private String id;
    private String name;
    private String type;
}

// 构建Model
var model_01 = Model.of("101", "model_01", "model");

// 构建空Model
var model_02 = Model.of();

// 构建指定参数的Model
var model_03 = Moder.toBuilder().id("301").name("model_03").build();

// 修改Model的一个值,通过@With来生成一个全新的model
var model_04 = model_01.withName("model_04");

// 修改多个值,通过@Builder来生成一个全新的model
var model_05 = model_01.toBuilder.name("model_05").type("new_model").build();

四、总结

编写代码时,时刻提醒自己:控制数据的可变性 😂😂😂😂😂😂😂😂😂😂😂😂😂😂

本文内容参考来源于:极客时间专栏《软件设计之美》《代码之丑》 | 书籍《重构