经典伴读_java8实战_一网打尽

经典伴读系列文章,想写的不是读书笔记,也不是读后感,自己的理解加上实际项目中运用,让大家3-4天读懂这本书

预备知识:

  • 方法引用,Lambda相关知识点,特别是函数描述符的概念和使用。
    请参考 《经典伴读_Java8实战_Lambda》
  • 归约操作reduce,数值流,构建流的使用。
    请参考《经典伴读_java8实战_Stream基础》
  • 收集器,并行流的使用。
    请参考《经典伴读_java8实战_Stream高级》

前面的文章中已经学习了Lambda和Stream,它们是java8中最主要的特性,剩下知识点不多了,让我们再接再厉,将它们一网打尽吧。

八、默认方法

1、什么是默认方法
从学习java开始,大家都知道接口用于规范,约束子类的行为。那么现在我们从Comparator接口源码中,学习下它还有那些用途。

@FunctionalInterface
public interface Comparator {
    int compare(T o1, T o2);
    boolean equals(Object obj);
    
    default Comparator reversed() {
        return Collections.reverseOrder(this);
    }
    default Comparator thenComparing(Comparator other) {
        Objects.requireNonNull(other);
        return (Comparator & Serializable) (c1, c2) -> {
            int res = compare(c1, c2);
            return (res != 0) ? res : other.compare(c1, c2);
        };
    }
    .......
    public static > Comparator reverseOrder() {
        return Collections.reverseOrder();
    }
    @SuppressWarnings("unchecked")
    public static > Comparator naturalOrder() {
        return (Comparator) Comparators.NaturalOrderComparator.INSTANCE;
    }
    .......

由代码得知,接口的方法从签名上分为三种:

  • 抽象方法,无方法体,必须由子类实现。也是最主要的接口方法,如compare,equals。
  • 默认方法,使用default修饰,无需子类实现,常利用抽象方法,或者静态方法实现高级功能,类似于模版方法,如reversed,thenComparing等。
  • 静态方法,和类中的静态方法一样,有实现代码,常用于静态工厂方法,生成该接口对象,如reverseOrder,naturalOrder等,甚至可以将工具类和接口合二为一,如Collections和Collection。

如果你的工作中要做一些框架构建方面的事情,default方法一定不会陌生,大家有没有想过,在mybatis开发中,将所有的数据访问操作封装在mapper接口里,这在java8之前可是做不到的。

2、默认方法与多继承
"多接口实现"加上"default方法",java8有了多继承。
(1)实现的多接口中有相同签名的默认方法时会冲突。

    public interface Print1 {
        void print(Integer t);
        default void changeAndPrint(Integer t) {
            print(++t);
        }
    }

    public interface Print2{
        void print(Integer t);
        default void changeAndPrint(Integer t) { //和Print1签名相同,被多继承会冲突
            print(--t);
        }
    }
    
    public static class PrintImpl implements Print1, Print2 {
        @Override
        public void print(Integer t) {
            System.out.println(t);
        }
    }

PrintImpl报错:PrintImpl inherits unrelated defaults for changeAndPrint(Integer) from types Print1 and Print2

如何解决冲突?
实现类需要明确关联哪个默认方法:

        @Override
        public void changeAndPrint(Integer t) {
            Print1.super.changeAndPrint(t);
        }

(2)默认方法也可以继承被重写,和多态相同,调用方法时,实现类,或者子接口优先。

九、Optional

我们平时遇到最多的异常是空指针异常NullPointException,最不想写的代码是逐层判空。
问题:在学生管理系统中,根据学生查询班主任的姓名。
1、准备实体:

    //学生
    public static class Student {
        private String name;
        private Classes classes; //班级

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Classes getClasses() {
            return classes;
        }

        public void setClasses(Classes classes) {
            this.classes = classes;
        }
    }

    //班级
    public static class Classes {
        private String name;
        private Teacher teacher; //班主任

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Teacher getTeacher() {
            return teacher;
        }

        public void setTeacher(Teacher teacher) {
            this.teacher = teacher;
        }
    }

    //老师
    public static class Teacher {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

2、为了不报空指针异常,三种方式实现
(1)嵌套if逐层判空,会让代码变得臃肿,影响阅读。

    public static String getTeacherName(Student student) {
        if (student != null) {
            Classes classes = student.getClasses();
            if (classes != null) {
                Teacher teacher = classes.getTeacher();
                if (teacher != null) {
                    return teacher.getName();
                }
            }
        }
        return "Unknown";
    }

(2)提前判空多点退出,可以减少嵌套层数,但会增加了维护成本,有没有更加优雅的解决方案?

    public static String getTeacherName(Student student) {
        if (student == null) {
            return "Unknown";
        }

        if (student.getClasses() != null) {
            return "Unknown";
        }

        if (student.getClasses().getTeacher() == null) {
            return "Unkong";
        }
        return student.getClasses().getTeacher().getName();
    }

(3)使用Optional

    public static String getTeacherName(Student student) {
        return Optional.ofNullable(student)
                .map(Student::getClasses)
                .map(Classes::getTeacher)
                .map(Teacher::getName)
                .orElse("Unknown");
    }

Optional就像一个特殊的容器,最多存放一个值。Optional可以进行类似流的操作,常用方法同样分为三类:

  • 创建Optional对象
    ofNullable:把值包装成Optional对象,值可以为空,
    of:把值包装成Optional对象,值为空抛异常。
    empty:创建一个空的Optional对象。

  • 中间链操作
    map:当值为对象时,映射值的属性
    flatmap:当值的属性是一个Optional时使用,将多层嵌套的Optional属性扁平映射一个单层Optional属性。
    filter:判断值是否满足提供的谓词(一个返回true/false的函数),不满足返回空的Optional。

  • 终端操作
    orElse:返回值,如果为空则返回默认值。
    get:返回值,如果为空则抛异常。
    ifPresent:如果值存在,执行方法调用。
    isPresent:判断值是否存在。

十、新的日期和时间API

新版的日期和时间API中,有哪些主要变化:

  • 时间对象是不可变的:LocalDate,LocalTime,LocalDateTime
  • 使用新的类区分对人和机器不同的时间表示:Instant
  • 使用新的类处理时间间隔:Duration,Period
  • 使用新的线程安全的时间转换类:DateTimeFormatter
  • 使用新的类精细操作日期:TemporalAdjuster

虽然新的API有了本质的变化,但SpringMVC,Mybatis主流版本不支持(默认情况下),阻碍了项目中使用。这里不再一一介绍,分四个常见场景对比新老API:
1、时间转字符串类型

     public static void testDate2String() {
        //使用传统Date
        Date now1 = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String nowStr = sdf.format(now1); //非线程安全
        System.out.println("Date: " + nowStr);

        //使用LocalDateTime
        LocalDateTime now2 = LocalDateTime.now();
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        nowStr = now2.format(dtf); //线程安全
        System.out.println("LocalDateTime: " + nowStr);
    }

Date: 2022-06-27 11:19:35
LocalDateTime: 2022-06-27 11:19:35

2、字符串转时间类型

    public static void testString2Date() {
        //使用传统Date
        String dateStr = "2022-06-27 09:56:43";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        try {
            Date nowDate = sdf.parse(dateStr); //会抛异常
            System.out.println("Date: " + nowDate);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        //使用LocalDateTime
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime nowDateTime = LocalDateTime.parse(dateStr, dtf); //不抛异常
        System.out.println("LocalDateTime: " + nowDateTime);
    }

Date: Mon Jun 27 09:56:43 CST 2022
LocalDateTime: 2022-06-27T09:56:43

3、计算剩余时间

    public static void testDuration() throws ParseException {
        //使用传统Date
        String targetStr = "2022-11-11 00:00:00";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date target = sdf.parse(targetStr);
        long targetTime = target.getTime();
        long day = (targetTime - System.currentTimeMillis()) / (24 * 60 * 60 * 1000); //没有提供方法获取,需要计算
        System.out.println("Date:" + day + "天");

        //使用LocalDateTime
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime targetDateTime = LocalDateTime.parse(targetStr, dtf);
        Duration d1 = Duration.between(LocalDateTime.now(), targetDateTime);
        day = d1.toDays(); //提供方法获取
        System.out.println("LocalDateTime:" + day + "天");
    }

Date:136天
LocalDateTime:136天

4、计算结束时间

    public static void testEndTime() throws ParseException {
        //使用传统Date
        int addTime = 15; //经过15天
        String targetStr = "2022-06-18 00:00:00";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date beginDate = sdf.parse(targetStr);

        Calendar calendar = Calendar.getInstance();
        calendar.setTime(beginDate);
        calendar.add(Calendar.DAY_OF_MONTH, addTime); //Date无法自己处理,需要依靠Calendar
        Date endDate = calendar.getTime();
        System.out.println("Date: " + endDate);

        //使用LocalDateTime
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime beginDateTime = LocalDateTime.parse(targetStr, dtf);
        LocalDateTime endDateTime = beginDateTime.plus(addTime, ChronoUnit.DAYS);//LocalDateTime自己处理
        System.out.println("LocalDateTime: " + endDateTime);
    }

Date: Sun Jul 03 00:00:00 CST 2022
LocalDateTime: 2022-07-03T00:00

十一、并行流的实现

在《经典伴读_java8实战_Stream高级》中介绍的并行流,就是在CPU多核上并行。并行流依靠Fork/Join框架和Spliterator一起实现。让我们回到求和问题:1+2+3+4+....+49+50=?,看看它们如何实现并行。

Fork/Join

1、Fork/Join框架的目的是以递归方式将大任务拆分成更小的任务并行执行,然后将每个子任务的结果合并起来生成整体结果,在算法中也叫分治法。拆分出来的子任务分配给线程池(ForkJoinPool)中的工作线程。

2、定义子任务
(1)需要返回结果的子任务,继承RecursiveTask。

public abstract class RecursiveTask extends ForkJoinTask {
    protected abstract V compute();

(2)不需要返回结果的子任务,继承RecursiveAction。

public abstract class RecursiveAction extends ForkJoinTask {
    protected abstract void compute();

它们都有的compute()方法,里面是子任务计算逻辑,大致如下:

    if(任务足够小或不可分) {
        顺序计算该任务
    } else {
        将任务拆分成两个子任务,放入线程等待队列
        触发子任务执行(递归),等待所有子任务完成
        合并子任务结果
    }

3、子任务拆分与触发
(1)fork():创建子任务后,调用fork方法,可以将任务放入工作线程的等待队列中。(异步)
(2)join():子任务调用join方法,触发任务执行,等待任务执行结果。(同步)
(3)工作窃取:拆分的所有的子任务平均分配到ForkJoinPool中所有线程上,每一个工作线程都有一个双向等待队列,默认情况下,后进先出LIFO,即调用fork(),从队首(Top端)放入任务,调用join(),也是从队首(Top端)取出下一个任务执行。当某一个线程早早的完成了分配给它的所有任务,则会选择另一个线程,从它的等待队列的尾巴(Base端)“偷走”一个任务执行。这个过程一直继续下去,直到所有任务都执行完毕。

4、使用Fork/Join框架并行计算1+2+3+4+....+49+50=?

public class SumTask extends RecursiveTask {
    private long[] nums;
    private int start;
    private int end;

    public static final long THRESHOLD = 10;

    public SumTask(long[] nums) {
        this(nums, 0, nums.length);
    }

    public SumTask(long[] nums, int start, int end) {
        this.nums = nums;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        int length = end - start;
        if (length < THRESHOLD) { //任务足够小或不可分
            long sum = 0;
            for (int i = start; i < end; i++) {
                sum += nums[i]; //顺序计算该任务
            }
            return sum;
        }

        SumTask leftTask = new SumTask(nums, start, start + length/2); //拆分成子任务
        leftTask.fork(); //放入线程等待队列
        SumTask rightTask = new SumTask(nums, start + length/2, end); //拆分成子任务
        rightTask.fork(); //放入线程等待队列

        Long leftResult = leftTask.join(); //触发子任务执行,等待任务返回
        Long rightResult = rightTask.join(); //触发子任务执行,等待任务返回
        System.out.println(leftResult + "+" + rightResult);
        return leftResult + rightResult; //合并子任务结果
    }

    public static void main(String[] args) {
        long[] nums = LongStream.rangeClosed(1, 50).toArray(); //创建1到50数组

        ForkJoinTask task = new SumTask(nums); //创建原始任务
        Long result = new ForkJoinPool().invoke(task); //创建任务池,并启动原始任务
        System.out.println("结果:" + result);
    }
}

243+329
93+154
171+207
21+57
378+572
78+247
325+950
结果:1275

从上面的结果可以看出Fork和Join的过程:


image.png

Spliterator

1、Spliterator简介
Spliterator是一个可拆分的迭代器,为了并行执行而设计。

    public interface Spliterator {
        boolean tryAdvance(Consumer action);
        Spliterator trySplit();
        long estimateSize();
        ......
        default void forEachRemaining(Consumer action)
        ......

tryAdvance:获取下一个元素(类似next),进行逻辑处理,返回是否有下一个元素(类似hasNext)。
trySplit:把一些的元素(默认一半)划分出去作为第二个Spliterator返回。
estimateSize:估计还有多少元素没有迭代。
forEachRemaining:迭代剩余元素。

2、使用Spliterator.trySplit方法代替Fork/Join框架中拆分逻辑

public class SpliteratorSumTask extends RecursiveTask {
    Spliterator spliterator;
    public static final long THRESHOLD = 10;
    public SpliteratorSumTask(Spliterator spliterator) {
        this.spliterator = spliterator;
    }

    @Override
    protected Long compute() {
        if(spliterator.estimateSize() < THRESHOLD) {
            //lambda只能使用final变量,无法使用类似sum+=i,因此这里用累加器累加
            Accumulator accumulator = new Accumulator();
            spliterator.forEachRemaining(accumulator::add);
            return accumulator.get();
        }
        //代替SumTask leftTask = new SumTask(nums, start, start + length/2),使用trySplit拆分一半元素
        Spliterator remainSpliterator = spliterator.trySplit(); 

        SpliteratorSumTask rightTask = new SpliteratorSumTask(spliterator);
        SpliteratorSumTask leftTask = new SpliteratorSumTask(remainSpliterator);

        leftTask.fork();
        rightTask.fork();
        Long leftResult = leftTask.join();
        Long rightResult = rightTask.join();
        System.out.println(leftResult + "+" + rightResult);
        return leftResult + rightResult;
    }

    //累加器
    public static class Accumulator {
        private long total = 0;
        public void add(long value) {
            total+=value;
        }
        public long get() {
            return total;
        }
    }

    public static void main(String[] args) {
        long[] nums = LongStream.rangeClosed(1, 50).toArray();
        Spliterator spliterator = Arrays.stream(nums).spliterator();

        ForkJoinTask task = new SpliteratorSumTask(spliterator);
        long result = new ForkJoinPool().invoke(task);
        System.out.println("结果:" + result);
    }
}

21+57
93+154
243+329
171+207
78+247
378+572
325+950
结果:1275

控制台输出完全一致。

十二、CompletableFuture组合式编程

并行是利用CPU多核的能力同时执行任务的能力,但硬件资源总是有限的,如何继续提高处理能力,比如当请求接口需要等待较长时间时,能否不阻碍主流程继续进行,这类减少资源等待的能力,就是并发,可以简单理解为异步。这一章的主题CompletableFuture就可以理解为异步操作。

1、回顾线程池
线程池顾名思义,是集中管理线程资源的地方,有任务来的时候就从池子里取出线程执行。让我们快速回顾下:

  • Runnable和Callable区别
  • Future和FutureTask关系
  • Executor,Executors,ExecutorService,ThreadPoolExecutor区别

希望一个例子可以回答上面3个问题。

        //1.无返回值的线程
        Runnable runnable = () -> {
            System.out.println("hello");
        };
        new Thread(runnable).start();

        //2.有返回值的线程
        Callable callable = () -> {
            return "hello";
        };
        //FutureTask同时实现了Runnable, Future,
        // 因此可以作为适配器,将Callable适配到Runnable上
        FutureTask futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start(); //Thread需要Runnable
        System.out.println(futureTask.get());//get()需捕获异常,这里抛出了

        //3.无返回值的线程池
        //4种内置线程池工厂方法:
        // newFixedThreadPool,newCachedThreadPool,
        // newSingleThreadExecutor,newScheduledThreadPool
        ExecutorService threadPool = Executors.newCachedThreadPool();
        threadPool.execute(runnable);
        threadPool.shutdown();

        //4.有返回值的线程池
        //如果内置线程池无法满足,可以自定义参数。
        threadPool = new ThreadPoolExecutor(
                5,
                5,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue(64)
        );
        Future future = threadPool.submit(callable);
        System.out.println(future.get());//get()需捕获异常,这里抛出了
        threadPool.shutdown();

2、CompletableFuture与Future
使用Future可以获取线程中的返回值,但也有两个主要缺陷:

  • 通过主动获取的方法get(),阻塞调用线程获取返回值,这和我们印象中的异步似乎不同,如ajax的回调方法不会影响主线程。
  • 对于多个异步接口需要串行(前后依赖)或并行,甚至组合时,Future不容易开发。

而CompletableFuture可以使用函数式的思想,可以编织异步调用完成后的所有操作。我们先看个简单问题,直观感受下它们的不同。
问题:计算(1 + 2) * (3 + 4)
(1)准备代码

    //模拟服务器请求耗时
    public static void delay() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
        
    public static int add(int a, int b) {
        delay(); 
        return a + b;
    }

    public static int multiply(int a, int b) {
        delay();
        return a * b;
    }

(2)使用Future

    private static final ExecutorService threadPool = Executors.newCachedThreadPool();
    public static void useFuture() {
        Future f1 = threadPool.submit(() -> add(1, 2)); //先算 1+2=3
        Future  f2 = threadPool.submit(() -> add(3, 4)); //再算 3+4=7
        try {
            int result = multiply(f1.get(), f2.get()); //最后算 3*7=21
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

21
耗时:2008ms

结果分析:两个add方法异步执行,get方法获取结果时阻塞1s,multiply方法执行1s,主线程共花费2s,注意这里multiply依赖于前面add的结果,因此不需要再开线程。

(3)使用CompletableFuture

    CompletableFuture.supplyAsync(() -> add(1, 2), threadPool) //1+2=3
                .thenCombine(
                        CompletableFuture.supplyAsync(() -> add(3, 4), threadPool), //3+4=7
                        (r1, r2) -> multiply(r1, r2)) //3*7=21
                .thenAccept(System.out::println);

耗时:4ms
21

结果分析:两个add方法异步执行,在第1个add线程中等待两个结果,接着执行multiply方法,最后输出。主线程花费4ms,注意所有操作都在第1个add线程,不在主线程。

3、异步操作
(1)创建异步操作
CompletableFuture可以作为线程传递返回值或异常的桥梁,也可以直接创建异步操作(推荐),借助简单问题1+2,看下它们的不同:

        //CompletableFuture搭配线程或线程池一起完成异步操作
        CompletableFuture cf = new CompletableFuture<>();
        new Thread(() -> {
            try {
                int r = add(1, 2);
                cf.complete(r); //放入返回值
            } catch (Exception e) {
                cf.completeExceptionally(e); //抛出异常,get可以捕捉
            }
        }).start();
        //获取返回值,和Future一样阻塞(这里为了缩短代码将已将异常抛出)
        System.out.println(cf.get());

        //CompletableFuture直接创建异步操作
        CompletableFuture cf2 = CompletableFuture.supplyAsync(() -> add(1, 2), threadPool);
        System.out.println(cf2.get());

CompletableFuture直接创建异步操作的方法除了supplyAsync还有runAsync,区别在于后者没有返回值。

    public static  CompletableFuture supplyAsync(Supplier supplier,
                                                       Executor executor)
    public static CompletableFuture runAsync(Runnable runnable,
                                                   Executor executor)

(2)获取异步操作返回值
get和join都可以获取异步操作返回值,它们有什么不同?
问题:计算1 * 1 + 2 * 2 + 3 * 3 +.....+10 * 10

        List> numCompletableFutures = IntStream.rangeClosed(1, 10)
                .mapToObj(t -> CompletableFuture.supplyAsync(() -> multiply(t, t), threadPool))
                .collect(Collectors.toList());
        int sum = numCompletableFutures.stream()
                .map(CompletableFuture::join) //映射为返回值
                .reduce(0, Integer::sum);
        System.out.println(sum);

首先使用流创建数列1到10,然后使用流的映射分别创建每个数的异步操作(计算n * n),最后通过流的归约将所有返回值求和。最后一个map使用CompletableFuture.join方法获取每个返回值而不是使用get方法。这是因为join方法不抛异常,非常适合流的操作。另外,如果只使用一个流完成链式操作,如直接写成Stream.mapToObj.map,那么所有异步操作都会变成串行。

    public T join() 
    public T get() throws InterruptedException, ExecutionException
    public T get(long timeout, TimeUnit unit)

(3)异步操作数据处理
单个异步操作返回后的数据处理,主要就两种:无返回值消费 和 有返回值加工:

        CompletableFuture
                .supplyAsync(() -> add(1, 2), threadPool)
                .thenApply(t -> "处理数据" + t) //参数Function用于加工
                .thenAccept(System.out::println); //参数Consumer用于消费

(4)异步操作组合

        //计算(1 + 2) * 3
        CompletableFuture cf = CompletableFuture
                    .supplyAsync(() -> add(1, 2), threadPool)
                    .thenCompose(t -> CompletableFuture.supplyAsync(() -> multiply(t, 3), threadPool));
        System.out.println(cf.join());

        //计算(1 * 2) + (3 * 4)
        cf = CompletableFuture
                    .supplyAsync(() -> add(1, 2), threadPool)
                    .thenCombine(CompletableFuture.supplyAsync(() -> add(3, 4), threadPool),
                            (r1, r2) -> multiply(r1, r2));
        System.out.println(cf.join());

当多个异步操作需要前后依赖时使用thenCompose,需要并行时使用thenCombine。

十三、函数式编程

至此,java8的所有新增特性已经讲完,这些特性都围绕着一个主题-函数式编程,那么究竟什么是函数式编程?书从这个问题开始,让我们回答它作为结束。
1、在传统java编程世界中,最知名的一句话,一切都是对象(并非完全是),当调用方法时,有可能修改对象状态,有可能纯粹的逻辑计算。无法判断,也就无法排除方法之间是否会相互影响,从而导致各种并发问题。
2、有没有一种方式,就像数学函数,只做纯粹的逻辑计算,只有一个入口,一个出口,且不产生副作用-修改函数之外的任何数据(共享变量)。这就是java8的函数(和方法概念区分开)。函数式编程就是面向函数编程,将函数作为编程的最小单位(不再是对象)。
3、要让传统的java支持函数式编程,JDKer该怎样做呢?
第1步,要找一个东西表示函数,最接近的就是接口呢,但接口中有很多方法,我们想要的是一个函数就是一个最小的编程单位,于是规定函数就是只有一个方法的接口,没错,这就是函数式接口@FunctionalInterface
第2步,定义了函数就像声明了类,接着我们要创建函数,存储到变量,让函数像值一样传递。于是,便有了Lambda表达式,这就是函数的值。
第3步,有了函数,如何调用,再不能通过对象点操作了,函数才是主角。我们需要给数据直接附加上操作,也就是函数(不通过对象),接着有了流Stream。注意,流操作之间的数据传递是值传递,是拷贝,不会让函数相互影响。
第4步,数学函数执行过程中也会有异常,如计算1/0,如果出现异常,就会中断函数,这就相当于有了其他出口,于是就有了Optional,Stream也就不能向外抛异常了。
....
是不是这样就把整本书串联了起来,由此可见,函数式编程和面向对象编程主要区别:
(1)不修改共享变量
(2)不抛异常
(3)不进行IO操作
它们都是开发者手中的工具,合适的场景选用合适的工具,以上就是《Java8实战》中主要内容,让我们下一本经典再见。

你可能感兴趣的:(经典伴读_java8实战_一网打尽)