Java8特性小结

158 阅读10分钟

Java8 Optional

  • 例一

以前写法

public String getCity(User user)  throws Exception{
        if(user!=null){
            if(user.getAddress()!=null){
                Address address = user.getAddress();
                if(address.getCity()!=null){
                    return address.getCity();
                }
            }
        }
        throw new Excpetion("取值错误"); 
    }

Java8 写法

public String getCity(User user) throws Exception{
    return Optional.ofNullable(user)
                   .map(u-> u.getAddress())
                   .map(a->a.getCity())
                   .orElseThrow(()->new Exception("取指错误"));
}
  • 例二 以前写法
if(user!=null){
    dosomething(user);
}

Java8 写法

Optional.ofNullable(user)
         .ifPresent(u->{
            dosomething(u);
         });
  • 例三 以前写法
public User getUser(User user) throws Exception{
    if(user!=null){
        String name = user.getName();
        if("zhangsan".equals(name)){
            return user;
        }
    }else{
        user = new User();
        user.setName("zhangsan");
        return user;
    }
}

Java8 写法

public User getUser(User user) {
    return Optional.ofNullable(user)
                   .filter(u->"zhangsan".equals(u.getName()))
                   .orElseGet(()-> {
                        User user1 = new User();
                        user1.setName("zhangsan");
                        return user1;
                   });
}

Java8 妙用 Optional

Java8 Lambda 表达式

  • java 自己带的函数式接口都在 java.util.function 包里面,这里面有一些事正常的函数式接口,java 还为我们提供了一些不用自动装箱和拆箱的方法,因为正常的情况下泛型不支持基础类型,比如:int、double 之类的,只支持它们的包装类,所以如果我们传进去的是基础类型就会自动装箱,浪费时间和空间,所以 java 提供了一些基础类型的函数式接口

  • java 自带的函数式接口的那个函数不带抛出异常的,要想带,只能自己声明函数式接口

  • 通用套路:

// 先定义一个函数式接口,自己定义要加上@FunctionalInterface
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}

// 使用函数式接口定义一个通用处理函数
public static String processFile(BufferedReaderProcessor p) throws
IOException {
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return p.process(br);
    }
}

// 使用通用处理函数,传递 lambda 表达式
String result = processFile((BufferedReader br) ->
                            br.readLine() + br.readLine());
  • Lambda 表达式可以类型推倒,但是吧,有时候感觉还是带着类型,清晰些

  • Lambda 表达式可以使用局部变量,就像匿名类一样,==但是有一点需要注意,就是 Lambda 表达式里面使用的局部变量必须是 final 的==,上例子:

// 这种是报错的,因为 Lambda 引用的 portNumber 被赋值两次,不是 final 的
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;

之所以有这个 final 的限制,是因为局部变量是保存在栈上的,而实例变量是保存在堆上,保存在栈上的,就是每个线程独自的。如果我们的 Lambda 表达式是在另一个线程中运行的,比如 Runnable,那么就可能会有线程不安全的问题,因为这个时候相当于是两个线程访问同一个局部变量。但是堆上的东西就没事,本来也是所有线程共享的。==出于这个目的,Lambda 可以随意访问实例变量和静态变量,但是局部变量只能为 final。== Lambda 表达式能实现一部分闭包了,就是能让一个函数作为参数传递给另一个函数。

  • 方法引用

主要是分成三种情况:

  1. 类的静态方法,直接使用 类名::方法名
  2. 实例方法,使用 类名::方法名
String::length   <===>  (String str) -> str.length()
  1. 针对现有对象的实力方法,使用 对象名::方法名
expensiveTransaction::getValue <===> ()->expensiveTransaction.getValue()

4.构造函数的方法引用

// 如果是无参的构造函数,正好和 Supplier 接口符合
Supplier<Apple> c1 = Apple::new;  <===> Supplier<Apple> c1 = () -> new Apple();
Apple apple = c1.get();

// 如果带一个参数,就符合 Function 接口  Apple(Integer weight)
Function<Integer, Apple> c2 = Apple::new;   
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);

// 如果带两个参数,就符合 BiFunction 接口 Apple(String color, Integer weight)
BiFunction<String, Integer, Apple> c3 = Apple::new;
Apple c3 = c3.apply("green", 110);

// 如果带三个参数的话,就需要自己定义函数式接口了
public interface TriFunction<T, U, V, R>{
    R apply(T t, U u, V v);
}
TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;

java8 in action 中介绍的终极简化版本

inventory.sort(comparing(Apple::getWeight));
  • Lambda 复合
  1. 比较器复合
// Comparator 的静态方法 comparing 是为了将一个 Function 接口转换成一个 Comparator
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

// Comparator 中实现了很多默认接口,这都是 java8 的东西嘛,如下:
reversed() // 逆序
Comparator<T> thenComparing(Comparator<? super T> other) // 比较链
  1. 谓语符合
//  negate、and 和 or 
Predicate<Apple> notRedApple = redApple.negate();
redicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150)
                                        .or(a -> "green".equals(a.getColor()));

  1. 函数复合
//  andThen 和 compose
f.andThen(g) // 是把 f 得到的结果作为 g 的输入
f.compose(g) // 先做 g,然后把 g 的输出作为输入,输入到 f 中
// 主要是为了实现流水线功能
Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline = addHeader.andThen(Letter::checkSpelling)
.andThen(Letter::addFooter);
  • 匿名类和 Lambda 表达式的区别
  1. 匿名类和 Lambda 表达式中的 this 和 super 的含义是不同的。在匿名类中, this 代表的是类自身,但是在 Lambda 中,它代表的是包含类。
  2. 匿名类可以屏蔽包含类的变量,而 Lambda 表达式不能(它们会导致编译错误),因为局部变量只能是 final 的
  3. 如果 Lambda 表达式符合多个函数式接口的签名,可能产生歧义,这就会出现强制转换,使得原本优雅的 Lambda 变得不优雅

==Lambda 表达式现阶段的缺点在于调试困难==,你无法一看看出是啥问题,有一种打印的方法,使用 peek 函数,把流执行完的每步都打印出来

// 打印每一步的元素
List<Integer> result = numbers.stream()
                            .peek(x -> System.out.println("from stream: " + x))
                            .map(x -> x + 17)
                            .peek(x -> System.out.println("after map: " + x))
                            .filter(x -> x % 2 == 0)
                            .peek(x -> System.out.println("after filter: " + x))
                            .limit(3)
                            .peek(x -> System.out.println("after limit: " + x))
                            .collect(toList());
  • 使用 andThen 可以轻松实现责任链模式
naryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");

Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result = pipeline.apply("Aren't labdas really sexy?!!")

  • 流只能被消费一次,不能重复消费,只能重新获取一个新的流,然后去重新消费
  • 流的中间操作是什么都不干的,因为懒,都是在最终操作时遍历一次,然后执行中间操作的,而且还夹杂着各种优化,比如 limit,就意味着不用遍历那么多
  • 所谓的中间操作就是返回值是 Stream 的函数,终止操作就是返回值不是 Stream 的函数
  • 流的编写感觉有点类似于数据库查询
  • ==流常用操作==:
  1. filter 接受一个谓语判断
  2. distinct()没参数,去掉重复元素
  3. limit(int n) 截取前 n 个元素
  4. skip(int n) 丢弃掉前 n 个元素
  5. map 接受一个 Lambda 函数
  6. flatMap 扁平化流 其实是它既有 map 的效果又有 flat 的效果,map 的效果就是针对流各个元素进行操作,flat 是将所有的流连接到一起成为一个流,相当于是去掉一层 Stream
List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);
List<int[]> pairs = numbers1.stream().flatMap(i -> numbers2.stream()
                                                .map(j -> new int[]{i, j})
                                             ).collect(toList());

使用 flatMap 能使 Stream<Stream<Integer[]>>转变成 Stream<Integer[]>,就是去掉了一层 Stream 7. anyMatch 接受一个谓语,流中是否有一个元素满足谓语,返回 boolean 8. allMatch 同 anyMatch 返回 boolean 9. noneMatch 没有元素满足谓语 10. findAny 返回 Optional,返回符合条件的任意值,一般配合 filter 使用,再结合上 Optional 的方法

menu.stream()
    .filter(Dish::isVegetarian)
    .findAny()   <--- 返回一个 Optional<Dish>
    .ifPresent(d -> System.out.println(d.getName()); <--- 如果包含值的话执行 Lambda
  1. findFirst 返回 Optional,一般配合 filter 使用,和 findAny 类似,主要是针对有序的流
  2. reduce
// 求和
int sum = numbers.stream().reduce(0, Integer::sum); 
// 求和,主要是怕流中没有任何元素
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));

// 求最大值、最小值
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);

// 结合 map,构建 map-reduce
int count = menu.stream()
                .map(d -> 1)
                .reduce(0, (a, b) -> a + b);
// 当然这个也可以使用 count()
  • 数值流

数值流就是所谓的 IntStream、DoubleStream、LongStream,主要就是解决拆箱的问题,因为流是泛型,必须是 Integer,如果后续操作是用 int 的话,就得拆箱,而且这些数值流还定义了普通流没有的 sum、max、min

int calories = menu.stream().mapToInt(Dish::getCalories).sum();
// 数值流向对象流的转化
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();

IntStream、LongStream 有两个静态方法获取范围数值流:range、rangeClosed,前一个左闭右开,后一个左闭右闭

  • 构建流
  1. 使用 stream 方法
  2. Stream.of
  3. Arrays.stream
  4. Files.lines,java nio 新加入的,为了迎合 stream api
// Stream 实现了 AutoCloseable,可以使用 java7 的 try resource
long uniqueWords = 0;
try(
    Stream<String> lines =Files.lines(Paths.get("data.txt"),Charset.defaultCharset())
    ){
    uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
                        .distinct()
                        .count();
    }catch(IOException e){
    
    }

5.通过函数生成流,创建无限流

主要通过 Stream.iterate 和 Stream.generate

// 生成斐波那契的无限流
Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0]+t[1]})
       .limit(20)
       .map(t -> t[0])
       .forEach(t -> System.out.println("(" + t[0] + "," + t[1] +")"));
// 生成一个全是 1 的无限流
IntStream ones = IntStream.generate(() -> 1); 
  • 几点建议
  1. 尽量使用方法方法引用,减少代码长度,把复杂的逻辑封装成函数,然后使用方法引用
  2. 尽量考虑静态辅助方法,比如 comparing 、 maxBy

CompletableFuture

为了解决 Future 的问题,我们可以像 Future 那样使用,就是自己起线程,自己往 future 中赋值,自己处理异常。

public Future<Double> getPriceAsync(String product) {
    CompletableFuture<Double> futurePrice = new CompletableFuture<>();
    // 自己起线程
    try {
        double price = calculatePrice(product);
        // 自己往 Future 中设置结果
        futurePrice.complete(price);
    } catch (Exception ex) {
        // 自己往 Future 中设置异常
        futurePrice.completeExceptionally(ex);
    }
    return futurePrice;
}

简单一点的是通过 CompletableFuture 中的静态函数,比如 supplyAsync

public Future<Double> getPriceAsync(String product) {
    return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}
  • CompletableFuture 和并行流

    对集合进行并行计算有两种方式:要么将其转化为并行流,利用 map 这样的操作开展工作,要么枚举出集合中的每一个元素,创建新的线程,在 CompletableFuture 内对其进行操作。后者提供了更多的灵活性,你可以调整线程池的大小,而这能帮助你确保整体的计算不会因为线程都在等待 I/O 而发生阻塞。

    并行流的缺点在于它使用内置的线程池,但是内置的线程池的线程个数是 cpu 的个数,比如 cpu 是 4,就是 4 个线程,但是如果你有 5 个任务就得有一个等待了。而 CompletableFuture 的好处就在于线程池可以自定义,你可以创建多个线程

线程池的个数:
 N threads = N CPU * U CPU * (1 + W/C)

其中: N CPU 是处理器的核的数目,可以通过 Runtime.getRuntime().availableProce- ssors() 得到 U CPU 是期望的 CPU 利用率(该值应该介于 0 和 1 之间) W/C 是等待时间与计算时间的比率

这里的 W/C 是指阻塞的时长占总时长的多少,基本总时长是阻塞时间 + 计算时间,但是这个公式计算出来的值只是一个上限,一般任务几个就有几个线程

总结:如果是计算密集型的程序,不涉及 io,那就用 stream,连并行流都不用,如果你是 io 密集型的,那就使用 CompletableFuture,更灵活,而且出了错能定位,并行流不好定位

CompletableFuture 的使用:

// 这里一定要注意使用两次 map,如果合成一个的话,就变成串行的了,执行第一个得到 Future,然后执行 join,就相当是等执行完了,然后执行第二个得到 future,然后执行 join
public List<String> findPrices(String product) {
    List<CompletableFuture<String>> priceFutures = shops.stream()
                                                        .map(shop -> CompletableFuture.supplyAsync(
                                                        () -> shop.getName() + " price is " + shop.getPrice(product)))
                                                    .collect(Collectors.toList());
    return priceFutures.stream().map(CompletableFuture::join).collect(toList());
}

// 应该改成下面这样,效率更好
private final Executor executor = Executors.newFixedThreadPool(Math.min(shops.size(), 100), new ThreadFactory() {
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setDaemon(true);
        return t;
    }
})

public List<String> findPrices(String product) {
    List<CompletableFuture<String>> priceFutures = shops.stream()
                                                        .map(shop -> CompletableFuture.supplyAsync(
                                                        () -> shop.getName() + " price is " + shop.getPrice(product), executor))
                                                    .collect(Collectors.toList());
    return priceFutures.stream().map(CompletableFuture::join).collect(toList();
}

CompletableFuture 的静态方法中,没有 Asycn 的就代表是同步的,不要在这种函数中执行阻塞方法,thenApply 将第一步的结果作为输入,执行函数。thenCompose 对第一个 CompletableFuture 对象调用 thenCompose ,并向其传递一个函数。当第一个 CompletableFuture 执行完毕后,它的结果将作为该函数的参数,这个函数的返回值是以第一个 CompletableFuture 的返回做输入计算出的第二个 CompletableFuture 对象。

public List<String> findPrices(String product) {
    List<CompletableFuture<String>> priceFutures = 
    shops.stream().map(shop -> CompletableFuture.supplyAsync(
    () -> shop.getPrice(product), executor)
    ).map(future -> future.thenApply(Quote::parse))
    .map(future -> future.thenCompose(quote ->
    CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote),executor))
    ).collect(toList());
    
    return priceFutures.stream().map(CompletableFuture::join).collect(toList());
}