В JVM 1.8 отсутствует удобный и простой в использовании класс форматирования класса java.sql.Timestamp с микро и нано секундами. Есть специализированный пакет java.time с достаточно разнообразной функциональностью. Но его использование для преобразования типа java.sql.Timestamp в строку и из строки в тип выглядит как то сложновато. Хотелось иметь простой способ преобразования с функциональностью класса java.text.SimpleDateFormat.
Конечно есть множество советов и вариантов решения этих преобразований в интернет, но все они меня как то не устроили по разным причинам. Повозившись и помучавшись с поиском решения, решил написать свой класс, при этом реализовывать весь разбор строки и форматирование в строку совсем не хотелось. Решил наследовать свой класс от одного из существующих классов, переопределив поведение только интересуемой меня функциональности. Но и здесь меня ждало большое разочарование из-за ограничения зон видимости полей, методов и используемых классов в классах, от которых я пробовал наследоваться. .
Хочешь не хочешь, но надо все же писать полностью свой класс, но писать то все с нуля не хочется. И тут мне в голову пришла идея, раз нельзя наследоваться от класса java.text.SimpleDateFormat, но использовать-то его можно же. Идея реализации простая - форматировать и разбирать милли/микро/нано секунды отдельно. Сказано - сделано.
package ru.funsys.util;
import java.sql.Timestamp;
import java.text.MessageFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Locale;
/**
* Класс форматирования класса Timestamp c милли/микро/нано секундами
*
* @author Валерий Лиховских
*
*/
public class TimestampFormatter {
/**
* строка форматирования даты и времени. Формируется из исходной строки шаблона форматирования
*/
private String format;
/**
* строка форматирования, используемая для парсинга. Формируется из исходной строки шаблона форматирования
*/
private String parse;
/**
* местоположения в строке шаблона форматирования милли/микро/нано секунд. Формируется из исходной строки шаблона форматирования
*/
private int first;
/**
* разрядность после запятой милли/микро/нано секунд, вычисляется по строке шаблона форматирования
*/
private int size;
/**
* Объект локализации
*/
private Locale locale;
/**
* строка шаблона форматирования по умолчанию
*/
public static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX"; //$NON-NLS-1$
/**
* символ шаблона форматирования милли/микро/нано секунд
*/
public static final char CHAR_S = 'S'; //$NON-NLS-1$
/**
* символ экранирование - одиночный штрих (кавычка)
*/
public static final char CHAR_PRIME = '\''; //$NON-NLS-1$
public TimestampFormatter() {
this(DEFAULT_DATETIME_PATTERN, Locale.getDefault());
}
/**
* @param pattern строка шаблона форматирования по умолчанию
*/
public TimestampFormatter(String pattern) {
this(pattern, Locale.getDefault());
}
/**
* @param pattern строка шаблона форматирования по умолчанию
* @param locale объект локализации
*/
public TimestampFormatter(String pattern, Locale locale) {
first = pattern.indexOf(CHAR_S);
if (first != -1) {
String datetime = pattern.substring(0, first);
int last = pattern.lastIndexOf(CHAR_S) + 1;
if (last > 0 && last < pattern.length()) {
format = datetime + "'{0}'" + pattern.substring(last); // {0} - для милли/микро/нано секунд
parse = datetime + pattern.substring(last);
size = last - first;
} else {
format = datetime + "'{0}'"; // {0} - для милли/микро/нано секунд
parse = datetime;
size = pattern.length() - first;
}
} else {
this.format = pattern;
this.parse = pattern;
size = 0;
}
this.locale = locale;
// Исключить смещение позиции наносекунд из разбираемой строке символа '
int pos = pattern.indexOf(CHAR_PRIME); // текущая позиция символа
while (pos != -1) {
first--;
pos++; // искать следующий символ с позиции
if (pos < pattern.length()) pos = pattern.indexOf(CHAR_PRIME, pos);
else pos = -1;
}
// first теперь имеет значения начала позиции милли/микро/нано секунд в получаемой строке для парсинга
}
/**
* Форматировать Timestamp
*
* @param timestamp значение форматирования
*
* @return строковое представление Timestamp
*/
public String format(Timestamp timestamp) {
String nanos;
if (size == 0) nanos = "";
else nanos = Integer.toString(timestamp.getNanos() + 1000000000).substring(1, 1 + size);
return MessageFormat.format(new SimpleDateFormat(format, locale).format(timestamp), nanos);
}
/**
* Парсить строковое представление Timestamp
*
* @param source строка парсинга
*
* @return результат преобразования в Timestamp
*
* @throws ParseException ошибка форматирования
*/
public Timestamp parse(String source) throws ParseException {
StringBuffer tmpSource = new StringBuffer().append(source.substring(0, first));
if (size > 0) {
tmpSource.append(source.substring(first + size));
}
Timestamp timestamp = new Timestamp(new SimpleDateFormat(parse, locale).parse(tmpSource.toString()).getTime());
if (size > 0) {
int tmpNamo = Integer.parseInt(source.substring(first, first + size));
for (int index = size; index < 9; index++) {
tmpNamo = tmpNamo * 10;
}
timestamp.setNanos(tmpNamo);
}
return timestamp;
}
}
В результате получился компактный класс по своей функциональности сопоставимый с классом java.text.SimpleDateFormat. И самое главное, простой в использовании.
public static void main(String[] args) {
Timestamp t = new Timestamp(System.currentTimeMillis());
t.setNanos(345012000); // устанавливать наносекунды
TimestampFormatter f = new TimestampFormatter();
String s = f.format(t);
System.out.println(s);
try {
// обратное преобразование для проверки
Timestamp n = f.parse(s);
System.out.println(f.format(n));
} catch (Exception e) {
e.printStackTrace();
}
}
Результат
2022-11-18T09:20:35.345012+03:00
2022-11-18T09:20:35.345012+03:00
Дальнейшее использование этого класса, думаю, затруднений не вызовет.
Комментарии (10)
mrfloony
18.11.2022 11:35А Вы сравнивали производительность вашего решения и форматирования посредством "современных" вариантов (например, через Instant или LocalDateTime)?
Интересно было бы увидеть такое сравнение в статье, ибо может и не быть смысла в разработке к устаревшим SimpleDateFormatvl65 Автор
21.11.2022 07:45Дополню. Лучший способ повышения производительности - исключить лишние преобразования. При преобразовании Timestamp это легко можно сделать без потери качества данных. Передавать Timestamp как число.
foal
19.11.2022 15:12А в чём улучшение по сравнению со стандартным кодом?
public static void main(String[] args) { Timestamp t = new Timestamp(System.currentTimeMillis()); // устанавливать наносекунды t.setNanos(345012000); DateTimeFormatter f = DateTimeFormatter .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX") .withZone(ZoneId.systemDefault()); String s = f.format(t.toInstant()); System.out.println(s); try { // обратное преобразование для проверки Timestamp n = Timestamp.from(f.parse(s, Instant::from)); System.out.println(f.format(n.toInstant())); } catch (Exception e) { e.printStackTrace(); } }
Если очень хочется, то спрячьте инициализацию
DateTimeFormatter
,t.toInstant()
иTimestamp.from(f.parse(s, Instant::from))
внутрь своего TimestampFormatter, напримерpublic Timestamp parse(String source) { return Timestamp.from(f.parse(source, Instant::from)); }
public String format(Timestamp timestamp) { return f.format(t.toInstant()); }
public TimestampFormatter(String pattern, Locale locale, ZoneId tz) { f = DateTimeFormatter .ofPattern(pattern) .withLocale(locale) .withZone(tz); }
Плюс еще, конечно, проверка на
null
, но, для упрощения, я постарался придерживаться авторской нотации.grossws
20.11.2022 05:00Бонусом то что DateTimeFormatter -- thread safe и может спокойно лежать в final поле. SDF нужно оборачивать в ThreadLocal со всеми развлечениями в случае ротации тредов в пуле в управляемой среде.
vl65 Автор
21.11.2022 08:10Отвечу Вам той же монетой.
Ваше решение лишь только подтверждает мое утверждение, что и в пакете java.time нет удобного способа преобразования микро/нано секунд для Timestamp.
Молодец, предложили альтернативное решение. Коллеги могут выбирать, чем пользоваться.
И вишенка на торте - NullPointerException. У меня к Вам три вопроса.
Очень любопытно, а что Вы собрались или предполагаете делать с NullPointerException в моем коде, когда пишите "проверка на null"? Могу предположить такой вариант
if (source == null) return null;
Угадал?
Почему у Вас возник вопрос только про NullPointerException? ParseException ведь тоже не обрабатывается в предложенном мной варианте решения, если, как Вы пишите, придерживаться вашей "авторской нотации". В чем в данном случае разница между этими два исключениями?
А если все же немножко подумать? А если подумать, то NullPointerException в этом участке кода никогда не возникнет и любая проверка на null будет мертвым кодом. Спросите почему? А это я Вам оставлю в качестве домашнего задания, Вы же гуру, думаю, справитесь. Так как, справитесь?
vl65 Автор
21.11.2022 08:18Мой комментарий на Вашу реплику ниже, не обновил страницу при ответе и он из за этого похоже попал не туда
vl65 Автор
Нет, не сравнивал. Моих же собственных преобразований практически нет. Все основные преобразования делаются классом SimpleDateFormat. Устаревший или не устаревший этот класс ? Это определяется целью его использования. На мой взгляд, пользоваться им очень удобно при использовании JDBC.