到頭來還是逃不開Java - Java13核心類

来源:https://www.cnblogs.com/dawsonlee/archive/2020/02/17/12319787.html
-Advertisement-
Play Games

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編譯器在編譯期,會自動把所有相同的字元串當作一個對象放入常量池,自然s1s2的引用就是相同的。
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,因為CharSequenceString的父類。

  • 搜索子串的更多的例子:

"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[]

  • Stringchar[]類型可以互相轉換,方法是:
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編碼範圍從0127,每個字元用一個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的Stringchar在記憶體中總是以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的線程安全版本,StringBuilderStringBuffer介面完全相同,現在完全沒有必要使用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的數據類型分兩種:
    • 基本類型:byteshortintlongbooleanfloatdoublechar
    • 引用類型:所有classinterface類型
  • 引用類型可以賦值為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

  • 因為intInteger可以互換,所以Java可以幫助我們在intInteger之間轉型
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,可以快速生成gettersetter。例如,在Eclipse中,先輸入以下代碼,然後,點擊右鍵,在彈出的菜單中選擇“Source”,“Generate Getters and Setters”,在彈出的對話框中選中需要生成gettersetter方法的欄位,點擊確定即可由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

因為枚舉類天生具有類型信息和有限個枚舉常量,所以比intString類型更適合用在switch語句中。

BigInteger

  • Java中提供的整形最大範圍是個64位的long,要是超過了這個範圍就需要用BigInteger來表示數字。java.math.BigInteger就是用來表示任何數字的。
  • BigInteger進行運算的時候只能用實例方法,而且和long整形運算比起來速度較慢。
  • BigIntegerIntegerLong一樣,也是不可變類,並且也繼承自Number類。因為Number定義了轉換為基本類型的幾個方法:
    • 轉換為bytebyteValue()
    • 轉換為shortshortValue()
    • 轉換為intintValue()
    • 轉換為longlongValue()
    • 轉換為floatfloatValue()
    • 轉換為doubledoubleValue()
  • 通過上述方法,可以把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
  • BigDecimalscale()表示小數位數,例如:
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
  • 通過BigDecimalstripTrailingZeros()方法,可以將一個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
  • 如果一個BigDecimalscale()返回負數,例如,-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來產生安全的隨機數。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 選擇器是jquery的核心 jquery選擇器返回的對象是jquery對象,不會返回undefined或者null,因此不必進行判斷 基本選擇器: ID選擇器 $("#ID") element選擇器(標簽選擇器)$("標簽") 如 $("div") 獲取具體的標簽,使用數組下標的方式 $("div" ...
  • jquery是一個快速、小巧,功能強大的javascript函數庫。 jquery主要用來替代原生的javascript,簡化代碼。 前端最頭疼的就是相容:IE6/7/8相容的最高版本是jQuery1.9.1 jQuery的操作在一個類中,不會污染頂級變數 基本不用考慮瀏覽器的相容性 支持鏈式操作方 ...
  • JS中將數組轉換為鏈表 javascript / 將數組轉換為鏈表 @param array arr 需要轉換的數組 @param int type 轉換的類型,0為單鏈表,1為迴圈鏈表 @return object 返回鏈表 / function array2List(arr, type = 0) ...
  • 面試題:如何用apply實現一個bind? ...
  • 1.問題描述: 在日常練習的時候遇見的錯誤,雖然頁面可以順利顯示,但後臺報錯:(雖然不影響導出效果,但看到後臺的異常,內心還是不舒服的) java.lang.IllegalStateException: getOutputStream() has already been called for th ...
  • 切記:用table標簽來佈局form表單元素,table標簽必須放在form表單內部,否則可能會出現各種bug 原文地址:https://blog.csdn.net/weixin_43343144/article/details/88847275 表單與表格的嵌套必須將表單寫在外面,因為表格有嵌套規 ...
  • java設計模式6——代理模式 1、代理模式介紹: 1.1、為什麼要學習代理模式?因為這就是Spring Aop的底層!(SpringAop 和 SpringMvc) 1.2、代理模式的分類: 靜態代理 動態代理 1.3、代理模式關係圖(以租房子為例) 2、靜態代理 2.1、角色分析: 抽象角色:一 ...
  • Hosts 地址錯誤,導致 Dubbo 提供者服務 IP 選擇錯誤的問題 ...
一周排行
    -Advertisement-
    Play Games
  • C#TMS系統代碼-基礎頁面BaseCity學習 本人純新手,剛進公司跟領導報道,我說我是java全棧,他問我會不會C#,我說大學學過,他說這個TMS系統就給你來管了。外包已經把代碼給我了,這幾天先把增刪改查的代碼背一下,說不定後面就要趕鴨子上架了 Service頁面 //using => impo ...
  • 委托與事件 委托 委托的定義 委托是C#中的一種類型,用於存儲對方法的引用。它允許將方法作為參數傳遞給其他方法,實現回調、事件處理和動態調用等功能。通俗來講,就是委托包含方法的記憶體地址,方法匹配與委托相同的簽名,因此通過使用正確的參數類型來調用方法。 委托的特性 引用方法:委托允許存儲對方法的引用, ...
  • 前言 這幾天閑來沒事看看ABP vNext的文檔和源碼,關於關於依賴註入(屬性註入)這塊兒產生了興趣。 我們都知道。Volo.ABP 依賴註入容器使用了第三方組件Autofac實現的。有三種註入方式,構造函數註入和方法註入和屬性註入。 ABP的屬性註入原則參考如下: 這時候我就開始疑惑了,因為我知道 ...
  • C#TMS系統代碼-業務頁面ShippingNotice學習 學一個業務頁面,ok,領導開完會就被裁掉了,很突然啊,他收拾東西的時候我還以為他要旅游提前請假了,還在尋思為什麼回家連自己買的幾箱飲料都要叫跑腿帶走,怕被偷嗎?還好我在他開會之前拿了兩瓶芬達 感覺感覺前面的BaseCity差不太多,這邊的 ...
  • 概述:在C#中,通過`Expression`類、`AndAlso`和`OrElse`方法可組合兩個`Expression<Func<T, bool>>`,實現多條件動態查詢。通過創建表達式樹,可輕鬆構建複雜的查詢條件。 在C#中,可以使用AndAlso和OrElse方法組合兩個Expression< ...
  • 閑來無聊在我的Biwen.QuickApi中實現一下極簡的事件匯流排,其實代碼還是蠻簡單的,對於初學者可能有些幫助 就貼出來,有什麼不足的地方也歡迎板磚交流~ 首先定義一個事件約定的空介面 public interface IEvent{} 然後定義事件訂閱者介面 public interface I ...
  • 1. 案例 成某三甲醫預約系統, 該項目在2024年初進行上線測試,在正常運行了兩天後,業務系統報錯:The connection pool has been exhausted, either raise MaxPoolSize (currently 800) or Timeout (curren ...
  • 背景 我們有些工具在 Web 版中已經有了很好的實踐,而在 WPF 中重新開發也是一種費時費力的操作,那麼直接集成則是最省事省力的方法了。 思路解釋 為什麼要使用 WPF?莫問為什麼,老 C# 開發的堅持,另外因為 Windows 上已經裝了 Webview2/edge 整體打包比 electron ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...