# Interface
为了解决接口的修改与现有的实现不兼容的问题。新 interface 的方法可以用 default
或 static
修饰,这样就可以有方法体,实现类也不必重写此方法。
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(); //用lambda 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;} }); //Lambda 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(); } }); //lambda 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"); //传统foreach for (String s : strings) { System.out.println(s); } //Lambda foreach strings.forEach((s) -> System.out.println(s)); //or strings.forEach(System.out::println); //map 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); //i =3;
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() /** * 返回T的流 */ public static<T> Stream<T> of(T t) /** * 返回其元素是指定值的顺序流。 */ public static<T> Stream<T> of(T... values) { return Arrays.stream(values); } /** * 过滤,返回由与给定predicate匹配的该流的元素组成的流 */ Stream<T> filter(Predicate<? super T> predicate); /** * 此流的所有元素是否与提供的predicate匹配。 */ boolean allMatch(Predicate<? super T> predicate) /** * 此流任意元素是否有与提供的predicate匹配。 */ boolean anyMatch(Predicate<? super T> predicate); /** * 返回一个 Stream的构建器。 */ public static<T> Builder<T> builder(); /** * 使用 Collector对此流的元素进行归纳 */ <R, A> R collect(Collector<? super T, A, R> collector); /** * 返回此流中的元素数。 */ long count(); /** * 返回由该流的不同元素(根据 Object.equals(Object) )组成的流。 */ Stream<T> distinct(); /** * 遍历 */ void forEach(Consumer<? super T> action); /** * 用于获取指定数量的流,截短长度不能超过 maxSize 。 */ Stream<T> limit(long maxSize); /** * 用于映射每个元素到对应的结果 */ <R> Stream<R> map(Function<? super T, ? extends R> mapper); /** * 根据提供的 Comparator进行排序。 */ Stream<T> sorted(Comparator<? super T> comparator); /** * 在丢弃流的第一个 n元素后,返回由该流的 n元素组成的流。 */ Stream<T> skip(long n); /** * 返回一个包含此流的元素的数组。 */ Object[] toArray(); /** * 使用提供的 generator函数返回一个包含此流的元素的数组,以分配返回的数组,以及分区执行或调整大小可能需要的任何其他数组。 */ <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( 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:" ); System.out.println(mapResult); 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 //日期+时间 format: yyyy-MM-ddTHH:mm:ss.SSS LocalDate.class //日期 format: yyyy-MM-dd LocalTime.class //时间 format: HH:mm:ss
# 格式化
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 //北京时间:Wed Jan 27 14:05:29 CST 2021 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)); //如果直接print会自动转成当前时区的时间 System.out.println(date); //Wed Jan 27 14:05:29 CST 2021
在新特性中引入了 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); // ZonedDateTime 转 LocalDateTime LocalDateTime localDateTime = tokyoTime.toLocalDateTime(); System.out.println("东京时间转当地时间: " + localDateTime); //LocalDateTime 转 ZonedDateTime 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