Java13都要來了,你還不瞭解Java8的新(舊)特性?

Java現在的版本迭代速度簡直不要太快,一不留神,就錯過了好幾個版本了。官方版本雖然已經更新到Java12了,可是就目前來講,大多數Java系統仍是運行在Java8上的,剩下一部分歷史遺留系統還跑在Java7,甚至Java6上。我剛學Java的時候,正好處於Java7版本末期,彼時已經有不少關於Java8新特性的風聲,當時做爲初學者,其實對此關注很少,只是依稀記得「lambda表達式」、「函數式編程」之類的,也不甚明白其中真意。真正大量應用Java8,大概是我工做一年以後的事情了,還記得當時是從IBM論壇上的一篇文章開始的。java

前幾天和一位大學同窗聊天的時候,談到了他們公司的一些問題,他們的系統是基於JDK7的版本,而且大部分員工不肯意升級版本,由於不肯意接受Java8的新特性。 我是以爲很是驚訝的,都快Java13了,你還不肯意瞭解Java8的新(舊)特性?所以有了這篇文章,本文將結合通俗易懂的代碼介紹Java8的lambda和stream相關的新(舊)特性,從中體會函數式編程的思想。編程

Lambda表達式

咱們能夠簡單認爲lambda表達式就是匿名內部類的更簡潔的語法糖。看下面兩種線程建立方式,直觀感覺一下。數組

// 匿名內部類
new Thread(new Runnable() {
    @Override
    public void run() {
        // ...
    }
}).start();

// lambda
new Thread(() -> {
    // ...
}).start();
複製代碼

想要熟練使用lambda表達式,首先要了解函數式接口,那麼什麼是函數式接口呢?首先必須得是interface修飾的接口,而後接口有且只有一個待實現的方法。有個簡單的方法能夠區分函數式接口與普通接口,那就是在接口上添加@FunctionalInterface註解,若是不報錯,那就是函數式接口,就可使用lambda表達式來代替匿名內部類了。看下面幾個例子,很顯然,A和B都是函數式接口,而C沒有抽象方法,D不是接口,因此都不能使用lambda表達式。併發

// 是函數式接口
interface A {
    void test();
}

// 是函數式接口
interface B {
    default void def() {
        // balabala...
    }
    void test();
}

// 不是函數式接口
interface C {
    default void def() {}
}

// 不是函數式接口
abstract class D {
   public abstract void test();
}
複製代碼

lambda表達式根據實現接口的方法參數、返回值、代碼行數等,有幾種不一樣的寫法:框架

  1. 無參數的
interface A {
    void test();
}

A a = () -> {
    // ...
};
複製代碼
  1. 單個參數的
interface B {
    void test(String arg);
}

B b = arg -> {
    // ...
};
複製代碼
  1. 多個參數的
interface C {
    void test(String arg1, String arg2);
}

C c = (a1, a2) -> {
    // ...
};

interface D {
    void test(String arg1, String arg2, String arg3);
}

D d = (a1, a2, a3) -> {
    // ...
};
複製代碼
  1. 只有一行代碼的,能夠省略大括號
interface B {
    void test(String arg);
}

B b = arg -> System.out.println("hello " + arg);
複製代碼
  1. 有返回值的
interface E {
    String get(int arg);
}

E e = arg -> {
    int r = arg * arg;
    return String.valueOf(r);
};
// 只有一行代碼能夠省略return和大括號
e = arg -> String.valueOf(arg * arg);
複製代碼

有一點須要注意,lambda表達式和匿名內部類同樣,都只能引用final修飾的外部的資源,雖然Java8中能夠不用顯示的聲明變量爲final的,可是在lambda表達式內部是不能修改的。分佈式

int i = 0;
A a = () -> {
    i++; // 這裏編譯不經過
    // ...
};
複製代碼

lambda表達式還有更加簡便的寫法,看下面代碼,這個::符號是否是很熟悉啊?果真仍是脫離不了C體系的影響😆ide

class Math {
    int max(int x, int y) {
        return x < y ? y : x;
    }
    
    static int sum(int x, int y) {
        return x + y;
    }
}

interface Computer {
    int eval(int arg1, int arg2);
}


// 直接經過類名引用
Computer sumFun = Math::sum;
// 和上面是等價的
sumFun = (x, y) -> x + y;

Math math = new Math();
// 經過對象引用
Computer maxFun = math::max;
// 和上面是等價的
maxFun = (x, y) -> x < y ? y : x;

int sum = sumFun.eval(1, 2);
int max = maxFun.eval(2, 3);
複製代碼

將上面的例子擴展一下,看下面的代碼,體會一下函數式編程的思想。咱們把函數做爲參數,在真正調用compute方法的時候,才肯定應該進行何種運算。函數式編程

class Biz {
    int x, y;
    Biz(int x, int y) {
        this.x = x;
        this.y = y;
    }
    int compute(Computer cpt) {
        // ...
        return cpt.eval(x, y);
    }
}
Biz biz = new Biz(1, 2);
int result = biz.compute((x, y) -> x * y);
result = biz.compute(Math::sum);
複製代碼

內置函數式接口

Java8內置了不少函數式接口,所有放在java.util.function包下面,這些接口已經能知足平常開發中大部分的需求了,這些函數接口主要分爲如下幾類:函數

  1. 無返回值、有參數的 Consumer 類型
Consumer<String> consumer = str -> {
    // ...
};
BiConsumer<String, String> biConsumer = (left, right) -> {
    // ...
};
複製代碼
  1. 有返回值、無參數的 Supplier 類型
Supplier<String> supplier = () -> {
    // ...
    return "hello word";
};
複製代碼
  1. 有返回值、有參數的 Function 類型
Function<Integer, String> function = i -> {
    // ...
    return "hello word " + i;
};
BiFunction<Integer, Integer, String> biFunction = (m, n) -> {
    int s = m + n;
    return "sum = " + s;
};
複製代碼
  1. 返回boolean、有參數的Predicate類型,能夠看作是Function的一種特例
Predicate<String> predicate = str -> {
    // ...
    return str.charAt(0) == 'a';
};
BiPredicate<String, String> biPredicate = (left, right) -> {
    // ...
    return left.charAt(0) == right.charAt(0);
};
複製代碼

集合類的Stream

Java8爲集合框架添加了流式處理的功能,爲咱們提供了一種很方便的處理集合數據的方式。
Stream大致上能夠分爲兩種操做:中間操做和終端操做,這裏先不考慮中間操做狀態問題。中間操做能夠有多個,可是終端操做只能有一個。中間操做通常是一些對流元素的附加操做,這些操做不會在添加中間操做的時候當即生效,只有當終端操做被添加時,纔會開始啓動整個流。並且流是不可複用的,一旦流啓動了,就不能再爲這個流附加任何終端操做了。工具

Stream的建立方式

流的建立方式大概有如下幾種:

String[] array = 
Stream<String> stream;
// 1. 經過Stream的builder構建
stream = Stream.<String>builder()
        .add("1")
        .add("2")
        .build();

// 2. 經過Stream.of方法構建,這種方法能夠用來處理數組
stream = Stream.of("1", "2", "3");

// 3. 經過Collection類的stream方法構建,這是經常使用的作法
Collection<String> list = Arrays.asList("1", "2", "3");
stream = list.stream();

// 4. 經過IntStream、LongStream、DoubleStream構建
IntStream intStream = IntStream.of(1, 2, 3);
LongStream longStream = LongStream.range(0L, 10L);
DoubleStream doubleStream = DoubleStream.of(1d, 2d, 3d);

// 5. 其實上面這些方法都是經過StreamSupport來構建的
stream = StreamSupport.stream(list.spliterator(), false);
複製代碼
中間操做

若是你熟悉spark或者flink的話,就會發現,中間操做其實和spark、flink中的算子是同樣的,連命名都是同樣的,流在調用中間操做的方法是,並不會當即執行這個操做,會等到調用終端操做時,纔會執行,下面例子中都添加了一個toArray的終端操做,把流轉換爲一個數組。

  1. filter操做,參數爲Predicate,該操做會過濾掉數據流中斷言結果爲false的全部元素
// 將返回一個只包含大於1的元素的數組
// array = [2, 3]
Integer[] array = Stream.of(1, 2, 3)
                        .filter(i -> i > 1)
                        .toArray(Integer[]::new);
複製代碼
  1. map操做,參數爲Function,該操做會將數據流中元素都處理成新的元素,mapToInt、mapToLong、mapToDouble和map相似
// 將每一個元素都加10
// array = [11, 12, 13]
Integer[] array = Stream.of(1, 2, 3)
                        .map(i -> i + 10)
                        .toArray(Integer[]::new);
複製代碼
  1. flatMap操做,參數爲Function,不過Function返回值是個Stream,該操做和map同樣,都會處理每一個元素,不一樣的是map會將當前流中的一個元素處理成另外一個元素,而flatMap則是將當前流中的一個元素處理成多個元素,flatMapToInt、flatMapToDouble、flatMapToLong和flatMap相似。
// 把每一個元素都按","拆分,返回Stream
// array = ["1", "2", "3", "4", "5", "6"]
String[] array = Stream.of("1", "2,3", "4,5,6")
                       .flatMap(s -> {
                           String[] split = s.split(",");
                           return Stream.of(split);
                       })
                       .toArray(String[]::new);
複製代碼
  1. peek操做,參數爲Consumer,改操做會處理每一個元素,但不會返回新的對象。
Stream.of(new User("James", 40), new User("Kobe", 45), new User("Durante", 35))
      .peek(user -> {
          user.name += " NBA";
          user.age++;
      }).forEach(System.out::println);
// User(name=James NBA, age=41)
// User(name=Kobe NBA, age=46)
// User(name=Durante NBA, age=36)
複製代碼
  1. distinct操做,很顯然這是一個去重操做,會根據每一個元素的equals方法去重。
// array = [hello, hi]
String[] array = Stream.of("hello", "hi", "hello")
                       .distinct()
                       .toArray(String[]::new);
複製代碼
  1. sorted操做,很顯然這是個排序操做,若是使用無參數的sorted,則會先將元素轉換成Comparable類型,若是不能轉換會拋出異常。也能夠傳入一個比較器Comparator,而後會根據比較器的比較結果排序。
// 根據字符串長度排序
// sorted = [hi, haha, hello]
String[] sorted = Stream.of("hello", "hi", "haha")
                        .sorted(Comparator.comparingInt(String::length))
                        .toArray(String[]::new);
複製代碼
  1. limit操做,參數是一個非負的long類型整數,該操做會截取流的前n個元素,若是參數n大於流的長度,就至關於什麼都沒作。
// 截取前三個
// array = [hello, hi, haha]
String[] array = Stream.of("hello", "hi", "haha", "heheda")
                       .limit(3)
                       .toArray(String[]::new);
複製代碼
  1. skip操做,參數是一個非負的long類型整數,該操做會跳過流的前n個元素,若是參數n大於流的長度,就會跳過所有元素。
// 跳過前兩個
// array = [haha, heheda]
String[] array = Stream.of("hello", "hi", "haha", "heheda")
                       .skip(2)
                       .toArray(String[]::new);
複製代碼
終端操做

每一個流只能有一個終端操做,調用終端操做方法後,流才真正開始執行中間操做,通過多箇中間操做的處理後,最終會在終端操做這裏產生一個結果。

  1. forEach操做,參數爲Consumer,這至關於一個簡單的遍歷操做,會遍歷處理過的流中的每一個元素。
Stream.of("hello", "hi", "haha", "heheda")
      .limit(0)
      .forEach(s -> System.out.println(">>> " + s));
複製代碼
  1. toArray操做,這個操做在上面的已經屢次提到了,該操做根據中間操做的處理結果,生成一個新的數組
// array = [hello, hi, haha, heheda]
Object[] array = Stream.of("hello", "hi", "haha", "heheda")
                       .toArray();
複製代碼
  1. allMatch、anyMatch、noneMatch操做,都就接收一個Predicate,用於匹配查詢
// b = false
boolean b = Stream.of("hello", "hi", "haha", "heheda")
                  .allMatch(s -> s.equals("hello"));
// b = true
b = Stream.of("hello", "hi", "haha", "heheda")
          .anyMatch(s -> s.equals("hello"));
// b = true
b = Stream.of("hello", "hi", "haha", "heheda")
          .noneMatch(s -> s.equals("nihao"));
複製代碼
  1. findFirst、findAny操做,都會返回流中的一個元素,返回值使用Optional包裝。
String first = Stream.of("hello", "hi", "haha", "heheda")
                     .findFirst().get();
first = Stream.of("hello", "hi", "haha", "heheda")
              .findAny().get();
複製代碼
  1. reduce是比較複雜的一個操做,它有三個重載方法,單參數、雙參數和三參數的。主要用來作累計運算的,不管哪一個重載方法都須要咱們提供一個雙參數的BiFunction,這個BiFunction的第一個參數表示前面全部元素的累計值,第二個參數表示當前元素的值,咱們看幾個例子。
// 拼接字符串
// reduceS ="hello ; hi ; haha ; heheda"
String reduceS = Stream.of("hello", "hi", "haha", "heheda")
                 .reduce((x, y) -> x + " ; " + y)
                 .get();

// 統計全部字符串的長度
// lenght = 17
int length = Stream.of("hello", "hi", "haha", "heheda")
                   .map(String::length)
                   .reduce(0, (x, y) -> x + y);

// 同上,不同的是,第三個參數是個合併器,用於並行流各個並行結果的合併
int reduce = Stream.of("hello", "hi", "haha", "heheda")
                   .reduce(0, (x, y) -> x + y.length(), (m, n) -> m + n);
複製代碼
  1. max、min、count操做,這三個操做都比較簡單,分別返回流中最大值、最小值和元素個數
// max = "heheda"
String max = Stream.of("hello", "hi", "haha", "heheda")
                   .max(Comparator.comparingInt(String::length))
                   .get();
// min = "hi"
String min = Stream.of("hello", "hi", "haha", "heheda")
                   .min(Comparator.comparingInt(String::length))
                   .get();
// count = 4
long count = Stream.of("hello", "hi", "haha", "heheda")
                   .count();
複製代碼
  1. collect操做,這個操做相似於toArray,不過這裏是把流轉換成Collection或者Map。通常這個操做結合着Collectors工具類使用。看下面幾個簡單的例子:
// 轉換爲List [hello, hehe, hehe, hi, hi, hi]
List<String> list = Stream.of("hello", "hehe", "hehe", "hi", "hi", "hi")
                          .collect(Collectors.toList());
// 轉換爲Set [hi, hehe, hello]
Set<String> set = Stream.of("hello", "hehe", "hehe", "hi", "hi", "hi")
                        .collect(Collectors.toSet());
// 下面這個稍微複雜一些,實現了將字符串流轉換爲Map,map的key是字符串自己,value是字符串出現的次數
// map = {hi=3, hehe=2, hello=1}
Map<String, Integer> map = Stream.of("hello", "hehe", "hehe", "hi", "hi", "hi")
                                 .collect(Collectors.toMap(s -> {
                                     // 字符串做爲map的key
                                     return s;
                                 }, s -> {
                                     // 1做爲map的value
                                     return 1;
                                 }, (x, y) -> {
                                     // key相同時的合併操做
                                     return x + y;
                                 }, () -> {
                                     // 還能夠指定Map的類型
                                     return new LinkedHashMap<>();
                                 }));
複製代碼
單詞統計的案例

最後,我將上面介紹的一些操做結合起來,經過一個單詞統計的例子,讓你們更直觀的感覺流式處理的好處。

Path path = Paths.get("/Users/.../test.txt");
List<String> lines = Files.readAllLines(path);
lines.stream()
     .flatMap(line -> {
         String[] array = line.split("\\s+");
         return Stream.of(array);
     })
     .filter(w -> !w.isEmpty())
     .sorted()
     .collect(Collectors.toMap(w -> w, w -> 1,
                               (x, y) -> x + y,
                               LinkedHashMap::new))
     .forEach((k, v) -> System.out.println(k + " : " + v));
複製代碼

遺憾的是Java8的Stream並不支持分組和聚合操做,因此這裏使用了toMap方法來統計單詞的數量。

Java8的集合類提供了parallelStream方法用於獲取一個並行流(底層是基於ForkJoin作的),通常不推薦這麼作,數據規模較小時使用並行Stream反而不如串行來的高效,而數據規模很大的時候,單機的計算能力畢竟有限,我仍是推薦使用更增強大的spark或者flink來作分佈式計算。

至此,Java8關於lambda和Stream的特性就分析完畢了,固然Java8做爲一個經典版本,確定不止於此,Doug Lea大佬的併發包也在Java8版本更新了很多內容,提供了更加豐富多彩的併發工具,還有新的time包等等,這些均可以拿出來做爲一個新的的話題討論。指望以後的文章中能和你們繼續分享相關內容。

原創不易,轉載請註明出處!www.yangxf.top/

相關文章
相關標籤/搜索