Java8雖然提出了很多新特性,但是在日常寫專案和程式設計實踐的使用還不是特別熟悉,寫這個專欄記錄一下Java8的學習之路。
JDK5
自動裝、拆箱
從JDK5開始為所有基本資料型別都提供了與之對應的包裝類,使基本資料型別也能夠以OOP的方式來操作。
int -->Integer
double --> Double
long --> Long
char --> Character
float --> Float
boolean --> Boolean
short --> Short
byte -- > Byte
如下程式碼演示了基本資料型別和包裝類的相互轉換:
public class AutoBox {
public static void main(String[] args) {
int a = new Integer(66);
Integer b = 18;
Boolean flag = true;
boolean isBug = Boolean.FALSE;
}
}
自動裝箱:將基本資料型別轉換為物件:int
--> Integer
自動拆箱:將物件轉換為基本資料型別:Integer
--> int
對於JDK1.5之前集合不能儲存基本資料型別的問題,可使用自動裝、拆箱來解決。
列舉
列舉是 JDK1.5 推出的一個比較重要的特性。其關鍵字為 enum
例如:定義代表交通燈的列舉。常用於代表某些狀態值、識別符號或一些特定的型別。
public enum MyEnum{
RED,GREEN,YELLOW
}
for-each遍歷
常用於遍歷集合,可以簡化程式碼,邏輯更加清晰:
for(String s : strs){
System.out.println(s);
}
注意:使用for-each遍歷集合時,要遍歷的集合必須實現了Iterator介面
泛型
“泛型” 意味著編寫的程式碼可以被不同型別的物件所重用。 可見泛型的提出是為了編寫重用性更好的程式碼。 泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。
//給集合指定存入型別,上面這個集合在存入資料的時候必須存入String型別的資料,否則編譯器會報錯
List<String> strs = new ArrayList<String>();
靜態匯入
可以將類中的一些變數、方法以import static
的方式將其匯入,使被匯入類的靜態變數和方法於當前類可見,使用無需再給出全類名。
優點:程式碼簡潔;
缺點:過度使用會降低程式碼的可讀性,若某個方法重名時,會帶來歧義;
import static java.lang.System.out;
public class StaticImport {
public static void main(String[] args) {
out.println("Hi, Let's use the java 8!");
}
}
變長引數
在JDK1.5以前,當我們要為一個方法傳遞多個型別相同的引數時, 我們有兩種方法解決
- 直接傳遞一個陣列過去
- 有多少個引數就傳遞多少個引數。
例如:
public void printColor(String red,String green,String yellow){
// do something
}
或者:
public void printColor(String[] colors){
// do something
}
這樣編寫方法引數雖然能夠實現我們想要的效果,但是,這樣是不是有點麻煩呢? 再者,如果引數個數不確定,我們怎麼辦呢?Java JDK1.5為我們提供的可變引數就能夠完美的解決這個問題.
例如:
public void printColor(String... colors){
// do something
}
如果引數的型別相同,那麼可以使用 型別+三個點
,後面跟一個引數名稱的形式。 這樣的好處就是,只要引數型別相同,無論傳遞幾個引數都沒有限制 注意:可變引數必須是引數列表的最後一項(該特性對物件和基本資料型別都適用)。
執行緒併發庫
執行緒併發庫是 Java1.5 提出的關於多執行緒處理的高階功能,所在包:java.util.concurrent
包括
- 執行緒互斥工具類:Lock,ReadWriteLock
- 執行緒通訊:Condition
- 執行緒池:ExecutorService
- 同步佇列:ArrayBlockingQueue
- 同步集合:ConcurrentHashMap,CopyOnWriteArrayList
- 執行緒同步工具:Semaphore
JDK6
Compiler API
我們可以用JDK1.6 的Compiler API(JSR 199)去動態編譯Java原始檔, Compiler API結合反射功能就可以實現動態的產生Java程式碼並編譯執行這些程式碼,有點動態語言的特徵。
這個特性對於某些需要用到動態編譯的應用程式相當有用,比如JSP Web Server,當我們手動修改JSP後, 是不希望需要重啟Web Server才可以看到效果的,這時候我們就可以用Compiler API來實現動態編譯JSP檔案。 當然,現在的JSP Web Server也是支援JSP熱部署的,現在的JSP Web Server通過在執行期間通過Runtime.exec或ProcessBuilder來呼叫javac來編譯程式碼, 這種方式需要我們產生另一個程式去做編譯工作,不夠優雅而且容易使程式碼依賴與特定的作業系統; Compiler API通過一套易用的標準的API提供了更加豐富的方式去做動態編譯,而且是跨平臺的。
Console
JDK1.6 中提供了 java.io.Console
類專用來訪問基於字元的控制檯裝置。 你的程式如果要與 Windows 下的 cmd 或者 Linux 下的 Terminal 互動,就可以用 Console
類代勞。 但我們不總是能得到可用的 Console,一個JVM是否有可用的 Console 依賴於底層平臺和 JVM 如何被呼叫。 如果JVM是在互動式命令列(比如 Windows 的 cmd)中啟動的,並且輸入輸出沒有重定向到另外的地方,那麼就可以得到一個可用的 Console 例項。
Desktop類和SystemTray類
前者可以用來開啟系統預設瀏覽器瀏覽指定的URL,開啟系統預設郵件客戶端給指定的郵箱發郵件, 用預設應用程式開啟或編輯檔案(比如,用記事本開啟以 txt 為字尾名的檔案),用系統預設的印表機列印文件;
後者可以用來在系統托盤區建立一個托盤程式。
輕量級Http Server API
JDK1.6 提供了一個簡單的 Http Server API,據此我們可以構建自己的嵌入式 Http Server, 它支援Http和Https協議,提供了HTTP1.1的部分實現,沒有被實現的那部分可以通過擴充套件已有的 Http Server API來實現, 程式設計師必須自己實現 HttpHandler 介面,HttpServer 會呼叫 HttpHandler
實現類的回撥方法來處理客戶端請求, 在這裡,我們把一個 Http 請求和它的響應稱為一個交換,包裝成 HttpExchange
類,HttpServer
負責將 HttpExchange
傳給 HttpHandler
實現類的回撥方法。
對指令碼語言的支援
如:ruby,groovy,javascript。
下面展示瞭如何在Java中呼叫js程式碼。
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("ECMAScript");
JDK7
捕獲多異常
public static void first(){
try {
BufferedReader reader = new BufferedReader(new FileReader(""));
Connection con = null;
Statement stmt = con.createStatement();
} catch (IOException | SQLException e) {
//捕獲多個異常,e就是final型別的
e.printStackTrace();
}
}
優點:用一個 catch
處理多個異常,比用多個 catch
每個處理一個異常生成的位元組碼要更小更高效。
數字變數對下滑線的支援
JDK1.7可以在數值型別的變數裡新增下滑線,但是有幾個地方是不能新增的
- 數字的開頭和結尾
- 小數點前後
- F或者L前
int num = 1234_5678_9;
float num2 = 222_33F;
long num3 = 123_000_111L;
比如我們需要讓執行緒休眠10s,如果我們直接寫10000語義上不太清晰,而如果換成10_000
就比較清晰的可以表示休眠10s。
switch對String的支援
String status = "orderState";
switch (status) {
case "ordercancel":
System.out.println("訂單取消");
break;
case "orderSuccess":
System.out.println("預訂成功");
break;
default:
System.out.println("狀態未知");
}
try-with-resource
try-with-resources
是一個定義了一個或多個資源的 try 宣告,這個資源是指程式處理完它之後需要關閉它的物件。try-with-resources
確保每一個資源在處理完成後都會被關閉。
可以使用try-with-resources的資源有:任何實現了 java.lang.AutoCloseable
介面 java.io.Closeable
介面的物件。
例如:
public static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
在 java 7 以及以後的版本里,BufferedReader
實現了 java.lang.AutoCloseable
介面。 由於 BufferedReader
定義在 try-with-resources
宣告裡,無論 try
語句正常還是異常的結束, 它都會自動的關掉。而在 java7 以前,你需要使用 finally
塊來關掉這個物件。
建立泛型時型別推斷
只要編譯器可以從上下文中推斷出型別引數,你就可以用一對空著的尖括號 <>
來代替泛型引數。 這對括號私下被稱為菱形(diamond)。 在Java SE 7之前,你宣告泛型物件時要這樣
List<String> list = new ArrayList<String>();
而在Java SE7以後,你可以這樣
List<String> list = new ArrayList<>();
因為編譯器可以從前面(List)推斷出推斷出型別引數,所以後面的 ArrayList
之後可以不用寫泛型引數了,只用一對空著的尖括號就行。 當然,你必須帶著菱形 <>
,否則會有警告的。 Java SE7 只支援有限的型別推斷:只有構造器的引數化型別在上下文中被顯著的宣告瞭,你才可以使用型別推斷,否則不行。
List<String> list = new ArrayList<>();l
list.add("A");
//這個不行
list.addAll(new ArrayList<>());
// 這個可以
List<? extends String> list2 = new ArrayList<>();
list.addAll(list2);
Java8
Base64
對 Base64 編碼的支援已經被加入到Java 8官方庫中,這樣不需要使用第三方庫就可以進行Base64編碼,例子程式碼如下:
final String text = "Lets Learn Java 8!";
final String encoded = Base64
.getEncoder()
.encodeToString(text.getBytes(StandardCharsets.UTF_8));
System.out.println(encoded);
final String decoded = new String(
Base64.getDecoder().decode(encoded),
StandardCharsets.UTF_8);
System.out.println(decoded);
新的Base64API也支援URL和MINE的編碼解碼。
新的日期時間 API
Java 8引入了新的Date-Time API(JSR 310)來改進時間、日期的處理。時間和日期的管理一直是最令Java開發者痛苦的問題。 java.util.Date 和後來的 java.util.Calendar 一直沒有解決這個問題(甚至令開發者更加迷茫)。
因為上面這些原因,誕生了第三方庫Joda-Time,可以替代Java的時間管理API。 Java 8中新的時間和日期管理API深受Joda-Time影響,並吸收了很多Joda-Time的精華。 新的java.time
包包含了所有關於日期、時間、時區、Instant(跟日期類似但是精確到納秒)、duration(持續時間)和時鐘操作的類。 新設計的API認真考慮了這些類的不變性(從java.util.Calendar吸取的教訓),如果某個例項需要修改,則返回一個新的物件。
第二,關注下LocalDate和LocalTime類。LocalDate僅僅包含ISO-8601日曆系統中的日期部分;LocalTime則僅僅包含該日曆系統中的時間部分。這兩個類的物件都可以使用Clock物件構建得到。 LocalDateTime類包含了LocalDate和LocalTime的資訊,但是不包含ISO-8601日曆系統中的時區資訊。這裡有一些關於LocalDate和LocalTime的例子: 如果你需要特定時區的data/time資訊,則可以使用ZoneDateTime,它儲存有ISO-8601日期系統的日期和時間,而且有時區資訊。
lambda表示式
Lambda表示式(也稱為閉包)是Java 8中最大和最令人期待的語言改變。它允許我們將函式當成引數傳遞給某個方法, 或者把程式碼本身當作資料處理:函式式開發者非常熟悉這些概念。很多JVM平臺上的語言(Groovy、Scala等)從誕生之日就支援Lambda表示式,但是Java開發者沒有選擇,只能使用匿名內部類代替Lambda表示式。 Lambda的設計耗費了很多時間和很大的社群力量,最終找到一種折中的實現方案,可以實現簡潔而緊湊的語言結構。最簡單的Lambda表示式可由逗號分隔的引數列表、->符號和語句塊組成。
Lambda的設計者們為了讓現有的功能與Lambda表示式良好相容,考慮了很多方法,於是產生了函式介面這個概念。函式介面指的是隻有一個函式的介面,這樣的介面可以隱式轉換為Lambda表示式。java.lang.Runnable和java.util.concurrent.Callable是函式式介面的最佳例子。在實踐中,函式式介面非常脆弱:只要某個開發者在該介面中新增一個函式,則該介面就不再是函式式介面進而導致編譯失敗。為了克服這種程式碼層面的脆弱性,並顯式說明某個介面是函式式介面,Java 8 提供了一個特殊的註解@FunctionalInterface(Java 庫中的所有相關介面都已經帶有這個註解了)。
public class Lambda {
public static void main(String[] args) {
Arrays.asList("a", "b", "d").forEach(System.out::println);
}
}
函式式介面
lambda表示式的設計者為了讓lambda表示式和現有的介面有更好配合,提供了一個新的註解FunctionalInterface
用來標註這是一個函式式介面。會使編譯器在編譯器檢測介面是否只有一個抽象方法配合lambda表示式使用。
Optional
Optional
Java應用中最常見的bug就是空值異常。在Java 8之前,Google Guava引入了 Optionals
類來解決 NullPointerException
, 從而避免原始碼被各種 null
檢查汙染,以便開發者寫出更加整潔的程式碼。Java 8也將Optional加入了官方庫。 Optional
僅僅是一個容易存放T型別的值或者null。它提供了一些有用的介面來避免顯式的null檢查,可以參考Java 8官方文件瞭解更多細節。
如果Optional例項持有一個非空值,則 isPresent()
方法返回true,否則返回false;orElseGet()
方法,Optional例項持有null, 則可以接受一個lambda表示式生成的預設值;map()方法可以將現有的 Optional
例項的值轉換成新的值;orElse()方法與orElseGet()方法類似, 但是在持有null的時候返回傳入的預設值。
Streams
新增的Stream API(java.util.stream)將生成環境的函數語言程式設計引入了Java庫中。 這是目前為止最大的一次對Java庫的完善,以便開發者能夠寫出更加有效、更加簡潔和緊湊的程式碼。
Task 類有一個分數(或偽複雜度)的概念,另外還有兩種狀態:OPEN 或者 CLOSED。現在假設有一個task集合, 首先看一個問題:在這個task集合中一共有多少個OPEN狀態的點?在Java 8之前,要解決這個問題,則需要使用foreach迴圈遍歷task集合; 但是在Java 8中可以利用steams解決:包括一系列元素的列表,並且支援順序和並行處理。
final Collection<Task> tasks = Arrays.asList(
new Task(Status.OPEN, 5),
new Task(Status.OPEN, 13),
new Task(Status.CLOSED, 8)
);
// Calculate total points of all active tasks using sum()
final long totalPointsOfOpenTasks = tasks
.stream()
.filter(task -> task.getStatus() == Status.OPEN)
.mapToInt(Task::getPoints)
.sum();
System.out.println("Total points: " + totalPointsOfOpenTasks);
這裡有很多知識點值得說。首先,tasks集合被轉換成steam表示;其次,在steam上的filter操作會過濾掉所有CLOSED的task; 第三,mapToInt操作基於每個task例項的Task::getPoints方法將task流轉換成Integer集合;最後,通過sum方法計算總和,得出最後的結果。
更好的型別推斷
Java 8 編譯器在型別推斷方面有很大的提升,在很多場景下編譯器可以推匯出某個引數的資料型別,從而使得程式碼更為簡潔。
引數 Value.defaultValue()
的型別由編譯器推導得出,不需要顯式指明。在Java 7中這段程式碼會有編譯錯誤,除非使用 Value.<String>defaultValue()
。
並行陣列
Arrays.parallelSort
可以在多核情況下顯著提高對陣列排序的效率。
Nashron引擎
提供了nashron引擎可以在Java程式碼中直接編寫js程式碼執行。
介面的預設方法和靜態方法
Java 8使用兩個新概念擴充套件了介面的含義:預設方法和靜態方法。
預設方法使得介面有點類似traits,不過要實現的目標不一樣。預設方法使得開發者可以在 不破壞二進位制相容性的前提下,往現存介面中新增新的方法,即不強制那些實現了該介面的類也同時實現這個新加的方法。 預設方法和抽象方法之間的區別在於抽象方法需要實現,而預設方法不需要。介面提供的預設方法會被介面的實現類繼承或者覆寫 由於JVM上的預設方法的實現在位元組碼層面提供了支援,因此效率非常高。預設方法允許在不打破現有繼承體系的基礎上改進介面。該特性在官方庫中的應用是:給java.util.Collection介面新增新方法,如stream()、parallelStream()、forEach()和removeIf()等等。
儘管預設方法有這麼多好處,但在實際開發中應該謹慎使用:在複雜的繼承體系中,預設方法可能引起歧義和編譯錯誤。