主要内容
Lambda表达式简介
lambda表达式是JDK8新增的功能,可在一定程度上简化程序的代码。我们都知道,java在lambda表达式出现之前,它是一个纯面向对象的编程语言,lambda表达式的出现可以理解为java8对函数式编程的特定支持。lambda表达式由两个部分组成,它们分别是lambda表达式的声明和定义。声明部分的学术名称叫函数式接口,定义部分就是这个接口的实现。
最简单的lambda表达式
有一定编程经验的人可能会经常见到如下的代码
1 |
(n) -> n+1; |
实际上这一行简单的代码就是函数式接口的实现
,它由两个部分组成,第一个部分是箭头左边的参数列表,多个参数可用逗号( , )分隔;第二个部分是右边的表达式。上述lambda表达式的作用是对给定的参数n加一后返回。
当参数只有一个的时候,可把括号省略:
1 |
n -> n+1; |
什么是函数式接口
lambda表达式也是一种类型。例如Integer,String都是一种类型,类型的使用一般都会经过声明和赋值,例如:
1 |
String name = new String("hello"); |
n -> n+1 可以理解为lambda表达式的赋值部分,处于等号左边的就是函数式接口,也就是说,函数式接口是lambda表达式的类型定义。
函数式接口的创建
函数式接口和普通的java接口几乎一样,不同的是,函数式接口里面只能定义一个方法。例如:
1 2 3 4 |
@FunctionalInterface public Interface AddNumber{ int add(int i); } |
接口中的方法接受一个int类型的参数,返回值也是int。@FunctionalInterface
注解不是必须的,添加这个注解有助于我们在创建接口的时候就可以发现错误,例如接口里面添加了两个方法。创建了接口之后,就可以像普通对象一样创建lambda表达式:
1 2 3 |
AddNumber addNumber = n -> n+1; //n -> n+1实际上就是add方法的具体实现 System.out.println(addNumber.add(10)); |
函数式接口并不要求方法的实现是唯一的,只要它们的实现是兼容的,就可以多次实现接口的方法(和变量的多次赋值类似)。兼容是指方法签名一样,具体的实现可以不一样:
1 2 3 4 5 6 7 8 9 |
AddNumber addNumber = n -> n+1; //n -> n+1实际上就是add方法的具体实现 System.out.println(addNumber.add(10)); //第二个实现 addNumber = n -> n * 2; //输出结果 40 System.out.println(addNumber.add(20)); |
在第二个实现中,和第一个实现一样都是接受一个参数且返回int类型,但是第二个实现中是把参数乘2。
类型推断
一般情况下,我们不需要对lambda表达式的参数类型进行显式声明,编译器可以根据实际情况对参数的类型进行推断。当然你写上类型也不会错,只会显得多此一举:
1 |
(int n) -> n+1; |
如果有多个参数,一旦显式声明了其中一个参数的类型,那么必须为所有的参数声明类型:
1 2 3 4 5 |
//正确写法 (int n, int d) -> n * d; //错误写法 (int n, d) -> n * d; |
lambda表达式的两种类型
这两种类型分别是:
- 只有一行代码的lambda表达式
- 具有多行代码的lambda表达式
一行代码的表达式已经介绍过,具有多行代码的表达式叫做块表达式,代码使用{}
括起来。和单行的lambda表达式不同,块表达式的return关键字不能省略,例如:
1 2 3 4 |
addNumber = (n) -> { int i = 1; return n+i; }; |
变量捕获
lambda表达式可以访问其外层作用域内定义的变量,这个被访问的变量就是被捕获的变量。这个时候实际上形成了其他语言中的闭包。后果是被捕获后的变量会自动转变为final。
正确的代码:
1 2 3 4 5 6 7 8 9 |
public class Main { public static void main(String[] args) { int i = 20; //变量i被捕获 AddNumber addNumber = (n) -> n + i; System.out.println(addNumber.add(10)); } } |
错误的代码:
之所以有这个限制,是为了线程安全着想。试想一下,如果多个线程可以随意对一个变量进行修改,那么被修改的变量最后是一个什么值?其糟糕程度是无法想象。
泛型函数式接口
当lambda表达式操作的参数只有类型不同的时候,使用泛型函数式接口可以大大减少程序的代码量,泛型函数式接口类似java的方法重载。
例如前面的add方法,除了操作int类型外,我们还想操作double类型,那么,使用泛型函数式接口是一个好办法。
1 2 3 4 |
public interface AddNumber<T> { T add(T number); } |
接口的实现:
1 2 3 4 5 6 7 |
AddNumber<Integer> addInteger = n -> n + 20; System.out.println(addInteger.add(20)); AddNumber<Double> addDouble = n -> n * 20; System.out.println(addDouble.add(20.3)); |
作为参数传递
lambda表达式作为函数的参数来传递是最常见的需求。因为lambda表达式是一种类型,自然地,它就可以作为参数来传递。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Main { public static void main(String[] args) { SortFunc func = n -> Arrays.sort(n); int[] numbs = new int[]{10, 20, 3, 5, 6}; //使用lambda表达式进行排序 sort(func, numbs); System.out.println(Arrays.toString(numbs)); } public static void sort(SortFunc func, int[] data2bSort) { func.sort(data2bSort); } interface SortFunc { void sort(int[] nums); } } |
方法引用
方法引用可以使我们创建的函数式接口和实现相分离。也就是说,接口是我们创建的,但它的实现可以不是我们自己写的,而是通过用现有的方法为它赋值的方式来实现。当然,这个现有的方法必须和接口中的方法兼容。JDK8定义了一个新的操作符来使用方法引用,它就是 ::
方法引用有三种方式,分别是:
- 类::静态方法
- 对象::实例方法
- 类::实例方法
类::静态方法
下面的例子使用的是类::静态方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Main { public static void main(String[] args){ //doAdd方法传入的是其他人实现的方法 int result = doAdd(Calculator::add, 20, 20); System.out.println(result); } private static int doAdd(MyNumber myNumber, int a, int b){ return myNumber.add(a, b); } } //假设Calculator不是我们自己创建的 class Calculator{ static int add(int a, int b){ return a+b; } } |
从上面的例子可以看到,我们并没有实现lambda表达式,只是在需要的时候把和接口兼容的方法传递过去。
对象::实例方法
对象::实例方法和类::静态方法基本一样,不同的只是传递方法之前要把源对象先实例化出来,还是上面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class Main { public static void main(String[] args){ //实例化Calculator对象 Calculator calc = new Calculator(); //传递实例方法的引用 int result = doAdd(calc::add, 20, 20); System.out.println(result); } private static int doAdd(MyNumber myNumber, int a, int b){ return myNumber.add(a, b); } } //假设Calculator不是我们自己创建的 class Calculator{ //add方法已经不是静态 int add(int a, int b){ return a+b; } } |
类::实例方法引用
这种方式的引用好处是无需要实例化对象,就可以对类的任意对象方法进行引用。不过需要注意的是,对于使用方法引用的函数中,该函数的第一个参数的类型必须与这个类一致,因为该实例方法在运行时由第一个参数执行。这段话可能难以理解,但结合下面的代码来看,就很好理解。
例如:
1 2 3 4 5 6 7 8 9 10 11 |
public class Main { public static void main(String[] args) { String[] stringsArray = {"hello", "lambda", "expression"}; Arrays.sort(stringsArray, String::compareToIgnoreCase); System.out.println(Arrays.toString(stringsArray)); } } |
Arrays.sort方法就是使用方法引用的函数,它的第一个参数是stringsArray,类型是String[],和后面的方法引用类型不一致。不过,由于sort方法中使用了循环,每次处理的对象就是String类型的,和String::compareToIgnoreCase
中的String类型一致。
转载请注明:Pure nonsense » java 8中的lambda表达式