如何才能在Java中优雅的操纵时间?
阿里妹导读
怎么才能在Java中优雅的操纵时间呢,作者整理了相关的概念和工具类,希望帮助大家在代码开发的过程中对时间的使用更加优雅。
在开发时候,发现有很多需要用到时间的地方,例如记录操作的时间、比较时间判断产品是否有效等。总而言之,时间是我们业务开发必须关注、时刻注意的点。但目前工程的代码中使用了非常多时间的工具类,一会儿用Java.util.Date记录时间,一会用Java.time.LocalDateTime记录时间,怎么才能在Java中优雅的操纵时间呢,我整理了相关的概念和工具类,希望帮助大家在代码开发的过程中对对时间的使用更加优雅。
这里先写一个结论:
建议使用java8的时间API,在安全性和易用性上都远高于java.util.Date。 目前比较流行的封装java API的时间工具类大都基于java.util.Date,建议在开发过程中根据业务需要基于java.time.*的方法封装工具类(文末给出了一个简单的实现)。
时间在计算机中的存储和展示
时间以整数的方式进行存储:时间在计算机中存储的本质是一个整数,称为Epoch Time(时间戳),计算从1970年1月1日零点(格林威治时间/GMT+00:00)到现在所经历的秒数。
在java程序中,时间戳通常使用long表示毫秒数,通过System.currentTimeMillis()可以获取时间戳。时间戳对我们人来说是不易理解的,因此需要将其转换为易读的时间,例如,2024-10-7 20:21:59(实际上说的是本地时间),而同一时刻不同时区的人看到的本地时间是不一样,所以在时间展示的时候需要加上时区的信息,才能精准的找到对应的时刻。
时区与世界时间标准相关:
日期API
java.util
Date
public class Date {long fastTime;public Date(long date) {fastTime = date;}public long getTime() {return fastTime;}}
java.util.Date承载的功能有限,且在利用Date类获取具体年/月/日的时候需要注意:getYear()返回的年份必须加上1900,getMonth()返回的月份是0-11分别表示1-12月,所以要加1,而getDate()返回的日期范围是1~31,又不能加1。
Calendar
import java.util.*;public class Main {public static void main(String[] args) {// 获取当前时间:Calendar c = Calendar.getInstance();int y = c.get(Calendar.YEAR);//返回年份不用转换int m = 1 + c.get(Calendar.MONTH);//返回月份需要加1int d = c.get(Calendar.DAY_OF_MONTH);int w = c.get(Calendar.DAY_OF_WEEK);//返回的int hh = c.get(Calendar.HOUR_OF_DAY);int mm = c.get(Calendar.MINUTE);int ss = c.get(Calendar.SECOND);int ms = c.get(Calendar.MILLISECOND);System.out.println(y + "-" + m + "-" + d + " " + w + " " + hh + ":" + mm + ":" + ss + "." + ms);}}
import java.text.*;import java.util.*;public class Main {public static void main(String[] args) {// 当前时间:Calendar c = Calendar.getInstance();// 清除所有:c.clear();// 设置年月日时分秒:c.set(2019, 10 /* 11月 */, 20, 8, 15, 0);// 加5天并减去2小时:c.add(Calendar.DAY_OF_MONTH, 5);c.add(Calendar.HOUR_OF_DAY, -2);// 显示时间:var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Date d = c.getTime();System.out.println(sdf.format(d));// 2019-11-25 6:15:00}}
TimeZone
import java.text.*;import java.util.*;public class learnTime {public static void main(String[] args) {// 当前时间:Calendar c = Calendar.getInstance();// 清除所有字段:c.clear();// 设置为北京时区:c.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));// 设置年月日时分秒:c.set(2024, 9 /* 10月 */, 10, 8, 15, 0);// 显示时间:SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));System.out.println(sdf.format(c.getTime()));// 2024-10-09 20:15:00}}
java.text.SimpleDateFormat
// SimpleDateFormat线程不安全,每次使用都要构造新的,在初始的时候定义解析的字符串格式SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 将指定字符串String解析为DateDate date = format.parse("2024-10-07 16:10:22");// 将Date格式化为StringString str = format.format(date);
由于SimpleDateFormat线程不安全,为了提升性能,会使用ThreadLocalCache,如下:
static final ThreadLocal<SimpleDateFormat> SIMPLE_DATE_FORMAT_LOCAL= ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
Java.time.*
开源社区开发了一个日期类Joda,API清晰,性能较好,提交了JSR-310,在java8中称为JDK基础类库。
本地日期和时间:LocalDateTime(日期和时间),LocalDate(日期),LocalTime(时间);
因为没有时区无法与时间戳转换。 带时区的日期和时间:ZonedDateTime;
时刻:Instant;
时区:ZoneId,ZoneOffset;
时间间隔:Duration。
LocalDate/LocalTime/LocalDateTime
默认严格按照ISO 8601规定日期和时间格式进行打印(日期和时间的分隔符是T)。 日期:yyyy-MM-dd; 时间HH:mm:ss;
日期和时间:yyyy-MM-dd'T'HH:mm:ss; 可以解析简单格式获取类型:
LocalDateTime localDayTime=LocalDateTime.of(2024, 10, 07, 8, 15, 0);LocalDate localDay=LocalDate.of(2024, 10, 07);LocalTime localTime=LocalTime.parse("08:15:07");
有对日期和时间进行加减的非常简单的链式调用,通过plusXxx()/minusXxx()对时间进行变换:
public class learnTime {public static void main(String[] args) {LocalDateTime dt = LocalDateTime.of(2024, 10, 10, 20, 30, 59);System.out.println(dt);// 加5天减3小时:2024-10-10T20:30:59LocalDateTime dt2 = dt.plusDays(5).minusHours(3);System.out.println(dt2); // 2024-10-15T17:30:59// 减1月:LocalDateTime dt3 = dt2.minusMonths(1); //2024-09-15T17:30:59System.out.println(dt3); // 2019-09-30T17:30:59}}
对日期和时间进行调整使用withXxx(),例如将月份调整为:
9月 dataLocalTime.withMonth(9)
复杂的操作:获取特殊时间
with和TemporalAdjusters配合使用找到特殊时间(当月的第一天)。
public class Main {public static void main(String[] args) {LocalDateTime now = LocalDateTime.now();// 获取本月第一天0:00时刻:System.out.println("当月第一天0:00时刻"+now.withDayOfMonth(1).atStartOfDay());//获取当月第一天System.out.println("当月第一天:"+now.with(TemporalAdjusters.firstDayOfMonth()));//获取下月第一天System.out.println("下月第一天:"+now.with(TemporalAdjusters.firstDayOfNextMonth()));//获取明年第一天System.out.println("明年第一天:"+now.with(TemporalAdjusters.firstDayOfNextYear()));//获取本年第一天System.out.println("本年第一天:"+now.with(TemporalAdjusters.firstDayOfYear()));//获取当月最后一天System.out.println("当月最后一天:"+now.with(TemporalAdjusters.lastDayOfMonth()));//获取本年最后一天System.out.println("本年最后一天:"+now.with(TemporalAdjusters.lastDayOfYear()));//获取当月第三周星期五System.out.println("当月第三周星期五:"+now.with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.FRIDAY)));//获取上周一System.out.println("上周一:"+now.with(TemporalAdjusters.previous(DayOfWeek.MONDAY)));//获取下周日System.out.println("下周日:"+now.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)));}}
比较可以使用 isBefore()和isAfter()。
Duration和Period
Duration:
基于时间值(Instant/LocalDateTime),表示两个时刻时间的时间间隔,适合处理较短的时间,需要更高的精确性。 使用between()方法比较两个瞬间的差;
使用getSeconds()或getNanosecends()方法获取时间单元的值;
获得具体的粒度的间隔:ofDays(),ofHours(), ofMillis(), ofMinutes(), ofNanos(), ofSeconds();
通过文本创建Duration对象,格式为 “PnDTnHnMn.nS”, Duration.parse("P1DT1H10M10.5S");
使用toDays(), toHours(), toMillis(), toMinutes()方法把Duration对象可以转成其他时间单元;
通过 plusX()、minusX()方法增加或减少Duration对象,其中X表示days, hours, millis, minutes, nanos 或 seconds。 Period基于日期值,表示一段时间的年、月、日: 使用between()方法比较两个日期的差;
使用getYears(),getMonhs(),getDays()方法获取具体粒度差距(返回的类型是int);
通过文本创建Period对象,格式为 “PnYnMnD”:Period.parse("P2Y3M5D");
可以通过plusX()、minusX()方法进行增加或减少,其中X表示日期单元;
ZonedDateTime
ZonedDateTime 带时区时间的常见方法: now():获取当前时区的ZonedDateTime对象
now(ZoneId zone):获取指定时区的ZonedDateTime对象
getYear, getMonthValue, getDayOfMonth等: 获取年月日、时分秒、纳秒等
withXxx(时间):修改时间系列的方法
minusXxx(时间):减少时间系列的方法
plusXxx(时间):增加时间系列的方法 时区转换
import java.time.*;public class Main {public static void main(String[] args) {// 以中国时区获取当前时间:ZonedDateTime zbj = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));// 转换为纽约时间:ZonedDateTime zny = zbj.withZoneSameInstant(ZoneId.of("America/New_York"));System.out.println(zbj);System.out.println(zny);}}
ZoneId时区类
getAvailableZoneIds(): 获取Java中支持的所有时区
systemDefault(): 获取系统默认时区
of(String zoneId): 获取一个指定时区
Instant
时间线上的某个时刻/时间戳
作用:可以用来记录代码的执行时间,或用于记录用户操作某个事件的时间点。
传统的Date类,只能精确到毫秒,并且是可变对象。
新增的Instant类,可以精确到纳秒,并且是不可变对象,推荐用Instant代替Date。
//1、创建Instant的对象,获取此刻时间信息Instant now = Instant.now(); //不可变对象//2、获取总秒数long second = now.getEpochSecond();system.out.println(second) ;//3、不够1秒的纳秒数int nano = now.getNano();system.out.println(nano) ;system.out.println(now);//可以进行加减法Instant instant = now.plusNanos(111);//将纳秒加111// Instant对象的作用:做代码的性能分析,或者记录用户的操作时间点Instant now1 = Instant.now();//代码执行...Instant now2 = Instant.now();//用这两个时间点相减就可以知道这段代码运行了多少时间
DataTimeFormatter
import java.time.*;import java.time.format.*;import java.util.Locale;public class Main {public static void main(String[] args) {ZonedDateTime zdt = ZonedDateTime.now();DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");System.out.println(formatter.format(zdt));DateTimeFormatter zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);System.out.println(zhFormatter.format(zdt));DateTimeFormatter usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);System.out.println(usFormatter.format(zdt));//2024-10-08T00:25 GMT+08:00//2024 十月 08 星期二 00:25//Tue, October/08/2024 00:25}}
转换
新老API转换参考:https://blog.csdn.net/qq_31635851/article/details/120150588
LocalDateTime不包括时区,而——
为了从<font style="background-color:rgb(249, 242, 244);">LocalDateTime</font>转换到——
// LocalDateTime 转换为 DateLocalDateTime localDateTime = LocalDateTime.now();ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());Date date = Date.from(zonedDateTime.toInstant());// Date 转换为 LocalDateTimeDate date = new Date();Instant instant = date.toInstant();LocalDateTime localDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
数据库映射变化
java.util.Date和数据库映射:
<arg column="gmt_create" jdbcType="TIMESTAMP" javaType="java.util.Date"/>java.time.*和数据库映射:
<arg column="gmt_create" jdbcType="TIMESTAMP" javaType="java.time.LocalDateTime"/>mybatis 3.5.0以后已经支持,有LocalDateTimeTypeHandler等类型处理器支持,不需要额外操作。
比较老的mybatis版本可能会报错,需要添加相关的依赖。
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis-typehandlers-jsr310</artifactId><version>1.0.2</version></dependency>
Mybatis中和时间相关的 jdbcType和javaType、typeHandler的对照关系
操作时间相关的工具
有一些对基础的API进行了封装便于我们在开发中有效的处理时间。
蚂蚁时间工具类:
com.iwallet.biz.common.util.DateUtil
基于Java.Util.Date,提供了广泛的日期/时间处理方法,可满足绝大部分需求。 org.apache.commons.lang3.time
包括多种基于Java.util.Date封装的工具类,提供了很多方便操作日期和时间的算法。
import java.time.*;import java.time.format.DateTimeFormatter;import java.time.temporal.ChronoUnit;public class DateUtils {// 获取当前日期public static LocalDate getCurrentDate() {return LocalDate.now();}// 获取当前时间public static LocalTime getCurrentTime() {return LocalTime.now();}// 获取当前日期时间public static LocalDateTime getCurrentDateTime() {return LocalDateTime.now();}// 格式化日期为字符串public static String formatLocalDate(LocalDate date, String pattern) {DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);return date.format(formatter);}// 解析字符串为LocalDatepublic static LocalDate parseLocalDate(String dateStr, String pattern) {DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);return LocalDate.parse(dateStr, formatter);}// 增加指定天数public static LocalDate addDays(LocalDate date, long days) {return date.plusDays(days);}// 减少指定天数public static LocalDate minusDays(LocalDate date, long days) {return date.minusDays(days);}// 计算两个日期之间的天数差public static long getDaysBetween(LocalDate startDate, LocalDate endDate) {return ChronoUnit.DAYS.between(startDate, endDate);}// 获取指定日期所在月份的第一天public static LocalDate getFirstDayOfMonth(LocalDate date) {return date.withDayOfMonth(1);}// 获取指定日期所在月份的最后一天public static LocalDate getLastDayOfMonth(LocalDate date) {return date.withDayOfMonth(date.lengthOfMonth());}// 判断两个日期是否相等public static boolean isSameDate(LocalDate date1, LocalDate date2) {return date1.isEqual(date2);}// 判断日期是否在指定范围内public static boolean isDateInRange(LocalDate date, LocalDate startDate, LocalDate endDate) {return date.isAfter(startDate) && date.isBefore(endDate);}// 获取指定日期的星期几public static DayOfWeek getDayOfWeek(LocalDate date) {return date.getDayOfWeek();}// 判断是否为闰年public static boolean isLeapYear(int year) {return Year.of(year).isLeap();}// 获取指定月份的天数public static int getDaysInMonth(int year, int month) {return YearMonth.of(year, month).lengthOfMonth();}// 获取指定日期的年份public static int getYear(LocalDate date) {return date.getYear();}// 获取指定日期的月份public static int getMonth(LocalDate date) {return date.getMonthValue();}// 获取指定日期的天数public static int getDayOfMonth(LocalDate date) {return date.getDayOfMonth();}// 获取指定日期的小时数public static int getHour(LocalDateTime dateTime) {return dateTime.getHour();}// 获取指定日期的分钟数public static int getMinute(LocalDateTime dateTime) {return dateTime.getMinute();}// 获取指定日期的秒数public static int getSecond(LocalDateTime dateTime) {return dateTime.getSecond();}// 判断指定日期是否在当前日期之前public static boolean isBefore(LocalDate date) {return date.isBefore(LocalDate.now());}// 判断指定日期是否在当前日期之后public static boolean isAfter(LocalDate date) {return date.isAfter(LocalDate.now());}// 判断指定日期是否在当前日期之前或相等public static boolean isBeforeOrEqual(LocalDate date) {return date.isBefore(LocalDate.now()) || date.isEqual(LocalDate.now());}// 判断指定日期是否在当前日期之后或相等public static boolean isAfterOrEqual(LocalDate date) {return date.isAfter(LocalDate.now()) || date.isEqual(LocalDate.now());}// 获取指定日期的年龄public static int getAge(LocalDate birthDate) {LocalDate currentDate = LocalDate.now();return Period.between(birthDate, currentDate).getYears();}// 获取指定日期的季度public static int getQuarter(LocalDate date) {return (date.getMonthValue() - 1) / 3 + 1;}// 获取指定日期的下一个工作日public static LocalDate getNextWorkingDay(LocalDate date) {do {date = date.plusDays(1);} while (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY);return date;}// 获取指定日期的上一个工作日public static LocalDate getPreviousWorkingDay(LocalDate date) {do {date = date.minusDays(1);} while (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY);return date;}// 获取指定日期所在周的第一天(周一)public static LocalDate getFirstDayOfWeek(LocalDate date) {return date.with(DayOfWeek.MONDAY);}// 获取指定日期所在周的最后一天(周日)public static LocalDate getLastDayOfWeek(LocalDate date) {return date.with(DayOfWeek.SUNDAY);}// 获取指定日期所在年的第一天public static LocalDate getFirstDayOfYear(LocalDate date) {return date.withDayOfYear(1);}// 获取指定日期所在年的最后一天public static LocalDate getLastDayOfYear(LocalDate date) {return date.withDayOfYear(date.lengthOfYear());}// 获取指定日期所在季度的第一天public static LocalDate getFirstDayOfQuarter(LocalDate date) {int month = (date.getMonthValue() - 1) / 3 * 3 + 1;return LocalDate.of(date.getYear(), month, 1);}// 获取指定日期所在季度的最后一天public static LocalDate getLastDayOfQuarter(LocalDate date) {int month = (date.getMonthValue() - 1) / 3 * 3 + 3;return LocalDate.of(date.getYear(), month, Month.of(month).maxLength());}// 判断指定日期是否为工作日(周一至周五)public static boolean isWeekday(LocalDate date) {return date.getDayOfWeek() != DayOfWeek.SATURDAY && date.getDayOfWeek() != DayOfWeek.SUNDAY;}// 判断指定日期是否为周末(周六或周日)public static boolean isWeekend(LocalDate date) {return date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY;}// 获取指定日期所在月份的工作日天数public static int getWeekdayCountOfMonth(LocalDate date) {int weekdayCount = 0;LocalDate firstDayOfMonth = getFirstDayOfMonth(date);LocalDate lastDayOfMonth = getLastDayOfMonth(date);while (!firstDayOfMonth.isAfter(lastDayOfMonth)) {if (isWeekday(firstDayOfMonth)) {weekdayCount++;}firstDayOfMonth = firstDayOfMonth.plusDays(1);}return weekdayCount;}// 获取指定日期所在月份的周末天数public static int getWeekendCountOfMonth(LocalDate date) {int weekendCount = 0;LocalDate firstDayOfMonth = getFirstDayOfMonth(date);LocalDate lastDayOfMonth = getLastDayOfMonth(date);while (!firstDayOfMonth.isAfter(lastDayOfMonth)) {if (isWeekend(firstDayOfMonth)) {weekendCount++;}firstDayOfMonth = firstDayOfMonth.plusDays(1);}return weekendCount;}// 获取指定日期所在年份的工作日天数public static int getWeekdayCountOfYear(LocalDate date) {int weekdayCount = 0;LocalDate firstDayOfYear = getFirstDayOfYear(date);LocalDate lastDayOfYear = getLastDayOfYear(date);while (!firstDayOfYear.isAfter(lastDayOfYear)) {if (isWeekday(firstDayOfYear)) {weekdayCount++;}firstDayOfYear = firstDayOfYear.plusDays(1);}return weekdayCount;}}