Java : Generics

공부의 기록·2021년 11월 1일
0

Java

목록 보기
9/19
post-thumbnail

Generics

본 글은 2021년 12월 19일 에 기록되었다.

지네릭스에 대한 이론적인 공부가 전반되어 있지 않은 상태에서
그 특성 및 사용 예시에 대한 공부를 진행하였기 때문에 결점이 많다고 생각되어
이를 다시 공부하고 재 작성하게 되었다.

본 글의 이전 버전 작성일인 2021년 12월 13일
Effective Java 4 | Generics 를 배우고 이에 대한 내용의 필요성을 체감하고 배우게 되었다.


이론

정의

Generics 란
다양한 타입의 객체들을 다루는 메서드나 컬랙션 클래스에
컴파일 시의 타입 체크를 해주는 기능이다.

객체의 타입을 컴파일 시에 체크하기 때문에
다음과 같은 이점을 가지게 된다.

  1. 타입안정성을 높여준다.
  2. 형변환의 번거로움을 줄여준다.

용어

class Box <T> {}

  1. Box 원시타입
  2. Box<T> 지네릭 클래스 | T의 Box 혹은 T Box 라고 병음
  3. T 타입 변수 | 타입 매개변수

제한

아래의 경우는 Generics 를 사용할 수 없다.
그 아래 줄에는 자세한 설명이 써져있으니,
이해가 되지 않는다면 참고하도록 하자.

  1. generics static member variables | 지네릭스 타입 정적 맴버 변수
  2. generics array | 지네릭스 타입 배열

generics static member varaibles

원제목 | 지네릭스 타입 정적 매개변수

generics는 특정한 클래스 혹은 매개변수에 넣을 어떤 값(=객체)를
컴파일 시에 결정 지음으로 인해서 타입 안정성유연성 을 확보하는 것이다. 이러한 특징으로 generics는 인스턴스 변수로 참조된다.

그리고 static member variables 는 인스턴스 변수 를 참조할 수 없기 때문에, generics와 공존할 수 없다.

generics array

원제목 | 지네릭스 배열

결론부터 말하면 원칙적으로 불가 하지만 동적 객체 생성 혹은 Object 배열의 지네릭스 배열로 형번환 의 방법을 사용해야 한다.

  1. 원칙적으로 불가능한 이유는 new 연산자 떄문.
    T[] itemArr; // 가능
    T[] itemArr=new T[10]; // 불가능 | new 연산자는 컴파일 시 T 를 알아야 함.

  2. 동적 객체 생성
    Reflection APInewInstance() 참고
    단, Spring 을 공부하고 있거나 Java 의 전반을 공부한 사람에게 추천한다.

  3. Object 배열의 지네릭스 배열로 형변환
    T[] itemArr=(T[]) new Object[10]; // 가능


사용

지네릭스 를 처음 배우고 가장 혼란스러운 점은
다음과 같은 부분을 생각하지 않고 공부하기 때문에 발생한다고 생각한다.

  1. 지네릭스 객체(메서드) 의 필드 설계 파트인가?
  2. 지네릭스 객체(메서드) 의 객체 생성 파트인가?

또한 생각보다 지네릭스와 관련된 내용이 많다는 점이다.
Effective Java 4 | Generics 의 포스트를 작성하며 간단하게 작성했지만,
여기서 그 예를 다시 한 번 볼테니 이를 기준 삼아 이어나가면 편할 것 같다.

  1. 추상적 지네릭스 | T 혹은 E 등
  2. 구체적 지네릭스 | String, Integer, Object, CustomClass(내가 만든 클래스) 등
  3. 제한된 지네릭스 | T extends String, T extends String & Boolean, T extends Object 등
  4. 와일드 카드 | ? extends T, ? super T, ?(? extends Obejct) 등

추상적 지네릭스

원제목 | 타입 변수

타입변수란,
기호의 종류만 다를 뿐, 임의의 참조형 타입 을 의미한다.

  1. ArrayList<T> | T : type 타입
  2. ArrayList<E> | E : element 요소
  3. HashMap<K,V> | K,V : Key, Value 키-값

Essentials of java 와 Effective Java 뿐만 아니라 수많은 곳에서 이러한 타입 변수라는 명칭을 쓰고 있었다. 하지만 나는 이것을 추상적 지네릭스 라고 이해하는 것이 직관적이라고 생각할 뿐이다.

이 시점에서는 어떠한 구체적인 타입을 받아들일지 결정하지 않았다.
아래의 코드 예제를 참고해보자

예제 코드

class Box<T> {
   T item;
   
   T getItem() {
      return item;
   }
   
   void setItem(T item) {
      this.item=item;
   }
}

구체적 지네릭스

원제목 | 실제 타입

위 추상적 지네릭스를 제대로 읽고 이해했다면 바로 이해했을 것이라 생각한다.
말 그대로 어떠한 설계 도면을 기반으로 만든 객체(인스턴스)에 실제 타입을 넣어주는 순간을 의미한다.

바로 아래 코드를 보자

예제 코드

class Box<T> {
   // 추상적 지네릭스 예제 코드와 동일
}
class Main {
   Box<String> box1=new Box<String>(); // jdk 1.5 이후 가능
   Box<String> box2=new Box<>(); // jdk 1.7 이후 가능
}

제한된 지네릭스

원제목 | 제한된 지네릭스

추상적 지네릭스와 구체적 지네릭스 를 제대로 읽었다면 아래를 알게 되었을 것이다.
위의 두 내용이 클래스 작성 + 객체 생성과 닮았다는 점을 말이다.

제한된 지네릭스 는 extends 에 대한 내용이고
이 또한 객체의 상속 닮았다.

지금까지 공부한 내용을 기반으로,
제한된 지네릭스에 대해서 전술한 내용을 살펴보자.

  1. T extends String
  2. T extends String & Boolean
  3. T extends Object

표현식에 추상적 지네릭스가 들어가 있는 것을 보면,
본능적으로 아 이 문법은 클래스 작성 시점에 사용하는 것이구나 라고 알 수 있다.

그리고 상속이라는 본질을 생각해보면,
객체 생성 시점 에 추상적 지네릭스가 구체적 지네릭스로 바뀌게 되는데,
이 때, extends 뒤의 부분 String 혹은 String&Boolean 혹은 Object 등 만을 받아들일 수 있게 하는 구나 라고 알 수 있다.

와일드 카드

원제목 | 와일드 카드

추상적, 구체적, 제한적 지네릭스를 통해서
우리는 지네릭스 클래스, 객체 그리고 그 중간 사이의 무언가에 대한 것을 배웠다.
그러나 이 모든 것들은 한 가지 타입에 대한 것에 국한된 유연성 임을 알 수 있다.

위 문제를 실감하기 위해 다음과 같은 비즈니스 로직이 있다고 해보자.

우리는 2개의 Dto 를 가지고 있다.
그리고 그 Dto 는 모두 objectCode 라는 기본키 필드(멤버 변수) 를 가지고 있다.
이는 SQL 의 object_code SEIRAL PRIMARY KEY 와 대응된다.

아직 JDBC, SQL 을 배우지 않은 사람을 위해 풀어 쓰자면,
Java 와 DB 가 2개의 데이터 모델을 가지고 있고 각 모델은 objectCode 라는 유니크한 고유값을 가지고 있다고 이해하면 될 것같다.

우리는 Java와 DB 를 연결하기 위해 한개의 Dao 를 만들 것인데,
그 Dao 안에는 하나의 출력 메서드가 있는데 objectCode 와 targetTableName 을 넣어주면
targetTableName에 따라 2개의 데이터 모델 중 하나로 연결을 해주고
objectCode에 따라 2개의 데이터 모델 중 하나로 객체 생성을 하고
이를 리턴한다고 쳐보자.

위 내용을 코드로 작성해보면 다음과 같이 될 것이다.
그렇다면 저 <어떠한 지네릭스 문법> 안에 어떠한 내용이 들어가야 할 것이냐 라는 문제점이 생긴다. 왜냐하면 해당 Dao 는 2개의 Dto 에 대한 처리를 다루고 있기 때문이다.

public UtilityDao<어떠한 지네릭스 문법>{
   public void search (int objectCode, String targetTableName) {
     T dto=null;

     if(targetTableName.equals("모델 1의 이름")){
        // 프로세스 : 결과값들을 받음
        dto=new T(...args); // 결과값들을 기반으로 T 클래스를 만듬
     }else if(targetTableName.eqauls("모델 2의 이름")){
        // 프로세스 : 결과값들을 받음
        dto=new T(...args); // 결과값들을 기반으로 T 클래스를 만듬
     }
     return dto;
   }
}

그러한 문제를 해결하기 위해 우리는 아래와 같은 방법들을 사용할 수 있다.

  1. ? extends T | 자식타입 한정 와일드카드 | 지정한 부모 타입의 하위 클래스들만 사용 가능
  2. ? super T | 부모타입 한정 와일드카드 | 지정한 자식 타입의 상위 클래스만 사용
  3. ? (? extends Object) | 비한정 와일드카드 | T extends Object 와 동일

따라서 UserDto 와 PostDto 가 있다고 가정하고 이 둘의 공통 요소인 objectCode 를 가지고 있는 부모 클래스인 ObjectDto 를 만들어서 아래와 같이 코드를 개선할 수 있다.

class UtilityDao<? extends ObjectDto>{
   public void search(int objectCode, String targetTableName) {
      T dto=null;
      if(targetTableName.equals("UserDto")){
         dto=new T(...args);
      }else if(targetTableName.eqauls("PostDto")){
         dto=new T(...args);
      }
      return dto;
   }
}
public Main{
   public static void main(String[] args){
      // 부모 타입 사용가능
      UtilityDao<ObjectDto> utilityDao1=new UtiltiyDao<>();
      // 자식 타입 사용가능(모두)
      UtilityDao<PostDto> utilityDao2=new UtiltiyDao<>();
      UtilityDao<UserDto> utilityDao3=new UtiltiyDao<>();
   }
}

이것으로 이론 기본의 설명은 끝이 났다고 생각한다.
지네릭스를 사용해보고 감을 잡을 수 있는 방법은 다음과 같다.

  1. Collection Framework 사용
  2. Effective Java 공부
  3. 공부 및 경험을 기반으로 한 비즈니스 로직 설계 및 구현

나는 1,2 번을 하면서 3번을 통해 최종적으로 지식 검증을 하는 것이 좋은 것 같다.


활용

활용에서는 다음과 같은 내용을 포함하고 있다.

  1. 지네릭스 타입의 형변환
  2. 지네릭스 타입의 제거

형변환

지네릭스 타입의 형변환에서는 다음과 같은 케이스를 분석하고 있다.

  1. 원시타입과 지네릭스 타입 간의 형변환
  2. 대입된 타입이 다른 지네릭스 타입 간의 형변환
  3. 지네릭스 타입과 와일드 카드 간의 형변환

원시타입과 지네릭스 타입 간의 형변환

Box box=null;
Box<Object> objBox=null;

box=(Box) objBox; // 지네릭스 타입 > 원시타입 | **경고발생**
objBox=(Box<Object> box; // 원시타입 > 지네릭스 타입 | **경고발생**

대입된 타입이 다른 지네릭스 타입 간의 형변환

Box<Object> objBox=null;
Box<String> strBox=null;

objBox=(Box<Object>) strBox; // 타입 매개변수 String > Object | **에러**
strBox=(Box<String>) objBox; // 타입 매개변수 Object > String | **에러**

지네릭스 타입과 와일드 카드 간의 형변환

Box<? extends Object> objBox=null;
Box<String> strBox=null;

objBox=(Box<? exends Object>) strBox; // **가능**
strBox=(Box<String>) objBox; // **가능** | **경고발생** | 확인되지 않은 형변환

제거

컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고
필요한 곳에 형변환을 넣어주며 지네릭 타입을 제거한다.
즉, 컴파일된 파일(*.class) 에는 지네릭 타입에 대한 정보가 없는 것이다.

지네릭 타입의 제거는 다음과 같은 프로세스로 진행된다.

  1. 지네릭 타입의 경계(bound) 제거
  2. 제거 이후 일치하지 않는 타입에 대한 형변환 진행

지네릭 타입의 경계(bound) 제거

  1. <T extends String> 이면 <String> 으로 바뀐다.
  2. <T> 혹은 <T extends Object> 이면 <Object> 으로 바뀐다.

제거 이후 일치하지 않는 파일에 대한 형변환 진행

  1. T get (int i) { return new List().get(i); }
  2. String get (int i) { return (String) new List().get(i); }

본 글은 2021년 12월 13일 에 기록되었다.

지네릭스에 대한 이론적인 공부가 전반되어 있지 않은 상태에서
그 특성 및 사용 예시에 대한 공부를 진행하였기 때문에 결점이 많다고 생각되어
이를 다시 공부하고 재 작성하게 되었다.

Generics(구버전)

클래스 인스턴스화에서, 해당 인스턴스의 자료형 타입을 제한하는 기능
특정한 타입을 매개변수로 받을지 모르는 상황에서,
어떠한 타입을 매개변수로 받더라도 자동으로 형변환하여 에러를 방지해주는 기술
그 과정에서 제한하는 효과가 발생할 뿐이다.

class Numb {
	int i=0;
}
public class Main{
  public static void main(String[] args){
  	Numb<Integer> a=new Numb<Integer>;
  }
}

학습 과정에 사용한 npm propTypes 이랑 약간 비슷한 것같다.

node.js 기반으로 나는 prop-types 를 다음과 같이 사용했는데,
1. 클래스 안에 들어갈 Data Validation 을 하기 위하여
2. 객체 들을 주고받을 때 Data Validation 을 하기 위하여

Generics 는 아예 클레스 및 객체 단위로 사용된다는 점이 특이한 것 같다.

하지만, prop-types 에도 Object 단위로 Validation 하는 기능이 있으니,
결과적으로는 동일한 역할을 수행하는 것으로 대체할 수 있지 않을까 싶다.

Term

용어, Term 정의

class Class<Type>{
  // syntax
}

Class< Type > : Generics Class : 지네릭 클래스
Class : Origin Type : 원시타입
Type : Type Variables or Type Parameter : 타입 파라미터

Polymorphism

다형성, PolymorphismClass 에서 처음 언급된 용어이다.
Generics 는 기본적으로 객체 참조형인 Integer String Bool 등으로 사용하지만,
ClassInterface 도 사용할 수 있다.

Restrictions

static

관련 문서를 보면 알겠지만,
static 은 전역변수 키워드로 한 번 생성되면 메모리 종료 시까지 존재하게 된다.

이러한 특징 때문에,
클래스나 객체참조 시 static 을 붙이게 되면 해당 클래스의 모든 속성값이 동일한 공간에 존재하게 되고 구분이 불가능해 진다. 그러한 원리로 모든 객체가 같은 속성값을 공유하게 된다.

그런데, Generics 를 통해서 Type Parameter 를 설정하게 되면,
객체들이 각각 다른 값을 가지게 되는 모순이 생기게 된다.

Hello<AA> A=new Hello<AA>();
Hello<BB> B=new Hello<BB>();
A.props;
B.props;

A를 Type Parameter로 하는 Hello.props
B를 Type Parameter로 하는 Hello.props 가 같을 수 없기 때문이다.

genercis(type) array

Generics Array 는 기본적으로 생성이 불가능 하다.

class Test<A> {
  A[] Array; // Ok
  A[] Array2=new A[Array.length]; // Error
  A[] Array3=new A[3]; // Error
}

관련 문서를 보면 알겠지만,
new 연산자는 컴파일 시점에 A 가 뭔지 정확히 알아야 한다.

그런데,
Test< A > 를 컴파일 하는 시점에는 A 가 뭔지 예상할 수 없기 때문이다.

물론 생성하는 방법도 있다

  • Reflection API 의 newInstance()와 같이 동적 인스턴스 생성 도구를 이용
  • Object 배열로 생성해서 복사한 후 T[]로 형변환

Limited Generics Class

제한된 지네릭, Limited Generics Class
객체 생성 당시의 Generics 를 미리 지정한 Type 만 사용할 수 있게 제한하는 것이다.

1. Fruits 으로만 제한 시에는 extends
2. Eatable 으로만 제한 시에도 extends (not Implement)
3. 둘 다 받아들이는 것으로 제한 시에는 그 사이에 & 연산자 사용

interface Eatable { ... }
class Fruits { ... }
class Apple extends Fruits{ ... }
class Grape extends Fruits{ ... }
class FruitsBox<T extends Fruits & Eatable> { ... }
class FruitsOnly<T extends Fruits> {... }
class EatBax<T extends Eatable>{ ... }

아래와 같이 제한된다.

FruitsBox.add(new Apple()); //ok
FruitsBox.add(new Grape()); // ok
FruitsBos.add(new Toy()); // 에러

WildCard

사용해야 하는 순간

재료를 넣으면 해당 재료로 만들어진 쥬스를 return 하는 클래스를 만들기로 했다.

class Juicer {
  static Juice makeJuicer(FruitBox<T> box){
    sTirng tmp=""; // 임시 변수 생성
    for(Fuirt f:box.getList()) tmp +=f+" "; // 형변환
    return new Juice(tmp); // 생성된 쥬스를 반환
  }
}
  • 문제점
  1. Juicer 클래스가 Generics 클래스가 아니다.
  2. Juicer 클래스가 Generics 클래스여도, static 키워드는 Type Parameter 로 T를 가질 수 없고 구체적인 Type을 지정해주어야 한다.
class Juicer {
  static Juice makeJuicer(FruitBox<Fruit> box){
    sTirng tmp=""; // 임시 변수 생성
    for(Fuirt f:box.getList()) tmp +=f+" "; // 형변환
    return new Juice(tmp); // 생성된 쥬스를 반환
  }
}
  • 문제점
  1. 위와 같이 작성하면, 새로운 Type Parameter 를 받아들이기 위해 overlaoding 해야한다.
    -> 그런데, Type Parameter 가 다른 것으로는 overloading 조건이 불충족된다.
    -> 메서드 중복 정의 에러가 발생된다.
  • 해결방안
    WildCard 사용

사용 예시

가능
1. < ? extends T > T와 그 자녀들만 가능
2. < ? super T > T와 그 조상들만 가능
3. < ? >, < ? extends Objects > 와 동일

불가
1. < ? extends T & E>

Generics Class

지네릭 타입이 설정된 클래스

Genercis Method

지네릭 타입이 설정된 메서드

Generics Casting

Generics Type Remove

profile
2022년 12월 9일 부터 노션 페이지에서 작성을 이어가고 있습니다.

0개의 댓글