1. JDK 8之前的时间和日期

1.1 使用Date与DateFormat

如果想要取得系统时间,方法之一是使用System.currentTimeMillis()方法,返回的是long类型整数,代表1970年1月1日0时0分0秒0毫秒至今经过的毫秒数,可以使用Date类让这个整数变得有意义一些。

Date有两个构造函数可以使用:

  • 一个可使用System.currentTimeMillis()的值创建;
  • 一个为无自变量构造函数,调用Date的toString()可以用具意义的文字显示系统时间,调用getTime()可取得与System.currentTimeMillis()相同的结果。
public class DateDemo {
    public static void main(String[] args) {
        Date date1 = new Date(currentTimeMillis());
        Date date2 = new Date();

        out.println(date1.getTime());
        out.println(date2.getTime());
    }
}

如果想按照指定的格式输出日期或当中的时间怎么办?可以使用DateFormat来做格式化。

DateFormat是个抽象类,其操作类是java.text.SimpleDateFormat。可以直接创建SimpleDateFormat实例,或使用DateFormat的getDateInstance()、getTimeInstance()、getDateTimeInstance()等静态方法,用较简便方式依不同需求取得SimpleDateFormat实例。

public class DateFormatDemo {
    public static void main(String[] args) {
        Date date = new Date();
        dateInstanceDemo(date);
        timeInstanceDemo(date);
        dateTimeInstanceDemo(date);
    }
    
    static void dateInstanceDemo(Date date) {
        out.println("getDateInstance() demo");
        out.printf("\tSHORT: %s%n", getDateInstance(LONG).format(date));
        out.printf("\tSHORT: %s%n", getDateInstance(SHORT).format(date));
    }

    static void timeInstanceDemo(Date date) {
        out.println("getTimeInstance() demo");
        out.printf("\tLONG: %s%n", getTimeInstance(LONG).format(date));
        out.printf("\tMEDIUM: %s%n", getTimeInstance(MEDIUM).format(date));
        out.printf("\tSHORT: %s%n",getTimeInstance(SHORT).format(date));
    }
    
    static void dateTimeInstanceDemo(Date date) {
        out.println("getDateTimeInstance() demo");
        out.printf("\tLONG: %s%n", 
           getDateTimeInstance(LONG, LONG).format(date));
        out.printf("\tMEDIUM: %s%n",
           getDateTimeInstance(SHORT, MEDIUM).format(date));
        out.printf("\tSHORT: %s%n",
           getDateTimeInstance(SHORT, SHORT).format(date));
    }
}

直接创建SimpleDateFormat的好处是,可使用模式字符串自定义格式。例如:

  public static void main(String[] args) {
        DateFormat dateFormat =
                new SimpleDateFormat("EE-MM-dd-yyyy", Locale.JAPAN);  
        Locale locale = new Locale("ja", "JP", "JP");
        DateFormat kanjiFormat = new SimpleDateFormat("GGGGy年M月d日", locale);
        System.out.println(dateFormat.format(new Date())); 
        System.out.println(kanjiFormat.format(new Date())); 
    }

SimpleDateFormat会依指定的Locale设定显示对应语言,EE表示星期格式设定,MM表示月份格式设定,dd表示日期格式设定,而yyyy是公元格式设定。每个字符设定都有其意义,可参考SimpleDateFormat的API说明,了解每个字符的设定意义。

SimpleDateFormat还有个parse()方法,可以依创建SimpleDateFormat时指定的格式,将指定的字符串剖析为Date实例。例如:

    public static void main(String[] args) throws Exception {
        System.out.print("出生年月日(yyyy-mm-dd):");
        DateFormat dateFormat = new SimpleDateFormat("yyyy-mm-dd");
        Date birthDate = dateFormat.parse(new Scanner(System.in).nextLine());
        Date currentDate = new Date();
        long life = currentDate.getTime() - birthDate.getTime();
        System.out.println("你今年的岁数为:" + 
                  (life / (365 * 24 * 60 * 60 * 1000L)));
    }

1.2 使用Calendar

可以使用Date来取得完整日期时间,可单纯使用toString()取得日期文字描述,或使用DateFormat格式化日期,但如果想要取得某个时间或日期信息该如何进行?

例如想知道现在时间是6月的第几天。

若查看Date的API文件,会发现许多方法都不再建议使用(Deprecated),而建议改用Calendar的相关方法取代。

Calendar是个抽象类,java.util.GregorianCalendar是其子类成果,通常通过Calendar的getInstance()来取得Calendar实例,取得的实例代表当时系统时间。例如:

Calendar now = Calendar.getInstance(Locale.JAPAN)

取得Calendar实例后,可以使用getTime()取得Date实例。如果想知道Calendar代表公元几年,可以使用get()方法。例如:

System.out.println(now.get(Calendar.YEAR));

如果现在是2019年,则上例会显示2019的数字。依照这个范例,假设现在时间是9月份,而你想使用程序取得目前月份,则对以下执行结果你可能会困惑:

System.out.println(now.get(Calendar.MONTH));

上面的程序片段会显示5这个数字,而不是你预期的6。

事实上,返回值是对应于Calendar.SEPTEMBER常数值,get()指定某些常数会返回对应某些常数的值,记得查询API文件确认。

假设有个需求,想看看今天是星期几,显示不同的信息,那该怎么做呢?

public class CalendarDemo {
    public static void main(String[] args) {
        Map<Integer, String> days = new HashMap<>();
        days.put(Calendar.MONDAY, "星期一猴子穿新衣");
        days.put(Calendar.TUESDAY, "星期二猴子肚子餓");
        days.put(Calendar.WEDNESDAY, "星期三猴子去爬山");
        days.put(Calendar.THURSDAY, "星期四猴子去考試");
        days.put(Calendar.FRIDAY, "星期五猴子去跳舞");
        days.put(Calendar.SATURDAY, "星期六猴子去斗六");
        days.put(Calendar.SUNDAY, "星期天猴子乐翻天");
        
        Calendar calendar = Calendar.getInstance();
        int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
        System.out.println(days.get(dayOfWeek));
    }
}

如果想以区域化方式显示星期、月份名称,可以使用Calendar的getDisplayNames()或getDisplayName()方法。例如:

public class CalendarDemo2 {
    public static void main(String[] args) {
        Calendar rightNow = Calendar.getInstance();
       
        System.out.println("現在時間是:");
        System.out.printf("%s%d %n",
           rightNow.getDisplayName(ERA, LONG, Locale.JAPAN),
           rightNow.get(YEAR));
        System.out.println(
           rightNow.getDisplayName(MONTH, LONG, Locale.JAPAN));
        System.out.printf("%d日%n", 
           rightNow.get(DAY_OF_MONTH));
        System.out.println(
           rightNow.getDisplayName(DAY_OF_WEEK, LONG, Locale.JAPAN));
    }   
}

在取得一个Calendar的实例后,可以使用add()方法,来改变Calendar的时间。例如:

		 Calendar calendar = Calendar.getInstance();
		 calendar.add(Calendar.MONTH, 1);
		 System.out.println(calendar);
		 calendar.add(Calendar.HOUR, 12);
		 System.out.println(calendar);
		 calendar.add(Calendar.YEAR, -2);
		 System.out.println(calendar);
		 calendar.add(Calendar.DAY_OF_MONTH,  3);
		 System.out.println(calendar);

如果打算只针对日期中某个字段加减,则可以使用roll()方法。例如:

		 calendar.roll(Calendar.MONTH, 1);
		 System.out.println(calendar);

1.3 计算时间差

public static void main(String[] args) {
		Calendar c1 = Calendar.getInstance();
		c1.clear();

		Calendar c2 = Calendar.getInstance();
		c2.clear();

	
		c1.set(2020, 0, 1);
		c2.set(2020, 2, 1);


		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
		System.out.println("Date 1: " + sdf.format(c1.getTime()));
		System.out.println("Date 2: " + sdf.format(c2.getTime()));


		long time1 = c1.getTimeInMillis();
		long time2 = c2.getTimeInMillis();


		long diff = time2 - time1;

		long diffSec = diff / 1000;
		System.out.println("Difference in seconds " + diffSec);


		long diffMin = diff / (60 * 1000);
		System.out.println("Difference in minutes " + diffMin);

		
		long diffHours = diff / (60 * 60 * 1000);
		System.out.println("Difference in hours " + diffHours);

	
		long diffDays = diff / (24 * 60 * 60 * 1000);
		System.out.println("Difference in days " + diffDays);
	}

2. 新时间日期API

新的时间API位于java.time包下。

2.1 Instant

Instant 和 Date 一样,表示一个时间戳,用于描述一个时刻,只不过它较 Date 而言,可以描述更加精确的时刻。并且 Instant 是时区无关的。

Date 最多可以表示毫秒级别的时刻,而 Instant 可以表示纳秒级别的时刻。例如:

  • public static Instant now():根据系统当前时间创建一个 Instant 实例,表示当前时刻
  • public static Instant ofEpochSecond(long epochSecond):通过传入一个标准时间的偏移值来构建一个 Instant 实例
  • public static Instant ofEpochMilli(long epochMilli):通过毫秒数值直接构建一个 Instant 实例
	public static void main(String[] args) {
		Instant instant = Instant.now();
		System.out.println(instant);

		Instant instant1 = Instant.ofEpochSecond(20);
		System.out.println(instant1);

		Instant instant2 = Instant.ofEpochSecond(30, 100);
		System.out.println(instant2);

		Instant instant3 = Instant.ofEpochMilli(1000);
		System.out.println(instant3);

	}

可以看到,Instant 和 Date 不同的是,它是时区无关的,始终是格林零时区相关的,也即是输出的结果始终格林零时区时间。

2.2 LocalDateTime,LocalDate,LocalTime

以Local开头的都是对时间的描述。

2.2.1 LocalDate

LocalDate 专注于处理日期相关信息。LocalDate 依然是一个不可变类,它关注时间中年月日部分,我们可以通过以下的方法构建和初始化一个 LocalDate 实例:

  • public static LocalDate now():截断当前系统时间的年月日信息并初始化一个实例对象
  • public static LocalDate of(int year, int month, int dayOfMonth):显式指定年月日信息
  • public static LocalDate ofYearDay(int year, int dayOfYear):根据 dayOfYear 可以推出 month 和 dayOfMonth
  • public static LocalDate ofEpochDay(long epochDay):相对于格林零时区时间的日偏移量
		LocalDate localDate = LocalDate.now();
		System.out.println(localDate);

		LocalDate localDate1 = LocalDate.of(2019, 6, 29);
		System.out.println(localDate1);

		LocalDate localDate2 = LocalDate.ofYearDay(2019, 100);
		System.out.println(localDate2);

		LocalDate localDate3 = LocalDate.ofEpochDay(10);
		System.out.println(localDate3);

其他的方法

  • public int getYear():获取年份信息
  • public int getMonthValue():获取月份信息
  • public int getDayOfMonth():获取当前日是这个月的第几天
  • public int getDayOfYear():获取当前日是这一年的第几天
  • public boolean isLeapYear():是否是闰年
  • public int lengthOfYear():获取这一年有多少天
  • public DayOfWeek getDayOfWeek():返回星期信息
2.2.2 LocalTime

LocalTime 专注于时间的处理,它提供小时,分钟,秒,毫微秒的各种处理,我们依然可以通过类似的方式创建一个 LocalTime 实例。

  • public static LocalTime now():根据系统当前时刻获取其中的时间部分内容
  • public static LocalTime of(int hour, int minute):显式传入小时和分钟来构建一个实例对象
  • public static LocalTime of(int hour, int minute, int second):通过传入时分秒构造实例
  • public static LocalTime of(int hour, int minute, int second, int nanoOfSecond):传入时分秒和毫微秒构建一个实例
  • public static LocalTime ofSecondOfDay(long secondOfDay):传入一个长整型数值代表当前日已经过去的秒数
  • public static LocalTime ofNanoOfDay(long nanoOfDay):传入一个长整型代表当前日已经过去的毫微秒数

同样的,LocalTime 默认使用系统默认时区处理时间,看代码:

	public static void main(String[] args) {
		LocalTime localTime = LocalTime.now();
		System.out.println(localTime);

		LocalTime localTime1 = LocalTime.of(23, 59);
		System.out.println(localTime1);

		LocalTime localTime2 = LocalTime.ofSecondOfDay(10);
		System.out.println(localTime2);

	}

LocalTime 中也同样封装了很多好用的工具方法,例如:

  • public int getHour()
  • public int getMinute()
  • public int getSecond()
  • public int getNano()
  • public LocalTime withHour(int hour):修改当前 LocalTime 实例中的 hour 属性并重新返回一个新的实例
  • public LocalTime withMinute(int minute):类似
  • public LocalTime withSecond(int second):类似
2.2 LocalDateTime

LocalDateTime 类则是集成了 LocalDate 和 LocalTime,它既能表示日期,又能表述时间信息,方法都类似

2.3 时区相关

无论是我们的 LocalDate,或是 LocalTime,甚至是 LocalDateTime,它们基本是时区无关的,内部并没有存储时区属性,而基本用的系统默认时区。往往有些场景之下,缺乏一定的灵活性。

ZonedDateTime 可以被理解为 LocalDateTime 的外层封装,它的内部存储了一个 LocalDateTime 的实例,专门用于普通的日期时间处理。此外,它还定义了 ZoneId 和 ZoneOffset 来描述时区的概念。

ZonedDateTime 和 LocalDateTime 的一个很大的不同点在于,后者内部并没有存储时区,所以对于系统的依赖性很强,往往换一个时区可能就会导致程序中的日期时间不一致。

而后者则可以通过传入时区的名称,使用 ZoneId 进行匹配存储,也可以通过传入与零时区的偏移量,使用 ZoneOffset 存储时区信息。

所以,构建一个 ZonedDateTime 实例有以下几种方式:

  • public static ZonedDateTime now():系统将以默认时区计算并存储日期时间信息
  • public static ZonedDateTime now(ZoneId zone):指定时区
  • public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone):指定日期时间和时区
  • public static ZonedDateTime of(LocalDateTime localDateTime, ZoneId zone)
  • public static ZonedDateTime ofInstant(Instant instant, ZoneId zone):通过时刻和时区构建实例对象
		ZonedDateTime zonedDateTime = ZonedDateTime.now();
		System.out.println(zonedDateTime);

		LocalDateTime localDateTime = LocalDateTime.now();
		ZoneId zoneId = ZoneId.of("Asia/Tokyo");
		ZonedDateTime zonedDateTime1 = ZonedDateTime.of(localDateTime, zoneId);
		System.out.println(zonedDateTime1);

		Instant instant = Instant.now();
		ZoneId zoneId1 = ZoneId.of("GMT");
		ZonedDateTime zonedDateTime2 = ZonedDateTime.ofInstant(instant, zoneId1);
		System.out.println(zonedDateTime2);

		LocalTime localTime = LocalTime.of(0, 0, 0);
		LocalDate localDate = LocalDate.of(1975, 4, 1);
		ZonedDateTime zonedDateTime3 = ZonedDateTime.of(localDate, localTime, ZoneId.of("Asia/Tokyo"));

		out.println(zonedDateTime3);
		out.println(zonedDateTime3.toEpochSecond());
		out.println(zonedDateTime3.toInstant().toEpochMilli());

2.4 格式化日期时间

Java 8 的新式日期时间 API 中,DateTimeFormatter 作为格式化日期时间的主要类,它与之前的 DateFormat 类最大的不同就在于它是线程安全的,其他的使用上的操作基本类似。我们看看:

		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
		LocalDateTime localDateTime = LocalDateTime.now();
		System.out.println(formatter.format(localDateTime));

		String str = "2019年06月28日 23:59:59";
		DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
		LocalDateTime localDateTime2 = LocalDateTime.parse(str, formatter2);
		System.out.println(localDateTime2);

格式化主要有两种情况,一种是将日期时间格式化成字符串,另一种则是将格式化的字符串装换成日期时间对象。

DateTimeFormatter 提供将 format 方法将一个日期时间对象转换成格式化的字符串,但是反过来的操作却建议使用具体的日期时间类自己的 parse 方法,这样可以省去类型转换的步骤。

2.5 计算时间差

现实项目中,我们也经常会遇到计算两个时间点之间的差值的情况,最粗暴的办法是,全部幻化成毫秒数并进行减法运算,最后在转换回日期时间对象。

但是 java.time 包中提供了两个日期时间之间的差值的计算方法,我们一起看看。

关于时间差的计算,主要涉及到两个类:

  • Period:处理两个日期之间的差值
  • Duration:处理两个时间之间的差值
	public static void main(String[] args) {
		LocalDate date = LocalDate.of(2014, 7, 22);
		LocalDate date1 = LocalDate.now();
		Period period = Period.between(date, date1);
		System.out.println(period.getYears() + "年" + period.getMonths() + "月" + period.getDays() + "天");

		LocalTime time = LocalTime.of(20, 30);
		LocalTime time1 = LocalTime.of(23, 59);
		Duration duration = Duration.between(time, time1);
		System.out.println(duration.toMinutes() + "分钟");

	}