第一次遇到
java.lang.NumberFormatException: Invalid double: "0,3"
這樣包含逗號(hào)的浮點(diǎn)數(shù)異常,第一感覺就是服務(wù)器給的數(shù)據(jù)錯(cuò)誤,但前段時(shí)間復(fù)現(xiàn)了這個(gè)異常,才發(fā)現(xiàn)是代碼不規(guī)范導(dǎo)致了這樣的異常: 崩潰的詳細(xì)log如下:
E/AndroidRuntime: FATAL EXCEPTION: mainProcess: com.frank.lollipopdemo, PID:
10898java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.frank.lollipopdemo/com.frank.lollipopdemo.MainActivity}:
java.lang.NumberFormatException: Invalid double: "0,3" at
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2325) at
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2387) at
android.app.ActivityThread.access$800(ActivityThread.java:151) at
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1303) at
android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:135) at
android.app.ActivityThread.main(ActivityThread.java:5254) at java.lang.reflect.Method.invoke(Native
Method) at java.lang.reflect.Method.invoke(Method.java:372) at
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903) at
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698) Caused by:
java.lang.NumberFormatException: Invalid double: "0,3" at
java.lang.StringToReal.invalidReal(StringToReal.java:63) at
java.lang.StringToReal.initialParse(StringToReal.java:164) at
java.lang.StringToReal.parseDouble(StringToReal.java:282) at
java.lang.Double.parseDouble(Double.java:301) at
com.frank.lollipopdemo.MainActivity.onCreate(MainActivity.java:43) at
android.app.Activity.performCreate(Activity.java:5990) at
android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1106) at
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2278) at
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2387) at
android.app.ActivityThread.access$800(ActivityThread.java:151) at
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1303) at
android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:135) at
android.app.ActivityThread.main(ActivityThread.java:5254) at java.lang.reflect.Method.invoke(Native
Method) at java.lang.reflect.Method.invoke(Method.java:372) at
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903) at
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
而出錯(cuò)的代碼是這樣的:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv_title = (TextView) findViewById(R.id.tv_title);
tv_content = (TextView) findViewById(R.id.tv_content);
String jsonVal = "0.31415926";
String title = remain1Places(jsonVal);// -> 0.3
tv_title.setText(title);
tv_content.setText(getPercent(Double.parseDouble(title)));// -> ##.##%
}
public static String remain1Places(String string) {
if (TextUtils.isEmpty(string)) {
return "";
}
return String.format("%.1f", Double.parseDouble(string));
}
public static String getPercent(double value) {
NumberFormat numberFormat = NumberFormat.getPercentInstance();
numberFormat.setMinimumFractionDigits(1);
numberFormat.setMaximumFractionDigits(2);
return numberFormat.format(value);
}
想把浮點(diǎn)數(shù)”0.31415926”保留一位小數(shù),就使用了String.format()
方法完成,而且完全不顧編譯器對(duì)我們的建議:

格式化成百分比。 看似沒有問題,但是當(dāng)我們把系統(tǒng)語言換成法語(France)之后,程序直接Crash,而且當(dāng)我們只顯示remain1Places()
后的title時(shí),發(fā)現(xiàn)”0.31415926”變成了”0,3”,而不是”0.3”,這就直接導(dǎo)致了Double.parseDouble(“0,3”)拋出一個(gè)數(shù)據(jù)格式異常。 那為什么String.format()
會(huì)將小數(shù)點(diǎn)的”句點(diǎn)(.)“換成了”逗號(hào)(,)“呢?
摘自維基百科: A decimal mark is a symbol used to separate the integer part from the fractional part of a number written in decimal form. Different countries officially designate different symbols for the decimal mark. The choice of symbol for the decimal mark also affects the choice of symbol for the thousands separator used in digit grouping, so the latter is also treated in this article.

可以看到,有很多地區(qū)都是用逗號(hào)(,)作為小數(shù)點(diǎn)的,如19,90€表示19.9歐元;但計(jì)算機(jī)程序中的浮點(diǎn)數(shù)必須用句點(diǎn)(.)作為小數(shù)點(diǎn),如double price = 19.90;
表示浮點(diǎn)數(shù)19.9。所以在使用double的包裝類Double
的parseDouble(String)
或valueOf(String)
方法將字符串表示的double值轉(zhuǎn)成double時(shí),字符串所表示的double值必須是用句點(diǎn)(.)分隔的浮點(diǎn)數(shù),也就是計(jì)算機(jī)的浮點(diǎn)數(shù)表示形式。
Double.valueOf(String)
方法僅僅調(diào)用了Double.parseDouble(String)
并返回Double
對(duì)象。 Double.parseDouble(String)
方法返回原語類型的double
變量。
因此,我們就可以斷定這是一個(gè)本地化(Locale)的問題了?,F(xiàn)在再來看一下編譯器(lint)給我們String.format()
方法的建議:
Implicitly using the default locale is a common source of bugs: Use String.format(Locale, …) instead. 隱式地使用默認(rèn)的區(qū)域設(shè)置是常見Bug源,請(qǐng)使用String.format(Locale, …)等方法替換它。
也就是說,String.format(String format, Object... args)
會(huì)調(diào)用format(Locale.getDefault(), format, args)
使用用戶默認(rèn)的區(qū)域設(shè)置返回格式化好且本地化好的字符串,因用戶設(shè)置的不同而返回不同的字符串,進(jìn)而出現(xiàn)Bug。如果你只是想格式化字符串而不是人為干預(yù),應(yīng)該用
String.format(Locale locale, String format, Object... args)
方法,Locale參數(shù)用Locale.US
就可以了。
因此,我們應(yīng)該重視本地化問題:
將字符串所有字符轉(zhuǎn)為大/小寫的方法String.toLowerCase()
/String.toUpperCase()
并不一定能將字符真正的大/小寫(如區(qū)域設(shè)置為土耳其時(shí),i大寫后還是i),因此應(yīng)該指定要使用的區(qū)域設(shè)置String.toLowerCase(Locale locale)
/String.toUpperCase(Locale locale)
。
格式化字符串的方法String.format(String format, Object... args)
應(yīng)該指定區(qū)域設(shè)置,以避免區(qū)域設(shè)置變化導(dǎo)致的Bug。
千萬不要將數(shù)字format()
成字符串后再將該字符串轉(zhuǎn)回原語類型,因?yàn)閒ormat()
后的字符串可能不是合法的原語類型的數(shù)字了。即永遠(yuǎn)不要出現(xiàn)類似這樣
Double.parseDouble(new DecimalFormat("#.##").format(doubleValue))
的代碼。
建議使用NumberFormat
,如:
public static String remain1Places(String str) {
NumberFormat numberFormat = NumberFormat.getInstance(Locale.getDefault());
numberFormat.setMinimumFractionDigits(1);
numberFormat.setMaximumFractionDigits(1);
return numberFormat.format(Double.parseDouble(str));
}
public static String getPercent(String str) {
NumberFormat numberFormat = NumberFormat.getPercentInstance(Locale.getDefault());
numberFormat.setMinimumFractionDigits(1);
numberFormat.setMaximumFractionDigits(2);
return numberFormat.format(Double.parseDouble(str));
}