Java-日期时间

Java 日期时间相关笔记


时区

UTC世界协调时

UTC(Universal Coordinated Time) 协调世界时,又称世界统一时间、世界标准时间、国际协调时间。

GMT格林威治标准时(GMT=UTC)

GMT(Greenwich Mean Time) 格林尼治标准时间,是指位于英国伦敦郊区的皇家格林尼治天文台的标准时间,一般指世界时。

CST中国标准时(UTC/GMT+8)

CST(China Standard Time) 中国标准时间。 在时区划分上,属东八区,比协调世界时早 8 小时,记为 UTC+8,与中华民国国家标准时间(旧称“中原标准时间”)、香港时间和澳门时间相同。 当格林威治时间为凌晨 0:00 时,中国标准时间刚好为上午 8:00。

时间转换: CST=UTC/GMT + 8 小时


日期时间格式

日期时间格式表

Symbol Meaning Presentation Example
G era designator 纪元 Text AD
y year 年 Number 2009
M month in year 年中的月份 Text & Number July & 07
d day in month 月份中的天数 Number 10
h hour in am/pm (1-12) am/pm 中的小时数(1-12) Number 12
H hour in day (0-23) 一天中的小时数(0-23) Number 0
m minute in hour 小时中的分钟数 Number 30
s second in minute 分钟中的秒数 Number 55
S millisecond 毫秒数 Number 978
E day in week 星期中的天数 Text Tuesday
D day in year 年中的天数 Number 189
F day of week in month 月份中的星期 Number 2 (2nd Wed in July)
w week in year 年中的周数 Number 27
W week in month 月份中的周数 Number 2
a am/pm marker Am/pm 标记 Text PM
k hour in day (1-24) 一天中的小时数(1-24) Number 24
K hour in am/pm (0-11) am/pm 中的小时数(0-11) Number 0
z time zone 时区 Text Pacific Standard Time
Z time zone 时区 Text Pacific Standard Time
' escape for text 文本转义符 Delimiter (none)
' single quote 单引号 Literal '

常用日期格式如下

Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2018-12-25 19:01:12");

h大小写和m大小写

注意: 常用的日期格式 yyyy-MM-dd HH:mm:ss 中,只有 M 和 H 是大写的 1、h 大小写是为了区分12小时制和24小时制,小写的h是12小时制,大写的H是24小时制。 2、小写 m 是分钟,大写 M 是月份。

Java日期中的T和Z

有时候看到世界带 T Z (如:2018-01-31T14:32:19Z) 这是 UTC 统一时间,T 代表后面跟着是时间,Z代表0时区(相差北京时间8小时),转换为北京时间(CST)需要+8小时 构造时间格式时 T 和 Z 要加单引号。

@Test
public void testUTC() throws Exception {
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
    simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
    String utcTime = "2020-10-28T03:02:19Z";
    System.out.println(simpleDateFormat.parse(utcTime));
}

结果 Wed Oct 28 11:02:19 CST 2020

SSSZ(SSS毫秒Z时区)

yyyy-MM-dd’T’HH:mm:ss.SSSZ 后面的三个SSS指的是毫秒,Z代表的时区,中间的T代表可替换的任意字符。


Java 时区设置

TimeZone.setDefault()

System.out.println(TimeZone.getDefault());
final TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
TimeZone.setDefault(timeZone);

-Duser.timezone=Asia/Shanghai

java 启动脚本中增加 jvm 参数 java -Duser.timezone=Asia/Shanghai java -Duser.timezone=GMT+08

TZ环境变量

export TZ=Asia/Shanghai


Java8 新日期和时间 API

Java 8 的日期和时间类包含 LocalDate, LocalTime, LocalDateTime, Instant, Duration 以及 Period,这些类都包含在 java.time 包中。

Java 8新特性(四):新的时间和日期API https://lw900925.github.io/java/java8-newtime-api.html

为什么要引入新的日期API?

Java 的 java.util.Date 和 java.util.Calendar 类易用性差,不支持时区,而且他们都不是线程安全的;

用于格式化日期的类 DateFormat 被放在 java.text 包中,它是一个抽象类,所以我们需要实例化一个 SimpleDateFormat 对象来处理日期格式化,并且 DateFormat 也是非线程安全,这意味着如果你在多线程程序中调用同一个 DateFormat 对象,会得到意想不到的结果。

对日期的计算方式繁琐,而且容易出错,因为月份是从0开始的,从 Calendar 中获取的月份需要加一才能表示当前月份。

由于以上这些问题,出现了一些三方的日期处理框架,例如 Joda-Time,date4j 等开源项目。


LocalDate

LocalDate 类表示一个具体的日期,但不包含具体时间,也不包含时区信息。可以通过 LocalDate 的静态方法 of() 创建一个实例,LocalDate 也包含一些方法用来获取年份,月份,天,星期几等:

LocalDate localDate = LocalDate.of(2017, 1, 4);     // 初始化一个日期:2017-01-04
int year = localDate.getYear();                     // 年份:2017
Month month = localDate.getMonth();                 // 月份:JANUARY
int dayOfMonth = localDate.getDayOfMonth();         // 月份中的第几天:4
DayOfWeek dayOfWeek = localDate.getDayOfWeek();     // 一周的第几天:WEDNESDAY
int length = localDate.lengthOfMonth();             // 月份的天数:31
boolean leapYear = localDate.isLeapYear();          // 是否为闰年:false
LocalDateTime statDateStart = LocalDate.now().atStartOfDay(); // 当天0点

也可以调用静态方法 now() 来获取当前日期:

LocalDate now = LocalDate.now();

LocalTime

LocalTime 包含具体时间

LocalTime localTime = LocalTime.of(17, 23, 52);     // 初始化一个时间:17:23:52
int hour = localTime.getHour();                     // 时:17
int minute = localTime.getMinute();                 // 分:23
int second = localTime.getSecond();                 // 秒:52

LocalTime 转字符串 HH:mm:ss 格式

Hutool 的 DatePattern 提供了预定义的格式常量,避免手动编写格式字符串。 DatePattern.NORM_TIME_PATTERNHH:mm:ss

String timeStr = localTime.format(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN));

LocalDateTime

LocalDateTime 类是 LocalDate 和 LocalTime 的结合体,可以通过 of() 方法直接创建,也可以调用 LocalDate 的 atTime() 方法或 LocalTime 的 atDate() 方法将 LocalDate 或 LocalTime 合并成一个 LocalDateTime:

LocalDateTime ldt1 = LocalDateTime.of(2017, Month.JANUARY, 4, 17, 23, 52);
LocalDate localDate = LocalDate.of(2017, Month.JANUARY, 4);
LocalTime localTime = LocalTime.of(17, 23, 52);
LocalDateTime ldt2 = localDate.atTime(localTime);

until() 计算时间差

public long until(Temporal endExclusive, TemporalUnit unit) 计算 endExclusive 减去 this 的时间差,返回时间单位为 unit。 如果 endExclusive 小于 this,返回为负数。

LocalDateTime todayStart = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);//当天零点
LocalDateTime todayEnd = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);//当天结束时间
System.out.println(todayStart.until(todayEnd, ChronoUnit.HOURS)); // 结果 23

Instant 时间戳

Instant 用于表示一个时间戳,它与我们常使用的 System.currentTimeMillis() 有些类似,不过 Instant 可以精确到纳秒(Nano-Second),System.currentTimeMillis() 方法只精确到毫秒(Milli-Second)。 Instant 除了使用 now() 方法创建外,还可以通过 ofEpochSecond 方法创建:

Instant instant = Instant.ofEpochSecond(120, 100000);

ofEpochSecond() 方法的第一个参数为秒,第二个参数为纳秒,上面的代码表示从1970-01-01 00:00:00开始后两分钟的10万纳秒的时刻


LocalDateTime 和 Timestamp 互相转换

// LocalDateTime 转 Timestamp
Timestamp timestamp = Timestamp.valueOf(LocalDateTime.now());
// Timestamp 转 LocalDateTime
LocalDateTime localDateTime = timestamp.toLocalDateTime();

LocalDateTime 和 Date 互相转换

LocalDateTime 和 Date 的互相转换需要通过 Timestamp 或 Instant 完成。

// LocalDateTime 和 Date 互转
@Test
public void testConvert2Date() throws Exception {
    // LocalDateTime 转 Date
    LocalDateTime localDateTime = LocalDateTime.now();
    Date date1 = new Date(Timestamp.valueOf(localDateTime).getTime()); // 通过 Timestamp
    Date date2 = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); // 通过 Instant
    System.out.println("Date: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date1));
    System.out.println("Date: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date2));
    Thread.sleep(1000L);

    // Date 转 LocalDateTime
    Date date = new Date();
    LocalDateTime localDateTime1 = new Timestamp(date.getTime()).toLocalDateTime(); // 通过 Timestamp
    LocalDateTime localDateTime2 = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); // 通过 Instant
    LocalDateTime localDateTime3 = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); // 通过 Instant
    System.out.println("LocalDateTime: " + localDateTime1.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    System.out.println("LocalDateTime: " + localDateTime2.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    System.out.println("LocalDateTime: " + localDateTime3.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}

LocalDateTime 和时间戳秒/毫秒互转

几种方法中,通过 java.sql.Timestamp 转换是最方便的

// LocalDateTime 和时间戳互转
@Test
public void testConvert2Timestamp() {
    LocalDateTime localDateTime = LocalDateTime.now();
    // LocalDateTime 转 时间戳毫秒
    System.out.println("时间戳毫秒: " + Timestamp.valueOf(localDateTime).getTime());
    System.out.println("时间戳毫秒: " + localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    System.out.println("时间戳毫秒: " + localDateTime.toInstant(ZoneOffset.of("+8")).toEpochMilli());

    // LocalDateTime 转 时间戳秒
    System.out.println("时间戳秒: " + Timestamp.valueOf(localDateTime).getTime() / 1000);
    System.out.println("时间戳秒: " + localDateTime.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond());
    System.out.println("时间戳秒: " + localDateTime.toEpochSecond(ZoneOffset.of("+8")));

    // 时间戳毫秒 转 LocalDateTime
    long tsMillis = System.currentTimeMillis();
    LocalDateTime localDateTime2 = new Timestamp(tsMillis).toLocalDateTime();
    System.out.println("LocalDateTime: " + localDateTime2.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}

Duration 时间段(秒)

Duration 的内部实现与 Instant 类似,也是包含两部分:seconds 表示秒,nanos 表示纳秒。两者的区别是 Instant 用于表示一个时间戳(或者说是一个时间点),而 Duration 表示一个时间段,所以 Duration 类中不包含 now() 静态方法。

Duration 可方便的转为其他时间单位

long days = duration.toDays();              // 这段时间的总天数
long hours = duration.toHours();            // 这段时间的小时数
long minutes = duration.toMinutes();        // 这段时间的分钟数
long seconds = duration.getSeconds();       // 这段时间的秒数
long milliSeconds = duration.toMillis();    // 这段时间的毫秒数
long nanoSeconds = duration.toNanos();      // 这段时间的纳秒数

Duration 构造

可以通过 Duration.between() 方法创建 Duration 对象,表示两个时间之间时间段:

LocalDateTime from = LocalDateTime.of(2017, Month.JANUARY, 5, 10, 7, 0);    // 2017-01-05 10:07:00
LocalDateTime to = LocalDateTime.of(2017, Month.FEBRUARY, 5, 10, 7, 0);     // 2017-02-05 10:07:00
Duration duration = Duration.between(from, to);     // 表示从 2017-01-05 10:07:00 到 2017-02-05 10:07:00 这段时间

Duration 对象还可以通过 of() 方法创建,该方法接受一个时间段长度,和一个时间单位作为参数:

Duration duration1 = Duration.of(5, ChronoUnit.DAYS);       // 5天
Duration duration2 = Duration.of(1000, ChronoUnit.MILLIS);  // 1000毫秒

DurationUtils

org.apache.commons.lang3.time.DurationUtils

TimeUnit 时间单位转 ChronoUnit 时间单位
static ChronoUnit toChronoUnit(final TimeUnit timeUnit);
toDuration() 通过 TimeUnit 时间单位构造 Duration
public static Duration toDuration(final long amount, final TimeUnit timeUnit);

Duration 操作

Duration 可以进行一系列操作

@Test
public void testDuration() {
    Duration fiveMinutes = Duration.of(5, ChronoUnit.MINUTES);
    Duration oneMinutes = fiveMinutes.dividedBy(5); // 5分钟除以5,得1分钟
    Assertions.assertTrue(Duration.of(1, ChronoUnit.MINUTES).equals(oneMinutes));
    Assertions.assertTrue(oneMinutes.compareTo(fiveMinutes) < 0); // 比较,1分钟 - 5分钟 得负数表示小于

    LocalDateTime now = LocalDateTime.now();
    LocalDateTime fiveMinutesBefore = now.minus(fiveMinutes); // 当前时间减去5分钟的Duration
    Assertions.assertTrue(fiveMinutesBefore.isBefore(now));
}

Duration 格式化

// Duration 格式化
@Test
public void testDurationFormat() {
    Duration fiveMinutes = Duration.of(5, ChronoUnit.MINUTES);
    System.out.println(DurationFormatUtils.formatDuration(fiveMinutes.toMillis(), "d:H:mm:ss", false)); // 0:0:5:0
    System.out.println(DurationFormatUtils.formatDuration(fiveMinutes.toMillis(), "d:H:mm:ss", true)); // 0:0:05:00
}

Duration 格式化为中文时间段描述

@Test
public void testDurationFormatChinese() {
    Duration duration = Duration.of(31501245, ChronoUnit.SECONDS);
    long seconds = duration.getSeconds();
    long days = seconds / 86400;
    seconds = seconds % 86400;
    long hours = seconds / 3600;
    long minutes = (seconds % 3600) / 60;
    seconds = (seconds % 3600) % 60;
    String format = String.format("%d分%d秒", minutes, seconds);
    if (hours > 0) {
        format = String.format("%d小时%s", hours, format);
    }
    if (days > 0) {
        format = String.format("%d天%s", days, format);
    }
    System.out.println(format);
}

结果: 364天14小时20分45秒

Unit must not have an estimated duration

Duration d1 = Duration.of(3, ChronoUnit.YEARS); 抛异常 java.time.temporal.UnsupportedTemporalTypeException: Unit must not have an estimated duration 原因是 Duration 是基于时间(时分秒)的时间段,年/月范围太大不精确,无法直接使用 年/月 构造 Duration 对象 Period 才是基于日期(年月日)的视觉段


Period 时间段(天)

Period 在概念上和 Duration 类似,区别在于 Period 是以年月日来衡量一个时间段,比如2年3个月6天:

Period period = Period.of(2, 3, 6);

Period对象也可以通过between()方法创建,值得注意的是,由于Period是以年月日衡量时间段,所以between()方法只能接收LocalDate类型的参数:

// 2017-01-05 到 2017-02-05 这段时间
Period period = Period.between(
                LocalDate.of(2017, 1, 5),
                LocalDate.of(2017, 2, 5));

ZoneId 时区

新的时区类 java.time.ZoneId 是原有的 java.util.TimeZone 类的替代品。ZoneId 对象可以通过 ZoneId.of() 方法创建,也可以通过 ZoneId.systemDefault() 获取系统默认时区:

ZoneId shanghaiZoneId = ZoneId.of("Asia/Shanghai");
ZoneId systemZoneId = ZoneId.systemDefault();

of() 方法接收一个“区域/城市”的字符串作为参数,你可以通过 getAvailableZoneIds() 方法获取所有合法的“区域/城市”字符串:

Set<String> zoneIds = ZoneId.getAvailableZoneIds();

有了ZoneId,我们就可以将一个LocalDate、LocalTime或LocalDateTime对象转化为ZonedDateTime对象:

LocalDateTime localDateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, shanghaiZoneId);

日期操作

LocalDate date = LocalDate.of(2017, 1, 5);          // 2017-01-05

LocalDate date1 = date.withYear(2016);              // 修改为 2016-01-05
LocalDate date2 = date.withMonth(2);                // 修改为 2017-02-05
LocalDate date3 = date.withDayOfMonth(1);           // 修改为 2017-01-01

LocalDate date4 = date.plusYears(1);                // 增加一年 2018-01-05
LocalDate date5 = date.minusMonths(2);              // 减少两个月 2016-11-05
LocalDate date6 = date.plus(5, ChronoUnit.DAYS);    // 增加5天 2017-01-10

使用 TemporalAdjuster 调整时间

可以使用 with() 方法的另一个重载方法,它接收一个 TemporalAdjuster 参数,可以使我们更加灵活的调整日期:

LocalDate date7 = date.with(nextOrSame(DayOfWeek.SUNDAY));      // 返回下一个距离当前时间最近的星期日
LocalDate date9 = date.with(lastInMonth(DayOfWeek.SATURDAY));   // 返回本月最后一个星期六

1小时前按小时取整

LocalDateTime timeStart = LocalDateTime.now().minusHours(1).withMinute(0).withSecond(0).withNano(0); // 前 1 小时向下取整
LocalDateTime timeEnd = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0); // 当前小时向下取整

5分钟前按分取整

LocalDateTime timeStart = LocalDateTime.now().minusMinutes(5).withSecond(0).withNano(0);
LocalDateTime timeEnd = LocalDateTime.now().withSecond(0).withNano(0);

四舍五入对齐到上/下一个5分钟整

如果当前时间 dt 就是 5 分钟整,则结果就是自己

@Test
public void testAligned5Min() {
    LocalDateTime dt = LocalDateTime.now();
    LocalDateTime nextAlignedDt = dt.withSecond(0).withNano(0).plusMinutes((65 - dt.getMinute()) % 5);
    LocalDateTime preAlignedDt = dt.withSecond(0).withNano(0).minusMinutes((dt.getMinute()) % 5);
    System.out.println("当前时间:" + dt);
    System.out.println("下一个5分钟整:" + nextAlignedDt);
    System.out.println("上一个5分钟整:" + preAlignedDt);
}

遍历时间间隔内的全部5分钟整

遍历闭区间 [dt1, dt2] 之间的全部 5分钟整 时间

LocalDateTime dt1 = LocalDateTime.of(2022, 7, 10, 11, 68, 46);
LocalDateTime dt2 = LocalDateTime.of(2022, 7, 11, 17, 68, 46);
// 下一个整 5 分钟对应的时间点
LocalDateTime nextAlignedDateTime = dt1.withSecond(0).withNano(0).plusMinutes((65 - startTimeFrom.getMinute()) % 5);
while (nextAlignedDateTime.isBefore(dt2) || nextAlignedDateTime.equals(dt2)) {
    System.out.println(nextAlignedDateTime);
    nextAlignedDateTime = nextAlignedDateTime.plusMinutes(5);
}

获取昨天/今天起始时间和结束时间

Java 8 LocalDateTime 获取当天和昨天的起始时间和结束时间 注意 with(LocalTime.MAX) 产生的是当天 2022-06-23 23:59:59.9999 如果直接存到不带毫秒精度的 MySQL datetime 字段上,会显示为第二天零点 2022-06-24 00:00:00,如果想获得准确的 2022-06-23 23:59:59.0000 可以先获取第二天0点,然后再减一秒

LocalDateTime yesterdayStart = LocalDateTime.now().plusDays(-1).with(LocalTime.MIN); // 昨天零点 2022-06-23 00:00:00.0000
LocalDateTime yesterdayEnd = LocalDateTime.now().plusDays(-1).with(LocalTime.MAX); // 昨天结束时间 2022-06-23 23:59:59.9999
LocalDateTime yesterdayEnd2 = LocalDateTime.now().with(LocalTime.MIN).minusSeconds(1); // 昨天23点59分59秒 2022-06-23 23:59:59.0000

LocalDateTime todayStart = LocalDateTime.of(LocalDate.now(), LocalTime.MIN); //当天零点
LocalDateTime todayStart2 = LocalDateTime.now().with(LocalTime.MIN); //当天零点

LocalDateTime todayEnd = LocalDateTime.of(LocalDate.now(), LocalTime.MAX); //当天结束时间 2022-06-23 23:59:59.9999
LocalDateTime todayEnd2 = LocalDateTime.now().with(LocalTime.MAX); //当天结束时间 2022-06-23 23:59:59.9999
System.out.println(yesterdayStart);
System.out.println(yesterdayEnd);
System.out.println(todayStart);
System.out.println(todayStart2);
System.out.println(todayEnd);
System.out.println(todayEnd2);

抹去秒

@Test
public void testCeilFloor() {
    LocalDateTime now = LocalDateTime.now();
    System.out.println("当前时间: " + now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
    // 5分钟前向下取整(抹去秒)
    System.out.println("5分钟前抹去秒: " + now.minusMinutes(5).withSecond(0).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
    // 当前时间按分钟向下取整(抹去秒)
    System.out.println("当前时间抹去秒: " + now.withSecond(0).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}

DateTimeFormatter 日期时间格式化

新的日期 API 中提供了一个 DateTimeFormatter 类用于处理日期格式化操作,它被包含在 java.time.format 包中,Java 8 的日期类有一个 format() 方法用于将日期格式化为字符串,该方法接收一个 DateTimeFormatter 类型参数:

// 格式化
@Test
public void testFormatter() {
    LocalDateTime dateTime = LocalDateTime.now();
    System.out.println("时间戳秒: " + dateTime.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond());
    System.out.println("时间戳毫秒: " + dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    System.out.println(dateTime.format(DateTimeFormatter.BASIC_ISO_DATE));
    System.out.println(dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE));
    System.out.println(dateTime.format(DateTimeFormatter.ISO_LOCAL_TIME));
    System.out.println(dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
    System.out.println(dateTime.format(DateTimeFormatter.ofPattern("今天是:yyyy年 MM月 dd日 E", Locale.CHINESE)));
}

结果

时间戳秒: 1587357985
时间戳秒: 1587357985393
20200420
2020-04-20
12:46:25.393
2020-04-20
今天是:2020年 04月 20日 星期一

同样,日期类也支持将一个字符串解析成一个日期对象,例如:

String strDate6 = "2017-01-05";
String strDate7 = "2017-01-05 12:30:05";

LocalDate date = LocalDate.parse(strDate6, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
LocalDateTime dateTime1 = LocalDateTime.parse(strDate7, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

Date

java.util.Date public class Date extends Object implements Serializable, Cloneable, Comparable<Date> 类 Date 表示特定的瞬间,精确到毫秒。从 JDK 1.1 开始,应该使用 Calendar 类实现日期和时间字段之间转换,使用 DateFormat 类来格式化和分析日期字符串。Date 中的把日期解释为年、月、日、小时、分钟和秒值的方法已废弃。

Date() 当前时间

public Date() 构造方法,分配 Date 对象并初始化此对象,以表示分配它的时间(精确到毫秒)。 例如:Date date = new Date(); //以当前的日期和时间作为其初始值

Date(long) 毫秒时间戳指定的时间

public Date(long date) 分配 Date 对象并初始化此对象,以表示自从标准基准时间(称为“历元(epoch)”,即 1970 年 1 月 1 日 00:00:00 GMT)以来的指定毫秒数。 参数:date - 自 1970 年 1 月 1 日 00:00:00 GMT 以来的毫秒数。


获取当前日期+23:59:59对应的时间

sdf的日志格式字符串中直接写入固定时间即可

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd 23:59:59");
String s = sdf.format(new Date());
Date date =  sdf.parse(s);

Date去掉时分秒

方法一,用 SimpleDateFormat

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String s = sdf.format(new Date());
Date date =  sdf.parse(s);

或者

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd 00:00:00");
String s = sdf.format(new Date());
Date date =  sdf.parse(s);

方法二,用 Calendar

Calendar calender = Calendar.getInstance();
calender.setTime(new Date());
// 将时分秒,毫秒域清零
calender.set(Calendar.HOUR_OF_DAY, 0);
calender.set(Calendar.MINUTE, 0);
calender.set(Calendar.SECOND, 0);
calender.set(Calendar.MILLISECOND, 0);
System.out.printf("%1$tF %1$tT\n", calender.getTime());// calender.getTime()返回的Date已经是更新后的对象

java 8:只取年月日的java.util.Date(时分秒清零)对象 https://blog.csdn.net/10km/article/details/53906993


获取当前毫秒时间戳

获取当前时间戳,单位毫秒:

//方法 一
System.currentTimeMillis();
//方法 二
Calendar.getInstance().getTimeInMillis();
//方法 三
new Date().getTime();

其中 Calendar.getInstance().getTimeInMillis() 这种方式速度最慢,这是因为Canlendar要处理时区问题会耗费较多的时间。

注意System.currentTimeMillis()潜在的性能问题

其实 System.currentTimeMillis() 在调用频次高时也会有性能问题, public static native long currentTimeMillis(); 是个 native 方法,调用了 hotspot/src/os/linux/vm/os_linux.cpp 中的 javaTimeMillis() 方法,此方法中 调用gettimeofday()需要从用户态切换到内核态; 1、gettimeofday()的表现受Linux系统的计时器(时钟源)影响,在HPET计时器下性能尤其差;HPET计时器性能较差的原因是会将所有对时间戳的请求串行执行 2、系统只有一个全局时钟源,高并发或频繁访问会造成严重的争用。 3、基于以上几点,所以 System.currentTimeMillis() 在调用频次高时也会有性能问题

解决方法:用一个守护线程专门来获取时间戳并存入内存缓存中

public class CurrentTimeMillisClock {
    private volatile long now;

    private CurrentTimeMillisClock() {
        this.now = System.currentTimeMillis();
        scheduleTick();
    }

    private void scheduleTick() {
        new ScheduledThreadPoolExecutor(1, runnable -> {
            Thread thread = new Thread(runnable, "current-time-millis");
            thread.setDaemon(true);
            return thread;
        }).scheduleAtFixedRate(() -> {
            now = System.currentTimeMillis();
        }, 1, 1, TimeUnit.MILLISECONDS);
    }

    public long now() {
        return now;
    }

    public static CurrentTimeMillisClock getInstance() {
        return SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder {
        private static final CurrentTimeMillisClock INSTANCE = new CurrentTimeMillisClock();
    }
}

使用的时候,直接CurrentTimeMillisClock.getInstance().now()就可以了。不过,在System.currentTimeMillis()的效率没有影响程序整体的效率时,就不必忙着做优化

注意System.currentTimeMillis()潜在的性能问题 https://www.jianshu.com/p/d2039190b1cb


Unix时间戳和Date互相转换

http://www.cnblogs.com/killbug/archive/2013/04/08/3008764.html 时间戳转Date

request.setStartTime(new Date(1530720000 * 1000L));
request.setEndTime(new Date(1531238400L * 1000L));

Date转时间戳

params.put("start_time", (request.getStartTime().getTime() / 1000) + "");
params.put("end_time", (request.getEndTime().getTime() / 1000) + "");

Timestamp 和 Date 互相转换

java.sql.Timestampjava.util.Date 的子类,所以 Timestamp 本身就是 Date

public class Timestamp extends java.util.Date {
    ...
}

Timestamp 可直接赋值给 Date Date date = new Timestamp(System.currentTimeMillis())

Date 通过毫秒时间戳转换为 Timestamp Timestamp ts = new Timestamp(new Date().getTime())


Date 与 Timestamp compareTo()/before()/after() 比较问题

问题1、相同的 Date 和 Timestamp 用 Date.compareTo() 比较但不相等

Date date = new Date();
Timestamp timestampEqual = new Timestamp(date.getTime());
Assertions.assertEquals(0, timestampEqual.compareTo(date));  // 相等
Assertions.assertNotEquals(0, date.compareTo(timestampEqual));  // 不相等
Assertions.assertEquals(date.getTime(), timestampEqual.getTime());  // 相等

原因是如果用 Date.compareTo() 比较,都会转换为 Date 类型,使用 Date 内的 compareTo() 和 getMillisOf() 方法比较,直接取了 Timestamp.fastTime 字段,但 Timestamp 里的 fastTime 字段只存储秒,Timestamp 中使用单独的 nanos 字段存储毫秒,从而 getMillisOf() 只获取了 Timestamp 的秒,丢失了毫秒,导致比较结果不一致。

public int compareTo(Date anotherDate) {
    long thisTime = getMillisOf(this);
    long anotherTime = getMillisOf(anotherDate);
    return (thisTime<anotherTime ? -1 : (thisTime==anotherTime ? 0 : 1));
}

static final long getMillisOf(Date date) {
    if (date.cdate == null || date.cdate.isNormalized()) {
        return date.fastTime;
    }
    BaseCalendar.Date d = (BaseCalendar.Date) date.cdate.clone();
    return gcal.getTime(d);
}

如果使用 Timestamp.compareTo() 比较是没问题的,内部考虑到了 Timestamp 和 Date 比较的情况,都是取的毫秒比较:

public int compareTo(java.util.Date o) {
    if(o instanceof Timestamp) {
        // When Timestamp instance compare it with a Timestamp
        // Hence it is basically calling this.compareTo((Timestamp))o);
        // Note typecasting is safe because o is instance of Timestamp
        return compareTo((Timestamp)o);
    } else {
        // When Date doing a o.compareTo(this)
        // will give wrong results.
        Timestamp ts = new Timestamp(o.getTime());
        return this.compareTo(ts);
    }
}

问题2、Date 和 Timestamp 之间的 before() 和 after() 比较也有问题

Date date = new Date();
Timestamp timestampPlus1Mills = new Timestamp(date.getTime() + 1);
Assertions.assertTrue(timestampPlus1Mills.getTime() > date.getTime()); // true
Assertions.assertFalse(timestampPlus1Mills.after(date)); // false
Assertions.assertFalse(date.before(timestampPlus1Mills)); // false

原因也是 Date 和 Timestamp 之间的 before() 和 after() 比较也都会先转为 Date 类型再使用 Date 内的 before()/after() 比较,内部都调用了 getMillisOf(),还是只取了 Timestamp 的秒部分,丢失了毫秒。

public boolean before(Date when) {
    return getMillisOf(this) < getMillisOf(when);
}
public boolean after(Date when) {
    return getMillisOf(this) > getMillisOf(when);
}

解决方法: Date 和 Timestamp 比较时,调用 getTime() 直接比较毫秒时间戳


SimpleDateFormat

java.text.SimpleDateFormat public class SimpleDateFormat extends DateFormat SimpleDateFormat 是一个以与语言环境相关的方式来格式化和分析日期的具体类。它允许进行格式化(日期 -> 文本)、分析(文本 -> 日期)和规范化。 SimpleDateFormat 使得可以选择任何用户定义的日期-时间格式的模式。但是,仍然建议通过 DateFormat 中的 getTimeInstance、getDateInstance 或 getDateTimeInstance 来新的创建日期-时间格式化程序。

构造方法

public SimpleDateFormat(String pattern) 用给定的模式和默认语言环境的日期格式符号构造 SimpleDateFormat。注:此构造方法可能不支持所有语言环境。要覆盖所有语言环境,请使用 DateFormat 类中的工厂方法。

  • 参数:pattern - 描述日期和时间格式的模式
  • 抛出:
    • NullPointerException - 如果给定的模式为 null
    • IllegalArgumentException - 如果给定的模式无效

parse()

Date parse(String text, ParsePosition pos) 解析字符串的文本,生成 Date。 此方法试图解析从 pos 给定的索引处开始的文本。

format()

StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) 将给定的 Date 格式化为日期/时间字符串,并将结果添加到给定的 StringBuffer。格式为类 DateFormat 中的 format。返回格式化的日期-时间字符串。

Date转String

Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.format(date);

String转Date

Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2018-12-25 19:01:12");

注意 SimpleDateFormat.parse() 方法会抛出 ParseException 受检异常,必须捕获或抛出处理。


SimpleDateFormat与线程安全

如下面代码中,将 SimpleDateFormat 声明为 static ,开多个线程并发去parse日期

public class SimpleDateFormatTest {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String format(Date date) {
        return sdf.format(date);
    }

    public static Date parse(String dateStr) throws ParseException {
        return sdf.parse(dateStr);
    }

    @Test
    public void testSDFConcurrent() throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 20; i++) {
            executorService.execute(() -> {
                try {
                    System.out.println(parse(format(new Date())));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
        // 等待线程池中的线程执行完毕
        executorService.shutdown();
        // 阻塞当前线程,等待线程池执行完毕
        executorService.awaitTermination(10, TimeUnit.SECONDS);
    }
}

执行结果如下图,直接异常退出


SimpleDateFormat并发问题

原因: SimpleDateFormat 以及其实现的抽象类 DateFormat 都是使用了内部的成员变量 protected Calendar calendar; 来做转换,多线程情况下共享 SimpleDateFormat 对象会导致线程间数据互相影响,导致出错。

阿里巴巴Java开发手册中强制禁止将 SimpleDateFormat 定义为static

【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类。

解决方法: 1、只在需要的时候创建新实例,不用static修饰

public static String format(Date date) throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    return sdf.format(date);
}

public static Date parse(String strDate) throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    return sdf.parse(strDate);
}

不过会 会频繁地创建和销毁对象,效率较低。

2、synchronized对象锁

private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static String format(Date date) throws ParseException {
    synchronized(sdf){
        return sdf.format(date);
    }
}

public static Date parse(String strDate) throws ParseException {
    synchronized(sdf){
        return sdf.parse(strDate);
    }
}

并发量大的时候会对性能有影响,线程阻塞

3、ThreadLocal线程间隔离

private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
};

public static Date parse(String dateStr) throws ParseException {
    return threadLocal.get().parse(dateStr);
}

public static String format(Date date) {
    return threadLocal.get().format(date);
}

ThreadLocal可以确保每个线程都可以得到单独的一个SimpleDateFormat的对象,那么自然也就不存在竞争问题了。

4、使用Java8的DateTimeFormatter

private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

public static String format2(LocalDateTime date) {
    return formatter.format(date);
}

public static LocalDateTime parse2(String dateNow) {
    return LocalDateTime.parse(dateNow, formatter);
}

还在使用SimpleDateFormat?你的项目崩没?


Calendar

java.util.Calendar public abstract class Calendar extends Object implements Serializable, Cloneable, Comparable<Calendar> Calendar 类是一个抽象类,它为特定瞬间与一组诸如 YEAR、MONTH、DAY_OF_MONTH、HOUR 等 日历字段之间的转换提供了一些方法,并为操作日历字段(例如获得下星期的日期)提供了一些方法。瞬间可用毫秒值来表示,它是距历元(即格林威治标准时间 1970 年 1 月 1 日的 00:00:00.000,格里高利历)的偏移量。 与其他语言环境敏感类一样,Calendar 提供了一个类方法 getInstance,以获得此类型的一个通用的对象。Calendar 的 getInstance 方法返回一个 Calendar 对象,其日历字段已由当前日期和时间初始化:Calendar rightNow = Calendar.getInstance();

字段摘要

int DATE,get 和 set 的字段数字,指示一个月中的某天。它与 DAY_OF_MONTH 是同义词。一个月中第一天的值为 1。

getInstance()

public static Calendar getInstance() 使用默认时区和语言环境获得一个日历对象。返回的 Calendar已初始化为当前日期和时间,使用了默认时区和默认语言环境。 例如:Calendar rightNow = Calendar.getInstance();

setTime()

public final void setTime(Date date) 使用给定的 Date 设置此 Calendar 的时间。

getTime()

public final Date getTime() 返回一个表示此 Calendar 时间值(从历元至现在的毫秒偏移量)的 Date 对象。 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。

get()

int get(int field) 返回给定日历字段的值。

add()

public abstract void add(int field, int amount) 根据日历的规则,为给定的日历字段添加或减去指定的时间量。 add(f, delta) 将 delta 添加到 f 字段中。这等同于调用 set(f, get(f) + delta),但要带以下两个调整: Add 规则 1:调用后 f 字段的值减去调用前 f 字段的值等于 delta,以字段 f 中发生的任何溢出为模。溢出发生在字段值超出其范围时,结果,下一个更大的字段会递增或递减,并将字段值调整回其范围内。 例如,要从当前日历时间减去 5 天,可以通过调用以下方法做到这一点: add(Calendar.DAY_OF_MONTH, -5);

获取今天前n天的日期

public static Integer getNdaysBeforeNow(int n){
    Calendar cal=Calendar.getInstance();
    cal.add(Calendar.DAY_OF_MONTH, -n);
    SimpleDateFormat sdf=new SimpleDateFormat("yyyyMMdd");
    return Integer.parseInt(sdf.format(cal.getTime()));
}

获取指定日期的下一天

输入和输出参数都是Integer

public static Integer getNextDay(Integer datein) {
    String dateinStr = Integer.toString(datein);
    SimpleDateFormat sdf=new SimpleDateFormat("yyyyMMdd");
    java.util.Date date=null;
    try {
        date = sdf.parse(dateinStr);
    }catch(java.text.ParseException e) {
        e.printStackTrace();
    }
    Calendar cal=Calendar.getInstance();
    cal.setTime(date);
    cal.add(cal.DATE, 1); //日期加1天
    String dateoutStr = sdf.format(cal.getTime());
    return Integer.valueOf(dateoutStr);
}