前言
同事在一次偶然的Debug過(guò)程中吃衅,發(fā)現(xiàn)了一件不可思議的事情,程序在Run和Debug模式下運(yùn)行結(jié)果居然不一致!
問(wèn)題復(fù)現(xiàn)
測(cè)試代碼很簡(jiǎn)單,堪比Hello world留瞳。
public class Main {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("123.455");
System.out.println("結(jié)果是 -> " + a);
}
}
這段程序會(huì)輸出什么悯嗓?
腦編譯一波件舵,輸出結(jié)果結(jié)果是 -> 123.455
。
IDEA中Run模式跑一把兒脯厨,驗(yàn)證一下
一切看起來(lái)很合理铅祸,不過(guò),我對(duì)BigDecimal解析字符串的過(guò)程有點(diǎn)興趣合武,在構(gòu)造函數(shù)上打個(gè)斷點(diǎn)跟蹤調(diào)試一下
現(xiàn)在以Debug模式啟動(dòng)临梗,一路next后,看看控制臺(tái)輸出
詭異的事情發(fā)生了稼跳,結(jié)果居然是0盟庞。
問(wèn)題研究
現(xiàn)在再加兩行代碼看看
public class Main {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("123.455");
System.out.println("結(jié)果是 -> " + a);
BigDecimal b = a.add(BigDecimal.TEN);
System.out.println("加10,結(jié)果是 -> " + b);
}
}
Run模式啟動(dòng)
Debug模式啟動(dòng)
問(wèn)題似乎變得更詭異了汤善。什猖。。
一波冷靜思考(瞎折騰)后红淡,貌似找對(duì)了方向不狮。
接下來(lái)嘗試對(duì)修改后程序Debug模式下的調(diào)試結(jié)果進(jìn)行解釋:
首先,我們來(lái)整理一下思路:
- 控制臺(tái)打印結(jié)果時(shí)在旱,調(diào)用的是
BigDecimal.toString()
方法摇零; - 打印出
a
錯(cuò)誤的結(jié)果0
; -
a + 10
之后桶蝎,將結(jié)果賦予b
驻仅,打印出正確的結(jié)果133.45
;
這樣看來(lái)登渣,問(wèn)題極有可能出在BigDecimal.toString()
方法中雾家!
OK,看看這段代碼绍豁。
@Override
public String toString() {
String sc = stringCache;
if (sc == null)
stringCache = sc = layoutChars(true);
return sc;
}
很明顯芯咧,這里做了一個(gè)緩存,真正的字符串生成在layoutChars()
,進(jìn)去看一下敬飒,過(guò)程還是有些復(fù)雜的邪铲,加一個(gè)緩存減少計(jì)算量的確是一個(gè)明智的設(shè)計(jì)。
那么這個(gè)詭異的現(xiàn)象就很好解釋了无拗,肯定是stringCache
這個(gè)作為緩存的成員變量被污染了带到。
下面開(kāi)始搜索stringCache
變量被引用的位置,發(fā)現(xiàn)只有toString()
方法這兩處英染,那也就是說(shuō)揽惹,debug模式下,過(guò)早的調(diào)用了toString()
方法四康。因?yàn)槲覀儼褦帱c(diǎn)打在了構(gòu)造函數(shù)上搪搏,這時(shí)BigDecimal對(duì)象還沒(méi)有構(gòu)造完成,調(diào)試器在展示對(duì)象值的時(shí)候闪金,通過(guò)特殊方式調(diào)用了toString()
方法疯溺,污染了stringCache
緩存,也就導(dǎo)致了后續(xù)調(diào)用toString()
方法時(shí)一直拿到的都是這個(gè)臟緩存哎垦。
PS:我把斷點(diǎn)打在toString()
方法里囱嫩,發(fā)現(xiàn)并沒(méi)有攔住,IDEA給了一句這樣的提示:
擴(kuò)展思考
在toString()
方法中使用緩存的我還是第一次見(jiàn)漏设,平時(shí)我們寫POJO時(shí)墨闲,IDE生成的代碼都是直接拼接,為什么不也生成一個(gè)帶緩存的方法郑口?
問(wèn)題的關(guān)鍵點(diǎn)是BigDecimal對(duì)象是不可變對(duì)象鸳碧。
不可變對(duì)象也就意味著對(duì)象一旦構(gòu)造完成后,不可再改變潘酗。如果需要修改,原來(lái)的對(duì)象不變雁仲,生成一個(gè)新對(duì)象仔夺。
乍一看起來(lái),這似乎是一個(gè)非常奢侈的舉動(dòng)攒砖,會(huì)浪費(fèi)大量的內(nèi)存缸兔。但帶來(lái)的好處卻也是巨大的:
- 在Java中,所有的對(duì)象都是引用類型吹艇,在修改一個(gè)對(duì)象時(shí)惰蜜,會(huì)影響該對(duì)象的所有持有者,導(dǎo)致一些難以排查的問(wèn)題受神。
- 不可變對(duì)象對(duì)函數(shù)式編程和并發(fā)編程是極其友好的抛猖,對(duì)象不可變意味著不用加鎖,提高了性能,并降低了開(kāi)發(fā)難度财著。
- 不可變對(duì)象可以使用緩存技術(shù)提高性能联四,例如JVM針對(duì)Integer、String等類型會(huì)維護(hù)常量池撑教。
我們?cè)賮?lái)分析一下BigDecimal.toString()
方法使用的緩存設(shè)計(jì)朝墩。
- 首先,BigDecimal是一個(gè)不可變對(duì)象伟姐,Java標(biāo)準(zhǔn)要求對(duì)象必須在構(gòu)造完成后才能調(diào)用方法收苏,因此在程序正常Run過(guò)程中,是沒(méi)有任何問(wèn)題的愤兵,但是debug過(guò)程采用的特殊方式提前調(diào)用了
toString()
方法鹿霸,其實(shí)是破壞了這種約定,才導(dǎo)致的詭異現(xiàn)象發(fā)生恐似。 - 其次杜跷,這是一個(gè)懶加載緩存,只有在首次調(diào)用
BigDecimal.toString()
時(shí)矫夷,才觸發(fā)stringCache
的更新葛闷,在實(shí)際的使用中,我們并不總會(huì)將BigDecimal對(duì)象按字符串輸出双藕,這樣也就不會(huì)觸發(fā)stringCache
的計(jì)算淑趾,降低性能開(kāi)銷。
Date類的設(shè)計(jì)一直飽受爭(zhēng)議忧陪,其可變性就被認(rèn)為是一個(gè)設(shè)計(jì)的失誤扣泊。
比如如下代碼
public class Person {
private Date birth;
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public static void main(String[] args) {
Person person = new Person();
person.setBirth(new Date(2000 - 1900, 0, 1));
System.out.println(person.getBirth());
Date date = person.getBirth();
date.setMonth(1);
System.out.println(person.getBirth());
}
}
這段代碼的輸出是
Sat Jan 01 00:00:00 CST 2000
Tue Feb 01 00:00:00 CST 2000
想象一下,如果person
是從數(shù)據(jù)庫(kù)里讀取出來(lái)的一條記錄嘶摊,我們僅僅是想得到person
生日那年的一個(gè)2月份時(shí)間延蟹,卻在不經(jīng)意間改變了person
本身的birth
屬性,在某些ORM框架中叶堆,即使沒(méi)有顯示調(diào)用update操作阱飘,對(duì)person
的修改也會(huì)保存到數(shù)據(jù)庫(kù),這種問(wèn)題帶來(lái)的影響是十分難以排查的虱颗。
但是如果Date是不可變類型沥匈,對(duì)于每一次的修改實(shí)際是生成了一個(gè)新對(duì)象,那么上述問(wèn)題將不復(fù)存在忘渔「咛基于此,Java 8全新的Time API都是不可變的畦粮。
最后散址,我們看一下LocalDate.toString()
源碼
@Override
public String toString() {
int yearValue = year;
int monthValue = month;
int dayValue = day;
int absYear = Math.abs(yearValue);
StringBuilder buf = new StringBuilder(10);
if (absYear < 1000) {
if (yearValue < 0) {
buf.append(yearValue - 10000).deleteCharAt(1);
} else {
buf.append(yearValue + 10000).deleteCharAt(0);
}
} else {
if (yearValue > 9999) {
buf.append('+');
}
buf.append(yearValue);
}
return buf.append(monthValue < 10 ? "-0" : "-")
.append(monthValue)
.append(dayValue < 10 ? "-0" : "-")
.append(dayValue)
.toString();
}
這里貌似沒(méi)有使用類似BigDecimal的緩存乖阵,不知道設(shè)計(jì)人員是不是也考慮到了BigDecimal在debug下可能會(huì)產(chǎn)生一些問(wèn)題,亦或者是感覺(jué)緩存帶來(lái)的性能優(yōu)化是寥寥無(wú)幾的爪飘。