본문 바로가기
공부/JAVA

[JAVA] 제네릭(Generic)

by yeaseul912 2022. 6. 21.
728x90

문득 강의를 듣다가 "제네릭 타입으로 지정해 줍니다" 라는 말을 정확하게 이해 하고 싶어서 포스팅을 하게 되었다.

너무너무 잘 정리되어 있는 블로그가 있어서 내용을 좀 많이 발췌해왔지만!! 나의 언어로 다시 써보도록 한다.

참고 블로그 : https://st-lab.tistory.com/153

 

자바 [JAVA] - 제네릭(Generic)의 이해

정적언어(C, C++, C#, Java)을 다뤄보신 분이라면 제네릭(Generic)에 대해 잘 알지는 못하더라도 한 번쯤은 들어봤을 것이다. 특히 자료구조 같이 구조체를 직접 만들어 사용할 때 많이 쓰이기도 하고

st-lab.tistory.com


1. 제네릭이란?

직역 하면 "일반적인" 이라는 뜻이다.

"데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법"으로

특정(Specific) 타입을 미리 지정해주는 것이 아닌 필요에 의해 지정할 수 있도록 하는 일반(Generic) 타입이라는 것이다.

제네릭은 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정된다.

(좀 더 정확히 말하자면 지정된다는 것 보다는 타입의 경계를 지정하고, 컴파일 때 해당 타입으로 캐스팅하여 매개변수화 된 유형을 삭제하는 것이다.)

2. 언제쓰나?

우리가  흔히 ArrayList, LinkedList 등을 생성할 때, <> 안에 타입을 적어서 객체를 생성한다.

ArrayList<Integer> list1 = new ArrayList<Integer>(); // Integer TYPE
ArrayList<String> list2 = new ArrayList<String>();  // String TYPE

LinkedList<Double> list3 = new LinkedList<Double>():   // Double TYPE
LinkedList<Character> list4 = new LinkedList<Character>();  // Character TYPE

- ArrayList는 Element Type의 Generic 을 지정할 수 있다.!

3. 타입?

가장 일반적으로 사용되는 유형 매개 변수 이름들이다. 암묵적으로 아래와 같이 많이 사용된다.

매개변수 이름을 <E>가 아니라 <Ele> 처럼 지정해서 사용 할 수도 있고, 설명과 일치하지 않아도 된다.

타입 안에는 참조 타입(Reference Type)을 넣을 수 있다.

보통 Integer, String 같은 Wrapper class사용자 정의 class 를 넣어서 사용한다.

타입 설명
<T> Type
<E> Element
<K> Key
<V> Value
<N> Number

자 이제 제네릭 클래스를 선언하고 사용해보자.!


1. 제네릭 클래스 및 인터페이스 선언

public class ClassName <T> { ... }
public Interface InterfaceName <T> { ... }

위와 같이 제네릭 타입으로 선언을 할 수 있으며, T 타입은 해당블럭 ( {...} ) 안에서까지 유효한다.

 

제네릭 타입은 두개 이상의 멀티 타입 파라미터로 사용할 수도 있다. ( HashMap< K, V> 가 대표적이다. )

public class ClassName <T, K> { ... }
public Interface InterfaceName <T, K> { ... }
public class HashMap <K, V> { ... }

 

2. 제네릭 클래스 생성

위에서 말한 대로 타입 안에는 참조 타입(Reference Type)만 넣을 수 있다.

 

- Wrapper Class로 생성

public class GenericClass <T, K> { ... }
 
public class Main {
	public static void main(String[] args) {
		GenericClass<String, Integer> a = new GenericClass<>();
	}
}

제네릭 클래스의 객체를 생성해서 T는 String, K는 Integer 타입으로 명시해주었다.

 

- 사용자 정의 클래스로 생성

public class GenericClass <T> { ... }
 
public class Student { ... }
 
public class Main {
	public static void main(String[] args) {
		GenericClass<Student> a = new GenericClass<>();
	}
}

Student 타입의 객체가 생성되었다.

 

3. 제네릭 클래스 사용

- single type parameter

// 제네릭 클래스
class GenericClass<E> {
	// 제네릭 타입 변수
	private E element;	
    
	// 제네릭 파라미터 메소드
	void set(E element){ this.element = element; }
    
	// 제네릭 타입 반환 메소드
	E get(){ return element; }
}
 
class Main {
	public static void main(String[] args) {
		
		GenericClass<String> a = new GenericClass<>();
		GenericClass<Integer> b = new GenericClass<>();
		
		a.set("10");
		b.set(10);
	
		System.out.println(a.get());  // 10
		System.out.println(a.get().getClass().getName());  // java.lang.String
		
		System.out.println(b.get());  // 10
		System.out.println(b.get().getClass().getName());  // java.lang.Integer
		
	}
}

 a 객체의 GenericClass의 E 제네릭 타입은 String으로 변환되었고,

 b 객체의 GenericClass의 E 제네릭 타입은 Integer로 변환되었다.

 

- multi type parameter

// 제네릭 클래스
class GenericClass<E> {
	private K first;	// K 타입(제네릭)
	private V second;	// V 타입(제네릭) 
	
	void set(K first, V second) {
		this.first = first;
		this.second = second;
	}
	
	K getFirst() { return first; }
	V getSecond() { return second; }
}
 
class Main {
	public static void main(String[] args) {
		
		GenericClass<String, Integer> a = new GenericClass<>();
		
		a.set("value", 10);
	
		System.out.println(a.getFirst());  // value
		System.out.println(a.getFirst().getClass().getName());  // java.lang.String
		
		System.out.println(a.getSecond());  // 10
		System.out.println(b.getSecond().getClass().getName());  // java.lang.Integer
		
	}
}

 

외부 클래스에서 제네릭 클래스를 생성할 때 <> 괄호 안에 타입을 파라미터로 보내 제네릭 타입을 지정해주는 것.!! 이 바로 제네릭 프로그래밍이다.

 

이제 메서드에 대해 알아보자.


1.  제네릭 메서드 선언

// [접근 제어자] <제네릭타입> [반환타입] [메소드명]([제네릭타입] [파라미터])
public <T> T genericMethod(T o) { ... }

<T> 타입으로 선언해봤다.

클래스와는 다르게 반환타입 이전에 <> 제네릭 타입을 선언한다.

 

2. 제네릭 메서드 사용

// 제네릭 클래스
class GenericClass<E> {

         ...
        
    // 제네릭 메서드
    <T> T genericMethod(T o) { return o; }
 
	
}
 
public class Main {
  public static void main(String[] args) {

    GenericClass<String> a = new GenericClass<>();
    GenericClass<Integer> b = new GenericClass<>();
		
    // Integer
    System.out.println("<T> returnType : " + a.genericMethod(3).getClass().getName());
		
    // String
    System.out.println("<T> returnType : " + a.genericMethod("ABCD").getClass().getName());
		
    // GenericClass
    System.out.println("<T> returnType : " + a.genericMethod(b).getClass().getName());
  }
}

method는 class 객체 생성과 동시에 생성된다.

genericMethod의 파라미터 타입에 따라 제네릭의 타입 <T> 가 결정된다.

위 결과로 제네릭 메서드는 클래스의 제네릭 타입과는 독립적으로 제네릭을 선언할 수 있다는 것을 알 수 있다.


그럼 위와같은 독립적 선언 방식이 왜 필요한가? 바로 '정적 메소드로 선언할 때 필요'하기 때문이다.

생각해보자. 제네릭은 타입을 외부에서 지정해준다고 했다. 즉 해당 클래스 객체가 인스턴스화 했을 때, 쉽게 말해 new 생성자로 클래스 객체를 생성하고 <> 괄호 사이에 파라미터로 넘겨준 타입으로 지정이 된다는 뜻이다.

 

하지만 static 변수, static 함수 등 static이 붙은 것들은 기본적으로 프로그램 실행시 메모리에 이미 올라가있다.

이 말은 객체 생성을 통해 접근할 필요 없이 이미 메모리에 올라가 있기 때문에 클래스 이름을 통해 바로 쓸 수 있다는 것이다.

 

이때 static으로 메소드를 만들면 객체가 생성되기 전에 이미 메모리에 올라가는데 타입을 어디서 얻어올 수 있을까? 

class GenericClass<E> {
 
	/*
	 * 클래스와 같은 E 타입이더라도
	 * static 메소드는 객체가 생성되기 이전 시점에
	 * 메모리에 먼저 올라가기 때문에
	 * E 유형을 클래스로부터 얻어올 방법이 없다.
	 */
	static E genericMethod(E o) {	// error!
		return o;
	}
	
}
 
class Main {
	public static void main(String[] args) {
 
		// ClassName 객체가 생성되기 전에 접근할 수 있으나 유형을 지정할 방법이 없어 에러남
		GenericClass.getnerMethod(3);
 
	}
}

그렇기 때문에 제네릭이 사용되는 메소드를 정적메소드로 두고 싶은 경우 제네릭 클래스와 별도로 독립적인 제네릭이 사용되어야 한다는 것이다.

 

// 제네릭 클래스
class GenericClass<E> {

  // 아래 메소드의 E타입은 제네릭 클래스의 E타입과 다른 독립적인 타입이다.
  // 제네릭 메소드
  static <E> E genericMethod1(E o) { return o; }
  
  static <T> T genericMethod2(T o) { return o; }
}
 
public class Main {
public static void main(String[] args) {
    GenericClass<String> a = new GenericClass<>();
  
    // Integer
    System.out.println("<E> returnType : " + ClassName.genericMethod1(3).getClass().getName());
 
    // String
    System.out.println("<E> returnType : " + ClassName.genericMethod1("A").getClass().getName());
    
    // GenericClass
    System.out.println("<T> returnType : " + ClassName.genericMethod1(a).getClass().getName());
 
    // Double
    System.out.println("<T> returnType : " + ClassName.genericMethod1(3.0).getClass().getName());
  }
}

제네릭 메서드는 제네릭 클래스 타입과 별도로 지정된 것을 볼 수 있다.

외부에서 <> 괄호 안에 타입을 파라미터로 보내 제네릭 타입을 지정해 주는것. 이것이 제네릭 프로그래밍!!

 


그런데 이런 의문이 들 수 있다. "아니 그러면 특정 범위만 허용하고 나머지 타입은 제한 할 수 없나요?"라는 얘기가 나오기 마련이다.

 

지금까지는 제네릭의 가장 일반적인 예시들을 보여주었다. 예로들어 타입을 T라고 하고 외부클래스에서 Integer을 파라미터로 보내면 T 는 Integer가 되고, String을 보내면 T는 String이 된다. 만약 당신이 Student 라는 클래스를 만들었을 때 T 파라미터를 Student로 보내면 T는 Student가 된다. 즉, 제네릭은 참조 타입 모두 될 수 있다.

 

근데, 만약 특정 범위 내로 좁혀서 제한하고 싶다면 어떻게 해야할까? 

이 때 필요한 것이 바로 extends 와 super, 그리고 ?(물음표)다. ? 는 와일드 카드라고 해서 쉽게 말해 '알 수 없는 타입'이라는 의미다.

1. 제한된 Generic

super와 extends에 대한 설명은 따로 포스팅 해 놓았다.

<K extends T>	// T와 T의 자손 타입만 가능 (K는 들어오는 타입으로 지정 됨)
<K super T>	// T와 T의 부모(조상) 타입만 가능 (K는 들어오는 타입으로 지정 됨)
 
<? extends T>	// T와 T의 자손 타입만 가능
<? super T>	// T와 T의 부모(조상) 타입만 가능
<?>		// 모든 타입 가능. <? extends Object>랑 같은 의미

 

이 때 주의해야 할 게 있다. K extends T와 ? extends T는 비슷한 구조지만 차이점이 있다.

'유형 경계를 지정'하는 것은 같으나 경계가 지정되고 K는 특정 타입으로 지정이 되지만, ?는 타입이 지정되지 않는다는 의미다.

 

/*
 * Number와 이를 상속하는 Integer, Short, Double, Long 등의
 * 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 K는
 * 지정된 타입으로 변환이 된다.
 */
<K extends Number>
 
 
/*
 * Number와 이를 상속하는 Integer, Short, Double, Long 등의
 * 타입이 지정될 수 있으며, 객체 혹은 메소드를 호출 할 경우 지정 되는 타입이 없어
 * 타입 참조를 할 수는 없다.
 */
<? extends T>	// T와 T의 자손 타입만 가능

 

2. <?> (와일드 카드 : Wild Card)

와일드 카드 <?> 은 <? extends Object> 와 마찬가지라고 했다. Object는 자바에서의 모든 API 및 사용자 클래스의 최상위 타입이다. 한마디로 다음과 같은 의미나 마찬가지다.

public class ClassName { ... }
public class ClassName extends Object { ... }

우리가 public class ClassName extends Object {} 를 묵시적으로 상속받는 것이나 다름이 없다.

한마디로 <?>은 무엇이냐. 어떤 타입이든 상관 없다는 의미다. 

이는 보통 데이터가 아닌 '기능'의 사용에만 관심이 있는 경우에 <?>로 사용할 수 있다.

 


ㅁㅏ치며

 

빡세다.. 하지만 제네릭에 대해서 잘 알수 있게 된듯하다

제네릭은.. 외부 클래스에서 제네릭 클래스를 인스턴스 화 할때 <> 괄호 안에 파라미터로 제네릭 타입을 지정하는 것!! 참조 타입만 올수 있다!!! 잊지말자

처음에 잘 정리 되어있는 글을 봤을때 내용도 길고 복잡시러 보였는데 읽고 이해하려고 노력하니까 챱챱정리가 되었다.

여러가지 제네릭 관련 포스팅을 찾아보았는데 솔직히 잘 정리된 글 하나를 복붙한 내용들이 많았다.

나는 그래서 내가 읽기 편하고 좀더 간결하게? 필요한 내용들만 정리를 한것이다. 나름 잘 한것같다.

다음에는 뭐 해볼까~~

반응형

댓글