Java13核心類 沒有特殊說明,我的所有學習筆記都是從 廖老師 那裡摘抄過來的,侵刪 引言 兜兜轉轉到了大四,學過了C,C++,C ,Java,Python,學一門丟一門,到了最後還是要把Java撿起來。所以奉勸大家,面向對象還是要掌握一門,雖然Python好寫舒服,但是畢竟不能完全面向對象,也沒 ...
Java13核心類
沒有特殊說明,我的所有學習筆記都是從廖老師那裡摘抄過來的,侵刪
引言
兜兜轉轉到了大四,學過了C,C++,C#,Java,Python,學一門丟一門,到了最後還是要把Java撿起來。所以奉勸大家,面向對象還是要掌握一門,雖然Python好寫舒服,但是畢竟不能完全面向對象,也沒有那麼多的應用場景,所以,奉勸看到本文的各位,還是提前學好C#或者Java。
字元串和編碼
String
在Java中,
String
是一個引用類型,它本身也是一個class
。但是,Java編譯器對String
有特殊處理,即可以直接用"..."
(這裡的...是象徵字元串的)來表示一個字元串Java字元串的一個重要特點就是字元串不可變。這種不可變性是通過內部的
private final char[]
欄位,以及沒有任何修改char[]
的方法實現的。
public class Main {
public static void main(String[] args) {
String s = "Hello";
System.out.println(s);
s = s.toUpperCase();
System.out.println(s);
}
}
字元串比較
- 當我們想比較兩個字元串時,是想比較兩個字元串的內容是否相同。這個時候要用
equals()
而不能用==
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}
- 從錶面上看,兩個字元串用
==
和equals()
比較都為true
,但實際上那隻是Java編譯器在編譯期,會自動把所有相同的字元串當作一個對象放入常量池,自然s1
和s2
的引用就是相同的。
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}
- 兩個字元串比較,必須總是使用
equals()
方法。 要忽略大小寫比較,使用
equalsIgnoreCase()
方法。String
類還提供了多種方法來搜索子串、提取子串。常用的方法有:
// 是否包含子串:
"Hello".contains("ll"); // true
註意到
contains()
方法的參數是CharSequence
而不是String
,因為CharSequence
是String
的父類。搜索子串的更多的例子:
"Hello".indexOf("l"); // 2
"Hello".lastIndexOf("l"); // 3
"Hello".startsWith("He"); // true
"Hello".endsWith("lo"); // true
- 提取子串的例子:
- 註意索引號是從
0
開始的。
"Hello".substring(2); // "llo"
"Hello".substring(2, 4); "ll"
去除首尾空白字元
- 使用
trim()
方法可以移除字元串首尾空白字元。空白字元包括空格,\t
,\r
,\n
:
" \tHello\r\n ".trim(); // "Hello"
註意:
trim()
並沒有改變字元串的內容,而是返回了一個新字元串。另一個
strip()
方法也可以移除字元串首尾空白字元。它和trim()
不同的是,類似中文的空格字元\u3000
也會被移除:
"\u3000Hello\u3000".strip(); // "Hello"
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"
String
還提供了isEmpty()
和isBlank()
來判斷字元串是否為空和空白字元串:
"".isEmpty(); // true,因為字元串長度為0
" ".isEmpty(); // false,因為字元串長度不為0
" \n".isBlank(); // true,因為只包含空白字元
" Hello ".isBlank(); // false,因為包含非空白字元
替換子串
- 兩種方法,一種是根據字元或者字元串替換。
String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字元'l'被替換為'w'
s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替換為"~~"
- 另一種是通過正則表達式替換:
String s = "A,,B;C ,D";
s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"
分割字元串
- 要分割字元串,使用
split()
方法,並且傳入的也是正則表達式:
String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}
拼接字元串
- 拼接字元串使用靜態方法
join()
,它用指定的字元串連接字元串數組:
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
類型轉換
- 要把任意基本類型或引用類型轉換為字元串,可以使用靜態方法
valueOf()
。這是一個重載方法,編譯器會根據參數自動選擇合適的方法:
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 類似java.lang.Object@636be97c
- 要把字元串轉換為其他類型,就需要根據情況。例如,把字元串轉換為
int
類型:
int n1 = Integer.parseInt("123"); // 123
int n2 = Integer.parseInt("ff", 16); // 按十六進位轉換,255
- 把字元串轉換為
boolean
類型:
boolean b1 = Boolean.parseBoolean("true"); // true
boolean b2 = Boolean.parseBoolean("FALSE"); // false
- 要特別註意,
Integer
有個getInteger(String)
方法,它不是將字元串轉換為int
,而是把該字元串對應的系統變數轉換為Integer
:
Integer.getInteger("java.version"); // 版本號,11
轉換為char[]
String
和char[]
類型可以互相轉換,方法是:
char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String
- 如果修改了
char[]
數組,String
並不會改變: - 這是因為通過
new String(char[])
創建新的String
實例時,它並不會直接引用傳入的char[]
數組,而是會複製一份,所以,修改外部的char[]
數組不會影響String
實例內部的char[]
數組,因為這是兩個不同的數組。
public class Main {
public static void main(String[] args) {
char[] cs = "Hello".toCharArray();
String s = new String(cs);
System.out.println(s);
cs[0] = 'X';
System.out.println(s);
}
}
從
String
的不變性設計可以看出,如果傳入的對象有可能改變,我們需要複製而不是直接引用。
public class Main {
public static void main(String[] args) {
int[] scores = new int[] { 88, 77, 51, 66 };
Score s = new Score(scores);
s.printScores();
scores[2] = 99;
s.printScores();
}
}
class Score {
private int[] scores;
public Score(int[] scores) {
// 這樣傳入的就是scores的複製
this.scores = Arrays.copyOf(scores, scores.length);
// 使用如下方法也可以
// this.scores = scores.clone();
}
public void printScores() {
System.out.println(Arrays.toString(scores));
}
}
字元編碼
ASCII
編碼範圍從0
到127
,每個字元用一個byte
表示。GB2312
使用兩個byte
表示一個中文字元。Unicode
是全球統一編碼,其中的UTF-8
是變長編碼,英文字元為1個byte
,中文字元為3個byte
。在Java中,
char
類型實際上就是兩個位元組的Unicode
編碼。如果我們要手動把字元串轉換成其他編碼,可以這樣做:
byte[] b1 = "Hello".getBytes(); // 按ISO8859-1編碼轉換,不推薦
byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8編碼轉換
byte[] b2 = "Hello".getBytes("GBK"); // 按GBK編碼轉換
byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8編碼轉換
- 如果要把已知編碼的
byte[]
轉換為String
,可以這樣做:
byte[] b = ...
String s1 = new String(b, "GBK"); // 按GBK轉換
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8轉換
- 始終牢記:Java的
String
和char
在記憶體中總是以Unicode
編碼表示。
StringBuilder
- Java編譯器對
String
做了特殊處理,使得我們可以直接用+
拼接字元串。
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + "," + i;
}
- 雖然可以直接拼接字元串,但是,在迴圈中,每次迴圈都會創建新的字元串對象,然後扔掉舊的字元串。這樣,絕大部分字元串都是臨時對象,不但浪費記憶體,還會影響GC效率。
- 為了能高效拼接字元串,Java標準庫提供了
StringBuilder
,它是一個可變對象,可以預分配緩衝區,這樣,往StringBuilder
中新增字元時,不會創建新的臨時對象:
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(',');
sb.append(i);
}
String s = sb.toString();
StringBuilder
還可以進行鏈式操作
:
public class Main {
public static void main(String[] args) {
var sb = new StringBuilder(1024);
sb.append("Mr ")
.append("Bob")
.append("!")
.insert(0, "Hello, ");
System.out.println(sb.toString());
}
}
如果我們查看
StringBuilder
的源碼,可以發現,進行鏈式操作的關鍵是,定義的append()
方法會返回this
,這樣,就可以不斷調用自身的其他方法。使用鏈式操作的關鍵點就在於返回本身。你可能還聽說過
StringBuffer
,這是Java早期的一個StringBuilder
的線程安全版本,StringBuilder
和StringBuffer
介面完全相同,現在完全沒有必要使用StringBuffer
。
StringJoiner
- 類似用分隔符拼接數組的需求很常見,所以Java標準庫還提供了一個
StringJoiner
來乾這個事:
public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
var sj = new StringJoiner(", ");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());
}
}
- 但是這樣還不夠,還少了開頭的
hello
和結尾的!
,於是我們給StringJoiner
指定開頭和結尾
public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
// param1 是需要給數組之間插入的字元串 para2和3是指定了StringJoiner的開頭和結尾
var sj = new StringJoiner(", ", "Hello ", "!");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());
}
}
- 其實StringJoiner的內部就是用的StringBuilder來拼接字元串的,所以拼接效率幾乎和StringBuilder一模一樣
String.join()
String
還提供了一個靜態方法join()
,這個方法在內部使用了StringJoiner
來拼接字元串,在不需要指定“開頭”和“結尾”的時候,用String.join()
更方便:
String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);
包裝類型
- Java的數據類型分兩種:
- 基本類型:
byte
,short
,int
,long
,boolean
,float
,double
,char
- 引用類型:所有
class
和interface
類型
- 基本類型:
- 引用類型可以賦值為
null
,表示空,但基本類型不能賦值為null
:
String s = null;
int n = null; // compile error!
- 提問:如何把一個基本類型視為對象(引用類型)?
- 想要把
int
基本類型變成一個引用類型,我們可以定義一個Integer
類,它只包含一個實例欄位int
,這樣,Integer
類就可以視為int
的包裝類(Wrapper Class):
- 想要把
Integer n = null;
Integer n2 = new Integer(99);
int n3 = n2.intValue();
- 實際上因為包裝類型非常有用,所以Java對於每一種基本類型都有他的包裝類型。可以直接用,不用自行定義。
基本類型 | 對應的引用類型 |
---|---|
boolean | java.lang.Boolean |
byte | java.lang.Byte |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.lang.Double |
char | java.lang.Character |
public class Main {
public static void main(String[] args) {
int i = 100;
// 通過new操作符創建Integer實例(不推薦使用,會有編譯警告):
Integer n1 = new Integer(i);
// 通過靜態方法valueOf(int)創建Integer實例:
Integer n2 = Integer.valueOf(i);
// 通過靜態方法valueOf(String)創建Integer實例:
Integer n3 = Integer.valueOf("100");
// 使用示範
System.out.println(n3.intValue());
}
}
Auto Boxing
- 因為
int
和Integer
可以互換,所以Java可以幫助我們在int
和Integer
之間轉型
Integer n = 100; // 編譯器自動使用Integer.valueOf(int)
int x = n; // 編譯器自動使用Integer.intValue()
- 直接把
int
變為Integer
的賦值寫法,稱為自動裝箱(Auto Boxing),反過來,把Integer
變為int
的賦值寫法,稱為自動拆箱(Auto Unboxing)。
自動裝箱和自動拆箱只發生在編譯階段,目的是為了少寫代碼。
- 裝箱和拆箱會影響代碼的執行效率,因為編譯後的
class
代碼是嚴格區分基本類型和引用類型的。並且,自動拆箱執行時可能會報NullPointerException
:
不變類
- 所有的包裝類型都是不變類。我們查看
Integer
的源碼可知,它的核心代碼如下:
public final class Integer {
private final int value;
}
- 因此,一旦創建了
Integer
對象,該對象就是不變的。 對兩個
Integer
實例進行比較要特別註意:絕對不能用==
比較,因為Integer
是引用類型,必須使用equals()
比較。(引用類型必須用equals()比較)編譯器把
Integer x = 127;
自動變為Integer x = Integer.valueOf(127);
,為了節省記憶體,Integer.valueOf()
對於較小的數,始終返回相同的實例,因此,==
比較“恰好”為true
,但我們絕不能因為Java標準庫的Integer
內部有緩存優化就用==
比較,必須用equals()
方法比較兩個Integer
。
按照語義編程,而不是針對特定的底層實現去“優化”。
因為
Integer.valueOf()
可能始終返回同一個Integer
實例,因此,在我們自己創建Integer
的時候,以下兩種方法:方法1:
Integer n = new Integer(100);
方法2:
Integer n = Integer.valueOf(100);
方法2更好,因為方法1總是創建新的
Integer
實例,方法2把內部優化留給Integer
的實現者去做,即使在當前版本沒有優化,也有可能在下一個版本進行優化。我們把能創建“新”對象的靜態方法稱為靜態工廠方法。
Integer.valueOf()
就是靜態工廠方法,它儘可能地返回緩存的實例以節省記憶體。
創建新對象時,優先選用靜態工廠方法而不是new操作符。
進位轉換
Integer
類本身還提供了大量方法,例如,最常用的靜態方法parseInt()
可以把字元串解析成一個整數:
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因為按16進位解析
Integer
還可以把整數格式化為指定進位的字元串:
public class Main {
public static void main(String[] args) {
System.out.println(Integer.toString(100)); // "100",表示為10進位
System.out.println(Integer.toString(100, 36)); // "2s",表示為36進位
System.out.println(Integer.toHexString(100)); // "64",表示為16進位
System.out.println(Integer.toOctalString(100)); // "144",表示為8進位
System.out.println(Integer.toBinaryString(100)); // "1100100",表示為2進位
}
}
- 整數和浮點數的包裝類型都繼承自
Number
。
JavaBean
在Java中,有很多
class
的定義都符合這樣的規範:若幹
private
實例欄位;通過
public
方法(getter、setter方法)來讀寫實例欄位。
public class Person {
private String name;
private int age;
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
public int getAge() { return this.age; }
public void setAge(int age) { this.age = age; }
}
- 如果讀寫方法符合以下這種命名規範,則稱為JavaBean
// 讀方法:
public Type getXyz()
// 寫方法:
public void setXyz(Type value)
boolean
欄位比較特殊,它的讀方法一般命名為isXyz()
:
// 讀方法:
public boolean isChild()
// 寫方法:
public void setChild(boolean value)
我們通常把一組對應的讀方法(
getter
)和寫方法(setter
)稱為屬性(property
)。例如,name
屬性:對應的讀方法是
String getName()
對應的寫方法是
setName(String)
只有
getter
的屬性稱為只讀屬性(read-only),例如,定義一個age只讀屬性:對應的讀方法是
int getAge()
無對應的寫方法
setAge(int)
類似的,只有
setter
的屬性稱為只寫屬性(write-only)。很明顯,只讀屬性很常見,只寫屬性不常見。
JavaBean的作用
- JavaBean主要用來傳遞數據。
JavaBean可以方便地被IDE工具分析,生成讀寫屬性的代碼,主要用在圖形界面的可視化設計中。
通過IDE,可以快速生成
getter
和setter
。例如,在Eclipse中,先輸入以下代碼,然後,點擊右鍵,在彈出的菜單中選擇“Source”,“Generate Getters and Setters”,在彈出的對話框中選中需要生成getter
和setter
方法的欄位,點擊確定即可由IDE自動完成所有方法代碼。
public class Person {
private String name;
private int age;
}
枚舉JavaBean屬性
- 要枚舉一個JavaBean的所有屬性,可以直接使用Java核心庫提供的
Introspector.getBeanInfo(ClassName.class)
枚舉類
- 在Java中,我們可以通過
static final
來定義常量。例如,我們希望定義周一到周日這7個常量,可以用7個不同的int
表示
public class Weekday {
public static final int SUN = 0;
public static final int MON = 1;
public static final int TUE = 2;
public static final int WED = 3;
public static final int THU = 4;
public static final int FRI = 5;
public static final int SAT = 6;
}
- 無論是
int
常量還是String
常量,使用這些常量來表示一組枚舉值的時候,有一個嚴重的問題就是,編譯器無法檢查每個值的合理性。例如:
if (weekday == 6 || weekday == 7) {
if (tasks == Weekday.MON) {
// TODO:
}
}
上述代碼編譯和運行均不會報錯,但存在兩個問題:
註意到
Weekday
定義的常量範圍是0
~6
,並不包含7
,編譯器無法檢查不在枚舉中的int
值;定義的常量仍可與其他變數比較,但其用途並非是枚舉星期值。
enum
- 為了讓編譯器能自動檢查某個值在枚舉的集合內,並且,不同用途的枚舉需要不同的類型來標記,不能混用,我們可以使用
enum
來定義枚舉類。
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day == Weekday.SAT || day == Weekday.SUN) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}
- 枚舉的好處
- 編譯器會自動檢查出類型錯誤。
- 不可能引用到非枚舉的值,因為無法通過編譯。
- 不同類型的枚舉不能互相比較或者賦值,因為類型不符。例如,不能給一個
Weekday
枚舉類型的變數賦值為Color
枚舉類型的值。
Weekday x = Weekday.SUN; // ok!
Weekday y = Color.RED; // Compile error: incompatible types
enum的比較
前面講解過引用類型的比較需要使用equals()
,雖然enum
定義的是一種枚舉類型,但是卻可以例外用==
來比較。這是因為enum
類型的每個常量在JVM中只有一個唯一實例,所以可以直接用==
比較。
enum類型
通過enum
定義的枚舉類,和其他的class
有什麼區別?
答案是沒有任何區別。enum
定義的類型就是class
,只不過它有以下幾個特點:
- 定義的
enum
類型總是繼承自java.lang.Enum
,且無法被繼承; - 只能定義出
enum
的實例,而無法通過new
操作符創建enum
的實例; - 定義的每個實例都是引用類型的唯一實例;
- 可以將
enum
類型用於switch
語句。
例如,我們定義的Color
枚舉類:
public enum Color {
RED, GREEN, BLUE;
}
編譯器編譯出的class
大概就像這樣:
public final class Color extends Enum { // 繼承自Enum,標記為final class
// 每個實例均為全局唯一:
public static final Color RED = new Color();
public static final Color GREEN = new Color();
public static final Color BLUE = new Color();
// private構造方法,確保外部無法調用new操作符:
private Color() {}
}
所以,編譯後的enum
類和普通class
並沒有任何區別。但是我們自己無法按定義普通class
那樣來定義enum
,必須使用enum
關鍵字,這是Java語法規定的。
因為enum
是一個class
,每個枚舉的值都是class
實例,因此,這些實例有一些方法:
name()
返回常量名,例如:
String s = Weekday.SUN.name(); // "SUN"
ordinal()
返回定義的常量的順序,從0開始計數,例如:
int n = Weekday.MON.ordinal(); // 1
改變枚舉常量定義的順序就會導致ordinal()
返回值發生變化。
如果不小心修改了枚舉的順序,編譯器是無法檢查出這種邏輯錯誤的。要編寫健壯的代碼,就不要依靠ordinal()
的返回值。因為enum
本身是class
,所以我們可以定義private
的構造方法,並且,給每個枚舉常量添加欄位:
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);
public final int dayValue;
private Weekday(int dayValue) {
this.dayValue = dayValue;
}
}
預設情況下,對枚舉常量調用toString()
會返回和name()
一樣的字元串。但是,toString()
可以被覆寫,而name()
則不行。我們可以給Weekday
添加toString()
方法。
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Today is " + day + ". Work at home!");
} else {
System.out.println("Today is " + day + ". Work at office!");
}
}
}
enum Weekday {
MON(1, "星期一"), TUE(2, "星期二"), WED(3, "星期三"), THU(4, "星期四"), FRI(5, "星期五"), SAT(6, "星期六"), SUN(0, "星期日");
public final int dayValue;
private final String chinese;
private Weekday(int dayValue, String chinese) {
this.dayValue = dayValue;
this.chinese = chinese;
}
@Override
public String toString() {
return this.chinese;
}
}
註意:判斷枚舉常量的名字,要始終使用name()方法,絕不能調用toString()!
switch
因為枚舉類天生具有類型信息和有限個枚舉常量,所以比int
、String
類型更適合用在switch
語句中。
BigInteger
- Java中提供的整形最大範圍是個64位的
long
,要是超過了這個範圍就需要用BigInteger
來表示數字。java.math.BigInteger
就是用來表示任何數字的。 BigInteger
進行運算的時候只能用實例方法,而且和long
整形運算比起來速度較慢。BigInteger
和Integer
、Long
一樣,也是不可變類,並且也繼承自Number
類。因為Number
定義了轉換為基本類型的幾個方法:- 轉換為
byte
:byteValue()
- 轉換為
short
:shortValue()
- 轉換為
int
:intValue()
- 轉換為
long
:longValue()
- 轉換為
float
:floatValue()
- 轉換為
double
:doubleValue()
- 轉換為
- 通過上述方法,可以把
BigInteger
轉換成基本類型。如果BigInteger
表示的範圍超過了基本類型的範圍,轉換時將丟失高位信息,即結果不一定是準確的。如果需要準確地轉換成基本類型,可以使用intValueExact()
、longValueExact()
等方法(沒有其他的typeValueExact
方法),在轉換時如果超出範圍,將直接拋出ArithmeticException
異常。
BigInteger i1 = new BigInteger("1234567890");
BigInteger i2 = new BigInteger("12345678901234567890");
BigInteger sum = i1.add(i2); // 12345678902469135780
BigInteger mul = i1.multiply(i2); //不知道多大了
System.out.println(i.multiply(i).longValueExact());
// java.lang.ArithmeticException: BigInteger out of long range
// 使用longValueExact()方法時,如果超出了long型的範圍,會拋出ArithmeticException
BigDecimal
- 和
BigInteger
類似,BigDecimal
可以表示一個任意大小且精度完全準確的浮點數。
BigDecimal bd = new BigDecimal("123.4567");
System.out.println(bd.multiply(bd)); // 15241.55677489
BigDecimal
用scale()
表示小數位數,例如:
BigDecimal d1 = new BigDecimal("123.45");
BigDecimal d2 = new BigDecimal("123.4500");
BigDecimal d3 = new BigDecimal("1234500");
System.out.println(d1.scale()); // 2,兩位小數
System.out.println(d2.scale()); // 4
System.out.println(d3.scale()); // 0
- 通過
BigDecimal
的stripTrailingZeros()
方法,可以將一個BigDecimal
格式化為一個相等的,但去掉了末尾0的BigDecimal
:
BigDecimal d1 = new BigDecimal("123.4500");
BigDecimal d2 = d1.stripTrailingZeros();
System.out.println(d1.scale()); // 4
System.out.println(d2.scale()); // 2,因為去掉了00
BigDecimal d3 = new BigDecimal("1234500");
BigDecimal d4 = d3.stripTrailingZeros();
System.out.println(d3.scale()); // 0
System.out.println(d4.scale()); // -2
- 如果一個
BigDecimal
的scale()
返回負數,例如,-2
,表示這個數是個整數,並且末尾有2個0。 - 可以對一個
BigDecimal
設置它的scale
,如果精度比原始值低,那麼按照指定的方法進行四捨五入或者直接截斷:
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Main {
public static void main(String[] args) {
BigDecimal d1 = new BigDecimal("123.456789");
BigDecimal d2 = d1.setScale(4, RoundingMode.HALF_UP); // 四捨五入,123.4568
BigDecimal d3 = d1.setScale(4, RoundingMode.DOWN); // 直接截斷,123.4567
System.out.println(d2);
System.out.println(d3);
}
}
- 對
BigDecimal
做加、減、乘時,精度不會丟失,但是做除法時,存在無法除盡的情況,這時,就必須指定精度以及如何進行截斷:
BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("23.456789");
BigDecimal d3 = d1.divide(d2, 10, RoundingMode.HALF_UP); // 保留10位小數並四捨五入
BigDecimal d4 = d1.divide(d2); // 報錯:ArithmeticException,因為除不盡
- 還可以對
BigDecimal
做除法的同時求餘數:
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
BigDecimal n = new BigDecimal("12.345");
BigDecimal m = new BigDecimal("0.12");
BigDecimal[] dr = n.divideAndRemainder(m);
System.out.println(dr[0]); // 102
System.out.println(dr[1]); // 0.105
}
}
- 調用
divideAndRemainder()
方法時,返回的數組包含兩個BigDecimal
,分別是商和餘數,其中商總是整數,餘數不會大於除數。我們可以利用這個方法判斷兩個BigDecimal
是否是整數倍數:
BigDecimal n = new BigDecimal("12.75");
BigDecimal m = new BigDecimal("0.15");
BigDecimal[] dr = n.divideAndRemainder(m);
if (dr[1].signum() == 0) {
// n是m的整數倍
}
比較BigDecimal
- 在比較兩個
BigDecimal
的值是否相等時,要特別註意,使用equals()
方法不但要求兩個BigDecimal
的值相等,還要求它們的scale()
相等:
BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("123.45600");
System.out.println(d1.equals(d2)); // false,因為scale不同
System.out.println(d1.equals(d2.stripTrailingZeros())); // true,因為d2去除尾部0後scale變為2
System.out.println(d1.compareTo(d2)); // 0
- 必須使用
compareTo()
方法來比較,它根據兩個值的大小分別返回負數、正數和0
,分別表示小於、大於和等於。 - 總是使用compareTo()比較兩個BigDecimal的值,不要使用equals()!
- 如果查看
BigDecimal
的源碼,可以發現,實際上一個BigDecimal
是通過一個BigInteger
和一個scale
來表示的,即BigInteger
表示一個完整的整數,而scale
表示小數位數:
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
}
BigDecimal
也是從Number
繼承的,也是不可變對象。
常用工具類
Math
顧名思義,Math
類就是用來進行數學計算的,它提供了大量的靜態方法來便於我們實現數學計算:
求絕對值:
Math.abs(-100); // 100
Math.abs(-7.8); // 7.8
取最大或最小值:
Math.max(100, 99); // 100
Math.min(1.2, 2.3); // 1.2
計算xy次方:
Math.pow(2, 10); // 2的10次方=1024
計算√x:
Math.sqrt(2); // 1.414...
計算ex次方:
Math.exp(2); // 7.389...
計算以e為底的對數:
Math.log(4); // 1.386...
計算以10為底的對數:
Math.log10(100); // 2
三角函數:
Math.sin(3.14); // 0.00159...
Math.cos(3.14); // -0.9999...
Math.tan(3.14); // -0.0015...
Math.asin(1.0); // 1.57079...
Math.acos(1.0); // 0.0
Math還提供了幾個數學常量:
double pi = Math.PI; // 3.14159...
double e = Math.E; // 2.7182818...
Math.sin(Math.PI / 6); // sin(π/6) = 0.5
生成一個隨機數x,x的範圍是0 <= x < 1
:
Math.random(); // 0.53907... 每次都不一樣
如果我們要生成一個區間在[MIN, MAX)
的隨機數,可以藉助Math.random()
實現,計算如下:
// 區間在[MIN, MAX)的隨機數
public class Main {
public static void main(String[] args) {
double x = Math.random(); // x的範圍是[0,1)
double min = 10;
double max = 50;
double y = x * (max - min) + min; // y的範圍是[10,50)
long n = (long) y; // n的範圍是[10,50)的整數
System.out.println(y);
System.out.println(n);
}
}
有些童鞋可能註意到Java標準庫還提供了一個StrictMath
,它提供了和Math
幾乎一模一樣的方法。這兩個類的區別在於,由於浮點數計算存在誤差,不同的平臺(例如x86和ARM)計算的結果可能不一致(指誤差不同),因此,StrictMath
保證所有平臺計算結果都是完全相同的,而Math
會儘量針對平臺優化計算速度,所以,絕大多數情況下,使用Math
就足夠了。
Random
Random
用來創建偽隨機數。所謂偽隨機數,是指只要給定一個初始的種子,產生的隨機數序列是完全一樣的。
要生成一個隨機數,可以使用nextInt()
、nextLong()
、nextFloat()
、nextDouble()
:
Random r = new Random();
r.nextInt(); // 2071575453,每次都不一樣
r.nextInt(10); // 5,生成一個[0,10)之間的int
r.nextLong(); // 8811649292570369305,每次都不一樣
r.nextFloat(); // 0.54335...生成一個[0,1)之間的float
r.nextDouble(); // 0.3716...生成一個[0,1)之間的double
有童鞋問,每次運行程式,生成的隨機數都是不同的,沒看出偽隨機數的特性來。
這是因為我們創建Random
實例時,如果不給定種子,就使用系統當前時間戳作為種子,因此每次運行時,種子不同,得到的偽隨機數序列就不同。
如果我們在創建Random
實例時指定一個種子,就會得到完全確定的隨機數序列:
import java.util.Random;
public class Main {
public static void main(String[] args) {
Random r = new Random(12345);
for (int i = 0; i < 10; i++) {
System.out.println(r.nextInt(100));
}
// 51, 80, 41, 28, 55...
}
}
前面我們使用的Math.random()
實際上內部調用了Random
類,所以它也是偽隨機數,只是我們無法指定種子。
SecureRandom
有偽隨機數,就有真隨機數。實際上真正的真隨機數只能通過量子力學原理來獲取,而我們想要的是一個不可預測的安全的隨機數,SecureRandom
就是用來創建安全的隨機數的:
SecureRandom sr = new SecureRandom();
System.out.println(sr.nextInt(100));
SecureRandom
無法指定種子,它使用RNG(random number generator)演算法。JDK的SecureRandom
實際上有多種不同的底層實現,有的使用安全隨機種子加上偽隨機數演算法來產生安全的隨機數,有的使用真正的隨機數生成器。實際使用的時候,可以優先獲取高強度的安全隨機數生成器,如果沒有提供,再使用普通等級的安全隨機數生成器:
import java.util.Arrays;
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
public class Main {
public static void main(String[] args) {
SecureRandom sr = null;
try {
sr = SecureRandom.getInstanceStrong(); // 獲取高強度安全隨機數生成器
} catch (NoSuchAlgorithmException e) {
sr = new SecureRandom(); // 獲取普通的安全隨機數生成器
}
byte[] buffer = new byte[16];
sr.nextBytes(buffer); // 用安全隨機數填充buffer
System.out.println(Arrays.toString(buffer));
}
}
SecureRandom
的安全性是通過操作系統提供的安全的隨機種子來生成隨機數。這個種子是通過CPU的熱雜訊、讀寫磁碟的位元組、網路流量等各種隨機事件產生的“熵”。
在密碼學中,安全的隨機數非常重要。如果使用不安全的偽隨機數,所有加密體系都將被攻破。因此,時刻牢記必須使用SecureRandom
來產生安全的隨機數。