Вітання! Сьогодні ми почнемо працювати з новим типом даних, з яким раніше не стикалися, а саме з датами. Як не загубитися в часі - DateTime і Calendar - 1Що таке дата, думаю, пояснювати не потрібно :) У принципі, записати поточну дату й час у Java цілком можливо у звичайний рядок.

public class Main {
   public static void main(String[] args) {

       String date = "11 червня 2018 року";
       System.out.println(date);
   }
}
Але такий підхід має багато недоліків. Клас String створено для роботи з текстом, і методи в нього відповідні. Якщо нам потрібно буде якось керувати датою (додати до неї 2 години, наприклад), String тут не впорається. Або, наприклад, вивести в консоль поточну дату й час на момент запуску програми. Тут String теж не допоможе: поки ти напишеш код і запустиш його, час зміниться і в консоль буде виведено неактуальний час. Тому творцями Java було передбачено кілька класів для роботи з датами й часом. Перший із них – це клас java.util.Date.

Клас Date Java

Ми вказали для нього повну назву, оскільки в іншому пакеті Java є ще клас java.sql.Date. Не переплутай! Перше, що потрібно про нього знати: він зберігає дату у вигляді кількості мілісекунд, які пройшли з 1 січня 1970 року. Для цієї дати є навіть окрема назва – "Unix-час". Досить цікавий спосіб, згоден? :) Друге, що варто запам'ятати: якщо створити об'єкт Date з порожнім конструктором, результатом будуть поточні дата й час на момент створення об'єкта. Пам'ятаєш, ми писали, що для дати у форматі String таке завдання буде проблематичним? Клас Date легко його вирішує.

public class Main {
   public static void main(String[] args) {

       Date date = new Date();
       System.out.println(date);
   }
}
Запусти цей код кілька разів, і побачиш, як час щоразу змінюватиметься :) Це можливо саме завдяки зберіганню часу в мілісекундах: вони є найменшою одиницею вимірювання часу, тому результати настільки точні. Існує й інший конструктор для Date: можна вказати точну кількість мілісекунд, яка пройшла з 00:00 1 січня 1970 року до потрібної дати, і вона буде створена:

public class Main {
   public static void main(String[] args) {

       Date date = new Date(1212121212121L);
       System.out.println(date);
   }
}
Вивід в консоль:

Fri May 30 08:20:12 MSD 2008
У нас вийшло 30 травня 2008 року. "Fri" означає день тижня – "Friday" (п'ятниця), а MSD – "Moscow Daylight Saving" (московський літній час). Мілісекунди передаються у форматі long, оскільки їх кількість найчастіше не вміщується в int. Отже, які операції з датами нам можуть знадобитися в роботі? Ну, найочевидніше, звісно, – порівняння. Визначити, чи була одна дата пізніше чи раніше за іншу. Це можна зробити по-різному. Наприклад, можна викликати метод Date.getTime(). Він поверне кількість мілісекунд, що пройшли з півночі 1 січня 1970 року. Просто викличемо його для двох об'єктів Date і порівняємо між собою:

public class Main {
   public static void main(String[] args) {

       Date date1 = new Date();

       Date date2 = new Date();

       System.out.println((date1.getTime() > date2.getTime())?
               "date1 пізніше date2" : "date1 раніше date2");
   }
}
Вивід:
date1 раньше date2
Але є й зручніший спосіб, а саме – використовувати спеціальні методи класу Date: before(), after() і equals(). Усі вони повертають результат у форматі boolean. Метод before() перевіряє, чи була наша дата раніше за ту, яку ми передаємо як аргумент:

public class Main {
   public static void main(String[] args) throws InterruptedException {

       Date date1 = new Date();

       Thread.sleep(2000);//припинимо роботу програми на 2 секунди
       Date date2 = new Date();

       System.out.println(date1.before(date2));
   }
}
Вивід в консоль:
true
Схожим чином працює і метод after(), він перевіряє, чи була наша дата пізніше тієї, яку ми передаємо як аргумент:

public class Main {
   public static void main(String[] args) throws InterruptedException {

       Date date1 = new Date();

       Thread.sleep(2000);//припинимо роботу програми на 2 секунди
       Date date2 = new Date();

       System.out.println(date1.after(date2));
   }
}
Вивід в консоль:
false
У прикладах ми "присипляємо" програму на 2 секунди, щоб дві дати гарантовано відрізнялися. На швидких комп'ютерах час між створенням date1 і date2 може бути меншим за одну мілісекунду, і в такому випадку обидва методи – before() і after() – повертатимуть false. А ось метод equals() у такій ситуації поверне true! Адже він порівнює саме кількість мілісекунд, що пройшли з 00:00 1 січня 1970 року, для кожної дати. Об'єкти вважатимуться рівними тільки в тому випадку, якщо ці значення збігаються аж до мілісекунди:

public static void main(String[] args) {

   Date date1 = new Date();
   Date date2 = new Date();

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

   System.out.println(date1.equals(date2));
}
Ось ще на що треба звернути увагу. Якщо ти відкриєш документацію класу Date на сайті Oracle, то побачиш, що багато його методів та конструкторів були позначені словом Deprecated ("нерекомендований"). Ось, подивися: Class Date. Ось що самі творці Java говорять про ті частини класів, які стали deprecated: «Програмний елемент, анотований @Deprecated, є тим, що програмістам не рекомендується використовувати, як правило, тому, що це небезпечно, або тому, що є краща альтернатива.» Це не означає, що цими методами взагалі не можна користуватися. Більше того, якщо ти сам спробуєш запустити код з їх використанням в IDEA, він, найімовірніше, працюватиме. Візьмемо для прикладу deprecated-метод Date.getHours(), який повертає кількість годин з об'єкта Date.

public static void main(String[] args) {

   Date date1 = new Date();

   System.out.println(date1.getHours());

}
Якщо на момент запуску коду у вас, наприклад, час 14:21, він виведе число 14. Як бачите, deprecated-метод закреслено, але він цілком працює. Ці методи не стали прибирати зовсім, щоб не зламати велику кількість уже написаного коду, який їх використовує. Тобто ці методи не "зламані" і не "видалені", просто їх не рекомендують використовувати через наявність більш зручної альтернативи. Про неї, до речі, написано прямо в документації: Як не загубитися в часі - DateTime та Calendar - 2Більшість методів класу Date було перенесено до його покращеної, розширеної версії – класу Calendar. З ним ми й познайомимося далі :)

Java Calendar

У версії Java 1.1 з'явився новий клас Calendar. Він зробив роботу з датами в Java дещо простішою, ніж вона була раніше. Єдиною реалізацією класу Calendar, з якою ми і працюватимемо, є клас GregorianCalendar (він реалізує григоріанський календар, за яким живе більшість країн світу). Його основна зручність полягає в тому, що він уміє працювати з датами у зручнішому форматі. Наприклад, він може:
  • додати до поточної дати місяць або день;
  • перевірити, чи є рік високосним;
  • отримати окремі компоненти дати (наприклад, отримати з цілої дати номер місяця);
  • а також усередині нього реалізована дуже зручна система констант (багато з них ми побачимо нижче).
Ще однією важливою відмінністю класу Calendar є те, що в ньому реалізована константа Calendar.ERA: ти можеш встановити для дати еру BC ("Before Christ" – до Різдва Христового, тобто "до нашої ери") або AD ("Anno Domini" – "наша ера"). Давайте розглянемо все це на прикладах. Створимо календар із датою 25 січня 2017 року:

public static void main(String[] args) {

  Calendar calendar = new GregorianCalendar(2017, 0 , 25);
}
Місяці в класі Calendar (як і в Date, до речі) починаються з нуля, тому ми передали число 0 як другий аргумент. Головне під час роботи з класом Calendar – розуміти, що це календар, а не окрема дата. Як не загубитися в часі - DateTime та Calendar - 3Дата – це кілька чисел, що позначають конкретний момент часу. А календар – це цілий механізм, за допомогою якого можна багато чого робити з датами :) Це досить добре видно, якщо спробувати вивести об'єкт Calendar у консоль. Вивід:

java.util.GregorianCalendar[time=?,areFieldsSet=false,areAllFieldsSet=false,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Europe/Moscow",offset=10800000,dstSavings=0,useDaylight=false,transitions=79,lastRule=null],firstDayOfWeek=2,minimalDaysInFirstWeek=1,ERA=?,YEAR=2017,MONTH=0,WEEK_OF_YEAR=?,WEEK_OF_MONTH=?,DAY_OF_MONTH=25,DAY_OF_YEAR=?,DAY_OF_WEEK=?,DAY_OF_WEEK_IN_MONTH=?,AM_PM=0,HOUR=0,HOUR_OF_DAY=0,MINUTE=0,SECOND=0,MILLISECOND=?,ZONE_OFFSET=?,DST_OFFSET=?]
Бачиш, скільки інформації! У календаря є купа властивостей, яких немає у звичайної дати, і всі вони виводяться в консоль (так працює метод toString() у класі Calendar). Якщо під час роботи тобі потрібно отримати з календаря просту дату, тобто об'єкт Date, це робиться за допомогою методу Calendar.getTime() (назва не зовсім логічна, але тут уже нічого не поробиш):

public static void main(String[] args) {

   Calendar calendar = new GregorianCalendar(2017, 0 , 25);
   Date date = calendar.getTime();
   System.out.println(date);
}
Вивід:
Wed Jan 25 00:00:00 MSK 2017
Ось тепер ми спростили календар до звичайної дати. Поїхали далі. Крім цифрових позначень місяців, у класі Calendar можна використовувати константи. Константи – це статичні поля класу Calendar зі встановленим значенням, яке не можна змінити. Цей варіант насправді навіть кращий, оскільки таке написання покращує читабельність коду.

public static void main(String[] args) {
   GregorianCalendar calendar = new GregorianCalendar(2017, Calendar.JANUARY , 25);
}
Calendar.JANUARY – одна з констант для позначення місяця. За такого варіанта запису ніхто не забуде, наприклад, що цифра "3" означає квітень, а не звичний для нас третій місяць – березень. Просто пишеш Calendar.APRIL – і все :) Усі поля календаря (число, місяць, хвилини, секунди тощо) можна встановлювати окремо за допомогою методу set(). Він дуже зручний, оскільки в класі Calendar для кожного поля виділено свою константу, і підсумковий код матиме максимально простий вигляд. Наприклад, у минулому прикладі ми створили дату, але не встановили для неї поточний час. Давай встановимо час 19:42:12.

public static void main(String[] args) {
   Calendar calendar = new GregorianCalendar();
   calendar.set(Calendar.YEAR, 2017);
   calendar.set(Calendar.MONTH, 0);
   calendar.set(Calendar.DAY_OF_MONTH, 25);
   calendar.set(Calendar.HOUR_OF_DAY, 19);
   calendar.set(Calendar.MINUTE, 42);
   calendar.set(Calendar.SECOND, 12);

   System.out.println(calendar.getTime());
}
Вивід:
Wed Jan 25 19:42:12 MSK 2017
Ми викликаємо метод set(), передаємо до нього константу (залежно від того, яке поле хочемо змінити) та нове значення для цього поля. Виходить, що метод set() – такий собі "суперсетер", який вміє встановлювати значення не для одного поля, а для безлічі полів :) Додавання та віднімання значень у класі Calendar здійснюється за допомогою методу add(). У нього необхідно передати те поле, яке ти хочеш змінити, і число – скільки саме ти хочеш додати або відняти від поточного значення. Наприклад, повернемо дату, яку ми створили, на 2 місяці назад:

public static void main(String[] args) {
   Calendar calendar = new GregorianCalendar(2017, Calendar.JANUARY , 25);
   calendar.set(Calendar.HOUR, 19);
   calendar.set(Calendar.MINUTE, 42);
   calendar.set(Calendar.SECOND, 12);

   calendar.add(Calendar.MONTH, -2);//щоб відібрати значення - метод потрібно передати негативне число
   System.out.println(calendar.getTime());
}
Вивід:
Fri Nov 25 19:42:12 MSK 2016
Чудово! Ми повернули дату на 2 місяці назад. У результаті змінився не лише місяць, а й рік – з 2017 на 2016. Підрахунок поточного року під час перенесення дат, звісно, виконується автоматично, і його не потрібно контролювати вручну. Але якщо для якихось цілей тобі потрібно вимкнути цю поведінку, це теж можливо. Спеціальний метод roll() може додавати й віднімати значення, не змінюючи при цьому інші значення. Наприклад, ось так:

public static void main(String[] args) {
   Calendar calendar = new GregorianCalendar(2017, Calendar.JANUARY , 25);
   calendar.set(Calendar.HOUR, 10);
   calendar.set(Calendar.MINUTE, 42);
   calendar.set(Calendar.SECOND, 12);

   calendar.roll(Calendar.MONTH, -2);
   System.out.println(calendar.getTime());
}
Ми зробили те саме, що й у попередньому прикладі, – відняли 2 місяці від поточної дати. Але тепер код спрацював по-іншому: місяць змінився із січня на листопад, але рік як був 2017-м, так і залишився! Вивід:
Sat Nov 25 10:42:12 MSK 2017
Далі. Як ми й казали вище, усі поля об'єкта Calendar можна отримати окремо. За це відповідає метод get():

public static void main(String[] args) {
   GregorianCalendar calendar = new GregorianCalendar(2017, Calendar.JANUARY , 25);
   calendar.set(Calendar.HOUR, 10);
   calendar.set(Calendar.MINUTE, 42);
   calendar.set(Calendar.SECOND, 12);

   System.out.println("Рік:" + calendar.get(Calendar.YEAR));
   System.out.println("Місяць:" + calendar.get(Calendar.MONTH));
   System.out.println("Порядковий номер тижня на місяці:" + calendar.get(Calendar.WEEK_OF_MONTH));//порядковий номер тижня на місяці

   System.out.println("Кількість: " + calendar.get(Calendar.DAY_OF_MONTH));

   System.out.println("Годинник:" + calendar.get(Calendar.HOUR));
   System.out.println("Хвабони:" + calendar.get(Calendar.MINUTE));
   System.out.println("Секунди:" + calendar.get(Calendar.SECOND));
   System.out.println(Мілісекунди: + calendar.get(Calendar.MILLISECOND));

}
Вивід:

Год: 2017 
Месяц: 0 
Порядковый номер недели в месяце: 4 
Число: 25 
Часы: 10 
Минуты: 42 
Секунды: 12 
Миллисекунды: 0
Тобто, крім "суперсетера", у класі Calendar є ще й "супергетер". Ще один цікавий момент – це, звичайно, робота з ерами. Для створення дати "до нашої ери" потрібно використовувати поле Calendar.ERA. Наприклад, створимо дату, що позначає битву при Каннах, у якій Ганнібал переміг військо Риму. Це сталося 2 серпня 216 р. до н. е.:

public static void main(String[] args) {
   GregorianCalendar cannes = new GregorianCalendar(216, Calendar.AUGUST, 2);
   cannes.set(Calendar.ERA, GregorianCalendar.BC);

   DateFormat df = new SimpleDateFormat("dd MMM yyy GG");
   System.out.println(df.format(cannes.getTime()));
}
Тут ми використовували клас SimpleDateFormat, щоб вивести дату у зрозумілішому для нас форматі (літери "GG" відповідають саме за вивід ери). Вивід:
02 сер 216 до н.е.
У класі Calendarє ще багато методів та констант, почитай про них у документації:

Перетворення рядка на Date

Для перетворення String на Date можна скористатися допоміжним класом Java – SimpleDateFormat. Це клас, який потрібен для приведення дати до визначеного формату.Як не загубитися в часі - DateTime та Calendar - 4У свою чергу, він дуже схожий на DateFormat. Єдина помітна різниця між ними полягає в тому, що SimpleDateFormat можна використовувати для форматування (перетворення дати на рядок) та для парсингу рядка в дату з підтримкою мовного стандарту, тоді як DateFormat не підтримує мовний стандарт. Крім того, DateFormat – це абстрактний клас, який забезпечує базову підтримку для форматування та аналізу дат, а SimpleDateFormat – це конкретний клас, який розширює клас DateFormat. Ось так виглядає приклад створення об'єкта SimpleDateFormat та форматування Date:

SimpleDateFormat formater = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date(1212121212121L);

System.out.println(formatter.format(date));
У наведеному вище прикладі ми використовували шаблон "yyyy-MM-dd HH:mm:ss", який означає:
  • 4 цифри для року (yyyy);
  • 2 цифри для місяця (MM);
  • 2 цифри для дня (dd);
  • 2 цифри для годин у 24-годинному форматі (HH);
  • 2 цифри для хвилин (mm);
  • 2 цифри для секунд (ss).
Розділові знаки та порядок розташування символів шаблону зберігаються. Вивід у консоль:
2008-05-30 08:20:12
Шаблонних букв для класу SimpleDateFormat досить багато. Щоб ти не заплутався, ми зібрали їх у таблицю:
Символ Опис приклад
G ера (в англійській локалізації - AD і BC) н.е.
y рік (4-х значне число) 2020
yy рік (останні 2 цифри) 20
yyyy рік (4-х значне число) 2020
M номер місяця (без лідируючих нулів) 8
MM номер місяця (з лідируючими нулями, якщо порядковий номер місяця < 10) 04
MMM трилітерне скорочення місяця (відповідно до локалізації) січ
MMMM повна назва місяця Червень
w тиждень у році (без лідируючих нулів) 4
ww тиждень у році (з лідируючими нулями) 04
W тиждень у місяці (без лідируючих нулів) 3
WW тиждень у місяці (з лідируючим нулем) 03
D день у році 67
d день місяця (без лідируючих нулів) 9
dd день місяця (з лідируючими нулями) 09
F день тижня на місяці (без лідируючих нулів) 9
FF день тижня на місяці (з лідируючими нулями) 09
E день тижня (скорочення) Вт
EEEE день тижня (повністю) п'ятниця
u номер дня тижня (без лідируючих нулів) 5
uu номер дня тижня (з лідируючими нулями) 05
a маркер AM/PM AM
H годинник у 24-годинному форматі без лідируючих нулів 6
HH годинник у 24-годинному форматі з лідируючим нулем 06
k кількість годин у 24-годинному форматі 18
K кількість годин у 12-годинному форматі 6
h час у 12-годинному форматі без лідируючих нулів 6
hh час у 12-годинному форматі з лідируючим нулем 06
m хвабони без лідируючих нулів 32
mm хвабони з лідируючим нулем 32
s секунди без лідируючих нулів 11
ss секунди з лідируючим нулем 11
S мілісекунди 297
z часовий пояс EET
Z часовий пояс у форматі RFC 822 300
Приклади комбінацій символів шаблонів:
Шаблон приклад
dd-MM-yyyy 01-11-2020
yyyy-MM-dd 2019-10-01
HH:mm:ss.SSS 23:59.59.999
yyyy-MM-dd HH:mm:ss 2018-11-30 03:09:02
yyyy-MM-dd HH:mm:ss.SSS 2016-03-01 01:20:47.999
yyyy-MM-dd HH:mm:ss.SSS Z 2013-13-13 23:59:59.999 +0100
Якщо трохи помилитися з форматом, то можна отримати java.text.ParseException, а це не надто приємне досягнення. Ну що ж, невеликий екскурс у SimpleDateFormat закінчено – повернімося до перетворення String на Date. SimpleDateFormat дає нам таку можливість, і ми розглянемо цей процес поетапно.
  1. Створюємо рядок, з якого потрібно отримати дату:

    
    String strDate = "Sat, April 4, 2020";
    

  2. Створюємо новий об'єкт SimpleDateFormat із шаблоном, який збігається з тим, що є в нашому рядку (інакше розпарсити його не вийде):

    
    SimpleDateFormat formatter = new SimpleDateFormat("EEE, MMMM d, yyyy", Locale.ENGLISH);
    

    Як бачите, у нас тут з'явився аргумент Locale. Якщо ми його не вкажемо, буде використано значення Locale за замовчуванням, яке завжди є англійською.

    Якщо мовний стандарт не збігається з вхідним рядком, то рядкові дані, прив'язані до мови, як-от Mon або April, не будуть розпізнані й спричинять виникнення java.text.ParseException, навіть якщо шаблон підходить.

    Проте можна не вказувати Locale, якщо ми використовуємо шаблон, який не прив'язаний до мови. Наприклад: yyyy-MM-dd HH:mm:ss.

  3. Створюємо дату за допомогою формату, який, своєю чергою, парсить її з вхідного рядка:

    
    try {
      Date date = formatter.parse(strDate);
      System.out.println(date);
    }
    catch (ParseException e) {
      e.printStackTrace();
    }
    

    Вивід в консоль:

    Sat Apr 04 00:00:00 EEST 2020

    Хммм... Але формат уже не той!

    Щоб отримати той самий формат, знову використовуємо форматтер:

    
    System.out.println(formatter.format(date));
    

    Вивід в консоль:

    Sat, April 4, 2020

SimpleDateFormat та Calendar

SimpleDateFormat дозволяє форматувати всі створені об'єкти Date та Calendar для подальшого використання. Розглянемо такий цікавий момент, як робота з ерами. Для створення дати "до нашої ери" потрібно використовувати поле Calendar.ERA. Наприклад, створимо дату, що означає битву при Каннах, у якій Ганнібал переміг військо Риму. Це сталося 2 серпня 216 р. до н. е.:

public static void main(String[] args) {
   GregorianCalendar cannes = new GregorianCalendar(216, Calendar.AUGUST, 2);
   cannes.set(Calendar.ERA, GregorianCalendar.BC);

   DateFormat df = new SimpleDateFormat("dd MMM yyy GG");
   System.out.println(df.format(cannes.getTime()));
}
Тут ми використовували клас SimpleDateFormat, щоб вивести дату у зрозумілішому для нас форматі (як зазначено вище, літери "GG" відповідають саме за вивід ери).Вивід:
02 сер 216 до н.е.

Java Date Format

А ось ще один випадок. Припустімо, що цей формат дати нас не влаштовує:
Sat Nov 25 10:42:12 MSK 2017
Так ось. За допомогою можливостей java.text.SimpleDateFormat ми можемо без особливих складнощів змінити його на власний формат:

public static void main(String[] args) {

   SimpleDateFormat dateFormat = new SimpleDateFormat("EEEE, d MMMM yyyy");
   Calendar calendar = new GregorianCalendar(2017, Calendar.JANUARY , 25);
   calendar.set(Calendar.HOUR, 10);
   calendar.set(Calendar.MINUTE, 42);
   calendar.set(Calendar.SECOND, 12);

   calendar.roll(Calendar.MONTH, -2);
   System.out.println(dateFormat.format(calendar.getTime()));
}
Вивід:
субота, 25 Листопада 2017
Набагато краще, так? :)