函数式编程(二)Lambda表达式

一、摘要

Lambda表达式是Java 8引入的一种函数式编程特性,它可以用于替代匿名内部类或重复的代码块,使代码更加简洁和易读。Lambda表达式的使用方式如下:

  • 语法形式:Lambda表达式的语法由箭头符号(->)分隔为两部分:左侧是参数列表,右侧是方法体。例如,(param) -> expression是一个简单的Lambda表达式。
  • 函数接口:Lambda表达式通常与函数接口(Functional Interface)一起使用。函数接口是只有一个抽象方法的接口,可以用作Lambda表达式的目标类型。例如,RunnableComparator等都是函数接口。
  • 参数列表:Lambda表达式的参数列表可以为空,也可以包含一个或多个参数。参数的类型可以显式声明,也可以根据上下文进行推断。
  • 方法体:Lambda表达式的方法体可以是一个表达式或一个代码块。如果方法体是一个表达式,则可以直接返回结果。如果方法体是一个代码块,则需要使用大括号括起来,并且需要使用return语句返回结果。

下面是一些Lambda表达式的使用示例:

// Lambda表达式示例1:无参数,无返回值
Runnable runnable = () -> System.out.println("Hello, Lambda!");

// Lambda表达式示例2:带参数,无返回值
Consumer consumer = (name) -> System.out.println("Hello, " + name);

// Lambda表达式示例3:带参数,有返回值
Function square = (num) -> num * num;

// Lambda表达式示例4:多个参数,有返回值
BiFunction sum = (a, b) -> a + b;

在上面的示例中,我们展示了Lambda表达式的不同使用方式。根据具体的需求,可以根据参数和返回值的类型来定义Lambda表达式,并将其赋值给相应的函数接口变量。Lambda表达式使代码更加简洁和易读,提高了开发效率。

二、引用方式

它可以替代Lambda表达式,将方法的调用作为值传递。方法引用的语法由双冒号(::)分隔为两部分:左侧是类名或对象名,右侧是方法名。例如,System.out::println是一个方法引用,引用了System.out对象的println()方法。

Lambda方法引用的语法形式有以下几种:

  1. 静态方法引用:语法( 类名::静态方法名 )。例如,Math::sqrt引用了Math类的静态方法sqrt
  2. 实例方法引用:语法( 对象::实例方法名 )。例如,str::toUpperCase引用了字符串对象str的实例方法toUpperCase
  3. 类实例方法引用:语法( 类名::实例方法名 )。例如,String::length引用了String类的实例方法length
  4. 构造方法引用:语法( 类名::new )。例如,ArrayList::new引用了ArrayList类的构造方法。
  5. 数组引用:语法( 数组[]::new )。例如,int[]::new 指创建了一个新的int类型的数组

静态方法引用

用于引用已存在的静态方法。它可以用于直接调用静态方法,并将其作为函数式接口的实例。

Lambda静态方法引用的语法形式是类名::静态方法名,其中类名是要引用的类的名称,静态方法名表示静态方法的名称。

// 静态方法引用示例
Function parseInt = Integer::parseInt;
int number = parseInt.apply("123"); // 调用Integer类的静态方法parseInt

// 使用自定义的静态方法
Function toUpperCase = StringUtils::toUpperCase;
String result = toUpperCase.apply("hello"); // 调用自定义的静态方法toUpperCase

实例方法引用

用于引用已存在的对象的实例方法。它可以用于直接调用对象的实例方法,并将其作为函数式接口的实例。

Lambda实例方法引用的语法形式是对象::实例方法名,其中对象是要引用的对象,实例方法名表示实例方法的名称。

// 实例方法引用示例
List names = Arrays.asList("Alice", "Bob", "Charlie");
Consumer printName = System.out::println;
names.forEach(printName); // 调用System.out对象的println方法打印每个名字

// 使用自定义的实例方法
List numbers = Arrays.asList(1, 2, 3, 4, 5);
Function square = Integer::square;
numbers.stream()
       .map(square)
       .forEach(System.out::println); // 调用Integer对象的square方法计算每个数的平方并打印

类实例方法引用

用于引用已存在的类的实例方法。它可以用于直接调用类的实例方法,并将其作为函数式接口的实例。

Lambda类实例方法引用的语法形式是类名::实例方法名,其中类名是要引用的类的名称,实例方法名表示实例方法的名称。

// 类实例方法引用示例
List names = Arrays.asList("Alice", "Bob", "Charlie");
Function lengthFunction = String::length;
names.stream()
     .map(lengthFunction)
     .forEach(System.out::println); // 调用String类的实例方法length计算每个名字的长度并打印

构造方法引用

Lambda表达式可以使用构造方法引用来引用无参构造方法和有参构造方法。用于引用已存在的构造方法。它可以用于创建新对象,并将构造方法作为函数式接口的实例。

Lambda构造方法引用的语法形式是类名::new,其中类名是要引用的类的名称,new表示构造方法。

public class Employee {
    private Integer id;
    private String name;
    private Integer age;

    @Override
    public String toString() {
        return "Employee{" +
        "id=" + id +
        ", name='" + name + '\'' +
        ", age=" + age +
        '}';
    }
    public Employee(){

    }

    public Employee(Integer id) {
        this.id = id;
    }

    public Employee(Integer id, Integer age) {
        this.id = id;
        this.age = age;
    }

    public Employee(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
}

public class Main {
    public static void main(String[] args) {
        //引用无参构造器
        Supplier supplier=Employee::new;
        System.out.println(supplier.get());
        //引用有参构造器
        Function function=Employee::new;
        System.out.println(function.apply(21));
        BiFunction biFunction=Employee::new;
        System.out.println(biFunction.apply(8,24));
    }
}

数组引用

可以用于创建新数组或者访问已存在的数组。

Lambda数组引用的语法形式是类型[]::new,其中类型表示要引用的数组元素类型。

// 创建新数组
Function newArray = int[]::new;
int[] numbers = newArray.apply(5); // 创建一个包含5个整数的新数组

// 访问已存在的数组
String[] names = {"Alice", "Bob", "Charlie"};
Consumer printArray = System.out::println;
printArray.accept(names); // 打印已存在的字符串数组

三、访问作用域

Lambda表达式的作用域是指在哪个范围内可以访问Lambda表达式中定义的变量。

Lambda表达式可以访问以下类型的变量:

  1. 局部变量:Lambda表达式可以访问包含它的方法或代码块中的局部变量。但是,这些局部变量必须是final或者是事实上的final(即不可修改)。Lambda表达式可以读取这些变量的值,但不能修改它们。
  2. 局部引用:与局部变量类似,Lambda表达式也可以访问包含它的方法或代码块中的局部引用。同样,这些引用必须是final或者是事实上的finalLambda表达式可以使用这些引用来调用方法或访问对象的属性,但不能修改引用所指向的对象。
  3. 静态变量:Lambda表达式可以访问包含它的类的静态变量。它可以读取和修改静态变量的值。
  4. 实例变量:Lambda表达式可以访问包含它的类的实例变量。它可以读取和修改实例变量的值。

下面是一个示例代码,演示了Lambda表达式如何访问局部变量、局部引用、静态变量和实例变量:

public class LambdaVariableExample {
    private static int staticVar = 10;
    private int instanceVar = 20;

    public void testLambdaVariables() {
        int localVar = 30;
        String localRef = "Hello";

        MyFunctionalInterface myLambda = () -> {
            System.out.println("局部变量:" + localVar);
            System.out.println("局部引用:" + localRef);
            System.out.println("静态变量:" + staticVar);
            System.out.println("实例变量:" + instanceVar);
        };

        myLambda.doSomething();
    }

    public static void main(String[] args) {
        LambdaVariableExample example = new LambdaVariableExample();
        example.testLambdaVariables();
    }
}

@FunctionalInterface
interface MyFunctionalInterface {
    void doSomething();
}

在上面的示例中,Lambda表达式myLambda可以访问testLambdaVariables方法中的局部变量localVar和局部引用localRef,以及包含它的类的静态变量staticVar和实例变量instanceVar

访问局部变量作限制的原因

Lambda表达式访问局部变量作限制的原因是为了确保在多线程环境下的安全性和可靠性。

Lambda表达式被创建时,它可以捕获(访问)包含它的方法或代码块中的局部变量。然而,由于Lambda表达式可以在不同的线程中执行,可能会导致以下问题:

  • 线程安全问题:如果多个线程同时访问和修改同一个局部变量,可能会导致竞态条件和数据不一致的问题。
  • 内存管理问题:当Lambda表达式被捕获的局部变量超出其作用域时,该变量可能已经被销毁。但是,由于Lambda表达式仍然可以访问该变量,可能会导致悬空引用和内存泄漏的问题。

为了解决这些问题,Java要求在Lambda表达式中访问的局部变量必须是final或者是事实上的final(即不可修改)。这样做的目的是确保局部变量的值在Lambda表达式中是不可变的,从而避免了多线程并发访问和修改的问题。

此外,Java编译器还对Lambda表达式的实现进行了优化,使得它可以直接访问final局部变量的值,而不是通过复制或传递引用来实现。这样可以提高Lambda表达式的性能和效率。

总结起来,Lambda表达式访问局部变量作限制的原因是为了确保多线程环境下的安全性和可靠性,并通过final限制来优化性能。

四、修改变量

Lambda表达式中,可以访问但不能直接修改局部变量。局部变量必须是final或者是事实上的final(即不可修改)。

然而,如果你使用的是Java 8及更高版本,你可以使用Java 8引入的新特性" effectively final "。这意味着,尽管你没有显式地将变量声明为final,但只要你没有对该变量进行修改,它仍然被视为final

下面是一个示例代码,演示了如何在Lambda表达式中修改变量:

public class LambdaModifyVariableExample {
    public static void main(String[] args) {
        int localVar = 10; // 局部变量

        MyFunctionalInterface myLambda = () -> {
            localVar = localVar + 5; // 错误!尝试修改局部变量的值
            System.out.println("局部变量:" + localVar);
        };

        myLambda.doSomething();
    }
}

@FunctionalInterface
interface MyFunctionalInterface {
    void doSomething();
}

在上面的示例中,Lambda表达式尝试修改局部变量localVar的值,但会导致编译错误。因为局部变量必须是final或者是事实上的final,即不能被修改。

如果你想在Lambda表达式中修改变量的值,你可以使用可变的容器类,如AtomicIntegerAtomicReference,或者将变量声明为数组的元素。这样,你可以修改容器对象或数组元素的状态,而不是直接修改变量本身。

import java.util.concurrent.atomic.AtomicInteger;

public class LambdaModifyVariableExample {
    public static void main(String[] args) {
        AtomicInteger localVar = new AtomicInteger(10); // 可变容器类

        MyFunctionalInterface myLambda = () -> {
            localVar.addAndGet(5); // 修改容器对象的值
            System.out.println("局部变量:" + localVar.get());
        };

        myLambda.doSomething();
    }
}

@FunctionalInterface
interface MyFunctionalInterface {
    void doSomething();
}

五、优缺点

Lambda表达式是Java 8引入的一个重要特性,它提供了一种简洁、灵活和函数式的编程方式。Lambda表达式具有以下优点和缺点:

优点:

  1. 简洁性:Lambda表达式可以大大减少代码的冗余,使得代码更加简洁、易读和易维护。
  2. 函数式编程:Lambda表达式支持函数式编程风格,可以将函数作为参数传递给其他方法,使得代码更加灵活和可组合。
  3. 并行处理:Lambda表达式可以与Java 8引入的Stream API结合使用,实现方便的并行处理,提高程序的性能。
  4. 代码可读性:通过使用Lambda表达式,可以将代码的重点放在业务逻辑上,而不是繁琐的语法和样板代码上,提高代码的可读性。

缺点:

  1. 学习曲线:对于初学者来说,理解和使用Lambda表达式可能需要一定的学习曲线,特别是对于那些没有函数式编程经验的开发人员。
  2. 可读性降低:虽然Lambda表达式可以使代码更加简洁,但过度使用Lambda表达式可能会导致代码可读性降低,特别是对于复杂的逻辑和长的Lambda表达式。
  3. 调试困难:Lambda表达式的调试可能会比传统的方法调试更加困难,因为Lambda表达式通常是匿名的,没有明确的方法名和堆栈跟踪信息。
  4. 限制:Lambda表达式只能用于函数式接口(只有一个抽象方法的接口),这限制了Lambda表达式的使用范围。

综上所述,Lambda表达式在简洁性、灵活性和函数式编程方面具有很多优点,但也存在一些学习曲线、可读性降低和调试困难等缺点。在使用Lambda表达式时,需要权衡其优缺点,并根据具体情况进行选择。

你可能感兴趣的:(java,经验分享,java,函数编程,Lambda,stream)