java特性回顾——Lambda表达式
目前oracle已经推出了java10.0.1,然而java8的很多优秀特性却没有被有效的利用起来。我也是最近看hibernate最新的文档的时候,看到了Lambda表达式的写法,感觉比较新奇,于是就稍微研究了一下,这里我就把我的研究结果分享给大家。
一、概述
Lambda这个词源于数学上的λ演算,在程序里,指的是匿名函数,也就是没有名称的函数。为了方便理解匿名函数,我举一个JavaScript的例子,因为在js中比较好理解。
//定义一个函数
function test(a){
if(a != null && typeof a === 'function'){
a("call");
}
}
//调用函数
test(function(text){
console.log(text);
});
在调用过程中传入的那个函数就是一个匿名函数。java8开始可以在java代码中实现上面的js代码,如下:
package com.sv.lambda;
import java.util.function.Consumer;
/**
* Lambda测试类
* @author 银发Victorique
* @date 2018年6月13日
*/
public class Main {
/**
* 定义一个函数
*/
public void test(Consumer<String> a) {
a.accept("call");
}
/**
* 调用函数
*/
public static void main(String[] args) {
new Main().test((text) -> {
System.out.println(text);
});
}
}
由于java不是动态类型(弱类型)的编程语言,因此需要定义一个匿名函数类型,上面的例子用的就是Consumer这个接口,传入的泛型是函数参数的类型,需要注意的是只能有一个参数。传入的匿名函数使用格式如下:
(入参1, 入参2,...) ->{ 函数体 }
这时你肯定会有一个疑问,匿名函数可以传很多的参数,但是函数类型却只能传一个参数,这样是不是很不灵活?先别急,我们先来看看几个使用Lambda的例子,再来讨论这个问题。
二、使用Lambda的例子
1. forEach循环
List<String> list = new ArrayList<>();
//传统写法
for(String str : list){
System.out.println(str);
}
//Lambda等价写法
list.forEach((str) -> {
System.out.println(str);
});
2.Runnable接口
//传统写法
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("start thread");
}
}).start();
//Lambda等价写法
new Thread(() -> {
System.out.println("start thread");
}).start();
上面的第一个例子比较好理解,forEach的形参是
Consumer<? super T> action
所以直接传匿名函数就可以,但第二个形参是
Runnable target
那为什么可以直接传匿名函数呢?我们先来看看Runnable接口的源码:
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
再来看一下Consumer的源码:
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
/**
* Returns a composed {@code Consumer} that performs, in sequence, this
* operation followed by the {@code after} operation. If performing either
* operation throws an exception, it is relayed to the caller of the
* composed operation. If performing this operation throws an exception,
* the {@code after} operation will not be performed.
*
* @param after the operation to perform after this operation
* @return a composed {@code Consumer} that performs in sequence this
* operation followed by the {@code after} operation
* @throws NullPointerException if {@code after} is null
*/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
这两个接口都有一个共同特点,就是使用了@FunctionalInterface注解,没错这个注解就是声明接口时匿名函数类,但这个类有一定要求的。首先接口中有且只有一个抽象方法,这样在传入匿名函数时,默认就是这个抽象方法的实现,也就是说入参和返回值和这个抽象方法相同。另外,java8的另外一个特性,就是在接口中可以定义默认方法实现,这也是一个很大的突破,正如Consumer源码中使用default关键字定义的方法,就是默认方法实现,在实现该接口的类中,可以实现或不实现该方法。而这种方法在@FunctionalInterface注解声明的接口中是可以存在的。
因此,可以利用@FunctionalInterface注解,定义自己的匿名函数类,从而突破参数传递数量的限制。
/**
* 匿名函数接口测试(当然可以不使用泛型)
* @author 银发Victorique
* @date 2018年6月13日
* @param <P1> 参数1类型
* @param <P2> 参数2类型
* @param <P3> 参数3类型
* @param <R> 返回类型
*/
@FunctionalInterface
public interface TestInterface<P1, P2, P3, R> {
public R test(P1 x, P2 y, P3 z);
default public void defaultFunction() {
System.out.println("default");
}
}
/**
* 定义一个函数
*/
public String test(TestInterface<String, String, String, String> a) {
return a.test("p1", "p2", "p3");
}
/**
1. 调用函数
*/
public static void main(String[] args) {
System.out.println(new Main().test((p1, p2, p3) -> {
System.out.println(p1+"\t"+p2+"\t"+p3);
return p1+p2+p3;
}));
}
三、关于Lambda的其他说明
- 预设匿名函数类
在java.util.function中有很多预设的匿名函数接口,之前使用的Consumer也在这个包下,一般情况下这些预设接口应该就可以满足要求,如果没有符合的,那就用上面提到的@FunctionalInterface注解进行自定义。 - ::符号
使用这个符号,可以拿到一个实例或类中方法的句柄,上面最后的例子可以改写为:
/**
* 定义一个函数
*/
public String test(TestInterface<String, String, String, String> a) {
return a.test("p1", "p2", "p3");
}
/**
* TestInterface接口方法实现
*/
public String testFunction(String p1, String p2, String p3) {
System.out.println(p1+"\t"+p2+"\t"+p3);
return p1+p2+p3;
}
/**
* 调用函数
*/
public static void main(String[] args) {
Main main = new Main();
System.out.println(main.test(main::testFunction));;
}
四、题外话
以下内容纯属笔者胡言乱语,如有不适,请自行离开
Lambda的这个改动,实现了传递函数的代码语法,从而可以实现类似JavaScript中的闭包。而且在java10中,对于局部变量可以使用var来进行声明。虽然在java中这些都是语法糖,到虚拟机编译时与之前没有什么变化,但是对于编程者来说,使用这种语法糖,可以极大的简化代码,而且在思维上也有了很多扩展。而且这一向JavaScript借鉴和模仿的举动,侧面反映出,JavaScript中的很多特性是指的学习的。个人认为js最大的缺点就是非编译语言,执行效率直接取决于代码编写,但在java中就完全不存在这个问题。
编程也许就是这样,不同的编程语言之间其实,并没有明显的界限。互相可以借鉴优秀的编程思想,即使只是语法糖,但对于开发思想来说也是一个飞跃。更何况,无论使用的是何种编程语言,最后不是都会变成汇编语言,不如说,所有的高级汇编语言都是语法糖。
路漫漫系其修远兮,吾将上下而求索。
端午快到了,正好把屈原的这句话献给程序届以及我们这些程序员,愿程序不断发展,芝麻开花!