실습 - 레스토랑 키오스크 프로그램(객체지향)
💡 레스토랑 키오스크 프로그램 만들기 본 예제는 출력된 안내 사항에 맞게 원하는 메뉴와 수량을 입력하여 주문을 진행하는 프로그램을 만들어 봅니다. (※ 본 예제를 통해 객체지향을 및 가볍게 다뤄보도록 합니다)
내 코드
Kiosk
package com.jungmin.seb.kiosk;
import java.util.Scanner;
public class Kiosk {
private final static MenuItem menuItem1 = new MenuItem("김밥", 1000);
private final static MenuItem menuItem2 = new MenuItem("계란 김밥", 1500);
private final static MenuItem menuItem3 = new MenuItem("충무 김밥", 1000);
private final static MenuItem menuItem4 = new MenuItem("떡볶이", 2000);
// 주문 안내 메시지를 출력하는 메서드를 정의할 수 있습니다.
public void welcome() {
System.out.println("안녕하세요. 김밥천국에 오신 것을 환영합니다.");
System.out.println("=".repeat(66));
}
// 주문할 음식을 선택하는 메서드를 정의 할 수 있습니다.
public MenuItem choice() {
MenuItem menuItem;
boolean isTrue = true;
do {
System.out.println("[안내] 원하시는 메뉴의 번호를 입력하여 주세요. 주문을 끝내시려면 0번을 눌러주세요.");
System.out.println("1) 김밥(1000원) 2) 계란 김밥(1500원) 3) 충무 김밥(1000원) 4) 떡볶이(2000원)");
Scanner sc = new Scanner(System.in);
int menuNum = sc.nextInt();
if (menuNum != 0) {
switch (menuNum) {
case 1:
return menuItem1;
case 2:
return menuItem2;
case 3:
return menuItem3;
case 4:
return menuItem4;
default:
System.out.println("잘못된 숫자를 입력하셨습니다. 다시 입력해 주세요.");
isTrue = false;
}
} else {
break;
}
} while (isTrue == false);
return null;
}
// 주문할 음식의 수량을 입력하는 메서드를 정의할 수 있습니다.
public int count() {
System.out.println("메뉴의 수량을 입력해주세요.");
System.out.println("(※ 최대 주문 가능 수량 : 99)");
Scanner sc = new Scanner(System.in);
int countMenu = sc.nextInt();
if (countMenu < 0 || countMenu > 99) {
System.out.println("잘못된 수량을 입력하셨습니다. 다시 입력해 주세요.");
}
return countMenu;
}
// 음식 주문을 위한 메서드를 정의할 수 있습니다.
public int order(MenuItem menuItem, int count) {
int totalCost = 0;
totalCost += menuItem.getPrice() * count;
return totalCost;
}
// 주문 결과를 출력하는 메서드를 정의할 수 있습니다.
public void output(int totalCost) {
System.out.println("고객님이 주문하신 메뉴의 총 금액은 " + totalCost + "입니다.");
System.out.println("[안내]이용해 주셔서 감사합니다.");
}
}
KioskApplication
package com.jungmin.seb.kiosk;
public class KioskApplication {
public static void main(String[] args) {
// Kiosk 클래스의 기능을 구현 및 활용해서 프로그래밍 요구 사항에 맞는 키오스크 프로그램을 구현하세요.
// MenuItem 클래스는 음식 정보를 표현하고 있으며 MenuItem 클래스를 이용해 주문할 음식 정보를 출력할 수 있습니다.
// 이 외의 필요한 클래스가 있다면 자유롭게 추가해서 여러분들만의 키오스크 프로그램을 만들면 됩니다.
int totalCost = 0;
Kiosk kiosk = new Kiosk();
// Welcome 메시지 출력
kiosk.welcome();
// 메뉴 선택 메시지 출력
MenuItem menu = kiosk.choice();
// 개수 입력
int count = kiosk.count();
totalCost = kiosk.order(menu, count);
// 최종 출력 메시지
if (count < 100) {
kiosk.output(totalCost);
}
}
}
MenuItem
package com.jungmin.seb.kiosk;
public class MenuItem {
private String name;
private int price;
public MenuItem(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
레퍼런스 코드
v1
KioskApplication
package com.jungmin.seb.kioskref.V1;
import java.util.Scanner;
public class KioskApplication {
public static void main(String[] args) {
// Kiosk 클래스의 기능을 구현 및 활용해서 프로그래밍 요구 사항에 맞는 키오스크 프로그램을 구현하세요.
// MenuItem 클래스는 음식 정보를 표현하고 있으며 MenuItem 클래스를 이용해 주문할 음식 정보를 출력할 수 있습니다.
// 이 외의 필요한 클래스가 있다면 자유롭게 추가해서 여러분들만의 키오스크 프로그램을 만들면 됩니다.
Scanner sc = new Scanner(System.in);
// 웰컴 메시지 출력
System.out.println("[안내] 안녕하세요. 김밥천국에 오신 것을 환영합니다.");
System.out.println("--------------------------------");
// 주문 안내 메시지를 출력하는 메서드를 정의할 수 있습니다.
System.out.println("[안내] 원하시는 메뉴의 번호를 입력하여 주세요.");
System.out.println("1) 김밥(1000원) 2) 계란 김밥(1500원) 3) 충무 김밥(1000원) 4) 떡볶이(2000원)");
// 주문할 음식을 선택하는 메서드를 정의 할 수 있습니다.
int menuNumber = 0;
do {
menuNumber = Integer.parseInt(sc.nextLine());
if(menuNumber < 1 || menuNumber > 4) {
System.out.println("[안내] 메뉴에 포함된 번호를 입력하여 주세요.");
System.out.println();
}
} while (menuNumber < 1 || menuNumber > 4);
System.out.println("메뉴가 선택되었습니다.");
// 주문할 음식의 수량을 입력하는 메서드를 정의할 수 있습니다.
int count = 0;
do {
System.out.println("----------------------------------------");
System.out.println("메뉴의 수량을 입력해주세요.");
System.out.println("(※ 최대 주문 가능 수량 : 99)");
count = Integer.parseInt(sc.nextLine());
if (count < 1 || count > 99) {
System.out.println("[경고] " + count + "개는 입력하실 수 없습니다.");
System.out.println("[경고] 수량 선택 화면으로 돌아갑니다.");
}
}while(count < 1 || count > 99);
System.out.println(count);
// 음식 주문을 위한 메서드를 정의할 수 있습니다.
int price = 0;
switch (menuNumber) {
case 1:
price = 1000;
break;
case 2:
price = 1500;
break;
case 3:
price = 1000;
break;
case 4:
price = 2000;
break;
default:
}
int currentOrderPrice = count * price;
// 주문 결과를 출력하는 메서드를 정의할 수 있습니다.
System.out.println("[안내] 주문하신 메뉴의 총 금액은" + currentOrderPrice + "원 입니다.");
System.out.println("[안내] 이용해 주셔서 감사합니다.");
}
}
v2
V1을 클래스 분리
Kiosk
package com.jungmin.seb.kioskref.V2;
import java.util.Scanner;
public class Kiosk {
private static final MenuItem menuItem1 = new MenuItem("김밥", 1000);
private final static MenuItem menuItem2 = new MenuItem("계란 김밥", 1500);
private final static MenuItem menuItem3 = new MenuItem("충무 김밥", 1000);
private final static MenuItem menuItem4 = new MenuItem("떡볶이", 2000);
Scanner sc = new Scanner(System.in);
// 웰컴 메시지 출력
public void welcomePrintMessage() {
System.out.println("[안내] 안녕하세요. 김밥천국에 오신 것을 환영합니다.");
System.out.println("--------------------------------");
}
public int selectMenu() {
// 주문 안내 메시지를 출력하는 메서드를 정의할 수 있습니다.
int menuNumber;
// 주문할 음식을 선택하는 메서드를 정의 할 수 있습니다.
do {
PrintMenuSelectMessage();
menuNumber = selectMenuNumber();
if(menuNumber < 1 || menuNumber > 4) {
System.out.println("[안내] 메뉴에 포함된 번호를 입력하여 주세요.");
System.out.println();
}
} while(menuNumber < 1 || menuNumber > 4);
return menuNumber;
}
// 주문 안내 메시지를 출력하는 메서드를 정의할 수 있습니다.
private void PrintMenuSelectMessage() {
System.out.println("[안내] 원하시는 메뉴의 번호를 입력하여 주세요.");
System.out.println("1) 김밥(1000원) 2) 계란 김밥(1500원) 3) 충무 김밥(1000원) 4) 떡볶이(2000원)");
}
// 주문할 음식을 선택하는 메서드를 정의 할 수 있습니다.
private int selectMenuNumber() {
int menuNumber = Integer.parseInt(sc.nextLine());
return menuNumber;
}
// 주문할 음식의 수량을 입력하는 메서드를 정의할 수 있습니다.
private void printMenuCountMessage() {
System.out.println("----------------------------------------");
System.out.println("메뉴의 수량을 입력해주세요.");
System.out.println("(※ 최대 주문 가능 수량 : 99)");
}
private int inputMenuCountNumber() {
int count = Integer.parseInt(sc.nextLine());
return count;
}
private void printMenuCountExceptionMessage(int count) {
System.out.println("[경고] " + count + "개는 입력하실 수 없습니다.");
System.out.println("[경고] 수량 선택 화면으로 돌아갑니다.");
}
public int selectMenuCount() {
int count = 0;
do {
printMenuCountMessage();
count = inputMenuCountNumber();
if (count < 1 || count > 99) {
printMenuCountExceptionMessage(count);
}
}while(count < 1 || count > 99);
return count;
}
private int getCurrentMenuPrice(int menuNumber) {
int price = -1;
switch (menuNumber) {
case 1:
price = 1000;
break;
case 2:
price = 1500;
break;
case 3:
price = 1000;
break;
case 4:
price = 2000;
break;
default:
}
return price;
}
// 음식 주문을 위한 메서드를 정의할 수 있습니다.
public int calculateOrderPrice(int menuNumber, int count) {
int price = getCurrentMenuPrice(menuNumber);
if(price == -1) {
return -1;
}
return count * price;
}
// 주문 결과를 출력하는 메서드를 정의할 수 있습니다.
public void printOrderPriceMessage(int currentOrderPrice) {
System.out.println("[안내] 주문하신 메뉴의 총 금액은 : " + currentOrderPrice + "원 입니다.");
System.out.println("[안내] 이용해 주셔서 감사합니다.");
}
}
KioskApplication
package com.jungmin.seb.kioskref.V2;
import java.util.Scanner;
public class KioskApplication {
public static void main(String[] args) {
// Kiosk 클래스의 기능을 구현 및 활용해서 프로그래밍 요구 사항에 맞는 키오스크 프로그램을 구현하세요.
// MenuItem 클래스는 음식 정보를 표현하고 있으며 MenuItem 클래스를 이용해 주문할 음식 정보를 출력할 수 있습니다.
// 이 외의 필요한 클래스가 있다면 자유롭게 추가해서 여러분들만의 키오스크 프로그램을 만들면 됩니다.
// Kiosk 객체 생성
Kiosk kiosk = new Kiosk();
// 웰컴 메시지 출력
kiosk.welcomePrintMessage();
// 주문 안내 메시지를 출력하는 메서드를 정의할 수 있습니다.
int menuNumber = kiosk.selectMenu();
// 주문할 음식의 수량을 입력하는 메서드를 정의할 수 있습니다.
int count = kiosk.selectMenuCount();
// 음식 주문을 위한 메서드를 정의할 수 있습니다.
int currentOrderPrice = kiosk.calculateOrderPrice(menuNumber, count);
if(currentOrderPrice == -1) {
System.out.println("알 수 없는 오류가 발생했습니다.\n" + "처음부터 다시 주문을 시작해 주세요.");
return;
}
// 주문 결과를 출력하는 메서드를 정의할 수 있습니다.
kiosk.printOrderPriceMessage(currentOrderPrice);
}
}
v3
메서드 분리
Kiosk
package com.jungmin.seb.kioskref.v3;
import java.util.Scanner;
public class Kiosk {
private final static MenuItem menuItem1 = new MenuItem("김밥", 1000);
private final static MenuItem menuItem2 = new MenuItem("계란 김밥", 1500);
private final static MenuItem menuItem3 = new MenuItem("충무 김밥", 1000);
private final static MenuItem menuItem4 = new MenuItem("떡볶이", 2000);
Scanner sc = new Scanner(System.in);
// 웰컴 메시지 출력
public void welcomePrintMessage() {
System.out.println("[안내] 안녕하세요. 김밥천국에 오신 것을 환영합니다.");
System.out.println("--------------------------------");
}
public MenuItem selectMenu() {
int menuNumber;
// 주문할 음식을 선택하는 메서드를 정의 할 수 있습니다.
while(true) {
PrintMenuSelectMessage();
menuNumber = selectMenuNumber();
switch (menuNumber) {
case 1:
return menuItem1;
case 2:
return menuItem2;
case 3:
return menuItem3;
case 4:
return menuItem4;
}
printMenuSelectExceptionMessage();
}
}
// 주문 안내 메시지를 출력하는 메서드를 정의할 수 있습니다.
private void printMenuSelectExceptionMessage() {
System.out.println("[안내] 메뉴에 포함된 번호를 입력하여 주세요.");
}
private void PrintMenuSelectMessage() {
System.out.println("[안내] 원하시는 메뉴의 번호를 입력하여 주세요.");
System.out.println("1) 김밥(1000원) 2) 계란 김밥(1500원) 3) 충무 김밥(1000원) 4) 떡볶이(2000원)");
}
// 주문할 음식을 선택하는 메서드를 정의 할 수 있습니다.
private int selectMenuNumber() {
int menuNumber = Integer.parseInt(sc.nextLine());
return menuNumber;
}
// 주문할 음식의 수량을 입력하는 메서드를 정의할 수 있습니다.
private void printMenuCountMessage() {
System.out.println("----------------------------------------");
System.out.println("메뉴의 수량을 입력해주세요.");
System.out.println("(※ 최대 주문 가능 수량 : 99)");
}
private int inputMenuCountNumber() {
int count = Integer.parseInt(sc.nextLine());
return count;
}
private void printMenuCountExceptionMessage(int count) {
System.out.println("[경고] " + count + "개는 입력하실 수 없습니다.");
System.out.println("[경고] 수량 선택 화면으로 돌아갑니다.");
}
public int selectMenuCount() {
int count = 0;
do {
printMenuCountMessage();
count = inputMenuCountNumber();
if (count < 1 || count > 99) {
printMenuCountExceptionMessage(count);
}
}while(count < 1 || count > 99);
return count;
}
// 음식 주문을 위한 메서드를 정의할 수 있습니다.
public int calculateOrderPrice(MenuItem menu, int count) {
int price = menu.getPrice();
if(price == -1) {
return -1;
}
return count * price;
}
// 주문 결과를 출력하는 메서드를 정의할 수 있습니다.
public void printOrderPriceMessage(int currentOrderPrice, MenuItem menu, int count) {
System.out.println("[안내] 주문하신 상품은 " + menu.getName() + " 총 상품의 갯수는 : " + count + "개 입니다.");
System.out.println("[안내] 주문하신 메뉴의 총 금액은 : " + currentOrderPrice + "원 입니다.");
System.out.println("[안내] 이용해 주셔서 감사합니다.");
}
}
KioskApplication
package com.jungmin.seb.kioskref.v3;
public class KioskApplication {
public static void main(String[] args) {
// Kiosk 클래스의 기능을 구현 및 활용해서 프로그래밍 요구 사항에 맞는 키오스크 프로그램을 구현하세요.
// MenuItem 클래스는 음식 정보를 표현하고 있으며 MenuItem 클래스를 이용해 주문할 음식 정보를 출력할 수 있습니다.
// 이 외의 필요한 클래스가 있다면 자유롭게 추가해서 여러분들만의 키오스크 프로그램을 만들면 됩니다.
// Kiosk 객체 생성
Kiosk kiosk = new Kiosk();
// 웰컴 메시지 출력
kiosk.welcomePrintMessage();
// 주문 안내 메시지를 출력하는 메서드를 정의할 수 있습니다.
MenuItem menu = kiosk.selectMenu();
// 주문할 음식의 수량을 입력하는 메서드를 정의할 수 있습니다.
int count = kiosk.selectMenuCount();
// 음식 주문을 위한 메서드를 정의할 수 있습니다.
int currentOrderPrice = kiosk.calculateOrderPrice(menu, count);
if(currentOrderPrice == -1) {
System.out.println("알 수 없는 오류가 발생했습니다.\n" + "처음부터 다시 주문을 시작해 주세요.");
return;
}
// 주문 결과를 출력하는 메서드를 정의할 수 있습니다.
kiosk.printOrderPriceMessage(currentOrderPrice, menu, count);
}
}
MenuItem
package com.jungmin.seb.kioskref.v3;
public class MenuItem {
private String name;
private int price;
public MenuItem(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
V4
배열을 사용하여 리펙토링
Kiosk
package com.jungmin.seb.kioskref.v4;
import java.util.Scanner;
public class Kiosk {
private MenuItem[] menuItemArray;
public MenuItem[] getMenuItemArray() {
return menuItemArray;
}
public Kiosk(MenuItem[] menuItemArray) {
this.menuItemArray = menuItemArray;
}
Scanner sc = new Scanner(System.in);
// 웰컴 메시지 출력
public void welcomePrintMessage() {
System.out.println("[안내] 안녕하세요. 김밥천국에 오신 것을 환영합니다.");
System.out.println("--------------------------------");
}
public MenuItem selectMenu() {
int menuNumber;
// 주문할 음식을 선택하는 메서드를 정의 할 수 있습니다.
while(true) {
PrintMenuSelectMessage(menuItemArray);
menuNumber = selectMenuNumber();
if(menuNumber <= menuItemArray.length &&
menuNumber >= 1) {
return menuItemArray[menuNumber - 1];
}
printMenuSelectExceptionMessage();
}
}
// 주문 안내 메시지를 출력하는 메서드를 정의할 수 있습니다.
private void printMenuSelectExceptionMessage() {
System.out.println("[안내] 메뉴에 포함된 번호를 입력하여 주세요.");
}
private void PrintMenuSelectMessage(MenuItem[] arr) {
System.out.println("[안내] 원하시는 메뉴의 번호를 입력하여 주세요.");
for (int i = 0; i < arr.length; i++) {
System.out.printf("(메뉴 %d) %s(%d원)%n",i + 1, arr[i].getName(), arr[i].getPrice());
}
System.out.println("1) 김밥(1000원) 2) 계란 김밥(1500원) 3) 충무 김밥(1000원) 4) 떡볶이(2000원)");
System.out.println("--------------------------------");
System.out.println("메뉴를 입력해 주세요.");
}
// 주문할 음식을 선택하는 메서드를 정의 할 수 있습니다.
private int selectMenuNumber() {
int menuNumber = Integer.parseInt(sc.nextLine());
return menuNumber;
}
// 주문할 음식의 수량을 입력하는 메서드를 정의할 수 있습니다.
private void printMenuCountMessage() {
System.out.println("----------------------------------------");
System.out.println("메뉴의 수량을 입력해주세요.");
System.out.println("(※ 최대 주문 가능 수량 : 99)");
}
private int inputMenuCountNumber() {
int count = Integer.parseInt(sc.nextLine());
return count;
}
private void printMenuCountExceptionMessage(int count) {
System.out.println("[경고] " + count + "개는 입력하실 수 없습니다.");
System.out.println("[경고] 수량 선택 화면으로 돌아갑니다.");
}
public int selectMenuCount() {
int count = 0;
do {
printMenuCountMessage();
count = inputMenuCountNumber();
if (count < 1 || count > 99) {
printMenuCountExceptionMessage(count);
}
}while(count < 1 || count > 99);
return count;
}
// 음식 주문을 위한 메서드를 정의할 수 있습니다.
public int calculateOrderPrice(MenuItem menu, int count) {
int price = menu.getPrice();
if(price == -1) {
return -1;
}
return count * price;
}
// 주문 결과를 출력하는 메서드를 정의할 수 있습니다.
public void printOrderPriceMessage(int currentOrderPrice, MenuItem menu, int count) {
System.out.println("[안내] 주문하신 상품은 " + menu.getName() + " 총 상품의 갯수는 : " + count + "개 입니다.");
System.out.println("[안내] 주문하신 메뉴의 총 금액은 : " + currentOrderPrice + "원 입니다.");
System.out.println("[안내] 이용해 주셔서 감사합니다.");
}
}
KioskApplication
package com.jungmin.seb.kioskref.v4;
public class KioskApplication {
public static void main(String[] args) {
// Kiosk 클래스의 기능을 구현 및 활용해서 프로그래밍 요구 사항에 맞는 키오스크 프로그램을 구현하세요.
// MenuItem 클래스는 음식 정보를 표현하고 있으며 MenuItem 클래스를 이용해 주문할 음식 정보를 출력할 수 있습니다.
// 이 외의 필요한 클래스가 있다면 자유롭게 추가해서 여러분들만의 키오스크 프로그램을 만들면 됩니다.
// Kiosk 객체 생성
MenuItem[] menuItems = new MenuItem[] {
new MenuItem("김밥", 1000),
new MenuItem("계란 김밥", 2000),
new MenuItem("충무 김밥", 8000),
new MenuItem("치즈 김밥", 4000),
new MenuItem("참치 김밥", 4500),
new MenuItem("돈까스 김밥", 5000),
new MenuItem("떡볶이", 5000),
new MenuItem("라볶이", 6000),
new MenuItem("쫄면", 6500),
new MenuItem("우동", 4500)
};
Kiosk kiosk = new Kiosk(menuItems);
// 웰컴 메시지 출력
kiosk.welcomePrintMessage();
// 주문 안내 메시지를 출력하는 메서드를 정의할 수 있습니다.
MenuItem menu = kiosk.selectMenu();
// 주문할 음식의 수량을 입력하는 메서드를 정의할 수 있습니다.
int count = kiosk.selectMenuCount();
// 음식 주문을 위한 메서드를 정의할 수 있습니다.
int currentOrderPrice = kiosk.calculateOrderPrice(menu, count);
if(currentOrderPrice == -1) {
System.out.println("알 수 없는 오류가 발생했습니다.\n" + "처음부터 다시 주문을 시작해 주세요.");
return;
}
// 주문 결과를 출력하는 메서드를 정의할 수 있습니다.
kiosk.printOrderPriceMessage(currentOrderPrice, menu, count);
}
}
MenuItem
package com.jungmin.seb.kioskref.v4;
public class MenuItem {
private String name;
private int price;
public MenuItem(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
Collection
열거형(Enum)
열거형(enum; enumerated type)은 여러 상수들을 보다 편리하게 선언할 수 있도록 만들어진 자바의 문법요소입니다. 열거형이 등장하게 된 자세한 배경에 대해서는 다음 챕터에서 살펴보도록 하겠습니다.
열거형은 주로 서로 관련 있는 내용들을 모아 한 번에 간편하게 관리할 때 사용합니다.
예를 들면, 아래와 같은 내용들을 하나로 묶어 관리할 수 있습니다.
- 봄, 여름, 가을, 겨울
- 동쪽, 서쪽, 남쪽, 북쪽
- 짜장면, 짬뽕, 탕수육, 볶음밥
- 국어, 영어, 수학, 과학, 사회
본격적인 학습에 들어가기에 앞서, 아래 학습 목표를 통해 이번 챕터의 학습 내용을 먼저 확인해 봅시다.
학습 목표
- Enum의 기본적인 개념을 이해하고 설명할 수 있다.
- Enum이 등장하게 된 배경에 대해서 이해하고, 그 장점에 대해 설명할 수 있다.
- Enum의 문법 요소를 이해하고 적절하게 사용할 수 있다.
열거형(Enum)
앞서 설명했듯이, 열거형(enum)은 서로 연관된 상수들의 집합을 의미합니다.
우리가 앞서 배웠듯이, 상수란 변하지 않는 값을 의미하며 final 키워드를 사용하여 선언할 수 있었습니다.
열거형은 이러한 상수들을 보다 간편하게 관리할 때 유용하게 사용할 수 있는 자바의 문법 요소이며, 주로 몇 가지로 한정된 변하지 않는 데이터를 다루는 데 사용합니다.
원래 JDK 1.5 이전 버전에서는 enum 문법을 지원하지 않았습니다. 따라서 여러 상수를 정의해서 사용하기 위해서는 public static final을 통해 전역변수로 상수를 설정하여 아래와 같이 사용했습니다.
// 여러 상수를 정의하기 위한 예전 방식
public static final int SPRING = 1;
public static final int SUMMER = 2;
public static final int FALL = 3;
public static final int WINTER = 4;
참고로, 상수에 부여된 1, 2, 3, 4의 값은 각 상수를 구분하기 위해서 사용합니다.
그런데 이렇게 정수값을 통해서 상수를 할당하면, 아래의 예시처럼 상수 명이 중복되는 경우가 종종 발생할 수 있습니다.
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;
그리고 이렇게 상수의 이름이 중복되면 컴파일 에러가 발생합니다.
이 문제는 인터페이스를 사용하여 상수를 구분함으로써 다음과 같이 일차적으로 해결할 수 있습니다.
interface Seasons {
int SPRING = 1, SUMMER = 2, FALL = 3, WINTER = 4;
}
interface Frameworks {
int DJANGO = 1, SPRING = 2, NEST = 3, EXPRESS = 4;
}
그러나 이 경우 중복 상수 명의 문제는 피할 수 있지만, 타입 안정성이라는 새로운 문제가 생깁니다.
예를 들면, 위의 예시 코드에서 Seasons.SPRING의 정수값 1과 Frameworks.SPRING의 정수값 2는 상수를 열거하기 위해 임의로 주어진 값이고, 그 외에 어떤 의미가 있는 값이 아님에도 아래와 같이 비교하는 코드를 작성할 수 있습니다.
if (Seasons.SPRING == Frameworks.SPRING) {...생략...}
어떤가요?
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입니다.
이제 eum을 활용한 상수 정의를 한번 살펴봅시다.
enum Seasons { SPRING, SUMMER, FALL, WINTER }
enum Frameworks { DJANGO, SPRING, NEST, EXPRESS }
어떤가요?
이처럼 enum을 사용하면 앞서 발생했던 문제들을 모두 효과적으로 해결할 수 있을 뿐 아니라 코드를 단순하고 가독성이 좋게 만들 수 있다는 장점이 있습니다. 또한 enum으로 정의한 상수는 switch문에서도 사용이 가능합니다.
이제 마지막으로 switch문을 사용할 때 public static final을 사용하여 정의한 상수와 enum을 사용하여 정의한 상수가 각각 어떻게 작동하는지 확인해 보겠습니다.
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
*/
먼저 전자의 경우, 코드를 실행하면 호환되지 않는 타입이라는 에러가 발생합니다.
그 이유는 switch문의 조건은 char, byte, short, int, Character, Byte, Short, Integer, String, enum 타입만 가능하지만, 위의 seasons는 사용자 정의 타입이기 때문입니다.
이제 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문을 사용할 수 있다는 것을 확인할 수 있습니다.
정리하면, 자바에서 열거형은 여러 상수들을 보다 편리하게 선언하고 관리할 수 있게 하며, 상수 명의 중복을 피하고, 타입에 대한 안정성을 보장합니다.
또한 같은 효과를 낼 수 있는 다른 코드에 반해 훨씬 더 간결하고 가독성이 좋은 코드를 작성할 수 있으며 switch문에서도 작동이 가능합니다.
이제 다음 챕터에서 조금 더 구체적으로 열거형을 어떻게 정의하고 사용할 수 있는지 살펴보도록 하겠습니다.
열거형의 사용
이제 열거형을 어떻게 정의하고 사용할 수 있는지 살펴보도록 합시다.
직전의 예시에서 봤었던 것처럼, 열거형을 정의하는 방법은 아주 간단합니다.
다음과 같이 코드 블록 안에 선언하고자 하는 상수의 이름을 나열하기만 하면 됩니다.
enum 열거형이름 { 상수명1, 상수명2, 상수명3, ...}
앞서 봤었던 사계절을 예로 들어보겠습니다. enum을 사용하여 사계절을 상수로 정의하면 다음과 같습니다.
enum Seasons {
SPRING, //정수값 0 할당
SUMMER, //정수값 1 할당
FALL, //정수값 2 할당
WINTER //정수값 3 할당
}
참고로, 상수는 대소문자로 모두 작성이 가능하지만, 관례적으로 대문자로 작성합니다.
또한 각각의 열거 상수들은 객체이기 때문에, 위의 예시에서 Seasons라는 이름의 열거형은 SPRING, SUMMER, FALL, WINTER는 총 네 개의 열거 객체를 포함하고 있다고 말할 수 있습니다.
마지막으로 각각의 상수들에는 따로 값을 지정해주지 않아도 자동적으로 0부터 시작하는 정수값이 할당되어 각각의 상수를 가리키게 됩니다.
그러면 이렇게 선언한 열거형을 어떻게 사용할 수 있을까요?
아래 예시를 한번 살펴보도록 합시다.
enum Seasons { SPRING, SUMMER, FALL, WINTER }
public class EnumExample {
public static void main(String[] args) {
System.out.println(Seasons.SPRING); // SPRING
}
}
열거형에 선언된 상수에 접근하는 방법은 열거형이름.상수명 을 통해서 가능합니다. 앞서 배웠던 클래스에서 static 변수를 참조하는 것과 동일하다고 할 수 있습니다.
이제 내가 가장 좋아하는 계절이라는 의미의 참조 변수 favoriteSeason에 Seasons.SPRING을 담아보도록 하겠습니다.
enum Seasons { SPRING, SUMMER, FALL, WINTER }
public class EnumExample {
public static void main(String[] args) {
Seasons favoriteSeason = Seasons.SPRING;
System.out.println(favoriteSeason); // SPRING
}
}
보시는 것처럼, Seasons.SPRING을 Seasons 타입의 참조 변수에 할당하고 있습니다.
비슷한 예제를 하나 더 살펴보도록 하겠습니다.
enum Level {
LOW, // 0
MEDIUM, // 1
HIGH // 2
}
public class Main {
public static void main(String[] args) {
Level level = Level.MEDIUM;
switch(level) {
case LOW:
System.out.println("낮은 레벨");
break;
case MEDIUM:
System.out.println("중간 레벨");
break;
case HIGH:
System.out.println("높은 레벨");
break;
}
}
}
//출력값
중간 레벨
위의 코드예제를 보면, Level이라는 열거형을 하나 만들고, 그 안에 세 가지의 열거 상수(LOW, MEDIUM, HIGH)를 선언해 주었습니다.
그리고 열거형과 같은 타입의 참조 변수 level에 Level.MEDIUM 값을 할당하고 switch문을 통해 해당 값에 대한 출력값을 얻을 수 있었습니다.
이처럼 enum을 사용하면 변경되지 않는 한정적인 데이터들을 효과적으로 관리할 수 있습니다.
마지막으로 열거형에서 사용할 수 있는 메서드를 살펴보겠습니다.
아래 메서드들은 모든 열거형의 조상인 java.lang.Enum 에 정의되어 있는 것으로, 클래스에서 최상위 클래스 Object에 정의된 메서드들을 사용할 수 있었던 것과 동일하다고 할 수 있습니다.
리턴 타입 메서드(매개변수) 설명
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
중간 레벨
앞서 봤었던 코드에서 몇 가지 열거형 메서드를 사용한 예제입니다.
먼저 values() 메서드는 컴파일러가 자동적으로 모든 열거형에 추가해 주는 메서드로 Level에 정의된 모든 상수를 배열로 반환했습니다.
이렇게 받은 배열을 앞서 우리가 자바 기초에서 배웠던 향상된 for문과 열거형의 최상위 클래스로부터 확장된 name()과 ordinal()을 사용하여 각각 이름과 순서를 출력값으로 반환하고 있습니다.
마지막으로 valueOf() 메서드를 활용하여 지정된 열거형에서 이름과 일치하는 열거형 상수를 반환하고, 반환된 상수가 의도했던 상수와 일치하는지 여부를 불리언 값으로 확인해주고 있습니다.
제네릭(Generic)
지금까지 여러분은 어떤 클래스 내부의 코드를 작성할 때 아래와 같이 구체적인 타입을 지정해 왔습니다.
class Basket {
private String item;
Basket(String item) {
this.item = item;
}
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
}
위 예제에서, item의 타입은 String으로 지정되어 있으며, 그에 따라 위 클래스를 통해 만들어진 인스턴스는 String 타입의 데이터만 저장할 수 있습니다.
만약, Basket 클래스와 똑같은 기능을 하면서 인스턴스 변수 item에 다른 타입의 데이터도 저장할 수 있게 하려면 원래는 각 타입별로 같은 내용의 클래스를 만들어야만 합니다.
class BasketString { private String item; ... }
class BasketInteger { private int item; ... }
class BasketChar { private char item; ... }
class BasketDouble { private double item; ... }
...
그러나 제네릭을 사용하면 이러한 번거로움을 줄일 수 있습니다. 이어지는 콘텐츠에서부터 제네릭을 사용하여 위의 문제를 어떻게 해결할 수 있는지, 어떻게 제네릭을 활용할 수 있는지에 대해 학습해 봅시다.
지금 여러분이 학습할 제네릭은 곧바로 이어지는 컬렉션에서 활용됩니다. 콘텐츠를 읽어나가며 제네릭의 중요 개념만 천천히 이해해 보시되, 세부적인 내용을 모두 외우려고 하지는 않으셔도 됩니다. 이후 컬렉션을 학습하면서 제네릭이 어떻게 사용되는지 주목하며 익숙해지시기 바랍니다.
학습 목표
- 제네릭의 장점을 이해한다.
- 제네릭 클래스를 정의하고 활용할 수 있다.
- 제네릭 메서드를 정의하고 활용할 수 있다.
제네릭이란?
제네릭의 필요성
개요에서 살펴본 예제를 다시 살펴봅시다. 아래 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;
}
}
맞습니다. <T>가 클래스 이름 옆에 추가되었으며, 클래스 몸체 내에 String으로 지정했던 타입들이 T라는 문자 하나로 바뀌었습니다. 이 <T>와 T가 바로 제네릭의 문법에 해당합니다. 이것이 무엇인지에 대해서는 이어지는 콘텐츠에서 설명하겠습니다.
위의 Basket 클래스는 다음과 같이 인스턴스화할 수 있습니다.
Basket<String> basket1 = new Basket<String>("기타 줄");
예제 코드에서 Basket 클래스가 인스턴스화될 때 클래스 이름 뒤에 <String>이 따라붙고 있습니다. 이 또한 제네릭의 문법으로, 아래와 같은 의미로 간주할 수 있습니다.
“Basket 클래스 내의 T를 String으로 바꿔라.”
위 코드를 실행하면 Basket 클래스 내부의 T가 모두 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;
}
}
만약, 아래와 같이 <> 안에 Integer를 넣어 인스턴스화한다면 Basket 클래스 내부의 T는 모두 Integer로 치환됩니다.
이 예제에서 Integer는 int와 같이 정수를 나타내는 타입이라고 생각해 주세요. 정확히 말하면, Integer는 int의 래퍼 클래스(wrapper class)입니다. 검색을 통해 래퍼 클래스가 무엇이고, Integer와 int의 차이에 대해 스스로 공부해 보세요!
Basket<Integer> basket2 = new Basket<Integer>(1);
// 위와 같이 인스턴스화하면 Basket 클래스는 아래와 같이 변환됩니다.
class Basket<Integer> {
private Integer item;
public Basket(Integer item) {
this.item = item;
}
public Integer getItem() {
return item;
}
public void setItem(Integer item) {
this.item = item;
}
}
그렇다면, Basket 클래스를 아래와 같이 인스턴스화한다면 Basket 클래스 내부의 코드가 어떻게 동작할까요? 아래의 상황을 확인하고, Basket 내부의 코드가 결과적으로 어떻게 실행될지 스스로 답해보세요.
- Basket<Boolean> basket3 = new Basket<Boolean>(true);
- Basket<Double> basket4 = new Basket<Double>(3.14);
제네릭이란 무엇일까요?
자, 여기까지 제네릭이 왜 필요한지에 대해서 살펴보았으며, 여러분들도 충분히 제네릭의 필요성에 대해서 감을 잡으셨을 거라고 생각합니다. 그렇다면, 이제 제네릭이 본질적으로 무엇인지에 대해서 알아봅시다.
제네릭(Generic)은 사전적으로 ‘일반적인’이라는 의미를 가지고 있습니다. 자바에서 제네릭이란, 위에서 살펴본 것처럼 클래스나 메서드의 코드를 작성할 때, 타입을 구체적으로 지정하는 것이 아니라, 추후에 지정할 수 있도록 일반화해 두는 것을 의미합니다. 즉, 작성한 클래스 또는 메서드의 코드가 특정 데이터 타입에 얽매이지 않게 해 둔 것을 의미합니다.
제네릭은 클래스와 메서드에 사용할 수 있습니다. 이어지는 콘텐츠에서부터는 제네릭 클래스와 제네릭 메서드를 정의하는 방법을 학습해 봅시다.
제네릭 클래스
제네릭 클래스 정의
제네릭이 사용된 클래스를 제네릭 클래스라고 합니다. 앞서 익히 살펴보았던 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;
}
}
위의 코드에서, T를 타입 매개변수라고 하며, <T>와 같이 꺾쇠 안에 넣어 클래스 이름 옆에 작성해 줌으로써 클래스 내부에서 사용할 타입 매개변수를 선언할 수 있습니다.
즉, 아래와 같이 타입 매개 변수 T를 선언하면,
class Basket<T> {
}
클래스 몸체에서 T를 임의의 타입으로 사용할 수 있습니다.
class Basket<T> {
private T item;
...
}
만약, 타입 매개변수를 여러 개 사용해야 한다면, 아래와 같이 선언하면 됩니다.
class Basket<K, V> { ... }
타입 매개변수는 임의의 문자로 지정할 수 있습니다. 위에서 사용한 T, K, V는 각각 Type, Key, Value의 첫 글자를 따온 것입니다. 이 외에도, Element를 뜻하는 E, Number를 뜻하는 N, 그리고 Result를 뜻하는 R도 자주 사용됩니다.
제네릭 클래스를 정의할 때 주의할 점
제네릭 클래스에서 타입 매개변수를 임의의 타입으로 사용할 수 있다고 하였습니다. 이때, 아래와 같이 클래스 변수에는 타입 매개변수를 사용할 수 없습니다.
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<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()); // 에러
}
}
new Rose()를 통해 생성된 인스턴스는 Rose 타입이며, Rose 클래스는 Flower 클래스를 상속받고 있으므로, Basket<Flower>의 item에 할당될 수 있습니다. Basket<Flower>은 결국 item의 타입을 Flower로 지정하는 것이고, Flower 클래스는 Rose 클래스의 상위 클래스이기 때문입니다.
반면, new RosePasta()를 통해 생성된 인스턴스는 RosePasta 타입이며, RosePasta 클래스는 Flower 클래스와 아무런 관계가 없습니다. 따라서, flowerBasket의 item에 할당될 수 없습니다.
제한된 제네릭 클래스
앞서 살펴본 예제의 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<>(); // 에러
}
}
이와 같이 특정 클래스를 상속받은 클래스만 타입으로 지정할 수 있도록 제한하는 것뿐만 아니라, 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한할 수도 있습니다. 이 경우에도 동일하게 extends 키워드를 사용합니다.
interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }
class Basket<T extends Plant> {
private T item;
...
}
class Main {
public static void main(String[] args) {
// 인스턴스화
Basket<Flower> flowerBasket = new Basket<>();
Basket<Rose> roseBasket = new Basket<>();
}
}
만약, 특정 클래스를 상속받으면서 동시에 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한하려면 아래와 같이 &를 사용하여 코드를 작성해 주면 됩니다.
다만, 이러한 경우에는 클래스를 인터페이스보다 앞에 위치시켜야 합니다. 아래 예제의 (1)을 참고하세요.
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) {
...
}
}
제네릭 메서드의 타입 매개 변수는 제네릭 클래스의 타입 매개 변수와 별개의 것입니다. 즉, 아래와 같이 동일하게 T라는 타입 매개 변수명을 사용한다고 하더라도, 같은 알파벳 문자를 이름으로 사용하는 것일 뿐, 서로 다른 타입 매개 변수로 간주됩니다.
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) {
...
}
}
제네릭 메서드는 메서드가 호출되는 시점에서 제네릭 타입이 결정되므로, 제네릭 메서드를 정의하는 시점에서는 실제 어떤 타입이 입력되는지 알 수 없습니다. 따라서 length()와 같은 String 클래스의 메서드는 제네릭 메서드를 정의하는 시점에 사용할 수 없습니다.
class Basket {
public <T> void print(T item) {
System.out.println(item.length()); // 불가
}
}
하지만 모든 자바 클래스의 최상위 클래스인 Object 클래스의 메서드는 사용 가능합니다. 모든 클래스는 Object 클래스를 상속받기 때문입니다. 지금까지 여러분이 사용해 본 equals(), toString() 등이 Object 클래스의 메서드에 속합니다.
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>와 같습니다. 즉, 모든 클래스 타입은 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 : 휴대전화의 기본적인 통화 기능으로, 모든 휴대전화에서 사용할 수 있는 기능입니다.
- →? extends 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("안드로이드 폰에서만 통화 녹음이 가능합니다. ");
}
}
이제 위에서 만들어둔 휴대전화 기종 클래스들을 인자로 전달하면서 PhoneFunction의 각 메서드들을 호출해 볼 것입니다.
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
}
}
위 코드에서, 주석으로 처리된 부분은 에러로 감지되는 부분들입니다. 여기에서 생각해보아야 할 점은 크게 두 가지입니다.
- 왜 에러가 발생할까요?예를 들어, faceId의 매개 변수는 User<? extends IPhone>로, faceId를 호출할 때에는 User의 타입으로 IPhone 또는 IPhone을 상속받는 클래스를 타입으로 넣어주어야 합니다.
- 따라서, IPhone 타입이 아니거나, IPhone을 상속받는 클래스 타입이 아닌 다른 클래스 타입의 객체를 넣어 faceId를 호출하는 경우에 에러가 발생합니다.
- 위 예제 코드에서 주석처리된 부분, 즉 X로 표기된 코드들은 호출하고 있는 메서드에 표기된 매개 변수의 타입과 정확히 일치하지 않는 경우를 의미합니다.
- recordVoice는 S22와 ZFlip3을 타입으로 지정하면서 호출할 때 왜 에러가 발생하나요?<? super Galaxy>는 상속 계층도 상에서 Galaxy 및 Galaxy보다 위에 있는 상위 클래스만 타입으로 지정할 수 있게 제한해 줍니다.
- 따라서, Galaxy보다 상속 계층도 상 아래에 있는 S22와 ZFlip3을 타입으로 지정하면서 recordVoice를 호출할 수 없습니다.
- recordVoice의 매개 변수를 보면 User<? super Galaxy> 타입의 객체를 매개 변수로 받고 있습니다.
아래 그림과 함께 예제 코드에서 사용된 와일드카드를 다시 한번 이해해 보시기 바랍니다.