1. Generics
1.1 지네릭스 (Generics)
Generics : 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능.
- 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높인다. 의도하지 않은 타입의 객체가 저장되는 것을 막고 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형 변환되어 발생할 수 있는 오류를 줄여준다.
- 형변환의 번거로움이 줄어든다.
1.2 지네릭 클래스의 선언
클래스와 메서드에 선언할 수 있다.
클래스에 선언하는 지네릭 타입
class Box {
Object item;
void setItem(Object Item) {this.item = item;}
Object getItem() {return item;}
}
->
class Box<T> {
T item;
void setItem(T item) {this.item = item;}
T getItem() {return item;}
}
Box<T> 에서 T를 타입 변수(type variable)라고 하며 "Type"의 첫 글자에서 따온 것이다. 타입 변수는 T가 아닌 다른 것을 사용해도 된다. (ex. ArrayList<E>에서 타입 변수 E는 element(요소)의 첫 글자를 따서 사용) 타입 변수가 두개인 경우는 Map<K, V> 처럼 콤마를 이용해서 사용해주면 된다. 의미에 맞게 글자를 사용하는 것이 좋다. 글자는 달라서 임의의 참조형 타입이라는 것은 동일하다.
Box<String> b = new Box<String>();
b.setItem(new Object());
b.setItem("ABC");
String item = (String) b.getItem();
위의 예시에서 마지막 줄에 (String)이라는 형 변환은 불필요하다. T 대신에 String 타입을 지정해줬기 때문이다. 지네릭 클래스이지만 이전 버전과의 호환성을 위해 아래와 같이 (기존 코드처럼) 작성할 수도 있다. 하지만 안전하지 않다는 경고가 뜬다.
Box b = new Box(); // T는 Object로 간주된다.
b.setItem("ABC"); // 경고. unchecked or unsafe operation
b.setItem(new Object()); // 경고. unchecked or unsafe operation
Box<Object> b = new Box<Object>();
b.setItem("ABC"); // 경고발생 안 함
b.setItem(new Object()); // 경고발생 안 함
지네릭스의 용어
- Box<T> : 지네릭 클래스, 'T의 Box' 또는 'T Box'라고 읽는다.
- T : 타입 변수 또는 타입 매개변수
- Box : 원시 타입 (raw type)
컴파일 후에는 Box<String>과 Box<Integer>는 이들의 원시 타입인 Box로 바뀐다.
지네릭스의 제한
모든 객체에 대해 동일하게 동작해야 하는 static 멤버에는 타입 변수 T를 사용할 수 없다. T는 인스턴스 변수로 간주되기 때문이다. static 멤버는 인스턴스 변수를 참조할 수 없다.
class Box<T>{
static T item; // 에러
static int compare(T t1, T t2) {...} //에러
...
}
static 멤버의 경우 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다.
또한 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 그 이유는 new 연산자 때문이다.
class Box<T> {
T[] itemArr; // OK. T 타입의 배열을 위한 참조변수
T[] toArray() {
T[] tmpArr = new T[itemArr.length]; // 에러.
}
}
new 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. instanceof 연산자도 동일한 이유로 T를 피연산자로 사용할 수 없다.
1.3 제네릭 클래스의 객체 생성과 사용
class Box<T> {
ArrayList<T> list = new ArrayList<T> ();
void add(T item) {list.add(item);}
T get(int i) {return list.get(i);}
ArrayList<T> getList() {return list;}
int size() {return list.size();}
public String toString() {return list.toStirng();}
}
위와 같은 예시가 있다고 하자. Box<T>의 객체를 생성할 때는 다음과 같이 한다. 참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다. 상속 관계에 있어도 마찬가지이다. Apple이 Fruit의 자손이라고 하면 다음과 같다.
Box<Fruit> appleBox = new Box<Apple>(); // 에러
Box<Apple> appleBox = new Box<Apple>(); // 다형성으로 OK
Box<Apple> appleBox2 = new Box<>(); //JDK 1.7부터 생략 가능.
appleBox.add(new Apple()); //OK
appleBox.add(new Grape()); // 에러
1.4 제한된 지네릭 클래스
제네릭 타입에 extends를 사용하면 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.
class FruitBox<T extends Fruit> { //Fruit의 자손 타입만 지정 가능
...
}
클래스가 아닌 인터페이스를 구현해야 한다는 제약이 필요하다고 해도 implements가 아닌 extends를 사용한다.
클래스 Fruit의 자손이면서 Eatable의 구현체가 사용되어야 한다면 & 기호로 연결된다.
class FruitBox<T extends Fruit & Eatable> {...}
1.5 와일드 카드
매개변수에 과일박스를 대입하면 주스를 만들어서 반환하는 Juicer 클래스가 있고 이 클래스에는 과일을 주스로 만들어서 반환하는 makeJuice()라는 static 메서드가 다음과 같이 정의되어 있다.
class Juicer {
static Juice makeJuice(FruitBox<Fruit> box) { // <Fruit>으로 지정
String tmp = " ";
for (Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
}
Juicer 클래스는 지네릭 클래스도 아니고 static 메서드에서는 T를 매개변수로 사용할 수 없다. 따라서 지네릭 타입을 FruitBox<Fruit> 로 고정해 놓으면 makeJuice(FruitBox<Apple>)는 실행할 수 없다. 따라서 여러 타입의 매개변수를 갖는 makeJuice()를 만드려면 오버라이딩 해야 한다. 하지만 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않는다. (지네릭 타입은 컴파일러가 컴파일 할 때만 사용하고 제거해 버림) 따라서 메서드 중복 정의를 해야 한다.
이 때 사용할 수 있는 것이 와일드 카드이다. 와일드 카드는 기호 '?'로 표현하는데 와일드 카드는 어떠한 타입도 될 수 있다. Object 타입과는 다르게 extends, super로 상한과 하한을 정할 수 있다.
- <? extends T> : 와일드 카드의 상한 제한. T와 그 자손들만 가능
- <? super T> : 와일드 카드의 하한 제한. T와 그 그 조상들만 가능
- <?> : <? extends Object>와 동일, 제한 없음, 모든 타입이 가능.
1.6 지네릭 메서드
제네릭 메서드 : 메서드의 선언부에 지네릭 타입이 선언된 메서드 (ex. Collections.sort())
지네릭 타입의 선언 위치는 반환 타입 바로 앞이다. 지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것이다. 같은 타입 문자 T를 사용해도 같은 것이 아니다.
class FruitBox<T> {
...
static <T> void sort(List<T> list, Comparator<? super T> c){
...
}
}
static 멤버에는 타입 매개 변수를 사용할 수 없지만, 이처럼 메서드에 지네릭 타입을 선언하고 사용하는 것은 가능하다. 메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 된다. 이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이기 때문에 메서드가 static이건 아니건 상관이 없다.
매개변수의 타입이 복잡할 때 제네릭 메서드가 유용하다. 다음과 같은 경우를 쉽게 선언해줄 수 있다.
public static void printAll(ArrayList<? extends Product> list, ArrayList<? extends Product> list2) {
for (Unit u : list) {
System.out.println(u);
}
}
->
public static <T extends Product> void printAll(ArrayList<T> list, ArrayList<T> list2){
for(Unit u : list) {
System.out.println(u);
}
}
복잡하게 선언된 지네릭 메서드 하나를 예를 들어보면 다음과 같다.
public static <T extends Comparable<? super T>> void sort(List<T> list)
== public static <T extends Comparable<T>> void sort (List<T> list)
- 타입 T를 요소로 하는 List를 매개변수로 허용한다.
- T는 Comparable을 구현한 클래스여야 하며 (T extends Comparable) T 또는 그 조상의타입을 비교하는 Comparable 이어야 한다는 것 (Comparable<? super T>) 을 의미한다. 만일 T가 Student 이고, Person의 자손이라면, <? super T>는 Student, Person, Object가 모두 가능하다.
1.7 제네릭 타입의 형변환
- 제네릭과 non-generic 타입 간의 형 변환은 항상 가능하다. (경고는 발생한다)
- 대입된 타입이 다른 제네릭 타입 간에는 형 변환이 불가능하다.
- Box<String>이 Box<? extends Object>로 형변환이 된다. (다형성 적용 가능)
- 반대의 경우 경고가 발생하지만 가능하다.
1.8 제네릭 타입의 제거
컴파일러는 제네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형 변환을 넣어준다. 그리고 제네릭 타입을 제거한다. 즉, *.class 에는 제네릭 타입에 대한 정보가 없다.
제네릭 타입의 기본적인 제거 과정은 다음과 같다.
1. 제네릭 타입의 경계를 제거한다.
<T extends Fruit>라면 T는 Fruit로 치환한다. <T> 인 경우는 Object로 치환한다. 그리고 클래스 옆의 선언은 제거된다.
2. 제네릭 타입을 제거한 후에 타입이 일치하지 않으면 형변환을 추가한다.
List의 get()은 Object 타입을 반환하므로 형변환이 필요하다. 와일드 카드가 포함되어 있는 경우에는 적절한 타입으로의 형변환이 추가된다.
2. 열거형 (Enums)
2.1 열거형이란?
열거형 : 서로 관련된 상수를 편리하게 선언하기 위한 것으로 여러 상수를 정의할 때 사용하면 유용하다.
열거형이 갖는 값과 타입을 모두 관리한다는 점에서 C언어에서 보다 논리적인 오류를 줄일 수 있다.
2.2 열거형의 정의와 사용
// 정의방법
enum 열거형이름 {상수명1, 상수명2, ...}
열거형에 정의된 상수를 사용하는 방법은 '열거형이름.상수명'이다. (클래스의 static변수를 참조하는 것과 동일)
열거형 상수간의 비교에는 "=="를 사용할 수 있다. (equals()가 아니라 "=="로 비교가 가능하다는 것은 그만큼 빠른 성능을 제공한다는 것) 그러나 비교 연산자는 사용할 수 없고 compareTo()는 사용가능하다.
모든 열거형의 조상 - java.lang.Enum
열거형 Direction에 정의된 모든 상수를 출력하려면 다음과 같이 하면 된다.
Direction[] dArr = Direciton.values();
for (Direction d: dArr)
System.out.printf("%s = %d\n", d.name(), d.ordinal());
- values() : 열거형의 모든 상수를 배열에 담아 반환
- ordinal() : 열거형 상수가 정의된 순서를 정수로 반환 (0부터 시작)
- Class<E> getDeclaringClass() : 열거형의 Class 객체 반환
- String name() : 열거형 상수의 이름을 문자열로 반환
- int ordinal() : 열거형 상수가 정의된 순서를 반환 (0부터 시작)
- T valueOf(Class<T> enumType, String name) : 지정된 열거형에서 name과 일치하는 열거형 상수를 반환
2.3 열거형에 멤버 추가하기
Enum 클래스에 정의된 ordinal()이 열거형 상수가 정의된 순서를 반환하지만, 이 값은 열거형 상수의 값으로 사용하지 않는 것이 좋다. (내부적인 용도로만 사용)
불연속적인 경우에는 열거형 상수의 이름 옆에 원하는 값을 괄호()와 함께 적어주면 된다.
enum Direction {
EAST(1), SOUTH(5), WEST(-1), NORTH(10);
private final int value;
Direction (int value) { this.value = value; }
public int getValue() { return value; }
}
value는 열거형 상수의 값을 저장하기 위한 것이므로 final을 붙였다. 그리고 외부에서 이 값을 얻을 수 있게 getValue()도 추가한다.
필요 시 하나의 열거형 상수에 여러 값을 지정할 수도 있지만 그에 맞게 인스턴스 변수와 생성자 등을 새로 추가해주어야 한다.
열거형에 추상 메서드 추가하기
enum Transportation {
BUS(100), TRAIN(150), SHIP(100), AIRPLANE(300);
private final int BASIC_FARE;
private Transportation(int basicFare) {
BASIC_FARE = basicFare;
}
int fare() { // 운송 요금을 반환
return BASIC_FARE;
}
}
이런 예제가 있을 때, 운송 수단의 종류 별로 상수를 정의하고 있으며, 각 운송수단에는 기본 요금이 책정되어 있다. 여기에 거리에 따라 요금을 계산하는 방식이 각 운송 수단마다 다를 것이다. 이럴 때 추상메서드 fare(int distance)를 선언하면 각 열거형 상수가 이 추상 메서드를 반드기 구현해야 한다.
enum Transportation {
BUS(100) {
int fare(int distance) { return distance * BASIC_FARE; }
},
TRAIN(150) { int fare(int distance) { return distance * BASIC_FARE; } },
SHIP(100) { int fare(int distance) { return distance * BASIC_FARE; } },
AIRPLANE(300) { int fare(int distance) { return distance * BASIC_FARE; } };
abstract int fare(int distance); // 거리에 따른 요금을 계산하는 추상 메서드
protected final int BASIC_FARE; // protected로 해야 각 상수에서 접근 가능
private Transportation(int basicFare) {
BASIC_FARE = basicFare;
}
int fare() { // 운송 요금을 반환
return BASIC_FARE;
}
}
2.4 열거형의 이해
enum Direction { EAST, SOUTH, WEST, NORTH }
열거형 상수 하나하나가 Direction 객체이다. Direction 클래스의 static 상수 EAST, SOUTH, WEST, NORTH의 값은 객체의 주소이고, 이값은 바뀌지 않는 값이므로 '=='로 비교가 가능하다. 모든 열거형은 추상 클래스 Enum의 자손이다.
3. 애너테이션 (annotation)
3.1 애노테이션이란?
자바는 소스코드에 대한 문서를 따로 만들기보다는 소스코드와 문서를 하나의 파일로 관리하고 있다. /** ~ */ 으로 소스코드에 대한 설명을 적어둔다. 프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것이 바로 애노테이션이다.
주석처럼 프로그래밍 언어에 영향을 미치지 않으면서도 다른 프로그램에게 유용한 정보를 제공할 수 있다는 장점이 있다.
3.2 표준 애노테이션
자바에서 기본적으로 제공하는 애노테이션들은 몇 개 없다. 그나마 이들이 일부는 '메타 애노테이션'으로 다른 애노테이션을 정의하는데 사용된다.
- @Override
- @Deprecated : 앞으로 사용하지 않을 것을 권장하는 대상에 붙인다.
- @SuppressWarnings : 컴파일러의 특정 경고메시지가 나타나지 않게 해준다.
- @SafeVarangs : 제네릭스 타입의 가변인자에 사용한다.
- @FunctionalInterface : 함수형 인터페이스라는 것을 알린다.
3.3 메타 에너테이션
애노테이션을 위한 애노테이션이다. 애노테이션을 정의할 때 적용대상이나 유지기간등을 지정하는데 사용된다.
- @Target : 애너테이션이 적용가능한 대상을 지정하는데 사용. 여러 개의 값을 지정할 때는 배열에서처럼 괄호{}를 사용
- @Retention : 애너테이션이 유지되는 기간. SOURCE- 소스 파일에만 존재 / CLASS - 클래스 파일에 존재. 실행 시 사용 불가 / RUNTIME - 클래스에 존재. 실행 시에 사용 가능
- @Documented : 관련 정보가 javadoc으로 작성한 문서에 포함되도록 한다.
- @Inherited : 애노테이션이 자손클래스에 상속되도록 한다.
- @Repeatable : 보통은 하나의 대상에 한 종류의 애너테이션을 붙이는데 @Repeatable이 붙은 애너테이션은 여러 번 붙일 수 있다.
- @Native : 상수 필드에 붙이는 애너테이션이다.
3.4 애너테이션 타입 정의하기
@Override는 애너테이션, Override는 애너테이션의 타입이다.
애너테이션의 요소
애너테이션 내에 선언된 메서드를 요소라고 한다. 애너테시연도 인터페이스처럼 상수를 정의할 수 있지만 디폴트 메서드는 정의할 수 없다.
java.lang.annotation.Annotation
모든 애너테이션의 조상은 Annotation이다. 그러나 애너테이션은 상속이 허용되지 않으므로 명시적으로 Annotation을 조상으로 지정할 수 없다. (@Interface TestInfo extends Annotation {} 불가)
Annotation은 일반적인 인터페이스로 정의되어 있다. 모든 애너테이션의 조상인 Annotation 인터페이스에 정의된 대로 모든 애너테이션 객체에 대해 equals(), hashCode(), toString()과 같은 메서드를 호출하는 것이 가능하다.
마커 애너테이션
값을 지정할 필요가 없는 경우, 애너테이션의 요소를 하나도 정의하지 않을 수 있다. 이런 것들을 마커 애너테이션이라고 한다.
애너테이션 요소의 규칙
- 요소의 타입은 기본형, String, enum, 애너테이션, Class만 허용된다.
- () 안에 매개변수를 선언할 수 없다.
- 예외를 선언할 수 없다.
- 요소를 타입 매개변수로 정의할 수 없다.
'TIL > Java | Spring Boot' 카테고리의 다른 글
[Java] 스터디 8주차_람다와 스트림(Lambda & Stream) (0) | 2022.07.16 |
---|---|
[Java] 스터디 7주차_쓰레드 (Thread) (0) | 2022.07.02 |
[Java] 스터디 5주차_컬렉션 프레임웍 (0) | 2022.05.22 |
[Java] 스터디 4주차_java.lang 패키지와 날짜, 시간, 형식화 (0) | 2022.05.15 |
[Java] 스터디 3주차_예외 처리 (exception handling) (0) | 2022.05.06 |
댓글