[자바] 제네릭스

ChoRong0824·2023년 7월 26일
0

Java

목록 보기
30/31
post-thumbnail

제네릭스,열거형,애노테이션을 공부하면서 내가 모르거나, 의아하거나(애매), 틀리게 안 것들 위주로 작성해봤습니다.
필자의 공부를 위해 작성한 글임을 양해바랍니다.

제네릭스 Generics

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크(compile-time type check)를 해주는 기능이다.
객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형 변환의 번거로움이 줄어들게 되었다.

여기서 개발자라면, 의문이 들어야한다.
필자는, "어떻게 ?" 라는 생각이 들게 되었다. "타입의 안전성을 높인다는게 뭔 소리야?" 라고 생각했다.

  • 이는 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 꺼내오는 경우를 줄여준다는 것이다.

ArrayList 는 다양한 종류의 객체를 담을 수 있는 장점이 있다.
그러나 사실상 사용해보면, 보통 한 종류의 객체를 담는 경우가 더 많다.
또한, 꺼낼 때마다 타입체크하면서 형변환 하는것은 매우매우매우매우 번거롭다.
이는 제네릭스를 활용하면 해결할 수 있다.

  • 어떻게 ?
    --> 다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다는 것이다.

🔥 거두절미하고, 코드로 알아보자

class unit{
	Object item;
    void setItem(Object item){
    this.item = item;
    }
    Object getItem(){
    return item;
    }
}
위 코드를 보면 그냥 간단한 클래스다.
지역변수에 게더세터 써서 변수에 설정 및 불러오는 느낌이라고 보면 된다.
=========
class unit<T>{
	T item;
    void setItem(T item){this.item = item;}
    T getItem(){return item;}
}

사실 필자도 공부하고 있는 입장이다.
차이점을 알겠는가? --> 몰라야 정상이다. 위만 보고는 필자도 차이점이 없다고 생각한다.
그러나, 형변환을 생각하면 다르다. 이는 코드로 알아보겠다.

unit<String> Leeshin = new unit<String>();
Leeshin.setItem(new Object());
Leeshin.setItem("블클");
String item = (String) Leeshin.getItem();

이 코드는 어떻게 될 것 같나요 ?

unit<String> Leeshin = new unit<String>();
Leeshin.setItem(new Object()); // ERR --> String 외엔 불가. 위에 타입을 String으로 생성했기 때문
Leeshin.setItem("블클"); // 정상적
String item = (String) Leeshin.getItem(); // 형변환이 필요없음.
// 왜 필요 없을까요 ? --> 이미 컴파일러가 제네릭을 통해 getItem() 메소드에서 
반환되는 객체가 String이라고 인식하기 때문에 String 타입으로 받아올 수 있게 된다.

참고로, 제네릭스를 사용할 때에는 반드시 타입을 지정해줘야합니다.


🔥 제네릭스의 제한

unit클래스의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절한 것이다.
제네릭스는 객체별로 다르게 동작하려고 만들었기 때문이다.

unit<jungle> jg = new unit<jungle>();
unit<mid> ap = new unit<mid>();

여기서 중요하게 짚고 넘어가야할 것이 있다.

  • 모든 객체에 대해 동일하게 동작해야하는 static멤버에 T(타입변수)를 사용할 수는 없다.
    --> T는 객체변수로 인지되기 때문이다.
    --> 왜냐하면, static멤버는 객체변수를 참조할 수 없다.

만약, 제네릭스 배열을 생성해야할 필요가 있을 떄는, new 연산자 대신 'newInstance()'와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object 배열을 생성해서 복사한 다음 T[]로 형변환 해야한다.

  • 코드예시
class Unit<T>{
	T[] item;
    T[] toArray(){
    	T[] tmpArr = new T[item.length]; // ERR. 제네릭 배열 생성 불가.
        //  T[] tmpArr = (T[]) java.lang.reflect.Array.newInstance(item.getClass().getComponentType(), item.length); 
        // 위와같이 만들어주면된다.
        return tmpArr;
    }
  • jdk 1.7 부터는 추정이 가능한 경우 타입을 생략할 수 있다.
    참조변수의 타입으로부터 unit이 jungle타입의 객체만 저장한다는 것을 알 수 있기 떄문에, 생성자에 반복해서 타입을 지정해주지 않아도 된다는 것이다.
    이는, 코드로 알아보자.
unit<jungle> Leeshin = new unit<jungle>();
unit<jungle> Leeshin = new unit<>(); // 둘 다 동일한코드
  • 만약, 타입 T가 lol인경우, 다형성에 의거하여 lol의 자손들은 이 메서드의 매개변수가 될 수 있다. --> mid가 lol의 자손으로 가정하는 경우
unit<lol> Leeshin = new unit<lol>();
Leeshin.add(new lol()); 
Leeshin.add(new jungle());

🔥 제한된 제네릭 클래스

import java.util.ArrayList;

class lol {
    public String toString(){
        return "lol";
    }
}
class mid extends lol {
    public String toString(){
        return "mid";
    }
}
class top extends lol {
    public String toString(){
        return "top";
    }
}
class jungle {
    public String toString(){
        return "jungle";
    }
}
class unit extends jungle {
    public String toString(){
        return "unit";
    }
}
interface usingAble{
    
}

public class Ex1 {
    public static void main(String[] args) {
        Box<lol> lolBox = new Box<lol>();
        Box<mid> midBox = new Box<mid>();
        Box<unit> unitBox = new Box<unit>();

        lolBox.add(new lol());
        lolBox.add(new mid());
        // midBox.add(new Fruit()); ERR
        midBox.add(new mid());
        // midBox.add(new unit()); ERR
        unitBox.add(new unit());
        System.out.println(lolBox);
        System.out.println(midBox);
        System.out.println(unitBox);
    }

    class LolBox<T extends lol & usingAble> extends Box<T> {
        
    }
    static class Box<T>{
        ArrayList<T> list = new ArrayList<>();
        void add(T item){
            list.add(item);
        }
        T get(int i){
            return list.get(i);
        }
        int size(){
            return list.size();
        }
        public String toString(){
            return list.toString();
        }
    }
}


🔥 제네릭타입의 오버로딩

  • 오버로딩 : 같은 이름의 메서드를 인자의 타입이나 개수를 다르게 정의하여 사용한다.
    그럼, 제네릭스에서는 ? --> 제네릭 타입은 컴파일 시에 타입이 결정된다. --> 따라서, 인자의 타입이 다르더라도 제네릭 타입 자체가 다른 메서드로 인식되지 않는다.

🤜 코드로 알아보자

public class Ex{
	public static <T> void printValue(T value){
    sout(value);
    }
    public static <E> void printValue(E value){
    sout(value);
	}
}

로 오버로딩을 구현한다면, 각 메서드는 T,E 라는 제네릭 타입을 사용한다.
But, 이러한 경우, 컴파일러는 이 두 메서드를 동일하게 간주하여 오버로딩으로 처리하지 않게 된다. --> 따라서, 제네릭 타입이 다른 것만으로는 오버로딩이 성립되지 않는다.
그렇다면, 오버로딩을 하기위해서 어떻게 해야할까?
--> 다른 파라미터 유형이나 파라미터 개수가 필요하다.

🤜 코드로 알아보자

public class Ex{
	public static <T> void printValue1(T value){
    sout(value);
    }
    public static <E> void printValue2(E value){
    sout(value);
	}
}

다시 한 번 강조하지만, 제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않는다.

🔥 제네릭 와일드카드

제네릭스(Generics)를 사용할 때 매우 유용한 기능입니다.
제네릭스는 클래스나 메서드를 작성할 때 타입을 파라미터화하여 재사용성과 타입 안정성을 높이는 기능을 제공한다.

스프링 프레임워크는 제네릭을 널리 사용하며, 제네릭 와일드카드를 통해 코드의 재사용성과 유연성을 높일 수 있습니다.

🤜 제네릭 와일드카드를 사용하는 주요 상황

  1. 스프링 빈 주입 (Dependency Injection)
    스프링의 DI 기능은 컨테이너에서 빈(Bean)을 관리하고,
    이를 의존성 주입을 통해 필요한 클래스에 주입하는 기능이다.
    제네릭 와일드카드를 사용하면 빈의 타입을 명시적으로 지정하지 않고,
    빈을 더 유연하게 주입할 수 있습니다.

  2. 제네릭 타입의 메서드 호출
    스프링 프레임워크에서는 많은 유틸리티 메서드나 라이브러리 메서드들이 제네릭 타입을 사용합니다.
    이러한 메서드들을 호출할 때 제네릭 와일드카드를 이용하여 타입 안정성을 유지하면서 다양한 타입을 처리할 수 있습니다.

  3. 컬렉션 타입 다루기
    스프링에서는 컬렉션 타입(List, Set, Map 등)을 다루는 경우가 많습니다. 제네릭 와일드카드를 사용하여 다양한 타입의 컬렉션을 처리하면서 타입 안정성을 유지할 수 있습니다.

  • 예를 들어,
    스프링에서 제공하는 Autowired 어노테이션은 제네릭 와일드카드를 이용하여 의존성을 주입할 수 있습니다.
    이렇게 함으로써 특정 타입이 아닌 여러 타입의 빈을 주입받을 수 있습니다.
@Autowired
private List<? extends Animal> animals;

위 예제에서 animals 필드는 Animal 클래스 또는 Animal을 상속하는 클래스들의 리스트를 주입받을 수 있습니다.

제네릭 와일드카드를 사용하면 타입 안정성을 유지하면서 유연한 코드를 작성할 수 있기 때문에, 스프링 프로젝트에서 많은 상황에서 유용하게 활용될 수 있습니다

그러나 모든 상황에서 제네릭 와일드카드를 사용해야 하는 것은 아닙니다.
때때로 명시적인 타입 지정이 필요한 경우도 있습니다. 제네릭 와일드카드는 유연성을 높이지만, 컴파일 시 타입 체크가 덜 엄격해질 수 있으므로 주의가 필요합니다.

🤜 와일드 카드의 표현 ' ? '

크게 3가지 형태로 나뉜다.

  1. <?> : 제한 없음. 모든 타입이 가능. <? extends Object>와 동일
    --> 모든 타입을 대표하는 와일드카드입니다.
    예시: List<?>는 List의 원소 타입에 상관없이 어떤 타입의 List도 처리할 수 있습니다.

  2. <? extends T> : 와일드 카드의 상한 제한. T와 그 자손들만 가능
    --> T 타입 또는 T의 하위 클래스들을 대표하는 와일드카드입니다.
    예시: List<? extends Number>는 Number 클래스 또는 Number를 상속하는 하위 클래스들로 제한된 List를 처리할 수 있습니다.
    --> 쉽게 말해, 모든 종류의 List가 매개변수로 가능합니다.

  3. <? super T> : 와일드 카드의 하한 제한. T와 그 조상들만 가능
    --> T 타입 또는 T의 상위 클래스들을 대표하는 와일드카드입니다.
    예시: List<? super Integer> 는 Integer 클래스 또는 Integer를 상속하는 상위 클래스들로 제한된 List를 처리할 수 있습니다.

🔥 제네릭 타입의 다형성

  1. 참조 변수와 생성자의 대입된 타입은 일치해야 한다.
class Product{}
class Tv extends Porduct{}
ArrayList<Tv> 	   list = new ArrayList<Tv>(); // 일치
ArrayList<Product> list = new ArrayList<Tv>(); // 불일치
  1. 제네릭 클래스간의 다형성은 성립(여전히 대입된 타입은 일치)해야 한다.
List<Tv> 		list = new ArrayList<Tv>(); // 일치
List<Product>	list = new LinkedList<Tv>(); // 일치
  1. 매개변수의 다형성도 성립한다.
ArrayList<Product> list = new ArrayList<Product>();
	list.add(new Product());
    list.add(new Tv());		// Product의 자손도 가능하다.
    list.add(new Audio()); // class Audio extends Product{}

🔥 제네릭 메서드

메서드의 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라고 한다.
제네릭 타입의 선언 위치는 반환타입 바로 앞이다.
제네릭 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 전혀 다른 것이다. --> 즉, 같은 타입의 문자 T를 사용해도 같은 것이 아니다는 것이다.

🤜 제네릭 클래스

클래스 정의 시점에 타입 매개변수를 선언하여 재사용하기 쉬운 클래스를 만들 수 있다.
제네릭 클래스는 클래스 이름 다음에 꺾쇠괄호(<>) 안에 타입 매개변수를 넣어서 선언한다.

public class GenericClass<T> {
    private T data;
    public void setData(T data) {
        this.data = data;
    }
    public T getData() {
        return data;
    }
}

🤜 제네릭 메서드

메서드 선언부에 타입 매개변수를 가진 메서드를 제네릭 메서드라고 한다.
제네릭 메서드는 반환 타입 앞에 꺾쇠괄호(<>) 안에 타입 매개변수를 넣어서 선언한다.

public class GenericExample {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }
}

제네릭 클래스와 제네릭 메서드는 각기 다른 문맥에서 사용되며, 같은 타입 매개변수(T)를 사용해도 각각은 별개의 의미를 갖는다.
제네릭 클래스와 메서드는 매개변수 타입을 정하지 않고 다양한 데이터 타입에서 작동하여 재사용성과 유연성을 향상시키는데 도움이 되기때문에 사용한다.
그렇다면 필자는 의문점이 생겼다. 제네릭 메서드와 클래스 동시엔 사용 불가능한가 ?
--> 결론만 말하자면 Yes 다. 아래에서 코드로 알아보자.

🤜 메서드와 클래스 동시 사용

public class GenericClass<T> {
    private T data;

    public void setData(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public <U> void printData(U data) {
        System.out.println("Data: " + data);
    }

    public static void main(String[] args) {
        GenericClass<Integer> genericInt = new GenericClass<>();
        genericInt.setData(10);
        genericInt.printData("Hello, World!"); // 여기에서 U는 String 타입입니다.
        genericInt.printData(12345); // 여기에서 U는 Integer 타입입니다.
    }
}

이 클래스와 메서드의 타입 매개변수(T와 U)는 서로 다른 역할을 수행하며 각기 독립적이다.
그럼, STATIC멤버에는 타입 매개변수를 사용할 수 없지만, 메서드에 제네릭 타입을 선언하고 사용하는 것은 가능하다는데. 이건 무슨 말일까 ?

  • 메서드에 선언된 제네릭 타입은 지역변수를 선언한 것과 같은 것이다. 라고 생각하면 쉽다. 즉, 이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 메서드가 static이건 아니건 상관없다는 말이다.

참고로,
같은 이유로 내부 클래스에 선언된 타입 문자가 외부 클래스의 타입 문자와 같아도 구별될 수 있다는 것을 의미한다,

🤜 되새김질 코드

static <T> void sort (List<T> list, Comparator< ? super T> c){}
/*
- List<T> list: 타입 매개변수 T를 가진 리스트 객체입니다. 
이 매개변수는 정렬될 대상 리스트입니다.
- Comparator<? super T> c: Comparator 인터페이스의 구현체로 
제네릭 타입이 T의 상위 타입이거나 T 자체인 것을 받아들입니다. 
해당 Comparator 구현체는 리스트 내에 T 타입 요소들을 비교하여 정렬하는 데 사용됩니다.

- 메서드는 주어진 Comparator를 사용하여 리스트 내 요소를 정렬하며
결과적으로 매개변수로 전달된 리스트 객체를 정렬될 예정입니다
(즉, 정렬 후에도 리스트는 동일한 객체를 참조합니다). 
? super T 표현식을 사용하면 메서드 사용 시, 
T의 상위 클래스도 받아들일 수 있게 됩니다. 이렇게 함으로써 유연성을 높여, 
다양한 Comparator 구현체를 사용할 수 있습니다. 
예를 들어, Number 클래스의 하위 클래스인 Integer와 Double이 있을 때,
Comparator<Number>를 사용해 둘 다 정렬할 수 있게 되는 것입니다.
*/

🤜 복잡한 제네릭 메서드

//매개변수로 지정한 List<T>를 정렬하는 코드
public static <T extends Comparable<? super T>> void sort(List<T> list)
public static <T extends Comparable<T>> void sort(List<T> list)
  • 위 코드는 같은 것일까? --> YES.
    -> List<T> : 타입 T를 요소로 하는 List를 매개변수로 허용
    -> <T extends Comparable<? super T>> : ' T '는 Comparable을 구현한 클래스이어야 하며, (<T extends Comparable>) 이다.

즉, List<T> 의 요소가 Comparable인터페이스를 구현한 것이어야 한다는 것이다. 참고로, 인터페이스라고 해서 implements 라고 쓰지 않는다.

🔥 제네릭 타입의 형변환

  1. 제네릭타입과 넌제네릭 타입간의 형변환은 항상 가능하다.
    But, 경고가 발생할 뿐이다.
  2. 대입된 타입이 다른 제네릭 타입 간에는 형변환이 불가능하다.
    Object라도 불가능하다.
profile
컴퓨터공학과에 재학중이며, 백엔드를 지향하고 있습니다. 많이 부족하지만 열심히 노력해서 실력을 갈고 닦겠습니다. 부족하고 틀린 부분이 있을 수도 있지만 이쁘게 봐주시면 감사하겠습니다. 틀린 부분은 댓글 남겨주시면 제가 따로 학습 및 자료를 찾아봐서 제 것으로 만들도록 하겠습니다. 귀중한 시간 방문해주셔서 감사합니다.

0개의 댓글