Java8新特性:lambda表達式(下)

3.lambda表達式高級拓展

方法引用

方法引用是結合lambda表達式的一組語法特性,在開發過程中方法引用配合lambda表達式可以對代碼進行簡化,但是相應的會損失掉一些可讀性。

方法引用具體分爲靜態方法引用、實例方法引用和構造方法引用。首先創建測試用類,

@Data
@AllArgsConstructor
@NoArgsConstructor
class Person {
	private String name, gender;
	private int age;

	// 靜態方法比較 age
	public static int compareAge(Person p1, Person p2) {
		return p1.getAge() - p2.getAge();
	}

	// 實例方法比較 Name
	public static int compareName(Person p1, Person p2) {
		return p1.getAge().hashCode() - p2.getAge().hashCode();
	}
}

此處使用 lombok語法支持,增加所有屬性的構造方法和空構造方法。

1)靜態方法引用的使用

  • 原始形式:類名.靜態方法名()
  • 引用形式:類名::靜態方法名
public class Test() {
	public static void main(String[] args) {
		List<Person> personList = new ArrayList<>();
		personList.add(new Person("a", "male", 16));
		personList.add(new Person("c", "female", 32));
		personList.add(new Person("de", "female", 34));

		// 使用匿名內部類的方式對 personList進行排序
		Collections.sort(personList, new Comparator() {
			@override
			public int compare(Person o1, Person o2) {
				return o1.getAge() - o2.getAge();
			}
		});
		System.out.println(personList);

		// lambda表達式實現
		Collections.sort(personList, (p1, p2) -> p1.getAge() - p2.getAge());
		System.out.println(personList);

		// 靜態方法引用
		Collections.sort(personList, Person::compareAge);
		System.out.println(personList);
	}
}

2)實例方法引用的使用

  • 原始形式:類實例對象.實例方法名()
  • 引用形式:類實例對象::實例方法名
public class Test() {
	public static void main(String[] args) {
		Person p = new Person();
		List<Person> personList = new ArrayList<>();
		personList.add(new Person("a", "male", 16));
		personList.add(new Person("c", "female", 32));
		personList.add(new Person("de", "female", 34));
		
		// 實例方法引用
		Collections.sort(personList, p::compareName);
		System.out.println(personList);
	}
}

3)構造方法引用的使用

與實例方法和靜態方法不同,構造方法的引用需要綁定函數式接口

創建一個函數式接口,用於初始化 Person類對象,

@FunctionalInterface
interface IPerson {
	Person initPerson(String name, String gender, int age);
}

public class Test() {
	public static void main(String[] args) {

		// 構造方法引用,綁定函數式接口 IPerson
		IPerson ip = Person::new;
		Person a = ip.initPerson("a", "male", 16);
	}
}

Stream

1)概述及演示

Stream流的引入是針對存儲多個數據的容器(如數組、集合等)在批量處理數據過程中的繁雜操作提出的API,可以通過結合lambda表達式通過串行和並行兩種方式完成對批量數據的增強操作。

public class Test() {
	public static void main(String[] args) {
		List<String> accounts = new ArrayList<>();
		accounts.add("tom");
		accounts.add("jerry");
		accounts.add("beta");

		// 用戶名大於3纔算有效賬號
		// 傳統方式遍歷集合
		for (String account: sccounts) {
			if (account.length() > 3)
				System.out.println(account);
		}

		// 通過迭代器進行處理(實際上for循環就是使用了迭代器)
		Iterator<String> it = accounts.iterator();
		while(it.hasNext()) {
			String account = it.next();
			if (account.length() > 3)
				System.out.println(account);
		}

		// 通過Stream流方式簡化代碼
		List validAccounts = accounts.stream().filter(s -> s.length()>3).collect(Collectors.toList());
		System.out.println(validAccounts);
	}
}

首先通過stream方法獲取相應數據的流對象,之後通過filter方法配合 lambda表達式對流中的數據進行過濾。最後以List的形式返回結果。

2)Stream API

Stream的處理流程可總結爲,
>>數據源 -> 數據轉換 -> 獲取結果

獲取Stream對象的方式有多種,舉例如下

  • 從集合中獲取,集合對象.stream()集合對象.parallelStream()方法可獲取集合的普通Stream對象和支持併發處理的Stream對象
  • 從數組中獲取,Arrays.stream(T[] t)方法可獲取數組的Stream對象
  • 通過緩衝流獲取Stream對象,如BufferReader對象.lines()方法
  • 通過靜態工廠方法獲取Stream對象,如 java.util.stream包中對基本類型都創建了流對象的構造器。java.nio.file中也提供了流對象的構造方法
  • 自定義流對象

Stream API大致可分爲兩個主要操作方式和一個輔助操作方式,

  1. 中間操作API,intermediate操作
    可以理解爲邏輯處理,操作結果是一個Steam對象,一個流程中可以有多個連續的中間操作。中間操作只記錄操作方式,不做具體執行,直到結束操作發生時纔對數據最終執行。
    **i.**無狀態:數據處理時不受之前的中間操作影響。主要包含 map/filter/parallel/sequential/unorder等操作
    **ii.**有狀態:數據處理時,受到前置中間操作影響。主要包含 distinct/sorted/limit/skip等操作
  2. 結束操作API,terminal操作
    一個Stream流對象的處理流程只能有一個結束操作。一旦這個操作被觸發,就會開啓數據處理的整個中間過程,最終生成結果。該過程是不可逆的。
    **i.**非短路操作:當前的Stream對象必須處理完集合中所有數據才能得到處理結果。注意包含forEach/forEachOrdweed/toArray/reduce/collect/min/max/count/iterator
    **ii.**短路操作:當前的Stream對象在處理過程中一旦滿足某個條件,就可以得到結果。主要包含anyMatch/allMatch/noneMatch/findFirst/findAny

3)Stream對象對集合處理

由批量數據得到Stream對象

public class Test {
	public static void main(String[] args) {
		// 多個數據得到stream
		Stream stream1 = Stream.of("adad", "dadads", "dewf");

		// 由數組得到Stream對象
		String[] strArrays = new String[] {"a", "c", "f"};
		Stream stream2 = Arrays.stream(strArrays);

		// 由列表得到Stream對象
		List<String> list = new ArrayList<>();
		list.add("dsad");
		list.add("dahdi");
		Stream stream3 = list.stream();

		// 由集合得到Stream對象
		Set<String> set = new HashSet<>();
		set.add("dsad");
		set.add("dahdi");
		Stream stream4 = set.stream();

		// 由Map得到Stream對象
		Map<String> map = new HashMap<>();
		map.put("dsad", 1000);
		map.put("dahdi", 1200);
		Stream stream5 = map.entrySet().stream();
	}
}

Stream對象對基本數據類型的底層封裝

jdk8中目前只針對基本類型中最常使用的 int、long和double類型進行了封裝。以 int類型爲例,

public class Test {
	public static void main(String[] args) {
		Stream stream = IntStream.of(new int[] {20, 30, 40});
		stream.foreach(System.out::println);

		IntStream.range(1, 5).forEach(System.out::println);		// 輸出 1,2,3,4
		
	}
}

Stream對象轉換得到指定數據類型

public class Test {
	public static void main(String[] args) {

		// 由Stream對象得到數組
		String[] strArrays = new String[] {"a", "c", "f"};
		Stream stream2 = Arrays.stream(strArrays);
		String strArr = stream2.toArray(String::new);

		// 由Stream對象得到字符串,拼接對象中的元素
		strArrays = new String[] {"a", "c", "f"};
		stream2 = Arrays.stream(strArrays);
		String str = stream2.collect(Collectors.joining()).toString();
		
		// 由Stream對象得到列表
		List<String> list = new ArrayList<>();
		list.add("dsad");
		list.add("dahdi");
		Stream stream3 = list.stream();
		List<String> strList = (List<String>) stream3.collect(Collectors.toList());

		// 由Stream對象得到集合
		Set<String> set = new HashSet<>();
		set.add("dsad");
		set.add("dahdi");
		Stream stream4 = set.stream();
		set<String> strSet = (Set<String>) stream3.collect(Collectors.toSet());


		// 由Stream對象得到Map
		strArrays = new String[] {"a", "c", "f"};
		stream2 = Arrays.stream(strArrays);
		Map<String, Integer> strMap = Map<String> stream2.collect(Collectors.toMap(x->x, y->"haha:"+y));
		System.out.println(strMap);		// 得到 ["a"="haha:a", "b"="haha:b", "c"="haha:c"]
	}
}

Stream中常見API操作

public class Test {
	public static void main(String[] args) {

		List<String> accountList = new ArrayList<>();
		accountList.add("songjiang");
		accountList.add("linchong");
		accountList.add("luzhishen");
		
		// map()中間操作,接收一個FunctionalInterface,對數據逐個操作
		accountList = accountList.stream().map(x->"梁山好漢:"+x).collect(Collectors.toList());

		// filter()過濾
		accountList = accountList.stream().filter(x->x.length()>5).collect(Collectors.toList());

		// forEach 增強for循環
		accountList.forEach(x->System.out.println("forEach->"+x));
		
		// 如果需要多次循環建議不要使用多次的forEach,因爲多次調用forEach實際上是開啓了多次Stream操作
		// 建議使用peek()完成多次循環遍歷,在一次遍歷過程中完成所有步驟的操作(將多次循環合併)
		accountList.stream().peek(x->"peek1:"+x)
				   .peek(x->"peek2:"+x).forEach(System.out::println);
		
	}
}

Stream中對數字運算的支持

public class Test {
	public static void main(String[] args) {
		List<Integer> intList = new ArrayList<>();
		intList.add(20);
		intList.add(19);
		intList.add(7);
		intList.add(8);
		intList.add(86);
		intList.add(11);
		intList.add(3);
		intList.add(20);

		// skip跳過部分數據
		intList.stream().skip(3).forEach(System.out::println);	// 跳過前三個數據

		// limit顯著輸出數據數目
		intList.stream().skip(3).limit(2).forEach(System.out::println);		// 跳過前三個數據且只對之後兩個數據進行處理

		// distinct剔除重複數據
		intList.stream().distinct().forEach(System.out::println);

		// sorted排序
		intList.stream().sorted().forEach(System.out::println);

		// max、min獲取極值,需傳入一個Comparator
		Optional optional = intList.stream().max((x, y) -> x - y);
		System.out.println(optional.get());

		// reduce進行合併操作
		optional = intList.stream().reduce((cumSum, x) -> cumSum + x);
		System.out.println(optional.get());
	}
}

4)Stream性能

  1. 在基本數據類型的操作上(如 ArrayList<Integer>)建議使用迭代器、增強for循環和普通for循環。如果是多核環境,可以使用parallelStream()並行處理能夠有效提升性能
  2. 當面對複雜數據的處理時(如 ArrayList<className>),並行Stream在多核環境下可以有效提升數據處理的性能

5)並行Stream的線程安全

並行Stream對象底層原理是將大任務拆分爲多個子任務執行。

public class Test {
	public static void main(String[] args) {
		List<Integer> list = new ArrayList<>();
		for (int i=0; i<1000; i++) {
			list.add(i);
		}

		// 將list中的元素複製到另一個List
		// 串行Stream
		List list2 = new ArrayList<>();
		list.stream().forEach(x -> list2.add(x));
		System.out.println(list.size());
		System.out.println(list2.size());
		// 並行Stream
		List list3 = new ArrayList<>();
		list.parallelStream().forEach(x -> list3.add(x));
		System.out.println(list3.size());
	}
}

上述代碼的運行結果爲,

1000
1000
995

出現了數據丟失的情況,原因是 ArrayList類並非是線程安全的類,且 Stream API中明確寫明forEach方法並不是線程安全的。解決這一問題可以使用 Stream API中提供的,在官方文檔中明確說明的並行情況下保證線程安全的方法。如

public class Test {
	public static void main(String[] args) {
		List<Integer> list = new ArrayList<>();
		for (int i=0; i<1000; i++) {
			list.add(i);
		}

		// collect方法用於收集最終的Stream處理結果,是線程安全的方法
		List<Integer> list4 = list.parallelStream().collect(Collectors.toList());
		System.out.println(list4.size());
	}
}

此外更簡單的解決方法是使用線程安全的併發集合類。

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