Java8學習之路-Java8的發展

NullPointer_C發表於2022-01-25

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以前,當我們要為一個方法傳遞多個型別相同的引數時, 我們有兩種方法解決

  1. 直接傳遞一個陣列過去
  2. 有多少個引數就傳遞多少個引數。

例如:

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 包括

  1. 執行緒互斥工具類:Lock,ReadWriteLock
  2. 執行緒通訊:Condition
  3. 執行緒池:ExecutorService
  4. 同步佇列:ArrayBlockingQueue
  5. 同步集合:ConcurrentHashMap,CopyOnWriteArrayList
  6. 執行緒同步工具: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可以在數值型別的變數裡新增下滑線,但是有幾個地方是不能新增的

  1. 數字的開頭和結尾
  2. 小數點前後
  3. 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()等等。

儘管預設方法有這麼多好處,但在實際開發中應該謹慎使用:在複雜的繼承體系中,預設方法可能引起歧義和編譯錯誤。

相關文章