Lambda表达式学习笔记(整理)

Lambda表达式

一.为什么要使用Lambda表达式?

在JDK8之前,如果我们想把某些功能传递给某个方法,总要自己去创建一个类或写一个匿名内部类,非常麻烦.Lambda表达式就很好的解决了这个问题.Lambda表达式是一个匿名函数,可理解为一段可以传递的代码(将代码想数据一样进行传递),使用它可以写出更加简洁,灵活的代码,使java语言表达能力达到了提升.

Lambda 表达式的基础语法:
          "->" 箭头操作符 或 Lambda 操作符
              左侧:Lambda 表达式的参数列表
              右侧:Lambda 表达式所需执行的功能,即Lambda体
    

      语法格式一:无参数,无返回值
          () -> System.out.println("Hello World");
      语法格式二:有一个参数,并且无返回值
          (e) -> System.out.println(e);
          小括号可以不写
          e -> System.out.println(e);
      语法格式三:有两个以上的参数,有返回值,并且Lambda 体中有多条语句
          如果只有一条语句,return和{}都可以省略不写(通用)
      语法格式四:Lambda 表达式的参数类型可以省略不写
          因为JVM编译器可以通过上下文推断出参数数据类型,叫做”类型推断“
     上联:左右遇一括号省
     下联:左侧推断类型省
     横批:能省就省
例子1:无参函数的简写

如果需要新建一个线程,一种常见的写法是这样:

// JDK7 匿名内部类写法
new Thread(new Runnable(){// 接口名
 @Override
 public void run(){// 方法名
  System.out.println("Thread run()");
 }
}).start();

上述代码给Tread类传递了一个匿名的Runnable对象,重载Runnable接口的run()方法来实现相应逻辑。这是JDK7以及之前的常见写法。匿名内部类省去了为类起名字的烦恼,但还是不够简化,在Java 8中可以简化为如下形式:

// JDK8 Lambda表达式写法
new Thread(
  () -> System.out.println("Thread run()")// 省略接口名和方法名
).start();

上述代码跟匿名内部类的作用是一样的,但比匿名内部类更进一步。这里连接口名和函数名都一同省掉了,写起来更加神清气爽。如果函数体有多行,可以用大括号括起来,就像这样:

// JDK8 Lambda表达式代码块写法
new Thread(
        () -> {
            System.out.print("Hello");
			System.out.println(" Hoolee");
        }
).start();
例子2:重写compare方法实现排序功能
//匿名内部类写法
public class LambdaDemo1 {

    public static void main(String[] args) {        
        //原有的重写compare的方法
        Comparator<Integer> com1 = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return Integer.compare(o1,o2);
            }
        };
        int compare1 = com1.compare(12,21);
        System.out.println(compare1);
       
    }
}
//Lambda表达式
public class LambdaDemo1 {
    public static void main(String[] args) {     
        //使用Lambda表达式的方法
        Comparator<Integer> com2 = ((o1, o2) -> Integer.compare(o1,o2));
        int compare2 = com2.compare(12,21);
        System.out.println(compare2);
    }
}       
例子3 获取当前公司中员工年龄大于35岁的员工信息
List<Employee> employees = Arrays.asList(
		//id、name、age、salary
        new Employee(1, "张三", 35, 9999.9),
        new Employee(2, "李四", 18, 18888.0),
        new Employee(3, "王五", 44, 14444.0),
        new Employee(4, "赵六", 23, 2333.0),
        new Employee(5, "钱七", 47, 14244.0),
        new Employee(6, "陈八", 34, 7444.0),
        new Employee(7, "曹九", 39, 944.0)
);

一般的做法是,直接写个过滤方法 如下

@Test
public void test3() {
    List<Employee> filter = filterEmployee(employees);
    for (Employee employee : filter) {
        System.out.println(employee);
    }
}
public List<Employee> filterEmployee(List<Employee> list) {
    List<Employee> emps = new ArrayList<>();
    for (Employee emp : list) {
        if (emp.getAge() > 35) {
            emps.add(emp);
        }
    }
    return emps;
}

但是如果又增加了需求,又要重新写一个方法,然后直接cope上面的只改一句话,这样代码冗长,繁琐
需求:获取当前公司中员工工资大于5000的员工信息

public List<Employee> filterEmployee2(List<Employee> list) {
    List<Employee> emps = new ArrayList<>();
    for (Employee emp : list) {
        //只需改变一句话,其他完全一样,代码冗长
        if (emp.getSalary() > 5000.0) {
            emps.add(emp);
        }
    }
    return emps;
}

优化方式一:引入策略设计模式
新建一个接口,和一个实现该接口的类

public interface MyPredicate<T> {
    public boolean test(T t);
}

--------------分割线----------------

public class FilterEmpByAge implements MyPredicate<Employee> {
    @Override
    public boolean test(Employee employee) {
        return employee.getAge()>35;
    }
}

然后新建方法,如果mp.test(emp)为true,则执行emp添加到新数组,完成过滤

@Test
public void test4(){
    MyPredicate<Employee> mp = new FilterEmpByAge();
    List<Employee> employees = filterEmployee(this.employees, mp);
    for (Employee employee : employees) {
        System.out.println(employee);
    }
}

public List<Employee> filterEmployee(List<Employee> list, MyPredicate<Employee> mp) {
    List<Employee> emps = new ArrayList<>();
    for (Employee emp : list) {
        if (mp.test(emp)) {
            emps.add(emp);
        }
    }
    return emps;
}

如果出现了新需求,只需要再写一个实现此接口的类,多多少少还是有点繁琐

优化方式二:匿名内部类
不用写一个实现此接口的类,直接通过匿名内部类的方式实现

@Test
    public void test6(){
        MyPredicate<Employee> mp = new MyPredicate<Employee>() {
            @Override
            public boolean test(Employee employee) {
                return employee.getSalary()>=9797;
            }
        };
        List<Employee> employees = filterEmployee(this.employees, mp);
        for (Employee employee : employees) {
            System.out.println(employee);
        }
    }

实现匿名内部类多多少少还是有点繁琐,毕竟有效代码就一句话
return employee.getSalary()>=9797;

优化方式三:lambda表达式
使用lambda表达式如下,做到了正在的简洁,只需要把最关键的东西写出来就行了

@Test
public void test7(){
    List<Employee> list = filterEmployee(employees, (e) -> e.getSalary() <= 5000);
    for (Employee employee : employees) {
        System.out.println(employee);
    }
}

在这之后还有一种方式,那就是Stream API

优化方式四:Stream API
这个极简,连filterEmployee方法都不用写就完成了过滤

@Test
public void test8(){
    employees.stream()
            .filter(e -> e.getSalary() >= 5000) //过滤
            .limit(1) //限制1个
            .forEach(System.out::println); //遍历
}
二.方法引用

方法引用主要有三种语法格式:
对象::实例方法名

类::静态方法名

类::实例方法名

注意:Lambda 体中调用方法的参数列表返回值类型,要与函数式接口中抽象方法的函数列表返回值类型保持一致

对象::实例方法名
	/**
     * 例子1
     */
    @Test
    public void test1(){
        //正常的Lambda表达式
        Consumer<String> con = (x) -> System.out.println(x);
        //方法引用
        //println为out对象的一个实例方法
        PrintStream ps = System.out;
        Consumer<String> con2 = ps::println;
        Consumer<String> con3 = System.out::println;
    }
	/**
     * 例子2
     */
    @Test
    public void test2(){
        Employee emp = new Employee(1,"niu",33,49999.0);
        //正常的Lambda表达式
        Supplier<String> sup = () -> emp.getName();
        System.out.println(sup.get());
        //方法引用
        //getSalary为emp对象的一个实例方法
        Supplier<Double> sup2 = emp::getSalary;
        System.out.println(sup2. get());
    }
    

三.Lambda参数

由于Java lambda表达式实际上只是方法,因此lambda表达式可以像方法一样接受参数。前面显示的lambda表达式的(oldState,newState)部分指定lambda表达式使用的参数。这些参数必须与函数式接口的抽象方法参数匹配。在当前这个示例,参数必须与StateChangeListener接口的onStateChange()方法的参数匹配:

public void onStateChange(State oldState, State newState);

首先,lambda表达式中的参数数量必须与方法匹配。

其次,如果你在lambda表达式中指定了任何参数类型,则这些类型也必须匹配。我还没有向你演示如何在lambda表达式参数上设置类型(本文稍后展示),但是在大多数情况下,你不会用到它。

无参数

如果lambda表达式匹配的方法无参数,则可以这样写lambda表达式:

() -> System.out.println("Zero parameter lambda");

请注意,括号中没有内容。那就是表示lambda不带任何参数。

一个参数

如果Java lambda表达式匹配的方法有一个参数,则可以这样写lambda表达式:

(param) -> System.out.println("One parameter: " + param);

请注意,参数在括号内列出。

当lambda表达式是单个参数时,也可以省略括号,如下所示:

 param -> System.out.println("One parameter: " + param);
指定参数类型

如果编译器无法从lambda匹配的函数式接口抽象方法推断参数类型,则有时可能需要为lambda表达式指定参数类型。不用担心,编译器会在这种情况下会有提醒。这是一个Java lambda指定参数类型示例:

(Car car) -> System.out.println("The car is: " + car.getName());
四.Lambda表达式主体

lambda表达式的主体以及它表示的函数/方法的主体在lambda声明中的->的右侧指定:

这是一个示例:

 (oldState, newState) -> System.out.println("State changed")

如果你的lambda表达式需要包含多行,则可以将lambda函数主体括在{}括号内,Java在其他地方声明方法时也需要将其括起来。这是一个例子:

(oldState, newState) -> {
    System.out.println("Old state: " + oldState);
    System.out.println("New state: " + newState);
  }
五.Lambda表达式返回值

你可以从Java lambda表达式返回值,就像从方法中返回值一样。你只需向lambda表达式主体添加一个return,如下所示:

 (param) -> {
    System.out.println("param: " + param);
    return "return value";
  }

如果你的lambda表达式只需要计算一个返回值并将其返回,则可以用更短的方式指定返回值。例如这个:

(a1, a2) -> { return a1 > a2; }
你可以写成:
 (a1, a2) -> a1 > a2;

然后,编译器会断定表达式 a1> a2 是lambda表达式的返回值。

六.Lambda作为对象

Java lambda表达式本质上是一个对象。你可以将变量指向lambda表达式并传递,就像处理其他任何对象一样。这是一个例子:

public interface MyComparator {

	public boolean compare(int a1, int a2);

}
MyComparator myComparator = (a1, a2) -> return a1 > a2;

boolean result = myComparator.compare(2, 5);

第一个代码块显示了lambda表达式实现的接口。

第二个代码块显示了lambda表达式的定义,lambda表达式如何分配给变量,以及最后如何通过调用其实现的接口方法来调用lambda表达式。

七.变量捕获

在某些情况下,Java lambda表达式能够访问在lambda表达式主体外部声明的变量。

Java lambdas可以捕获以下类型的变量:

局部变量
实例变量
静态变量
这些变量捕获的每一个将在以下各节中进行描述。

局部变量捕获

Java lambda可以捕获在lambda主体外部声明的局部变量的值。为了说明这一点,首先看一下这个函数式接口:

public interface MyFactory {
    public String create(char[] chars);
}

现在,看一下实现MyFactory接口的lambda表达式:

MyFactory myFactory = (chars) -> {
    return new String(chars);
};

现在,此lambda表达式仅引用传递给它的参数值(chars)。但是我们可以改变一下。这是引用在lambda函数主体外部声明的String变量的更新版本:

String myString = "Test";

MyFactory myFactory = (chars) -> {
    return myString + ":" + new String(chars);
};

如你所见,lambda表达式主体现在引用了在lambda表达式主体外部声明的局部变量myString。当且仅当被引用的变量是“有效只读(如果一个局部变量在初始化后从未被修改过,那么它就是有效只读)”时才有可能,这意味着在赋值之后它不会改变其值。如果myString变量的值稍后更改,则编译器将抱怨从lambda主体内部对其的引用。

实例变量捕获

Lambda表达式还可以捕获创建Lambda的对象中的实例变量。这是示例:

public class EventConsumerImpl {

private String name = "MyConsumer";

public void attach(MyEventProducer eventProducer){
    eventProducer.listen(e -> {
        System.out.println(this.name);
    });
  }
}

注意lambda表达式主体中对this.name的引用。这将捕获封闭的EventConsumerImpl对象的 name 实例变量。甚至可以在捕获实例变量后更改其值——该值将反映在lambda内部。

this语义实际上是Java lambda与接口的匿名实现不同的地方之一。匿名接口实现可以有自己的实例变量,这些实例变量可以通过this进行引用。但是,lambda不能拥有自己的实例变量,因此它始终指向封闭的对象。

注意:EventConsumer的设计不是很优雅。我只是这样写来说明实例变量捕获。

静态变量捕获

Java lambda表达式还可以捕获静态变量。

因为只要可以访问静态变量(包作用域或public作用域),Java应用程序中的任何地方都可以访问静态变量。

这是一个创建lambda表达式的示例类,该lambda表达式从lambda表达式主体内部引用静态变量:

public class EventConsumerImpl {
    private static String someStaticVar = "Some text";

public void attach(MyEventProducer eventProducer){
    eventProducer.listen(e -> {
        System.out.println(someStaticVar);
    });
	}
}

lambda捕获到静态变量后,它的值也可以更改。

同样,上述类设计不太合理。不要对此考虑太多。该类主要用于向你显示lambda表达式可以访问静态变量。

八.Lambda方法引用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A8rayMzo-1664711933278)(D:\学习资料\面试\整理面经\lambda方法引用与构造引用.png)]

如果你的lambda表达式所做的只是用传递给lambda的参数调用另一个方法,则Java lambda实现提供了更简洁的方式表示该方法调用。

首先,这是一个函数式接口:

public interface MyPrinter{
    public void print(String s);
}

以下是创建实现MyPrinter接口的Java lambda表达式的示例:

MyPrinter myPrinter = (s) -> { System.out.println(s); };

由于lambda主体仅由一个语句组成,因此我们实际上可以省略括号{}。另外,由于lambda方法只有一个参数,因此我们可以省略该参数周围的括号()。更改之后的lambda表达式:

MyPrinter myPrinter = s -> System.out.println(s);

由于所有lambda主体所做的工作都是将字符串参数转发给System.out.println()方法,因此我们可以将上述lambda声明替换为方法引用。以下是lambda表达式引用方法的实例:

MyPrinter myPrinter = System.out::println;

注意双冒号::。它会向Java编译器发出信号,这是方法引用。引用的方法是双冒号之后的内容。拥有被引用方法的任何类或对象都在双冒号之前。

你可以引用以下类型的方法:

静态方法
参数对象的实例方法
实例方法
构造方法
以下各节介绍了每种类型的方法引用。

静态方法引用

最容易引用的方法是静态方法。

首先是函数式接口的示例:

public interface Finder {
    public int find(String s1, String s2);
}

这是一个静态方法:

public class MyClass{
    public static int doFind(String s1, String s2){
        return s1.lastIndexOf(s2);
    }
}

最后是引用静态方法的Java lambda表达式:

Finder finder = MyClass::doFind;

由于Finder.find()和MyClass.doFind()方法的参数匹配,因此可以创建实现Finder.find()并引用MyClass.doFind()方法的lambda表达式。

参数方法引用

也可以将其中一个参数的方法引用到lambda。

函数式接口如下:

public interface Finder {
    public int find(String s1, String s2);
}

该接口用于表示能在s1中搜索s2的出现的部分。下面是一个Java lambda表达式的示例,它调用indexOf() 搜索:

Finder finder = String::indexOf;

这等价以下lambda定义:

Finder finder = (s1, s2) -> s1.indexOf(s2);

请注意简洁方式版本是如何引用单个方法的。Java编译器尝试将引用的方法与第一个参数类型相匹配,使用第二个参数类型作为被引用方法的参数。

实例方法引用

第三,还可以从lambda表达式中引用实例方法。

首先,让我们来看一个函数式接口定义:

public interface Deserializer {
    public int deserialize(String v1);
}

此接口表示一个组件,该组件能够将字符串“反序列化”为int。

现在看看这个StringConverter类:

public class StringConverter {
    public int convertToInt(String v1){
        return Integer.valueOf(v1);
    }
}

convertToInt()方法与Deserializer deserialize()方法的deserialize()方法具有相同的签名。因此,我们可以创建StringConverter的实例,并从Java lambda表达式引用其convertToInt()方法,如下所示:

StringConverter stringConverter = new StringConverter();

Deserializer des = stringConverter::convertToInt;

第二行创建的lambda表达式引用在第一行创建的StringConverter实例的convertToInt方法。

构造方法引用

最后,可以引用一个类的构造方法。你可以通过在类名后加上:: new来完成此操作,如下所示:

MyClass::new

来看看如何在lambda表达式中引用构造方法。

函数式接口定义如下:

public interface Factory {
    public String create(char[] val);
}

此接口的create()方法与String类中某个构造函数的签名匹配。因此,此构造函数可以被lambda表达式用到。下面是一个示例:

Factory factory = String::new;

等同于如下lambda表达式:

Factory factory = chars -> new String(chars);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值