열거형이란 여러 상수를 편리하게 선언할 수 있게 만들어진 문법 요소다.
주로 관련있는 내용들을 모아서 간편하게 관리할 때 사용한다.
예를 들면, 아래와 같은 내용을 하나로 묶어 관리할 수 있다.
예전에는 아래와 같이 여러 상수를 정의했다.
public static final int SPRING = 1;
public static final int SUMMER = 2;
public static final int FALL = 3;
public static final int WINTER = 4;
public static final int DJANGO = 1;
public static final int SPRING = 2; // 계절의 SPRING과 중복 발생!
public static final int NEST = 3;
public static final int EXPRESS = 4;
이렇게 상수를 할당하면 종종 상수명이 중복되는 경우가 발생할 수 있다.
중복상수명의 문제를 피하기 위해 인터페이스를 사용해 일차적으로 해결할 수도 있지만, 인터페이스를 사용한 방법은 타입 안정성이라는 새로운 문제가 발생한다.
간단하게 말하자면, Seasons의 SPRING과 Frameworks의 SPRING은 다른 개념이지만 이 둘을 비교하면 에러가 발생하지 않아 타입 안정성이 떨어지는 것이다.
이러한 문제를 해결하기 위해 아래와 같이 서로 다른 객체로 만들어주는 것이 좋다.
class Seasons {
public static final Seasons SPRING = new Seasons();
public static final Seasons SUMMER = new Seasons();
public static final Seasons FALL = new Seasons();
public static final Seasons WINTER = new Seasons();
}
class Frameworks {
public static final Frameworks DJANGO = new Frameworks();
public static final Frameworks SPRING = new Frameworks();
public static final Frameworks NEST = new Frameworks();
public static final Frameworks EXPRESS = new Frameworks();
}
상수명 중복과 타입 안정성 문제를 모두 해결했지만, 코드가 길고 사용자 정의 타입으로 switch문에 사용할 수 없는 문제가 있다.
이 같은 문제들을 모두 해결하기 위해 만들어진 것이 바로 enum이다.
enum Seasons { SPRING, SUMMER, FALL, WINTER }
enum Frameworks { DJANGO, SPRING, NEST, EXPRESS }
enum을 사용하면 앞에서 발생했던 문제들을 모두 해결할 수 있고 코드 가독성 또한 좋게 할 수 있다. 또한, switch문에서도 사용이 가능하다.
enum Seasons {SPRING, SUMMER, FALL, WINTER}
public class Main {
public static void main(String[] args) {
Seasons seasons = Seasons.SPRING;
switch (seasons) {
case SPRING:
System.out.println("봄");
break;
case SUMMER:
System.out.println("여름");
break;
case FALL:
System.out.println("가을");
break;
case WINTER:
System.out.println("겨울");
break;
}
}
}
정리하자면, 열거형은 여러 상수들을 편리하게 선언하고 관리할 수 있으며 상수명의 중복을 피하고 타입 안정성을 보장한다.
또한, 간결하고 가독성이 좋은 코드를 작성할 수 있고 switch문에서도 작동이 가능하다.
열거형을 정의하는 방법은 아래와 같다. 코드 블럭 안에 선언하고자 하는 상수의 이름을 나열하면 된다.
각각의 상수들에는 따로 값을 지정하지 않아도 자동으로 0부터 시작하는 정수값이 할당된다.
enum 열거형이름 { 상수명1, 상수명2, 상수명3, ...}
enum Seasons { SPRING, SUMMER, FALL, WINTER }
public class EnumExample {
public static void main(String[] args) {
Seasons favoriteSeason = Seasons.WINTER;
System.out.println(favoriteSeason); //WINTER
}
}
가장 좋아하는 계절이라는 의미의 참조변수 favoriteSeason
에 Seasons.SPRING
을 담아보았다.
아래 Basket 클래스는 오로지 String 타입의 데이터만을 저장할 수 있는 인스턴스를 만들 수 있다. 다양한 타입의 데이터를 저장할 수 있는 객체를 만들고자 한다면, 각 타입별로 별도의 클래스를 만들어야 한다.
class Basket {
private String item;
Basket(String item) {
this.item = item;
}
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
}
아래와 같이 제네릭을 사용한다면, 단 하나의 Basket 클래스만으로 모든 타입의 데이터를 저장할 수 있는 인스턴스를 만들 수 있다.
class Basket<T> {
private T item;
public Basket(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
“Basket 클래스 내의 T를 String으로 바꿔라”
위의 Basket 클래스는 다음과 같이 인스턴스화 할 수 있다.
Basket<String> basket1 = new Basket<String>("기타줄");
즉, 제네릭이란 타입을 구체적으로 지정하는 것이 아니라 나중에 지정할 수 있게 일반화하는 것이다. 작성한 클래스나 메서드의 코드가 특정 데이터 타입에 얽매이지 않게 한 것을 의미한다.
제네릭 클래스에서 타입 매개변수를 임의의 타입으로 사용할 수 있다고 했는데, 클래스 변수에는 타입 매개변수를 사용할 수 없다.
class Basket<T> {
private T item1; //O
static T item2; //X
}
클래스 변수는 모든 인스턴스가 공유하는 변수다. 만일, 클래스 변수에 타입 매개변수를 사용할 수 있다면 클래스 변수의 타입이 인스턴스 별로 달라지게 된다.
Basket<String>
으로 만든 인스턴스와 Basket<Integer>
로 만든 인스턴스가 공유하는 클래스 변수의 타입이 서로 달라져서, 클래스 변수를 통해 같은 변수를 공유하는 것이 아니게 된다. 따라서 static이 붙은 변수 또는 메서드에는 타입 매개변수를 사용할 수 없다.
제네릭 클래스는 멤버를 구성하는 코드에 특정한 타입이 지정되지 않은 클래스이므로, 제네릭 클래스를 인스턴스화할 때에는 의도하고자 하는 타입을 아래와 같이 지정해야 한다.
단, 타입 매개변수에 치환될 타입으로 기본 타입을 지정할 수 없다.
예를 들어 int
, double
과 같은 원시 타입을 지정해야 하는 맥락에서는 Integer
, Double
과 같은 래퍼 클래스를 활용해야 한다.
Basket<String> basket1 = new Basket<String>("Hello");
Basket<Integer> basket2 = new Basket<Integer>(10);
Basket<Double> basket3 = new Basket<>(3.14); //구체적인 타입 생략
class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }
class Basket<T> {
private T item;
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
public static void main(String[] args) {
Basket<Flower> flowerBasket = new Basket<>();
flowerBasket.setItem(new Rose()); //다형성 적용
flowerBasket.setItem(new RosePasta()); //에러발생
}
new Rose()
를 통해 생성된 인스턴스는 Rose
타입이고
Rose
클래스는 Flower
클래스를 상속받고 있으므로, Basket<Flower>
의 item에 할당될 수 있다.
Basket<Flower>
은 item
의 타입을 Flower
로 지정하는 것이고, Flower
클래스는 Rose
클래스의 상위 클래스이기 때문입니다.
반면, new RosePasta()
를 통해 생성된 인스턴스는 RosePasta
타입이며, RosePasta
클래스는 Flower
클래스와 아무런 관계가 없기 때문에 flowerBasket
의 item
에 할당될 수 없다.
class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }
class Basket<T extends Flower> {
private T item;
...
}
public static void main(String[] args) {
// 인스턴스화
Basket<Rose> roseBasket = new Basket<>();
Basket<RosePasta> rosePastaBasket = new Basket<>(); // 에러
}
타입 매개변수를 선언할 때, 위와 같이 코드를 작성하면 Basket
클래스를 인스턴스화할 때 타입으로 Flower
클래스의 하위 클래스만 지정하도록 제한된다.
interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }
class Basket<T extends Flower & Plant> { // (1)
private T item;
...
}
public static void main(String[] args) {
// 인스턴스화
Basket<Flower> flowerBasket = new Basket<>();
Basket<Rose> roseBasket = new Basket<>();
}
만일 특정 클래스를 상속받는 동시에 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한하려면 위와 같이 &
를 사용해 작성하면 된다.
다만, 이러한 경우 클래스를 인터페이스보다 앞에 위치시켜야 한다.
클래스 전체를 제네릭으로 선언할 수도 있지만, 클래스 내부의 특정 메서드만 제네릭으로 선언할 수도 있는데, 이를 제네릭 메서드라 한다.
class Basket<T> { //1 : 여기에서 선언한 타입 매개변수 T와
...
public <T> void add(T element) { //2 : 여기에서 선언한 타입 매개변수 T는 서로 다르다.
...
}
}
제네릭 메서드의 타입 매개변수는 제네릭 클래스의 타입 매개변수와 별개이다. 타입이 지정되는 시점이 서로 다르기 때문에, 클래스명 옆에서 선언한 타입 매개변수는 클래스가 인스턴스화 될 때 타입이 지정된다.
그러나 제네릭 메서드의 타입 지정은 메서드가 호출될 때 이루어진다.
Basket<String> basket = new Bakset<>(); //위 예제의 1의 T가 String으로 지정
basket.<Integer>add(10); //위 예제의 2의 T가 Integer로 지정
basket.add(10); //타입 지정 생략 가능
또한 타입 매개변수는 static
메서드에서도 선언하여 사용할 수 있다.
class Basket {
...
static <T> int setPrice(T element) {
...
}
}
와일드 카드는 어떠한 타입이로든 대체 가능한 타입 파라미터를 의미한다. ?
로 와일드 카드를 사용할 수 있다.
일반적으로 extends
와 super
키워드를 조합하여 사용한다.
<? extends T>
는 와일드카드에 상한 제한을 두는 것으로서, T와 T를 상속받는 하위 클래스 타입만 타입 파라미터로 받을 수 있도록 지정한다.
<? super T>
는 와일드카드에 하한 제한을 두는 것으로, T와 T의 상위 클래스만 타입 파라미터로 받게 한다.
extends
및 super
키워드와 조합하지 않은 와일드카드<?>
는 <? extends Object>
와 같다.
위의 상속 계층도는 아래 코드와 같이 정의할 수 있다.
class Phone {}
class IPhone extends Phone {}
class Galaxy extends Phone {}
class IPhone12Pro extends IPhone {}
class IPhoneXS extends IPhone {}
class S22 extends Galaxy {}
class ZFlip3 extends Galaxy {}
class User<T> {
public T phone;
public User(T phone) {
this.phone = phone;
}
}