[Java] Generics (1)

PADO·2021년 1월 14일
1

Java

목록 보기
6/6
post-thumbnail

모던 자바(자바 5 이후 기술)에서 가장 중요한 개념은 Generics, Lambda, Annotation

타입 파라미터를 사용하면 Generic

class Hello<T> {

}

타입 파라미터를 추가했다.

메소드의 파라미터: 먼저 선언이 되어있고(괄호 안에) input이 됨.
타입에 해당하는 값을 던지면, 그 값이 선언된 파라미터에 바인딩이 되어서 사용할 수 있게 되는 것

에 실제 타입 정보를 주면 T에 해당하는 자리에 딱 맵핑이 되어서

public class Generics {
	static class Hello<T> { // 타입 파라미터 추가
		T t; // 필드로 사용되기도 하고
		void method(T val) {} // 메서드의 value type으로 사용되기도 함.
	}
	public static void main(String[] args) {
		new Hello<String>(); // 타입 인자로 String을 넘겨줌
	}

넘기는 정보를 타입 인자 (type argument)라고 하고, T를 타입 파라미터 (type parameter)라고 함

  • 타입 파라미터(type parameter): 선언된 것
  • 타입 인자(type argument): 넣어준 것

제네릭을 사용하는 이유

  1. 컴파일 시점 컴파일러가 정확하게 타입 체킹, 캐스팅을 해줄 수 있다.
    → 런타임 시점에 밖에 checking이 안 되는 bug들이 숨어들어갈 수 있다.

    ```java
    List list = new ArrayList();
    list.add("Str");
    Integer i = (Integer) list.get(0);
    ```
    
    ```java
    List list = Array.asList(1,2,3);
    list.add("abc");
    ```
  2. Generic한 코드를 만들어 타입만 바꿔 재사용하기 유용하다.
    99% 같은 코드를 Integer용, String용으로 여러 벌 만들기는 불필요.

Raw Type

타입과 관련해 심각한 에러를 낼 수 있다.

List<Integer> ints = new ArrayList<Integer>();
List rawInts = ints;

제네릭이면서 타입 파라미터 값 없이 선언한 것. 이렇게 써도 동작은 한다.
반대는 컴파일러가 잡지 못 하지만 런타임 에러가 발생함.

Generic method

public static class Hello<T> {}처럼 Generic이 class에 붙는 경우도 있지만, 메소드 혹은 인터페이스에도 붙을 수 있다.

인스턴스 메소드에 제네릭을 사용한 예

public class Generics {
	<T> void print(T t) {
		System.out.println(t.toString());
	}
}

인자로 제네릭하게 T를 받을 거면, return type 앞에 타입 파라미터 를 붙임.

public static void main(String[] args) {
	new Generics().print("Hello");
}

static 메소드에 제네릭을 사용한 예

(static 메소드: 인스턴스를 만들지 않아도 쓸 수 있는 메소드)

public class Generics {
	<T> static void print(T t) {
		System.out.println(t.toString());
	}
}
public static void main(String[] args) {
	print("Hello");
}

Class level과 method level에서 Generic 사용의 혼동

아래처럼 Class 레벨에 타입 파라미터를 정의하고, 메소드에서 갖다 쓰는 건 문제가 없음.

public class Generics<T> {
	static void print(T t) {
		System.out.println(t.toString());
	}
}

이 경우 print 메소드를 static으로 사용할 수 없음.

public class Generics<T> {
	static void print(T t) {
		System.out.println(t.toString());
	}
}

Type variable은 class의 instance가 만들어질 때 인자를 받아오게 되어 있음.

static은 Generics라는 클래스의 오브젝트를 만들지 않고 사용하는 것이므로 T type이 뭐가 될지 모름.

따라서 아래처럼 method 레벨에서 type 파라미터를 정의해줘야함

public class Generics<T> {
	static <T> void print(T t) {
		System.out.println(t.toString());
	}
}

이럴 경우 class레벨의 와 메소드 레벨의 가 혼동이 올 수 있으니

public class Generics<T> {
	static <S> void print(S s) {
		System.out.println(s.toString());
	}
}

알파벳을 바꿨음.

public class Generics<T> {
	static <S,T> T print(S s) {
		System.out.println(s.toString());
	}
}

메소드 레벨에서 정의한 타입 파라미터 S를 인자로 받고, 클래스 레벨에서 정의한 타입 파라미터 T를 리턴 (혼용 가능)

생성자에서의 Generic 사용

public class Generics<T> {
	public <S> Generics(S s) {
	}
}

Bounded Type Parameter

쓸 수 있는 타입에 제약을 걸어주는 것

타입 파라미터 자리엔 어떠한 타입도 올 수 있다. 여기에 이떠한 파라미터만 오도록 제한을 두겠음

public class Generics<T extends List> {
}

T라는 타입 파라미터는 List 타입의 subtype만 가능하다. (upper bound)

multiple bounds (intersection type)

타입의 제약조건을 여러 타입으로 정해주는 것. 모두 만족하는 타입만 들어올 수 있음

인터페이스를 여러 개 구현할 수 있는 것과 마찬가지로 인터페이스만 가능.

클래스라면 상속은 하나만 할 수 있으니까 클래스는 하나만 가능.

public class Generics {
	static <T extends List> void print(T t) {}

	public static void main(String[] args) {
	}
}

타입에 Boundary를 줄 때 &로 분리해 여러 개를 줄 수 있음. <T extends List & Serializable>

example

public class Generics {
	static long countGreaterThanTarget(Integer[] nums, int target) {
		return Arrays.stream(arr).filter(num -> num > target).count();
	}

	public static void main(String[] args) {
		Integer[] nums = new Integer[] { 1, 2, 3, 4, 5, 6, 7 };
		System.out.println(countGreaterThanTarget(nums, 5));
	}
}

String도 정렬할 수 있음. (사전 순에 따라) 만약에 이 함수를 스트링에 대해 쓰고 싶으면?

→ Generic 메소드로 정의하면 되겠구나

public class Generics {
	static <T> long countGreaterThanTarget(T[] arr, T target) {
		return Arrays.stream(arr).filter(element -> ).count();
	}

	public static void main(String[] args) {
		Integer[] nums = new Integer[] { 1, 2, 3, 4, 5, 6, 7 };
		String[] strs = new String[] { "a", "b", "c", "d" };
		System.out.println(countGreaterThanTarget(nums, 5));
		System.out.println(countGreaterThanTarget(strs, "b"));
	}
}

메소드 레벨에서 타입을 정의하고, T type 배열, T type 원소 하나를 받게 함

부등호를 이용한 크기 비교가 문제임. 일반적으로 Object 사이에 크기 비교를 하려면 Comparable 인터페이스를 구현하여 compareTo 메소드를 사용하면 됨.

element.compareTo 를 하면 T type에 compareTo가 있는지 모름.

→ countGreaterThanTarget 메소드가 받는 인자 타입에 제한을 걸 수 있음

comparable이라는 인터페이스를 구현한 클래스에 대해서만, Comparable의 타입도 T임

	static <T extends Comparable<T>> long countGreaterThanTarget(T[] arr, T target) {
		return Arrays.stream(arr).filter(element -> element.comapreTo(target) > 0).count();
	}

이게 가능한 이유는 Integer와 String 모두 Comparable를 구현했기 때문

Generic과 상속

Java의 숫자값을 표현하는데 쓰는 클래스들은 다 Number class의 서브 클래스

public class Generics {
	public static void main(String[] args) {
		Integer i = 10;
		Number n = i; // 업캐스팅 가능 

		List<Integer> intList = new ArrayList<>();
		List<Number> numberList = intList; // 컴파일 에러 
}

왜 컴파일 에러가 발생할까? Integer는 Number의 서브타입이 맞지만, List는 List의 서브타입이 아님.

타입 파라미터(Integer와 Number)의 상속 관계는 적용이된 제네릭 타입(List)의 상속 관계에 영향을 미치지 않는다.

ArrayList<Integer> arrList = new ArrayList<>();
List<Integer> list = arrList; // 가능

이건 가능. ArrayList가 List의 서브타입이라 업캐스팅이 가능.

example

public class Generics {
	static class MyList<E, V> implements List<E> {
	}
	public static void main(String[] args) {
	}
}

타입 파라미터가 두 개인 MyList가 타입 파라미터가 하나인 List를 implements

타입 파라미터가 더 많은 경우 대입이 가능한가?

public static void main(String[] args) {
	List<String> s1 = new MyList<String, Integer>();
	List<String> s2 = new MyList<String, String>();
}

List type의 subtype으로 선언을 했기 때문에 대입이 됨! 타입 파라미터가 어떻게 붙든 상관이 없음

Type inference (타입 추론)

메서드 호출할 때 호출하는 정보를 보고 type argument가 뭐가 들어가야할지 컴파일러가 추론을 하는 기능

public class Generics {
	static <T> void method(T t, List<T> list) {
	
	}
	public static void main(String[] args) {
		method(1, Arrays.asList(1,2,3);
	}
}

이 경우엔 컴파일러가 알아서 타입추론을 하여 잘 넣어주지만, 만약 모를 경우,

public static void main(String[] args) {
		method<Integer>(1, Arrays.asList(1,2,3);
}

<Integer> 하고 알려주면 된다.

List<String> list = new ArrayList<>();

<> 안에 들어갈 것이 String이라는 것을 컴파일러가 알 수 있도록 해주는 타입 추론 중 하나.

List<String> c = Collections.emptyList();

T라는 타입 정보는 보통 메서드의 파라미터로 넘기는 인자 값의 파라미터 타입이 뭔가로 체크.

이런 경우는 매개변수가 없지만, 컴파일러가 String 타입의 빈 list구나를 판단.

List<String> c = Collections.<String>emptyList();

컴파일러가 알지 못 한다면 명시적으로 타입을 지정해줄 수 있음

? = wildcard (뭐라도 들어갈 수 있다 = 모른다 = 알 필요 없다)

List<T> list;
지금 선언하는 시점엔 T가 무슨 type인지 모르지만, 이 type이 정해지면 뭔지 알고 이후에 사용 하겠다.

List<?> list; = List<? extends Object> list;
Object의 subtype인데, Object class에 정의된 기능 가져다 사용하겠다고 정의 (equals, toString 등)
이 안에 type이 뭐가 오든 상관 없음. List interface가 가지고 있는 메서드를 사용하겠다고 정의 (size 등)

static void method(List<?> t) {
}
static <T> void method(List<T> t) {
}
static void method(List<? extends Comparable> t) {
}
static <T extends Comparable> void method(List<T> t) {
}

example: 와 <?> 의 차이

? == ? extends Object

static void printList(List<Object> list) {
	list.forEach(s -> System.out.println(s));
}

명확하게 List의 원소가 Object여야 한다고 정의한 위와 같은 메소드를

List<Integer> list = Arrays.asList(1,2,3);
printList(list); // 컴파일 에러

이렇게 사용한다면 컴파일 에러.

List 를 List에 대입하는 것.
아까 example에서 대입했던 것과 같이 Integer는 Object의 하위 클래스지만,
List는 List의 하위 타입이 아님.

static void printList(List<?> list) {
	list.forEach(s -> System.out.println(s));
}

List의 원소는 모르지만 어떤 거든 상관 없다고 정의한 위와 같은 메소드를

List<Integer> list = Arrays.asList(1,2,3);
***printList(list); // 컴파일 가능***

이렇게 사용한다면 컴파일 에러가 나지 않음.

? extends Object라고 해도 오류나지 않음.

0개의 댓글