-
Generic 총정리java/Generic 2022. 7. 2. 03:59
1. Generic 클래스의 타입 변수에 대입될 수 있는 것들
- primitive 타입(int,long,double,float ....등등)이 아닌 모든 것
primitive배열 타입 //int[] , double[] ...
참조 타입 배열 //String[], Integer[] ...
인터페이스 타입
클래스 타입
2. Generic 클래스의 상속 관계
class Box<T> {}
Object와 String은 조상 자손 관계라서 다형성이 적용되지만
Box<Object>와 Box<String>은 아무런 관계가 없다.
Box<Object>와 Box<String>모두 Object클래스의 자손이다.
지네릭 클래스를 상속한 지네릭 클래스의 경우 다음과 같이 된다.
class Box<T>{} class ChildBox<T> extends Box<T>{}
3. 변수 선언시 타입 선언
Box<Integer> box1;//무조건 대입된 타입이 Integer인 인스턴스만 box1에 대입될 수 있다. Box<Object> box2;//무조건 대입된 타입이 Object인 인스턴스만 box2에 대입될 수 있다.
java7부터 인스턴스 생성시 다이아몬드 <>를 사용하면 대입될 타입을 생략할 수 있다.
class Box<T> { } public class Program1 { public static void main(String[] args) { Box<Object> box; //컴파일러는 box변수의 타입 Box<Object>을 확인하여 = new Box<>();시에 //= new Box<Object>();로 타입 추론한다. box = new Box<>(); } }
4. Generic 클래스 선언시 선언부의 제네릭 타입 변수에 대입될 수 있는 대입될 수 있는 타입의 종류를 제한
대입될 수 있는 타입의 제한은 1. 클래스 선언부 2. 제네릭 메서드 에 사용가능한 문법이다.
우선 1. 클래스 선언부의 대입될 수 있는 타입의 종류 제한을 먼저 살펴보고 2. 제네릭 메서드는 뒷부분에 정리하였다.
클래스 상속도
class A { } class B extends A { } class C extends B { } class D extends C { }
- 대입될 수 있는 타입의 제한이 없는 경우
아래의 제네릭 클래스 Box에는 선언시에 타입 변수 T에 아무런 제약이 걸려있지 않기 때문에
T에는 모든 타입(1. Generic 클래스의 타입 변수에 대입될 수 있는 것들) 이 대입될 수 있다.
class Box<T> { //대입 가능한 모든 타입이 T에 대입될 수 있다. private T item; public T getItem() { return item; } public void setItem(T item) { this.item = item; } }
아래와 같이 타입 변수 T에 1. Generic 클래스의 타입 변수에 대입될 수 있는 것들 모두 대입가능하다.
- 대입될 수 있는 타입 제한이 <T extends C> 인 경우
제네릭 클래스 선언시 타입에 대입될 수 있는 타입을 C 또는 C의 자손으로 제한
이제 Box 제네릭 클래스의 타입 변수 T에는 C와 C를 상속한 타입 이외의 타입은 대입될 수 없다.
class Box<T extends C> { private T item; public T getItem() { return item; } public void setItem(T item) { this.item = item; } }
- 클래스를 상속했고 인터페이스를 구현해야 하는 타입으로 제한
1. 클래스 상속 조건이 없고 인터페이스만 구현 필요라면 <T extends 인터페이스명> (implements가 아님에 주의)
class A { } class B extends A { } class C extends B { } class D extends C { } interface Inter { } class InterImpl implements Inter { } class InterImpl2 implements Inter { }
ex) 대입될 수 있는 타입을 Inter 인터페이스를 구현한 타입으로만 제한
class Box<T extends Inter> { private T item; public T getItem() { return item; } public void setItem(T item) { this.item = item; } }
2. 클래스를 상속해야 하면서 인터페이스를 구현해야 하는 경우 ( <extends 클래스명 & 인터페이스명> )
<extends 인터페이스 & 클래스명>이 아니라 반드시 클래스를 먼저 선언해야한다!!!
ex) 대입될 수 있는 타입을 B또는 B의 sub 클래스여야하며 and 동시에 Inter 인터페이스를 구현한 타입으로 제한
class A implements Inter { } class B extends A implements Inter { } class C extends B implements Inter { } class D extends C implements Inter { } class InterImpl implements Inter { } class InterImpl2 implements Inter { } interface Inter { } class Box<T extends B & Inter> { private T item; public T getItem() { return item; } public void setItem(T item) { this.item = item; } }
- 상속 관계에 있는 두 제네릭 클래스의 경우 대입될 타입 제한을 어떻게 걸어야되고 어떻게 해석해야 할까?
위의 에러는 타입 제한을 맞춰붜서 에러가 난것이 아니라 사용해서는 안되는 곳에 타입 제한 문법을 사용해서 발생한 에러이다.
일단 대입될 타입 제한은 선언된 제네릭 클래스의 타입 변수쪽에 사용하는 있는 문법이다.
제네릭 ChildBox 클래스 선언에 해당하는 자기 자신의 타입 변수(왼쪽)에만 제한을 걸수 있으며 extends Box쪽(오른쪽)에는 타입 제한을 거는 것 자체가 문법 오류이다.
그렇기 때문에 다음과 같이 우측에는 무조건 T로 선언해야한다.
에러가 난것은 위와 아래의 제한을 잘 맞춰주지 않아서 발생한 것이다.
타입변수의 타입제한은 선언된 클래스쪽을 기준으로 한다고 했다.
ChildBox<T>를 통해 T에는 모든 타입이 들어갈 수 있다. 이에 맞춰 우측 Box<T>를 해석해보면 Box에도 모든 타입이 들어갈 수 있다라는 의미이다. 그러나 Box클래스 선언을 보면 B또는 B의 자식들만 들어갈 수 있다.
이에 맞춰 ChildBox쪽에 타입제한을 걸어줘야한다.
제네릭 Box 클래스의 선언부로부터 Box<T extends B>를 확인해보면 Box에 대입될 타입이 B또는 B의 자손들만 대입될 수 있다. 이 규칙에 맞게만 ChildBox의 옆 타입변수를 맞춰주면 에러가 나지 않는다.
제네릭 클래스 ChildBox에 대입될 수 있는 타입을 ChildBox<E extends B>로 맞춰추면 E에는 B또는 B자손들만 대입될 수 있고 extends Box<E>쪽의 조건또한 class Box<T extends B>를 어기지 않게된다.
class Box<T extends B>의 조건인 Box에 대입될 타입이 B포함 B의 자손들만 어기지 않으면 되므로
ChildBox에는 C포함 C의 자손들만 담기게 할 수 있다.
각 제네릭 클래스에서 사용하는 타입 변수 문자가 동일하다면 다음과 같이 되겠다.
static inner 클래스에 선언되어 있는 타입변수는 외부 클래스의 타입변수와 별개로 정의된 것임
package generic; public class StaticInnerClassTypeVariable { // ok Box.InnerBox<A> innerABox; } class Box<T extends B> { // InnerBox의 타입 변수 T와 Box의 타입 변수 T와 별개 public static class InnerBox<T> { } } class A { } class B extends A { }
5. Generic 클래스의 타입 변수의 범위와 사용할 수 없는 곳
클래스 선언부에 선언된 타입 변수는 제네릭 클래스 전체구간에서 사용될 수 있다.
클래스 선언부에 선언된 타입 변수를 사용할 수 없는 곳
- static 메서드, static 필드에는 제네릭 클래스의 타입 변수를 사용할 수 없다.
(제네릭 타입이 제거된 후 Box의 static 멤버는 모두 Box<대입된타입>에 관계없이 동일한 동작을 해야하기 때문)
- 타입 변수(T가정)를 사용하여 new T()를 사용할 수 없다.
- 제네릭 클래스 내부에서(타입변수가 T인 경우가정) 특정객체 instanceof T를 사용할 수 없다.
6. Generic 메서드
클래스 선언부 이외에 타입 변수가 선언된 메서드를 제네릭 메서드라고 한다. 클래스 선언부에 타입 변수의 선언 유무로 일반 클래스 제네릭 클래스를 나누며 제네릭 메서드는 클래스와 별개이다. 즉 일반 클래스도 제네릭 메서드를 선언할 수 있다.
instance 메서드, static 메서드 둘다 타입 변수를 선언할 수 있다 (리턴타입 왼쪽에 타입 변수를 선언)
제네릭 메서드에서 선언된 타입 변수는 해당 메서드(파라메터 선언부와 바디)에서만 사용가능하다.
static 제네릭 메서드의 타입 변수 문자와 제네릭 클래스의 타입 변수 문자가 겹쳐도 클래스와 인스턴스 입장이므로 서로 개별적인 것으로 취급된다. (static 메서드에는 클래스 선언부의 타입 변수를 사용할 수 없는 것과 혼동 x)
instance 제네릭 메서드의 타입 변수 문자와 제네릭 클래스의 타입 변수 문자가 동일하며 겹치는 경우
instance 제네릭 메서드의 선언부에 선언된 타입 변수가 우선시 되며 클래스 선언부의 타입 변수는 무시된다(hiding)
class GenericClass<T> { // instance 제네릭 메서드의 타입 변수가 제네릭 클래스의 타입 변수와 겹치면 메서드의 것 우선 public <T> T genericMethod(T t) { return t; } // static 일반 메서드에는 제네릭 클래스 선언부의 타입 변수를 사용할 수 없다. //public static T staticNormalMethod() { // 에러 //} // static 제네릭 메서드의 타입 변수는 인스턴스와 관련이 없으므로 제네틱 타입 변수와 별개 public static <T> T staticGenericMethod(T t) { return t; } } public class Program1 { public static void main(String[] args) { //클래스 선언부의 타입변수 T(String,Object)가 hiding 되고 Integer가 사용됨 Integer integer = new GenericClass<String>().genericMethod(Integer.valueOf(100)); Integer integer2 = new GenericClass<Object>().genericMethod(Integer.valueOf(100)); String str = GenericClass.staticGenericMethod(new String("헬로우")); } }
제네릭 클래스의 대입될 타입의 제한(4번 항목)처럼 제네릭 메서드에도 타입 변수 선언부에 대입될 타입 제한을 걸수 있다.
7. 와일드 카드
아래와 같이 객체의 hashcCode를 출력하는 printObjectHashCode 메서드가 선언되어 있다.
Object와 String간에는 상속 관계가 존재하지만 ArrayList<Object>
import java.util.ArrayList; public class NoWildcardProblem { public static void main(String[] args) { ArrayList<String> stringArrayList = new ArrayList<>(); stringArrayList.add("가"); stringArrayList.add("나"); stringArrayList.add("다"); printObjectHashCode(stringArrayList); // 컴파일 에러 } public static void printObjectHashCode(ArrayList<Object> arrayList) { // Object의 해시코드를 출력한다. for (Object obj : arrayList) { System.out.println(obj.hashCode()); } } }
와일드 카드를 사용하면 ArrayList<Object>와 ArrayList<String>간에 상속관계가 생긴 것은 아니지만 다형성을 적용할 수 있게 도와준다. 와일드 카드에 대해서 알아보고 와일드 카드 종류에 따라 사용가능한 코드와 불가능한 코드를 살펴보자.
7-1. 언바운드 와일드 카드 <?>
미지의 타입이 대입될 수 있다.
런타임에 unboundArrayList에는 어떤 타입의 ArrayList객체가 넘어올지 모른다.
import java.util.ArrayList; public class UnboundWildcard { public static void main(String[] args) { unboundWildcard(new ArrayList<Object>()); unboundWildcard(new ArrayList<String>()); unboundWildcard(new ArrayList<Number>()); unboundWildcard(new ArrayList<Integer>()); unboundWildcard(new ArrayList<int[]>()); } public static void unboundWildcard(ArrayList<?> unboundArrayList) { } }
메서드 내부에서 unboundArrayList 변수를 통해 ArrayList의 어떤 메서드를 사용할 수 있을까??
- 런타임시 어떤 타입이 넘어올지 모르기 때문에 아이템 추가(produce)작업이 존재하는 코드는 컴파일러가 컴파일 에러를 낸다. produce작업(= 객체 변경, 객체 추가)
import java.util.ArrayList; public class UnboundWildcardProduceImpossible { public static void main(String[] args) { unboundWildcard(new ArrayList<Object>()); unboundWildcard(new ArrayList<String>()); unboundWildcard(new ArrayList<Number>()); unboundWildcard(new ArrayList<Integer>()); unboundWildcard(new ArrayList<int[]>()); } public static void produceItem(ArrayList<?> unboundArrayList) { // ArrayList<Object>이외의 타입이 넘어올 수 있다. unboundArrayList.add(new Object()); //컴파일 에러 // ArrayList<String>이외의 타입이 넘어올 수 있다. unboundArrayList.add("test"); //컴파일 에러 // 컴파일러는 이러한 객체 추가(produce) 작업인 // ArrayList의 add()메서드를 사용할 수 없게 한다. } }
- 객체 삭제 작업은 각 대입된 타입 확인을 통해 실시되기 때문에 문제가 되지 않는다.
import java.util.ArrayList; public class UnboundWildcardRemoveItem { public static void main(String[] args) { ArrayList<Object> objects = new ArrayList<>(); objects.add(new Object()); objects.add(new Object()); clearItem(objects); System.out.println("ArrayList<Object> 사이즈: " + objects.size()); ArrayList<String> strings = new ArrayList<>(); strings.add("가"); strings.add("나"); clearItem(strings); System.out.println("ArrayList<String> 사이즈: " + strings.size()); } public static void clearItem(ArrayList<?> unboundArrayList) { unboundArrayList.clear(); } }
- 객체를 사용(Consume)시 Object타입으로만 받아올 수 있다.
미지의 타입이 대입되더라도 ArrayList<미지의타입> 에 들어있는 각 아이템들은 모두 Object형으로 참조될 수 있기때문에 내부 아이템을 읽을 때 컴파일러는 Object형으로 아이템을 리턴시켜준다.
import java.util.ArrayList; public class UnboundWildcardConsumeItem { public static void main(String[] args) { ArrayList<Object> objects = new ArrayList<>(); objects.add(new Object()); objects.add(new Object()); consumeItem(objects); ArrayList<String> strings = new ArrayList<>(); strings.add("가"); strings.add("나"); consumeItem(strings); } public static void consumeItem(ArrayList<?> unboundArrayList) { for (Object obj : unboundArrayList) { System.out.println(obj); } System.out.println(); } }
7-2. 상한 경계 와일드카드 <? extends 특정타입>
특정타입 또는 특정타입을 상속한 타입만 받을 수 있다.
번외: 4번 항목의 대입될 수 있는 타입을 제한하는 문법과 상한 경계 와일드카드 문법의 개념이 헷갈릴 수도 있으니 추가하였다.
class A { } class B extends A { } class C extends B { } class D extends C { } class Box<T extends B> { } public class RestrictTypeAndWildcardType { public static void main(String[] args) { // 대입될 수 있는 타입을 제한 Box<A> box1; // class Box<T extends B> 만족 x Box<B> box2; Box<C> box3; Box<D> box4; // 상한 경계 와일드카드 Box<B> box5 = new Box<B>(); test(box5); // 상한 경계 와일드카드 Box<? extends C> 만족 x Box<C> box6 = new Box<C>(); test(box6); Box<D> box7 = new Box<D>(); test(box7); } // upperBoundArrayList에는 Box<C> 또는 Box<C의자손>만 대입이 가능하다. public static void test(Box<? extends C> upperBoundArrayList) { } }
본론으로 돌아와서...
- 상한 경계 와일드카드의 produce작업은 넘어오는 타입을 특정할 수 없으므로 불가능하다.
import java.util.ArrayList; public class UpperBoundWildcardProduceImpossible { public static void main(String[] args) { // produceItem 메서드에 대입 불가 // produceItem(new ArrayList<Object>()); // produceItem 메서드에 대입 불가 // produceItem(new ArrayList<String>()); ArrayList<Number> numbers = new ArrayList<>(); produceItem(numbers); ArrayList<Integer> integers = new ArrayList<>(); produceItem(integers); } public static void produceItem(ArrayList<? extends Number> upperBoundArrayList) { // upperBoundArrayList.add 사용 -> 컴파일 에러 } }
- 상한 경계 와일드카드의 consume작업은 각 대입된 타입의 각 아이템들은 모두 Number로 형변환 가능하기 때문에 Number타입으로 읽을 수 있게된다.
import java.util.ArrayList; public class UpperBoundWildcardConsume { public static void main(String[] args) { ArrayList<Number> numbers = new ArrayList<>(); numbers.add(1.1d); numbers.add(7); numbers.add(3.3f); consumeItem(numbers); ArrayList<Integer> integers = new ArrayList<>(); integers.add(1); integers.add(10); integers.add(5); consumeItem(integers); } public static void consumeItem(ArrayList<? extends Number> upperBoundArrayList) { System.out.println("모두 int로 변환"); for (Number item : upperBoundArrayList) { System.out.println(item.intValue()); } System.out.println(); } }
- remove, clear작업도 가능하다.
7-3. 하한 경계 와일드카드 <? super 특정타입>
- 언바운드 와일드카드, 상한 경계 와일드카드와는 다르게 하한 경계 와일드카드는 produce 작업이 가능하다.
import java.util.ArrayList; public class LowerBoundWildcardProducePossible { public static void main(String[] args) { ArrayList<Object> objList = new ArrayList<>(); produceItem(objList); ArrayList<A> aList = new ArrayList<>(); produceItem(aList); ArrayList<B> bList = new ArrayList<>(); produceItem(bList); } public static void produceItem(ArrayList<? super B> lowerBoundArrayList) { lowerBoundArrayList.add(new B()); lowerBoundArrayList.add(new C()); lowerBoundArrayList.add(new D()); } }
A객체를 추가할 때
Object타입을 담을 수 있는 objList, A타입을 담을 수 있는 aList 는 만족하지만
B타입을 담을 수 있는 bList 는 만족하지 않는다.
Object객체 추가, B객체 추가 마찬가지로 만족하지 않는 경우가 존재한다.
그러나 B이하의 객체를 추가할 떄는 objList, aList, bList모두 문제가 되지 않는다.
하한 경계로 걸려있는 B에 대하여 -> B또는 B의 자손들의 add가 가능하다.
- Object타입으로 객체를 consume할 수 있다.
import java.util.ArrayList; public class LowerBoundWildcardConsume { public static void main(String[] args) { ArrayList<Object> objList = new ArrayList<>(); objList.add(new Object()); objList.add(new A()); consumeItem(objList); ArrayList<A> aList = new ArrayList<>(); aList.add(new A()); aList.add(new B()); consumeItem(aList); ArrayList<B> bList = new ArrayList<>(); bList.add(new B()); bList.add(new C()); consumeItem(bList); } public static void consumeItem(ArrayList<? super B> lowerBoundArrayList) { for (Object item : lowerBoundArrayList) { System.out.println(item); } System.out.println(); } }
7-4 변수 타입선언에 사용될 수 있는 와일드 카드, 와일드 카드가 사용된 변수가의 대입
와일드 카드는 보통 메서드의 파라메터 선언시 많이 사용되지만 변수의 타입 선언시에도 사용될 수 있다.
7-1, 7-2, 7-3을 잘 이해했다면 7-4의 이해는 어려움이 별로 없을 것이다.
import java.util.ArrayList; public class WildcardAssignment1 { public static void main(String[] args) { ArrayList<A> aList = new ArrayList<>(); aList.add(new A()); aList.add(new A()); // 메서드의 인자로 넘어가는 것과 동일한 것이다. ArrayList<?> unbound = aList; // 컴파일 에러 // unbound.add(new Object()); for (Object item : unbound) { System.out.println(item); } } }
import java.util.ArrayList; public class WildcardAssignment2 { public static void main(String[] args) { // 다음 과정이 생략된 것임 // ArrayList<? extends B> lowerB; // ArrayList<C> cList = new ArrayList<C>(); // lowerB = cList; ArrayList<? extends B> lowerB = new ArrayList<C>(); } }
import java.util.ArrayList; public class WildcardAssignment3 { public static void main(String[] args) { ArrayList<C> cList = new ArrayList<>(); cList.add(new C()); cList.add(new C()); ArrayList<? extends B> lowerB = cList; for (B b : lowerB) { System.out.println(b); } System.out.println(); ArrayList<D> dList = new ArrayList<>(); dList.add(new D()); dList.add(new D()); ArrayList<? extends C> lowerC = dList; for (C c : lowerC) { System.out.println(c); } System.out.println(); // 와일드 카드가 사용된 변수간의 대입 // lowerB에는 ArrayList<B>가 대입될 수 있지만 lowerC에는ArrayList<B>가들어갈수없기때문 // -> lowerC = lowerB시 컴파일 에러 lowerB = lowerC; for (B b : lowerB) { System.out.println(b); } System.out.println(); } }
8. Raw type
제네릭 클래스중 제네릭 타입 변수에 아무것도 대입되지 않은 것 = Raw Type
java5의 Generic이 도입되기 전의 내부 코드와의 호환을 위해 남겨둔 타입이다.
raw type을 사용하면 컴파일러 경고가 나타난다.
호환을 위해 남겨둔 것으로 raw type을 사용하면 상식적으로 문제가 있는 코드도 가능하게 된다.
아래의 코드들은 raw type의 동작에 대해 쓴 것으로 실제 raw type은 사용하지 않아야 한다.
우선 raw type 사용시 타입 변수는 Object로 간주된다.
이러한 형변환도 가능하여 에러가 나는 코드가 존재하므로 raw type은 사용하지 말아야 한다.
raw type은 타입 변수가 Object로 간주된다.
다음과 같이 rawType에 객체를 넣고 Object로 consume시 문제가 되지 않지만
import java.util.ArrayList; public class RawType1 { public static void main(String[] args) { ArrayList rawType = new ArrayList(); rawType.add(new Object()); rawType.add("test"); rawType.add(100); } }
raw type은 반대로도 대입이 가능하기 때문에 컴파일러가 타입 체크를 해주지 못한다.(컴파일 에러 발생하지 않음)
import java.util.ArrayList; public class RawType2 { public static void main(String[] args) { ArrayList rawType = new ArrayList(); rawType.add(new Object()); rawType.add("test"); // 컴파일 에러 발생하지 않음 ArrayList<String> stringType = rawType; // String item에서 String으로 형변환시 runtime 예외 발생 for (String item : stringType) { //16라인 System.out.println(item); } } }
raw type과 Object타입 모두 타입 변수가 Object로 처리되지만 raw type의 경우 제네릭 타입이 제거된 것이기 때문에 인자에 사용하면 모든 타입이 전달될 수 있다.
import java.util.ArrayList; public class RawType3 { public static void main(String[] args) { ArrayList rawTypeList = new ArrayList(); ArrayList<Object> objList = new ArrayList<>(); ArrayList<String> strList = new ArrayList<>(); rawTypeTest(rawTypeList); rawTypeTest(objList); rawTypeTest(strList); // 가능 objTypeTest(rawTypeList); objTypeTest(objList); objTypeTest(strList); // 컴파일 에러 } public static void rawTypeTest(ArrayList list) { } public static void objTypeTest(ArrayList<Object> list) { } }
import java.util.ArrayList; public class RawType3 { public static void main(String[] args) { ArrayList<String> strList = new ArrayList<>(); rawTypeTest(strList); String str = strList.get(0); // runtime 에러 } public static void rawTypeTest(ArrayList list) { list.add(new Object()); } }
https://mangkyu.tistory.com/241