JDK1.8新特性(詳解)

一、新特性

  1、default關鍵字

  在java裏面,我們通常都是認爲接口裏面是只能有抽象方法,不能有任何方法的實現的,那麼在jdk1.8裏面打破了這個規定,引入了新的關鍵字default,通過使用default修飾方法,可以讓我們在接口裏面定義具體的方法實現,如下。

 

public interface NewCharacter {
    
    public void test1();
    
    public default void test2(){
        System.out.println("我是新特性1");
    }

}

 

  那這麼定義一個方法的作用是什麼呢?爲什麼不在接口的實現類裏面再去實現方法呢?

  其實這麼定義一個方法的主要意義是定義一個默認方法,也就是說這個接口的實現類實現了這個接口之後,不用管這個default修飾的方法,也可以直接調用,如下。

public class NewCharacterImpl implements NewCharacter{

    @Override
    public void test1() {
        
    }
    
    public static void main(String[] args) {
        NewCharacter nca = new NewCharacterImpl();
        nca.test2();
    }

}

 

  所以說這個default方法是所有的實現類都不需要去實現的就可以直接調用,那麼比如說jdk的集合List裏面增加了一個sort方法,那麼如果定義爲一個抽象方法,其所有的實現類如arrayList,LinkedList等都需要對其添加實現,那麼現在用default定義一個默認的方法之後,其實現類可以直接使用這個方法了,這樣不管是開發還是維護項目,都會大大簡化代碼量。

  2、Lambda 表達式

  Lambda表達式是jdk1.8裏面的一個重要的更新,這意味着java也開始承認了函數式編程,並且嘗試引入其中。

  首先,什麼是函數式編程,引用廖雪峯先生的教程裏面的解釋就是說:函數式編程就是一種抽象程度很高的編程範式,純粹的函數式編程語言編寫的函數沒有變量,因此,任意一個函數,只要輸入是確定的,輸出就是確定的,這種純函數我們稱之爲沒有副作用。而允許使用變量的程序設計語言,由於函數內部的變量狀態不確定,同樣的輸入,可能得到不同的輸出,因此,這種函數是有副作用的。函數式編程的一個特點就是,允許把函數本身作爲參數傳入另一個函數,還允許返回一個函數!

  簡單的來說就是,函數也是一等公民了,在java裏面一等公民有變量,對象,那麼函數式編程語言裏面函數也可以跟變量,對象一樣使用了,也就是說函數既可以作爲參數,也可以作爲返回值了,看一下下面這個例子。

 

//這是常規的Collections的排序的寫法,需要對接口方法重寫
        public void test1(){
        List<String> list =Arrays.asList("aaa","fsa","ser","eere");
        Collections.sort(list, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o2.compareTo(o1);
            }
        });
        for (String string : list) {
            System.out.println(string);
        }
    }
//這是帶參數類型的Lambda的寫法
        public void testLamda1(){
        List<String> list =Arrays.asList("aaa","fsa","ser","eere");
        Collections.sort(list, (Comparator<? super String>) (String a,String b)->{
            return b.compareTo(a);
        }
        );
        for (String string : list) {
            System.out.println(string);
        }
    }
//這是不帶參數的lambda的寫法
        public void testLamda2(){
        List<String> list =Arrays.asList("aaa","fsa","ser","eere");
        Collections.sort(list, (a,b)->b.compareTo(a)
        );
        for (String string : list) {
            System.out.println(string);
        }
 

 

  可以看到不帶參數的寫法一句話就搞定了排序的問題,所以引入lambda表達式的一個最直觀的作用就是大大的簡化了代碼的開發,像其他一些編程語言Scala,Python等都是支持函數式的寫法的。當然,不是所有的接口都可以通過這種方法來調用,只有函數式接口才行,jdk1.8裏面定義了好多個函數式接口,我們也可以自己定義一個來調用,下面說一下什麼是函數式接口。

  3、函數式接口

  定義:“函數式接口”是指僅僅只包含一個抽象方法的接口,每一個該類型的lambda表達式都會被匹配到這個抽象方法。jdk1.8提供了一個@FunctionalInterface註解來定義函數式接口,如果我們定義的接口不符合函數式的規範便會報錯。

@FunctionalInterface
public interface MyLamda {
    
    public void test1(String y);

//這裏如果繼續加一個抽象方法便會報錯
//    public void test1();
    
//default方法可以任意定義
    default String test2(){
        return "123";
    }
    
    default String test3(){
        return "123";
    }

//static方法也可以定義
    static void test4(){
        System.out.println("234");
    }

}
 

  看一下這個接口的調用,符合lambda表達式的調用方法。

MyLamda m = y -> System.out.println("ss"+y);

 

 4.方法與構造函數引用

  jdk1.8提供了另外一種調用方式::,當 你 需 要使用 方 法 引用時 , 目 標引用 放 在 分隔符::前 ,方法 的 名 稱放在 後 面 ,即ClassName :: methodName 。例如 ,Apple::getWeight就是引用了Apple類中定義的方法getWeight。請記住,不需要括號,因爲你沒有實際調用這個方法。方法引用就是Lambda表達式(Apple a) -> a.getWeight()的快捷寫法,如下示例。

//先定義一個函數式接口
@FunctionalInterface
public interface TestConverT<T, F> {
    F convert(T t);
}

   測試如下,可以以::形式調用。

public void test(){
    TestConverT<String, Integer> t = Integer::valueOf;
    Integer i = t.convert("111");
    System.out.println(i);
}

 

   此外,對於構造方法也可以這麼調用。

 

//實體類User和它的構造方法
public class User {    
    private String name;
    
    private String sex;

    public User(String name, String sex) {
        super();
        this.name = name;
        this.sex = sex;
    }
}
//User工廠
public interface UserFactory {
    User get(String name, String sex);
}
//測試類
    UserFactory uf = User::new;
    User u = uf.get("ww", "man");

 

   這裏的User::new就是調用了User的構造方法,Java編譯器會自動根據UserFactory.get方法的簽名來選擇合適的構造函數。

  5、局部變量限制

  Lambda表達式也允許使用自由變量(不是參數,而是在外層作用域中定義的變量),就像匿名類一樣。 它們被稱作捕獲Lambda。 Lambda可以沒有限制地捕獲(也就是在其主體中引用)實例變量和靜態變量。但局部變量必須顯式聲明爲final,或事實上是final。
  爲什麼局部變量有這些限制?
  (1)實例變量和局部變量背後的實現有一個關鍵不同。實例變量都存儲在堆中,而局部變量則保存在棧上。如果Lambda可以直接訪問局部變量,而且Lambda是在一個線程中使用的,則使用Lambda的線程,可能會在分配該變量的線程將這個變量收回之後,去訪問該變量。因此, Java在訪問自由局部變量時,實際上是在訪問它的副本,而不是訪問原始變量。如果局部變量僅僅賦值一次那就沒有什麼區別了——因此就有了這個限制。
  (2)這一限制不鼓勵你使用改變外部變量的典型命令式編程模式。

final int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
stringConverter.convert(2); 

 

   6、Date Api更新  

  1.8之前JDK自帶的日期處理類非常不方便,我們處理的時候經常是使用的第三方工具包,比如commons-lang包等。不過1.8出現之後這個改觀了很多,比如日期時間的創建、比較、調整、格式化、時間間隔等。這些類都在java.time包下。比原來實用了很多。

  6.1 LocalDate/LocalTime/LocalDateTime

  LocalDate爲日期處理類、LocalTime爲時間處理類、LocalDateTime爲日期時間處理類,方法都類似,具體可以看API文檔或源碼,選取幾個代表性的方法做下介紹。

  now相關的方法可以獲取當前日期或時間,of方法可以創建對應的日期或時間,parse方法可以解析日期或時間,get方法可以獲取日期或時間信息,with方法可以設置日期或時間信息,plus或minus方法可以增減日期或時間信息;

  6.2TemporalAdjusters

   這個類在日期調整時非常有用,比如得到當月的第一天、最後一天,當年的第一天、最後一天,下一週或前一週的某天等。

   6.3DateTimeFormatter

   以前日期格式化一般用SimpleDateFormat類,但是不怎麼好用,現在1.8引入了DateTimeFormatter類,默認定義了很多常量格式(ISO打頭的),在使用的時候一般配合LocalDate/LocalTime/LocalDateTime使用,比如想把當前日期格式化成yyyy-MM-dd hh:mm:ss的形式:

LocalDateTime dt = LocalDateTime.now();  
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");         
System.out.println(dtf.format(dt));

 

   7、流

  定義:流是Java API的新成員,它允許我們以聲明性方式處理數據集合(通過查詢語句來表達,而不是臨時編寫一個實現)。就現在來說,我們可以把它們看成遍歷數據集的高級迭代器。此外,流還可以透明地並行處理,也就是說我們不用寫多線程代碼了。

  Stream 不是集合元素,它不是數據結構並不保存數據,它是有關算法和計算的,它更像一個高級版本的 Iterator。原始版本的 Iterator,用戶只能顯式地一個一個遍歷元素並對其執行某些操作;高級版本的 Stream,用戶只要給出需要對其包含的元素執行什麼操作,比如 “過濾掉長度大於 10 的字符串”、“獲取每個字符串的首字母”等,Stream 會隱式地在內部進行遍歷,做出相應的數據轉換。

  Stream 就如同一個迭代器(Iterator),單向,不可往復,數據只能遍歷一次,遍歷過一次後即用盡了,就好比流水從面前流過,一去不復返。而和迭代器又不同的是,Stream 可以並行化操作,迭代器只能命令式地、串行化操作。顧名思義,當使用串行方式去遍歷時,每個 item 讀完後再讀下一個 item。而使用並行去遍歷時,數據會被分成多個段,其中每一個都在不同的線程中處理,然後將結果一起輸出。Stream 的並行操作依賴於 Java7 中引入的 Fork/Join 框架(JSR166y)來拆分任務和加速處理過程。

  流的操作類型分爲兩種:

  • Intermediate:一個流可以後面跟隨零個或多個 intermediate 操作。其目的主要是打開流,做出某種程度的數據映射/過濾,然後返回一個新的流,交給下一個操作使用。這類操作都是惰性化的(lazy),就是說,僅僅調用到這類方法,並沒有真正開始流的遍歷。
  • Terminal:一個流只能有一個 terminal 操作,當這個操作執行後,流就被使用“光”了,無法再被操作。所以這必定是流的最後一個操作。Terminal 操作的執行,纔會真正開始流的遍歷,並且會生成一個結果,或者一個 side effect。

   在對於一個 Stream 進行多次轉換操作 (Intermediate 操作),每次都對 Stream 的每個元素進行轉換,而且是執行多次,這樣時間複雜度就是 N(轉換次數)個 for 循環裏把所有操作都做掉的總和嗎?其實不是這樣的,轉換操作都是 lazy 的,多個轉換操作只會在 Terminal 操作的時候融合起來,一次循環完成。我們可以這樣簡單的理解,Stream 裏有個操作函數的集合,每次轉換操作就是把轉換函數放入這個集合中,在 Terminal 操作的時候循環 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();
 

對文章感興趣的朋友,可以關注公衆號,獲取更多學習資料,還有學習視頻

發佈了71 篇原創文章 · 獲贊 29 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章