|
背景 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类型系统结合的十分紧密:每一个函数对象都对应一个接口类型。之所以说这个特性是良好的,是因为: - 接口是 Java 类型系统的一部分
- 接口天然就拥有其运行时表示(Runtime representation)
- 接口可以通过 Javadoc 注释来表达一些非正式的协定(contract),例如,通过注释说明该操作应可交换(commutative)
接口只有一个方法,大多数回调接口都拥有这个特征:比如 Runnable 接口和 Comparator 接口。我们把这些只拥有一个方法的接口称为 函数式接口。(之前它们被称为 SAM类型,即 单抽象方法类型(Single Abstract Method)) 我们并不需要额外的工作来声明一个接口是函数式接口:编译器会根据接口的结构自行判断(判断过程并非简单的对接口方法计数:一个接口可能冗余的定义了一个 Object 已经提供的方法,比如 toString(),或者定义了静态方法或默认方法,这些都不属于函数式接口方法的范畴)。不过API作者们可以通过 @FunctionalInterface 注解来显式指定一个接口是函数式接口(以避免无意声明了一个符合函数式标准的接口),加上这个注解之后,编译器就会验证该接口是否满足函数式接口的要求。 实现函数式类型的另一种方式是引入一个全新的 结构化 函数类型,我们也称其为“箭头”类型。例如,一个接收 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.Runnablejava.util.concurrent.Callablejava.security.PrivilegedActionjava.util.Comparatorjava.io.FileFilterjava.beans.PropertyChangeListener除此之外,Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如: Predicate<T>——接收 T 并返回 booleanConsumer<T>——接收 T,不返回值Function<T, R>——接收 T,返回 RSupplier<T>——提供 T 对象(例如工厂),不接收值UnaryOperator<T>——接收 T 对象,返回 TBinaryOperator<T>——接收两个 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 表达式在这些场景下尽可能简洁,我们去除了不必要的分隔符。不过在某些情况下我们也可以把它分为多行,然后用括号包起来,就像其它普通表达式一样。 实战应用FunctionFunction 接口有一个参数并且返回一个结果,并附带了一些可以和其他函数组合的默认方法:andThen和compose。 @Testpublic 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。 SupplierSupplier 接口返回一个任意范型的值,和Function接口不同的是该接口没有任何参数。代码如下: @Testpublic void testSupplier() { //Supplier 接口返回一个任意范型的值,和Function接口不同的是该接口没有任何参数 Supplier sp = () -> "sp"; System.out.println(sp.get());}如上代码将会返回一个字符串“sp”,通过get方法获取到返回的值。 PredicatePredicate 接口只有一个参数,返回boolean类型。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非): @Testpublic 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"));}如上代码判断了字符串是否为空,并应用了与、非操作。 ConsumerConsumer 接口表示执行在单个参数上的操作,主要的方法为andThen和accept。 @Testpublic 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表示当前操作结束之后附加的操作。 ComparatorComparator接口用于比较, Java 8在此之上添加了多种默认方法,如reversed和thenComparing等。 @Testpublic 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用来防止NullPointerException异常的辅助类型,现在看看这个接口能干什么: Optional被定义为一个简单的容器,其值可能是null或者不是null。在Java 8之前一般某个函数应该返回非空对象但是偶尔却可能返回了null,而在Java 8中,不推荐你返回null而是返回Optional。 @Testpublic void testOptional() { //用来防止NullPointerException异常的辅助类型 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用来对异常情况返回预设的返回结果。 Streamjava.util.Stream 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回Stream本身,这样你就可以将多个操作依次串起来。Stream 的创建需要指定一个数据源,比如 java.util.Collection的子类,List或者Set, Map不支持。 @Testpublic 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传递进去的函数的返回值决定的。 @Testpublic void testMap() { List<String> list = Arrays.asList("abe", "abc"); //map返回的Stream类型是根据传递进去的函数的返回值决定 list.stream().map(String::toCharArray).forEach(array -> System.out.println(array.length));}MatchStream提供了多种匹配操作,允许检测指定的Predicate是否匹配整个Stream。所有的匹配操作都是最终操作,并返回一个boolean类型的值。 @Testpublic 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接口表示的: @Testpublic 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则是在多个线程上同时执行。 @Testpublic 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提供了一些新的有用的方法来处理一些日常任务。 @Testpublic 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接口,表示对单个操作数的操作,该操作生成与其操作数相同类型的结果。 @Testpublic void testUnaryOperator() { UnaryOperator<String> 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是否被你的代码的其他阅读者所熟悉。
|