JDK8新特性之Lambda表达式
Lambda表达式
1、为什么要有lambda表达式
面向对象中不好的地方:
1、不得不去创建一个类,来对接口来进行实现;
2、如果不想创建具体的类,那么需要使用匿名内部类;
3、使用内部类不得不去实现其中的抽象方法;
4、重写方法,不得不去书写跟父类一样返回值类型、方法名称和对应的参数列表;
面向对象侧重点:怎么做?
函数式编程侧重点:强调的是做什么?
2、lambda表达式格式
函数式编程在java中又叫lambda表达式
格式:
()->{}
注意:lambda表达式其实是对匿名内部类的一种改写,如果用不了匿名内部类,那么一定用不了lambda表达式;
其中:
():匿名内部类中接口的参数列表;
->:将参数列表交给{}中的java表达式来进行操作;
{}:匿名内部类中重写方法之后要做的操作;
使用lambda表达式的前提:
1、一定是接口;
2、接口中只有一个抽象方法;并不是说接口中只能够有一个方法;
3、函数式接口要作为参数或者是返回值;(99%情况下都是作为参数来进行传递的)
注意:并不是所有的匿名内部类使用的地方都可以改造成lambda表达式来进行操作;
/**
* @Description 数组和集合来进行排序操作
* @Author liguang
* @Date 2022/04/13/23:32
*/
public class ListComparator {
public static void main(String[] args) {
// 来对list集合来进行进行排序
List<Integer> integers = Arrays.asList(12, 3, 6, 11, 999, 666);
// 升序操作:前--->后,升序操作;后--前;降序操作;
Collections.sort(integers,((o1, o2) -> o1-o2));
for (Integer integer : integers) {
System.out.println(integer);
}
System.out.println("使用匿名内部类来进行操作的方式");
Collections.sort(integers, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
for (Integer integer : integers) {
System.out.println(integer);
}
System.out.println("来对数组进行排序操作");
// 这里只能够是引用数据类型
Integer[] intsArr = {12,85,66,77,11,55,66};
Arrays.sort(intsArr,new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2) {
return o1-02;
}
});
for (Integer integer : integers) {
System.out.println(integer);
}
// 使用lambda表达式来进行操作,但是通过实践来进行操作之后,发现这里的匿名内部类无法更改成lambda表达式来进行操作
// Arrays.sort(integers,(o1,o2)->o2-o1);
}
}
3、lambda表达式
案例一
来举一个例子,计算接口Compute:
public interface Compute {
int sum(int i,int j);
}
写一个需要使用到Compute的类:
public class MyMath {
/**
* 计算的一个方法
* @param compute 接口对象
* @param i 需要来进行求和的变量
* @param j 需要来进行求和的变量
*/
public void sum(Compute compute, int i, int j) {
// 这里可以有通用操作,但是目标就是这种的对象来调用这里的方法
int sum = compute.sum(i, j);
// do other something
// 但是关键的一个步骤就是这里的操作
System.out.println(i + "+" +j+ "=" + sum);
}
}
再来一个测试类来进行测试:
public class Test {
public static void main(String[] args) {
MyMath myMath = new MyMath();
// 按照传统的方式的话,那么这里应该是创建实现类来进行实现
myMath.sum(new Compute() {
@Override
public int sum(int i, int j) {
return i+j;
}
}, 250, 520);
}
}
可以看到这里的使用方式,书写了匿名内部类,就是为了做到返回i+j这个效果而已。
下面看下使用lambda表达式的效果:
public class Test {
public static void main(String[] args) {
// 其实可以不要这么进行操作
MyMath myMath1 = new MyMath();
myMath1.sum((i,j)->i+j,200,300);
}
}
这里可以看到只需要实现我们需在在Compute接口中的方法需要做的事情而已,不用去管那些怎么如何去实现的内容而已。
所以这种使用方式对于开发人员来说,更加符合我们的开发习惯。所以在这种使用方式比较多的情况之下,引入出来了对应的lambda表达式。
案例二
那么再来一个案例,如下所示:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("test functional interface");
}
}).start();
通过匿名内部类的方式来进行书写代码,可以看到上面的效果。
那么再看一下,如果通过实现类的方式:
先创建一个实现类:
public class ThreadDemoOne extends Thread{
@Override
public void run() {
System.out.println("test functional interface");
}
}
public class ThreadDemo {
public static void main(String[] args) {
ThreadDemoOne threadDemoOne= new ThreadDemoOne();
new Thread(threadDemoOne).start();
}
}
可以看到上面的两种方式的书写风格。
但是回想一下,我们写这段代码的目的是什么?我们只是想用接口中的一个方法,去做我们想做的事情。但是往往我们想做的事情事与愿违,我们想要一个功能,不得不去做这些事情:实现、继承、创建对象...
这种方式非常繁琐,但是为了去实现,我们又不得不去做这个事情。
但是在JDK8中,官方出版了函数式接口,那么函数式接口大大简化了我们的开发,让我们开发人员再面对这种模式的情况,让我们只关注我们的目标功能,而不是过程。只注重结果,而并不注重过程。那么问题来了,如果接口中有多个方法,开发人员怎么能够知道该去实现哪个方法?所以java规定,接口中只能有一个抽象方法。
注意:只能有一个抽象方法。这段话的描述是接口中可以有其他的非抽象方法。
为了方便这种方法接口,不需要使用匿名内部类,而直接使用lambda来进行实现。
比如说Runnable接口,这就是一个标准的函数式接口:
@FunctionalInterface // 函数式接口
public interface Runnable {
// 里面可以写抽象方法,也可以是Object类中的方法或者是其他的默认方法、静态方法等等。
public abstract void run();
}
紧接着开始写对应的功能代码:
Runnable r = ()->{
System.out.println("hello");
};
new Thread(r).start();
或者说再次简化一下对应的书写方式:
new Thread(()->{
System.out.println("hello");
};).start();
这种书写方式乍一看很不合理,但是写习惯了就好了(苦笑)。
上面使用尖括号将参数和函数体分开,由于Runnable接口中的run方法没有参数,所以括号中什么都没有写;
又因为run方法没有返回值,所以上面的方法体中println方法也是没有返回值的。
所以lambda表达式的参数和返回值和函数式接口中的方法签名是匹配的,这样就硬性规定了labmda表达式使用的时候必须匹配函数式接口中的参数和返回值,这也被称之为和方法签名兼容。因此lambda表达式属于接口方法的实现并将其值赋值给接口类型的引用。
java中并不存在着Lambda的类,lambda表达式只能够作为函数式接口的引用。
通过lambda表达式和匿名内部类的实现可以看到,lambda表达式就相当于是整个匿名内部类,而且在匿名内部类中对于函数式接口中的方法是并不在意的,在意的是传入的参数和经过处理后的结果。也就是输入和输出。也就是说使用lambda表达式的目的是注重功能实现的过程,而不是去做一些与功能实现无关的事情
如果没有输入,那么lambda表达式的参数括号中,即()不需要传入参数;如果没有输出,那么方法体中不需要进行返回,即没有return。
3、自定义函数式接口
首先一定要明确的是:使用lambda表达式的本质就是接口,接口中要做的事情就是lambda表达式要来进行操作的。
所以这里更加应该明确在什么情况下来使用函数式接口来进行编程!!
接口是一个高度抽象的操作!所以会让子类来对其进行实现对应的相对应的操作。
将lambda表达式所有的类型列举出来,然后举例子来进行说明。函数式接口一共有以下几种使用方式:
无参无返回值;
有参无返回值;
无参有返回值;
有参有返回值;
下面分别用这几种方式来实现一下
3.1、无参无返回值
上面的demo引入的就是一个典型的例子:
public class DemoOne {
public static void main(String[] args) {
// 一定要注意:这里是start方法调用了run方法。注意这里的执行顺序
new Thread(new Runnable1()).start(); // 这行代码就相当于是下面的代码,但是这么写的话,还需要写一个类来进行实现,然后new一个对象传入进来。使用下面方式简化
// 首先分析一波:这里是接口,那么利用的肯定是多态机制,传入的最终是对象形式。但是只是使用一次,所以这里利用的是匿名对象
// 对象得去实现接口中的抽象方法,方便进行调用。这里匿名内部类直接进行实现,new了一个匿名对象
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("开启了一条新线程1");
}
}).start();
// 上面的代码在jdk8中又进行优化,可以写成下面这种方式来进行简化。
// 函数式编程: 强调: 做什么
// 只用了一行代码写了上面五行代码做的事情。
// 实行start方法,自动调用了里面的run方法
new Thread(() -> System.out.println("开启了一条新线程2") ).start();
new Thread(() -> System.out.println("开启了一条新线程3")).start();
new Thread(() -> System.out.println("开启了一条新线程4")).start();
new Thread(() -> System.out.println("开启了一条新线程5")).start();
new Thread(() -> System.out.println("开启了一条新线程6")).start();
new Thread(() -> System.out.println("开启了一条新线程7")).start();
}
static class Runnable1 implements Runnable{
// 需要重写的方法是无参无返回值的。所以直接在lambda表达式中写方法体即可
@Override
public void run() {
System.out.println("开启了一条新线程0");
}
}
}
3.2、有参无返回值
// 在这个类中来测试无参无返回值
public class Demo1 {
public static void main(String[] args) {
// 调用invoke方法,最终是要调用里面的show方法。看下show方法是一个有参无返回值的
// invoke方法调用了里面的show方法。show方法需要一个参数,这里传入33。lamba表达式表示了show方法的具体执行方式
// 这里的lambda表达式不是代表MyInterface myInterface,而是里面的方法的具体执行方式。【重点】
invoke(33,(n)->System.out.println("有参无返回值"));
}
// 要进行调用,因为这里接口中的方法需要进行调用,所以需要一个参数,在这里进行调用
public static void invoke(Integer age,MyInterface myInterface) {
myInterface.show(age);
}
}
// 这里加不加@FunctionalInterface都可以
interface MyInterface {
// 有参无返回值。所以在实现的时候,只需要对参数来进行消费即可。
void show(Integer integer);
}
以函数式接口作为方法的参数来进行使用,这种才是最常用的。
至于使用者如何来对方法进行实现,那么属于使用者来进行自定义的方式来进行操作的了。使用如上所示,我这里的实现仅仅是将传递过来的数据进行输出到控制台而已。
那么也就是说这里的使用,1、首先需要定义一个函数式接口;2、然后将其作为方法参数传递;3、在进行调用的时候传递参数和对函数式接口抽象方法的实现即可。
3.3、无参有返回值
public class Demo2 {
public static void main(String[] args) {
// print方法来进行调用,一看参数是一个接口,应该是匿名对象调用了fun方法,print方法在进行执行的时候,传入了匿名对象,然后匿名对象调用了fun方法
// fun方法如何进行实现?lambda来进行实现的。将其进行还原
/**
* new MyInterface(){
* @Override
* public String fun() {
* System.out.println("开启了一条新线程0");
* }
* }
*/
// 有一个返回值,需要来进行接收。
print(()->"hello,无参有返回值");
}
public static void print(MyInterface lambda) {
// lambda来实现里面具体的逻辑,节省代码
String s = lambda.fun();
}
static interface MyInterface {
// 无参有返回值,那么说明了需要有返回值需要接收。在lambda表达式中可以直接进行返回接收即可。
String fun();
}
}
3.4、有参有有返回值
public class Demo3 {
public static void main(String[] args) {
// 调用的时候,传递参数,然后匿名对象调用show方法,lambda中有具体的实现方式。
fun("hello,有参有返回值",(n)->n);
}
public static String fun(String str,MyInterface myInterface){
String show = myInterface.show(str);
System.out.println(show);
return show;
}
static interface MyInterface{
// 有参有返回值,那么()需要写,{}中也需要写
String show(String s);
}
}
4、看一下JDK中的使用方式
最常见的在这里举例子说明,比如说Optional类:
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
看一下这里的方法操作,传入进来的consumer就是一个匿名内部类,在接口Consumer中的accept中的方法就是我们需要来进行的操作
那么看一下接口中的方法是否符合我们的操作方法,可以看到接口中只有一个抽象方法,没有返回值
void accept(T t);
所以我们可以模仿这种使用方式来进行操作。也就是上面的有参无返回值的情况。(可以看到至少需要三个类来进行操作)
但是这种使用方式已经将接口和实际调用者进行分离开来。具体的实现只需要根据自己的需要来进行实现即可
过滤方式:
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
// 如果符合过滤条件,那么就返回本身
return predicate.test(value) ? this : empty();
}
转换方式:
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
使用Function函数将T转换成U,转换方式,也就是对应转换功能函数,可以由自己来进行定义。
5、使用方法引用
方法引用的前提:
方法引用是对lambda表达式更深层的一种抽象使用,对于方法引用来说,lambda表达式所表达的操作也是有点冗余的。
所以方法引用是对lambda的进一步简化!所以要想写出来对应的方法引用,那么首先确定的是当前的操作中可以写出来对应的方法引用
同样的道理:
不是所有的lambda表达式都可以改造成方法引用
那么如何确定什么时候可以使用方法引用?
在lambda表达式中的操作是已经定义好的
在之前的操作中:
stream.forEach(s->System.out.println(s))
这里指定的是在lambda中,对应的println方法已经定义好了。那么这个时候就可以来对其进行操作:
stream.forEach(System.out::println);
方法引用格式:
A::B
:: 引用运算符
B:是一个功能,不能肯定的说是一个方法;比如说:new操作
那么A::B的语义就是使用A中的B功能;
那么再看一下:
stream.forEach(System.out::println);
使用System.out::println 功能
再次来总结一下:这里的方法一定是要定义好的,能够使用的,才可以这么来进行使用。
使用方式通常有:
1、对象::成员方法;
2、类名::静态方法;
3、类名::成员方法 (这种应该是最常用的一种方式)
6、总结
0、jdk8中的接口
1、lambda表达式产生原因
2、当一个方法的参数是函数式接口的时候,我们更加在意的是函数式接口中的入参和返回值类型,而不是过多的在于方法名字;
3、使用函数式接口的时候,除了上面的入参和返回值之外,更加注重的是方法体的操作;