大话 Java 中的 Lambda 表达式丨【奔跑吧!JAVA】(大话西游)

网友投稿 956 2022-05-30

2014年3月18日,Oracle公司发布了Java SE 8。最近正好抽空整理了Java 8的特性如下:

接口的默认方法

Lambda 表达式

函数式接口

方法与构造函数引用

Lambda 作用域

访问局部变量

访问对象字段与静态变量

访问接口的默认方法

Date API

Annotation 注解

本文将会重点讲解Java 8中的Lambda表达式,其他特性将会在后续文章中讲解。lambda 表达式,又被成为“闭包”或“匿名方法”。

背景

Java 是一门面向对象编程语言。面向对象编程语言和函数式编程语言中的基本元素(Basic Values)都可以动态封装程序行为:面向对象编程语言使用带有方法的对象封装行为,函数式编程语言使用函数封装行为。但这个相同点并不明显,因为Java 对象往往比较“重量级”:实例化一个类型往往会涉及不同的类,并需要初始化类里的字段和方法。

不过有些 Java 对象只是对单个函数的封装。例如下面这个典型用例:Java API 中定义了一个接口(一般被称为回调接口),用户通过提供这个接口的实例来传入指定行为。

public interface ActionListener { void actionPerformed(ActionEvent e); }

这里并不需要专门定义一个类来实现 ActionListener,因为它只会在调用处被使用一次。用户一般会使用匿名类型把行为内联(inline):

button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ui.dazzle(e.getModifiers()); } });

很多库都依赖于上面的模式。对于并行 API 更是如此,因为我们需要把待执行的代码提供给并行 API,并行编程是一个非常值得研究的领域,因为在这里摩尔定律得到了重生:尽管我们没有更快的 CPU 核心(core),但是我们有更多的 CPU 核心。而串行 API 就只能使用有限的计算能力。

匿名内部类

随着回调模式和函数式编程风格的日益流行,我们需要在Java中提供一种尽可能轻量级的将代码封装为数据(Model code as data)的方法。匿名内部类并不是一个好的 选择,因为:

语法过于冗余

匿名类中的 this 和变量名容易使人产生误解

类型载入和实例创建语义不够灵活

无法捕获非 final 的局部变量

无法对控制流进行抽象

函数式接口

大话 Java 中的 Lambda 表达式丨【奔跑吧!JAVA】(大话西游)

尽管匿名内部类有着种种限制和问题,但是它有一个良好的特性,它和Java类型系统结合的十分紧密:每一个函数对象都对应一个接口类型。之所以说这个特性是良好的,是因为:

接口是 Java 类型系统的一部分

接口天然就拥有其运行时表示(Runtime representation)

接口可以通过 Javadoc 注释来表达一些非正式的协定(contract),例如,通过注释说明该操作应可交换(commutative)

接口只有一个方法,大多数回调接口都拥有这个特征:比如 Runnable 接口和 Comparator 接口。我们把这些只拥有一个方法的接口称为 函数式接口。(之前它们被称为 SAM类型,即 单抽象方法类型(Single Abstract Method))

实现函数式类型的另一种方式是引入一个全新的 结构化 函数类型,我们也称其为“箭头”类型。例如,一个接收 String 和 Object 并返回 int 的函数类型可以被表示为 (String, Object) -> int。我们仔细考虑了这个方式,但出于下面的原因,最终将其否定:

它会为Java类型系统引入额外的复杂度,并带来 结构类型(Structural Type) 和 指名类型(Nominal Type) 的混用。(Java 几乎全部使用指名类型)

它会导致类库风格的分歧——一些类库会继续使用回调接口,而另一些类库会使用结构化函数类型

它的语法会变得十分笨拙,尤其在包含受检异常(checked exception)之后

每个函数类型很难拥有其运行时表示,这意味着开发者会受到 类型擦除(erasure) 的困扰和局限。比如说,我们无法对方法 m(T->U) 和 m(X->Y) 进行重载(Overload)

所以我们选择了“使用已知类型”这条路——因为现有的类库大量使用了函数式接口,通过沿用这种模式,我们使得现有类库能够直接使用 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 并返回 boolean Consumer——接收 T,不返回值 Function——接收 T,返回 R Supplier——提供 T 对象(例如工厂),不接收值 UnaryOperator——接收 T 对象,返回 T BinaryOperator——接收两个 T,返回 T

除了上面的这些基本的函数式接口,我们还提供了一些针对原始类型(Primitive type)的特化(Specialization)函数式接口,例如 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 表达式的语法由参数列表、箭头符号 -> 和函数体组成。函数体既可以是一个表达式,也可以是一个语句块:

表达式:表达式会被执行然后返回执行结果。

语句块:语句块中的语句会被依次执行,就像方法中的语句一样——

return 语句会把控制权交给匿名方法的调用者

break 和 continue 只能在循环中使用

如果函数体有返回值,那么函数体内部的每一条路径都必须返回值

表达式函数体适合小型 lambda 表达式,它消除了 return 关键字,使得语法更加简洁。

lambda 表达式也会经常出现在嵌套环境中,比如说作为方法的参数。为了使 lambda 表达式在这些场景下尽可能简洁,我们去除了不必要的分隔符。不过在某些情况下我们也可以把它分为多行,然后用括号包起来,就像其它普通表达式一样。

实战应用

Function

Function 接口有一个参数并且返回一个结果,并附带了一些可以和其他函数组合的默认方法:andThen和compose。

@Test public void testFun() { //Function 接口有一个参数并且返回一个结果 Function toInteger = (t) -> Integer.valueOf(t); System.out.println("compose: " + toInteger.andThen(a -> a + 10).compose(str -> str + "1").apply("123")); Function backToString = toInteger.andThen(String::valueOf); Function 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 isEmpty = String::isEmpty; Predicate 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 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 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

用来防止NullPointerException异常的辅助类型,现在看看这个接口能干什么:

Optional被定义为一个简单的容器,其值可能是null或者不是null。在Java 8之前一般某个函数应该返回非空对象但是偶尔却可能返回了null,而在Java 8中,不推荐你返回null而是返回Optional。

@Test public void testOptional() { //用来防止NullPointerException异常的辅助类型 List list = Arrays.asList("ab", "bc"); System.out.println(list.stream().findFirst().orElse("null str")); Optional 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 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 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 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 list = Arrays.asList("ab", "abc", "abcd"); Optional 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 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 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 unaryOperator = str -> str + "-test"; System.out.println(unaryOperator.apply("123")); }

小结

本文主要介绍了Java8中的Lambda表达式,选择其中常用的方法进行了简单的应用讲解。Lambda表达式是Java SE 8中一个重要的新特性。Lambda表达式允许你通过表达式来代替功能接口。Lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)。

Lambda表达式还增强了集合库,包括java.util.function 包以及java.util.stream包。Lambda表达式非常简洁,大大简化代码行数,使代码在一定程度上变的简洁干净,但是同样的,这可能也会是一个缺点,由于省略了太多东西,代码可读性有可能在一定程度上会降低,这个完全取决于你使用lambda表达式的位置所设计的API是否被你的代码的其他阅读者所熟悉。

参考文档

Java 8中一些常用的全新的函数式接口

深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)

【奔跑吧!JAVA】有奖征文火热进行中:https://bbs.huaweicloud.com/blogs/265241

API Java 面向对象编程

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:excel表格确认文字卡顿的解决方法(excel表格卡顿)
下一篇:【nodejs原理&源码赏析(7)】【译】Node.js中的事件循环,定时器和process.nextTick(nodejs 原理)
相关文章