缠丝猫 发表于 2024-11-3 14:20:43

Lambda表达式详解

1、Lambda表达式简介

在开发中,使用行为参数化来转达代码有助于应对不停变革的需求。大白话就是将方法作为参数传来转达,如许可以让我们的代码变得更灵活。Lambda表达式就是让方法作为参数转达变得更简洁明白。 下面通过一个简单的示例来了解Lambda表达式。
一个简单的示例:
从一个果篮中去获取指定的水果
1、起首创建一个水果类
/**
* 水果类
*/
public class Fruits {
    /**
   * 名称 eg: 香蕉、苹果、橙子
   */
    String name;

    /**
   * 重量 单位:克(g)
   */
    Integer weight;
   
    public Integer getWeight() {
      return weight;
    }

    public String getName() {
      return name;
    }

    public Fruits(String name, Integer weight) {
      this.name = name;
      this.weight = weight;
    }

    @Override
    public String toString() {
      return "Fruits{" +
                "name='" + name + '\'' +
                ", weight=" + weight +
                '}';
    }
}
2、使用最基础的方式来获取”苹果“
// 通过if语句来判断是否为苹果
private static List<Fruits> getApples(List<Fruits> list) {
    List<Fruits> listApple = new ArrayList<>();
    for (Fruits fruits : list) {
      if ("苹果".equals(fruits.name)) {
            listApple.add(fruits);
      }
    }
    return listApple;
}
3、使用上面这种是最简单的,但是却是最不容易扩展的。
在软件工程中,一个众所周知的问题就是,不管你做什么,用户的需求肯定会变的。假如此时让我们来获取”香蕉“、获取”香蕉“和”苹果“,以致要重量作为筛选条件呢?我们就须要写许多的代码,而且这些代码是高度相似的,如许的代码是冲破DRY(Don’t Repeat Yourself,不要重复自己)的软件工程原则的。
        // 筛选苹果和香蕉
    private static List<Fruits> getApplesAndBanana(List<Fruits> list) {
      List<Fruits> listApple = new ArrayList<>();
      for (Fruits fruits : list) {
            if ("苹果".equals(fruits.name) || "香蕉".equals(fruits.name)) {
                listApple.add(fruits);
            }
      }
      return listApple;
    }

    // 筛选苹果、香蕉和重量超过150g的苹果
    private static List<Fruits> getApplesAndBananaAndWeight(List<Fruits> list) {
      List<Fruits> listApple = new ArrayList<>();
      for (Fruits fruits : list) {
            if ("苹果".equals(fruits.name) && fruits.weight > 150) {
                listApple.add(fruits);
            }
            if ("香蕉".equals(fruits.name)) {
                listApple.add(fruits);
            }
      }
      return listApple;
    }
4、写出上面如许的代码是令人有点扫兴的。
让我们退却一步来看看更高层次的抽象,我们可以界说一个接口来实现这个获取动作。假如有新的需求就实现这个接口来界说实现方法。
a、界说接口
public interface FruitsPredicate {
    // 过滤水果
    boolean filter(Fruits fruits);
}
b、选择不同水果的战略
https://i-blog.csdnimg.cn/blog_migrate/bbb0bcc261b963420f96204d1b52872a.png
c、具体类实现
public class ApplesPredicate implements FruitsPredicate{
    @Override
    public boolean filter(Fruits fruits) {
      return "苹果".equals(fruits.name) ? true : false;
    }
}
public class ApplesAndBananaPredicate implements FruitsPredicate{
    @Override
    public boolean filter(Fruits fruits) {
      return "苹果".equals(fruits.name) || "香蕉".equals(fruits.name) ? true : false;
    }
}
public class ApplesAndBananaAndWeightPredicate implements FruitsPredicate{
    @Override
    public boolean filter(Fruits fruits) {
      if ("苹果".equals(fruits.name) && fruits.weight > 150) {
            return true;
      }
      if ("香蕉".equals(fruits.name)) {
            return true;
      }
      return false;
    }
}
d、过滤方法
private static List<Fruits> filterFruits(List<Fruits> list, FruitsPredicate fruitsPredicate) {
   List<Fruits> listApple = new ArrayList<>();
   for (Fruits fruits : list) {
       if (fruitsPredicate.filter(fruits)) {
         listApple.add(fruits);
       }
   }
   return listApple;
}
5、这里值得小小地庆祝一下。
上面的代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易。filterFruits方法的行为取决于你通过FruitsPredicate对象转达的代码,我们已经实现了filterFruits方法的行为参数化了。
但是上面的方式我们不得不声明好几个实现FruitsPredicate接口的类,然后实例化好几个只会提到一次的FreitsPredicate对象。这是很啰嗦的,很费时间。所以我们想到了可以使用Java的匿名类来进一步改善代码。:
List<Fruits> oranges = filterFruits(list, new FruitsPredicate() {
    @Override
    public boolean filter(Fruits fruits) {
      return "橙子".equals(fruits.name);
    }
});
6、使用Lambda表达式。
好的代码应该是一览无余的,即使匿名类处理处罚在某种水平上改善了为一个接口声明好几个实体类的啰嗦问题,但它仍不能令人满足,须要书写很多重复的代码。所以Java8的语言设计者通过引入Lambda表达式来解决这个问题。
// 获取所有苹果
List<Fruits> apples = filterFruits(list, fruits -> "苹果".equals(fruits.name));

// 获取所有苹果和香蕉
List<Fruits> applesAndBananas = filterFruits(list, fruits -> "苹果".equals(fruits.name) || "香蕉".equals(fruits.name));

// 获取重量大于150g的苹果和所有香蕉
List<Fruits> applesAndBananasAndWeight = filterFruits(list, fruits -> {
if ("苹果".equals(fruits.name) && fruits.weight > 150) {
    return true;
}
if ("香蕉".equals(fruits.name)) {
    return true;
}
    return false;
});
使用Lambda表达式看起来是不是非常的灵活和简单,这就是Lambda表达式的长处。如今你还不须要学会使用Lambda表达式,只须要认识到Lambda的长处。后面会详细介绍如何使用。
2、如何使用Lambda表达式

可以把Lambda表达式理解为简洁地表示可转达的匿名函数的一种方式:它没著名称,但它有参数列表、函数主体、返回范例,可能另有一个可以抛出的非常列表。
Lambda主要又三部分组成:参数列表、箭头和Lambda主体。
https://i-blog.csdnimg.cn/blog_migrate/0bcd52d79ff4df811caed34108e52e27.png
其实Lambda就是匿名类的简化,我们可以将这个匿名类实现出来举行比较,发现它将匿名类的许多代码都举行了省略,只生存了最重要的部分。
list.sort(new Comparator<Fruits>() {
   @Override
   public int compare(Fruits f1, Fruits f2) {
          return f1.weight - f2.weight;
   }
});
下面对每个语法格式的特征举行举例阐明:
(1)、语法格式一:无参,无返回值,Lambda主体只有一条语句。
Runnable runnable = () -> System.out.println("Hello World!");
(2)、语法格式二:有一个参数,无返回值,Lambda主体只有一条语句。
Consumer<String> consumer = (x) -> System.out.println(x);
只有一个参数时参数的小括号可以省略
Consumer<String> consumer = x -> System.out.println(x);
(3)、语法格式三:两个参数及以上,有返回值,Lambda主体只有一条语句。
Lambda没有return语句,由于已经隐含了return。
Comparator<Integer> com=(x, y)-> Integer.compare(x,y);
(4)、语法格式四:两个参数及以上,有返回值,Lambda主体有多条语句。
Comparator<Integer> com=(x, y)->{
   System.out.println("x的值为:"+x);
   System.out.println("y的值为:"+y);
   return x*y;
};
(5)、Lambda表达式的参数列表数据范例可以省略不写,由于JVM可以通过上下文推断出数据范例。
Comparator<Integer> com=(Integer x,Integer y)-> Integer.compare(x,y);
Comparator<Integer> com=(x,y)-> Integer.compare(x,y);
3、在那里使用Lambda表达式

在函数式接口上使用Lambda表达式,当方法的参数是一个函数式接口时就可以使用Lambda表达式。
3.1 函数式接口

函数式接口就是只界说一个抽象方法的接口。
例如我们写的FruitsPredicate接口:
@FunctionalInterface
public interface FruitsPredicate {
    /**
   * 过滤水果
   *
   * @param fruits
   * @return
   */
    boolean filter(Fruits fruits);
}
注意:
1、函数式接口可以使用 @FunctionalInterface 注解来声明。
这个注解只是用来表明这是一个函数式接口,并不是没有这个注解就不是函数式接口了。
有这个注解的话,假如它不是函数式接口的话,编译器将报错。
2、Java8在接口中可以增加默认方法了。哪怕有很多默认方法,只要接口只界说了一个抽象方法,它就仍旧是一个函数式接口。
3.2函数描述符

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作
函数描述符。例如,Runnable接口可以看作一个什么也不担当什么也不返回(void)的函数的
签名,由于它只有一个叫作run的抽象方法,这个方法什么也不担当,什么也不返回(void)。
() -> void// 这个函数描述符代表了参数列表为空,且返回void的函数。Runnable就是这种。
其他常见的函数式接口和对应的函数描述符:
函数式接口函数描述符Preicate<T>T -> booleanConsumer<T>T -> voidFunction<T,R>T -> RSupplier<T>() -> TUnaryOperator<T>T -> TBinaryOperator<T>(T,T) -> TBiPredicate<L,R>(L,R) -> booleanBiConsumer<T,U>(T,U) -> voidBiFunction<T,U,R>(T,U) -> R 4、四大焦点函数式接口

Java 8的库设计师在java.util.function包中引入了几个新的函数式接口,最焦点的四个函数式接口分别为Predicate<T>、Consumer<T>、Function<T,R>和Supplier<T>。下面我们来逐一介绍它们。
4.1 Predicate

源码:
public interface Predicate<T> {
    boolean test(T t);
}
java.util.function.Predicate<T>接口界说了一个test的抽象方法,他担当泛型T对象,并返回一个boolean值。是一个断定型接口。
使用:
// 优化获取所有果篮中苹果的方法,使用Predicate接口
private static List<Fruits> filterApples(List<Fruits> list, Predicate<Fruits> predicate) {
    List<Fruits> listApple = new ArrayList<>();
    for (Fruits fruits : list) {
      if (predicate.test(fruits)) {
            listApple.add(fruits);
      }
    }
    return listApple;
}

// 使用filterApples方法来获取结果
filterApples(list,s -> "苹果".equals(s.name) ? true : false);

4.2 Consumer

源码:
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
java.util.function.Consumer<T>界说了一个名叫accept的抽象方法,它担当泛型T
的对象,没有返回(void)。是一个消费型接口。
使用:
// 去消费果篮中的水果,【可根据不同的消费行为来消费】
private static void printFruits(List<Fruits> list, Consumer<Fruits> consumer) {
    for (Fruits fruits : list) {
      consumer.accept(fruits);
    }
}
// 输出果篮中全部水果名称和重量
printFruits(list,s -> System.out.println("水果名称:" + s.name + ";" + "水果重量:" + s.weight));

// 输出果篮中苹果和重量
printFruits(list,s -> {
    if ("苹果".equals(s.name)) {
      System.out.println("水果名称:" + s.name + ";" + "水果重量:" + s.weight);
    }
});
4.3 Function

源码:
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
java.util.function.Function<T, R>接口界说了一个叫作apply的方法,它担当一个
泛型T的对象,并返回一个泛型R的对象。是一个函数型接口。
使用:
/**
* 此方法将果篮中的水果一一检查执行对应的操作
*
* 使用了Java泛型,进一步抽象方法。
* 在Java 8中泛型的类型编译器可以自行推断出来
*/
private static <T,R> List<R> getAllFruits(List<T> list, Function<T,R> function) {
    List<R> result = new ArrayList<>();
    for (T t : list) {
      result.add(function.apply(t));
    }
    return result;
}
// 将果篮中的所有水果贴上标签
List<FruitsLabel> allFruits = getAllFruits(list, t -> {
    FruitsLabel fruitsLabel = new FruitsLabel();
    fruitsLabel.name = t.name;
    fruitsLabel.weight = t.weight;
    fruitsLabel.label = "新鲜";
    return fruitsLabel;
});
System.out.println(allFruits);
4.4 Supplier

源码:
@FunctionalInterface
public interface Supplier<T> {
    T get();
}

java.util.function.Supplier<T>接口界说了一个叫作get的方法,它不须要担当参数,即可返回一个泛型T的对象。是一个供给型接口。
使用:
private static <T> List<T> addFruits(List<T> list, Supplier<T> supplier) {
    list.add(supplier.get());
    return list;
}
// 向果篮中放一个橙子
addFruits(list,() -> new Fruits("橙子", 100));
其他的函数式接口我们就不在过多的介绍,假如各人后续有用到可以自行查阅。
5、方法引用

当要转达给Lambda体的操作,已经有实现的方法了,就可以使用方法引用!(实现抽象方法的参数列表,必须与方法引用的参数列表一致,方法的返回值也必须一致,即方法的签名一致)。方法引用可以理解为方法引用是Lambda表达式的另一种表现形式。
方法引用的语法:使用操作符"::"将对象或类和方法名分隔开。
我们使用一个实例来举行演示:
// 根据水果的重量来排序
list.sort(Comparator.comparing(s -> s.getWeight()));
我们发现后面的Lambda表达式其实就是调用getWeight()方法来获取Fruits类的重量,这个方法已经存在Fruits类中了,所以我们可以直接使用这个方法,这就是方法引用。
list.sort(Comparator.comparing(Fruits::getWeight));

5.1 方法引用的使用情况

方法引用的使用情况共分为以下三种:


[*]对象::实例方法名
[*]类::静态方法名
[*]类::实例方法名
使用实例:
1、对象::实例方法名
public void test(){
    PrintStream out = System.out;
    // 使用Lambda实现
        Consumer<String> consumer = s -> out.println(s);

        // 使用方法引用实现相同效果
    Consumer<String> consumer1 = out::println;
    // 测试
    consumer.accept("hello world");
    consumer1.accept("hello world");
}
2、类::静态方法名
public void test(){
        // 使用Lambda实现
    Comparator<Integer> comparable=(x,y)->Integer.compare(x,y);
   
    //使用方法引用实现相同效果
    Comparator<Integer> integerComparable=Integer::compare;
        // 测试
        System.out.println(integerComparable.compare(4,2));//结果:1
    System.out.println(comparable.compare(4,2));//结果:1
}
3、类::实例方法名
public void test(){
        // 使用Lambda实现
    BiPredicate<String,String> bp=(x,y)->x.equals(y);
   
    //使用方法引用实现相同效果
    BiPredicate<String,String> bp2=String::equals;
        // 测试
    System.out.println(bp.test("1","2"));//结果:false
    System.out.println(bp2.test("1","2"));//结果:false
}

注意:
对象::实例化方法和类::实例化方法可能乍看起来有点儿晕。类似于String::equals的方法引
用的思想就是你在引用一个对象的方法,而这个对象自己是Lambda的一个参数。
6、构造器引用

对于一个现有构造函数,你可以使用它的名称和关键字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 void test(){
    //引用无参构造器
    Supplier<Employee> supplier=Employee::new;
    System.out.println(supplier.get());
    //引用有参构造器
    Function<Integer,Employee> function=Employee::new;
    System.out.println(function.apply(21));
    BiFunction<Integer,Integer,Employee> biFunction=Employee::new;
    System.out.println(biFunction.apply(8,24));
}

输出结果:
Employee{id=null, name='null', age=null}
Employee{id=21, name='null', age=null}
Employee{id=8, name='null', age=24}

7、数组引用

数组引用格式:type[]::new
使用示例:
public void test02(){
    Function<Integer,String[]> function=String[]::new;
    String[] apply = function.apply(10);
    System.out.println(apply.length); //结果:10
}
8、复合Lambda表达式的有用方法

Java 8的好几个函数式接口都有为方便而设计的复合方法。例如Comparator、Function和Predicate都提供了答应你举行复合的方法。
这是什么意思呢?在实践中,这意味着你可以把多个简单的Lambda复合成复杂的表达式。比如,
你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结
果成为另一个函数的输入。
你可能会想,函数式接口中怎么可能有更多的方法呢?(毕竟,这违背了函数式接口的界说啊!)窍门在于,Java 8中增加了默认方法。它们不是抽象方法。
比较器复核
之前我们使用了方法引用对果篮中的水果举行排序:
list.sort(Comparator.comparing(Fruits::getWeight));

这个序次是从小到大排序的,那么如何让它从大到小排序呢?我们须要去创建另一个方法吗?如许岂不是麻烦了?
在Comparator的接口中,提供了一个默认方法reversed可以对比较器逆序,使用如下:
list.sort(Comparator.comparing(Fruits::getWeight).reversed());
假如两个水果重量相同,我们又想按照名称排序怎么办?
在Comparator接口中提供了一个方法thenComParing方法可以担当另一个比较器。使用如下:
list.sort(Comparator.comparing(Fruits::getWeight)
   .thenComparing(Fruits::getName));
谓词复合
谓词接口包罗三个方法:negate、and和or,已知的Predicate接口就实现了如许的方法,我们测试一下:
1、negate表示取相反的值。例如我们之前从果篮中选取苹果的逻辑,我们假如对Predicate接口加上negate方法则表示取不是苹果的其他水果。
   // 得到所有苹果
   Predicate<Fruits> getApples = s -> "苹果".equals(s.name) ? true : false;
   // 得到所有除苹果外其他水果
   Predicate<Fruits> getNotApples = getApples.negate();
2、and方法相当于&&
例如我们之前逻辑选取苹果且重量大于150g的逻辑,我们可以如许写代码
   // 得到所有重量大于150g的苹果
   getApples.and(s -> s.getWeight() > 150 ? true : false);
3、or方法相当于||
例如我们可以选取果篮中的苹果和香蕉
        // 得到香蕉和苹果
        getApples.or(s -> "香蕉".equals(s.name) ? true : false);
注意:
and和or方法在表达式中的优先级是从左向右确定优先级的,例如a.or(b).and(c)可以看作(a||b)&&c
函数复核
Function接口有andThen和compose两个默认方法,它们都会返回Function的一个实例。我们可以先界说两个Function,后面将会依赖Funtion来举行演示。
// f = x + 1
Function<Integer, Integer> f = x -> x + 1;
// f = x * 2
Function<Integer, Integer> g = x -> x * 2;
1、andThen方法等价于 g(f(x))
// 相当于 (x+1)*2
Function<Integer, Integer> g = x -> x * 2;
int result = h.apply(1); // 返回4
2、compose方法等价于 f(g(x))
// 相当于 (x*2)+1
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1); // 返回3
9、Lambda表达式的作用域

Lambda表达式可以看作是匿名内部类实例化的对象,Lambda表达式对变量的访问限定和匿名内部类一样,因此Lambda表达式可以访问局部变量、局部引用,静态变量,实例变量。
1、访问局部变量
在Lambda表达式中规定只能引用标记了final的外层局部变量。我们不能在lambda 内部修改界说在域外的局部变量,否则会编译错误。
public class TestFinalVariable {

    interface VarTestInterface{
      Integer change(String str);
    }

    public static void main(String[] args) {
       //局部变量不使用final修饰
      Integer tempInt = 1;
      VarTestInterface var = (str -> Integer.valueOf(str+tempInt));
      //再次修改,不符合隐式final定义
      tempInt =2;
      Integer str =var.change("111") ;
      System.out.println(str);
    }
}

编译会报错:
https://i-blog.csdnimg.cn/blog_migrate/3b7487d31c579723bf11154c48285844.png
2、访问局部引用、静态变量、实例变量
Lambda表达式不限定访问局部引用变量,静态变量,实例变量。代码测试都可正常实行,代码:
public class LambdaScopeTest {

    /**
   * 静态变量
   */
    private static String staticVar;

    /**
   * 实例变量
   */
    private static String instanceVar;

    @FunctionalInterface
    interface VarChangeInterface{
      Integer change(String str);
    }

    /**
   * 测试引用变量
   */
    private voidtestReferenceVar(){
      ArrayList<String> list = new ArrayList<>();
      list.add("111");
      //访问外部引用局部引用变量
      VarChangeInterface varChangeInterface = ((str) -> Integer.valueOf(list.get(0)));
      //修改局部引用变量
      list.set(0,"222");
      Integer str =varChangeInterface.change("");
      System.out.println(str);
    }


    /**
   * 测试静态变量
   */
    void testStaticVar(){
      staticVar="222";
      VarChangeInterface varChangeInterface = (str -> Integer.valueOf(str+staticVar));
      staticVar="333";
      Integer str =varChangeInterface.change("111") ;
      System.out.println(str);
    }


    /**
   * 测试实例变量
   */
    void testInstanceVar(){
      instanceVar="222";
      VarChangeInterface varChangeInterface = (str -> Integer.valueOf(str+instanceVar));
      instanceVar="333";
      Integer str =varChangeInterface.change("111") ;
      System.out.println(str);
    }



    public static void main(String[] args) {
      new LambdaScopeTest().testReferenceVar();
      new LambdaScopeTest().testStaticVar();
      new LambdaScopeTest().testInstanceVar();
    }
}

注意:
Lambda表达式里不答应声明一个与局部变量同名的参数或者局部变量。
        //编程报错
    Integer tempInt = 1;
    VarTestInterface varTest01 = (tempInt -> Integer.valueOf(tempInt));
    VarTestInterface varTest02 = (str -> {
      Integer tempInt = 1;
      Integer.valueOf(str);
    });
3、Lambda表达式访问局部变量作限定的原因
Lambda表达式不能访问非final修饰的局部变量的原因是,局部变量是生存在栈帧中的。而在Java的线程模子中,栈帧中的局部变量是线程私有的,假如答应Lambda表达式访问到栈帧中的变量地址(可改变的局部变量),则会可能导致线程私有的数据被并发访问,造成线程不安全问题。
基于上述,对于引用范例的局部变量,由于Java是值转达,又由于引用范例的指向内容是生存在堆中,是线程共享的,因此Lambda表达式中可以修改引用范例的局部变量的内容,而不能修改该变量的引用。
对于基本数据范例的变量,在 Lambda表达式中只是获取到该变量的副本,且局部变量是线程私有的,因此无法知道其他线程对该变量的修改,假如该变量不做final修饰,会造成数据不同步的问题。
但是实例变量,静态变量不作限定,由于实例变量,静态变量是生存在堆中(Java8之后),而堆是线程共享的。在Lambda表达式内部是可以知道实例变量,静态变量的变革。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Lambda表达式详解