MIT 6.031 Software Construction 學習筆記:(三) Mutability & Immutability
這節主要是講 可變物件給程式設計帶來的危害,所謂不可變物件,就是整個生命週期中不可變的物件(廢話), e.g. : String
Risks of mutation
risk1:passing mutable values
看以下兩段程式碼:
/** @return the sum of the numbers in the list */
public static int sum(List<Integer> list) {
int sum = 0;
for (int x : list)
sum += x;
return sum;
}
/** @return the sum of the absolute values of the numbers in the list */
public static int sumAbsolute(List<Integer> list) {
// let's reuse sum(), because DRY, so first we take absolute values
for (int i = 0; i < list.size(); ++i)
list.set(i, Math.abs(list.get(i)));
return sum(list);
}
// meanwhile, somewhere else in the code...
public static void main(String[] args) {
// ...
List<Integer> myData = Arrays.asList(-5, -3, -2);
System.out.println(sumAbsolute(myData));
System.out.println(sum(myData));
}
- 可變性帶來了一個潛藏的bug(passing mutable objects around is a latent bug)
- 可讀性非常不好,作為讀者,見到
main
方法的第一印象應該是sumAbs()
是求絕對值,而sum
是求和,而這導致了一個非常深的bug - 破壞了 fail-fast 原則, 往往很久才能定位真正的bug
Safe from bugs? In this example, it’s easy to blame the implementer of sumAbsolute() for going beyond what its spec allowed. But really, passing mutable objects around is a latent bug. It’s just waiting for some programmer to inadvertently mutate that list, often with very good intentions like reuse or performance, but resulting in a bug that may be very hard to track down.
Easy to understand? When reading main(), what would you assume about sum() and sumAbsolute()? Is it clearly visible to the reader that myData gets changed by one of them?
那麼怎麼解決呢?
至少可以做到兩點,
- 儘量在不可變的物件前加
final
修飾 - 會改變引數的時候,在規範(specfication)中說明
risk2: returning mutable values
先看下面一段程式碼:
/** @return the first day of spring this year */
public static Date startOfSpring() {
if (groundhogAnswer == null) groundhogAnswer = askGroundhog();
return groundhogAnswer;
}
private static Date groundhogAnswer = null;
這段程式碼在求春天的第一天的時候加了一個 cache, 並且return 了一個可變物件。
如果有如下的client 做以下呼叫:
// somewhere else in the code...
public static void partyPlanning() {
// let's have a party one month after spring starts!
Date partyDate = startOfSpring();
partyDate.setMonth(partyDate.getMonth() + 1);
// ... uh-oh. what just happened?
}
那麼很顯然以後再次呼叫 startOfSpring
原來的靜態私有變數 groundhogAnswer
就被丟擲了。
至少有兩種解決方案可解決這個問題:
- 不用mutable 的
date
而是用imutable 的替代物: package java.time: LocalDateTime, Instant, - defensive copying pattern 也就是說返回的時候返回
groudAns
的副本return new Date(groundhogAnswer.getTime());
不過第二種方法雖然解決的問題,可是在大多數情況下我們都只需要一個共享的 groudAns
因此會帶來一些效能上的問題。
Aliasing is what makes mutable types risky
重點說一些這個可變物件多出引用的問題,如果可變物件僅在一個區域性被一個變數引用,那麼用可變物件肯定是沒有什麼危害的,可是大多數情況下,可變物件被多出引用,這樣就導致了可變物件引入的bug
Useful immutable types
- he primitive types and primitive wrappers are all immutable. If you need to compute with large numbers, BigInteger and BigDecimal are immutable.
- Don’t use mutable
Date
s, use the appropriate immutable type from java.time based on the granularity of timekeeping you need.
Summary
The key design principle here is immutability: using immutable objects and unreassignable variables as much as possible. Let’s review how immutability helps with the main goals of this course:
-
Safe from bugs. Immutable objects aren’t susceptible to bugs caused by aliasing. Unreassignable variables always point to the same object.
-
Easy to understand. Because an immutable object or unreassignable variable always means the same thing, it’s simpler for a reader of the code to reason about — they don’t have to trace through all the code to find all the places where the object or variable might be changed, because it can’t be changed.
-
Ready for change. If an object or a variable can’t be changed at runtime, then code that depends on that object or variable won’t have to be revised when the program changes.
關於mutable物件帶來的更多危害,可見下面的原文:
reference