immutable java_理解不可变(immutable)思想

初识不可变(immutable)

相信大家或多或少都听说过 不可变(immutable)这个词,比如 学Java的同学应该知道 final 关键字,也应该知道 String 在java中是不可变类型。

听说过ES6的前端同学,也大概知道 const 关键字。

这些都是不可变(immutable)在编程语言方面最基础的例子。

咱们先给 不可变(immutable) 下个定义:

不可变(immutable)指的是 在变量赋值或对象创建结束之后就使用者就不能再改变它的值或状态。

举例:

不可变 关键字

//Java

final int year = 2019;

// year = 2020; 报错,因为year变量声明被final修饰,不可改变

//JavaScript

const year = 2019;

// year = 2020; 报错,因为year变量声明被const修饰,不可改变

------------------

不可变对象

Java中的String是最常见的不可变对象

同学可能会说

//Java

String name = "Jobs";

name = "Cook"

name不就改变了吗?为什么说String是不可变的?

答案是 —— 对象引用(reference to object)

代码中 name 实际上是个 String对象的引用,而 "Jobs"和"Cook"都是String对象,所以 代码第二行只是改变了 name 这个引用的指向。

那我怎么证明 String 是不可变对象。

//Java

String name = "Jobs";

name.substring(1); //从第二个字符开始截取 —— 预期结果是 obs

name.replace("o","x");//把o替换成x —— 预期结果是 xbs

name.toUpperCase();//所有字符都大写 —— 预期结果是 OBS

System.out.println(name); // 依然是 Jobs

我创建了值为"Jobs"的String对象,然后我调用了 String 的 三个方法(你可以尝试调用 String 的所有方法),企图更改name的值,然而并不生效。

因为实际上 String的所有这类处理值的方法都不是更改对象自己的值,而是创建一个新的String对象作为返回值。

这个例子说明了,String暴露的所有公共方法都无法改变已经创建好的String对象的值 —— 所以,String 是不可变对象。

大家初学的时候可能都会吐槽:

“final 和 const 不让更新值,那就不得不声明新的变量,变量数目不是越来越多了吗?”

“String设计得也太麻烦了吧!复用同一个String对象,然后修改内部值,这样不是很方便吗?为什么非得创建这么多’多余‘的对象?”

不可变(immutable)的用途和意义

一, 常量(constant)

啥是常量,就是我们默认不会变的值,比如:圆周率,摄氏度转华氏度的差值。我们在还没写代码前就知道这些值了,且它们永远不会变。

//Java

/**

* The {@code double} value that is closer than any other to

* pi, the ratio of the circumference of a circle to its

* diameter.

*/

public static final double PI = 3.14159265358979323846;

二, 只读变量(read-only)

不同于常量的点在于 只读变量的值是在 程序运行时才初始化的,初始化后就不会再变化。

比如下面是 apache 工具包的一个线程工厂实现类,线程前缀初始化后就不会(也没必要)改变了。

//Java

package org.apache.http.impl.bootstrap;

import java.util.concurrent.ThreadFactory;

import java.util.concurrent.atomic.AtomicLong;

class ThreadFactoryImpl implements ThreadFactory {

private final String namePrefix;

private final ThreadGroup group;

private final AtomicLong count;

ThreadFactoryImpl(String namePrefix, ThreadGroup group) {

this.namePrefix = namePrefix; // 赋值后将不会再改变

this.group = group;

this.count = new AtomicLong();

}

public Thread newThread(Runnable target) {

return new Thread(this.group, target, this.namePrefix + "-" + this.count.incrementAndGet());

}

}

不可变关键字的上述这两种用法主要出于两个目的:增强语义,提高代码可读性 —— 阅读代码时,一看就能把常量和只读变量和其他可变变量区别开。

(拓展:不少学生/初级程序员轻视了代码的可读性,其实优秀的代码最重要的指标之一就是可读性 —— 高质量的代码结构分明,一目了然,方便扩展。可读性高的代码可维护性自然也高,即使有bug也能快速定位到。)

防止人为错误不小心修改 —— 试想一下一个数学计算程序里, PI 的值不小心被修改了,会有什么后果

三. 线程安全

后端开发中,多线程是非常重要的一个领域。

不可变对象天然支持线程安全 —— 创建成功后就不变,可读不可写,大大降低了编写多线程代码的负担。

四. 参数传递和副作用

设计良好的函数/方法通常不会更改参数对象的值,如果修改了就叫发生了副作用(side effect)。

有副作用的函数/方法经常会产生一些隐晦的bug —— 一个对象作为参数传入了一些函数/方法,却被故意/无意地修改了其中的值,然而代码的其他部分并不知情,就会产生意料之外的bug。

根本的解决办法就是避免或重构这类有副作用的函数/方法,但是执行起来却并不容易或者容易有漏网之鱼。

一个有益的办法就是将作为参数的对象类设计成不可变的,在编码和编译时期就直接避免了这个问题。

五. interning 和 object pooling(对象池)

跟不可变对象通常一起使用的策略叫interning(不确定中文翻译叫什么) 或者 object pooling(对象池),Java String 就使用了这种策略。

Java 程序中的大多数String literals 字面量(尤其是比较短的,且使用频繁的)都被存放在了一个 String pool里,下面代码中的s1 和 s2 实际上都指向String pool 里的同一个 String 对象。可以做个小实验来验证

//Java

String s1 = "Hello";

String s2 = "Hello";

System.out.println(s1 == s2); // 输出 true

当然,我不是让大家以后都用 == 来比较 String 值 (有兴趣的同学可以自行搜索 == 和 equals() 方法) 比如下面的代码中 == 就判断为 false了, 因为 s1 和 s2 这次都不在 String pool里。

//Java

String s1 = new String("Hello"); // 非字面量

String s2 = new String("Hello");

System.out.println(s1 == s2); // 输出 false

六. 不可变对象的变化和比较优势

由于不可变对象创建后值就不再变化,想“更改”它的值只有一个办法 —— 创建一个新的对象。如果“修改的逻辑”处理之后不发生任何变化,那么就把原对象返回。

上述实验中 String 的那些方法(trim, substring, upperCase ..) 就是这么做的。

这种设计的好处就是 —— 直接比较原对象和新对象的引用地址就可以知道是否发生变化,大大提高了比较的性能。

前端领域里, react 和 redux 就采用了这种设计:redux store 内的 state 可以直接使用 === 来比较,确认是否发生了变化。React 的 state, props 也是如此。

痛点

1. 很多时候,不可变对象的设计都会导致更多的对象被创建,造成了一定的内存压力,但事实上,这点内存压力并不会对性能造成太大影响。相比不可变带来的好处而言,这不算什么。

2. 创建不可变对象有时候需要会写更多的代码

比如在 redux 或 react 中,假设有一下 state 定义

/JavaScript

const state = {

a: "jobs",

b: {

e: {

f: {

g: "hello",

y: "omg"

},

k: 3

},

i: 2

},

h: "deep"

}

我们要基于state生成一个新的state,且更改 g 的值,用原生ES6语法就得这么写

//JavaScript

const newState = {

...state,

b: {

...state.b,

e: {

...state.b.e,

f: {

...state.b.e.f,

g: "new hello"

}

}

}

}

这种深层嵌套的对象简直是噩梦。

好在业界有人早就发现了这种问题,有不少解决方案,比如immer.js

使用了 immer.js 之后,上述代码可以变成这么写, 是不是轻松多了!

import produce from "immer"

const newState = produce(state, draftState => {

draftState.b.e.f.g="new hello"

})

关于 不可变(immutable)思想,本文就分享到这里了。如果有兴趣,可以进一步去搜索相关内容,然后尝试在自己的项目中使用起来吧!

本文摘自我的公众号文章理解不可变(immutable)思想​mp.weixin.qq.com3812d69ef5e936045fe59ef057c2ea2a.png

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值