Oracle 于 2014 发布了 Java8(jdk1.8),它较 jdk7 有很多变化或者说是优化,比如 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
这些都是开发当中比较常用的特性。梳理下来发现它们真香,而我却没有更早的应用。总觉得学习 java 8 新特性比较麻烦,一直使用老的实现方式。其实这些新特性几天就可以掌握,一但掌握,效率会有很大的提高。其实我们涨工资也是涨的学习的钱,不学习终究会被淘汰,35 岁危机会提前来临。