Java8新特性(一)

Lambda表达式

//匿名内部类
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("测试");
    }
};
r1.run();
System.out.println("******************");
//Lambda表达式
Runnable r2 = () -> System.out.println("测试2");
r2.run();
//匿名内部类
Comparator<Integer> c1 = new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return Integer.compare(o1,o2);
    }
};
int compare = c1.compare(12, 23);
System.out.println(compare);
System.out.println("*********");
//Lambda表达式
Comparator<Integer> c2 = (o1,o2) -> Integer.compare(o1,o2);
int compare1 = c2.compare(12, 10);
System.out.println(compare1);

System.out.println("*********");
//方法引用
Comparator<Integer> c3 = Integer::compare;
int compare2 = c3.compare(23, 23);
System.out.println(compare2);
//匿名内部类
Consumer<String> c1 = new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
};
c1.accept("测试");
System.out.println("*****************");
//Lambda表达式
Consumer<String> c2 = s -> System.out.println(s);
c2.accept("hello");
//简化Lambda表达式
Comparator<Integer> c1 = (o1, o2) -> Integer.compare(o1,o2);
int compare = c1.compare(10, 20);
System.out.println(compare);

Functional Interfaces(函数接口)

ambda 表达式如何适应 Java 的类型系统?每个 lambda 对应一个由接口指定的类型。一个所谓的函数接口必须包含一个抽象方法声明。该类型的每个 lambda 表达式都将与此抽象方法匹配。由于默认方法不是抽象的,所以你可以自由地添加默认方法到你的函数接口。

只要保证接口仅包含一个抽象方法,就可以使用任意的接口作为 lambda 表达式。为确保您的接口符合要求,您应该添加 @FunctionalInterface 注解。编译器注意到这个注解后,一旦您尝试在接口中添加第二个抽象方法声明,编译器就会抛出编译器错误。

四种常用的函数式接口:

四大核心函数式接口
 消费型Consumer<T> void accept(T t);
 供给型 Supplier<T> T get();
 函数型 Function<T,R> R apply(T);
 断言型 Predicate(T) boolean test(T);
Consumer<String> c1 = new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
};
System.out.println("****************");
Consumer<String> c2 = s -> System.out.println(s);
c2.accept("hello");
Supplier<Integer> supplier = () -> 10;
System.out.println(supplier.get());
Function<Integer,String> function = (o1) -> "ces"+o1*2;
System.out.println(function.apply(20));
Predicate<Integer> predicate = o1 -> o1 > 10;
System.out.println(predicate.test(20));
System.out.println(predicate.test(8));

Method and Constructor References(方法和构造器引用)

上面的示例代码可以通过使用静态方法引用进一步简化:

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

Java 8 允许您通过 :: 关键字传递方法或构造函数的引用。上面的例子展示了如何引用一个静态方法。但是我们也可以引用对象方法:

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 对象的 PersonFactory 接口。

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 构造器的引用。Java 编译器会根据PersonFactory.create 的签名自动匹配正确的构造器。

Lambda Scopes(Lambda 作用域)

从 lambda 表达式访问外部作用域变量与匿名对象非常相似。您可以访问本地外部作用域的常量以及实例的成员变量和静态变量。

Accessing local variables(访问本地变量)

我们可以访问 lambda 表达式作用域外部的常量:

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 必须是隐式常量的。下面的代码不能编译通过:

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

此外,在 lambda 表达式中对 num 做写操作也是被禁止的。

Accessing fields and static variables(访问成员变量和静态变量)

与局部变量相比,我们既可以在 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);
        };
    }
}

Accessing Default Interface Methods(访问默认的接口方法)

还记得第一节的 formula 例子吗? Formula 接口定义了一个默认方法 sqrt,它可以被每个 formula 实例(包括匿名对象)访问。这个特性不适用于 lambda 表达式。

默认方法不能被 lambda 表达式访问。下面的代码不能编译通过:

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

Optionals

Optional 不是功能性接口,而是防止 NullPointerException 的好工具。这是下一节的一个重要概念,所以让我们快速看看 Optional 是如何工作的。

Optional 是一个简单的容器,其值可以是 null 或非 null。想想一个可能返回一个非空结果的方法,但有时候什么都不返回。不是返回 null,而是返回 Java 8 中的 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 表示可以在其上执行一个或多个操作的元素序列。流操作是中间或终端。当终端操作返回一个特定类型的结果时,中间操作返回流本身,所以你可以链接多个方法调用。流在源上创建,例如一个 java.util.Collection 像列表或集合(不支持映射)。流操作既可以按顺序执行,也可以并行执行。

一、Stream 流是如何工作的?

流表示包含着一系列元素的集合,我们可以对其做不同类型的操作,用来对这些元素执行计算。听上去可能有点拗口,让我们用代码说话:

List<String> myList =
    Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList
    .stream() // 创建流
    .filter(s -> s.startsWith("c")) // 执行过滤,过滤出以 c 为前缀的字符串
    .map(String::toUpperCase) // 转换成大写
    .sorted() // 排序
    .forEach(System.out::println); // for 循环打印

我们可以对流进行中间操作或者终端操作。小伙伴们可能会疑问?什么是中间操作?什么又是终端操作?

Stream中间操作,终端操作

  • :中间操作会再次返回一个流,所以,我们可以链接多个中间操作,注意这里是不用加分号的。上图中的filter 过滤,map 对象转换,sorted 排序,就属于中间操作。
  • :终端操作是对流操作的一个结束动作,一般返回 void 或者一个非流的结果。上图中的 forEach循环 就是一个终止操作。

看完上面的操作,感觉是不是很像一个流水线式操作呢。

实际上,大部分流操作都支持 lambda 表达式作为参数,正确理解,应该说是接受一个函数式接口的实现作为参数。

二、不同类型的 Stream 流

我们可以从各种数据源中创建 Stream 流,其中以 Collection 集合最为常见。如 ListSet 均支持 stream() 方法来创建顺序流或者是并行流。

并行流是通过多线程的方式来执行的,它能够充分发挥多核 CPU 的优势来提升性能。本文在最后再来介绍并行流,我们先讨论顺序流:

List<String> myList =
                Arrays.asList("a1", "a2", "b1", "c2", "c1");

        myList.stream() // 创建流
                .filter(s -> s.startsWith("c")) // 执行过滤,过滤出以 c 为前缀的字符串
                .map(String::toUpperCase) // 转换成大写
                .sorted() // 排序
                .forEach(System.out::println); // for 循环打印

在集合上调用stream()方法会返回一个普通的 Stream 流。但是, 您大可不必刻意地创建一个集合,再通过集合来获取 Stream 流,您还可以通过如下这种方式:

Stream.of("a1", "a2", "a3")
    .findFirst()
    .ifPresent(System.out::println);  // a1

例如上面这样,我们可以通过 Stream.of() 从一堆对象中创建 Stream 流。

除了常规对象流之外,Java 8还附带了一些特殊类型的流,用于处理原始数据类型intlong以及double。说道这里,你可能已经猜到了它们就是IntStreamLongStream还有DoubleStream

其中,IntStreams.range()方法还可以被用来取代常规的 for 循环

IntStream.range(1,4)
                .mapToObj(i -> "a"+i)
                .forEach(System.out::println);

上面这些原始类型流的工作方式与常规对象流基本是一样的,但还是略微存在一些区别:

  • 原始类型流使用其独有的函数式接口,例如IntFunction代替FunctionIntPredicate代替Predicate
  • 原始类型流支持额外的终端聚合操作,sum()以及average(),如下所示:
Arrays.stream(new int[]{1,2,3,4,5,6,7,8,9,10})
                .map(i -> i*2+1).average()
                .ifPresent(System.out::println);

但是,偶尔我们也有这种需求,需要将常规对象流转换为原始类型流,这个时候,中间操作 mapToInt()mapToLong() 以及mapToDouble就派上用场了:

//将对象流转换成原始类型流
        Stream.of("s1","s2","s3")
                .map(s -> s.substring(1))
                .mapToInt(Integer::parseInt)
                .max()
                .ifPresent(System.out::println);

如果说,您需要将原始类型流装换成对象流,您可以使用 mapToObj()来达到目的:

IntStream.range(1,4)
                .mapToObj(i -> "a"+i)
                .forEach(System.out::println);

下面是一个组合示例,我们将双精度流首先转换成 int 类型流,然后再将其装换成对象流:

Stream.of(1.0,2.0,3.0)
                .mapToInt(Double::intValue)//将double类型转换成int
                .mapToObj(i -> "a" + i)//将int类型转换成Object
                .forEach(System.out::println);//依次输出

三、Stream 流的处理顺序

在讨论处理顺序之前,您需要明确一点,那就是中间操作的有个重要特性 —— 延迟性。观察下面这个没有终端操作的示例代码:

//延迟执行
        Stream.of("a1","a2","a3","a4").filter(s -> {
            System.out.println(s);
            return true;
        });

原因是:当且仅当存在终端操作时,中间操作操作才会被执行。

Stream.of("a1","a2","a3","a4").filter(s -> {
            System.out.println("filter:" + s);
            return true;
        }).forEach(s -> System.out.println("foreach:" + s));

四、中间操作顺序这么重要?

下面的例子由两个中间操作mapfilter,以及一个终端操作forEach组成。让我们再来看看这些操作是如何执行的:

Stream.of("d2", "a2", "b1", "b3", "c")
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase(); // 转大写
    })
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("A"); // 过滤出以 A 为前缀的元素
    })
    .forEach(s -> System.out.println("forEach: " + s)); // for 循环输出

如果我们改变中间操作的顺序,将filter移动到链头的最开始,就可以大大减少实际的执行次数:

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s)
        return s.startsWith("a"); // 过滤出以 a 为前缀的元素
    })
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase(); // 转大写
    })
    .forEach(s -> System.out.println("forEach: " + s)); // for 循环输出

五、数据流复用问题

Java8 Stream 流是不能被复用的,一旦你调用任何终端操作,流就会关闭:

Stream<String> stream =
    Stream.of("d2", "a2", "b1", "b3", "c")
        .filter(s -> s.startsWith("a"));

stream.anyMatch(s -> true);    // ok
stream.noneMatch(s -> true);   // exception

为了克服这个限制,我们必须为我们想要执行的每个终端操作创建一个新的流链,例如,我们可以通过 Supplier 来包装一下流,通过 get() 方法来构建一个新的 Stream 流,如下所示:

Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
            .filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true);   // ok
streamSupplier.get().noneMatch(s -> true);  // ok

JDK8 入门指南
一文带你玩转 Java8 Stream 流,从此操作集合 So Easy

posted @ 2021-11-30 17:32  无涯子wyz  阅读(56)  评论(0)    收藏  举报