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的语言表达能力得到了提升 。(2014年3月Oracle所发布的Java 8(JDK 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个参数,并且有返回值
BinaryOperatorbo = (num1,num2)->{ return num1+num2;};
当Lambda体中只有一条语句时,return 和 大括号、分号可以同时省略。
BinaryOperatorbo = (num1,num2)-> num1+num2;
有没有发现我们没写参数类型,Lambda表达式依然可以正确编译和运行,这是因为Lambda表达式拥有的类型推断功能。
上述 Lambda 表达式中的参数类型都是由编译器推断得出的。 Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,
在后台推断出了参数的类型。 Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的“类型推断” 。
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); }
到此,Lambda表达式的基本知识就算学完了。
有人可能会提出疑问,Lambda表达式使用前要定义一个函数式接口,并在接口中有抽象方法,还要创建一个以函数式接口为参数的方法,之后调用该方法才能使用Lambda表达式,感觉并没有省很多代码!!哈哈,之所以有这样的想法,那是因为是我们自定义的函数式接口,而JDK1.8及更高的版本都给我们定义函数式接口供我们直接使用,就没有这么繁琐了。接下来我们学习一下JDK为我们提供的常用函数式接口。
常用的函数式接口
1.Supplier
@FunctionalInterface public interface Supplier{ T get(); }
用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。
如果要定义一个无参的有Object返回值的抽象方法的接口时,可以直接使用Supplier
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(Suppliersupplier) { return supplier.get(); } }
2.Consumer
@FunctionalInterface public interface Consumer{
void accept(T t);
//合并2个消费者生成一个新的消费者,先执行第一个消费者的accept方法,再执行第二个消费者的accept方法 default ConsumerandThen(Consumer super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; } }
Consumer接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛 型参数决定 。
如果要定义一个有参的无返回值的抽象方法的接口时,可以直接使用Consumer,不用自己定义接口了。
public class Demo { public static void main(String[] args) { consumerString(string -> System.out.println(string)); consumerString(System.out::println);//方法引用形式 } public static void consumerString(Consumerconsumer) { consumer.accept("fall in love!"); } }
3.Predicate
@FunctionalInterface
public interface Predicate{
//用来判断传入的T类型的参数是否满足筛选条件,满足>true
boolean test(T t);
//合并2个predicate成为一个新的predicate---->并且&&
default Predicateand(Predicate super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
//对调用的predicate原来的结果进行取反---->取反 !
default Predicatenegate() {
return (t) -> !test(t);
}
//合并2个predicate成为一个新的predicate---->或||
default Predicateor(Predicate super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
}
Predicate
当需要定义一个有参并且返回值是boolean型的方法时,可以直接使用Predicate接口中的抽象方法
1 //1.必须为女生; 2 //2. 姓名为4个字。 3 public class Demo { 4 public static void main(String[] args) { 5 String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男", "赵丽颖,女" }; 6 Listlist = 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 filter(String[] array, Predicate one, Predicate two) { 12 List 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
@FunctionalInterface public interface Function{ //表示数据转换的实现。T--->R R apply(T t); //合并2个function,生成一个新的function,调用apply方法的时候,先执行before,再执行this default Function compose(Function super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } //合并2个function,生成一个新的function,调用apply方法的时候,先执行this,再执行after default Function andThen(Function super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } }
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, Functionone, Function two, Function 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的转换,将一个类型的流转换为另一个类型的流。