不知道面试会不会问 Lambda 怎么用

我们先假设一个场景想象一下,当一个项目出现bug的时候,恰巧这个时候需要你去修改,而当你打开项目之后,眼前的代码让你有一种特别严重的陌生感,你会不会慌?心里是不是瞬间就会喷涌而出各种想法:我这是打开的啥语言的项目?还是我眼花看错了?难道是我过时了?这写的是个啥子玩意儿…

java8在14年就出来了,已经很久了,但是还是有很多人没用过,包括我之前的同事都对这个不太熟悉,原因可能是多样的,可能是老程序员觉得没必要;也可能是性格使然,拒绝接受新的东西,一切守旧,能用就行;也可能是项目太老了,还在用JDK1.7,或者更老的版本,平时根本就接触不到java8的写法,也不需要去接触。

无论是什么原因,在新事物出现之后,没有一股探险精神,不去尝试,不去结合自己的处境去思考,这样下去就算天上掉馅饼也轮不到你啊。

这篇短文说下Lambda表达式,有一定的编程基础的小伙伴简单看下应该就会明白,不仅仅写着舒服,更能提供你的工作效率,让你有更多的时间带薪划水,自我提高,走向人生巅峰。

Lambda表达式

  1. Lambda表达式可以理解为一种匿名函数:没有名称、有参数列表、函数主体、返回类型,可能还会有异常的列表。

    参数 -> 主体

  2. lambda表达式:(parameters) -> expression 或者是 (parameters) -> { statements; }

函数式接口

什么是函数式接口?

仅仅定义了一个抽象方法的接口,类似于 PredicateComparatorRunnable

@FunctionalInterface 函数式接口都带有这个注解,这个注解表示这个接口会被设计为函数式接口。

行为参数化

一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。

函数式接口可以做些什么?

Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并且把整个表达式作为函数式接口的实例,也就是说,Lambda是函数式接口的一个具体实现。函数式接口和Lambda会在项目中写出更加简洁易懂的代码。

接下来我们看下几种函数式接口:

  • java.util.function.Predicate :这个接口中定义了一个test的抽象方法,它接受泛型T对象,并返回一个boolean值,在你需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。

  • java.util.function.Consumer :这个接口中定义了accept抽象方法,它接受泛型T的对象,没有返回。如果你需要访问类型T的对象,并执行某些操作,可以用它。

  • java.util.function.Function :这个接口定义了一个apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象,如果你需要定一个Lambda,将输入对象的信息映射到输出对象,就可以使用这个接口。

  • ps:我们也可以自己定义一个自己需要的函数式接口。

这么说实在是太生涩了,还是贴点代码,让大家都看看:

@FunctionalInterface
public interface Predicate {
    //我只截取了部分代码,test是这个接口唯一的抽象方法,话说从java8开始,接口中不仅   
    //仅只能有抽象方法了,实现的方法也可以存在,用default和static来修饰。
    boolean test(T t);
    default Predicate and(Predicate other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

接下来,看下Lambda和函数式接口是怎么配合,一起快乐的工作的:

首先定义一个方法,这个方法的参数中有函数式接口:

    private static  List filter(List list, Predicate predicate) {
        List result = new ArrayList();
        for (T e : list) {
            if (predicate.test(e)) {
                result.add(e);
            }
        }
        return result;
    }

接下来你就可以这么写:

List apples = filter(list, (Apple apple) -> "red".equals(apple.getColor()));

以上,filter方法的参数是一个泛型集合和Predicate,这个函数式接口中的抽象方法是接受一个对象并返回一个布尔值,所以Lambda我们可以写成参数是一个实体对象Apple,主体是一个返回boolean值的表达式,将这段Lambda作为参数传给filter()方法,这也是java8的行为参数化特性。以上我们就可以挑选出红苹果。

使用了泛型,就代表着我们还可以复用这段代码做些别的事情,挑选出你想要东东的:

List stringList = filter(strList, StringUtils::isNoneBlank);

抽象方法的方法签名和Lambda表达式的签名是一一对应的,如果你要应用不同的Lambda表达式,就需要多个函数式接口,当然了我也是可以自己定义的。

在java中只有引用类型或者是原始类型,这是由泛型内部的实现方式造成的。因此,在Java里有一个将原始类型转换为对应的引用类型的机制,这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱(unboxing)。

Java还有一个自动装箱机制,也就是说装箱和拆箱操作是自动完成的,但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存来搜索获取被包裹的原始值。

针对于这一点,java8中的函数式接口提供了单独的接口,就是为了在输入和输出的时候避免自动装箱拆箱的操作,是不是很贴心。

一般情况下,在名称上我们就能看得出来,一目了然。在原来的名称上会有原始类型前缀。像Function接口针对输出参数类型的变形。比如说:ToIntFunction

、IntToDoubleFunction

等。

在必要的情况下,我们也可以自己定义一个函数式接口,请记住,(T,U) -> R的表达方式展示了对一个函数的简单描述,箭头的的左侧代表了参数类型,右侧代表着返回类型,这里它代表一个函数,具有两个参数,分别为泛型T和U,返回类型为R。

函数式接口是不允许抛出 受检异常(checked exception),但是有两个方法可以抛出异常:

  • 定义一个自己的函数式接口,在唯一的抽象方法抛出异常;

  • 用try-catch 将lambda 包起来。

类型检查

java7是通过泛型从上下文推断类型,lambda的类型检查是通过它的上下文推断出来的。lambda会找到它所在的方法的 方法签名 ,也就是它的参数,也就是他们说的目标类型,再找到这个方法中定义的 抽象方法 ,这个方法描述的函数描述符是什么?也就是这个方法是个什么样的,接受什么参数,返回什么。lambda也必须是符合这样的。当lambda抛出异常的时候,那个抽象方法也必须要抛出异常。

有了目标类型,那么同一个lambda就可以与不同的函数式接口联系起来。只要他们的抽象方法签名是一样的。

例如:

Callable c = () -> 42;
PrivilegedAction p = () -> 42;

这两个接口都是没有参数,且返回一个泛型T的函数。

void兼容规则

lambda的主题是一个语句表达式,和一个返回void的函数描述符兼容,包括参数列表,

比如下面:

// Predicate返回了一个boolean
Predicate p = s -> list.add(s);
// Consumer返回了一个void
Consumer b = s -> list.add(s);

在lambda中使用局部变量

final int local_value = 44;
Consumer stringConsumer = (String s) -> {
            int new_local_value = s.length() + local_value;
        };

在lambda中可以无限制的使用实例变量和静态变量,但是只能是final的,如果在表达式里面给变量赋值,就会编译不通过。为什么会有这样的呢?

因为实例变量存储在堆中,局部变量存储在栈中,lambda是在一个线程中,如果lambda可以直接访问局部变量,lambda的线程可能会在分配该变量的线程将这个变量回收之后,再去访问该变量。在访问局部变量的时候,实际上是访问他的副本,而不是原始变量。

方法引用

方法引用,方法目标实体放在::的前面,方法名放在后面。比如 Apple::getWeight,不需要括号。

构造函数是可以利用它的名称和关键字 new来创建一个引用。

//Supplier也是一个函数式接口,唯一的抽象方法不接受参数,直接返回一个对象
Supplier sup = Apple::new;
        Apple apple = sup.get();

但是如果是有参数的呢?

//一个参数
Function fun = Apple::new;
        Apple apple1 = fun.apply(110L);
//两个参数
 BiFunction biFunction = Apple::new;
 Apple biApple = biFunction.apply(3L, "red");

但是如果有三个参数、四个参数呢?我们上面说了怎么样可以自定义一个自己需要的函数式接口。

@FunctionalInterface
public interface AppleWithParam {
    R apply(T t, U u, V v);
}

总结:

  • java8中自带的函数式接口,以及为了避免拆装箱操作而产生的函数式接口的原始类型转化。

  • 函数式接口就是仅仅定义一个抽象方法的接口。抽象方法的签名(称为函数描述符) 描述了Lambda表达式的签名。

  • 只有在接受函数式接口的地方才可以使用Lambda表达式。

  • 接口现在还可以拥有默认方法,(就是类没有对方法进行实现的时候,它实现的接口来提供默认实现的方法)  

最后

只有主动拥抱变化,才能更快的成长。

如果对本文有任何异议或者说有什么好的建议,可以加我好友(公众号后台联系作者),也可以在下面留言区留言。希望这篇文章能帮助大家披荆斩棘,乘风破浪。

这样的分享我会一直持续,你的关注、转发和好看是对我最大的支持,感谢。