성장일지 2기 1차

이태형·2024년 9월 22일

Growth Log

목록 보기
5/18

정의

  • 데이터 타입을 특정한 타입 하나로 지정하지 않고 사용할 때마다 바뀔 수 있게 범용적이고 포괄적으로 지정하는 방법
  • 데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법
  • 클래스 내부에서 타입을 지정하지 않고, 외부 ( 사용하는 측 ) 에서 지정되는 것을 의미

장점

  • 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지 가능
  • 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없음, 관리 용이
  • 비슷한 기능을 지원하는 경우 코드의 재사용이 높아짐

타입

  • 보통 많이 쓰이는 타입 지정 방식
  • 반드시 아래에 타입과 동일할 필요는 없으나, 아래와 같이 명명 규칙임
타입설명
TType
EElement
KKey
VValue
NNumber

제네릭 클래스

  • 클래스 및 인터페이스 선언
  • 타입은 해당 블럭 { … } 안에서까지만 유효
public class ClassName <T> { ... }

public Interface InterfaceName <T> { ... }
  • 제네릭 타입을 두개로 선언 및 생성
  • 타입 파라미터로 명시할 수 있는 것은 참조 타입(Reference Type) 밖에 지정 못함
    • int , double, char 같은 primitive type 은 올 수 없음
    • Integer, Double 형등으로 Wrapper Type 으로 사용
public class ClassName <T , K> { ... }

public class Main {
	public static void main(String[] args) {
		// 해당 클래스를 생성할 경우, 구체적인 타입 명시
		ClassName<String, Integer> a = new ClassName<String, Integer>();
	}
}
  • 제네릭 하나 선언
    • 제네릭 클래스 선언

      class ClassName<E> {
      		private E element ;
      		
      		void set(E element){
      			this.element = element; 
      		}
      
      		E get(){
      			return element;
      		}
      }
    • Main 클래스

      class Main{
      		public static void main(String[] args){
      				ClassName<String> a = new ClassName<String>();
      				ClassName<Integer> b = new ClassName<Integer>(); 
      
      				a.set("10"); 
      				b.set(10);
      
      				System.out.println("a data : " + a.get());
      				// 반환된 변수의 타입 출력
      				System.out.println("a E Type : " + a.get().getClass().getName());
      
      				System.out.println("b data : " + b.get());
      				// 반환된 변수의 타입 출력
      				System.out.println("b E Type : " + b.get().getClass().getName());
      		}
      }
      
      /*   RESULT     */
      a data : 10
      a E Type : java.lang.String
      
      b data : 10 
      b E Type : java.lang.Integer 
  • 제네릭 두개 선언
    • 제네릭 클래스

      class ClassName<K, V> {
      		private K key; // K 타입 
      		private V value; // V 타입
      
      		void set(K key , V value){
      				this.key = key; 
      				this.value = value;
      		}
      
      		K getKey() {
      				return key;
      		}
      
      		V getValue() {
      				return value; 
      		}
      }
    • Main 클래스

      class Main {
      		public static void main(String[] args) {
      				ClassName<String, Integer> a = new ClassName<String, Integer>();
      				
      				a.set("10", 10);
      
      				System.out.println(" Key data : " + a.getKey());  
      				// 반환된 변수의 타입 출력
      				System.out.println(" K Type : " + a.getKey.getClass().getName());
      
      				System.out.println(" Value Data : "+ a.getValue());
      				// 반환된 변수의 타입 출력
      				System.out.println(" V Type : "  + a.getSecond().getClass().getName());
      		}
      }
      
      /* RESULT */
      Key data : 10
      K Type : java.lang.String
      Value data : 10
      V Type : java.lang.Integer

제네릭 메서드

  • 반환타입 이전에 <> 제네릭 타입 선언
    • [접근 제이자] <제네릭타입> [반환타입][메소드명] ( [제네릭타입][파라미터] )
public <T> T genericMethod(T o){
		...
}
  • 사용 이유
    • 정적 메소드로 선언할 때 필요 ( static 메소드 )
      • Class 제네릭 유형은 외부에서 지정해줌, 해당 클래스 객체가 인스턴스화 했을 때, <파라미터> 타입으로 지정
      • static 메서드는 객체가 생성되기 전에 이미 메모리에 올라가기 때문에, 외부에서 지정한 타입을 얻어올 방법이 없음
      • 그래서 정적 메소드를 사용하고 싶은 경우는 제네릭 클래스와 별도로 독립적인 제네릭이 사용되어야 함
  • 사용 예시
    • 제네릭 메서드는 파라미터 타입에 따라 T 타입이 결정
    • 클래스에서 지정한 제네릭 유형과 별도로 메소드에서 독립적으로 제네릭 유형을 선언하여 사용 가능
class ClassName<E> {
		private E element;
		
		void set(E element) {
			this.element = element;
		}
		
		E get(){
			return element;
		}

		// 아래 메소드의 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) {
				// 제네릭 메소드1 Integer
				System.out.println("<E> returnType : " + ClassName.genericMethod1(3).getClass().getName());
				// 제네릭 메소드1 String
				System.out.println("<E> returnType : " + ClassName.genericMethod1("ABCD").getClass().getName());
				// 제네릭 메소드2 ClassName a
				System.out.println("<T> returnType : " + ClassName.genericMethod1(a).getClass().getName());
				// 제네릭 메소드2 Double
				System.out.println("<T> returnType : " + ClassName.genericMethod1(3.0).getClass().getName());
				
		}
}

타입 제한 과 와일드 카드

  • 일반적인 예시에서 제네릭 T 는 Interger , String 등, 외부 선언 방식에 따라 모든 참조 타입을 가질 수 있음
  • 와일드 카드( extends , super , ? ) 를 사용하여 범위 제한이 가능
    • extends T : 상한 경계
    • ? super T : 하한 경계
    • : 와일드 카드(wild card)
<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, Dobule, Long 등의 
	타입이 지정될 수 있고, 객체 혹은 메소드를 호출 할 경우 K는 
	지정된 타입으로 변환
*/
<K extends Number>

/*
	Number 와 이를 상속하는 Integer, Short, Double, Long 등의
	타입이 지정될 수 있고, 객체 혹은 메소드를 호출 할 경우 지정되는 타입이 없어
	타입 참조를 할 수 는 없다
*/
<? extends T > // T와 T의 자손 타입만 가능
  • extends 와 super
    - extends 뒤에 오는 타입이 최상위 타입으로 한계가 정해짐
    - 특정 타입 및 그 하위 타입만 제한하고 싶은 경우에 사용

// T 타입을 포함한 자식(자손) 타입만 가능
< T extends B > // B와 C타입만 올 수 있음
< T extends E > // E 타입만 올 수 있음
< T extends A > // A, B, C, D, E 타입이 올 수 있음

< ? extends B > // B와 C타입만 올 수 있음
< ? extends E > // E 타입만 올 수 있음
< ? extends A > // A, B, C, D, E 타입이 올 수 있음

// Integer, Long, Byte, Double, Float, Short 같은 수를 표현하는 래퍼 클래스로만 제한
public class ClassName <K extends Number> { ... }

public class Main {
	public static void main(String[] args) {
 
		ClassName<Double> a1 = new ClassName<Double>();	// OK!
 
		ClassName<String> a2 = new ClassName<String>();	// Bound mismatch
	}
}
  • , <? super T>
    • T 타입의 부모(조상) 타입만 가능
    • 대표적으로 해당 객체가 업캐스팅 (Up Casting) 이 될 필요가 있을 때 사용
<K super B> // B와 A타입만 올 수 있음
<K super E> // E, D, A 타입만 올 수 있음
<K super A> // A타입만 올 수 있음

<? super B> // B와 A타입만 올 수 있음
<? super E> // E, D, A 타입만 올 수 있음
<? super A> // A 타입만 가능
  • 업캐스팅 예제 -1
    • extends 뒤에오는 타입이 최상위 타입이 되고, 해당 타입과 그에 대한 하위 타입, 역으로 생각해보면 E 객체는 반드시 Comparable을 구현해야한다는 의미

      public class ClassName<E extends Comparable<? super  E>>{ ... }
      
      // Example
      public class SaltClass <E extends Comparable<E>> { ... }
      
      public class Student implements Comparable<Student> {
      		@Override
      		public int compareTo(Person o) { ... }; 
      }
      
      public class Main{
      		public static void main(String[] args) {
      				SaltClass<Student> a = new SaltClass<Student>();
      		}
      }
  • 업캐스팅 예제 -2
    • Comparable 가 아닌 <? super E> 인 이유
      - super E 는 E를 포함한 상위 타입 객체들이 올 수 있음
      - Person을 상속받고 Comparable 구현부인 comparTo 에서 Person 타입으로 업캐스팅 한다면 아래 상황이 발생
      - <E extends Comparable> 라면 SaltClass a 객체가 타입 파라미터로 Student 를 주지만, Comparable 에서는 그보다 상위 타입인 Person으로 비교하기 때문에 Comparable 의 E인 Student보다 상위 타입 객체이기 때문에 제대로 정렬이 안되거나 에러가 날 수 있음
      - 그렇기 때문에 E 객체의 상위 타입, 즉 <? super E> 을 해줌으로써 위와 같은 불상사를 방지할 수가 있음
      - 즉 <E extends Comparable<? super E>> 는 E 타입 또는 슈퍼클래스가 Comparable 을 의무적으로 구현해야한다는 뜻으로 슈퍼 클래스 타입으로 업캐스팅이 발생하더라도 안정성을 보장

      public class SaltClass <E extends Comparable<E>> { ... } // Error 가능성
      public class SaltClass <E extends Comparable<? super E>> { ... } // 안전성이 높음
      
      public class Person { ... }
      
      public class Student extends Person implements Comparable<Person> {
      		@Override
      		public int compareTo(Person o) { ... };
      }
      
      public class Main{
      		public static void main(String[] args){
      				SaltClass<Student> a = new SaltClass<Student>();
      		}
      }
  • 와일드카드
    • 와 동일, 즉 어떤 타입이든 상관없음 ```java public class ClassName { ... } public class ClassName extends Object { ... } ```

0개의 댓글