jdk8的lambda表达式和StreamAPI能够简化以前我们需要重复编写的代码,以前一直都是用jdk6,最近一个新项目开始使用jdk8,经过一段时间的使用,着实感受到了jdk8的强大便捷,本文对jdk8的新特性做了一个总结,希望可以帮助大家快速的了解并上手jdk8。
一、lambda表达式
lambda表达式让我们可以将方法体作为参数进行传递,最常见的是匿名内部类的实现,以前我们需要new一个对象,然后实现抽象方法,就像下面这样:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("传统new线程的方式");
}
});
复制代码
而我们用lambda表达式只需要一行代码,将run方法的方法体传递给Thread即可:
Thread thread = new Thread(() -> System.out.println("hello lambda"));
复制代码
这里有一个疑问,这种写法JDK是如何知道调用Thead的哪个构造器的呢?
首先lambda表达式传递是一行代码,jdk会找一个只有一个参数,并且这个参数为函数式接口的构造方法。来解释一下什么叫函数式接口。只有一个抽象方法的接口就叫做函数式接口,函数式接口可以用@FunctionalInterface来标识。 而Runnable就是一个函数式接口。我们可以看一下Runable的源码:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
复制代码
总结:lambda表达式就是括号 + 箭头 + 方法体。如果方法体只有一行代码,则可以省略大括号,如果有返回值还可以省略return。lambda常见的格式:
- (参数1, 参数2) -> 表达式
- (参数1, 参数2) -> { 一系列表达式 }
- 参数 -> 表达式
二、函数式接口
接下来我们来描述一个业务场景,通过这个业务场景来继续了解一下函数式接口,假设有一个需求,传入一个值,经过一系列计算之后返回一个值,这一系列计算是未知的,需要具体调用者去实现,代码如下:
public class MyTest {
@Test
public void test(){
//这里为了演示就直接定义内部类。
int result1 = handle(10, new HandleInterface() {
@Override
public Integer handle(Integer i) {
//...假装有一系列业务操作
return i * 100;
}
});
System.out.println(result1);//输出1000
int result2 = handle(10, new HandleInterface() {
@Override
public Integer handle(Integer i) {
//...假装有一系列业务操作
return i + 100;
}
});
System.out.println(result2); //输出110
//如同上面一样,每一种业务操作,我们都会创建一个HandleInterface的实现类来实现具体的业务逻辑
}
/**
* 处理方法,接受被处理的数字和HandleInterface的实现方法(具体业务逻辑)
* @param num
* @param h
* @return
*/
public int handle(Integer num, HandleInterface h){
return h.handle(num);
}
/**
* 一般从架构的考虑,我们都会定义一个抽象类,然后每一种计算(也可以说业务场景)我们都会定义一个类
* 来实现它。
* 因为这个类只有一个抽象方法,所以我们也可以加上@Functionalinterface注解标识此接口为函数式接口
*/
@FunctionalInterface
interface HandleInterface {
public Integer handle(Integer i);
}
}
复制代码
而在jdk8中,已经在内部给我们定义好了许多的函数式接口,常用的有Suppiler(供给型),Comsumer(消费型),Function(函数型),Predicate(判断型)。这些接口都是带泛型的,有了这些内置的函数接口,我们就不需要自己定义接口了,接下来我们就用内置的函数式接口实现上线的功能:
public class MyTest2 {
@Test
public void test(){
int result1 = handle(10, (e) -> e * 100);
System.out.println(result1); //输出1000
int result2 = handle(10, (e) -> e + 100);
System.out.println(result2); //输出110
}
/**
* Function>函数接口有两个泛型, T表示参数类型,R表示返回值类型
* @param num
* @param f
* @return
*/
public Integer handle(Integer num, Function f) {
return f.apply(num);
}
}
复制代码
三、方法引用
首先我们学习一下方法引用,我们来思考一个问题,在lambda表达式中传递的是一段方法体,每次我都要去写这一段方法体,但是如果我要进行的操作已经有现成的方法了,那么能不能直接将这个方法传给lambda表达式呢?答案是可以的,这就是方法引用,方法引用使用双冒号“::”作为操作符,主要有以下两种方式:
实例对象::实例方法名 (因为右边是实例方法,所以左边的实例对象必须是一个已经被new出来的对象) 类::静态方法 名 (因为右边是静态方法,所以左边的实例对象写类名即可)
需要注意的是,实现抽象方法的参数列表,必须与方法引用的方法参数列表保持一致。接下来我们来写一些方法引用的例子:
@Test
public void test2() {
//例如1:
Thread thead1 = new Thread(() -> System.out.println("hellow lambda"));
//等同于,这里的System.out返回的是一个PrintStream对象,也就是 实例对象::实例方法名 的写法
Thread thead2 = new Thread(System.out::toString);
//例如2:这里的BinaryOperator是一个二元操作接口函数
BinaryOperator bo1 = (x, y) -> Math.pow(x,y);
//等同于
BinaryOperator bo2 = Math::pow;
//例如3:比较两个字符串是否相等
BiPredicate pre1 = (str1, str2) -> str1.equals(str2);
//等同于
/**
* 这种写法需要好好解释一下,当需要引用的方法的第一个参数是调用对象,并且第二个参数是需要引用方法
* 的第二个参数(或无参或多个参数)时,可以采用ClassName::methodName的写法,这个地方就相当于是
* "nihao".equals("hellow")。
* PS:如果有多个参数不知道是否支持这种写法,有兴趣的可以试一下。
*/
BiPredicate pre2 = String::equals;
pre2.test("nihao", "hellow");
}
复制代码
四、构造器引用
构造器引用和方法引用是一个道理,不过右边不需要写方法名,只需要写new就可以了,格式:ClassName::new,接下来我们写几个例子。
@Test
public void test3() {
//例如1
Function fun1 = (i) -> new Double(i);
//等同于,这时候会调用Double类中有一个参数为Interger的构造器方法
Function fun2 = Double::new;
//例如2,定义长度为i的数组
Function fun3 = (i) -> new Integer[i];
//等同于
Function fun4 = Integer[]::new;
}
复制代码
总结:
- lambda表达式左边的括号中可以写入参,多个用逗号隔开,当只有一个参数时,小括号也可以不写,并且可以省略参数类型,JDK会通过上下文进行类型推断,推算出参数类型;
- lambda表达式的右边写具体的方法内容,如果只有一行代码,则可以省略{}和return语句;
- 函数式接口表示只有一个抽象方法的接口,并且可以用@FunctionalInterface标识;
- Supplier表示供给型接口,没有入参,通过返回T类型结果,抽象方法:T get(),当
- Comsumer表示消费型接口,有一个T类型的入参,没有返回值,抽象方法:void accpet(T t),当你需要有两个入参时,你可以使用它的子类BiConsumer
。 - Function
表示函数型接口,有一个T类型的入参,一个R类型的返回值,抽象方法:R apply(T t),同样,当你有两个入参时,你也可以使用它的子类,BiFunction - Predicate表示判断型接口,有一个T类型的入参,一个boolean类型的返回值,抽象方法:boolean test(T t),它也有多个入参类型的BiPredicate
- 内置的函数型接口都放在java.util.function包下,里面还有很多不带泛型的接口,当你确定参数或返回值的数据类型时,可以使用这些函数接口。
- 当抽象方法的参数列表和引用方法的参数列表参数一致时,可以采用方法引用,方法引用的写法 实例对象::方法名,类::静态方法名。当第一个参数是方法的调用者,第二个参数(或无参)是方法的参数时,可以采用ClassName::methodName形式的写法。
- 构造器引用:ClassName::new和 ClassName[]::new。 以上就是lambda表达式的所有内容,下一篇文章我们一起来学习强大的StreamAPI。