여러 상수들을 보다 편리하게 선언할 수 있는 문법요소이다.
예전의 방식
public static final int SPRING = 1;
public static final int SUMMER = 2;
public static final int FALL = 3;
public static final int WINTER = 4;
enum을 사용하면 개선 가능
enum Seasons { SPRING, SUMMER, FALL, WINTER }
enum 열거형이름 { 상수명1, 상수명2, 상수명3, ...}
enum으로 정의한 상수는 switch문에서도 사용이 가능하다.
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();
}
public class Main {
public static void main(String[] args) {
Seasons seasons = Seasons.SPRING;
switch (seasons) {
case Seasons.SPRING:
System.out.println("봄");
break;
case Seasons.SUMMER:
System.out.println("여름");
break;
case Seasons.FALL:
System.out.println("가을");
break;
case Seasons.WINTER:
System.out.println("겨울");
break;
}
}
}
/*
출력값
java: incompatible types: Seasons cannot be converted to int
*/
이 코드에서
seansons
사용자 정의 타입이기 때문에 에러가 발생한다
switch
문은char, byte, short, int, Character, Byte, Integer, String, enum
타입만 가능하다.
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;
}
}
}
//출력값
봄
이처럼 enum을 사용하면 정상적으로 작동
정리하자면, 자바에서 열거형은 여러 상수들을 보다 편리하게 선안하고 관리할 수 있게하며, 상수명의 중복을 피하고, 타입에 대한 안정성을 보장
또한 더 간결하고 가독성이 좋은 코드를 작상할 수 있고, switch 문에서도 작동이 가능
리턴 타입 | 메소드(매개변수) | 설명 |
---|---|---|
String | name() | 열거 객체가 가지고 있는 문자열을 리턴하며, 리턴되는 문자열은 열거타입을 정의할 때 사용한 상수 이름과 동일하다. |
int | ordinal() | 열거 객체의 순번( 0부터 시작 )을 리턴한다. |
int | compareTo(비교값) | 주어진 매개값과 비교해서 순번 차이를 리턴한다. |
열거 타입 | valueOf(String name) | 주어진 문자열의 열거 객체를 리턴한다. |
열거 배열 | values() | 모든 열거 객체들을 배열로 리턴한다. |
//예제
enum Level {
LOW, // 0
MEDIUM, // 1
HIGH // 2
}
public class EnumTest {
public static void main(String[] args) {
Level level = Level.MEDIUM;
Level[] allLevels = Level.values();
for(Level x : allLevels) {
System.out.printf("%s=%d%n", x.name(), x.ordinal());
}
Level findLevel = Level.valueOf("LOW");
System.out.println(findLevel);
System.out.println(Level.LOW == Level.valueOf("LOW"));
switch(level) {
case LOW:
System.out.println("낮은 레벨");
break;
case MEDIUM:
System.out.println("중간 레벨");
break;
case HIGH:
System.out.println("높은 레벨");
break;
}
}
}
//출력값
LOW=0
MEDIUM=1
HIGH=2
LOW
true
중간 레벨
타입을 구체적으로 지정하는 것이 아니라, 추후에 지정할 수 있도록 일반화해두는 것을 의미한다.
-> 작성한 클래스 또는 메서드의 코드가 특정 데이터 타입에 얽매이지 않게 해둔 것을 의미한다.
제네릭이 사용된 클래스를 제네릭 클래스라고 한다.
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;
}
}
여기서
T
를 타입 매개변수라고 한다.
class Basket<T> {
private T item1; // O
static T item2; // X
}
클래스 변수에는 타입 매개변수를 사용할 수 없다.
클래스 변수에 타입 매개변수를 사용할 수 있다면 클래스 변수의 타입이 인스턴스 별로 달라지게 된다.
즉, Basket<String>
으로 만든 인스턴스와, Basket<Integer>
로 만든 인스턴스가 공유하는 클래스 변수의 타입이 서로 달라지게 되어, 클래스 변수를 통해 같은 변수를 공유하는 것이 아니게 된다.
타입 매개변수에 치환될 타입으로 기본 타입을 지정할 수 없다.
Basket<String> basket1 = new Basket<String>("Hello");
Basket<Integer> basket2 = new Basket<Integer>(10);
Basket<Double> basket3 = new Basket<Double>(3.14);
new Basket<...>
은 생략 가능
Basket<String> basket1 = new Basket<>("Hello");
Basket<Integer> basket2 = new Basket<>(10);
Basket<Double> basket2 = 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;
}
}
class Main {
public static void main(String[] args) {
Basket<Flower> flowerBasket = new Basket<>();
flowerBasket.setItem(new Rose()); // 다형성 적용
flowerBasket.setItem(new RosePasta()); // 에러
}
}
Basket
클래스는 타입을 지정하는 데에 있어 제한이 없다.
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;
}
}
class Main {
public static void main(String[] args) {
// 인스턴스화
Basket<Rose> roseBasket = new Basket<>();
Basket<RosePasta> rosePastaBasket = new Basket<>();
}
}
단, 아래 코드처럼 매개변수를 선언하면, Basket
클래스를 인스턴스화할 때 타입으로 Flower
클래스의 하위 클래스만 지정하도록 제한된다.
class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }
class Basket<T extends Flower> {
private T item;
...
}
class Main {
public static void main(String[] args) {
// 인스턴스화
Basket<Rose> roseBasket = new Basket<>();
Basket<RosePasta> rosePastaBasket = new Basket<>(); // 에러
}
}
특정 클래스를 상속받으면서 동시에 특정 인터페이스를 구ㅕㄴ한 클래스만 타입으로 지정할 수 있도록 제한하려면 아래와 같이&
를 사용하여 코드를 작성해준다.
interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }
class Basket<T extends Flower & Plant> { // (1)
private T item;
...
}
class Main {
public static void main(String[] args) {
// 인스턴스화
Basket<Flower> flowerBasket = new Basket<>();
Basket<Rose> roseBasket = new Basket<>();
}
}
클래스 내부의 특정 메서드만 제네릭으로 선언한 것
class Basket {
...
public <T> void add(T element) {
...
}
}
제네릭 메서드의 타입 매개변수는 제네릭 클래스의 타입 매개변수와 별개의 것이다.
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) {
...
}
}
제네릭 메서드는 메서드가 호출되는 시점에서 제네릭 타입이 결정되므로, 제네릭 메서드를 정의하는 시점에서는 실제 어떤 타입이 입력되는지 알 수 없다.
class Basket {
public <T> void print(T item) {
System.out.println(item.length()); // 불가
}
}
자바의 최상위 클래스인 Object
클래스의 메서드는 사용 가능하다.
ex) equals()
, toString()
class Basket {
public <T> void getPrint(T item) {
System.out.println(item.equals("Kim coding")); // 가능
}
}
어떠한 타입으로든 대체될 수 있는 타입 파라미터를 의미한다.
일반적으로는 extends
와 super
키워드를 조합하여 사용한다.
<? extends T>
<? super T>
<? 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;
}
}
휴대폰별로 사용할 수 있는 기능을 분류해보자.
call
: 모든 휴대폰에서 사용 가능? extedns Phone
으로 타입에 제한faceId
: 아이폰만 사용 가능? extends IPhone
으로 타입에 제한samsungPay
: 삼성 휴대폰에서만 사용 가능? extends Galaxy
으로 타입에 제한recordVoice
: 아이폰을 제외한 안드로이드 휴대폰에서만 사용 가능? super Galaxy
으로 타입에 제한class PhoneFunction {
public static void call(User<? extends Phone> user) {
System.out.println("-----------------------------");
System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
System.out.println("모든 Phone은 통화를 할 수 있습니다.");
}
public static void faceId(User<? extends IPhone> user) {
System.out.println("-----------------------------");
System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
System.out.println("IPhone만 Face ID를 사용할 수 있습니다. ");
}
public static void samsungPay(User<? extends Galaxy> user) {
System.out.println("-----------------------------");
System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
System.out.println("Galaxy만 삼성 페이를 사용할 수 있습니다. ");
}
public static void recordVoice(User<? super Galaxy> user) {
System.out.println("-----------------------------");
System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
System.out.println("안드로이드 폰에서만 통화 녹음이 가능합니다. ");
}
}
여기서 호출을 해보면
public class Example {
public static void main(String[] args) {
PhoneFunction.call(new User<Phone>(new Phone()));
PhoneFunction.call(new User<IPhone>(new IPhone()));
PhoneFunction.call(new User<Galaxy>(new Galaxy()));
PhoneFunction.call(new User<IPhone12Pro>(new IPhone12Pro()));
PhoneFunction.call(new User<IPhoneXS>(new IPhoneXS()));
PhoneFunction.call(new User<S22>(new S22()));
PhoneFunction.call(new User<ZFlip3>(new ZFlip3()));
System.out.println("\n######################################\n");
// PhoneFunction.faceId(new User<Phone>(new Phone())); // X
PhoneFunction.faceId(new User<IPhone>(new IPhone()));
PhoneFunction.faceId(new User<IPhone12Pro>(new IPhone12Pro()));
PhoneFunction.faceId(new User<IPhoneXS>(new IPhoneXS()));
// PhoneFunction.faceId(new User<Galaxy>(new Galaxy())); // X
// PhoneFunction.faceId(new User<S22>(new S22())); // X
// PhoneFunction.faceId(new User<ZFlip3>(new ZFlip3())); // X
System.out.println("\n######################################\n");
// PhoneFunction.samsungPay(new User<Phone>(new Phone())); // X
// PhoneFunction.samsungPay(new User<IPhone>(new IPhone())); // X
// PhoneFunction.samsungPay(new User<IPhone12Pro>(new IPhone12Pro())); // X
// PhoneFunction.samsungPay(new User<IPhoneXS>(new IPhoneXS())); // X
PhoneFunction.samsungPay(new User<Galaxy>(new Galaxy()));
PhoneFunction.samsungPay(new User<S22>(new S22()));
PhoneFunction.samsungPay(new User<ZFlip3>(new ZFlip3()));
System.out.println("\n######################################\n");
PhoneFunction.recordVoice(new User<Phone>(new Phone()));
// PhoneFunction.recordVoice(new User<IPhone>(new IPhone())); // X
// PhoneFunction.recordVoice(new User<IPhone12Pro>(new IPhone12Pro())); // X
// PhoneFunction.recordVoice(new User<IPhoneXS>(new IPhoneXS())); // X
PhoneFunction.recordVoice(new User<Galaxy>(new Galaxy()));
// PhoneFunction.recordVoice(new User<S22>(new S22())); // X
// PhoneFunction.recordVoice(new User<ZFlip3>(new ZFlip3())); // X
}
}
주석으로 처리된 부분은 에러로 감지되는 부분이다.
recordVoice
는 S22
와 ZFlip3
를 타입으로 지정하면서 호출할 때 왜 에러가 발생할까?
<? super Galaxy>
는 상속 계층도 상에서 Galaxy
및 Galaxy
보다 위에 있는 상위 클래스만 타입으로 지정할 수 있게 제한