Java基础 lambda表达式
一、Lambda表达式简介
1.1 概念
Lambda表达式是Java 8 添加的一个新特性,可以认为,Lambda是一个匿名函数(相似于匿名内部类),作用是返回一个实现了接口的对象。
使用Lambda表达式对比于其他接口实现方式显得非常简洁。
1.2 Lambda对接口的要求
虽然Lambda表达式对某些接口进行简单的实现,但是并不是所有的接口都可以使用Lambda表达式来实现,要求接口种定义的必须要实现的抽象方法只能是一个(注意:严格来说是只能有一个抽象方法,因为现在允许接口里有默认实现了)。
在Java 8 中对接口增加了新特性:default,提供了一个默认的抽象方法,但是Lambda对此没有特殊的影响,方法可以按Lambda所表达的来。
注意事项
@FunctionalInterface
这个注解用于修饰函数式接口,即意味着接口中的抽象方法只能有一个,否则编译器会报错。
我们总是需要对象来实现接口,Lambda表达式就是帮助我们简化这个过程,而对象中的单独的方法在对象的创建接口对象的创建过程中并不会执行。
1.3 Lambda表达式的延迟执行原因
(以下来源于Java 核心技术 卷一)
使用lambda表达式的重点是延迟执行(deffered execution)。毕竟,如果想要立即执行代码,完全可以直接执行,而无需将它包装到一个lambda表达式中。之所以希望以后再执行代码,这有很多原因,比如:
- 在一个单独的线程中运行代码
- 多次运行代码、在算法的适当位置运行代码(例如,排序的比较操作)
- 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等)】
- 只在必要时才运行代码
1.4 接口实现的不同方式
实现接口的对象创建方式有三种,如代码所示,分为:
- 使用接口实现类(接口对象指向已实现了接口的类对象)
- 使用匿名内部类实现接口
- 使用lambda表达式来实现接口
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class Program { 2 public static void main(String[] args) { 3 /*** 4 * 1.使用接口实现类 5 */ 6 7 Comparator comparator = new MyComparator(); 8 9 /** 10 * 2.使用匿名内部类实现接口 11 */ 12 13 Comparator comparator1 = new Comparator() { 14 @Override 15 public int compare(int a, int b) { 16 return a - b; 17 } 18 }; 19 20 /** 21 * 3.使用lambda表达式来实现接口 22 * 23 */ 24 Comparator comparator2 = (a, b) -> a - b; 25 26 27 /** 28 * 测试部分 若不出错,则显示三个-1 29 */ 30 31 System.out.println(comparator.compare(1, 2)); 32 System.out.println(comparator1.compare(1, 2)); 33 System.out.println(comparator2.compare(1, 2)); 34 } 35 } 36 37 38 class MyComparator implements Comparator { 39 @Override 40 public int compare(int a, int b) { 41 return a - b; 42 } 43 } 44 45 @FunctionalInterface 46 interface Comparator { 47 int compare(int a, int b); 48 }
二、Lambda表达式的基础语法
2.1 基本语法注意事项
- Lambda表达式是一个匿名函数
- 关注重点:参数列表 方法体
- 小括号():=用来描述一个参数列表(形参)
- 大括号{} 来描述一个方法体
- ->:即Lambda运算符,读作goes to ,用于分割参数列表和方法体
2.2 代码实例
使用Lambda表达式的多种形式,分为有无返回值的普通方法(构造方法后面再讲),无参、一参、多参方法,总共有6个方法。所以定义了多个拥有不同方法的接口。从接口的命名方式就可以知道其意味着的含义(如果不在同一包种,注意import)。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.guilin.java; 2 3 /** 4 * @description: 5 * @author: luguilin 6 * @date: 2021-12-22 17:37 7 **/ 8 9 10 /** 11 * 1.无返回值的多参接口 12 */ 13 @FunctionalInterface 14 interface LambdaNoneReturnMultipleParameter { 15 void test(int a, int b); 16 } 17 18 /** 19 * 2.无返回值的无参接口 20 */ 21 @FunctionalInterface 22 interface LambdaNoneReturnNoneParameter { 23 void test(); 24 } 25 26 /** 27 * 3.无返回值的一参接口 28 */ 29 @FunctionalInterface 30 interface LambdaNoneReturnSingleParameter { 31 void test(int n); 32 } 33 34 /** 35 * 4.有返回值的多参接口 36 */ 37 @FunctionalInterface 38 interface LambdaSingleReturnMultipleParameter { 39 int test(int a, int b); 40 } 41 42 /** 43 * 5.有返回值的无参接口 44 */ 45 @FunctionalInterface 46 interface LambdaSingleReturnNoneParameter { 47 int test(); 48 } 49 50 /** 51 * 6.有返回值的一参接口 52 */ 53 @FunctionalInterface 54 interface LambdaSingleReturnSingleParameter { 55 int test(int n); 56 } 57 58 /** 59 * Lambda表达式的基础语法 60 */ 61 public class LambdaTest { 62 public static void main(String[] args) { 63 /** 64 * 1.无参无返回的Lambda表达式使用样例 65 */ 66 LambdaNoneReturnNoneParameter lambda1 = () -> { 67 System.out.println("lambda1:" + "Hello World!"); 68 }; 69 lambda1.test(); 70 71 /** 72 * 2.无返回值的单参数的Lambda表达式使用样例 73 */ 74 75 LambdaNoneReturnSingleParameter lambda2 = (int i) -> { 76 System.out.println("lambda2:" + i); 77 }; 78 lambda2.test(1024); 79 80 81 /** 82 * 3.无返回值的多参数的Lambda表达式使用样例 83 */ 84 LambdaNoneReturnMultipleParameter lambda3 = (int a, int b) -> 85 { 86 System.out.println("lambda3:" + (a + b)); 87 }; 88 lambda3.test(1000, 24); 89 90 /** 91 * 4.有返回值的无参数的Lambda表达式使用样例 92 */ 93 94 LambdaSingleReturnNoneParameter lambda4 = () -> { 95 return 1024; 96 }; 97 int res = lambda4.test(); 98 System.out.println("lambda4:" + res); 99 100 /** 101 * 5.有返回值,单个参数的Lambdad的表达式使用 102 */ 103 104 LambdaSingleReturnSingleParameter lambda5 = (int a) -> { 105 return a; 106 }; 107 int res2 = lambda5.test(1024); 108 System.out.println("lambda5:" + res2); 109 110 /** 111 * 6.有返回值,多个参数的Lambdad的表达式使用 112 */ 113 LambdaSingleReturnMultipleParameter lambda6 = (int a, int b) -> { 114 int sum = a + b; 115 return sum; 116 }; 117 int res3 = lambda6.test(1000, 24); 118 System.out.println("lambda6:" + res3); 119 120 } 121 }
三、Lambda表达式语法精简
从Lambda表达式的基础语法样例中我们几乎没有看Lambda语法的优势,特别是和匿名内部类对比,更是没发现Lambda带来的代码的优雅和简化。但是,Lambda语法提供了合理的代码化简方式。
3.1 Lambda表达式精简的方式
- 参数类型的精简:
由于在接口中已经定义了参数,所以在Lambda表达式中参数的类型可以省略;
备注:如果需要进行省略类型,那么所有参数的类型都必须都得省略,省略部分会报错;
匿名内部类中省略参数类型是不可取的,这是Lambda表达式的优势;
- 小括号的精简:
如果参数列表中,参数的个数有且只有一个(多了少了都不行),那么小括号可以省略,且仍然可以省略参数的类型
- 方法大括号的精简:
类似于if,while语句,如果语句块只有一条语句,那么此时大括号可以省略
- return的省略:
如果出现接口只有唯一方法且方法中只有唯一语句,且是返回语句,那么如果要省略,只能一起省略掉大括号以及return,不能省略其中之一,否则会报错。
代码如下
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /** 2 * 此类用于语法精简的Lambda表达式演示 3 */ 4 public class Syntax2 { 5 /** 6 * 参数精简 7 * 1.参数的精简 8 * 由于在接口中已经定义了参数,所以在Lambda表达式中参数的类型可以省略 9 * 备注:如果需要进行省略类型,但是所有参数的类型必须都得省略,省略部分会报错 10 * 匿名内部类中省略参数类型是不可取的 11 */ 12 13 LambdaNoneReturnMultipleParameter lambda1 = (a, b) -> { 14 System.out.println(a + b); 15 16 }; 17 /** 18 * 2.精简参数小括号 19 * 如果参数列表中,参数的个数有且只有一个(多了少了都不行),那么小括号可以省略 20 * 且仍然可以省略参数的类型 21 */ 22 LambdaNoneReturnSingleParameter lambda2 = a -> { 23 System.out.println(a); 24 }; 25 26 /** 27 * 3.方法大括号的省略 28 * 类似于if,while语句,如果语句块只有一条语句,那么此时大括号可以省略、 29 * 前面的省略方式仍然成立 30 */ 31 LambdaNoneReturnSingleParameter lambda3 = a -> 32 System.out.println(a); 33 /** 34 * 4.如果接口的唯一方法只有唯一返回语句,那么可以省略大括号,但是在省略大号的同时必须省略return 35 */ 36 LambdaSingleReturnNoneParameter lambda4 = () -> 10; 37 38 }
四、 Lambda表达式进阶之函数引用
具体见java方法引用
方法引用的提出: 由于如果存在一种情况,我们新建了多个接口的实现对象,其方法都是相同的,但是如果方法需要修改,那么修改的复杂度就随着对象数量的上升而上升。
方法引用的定义: 快速将一个Lambda表达式的实现指向一个已经写好的方法
方法引用可以看作是lambda表达式的特殊形式,或者称之为语法糖。一般方法已经存在才可以使用方法引用,而方法若未存在,则只能使用lambda表达式。
我们可以采用两种方式来在Lambda表达式中调用其他方法,第一种如一般的方法调用,第二种就是方法引用。
方法引用的语法说明:
即:“方法的隶属者::方法名”。方法的隶属者,即静态方法隶属者为类,非静态方法的隶属者是对象(隶属者不是接口,而是定义引用方法的类或者对象)。
注意事项:
- 被引用的方法的参数数量以及类型一定要和接口中的方法参数数目一致;
- 被引用的方法的返回值一定要和接口中的方法返回值一致,方法引用这个整体表达式可以返回函数式接口的实现对象,但其调用/引用的方法其返回类型绝不是接口实例对象;
- 方法名的后面没有括号“()”;
- 方法的引用是可以有多个参数入口的,虽然在
::
表达式中没有体现(由于没有小括号),但是接口中对其已有所规定了;
4.1 普通方法在Lambda表达式中的调用
2种不同的普通方法调用的样例说明:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.guilin.java; 2 3 /** 4 * 有返回值的多参接口 5 */ 6 @FunctionalInterface 7 interface LambdaSingleReturnMultipleParameter { 8 int test(int a, int b); 9 } 10 11 /** 12 * 有返回值的一参接口 13 */ 14 @FunctionalInterface 15 interface LambdaSingleReturnSingleParameter { 16 int test(int n); 17 } 18 19 /** 20 * Lambda表达式的基础语法 21 */ 22 public class TestDemo { 23 public static void main(String[] args) { 24 25 /** 26 *方法引用:可以快速将一个Lambda表达式的实现指向一个已经写好的方法 27 *语法:方法的隶属者,静态方法隶属者为类,非静态方法的隶属者是对象 28 * 即:“方法的隶属者:方法名” 29 * 注意事项: 30 * 1.被引用的方法的参数数量以及类型一定要和接口中的方法参数数目一致 31 * 2.被引用的方法的返回值一定要和接口中的方法返回值一致 32 * 33 * 34 * 假如我们在程序中对于某个接口方法需要调用许多次,那么用以下的方法创建对象,来调用方法就是不太好的 35 * 缺点:如果将来要对方法进行改变,那么所有用Lambda表达式定义的对象都要更改,这在设计模式上就是有问题的; 36 */ 37 LambdaSingleReturnSingleParameter lambda1 = a -> a * 2; 38 LambdaSingleReturnSingleParameter lambda2 = a -> a * 2; 39 40 /** 41 * 我们一般是写一个通用的方法,并将其引用至Lambda表达式中 42 */ 43 LambdaSingleReturnSingleParameter lambda3 = a -> change(a);//在Lambda表达式中使用一般方法的调用方式 44 LambdaSingleReturnSingleParameter lambda4 = TestDemo::change;//在Lambda表达式种使用方法引用(方法隶属于类) 45 System.out.println(lambda4.test(2)); 46 TestDemo test = new TestDemo();//非静态方法需要对象才能被调用 47 LambdaSingleReturnSingleParameter lambda5 = test::change2;//在Lambda表达式种使用方法引用(方法隶属于对象) 48 LambdaSingleReturnMultipleParameter lambda6 = test::change3;//多参数的引用方法使用 49 } 50 51 private static int change(int a) { 52 return a * 2; 53 } 54 55 private int change2(int a) { 56 return a * 2; 57 } 58 59 private int change3(int a, int b) { 60 return a * 2 + b * 3; 61 } 62 }
4.2 构造方法在Lambda表达式中的调用
Person类具有无参和有参构造方法。
定义一个类,构造方法创建的对象
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.guilin.java; 2 3 class Person { 4 public String name; 5 public int age; 6 7 public Person() { 8 System.out.println("Person类的无参构造方法执行了"); 9 } 10 11 public Person(String name, int age) { 12 this.name = name; 13 this.age = age; 14 System.out.println("方法的有参构造方法执行了"); 15 } 16 } 17 18 public class TestDemo { 19 public static void main(String[] args) { 20 21 PersonCreater person = () -> new Person(); 22 23 /**构造方法的引用 24 * 有参和无参构造器的调用区别在于所定义的接口中构造方法的参数区别 25 */ 26 PersonCreater creater1 = Person::new; 27 //无参 28 Person person1 = creater1.getPerson(); 29 //有参 30 PersonCreater2 creater2=Person::new; 31 Person person2 = creater2.getPerson("Fisherman",18 ); 32 33 } 34 35 } 36 37 //需求为:一个返回一个Person类的接口 38 39 interface PersonCreater { 40 Person getPerson(); 41 } 42 43 interface PersonCreater2 { 44 Person getPerson(String name, int age); 45 }
注意事项:
- 在Lambda表达式中,一个接口要么对应一个无参构造方法,要么含有一个有参构造方式,其在接口中所定义的抽象返回Person对象的方法已经决定有参还是无参了。
- 构造方法和静态方法一样都是隶属于类的方法
- 构造方法不同于一般静态方法“类名::方法名一样调用”,而是采用"类名::new"的方式来进行构造方法的调用。
- 使用new关键字是为了明确地知道调用的是构造函数。而其并不需要入口参数的原因是因为编译器完全可以通过接口的定义推断出参数类型和个数。构造方法的方法引用和普通方法引用并没有本质的区别,比如在CodeBlock-5中用change(a)来实现原接口中定义的返回整形数据的test方法,而new关键字使用对应形参的构造器来实现接口中定义的返回Person对象的getPerson方法。
::
在IDE(比如Intllij IDEA)中总是指向当前方法引用实现的函数式接口,以此可以方便地确定方法引用所实现的函数式接口为哪个。
4.3 方法引用的格式总结:
引用方法的类型 | 格式规定 | 方法的发出者 | 等价lambda表达式 |
---|---|---|---|
引用静态方法 | ClassName::staticMethodName | 类 | (s) -> String.valueOf(s) |
引用某个对象的实例(非静态)方法 | ObjectName::instanceMethodName | 当前对象 | ObjectName.instanceMethodName |
引用某个类型的任意对象的实例方法 | ClassName::methodName | 任意此类或子类的对象 | (任意对象,s) -> methodName(任意对象,s) |
引用构造方法 | ClassName::new | 类 | (s) -> new ClassName(s); |
4.4 常见的引用形式
类名::静态方法名
调用类的静态方法
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //接口定义 2 interface parseIntNum{ 3 //定义一个String转化成Integer的方法 4 int pass(String s); 5 } 6 public static void main(String[] args) { 7 parseIntNum parseIntNum1; 8 parseIntNum parseIntNum2; 9 //原始lambda 10 parseIntNum1 = (str)-> Integer.parseInt(str); 11 System.out.println(parseIntNum1.pass("1")); 12 //方法引用改进版本 13 parseIntNum2 = Integer::parseInt; 14 System.out.println(parseIntNum2.pass("1")); 15 }
对象::实例方法
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class Test { 2 public static void main(String[] args) { 3 4 Power powerObject = new Power(); 5 Function<Integer,Integer> function1 = a->powerObject.power(a); 6 Function<Integer,Integer> function2 = powerObject::power; 7 /** 8 *不管哪种实现,方法的调用是相同的,都用接口的已实现抽象方法名调用。 9 */ 10 System.out.println(function1.apply(2)); 11 System.out.println(function2.apply(3)); 12 } 13 14 } 15 16 class Power { 17 public int power (int a ){ 18 return a*a; 19 } 20 }
数据类型:new
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public static void main(String[] args) { 2 //方式一 3 IntFunction<int []> arr1 = new IntFunction<int[]>() { 4 @Override 5 public int[] apply(int num) { 6 return new int[num]; 7 } 8 }; 9 arr1.apply(10); 10 //方式二(方法引用) 11 IntFunction<int []> arr2 = int[]::new; 12 arr2.apply(10); 13 14 }
除此之外还有很多种形式,这里就不过多赘述了
4.5 开发常用
遍历数组打印
import java.util.ArrayList; import java.util.function.Consumer; public class TestDemo { public static void main(String[] args) { ArrayList<Integer> arrayList = new ArrayList<>(); arrayList.add(1); arrayList.add(2); arrayList.add(3); //匿名内部类 arrayList.forEach(new Consumer<Integer>() { @Override public void accept(Integer integer) { System.out.println(integer); } }); //lambda优化 arrayList.forEach((integer) -> System.out.println(integer)); //方法引用打印,用方法引用替代了我们的匿名内部类(相当于替代了lambda) arrayList.forEach(System.out::println); } }
遍历map
public class TestDemo { public static void main(String[] args) { Map<Integer, String> map = new HashMap<>(); map.forEach((k,v)->System.out.println(v)); } }
五、Lambda表达式作用域
5.1 访问局部普通变量
- 只能引用标记了『final』的外层局部变量
即 : 不能在lambda 内部修改定义在域外的局部变量,否则会编译错误。
1 public class TestFinal { 2 interface ChangeInterface { 3 Integer change(String str); 4 } 5 6 public static void main(String[] args) { 7 final String s = "111"; 8 ChangeInterface change = str -> Integer.valueOf(str+s); 9 Integer str = change.change("222"); 10 System.out.println(str); 11 } 12 } 13 14 /** 15 * 结果为 16 * 222111 17 */
- 特殊情况下,局部变量也可以不用声明为 『final』,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)
public class TestFinal { interface ChangeInterface { Integer change(String str); } public static void main(String[] args) { String s = "111"; ChangeInterface change = str -> Integer.valueOf(str+s); Integer str = change.change("222"); System.out.println(str); } } /** * 结果为 * 222111 */
1 public class TestFinal { 2 interface ChangeInterface { 3 Integer change(String str); 4 } 5 6 public static void main(String[] args) { 7 String s = "111"; 8 ChangeInterface change = str -> Integer.valueOf(str+s); 9 s = "333"; 10 Integer str = change.change("222"); 11 System.out.println(str); 12 } 13 }
报错,提示
5.2 访问局部引用变量
- 访问引用变量的话,是没有问题的。因为lambda可以感知到外部对该引用变量的改变,不会出现数据不同步问题
1 import java.util.ArrayList; 2 public class TestFinal { 3 4 interface TestDemo{ 5 Integer change(); 6 } 7 void testArrayList(){ 8 ArrayList<String> list = new ArrayList<>(); 9 list.add("111"); 10 //访问外部引用局部引用变量 11 TestDemo meiYan = (() -> Integer.valueOf(list.get(0))); 12 //修改局部引用变量 13 list.set(0,"222"); 14 Integer str =meiYan.change(); 15 System.out.println(str); 16 } 17 public static void main(String[] args) { 18 new TestFinal().testArrayList(); 19 } 20 } 21 22 /** 23 * 结果为 24 * 222 25 */
5.3 访问静态变量和实例变量
都是可以的,再次修改也不会报错
1 public class TestFinal { 2 //静态变量 3 static String StaticTemp; 4 //实例变量 5 String instanceTemp; 6 7 interface TestDemo { 8 Integer change(String str); 9 } 10 11 void testStatic() { 12 StaticTemp = "222"; 13 TestDemo t1 = (str -> Integer.valueOf(str + StaticTemp)); 14 StaticTemp = "333"; 15 Integer str = t1.change("111"); 16 System.out.println(str); 17 18 instanceTemp = "555"; 19 TestDemo t2 = (str2 -> Integer.valueOf(str2 + instanceTemp)); 20 instanceTemp = "666"; 21 Integer str2 = t2.change("111"); 22 System.out.println(str2); 23 24 } 25 26 public static void main(String[] args) { 27 new TestFinal().testStatic(); 28 } 29 } 30 31 /** 32 * 结果为 33 * 111333 34 * 111666 35 */
5.4 理解
实例变量和局部变量的区别
这里的关键问题转换到: 实例变量和局部变量的区别是什么?
-
- 实例变量存储在堆上
堆是在线程之间共享的。
-
- 而局部变量存储在栈上
有可能会有相应的线程问题(见下)
线程问题
比如A线程分配了一个变量temp
-
- 有可能Lambda是在另一个线程B中使用的,使用Lambda的线程B,可能会在分配该变量的线程A将temp变量收回之后,还去访问temp变量。
数据不同步问题
联想一下我们普通的方法,方法的参数只是存活在方法栈这个空间里,而我们lambda的{ }里实际上也相当于一个方法块。
-
- 如果我们这里的方法块访问了外部的变量,而这个变量只是一个普通数据类型的话,相当于只是访问到了一份副本。当外部对这个变量进行修改时,lambda内部(只有副本)是无法感知到这个变量的修改的。
因此为了防止出现数据不同步的问题,java8就限制了:lambda访问局部普通数据类型变量时,需要用final修饰或使用隐式final的方法!
5.5 疑问解决
看起来方法引用省略了参数,那我们Intger.parseInt是去对谁操作?
- 其实是自己混淆了lambda,lambda定义的时候那个参数,根本不是实际的参数
可以说那个参数,只是为方法体服务的,只是方法体里边会用到. 而我们都用了方法引用了,前提就是参数和返回值一样,方法体也是我们想要实现的内容,这时自然而然都不用我们写方法体了,那方法体所依赖的参数也自然不用派上用场了
总结
Lambda表达式和方法引用的目的都是使用具体的方法来代替接口中抽象的方法,但是在实际使用中,调用的是接口中被实现的方法名
,lambda表达式和方法引用只应用于接口实例的方式实现了的构造过程,