# Java-lambda表达式

标签(空格分隔): Java


lambda表达式是Java8中添加的一个新特性,代替匿名内部类,可以极大的减少代码冗余,提高可读性,是Java的一个语法糖。下面针对lambda表达式,一步步讲述lambda表达式的一般写法,和简洁写法(方法引用),以及Java8提供的函数式接口和Stream,配合lambda表达式写出高效代码。

1. lambda表达式

1.1 lambda表达式语法

1.1.1 一般写法

格式:

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
  statment1;
  statment2;
  //...
  return statmentM;
}

示例:

// lambda表达式一般写法
Comparator<Integer> comp = (Integer i, Integer j) -> {
    return i < j;
});

这是lambda表达式的一般写法,在一些情况下是可以继续简化,简化见下文。

1.1.2 单参数写法

格式:

param1 -> {
  statment1;
  statment2;
  //...
  return statmentM;
}

示例:

ActionListner listener = event -> {
    System.out.println(event);
};

在表达式的参数单个的时候,就可以忽略小括号

一般来讲,参数类型是可以通过返回值推导出来的,故一般省略

Comparator<String> comp = (first, second) -> {
    return first.length() - second.length();
};

在表达式的参数零个的时候,不可以忽略小括号

Runnable msg = () -> {
    for (int i = 100; i >= 0; --i)
        System.out.println(i);
};

1.1.3 单语句写法

格式:

param1 -> statment

示例:

Comparator<String> comp = (first, second) -> first.length() - second.length();

当lambda表达式只包含一条语句时,可以省略花括号,return关键字和结尾分号

1.1.4 lambda表达式的变量

和匿名内部类一样,lambda表达式内部的变量有相应的规则:

如果是在表达式内部创建那么可以随意改动,但是不能和外部有同名的局部变量

lambda表达式可以访问外部变量,但是外部变量必须不可变(final),即表达式内部不能改变它的值,外部也不能改变它的值,必须用final关键字来修饰lambda表达式访问的外部变量,java8对这个限制做了优化,可以不用显示使用final修饰,但是编译器隐式当成final来处理。

void countDown(int start, int delay) {
    ActionListner listener = event -> {
        start--; // compile-error
        System.out.println(start);
    };
}

1.2 lambda表达式的this对象

this关键字指的是表达式所在的那个类的对象,即表达式外部对象,而非表达式本身。

public class Test {

     public void whatThis(){
           List<String> proStrs = Arrays.asList(new String[]{"Ni"});
           List<String> execStrs = proStrs.stream().map(str -> {
                 System.out.println(this.getClass().getName());
                 return str.toLowerCase();
           }).collect(Collectors.toList());
           execStrs.forEach(System.out::println);
     }

     public static void main(String[] args) {
           Test wt = new Test();
           wt.whatThis();
     }
}

// 输出:
// lambda.Test 
// Ni

2. 方法引用与构造器引用

在上述lambda表达式的基础上进一步简化写法

2.1 方法引用

object::instanceMethod
ClassName::staticMethod
ClassName::instanceMethod

前两种方式的写法类似,等同于把lambda表达式的参数当作object.instanceMethod或ClassName.staticMethod的参数:

// lambda表达式一般写法:
Predicate<String> pre = str -> System.out.println(str); // object::instanceMethod
...(x, y) -> Math.max(x, y); // ClassName::staticMethod
// 方法引用写法:
Predicate<String> pre = System.out::println; 
...Math::max;

最后一种的方式,等同于把lambda表达式的第一个参数当作instanceMethod的目标对象,其它剩余参数当成该方法的参数:

// lambda表达式一般写法:
...x -> x.toLowerCase();
// 方法引用写法:
...String::toLowerCas

2.2 构造器引用

ClassName::new
ClassName[]::new

等同于把lambda表达式的参数当成ClassName构造器的参数:

// lambda表达式一般写法:
...x->new BigDecimal(x);
...x->new BigDecimal[x];
// 构造器引用
...BigDecimal::new
...BigDecimal[]::new

3. 函数式接口

lambda表达式的作用在于封装代码块,并可以延迟执行,而lambda表达式使用条件则在于一种接口:函数式接口
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式,这种接口就叫做函数式接口。

Java封装了函数式接口在java.util.Function包中,以下是常用的函数式接口:

函数式接口 参数类型 返回类型 抽象方法名 描述
Runnable void run 作为无参数或返回值的动作进行
Supplier T get 提供一个无参数有返回值T的函数
Consumer T void accept 处理一个参数T无返回值的函数
BiConsumer<T,U> T,U void accept 处理两个参数T、U无返回值的函数
Function<T,R> T R apply 处理一个参数T一个返回值R的函数
BiFunction<T,U,R> T,U R apply 处理两个参数T,U一个返回值R的函数
UnaryOperator T T apply 类型T上的一元操作符
BinaryOperator T,T T apply 类型T上的二元操作符
Predicate T boolean test 布尔值函数
BiPredicate<T,U> T,U boolean test 两个参数的布尔值函数

4. Stream

4.1 Stream概念

Stream和Function一样,都是Java8提供的,配合lambda表达式来简化代码结构。

Stream是元素的集合,这点让Stream看起来用些类似Iterator,但是是装饰后的Iterator,可以手动创建Stream或从集合中生成Stream,可以对Stream进行转换从而生成新的Stream,可以支持顺序和并行的对原Stream进行汇聚的操作。

以上的几个Stream的用法可以用下图来表示:

re-stream.jpg-54.6kB

4.2 Stream的创建

4.2.1 通过其静态方法创建

通过Stream接口的静态工厂方法(Java8里接口可以带静态方法)。

1)of方法

两个重载方法:一个接受变长参数,一个接口单一值

Stream<Integer> integerStream = Stream.of(1, 2, 3, 5);
Stream<String> stringStream = Stream.of("taobao");

2)generator方法

生成一个无限长度的Stream,其元素的生成是通过给定的Supplier(这个接口可以看成一个对象的工厂,每次调用返回一个给定类型的对象)
这个无限长度Stream是懒加载,一般这种无限长度的Stream都会配合Stream的limit()方法来用。

// 内部类写法
Stream.generate(new Supplier<Double>() {
    @Override
    public Double get() {
         return Math.random();
    }
});
// 方法引用写法
Stream.generate(Math::random);

3) iterate方法

生成无限长度的Stream,和generator不同的是,其元素的生成是重复对给定的种子值(seed)调用用户指定函数来生成的。其中包含的元素可以认为是:seed,f(seed),f(f(seed))无限循环。

Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println);

4.2.2 通过Collectin子类获取

Collection接口有一个stream方法,所以其所有子类都都可以获取对应的Stream对象。

public interface Collection<E> extends Iterable<E> {
     default Stream<E> stream() {
          return StreamSupport.stream(spliterator(), false);
     }
}

4.3 Stream的转换

转换Stream其实就是把一个Stream通过某些行为转换成一个新的Stream,下面介绍常用转换的方法:

1)distinct

对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法)

re-distinct.jpg-38.3kB

2)filter

对于Stream中包含的元素使用给定的过滤函数进行过滤操作,新生成的Stream只包含符合条件的元素

re-filter.jpg-25.7kB

3)map

对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。这个方法有三个对于原始类型的变种方法,分别是:mapToInt,mapToLong和mapToDouble。这三个方法也比较好理解,比如mapToInt就是把原始Stream转换成一个新的Stream,这个新生成的Stream中的元素都是int类型。之所以会有这样三个变种方法,可以免除自动装箱/拆箱的额外消耗。

re-map.png-318kB

4)flatMap

原先的Stream中的每个元素都是一个Stream对象,经过转换得到的是每个Stream对象集合在一个Stream对象中,包含了所有元素。

Stream<List<Integer>> inputStream = Stream.of(
 Arrays.asList(1),
 Arrays.asList(2, 3),
 Arrays.asList(4, 5, 6)
 );
Stream<Integer> outputStream = inputStream.
flatMap((childList) -> childList.stream());

flatMap 把inputStream将最底层元素抽出来放到一起,最终output的新Stream都是直接的数字。

5)peek

生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数。

re-peek.jpg-33.1kB

6)limit

对一个Stream进行截断操作获取前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素

re-limit.jpg-29.9kB

7)skip

返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream

re-skip.jpg-26.4kB

关于时间复杂度的讨论:
对于一个Stream进行多次转换操作,每次都对Stream的每个元素进行转换,而且是执行多次,这样时间复杂度就是O(n^2)?其实不是这样的,转换操作都是lazy的,多个转换操作只会在汇聚操作时候融合起来,一次循环完成,是O(n)的。

4.4 Stream的汇聚

汇聚操作接受一个元素序列为输入,反复使用某个合并操作(转换操作是lazy的),把序列中的元素合并成一个汇总的结果。

汇聚操作分成两种:

  • 可变汇聚:把输入的元素们累积到一个可变的容器中,比如累积到Collection或者StringBuilder;
  • 其他汇聚:除可变汇聚外,一般都不是通过反复修改某个可变对象,而是通过把前一次的汇聚结果当成下一次的入参,反复如此。比如reduce,count,allMatch;

4.4.1 可变汇聚

可变汇聚对应的只有一个方法:collect,它可以把Stream中的要有元素收集到一个Collection中,一种方法是直接利用collect方法来进行构造,另外一种是Collectors工具类的toList()方法。

1)collect的函数构造:

<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);

其中三个参数的定义:supplier是一个工厂函数,用来生成一个新的容器;accumulator也是一个函数,用来把Stream中的元素添加到结果容器中;combiner还是一个函数,用来把中间状态的多个结果容器合并成为一个(并发的时候会用到)。

List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
collect(() -> new ArrayList<Integer>(),
(list, item) -> list.add(item),
(list1, list2) -> list1.addAll(list2));

对Lis过滤掉全部的null,然后把剩下的元素收集到一个新的List中。collect的第一个函数生成一个新的ArrayList实例;第二个函数接受两个参数,第一个是前面生成的ArrayList对象,第二个是stream中包含的元素,函数体就是把stream中的元素加入ArrayList对象中;第二个函数被反复调用直到原stream的元素被消费完毕;
第三个函数也是接受两个参数,这两个都是ArrayList类型的,函数体就是把第二个ArrayList全部加入到第一个中;

2)Collections.toList()

List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
collect(Collectors.toList());

4.4.2 其它汇聚

其它汇聚有reducecount等等

1)reduce

Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);

第一个重载方法接受一个BinaryOperator对象,第二个重载方法比第一个多了一个参数,这个参数是设置操作的初始值的。下面分别讲这两个重载方法。

List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println("ints sum is:" + ints.stream().reduce((sum, item) -> sum + item).get());

reduce方法接受一个函数,这个函数有两个参数,第一个参数是上次函数执行的返回值(也称为中间结果),第二个参数是stream中的元素,这个函数把这两个值相加,得到的和会被赋值给下次执行这个函数的第一个参数。

List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println("ints sum is:" + ints.stream().reduce(0, (sum, item) -> sum + item));

第二个重载方法和第一个重载方法不同的是:它允许用户提供一个循环计算的初始值,如果Stream为空,就直接返回该值。而且这个方法不会返回Optional,因为其不会出现null值。

3)其它

  • count: 获取Stream中元素的个数。比较简单,这里就直接给出例子,不做解释了。
  • allMatch:是不是Stream中的所有元素都满足给定的匹配条件
  • anyMatch:Stream中是否存在任何一个元素满足匹配条件
  • findFirst: 返回Stream中的第一个元素,如果Stream为空,返回空Optional
  • noneMatch:是不是Stream中的所有元素都不满足给定的匹配条件
  • max和min:使用给定的比较器(Operator),返回Stream中的最大|最小值

5. 参考资料