使用Java构建一个简洁清晰的日期API

 更新时间:2025年10月29日 09:04:41   作者:sp42_frank  
这篇文章主要为大家详细介绍了如何使用Java构建一个简洁清晰的日期API,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

说起日期类型,相对其他原始数据类型(int/long/boolean/string)处理起来是比较棘手的,原因也是多方面的,首先日期是个笼统的概念,年月日可以说成是日期,年月日时分秒也可以说成日期,或者说单独的时分秒也是日期,——那你到底说着哪个日期呢,是不是?Java 传统日期类型java.util.Date就是这么干的,一个类型囊括上述所有的日期概念,也是造成日期 API 混乱的根源;其次日期的表达方式,也称之为“格式 Format”,可以2025-12-22 11:05,也可以2025-12-22 11-05甚至3 Jun 2025 11:05,种类繁多;最后日期还有时区的概念,比如2024-12-03T10:15:30+01:00[Europe/Paris]Tue, 3 Jun 2025 11:05:30 GMT等等。

关于Date类型

早期 Java 版本中就是基于java.util.Date类型一统天下,很多日期处理方法都是围绕它进行的(Java 1.1 之前)。后来版本重构中废弃了很多的这些方法,改由java.util.Calendarjava.text.DateFormat完成。Calendar 解决了日期的棘手问题了吗?并没有,虽然 Calendar 处理日期起来更灵活但是使用仍然过于复杂,设置、比较、格式化仍旧比较麻烦、冗长,所以社区又有了代替品 Joda-Time,被许多项目所采用。于是这个优秀的开源项目渐渐地成为 Java 标准的一部分,便是 JSR310,最后集成到 Java 8 中变成java.time

总之,我们主张使用的日期类型就是java.time里面的 Instant 及 LocalDateTime,其中 Instant 表示某一时刻的时间,大致对应时间戳,可支持纳秒的级别。LocalDateTime 可能更我们更常用一些,以及它衍生的其他精确类型。

新 Java 常用日期时间类型

类型所在包Java 版本说明是否推荐
Instantjava.time.Instant8+表示时间线上的一个瞬时点(UTC),精确到纳秒推荐用于时间戳
LocalDatejava.time.LocalDate8+只包含日期(年-月-日),如 2025-03-08强烈推荐用于“纯日期”场景(如列车出发日)
LocalTimejava.time.LocalTime8+只包含时间(时:分:秒.纳秒),如 14:30:00推荐用于“纯时间”场景(如发车时间)
LocalDateTimejava.time.LocalDateTime8+日期 + 时间(无时区),如 2025-03-08T14:30:00推荐用于本地时间(如列车时刻表)
ZonedDateTimejava.time.ZonedDateTime8+带时区的日期时间,支持夏令时等推荐用于跨时区系统
OffsetDateTimejava.time.OffsetDateTime8+带偏移量的时间(如 +08:00)适合存储数据库时间
Durationjava.time.Duration8+表示时间段(秒、纳秒),如 2 小时推荐用于计算时间差
Periodjava.time.Period8+表示时间段(年、月、日),如 1个月后推荐用于日历周期计算
DateTimeFormatterjava.time.format.DateTimeFormatter8+线程安全的格式化/解析工具替代 SimpleDateFormat

那么 Date 类型就不用,完全废弃了吗?——并不是,虽然围绕 Date 的相关 API 早在 Java 1.1 之后就被废弃,但是不代表 Date 本身被废弃。如果废弃了,为什么你在新版 JDK17 上找不到@Deprecated注解?说明该类型还是保留着的。作为值对象(Value Object)使用 Date 本身没什么问题,例如两种构造器的用法仍保留:new Date()以及new Date(long timestamp)

API 简介

这套笔者封装的 日期 API,主要功能有以下三点:

  • 日期类型的转换。面对如此繁多的日期类型:Date、Instant、Int/Long/String、LocalDate/LocalDateTime/LocalTime、ZonedDateTime 等等,给出一个相互之间都能够转换的方法,即输入T 返回的日期 = input(Object anyType).to(Class<T> 期望类型)
  • 通用的日期格式化。
  • 返回当前日期的now()函数及其他工具函数。

源码在:gitee.com/lightweight-components/aj-util/tree/main/aj-util/src/main/java/com/ajaxjs/util/date

此 API属于 aj-util 库的一部分,而 aj-util 库又是 aj-framework 框架的一部分,——敬请参阅了解。

万能日期类型转换

起初想到的转换方式是这样的,一个类型对应着一个类型逐个转换。

@SuppressWarnings("unchecked")
public <T> T to(Class<T> clz, ZoneId zoneId) {
    if (input != null) {
        if (clz == LocalDate.class) {
            LocalDate localDate = input.toInstant()
                    .atZone(zoneId == null ? ZoneId.systemDefault() : zoneId)
                    .toLocalDate();

            return (T) localDate;
        }
    }

    if (localDate != null) {
        if (clz == Date.class) {
            Date date = Date.from(localDate.atStartOfDay(zoneId == null ? ZoneId.systemDefault() : zoneId).toInstant());

            return (T) date;
        }
    }

    throw new UnsupportedOperationException("Can not transform this date type to another date type");
}

可是这样的方式太累了,代码也啰嗦。于是咨询了一下 AI 的意见,改为下面清爽的代码。

/**
 * Convert the input to the specified date type
 *
 * @param clz    The target date type
 * @param zoneId The time zone. Optional, defaults to system default if passed null
 * @param <T>    The target date type
 * @return The converted date
 */
@SuppressWarnings("unchecked")
public <T> T to(Class<T> clz, ZoneId zoneId) {
    ZoneId zone = zoneId != null ? zoneId : ZoneId.systemDefault();
    Instant baseInstant;

    if (input != null) {/* SB way, actually u can set any values to Instant by constructor or setter method */
        baseInstant = input.toInstant();
    } else if (sqlDate != null) {
        baseInstant = sqlDate.toLocalDate().atStartOfDay(zone).toInstant();
    } else if (sqlTimestamp != null) {
        baseInstant = sqlTimestamp.toInstant();
    } else if (localDate != null) {
        baseInstant = localDate.atStartOfDay(zone).toInstant();
    } else if (localDateTime != null) {
        baseInstant = localDateTime.atZone(zone).toInstant();
    } else if (zonedDateTime != null) {
        baseInstant = zonedDateTime.toInstant();
    } else if (offsetDateTime != null) {
        baseInstant = offsetDateTime.toInstant();
    } else if (offsetTime != null) {
        baseInstant = offsetTime.atDate(LocalDate.now()).toInstant();
    } else if (instant != null) {
        baseInstant = instant;
    } else if (timestamp != 0L) {
        baseInstant = Instant.ofEpochMilli(timestamp);
    } else
        throw new UnsupportedOperationException("No input date/time set.");

    // Convert baseInstant to target
    if (clz == Instant.class) {
        return (T) baseInstant;
    } else if (clz == Date.class) {
        return (T) Date.from(baseInstant);
    } else if (clz == java.sql.Date.class) {
        return (T) java.sql.Date.valueOf(baseInstant.atZone(zone).toLocalDate());
    } else if (clz == Timestamp.class) {
        return (T) Timestamp.from(baseInstant);
    } else if (clz == LocalDate.class) {
        return (T) baseInstant.atZone(zone).toLocalDate();
    } else if (clz == LocalTime.class) {
        return (T) baseInstant.atZone(zone).toLocalTime();
    } else if (clz == LocalDateTime.class) {
        return (T) baseInstant.atZone(zone).toLocalDateTime();
    } else if (clz == ZonedDateTime.class) {
        return (T) baseInstant.atZone(zone);
    } else if (clz == OffsetDateTime.class) {
        return (T) baseInstant.atOffset(zone.getRules().getOffset(baseInstant));
    } else if (clz == OffsetTime.class) {
        return (T) baseInstant.atZone(zone).toOffsetDateTime().toOffsetTime();
    } else if (clz == Calendar.class) {
        Calendar calendar = Calendar.getInstance();
        baseInstant.atZone(zone);
        calendar.setTimeInMillis(baseInstant.toEpochMilli());

        return (T) calendar;
    }

    throw new UnsupportedOperationException("Unsupported target type: " + clz.getName());
}

主要就是不管什么输入类型,先统一转换到Instant类再转为目标类型。

下面是一些用法例子:

long timestamp = System.currentTimeMillis();
Instant expected = Instant.ofEpochMilli(timestamp);

Instant result = new DateTypeConvert(timestamp).to(Instant.class, null);
assertEquals(expected, result);


Instant instant = Instant.now();
LocalDateTime expected = instant.atZone(zone).toLocalDateTime();

LocalDateTime result = new DateTypeConvert(instant).to(LocalDateTime.class, null);
assertEquals(expected, result);

LocalDate localDate = LocalDate.of(2025, 10, 23);
Date expected = Date.from(localDate.atStartOfDay(zone).toInstant());

Date result = new DateTypeConvert(localDate).to(Date.class, null);
assertEquals(expected, result);

日期格式化

新版推荐使用DateTimeFormatter替代SimpleDateFormat,使之线程安全与 API 清晰。同时DateTimeFormatter.ofPattern()实例有一定开销,尤其是自定义模式(如 "yyyy-MM-dd HH:mm:ss"),于是我们可以缓存 DateTimeFormatter 实例的方式来优化。

Formatter 用法如下:

new Formatter(TemporalAccessor temporal).format();
new Formatter(TemporalAccessor temporal).format(String format)

接口TemporalAccessor实现都是常见的那些日期类型,于是我们在构造器传入即可,然后指定日期格式的字符串。

工具函数

主要是now()返回当前时间字符串的工具函数。

/**
 * Obtain current time by specified format.
 *
 * @param formatter The formatter object
 * @return The current time
 */
public static String now(DateTimeFormatter formatter) {
    return LocalDateTime.now().format(formatter);
}

/**
 * Obtain current time by specified format.
 *
 * @param format The format string
 * @return The current time
 */
public static String now(String format) {
    return now(Formatter.getDateFormatter(format));
}

/**
 * Obtain current time, which is formatted by default like "yyyy-MM-dd HH:mm:ss".
 *
 * @return The current time
 */
public static String now() {
    return now(Formatter.getDateTimeFormatter());
}

/**
 * Obtain current time, which is formatted like "yyyy-MM-dd HH:mm".
 *
 * @return The current time
 */
public static String nowShort() {
    return now(Formatter.getDateTimeShortFormatter());
}

/**
 * 请求的时间戳,格式必须符合 RFC1123 的日期格式
 *
 * @return The current time
 */
public static String nowGMTDate() {
    return Formatter.GMT_FORMATTER.format(Instant.now());
}

/**
 * 请求的时间戳。按照 ISO8601 标准表示,并需要使用 UTC 时间,格式为 yyyy-MM-ddTHH:mm:ssZ
 *
 * @return The current time
 */
public static String newISO8601Date() {
    return Formatter.ISO8601_FORMATTER.format(Instant.now());
}

另外还包括一个字符串转日期的函数,其主要使用正则来匹配是否日期字符串然后进行转换。

小结

这套 API 原理并不是太复杂,主要是通过封装来厘清 Java 日期 API 的使用,务求更简单、清晰。

到此这篇关于使用Java构建一个简洁清晰的日期API的文章就介绍到这了,更多相关Java构建日期API内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java读取文本文件的各种方法

    Java读取文本文件的各种方法

    这篇文章主要介绍了Java读取文本文件的各种方法,在 Java 中有多种方法可以读取纯文本文件,例如你可以使用FileReader、BufferedReader或Scanner来读取文本文件,感兴趣的小伙伴和小编一起进入文章了解更多内容吧,希望能帮助到大家
    2021-11-11
  • 基数排序简介及Java语言实现

    基数排序简介及Java语言实现

    这篇文章主要介绍了基数排序简介及Java语言实现,涉及基数排序的基本思想简单介绍和桶排序的分析,以及基数排序的Java实现,具有一定借鉴价值,需要的朋友可以参考下。
    2017-11-11
  • SpringBoot集成支付宝沙箱支付的实现示例

    SpringBoot集成支付宝沙箱支付的实现示例

    本文主要介绍了SpringBoot集成支付宝沙箱支付的实现示例,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • java中构造方法及this关键字的用法实例详解(超详细)

    java中构造方法及this关键字的用法实例详解(超详细)

    大家都知道,java作为一门内容丰富的编程语言,其中涉及的范围是十分广阔的,下面这篇文章主要给大家介绍了关于java中构造方法及this关键字用法的相关资料,需要的朋友可以参考下
    2022-04-04
  • 详解Java线程池的使用(7种创建方法)

    详解Java线程池的使用(7种创建方法)

    这篇文章主要介绍了详解Java线程池的使用(7种创建方法),线程池的创建⽅式总共包含7种,其中6种是通过Executors创建的,1种是通过ThreadPoolExecutor创建的,今天我们就来详细说一下
    2023-03-03
  • 通过实例解析传统jar包引用方式

    通过实例解析传统jar包引用方式

    这篇文章主要介绍了通过实例解析传统jar包引用方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-07-07
  • Jvm sandbox mock机制的实践过程

    Jvm sandbox mock机制的实践过程

    这篇文章主要介绍了Jvm sandbox mock机制的实践过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-05-05
  • 支付宝APP支付(IOS手机端+java后台)版

    支付宝APP支付(IOS手机端+java后台)版

    这篇文章主要为大家详细介绍了支付宝APP支付(IOS手机端+java后台)版,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-05-05
  • SpringBoot绿叶显示yml和端口问题及解决方法

    SpringBoot绿叶显示yml和端口问题及解决方法

    今天是解决报错的一天,首先在操作Springboot中的时候,有些朋友的yml显示的不是绿叶的图标,或者是配置了之后不生效的问题,今天就给大家分享SpringBoot绿叶显示yml和端口问题,感兴趣的朋友一起看看吧
    2023-01-01
  • 使用Java编写一个图片word互转工具

    使用Java编写一个图片word互转工具

    这篇文章主要介绍了使用Java编写一个PDF Word文件转换工具的相关资料,需要的朋友可以参考下
    2023-01-01

最新评论