美文网首页
JDK1.8新特性(一):Lambda表达式

JDK1.8新特性(一):Lambda表达式

作者: CoderLucas | 来源:发表于2020-08-09 21:24 被阅读0次

    JDK1.8系列文章

    Lambda 表达式

    Java 8 是 Java 语言开发的一个主要版本,Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ,现在已经发布了 Java 11 和 Java 13。但 Java 8 还是目前使用最多的版本,最主要的原因是能在我们编程过程中带来很多便利,特别是 Lambda 表达式和 Stream 的支持,使得程序设计更加简洁,代码量更少,能把二三十行的代码,简化到十行以内,你敢信?快用上这些新特性来试试吧。
    看到 GitHub 上面有个关于 Java 8 的英文仓库, 新特性的内容比较全,地址:https://github.com/winterbe/java8-tutorial ,有能力的朋友可以阅读。我以这个仓库作为参考,写了下面这一系列文章。

    一、新特性

    在 Java 8 新特性系列文章的第一篇中,稍微先介绍一下新特性有哪些:

    • 接口的静态方法和默认方法
    • Lambda 表达式
    • 函数式接口
    • 方法和构造函数引用
    • Optional 类
    • Streams (流)
    • Maps
    • 新的时间日期 API
    • Annotations (注解)

    在本文中,将详细介绍前4点新特性(接口的静态方法和默认方法、Lambda 表达式、函数式接口、方法和构造函数引用),其他的新特性在接下来的几篇文章会介绍到。

    二、接口的默认方法和静态方法

    1、默认方法

    Java 8使我们能够通过使用 default 关键字向接口添加非抽象方法实现。

    interface Formula {
        double calculate(int a);
    
        default double sqrt(int a) {
            return Math.sqrt(a);
        }
    }
    

    Formula 接口中除了定义抽象方法 calculate,还定义了默认方法 sqrt。抽象方法需要先实现,然后才能够进行使用,默认方法 sqrt 可以直接使用,我们通过代码来演示一下。

    Formula formula = new Formula() {
        @Override
        public double calculate(int a) {
            return sqrt(a * 100);
        }
    };
    
    formula.calculate(100);     // 100.0
    formula.sqrt(16);           // 4.0
    

    通过增加 default 关键字,使接口可以有实现方法,而且不需要实现类去实现这个方法。思考一下为什么 Java 8 中要新增这个特性?

    首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的 Java 8 之前的集合框架没有 foreach 方法,通常能想到的解决办法是在 JDK 里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法,目的是为了解决接口的修改与现有的实现不兼容的问题。

    2、静态方法

    Java 8 中接口除了可以使用默认方法,也可以使用静态方法。

    public interface Formula {
        public static void print(){
            System.out.println("接口中的静态方法");
        }
    }
    

    三、Lambda 表达式

    Lambda 表达式是推动 Java 8 发布的最重要特性,允许把函数作为一个方法的参数,传递到方法中,使用 Lambda 表达式可以使代码变得更加简洁紧凑,接下来我们就来了解一下什么是 Lambda 表达式。

    1、使用 Lambda 表达式

    Java 8 中引入了一个新的操作符 -> ,该操作符被称为 Lambda 操作符,Lambda 操作符把 Lambda 表达式拆分成两部分,左侧为 Lambda 表达式的参数列表,右侧为 Lambda 表达式中所需的函数方法。
    让我们从一个简单的例子开始,看看在以前的版本的 Java 中如何排序的字符串列表:

    List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
    
    Collections.sort(names, new Comparator<String>() {
        @Override
        public int compare(String a, String b) {
            return b.compareTo(a);
        }
    });
    

    给静态方法 Collections.sort 传入一个 List 对象和一个比较器,来对给定列表中的元素进行排序。通常做法都是创建一个匿名的比较器对象然后将其传递给 sort 方法。Java 8 提供了更简洁的语法,Lambda 表达式,不需要再创建匿名对象:

    Collections.sort(names, (String a, String b) -> {
        return b.compareTo(a);
    });
    

    可以看出,代码变得更短更具简洁,但实际上还可以写得更短:

    Collections.sort(names, (String a, String b) -> b.compareTo(a));
    

    可以去掉大括号 {} 以及 return 关键字,可以写得更短点:

    names.sort((a, b) -> b.compareTo(a));
    

    2、Lambda 表达式中的作用域

    Lambda 表达式只能引用标记为 final 的外层局部变量,不能在 Lambda 内部修改定义在作用域外的局部变量。

    • 访问局部变量

    可以直接在 Lambda 表达式中访问外部的局部变量:

    final int num = 1;
    Converter<Integer, String> stringConverter =
            (from) -> String.valueOf(from + num);
    
    stringConverter.convert(2);     // 3
    

    Lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义),该代码同样正确:

    int num = 1;
    Converter<Integer, String> stringConverter =
            (from) -> String.valueOf(from + num);
    
    stringConverter.convert(2);     // 3
    

    在Lambda表达式中试图修改num同样是不允许的,例如下面的就无法编译:

    int num = 1;
    Converter<Integer, String> stringConverter =
            (from) -> String.valueOf(from + num);
    num = 4;
    
    • 访问字段和静态变量

    与局部变量相比,对Lambda表达式中的实例字段和静态变量都有读写访问权限。

    class Lambda4 {
        static int outerStaticNum;
        int outerNum;
    
        void testScopes() {
            Converter<Integer, String> stringConverter1 = (from) -> {
                outerNum = 23;
                return String.valueOf(from);
            };
    
            Converter<Integer, String> stringConverter2 = (from) -> {
                outerStaticNum = 72;
                return String.valueOf(from);
            };
        }
    }
    

    四、函数式接口

    Java 语言设计者增加了函数式接口的概念,来使现有的函数友好地支持 Lambda。

    1、概念

    函数式接口(Functional Interface)在 Java 中是指,有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,适用于函数式编程场景。需要确保接口中只有一个抽象方法,Java 中的 Lambda 才能顺利地进行推导。

    在 Java 8 之前,java.lang.Runnablejava.util.concurrent.Callable 是函数式接口最典型的两个例子。Java 8 增加了一种特殊的注解 @FunctionalInterface ** ,但是这个注解不是必须的(建议使用),只要接口只包含一个抽象方法,虚拟机会自动判断该接口为函数式接口。一般建议在接口上使用 @FunctionalInterface** ** ** 注解进行声明,这样的话,编译器如果发现你标注了这个注解的接口有多于一个抽象方法的时候会报错的

    如何定义一个函数式接口:

    @FunctionalInterface
    interface GreetingService 
    {
        void sayMessage(String message);
    }
    

    然后使用 Lambda 表达式来表示该接口的一个实现:

    GreetingService greetService1 = message -> System.out.println("Hello " + message);
    

    2、函数接口

    JDK 1.8 API包含许多内置的函数接口。其中一些在旧版本的Java中比较常用,比如 Comparator 或 Runnable。对这些现有接口进行扩展,以通过 @FunctionalInterface 注释对 Lambda 的进行支持。

    JDK 1.8 新增加的函数接口:

    • java.util.function

    JDK 1.8 之前已有的函数式接口:

    • java.lang.Runnable
    • java.util.concurrent.Callable
    • java.security.PrivilegedAction
    • java.util.Comparator
    • java.io.FileFilter
    • java.nio.file.PathMatcher
    • java.lang.reflect.InvocationHandler
    • java.beans.PropertyChangeListener
    • java.awt.event.ActionListener
    • javax.swing.event.ChangeListener

    3、函数接口实例

    看到这里相信大家已经基本了解什么是函数式接口,如果想对函数式接口有更深入的了解,请继续看下面的内容,将为大家详细介绍常用的函数式接口如何使用。
    在新增加的函数接口在 java.util.function包下,它包含了很多类,用来支持 Java 的函数式编程,那么让我们来看一看哪些是比较常用的:

    java.util.function.Predicate
    java.util.function.Consumer
    java.util.function.Function
    java.util.function.Supplier
    

    Predicate

    java.util.function.Predicate 接口定义了一个名叫 test 的抽象方法,它接受泛型 T 对象,并返回一个 boolean 值。在对类型 T 进行断言判断时,可以使用这个接口。
    使用 Predicate 实现字符串判空操作,测试代码

    // 实现 Predicate test 方法,进行判断传入的字符串是否为空
    Predicate<String> predicate = new Predicate<String>() {
        @Override
        public boolean test(String s) {
            return s.isEmpty() || s.trim().isEmpty();
        }
    };
    // 测试传入的字符串是否为空
    System.out.println(predicate.test(""));
    System.out.println(predicate.test("  "));
    System.out.println(predicate.test("admin"));
    

    测试结果

    true
    true
    false
    

    Consumer

    java.util.function.Consumer 接口定义了一个名叫 accept 的抽象方法,接受泛型 T,没有返回值。如果需要访问类型 T 的对象,并对其执行某些操作,可以使用这个接口,通常称为消费性接口。

    使用 Consumer 实现集合遍历操作,测试代码

    // 实现 Consumer accept 方法,进行集合的遍历并打印
    Consumer<Collection> consumer = new Consumer<Collection>() {
        @Override
    public void accept(Collection collection) {
            if (null != collection && collection.size() > 0) {
                for (Object c : collection) {
                    System.out.println(c);
                }
            }
        }
    };
    
    // 在集合中添加元素
    List<String> list = new ArrayList<>();
    list.add("杭州");
    list.add("北京");
    list.add("上海");
    
    // 遍历 list 输出元素内容到控制台
    consumer.accept(list);
    

    测试结果

    杭州
    北京
    上海
    

    Function

    java.util.function.Function<T, R> 接口定义了一个叫做 apply 的方法,接受一个泛型 T 的对象,并返回一个泛型 R 的对象。如果需要定义一个 Lambda,将输入的信息映射到输出,可以使用这个接口,通常称为功能性接口。

    使用 Function 实现用户密码 Base64 加密操作,测试代码

    // 实现 Function apply 方法,进行用户密码 Base64 加密操作
    Function<String, String> function = new Function<String, String>() {
        @Override
        public String apply(String s) {
            return Base64.getEncoder().encodeToString(s.getBytes());
        }
    };
    // 输出加密后的字符串
    System.out.println(function.apply("123456"));
    

    测试结果

    MTIzNDU2
    

    Supplier

    java.util.function.Supplier 接口定义了一个 get 的抽象方法,它没有参数,返回一个泛型 T 的对象,这类似于一个工厂方法。

    使用 Supplier 实现返回字符串,测试代码

    // 实现 Supplier get 方法,进行字符串返回
    Supplier<String> supplier = new Supplier<String>() {
        @Override
        public String get() {
            return "get!";
        }
    };
    // 输出返回的字符串
    System.out.println(supplier.get());
    

    测试结果

    get!
    

    五、方法和构造函数引用

    Java 8 允许通过::关键字传递方法或构造函数的引用。
    下面,我们在 Car 类中定义了 4 个方法作为例子来区分 Java 中 4 种不同方法的引用。

    public class Car {
        public static Car create(final Supplier<Car> supplier) {
            return supplier.get();
        }
    
        public static void collide(final Car car) {
            System.out.println("Collided " + car.toString());
        }
    
        public void follow(final Car another) {
            System.out.println("Following the " + another.toString());
        }
    
        public void repair() {
            System.out.println("Repaired " + this.toString());
        }
    }
    

    1、构造器引用

    它的语法是Class::new,或者更一般的Class< T >::new实例如下:

    final Car car = Car.create( Car::new );
    final List< Car > cars = Arrays.asList( car );
    

    2、静态方法引用

    它的语法是 Class::static_method,实例如下:

    cars.forEach( Car::collide );
    

    3、特定类的任意对象的方法引用

    它的语法是 Class::method 实例如下:

    cars.forEach( Car::repair );
    

    4、特定对象的方法引用

    它的语法是 instance::method 实例如下:

    final Car police = Car.create( Car::new );
    cars.forEach( police::follow );
    

    参考网站:

    六、公众号

    如果大家想要第一时间看到我更新的 Java 方向学习文章,可以关注一下公众号【Lucas的咖啡店】。所有学习文章公众号首发,请各位大大扫码关注一下哦!

    Java 8 新特性学习视频请关注公众号,私信【Java8】即可免费无套路获取学习视频。

    相关文章

      网友评论

          本文标题:JDK1.8新特性(一):Lambda表达式

          本文链接:https://www.haomeiwen.com/subject/dlerdktx.html