Invalid double崩潰分析

weixin_34087301發表於2017-11-25

第一次遇到

java.lang.NumberFormatException: Invalid double: "0,3"
這樣包含逗號的浮點數異常,第一感覺就是伺服器給的資料錯誤,但前段時間復現了這個異常,才發現是程式碼不規範導致了這樣的異常: 崩潰的詳細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)

而出錯的程式碼是這樣的:

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);
    }

想把浮點數”0.31415926”保留一位小數,就使用了String.format()
方法完成,而且完全不顧編譯器對我們的建議:

4398977-6b4a754e0cd9d8cb.jpg
這裡寫圖片描述
之後,我們想把保留了一位小數後的浮點數”0.3”轉成百分比的形式”30.0%”,當然先轉成double,然後利用NumberFormat
格式化成百分比。 看似沒有問題,但是當我們把系統語言換成法語(France)之後,程式直接Crash,而且當我們只顯示remain1Places()
後的title時,發現”0.31415926”變成了”0,3”,而不是”0.3”,這就直接導致了Double.parseDouble(“0,3”)丟擲一個資料格式異常。 那為什麼String.format()
會將小數點的”句點(.)“換成了”逗號(,)“呢?
摘自維基百科: 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.
4398977-562e843356d6da5e.jpg
這裡寫圖片描述

可以看到,有很多地區都是用逗號(,)作為小數點的,如19,90€表示19.9歐元;但計算機程式中的浮點數必須用句點(.)作為小數點,如double price = 19.90;
表示浮點數19.9。所以在使用double的包裝類Double
的parseDouble(String)
或valueOf(String)
方法將字串表示的double值轉成double時,字串所表示的double值必須是用句點(.)分隔的浮點數,也就是計算機的浮點數表示形式。
Double.valueOf(String)
方法僅僅呼叫了Double.parseDouble(String)
並返回Double
物件。 Double.parseDouble(String)
方法返回原語型別的double
變數。

因此,我們就可以斷定這是一個本地化(Locale)的問題了。現在再來看一下編譯器(lint)給我們String.format()
方法的建議:
Implicitly using the default locale is a common source of bugs: Use String.format(Locale, …) instead. 隱式地使用預設的區域設定是常見Bug源,請使用String.format(Locale, …)等方法替換它。

也就是說,String.format(String format, Object... args)
會呼叫format(Locale.getDefault(), format, args)
使用使用者預設的區域設定返回格式化好且本地化好的字串,因使用者設定的不同而返回不同的字串,進而出現Bug。如果你只是想格式化字串而不是人為干預,應該用

String.format(Locale locale, String format, Object... args)
方法,Locale引數用Locale.US
就可以了。
因此,我們應該重視本地化問題:
將字串所有字元轉為大/小寫的方法String.toLowerCase()
/String.toUpperCase()
並不一定能將字元真正的大/小寫(如區域設定為土耳其時,i大寫後還是i),因此應該指定要使用的區域設定String.toLowerCase(Locale locale)
/String.toUpperCase(Locale locale)

格式化字串的方法String.format(String format, Object... args)
應該指定區域設定,以避免區域設定變化導致的Bug。
千萬不要將數字format()
成字串後再將該字串轉回原語型別
,因為format()
後的字串可能不是合法的原語型別的數字了。即永遠不要出現類似這樣

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));
}

相關文章