java函数式编程/lambda表达式学习笔记

1 函数式编程

1.1 个人理解

函数式编程属于一种编程范式,它不是一种具体的技术,而是一种编程的方法论,或者是思想。
相对于命令式编程,函数式编程强调函数的计算比指令的执行重要。
个人理解就是我们能熟练使用stream流API和lambda表达,有流相关的思想,并能应用到我们的工作中,就可以说我们会用函数式编程了。

1.2 为什么要用函数编程,有什么好处

函数式编程和命令式编程最大的不同点就是他们的关注点不一样。命令式编程关注这件事怎么做,而函数式编程只关注这件事做什么。简单来说,我们一般在写代码的时候需要通过写代码告诉程序怎么做来实现功能,而函数式编程时,我们只需要告诉程序我们要实现什么效果。这样可以简化我们的代码,加快开发速度,同时也能使代码更容易理解。
例如
我们获取一个数组中的最小值,命令式编程的做法如下:

@Test
public void contextLoads() {
	int[] arr = { 11, 22, 33, -11, 300, 200 };
	int min = Integer.MAX_VALUE;
	for (int i : arr) {
		if (i < min) {
			min = i;
		}
	}
	System.out.println(min);
}

函数式编程做法

@Test
public void contextLoads() {
	int[] arr = { 11, 22, 33, -11, 300, 200 };
	int min = IntStream.of(arr).min().getAsInt();
	System.out.println(min);
}

2 lambda表达式

2.1 lambda表达式初接触

@Test
public void contextLoads() {
	Runnable target = new Runnable() {

		@Override
		public void run() {
			System.out.println("yes");

		}
	};
	new Thread(target).start();
	// lambda表达式
	Runnable target2 = () -> System.out.println("ok");
	new Thread(target2).start();
}

如上代码,我们可以将lambda表达式理解为一个实现了指定接口的对象实例,上面的代码 箭头的左边就是方法的参数,右边是方法的执行体。

2.2 jdk8接口新特性

2.2.1 函数式接口

既然lambda表达式是一个实现了指定接口的对象实例,那么这个接口有没什么限制呢,其实是有的,这个指定接口只能有一个要实现的方法,很好理解,如果这个接口有多个需要实现的方法,那么lambda表达式放在哪个方法里面呢?
这种接口就是在jdk8中心引入的函数式接口,同时还新增了一个注解:@FunctionalInterface,用于编译期间校验,若想要写一个函数式接口,加上这个注解以后,若这个接口中有一个以上的要实现方法,就会编译报错。

2.2.2 默认方法

有人可能会问,函数式接口既然只能有一个要实现的方法,那这样不是会很不方便。其实不用担心,jdk8中提供了默认方法。

@FunctionalInterface
interface Test {
	// 需要实现的
	int doubleNum(int i);

	// 默认方法
	default int add(int x, int y) {
		return x + y;
	}
}

public class LambdaDamo1 {

	public static void main(String[] args) {
		// 箭头右边若只有一行,默认有一个return
		Test t1 = (i) -> i * 2;

		// 多行需要写return
		Test t2 = (i) -> {
			return i * 3;
		};

		System.out.println(t1.doubleNum(2));
		// 用lambda表达式获取到的接口实例调用默认方法
		System.err.println(t1.add(1, 1));
		
	}

}

我们可以将默认方法当成普通方法来用,基本步骤就是
1.首先通过lambda表达式获取到一个函数式接口的实例
2.然后就可以愉快的调用函数式接口中的默认方法了。
那么若两个函数式接口有相同的默认方法,另外一个接口继承了这两个接口会怎么样呢?如下:

@FunctionalInterface
interface Test {
	// 需要实现的
	int doubleNum(int i);

	// 默认方法
	default int add(int x, int y) {
		return x + y;
	}
}

@FunctionalInterface
interface Test1 {
	// 需要实现的
	int doubleNum(int i);

	// 默认方法
	default int add(int x, int y) {
		return x + y;
	}
}

@FunctionalInterface
interface Test2 extends Test, Test1 {

	@Override
	default int add(int x, int y) {
		// 在这里可以指定实现那个接口的默认方法
		return Test1.super.add(x, y);
	}

}

2.3 函数接口

早在你jdk8之前,jdk已经有的函数接口如下:

java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.nio.file.PathMatcher
java.lang.reflect.InvocationHandler
java.beans.PropertyChangeListener
java.awt.event.ActionListener
javax.swing.event.ChangeListener

jdk8之后新增的函数接口如下图:
在这里插入图片描述
演示

public class IntefaceTest {

	public static void main(String[] args) {
		// 断言函数Predicate
		Predicate<Integer> predicate = i -> i > 0;
		System.out.println("断言函数predicate: " + predicate.test(-9));

		// 消费者函数Consumer
		Consumer<Integer> consumer = i -> {
			System.out.println("消费者函数Consumer:  " + i * 2);
		};
		consumer.accept(5);

		// 输入T输出R的函数Function
		Function<Integer, String> function = i -> {
			return "我得到了" + i + "个苹果";
		};
		System.out.println("输入T输出R的函数Function:   " + function.apply(8));

		// 提供者函数Supplier
		Supplier<String> supplier = () -> "给你个supplier";
		System.out.println("提供者函数Supplier:   " + supplier.get());

		// 一元函数UnaryOperator,其实就是特殊的Function,就是输入和输出类型必须相同
		UnaryOperator<String> unaryOperator = i -> i + "哈哈哈";
		System.out.println("一元函数UnaryOperator:  " + unaryOperator.apply("笑!"));

		// 2个输入的函数BiFunction
		BiFunction<Integer, String, String> biFunction = (x, y) -> x + y;
		System.out.println("2个输入的函数BiFunction  " + biFunction.apply(3, "个开发"));

		// 二元函数BinaryOperator,特殊的BiFunction 两个输入的参数类型一样
		BinaryOperator<String> binaryOperator = (x, y) -> x + "和" + y + "笑嘻嘻";
		System.out.println("二元函数BinaryOperator:  " + binaryOperator.apply("张三", "李四"));
	}

}

控制台

断言函数predicate: false
消费者函数Consumer:  10
输入T输出R的函数Function:   我得到了8个苹果
提供者函数Supplier:   给你个supplier
一元函数UnaryOperator:  笑!哈哈哈
2个输入的函数BiFunction  3个开发
二元函数BinaryOperator:  张三和李四笑嘻嘻

2.4 lambda中的方法引用

我们都知道lambda表达式其实就是返回一个函数接口的对象实例的匿名函数,箭头的左边是参数,右边是执行体。当你的执行体中只有一个函数调用,且这个函数调用的参数跟箭头左边的参数一样的话就可以使用方法引用的形式。lambda中的方法引用能使我们的表达式更加简洁。

public class LambdaDemo2 {

	public static void main(String[] args) {
		// 一般形式的lambda表达式
		Consumer<String> consumer = s -> System.out.println(s);

		// 方法引用形式的lambda表达式
		Consumer<String> consumer1 = System.out::println;
	}
}

2.4.1静态方法的方法引用

静态方法引用,我们直接可以用类名来引用即可。

class Dog {

	private String name = "阿黄";

	public static void bark(Dog dog) {
		System.out.println(dog + "叫了");
	}

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

}

public class LambdaDemo2 {

	public static void main(String[] args) {
		// 对Dod的bark方法进行引用,直接用类名即可
		Consumer<Dog> consumer = Dog::bark;
		Dog dog = new Dog();
		consumer.accept(dog);
	}
}

阿黄叫了

2.4.2 成员方法的方法引用

用实例变量来引用

class Dog {

	private String name = "阿黄";

	private int food = 10;

	public static void bark(Dog dog) {
		System.out.println(dog + "叫了");
	}

	public int eat(int num) {
		System.out.println("吃了" + num + "斤食物");
		this.food -= num;
		return food;
	}

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

}

public class LambdaDemo2 {

	public static void main(String[] args) {
		// 对Dod的bark方法进行引用,直接用类名即可
		Consumer<Dog> consumer = Dog::bark;
		Dog dog = new Dog();
		consumer.accept(dog);

		// 对Dod的eat方法进行引用,用实例来引用
		UnaryOperator<Integer> unaryOperator = dog::eat;
		System.out.println("还剩下:" + unaryOperator.apply(3) + "斤");
	}
}
阿黄叫了
吃了3斤食物
还剩下:7斤

用类名来引用
JDK默认会把当前实例传入非静态方法,参数名为this,位置是第一个。那么此时eat方法就相当于有两个参数一个是Dog类型的参数,一个int类型的参数。

class Dog {

	private String name = "阿黄";

	private int food = 10;

	public static void bark(Dog dog) {
		System.out.println(dog + "叫了");
	}

	public int eat(int num) {
		System.out.println("吃了" + num + "斤食物");
		this.food -= num;
		return food;
	}

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

}

public class LambdaDemo2 {

	public static void main(String[] args) {
		// 对Dod的bark方法进行引用,直接用类名即可
		Consumer<Dog> consumer = Dog::bark;
		Dog dog = new Dog();
		consumer.accept(dog);

		// 对Dod的eat方法进行引用,用实例来引用
		UnaryOperator<Integer> unaryOperator = dog::eat;
		System.out.println("还剩下:" + unaryOperator.apply(3) + "斤");

		// 实际上成员方法的参数上是有一个隐藏的this的,即一个隐藏的输入
		BiFunction<Dog, Integer, Integer> biFunction = Dog::eat;
		System.out.println("还剩下:" + biFunction.apply(dog, 3) + "斤");
	}
}

阿黄叫了
吃了3斤食物
还剩下:7斤
吃了3斤食物
还剩下:4斤

2.4.3 构造方法的方法引用

构造方法比较特殊,它的方法引用是new

无参的构造方法可以看做一个supplier函数

class Dog {

	private String name = "阿黄";

	private int food = 10;

	public static void bark(Dog dog) {
		System.out.println(dog + "叫了");
	}

	public int eat(int num) {
		System.out.println("吃了" + num + "斤食物");
		this.food -= num;
		return food;
	}

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

}

public class LambdaDemo2 {

	public static void main(String[] args) {
		// 无参数构造函数
		Supplier<Dog> supplier = Dog::new;
		System.out.println(supplier.get());
	}
}
阿黄

有参的构造函数

class Dog {

	private String name = "阿黄";

	private int food = 10;

	/**
	 * 无参构造
	 */
	public Dog() {
		super();
	}

	/**
	 * 有参构造
	 * 
	 * @param name
	 */
	public Dog(String name) {
		this.name = name;
	}

	public static void bark(Dog dog) {
		System.out.println(dog + "叫了");
	}

	public int eat(int num) {
		System.out.println("吃了" + num + "斤食物");
		this.food -= num;
		return food;
	}

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

}

public class LambdaDemo2 {

	public static void main(String[] args) {
		// 无参数构造函数的方法引用
		Supplier<Dog> supplier = Dog::new;
		System.out.println(supplier.get());

		// 有参构造函数的方法引用
		Function<String, Dog> function = Dog::new;
		System.out.println(function.apply("小黑"));
	}
}

阿黄
小黑

2.5 类型推断

lambda是一个返回一个函数接口的对象实例的匿名函数,所以在写的时候就应该告诉lambda要实现的哪个接口,这就是lambda类型推断。有以下几种形式来告诉lambda要实现的接口
第一种方式:直接通过变量类型定义的方式

@FunctionalInterface
interface IMath {

	int add(int x, int y);

}

public class LambdaDemo3 {

	public static void main(String[] args) {

		// 变量类型定义,指定返回的类型就是要实现的接口
		IMath lambda = (x, y) -> x + y;

		// 数组形式
		IMath[] lambdas = { (x, y) -> x + y };
		
	}

}

第二种方式:强转的方式

@FunctionalInterface
interface IMath {

	int add(int x, int y);

}

public class LambdaDemo3 {

	public static void main(String[] args) {

		// 强转,通过强转来告诉后面的lambda表达式要实现的接口
		Object lambda = (IMath) (x, y) -> x + y;

	}

}

第三种方式:通过返回类型

@FunctionalInterface
interface IMath {

	int add(int x, int y);

}

public class LambdaDemo3 {

	public static void main(String[] args) {
		// 通过返回类型,指定lambda表达式要实现的接口类型
		IMath lambda = createLambda();
	}

	public static IMath createLambda() {
		return (x, y) -> x + y;
	}
}

注意:当使用lambda表达式出现冲突时,必须使用强转,指定lambda要实现的接口类型,否则编译报错。

2.6变量引用

在lambda表达式中引用外部变量时,外部变量必须是final,在jdk7之前是必须加上final修饰词的,但是在jdk8以后可以不用加这个修饰词,但是外部变量初始化以后依旧是不能修改的。
为什么呢?这涉及到java的参数传递问题,java的参数传递是值传递。
外部变量的引用和lambda表达式中的引用是指向的同一个对象,如下图
在这里插入图片描述
若在外部我们重新给list附了值,那么其实外面的代码执行和lambda表达式中的代码执行顺序是没有先后关系的,若lambda表达式中的代码执行过程中,其中的list还是指向原来的集合,外面的却改变了,那么就会出现错误的结果。
若是像C语言一样,参数传递是引用传递,那么久如下图
在这里插入图片描述

2.7级联表达式和柯里化

lamdab的级联表达式其实就是多层调用,
柯里化:把多个参数的函数转换为只有一个参数的函数
目的:函数标准化
如下代码

public class LambdaDemo4 {

	public static void main(String[] args) {
		// x+y的级联表达式
		Function<Integer, Function<Integer, Integer>> function = x -> y -> x + y;
		// 第一次调用apply返回的是一个Function函数,第二次调用apply返回的才是一个Integer
		System.out.println(function.apply(1).apply(2));
	}

}

猜你喜欢

转载自blog.csdn.net/qq_38846242/article/details/84984657