제네릭(Generic)

임준철·2021년 4월 11일
0

OOPAdvance

목록 보기
3/4

제네릭이란

  • 타입을 파라미터화해서 컴파일시 구체적인 타입이 결정되도록 하는 것 컬렉션, 람다식(함수적 인터페이스), 스트림 ,NIO에서 널리 사용된다.
  • 대상 객체의 타입을 입력받아서 사용하는 형식
  • 실행시 타입에러가 나는 것보다 컴파일시에 미리 타입을 강하게 체크해서 에러를 사전에 방지할 수 있다, 타입 변환을 제거할 수 있다.
    • 일부 타입을 제한할 수 있는 기능이 있음.
    • 입력을 object 로 할 수 있으나, 런타임에 instanceof 로 객체를 체크해야 함.
      • 원하는 객체가 들어왔는지 계속 확인해야 한다.
  • 제네릭을 사용할 경우 간결하게 코드 작성을 할 수 있다.
  • 클래스를 선언할 때에는 타입이 알려지지 않으며, 타입 파라미터를 사용한다.
  • 생성자도 갖을 수 있다, 객체 참조 가능. 인터페이스, 추상클래스 상속 가능, 프리미티브 타입 불가능.
  • 타입의 상속시 부모와 자식클래스의 타입을 맞추어 줘야 한다. 타입 파라미터도 모두 채워 주어야 한다.
    부모클래스/인터페이스의 동일한 타입 파라미터를 넘겨줄 수 있다.
  • static 멤버 변수, static 메소드는 타입 파라미터를 사용할 수 없다.
    • static void staticMethod(T t)는 안되고 / static

      p staticMethod(P p) 는 가능하다.

    • static 메소드도 사용이 가능하다. 다만, 클래스와 static 메소드의 타입 파라미터를 갖게 하면 안된다.
    • 왜냐하면, static은 객체가 생성되기 전에 메모리에 할당되는데 메모리에 할당되려면 타입이 있어야 하고,
    • 타입 파라미터는 객체를 생성할 때 값이 넘어오기 때문에
  • new 키워드를 사용하여 객체를 참조할 수 없다.
  • instanceof의 피연산자로 사용할 수 없다.

제네릭 타입을 사용할 경우의 효과

  • 비 제네릭을 사용할 경우 Object 타입을 사용하므로 빈번한 타입 변환생성 -> 성능 저하
  • 제네릭을 사용할 경우 클래스를 선언할 때 타입 파라미터를 기술한다. 컴파일시 타입 파라미터가 구체적인 클래스로 변경된다.
    유연하게 타입을 선언하여 처리할 수 있다.

제네릭 클래스

제네릭 타입

  • 제네릭 타입이란 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다.
    타입을 파라미터로 가진다 -> 선언시 클래스 또는 인터페이스 이름 뒤에 <> 부호가 붙는다.
    • 타입 파라미터 - 개발코드(실행하는 곳)에서는 타입 파라미터 자리에 구체적인 타입을 지정해야 한다.
  • 클래스와 인터페이스에 제네릭이 적용된 타입
  • 클래스를 선언할 때에 타입이 알려지지 않으며, 타입 파라미터를 사용.
     //일반 클래스
     class Foo{
     
     }
     
     // 제네릭 클래스
     // 클래스를 선언할 때에는 설정되지 않은 타입이 있으며, 이것을 타입 파라미터로 표현한다.
     class GenericFoo<T> { // T: 타입 파라미터
         String name;
         T memberVar; // T를 자료형으로 사용.
     
         public GenericFoo(String name, T memberVar) {
             //생성자 생성 어떤 타입의 자료형이 들어오는지 모르는데 구현 가능.
             this.name = name;
             this.memberVar = memberVar;
         }
     }
     //제네릭 인터페이스
     interface GenericInterface<T>{ //인터페이스도 제네릭이 가능
         
     }
     class HashMap<k,w>{ // 여러 개의 타입 파라미터로 쓸 수 있다.
     
     }
  • 제네릭 타입은 실제로 사용될 때 타입 파라미터에 자료형을 입력받는다.
     GenericFoo<String> foo = new GenericFoo<String>("name","var");
      //**생성할 때 자료형을 정해주게 되어 있음.**
      // <>안에 스트링으로 넣어줬기 때문에 T도 스트링으로 된다.
     
     GenericFoo<String> foo1 = new GenericFoo<>("name","var");//뒤에 <>생략가능

주의 해야할 문법들

  • 메소드의 중괄호 {} 안에서 타입 파라미터 변수로 사용 가능한 것은 상위 타입의 멤버(필드,메소드)로 제한된다.
  • 하위 타입에만 있는 필드와 메소드는 사용할 수 없다.
  • static은 객체가 생성 전에 자료형이 결정되어야 하는데,
  • class GenericBar의 타입 파라미터와 static 변수, 메소드는 같은 타입 파라미터를 갖고 있기 때문에 안됨!
class  GenericBar<T>{
  
    // 제네릭은 객체를 생성할때 자료형을 입력받기 때문에
    // 스태틱은 객체 생성 전에 자료형이 결정되어야 하기때문에 
    // 클래스에 속하기 때문에 T를 사용하면 자료형 알 수 없음.
    // static은 클래스에 속하기 때문에 애초에 T가 쓸수 없음 문법적 문제
  
    static T classVar; 
    // 안된다.. 클래스의 스테틱변수는 객체와 관계없이 클래스에 속해있기 때문에
    // 스태틱 변수가 생겨야하는 생성시점에는 T 자료형이 없기 때문에
    static void method(T var){} //이것도 마찬가지로 불가능.
  • 문법적으로 문제가 없을 것 같으나, 안정성 문제로 금지. 암기해야 하는 됨!
  • instanceof의 피연산자로 사용할 수 없다.
  • new 키워드를 사용하여 객체 생성을 할 수 없다.
    // T memberVar = new T(); t는 new 키워드를 쓸 수 없음. 안정성 문제 때문에 불가능하다.
    // T라는 자료형이 결정되지 않기때문에 생성자가 어떻게 정의될지 모르기 때문에 불가능 자바에서 막혀있음.
    // 타입파라미터의 객체를 생성하는 것은 불가능 하다.
      {
          Object obj = new Object();
          if(obj instanceof T){ 
  // 어떤 객체가 있을 때 T에 속하는지 확인하기 불가능. 안정성 문제 때문에 그냥 막혀있음.
    
       }
    }

제네릭 타입의 상속

  • 부모 클래스 또는 인터페이스에 선언한 타입 파라미터는 반드시 자식에서도 선언해야 된다.

  • 자식 클래스에서 추가적인 타입 파라미터를 선언할 수 있다.

  • 부모 클래스와 인터페이스의 타입 파라미터가 같을 경우 상속받는 클래스에서 다르게 넣어주면 된다.

    class Product<T,M> {
    	private T kind;
    	private M model;
    	
    	public T getKind() {
    		return kind;
    	}
    	public void setKind(T kind) {
    		this.kind = kind;
    	}
    	public M getModel() {
    		return model;
    	}
    	public void setModel(M model) {
    		this.model = model;
    	}
    }
    class Tv{
    }
    class ChildProduct<K,V,C> extends Product<K,V>{
    	private C company;
    
    	public C getCompany() {
    		return company;
    	}
    
    	public void setCompany(C company) {
    		this.company = company;
    	}
    }
    public class ChildProductAndStorageExam {
    
    	public static void main(String[] args) {
    		ChildProduct<Tv,String,String> childProduct = new ChildProduct<Tv, String, String>();
    		childProduct.setKind(new Tv());
    		childProduct.setModel("스마트티비");
    		childProduct.setCompany("삼성");
    	}
    }

파라미터 타입의 제한

  • extends를 이용하여 파라미터 타입을 제한할 수 있다.
    • 인터페이스의 경우에도 extends 키워드를 사용한다.
    • 클래스와 인터페이스를 동시에 제약하려면 &로 연결한다.
  • 제한한 자료형의 자식 클래스는 모두 사용할 수 있다.
     //타입 제한을 하지 않으면 extends object와 동일하다.
     class GenericNoTypeLimit<T extends Object>{
     }
     
     //extends를 이용해서 부모클래스를 제한 할 수 있음, 추상클래스를 상속하면서 인터페이스를 추가로 구현할 수 있음 
     // 추상클래스+인터페이스를 구현해야한다
     class GenericTypeLimitation<T extends Number & Cloneable>{
     
     }

제네릭 메소드

  • 매개변수 타입과 리턴타입으로 타입 파라미터를 갖는 메소드를 말한다.
    실제 메소드를 호출할 때 매개변수와 리턴 타입을 지정해서 사용한다.

메소드에 선언된 제네릭

  • 메소드의 리턴 타입 앞에 타입 파라미터 변수를 선언하여 사용
  • public <타입파라미터,...> 리턴타입 메소드명 (매개변수){ ... }
class GenericMethod{
    public <T> T method(T x){
    // 여기서 <T> 는 제네릭 타입으로 선언을 하겠다라는 의미이다.
        return x;
    }
}
  • 메소드에 선언된 제네릭은 정적 메서드에도 사용 가능.
class GenericMethod<T> {
  public static void method(T t) { // Error
    ...
  }

  public static <P> P method (P p) { // Possible
    ...
  }
}

제네릭 메소드를 호출하는 두가지 방법

  • 리턴타입 변수 = <구체적인 타입> 메소드명(매개값);
  • 리턴타입 변수 = 메소드명(매개값); // 주로 이방법을 더 많이 사용한다.
class Box<T> {
	private T t;
	public void set(T t) {
		this.t = t;
	}
	public T get() {
		return t;
	}
}

class Util {
	public static <T> Box<T> boxing(T t){
		Box<T> box = new Box<T>();
		box.set(t);
		return box;
	}
}

public class BoxingMethodExam {

	public static void main(String[] args) {
		Box<Integer> box = Util.<Integer>boxing(123);
		Box<String> box2 = Util.boxing("홍길동");
		
		System.out.println(box.get());
		System.out.println(box2.get());
	}
}

와일드 카드

  • 이미 선언되어 있는 제네릭 타입을 매개변수나 리턴타입으로 사용할 때 타입 파라미터를 제한할 목적

    • 비교 <T extends 상위 또는 인터페이스> 는 제네릭 타입과 제네릭 메소드를 선언할 때 제한한다.
  • 와일드카드 ?는 메소드의 입력 타입에 제네릭이 쓰일 경우, 제네릭의 타입 변수를 제한할 수 있다.

  • <?> => <? extends Object>와 동일

  • <? extends T> => 와일드카드의 상한을 제한

  • <? super T> => 와일드카드의 하한을 제한

    // 와일드카드
    class Person {
    	private String name;
    	public Person(String name) {
    		this.name = name;
    	}
    	
    	public String getName() {
    		return name;
    	}
    	
    	@Override
    	public String toString() {
    		
    		return name;
    	}
    }
    class Worker extends Person {
    
    	public Worker(String name) {
    		super(name);
    	}
    }
    class Student extends Person{
    
    	public Student(String name) {
    		super(name);
    	}
    }
    class HighStudent extends Student{
    
    	public HighStudent(String name) {
    		super(name);
    	}
    }
    public class Course<T> {
    	private String name;
    	private T[] students;
    	
    	public Course(String name, int capacity) {
    		this.name = name;
    		students = (T[])new Object[capacity];
    		// T타입으로 new 로 생성을 못하니깐 오브젝트로 만든다음
    		// 타입 변환을 해야 한다.
    	}
    	
    	public String getName() {
    		return name;
    	}
    	public T[] getStudents() {
    		return students;
    	}
    	
    	public void add(T t) {
    		for(int i=0; i<students.length; i++) {
    			if (students[i] == null) {
    				students[i] = t;
    				break;
    			}
    		}
    	}
    }

출력

package sec02.exam06_generic_wildCard;

import java.util.Arrays;

public class WildCardExam {
	public static void registerCourse(Course<?> course) {
		System.out.println(course.getName()+"수강생 : "+Arrays.toString(course.getStudents()));
	}
	// Student, HightStudent 클래스 타입만 가능, 상한을 제한한다
    public static void registerCourseStudent(Course<? extends Student> course) {
    	System.out.println(course.getName()+"수강생 : "+Arrays.toString(course.getStudents()));
	}

    // Person, Worker 클래스 타입만 가능, 하한을 제한한다.
    public static void registerCourseWorker(Course<? super Worker> course) {
    	System.out.println(course.getName()+"수강생 : "+Arrays.toString(course.getStudents()));
  	}

	public static void main(String[] args) {
		Course<Person> personCourse = new Course<Person>("일반인 과정",5);
		personCourse.add(new Person("일반인"));
		personCourse.add(new Person("직장인"));
		personCourse.add(new Person("학생"));
		personCourse.add(new Person("고등학생"));
		
		Course<Worker> workerCourse = new Course<Worker>("직장인 과정",5);
		workerCourse.add(new Worker("직장인"));
		
		Course<Student> studentCourse = new Course<Student>("학생 과정",5);
		studentCourse.add(new Student("학생"));
		studentCourse.add(new HighStudent("고등학생"));

		
		Course<HighStudent> highStudentCourse = new Course<HighStudent>("고등학생 과정",5);
//		highStudentCourse.add(new Student("일반인")); //error
		highStudentCourse.add(new HighStudent("직장인"));
		
		registerCourse(personCourse); // 일반인 과정수강생 : [일반인, 직장인, 학생, 고등학생, null]
		registerCourse(workerCourse); // 직장인 과정수강생 : [직장인, null, null, null, null]
		registerCourse(studentCourse); // 학생 과정수강생 : [학생, 고등학생, null, null, null]
		registerCourse(highStudentCourse); // 고등학생 과정수강생 : [직장인, null, null, null, null]


		//registerCourseStudent(personCourse); error
		//registerCourseStudent(workerCourse); error
		registerCourseStudent(studentCourse); // 학생 과정수강생 : [학생, 고등학생, null, null, null]
		registerCourseStudent(highStudentCourse); // 고등학생 과정수강생 : [직장인, null, null, null, null]
		
		
		registerCourseWorker(personCourse); // 일반인 과정수강생 : [일반인, 직장인, 학생, 고등학생, null]
		registerCourseWorker(workerCourse); // 직장인 과정수강생 : [직장인, null, null, null, null]
		//registerCourseWorker(studentCourse); error
		//registerCourseWorker(highStudentCourse); error
	}
}

동적 바인딩

  • 메소드가 생길 때 자료형이랑 정의되어 있어서 기계가 동작하게 만들어지게 정상적인데, 자료형이 정해져있지 않아서 들어오는거에 따라 동작이 달라짐.
    실제로 런타임이 되지 않으면 컴파일 타임이 알 수 없음.( 컴파일 타임에는 동작이 완전히 정의가 되지 않음)
profile
지금, 새로운 문을 열자! 문 저편에 무엇이 있을지 두렵더라도!!

0개의 댓글