Java 新特性總結——簡單實用


lambda 表達式

簡介

在我看來 lambda 表達式就是簡化了以前的一大堆繁瑣的操作,讓我們代碼看起來更加簡潔,讓以前五六行甚至更多行的代碼只需要兩三行就能解決,但是對於 Java 初學者可能不是特別友好,可能一下子理解不過來該代碼想表達什么。

lambda 表達式是一段可以傳遞的代碼,因此它可以被執行一次或多次

lambda 表達式的語法

我們先來看看老版本的排序字符串的辦法,這里我們不按照字典序,而按照字符串的大小來排序

Comparator<String> comparator = new Comparator<String>() {
    @Override
    public int compare(String o1, String o2) {
        return Integer.compare(o1.length(),o2.length());
    }
};
List<String> list = Arrays.asList("aaaa", "aaa", "aa", "a", "aaaaa");
Collections.sort(list,comparator);// [a, aa, aaa, aaaa, aaaaa]

老版本的排序我們會先創建一個自定義比較器,然后按照比較器的規則進行排序。

現在我們來看看 lambda 表達式如何來實現的

List<String> list = Arrays.asList("aaaa", "aaa", "aa", "a", "aaaaa");
Collections.sort(list,(String o1,String o2)->{
    return Integer.compare(o1.length(),o2.length());
});//[a, aa, aaa, aaaa, aaaaa]

可以看到,代碼濃縮了不少,但是可讀性沒有原來好,原來需要先創建一個比較器然后將比較器傳到 Collections 工具類進行排序,典型的面向對象編程,但是 lambda 表達式確是將代碼傳進去然后直接進行比較。如果你以為這樣就是最簡便的,那你就錯了,有更簡便的。

List<String> list = Arrays.asList("aaaa", "aaa", "aa", "a", "aaaaa");
Collections.sort(list,(String o1,String o2)
                 ->Integer.compare(o1.length(),o2.length()));

如果返回值只有一行,可以省略大括號和 return 關鍵字。

List<String> list = Arrays.asList("aaaa", "aaa", "aa", "a", "aaaaa");
Collections.sort(list,(o1,o2)->Integer.compare(o1.length(),o2.length()));

如果是帶泛型的容器,參數的類型可以省略,JVM 會自己進行上下文判斷出類型。

我們可以看到從原來的那么多行代碼濃縮成了一行,看着清爽了很多,但是可讀性卻沒有原來那么友好了。

變量作用域

  • 訪問局部變量

    可以在 lambda 表達式中訪問外部的局部變量

    int number = 10;
    Converter<String,Integer> converter = num->Integer.valueOf(num + number);
    System.out.println(converter.convert("123"));//12310
    

    在匿名內部類中外部的局部變量必須聲明為 final。而我們這里不需要。

    但是要注意的是這的 number 不能被后面的代碼修改,否則編譯不通過,也就是具有隱性的 final 語義。

  • 訪問 字段和靜態變量

    我們對 lambda 表達式中的實例字段和靜態字段變量都有讀寫訪問權限。

    class Lambda4 {
        static int outerStaticNum;
        int outerNum;
    
        void testScopes() {
            Converter<Integer, String> stringConverter1 = (from) -> {
                outerNum = 23;
                return String.valueOf(from);
            };
    
            Converter<Integer, String> stringConverter2 = (from) -> {
                outerStaticNum = 72;
                return String.valueOf(from);
            };
        }
    }
    
  • 無法在 lambda 表達式中訪問默認接口方法。

函數式接口

在 Java 中有許多已有的接口都選哦封裝成代碼塊,比如 Runnable 或者 Comparator 。 lambda 表達式與這些接口是像后兼容的。

對於只包含一個抽象方法的接口,但是可以有多個非抽象方法,(非抽象方法也是 java 8 新特性,我們后面會講到),我們可以通過 lambda 表達式來創建該接口的對象。這種接口被稱為函數式接口。

Java 8 新增加了一種特殊的注解 @FunctionalInterface,該接口會自動判斷你的接口中是否只有一個抽象方法,如果多於一個抽象方法就會報錯。

現在我們來自定義一個函數式接口:

@FunctionalInterface
interface Converter<F,T>{
    T convert(F num);
}
// 將數字形式的字符串轉化成整型
Converter<String,Integer> converter = (num -> Integer.valueOf(num));
System.out.println(converter.convert("123").getClass());//class java.lang.Integer

現在來解釋下該代碼,在該代碼中我們的函數式接口中定義了一個方法,該方法能夠實現傳入一個 F 類型的參數,我們可以對這個類型的參數進行各種處理,最后返回一個 T 類型的結果。在這里我只是簡單的將傳進來的 string 轉成了 integer。這里的 F 與 T 都是泛型類型,可以為任何實體類。

java 8 幫我們實現了很多函數式接口,大部分都不需要我們自己寫,這些接口在 java.util.function 包 里,可以自行進行查閱。

上面的代碼可以寫的更加簡單:

Converter<String,Integer> converter = Integer::valueOf;
System.out.println(converter.convert("123").getClass());//class java.lang.Integer

java 8 可以通過 ** : : **來傳遞方法或者構造函數的引用。上面的演示了如果引用靜態方法,引用對象方法也相差不大,只是需要聲明一個對象:

class Demo{
    public Integer demo(String num){
        return Integer.valueOf(num);
    }
}
public class Main {
    public static void main(String[] args) {
        Demo demo = new Demo();
        Converter<String,Integer> converter = demo::demo;
        System.out.println(converter.convert("123").getClass());
        //class java.lang.Integer
    }
}

內置函數式接口

  • Predicates

    Predicate 接口是只有一個參數的返回布爾類型值的 斷言型 接口。該接口包含多種默認方法來將 Predicate 組合成其他復雜的邏輯(比如:與,或,非)

    @FunctionalInterface
    public interface Predicate<T> {
        // 該方法是接受一個傳入類型,返回一個布爾值.此方法應用於判斷.
        boolean test(T t);
        ....
    }
    
  • Functions

    Function 接口接受一個參數並生成結果。默認方法可用於將多個函數鏈接在一起(compose, andThen)

    @FunctionalInterface
    public interface Function<T, R> {
        
        //將Function對象應用到輸入的參數上,然后返回計算結果。
        R apply(T t);
        ...
    }
    
  • Suppliers

    Supplier 接口產生給定泛型類型的結果。 與 Function 接口不同,Supplier 接口不接受參數。

  • Consumers

    Consumer 接口表示要對單個輸入參數執行的操作。

  • Comparators

    Comparator 是老Java中的經典接口, Java 8在此之上添加了多種默認方法

默認方法

前面已經有寫地方提到了接口的默認方法,這里對其做下介紹。接口的默認方法也是 java 8 新出的功能。能夠通過使用 default 關鍵字向接口添加非抽象方法實現。

interface Formula{

    double calculate(int a);

    default double sqrt(int a) {
        return Math.sqrt(a);
    }

}

Formula 接口中除了抽象方法計算接口公式還定義了默認方法 sqrt。 實現該接口的類只需要實現抽象方法 calculate。 默認方法sqrt 可以直接使用。當然你也可以直接通過接口創建對象,然后實現接口中的默認方法就可以了,我們通過代碼演示一下這種方式。

public class Main {
  public static void main(String[] args) {
    // TODO 通過匿名內部類方式訪問接口
    Formula formula = new Formula() {
        @Override
        public double calculate(int a) {
            return sqrt(a * 100);
        }
    };
    System.out.println(formula.calculate(100));     // 100.0
    System.out.println(formula.sqrt(16));           // 4.0
  }
}

formula 是作為匿名對象實現的。該代碼非常容易理解,6行代碼實現了計算 sqrt(a * 100)

不管是抽象類還是接口,都可以通過匿名內部類的方式訪問。不能通過抽象類或者接口直接創建對象。對於上面通過匿名內部類方式訪問接口,我們可以這樣理解:一個內部類實現了接口里的抽象方法並且返回一個內部類對象,之后我們讓接口的引用來指向這個對象。

Stream(流)

Stream 是在 java.util 下的。Stream 表示能應用在一組元素上一次執行的操作序列。Stream 操作分為中間操作或者最終操作兩種,最終操作返回一特定類型的計算結果,而中間操作返回 Stream 本身,這樣我們就可以將多個操作依次串起來。Stream 的創建需要指定一個數據源,比如 java.util.Collection 的子類:List 或者 Set。Map 不支持。Stream 的操作可以串行執行或者並行執行。

當我們使用 Stream 時,我們將通過三個階段來建立一個操作流水線。

  1. 創建一個 Stream。
  2. 在一個或多個步驟中,指定將初始 Stream 轉換成為另一個 Stream 的中間操作。
  3. 使用一個終止操作來產生一個結果。該操作會強制它之前的延遲操作立即執行。

在這之后 stream 就不會再被使用了。

創建 stream

通過 Java 8 在 Collection 接口中新提娜佳的 stram 方法,可以將任何集合轉化為一個 Stream。如果我們面對的是一個數組,也可以用靜態的 Stream.of 方法將其轉化為一個 Stream。

@Test
public void test1(){
    List<String> stringList = new ArrayList<>();
    stringList.add("ddd2");
    stringList.add("aaa2");
    stringList.add("bbb1");
    stringList.add("aaa1");
    stringList.add("bbb3");
    stringList.add("ccc");
    stringList.add("bbb2");
    stringList.add("ddd1");
    Stream<String> stream = stringList.stream();
    //Stream<String> stringStream = stringList.parallelStream();
}

我們可以通過 Collection.stream() 或者 Collection.parallelStream() 來創建一個Stream。

Filter(過濾)

過濾通過一個predicate接口來過濾並只保留符合條件的元素,該操作屬於中間操作,所以我們可以在過濾后的結果來應用其他Stream操作。(比如forEach)。forEach需要一個函數來對過濾后的元素依次執行。forEach是一個最終操作,所以我們不能在forEach之后來執行其他Stream操作。

stringList
    .stream()
    .filter(s->s.startsWith("a"))
    .forEach(System.out::println);

forEach 是為 Lambda 而設計的,保持了最緊湊的風格。而且 Lambda 表達式本身是可以重用的,非常方便。

Sorted(排序)

排序是一個中間操作,返回的是排序好的 Stream 。如果我們不指定一個自定義的 Comparator 則會使用默認排序。

stringList
    .stream()
    .sorted((o1,o2)->Integer.compare(o1.length(),o2.length()))
    .forEach(System.out::println);

需要注意的是,排序只創建了一個排列好后的Stream,而不會影響原有的數據源,排序之后原數據stringCollection是不會被修改的。

Map(映射)

中間操作 map 會將元素根據指定的 Function 接口來依次將元素轉成另外的對象。

map返回的 Stream 類型是根據我們 map 傳遞進去的函數的返回值決定的。

stringList
    .stream()
    .map(String::toUpperCase)
    .sorted((o1,o2)->Integer.compare(o1.length(),o2.length()))
    .forEach(System.out::println);

Match(匹配)

Stream提供了多種匹配操作,允許檢測指定的Predicate是否匹配整個Stream。所有的匹配操作都是 最終操作 ,並返回一個 boolean 類型的值。

boolean anyStartsWithA =
    stringList
    .stream()
    .anyMatch((s) -> s.startsWith("a"));
System.out.println(anyStartsWithA);      // true

boolean allStartsWithA =
    stringList
    .stream()
    .allMatch((s) -> s.startsWith("a"));
System.out.println(allStartsWithA);      // false

boolean noneStartsWithZ =
    stringList
    .stream()
    .noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ);      // true

Count(計數)

計數是一個 最終操作,返回Stream中元素的個數,返回值類型是 long

long count = stringList
    .stream()
    .map(String::toUpperCase)
    .sorted((o1, o2) -> Integer.compare(o1.length(), o2.length()))
    .count();
System.out.println(count);

Parallel Stream(並行流)

Stream有串行和並行兩種,串行Stream上的操作是在一個線程中依次完成,而並行Stream則是在多個線程上同時執行。

下面使用串行流和並行流為一個大容器進行排序,比較兩者性能。

串行排序

@Test
public void test1(){
    int max = 1000000;
    List<String> list = new ArrayList<>(max);
    for (int i = 0; i < max; i++) {
        UUID uuid = UUID.randomUUID();
        list.add(uuid.toString());
    }
    long startTime = System.nanoTime();
    long count = list.stream().sorted().count();
    System.out.println(count);
    long endTime = System.nanoTime();
    long millis = TimeUnit.NANOSECONDS.toMillis(endTime-startTime);
    System.out.println(millis);
    //1000000
    //877
}

並行排序

@Test
public void test2(){
    int max = 1000000;
    List<String> list = new ArrayList<>(max);
    for (int i = 0; i < max; i++) {
        UUID uuid = UUID.randomUUID();
        list.add(uuid.toString());
    }
    long startTime = System.nanoTime();
    long count = list.parallelStream().sorted().count();
    System.out.println(count);
    long endTime = System.nanoTime();
    long millis = TimeUnit.NANOSECONDS.toMillis(endTime-startTime);
    System.out.println(millis);
}
//1000000
//512

可以明顯看出在大數據量的情況下並行排序比串行來的快。但是小數據量的話卻是串行排序比較快,原因是並行需要涉及到上下文切換。

Collector 和 Collectors

Collector 是專門用來作為 Stream 的 collect 方法的參數的。而 Collectors 是作為生產具體 Collector 的工具類。

  • toList():將流構造成 list

    List<String> collect = list.stream().collect(Collectors.toList());
    
  • toSet():將流構造成set

    Set<String> set = list.stream().collect(Collectors.toSet());
    Set<String> treeSet = list.stream().collect(Collectors.toCollection(TreeSet::new));
    
  • joining():拼接流中所有字符串

    String collect = list.stream().collect(Collectors.joining());
    String collect = list.stream().collect(Collectors.joining(";"));
    
  • toMap():將流轉成 map

    Map<String, String> collect = list
                    .stream()
                    .collect(Collectors
                    .toMap(e -> "key" + e, e -> "v" + e,(a,b)->b,HashMap::new));
    

    上面的 e -> "key" + e 定義了 map 的 key 的生成規則,e -> "v" + e 定義了 map 的 value 的生成規則,(a,b)->b 表示沖突的解決方案,如果鍵 a 和 鍵 b 沖突了則該鍵鍵值取 b 的,HashMap::new 定義了生成的 map 為 hashmap。

Map 新方法

Map 雖然不支持 Stream 但是我們可以通過 map.keySet().stream(),map.values().stream()map.entrySet().stream() 來通過過去鍵、值的集合再轉換成流進行處理。

Java 8 中 map 新方法:

  • putIfAbsent(key, value)//有則不加,無則加
    
  • map.forEach((key, value) -> System.out.println(value));//循環打印
    
  • map.computeIfPresent(3, (num, val) -> val + num);//當key 存在則執行后面方法
    
  • map.computeIfAbsent(23, num -> "val" + num);//當key 不存在時執行后面方法
    
  • map.getOrDefault(42, 1);//有則獲取,無則置 1
    
  • map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
    //如果鍵名不存在則插入,否則則對原鍵對應的值做合並操作並重新插入到map中。
    

新的日期與時間 API

  • LocalTime(本地時間)

    LocalTime 定義了一個沒有時區信息的時間

    方法 描述
    now,of 這些靜態方法可以根據當前時間或指定的年、月、日來創建一個 LocalTime 對象
    getHour,getMinute,getSecond,getNano 獲得當前 LocalTime 的小時、分鍾、秒鍾及微妙值
    isBefore,isAfter 比較兩個LocalTime
  • LocalDate(本地日期)

    LocalDate 表示了一個確切的日期,比如 2014-03-11。該對象值是不可變的,用起來和LocalTime基本一致。

    方法 描述
    now,of 這些靜態方法可以根據當前時間或指定的年、月、日來創建一個LocalDate對象
    getDayOfMonth 獲取月份天數(在 1~ 31 之間)
    getDayOfYear 獲取年份天數(在1~366之間)
    getMonth,getMonthValue 獲得月份,或者為一個 Month 枚舉的值,或者是 1 ~ 12 之間的一個數字
    getYear 獲取年份
    isBefore,isAfter 比較兩個LocalDate

上面這些方法是比較常用的,其余的可以自行查閱。

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM