본문 바로가기
programming_kr/java

제네릭(generic)

by JSsunday 2022. 9. 24.
728x90

제네릭이란 무엇일까?

 

제네릭(Generic)은 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미합니다. 특정(Specific) 타입을 미리 지정해주는 것이 아닌 필요에 의해 지정할 수 있도록 하는 일반(Generic) 타입입니다.

 

Generic(제네릭)의 장점

 

  1. 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있습니다.
  2. 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없습니다.
  3. 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아집니다.

 

Generic(제네릭) 사용방법

 

보통은 아래 표의 타입들이 많이 사용됩니다.

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

 


Generic(제네릭)의 사용

 

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

public class Generic1 <T> {}
public Interface Generic1 <T> {}

 

기본적으로 generic 타입의 클래스와 인터페이스의 경우는 위와 같이 선언합니다.

T타입은 해당 블럭 {} 안에서까지 유효합니다.

 

public class GenericTest1 <T, K> {}

또한 타입은 두 개 이상 지정할 수 있습니다.

 

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

HashMap은 위와 같이 두 개의 generic 타입으로 지정되어 있습니다.

 

위와 같이 생성된 generic 타입의 클래스를 사용하려고 할 때는 인스턴스를 생성시 구체적인 타입을 명시해주어야 합니다.

public class GenericTest1 {
  public static void main(String args[]) {
    GenericClass<String, Integer> gc = new GenericClass<String, Integer>("John", 38);
    gc.printString();
    //John
    //38
  }
}

class GenericClass<T, K> {
  private T tType;
  private K kType;

  public GenericClass(T t, K k){
    this.tType = t;
    this.kType = k;
  }

  public void printString(){
    System.out.println(this.tType);
    System.out.println(this.kType);
  }
}

위의 예시에서는 GenericClass의 T 타입은 String이 되고 K 타입은 Integer가 됩니다.

타입으로는 참조 타입(Reference Type)만 올 수 있습니다. int, double, char 같은 원시 타입(primitive type)은 올 수 없습니다. 그래서 int 대신에 Integer, double 대신에 Double 같은 Wrapper class를 사용합니다.

 

 

또한 사용자가 만든 클래스도 generic 타입으로 사용이 가능합니다.

GenericClass<Student, String> gc2 
    = new GenericClass<Student, String>(new Student("John", 38), "John");
class Student{
  private String name;
  private Integer age;

  public Student(String name , Integer age){
    this.name= name;
    this.age = age;
  }
}

 

2. Generic 메서드

 

generic 메서드는 메서드의 선언 부에 적은 generic으로 리턴 타입, 파라미터의 타입이 정해지는 메서드 입니다.

 

generic에 대한 예시를 보면서 이해해봅시다.

public class Student<T> {
	static T name;
}

 

static 변수는 generic에서 사용할 수 없습니다. Student 클래스가 인스턴스화 되기 전에 static은 메모리에 올라가는데 이 때 name의 타입인 T가 어떤 타입인지 결정되지 않았기 때문에 사용할 수 없습니다.

 

public class Student<T> {
    static T getName(T name){
    	return name;
    }
}

static 메서드에도 generic을 사용하면 static 변수와 마찬가지로 Student 클래스가 인스턴스화 되기전에 메모리에 올라가는데 T의 타입이 정해지지 않았기 때문에 에러가 발생합니다.

 

하지만 사용 가능한 경우가 있습니다.

generic 메서드는 호출 시에 파라미터 타입을 지정하기 때문에 static이 가능합니다.

public class Student<T> {
  static <T> T getStudent(T id){
    return id;
  }
}

사용방법은 return type 앞에 generic을 사용해주면 됩니다. 여기서 Student 클래스에서 지정한 generic 타입 <T>와 generic 메서드 리턴 타입에 있는 <T>는 같은 T를 사용한다고 하더라도 다른 타입입니다.

return type 앞에 있는 T는 파라미터 T를 나타냅니다.

 

클래스에 표시하는 <T>는 인스턴스 변수라고 생각합시다. 인스턴스가 생성될 때 마다 지정되기 때문입니다. 그리고 generic 메서드에 붙은 T는 지역변수 선언한 것과 같다고 생각합시다.

 

그러면 아래의 코드의 경우를 생각해봅시다.

public interface List<E> extends Collection<E> {
    boolean add(E e);
}

List의 인터페이스의 add 메서드입니다. List에 있는 E와 add 메서드에 있는 파라미터 타입 E는 같은 타입일까? 생각을 해보면 이 때는 두 개의 타입이 같습니다.

 

generic 메서드를 사용하면 T가 지역변수로 바뀝니다.

위의 Student 클래스를 보면, 클래스에 붙은 T(Student<T>)와 메서드에 붙은 T(static <T> getStudent(T id))는 다르고, List에 있는 E와 List인 인터페이스의 add 메서드에 있는 E는 같은 E입니다.

 

public static void printAll(ArrayList<? extends Test> list1, ArrayList<? extends Test> list2) {
        // 로직
}
    
public static <T extends Test> void printAll(ArrayList<T> list1, ArrayList<T> list2) {
        // 로직
}

generic 메서드를 사용하지 않는다면 첫 번째의 파라미터의 타입에다가 타입제한을 해야 합니다. 파라미터마다 generic 타입을 이용하는 것이 아니라 두 번째와 같이 generic 메서드를 사용하면 보다 간결하게 바꿀 수 있습니다.

 

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

위의 generic 메서드를 보면 T는 모두 같은 타입입니다.

 

<T extends Comparable<T>

위의 generic 메서드에 사용한 것을 보면 반드시 Comparable 인터페이스를 구현한 클래스 타입이어야 합니다.

 

<? super T>

위 뜻은 T와 타입이 같거나 조상클래스만 ?에 가능하다는 의미입니다. 예를 들면 A클래스가 있고 A클래스의 조상 B클래ㅡ 일때 ?에는 A, B, Object가 가능합니다.

 

Generic 메서드의 사용 이유

타입캐스팅 에러의 경우를 제외시킬 수 있기 때문에 훨씬 안전하게 사용할 수 있어서 사용합니다. 그리고 generic 메소드를 사용하면 클래스의 T와 메소드의 T는 같은 문자를 사용하더라도 다른 문자라는 것을 기억합시다.

 

Generic 메서드의 사용

public class Box<T> {
    private T t;
    
    public T getT() {
        return t;
    }
    
    public void setT(T t) {
        this.t = t;
    }
}
public class Util {
    
    public static<T> Box<T> boxing(T t) {
        Box<T> box = new Box<T>();
        box.setT(t);
        return box;
    } 
 }
public class Main {
    public static void main(String[] args) {
        Box<Integer> box1 = Util.<Integer>boxing(100);
        
        Box<String> box2 = Util.boxing("암묵적호출");
    }
}

generic 메서드를 호출하는 방법에는 2가지가 있는데 타입을 지정하는 방법과 지정하지 않는 방법이 있습니다. 타입을 지정하게 되면 컴파일러가 <Integer>를 보고 타입을 지정하고 지정하지 않는 경우, 파라미터 타입이 String인 것을 확인하고 컴파일러가 타입을 추정합니다.

 

제너릭 클래스와 독립적

 

형식과 사용 방법이 generic 클래스와 똑같지만, 클래스의 <T>와 제너릭 메소드의 <T>는 다르기 때문에 잘 생각해야 합니다. 그리고 generic 메소드는 그 메소드를 포함하고 있는 클래스가 제네릭인지 아닌지 상관하지 않습니다.

 

class Student<T>{

    public T getStudent(T id){ return id; }  // 1
    
    public <T> T getId(T id){return id;} // 2 제네릭 클래스의 T와 다름  
    
    public <S> T toT1(S id){return id; }  // 3
    
    public static <S> T toT2(S id){return id;}  // 4 에러 
}
  • 1번의 경우는 return 타입, 파라미터 타입이 T이고 클래스의 generic 타입 T를 그대로 사용하는 경우입니다.
  • 2번의 경우 클래스의 generic 타입 T와 제너릭 메소드 타입 T는 다릅니다.
  • 3번의 경우 static 메소드가 아닌 일반메소드기 때문에 클래스의 타입과 generic 메소드의 타입을 같이 사용가능하다.
  • 4번의 경우 static 메소드이기 때문에 클래스의 제너릭 타입 T를 사용하기 때문에 에러가 발생합니다.(클래스의 T 타입을 사용하려면 인스턴스화가 되어야 합니다.)

StreamTest1.java

import java.util.ArrayList;
import java.util.Collections;
import java.util.Optional;

public class StreamTest1 {
  
  public static void main(String[] args){

    Box<Vehicle> vehicleBox = new Box<>();

    vehicleBox.add(new Car(4, "car", 1000));
    vehicleBox.add(new Ship("ship", 500000));
    vehicleBox.add(new Car(6, "car", 10000));

    Collections.sort(vehicleBox.getList(), (v1, v2)->{
      return v1.weight - v2.weight;
    });

    Print.printMethod(vehicleBox);
    // type : car, weight : 1000, wheel : 4
    // type : car, weight : 10000, wheel : 6
    // type : ship, weight : 500000
  }

}

class Print {
  static <T extends Vehicle> void printMethod(Box<T> box){

    for(Vehicle vehicle: box.getList()){

      StringBuffer result = new StringBuffer("type : " + vehicle.type + ", weight : " + vehicle.weight);

      Optional.ofNullable(vehicle)
        .filter((v)-> v instanceof Car)
        .ifPresent((v)->{
          result.append(", wheel : " + ((Car) v).wheel);
        });
      
      System.out.println(result);
    }
  } 
}

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

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

  ArrayList<T> getList(){
    return list;
  }

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

class FruitBox<T extends Fruit> extends Box<T> {};

class Car extends Vehicle {

  int wheel;

  public Car(int wheel, String type, int weight){
    super(type, weight);
    this.wheel = wheel;
  }

}

class Ship extends Vehicle {

  public Ship(String type, int weight){
    super(type, weight);
  }
}

class Vehicle {

  String type;
  int weight;

  public Vehicle(String type, int weight){
    this.type = type;
    this.weight = weight;
  }

  public String toString(){
    return "type : " + type + ", weight : " + weight; 
  }

}

class Fruit {
  String name;
  int weight;

  public Fruit(String name, int weight){
    this.name = name;
    this.weight = weight;
  }
}

참조
[JAVA] 제네릭 메서드
자바 [JAVA] - 제네릭(Generic)의 이해

 

728x90

'programming_kr > java' 카테고리의 다른 글

열거형(enum)  (0) 2022.10.29
JAVA8 STREAM(1)  (1) 2022.10.15
JAVA8 Optional 사용법  (1) 2022.09.17
제어자(modifier)  (0) 2022.09.10
람다식(Lambda)  (0) 2022.08.30

댓글