使用 Scanner、BigDecimal

May 24, 2022

Java SE 提供了標準 API,這些 API 就是由許多類別組成,你可以直接取用這些標準類別,省去你撰寫程式時重新打造輪子的需求。底下來舉兩個基本的標準類別:java.util.Scannerjava.math.BigDecimal

使用 Scanner

如果要在文字模式下取得使用者輸入,基本上可以使用 System.in 物件上的 read 方法,不過這個方法是以 int 型態傳回讀入的字元編碼。想想,如果你輸入了一個 '9' 字元,使用 System.in.read 的話,就還得自己將 '9' 字元轉換為整數 9,當然是不方便,實際上,System.in.read 這類的方法,是作為底層 API 使用,開發者會在上頭依需求施予更高層次的封裝。

java.util.Scanner 就是這類的封裝,底下直接以實際範例來說明:

package cc.openhome;

import java.util.Scanner;

public class Guess {
    public static void main(String[] args) {
        var console = new Scanner(System.in);
        var number = (int) (Math.random() * 10);
        var guess = -1;
        
        do {
            System.out.print("猜數字(0 ~ 9):");
            guess = console.nextInt();
        } while(guess != number);
        
        System.out.println("猜中了...XD");
    }
}

由於不想每次都鍵入 java.util.Scanner,一開始就使用 import 告訴編譯器,如此之後就只要鍵入 Scanner 就可以了。

在建立 Scanner 實例時,必須傳入 java.io.InputStream 的實例,之後介紹到輸入輸出串流時會知道,System.in 就是一種 InputStream

接下來要什麼資料,就跟 Scanner 物件要就可以了,正如其名,Scanner 實例會幫你掃描標準輸入,看看使用者有無輸入字元,怎麼掃描你就不用管了。

ScannernextInt 方法會看看標準輸入中,有沒有輸入下一個字串(以空白或換行為區隔),有的話會嘗試將之剖析為 int 型態,Scanner 對每個基本型態,都會有個對應的 nextXXX 方法,例如 nextBytenextShortnextLongnextFloatnextDoublenextBoolean 等,如果直接取得上一個字串(以空白或換行為區隔),則使用 next,如果想取得使用者輸入的整行文字,則使用 nextLine(以換行為區隔)。

慣例上,套件名稱為 java 開頭的類別,表示標準類別。

使用 BigDecimal

知道在 Java 中執行 1.0 – 0.8 的結果是多少嗎?答案不是 0.2,而是 0.19999999999999996!為什麼?這是 Java的臭蟲(Bug)嗎?不!不是的!你使用別的程式語言(例如 JavaScript、Python 等)也有可能是顯示這個結果。

簡單來說,Java(包括其它程式語言)遵合 IEEE 754 浮點數演算(Floating-point arithmetic)規範,使用分數與指數來表示浮點數。例如 0.5 會使用 1/2 來表示,0.75 會使用 1/2 + 1/4 來表示,0.875 會使用 1/2 + 1/4 + 1/8 來表示,而 0.1 會使用 1/16 + 1/32 + 1/256 + 1/512 +1/4096 + 1/8192 + …無限循環下去,無法精確表示,因而造成運算上的誤差。

再來舉個例子,你覺得以下程式片段會顯示什麼結果?

var a = 0.1;
var b = 0.1;
var c = 0.1;
if((a + b + c) == 0.3) {
    System.out.println("等於 0.3");
}
else {
    System.out.println("不等於 0.3");
}

由於浮點數誤差的關係,結果是顯示「不等於 0.3」!類似的例子還很多,結論就是,如果要求精確度,那就要小心使用浮點數,而且別用 == 直接比較浮點數運算結果。

要怎麼辦得到更好的精確度?可以使用 java.math.BigDecimal 類別,以方才的 1.0 – 0.8 為例,如何得到 0.2 的結果?直接使用程式來示範:

package cc.openhome;

import java.math.BigDecimal;

public class DecimalDemo {
    public static void main(String[] args) {
        var operand1 = new BigDecimal("1.0");
        var operand2 = new BigDecimal("0.8");
        var result = operand1.subtract(operand2);
        System.out.println(result);
    }
}

建構 BigDecimal 的方法之一是使用字串,BigDecmial 在建構時會剖析傳入字串,以預設精度進行接下來的運算,BigDecimal 提供有 plussubstractmultiplydivide 等方法,可以進行加、減、乘、除等運算,這些方法都會傳回代表運算結果的 BigDecimal

上面這個範例可以顯示出 0.2 的結果,再來看利用 BigDecimal 比較相等的例子:

package cc.openhome;

import java.math.BigDecimal;

public class DecimalDemo2 {
    public static void main(String[] args) {
        var op1 = new BigDecimal("0.1");
        var op2 = new BigDecimal("0.1");
        var op3 = new BigDecimal("0.1");
        var result = new BigDecimal("0.3");

        if(op1.add(op2).add(op3).equals(result)) {
            System.out.println("等於 0.3");
        }
        else {
            System.out.println("不等於 0.3");
        }
    }
}

由於 BigDecimaladd 等方法都會傳回代表運算結果的 BigDecmial,所以就直接利用傳回的 BigDecimal 再呼叫 add 方法,最後再呼叫 equals 比較兩個 BigDecimal 實質上是否相同,所以有了 op1.add(op2).add(op3).equals(result) 的寫法。

物件指定與相等性

比較兩個 BigDecimal 是否相等,是使用 equals 方法而非使用 == 運算子,為什麼?Java 中並非全部都是物件,Java 有兩大型態系統,基本型態與類別型態。

= 用於基本型態時,是將值複製給變數,== 用於基本型態時,是比較兩個變數儲存的值是否相同,這對初學者來說沒有問題,所以底下的程式片段會顯示兩個 true,因為 ab 儲存的值都是 10,而 ac 儲存的值也都是 10:

var a = 10;
var b = 10;
var c = a;
System.out.println(a == b);
System.out.println(a == c);

如果你在操作物件,= 是用在指定參考名稱參考某個物件,而 == 是用在比較兩個參考名稱是否參考同一物件。白話來說,= 是用在將某個名牌綁到某個物件,而 == 是用在比較兩個名牌是否綁到同一物件。來看個範例:

var a = new BigDecimal("0.1");
var b = new BigDecimal("0.1");
System.out.println(a == b);        // 顯示 false
System.out.println(a.equals(b));  // 顯示 true

上面的程式片段,以繪圖方式表示的話,以第一行為例,看到 new 關鍵字,就是建立物件,那就畫個圓圈表示物件,這個物件內含 "0.1",並建立一個名牌 a 綁到這個新建立的物件,所以第一行與第二行執行後,可用以下的圖來表示:

使用 Scanner、BigDecimal

程式中使用 a == b,就是在問,a 牌子綁的物件是否就是 b 牌子綁的物件?答案「不是」,也就是 false 的結果,程式中使用 a.equals(b),就是在問,a 牌子綁的物件與 b 牌子綁的物件,實際上內含值是否相同,因為 ab 綁的物件,內含值都是 "0.1" 代表的數值,答案「是」,也就是 true 的結果。

再來看一個例子:

var a = new BigDecimal("0.1");
var b = new BigDecimal("0.1");
var c = a;
System.out.println(a == b);        // 顯示 false
System.out.println(a == c);        // 顯示 true
System.out.println(a.equals(b));   // 顯示 true

這個程式片段若執行至第三行 c = a,表示將 a 牌子綁的物件,也給 c 牌子來綁,用圖表示就是:

使用 Scanner、BigDecimal

所以問到 a == b,就是在問 ab 是否綁在同一物件?結果就是 false,問到 a == c,就是在問 ac 是否綁在同一物件?結果就是 true,問到 a.equals(b),就是在問 ab 綁的物件實際上內含值是否相同?結果就是 true

== 用在物件型態,是比較兩個名稱是否參考同一物件,而 != 正好相反,是比較兩個名稱是否沒參考同一物件。實際上,equals 可以自行定義如何比較兩物件的內含值,之後會說明。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter