Lambda表达式(为简化代码而生)

什么是Lambda表达式呢?

有人把它称之为“闭包的替代品”,也有人把它称之为匿名函数,它究竟是何方神圣,它在Java中到底充当了什么样的角色?一起关注今晚8点“程序员有话说”,我们不见不散!

哈哈,开个玩笑。它本质上就是一个代码块。

那这个Lambda表达式它具体表现为什么样的结构呢?

它的基础语法其实就是引入了一个操作符:(->),因此也有人把它称之为箭头函数。

这个箭头把一个代码分为左侧和右侧两部分,它的左侧就代表这个表达式的参数列表,而右侧就代表这个表达式的功能,也叫Lambda体。

了解并使用lambda表达式以后可以大大降低我们的代码量,不会增加我们的理解难度,并且让我们的逻辑看起来更加清晰。

首先我们先看这样一个案例:

List<String> list = Arrays.asList("小明","小刚","小艾");

需求:打印该列表中每一个同学的姓名,名字之间用“,”隔开

我们很自然地想到,利用循环来遍历这个列表,依次打印列表中每一个值:

for(String s:list){
    System.out.print(s+",");
}
//执行结果:小明,小刚,小艾,

IntelliJ IDEA小技巧,写遍历语句时,可以直接输入以下内容(即可自动生成代码):

  • itar:普通for遍历数组
  • itli:普通for遍历列表
  • iter:增强for遍历列表

但是这样一来我们发现,它打印的结果是:小明,小刚,小艾,

最后一个人后面也有逗号,很显然不合理。

那么如何解决这个问题呢?

程序中一个问题的解决方案可能是千千万万的,那么怎么做才更合理,更优雅一些呢?

我们可以通过字符串的截取操作,把最后一个逗号截取掉,当然我们也可以通过另外一种遍历形式得到:

for(int i = 0;i<list.size()-1;i++){
    System.out.print(list.get(i)+",");
}
if(list.size()>0){
    System.out.println(list.get(list.size()-1));
}
//执行结果:小明,小刚,小艾

但是这样的代码看起来显得冗余,一点也不优雅。

这里我们给出优雅的解决方案:

//Java8之后的String里的join方法
System.out.println(String.join(",",list));//小明,小刚,小艾

这一行代码即可实现我们以前用多行循环语句加一次if语句才可以做到的事情。

它虽然跟Lambda没有啥关系,但它体现了代码简化的必要性和优雅性,支持这种方式实际上也是未来的一种趋势,就是写代码越来越简单了。

实质上Java8对于代码的优化不止于此

本文中所介绍的 Lambda表达式就是Java8对于代码优化的一种方式

现在我们改变需求,还是刚刚那个列表,现在只需要遍历每一项进行打印即可。

List<String> list = Arrays.asList("小明","小刚","小艾");
//快捷-->list遍历:itli(增强for:iter)    数组遍历;itar
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}
for (String s : list) {
    System.out.println(s);
}

增强for显然比普通for看起来更加简洁。

但是增强for还可不可以更加简洁呢?

我们可以直接调用这个list的forEach()方法,而这个forEach()方法中,它是可以支持函数式接口的(参数是Consumer,后面再详细介绍它):

//list的forEach方法,它支持Lambda表达式
//lambda的复杂方式
list.forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
});

通过这种方式,我们仍然可以遍历list,但是这种方式看起来貌似并没有比上面的增强for简洁多少,甚至还要复杂一些。这里我们需要注意,这是Lambda的复杂方式。

这种Lambda的实现方式实际上是可以逐步简化的:

//lambda的简易方式
list.forEach((String s) -> System.out.println(s) );

这种方式甚至可以更加简化:

//lambda的最简易方式
list.forEach(System.out::println);//它可以直接打印出list中每一项的值

不只是集合可以用,数组也可以用,很多其它地方也可以使用Lambda表达式,这里仅仅是使用列表的遍历来举例说明。

和匿名内部类的渊源

Lambda确实是简化代码了,方便了,简洁了,但是它为什么这么方便,它是如何使用,并且是如何做到的呢?

我们先来看看以下的案例:

Lambda的使用其实最早是和匿名内部类相关的。

匿名内部类其实也是一种内部类,只不过没有名字,所以它是一次性的,即只能使用一次。

匿名内部类也是用来简化代码的,但是它需要有一个前提条件:继承父类或实现一个接口:

现在我们来写一个不使用匿名内部类的方式:

package testLambda;
/**
 * 不使用匿名内部类的方式
 */
abstract class Person {
    public abstract void eat();
}

class Child extends Person{
    @Override
    public void eat() {
        System.out.println("小孩吃零食");
    }
}
public class Demo1 {
    public static void main(String[] args) {
        Person person = new Child();
        person.eat();
    }
}

我们再写一个使用匿名内部类的方式:

package testLambda2;
/**
 * 使用匿名内部类的方式
 */
abstract class Person {
    public abstract void eat();
}

public class Demo2 {
    public static void main(String[] args) {
        //使用匿名内部类,相当于我们只使用一次,不需要给它创建额外的类了
        Person person = new Person() {
            @Override
            public void eat() {
                System.out.println("小孩吃零食");
            }
        };
        person.eat();
    }
}

如果我们需要使用到多线程,那么我们如何使用它的匿名内部类呢?

package testLambda2;
/**
 * 匿名内部类+多线程
 */
public class Demo3 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("threading run");
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

如果使用Lambda表达式来声明一个Runnable,可以这样做:

//无参数,无返回值
Runnable runnable1 = () -> System.out.println("threading1 run");
new Thread(runnable1).start();

这是比较简单的Lambda表达式。

它左侧是一个空的(),表示:无参

右侧有且仅有一句打印输出的代码,表示:无返回值

它的基础语法其实就是引入了一个操作符:->

左侧:参数列表
右侧:执行的功能代码(lambda体)

它实际上就是一种的语法糖,只是看起来简单了,但编译器还是要老老实实得把它推导编译成常规的代码。

接下来我们看看这样一组案例:

String [] strArr = {"LiHua","Tom","HongHong"};

需求:我们希望通过字符串的长度,进行排序

不使用Lambda表达式,我们是这样实现的:

package testLambda2;

import java.util.Arrays;
import java.util.Comparator;

public class Demo4 {
    public static void main(String[] args) {
        String [] strArr = {"LiHua","Tom","HongHong"};
        //我们希望通过字符串的长度,进行排序
        //Arrays.sort()默认是根据字符串首字母来进行排序的
        //因此我们希望自定义Arrays.sort()的排序规则
        //首先自己生成一个比较器
        class LengthComparator implements Comparator<String>{
            @Override
            public int compare(String o1, String o2) {
                //借用Integer的compare方法为我们比较
            /*如果o1.length()<o2.length() 返回-1
            相等:返回0
            o1.length()>o2.length() 返回1
             */
                return Integer.compare(o1.length(),o2.length());
            }
        }
        //复用了Arrays的排序方法
        Arrays.sort(strArr,new LengthComparator());
        System.out.println(Arrays.toString(strArr));//[Tom, LiHua, HongHong]
    }
}

当然上面的方法显得很麻烦,我们还得自己定义一个类,实现Comparator接口,虽然我们复用了Integer的compare方法为我们比较,但代码还是有点冗余。

优化方案:

实质上Arrays.sort()是可以直接支持Lambda表达式的,它的第一个参数仍然是数组本身,第二个参数就可以是Lambda表达式。

public static void main(String[] args) {
    String [] strArr = {"LiHua","Tom","HongHong"};
	//有参数,有返回值
    Arrays.sort(strArr,(String o1,String o2) -> 
                Integer.compare(o1.length(),o2.length()) );
    System.out.println(Arrays.toString(strArr));//[Tom, LiHua, HongHong]
}

以上是我们使用Integer的比较方法,Lambda体一行即可搞定。

但是如果我们自己定义compare比较规则,Lambda体就不止一行了,那么多行的Lambda该怎么写呢?

把多行代码用大括号括起来,代替上面一行的Lambda体:

Arrays.sort(strArr,(String o1,String o2) -> {
    if (o1.length()<o2.length()) return -1;
    else if(o1.length()>o2.length()) return 1;
    else return 0;
});

注意:这种形式的Lambda要求必须覆盖 if 语句的所有可能情况,漏掉一种都不行。

即必须确保它一定有返回值。

我们现在再来看上面的那种形式的Lambda表达式:

Arrays.sort(strArr,(String o1,String o2) ->
        Integer.compare(o1.length(),o2.length()) );

对于这种有参数有返回值的Lambda,它有两个参数,o1和o2分别都是String类型的。而且我们要处理的是String数组里的字符串。也就是说,我们很明确自己要处理的就是String类型,这个时候我们就可以把参数的类型省略:

Arrays.sort(strArr,(o1,o2) ->
        Integer.compare(o1.length(),o2.length()) );

以上代码中,我们把参数类型省略,编译器仍然能够推算出参数的类型,并且能够正确执行。

这说明,如果参数的类型可以推断出来,那么类型可以省略。

当然,如果参数添加了类型,那么该参数也是可以添加一些修饰符的,例如 final等,还可以添加注解。

Arrays.sort(strArr,(final String o1,final String o2) ->
        Integer.compare(o1.length(),o2.length()) );

这里我们再举一些例子,看看其它的Lambda表达式是怎样写的:

  • 无参并返回一个数值5:() -> 5

  • 单一参数的 lambda:(int x) -> 2*x

  • 单一参数,且可以直接推断出其类型,就可省略小括号:x -> 2*x

以上就是Lambda表达式最基础的使用以及它的简化方式。

有时Lambda还可以更加简化,连箭头 -> 都可以省略。

在什么情况下可以省略箭头呢?这里就不得不提到另一种使用方式了,叫做方法引用

先看以下代码:

//这里还是使用Java8给我们提供的函数式接口Consumer,声明Lambda
Consumer<String> consumer = (x) -> System.out.println(x);
//相当于调用Lambda
consumer.accept("Studying Lambda");

以上的Lambda中,前面的x是对参数的接收,后面lambda体中也仅仅只是对参数x的使用。

像这种表达式的参数和方法的参数完全相同的情况,参数和箭头就可以隐藏:

Consumer<String> consumer = System.out::println;
consumer.accept("Studying Lambda");

以上是更加简单的Lambda表达式的使用方式,而这种方式我们称之为方法引用。而且它仅仅是方法引用其中的一种。

方法引用

方法引用的方式是使得调用者和两个方法之间使用两个冒号(::)进行分隔,总的来说它大致分为3种情况。

  1. 对象调用它里面的方法:object :: instanceMethod
  2. 类调用它的静态方法:class :: staticMethod
  3. 类调用它的实例方法:class :: instanceMethod

说到调用方法,在类的内部,有两个很重要的关键字:this和super

它们在Lambda表达式中也是可以使用方法引用的。

举个小例子:

class Student{
    public void print(){
        System.out.println("I am a Student");
    }
}
class CollegeStudent extends Student{
    @Override
    public void print() {
        //通过线程调用父类的方法
        Thread thread = new Thread(super::print);//this同理
        thread.start();
    }
}

函数式接口

首先思考这样一个问题:为什么我们需要Lambda表达式呢?

从Java语言诞生起它的变化其实是不大的,而且Java中有个很重要的特征是面向对象的编程思想,它和函数式语言JavaScript是截然不同的,那么Java如何去强调它面向对象的本质呢?就是因为在java的世界里函数是没有办法独立存在的,如果说在函数式语言中这个函数是“头等公民”,那么在Java中就不提供这种支持,那它怎么能够去更好得支持函数在其中的作用呢?所以Java的设计者就冥思苦想,想出了这种Lambda表达式的方式,比较类似于 js 里的闭包,但它们还是有一些差别的,但是它相当于闭包的一种替代方案。

Lambda表达式,首先它其实是一种函数,而他这个函数也要是一种对象啊,在Java中万物皆对象,那么这个表达式它是一种什么样的对象类型呢?它是一种函数式接口(functional interface)。

函数式接口,首先它必须是一个接口,并且它里面只能够有一个抽象方法,那么这种接口我们称之为:SAM,它是Single Abstract Method Interfaces的简称。

我们能够使用Lambda表达式,本质上是那个方法它支持了Lambda表达式,包括Integer的sort(),包括Consumer<>等。这是Java8给我们提供的对于原方法的升级,它并没有改变Java面向对象的本质。

那么如果我们希望自己创建的类也可以使用Lambda表达式,该怎么做呢?

我们可以自己声明:(自定义函数式接口)

//首先它必须是一个接口,这是我们自定义的。
//为了更好的满足编译级错误检查,Java给我们提供了这样一个注解:@FunctionalInterface(函数式接口注解)
@FunctionalInterface
interface MyService{
    //其次它必须有且仅有一个抽象方法
    void print(String message);
}
public class TestSAM {
    public static void main(String[] args) {
        MyService service = msg -> System.out.println(msg);
        service.print("Hello SAM");

        //因为就一个参数,所以可隐藏参数以及箭头
        MyService service1 = System.out::println;
        service1.print("Hello SAM 1");
    }
}

为了更好的满足 编译级错误检查,Java给我们提供了这样一个注解:函数式接口注解(@FunctionalInterface)

当我们写的接口不符合函数式接口的定义时,它会报错:

在这里插入图片描述

以上是我们自定义的函数式接口,它非常得简单。

Java8给我们提供了一些设计完备的函数式接口,它们可以更好得支持Lambda表达式。

我们刚刚使用的Consumer就是其中之一,它是消费型接口。

四大函数式接口

  • Consumer 消费型接口

T是接收的参数类型

(只提供处理参数的类型)

支持那种单一参数且无返回值的形式

通过accept()方法调用

Consumer前面代码涉及得比较多,这里不再展示。

  • Function<T,R> 函数型接口

​ 接收T类型的参数,返回R类型的结果

通过apply()方法调用,apply()也叫做应用方法

public class TestSAM {
    public static String subStr(String str, Function<String,String> function){
        return function.apply(str);
    }
    public static void main(String[] args) {
        String result = subStr("月亮不睡你不睡",str -> str.substring(0,4));
        System.out.println(result);//月亮不睡
    }
}
  • Supplier 供应型接口

​ T是返回结果的类型

(只提供返回结果的类型)

​ 通过get()方法调用

public class TestSAM {
    public static String generateUUID(Supplier<String> supplier){
        return supplier.get();
    }
    public static void main(String[] args) {
        String result = generateUUID( () -> UUID.randomUUID().toString());
        System.out.println(result);
    }
}
  • Predicate 断言型接口

​ T是接收参数的类型

​ 返回类型只能是boolean

​ 通过test方法调用

public class TestSAM {
    public static boolean startWith(String str, Predicate<String> predicate){
        return predicate.test(str);
    }
    public static void main(String[] args) {
        boolean result = startWith("LiHua",str -> str.startsWith("Li"));
        System.out.println(result);
    }
}
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值