在没有* toInstant()的情况下将java.util.Date截断为LocalDate *,因为java.sql.Date给出了UnsupportedOperationException

Truncating java.util.Date to LocalDate *without* toInstant() because java.sql.Date gives UnsupportedOperationException

如果我在一个恰好是java.sql.Date的变量上使用java.util.DatetoInstant(),我得到一个UnsupportedOperationException

1
2
3
4
5
6
try {
    java.util.Date input = new java.sql.Date(System.currentTimeMillis());
    LocalDate date = input.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
} catch (UnsupportedOperationException e) {
    // grrr!
}

我关心的java.util.Date来自mysql DB中的DATE字段,通过遗留API,实际上是java.sql.Date

现在以下相关问题都非常有趣:

UnsupportedOperationException - 为什么不能在java.sql.Date上调用onInstant()?

将java.util.Date转换为java.time.LocalDate

LocalDate到java.util.Date,反之亦然最简单的转换?

但它们没有提供任何优雅的方法来截断java.util.Date来摆脱时间组件并获得Java 8 LocalDate

我承认存在一个问题,即一个时区中的同一时刻可能与另一个时区的同一时刻不同。

我怀疑解决方案将涉及java.util.Calendar,而不是制定我自己的解决方案,我宁愿建立别人先做的事情。

我宁愿找到比这更短的东西:

from Resetting the time part of a timestamp in Java :

1
2
3
4
5
6
7
8
Date date = new Date();                      // timestamp now
Calendar cal = Calendar.getInstance();       // get calendar instance
cal.setTime(date);                           // set cal to date
cal.set(Calendar.HOUR_OF_DAY, 0);            // set hour to midnight
cal.set(Calendar.MINUTE, 0);                 // set minute in hour
cal.set(Calendar.SECOND, 0);                 // set second in minute
cal.set(Calendar.MILLISECOND, 0);            // set millis in second
Date zeroedDate = cal.getTime();             // actually computes the new Date


通常最简单的解决方案是最难找到的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public LocalDate convertDateObject(java.util.Date suspectDate) {

    try {
        // Don't do this if there is the smallest chance
        // it could be a java.sql.Date!
        return suspectDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

    } catch (UnsupportedOperationException e) {
        // BOOM!!
    }

    // do this first:
    java.util.Date safeDate = new Date(suspectDate.getTime());

    return safeDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

}

如果input变量已知为java.sql.Date,那么您可以简单地转换它并调用toLocalDate()方法:

1
LocalDate date = ((java.sql.Date) input).toLocalDate();

不幸的是,你不能在java.sql.Date上调用toInstant(),因为根据javadoc,它总是抛出UnsupportedOperationException

如果你不知道类型(它可以是java.util.Datejava.sql.Date),你可以使用getTime()方法返回的值来构建Instant,然后将其转换为时区(下面)我正在使用JVM的默认值)并最终从中获取本地日期:

1
2
3
4
5
6
7
LocalDate date = Instant
    // get the millis value to build the Instant
    .ofEpochMilli(input.getTime())
    // convert to JVM default timezone
    .atZone(ZoneId.systemDefault())
    // convert to LocalDate
    .toLocalDate();

toLocalDate()方法获取日期部分(日/月/年),忽略其余部分,因此无需截断它:如果时间是午夜,上午10点或当天的任何其他时间都无关紧要,toLocalDate()将忽略它并获得日期部分。

但是,如果您确实要将时间设置为午夜,则可以使用with方法并将LocalTime传递给它:

1
2
3
4
5
6
7
8
9
LocalDate date = Instant
    // get the millis value to build the Instant
    .ofEpochMilli(input.getTime())
    // convert to JVM default timezone
    .atZone(ZoneId.systemDefault())
    // set time to midnight
    .with(LocalTime.MIDNIGHT)
    // convert to LocalDate
    .toLocalDate();

但正如我所说,toLocalDate()方法将忽略时间部分,因此在这种情况下不需要设置时间(LocalDate将是相同的)。

您还可以检查日期的类型并相应地选择相应的操作,如下所示:

1
2
3
4
5
if (input instanceof java.sql.Date) {
    date = ((java.sql.Date) input).toLocalDate();
} else {
    date = input.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}

您可以根据需要使用任何其他时区,而不是使用JVM默认时区(ZoneId.systemDefault()),方法是调用ZoneId.of("zoneName"),其中区域名称是任何有效的IANA时区名称(始终格式为,如America/New_YorkEurope/London)。
避免使用3个字母的缩写(如CETPST),因为它们不明确且不标准。

您可以通过调用ZoneId.getAvailableZoneIds()获取可用时区列表(并选择最适合您系统的时区)。如果需要,您还可以继续使用JVM默认时区,但提醒它可以在不事先通知的情况下进行更改,即使在运行时也是如此,因此最好始终明确指出您正在使用的时区。


TL;博士

1
myResultSet.getObject( … , LocalDate.class )

java.time

使用遗留日期时间类(例如java.sql.Datejava.util.DateCalendar)时,您的问题就开始了。完全避免这些课程。它们现在已经遗留下来,取而代之的是java.time类。

您说该值在DATE类型的MySQL列中以存储值开头。该类型仅限日期,没有时间。因此,您通过使用错误的类不必要地引入了时间值。

使用符合JDBC 4.2或更高版本的JDBC驱动程序,使用java.time类与数据库交换值。

1
LocalDate ld = myResultSet.getObject( … , LocalDate.class ) ;

然后转到PreparedStatement

1
myPstmt.setObject( … , myLocalDate ) ;


试试这个。

1
2
3
java.util.Date input = new java.sql.Date(System.currentTimeMillis());
Instant instant = Instant.ofEpochMilli(input.getTime());
LocalDate date = instant .atZone(ZoneId.systemDefault()).toLocalDate();

TL;博士

你在java.sql.Datejava.util.Date之间交叉的前提是不正确的。

If I use java.util.Date … on a variable which happens to be a java.sql.Date

您违反了课程文件中规定的合同。你被告知要忽略java.sql.Datejava.util.Date的子类这一事实。

但解决方案可能非常简单。

1
ZonedDateTime zdt = myJavaSqlDate.toLocalDate().atStartOfDay( ZoneId.of("America/Montreal" ) ) ;

细节

显然你手头有java.sql.Date。该类表示没有时间且没有时区的仅日期值。至少那是那个班级的意图。从java.util.Date继承了一个悲惨的设计选择,尽管名称,它代表UTC的日期和时间。这种继承关系是一个黑客,一个糟糕的黑客,他们将内部时间设置为"00:00:00"。文档清楚地指示我们忽略这种继承关系,并忽略了拥有一个时间组件的事实,并假装两个Date类是无关的。但是许多人如作者并没有仔细阅读该文档,并根据类名进行了大量假设。

两个Date类都是可怕混乱的一部分,这是最早版本的Java附带的日期时间框架。这些已被Java 8及更高版本中内置的java.time类所取代。

遇到遗留日期时间对象时的第一步:使用添加到旧类的新方法转换为java.time。

将我们的假日期对象java.sql.Date转换为类型为java.util.LocalDate的真正的仅日期对象。"本地"字表示没有时区或偏离UTC。

1
LocalDate ld = myJavaSqlDate.toLocalDate() ;

显然,您希望将该仅限日期的值转换为具有一天中的时间的日期。也许你想要一天的第一时刻。确定第一个时刻需要一个时区。例如,新的一天早于Europe/Paris而不是America/Montreal,而且仍然早于Asia/Kolkata

1
2
ZoneId z = ZoneId.of("Pacific/Auckland" ) ;
ZonedDateTime zdt = ld.atStartOfDay( z ) ;

您是否尝试使用SimpleDateFormat来帮助您?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;

public class Utils {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd" );

    public static void main( String[] args ) {
        LocalDate ld = sqlDateToLocalDate( new java.sql.Date( System.currentTimeMillis() ) );
        System.out.println( ld );
    }

    public static LocalDate sqlDateToLocalDate( java.sql.Date sqlDate ) {

        try {
            Date d = sdf.parse( sdf.format( sqlDate ) );
            return d.toInstant().atZone( ZoneId.systemDefault() ).toLocalDate();
        } catch ( ParseException exc ) {
        }

        return null;

    }

}