JavaRush /Java 博客 /Random-ZH /Java 8 指南。1 部分。
ramhead
第 13 级

Java 8 指南。1 部分。

已在 Random-ZH 群组中发布

“Java 仍然存在——人们开始理解它。”

欢迎阅读我对 Java 8 的介绍。本指南将引导您逐步了解该语言的所有新功能。通过简短的代码示例,您将学习如何使用接口默认方法lambda 表达式引用方法可重复注释。读完本文后,您将熟悉 API 的最新更改,例如流、函数接口、关联扩展和新的 Date API。没有无聊的文字墙——只有一堆带注释的代码片段。享受!

接口的默认方法

Java 8 允许我们通过使用default 关键字来添加在接口中实现的非抽象方法。此功能也称为 扩展方法。这是我们的第一个示例: interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } 除了抽象方法 calculate之外, Formula接口还定义了一个默认方法 sqrt。实现 Formula接口的类仅实现抽象 计算方法。默认的 sqrt方法可以直接使用。 公式 Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0 对象作为匿名对象实现。代码相当令人印象深刻:6 行代码就可以简单地计算 sqrt(a * 100)。正如我们将在下一节中看到的,在 Java 8 中实现单方法对象有一种更有吸引力的方式。

Lambda 表达式

让我们从一个简单的例子开始,说明如何在 Java 的早期版本中对字符串数组进行排序: 统计辅助方法 Collections.sort使用一个列表和一个 Comparator 给定列表的元素进行排序。经常发生的情况是您创建匿名比较器并将它们传递给排序方法。Java 8 不再总是创建匿名对象,而是让您能够使用更少的语法、 lambda 表达式: 如您所见,代码更短且更易于阅读。但这里它变得更短: 对于单行方法,您可以去掉 {}大括号和 return关键字。但这里的代码变得更短: Java 编译器知道参数的类型,因此您也可以将它们省略。现在让我们更深入地了解如何在现实生活中使用 lambda 表达式。 List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator () { @Override public int compare(String a, String b) { return b.compareTo(a); } }); Collections.sort(names, (String a, String b) -> { return b.compareTo(a); }); Collections.sort(names, (String a, String b) -> b.compareTo(a)); Collections.sort(names, (a, b) -> b.compareTo(a));

功能接口

lambda 表达式如何适应 Java 类型系统?每个 lambda 对应于接口定义的给定类型。而所谓的函数式接口必须恰好包含一个声明的抽象方法。每个给定类型的 lambda 表达式都会对应这个抽象方法。由于默认方法不是抽象方法,因此您可以自由地将默认方法添加到函数式接口中。我们可以使用任意接口作为 lambda 表达式,前提是该接口仅包含一个抽象方法。为了确保您的接口满足这些条件,您必须添加 @FunctionalInterface注解。编译器会通过这个注解告知该接口必须只包含一个方法,如果在该接口中遇到第二个抽象方法,就会抛出错误。示例: 请记住,即使尚未声明 @FunctionalInterface注释,此代码也将有效。 @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123

对方法和构造函数的引用

通过使用统计方法引用可以进一步简化上面的示例: Java 8 允许您使用 ::关键字符号 传递对方法和构造函数的引用。上面的例子展示了如何使用统计方法。但我们也可以引用对象上的方法: 让我们看一下如何使用 ::用于构造函数。首先,让我们定义一个具有不同构造函数的示例: 接下来,我们定义 PersonFactory工厂接口来创建新的 person对象: Converter converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123 class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } } Something something = new Something(); Converter converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J" class Person { String firstName; String lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } interface PersonFactory

{ P create(String firstName, String lastName); } 我们没有手动实现工厂,而是使用构造函数引用将所有内容绑定在一起: 我们通过Person::new创建对Person类 的构造函数的引用。Java 编译器将通过比较构造函数的签名与PersonFactory.create方法的签名来自动调用适当的构造函数。 PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");

拉姆达区

从 lambda 表达式组织对外部作用域变量的访问类似于从匿名对象进行访问。 您可以从本地范围访问最终变量,以及实例字段和聚合变量。
访问局部变量
我们可以从 lambda 表达式的作用域中 读取带有 final修饰符的局部变量: 但与匿名对象不同的是,变量不需要声明为 final就可以从lambda表达式访问。此代码也是正确的: 但是, num变量必须保持不可变,即 对于代码编译来说是隐式的 final。以下代码将无法编译: 也不允许在 lambda 表达式中 更改 num 。 final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3;
访问实例字段和统计变量
与局部变量不同,我们可以读取和修改 lambda 表达式内的实例字段和统计变量。我们从匿名对象中知道这种行为。 class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
访问接口的默认方法
还记得第一部分中带有 公式实例 的示例吗? Formula接口定义了一个默认的 sqrt方法,可以从 Formula的每个实例(包括匿名对象)访问该方法。这不适用于 lambda 表达式。无法在 lambda 表达式内访问默认方法。以下代码无法编译: Formula formula = (a) -> sqrt( a * 100);

内置功能接口

JDK 1.8 API 包含许多内置的函数接口。其中一些在以前的 Java 版本中是众所周知的。例如 ComparatorRunnable。这些接口经过扩展以包含使用 @FunctionalInterface注释的 lambda 支持。但 Java 8 API 还充满了新的功能接口,这将使您的生活更轻松。其中一些接口在 Google 的 Guava库中是众所周知的。即使您熟悉这个库,您也应该仔细看看这些接口是如何扩展的,以及一些有用的扩展方法。
谓词
谓词是具有一个参数的布尔函数。该接口包含使用谓词创建复杂逻辑表达式(and、or、negate)的各种默认方法 Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();
功能
函数接受一个参数并产生一个结果。默认方法可用于将多个函数组合成一个链(compose、andThen)。 Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
供应商
供应商返回一种或另一种类型的结果(实例)。与函数不同,提供者不接受参数。 Supplier personSupplier = Person::new; personSupplier.get(); // new Person
消费者
消费者用单个参数表示接口方法。 Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
比较器
我们从 Java 的早期版本就知道比较器了。Java 8 允许您向接口添加各种默认方法。 Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
选项
Optionals 接口没有功能,但它是防止 NullPointerException的一个很好的实用程序。这是下一节的重点,因此让我们快速了解一下该界面的工作原理。Optional 接口是一个简单的容器,用于存储可以为 null或非 null 的值。想象一下,一个方法可以返回一个值,也可以不返回任何内容。在 Java 8 中,您不返回 null,而是返回一个 Optional实例。 Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0

溪流

java.util.Stream是对其执行一个或多个操作的元素序列。每个 Stream 操作要么是中间操作,要么是最终操作。终端操作返回特定类型的结果,而中间操作返回流对象本身,从而允许创建方法调用链。Stream 是一个接口,类似于列表和集合的 java.util.Collection(不支持映射)。每个 Stream 操作可以顺序执行,也可以并行执行。让我们看看流是如何工作的。首先,我们将以字符串列表的形式创建示例代码: Java 8 中的集合得到了增强,因此您可以通过调用 Collection.stream()Collection.parallelStream()非常简单地创建流。下一节将解释最重要、最简单的流操作。 List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
筛选
过滤器接受谓词来过滤流的所有元素。此操作是中间操作,它允许我们对生成的(过滤后的)结果调用其他流操作(例如 forEach)。ForEach 接受将对已过滤流的每个元素执行的操作。ForEach 是一个终端操作。此外,调用其他操作也是不可能的。 stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
已排序
Sorted 是一个中间操作,它返回流的排序表示。 除非您指定Comparator,否则元素会按正确的顺序排序。 stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" 请记住,sorted 创建流的排序表示,而不影响集合本身。 stringCollection元素的顺序保持不变: System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
地图
中间映射操作使用结果函数将每个元素转换为另一个对象。以下示例将每个字符串转换为大写字符串。但您也可以使用映射将每个对象转换为不同的类型。生成的流对象的类型取决于传递给映射的函数类型。 stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
匹配
可以使用各种匹配操作来测试流关系中特定谓词的真实性。所有匹配操作都是终端操作并返回布尔结果。 boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
数数
Count 是一个终端操作,它以long 形式 返回流的元素数量。 long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
减少
这是一个终端操作,它使用传递的函数缩短流的元素。结果将是 一个包含缩短值的可选值。 Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

并行流

如上所述,流可以是顺序的或并行的。顺序流操作在串行线程上执行,而并行流操作在多个并行线程上执行。以下示例演示了如何使用并行流轻松提高性能。首先,让我们创建一个包含唯一元素的大型列表: 现在我们将确定对该集合的流进行排序所花费的时间。 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.stream().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)); // sequential sort took: 899 ms
并行流
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("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms 正如您所看到的,两个片段几乎相同,但并行排序快了 50%。您所需要做的就是将 stream()更改为 parallelStream()

地图

正如已经提到的,地图不支持流。相反,地图开始支持解决常见问题的新且有用的方法。 上面的代码应该很直观: putIfAbsent警告我们不要编写额外的空检查。 forEach接受一个为每个映射值执行的函数。这个例子展示了如何使用函数对映射值执行操作: 接下来,我们将学习如何删除给定键的条目(仅当它映射到给定值时): 另一种好方法: 合并映射条目非常简单: 合并如果给定键没有条目,则将把键/值插入到映射中,或者调用合并函数,这将更改现有条目的值。 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); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33 map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null map.getOrDefault(42, "not found"); // not found map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION