lambda表达式和函数式接口最详细的介绍

Lambda表达式

在说Lambda表达式之前我们了解一下函数式编程思想,在数学中,函数就是有输入量、输出量的一套计算方案,也就是拿什么东西做什么事情

相对而言,面向对象过分强调必须通过对象的形式来做事情,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。 下面以匿名内部类创建线程的代码案例详细说明这个问题。

 

public class ThreadDemo {
public static void main(String[] args) {
//实现Runnable方式创建简单线程--传统匿名内部类形式
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("开启了一个线程----匿名内部类");
}
}).start();

//实现Runnable方式创建简单线程--Lambda表达式形式
new Thread(()-> System.out.println("开启了一个线程---Lambda表达式")).start();
}
}
运行结果:

开启了一个线程----匿名内部类
开启了一个线程---Lambda表达式

对以上代码的分析:

对于 Runnable 的匿名内部类用法,可以分析出几点内容:

Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来指定线程任务内容的核心;
为了指定 run 的方法体,不得不需要 Runnable 接口的实现类;
为了省去定义一个 RunnableImpl 实现类的麻烦,不得不使用匿名内部类;
必须覆盖重写抽象 run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
而实际上,似乎只有方法体才是关键所在

传统的写法比Lambda表达式写法显而易见代码繁琐了许多,而且2者实现目的是相同的。

我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将 run 方法体内的代码传递给 Thread 类知晓。

传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。

那,有没有更加简单的办法?如果我们将关注点从怎么做回归到做什么的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。 

这时就要用到函数式编程思想了,只关注“做什么”,而不是以什么方式做!!

了解过函数式编程思想后,我们要尝试着转变思想,从面向对象的"怎么做"转换为函数式编程思想的“做什么”,只有思想有了转变,才能更好的了解和学习Lambda表达式。

什么是Lambda表达式?

Lambda 是一个匿名函数,我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。

作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升 。20143Oracle所发布的Java 8JDK 1.8)中,加入了Lambda表达式 

Lambda表达式语法:( ) ->  { }

Lambda 表达式在Java 语言中引入了一个新的语法元素和操作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符或剪头操作符。它将 Lambda 分为

两个部分:

左侧 (): 指定了 Lambda 表达式需要的所有参数

右侧  {}: 指定了 Lambda 体,即 Lambda 表达式要执行的功能。 

Lambda表达式标准格式:(参数类型 参数名称) ‐> { 代码语句 }

格式进一步说明:

小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
-> 是新引入的语法格式,代表指向动作。
大括号内的语法与传统方法体要求基本一致

Lambda表达式如何使用呢?

Lambda表达式的使用是有前提的,必须要满足2个条件:1.函数式接口      2.可推导可省略。

函数式接口是指一个接口中只有一个必须被实现的方法。这样的接口都满足一个注解@FunctionalInterface

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

可推导可省略是指上下文推断,也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例 。

下面我们自定义一个函数式接口,使用Lambda表达式完成功能。

public class Demo {
    public static void main(String[] args) {
        invokeCook(()->{
            System.out.println("做了一盘红烧鱼....");
        });

    }
    //需要有个以函数式接口为参数的方法
    public static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}
//自定义函数式接口
@FunctionalInterface
interface Cook{
    void makeFood();
}

以上案例是函数式接口以及Lambda表达式最简单的定义和用法。

针对Lambda表达式还可以做出进一步的省略写法:

1.小括号内参数的类型可以省略;
2. 如果小括号内有且仅有一个参,则小括号可以省略;
3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。 

所以上面的代码可以简写为:

invokeCook(()-> System.out.println("做了一盘红烧鱼...."));

Lambda表达式有多种语法,下面我们了解一下。(直接写省略形式)

1.无参,无返回值,Lambda体只需一条语句

Runnable r  = ()->System.out.println("hell lambda");

2.Lambda表达式需要一个参数,无返回值

Consumer c = (str)-> System.out.println(args);

当Lambda表达式只有一个参数时,参数的小括号可以省略

 Consumer c = str-> System.out.println(args);

3.Lambda表达式需要2个参数,并且有返回值

BinaryOperator<Long> bo = (num1,num2)->{ return num1+num2;};

当Lambda体中只有一条语句时,return 和 大括号、分号可以同时省略。

BinaryOperator<Long> bo = (num1,num2)-> num1+num2;

有没有发现我们没写参数类型,Lambda表达式依然可以正确编译和运行,这是因为Lambda表达式拥有的类型推断功能。

上述 Lambda 表达式中的参数类型都是由编译器推断得出的。 Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,

在后台推断出了参数的类型。 Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的“类型推断” 。

到此,Lambda表达式的基本知识就算学完了。

有人可能会提出疑问,Lambda表达式使用前要定义一个函数式接口,并在接口中有抽象方法,还要创建一个以函数式接口为参数的方法,之后调用该方法才能使用Lambda表达式,感觉并没有省很多代码!!哈哈,之所以有这样的想法,那是因为是我们自定义的函数式接口,而JDK1.8及更高的版本都给我们定义函数式接口供我们直接使用,就没有这么繁琐了。接下来我们学习一下JDK为我们提供的常用函数式接口。

Lambda表达式还具有延迟执行的作用:改善了性能浪费的问题,代码说明。

public class Demo {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = "Lambda";
        String str3 = "表达式";
        log(1,str1+str2+str3);
    }
    public static void log(int level,String str) {
        if (level == 1) {
            System.out.println(str);
        }
    }
}

在上面代码中,存在的性能浪费问题是如果 输入的level!=1,而str1+str2+str3作为log方法的第二个参数还是参与了拼接运算,但是我们的实际想法应该是不满足level=1的条件就不希望str1+str2+str3进行拼接运算,下面通过Lambda表达式来实现这个功能。

public class Demo {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = "Lambda";
        String str3 = "表达式";
        log(1,()->str1+str2+str3);
    }
    public static void log(int level,Message message) {
        if (level == 1) {
            System.out.println(message.message());
        }
    }
}
@FunctionalInterface
interface Message {
    String message();
}

以上代码功能相同,Lambda表达式却实现了延迟,解决了性能浪费,下面我们来验证一下:

public class Demo {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = "Lambda";
        String str3 = "表达式";
        log(2,()->{
            System.out.println("lambda 执行了");
            return str1+str2+str3;
        });
    }
    public static void log(int level,Message message) {
        if (level == 1) {
            System.out.println(message.message());
        }
    }
}
@FunctionalInterface
interface Message {
    String message();
}

此时在输入level=2的条件时,如果Lambda不延迟加载的话会执行输出语句输出lambda 执行了,而实际是控制台什么也没输出,由此验证了Lambda表达式的延迟执行。

在Lambda表达式的应用过程中还有一种比较常用的方式:方法引用。方法引用比较难以理解,而且种类也较多,需要多费脑筋去理解。

Lambda表达式应用之 :方法引用

方法引用也是有前提的,分别为:

1.前后的参数名一致,

2.Lambda表达式的方法体跟对应的方法的功能代码要一模一样



方法引用种类可以简单的分为4+2种,4种跟对象和类有关,2种跟构造方法有关。下面一一说明。

跟对象和类有关的方法引用:

1.对象引用成员方法

  格式:对象名 :: 成员方法名      (双冒号 :: 为引用运算符,而它所在的表达式被称为方法引用

  原理:将对象的成员方法的参数和方法体,自动生成一个Lambda表达式。

 1 public class Demo {
 2     public static void main(String[] args) {
 3         Assistant assistant = new Assistant();
 4         work(assistant::dealFile);//对象引用成员方法(注意是成员的方法名,没有小括号)
 5     }
 6     //以函数式接口为参数的方法
 7     public static void work(WokerHelper wokerHelper) {
 8         wokerHelper.help("机密文件");
 9     }
10 }
11 //助理类,有个成员方法
12 class Assistant{
13     public void dealFile(String file) {
14         System.out.println("帮忙处理文件:"+file);
15     }
16 }
17 //函数式接口,有个需要实现的抽象方法
18 @FunctionalInterface
19 interface WokerHelper {
20     void help(String file);
21 }

2.类调用静态方法

  格式:类名 :: 静态方法名

  原理:将类的静态方法的参数和方法体,自动生成一个Lambda表达式。

public class Demo {
    public static void main(String[] args) {
        methodCheck((str)->StringUtils.isBlank(str),"   ");//非省略模式
        methodCheck(StringUtils::isBlank,"  ");//省略模式  类名调用静态方法
    }
    //
    public static void methodCheck(StringChecker stringChecker,String str) {
        System.out.println(stringChecker.checkString(str));
    }
}
//定义一个类包含静态方法isBlank方法
class StringUtils{
    public static boolean isBlank(String str) {
        return str==null || "".equals(str.trim());//空格也算空
    }
}
//函数式接口,有个需要实现的抽象方法
@FunctionalInterface
interface StringChecker {
    boolean checkString(String str);
}

3.this引用本类方法

  格式:this :: 本类方法名

  原理:将本类方法的参数和方法体,自动生成一个Lambda表达式。

public class Demo {
    public static void main(String[] args) {
        new Husband().beHappy();
    }
}
class Husband{
    public void buyHouse() {
        System.out.println("买套房子");
    }

    public void marry(Richable richable) {
        richable.buy();
    }
    public void beHappy() {
        marry(this::buyHouse);//调用本类中方法
    }
}
//函数式接口,有个需要实现的抽象方法
@FunctionalInterface
interface Richable {
    void buy();
}

4.super引用父类方法

  格式:super :: 父类方法名

  原理:将父类方法的参数和方法体,自动生成一个Lambda表达式。

public class Demo {
    public static void main(String[] args) {
       new Man().sayHello();
    }
}
//子类
class Man extends Human{
    public void method(Greetable greetable) {
        greetable.greet();
    }
    @Override
    public void sayHello() {
        method(super::sayHello);
    }
}
//父类
class Human{
  public void sayHello() {
      System.out.println("Hello");
  }
}
//函数式接口,有个需要实现的抽象方法
@FunctionalInterface
interface Greetable {
    void greet();
}

跟构造方法有关的方法引用:

5.类的构造器引用

  格式:  类名  :: new

  原理:将类的构造方法的参数和方法体自动生成Lambda表达式。

public class Demo {
    public static void main(String[] args) {
        printName("张三",(name)->new Person(name));
        printName("张三",Person::new);//省略形式,类名::new引用
    }

    public static void printName(String name, BuildPerson build) {
        System.out.println(build.personBuild(name).getName());
    }
}

//函数式接口,有个需要实现的抽象方法
@FunctionalInterface
interface BuildPerson {
    Person personBuild(String name);
}
//实体类
class Person{
    String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}

6.数组的构造器引用

  格式: 数组类型[] :: new

  原理:将数组的构造方法的参数和方法体自动生成Lambda表达式。

public class Demo {
    public static void main(String[] args) {
        int[] array1 = method(10, (length) -> new int[length]);
        int[] array2 = method(10, int[]::new);//数组构造器引用
    }

    public static int[] method(int length, ArrayBuilder builder) {
       return builder.buildArray(length);
    }
}

//函数式接口,有个需要实现的抽象方法
@FunctionalInterface
interface ArrayBuilder {
    int[] buildArray(int length);
}

常用的函数式接口

1.Supplier<T> 供给型接口

  

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要对外提供一个符合泛型类型的对象数据。 

 如果要定义一个无参的有Object返回值的抽象方法的接口时,可以直接使用Supplier<T>,不用自己定义接口了。

public class Demo {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = "lambda";
        String s = method(() -> str1 + str2);
        System.out.println("s = " + s);
    }

    public static String method(Supplier<String> supplier) {
       return supplier.get();
    }
}

2.Consumer<T> 消费型接口

@FunctionalInterface
public interface Consumer<T> {

void accept(T t);
  
  //合并2个消费者生成一个新的消费者,先执行第一个消费者的accept方法,再执行第二个消费者的accept方法   default Consumer<T> andThen(Consumer<? super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; } }
Consumer<T> 接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定 。
如果要定义一个有参的无返回值的抽象方法的接口时,可以直接使用Consumer<T>,不用自己定义接口了。
public class Demo {
    public static void main(String[] args) {
       consumerString(string -> System.out.println(string));
       consumerString(System.out::println);//方法引用形式
    }

    public static void consumerString(Consumer<String> consumer) {
       consumer.accept("fall in love!");
    }
}

3.Predicate<T> 断定型接口

@FunctionalInterface
public interface Predicate<T> {

 //用来判断传入的T类型的参数是否满足筛选条件,满足>true
boolean test(T t);

//合并2个predicate成为一个新的predicate---->并且&&
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}

 //对调用的predicate原来的结果进行取反---->取反 !
default Predicate<T> negate() {
return (t) -> !test(t);
}

//合并2个predicate成为一个新的predicate---->或||
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}

}

  Predicate<T>接口主要是对某种类型的数据进行判断,返回一个boolean型结果。可以理解成用来对数据进行筛选

  当需要定义一个有参并且返回值是boolean型的方法时,可以直接使用Predicate接口中的抽象方法

  

 1 //1.必须为女生;
 2 //2. 姓名为4个字。
 3 public class Demo {
 4     public static void main(String[] args) {
 5         String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男", "赵丽颖,女" };
 6         List<String> list = filter(array,
 7                 str-> "女".equals(str.split(",")[1]),
 8                 str->str.split(",")[0].length()==3);
 9         System.out.println(list);
10     }
11     private static List<String> filter(String[] array, Predicate<String> one, Predicate<String> two) {
12         List<String> list = new ArrayList<>();
13         for (String info : array) {
14             if (one.and(two).test(info)) {
15                 list.add(info);
16             }
17         }
18         return list;
19     }
20 }

4.Function<T,R> 函数型接口  

@FunctionalInterface
public interface Function<T, R> {

   //表示数据转换的实现。T--->R
    R apply(T t);

   //合并2个function,生成一个新的function,调用apply方法的时候,先执行before,再执行this
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
  //合并2个function,生成一个新的function,调用apply方法的时候,先执行this,再执行after
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
}

  Function<T,R> 接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有进有出,所以称为函数Function”

   该接口可以理解成一个数据工厂,用来进行数据转换,将一种数据类型的数据转换成另一种数据.   泛型参数T:要被转换的数据类型(原料),泛型参数R:想要装换成的数据类型(产品)。

public class Demo {
    public static void main(String[] args) {
        String str = "赵丽颖,20";
        int age = getAgeNum(str,
                string ->string.split(",")[1],
                Integer::parseInt,//str->Integer.parseInt(str);
                n->n+=100);
        System.out.println(age);
    }
    //实现三个数据转换 String->String, String->Integer,Integer->Integer
    private static int getAgeNum(String str, Function<String, String> one,
                                Function<String, Integer> two,
                                Function<Integer, Integer> three) {
        return one.andThen(two).andThen(three).apply(str);
    }
}

至此,常用的四个函数式接口已学习完毕。

总结一下函数式表达式的延迟方法与终结方法:

延迟方法:默认方法都是延迟的。

终结方法:抽象方法都是终结的。

接口名称 方法名称 抽象方法/默认方法 延迟/终结
Supplier get 抽象 终结
Consumer accept 抽象   终结
  andThen 默认 延迟
Predicate test 抽象 终结
  and 默认 延迟
  or 默认 延迟
  negate 默认 延迟
Function apply 抽象 终结
  andThen   默认   延迟

函数式接口在Stream流中的应用较为广泛,其中Stream流中的过滤Filter方法使用到了Predicate的判定,map方法使用到了Function的转换,将一个类型的流转换为另一个类型的流。

猜你喜欢

转载自www.cnblogs.com/JohnsonLiu/p/9863309.html