Java 中设计日期工具类 DateTools 和日期工厂类 DateFactory 完善饱受诟病的原生 Date 类
Java 原生的日期时间类 Date 有很多体验不好的地方,比如里面的年份字段存的是距离 1900 年的年数,月份字段用 0~11 代表 1~12 月,用里面的 getYear(), getMonth() 等得到的都不是我们想要的结果,还要再进行额外的处理(setter 同理。这也是为什么这些方法在 JDK 1.1 之后马上就被标注为废弃的原因。)后来的 Calendar 类虽然有所改良,但为什么唯独留着那个月份字段不改,仍然是用 0~11 来存储,我百思不得其解。
好在还有一个 DateFormat 类算是比较友好的,日期字符串里的各个部分都是自然数,直接就能转换为 Date 对象,没有年 + 1900,月 + 1 这样的顾虑。那么我们现在就结合 Date 和 DateFormat 这两个类做一个日期工具类 DateTools 和日期工厂类 DateFactory,用于完善原生 Date 类。
首先我们来设计 DateFactory 工厂类,提供一些方法用于创建一个 Date 对象。先设计一个简单的方法:
public class DateFactory {
private static final DateFormat DATE_FORMAT;
private static final String STR_FORMAT;
static {
DATE_FORMAT = new SimpleDateFormat("yyyy-M-d H:m:s", Locale.CHINA);
STR_FORMAT = "%d-%d-%d %d:%d:%d";
} // static
public static Date create(int y, int m, int d, int h, int min, int sec) {
String dateString = String.format(STR_FORMAT, y, m, d, h, min, sec);
try {
return DATE_FORMAT.parse(dateString);
} // try
catch (ParseException e) {
throw new RuntimeException(e.toString());
} // catch (ParseException e)
} // create(int * 6)
} // DateFactory Class
在这个工厂方法里,我们将传入的年、月、日、时、分、秒组织起来,并按照【%d-%d-%d %d:%d:%d】的格式转化成日期字符串,这个字符串格式和【yyyy-M-d H:m:s】的日期格式是完全一致的。然后我们调用 DateFormat 类的 parse 方法,将日期字符串转换为 Date 对象并返回。
这个方法要求提供精确到秒的时间。我们还可以再创建精确到分或者小时的工厂方法:
public class DateFactory {
// 省略已有代码
// 精确到分钟
public static Date create(int y, int m, int d, int h, int min) {
return create(y, m, d, h, min, 0);
} // create(int * 5)
// 精确到小时
public static Date create(int y, int m, int d, int h) {
return create(y, m, d, h, 0, 0);
} // create(int * 4)
} // DateFactory Class
现在,假如我们要创建一个时间为 2019 年 3 月 1 日 18 点 30 分的 Date 对象,我们就可以执行下面这条语句来创建了:
Date date = DateFactory.create(2019, 3, 1, 18, 30);
然后,我们再提供一些方法用于创建相对时间,即比一个基准 Date 对象早/晚一定时间的 Date 对象。
public class DateFactory {
// 省略已有代码
// 创建比基准时间 date 要早 minutes 分钟的 Date 对象
public static Date backwardMinutes(Date date, int minutes) {
long millis = date.getTime();
millis -= 60000 * minutes;
return new Date(millis);
} // backwardMinutes()
// 创建比基准时间 date 要晚 minutes 分钟的 Date 对象
public static Date forwardMinutes(Date date, int minutes) {
long millis = date.getTime();
millis += 60000 * minutes;
return new Date(millis);
} // forwardMinutes()
} // DateFactory Class
Date 对象内部记录的其实是距离 1970 年 1 月 1 日零时的毫秒数,用 getTime() 方法获得,同样 Date 类也有一个传入毫秒数的构造器。所以,创建比基准时间早/晚若干分钟的 Date 对象,就是先将基准对象里记录的毫秒数提取出来,然后在此基础上减去/加上 minutes 的值乘以 60000(一分钟为 60000 毫秒),再使用 Date 类的有参构造方法传入新的毫秒数,创建新的 Date 对象返回即可。同理可以写出创建比基准时间早/晚若干秒、若干小时、若干天的 Date 对象的工厂方法。
但是我们该如何调整 Date 对象的某一个或某些字段的值呢?比如某个 Date 对象,我只想改它的月份,或者日期不动,只改时间。诚然,目前的 DateFactory 类还不够完善,但也请允许我暂时打个岔,去设计一下 DateTools 的工具类。设计完 DateTools 工具类后,上面的这些问题就能迎刃而解了。
我们在 DateTools 工具类中添加获得一个 Date 对象中各日期时间字段值的方法:
public class DateTools {
private static final DateFormat YEAR_SDF;
private static final DateFormat MON_SDF;
private static final DateFormat DAY_SDF;
private static final DateFormat HOUR_SDF;
private static final DateFormat MIN_SDF;
private static final DateFormat SEC_SDF;
static {
YEAR_SDF = new SimpleDateFormat("yyyy", Locale.CHINA);
MON_SDF = new SimpleDateFormat("M", Locale.CHINA);
DAY_SDF = new SimpleDateFormat("d", Locale.CHINA);
HOUR_SDF = new SimpleDateFormat("H", Locale.CHINA);
MIN_SDF = new SimpleDateFormat("m", Locale.CHINA);
SEC_SDF = new SimpleDateFormat("s", Locale.CHINA);
} // static
// 获得年份
public static int yearOf(Date date) {
return Integer.parseInt(YEAR_SDF.format(date));
} // yearOf()
// 获得月份
public static int monthOf(Date date) {
return Integer.parseInt(MON_SDF.format(date));
} // monthOf()
// 获得日期
public static int dayOf(Date date) {
return Integer.parseInt(DAY_SDF.format(date));
} // dayOf()
// 获得小时
public static int hourOf(Date date) {
return Integer.parseInt(HOUR_SDF.format(date));
} // hourOf()
// 获得分钟
public static int minuteOf(Date date) {
return Integer.parseInt(MIN_SDF.format(date));
} // minuteOf()
// 获得秒
public static int secondOf(Date date) {
return Integer.parseInt(SEC_SDF.format(date));
} // secondOf()
} // DateTools Class
在 DateTools 类里,我们给日期时间的每一个字段(年、月、日、时、分、秒)都设置了一个 SimpleDateFormat 对象用于解析,然后解析出来的其实是字符串表示的对应字段的值,接着我们再用 Integer.parseInt() 方法将它们转换成 int 类型并返回。这里所有方法返回的字段值都是符合人类直观的值,不会出现年要 + 1900,月要 +1 这样的荒唐事。
当然你还可以在 DateTools 类里添加其他和日期相关的方法,这里举一例,计算一个 Date 对象中的时间离当前时间多远,以字符串形式返回(例如“1 小时前”、“2 天后”等)。更多的方法我不再过多阐述,各位可以*添加。
public class DateTools {
// 省略已有代码
// 计算一个 Date 对象所记载的时间距今多久,并以字符串形式返回
public static String distanceToNow(Date date) {
long millisToNow;
long days, hours, minutes, seconds;
if (date != null) {
millisToNow = date.getTime() - System.currentTimeMillis();
if (millisToNow < 0) {
// 早于当前时间
if (millisToNow <= -86400000) {
days = -millisToNow / 86400000;
return String.format("%d 天前", days);
} // if (millisToNow <= -86400000)
else if (millisToNow <= -3600000) {
hours = -millisToNow / 3600000;
return String.format("%d 小时前", hours);
} // else if (millisToNow <= -3600000)
else if (millisToNow <= -60000) {
minutes = -millisToNow / 60000;
return String.format("%d 分钟前", minutes);
} // else if (millisToNow <= -60000)
else {
seconds = -millisToNow / 1000;
return String.format("%d 秒前", seconds);
} // else
} // if (millisToNow < 0)
else {
// 晚于当前时间
if (millisToNow >= 86400000) {
days = millisToNow / 86400000;
return String.format("%d 天后", days);
} // if (millisToNow >= 86400000)
else if (millisToNow >= 3600000) {
hours = millisToNow / 3600000;
return String.format("%d 小时后", hours);
} // else if (millisToNow >= 3600000)
else if (millisToNow >= 60000) {
minutes = millisToNow / 60000;
return String.format("%d 分钟后", minutes);
} // else if (millisToNow >= 60000)
else {
seconds = millisToNow / 1000;
return String.format("%d 秒后", seconds);
} // else
} // else
} // if (date != null)
else {
return "未知";
} // else
} // distanceToNow()
} // DateTools Class
现在让我们回到 DateFactory 工厂类。有了 DateTools 类的获得各字段值的方法后,我们可以很轻松地调整一个 Date 对象中各字段的值了。直接上代码:
public class DateFactory {
// 省略已有代码
// 定义调校类,用于调整一个 Date 对象中各字段的值
public static class Adjuster {
private int year, month, day, hour, minute, second;
// 构造器中传入基准 Date 对象
public Adjuster(Date baseDate) {
this.year = DateTools.yearOf(baseDate);
this.month = DateTools.monthOf(baseDate);
this.day = DateTools.dayOf(baseDate);
this.hour = DateTools.hourOf(baseDate);
this.minute = DateTools.minuteOf(baseDate);
this.second = DateTools.secondOf(baseDate);
} // Adjuster() (Class Constructor)
// 提供各时间字段的 setter。返回自身以便链式调用。
public Adjuster setYear(int year) {
this.year = year;
return this;
} // setYear()
public Adjuster setMonth(int month) {
this.month = month;
return this;
} // setMonth()
public Adjuster setDay(int day) {
this.day = day;
return this;
} // setDay()
public Adjuster setHour(int hour) {
this.hour = hour;
return this;
} // setHour()
public Adjuster setMinute(int minute) {
this.minute = minute;
return this;
} // setMinute()
public Adjuster setSecond(int second) {
this.second = second;
return this;
} // setSecond()
// 提交更改,返回一个新的 Date 对象
public Date commit() {
return DateFactory.create(year, month, day, hour, minute, second);
} // commit()
} // Adjuster Inner Class
} // DateFactory Class
假如需要生成一个 Date2 对象,将原先的 date 的时间修改为 12:34:56,则执行下面这条指令即可:
Date date2 = new DateFactory.Adjuster(date)
.setHour(12)
.setMinute(34)
.setSecond(56)
.commit(); // date2 = new DateFactory.Adjuster(date)...
其实你们可能已经看出来了,这其实就是一个 Builder 模式。Builder 模式可以一次提交多项属性的修改,如果我们用一般的工厂方法,每次只改一个字段,像下面这样的:
public static Date changeYear(Date baseDate, int year) {
return create(
year,
DateTools.monthOf(baseDate),
DateTools.dayOf(baseDate),
DateTools.hourOf(baseDate),
DateTools.minuteOf(baseDate),
DateTools.secondOf(baseDate)
); // create()
} // changeYear()
那么每改一个字段,都会新生成一个 Date 对象,除了最终结果外,中间生成的 Date 对象其实最终也都丢弃了,这就造成了资源和效率的浪费。使用 Builder 模式的话,我们可以把中间的临时状态缓存起来,到最后统一提交,既节省了资源,又提高了效率。
至此,我们的 DateTools 工具类和 DateFactory 工厂类就创建完成了。各位在 Java 中玩转 Date 是不是更轻松了呢?