天天看點

第七章 Java基礎類庫第七章 Java基礎類庫小結:小結:

第七章 Java基礎類庫

Oracle為java提供了豐富的基礎類庫,java8提供了4000多個基礎類(包括下一章将要介紹的集合架構),通過這些基礎類庫可以提高開發效率,降低開發難度。對于合格的Java程式員而言,至少要熟悉 Java SE中70%以上的類(當然本書并不是讓讀者去背誦 Java API文檔),但在反複查閱API文檔 的過程中,會自動記住大部分類的功能、方法,是以程式員一定要多練,多敲代碼。

Java提供了 String、 StringBuffer和 StringBuilder來理字元串,它們之間存在少許差别,本章會詳細介紹它們之間的差别,以及如何選擇合适的字元串類。Java還提供了Date和 Calendar來處理日期、 時間,其中Date是一個已經過時的API,通常推薦使用 Calendar來處理日期、時間。

正規表達式是一個強大的文本處理工具,通過正規表達式可以對文本内容進行查找、替換、分割等 操作。從K1.4以後,Java也增加了對正規表達式的支援,包括新增的 Pattem和 Matcher兩個類,并 改寫了 String類,讓 String類增加了正規表達式支,增加了正規表達式功能後的 String類更加強大。

Java還提供了非常簡單的國際化支援, Java使用 Locale對象封裝一個國家、語言環境,再使用 ResourceBundle根據 Locale加載語言資源包,當 ResourceBundle加載了指定 Locale對應的語言資源檔案後, ResourceBundle對象就可調用 getString()方法來取出指定key所對應的消息字元串。

7.1 與使用者互動

本節主要介紹如何獲得使用者的鍵盤輸入

7.1.1 運作Java程式的參數

main()方法的方法簽名

//Java程式入口:main()方法
public static void main(String[] args)
           
  • public 修飾符:Java類由JVM調用,為了讓JVM可以自由調用這個main()方法,是以用public修飾符把這個方法暴露出來。
  • static 修飾符:JVM調用這個主方法時,會建立該主類的對象,然後通過對象來調用該主方法。JVM直接通過該類來調用主方法,是以使用static修飾該方法。
  • void 傳回值:因為主方法被JVM調用,該方法的傳回值将傳回給JVM,這沒有任何意義,是以main()方法沒有傳回值。

根據方法調用規則:誰調用方法,誰負責為形參指派。也就是說main()方法由JVM調用,即args形參應該由JVM負責指派,但JVM怎麼知道如何為args指派呢,看如下:

public class ArgsTest 
{
	public static void main(String[] args)
	{
//		輸出args數組的長度
		System.out.println(args.length);
//		周遊args數組的每個元素
		for(var arg : args)
		{
			System.out.println(arg);
		}
	}
}
           

上面程式輸出0,表明args數組是一個長度為0的數組——這是合理的。因為計算機是沒有思考能力的,他隻能忠實地執行使用者交給它的任務,既然程式沒有給args數組設定參數值,那麼JVM就不知道args數組的元素,是以JVM将args數組設定成一個長度為0的數組。

改為一下指令運作此程式:

java ArgsTest.java Java Spring
           

即可看到如下結果

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-QPA2xcmC-1618031098508)(C:\Users\MingyangLiu\Desktop\張智超\image\Snipaste_2021-03-19_16-17-33.png)]

如果在類名後緊跟一個或多個字元串(多個字元串之間以空格隔開),JVM就會把這些字元串依次賦給args數組元素。運作Java程式時的參數與args數組之間的對應關系:

外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-BocDUIY6-1618031098510)(C:\Users\MingyangLiu\Desktop\張智超\image\P10319-162517(1).jpg)

如果某參數本身包含了空格,則應該将該參數用雙引用引用,否則JVM會把這個空格當成參數分隔符,而不是參數本身。

7.1.2 使用Scanner擷取鍵盤輸入

使用Scanner類可以很友善地擷取使用者鍵盤輸入,Scanner是一個基于正規表達式的文本掃描器,他可以用檔案,輸入流,字元串作為資料源,用于檔案、輸入流、字元串中解析資料。

Scanner主要看以下兩個方法進行掃描輸入:

  1. hasNextXxx():是否還有下一個輸入項,其中Xxx可以是Int、Long等代表基本資料類型的字元串。如果隻是判斷是否包含下一個字元串。則直接用hasNext()。
  2. nextXxx():擷取下一個輸入項。Xxx的含義與前一個方法中的Xxx相同。

在預設情況下,Scanner使用空白(空格,tab,回車)作為多個輸入項之間的分隔符。

import java.util.Scanner;

public class ScannerKeyBoardTest
{
	public static void main(String[] args) 
	{
		var sc = new Scanner(System.in);
        
		while(sc.hasNext())
		{
			System.out.println("鍵盤輸入的内容是:"+sc.next());
		}
	}
}
           

上面的程式可以不斷地輸入輸出。

如果希望改變Scanner的分隔符(不使用空白作為分隔符),例如,程式需要每次讀取一行,不管這一行中是否包含空格,Scanner都把它當成一個輸入項。在這種需求下,可以把Scanner的分隔符設定為回車符,不在使用預設的空白作為分隔符。

Scanner的讀取操作可能被阻塞(目前執行順序流暫停)來等待資訊的輸入,如果輸入源沒有結束,Scanner有讀取不到更多輸入項時(尤其在鍵盤輸入時比較常見),Scanner的hasNext()和next()方法都有可能阻塞,hasNext()方法是否阻塞與其相關的next()方法是否阻塞無關。

為Scanner設定分隔符使用useDelimiter(String pattern)方法即可,該方法的參數應該是一個正規表達式,,隻要把上面程式中粗體字代碼行的注釋去掉,該程式就會把鍵盤的每行輸入當成一個輸入項,不會以空格、TAB空白等作為分隔符。事實上,Scanner提供了兩個簡單的方法來逐行讀取。

  • boolean hasNextLine():傳回輸入源中是否還有下一行。
  • String nextLine():傳回輸入源中下一行的字元串。

Scanner不僅可以擷取字元串輸入項,也可以擷取任何基本類型的輸入項:

import java.util.Scanner;

public class ScannerLongTest {
public static void main(String[] args) {
	var sc = new Scanner(System.in) ;
    //控制輸入的格式
while(sc.hasNextLong())//粗
	{
		System.out.println("鍵盤輸入的是:"
                           +sc.nextLong());		//粗
	}
}
}
           

注意粗,正如通過hasNextLong()和nextLong()兩個方法,Scanner可以直接從輸入流中獲得long型證書輸入項。與此類似,如果需要輸入其他基本類型的輸入項,則可以使用相應的方法。

==上面的程式不如ScannerKeyBoardTest程式适應性強,因為這個程式要求必須輸入的格式,否則退出。

Scanner不僅能讀取使用者的鍵盤鍵入,還可以讀取檔案輸入。隻要在建立Scanner對象時傳入了一個File對象作為參數,就可以讓Scanner讀取該檔案的内容。

import java.io.File;
import java.util.Scanner;

public class ScannerFileTest 
{
	public static void main(String[] args)
			throws Exception
	{
		{
			//将一個File對象作為Scanner的構造器參數,Scanner讀取檔案内容
			var sc = new Scanner(new File("ScannerFileTest.java"));//粗體字
			System.out.println("ScannerFileTest.java檔案内容如下");
			//判斷是否還有下一行
			while(sc.hasNextLine())//粗體字
			{
				//輸出檔案中的下一行
				System.out.println(sc.nextLine());//粗體字
			}
		}
	}
}

           

上面程式建立Scanner對象時傳入一個File對象作為參數,這表明該程式将會讀取ScannerFileTest.java檔案中的内容。上面程式使用了hasNextLine()和nextLine()兩個方法來讀取檔案内容。這表明該程式将逐行讀取ScannerFileTest.java檔案的内容。

因為上面程式涉及檔案輸入,可能引發檔案IO相關異常,故主程式聲明throws Exception表明main方法不處理任何異常。

7.2 系統相關

Java提供了System類和Runtime類來與程式的運作平台進行互動。

7.2.1 System類

System類代表目前Java程式的運作平台,程式不能建立System類的對象,System提供了一些類變量和類方法,允許直接通過System類來條用這些類變量和類方法。

System類提供了代表标準輸入、标準輸出和錯誤輸出類變量,并提供了一些靜态方法用 環境變量、系統屬性的方法,還提供了加載檔案和動态連結庫的方法。下面程式通過 System類 操作的環境變量和系統屬性。

注意: 加載檔案和動态連結庫主要對 native方法有用,對于一些特殊的功能(如通路作業系統底層硬體裝置等)Java程式無法實作,必須借助C語言來完成,此時需要使用C語言為Java方法提供實作。其實作步驟如下: ①Java程式中聲明 native修飾的方法,類似于abstract方法,隻有方法簽名,沒有實作。使用帶h選項的 Javac指令編譯該Java程式,将生成一個class檔案和一個h頭檔案 ②寫一個.cpp檔案實作 native方法,這一步需要包含1步産生的.h檔案(這個h 檔案中又包含了JDK帶的jni.h檔案) ③将第2步的.cpp檔案編譯成動态連結庫檔案 ④在Java中用 System類的loadLibrary…()方法加載第3步産生的動态連結庫檔案,Java程式中就可以調用這個 natIve方法了。

注 在Java9以前, Javac指令沒有-h選項,是以JDK提供了 javah指令來為. class檔案生.成h頭檔案。Java10徹底删除了 javah指令 , Javac的-h選項代替了 javah。

import java.io.FileOutputStream;
import java.util.Map;
import java.util.Properties;

public class SystemTest 
{
	public static void main(String[] args) throws Exception
	{
		//擷取系統所有的環境變量
		Map < String, String > env = System.getenv();
		for (var name : env.keySet())
		{
			System.out.println(name + "--->" + env.get(name));
		}
		//擷取指定環境變量的值
		System.out.println(System.getenv("JAVA_HOME"));
		//擷取所有的系統屬性
		Properties props = System.getProperties();
		props.store(new FileOutputStream("props.txt"), "System Properties");
		//輸出特定的系統屬性
		System.out.println(System.getProperty("os.name"));
	}
}
           

該程式運作結束之後還會在目前路徑下生産一個Props.txt檔案,該檔案中記錄了目前平台所有系統屬性。

System類提供了通知系統進行垃圾回收的gc()方法,以及通知系統進行資源清理的 runFinalization(方法。關于這兩個方法的用法請參考本書6.10節的内容

System類還有兩個擷取系統目前時間的方法 : currentTimeMillis()和nanoTime(),它們都傳回一個long型整數。實際上它們都傳回目前時間與UTC1970年1月1日午夜的時間差,前者以毫秒作為機關, 後者以納秒作為機關。必須指出的是,這兩個方法傳回的間粒度取決于底層作業系統,可能所在的作業系統根本不支援以毫秒、納秒作為計時機關。例如,許多作業系統以幾十毫秒為機關測量時間,currentTimeMillis()方法不可能傳回精确的毫秒數,而nanoTime()方法很少用,因為大部分作業系統都不支援使用納秒作為計時機關。

除此之外, System類的in、out和err分别代表系統的标準輸入(通常是鍵盤)、标準輸出(通常是 顯示器)和錯誤輸出流,并提供了 setIn(),setout()和setErr()方法來改變系統的标準輸入、标準輸出和标準錯誤輸出流。關于如何改變系統的标準輸入、輸出的方法,可以參考本書第15章的内容。

System類還提供了一個 identityHashCode( Object X)方法,該方法傳回指定對象的精确 hashCode值 也就是根據該對象的位址計算得到的 hashCode值。當某個類的 hashCode方法被重寫後,該類執行個體的 hashCode()方法就不能唯一地辨別該對象;但通過 identityHashCode()方法傳回的 hash Code值,依然是根據該對象的位址計算得到的 hash Code值。是以如果兩個對象的 identityHashCode值相同,則兩個對象絕對是同一個對象。如下程式所示:

public class IdentityHashCodeTest 
{
	public static void main(String[] args) 
	{
//		下面程式中的s1和s2是兩個不同的對象
		var s1 = new String("Hello");
		var s2 = new String("Hello");
//		String重寫了hasCode()方法——改為根據字元序列計算hashCode值
//		因為S1和S2的字元序列相同,是以他們的hasCode()方法傳回值相同
		System.out.println(s1.hashCode()+"-----"+s2.hashCode());
//		s1和s2的不同的字元串對象,是以他們的identityHashCode值不同
		System.out.println(System.identityHashCode(s1) + "----"+System.identityHashCode(s2));
		var s3 = "Java";
		var s4 = "Java";
//		s3和s4是相同的字元串對象,是以他們的identityHashCode值相同。
		System.out.println(System.identityHashCode(s3)+"---"+System.identityHashCode(s4));
	}
}
           

通過 identityHashCode(Object x)方法可以獲得對象的 identityHashCode值,這個特殊的identityHashCode值可以唯一地辨別該對象。因為identityHashCode值是根據對象的位址計算得到的, 是以任何兩個對象的 identityHashCode值總是不相等。

7.2.2 Runtime類與java9的 ProcessHandle

Runtime類代表Java程式的運作時環境,每個Java程式都有一個與之對應的 Runtime執行個體,應用程式通過該對象與其運作時環境相連。應用程式不能建立自己的 Runtime執行個體,但可以通過 getRuntime() 方法擷取與之關聯的 Runtime對象。

與System類似的是, Runtime類也提供了 gc() 方法和 runFinalization() 方法來通知系統進行垃圾回收清理系統資源,并提供了 load(String filename) 和 loadLibrary( String libname)方法來加載檔案和動态連結庫。

Runtime類代表Java程式的運作時環境,可以通路JM的相關資訊,如處理器數量、記憶體資訊等。 如下程式所示:

public class RuntimeTest 
{
	public static void main(String[] args) 
	{
//		擷取Java程式關聯的運作時的環境
		var rt = Runtime.getRuntime();
		System.out.println("處理器數量:"+rt.availableProcessors());
		System.out.println("空閑記憶體數:"+rt.freeMemory());
		System.out.println("總記憶體數:"+rt.totalMemory());
		System.out.println("可用最大記憶體數:"+rt.maxMemory());
	}
}
           

Runtime類還有一個功能——他可以直接啟動一個程序來運作作業系統的指令:

public class ExecTest 
{
	public static void main(String[] args) throws Exception
	{
		var rt = Runtime.getRuntime();
//		運作記事本
		rt.exec("notepad.exe");
	}
}
           

上面程式中粗體字代碼将啟動 Windows系統裡的“記事本”程式。 Runtime提供了一系列 exec() 來運作作業系統指令,關于它們之間的細微差别,請讀者自行查閱API文檔。

通過exec啟動平台上的指令之後,它就變成了一個程序,Java使用 Process來代表程序java9還新增了一個ProcessHandle接口,通過該接口可擷取程序的ID、父程序和後代程序:通過該接口的onExit() 方法可在程序結束時完成某些行為。

ProcessHandle還提供了一個 ProcessHandle.Info類,用于擷取程序的指令、參數、啟動時間、累計運作時間、使用者等資訊。下面程式示範了通過 ProcessHandle擷取程序的相關資訊。

import java.util.concurrent.CompletableFuture;

public class ProcessHandleTest 
{
	public static void main(String[] args) throws Exception
	{
		var rt = Runtime.getRuntime();
//		運作記事本程式
		Process p = rt.exec("notepad.exe");
		ProcessHandle ph = p.toHandle();
		System.out.println("程序是否運作:"+ph.isAlive());
		System.out.println("程序ID:"+ph.pid());
		System.out.println("父程序:"+ph.parent());
//		擷取ProcessHandle.Info資訊
		ProcessHandle.Info info = ph.info();
//		通過ProcessHandle.Info資訊擷取程序相關資訊
		System.out.println("程序指令:"+info.command());
		System.out.println("程序參數:"+info.arguments());
		System.out.println("程序啟動時間:"+info.startInstant());
		System.out.println("程序累計運作時間:"+info.totalCpuDuration());
//		通過CompletableFuture在程序結束後運作某個任務
		CompletableFuture<ProcessHandle>cf = ph.onExit();
		cf.thenRunAsync(()->{
		System.out.println("程式退出");});
		Thread.sleep(5000);
	}
}
           

通過粗體字擷取Process對象的ProcessHandle對象,接下來即可通過ProcessHandle對象來擷取程序相關資訊。

7.3 常用類

7.3.1 Object類

Object類是所有類、數組、枚舉類的父類,也就是說,java允許把任何類型的對象賦給 Object類型的變量。當定義一個類時沒有使用 extends關鍵字為它顯式指定父類,則該類預設繼承 Object父類。 因為所有的Java類都是 Object類的子類,是以任何對象都可以調用 Object類的方法。 Object 類提供了如下幾個常用方法。

  • boolean equals( Object obj):判斷指定對象該對象是否相等。此處相等的标準是,兩個對象是 同一個對象,是以該 equals方法通常沒有太大的實用價值。
  • protected void finalize():當系統中沒有引用變量引用到該對象時,垃圾回收器調用此方法來清理該對象的資源。
  • Class<?>getClass():傳回該對象的運作時類,該方法在本書第18章還有更詳細的介紹。
  • int hashCode():傳回該對象的 hashCode()值。在預設情況下, Object類的hashCode()方法根據該對象的位址來計算(即與 System.identityHashCode( Object x)方法的計算結果相同)。但很多類都重寫了 Object 類的 hashCode()方法,不再根據位址計算其 hashCode()方法值。
  • String toString()傳回該對象的字元串表示,當程式使用System.out.printlm()方法輸出一個對象,或者把某個對象和字元串進行連接配接運算時,系統會自動調用該對象的 toString()方法傳回該對象的字元串表示。Object類的toString()方法傳回”運作時類名@十六進制 hash Code值”格式的字元串,但很多類都重寫了Object類的 toString()方法,用于傳回可以表述該對象資訊的字元串。

除此之外, Object類還提供了 wait()、 notify()、notifyAll()幾個方法,通過這幾個方法可以控制線程的暫停和運作。

Java還提供了一個 protected修飾的 clone()方法,該方法用于幫助其他對象來實作“自我克隆”,所謂“自我克隆”就是得到一個目前對象的副本,而且二者之間完全隔離。由于Object類提供的clone()方法使用了protected修飾,是以該方法隻能被子類重寫或調用。

自定義類實作“克隆”的步驟如下:

​ ①自定義類實作 Cloneable接口。這是一個标記性的接口,實作該接口的對象可以實作“自我克隆”,接口裡沒有定義任何方法。

​ ②自定義類實作自己的 clone()方法。

​ ③實作 clone()方法時通過super.clone();調用 Object實作的clone()方法來得到該對象的副本,并傳回該對象的副本。如下程式示範了如何實作“自我克隆”。

class Address
{
	String detail;
	public Address(String detail)
	{
		this.detail = detail;
	}
}
//實作Cloneable接口
class User implements Cloneable
{
	int age;
	Address address;
	public User(int age)
	{
		this.age = age;
		address = new Address("廣州天河"); 
	}
	//通過調用supre.clone()來實作clone()方法
	public User clone()
			throws CloneNotSupportedException
	{
	return(User) super.clone();
	}
}
public class CloneTest 
{
	public static void main(String[] args) 
		throws CloneNotSupportedException
	{
		var u1 = new User(29);
//		clone得到u1對象的副本
		var u2 = u1.clone();
//		判斷u1和u2是否相等
		System.out.println(u1 == u2);  //①
//		判斷u1和u2的address是否相等
		System.out.println(u1.address = u2.address); //②
	}
}
           

上面程式讓User類實作了Cloneable接口,而且實作clone()方法,是以User對象就可實作“自我克隆”——克隆出來的對象隻是原有對象的副本。程式在①号粗體字代碼處判斷原有的User對象與克隆出來的User對象是否想到,程式傳回false。

Object類提供的Clone機制隻對對象裡各類變量進行“簡單複制”,如果執行個體變量的類型是引用類型,Object的clone機制也隻是簡單地複制這個引用變量,這樣原有類型的引用類型的執行個體變量與克隆對象的引用類型的執行個體變量依然指向記憶體中的同一執行個體,是以上面程式在②時輸出true。上面克隆出來的u1,u2所指向的獨享在記憶體中的存儲示意圖:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-EbouxrSf-1618031098513)(C:\Program Files\Typora\image\de3267f363f41df5b666c846be80dfd.jpg)]

Object類提供的clone()方法不僅能簡單地處理“複制”對象的問題,而且這之前“自我克隆”的機制十分高效。比如clone一個包含100個元素的int[]數組,用系統預設的clone方法比靜态copy方法快近2倍。

需要指出的是,Object類的clone()方法雖然簡單、易用,但它隻是一種“淺克隆”——他隻克隆該對象的所有成員變量值,不會對引用類型的成員變量值所引用的對象進行克隆。如果開發者需要對對象進行“深克隆”,那麼開發者需要自己進行“遞歸克隆”,保證所有引用類型的成員變量值所引用的對象都被複制了。

7.3.2 操作對象的Object工具類

Java 7 提供了一個Object工具類,他提供了一些工具來操作對象,這些工具方法大多是“空指針”安全的。比如不能确定一個引用變量是否為null,如果貿然的調用該對象的toString()方法,則有可能引發異常:如果使用Object使用的toString(Object o)方法,則不會引發空指針異常,當o為null時,程式将傳回一個null字元串。

提示:Java為工具類的命名習慣是提供一個字母s,比如作業系統的工具類是Arrays,操作集合的工具類是Collections。

import java.util.Objects;

public class ObjectsTest 
{
	//定義一個obj變量,它的預設值為null
	static ObjectsTest obj;
	public static void main(String[] args)
	{
//		輸出一個null對象的hashCode值,輸出0
		System.out.println(Objects.hash(obj));
//		輸出一個null對象的toString,輸出null
		System.out.println(Objects.toString(obj));
//		要求obj不能為null,如果obj為null則引發異常
		System.out.println(Objects.requireNonNull(obj,"obj參數不能是null!"));
	}
}
           

上面程式還示範了Objects提供的requireNonNull()方法,當傳入的參數不為null時,該方法傳回參數本身;否則會引發NullPointerException異常。該方法主要是對方法形參輸入校驗,例如如下代碼:

public Foo(Bar bar)
{
	//校驗bar參數,如果bar參數為null将引發異常;否則this.bar被指派為bar參數
	this.bar = Objects.requireNull(bar)
}
           

7.3.3 Java 9改進的String、StringBuffer和StringBuilder類

字元串就是一連串的字元序列,Java提供了 String, StringBuffer和 StringBuilder三個類來封裝字元串,并提供了一系列方法來操作字元串對象。

String類是不可變類,即一旦一個對象被建立以後,包含在這個對象中的字元序列是不可改變的,直至這個對象被銷毀。

StringBuffer對象則代表一個字元序列可變的字元串,當一個 StringBuffer被建立以後,通過StringBuffer提供的 append()、insert()、 reverse()、 setCharAt()、 setLength()等方法可以改變這個字元串對象的字元序列。一旦通過StringBuffer生成了最終想要的字元串,就可以調用它的toString()方法将其轉換為一個 String對象。

StringBuilder類是JDK1.5新增的類,它也代表可變字元對象。實際上, StringBuilder和StringBuffer 基本相似,兩個類的構造器和方法也基本相同。不同的是, StringBuffer是線程安全的,而 StringBuilder 則沒有實作線程安全功能,是以性能略高。是以在通常情況下,如果需要建立一個内容可變的字元串對象,則應該優先考慮使用 StringBuilder類。

提示:String、StringBuilder、StringBuffer 都實作了 CharSequence接口,是以CharSequence可認為是一個字元串的協定接口。

Java 9改進了字元串(包括 String、StringBulider,、StringBuilder) 的實作。在Java 9以前字元串采用 char[ ]數組來儲存字元,是以字元串的每個字元占2位元組;而Java 9及更新版本的JDK的字元串采用byte[ ] 數組再加一個 encoding-flag字段來儲存字元,是以字元串的每個字元隻占1位元組。是以Java 9及更新版本的JDK的字元串更加節省空間,但字元串的功能方法沒有受到任何影響。

char charAt(int index):擷取字元串中指定位置的字元,其中index指的是字元串的序數,字元串的序數從0開始到length()-1。

var s = "fkit..org";
		System.out.println("s.charAt(5):"+s.charAt(5));
           

int compareTo(String anotherString):比較兩個字元串的大小。如果兩個字元串的字元串序列相等,則傳回0,;不相等時,從兩個字元串的第0個字元開始比較,傳回第一個不相等的字元差。另一種情況,較長的字元串的前面部分剛好是較短的字元串,傳回長度差。

var s1 = "abcdefghigklmnopq";
		var s2 = "abcdefghig";
		var s3 = "abcdefghigrstuvw";
		System.out.println("s1.compareTo(s2):"+s1.compareTo(s2)); //傳回長度差
		System.out.println("s1.compareTo(s3):"+s1.compareTo(s3)); //傳回'k'-'r'
           

​ boolean ends With(String suffix):傳回String是否一suffix結尾。

var s4 = "fkit.org";
		var s5 = ".org";
		System.out.println("s4.endsWith(s5):"+s4.endsWith(s5));
           

void getChar(int scrBegin,int scrEnd,char [ ] dst,int dstBegin):該方法将字元串中從scrBegin開始,到scrEnd結束的字元串複制到dst字元串數組中,其中dstBegin為目标字元數組的起始複制位置。

char [] s6 = {'I',' ','l','o','v','e',' ','J','a','v','a'};
		var s7 = "ejb";
		s7.getChars(0, 3, s6, 7);  //s6 = I love ejba
		System.out.println(s6);
           

int indexOf(String str,int fromIndex):找出str子字元串在該字元串中從fromIndex開始後第一次出現的位置。

var s8 = "www.fkit.org";
		var s9 = "it";
		System.out.println("s8.indexOf('r'):"+s8.indexOf('r'));
		System.out.println("s8.indexOf('r',2):"+s8.indexOf('r',2));
		System.out.println("s8.indexOf(s9):"+s8.indexOf(s9));
           

剩下的讀者可以查閱API自己學習。

因為String是不可變的,多以會額外産生很多零食變量,使用StringBuffer或StringBulider就可以避免這個問題。

StringBulider提供了一系列的插入、追加、改變該字元串裡包含的字元序列的方法。而StringBuffer與其用法完全相同,隻是StringBuffer是線程安全的。

StringBulider、StringBuffer有兩個屬性:length和capaity,其中lengith,其中length屬性表示其包含的字元序列的長度。與String對象的length對象的length不同的是,StringBulider、StringBuffer的length是可以改變的,可以通過length()、setLength(int len)方法來通路和修改其字元序列的長度。capaity屬性表示StringBuilder類的用法。

public class StringBuilderTest 
{
	public static void main(String[] args) 
	{
//		通過StringBuilder類,建立一個内容可變的字元串變量
		StringBuilder sb = new StringBuilder();
//		追加字元串
		sb.append("Java");
//		插入
		sb.insert(0, "hello");
//		替換
		sb.replace(5, 6, ",");
//		删除
		sb.delete(5, 6);
		System.out.println(sb);
//		反轉
		sb.reverse();
		System.out.println(sb);
		System.out.println(sb.length());
		System.out.println(sb.capacity());//輸出16
//		改變StringBuilder的長度,将隻保留前面部分
		sb.setLength(5);
		System.out.println(sb);
	}
}
           

上面程式中粗體字部分師範看StringBulider類的追加、插入、替換、删除等操作,這些操作改變了StringBulider裡的字元序列,這就是StringBulider與String之間最大的差別:StringBulider的字元序列是可變的。從程式可看到StringBulider的Length()方法傳回其字元序列的長度,而capacity()傳回值則比length()傳回值大。

7.3.4 Math類

Java提供了的基本的+、-、*、/、%等基本算術運算的運算符,但對于更複雜的數學運算,例如,三角函數、對數運算、指數運算等無能為力。Java提供了Math工具類來完成這些複雜的運算,Math類是一個工具類、它的構造器被定義成private的,是以無法建立Math類的對象:Math類中的所有方法都是類方法,可以直接通過類名來調用他們。Math類提供了大量靜态方法之外,還提供了兩個類變量:PI和E,指π和e。

public class MathTest 
{
	public static void main(String[] args) 
	{
		/*----------下面是三角運算-----------*/
		// 将弧度轉換成角度
		System.out.println("Math.toDegrees(1.57):" + Math.toDegrees(1.57));
		// 将角度轉換為弧度
		System.out.println("Math.toRadians(90):" + Math.toRadians(90));
//		計算反餘弦,傳回的角度範圍在0.0到pi之間
		System.out.println("Math.acos(1.2)" + Math.acos(1.2));
//		計算反正弦,傳回的角度範圍在-pi/2到pi/2之間
		System.out.println(Math.asin(0.8));
//		計算反正切,傳回角度範圍在-pi/2到pi/2之間
		System.out.println(Math.atan(2.3));
//		計算三角餘弦
		System.out.println(Math.cos(1.57));
//		計算雙曲餘弦
		System.out.println(Math.cosh(1.2));
//		計算正弦
		System.out.println(Math.sin(1.57));
//		計算雙曲正弦
		System.out.println(Math.sin(1.2));
//		計算三角正切
		System.out.println(Math.tan(0.8));
//		計算雙曲正切
		System.out.println(Math.tanh(2.1));
//		将矩形坐标(x,y)轉換成極坐标(r,thet)
		System.out.println(Math.atan2(0.1,0.2));
		/*----------下面是取整運算-----------*/
//		取整,傳回小于目标數的最大整數
		System.out.println(Math.floor(-1.2));
//		取整,傳回大于目标數的最小整數
		System.out.println(Math.ceil(1.2));
//		四舍五入取整
		System.out.println(Math.round(2.3));
		/*----------下面是乘方、開方、指數運算-----------*/
//		計算平方根
		System.out.println(Math.sqrt(2.3));
//		其餘Math功能省略,讀者可查閱Math API自行學習
	}
}
           

7.3.5 ThreadLocalRandom與Random

Rondom類專門用于生成一個僞随機數,他有兩個構造器:一個構造器舒勇預設的種子(以目前時間為種子),另一個構造器需要程式員顯式傳入一個long整數的種子。

ThreadLocalRandom類是Java 7新增的一個類,是Random的增強版。在并發通路的環境下,使用ThreadLocalRandom來代替Random可以減少多線程資源競争,最終保證系統具有更好的線程安全性。

ThreadLocalRandom類的用法與Random類的用法基本相同,他提供了一個靜态的current()方法來擷取ThreadLocalRandom對象,擷取該對象之後即可調用各種nextXxx()方法來擷取僞随機數了。

可以生成浮點類型的僞随機數,也可以生成整數類型的僞随機數,也可以指定生成随機數的範圍。下面關于Random類的用法:

import java.util.Arrays;
import java.util.Random;

public class RandomTest 
{
	public static void main(String[] args) 
	{
		var rand = new Random();
//		從這個随機數生成器的序列中傳回下一個僞随機、均勻分布的布爾值。
//		nextBoolean的一般約定是,僞随機生成并傳回一個布爾值。
//		值true和false産生的機率(大約)相等。
		System.out.println(rand.nextBoolean());
		var buffer = new byte[16];
//		生成随機位元組并将它們放入使用者提供的位元組數組中。
//		産生的随機位元組數等于位元組數組的長度。
		rand.nextBytes(buffer);
		System.out.println(Arrays.toString(buffer));
//		生成0.0~1.0之間的僞随機double數
		System.out.println(rand.nextDouble());
//		生成0.0~1.0之間的僞随機float數
		System.out.println(rand.nextFloat());
//		生成平均值是0.0,标準差是1.0的僞高斯數
		System.out.println(rand.nextGaussian());
//		生成一個處于int整數取值範圍的僞随機數
		System.out.println(rand.nextInt());
//		生成0~26之間的僞随機數
		System.out.println(rand.nextInt(26));
//		生成一個處于long整數須知範圍的僞随機整數
		System.out.println(rand.nextLong());
	}
}
           

Random使用一個48位的種子,如果這個類的兩個執行個體使用同一個種子建立的,對他們以同樣的順序調用方法,則他們會産生相同的數字序列。

下面做了一個實驗,可以看到當兩個Random對象種子相同時,他們會産生相同的數字序列。值得指出的是,當使用預設的種子構造Random對象時,他們屬于同一個種子。

import java.util.Random;

public class SeedTest 
{
	public static void main(String[] args) 
	{
		var r1 = new Random(50);
		System.out.println("第一個種子為50的Random對象");
		System.out.println(r1.nextBoolean());
		System.out.println(r1.nextDouble());
		System.out.println(r1.nextFloat());
		System.out.println(r1.nextGaussian());
		var r2 = new Random(50);
		System.out.println("第二個種子為50的Random對象");
		System.out.println(r2.nextBoolean());
		System.out.println(r2.nextDouble());
		System.out.println(r2.nextFloat());
		System.out.println(r2.nextGaussian());
		var r3 = new Random(100);
		System.out.println("種子為100的Random對象");
		System.out.println(r3.nextBoolean());
		System.out.println(r3.nextDouble());
		System.out.println(r3.nextFloat());
		System.out.println(r3.nextGaussian());
	}
}
           

從上面的運作結果可以看出,隻要兩個Random對象的種子相同,而且方法的調用順序也相同,他們就會産生相同的數字序列。也就是說,Random産生的數字并不是随機的,而是僞随機。

為了避免兩個Random對象産生相同的數字序列,通常建議使用目前時間作為Random對象的種子:

在多線程環境下使用ThreadLocalRandom的方式使用Random基本類似,

ThreadLocalRandom rand = ThreadLocalRandom.current();
//生成一個4~20之間的随機整數
int vall = rand.nextInt(4,20);
//生成一個2.0~10.0之間的僞随機浮點數
int val2 = rand.nextDouble(2.0~10.0);
           

7.3.6 BigDecimal類

前面介紹double、fioat兩種基本浮點類型時已經指出,這兩個基本類型的浮點數容易丢失精度。

public class DoubleTest 
{
	public static void main(String[] args) 
	{
		System.out.println("0.05 + 0.01 = "+(0.05+0.01));
		System.out.println("1.0 - 0.42 = "+(1.0-0.42));
		System.out.println("123.3/100 = "+(123.3/100));
		System.out.println("4.015*100 = " + (4.015*100));
	}
}
           

上面程式運作結果表明,Java的 double類型會發生精度丢失,尤其在進行算術運算時更容易發生這種情況。不僅是Java,很多程式設計語言也存在這樣的問題。

為了能精确表示、計算浮點數,Java提供了BigDecimal類,該類提供了大量的構造器用于建立BigDecimal對象,包括把所有的基本數值類型變量轉換成一個BigDecimal對象,也包括利用數字字元串、數字字元數組來建立BigDecimal對象。

檢視 BigDecimal 類的 BigDecimal( double val)構造器的詳細說明時,可以看到不推薦使用該構造器的說明,主要是因為使用該構造器時有一定的不可預知性。當程式使用 new BigDecimal(0.1)來建立一個 BigDecimal 對象時,它的值并不是0.1,它實際上等于一個近似0.1的數。這是因為0.1無法準确地表示為double浮點數,是以傳入 BigDecimal構造器的不會正好等于0.1(雖然表面上等于該值)。

如果使用 BigDecimal( String val)構造器的結果是可預知的——寫入 new BigDecimall(0.1)将建立 個 BigDecimal,它正好等于預期的0.1。是以通常建議優先使用基于 String的構造器。

如果必須使用 double 浮點數作為 BigDecimal構造器的參數時,不要直接将該 double浮點數作為構造器參數建立 BigDecimal對象,而是應該通過 BigDecimal.valueOf(double value)靜态方法來建立 BigDecimal對象。

BigDecimal類提供了add()、 subtract()、 multiply()、dvide()、pow()等方法對精确浮點數進行正常算術的基本運算:

import java.math.BigDecimal;

public class BigDecimalTest
{
	public static void main(String[] args)
	{
		var f1 = new BigDecimal("0.05");
		var f2 = BigDecimal.valueOf(0.01);
		var f3 = new BigDecimal(0.05);
		System.out.println("使用String作為BigDecimal構造器參數:");
		System.out.println("0.05 + 0.01 = " + f1.add(f2));
		System.out.println("0.05 - 0.01 = " + f1.subtract(f2));
		System.out.println("...............");
		System.out.println("使用double作為BigDecimal構造器參數:");
		System.out.println("0.05 + 0.01 = " + f3.add(f2));
		System.out.println("0.05 - 0.01 = " + f3.subtract(f2));
	}
}
           

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-i2hUOSCr-1618031098517)(C:\Program Files\Typora\image\image-20210328100130103.png)]

上面程式中f1和f3都是基于0.05建立的BigDecimal對象,其中f1是基于“0.05”字元串,但是f3是基于0.05的double浮點數。是以建立BigDecimal對象時,一定要使用String對象作為參數構造器,而不是直接使用double數字。

如果程式要求對double浮點數進行加減乘除,則需要現将double類型數值包裝廠BigDecimal對象,調用BigDecimal對象的方法執行運算後再将結果轉換成double型,比較繁瑣,可以使用BigDecimal為基礎定義一個Arith工具類:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Arith
{
//	預設除法運算精度
	private static final int DEF_DIV_SCALE = 10;
//	構造器私有,讓這個類不能執行個體化
	private Arith() {}
//	提供精确的加法運算
	public static double add(double d1,double d2)
	{
//		使用double . tostring (double)方法提供的規範的double字元串表示形式,将double轉換為BigDecimal。
		var b1 = BigDecimal.valueOf(d1);
		var b2 = BigDecimal.valueOf(d2);
//		将此BigDecimal轉換為double。
		return b1.add(b2).doubleValue();
	}
//	提供精确的減法運算
	public static double sub(double d3,double d4)
	{
		var b3 = BigDecimal.valueOf(d3);
		var b4 = BigDecimal.valueOf(d4);
		return b3.subtract(b4).doubleValue();
	}
//	提供精确的乘法運算
	public static double mul(double d1,double d2)
	{
		var b1 = BigDecimal.valueOf(d1);
		var b2 = BigDecimal.valueOf(d2);
//		将此BigDecimal轉換為double。
		return b1.multiply(b2).doubleValue();
	}
//	提供(相對)精确的除法運算,當發生除不盡時
//	精确到小數點後10位數字後四舍五入
	public static double div(double d3,double d4)
	{
		var b3 = BigDecimal.valueOf(d3);
		var b4 = BigDecimal.valueOf(d4);
/*		四舍五入模式向“最近的鄰居”舍入,除非兩個鄰居的距離相等,這種情況下取四舍五入。
		表現為舍入模式。
		discardedfraction≥0.5則為UP;
		否則,表現為舍入模式。down。
		注意,這是學校裡常用的舍入模式。
		該模式對應于IEEE 754-2019 round - dingattribute roundTiesToAway。
*/
		return b3.divide(b4,DEF_DIV_SCALE,RoundingMode.HALF_UP).doubleValue();
	}
	public static void main(String[] args)
	{
		System.out.println(Arith.add(0.05, 0.01));
		System.out.println(Arith.sub(1.0, 0.42));
		System.out.println(Arith.mul(4.015, 100));
		System.out.println(Arith.div(123.4, 100));
	}
}

           

上面運作的結果才是期望的記過,這也正是使用BigDecimal類的作用。

7.4 Java 8的日期、時間類

Java原本提供了Date和 Calendar用于處理日期、時間的類,包括建立日期、時間對象,擷取系統目前日期、時間等操作。但Date不僅無法實作國際化,而且它對不同屬性也使用了前後沖突的偏移量, 比如月份與小時都是從0開始的,月份中的天數則是從1開始的,年又是從1900開始的,而 java.util.Calendar則顯得過于複雜,從下面介紹中會看到傳統Java對日期、時間處理的不足。ava8吸取了 Joda-Time庫(一個被廣泛使用的日期、時間庫)的經驗,提供了一套全新的日期時間庫。

7.4.1Date類

Java提供了Date類來處理日期、時間(此處的Date是指java.util包下的Date類,而不是java.sql 包下的Date類),Date對象既包含日期,也包含時間。Date類從JDK1.0起就開始存在了,但正因為它曆史悠久,是以它的大部分構造器、方法都已經過時,不再推薦使用了。

Date類提供了6個構造器,其中4個已經 Deprecated(java不再推薦使用,使用不再推薦的構造器時編譯器會提出警告資訊,并導緻程式性能、安全性方面的問題),剩下的兩個構造器如下 :

  • Date():生成一個代表目前日期時間的date對象。該構造器在底層調用 System. currentTimeMillis() 獲得long整數作為日期參數。
  • Date(long date):根據指定的long型整數來生成一個Date對象。該構造器的參數表示建立的Date 對象和GMT1970年1月1日00:00:00之間的時差,以毫秒作為計時機關與Date構造器相同的是,Date對象的大部分方法也 Deprecated了,剩下為數不多的幾個方法。
  • boolean after(Date when):測試該日期是否在指定日期when之後。
  • boolean before( Date when):測試該日期是否在指定日期when之前。
  • long getTime():傳回該時間對應的long型整數,即從GMT1970-01-01 00:00:00到該Date對象之間的時間差,以毫秒作為計時機關
  • void setTime( (long time):設定該Date對象的時間。
import java.util.Date;

public class DateTest
{
	public static void main(String[] args)
	{
		var d1 = new Date();
//		擷取目前時間之後100ms的時間
		var d2 = new Date(System.currentTimeMillis()+100);
		System.out.println(d2);
		System.out.println(d1.compareTo(d2));
		System.out.println(d1.before(d2));
	}
}
           

總體來說,Date是一個設計相當糟糕的類,是以Java官方推薦盡量少用Date的構造器和方法。如果需要對日期、時間進行加減運算,或者擷取指定時間的年、月、日、時、分、秒資訊,可使用Calendar工具類。

7.4.2 Calendar類

因為Date類在設計上存在一些缺陷,是以Java提供了Calendar來更好的處理日期和時間。Calendar是一個抽象類,他用來表示月曆。

全世界通常選擇最普及、最通用的月曆:Gregorian Calendar,也就是日常介紹年份時常用的“公元幾幾年” 。

Calendar類本身是一個抽象類,它是所有月曆類的闆,并提供了一些所有月曆通用的方法;但它本身不能直接執行個體化,程式隻能建立 Calendar子類的執行個體,Java本身提供了一個 GregorianCalendar類,一個代表格裡高利月曆的子類,它代表了通常所說的公曆。

當然,也可以建立自己的 Calendar子類,然後将它作為 Calendar對象使用(這就是多态)。因為篇幅關系,本章不會詳細介紹如何擴充 Calendar子類,讀者可通過網際網路檢視 Calendar各子類的源碼來學習。

Calendar類是一個抽象類,是以不能使用構造器來建立 Calendar對象。但它提供了幾個靜态 getlnstance()方法來擷取 Calendar對象,這些方法根據 TImeZone, Locale類來擷取特定的 Calendar,如 果不指定TimeZone、 Locale,則使用預設的 TimeZone、 Locale來建立 Calendar.

Calendar與Date都是表示日期的工具類,他們直接可以自由轉換:

//建立一個預設的Calendar對象
var calendar = Calendar.getInstance();
//從Calendar中取出Date對象
var date = .getTime();
//通過Date對象擷取對應的Calendar對象
//因為Calendar/GregorianCalendar沒有構造函數可以接收Date對象
//是以必須建立一個Calendar執行個體,然後調用其setTime()方法。
var calendar2 = Calendar.getInstance();
calendar2.setTime(date);
           

Calender類提供了大量修改日期時間的方法,常用方如下:

  • void add(int field,int amount ): 根據月曆的規則,為給定的月曆字段添加或減去指定的時間量。
  • Int get(int field): 傳回指定月曆字段的值。
  • int getActualMaximum(int field):傳回指定月曆字段可能擁有的最大值。例如月,最大值為11
  • int getActualMinimum(int field):傳回指定月曆字段可能擁有的最小值。例如月,最小值為0
  • void roll(int field, Int amount):與add()方法類似,差別在于加上 amount後超過了該字段所能表示的最大範圍時,也不會向上一個字段進位。
  • void set(int field, int value):将給定的月曆字段設定為給定值 。
  • void set(int year, int month, int date):設定 Calendar對象的年、月、日三個字段的值。
  • void set(int year, int month, int date, int hourOfDay, int minute, Int second):設定 Calendar對象的年、 月、日、時 分、秒6個字段的值。

上面的很多方法都需要一個int類型的field參數,field是 Calendar類的類變量,如Calendar.YEAR、Calendar. MONTH等分别代表了年、月、日、小時分鐘、秒等時間字段。需要指出的是, Calendar. MONTH 字段代表月份,月份的起始值不是1,而是0,是以要設定8月時,用7而不是8,如下程式示範了 Calendar 類的正常用法。

import java.util.Calendar;

public class CalendarTest
{
	private static final int YEAR = 0;
	private static final int MONTH = 0;
	private static final int DATE = 0;

	public static void main(String[] args)
	{
		var c = Calendar.getInstance();
//		取出年
		System.out.println(c.get(YEAR));//粗
		System.out.println(c.get(MONTH));//粗
		System.out.println(c.get(DATE));//粗
		c.set(2003,10,23,12,32,23);//粗//2003-11-23 12.32.23
		System.out.println(c.getTime());
		c.add(YEAR, -1);//粗//2002-11-23 12.32.23
		System.out.println(c.getTime());
		c.roll(MONTH, -8);
		System.out.println(c.getTime());
	}
}
           

上面程式中粗體字代碼示範了Calendar類的用法,Calendar可以很靈活的改變它對應的日期。

上面程式中使用了靜态導入,它導入了Calendar類裡的所有類變量,是以上面程式可以直接使用Calendar類的YEAR、MONTH、DATE等類變量。

Calendar類還有如下幾個注意點。

1.add與roll的差別

add(int field,int amount)的功能非常強大,add主要用于改變Calendar的特定字段的值。如果需要增加某字段的值,則讓amount為整數;如果需要減少某字段的值,則讓amout為負數即可。

add(int field,int amount)有如下兩條規則。

  • 當被修改的字段超出它允許的範圍的時候,會發生進位,即上一級字段也會增大:
var call = Calendar.getInstance();
call.set(2003,7,23,0,0,0);//2003-8-23
call.add(MONTH,6);//2004-2-23
           
  • 如果下一級字段也需要改變,那麼該字段會修正到變化最小的值:
var cal2 = Calendar.getInstance();
cal2.set(1003,7,31,0,0,0);
//因為進位後月份改為2月,2月沒有31日,自動變成29日
val2.add(MONTH,6);//2003-8-31 → 2004-2-29
           

對于上面的例子,8-31就會變成2-29.因為MOUTH的下一級字段是DATE,從31到29改變最小,是以上面2003-8-31的MOUTH字段增加6後,不是變成2004-3-2,而是變成2004-2-29。

roll()的規則與add()的處理規則不同:當貝修改的字段超出它允許的範圍時,上一級字段不會增大。

var cal3 = Calendar.getInstance();
cal3.set(2003,7,23,0,0,0);
//MOUTH字段“進位”,但是YEAR并不增加
val3.roll(MOUTH,6);//2003-8-23 → 2003-2-23
           

下一級字段的處理規則使用者add()相似;

var cal4 = Calendar.getInstance();
cal3.set(2003,7,31,0,0,0);
//MOUTH字段“進位”,但是YEAR并不增加
val3.roll(MOUTH,6);//2003-8-23 → 2003-2-28
           

2.設定Calendar的容錯率

調用Calendar對象的set()方法來改變指定字元串的值時,有可能傳入一個不合法的參數,例如為MOUTH字段設定13,這将會導緻如下後果

import java.util.Calendar;

public class LenientTest
{
	private static final int MOUTH = 0;

	public static void main(String[] args)
	{
		Calendar ca1 = Calendar.getInstance();
//		如果是字段YEAR字段加一,MOUTH字段為1(2月)
		ca1.set(MOUTH,13);//①
		System.out.println(ca1.getTime());
//		關閉容錯性
		ca1.setLenient(false);
//		導緻運作時異常
		ca1.set(MOUTH, 13);//②
		System.out.println(ca1.getTime());
	}
}
           

①和②完全相似,但他們的結果不同:①處代碼可以正常運作,因為設定MOUTH字段的值為13,将導緻YEAR字段加一;②處代碼将會導緻運作時異常,因為設定的MOUTH字段的值超出了MOUTH字段允許的範圍。關鍵在于程式粗體字的運作,Calendar設定了一個setLenient()用于設定它的容錯率,Calendar預設支援好的容錯性,通過setLenient(false)可以關閉它的容錯率,讓他進行嚴格的參數檢查。

Calendar有兩種月曆模式:

​ lenient模式:每個字段可接受超出它允許範圍的值

​ non-lenient模式時,如果某個時間字段設定超出了它允許的取值範圍,程式将抛出異常。

3.set()方法延遲修改

set(f,value)方法将月曆字段f修改為value,此外它還設定了一個内部成員變量,以訓示月曆字段f已經被更改。盡管月曆字段f是立即更改的,但該Calendar所代表的的時間卻不會立即修改,直到下次調用get(),set(),getTimeInMillis(),add(),或roll()時才會重新計算月曆的時間。這被稱為set()方法延遲修改,采用延遲修改的優勢是多次調用set()不會觸發多次不必要的計算(需要計算出一個代表實際時間的long型整數)。

import java.util.Calendar;

public class LazyTest
{
	public static void main(String[] args)
	{
		Calendar cal = Calendar.getInstance();
		cal.set(2003,7,31);
//		将月份設定為9月,但是9月沒有31日
//		如果立即修改,系統将會把cal自動調整到10月1日
		cal.set(Calendar.MONTH, 8);
//		System.out.println(cal.getTime());//①
//		設定DATE字段為5
		cal.set(Calendar.DATE,5);//②
		System.out.println(cal.getTime());//③
	}
}
           

程式将①注釋了,因為Calendar的set()方法具有延遲修改的特性,即調用set()方法後Calendar實際上并未計算真實的日期,他隻是使用内部成員變量表記錄MOUTH字段被修改為8,接着程式設定DATE字段值為5,程式内部在此記錄DATE——就是9月5日,是以③輸出2003-9-5。

7.4.3新的日期、時間包

Jva8專門新增了一個java.time包,該包下包含了如下常用的類。

  • Clock:該類用于擷取指定時區的目前日期、時間。該類可取代 System類的currentTimeMillis() 方法,而且提供了更多方法來擷取目前日期、時間。該類提供了大量靜态方法來擷取 Clock對象。
  • Duration:該類代表持續時間。該類可以非常友善地擷取一段時間。
  • Instant:代表一個具體的時刻,可以精确到納秒。該類供了靜态的now()方法來擷取目前時刻, 也提供了靜态的 now(Clock clock)方法來擷取 clock對應的時刻。除此之外,它還提供了一系列 minusXXX() 方法在目前時刻基礎上減去一段時間,也提供了 plusXxx()方法在目前時刻基礎上加上一段時間。
  • LocalDate:該類代表不帶時區的日期,例如2007-12-03。該類提供了靜态的now方法來擷取 目前日期,也提供了靜态的now( Clock cloc)方法來擷取 clock對應的日期。除此之外,它還提 供了 minusXxx()方法在目前年份基礎上減去幾年、幾月、幾周或幾日等,也提供了 plusXxx() 方法在目前年份基礎上加上幾年、幾月、幾周或幾日等
  • LocalTime:該類代表不帶時區的時間,例如10:15:30。該類提供了靜态的now方法來擷取當 前時間,也提供了靜态的now( (Clock clock)方法來擷取 clock對應的時間。除此之外,它還提供 了 minus Xxx()方法在目前年份基礎上減去幾小時、幾分、幾秒等,也提供了 plusXxx()方法在當 前年份基礎上加上幾小時、幾分、幾秒等。
  • LocaIDate Time:該類代表不帶時區的日期、時間,例如2007-12-07T10:15:30。該類提供了靜态的now方法來擷取目前日期、時間,也提供了靜态的now( Clock clock)方法來擷取 clock對應的日期、時間。除此之外,它還提供了 minusXxx()方法在目前年份基礎上減去幾年、幾月、幾 日、幾小時、幾分、幾秒等,也提供了 plusXxx()方法在目前年份基礎上加上幾年、幾月、幾日、 幾小時、幾分、幾秒等。
  • MonthDay:該類僅代表月日,例如-04-12。該類提供了靜态的now方法來擷取目前月日,也提供了靜态的 now(Clock clock)方法來擷取 clock對應的月日
  • Year:該類僅代表年,例如2014。該類提供了靜态的now方法來擷取目前年份,也提供了靜态的now (Clock clock)方法來擷取 clock對應的年份。除此之外,它還提供了 minusYears()方法 在目前年份基礎上減去幾年,也提供了 plusYears()方法在目前年份基礎上加上幾年。
  • Year Month:該類僅代表年月,例如2014-04。該類提供了靜态的now方法來擷取目前年月, 也提供了靜态的 now(Clock clock)方法來擷取clok對應的年月。除此之外,它還提供了minusXxx()方法在目前年月基礎上減去幾年、幾月,也提供了 plusXxx()方法在目前年月基礎上 加上幾年、幾月。
  • ZonedDateTime:該類代表一個時區化的日期、時間。
  • Zoneld:該類代表一個時區。
  • DayOfWeek:這是一個枚舉類,定義了周日到周六的枚舉值
  • Month:這也是一個枚舉類,定義了一月到十二月的枚舉值。

下面通過一個簡單的程式來示範這些類的用法。

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.MonthDay;
import java.time.Year;

public class NewDatePackageTest
{
	public static void main(String[] args)
	{
//		-------下面關于Clock的用法---------
//		擷取目前的Clock
		var clock = Clock.systemUTC();
//		通過Clock擷取目前時刻
		System.out.println("目前時刻為"+clock.instant());
//		擷取Clock對應的毫秒數,與System.currentTimeMills()輸出相同
		System.out.println(clock.millis());
		System.out.println(System.currentTimeMillis());
//		--------下面關于Duration用法--------
		var d = Duration.ofSeconds(6000);
		System.out.println("6000秒相當于"+d.toMinutes()+"分");
		System.out.println("6000秒相當于"+d.toHours()+"小時");
		System.out.println("6000秒相當于"+d.toDays()+"天");
//		在clock基礎上增加6000秒,傳回新的clock
//		offset:擷取一個時鐘,從添加了指定持續時間的指定時鐘傳回瞬時值
		var  clock2 = Clock.offset(clock, d);
//		可以看出clock與clock2相差的時間
		System.out.println("目前時刻加6000秒為:"+clock2.instant());
//		-------下面關于Instant的用法---------
//		擷取目前時間
		var instant = Instant.now();
		System.out.println(instant);
		var instant2 = instant.plusSeconds(6000);
		System.out.println(instant2);
//		根據字元串解析instant對象
		var instant3 = Instant.parse("2014-02-03T20:18:09Z");
		System.out.println(instant3);
		var instant4 = instant3.plus(Duration.ofHours(1).plusMinutes(4));
		System.out.println(instant4);
//		擷取Instant4 5天前的時刻
		var instant5 = instant4.minus(Duration.ofDays(5));
//		-------下面關于LoaclDate的用法---------
		var localdate = LocalDate.now();
		System.out.println(localdate);
//		擷取2021年的第219天
		localdate = LocalDate.ofYearDay(2021, 219);
		System.out.println(localdate);
//		設定為2021年5月21日
		localdate = LocalDate.of(2021, Month.MAY, 21);
		System.out.println(localdate);
//		-------下面關于LocalTime的用法---------
		var localtime = LocalTime.now();
		System.out.println(localtime)
		localtime = LocalTime.of(22, 01);
		System.out.println(localtime);
//		傳回一天中的5503秒
		localtime = LocalTime.ofSecondOfDay(5503);
		System.out.println(localtime);
//		-------下面關于LocalDateTime的用法---------
		var localdatetime = LocalDateTime.now();
		System.out.println(localdatetime);
		var future = localdatetime.plusHours(25).plusMinutes(3);
		System.out.println(future);
//		-------下面關于Year,YearMonth,MonthDay的用法---------
		var year = Year.now();
		System.out.println(year);
		var futureyear = year.plusYears(5);
		System.out.println(futureyear);
//		根據指定月份擷取YearMonth
		var ym = year.atMonth(10);
		System.out.println(ym);
		ym = ym.plusYears(5).minusYears(3);
		System.out.println(ym);
		var md = MonthDay.now();
		System.out.println(md);
		var md2 = MonthDay.of(5, 22);
		System.out.println(md2);
	}
}
           

該程式就是這些常用類的用法示例,這些API和他們的方法都非常簡單。

7.5 正規表達式

正規表達式是一個強大的字元串處理工具,可以對字元串進行查找、提取、分割、替換等操作。String類裡也提供了幾個特殊的方法:

  • boolean matches(String regex):判斷該字元串是否比對指定的正規表達式。
  • String replaceAll(String regex,String replacement):将該字元串中所比對regex的子串替換成replacement。
  • String replaceFirst(String regex,String replacement):将該字元串中第一個比對regex的子串替換成replacement。
  • String[ ] split(String regex):以regex作為分隔符,把該字元串分割成多個子串。

Java還提供Pattern和Matcher兩個類專門用于提供正規表達式支援。

Matcher類:一種引擎,通過解釋模式對字元序列執行比對操作。

比對器是通過調用模式的比對器方法來建立的。一旦建立,比對器可用于執行三種不同的比對操作:

•matcher():方法嘗試将整個輸入序列與模式比對。

•lookingAt():方法嘗試比對輸入序列,從開頭開始,與模式比對。

•find():方法掃描輸入序列,尋找下一個比對模式的子序列。

每個方法都傳回一個訓示成功或失敗的布爾值。關于成功比對的更多資訊可以通過查詢比對器的狀态來獲得。

比對器在稱為region的輸入子集中查找比對項。預設情況下,該區域包含比對器的所有輸入。可以通過region方法修改region,通過regionStart和regionEnd方法查詢region。區域邊界與某些模式結構互動的方式可以改變。

這個類還定義了用新字元串替換比對的子序列的方法,如果需要,可以從matchresult計算新字元串的内容。

可以同時使用appendReplacement和appendTail方法,以便将結果收集到現有的字元串緩沖區或字元串生成器中。

或者,可以使用更友善的replaceAll方法建立一個字元串,其中輸入序列中的每個比對子序列都被替換。

7.5.1 建立正規表達式

正規表達式所支援的合法字元

字元 解釋
x 字元x(x可代表任何合法字元)
\0mnn 八進制數0mnn所表示的字元
\xhh 十六進制xhh所表示的字元
\uhhhh 十六進制uhhhh所表示的字元
\t 制表符
\n 換行符
\r 回車符
\f 換頁符
\a 報警符
\e Escape符
\cx x對應的控制符。例如,\cM比對ctrl+M。x必須為A~Z或a ~ z之一。

正規表達式中的特殊字元

字元 說明
$ 比對一行的結尾。要比對$字元本身,請使用\$
^ 比對一行的開頭。要比對^字元本身,請使用\ ^
() 标記子表達式的開始和結束位置。要比對這些字元,請使用\(和\)
[ ] 用于确定中括号表達式的開始和結束位置。要比對這些字元,請使用\[和\]
{ } 用于标記前面子表達式的出現頻度。要比對這些字元,請使用\{和\}
* 指定前面子表達式可以出現零次或多次。要比對*字元本身,請使用\ *
+ 指定前面子表達式可以出現一次或多次。要比對+字元本身,請使用\+
? 指定前面子表達式可以出現零次或一次。要匹?字元本身,請使用\?
. 比對除換行符n之外的任何單字元。要比對.字元本身,請使用\.
\ 用于轉義下一個字元,或指定八進制、十六進制字元。如果需比對\字元,請用\\
| 指定兩項之間任選一項。如果要比對|字元本身,請使用\

将上面多個字元拼起來,就可以建立一個正規表達式。例如:

"\u0041\\\\"//比對A\
"\u0061\t " //比對a<制表符>  
"\\?\\["//比對?[
           

注意:可能有讀者覺得第一個正規表達式中怎麼有那麼多反斜杠啊?這是由于Java字元串中反斜杠本身需要轉義,是以兩個反斜杠(\\)實際上相當于一個(前一個用于轉義)。

上面的正規表達式依然隻能比對單個字元,這是因為還未在正規表達式中使用“通配符”,“通配符是可以比對多個字元的特殊字元。正規表達式中的通配符”遠遠超出了普通通配符的功能,它被稱 預定義字元,正規表達式支援如下表所示的預定義字元。

預定義字元 說明
. 可以比對任何字元
\d 比對0~9的所有數字
\D 比對非數字
\s 比對所有的空白字元,包括空格、制表符、回車符、換頁符、換行符等
\S 比對所有的非空白字元
\w 比對所有的單詞字元,包括0~9所有數字、26個英文字母和下畫線(_)
\W 比對所有的非單詞字元

提示:上面的7個預定義字元其實很容易記憶,d是dit的意思,代表數字;s是 space 的意思,代表空白;w是word的意思,代表單詞。d、s、w的大寫形式恰好比對與之相反的字元。

有了上面的預定義字元後,接下來就可以建立更強大的正規表達式了。例如:

c\\wt  //可以比對cat、cbt、cct、cot、c9t等一批字元串
\\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d //比對如000-000-0000形式的電話号碼
           

在一些特殊情況下,例如,若隻想比對a~f的母,或者比對除ab之外的所有小寫字母,或者比對中文字元,上面這些預定義字元就無能為力了,此時就需要使用方括号表達式,方括号表達式有如下表所示的幾種形式。

方括号表達式

方括号表達式 說明
表示枚舉 例如[abc],表示a、b、c其中任意一個字元;[gz],表示g,z其中任意一個字元
表示範 圍:- 例如[a-f],表示a~f範圍内的任意字元;[\\u0041-\\u0056],表示十六進制字元\u0041到\u0056範圍的字元。表示範圍: 範圍可以和枚舉結合使用,如[a-cx-z],表示a~c、x ~z範圍内的任意字元
表示求否:^ 例如[^abc],表示非a、b、c的任意字元;[^a-f]表示不是a-f 範圍内的任意字元
表示“與”運算,&&

例如[a-z&&[def]],求a~z和[def]的交集,表示d、e或f

[a-z&&[ ^bc]],a-z範圍内的所有字元,除b和c之外,即[ad-z],

[a-z&&[ ^m-p]]範圍内的所有字元,除m~p範圍之外的字元,即[a-lq-z]

表示“并”運算 并運算與前面的枚舉類似。例如[a-d[m-p]],表示[a-dm-p]即a~d,m ~p。

提示:方括号表達式比前面的預定義字元靈活多了,幾乎可以比對任何字元。例如,若需要 比對所有的中文字元,就可以利用u0041-056形式因為所有中文字元的 Unicode 值是連續的,隻要找出所有中文字元中最小、最大的 Unicode值,就可以利用上面形式來 比對所有的中文字元。

正則表示還支援圓括号表達式,用于将多個表達式組成一個子表達式,圓括号中可以使用或運算符(|)。例如,正規表達式“( (public)|(protected)|(private))”用于比對Java的三個通路控制符其中之一。

除此之外,Java正規表達式還支援如下表的幾個邊界比對符。

邊界比對符 說明
^ 行的開頭
$ 行的結尾
\b 單詞的邊界
\B 非單詞的邊界
\A 輸入的開頭
\G 前一個比對的結尾
\Z 輸入的結尾,僅用于最後的結束符
\z 輸入的結尾

前面例子中需要建一個000-000-0000式的電話号碼時,使用了\d\d\d-\d\d\d-\d\d\d\d 正規表達式,這看起來比較煩瑣。實際上,正規表達式還提供了數量辨別符,正規表達式支援的數量辨別符有如下幾種模式。

  • Greedy(貪婪模式):數量表示符預設采用貪婪模式,除非另有表示。貪婪模式的表達式會一直比對下去,直到無法比對為止。如果你發現表達式比對的結果與預期的不符,很有可能是因為——你以為表達式隻會比對前面幾個字元,而實際上它是貪婪模式,是以會一直比對下去。
  • Reluctant(勉強模式):用問号字尾(?)表示,它隻會比對最少的字元。也稱為最小比對模式 。
  • PossessIve(占有模式):用加号字尾(+)表示,目前隻有Java支援占有模式,通常比較少用。

    三種模式的數量表示符

    貪婪模式 勉強模式 占用模式 說明
    X? X?? X?+ X表達式出現零次或一次
    X* X*? X*+ X表達式出現零次或多次
    X+ X+? X++ X表達式出現一次或多次
    X{n} X{n}? X{n}+ X表達式出現n次
    X{n,} X{n,}? X{n,}+ X表達式最少出現n次
    X{n,m} X{n,m}? X{n,m}+ X表達式最少出現n次,最多出現m次
    貪婪模式與勉強模式的對比:
    String str = "Hello ,world!";
    //貪婪模式的正規表達式
    System.out.println(str.replaceFirst("\\w*","    "));//輸出    ,world!
    //勉強模式的正規表達式
    System.out.println(str.replaceFirst("\\w*?","    "));//輸出  helle,world!
               
    當從“Hello,world!”字元串中查找比對"//w*"子串時,因為“//w *”使用了貪婪模式,數量表示符( *)會一直比對下去,是以該字元串前面的所有的單詞字元都會被比對到,直到遇到空格;如果使用勉強模式,數量表示符 ( *)會盡量比對最少字元,即比對0個字元。

    7.5.2 使用正規表達式

    **正規表達式字元串必須先被編譯為Pattern對象,然後利用該Pattern對象建立對應的Matcher對象。**執行比對所涉及的狀态保留在Matcher對象中,多個Matcher可共享一個Pattern對象。
    //将一個字元串編譯成Pattern對象
    Pattern p = Pattern.compile("a*b");
    //使用Pattern對象建立Matcher()對象
    //matcher()自動把指定字元串編譯成匿名的Pattern對象,并執行比對
    Matcher m = p.matcher("aaaab");
    /*matches():試圖将整個區域與模式進行比對。
    如果比對成功,則可以通過start、end和group方法獲得更多資訊。
    當且僅當整個區域序列比對此比對器的模式時為真*/
    boolean b = m.matches(); //傳回true
               

    上面定義的Pattern對象可以多次重複使用。如果某個正規表達式隻需要一次使用,則可以直接使用Pattern類的matcher方法,此方法可以自動把指定字元串編譯成匿名的Pattern對象,并執行比對

    這句等效于以上三局,但采用這種語句都需要重新編譯新的Pattern對象,不能重複利用以編譯的Pattern對象,是以效率不高。

    Matcher類中的幾種方法:

    • find():傳回目标字元串中是否包含Pattern比對的子串。
    • group():傳回上一次與Pattern比對的子串。
    • start():傳回上一次與Pattern比對的子串在目标字元串中的開始位置。
    • end():傳回上一次與Pattern比對的子串在目标字元串中的結束位置加1。
    • lookungAt():傳回目标字元串前面部分與Pattern是否比對。
    • matches():傳回整個目标字元串與Pattern是否比對。
    • reset():将現有的Matcher對象應用于一個新的字元串序列。
    使用Maycher類的find()和group()方法可以從目标字元串中依次取出特定子串(比對這則表達式的子串),例如網際網路的網絡爬蟲。他們可以自動從網頁上識别出所有的電話号碼。
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class FindGroup
{
	public static void main(String[] args)
	{
//		使用字元串模拟從網絡上得到網頁的源碼
		var str = "我想求購一本《瘋狂Java講義》,盡快聯系我15333634564"
				+"交朋友,電話号碼是17333636698"
				+"美女的微信是13165489874"
				+"出售二手電腦,聯系方式15633644562";
//		建立一個Pattern對象,并用它建立一個Matcher對象
//		該正規表達式要抓取17x和15x段的手機号
//		實際要抓取哪些代碼隻需要修改正規表達式即可
//		compile:将給定的正規表達式編譯成模式。
//		matcher():建立一個比對器,它将根據這個模式比對給定的輸入。
		Matcher m = Pattern.compile("((13\\d)|(15\\d))\\d{8}").matcher(str);
//		将所有符合正規表達式的子串全部輸出
//		find():嘗試查找與模式比對的輸入序列的下一個子序列。
		while(m.find())
		{
//			group():傳回與前一個比對項比對的輸入子序列。
			System.out.println(m.group());
		}
	}
}
           

find()方法依次查找字元串中與Pattern比對的子串,一旦找到對應的子串,下次調用find()方法時将接着向下查找。

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class StartEnd
{
	public static void main(String[] args)
	{
//		建立一個Pattern對象,并用它建立一個Matcher對象
		var regStr = "Java is very easy!";
		System.out.println("目标字元串是:" + regStr);
		Matcher m = Pattern.compile("\\w+").matcher(regStr);
		while(m.find())
		{
			System.out.println(m.group()+"子串的起始位置:"
					+m.start()+"其結束位置"+m.end());
		}
	}
}
           

上面程式使用find()、group()方法逐項取出字元串與指定正規表達式比對的子串,并使用start()、end()方法傳回子串在目标字元串中的位置。

matches()和lookingAt()方法有點相似,隻是matches()方法要求整個字元串和Pattern完全比對時,才傳回true,而lookingAt()隻要以Pattern開頭就會傳回true。reset()方法可将現有的Matcher對象應用于新的字元序列。

//import java.util.regex.Pattern;
import java.util.regex.*;

public class MatchesTest
{
	public static void main(String[] args)
	{
		String[] mails = 
			{
					"[email protected]",
					"[email protected]",
					"sjndbbcdv.org",
					"whfioavl.xx"
			};
		var mailRegEx = "\\w(3,20)@\\w+\\.(com|org|cn|net|gov)";
		var mailPattern = Pattern.compile(mailRegEx);
		Matcher matcher = null;
		for(var mail:mails)
		{
			if(matcher == null)
			{
				matcher = mailPattern.matcher(mail);
			}
			else
			{
//				reset:用一個新的輸入序列重置這個比對器。
				matcher.reset(mail);
			}
			String result = mail +(matcher.matches()?"是":"不是")
					+"一個有效的郵件位址";
			System.out.println(result);
		}
	}
}
           

上面程式建立了一個郵件位址的Pattern,接着用這個Pattern與多個郵箱位址進行比對。當程式的Matcher為null時,程式調用matcher()來建立一個Matcher對象,一旦Mathcer對象被建立,程式就調用reset()方法将Mathcer應用到新的字元序列。

對目标字元串進行分割、查找、替換等操作:

import java.util.regex.Pattern;
import java.util.regex.*;

public class ReplaceTest
{
	public static void main(String[] args)
	{
		String[] msgs =
		{ "Java hsa regular expressions in 1.4", "regular expressions now expressing in Java",
				"Java represses oracular expressions" };
		var p = Pattern.compile("re\\w*");
		Matcher matcher = null;
		for (var i = 0; i < msgs.length; i++)
		{
			if(matcher == null)
			{
				matcher = p.matcher(msgs[i]);
			}
			else
			{
				matcher.reset(msgs[i]);
			}
//			String replaceAll():将該字元串中所有比對regex的子串退換成replacement
			System.out.println(matcher.replaceAll("哈哈:)"));
		}
	}
}
           

上面程式使用了Matcher類提供的replaceAll()把字元串中所有與正規表達式比對的子串替換成 哈哈:)“,實際上,Mathcer類還提供了一個replaceFirst(),把方法隻替換第一個比對的子串,運作上面程式,會看到字元串中所有以”re“開頭的單詞都會替換成 哈哈:)”。

String類中也提供了replaceAll()、replaceFirst()、split()等方法,下面的例子直接使用String類提供的正規表達式功能來進行替換和分割。

import java.util.Arrays;

public class StringReg
{
	public static void main(String[] args)
	{
		String[] mags =
		{ "Java hsa regular expressions in 1.4", "regular expressions now expressing in Java",
				"Java represses oracular expressions" };
		for(var mag : mags)
		{
			System.out.println(mag.replaceFirst("re\\w*", "哈哈"));
			System.out.println(Arrays.toString(mag.split(" ")));
		}
	}
}
           

上面程式隻使用String類的replaceFirst()和split()方法對目标對目标字元串進行了一次替換和分割。

7.6 變量處理和方法處理

Java 9 引入了一個新的VarHandle類,并增強了原有的MethodHandle類。通過這兩個類,允許Java像動态語言一樣引用變量、引用方法,并調用它們。

7.6.1 Java 9 增強的MethodHandle

這種方法引用是一種輕量級的引用方式,他不會檢查方法的通路權限,也不會管方法所屬的類、執行個體方法或靜态方法,MethodHandle就是簡單代表特定的方法,并可通過MethodHandle來調用方法。

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.regex.*;

public class MethodHandleTest
{
//	定義一個private類方法
	private static void hello()
	{
		System.out.println("Hello,World!");
	}

//	定義一個private執行個體方法
	private String hello(String name)
	{
		System.out.println("執行帶參數的hello"+ name);
		return name +"您好";
	}
	public static void main(String[] args) throws Throwable
	{
//		定義一個傳回值為void,不帶形參的方法類型。
		var type = MethodType.methodType(void.class);
//		使用MethodHandles.lookup的findStatic擷取類方法。
		var mtd = MethodHandles.lookup()
				.findStatic(MethodHandleTest.class,"hello",type);
//		通過MethodHandle執行方法
		mtd.invoke();
//		使用MethodHandles.lookup()的findVirtual擷取執行個體方法
		var mtd2 = MethodHandles.lookup()
				.findVirtual(MethodHandleTest.class,"hello",
//						指定擷取傳回值為String、形參為String的方法類型
						MethodType.methodType(String.class,String.class));
//		通過MethodType執行方法,傳入主調對象和參數
		System.out.println(mtd2.invoke(new MethodHandleTest(),"孫悟空"));
	}
}
           

從上面三行粗體字代碼中可以看出,程式使用MethodHandles.lookup對象根據類、方法名,方法類型來擷取MethodHandle對象。由于此處的方法名隻是一個字元串,而該字元串可以來自變量、配置檔案等,這意味着MethodHandle可以讓Java動态的調用某個方法。

7.6.2 Java 9 新增的varHandle

用于動态的操作數組或者對象的成員變量。

import java.lang.invoke.MethodHandles;
import java.util.Arrays;

class User7
{
	String name;
	static int MAX_AGE;
}
public class VarHandleTest
{
	public static void main(String[] args) throws Throwable
	{
		var sa = new String[] {"Java","Kotlin","Go"}; 
//		擷取一個String[]數組的VarHandle對象
		var avh = MethodHandles.arrayElementVarHandle(String[].class);//粗體·
//		比較并設定:2表示第三個元素,如果第三個元素時Go,則被設定為Lua
		var r = avh.compareAndSet(sa,2,"Go","Lua");//粗體·
		System.out.println(r);
		System.out.println(Arrays.toString(sa));
//		擷取并設定:
		System.out.println(avh.getAndSet(sa,2,"Swift"));
		System.out.println(Arrays.toString(sa));
		
//		用findVarHandle方法擷取uesr7類中名為name
//		類型為String的執行個體變量
		var vh1 = MethodHandles.lookup().findVarHandle(User7.class, 
				"name",String.class);  //粗體·
		var user = new User7();
//		通過varHandle擷取執行個體變量的值,需要傳入對象作為調用者
		System.out.println(vh1.get(user));//粗體·
//		通過varHandle擷取執行個體變量的值,需要傳入對象作為調用者
		vh1.set(user,"孫悟空");//粗體·
//		輸出user的name執行個體的值
		System.out.println(user.name);
//		用findVarHandle方法擷取User類中名為MAX_AGE
//		類型為Integer的類變量
		var vh2 = MethodHandles.lookup().findStaticVarHandle(User7.class, 
				"MAX_AGE", int.class);
		System.out.println(vh2.get());
		vh2.set(88);
		System.out.println(User7.MAX_AGE);
	}
}
           

粗可以看出,程式調用MethodHandle類的靜态方法可擷取操作數組的VarHandle對象,接下來程式可以通過VarHandle對象來操作數組的方法,包括比較并設定數組元素、擷取并這隻數組元素等,VarHandle具體支援哪些方法則可參考API文檔。

後三粗示範了使用VarHandle操作執行個體變量的情景,由于執行個體變量需要使用對象來通路,是以使用VarHandle操作執行個體變量時需要傳入一個User對象。

操作類變量和執行個體變量差别不大,差別隻是類變量不需要對象,而執行個體變量需要對象,是以VarHandle操作類變量是無須傳入對象作為參數。

當程式通過MethodHandles.Lookup來擷取成員變量時,可根據字元串名稱來擷取成員變量,這個字元串名稱同樣是可以動态改變的。

7.7 Java 11改進的國際化與格式化

國際化(Internationaliaztion)簡稱I18N。

7.7.2 Java支援的國家或語言

調用Locale類的getAvailableLocales()方法,該方法傳回一個Locale數組,該數組中包含了Java所支援的國家和語言。

import java.util.Locale;

public class LocaleList
{
	public static void main(String[] args)
	{
//		傳回Java所支援的全部國家和語言的數組
		Locale[] localeList = Locale.getAvailableLocales();
//		周遊每個數組的元素,依次擷取所支援的國家和語言
		for(var i = 0;i<localeList.length;i++)
		{
			System.out.println(localeList[i].getDisplayCountry()
					+"="+localeList[i].getCountry()+" "
					+localeList[i].getDisplayLanguage()
					+"="+localeList[i].getLanguage());
		}
	}
}
           

7.7.3 完成程式國際化

import java.util.Locale;
import java.util.ResourceBundle;

public class Hello
{
	public static void main(String[] args)
	{
//		獲得系統預設的國家\語言環境
		var myLocale = Locale.getDefault(Locale.Category.FORMAT);
//		根據指定的國家/語言環境加載資源檔案
		var bundle = ResourceBundle.getBundle("mess",myLocale);
		System.out.println(bundle.getString("Hello"));
	}
}
           

7.7.4 使用MessageFormat處理包含占位符的字元串

myMess_en_US.properties

msg = Hello,{0}!Today is {1}.
           
import java.text.MessageFormat;
import java.util.Date;
import java.util.Locale;
import java.util.ResourceBundle;

public class HelloArg
{
	public static void main(String[] args)
	{
//		定義一個Locale變量
		Locale currentLocale = null;
//		運作程式指定了兩個參數
		if(args.length == 2)
		{
//			使用運作時的兩個參數構造Locale執行個體
			currentLocale = new Locale(args[0],args[1]);
		}
		else
		{
//			否則直接用系統預設的Locale
			currentLocale = Locale.getDefault(Locale.Category.FORMAT);
		}
//		根據Locale加載語言資源
		var bundle = ResourceBundle.getBundle("myMess",currentLocale);
//		取得已加載中的資源檔案中的msg對應消息
		var msg = bundle.getString("msg");
//		使用MessageFormat為帶占位符的字元串傳入參數
		System.out.println(MessageFormat.format(msg, "Mr.zhang",new Date()));
	}
}
           

對于占位符字元串,隻需要使用MessageFormat類的format()方法為消息中的占位符指定參數即可。

7.7.5 使用類檔案代替資源檔案

除使用屬性檔案為資源檔案外,Java也允許使用類檔案代替資源檔案,即将所有的key-value對存入class檔案,而不是屬性檔案。

使用類檔案來代替資源檔案必須滿足如下條件。

  • 該類的類名必須是baseName_language_country,這與屬性檔案的命名相似。
  • 該類必須繼承ListResourceBundle,并重寫getContents()方法,該方法傳回Object數組,該數組的每一項都是鍵值對(key-value對)。
import java.util.ListResourceBundle;

public class myMess_zh_CN extends ListResourceBundle
{
	private final Object myData[] []=
		{
				{
					"msg","{0},你好!今天的日期是{1}"
				}
		};
//	重寫getContents()方法
	public Object[][]getContents()
	{
//		該方法傳回資源的key-value對
		return myData;
	}
}
           

上面檔案是一個簡體中文語言環境的資源檔案,該檔案可以代替myMess_zh_CN.properties 檔案;如果需要代替美國英語語言環境的資源檔案,則還應該提供一個myMess_en_US 類。

如果系統同時存在資源檔案、類檔案,系統将以類檔案為主,而不會調用資源檔案。對于簡體中文的 Locale,ResourceBundle 搜尋資源檔案的順序是:

(1)baseName_zh_CN.class

(2)baseName_zh_CN.properties(3)baseName_zh.class

(4)baseName_zh.properties

(5)baseName.class

系統按上面的順序搜尋資源檔案,如果面的檔案不存在,才會使用下一個檔案,如果一直找不到對應的檔案,系統将抛出異常。

7.7.6 Java 9新增的日志API

7.7.7 使用NumberFormat格式化數字

MessageFormat是抽象類Format的子類,Format抽象類還有兩個子類:NumberFormat和DateFormat,兩個子類中提供了兩個方法:format()和parse()方法,format()用于将數值、日期轉換為字元串,parse()用于将字元串裝換為數值和日期。

import java.text.NumberFormat;
import java.util.Locale;

public class NumberFormatTest
{
	public static void main(String[] args)
	{
		var bd = 11230000.76;
		Locale[] locales = {
				Locale.CHINA,Locale.JAPAN,Locale.GERMAN,Locale.US
		};
		var nf = new NumberFormat[12];
//		為上面的Locale建立12個NumberFormat對象
//		每個Locale分别有數值格式器、百分數格式器、貨币格式器
		for(var i = 0;i<locales.length;i++)
		{
			nf[i*3] = NumberFormat.getNumberInstance(locales[i]);
			nf[i*3+1] = NumberFormat.getPercentInstance(locales[i]);
			nf[i*3+2] = NumberFormat.getCurrencyInstance(locales[i]);
		}
		for(var i = 0;i<locales.length;i++)
		{
			var tip = i == 0 ? "--------中國格式--------"
					:i==1? "--------日本格式--------":
						i==2?"--------德國格式--------":"--------美國格式--------";
			System.out.println(tip);
			System.out.println("通用的數值格式:"
					+nf[i*3].format(bd));
			System.out.println("百分比數值格式:"
					+nf[i*3+1].format(bd));
			System.out.println("貨币數值格式:"
					+nf[i*3+2].format(bd));
		}
	}
}
           

7.7.8 使用DateFormat格式化日期、時間

import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.Locale;

public class DateFormatTest
{
	public static void main(String[] args) throws ParseException
	{
//		需要格式化的時間
		var dt = new Date();
		Locale[] locales = {Locale.CHINA,Locale.US};
		var df = new DateFormat[16];
//		為上面兩個Locale建立16個DateFormat對象
		for(var i = 0;i<locales.length;i++)
		{
			df[i*8] = DateFormat.getDateInstance(DateFormat.SHORT,locales[i]);
			df[i*8+1] = DateFormat.getDateInstance(DateFormat.MEDIUM,locales[i]);
			df[i*8+2] = DateFormat.getDateInstance(DateFormat.LONG,locales[i]);
			df[i*8+3] = DateFormat.getDateInstance(DateFormat.FULL,locales[i]);
			df[i*8+4] = DateFormat.getTimeInstance(DateFormat.SHORT,locales[i]);
			df[i*8+5] = DateFormat.getTimeInstance(DateFormat.MEDIUM,locales[i]);
			df[i*8+6] = DateFormat.getTimeInstance(DateFormat.LONG,locales[i]);
			df[i*8+7] = DateFormat.getTimeInstance(DateFormat.FULL,locales[i]);
		}
		for(var i = 0;i<locales.length;i++)
		{
			var tip = i == 0 ? "--------中國格式日期--------"
					:"--------美國日期格式--------";
			System.out.println(tip);
			System.out.println("通用的SHORT格式:"
					+df[i*8].format(dt));
			System.out.println("通用的MEDIUM格式:"
					+df[i*8+1].format(dt));
			System.out.println("通用的LONG格式:"
					+df[i*8+2].format(dt));
			System.out.println("通用的FULL格式:"
					+df[i*8+3].format(dt));
			System.out.println("通用的SHORT格式:"
					+df[i*8+4].format(dt));
			System.out.println("通用的SHORT格式:"
					+df[i*8+5].format(dt));
			System.out.println("通用的SHORT格式:"
					+df[i*8+6].format(dt));
			System.out.println("通用的SHORT格式:"
					+df[i*8+7].format(dt));
		}
		System.out.println("---------------下面為測試是否采用嚴格文法---------------");
		var str1 = "2021/2/31";
		var str2 = "2021年2月31日";
		System.out.println(DateFormat.getDateInstance().parse(str2));
		System.out.println(DateFormat.getDateInstance(DateFormat.SHORT).parse(str1));
        //引發異常,因為str1是一個SHORT字元串,必須使用SHORT樣式的DateFormat執行個體解析
		System.out.println(DateFormat.getDateInstance().parse(str1));
	}
}
           

7.7.9 使用SimpleDateFormat格式化日期

前面介紹的DateFormat的parse()方法可以把字元串解析成Date對象,但實際上DateFormat的parse()方法不夠靈活——他被要求解析的字元串必須滿足特定的格式!為了更好地格式化日期、解析日期字元串,Java提供了SimpleDateFormat類。

SimpleDateFormat是DateFormat的子類,正如它的名字所暗示的,他是簡單的日期格式器。很多讀者對“簡單的”格式器不屑一顧,實際上SimpleDateFormat 比DateFormat更簡單,功能更強大。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class SimpleDateFormatTest
{
	public static void main(String[] args) throws ParseException
	{
		var d = new Date();
		var sdf1 = new SimpleDateFormat("Gyyyy年中的第D天");
//		将D格式化為日期
		var dateStr = sdf1.format(d);
		System.out.println(dateStr);
//		一個非常特殊的日期字元串
		var str = "14####3月##21";
		var sdf2 = new SimpleDateFormat("y####MMM##d");
//		将日期字元串解析成日期,輸出:Fri Mar 21 00:00..........
		System.out.println(sdf2.parse(str));
	}
}
           

這樣的字元串解析成日期,功能非常強大。格式化怎麼樣的字元串完全取決于建立該對象是指定的pattern參數,oattern是一個使用日期字段占位符的日期模闆。

7.8 Java 8新增的日期、時間格式器

DateTimeFormatter類相當與前面介紹的DateFormat和SimpleDateFormatter的合體,功能十分強大。

使用DateTimeFormatter進行格式化或解析,必須進行格式化或解析,必須先擷取DateTimeFormatter對象。

7.8.1 使用DateTimeFormatter完成格式化

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;

public class NewFormatterTest
{
	public static void main(String[] args)
	{
		var formatters = new DateTimeFormatter[] 
				{
//						直接使用常量建立DateTimeFormatter格式器
						DateTimeFormatter.ISO_LOCAL_DATE,
						DateTimeFormatter.ISO_LOCAL_TIME,
						DateTimeFormatter.ISO_LOCAL_DATE_TIME,
//						使用本地化的不同風格來建立DateTimeFormatter格式器
						DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL,FormatStyle.MEDIUM),
						DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG),
//						根據模式字元串來建立DateFormatter格式化
						DateTimeFormatter.ofPattern("Gyyyy%%MMM%%dd HH:mm:ss")
				};
		var date = LocalDateTime.now();
//		依次使用不同的格式器對LocalDateTime進行格式化
		for(var i = 0;i < formatters.length;i++)
		{
			System.out.println(date.format(formatters[i]));//粗
			System.out.println(formatters[i].format(date));//粗
		}
	}
}
           

粗從不同方式來格式化日期。

7.8.2 使用DateTimeFoematter解析字元串

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class NewFormatterParse
{
	public static void main(String[] args)
	{
//		定義一個任意格式的日期、時間字元串
		var str1 = "2012==04==12 01時07分09秒";
//		根據需要解析的日期、時間字元串定義解析所有的格式器
		var formatter1 = DateTimeFormatter.ofPattern("yyyy==MM==dd HH時jj分pp秒");
//		執行解析
		var dt1 = LocalDateTime.parse(str1,formatter1);
		System.out.println(dt1);
//省略第二個
		
	}
}
           

小結:

本章介紹了運作Java程式時的參數,并詳細解釋了main方法簽名的含義。為了實作字元界面程式與暈乎互動功能,介紹了兩種讀取鍵盤輸入的方法(hasNext(),next()),還介紹了System、Runtime、String、StringBuffer、StringBuilder、Math、BigDecimal、Random、Date、Calendar和TimeZone等常用類的用法。

重點介紹了正規表達式,以及使用Pattern、Matcher、String等類來使用正則。還介紹了程式國際化等,還介紹了新增的日期、時間包,以及新增的日期時間格式符。

SimpleDateFormatter的合體,功能十分強大。

使用DateTimeFormatter進行格式化或解析,必須進行格式化或解析,必須先擷取DateTimeFormatter對象。

7.8.1 使用DateTimeFormatter完成格式化

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;

public class NewFormatterTest
{
	public static void main(String[] args)
	{
		var formatters = new DateTimeFormatter[] 
				{
//						直接使用常量建立DateTimeFormatter格式器
						DateTimeFormatter.ISO_LOCAL_DATE,
						DateTimeFormatter.ISO_LOCAL_TIME,
						DateTimeFormatter.ISO_LOCAL_DATE_TIME,
//						使用本地化的不同風格來建立DateTimeFormatter格式器
						DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL,FormatStyle.MEDIUM),
						DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG),
//						根據模式字元串來建立DateFormatter格式化
						DateTimeFormatter.ofPattern("Gyyyy%%MMM%%dd HH:mm:ss")
				};
		var date = LocalDateTime.now();
//		依次使用不同的格式器對LocalDateTime進行格式化
		for(var i = 0;i < formatters.length;i++)
		{
			System.out.println(date.format(formatters[i]));//粗
			System.out.println(formatters[i].format(date));//粗
		}
	}
}
           

粗從不同方式來格式化日期。

7.8.2 使用DateTimeFoematter解析字元串

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class NewFormatterParse
{
	public static void main(String[] args)
	{
//		定義一個任意格式的日期、時間字元串
		var str1 = "2012==04==12 01時07分09秒";
//		根據需要解析的日期、時間字元串定義解析所有的格式器
		var formatter1 = DateTimeFormatter.ofPattern("yyyy==MM==dd HH時jj分pp秒");
//		執行解析
		var dt1 = LocalDateTime.parse(str1,formatter1);
		System.out.println(dt1);
//省略第二個
		
	}
}
           

小結:

本章介紹了運作Java程式時的參數,并詳細解釋了main方法簽名的含義。為了實作字元界面程式與暈乎互動功能,介紹了兩種讀取鍵盤輸入的方法(hasNext(),next()),還介紹了System、Runtime、String、StringBuffer、StringBuilder、Math、BigDecimal、Random、Date、Calendar和TimeZone等常用類的用法。

重點介紹了正規表達式,以及使用Pattern、Matcher、String等類來使用正則。還介紹了程式國際化等,還介紹了新增的日期、時間包,以及新增的日期時間格式符。