자바 Date와 Calendar가 가지고 있던 단점들을 해소하기 위해 JDK1.8부터 'java.time 패키지'가 추가되었다.
이 패키지는 아래와 같이 4개의 하위 패키지를 가지고 있다.
java.time | 날짜와 시간을 다루는데 필요한 핵심 클래스들을 제공
java.time.chrono | 표준(ISO)이 아닌 달력 시스템을 위한 클래스들을 제공
java.time.format | 날짜와 시간을 파싱하고, 형식화하기 위한 클래스들을 제공
java.time.zone | 시간대(time-zone)와 관련된 클래스들을 제공
날짜나 시간을 변경하는 메서들은 기존의 객체를 변경하는 대신 항상 변경된 새로운 객체를 반환한다. 기존 Calendar 클래스는 변경 가능하므로, 멀티 쓰레드 환경에서 안전하지 못 하다.
멀티 쓰레드 환경에서는 동시에 여러 쓰레드가 같은 객체에 접근할 수 있기 때문에, 변경 가능한 객체는 데이터가 잘못될 가능성이 있으며, 이를 쓰레드에 안전하지 않다고 한다. 그러나 기존 Calendar
와 Date
호환성때문에 Calendar
와 Date
도 여전히 사용 될 것으로 보인다.
1. java.time 패키지의 핵심 클래스
날짜와 시간을 하나로 표현하는 Calendar
클래스와 달리, java.time
패키지에서는 날짜와 시간을 별도의 클래스로 분리했다.
시간을 표현할 때는 LocalTime
클래스를 사용하고, 날짜를 표현할 때는 LocalDate
클래스를 사용하면 된다.
그리고, 날짜와 시간이 모두 필요한 경우에는 LocalDateTime
클래스를 사용한다.
만약 시간대(time-zone)까지 추가해야 한다면, ZonedDateTime
클래스를 사용한다.
날짜와 시간을 초단위로 표현한 값을 타임스탬프(time-stamp)라고 부르는데, 이 값은 날짜와 시간을 하나의 정수로 표현할 수 있으므로 날짜와 시간의 차이를 계산하거나 순서를 비교하는데 유리해서 데이터베이스에서 많이 사용 된다.
Period와 Duration
날짜와 시간의 간격을 표현하기 위한 클래스도 있다.
Period
: 두 날짜간의 차이를 표현하기 위한 것 (날짜 - 날짜)Duration
: 시간의 차이를 표현(시간 - 시간)
now()와 of()
java.time
패키지에 속한 클래스의 객체를 생성하는 가장 기본적인 방법은 now()
와 of()
를 사용하는 것이다.
now()는 현재 날짜와 시간을 저장하는 객체를 생성한다.
LocalDate date = LocalDate.now(); //2023-02-02
LocalTime time = LocalTime.now(); //23:58:22.873
LocalDateTime dateTime = LocalDateTime.now(); //2023-02-02T23:58:22.873
ZonedDateTime dateTimeInKr = ZonedDatetime.now(); //23:58:22.873+09:00[Asia/Seoul]
of()는 단순히 필드의 값을 순서대로 지정해 주기만 하면 된다.
LocalDate date = LocalDate.of(2023, 02, 02); //2023년 02월 02일
LocalTime time = LocalTime.of(23, 59, 59); //23시 59분 59초
LocalDateTime dateTime = LocalDateTime.of(date, time);//2023-02-02T23:59:59
ZonedDateTime zDateTime = ZonedDateTime.of(dateTime, ZoneId,of("Asia/Seoul"));//2023-02-02T23:59:59+09:00[Asia/Seoul]
Temporal과 TemporalAmount
LocalDate
, LocalTime
, LocalDateTime
, ZonedDateTime
등 날짜와 시간을 표현하기 위한 클래스들은 모두 Temporal
, TomporalAccessor
, TemporalAdjuster
인터페이스를 구현했고, Duration
과 Period
는 TemporalAmount
인터페이스를 구현했다.
매개변수의 타입이 Temporal
로 시작하는 것들이 자주 등장하는데 대부분 날짜와 시간을 위한 것이므로, TemporalAmount
인지 아닌지 확인하면 된다.
Temporal, TemporalAccessor, TemporalAjuster를 구현한 클래스 - LocalDate, LocalTime, LocalDateTime, ZonedateTime, Instant 등 TemporalAmout를 구현한 클래스 - Period, Duration |
---|
Temporalunit과 TemporalField
날짜와 시간의 단위를 정의해 놓은 것이 TemporalUnit 인터페이스고, 이 인터페이스를 구현한 것이 열거형 ChronoUnit이다.
그리고, TemporalField
는 년, 월, 일 등 날짜와 시간의 필드를 정의해 놓은 것으로 열거형 ChronoField
가 이 인터페이스를 구현하였다.
LocalTime now = LocalTime.now(); //현재시간
int minute = now.getMinute(); //현재 시간에서 분(minute)만 조회
System.out.println(minute + "분");
int minute2 = now.get(ChronoField.MINUTE_OF_HOUR); //현재 시간에서 분(minute)만 조회
System.out.println(minute2 + "분");
//-------------------------------------------------------
//결과
//-------------------------------------------------------
//42분
//42분
날짜와 시간에서 특정 필드의 값만을 얻을 때는 get()이나, get으로 시작하는 이름의 메서드를 이용한다.
아래와 같이 특정 날짜와 시간에서 지정된 단위의 값을 더하거나 뺄 때는 plus() 또는 minus()에 값과 함께 열거형 ChronoUnit을 사용한다.
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
System.out.println("내일 : " + tomorrow);
LocalDate tomorrow2 = today.plusDays(1);
System.out.println("내일 : " + tomorrow2);
//-------------------결과----------------------
//내일 : 2023-02-04
//내일 : 2023-02-04
참고로, get 메서드는 'int get(TemporalField field)' 로 정의되고, plus()는 'LocaDate plus(long amountToAdd, TemporalUnit unit)' 으로 정의된다.
특정 TemporalField
나 TemporalUnit
을 사용할 수 있는지 확인하는 메서드는 다음과 같은데, 이 메서드들은 날짜와 시간을 표현하는데 사용하는 모든 크래스에 포함되어 있다.
boolean isSupported(TemporalUnit unit) //Temporal에 정의
boolean isSupported(TemporalField field) //TemporalAcessor에 정의
2. LocalDate와 LocalTime
LocalDate
와 LocalTime
은 java.time
패키지의 가장 기본이 되는 클래스이다. 나머지 클래스들은 이들의 확장이므로 이 두 클래스만 잘 이해해도 충분하다.
객체를 생성하는 방법은 현재의/ 날짜와 시간을 LocalDate
와 LocalTime
으로 각각 반환하는 now()와 지정된 날짜와 시간으로 LocalDate
와 LocalTime
객체를 생성하는 of()가 있고 둘 다 static
메서드이다.
//LocalDate, LocalTime
LocalDate today = LocalDate.now(); //오늘의 날짜
LocalTime now = LocalTime.now(); //현재시간
LocalDate birthDate = LocalDate.of(2022, 02, 03); //2022년 02월 03일
LocalTime birthTime = LocalTime.of(23, 59, 59); //23시 59분 59초
of()는 다음과 같이 여러 가지 버전이 제공된다.
static LocalDate of(int year, Month month, int dayOfMonth)
static LocalDate of(int year, int month, int dayOfMonth)
static LocalTime of(int hour, int min)
static LocalTime of(int hour, int min, int sec)
static LocalTime of(int hour, int min, int sec, int nanoOfSecond)
일 단위나 초 단위로도 지정할 수 있는데, 아래의 첫 번째 문장은 1999년의 365번째 날, 즉 마지막 날을 의미하며, 두 번째 문자은 그 날의 0시 0분 0초/부터 86399초(하루는 86400초)가 지난 시간, 즉 23시 59분 59초를 의미한다.
LocalDate birthDate = LocalDate.ofYearDay(1999, 365); //1999년 12월 31일
LocalTime birthTime = LocalTime.ofSeconday(86399) //23시 59분 59초
parse()를 이용하면 문자열을 날짜와 시간으로 변환 가능하다.
LocalDate birthDate = LocalDate.parse("1999-12-31"); //1999년 12월 31일
LocalTime birthTime = LocalTime.ofSecondDay(86399); //23시 59분 59초
특정 필드의 값 가져오기 - get(). getXXX()
LocalDate와 LocalTime은 객체에서 특정 필드의 값을 가져올 때 아래의 메서드를 사용한다. 주의할 점은 Calendar와 달리 월(month)의 범위가 1~12이고, 요일은 월요일이 1부터 시작한다.
클래스 | 메서드 | 예시 |
LocalDate | int getYear() | 년도(1999) |
int getMonthValue() | 월(12) | |
Month getMonth() | 월(DECEMBER) get Month().getValue() = 12 | |
int getDayOfMonth() | 일(31) | |
int getDayOfYear() | 같은 해의 1월 1일부터 몇번째 일(365) | |
DayOfWeek getDayOfWeek() | 요일(FRIDAY) getDayOfWeek().getValue() = 5 | |
int lengthOfMonth() | 같은 달의 총 일수(31) | |
int lengthOfYear() | 같은 해의 총 일수(365), 윤년이면 366 | |
boolean lssLeapYear() | 윤년여부 확인(false) | |
LocalTime | int get Hour() | 시(23) |
int getMinute() | 분(59) | |
int getSecond() | 초(59) | |
int geetNano() | 나노초(0) |
만약. int타입의 범위를 넘을 수 있다면 get() 대신 getLong()을 사용하면 된다.
int get(TemporalField field)
long getLong(TemporalField field)
해당 메서들의 매개변수로 사용할 수 있는 필드의 목록은 아래와 같다.
TemporalField(ChronoField) * 표시는 getLong()을 사용 | 설명 |
ERA | 시대 |
YEAR_OF_ERA, YEAR | 년 |
MONTH_OF_YEAR | 월 |
DAY_OF_WEEK | 요일(1:월) |
DAY_OF_MONTH | 일 |
AMPM_OF_DAY | 오전/오후 |
HOUR_OF_DYA | 시간(0~23) |
CLOCK_HOUR_OF_DAY | 시간(1~24) |
HOUR_OF_AMPM | 시간(0~11) |
CLOCK_HOUR_OF_AMPM | 시간(1~12) |
MINUTE_OF_HOUR | 분 |
SECOND_OF_MINUTE | 초 |
MILLI_OF_SECOND | 천분의 일초 |
MICRO_OF_SECOND * | 백만분의 일초 |
NANO_OF_SECOND * | 10억분의 일초 |
DAY_OF_YEAR | 그 해의 몇번째 날 |
EPOCH_DAY * | EPOCH(1970.1.1)부터 몇번째 날 |
MINUTE_OF_DAY | 그 날의 몇 번째 분(시간을 분으로 환산) |
SECOND_OF_DAY | 그 날의 몇 번째 초(시간을 초로 환산) |
MILL_OF_DAY | 그 날의 몇 번째 밀리초 |
MICRO_OF_DAY * | 그 날의 몇 번째 마이크로초 |
NANO_OF_DAY * | 그 날의 몇 번째 나노초 |
ALIGEND_WEEK_OF_MONTH | 그 달의 n번째 주(1~7일 1주, 8~14일 2주, ...) |
ALIGEND_WEEK_OF_YEAR | 그 해의 n번째 주(1월 1~7일 1주, 8~14 2주, ...) |
ALIGEND_DAY_OF_WEEK_IN_MONTH | 요일 ( 그 달의 1일을 월요일로 간주하여 계산) |
ALGEND_DAY_OF_WEEK_IN_YEAR | 요일 ( 그 해의 1월 1일을 월요일로 간주하여 계산) |
INSTANT_SECONDS | 년월일을 초단위로 환산(1970-01-01 00:00:00 UTC를 0초로 계산)Instant에만 사용 |
OFFSET_SECONDS | UTC와의 시차, ZoneOffset에만 사용 가능 |
PROLEPTIC_MONTH | 년월을 월단위로 환산(2015년 11월 = 2015 * 12 + 11) |
위 표는 ChronoField에 정의된 상수를 나열한 것이므로, 사용할 수 있는 필드는 클래스마다 다르다.
예를 들어 LocalDate는 날짜를 표현하기 위한 것이므로, MINUTE_OF_HOUR와 같이 시간에 관련된 필드는 사용할 수 없다.
사용할 수 없는 필드를 사용하면 UnsupportedTemporalTypeException이 발생한다.
만약, 특정 필드가 가질 수 있는 값의 범위를 알고 싶으면 다음과 같이 사용하면 된다.
System.out.println(ChronoField.CLOCK_HOUR_OF_DAY.range()); //1~24
System.out.println(ChronoField.HOUR_OF_DAY.range()); //0 ~23
HOUR은 밤 12시를 0으로 표현하고, CLOCK은 24로 표현한다는 것을 알 수 있다.
필드의 값 변경하기 - with(), plus(), minus()
날짜와 시간에서 특정 필드값을 변경하려면 with로 시작하는 메서드를 사용하면 된다.
LocalDate withYear(int year)
LocalDate withMonth(it month)
LocalDate withDayOfMonth(int dayOfMonth)
LocalDate withDayOfYear(it dayOfYear)
LocalTime withHour(int hour)
LocalTime withMinute(int minute)
LocalTime withSecond(int second)
LocalTime withNano(int nanoOfSecond)
LocalDate with(TemporalField field, long newValue)
필드를 변경하는 메서드들은 항상 새로운 객체를 생성해서 반환하므로 아래와 같이 대입 연산자를 같이 사용해야 한다.
date = date.withYear(2023); //년도를 2023년으로 변경
time = time.withHour(12); //시간을 12시로 변경
그 외에도 특정 필드에 값을 더하거나 빼는 plus()와 minus()가 있다.
LocalTime plus(TemporalAmount amountToAdd)
LocalTime plus(long amountToAdd, TemporalUnit unit)
LocalDate plus(TemporalAmout amountToAdd)
LocalDate plus(long amountToAdd, TemporalUnit unit)
//plus()로 만든 메서드
LocalDate plusYears(long yearsToAdd)
LocalDate plusMonth(long monthsToAdd)
LocalDate plusDays(long daysToAdd)
LocalDate plusWeeks(long weeksToAdd)
LocalTime plusHours(long hoursToAdd)
LocalTime plusMinutes(long minutesToAdd)
LocalTime plusSeconds(long secondsToAdd)
LocalTime plusNanos(long nanosToAdd)
//LocalTime의 truncatedTo()는 지정된 것보다 작은 단위의 필드를 0으로 만든다.
LocalTime time = LocalTime.of(12, 34, 56); // 12시 34분 56초
time = time.truncatedTo(ChronoUnit.HOURS); //시(hour)보다 작은 단위를 0으로
System.out.println(time); //12:00
LocalDate는 LocalTime과 달리 truncatedTo()가 없는데, LocalDate의 필드인 년, 월, 일은 0이 될 수 없기 때문이다.
그래서 truncatedTo()의 매개변수로는 시간과 관련된 필드만 사용가능하다.
날짜와 시간의 비교 - isAfter(), isBefore(), isEqual()
LocalDate와 LocalTimedpsms compareTo()가 오버라이딩되어 있어서 compareTo()로 비교할 수 있다.
하지만 더 편리하게 비교할 수 있는 메서드들이 추가로 제공되고 있다.
boolean isAfter(ChronoLocalDate other)
boolean isBefor(ChronoLocalDate other)
boolean isEqual(ChronoLocalDate other) //LocalDate만 제공
equals()가 있는데도, isEqual()를 제공하는 이유는 연표(chronolory)가 다른 두 날짜를 비교하기 위해서다. equals()는 모든 필드가 일치해야 하지만, isEqual()은 날짜만 비교한다.
LocalDate kDate = LocalDate.of(1999, 12, 31);
JapaneseDate jDate = JapaneseDate.of(1999, 12, 31);
System.out.println(kDate.equals(jDate)); //false -> YEAR_OF_ERA가 다름
System.out.println(kDate.isEqual(jDate)); //true
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
public class NewTimeEx1 {
public static void main(String[] args) {
LocalDate today = LocalDate.now(); //오늘의 날짜
LocalTime now = LocalTime.now(); //현재 시간
LocalDate birthDate = LocalDate.of(2022, 02, 03);
LocalTime birthTime = LocalTime.of(23, 59, 59);
System.out.println("today : " + today);
System.out.println("now : " + now);
System.out.println("birthDate : " + birthDate);
System.out.println("birthTime : " + birthTime);
System.out.println(birthDate.withYear(2024)); //월, 일은 그대로두고 년도만 변경
System.out.println(birthDate.plusDays(1)); //년, 월은 그대로두고 일만 + 1
System.out.println(birthDate.plus(1, ChronoUnit.DAYS)); //위와 동일
//23:59:59 -> 23:00
System.out.println(birthTime.truncatedTo(ChronoUnit.HOURS));
//특정 ChronoField의 범위 알아냄
System.out.println(ChronoField.CLOCK_HOUR_OF_DAY.range()); // 1 - 24
System.out.println(ChronoField.HOUR_OF_DAY.range()); // 0 - 23
}
}
결과
today : 2023-02-03
now : 15:13:12.929343
birthDate : 2022-02-03
birthTime : 23:59:59
2024-02-03
2022-02-04
2022-02-04
23:00
1 - 24
0 - 23
3.Instant
Instant는 에포크 타임부터 경과된 시간을 나노초 단위로 포현한다. 단일 진법으로만 다루기 때문에 사람에겐 불편하지만 계산이 쉽다. 사람이 사용하는 날짜와 시간은 여러 진법이 섞여 있어서 계산하기 어렵다.
Instant now = Instant.now();
Instant now2 = Instant.ofEpochSecond(now.getEpochSecond());
Instant now3 = Instant.ofEpochSecond(now.getEpochSecond(), now.getNano());
Instant를 생성할 때는 위와 같이 now()와 ofEpochSecond()를 사용한다.
필드에 저장된 값을 가져올 때는 다음과 같이 한다.
long epochSec = now.getEpochSecond();
int nano = now.getNano();
Instant는 시간을 초 단위와 나노초 단위로 나누어 저장한다. 오라클 데이터베이스의 타임스탬프(timestamp)처럼 밀리초 단위의 EPOCH TIME을 필요로 하는 경우를 위해 toEpochMilli()가 정의되어 있다.
long toEpochMilli()
Instant는 항상 UTC(+00:00)를 기준으로 하기 때문에, LocalTime과 차이가 있을 수 있다. 시간대를 고려해야하는 경우 OffsetDateTime을 사용하는 것이 더 나은 선택이다.
UTC는 '세계 협정시(Coordinated Universal Time)'라고 하며, 1972년 1월 1일부터 시행된 국제 표준시이다. 이전에 사용되던 GMT(Greenwich Mean Time)와 UTC는 거의 같지만, UTC가 좀 더 정확하다.
Instant는 기존의 java.util.Date를 대체하기 위한 것이며 JDK1.8부터 Date에 Instant로 변환할 수 있는 새로운 메서드가 추가되었다.
static Date from(Instant instant); //Instant -> Date
static toInstant() //Date -> Instant
4. LoclaDateTime 과 ZonedDateTime
LocalDateTime은 LocalDate와 LocalTime을 합쳐놓은 것이다. ZonedDateTime은 LocalDateTime에 시간대(time zone)을 추가한 것이다.
LocalDate와 LocalTime으로 LocalDateTime 만들기
LocalDate date = LocalDate.of(2015, 12, 31);
LocalTime time = LocalTime.of(12, 34, 56);
LocalDateTime dt = LocalDateTime.of(date, time);
LocalDateTime dt2 = date.atTime(time);
LocalDateTime dt3 = time.atDate(date);
LocalDateTime dt4 = date.atTime(12, 34, 56);
LocalDateTime dt5 = time.atDate(LocalDate.of(2015, 12, 31));
LocalDateTiem dt6 = date.atStartOfDay(); //dt6 = date.atTime(0, 0, 0);
LocalDateTime으로 날짜와 시간 지정
LocalDateTime dateTime = LocalDateTime.of(2015, 12, 31, 12, 34, 56); //2015년 12월 31일 12시 34분 56초
LocalDateTime today = LocalDateTime.now();
LocalDateTime을 LocalDateTime 또는 LocalDate로 변환
LocalDateTime dt = LocalDateTime.of(2015, 12, 31, 12, 34, 56);
LocalDate date = dt.toLocalDate(); //LocalDateTime -> LocalDate
LocalTime time = dt.toLocalTime(); //LocalDateTime -> LocalTime
LocalDateTime으로 ZonedDateTime 만들기
LocalDateTime에 시간대(time-zone)을 추가하면, ZonedDateTime이 된다.
기존에는 TimeZone클래스로 시간대를 다뤘지만 새로운 시간 패키지에서는 ZoneId라는 클래스를 사용한다.
ZoneId는 일광 절약시간(DST, Daylight Saving Time)을 자동적으로 처리해주므로 더 편리하다.
LocalDate에 시간 정보를 추가하는 atTime()을 쓰면 LocalDateTime을 얻을 수 있는 것처럼, LocalDateTime에 atZone()으로 시간대 정보를 추가하면, ZonedDateTime을 얻을 수 있다.
ZoneId zid = ZoneId.of("Asia/Seoul");
ZoneDateTime zdt = dateTime.atZone(zid);
System.out.println(zdt); //2023-02-03T17:47:50.451+09:00[Asia/seoul]
LocalDate에 atStartOfDay()라는 메서드가 있는데 이 메서드에 매개변수로 ZoneId를 지정해도 ZonedDateTime을 얻을 수 있다.
ZonedDateTime zdt = LocalDate.now().atStartOfDay(zid);
System.out.println(zdt); //2023-02-03T00:00+09:00[Asia/Seoul]
메서드이름(startOfDay)에서 알 수 있듯이 시간이 0시 0분 0초로 되어 있다.
만약 특정 시간대의 나라 Zone을 알고 싶다면 다음과 같이 변환하면 된다.
ZoneId nyId = ZoneId.of("Amareica/New_York");
ZoneDateTime nyTime = ZonedDateTime.now().withZoneSameInstant(nyId);
now() 대신 of()를 사용하면 날짜와 시간을 지정할 수 있다.
ZoneOffset
UTC로부터 얼마만큼 떨어져 있는지는 ZoneOffset으로 표현한다. 서울은 UTC보다 9시간 빠른걸 알 수 있다.
ZoneOffSet krOffset = ZonedDateTime.now().getOffset();
//ZoneOffSet krOffset = ZonedDateTime.of("+9"); //h, hh, hhmm, hh:mm
int krOffsetInSec = krOffset.get(ChronoField.OFFSET_SECONDS); // 324000초(60*60*9) 즉, +9
OffsetDateTime
ZonedDateTime은 ZoneId로 구역을 표현하는데, ZoneId가 아닌 ZoneOffset을 사용하는 것이 OffsetDateTime이다.
ZoneOffset은 단지 시간대를 시간의 차이로만 구분한다. 컴퓨터에게 일광절약시간(ZoneId)처럼 계절별로 시간을 더했다 뺐다하는 것과 같은 행위는 위험하다. 아무런 변화 없이 일관된 시간체계를 유지하는 것이 안전하다. 같은 지역 내의 컴퓨터간에 데이터를 주고받을 때, 전송시간을 표현하기에 LocalDateTime이면 충분하겠지만. 서로 다른 시간대에 존재하는 컴퓨터간의 통신에는 OffsetDateTime이 필요하다.
ZonedDateTime zdt = ZondedDateTime.of(date, time, zid);
OffsetDateTime odt = OffsetDateTime.of(date, time, krOffset);
//ZonedDatetime -> OffsetDateTime
OffsetDateTime odt = zdt.toOffsetDateTime();
OffsetDateTime은 ZonedDateTime처럼, LocalDate와 LocalTime에 ZoneOffset을 더하거나, ZonedDateTime에 toOffsetDateTime()을 호출해서 얻을 수도 있다.
ZonedDateTime의 변환
ZonedDateTime도 LocalDateTime 처럼 날짜와 시간에 관련된 다른 클래스로 변환하는 메서드를 가지고 있다.
LocalDate toLocalDate()
LocalTime tolocalTime()
LocalDateTime toLocalDateTime()
OffsetDateTime toOffsetDateTime()
long toEpochSecond()
Instant toInstant()
GregorianCalenda와 가장 유사한 것이 ZonedDateTime 이다. GregorianCalendar와 ZonedDateTime 간의 변환방법만 알면, 위에 나열한 메서드를 이용해서 다른 날짜와 시간 클래스들로 변환할 수 있다.
GregorianCalendar from(ZonedDateTime zdt); //ZonedDateTime -> GregorianCalendar
ZonedDateTime toZonedDateTime(); //GregorianCalendar -> ZonedDateTime
import java.time.*;
public class NewTimeEx2 {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2023, 02, 03); // 2023년 02월 03일
LocalTime time = LocalTime.of(12, 34, 56); //12시 34분 56초
LocalDateTime dt = LocalDateTime.of(date, time);
ZoneId zid = ZoneId.of("Asia/Seoul");
ZonedDateTime zdt = dt.atZone(zid);
String strZid = zdt.getZone().getId();
ZonedDateTime seoulTime = ZonedDateTime.now();
ZoneId nyId = ZoneId.of("America/New_York");
ZonedDateTime nyTime = ZonedDateTime.now().withZoneSameInstant(nyId);
//ZonedDatetime -> OffsetDateTime
OffsetDateTime odt = zdt.toOffsetDateTime();
System.out.println(dt);
System.out.println(zid);
System.out.println(zdt);
System.out.println(seoulTime);
System.out.println(nyTime);
System.out.println(odt);
}
}
//결과
//2023-02-03T12:34:56
//Asia/Seoul
//2023-02-03T12:34:56+09:00[Asia/Seoul]
//2023-02-03T16:45:42.141431+09:00[Asia/Seoul]
//2023-02-03T02:45:42.143678-05:00[America/New_York]
//2023-02-03T12:34:56+09:00
5. TemporalAdjusters
plus(), minus()와 같은 메서드로 날짜와 시간을 계산할 수 있지만 지난 주 토요일이 며칠인지, 또는 이번 달의 3번째 금요일이 며칠인지와 같은 날짜계산을 하기에는 불편하다.
그래서 자주 쓰일만한 날짜 계산들을 대신 해주는 메서드를 정의해놓은 것이 있는데 TemporalAdjusters 클래스이다.
LocalDate today = LocalDate.now();
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
메서드 | 설명 |
firstDayOfNextYear() | 다음 해의 첫 날 |
firstDayOfNextMonth() | 다음 달의 첫 날 |
firstDayOfYear() | 올 해의 첫 날 |
firstDayOfMonth() | 이번 달의 첫 날 |
lastDayOfYear() | 올 해의 마지막 날 |
lastDayOfMonth() | 이번 달의 마지막 날 |
firstInmonth(DayOfWeek dayOfWeek) | 이번 달의 첫 번째 ? 요일 |
lastInMonth(DayOfWeek dayOfWeek) | 이번 달의 마지막 ? 요일 |
previous(DayOfWeek dayOfWeek) | 지난 ? 요일(당일 미포함) |
previousOrSame(DayOfWeek dayOfWeek) | 지난 ? 요일(당일 포함) |
next(DayOfWeek dayOfWeek) | 다음 ? 요일(당일 미포함) |
nextOrSame(DayOfWeek dayOfWeek) | 다음 ? 요일(당일 포함) |
dayOfWeekInMonth(int ordinal, (DayOfWeek dayOfWeek) | 이번 달의 n번째 ? 요일 |
TemporalAdjuster 직접 구현하기
TemporalAdjusters에 정의된 메서드로도 충분할 수 있지만, 필요하다면 자주 사용되는 날짜 계산을 해주는 메서드를 직접 만들수도 있다.
LocalDate의 with()는 다음과 같이 정의되어 있으며, TemporalAdjuster 인터페이스를 구현한 클래스의 객체를 매개변수로 제공해야 한다.
LocalDate with(TemporalAdjuster adjuster)
with()는 LocalTime, LocalDateTime, ZonedDateTime, Instent 등 대부분의 날짜와 시간에 관련된 클래스에 포함되어 있다.
TemporalAdjuster인터페이스는 다음과 같이 추상 메서드 하나만 정의되어 있으며, 이 메서드만 구현하면 된다.
@FunctionalInterface
publi interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
실제로 구현해야하는 것은 adjustInto()지만, TemporalAdjuster와 같이 사용해야 하는 메서드는 with()다.
with()와 adjustInto() 중에서 어느 쪽을 사용해도 상관없지만, adjustInto()는 내부적으로만 사용할 의도로 작성된 것이기 때문에 with()를 사용하면 된다.
날짜와 시간에 관련된 대부분의 클래스는 Temporal 인터페이스를 구현하였으므로, adjustInto()의 매개변수가 될 수 있다.
예를 들어 특정 날짜로부터 2일후의 날짜를 계산하는 DayAfterTomorrow는 다음과 같이 작성할 수 있다.
class DayAfterTomorrow implements TemporalAdjuster {
@Override
public Temporal adjustinto(Temporal temporal) {
return temporal.plus(2, Chronounit.DAYS); //2일 더함
}
}
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
import static java.time.DayOfWeek.TUESDAY;
import static java.time.temporal.TemporalAdjusters.*;
class DayAfterTomorrow implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
return temporal.plus(2, ChronoUnit.DAYS);
}
}
public class NewTimeEx3 {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
LocalDate date = today.with(new DayAfterTomorrow());
p(today); //오늘
p(date); //2일 뒤
p(today.with(firstDayOfNextMonth())); //다음 달의 첫 날
p(today.with(firstDayOfMonth())); //이 달의 첫 날
p(today.with(lastDayOfMonth())); //이 달의 마지막 날
p(today.with(firstInMonth(TUESDAY))); //이 달의 첫번째 화요일
p(today.with(lastInMonth(TUESDAY))); //이 달의 마지막 화요일
p(today.with(previous(TUESDAY))); //지난주 화요일
p(today.with(previousOrSame(TUESDAY))); //지난주 화요일(오늘 포함)
p(today.with(next(TUESDAY))); //다음주 화요일
p(today.with(nextOrSame(TUESDAY))); //다음주 화요일(오늘 포함)
p(today.with(dayOfWeekInMonth(4, TUESDAY))); //이 달의 4번째 화요일
}
static void p(Object obj) {
System.out.println(obj);
}
}
6. Period 와 Duration
Period : 날짜 차이
Duration : 시간 차이
between()
두 날짜 차이를 나타내는 period는 between()으로 값을 얻을 수 있다.
LocalDate date1 = LocalDate.of(2023, 1, 1);
LocalDate date2 = LocalDate.of(2024, 12, 31);
Period pe = Period.between(date1, date2);
날짜 상으로 이전이면 양수, 이후면 음수로 period에 저장된다.
시간차이를 구할 때는 Duration을 사용한다는것만 빼면 똑같다.
LocalDate date1 = LocalDate.of(00, 00, 00);
LocalDate date2 = LocalDate.of(12, 34, 56); //12시 34분 56초
Period pe = Duration.between(date1, date2);
Period, Duration에서 특정 필드의 값을 얻을 때는 get()을 사용한다.
long year = pe.get(ChronoUnit.YEARS); //int getYears()
long month = pe.get(ChronoUnit.MONTHS); //int getMonths()
long day = pe.get(ChronoUnit.DAYS); //int getDays()
long sec = pe.get(ChronoUnit.SECONDS); //long getSeconds()
long nano = pe.get(ChronoUnit.NANOS); //int getNano()
Period와 달리 Duration에는 getHours() 같은 메서드가 없다. Chrono.SECONDS와 Chrono.NANOS 밖에 사용할 수 없다. 직접 계산하는 방법이 있지만 불편하다. Duration을 LocalTime으로 변환한 다음에, LocalTime이 가지고 있는 get 메서드들을 사용하면 직접 계산보다는 조금 간단하다.
LocalTime tmpTime = localTime.of(0,0).plusSeconds(du.getSeconds());
int hour = tmpTime.getHour();
int min = tmpTime.getMinute();
int sec = tmpTime.getSecond();
int nano = tmpTime.getNono();
between()과 until()
until()은 between()과 거의 같은 일을 한다. between()은 static 메서드이고, untill()은 인스턴스 메서드라는 차이가 있다.
//Period pe = Period.between(today, myBirthDay);
Period pe = today.until(myBirthDay);
long dday = today.until(myBirthDay, ChronoUnit.DAYS);
Period는 년월일을 분리해서 저장하기 때문에, D-day를 구하려는 경우에는 두 개의 메개변수를 받는 until()을 사용하는 것이 낫다.
날짜가 아닌 시간에도 until()을 사용할 수 있지만, Duration을 반환하는 until()은 없다.
long sec = Localtime.now().until(endTime, ChronoUnit.SECONDS);
사칙연산, 비교연산, 기타 메서드
plus(), minus()외에 곱셈과 나눗셈을 위한 메서드도 있다.
pe.minusYears(1).multipledBy(2); //1년을 빼고, 2배를 곱한다.
du.plusHours(1).dividedBy(60); //1시간을 더하고 60으로 나눈다.
음수인지 확인하는 isNegative()와 0인지 확인하는 isZero()가 있다. 두 날짜 또는 시간을 비교할 때 사용하면 어느쪽이 앞인지 또는 같은지 알아낼 수 있다.
boolean sameDate = Period.between(date1, date2).isZero();
boolean isBefore = Duration.between(time1, time2).isNegative();
부호를 반대로 변경하는 negate()와 부호를 없애는 abs()가 있다. 아래 두 소스는 같으나 Period에는 abs()가 없어서 negated()를 사용해야 한다.
du = du.abs();
if(du.isNegatiove()) du = du.negated(); //위와 같다.
Period에 normalized()라는 메서드가 있는데, 이 메서드는 월의 값이 12를 넘지않게 바꿔준다. 일의 길이는 일정하지 않다보니 계산해주지 않는다.
pe = Period.of(1, 13, 32).normalized(); //1년 13개월 32일 -> 2년 1개월 32일
다른 단위로 변환 - toTotalMonths(), toDays(), tohours(), toMinutes()
이름이 'to'로 시작하는 메서들들은 Peroid와 Duration을 다른 단위의 값으로 변환하는데 주로 사용된다. get()은 특정 필드의 값을 그대로 가져오지만, 아래의 메서드들은 특정 단위로 변환한 결과를 반환한다는 차이가 있다.
참고) 이 메서드들의 반환타입은 모두 정수(long타입)인데, 지정된 단위 이하의 값들은 버려진다.
클래스 | 메서드 | 설명 |
Period | long to TotalMonths() | 년월일을 월단위로 변환해서 반환(일 단위는 무시) |
Duration | long toDays() | 일단위로 변환해서 반환 |
long toHours() | 시간단위로 변환해서 반환 | |
long toMinutes() | 분단위로 변환해서 반환 | |
long toMillis() | 천분의 일초 단위로 변환해서 반환 | |
long toNonos() | 나노초 단위로 변환해서 반환 |
LocalDate의 toEpochDay()라는 메서드는 Epoch Day인 '1970-01-01'부터 날짜를 세어 반환한다.
두 날짜 모두 Epoch Day 이후라면, Period를 사용하지 않고도 두 날짜간의 일수를 편리하게 계산할 수 있다.
LocalDate date1 = LocalDate.of(2023, 01, 01);
LocalDate date2 = LocalDate.of(2023, 02, 03);
long period = date2.toEpochDay() - date1.toEpochDay();
LocalTime에도 다음과 같은 메서드가 있어서, Duration을 사용하지 않고도 뺄셈으로 시간차이를 계산할 수 있다.
int toSecondOfDay()
long toNanoOfDay()
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;
import java.time.temporal.ChronoUnit;
public class NewTimeEx4 {
public static void main(String[] args) {
LocalDate date1 = LocalDate.of(2022, 1, 1);
LocalDate date2 = LocalDate.of(2023, 12, 31);
Period pe = Period.between(date1, date2);
System.out.println("date1 : " + date1);
System.out.println("date2 : " + date2);
System.out.println("pe : " + pe);
System.out.println("YEAR : " + pe.get(ChronoUnit.YEARS));
System.out.println("MONTH : " + pe.get(ChronoUnit.MONTHS));
System.out.println("DAY : " + pe.get(ChronoUnit.DAYS));
LocalTime time1 = LocalTime.of(2, 12, 50);
LocalTime time2 = LocalTime.of(12, 34, 56);
Duration du = Duration.between(time1, time2);
System.out.println("time1 : " + time1);
System.out.println("time2 : " + time2);
System.out.println("du : " + du);
LocalTime tempTime = LocalTime.of(0, 0).plusSeconds(du.getSeconds());
System.out.println("HOUR : " + tempTime.getHour());
System.out.println("MINUTE : " + tempTime.getMinute());
System.out.println("SECOND : " + tempTime.getSecond());
System.out.println("NANO : " + tempTime.getNano());
}
}
//결과
//date1 : 2022-01-01
//date2 : 2023-12-31
//pe : P1Y11M30D
//YEAR : 1
//MONTH : 11
//DAY : 30
//time1 : 02:12:50
//time2 : 12:34:56
//du : PT10H22M6S
//HOUR : 10
//MINUTE : 22
//SECOND : 6
//NANO : 0
7. 파싱과 포맷
형식화(formatiing)와 관련된 클래스들은 java.time.format 패키지에 들어있는데, 이 중에서 DateTimeFormatter가 가장 핵심이다.
자주 쓰이는 다양한 형식들을 기본적으로 정의하고 있으며, 필요하다면 직접 정의해서 사용할 수도 있다.
LocalDate date = LocalDate.of(2023, 1, 2);
String yyyymmdd1 = DateTimeFormatter.ISO_LOCAL_DATE.format(date); //2023-01-02
String yyyymmdd2 = date.format(DateTimeForatter.ISO_LOCAL_DATE); //2023-01-02
위와 같이 format()을 사용하여 날짜와 시간을 형식화하는데, 이 메서드는 DateTimeFormatter 뿐만 아니라 LocalDate나 LocalTime 같은 클래스에도 있다. 같은 기능을 하니까 상황에 따라 편한 쪽을 선택해서 사용하면 된다.
DateTimeFormatter | 설명 | 보기 |
ISO_DATE_TIME | Date and time with ZoneId | 2023-12-03T10:15:30+01:00[Europe/Paris] |
ISO_LOCAL_DATE | ISO Local Date | 2023-12-03 |
ISO_LOCAL_TIME | Time without offset | 10:15:30 |
ISO_LOCAL_DATE_TIME | ISO Local Date and Time | 2023-12-03T10:15:30 |
ISO_OFFSET_DATE | Time with offset | 2023-12-03+01:00 |
ISO_OFFSET_TIME | Date with offset | 10:15:30+01:00 |
ISO_OFFSET_DATE_TIME | Date Time with Offset | 2023-12-03T10:15:30+01:00 |
ISO_ZONED_DATE_TIME | Zoned Date Time | 2023-12-03T10:15:30+01:00[Eupope/Paris] |
ISO_INSTANT | Date and Time of an Instant | 2023-12-03T10:15:30Z |
BASIC_ISO_DATE | Basic ISO date | 20231203 |
ISO_DATE | ISO Date with or without offset | 2023-12-03+01:00 2023-12-03 |
ISO_TIME | Time with or without offset | 10:15:30+01:00 10:15:30 |
ISO_ORDINAL_DATE | Year and day of year | 2023-337 |
ISO_WEEK_DATE | Year and Week | 2023-W48-6 |
RFC_1123_DATE_TIME | RFC 1123 / RFC 822 | Tue, 3 Jun 2023 11:05:30 GMT |
로케일에 종속된 형식화
DateTimeFormatter의 static 메서드 ofLocalzedDate(), ofLocalizedTime(), ofLocalized DateTime() 로케일(locale)에 종속적인 formmater를 생성한다.
DateTimeFormatter. formatter = DateTimeFormatter.ofLocaizedDate(FormatStyle.SHORT);
String shortFormat = formatter.format(LocalDate.now());
FormatStyle의 종류에 따른 출력 형태는 다음과 같다.
FormatStyle | 날짜 | 시간 |
FULL | 2023년 02월 03일 | N/A |
LONG | 2023년 02년 03일 (금) | 오후 9시 15분 13초 |
MEDIUM | 2023. 02. 03 | 오후 9:15:13 |
SHORT | 23. 02. 03 | 오후 9:15 |
출력형식 직접 정의하기
DateTimeFormatter의 ofPattern()으로 원하는 출력형식을 직접 작성할 수 있다.
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
기호 | 의미 | 보기 |
G | 연대(BC, AD) | 서기 또는 AD |
y 또는 u | 년도 | 2023 |
M 또는 L | 월(1~12 또는 1월~12월) | 11 |
Q 또는 q | 분기(quarter) | 4 |
w | 년의 몇 번째 주(1~53) | 48 |
W | 월의 몇 번째 주(1~5) | 4 |
D | 년의 몇 번째 일(1~366) | 332 |
d | 월의 몇 번째 일(1~31) | 28 |
F | 월의 몇 번째 요일(1~5) | 4 |
E 또는 e | 요일 | 토 또는 7 |
a | 오전/오후(AM, PM) | 오후 |
H | 시간(0~23) | 22 |
k | 시간(1~24) | 22 |
K | 시간(0~11) | 10 |
h | 시간(1~12) | 10 |
m | 분(0~59) | 12 |
s | 초(0~59) | 35 |
S | 천분의 일초(0~999) | 7 |
A | 천분의 일초(그 날의 0시 0분 0초의 부터의 시간) | 80263808 |
n | 나노초(0~999999999) | 475000000 |
N | 나노초(그 날의 0시 0분 0초 부터의 시간) | 81069992000000 |
V | 시간대 ID(VV) | Asia/Seoul |
z | 시간대(time-zone) 이름 | KST |
O | 지역화된 zone-offset | GMT+9 |
Z | zone-offset | +0900 |
X 또는 x | zone-offset(Z는 +00:00를 의미) | +09 |
' | escape문자(특수문자를 표현하는데 사용) |
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class DateFormatterEx1 {
public static void main(String[] args) {
ZonedDateTime zDateTime = ZonedDateTime.now();
System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("yyyy-mm-dd HH:mm:ss")));
System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("''yy년 MMM dd일 E요일")));
System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS z VV")));
System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss a")));
System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("오늘은 올 해의 D번째 날")));
System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("오늘은 이 달의 d번째 날")));
System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("오늘은 올 해의 w번째 주")));
System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("오늘은 이 달의 W번째 주")));
System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("오늘은 이 달의 W번째 E요일")));
}
}
//결과
//2023-19-03 23:19:50
//'23년 2월 03일 금요일
//2023-02-03 23:19:50.698 KST Asia/Seoul
//2023-02-03 11:19:50 오후
//오늘은 올 해의 34번째 날
//오늘은 이 달의 3번째 날
//오늘은 올 해의 5번째 주
//오늘은 이 달의 1번째 주
//오늘은 이 달의 1번째 금요일
문자열을 날짜와 시간으로 파싱하기
static 메서드 parse()를 사용하면 된다. 날짜와 시간을 표현하는데 사용되는 클래스에는 parse() 메서드가 거의 다 포함되어 있다.
parse()는 오버로딩 된 메서드가 여러 개 있는데 그 중 아래 2개가 자주 쓰인다.
static LocalDateTime parse(CharSequence text)
static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter)
DateTimeFormatter에 상수로 정의된 형식을 사용할 때는 다음과 같이 표현한다.
LocalDate date = LocalDate.parse("2023-01-02", DateTimeFormatter.ISO_LOCAL_DATE);
아래와 같이 상수를 사용하지 않고도 파싱이 가능하다.
LocalDate newDate = LocalDate.parse("2023-01-01");
LocalTime newTime = LocalTime.parse("23:59:59");
LocalDateTime newDateTime = LocalDateTime.parse("2023-01-01T23:59:59");
//ofPattern() 사용
DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime endOfYear = LocalDateTime.parse("2023-01-01 23:59:59", pattern)";
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public class DateFormatterEx2 {
public static void main(String[] args) {
LocalDate newYear = LocalDate.parse("2023-01-01", DateTimeFormatter.ISO_LOCAL_DATE);
System.out.println(newYear); //2023-01-01
LocalDate date = LocalDate.parse(("2023-01-01"));
System.out.println(date); //2023-01-01
LocalTime time = LocalTime.parse("23:59:59");
System.out.println(time); //23:59:59
LocalDateTime dateTime = LocalDateTime.parse("2023-01-10T23:59:59");
System.out.println(dateTime); //2023-01-10T23:59:59
DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime endOfYear = LocalDateTime.parse("2023-01-01 23:59:59", pattern);
System.out.println(endOfYear); //2023-01-01T23:59:59
}
}
참고: 자바의 정석
'DEV > Java' 카테고리의 다른 글
[자바]ArrayList (6) | 2023.02.06 |
---|---|
[자바] 컬렉션 프레임워크(Collections Framework) (4) | 2023.02.05 |
[자바] MessageFormat (3) | 2023.02.02 |
[자바] ChoiceFormat (1) | 2023.02.02 |
[자바] SimpleDateFormat (2) | 2023.02.02 |
댓글