5. 자바 중급 활용-2
이번 강좌에서는 자바를 좀 더 제대로 사용하기위해 java.time 패키지의 LocalDate, LocalDateTime 클래스등을 사용해 날짜와 시간등 을 다루는 방법을 알아 봅니다. 또한 제네릭을 통해 타입을 일반화 하는 방법을 배우고 자료구조 프로그래밍에 활용할 수 있습니다.
이 강의를 통해 자바언어의 중급활용을 통해 기초를 다지고 보다 복잡한 자바프로그램 구조를 이해하고 개발 할 수 있습니다.
01: 날짜와 시간 다루기
프로그램에서 날짜와 시간을 다루는 부분은 간단해 보이지만 사실 꽤 번거로운 작업입니다. 문자열과 유사하게 날짜와 시간역시 기본적인 구조와 동작원리를 이해하고 주요 API의 사용법을 익혀서 제대로 사용해야 합니다.
날짜와 시간은 현재 뿐만 아니라 과거, 미래의 시간등이 모두 다뤄질 수 있는데 윤년 계산을 비롯해 국가별로 일광절약시간제 즉 써머타임 등의 적용 유무가 다르며 특히 과거의 경우 역사적으로 시간과 관련된 변경 사항들이 많아 단순하게 생각하기 어려운 부분이 있습니다.(예를들어 대한민국의 시간대는 1961년 8월10일 UTC+8:30 에서 UTC+9 로 변경됨)
기본적으로 자바에서는 java.util.Date
와 java.util.Calendar
클래스를 사용했지만 사용에 불편함이 많아 JDK8 이후부터는 java.time
패키지의 클래스로 많은 부분이 대체 되었습니다.
현재는 대부분의 날짜와 시간관련 처리는 java.time
패키지를 통해 처리할 수 있으므로 이를 중심으로 학습하면 됩니다. 또한 날짜를 화면에 표현하기 위해서는 적절하게 형식을 지정해 주어야 하므로 형식 지정 클래스들도 잘알고 있어야 합니다.
날짜, 시간 구하기
LocalDate, LocalTime, LocalDateTime
가장 기본이 되는 클래스들로 각각 날짜, 시간, 날짜와시간 을 다루는 클래스 입니다. new()
를 통해 직접적인 인스턴스 생성이 불가능 하고 static
메서드를 이용하는 방식을 사용 합니다.
- now(): 현재 날짜, 시간에 기반해 인스턴스 생성
- of(): 인자로 전달되는 특정 날짜, 시간에 기반한 인스턴스 생성
- atTime(): 특정 시간정보에 기반해 LocalDateTime 인스턴스를 생성
LocalDate d1 = LocalDate.now();
LocalDate d2 = LocalDate.of(2019,10,10);
LocalTime t1 = LocalTime.now();
LocalTime t2 = LocalTime.of(7, 20,20);
» 실습: 기본적인 날짜 시간 구하기
실습개요
LocalDate, LocalTime, LocalDateTime 을 이용한 기본적인 날짜, 시간등을 구하는 방법을 실습합니다.
소스코드
public class DateTimeTest1 {
public static void main(String[] args) {
LocalDate d1 = LocalDate.now();
LocalDate d2 = LocalDate.of(2019, 10, 10);
LocalTime t1 = LocalTime.now();
LocalTime t2 = LocalTime.of(7, 20, 20);
System.out.printf("LocalDate.now() : %s\n", d1);
System.out.printf("LocalDate.of(2019,10,10) : %s\n", d2);
System.out.printf("LocalTime.now() : %s\n", t1);
System.out.printf("LocalTime.of(7,20,20) : %s\n", t2);
LocalDateTime dt1 = LocalDate.now().atTime(LocalTime.MIDNIGHT);
LocalDateTime dt2 = LocalDate.now().atTime(LocalTime.MAX);
System.out.printf("LocalDate.now().atTime(LocalTime.MIDNIGHT) : %s\n", dt1);
System.out.printf("LocalDate.now().atTime(LocalTime.MAX) : %s\n", dt2);
}
}
- now() 는 현재 날짜 및 시간, of() 는 특정 날짜 및 시간 객체 생성.
- LocalDate 에 Time 을 추가하려면 atTime() 를 사용하며 LocalDateTime 이 리턴됨.
- LocalTime.MIDNIGHT, MAX, MIN, NOON 등 사용 가능.
실행결과
별도의 형식을 지정하지 않았기 때문에 해당 객체에서 제공하는 기본 형식으로 출력됩니다. 2019-07-28T00:00
과 같은 형식은 UTC 시간이라고 하는 협정세계시간의 표기법 입니다.
LocalDate.now() : 2019-07-28
LocalDate.of(2019,10,10) : 2019-10-10
LocalTime.now() : 18:09:55.505280
LocalTime.of(7,20,20) : 07:20:20
LocalDate.now().atTime(LocalTime.MIDNIGHT) : 2019-07-28T00:00
LocalDate.now().atTime(LocalTime.MAX) : 2019-07-28T23:59:59.999999999
형식 지정하기
java.time.format
패키지의 클래스들이 사용되며 대표적인 것은 DateTimeFormatter
클래스 입니다.
- ofLocalizedDate(): 현재 지역(국가)에 따른 기본 표기 형식으로 출력
- ofPattern(): 인자로 특정 포맷을 지정
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); // 2015-04-18 00:42:24
DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT); //
날짜와 시간차이 계산하기
보통 날짜와 시간이 많이 사용되는 부분은 월별로 데이터를 관리하거나 특정 기간의 데이터를 조회하거나 하는등의 작업 입니다.
- Period: 날짜 사이의 간격을 년/월/일 단위로 나타냄
- Duration: 시간 사이의 간격을 나노초 단위로 나타냄
LocalDate.of(2019, 5, 15).plus(Period.ofDays(1)); // (2019년5월15일 + 1일간) = 2019년5월16일
LocalTime.of(9, 0, 0).plus(Duration.ofMinutes(10)); // (9시 + 10분간) = 9시10분
LocalDate.now().plusDays(1); // (오늘 + 1일) = 내일
LocalTime.now().minusHours(3); // (지금 - 3시간) = 3시간 전
Period period = Period.between(startDate, endDate); // 두 날짜 사이의 연/원/일 계산
» 실습: 날짜 형식 지정 및 시간차이 계산
실습개요
DateTimeFormatter 를 이용한 형식 지정 및 Period 와 Duration 클래스를 사용한 날짜 , 시간 차이 계산 및 조정 예제 입니다.
소스코드
public class DateTimeTest2 {
public static void main(String[] args) {
LocalDateTime dt1 = LocalDateTime.now();
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);
System.out.println(dtf.format(dt1));
System.out.println(dt1.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
System.out.println(dt1.plusDays(2));
System.out.println(LocalTime.now().minusHours(3));
System.out.println(Duration.ofMinutes(10).getSeconds());
LocalTime start = LocalTime.of(11, 40, 50);
LocalTime end = LocalTime.of(11, 42, 50);
Duration duration = Duration.between(start, end);
System.out.println("Seconds: " + duration.getSeconds());
LocalDate startDate = LocalDate.of(1950, 9, 1);
LocalDate endDate = LocalDate.of(2010, 9, 2);
Period period = Period.between(startDate, endDate);
System.out.println("Years: " + period.getYears());
System.out.println("Months: " + period.getMonths());
System.out.println("Days: " + period.getDays());
}
}
- Duration 을 통해 11:40:50 ~ 11:42:50 의 시간차이를 초단위로 계산.
- Period 를 이용해 1950-9-1 ~ 2010-9-2 까지의 연/월/일 차이를 계산.
실행결과
19. 7. 28. 오후 6:18
2019-07-28 18:18:33
2019-07-30T18:18:33.905151
15:18:33.951556
600
Seconds: 70
Nano Seconds: 800
Years: 60
Months: 0
Days: 1
02: 제네릭(Generics)
타입 안전성(Type Safe)
객체지향 프로그램 개발에 있어 타입의 중요성은 말할 필요도 없다. 고정된 코드에서는 크게 문제가 되지 않지만 실행과정에서 동적으로 전달되는 객체를 참조해야 하는 경우 잘못된 타입이 전달되면 문제가 됩니다.
조금 다른 경우이기는 하지만 예를 들어 다음코드는 문제가 있습니다.
Color color = new Color("red");
- Color 객체를 생성하는데 “red” 라는 문자열 데이터가 사용됨.
- 컴파일상에는 문제가 없으나 만일 문자열 값이 잘못된 것이라면 실행중 에러가 발생.
이 경우에는 6장에서 배우게될 enum 을 사용하는 것이 바람직 합니다.
Color color = new Color(Color.RED);
이과 같이 객체 타입으로 지정을 해버리면 컴파일시 잘못된 색상을 사용할 가능성이 원천 차단 됩니다. 마찬가지로 다양한 타입으로 데이터가 구성될 수 있는 클래스를 설계할때 제네릭을 사용하면 이와 같은 타입 문제를 해결할 수 있습니다.
제네릭
예를들어 ArrayList
는 배열과 유사한 자료구조를 제공하는 클래스로 Object
타입의 데이터를 저장할 수 있습니다.
그런데 Object
클래스는 모든 자바 클래스의 슈퍼클래스 이므로 실제로는 모든 자바 클래스가 원소로 들어갈 수 있다는 의미가 됩니다. 매우 편한 구조이기는 하지만 ArrayList 로 부터 참조 원소들을 꺼내 사용할때 타입들이 서로 다를 수 있기 때문에 메서드의 사용등이 차이가 있어 타입 비교를 해야하는 문제가 발생 합니다.
이와 같이 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 과정에 타입체크(compile-time type check)를 해주는 기능을 제네릭 이라고 합니다.
제네릭 사용의 장점은 다음과 같습니다.
- 제네릭 클래스 타입의 객체를 생성할때 개발자가 원하는 타입을 지정할 수 있음.
- 타입 안정성을 제공.
- 의도하지 않은 타입의 객체가 저장되는 것을 막아 잘못 형변환 되는 오류를 방지.
- 형변환의 번거로움을 줄여줌. -> 간결한 코드 유지 가능
class Storage<T> {
T item;
// getter, setter 생략
}
class App {
public static void main(String[] args) {
Storage<String> storage = new Storage<>();
}
}
-
는 타입파라미터를 의미하며 임의의 객체타입 지정이 가능. - 타입파라미터는 원시형은 안되고 객체타입만 가능하므로
int
의 경우Integer
랩퍼클래스를 사용. - new 에서는 <> 타입 명시 하지 않아도 됨.(타입 추론 가능)
제네릭을 사용할때 주의 할 점은 다음과 같습니다.
- T는 인스턴스변수로 간주되기 때문에 static멤버에는 타입변수 T를 사용할 수 없음.
- 지네릭 타입의 배열을 생성하는 것은 불가능.
- new, instacneof 연산자의 경우 컴파일 시점에 타입T를 명확하게 알아야 하기 때문에 T를 피연산자로 사용할 수 없음.
» 실습: 기본적인 제네릭 클래스 생성과 사용
실습개요
타입 파라미터를 이용해 간단한 제네릭 클래스를 생성하고 사용하는 실습 예제 입니다.
소스코드
public class Storage<T> {
T item;
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
소스코드
public class GenericsTest1 {
public static void main(String[] args) {
Storage<String> storage1 = new Storage<>();
storage1.setItem("MyItem");
System.out.println(storage1.getItem());
Storage<Integer> storage2 = new Storage<>();
storage2.setItem(20201121);
System.out.println(storage2.getItem());
}
}
- 동일한 Storage 클래스를 사용하지만 각각
String
과Integer
타입으로 선언. - 선언된 타입 파라미터에 따라 setItem() 의 값의 타입 일치를 컴파일러가 체크.
실행결과
MyItem
20201121
제네릭 고급 사용
제네릭 메서드
이번에는 메서드의 선언에 제네릭이 사용되는 형태를 살펴봅니다. 메서드의 인자 혹은 리턴에 제네릭이 사용될 수 있으며 다양한 타입을 처리해야 하는 경우 유용하게 활용할 수 있습니다.
앞에서 만든 Storage
public <T> void print(Storage<T> storage) {
}
- 인자에 제네릭 클래스를 사용한 경우 메서드 앞에
를 붙여 주어야 함.
리턴타입 역시 제네릭 클래스를 사용할 수 있습니다.
public List<String> getList() {
}
인자와 리턴이 모두 제네릭을 가지는 경우에는 다소 복잡할 수 있습니다.
public <T> List<Character> convert(Storage<T> storage) {
}
- 다음에 나오는 와일드 카드 사용도 가능.
와일드 카드
제네릭 타입을 사용할때 발생할 수 있는 문제점으로 특정 제네릭 타입을 인자로 받는 메서드를 구현하는 상황을 예로 들 수 있습니다. 앞에서 만든 Storage 를 인자로 하는 메서드를 살펴보도록 합니다.
public void print(Storage<String>) {
..
}
인자로 String 타입 파라미터를 가지는 Storage 클래스가 지정되어 있기 때문에 Storage
이 경우 와일드 카드를 사용해 사용할 수 있는 타입에 유연성을 부여하는 방법이 있습니다.
public void print(Storage<? extends Storage) {
}
- ?는 사실상 Object 클래스라고 볼 수 있음.
- extends , super 를 통해 올수 있는 타입의 관계를 특정할 수 있음.
- ? extends T : T와 그 자손만 가능.
- ? super T : T와 그 부모만 가능.
» 실습: 제네릭 메서드 및 와일드 카드 사용 예제
실습개요
인자 및 리턴타입에 제네릭 클래스를 사용하는 구조를 익히고 와일드 카드를 이용해 제네릭 타입을 제한하는 방법을 배우기 위한 실습 예제 입니다.
소스코드
import java.util.ArrayList;
import java.util.List;
public class GenericsTest2 {
public <T> List<Character> convert(Storage<T> storage) {
ArrayList<Character> list = new ArrayList<>();
String s = String.valueOf(storage.getItem());
int size = s.length();
for (int i = 0; i < size; i++) {
list.add(s.charAt(i));
}
return list;
}
public static void main(String[] args) {
Storage<String> s1 = new Storage<>();
s1.setItem("MyItem");
Storage<Integer> s2 = new Storage<>();
s2.setItem(20201121);
GenericsTest2 gt2 = new GenericsTest2();
System.out.println(gt2.convert(s1));
System.out.println(gt2.convert(s2));
}
}
- 앞에서 만든 Storage
클래스를 활용. - 2개의 서로 다른 타입을 가지는 Storage 인스턴스 생성.
- convert() 메서드는 Storage
타입을 받아 getItem() 결과를 문자열로 변환. - 문자열을 Character List 로 변환해서 리턴.
실행결과
[M, y, I, t, e, m]
[2, 0, 2, 0, 1, 1, 2, 1]
참고 자료
- 오라클 자바 홈페이지: http://java.oracle.com
- Introduction to Java Programming-IBM : https://www.ibm.com/developerworks
- Java Tutorial for Complete Beginners: https://www.udemy.com