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

大家好,我是冰河~~

說實話,肝這篇文章花了我一個月的時間,關於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 void 對類型爲T的對象應用操作,接口定義的方法:void accept(T t)
Supplier供給型接口 T 返回類型爲T的對象,接口定義的方法:T get()
Function<T, R>函數式接口 T R 對類型爲T的對象應用操作,並R類型的返回結果。接口定義的方法:R apply(T t)
Predicate斷言型接口 T boolean 確定類型爲T的對象是否滿足約束條件,並返回boolean類型的數據。接口定義的方法:boolean test(T t)

其他函數接口總覽

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

函數式接口 參數類型 返回類型 使用場景
BiFunction(T, U, R) T, U R 對類型爲T,U的參數應用操作,返回R類型的結果。接口定義的方法:R apply(T t, U u)
UnaryOperator(Function子接口) T T 對類型爲T的對象進行一 元運算, 並返回T類型的 結果。 包含方法爲 T apply(T t)
BinaryOperator (BiFunction 子接口) T, T T 對類型爲T的對象進行二 元運算, 並返回T類型的 結果。 包含方法爲 T apply(T t1, T t2)
BiConsumer<T, U> T, U void 對類型爲T, U 參數應用 操作。 包含方法爲 void accept(T t, U u)
ToIntFunction T int 計算int值的函數
ToLongFunction T long 計算long值的函數
ToDoubleFunction T double 計算double值的函數
IntFunction int R 參數爲int 類型的函數
LongFunction long R 參數爲 long類型的函數
DoubleFunction double R 參數爲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 stream() : 返回一個順序流

  • default Stream parallelStream() : 返回一個並行流

2.由數組創建Stream

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

  • static Stream 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 Stream of(T... values) : 返回一個流

4.由函數創建流

由函數創建流可以創建無限流。

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

  • 迭代

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

  • 生成

public static Stream generate(Supplier 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

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實用類提供了很多靜態方法,可以方便地創建常見收集器實例, 具體方法與實例如下表:

方法 返回類型 作用
toList List 把流中元素收集到List
toSet Set 把流中元素收集到Set
toCollection Collection 把流中元素收集到創建的集合
counting Long 計算流中元素的個數
summingInt Integer 對流中元素的整數屬性求和
averagingInt Double 計算流中元素Integer屬性的平均 值
summarizingInt IntSummaryStatistics 收集流中Integer屬性的統計值。 如:平均值
joining String 連接流中每個字符串
maxBy Optional 根據比較器選擇最大值
minBy Optional 根據比較器選擇最小值
reducing 歸約產生的類型 從一個作爲累加器的初始值 開始,利用BinaryOperator與 流中元素逐個結合,從而歸 約成單個值
collectingAndThen 轉換函數返回的類型 包裹另一個收集器,對其結 果轉換函數
groupingBy Map<K, List> 根據某屬性值對流分組,屬 性爲K,結果爲V
partitioningBy Map<Boolean, List> 根據true或false進行分區

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

方法 使用示例
toList List employees= list.stream().collect(Collectors.toList());
toSet Set employees= list.stream().collect(Collectors.toSet());
toCollection Collection employees=list.stream().collect(Collectors.toCollection(ArrayList::new));
counting long count = list.stream().collect(Collectors.counting());
summingInt int total=list.stream().collect(Collectors.summingInt(Employee::getSalary));
averagingInt double avg= list.stream().collect(Collectors.averagingInt(Employee::getSalary))
summarizingInt IntSummaryStatistics iss= list.stream().collect(Collectors.summarizingInt(Employee::getSalary));
Collectors String str= list.stream().map(Employee::getName).collect(Collectors.joining());
maxBy Optionalmax= list.stream().collect(Collectors.maxBy(comparingInt(Employee::getSalary)));
minBy Optional min = list.stream().collect(Collectors.minBy(comparingInt(Employee::getSalary)));
reducing int total=list.stream().collect(Collectors.reducing(0, Employee::getSalar, Integer::sum));
collectingAndThen int how= list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
groupingBy Map<Emp.Status, List> map= list.stream() .collect(Collectors.groupingBy(Employee::getStatus));
partitioningBy Map<Boolean,List>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 類(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,我拉你進羣,一起交流技術,一起進階,一起牛逼~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章