2014年3月18日Oracle发布Java SE 8,详解Java 8中的Lambda表达式

本文将会重点讲解Java 8中的Lambda表达式,后续文章会讲解其他特性。它又被称作“闭包”,也被称作“匿名方法”。
背景
Java是一门面向对象的编程语言,面向对象编程语言和函数式编程语言中的基本元素都能够动态封装程序行为,面向对象编程语言借助带有方法的对象来封装行为,函数式编程语言运用函数来封装行为 。但这个相同点不太显著,因为Java对象通常较为“重量级”,实例化一个类型常常会涉及不同的类,还需要初始化类里的字段和方法。
不过有些Java对象只是对单个函数进行封装。例如下面这个典型用例:Java API中定义了一个接口,这个接口一般被称为回调接口,用户通过提供该接口的实例来传入指定行为。
public interface ActionListener {
void actionPerformed(ActionEvent e);
}
这里不需要专门定义一个类来实现ActionListener,它仅在调用处被使用一回,用户通常会运用匿名类型将行为内联:
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
ui.dazzle(e.getModifiers());
}
});
很多库都依赖于上述模式,并行 API 更是这样,因为我们要将待执行的代码提供给并行 API,并行编程是个很值得研究的领域,这是由于摩尔定律在其中得以重生,即尽管我们没有更快的 CPU 核心,但我们有更多的 CPU 核心,而串行 API 只能利用有限的计算能力。
匿名内部类
随着回调模式以及函数式编程风格越来越流行,我们要在Java里提供一种尽量轻量级的把代码封装成数据的办法。匿名内部类不是个好选择,原因如下:
函数式接口
尽管匿名内部类存在着种种限制与问题,然而它具备一个良好的特性,它与Java类型系统结合得极为紧密,每一个函数对象都对应着一个接口类型。之所以认为这个特性是良好的,原因在于:
接口仅有一个方法,多数回调接口都具备这一特性,像Runnable接口和Comparator接口。我们将这些仅有一个方法的接口称作函数式接口。(此前它们被叫做SAM类型,也就是单抽象方法类型(Single Abstract Method))
我们不需要额外工作来声明一个接口是函数式接口,编译器会根据接口结构自行判断,判断过程并非简单对接口方法计数,一个接口可能冗余定义了Object已提供的方法,比如toString(),或者定义了静态方法或默认方法,这些都不属于函数式接口方法的范畴。API作者们可以通过@FunctionalInterface注解显式指定一个接口为函数式接口,这样做是为了避免无意声明符合函数式标准的接口,加上这个注解后,编译器会验证该接口是否满足函数式接口的要求。
实现函数式类型还有一种方式,那就是引入一种全新的结构化函数类型,我们也把这种类型称作“箭头”类型。比如说,有一个函数类型,它接收String和Object,然后返回int,这种函数类型可以表示为(String, Object) -> int。我们对这个方式进行了仔细思考,不过由于下面这些原因,最后把它否定了:
所以我们选择了“使用已知类型”这条路,因为现有的类库大量运用了函数式接口,通过沿用这种模式,我们能让现有类库直接使用lambda表达式,例如下面是Java SE 7中已存在的函数式接口:
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.beans.PropertyChangeListener
除此之外,Java SE 8增添了一个新的包,即java.util.function,该包里面含有常用的函数式接口,比如:
Predicate<T>——接收 T 并返回 boolean
Consumer<T>——接收 T,不返回值
Function<T, R>——接收 T,返回 R
Supplier<T>——提供 T 对象(例如工厂),不接收值
UnaryOperator<T>——接收 T 对象,返回 T
BinaryOperator<T>——接收两个 T,返回 T
除了上述这些基本的函数式接口之外,我们另外提供了一些针对原始类型的特化函数式接口,比如 IntSupplier 和 LongBinaryOperator 。我们只为int、long和double提供了特化函数式接口,若需要使用其他原始类型,就需要进行类型转换,同样的,我们也提供了一些针对多个参数的函数式接口,比如BiFunction,它接收T对象和U对象,返回R对象。
lambda表达式
匿名类型存在着一个极大的问题,那就是其语法存在冗余。有人开玩笑说匿名类型引发了“高度问题”。lambda表达式属于匿名方法,它具备轻量级的语法,借此解决了匿名内部类所带来的“高度问题”。
(int x, int y) -> x + y
() -> 42
(String s) -> { System.out.println(s); }
第一个lambda表达式接收两个整形参数x和y,返回它们的和,第二个lambda表达式不接收参数,返回整数‘42’,第三个lambda表达式接收一个字符串,把它打印到控制台,不返回值。
lambda表达式的语法包含参数列表,有箭头符号->,还有函数体。函数体既能够是一个表达式,也能够是一个语句块:
表达式函数体适用于小型lambda表达式,它去除了return关键字,这让语法变得更加简洁。
lambda表达式会时常出现在嵌套环境里,例如作为方法的参数。为让lambda表达式在这些情形下尽量简洁,我们去掉了不必要的分隔符。不过在某些状况下我们也能够将它分成多行,接着用括号括起来,如同其他普通表达式一样。
实战应用 Function
Function接口有一个参数,Function接口返回一个结果,Function接口附带了一些默认方法,这些默认方法可以和其他函数组合,这些默认方法是andThen和compose。
@Test
public void testFun() {
//Function 接口有一个参数并且返回一个结果
Function<String, Integer> toInteger = (t) -> Integer.valueOf(t);
System.out.println("compose: " + toInteger.andThen(a -> a + 10).compose(str -> str + "1").apply("123"));
Function<String, String> backToString = toInteger.andThen(String::valueOf);
Function<String, Integer> f = toInteger.compose(backToString);
int str = f.apply("123");
System.out.println(str);
}
compose和andThen中定义的Function应用顺序相反,compose中的方法会首先被应用,当前Function随后才会被应用。
Supplier
Supplier接口返回一个值,这个值具有任意范型 ,与Function接口不一样 ,该接口没有任何参数 。代码如下:
@Test
public void testSupplier() {
Supplier接口返回一个值,这个值具有任意范型,与Function接口不同,该接口没有任何参数。
Supplier sp = () -> "sp";
System.out.println(sp.get());
}
上述代码会返回一个字符串“sp”,利用get方法来获取返回的值。
Predicate
Predicate接口有一个参数,返回类型为boolean。该接口包含多种默认方法,可将Predicate组合成其他复杂逻辑,比如与、或、非:
@Test
public void testPredicate() {
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
isEmpty.and(str -> str.equals("test"));
System.out.println("tes: " + isEmpty.and(str -> str.equals("test")).test("tes"));
}
如上代码判断了字符串是否为空,并应用了与、非操作。
Consumer
Consumer接口用于表示一种操作,该操作执行在单个参数上,其主要的方法有andThen和accept 。
@Test
public void testConsumer() {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
Consumer<String> greeter = (p) -> System.out.println("Hello, " + p);
greeter.andThen((t) -> System.out.println("now is :" + df.format(new Date()))).accept("Skywalker");
}
accept的意思是接收指定的参数来执行操作,andThen指的是在当前操作结束之后所附加的操作。
Comparator
Comparator接口具备比较的功能,Java 8在其基础上增添了多种默认方法,其中包括reversed方法以及thenComparing方法等。
@Test
public void testComparator() {
Comparator<String> comparator = String::compareTo;
String str1 = "eeeabc";
String str2 = "bcd";
System.out.println("str比较大小:" + comparator.compare(str1, str2));
System.out.println("str比较大小反转:" + comparator.reversed().compare(str1, str2));
}
Optional
一种辅助类型,其作用是防止空指针异常,现在来看看这个接口能够实现什么功能:
Optional被定义成一个简单的容器,它的值可能是null,也可能不是null。在Java 8之前,一般某个函数应该返回非空对象,不过偶尔也可能返回null。而在Java 8中,不建议返回null,而是返回Optional 。
@Test
public void testOptional() {
用于预防空指针异常的辅助类型
List<String> list = Arrays.asList("ab", "bc");
System.out.println(list.stream().findFirst().orElse("null str"));
Optional<String> optional = Optional.of("hello");
optional.isPresent(); // true
optional.get(); // "hello"
optional.orElse("hi"); // "hello"
optional.ifPresent((s) -> System.out.println("字符串不为空:" + s));
}
optional.orElse可用于在出现异常情况时,返回预先设定好的返回结果 。
Stream
java.util.Stream是指能应用在一组元素上一次执行的操作序列,Stream操作分为中间操作和最终操作这两种类型,最终操作会返回一特定类型的计算结果,中间操作则返回Stream本身,借助此特性能够将多个操作依次串起来 。创建Stream时,需要指定一个数据源,数据源可以是java.util.Collection的子类,例如List或者Set,Map不支持作为数据源。
@Test
public void testSort() {
List<String> list = Arrays.asList("abe", "abc");
list = list.stream().filter(s -> s.startsWith("a")).sorted().collect(Collectors.toList());
list.stream().forEach(System.out::println);
}
Map
中间操作map会依据指定的Function接口,依次把元素转成另外的对象,下面的示例展示了将字符串转换为大写字符串,你也能够通过map将对象转换成其他类型,map返回的Stream类型由你map传递进去的函数的返回值决定。
@Test
public void testMap() {
List<String> list = Arrays.asList("abe", "abc");
map返回的Stream类型,是由传递进去的函数的返回值来决定的。
list.stream().map(String::toCharArray).forEach(array -> System.out.println(array.length));
}
Match
Stream提供了多种匹配操作,这些操作允许检测指定的Predicate是否匹配整个Stream ,所有的匹配操作都是最终操作 ,且返回一个boolean类型的值 。
@Test
public void testMatch() {
List<String> list = Arrays.asList("ab", "abc");
boolean anyMatch = list.stream().map(String::toCharArray).anyMatch(array -> array.length == 3);
boolean allMatch = list.stream().map(String::toCharArray).allMatch(array -> array.length == 3);
boolean noneMatch = list.stream().map(String::toCharArray).noneMatch(array -> array.length == 3);
System.out.println("anyMatch:" + anyMatch);
System.out.println("allMatch:" + allMatch);
System.out.println("noneMatch:" + noneMatch);
}
Reduce
这是一个最终操作,它允许借助指定的函数,将stream中的多个元素规约成一个元素,规约后的结果通过Optional接口来表示。
@Test
public void testReduce() {
List<String> list = Arrays.asList("ab", "abc", "abcd");
Optional<String> reduce = list.stream().reduce((s1, s2) -> s1 + ":" + s2);
reduce.ifPresent(s -> System.out.println(s));
}
ParallelStream
串行Stream上的操作,是在一个线程中依次去完成的,而并行Stream的操作,是在多个线程上同时执行的。
@Test
public void testParallelStream() {
int max = 1000000;
List<String> values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
UUID uuid = UUID.randomUUID();
values.add(uuid.toString());
}
long t0 = System.nanoTime();
long count = values.parallelStream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("sequential sort took: %d ms", millis));
}
如上所示为并行排序,排序这个Stream耗时明显低于串行。
Map方法
Map类型不支持stream,Map提供了一些新的方法,这些方法很有用,能用来处理一些日常任务。
@Test
public void testMapFun() {
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
map.putIfAbsent(i, "val" + i);
}
map.forEach((id, val) -> System.out.println(val));
map.computeIfPresent(3, (num, val) -> val + num);
System.out.println(map.get(3));
map.computeIfPresent(9, (num, val) -> null);
System.out.println(map.containsKey(9));
map.computeIfAbsent(23, num -> "val" + num);
System.out.println(map.get(23));
map.putIfAbsent(3, "bam");
System.out.println(map.get(3));
map.remove(3, "val3");
System.out.println(map.get(3));
在进行Merge时,若键名不存在,那么就进行插入操作。若键名存在,那么就对原键对应的值做合并操作,之后再重新插入到map中。
map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
System.out.println(map.get(9));
map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
System.out.println(map.get(9));
}
UnaryOperator
它继承自Function接口,它表示对单个操作数进行操作,该操作会生成与其操作数相同类型的结果。
@Test
public void testUnaryOperator() {
UnaryOperator<String> unaryOperator = str -> str + "-test";
System.out.println(unaryOperator.apply("123"));
}
小结
本文主要介绍了Java8里的Lambda表达式,挑选其中常用的方法做了简单的应用讲解,Lambda表达式是Java SE 8中一项重要的新特性,它允许你用表达式取代功能接口。Lambda表达式和方法类似,它能提供一个正常的参数列表,它还能提供一个使用这些参数的主体,这个主体可以是一个表达式,也可以是一个代码块。
Lambda表达式对集合库起到了增强作用,这其中涵盖了java.util.function包,还包括java.util.stream包。Lambda表达式极为简洁,大幅简化了代码行数,让代码在一定程度上变得简洁干净,然而同样地,这或许也会成为一个缺点,因为省略了过多内容,代码可读性在一定程度上可能会降低,这完全取决于你使用lambda表达式的位置所设计的API是否为代码的其他阅读者所熟知。
参考文档,Java 8里一些常用的全新函数式接口,深入理解Java 8 Lambda(语言篇——lambda、方法引用、目标类型和默认方法)
【奔跑吧!JAVA】有奖征文火热进行中: