【讀書筆記】《寫給大忙人看的Java SE 8》——Java8新特性總結

原文來自:https://www.cnblogs.com/justcooooode/p/7701260.html

接口中的默認方法和靜態方法

先考慮一個問題,如何向Java中的集合庫中增加方法?例如在Java 8中向Collection接口中添加了一個forEach方法。

如果在Java 8之前,對於接口來說,其中的方法必須都爲抽象方法,也就是說接口中不允許有接口的實現,那麼就需要對每個實現Collection接口的類都需要實現一個forEach方法。

但這就會造成在給接口添加新方法的同時影響了已有的實現,所以Java設計人員引入了接口默認方法,其目的是爲了解決接口的修改與已有的實現不兼容的問題,接口默認方法可以作爲庫、框架向前兼容的一種手段。

默認方法就像一個普通Java方法,只是方法用default關鍵字修飾。

下面來舉一個簡單的例子

複製代碼
public interface Person {
    //默認方法
    default String getName(String name) {
        return name;
    }
}
///////////////////////////////////////////////////////////////////////
public class Student implements Person {

}
//////////////////////////////////////////////////////////////////////
public class Test {
    public static void main(String[] args) {
        Person p = new Student();
        String name = p.getName("小李");
        System.out.println(name);
    }
}
複製代碼

我們定義了一個Person接口,其中getName是一個默認方法。接着編寫一個實現類,可以從結果中看到,雖然Student是空的,但是仍然可以實現getName方法。

顯然默認接口的出現打破了之前的一些基本規則,使用時要注意幾個問題。

考慮如果接口中定義了一個默認方法,而另外一個父類或者接口中又定義了一個同名的方法,該選擇哪個?

1. 選擇父類中的接口。如果一個父類提供了具體的實現方法,那麼接口中具有相同名稱和參數的默認方法會被忽略。

2. 接口衝突。如果一個父接口提供了一個默認方法,而另一個接口也提供了具有相同名稱和參數類型的方法(不管該方法是否是默認方法),那麼必須通過覆蓋方法來解決。

記住一個原則,就是“類優先”,即當類和接口都有一個同名方法時,只有父類中的方法會起作用。

“類優先”原則可以保證與Java 7的兼容性。如果你再接口中添加了一個默認方法,它對Java 8以前編寫的代碼不會產生任何影響。

下面來說說靜態方法

靜態方法就像一個普通Java靜態方法,但方法的權限修飾只能是public或者不寫。

默認方法和靜態方法使Java的功能更加豐富。

在Java 8中Collection接口中就添加了四個默認方法,stream()、parallelStream()、forEach()和removeIf()。Comparator接口也增加了許多默認方法和靜態方法。

函數式接口和Lambda表達式

函數式接口(Functional Interface)是隻包含一個方法的抽象接口。

比如Java標準庫中的java.lang.Runnable,java.util.concurrent.Callable就是典型的函數式接口。

在Java 8中通過@FunctionalInterface註解,將一個接口標註爲函數式接口,該接口只能包含一個抽象方法。

@FunctionalInterface註解不是必須的,只要接口只包含一個抽象方法,虛擬機會自動判斷該接口爲函數式接口。

一般建議在接口上使用@FunctionalInterface註解進行聲明,以免他人錯誤地往接口中添加新方法,如果在你的接口中定義了第二個抽象方法的話,編譯器會報錯。

函數式接口是爲Java 8中的lambda而設計的,lambda表達式的方法體其實就是函數接口的實現。

爲什麼要使用lambda表達式?

“lambda表達式”是一段可以傳遞的代碼,因爲他可以被執行一次或多次。我們先回顧一下之前在Java中一直使用的相似的代碼塊。

當我們在一個線程中執行一些邏輯時,通常會將代碼放在一個實現Runnable接口的類的run方法中,如下所示:

複製代碼
new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++)
                    System.out.println("Without Lambda Expression");
            }}).start();
複製代碼

然後通過創建實例來啓動一個新的線程。run方法內包含了一個新線程中需要執行的代碼。

再來看另一個例子,如果想利用字符串長度排序而不是默認的字典順序排序,就需要自定義一個實現Comparator接口的類,然後將對象傳遞給sort方法。

複製代碼
class LengthComparator implements Comparator<String> {
    @Override
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
}
Arrays.sort(strings, new LengthComparator());
複製代碼

按鈕回調是另一個例子。將回調操作放在了一個實現了監聽器接口的類的一個方法中。

複製代碼
JButton button = new JButton("click");

button.addActionListener(new ActionListener() {    
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Without Lambda Expression");
    }
});
複製代碼

這三個例子中,出現了相同方式,一段代碼被傳遞給其他調用者——一個新線程、是一個排序方法或者是一個按鈕。這段代碼會在稍後被調用。

在Java中傳遞代碼並不是很容易,不可能將代碼塊到處傳遞。你不得不構建一個類的對象,由他的某個方法來包含所需的代碼。

而lambda表達式實際上就是代碼塊的傳遞的實現。其語法結構如下:

(parameters) -> expression 或者 (parameters) -> {statements;}

括號裏的參數可以省略其類型,編譯器會根據上下文來推導參數的類型,你也可以顯式地指定參數類型,如果沒有參數,括號內可以爲空。

方法體,如果有多行功能語句用大括號括起來,如果只有一行功能語句則可以省略大括號。

new Thread(() -> {
            for (int i = 0; i < 100; i++)
                System.out.println("Lambda Expression");
        }).start();
Comparator<String> c = (s1, s2) -> Integer.compare(s1.length(), s2.length());
button.addActionListener(e -> System.out.println("Lambda Expression"));

可以看到lambda表達式使代碼變得簡單,代替了匿名內部類。

下面來說一下方法引用,方法引用是lambda表達式的一種簡寫形式。 如果lambda表達式只是調用一個特定的已經存在的方法,則可以使用方法引用。

使用“::”操作符將方法名和對象或類的名字分隔開來。以下是四種使用情況:

  • 對象::實例方法
  • 類::靜態方法
  • 類::實例方法
  • 類::new
Arrays.sort(strings, String::compareToIgnoreCase);
// 等價於
Arrays.sort(strings, (s1, s2) -> s1.compareToIgnoreCase(s2));

上面的代碼就是第三種情況,對lambda表達式又一次進行了簡化。

Stream API

當處理集合時,通常會迭代所有元素並對其中的每一個進行處理。例如,我們希望統計一個字符串類型數組中,所有長度大於3的元素。

複製代碼
String[] strArr = { "Java8", "new", "feature", "Stream", "API" };
        int count = 0;
        for (String s : strArr) {
            if (s.length() > 3)
                count++;
        }
複製代碼

通常我們都會使用這段代碼來統計,並沒有什麼錯誤,只是它很難被並行計算。這也是Java8引入大量操作符的原因,在Java8中,實現相同功能的操作符如下所示:

long count = Stream.of(strArr).filter(w -> w.length() > 3).count();

stream方法會爲字符串列表生成一個Streamfilter方法會返回只包含字符串長度大於3的一個Stream,然後通過count方法計數。

一個Stream表面上與一個集合很類似,允許你改變和獲取數據,但實際上卻有很大區別:

  1. Stream自己不會存儲元素。元素可能被存儲在底層的集合中,或者根據需要產生出來。
  2. Stream操作符不會改變源對象。相反,他們返回一個持有新結果的Stream。
  3. Stream操作符可能是延遲執行的。意思是它們會等到需要結果的時候才執行。

Stream相對於循環操作有更好的可讀性。並且可以並行計算:

long count = Arrays.asList(strArr).parallelStream().filter(w -> w.length() > 3).count();

只需要把stream方法改成parallelStream,就可以讓Stream去並行執行過濾和統計操作。

Stream遵循“做什麼,而不是怎麼去做”的原則。只需要描述需要做什麼,而不用考慮程序是怎樣實現的。

Stream很像Iterator,單向,只能遍歷一遍。但是Stream可以只通過一行代碼就實現多線程的並行計算。

當使用Stream時,會有三個階段:

  1. 創建一個Stream。
  2. 在一個或多個步驟中,將初始Stream轉化到另一個Stream的中間操作
  3. 使用一個終止操作來產生一個結果。該操作會強制他之前的延遲操作立即執行。在這之後,該Stream就不會在被使用了。

從着三個階段來看,對應着三種類型的方法,首先是Stream的創建方法。

複製代碼
// 1. Individual values
Stream stream = Stream.of("a", "b", "c");
// 2. Arrays
String [] strArray = new String[] {"a", "b", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. Collections
List<String> list = Arrays.asList(strArray);
stream = list.stream();
複製代碼

中間操作包括:map (mapToInt, flatMap 等)、 filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered。

終止操作包括:forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator。

關於Stream的每個方法如何使用就不展開了,更詳盡的介紹看這篇文章:https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/

新的日期和時間 API

Java8 引入了一個新的日期和時間API,位於java.time包下。

新的日期和時間API借鑑了Joda Time庫,其作者也爲同一人,但它們並不是完全一樣的,做了很多改進。

下面來說一下幾個常用的類。首先是Instant,一個Instant對象表示時間軸上的一個點。

Instant.now()會返回當前的瞬時點(格林威治時間)。Instant.MIN和Instant.MAX分別爲十億年前和十億年後。

如下代碼可以計算某算法的運行時間:

複製代碼
Instant start = Instant.now();
runAlgorithm();
Instant end = Instant.now();
Duration timeElapsed = Duration.between(start, end);
long millis = timeElapsed.toMillis();
複製代碼

Duration對象表示兩個瞬時點間的時間量。可以通過不同的方法,換算成各種時間單位。

上面所說的絕對時間並不能應用到生活中去,所以新的Java API中提供了兩種人類時間,本地日期/時間帶時區的時間

LocalDate是一個帶有年份、月份和天數的日期。創建他可以使用靜態方法now或者of。

複製代碼
LocalDate today = LocalDate.now();
LocalDate myBirthday = LocalDate.of(1994, 03, 15);
// use Enum
myBirthday = LocalDate.of(1994, Month.MARCH, 15);

System.out.println(today); // 2017-10-23
System.out.println(myBirthday); // 1994-03-15
複製代碼

下面是LocalDate中的一些常用方法:

LocalTime表示一天中的某個時間,同樣可以使用now或者of來創建實例。

LocalTime rightNow = LocalTime.now();
LocalTime bedTime = LocalTime.of(2, 0);
System.out.println(rightNow); // 01:26:17.139
System.out.println(bedTime); // 02:00

LocalDateTime表示一個日期和時間,用法和上面類似。

上面幾種日期時間類都屬於本地時間,下面來說一下帶時區的時間。

ZonedDateTime通過設置時區的id來創建一個帶時區的時間。

ZonedDateTime beijingOlympicOpenning = ZonedDateTime.of(2008, 8, 8, 20, 0, 0, 0, ZoneId.of("Asia/Shanghai"));
System.out.println(beijingOlympicOpenning); // 2008-08-08T20:00+08:00[Asia/Shanghai]

更新後的API同樣加入了新的格式化類DateTimeFormatter。DateTimeFormatter提供了三種格式化方法來打印日期/時間:

  • 預定義的標準格式
String formattered = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(beijingOlympicOpenning);
System.out.println(formattered); // 2008-08-08T20:00:00

DateTimeFormatter類提供了多種預定義的標準格式可供使用。

  • 語言環境相關的格式

標準格式主要用於機器可讀的時間戳。爲了讓人能夠讀懂日期和時間,你需要使用語言環境相關的格式。

Java8提供了4種風格,SHORT、MEDIUM、LONG、FULL。

複製代碼
String formattered = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).format(beijingOlympicOpenning);
System.out.println(formattered);    //2008年8月8日 星期五 下午08時00分00秒 CST
複製代碼
  • 自定義的格式

或者你也可以自定義日期和時間的格式。

String formattered = DateTimeFormatter.ofPattern("E yyyy-MM-dd HH:mm").format(beijingOlympicOpenning);
System.out.println(formattered); // 星期五 2008-08-08 20:00

  下圖中爲一些常用的模式元素。

新的API提供了從字符串解析出日期/時間的parse靜態方法和與遺留類(java.util.Date、java.sql.Time和java.txt.DateFormat等)互相轉換的方法。

雜項改進

 Java8在String類中只添加了一個新方法,就是join,該方法實現了字符串的拼接,可以把它看作split方法的逆操作。

String joined = String.join(".", "www", "cnblogs", "com");
System.out.println(joined); // www.cnblogs.com

數字包裝類提供了BYTES靜態方法,以byte爲單位返回長度。

所有八種包裝類都提供了靜態的hashCode方法。

Short、Integer、Long、Float和Double這5種類型分別提供了了sum、max和min,用來在流操作中作爲聚合函數使用。

集合類和接口中添加的方法:

Java8爲使用流讀取文件行及訪問目錄項提供了一些簡便的方法(Files.linesFiles.list)。同時也提供了進行Base64編碼/解碼的方法。

Java8對GUI編程(JavaFX)、併發等方面也做了改進,本文沒有一一列出。


轉載請註明原文鏈接:http://www.cnblogs.com/justcooooode/p/7701260.html


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章