暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

java8新特性(下)

程序媛和她的猫 2020-08-23
374

1、接口中新增默认方法和静态方法

(1)什么是默认方法和静态方法?

在java8之前,接口interface中只允许存在全局静态常量和抽象方法(抽象方法,只定义方法,不实现方法,即没有方法体)。

java8对interface做了改动,允许接口中包含具有具体实现的方法,即方法有方法体。这些方法称为默认方法和静态方法,默认方法用default修饰符修饰,静态方法用static修饰符修饰。

(2)java8中为什么要支持默认方法和静态方法呢?为什么在接口中要支持方法有方法实现呢?

之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程。缺陷是当需要修改接口时候,需要修改全部实现该接口的类。为了解决这个问题,所以引进默认方法。默认方法能让我们给接口增加新的方法,并且能保证对使用这个接口的老版本代码的兼容性。

(3)代码示例

(a)默认方法(源码见com.zxj.package1包)

public interface MyDefaultInterface {
    /**
     * 默认方法
     */

    default String getName(){
        return "哈哈哈";
    }

}

(b)静态方法(源码见com.zxj.package2包)

public interface MyStaticInterface {
    /**
     * 静态方法
     */

    static void show(){
        System.out.println("接口中的静态方法");
    }
}

public class TestStaticInterface {
    /**
     * 使用接口中的静态方法
     * @param args
     */

    public static void main(String[] args) {
        MyStaticInterface.show();
    }b


(4)方法冲突问题

(a)什么是方法冲突?

举个栗子,MyInteface接口中有一个静态方法method(),MyClass类中也有一个方法method(),SubClass类实现MyInteface接口,同时继承继承MyClass类,此时我们调用SubClass中的method()方法,那么调用的是MyInteface接口中的method()方法,还是MyClass类中的method()方法呢?

方法冲突:一个接口中定义了一个默认方法,而另外一个父类(或接口)中又定义了一个同名的方法,此时有一个子类实现第一个接口,并继承父类(或实现第二个接口),当调用子类的这个方法时,我们不知道调用的是第一个接口中的方法,还是父类(或第二个接口)中的方法。

(b)揭开谜底的时候

第一种情况:类和接口发生方法冲突,此时遵循接口默认方法的"类优先"原则,若一个接口中定义了一个默认方法,而另外一个父类又定义了一个同名的方法时,先调用父类中的同名方法。源码见com.zxj.package3包。

// 接口
public interface MyInterface {
    /**
     * 默认方法
     */

    default String getName(){
        return "调用接口中的方法";
    }
}

// 父类 
public class MyClass {
    public String getName(){
        return "调用父类中的方法";
    }
}

// 子类
public class SubClass extends MyClass implements MyInterface{

}

// 测试类
public class Test {
    @Test
    public void test(){
        SubClass subClass = new SubClass();
        System.out.println(subClass.getName());
    }
}

输出结果:调用父类中的方法

第二种情况:接口和接口发生方法冲突。此时要想解决冲突,必须在子类中覆盖接口的方法,覆盖的时候要指定覆盖的是哪个接口的方法。

1、方法冲突(源码见com.zxj.package4.conflict包)

// 接口1
public interface MyInterface1 {
    default String getName(){
        return "调用接口1中的方法";
    }
}

// 接口2
public interface MyInterface2 {
    default String getName(){
        return "调用接口2中的方法";
    }
}

// 子类
public class SubClass implements MyInterface1MyInterface2 {
    /**
     * getName方法是MyInterface1的getName方法,还是MyInterface2的getName方法。
     * @return
     */

    @Override
    public String getName() {
        return null;
    }
}

2、解决方法冲突(源码见com.zxj.package4.solveConflict包)

// 子类
public class SubClass implements MyInterface1MyInterface2 {
    /**
     * 方法冲突:getName方法是MyInterface1的getName方法,还是MyInterface2的getName方法。
     * @return
     */

    @Override
    public String getName() {
        return null;
    }
}

// 测试类
public class Test {
    @Test
    public void test(){
        SubClass subClass = new SubClass();
        System.out.println(subClass.getName());
    }
}

输出结果:调用接口1中的方法。

2、Optional容器类

(1)Optional是什么?

Optional是一个包装类。类中包装的对象可以为NULL或非NULL。简单说就是把 NULL包了一层,避免直接对NULL进行操作而报NullPointerException(空指针)异常。

(2)使用Optional的好处

1、尽量避免程序出现空指针异常,注意是避免而不是解决,不是说使用Optional之后,我们的程序就高枕无忧不会报NPE了,而是使用Optional的orElse等方法取对象,当对象为null的时候,给其一个默认值来避免对null做操作而产生NPE。

2、出现NPE的时候能够第一时间告诉开发人员可能发生NPE的代码位置,提前进行防范,而不是像java8之前那样,发生了NPE,然后一步步debug去找异常发生的位置

3、使用Optional之后,程序中不需要再写一大堆的非空判断if else,使代码更加简洁。

(3)用一个简单的示例,来说明使用Optional的好处,以及它是如何避免NPE的。

1、在Java8之前,任何访问对象方法或属性的调用都可能导致空指针异常,源码见com.zxj.package5包的Test1测试类。

// 人
public class People {
    private String name;// 这个人的姓名
    private Country country;// 这个人所在的国家

    public People(String name, Country country) {
        this.name = name;
        this.country = country;
    }

    public String getName() {
        return name;
    }

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

    public Country getCountry() {
        return country;
    }

    public void setCountry(Country country) {
        this.country = country;
    }
}

// 国家
public class Country {
    private String name;// 国家的名字
    private Flag flag;// 国家的国旗

    public Country(String name, Flag flag) {
        this.name = name;
        this.flag = flag;
    }

    public String getName() {
        return name;
    }

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

    public Flag getFlag() {
        return flag;
    }

    public void setFlag(Flag flag) {
        this.flag = flag;
    }
}

// 国旗
public class Flag {
    private String colour;// 国旗的颜色

    public Flag(String colour) {
        this.colour = colour;
    }

    public String getColour() {
        return colour;
    }

    public void setColour(String colour) {
        this.colour = colour;
    }
}

// 测试类
public class Test1 {
    @Test
    public void test(){
        // People对象
        People people = new People("小明",new Country("中国"new Flag("red")));
        String colour = people.getCountry().getFlag().getColour();
        System.out.println("小明所在国家的国旗的颜色是:" + colour);
    }
}

在Test测试类中,People对象可能是我们自己创建的,这个时候我们可以控制让它不为空,但在实际开发过程中,这个对象很可能是从第三方那里拿到的,我们根本不知道它是否是null,当我们不对它做非空判断就调用其内部的方法,就有可能出现空指针异常。

2、在Java8之前,为了避免空指针异常,必须在获得对象之前都要对其进行非空判断,如果是类似下面这种情况,对象A里面有对象B,对象B里面有对象C,对象C里面有对象D。。。使用上述方法,虽然避免了空指针异常,但是会让程序中有一大堆的if else,代码变得很冗长,后期很难维护。而且如果NPE发生在if else的深层次嵌套里,我们就需要不断地往深处debug查找NPE发生的地方,增加了程序员排查问题的难度。源码见com.zxj.package5包的Test2测试类。

public class Test2 {
    @Test
    public void test(){
        // People对象
        People people = new People("小明",new Country("中国"new Flag("red")));
        String colour = "";
        if(people != null){
            Country country = people.getCountry();
            if(country != null){
                Flag flag = country.getFlag();
                if (flag != null){
                    colour = flag.getColour();
                }
            }
        }
        System.out.println("小明所在国家的国旗的颜色是:" + colour);
    }
}

3、见证Optional奇迹的时刻,源码见com.zxj.package5包的Test3测试类。

  • 将对象封装进Optional中

你可以使用of()和ofNullable()方法把People对象放到Optional中。两个方法的不同之处在于,如果People对象是null,of()方法会抛出NPE异常,虽然仍然报NPE了,但是我们能够快速定位是Optional里面的对象发生NPE,提前进行防范。

因此在使用Optional封装对象的时候,如果你能够确定对象肯定不为null的时候,可以使用of(),如果对象有可能是null也可能是非null,你就应该使用ofNullable()。

  • 从Optional中取对象

OptionalorElse(T t),使用Optional中的orElse方法取出Optional中的对象,如果Optional中对象非null,直接返回,如果为null,返回我们在orElse方法中指定的对象默认值。

总结:以我的理解,认为使用ofNullble()和orElse()两个方法组合使用,能够最大程度上规避NPE。

public class Test3 {
    @Test
    public void test(){
        // People对象
        // 1、peope对象非空的情况
//      People people = new People("小明",new Country("中国", new Flag("red")));
        // 2、peope对象为空的情况
        People people = null;
        String colour = "";
        // 避免People对象为null,报空指针异常
        Optional<People> optional1 = Optional.ofNullable(people);
        People optionalPeople = optional1.orElse(new People());
        // 避免Country对象为null,报空指针异常
        Optional<Country> optional2 = Optional.ofNullable(optionalPeople.getCountry());
        Country optionalCountry = optional2.orElse(new Country());
        // 避免Flag对象为null,报空指针异常
        Optional<Flag> optional3 = Optional.ofNullable(optionalCountry.getFlag());
        Flag flag = optional3.orElse(new Flag());
        colour = flag.getColour();
        System.out.println("小明所在国家的国旗的颜色是:" + colour);
    }
}

看上述的代码,没有一处判断非空的if else,通过合理使用Optional,巧妙地规避了NPE的发生。

(4)Optional容器类的常用方法

(a)创建Optional实例

OptionalOptional.of(T t):创建一个Optional实例,里面包含一个类型为T的实例t。如果t不为null,创建的Optional实例中存储的是一个真实的实例。如果t为null,创建的Optional实例中存储的是一个null,此时调用Optional实例的get方法获取实例t时,of方法处会报java.lang.NullPointerException异常,告诉你Optional实例中存储的是null,使用它可能会发生NPE异常。

@Test
public void test1(){
    // of方法的参数是一个非空的实例
    Optional<Employee> optional1 = Optional.of(new Employee());
    Employee employee1 = optional1.get();
    System.out.println(employee1);

    /**
     * of方法的参数是null
     */

    Optional<Employee> optional2 = Optional.of(null);
    Employee employee2 = optional2.get();
    System.out.println(employee2);
}

OptionalOptional.ofNullable(T t):创建一个Optional实例,里面包含一个类型为T的实例t。如果t不为null,创建的Optional实例中存储的是一个真实的实例,此时和of方法的第一种情况相似。如果t为null,则创建一个空的Optional实例,里面啥都没有,既没有实例,也没有null,此时和empty()方法相似。

@Test
public void test2(){
    // ofNullable方法的参数是一个非空的实例
    Optional<Employee> optional1 = Optional.ofNullable(new Employee());
    System.out.println(optional1.get());

    // ofNullable方法的参数是null
    Optional<Employee> optional2 = Optional.ofNullable(null);
    System.out.println(optional2.get());


OptionalOptional.empty():创建一个空的Optional实例。此时调用Optional实例的get方法时,会报java.util.NoSuchElementException: No value present异常,表示get不到,说明你创建的是个空的Optional实例。

@Test
public void test3(){
    Optional optional = Optional.empty();
    System.out.println(optional.get());
}

(b)判断Optional实例中是否有一个真实的实例

boolean isPresent():判断Optional实例中是否有一个真实的实例,如果Optional实例里存储的是一个真实的实例,则返回true,如果Optional实例里存储的是null,调用isPresent()方法会报空指针异常,如果是一个空的Optional实例,返回false。

@Test
public void test4(){
    // 如果Optional实例里存储的是一个真实的实例,返回true
    Optional<Employee> optional1 = Optional.ofNullable(new Employee());
    System.out.println(optional1.isPresent());

    // 如果Optional实例里存储的是null,调用isPresent()方法会报空指针异常
    Optional<Employee> optional2 = Optional.of(null);
    System.out.println(optional2.isPresent());

    // 如果是一个空的Optional实例,返回false
    Optional optional3 = Optional.empty();
    System.out.println(optional3.isPresent());
}

(c)访问Optional实例中的值

OptionalorElse(T t):如果Optional实例中有值,就返回该值,否则返回我们传递给它的参数t。

@Test
public void test5(){
    /**
     * 如果Optional实例中有值,就返回该值,即new Employee("张三",18,2000)
     */

    Optional<Employee> optional1 = Optional.ofNullable(new Employee("张三",18,2000));
    System.out.println(optional1.orElse(new Employee()));

    /**
     * 否则返回我们传递给它的参数,即new Employee()
     */

    Optional<Employee> optional2 = Optional.empty();
    System.out.println(optional2.orElse(new Employee()));
}

输出结果:

Employee{id=0, name='张三', age=18, salary=2000.0}

Employee{id=0, name='null', age=null, salary=0.0}

T get():如果Optional实例中有值,就返回该值,否则抛出异常:java.util.NoSuchElementException: No value present,所以在使用get()方法时,为了避免异常,应该先验证Optional实例中是否有值。

@Test
public void test6(){
    /**
     * 如果Optional实例中有值,就返回该值,即new Employee("张三",18,2000)
     */

    Optional<Employee> optional1 = Optional.ofNullable(new Employee("张三",18,2000));
    System.out.println(optional1.get());

    /**
     * 否则抛出异常:NoSuchElementException
     */

    Optional<Employee> optional2 = Optional.empty();
    System.out.println(optional2.get());

    /**
     * 在使用get()方法时,为了避免异常,应该先验证Optional实例中是否有值。
     */

    Optional<Employee> optional3 = Optional.empty();
    if (optional3.isPresent()){
        System.out.println(optional3.get());
    }else{
        System.out.println("Optional实例是一个空实例");
    }
}

(d)对Optional实例中存储的值做操作

Optional<U>
 map(Function<? super T
,? extends U
> mapper):如果Optional实例中有值,则对其执行Function接口中的apply方法得到返回值。如果返回值不为null,则创建包含返回值的Optional实例作为map方法返回值,否则返回空的Optional实例。

@Test
public void test7(){
    Optional<Employee> employeeOptional = Optional.ofNullable(new Employee("张三",18,2000));
    /**
     * 使用map,将Optional实例中的Employee实例映射成Employee实例中的name字段。
     * 注意Function函数式接口中apply方法,返回值是employee.getName(),是String类型。
     */

    Optional<String> stringOptional = employeeOptional.map((employee) -> employee.getName());
    String name = stringOptional.get();
    System.out.println(name);
}

Optional<U>
 flatMap(Function<? super T
,Optional<U>
> mapper):与map类似,不同点在于Function中apply方法返回值类型不同,map的Function中apply方法返回值是U
类型,flatMap的Function中apply方法返回值是Optinal<U>

@Test
public void test8(){
    Optional<Employee> employeeOptional = Optional.ofNullable(new Employee("张三",18,2000));
    /**
     * 注意Function函数式接口中apply方法,返回值是Optional.of(employee.getName(),是Optional类型。
     */

    Optional<String> stringOptional = employeeOptional.flatMap((employee) -> Optional.of(employee.getName()));
    System.out.println(stringOptional.get());
}

(5)两种类型的Optional实例

1、非空的Optional实例:

(1)Optional实例中存储的是一个真实的实例(非null实例)

图1

(2)Optional实例中存储的是null

图2

2、空的Optional实例:Optional实例里什么都没有存储,既没有真实实例,也没有null。

图3

(6)总结

Optional是Java语言的有益补充 —— 它旨在减少代码中的NullPointerExceptions,虽然还不能完全消除这些异常。在你的项目中使用Optional,对所有对象使用Optional包裹,可以一定程度上避免空指针。

(7)Optional使用案例

源码见com.zxj.package7中的before包(未使用Optional)和after包(使用Optional)。

3、并行流

(1)并行流是为了解决什么问题?

在开发过程中,我们都知道想要提高程序效率,可以启用多线程去并行处理。

java8之前的多线程并行操作:在java8之前,并行操作有传统的多线程、线程池和java7提供的Fork/Join框架,使用这些并行操作的时候,程序很复杂很难写。这里我们以Fork/Join框架为例,使用Fork/Join框架,需要我们自己实现拆分合并算法(任务怎么拆分、拆分到哪就不拆了),算法很复杂,要写很多的代码,所以虽然使用Fork/Join会使程序效率提高很多,Fork/Join的使用频率并不高。

java8中的多线程并行操作(java8中的并行流):java8帮我们实现了上述那些操作,我们不需要自己来实现,只需要像调用API一样调用java8提供的方法就可以实现多线程并行处理,以前需要写成百上千行代码,现在只需要两三行代码即可搞定,提高了程序的简洁性,减轻开发人员的任务量

什么是并行流?并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。

并行流和串行流:使用并行流并不是一定会提高效率,因为jvm对数据进行切片和切换线程也是需要时间的。所以数据量越小,串行操作越快;数据量越大,并行操作效果越好。所以除了并行流,java8还提供了串行流,java8中的Stream API可以声明性地通过parallel()与scqucntial()在并行流与串行流之间进行切换。

(2)java7中的Fork/Join框架

源码见com.zxj.package8.java7包。

Fork/Join框架:是java7提供的一个用于执行任务的框架,就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可拆时),再将一个个小任务运算的结果进行汇总(join)。

Fork/Join框架与传统线程池的区别:Fork/Join框架的独特之处在于它使用工作窃取(work-stealing)算法。完成自己的工作而处于空闲的工作线程能够从其他仍处于忙碌状态的工作线程中窃取等待的任务来执行,每个工作线程都有自己的工作队列,这个工作队列是使用双端队列来实现的。当一个任务划分给一个线程时,它将自己推到这个线程的工作队列的头部。当线程的工作队列为空时,它将尝试从另一个线程的工作队列的尾部窃取另一个任务来执行,总之每个线程都不会闲着,所以Fork/Join框架相比于传统线程池,减少了线程的等待时间,提高了性能。

public class ForkJoinCalculate extends RecursiveTask<Long{

    // 累加,从start加到end,sum = start + (start+1) + ... + end
    private long start;
    private long end;

    public ForkJoinCalculate(long start, long end){
        this.start = start;
        this.end = end;
    }

    /**
     * 拆分的临界值,
     * 比如我们想从1加到100亿,一半一半地拆,1到50亿,50亿到100亿,
     * 1到50亿,拆分成,1到25亿,25亿到50亿,
     * 1到25亿,拆分成,1到12.5亿,12.5亿到25亿
     * 。。。。直到拆分成1到10000,10000到20000。。。。
     *
     * 这些一半一半的就是一个一个线程,然后把它们分配给CPU就可以了。
     */

    private static final long THRESHOLD = 10000;

    @Override
    protected Long compute() {
        long length = end - start;

        // length <= THRESHOLD,表示到达临界值,到达临界值,我们就要开始加(join)。
        if(length <= THRESHOLD){
            long sum = 0;
            for(long i = start; i <= end; i++){
                sum += i;
            }
            return sum;
        }else{
            /**
             * 如果没有到达临界值,我们就要继续拆(fork),Recursive,翻译是递归的意思,Fork/Join的拆分就是不断地递归拆分,
             * 所以当拆分的时候,我们在ForkJoinCalculate里面又new一个ForkJoinCalculate(递归)。
             */


            // 拆分,fork
            long middle = (start + end) / 2;
            ForkJoinCalculate leftForkJoinCalculate = new ForkJoinCalculate(start, middle);
            leftForkJoinCalculate.fork();// 拆分父任务,leftForkJoinCalculate是拆出来的左边这一半的子任务,将其压入线程队列

            ForkJoinCalculate rightForkJoinCalculate = new ForkJoinCalculate(middle+1, end);
            rightForkJoinCalculate.fork();// 拆分父任务,rightForkJoinCalculate是拆出来的右边这一半的子任务,将其压入线程队列

            // 合并,join
            return leftForkJoinCalculate.join() + rightForkJoinCalculate.join();
        }

    }
}

public class TestForkJoin {
    @Test
    public void test(){
        Instant start = Instant.now();// 开始时间戳

        ForkJoinPool forkJoinPool = new ForkJoinPool();
        // 执行0到1亿的累加
        ForkJoinTask<Long> forkJoinCalculate = new ForkJoinCalculate(010000000000L);
        Long sum = forkJoinPool.invoke(forkJoinCalculate);
        System.out.println(sum);

        Instant end = Instant.now();// 结束时间戳
        // 100亿:3966毫秒
        System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());// 单位:毫秒
    }
}

(3)java8中的并行流

源码见com.zxj.package8.java8包。

在java8中,java8帮我们实现了多线程并行操作算法,我们只需要像调用API一样调用java8提供的方法(rangeClosed和reduce方法),就能使用很简短简洁的代码,实现多线程并行处理。

public class Test {
    @Test
    public void test(){
        /**
         * LongStream.rangeClosed:生成一个LongStream流,流中数据从0到100亿。
         * sequential():将流转换成串行流
         * parallel():将流转换成并行流
         * reduce:对流中的数据执行累加操作,如果上面将流转换成了并行流,执行reduce操作会更快,耗时更短。
         */


        /**
         * 串行流:sequential()
         */

        Instant start1 = Instant.now();// 开始时间戳
        long sum1 = LongStream.rangeClosed(010000000000L)
                .sequential()
                .reduce(0, Long::sum);
        System.out.println(sum1);
        Instant end1 = Instant.now();// 结束时间戳
        System.out.println("串行流耗费时间为:" + Duration.between(start1, end1).toMillis());// 单位:毫秒

        /**
         * 并行流:parallel()
         */

        Instant start2 = Instant.now();
        long sum2 = LongStream.rangeClosed(010000000000L)
                .parallel()
                .reduce(0, Long::sum);
        System.out.println(sum2);
        Instant end2 = Instant.now();
        System.out.println("并行流耗费时间为:" + Duration.between(start2, end2).toMillis());
    }

}

4、全新的时间、日期API

(1)既然之前有时间日期API,为什么要再出一套呢?

在java8之前,jdk中已经提供了一套时间日期API,为什么在java8中要再出一套时间日期API呢,有以下几点原因:

1、原来的时间日期API用起来比较麻烦。举个栗子,当我们想创建某年某月某日时,需要使用Date(int year, int month, int date)这个API,这个year是想创建的年减去1900的差值,比如我们想创建2021年6月7日,步骤是(1)2021-1900=121。(2)Date date = new Date(121, 6, 7)。我们要先计算年份z之间的差值,再创建,有点麻烦。

2、Calender默认一周开始的一天是星期日,而我们这里一周的开始是周一。

3、java8之前的时间日期API,不提供时区(TimeZone)的支持。

4、时间、日期所在的包不规律,比如Date在java.util包下,DateFormat在java.text包下。。。在写代码引入包的时候,很麻烦,容易引错。而java8中所有的时间、日期API都在java.time包下,根据功能的不同,又分成java.time下的多个不同的包。

5、最后一点,也是最重要的一点,java8之前的时间日期API,都不是线程安全的,在多线程环境,存在线程安全问题。源码见com.zxj.package9包。

(1)线程不安全的情况:java.util.Date是可变类型,SimpleDateFormat非线程安全(源码见com.zxj.package9.threadUnsafe包)。

public class TestSimpleDateFormat {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
        // 线程
        Callable<Date> callable = new Callable<Date>() {
            @Override
            public Date call() throws Exception {
                return simpleDateFormat.parse("20161218");
            }
        };
        ExecutorService pool = Executors.newFixedThreadPool(10);// 线程池
        List<Future<Date>> results = new ArrayList<>();

        // callable执行10次,并将10次执行的结果加入到集合中。
        for(int i = 0; i < 10; i++){
            results.add(pool.submit(callable));
        }

         /**
         * 我们发现结果有两种情况,第一种运气比较好,10次的时间都不一样,
         * 第二种由于时间不断变化,时间解析不出来,抛异常
         */

        for(Future<Date> future : results){
            System.out.println(future.get());
        }
        // 关闭线程池
        pool.shutdown();
    }

}

输出结果:由于时间不断变化,时间解析不出来,报错,异常信息是,Caused by: java.lang.NumberFormatException: multiple points。

(2)线程安全的情况,两种解决方案,一是使用锁synchronized或者ThreadLocal,二是使用java8提供的API LocalDate和DateTimeFormatter。

(a)使用ThreadLocal解决线程不安全问题(源码见com.zxj.package9.threadSafe.useThreadLocal包)

public class TestThreadLocal {
    private static final ThreadLocal<SimpleDateFormat> df = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyyMMdd");
        }
    };

    // 将String类型的时间转换成Date格式
    private static Date convert(String date) throws ParseException {
        return df.get().parse(date);
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 线程
        Callable<Date> callable = new Callable<Date>() {
            @Override
            public Date call() throws Exception {
                return convert("20161218");
            }
        };
        ExecutorService pool = Executors.newFixedThreadPool(10);// 线程池
        List<Future<Date>> results = new ArrayList<>();
        // callable执行10次,并将10次执行的结果加入到集合中。
        for(int i = 0; i < 10; i++){
            results.add(pool.submit(callable));
        }
        /**
         * 我们发现10次的时间一样,都是Sun Dec 18 00:00:00 CST 2016
         */

        for(Future<Date> future : results){
            System.out.println(future.get());
        }
        /**
         * 注意及时关闭线程池,否则程序无限执行下去,打印一大堆的Sun Dec 18 00:00:00 CST 2016,大于10个
         */

        pool.shutdown();
    }
}

输出结果:

Sun Dec 18 00:00:00 CST 2016
Sun Dec 18 00:00:00 CST 2016
Sun Dec 18 00:00:00 CST 2016
Sun Dec 18 00:00:00 CST 2016
Sun Dec 18 00:00:00 CST 2016
Sun Dec 18 00:00:00 CST 2016
Sun Dec 18 00:00:00 CST 2016
Sun Dec 18 00:00:00 CST 2016
Sun Dec 18 00:00:00 CST 2016
Sun Dec 18 00:00:00 CST 2016

(b)使用java8提供的API LocalDate和DateTimeFormatter(源码见com.zxj.package9.threadSafe.useJava8包)

public class TestLocalDateAndDateTimeFormatter {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // java8中,时间格式化的API,DateTimeFormatter
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");

        // java8中,日期API,LocalDate
        Callable<LocalDate> callable = new Callable<LocalDate>() {
            @Override
            public LocalDate call() throws Exception {
                return LocalDate.parse("20161218",dateTimeFormatter);// 对20161218这个日期,按照dateTimeFormatter的格式进行转换。
            }
        };

        ExecutorService pool = Executors.newFixedThreadPool(10);
        List<Future<LocalDate>> results = new ArrayList<>();
        for(int i = 0; i < 10; i++){
            results.add(pool.submit(callable));
        }
        for(Future<LocalDate> future : results){
            System.out.println(future.get());
        }
    }
}

输出结果:

2016-12-18
2016-12-18
2016-12-18
2016-12-18
2016-12-18
2016-12-18
2016-12-18
2016-12-18
2016-12-18
2016-12-18

(2)java8中的时间日期API

java8中的时间、日期API,都放在java.time、java.time.chrono、java.time.format、java.time.temporal、java.time.zone这五个包下,每个包都有不同的作用。java8对时间日期API的作用做了分类,放在不同的包下,包的命名也很规范,完美解决了java8之前时间日期所在包混乱的问题,我们平常开发过程中可以借鉴这一点。

1、java.time包:主要存放基本时间日期格式以及对它们的操作。

(a)基本时间日期格式,包括人读的和机器读的。

  • 人读的时间日期

LocalDate:表示日期,年月日,比如2007-12-03

LocalTime:表示时间,时分秒,比如10:15:30

LocalDateTime:表示日期和时间,年月日时分秒,比如2007-12-03T10:15:30

public class TestLocalDateTime {
    /**
     * 使用LocalDateTime的now()方法,创建一个时间(包含年月日时分秒)。
     */

    @Test
    public void test1() {
        LocalDateTime localDateTime = LocalDateTime.now();// 获取当前系统时间,包含年月日时分秒
        System.out.println(localDateTime);// 输出时间格式,2020-06-25T21:06:57.963,符合ISO-8601标准
    }

    /**
     * 使用LocalDateTime的of(int year, Month month, int dayOfMonth, int hour, int minute, int second)方法,
     * 创建一个时间,在of方法中可以指定创建时间的年(year)、月(month)、日(dayOfMonth)、时(hour)、分(minute)、秒(second)
     */

    @Test
    public void test2(){
        LocalDateTime localDateTime = LocalDateTime.of(20151019132233);
        System.out.println(localDateTime);
    }

    /**
     * 对时间做运算,比如加上两天、加上两年等
     */

    @Test
    public void test3(){
        LocalDateTime localDateTime = LocalDateTime.now();// 当前系统时间
        /**
         * LocalDateTime包含很多时间运算操作
         * plusYears(long years):加上几年
         * plusMonths(long months) :加上几个月
         * plusWeeks(long weeks):加上几周
         * 。。。
         *
         * minusYears(long years):减去几年
         * minusMonths(long months):减去几个月
         * minusWeeks(long weeks):减去几周
         */

        LocalDateTime newLocalDateTime = localDateTime.plusYears(2);
        System.out.println(newLocalDateTime);

    }

    /**
     * LocalDateTime里的get方法,可以查看当前时间的年、月、日等分别是多少
     * getYear():获取年
     * getMonthValue():获取月
     * getMonth():返回Month对象,再调用Month的value()方法,获取月
     * getDayOfMonth():获取天
     * getHour():获取时
     * getMinute():获取分
     * getSecond():获取秒
     */

    @Test
    public void test4(){
        LocalDateTime localDateTime = LocalDateTime.now();// 当前系统时间
        int year = localDateTime.getYear();
        System.out.println("年:" + year);
        int month1 = localDateTime.getMonthValue();
        System.out.println("月:" + month1);
        int month2 = localDateTime.getMonth().getValue();
        System.out.println("月:" + month2);
        int day = localDateTime.getDayOfMonth();
        System.out.println("日:" + day);
        int hour = localDateTime.getHour();
        System.out.println("时:" + hour);
        int minute = localDateTime.getMinute();
        System.out.println("分:" + minute);
        int second = localDateTime.getSecond();
        System.out.println("秒:" + second);
    }
}

  • 计算机读的时间,称为时间戳,时间戳即1970年1月1日0点0分0秒到此时此刻的毫秒数。

Instant:计算机读的时间

public class TestInstant {
    /**
     * 创建一个Instant
     */

    @Test
    public void test1() {
        Instant instant = Instant.now();
        /**
         * 打印结果:2020-06-25T13:32:27.183Z
         *
         * 问题:我们发现当前计算机显示时间是2020-06-25T21:32:27,但打印出来的怎么是2020-06-25T13:32:27,
         * 差了8个小时。
         *
         * 答案:因为Instant默认获取UTC时区(本初子午线,即零度经线)的时间,所以打印的是美国时间,
         * 我们中国和美国相差8个小时,为了获取符合我们中国的时间,需要对Instant获取到的时间做一个
         * 偏移量运算,对其加上8个小时。
         */

        System.out.println(instant);

        /**
         * 对Instant获取到的时间做一个偏移量运算,对其加上8个小时。
         *
         * 打印:2020-06-25T21:39:15.906+08:00
         * 意思是,当前时间是2020-06-25T21:39:15.906,后面的+08:00,表示当前时间是在UTC时间基础
         * 上加8小时后得到的。
         */

        OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));
        System.out.println(offsetDateTime);
    }

    /**
     * 我们发现上面Instant.now()获取到的是一个时间,如果我们想要的是一个时间戳,即当前系统时间减去Unix
     * 元年之间的毫秒数,应该怎么办?
     *
     * 调用Instant的toEpochMilli()
     */

    @Test
    public void test2() {
        Instant instant = Instant.now();
        long epochMilli = instant.toEpochMilli();
        System.out.println("时间戳:" + epochMilli);// 打印 1593092691179

    }

    /**
     * 使用Instant在Unix元年的基础上进行时间的增减操作
     */

    @Test
    public void test3() {
        /**
         * 在Unix元年的基础上,加上一秒
         * 元年是:1970-01-01T00:00:00Z,加上一秒是1970-01-01T00:01:00Z
         */

        Instant instant = Instant.ofEpochSecond(60);
        System.out.println(instant);
    }

}

(b)操作基本时间日期的API

  • 计算日期、时间间隔

Period:计算两个日期之间的间隔。

Duration:计算两个时间之间的间隔。

public class TestDurationAndPeriod {
    /**
     * 使用Duration的between()方法计算两个Instant时间的间隔
     */

    @Test
    public void test1(){
        Instant instant1 = Instant.now();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Instant instant2 = Instant.now();
        Duration duration = Duration.between(instant1, instant2);
        System.out.println(duration);// 输出是PT1S,是IOS-8601的显示格式,但是我们想要间隔使用多少毫秒、多少秒表示的,应该怎么办?
        /**
         * Duration的toMillis():将间隔转换成毫秒数
         */

        long millis = duration.toMillis();
        System.out.println("两个时间的间隔毫秒数:" + millis + "毫秒");
    }

    /**
     * 使用Duration的between()方法计算两个LocalTime时间的间隔
     */

    @Test
    public void test2(){
        LocalTime localTime1 = LocalTime.now();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LocalTime localTime2 = LocalTime.now();
        long millis = Duration.between(localTime1, localTime2).toMillis();
        System.out.println("两个时间的间隔毫秒数:" + millis + "毫秒");
    }

    /**
     * 使用Period的between()方法计算两个LocalDate日期的间隔
     */

    @Test
    public void test3(){
        LocalDate localDate1 = LocalDate.of(201511);
        LocalDate localDate2 = LocalDate.now();
        Period period = Period.between(localDate1, localDate2);
        System.out.println(period);// 打印P5Y5M24D,是IOS-8601的显示格式,5Y表示相差5年,5M表示相差5个月,24D表示相差24天,总共就是相差5年5个月24天

        int year = period.getYears();//Period的getYears()方法,获取两个日期之间相差的年数。
        int month = period.getMonths();//Period的getMonths()方法,获取两个日期之间相差的月数。
        int day = period.getDays();//Period的getDays()方法,获取两个日期之间相差的天数。
        System.out.println("两个日期之间相差:" + year + "年" + month + "月" + day + "天");

    }
}

  • 对日期、时间做偏移量运算

OffsetDateTime

OffsetTime:加几个小时、减几个小时等操作。

  • 时区运算

ZoneDate、ZoneTime、ZoneDateTime:带时区的时间,不仅把在指定时区的时间打印出来,还会打印当前系统所在时区和指定时区之间相差的时间间隔,还有指定时区的信息。每个时区都对应着一个时区ID,即ZoneId,格式为“[区域]/[城市]”,例如:Asia/Shanghai。

ZonedId:时区ID类,该类中包含所有时区的信息。

ZoneOffset

public class TestZone {
    /**
     * 获取所有的时区
     */

    @Test
    public void test1(){
        Set<String> set = ZoneId.getAvailableZoneIds();
        set.forEach(System.out::println);
    }

    /**
     * 创建一个时区是在"Europe/Tallinn"的时间
     * LocalDateTime,只是把在指定时区的时间打印出来,不打印当前系统所在时区和指定时区之间相差的时间间隔,
     * 还有指定时区的信息
     */

    @Test
    public void test2(){
        ZoneId zoneId = ZoneId.of("Europe/Tallinn");
        System.out.println(zoneId);

        LocalDateTime localDateTime = LocalDateTime.now();
        System.out.println(localDateTime);// 当前计算机系统时间:2020-06-26T08:09:29.709

        LocalDateTime localDateTimeWithZone = LocalDateTime.now(ZoneId.of("Europe/Tallinn"));
        System.out.println(localDateTimeWithZone);// 指定时区的时间:2020-06-26T03:09:29.710
    }

    /**
     * 创建一个时区是在"Europe/Tallinn"的时间
     */

    @Test
    public void test3(){
        LocalDateTime localDateTime = LocalDateTime.now();
        ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of("Europe/Tallinn"));
        /**
         * 打印结果:2020-06-26T08:14:16.783+03:00[Europe/Tallinn]
         * +03:00:表示当前计算机系统所在时区和指定时区之间相差3个小时
         * [Europe/Tallinn]:指定的时区的信息
         */

        System.out.println(zonedDateTime);
    }
}

2、java.time.chrono包:用来对一些特殊的日期、时间做处理的,比如日本的日期是昭和几几年,台湾的日期是中华民国几几年。。。

3、java.time.format包:对时间、日期格式化的,比如将日期转换成"yyyyMMdd"格式,在java7中使用SimpleDateFormat,在java8中使用DateTimeFormatter。

  • DateTimeFormatter:提供大量的时间日期格式,比如ISO_LOCAL_DATE等,我们也可以使用ofPattern方法指定自己想要的格式,比如"yyyyMMdd"等格式。
public class TestDateTimeFormatter {
    /**
     * ISO-8601指定的格式化标准
     */

    @Test
    public void test1(){
        DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ISO_DATE_TIME;
        LocalDateTime localDateTime = LocalDateTime.now();

        String strDate1 = localDateTime.format(dateTimeFormatter1);
        System.out.println(strDate1);// 2020-06-26T07:40:08.345
        /**
         * 使用ISO-8601指定的格式化标准指定格式:DateTimeFormatter.ISO_DATE,只要时间日期中的日期(即Date,包含年月日)。
         */

        DateTimeFormatter dateTimeFormatter2 = DateTimeFormatter.ISO_DATE;
        String strDate2 = localDateTime.format(dateTimeFormatter2);
        System.out.println(strDate2);// 2020-06-26
    }

    /**
     * 当我们不想使用ISO-8601指定的格式化标准,只想使用自己指定的格式化标准,
     * 此时可以调用DateTimeFormatter的ofPattern()方法指定我们想要的格式。
     */

    @Test
    public void test2(){
        LocalDateTime localDateTime1 = LocalDateTime.now();
        System.out.println(localDateTime1);// 2020-06-26T07:43:24.483

        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
        String strDate1 = dateTimeFormatter.format(localDateTime1);
        System.out.println(strDate1);// 2020年06月26日 07:43:24

        // 将strDate1解析回LocalDateTime格式
        LocalDateTime localDateTime2 = localDateTime1.parse(strDate1, dateTimeFormatter);
        System.out.println(localDateTime2);// 2020-06-26T07:43:24
    }
}

4、java.time.temporal包:时间校正器,比如我们想看下一个工作日是哪天,下一个周日是哪天、下一年。。。类似的对时间、日期做运算的,就叫做时间校正器。

public class TestTemporalAdjuster {
    /**
     * 使用java8提供的时间校正器
     * 获取当前日期后面的第一个周日是哪一天
     */

    @Test
    public void test1(){
        LocalDate localDate = LocalDate.now();//当前时间,2020-06-26
        System.out.println(localDate);
        LocalDate newLocalDate = localDate.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
        System.out.println(newLocalDate);
    }

    /**
     * 自定义时间校正器
     * with(TemporalAdjuster adjuster),with方法的参数是TemporalAdjuster,
     * 如果我们想获取当前日期后面的第一个工作日,发现java8没有提供获取下一个工作日
     * 的时间校正器实例,所以我们可以自定义一个时间校正器。
     * TemporalAdjuster是个函数式接口,我们可以用匿名内部类来自定义
     */

    @Test
    public void test2(){
        LocalDate localDate = LocalDate.now();

        // 自定义时间校正器
        TemporalAdjuster temporalAdjuster = new TemporalAdjuster() {
            @Override
            public Temporal adjustInto(Temporal temporal) {
                LocalDate localDate = (LocalDate) temporal;
                DayOfWeek dayOfWeek = localDate.getDayOfWeek();// 获取当前日期在一周中的周几
                // 如果当前日期在周五,它后面的第一个工作日是周一,所以加上三天。
                if(dayOfWeek.equals(DayOfWeek.FRIDAY)){
                    return localDate.plusDays(3);
                }else if(dayOfWeek.equals(DayOfWeek.SATURDAY)){
                    // 如果当前日期在周六,它后面的第一个工作日是周一,所以加上两天。
                    return localDate.plusDays(2);
                }else{
                    // 其他情况,都加上一天。比如当前日期是周一到周四,后面的第一个工作日就是第二天,所以加上一天。
                    // 如果当前日期是周日,后面的第一个工作日是周一,也是加上一天。
                    return localDate.plusDays(1);
                }
            }
        };
        System.out.println(localDate);// 2020-06-26
        LocalDate newLocalDate = localDate.with(temporalAdjuster);
        System.out.println(newLocalDate);// 2020-06-29
    }

    /**
     * 将当前日期中的天指定为10
     */

    @Test
    public void test3(){
        LocalDate localDate = LocalDate.now();//当前时间,2020-06-26
        System.out.println(localDate);
        LocalDate newLocalDate = localDate.withDayOfMonth(10);
        System.out.println(newLocalDate);// 2020-06-10
    }
}

5、java.time.zone包:对时区做运算的。

总结:java8中关于时间日期API太多,我无法一一举例,只对几个经常用的API做了简单讲解,如果想详细了解java8时间日期操作,大家可以去官网下载java8API文档看一下。

(3)ISO-8601日历系统

ISO-8601日历系统是国际化标准组织制定的现代公民的日期和时间的表示法,下表是标准IOS日期时间格式。

时间日期API时间日期格式
LocalDate2010-12-03
LocalTime11:05:30
LocalDateTime2010-12-03T11:05:30
OffsetTime11:05:30+01:00
OffsetDateTime2010-12-03T11:05:30+01:00
ZonedDateTime2010-12-03T11:05:30+01:00 Europe/Paris
Year2010
YearMonth2010-12
MonthDay-12-03
Instant2576458258.266 seconds after 1970-01-01

5、重复注解和类型注解

(1)重复注解,在一个方法或者类上可以定义两个注解。注意使用重复注解有一个前提,必须创建重复注解的容器类,即重复注解的注解。

com.zxj.package10.repeatAnnotation.before包,没有创建可重复注解@MyAnnotation的容器类,此时无法使用可重复注解。

com.zxj.package10.repeatAnnotation.after包,创建了可重复注解@MyAnnotation的容器类@MyAnnotations,此时可以使用可重复注解。

(2)类型注解:在自定义注解的时候,在@Target中加上TYPE_PARAMETER,表明我们创建的注解是类型注解,这个自定义注解可以应用在任意类型上。源码见com.zxj.package10.typeAnnotation包。

6、jre、jvm的调整

java7中jvm区域划分,包含栈和堆,堆包含新生代、老年代、永久代(PremGren),方法区是永久代的一部分。

java7 jvm区域划分

java8,HotSpots取消了永久代,取而代之的是元空间(MetaSpace),方法区存在于元空间上。

java8 jvm区域划分

为什么要把永久代取消,转而换成元空间呢?

元空间和永久代的不同之处,元空间使用的是物理内存,而永久代使用的是JVM的内存。物理内存剩余多少,理论上metaspace就可以有多大,这解决了空间不足的问题,同时元空间发生OOM的几率变小很多,不过也不可能任其无限壮大,JVM默认在运行时会根据需要动态的设置其大小。

7、底层数据结构的调整

(1) HashMap

在jdk1.6和jdk1.7中,HashMap采用哈希表+链表实现,即使用链表处理哈希冲突,hash值相同的Entry(包括key、value)存储在一个链表中。如果发生哈希冲突的Entry很多,链表会特别长,链表查找时间复杂度是O(n),通过key值依次查找的效率很低。

在jdk1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值8或者HashMap中Entry总容量超过64时,链表就会转换为红黑树。红黑树查找的时间复杂度是O(logn),红黑树相比于链表,除了插入的效率低点,查询和删除的效率都很高。

HashMap图片

(2)ConcurrentHashMap 在jdk1.7中,ConcurrentHashMap使用锁分段技术来保证线程安全,ConcurrentHashMap分成16段,每段再分成16个。

在jdk1.8中,使用CAS无锁算法替换锁分段技术,使得对ConcurrentHashMap的操作效率提高很多。


源码位置:https://github.com/52Hzhaha/java,

java8.2.zip。


文章转载自程序媛和她的猫,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论