TimeUnit源码分享

很久没有写博客了,去年一整年都在忙一个重要的项目,现在回过头来有点时间,优化了一些业务流程、代码规范。在优化的时候就看到了很多"魔法数",项目里充斥着60 * 1000L、300000、120000等时间"魔法数",虽然不会看不懂,但是阅读起来始终有点障碍,还容易搞错单位。于是就统一使用TimeUnit改了一下,点进去之后发现是"c/c++风格"Java代码。回顾一下大学时代的C语言时也一起学习一下Doug Lea大佬的编程思维。

一、TimeUnit概览

TimeUnitjava.util.concurrent包中的枚举类,定义了7种时间单位:

Java 复制代码
public enum TimeUnit {
    NANOSECONDS(TimeUnit.NANO_SCALE),
    MICROSECONDS(TimeUnit.MICRO_SCALE),
    MILLISECONDS(TimeUnit.MILLI_SCALE),
    SECONDS(TimeUnit.SECOND_SCALE),
    MINUTES(TimeUnit.MINUTE_SCALE),
    HOURS(TimeUnit.HOUR_SCALE), 
    DAYS(TimeUnit.DAY_SCALE);
}

核心设计是基于纳秒的统一换算:

Java 复制代码
private static final long NANO_SCALE   = 1L;
private static final long MICRO_SCALE  = 1000L * NANO_SCALE;
private static final long MILLI_SCALE  = 1000L * MICRO_SCALE;
private static final long SECOND_SCALE = 1000L * MILLI_SCALE;
private static final long MINUTE_SCALE = 60L * SECOND_SCALE;
private static final long HOUR_SCALE   = 60L * MINUTE_SCALE;
private static final long DAY_SCALE    = 24L * HOUR_SCALE;

每个时间单位通过scale值定义相对于纳秒的倍数。这种设计将时间单位抽象为数学关系,确保所有转换均为整数运算,以纳秒为基准,粒度最细,覆盖所有单位;

二、构造方法

TimeUnit通过初始化时就把下面的字段计算好关键字段,优化性能

Java 复制代码
private final long scale;        // 基础转换倍数
private final long maxNanos;     // 纳秒转换溢出边界
private final long maxMicros;    // 微秒转换溢出边界
private final long maxMillis;    // 毫秒转换溢出边界
private final long maxSecs;      // 秒转换溢出边界
private final long microRatio;   // 微秒转换比
private final int milliRatio;    // 毫秒转换比
private final int secRatio;      // 秒转换比

private TimeUnit(long s) {
    this.scale = s;
    this.maxNanos = Long.MAX_VALUE / s;  // 防止溢出
    long ur = (s >= MICRO_SCALE) ? (s / MICRO_SCALE) : (MICRO_SCALE / s);
    this.microRatio = ur;
    this.maxMicros = Long.MAX_VALUE / ur;
    long mr = (s >= MILLI_SCALE) ? (s / MILLI_SCALE) : (MILLI_SCALE / s);
    this.milliRatio = (int)mr;
    this.maxMillis = Long.MAX_VALUE / mr;
    long sr = (s >= SECOND_SCALE) ? (s / SECOND_SCALE) : (SECOND_SCALE / s);
    this.secRatio = (int)sr;
    this.maxSecs = Long.MAX_VALUE / sr;
}

通过初始化计算转换比和边界值,利用极少量的内存换取O(1)时间复杂度的转换。

三、放大与缩小的艺术

toSeconds方法

TimeUnit的核心转换方法(如toSecondstoMillis)通过比较scale与目标单位scale的大小,优雅地决定是"放大"还是"缩小"。以下以toSeconds为例:

Java 复制代码
public long toSeconds(long duration) {
    long s, m;
    if ((s = scale) <= SECOND_SCALE)
        return (s == SECOND_SCALE) ? duration : duration / secRatio;
    else if (duration > (m = maxSecs))
        return Long.MAX_VALUE;
    else if (duration < -m)
        return Long.MIN_VALUE;
    else
        return duration * secRatio;
}

每个类型的TimeUnitscale表示其相对于纳秒的倍数,SECOND_SCALE1_000_000_000(1秒的纳秒数)。通过比较scaleSECOND_SCALE,即可判断转换方向:

  • scale < SECOND_SCALE (如MILLISECONDS):当前单位比秒小,输入duration更精细,需缩小(除法) ,用secRatio(如1000)除以输入值。
  • scale == SECOND_SCALE (如SECONDS):单位相同,直接返回duration
  • scale > SECOND_SCALE (如HOURS):当前单位比秒大,输入duration更粗糙,需放大(乘法) ,用secRatio(如3600)乘以输入值。
转换方向 数学关系 算法选择 数学原理
细→粗 scale < SECOND_SCALE 除法 粗单位包含多个细单位,需整除
同级 scale == SECOND_SCALE 直接返回 无需转换
粗→细 scale > SECOND_SCALE 乘法 粗单位数值需倍增为细单位
  1. TimeUnit.MILLISECONDS.toSeconds(1)
  • scale = MILLI_SCALE = 1_000_000
  • SECOND_SCALE = 1_000_000_000
  • 判断:scale < SECOND_SCALE → 缩小
  • 计算:secRatio = SECOND_SCALE / MILLI_SCALE = 1000
  • 结果:1 / 1000 = 0(1毫秒不足1秒)
  1. TimeUnit.SECONDS.toSeconds(1)
  • scale = SECOND_SCALE = 1_000_000_000
  • 判断:scale == SECOND_SCALE → 相等
  • 结果:直接返回1
  1. TimeUnit.HOURS.toSeconds(1)
  • scale = HOUR_SCALE = 3_600 * 1_000_000_000
  • 判断:scale > SECOND_SCALE → 放大
  • 计算:secRatio = HOUR_SCALE / SECOND_SCALE = 3600
  • 结果:1 * 3600 = 3600(1小时 = 3600秒)

cvt方法

Java 复制代码
private static long cvt(long d, long dst, long src) {
    long r, m;
    if (src == dst)
        return d;
    else if (src < dst)
        return d / (dst / src);
    else if (d > (m = Long.MAX_VALUE / (r = src / dst)))
        return Long.MAX_VALUE;
    else if (d < -m)
        return Long.MIN_VALUE;
    else
        return d * r;
}
  1. 毫秒到秒(缩小):cvt(1000L, SECOND_SCALE, MILLI_SCALE)
  • src = MILLI_SCALE = 1_000_000
  • dst = SECOND_SCALE = 1_000_000_000
  • 判断:src < dst → 缩小
  • 计算:dst / src = 1_000_000_000 / 1_000_000 = 1000
  • 结果:1000 / 1000 = 1(1000毫秒 = 1秒)
  1. 秒到秒(同单位):cvt(1L, SECOND_SCALE, SECOND_SCALE)
  • src = SECOND_SCALE = 1_000_000_000
  • dst = SECOND_SCALE = 1_000_000_000
  • 判断:src == dst → 相等
  • 结果:直接返回1(1秒 = 1秒)
  1. 分钟到秒(放大):cvt(2L, SECOND_SCALE, MINUTE_SCALE)
  • src = MINUTE_SCALE = 60_000_000_000
  • dst = SECOND_SCALE = 1_000_000_000
  • 判断:src > dst → 放大
  • 计算:r = src / dst = 60_000_000_000 / 1_000_000_000 = 60
  • 结果:2 * 60 = 120(2分钟 = 120秒)

无论是toSeconds方法还是cvt方法,都是把TimeUnit将复杂的时间单位转换抽象为数学运算,再通过简单的scale比较去判断进行放大还是缩小操作,逻辑优雅且高效。

总结

Doug Lea大佬把TimeUnit写的非常精炼且明了,读下来也受益匪浅,业务赶多了,还是需要静下心来学习一下大佬的想法和写法,不只是把代码实现了,还能写的干净利落。

相关推荐
yaoxin52112311 小时前
279. Java Stream API - Stream 拼接的两种方式:concat() vs flatMap()
java·开发语言
坚持学习前端日记12 小时前
2025年的个人和学习年度总结以及未来期望
java·学习·程序人生·职场和发展·创业创新
Cosmoshhhyyy12 小时前
《Effective Java》解读第29条:优先考虑泛型
java·开发语言
Chen不旧12 小时前
java基于reentrantlock/condition/queue实现阻塞队列
java·开发语言·signal·reentrantlock·await·condition
寒水馨12 小时前
com.github.oshi : oshi-core 中文文档(中英对照·API·接口·操作手册·全版本)以6.4.0为例,含Maven依赖、jar包、源码
java·后端
0和1的舞者12 小时前
SpringBoot日志框架全解析
java·学习·springboot·日志·打印·lombok
小毅&Nora12 小时前
【Java线程安全实战】② ConcurrentHashMap 源码深度拆解:如何做到高性能并发?
java·安全·多线程
Knight_AL13 小时前
阿里《Java 开发手册》下的对象构建与赋值规范实践
java·开发语言
步步为营DotNet13 小时前
深入理解.NET 中的IHostedService:后台任务管理的基石
java·网络·.net
独自破碎E13 小时前
Leetcode862和至少为K的最短子数组
java·开发语言