java8新特性原理以及实战应用-lambda函数式编程与日期篇


1:简介

目前截止2021年9月java最新的jdk版本为17,而我们目前大部分公司现在的java项目都还在运行的是java8的版本,对于我们目前的项目而言,java8已经能够满足我们大部分工作中的应用场景。而且截止目前为止oracle公司还仍在继续维护着java8的更新操作。其实java8早在2014年就已经推出了发型版。那么java8都有哪些新特性呢?我们是否都一一掌握了?我想如果我们都掌握并熟知了java8的新特性以及应用技巧之后,一定会对我们日常的开发效率会有或多或少的帮助,提升我们的撸码效率,避免踩坑。规避项目中一些不必要的风险。总之我们可以这么想,如果我们都不了解java8新特新的魅力,又怎会体验到java14的快乐。(哈哈😝,题外话)

2:新特性介绍与序言

在开始介绍java8新特性之前,必须先引出一篇oracle官方非常具有权威的java8新特性(相对于java8以前的版本)其实可以参考我们如下这张图
JDK8新特性.png
简单介绍jdeps类依赖分析器工具的使用。着重讲解Lambda特性和Stram、日期,配合起来用可以极大提高生产力,写出高效率、干净、简洁的代码。

3:jdeps工具的使用

熟悉java的小伙伴应该清楚jdk在默认给我们自带了一些方便的tools执行工具如:
jps:是JDK 1.5提供的一个显示当前所有java进程pid的命令,简单实用,非常适合在linux/unix平台上简单察看当前java进程的一些简单情况。

javac: 该工具可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于Java虚拟机的字节码.

java: 执行启动java服务,可以执行.class后缀文件或者是jar file

javap:javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。

jjs 等等…..
这样的命令工具有好多,我们接下来看下jdeps

//将java文件编译为class文件
javac /Users/xiaozhangge/ideaProjects/java8feature/src/main/java/com/xes/sample/function/OptionalTest.java
//jdeps命令执行可以看到class的依赖关系
jdeps /Users/xiaozhangge/ideaProjects/java8feature/src/main/java/com/xes/sample/function/OptionalTest.class
OptionalTest.class -> /Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/rt.jar
   com.xes.sample.function (OptionalTest.class)
      -> java.io
      -> java.lang
      -> java.time
      -> java.time.format
      -> java.time.temporal
//不仅可以分析class文件还可以分析jar包,感兴趣的小伙伴可以自行尝试

4:Stream流编程实战

现在我们开始进入jdk1.8 stream流编程,这是jdk1.8中最大的亮点,也是最有用的一个特性。
概念:
首先stream是一个高级的迭代器,单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。它不是一个数据结构,也不是一个集合,它不会存放数据,stream最关注的事情
是如何将数据进行高效的处理,它其实就是将数据在流水线一样的环境中进行处理(parallelStream)。
而和传统迭代器又不同的是,Stream可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个item读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,
其中每一个都在不同的线程中处理,然后将结果一起输出。Stream的并行操作依赖于Java7中引入的Fork/Join框架(JSR166y)来拆分任务和加速处理过程。

4.1:初识Stream:

import java.util.stream.IntStream;

public class StreamDemeo2 {
    public static void main(String[] args) {
        int[] nums = {3, 1, 2};
        //使用Stream内部迭代
        //map就是中间操作(返回Stream的操作)
        //sum就是终止操作
         int sum = IntStream.of(nums).map(i -> i * 2).sum();
        System.out.println(sum);
        int intNum = IntStream.of(nums).map(StreamDemeo2::integerNum).sum();
        System.out.println(intNum);
        System.out.println("惰性求值就是最终没有调用的情况下,中间操作不会被执行");
        IntStream.of(nums).map(StreamDemeo2::integerNum);
    }

    /**
     * 求值运算
     *
     * @param i
     * @return
     */
    public static int integerNum(int i) {
        System.out.println("执行了乘以2操作");
        return i * 2;
    }
}

通过以上的例子我们可以看到,如果方法返回的数据类型是stream流对象,那么默认程序是不会做任何操作的,只有真正最后有取值操作才会进行中间过程的逻辑运算。

4.2:Stream流编程创建:

类别 相关方法
集合 Collection.stream/parallelStream
数组 Arrays.stream
数字Stream IntSream/LongStream
Random.ins/longs/doubles
自己创建 Stream.generate/iterate
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class StreamDemo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("");
        //从结合创建'
        list.stream();
        list.parallelStream();
        //从数组创建
        Arrays.stream(new int[]{2, 3, 4});
        //创建数字流
        IntStream.of(1, 2, 3);
        IntStream.rangeClosed(1, 10);
        //使用random创建一个无限流
        new Random().ints().limit(10);
        Random random=new Random();
        //自己产生流
        Stream.generate(()->random.nextInt()).limit(1000);
    }
}

4.3:Stream流编程-中间操作

相关方法 描述
无状态操作 map/mapToXxx 无状态就表示我当前的操作跟其它元素的前后没有依赖关系
flatMap/flatMapToXxx
filter
peek
unordered
有状态操作 distinct 有状态的结果需要依赖于其它元素
sorted
limit/skip
import java.util.Random;
import java.util.stream.Stream;

public class StreamDemo {
    public static void main(String[] args) {
        String str = "my name is 007";
        //输出每个单词的长度
        Stream.of(str.split(" ")).map(world -> world.length()).forEach(System.out::println);
        //输出符合长度的单词
        Stream.of(str.split(" ")).filter(world -> world.length() > 3).forEach(System.out::println);
        //长度大于3单词的长度分别有哪些
        Stream.of(str.split(" ")).filter(world -> world.length() > 3)
                .map(world -> world.length()).forEach(System.out::println);
        //flatMap操作
        Stream.of(str.split(" ")).flatMap(s -> s.chars().boxed()).forEach(System.out::println);
        //peek操作 peek主要用于debug 是中间操作 而forEach是终止操作
        Stream.of(str.split(" ")).peek(System.out::println).forEach(System.out::println);
        //limit使用主要用于无限流
        new Random().ints().filter(i -> i > 100 && i < 10000).limit(10).forEach(System.out::println);
    }
}

4.4:Stream流编程-终止操作

相关方法 描述
非短路操作 forEach/forEachOrderd 只有将所有的数据进行汇总完毕才会中断流程
collect/toArray
reduce
min/max/count
短路操作 findFirst/findAny 我们不需要所有的结果计算完就可以结束这个流的操作
allMatch/anyMatch/noneMatch
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StreamDemo {
    public static void main(String[] args) {
        String str = "my name is 007";
        System.out.println("=======================非短路操作====================");
        //使用并行流操作
        str.chars().parallel().forEach(i -> System.out.println("i = " + (char) i));
        //使用forEachOrdered来保证有序
        str.chars().parallel().forEachOrdered(i -> System.out.println("i = " + (char) i));
        //收集list
        List<String> collect = Stream.of(str.split(" ")).collect(Collectors.toList());
        //reduce结果集
        Optional<String> reduce = Stream.of(str.split(" ")).reduce((s1, s2) -> s1 + "|" + s2);
        System.out.println(reduce.orElse(""));
        //max操作
        Stream.of(str.split(" ")).max(Comparator.comparingInt(String::length));

        System.out.println("=======================短路操作====================");
        OptionalInt first = new Random().ints().findFirst();
        System.out.println(first.orElse(0));
    }
}

4.5:Stream流编程-并行流

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class StreamDemo {
    public static void main(String[] args) {
        //同步执行
        IntStream.range(0, 10).peek(StreamDemo::debug).count();
        //并行流执行
        IntStream.range(0, 10).parallel().peek(StreamDemo::debug).count();
        //先并行在串行
        //结论:多次调用parallel和sequential,以最后一次调用为准
        IntStream.range(0, 10)
                //调用parallel产生并行流
                .parallel().peek(StreamDemo::debug)
                //调用sequential产生串行流
                .sequential().peek(StreamDemo::debug2).count();

        //并行流使用的线程池为ForkJoinPool.commonPool
        //默认线程池数是当前机器的cpu个数,可以通过以下参数进行修改线程数,我们可以通过java.util.concurrent.ForkJoinPool类的
        //makeCommonPool()方法进行参考:
        System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20");
        IntStream.range(0, 100).parallel().peek(StreamDemo::debug).count();

        //使用自己的线程池,不使用默认的,防止任务被阻塞
        ForkJoinPool pool = new ForkJoinPool(20);
        pool.submit(() -> IntStream.range(1, 100).parallel().peek(StreamDemo::debug).count());
        pool.shutdown();
        synchronized (pool){
            try {
                pool.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void debug(int i) {
        System.out.println(Thread.currentThread().getName() + "debug" + i);
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void debug2(int i) {
        System.err.println("debug" + i);
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

4.6:Stream流编程-收集器

收集器言外之意就是要将我们流处理的数据收集起来。它可以返回List、Map等。使用收集器的地方比较多,比如sum/count/distinct/group by

import lombok.Data;
import org.apache.commons.collections4.MapUtils;

import java.util.Arrays;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

/**
 * 性别
 */
enum Gender {
    MALE, FEMALE
}

/**
 * 班级
 */
enum Grade {
    ONE, TWO, THREE, FOUR;
}

/**
 * 学生 对象
 */
@Data
class Student {
    /**
     * 姓名
     */
    private String name;

    /**
     * 年龄
     */
    private int age;

    /**
     * 性别
     */
    private Gender gender;

    /**
     * 班级
     */
    private Grade grade;

    public Student(String name, int age, Gender gender, Grade grade) {
        super();
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.grade = grade;
    }

    @Override
    public String toString() {
        return "[name=" + name + ", age=" + age + ", gender=" + gender
                + ", grade=" + grade + "]";
    }
}

public class StreamDemo {
    public static void main(String[] args) {
        // 测试数据
        List<Student> students = Arrays.asList(
                new Student("小明", 10, Gender.MALE, Grade.ONE),
                new Student("大明", 9, Gender.MALE, Grade.THREE),
                new Student("小白", 8, Gender.FEMALE, Grade.TWO),
                new Student("小黑", 13, Gender.FEMALE, Grade.FOUR),
                new Student("小红", 7, Gender.FEMALE, Grade.THREE),
                new Student("小黄", 13, Gender.MALE, Grade.ONE),
                new Student("小青", 13, Gender.FEMALE, Grade.THREE),
                new Student("小紫", 9, Gender.FEMALE, Grade.TWO),
                new Student("小王", 6, Gender.MALE, Grade.ONE),
                new Student("小李", 6, Gender.MALE, Grade.ONE),
                new Student("小马", 14, Gender.FEMALE, Grade.FOUR),
                new Student("小刘", 13, Gender.MALE, Grade.FOUR));

        // 得到所有学生的年龄列表
        // s -> s.getAge() --> Student::getAge , 不会多生成一个类似 lambda$0这样的函数
        Set<Integer> ages = students.stream().map(Student::getAge)
                .collect(Collectors.toCollection(TreeSet::new));
        System.out.println("所有学生的年龄:" + ages);

        // 统计汇总信息
        IntSummaryStatistics agesSummaryStatistics = students.stream()
                .collect(Collectors.summarizingInt(Student::getAge));
        System.out.println("年龄汇总信息:" + agesSummaryStatistics);

        // 分块
        Map<Boolean, List<Student>> genders = students.stream().collect(
                Collectors.partitioningBy(s -> s.getGender() == Gender.MALE));
        // System.out.println("男女学生列表:" + genders);
        MapUtils.verbosePrint(System.out, "男女学生列表", genders);

        // 分组
        Map<Grade, List<Student>> grades = students.stream()
                .collect(Collectors.groupingBy(Student::getGrade));
        MapUtils.verbosePrint(System.out, "学生班级列表", grades);

        // 得到所有班级学生的个数
        Map<Grade, Long> gradesCount = students.stream().collect(Collectors
                .groupingBy(Student::getGrade, Collectors.counting()));
        MapUtils.verbosePrint(System.out, "班级学生个数列表", gradesCount);
    }

5:从入门开始深度理解lambda

先从几段代码中进行简单去理解:

----predicate test---------
//断言测试,输入任意类型,输出boolean
Predicate<Integer> predicate = i -> i > 30;
System.out.println(predicate.test(40));
//只接收int值的断言
IntPredicate intPredicate = i -> 5 > i;
System.out.println(intPredicate.test(3));
//接收两个参数的断言判断
BiPredicate<Integer, Integer> biPredicate = (i1, i2) -> i1 + i2 > 5;
System.out.println(biPredicate.test(1, 4));
----Consumer test简单版使用---------
//消费函数,只有输入无任何返回,箭头左边为方法入参,箭头右边为函数执行过程
Consumer<String> consumer = content -> System.out.println(content);
consumer.accept("小张哥");
//由于左侧的输入参数和右侧的输出参数相同,所我们完全可以使用方法引用的方式继续精简其输出
Consumer<String> simplify = System.out::println;
simplify.accept("简化版");

5.1:日常开发中高级使用—方法引用:

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntUnaryOperator;
import java.util.function.UnaryOperator;

public class Dog {
    private String name = "小黄";
    private int food = 10;

    /**
     * 狗叫的静态方法
     *
     * @param dog
     */
    public static void bark(Dog dog) {
        System.out.println(dog + "叫了");
    }

    /**
     * 吃狗粮
     *
     * @param num
     * @return 还剩多少斤
     */
    public int eat(int num) {
        System.out.println("吃了" + num + "斤狗粮");
        this.food -= num;
        return this.food;
    }

    @Override
    public String toString() {
        return this.name;
    }

    public static void main(String[] args) {
        //静态方法引用
        Consumer<Dog> dogConsumer = Dog::bark;
        dogConsumer.accept(new Dog());
        //非静态方法使用对象实例引用,eat方法符合输入/输出参数的形式
        Function<Integer, Integer> eatFunction = new Dog()::eat;
        System.out.println("eatFunction还剩" + eatFunction.apply(3) + "斤狗粮");
        //由于输入和输出的参数类型一致,我们可以替换为一元函数形式调用
        UnaryOperator<Integer> unaryDog = new Dog()::eat;
        System.out.println("unaryDog还剩" + unaryDog.apply(3) + "斤狗粮");
        //当然jdk1.8针对输入和输出参数相同的操作还指定了具体类型的一元函数,这样我们在操作医院还是就不需要再指明参具体参数的泛型
        IntUnaryOperator intUnaryDog = new Dog()::eat;
        System.out.println("intUnaryDog还剩" + intUnaryDog.applyAsInt(3) + "斤狗粮");
    }
}

反思:上面的代码示例中调用静态方法可以使用Dog::bark;进行引用方法的实例。那么非静态的方法我们该如何进行使用Dog::eat方式来进行引用呢?这里牵涉到不同的语言里面对this的实现方法。我们知道静态方法和实例方法的区别是实例方法有this,静态方法没有。java里面是怎么样实现this的呢?
java里面默认把this作为参数,放到实例(非静态)方法的第一个参数
就是说

public int eat(int num) {
        System.out.println("吃了" + num + "斤狗粮");
        this.food -= num;
        return this.food;
    }

编译之后和下面这样的代码编译之后是一样的!

public int eat(Dog this,int num) {
        System.out.println("吃了" + num + "斤狗粮");
        this.food -= num;
        return this.food;
    }

执行 javap -c -s -v -l Dog.class 其中我们看到如下:
非静态方法this原理.png
本地变量表查看:
非静态方法本地变量表.png
所以,我的理解,java里面的所有方法都是静态方法,只是有些方法有this变量,有些没有。
所以,成员方法我们也可以写成静态方法的方法引用。如下:

import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

public class Dog {
    private String name = "小黄";
    private int food = 10;

    public Dog() {

    }

    public Dog(String name) {
        this.name = name;
    }

    /**
     * 狗叫的静态方法
     *
     * @param dog
     */
    public static void bark(Dog dog) {
        System.out.println(dog + "叫了");
    }

    /**
     * 吃狗粮
     *
     * @param num
     * @return 还剩多少斤
     */
    public int eat(int num) {
        System.out.println("吃了" + num + "斤狗粮");
        this.food -= num;
        return this.food;
    }

    @Override
    public String toString() {
        return this.name;
    }

    public static void main(String[] args) {
        //实例方法通过类名引用
        BiFunction<Dog, Integer, Integer> biFunction = Dog::eat;
        System.out.println(biFunction.apply(new Dog(), 3));
        //无参数构造引用
        Supplier<Dog> supplierDog = Dog::new;
        System.out.println(supplierDog.get());
        //带参数的构造引用
        Function<String, Dog> functionDog = Dog::new;
        System.out.println(functionDog.apply("小花"));
    }
}

5.2:日常开发中高级使用—类型推断(有时需要我们进行显示进行类型转换):

@FunctionalInterface
interface IMath {
    int add(int x, int y);
}

@FunctionalInterface
interface IMath2 {
    int add(int x, int y);
}

public class TypeDemo {
    public static void main(String[] args) {
        //变量类型定义
        IMath lambda = (x, y) -> x + y;
        //数组定义
        IMath[] lambdas = {(x, y) -> x + y};
        //强转类型
        Object lambdaObj = (IMath) (x, y) -> x + y;
        //通过方法返回类型
        IMath createLambda = createLambda();

        executeIMath((x, y) -> x + y);
    }

    public static void executeIMath(IMath iMath) {

    }
//    public static void executeIMath(IMath2 iMath) {
//
//    }

    public static IMath createLambda() {
        return (x, y) -> x + y;
    }
}

5.3:日常开发中高级使用—变量引用:

import java.util.function.Consumer;

public class VarDemo {
    public static void main(String[] args) {
        /**
         * 匿名内部类引用外部变量为什么必须是final?
         * 原因是java的传递是值传递而不是引用传递。如果是值传递的话,假设外面的参数值进行了修改,
         * 那么会导致内部类中的计算实现不正确性。lambda为了避免这样的问题所以隐式的给为内部类外面的
         * 变量设置为了final,如果是引用传递,那么匿名内部类就不需要这样的考虑了。
         */
        String str = "我们的时间";
        Consumer<String> consumer = s -> System.out.println(s + str);
        consumer.accept("20191022");
    }
}

值传递:
值传递.png
引用传递:
引用传递.png

5.4:日常开发中高级使用—级联表达式及柯里化:

在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
这个技术由克里斯托弗·斯特雷奇以逻辑学家哈斯凯尔·加里命名的,尽管它是Moses Schönfinkel和戈特洛布·弗雷格发明的。—–摘自维基百科(wikipedia)

import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.IntUnaryOperator;

/**
 * @Author zyf
 * @Date 2019/12/15 8:50 PM
 * @Description 级联表达式和柯里化
 * 柯里化:将多个参数的函数转换为只有一个参数的函数
 * 柯里化的目的:函数的标准化
 * 高阶函数:返回函数的函数
 */
public class CurryDemo {
    public static void main(String[] args) {
        //实现x+y的级联表达式(嵌套多层Function)
        Function<Integer, Function<Integer, Integer>> fun = x -> y -> x + y;
        System.out.println(fun.apply(1).apply(3));

        IntFunction<IntFunction<IntUnaryOperator>> f = x -> y -> z -> (x + y) * z;
        System.out.println(f.apply(4).apply(5).applyAsInt(6)); //54

        int[] nums = {2, 4};
        Function funObj = fun;
        for (int i = 0; i < nums.length; i++) {
            if (funObj instanceof Function) {
                Object obj = funObj.apply(nums[i]);
                if (obj instanceof Function) {
                    funObj = (Function) obj;
                } else {
                    System.out.println("调用结束了" + obj);
                }
            }
        }
    }
}

5.4:日常开发中高级使用—级联表达式及柯里化:

在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
这个技术由克里斯托弗·斯特雷奇以逻辑学家哈斯凯尔·加里命名的,尽管它是Moses Schönfinkel和戈特洛布·弗雷格发明的。—–摘自维基百科(wikipedia)

import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.IntUnaryOperator;

/**
 * @Author zyf
 * @Date 2019/12/15 8:50 PM
 * @Description 级联表达式和柯里化
 * 柯里化:将多个参数的函数转换为只有一个参数的函数
 * 柯里化的目的:函数的标准化
 * 高阶函数:返回函数的函数
 */
public class CurryDemo {
    public static void main(String[] args) {
        //实现x+y的级联表达式(嵌套多层Function)
        Function<Integer, Function<Integer, Integer>> fun = x -> y -> x + y;
        System.out.println(fun.apply(1).apply(3));

        IntFunction<IntFunction<IntUnaryOperator>> f = x -> y -> z -> (x + y) * z;
        System.out.println(f.apply(4).apply(5).applyAsInt(6)); //54

        int[] nums = {2, 4};
        Function funObj = fun;
        for (int i = 0; i < nums.length; i++) {
            if (funObj instanceof Function) {
                Object obj = funObj.apply(nums[i]);
                if (obj instanceof Function) {
                    funObj = (Function) obj;
                } else {
                    System.out.println("调用结束了" + obj);
                }
            }
        }
    }
}

6:深入理解函数式接口&自定义lambda

要想真正彻底的理解函数式接口以及lambda的真正实现细节原理,我们必须先得熟悉和理解以下几个函数式接口。

java.util.function中定义了几组类型的函数式接口以及针对基本数据类型的子接口。
Predicate -- 传入一个参数,返回一个bool结果,方法为boolean test(T t)
Consumer -- 传入一个参数,无返回值,纯消费。 方法为void accept(T t)
Function -- 传入一个参数,返回一个结果,方法为R apply(T t)
Supplier -- 无参数传入,返回一个结果,方法为T get()
UnaryOperator -- 一元操作符, 继承Function,传入参数的类型和返回类型相同。
BiFunction-- 二元操作符, 传入两个参数返回一个结果。这个结果的类型可以与任一参数的类型不相同。
BinaryOperator -- 二元操作符, 传入的两个参数的类型和返回类型相同,继承BiFunction

详细更多请参考
下面截图中列出了这些接口在jdk rt.jar包下的java.util.function包
函数表达式常用interface接口.png

6.1初识java lambda表达式

想要初步了解java中的lambda表达式,先来看下一个非常简单的例子,透过例子来熟悉lambda表达式的简洁与强大。

import java.util.stream.IntStream;

public class ArrayMin {
    public static void main(String[] args) {
        int[] nums = {33, 55, -55, -90, -660, 90};
        int min = Integer.MAX_VALUE;
        for (int num : nums) {
            if (num < min) {
                min = num;
            }
        }
        System.out.println(min);
        min = IntStream.of(nums).min().getAsInt();
        System.out.println(min);
        //当然数据量过大的话我们也可以使用并发的方式进行处理我们的数据信息
        min = IntStream.of(nums).parallel().min().getAsInt();
        System.out.println(min);
    }
}

对程序员来说最直观的感受就是用Lambda表达式可以简化很多代码。使用它可以很轻松的将很多行代码缩减成一行。也可以更有效提高我们代码的执行效率,Java Lambda表达式的一个重要用法是简化某些匿名内部类(Anonymous Classes)的写法。
在jdk1.8以下,无法将函数作为参数进行传递给一个方法,也无法声明返回一个函数的方法。所以需要lambda函数式编程。

6.2lambda匿名内部类知识点:

1:lambda匿名内部类的运行原理
匿名内部类仍然是一个类,只是不需要程序员显示指定类名,编译器会自动为该类取名.因此如果有如下形式的代码,编译之后将会产生两个class文件:

public class MainAnonymousClass {
    public static void main(String[] args) {
        new Thread(new Runnable(){
            @Override
            public void run(){
                System.out.println("Anonymous Class Thread run()");
            }
        }).start();
    }
}

执行命令 javac MainAnonymousClass.java,编译之后文件分布如下,两个class文件分别是主类和匿名内部类产生的:
lambda匿名内部类核心实现.png
进一步分析主类MainAnonymousClass.class的字节码,可发现其创建了匿名内部类的对象:

xiaozhanggedeMacBook-Pro:Desktop xiaozhangge$ javap -c MainAnonymousClass.class
Compiled from "MainAnonymousClass.java"
public class MainAnonymousClass {
  public MainAnonymousClass();
    Code
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/Thread
       3: dup
       4: new           #3                  // class MainAnonymousClass$1 创建了内部类对象
       7: dup
       8: invokespecial #4                  // Method MainAnonymousClass$1."<init>":()V
      11: invokespecial #5                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      14: invokevirtual #6                  // Method java/lang/Thread.start:()V
      17: return
}

2:lambda匿名内部类this的作用域
lambda表达式最终会返回一个实现了指定接口的实例,看上去和内部匿名类很像,但有一个最大的区别就是代码里面的this,内部匿名类
this指向的就是匿名类,而lambda表达式里面的this指向的当前类。

/**
 * lambda表达式的this
 *
 */
public class ThisDemo {

  private String name = "ThisDemo";

  public void test() {
    // 匿名类实现
    new Thread(new Runnable() {

      private String name = "Runnable";

      @Override
      public void run() {
        System.out.println("这里的this指向匿名类:" + this.name);
      }
    }).start();

     // lambda实现
    // 下面会自动生成lambda$0方法,由于使用了this,所以是非static方法
    new Thread(() -> {
      System.out.println("这里的this指向当前的ThisDemo类:" + this.name);
    }).start();

    // lambda实现
    // 下面会自动生成lambda$1方法,由于没有使用了this,所以是static方法
    new Thread(() -> {
      System.out.println("这里没有引用this,生成的lambda1方法是static的");
    }).start();
  }

  public static void main(String[] args) {
    ThisDemo demo = new ThisDemo();
    demo.test();
  }
}

编译后字节码详情.png
使用javap -s -p 类名,可以看出一个是static,一个是非staic的
javap反编译详情.png
这就是为什么lambda表达式里面的this指向当前类的底层机制!因为代码就是在本类的一个方法里面执行的。
额外说一句,自动生成的方法是否带参数取决于lambda是否有参数,例子中表达式没有参数(箭头左边是空的),所以自动生成的也没有。

6.3自定义实现lambda表达式

自定义函数接口很容易,只需要编写一个只有一个抽象方法的接口即可。

//无参数样例
@FunctionalInterface
interface MyInterface {
    void eat();
}
/**
 * @Author zyf
 * @Date 2019/12/10 5:01 PM
 * @Description 无参数function
 */
public class NotParamFunctionInterface {
    public static void printContent(MyInterface myInterface) {
        myInterface.eat();
    }

    public static void main(String[] args) {
        NotParamFunctionInterface.printContent(new MyInterface() {
            @Override
            public void eat() {
                System.out.println("吃饭方法~~~");
            }
        });
        MyInterface myInterface = () -> System.out.println("默认函数接口实现");
        myInterface.eat();
    }
}
//有参数样例
import java.util.Arrays;
import java.util.List;

@FunctionalInterface
interface ConsumerInterface<T> {
    void accept(T t);

    default void eat() {
        System.out.println(111);
    }
}

class ParamterCustomInterface<T> {
    private List<T> list;

    public void myForEach(ConsumerInterface<T> consumer) {// 1
        for (T t : list) {
            consumer.accept(t);
        }
    }

    public static void main(String[] args) {
        ParamterCustomInterface<String> stream = new ParamterCustomInterface<String>();
        stream.list = Arrays.asList("1", "2");
        stream.myForEach(str -> System.out.println(str));// 使用自定义函数接口书写Lambda表达式
        ConsumerInterface consumerInterface = str -> System.out.println(str);
        consumerInterface.accept("函数式调用!");
        System.out.println(consumerInterface.getClass().getSuperclass());
    }
}

其实很多java 1.8中的lambda表达式使用了以上方式进行实现,比如:list–>forEach/Stream—–>peek
知识点:
1.如果一个接口只有一个抽象方法,那么该接口就是一个函数式接口。
2. 如果我们在某个接口上声明了@FunctionalInterface注解,那么编译器就会按照函数式接口的定义来要求该接口。
3. 如果某个接口只有一个抽象方法,但我们并没有给接口声明@FunctionalInterface注解,那么编译器依旧会将该接口看做是函数式接口。

7:Optional 类的引入

Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
Optional 类的引入很好的解决空指针异常。
在我们的开发中,NullPointerException可谓是随时随处可见,为了避免空指针异常,我们常常需要进行一些防御式的检查,所以在代码中常常可见if(obj != null) 这样的判断。幸好在JDK1.8中,java为我们提供了一个Optional类,Optional类能让我们省掉繁琐的非空的判断。下面先说一下Optional中为我们提供的方法.
Optional类相关常用方法.png
下面我们写几个例子来具体看一下每个方法的作用:
of

//创建一个值为张三的String类型的Optional
Optional<String> ofOptional = Optional.of("张三");
//如果我们用of方法创建Optional对象时,所传入的值为null,则抛出NullPointerException如下图所示
Optional<String> nullOptional = Optional.of(null);

option.off()运行结果
ofNullable

//为指定的值创建Optional对象,不管所传入的值为null不为null,创建的时候都不会报错
Optional<String> nullOptional = Optional.ofNullable(null);
Optional<String> nullOptional = Optional.ofNullable("lisi");

empty

//创建一个空的String类型的Optional对象
Optional<String> emptyOptional = Optional.empty();

get
如果我们创建的Optional对象中有值存在则返回此值,如果没有值存在,则会抛出NoSuchElementException异常。小demo如下:

Optional<String> stringOptional = Optional.of("张三");
System.out.println(stringOptional.get());

option.get()运行结果

Optional<String> emptyOptional = Optional.empty();
System.out.println(emptyOptional.get());

option.empty()运行结果
orElse
如果创建的Optional中有值存在,则返回此值,否则返回一个默认值

Optional<String> stringOptional = Optional.of("张三");
System.out.println(stringOptional.orElse("zhangsan"));

Optional<String> emptyOptional = Optional.empty();
System.out.println(emptyOptional.orElse("李四"));

option.orElse()运行结果
orElseGet
如果创建的Optional中有值存在,则返回此值,否则返回一个由Supplier接口生成的值

Optional<String> stringOptional = Optional.of("张三");
System.out.println(stringOptional.orElseGet(() -> "zhangsan"));

Optional<String> emptyOptional = Optional.empty();
System.out.println(emptyOptional.orElseGet(() -> "orElseGet"));

option.orElseGet()运行结果
orElseThrow
如果创建的Optional中有值存在,则返回此值,否则抛出一个由指定的Supplier接口生成的异常

Optional<String> stringOptional = Optional.of("张三");
System.out.println(stringOptional.orElseThrow(CustomException::new));

Optional<String> emptyOptional = Optional.empty();
System.out.println(emptyOptional.orElseThrow(CustomException::new));

private static class CustomException extends RuntimeException {
   private static final long serialVersionUID = -4399699891687593264L;

   public CustomException() {
       super("自定义异常");
   }

   public CustomException(String message) {
      super(message);
   }
  }

option.orElseThrow()运行结果
filter
如果创建的Optional中的值满足filter中的条件,则返回包含该值的Optional对象,否则返回一个空的Optional对象

Optional<String> stringOptional = Optional.of("zhangsan");
System.out.println(stringOptional.filter(e -> e.length() > 5).orElse("王五"));
stringOptional = Optional.empty();
System.out.println(stringOptional.filter(e -> e.length() > 5).orElse("lisi"));

option.filter()运行结果
注意Optional中的filter方法和Stream中的filter方法是有点不一样的,Stream中的filter方法是对一堆元素进
行过滤,而Optional中的filter方法只是对一个元素进行过滤,可以把Optional看成是最多只包含一个元素的Stream。
map
如果创建的Optional中的值存在,对该值执行提供的Function函数调用

//map方法执行传入的lambda表达式参数对Optional实例的值进行修改,修改后的返回值仍然是一个Optional对象
Optional<String> stringOptional = Optional.of("zhangsan");
System.out.println(stringOptional.map(e -> e.toUpperCase()).orElse("失败"));
stringOptional = Optional.empty();
System.out.println(stringOptional.map(e -> e.toUpperCase()).orElse("失败"));

option.map()运行结果
flagMap
如果创建的Optional中的值存在,就对该值执行提供的Function函数调用,返回一个Optional类型的值,否
则就返回一个空的Optional对象.flatMap与map(Funtion)方法类似,区别在于flatMap中的mapper返回
值必须是Optional,map方法的mapping函数返回值可以是任何类型T。调用结束时,flatMap不会对结果
用Optional封装。

//map方法中的lambda表达式返回值可以是任意类型,在map函数返回之前会包装为Optional。 
//但flatMap方法中的lambda表达式返回值必须是Optionl实例
Optional<String> stringOptional = Optional.of("zhangsan");
System.out.println(stringOptional.flatMap(e -> Optional.of("lisi")).orElse("失败"));

stringOptional = Optional.empty();
System.out.println(stringOptional.flatMap(e -> Optional.empty()).orElse("失败"));

option.flagMap()运行结果
ifPresent

//ifPresent方法的参数是一个Consumer的实现类,Consumer类包含一个抽象方法,该抽象方法对传入的值进行处理,只处理没有返回值。
Optional<String> stringOptional = Optional.of("zhangsan");
stringOptional.ifPresent(e-> System.out.println("我被处理了。。。"+e));

option.ifPresent()运行结果

8:java8日期和时间

Java 8通过发布新的Date-Time API 来进一步加强对日期与时间的处理。
在旧版的 Java 中,日期时间 API 存在诸多问题,其中有:
非线程安全 − java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
用于格式化日期的类DateFormat被放在java.text包中,它是一个抽象类,所以我们需要实例化一个SimpleDateFormat对象来处理日期格式化,并且DateFormat也是非线程安全,这意味着如果你在多线程程序中调用同一个DateFormat对象,会得到意想不到的结果。
设计很差 − Java的日期/时间类的定义并不一致,在java.util和java.sql的包中都有日期类,此外用于格式化和解析的类在java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。
对日期的计算方式繁琐,而且容易出错,因为月份是从0开始的,从Calendar中获取的月份需要加一才能表示当前月份.
Java 8 在 java.time 包下提供了很多新的 API。以下为两个比较重要的 API:
Local(本地) − 简化了日期时间的处理,没有时区的问题。
Zoned(时区) − 通过制定的时区处理日期时间。
新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。

Java 8日期/时间类
Java 8的日期和时间类包含LocalDate、LocalTime、Instant、Duration以及Period,这些类都包含在java.time包中,下面我们看看这些类的用法。
LocalDate和LocalTime
LocalDate类表示一个具体的日期,但不包含具体时间,也不包含时区信息。可以通过LocalDate的静态方法of()创建一个实例,LocalDate也包含一些方法用来获取年份,月份,天,星期几

LocalDate localDate = LocalDate.of(2017, 1, 4);     // 初始化一个日期:2017-01-04
int year = localDate.getYear();                     // 年份:2017
Month month = localDate.getMonth();                 // 月份:JANUARY
int dayOfMonth = localDate.getDayOfMonth();         // 月份中的第几天:4
DayOfWeek dayOfWeek = localDate.getDayOfWeek();     // 一周的第几天:WEDNESDAY
int length = localDate.lengthOfMonth();             // 月份的天数:31
boolean leapYear = localDate.isLeapYear();          // 是否为闰年:false

也可以调用静态方法now()来获取当前日期:

LocalDate now = LocalDate.now();

LocalTime和LocalDate类似,他们之间的区别在于LocalDate不包含具体时间,而LocalTime包含具体时间,例如:

LocalTime localTime = LocalTime.of(17, 23, 52);     // 初始化一个时间:17:23:52
int hour = localTime.getHour();                     // 时:17
int minute = localTime.getMinute();                 // 分:23
int second = localTime.getSecond();                 // 秒:52

LocalDateTime
LocalDateTime类是LocalDate和LocalTime的结合体,可以通过of()方法直接创建,也可以调用LocalDate的atTime()方法或LocalTime的atDate()方法将LocalDate或LocalTime合并成一个LocalDateTime:

LocalDateTime ldt1 = LocalDateTime.of(2017, Month.JANUARY, 4, 17, 23, 52);
LocalDate localDate = LocalDate.of(2017, Month.JANUARY, 4);
LocalTime localTime = LocalTime.of(17, 23, 52);
LocalDateTime ldt2 = localDate.atTime(localTime);

LocalDateTime也提供用于向LocalDate和LocalTime的转化:

LocalDate date = ldt1.toLocalDate();
LocalTime time = ldt1.toLocalTime();

Instant
Instant用于表示一个时间戳,它与我们常使用的System.currentTimeMillis()有些类似,不过Instant可以精确到纳秒(Nano-Second),System.currentTimeMillis()方法只精确到毫秒(Milli-Second)。如果查看Instant源码,发现它的内部使用了两个常量,seconds表示从1970-01-01 00:00:00开始到现在的秒数,nanos表示纳秒部分(nanos的值不会超过999,999,999)。Instant除了使用now()方法创建外,还可以通过ofEpochSecond方法创建:

Instant instant = Instant.ofEpochSecond(120, 100000);

ofEpochSecond()方法的第一个参数为秒,第二个参数为纳秒,上面的代码表示从1970-01-01 00:00:00开始后两分钟的10万纳秒的时刻,控制台上的输出为:

1970-01-01T00:02:00.000100Z

Duration
Duration的内部实现与Instant类似,也是包含两部分:seconds表示秒,nanos表示纳秒。两者的区别是Instant用于表示一个时间戳(或者说是一个时间点),而Duration表示一个时间段,所以Duration类中不包含now()静态方法。可以通过Duration.between()方法创建Duration对象:

LocalDateTime from = LocalDateTime.of(2017, Month.JANUARY, 5, 10, 7, 0);    // 2017-01-05 10:07:00
LocalDateTime to = LocalDateTime.of(2017, Month.FEBRUARY, 5, 10, 7, 0);     // 2017-02-05 10:07:00
Duration duration = Duration.between(from, to);     // 表示从 2017-01-05 10:07:00 到 2017-02-05 10:07:00 这段时间

long days = duration.toDays();              // 这段时间的总天数
long hours = duration.toHours();            // 这段时间的小时数
long minutes = duration.toMinutes();        // 这段时间的分钟数
long seconds = duration.getSeconds();       // 这段时间的秒数
long milliSeconds = duration.toMillis();    // 这段时间的毫秒数
long nanoSeconds = duration.toNanos();      // 这段时间的纳秒数

Duration对象还可以通过of()方法创建,该方法接受一个时间段长度,和一个时间单位作为参数:

Duration duration1 = Duration.of(5, ChronoUnit.DAYS);       // 5天
Duration duration2 = Duration.of(1000, ChronoUnit.MILLIS);  // 1000毫秒

Period
Period在概念上和Duration类似,区别在于Period是以年月日来衡量一个时间段,比如2年3个月6天:

Period period = Period.of(2, 3, 6);

Period对象也可以通过between()方法创建,值得注意的是,由于Period是以年月日衡量时间段,所以between()方法只能接收LocalDate类型的参数:

// 2017-01-05 到 2017-02-05 这段时间
Period period = Period.between(
                LocalDate.of(2017, 1, 5),
                LocalDate.of(2017, 2, 5));

日期的操作和格式化
增加和减少日期
Java 8中的日期/时间类都是不可变的,这是为了保证线程安全。当然,新的日期/时间类也提供了方法用于创建对象的可变版本,比如增加一天或者减少一天:

LocalDate date = LocalDate.of(2017, 1, 5);          // 2017-01-05

LocalDate date1 = date.withYear(2016);              // 修改为 2016-01-05
LocalDate date2 = date.withMonth(2);                // 修改为 2017-02-05
LocalDate date3 = date.withDayOfMonth(1);           // 修改为 2017-01-01

LocalDate date4 = date.plusYears(1);                // 增加一年 2018-01-05
LocalDate date5 = date.minusMonths(2);              // 减少两个月 2016-11-05
LocalDate date6 = date.plus(5, ChronoUnit.DAYS);    // 增加5天 2017-01-10

上面例子中对于日期的操作比较简单,但是有些时候我们要面临更复杂的时间操作,比如将时间调到下一个工作日,或者是下个月的最后一天,这时候我们可以使用with()方法的另一个重载方法,它接收一个TemporalAdjuster参数,可以使我们更加灵活的调整日期:

LocalDate date7 = date.with(nextOrSame(DayOfWeek.SUNDAY));      // 返回下一个距离当前时间最近的星期日
LocalDate date9 = date.with(lastInMonth(DayOfWeek.SATURDAY));   // 返回本月最后一个星期六

要使上面的代码正确编译,你需要使用静态导入TemporalAdjusters对象:

import static java.time.temporal.TemporalAdjusters.*;

TemporalAdjusters类中包含了很多静态方法可以直接使用,下面的表格列出了一些方法:
TemporalAdjusters相关静态方法.png
如果上面表格中列出的方法不能满足你的需求,你还可以创建自定义的TemporalAdjuster接口的实现,TemporalAdjuster也是一个函数式接口,所以我们可以使用Lambda表达式:

@FunctionalInterface
public interface TemporalAdjuster {
    Temporal adjustInto(Temporal temporal);
}

比如给定一个日期,计算该日期的下一个工作日(不包括星期六和星期天):

LocalDate date = LocalDate.of(2017, 1, 5);
date.with(temporal -> {
    // 当前日期
    DayOfWeek dayOfWeek = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));

    // 正常情况下,每次增加一天
    int dayToAdd = 1;

    // 如果是星期五,增加三天
    if (dayOfWeek == DayOfWeek.FRIDAY) {
        dayToAdd = 3;
    }

    // 如果是星期六,增加两天
    if (dayOfWeek == DayOfWeek.SATURDAY) {
        dayToAdd = 2;
    }

    return temporal.plus(dayToAdd, ChronoUnit.DAYS);
});

格式化日期
新的日期API中提供了一个DateTimeFormatter类用于处理日期格式化操作,它被包含在java.time.format包中,Java 8的日期类有一个format()方法用于将日期格式化为字符串,该方法接收一个DateTimeFormatter类型参数:

LocalDateTime dateTime = LocalDateTime.now();
String strDate1 = dateTime.format(DateTimeFormatter.BASIC_ISO_DATE);    // 20170105
String strDate2 = dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE);    // 2017-01-05
String strDate3 = dateTime.format(DateTimeFormatter.ISO_LOCAL_TIME);    // 14:20:16.998
String strDate4 = dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));   // 2017-01-05
String strDate5 = dateTime.format(DateTimeFormatter.ofPattern("今天是:YYYY年 MMMM DD日 E", Locale.CHINESE)); // 今天是:2017年 一月 05日 星期四

同样,日期类也支持将一个字符串解析成一个日期对象,例如:

String strDate6 = "2017-01-05";
String strDate7 = "2017-01-05 12:30:05";
LocalDate date = LocalDate.parse(strDate6, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
LocalDateTime dateTime1 = LocalDateTime.parse(strDate7, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

时区
Java 8中的时区操作被很大程度上简化了,新的时区类java.time.ZoneId是原有的java.util.TimeZone类的替代品。ZoneId对象可以通过ZoneId.of()方法创建,也可以通过ZoneId.systemDefault()获取系统默认时区:

ZoneId shanghaiZoneId = ZoneId.of("Asia/Shanghai");
ZoneId systemZoneId = ZoneId.systemDefault();

of()方法接收一个“区域/城市”的字符串作为参数,你可以通过getAvailableZoneIds()方法获取所有合法的“区域/城市”字符串:

Set<String> zoneIds = ZoneId.getAvailableZoneIds();

对于老的时区类TimeZone,Java 8也提供了转化方法:

ZoneId oldToNewZoneId = TimeZone.getDefault().toZoneId();

有了ZoneId,我们就可以将一个LocalDate、LocalTime或LocalDateTime对象转化为ZonedDateTime对象:

LocalDateTime localDateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, shanghaiZoneId);

将zonedDateTime打印到控制台为:

2019-12-26T16:36:41.971+08:00[Asia/Shanghai]

ZonedDateTime对象由两部分构成,LocalDateTime和ZoneId,其中2017-01-05T15:26:56.147部分为LocalDateTime,+08:00[Asia/Shanghai]部分为ZoneId。
另一种表示时区的方式是使用ZoneOffset,它是以当前时间和世界标准时间(UTC)/格林威治时间(GMT)的偏差来计算,例如:

ZoneOffset zoneOffset = ZoneOffset.of("+09:00");
LocalDateTime localDateTime = LocalDateTime.now();
OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime, zoneOffset);

其他历法
Java中使用的历法是ISO 8601日历系统,它是世界民用历法,也就是我们所说的公历。平年有365天,闰年是366天。闰年的定义是:非世纪年,能被4整除;世纪年能被400整除。为了计算的一致性,公元1年的前一年被当做公元0年,以此类推。
此外Java 8还提供了4套其他历法(很奇怪为什么没有汉族人使用的农历),每套历法都包含一个日期类,分别是:
ThaiBuddhistDate:泰国佛教历
MinguoDate:中华民国历
JapaneseDate:日本历
HijrahDate:伊斯兰历
每个日期类都继承ChronoLocalDate类,所以可以在不知道具体历法的情况下也可以操作。不过这些历法一般不常用,除非是有某些特殊需求情况下才会使用。
这些不同的历法也可以用于向公历转换:

LocalDate date = LocalDate.now();
JapaneseDate jpDate = JapaneseDate.from(date);

由于它们都继承ChronoLocalDate类,所以在不知道具体历法情况下,可以通过ChronoLocalDate类操作日期:

Chronology jpChronology = Chronology.ofLocale(Locale.JAPANESE);
ChronoLocalDate jpChronoLocalDate = jpChronology.dateNow();

我们在开发过程中应该尽量避免使用ChronoLocalDate,尽量用与历法无关的方式操作时间,因为不同的历法计算日期的方式不一样,比如开发者会在程序中做一些假设,假设一年中有12个月,如果是中国农历中包含了闰月,一年有可能是13个月,但开发者认为是12个月,多出来的一个月属于明年的。再比如假设年份是累加的,过了一年就在原来的年份上加一,但日本天皇在换代之后需要重新纪年,所以过了一年年份可能会从1开始计算。
在实际开发过程中建议使用LocalDate,包括存储、操作、业务规则的解读;除非需要将程序的输入或者输出本地化,这时可以使用ChronoLocalDate类。

最后推荐一篇非常不错切实用性很好的深度理解lambda表达式博客,这个博客总结了好几篇不错的labda表达式与Stream流的工作中常用到的案例能够更好的帮助到大家去理解和使用lambda表达式这个利器帮助我们快速的在工作中进行迭代开发,高效撸码~深入理解Java函数式编程


文章作者: 小张哥
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小张哥 !
评论
 上一篇
Go服务跨平台交叉编译打包与设置版本号 Go服务跨平台交叉编译打包与设置版本号
前言 最近在负责维护和开发守护进程、运营平台监控、连接池等相关Go服务,首先守护进程服务是部署在客户的windows平台下运行,其它大部分服务都是部署在linux环境下,首先现在面临的一个问题就是如果在一台机器上进行跨平台交叉编译后可以在
下一篇 
零基础深入学习携程Apollo配置中心 零基础深入学习携程Apollo配置中心
1:Apollo配置中心简介Apollo(阿波罗)是携程框架部门研发的配置管理平台,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。服务端基于Spring Boot和Sprin
  目录