日期与时间

日期与时间概念介绍

计算机表示的时间是以整数表示的时间戳存储的,即Epoch Time,Java使用long型来表示以毫秒为单位的时间戳,通过System.currentTimeMillis()获取当前时间戳

Java两套日期和时间的API

(1)旧的:Date、Calendar和TimeZone(java.util)
(2)新的:在Java 8引入的,LocalDateTime、LocalDate、LocalTime、ZonedDateTime、ZoneId等(java.time)


概念

日期:指某一天,它不是连续变化的,而是应该被看成离散的。
时间有两种概念
(1)一种是不带日期的时间,例如,12:30:59
(2)一种是带日期的时间,例如,2020-1-1 20:21:59,只有这种带日期的时间能唯一确定某个时刻,不带日期的时间是无法确定一个唯一时刻的


本地时间

当前时刻,实际上是本地时间
不同的时区,在同一时刻,本地时间是不同的。全球一共分为24个时区,伦敦所在的时区称为标准时区,其他时区按东/西偏移的小时区分,北京所在的时区是东八区

时区
光靠本地时间还无法唯一确定一个准确的时刻,所以还需要给本地时间加上一个时区


时区表示方式

(1)以GMT或者UTC加时区偏移

例如:GMT+08:00或者UTC+08:00表示东八区,GMT和UTC可以认为基本是等价的,只是UTC使用更精确的原子钟计时,每隔几年会有一个闰秒,在开发程序的时候可以忽略两者的误差,因为计算机的时钟在联网的时候会自动与时间服务器同步时间

(2)缩写

例如,CST表示China Standard Time,也就是中国标准时间。但是CST也可以表示美国中部时间Central Standard Time USA,因此,缩写容易产生混淆,尽量不要使用缩写

(3)以洲/城市表示

Asia/Shanghai,表示上海所在地的时区。特别注意城市名称不是任意的城市,而是由国际标准组织规定的城市


时刻

因为时区的存在,东八区的2019年11月20日早上8:15,和西五区的2019年11月19日晚上19:15,时刻是相同的
时刻:分别在两个时区的两个人,如果在这一刻通电话,各自报出自己手表上的时间,虽然本地时间是不同的,但是这两个时间表示的时刻是相同的


夏令时

所谓夏令时,就是夏天开始的时候,把时间往后拨1小时,夏天结束的时候,再把时间往前拨1小时。国家实行过一段时间夏令时,1992年就废除了,但是矫情的美国人到现在还在使用,所以时间换算更加复杂


本地化

在计算机中,通常使用Locale表示一个国家或地区的日期、时间、数字、货币等格式。Locale由(语言_国家)字母缩写构成,例如,zh_CN表示中文+中国,en_US表示英文+美国。语言使用小写,国家使用大写

对于日期来说,不同的Locale,例如,中国和美国的表示方式如下
(1)zh_CN:2016-11-30
(2)en_US:11/30/2016

(1)在编写日期和时间的程序前,要准确理解日期、时间和时刻的概念
(2)由于存在本地时间,需要理解时区的概念,并且必须牢记由于夏令时的存在,同一地区用GMT/UTC和城市表示的时区可能导致时间不同
(3)计算机通过Locale来针对当地用户习惯格式化日期、时间、数字、货币等

“同一个时刻”在计算机中存储的本质上只是一个整数,我们称它为Epoch Time
Epoch Time又称为时间戳,在不同的编程语言中,会有几种存储方式:
(1)以秒为单位的整数:1574208900,缺点是精度只能到秒
(2)以毫秒为单位的整数:1574208900123,最后3位表示毫秒数
(3)以秒为单位的浮点数:1574208900.123,小数点后面表示零点几秒

它们之间转换非常简单。而在Java程序中,时间戳通常是用long表示的毫秒数。要获取当前时间戳,可以使用System.currentTimeMillis(),这是Java程序获取时间戳最常用的方法。


旧的日期时间

Date(时间日期类,java.util)

java.util.Date是用于表示一个日期和时间的对象,注意与java.sql.Date区分,后者用在数据库中。如果观察Date的源码,可以发现它实际上存储了一个long类型的以毫秒表示的时间戳

1.构造方法

Date() 分配Date对象并初始化此对象,以表示分配它的事件(精确到毫秒)
Date(long date) 分配Date对象并初始化此对象,以表示自从标准的基准时间(即1970年1月1日00:00:00GMT)以来的指定毫秒

如何使用

long timeMillis = System.currentTimeMillis();
Date date = new Date(timeMillis);

System类的currentTimeMillis()方法主要用来获取系统当前时间距标准基准时间的毫秒数

2.Date类的常用方法

方法 功能
boolean after(Date when) 测试当前日期是否在指定的日期之后
boolean before(Date when) 测试当前日期是否在指定日期之前
long getTime() 获得自1970年1月1日GMT到现在的所表示的毫秒数
void setTime(long time) 设置当前Date对象所表示的日期时间值,该值用以表示1970年1月1日00:00:00GMT以后time毫秒的时间点

基本用法

Date date = new Date();// 获取当前时间
System.out.println(date.getYear() + 1900); // 必须加上1900
System.out.println(date.getMonth() + 1); // 0~11,必须加上1
System.out.println(date.getDate());      // 1~31,不能加1
System.out.println(date.toString());// 转换为String
System.out.println(date.toGMTString());// 转换为GMT时区
System.out.println(date.toLocaleString());// 转换为本地时区

注意:getYear()返回的年份必须加上1900,getMonth()返回的月份是011分别表示112月,所以要加1,而getDate()返回的日期范围是1~31,又不能加1

Date对象有几个严重的问题
(1)不能转换时区,除了toGMTString()可以按GMT+0:00输出外,Date总是以当前计算机系统的默认时区为基础进行输出
(2)也很难对日期和时间进行加减,计算两个日期相差多少天,计算某个月第一个星期一的日期等


DateFormat类

日期/时间格式化子类的抽象类(java.text)

方法 功能
SHORT 完全为数字,如12.12.52或3:30pm
MEDIUM 较长,如Jan12,1952
LONG 更长,如January 12,1952或3:30:32pm
FULL 完全指定,如Tuesday,April 12、1954AD或3:30:42pmPST

使用DateFormat要格式化一个当前语言环境下的日期,需要创建DateFormat类的一个对象;
由于它是抽象类,因此可以使用静态工厂方getDateInstance创建

DateFormat df = new DateFormat.getDateInstance();

常用方法

方法 功能
String format(Date date) 将一个Date格式化为日期/时间字符串
Calendar getCalendar() 获取与此日期/时间格式器关联的日历
static DateFormat getDateInstance() 获取日期格式器,该格式器具有默认语言环境的默认格式化分格
static DateFormat getDateTimeInstance() 获取日期/时间格式器,该格式器具有默认语言环境的默认格式化分格
static DateFormat getInstance() 获取为日期和时间使用SHORT分格的默认日期/时间格式器
static DateFormat getTimeInstance() 获取时间格式器,该格式器具有默认语言环境的默认格式化分格
Date parse(String source) 将字符串解析成一个日期,并返回这个日期的Date对象

打印本地时区表示的日期和时间时,不同的计算机可能会有不同的结果。如果想要针对用户的偏好精确地控制日期和时间的格式,就可以使用SimpleDateFormat对一个Date进行转换。它用预定义的字符串表示格式化:
(1)yyyy:年
(2)MM:月
(3)dd: 日
(4)HH: 小时
(5)mm: 分钟
(6)ss: 秒

SimpleDateFormat自定义日期格式

Date date = new Date();// 获取当前时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(date));

一般来说,字母越长,输出越长。以M为例,假设当前月份是9月:
M:输出9
MM:输出09
MMM:输出Sep
MMMM:输出September


Calendar(抽象类,java.util)

Calendar可以用于获取并设置年、月、日、时、分、秒,它和Date比,主要多了一个可以做简单的日期和时间运算的功能

一个抽象类,它为特定瞬间与一组YEAR、MONTH、DAY_OF_MONTH、HOUR等日历字段之间的转换提供了方法
Calendar提供了一个类方法getInstance,以获得此类型的一个通用的对象。返回一个Calendar对象,其日历字段已由当前日期和时间初始化,使用方法如下

Calendar rightNow = Calendar.getInstance();

由于Calendar类是一个抽象类,不能使用new实例化,因此除了使用getInstance方法创建,还可以使用其子类创建对象。

常用方法

方法 功能
void add(int fieId,int amount) 根据日历规则,为给定的日历字段添加或减去指定的时间量
boolean after(Object when) 判断此Calendar表示的时间是否在指定Object表示的时间之后,返回判断结果
boolean before(Object when) 判断此Calendar表示的时间是否在指定Object表示的时间之前,返回判断结果
int get(int fieId) 返回给定日历字段
static Calendar getInstance() 使用默认时区和语言环境获得一个日历
Date getTime() 返回一个表示Calendar时间值(从历元至现在的毫秒偏移量)的Date对象
abstract void roll(int fieId,boolean up) 在给定的时间字段上添加或减去(上/下)单个时间单元,不更改更大的字段
void set(int fieId,int value) 将给定的日历字段设置为给定值
void set(int year,int month,int data) 设置日历字段YEAR、MONTH和DAY_OF_MONTH的值
void set( int year,int month,int date,int hourOfDay,int minute,int second) 设置日历字段YEAR、MONTH、DAY_OF_MONTH、HOUR_OF_DAY和SECOND的值

获取Calendar

Calendar只有一种方式获取,即Calendar.getInstance(),而且一获取到就是当前时间

Calendar c = Calendar.getInstance();  // 获取当前时间
int y = c.get(Calendar.YEAR);
int m = 1 + c.get(Calendar.MONTH);
int d = c.get(Calendar.DAY_OF_MONTH);
int w = c.get(Calendar.DAY_OF_WEEK);
int hh = c.get(Calendar.HOUR_OF_DAY);
int mm = c.get(Calendar.MINUTE);
int ss = c.get(Calendar.SECOND);
int ms = c.get(Calendar.MILLISECOND);

注意:Calendar获取年月日这些信息变成了get(int field),返回的年份不必转换,返回的月份仍然要加1,返回的星期要特别注意,1~7分别表示周日,周一,……,周六

设置特定时间

如果想给它设置成特定的一个日期和时间,就必须先清除所有字段(clear)

Calendar c = Calendar.getInstance();// 当前时间
c.clear();// 清除所有
c.set(Calendar.YEAR, 2019);// 设置2019年
c.set(Calendar.MONTH, 8);// 设置9月,注意8表示9月
c.set(Calendar.DATE, 2);// 设置2日
c.set(Calendar.HOUR_OF_DAY, 21);// 设置时间
c.set(Calendar.MINUTE, 22);
c.set(Calendar.SECOND, 23);
// 2019-09-02 21:22:23
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(c.getTime()));

利用Calendar.getTime()可以将一个Calendar对象转换成Date对象,然后就可以用SimpleDateFormat进行格式化了。

对日期和时间进行简单的加减

Calendar c = Calendar.getInstance();  //当前时间
c.clear();  //清除所有
c.set(2019, 10 /* 11月 */, 20, 8, 15, 0);  //设置年月日时分秒
c.add(Calendar.DAY_OF_MONTH, 5);  //加5天并减去2小时
c.add(Calendar.HOUR_OF_DAY, -2);
// 显示时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 将Calendar对象转换为Date对象
Date d = c.getTime();
// 2019-11-25 6:15:00
System.out.println(sdf.format(d));

TimeZone
提供了时区转换的功能,时区用TimeZone对象表示

TimeZone tzDefault = TimeZone.getDefault(); // 当前时区
TimeZone tzGMT9 = TimeZone.getTimeZone(“GMT+09:00”); // GMT+9:00时区
TimeZone tzNY = TimeZone.getTimeZone(“America/New_York”); // 纽约时区
System.out.println(tzDefault.getID()); // Asia/Shanghai
System.out.println(tzGMT9.getID()); // GMT+09:00
System.out.println(tzNY.getID()); // America/New_York

时区的唯一标识是以字符串表示的ID,获取指定TimeZone对象也是以这个ID为参数获取,GMT+09:00、Asia/Shanghai都是有效的时区ID
使用TimeZone.getAvailableIDs(),可以列出系统支持的所有ID

时区转换

有了时区,就可以对指定时间进行转换
例如,将北京时间2019-11-20 8:15:00转换为纽约时间

Calendar c = Calendar.getInstance();// 当前时间
c.clear();// 清除所有
c.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));// 设置为北京时区
c.set(2019, 10 /* 11月 */, 20, 8, 15, 0);// 设置年月日时分秒
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 显示时间
sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));
System.out.println(sdf.format(c.getTime()));// 2019-11-19 19:15:00

利用Calendar进行时区转换的步骤
(1)清除所有字段
(2)设定指定时区
(3)设定日期和时间
(4)创建SimpleDateFormat并设定目标时区
(5)格式化获取的Date对象(注意Date对象无时区信息,时区信息存储在SimpleDateFormat中)
因此,本质上时区转换只能通过SimpleDateFormat在显示的时候完成



新的日期时间

从Java 8开始,java.time包提供了新的日期和时间API
(1)本地日期和时间:LocalDateTime,LocalDate,LocalTime
(2)带时区的日期和时间:ZonedDateTime、ZonedDate、ZonedTime
(3)时区:ZoneId,ZoneOffset
(4)时刻:Instant
(5)以及一套新的用于取代SimpleDateFormat的格式化类型DateTimeFormatter
(6)时间间隔
和旧的API相比,新API严格区分了时刻、本地日期、本地时间和带时区的日期时间,并且,对日期和时间进行运算更加方便

新API修正了旧API不合理的常量设计
(1)Month的范围用1~12表示1月到12月
(2)Week的范围用1~7表示周一到周日
(3)新API的类型几乎全部是不变类型(和String类似),可以放心使用不必担心被修改


本地日期时间

Clock 时钟

Clock类提供了访问当前日期和时间的方法,Clock是时区敏感的,可以用来取代 System.currentTimeMillis()来获取当前的微秒数。某一个特定的时间点也可以使用Instant类来表示,Instant类也可以用来创建老的java.util.Date对象

Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant);   // 使用Clock创建Date

LocalTime本地时间

LocalTime定义了一个没有时区信息的时间,例如:晚上10点,或者17:30:15。下面的例子使用前面代码创建的时区创建了两个本地时间。之后比较时间并以小时和分钟为单位计算两个时间的时间差:

Clock clock1 = Clock.systemDefaultZone();
Clock clock2 = Clock.systemDefaultZone();

LocalTime now1 = LocalTime.now(clock1);
LocalTime now2 = LocalTime.now(clock2);
System.out.println(now1.isBefore(now2)); // false

long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
System.out.println(hoursBetween); // -3
System.out.println(minutesBetween); // -239
// LocalTime 提供了多种工厂方法来简化对象的创建,包括解析时间字符串。

LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late); // 23:59:59

DateTimeFormatter germanFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
				.withLocale(Locale.GERMAN);

LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
System.out.println(leetTime); // 13:37

LocalDate本地日期

LocalDate表示了一个确切的日期,比如2014-03-11。该对象值是不可变的,用起来和LocalTime基本一致。下面的例子展示了如何给Date对象加减天/月/年。另外要注意的是这些对象是不可变的,操作返回的总是一个新实例

LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);
System.out.println(tomorrow + " " + yesterday); //2020-05-30 2020-05-28
		
LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek); // FRIDAY

LocalDateTime本地日期时间

一个本地日期和时间,通过now()获得

基本使用

LocalDateTime dt = LocalDateTime.now(); // 当前日期和时间
LocalDate d = LocalDate.now(); // 当前日期
LocalTime t = LocalTime.now(); // 当前时间
System.out.println(d); // 严格按照ISO 8601格式打印
System.out.println(t); // 严格按照ISO 8601格式打印
System.out.println(dt); // 严格按照ISO 8601格式打印

本地日期和时间通过now()获取到的总是以当前默认时区返回的,和旧API不同,LocalDateTime、LocalDate和LocalTime默认严格按照ISO 8601规定的日期和时间格式进行打印

上述代码其实有一个小问题,在获取3个类型的时候,由于执行一行代码总会消耗一点时间,因此,3个类型的日期和时间很可能对不上(时间的毫秒数基本上不同)。为了保证获取到同一时刻的日期和时间,可以改写如下

LocalDateTime dt = LocalDateTime.now(); // 当前日期和时间
LocalDate d = dt.toLocalDate(); // 转换到当前日期
LocalTime t = dt.toLocalTime(); // 转换到当前时间

注意ISO 8601规定的日期和时间分隔符是T。标准格式如下:
(1)日期:yyyy-MM-dd
(2)时间:HH:mm:ss
(3)带毫秒的时间:HH:mm:ss.SSS
(4)日期和时间:yyyy-MM-dd’T’HH:mm:ss
(5)带毫秒的日期和时间:yyyy-MM-dd’T’HH:mm:ss.SSS

带时区的日期和时间

JDK 8中加入了对时区的支持,带时区的时间为分别为:ZonedDate、ZonedTime、ZonedDateTime
其中每个时区都对应着 ID,地区ID都为 “{区域}/{城市}”的格式。例如 :Asia/Shanghai 等

ZonedDateTime

表示本地日期和时间,要表示一个带时区的日期和时间

可以简单地把ZonedDateTime理解成LocalDateTime加ZoneId。ZoneId是java.time引入的新的时区类,注意和旧的java.util.TimeZone区别

要创建一个ZonedDateTime对象,有以下几种方法

1.通过now()方法返回当前时间

ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
// 用指定时区获取当前时间
ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York")); 

时区不同,但表示的时间都是同一时刻(毫秒数不同是执行语句时的时间差)

2.通过给一个LocalDateTime附加一个ZoneId,就可以变成ZonedDateTime

LocalDateTime ldt = LocalDateTime.of(2019, 9, 15, 15, 16, 17);
ZonedDateTime zbj = ldt.atZone(ZoneId.systemDefault());  //默认时区
ZonedDateTime zny = ldt.atZone(ZoneId.of("America/New_York"));//纽约时区

时区转换

要转换时区,首先需要有一个ZonedDateTime对象,然后,通过withZoneSameInstant()将关联时区转换到另一个时区,转换后日期和时间都会相应调整

如何将北京时间转换为纽约时间

// 以中国时区获取当前时间:
ZonedDateTime zbj = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
// 转换为纽约时间:
ZonedDateTime zny = zbj.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println(zbj);
System.out.println(zny);

要特别注意,时区转换的时候,由于夏令时的存在,不同的日期转换的结果很可能是不同的。这是北京时间9月15日的转换结果

ZonedDateTime是带时区的日期和时间,可用于时区转换
ZonedDateTime和LocalDateTime可以相互转换


时区(ZoneId)

该类中包含了所有的时区信息

常用方法

方法 描述
getAvailableZoneIds() 可以获取所有时区时区信息
of(id) 用指定的时区信息获取ZoneId对象
// 获取世界各个地方的时区的集合
Set availableZoneIds = ZoneId.getAvailableZoneIds();

// 获取系统默认时区的ID
ZoneId zd = ZoneId.systemDefault();
System.out.println("系统默认时区" + zd); // Asia/Shanghai
System.out.println("系统默认时区" + zd.getId()); // Asia/Shanghai

时刻(Instant)

计算机存储的当前时间,本质上只是一个不断递增的整数。Java提供的System.currentTimeMillis()返回的就是以毫秒表示的当前时间戳。
这个当前时间戳在java.time中以Instant类型表示,用Instant.now()获取当前时间戳,效果和System.currentTimeMillis()类似

Instant now = Instant.now();
System.out.println(now.getEpochSecond()); // 秒
System.out.println(now.toEpochMilli());   // 毫秒

Instant内部只有两个核心字段

public final class Instant implements ... {
	private final long seconds;
	private final int nanos;
}

一个是以秒为单位的时间戳,一个是更精确的纳秒精度。它和System.currentTimeMillis()返回的long相比,只是多了更高精度的纳秒。

既然Instant就是时间戳,那么,给它附加上一个时区,就可以创建出ZonedDateTime:

// 以指定时间戳创建Instant
Instant ins = Instant.ofEpochSecond(1568568760);
ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());
System.out.println(zdt); // 2019-09-16T01:32:40+08:00[Asia/Shanghai]

可见,对于某一个时间戳,给它关联上指定的ZoneId,就得到了ZonedDateTime,继而可以获得了对应时区的LocalDateTime


LocalDateTime,ZoneId,Instant,ZonedDateTime和long都可以互相转换
日期与时间_第1张图片

转换的时候,只需要留意long类型以毫秒还是秒为单位即可
Instant表示高精度时间戳,它可以和ZonedDateTime以及long互相转换


时间间隔(ChronoUnit)

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDate startDate = LocalDate.parse(startTime, formatter);
CharSequence endTime;
LocalDate endDate = LocalDate.parse(endTime, formatter);
// 日期区间
long days = ChronoUnit.DAYS.between(startDate, endDate);
// 月
long month = ChronoUnit.MONTHS.between(startDate, endDate);
// 年
long year = ChronoUnit.YEARS.between(startDate, endDate);

格式化(DateTimeFormatter)

自定义输出的格式,或者要把一个非ISO 8601格式的字符串解析成LocalDateTime,可以使用新的DateTimeFormatter

// 自定义格式化
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
dtf.format(LocalDateTime.now());
// 用自定义格式解析
LocalDateTime dt2 = LocalDateTime.parse("2019/11/30 15:16:17", dtf);

LocalDateTime提供了对日期和时间进行加减的非常简单的链式调用

LocalDateTime dt = LocalDateTime.of(2019, 10, 26, 20, 30, 59);
System.out.println(dt);
LocalDateTime dt2 = dt.plusDays(5).minusHours(3);// 加5天减3小时
System.out.println(dt2); // 2019-10-31T17:30:59
LocalDateTime dt3 = dt2.minusMonths(1);// 减1月
System.out.println(dt3); // 2019-09-30T17:30:59

注意:到月份加减会自动调整日期,例如从2019-10-31减去1个月得到的结果是2019-09-30,因为9月没有31日

对日期和时间进行调整则使用withXxx()方法,例如:withHour(15)会把10:11:12变为15:11:12:
(1)调整年:withYear()
(2)调整月:withMonth()
(3)调整日:withDayOfMonth()
(4)调整时:withHour()
(5)调整分:withMinute()
(6)调整秒:withSecond()


Java 8引入了新的日期和时间API,它们是不变类,默认按ISO 8601标准格式化和解析;
使用LocalDateTime可以非常方便地对日期和时间进行加减,或者调整日期和时间,它总是返回新对象;
使用isBefore()和isAfter()可以判断日期和时间的先后;
使用Duration和Period可以表示两个日期和时间的“区间间隔”

(1)对ZonedDateTime或LocalDateTime进行格式化,需要使用DateTimeFormatter类
(2)DateTimeFormatter可以通过格式化字符串和Locale对日期和时间进行定制输出

使用旧的Date对象时,用SimpleDateFormat进行格式化显示。使用新的LocalDateTime或ZonedLocalDateTime时,要进行格式化显示,就要使用DateTimeFormatter。和SimpleDateFormat不同的是,DateTimeFormatter不但是不变对象,它还是线程安全的。线程的概念我们会在后面涉及到。现在我们只需要记住:因为SimpleDateFormat不是线程安全的,使用的时候,只能在方法内部创建新的局部变量。而DateTimeFormatter可以只创建一个实例,到处引用

这种方式可以按照Locale默认习惯格式化

ZonedDateTime zdt = ZonedDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");
System.out.println(formatter.format(zdt));

DateTimeFormatter zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
System.out.println(zhFormatter.format(zdt));

DateTimeFormatter usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
System.out.println(usFormatter.format(zdt));

当直接调用System.out.println()对一个ZonedDateTime或者LocalDateTime实例进行打印的时候,实际上,调用的是它们的toString()方法,默认的toString()方法显示的字符串就是按照ISO 8601格式显示的,我们可以通过DateTimeFormatter预定义的几个静态变量来引用:

LocalDateTime ldt = LocalDateTime.now();
System.out.println(DateTimeFormatter.ISO_DATE.format(ldt));
System.out.println(DateTimeFormatter.ISO_DATE_TIME.format(ldt));

得到的输出和toString()类似:
2019-09-15
2019-09-15T23:16:51.56217


从字符串解析一个LocalDate类型和解析LocalTime一样简单:

DateTimeFormatter germanFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
			.withLocale(Locale.GERMAN);

LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
System.out.println(xmas); // 2014-12-24

日期转换

由于Java提供了新旧两套日期和时间的API,除非涉及到遗留代码,否则应该坚持使用新的API

旧API转新API

如果要把旧式的Date或Calendar转换为新API对象,可以通过toInstant()方法转换为Instant对象,再继续转换为ZonedDateTime:

Instant ins1 = new Date().toInstant();// Date -> Instant
// Calendar -> Instant -> ZonedDateTime
Calendar calendar = Calendar.getInstance();
Instant ins2 = Calendar.getInstance().toInstant();
ZonedDateTime zdt = ins2.atZone(calendar.getTimeZone().toZoneId());

从上面的代码还可以看到,旧的TimeZone提供了一个toZoneId(),可以把自己变成新的ZoneId


新API转旧API

如果要把新的ZonedDateTime转换为旧的API对象,只能借助long型时间戳做一个“中转”:

ZonedDateTime zdt = ZonedDateTime.now();// ZonedDateTime -> long
long ts = zdt.toEpochSecond() * 1000;
Date date = new Date(ts);// long -> Date
Calendar calendar = Calendar.getInstance();// long -> Calendar
calendar.clear();
calendar.setTimeZone(TimeZone.getTimeZone(zdt.getZone().getId()));
calendar.setTimeInMillis(zdt.toEpochSecond() * 1000);

从上面的代码还可以看到,新的ZoneId转换为旧的TimeZone,需要借助ZoneId.getId()返回的String完成。


在数据库中存储日期和时间

除了旧式的java.util.Date,还可以找到另一个java.sql.Date,它继承自java.util.Date,但会自动忽略所有时间相关信息。这个奇葩的设计原因要追溯到数据库的日期与时间类型

在数据库中,也存在几种日期和时间类型:
(1)DATETIME:表示日期和时间
(2)DATE:仅表示日期
(3)TIME:仅表示时间
(4)TIMESTAMP:和DATETIME类似,但是数据库会在创建或者更新记录的时候同时修改TIMESTAMP

在使用Java程序操作数据库时,我们需要把数据库类型与Java类型映射起来。下表是数据库类型与Java新旧API的映射关系:

数据库 对应Java类(旧) 对应Java类(新)
DATETIME java.util.Date LocalDateTime
DATE java.sql.Date LocalDate
TIME java.sql.Time LocalTime
TIMESTAMP java.sql.Timestamp LocalDateTime

实际上,在数据库中,需要存储的最常用的是时刻(Instant),因为有了时刻信息,就可以根据用户自己选择的时区,显示出正确的本地时间。所以,最好的方法是直接用长整数long表示,在数据库中存储为BIGINT类型

通过存储一个long型时间戳,可以编写一个timestampToString()的方法,非常简单地为不同用户以不同的偏好来显示不同的本地时间

public static void main(String[] args) {
	long ts = 1574208900000L;
	System.out.println(timestampToString(ts, Locale.CHINA, "Asia/Shanghai"));
	System.out.println(timestampToString(ts, Locale.US, "America/New_York"));
}

static String timestampToString(long epochMilli, Locale lo, String zoneId) {
	Instant ins = Instant.ofEpochMilli(epochMilli);
	DateTimeFormatter f = DateTimeFormatter
			.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT);
	return f.withLocale(lo).format(ZonedDateTime.ofInstant(ins, ZoneId.of(zoneId)));
}

(1)处理日期和时间时,尽量使用新的java.time包
(2)在数据库中存储时间戳时,尽量使用long型时间戳,它具有省空间,效率高,不依赖数据库的优点





上一篇:Java包装类                        下一篇:异常(throwable)




你可能感兴趣的:(Java,java,开发语言)