제네릭 (Generic)

박주현·2022년 10월 26일
0

국비 공부

목록 보기
22/44

제네릭이란?

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의
타입 체크(compile-time type check)를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 똣이다.
예를 들어, ArrayList와 같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있긴 하지만 보통 한 종류의 객체를 담는 경우가 더 많다. 그런데도 꺼낼 때 마다 타입체크를 하고 형변환을 하는 것은 불편할 수 밖에 없다.

제네릭의 장점

1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.

간단히 얘기하면 다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다는 이야기라고 한다.

제네릭의 용어

class Box<T>{}

Box<T>	제네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽는다.
T		타입 변수 또는 타입 매개변수.(T는 타입 문자)
Box		원시 타입(raw type)

제네릭의 제한

제네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다.
제네릭은 이처럼 인스턴스 별로 다르게 동작하려고 만든 기능이니까.

Box<Apple> appleBox = new Box<Apple>();	// OK.	Apple 객체만 저장가능
Box<Grape> grapeBox = new Box<Grape>();	// OK.	Grape 객체만 저장가능

그러나 모든 객체에 대해 동일하게 동작해야하는 static멤버에 타입 변수 T를 사용할 수 없다. T는 인서튼스변수로 간주되기 때문이다. 이미 알고 있는 것처럼 static멤버는 인스턴스 변수를 참조할 수 없다.

제네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 'new T[10]'과 같이 배열을 생성하는 것은 안된다는 것이다.
제네릭 배열을 생성할 수 없는 것은 new연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. 그런데 코드에 정의된 Box<'T'> 컴파일하는 시점에서는 T가 어떤 타입이 될지 전혀 알 수 없다. instanceof연산자도 new 연산자와 같은 이유로 T를 피연산자로 사용할 수 없다. 꼭 제네릭 배열을 생성해야할 필요가 있을 때는, new 연산자 대신 'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 사용하거나, Object 배열을 생성해서 복사한 다음에 'T[]'로 형변환하는 방법 등을 사용한다.


  • 제너릭 적용 이전의 코드

  • 제너릭 적용 이전의 코드가 갖는 문제점

  • 제너릭 적용 이후의 코드


제네릭 타입의 선언

예제 코드

package sec03.inside;

import sec03.object.Cup;

public class GenericCalss2Demo {
	public static void main(String[] args) {
		Cup<Beer> cup = new Cup<>();		// Beer 타입으로 인스턴스 생성
		Cup<Boricha> cup2 = new Cup<>();// Boricha 타입으로 인스턴스 생성

		cup.setBeverage(new Beer());
		Beer b1 = (Beer) cup.getBeverage();

//		cup.setBeverage(new Boricha());		// Error! Beer 타입으로 생성했기 때문에
		cup2.setBeverage(new Boricha());	// Cup 인스턴스를 Boricha로 다시 생성해야함
		Boricha b2 = (Boricha) cup2.getBeverage();
	}

}

예제

package org.tutorials.java.days.tutorails;

import java.util.ArrayList;

class Fruit {
	@Override
	public String toString() {
		return "Fruit";
	}
}

class Apple extends Fruit {
	@Override
	public String toString() {
		return "Apple";
	}
}

class Grape extends Fruit {
	@Override
	public String toString() {
		return "Grape";
	}
}

class Toy {
	@Override
	public String toString() {
		return "Toy";
	}
}

class Box<T> {
	ArrayList<T> list = new ArrayList<T>();

	void add(T item) {
		list.add(item);
	}

	T get(int i) {
		return list.get(i);
	}

	int size() {
		return list.size();
	}

	@Override
	public String toString() {
		return list.toString();
	}
}

public class Java_bible {
	public static void main(String[] args) {
		Box<Fruit> fruitBox = new Box<Fruit>();
		Box<Apple> appleBox = new Box<Apple>();
		Box<Toy> toyBox = new Box<Toy>();
//		Box<Grape> grapeBox = new Box<Apple>();

		fruitBox.add(new Fruit());
		fruitBox.add(new Apple()); // Ok. void add(Fruit item)

		appleBox.add(new Apple());
		appleBox.add(new Apple());
//		appleBox.add(new Toy());	// Error. Box<Apple>에는 Apple만 담을 수 있음.

		toyBox.add(new Toy());
//		toyBox.add(new Fruit());	// Error. Box<Toy>에는 Fruit를 담을 수 없음.
//		toyBox.add(new Apple());	// Error. Box<Toy>에는 Apple를 담을 수 없음.

		System.out.println(fruitBox);	// [Fruit, Apple]
		System.out.println(appleBox);	// [Apple, Apple]
		System.out.println(toyBox);		// [Toy]

	} // main의 끝

}

제네릭의 제약

  • 기초 타입을 제네릭 인수로 사용 불가

  • 정적 제너릭 타입 금지

  • 제네릭 타입의 인스턴스화 금지. 즉, new T() 등 금지

  • 제네릭 타입의 배열 생성 금지

  • 실행 중에 제네릭 타입 점검 금지. 예를 들어, a instanceof ArrayLsit

  • 제네릭 클래스의 객체는 예외로 던지거나 잡을 수 없다

  • 제네릭의 서브 타입 허용 않음


제네릭의 상속과 타입 한정

다음과 같이 제네릭 타입에 'extends'를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다. 인터페이스도 extends를 사용한다. implements는 사용하지 않는다는 점을 주의해야 함.

// 타입을 Number의 상속 관계로 한정 짓는다.
public static <T extends Number> Box<T> makeBox(T o) {
		Box<T> box = new Box<T>();
		box.set(o);
  		// OK
 		FruitBox<Apple> appleBox = new FruitBox<Apple>();
		// Error, Toy는 Fruit의 자손이 아님.
  		FruitBox<Toy> toyBox = new FruitBox<Toy>();

		System.out.println("Boxed data : " + o.intValue());
		return box;
	}

WildCard

제네릭 타입을 하나로 고정 해놓으면, 위의 코드에서 알 수 있듯이 'FruitBox'타입의 객체는 각각의 다른 메서드의 매개변수가 될 수 없으므로, 다음과 같이 여러 가지 타입의 매개변수를 갖는 같은 메서드를 만들 수 밖에 없다. 그러나 제네릭 타입만 다른 것으로는 오버 로딩이 성립하지 않기 때문에 제네릭 타입은 컴파일러가 컴파일 할때만 사용하고 제거해버린다. 그래서 오버로딩이 아니라 '메서드 중복 정의'이다.
이럴때 사용하기 위해 고안된 것이 바로 '와일드 카드'이다. 와일드 카드는 기호 '?' 로 표현하는데 와일드 카드는 어떠한 타입도 될 수 있다.

'?'만으로는 Object타입과 다를게 없으므로 다음과 같이 'extends'와 'super'로 상한(upper bound)과 하한(lower bound)을 제한할 수 있다.

<? extends T>	와일드 카드의 상한 제한. T와 그 자손들만 가능.
<? super T>		와일드 카드의 하한 제한. T와 그 조상들만 가능
<?>				제한 없음. 모든 타입이 가능. <? extends Object>와 동일
  public class UnBoxer {
	public static <T> T openBox(Box<T> box) {
		return box.getOb();
	}

	public static <T> void peekBox(Box<T> box) {
		System.out.println(box);

	}

	public static void peekBox2(Box<?> box) {
		System.out.println(box);
	}

	public static void peekBox3(Box<? extends Number> box) {
		System.out.println(box);
	}

}

상한 제한

public static void peekBox3(Box<? extends Number> box) {
		System.out.println(box);
	}

이 구문에서 Number를 extends 해주면서 Number 이상의 클래스를 접근할 수 없게 해준다.

하한 제한

public static void peekBox3(Box<? super Number> box) {
		System.out.println(box);
	}

Number 이하의 클래스를 접근할 수 없게 해준다.

public static <T extends Comparable<? super T>> void sort(List<T> list)
							2								1

위의 코드에서
1. 타입 T를 요소로 하는 List를 매개변수로 허용한다.
2. 'T'는 Comparable을 구현한 클래스이어야 하며(T extends Comparable), 'T'또는 그 조상의 타입을 비교하는 Comparable이어야한다는 것 (Comparable<? super T>)을 의미한다. 만일 T가 Student이고, Person의 자손이라면, <? super T>는 Student, Person, Object가 모두 가능하다.


제네릭 메서드

public class GenMethod1Demo {
	static class Utils {
		// 반환 타입 앞의 <>는 타입 매개변수이다.
		public static <T> void showArray(T[] a) {
			for (T t : a) {
				System.out.printf("%s ", t);
			}
			System.out.println();
		}

		// 타입 매개변수의 뒤의 T는 반환 타입이다.
		public static <T> T getLast(T[] a) {
			return a[a.length - 1];
		}

	}

	public static void main(String[] args) {
		Integer[] ia = { 1, 2, 3, 4, 5, };
		Character[] ca = { 'H', 'E', 'L', 'L', 'O' };

		// 제네릭 메서드의 구체적 타입을 생략해도 된다.
		Utils.showArray(ia);

		// 호출할 때 구체적인 타입을 명시해도 된다.
		Utils.<Character>showArray(ca);

		System.out.println(Utils.getLast(ia));

	}

}

0개의 댓글