jdk1.8筆記(2)-Stream流及方法引用

1. Stream流

在java8中,得益於Lambda所帶來的函數式變成,引入一個***全新的Stream概念***,用於解決已有集合類庫既有的弊端。

1.1 傳統集合

1.1.1 傳統集合操作多步遍歷代碼

/**
 * 用傳統的方式,遍歷集合,對集合中的元素進行過濾
 * 從集合中查詢出以張開頭且姓名長度爲3的人,存儲到一個新的集合中
 * @author kevin
 *
 */
public class TestList1 {

	public static void main(String[] args) {
		
		//創建一個List集合,存儲姓名
		List<String> list = new ArrayList<>();
		list.add("張無忌");
		list.add("周芷若");
		list.add("趙敏");
		list.add("張強");
		list.add("張三丰");
		
		//對List集合中的元素進行過濾,只要以張開頭的元素,存儲到一個新的集合中
		List<String> listA = new ArrayList<>();
		for (String str : list) {
			if(str.startsWith("張")) {
				listA.add(str);
			}
		}
		
		//對listA集合進行過濾,只要姓名長度爲3的人,存儲到一個新的集合中
		List<String> listB = new ArrayList<>();
		for (String str : listA) {
			if(str.length() == 3) {
				listB.add(str);
			}
		}
		
		//打印輸出listB集合
		for (String str : listB) {
			System.out.println(str);
		}

	}

}

輸出結果爲:

張無忌
張三丰

1.1.2 循環遍歷的弊端

Java8的Lambda讓我們可以更加專注於***做什麼***(What),而不是***怎麼做***(How)。

  • for循環的語法就是“怎麼做
  • for循環的循環體纔是"做什麼"

爲什麼使用循環?因爲要進行遍歷,但循環是遍歷的唯一方式嗎?遍歷是指每一個元素逐一進行處理,而***並不是從第一個到最後一個順序處理的循環***。前者是目的,後者是方式

1.1.3 Stream的更優寫法

/**
 * 使用Stream流的方式,遍歷集合,對集合中的數據進行過濾
 * Stream流是JDK1.8之後出現的
 * 關注的是做什麼,而不是怎麼做
 * @author kevin
 *
 */
public class TestList2 {

	public static void main(String[] args) {
		
		//創建一個List集合,存儲姓名
		List<String> list = new ArrayList<>();
		list.add("張無忌");
		list.add("周芷若");
		list.add("趙敏");
		list.add("張強");
		list.add("張三丰");
		
		list.stream()
		.filter(name -> name.startsWith("張"))
		.filter(name -> name.length() == 3)
		.forEach(name -> System.out.println(name));

	}

}

1.2 流式思想描述

類似於工廠車間的“生產流水線

拼好一個“模型”步驟方案,然後再按照方案去執行它;方案就是一種“函數模型”。

而***模型***每一步都是一個***流***,調用指定的方法,可以從一個流模型轉換爲另一個流模型,直到最終步驟得到結果。

中間步驟對函數模型的操作,集合元素並沒有被真正處理,只有當終結方法執行時候,整個模型纔會按照指定策略執行操作,而這得益於Lambda的延遲執行特性。

“Stream流”其實是一個集合元素的函數模型,它並不是集合,也不是數據結構,其本身並不存儲任何元素(或其地址值)。

Stream(流)是一個來自數據源的元素隊列

  • 元素是特定類型的對象,形成一個隊列。Java中的Stream並不會存儲元素,而是按需計算。
  • 數據源 流的來源,可以是集合,數組等。

和以前的Collection操作不同,Stream操作還有兩個基礎的特徵:

  • Pipelining:中操作都會返回流對象本身,這樣多個操作可以串聯成一個管道,如圖流式風格(fluent style),這樣做可以操作進行優化,比如延遲執行(laziness)和短路(short-circuting)
  • 內部迭代:以前對集合遍歷都是通過Iterator或者增強for的方式。顯示的在集合外部進行迭代,這叫做外部迭代。Stream提供了內部迭代的方式,流可以直接調用遍歷方法。

當使用一個流的時候,通常包括三個基本步驟:獲取一個數據源(source)->數據轉換->執行操作獲取想要的結果,每次轉換原有Stream對象不改變,返回一個新的Stream對象(可以有多次轉換),這就允許對其操作可以像鏈條一樣排列,變成一個管道。

1.3 獲取流

java.util.stream.Stream<T>是Java8新加入的最常用的流接口。(這並不是一個函數式接口)

有以下幾種常用的方式獲取流:

  • 所有的Collection集合都可以通過stream默認方法獲取流
default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}
  • Stream接口的靜態方法of可以獲取數組對應的流
@SafeVarargs
@SuppressWarnings("varargs") // Creating a stream from an array is safe
public static<T> Stream<T> of(T... values) {
    return Arrays.stream(values);
}

參數是一個可變參數,那麼我們就可以傳遞一個數組

根據Collection獲取流

根據Map獲取流

根據數組獲取流

例子:獲取流Demo

public class TestGetStream {

	public static void main(String[] args) {
		//把集合轉換爲Stream流
		List<String> list = new ArrayList<>();
		Stream<String> streamList = list.stream();
		
		Set<String> set = new HashSet<>();
		Stream<String> streamSet = set.stream();
		
		Map<String, String> map = new HashMap<>();
		//獲取鍵,存儲到一個Set集合中
		Set<String> keySet = map.keySet();
		Stream<String> streamKeySet = keySet.stream();
		
		//獲取值,存儲到一個Collection集合中
		Collection<String> values = map.values();
		Stream<String> streamValues = values.stream();
		
		//獲取鍵值對(鍵與值的映射關係 entrySet)
		Set<Map.Entry<String, String>> entries = map.entrySet();
		Stream<Map.Entry<String, String>> streamEntries = entries.stream();
		
		//將數組轉換爲Stream流
		Stream<Integer> stream = Stream.of(1,2,3,4,5);
		
		//可變參數可以傳遞數組
		Integer[] arr = {1,2,3,4,5};
		Stream<Integer> streamArr1 = Stream.of(arr);
		
		String[] arr1 = {"aaa","bbb","ccc"};
		Stream<String> streamArr2 = Stream.of(arr1);
		

	}

}

1.4 常用方法

流模型的操作很豐富,這裏介紹一些常用的API。這些方法可以被分爲兩種:

[外鏈圖片轉存失敗(img-nLd7wneM-1562089104671)(E:\學習整理\java\jdk1.8新特性\img\stream方法1.png)]

  • 延遲方法:返回值類型仍然是Stream接口自身類型的方法,因此支持鏈式調用。(除了終結方法外,其餘方法均爲延遲方法)
  • 終結方法:返回值類型不再是Stream接口自身類型的方法,因此不能支持類似StringBuilder那樣的鏈式調用。包括countforEach方法等

1.4.1 逐一處理:forEach

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

該方法接收一個Consumer接口函數,會將每一個流元素交給該函數進行處理。

java.util.function.Consumer接口是一個消費型接口。

Consumer接口包含抽象方法void accept(T t),意爲消費一個指定泛型的數據。

例子:

/**
 * forEach方法用來遍歷流中的數據
 * 是一個終結方法,遍歷之後就不能繼續使用Stream流中的其他方法
 * @author kevin
 *
 */
public class TestForEach {

	public static void main(String[] args) {
		//獲取一個Stream流
		Stream<String> stream = Stream.of("Lily","Helena","Lucy","Simon");
		
		//使用Stream流中的forEach對Stream流中的數據進行遍歷
		/*stream.forEach((String str) -> {
			System.out.println(str);
		});*/
		
		stream.forEach(str -> System.out.println(str));
	}

}

輸出結果爲:

Lily
Helena
Lucy
Simon

1.4.2 過濾:filter

可以通過filter方法將一個流轉換成另一個子集流,方法簽名:

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

java.util.function.Predicate接口,其中唯一的抽象方法 boolean test(T t),該方法會產生一個boolean值結果,代表指定的條件是否滿足。如果結果爲true,那麼Stream流的filter方法將會留用元素;如果結果爲false,那麼filter方法將會捨棄元素。

例子:

public class TestFilter {

	public static void main(String[] args) {
		//創建一個Stream流
		Stream<String> stream = Stream.of("Lily","Helena","Lucy","Simon");
		
		//對Stream流中的元素進行過濾,只要以“L”開頭的
		Stream<String> stream1 = stream.filter(str -> str.startsWith("L"));
		
		//遍歷stream1流
		stream1.forEach(name -> System.out.println(name));
		
		/**
		 * Stream流屬於管道流,只能被消息(使用)一次
		 * 第一個Stream流調用完畢方法,數據就會流轉到下一個Stream上
		 * 而這時第一個Stream流已經使用完畢,就會關閉了
		 * 所以第一個Stream流就不能再調用方法了
		 * java.lang.IllegalStateException: stream has already been operated upon or closed
		 */
		//stream.forEach(name -> System.out.println(name));

	}

}

輸出結果爲:

Lily
Lucy

1.4.3 映射:map

將流中的元素映射到另外一個流中,方法簽名爲:

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

java.util.function.Function<T, R>接口

抽象方法 R apply(T t); 意爲將一種T類型轉換成R類型,而這種轉換的操作,就成爲“映射”

例子:

public class TestMap {
	
	public static void main(String[] args) {
		//獲取一個String類型的Stream流
		Stream<String> stream = Stream.of("1","2","3","4");
		
		//使用map方法,將字符串類型的整數,轉換(映射)爲Integer類型的整數
		Stream<Integer> stream1 = stream.map((String str) -> {
			return Integer.parseInt(str);
		});
		
		//遍歷Stream流
		stream1.forEach(i -> System.out.println(i));
	}
	
	

}

1.4.4 統計個數:count

如集合Collection中的size方法一樣,流提供count方法來返回其中的元素個數:

long count();

該方法返回一個long值代表元素個數(不像集合Collection那樣是int值)。

例子:

public class TestCount {

	public static void main(String[] args) {
		List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
		Stream<Integer> stream = list.stream();
		
		long count = stream.count();
		System.out.println(count);//7
		

	}

}

1.4.5 取用前幾個:limit

limit方法可以對流進行截取,只取用前幾個,方法簽名:

Stream<T> limit(long maxSize);

參數是一個long類型,如果集合當前長度大於參數則進行截取;否則不進行操作。

例子:

public class TestLimit {

	public static void main(String[] args) {
		String[] arr = {"aaa","bbb","ccc","ddd","eee"};
		Stream<String> stream = Stream.of(arr);
		
		//使用limit對Stream流中的元素進行截取,只要前3個元素
		Stream<String> stream2 = stream.limit(3);
		
		stream2.forEach(str -> System.out.println(str));

	}

}

輸出結果爲:

aaa
bbb
ccc

1.4.6 跳過前幾個:skip

如果需要跳過前n個元素,可以使用skip方法獲取一個截取之後的新流:

Stream<T> skip(long n);

如果流的長度大於n,則跳過前n個;否則將會得到一個長度爲0的空流。

例子:

public class TestSkip {
	
	public static void main(String[] args) {
		String[] arr = {"aaa","bbb","ccc","ddd","eee"};
		Stream<String> stream = Stream.of(arr);
		
		//使用skip方法跳過前3個元素
		Stream<String> stream2 = stream.skip(3);
		
		stream2.forEach(str -> System.out.println(str));

	}

}

輸出結果爲:

ddd
eee

1.4.7 組合:concat

如果有兩個流,希望合併成爲一個流,那麼可以使用Stream接口的靜態方法concat:

public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {
    Objects.requireNonNull(a);
    Objects.requireNonNull(b);

    @SuppressWarnings("unchecked")
    Spliterator<T> split = new Streams.ConcatSpliterator.OfRef<>(
        (Spliterator<T>) a.spliterator(), (Spliterator<T>) b.spliterator());
    Stream<T> stream = StreamSupport.stream(split, a.isParallel() || b.isParallel());
    return stream.onClose(Streams.composedClose(a, b));
}

備註:這是一個靜態方法,與java.lang.String當中的concat方法是不同的。

例子:

public class TestConcat {

	public static void main(String[] args) {
		String[] arr = {"aaa","bbb","ccc","ddd","eee"};
		Stream<String> stream1 = Stream.of(arr);
		
		Stream<String> stream2 = Stream.of("Lily","Lucy","Helena","Simon");
		
		//把以上兩個流組合爲一個流
		Stream<String> stream = Stream.concat(stream1, stream2);
		
		//遍歷concat流
		stream.forEach(str -> System.out.println(str));

	}

}

輸出結果爲:

aaa
bbb
ccc
ddd
eee
Lily
Lucy
Helena
Simon

2. 方法引用

在使用Lambda表達式的時候,我們實際上傳進去的代碼就是一種解決方案:拿什麼參數進行什麼操作。

如果已有地方存在相同的操作方案,這樣就會導致重複寫。可以使用方法引用來優化。

例子:

  • 函數式接口 Printable.java
@FunctionalInterface
public interface Printable {
	
	void print(String s);

}
  • 測試類 TestPrintable.java
public class TestPrintable {
	
	public static void printString(Printable p) {
		p.print("Hello World");
	}

	public static void main(String[] args) {
		printString((s) -> {
			System.out.println(s);
		});
		
		
		/*
		 * 分析:
		 * 	Lambda表達式的目的,打印參數傳遞的字符串
		 * 	把參數s,傳遞給了System.out對象,調用out對象中的方法println對字符串進行了輸出
		 * 	注意:
		 * 		1.System.out對象是已經存在的
		 * 		2.println方法也是已經存在的
		 * 	所以我們可以使用方法引用來優化Lambda表達式
		 * 	可以使用System.out方法直接引用(調用)println方法
		 */
		printString(System.out :: println);

	}

}

輸出結果爲:

Hello World
Hello World

2.1 方法引用符

雙冒號::爲引用運算符,而它所在的表達式被稱爲方法引用。如果Lambda要表達的函數方案已經存在於某個方法實現中,那麼則可以通過雙冒號來引用該方法作爲Lambda的替代者。

2.2 通過對象名引用成員方法

  • 函數式接口 Printable.java
@FunctionalInterface
public interface Printable {
	
	void print(String s);

}
  • MethodRerObject.java
public class MethodRerObject {
	//定義一個成員方法,傳遞字符串,把字符串按照大寫輸出
	public void printUpperCaseString(String str) {
		System.out.println(str.toUpperCase());
	}

}
  • 測試類 TestPrintable2.java
/**
 * 通過對象名引用成員方法
 * 使用前提是對象名是已經存在的,成員方法也是已經存在
 * 就可以使用對象名來引用成員方法
 * @author kevin
 *
 */
public class TestPrintable2 {
	//定義一個方法,方法的參數傳遞Printable接口
	public static void printString(Printable p) {
		p.print("Hello World");
	}

	public static void main(String[] args) {
		//調用printString方法,方法的參數Printable是一個函數式接口,所以可以傳遞Lambda表達式
		printString((s) -> {
			//創建MethodRerObject對象
			MethodRerObject m = new MethodRerObject();
			//調用MethodRerObject對象中的成員方法printUpperCaseString,把字符串按照大寫輸出
			m.printUpperCaseString(s);
		});
		
		/*
		 * 使用方法引用優化Lambda
		 * 對象是已經存在的MethodRerObject
		 * 成員方法也是已經存在的printUpperCaseString
		 * 所以我們可以使用對象名引用成員方法
		 */
		//創建MethodRerObject對象
		MethodRerObject m = new MethodRerObject();
		printString(m :: printUpperCaseString);

	}

}

輸出結果爲:

HELLO WORLD
HELLO WORLD

2.3 通過類名稱引用靜態方法

  • 函數式接口 Calcable.java
@FunctionalInterface
public interface Calcable {
	//定義一個抽象方法,傳遞一個整數,對整數進行絕對值計算並返回
	int calsAbs(int number);

}
  • 測試類 TestStaticMethodReference.java
/**
 * 通過類名稱引用靜態方法
 * 類已經存在,靜態成員方法也已經存在
 * 就可以通過類名直接引用靜態成員方法
 * @author kevin
 *
 */
public class TestStaticMethodReference {
	//定義一個方法,方法的參數傳遞要計算絕對值的整數和函數式接口Calcable
	public static int method(int number, Calcable c) {
		return c.calsAbs(number);
	}

	public static void main(String[] args) {
		//調用method方法,傳遞計算絕對值的整數和Lambda表達式
		int number = method(-10, (n) -> {
			//對參數進行絕對值計算並返回結果
			return Math.abs(n);
		});
		
		System.out.println(number);
		
		/*
		 * 使用方法引用優化Lambda表達式
		 * Math類是存在的
		 * abs計算絕對值的靜態方法也是已經存在的
		 * 所以我們可以直接通過類名引用靜態方法
		 */
		int number2 = method(-10, Math :: abs);
		System.out.println(number2);

	}

}

輸出結果爲:

10
10

2.4 通過super引用成員方法

如果存在繼承關係,當Lambda中需要出現super調用時,也可以使用方法引用進行替代。

  • 函數式接口 Greetable.java
/*
 * 定義見面的函數式接口
 */
@FunctionalInterface
public interface Greetable {
	//定義一個見面的方法
	void greet();

}
  • 父類 Human.java
/*
 * 定義父類
 */
public class Human {
	//定義一個sayHello的方法
	public void sayHello() {
		System.out.println("Hello 我是Human");
	}

}

  • 測試類 Man.java
/**
 * 通過Super引用成員方法
 * @author kevin
 *
 */
public class Man extends Human {
	//子類重寫父類sayHello的方法
	@Override
	public void sayHello() {
		System.out.println("Hello,我是Man!");
	}
	
	//定義一個方法參數傳遞Greetable接口
	public void method(Greetable g) {
		g.greet();
	}
	
	public void show() {
		//調用method方法
		/*method(() -> {
			Human h = new Human();
			h.sayHello();
		});*/
		
		/*method(() -> {
			super.sayHello();
		});*/
		
        /*
		 * 使用super引用類的成員方法
		 * super是已經存在的
		 * 父類的成員方法sayHello也是已經存在的
		 * 所以我們可以直接使用super引用弗雷德成員方法
		 */
		method(super :: sayHello);
	}

	public static void main(String[] args) {
		
		new Man().show();

	}

}

輸出結果爲:

Hello 我是Human

2.5 通過this引用成員方法

this代表當前對象,如果需要引用的方法就是當前類中的成員方法,那麼可以使用“this::成員方法”的各式來使用方法引用。

  • 函數式接口 Richable.java
/*
 * 定義一個富有的函數式接口
 */
@FunctionalInterface
public interface Richable {
	//定義一個想買什麼就買什麼的方法
	void buy();

}
  • 測試類 Husband.java
/**
 * 通過this引用本類的成員方法
 * @author kevin
 *
 */
public class Husband {
	//定義一個買房子的方法
	public void buyHouse() {
		System.out.println("北京二環內買一套四合院!");
	}
	
	//定義一個結婚的方法,參數傳遞Richable接口
	public void marry(Richable r) {
		r.buy();
	}
	
	//定義一個非常高興的方法
	public void soHappy() {
		//調用結婚的方法,方法的參數Richable是一個函數式接口,傳遞Lambda表達式
		/*marry(() -> {
			this.buyHouse();
		});*/
		
		/*
		 * 使用方法引用優化Lambda表達式
		 * this是已經存在的
		 * 本類的成員方法buyHouse也是已經存在的
		 * 所以我們可以直接使用this引用本類的成員方法buyHouse
		 */
		marry(this :: buyHouse);
	}

	public static void main(String[] args) {
		new Husband().soHappy();

	}

}

輸出結果爲:

北京二環內買一套四合院!

2.6 類的構造器引用

由於構造器的名稱與類名完全一樣,並不固定。所以構造器引用使用類名稱::new的格式表示。

  • Person.java
public class Person {
	
	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Person(String name) {
		super();
		this.name = name;
	}

	public Person() {
		super();
	}
	
	

}
  • 函數式接口 PersonBuilder.java
/*
 * 定義一個創建Person對象的函數式接口
 */
@FunctionalInterface
public interface PersonBuilder {
	//定義一個方法,根據傳遞的姓名,創建Person對象返回
	Person builderPerson(String name);

}
  • 測試類 Demo.java
/**
 * 類的構造器(構造方法)引用
 * @author kevin
 *
 */
public class Demo {
	//定義一個方法,參數傳遞姓名和PersonBuilder接口,方法中通過姓名創建Person對象
	public static void printName(String name, PersonBuilder pb) {
		Person person = pb.builderPerson(name);
		System.out.println(person.getName());
	}

	public static void main(String[] args) {
		//調用printName方法,方法的參數PersonBuilder接口是一個函數式接口,可以傳遞Lambda
		printName("迪麗熱巴", (String name) -> {
			return new Person(name);
		});
		
		/*
		 * 使用方法引用優化Lambda表達式
		 * 構造方法new Person(String name)已知
		 * 創建對象已知 new
		 * 就可以使用Person引用new創建對象
		 */
		printName("古力娜扎", Person :: new);

	}

}

輸出結果爲:

迪麗熱巴
古力娜扎

2.7 數組的構造器引用

數組也是object的子類對象,所以同樣具有構造器,只是語法稍微不同。

  • 函數式接口 ArrayBuilder.java
/*
 * 定義一個創建數組的函數式接口
 */
@FunctionalInterface
public interface ArrayBuilder {
	//定義一個創建int類型數組的方法,參數傳遞數組的長度,返回創建好的int類型數組
	int[] builderArray(int length);

}
  • 測試類 Demo.java
/*
 * 數組的構造器引用
 */
public class Demo {
	
	/*
	 * 定義一個方法
	 * 方法的參數傳遞創建數組的長度和ArrayBuilder接口
	 * 方法的內部根據傳遞的長度使用ArrayBuilder中的方法創建數組並返回
	 */
	public static int[] createArray(int length, ArrayBuilder ab) {
		return ab.builderArray(length);
	}

	public static void main(String[] args) {
		//調用createArray方法,傳遞數組的長度和Lambda表達式
		int[] arr1 = createArray(10, (len) -> {
			//根據數組的長度,創建數組並返回
			return new int[len];
		});
		
		System.out.println(arr1.length);
		
		
		/*
		 * 使用方法引用優化Lambda表達式
		 * 已知創建的就是int[]數組
		 * 數組的長度是已知的
		 * 就可以使用方法引用
		 * int[]引用new,根據參數傳遞的長度來創建數組
		 */
		int[] arr2 = createArray(10, int[] :: new);
		System.out.println(Arrays.toString(arr2));
		System.out.println(arr2.length);

	}

}

輸出結果爲:

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