Готовя тесты к лекции про пакет java.time столкнулся с проблемой, несовместимости работы JDK на разных платформах. Вообще с Java работаю с 2007 г, и с аналогичными проблемами не сталкивался. Возможно просто везло.
Итак, имеется в наличии 3 компьютера
- Ноутбук HP с операционной системой Windows 10
- Ноутбук Apple с операционной системой MacOs 10
- Виртуальный сервер в облаке с операционной системой CentOs 7
На всех установлена свежая версия JDK 13.0.2. И вот этот, простой пример, на всех 3-х системах работает по разному:
System.out.println(Instant.now());
На Windows : 2020-01-31T04:36:29.797244400Z
На CentOs : 2020-01-31T04:36:29.797244Z
На MacOs : 2020-01-31T04:36:29.797Z
Казалось бы не большая проблема, но если мы сериализуем дату в строку, и не важно каким способом она у нас передается с одного устройства на другое, то простой код, который парсит эту строку по фиксированному формату начинает выдавать ошибку, если количество символов после запятой в секундах не точно соответствует формату:
String strInstatnt = "2020-01-31T04:36:29.797Z"; DateTimeFormatter formatter = DateTimeFormatter. ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'"). withZone(ZoneOffset.UTC); Instant instant = Instant.from(formatter.parse(strInstatnt));
Exception in thread "main" java.time.format.DateTimeParseException: Text '2020-01-31T04:36:29.797Z' could not be parsed at index 20
Укорачивание формата до нужного количества долей секунды решает проблему:
String strInstatnt = "2020-01-31T04:36:29.797Z"; DateTimeFormatter formatter = DateTimeFormatter. ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"). withZone(ZoneOffset.UTC); Instant instant = Instant.from(formatter.parse(strInstatnt));
Этот код, несомненно, сработает, но как быть, если к нам приходит разное количество долей секунд? Казалось, эту проблему может решить optional section [] описанная в документации. Но, увы, она работает только на все секцию целиком, а не на часть, и вариант формата «yyyy-MM-dd’T’HH:mm:ss.SSS[SSSSSS]’Z'» не будет работать для 9 символов в долях секунды.
Что же делать? Поиск по уважаемому StackOverflow дает не очень интересные решения — парсить 3 раза, с форматами 3, 6 и 9 символов, обрабатывая DateTimeParseException, если таковое возникает. Не очень эффективная стратегия. Если уж идти этим путем, я бы лучше просто взял strInstatnt.length(), из него вычислил количество знаков в дробной части секунд
String strInstatnt = "2020-01-31T04:36:29.797Z"; String fractional = "S".repeat(strInstatnt.length()-21); DateTimeFormatter formatter = DateTimeFormatter. ofPattern("yyyy-MM-dd'T'HH:mm:ss."+fractional+"'Z'"). withZone(ZoneOffset.UTC); Instant instant = Instant.from(formatter.parse(strInstatnt));
Такой код будет работать во всех случаях безошибочно.
Но, что самое интересное — самый простой вариант тоже работает во всех 3-х случая без проблем! Это использование метода parse у Instant
String strInstatnt = "2020-01-31T04:36:29.79Z"; System.out.println(Instant.parse(strInstatnt));
И работает это при любом количестве цифр в дробной части секунд.
Мораль — либо пользуйтесь везде одним и тем же форматом, с фиксированным количеством знаков после точки, либо гибким парсингом, позволяющим указывать произвольную точность долей секунды.
Но на этом несовместимости не заканчиваются, поехали дальше. Рассмотрим такой код:
Locale loc = new Locale("en", "US"); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss.SSS Z zzzz", loc); ZonedDateTime zoned = Instant.now().atZone(ZoneId.of("Europe/Paris")); System.out.println(formatter.format(zoned));
Вся разница заключается в именах часовых поясов в различных locale. Но тут разница не в операционных системах, а в версиях JDK.
Locale loc = new Locale("en", "US"):
JDK 13 : 31.01.2020 07:07:04.209 +0100 Central European Standard Time
JDK 8 : 31.01.2020 07:07:04.209 +0100 Central European Time
Locale loc = new Locale("fr", "FR"):
JDK 13 : 31.01.2020 07:08:43.660 +0100 heure normale d’Europe centrale
JDK 8 : 31.01.2020 07:08:43.660 +0100 Heure d’Europe centrale
Locale loc = new Locale("ru", "RU"):
JDK 13 : 31.01.2020 07:12:36.128 +0100 Центральная Европа, стандартное время
JDK 8 : 31.01.2020 07:12:36.128 +0100 Central European Time
И при парсинге такой строки, полученной из системы с другим JDK законно возникнет DateTimeParseException.
Конечно, кто-то может сказать, что JDK 8 сильно устаревший, и я даже готов с этим согласиться, правда за небольшим исключением. JDK 8 это последний JDK, который поддерживает 32-х битные системы. И у некоторых наших студентов такие старые компьютеры периодически встречаются. Т.е. в реальной жизни это до сих пор есть. И в этом случае, при передаче информации из одной системы в другую могут возникнуть проблемы.
Но, предупрежден, значит вооружен! Зная эти особенности можно построить свой код так,чтобы избежать потенциальных проблем. В данном случае лучше не использовать полное имя часового пояса «zzzz» в формате, а использовать краткое («z»), или вообще имя ZoneId «VV». А использование только часового смещения, например +0100 потеряет имя зоны, так как бывает несколько зон с одинаковым смещением. Это, конечно, не сместит дату-время, но сделает вывод этой даты менее удобным для чтения. А в нашем деле нет мелочей!