<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Java8 的這個(gè)特性,用起來真的很爽!

          共 48885字,需瀏覽 98分鐘

           ·

          2021-04-13 02:35

          汪偉俊 作者

          Java技術(shù)迷 | 出品

          Java8新特性中最為重要的便是Lambda表達(dá)式和Stream API了,先來了解一下Lambda表達(dá)式吧。


          Lambda表達(dá)式


          Lambda表達(dá)式是一個(gè)匿名函數(shù),我們可以將Lambda表達(dá)式理解為一段可以作為參數(shù)傳遞的代碼,通過Lambda表達(dá)式,我們可以將Java程序變得更加簡(jiǎn)潔和靈活。

          來看一段程序:

          @Test
          public void test() {
              Comparator<Integer> comparator = new Comparator<Integer>() {
                  @Override
                  public int compare(Integer o1, Integer o2) {
                      return o2 - o1;
                  }
              };
              Set<Integer> set = new TreeSet<>(comparator);
              set.add(3);
              set.add(2);
              set.add(1);
              System.out.println(set);
          }

          在JDK8以前,若是想實(shí)現(xiàn)對(duì)TreeSet的自定義排序,我們可以創(chuàng)建匿名的比較器類將其傳入TreeSet的構(gòu)造方法,而JDK8之后,使用Lambda表達(dá)式可以簡(jiǎn)化匿名內(nèi)部類的代碼:

          @Test
          public void test() {
              Comparator<Integer> comparator = (o1, o2) -> o2 - o1;
              Set<Integer> set = new TreeSet<>(comparator);
              set.add(3);
              set.add(2);
              set.add(1);
              System.out.println(set);
          }

          原本匿名內(nèi)部類的創(chuàng)建現(xiàn)在直接被簡(jiǎn)化成了一行代碼:

          Comparator<Integer> comparator = (o1, o2) -> o2 - o1;

          下面就來看看Lambda該如何使用,也就是學(xué)習(xí)一下它的語法。


          Lambda表達(dá)式語法


          在Java8中新引入了一個(gè)操作符:-> ,它被稱為箭頭操作符或Lambda操作符,箭頭操作符將Lambda表達(dá)式拆分成了左側(cè)和右側(cè)的兩部分,其中左側(cè)為L(zhǎng)ambda表達(dá)式的 參數(shù)列表 ,右側(cè)為L(zhǎng)ambda表達(dá)式的功能代碼,也叫 Lambda體 。對(duì)應(yīng)一個(gè)具體的Lambda表達(dá)式:

          Comparator<Integer> comparator = (o1, o2) -> o2 - o1;

          此時(shí)箭頭操作符的左側(cè)就是Comparator接口中compare方法的參數(shù)列表,右側(cè)即為該方法所需要實(shí)現(xiàn)的功能:

          @FunctionalInterface
          public interface Comparator<T{
              int compare(T o1, T o2);
              ......
          }

          基于接口中方法聲明的不同,Lambda表達(dá)式的編寫方式也會(huì)隨之發(fā)生相應(yīng)的變化,大體分為以下幾類:

          • 無參無返回值:Runnable runnable = () -> System.out.println("Hello Lambda!");
          • 有一個(gè)參數(shù)但無返回值:Consumer consumer = (s) -> System.out.println(s);
            • 若是方法只有一個(gè)參數(shù),則可以省略小括號(hào),所以可以簡(jiǎn)寫為:Consumer consumer = s -> System.out.println(s);
          • 有多個(gè)參數(shù)有返回值,且Lambda體中有多條語句:
          Comparator<Integer> comparator = (o1, o2) -> {
              System.out.println("從大到小排列");
              return o2 - o1;
          };

          若是Lambda體中有多條語句,則Lambda體必須用大括號(hào)包裹起來,若是只有一條語句,則可以省略大括號(hào)。

          • 有多個(gè)參數(shù)有返回值,但Lambda體中只有一條返回語句:Comparator comparator = (o1, o2) -> o2 - o1;
            • 對(duì)于這種情況,可以省略大括號(hào)和return關(guān)鍵字,Lambda體中只剩下需要return的內(nèi)容

          會(huì)發(fā)現(xiàn)在所有的Lambda表達(dá)式中我們并沒有為參數(shù)定義任何類型,這是因?yàn)镴VM編譯器能夠通過上下文自動(dòng)推斷出參數(shù)類型。

          需要注意的是,并不是所有的接口實(shí)現(xiàn)都可以使用Lambda表達(dá)式,它需要 函數(shù)式接口 的支持,那么什么是函數(shù)式接口呢?

          函數(shù)式接口指的是接口中只有一個(gè)抽象方法的接口

          這非常好理解,若是接口中存在多個(gè)抽象方法,Lambda表達(dá)式是無法知曉我們到底需要實(shí)現(xiàn)哪個(gè)方法的,所以Lambda表達(dá)式的使用必須基于函數(shù)式接口。

          JDK1.8為此專門提供了 @FunctionalInterface 注解來聲明一個(gè)函數(shù)式接口,倘若在接口中聲明了該注解,則該接口必須擁有且只能擁有一個(gè)方法:

          @FunctionalInterface
          interface calc{
              void  add();
          }

          若不滿足要求則會(huì)編譯報(bào)錯(cuò)。

          有些同學(xué)可能會(huì)發(fā)現(xiàn)在使用Lambda表達(dá)式實(shí)現(xiàn)一些功能時(shí),還需要自己去額外編寫一個(gè)函數(shù)式接口,而事實(shí)上,JDK1.8已經(jīng)為我們內(nèi)置了四大核心函數(shù)式接口,分別是:

          1. Consumer:消費(fèi)型接口,抽象方法為:void accept(T t);
          2. Supplier:供給型接口,抽象方法為:T get();
          3. Function<T, R>:函數(shù)型接口,抽象方法為:R apply(T t);
          4. Predicate:斷言型接口,抽象方法為:boolean test(T t);

          通過它們就已經(jīng)能夠解決大部分的問題了,具體使用哪個(gè)接口可以根據(jù)自己的實(shí)際需求決定,比如若是需要實(shí)現(xiàn)的功能帶參數(shù)而無返回值,則使用消費(fèi)型接口;若是需要實(shí)現(xiàn)的功能無參數(shù)但有返回值,則使用供給型接口。

          這里以供給型接口為例,實(shí)現(xiàn)一個(gè)需求,產(chǎn)生指定個(gè)數(shù)的整數(shù),并放入集合中,代碼如下:

          @Test
          public void test() {
              List<Integer> list = getList(5, () -> new Random().nextInt(20));
              System.out.println(list);
          }

          public List<Integer> getList(int len, Supplier<Integer> supplier) {
              List<Integer> list = new ArrayList<>();
              for (int i = 0; i < len; i++) {
                  list.add(supplier.get());
              }
              return list;
          }

          方法引用


          若Lambda體中的內(nèi)容有方法已經(jīng)實(shí)現(xiàn)了,那么就可以使用方法引用,可以理解為方法引用是Lambda表達(dá)式的另一種表現(xiàn)形式,方法引用主要有以下三種形式:

          1. 對(duì)象::實(shí)例方法名
          2. 類::靜態(tài)方法名
          3. 類::實(shí)例方法名

          來看一個(gè)例子:

          @Test
          public void test() {
              Consumer<String> consumer = (x)-> System.out.println(x);
              consumer.accept("Hello!");
          }

          這里使用Consumer消費(fèi)型接口實(shí)現(xiàn)了一個(gè)輸出字符串的功能,由于Lambda體中的內(nèi)容已經(jīng)被 System.out.println 實(shí)現(xiàn)了,所以可以簡(jiǎn)寫為:

          @Test
          public void test() {
              Consumer<String> consumer = System.out::println; // 簡(jiǎn)寫為...
              consumer.accept("Hello!");
          }

          然而方法引用需要遵循一個(gè)原則,即:Lambda體中的方法參數(shù)和返回值需要與函數(shù)式接口中的抽象方法聲明一致,比如這里的Consumer接口中的抽象方法為:

          void accept(T t);

          而輸出語句的聲明如下:

          public void println(String x) {
              synchronized (this) {
                  print(x);
                  newLine();
              }
          }

          它們都帶有一個(gè)參數(shù)且無返回值,所以可以使用方法引用。

          又比如:

          @Test
          public void test() {
              User user = new User();
              user.setName("aaa");
              Supplier<String> supplier = () -> user.getName();
              String str = supplier.get();
              System.out.println(str);
          }

          這里的 user.getName() 也可以使用方法引用,簡(jiǎn)寫為:

          Supplier<String> supplier = user::getName;

          這也是因?yàn)閁ser中g(shù)et方法與函數(shù)式接口中方法參數(shù)和返回值的聲明相同:

          // User對(duì)象的getName方法
          public String getName() {
              return name;
          }

          // Supplier接口的get方法
          get();

          我們還可以方法引用類的靜態(tài)方法,例如:

          @Test
          public void test() {
              Comparator<Integer> comparator = Integer::compare;
              int result = comparator.compare(12);
              System.out.println(result);
          }

          因?yàn)長(zhǎng)ambda體中的內(nèi)容已經(jīng)被compare方法實(shí)現(xiàn)且參數(shù)和返回值聲明與Comparator接口中的抽象方法聲明相同,它就能夠使用方法引用:

          Comparator<Integer> comparator = Integer::compare;

          且compare是Integer類的靜態(tài)方法,這種引用方式被稱為類的靜態(tài)方法引用。

          最后一種形式是類的實(shí)例方法引用,比如:

          @Test
          public void test() {
              BiPredicate<String, String> biPredicate = (s1, s2) -> s1.equals(s2);
              boolean flag = biPredicate.test("abc""abc");
              System.out.println(flag);
          }

          這里實(shí)現(xiàn)了函數(shù)式接口的方法使其能夠判斷兩個(gè)字符串的內(nèi)容是否相同,它能夠被簡(jiǎn)寫為:

          BiPredicate<String, String> biPredicate = String::equals;

          對(duì)于類的實(shí)例方法引用,也有它的要求,必須滿足第一個(gè)參數(shù)是方法的調(diào)用者,第二個(gè)參數(shù)是調(diào)用方法的參數(shù)才能使用該引用方式。


          構(gòu)造器引用


          構(gòu)造器引用與方法引用類似,它通過 類名::new 實(shí)現(xiàn),比如:

          @Test
          public void test() {
              Supplier<User> supplier = ()->new User();
              User user = supplier.get();
              System.out.println(user);
          }

          此時(shí)創(chuàng)建User對(duì)象的過程就可以使用構(gòu)造器引用來簡(jiǎn)化:

          Supplier<User> supplier = User::new;

          我們都知道一個(gè)類可以有多個(gè)重載的構(gòu)造器,那么構(gòu)造器引用調(diào)用的是類中的哪個(gè)構(gòu)造器呢?和方法引用類似,我們?nèi)匀煌ㄟ^構(gòu)造器方法與接口中抽象方法參數(shù)和返回值的聲明來判斷調(diào)用哪個(gè)構(gòu)造器,這里的Supplier接口中的抽象方法是一個(gè)不帶參數(shù)的方法:

          get();

          所以它將調(diào)用對(duì)象的無參構(gòu)造方法,又比如:

          @Test
          public void test() {
              Function<String, User> function = User::new;
              User user = function.apply("zhangsan");
              System.out.println(user);
          }

          來看看Function接口中的抽象方法:

          apply(T t);

          它帶有一個(gè)參數(shù),所以調(diào)用的是User對(duì)象帶一個(gè)參數(shù)的構(gòu)造方法:

          public class User {
              private String name;
              
              public User(String name){
                  this.name = name;
              }
           ......
          }

          Stream API


          JDK8中另一重要的新特性就是Stream API,通過Stream,我們能夠在集合數(shù)據(jù)中進(jìn)行一些非常復(fù)雜的查找、過濾、映射等操作,而且實(shí)現(xiàn)起來會(huì)非常高效和簡(jiǎn)單。

          @Test
          public void test() {
              // 通過集合的stream方法獲取流
              List<String> list = new ArrayList<>();
              Stream<String> stream = list.stream();

              // 通過Arrays工具類的stream方法獲取流
              Stream<String> stream1 = Arrays.stream(new String[]{"1""2""3""4""5"});

              // 通過Stream類的of方法獲取流
              Stream<Integer> stream2 = Stream.of(12345);

              // 創(chuàng)建無限流
              Stream<Integer> stream3 = Stream.iterate(0, (x) -> x + 2);
          }

          以上是四種獲取Stream的方式,最后一種創(chuàng)建的是無限流,也就是說,該流創(chuàng)建的是從0開始,每次加2的無限序列。

          使用Stream我們能夠很輕松地實(shí)現(xiàn)過濾操作,比如獲取無限流中前5個(gè)元素,代碼如下:

          @Test
          public void test() {
              Stream<Integer> stream = Stream.iterate(0, (x) -> x + 2);
              Stream<Integer> stream1 = stream.limit(5);
              stream1.forEach(System.out::println);
          }

          通過limit方法即可實(shí)現(xiàn)獲取前5個(gè)元素,這里使用了forEach方法進(jìn)行遍歷,并使用了Lambda表達(dá)式,我們一起來復(fù)習(xí)一下,查看forEach方法的源碼:

          void forEach(Consumer<? super T> action);

          可以看到該方法的參數(shù)是一個(gè)消費(fèi)型的函數(shù)式接口,其接口的抽象方法是帶參而無返回值的,所以若是想輸出元素,則可以如此做:

          stream1.forEach((i) -> System.out.println(i));

          又因?yàn)檩敵稣Z句的參數(shù)和返回值與接口抽象方法的聲明一致,所以可以使用方法引用,最終簡(jiǎn)化為:

          stream1.forEach(System.out::println);

          我們還可以通過Stream類的generate方法生成無限流:

          @Test
          public void test() {
              Stream.generate(Math::random)
                  .limit(5)
                  .forEach(System.out::println);
          }

          generate方法需要的是一個(gè)供給型接口,它的抽象方法是不帶參而有返回值的,我們通過 Math.random() 方法生成隨機(jī)數(shù)作為返回值,又因?yàn)閞andom方法也是帶一個(gè)參而有返回值的,所以可以使用類的靜態(tài)方法引用,后續(xù)的Stream操作中將涉及大量的Lambda表達(dá)式,屆時(shí)將不再過多介紹Lambda表達(dá)式的寫法。


          Stream的篩選操作


          剛才我們通過limit方法簡(jiǎn)單地了解到Stream的篩選功能,當(dāng)然了,Stream的篩選能力可遠(yuǎn)不止如此,這里介紹四種篩選方法:

          1. filter
          2. limit
          3. skip
          4. distinct
          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"300),
                  new User("王五"251),
                  new User("趙六"421)
              );
              // 篩選出年齡大于25的用戶
              Stream<User> stream = userList.stream()
                  .filter((user) -> user.getAge() > 25);
              stream.forEach(System.out::println);
          }

          通過filter方法,我們能夠?qū)崿F(xiàn)很多的篩選功能,比如這里就可以篩選出年齡大于25的用戶,filter方法的參數(shù)是一個(gè)斷言型接口,接收一個(gè)參數(shù),返回值為boolean類型,即:為true則滿足篩選條件,為false則不滿足。

          limit方法我們已經(jīng)使用過了,它用來截?cái)郤tream,使得Stream獲取從開始位置到指定位置的元素內(nèi)容。

          第三個(gè)方法是skip:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"300),
                  new User("王五"251),
                  new User("趙六"421)
              );
              // 篩選出年齡大于25的用戶
              Stream<User> stream = userList.stream()
                  .skip(2);
              stream.forEach(System.out::println);
          }

          它與limit方法正好相反,skip會(huì)丟棄掉從開始位置到指定位置的元素內(nèi)容,比如上面這段程序便會(huì)將 張三 和 李四 用戶的信息丟掉。

          最后一個(gè)方法是distinct:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"300),
                  new User("王五"251),
                  new User("王五"251),
                  new User("王五"251),
                  new User("趙六"421)
              );
              // 篩選出年齡大于25的用戶
              Stream<User> stream = userList.stream()
                  .distinct();
              stream.forEach(System.out::println);
          }

          該方法用于去除流中重復(fù)的元素,需要注意的是該方法依賴于equals和hashCode方法,所以User對(duì)象必須重寫這兩個(gè)方法:

          public class User {

              private String name;
              private Integer age;
              private Integer sex;
           ......
              @Override
              public boolean equals(Object o) {
                  if (this == o) return true;
                  if (o == null || getClass() != o.getClass()) return false;
                  User user = (User) o;
                  return Objects.equals(name, user.name) && Objects.equals(age, user.age) && Objects.equals(sex, user.sex);
              }

              @Override
              public int hashCode() {
                  return Objects.hash(name, age, sex);
              }
          }

          Stream的映射操作


          Stream中的map方法用于映射操作,它能將元素轉(zhuǎn)換成其它形式或提取信息,接收一個(gè)函數(shù)作為參數(shù),該函數(shù)會(huì)被應(yīng)用到Stream中的每個(gè)元素上,并將其映射成為一個(gè)新的元素。

          @Test
          public void test() {
              List<String> list = Arrays.asList("aa""bb""cc""dd""ee");
              list.stream()
                  .map(String::toUpperCase)
                  .forEach(System.out::println);
          }

          map方法的參數(shù)是一個(gè)函數(shù)型接口,Stream中的每個(gè)元素都會(huì)被應(yīng)用到該接口方法的實(shí)現(xiàn)上,所以每個(gè)元素都會(huì)被轉(zhuǎn)換成大寫字母。

          map方法也能用于提取Stream中每個(gè)元素的信息:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"300),
                  new User("王五"251),
                  new User("趙六"421)
              );
              userList.stream()
                  .map(User::getName)
                  .forEach(System.out::println);
          }

          此時(shí)所有用戶的名字就都被提取出來了。


          Stream的排序操作


          Stream中的排序也分為兩種,自然排序和自定義排序,首先是自然排序,調(diào)用sorted方法即可實(shí)現(xiàn):

          @Test
          public void test() {
              List<Integer> list = Arrays.asList(316759);
              list.stream()
                  .sorted()
                  .forEach(System.out::println);
          }

          自定義排序就需要傳入自己實(shí)現(xiàn)的比較器:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("趙六"421)
              );
              userList.stream()
                  .sorted((u1, u2) -> {
                      if (u1.getAge().equals(u2.getAge())) {
                          return u1.getName().compareTo(u2.getName());
                      } else {
                          return u1.getAge().compareTo(u2.getAge());
                      }
                  })
                  .forEach(System.out::println);
          }

          該程序段實(shí)現(xiàn)了按照年齡對(duì)User信息進(jìn)行排序,若年齡相同,則再按照姓名排序。


          Stream的匹配操作


          Stream中提供了豐富的匹配方法用于校驗(yàn)數(shù)據(jù),分別如下:

          • allMatch:是否匹配所有元素
          • anyMatch:是否至少匹配一個(gè)元素
          • noneMatch:是否沒有匹配所有元素
          • findFirst:返回第一個(gè)元素
          • findAny:返回流中的任意元素
          • count:返回元素個(gè)數(shù)
          • max:返回最大值
          • min:返回最小值

          比如:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("趙六"421)
              );
              boolean flag = userList.stream()
                  .allMatch((u) -> u.getSex().equals(1));
              System.out.println(flag);
          }

          這段程序中,它會(huì)判斷Stream中的所有User對(duì)象的sex值是否為1,若滿足,則返回true,否則返回false,這里的結(jié)果當(dāng)然就是false了。

          若是將匹配方法修改為 anyMatch ,則結(jié)果會(huì)變?yōu)閠rue:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("趙六"421)
              );
              boolean flag = userList.stream()
                  .anyMatch((u) -> u.getSex().equals(1));
              System.out.println(flag);
          }

          當(dāng)所有User對(duì)象的sex為0時(shí)結(jié)果才為false。

          noneMatch方法:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("趙六"421)
              );
              boolean flag = userList.stream()
                  .noneMatch((u) -> u.getSex().equals(1));
              System.out.println(flag);
          }

          因?yàn)镾tream中有User對(duì)象的sex值為1,所以沒有匹配所有元素是不成立的,故結(jié)果為false,只有當(dāng)所有User對(duì)象的sex值均為0,此時(shí)沒有匹配所有元素成立,結(jié)果才為true。

          findFirst方法:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("趙六"421)
              );
              Optional<User> optional = userList.stream()
                  .sorted(Comparator.comparing(User::getAge))
                  .findFirst();
              User user = optional.get();
              System.out.println(user);
          }

          該程序段對(duì)Stream中的User對(duì)象按年齡進(jìn)行升序,并使用findFirst方法獲取第一個(gè)User對(duì)象,注意這里的返回值是Optional,這也是JDK1.8的新特性,它是用來避免頻繁出現(xiàn)的空指針異常的,這個(gè)我們后面會(huì)介紹。

          findAny方法:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("趙六"421)
              );
              Optional<User> optional = userList.stream()
                  .filter((u) -> u.getAge().equals(20))
                  .findAny();
              User user = optional.get();
              System.out.println(user);
          }

          這里首先使用filter過濾出年齡為20的用戶,此時(shí)張三和李四都符合條件,而findAny方法便會(huì)從這兩個(gè)對(duì)象中隨機(jī)選擇一個(gè)返回,然而這種情況它只會(huì)一直返回姓名為張三的User對(duì)象,因?yàn)楫?dāng)前的Stream是串行流,我們需要獲取并行流才能實(shí)現(xiàn)隨機(jī)獲取的效果:

          // parallelStream()方法獲取并行流
          Optional<User> optional = userList.parallelStream()
              .filter((u) -> u.getAge().equals(20))
              .findAny();

          count方法:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("趙六"421)
              );
              long count = userList.stream()
                  .count();
              System.out.println(count);
          }

          獲取當(dāng)前Stream中的元素個(gè)數(shù),結(jié)果為4。

          max方法:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("趙六"421)
              );
              Optional<User> optional = userList.stream()
                  .max(Comparator.comparing(User::getAge));
              User user = optional.get();
              System.out.println(user);
          }

          該程序段獲取的是Stream中年齡最大的User對(duì)象。

          min方法:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("趙六"421)
              );
              Optional<User> optional = userList.stream()
                  .min(Comparator.comparing(User::getAge));
              User user = optional.get();
              System.out.println(user);
          }

          將調(diào)用方法換為min,則它將獲取Stream中年齡最小的User對(duì)象。


          Stream的收集操作

          @Test
          public void test() {
              List<Integer> list = Arrays.asList(316759);
              Integer result = list.stream()
                  .reduce(0, Integer::sum);
              System.out.println(result);
          }

          該程序段中使用了一個(gè)新的方法:reduce ,它的作用是將流中的元素按規(guī)則反復(fù)地結(jié)合起來,得到一個(gè)值,比如這里的結(jié)果就是將流中的元素全部相加得到和,我們先將方法引用展開再說說其原理:

          @Test
          public void test() {
              List<Integer> list = Arrays.asList(316759);
              Integer result = list.stream()
                  .reduce(0, (i, j) -> i + j);
              System.out.println(result);
          }

          reduce會(huì)將第一個(gè)參數(shù)值0作為起始值,其第一次迭代便是將0作為變量i的值,并從流中取出第一個(gè)元素作為變量j的值,所以第一次的執(zhí)行結(jié)果為:0 + 1 = 1 ,并將該結(jié)果作為變量i的值,從流中取出第二個(gè)元素作為變量j的值進(jìn)行下一次運(yùn)算,結(jié)果為:1 + 3 = 4 ,以此類推,最終得到總和。

          Stream提供了collect方法用于收集操作,它可以將那些經(jīng)過過濾、映射、排序等操作后的Stream數(shù)據(jù)重新收集起來,成為一個(gè)新的集合數(shù)據(jù),代碼如下:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("趙六"421)
              );
              List<String> nameList = userList.stream()
                  .map(User::getName)
                  .collect(Collectors.toList());
              System.out.println(nameList);
          }

          此時(shí)我們便將所有User對(duì)象的姓名取出,并收集到了一個(gè)新的集合中。Collectors類提供了非常多的靜態(tài)方法供我們更方便地進(jìn)行收集,toList、toSet、toMap、toCollection等等,比如將數(shù)據(jù)收集成Set集合:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("王五"251),
                  new User("趙六"421)
              );
              Set<String> nameSet = userList.stream()
                  .map(User::getName)
                  .collect(Collectors.toSet());
              System.out.println(nameSet);
          }

          則這樣便取出了所有User對(duì)象的姓名且不重復(fù)。若是想將數(shù)據(jù)收集成HashSet呢?Collectors類中并未提供toHashSet方法,但我們可以通過toCollection方法實(shí)現(xiàn):

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("王五"251),
                  new User("趙六"421)
              );
              Set<String> nameSet = userList.stream()
                  .map(User::getName)
                  .collect(Collectors.toCollection(HashSet::new));
              System.out.println(nameSet);
          }

          toCollection方法需要接收一個(gè)供給型接口,通過構(gòu)造器引用創(chuàng)建HashSet對(duì)象即可。

          Collectors還提供了一些類似SQL的聚合操作,比如求元素個(gè)數(shù):

          Long count = userList.stream()
              .map(User::getName).count();

          求平均值:

          Double avgAge = userList.stream()
              .collect(Collectors.averagingDouble(User::getAge));

          求總和:

          Integer sumAge = userList.stream()
              .collect(Collectors.summingInt(User::getAge));

          最大值:

          Optional<User> optional = userList.stream()
              .collect(Collectors.maxBy(Comparator.comparingInt(User::getAge)));
          User user = optional.get();

          最小值:

          Optional<User> optional = userList.stream()
              .collect(Collectors.minBy(Comparator.comparingInt(User::getAge)));
          User user = optional.get();

          它甚至能夠?qū)崿F(xiàn)分組,比如按照年齡分組:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("王五"251),
                  new User("趙六"421)
              );
              Map<Integer, List<User>> map = userList.stream()
                  .collect(Collectors.groupingBy(User::getAge));
              System.out.println(map);
          }

          運(yùn)行結(jié)果:

          {20=[User{name='張三', age=20, sex=0}, User{name='李四', age=20, sex=0}], 25=[User{name='王五', age=25, sex=1}, User{name='王五', age=25, sex=1}], 42=[User{name='趙六', age=42, sex=1}]}

          還能夠進(jìn)行多級(jí)分組,比如先按照年齡分組,再按照性別分組:

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("王五"251),
                  new User("趙六"421)
              );
              Map<Integer, Map<String, List<User>>> map = userList.stream()
                  .collect(Collectors.groupingBy(User::getAge, Collectors.groupingBy((u) -> {
                      if (u.getSex() == 0) {
                          return "男";
                      } else {
                          return "女";
                      }
                  })));
              System.out.println(map);
          }

          運(yùn)行結(jié)果:

          {20={男=[User{name='張三', age=20, sex=0}, User{name='李四', age=20, sex=0}]}, 25={女=[User{name='王五', age=25, sex=1}, User{name='王五', age=25, sex=1}]}, 42={女=[User{name='趙六', age=42, sex=1}]}}

          按年齡進(jìn)行分區(qū):

          @Test
          public void test() {
              List<User> userList = Arrays.asList(
                  new User("張三"200),
                  new User("李四"200),
                  new User("王五"251),
                  new User("王五"251),
                  new User("趙六"421)
              );
              Map<Boolean, List<User>> map = userList.stream()
                  .collect(Collectors.partitioningBy((u) -> u.getAge() > 20));
              System.out.println(map);
          }

          運(yùn)行結(jié)果:

          {false=[User{name='張三', age=20, sex=0}, User{name='李四', age=20, sex=0}], true=[User{name='王五', age=25, sex=1}, User{name='王五', age=25, sex=1}, User{name='趙六', age=42, sex=1}]}

          它會(huì)按照年齡大于20和小于等于20的規(guī)則將User對(duì)象分為兩組。

           

          Optional


          Optional類是一個(gè)容器類,代表一個(gè)值存在或者不存在,JDK1.8中使用Optional來更好地表示一個(gè)null值,可以大大地避免空指針異常的產(chǎn)生。

          該類下有一些常用的方法:

          • of(T t):創(chuàng)建Optional實(shí)例
          • empty():創(chuàng)建一個(gè)空的Optional實(shí)例
          • ofNullable(T t):若t不為null,創(chuàng)建Optional實(shí)例,否則創(chuàng)建空的Optional實(shí)例
          • isPresent():判斷是否包含值
          • orElse(T t):如果調(diào)用對(duì)象包含值,返回該值,否則返回t
          • orElseGet(Suppiler s):如果調(diào)用對(duì)象包含值,返回該值,否則返回s獲取的值
          • map(Function f):如果有值對(duì)其處理,并返回處理后的Optional,否則返回空的Optional實(shí)例
          @Test
          public void test() {
              Optional<User> optional = Optional.of(new User());
              User user = optional.get();
              System.out.println(user);
          }

          需要注意,of方法不能接收一個(gè)為null的參數(shù),否則仍然會(huì)報(bào)空指針異常,此時(shí)應(yīng)該使用_ofNullable_方法來處理:

          @Test
          public void test() {
              Optional<Object> optional = Optional.ofNullable(null);
              System.out.println(optional.get());
          }

          它不會(huì)報(bào)空指針異常,隨之而來的是NoSuchElementException異常:

          java.util.NoSuchElementException: No value present

           at java.util.Optional.get(Optional.java:135)
              at com.wwj.test.TestDemo.test(TestDemo.java:15)

          其它方法的使用非常簡(jiǎn)單,這里直接列出:

          @Test
          public void test() {
              Optional<User> optional = Optional.of(new User("張三"200));
              // 獲取值
              User user = optional.get();
              // 判斷是否有值,結(jié)果為true
              boolean flag = optional.isPresent();
              // 若optional中有值,則返回該值,否則返回orElse新創(chuàng)建的值
              User user1 = optional.orElse(new User("李四"220));
              // 該方法與orElse功能相同,區(qū)別在于該方法的參數(shù)是一個(gè)供給型接口,通過接口實(shí)現(xiàn)可以編寫更加復(fù)雜的功能
              User user2 = optional.orElseGet(() -> new User("李四"220));
              // 若Optional中有值,則返回處理后的結(jié)果,否則返回一個(gè)Optional.empty實(shí)例
              Optional<String> str = optional.map(User::getName);
              // 該方法與map方法功能相同,區(qū)別在于該方法的返回值必須是Optional類型
              Optional<String> str2 = optional.flatMap((u) -> Optional.of(u.getName()));
          }

          接口可以有默認(rèn)方法和靜態(tài)方法了

          public class TestDemo {

              @Test
              public void test() {
                  TestInterface testInterface = new TestInterface();
                  int result = testInterface.calc(12);
                  System.out.println(result);
              }
          }

          interface MyInterface {
              default int calc(int x, int y) {
                  return x + y;
              }
          }

          class TestInterface implements MyInterface {

          }

          JDK1.8以后,接口中可以提供已實(shí)現(xiàn)的默認(rèn)方法了,此時(shí)類在實(shí)現(xiàn)該接口時(shí)就無需實(shí)現(xiàn)默認(rèn)方法了,但有這么一種情況:

          public class TestDemo {

              @Test
              public void test() {
                  TestClass testClass = new TestClass();
                  int result = testClass.calc(12);
                  System.out.println(result);
              }
          }

          class TestClass extends MyClass implements MyInterface{

          }

          interface MyInterface {
              default int calc(int x, int y) {
                  return x + y;
              }
          }

          class MyClass {
              public int calc(int x, int y) {
                  return x - y;
              }
          }

          TestClass類分別繼承了MyClass類和實(shí)現(xiàn)了MyInterface接口,然而MyClass類和MyInterface接口中都有一個(gè)相同的方法,此時(shí)調(diào)用TestClass對(duì)象的calc方法,執(zhí)行的究竟是誰的方法呢?

          事實(shí)上,JDK1.8規(guī)定,接口的默認(rèn)方法遵循 類優(yōu)先 的原則,即:一個(gè)接口中定義了一個(gè)默認(rèn)方法,而另外一個(gè)父類或者接口中又定義了一個(gè)相同的方法,那么它會(huì)優(yōu)先選擇父類中的方法,而忽略掉接口中的默認(rèn)方法。

          若是兩個(gè)接口中都有一個(gè)相同的默認(rèn)方法,則某個(gè)類在同時(shí)實(shí)現(xiàn)這兩個(gè)接口的時(shí)候便會(huì)發(fā)生沖突,此時(shí)實(shí)現(xiàn)類就必須覆蓋方法來解決沖突:

          class TestClass implements MyInterface,MyInterfac2{

              @Override
              public int calc(int x, int y) {
                  return x + y;
              }
          }

          interface MyInterface {
              default int calc(int x, int y) {
                  return x + y;
              }
          }

          interface MyInterfac2 {
              default int calc(int x, int y) {
                  return x + y;
              }
          }

          除了默認(rèn)方法,JDK1.8中還可以擁有靜態(tài)方法:

          public class TestDemo {

              @Test
              public void test() {
                  MyInterface.show();
              }
          }

          interface MyInterface {
              static void show() {
                  System.out.println("Hello!");
              }
          }

          全新的日期時(shí)間API


          傳統(tǒng)的日期時(shí)間API大部分已經(jīng)過期,比如Date類中的方法,而且方法比較難用,傳參復(fù)雜,多線程環(huán)境下還會(huì)有安全問題:

          @Test
          public void test() throws ExecutionException, InterruptedException {
              SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
              Callable<Date> callable = () -> format.parse("2021-04-11");
              ExecutorService pool = Executors.newFixedThreadPool(10);
              List<Future<Date>> result = new ArrayList<>();
              for (int i = 0; i < 10; i++) {
                  result.add(pool.submit(callable));
              }
              for (Future<Date> dateFuture : result) {
                  System.out.println(dateFuture.get());
              }
          }

          運(yùn)行結(jié)果:

          Sun Apr 11 00:00:00 CST 2021

              java.util.concurrent.ExecutionException: java.lang.NumberFormatException: multiple points
              at com.wwj.test.TestDemo.test(TestDemo.java:23)

          當(dāng)然了,解決這一線程安全問題的方法有很多,在每個(gè)線程中都新創(chuàng)建SimpleDateFormat對(duì)象:

          Callable<Date> callable = () -> new SimpleDateFormat("yyyy-MM-dd").parse("2021-04-11");

          或者使用ThreadLocal類:

          public class DateFormatThreadLocal {

              private static final ThreadLocal<DateFormat> format = new ThreadLocal<DateFormat>() {
                  @Override
                  protected DateFormat initialValue() {
                      return new SimpleDateFormat("yyyy-MM-dd");
                  }
              };

              public static Date convert(String date) throws ParseException {
                  return format.get().parse(date);
              }
          }

          然后使用該類進(jìn)行日期格式化:

          Callable<Date> callable = () -> DateFormatThreadLocal.convert("2021-04-11");

          而JDK1.8提供了一套全新的日期時(shí)間API用于簡(jiǎn)化對(duì)日期時(shí)間的處理,先來看一個(gè)簡(jiǎn)單示例:

          @Test
          public void test() throws ExecutionException, InterruptedException {
              DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE;
              LocalDate date = LocalDate.parse("2021-04-11", formatter);
              System.out.println(date);
          }

          通過DateTimeFormatter可以指定轉(zhuǎn)換日期的格式,它內(nèi)置了非常多的日期格式:若是這里沒有自己需要的日期格式,也可以自定義:

          DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
          LocalDate date = LocalDate.parse("20210411", formatter);
          System.out.println(date);

          因?yàn)檫@些新的日期時(shí)間API都是被final修飾的,所以它們天生就是線程安全的。

          先來介紹三個(gè)提供日期時(shí)間的API:

          • LocalDate
          • LocalTime
          • LocalDateTime

          它們的用法完全相同,所以這里只介紹LocalDate類的使用:

          @Test
          public void test() {
              // 獲取當(dāng)前日期的LocalDate實(shí)例
              LocalDate date = LocalDate.now();
              System.out.println(date);
              // 獲取指定日期的LocalDate實(shí)例
              LocalDate date2 = LocalDate.of(2021410);
              System.out.println(date2);
              System.out.println("------------");
              System.out.println("一天后的日期:" + date.plusDays(1));
              System.out.println("一星期后的日期:" + date.plusWeeks(1));
              System.out.println("一個(gè)月后的日期:" + date.plusMonths(1));
              System.out.println("一年后的日期:" + date.plusYears(1));
              System.out.println("------------");
              System.out.println("一天前的日期:" + date.minusDays(1));
              System.out.println("一星期前的日期:" + date.minusWeeks(1));
              System.out.println("一個(gè)月前的日期:" + date.minusMonths(1));
              System.out.println("一年前的日期:" + date.minusYears(1));
              System.out.println("------------");
              System.out.println("獲取日:" + date.getDayOfMonth());
              System.out.println("獲取月:" + date.getMonthValue());
              System.out.println("獲取年:" + date.getYear());
          }

          運(yùn)行結(jié)果:

          2021-04-11
          2021-04-10
          ------------
          一天后的日期:2021-04-12
          一星期后的日期:2021-04-18
          一個(gè)月后的日期:2021-05-11
          一年后的日期:2022-04-11
          ------------
          一天前的日期:2021-04-10
          一星期前的日期:2021-04-04
          一個(gè)月前的日期:2021-03-11
          一年前的日期:2020-04-11
          ------------
          獲取日:11
          獲取月:4
          獲取年:2021

          若是想獲取時(shí)間戳,可以使用Instant類:

          @Test
          public void test() throws InterruptedException {
              Instant instant = Instant.now();
              // 默認(rèn)是UTC時(shí)區(qū)
              System.out.println(instant);
              // 向后偏移8個(gè)小時(shí)即為我國的時(shí)間
              OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));
              System.out.println(offsetDateTime);
              // 獲取時(shí)間戳的毫秒數(shù)
              System.out.println(instant.toEpochMilli());
              // 獲取向后偏移1個(gè)小時(shí)的時(shí)間戳,時(shí)間戳默認(rèn)以1970年1月1日00:00:00開始
              Instant instant1 = Instant.ofEpochSecond(60 * 60);
              System.out.println(instant1);
              // 計(jì)算兩個(gè)時(shí)間戳之間的間隔
              Instant instant2 = Instant.now();
              Thread.sleep(1000);
              Instant instant3 = Instant.now();
              Duration duration = Duration.between(instant2, instant3);
              System.out.println(duration);
              // 計(jì)算兩個(gè)時(shí)間之間的間隔
              LocalTime localTime = LocalTime.now();
              Thread.sleep(1000);
              LocalTime localTime2 = LocalTime.now();
              Duration duration1 = Duration.between(localTime, localTime2);
              System.out.println(duration1);
              // 計(jì)算兩個(gè)日期之間的間隔
              LocalDate localDate = LocalDate.now();
              LocalDate localDate1 = LocalDate.of(202149);
              Period period = Period.between(localDate, localDate1);
              System.out.println(period);
          }

          運(yùn)行結(jié)果:

          2021-04-11T09:53:33.836Z
          2021-04-11T17:53:33.836+08:00
          1618134813836
          1970-01-01T01:00:00Z
          PT1.012S
          PT1.005S
          P-2D

          為了更加方便地操作日期時(shí)間,JDK1.8還為我們提供了時(shí)間校正器 TemporalAdjuster ,通過它我們就能夠非常方便地操縱日期和時(shí)間:

          @Test
          public void test() {
              LocalDateTime dateTime = LocalDateTime.now();
              System.out.println(dateTime);
              // 計(jì)算下一個(gè)周日
              LocalDateTime nextSunday = dateTime.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
              System.out.println(nextSunday);
              // 自定義日期標(biāo)識(shí),計(jì)算下一個(gè)工作日
              LocalDateTime localDateTime = dateTime.with((d) -> {
                  LocalDateTime ldt = (LocalDateTime) d;
                  DayOfWeek dayOfWeek = ldt.getDayOfWeek();
                  if (dayOfWeek.equals(DayOfWeek.FRIDAY)) {
                      return ldt.plusDays(3); // 如果是周五,則三天后就是工作日
                  } else if (dayOfWeek.equals(DayOfWeek.SATURDAY)) {
                      return ldt.plusDays(2); // 如果是周六,則兩天后就是工作日
                  } else {
                      return ldt.plusDays(1); // 其它時(shí)間都加一天
                  }
              });
              System.out.println(localDateTime);
          }

          最后是時(shí)間格式化操作,JDK1.8中使用DateTimeFormatter類來格式化日期和時(shí)間:

          @Test
          public void test() {
              // 使用內(nèi)置的日期格式
              DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE;
              LocalDateTime localDateTime = LocalDateTime.now();
              String strDate = localDateTime.format(formatter);
              System.out.println(strDate);
              // 使用自定義格式
              DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH時(shí)mm分ss秒");
              String strDate2 = localDateTime.format(formatter2);
              System.out.println(strDate2);
              // 將日期時(shí)間字符串解析成LocalDateTime實(shí)例
              LocalDateTime parse = localDateTime.parse(strDate2, formatter2);
              System.out.println(parse);
          }

          還有時(shí)區(qū)的操作:

          @Test
          public void test() {
              // 獲取所有支持的時(shí)區(qū)
              Set<String> set = ZoneId.getAvailableZoneIds();
              // 在獲取日期時(shí)設(shè)置時(shí)區(qū)
              LocalDateTime localDateTime = LocalDateTime.now(ZoneId.of("America/Cuiaba"));
              System.out.println(localDateTime);
              LocalDateTime localDateTime2 = LocalDateTime.now();
              // 獲取日期后設(shè)置時(shí)區(qū)
              ZonedDateTime zonedDateTime = localDateTime2.atZone(ZoneId.of("Asia/Shanghai"));
              System.out.println(zonedDateTime);
          }

          運(yùn)行結(jié)果:

          2021-04-11T08:00:44.308
          2021-04-11T20:00:44.334+08:00[Asia/Shanghai]

          以上便是有關(guān)Java8新特性的全部?jī)?nèi)容。

          本文作者:汪偉俊 為Java技術(shù)迷專欄作者 投稿,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載。

          1、網(wǎng)曝IDEA2020.3.2,自動(dòng)注釋類和方法注釋模板配置

          2、牛逼!IntelliJ IDEA居然支持視頻聊天了~速來嘗鮮!快來沖一波

          3、微信這些表情包,我可能再也不敢用了!你還用嗎?

          4、知名國產(chǎn)網(wǎng)盤翻車?清空免費(fèi)用戶文件后,又開始清理付費(fèi)用戶資源

          5、Chrome新功能曝光:你訪問的敏感網(wǎng)站可以自動(dòng)隱藏起來

          6、萬萬沒想到,“紅孩兒”竟然做了程序員,還是CTO!

          7、徒手?jǐn)]一個(gè)Spring Boot中的starter,解密自動(dòng)化配置,超級(jí)棒!

          點(diǎn)分享

          點(diǎn)收藏

          點(diǎn)點(diǎn)贊

          點(diǎn)在看

          瀏覽 62
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  看操屄| 日韩人妻无码一区 | 日韩va亚洲va欧美va高清 | 自拍偷拍在 | 欧美怡红院 |