Java8新特性都到碗裏來

對於Java開發者來講,Java8的版本顯然是一個具備里程碑意義的版本,蘊含了許多使人激動的新特性,若是能利用好這些新特性,可以大大提高咱們的開發效率。Java8的函數式編程可以大大減小代碼量和便於維護,同時,還有一些跟併發相關的功能。開發中經常使用到的新特性以下:html

  1. 接口的默認方法和靜態方法
  2. 函數式接口FunctionInterface與lambda表達式
  3. 方法引用
  4. Stream
  5. Optional
  6. Date/time API的改進
  7. 其餘改進

1. 接口的默認方法和靜態方法

在Java8以前,接口中只能包含抽象方法。那麼這有什麼樣弊端呢?好比,想再Collection接口中添加一個spliterator抽象方法,那麼也就意味着以前全部實現Collection接口的實現類,都要從新實現spliterator這個方法才行。而接口的默認方法就是爲了解決接口的修改與接口實現類不兼容的問題,做爲代碼向前兼容的一個方法java

那麼如何在接口中定義一個默認方法呢?來看下JDK中Collection中如何定義spliterator方法的:sql

default Spliterator<E> spliterator() {
    return Spliterators.spliterator(this, 0);
}
複製代碼

能夠看到定義接口的默認方法是經過default關鍵字。所以,在Java8中接口可以包含抽象方法外還可以包含若干個默認方法(即有完整邏輯的實例方法)。數據庫

public interface IAnimal {
    default void breath(){
        System.out.println("breath!");
    };
}


public class DefaultMethodTest implements IAnimal {
    public static void main(String[] args) {
        DefaultMethodTest defaultMethod = new DefaultMethodTest();
        defaultMethod.breath();
    }

}


輸出結果爲:breath!
複製代碼

能夠看出IAnimal接口中有由default定義的默認方法後,那麼其實現類DefaultMethodTest也一樣可以擁有實例方法breath。可是若是一個類繼承多個接口,多個接口中有相同的方法就會產生衝突該如何解決?實際上默認方法的改進,使得java類可以擁有相似多繼承的能力,即一個對象實例,將擁有多個接口的實例方法,天然而然也會存在方法重複衝突的問題。express

下面來看一個例子:編程

public interface IDonkey{
    default void run() {
        System.out.println("IDonkey run");
    }
}

public interface IHorse {

    default void run(){
        System.out.println("Horse run");
    }

}

public class DefaultMethodTest implements IDonkey,IHorse {
    public static void main(String[] args) {
        DefaultMethodTest defaultMethod = new DefaultMethodTest();
        defaultMethod.breath();
    }

}
複製代碼

定義兩個接口:IDonkey和IHorse,這兩個接口中都有相同的run方法。DefaultMethodTest實現了這兩個接口,因爲這兩個接口有相同的方法,所以就會產生衝突,不知道以哪一個接口中的run方法爲準,編譯會出錯:inherits unrelated defaults for run.....api

解決方法數組

針對由默認方法引發的方法衝突問題,只有經過重寫衝突方法,並方法綁定的方式,指定以哪一個接口中的默認方法爲準安全

public class DefaultMethodTest implements IAnimal,IDonkey,IHorse {
    public static void main(String[] args) {
        DefaultMethodTest defaultMethod = new DefaultMethodTest();
        defaultMethod.run();
    }

    @Override
    public void run() {
        IHorse.super.run();
    }
}
複製代碼

DefaultMethodTest重寫了run方法,並經過 IHorse.super.run();指定以IHorse中的run方法爲準。併發

靜態方法

在Java8中還有一個特性就是,接口中還能夠聲明靜態方法,以下例:

public interface IAnimal {
    default void breath(){
        System.out.println("breath!");
    }
    static void run(){}
}
複製代碼

2.函數式接口FunctionInterface與lambda表達式

函數式接口

Java8最大的變化是引入了函數式思想,也就是說函數能夠做爲另外一個函數的參數。函數式接口,要求接口中有且僅有一個抽象方法,所以常用的Runnable,Callable接口就是典型的函數式接口。可使用@FunctionalInterface註解,聲明一個接口是函數式接口。若是一個接口知足函數式接口的定義,會默認轉換成函數式接口。可是,最好是使用@FunctionalInterface註解顯式聲明。這是由於函數式接口比較脆弱,若是開發人員無心間新增了其餘方法,就破壞了函數式接口的要求,若是使用註解@FunctionalInterface,開發人員就會知道當前接口是函數式接口,就不會無心間破壞該接口。下面舉一個例子:

@java.lang.FunctionalInterface
public interface FunctionalInterface {
    void handle();
}
複製代碼

該接口只有一個抽象方法,而且使用註解顯式聲明。可是,函數式接口要求只有一個抽象方法卻能夠擁有若干個默認方法的(實例方法),好比下例:

@java.lang.FunctionalInterface
public interface FunctionalInterface {
    void handle();

    default void run() {
        System.out.println("run");
    }
}
複製代碼

該接口中,除了有抽象方法handle外,還有默認方法(實例方法)run。另外,任何被Object實現的方法都不能當作是抽象方法

lambda表達式

lambda表達式是函數式編程的核心,lambda表達式即匿名函數,是一段沒有函數名的函數體,能夠做爲參數直接傳遞給相關的調用者。lambda表達式極大的增長了Java語言的表達能力。lambda的語法結構爲:

(parameters) -> expression
或
(parameters) ->{ statements; }
複製代碼
  • 可選類型聲明:不須要聲明參數類型,編譯器能夠統一識別參數值。

  • 可選的參數圓括號:一個參數無需定義圓括號,但多個參數須要定義圓括號。

  • 可選的大括號:若是主體包含了一個語句,就不須要使用大括號。

  • 可選的返回關鍵字:若是主體只有一個表達式返回值則編譯器會自動返回值,大括號須要指定明表達式返回了一個數值。

完整示例爲(摘自菜鳥教程

public class Java8Tester {
   public static void main(String args[]){
      Java8Tester tester = new Java8Tester();
        
      // 類型聲明
      MathOperation addition = (int a, int b) -> a + b;
        
      // 不用類型聲明
      MathOperation subtraction = (a, b) -> a - b;
        
      // 大括號中的返回語句
      MathOperation multiplication = (int a, int b) -> { return a * b; };
        
      // 沒有大括號及返回語句
      MathOperation division = (int a, int b) -> a / b;
        
      System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
      System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
      System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
      System.out.println("10 / 5 = " + tester.operate(10, 5, division));
        
      // 不用括號
      GreetingService greetService1 = message ->
      System.out.println("Hello " + message);
        
      // 用括號
      GreetingService greetService2 = (message) ->
      System.out.println("Hello " + message);
        
      greetService1.sayMessage("Runoob");
      greetService2.sayMessage("Google");
   }
    
   interface MathOperation {
      int operation(int a, int b);
   }
    
   interface GreetingService {
      void sayMessage(String message);
   }
    
   private int operate(int a, int b, MathOperation mathOperation){
      return mathOperation.operation(a, b);
   }
}
複製代碼

另外,lambda還能夠訪問外部局部變量,以下例所示:

int adder = 5;
Arrays.asList(1, 2, 3, 4, 5).forEach(e -> System.out.println(e + adder));
複製代碼

實際上在lambda中訪問類的成員變量或者局部變量時,會隱式轉換成final類型變量,因此上例實際上等價於:

final int adder = 5;
Arrays.asList(1, 2, 3, 4, 5).forEach(e -> System.out.println(e + adder));
複製代碼

3. 方法引用

方法引用是爲了進一步簡化lambda表達式,經過類名或者實例名與方法名的組合來直接訪問到類或者實例已經存在的方法或者構造方法。方法引用使用**::來定義,::**的前半部分表示類名或者實例名,後半部分表示方法名,若是是構造方法就使用NEW來表示。

方法引用在Java8中使用方式至關靈活,總的來講,一共有如下幾種形式:

  • 靜態方法引用:ClassName::methodName;
  • 實例上的實例方法引用:instanceName::methodName;
  • 超類上的實例方法引用:supper::methodName;
  • 類的實例方法引用:ClassName:methodName;
  • 構造方法引用Class:new;
  • 數組構造方法引用::TypeName[]::new

下面來看一個例子:

public class MethodReferenceTest {

    public static void main(String[] args) {
        ArrayList<Car> cars = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Car car = Car.create(Car::new);
            cars.add(car);
        }
        cars.forEach(Car::showCar);

    }

    @FunctionalInterface
    interface Factory<T> {
        T create();
    }

    static class Car {
        public void showCar() {
            System.out.println(this.toString());
        }

        public static Car create(Factory<Car> factory) {
            return factory.create();
        }
    }
}


輸出結果:

learn.MethodReferenceTest$Car@769c9116
learn.MethodReferenceTest$Car@6aceb1a5
learn.MethodReferenceTest$Car@2d6d8735
learn.MethodReferenceTest$Car@ba4d54
learn.MethodReferenceTest$Car@12bc6874
複製代碼

在上面的例子中使用了Car::new,即經過構造方法的方法引用的方式進一步簡化了lambda的表達式,Car::showCar,即表示實例方法引用。

4. Stream

Java8中有一種新的數據處理方式,那就是流Stream,結合lambda表達式可以更加簡潔高效的處理數據。Stream使用一種相似於SQL語句從數據庫查詢數據的直觀方式,對數據進行如篩選、排序以及聚合等多種操做。

4.1 什麼是流Stream

Stream是一個來自數據源的元素隊列並支持聚合操做,更像是一個更高版本的Iterator,原始版本的Iterator,只能一個個遍歷元素並完成相應操做。而使用Stream,只須要指定什麼操做,如「過濾長度大於10的字符串」等操做,Stream會內部遍歷並完成指定操做。

Stream中的元素在管道中通過中間操做(intermediate operation)的處理後,最後由最終操做(terminal operation)獲得最終的結果。

  • 數據源:是Stream的來源,能夠是集合、數組、I/O channel等轉換而成的Stream;
  • 基本操做:相似於SQL語句同樣的操做,好比filter,map,reduce,find,match,sort等操做。

當咱們操做一個流時,實際上會包含這樣的執行過程:

獲取數據源-->轉換成Stream-->執行操做,返回一個新的Stream-->再以新的Stream繼續執行操做--->直至最後操做輸出最終結果

4.2 生成Stream的方式

生成Stream的方式主要有這樣幾種:

  1. 從接口Collection中和Arrays:

    • Collection.stream();
    • Collection.parallelStream(); //相較於串行流,並行流可以大大提高執行效率
    • Arrays.stream(T array);
  2. Stream中的靜態方法:

    • Stream.of();
    • generate(Supplier s);
    • iterate(T seed, UnaryOperator f);
    • empty();
  3. 其餘方法

    • Random.ints()
    • BitSet.stream()
    • Pattern.splitAsStream(java.lang.CharSequence)
    • JarFile.stream()
    • BufferedReader.lines()

下面對前面常見的兩種方式給出示例:

public class StreamTest {


    public static void main(String[] args) {
        //1.使用Collection中的方法和Arrays
        String[] strArr = new String[]{"a", "b", "c"};
        List<String> list = Arrays.asList(strArr);
        Stream<String> stream = list.stream();
        Stream<String> stream1 = Arrays.stream(strArr);

        //2. 使用Stream中提供的靜態方法
        Stream<String> stream2 = Stream.of(strArr);
        Stream<Double> stream3 = Stream.generate(Math::random);
        Stream<Object> stream4 = Stream.empty();
        Stream.iterate(1, i -> i++);

    }
}	
複製代碼

4.3 Stream的操做

常見的Stream操做有這樣幾種:

  1. Intermediate(中間操做):中間操做是指對流中數據元素作出相應轉換或操做後依然返回爲一個流Stream,仍然能夠供下一次流操做使用。經常使用的有:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip。
  2. Termial(結束操做):是指最終對Stream作出聚合操做,輸出結果。

中間操做

filter:對Stream中元素進行過濾

過濾元素爲空的字符串:

long count = stream.filter(str -> str.isEmpty()).count();
複製代碼

map:對Stream中元素按照指定規則映射成另外一個元素

將每個元素都添加字符串「_map」

stream.map(str -> str + "_map").forEach(System.out::println);
複製代碼

map方法是一對一的關係,將stream中的每個元素按照映射規則成另一個元素,而若是是一對多的關係的話就須要使用flatmap方法。

concat:對流進行合併操做

concat方法將兩個Stream鏈接在一塊兒,合成一個Stream。若兩個輸入的Stream都時排序的,則新Stream也是排序的;若輸入的Stream中任何一個是並行的,則新的Stream也是並行的;若關閉新的Stream時,原兩個輸入的Stream都將執行關閉處理。

Stream.concat(Stream.of(1, 2, 3), Stream.of(4, 5, 6)).
	forEach(System.out::println);
複製代碼

distinct:對流進行去重操做

去除流中重複的元素

Stream<String> stream = Stream.of("a", "a", "b", "c");
        stream.distinct().forEach(System.out::println);

輸出結果:
a
b
c
複製代碼

limit:限制流中元素的個數

截取流中前兩個元素:

Stream<String> stream = Stream.of("a", "a", "b", "c");
        stream.limit(2).forEach(System.out::println);

輸出結果:
a
a
複製代碼

skip:跳過流中前幾個元素

丟掉流中前兩個元素:

Stream<String> stream = Stream.of("a", "a", "b", "c");
        stream.skip(2).forEach(System.out::println);
輸出結果:
b
c
複製代碼

peek:對流中每個元素依次進行操做,相似於forEach操做

JDK中給出的例子:

Stream.of("one", "two", "three", "four")
            .filter(e -> e.length() > 3)
            .peek(e -> System.out.println("Filtered value: " + e))
            .map(String::toUpperCase)
            .peek(e -> System.out.println("Mapped value: " + e))
            .collect(Collectors.toList());
輸出結果:
Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR
複製代碼

sorted:對流中元素進行排序,能夠經過sorted(Comparator<? super T> comparator)自定義比較規則

Stream<Integer> stream = Stream.of(3, 2, 1);
        stream.sorted(Integer::compareTo).forEach(System.out::println);
輸出結果:
1
2
3
複製代碼

match:檢查流中元素是否匹配指定的匹配規則

Stream 有三個 match 方法,從語義上說:

  • allMatch:Stream 中所有元素符合傳入的 predicate,返回 true;
  • anyMatch:Stream 中只要有一個元素符合傳入的 predicate,返回 true;
  • noneMatch:Stream 中沒有一個元素符合傳入的 predicate,返回 true。

如檢查Stream中每一個元素是否都大於5:

Stream<Integer> stream = Stream.of(3, 2, 1);
boolean match = stream.allMatch(integer -> integer > 5);
System.out.println(match);
輸出結果:
false
複製代碼

結束操做

Collectors中常見歸約方法總結

重構和定製收集器Collectors

count:統計Stream中元素的個數

long count = stream.filter(str -> str.isEmpty()).count();
複製代碼

max/min:找出流中最大或者最小的元素

Stream<Integer> stream = Stream.of(3, 2, 1);
    System.out.println(stream.max(Integer::compareTo).get());

輸出結果:
3
複製代碼

forEach

forEach方法前面已經用了好屢次,其用於遍歷Stream中的所元素,避免了使用for循環,讓代碼更簡潔,邏輯更清晰。

示例:

Stream.of(5, 4, 3, 2, 1)
    .sorted()
    .forEach(System.out::println);
    // 打印結果
    // 1,2,3,4,5
複製代碼

reduce

Stream中的Reduce講解

Stream歸約方法總結以下:

Collectors提供的歸約方法.png

5. Optional

爲了解決空指針異常,在Java8以前須要使用if-else這樣的語句去防止空指針異常,而在Java8就可使用Optional來解決。Optional能夠理解成一個數據容器,甚至能夠封裝null,而且若是值存在調用isPresent()方法會返回true。爲了可以理解Optional。先來看一個例子:

public class OptionalTest {


    private String getUserName(User user) {
        return user.getUserName();
    }

    class User {
        private String userName;

        public User(String userName) {
            this.userName = userName;
        }

        public String getUserName() {
            return userName;
        }
    }
}
複製代碼

事實上,getUserName方法對輸入參數並無進行判斷是否爲null,所以,該方法是不安全的。若是在Java8以前,要避免可能存在的空指針異常的話就須要使用if-else進行邏輯處理,getUserName會改變以下:

private String getUserName(User user) {
    if (user != null) {
        return user.getUserName();
    }
    return null;
}
複製代碼

這是十分繁瑣的一段代碼。而若是使用Optional則會要精簡不少:

private String getUserName(User user) {
    Optional<User> userOptional = Optional.ofNullable(user);
    return userOptional.map(User::getUserName).orElse(null);
}
複製代碼

Java8以前的if-else的邏輯判斷,這是一種命令式編程的方式,而使用Optional更像是一種函數式編程,關注於最後的結果,而中間的處理過程交給JDK內部實現。

到如今,能夠直觀的知道Optional對避免空指針異常頗有效,下面,對Optional的API進行概括:

建立Optional

  1. Optional.empty():經過靜態工廠方法Optional.empty,建立一個空的Optional對象;
  2. Optional of(T value):若是value爲null的話,當即拋出NullPointerException;
  3. Optional ofNullable(T value):使用靜態工廠方法Optional.ofNullable,你能夠建立一個容許null值的Optional對象。

實例代碼:

//建立Optional
Optional<Object> optional = Optional.empty();
Optional<Object> optional1 = Optional.ofNullable(null);
Optional<String> optional2 = Optional.of(null);
複製代碼

經常使用方法

1.	boolean equals(Object obj):判斷其餘對象是否等於 Optional;
2. Optional<T> filter(Predicate<? super <T> predicate):若是值存在,而且這個值匹配給定的 predicate,返回一個Optional用以描述這個值,不然返回一個空的Optional;
3. <U> Optional<U> flatMap(Function<? super T,Optional<U>> mapper):若是值存在,返回基於Optional包含的映射方法的值,不然返回一個空的Optional;
4. T get():若是在這個Optional中包含這個值,返回值,不然拋出異常:NoSuchElementException;
5. int hashCode():返回存在值的哈希碼,若是值不存在 返回 0;
6. void ifPresent(Consumer<? super T> consumer):若是值存在則使用該值調用 consumer , 不然不作任何事情;
7. boolean isPresent():若是值存在則方法會返回true,不然返回 false;
8. <U>Optional<U> map(Function<? super T,? extends U> mapper):若是存在該值,提供的映射方法,若是返回非null,返回一個Optional描述結果;
9. T orElse(T other):若是存在該值,返回值, 不然返回 other;
10. T orElseGet(Supplier<? extends T> other):若是存在該值,返回值, 不然觸發 other,並返回 other 調用的結果;
11. <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier):若是存在該值,返回包含的值,不然拋出由 Supplier 繼承的異常;
12. String toString():返回一個Optional的非空字符串,用來調試
複製代碼

Optional經常使用方法總結:

Option的一些方法.png

6. Date/time API的改進

在Java8以前的版本中,日期時間API存在不少的問題,好比:

  • 線程安全問題:java.util.Date是非線程安全的,全部的日期類都是可變的;
  • 設計不好:在java.util和java.sql的包中都有日期類,此外,用於格式化和解析的類在java.text包中也有定義。而每一個包將其合併在一塊兒,也是不合理的;
  • 時區處理麻煩:日期類不提供國際化,沒有時區支持,所以Java中引入了java.util.Calendar和Java.util.TimeZone類;

針對這些問題,Java8從新設計了日期時間相關的API,Java 8經過發佈新的Date-Time API (JSR 310)來進一步增強對日期與時間的處理。在java.util.time包中經常使用的幾個類有:

  • 它經過指定一個時區,而後就能夠獲取到當前的時刻,日期與時間。Clock能夠替換System.currentTimeMillis()與TimeZone.getDefault()
  • Instant:一個instant對象表示時間軸上的一個時間點,Instant.now()方法會返回當前的瞬時點(格林威治時間);
  • Duration:用於表示兩個瞬時點相差的時間量;
  • LocalDate:一個帶有年份,月份和天數的日期,可使用靜態方法now或者of方法進行建立;
  • LocalTime:表示一天中的某個時間,一樣可使用now和of進行建立;
  • LocalDateTime:兼有日期和時間;
  • ZonedDateTime:經過設置時間的id來建立一個帶時區的時間;
  • DateTimeFormatter:日期格式化類,提供了多種預約義的標準格式;

示例代碼以下:

public class TimeTest {
    public static void main(String[] args) {
        Clock clock = Clock.systemUTC();
        Instant instant = clock.instant();
        System.out.println(instant.toString());

        LocalDate localDate = LocalDate.now();
        System.out.println(localDate.toString());

        LocalTime localTime = LocalTime.now();
        System.out.println(localTime.toString());

        LocalDateTime localDateTime = LocalDateTime.now();
        System.out.println(localDateTime.toString());

        ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
        System.out.println(zonedDateTime.toString());
    }
}
輸出結果爲:
2018-04-14T12:50:27.437Z
2018-04-14
20:50:27.646
2018-04-14T20:50:27.646
2018-04-14T20:50:27.647+08:00[Asia/Shanghai]
複製代碼

7. 其餘改進

Java8還在其餘細節上也作出了改變,概括以下:

  1. 以前的版本,註解在同一個位置只能聲明一次,而Java8版本中提供@Repeatable註解,來實現可重複註解;
  2. String類中提供了join方法來完成字符串的拼接;
  3. 在Arrays上提供了並行化處理數組的方式,好比利用Arrays類中的parallelSort可完成並行排序;
  4. 在Java8中在併發應用層面上也是下足了功夫:(1)提供功能更強大的Future:CompletableFuture;(2)StampedLock可用來替代ReadWriteLock;(3)性能更優的原子類::LongAdder,LongAccumulator以及DoubleAdder和DoubleAccumulator;
  5. 編譯器新增一些特性以及提供一些新的Java工具

參考資料

Stream的參考資料:

Stream API講解

Stream講解系列文章

對Stream的講解很細緻

Java8新特性之Stream API

Optional的參考資料:

Optional的API講解很詳細

對Optional部分的講解還不錯,值得參考

Java8新特性的介紹:

Java8新特性指南,很好的資料

跟上 Java 8 – 你忽略了的新特性

Java8新特性學習系列教程

相關文章
相關標籤/搜索