Java 中的日期和时间(上)
目录
自从 JDK 1.0 开始,Java 就提供了 Date 来处理时间和日期,作为老古董自然有很多东西是过时的。然后出现了 Calendar 来解决了很多问题,但是 Calendar 使用比较复杂,并且有些反人类的地方。直到 Java 8 中 LocalDateTime 中的出现,它吸收了 Joda-Time 库的经验,使得 Java 处理时间和日期变得比较“人性化”了。本篇介绍 Java 中的 Date 、 Calendar,以及 SimpleDateFormat 的使用。
Date
构造方法
Date 类的构造方法如下:
public Date() {
this(System.currentTimeMillis());
}
public Date(long date) {
fastTime = date;
}
@Deprecated
public Date(int year, int month, int date) {
this(year, month, date, 0, 0, 0);
}
@Deprecated
public Date(int year, int month, int date, int hrs, int min) {
this(year, month, date, hrs, min, 0);
}
@Deprecated
public Date(int year, int month, int date, int hrs, int min, int sec) {
int y = year + 1900;
// month is 0-based. So we have to normalize month to support Long.MAX_VALUE.
if (month >= 12) {
y += month / 12;
month %= 12;
} else if (month < 0) {
y += CalendarUtils.floorDivide(month, 12);
month = CalendarUtils.mod(month, 12);
}
BaseCalendar cal = getCalendarSystem(y);
cdate = (BaseCalendar.Date) cal.newCalendarDate(TimeZone.getDefaultRef());
cdate.setNormalizedDate(y, month + 1, date).setTimeOfDay(hrs, min, sec, 0);
getTimeImpl();
cdate = null;
}
可以看到6个构造方法中,有4个已经被添加了 @Deprecated 的注解了,尚未过时的只有两个: Date() 和 Date(long date)。其中,第一个构造方法调用的是 System.currentTimeMillis() 方法,这个方法返回的是一个 long 整数,表示从 GMT 1970年1月1日 00:00:00 到现在所经历的毫秒数。这个毫秒数很重要,无论是在 Date 类中还是 Calendar 类中,这个毫秒数都是计算时间日期的基准。
怎么获得这个 long 的毫秒数呢?需要调用 getTime() 方法,注意并不是调用 toString() 方法,它们两者的差异可以从下面的代码中体现出来:
public static void main(String[] args) {
Date date = new Date();
System.out.println(date.getTime()); // 1489320281826
System.out.println(date.toString()); // Sun Mar 12 20:04:41 CST 2017
}
而第二个构造方法的参数 date,表示创建的 Date 对象与 GMT 1970年1月1日 00:00:00 的时间差。
Date date = new Date(60 * 60 * 1000);
System.out.println(date.getTime()); // 36000000
System.out.println(date.toString()); // Thu Jan 01 09:00:00 CST 1970
为什么不是 1 点呢?原因在于 CST(China Standard Time)是指的北京时间,而 GMT(Greenwich Mean Time)是指的是格林尼治标准时间。由于北京处于东八区,比 GMT 早8小时,所以打印的时间指的是北京时间 1970 年 1 月 1 日上午 9 点。
其它常用方法
谈完了构造方法,下面说说 Date 这个古老的类中可以使用的方法。
public long getTime() :返回从 GMT 1970年1月1日 00:00:00 到这个类创建的毫秒数。
public void setTime(long time) :设置该 Date 对象的时间。参数 time 表示从 GMT 1970年1月1日 00:00:00 后的毫秒数。
public boolean after(Date when) :测试该日期是否是在指定的日期之后。
public boolean before(Date when) :测试该日期是否是在指定的日期之前。
Date 类中的构造方法和常用的未过时的方法基本就是这些了。看了之后是不是觉得其实 Date 类能用的东西很少?很多日期和时间的操作都很难实现,这时候就要使用到 Calendar 类,或者使用 JDK 8 中的日期时间包。
庞大的 Calendar
构造方法
与 Date 类不同,Calendar 类是一个抽象类,其直接子类是 GregorianCalendar。既然 Calendar 类是抽象类,需要使用 Calendar calendar = Calendar.getInstance(); 来创建一个 Calendar 对象。和 Date 类一样,创建好对象之后,便包含了当前日期和时间的信息。
实际上,getInstance() 这个静态方法还有三个重载方法,它们的源码如下:
public static Calendar getInstance() {
return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
}
public static Calendar getInstance(TimeZone zone) {
return createCalendar(zone, Locale.getDefault(Locale.Category.FORMAT));
}
public static Calendar getInstance(Locale aLocale) {
return createCalendar(TimeZone.getDefault(), aLocale);
}
public static Calendar getInstance(TimeZone zone, Locale aLocale) {
return createCalendar(zone, aLocale);
}
显然,这 4 个重载方法的区别在于时区和地区的不同。而它们都是通过创建子类 GregorianCalendar 的对象来返回一个 Calendar 对象,这其实是多态的一种应用,也是设计模式中工厂模式的应用。
get() 方法
Date 对象可以通过 getTime() 方法和 toString() 方法,能很容易的取到对应的时间信息。而在 Calendar 对象中,获取到时间和日期并不这么直接。在 Calendar 对象中,getTime() 方法返回的是一个 Date 对象,至于 toString() 方法的输出结果……
java.util.GregorianCalendar[time=1489323573250,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=19,lastRule=null],firstDayOfWeek=1,minimalDaysI
nFirstWeek=1,ERA=1,YEAR=2017,MONTH=2,WEEK_OF_YEAR=11,WEEK_OF_MONTH=3,DAY_OF_MONTH=12,DAY_OF_YEAR=71,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=2,AM_PM=1,HOUR=8,HOUR_OF_DAY=20,MINUTE=59,SECOND=33,MILLISECOND=250,ZONE_OFFSET=28800000,DST_OFFSET=0]
这从侧面反映了 Calendar 比 Date 强大,能提供更多的信息。其实在 Calendar 类中,有一点和 Date 类是一样的:它们都会保存一个毫秒值。Calendar 类对于时间日期的各种计算也都是基于这个值来的。toString() 方法返回的第一个值就是它,足以说明这个值的重要性。
要获取 Calendar 对象所提供的信息,需要调用 get() 方法。
public int get(int field) {
complete();
return internalGet(field);
}
protected final int internalGet(int field) {
return fields[field];
}
在 complete() 方法中,会调用 computeTime() 和 computeFields() 方法,这两个方法都是抽象方法,由子类 GregorianCalendar 实现。在 computeTime() 方法中,计算出年月日时分秒等时间相关的信息,而在 computeFields() 方法中获取到对应的时区地区等信息。这两个方法会将所计算出来的信息保存在 int 类型的数组 fields。fields数组保存的信息如下所示:
public static final int ERA = 0;
public static final int YEAR = 1;
public static final int MONTH = 2;
public static final int WEEK_OF_YEAR = 3;
public static final int WEEK_OF_MONTH = 4;
public static final int DATE = 5;
public static final int DAY_OF_MONTH = 5;
public static final int DAY_OF_YEAR = 6;
public static final int DAY_OF_WEEK = 7;
public static final int DAY_OF_WEEK_IN_MONTH = 8;
public static final int AM_PM = 9;
public static final int HOUR = 10;
public static final int HOUR_OF_DAY = 11;
public static final int MINUTE = 12;
public static final int SECOND = 13;
public static final int MILLISECOND = 14;
public static final int ZONE_OFFSET = 15;
public static final int DST_OFFSET = 16;
get() 方法中的参数就是上述 18 个常量之一。其中:
ERA 表示该日期是在公元元年之前还是之后,返回 0 表示在公元元年之前,返回 1 表示在公元元年之后。
AM_PM 表示该时间是在中午 12 点之前还是之后,返回 0 表示在中午 12 点之前,返回 1 表示在中午 12 点之后。
DST_OFFSET 表示该时间距夏令时的毫秒数。
set() 方法
public void set(int field, int value)
{
// If the fields are partially normalized, calculate all the fields before changing any fields.
if (areFieldsSet && !areAllFieldsSet) {
computeFields();
}
internalSet(field, value);
isTimeSet = false;
areFieldsSet = false;
isSet[field] = true;
stamp[field] = nextStamp++;
if (nextStamp == Integer.MAX_VALUE) {
adjustStamp();
}
}
public final void set(int year, int month, int date)
{
set(YEAR, year);
set(MONTH, month);
set(DATE, date);
}
public final void set(int year, int month, int date, int hourOfDay, int minute)
{
set(YEAR, year);
set(MONTH, month);
set(DATE, date);
set(HOUR_OF_DAY, hourOfDay);
set(MINUTE, minute);
}
public final void set(int year, int month, int date, int hourOfDay, int minute, int second)
{
set(YEAR, year);
set(MONTH, month);
set(DATE, date);
set(HOUR_OF_DAY, hourOfDay);
set(MINUTE, minute);
set(SECOND, second);
}
原理很简单,就是将我们传入的值存放到 fields 数组中。调用 set() 方法会改变当前 Calendar 对象的日期和时间。
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.MONTH, 3);
System.out.println(calendar.get(Calendar.MONTH)); // 3
calendar.set(2017, Calendar.MARCH, 12);
System.out.println(calendar.get(Calendar.YEAR)); // 2017
System.out.println(calendar.get(Calendar.MONTH)); // 2
System.out.println(calendar.get(Calendar.DAY_OF_MONTH)); // 12
}
由于月份是从 0 开始计数的,所以计算月份的时候需要特别小心。这就是 Calendar 反人类的地方之一。
add() 和 roll() 方法
使用 get() 和 set() 方法可以获取当前日期和时间,也可以设置 Calendar 对象的日期和时间。但是一些简单的日期时间计算就需要使用到 add() 和 roll() 方法了。
add(int field, int amount) 方法接受两个参数,第一个参数表示希望改变的字段,这个字段是 fields 数组中的某一个,第二个参数是改变的值。使用 add() 方法需要注意几点:
月份是从 0 开始计数的。
第二个参数如果为正,则数据增加;如果为负,则数据减少。
当增加的数值超过法定额度时,会自动向更高一级的单位加 1。
roll() 方法和 add() 类似,二者最主要的区别在于 roll() 方法中不会进行进位和退位运算。
其它常用方法
getMaximum(int field) 和 getMinimum(int field) 两个方法,返回 Calendar 对象的 fields 数组中对应数据的最大值和最小值。例如:
Calendar calendar = Calendar.getInstance();
System.out.println(calendar.getMinimum(Calendar.DAY_OF_MONTH)); // 1
System.out.println(calendar.getMaximum(Calendar.DAY_OF_MONTH)); // 31
getActualMaximum(int field) 和 getActualMinimum(int field) 两个方法同上面两个方法相比,多了一个 Actual 单词,表示返回 Calendar 对象的 fields 数组中,在当前日期的环境条件下,对应数据的最大值和最小值。例如:
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.MONTH, 1);
System.out.println(calendar.getMinimum(Calendar.DAY_OF_MONTH)); // 1
System.out.println(calendar.getMaximum(Calendar.DAY_OF_MONTH)); // 28
注意,这里设置的是二月。fields 数组中的各个字段的最大值和最小值如下:
private static int[] maximums = new int[] { 1, 292278994, 11, 53, 6, 31, 366, 7, 6, 1, 11, 23, 59, 59, 999, 14 * 3600 * 1000, 7200000 };
private static int[] minimums = new int[] { 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, -13 * 3600 * 1000, 0 };
Date 和 Calendar 的关系
Calendar 作为 Date 类的补充和替代,当然得和 Date 进行相互转化。
使用 getTime() 方法,从 Calendar 对象中获取 Date 对象。
使用 setTime() 方法,将 Date 对象的值赋给 Calendar 对象。
Date 的 6 个构造方法中,有 4 个已经过时了,其实它们是被 Calendar 类的相关方法替代了。
Date(int year, int month, int date) 被 Calendar.set(year + 1900, month, date) 替代。
Date(int year, int month, int date, int hrs, int min) 被 Calendar.set(year + 1900, month, date, hrs, min) 替代。
Date(int year, int month, int date, int hrs, int min, int sec) 被 Calendar.set(year + 1900, month, date, hrs, min, sec) 替代。
Date(String s) 被 DateFormat.parse(String s) 替代。
SimpleDateFormat
有了 Date 和 Calendar,我们能获取并设置日期和时间,也能对日期和时间进行简单的计算。SimpleDateFormat 可以对日期和时间进行格式化。
SimpleDateFormat 的构造方法如下:
public SimpleDateFormat() {
this("", Locale.getDefault(Locale.Category.FORMAT));
applyPatternImpl(LocaleProviderAdapter.getResourceBundleBased().getLocaleResources(locale).getDateTimePattern(SHORT, SHORT, calendar));
}
public SimpleDateFormat(String pattern)
{
this(pattern, Locale.getDefault(Locale.Category.FORMAT));
}
public SimpleDateFormat(String pattern, Locale locale)
{
if (pattern == null || locale == null) {
throw new NullPointerException();
}
initializeCalendar(locale);
this.pattern = pattern;
this.formatData = DateFormatSymbols.getInstanceRef(locale);
this.locale = locale;
initialize(locale);
}
public SimpleDateFormat(String pattern, DateFormatSymbols formatSymbols)
{
if (pattern == null || formatSymbols == null) {
throw new NullPointerException();
}
this.pattern = pattern;
this.formatData = (DateFormatSymbols) formatSymbols.clone();
this.locale = Locale.getDefault(Locale.Category.FORMAT);
initializeCalendar(this.locale);
initialize(this.locale);
useDateFormatSymbols = true;
}
字符串 pattern 代表日期和时间格式化的样式,例如:
String pattern = "yyyy/MM/dd HH:mm:ss";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
System.out.println(simpleDateFormat.format(new Date())); // 2017/03/12 21:43:59
pattern 中的字符含义如下表,注意区分大小写:
| Letter | Date or Time Component | Presentation | Examples |
|---|---|---|---|
| G | Era designator | Text | AD |
| y | Year | Year | 1996; 96 |
| Y | Week year | Year | 2009; 09 |
| M | Month in year (context sensitive) | Month | July; Jul; 07 |
| L | Month in year (standalone form) | Month | July; Jul; 07 |
| w | Week in year | Number | 27 |
| W | Week in month | Number | 2 |
| D | Day in year | Number | 189 |
| d | Day in month | Number | 10 |
| F | Day of week in month | Number | 2 |
| E | Day name in week | Text | Tuesday; Tue |
| u | Day number of week (1 = Monday, …, 7 = Sunday) | Number | 1 |
| a | Am/pm marker | Text | PM |
| H | Hour in day (0-23) | Number | 0 |
| k | Hour in day (1-24) | Number | 24 |
| K | Hour in am/pm (0-11) | Number | 0 |
| h | Hour in am/pm (1-12) | Number | 12 |
| m | Minute in hour | Number | 30 |
| s | Second in minute | Number | 55 |
| S | Millisecond | Number | 978 |
| z | Time zone | General time zone | Pacific Standard Time; PST; GMT-08:00 |
| Z | Time zone | RFC 822 time zone | -0800 |
| X | Time zone | ISO 8601 time zone | -08; -0800; -08:00 |
SimpleDateFormat 类除了能将 Date 对象进行格式化返回字符串,也能将一个日期字符串解析一个 Date 对象。
String s = "2017#03#12#21#49#28";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy#MM#dd#HH#mm#ss");
Date date = null;
try {
date = simpleDateFormat.parse(s);
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(date); // Sun Mar 12 21:49:28 CST 2017
注意,数据之间需要用 # 来分割。
最后说一点,SimpleDateFormat 类不是线程安全的,如果有多个线程需要使用到 SimpleDateFormat 对象,建议每个线程单独创建,如果多个线程要获取同一个 SimpleDateFormat 对象,记得要加锁。
请看下集。