7.3萬字肝爆Java8新特性,我不信你能看完!(建議收藏)

冰河發表於2022-01-05

大家好,我是冰河~~

說實話,肝這篇文章花了我一個月的時間,關於Java8的新特性全在這兒了,建議先收藏後閱讀

Java8有哪些新特性?

簡單來說,Java8新特性如下所示:

  • Lambda表示式
  • 函式式介面
  • 方法引用與構造器引用
  • Stream API
  • 介面的預設方法與靜態方法
  • 新時間日期API
  • 其他新特性

其中,引用最廣泛的新特性是Lambda表示式和Stream API。

Java8有哪些優點?

簡單來說Java8優點如下所示。

  • 速度更快
  • 程式碼更少(增加了新的語法Lambda表示式)
  • 強大的Stream API
  • 便於並行
  • 最大化減少空指標異常Optional

Lambda表示式

什麼是Lambda表示式?

Lambda表示式是一個匿名函式,我們可以這樣理解Lambda表示式:Lambda是一段可以傳遞的程式碼(能夠做到將程式碼像資料一樣進行傳遞)。使用Lambda表示式能夠寫出更加簡潔、靈活的程式碼。並且,使用Lambda表示式能夠使Java的語言表達能力得到提升。

匿名內部類

在介紹如何使用Lambda表示式之前,我們先來看看匿名內部類,例如,我們使用匿名內部類比較兩個Integer型別資料的大小。

Comparator<Integer> com = new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return Integer.compare(o1, o2);
    }
};

在上述程式碼中,我們使用匿名內部類實現了比較兩個Integer型別資料的大小。

接下來,我們就可以將上述匿名內部類的例項作為引數,傳遞到其他方法中了,如下所示。

 TreeSet<Integer> treeSet = new TreeSet<>(com);

完整的程式碼如下所示。

@Test
public void test1(){
    Comparator<Integer> com = new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return Integer.compare(o1, o2);
        }
    };
    TreeSet<Integer> treeSet = new TreeSet<>(com);
}

我們分析下上述程式碼,在整個匿名內部類中,實際上真正有用的就是下面一行程式碼。

 return Integer.compare(o1, o2);

其他的程式碼本質上都是“冗餘”的。但是為了書寫上面的一行程式碼,我們不得不在匿名內部類中書寫更多的程式碼。

Lambda表示式

如果使用Lambda表示式完成兩個Integer型別資料的比較,我們該如何實現呢?

Comparator<Integer> com = (x, y) -> Integer.compare(x, y);

看到沒,使用Lambda表示式,我們只需要使用一行程式碼就能夠實現兩個Integer型別資料的比較。

我們也可以將Lambda表示式傳遞到TreeSet的構造方法中,如下所示。

 TreeSet<Integer> treeSet = new TreeSet<>((x, y) -> Integer.compare(x, y));

直觀的感受就是使用Lambda表示式一行程式碼就能搞定匿名內部類多行程式碼的功能。

看到這,不少讀者會問:我使用匿名內部類的方式實現比較兩個整數型別的資料大小並不複雜啊!我為啥還要學習一種新的語法呢?

其實,我想說的是:上面我們們只是簡單的列舉了一個示例,接下來,我們們寫一個稍微複雜一點的例子,來對比下使用匿名內部類與Lambda表示式哪種方式更加簡潔。

對比常規方法和Lambda表示式

例如,現在有這樣一個需求:獲取當前公司中員工年齡大於30歲的員工資訊。

首先,我們需要建立一個Employee實體類來儲存員工的資訊。

@Data
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
    private static final long serialVersionUID = -9079722457749166858L;
    private String name;
    private Integer age;
    private Double salary;
}

在Employee中,我們簡單儲存了員工的姓名、年齡和薪資。

接下來,我們建立一個儲存多個員工的List集合,如下所示。

protected List<Employee> employees = Arrays.asList(
        new Employee("張三", 18, 9999.99),
        new Employee("李四", 38, 5555.55),
        new Employee("王五", 60, 6666.66),
        new Employee("趙六", 16, 7777.77),
        new Employee("田七", 18, 3333.33)
);

1.常規遍歷集合

我們先使用常規遍歷集合的方式來查詢年齡大於等於30的員工資訊。

public List<Employee> filterEmployeesByAge(List<Employee> list){
    List<Employee> employees = new ArrayList<>();
    for(Employee e : list){
        if(e.getAge() >= 30){
            employees.add(e);
        }
    }
    return employees;
}

接下來,我們測試一下上面的方法。

@Test
public void test3(){
    List<Employee> employeeList = filterEmployeesByAge(this.employees);
    for (Employee e : employeeList){
        System.out.println(e);
    }
}

執行test3方法,輸出資訊如下所示。

Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)

總體來說,查詢年齡大於或者等於30的員工資訊,使用常規遍歷集合的方式稍顯複雜了。

例如,需求發生了變化:獲取當前公司中員工工資大於或者等於5000的員工資訊。

此時,我們不得不再次建立一個按照工資過濾的方法。

public List<Employee> filterEmployeesBySalary(List<Employee> list){
    List<Employee> employees = new ArrayList<>();
    for(Employee e : list){
        if(e.getSalary() >= 5000){
            employees.add(e);
        }
    }
    return employees;
}

對比filterEmployeesByAge()方法和filterEmployeesBySalary方法後,我們發現,大部分的方法體是相同的,只是for迴圈中對於條件的判斷不同。

如果此時我們再來一個需求,查詢當前公司中年齡小於或者等於20的員工資訊,那我們又要建立一個過濾方法了。 看來使用常規方法是真的不方便啊!

這裡,問大家一個問題:對於這種常規方法最好的優化方式是啥?相信有不少小夥伴會說:將公用的方法抽取出來。沒錯,將公用的方法抽取出來是一種優化方式,但它不是最好的方式。最好的方式是啥?那就是使用 設計模式 啊!設計模式可是無數前輩不斷實踐而總結出的設計原則和設計模式。大家可以檢視《設計模式彙總——你需要掌握的23種設計模式都在這兒了!》一文來學習設計模式專題。

2.使用設計模式優化程式碼

如何使用設計模式來優化上面的方法呢,大家繼續往下看,對於設計模式不熟悉的同學可以先根據《設計模式彙總——你需要掌握的23種設計模式都在這兒了!》來學習。

我們先定義一個泛型介面MyPredicate,對傳遞過來的資料進行過濾,符合規則返回true,不符合規則返回false。

public interface MyPredicate<T> {

    /**
     * 對傳遞過來的T型別的資料進行過濾
     * 符合規則返回true,不符合規則返回false
     */
    boolean filter(T t);
}

接下來,我們建立MyPredicate介面的實現類FilterEmployeeByAge來過濾年齡大於或者等於30的員工資訊。

public class FilterEmployeeByAge implements MyPredicate<Employee> {
    @Override
    public boolean filter(Employee employee) {
        return employee.getAge() >= 30;
    }
}

我們定義一個過濾員工資訊的方法,此時傳遞的引數不僅有員工的資訊集合,同時還有一個我們定義的介面例項,在遍歷員工集合時將符合過濾條件的員工資訊返回。

//優化方式一
public List<Employee> filterEmployee(List<Employee> list, MyPredicate<Employee> myPredicate){
    List<Employee> employees = new ArrayList<>();
    for(Employee e : list){
        if(myPredicate.filter(e)){
            employees.add(e);
        }
    }
    return employees;
}

接下來,我們寫一個測試方法來測試優化後的程式碼。

@Test
public void test4(){
    List<Employee> employeeList = this.filterEmployee(this.employees, new FilterEmployeeByAge());
    for (Employee e : employeeList){
        System.out.println(e);
    }
}

執行test4()方法,輸出的結果資訊如下所示。

Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)

寫到這裡,大家是否有一種豁然開朗的感覺呢?

沒錯,這就是設計模式的魅力,對於設計模式不熟悉的小夥伴,一定要參照《設計模式彙總——你需要掌握的23種設計模式都在這兒了!》來學習。

我們繼續獲取當前公司中工資大於或者等於5000的員工資訊,此時,我們只需要建立一個FilterEmployeeBySalary類實現MyPredicate介面,如下所示。

public class FilterEmployeeBySalary implements MyPredicate<Employee>{
    @Override
    public boolean filter(Employee employee) {
        return employee.getSalary() >= 5000;
    }
}

接下來,就可以直接寫測試方法了,在測試方法中繼續呼叫filterEmployee(List<Employee> list, MyPredicate<Employee> myPredicate)方法。

@Test
public void test5(){
    List<Employee> employeeList = this.filterEmployee(this.employees, new FilterEmployeeBySalary());
    for (Employee e : employeeList){
        System.out.println(e);
    }
}

執行test5方法,輸出的結果資訊如下所示。

Employee(name=張三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
Employee(name=趙六, age=16, salary=7777.77)

可以看到,使用設計模式對程式碼進行優化後,無論過濾員工資訊的需求如何變化,我們只需要建立MyPredicate介面的實現類來實現具體的過濾邏輯,然後在測試方法中呼叫filterEmployee(List<Employee> list, MyPredicate<Employee> myPredicate)方法將員工集合和過濾規則傳入即可。

這裡,問大家一個問題:上面優化程式碼使用的設計模式是哪種設計模式呢?如果是你,你會想到使用設計模式來優化自己的程式碼嗎?小夥伴們自己先思考一下到底使用的設計模式是什麼?文末我會給出答案!

使用設計模式優化程式碼也有不好的地方:每次定義一個過濾策略的時候,我們都要單獨建立一個過濾類!!

3.匿名內部類

那使用匿名內部類是不是能夠優化我們書寫的程式碼呢,接下來,我們就使用匿名內部類來實現對員工資訊的過濾。先來看過濾年齡大於或者等於30的員工資訊。

@Test
public void test6(){
    List<Employee> employeeList = this.filterEmployee(this.employees, new MyPredicate<Employee>() {
        @Override
        public boolean filter(Employee employee) {
            return employee.getAge() >= 30;
        }
    });
    for (Employee e : employeeList){
        System.out.println(e);
    }
}

執行test6方法,輸出如下結果資訊。

Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)

再實現過濾工資大於或者等於5000的員工資訊,如下所示。

@Test
public void test7(){
    List<Employee> employeeList = this.filterEmployee(this.employees, new MyPredicate<Employee>() {
        @Override
        public boolean filter(Employee employee) {
            return employee.getSalary() >= 5000;
        }
    });
    for (Employee e : employeeList){
        System.out.println(e);
    }
}

執行test7方法,輸出如下結果資訊。

Employee(name=張三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
Employee(name=趙六, age=16, salary=7777.77)

匿名內部類看起來比常規遍歷集合的方法要簡單些,並且將使用設計模式優化程式碼時,每次建立一個類來實現過濾規則寫到了匿名內部類中,使得程式碼進一步簡化了。

但是,使用匿名內部類程式碼的可讀性不高,並且冗餘程式碼也比較多!!

那還有沒有更加簡化的方式呢?

4.重頭戲:Lambda表示式

在使用Lambda表示式時,我們還是要呼叫之前寫的filterEmployee(List<Employee> list, MyPredicate<Employee> myPredicate)方法。

注意看,獲取年齡大於或者等於30的員工資訊。

@Test
public void test8(){
    filterEmployee(this.employees, (e) -> e.getAge() >= 30).forEach(System.out::println);
}

看到沒,使用Lambda表示式只需要一行程式碼就完成了員工資訊的過濾和輸出。是不是很6呢。

執行test8方法,輸出如下的結果資訊。

Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)

再來看使用Lambda表示式來獲取工資大於或者等於5000的員工資訊,如下所示。

@Test
public void test9(){
    filterEmployee(this.employees, (e) -> e.getSalary() >= 5000).forEach(System.out::println);
}

沒錯,使用Lambda表示式,又是一行程式碼就搞定了!!

執行test9方法,輸出如下的結果資訊。

Employee(name=張三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
Employee(name=趙六, age=16, salary=7777.77)

另外,使用Lambda表示式時,只需要給出需要過濾的集合,我們就能夠實現從集合中過濾指定規則的元素,並輸出結果資訊。

5.重頭戲:Stream API

使用Lambda表示式結合Stream API,只要給出相應的集合,我們就可以完成對集合的各種過濾並輸出結果資訊。

例如,此時只要有一個employees集合,我們使用Lambda表示式來獲取工資大於或者等於5000的員工資訊。

@Test
public void test10(){
    employees.stream().filter((e) -> e.getSalary() >= 5000).forEach(System.out::println);
}

沒錯,只給出一個集合,使用Lambda表示式和Stream API,一行程式碼就能夠過濾出想要的元素並進行輸出。

執行test10方法,輸出如下的結果資訊。

Employee(name=張三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
Employee(name=趙六, age=16, salary=7777.77)

如果我們只想要獲取前兩個員工的資訊呢?其實也很簡單,如下所示。

@Test
public void test11(){
    employees.stream().filter((e) -> e.getSalary() >= 5000).limit(2).forEach(System.out::println);
}

可以看到,我們在程式碼中新增了limit(2)來限制只獲取兩個員工資訊。執行test11方法,輸出如下的結果資訊。

Employee(name=張三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)

使用Lambda表示式和Stream API也可以獲取指定的欄位資訊,例如獲取工資大於或者等於5000的員工姓名。

@Test
public void test12(){
    employees.stream().filter((e) -> e.getSalary() >= 5000).map(Employee::getName).forEach(System.out::println);
}

可以看到,使用map過濾出了工資大於或者等於5000的員工姓名。執行test12方法,輸出如下的結果資訊。

張三
李四
王五
趙六

是不是很簡單呢?

最後,給出文中使用的設計模式:策略模式。

匿名類到Lambda表示式

我們先來看看從匿名類如何轉換到Lambda表示式呢?

這裡,我們可以使用兩個示例來說明如何從匿名內部類轉換為Lambda表示式。

  • 匿名內部類到Lambda表示式

使用匿名內部類如下所示。

Runnable r = new Runnable(){
    @Override
    public void run(){
        System.out.println("Hello Lambda");
    }
}

轉化為Lambda表示式如下所示。

Runnable r = () -> System.out.println("Hello Lambda");
  • 匿名內部類作為引數傳遞到Lambda表示式作為引數傳遞

使用匿名內部類作為引數如下所示。

TreeSet<Integer> ts = new TreeSet<>(new Comparator<Integer>(){
    @Override
    public int compare(Integer o1, Integer o2){
        return Integer.compare(o1, o2);
    }
});

使用Lambda表示式作為引數如下所示。

TreeSet<Integer> ts = new TreeSet<>(
    (o1, o2) -> Integer.compare(o1, o2);
);

從直觀上看,Lambda表示式要比常規的語法簡潔的多。

Lambda表示式的語法

Lambda表示式在Java語言中引入了 “->” 操作符, “->” 操作符被稱為Lambda表示式的操作符或者箭頭操作符,它將Lambda表示式分為兩部分:

  • 左側部分指定了Lambda表示式需要的所有引數。

Lambda表示式本質上是對介面的實現,Lambda表示式的引數列表本質上對應著介面中方法的引數列表。

  • 右側部分指定了Lambda體,即Lambda表示式要執行的功能。

Lambda體本質上就是介面方法具體實現的功能。

我們可以將Lambda表示式的語法總結如下。

1.語法格式一:無參,無返回值,Lambda體只有一條語句

Runnable r = () -> System.out.println("Hello Lambda");

具體示例如下所示。

@Test
public void test1(){
    Runnable r = () -> System.out.println("Hello Lambda");
    new Thread(r).start();
}

2.語法格式二:Lambda表示式需要一個引數,並且無返回值

Consumer<String> func = (s) -> System.out.println(s);

具體示例如下所示。

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

3.語法格式三:Lambda只需要一個引數時,引數的小括號可以省略

Consumer<String> func = s -> System.out.println(s);

具體示例如下所示。

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

4.語法格式四:Lambda需要兩個引數,並且有返回值

BinaryOperator<Integer> bo = (a, b) -> {
    System.out.println("函式式介面");
    return a + b;
};

具體示例如下所示。

@Test
public void test4(){
    Comparator<Integer> comparator = (x, y) -> {
        System.out.println("函式式介面");
        return Integer.compare(x, y);
    };
}

5.語法格式五:當Lambda體只有一條語句時,return和大括號可以省略

BinaryOperator<Integer> bo = (a, b) -> a + b;

具體示例如下所示。

@Test
public void test5(){
    Comparator<Integer> comparator = (x, y) ->  Integer.compare(x, y);
}

6.語法格式六:Lambda表示式的引數列表的資料型別可以省略不寫,因為JVM編譯器能夠通過上下文推斷出資料型別,這就是“型別推斷”

BinaryOperator<Integer> bo = (Integer a, Integer b) -> {
    return a + b;
};

等同於

BinaryOperator<Integer> bo = (a, b) -> {
    return a + b;
};

上述 Lambda 表示式中的引數型別都是由編譯器推斷得出的。 Lambda 表示式中無需指定型別,程式依然可以編譯,這是因為 javac 根據程式的上下文,在後臺推斷出了引數的型別。 Lambda 表示式的型別依賴於上下文環境,是由編譯器推斷出來的。這就是所謂的“型別推斷”。

函式式介面

Lambda表示式需要函式式介面的支援,所以,我們有必要來說說什麼是函式式介面。

只包含一個抽象方法的介面,稱為函式式介面。

可以通過 Lambda 表示式來建立該介面的物件。(若 Lambda表示式丟擲一個受檢異常,那麼該異常需要在目標介面的抽象方法上進行宣告)。

可以在任意函式式介面上使用 @FunctionalInterface 註解,這樣做可以檢查它是否是一個函式式介面,同時 javadoc 也會包含一條宣告,說明這個介面是一個函式式介面。

我們可以自定義函式式介面,並使用Lambda表示式來實現相應的功能。

例如,使用函式式介面和Lambda表示式實現對字串的處理功能。

首先,我們定義一個函式式介面MyFunc,如下所示。

@FunctionalInterface
public interface MyFunc <T> {
    public T getValue(T t);
}

接下來,我們定義一個操作字串的方法,其中引數為MyFunc介面例項和需要轉換的字串。

public String handlerString(MyFunc<String> myFunc, String str){
    return myFunc.getValue(str);
}

接下來,我們對自定義的函式式介面進行測試,此時我們傳遞的函式式介面的引數為Lambda表示式,並且將字串轉化為大寫。

@Test
public void test6(){
    String str = handlerString((s) -> s.toUpperCase(), "binghe");
    System.out.println(str);
}

執行test6方法,得出的結果資訊如下所示。

BINGHE

我們也可以擷取字串的某一部分,如下所示。

@Test
public void test7(){
    String str = handlerString((s) -> s.substring(0,4), "binghe");
    System.out.println(str);
}

執行test7方法,得出的結果資訊如下所示。

bing

可以看到,我們可以通過handlerString(MyFunc<String> myFunc, String str)方法結合Lambda表示式對字串進行任意操作。

注意:作為引數傳遞 Lambda 表示式:為了將 Lambda 表示式作為引數傳遞,接收Lambda 表示式的引數型別必須是與該 Lambda 表示式相容的函式式介面的型別 。

Lambda表示式典型案例

案例一

需求

呼叫Collections.sort()方法,通過定製排序比較兩個Employee(先比較年齡,年齡相同按姓名比較),使用Lambda表示式作為引數傳遞。

實現

這裡,我們先建立一個Employee類,為了滿足需求,我們在Employee類中定義了姓名、年齡和工資三個欄位,如下所示。

@Data
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
    private static final long serialVersionUID = -9079722457749166858L;
    private String name;
    private Integer age;
    private Double salary;
}

接下來,我們在TestLambda類中定義一個成員變數employees,employees變數是一個List集合,儲存了Employee的一個列表,如下所示。

protected List<Employee> employees = Arrays.asList(
    new Employee("張三", 18, 9999.99),
    new Employee("李四", 38, 5555.55),
    new Employee("王五", 60, 6666.66),
    new Employee("趙六", 8, 7777.77),
    new Employee("田七", 58, 3333.33)
);

前期的準備工作完成了,接下來,我們就可以實現具體的業務邏輯了。

@Test
public void test1(){
    Collections.sort(employees, (e1, e2) -> {
        if(e1.getAge() == e2.getAge()){
            return e1.getName().compareTo(e2.getName());
        }
        return Integer.compare(e1.getAge(), e2.getAge());
    });
    employees.stream().forEach(System.out::println);
}

上述程式碼比較簡單,我就不贅述具體邏輯了。執行test1方法,得出的結果資訊如下所示。

Employee(name=趙六, age=8, salary=7777.77)
Employee(name=張三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)
Employee(name=田七, age=58, salary=3333.33)
Employee(name=王五, age=60, salary=6666.66)

如果想倒敘輸出如何處理呢,只需要在將return Integer.compare(e1.getAge(), e2.getAge());修改成-return Integer.compare(e1.getAge(), e2.getAge());即可,如下所示。

@Test
public void test1(){
    Collections.sort(employees, (e1, e2) -> {
        if(e1.getAge() == e2.getAge()){
            return e1.getName().compareTo(e2.getName());
        }
        return -Integer.compare(e1.getAge(), e2.getAge());
    });
    employees.stream().forEach(System.out::println);
}

再次執行test1方法,得出的結果資訊如下所示。

Employee(name=王五, age=60, salary=6666.66)
Employee(name=田七, age=58, salary=3333.33)
Employee(name=李四, age=38, salary=5555.55)
Employee(name=張三, age=18, salary=9999.99)
Employee(name=趙六, age=8, salary=7777.77)

結果符合我們的需求。

案例二

需求

1.宣告函式式介面,介面中宣告抽象方法public String getValue(String str);

2.宣告類TestLambda,類中編寫方法使用介面作為引數,將一個字串轉換為大寫,並作為方法的返回值。

3.再將一個字串的第2個和第4個索引位置進行擷取子串。

實現

首先,建立一個函式式介面MyFunction,在MyFunction介面上加上註解@FunctionalInterface標識介面是一個函式式介面。如下所示。

@FunctionalInterface
public interface MyFunction {
    public String getValue(String str);
}

在TestLambda類中宣告stringHandler方法,引數分別為待處理的字串和函式式介面的例項,方法中的邏輯就是呼叫函式式介面的方法來處理字串,如下所示。

public String stringHandler(String str, MyFunction myFunction){
    return myFunction.getValue(str);
}

接下來,我們實現將一個字串轉換為大寫的邏輯,如下所示。

@Test
public void test2(){
    String value = stringHandler("binghe", (s) -> s.toUpperCase());
    System.out.println(value);
}

執行test2方法,得出如下的結果資訊。

BINGHE

我們再來實現字串擷取的操作,如下所示。

@Test
public void test3(){
    String value = stringHandler("binghe", (s) -> s.substring(1, 3));
    System.out.println(value);
}

注意:需求中是按照第2個和第4個索引位置進行擷取子串,字串的下標是從0開始的,所以這裡擷取字串時使用的是substring(1, 3),而不是substring(2, 4),這也是很多小夥伴容易犯的錯誤。

另外,使用上述Lambda表示式形式,可以實現字串的任意處理,並返回處理後的新字串。

執行test3方法,結果如下所示。

in

案例三

需求

1.宣告一個帶兩個泛型的函式式介面,泛型型別為<T, R>,其中,T作為引數的型別,R作為返回值的型別。

2.介面中宣告物件的抽象方法。

3.在TestLambda類中宣告方法。使用介面作為引數計算兩個long型引數的和。

4.再就按兩個long型引數的乘積。

實現

首先,我們按照需求定義函式式介面MyFunc,如下所示。

@FunctionalInterface
public interface MyFunc<T, R> {

    R getValue(T t1, T t2);
}

接下來,我們在TestLambda類中建立一個處理兩個long型資料的方法,如下所示。

public void operate(Long num1, Long num2, MyFunc<Long, Long> myFunc){
    System.out.println(myFunc.getValue(num1, num2));
}

我們可以使用下面的方法來完成兩個long型引數的和。

@Test
public void test4(){
    operate(100L, 200L, (x, y) -> x + y);
}

執行test4方法,結果如下所示。

300

實現兩個long型資料的乘積,也很簡單。

@Test
public void test5(){
    operate(100L, 200L, (x, y) -> x * y);
}

執行test5方法,結果如下所示。

20000

看到這裡,我相信很多小夥伴已經對Lambda表示式有了更深層次的理解。只要多多練習,就能夠更好的掌握Lambda表示式的精髓。

函式式介面總覽

這裡,我使用表格的形式來簡單說明下Java8中提供的函式式介面。

四大核心函式式介面總覽

首先,我們來看四大核心函式式介面,如下所示。

函式式介面引數型別返回型別使用場景
Consumer<T>消費型介面Tvoid對型別為T的物件應用操作,介面定義的方法:void accept(T t)
Supplier<T>供給型介面T返回型別為T的物件,介面定義的方法:T get()
Function<T, R>函式式介面TR對型別為T的物件應用操作,並R型別的返回結果。介面定義的方法:R apply(T t)
Predicate<T>斷言型介面Tboolean確定型別為T的物件是否滿足約束條件,並返回boolean型別的資料。介面定義的方法:boolean test(T t)

其他函式介面總覽

除了四大核心函式介面外,Java8還提供了一些其他的函式式介面。

函式式介面引數型別返回型別使用場景
BiFunction(T, U, R)T, UR對型別為T,U的引數應用操作,返回R型別的結果。介面定義的方法:R apply(T t, U u)
UnaryOperator<T>(Function子介面)TT對型別為T的物件進行一 元運算, 並返回T型別的 結果。 包含方法為 T apply(T t)
BinaryOperator<T> (BiFunction 子介面)T, TT對型別為T的物件進行二 元運算, 並返回T型別的 結果。 包含方法為 T apply(T t1, T t2)
BiConsumer<T, U>T, Uvoid對型別為T, U 引數應用 操作。 包含方法為 void accept(T t, U u)
ToIntFunction<T>Tint計算int值的函式
ToLongFunction<T>Tlong計算long值的函式
ToDoubleFunction<T>Tdouble計算double值的函式
IntFunction<R>intR引數為int 型別的函式
LongFunction<R>longR引數為 long型別的函式
DoubleFunction<R>doubleR引數為double型別的函式

四大核心函式式介面

Consumer介面

1.介面說明

Consumer介面是消費性介面,無返回值。Java8中對Consumer的定義如下所示。

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);
    
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

2.使用示例

public void handlerConsumer(Integer number, Consumer<Integer> consumer){
    consumer.accept(number);
}

@Test
public void test1(){
    this.handlerConsumer(10000, (i) -> System.out.println(i));
}

Supplier介面

1.介面說明

Supplier介面是供給型介面,有返回值,Java8中對Supplier介面的定義如下所示。

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

2.使用示例

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

@Test
public void test2(){
    List<Integer> numberList = this.getNumberList(10, () -> new Random().nextInt(100));
    numberList.stream().forEach(System.out::println);
}

Function介面

1.介面說明

Function介面是函式型介面,有返回值,Java8中對Function介面的定義如下所示。

@FunctionalInterface
public interface Function<T, R> {
    
    R apply(T t);
    
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

2.使用示例

public String handlerString(String str, Function<String, String> func){
    return func.apply(str);
}

@Test
public void test3(){
    String str = this.handlerString("binghe", (s) -> s.toUpperCase());
    System.out.println(str);
}

Predicate介面

1.介面說明

Predicate介面是斷言型介面,返回值型別為boolean,Java8中對Predicate介面的定義如下所示。

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

2.使用示例

public List<String> filterString(List<String> list, Predicate<String> predicate){
    List<String> strList = new ArrayList<>();
    for(String str : list){
        if(predicate.test(str)){
            strList.add(str);
        }
    }
    return strList;
}

@Test
public void test4(){
    List<String> list = Arrays.asList("Hello", "Lambda", "binghe", "lyz", "World");
    List<String> strList = this.filterString(list, (s) -> s.length() >= 5);
    strList.stream().forEach(System.out::println);
}

注意:只要我們學會了Java8中四大核心函式式介面的用法,其他函式式介面我們也就知道如何使用了!

Java7與Java8中的HashMap

  • JDK7 HashMap結構為陣列+連結串列(發生元素碰撞時,會將新元素新增到連結串列開頭)
  • JDK8 HashMap結構為陣列+連結串列+紅黑樹(發生元素碰撞時,會將新元素新增到連結串列末尾,當HashMap總容量大於等於64,並且某個連結串列的大小大於等於8,會將連結串列轉化為紅黑樹(注意:紅黑樹是二叉樹的一種))

JDK8 HashMap重排序

如果刪除了HashMap中紅黑樹的某個元素導致元素重排序時,不需要計算待重排序的元素的HashCode碼,只需要將當前元素放到(HashMap總長度+當前元素在HashMap中的位置)的位置即可。

篩選與切片

  • filter——接收 Lambda , 從流中排除某些元素。
  • limit——截斷流,使其元素不超過給定數量。
  • skip(n) —— 跳過元素,返回一個扔掉了前 n 個元素的流。若流中元素不足 n 個,則返回一個空流。與 limit(n) 互補
  • distinct——篩選,通過流所生成元素的 hashCode() 和 equals() 去除重複元素

中間操作

  • map——接收 Lambda , 將元素轉換成其他形式或提取資訊。接收一個函式作為引數,該函式會被應用到每個元素上,並將其對映成一個新的元素。
  • flatMap——接收一個函式作為引數,將流中的每個值都換成另一個流,然後把所有流連線成一個流
  • sorted()——自然排序
  • sorted(Comparator com)——定製排序

終止操作

  • allMatch——檢查是否匹配所有元素
  • anyMatch——檢查是否至少匹配一個元素
  • noneMatch——檢查是否沒有匹配的元素
  • findFirst——返回第一個元素
  • findAny——返回當前流中的任意元素
  • count——返回流中元素的總個數
  • max——返回流中最大值
  • min——返回流中最小值

歸約

  • reduce(T identity, BinaryOperator) / reduce(BinaryOperator) ——可以將流中元素反覆結合起來,得到一個值。
  • collect——將流轉換為其他形式。接收一個 Collector介面的實現,用於給Stream中元素做彙總的方法

注意:流進行了終止操作後,不能再次使用

Optional 容器類

用於儘量避免空指標異常

  • Optional.of(T t) : 建立一個 Optional 例項
  • Optional.empty() : 建立一個空的 Optional 例項
  • Optional.ofNullable(T t):若 t 不為 null,建立 Optional 例項,否則建立空例項
  • isPresent() : 判斷是否包含值
  • orElse(T t) : 如果呼叫物件包含值,返回該值,否則返回t
  • orElseGet(Supplier s) :如果呼叫物件包含值,返回該值,否則返回 s 獲取的值
  • map(Function f): 如果有值對其處理,並返回處理後的Optional,否則返回 Optional.empty()
  • flatMap(Function mapper):與 map 類似,要求返回值必須是Optional

方法引用與構造器引用

方法引用

當要傳遞給Lambda體的操作,已經有實現的方法了,可以使用方法引用!這裡需要注意的是:實現抽象方法的引數列表,必須與方法引用方法的引數列表保持一致!

那麼什麼是方法引用呢?方法引用就是操作符“::”將方法名和物件或類的名字分隔開來。

有如下三種使用情況:

  • 物件::例項方法
  • 類::靜態方法
  • 類::例項方法

這裡,我們可以列舉幾個示例。

例如:

(x) -> System.out.println(x);

等同於:

System.out::println

例如:

BinaryOperator<Double> bo = (x, y) -> Math.pow(x, y);

等同於

BinaryOperator<Double> bo = Math::pow;

例如:

compare((x, y) -> x.equals(y), "binghe", "binghe")

等同於

compare(String::equals, "binghe", "binghe")

注意: 當需要引用方法的第一個引數是呼叫物件,並且第二個引數是需要引用方法的第二個引數(或無引數)時: ClassName::methodName 。

構造器引用

格式如下所示:

ClassName::new

與函式式介面相結合,自動與函式式介面中方法相容。可以把構造器引用賦值給定義的方法,與構造器引數列表要與介面中抽象方法的引數列表一致!

例如:

Function<Integer, MyClass> fun = (n) -> new MyClass(n);

等同於

Function<Integer, MyClass> fun = MyClass::new;

陣列引用

格式如下所示。

type[]::new

例如:

Function<Integer, Integer[]> fun = (n) -> new Integer[n];

等同於

Function<Integer, Integer[]> fun = Integer[]::new;

Java8中的Stream

什麼是Stream?

Java8中有兩大最為重要的改變。第一個是 Lambda 表示式;另外一個則是 Stream API(java.util.stream.*)。

Stream 是 Java8 中處理集合的關鍵抽象概念,它可以指定你希望對集合進行的操作,可以執行非常複雜的查詢、過濾和對映資料等操作。使用Stream API 對集合資料進行操作,就類似於使用 SQL 執行的資料庫查詢。也可以使用 Stream API 來並行執行操作。簡而言之,Stream API 提供了一種高效且易於使用的處理資料的方式

流是資料渠道,用於運算元據源(集合、陣列等)所生成的元素序列。“集合講的是資料,流講的是計算! ”

注意:
① Stream 自己不會儲存元素。
② Stream 不會改變源物件。相反,他們會返回一個持有結果的新Stream。
③ Stream 操作是延遲執行的。這意味著他們會等到需要結果的時候才執行。

Stream操作的三個步驟

  • 建立 Stream

一個資料來源(如: 集合、陣列), 獲取一個流。

  • 中間操作

一箇中間操作鏈,對資料來源的資料進行處理。

  • 終止操作(終端操作)

一個終止操作,執行中間操作鏈,併產生結果 。

如何建立Stream?

Java8 中的 Collection 介面被擴充套件,提供了兩個獲取流的方法:

1.獲取Stream

  • default Stream<E> stream() : 返回一個順序流
  • default Stream<E> parallelStream() : 返回一個並行流

2.由陣列建立Stream

Java8 中的 Arrays 的靜態方法 stream() 可以獲取陣列流:

  • static <T> Stream<T> stream(T[] array): 返回一個流

過載形式,能夠處理對應基本型別的陣列:

  • public static IntStream stream(int[] array)
  • public static LongStream stream(long[] array)
  • public static DoubleStream stream(double[] array)

3.由值建立流

可以使用靜態方法 Stream.of(), 通過顯示值建立一個流。它可以接收任意數量的引數。

  • public static<T> Stream<T> of(T... values) : 返回一個流

4.由函式建立流

由函式建立流可以建立無限流。

可以使用靜態方法 Stream.iterate() 和Stream.generate(), 建立無限流 。

  • 迭代

public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)

  • 生成

public static<T> Stream<T> generate(Supplier<T> s)

Stream的中間操作

多箇中間操作可以連線起來形成一個流水線,除非流水線上觸發終止操作,否則中間操作不會執行任何的處理!而在終止操作時一次性全部處理,稱為“惰性求值”

1.篩選與切片

2.對映

3.排序

Stream 的終止操作

終端操作會從流的流水線生成結果。其結果可以是任何不是流的值,例如: List、 Integer,甚至是 void 。

1.查詢與匹配

2.規約

3.收集

Collector 介面中方法的實現決定了如何對流執行收集操作(如收集到 List、 Set、 Map)。但是 Collectors 實用類提供了很多靜態方法,可以方便地建立常見收集器例項, 具體方法與例項如下表

並行流與序列流

並行流就是把一個內容分成多個資料塊,並用不同的執行緒分別處理每個資料塊的流。

Java 8 中將並行進行了優化,我們可以很容易的對資料進行並行操作。 Stream API 可以宣告性地通過 parallel() 與
sequential() 在並行流與順序流之間進行切換

Fork/Join 框架

1.簡單概述

Fork/Join 框架: 就是在必要的情況下,將一個大任務,進行拆分(fork)成若干個小任務(拆到不可再拆時),再將一個個的小任務運算的結果進行 join 彙總.

2.Fork/Join 框架與傳統執行緒池的區別

採用 “工作竊取”模式(work-stealing):
當執行新的任務時它可以將其拆分分成更小的任務執行,並將小任務加到執行緒佇列中,然後再從一個隨機執行緒的佇列中偷一個並把它放在自己的佇列中。

相對於一般的執行緒池實現,fork/join框架的優勢體現在對其中包含的任務的處理方式上.在一般的執行緒池中,如果一個執行緒正在執行的任務由於某些原因無法繼續執行,那麼該執行緒會處於等待狀態.而在fork/join框架實現中,如果某個子問題由於等待另外一個子問題的完成而無法繼續執行.那麼處理該子問題的執行緒會主動尋找其他尚未執行的子問題來執行.這種方式減少了執行緒的等待時間,提高了效能。

Stream概述

Java8中有兩大最為重要的改變。第一個是 Lambda 表示式;另外一個則是 Stream API(java.util.stream.*)。

Stream 是 Java8 中處理集合的關鍵抽象概念,它可以指定你希望對集合進行的操作,可以執行非常複雜的查詢、過濾和對映資料等操作。使用Stream API 對集合資料進行操作,就類似於使用 SQL 執行的資料庫查詢。也可以使用 Stream API 來並行執行操作。簡而言之,Stream API 提供了一種高效且易於使用的處理資料的方式。

何為Stream?

流(Stream) 到底是什麼呢?

可以這麼理解流:流就是資料渠道,用於運算元據源(集合、陣列等)所生成的元素序列。

“集合講的是資料,流講的是計算! ”

注意:

①Stream 自己不會儲存元素。

②Stream 不會改變源物件。相反,他們會返回一個持有結果的新Stream。

③Stream 操作是延遲執行的。這意味著他們會等到需要結果的時候才執行。

Stream操作步驟

1.建立 Stream

一個資料來源(如: 集合、陣列), 獲取一個流。

2.中間操作

一箇中間操作鏈,對資料來源的資料進行處理。

3.終止操作(終端操作)

一個終止操作,執行中間操作鏈,併產生結果 。

如何建立Stream流?

這裡,建立測試類TestStreamAPI1,所有的操作都是在TestStreamAPI1類中完成的。

(1)通過Collection系列集合提供的stream()方法或者parallelStream()方法來建立Stream。

在Java8中,Collection 介面被擴充套件,提供了兩個獲取流的預設方法,如下所示。

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
    return StreamSupport.stream(spliterator(), true);
}

其中,stream()方法返回一個順序流,parallelStream()方法返回一個並行流。

我們可以使用如下程式碼方式來建立順序流和並行流。

List<String> list = new ArrayList<>();
list.stream();
list.parallelStream();

(2)通過Arrays中的靜態方法stream()獲取陣列流。

Java8 中的 Arrays類的靜態方法 stream() 可以獲取陣列流 ,如下所示。

public static <T> Stream<T> stream(T[] array) {
    return stream(array, 0, array.length);
}

上述程式碼的的作用為:傳入一個泛型陣列,返回這個泛型的Stream流。

除此之外,在Arrays類中還提供了stream()方法的如下過載形式。

public static <T> Stream<T> stream(T[] array) {
    return stream(array, 0, array.length);
}

public static <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive) {
    return StreamSupport.stream(spliterator(array, startInclusive, endExclusive), false);
}

public static IntStream stream(int[] array) {
    return stream(array, 0, array.length);
}

public static IntStream stream(int[] array, int startInclusive, int endExclusive) {
    return StreamSupport.intStream(spliterator(array, startInclusive, endExclusive), false);
}

public static LongStream stream(long[] array) {
    return stream(array, 0, array.length);
}

public static LongStream stream(long[] array, int startInclusive, int endExclusive) {
    return StreamSupport.longStream(spliterator(array, startInclusive, endExclusive), false);
}

public static DoubleStream stream(double[] array) {
    return stream(array, 0, array.length);
}

public static DoubleStream stream(double[] array, int startInclusive, int endExclusive) {
    return StreamSupport.doubleStream(spliterator(array, startInclusive, endExclusive), false);
}

基本上能夠滿足基本將基本型別的陣列轉化為Stream流的操作。

我們可以通過下面的程式碼示例來使用Arrays類的stream()方法來建立Stream流。

Integer[] nums = new Integer[]{1,2,3,4,5,6,7,8,9};
Stream<Integer> numStream = Arrays.stream(nums);

(3)通過Stream類的靜態方法of()獲取陣列流。

可以使用靜態方法 Stream.of(), 通過顯示值建立一個流。它可以接收任意數量的引數。

我們先來看看Stream的of()方法,如下所示。

public static<T> Stream<T> of(T t) {
    return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}
@SafeVarargs
@SuppressWarnings("varargs") 
public static<T> Stream<T> of(T... values) {
    return Arrays.stream(values);
}

可以看到,在Stream類中,提供了兩個of()方法,一個只需要傳入一個泛型引數,一個需要傳入一個可變泛型引數。

我們可以使用下面的程式碼示例來使用of方法建立一個Stream流。

Stream<String> strStream = Stream.of("a", "b", "c");

(4)建立無限流

可以使用靜態方法 Stream.iterate() 和Stream.generate(), 建立無限流。

先來看看Stream類中iterate()方法和generate()方法的原始碼,如下所示。

public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {
    Objects.requireNonNull(f);
    final Iterator<T> iterator = new Iterator<T>() {
        @SuppressWarnings("unchecked")
        T t = (T) Streams.NONE;

        @Override
        public boolean hasNext() {
            return true;
        }

        @Override
        public T next() {
            return t = (t == Streams.NONE) ? seed : f.apply(t);
        }
    };
    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
        iterator,
        Spliterator.ORDERED | Spliterator.IMMUTABLE), false);
}

public static<T> Stream<T> generate(Supplier<T> s) {
    Objects.requireNonNull(s);
    return StreamSupport.stream(
        new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
}

通過原始碼可以看出,iterate()方法主要是使用“迭代”的方式生成無限流,而generate()方法主要是使用“生成”的方式生成無限流。我們可以使用下面的程式碼示例來使用這兩個方法生成Stream流。

  • 迭代
Stream<Integer> intStream = Stream.iterate(0, (x) -> x + 2);
intStream.forEach(System.out::println);

執行上述程式碼,會在終端一直輸出偶數,這種操作會一直持續下去。如果我們只需要輸出10個偶數,該如何操作呢?其實也很簡單,使用Stream物件的limit方法進行限制就可以了,如下所示。

Stream<Integer> intStream = Stream.iterate(0, (x) -> x + 2);
intStream.limit(10).forEach(System.out::println);
  • 生成
Stream.generate(() -> Math.random()).forEach(System.out::println);

上述程式碼同樣會一直輸出隨機數,如果我們只需要輸出5個隨機數,則只需要使用limit()方法進行限制即可。

Stream.generate(() -> Math.random()).limit(5).forEach(System.out::println);

(5)建立空流

在Stream類中提供了一個empty()方法,如下所示。

public static<T> Stream<T> empty() {
    return StreamSupport.stream(Spliterators.<T>emptySpliterator(), false);
}

我們可以使用Stream類的empty()方法來建立一個空Stream流,如下所示。

Stream<String> empty = Stream.empty();

Stream的中間操作

多箇中間操作可以連線起來形成一個流水線,除非流水線上觸發終止操作,否則中間操作不會執行任何的處理!而在終止操作時一次性全部處理,稱為“惰性求值” 。 Stream的中間操作是不會有任何結果資料輸出的。

Stream的中間操作在整體上可以分為:篩選與切片、對映、排序。接下來,我們就分別對這些中間操作進行簡要的說明。

篩選與切片

這裡,我將與篩選和切片有關的操作整理成如下表格。

方法描述
filter(Predicate p)接收Lambda表示式,從流中排除某些元素
distinct()篩選,通過流所生成元素的 hashCode() 和 equals() 去 除重複元素
limit(long maxSize)截斷流,使其元素不超過給定數量
skip(long n)跳過元素,返回一個扔掉了前 n 個元素的流。若流中元素 不足 n 個,則返回一個空流。與 limit(n) 互補

接下來,我們列舉幾個簡單的示例,以便加深理解。

為了更好的測試程式,我先構造了一個物件陣列,如下所示。

protected List<Employee> list = Arrays.asList(
    new Employee("張三", 18, 9999.99),
    new Employee("李四", 38, 5555.55),
    new Employee("王五", 60, 6666.66),
    new Employee("趙六", 8, 7777.77),
    new Employee("田七", 58, 3333.33)
);

其中,Employee類的定義如下所示。

@Data
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
    private static final long serialVersionUID = -9079722457749166858L;
    private String name;
    private Integer age;
    private Double salary;
}

Employee類的定義比較簡單,這裡,我就不贅述了。之後的示例中,我們都是使用的Employee物件的集合進行操作。好了,我們開始具體的操作案例。

1.filter()方法

filter()方法主要是用於接收Lambda表示式,從流中排除某些元素,其在Stream介面中的原始碼如下所示。

Stream<T> filter(Predicate<? super T> predicate);

可以看到,在filter()方法中,需要傳遞Predicate介面的物件,Predicate介面又是個什麼鬼呢?點進去看下原始碼。

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }
    
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }
    
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

可以看到,Predicate是一個函式式介面,其中介面中定義的主要方法為test()方法,test()方法會接收一個泛型物件t,返回一個boolean型別的資料。

看到這裡,相信大家明白了:filter()方法是根據Predicate介面的test()方法的返回結果來過濾資料的,如果test()方法的返回結果為true,符合規則;如果test()方法的返回結果為false,則不符合規則。

這裡,我們可以使用下面的示例來簡單的說明filter()方法的使用方式。

//內部迭代:在此過程中沒有進行過迭代,由Stream api進行迭代
//中間操作:不會執行任何操作
Stream<Person> stream = list.stream().filter((e) -> {
    System.out.println("Stream API 中間操作");
    return e.getAge() > 30;
});

我們,在執行終止語句之後,一邊迭代,一邊列印,而我們並沒有去迭代上面集合,其實這是內部迭代,由Stream API 完成。

下面我們來看看外部迭代,也就是我們人為得迭代。

//外部迭代
Iterator<Person> it = list.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

2.limit()方法

主要作用為:截斷流,使其元素不超過給定數量。

先來看limit方法的定義,如下所示。

Stream<T> limit(long maxSize);

limit()方法在Stream介面中的定義比較簡單,只需要傳入一個long型別的數字即可。

我們可以按照如下所示的程式碼來使用limit()方法。

//過濾之後取2個值
list.stream().filter((e) -> e.getAge() >30 ).limit(2).forEach(System.out :: println);

在這裡,我們可以配合其他得中間操作,並截斷流,使我們可以取得相應個數得元素。而且在上面計算中,只要發現有2條符合條件得元素,則不會繼續往下迭代資料,可以提高效率。

3.skip()方法

跳過元素,返回一個扔掉了前 n 個元素的流。若流中元素 不足 n 個,則返回一個空流。與 limit(n) 互補。

原始碼定義如下所示。

Stream<T> skip(long n);

原始碼定義比較簡單,同樣只需要傳入一個long型別的數字即可。其含義是跳過n個元素。

簡單示例如下所示。

//跳過前2個值
list.stream().skip(2).forEach(System.out :: println);

4.distinct()方法

篩選,通過流所生成元素的 hashCode() 和 equals() 去 除重複元素。

原始碼定義如下所示。

Stream<T> distinct();

旨在對流中的元素進行去重。

我們可以如下面的方式來使用disinct()方法。

list.stream().distinct().forEach(System.out :: println);

這裡有一個需要注意的地方:distinct 需要實體中重寫hashCode()和 equals()方法才可以使用。

對映

關於對映相關的方法如下表所示。

方法描述
map(Function f)接收一個函式作為引數,該函式會被應用到每個元 素上,並將其對映成一個新的元素。
mapToDouble(ToDoubleFunction f)接收一個函式作為引數,該函式會被應用到每個元 素上,產生一個新的 DoubleStream。
mapToInt(ToIntFunction f)接收一個函式作為引數,該函式會被應用到每個元 素上,產生一個新的 IntStream。
mapToLong(ToLongFunction f)接收一個函式作為引數,該函式會被應用到每個元 素上,產生一個新的 LongStream
flatMap(Function f)接收一個函式作為引數,將流中的每個值都換成另 一個流,然後把所有流連線成一個流

1.map()方法

接收一個函式作為引數,該函式會被應用到每個元 素上,並將其對映成一個新的元素。

先來看Java8中Stream介面對於map()方法的宣告,如下所示。

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

我們可以按照如下方式使用map()方法。

//將流中每一個元素都對映到map的函式中,每個元素執行這個函式,再返回
List<String> list = Arrays.asList("aaa", "bbb", "ccc", "ddd");
list.stream().map((e) -> e.toUpperCase()).forEach(System.out::printf);

//獲取Person中的每一個人得名字name,再返回一個集合
List<String> names = this.list.stream().map(Person :: getName).collect(Collectors.toList());

2.flatMap()

接收一個函式作為引數,將流中的每個值都換成另 一個流,然後把所有流連線成一個流。

先來看Java8中Stream介面對於flatMap()方法的宣告,如下所示。

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

我們可以使用如下方式使用flatMap()方法,為了便於大家理解,這裡,我就貼出了測試flatMap()方法的所有程式碼。

/**
     * flatMap —— 接收一個函式作為引數,將流中的每個值都換成一個流,然後把所有流連線成一個流
     */
    @Test
    public void testFlatMap () {
        StreamAPI_Test s = new StreamAPI_Test();
        List<String> list = Arrays.asList("aaa", "bbb", "ccc", "ddd");
        list.stream().flatMap((e) -> s.filterCharacter(e)).forEach(System.out::println);

        //如果使用map則需要這樣寫
        list.stream().map((e) -> s.filterCharacter(e)).forEach((e) -> {
            e.forEach(System.out::println);
        });
    }

    /**
     * 將一個字串轉換為流
     */
    public Stream<Character> filterCharacter(String str){
        List<Character> list = new ArrayList<>();
        for (Character ch : str.toCharArray()) {
            list.add(ch);
        }
        return list.stream();
    }

其實map方法就相當於Collaction的add方法,如果add的是個集合得話就會變成二維陣列,而flatMap 的話就相當於Collaction的addAll方法,引數如果是集合得話,只是將2個集合合併,而不是變成二維陣列。

排序

關於排序相關的方法如下表所示。

方法描述
sorted()產生一個新流,其中按自然順序排序
sorted(Comparator comp)產生一個新流,其中按比較器順序排序

從上述表格可以看出:sorted有兩種方法,一種是不傳任何引數,叫自然排序,還有一種需要傳Comparator 介面引數,叫做定製排序。

先來看Java8中Stream介面對於sorted()方法的宣告,如下所示。

Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

sorted()方法的定義比較簡單,我就不再贅述了。

我們也可以按照如下方式來使用Stream的sorted()方法。

// 自然排序
List<Employee> persons = list.stream().sorted().collect(Collectors.toList());

//定製排序
List<Employee> persons1 = list.stream().sorted((e1, e2) -> {
    if (e1.getAge() == e2.getAge()) {
        return 0;
    } else if (e1.getAge() > e2.getAge()) {
        return 1;
    } else {
        return -1;
    }
}).collect(Collectors.toList());

Stream的終止操作

終端操作會從流的流水線生成結果。其結果可以是任何不是流的值,例如: List、 Integer、Double、String等等,甚至是 void 。

在Java8中,Stream的終止操作可以分為:查詢與匹配、規約和收集。接下來,我們就分別簡單說明下這些終止操作。

查詢與匹配

Stream API中有關查詢與匹配的方法如下表所示。

方法描述
allMatch(Predicate p)檢查是否匹配所有元素
anyMatch(Predicate p)檢查是否至少匹配一個元素
noneMatch(Predicate p)檢查是否沒有匹配所有元素
findFirst()返回第一個元素
findAny()返回當前流中的任意元素
count()返回流中元素總數
max(Comparator c)返回流中最大值
min(Comparator c)返回流中最小值
forEach(Consumer c)內部迭代(使用 Collection 介面需要使用者去做迭代,稱為外部迭代。相反, Stream API 使用內部迭代)

同樣的,我們對每個重要的方法進行簡單的示例說明,這裡,我們首先建立一個Employee類,Employee類的定義如下所示。

@Data
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
    private static final long serialVersionUID = -9079722457749166858L;
    private String name;
    private Integer age;
    private Double salary;
    private Stauts stauts;
    public enum Stauts{
        WORKING,
        SLEEPING,
        VOCATION
    }
}

接下來,我們在測試類中定義一個用於測試的集合employees,如下所示。

protected List<Employee> employees = Arrays.asList(
    new Employee("張三", 18, 9999.99, Employee.Stauts.SLEEPING),
    new Employee("李四", 38, 5555.55, Employee.Stauts.WORKING),
    new Employee("王五", 60, 6666.66, Employee.Stauts.WORKING),
    new Employee("趙六", 8, 7777.77, Employee.Stauts.SLEEPING),
    new Employee("田七", 58, 3333.33, Employee.Stauts.VOCATION)
);

好了,準備工作就緒了。接下來,我們就開始測試Stream的每個終止方法。

1.allMatch()

allMatch()方法表示檢查是否匹配所有元素。其在Stream介面中的定義如下所示。

boolean allMatch(Predicate<? super T> predicate);

我們可以通過類似如下示例來使用allMatch()方法。

boolean match = employees.stream().allMatch((e) -> Employee.Stauts.SLEEPING.equals(e.getStauts()));
System.out.println(match);

注意:使用allMatch()方法時,只有所有的元素都匹配條件時,allMatch()方法才會返回true。

2.anyMatch()方法

anyMatch方法表示檢查是否至少匹配一個元素。其在Stream介面中的定義如下所示。

boolean anyMatch(Predicate<? super T> predicate);

我們可以通過類似如下示例來使用anyMatch()方法。

boolean match = employees.stream().anyMatch((e) -> Employee.Stauts.SLEEPING.equals(e.getStauts()));
System.out.println(match);

注意:使用anyMatch()方法時,只要有任意一個元素符合條件,anyMatch()方法就會返回true。

3.noneMatch()方法

noneMatch()方法表示檢查是否沒有匹配所有元素。其在Stream介面中的定義如下所示。

boolean noneMatch(Predicate<? super T> predicate);

我們可以通過類似如下示例來使用noneMatch()方法。

boolean match = employees.stream().noneMatch((e) -> Employee.Stauts.SLEEPING.equals(e.getStauts()));
System.out.println(match);

注意:使用noneMatch()方法時,只有所有的元素都不符合條件時,noneMatch()方法才會返回true。

4.findFirst()方法

findFirst()方法表示返回第一個元素。其在Stream介面中的定義如下所示。

Optional<T> findFirst();

我們可以通過類似如下示例來使用findFirst()方法。

Optional<Employee> op = employees.stream().sorted((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())).findFirst();
System.out.println(op.get());

5.findAny()方法

findAny()方法表示返回當前流中的任意元素。其在Stream介面中的定義如下所示。

Optional<T> findAny();

我們可以通過類似如下示例來使用findAny()方法。

Optional<Employee> op = employees.stream().filter((e) -> Employee.Stauts.WORKING.equals(e.getStauts())).findFirst();
System.out.println(op.get());

6.count()方法

count()方法表示返回流中元素總數。其在Stream介面中的定義如下所示。

long count();

我們可以通過類似如下示例來使用count()方法。

long count = employees.stream().count();
System.out.println(count);

7.max()方法

max()方法表示返回流中最大值。其在Stream介面中的定義如下所示。

Optional<T> max(Comparator<? super T> comparator);

我們可以通過類似如下示例來使用max()方法。

Optional<Employee> op = employees.stream().max((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));
System.out.println(op.get());

8.min()方法

min()方法表示返回流中最小值。其在Stream介面中的定義如下所示。

Optional<T> min(Comparator<? super T> comparator);

我們可以通過類似如下示例來使用min()方法。

Optional<Double> op = employees.stream().map(Employee::getSalary).min(Double::compare);
System.out.println(op.get());

9.forEach()方法

forEach()方法表示內部迭代(使用 Collection 介面需要使用者去做迭代,稱為外部迭代。相反, Stream API 使用內部迭代)。其在Stream介面內部的定義如下所示。

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

我們可以通過類似如下示例來使用forEach()方法。

employees.stream().forEach(System.out::println);

規約

Stream API中有關規約的方法如下表所示。

方法描述
reduce(T iden, BinaryOperator b)可以將流中元素反覆結合起來,得到一個值。 返回 T
reduce(BinaryOperator b)可以將流中元素反覆結合起來,得到一個值。 返回 Optional<T>

reduce()方法在Stream介面中的定義如下所示。

T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

我們可以通過類似如下示例來使用reduce方法。

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
Integer sum = list.stream().reduce(0, (x, y) -> x + y);
System.out.println(sum);
System.out.println("----------------------------------------");
Optional<Double> op = employees.stream().map(Employee::getSalary).reduce(Double::sum);
System.out.println(op.get());

我們也可以搜尋employees列表中“張”出現的次數。

 Optional<Integer> sum = employees.stream()
   .map(Employee::getName)
   .flatMap(TestStreamAPI1::filterCharacter)
   .map((ch) -> {
    if(ch.equals('六'))
     return 1;
    else
     return 0;
   }).reduce(Integer::sum);
  System.out.println(sum.get());

注意:上述例子使用了硬編碼的方式來累加某個具體值,大家在實際工作中再優化程式碼。

收集

方法描述
collect(Collector c)將流轉換為其他形式。接收一個 Collector介面的實現,用於給Stream中元素做彙總的方法

collect()方法在Stream介面中的定義如下所示。

<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);

<R, A> R collect(Collector<? super T, A, R> collector);

我們可以通過類似如下示例來使用collect方法。

Optional<Double> max = employees.stream()
   .map(Employee::getSalary)
   .collect(Collectors.maxBy(Double::compare));
  System.out.println(max.get());
  Optional<Employee> op = employees.stream()
   .collect(Collectors.minBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())));
  System.out.println(op.get());
  Double sum = employees.stream().collect(Collectors.summingDouble(Employee::getSalary));
  System.out.println(sum);
  Double avg = employees.stream().collect(Collectors.averagingDouble(Employee::getSalary));
  System.out.println(avg);
  Long count = employees.stream().collect(Collectors.counting());
  System.out.println(count);
  System.out.println("--------------------------------------------");
  DoubleSummaryStatistics dss = employees.stream()
   .collect(Collectors.summarizingDouble(Employee::getSalary));
  System.out.println(dss.getMax());

如何收集Stream流?

Collector介面中方法的實現決定了如何對流執行收集操作(如收集到 List、 Set、 Map)。 Collectors實用類提供了很多靜態方法,可以方便地建立常見收集器例項, 具體方法與例項如下表:

方法返回型別作用
toListList<T>把流中元素收集到List
toSetSet<T>把流中元素收集到Set
toCollectionCollection<T>把流中元素收集到建立的集合
countingLong計算流中元素的個數
summingIntInteger對流中元素的整數屬性求和
averagingIntDouble計算流中元素Integer屬性的平均 值
summarizingIntIntSummaryStatistics收集流中Integer屬性的統計值。 如:平均值
joiningString連線流中每個字串
maxByOptional<T>根據比較器選擇最大值
minByOptional<T>根據比較器選擇最小值
reducing歸約產生的型別從一個作為累加器的初始值 開始,利用BinaryOperator與 流中元素逐個結合,從而歸 約成單個值
collectingAndThen轉換函式返回的型別包裹另一個收集器,對其結 果轉換函式
groupingByMap<K, List<T>>根據某屬性值對流分組,屬 性為K,結果為V
partitioningByMap<Boolean, List<T>>根據true或false進行分割槽

每個方法對應的使用示例如下表所示。

方法使用示例
toListList<Employee> employees= list.stream().collect(Collectors.toList());
toSetSet<Employee> employees= list.stream().collect(Collectors.toSet());
toCollectionCollection<Employee> employees=list.stream().collect(Collectors.toCollection(ArrayList::new));
countinglong count = list.stream().collect(Collectors.counting());
summingIntint total=list.stream().collect(Collectors.summingInt(Employee::getSalary));
averagingIntdouble avg= list.stream().collect(Collectors.averagingInt(Employee::getSalary))
summarizingIntIntSummaryStatistics iss= list.stream().collect(Collectors.summarizingInt(Employee::getSalary));
CollectorsString str= list.stream().map(Employee::getName).collect(Collectors.joining());
maxByOptional<Emp>max= list.stream().collect(Collectors.maxBy(comparingInt(Employee::getSalary)));
minByOptional<Emp> min = list.stream().collect(Collectors.minBy(comparingInt(Employee::getSalary)));
reducingint total=list.stream().collect(Collectors.reducing(0, Employee::getSalar, Integer::sum));
collectingAndThenint how= list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
groupingByMap<Emp.Status, List<Emp>> map= list.stream() .collect(Collectors.groupingBy(Employee::getStatus));
partitioningByMap<Boolean,List<Emp>>vd= list.stream().collect(Collectors.partitioningBy(Employee::getManage));
public void test4(){
    Optional<Double> max = emps.stream()
        .map(Employee::getSalary)
        .collect(Collectors.maxBy(Double::compare));
    System.out.println(max.get());

    Optional<Employee> op = emps.stream()
        .collect(Collectors.minBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())));

    System.out.println(op.get());

    Double sum = emps.stream()
        .collect(Collectors.summingDouble(Employee::getSalary));

    System.out.println(sum);

    Double avg = emps.stream()
        .collect(Collecors.averagingDouble(Employee::getSalary));
    System.out.println(avg);
    Long count = emps.stream()
        .collect(Collectors.counting());

    DoubleSummaryStatistics dss = emps.stream()
        .collect(Collectors.summarizingDouble(Employee::getSalary));
    System.out.println(dss.getMax());
 

什麼是並行流?

簡單來說,並行流就是把一個內容分成多個資料塊,並用不同的執行緒分別處理每個資料塊的流。

Java 8 中將並行進行了優化,我們可以很容易的對資料進行並行操作。 Stream API 可以宣告性地通過 parallel() 與sequential() 在並行流與順序流之間進行切換 。

Fork/Join 框架

Fork/Join 框架: 就是在必要的情況下,將一個大任務,進行拆分(fork)成若干個小任務(拆到不可再拆時),再將一個個的小任務運算的結果進行 join 彙總 。

Fork/Join 框架與傳統執行緒池有啥區別?

採用 “工作竊取”模式(work-stealing):

當執行新的任務時它可以將其拆分成更小的任務執行,並將小任務加到執行緒佇列中,然後再從一個隨機執行緒的佇列中偷一個並把它放在自己的佇列中。

相對於一般的執行緒池實現,fork/join框架的優勢體現在對其中包含的任務的處理方式上。在一般的執行緒池中,如果一個執行緒正在執行的任務由於某些原因無法繼續執行,那麼該執行緒會處於等待狀態。而在fork/join框架的實現中,如果某個子任務由於等待另外一個子任務的完成而無法繼續執行。那麼處理該子問題的執行緒會主動尋找其他尚未執行的子任務來執行。這種方式減少了執行緒的等待時間,提高了程式的效能。

Fork/Join框架例項

瞭解了ForJoin框架的原理之後,我們就來手動寫一個使用Fork/Join框架實現累加和的示例程式,以幫助讀者更好的理解Fork/Join框架。好了,不廢話了,上程式碼,大家通過下面的程式碼好好體會下Fork/Join框架的強大。

package io.binghe.concurrency.example.aqs;
 
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
@Slf4j
public class ForkJoinTaskExample extends RecursiveTask<Integer> {
    public static final int threshold = 2;
    private int start;
    private int end;
    public ForkJoinTaskExample(int start, int end) {
        this.start = start;
        this.end = end;
    }
    @Override
    protected Integer compute() {
        int sum = 0;
        //如果任務足夠小就計算任務
        boolean canCompute = (end - start) <= threshold;
        if (canCompute) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            // 如果任務大於閾值,就分裂成兩個子任務計算
            int middle = (start + end) / 2;
            ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
            ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle + 1, end);
 
            // 執行子任務
            leftTask.fork();
            rightTask.fork();
 
            // 等待任務執行結束合併其結果
            int leftResult = leftTask.join();
            int rightResult = rightTask.join();
 
            // 合併子任務
            sum = leftResult + rightResult;
        }
        return sum;
    }
    public static void main(String[] args) {
        ForkJoinPool forkjoinPool = new ForkJoinPool();
 
        //生成一個計算任務,計算1+2+3+4
        ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);
 
        //執行一個任務
        Future<Integer> result = forkjoinPool.submit(task);
 
        try {
            log.info("result:{}", result.get());
        } catch (Exception e) {
            log.error("exception", e);
        }
    }
}

Java8中的並行流例項

Java8對並行流進行了大量的優化,並且在開發上也極大的簡化了程式設計師的工作量,我們只需要使用類似如下的程式碼就可以使用Java8中的並行流來處理我們的資料。

LongStream.rangeClosed(0, 10000000L).parallel().reduce(0, Long::sum);

在Java8中如何優雅的切換並行流和序列流呢?

Stream API 可以宣告性地通過 parallel() 與sequential() 在並行流與序列流之間進行切換 。

Optional類

什麼是Optional類?

Optional<T> 類(java.util.Optional) 是一個容器類,代表一個值存在或不存在,原來用 null 表示一個值不存在,現在 Optional 可以更好的表達這個概念。並且可以避免空指標異常。

Optional類常用方法:

  • Optional.of(T t) : 建立一個 Optional 例項。
  • Optional.empty() : 建立一個空的 Optional 例項。
  • Optional.ofNullable(T t):若 t 不為 null,建立 Optional 例項,否則建立空例項。
  • isPresent() : 判斷是否包含值。
  • orElse(T t) : 如果呼叫物件包含值,返回該值,否則返回t。
  • orElseGet(Supplier s) :如果呼叫物件包含值,返回該值,否則返回 s 獲取的值。
  • map(Function f): 如果有值對其處理,並返回處理後的Optional,否則返回 Optional.empty()。
  • flatMap(Function mapper):與 map 類似,要求返回值必須是Optional。

Optional類示例

1.建立Optional類

(1)使用empty()方法建立一個空的Optional物件:

Optional<String> empty = Optional.empty();

(2)使用of()方法建立Optional物件:

String name = "binghe";
Optional<String> opt = Optional.of(name);
assertEquals("Optional[binghe]", opt.toString());

傳遞給of()的值不可以為空,否則會丟擲空指標異常。例如,下面的程式會丟擲空指標異常。

String name = null;
Optional<String> opt = Optional.of(name);

如果我們需要傳遞一些空值,那我們可以使用下面的示例所示。

String name = null;
Optional<String> opt = Optional.ofNullable(name);

使用ofNullable()方法,則當傳遞進去一個空值時,不會丟擲異常,而只是返回一個空的Optional物件,如同我們用Optional.empty()方法一樣。

2.isPresent

我們可以使用這個isPresent()方法檢查一個Optional物件中是否有值,只有值非空才返回true。

Optional<String> opt = Optional.of("binghe");
assertTrue(opt.isPresent());

opt = Optional.ofNullable(null);
assertFalse(opt.isPresent());

在Java8之前,我們一般使用如下方式來檢查空值。

if(name != null){
    System.out.println(name.length);
}

在Java8中,我們就可以使用如下方式來檢查空值了。

Optional<String> opt = Optional.of("binghe");
opt.ifPresent(name -> System.out.println(name.length()));

3.orElse和orElseGet

(1)orElse

orElse()方法用來返回Optional物件中的預設值,它被傳入一個“預設引數‘。如果物件中存在一個值,則返回它,否則返回傳入的“預設引數”。

String nullName = null;
String name = Optional.ofNullable(nullName).orElse("binghe");
assertEquals("binghe", name);

(2)orElseGet

與orElse()方法類似,但是這個函式不接收一個“預設引數”,而是一個函式介面。

String nullName = null;
String name = Optional.ofNullable(nullName).orElseGet(() -> "binghe");
assertEquals("binghe", name);

(3)二者有什麼區別?

要想理解二者的區別,首先讓我們建立一個無參且返回定值的方法。

public String getDefaultName() {
    System.out.println("Getting Default Name");
    return "binghe";
}

接下來,進行兩個測試看看兩個方法到底有什麼區別。

String text;
System.out.println("Using orElseGet:");
String defaultText = Optional.ofNullable(text).orElseGet(this::getDefaultName);
assertEquals("binghe", defaultText);

System.out.println("Using orElse:");
defaultText = Optional.ofNullable(text).orElse(getDefaultName());
assertEquals("binghe", defaultText);

在這裡示例中,我們的Optional物件中包含的都是一個空值,讓我們看看程式執行結果:

Using orElseGet:
Getting default name...
Using orElse:
Getting default name...

兩個Optional物件中都不存在value,因此執行結果相同。

那麼,當Optional物件中存在資料會發生什麼呢?我們一起來驗證下。

String name = "binghe001";

System.out.println("Using orElseGet:");
String defaultName = Optional.ofNullable(name).orElseGet(this::getDefaultName);
assertEquals("binghe001", defaultName);

System.out.println("Using orElse:");
defaultName = Optional.ofNullable(name).orElse(getDefaultName());
assertEquals("binghe001", defaultName);

執行結果如下所示。

Using orElseGet:
Using orElse:
Getting default name...

可以看到,當使用orElseGet()方法時,getDefaultName()方法並不執行,因為Optional中含有值,而使用orElse時則照常執行。所以可以看到,當值存在時,orElse相比於orElseGet,多建立了一個物件。如果建立物件時,存在網路互動,那系統資源的開銷就比較大了,這是需要我們注意的一個地方。

4.orElseThrow

orElseThrow()方法當遇到一個不存在的值的時候,並不返回一個預設值,而是丟擲異常。

String nullName = null;
String name = Optional.ofNullable(nullName).orElseThrow( IllegalArgumentException::new);

5.get

get()方法表示是Optional物件中獲取值。

Optional<String> opt = Optional.of("binghe");
String name = opt.get();
assertEquals("binghe", name);

使用get()方法也可以返回被包裹著的值。但是值必須存在。當值不存在時,會丟擲一個NoSuchElementException異常。

Optional<String> opt = Optional.ofNullable(null);
String name = opt.get();

6.filter

接收一個函式式介面,當符合介面時,則返回一個Optional物件,否則返回一個空的Optional物件。

String name = "binghe";
Optional<String> nameOptional = Optional.of(name);
boolean isBinghe = nameOptional.filter(n -> "binghe".equals(name)).isPresent();
assertTrue(isBinghe);
boolean isBinghe001 = nameOptional.filter(n -> "binghe001".equals(name)).isPresent();
assertFalse(isBinghe001);

使用filter()方法會過濾掉我們不需要的元素。

接下來,我們再來看一例示例,例如目前有一個Person類,如下所示。

public class Person{
    private int age;
    public Person(int age){
        this.age = age;
    }
    //省略get set方法
}

例如,我們需要過濾出年齡在25歲到35歲之前的人群,那在Java8之前我們需要建立一個如下的方法來檢測每個人的年齡範圍是否在25歲到35歲之前。

public boolean filterPerson(Peron person){
    boolean isInRange = false;
    if(person != null && person.getAge() >= 25 && person.getAge() <= 35){
        isInRange =  true;
    }
    return isInRange;
}

看上去就挺麻煩的,我們可以使用如下的方式進行測試。

assertTrue(filterPerson(new Peron(18)));
assertFalse(filterPerson(new Peron(29)));
assertFalse(filterPerson(new Peron(16)));
assertFalse(filterPerson(new Peron(34)));
assertFalse(filterPerson(null));

如果使用Optional,效果如何呢?

public boolean filterPersonByOptional(Peron person){
     return Optional.ofNullable(person)
       .map(Peron::getAge)
       .filter(p -> p >= 25)
       .filter(p -> p <= 35)
       .isPresent();
}

使用Optional看上去就清爽多了,這裡,map()僅僅是將一個值轉換為另一個值,並且這個操作並不會改變原來的值。

7.map

如果有值對其處理,並返回處理後的Optional,否則返回 Optional.empty()。

List<String> names = Arrays.asList("binghe001", "binghe002", "", "binghe003", "", "binghe004");
Optional<List<String>> listOptional = Optional.of(names);

int size = listOptional
    .map(List::size)
    .orElse(0);
assertEquals(6, size);

在這個例子中,我們使用一個List集合封裝了一些字串,然後再把這個List使用Optional封裝起來,對其map(),獲取List集合的長度。map()返回的結果也被封裝在一個Optional物件中,這裡當值不存在的時候,我們會預設返回0。如下我們獲取一個字串的長度。

String name = "binghe";
Optional<String> nameOptional = Optional.of(name);

int len = nameOptional
    .map(String::length())
    .orElse(0);
assertEquals(6, len);

我們也可以將map()方法與filter()方法結合使用,如下所示。

String password = " password ";
Optional<String> passOpt = Optional.of(password);
boolean correctPassword = passOpt.filter(
    pass -> pass.equals("password")).isPresent();
assertFalse(correctPassword);

correctPassword = passOpt
    .map(String::trim)
    .filter(pass -> pass.equals("password"))
    .isPresent();
assertTrue(correctPassword);

上述程式碼的含義就是對密碼進行驗證,檢視密碼是否為指定的值。

8.flatMap

與 map 類似,要求返回值必須是Optional。

假設我們現在有一個Person類。

public class Person {
    private String name;
    private int age;
    private String password;
 
    public Optional<String> getName() {
        return Optional.ofNullable(name);
    }
 
    public Optional<Integer> getAge() {
        return Optional.ofNullable(age);
    }
 
    public Optional<String> getPassword() {
        return Optional.ofNullable(password);
    }
    // 忽略get set方法
}

接下來,我們可以將Person封裝到Optional中,並進行測試,如下所示。

Person person = new Person("binghe", 18);
Optional<Person> personOptional = Optional.of(person);

Optional<Optional<String>> nameOptionalWrapper = personOptional.map(Person::getName);
Optional<String> nameOptional = nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
String name1 = nameOptional.orElse("");
assertEquals("binghe", name1);

String name = personOptional
    .flatMap(Person::getName)
    .orElse("");
assertEquals("binghe", name);

注意:方法getName返回的是一個Optional物件,如果使用map,我們還需要再呼叫一次get()方法,而使用flatMap()就不需要了。

預設方法

介面中的預設方法

Java 8中允許介面中包含具有具體實現的方法,該方法稱為“預設方法”,預設方法使用 default 關鍵字修飾 。

例如,我們可以定義一個介面MyFunction,其中,包含有一個預設方法getName,如下所示。

public interface MyFunction<T>{
    T get(Long id);
    default String getName(){
        return "binghe";
    }
}

預設方法的原則

在Java8中,預設方法具有“類優先”的原則。

若一個介面中定義了一個預設方法,而另外一個父類或介面中又定義了一個同名的方法時,遵循如下的原則。

1.選擇父類中的方法。如果一個父類提供了具體的實現,那麼介面中具有相同名稱和引數的預設方法會被忽略。

例如,現在有一個介面為MyFunction,和一個類MyClass,如下所示。

  • MyFunction介面
public interface MyFunction{
    default String getName(){
        return "MyFunction";
    }
}
  • MyClass類
public class MyClass{
    public String getName(){
        return "MyClass";
    }
}

此時,建立SubClass類繼承MyClass類,並實現MyFunction介面,如下所示。

public class SubClass extends MyClass implements MyFunction{
    
}

接下來,我們建立一個SubClassTest類,對SubClass類進行測試,如下所示。

public class SubClassTest{
    @Test
    public void testDefaultFunction(){
        SubClass subClass = new SubClass();
        System.out.println(subClass.getName());
    }
}

執行上述程式,會輸出字串:MyClass。

2.介面衝突。如果一個父介面提供一個預設方法,而另一個介面也提供了一個具有相同名稱和引數列表的方法(不管方法是否是預設方法), 那麼必須覆蓋該方法來解決衝突。

例如,現在有兩個介面,分別為MyFunction和MyInterface,各自都有一個預設方法getName(),如下所示。

  • MyFunction介面
public interface MyFunction{
    default String getName(){
        return "function";
    }
}
  • MyInterface介面
public interface MyInterface{
    default String getName(){
        return "interface";
    }
}

實現類MyClass同時實現了MyFunction介面和MyInterface介面,由於MyFunction介面和MyInterface介面中都存在getName()預設方法,所以,MyClass必須覆蓋getName()方法來解決衝突,如下所示。

public class MyClass{
    @Override
    public String getName(){
        return MyInterface.super.getName();
    }
}

此時,MyClass類中的getName方法返回的是:interface。

如果MyClass中的getName()方法覆蓋的是MyFunction介面的getName()方法,如下所示。

public class MyClass{
    @Override
    public String getName(){
        return MyFunction.super.getName();
    }
}

此時,MyClass類中的getName方法返回的是:function。

介面中的靜態方法

在Java8中,介面中允許新增靜態方法,使用方式介面名.方法名。例如MyFunction介面中定義了靜態方法send()。

public interface MyFunction{
    default String getName(){
        return "binghe";
    }
    static void send(){
        System.out.println("Send Message...");
    }
}

我們可以直接使用如下方式呼叫MyFunction介面的send靜態方法。

MyFunction.send();

本地時間和時間戳

主要方法:

  • now:靜態方法,根據當前時間建立物件
  • of:靜態方法,根據指定日期/時間建立物件
  • plusDays,plusWeeks,plusMonths,plusYears:向當前LocalDate 物件新增幾天、幾周、幾個月、幾年
  • minusDays,minusWeeks,minusMonths,minusYears:從當前LocalDate 物件減去幾天、幾周、幾個月、幾年
  • plus,minus:新增或減少一個Duration 或Period
  • withDayOfMonth,withDayOfYear,withMonth,withYear:將月份天數、年份天數、月份、年份修改為指定的值並返回新的LocalDate 物件
  • getDayOfYear:獲得年份天數(1~366)
  • getDayOfWeek:獲得星期幾(返回一個DayOfWeek列舉值)
  • getMonth:獲得月份, 返回一個Month 列舉值
  • getMonthValue:獲得月份(1~12)
  • getYear:獲得年份
  • until:獲得兩個日期之間的Period 物件,或者指定ChronoUnits 的數字
  • isBefore,isAfter:比較兩個LocalDate
  • isLeapYear:判斷是否是閏年

使用 LocalDate、 LocalTime、 LocalDateTime

LocalDate、 LocalTime、 LocalDateTime 類的例項是不可變的物件,分別表示使用 ISO-8601日曆系統的日期、時間、日期和時間。它們提供了簡單的日期或時間,並不包含當前的時間資訊。也不包含與時區相關的資訊。

注: ISO-8601日曆系統是國際標準化組織制定的現代公民的日期和時間的表示法
方法描述
now()靜態方法,根據當前時間建立物件
of()靜態方法,根據指定日期/時間建立 物件
plusDays, plusWeeks, plusMonths, plusYears向當前 LocalDate 物件新增幾天、 幾周、 幾個月、 幾年
minusDays, minusWeeks, minusMonths, minusYears從當前 LocalDate 物件減去幾天、 幾周、 幾個月、 幾年
plus, minus新增或減少一個 Duration 或 Period
withDayOfMonth, withDayOfYear, withMonth, withYear將月份天數、 年份天數、 月份、 年 份 修 改 為 指 定 的 值 並 返 回 新 的 LocalDate 物件
getDayOfMonth獲得月份天數(1-31)
getDayOfYear獲得年份天數(1-366)
getDayOfWeek獲得星期幾(返回一個 DayOfWeek 列舉值)
getMonth獲得月份, 返回一個 Month 列舉值
getMonthValue獲得月份(1-12)
getYear獲得年份
until獲得兩個日期之間的 Period 物件, 或者指定 ChronoUnits 的數字
isBefore, isAfter比較兩個 LocalDate
isLeapYear判斷是否是閏年

示例程式碼如下所示。

// 獲取當前系統時間
LocalDateTime localDateTime1 = LocalDateTime.now();
System.out.println(localDateTime1);
// 執行結果:2019-10-27T13:49:09.483

// 指定日期時間
LocalDateTime localDateTime2 = LocalDateTime.of(2019, 10, 27, 13, 45,10);
System.out.println(localDateTime2);
// 執行結果:2019-10-27T13:45:10

LocalDateTime localDateTime3 = localDateTime1
        // 加三年
        .plusYears(3)
        // 減三個月
        .minusMonths(3);
System.out.println(localDateTime3);
// 執行結果:2022-07-27T13:49:09.483

System.out.println(localDateTime1.getYear());       // 執行結果:2019
System.out.println(localDateTime1.getMonthValue()); // 執行結果:10
System.out.println(localDateTime1.getDayOfMonth()); // 執行結果:27
System.out.println(localDateTime1.getHour());       // 執行結果:13
System.out.println(localDateTime1.getMinute());     // 執行結果:52
System.out.println(localDateTime1.getSecond());     // 執行結果:6

LocalDateTime localDateTime4 = LocalDateTime.now();
System.out.println(localDateTime4);     // 2019-10-27T14:19:56.884
LocalDateTime localDateTime5 = localDateTime4.withDayOfMonth(10);
System.out.println(localDateTime5);     // 2019-10-10T14:19:56.884

Instant 時間戳

用於“時間戳”的運算。它是以Unix元年(傳統的設定為UTC時區1970年1月1日午夜時分)開始所經歷的描述進行運算 。

示例程式碼如下所示。

Instant instant1 = Instant.now();    // 預設獲取UTC時區
System.out.println(instant1);
// 執行結果:2019-10-27T05:59:58.221Z

// 偏移量運算
OffsetDateTime offsetDateTime = instant1.atOffset(ZoneOffset.ofHours(8));
System.out.println(offsetDateTime);
// 執行結果:2019-10-27T13:59:58.221+08:00

// 獲取時間戳
System.out.println(instant1.toEpochMilli());
// 執行結果:1572156145000

// 以Unix元年為起點,進行偏移量運算
Instant instant2 = Instant.ofEpochSecond(60);
System.out.println(instant2);
// 執行結果:1970-01-01T00:01:00Z

Duration 和 Period

Duration:用於計算兩個“時間”間隔。

Period:用於計算兩個“日期”間隔 。

Instant instant_1 = Instant.now();
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
Instant instant_2 = Instant.now();

Duration duration = Duration.between(instant_1, instant_2);
System.out.println(duration.toMillis());
// 執行結果:1000

LocalTime localTime_1 = LocalTime.now();
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
LocalTime localTime_2 = LocalTime.now();

System.out.println(Duration.between(localTime_1, localTime_2).toMillis());
// 執行結果:1000
LocalDate localDate_1 = LocalDate.of(2018,9, 9);
LocalDate localDate_2 = LocalDate.now();

Period period = Period.between(localDate_1, localDate_2);
System.out.println(period.getYears());      // 執行結果:1
System.out.println(period.getMonths());     // 執行結果:1
System.out.println(period.getDays());       // 執行結果:18

日期的操縱

TemporalAdjuster : 時間校正器。有時我們可能需要獲取例如:將日期調整到“下個週日”等操作。

TemporalAdjusters : 該類通過靜態方法提供了大量的常用 TemporalAdjuster 的實現。

例如獲取下個週日,如下所示:

LocalDate nextSunday = LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.SUNDAY));

完整的示例程式碼如下所示。

LocalDateTime localDateTime1 = LocalDateTime.now();
System.out.println(localDateTime1);     // 2019-10-27T14:19:56.884

// 獲取這個第一天的日期
System.out.println(localDateTime1.with(TemporalAdjusters.firstDayOfMonth()));            // 2019-10-01T14:22:58.574
// 獲取下個週末的日期
System.out.println(localDateTime1.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)));       // 2019-11-03T14:22:58.574

// 自定義:下一個工作日
LocalDateTime localDateTime2 = localDateTime1.with(l -> {
    LocalDateTime localDateTime = (LocalDateTime) l;
    DayOfWeek dayOfWeek =  localDateTime.getDayOfWeek();

    if (dayOfWeek.equals(DayOfWeek.FRIDAY)) {
       return localDateTime.plusDays(3);
    } else if (dayOfWeek.equals(DayOfWeek.SATURDAY)) {
       return localDateTime.plusDays(2);
    } else {
       return localDateTime.plusDays(1);
    }
});
System.out.println(localDateTime2);
// 執行結果:2019-10-28T14:30:17.400

解析與格式化

java.time.format.DateTimeFormatter 類:該類提供了三種格式化方法:

  • 預定義的標準格式
  • 語言環境相關的格式
  • 自定義的格式

示例程式碼如下所示。

DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ISO_DATE;
LocalDateTime localDateTime = LocalDateTime.now();
String strDate1 = localDateTime.format(dateTimeFormatter1);
System.out.println(strDate1);
// 執行結果:2019-10-27

// Date -> String
DateTimeFormatter dateTimeFormatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd  HH:mm:ss");
String strDate2 = dateTimeFormatter2.format(localDateTime);
System.out.println(strDate2);
// 執行結果:2019-10-27  14:36:11

// String -> Date
LocalDateTime localDateTime1 = localDateTime.parse(strDate2, dateTimeFormatter2);
System.out.println(localDateTime1);
// 執行結果:2019-10-27T14:37:39

時區的處理

Java8 中加入了對時區的支援,帶時區的時間為分別為:ZonedDate、 ZonedTime、 ZonedDateTime。

其中每個時區都對應著 ID,地區ID都為 “{區域}/{城市}”的格式,例如 : Asia/Shanghai 等。

  • ZoneId:該類中包含了所有的時區資訊
  • getAvailableZoneIds() : 可以獲取所有時區時區資訊
  • of(id) : 用指定的時區資訊獲取 ZoneId 物件

示例程式碼如下所示。

// 獲取所有的時區
Set<String> set = ZoneId.getAvailableZoneIds();
System.out.println(set);
// [Asia/Aden, America/Cuiaba, Etc/GMT+9, Etc/GMT+8, Africa/Nairobi, America/Marigot, Asia/Aqtau, Pacific/Kwajalein, America/El_Salvador, Asia/Pontianak, Africa/Cairo, Pacific/Pago_Pago, Africa/Mbabane, Asia/Kuching, Pacific/Honolulu, Pacific/Rarotonga, America/Guatemala, Australia/Hobart, Europe/London, America/Belize, America/Panama, Asia/Chungking, America/Managua, America/Indiana/Petersburg, Asia/Yerevan, Europe/Brussels, GMT, Europe/Warsaw, America/Chicago, Asia/Kashgar, Chile/Continental, Pacific/Yap, CET, Etc/GMT-1, Etc/GMT-0, Europe/Jersey, America/Tegucigalpa, Etc/GMT-5, Europe/Istanbul, America/Eirunepe, Etc/GMT-4, America/Miquelon, Etc/GMT-3, Europe/Luxembourg, Etc/GMT-2, Etc/GMT-9, America/Argentina/Catamarca, Etc/GMT-8, Etc/GMT-7, Etc/GMT-6, Europe/Zaporozhye, Canada/Yukon, Canada/Atlantic, Atlantic/St_Helena, Australia/Tasmania, Libya, Europe/Guernsey, America/Grand_Turk, US/Pacific-New, Asia/Samarkand, America/Argentina/Cordoba, Asia/Phnom_Penh, Africa/Kigali, Asia/Almaty, US/Alaska, Asia/Dubai, Europe/Isle_of_Man, America/Araguaina, Cuba, Asia/Novosibirsk, America/Argentina/Salta, Etc/GMT+3, Africa/Tunis, Etc/GMT+2, Etc/GMT+1, Pacific/Fakaofo, Africa/Tripoli, Etc/GMT+0, Israel, Africa/Banjul, Etc/GMT+7, Indian/Comoro, Etc/GMT+6, Etc/GMT+5, Etc/GMT+4, Pacific/Port_Moresby, US/Arizona, Antarctica/Syowa, Indian/Reunion, Pacific/Palau, Europe/Kaliningrad, America/Montevideo, Africa/Windhoek, Asia/Karachi, Africa/Mogadishu, Australia/Perth, Brazil/East, Etc/GMT, Asia/Chita, Pacific/Easter, Antarctica/Davis, Antarctica/McMurdo, Asia/Macao, America/Manaus, Africa/Freetown, Europe/Bucharest, Asia/Tomsk, America/Argentina/Mendoza, Asia/Macau, Europe/Malta, Mexico/BajaSur, Pacific/Tahiti, Africa/Asmera, Europe/Busingen, America/Argentina/Rio_Gallegos, Africa/Malabo, Europe/Skopje, America/Catamarca, America/Godthab, Europe/Sarajevo, Australia/ACT, GB-Eire, Africa/Lagos, America/Cordoba, Europe/Rome, Asia/Dacca, Indian/Mauritius, Pacific/Samoa, America/Regina, America/Fort_Wayne, America/Dawson_Creek, Africa/Algiers, Europe/Mariehamn, America/St_Johns, America/St_Thomas, Europe/Zurich, America/Anguilla, Asia/Dili, America/Denver, Africa/Bamako, Europe/Saratov, GB, Mexico/General, Pacific/Wallis, Europe/Gibraltar, Africa/Conakry, Africa/Lubumbashi, Asia/Istanbul, America/Havana, NZ-CHAT, Asia/Choibalsan, America/Porto_Acre, Asia/Omsk, Europe/Vaduz, US/Michigan, Asia/Dhaka, America/Barbados, Europe/Tiraspol, Atlantic/Cape_Verde, Asia/Yekaterinburg, America/Louisville, Pacific/Johnston, Pacific/Chatham, Europe/Ljubljana, America/Sao_Paulo, Asia/Jayapura, America/Curacao, Asia/Dushanbe, America/Guyana, America/Guayaquil, America/Martinique, Portugal, Europe/Berlin, Europe/Moscow, Europe/Chisinau, America/Puerto_Rico, America/Rankin_Inlet, Pacific/Ponape, Europe/Stockholm, Europe/Budapest, America/Argentina/Jujuy, Australia/Eucla, Asia/Shanghai, Universal, Europe/Zagreb, America/Port_of_Spain, Europe/Helsinki, Asia/Beirut, Asia/Tel_Aviv, Pacific/Bougainville, US/Central, Africa/Sao_Tome, Indian/Chagos, America/Cayenne, Asia/Yakutsk, Pacific/Galapagos, Australia/North, Europe/Paris, Africa/Ndjamena, Pacific/Fiji, America/Rainy_River, Indian/Maldives, Australia/Yancowinna, SystemV/AST4, Asia/Oral, America/Yellowknife, Pacific/Enderbury, America/Juneau, Australia/Victoria, America/Indiana/Vevay, Asia/Tashkent, Asia/Jakarta, Africa/Ceuta, Asia/Barnaul, America/Recife, America/Buenos_Aires, America/Noronha, America/Swift_Current, Australia/Adelaide, America/Metlakatla, Africa/Djibouti, America/Paramaribo, Europe/Simferopol, Europe/Sofia, Africa/Nouakchott, Europe/Prague, America/Indiana/Vincennes, Antarctica/Mawson, America/Kralendijk, Antarctica/Troll, Europe/Samara, Indian/Christmas, America/Antigua, Pacific/Gambier, America/Indianapolis, America/Inuvik, America/Iqaluit, Pacific/Funafuti, UTC, Antarctica/Macquarie, Canada/Pacific, America/Moncton, Africa/Gaborone, Pacific/Chuuk, Asia/Pyongyang, America/St_Vincent, Asia/Gaza, Etc/Universal, PST8PDT, Atlantic/Faeroe, Asia/Qyzylorda, Canada/Newfoundland, America/Kentucky/Louisville, America/Yakutat, Asia/Ho_Chi_Minh, Antarctica/Casey, Europe/Copenhagen, Africa/Asmara, Atlantic/Azores, Europe/Vienna, ROK, Pacific/Pitcairn, America/Mazatlan, Australia/Queensland, Pacific/Nauru, Europe/Tirane, Asia/Kolkata, SystemV/MST7, Australia/Canberra, MET, Australia/Broken_Hill, Europe/Riga, America/Dominica, Africa/Abidjan, America/Mendoza, America/Santarem, Kwajalein, America/Asuncion, Asia/Ulan_Bator, NZ, America/Boise, Australia/Currie, EST5EDT, Pacific/Guam, Pacific/Wake, Atlantic/Bermuda, America/Costa_Rica, America/Dawson, Asia/Chongqing, Eire, Europe/Amsterdam, America/Indiana/Knox, America/North_Dakota/Beulah, Africa/Accra, Atlantic/Faroe, Mexico/BajaNorte, America/Maceio, Etc/UCT, Pacific/Apia, GMT0, America/Atka, Pacific/Niue, Australia/Lord_Howe, Europe/Dublin, Pacific/Truk, MST7MDT, America/Monterrey, America/Nassau, America/Jamaica, Asia/Bishkek, America/Atikokan, Atlantic/Stanley, Australia/NSW, US/Hawaii, SystemV/CST6, Indian/Mahe, Asia/Aqtobe, America/Sitka, Asia/Vladivostok, Africa/Libreville, Africa/Maputo, Zulu, America/Kentucky/Monticello, Africa/El_Aaiun, Africa/Ouagadougou, America/Coral_Harbour, Pacific/Marquesas, Brazil/West, America/Aruba, America/North_Dakota/Center, America/Cayman, Asia/Ulaanbaatar, Asia/Baghdad, Europe/San_Marino, America/Indiana/Tell_City, America/Tijuana, Pacific/Saipan, SystemV/YST9, Africa/Douala, America/Chihuahua, America/Ojinaga, Asia/Hovd, America/Anchorage, Chile/EasterIsland, America/Halifax, Antarctica/Rothera, America/Indiana/Indianapolis, US/Mountain, Asia/Damascus, America/Argentina/San_Luis, America/Santiago, Asia/Baku, America/Argentina/Ushuaia, Atlantic/Reykjavik, Africa/Brazzaville, Africa/Porto-Novo, America/La_Paz, Antarctica/DumontDUrville, Asia/Taipei, Antarctica/South_Pole, Asia/Manila, Asia/Bangkok, Africa/Dar_es_Salaam, Poland, Atlantic/Madeira, Antarctica/Palmer, America/Thunder_Bay, Africa/Addis_Ababa, Asia/Yangon, Europe/Uzhgorod, Brazil/DeNoronha, Asia/Ashkhabad, Etc/Zulu, America/Indiana/Marengo, America/Creston, America/Punta_Arenas, America/Mexico_City, Antarctica/Vostok, Asia/Jerusalem, Europe/Andorra, US/Samoa, PRC, Asia/Vientiane, Pacific/Kiritimati, America/Matamoros, America/Blanc-Sablon, Asia/Riyadh, Iceland, Pacific/Pohnpei, Asia/Ujung_Pandang, Atlantic/South_Georgia, Europe/Lisbon, Asia/Harbin, Europe/Oslo, Asia/Novokuznetsk, CST6CDT, Atlantic/Canary, America/Knox_IN, Asia/Kuwait, SystemV/HST10, Pacific/Efate, Africa/Lome, America/Bogota, America/Menominee, America/Adak, Pacific/Norfolk, Europe/Kirov, America/Resolute, Pacific/Tarawa, Africa/Kampala, Asia/Krasnoyarsk, Greenwich, SystemV/EST5, America/Edmonton, Europe/Podgorica, Australia/South, Canada/Central, Africa/Bujumbura, America/Santo_Domingo, US/Eastern, Europe/Minsk, Pacific/Auckland, Africa/Casablanca, America/Glace_Bay, Canada/Eastern, Asia/Qatar, Europe/Kiev, Singapore, Asia/Magadan, SystemV/PST8, America/Port-au-Prince, Europe/Belfast, America/St_Barthelemy, Asia/Ashgabat, Africa/Luanda, America/Nipigon, Atlantic/Jan_Mayen, Brazil/Acre, Asia/Muscat, Asia/Bahrain, Europe/Vilnius, America/Fortaleza, Etc/GMT0, US/East-Indiana, America/Hermosillo, America/Cancun, Africa/Maseru, Pacific/Kosrae, Africa/Kinshasa, Asia/Kathmandu, Asia/Seoul, Australia/Sydney, America/Lima, Australia/LHI, America/St_Lucia, Europe/Madrid, America/Bahia_Banderas, America/Montserrat, Asia/Brunei, America/Santa_Isabel, Canada/Mountain, America/Cambridge_Bay, Asia/Colombo, Australia/West, Indian/Antananarivo, Australia/Brisbane, Indian/Mayotte, US/Indiana-Starke, Asia/Urumqi, US/Aleutian, Europe/Volgograd, America/Lower_Princes, America/Vancouver, Africa/Blantyre, America/Rio_Branco, America/Danmarkshavn, America/Detroit, America/Thule, Africa/Lusaka, Asia/Hong_Kong, Iran, America/Argentina/La_Rioja, Africa/Dakar, SystemV/CST6CDT, America/Tortola, America/Porto_Velho, Asia/Sakhalin, Etc/GMT+10, America/Scoresbysund, Asia/Kamchatka, Asia/Thimbu, Africa/Harare, Etc/GMT+12, Etc/GMT+11, Navajo, America/Nome, Europe/Tallinn, Turkey, Africa/Khartoum, Africa/Johannesburg, Africa/Bangui, Europe/Belgrade, Jamaica, Africa/Bissau, Asia/Tehran, WET, Europe/Astrakhan, Africa/Juba, America/Campo_Grande, America/Belem, Etc/Greenwich, Asia/Saigon, America/Ensenada, Pacific/Midway, America/Jujuy, Africa/Timbuktu, America/Bahia, America/Goose_Bay, America/Virgin, America/Pangnirtung, Asia/Katmandu, America/Phoenix, Africa/Niamey, America/Whitehorse, Pacific/Noumea, Asia/Tbilisi, America/Montreal, Asia/Makassar, America/Argentina/San_Juan, Hongkong, UCT, Asia/Nicosia, America/Indiana/Winamac, SystemV/MST7MDT, America/Argentina/ComodRivadavia, America/Boa_Vista, America/Grenada, Asia/Atyrau, Australia/Darwin, Asia/Khandyga, Asia/Kuala_Lumpur, Asia/Famagusta, Asia/Thimphu, Asia/Rangoon, Europe/Bratislava, Asia/Calcutta, America/Argentina/Tucuman, Asia/Kabul, Indian/Cocos, Japan, Pacific/Tongatapu, America/New_York, Etc/GMT-12, Etc/GMT-11, Etc/GMT-10, SystemV/YST9YDT, Europe/Ulyanovsk, Etc/GMT-14, Etc/GMT-13, W-SU, America/Merida, EET, America/Rosario, Canada/Saskatchewan, America/St_Kitts, Arctic/Longyearbyen, America/Fort_Nelson, America/Caracas, America/Guadeloupe, Asia/Hebron, Indian/Kerguelen, SystemV/PST8PDT, Africa/Monrovia, Asia/Ust-Nera, Egypt, Asia/Srednekolymsk, America/North_Dakota/New_Salem, Asia/Anadyr, Australia/Melbourne, Asia/Irkutsk, America/Shiprock, America/Winnipeg, Europe/Vatican, Asia/Amman, Etc/UTC, SystemV/AST4ADT, Asia/Tokyo, America/Toronto, Asia/Singapore, Australia/Lindeman, America/Los_Angeles, SystemV/EST5EDT, Pacific/Majuro, America/Argentina/Buenos_Aires, Europe/Nicosia, Pacific/Guadalcanal, Europe/Athens, US/Pacific, Europe/Monaco]

// 通過時區構建LocalDateTime
LocalDateTime localDateTime1 = LocalDateTime.now(ZoneId.of("America/El_Salvador"));
System.out.println(localDateTime1);
// 2019-10-27T00:46:21.268

// 以時區格式顯示時間
LocalDateTime localDateTime2 = LocalDateTime.now();
ZonedDateTime zonedDateTime1 = localDateTime2.atZone(ZoneId.of("Africa/Nairobi"));
System.out.println(zonedDateTime1);
// 2019-10-27T14:46:21.273+03:00[Africa/Nairobi]

與傳統日期處理的轉換

JDK註解

JDK5中的註解

1.註解(@)

註解就相當於一種標記,在程式中加了註解就等於為程式加了某種標記。(JDK1.5新特性)。

2.作用

告訴javac編譯器或者java開發工具……向其傳遞某種資訊,作為一個標記。

3.如何理解註解?

一個註解就是一個類。

標記可以加在包、類、欄位、方法,方法引數以及區域性變數上。可以同時存在多個註解。

每一個註解結尾都沒有“;”或者其他特別符號。

定義註解需要的基礎註解資訊如下所示。

@SuppressWarnings("deprecation")  //編譯器警告過時(source階段)
@Deprecated                        //過時(Runtime階段)
@Override                        //重寫(source階段)
@Retention(RetentionPolicy.RUNTIME)    
//保留註解到程式執行時。(Runtime階段)
@Target({ElementType.METHOD,ElementType.TYPE})
//標記既能定義在方法上,又能定義在類、介面、列舉上等。

注意:

1)新增註解需要有註解類。RetentionPolicy是一個列舉類(有三個成員)。

2)Target中可以存放陣列。它的預設值為任何元素。

  • ElementType.METHOD:表示只能標記在方法上。
  • ElementType.TYPE:表示只能標記定義在類上、介面上、列舉上等

    3)ElementType也是列舉類。成員包括:ANNOTATION_TYPE(註解)、CONSTRUCTOR(構造方法)、FIEID(成員變數)、LOCAL_VARIABLE(變數)、METHOD(方法)、PACKAGE(包)、PARAMETER(引數)、TYPE。

4.關於註解

  • 元註解:註解的註解(理解:給一個註解類再加註解)
  • 後設資料:資料的資料
  • 元資訊:資訊的資訊

5.註解分為三個階段

java原始檔--> class檔案 --> 記憶體中的位元組碼。

Retention的註解有三種取值:(分別對應註解的三個階段)

  • RetentionPolicy.SOURCE
  • RetentionPolicy.CLASS
  • RetentionPolicy.RUNTIME

注意:註解的預設階段是Class。

6.註解的屬性型別

原始型別(就是八個基本資料型別)、String型別、Class型別、陣列型別、列舉型別、註解型別。

7.為註解增加屬性

value:是一個特殊的屬性,若在設定值時只有一個value屬性需要設定或者其他屬性都採用預設值時 ,那麼value=可以省略,直接寫所設定的值即可。

例如:@SuppressWarnings("deprecation")

為屬性指定預設值(預設值):
例如:String value() default "blue"; //定義在註解類中

陣列型別的屬性:
例如:int[] arrayArr() default {3,4,5,5};//定義在註解類中
SunAnnotation(arrayArr={3,9,8}) //設定陣列值
注意:如果陣列屬性中只有一個元素時,屬性值部分可以省略大括號。
例如:SunAnnotation(arrayArr=9)

列舉型別的屬性:
例如:EnumDemo.TrafficLamp lamp()
////列舉型別屬性, 定義在註解類中,這裡使用了自定義的列舉類EnumDemo.java並沒有給出相關程式碼,這裡只是舉個例子
default EnumDemo.TrafficLamp.RED;

註解型別的屬性:
例如:MetaAnnotation annotationAttr()
//定義在一個註解類中,並指定預設值,
//此屬性關聯到註解類:MetaAnnotation.java, 
default @MetaAnnotation("lhm");
//設定註解屬性值
@SunAnnotation(annotationAttr=@MetaAnnotation("flx"))

Java8中的註解

對於註解(也被稱做後設資料),Java 8 主要有兩點改進:型別註解和重複註解。

1.型別註解

1)Java 8 的型別註解擴充套件了註解使用的範圍。

在java 8之前,註解只能是在宣告的地方所使用,java8開始,註解可以應用在任何地方。

例如:

建立類例項

new @Interned MyObject();

型別對映

myString = (@NonNull String) str;

implements 語句中

class UnmodifiableList<T> implements@Readonly List<@Readonly T> { ... }

throw exception宣告

void monitorTemperature() throws@Critical TemperatureException { ... }

注意:

在Java 8裡面,當型別轉化甚至分配新物件的時候,都可以在宣告變數或者引數的時候使用註解。
Java註解可以支援任意型別。

型別註解只是語法而不是語義,並不會影響java的編譯時間,載入時間,以及執行時間,也就是說,編譯成class檔案的時候並不包含型別註解。

2)新增ElementType.TYPE_USE 和ElementType.TYPE_PARAMETER(在Target上)

新增的兩個註釋的程式元素型別 ElementType.TYPE_USE 和 ElementType.TYPE_PARAMETER用來描述註解的新場合。

  • ElementType.TYPE_PARAMETER 表示該註解能寫在型別變數的宣告語句中。
  • ElementType.TYPE_USE 表示該註解能寫在使用型別的任何語句中(例如:宣告語句、泛型和強制轉換語句中的型別)。

例如,下面的示例。

@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}

3)型別註解的作用

型別註解被用來支援在Java的程式中做強型別檢查。配合第三方外掛工具Checker Framework(注:此外掛so easy,這裡不介紹了),可以在編譯的時候檢測出runtime error(例如:UnsupportedOperationException; NumberFormatException;NullPointerException異常等都是runtime error),以提高程式碼質量。這就是型別註解的作用。

注意:使用Checker Framework可以找到型別註解出現的地方並檢查。

例如下面的程式碼。

import checkers.nullness.quals.*;
public class TestDemo{
    void sample() {
        @NonNull Object my = new Object();
    }
}

使用javac編譯上面的類:(當然若下載了Checker Framework外掛就不需要這麼麻煩了)

javac -processor checkers.nullness.NullnessChecker TestDemo.java

上面編譯是通過的,但若修改程式碼:

@NonNull Object my = null;

但若不想使用型別註解檢測出來錯誤,則不需要processor,正常javac TestDemo.java是可以通過編譯的,但是執行時會報 NullPointerException 異常。

為了能在編譯期間就自動檢查出這類異常,可以通過型別註解結合 Checker Framework 提前排查出來錯誤異常。

注意java 5,6,7版本是不支援註解@NonNull,但checker framework 有個向下相容的解決方案,就是將型別註解@NonNull 用/**/註釋起來。

import checkers.nullness.quals.*;
public class TestDemo{
    void sample() {
        /*@NonNull*/ Object my = null;
    }
}

這樣javac編譯器就會忽略掉註釋塊,但用checker framework裡面的javac編譯器同樣能夠檢測出@NonNull錯誤。
通過 型別註解 + checker framework 可以在編譯時就找到runtime error。

2.重複註解

允許在同一宣告型別(類,屬性,或方法)上多次使用同一個註解。

Java8以前的版本使用註解有一個限制是相同的註解在同一位置只能使用一次,不能使用多次。

Java 8 引入了重複註解機制,這樣相同的註解可以在同一地方使用多次。重複註解機制本身必須用 @Repeatable 註解。

實際上,重複註解不是一個語言上的改變,只是編譯器層面的改動,技術層面仍然是一樣的。

例如,我們可以使用如下示例來具體對比Java8之前的版本和Java8中的註解。

1) 自定義一個包裝類Hints註解用來放置一組具體的Hint註解

@interface MyHints {
    Hint[] value();
}
 
@Repeatable(MyHints.class)
@interface Hint {
    String value();
}

使用包裝類當容器來存多個註解(舊版本方法)

@MyHints({@Hint("hint1"), @Hint("hint2")})
class Person {}

使用多重註解(新方法)

@Hint("hint1")
@Hint("hint2")
class Person {}

2) 完整類測試如下所示。

public class RepeatingAnnotations {
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Filters {
        Filter[] value();
    }
    
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Repeatable(Filters.class)
    public @interface Filter {
        String value();
    }
    @Filter("filter1")
    @Filter("filter2")
    public interface Filterable {
    }
    public static void main(String[] args) {
        for (Filter filter : Filterable.class.getAnnotationsByType(Filter.class)) {
            System.out.println(filter.value());
        }
    }
}

輸出結果:

filter1
filter2

分析:

註釋Filter被@Repeatable( Filters.class )註釋。Filters 只是一個容器,它持有Filter, 編譯器盡力向程式設計師隱藏它的存在。通過這樣的方式,Filterable介面可以被Filter註釋兩次。

另外,反射的API提供一個新方法getAnnotationsByType() 來返回重複註釋的型別(注意Filterable.class.getAnnotation( Filters.class )將會返回編譯器注入的Filters例項。

3) java 8之前也有重複使用註解的解決方案,但可讀性不好。

public @interface MyAnnotation {  
     String role();  
}  
 
public @interface Annotations {  
    MyAnnotation[] value();  
}  
 
public class RepeatAnnotationUseOldVersion {  
    @Annotations({@MyAnnotation(role="Admin"),@MyAnnotation(role="Manager")})  
    public void doSomeThing(){  
    }  
}

Java8的實現方式(由另一個註解來儲存重複註解,在使用時候,用儲存註解Authorities來擴充套件重複註解),可讀性更強。

@Repeatable(Annotations.class) 
public @interface MyAnnotation {  
     String role();  
}  
 
public @interface Annotations {  
    MyAnnotation[] value();  
}  
 
public class RepeatAnnotationUseOldVersion {  
    @MyAnnotation(role="Admin")  
    @MyAnnotation(role="Manager")
    public void doSomeThing(){  
    }  
} 

什麼?沒看懂?那就再來一波!!!

Java8對註解的增強

Java 8對註解處理提供了兩點改進:可重複的註解及可用於型別的註解。總體來說,比較簡單,下面,我們就以例項的形式來說明Java8中的重複註解和型別註解。

首先,我們來定義一個註解類BingheAnnotation,如下所示。

package io.mykit.binghe.java8.annotition;

import java.lang.annotation.*;

/**
 * @author binghe
 * @version 1.0.0
 * @description 定義註解
 */
@Repeatable(BingheAnnotations.class)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE,ElementType.TYPE_PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface BingheAnnotation {
    String value();
}

注意:在BingheAnnotation註解類上比普通的註解多了一個@Repeatable(BingheAnnotations.class)註解,有小夥伴會問:這個是啥啊?這個就是Java8中定義可重複註解的關鍵,至於BingheAnnotations.class,大家別急,繼續往下看就明白了。

接下來,我們們定義一個BingheAnnotations註解類,如下所示。

package io.mykit.binghe.java8.annotation;

import java.lang.annotation.*;

/**
 * @author binghe
 * @version 1.0.0
 * @description 定義註解
 */
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE,ElementType.TYPE_PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface BingheAnnotations {
    BingheAnnotation[] value();
}

看到這裡,大家明白了吧!!沒錯,BingheAnnotations也是一個註解類,它相比於BingheAnnotation註解類來說,少了一個@Repeatable(BingheAnnotations.class)註解,也就是說,BingheAnnotations註解類的定義與普通的註解幾乎沒啥區別。值得注意的是,我們在BingheAnnotations註解類中,定義了一個BingheAnnotation註解類的陣列,也就是說,在BingheAnnotations註解類中,包含有多個BingheAnnotation註解。所以,在BingheAnnotation註解類上指定@Repeatable(BingheAnnotations.class)來說明可以在類、欄位、方法、引數、構造方法、引數上重複使用BingheAnnotation註解。

接下來,我們建立一個Binghe類,在Binghe類中定義一個init()方法,在init方法上,重複使用@BingheAnnotation註解指定相應的資料,如下所示。

package io.mykit.binghe.java8.annotation;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試註解
 */
@BingheAnnotation("binghe")
@BingheAnnotation("class")
public class Binghe {

    @BingheAnnotation("init")
    @BingheAnnotation("method")
    public void init(){

    }
}

到此,我們就可以測試重複註解了,建立類BingheAnnotationTest,對重複註解進行測試,如下所示。

package io.mykit.binghe.java8.annotation;

import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試註解
 */
public class BingheAnnotationTest {

    public static void main(String[] args) throws NoSuchMethodException {
        Class<Binghe> clazz = Binghe.class;
        BingheAnnotation[] annotations = clazz.getAnnotationsByType(BingheAnnotation.class);
        System.out.println("類上的重複註解如下:");
        Arrays.stream(annotations).forEach((a) -> System.out.print(a.value() + " "));

        System.out.println();
        System.out.println("=============================");

        Method method = clazz.getMethod("init");
        annotations = method.getAnnotationsByType(BingheAnnotation.class);
        System.out.println("方法上的重複註解如下:");
        Arrays.stream(annotations).forEach((a) -> System.out.print(a.value() + " "));
    }
}

執行main()方法,輸出如下的結果資訊。

類上的重複註解如下:
binghe class 
=============================
方法上的重複註解如下:
init method 

好了,今天就到這兒吧,我是冰河,大家有啥問題可以在下方留言,也可以加我微信:sun_shine_lyz,我拉你進群,一起交流技術,一起進階,一起牛逼~~

整理自Github:https://github.com/MaRuifu/Ja... Github作者:小馬哥

相關文章