Lamdba表达式应用及总结

一,前言

目前java已经更新到13,但应用比较多的还是8。其中java8中一个重要新特性就是Lambda表达式。代表java对“函数式编程”的增强。大部分人人应该在工作中,或者其他人的代码中多多少少都见过Lambda表达式,并且自己也简单的用过,也大致了解过“函数式”编程的意思。

二,常见的Lambda表达式

最常见的应该就是应用多线程的时候,比如:

new Thread(() -> {
			System.out.println("abc");
		}).start();

特征就是: Thread里的参数由原来的

new Runnable {

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

变成了更为简单的写法:

() -> {System.out.println("abc");}

第一个括号代表参数列表(这里因为没参数,所以空着),然后加一个箭头(“->”),最后的花括号表示要执行的函数。
看起来就是 函数式编程里的:把函数作为一个参数传入另一个函数 的样子。
这就是所谓的“Lambda表达”。

三,函数式接口

runable接口可以写成Lambda表达,那么还有什么可以呢。
实际上是有一类接口都可以写成Lambda表达式,这样的接口在jdk8中称之为“函数式接口”。(既然“函数式编程”这种思想 被列为jdk8的重要更新,那么一定不会仅仅只有几个特定的接口可以写Lamdba表达)。
前面我们一直说函数式接口,因为函数式接口的定义(规则)就是:有且仅有一个抽象方法的接口(不能是抽象类)。
Lambda表达式是函数式接口的内联实现,也就是匿名内部类

下面通过java8内置的几个重要函数式接口,看看这样的接口的特征(Runable也是,但由于我们太熟悉了,容易产生“理所应当”的错觉,所以通过新接口更能找到特征)。

Function

这个接口代表一个函数(别看这个接口名叫function,好像代表了所有函数似的,但其实不是,它只代表一类函数),什么样函数呢:接收一个类型为T的参数,并且返回一个类型为R的返回值

Demo
import java.util.function.Function;

public class TestLambda {

	/**
	 * 功能:
	 * 传入一个参数,用函数处理一下,返回一个int。
	 * 具体怎么处理,也由调用者决定
	 * @param a			传入的待处理的参数
	 * @param function	调用者传入的函数,用来处理参数a的
	 * @return
	 */
    public int testFunction(int a, Function<Integer, Integer> function) {
            //apply 接收一个参数,传入function函数,返回结果
    		return function.apply(a);
    }
    
    public static void main(String[] args) {
    	TestLambda tl = new TestLambda();
    	
    	// testFunction
    	int res1 = tl.testFunction(2, a -> a*a);
    	System.out.println("res1 = " + res1);
    }
}

输出结果:res1 = 4

功能解释

testFunction方法有两个参数:一个int值a,一个用来处理参数a的函数。
内部只有一句话完成这个业务需求:function.apply(a),这句话看起来还比较好理解的,就是用函数处理一下参数。

首先看一下参数类型Function<Integer, Integer>
这个接口类带有两个范型类,第一个表示函数的参数,第二个表示函数的返回值类型。(这里参数和返回值都是Integer)

然后看传入的实参a -> a*a, 这就是一个Lambda表达,第一个a表示参数,a*a表示返回值。

源码分析

Function接口源码还是比较简单的,如下图所示
在这里插入图片描述
里面除了一个抽象方法apply,还有两个default方法(接口里允许写default方法也是jdk8的特性之一)和一个静态方法。主要看看两个default方法
compose:

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

函数1.compose(函数2).apply(参数)
(V v) -> apply(before.apply(v))
结果上表现出来是:先执行函数2,再执行函数1。本质上是把函数2作为一个参数传入一个构造出来的函数1((V v) -> apply())并返回这个函数,只有在执行apply方法的时候才真正在往函数传参并返回数据。

andThen:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

和compose相反,这个是先执行本函数的方法(apply(t)),然后再“构造”一个函数2,并把函数1作为参数传入这个函数2

其他内置函数式接口

和Function类似,jdk8还有其他一些内置的函数接口,也都比较简单,比如BiFunction(增加了一个参数)、Consumer(消费者)、Supplier(提供者)等。

四,Lambda表达式的写法

Lambda表达式除了可以写成a -> a*a,还可以写成
(a) -> a*a
(a) -> {return a*a;}
(Integer a) -> a*a
。。。
等等

总结起来就是变动两个地方:1.箭头前的参数列表,2.箭头后的执行代码
参数列表:

  1. 用小括号表示
  2. 如果只有一个参数可以省略小括号
  3. 如果没有参数,必须要有小括号
  4. 在用小括号的前提下,可以再次声明参数类型。注意不能是基本类型,比如(int a) -> a*a就是错的。(可以这么理解:因为函数接口里的范型Function<Integer, Integer>已经决定了参数类型是Integer而不是int,而范型是不能用基本类型的,所以参数同样也不会是基本类型。)

执行代码:

  1. 基本写法:{表达式;}{return 表达式;}
  2. 如果只有一句没有返回值的表达式时,可以把花括号和分号一起省略掉。(花括号和分号必须同时出现或一起消失)
  3. 如果有返回值,要么不用花括号/分号/return,如a*a这样简写,要么写成{return a*a;}这样的代码块。

五,作用范围

在使用lambda表达式时,可能会遇到…enclosing scope…final…这样字样的编译报错,这就涉及到lambda的引用作用范围的问题。

Demo

示例1:可以引用成员变量/静态成员变量

	String name = "张三";
	static String name_s = "李四";
	
	/**
	 * 测试作用范围1:可以引用成员变量/静态成员变量
	 * 测试结果:编译成功
	 */
	public void testScope1() {
		Function<String, String> function1 = p -> name;
		Function<String, String> function2 = p -> name_s;
	}

示例2:可以引用局部变量

	/**
	 * 测试作用范围2:可以引用局部变量
	 * 测试结果:编译成功
	 */
	public void testScope2() {
		String name_n = "王五";
		Function<String, String> function1 = p -> name_n;
	}

示例3:可以引用局部变量,但不允许修改

	/**
	 * 测试作用范围3:可以引用局部变量,但不允许修改
	 * 测试结果:编译失败
	 */
	public void testScope3() {
		String name_n = "王五";
		//编译报错:Local variable name_n defined in an enclosing scope must be final or effectively final
		//翻译:本地变量 name_n 定义在一个闭包范围内必须是final或者“表现出final”(意思是虽然可能不是final,但我们不去修改就没问题)
		Function<String, String> function1 = p -> name_n;
		name_n = "王五2";
	}

示例4:可以引用成员(静态)变量,允许修改

	String name = "张三";
	static String name_s = "李四";
	
	/**
	 * 测试作用范围4:可以引用成员变量,允许修改
	 * 测试结果:编译成功
	 */
	public void testScope4() {
		Function<String, String> function1 = p -> name;
		Function<String, String> function2 = p -> name_s;
		name = "张三2";
		name_s = "李四2";
	}

小结

lambada表达式内可以引用外部成员变量或静态成员变量,如果引用同一作用范围内的局部变量时,局部变量必须是final或者“表现出final特点:不修改值”。

原理

局部变量是栈数据,是线程隔离的,一旦lambda表达式起了一个新线程(比如Runable接口),那么就无法读到当前线程的栈内局部变量了。
而成员变量和对象一样都在堆中,静态变量在方法区中(jkd8中把静态变量和对象都放在了堆中)无论是堆还是jdk8之前的方法区都是线程共享的,即便lambda表示跑在另外的线程中也能访问的到。

六,和匿名内部类的关系

匿名内部类Demo

抽象类上使用匿名内部类

abstract class Person {
    public abstract void eat();
}
 
public class Demo {
    public static void main(String[] args) {
        Person p = new Person() {
            public void eat() {
                System.out.println("eat something");
            }
        };
        p.eat();
    }
}

接口上使用匿名内部类

interface Person {
    public void eat();
}
 
public class Demo {
    public static void main(String[] args) {
        Person p = new Person() {
            public void eat() {
                System.out.println("eat something");
            }
        };
        p.eat();
    }
}

小结

  1. 匿名内部类的特征:直接new一个未实现的抽象类(或接口),然后加一个花括号,里面写对抽象方法的实现。
  2. 匿名内部类可以用接口(也包括符合函数式接口)或抽象类。但函数式接口只允许使用接口形式。

七,方法引用

直接看demo

Demo

    /**
	 * 测试方法引用1: 使用Comparator内部类
	 * 对Integer数组排序
	 */
	public void testMethod1() {
		Integer[] arr = new Integer[] {8,2,4,1,0,6,7};
		Arrays.sort(arr, new Comparator<Integer>() {

			@Override
			public int compare(Integer o1, Integer o2) {
				return o1-o2;
			}
		});
		
		for (Integer integer : arr) {
			System.out.println(integer);
		}
		
	}
	/**
	 * 测试方法引用2: 使用Lambda表达式替代内部类
	 * 对Integer数组排序
	 */
	public void testMethod2() {
		Integer[] arr = new Integer[] {8,2,4,1,0,6,7};
		Arrays.sort(arr, (o1, o2) -> {
			return o1-o2;
		});
		
		for (Integer integer : arr) {
			System.out.println(integer);
		}
		
	}
	/**
	 * 测试方法引用3: 使用方法引用再简化Lambda写法
	 * 对Integer数组排序
	 */
	public void testMethod3() {
		Integer[] arr = new Integer[] {8,2,4,1,0,6,7};
		Arrays.sort(arr, Integer::compareTo);
		
		for (Integer integer : arr) {
			System.out.println(integer);
		}
		
	}

上面三个示例演示了一个排序Integer数组的写法。从传统的内部类到Lambda再到方法引用(这里使用的是Integer内的方法)一步步简化写法。

写法

类名::方法名。(注意:只需要写方法名,不需要写括号)
有以下四种形式的方法引用:

类型示例
引用静态方法ContainingClass::staticMethodName
引用某个对象的实例方法containingObject::instanceMethodName
引用某个类型的任意对象的实例方法ContainingType::methodName
引用构造方法ClassName::new

前面Demo中的方法引用使用的就是第三种引用某个类型的任意对象的实例方法

小结

  1. 方法引用也是一种lambda表达式
  2. 方法引用不是用函数式接口来实现,而是直接使用现有的类/对象的方法。或者说是 方法引用是一种把普通方法(静态或非静态)写成lambda表达式的语法。
  3. 方法一定程度上弥补了Lambda表达式的缺陷:需要定义函数式接口。

八,应用

  1. 当需要用的方法很简单(一般只有一句话),而且不需要重用,那么考虑使用Lambda表达式会简化很多。
  2. 需要写匿名内部类的地方,使用Lambda表达式,写法会更加优雅简洁。
  3. 一般用于对集合元素的批量迭代处理。

九,总结

  1. 有且仅有一个抽象方法的接口,称之为函数式接口,注解@FunctionInterface可以作为函数式接口的标记,也可以用来检验接口是否符合函数式接口的规则。
  2. Lambda表达式是函数式接口的内联实现,也就是匿名内部类。
  3. Lamdba表达式是java8引入的“函数式编程思想”的直接体现。
  4. 方法引用可以进一步简化Lambda表达式的写法。

知识结构图:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值