Java 8 聚合操作詳解

InfoQ - 趙永發表於2015-02-06

Oracle在2014年3月19日如期釋出了Java 8。Java 8版本被認為是具有里程碑意義的一個版本,Oracle在該版本中新增了許多新特性,包括Lambda表示式、方法引用、加強了安全等等。

在眾多的新特性中,聚合操作(Aggregate Operations)是針對集合類的一個比較大的變化。通過聚合操作,開發者可以更容易地使用Lambda表示式,並且更方便地實現對集合的查詢、遍歷、過濾以及常見計算等。

聚合操作與Java 8中的Lambda表示式、方法引用等新特性是相關的,一般一起組合使用,但這裡只說明聚合操作的使用,下面就聚合操作的使用進行簡單說明。

集合類的層次結構

集合類是Java語言提供的輔助類,是一種較為通用的資料結構,如Map、Set、List等。Java中集合類層次關係如下:

圖 1

如上圖,Collection是主要集合類的介面,其子介面(具化介面)有Deque、Queue、Set、List等。

Map是另一種型別的集合,以Key、Value的鍵值對儲存資料集。

在Java 8中,在java.util.Collection介面中新增了如下方法:

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

stream()方法的可見性修飾符為default,這又是Java 8的新特性。在介面中(Collection為interface),本不需要(也不能)進行方法實現,但引入default修飾後就不同了。開發者不但可以進行方法的實現,而且還不用考慮向後相容的問題。關於Default Method的詳細解釋,讀者可以參考Java 8的官方文件。

正是stream方法引出了集合類的聚合操作。

[注意]

Map介面中並沒有stream()方法,但是Map的values()和keySet()均返回集合物件,在集合物件上當然是可以使用stream()方法的。

聚合操作例項

為說明聚合操作的使用,首先定義一個資料元素類Person,如下:

import java.time.LocalDate;

public class Person {
		String name;
		LocalDate birthday;
		Sex gender;
		String emailAddress;

		public int getAge() {
			return LocalDate.now().getYear() - birthday.getYear();
		}

		public void setBirthday(LocalDate birthday){
			this.birthday = birthday;
		}

		public void setGender(Sex sex){
			this.gender = sex;
		}

		public void printPerson() {
			System.out.println("The name is " + name);
		}

		public Sex getGender(){
			return gender;
		}

		public enum Sex {
			MALE, FEMALE
		}
	}

在Java 8以前的版本中,對Person集合的遍歷往往採用以下方式:

Set<Person> persons = new HashSet<Person>();

//傳統遍歷方式 for (Person person : persons) { if (person.getAge() > 18) { System.out.println(person.name + ” is elder than 18.”); } }

同樣的功能,在Java 8中使用聚合操作,可以實現如下:

//使用聚合操作
persons.stream().filter(new Predicate<Person>() {
	   @Override
		public boolean test(Person person) {
			if (person.getAge() > 18) {
				return true;
			} else {
				return false;
			}
		}
	}).forEach(new Consumer<Person>() {
		@Override
		public void accept(Person person) {
			System.out.println(person.name + " is elder than 18.");
		}
	});

首先,在集合物件persons上呼叫stream()方法(聚合操作),取得person物件的資料集(elements),然後呼叫聚合操作filter()對集合中的元素進行過濾,再呼叫forEach()完成對符合條件的person的列印。

Predicate和Consumer為Java 8中定義的函式介面(Functional Interface),在java.util.function包下面,函式介面也是Java 8的新特性。在上述程式碼中,使用了兩個匿名類分別對Predicate和Consumer進行了實現,這兩個介面都只有一個方法,這也是函式介面的特徵之一。

上述程式碼中的寫法還是比較繁瑣的,為進一步簡化,可以使用Lambda表示式實現,如下:

// 使用聚合操作及Lambda
	persons.stream()
		.filter(p -> p.getAge() >= 18)
		.forEach(p -> System.out.println(p.name + " is elder than 18."));

因為filter()、forEach()的引數均為函式介面,所以可以替換為Lambda表示式的方式。簡單來理解,Lambda表示式就是允許開發者將程式碼邏輯作為引數進行傳遞,關於Lambda表示式的詳細內容,請參Java 8的官方文件。

聚合操作的使用

聚合操作是Java 8針對集合類,使程式設計更為便利的方式,可以與Lambda表示式一起使用,達到更加簡潔的目的。

前面例子中,對聚合操作的使用可以歸結為3個部分:

  1. 資料來源部分:通過stream()方法,取得集合物件的資料集。
  2. 通過一系列中間(Intermediate)方法,對資料集進行過濾、檢索等資料集的再次處理。如上例中,使用filter()方法來對資料集進行過濾。
  3. 通過最終(terminal)方法完成對資料集中元素的處理。如上例中,使用forEach()完成對過濾後元素的列印。

中間方法除了filter()外,還有distinct()、sorted()、map()等等,其一般是對資料集的整理(過濾、排序、匹配、抽取等等),返回值一般也是資料集。

最終方法往往是完成對資料集中資料的處理,如forEach(),還有allMatch()、anyMatch()、findAny()、findFirst(),數值計算類的方法有sum、max、min、average等等。最終方法也可以是對集合的處理,如reduce()、collect()等等。reduce()方法的處理方式一般是每次都產生新的資料集,而collect()方法是在原資料集的基礎上進行更新,過程中不產生新的資料集。

從上面的例子中可以看出,通過stream()方法,從集合物件獲取的資料集與集合物件的迭代器(Iterator)有些類似,但他們也不完全相同:

  1. 迭代器提供next()、hasNext()等方法,開發者可以自行控制對元素的處理,以及處理方式,但是隻能順序處理;
  2. stream()方法返回的資料集無next()等方法,開發者無法控制對元素的迭代,迭代方式是系統內部實現的,同時系統內的迭代也不一定是順序的,還可以並行,如parallelStream()方法。並行的方式在一些情況下,可以大幅提升處理的效率。

除上述介紹的聚合操作外,Java 8中還提供了其他更為豐富的聚合操作,讀者可以參考Java 8的開發參考,瞭解更多內容。

總結

Java 8提供的聚合操作,以及一起使用的Lambda表示式為開發者帶來了便利,尤其在面向邏輯易變、開發迭代較快的專案應用時。但筆者個人認為,在帶來方便的同時,可能也帶來了一些麻煩,如相同邏輯的複用,以及程式碼的查錯、修改等,當然這些問題也是相對而言的。畢竟,任何事物都有兩面性,技術在不斷的發展,Java也在不斷地調整自己的適應性,變得功能越來越多,越來越強大了。

相關文章