基本概念
- 日期:2019-11-20
- 时间:12:30:59
- 本地时间:不同时区同一时刻,本地时间不同
- 夏令时
- 时区:GMT/UTC+08:00表示东八区
- 本地化:Locale由语言_国家的字母缩写构成。zh_CN表示中文+中国;en_US表示英文+美国
通常使用Locale表示一个国家或地区的日期、时间、数字、货币等格式。
Date和Calendar
时间戳Epoch Time
计算从1970年1月1日零点(格林威治时区/GMT+00:00)到现在所经历的秒数。例如1574208900表示从从1970年1月1日零点GMT时区到该时刻一共经历了1574208900秒,换算成伦敦、北京和纽约时间分别是:
1574208900 = 北京时间2019-11-20 8:15:00
= 伦敦时间2019-11-20 0:15:00
= 纽约时间2019-11-19 19:15:00
时间戳在Java中是long表示的毫秒数
获取当前时间戳:System.currentTimeMillis()
标准库API
处理时间和日期的API:
- 定义在java.util包里,主要包括Date、Calendar和TimeZone这几个类
- Java8引入的,定义在java.time包里,包括LocalDateTime、ZoneDateTime、ZoneId等。
Date
import java.util.*;
public class Main {
public static void main(String[] args) {
//获取当前时间
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
// 转换为String:
System.out.println(date.toString());
// 转换为GMT时区:
System.out.println(date.toGMTString());
// 转换为本地时区:
System.out.println(date.toLocaleString());
}
}
使用SimpleDateFormat对一个Date进行转换,控制日期和时间的格式:
Date date = new Date();
var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//年-月-日 时:分:秒
System.out.println(sdf.format(date));
Date不能转换时区,除了toGMTString()可以按照GMT+0:00输出。Date总是按照当前计算机系统默认时区为基础进行输出。
Calendar
Calendar获取年月日这些信息变成了get(int field),返回的年份不必转换,返回的月份仍然要加1,返回的星期要特别注意,1~7分别表示周日,周一,……,周六。
Calendar只有一种方式获取,即Calendar.getInstance(),而且一获取到就是当前时间。如果我们想给它设置成特定的一个日期和时间,就必须先清除所有字段(clear方法)。利用Calendar.getTime()可以将一个Calendar对象转换成Date对象,然后就可以用SimpleDateFormat进行格式化了。
TimeZone
Calendar和Date相比,它提供了时区转换的功能。时区用TimeZone对象表示。
- 获取当前时区:
TimeZone tzDefault = TimeZone.getDefault();
- 获取时区标识(以字符串标识的ID):
tzDefault.getID(); //Asia/Shanghai
要列出系统支持的所有ID,请使用TimeZone.getAvailableIDs()。
有了时区,我们就可以对指定时间进行转换。例如,下面的例子演示了如何将北京时间2019-11-20 8:15:00转换为纽约时间:
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
// 当前时间:
Calendar c = Calendar.getInstance();
// 清除所有:
c.clear();
// 设置为北京时区:
c.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
// 设置年月日时分秒:
c.set(2019, 10 /* 11月 */, 20, 8, 15, 0);
// 显示时间:
var 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在显示的时候完成。
Calendar也可以对日期和时间进行简单的加减:add()方法。
LocalDateTime
从Java 8开始,java.time包提供了新的日期和时间API,主要涉及的类型有:
- 本地日期和时间:LocalDateTime,LocalDate,LocalTime;
- 带时区的日期和时间:ZonedDateTime;
- 时刻:Instant;
- 时区:ZoneId,ZoneOffset;
- 时间间隔:Duration。
以及一套新的用于取代SimpleDateFormat的格式化类型DateTimeFormatter
LocalDateTime dt = LocalDateTime.now(); // 当前日期和时间
LocalDate d = dt.toLocalDate(); // 转换到当前日期
LocalTime t = dt.toLocalTime(); // 转换到当前时间
通过now()方法获取到的本地日期和时间总是以当前默认时区返回的。
通过指定的日期和时间创建LocalDateTime可以通过of()方法。
通过parse()方法传入标准格式LocalDateTime dt = LocalDateTime.parse("2019-11-19T15:16:17");
日期和时间的分割符是T。
DateTimeFormatter
自定义输出格式:DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");//自定义格式化 System.out.println(dtf.format(LocalDateTime.now()));//用自定义格式解析
LocalDateTime提供了对日期和时间进行加减的非常简单的链式调用:plusDays() minusHours() minusMonths()方法等。
对日期和时间调整使用with()Xxx()方法。如WithHour(15)——将时设置成15
要判断两个LocalDateTime的先后,可以使用isBefore()、isAfter()方法,对于LocalDate和LocalTime类似
LocalDateTime无法与时间戳进行转换,因为LocalDateTime没有时区,无法确定某一时刻。
ZonedDateTime相当于LocalDateTime加时区的组合,它具有时区,可以与long表示的时间戳进行转换。
Duration和Period
Duration表示两个时刻之间的时间间隔。Period表示两个日期之间的天数。Duration d = Duration.between(start, end);//start和end都是LocalDateTime类型
Period p = LocalDate.of(2019, 11, 19).until(LocalDate.of(2020, 1, 9));
ZonedDateTime
ZonedDateTime可以表示带时区的日期和时间,相当于LocalDateTime+ZoneId。
//通过now()方法返回当前时间
ZonedDateTime zbj = ZonedDateTime.now();//默认时区
ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York"));//用指定时区获取当前时间
通过给一个LocalDateTime附加一个ZoneId,就可以变成ZonedDateTime:它的日期和时间与LocalDateTime相同,但附加的时区不同,因此是两个不同的时刻.
LocalDateTime ldt = LocalDateTime.of(2019,9,15,15,16,17);
ZonedDateTime zbj = ldt.atZone(ZoneId.SystemDefault());
时区转换
ZonedDateTime对象的withZoneSameInstant()方法将关联时区转换到另一个时区,转换后日期和时间都会相应调整。
//以中国时区获取当前时间
ZonedDateTime zbj = ZoneDateTime.now(ZoneId.of("Asia/Shanghai"));
//转换为纽约时间
ZonedDateTime zny = zbj.withZoneSameInstant(ZoneId.of("America/New_York"));
有了ZonedDateTime,将其转换为本地时间就非常简单:
ZonedDateTime zdt = …
LocalDateTime ldt = zdt.toLocalDateTime();
ZonedDateTime仍然提供了plusDays()等加减操作。
DateTimeFormatter
DateTimeFormatter是不变对象,线程安全的,用于格式化显示。
import java.time.*;
import java.time.format.*;
import java.util.Locale;
public class Main {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.now();
var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");
System.out.println(formatter.format(zdt));
var zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
System.out.println(zhFormatter.format(zdt));
var usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
System.out.println(usFormatter.format(zdt));
}
Instant
当前时间戳在java.time中以Instant类型表示,用Instant.now()获取当前时间戳。
Instance now = Instance.now();
now.getEpochSecond();//秒
now.toEpochMilli();//毫秒
//给某一个时间戳关联上ZoneId,就得到了ZonedDateTime,继而可以获取对应时区的LocalDateTime
// 以指定时间戳创建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]
最佳实践
旧API转新API
旧式的Date或Calendar转换为新API对象,通过toInstant()方法转换为Instant对象,再继续转换为ZonedDateTime:
// Date -> Instant:
Instant ins1 = new Date().toInstant();
// Calendar -> Instant -> ZonedDateTime:
Calendar calendar = Calendar.getInstance();
Instant ins2 = Calendar.getInstance().toInstant();
ZonedDateTime zdt = ins2.atZone(calendar.getTimeZone().toZoneId());
新API转旧API
ZonedDateTime转换为旧的API对象,只能借助long型时间戳做一个“中转”:
// ZonedDateTime -> long:
ZonedDateTime zdt = ZonedDateTime.now();
long ts = zdt.toEpochSecond() * 1000;
// long -> Date:
Date date = new Date(ts);
// long -> Calendar:
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.setTimeZone(TimeZone.getTimeZone(zdt.getZone().getId()));
calendar.setTimeInMillis(zdt.toEpochSecond() * 1000);
在数据库中存储时间和日期
在数据库中,我们需要存储的最常用的是时刻(Instant),因为有了时刻信息,就可以根据用户自己选择的时区,显示出正确的本地时间。所以,最好的方法是直接用长整数long表示,在数据库中存储为BIGINT类型。
通过存储一个long型时间戳,我们可以编写一个timestampToString()的方法,非常简单地为不同用户以不同的偏好来显示不同的本地时间:
import java.time.*;
import java.time.format.*;
import java.util.Locale;
public class Main {
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)));
}
}