-
[Java] 제네릭/지네릭스(Generics)란?Java 2021. 12. 30. 16:39728x90
제네릭(Generics)란?
다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능입니다.
객체의 타입을 컴파일 시에 체크하기 때문에 의도하지 않은 타입의 객체가 저장되는 것을 막고 다른 타입으로 잘못 형변환 되어 발생할 수 있는 오류를 줄여줍니다.
제네릭을 사용하는 이유? (등장배경)
ArrayList와 같은 컬랙션 클래스에는 다양한 종류의 객체를 담을 수 있지만 보통 한 종류의 객체를 담는 경우가 많습니다.
객체들을 꺼낼 때 마다 타입체크를 하고 형변환을 하는 것을 불편하고, 원하지 않는 종류의 객체가 포함되는 것을 막을 수 없었습니다. 이러한 문제들을 해결하기 위해 제너릭스가 등장하게 되었습니다.
사용예시
ArrayList aList = new ArrayList(); aList.add("hello"); aList.add("java"); String hello = (String) aList.get(0); String java = (String) aList.get(1);
제네릭이 도입되기 전에는 일일히 데이터를 형변환해주어야 합니다.
이때 String이 아닌값이 들어간다면..? 에러가 발생할 수 있습니다.
ArrayList<String> aList = new ArrayList<String>(); aList.add("hello"); aList.add("java"); String hello = aList.get(0); // 형 변환이 필요없다. String java = aList.get(1); // 형 변환이 필요없다.
위의 예제는 제네릭을 사용한 예제입니다.
<>를 제네릭이라 하며, <>안에 어떠한 타입을 선언해주어 해당 ArrayList 컬랙션이 사용할 객체의 타입을 지정해줍니다.
여기서는 String 타입을 명시해주었기 때문에 aList객체에 int 타입이나 다른 타입을 add할 경우 컴파일 시 에러가 발생하게 됩니다.
또한 제네릭이 도입되기 전에는 ArrayList 컬랙션안에 추가되는 객체가 Object로 인식되기 때문에 항상 String으로 형변환을 해주어야 하는 불편함이 있지만 제네릭을 사용한다면 이를 해결할 수 있습니다.
제네릭의 장점
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;} }
일반적인 클래스와 제너릭 클래스를 비교하였을때 클래스명에 <T>가 붙으며 Object가 모두 T로 바뀐모습을 볼 수 있습니다.
여기서 T란 무엇일까요?
T는 타입 변수(type variable)이라고 하며 일종의 매개변수같은 역할을 수행합니다.
그 자리에 어떠한 참조형 타입이 들어갈 수 있다는 것을 표시해줍니다.
임의의 변수이므로 T가 아니어도 되며 Element의 E, Key Value의 K,V를 사용하기도 합니다.
이처럼 T말고도 상황에 맞는 의미있는 문자를 선택해서 사용하시면 됩니다.
제네릭 클래스 다루어보기
Box<String> b= new Box<String>(); // 실제 타입을 지정 b.setItem("ABC"); // OK b.setItem(3); // Err! 스트링 타입만 넣어야 함
타입 변수 T자리에 String을 넣어서 Box 객체를 가진 b를 선언했습니다.
이후 setItem 메소드를 사용해 String 타입인 "ABC"를 item으로 설정하였습니다.
이후 setItem 메소드를 사용해 int형 타입인 3을 item으로 설정했을 때는 String이 아니기 때문에 에러가 발생하게 됩니다.
사용시 주의사항
제너릭스 클래스에 허용되지 않는 두가지가 있습니다.
1. static 멤버에 대하여 타입 매개변수를 사용하는 것
2. 제너릭 타입의 배열을 생성하는 것
1번의 이유
static은 시스템이 실행되었을 때 메모리로 바로 올라가 모든 객체에 대해 동일하게 동작해야 하지만 T는 인스턴스 변수로 달라질 수 있습니다.
하지만 제너릭스 메서드에 대해서는 타입 매개변수가 지역변수로 사용되기 때문에 static이여도 상관 없습니다.
2번의 이유
배열의 new 연산자 때문에 컴파일 시점에 타입 T가 뭔지 정확히 알아야 하지만 T가 어떤 타입이 될지 전혀 알 수 없습니다.
제한된 제너릭스 클래스
타입 문자(예시 T)로 사용할 타입을 지정하는 경우 모든 종류의 타입을 지정할 수 있는데 이 종류를 제한하기 위해 사용됩니다.
만약 과일박스 제너릭 클래스를 선언했는데 T타입에 장난감이 들어가버리면 곤란하기 때문에 T의 타입을 제한해야 하는 경우가 생길 수 있습니다.
class FruitBox<T extneds Fruit> {...}
위의 코드 처럼 extends Fruit을 이용하게 된다면 T는 Fruit, Fruit을 상속하는 객체로 제한됩니다.
이 때 인터페이스로 제한을 둘 경우에도 implements가 아닌 extends를 사용합니다.
class FruitBox<T extends Fruit & Eatble>{...}
이때 &기호를 사용하여 같이 사용할 수도 있습니다.
와일드카드 <?>
만약에 매개변수에 과일박스를 대입하면 쥬스를 만들어서 반환하는 Juicer라는 클래스가 존재하고 이 클래스에는 과일을 주스로 만들어서 반환하는 makeJuice()라는 static메서드가 있다고 가정해보겠습니다.
class Juicer { static Juice makeJuice(FruitBox<Fruit> box){ String temp =""; for(Fruit f : box.getList()) temp += f + " "; return new Juice(tmp); } }
Juicer 클래스는 제너릭스 클래스가 아니며 만약 제너릭스 클래스라 하더라도 static 메서드에는 T 매개변수를 사용할 수 없으므로 제너릭스를 적용하지 않던가, 위와 같이 T 매개변수 대신에 특정 타입을 지정해줘야 합니다.
하지만 위처럼 FruitBox<Fruit>으로 고정해 놓으면 FruitBox<Apple> 타입의 객체는 makeJuice 메서드를 사용할 수 없을 것입니다.
이를 해결하기 위해 등장한 것이 바로 '와일드 카드' 입니다.
와일크 카드는 기호 "?"로 표현하며 어떠한 타입도 될 수 있습니다.
?는 어떠한 타입도 될 수 있기 때문에 Ojbect타입과 동일하게 여겨질 수 있습니다.
따라서 extends와 super를 사용하여 상한과 하한을 제한할 수 있습니다.
<? extends T> // 와일드 카드의 상한 제한. T와 그 자손들만 가능 <? super T> // 와일드 카드의 하한 제한. T와 그 조상들만 가능 <?> // 제한 없음. <? extends Obejct>와 동일한 기능
와일드 카드를 사용한다면 FruitBox<Fruit> 대신에 FruitBox<? extends Fruit>를 사용함으로써 FruitBox<Apple>, FruitBox<Grape> 등이 가능하게 됩니다.
출처
자바의 정석
https://gangnam-americano.tistory.com/47
https://history1994.tistory.com/20
'Java' 카테고리의 다른 글
[Java] 오버로딩(Overloading)이란? (0) 2022.01.05 [Java] 자바 Enum이란? (0) 2022.01.01 [Java] 어노테이션(Annotation) (0) 2021.12.20 [Java] 인터페이스(Interface) (0) 2021.12.16 [Java] 추상클래스(Abstract Class) (0) 2021.12.15