# 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的用法可以用下图来表示:
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方法)
2)filter
对于Stream中包含的元素使用给定的过滤函数进行过滤操作,新生成的Stream只包含符合条件的元素
3)map
对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。这个方法有三个对于原始类型的变种方法,分别是:mapToInt,mapToLong和mapToDouble。这三个方法也比较好理解,比如mapToInt就是把原始Stream转换成一个新的Stream,这个新生成的Stream中的元素都是int类型。之所以会有这样三个变种方法,可以免除自动装箱/拆箱的额外消耗。
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每个元素被消费的时候都会执行给定的消费函数。
6)limit
对一个Stream进行截断操作获取前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素
7)skip
返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream
关于时间复杂度的讨论:
对于一个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 其它汇聚
其它汇聚有reduce
、count
等等
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. 参考资料
- Cay S. Horstmann.JAVA核心技术(卷1)[M]. 机械工业出版社, 2008.
- Java8初体验(一)lambda表达式语法