lambda 表示式
簡介
在我看來 lambda 表示式就是簡化了以前的一大堆繁瑣的操作,讓我們程式碼看起來更加簡潔,讓以前五六行甚至更多行的程式碼只需要兩三行就能解決,但是對於 Java 初學者可能不是特別友好,可能一下子理解不過來該程式碼想表達什麼。
lambda 表示式是一段可以傳遞的程式碼,因此它可以被執行一次或多次
lambda 表示式的語法
我們先來看看老版本的排序字串的辦法,這裡我們不按照字典序,而按照字串的大小來排序
Comparator<String> comparator = new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return Integer.compare(o1.length(),o2.length());
}
};
List<String> list = Arrays.asList("aaaa", "aaa", "aa", "a", "aaaaa");
Collections.sort(list,comparator);// [a, aa, aaa, aaaa, aaaaa]
老版本的排序我們會先建立一個自定義比較器,然後按照比較器的規則進行排序。
現在我們來看看 lambda 表示式如何來實現的
List<String> list = Arrays.asList("aaaa", "aaa", "aa", "a", "aaaaa");
Collections.sort(list,(String o1,String o2)->{
return Integer.compare(o1.length(),o2.length());
});//[a, aa, aaa, aaaa, aaaaa]
可以看到,程式碼濃縮了不少,但是可讀性沒有原來好,原來需要先建立一個比較器然後將比較器傳到 Collections 工具類進行排序,典型的物件導向程式設計,但是 lambda 表示式確是將程式碼傳進去然後直接進行比較。如果你以為這樣就是最簡便的,那你就錯了,有更簡便的。
List<String> list = Arrays.asList("aaaa", "aaa", "aa", "a", "aaaaa");
Collections.sort(list,(String o1,String o2)
->Integer.compare(o1.length(),o2.length()));
如果返回值只有一行,可以省略大括號和 return 關鍵字。
List<String> list = Arrays.asList("aaaa", "aaa", "aa", "a", "aaaaa");
Collections.sort(list,(o1,o2)->Integer.compare(o1.length(),o2.length()));
如果是帶泛型的容器,引數的型別可以省略,JVM 會自己進行上下文判斷出型別。
我們可以看到從原來的那麼多行程式碼濃縮成了一行,看著清爽了很多,但是可讀性卻沒有原來那麼友好了。
變數作用域
訪問區域性變數
可以在 lambda 表示式中訪問外部的區域性變數
int number = 10; Converter<String,Integer> converter = num->Integer.valueOf(num + number); System.out.println(converter.convert("123"));//12310
在匿名內部類中外部的區域性變數必須宣告為 final。而我們這裡不需要。
但是要注意的是這的 number 不能被後面的程式碼修改,否則編譯不通過,也就是具有隱性的 final 語義。
訪問 欄位和靜態變數
我們對 lambda 表示式中的例項欄位和靜態欄位變數都有讀寫訪問許可權。
class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter<Integer, String> stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter<Integer, String> stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
無法在 lambda 表示式中訪問預設介面方法。
函式式介面
在 Java 中有許多已有的介面都選哦封裝成程式碼塊,比如 Runnable 或者 Comparator 。 lambda 表示式與這些介面是像後相容的。
對於只包含一個抽象方法的介面,但是可以有多個非抽象方法,(非抽象方法也是 java 8 新特性,我們後面會講到),我們可以通過 lambda 表示式來建立該介面的物件。這種介面被稱為函式式介面。
Java 8 新增加了一種特殊的註解 @FunctionalInterface,該介面會自動判斷你的介面中是否只有一個抽象方法,如果多於一個抽象方法就會報錯。
現在我們來自定義一個函式式介面:
@FunctionalInterface
interface Converter<F,T>{
T convert(F num);
}
// 將數字形式的字串轉化成整型
Converter<String,Integer> converter = (num -> Integer.valueOf(num));
System.out.println(converter.convert("123").getClass());//class java.lang.Integer
現在來解釋下該程式碼,在該程式碼中我們的函式式介面中定義了一個方法,該方法能夠實現傳入一個 F 型別的引數,我們可以對這個型別的引數進行各種處理,最後返回一個 T 型別的結果。在這裡我只是簡單的將傳進來的 string 轉成了 integer。這裡的 F 與 T 都是泛型型別,可以為任何實體類。
java 8 幫我們實現了很多函式式介面,大部分都不需要我們自己寫,這些介面在 java.util.function 包 裡,可以自行進行查閱。
上面的程式碼可以寫的更加簡單:
Converter<String,Integer> converter = Integer::valueOf;
System.out.println(converter.convert("123").getClass());//class java.lang.Integer
java 8 可以通過 ** : : **來傳遞方法或者建構函式的引用。上面的演示瞭如果引用靜態方法,引用物件方法也相差不大,只是需要宣告一個物件:
class Demo{
public Integer demo(String num){
return Integer.valueOf(num);
}
}
public class Main {
public static void main(String[] args) {
Demo demo = new Demo();
Converter<String,Integer> converter = demo::demo;
System.out.println(converter.convert("123").getClass());
//class java.lang.Integer
}
}
內建函式式介面
Predicates
Predicate 介面是隻有一個引數的返回布林型別值的 斷言型 介面。該介面包含多種預設方法來將 Predicate 組合成其他複雜的邏輯(比如:與,或,非)
@FunctionalInterface public interface Predicate<T> { // 該方法是接受一個傳入型別,返回一個布林值.此方法應用於判斷. boolean test(T t); .... }
Functions
Function 介面接受一個引數並生成結果。預設方法可用於將多個函式連結在一起(compose, andThen)
@FunctionalInterface public interface Function<T, R> { //將Function物件應用到輸入的引數上,然後返回計算結果。 R apply(T t); ... }
Suppliers
Supplier 介面產生給定泛型型別的結果。 與 Function 介面不同,Supplier 介面不接受引數。
Consumers
Consumer 介面表示要對單個輸入引數執行的操作。
Comparators
Comparator 是老Java中的經典介面, Java 8在此之上新增了多種預設方法
預設方法
前面已經有寫地方提到了介面的預設方法,這裡對其做下介紹。介面的預設方法也是 java 8 新出的功能。能夠通過使用 default
關鍵字向介面新增非抽象方法實現。
interface Formula{
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
Formula 介面中除了抽象方法計算介面公式還定義了預設方法 sqrt
。 實現該介面的類只需要實現抽象方法 calculate
。 預設方法sqrt
可以直接使用。當然你也可以直接通過介面建立物件,然後實現介面中的預設方法就可以了,我們通過程式碼演示一下這種方式。
public class Main {
public static void main(String[] args) {
// TODO 通過匿名內部類方式訪問介面
Formula formula = new Formula() {
@Override
public double calculate(int a) {
return sqrt(a * 100);
}
};
System.out.println(formula.calculate(100)); // 100.0
System.out.println(formula.sqrt(16)); // 4.0
}
}
formula 是作為匿名物件實現的。該程式碼非常容易理解,6行程式碼實現了計算 sqrt(a * 100)
。
不管是抽象類還是介面,都可以通過匿名內部類的方式訪問。不能通過抽象類或者介面直接建立物件。對於上面通過匿名內部類方式訪問介面,我們可以這樣理解:一個內部類實現了介面裡的抽象方法並且返回一個內部類物件,之後我們讓介面的引用來指向這個物件。
Stream(流)
Stream 是在 java.util 下的。Stream 表示能應用在一組元素上一次執行的操作序列。Stream 操作分為中間操作或者最終操作兩種,最終操作返回一特定型別的計算結果,而中間操作返回 Stream 本身,這樣我們就可以將多個操作依次串起來。Stream 的建立需要指定一個資料來源,比如java.util.Collection
的子類:List 或者 Set。Map 不支援。Stream 的操作可以序列執行或者並行執行。
當我們使用 Stream 時,我們將通過三個階段來建立一個操作流水線。
- 建立一個 Stream。
- 在一個或多個步驟中,指定將初始 Stream 轉換成為另一個 Stream 的中間操作。
- 使用一個終止操作來產生一個結果。該操作會強制它之前的延遲操作立即執行。
在這之後 stream 就不會再被使用了。
建立 stream
通過 Java 8 在 Collection 介面中新提娜佳的 stram 方法,可以將任何集合轉化為一個 Stream。如果我們面對的是一個陣列,也可以用靜態的 Stream.of 方法將其轉化為一個 Stream。
@Test
public void test1(){
List<String> stringList = new ArrayList<>();
stringList.add("ddd2");
stringList.add("aaa2");
stringList.add("bbb1");
stringList.add("aaa1");
stringList.add("bbb3");
stringList.add("ccc");
stringList.add("bbb2");
stringList.add("ddd1");
Stream<String> stream = stringList.stream();
//Stream<String> stringStream = stringList.parallelStream();
}
我們可以通過 Collection.stream() 或者 Collection.parallelStream() 來建立一個Stream。
Filter(過濾)
過濾通過一個predicate介面來過濾並只保留符合條件的元素,該操作屬於中間操作,所以我們可以在過濾後的結果來應用其他Stream操作。(比如forEach)。forEach需要一個函式來對過濾後的元素依次執行。forEach是一個最終操作,所以我們不能在forEach之後來執行其他Stream操作。
stringList
.stream()
.filter(s->s.startsWith("a"))
.forEach(System.out::println);
forEach 是為 Lambda 而設計的,保持了最緊湊的風格。而且 Lambda 表示式本身是可以重用的,非常方便。
Sorted(排序)
排序是一箇中間操作,返回的是排序好的 Stream 。如果我們不指定一個自定義的 Comparator 則會使用預設排序。
stringList
.stream()
.sorted((o1,o2)->Integer.compare(o1.length(),o2.length()))
.forEach(System.out::println);
需要注意的是,排序只建立了一個排列好後的Stream,而不會影響原有的資料來源,排序之後原資料stringCollection是不會被修改的。
Map(對映)
中間操作 map 會將元素根據指定的 Function 介面來依次將元素轉成另外的物件。
map返回的 Stream 型別是根據我們 map 傳遞進去的函式的返回值決定的。
stringList
.stream()
.map(String::toUpperCase)
.sorted((o1,o2)->Integer.compare(o1.length(),o2.length()))
.forEach(System.out::println);
Match(匹配)
Stream提供了多種匹配操作,允許檢測指定的Predicate是否匹配整個Stream。所有的匹配操作都是 最終操作 ,並返回一個 boolean 型別的值。
boolean anyStartsWithA =
stringList
.stream()
.anyMatch((s) -> s.startsWith("a"));
System.out.println(anyStartsWithA); // true
boolean allStartsWithA =
stringList
.stream()
.allMatch((s) -> s.startsWith("a"));
System.out.println(allStartsWithA); // false
boolean noneStartsWithZ =
stringList
.stream()
.noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ); // true
Count(計數)
計數是一個 最終操作,返回Stream中元素的個數,返回值型別是 long。
long count = stringList
.stream()
.map(String::toUpperCase)
.sorted((o1, o2) -> Integer.compare(o1.length(), o2.length()))
.count();
System.out.println(count);
Parallel Stream(並行流)
Stream有序列和並行兩種,序列Stream上的操作是在一個執行緒中依次完成,而並行Stream則是在多個執行緒上同時執行。
下面使用序列流和並行流為一個大容器進行排序,比較兩者效能。
序列排序
@Test
public void test1(){
int max = 1000000;
List<String> list = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
UUID uuid = UUID.randomUUID();
list.add(uuid.toString());
}
long startTime = System.nanoTime();
long count = list.stream().sorted().count();
System.out.println(count);
long endTime = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(endTime-startTime);
System.out.println(millis);
//1000000
//877
}
並行排序
@Test
public void test2(){
int max = 1000000;
List<String> list = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
UUID uuid = UUID.randomUUID();
list.add(uuid.toString());
}
long startTime = System.nanoTime();
long count = list.parallelStream().sorted().count();
System.out.println(count);
long endTime = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(endTime-startTime);
System.out.println(millis);
}
//1000000
//512
可以明顯看出在大資料量的情況下並行排序比序列來的快。但是小資料量的話卻是序列排序比較快,原因是並行需要涉及到上下文切換。
Collector 和 Collectors
Collector 是專門用來作為 Stream 的 collect 方法的引數的。而 Collectors 是作為生產具體 Collector 的工具類。
toList():將流構造成 list
List<String> collect = list.stream().collect(Collectors.toList());
toSet():將流構造成set
Set<String> set = list.stream().collect(Collectors.toSet()); Set<String> treeSet = list.stream().collect(Collectors.toCollection(TreeSet::new));
joining():拼接流中所有字串
String collect = list.stream().collect(Collectors.joining()); String collect = list.stream().collect(Collectors.joining(";"));
toMap():將流轉成 map
Map<String, String> collect = list .stream() .collect(Collectors .toMap(e -> "key" + e, e -> "v" + e,(a,b)->b,HashMap::new));
上面的 e -> "key" + e 定義了 map 的 key 的生成規則,e -> "v" + e 定義了 map 的 value 的生成規則,(a,b)->b 表示衝突的解決方案,如果鍵 a 和 鍵 b 衝突了則該鍵鍵值取 b 的,HashMap::new 定義了生成的 map 為 hashmap。
Map 新方法
Map 雖然不支援 Stream 但是我們可以通過 map.keySet().stream()
,map.values().stream()
和map.entrySet().stream()
來通過過去鍵、值的集合再轉換成流進行處理。
Java 8 中 map 新方法:
putIfAbsent(key, value)//有則不加,無則加
map.forEach((key, value) -> System.out.println(value));//迴圈列印
map.computeIfPresent(3, (num, val) -> val + num);//當key 存在則執行後面方法
map.computeIfAbsent(23, num -> "val" + num);//當key 不存在時執行後面方法
map.getOrDefault(42, 1);//有則獲取,無則置 1
map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); //如果鍵名不存在則插入,否則則對原鍵對應的值做合併操作並重新插入到map中。
新的日期與時間 API
LocalTime(本地時間)
LocalTime 定義了一個沒有時區資訊的時間
方法 描述 now,of 這些靜態方法可以根據當前時間或指定的年、月、日來建立一個 LocalTime 物件 getHour,getMinute,getSecond,getNano 獲得當前 LocalTime 的小時、分鐘、秒鐘及微妙值 isBefore,isAfter 比較兩個LocalTime LocalDate(本地日期)
LocalDate 表示了一個確切的日期,比如 2014-03-11。該物件值是不可變的,用起來和LocalTime基本一致。
方法 描述 now,of 這些靜態方法可以根據當前時間或指定的年、月、日來建立一個LocalDate物件 getDayOfMonth 獲取月份天數(在 1~ 31 之間) getDayOfYear 獲取年份天數(在1~366之間) getMonth,getMonthValue 獲得月份,或者為一個 Month 列舉的值,或者是 1 ~ 12 之間的一個數字 getYear 獲取年份 isBefore,isAfter 比較兩個LocalDate
上面這些方法是比較常用的,其餘的可以自行查閱。