如何将java.util.Date转换为Java8的java.time.YearMonth

18

我应如何最好地将java.util.Date转换为Java 8的java.time.YearMonth

不幸的是,以下内容会抛出DateTimeException异常:

YearMonth yearMonth = YearMonth.from(date.toInstant());

结果为:

java.time.DateTimeException: Unable to obtain YearMonth from TemporalAccessor: 2015-01-08T14:28:39.183Z of type java.time.Instant
at java.time.YearMonth.from(YearMonth.java:264)
...

我需要这个功能是因为我想使用JPA在数据库中存储年月的值。目前JPA不支持年月,所以我设计了以下YearMonthConverter(省略导入):

// TODO (future): delete when next version of JPA (i.e. Java 9?) supports YearMonth. See https://java.net/jira/browse/JPA_SPEC-63
@Converter(autoApply = true)
public class YearMonthConverter implements AttributeConverter<YearMonth, Date> {

  @Override
  public Date convertToDatabaseColumn(YearMonth attribute) {
    // uses default zone since in the end only dates are needed
    return attribute == null ? null : Date.from(attribute.atDay(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
  }

  @Override
  public YearMonth convertToEntityAttribute(Date dbData) {
    // TODO: check if Date -> YearMonth can't be done in a better way
    if (dbData == null) return null;
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(dbData);
    return YearMonth.of(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1);
  }
}

难道没有更好(更简洁,更短)的解决方案吗?(针对双向都适用的情况)


1
个人而言,我会避免映射到 java.util.Date,因为 a) 它不是 java.sql.* 类型,b) 包含隐式时区(性能差且模糊使用系统时区),c) SQL 没有为年月定义这样的类型。更好的方法是使用 PROLEPTIC_MONTHYearMonth 映射到整数。 - Meno Hochschild
@MenoHochschild: 这是一个好主意 - 唯一的问题是在查看数据库本身时它不可读。在我的情况下没有问题... - Sebastian
好的,仅仅一个整数并不能直接查看数据库内容,但需要注意的是,将其映射到 j.u.Date 可能会因为时区转换和数据库/服务器设置的原因改变可视化的日期和月份(作为数据库管理员所看到的)。 - Meno Hochschild
我选择了这个解决方案——虽然它没有回答我的原始问题,但在我的特殊情况下对我有所帮助...public Integer convertToDatabaseColumn(YearMonth yearMonth) { return (yearMonth == null) ? null : (int) yearMonth.getLong(ChronoField.PROLEPTIC_MONTH); }public YearMonth convertToEntityAttribute(Integer dbData) { return (dbData == null) ? null : YearMonth.from(YearMonth.now().with(ChronoField.PROLEPTIC_MONTH, dbData)); } - Sebastian
1个回答

30

简短回答:

// From Date to YearMonth
YearMonth yearMonth =
        YearMonth.from(date.toInstant()
                           .atZone(ZoneId.systemDefault())
                           .toLocalDate());

// From YearMonth to Date
// The same as the OP:s answer
final Date convertedFromYearMonth = 
        Date.from(yearMonth.atDay(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
说明:

YearMonth.from(TemporalAccessor)方法的JavaDoc说明:

该转换从YEAR和MONTH_OF_YEAR字段中提取数据。只有当时间对象具有ISO年表或可以转换为LocalDate时,才允许进行提取。

因此,您需要能够执行以下操作之一:

  1. 提取YEARMONTH_OF_YEAR字段,或者
  2. 使用可转换为LocalDate的内容。

让我们试试吧!

final Date date = new Date();
final Instant instant = date.toInstant();
instant.get(ChronoField.YEAR); // causes an error

这是不可能的,会抛出异常:

java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: Year at java.time.Instant.get(Instant.java:571) ...

这意味着备选方案1不能实现。关于如何将 Date 转换为 LocalDate 的出色答案解释了原因,可以在这里找到。

尽管它的名称是 java.util.Date,但它表示时间线上的一个瞬间,而不是“日期”。对象内部存储的实际数据是自1970-01-01T00:00Z(1970年GMT / UTC开始时的午夜)以来的毫秒数。

在JSR-310中,与 java.util.Date 相当的类是 Instant,因此有一个方便的方法 toInstant() 可以进行转换。

因此,Date 可以转换为 Instant,但这并没有帮助我们,对吧?

然而,备选方案2证明是成功的。将 Instant 转换为 LocalDate,然后使用 YearMonth.from(TemporalAccessor) 方法即可。

    Date date = new Date();

    LocalDate localDate = date.toInstant()
                              .atZone(ZoneId.systemDefault())
                              .toLocalDate();

    YearMonth yearMonth = YearMonth.from(localDate);
    System.out.println("YearMonth: " + yearMonth);

(由于代码是在2015年1月使用的),输出结果如下:

年月: 2015-01


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接