理解时间和日期库需要理解如下问题: @anarkh
- Java8之前的Date有哪些槽点?
- Java8之前使用哪些常用的第三方时间库?
- Java8关于时间和日期有哪些类和方法,变比Java8之前它的特点是什么?
- 其它语言时间库?
Java8之前的Date有哪些槽点
Tiago Fernandez做过一次投票,选举最烂的JAVA API,排第一的EJB2.X,第二的就是日期API。
槽点一
最开始的时候,Date既要承载日期信息,又要做日期之间的转换,还要做不同日期格式的显示,职责较繁杂(不懂单一职责,你妈妈知道吗? 纯属恶搞~哈哈)
后来从JDK 1.1 开始,这三项职责分开了:
使用Calendar类实现日期和时间字段之间转换;
使用DateFormat类来格式化和分析日期字符串;
而Date只用来承载日期和时间信息。原有Date中的相应方法已废弃。不过,无论是Date,还是Calendar,都用着太不方便了,这是API没有设计好的地方。
槽点二
坑爹的year和month
Date date = new Date(2012,1,1);
System.out.println(date);
输出Thu Feb 01 00:00:00 CST 3912观察输出结果,year是2012+1900,而month,月份参数我不是给了1吗? 怎么输出二月(Feb)了?
应该曾有人告诉你,如果你要设置日期,应该使用 java.util.Calendar,像这样…
Calendar calendar = Calendar.getInstance();
calendar.set(2013, 8, 2);这样写又不对了,calendar的month也是从0开始的,表达8月份应该用7这个数字,要么就干脆用枚举
calendar.set(2013, Calendar.AUGUST, 2);注意上面的代码,Calendar年份的传值不需要减去1900(当然月份的定义和Date还是一样),这种不一致真是让人抓狂!
有些人可能知道,Calendar相关的API是IBM捐出去的,所以才导致不一致。
槽点三
java.util.Date与java.util.Calendar中的所有属性都是可变的
下面的代码,计算两个日期之间的天数….
public static void main(String[] args) {
Calendar birth = Calendar.getInstance();
birth.set(1975, Calendar.MAY, 26);
Calendar now = Calendar.getInstance();
System.out.println(daysBetween(birth, now));
System.out.println(daysBetween(birth, now));
}
public static long daysBetween(Calendar begin, Calendar end) {
long daysBetween = 0;
while(begin.before(end)) {
begin.add(Calendar.DAY_OF_MONTH, 1);
daysBetween++;
}
return daysBetween;
}daysBetween有点问题,如果连续计算两个Date实例的话,第二次会取得0,因为Calendar状态是可变的,考虑到重复计算的场合,最好复制一个新的Calendar
public static long daysBetween(Calendar begin, Calendar end) {
Calendar calendar = (Calendar) begin.clone();
long daysBetween = 0;
while(calendar.before(end)) {
calendar.add(Calendar.DAY_OF_MONTH, 1);
daysBetween++;
}
return daysBetween;
}槽点四
SimpleDateTimeFormat是非线程安全的。
Java8时间和日期
类概览
Java 8仍然延用了ISO的日历体系,并且与它的前辈们不同,java.time包中的类是不可变且线程安全的。新的时间及日期API位于java.time包中,下面是里面的一些关键的类:
- Instant——它代表的是时间戳
- LocalDate——不包含具体时间的日期,比如2014-01-14。它可以用来存储生日,周年纪念日,入职日期等。
- LocalTime——它代表的是不含日期的时间
- LocalDateTime——它包含了日期及时间,不过还是没有偏移信息或者说时区。
- ZonedDateTime——这是一个包含时区的完整的日期时间,偏移量是以UTC/格林威治时间为基准的。
新的库还增加了ZoneOffset及Zoned,可以为时区提供更好的支持。有了新的DateTimeFormatter之后日期的解析及格式化也变得焕然一新了。
方法概览
该包的API提供了大量相关的方法,这些方法一般有一致的方法前缀:
- of: 静态工厂方法。
- parse: 静态工厂方法,关注于解析。
- get: 获取某些东西的值。
- is: 检查某些东西的是否是true。
- with: 不可变的setter等价物。
- plus: 加一些量到某个对象。
- minus: 从某个对象减去一些量。
- to: 转换到另一个类型。
- at: 把这个对象与另一个对象组合起来,例如: date.atTime(time)。
一些例子
public class TimeIntroduction {
public static void testClock() throws InterruptedException {
Clock c1 = Clock.systemUTC();
System.out.println(c1.millis());
Clock c2 = Clock.systemDefaultZone();
Clock c31 = Clock.system(ZoneId.of("Europe/Paris"));
System.out.println(c31.millis());
Clock c32 = Clock.system(ZoneId.of("Asia/Shanghai"));
System.out.println(c32.millis());
Clock c4 = Clock.fixed(Instant.now(), ZoneId.of("Asia/Shanghai"));
System.out.println(c4.millis());
Thread.sleep(1000);
System.out.println(c4.millis());
Clock c5 = Clock.offset(c1, Duration.ofSeconds(2));
System.out.println(c1.millis());
System.out.println(c5.millis());
}
public static void testInstant() {
Instant instant1 = Instant.now();
System.out.println(instant1.getEpochSecond());
System.out.println(instant1.toEpochMilli());
Clock clock1 = Clock.systemUTC();
Instant instant2 = Instant.now(clock1);
System.out.println(instant2.toEpochMilli());
Clock clock2 = Clock.fixed(instant1, ZoneId.systemDefault());
Instant instant3 = Instant.now(clock2);
System.out.println(instant3.toEpochMilli());
}
public static void testLocalDateTime() {
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
LocalDateTime now2 = LocalDateTime.now(ZoneId.of("Europe/Paris"));
System.out.println(now2);
Clock clock = Clock.system(ZoneId.of("Asia/Dhaka"));
LocalDateTime now3 = LocalDateTime.now(clock);
System.out.println(now3);
LocalDateTime d1 = LocalDateTime.of(2013, 12, 31, 23, 59);
LocalDateTime d2 = LocalDateTime.of(2013, 12, 31, 23, 59, 59, 11);
Instant instant = Instant.now();
LocalDateTime d3 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
System.out.println(d3);
LocalDateTime d4 = LocalDateTime.parse("2013-12-31T23:59");
System.out.println(d4);
LocalDateTime d5 = LocalDateTime.parse("2013-12-31T23:59:59.999");
System.out.println(d5);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
LocalDateTime d6 = LocalDateTime.parse("2013/12/31 23:59:59", formatter);
System.out.println(formatter.format(d6));
System.out.println(d6.getYear());
System.out.println(d6.getMonth());
System.out.println(d6.getDayOfYear());
System.out.println(d6.getDayOfMonth());
System.out.println(d6.getDayOfWeek());
System.out.println(d6.getHour());
System.out.println(d6.getMinute());
System.out.println(d6.getSecond());
System.out.println(d6.getNano());
LocalDateTime d7 = d6.minusDays(1);
LocalDateTime d8 = d7.plus(1, IsoFields.QUARTER_YEARS);
System.out.println(d1.equals(d2));
LocalDate dateOfBirth = LocalDate.of(2010, 01, 14);
MonthDay birthday = MonthDay.of(dateOfBirth.getMonth(), dateOfBirth.getDayOfMonth());
MonthDay currentMonthDay = MonthDay.from(today);
System.out.println(currentMonthDay.equals(birthday));
YearMonth currentYearMonth = YearMonth.now(); System.out.printf("Days in month year %s: %d%n", currentYearMonth, currentYearMonth.lengthOfMonth());
YearMonth creditCardExpiry = YearMonth.of(2018, Month.FEBRUARY);
System.out.printf("Your credit card expires on %s %n", creditCardExpiry);
System.out.println(dateOfBirth.isLeapYear());
}
public static void testZonedDateTime() {
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now);
ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("Europe/Paris"));
System.out.println(now2);
ZonedDateTime z1 = ZonedDateTime.parse("2013-12-31T23:59:59Z[Europe/Paris]");
System.out.println(z1);
}
public static void testDuration() {
Duration d1 = Duration.between(Instant.ofEpochMilli(System.currentTimeMillis() - 12323123), Instant.now());
System.out.println(d1.toDays());
System.out.println(d1.toHours());
System.out.println(d1.toMinutes());
System.out.println(d1.toMillis());
System.out.println(d1.toNanos());
Duration d2 = Duration.ofDays(1);
System.out.println(d2.toDays());
}
public static void testChronology() {
Chronology c = HijrahChronology.INSTANCE;
ChronoLocalDateTime d = c.localDateTime(LocalDateTime.now());
System.out.println(d);
}
public static void testNewOldDateConversion(){
Instant instant=new Date().toInstant();
Date date=Date.from(instant);
System.out.println(instant);
System.out.println(date);
}
public static void main(String[] args) throws InterruptedException {
testClock();
testInstant();
testLocalDateTime();
testZonedDateTime();
testDuration();
testChronology();
testNewOldDateConversion();
}
}其它语言时间
日期与时间处理API,在各种语言中,可能都只是个不起眼的API,如果你没有较复杂的时间处理需求,可能只是利用日期与时间处理API取得系统时间,简单做些显示罢了,然而如果认真看待日期与时间,其复杂程度可能会远超过你的想象,天文、地理、历史、政治、文化等因素,都会影响到你对时间的处理。所以在处理时间上,最好选用JSR310(如果你用java8的话就实现310了),或者Joda-Time。
不止是java面临时间处理的尴尬,其他语言同样也遇到过类似的问题,比如
Arrow: Python 中更好的日期与时间处理库
Moment.js: JavaScript 中的日期库
Noda-Time: .NET 阵营的 Joda-Time 的复制
总结
看完了这些例子后,我相信你已经对Java 8这套新的时间日期API有了一定的了解了。现在我们来回顾下关于这个新的API的一些关键的要素。
它提供了javax.time.ZoneId用来处理时区。
它提供了LocalDate与LocalTime类 Java 8中新的时间与日期API中的所有类都是不可变且线程安全的,这与之前的Date与Calendar API中的恰好相反,那里面像java.util.Date以及SimpleDateFormat这些关键的类都不是线程安全的。
新的时间与日期API中很重要的一点是它定义清楚了基本的时间与日期的概念,比方说,瞬时时间,持续时间,日期,时间,时区以及时间段。它们都是基于ISO日历体系的。
每个Java开发人员都应该至少了解这套新的API中的这五个类: Instant 它代表的是时间戳,比如2014-01-14T02:20:13.592Z,这可以从java.time.Clock类中获取,像这样: Instant current = Clock.system(ZoneId.of(“Asia/Tokyo”)).instant(); LocalDate 它表示的是不带时间的日期,比如2014-01-14。它可以用来存储生日,周年纪念日,入职日期等。 LocalTime – 它表示的是不带日期的时间 LocalDateTime – 它包含了时间与日期,不过没有带时区的偏移量 ZonedDateTime – 这是一个带时区的完整时间,它根据UTC/格林威治时间来进行时区调整
这个库的主包是java.time,里面包含了代表日期,时间,瞬时以及持续时间的类。它有两个子package,一个是java.time.foramt,这个是什么用途就很明显了,还有一个是java.time.temporal,它能从更低层面对各个字段进行访问。
时区指的是地球上共享同一标准时间的地区。每个时区都有一个唯一标识符,同时还有一个地区/城市(Asia/Tokyo)的格式以及从格林威治时间开始的一个偏移时间。比如说,东京的偏移时间就是+09:00。 OffsetDateTime类实际上包含了LocalDateTime与ZoneOffset。它用来表示一个包含格林威治时间偏移量(+/-小时: 分,比如+06:00或者 -08: 00)的完整的日期(年月日)及时间(时分秒,纳秒)。 DateTimeFormatter类用于在Java中进行日期的格式化与解析。与SimpleDateFormat不同,它是不可变且线程安全的,如果需要的话,可以赋值给一个静态变量。DateTimeFormatter类提供了许多预定义的格式器,你也可以自定义自己想要的格式。当然了,根据约定,它还有一个parse()方法是用于将字符串转换成日期的,如果转换期间出现任何错误,它会抛出DateTimeParseException异常。类似的,DateFormatter类也有一个用于格式化日期的format()方法,它出错的话则会抛出DateTimeException异常。
再说一句,“MMM d yyyy”与“MMm dd yyyy”这两个日期格式也略有不同,前者能识别出”Jan 2 2014″与”Jan 14 2014″这两个串,而后者如果传进来的是”Jan 2 2014″则会报错,因为它期望月份处传进来的是两个字符。为了解决这个问题,在天为个位数的情况下,你得在前面补0,比如”Jan 2 2014″应该改为”Jan 02 2014″。