1. 제네릭(generic)이란?
자바에서 제네릭(generic)이란 데이터의 타입(data type)을 일반화한다(generalize)는 것을 의미합니다.
제네릭(Generic)은 Java 프로그래밍 언어에서 타입(type)의 파라미터화를 가능하게 하는 언어 기능입니다.
이는 클래스, 인터페이스, 메소드를 정의할 때 타입을 하나의 파라미터처럼 취급할 수 있게 해 줍니다.
즉, 구체적인 타입을 명시하지 않고도 타입을 사용할 수 있는 방법을 제공합니다.
2. 제네릭 사용 시 얻을 수 있는 이점
1) 타입 안정성 : 제네릭을 사용하면 컴파일 시점에 타입 체크를 할 수 있어서 런타임에 발생할 수 있는 ClassCastException과 같은 오류를 방지할 수 있습니다.
2) 코드의 재사용성 증가
3) 유지보수성 향상
3. 제네릭 사용법
타입 매개변수를 꺾쇠괄호(< >) 안에 지정하여 선언합니다.
4. 제네릭을 사용하지 않은 코드 VS 제네릭 사용한 코드
4-1) 코드 중복 문제
*제네릭을 사용하지 않은 코드
package Day10_Generic;
public class _01_Generics {
public static void main(String[] args) {
int[] iArr = {1,2,3,4,5};
double[] dArr = {1.0,2.0,3.0,4.0,5.0};
String[] sArr = {"A","B","C","D","E"};
//문제점 1.
//타입에 따라 메서드를 따로 선언해서 사용하고 있다.
//추후 타입이 추가되면 메서드를 또 만들어야한다.
printIntArr(iArr);
printDoubleArr(dArr);
printStringArr(sArr);
}
//문제점 2.
//코드가 중복되고 있다.
//비효율적인 코드로 보인다.
private static void printStringArr(String[] sArr) {
for (String i: sArr) {
System.out.print(i+ " ");
}
}
private static void printDoubleArr(double[] dArr) {
for (double i: dArr) {
System.out.print(i+ " ");
}
}
private static void printIntArr(int[] iArr) {
for (int i: iArr) {
System.out.print(i+ " ");
}
}
}
주석에도 설명을 달아놨지만, 타입의 갯수만큼 메서드를 생성해야하는 코드입니다.
따라서 코드 중복도 보입니다.
*제네릭을 사용한 코드
package Day10_Generic;
public class _01_Generics {
public static void main(String[] args) {
Integer[] wrapiArr = {1,2,3,4,5};
Double[] wrapdArr = {1.0,2.0,3.0,4.0,5.0};
String[] sArr = {"A","B","C","D","E"};
//제네릭스가 지원하는 건 객체!
//기본자료형은 쓸 수 없다. 래퍼클래스를 써야된다. 첫문자가 대문자!
printAnyArr(wrapiArr);
printAnyArr(wrapdArr);
printAnyArr(sArr);
}
// T : Type
private static <T> void printAnyArr(T[] arr){
for (T t: arr) {
System.out.print(t+ " ");
}
}
}
훨씬 코드가 깔끔해 졌습니다.
타입의 갯수만큼 메서드를 만들지 않아도 됩니다.
유지보수성도 향상되고 코드의 중복도 없앴습니다.
다만, 주의해야할 점은 제네릭이 지원하는 건 객체라는 것입니다.
int, double등은 기본자료형이기 때문에 래퍼클래스로 값을 넘겨줘야 제네릭을 활용할 수 있습니다.
4-2) 코드 중복 및 형변환 실수
*제네릭을 사용하지 않은 코드
/*클래스 : CoffeeByNumber*/
public class CoffeeByNumber {
public int waitingNumber;
public CoffeeByNumber(int waitingNumber) {
this.waitingNumber = waitingNumber;
}
public void ready(){
System.out.println("커피 준비 완료: "+ waitingNumber);
}
}
/*클래스 : CoffeeByNickname*/
package Day10_Generic.coffee;
public class CoffeeByNickname {
public String nickname;
public CoffeeByNickname(String nickname) {
this.nickname = nickname;
}
public void ready(){
System.out.println("커피 준비 완료 : " + nickname);
}
}
/*클래스 : CoffeeByName*/
package Day10_Generic.coffee;
public class CoffeeByName {
public Object name;
public CoffeeByName(Object name) {
this.name = name;
}
public void ready(){
System.out.println("커피 준비 완료 : "+name);
}
}
/*main메서드*/
public class _02_GenericClass {
public static void main(String[] args) {
//int
CoffeeByNumber c1 = new CoffeeByNumber(33);
c1.ready();
//String
CoffeeByNickname c2 = new CoffeeByNickname("우루루파도");
c2.ready();
//-------------------------------//
//Object
CoffeeByName c3 = new CoffeeByName(34);
c3.ready();
CoffeeByName c4 = new CoffeeByName("돌맹이");
c4.ready();
//c3는 Object타입이기때문에 Integer객체로 박싱됐다.
// (int)로 언박싱해줘야한다.
int c3Name = (int)c3.name;
System.out.println("주문 고객 번호 : "+c3Name);
// c4.name은 Object 타입이지만, 실제로 저장된 값은 String이다.
// 그러나 컴파일러는 이것을 알지 못하기 때문에
// 이것이 String임을 명시적으로 알려주어야 한다.
String c4Name = (String)c4.name;
System.out.println("주문 고객 이름 : "+c4Name);
//⭐⭐⭐⭐⭐Integer객체를 (String)으로 형변환하려고 하면
//ClassCastException 에러가 난다.
c4Name = (String)c3.name;
}
}
위 코드는 스타벅스 예제입니다.
커피가 준비되면 주문번호로 불리거나 닉네임으로 불립니다.
1️⃣ 자료형에 따라 CoffeeByNumber, CoffeeByNickname 두 클래스로 나눴습니다.
자료형에 따라 클래스를 계속 추가해줘야하니 번거롭습니다.
2️⃣ Object타입으로 받으니 클래스 하나로도 가능합니다.
그런데, 두가지 문제점이 보이네요.
- name을 받아올 때, 매번 형변환을 해줘야 한다는 점.
- 형변환을 할 때, 바꿀 수 없는 것을 형변환하면 ClassCastException에러가 나올 수 있다는 점.
위 문제들을 제네릭 클래스로 해결할 수 있습니다.
*제네릭 사용한 코드
/* 클래스 : Coffee */
public class Coffee <T>{
public T name;
public Coffee(T name) {
this.name = name;
}
public void ready() {
System.out.println("커피 준비 완료 : "+name);
}
}
/* main메소드 */
public class _02_GenericClass {
public static void main(String[] args) {
Coffee<Integer> c5 = new Coffee<>(35);
c5.ready();
int c5Name = c5.name;
System.out.println("주문 고객 번호 : "+ c5Name);
Coffee<String> c6 = new Coffee<>("카카오");
c6.ready();
String c6Name = c6.name;
System.out.println("주문 고객 번호 : "+ c6Name);
}
}
형변환을 따로 명시하지 않아도 됩니다.
컴파일 시 타입 체크를 수행하여, (빨간밑줄)
런타임 시 발생할 수 있는 ClassCastException에러를 사전에 방지할 수 있습니다.
5. 제네릭 타입 제한하는 법
상속을 활용하여 타입을 제한할 수 있습니다.
바로 예제 코드로 이해해보겠습니다.
/*클래스 : User*/
public class User {
public String name;
public User(String name) {
this.name = name;
}
public void addPoint(){
System.out.println(this.name + "님 포인트 적립되었습니다.");
}
}
/*클래스 : VIPUser*/
public class VIPUser extends User{
public VIPUser(String name) {
super("특별한"+name);
}
}
/*클래스 : CoffeeByUser*/
public class CoffeeByUser <T extends User>{//User클래스만 T에 들어올 수 있다.
public T user;
public CoffeeByUser(T user) {
this.user = user;
}
public void ready(){
System.out.println("커피 준비 완료 : "+user.name);
user.addPoint();
}
}
/*main 메서드*/
public class _02_GenericClass {
public static void main(String[] args) {
CoffeeByUser<User> c7 = new CoffeeByUser<>(new User("강호동"));
c7.ready();//User클래스
CoffeeByUser<User> c8 = new CoffeeByUser<>(new VIPUser("서장훈"));
c8.ready(); //User를 상속받은 클래스
CoffeeByUser<User> c9 = new CoffeeByUser<>(new OtherType("뿡뿡이"));
//new OtherType은 User클래스 또는 User클래스를 상속받은 클래스가 아니기에 에러가 납니다.
}
6. 제네릭 여러개 사용하기
public class _02_GenericClass {
public static void main(String[] args) {
orderCoffee("우루루파도","카페라떼");
}
public static <T,V> void orderCoffee(T name,V coffee){
System.out.println(coffee+" 준비 완료 : " + name);
}
}
쉼표로 구분해서 쓰면 됩니다. 참 쉽죠.
'프로그래밍 언어 > Java' 카테고리의 다른 글
[Java]Socket을 활용한 간단한 클라이언트-서버 통신 (1) | 2023.12.28 |
---|---|
[과제] 경마게임 만들기 | Thread 활용 (1) | 2023.12.20 |
[과제] UML에 맞게 싱글톤 패턴 적용하기 (0) | 2023.12.18 |
[Java] 싱글톤 패턴 개념 및 구현방법 (0) | 2023.12.18 |
[Java] 업캐스팅(Upcasting)과 다운캐스팅(Downcasting) (0) | 2023.12.18 |