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

Java13核心類

沒有特殊說明,個人全部學習筆記都是從廖老師那裏摘抄過來的,侵刪java

引言

兜兜轉轉到了大四,學過了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()並無改變字符串的內容,而是返回了一個新字符串。app

  • 另外一個strip()方法也能夠移除字符串首尾空白字符。它和trim()不一樣的是,相似中文的空格字符\u3000也會被移除:dom

"\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來產生安全的隨機數。

相關文章
相關標籤/搜索