본문 바로가기

배운내용 정리

Java Generic

1. Generic 알아보기

2. Generic 문법 자세히 살펴보기

 

 

1. Generic 알아보기

제네릭의 효용

  • 첫 번째로 제네릭의 효용은 타입 언어에서 “중복되거나 필요 없는 코드를 줄여주는 것”이다.
  • 두 번째 제네릭의 효용은 그러면서도 타입 안정성을 해치지 않는 것이다
  • Step 1. 타입 언어에서의 중복되거나 필요 없는 코드?
    • 만약 우리가 자바스크립트나 파이썬과 같은 약타입 언어를 이용한다면 일은 간단하다.
    • 기본적으로 타입을 지정해 줄 필요가 없기에 하나의 함수만 구현하면 끝.
// JavaScript
function plusReturnFunction(a, b) {
    return a + b;
}

const a = 1;
const b = 2;
const c = 1.1;
const d = "hello";

plusReturnFunction(a + b); // 3
plusReturnFunction(a + c); // 2.1
plusReturnFunction(a + d); // 1hello
  • 하지만 우리는 타입을 지정해줘야 하는 언어를 사용한다.
  • 그래서 똑같은 로직을 수행하는 함수를 타입을 지정해야 한다는 이유로 세 차례나 구현해야 한다.
public class Generic {
    public String plusReturnFunction(int a, int b) { ... }

    public String plusReturnFunction(int a, long b) { ... }

    public String plusReturnFunction(int a, String b) { ... }
}
  • 타입의 안정성을 해치지 않으면서 로직을 수행할 수 있는 함수를 생각하면 이런 식 일 것이다
public class Generic {
    public Object plusReturnFunction(Object a,Object b) { ... }
}
  • 자바의 “거의 모든 것”은 객체이고, 객체라는 것은 Object 클래스를 상속한다.
  • Object를 상속받기 때문에 위의 코드와 같이 작성을 한다면, (Wrapper 객체에 대한 설명이 조금은 필요하겠지만) 실제로 여러분은 타입과 상관없이 메서드 안에 두 파라미터를 전달하는 것은 가능할 것이다.
  • 하지만 이런 경우 타입 안정성이 침해받게 된다.
  • 직접 메서드를 위와 같이 구현했기 때문에, 지금은 아주 작아 보이는 { … } 블록 안에 모든 경우의 수를 대비해야 한다.
  • 또한 우리가 구현한 메서드 내부는 아직도 타입에 지배받고 있다.
  • 결론적으로는 다음과 같은 문제들이 발생.
    • 예를 들어 a 객체와 b 객체에 단항 연산자를 사용할 수 있을까?
    • 또는 두 타입이 다르다면 연산자를 사용하기 위해 같은 타입으로 맞춰줘야 하는데, 어떠한 타입으로 맞출 수 있을까? int? long?
    • 마지막으로 순서는 어떻게 할까? int long을 형변환 해서 처리하는 로직의 코드를 작성했는데, long과 int로 들어오면 어떻게 할까?
  • 타입의 논리로 동작하는 세상에서, 타입 안정성을 침해하는 행위를 했다. 그러한 댓가로 우리는 잘 피해간 것 처럼 보였지만, 형변환과 같은 부수적인 코드는 오히려 늘어났다.

제네릭 문법 살펴보기

// 1.
public class Generic<T> {
		// 2.
    private T t;
    // 3.
    public T get() {
        return this.t;
    }

    public void set(T t) {
        this.t = t;
    }

    public static void main(String[] args) {
				// 4.
        Generic<String> stringGeneric = new Generic<>();
				// 5.
        stringGeneric.set("Hello World");
				
        String tValueTurnOutWithString = stringGeneric.get();

        System.out.println(tValueTurnOutWithString);
    }
}
  1. 제네릭은 클래스 또는 메서드에 사용 할 수 있습니다. 클래스 이름 뒤에 <> 문법 안에 들어가야 할 타입 변수를 지정한다. 대부분 T, U, V, E 순으로 사용
  2. 선언해둔 타입 변수는 해당 클래스 내에서 특정한 타입이 들어갈 자리에 대신 들어갈 수 있다. 
  3. 메서드의 리턴타입에 들어가는 것 역시 마찬가지이다.
  4. 여기부터는 제네릭을 통해 구현한 클래스를 사용하는 부분입니다, 클래스에 선언했기 때문에 인스턴스를 만들기 위해서 타입변수에 들어갈 실제 변수의 값을 넣어줘야 한다. 여기서는 String이다.
  5. 아까 타입변수로 대체해뒀던 곳에 String이 들어가 있기 때문에, 이와 같이 사용할 수 있다.

2. Generic 문법 자세히 살펴보기

제네릭 용어

public class Generic<T> { ... }

Generic<String> stringGeneric = new Generic<>();
  • Generic <T>의 클래스처럼, 제네릭을 사용한 클래스를 제네릭 클래스라고 한다.
  • 제네릭에서 <> 사이에 들어가는 변수명 T는 타입 변수라고 한다.
  • Generic 클래스를 원시 타입이라고 한다.

제네릭 제한

1. 객체의 static 멤버에 사용할 수 없다.

static T get() { ... } // 에러

static void set(T t) { ... } // 에러

 

  • 타입 변수는 인스턴스 변수로 간주되고, 모든 객체에 동일하게 동작해야 하는 static 필드 특성상 사용할 수 없다.

2. 제네릭 배열을 생성할 수 없습니다.

 

제네릭의 문법

1. 다수의 타입변수를 사용할 수 있습니다.

public class Generic<T, U, E> {
    public E multiTypeMethod(T t, U u) { ... }
}


Generic<Long, Integer, String> instance = new Generic();
instance.multiTypeMethod(longVal, intVal);

2. 다형성 즉 상속과 타입의 관계는 그대로 적용

  • 대표적으로 부모 클래스로 제네릭 타입변수를 지정하고, 그 안에 자식클래스를 넘기는 것은 잘 동작한다.

3. 와일드카드를 통해 제네릭의 제한을 구체적으로 정할 수 있다.

public class ParkingLot<T extends Car> { ... }

ParkingLot<BMW> bmwParkingLot = new ParkingLot();
ParkingLot<Iphone> iphoneParkingLot = new ParkingLog(); // error!
  1. <? extends T> : T와 그 자손들만 사용 가능
  2. <? super T> : T와 그 조상들만 가능
  3. <?> : 제한 없음

이렇게 제한을 하는 이유는 다형성 때문. 위의 코드에서, T는 Car의 자손클래스들이라고 정의했기 때문에, 해당 클래스 내부에서 최소 Car 객체에 멤버를 접근하는 코드를 적을 수 있다. 반대로 그러한 코드들이 있을 여지가 있기 때문에, Car 객체의 자손이 아닌 클래스는 제한하는 것이다.

 

4. 메서드를 스코프로 제네릭을 별도로 선언할 수 있다.

static <T> void sort(List<T> list, Comparator<? super T> c) { ... }
  1. 이렇게 반환 타입 앞에 <> 제네릭을 사용한 경우, 해당 메서드에만 적용되는 제네릭 타입변수를 선언 할 수 있다.
  2. 타입변수를 클래스 내부의 인스턴스 변수 취급하기 때문에 제네릭 클래스의 타입변수를 static 메서드에는 사용할 수 없었지만, 제네릭 메서드의 제네릭 타입 변수는 해당 메소드에만 적용되기 때문에 메소드 하나를 기준으로 선언하고 사용 할 수 있다.
  3. 같은 이름의 변수를 사용했다고 해도 제네릭 메소드의 타입변수는 제네릭 클래스의 타입변수와 다르다.
public class Generic<T, U, E> {
		// Generic<T,U,E> 의 T와 아래의 T는 이름만 같을뿐 다른 변수
    static <T> void sort(List<T> list, Comparator<? super T> c) { ... }
}

 

오늘 자바 제네릭을 공부하며 제네릭의 효용과 문법 여러 제한사항과 어떤 상황에서 사용하는지에 알아봤다. 자바를 공부하면서 제네릭 같은 문법하나 있으면 좋겠다고 했지만. 막상 사용하려고 보니 여러 제한 조건과 문법이 내 발목을 잡을 것 같다ㅎㅎ... 그래도 자주 사용할 문법이니 눈에 불이 나게 익혀보자.

 

'배운내용 정리' 카테고리의 다른 글

Java Thread(2)  (0) 2023.10.27
Java Thread(1)  (0) 2023.10.24
Java 예외처리  (1) 2023.10.23
자바의 구조체  (0) 2023.10.18
객체지향(3)  (1) 2023.10.17