New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java8 导览 #17

Open
thinkerou opened this Issue Jun 10, 2018 · 0 comments

Comments

1 participant
@thinkerou
Copy link
Owner

thinkerou commented Jun 10, 2018

接口默认方法

在Java8中允许增加一个非抽象方法实现,通过关键字 default 来标识。相关问答阅读这里.

interface Formula {
    double calculate(int a);

    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}

接口 Formula 有一个抽象方法 calculate 和 一个默认方法 sqrt,继承自该接口的类仅需实现抽象方法 calculate 即可,默认方法 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

formula 以匿名对象的方式实现,虽然比起直接实现具名类代码精简了,但仍然显得有点冗长。

Lambda 表达式

如下代码是在之前版本 Java 中给字符串列表进行排序的示例:

List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");

Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return b.compareTo(a);
    }
});

方法 Collections.sort 接收一个 list 和一个用于给列表元素排序的 comparator,通常做法都是创建一个匿名 comparator 并传给 sort 方法。

在 Java8 中使用更简短的 lambda 表达式 来代替创建匿名对象。

Collections.sort(names, (String a, String b) -> {
    return b.compareTo(a);
});

上面的代码比起创建匿名对象已经简洁了许多,但还可以更简洁明了:

Collections.sort(names, (String a, String b) -> b.compareTo(a));

仅需要一行方法体代码即可,甚至都不需要 {}return 语句,以为这就是最简单的了?其实不是。

names.sort((a, b) -> b.compareTo(a));

注意,这里 sort 方法是 List 的成员。

函数式接口

Lambda 表达式是如何匹配 Java 类型系统的?每个 Lambda 表达式通过一个特定的接口与一个给定的类型进行匹配。函数式接口有且仅有一个抽象方法声明。每个 Lambda 表达式必须与抽象方法的声明匹配。因为默认方法并不是抽象的,所以可以在函数式接口里任意添加默认方法。

任意仅包含一个抽象方法的接口,都可以用来做成 Lambda 表达式。为了定义的接口满足要求,应当在接口前加上 @FunctionalInterface 注解。如果接口中定义了第二个抽象方法,则编译器会抛出异常。

@FunctionalInterface
interface Converter<F, T> {
    T convert(F from);
}
Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted); // 123

注意,如果没有写 @FunctionalInterface 注解,程序也是正确的。

方法和构造函数引用

前面的示例代码可以通过静态方法引用变得更加简洁:

Converter<String, Integer> converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted); // 123

在 Java8 中可以通过 :: 关键字来获取方法或构造函数的引用。上述代码就展示了如何引用一个静态方法,甚至还可以引用一个对象的方法。

class Something {
    String startsWith(String s) {
        return String.valueOf(s.charAt(0));
    }
}
Something something = new Something();
Converter<String, String> 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;
    }
}

Person 包含两个构造函数,接下来定义一个工厂接口,用来创建新的对象。

interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
}

然后就可以通过构造函数引用把所有东西拼到一起,不再像以前那样手动实现一个工厂。

PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");

通过 Person::new 来创建一个 Person 类构造函数的引用,编译器会自动选择合适的构造函数来匹配 PersonFactory.create 函数的签名。

Lambda 的范围

对 Lambda 表达式外部的变量,其访问权限的粒度和匿名对象的方式很相似。可以访问局部对应的外部区域的局部 final 变量,以及成员变量和静态变量。

访问局部变量

可以访问 Lambda 表达式外部的 final 局部变量。

final int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2); // 3

但是与匿名对象不同的是变量 num 并不需要声明为 final,下面代码依然有效:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2); // 3

然而变量 num 在编译期必须被隐式的当做 final 处理,如下代码就不能被正确编译:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
num = 3;

注意,在 Lambda 表达式内部试图改变 num 变量的值也是不允许的。

访问成员变量和静态变量

和局部变量不同,在 Lambda 表达式内部能获取到对成员变量和静态变量的读写权限。这种行为在匿名对象里是非常典型的。

class Lambda4 {
    static int outerStaticNum;
    int outerNum;

    void testScopes() {
        Converter<Integer, String> stringConverter1 = (from) -> {
            outerNum = 23;
            return String.valueOf(from);
        };

        Converter<Integer, String> stringConverter2 = (from) -> {
            outerStaticNum = 72;
            return String.valueOf(from);
        };
    }
}

访问默认接口方法

前述中的接口 Formula 定义了一个默认的方法 sqrt,该方法能够访问 formula 所有的对象实例,包括匿名对象。这条对 Lambda 表达式并不适用。

默认方法无法在 Lambda 表达式内部被访问。如下代码并不能编译通过:

Formula formula = (a) -> sqrt(a * 100);

内置函数式接口

在 JDK8 API 中包含了很多内置的函数式接口,比如以前版本中耳熟能详的 ComparatorRunnable,对这些现成的接口进行实现,可以通过 @FunctionalInterface 注解来启用 Lambda 功能。

在 Java8 API 中还提供了大量新的函数式接口,有些新的接口已经在 Google Guava 库中很有名了。

Predicates

Predicates 是一个布尔类型的函数,该函数只有一个输入参数。Predicate 接口包含了大量默认方法,用于处理复杂的逻辑运算。

Predicate<String> predicate = (s) -> s.length() > 0;

predicate.test("foo"); // true
predicate.negate().test("foo"); // false

Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;

Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

Functions

Functions 接口接收一个参数,并返回单一的结果。默认方法可以将多个函数串连在一起。

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);

backToString.apply("123"); // "123"

Suppliers

Suppliers 接口产生一个给定类型的结果,与 Function 不同的是,它没有输入参数。

Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person

Consumers

Consumers 代表了在一个输入参数上需要进行的操作。

Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));

Comparators

Comparators 接口在以前版本中已经很出名了,在 Java8 中为其增加了许多默认方法。

Comparator<Person> 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

Optionals 并不是一个函数式接口,而是一个精巧的工具接口,用来防止 NullPointerException 产生。

Optional 是一个简单的值容器,这个值可以是 nullnon-null。考虑到一个方法可能返回一个 non-null 值,也可能返回一个 null 值,为了不直接返回 null 值,在 Java8 中就返回一个 Optional

Optional<String> optional = Optional.of("bam");

optional.isPresent();           // true
optional.get();                 // "bam"
optional.orElse("fallback");    // "bam"

optional.ifPresent((s) -> System.out.println(s.charAt(0)));     // "b"

Streams

java.util.Stream 表示了某一种元素的序列,在这些元素上可以进行各种操作。Stream 操作可以是中间操作,也可以是完结操作。完结操作会返回一个某种类型的值,儿中间操作会返回流对象本身,并且可以通过多次调用同一个流操作方法来讲操作结果串连起来。Stream 是在一个源的基础上创建出来的,如 java.util.Collection 中的 listset (但 map 不能作为 Stream 源)。Stream 操作往往可以通过顺序或并行两种方式来执行。

参考 Java 8 Streams Tutorial.
参考 Sequency

首先看看序列流是如何工作的?

List<String> 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");

在 Java8 中 Collections 类的功能已经有所增强,可以直接通过调用 Collection.stream()Collection.parallelStream() 方法来创建一个对象流。

Filter

Filter 接收一个 predicate 接口类型的变量,并将所有流对象中的元素进行过滤。该操作是一个中间操作,因此它允许在返回结果的基础上再进行其他的流操作(forEach)。forEach 接收一个函数式接口类型的变量,用来执行对每一个元素的操作。forEach 是一个完结操作,它不返回流,因此不能再调用其他的流操作。

stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

// "aaa2", "aaa1"

Sorted

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

Map

map 是一个对于流对象的中间操作,通过给定的方法,能够把对象中的每一个元素对应到另外一个对象上。如下示例展示了如何把每个 string 都转换成大写的 string,也可以把每一种对象映射成为某种其他类型。对于带泛型结果的流对象,具体的类型需要由传递给 map 的泛型方法来决定。

stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);

// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

Match

匹配操作有多种不同的类型,都是用来判断某一种规则是否与流对象相互吻合。所有的匹配操作都是完结操作,只返回一个布尔类型的结果。

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

Count 是一个完结操作,返回一个数值,用来标识当前流对象中包含的元素数量。

long startsWithB =
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();

System.out.println(startsWithB); // 3

Reduce

该操作是一个完结操作,能够通过某一个方法,对元素进行删减操作,操作结果会放在一个 Optional 变量里返回。

Optional<String> reduced =
    stringCollection
        .stream()
        .sorted()
        .reduce((s1, s2) -> s1 + "#" + s2);

reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Parallel Streams

如前所述,流操作可以是顺序的,也可以是并行的。顺序操作通过单线程执行,而并行操作则通过多线程执行。

如下示例展示了如何使用并行流进行操作来提高运行效率。

首先创建一个大的列表,里面元素都是唯一的:

int max = 1000000;
List<String> values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
    UUID uuid = UUID.randomUUID();
    values.add(uuid.toString());
}

现在来测试一个对这个集合进行排序所耗费的时机。

Sequential Sort

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

Parallel Sort

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

如你所见,所有的代码段几乎相同,唯一的不同就是把 stream() 改为 parallelStream() 了,但结果显示并行排序快了 50% 左右。

Maps

如前所述,map 并不支持流操作。接口 Map 本身并没有 stream() 方法,但是可以在 map 的 key、value 和 entry 上创建特定的流,对应于
map.keySet().stream()map.values().stream()map.entrySet().stream()

现在,更新后的 map 现在则支持多种实用的新方法来完成常规任务。

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));

上面的代码是完全自解释的:putIfAbsent 会避免将 null 写入,而 forEach 接收一个消费者对象将操作实施到每一个 map 中的值上。

下面的示例展示了如何使用函数来计算 map 编码:

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

接下来,看看给定 key 值如何把一个实例从对应的 key 中删除?

map.remove(3, "val3");
map.get(3); // val33

map.remove(3, "val33");
map.get(3); // null

另一个有用的方法:

map.getOrDefault(42, "not found"); // not found

将 map 中的实例合并也是很容易的:

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

合并操作先看 map 中是否有特定的 key/value 存在,如果有则把 key/value 存入 map,否则 merging 函数就会被调用,对现有的数值进行更新。

原文见这里

@thinkerou thinkerou self-assigned this Jun 10, 2018

@thinkerou thinkerou added the Java label Jun 10, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment