1. 程式人生 > >JDK1.8升級之後的優勢在哪裡。

JDK1.8升級之後的優勢在哪裡。

一、引言

  jdk1.8出來已經一段時間了,現在1.9也已經出來了,但是很多公司(我們公司也一樣)不太願意升級到高版本的jdk,主要是有老的專案要維護,還有升級的話配套的框架也要升級,要考慮的細節事情太多。

  前段時間去面試,問到了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();