Java-lambda表达式、函数式接口、方法引用

1. Lambda表达式

1.1 Lambda概念

  • 1.8的新特性
  • 用来简化接口匿名内部类

1.2 使用前提(重点)

  • 必须有接口且接口中有且只有一个抽象方法!!

1.3 标准格式

- (参数类型 参数名称) -> {执行代码语句}

说明:

  1. ()里面放接口中抽象方法的参数列表;
  2. ->新语法格式,代表指向动作;
  3. {}里面放重写抽象方法的执行语句。

1.4 省略格式

  • 省略规则
    • ()中的参数的数据类型可省略;
    • 如果参数列表只有一个参数时,小括号可省略;
    • 若方法体只有一条语句时,可省略大括号,此时return和大括号后面的分号都必须省略!

1.5 示例代码

/*
无参无返回值
*/
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
    }
}).start();
//使用lambda表达式转换匿名内部类//
new Thread(() -> {System.out.println("hello");}).start();
//用省略格式写
new Thread(()->System.out.println("hello")).start();
/*
有参有返回值
*/
Collections.sort(list,new Comparator<Student>(){
    @Override
    public int compart(Student o1,Student o2) {
        return o1.getScore() - o2.getScore();
    }
});
//使用lambda标准格式
Collections.sort(list,(Student o1,Student o2) -> {return o1.getScore() - o2.getScore();});
//使用lambda省略格式
Collections.sort(list, (o1,o2) ->o1.getScore() - o2.getScore());

1.6 小结(记住、理解)

  • **(重点)**lambda就是 参数列表->重写方法体
  • 在后面的函数式接口中,lambda就是参数列表->重写的抽象方法,一旦函数式接口实例调用抽象方法,执行的就是lambda的方法体。

2. 函数式接口

2.1 概念引入

  • 函数式接口:有且只有一个抽象方法的接口

    适用于函数式编程的场景,其体现正是Lambda表达式!

“语法糖”指原理不变而更为便捷的代码语法。

2.2 格式

  1. 正常格式:

    修饰符 interface 接口名 {
       public abstract 返回值类型 方法名(参数列表);
       //其他非抽象方法内容
    }
  2. 函数式接口简略格式:

    public interface MyFunctionalInterface {
       void myMethod();
    }

2.3 @FunctionalInterface注解

  • 1.8的新注解,用于接口的定义上,并判断该接口是否有且只有一个抽象方法。若否,则报错。
  • 该注解只为了让系统辅助检查接口,即使不用,只要接口满足函数式接口定义,该接口还是一个函数式接口。

2.4 无参无返回值的自定义函数式接口示例

定义:

@FunctionalInterface
public interface MyFunctionalInterface {
    void myMethod();
}

应用:

public class Note {
    private static void Test(MyFunctionalInterface mfi) {
        mfi.myMethod();
    }
    public static void main(String[] args)  {
        Test(()->System.out.println("Do something!"));

        //使用匿名内部类还原
        Test(new MyFunctionalInterface() {
            @Override
            public void myMethod() {
                System.out.println("Do something!");
            }
        });
    }
}

​ 示例解析:先定义一个自定义函数式接口。由于Test方法传入的参数是函数式接口的实现类对象,所以在调用静态方法Test()时,示例用Lambda表达式代替匿名内部类作为该对象,并重写抽象方法。

要理解为何在Lambda表达式外是写Test,因为main方法里调用的是Test方法,而不是mfi或者myMethod!后两者都只是调用Test方法所需要的东西,Lambda表达式已经充当后两者

2.5 有参有返回的自定义函数式接口示例

​ 定义:

@FunctionalInterface
public interface Sumable {
    int sum(int a, int b);
}

​ 应用:

public class Note {
    private static void countSum (int x, int y, Sumable cal) {
       System.out.println(cal.sum(x, y));
    }
    public static void main(String[] args)  {
        countSum(1,2,(i,j)->i+j);

        //使用匿名内部类还原
        countSum(1, 2, new Sumable() {
            @Override
            public int sum(int a, int b) {
                return a + b;
            }
        });
    }
}

​ 示例解析:同样定义一个函数式接口,调用静态方法countSum,第三个参数需要传入函数式接口的实现类对象,则用lambda表达式替代匿名内部类传入方法。

3. 函数式编程

3.1 Lambda的延迟执行

  • 延迟执行:在需要的时候才执行

  • 解决部分场景中某些代码执行后不一定被使用所导致的性能浪费的问题

3.2 Lambda作为参数和返回值

3.2.1 Lambda作为参数

  • 当函数式接口作为方法参数时,可以用Lambda代替(比如Thread类的构造方法参数Runnable)。

3.2.2 Lambda作为返回值

  • 同理,当函数式接口作为方法的返回值类型时,可以用Lambda代替。(比如用一个方法获取Comparator接口)。

4. 方法引用

4.1 概念引入

4.1.1 概念

  • 1.8的新特性

  • 通过类名或对象名引用已存在的方法以简化lambda表达式。

  • 方法引用和方法调用时两种概念,方法调用时马上执行。而方法引用在没有执行函数式接口的抽象方法时,该抽象方法的重写方法并不会被执行。

4.1.2 格式

  • 类名::方法名

  • 对象名::方法名

    看起来有点像方法调用的格式

4.1.3 原理

  • 创建函数式接口的匿名内部类对象
  • 重写函数式接口的抽象方法并在重写的方法中调用被引用的方法。

4.1.4 方法引用的四种类型

  1. 静态方法引用
  2. 对象方法引用
  3. 特定类型的实例方法引用
  4. 构造方法引用

4.1.5 方法引用优点

  • 简化lambda表达式
  • 重复利用已存在的方法

4.1.6 方法引用的使用场景(重点)

  • 当lambda表达式仅是调用一个已存在的方法时,才可以使用方法引用简化lambda表达式!!

4.2 方法引用的四种类型(重点)

4.2.1 静态方法引用(通过类名引用)

  • 语法格式:ClassName::staticMethodName(类名::静态方法名)

  • 使用场景

    比如Lambda表达式:array->ArrayUtils.getMax(array),可以简化成ArrayUtils::getMax,注意观察规律

  • 注意事项

    1. 被引用的方法和函数式接口中的抽象方法必须有相同的参数列表

    2. 若函数式接口的抽象方法有返回值,则被引用的方法必须也有相同的返回值;

    3. 若函数式接口的抽象方法无返回值,被引用方法的返回值可有可无。

      在使用前,需要判断上述条件,均满足之后lambda表达式才能用方法引用简化,以下三种类型同理

  • 示例代码

    ​ 需求:使用静态方法引用,调用已存在的方法获得数组的最小值

    /*
    使用静态引用前提:有函数式接口,有被引用的静态方法,lambda仅调用一个被引用的方法,以及上文提到的注意事项
    */
    
    /*函数式接口*/
    @FunctionalInterface
    interface ArrayInterface {
      //返回最小值
      int min(int[] arr);
    }
    
    /*数组工具类,内含被引用的静态方法*/
    class ArrayTools {
      public static int getMinValue(int[] arr) {
          int min = arr[0];
          for (int i = 1; i < arr.length; i++) {
              if (arr[i] < min) {
                  min = arr[i];
              }
          }
          return min;
      }
    }
    
    public class Notes03 {
      public static void main(String[] args) {
          int[] arr = {11,53,68,15,86,1,2,16,5};
          //1.直接调用静态方法
          int min1 = ArrayTools.getMinValue(arr);
          System.out.println("min1 = " + min1);
          System.out.println("----------------");
          //2.使用ArrayInterface实现类对象(匿名内部类)
          ArrayInterface ai1 = new ArrayInterface(){
              @Override
              public int min(int[] arr) {
                  int min = arr[0];
                  for (int i = 1; i < arr.length; i++) {
                      if (arr[i] < min) {
                          min = arr[i];
                      }
                  }
                  return min;
              }
          };
          int min2 = ai1.min(arr);
          System.out.println("min2 = " + min2);
          System.out.println("----------------");
          //使用lambda替换匿名内部类
          ArrayInterface ai2 = array -> {
              int min = arr[0];
              for (int i = 1; i < arr.length; i++) {
                  if (arr[i] < min) {
                      min = arr[i];
                  }
              }
              return min;
          };
          int min3 = ai2.min(arr);
          System.out.println("min3 = " + min3);
          System.out.println("----------------");
          //使用方法引用简化lambda
          ArrayInterface ai3 = ArrayTools::getMinValue;
          /*
          上述代码等价于:
          ArrayInterface ai3 = new ArrayInterface(){
              //重写抽象方法
              int min(int[] arr) {
                  //重写的抽象方法中仅调用一个被应用的静态方法,相当于方法套方法,而两个方法的参数列表也必须相同
                  return ArrayTools.getMinValue(int[] arr);
                  }
              }
          }
           */
          int min4 = ai3.min(arr);
          System.out.println("min4 = " + min4);
      }
    }

4.2.2 对象方法引用(通过对象名引用)

  • 语法格式:instance::methodName(对象名::非静态方法名)

    • 与静态方法的语法类似,只是这使用对象引用而不是类名;
    • 如果引用的方法不是静态方法,不能直接用类名调用方法的时候,就要用对象方法引用。
  • 使用场景

    比如Lambda表达式:

    MyRandom myRandom = new MyRandom; (a,b)->myRandom.nextIntAtoB(a,b),可以简化成myRandom::nextIntAtoB,注意观察规律

  • 注意事项

    1. 被引用的方法和函数式接口中的抽象方法必须有相同的参数列表
    2. 若函数式接口的抽象方法有返回值,则被引用的方法必须也有相同的返回值;
    3. 若函数式接口的抽象方法无返回值,被引用方法的返回值可有可无。

    跟静态方法引用一致

  • 示例代码

    ​ 需求:使用对象方法引用,调用已存在的方法获得随机数

    /*函数式接口*/
    @FunctionalInterface
    interface getRandomNumber {
      // 获得a到b之间的随机数
      int randomAToB(int a,int b);
    }
    /*被引用方法*/
    class RanNumber {
      public int randomNum(int a,int b) {
          Random r = new Random();
          return r.nextInt(b - a + 1) + a;
      }
    }
    public class Notes04 {
      public static void main(String[] args) {
          //1.使用匿名内部类
          getRandomNumber rn1 = new getRandomNumber() {
              @Override
              public int randomAToB(int a, int b) {
                  Random r = new Random();
                  return r.nextInt(b - a + 1) + a;
              }
          };
          int ranNum1 = rn1.randomAToB(10, 20);
          System.out.println("ranNum1 = " + ranNum1);
          System.out.println("-------*---------");
          //2.使用lambda替代
          getRandomNumber rn2 = (a, b) -> new Random().nextInt(b - a + 1) + a;
          int ranNum2 = rn2.randomAToB(10, 20);
          System.out.println("ranNum2 = " + ranNum2);
          System.out.println("-------*---------");
          //3.lambda另一写法,为了方便理解后面的方法引用
          //先创建被引用方法的实例
          RanNumber n3 = new RanNumber();
          getRandomNumber rn3 = (a, b) -> n3.randomNum(a, b);
          int ranNum3 = rn3.randomAToB(10, 20);
          System.out.println("ranNum3 = " + ranNum3);
          System.out.println("-------*---------");
          //4.因为lambda只调用一个方法,可用方法引用。因为被引用方法不是静态,所以要用对象方法引用
          RanNumber n4 = new RanNumber();
          getRandomNumber rn4 = n4::randomNum;
          /*
          上式等价于:
          getRandomNumber rn4 = new getRandomNumber() {
              //重写抽象方法
              int randomAToB(int a, int b){
                  //重写的抽象方法只调用一个非静态方法
                  return n4.randomNum(int a, int b);
                  }
              }
          }
           */
          int ranNum4 = rn4.randomAToB(10, 20);
          System.out.println("ranNum4 = " + ranNum4);
      }
    }

4.2.3 构造方法引用

  • 语法格式:Clsaa::new(类名::new)

    • 构造函数本质上是静态方法,仅方法名比较特殊,使用new关键字
  • 使用场景

    比如Lambda表达式:

    brand->new Car(brand),可以简化成Car::new,注意观察规律

  • 注意事项

    1. 函数式接口中的抽象方法返回值是引用数据类型;
    2. 被引用的类必须存在一个参数列表与函数式接口的参数列表相同的构造方法;
  • 示例代码

    需求:运用构造方法引用,创建新的顾客对象

    /*函数式接口*/
    @FunctionalInterface
    interface login{
      Guest loginGuest(String name);
    }
    /*引用类*/
    class Guest {
      private String name;
    
      public Guest() {
      }
    
      public Guest(String name) {
          this.name = name;
      }
    
      public String getName() {
          return name;
      }
    
      public void setName(String name) {
          this.name = name;
      }
    
      @Override
      public String toString() {
          return "Guest{" +
                  "name='" + name + '\'' +
                  '}';
      }
    }
    public class Notes06 {
      public static void main(String[] args) {
          //1.匿名内部类
          login l1 = new login() {
              @Override
              public Guest loginGuest(String name) {
                  return new Guest(name);
              }
          };
          Guest guest1 = l1.loginGuest("达尔文");
          System.out.println(guest1);
          //2.lambda
          login l2 = name -> new Guest(name);
          Guest guest2 = l2.loginGuest("高尔夫");
          System.out.println(guest2);
          //3.构造方法引用
          login l3 = Guest::new;
          /*
          上式等价于:
          login l3 = new login() {
              //重写抽象方法
              Guest loginGuest(String name) {
                  //抽象方法调用构造方法,必须保证有对应的构造方法
                  return new Guest(name);
              }
          };
           */
          Guest guest3 = l3.loginGuest("马克思");
          System.out.println(guest3);
      }
    }

4.2.4 数组构造方法引用

数组作为Object的子类,具有构造方法,但语法稍有不同。若要应用Lambda,需要一个函数式接口:
@FunctionalInterface
public interface ArrayBuilder {
    int[] buildArray(int length);
}

​ 使用lambda表达式:

public class Notes05 {
    private static int[] initArray(int length, ArrayBuilder builder) {
        return builder.buildArray(length);
    }
    public static void main(String[] args) {
        int[] array = initArray(10, length-> new int[length]);

    //用匿名内部类还原
        int[] array1 = initArray(10, new ArrayBuilder() {
            @Override
            public int[] buildArray(int length) {
                return new int[length];
            }
        });
    }
}

​ 简化lambda

public class Notes05_Another {
    private static int[] initArray(int length, ArrayBuilder builder) {
        return builder.buildArray(length);
    }
    public static void main(String[] args) {
        int[] array = initArray(10, int[]::new);
    }
}

以下两种写法是等效的

  • Lambda表达式: length -> new int[length]
  • 方法引用: int[]::new

4.2.5 特定类型的实例方法引用

  • 语法格式:类名::实例方法名
    • 所谓的实例方法其实就是指非静态方法。
  • 注意事项
    • 被引用的方法参数列表要比函数式接口中的抽象方法的参数列表少一个参数。
    • 被引用的方法是由函数式接口中抽象方法的参数列表中的第一个参数调用,其余参数作为被引用方法的参数传递。

示例代码

​ 需求:字符串数组忽略大小写排序

public class Notes07 {
    public static void main(String[] args) {
        String[] strs = {"niahao","Jonny","JoJo","GioGio","mango","123"};
        //使用lambda,忽略大小写
        Arrays.sort(strs,(o1, o2) -> o1.compareToIgnoreCase(o2));
        for (String str : strs) {
            System.out.println(str);
        }
        System.out.println("---------------");
        //使用方法引用
        Arrays.sort(strs,String::compareToIgnoreCase);
        /*
        上式等价于
        Arrays.sort(strs, new Comparator<String>() {
            //重写抽象方法
            public int compare(String o1, String o2) {
                return o1.compareToIgnoreCase(o2);  //o1调用方法对除o1以外的参数进行比较
            }
        });
         */
        for (String str : strs) {
            System.out.println(str);
        }
    }
}

示例解释

  • 使用 类名::实例方法名进行方法引用时,一定是Lambda表达式所接受的第一个参数来调用实例方法,如果lambda表达式接受多个参数,则其余的参数将被作为被引用方法的参数传递进去

4.4 通过this引用成员方法

  • this代表当前对象,若被引用的方法就是当前类中的成员方法,则可用this::成员方法的格式来进行方法引用。(对象方法引用的特殊情况,对象名就是this)

补充示例代码

​ 针对上例,以下两种写法是等效的:

  • Lambda表达式: () -> this.bookMsg()
  • 方法引用:this::bookMsg

4.5 通过super引用成员方法

  • 若存在继承关系,若被引用的方法就是当前类的父类中的成员方法,则可用super::成员方法的格式来进行方法引用。(对象方法引用的特殊情况,对象名就是super)

示例代码

​ 需求:利用方法引用,子类调用父类方法

@FunctionalInterface
public interface Helper {
    void help();
}
public class Father {
    public void sayHello(){
        System.out.println("你好");
    }
    public void sayBye(){
        System.out.println("再见");
    }
}
public class Son extends Father {
    @Override
    public void sayHello() {
        System.out.println("hi");
    }
    @Override
    public void sayBye() {
        System.out.println("886");
    }
    public void helpFather(Helper helper) {
        helper.help();
    }
    //在子类中调用父类的方法
    public void greeting() {
        helpFather(super::sayHello);
        /*
        上述代码等价于:
        helpFather(()->super.sayHello());
         */
        helpFather(super::sayBye);
    }
}

​ 针对上例,以下两种写法是等效的:

​ Lambda表达式: () -> super.sayHello()

  • 方法引用:super::sayHello

5 常用函数式接口

  • java.util.function包提供大量的函数式接口

5.1 Supplier接口

  • java.util.function.Supplier<T>接口仅包含一个无参的方法:T get(),用来获得一个泛形参数指定类型的对象数据

示例代码

​ 需求:使用Supplier 接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值.

public class homework11 {
    public static void main(String[] args) {
        int[] arr = {15,1,3,165,168,31,18,3,51,33,155,4,53};
        /*
        抽象方法是在lambda表达式中重写,就像抽象方法在匿名内部类重写一样!!
        这里重写的是Supplier接口的void get()方法
         */
        int max = getMax(() -> {
            int maxValue = arr[0];
            for (int i = 1; i < arr.length; i++) {
                if (arr[i] > maxValue) {
                    maxValue = arr[i];
                }
            }
            return maxValue;
        });
        /*
        上述代码等价于:
        int max = getMax(new Supplier{
            //重写抽象方法
            void get(){
            //下面的是重写方法的内容:
                int maxValue = arr[0];
                for (int i = 1; i < arr.length; i++) {
                    if (arr[i] > maxValue) {
                        maxValue = arr[i];
                    }
                }
                return maxValue;
            }
        });
         */
        System.out.println("max = " + max);

    }
    public static int getMax(Supplier<Integer> supplier) {
        return supplier.get();
    }
}   

5.2 Consumer接口

  • java.util.function.Consumer<T>接口与Supplier接口相反,会消费一个数据。

5.2.1 抽象方法:accept

  • 消费一个指定泛型的数据
  • 格式为void accept(T t);

5.2.2 默认方法:andThen

  • 在消费一个数据时,先做一个操作,再做一个操作,实现组合

示例代码

​ 需求:使用Consumer接口实现字符串大小写

public class Notes09 {
    public static void main(String[] args){
        String str = "HelloWorld";
        // 消费者1:将字符串转换为大写输出
        Consumer<String> one = s -> System.out.println(s.toUpperCase());
        // 消费者2:将字符串转换为小写输出
        Consumer<String> two = s -> System.out.println(s.toLowerCase());
        /*
        可以直接按顺序消费
        one.accept(str);
        two.accept(str);
        也可以通过andThen方法指定顺序,而且可以像链式结构一样一直顺延
        Consumer<String> c = one.andThen(two);
        c.accept(str);*/
        //简化
        one.andThen(two).accept(str);
    }
}

​ 需求:根据字符串数组,按照指定格式输出

public class homework12 {
    public static void main(String[] args) {
        String[] array = { "deep♂dark♂fantacy,男♂", "搞♂比利,男♂", "如花,女♂" };
        //指定消费者
        Consumer<String> c1 = new Consumer<String>() {
            @Override
            public void accept(String s) {
                String[] strs = s.split(",");
                System.out.print("姓名:" + strs[0] + "。");
            }
        };
        Consumer<String> c2 = s -> System.out.println("性别:" + s.split(",")[1] + "。");
        //遍历数组
        for (String str : array) {
            //按序执行
            c1.andThen(c2).accept(str);
        }
    }
}

5.3 Predicate接口

  • 根据某种类型的数据进行判断,返回一个布尔类型的值:java.util.function.Predicate<T>
  • 在Stream流中使用的比较频繁!

5.3.1 抽象方法test

  • 场景判断,满足条件返回true,否则false

  • 格式:boolean test(T t)

  • 使用格式:Predicate<T> predicate.test(T t)

    //示例
    public class Notes01 {
    private static void method(Predicate<String> one) {
        boolean isLong = one.test("Helloworld");//根据外面传入的判断条件调用test(T t),将t放入到lambda中进行判断,返回布尔值
        System.out.println("字符串是否包含H:" + isValid);
        }
    public static void main(String[] args) {
        method(s ‐> s.contains("H"));//lambda作为判断条件传入method中
    }
    }

5.3.2 默认方法and

  • 类似于条件判断“与”
  • 使用格式:Predicate<T> one.and(Predicate<T> two).test(t);
    • 其中one,two只是接口实现对象名,可以用其他名字代替。
//示例
public class Notes01 {
    private static void method(Predicate<String> one, Predicate<String> two) {
        boolean isValid = one.and(two).test("Helloworld");
        System.out.println("字符串符合要求吗:" + isValid);
        }
    public static void main(String[] args) {
        method(s ‐> s.contains("H"), s ‐> s.contains("W"));
    }
}

5.3.2 默认方法or

  • 类似于条件判断“或”
  • 使用格式:Predicate<T> one.or(Predicate<T> two).test(t);
    • 其中one,two只是接口实现对象名,可以用其他名字代替。
//示例
public class Notes01 {
    private static void method(Predicate<String> one, Predicate<String> two) {
        boolean isValid = one.or(two).test("Helloworld");
        System.out.println("字符串符合要求吗:" + isValid);
        }
    public static void main(String[] args) {
        method(s ‐> s.contains("H"), s ‐> s.contains("W"));
    }
}

5.3.2 默认方法negate

  • 类似于条件判断“非”
  • 使用格式:Predicate<T> predicate.negate().test(T t)
//示例
public class Notes01 {
    private static void method(Predicate<String> one) {
        boolean isLong = one.negeate().test("Helloworld");
        System.out.println("字符串是否包含H:" + isValid);
        }
    public static void main(String[] args) {
        method(s ‐> s.contains("H"));
    }
}

5.4 Function接口

  • java.util.function.Function<T,R>根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。R=>T

5.4.1 抽象方法 apply

​ 其中最主要的apply方法为:R apply(T t),根据T的参数获得类型R的结果。比如数据类型的转换。

public class Demo11FunctionApply {
    private static void method(Function<String, Integer> function) {
        int num = function.apply("10");
        System.out.println(num + 20);
    }
    public static void main(String[] args) {
        //lambda
        method(s ‐> Integer.parseInt(s));
        //方法引用
        method(Integer::parseInt);
    }
}

5.4.2 默认方法andThen

  • 组合操作,与Consumer接口的andThen用法一致
  • 使用格式:Function<T,R> one.andThen(Function<T,R> two).apply(t);

6. 总结(延迟方法/终结方法)

  • 延迟方法:只是在拼接Lambda函数模型的方法,并不立即执行得到结果。
  • 终结方法:根据拼好的Lambda函数模型,立即执行得到结果值的方法。

猜你喜欢

转载自blog.csdn.net/KeepStruggling/article/details/81701952