Java8特性——Lambda表达式

  • 引言

    lambda表达式,可以理解为简洁的表示可传递的匿名函数的一种方式,它没有名称,但它有参数列表、函数主体、返回类型。

    下面是一个比较器的传统写法:

//以前定义一个比较器的写法
        Comparator<Apple> comparator = new Comparator<Apple>() {
            @Override
            public int compare(Apple a1, Apple a2) {
                return a1.getWeight() - a2.getWeight();
            }
        };

    使用lambda表达式后:

(a1,a2) -> a1.getWeight() - a2.getWeight();

   对比前后的写法,第一种写法显得十分的繁琐和冗长,第二种写法就显得十分的简洁和清晰。

  • 定义

函数式接口只定义了一个抽象方法的接口,在Java8之前,Java已经有包括Comparator、Runnable、Callable函数式接口,Java8在java.util.function包中提供了如下常用的函数式接口:

函数式接口 函数描述符
Predicate<T> T -> boolean
Consumer<T> T -> void
Function<T,R> T -> R
Supplier<T> void -> T
UnaryOperator<T> (T,T) -> T

函数式描述符就是函数式接口的抽象方法的签名。把lambda函数作为函数式接口的参数传递给一个方法时,必须满足lambda表达式的签名要和函数式接口的抽象方法一样。

方法签名:方法签名由方法名称和一个参数列表(方法的参数的顺序和类型)组成(注意,方法签名不包括方法的返回类型。不包括返回值和访问修饰符。)。函数的重写和重载就是根据方法签名去判断的。

@FunctionalInterface:函数式接口的注解,用于检查接口是否为函数式接口,和override的作用类似的注解。

  • 使用和原理

    一个方法的入参如果定义的是一个函数式接口,就可以使用lambda表达式,其中lambda表达式的描述符或者说方法签名应该和函数式接口的一样,说的更明白点,函数式接口入参定义的是一个入参规范,lambda表达式必须满足这个入参规范,可以用下面的伪代码表示:

    函数式接口 = ()->{}

在定义中罗列出了常用的函数式接口,下面介绍其中的几个函数式接口的原理和应用。

    先来一段用Stream API去操作集合的代码,然后分析其中用的lambda表达式:

    @Test
    public void testStream() {
        List<Dish> menu = Arrays.asList(
                new Dish("pork", false, 800, Type.MEAT),
                new Dish("beef", false, 700, Type.MEAT),
                new Dish("chicken", false, 400, Type.MEAT),
                new Dish("french fries", true, 530, Type.OTHER),
                new Dish("rice", true, 350, Type.OTHER),
                new Dish("season fruit", true, 120, Type.OTHER),
                new Dish("pizza", true, 550, Type.OTHER),
                new Dish("prawns", false, 300, Type.FISH),
                new Dish("salmon", false, 450, Type.FISH)
        );

        menu.stream()
                .filter(dish -> dish.getCalories() > 300)
                .map(dish -> dish.getName()).
                forEach(name ->{
                    System.out.println("name:" + name);
                });

    }

    @Data
    class Dish {
        private String name;
        private boolean vegetarian;
        private int calories;
        private Type type;

        public Dish(String name, boolean vegetarian, int calories, Type type) {
            this.name = name;
            this.vegetarian = vegetarian;
            this.calories = calories;
            this.type = type;
        }
    }

    public enum Type {MEAT, FISH, OTHER}

    输出结果为:

name:pork
name:beef
name:chicken
name:french fries
name:rice
name:pizza
name:salmon

Predicate

    java.util.function.Predicate<T>接口定义了一个名叫test的抽象方法,它能够接受泛型T对象,并返回一个boolean。

    Stream API中对集合操作的filter接受的就是一个Predicate函数式接口:

Stream<T> filter(Predicate<? super T> predicate);

    内部实现类似于:

    public static List<T> filter(List<T> list, Predicate<T> p){
        List<T> results = new ArrayList<>();
        for (T s : list) {
            if(p.test(s)){
                results.add(s);
            }
        }
        return results;
    }

    通过test方法看是否满足条件,然后满足条件的元素放进一个新的容器中,并返回,实现了过滤。

Consumer

    java.util.function.Consumer<T>定义了一个名为accept的抽象方法,它接收泛型T的对象,没有返回。如同名字描述的那样是一种消费,在accept中定义消费动作,即lambda表达式中的表达式主体。

    Stream API中的forEach接受的就是一个Consumer函数式接口:

void forEach(Consumer<? super T> action);

    内部实现类似于:

    public static void forEach(List<T> list, Consumer<T> c){
        for (T s : list) {
            c.accept(s);
        }
    }

Function

    java.util.function.Function<T,R>定义了一个名为apply的抽象方法,它接收泛型T的对象,返回一个R型对象。

    Stream API中的map接受的就是一个Function函数式接口:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

    内部实现类似于:

    public static List<R> forEach(List<T> list, Function<T,R> f){
        List<R> results = new ArrayList<>();
        for (T s : list) {
            results.add(f.apply(s));
        }
        return results;
    }

注意

    lambda表达式除了可以用主体里面的参数以外,也允许使用自由变量(作用域外的变量),就像匿名类一样。也和匿名类一样,对实例变量和静态变量可以没有限制使用,但是局部变量必须显示声明为final。主要原因是两种变量的存放位置的差别,造成线程共享和不共享的问题。

方法引用

    方法引用可以让你重复使用现有的方法定义,并像lambda一样传递他们,例如我们可以使用方法引用筛选出素菜:

 List<Dish> menuNames = menu.stream().
                filter(Dish::isVegetarian).collect(toList());

    不用方法引用我们需要这样写lambda表达式:

        List<Dish> menuNames1 = menu.stream().
                filter(dish -> dish.isVegetarian()).collect(toList());

    我们还可以通过关键字new来创建一个对象:className:new。

        //无参的构造函数
        Supplier<Apple> supplier = Apple::new;
        Apple a1 = supplier.get();

        //一个参数的构造函数
        Function<String,Apple> function = Apple::new;
        Apple a2 = function.apply("my iphone");
        
        //两个参数的构造函数的获取
        BiFunction<String,Integer,Apple> biFunction = Apple::new;
        Apple a3 = biFunction.apply("my ipad",999);

    如果构造函数式三个入参的时候,就需要自己定义一个函数式接口了。

总结

    lambda表达式的引入极大的提高了代码的效率和编程思想向函数式编程的转变,本文只是简要的介绍了lambda表达式使用和简单原理,更详细的解读可以参阅《Java8 in action》。

猜你喜欢

转载自my.oschina.net/u/3470849/blog/1790743