Java8的函數語言程式設計

Panda_XiaoXi發表於2019-03-01

Java8中的函數語言程式設計

函數語言程式設計:是一種如何搭建應用程式的方法論。(lambda表示式+Stream流的思想)

指令式程式設計和函數語言程式設計的異同點:

  1. 指令式程式設計關注怎麼樣做,而函數語言程式設計中關注做什麼

     import java.util.stream.IntStream;
    
     public class MinDemo {
    
     	public static void main(String[] args) {
     		int[] nums = {35,65,-55,100,-676,95};
     		
     		//指令式程式設計(具體怎麼做)
     		int min = Integer.MAX_VALUE;
     		for (int i : nums) {
     			if(i < min) {
     				min = i;
     			}
     		}
     		
     		System.out.println(min);
     		
     		//jdk8的函式式編,多執行緒並行處理(需要做什麼)
     		int min2 = IntStream.of(nums).parallel().min().getAsInt();
     		System.out.println(min2);
     	}
    
     }
    複製程式碼
  2. 函數語言程式設計讓程式碼更加可讀

     public class ThreadDemo {
     	public static void main(String[] args) {
     		//指令式程式設計中的寫法
     		Object target = new Runnable() {
     			@Override
     			public void run() {
     				System.out.println("新建了一個執行緒");
     			}
     		};
     		new Thread((Runnable) target).start();
     
     		// jdk8 lambda函數語言程式設計中的寫法
     		Object target2 = (Runnable)() -> System.out.println("新建了一個執行緒");
     		Runnable target3 = () -> System.out.println("新建了一個執行緒");
     		System.out.println(target2 == target3); // false
     		
     		new Thread((Runnable) target2).start();
     	}
    
     }
    複製程式碼
lambda表示式中返回一個介面需要滿足的條件:
  1. 該介面中只能有一個方法

  2. 需要在介面上加上@FunctionalInterface註解(編譯器的校驗)

  3. JDK8中在介面中增加的預設的方法

     package lambda;
     
     import java.text.DecimalFormat;
     import java.util.Arrays;
     import java.util.function.Function;
     
     @FunctionalInterface
     interface Interface1 {
     	int doubleNum(int i);
     
     	default int add(int x, int y) {
     		return x + y;
     	}
     
     	static int sub(int x, int y) {
     		return x - y;
     	}
     }
     
     @FunctionalInterface
     interface Interface2 {
     	int doubleNum(int i);
     
     	default int add(int x, int y) {
     		return x + y;
     	}
     }
     
     @FunctionalInterface
     interface Interface3 extends Interface2, Interface1 {
     
     	@Override
     	default int add(int x, int y) {
     		return Interface1.super.add(x, y);
     	}
     
     }
     
     public strictfp class LambdaDemo1 {
     
     	public static void main(String[] args) {
     		Interface1 i1 = (i) -> i * 2;
     
     		Interface1.sub(10, 3);
     		System.out.println(i1.add(3, 7));
     		System.out.println(i1.doubleNum(20));
     
     		Interface1 i2 = i -> i * 2;
     
     		Interface1 i3 = (int i) -> i * 2;
     
     		Interface1 i4 = (int i) -> {
     			System.out.println("-----");
     			return i * 2;
     		};
     	}
     }
    複製程式碼
JDK8自帶的函式介面的鏈式操作:
class Money {
	private final int money;

	public MyMoney(int money) {
		this.money = money;
	}

	public void printMoney(Function<Integer, String> moneyFormat) {
		System.out.println("我的存款" + moneyFormat.apply(this.money));
	}
}

public class MoneyDemo {

	public static void main(String[] args) {
		Money me = new Money(99999999);

		Function<Integer, String> moneyFormat = i -> new DecimalFormat("#,###")
				.format(i);
		
		// 函式介面的鏈式操作
		me.printMoney(moneyFormat.andThen(s -> "人們幣" + s));
	}

}
複製程式碼
其他JdK8自帶的介面函式

Java8的函數語言程式設計

import java.util.function.Consumer;
import java.util.function.IntPredicate;

public class FunctionDemo {
	
	public static void main(String[] args) {
		// 斷言函式介面
		IntPredicate predicate = i -> i > 0;
		System.out.println(predicate.test(-9));//false
		
		// IntConsumer
		// 消費函式介面
		Consumer<String> consumer = s -> System.out.println(s);
		consumer.accept("輸入的資料");//輸入的資料
	}

}
複製程式碼

Java8中lamdba中方法的引用:

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.IntUnaryOperator;

class Dog {
	private String name = "哮天犬";

	/**
	 * 預設10斤狗糧
	 */
	private int food = 10;

	public Dog() {

	}

	/**
	 * 帶引數的建構函式
	 * 
	 * @param name
	 */
	public Dog(String name) {
		this.name = name;
	}

	/**
	 * 狗叫,靜態方法
	 * 
	 * @param dog
	 */
	public static void bark(Dog dog) {
		System.out.println(dog + "叫了");
	}

	/**
	 * 吃狗糧 JDK
	 * 
	 * 預設會把當前例項傳入到非靜態方法,引數名為this,位置是第一個;
	 * 
	 * @param num
	 * @return 還剩下多少斤
	 */
	public int eat(int num) {
		System.out.println("吃了" + num + "斤狗糧");
		this.food -= num;
		return this.food;
	}

	@Override
	public String toString() {
		return this.name;
	}
}

public class MethodRefrenceDemo {

	public static void main(String[] args) {
		Dog dog = new Dog();
		dog.eat(3);

		// 方法引用
		Consumer<String> consumer = System.out::println;
		consumer.accept("接受的資料");

		// 靜態方法的方法引用
		Consumer<Dog> consumer2 = Dog::bark;
		consumer2.accept(dog);

		// 非靜態方法,使用物件例項的方法引用
		// Function<Integer, Integer> function = dog::eat;
		// UnaryOperator<Integer> function = dog::eat;
		IntUnaryOperator function = dog::eat;
		
		// dog置空,不影響下面的函式執行,因為java 引數是傳值
		dog = null;//java中的函式是值傳遞不是傳引用,所以不會出現空指標異常
		System.out.println("還剩下" + function.applyAsInt(2) + "斤");
		
		// 使用類名來方法引用
		// BiFunction<Dog, Integer, Integer> eatFunction = Dog::eat;
		// System.out.println("還剩下" + eatFunction.apply(dog, 2) + "斤");
		
		// 建構函式的方法引用
		// Supplier<Dog> supplier = Dog::new;
		// System.out.println("建立了新物件:" + supplier.get());
		
		// 帶引數的建構函式的方法引用
		// Function<String, Dog> function2 = Dog::new;
		// System.out.println("建立了新物件:" + function2.apply("旺財"));

		// 測試java變數是傳值還是傳引用即值傳遞
		List<String> list = new ArrayList<>();
		test(list);

		System.err.println(list);
	}

	private static void test(List<String> list) {
		list = null;
	}
}
複製程式碼

lambda表示式中的級聯表示式和函式的柯里化

import java.util.function.Function;

/**
 * 級聯表示式和柯里化 
 * 柯里化:把多個引數的函式轉換為只有一個引數的函式 
 * 柯里化的目的:函式標準化
 * 高階函式:就是返回函式的函式
 */
public class CurryDemo {

	public static void main(String[] args) {
		// 實現了x+y的級聯表示式
		Function<Integer, Function<Integer, Integer>> fun = x -> y -> x
				+ y;
		System.out.println(fun.apply(2).apply(3));

		Function<Integer, Function<Integer, Function<Integer, Integer>>> fun2 = x -> y -> z -> x
				+ y + z;
		//函式的柯里化
		System.out.println(fun2.apply(2).apply(3).apply(4));

		int[] nums = { 2, 3, 4 };
		Function f = fun2;
		
		//柯里化的函式的遞迴呼叫
		for (int i = 0; i < nums.length; i++) {
			if (f instanceof Function) {
				Object obj = f.apply(nums[i]);
				if (obj instanceof Function) {
					f = (Function) obj;
				} else {
					System.out.println("呼叫結束:結果為" + obj);
				}
			}
		}
	}
}
複製程式碼

lambda底層實現原理

  1. 編譯器會為每一個lambda表示式生成一個方法 方法名是lambda$0,1,2,3,但方法引用的表示式不會生成方法。
  2. 在lambda地方會產生一個invokeDynamic指令,這個指令會呼叫 bootstrap(引導)方法,bootstrap方法會指向自動生成的lambda$0 方法或者方法引用的方法。
  3. bootstrap方法使用上是呼叫了LambdaMetafactory.metafactory靜態方法 該方法返回了CallSite(呼叫站點),裡面包含了MethodHandle(方法控制程式碼) 也就是最終呼叫的方法。
  4. 引導方法只會呼叫一次。

自動生成的方法:

1). 輸入和輸出和lambda一致

2). 如果沒有使用this,那麼就是static方法,否則就是成員方法

Stream流的建立:

Stream流的建立

public static void main(String[] args) {
	List<String> list = new ArrayList<>();

	// 從集合建立
	list.stream();
	list.parallelStream();

	// 從陣列建立
	Arrays.stream(new int[] { 2, 3, 5 });

	// 建立數字流
	IntStream.of(1, 2, 3);
	IntStream.rangeClosed(1, 10);

	// 使用random建立一個無限流
	new Random().ints().limit(10);
	Random random = new Random();

	// 自己產生流
	Stream.generate(() -> random.nextInt()).limit(20);

}
複製程式碼

Stream流的中間操作:

Stream流的中間操作

public static void main(String[] args) {
	String str = "my name is 007";

	// 把每個單詞的長度呼叫出來
	Stream.of(str.split(" ")).filter(s -> s.length() > 2)
			.map(s -> s.length()).forEach(System.out::println);

	// flatMap A->B屬性(是個集合), 最終得到所有的A元素裡面的所有B屬性集合
	// intStream/longStream 並不是Stream的子類, 所以要進行裝箱 boxed
	Stream.of(str.split(" ")).flatMap(s -> s.chars().boxed())
			.forEach(i -> System.out.println((char) i.intValue()));

	// peek 用於debug. 是個中間操作,和 forEach 是終止操作
	System.out.println("--------------peek------------");
	Stream.of(str.split(" ")).peek(System.out::println)
			.forEach(System.out::println);

	// limit 使用, 主要用於無限流
	new Random().ints().filter(i -> i > 100 && i < 1000).limit(10)
			.forEach(System.out::println);

}
複製程式碼

Stream流的終止操作:

Stream流的中間操作

public static void main(String[] args) {
	String str = "my name is 007";

	// 使用並行流
	str.chars().parallel().forEach(i -> System.out.print((char) i));
	System.out.println();
	// 使用 forEachOrdered 保證順序
	str.chars().parallel().forEachOrdered(i -> System.out.print((char) i));

	// 收集到list
	List<String> list = Stream.of(str.split(" "))
			.collect(Collectors.toList());
	System.out.println(list);

	// 使用 reduce 拼接字串
	Optional<String> letters = Stream.of(str.split(" "))
			.reduce((s1, s2) -> s1 + "|" + s2);
	System.out.println(letters.orElse(""));

	// 帶初始化值的reduce
	String reduce = Stream.of(str.split(" ")).reduce("",
			(s1, s2) -> s1 + "|" + s2);
	System.out.println(reduce);

	// 計算所有單詞總長度
	Integer length = Stream.of(str.split(" ")).map(s -> s.length())
			.reduce(0, (s1, s2) -> s1 + s2);
	System.out.println(length);

	// max 的使用
	Optional<String> max = Stream.of(str.split(" "))
			.max((s1, s2) -> s1.length() - s2.length());
	System.out.println(max.get());

	// 使用 findFirst 短路操作
	OptionalInt findFirst = new Random().ints().findFirst();
	System.out.println(findFirst.getAsInt());
}
複製程式碼
Stream中的並行流:
public static void main(String[] args) {
	// 呼叫parallel 產生一個並行流
	// IntStream.range(1, 100).parallel().peek(StreamDemo5::debug).count();

	// 現在要實現一個這樣的效果: 先並行,再序列
	// 多次呼叫 parallel / sequential, 以最後一次呼叫為準.
	// IntStream.range(1, 100)
	// // 呼叫parallel產生並行流
	// .parallel().peek(StreamDemo5::debug)
	// // 呼叫sequential 產生序列流
	// .sequential().peek(StreamDemo5::debug2)
	// .count();

	// 並行流使用的執行緒池: ForkJoinPool.commonPool
	// 預設的執行緒數是 當前機器的cpu個數
	// 使用這個屬性可以修改預設的執行緒數
	// System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism",
	// "20");
	// IntStream.range(1, 100).parallel().peek(StreamDemo5::debug).count();

	// 使用自己的執行緒池, 不使用預設執行緒池, 防止任務被阻塞
	// 執行緒名字 : ForkJoinPool-1
	ForkJoinPool pool = new ForkJoinPool(20);
	pool.submit(() -> IntStream.range(1, 100).parallel()
			.peek(StreamDemo5::debug).count());
	pool.shutdown();
	
	synchronized (pool) {
		try {
			pool.wait();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

public static void debug(int i) {
	System.out.println(Thread.currentThread().getName() + " debug " + i);
	try {
		TimeUnit.SECONDS.sleep(3);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}

public static void debug2(int i) {
	System.err.println("debug2 " + i);
	try {
		TimeUnit.SECONDS.sleep(3);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}
複製程式碼

lambda中的懶載入機制:在lambda中不呼叫終止操作,中間的所有過程是不會執行的。

import org.apache.commons.collections4.MapUtils;

/**
 * 學生 物件
 */
class Student {
	/**
	 * 姓名
	 */
	private String name;

	/**
	 * 年齡
	 */
	private int age;

	/**
	 * 性別
	 */
	private Gender gender;

	/**
	 * 班級
	 */
	private Grade grade;

	public Student(String name, int age, Gender gender, Grade grade) {
		super();
		this.name = name;
		this.age = age;
		this.gender = gender;
		this.grade = grade;
	}

	public String getName() {
		return name;
	}

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

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	public Grade getGrade() {
		return grade;
	}

	public void setGrade(Grade grade) {
		this.grade = grade;
	}

	public Gender getGender() {
		return gender;
	}

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

	@Override
	public String toString() {
		return "[name=" + name + ", age=" + age + ", gender=" + gender
				+ ", grade=" + grade + "]";
	}

}

/**
 * 性別
 */
enum Gender {
	MALE, FEMALE
}

/**
 * 班級
 */
enum Grade {
	ONE, TWO, THREE, FOUR;
}

public class CollectDemo {

	public static void main(String[] args) {
		// 測試資料
		List<Student> students = Arrays.asList(
				new Student("小明", 10, Gender.MALE, Grade.ONE),
				new Student("大明", 9, Gender.MALE, Grade.THREE),
				new Student("小白", 8, Gender.FEMALE, Grade.TWO),
				new Student("小黑", 13, Gender.FEMALE, Grade.FOUR),
				new Student("小紅", 7, Gender.FEMALE, Grade.THREE),
				new Student("小黃", 13, Gender.MALE, Grade.ONE),
				new Student("小青", 13, Gender.FEMALE, Grade.THREE),
				new Student("小紫", 9, Gender.FEMALE, Grade.TWO),
				new Student("小王", 6, Gender.MALE, Grade.ONE),
				new Student("小李", 6, Gender.MALE, Grade.ONE),
				new Student("小馬", 14, Gender.FEMALE, Grade.FOUR),
				new Student("小劉", 13, Gender.MALE, Grade.FOUR));

		// 得到所有學生的年齡列表
		// s -> s.getAge() --> Student::getAge , 不會多生成一個類似 lambda$0這樣的函式
		Set<Integer> ages = students.stream().map(Student::getAge)
				.collect(Collectors.toCollection(TreeSet::new));
		System.out.println("所有學生的年齡:" + ages);

		// 統計彙總資訊
		IntSummaryStatistics agesSummaryStatistics = students.stream()
				.collect(Collectors.summarizingInt(Student::getAge));
		System.out.println("年齡彙總資訊:" + agesSummaryStatistics);

		// 分塊
		Map<Boolean, List<Student>> genders = students.stream().collect(
				Collectors.partitioningBy(s -> s.getGender() == Gender.MALE));
		// System.out.println("男女學生列表:" + genders);
		MapUtils.verbosePrint(System.out, "男女學生列表", genders);

		// 分組
		Map<Grade, List<Student>> grades = students.stream()
				.collect(Collectors.groupingBy(Student::getGrade));
		MapUtils.verbosePrint(System.out, "學生班級列表", grades);

		// 得到所有班級學生的個數
		Map<Grade, Long> gradesCount = students.stream().collect(Collectors
				.groupingBy(Student::getGrade, Collectors.counting()));
		MapUtils.verbosePrint(System.out, "班級學生個數列表", gradesCount);

	}

}
複製程式碼

lambda表示式的執行機制:

驗證stream執行機制

  1. 所有操作是鏈式呼叫, 一個元素只迭代一次

  2. 每一箇中間操作返回一個新的流. 流裡面有一個屬性sourceStage 指向同一個 地方,就是Head

  3. Head->nextStage->nextStage->... -> null(底層維護一個連結串列資料結構)

  4. 有狀態操作會把無狀態操作階段,單獨處理

  5. 並行環境下, 有狀態的中間操作不一定能並行操作.

  6. parallel/ sequetial 這2個操作也是中間操作(也是返回stream) 但是他們不建立流, 他們只修改 Head的並行標誌

    public class RunStream {

       public static void main(String[] args) {
       	Random random = new Random();
       	// 隨機產生資料
       	Stream<Integer> stream = Stream.generate(() -> random.nextInt())
       			// 產生500個 ( 無限流需要短路操作. )
       			.limit(500)
       			// 第1個無狀態操作
       			.peek(s -> print("peek: " + s))
       			// 第2個無狀態操作
       			.filter(s -> {
       				print("filter: " + s);
       				return s > 1000000;
       			})
       			// 有狀態操作
       			.sorted((i1, i2) -> {
       				print("排序: " + i1 + ", " + i2);
       				return i1.compareTo(i2);
       			})
       			// 又一個無狀態操作
       			.peek(s -> {
       				print("peek2: " + s);
       			}).parallel();
    
       	// 終止操作
       	stream.count();
       }
    
       /**
        * 列印日誌並sleep 5 毫秒
        * 
        * @param s
        */
       public static void print(String s) {
       	// System.out.println(s);
       	// 帶執行緒名(測試並行情況)
       	System.out.println(Thread.currentThread().getName() + " > " + s);
       	try {
       		TimeUnit.MILLISECONDS.sleep(5);
       	} catch (InterruptedException e) {
       	}
       }
    複製程式碼

    }

相關文章