Lambda与函数接口

文章目录

    • 一、Lambda表达式
    • 一、什么是lambda ?
      • 1.1 传统写法
      • 1.2 Lambda写法
    • 二、Lambda基础语法
      • 2.1 格式规范
      • 2.2 示例
    • 三、函数式接⼝
      • 3.1 Runnable
      • 3.2 自定义一个函数式接⼝, 并完整使用一下
      • 3.3 jdk中默认提供的函数接口
        • 3.3.1 Consumer
        • 3.3.2 Function
        • 3.3.3 Supply
        • 3.3.4 Predicate
    • 四、方法引用与构造引用
      • 4.1 静态方法引用
      • 4.2 普通方法引用
      • 4.3 构造函数引用
    • 五、默认⽅法
      • 5.1 什么是默认方法
      • 5.2 为什么要有这个特性?
      • 5.3 java 8抽象类与接⼝对⽐
    • 六、扩展:lambda表达式和闭包
    • 七、总结一下

一、Lambda表达式

以前我们编写一个匿名内部类所书写的代码是十分繁杂的,如下示例:创建线程,而其中只有log.info("do something.");,这一行代码才是对我们有用的,这也是Java一直备受吐槽的地方,语法定义的很严格,但不免有些太过繁杂了。

@Slf4j
public class LambdaExample1 {

    public static void main(String[] args) {
        // 传统的线程写法
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                log.info("do something");
            }
        };
    }
}

好在Java也在一直进步着,在Java8中引入了lambda表达式,提供了更加简单的写法,上面的例子可以用如下写法来完成。

// Lambda写法
Runnable run2 = () -> log.info("do something");

Lambda与函数接口_第1张图片

是不是简单很多呢 ~

一、什么是lambda ?

lambda表达式是⼀段可以传递的代码,它的核⼼思想是将⾯向对象中的传递数据变成传递⾏为,换种说法就是只关注了具体的行为实现,也可以说是一种语法糖。为了更详尽的解释,我们定义一个自己的接口来演示。

// 两个泛型入参,一个泛型返回值
interface Adder<T1, T2, R> {
    R get(T1 t1, T2 t2);
}

1.1 传统写法

//传统写法,计算两个数字和,并返回字符结果
Adder<Integer, Integer, String> func = new Adder<Integer, Integer, String>() {
    @Override
    public String get(Integer t1, Integer t2) {
        int sum = t1 + t2;
        return String.format("sum is %d", sum);
    }
};
log.info(func.get(1, 2));

在上面的代码中,哪些对我们是有用的信息呢?

  • 方法的两个入参与返回值,甚至入参和返回值的类型都是可以不需要的。
  • 方法体中的运算代码。

那么怎么可以化繁变简,让我们更容易去编写呢?

1.2 Lambda写法

//lambda写法
Adder<Integer, Integer, String> func2 = (t1, t2) -> {
    int sum = t1 + t2;
    return String.format("sum is %d", sum);
};
log.info(func2.get(2, 3));

这代码相比上面看起来要简单的很多,⽤()和->的⽅式完成了这件事。使⽤->将参数和实现逻辑分离,当运⾏这个线程的时候执⾏的是->之后的代码⽚段,且编译器帮助我们做了类型推导;

那么接下来和传统的实现对比一下:

  • 方法的定义被省略了。其实很好理解,因为这个接口只有这一个方法,以java的聪明才智,Java是可以自动推导出来的。
  • 方法参数只需要定义名称即可。同样的,Java是可以根据接口定义的方法推导对应的参数类型的。

Tips

  • 当方法参数仅为一个时,可以省略小括号。 x -> { int y = x + 1;return y; }
  • 当方法体只有一行时,可以省略{}, x -> x + 1
  • 当方法体只有一行时,可以省略return关键字, x -> x + 1
  • 没有参数时,可以只书写()
//求和
Adder<Integer, Integer, Integer> func2 = (t1, t2) -> t1 + t2;

通过这个示例和上面的示例,我们使用了同一个接口,却实现了不同的功能,可以感受到lambda带来的方便。

二、Lambda基础语法

2.1 格式规范

在lambda中我们遵循如下的表达式来编写:

(var1, var2, ...) -> {
  action1;
  action2;
}

var

  • 变量占位符。可以有多个变量,也就对应我们原先匿名内部类的方法参数,没有参数可以只书写();

action

  • 这是我们实现的代码逻辑部分,它可以是⼀⾏, 也可以是⼀个代码⽚段, 如果是代码片段,需要用{}进行包裹。如果只有一行,可以省略{},同时可以省略return语句

2.2 示例

单行无参

Runnable r = () -> System.out.println("do something.");

多个参数

Adder adder = (x, y) -> x + y;
  • 这时候我们应该思考这段代码不是之前的x和y数字相加,⽽是创建了⼀个函数, ⽤来计算两个操作数的和。后⾯⽤int类型进⾏接收,在lambda中为我们省略去 了return。

三、函数式接⼝

在前面我们演示lambda时说过,只有当这个接口中有且仅有一个普通接口方法时,Java才可以进行类型推导,我们才可以使用lambda,这是大前提。这种只有一个方法的接口我们称之为函数式接口,也可以叫做功能型接口。Runnable其实就是一个函数接口。

3.1 Runnable

函数式接⼝是只有⼀个⽅法的接⼝,⽤作lambda表达式的类型。前⾯写的例⼦ 同样也是⼀个函数式接⼝,来看看jdk中的Runnable源码

//可以使用该元注解标识函数接口,只是用作标记,就像@Override一样
@FunctionalInterface
Interface public interface Runnable {
  //只有这一个方法,所以lambda的形式可以推导
  public abstract void run();
}

如果在接⼝中编写多个⽅法的时候编译器就会报错。

3.2 自定义一个函数式接⼝, 并完整使用一下

我们来编写⼀个函数式接⼝,输⼊⼀个年龄,判断这个⼈是否是成⼈。

public class FunctionInterfaceDemo {

  @FunctionalInterface
  interface Predicate <T> {
    boolean test(T t);
  }
  /**
   * 执⾏Predicate判断
   * @param age 年龄
   * @param predicate Predicate函数式接⼝
   * @return 返回布尔类型结果
   */
  public static boolean doPredicate(int age, Predicate<Integer> predicate) {
    return predicate.test(age);
  }

  public static void main(String[] args) {
    boolean isAdult = doPredicate(20, x -> x >= 18);
    System.out.println(isAdult);
  }
}

从这个例⼦我们很轻松的完成是否是成⼈的动作,其次判断是否是成⼈,在此之前我们的做法⼀般是编写⼀个判断是否是成⼈的⽅法,是⽆法将判断共⽤的。⽽在本例只,你要做的是将⾏为 (判断是否是成⼈,又或者是判断是否⼤于30 岁) 传递进去,函数式接⼝就可以告诉你结果是什么。

3.3 jdk中默认提供的函数接口

实际上诸如上述例⼦中的接⼝,我们根据参数个数及返回值可以分为四大类

  • 有参无返回值 Consumer 消费型接口
  • 有参有返回值 Function 功能型接口
  • 无参有返回值 Supplier 供给型接口
  • 返回Bool类型 Predicate 断言型接口

我们前⾯写的Adder接⼝就属于断言型接口。那我们思考一下,如果每次都要我们自己去写这样的一个接口,是不是很麻烦?所以伟⼤的jdk设计者为我们准备了java.util.function包,也是JDK中的⼀个实现,主要类型可以分为以下四类:

Lambda与函数接口_第2张图片

3.3.1 Consumer

  • 消费型接口,只有入参,没有返回值
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

使用示例

Consumer consumer = money -> System.out.println("消费了" + money + "元");
consumer.accept(1000);

3.3.2 Function

  • 函数型接口,有入参有返回值
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

使用示例

Function<String, String> function = s -> s.toLowerCase();
System.out.println(function.apply("Hello World"));

3.3.3 Supply

  • 供给型接⼝, 不传入,有返回值
@FunctionalInterface
public interface Supplier<T> {
    T get();
}

使用示例

Supplier<Integer> supplier = () -> (int) (Math.random() * 100);
System.out.println(supplier.get());

3.3.4 Predicate

  • 断⾔型接⼝, 传入参数,返回Boolean类型
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

使用示例: 筛选出只有2个字符长度的单词

public static List<String> filter(List<String> fruit, Predicate<String> predicate){
  List<String> f = new ArrayList<>();
  for (String s : fruit) {
    if(predicate.test(s)) {
      f.add(s);
    }
  }
  return f;
}

public static void main(String[] args) {
  List<String> fruit = Arrays.asList("⾹蕉", "哈密⽠", "榴莲", "⽕⻰龙果", "⽔蜜桃");
  List<String> newFruit = filter(fruit, (f) -> f.length() == 2);
  System.out.println(newFruit);
}

四、方法引用与构造引用

在上面的例子中我们都是通过自己实现具体的方法内容来完成指定的功能,那么如果相应的方法内容已经有写好的实现了,那么我们自己再写一遍就有些多余了,我们可不可以直接引用现有的实现方法呢?答案当然是可以的,下面以示例进行演示。

4.1 静态方法引用

格式

类名::方法名

@Slf4j
public class LambdaExample4 {

    public static void main(String[] args) {
        // 自己实现lambda
        Consumer<String> consumer = (str) -> log.info("Hello {}!", str);
        consumer.accept("World");

        //调用已有的方法进行引用
        Consumer<String> consumer2 = LambdaExample4::hello;
        consumer2.accept("World");

    }

    public static void hello(String word) {
        log.info("Hello {}!", word);
    }

}

4.2 普通方法引用

根据常识,如果是普通方法,肯定需要由相应的对象实例来调用的,这里也不例外。

格式

实例::方法名

@Slf4j
public class LambdaExample5 {

    public static void main(String[] args) {
        LambdaExample5 lambdaExample = new LambdaExample5();
        Consumer<String> consumer = lambdaExample::hello;

        consumer.accept("World");
    }

    public  void hello(String word) {
        log.info("Hello {}!", word);
    }
}

4.3 构造函数引用

某个角度来说,构造函数只是一种特殊的方法而已。返回值是一个对象,使用new进行调用。

@Slf4j
public class LambdaExample6 {

    public static void main(String[] args) throws Exception {
        // 构造一个指定大小的ArrayList
        Function<Integer, ArrayList<String>> function = ArrayList::new;
        ArrayList<String> list = function.apply(10);

        //利用反射查看容量,for practice
        Class clazz = list.getClass();
        Field field = clazz.getDeclaredField("elementData");
        field.setAccessible(true);
        Object[] objects = (Object[]) field.get(list);
        log.info("list capacity is {}", objects.length);
    }
}

小提示:如果你觉得lambda的⽅法体会很⻓长,影响代码可读性,⽅法引⽤就是个解决办法

五、默认⽅法

5.1 什么是默认方法

简单说,就是接⼝可以有实现⽅法,⽽且不需要实现类去实现其⽅法。只需在 ⽅法名前⾯加个default关键字即可。

5.2 为什么要有这个特性?

⾸先,之前的接⼝是个双刃剑,好处是⾯向抽象⽽不是⾯向具体编程,缺陷是,当需要修改接⼝时候,需要修改全部实现该接⼝的类,⽬前的java8之前的集合框架没有foreach⽅法,通常能想到的解决办法是在JDK⾥给相关的接⼝添加新的⽅法及实现。然⽽,对于已经发布的版本,是没法在给接⼝添加新⽅法的同时不影响已有的实现。所以引进的默认⽅法。他们的⽬的是为了解决接⼝的修改与现有的实现不兼容的问题。

所以在Java8中加入了默认方法,由接口去实现一些方法,而子类则直接拥有这些方法实现,并可以直接调用。

ps: 1.其实某种程度来说,加入默认方法也是无奈之举。
ps: 2.所以接口中看到方法实现,也没什么了,面试题要换换了

在Java8种引⼊新的机制:默认方法,⽀持在接⼝中声明⽅法同时提供实现。 有两种⽅式完成

  1. 在接⼝内声明静态⽅法
  2. 指定⼀个默认⽅法。

如下示例是不会报错的。

public interface DefaultMethodExample7 {

    default String hello() {
        return "Hello World!";
    }

    default String sameMethod() {
        return "From DefaultMethodExample7";
    }

    static String say() {
        return "Hello Friends!";
    }
}

这时子类是可以调用到父类的默认方法的。

@Slf4j
class TestDemo implements DefaultMethodExample7, SameInterface {

    public static void main(String[] args) {
        TestDemo testDemo = new TestDemo();
        // 可以直接访问接口的默认方法
        log.info(testDemo.hello());
        // 接口的静态方法只能通过接口访问
        log.info(DefaultMethodExample7.say());

        // 当实现的接口有相同的方法时,需要子类覆盖
        log.info(testDemo.sameMethod());

    }

    @Override
    public String sameMethod() {
        // 在方法中可以调用相应的接口的同名默认方法
        // 格式: 接口.super.方法名
        DefaultMethodExample7.super.sameMethod();
        SameInterface.super.sameMethod();
        return "My Owner Method";
    }
}

interface SameInterface {

    default String sameMethod() {
        return "From DefaultMethodExample7";
    }

}

5.3 java 8抽象类与接⼝对⽐

这⼀个功能特性出来后,可能会觉得java8的接⼝都有实现⽅法了, 跟抽象类还有什么区别?其实还是有的,请看下表对⽐。

相同点 不同点
都是抽象类型; 抽象类不可以多重继承,接⼝可以 (⽆论是多重类型继承还是多重⾏为 继承);
都可以有实现⽅法(以前接⼝不⾏ ); 抽象类和接⼝所反映出的设计理念 不同。其实抽象类表示的是"is-a"关 系,接⼝表示的是"like-a"关系
都可以不需要实现类或者继承者去 实现所有⽅法,(以前不⾏,现在接 ⼝中默认⽅法不需要实现者实现) 接⼝中定义的变量默认是public st atic final

六、扩展:lambda表达式和闭包

闭包就是把函数以及变量包起来,使得变量的⽣存周期延⻓。

其实java中从1.6就支持闭包了,talk is cheap, show the code below:

@Slf4j
public class ClosureDemo {

    public static void main(String[] args) {
        log.info("closure value: {}", closureTest().get());
        log.info("lambda value: {}", lambdaTest().get());
    }

    /**
     * 使用闭包.
     *
     * @return lambda.Supplier
     */
    private static Supplier<Integer> closureTest() {
        final int i = 1;
        return new Supplier<Integer>() {
            @Override
            public Integer get() {
                return i;
            }
        };
    }

    /**
     * 使用lambda表达式.
     *
     * @return lambda.Supplier
     */
    private static Supplier<Integer> lambdaTest() {
        int i = 1;
        return () -> i;
    }
}

/**
 *
 * @param 
 */
interface Supplier<T> {
    T get();
}

七、总结一下

  • Lambda是一个语法糖,可以帮我们简化写法,并且提供了一些常用函数接口供我们使用。
  • 接口可以有默认方法,和静态方法实现。

你可能感兴趣的:(☀Java,-------【JavaSE】)