欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java 中设计日期工具类 DateTools 和日期工厂类 DateFactory 完善饱受诟病的原生 Date 类

程序员文章站 2022-05-23 19:31:36
...

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 是不是更轻松了呢?