[WhiteShip][14주차] 제네릭
WhiteShip Java Live Study 의 커리큘럼을 따라 개인적으로 공부한 내용입니다.
Codestates BE 42 : Study, Commercise 01 Java
Thanks to
" Codestates_SEB_BE_42 : Commercise "
강지은, 김례화, 전진우
22.11.12
14주차 : 제네릭
목차
- 제네릭 용어 정리
- 제네릭
- 제네릭 사용법
- 제네릭 클래스와 메소드
- 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
- Erasure
- + 다이아몬드 연산자 <> (자바7)
✅ 제네릭 용어 정리
class Box<T> {}
Box<String> b = new Box<String>();
- Box<T> : 제네릭 클래스 - T의 Box 또는 T Box 라고 읽는다.
- T : 타입변수 , 타입매개변수 ( T 는 타입문자 )
- Box : 원시타입 ( raw type)
- 제네릭 타입 호출
- 타입 매개변수에 타입을 지정하는 것
- 파라미터화된 타입
- 예시에서 지정된 타입 String
- 매개변수화 된 타입
- parameterized type
✅ 제네릭 (Generics)
다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시 타입체크를 해주는 기능
- 자바5에서 추가
- 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거 가능
- 타입 변환을 제거함
- 상속이 가능하다.
- 기본 타입은 사용불가함.
⏺ 제네릭의 장점 (사용하는 이유 WHY)
- 타입안전성을 제공함
- 타입안전성이란
1) 의도하지 않은 타입의 객체를 저장하는 문제를 막음
2) 저장된 객체를 꺼낼 때 형변환되어 발생할 수 있는 오류를 줄임 → 타입변환이 발생하지 않게 도와줌 - 객체의 타입을 컴파일시 체크하기 때문이다.
- 컴파일 타임에 타입의 안정성을 보장받는것이 가능함.
- 타입안전성이란
- 타입체크, 형변환을 생략할 수 있어서 코드가 간결해짐
✅ 제네릭 사용법
⏺ 타입변수 < T >
제네릭 타입 class<T> , interface<T>
클래스나 인터페이스 뒤에 <>부호를 붙히고, 사이에 타입파라미터가 위치한다.
- 일반적으로 타입변수는 대문자 알파벳 한글자로 표현하고, 반드시 T를 사용하지는 않는다.
public class 클래스명 <T> { }
public interface 인터페이스명 <T> { }
명명규칙 (가독성을 위함!)
타입 | 의미 |
T | Type |
E | Element |
N | Number |
K | Key |
V | Value |
S, U, V … | 2nd, 3rd, 4th types |
⏺ 타입변수에 실제 타입 지정
객체 생성시 실제 타입을 대입해 지정해준다.
ArrayList<Tv> tvList = new ArrayList<Tv> ();
public class ArrayList<E> extends AbstractList<E>{
private transieont E[] elementData;
public boolean add(E o){ /*생략*/ }
public E get(int index){ /*생략*/ }
}
⏺ 멀티 타입
제네릭 타입이 두개 이상인 경우 컴마구분자(,) 로 나열해 사용
public class Product <T, M>{
private T kind;
private M model;
public T getKind() {return this.kind;}
public M getModel() {return this.model;}
public void setKind(T kind){
this.kind = kind;
}
public void setModel(M model){
this.model = model;
}
}
⏺ 제네릭 타입의 다형성
참조변수에 지정해주는 제네릭 타입과 생성자에 지정해주는 제네릭 타입이 일치해야한다.
ArrayList<Tv> list1 = new ArrayList<Tv>(); // 가능
ArrayList<Product> list2 = new ArrayList<Tv>(); // 에러
class Product {}
class Tv extends Product {}
* 클래스 타입간 다형성 적용은 가능하다.
List <Tv> list1 = new ArrayList<Tv>(); // 가능
List <Tv> list2 = new LinkedList<Tv>(); // 가능
* Product의 자손 객체만 저장하고 싶다면, Product로 제네릭타입을 생성하고, 자손(Tv...) 객체를 저장하면 된다.
ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());
list.add(new Tv());
⏺ Iterator<E>
iterator에도 제네릭이 적용되어 있다.
제네릭이 도입되면서 이터레이터(반복자)를 적용해 해당 타입의 next()값을 가져올 수 있는것임 !
public interface Iterator<E>{
boolean hasNext();
E next();
void remove();
}
✅ 제네릭 클래스와 메소드
⏺ 제네릭 클래스
interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }
// 특정 타입만 지정가능하도록 제한
class Basket<T extends Flower & Plant> {
private T item;
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
// 에러 : static 메서드 안에서 T 사용 불가능
static int compare(T t1, T t2){...}
}
public static void main(String[] args) {
// 인스턴스화
Basket<Flower> flowerBasket = new Basket<>();
Basket<Rose> roseBasket = new Basket<>();
}
클래스의 타입 매개변수는 클래스가 인스턴스화 될때 타입이 확정된다.
💥 주의할점 💥
1) static은 모든 객체에 대해 동일하게 동작하기 때문에,
static멤버(필드,메서드)에 제네릭 클래스의 타입변수 T를 사용할 수 없다.
(T는 인스턴스 변수로 간주)
class Box <T>{
static T item ; // 에러
}
// static을 쓰면, Box<tool>.item 과 Box<Chicken>.item은 다른것이 되면 안된다는 의미임
// 공구와 치킨은 다른것이라 모순이 됨
2) 제네릭 타입 배열을 생성할 수 없다.
- 제네릭 배열 타입의 참조변수를 선언하는 것은 가능함
- 배열 생성하는것은 불가능하다. (new연산자 때문에)
- new는 컴파일 시점에 T가 무엇인지 정확하게 알아야하는데,
배열을 생성하면, T는 런타입에 결정되기 때문에 생성할 수 없는것!
- new는 컴파일 시점에 T가 무엇인지 정확하게 알아야하는데,
class Box <T>{
T[] items;
T[] toArray(){
T[] tmp = new T[item.length]; // 에러 : 제네릭 배열은 생성 불가
return tmp
}
}
3) 같은 이유로 instanceOf 역시 피연산자로 T 사용 불가능하다.
⏺ 제네릭 메소드?
메서드 선언부에 제네릭 타입이 선언된 메서드
- 타입 매개변수 선언은 반환타입 앞에 작성된다.
- 해당 메서드 내에서만 선언한 타입 매개변수 사용 가능
- 제네릭 메서드는 제네릭 클래스가 아닌 클래스에도 정의 될 수 있다.
- 제네릭 메서드의 타입 매개변수는 제네릭 클래스의 타입 매개변수와 다른것이다.
- 클래스의 타입매개변수는 클래스가 인스턴스화 될때 타입이 확정된다.
- 반면 제네릭 메서드의 타입 지정은 메서드가 호출될 때 이루어진다.
// 1 : 여기에서 선언한 타입 매개변수 T와
class Basket<T> {
...
// 2 : 여기에서 선언한 타입 매개변수 T는 서로 다른 것입니다.
public <T> void add(T element) {
...
}
// static 메서드에도 선언 가능
static <T> int setPrice(T element) {
...
}
// 메서드 정의 시점에서 사용 불가능한 경우
public <T> void print(T item) {
System.out.println(item.length()); // 불가 : 정의하는 시점에서 item이 String 인지 알 수 없으므로
System.out.println(item.equals("Kim coding")); // 가능 : Object 의 메서드는 활용 가능
}
}
✅ 제네릭 주요개념 (바운디드 타입, 와일드카드)
⏺ Bounded Type Parameters (제한된 제네릭 클래스)
타입 매개변수 T 에 지정할 수 있는 타입의 종류를 제한하는 방법
// bounded type parameters 미사용
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
// bounded type parameters 사용시
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
- 특정 타입만 받도록 지정 가능 ( == 바운디드 타입 매개변수 == 경계 타입 매개변수)
- extends 키워드를 사용하여 특정 타입의 자손들만 대입가능하게 설정 가능
- 인터페이스나 클래스 관계없이 extends 를 사용
- 여러 개를 사용시 & 사용
- 여러 타입을 제한하고 싶을때 & 사용시 클래스가 인터페이스보다 앞쪽에 위치해야한다.
- 예시에서 위의 경우는 매개변수로 받는 elem 이 비교가 가능한대상인지 확정되지 않아 컴파일 에러가 발생하게 된다.
- 컴파일 타임에 에러가 난다는것 역시 중요한 점이다. : 타입을 지정하여 컴파일 타임에 타입의 안정성을 보장
- 이를 아래의 경우로 변경하여 경계타입매개변수를 <T extends Comparable<T>> 와 같이 지정하여 비교가 가능한 타입만 받도록 제한하여 해결할 수 있다.
💻 정수, 실수 관계없이 숫자만 사용하도록 제한
public class GenericClass<T extends Number> {
private T value;
public GenericClass(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
→ String은 Number를 상속받은 객체가 아니라서 컴파일 에러 발생
⏺ 와일드 카드 ( ? )
어떤 타입도 받을 수 있게 사용하는 것.
- 제네릭 타입에 다형성을 적용하는 방법이다
- 의미하는 바를 명확히 하기 위한 목적이 있다.
상한제한(Upper Bound), 하한제한(Lower Bound), 제한 없음(Unbounded Wildcard) 존재
<? extends T> // 상한 제한, T와 그 자손들 만 가능
<? super T> // 하한 제한 ,T와 그 조상들만 가능
<T> // 제한 없음 == <? extends Object>
- 와일드카드는 상한/하한 둘 다 지정할 수 없다.
1️⃣ 상한 제한 (Upper Bound Wildcard)
- 상한 와일드카드를 사용해 변수에 대한 제한을 완화할 수 있다.
Example
List<Integer> , List<Double> 및 List<Number> 에서 작동하는 메서드를 작성하려고 한다고 가정
List<Number> 는 List<? extends Number>보다 더 제한적이다
왜? 전자가 Number 유형과 일치여부를 체크
후자는 Number 유형 또는 그 하위 유형들과 일치여부 체크
sumOfList : 목록에 있는 숫자의 합계를 반환
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
<? extends Number> : Number과 Number의 하위 클래스인, Integer, Double모두 가능하다.
List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));
List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));
2️⃣ 하한 제한 (Lower Bound Wildcard)
특정 유형 또는 해당 유형의 super유형 으로 제한한다.
example
Integer 개체를 목록에 넣는 메서드를 작성한다고 가정
유연성을 최대화하려면 메서드가 List<Integer> , List<Number> 및 List<Object>에서 작동하게 해야함.
즉, Integer 값 을 보유할 수 있는 타입은 모두 작동할 수 있어야한다.
List<Integer> 라는 용어 는 List<? super Integer> 보다 더 제한적이다
왜? 전자는 Integer 유형과 일치여부를 체크,
후자는 Integer또는 Integer의 상위 유형들과 일치여부를 체크
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
3️⃣ 제한 없음(Unbounded Wildcard)
unbounded wildcard가 유용한 경우는 2가지이다.
- Object 클래스 에서 제공하는 기능을 사용하여 구현할 수 있는 메서드를 작성하는 경우 .
- 코드가 형식 매개변수에 의존하지 않는 제네릭 클래스의 메서드를 사용하는 경우.
ex : List.size, List.clear
- Class<?>는 ::: Class<T> 에 있는 대부분의 메서드가 T 에 의존하지 않을 때 자주 사용된다.
printList 메소드 : any type의 List를 출력하는 것이 목표라고 하자.
근데 아래 코드는 Object 인스턴스만 출력할 수 있다. (목표 달성 실패!)
→ List<Integer>, List<String>, List<Double>은 불가능
WHY ? :: List<Object>의 subtype이 아니기 때문이다.이런 경우에 List<?>를 써야한다.
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
* List<Object> 와 List<?> 는 같지 않다는 점에 유의할 것.
++ List 와 List<Object> 의 차이
List<Object>는 List의 다른 매개변수 타입과 와 호환되지 않는다 (Object만 허용한다.)
그러나 List(raw)는 모든 매개변수 List와 호환된다.
출처 : https://stackoverflow.com/questions/7885920/java-difference-between-list-and-listobject
- 어떤 메서드에 파라미터로 List 나 List<Object> 가 명시된 상황에서의 차이
- List 는 명확히 제네릭이 아님
- raw type 이며 제네릭 이전의 사용과의 호환성으로 만들어졌음
- 따라서 List<String>, List<Integer> 과 같은 모든 타입을 수용가능하다.
- List<Object>는 Object 타입을 허용한다는 의사를 컴파일러에게 명확히 전달하는 꼴
- List<Object> 가 아니고는 수용이 불가능하다.
- List 와 List<Object> 타입은 전혀 다른 파라미터화된 타입을 가지며 각각 하나의 독립된 타입이므로 서로를 관계가 없다.
- 타입 매개변수가 일치하고 두 클래스가 상속관계에 있다면 상속이 가능하다.
- Box → SteelBox
- List<Object> 가 아니고는 수용이 불가능하다.
List<String> a = new ArrayList<String>();
List<Object> b = a; // compile error
List c = a; // fine
⏺ 제네릭 타입의 형변환
- 제네릭타입과 원시타입간의 형변환은 가능하다. (단, 경고가 발생한다.)
- 대입된 타입(파라미터화 된 타입)이 다른 제네릭 타입간의 형변환
- 대입된 타입이 Object 일지라도 대입된 타입이 다르면 형변환이 불가능하다.
Box<Object> objBox = null; Box<String> strBox = null; objBox = (Box<Object>) strBox; // 에러 strBox = (Box<String>) objBox; // 에러 Box<? extends Object> wBox = new Box<String>(); // OK
- Box 를 Box 으로 직접 형변환하는것은 불가능
- 와일드카드가 포함된 제네릭 타입으로 형변환 하면 가능 (단, 확인되지않은 타입으로 형변환 한다는 경고 발생)
- 마찬가지로 와일드카드가 사용된 제네릭 타입끼리도 형변환이 가능하다.
💻 와일드 카드 예시
// 와일드 카드 사용 이전 코드 // 과일박스를 넣으면 주스를 만들어 반환하는 Juicer class Juicer { ... // 타입매개변수 대신 특정 타입을 지정해준 상황 static Juice makeJuice ( FruitBox<Fruit> box) { String temp = ""; for (Fruit f : box.getList()) { temp += f + ""; } return new Juice(temp); } }
- 우선 Juicer는 제네릭 클래스가 아니다. 또한 makeJuice()는 static 메서드이다.
- FruitBox 로 고정시 FruitBox 과 같은 다른 타입을 매개변수로 대입할 수 없다.
- 타입이 맞지 않으므로
- 메서드 오버로딩을 통해 만들면 되지 않나 ?
- 제네릭 타입이 다른것만으로는 오버로딩이 성립하지않는다
- 제네릭 타입은 컴파일러가 컴파일 할때만 사용하고 제거해버린다.
- 이같은 상황에서 와일드 카드를 도입하여 사용한다.
- 위의 예시에서 와일드 카드 도입시
static Juice makeJuice ( FruitBox<? extends Fruit> box) { }
- FruitBox< 모든 종류 Fruit 및 그 이하 클래스> 를 수용 가능하게된다.
누군가 말한 요약.. 아직 이해 불가능
제네릭 : 지금은 이 타입을 모르지만, 이 타입이 정해지면 그 타입 특성에 맞게 사용하겠다.
와일드 카드 : 지금도 이 타입을 모르고, 앞으로도 모를 것이다.
✅ Erasure (삭제, 소거)
제네릭을 사용하면 그 타입은 컴파일 후에 지워지게 된다.
즉 컴파일 타임에만, 타입에 대한 제약 조건을 적용하고,
컴파일러가 타입변수를 실제 유형으로 바꿔버리기 때문에 런타임에는 타입에 대한 정보를 소거해 해당 타입 정보를 알 수 없다.
List<String> myList = new ArrayList<String>(); // 코딩 내용
ArrayList myList = new ArrayList(); // 디컴파일 결과
변수명은 동일하지만 제네릭 타입 파라미터는 제거되었다.
→ 타입 파라미터는 컴파일러에 의해 해석되는 부분이고 자바 가상 머신에서는 해석이 되지 않기 때문이다.
그래서 제네릭은 런타임에 체크하는 것이 아니라 컴파일 시에 정합성을 체크하게 된다.
→ 자바 가상머신은 제네릭을 고려하지 않고 실행된다. (제네릭이 제거된 기본 클래스형으로만 처리)
→ 가상머신상에서 제네릭 코드를 제거하는 이유는
제네릭을 해석하기 위해 추가적인 자원 소모를 없애고, 빠르고 명확하게 동작하도록 하기 위해서 사용한다.
⏺ 제네릭 타입 제거
1) T를 실제 타입 파라미터 / Object로 바꾼다.
2) 타입 안전성 보존을 위해 필요시 type casting을 넣는다.
- 확장된 제네릭 타입에서 다형성 보존을 위해 3) bridge method를 생성한다.
1-1 ) 컴파일러가 <E>는 unbouned이기 때문에 E를 Object로 바꿔준다.
public class Stack<E> {
private E[] stackContent;
public Stack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data) {
// ..
}
public E pop() {
// ..
}
}
public class Stack {
private Object[] stackContent;
public Stack(int capacity) {
this.stackContent = (Object[]) new Object[capacity];
}
public void push(Object data) {
// ..
}
public Object pop() {
// ..
}
}
1-2) 만약 bound 되었다면
제네릭 타입의 경계(bound)를 제거한다.
첫번째로 바운드된 클래스 (예제 코드에선 Comparable이다.) 로 바꾼다.
public class BoundStack<E extends Comparable<E>> {
private E[] stackContent;
public BoundStack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data) {
// ..
}
public E pop() {
// ..
}
}
public class BoundStack {
private Comparable [] stackContent;
public BoundStack(int capacity) {
this.stackContent = (Comparable[]) new Object[capacity];
}
public void push(Comparable data) {
// ..
}
public Comparable pop() {
// ..
}
}
2) 제네릭 타입을 제거한 후 타입이 일치하지 않으면 형변환을 추가한다.
- List의 get() 의 경우 Object 타입을 반환하므로 형변환이 필요하다.
- 따라서 형변환을 추가해준다.
- 와일드 카드 가 포함된 경우 역시 적절한 형변환과정을 추가한다.
T get(int i) {
return list.get(i); // list 의 get() 은 Object 형을 반환
}
----------------------------------
Fruit get(int i) {
return (Fruit) list.get(i);
}
3) 브릿지 메서드
- 매개변수화된 클래스를 확장하거나, 메소드 서명이 약간 다르거나,
모호할 수 있는 매개변수화된 인터페이스를 구현하는 클래스 또는 인터페이스를 컴파일하는 동안
Java 컴파일러에 의해 생성된 합성 메소드 - 메서드의 매개변수나 리턴타입을 소거하기 위해서 만들어지는 메서드
public class SomeList extends ArrayList<String> {
@Override
public boolean add(String e) {
...
}
}
void Test() {
ArrayList<String> list = new SomeList();
list.add("string");
}
- ArrayList의 타입소거된 add() 메서드를 호출할 경우 SomeList에는 add(Object e) 메서드가 존재하지 않기 때문에 이를 위해 브릿지 메서드를 생성한다.
+ 다이아몬드 연산자 (자바 7)
* 객체 선언부에 <> 를 기술하면 변수 선언 시에 사용한 제네릭 타입이 그대로 적용 됨
Map<String, List<String>> myMap = new HashMap<>();
* 다이아몬드 연산자는 오직 객체를 생성하는 부분에서만 사용할 수 있다.
아래는 컴파일러가 제네릭의 타입을 추정할 때 변수에 선언해 놓은 파라미터를 바탕으로 추정하기 때문에 컴파일 에러 발생
Map<> myMap = new HashMap<String, List<String>>(); // 컴파일 에러 발생
* (자바 10) 변수를 선언 시 특정 클래스 지정안하고, 타입추론 가능하게 var 사용가능
var myMap = new HashMap<String, List<String>>();
references
- 오라클 공식문서
- 남궁성 - 자바의 정석
- https://www.baeldung.com/java-type-erasure
- https://jyami.tistory.com/99
- https://bingbingpa.github.io/whiteship-live-study-week14/
- https://docs.oracle.com/javase/tutorial/java/generics/bounded.html
- https://vvshinevv.tistory.com/54
- https://devfunny.tistory.com/563
- https://blog.hexabrain.net/379