Oracle 于 2014 发布了 Java8(jdk1.8),诸多原因使它成为目前市场上使用最多的 jdk 版本。虽然发布距今已将近 7 年,但很多程序员对其新特性还是不够了解,尤其是用惯了 Java8 之前版本的老程序员,比如我。
为了不脱离队伍太远,还是有必要对这些新特性做一些总结梳理。它较 jdk.7 有很多变化或者说是优化,比如 interface 里可以有静态方法,并且可以有方法体,这一点就颠覆了之前的认知;java.util.HashMap
数据结构里增加了红黑树;还有众所周知的 Lambda 表达式等等。本文不能把所有的新特性都给大家一一分享,只列出比较常用的新特性给大家做详细讲解。更多相关内容请看官网关于 Java8 的新特性的介绍 。
Interface interface 的设计初衷是面向抽象,提高扩展性。这也留有一点遗憾,Interface 修改的时候,实现它的类也必须跟着改。
为了解决接口的修改与现有的实现不兼容的问题。新 interface 的方法可以用default
或 static
修饰,这样就可以有方法体,实现类也不必重写此方法。
一个 interface 中可以有多个方法被它们修饰,这 2 个修饰符的区别主要也是普通方法和静态方法的区别。
default
修饰的方法,是普通实例方法,可以用this
调用,可以被子类继承、重写。
static
修饰的方法,使用上和一般类静态方法一样。但它不能被子类继承,只能用Interface
调用。
我们来看一个实际的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public interface InterfaceNew { static void sm () { System.out.println("interface提供的方式实现" ); } static void sm2 () { System.out.println("interface提供的方式实现" ); } default void def () { System.out.println("interface default方法" ); } default void def2 () { System.out.println("interface default2方法" ); } void f () ; } public interface InterfaceNew1 { default void def () { System.out.println("InterfaceNew1 default方法" ); } }
如果有一个类既实现了 InterfaceNew
接口又实现了 InterfaceNew1
接口,它们都有def()
,并且 InterfaceNew
接口和 InterfaceNew1
接口没有继承关系的话,这时就必须重写def()
。不然的话,编译的时候就会报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class InterfaceNewImpl implements InterfaceNew , InterfaceNew1{ public static void main (String[] args) { InterfaceNewImpl interfaceNew = new InterfaceNewImpl (); interfaceNew.def(); } @Override public void def () { InterfaceNew1.super .def(); } @Override public void f () { } }
在 Java 8 ,接口和抽象类有什么区别的?
很多人认为:“既然 interface 也可以有自己的方法实现,似乎和 abstract class 没多大区别了。”
其实它们还是有区别的
interface 和 class 的区别,好像是废话,主要有:
接口多实现,类单继承
接口的方法是 public abstract 修饰,变量是 public static final 修饰。 abstract class 可以用其他修饰符
interface 的方法是更像是一个扩展插件。而 abstract class 的方法是要继承的。
开始我们也提到,interface 新增default
和static
修饰的方法,为了解决接口的修改与现有的实现不兼容的问题,并不是为了要替代abstract class
。在使用上,该用 abstract class 的地方还是要用 abstract class,不要因为 interface 的新特性而将之替换。
记住接口永远和类不一样。
functional interface 函数式接口 定义 :也称 SAM 接口,即 Single Abstract Method interfaces,有且只有一个抽象方法,但可以有多个非抽象方法的接口。
在 java 8 中专门有一个包放函数式接口java.util.function
,该包下的所有接口都有 @FunctionalInterface
注解,提供函数式编程。
在其他包中也有函数式接口,其中一些没有@FunctionalInterface
注解,但是只要符合函数式接口的定义就是函数式接口,与是否有
@FunctionalInterface
注解无关,注解只是在编译时起到强制规范定义的作用。其在 Lambda 表达式中有广泛的应用。
Lambda 表达式 接下来谈众所周知的 Lambda 表达式。它是推动 Java 8 发布的最重要新特性。是继泛型(Generics
)和注解(Annotation
)以来最大的变化。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。让 java 也能支持简单的函数式编程 。
Lambda 表达式是一个匿名函数,java 8 允许把函数作为参数传递进方法中。
语法格式 1 2 (parameters) -> expression 或 (parameters) ->{ statements; }
Lambda 实战 我们用常用的实例来感受 Lambda 带来的便利
替代匿名内部类 过去给方法传动态参数的唯一方法是使用内部类。比如
1.Runnable
接口
1 2 3 4 5 6 7 8 new Thread (new Runnable () { @Override public void run () { System.out.println("The runable now is using!" ); } }).start(); new Thread (() -> System.out.println("It's a lambda function!" )).start();
2.Comparator
接口
1 2 3 4 5 6 7 8 9 10 11 12 13 List<Integer> strings = Arrays.asList(1 , 2 , 3 ); Collections.sort(strings, new Comparator <Integer>() { @Override public int compare (Integer o1, Integer o2) { return o1 - o2;} }); Collections.sort(strings, (Integer o1, Integer o2) -> o1 - o2); Comparator<Integer> comparator = (Integer o1, Integer o2) -> o1 - o2; Collections.sort(strings, comparator);
3.Listener
接口
1 2 3 4 5 6 7 8 9 JButton button = new JButton ();button.addItemListener(new ItemListener () { @Override public void itemStateChanged (ItemEvent e) { e.getItem(); } }); button.addItemListener(e -> e.getItem());
4.自定义接口
上面的 3 个例子是我们在开发过程中最常见的,从中也能体会到 Lambda 带来的便捷与清爽。它只保留实际用到的代码,把无用代码全部省略。那它对接口有没有要求呢?我们发现这些匿名内部类只重写了接口的一个方法,当然也只有一个方法须要重写。这就是我们上文提到的函数式接口 ,也就是说只要方法的参数是函数式接口都可以用 Lambda 表达式。
1 2 3 4 5 @FunctionalInterface public interface Comparator <T>{}@FunctionalInterface public interface Runnable {}
我们自定义一个函数式接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @FunctionalInterface public interface LambdaInterface { void f () ; } public class LambdaClass { public static void forEg () { lambdaInterfaceDemo(()-> System.out.println("自定义函数式接口" )); } static void lambdaInterfaceDemo (LambdaInterface i) { i.f(); } }
集合迭代 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void lamndaFor () { List<String> strings = Arrays.asList("1" , "2" , "3" ); for (String s : strings) { System.out.println(s); } strings.forEach((s) -> System.out.println(s)); strings.forEach(System.out::println); Map<Integer, String> map = new HashMap <>(); map.forEach((k,v)->System.out.println(v)); }
方法的引用 Java 8 允许使用 ::
关键字来传递方法或者构造函数引用,无论如何,表达式返回的类型必须是 functional-interface。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class LambdaClassSuper { LambdaInterface sf () { return null ; } } public class LambdaClass extends LambdaClassSuper { public static LambdaInterface staticF () { return null ; } public LambdaInterface f () { return null ; } void show () { LambdaInterface t = LambdaClass::staticF; LambdaClass lambdaClass = new LambdaClass (); LambdaInterface lambdaInterface = lambdaClass::f; LambdaInterface superf = super ::sf; LambdaInterface tt = LambdaClassSuper::new ; } }
访问变量 1 2 3 int i = 0 ;Collections.sort(strings, (Integer o1, Integer o2) -> o1 - i);
lambda 表达式可以引用外边变量,但是该变量默认拥有 final 属性,不能被修改,如果修改,编译时就报错。
Stream java 新增了 java.util.stream
包,它和之前的流大同小异。之前接触最多的是资源流,比如java.io.FileInputStream
,通过流把文件从一个地方输入到另一个地方,它只是内容搬运工,对文件内容不做任何CRUD 。
Stream
依然不存储数据,不同的是它可以检索(Retrieve)和逻辑处理集合数据、包括筛选、排序、统计、计数等。可以想象成是 Sql 语句。
它的源数据可以是 Collection
、Array
等。由于它的方法参数都是函数式接口类型,所以一般和 Lambda 配合使用。
流类型
stream 串行流
parallelStream 并行流,可多线程执行
常用方法 接下来我们看java.util.stream.Stream
常用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 default Stream<E> stream () default Stream<E> parallelStream () public static <T> Stream<T> of (T t) public static <T> Stream<T> of (T... values) { return Arrays.stream(values); } Stream<T> filter (Predicate<? super T> predicate) ; boolean allMatch (Predicate<? super T> predicate) boolean anyMatch (Predicate<? super T> predicate) ;public static <T> Builder<T> builder () ;<R, A> R collect (Collector<? super T, A, R> collector) ; long count () ;Stream<T> distinct () ; void forEach (Consumer<? super T> action) ;Stream<T> limit (long maxSize) ; <R> Stream<R> map (Function<? super T, ? extends R> mapper) ; Stream<T> sorted (Comparator<? super T> comparator) ; Stream<T> skip (long n) ; Object[] toArray(); <A> A[] toArray(IntFunction<A[]> generator); public static <T> Stream<T> concat (Stream<? extends T> a, Stream<? extends T> b)
实战 本文列出 Stream
具有代表性的方法之使用,更多的使用方法还是要看 Api。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @Test public void test () { List<String> strings = Arrays.asList("abc" , "def" , "gkh" , "abc" ); Stream<String> stringStream = strings.stream().filter(s -> "abc" .equals(s)); long count = stringStream.count(); strings.stream().forEach(System.out::println); Stream<String> limit = strings.stream().limit(1 ); String[] array = limit.toArray(String[]::new ); Stream<String> map = strings.stream().map(s -> s + "22" ); strings.stream().sorted().forEach(System.out::println); List<String> collect = strings.stream().filter(string -> "abc" .equals(string)).collect(Collectors.toList()); String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining("," )); List<Integer> number = Arrays.asList(1 , 2 , 5 , 4 ); IntSummaryStatistics statistics = number.stream().mapToInt((x) -> x).summaryStatistics(); System.out.println("列表中最大的数 : " +statistics.getMax()); System.out.println("列表中最小的数 : " +statistics.getMin()); System.out.println("平均数 : " +statistics.getAverage()); System.out.println("所有数之和 : " +statistics.getSum()); List<String> strings2 = Arrays.asList("xyz" , "jqx" ); Stream.concat(strings2.stream(),strings.stream()).count(); Stream stream = strings.stream(); stream.limit(2 ); stream.forEach(System.out::println); stream.limit(2 ).forEach(System.out::println); }
延迟执行 在执行返回 Stream
的方法时,并不立刻执行,而是等返回一个非 Stream
的方法后才执行。因为拿到 Stream
并不能直接用,而是需要处理成一个常规类型。这里的 Stream
可以想象成是二进制流(2 个完全不一样的东东),拿到也看不懂。
我们下面分解一下 filter
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void laziness () { List<String> strings = Arrays.asList("abc" , "def" , "gkh" , "abc" ); Stream<Integer> stream = strings.stream().filter(new Predicate () { @Override public boolean test (Object o) { System.out.println("Predicate.test 执行" ); return true ; } }); System.out.println("count 执行" ); stream.count(); } count 执行 Predicate.test 执行 Predicate.test 执行 Predicate.test 执行 Predicate.test 执行
按执行顺序应该是先打印 4 次「Predicate.test
执行」,再打印「count
执行」。实际结果恰恰相反。说明 filter 中的方法并没有立刻执行,而是等调用count()
方法后才执行。
上面都是串行 Stream
的实例。并行 parallelStream
在使用方法上和串行一样。主要区别是 parallelStream
可多线程执行,是基于 ForkJoin 框架实现的,有时间大家可以了解一下 ForkJoin
框架和 ForkJoinPool
。这里可以简单的理解它是通过线程池来实现的,这样就会涉及到线程安全,线程消耗等问题。下面我们通过代码来体验一下并行流的多线程执行。
1 2 3 4 5 6 7 8 9 10 @Test public void parallelStreamTest () { List<Integer> numbers = Arrays.asList(1 , 2 , 5 , 4 ); numbers.parallelStream() .forEach(num->System.out.println(Thread.currentThread().getName()+">>" +num)); } main>>5 ForkJoinPool.commonPool-worker-2 >>4 ForkJoinPool.commonPool-worker-11 >>1 ForkJoinPool.commonPool-worker-9 >>2
从结果中我们看到,for-each 用到的是多线程。
小结 从源码和实例中我们可以总结出一些 stream 的特点
通过简单的链式编程,使得它可以方便地对遍历处理后的数据进行再处理。
方法参数都是函数式接口类型
一个 Stream 只能操作一次,操作完就关闭了,继续使用这个 stream 会报错。
Stream 不保存数据,不改变数据源
Optional 在阿里巴巴开发手册关于 Optional 的介绍 中这样写到:
防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。
反例:public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
2) 数据库的查询结果可能为 null。
3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5) 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
正例:使用 JDK8 的 Optional 类来防止 NPE 问题。
他建议使用 Optional
解决 NPE(java.lang.NullPointerException
)问题,它就是为 NPE 而生的,其中可以包含空值或非空值。下面我们通过源码逐步揭开 Optional
的红盖头。
假设有一个 Zoo
类,里面有个属性 Dog
,需求要获取 Dog
的 age
。
1 2 3 4 5 6 7 class Zoo { private Dog dog; } class Dog { private int age; }
传统解决 NPE 的办法如下:
1 2 3 4 5 6 7 8 Zoo zoo = getZoo();if (zoo != null ){ Dog dog = zoo.getDog(); if (dog != null ){ int age = dog.getAge(); System.out.println(age); } }
层层判断对象非空,有人说这种方式很丑陋不优雅,我并不这么认为。反而觉得很整洁,易读,易懂。你们觉得呢?
Optional
是这样的实现的:
1 2 3 Optional.ofNullable(zoo).map(o -> o.getDog()).map(d -> d.getAge()).ifPresent(age -> System.out.println(age) );
是不是简洁了很多呢?
如何创建一个 Optional 上例中Optional.ofNullable
是其中一种创建 Optional 的方式。我们先看一下它的含义和其他创建 Optional 的源码方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 private static final Optional<?> EMPTY = new Optional <>();private final T value;public static <T> Optional<T> ofNullable (T value) { return value == null ? empty() : of(value); } public static <T> Optional<T> empty () { Optional<T> t = (Optional<T>) EMPTY; return t; } public static <T> Optional<T> of (T value) { return new Optional <>(value); } private Optional (T value) { this .value = Objects.requireNonNull(value); } public static <T> T requireNonNull (T obj) { if (obj == null ) throw new NullPointerException (); return obj; }
ofNullable
方法和of
方法唯一区别就是当 value 为 null 时,ofNullable
返回的是EMPTY
,of 会抛出 NullPointerException
异常。如果需要把 NullPointerException
暴漏出来就用 of
,否则就用 ofNullable
。
map()
和 flatMap()
有什么区别的?
map
和 flatMap
都是将一个函数应用于集合中的每个元素,但不同的是map
返回一个新的集合,flatMap
是将每个元素都映射为一个集合,最后再将这个集合展平。
在实际应用场景中,如果map
返回的是数组,那么最后得到的是一个二维数组,使用flatMap
就是为了将这个二维数组展平变成一个一维数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class MapAndFlatMapExample { public static void main (String[] args) { List<String[]> listOfArrays = Arrays.asList( new String []{"apple" , "banana" , "cherry" }, new String []{"orange" , "grape" , "pear" }, new String []{"kiwi" , "melon" , "pineapple" } ); List<String[]> mapResult = listOfArrays.stream() .map(array -> Arrays.stream(array).map(String::toUpperCase).toArray(String[]::new )) .collect(Collectors.toList()); System.out.println("Using map:" ); mapResult.forEach(arrays-> System.out.println(Arrays.toString(arrays))); List<String> flatMapResult = listOfArrays.stream() .flatMap(array -> Arrays.stream(array).map(String::toUpperCase)) .collect(Collectors.toList()); System.out.println("Using flatMap:" ); System.out.println(flatMapResult); } }
运行结果:
1 2 3 4 5 Using map: [[APPLE, BANANA, CHERRY], [ORANGE, GRAPE, PEAR], [KIWI, MELON, PINEAPPLE]] Using flatMap: [APPLE, BANANA, CHERRY, ORANGE, GRAPE, PEAR, KIWI, MELON, PINEAPPLE]
最简单的理解就是flatMap()
可以将map()
的结果展开。
在Optional
里面,当使用map()
时,如果映射函数返回的是一个普通值,它会将这个值包装在一个新的Optional
中。而使用flatMap
时,如果映射函数返回的是一个Optional
,它会将这个返回的Optional
展平,不再包装成嵌套的Optional
。
下面是一个对比的示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static void main (String[] args) { int userId = 1 ; String cityUsingFlatMap = getUserById(userId) .flatMap(OptionalExample::getAddressByUser) .map(Address::getCity) .orElse("Unknown" ); System.out.println("User's city using flatMap: " + cityUsingFlatMap); Optional<Optional<Address>> optionalAddress = getUserById(userId) .map(OptionalExample::getAddressByUser); String cityWithoutFlatMap; if (optionalAddress.isPresent()) { Optional<Address> addressOptional = optionalAddress.get(); if (addressOptional.isPresent()) { Address address = addressOptional.get(); cityWithoutFlatMap = address.getCity(); } else { cityWithoutFlatMap = "Unknown" ; } } else { cityWithoutFlatMap = "Unknown" ; } System.out.println("User's city without flatMap: " + cityWithoutFlatMap); }
在Stream
和Optional
中正确使用flatMap
可以减少很多不必要的代码。
判断 value 是否为 null 1 2 3 4 5 6 7 8 9 10 11 12 13 public boolean isPresent () { return value != null ; } public void ifPresent (Consumer<? super T> consumer) { if (value != null ) consumer.accept(value); }
获取 value 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public T orElseGet (Supplier<? extends T> other) { return value != null ? value : other.get(); } public T orElse (T other) { return value != null ? value : other; } public <X extends Throwable > T orElseThrow (Supplier<? extends X> exceptionSupplier) throws X { if (value != null ) { return value; } else { throw exceptionSupplier.get(); } } public T get () { if (value == null ) { throw new NoSuchElementException ("No value present" ); } return value; }
过滤值 1 2 3 4 5 6 7 8 9 10 11 public Optional<T> filter (Predicate<? super T> predicate) { Objects.requireNonNull(predicate); if (!isPresent()) return this ; else return predicate.test(value) ? this : empty(); }
小结 看完 Optional
源码,Optional
的方法真的非常简单,值得注意的是如果坚决不想看见 NPE
,就不要用 of()
、 get()
、flatMap(..)
。最后再综合用一下 Optional
的高频方法。
1 Optional.ofNullable(zoo).map(o -> o.getDog()).map(d -> d.getAge()).filter(v->v==1 ).orElse(3 );
Date-Time API 这是对java.util.Date
强有力的补充,解决了 Date 类的大部分痛点:
非线程安全
时区处理麻烦
各种格式化、和时间计算繁琐
设计有缺陷,Date 类同时包含日期和时间;还有一个 java.sql.Date,容易混淆。
我们从常用的时间实例来对比 java.util.Date 和新 Date 有什么区别。用java.util.Date
的代码该改改了。
java.time 主要类 java.util.Date
既包含日期又包含时间,而 java.time
把它们进行了分离
1 2 3 LocalDateTime.class LocalDate.class LocalTime.class
格式化 Java 8 之前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void oldFormat () { Date now = new Date (); SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd" ); String date = sdf.format(now); System.out.println(String.format("date format : %s" , date)); SimpleDateFormat sdft = new SimpleDateFormat ("HH:mm:ss" ); String time = sdft.format(now); System.out.println(String.format("time format : %s" , time)); SimpleDateFormat sdfdt = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); String datetime = sdfdt.format(now); System.out.println(String.format("dateTime format : %s" , datetime)); }
Java 8 之后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void newFormat () { LocalDate date = LocalDate.now(); System.out.println(String.format("date format : %s" , date)); LocalTime time = LocalTime.now().withNano(0 ); System.out.println(String.format("time format : %s" , time)); LocalDateTime dateTime = LocalDateTime.now(); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ); String dateTimeStr = dateTime.format(dateTimeFormatter); System.out.println(String.format("dateTime format : %s" , dateTimeStr)); }
字符串转日期格式 Java 8 之前:
1 2 3 4 5 Date date = new Date ("2021-01-26" );SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd" );Date date1 = sdf.parse("2021-01-26" );
Java 8 之后:
1 2 3 4 5 6 7 8 LocalDate date = LocalDate.of(2021 , 1 , 26 );LocalDate.parse("2021-01-26" ); LocalDateTime dateTime = LocalDateTime.of(2021 , 1 , 26 , 12 , 12 , 22 );LocalDateTime.parse("2021-01-26 12:12:22" ); LocalTime time = LocalTime.of(12 , 12 , 22 );LocalTime.parse("12:12:22" );
Java 8 之前 转换都需要借助 SimpleDateFormat
类,而Java 8 之后 只需要 LocalDate
、LocalTime
、LocalDateTime
的 of
或 parse
方法。
日期计算 下面仅以一周后日期 为例,其他单位(年、月、日、1/2 日、时等等)大同小异。另外,这些单位都在 java.time.temporal.ChronoUnit 枚举中定义。
Java 8 之前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void afterDay () { SimpleDateFormat formatDate = new SimpleDateFormat ("yyyy-MM-dd" ); Calendar ca = Calendar.getInstance(); ca.add(Calendar.DATE, 7 ); Date d = ca.getTime(); String after = formatDate.format(d); System.out.println("一周后日期:" + after); String dates1 = "2021-12-23" ; String dates2 = "2021-02-26" ; SimpleDateFormat format = new SimpleDateFormat ("yyyy-MM-dd" ); Date date1 = format.parse(dates1); Date date2 = format.parse(dates2); int day = (int ) ((date1.getTime() - date2.getTime()) / (1000 * 3600 * 24 )); System.out.println(dates1 + "和" + dates2 + "相差" + day + "天" ); }
Java 8 之后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public void pushWeek () { LocalDate localDate = LocalDate.now(); LocalDate after = localDate.plus(1 , ChronoUnit.WEEKS); LocalDate after2 = localDate.plusWeeks(1 ); System.out.println("一周后日期:" + after); LocalDate date1 = LocalDate.parse("2021-02-26" ); LocalDate date2 = LocalDate.parse("2021-12-23" ); Period period = Period.between(date1, date2); System.out.println("date1 到 date2 相隔:" + period.getYears() + "年" + period.getMonths() + "月" + period.getDays() + "天" ); long day = date2.toEpochDay() - date1.toEpochDay(); System.out.println(date1 + "和" + date2 + "相差" + day + "天" ); }
获取指定日期 除了日期计算繁琐,获取特定一个日期也很麻烦,比如获取本月最后一天,第一天。
Java 8 之前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public void getDay () { SimpleDateFormat format = new SimpleDateFormat ("yyyy-MM-dd" ); Calendar c = Calendar.getInstance(); c.set(Calendar.DAY_OF_MONTH, 1 ); String first = format.format(c.getTime()); System.out.println("first day:" + first); Calendar ca = Calendar.getInstance(); ca.set(Calendar.DAY_OF_MONTH, ca.getActualMaximum(Calendar.DAY_OF_MONTH)); String last = format.format(ca.getTime()); System.out.println("last day:" + last); Calendar currCal = Calendar.getInstance(); Calendar calendar = Calendar.getInstance(); calendar.clear(); calendar.set(Calendar.YEAR, currCal.get(Calendar.YEAR)); calendar.roll(Calendar.DAY_OF_YEAR, -1 ); Date time = calendar.getTime(); System.out.println("last day:" + format.format(time)); }
Java 8 之后:
1 2 3 4 5 6 7 8 9 10 11 12 13 public void getDayNew () { LocalDate today = LocalDate.now(); LocalDate firstDayOfThisMonth = today.with(TemporalAdjusters.firstDayOfMonth()); LocalDate lastDayOfThisMonth = today.with(TemporalAdjusters.lastDayOfMonth()); LocalDate nextDay = lastDayOfThisMonth.plusDays(1 ); LocalDate lastday = today.with(TemporalAdjusters.lastDayOfYear()); LocalDate lastMondayOf2021 = LocalDate.parse("2021-12-31" ).with(TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY)); }
java.time.temporal.TemporalAdjusters
里面还有很多便捷的算法,这里就不带大家看 Api 了,都很简单,看了秒懂。
JDBC 和 java8 现在 jdbc 时间类型和 java8 时间类型对应关系是
Date
—> LocalDate
Time
—> LocalTime
Timestamp
—> LocalDateTime
而之前统统对应 Date
,也只有 Date
。
时区
时区:正式的时区划分为每隔经度 15° 划分一个时区,全球共 24 个时区,每个时区相差 1 小时。但为了行政上的方便,常将 1 个国家或 1 个省份划在一起,比如我国幅员宽广,大概横跨 5 个时区,实际上只用东八时区的标准时即北京时间为准。
java.util.Date
对象实质上存的是 1970 年 1 月 1 日 0 点( GMT)至 Date 对象所表示时刻所经过的毫秒数。也就是说不管在哪个时区 new Date,它记录的毫秒数都一样,和时区无关。但在使用上应该把它转换成当地时间,这就涉及到了时间的国际化。java.util.Date
本身并不支持国际化,需要借助 TimeZone
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Date date = new Date ();SimpleDateFormat bjSdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" );bjSdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai" )); System.out.println("毫秒数:" + date.getTime() + ", 北京时间:" + bjSdf.format(date)); SimpleDateFormat tokyoSdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" );tokyoSdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo" )); System.out.println("毫秒数:" + date.getTime() + ", 东京时间:" + tokyoSdf.format(date)); System.out.println(date);
在新特性中引入了 java.time.ZonedDateTime
来表示带时区的时间。它可以看成是 LocalDateTime + ZoneId
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ZonedDateTime zonedDateTime = ZonedDateTime.now();System.out.println("当前时区时间: " + zonedDateTime); ZoneId zoneId = ZoneId.of(ZoneId.SHORT_IDS.get("JST" ));ZonedDateTime tokyoTime = zonedDateTime.withZoneSameInstant(zoneId);System.out.println("东京时间: " + tokyoTime); LocalDateTime localDateTime = tokyoTime.toLocalDateTime();System.out.println("东京时间转当地时间: " + localDateTime); ZonedDateTime localZoned = localDateTime.atZone(ZoneId.systemDefault());System.out.println("本地时区时间: " + localZoned); 当前时区时间: 2021 -01 -27T14:43 :58.735 +08:00 [Asia/Shanghai] 东京时间: 2021 -01 -27T15:43 :58.735 +09:00 [Asia/Tokyo] 东京时间转当地时间: 2021 -01 -27T15:43 :58.735 当地时区时间: 2021 -01 -27T15:53 :35.618 +08:00 [Asia/Shanghai]
小结 通过上面比较新老 Date
的不同,当然只列出部分功能上的区别,更多功能还得自己去挖掘。总之 date-time-api 给日期操作带来了福利。在日常工作中遇到 date 类型的操作,第一考虑的是 date-time-api,实在解决不了再考虑老的 Date。
总结 我们梳理总结的 java 8 新特性有
Interface & functional Interface
Lambda
Stream
Optional
Date time-api