文章目录
Lambda表达式
Lambda表达式的介绍
Lambda表达式是在java规范提案JSR 335中定义的,Java 8 中引入了Lambda表达式,并被认为是Java 8最大的新特性,Lambda表达式促进了函数式编程,简化了Java编程开发。它允许你通过表达式来代替功能接口。 lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体 (body,可以是一个表达式或一个代码块)。
不仅仅简化了Java编程开发,Lambda表达式还增强了集合库。 Java SE 8添加了2个对集合数据进行批量操作的包: java.util.function 包以及 java.util.stream 包。 流(stream)就如同迭代器(iterator), 但附加了许多额外的功能。
总的来说,lambda表达式和 stream 是自Java语言添加泛型(Generics)和注解(annotation)以来最大的变化。 本文的重点放在Lambda表达式上
扯点函数式编程思想:
在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。
Lambda表达式的语法
在介绍语法之前我们可以体验一下:Lambda表达式
public class Main{
public static void main(String[] args) {
//实现多线程程序
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"新线程创建了");
}
};
new Thread(r).start();
//简化代码
new Thread(new Runnable() {
//使用匿名内部类创建多线程
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"新线程创建了");
}
}).start();
//使用Lambda表达式创建多线程
new Thread( ( ) ->{
System.out.println(Thread.currentThread().getName() + "新线程创建了");
}
).start();
}
}
结果:
很容易发现Lambda表达式是非常简洁的,使用起来其实也是比较的简单的,其实从本质上来说Lambda表达式就是一种语法糖。
Lambda完全没有面向对象复杂的束缚。但在使用的时候,要明确它的适用前提:
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
无论是JDK内置的 Runnable 、 Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。 - 使用Lambda必须具有上下文推断。
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
备注:有且仅有一个抽象方法的接口,称为“函数式接口”。
那我们看一下它的语法吧:
parameter -> expression body
主要组成:参数列表 + 箭头 + 表达式体,如 (int x, int y) -> x + y
其中,表达式体可以是一条表达式,也可以是一个语句块(多条代码语句);
Lambda表达式具有如下特征:
【可选】类型声明:参数的类型不需要声明,编译器可以根据参数值推断出其类型;
【可选】括号:单个参数的话,不需要用圆括号包围参数,当然,对于多个参数或无参数的话,括号是需要的;
【可选】花括号:如果表达式主体只有一条语句的话,不需要用花括号包围,当然,对于多条语句,花括号是需要的;
【可选】return关键字:如果表达式主体是单一表达式,return关键字可以不写,编译器可以自动返回该值,当然,如果写了return,则需要加上花括号;
演示:
public class LambdaTest
{
public static void main(String args[])
{
LambdaTest tester = new LambdaTest();
// 有参数类型
MathOperation addition = (int a, int b) -> a + b;
// 无参数类型
MathOperation subtraction = (a, b) -> a - b;
// 有花括号,有return关键字
MathOperation multiplication = (int a, int b) -> {
return a * b;
};
// 无花括号,无return关键字,单一表达式情况
MathOperation division = (int a, int b) -> a / b;
// MathOperation调用示例
System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
System.out.println("10 / 5 = " + tester.operate(10, 5, division));
// 有括号
GreetingService greetService1 = message -> System.out.println("Hello " + message);
// 无括号,单个参数情况
GreetingService greetService2 = (message) -> System.out.println("Hello " + message);
// GreetingService调用示例
greetService1.sayMessage("Mahesh");
greetService2.sayMessage("Suresh");
//有括号, 无参情况
Runnable runTest = () -> System.out.println("Running");
//Runnable调用示例
runTest.run();
}
// 内部接口
interface MathOperation
{
int operation(int a, int b);
}
interface GreetingService
{
void sayMessage(String message);
}
interface Runnable
{
void run();
}
private int operate(int a, int b, MathOperation mathOperation)
{
return mathOperation.operation(a, b);
}
}
Method References
Method References指的是方法引用,简单地说,就是一个Lambda表达式。在Java 8中,我们会使用Lambda表达式创建匿名方法,但是有时候,我们的Lambda表达式可能仅仅调用一个已存在的方法,而不做任何其它事,对于这种情况,通过一个方法名字来引用这个已存在的方法会更加清晰,Java 8的方法引用允许我们这样做。方法引用是一个更加紧凑,易读的Lambda表达式,注意方法引用是一个Lambda表达式,其中方法引用的操作符是双冒号"::"。
演示一下,你就明白了
import java.time.LocalDate;
public class Person
{
String name;
LocalDate birthday;
public Person(String name, LocalDate birthday)
{
this.name = name;
this.birthday = birthday;
}
public LocalDate getBirthday()
{
return birthday;
}
public static int compareByAge(Person a, Person b)
{
return a.birthday.compareTo(b.birthday);
}
@Override
public String toString()
{
return this.name;
}
}
我们对Person类进行排序
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Comparator;
public class Main
{
static class PersonAgeComparator implements Comparator<Person> {
public int compare(Person a, Person b) {
return a.getBirthday().compareTo(b.getBirthday());
}
}
public static void main(String[] args)
{
Person[] pArr = new Person[]{
new Person("003", LocalDate.of(2016,9,1)),
new Person("001", LocalDate.of(2016,2,1)),
new Person("002", LocalDate.of(2016,3,1)),
new Person("004", LocalDate.of(2016,12,1))};
Arrays.sort(pArr, new PersonAgeComparator());
System.out.println(Arrays.asList(pArr));
}
}
结果:
其中,Arrays类的sort方法定义如下:
public static <T> void sort(T[] a, Comparator<? super T> c)
这里,我们首先要注意Comparator接口是一个函数式接口,因此我们可以使用Lambda表达式,而不需要定义一个实现Comparator接口的类,并创建它的实例对象,传给sort方法。
使用Lambda表达式,我们可以这样写:
😇 一,使用Lambda表达式,未调用已存在的方法
import java.time.LocalDate;
import java.util.Arrays;
public class Main
{
public static void main(String[] args)
{
Person[] pArr = new Person[]{
new Person("003", LocalDate.of(2016,9,1)),
new Person("001", LocalDate.of(2016,2,1)),
new Person("002", LocalDate.of(2016,3,1)),
new Person("004", LocalDate.of(2016,12,1))};
Arrays.sort(pArr, (Person a, Person b) -> {
return a.getBirthday().compareTo(b.getBirthday());
});
System.out.println(Arrays.asList(pArr));
}
}
然而,在以上代码中,关于两个人生日的比较方法在Person类中已经定义了,因此,我们可以直接使用已存在的Person.compareByAge方法。
😇 二、使用Lambda表达式,调用已存在的方法
import java.time.LocalDate;
import java.util.Arrays;
public class Main
{
public static void main(String[] args)
{
Person[] pArr = new Person[]{
new Person("003", LocalDate.of(2016,9,1)),
new Person("001", LocalDate.of(2016,2,1)),
new Person("002", LocalDate.of(2016,3,1)),
new Person("004", LocalDate.of(2016,12,1))};
Arrays.sort(pArr, (a, b) -> Person.compareByAge(a, b));
System.out.println(Arrays.asList(pArr));
}
}
因为这个Lambda表达式调用了一个已存在的方法,因此,我们可以直接使用方法引用来替代这个Lambda表达式,
😇 三,使用方法引用
import java.time.LocalDate;
import java.util.Arrays;
public class Main
{
public static void main(String[] args)
{
Person[] pArr = new Person[]{
new Person("003", LocalDate.of(2016,9,1)),
new Person("001", LocalDate.of(2016,2,1)),
new Person("002", LocalDate.of(2016,3,1)),
new Person("004", LocalDate.of(2016,12,1))};
Arrays.sort(pArr, Person::compareByAge);
System.out.println(Arrays.asList(pArr));
}
}
在以上代码中,方法引用Person::compareByAge在语义上与Lambda表达式 (a, b) -> Person.compareByAge(a, b) 是等同的,都有如下特性:
- 真实的参数是拷贝自Comparator<Person>.compare方法,即(Person, Person);
- 表达式体调用Person.compareByAge方法;
四种方法引用类型
静态方法引用
我们前面举的例子Person::compareByAge就是一个静态方法引用。
特定实例对象的方法引用
如下示例,引用的方法是myComparisonProvider 对象的compareByName方法;
class ComparisonProvider
{
public int compareByName(Person a, Person b)
{
return a.getName().compareTo(b.getName());
}
public int compareByAge(Person a, Person b)
{
return a.getBirthday().compareTo(b.getBirthday());
}
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);
任意对象(属于同一个类)的实例方法引用
如下示例,这里引用的是字符串数组中任意一个对象的compareToIgnoreCase方法。
String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);
构造方法引用
如下示例,这里使用了关键字new,创建了一个包含Person元素的集合。
Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);
transferElements方法的定义如下,功能为集合拷贝,
public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
DEST transferElements(
SOURCE sourceCollection,
Supplier<DEST> collectionFactory) {
DEST result = collectionFactory.get();
for (T t : sourceCollection) {
result.add(t);
}
return result;
}
适合使用方法引用的情况
当一个Lambda表达式调用了一个已存在的方法
不适合使用方法引用的情况
当我们需要往引用的方法传其它参数的时候,不适合,如下示例:
IsReferable demo = () -> ReferenceDemo.commonMethod("Argument in method.");
Default methods
Default methods 是默认方法的意思
简单的说,就是可以在接口中定义一个已实现方法,且该接口的实现类不需要实现该方法;
举个例子
interface GOService
{
void sayMessage(String message);
//可以在接口中定义默认方法
default void sayHello(){
System.out.println("Hello");
}
}
//实现类不需要实现接口中的默认方法
class GoServiceImpl implements GoService{
@Override
public void sayMessage(String message)
{
}
}
默认方法主要是为了方便扩展已有接口;如果没有默认方法,加入给JDK中的某个接口添加一个新的抽象方法,那么所有实现了该接口的类都得修改,影响将非常大。
使用默认方法,可以给已有接口添加新方法,而不用修改该接口的实现类。当然,接口中新添加的默认方法,所有实现类也会继承该方法。
举个例子,在Java 8的Iterable接口中,新增了一个默认方法forEach,也正因为forEach是默认方法,才不用修改所有Iterable接口的实现类。
Iterable接口新增的forEach方法如下(入参是一个函数式接口,因此支持Lambda表达式):
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
因为Collection接口继承了Iterable接口,所以我们可以在集合类中使用forEach方法,如下,这里使用了方法引用
List<String> list = new ArrayList<String>();
list.add("001");
list.add("002");
list.forEach(System.out::println);
可见,我们在未破坏Iterable接口实现类的前提下,给Iterable接口的所有实现类添加了一个新方法forEach,这在Java 8之前是不可能的。
关于重写 Override默认方法:
如果子类没有重写父接口默认方法的话,会直接继承父接口默认方法的实现;
如果子类重写父接口默认方法为普通方法,则与普通方法的重写类似;
如果子类(接口或抽象类)重写父接口默认方法为抽象方法,那么所有子类的子类需要实现该方法;
关于默认方法调用冲突
因为一个类是可以实现多个接口的,如果多个接口定义了同样的默认方法,那么子类如何调用父类的默认方法呢?
具体调用流程如下:
1、首先,如果子类覆盖了父类的默认方法,那么什么也不用想,直接使用调用子类覆盖后的方法;
2、其次,优先选择调用更加具体的接口默认方法,什么意思呢,举个例子,如果A1接口继承A接口,那么A1接口相对A接口就更加具体,当C类实现了A1接口的时候,就优先调用A1接口的默认方法;
3、最后,如果C类同时实现A1接口和A2接口,且A1和A2有同名的默认方法,那么选择哪个接口的默认方法呢?答案是编译器报错,提示定义了重名的方法,快速修复方式是覆盖其中的一个即可;
尽管有默认方法这一说,但是万不得已不建议使用!!!
Functional Interface
Functional Interface是函数式接口的意思
所谓的函数式接口,是在接口里面只能有一个抽象方法。这种类型的接口也称为SAM接口,即Single Abstract Method
interfaces。
它们主要用在Lambda表达式和方法引用(实际上也可认为是Lambda表达式)上。
比如说定义了一个函数式接口如下:
@FunctionalInterface
interface MakeFood{
void doSomething();
}
那么就可以使用Lambda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的)
MakeFood makeFood = ()-> System.out.println("蒸包子");
你会注意到定义函数式接口的时候有一个注解 @FunctionalInterface
这是Java 8为函数式接口引入的新注解,主要用于编译级错误检查,加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。
比如说接口中包含了两个抽象方法,这就违反了函数式接口的定义,一般IDE会提示你。
😇 函数式接口里允许定义默认方法
函数式接口里是可以包含默认方法,因为默认方法不是抽象方法,其有一个默认实现,所以是符合函数式接口的定义的;
如下代码不会报错:
@FunctionalInterface
interface GOService
{
void sayMessage(String message);
default void doSomeMoreWork1()
{
// Method body
}
default void doSomeMoreWork2()
{
// Method body
}
}
😇 函数式接口里允许定义静态方法
函数式接口里是可以包含静态方法,因为静态方法不能是抽象方法,是一个已经实现了的方法,所以是符合函数式接口的定义的;
如下代码不会报错:
@FunctionalInterface
interface GOService
{
void sayMessage(String message);
static void printHello(){
System.out.println("Hello");
}
}
😇 函数式接口里允许定义 java. lang Object里的 public方法
函数式接口里是可以包含Object里的public方法,这些方法对于函数式接口来说,不被当成是抽象方法(虽然它们是抽象方法);因为任何一个函数式接口的实现,默认都继承了Object类,包含了来自java.lang.Object里对这些抽象方法的实现;
如下代码不会报错:
@FunctionalInterface
interface GreetingService
{
void sayMessage(String message);
@Override
boolean equals(Object obj);
}
对接口底层不熟悉,可以参考这篇文章:【Java核心技术卷】深入理解Java的接口
浅析Lambda表达式的本质
前面也说了Lambda表达式本质是一种语法糖,那么我们反编译一下,看看编译器为我们做了哪些事情
原始代码
import java.util.ArrayList;
import java.util.List;
public class Lambda {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
list.forEach(System.out::println);
}
}
编译后代码
import java.io.PrintStream;
import java.lang.invoke.LambdaMetafactory;
import java.util.ArrayList;
import java.util.function.Consumer;
public class Lambda {
public static void main(String[] arrstring) {
ArrayList<Integer> arrayList = new ArrayList<Integer>();
for (int i = 0; i < 10; ++i) {
arrayList.add(i);
}
PrintStream printStream = System.out;
printStream.getClass();
arrayList.forEach(
(Consumer<Integer>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, println(java.lang.Object ), (Ljava/lang/Integer;)V)((PrintStream)printStream));
}
}
这里可以简单看出lambda是在一个类文件中,最多生成多个方法而已,这可以与内部类区分开,内部类是生成了多个类文件。当然编译器处理Lambda语法糖的方式差不多了,其他情况自己试试呗~,当然Lambda本质这一块还涉及到类型推断相关的内容,
类型推断
的大致意思是Java编译器来查看每一个方法调用和相应的声明,以确定类型参数(或参数),使调用能够正常实现。推理算法确定参数类型,如果类型推断成功,那么方法返回的值就是那个类型的。最后,推理算法试图找到与所有的变量工作的最具体类型。这又是一大块内容了,想要更进一步了解的,搜搜呗~
如果你不了解内部类底层,可以参考我之前写的这篇文章:【Java核心技术卷】深入理解Java的内部类
本文是基于《Java 8实战》这本书的思考与学习的总结笔记,含少量内容的摘录。
在方法引用部分参考了 平凡希 – Java8之方法引用
转载:https://blog.csdn.net/qq_42322103/article/details/101681233