jdk8的新特性总结(一):lambda表达式

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就是一个函数式接口。我们可以看一下Runnable的源码:

@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></>函数接口有两个泛型, T表示参数类型,R表示返回值类型
     * @param num
     * @param f
     * @return
     */
    public Integer handle(Integer num, Function<Integer, Integer> 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<Double> bo1 = (x, y) -> Math.pow(x,y);
        //等同于
        BinaryOperator<Double> bo2 = Math::pow;

        //例如3:比较两个字符串是否相等
        BiPredicate<String, String> pre1 = (str1, str2) -> str1.equals(str2);
        //等同于
        /**
         * 这种写法需要好好解释一下,当需要引用的方法的第一个参数是调用对象,并且第二个参数是需要引用方法
         * 的第二个参数(或无参或多个参数)时,可以采用ClassName::methodName的写法,这个地方就相当于是
         * "nihao".equals("hellow")。
         * PS:如果有多个参数不知道是否支持这种写法,有兴趣的可以试一下。
         */
        BiPredicate<String, String> pre2 = String::equals;
        pre2.test("nihao", "hellow");
    }

四、构造器引用

构造器引用和方法引用是一个道理,不过右边不需要写方法名,只需要写new就可以了,格式:ClassName::new,接下来我们写几个例子。

    @Test
    public void test3() {
        //例如1
        Function<Integer, Double> fun1 = (i) -> new Double(i);
        //等同于,这时候会调用Double类中有一个参数为Interger的构造器方法
        Function<Integer, Double> fun2 = Double::new;
        //例如2,定义长度为i的数组
        Function<Integer, Integer[]> fun3 = (i) -> new Integer[i];
        //等同于
        Function<Integer, Integer[]> fun4 = Integer[]::new;
    }

总结一下:

  1. lambda表达式左边的括号中可以写入参,多个用逗号隔开,当只有一个参数时,小括号也可以不写,并且可以省略参数类型,JDK会通过上下文进行类型推断,推算出参数类型;
  2. lambda表达式的右边写具体的方法内容,如果只有一行代码,则可以省略{}和return语句;
  3. 函数式接口表示只有一个抽象方法的接口,并且可以用@FunctionalInterface标识;
  4. Supplier<T>表示供给型接口,没有入参,通过返回T类型结果,抽象方法:T get(),当
  5. Comsumer<T>表示消费型接口,有一个T类型的入参,没有返回值,抽象方法:void accpet(T t),当你需要有两个入参时,你可以使用它的子类BiConsumer<T, U>。
  6. Function<T, R>表示函数型接口,有一个T类型的入参,一个R类型的返回值,抽象方法:R apply(T t),同样,当你有两个入参时,你也可以使用它的子类,BiFunction<T, U, R>
  7. Predicate<T>表示判断型接口,有一个T类型的入参,一个boolean类型的返回值,抽象方法:boolean test(T t),它也有多个入参类型的BiPredicate<T, U>
  8. 内置的函数型接口都放在java.util.function包下,里面还有很多不带泛型的接口,当你确定参数或返回值的数据类型时,可以使用这些函数接口。
  9. 当抽象方法的参数列表和引用方法的参数列表参数一致时,可以采用方法引用,方法引用的写法 实例对象::方法名,类::静态方法名。当第一个参数是方法的调用者,第二个参数(或无参)是方法的参数时,可以采用ClassName::methodName形式的写法。
  10. 构造器引用:ClassName::new和 ClassName[]::new。

以上就是lambda表达式的所有内容,下一篇文章我们一起来学习强大的StreamAPI。

猜你喜欢

转载自blog.csdn.net/caishi13202/article/details/82458277