一、引言
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();
由於流這一塊博主目前使用的不是很多,所以有些新的用法也不是很熟悉,這一塊大概就寫到這里,如果有比較懂得還請多多指教。
三、總結
總的來說,jdk1.8的一些新特性主要還是簡化了代碼的寫法,減少了部分開發量,但是需要一些時間來熟悉。挺慚愧的,現在1.9都已經出來了,1.8的新特性還不是很熟悉,所以還是要繼續努力,多看些開源的東西。