Java8新特性
前言:
北京時間2018年9月26日,Oracle官方釋出Java 11。既然版本都更新到11了,現在才來學8是不是太晚了?其實不是的,目前應該大部分都還是使用的Java 7和Java 8,這兩個應該還是主流。而Java 8 又有一些激動人心的新特性,所以還是值得學習的。Java 8 新特性主要有以下幾點:
有了以上新特性,Java 8就可以做到:
- 速度更快;
- 程式碼更少(增加了新的語法 Lambda 表示式);
- 方便操作集合(Stream API)
- 便於並行;
- 最大化減少空指標異常 Optional。
接下來一起來了解一下Java 8的這些新特性。
一、Lambada表示式:
1、什麼是lambda?
Lambda 是一個匿名函式,我們可以把 Lambda 表示式理解為是一段可以傳遞的程式碼(將程式碼像資料一樣進行傳遞)。可以寫出更簡潔、更靈活的程式碼。
2、瞭解新操作符:
Java 8引入了新的操作符,->,叫箭頭操作符或者叫lambda操作符。當使用lambda表示式時就需要使用這個操作符。
3、lambda表示式語法:
箭頭操作符將lambda表示式分成了兩部分:
- 左側:lambda表示式的引數列表(介面中抽象方法的引數列表)
- 右側:lambda表示式中所需執行的功能(lambda體,對抽象方法的實現)
語法有如下幾種格式:
- 語法格式一(無引數無返回值): () -> 具體實現
- 語法格式二(有一個引數無返回值): (x) -> 具體實現 或 x -> 具體實現
- 語法格式三(有多個引數,有返回值,並且lambda體中有多條語句):(x,y) -> {具體實現}
- 語法格式四:若方法體只有一條語句,那麼大括號和return都可以省略
注: lambda表示式的引數列表的引數型別可以省略不寫,可以進行型別推斷。
看幾個例子:
例一:
@Test public void test1(){ // 實現一個執行緒 int num = 0;//jdk1.8以前,這個必須定義為final,下面才能用,1.8後預設就為final Runnable runnable = new Runnable() { @Override public void run() { System.out.println("hello world"+ num); } }; runnable.run(); }
建立一個執行緒,重寫run方法,在run方法裡面列印一句話。我們想要的就是 System.out.println("hello world"+ num);
這行程式碼,但是為了實現這行程式碼,不得不多寫了好多行。lambda就可以解決這一點,看看用lambda如何實現:
Runnable runnable1 = () -> System.out.println("hello world"+num); runnable1.run();
用lambda這樣就搞定了。首先還是 Runnable runnable1 =
,但是不用new了,右邊就用lambda實現。我們要使用的是該介面的run方法,run方法不需要引數,所以lambda表示式左邊就是(),lambda表示式右邊是抽象方法的實現,也就是第一種方式中run方法的方法體寫到lambda表示式右邊就可以了。
例二:
Comparator<Integer> comparator = new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return Integer.compare(o1,o2);//就這一行關鍵程式碼 } };
以前寫一個比較器就要像上面那樣寫,先new比較器類,然後在其compare方法裡寫核心程式碼。用lambda實現:
Comparator<Integer> comparator = (x,y) -> Integer.compare(x,y);
compare方法需要兩個引數,所以箭頭操作符左邊寫(x,y),右邊是compare方法的實現,所以應該寫 return Integer.compare(o1,o2);
,但是根據上面的語法格式四可知,return可以省略,因此就寫成了上面那樣。
通過這兩個例子可以感受到lambda表示式的簡潔,但是問題來了:我們說lambda表示式就是一個匿名函式,我們只需要指定引數和lambda體即可,那麼它是如何判斷重寫的是哪個方法呢?比如一個介面中有多個方法,如果使用lambda表示式來寫,那麼如何判斷我們使用的是該介面的哪個方法?其實是不能判斷的!通過上面兩個例子可以發現,Runnable介面和Comparator介面都是隻有一個方法的介面,所以可以使用lambda。
二、函式式介面:
1、什麼是函式式介面?
像Runnable和Comparator這樣只有一個方法的介面,稱為函式式介面。也可以在介面上加上 @FunctionalInterface
註解,如果編譯通過,則該介面就是函式式介面。lambda表示式就需要函式式介面的支援。
2、看一個需求:
需求:需要對兩個數進行加減乘除等運算,怎麼實現?
- 傳統做法:傳統做法中,需要進行幾種運算,我們就要寫幾個方法。一種運算對應一個方法。
- lambda做法:首先要定義一個函式式介面,介面中只有一個方法,接收兩個引數。
@FunctionalInterface public interface MyInterface { public Integer getValue(Integer num1,Integer num2); }
然後就可以使用了:
@Test public void test5(){ MyInterface myInterface = (x,y) -> x*y;//乘法運算 MyInterface myInterface1 = (x,y) -> x+y;//加法運算 Integer result1 = myInterface.getValue(100,200); Integer result2 = myInterface1.getValue(1024,2048); System.out.println(result1); System.out.println(result2); }
所以用lambda的話,只需要定義一個函式式介面,不管進行什麼操作,都可以用lambda解決,不用再一種運算對應一個方法。但是,還需要自己定義函式式介面,好像也沒簡單很多。Java考慮到這點了,所以內建了函式式介面。
3、四大內建函式式介面:
為了不需要我們自己定義函式式介面,Java內建了四大函式式介面,這四大介面加上它們的子類,完全滿足我們的使用了。四大函式式介面是:
- Consumer<T>:消費型介面(void accept(T t)),接收一個引數,無返回值。
- Supplier<T>:供給型介面(T get()),無引數,有返回值。
- Function<T,R>:函式型介面(R apply(T t)),接收一個引數,有返回值。
- Predicate<T>:斷言型介面(boolean test(T t)),接收一個引數,返回Boolean值。
4、四大函式式介面的使用:
接下來看看具體如何使用這四大函式式介面。
消費型介面的使用:
Consumer consumer = (x) -> System.out.println("消費了"+x+"元"); consumer.accept(100);
供給型介面的使用:
Supplier<Integer> supplier = () -> (int)(Math.random() * 100);//生成隨機數 System.out.println(supplier.get());
函式型介面的使用:
Function<String,String> function = str -> str.toUpperCase();//將傳入的字串轉成大寫 String s = function.apply("adcdefggffs"); System.out.println(s);
斷言型介面的使用:
//需求:將滿足條件的字串新增到集合中去 public List<String> filterString(List<String> strings, Predicate<String> predicate){ List<String> stringList = new ArrayList<>(); for (String string : strings) { if (predicate.test(string)){ stringList.add(string); } } return stringList; } //測試 @Test public void test4(){ List<String> list = Arrays.asList("hello","world","niu","bi"); List<String> newList = filterString(list,str -> str.length() > 3);//選出長度大於3的字串 newList.forEach(System.out::println); }
三、方法引用與構造器引用:
當要傳遞給Lambda體的操作,已經有實現的方法了,可以使用方法引用。不過實現抽象方法的引數列表,必須與引用方法的引數列表保持一致。
1、方法引用語法:
- 物件::例項方法
- 類::靜態方法
- 類::例項方法
2、方法引用具體用法:
說了那麼多可能還不清楚到底什麼意思,一起來看幾個例子。
語法一例子:
Consumer<String> consumer = x -> System.out.println(x);//傳統寫法 Consumer<String> consumer = System.out::println;//使用方法引用
println方法和Consumer的accept方法都是無返回值,接收一個引數,所以可以這樣寫。
語法二例子:
Comparator<Integer> comparator = (x,y) -> Integer.compare(x,y); //因為compare方法已經被Integer實現了,且是靜態的,所以這樣用就行。 Comparator<Integer> comparator1 = Integer::compare;
語法三例子:
BiPredicate<String,String> biPredicate = (x,y) -> x.equals(y); //可以改成如下寫法 //不過要滿足:第一個引數是例項方法的呼叫者,第二個引數是例項方法的引數時,就可以這樣用 BiPredicate<String,String> biPredicate1 = String::equals;
3、構造器引用:
Supplier<Employee> supplier = () -> new Employee(); //可以改寫成這樣 //注意:需要呼叫的構造器的引數列表要與函式介面中抽象方法的引數列表一致 Supplier<Employee> supplier1 = Employee::new; Employee employee = supplier.get();
四、Stream API:
Stream 是 Java8 中處理集合的關鍵抽象概念,它可以指定你希望對集合進行的操作,可以執行非常複雜的查詢、過濾和對映資料等操作。使用Stream API 對集合資料進行操作,就類似於使用 SQL 執行的資料庫查詢。也可以使用 Stream API 來並行執行操作。簡而言之,Stream API 提供了一種高效且易於使用的處理資料的方式。
1、理解Stream:
Stream被稱作流,是用來處理集合以及陣列的資料的。它有如下特點:
- Stream 自己不會儲存元素。
- Stream 不會改變源物件。相反,他們會返回一個持有結果的新Stream。
- Stream 操作是延遲執行的。這意味著他們會等到需要結果的時候才執行。
2、使用Stream的三個步驟:
- 建立Stream:一個數據源(如:集合、陣列),獲取一個流
- 中間操作:一箇中間操作鏈,對資料來源的資料進行處理
- 終止操作:一個終止操作,執行中間操作鏈,併產生結果
3、建立Stream:
直接看程式碼:
//1、通過集合提供的stream方法或parallelStream()方法建立 List<String> list = new ArrayList<>(); Stream<String> stringStream = list.stream(); //2、通過Arrays中的靜態方法stream獲取陣列流 Employee[] employees = new Employee[10]; Stream<Employee> stream = Arrays.stream(employees); //3、通過Stream類的靜態方法of()建立流 Stream<String> stream1 = Stream.of("aa","bb","cc"); //4、建立無限流 //迭代方式建立無限流 //從0開始,每次加2,生成無限個 Stream<Integer> stream2 = Stream.iterate(0,(x) -> x+2); //生成10個 stream2.limit(10).forEach(System.out::println); //生成方式建立無限流 Stream.generate(() -> Math.random()) .limit(5) .forEach(System.out::println);
上面介紹了集合、陣列建立流的幾種方式,都有對應的註解。
4、中間操作:
篩選與切片:
- filter -- 接收lambda,從流中排除某些資料。
- limit -- 截斷流,使其元素不超過給定數量。
- skip(n) -- 跳過元素,返回一個扔掉了前n個元素的流,若不足n個元素,則返回空流。
- distinct -- 篩選,通過流所生成元素的hashCode()和equals()去除重複元素,所以物件必須重新hashCode方法和equals方法。
看程式碼:
employees.stream()//已有employees集合 .filter((e) -> e.getAge() > 18)//中間操作(選出年齡大於18的) .limit(1)//中間操作(只返回一個) .forEach(System.out::println);//終止操作
對映:
- map -- 接收lambda,將元素轉換成其他形式或提取資訊。接收一個函式作為引數,該函式會被應用到每個元素上,並將其對映成一個新的元素。
- flatMap -- 接收一個函式作為引數,將流中的每個值都換成另一個流,然後把所以流連線成一個流。
看例子:
List<String> list = Arrays.asList("aa","bb","cc","dd"); list.stream() .map(str -> str.toUpperCase())//將所有的轉成大寫 .forEach(System.out::println);
排序:
- sorted() -- 自然排序(按照Comparable來排序)。
- sorted(Comparator com) -- 定製排序(按照Comparator來排序)。
看例子:
List<String> list = Arrays.asList("ccc","bbb","aaa","ddd"); list.stream() .sorted()//自然排序 .forEach(System.out::print);//aaa,bbb,ccc,ddd //定製排序 employees.stream()//employees是一個存有多名員工的集合 .sorted((e1, e2) -> { if (e1.getAge().equals(e2.getAge())){ //如果年齡一樣 return e1.getName().compareTo(e2.getName());//就比較姓名 }else { return e1.getAge().compareTo(e2.getAge());//年齡不一樣就比較年齡 } }).forEach(System.out::println);
5、終止操作:
查詢與匹配:
- allMatch -- 檢查是否匹配所有元素。
- anyMatch -- 檢查是否至少匹配一個元素。
- noneMatch -- 檢查是否沒有匹配所有元素。
- findFirst -- 返回第一個元素。
- findAny -- 返回當前流中任意元素。
- count -- 返回流中元素總個數。
- max -- 返回流中最大值。
- min -- 返回流中最小值。
//看看employee集合中是不是所有都是男的 boolean b = employees.stream() .allMatch(e -> e.getGender().equals("男")); System.out.println(b);
規約:
//規約求和 List<Integer> list = Arrays.asList(1,3,5,4,4,3); Integer sum = list.stream() .reduce(0,(x,y) -> x+y);//首先把0作為x,把1作為y,進行加法運算得到1,把1再作為x,把3作為y,以此類推 System.out.println(sum); //獲取工資總和 Optional<Double> optional = employees.stream() .map(Employee::getSalary)//提取工資 .reduce(Double::sum);//求工資總和 System.out.println(optional2.get());
收集:
- collect -- 將流轉換為其他形式。接收一個Collector介面的實現,用於給Stream中元素做彙總的方法。
//把公司中所有員工的姓名提取出來並收集到一個集合中去 List<String> stringList = employees.stream() .map(Employee::getName)//提取員工姓名 //.collect(Collectors.toList());//收集到list集合 //.collect(Collectors.toSet());//收集到set集合 .collect(Collectors.toCollection(LinkedList::new));//這種方式可收集到任意集合 stringList.forEach(System.out::println);//遍歷集合 //計算工資平均值 Double avgSalary = employees.stream() .collect(Collectors.averagingDouble(Employee::getSalary)); System.out.println(avgSalary); //根據年齡分組 Map<Integer,List<Employee>> map = employees.stream() .collect(Collectors.groupingBy(Employee::getAge)); System.out.println(map); //先按性別分組,性別一樣時按年齡分組 Map<String,Map<Integer,List<Employee>>> map1 = employees.stream() .collect(Collectors.groupingBy(Employee::getGender,Collectors.groupingBy(Employee::getAge))); System.out.println(map1); //分割槽,滿足條件的一個區,不滿足的另一個區 Map<Boolean,List<Employee>> map2 = employees.stream() .collect(Collectors.partitioningBy(e -> e.getSalary() > 6000));//工資大於6000的為true區,否則為false區 System.out.println(map2); //獲取工資的總額、平均值等 DoubleSummaryStatistics dss = employees.stream() .collect(Collectors.summarizingDouble(Employee::getSalary)); System.out.println(dss.getSum()); System.out.println(dss.getAverage()); System.out.println(dss.getMax());
五、並行流與序列流:
1、fork/join框架:
此框架就是在必要的情況下,將一個大任務,進行拆分(fork)成若干個小任務(拆到不可再拆時),再將一個個的小任務運算的結果進行 join 彙總。

fork/join
2、並行流與序列流:
通過上面的圖可以知道,使用fork/join框架可以提高效率(運算量越大越明顯,運算量可能反而更慢,因為拆分也需要時間),但是在Java 8之前需要自己實現fork/join,還是挺麻煩的,Java 8就方便多了,因為提供了並行流,底層就是使用了fork/join。Stream API 可以宣告性地通過 parallel() 與 sequential() 在並行流與順序流之間進行切換。
@Test public void test(){ Instant start = Instant.now(); //普通做法求0加到10000000000的和 LongStream.rangeClosed(0,100000000000L) .reduce(0,Long::sum); Instant end = Instant.now(); System.out.println("耗費"+ Duration.between(end ,start) + "秒");//55秒 } @Test public void test2(){ Instant start = Instant.now(); //並行流求0加到10000000000的和 LongStream.rangeClosed(0,100000000000L) .parallel()//使用並行流 .reduce(0,Long::sum); Instant end = Instant.now(); System.out.println("耗費"+ Duration.between(end ,start) + "秒");//30秒 }
通過執行上面的程式可以明顯感受到並行流的高效。
六、新時間日期API:
Java 8之前的Date和Calendar都是執行緒不安全的,而且使用起來比較麻煩,Java 8提供了全新的時間日期API,LocalDate(日期)、LocalTime(時間)、LocalDateTime(時間和日期) 、Instant (時間戳)、Duration(用於計算兩個“時間”間隔)、Period(用於計算兩個“日期”間隔)等。
1、LocalDate、LocalTime、LocalDateTime:
這三個用法一樣。
//獲取當前系統時間 LocalDateTime localDateTime = LocalDateTime.now();//當前時間日期 LocalDateTime localDateTime2 = localDateTime.plusYears(2);//加兩年 System.out.println(localDateTime.getMonth()); System.out.println(localDateTime); System.out.println(localDateTime2); //指定時間 LocalDateTime localDateTime1 = LocalDateTime.of(2018,12,13,21,8); System.out.println(localDateTime1);
2、Instant 時間戳:
時間戳就是計算機讀的時間,它是以Unix元年(傳統 的設定為UTC時區1970年1月1日午夜時分)開始算起的。
//計算機讀的時間:時間戳(Instant),1970年1月1日0時0分0秒到此時的毫秒值 Instant instant = Instant.now(); System.out.println(instant);//預設是美國時區,8個時差 OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));//加上時差 System.out.println(offsetDateTime); System.out.println(instant.toEpochMilli());//顯示毫秒值
3、Duration 和 Period:
LocalTime localTime = LocalTime.now(); try { Thread.sleep(1000); }catch (Exception e){ e.printStackTrace(); } LocalTime localTime1 = LocalTime.now(); System.out.println(Duration.between(localTime,localTime1).toMillis()); //獲取兩個日期之間的間隔 LocalDate localDate = LocalDate.of(2012,1,1); LocalDate localDate1 = LocalDate.now(); Period period = Period.between(localDate,localDate1); System.out.println(period); System.out.println(period.getYears()+"年"+period.getMonths()+"月"+period.getDays()+"日");
4、時間校正器(TemporalAdjuster):
LocalDateTime localDateTime = LocalDateTime.now(); System.out.println(localDateTime); LocalDateTime localDateTime1 = localDateTime.withDayOfMonth(1);//localDate日期中月份的1號 System.out.println(localDateTime1); localDateTime1.with(TemporalAdjusters.firstDayOfNextMonth());//下一個月的第一天 localDateTime.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));//下週日
5、格式化日期(.DateTimeFormatter ):
@Test public void test6(){ //DateTimeFormatter:格式化 //使用預設格式 DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME; LocalDateTime localDateTime = LocalDateTime.now(); String str = localDateTime.format(dateTimeFormatter); System.out.println(str); System.out.println("=========================="); //自定義格式 DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss"); String str2 = localDateTime.format(dateTimeFormatter1); //這樣格式化也可以 String str3 = dateTimeFormatter1.format(localDateTime); System.out.println(str2); System.out.println(str3); //退回到解析前的格式 LocalDateTime newDate = localDateTime.parse(str,dateTimeFormatter); System.out.println(newDate); }
6、時區的處理:
Java8 中加入了對時區的支援,帶時區的時間為分別為:ZonedDate、ZonedTime、ZonedDateTime。
@Test public void test7(){ //ZonedDate ZonedTime ZonedDateTime LocalDateTime dateTime = LocalDateTime.now(ZoneId.of("Europe/Tallinn")); System.out.println(dateTime); }
七、介面中的預設方法和靜態方法:
public interface MyInterface { default String test(){ return "允許存在有具體實現的方法"; } public static String test2(){ return "介面中還可以有靜態方法"; } }
如上所示,Java 8的介面中允許有預設方法和靜態方法。如果一個類繼承了一個類還實現了一個介面,而且介面中的預設方法和父類中的方法同名,這時採用類優先原則。也就是說,子類使用的是父類的方法,而不是介面中的同名方法。
八、其他新特性:
1、Optional類:
這個類是為了儘可能減少空指標異常的。就是把普通物件用Optional包起來,做了一些封裝。看看其用法:
@Data public class Man { //男人類 private Godness godness;//女神 }
@Data public class Godness { private String name; public Godness(String name){ this.name = name; } public Godness(){ } }
//獲取男人心中的女神的名字(有的人不一定有女神,也就是說女神可能為空) //常規做法要加很多判斷 public String getGodnessName(Man man){ if (man != null){ Godness godness = man.getGodness(); if (godness != null){ return godness.getName(); }else{ return "我心中沒有女神"; } }else { return "男人為空"; } }
一個man類,有一個成員變數女神,女神也是一個類,有一個成員變數,名字。要獲取man心中的女神,為了防止控制針異常,要做很多的判斷。如果使用Optional呢?做法如下:
//新男人類 @Data public class NewMan { private Optional<Godness> godness = Optional.empty(); }
//使用optional後的方法 public String getGodnessName2(Optional<NewMan> man){ return man.orElse(new NewMan()) .getGodness() .orElse(new Godness("我沒有女神")) .getName(); }
這樣就簡單多了。
2、重複註解與型別註解:
Java 8 可以使用重複註解和型別註解,如下圖:

重複註解&型別註解
總結:
本文說了一些Java 8 的新特性,重點就是lambda表示式和Stream API,可以簡化很多操作。肯可能還有些文中未涉及的,在此拋磚引玉,望各位大佬指點!