2024.09.03.화.TIL Generic

김기남·2024년 9월 3일
post-thumbnail

안녕하세요, 오늘은 GENERIC 에 대해 내용을 정리해보았습니다.

다형성

💡 제네릭 없이, 다형성만 활용하여 코드를 작성한다면?
  • 다형성을 사용하면 코드의 중복을 제거할 수 있다.
    • 기존 코드를 재사용할 수 있다.
  • 하지만, 다형성만 사용한다면 타입 안정성 문제가 발생한다.

타입 안정성 문제

  • 다형성을 사용하기 전 코드
public class IntegerStore {

	private Integer field;
	
	public void setField(Object object) {
		this.field = field;
	}
	
	public Integer getField() {
		return field;
	}

}

public class LongStore {

	private Long field;
	
	public void setField(Object object) {
		this.field = field;
	}
	
	public Long getField() {
		return field;
	}

}

public class StringStore {

	private String field;
	
	public void setField(Object object) {
		this.field = field;
	}
	
	public String getField() {
		return field;
	}

}

public class Main {

	public static void main(String[] args) {
	
		// Integer
		IntegerStore integerStore = new IntegerStore();
		integerStore.setField(1000);
		
		// Long
		LongStore longStore = new LongStore();
		// String
		StringStore stringStore = new StringStore();

	}

}
  • 코드 중복 발생

  • Obejct 를 활용한 다형성

public class ObjectStore {

	private Object field;
	
	public void setField(Object object) {
		this.field = field;
	}
	
	// return Type이 Obejct 이다.
	public Object getField() {
		return field;
	}

}


public class Main {

	public static void main(String[] args) {
	
		// Integer
		ObjectStore integerStore = new ObjectStore();
		integerStore.setField(10);
		
		// Casting : Object -> Integer
		Integer integer = (Integer) integerStore.getField();
		System.out.println(integer);
		
		
		// String
		ObjectStore stringStore = new ObjectStore();
		stringStore.setField("sparta");
		
		// Casting : Object -> String
		String string = (String) stringStore.getField();
		System.out.println(string);
		
		
		// Long, Float, 다른 Object 등등..
		
		// Integer가 아닌 String을 넣는다면?
		integerStore.setField("Spring");
		// Integer integerString = (Integer) "Spring"; ?????
		Integer integerString = (Integer) integerStore.getField();
		System.out.println(integerString);

	}

}

// ClassCastException 발생!
  • 하위 클래스는 상위 클래스를 참조할 수 없다, 담을 수 없다.
    • Child child = new Parent(); 불가능.
  • String, Integer 등 어떤 값이 와도 입력에는 아무 문제가 없다.
  • 하지만, 해당 값을 꺼내어 사용할 때 문제가 발생한다.
  • 지난 심화반 다형성 수업 때 나왔던 다운캐스팅 문제와 같다.
    • 타입 안정성 문제

위와 같은 타입 안정성 문제는 어떻게 해결해야 하는가?

제네릭

💡 제네릭을 활용하여 타입 안정성 문제를 해결해보자.
  • 제네릭은 특정 타입에 제한되지 않고, 범용적으로 사용할 수 있다.
  • <…> 을 사용한것이 제네릭이다.
  • 제네릭 클래스는 Type을 미리 결정하지 않는다.
    • 실제 생성하는 시점에 Type을 결정한다.

제네릭 사용

  • 제네릭 클래스
    • T는 Type이다.
public class GenericStore<T> {
	
	private T field;
	
	public void setField(T field) {
		this.field = field;
	}
	
	public T getField() {
		return field;
	}
	
}


public class Main {

	public static void main(String[] args) {
	
		// T의 Type이 결정되는 순간
		GenericStore<Integer> integerStore = new GenericStore<Integer>();
		
		// Compile 오류 발생
		integerStore.setField("Sparta");
		
		integerStore.setField(1000);
		
		// integerStore를 new 하는 순간 Integer로 생성되어있다.
		// 즉, 캐스팅이 필요없다.
		Integer integer = integerStore.getField();
		System.out.println(integer);
		
		// 생성하는 뒷부분의 <> 안의 타입은 생략이 가능하다.
		GenericStore<String> stringStore = new GenericStore<>();
		stringStore.setField("spring");
		String string = stringStore.getField();
		System.out.println(string);
		
		// Long, Double, Obejct 등등..

	}

}
  • String → GenericStore
  • Integer → GenericStore
  • Compiler가 T에 대한 타입 정보를 작성한 코드에 맞추어 적용한다.
    • 타입이 맞지 않으면 컴파일 에러가 발생한다.
  • 생성하는 부분의 <> 타입은 생략이 가능하다.
    • Type을 선언하는 부분의 타입은 생략이 불가능하다.

제네릭 규칙

💡 제네릭은 어떻게 사용하는가?

여러 타입 선언

  • 여러개의 타입을 선언할 수 있다.
public class KeyValueType<K, V> {
	...
}

class MultipleType<T, U> {
	...
}

제네릭 키워드

  1. 키워드 대신 다른것들이 들어갈 수 있다.
    1. ex) a, B, c, D 하지만, 규칙이 있다.
  2. 대문자를 사용한다.
  3. 키워드 첫글자를 사용한다.
  • E
    • Element
  • K
    • Key
  • N
    • Number
  • T
    • Type
  • V
    • Value
  • S, U, V
    • Type이 여러개인 경우 각각 Second, Third, Fourth

Row Type

  • 타입을 지정하지 않아도 된다.
GenericStore stringStore = new GenericStore();
  • 타입을 지정하지 않으면 T가 Object로 지정된다.
  • 제네릭이 없던 시절의 코드와 호환을 위해 Row Type을 지원한다.
    • 즉, 현재는 사용하지 않아야한다.

제네릭 타입 범위 제한

💡 제네릭에 타입을 지정하여 타입 안정성을 확보하면 좋지만, 너무 자유롭다. ex) 단순히 로 지정하게되면 모든 클래스를 대입할 수 있다. 그래서 사용하는 것이 **Bounded Type Parameter** 이다.

문제점

  • 다형성 코드예시
public class Fruit {
	
	private String name;
	private int size;
	
	public Fruit(String name, int size) {
		this.name = name;
		this.size = size;
	}
	
	public String getName() {
		return name;
	}
	
	public int getSize() {
		return size;
	}
	
	public void sayColor() {
		System.out.println("나는 무색이야");
	}

}


public class Apple extends Fruit {

	public Apple(String name, int size) {
		super(name, size);
	}
	
	@Override
	public void sayColor() {
		System.out.println("나는 빨간색이야");
	}

}


public class Grape extends Fruit {

	public Grape(String name, int size) {
		super(name, size);
	}
	
	@Override
	public void sayColor() {
		System.out.println("나는 보라색이야");
	}

}


public class FruitShop {
	
	private Fruit fruit;
	
	public void setFruit(Fruit fruit) {
		this.fruit = fruit;
	}
	
	public Fruit getFruit() {
		return fruit;
	}
	
	public void advertise() {
		System.out.println("맛있는 " + fruit.getName() + " 팝니다.");
		System.out.println("크기는 " + fruit.getSize() + " 입니다.");
		fruit.sayColor();
	}
	
	// 사이즈가 큰 것은 반환한다.
	public Fruit compareSize(Fruit compareFruit) {
	
		if (fruit.getSize() > compareFruit.getSize()) {
			return fruit;
		}
	
		return compareFruit;
	}

}


public class Main {

	public static void main(String[] args) {
		
		FruitShop appleShop = new FruitShop();
		FruitShop grapeShop = new FruitShop();
		
		Apple apple = new Apple("사과", 1);
		Grape grape = new Grape("포도", 2);
		
		appleShop.setFruit(apple);
		appleShop.advertise();
		
	  grapeShop.setFruit(grape);
	  grapeShop.advertise();
	  
	  // 사과 가게인데 포도를 가질 수 있다.
	  // appleShop.setFruit(grape);
	  
	  // fruit 타입으로 반환된다.
	  Fruit fruit = appleShop.compareSize(new Apple("풋사과", 2));
	  
	  // 다운 캐스팅이 필요하다.
	  Apple apple2 = (Apple) appleShop.compareSize(new Apple("풋사과", 2));
	  
	  // 사과를 비교해야 하는데 포도를 넣으면?
	  // Apple apple3 = (Apple) appleShop.compareSize(new Grape("청포도", 2));
	  
	}

}
  • T 사용
public class FruitShop<T> {
	
	private T fruit;
	
	public void setFruit(T fruit) {
		this.fruit = fruit;
	}
	
	public T getFruit() {
		return fruit;
	}
	
	public void advertise() {
		// T를 사용하면 어떤 타입이 들어올 지 모르기 때문에 Object와 같다.
		// 하위 클래스들의 메서드나, 필드를 미리 선언하여 사용할 수 없다.
		// System.out.println("맛있는 " + fruit.getName() + " 팝니다.");
		// System.out.println("크기는 " + fruit.getSize() + " 입니다.");
		// fruit.sayColor();
	}
	

	public T compareSize(Fruit compareFruit) {
		// T = Object
		// if (fruit.getSize() > compareFruit.getSize()) {
		// 	return fruit;
		// }
	
		// return compareFruit;
	}

}
  • T에 들어올 수 있는 값들이 너무 자유롭다.

  • 한마디로, Object가 제공하는 메서드만 호출할 수 있다.

    제한 방법

public class FruitShop<T extends Fruit> {
	
	private T fruit;
	
	public void setFruit(T fruit) {
		this.fruit = fruit;
	}
	
	public T getFruit() {
		return fruit;
	}
	
	public void advertise() {
		System.out.println("맛있는 " + fruit.getName() + " 팝니다.");
		System.out.println("크기는 " + fruit.getSize() + " 입니다.");
		fruit.sayColor();
	}
	
	// 사이즈가 큰 것은 반환한다.
	public T compareSize(T compareFruit) {
	
		if (fruit.getSize() > compareFruit.getSize()) {
			return fruit;
		}
	
		return compareFruit;
	}

}

public class Main {

	public static void main(String[] args) {
		
		FruitShop<Apple> appleShop = new FruitShop();
		FruitShop<Grape> grapeShop = new FruitShop();
		
		Apple apple = new Apple("사과", 1);
		Grape grape = new Grape("포도", 2);
		
		appleShop.setFruit(apple);
		appleShop.advertise();
		
	  grapeShop.setFruit(grape);
	  grapeShop.advertise();
	  
	  // 다른 타입을 넣게되면 컴파일 오류 발생
	  appleShop.setFruit(grape);
	  
	  // fruit 타입으로 반환된다.
	  Fruit fruit = appleShop.compareSize(new Apple("풋사과", 2));
	  
	  // 다운 캐스팅이 필요없다.
	  Apple returnApple = appleShop.compareSize(new Apple("풋사과", 2));
	  
	}

}
  • T의 최상위 클래스는 Fruit이다 = 제한한다. = T extends Fruit
  • 다형성으로 인해 Fruit과 그 하위 class만 받을 수 있다.
  • Fruit의 메서드들을 사용할 수 있게된다.
  • 타입 안정성으로 appleShop에 grape를 넣을 수 없게 되었다.
  • new 생성하는 시점에 타입이 지정되어 다운 캐스팅이 필요없다.
  • 모든 문제가 해결 완료되었다.

제네릭 메서드

💡 메서드에 제네릭을 적용한다.
  • <T> 를 메서드 반환타입 앞에 위치시킨다.
    • 메서드 명명법
      • 접근제어자 + 반환타입 + 메서드 이름 + (매개변수) { 구현코드 }
      • public Object get(Long id) { … }
  • 타입을 명시적으로 지정 후 호출한다.

제네릭 메서드

  • 코드 예시
public class GenericMethod {

	public static <T> T genericMethod(T t) {
	
		System.out.println("T 출력 : " + t);
		
		return t;
	}
	
	public static <T extends Number> T numberGenericMethod(T t) {
	
		System.out.println("number T 출력 : " + t);
		
		return t;
	}
	
}


public class Main {

	public static void main(String[] args) {
	
		Long value = 1L;
		
		// 타입을 명시한다.
		Long longValue = GenericMethod.<Long>genericMethod(value);
		Integer integerValue = GenericMethod.<Integer>numberGenericMethod(10);
	
	}
	
}
  • 제네릭 타입
    • 객체를 new 생성하는 시점에 타입이 결정된다.
  • 제네릭 메서드
    • 메서드를 호출하는 시점에 타입이 결정된다.
  • static 메서드
    • static 메서드에 제네릭을 도입하려면 반드시 제네릭 메서드를 사용해야 한다.
    • static 메서드는 객체를 생성하는 시점과 상관없이 사용할 수 있기 때문
      • new 하지 않는다.
  • 제네릭 메서드 또한 타입을 제한할 수 있다.
    • <T extends Number>

타입 추론

  • 제네릭 메서드를 호출할 때 마다, 타입을 명시해주기 힘들다.
public class Main {

	public static void main(String[] args) {
	
		Long value = 1L;
		
		// 타입을 생략한다.
		Long longValue = GenericMethod.genericMethod(value);
		Integer integerValue = GenericMethod.numberMethod(10);
	
	}
	
}
  • 매개변수, 반환 타입으로 Java Compiler 가 추론한다.
  • 눈에 보이지는 않지만, 컴파일러가 타입을 전달해준다.

우선순위

  • static 메서드는 제네릭 메서드만 적용할 수 있다.
  • new 로 생성하여 사용하는 일반 메서드는 제네릭 타입과 제네릭 메서드 모두 적용할 수 있다.
  • 동시에 사용한다면?
// 제네릭 타입 설정
public class FruitBox<T extends Fruit> {

	private T fruit;
	
	public void setFruit(T fruit) {
		this.fruit = fruit;
	}
	
	// 제네릭 메서드 설정
	public <T> T getClassName(T t) {
		System.out.println("fruit class" + fruit.getClass().getName());
		System.out.println("t class" + t.getClass().getName());
		return t;
	}

}


public class Main {

	public static void main(String[] args) {
	
		Apple apple = new Apple("사과", 1);
		Grape grape = new Grape("포도", 2);
		
		FruitBox<Apple> appleBox = new FruitBox<>();
		appleBox.setFruit(apple);
		
		Grape grape2 = appleBox.getClassName(grape);
		
	}
		
}
  • 제네릭 메서드가 우선순위를 가진다.

    • 항상 더욱 구체적으로 선언된것이 우선순위를 가진다.
    • 클래스 > 메서드
  • 제네릭 클래스의 T는 클래스의 T

    • <T extends Fruit> 으로 제한을 두었다.
  • 제네릭 메서드의 T는 메서드의 T

    • <T extends Fruit> 으로 제한을 두지 않았다. == Obejct로 취급된다.
    • .getClass()는 Obejct의 메서드이기 때문에 사용할 수 있다.
  • 이런 경우 꼭 제네릭 메서드에는 T 대신 다른 이름을 사용해야 한다.

public <S> S getClassName(S s) {

	System.out.println("fruit class" + fruit.getClass().getName());
	System.out.println("s class" + s.getClass().getName());
	return s;
	
}

와일드카드

💡 제네릭에서 사용하는 와일드카드는 `?` 키워드를 사용한다.
  • 와일드 카드는 제네릭 타입을 활용할 때 사용한다.

    코드예시

  • Unbounded Wildcards

    • ? 사용
public class Shop<T> {

	private T product;
	
	public void setProduct(T product) {
		this.product = product;
	}
	
	public T getProduct() {
		return product;
	}
	
}


public class WildCard {

	// 제네릭 메서드 사용
	static <T> void genericMethod(Shop<T> shop) {
		System.out.println("T Shop Product = " + shop.getProduct());
	}
	
	// ? 와일드 카드 사용
	static void wildCardGenericMethod(Shop<?> shop) {
		System.out.println("? : " + shop.getProduct());
	}

}
  • 제네릭 메서드 사용

    • 타입 추론에 의해, T는 전달받은 값이 된다.
  • ? 와일드 카드 사용

    • Shop과 관련된 모든 타입을 T처럼 받을 수 있다.
      • <? extends Object> 와 같다.
      • 제한이 없는 와일드카드 이다.
    • 이미 정해진 제네릭 타입을 활용할 때 쓴다.
  • 와일드 카드를 사용하면 제네릭 메서드가 아니다.

    • 일반 메서드이다.
    • 모든 타입을 받을 수 있다.
  • 타입 추론 과정은 복잡하다.

    • 와일드 카드를 사용하면 단순하게 동작한다.
    • 보통 제네릭 메서드가 아니라 와일드카드를 사용한다.
  • Upper Bounded Wildcards

    • <? extends 상위클래스> 사용
    • 상위 클래스 이하의 모든 클래스 허용
public class WildCard {
	
	// extends + 제네릭 메서드 사용
	static <T extends Fruit> void genericMethod(Shop<T> shop) {
		System.out.println("T Shop Product = " + shop.getProduct());
	}

	// extends + 와일드 카드 사용
	static void extendsGenericMethod(Shop<? extends Fruit> shop) {
		Fruit fruit = shop.getProduct();
		System.out.println("? extends Fruit : " + fruit.getName());
	}
	
}
  • 와일드 카드 또한 제한을 설정할 수 있다.

    • 다른 타입이 오면 컴파일 오류가 발생한다.
  • Fruit class의 기능을 사용할 수 있다.

  • 언제 제네릭 메서드를 사용하는가?

public class WildCard {
	
	// T 를 반환
	static <T extends Fruit> T genericMethod(Shop<T> shop) {
		T t = shop.getProduct();
		System.out.println("T Shop Product = " + shop.getProduct());
		return t;
	}

	// Fruit 타입을 반환, 전달할 타입을 명확하게 할 수 없다.
	static Fruit extendsGenericMethod(Shop<? extends Fruit> shop) {
		Fruit fruit = shop.getProduct();
		System.out.println("? extends Fruit : " + fruit.getName());
		return fruit;
	}
	
}



public class Main {

	public static void main(String[] arts) {

		Apple apple = new Apple("사과", 1);
		Grape grape = new Grape("포도", 2);
		
		Shop<Apple> appleShop = new Shop();
		Shop<Grape> grapeShop = new Shop();
	
		appleShop.setProduct(apple);
		grapeShop.setProduct(apple);
		
		// T를 반환
		Apple returnApple = Wildcard.genericMethod(appleShop);
		Grape returnGrapeb = Wildcard.genericMethod(grapeShop);
		
		// ? extends Fruit의 경우 Fruit을 반환한다.
		Fruit apple2 = Wildcard.extendsGenericMethod(appleShop);
		Fruit grape2 = Wildcard.extendsGenericMethod(grapeShop);
		
		// 즉, 형변환이 필요하다.
		Apple apple2 = (Apple) Wildcard.extendsGenericMethod(appleShop);
		Grape grape2 = (Grape) Wildcard.extendsGenericMethod(grapeShop);
		
	}
}
  • 메서드의 타입을 실행 시점에 변경하려면 제네릭 타입, 제네릭 메서드를 사용한다.
  • 꼭 필요한 상황에만 <T>를 사용하고 나머지는 와일드 카드를 사용한다.
    • 형변환이 필요할 때.
  1. 형변환이 필요한 경우 = T를 제네릭 메서드를 사용한다.
  2. 형변환이 필요 없는 경우 = ? 와일드 카드를 사용한다.
  • Lower Bounded Wildcards
    • <? super 하위클래스> 사용
    • 하위 클래스 이상의 모든 클래스 허용
public class Main {

	public static void main(String[] arts) {
	
		Shop<Object> objectShop = new Shop();
		Shop<Fruit> fruitShop = new Shop();
		Shop<Apple> appleShop = new Shop();
		Shop<Grape> grapeShop = new Shop();
		
		// Fruit 이상의 상위 클래스 허용
		init(objectShop);
		init(fruitShop);
		
		// 불가능
		init(appleShop);
		init(grapeShop);
		
	}
	
	static void init(Shop<? super Fruit> shop) {
		shop.setProduct(new Apple("사과", 10));
	}
}

Eraser

💡 Generic은 Compile 시점에 사용된다.

제네릭 동작방식

  • 컴파일 이후 코드 변화
// 컴파일 이전
// 상위 클래스 제한
public class FruitBox<T extends Fruit> {

	private T fruit;
	
	public void setFruit(T fruit) {
		this.fruit = fruit;
	}
	
	public T getFruit() {
		return fruit;
	}

}


public class Main {

		Apple apple = new Apple("사과", 1);
		Grape grape = new Grape("포도", 2);
		
		FruitBox<Apple> appleBox new FruitBox<>();
		appleBox.set(apple);
		
		Apple apple = appleBox.getFruit();
		
}

------------------------------------------------------

// 컴파일 이후
public class FruitBox<Apple extends Fruit> {

	private Apple fruit;
	
	public void setFruit(Apple fruit) {
		this.fruit = fruit;
	}
	
	public Apple getFruit() {
		return fruit;
	}

}

public class Main {

		Apple apple = new Apple("사과", 1);
		Grape grape = new Grape("포도", 2);
		
		FruitBox appleBox new FruitBox<>();
		appleBox.set(apple);
		
		Apple fruit = (Apple) appleBox.getFruit();
		
}
  • 개발자가 처리해야할 캐스팅 부분을 Compiler가 대신 처리한다.
  • .java → compiler → bytecode
  • Compile이 완료된 후 Runtime 때 Generic과 관련된 정보를 삭제한다.
    • 이것이 Eraser 이다.

제네릭의 한계

  • Runtime에는 타입을 활용할 수 없다.
// 컴파일 이전
public class Eraser<T> {

	// 불가능
	public void createEraser() {
		return new T();
	}

	// 불가능
	public boolean instanceOf(Object obj) {
		return obj instanceOf T;
	}
	
}


-----------------------------------------------

// 컴파일 이후, Runtime
public class Eraser {

	// 불가능
	public void createEraser() {
		return new Object();
	}

	// 불가능
	public boolean instanceOf(Object obj) {
		return obj instanceOf Object;
	}
	
}
  • createEraser()
    • 항상 new Object 로 타입이 생성된다.
    • 의도와 다르게 사용된다.
    • 따라서, 제네릭 T는 new 를 사용할 수 없다.
  • instanceOf()
    • 항상 true가 반환된다.
  • Eraser를 사용하기 때문이다.

정리

  • 제네릭을 사용하여 설계하는 일은 어렵다.
  • 반면에, 이미 제네릭을 활용하여 만들어진것을 사용하는 경우가 많다.
  • 나는 무적권 해야겠다! 하는분들만 위 참고자료 보시길 추천드립니다.
  • 실무가 아닌 학습으로 익숙해지려면?
    • 객체지향 다형성 → 제네릭 → 컬렉션 프레임워크 → Spring에서 사용되는 제네릭 순서 학습이 필요하다.

오늘의 코드카타

import java.util.ArrayList;
import java.util.List;

class Solution {

    public int[] solution(int[] arr) {
        List<Integer> stk = new ArrayList<>();
        int i = 0;

        while (i < arr.length) {
            if (stk.isEmpty()) {
                stk.add(arr[i]);
                i++;
            } else if (stk.get(stk.size() - 1) == arr[i]) {
                stk.remove(stk.size() - 1);
                i++;
            } else {
                stk.add(arr[i]);
                i++;
            }
        }

        if (stk.isEmpty()) {
            return new int[]{-1};
        }

        int[] answer = new int[stk.size()];
        for (int j = 0; j < stk.size(); j++) {
            answer[j] = stk.get(j);
        }
        return answer;
    }
}
profile
새로운 시작~!

0개의 댓글