[Java] Wrapper클래스, Generics(지네릭스)

킹발·2022년 9월 30일
0

Java

목록 보기
11/12
post-thumbnail

래퍼 클래스

객체지향 개념에서 모든 것은 객체로 다루어져야 한다.
때로는 기본형 변수도 어쩔 수 없이 객체로 다뤄야 하는 경우가 있다.
래퍼 클래스들을 이용하면 기본형 값을 객체로 다룰 수 있다.

  • 기본형 값들을 객체로 변환하여 작업을 수행하는 경우
    • 매개변수를 객체를 요구할 때
    • 기본형 값이 아닌 객체로 저장해야할 때
    • 객체간의 비교가 필요할 때

기본형래퍼클래스생성자활용예
booleanBooleanBoolean(boolean value)
Boolean(String s)
Boolean b1 = new Boolean(true);
Boolean b2 = new Boolean("true");
charCharacter
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble

문자열을 숫자로 변환하기

int i1 = new Integer("100").intValue();
Integer i3 = new Integer("100");

//많이 사용하는 방법
int i2 = Integer.parseInt("100");
Integer i4 = Integer.valueOf("100");

Generics(지네릭스)

정의

클래스 내부에서 사용할 데이터 타입을 외부에서 지정해주는 기법

지네릭 클래스 선언

// 일반적인 클래스
class Box {
  Object item;
  
  void setItem(Object item) {this.item = item;}
  Object getItem() {return item;}
}
 
// 제네릭 클래스
class Box<T> {
  T item;
  
  void setItem(T item) {this.item = item;}
  T getItem() {return item;}
}

여기에서 T는 임의의 변수이고 일종의 매개변수 역할과도 비슷하다. T 자리에는 어떠한 참조형 타입이 들어간다는 뜻이다.

지네릭스 사용 전

예제에서 사용할 클래스

class Apple {
    @Override
    public String toString() {
        return "Apple";
    }
}

class Orange {
    @Override
    public String toString() {
        return "Orange";
    }
}

class Box {
    private Object ob;

    public void set(Object o) {
        ob = o;
    }
    public Object get() {
        return ob;
    }
}

main()

원래 의도대로 잘 작성한 코드

public class GenericsEx {
    public static void main(String[] args) {
        Box aBox = new Box();
        Box oBox = new Box();

        aBox.set(new Apple());
        oBox.set(new Orange());
        
        Apple ap = (Apple) aBox.get();
        Orange og = (Orange) oBox.get();

        System.out.println(ap);
        System.out.println(og);
    }
}

문제1 - 런타임에러

  • set() 메서드는 파라미터 타입이 Object이므로 String도 받을 수 있다.
  • 협업하는 과정에서 의도와 다르게 메서드를 호출해서 사용한 결과 런타임 에러가 발생할 수 있다는 상황을 가정해 아래와 같이 코드를 작성 해본다.
public class GenericsEx {
    public static void main(String[] args) {
        Box aBox = new Box();
        Box oBox = new Box();
        
        // 개발자의 실수
        aBox.set("Apple");
        oBox.set("Orange");

        Apple ap = (Apple) aBox.get();
        Orange og = (Orange) oBox.get();

        System.out.println(ap);
        System.out.println(og);
    }
}
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class date220930.Apple (java.lang.String is in module java.base of loader 'bootstrap'; date220930.Apple is in unnamed module of loader 'app')
	at date220930.GenericsEx.main(GenericsEx.java:35)

런타임 에러야 어찌되었든 에러라도 뜨니까 어디에서 문제가 일어났는지 알아차릴 수 있다.
더 큰 문제는 다음 상황을 가정해보자.

문제2 - 의도와 다르게 실행

런타임 에러조차 뜨지 않고 정상적으로 코드가 실행은 되나 의도대로 전혀 프로그램이 작동하지 않을 수 있다.

public class GenericsEx {
    public static void main(String[] args) {
        Box aBox = new Box();
        Box oBox = new Box();
        
        // 개발자의 실수
        aBox.set("Apple");
        oBox.set("Orange");

        System.out.println(aBox.get());
        System.out.println(oBox.get());
    }
}
Apple
Orange

두 가지의 문제점으로 인해 Generics라는 새로운 문법을 1.5 버전 이상부터 도입하여 문제를 해결하는 방법이 있다.


지네릭스 사용 후

지네릭스 적용 클래스 정의

class Box<T> {
    private T ob;

    public void set(T o) {
        ob = o;
    }
    public Object get() {
        return ob;
    }
}

main()

지네릭스를 도입한 결과

public class GenericsEx {
    public static void main(String[] args) {
        Box<Apple> aBox = new Box<Apple>();
        Box<Orange> oBox = new Box<Orange>();
        
        Apple ap = aBox.get();  // 타입 변환이 불필요.
        Orange og = oBox.get(); // 타입 변환이 불필요.

        System.out.println(ap);
        System.out.println(og);
    }
}

문제 상황 재현

// 개발자의 실수
aBox.set("Apple");
oBox.set("Orange");
  • set() 메서드는 Apple 타입 객체만 받을 수 있으므로 컴파일 에러를 발생시킨다.

지네릭스의 장점

  1. 타입 안정성을 제공한다.
  2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.
  3. 런타임 에러를 컴파일 에러로 끌어올 수 있다.

제한된 지네릭 클래스

왜 필요할까?

  • 타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만, 그래도 여전히 모든 종류의 타입을 지정할 수 있다는 것에는 변함이 없다.
import java.util.ArrayList;

class Fruit { }
class Apple extends Fruit{ }
class Orange extends Fruit{ }

class Animal { }
class Cat extends Animal { }
class Dog extends Animal { }

class FruitBox<F> {
    ArrayList<F> list = new ArrayList<F>();

    void add(F item) {
        list.add(item);
    }
}
public class GenericsEx {
    public static void main(String[] args) {
        FruitBox<Fruit> fruitBox= new FruitBox<Fruit>();
        
        fruitBox.add(new Apple());
        fruitBox.add(new Orange());

//        fruitBox.add(new Cat()); //컴파일 에러
//        fruitBox.add(new Dog()); //컴파일 에러
        
        FruitBox<Animal> animalBox = new FruitBox<>();
        
    }
}

다음과 같이 fruitBox 객체에는 제네릭을 활용하였기 때문에 다형성을 이용해서 Fruit를 상속 받은 AppleOrange를 리스트에 넣을 수 있다.

그런데 FruitBox클래스를 이용해서 animalBox를 만들어도 아무런 오류는 발생하지 않는다.

만약에 FruitBox클래스에서 Fruit에만 있는 메서드를 호출하는 행위를 하게 된다면 오류가 나게 되므로 제한된 제네릭 클래스가 필요하다.

제한된 제네릭 클래스 적용

//---생략
class FruitBox<F extends Fruit> {
    ArrayList<F> list = new ArrayList<F>();

    void add(F item) {
        list.add(item);
    }
}
public class GenericsEx {
    public static void main(String[] args) {
        FruitBox<Fruit> fruitBox= new FruitBox<Fruit>();
        
        fruitBox.add(new Apple());
        fruitBox.add(new Orange());

//        fruitBox.add(new Cat()); //컴파일 에러
//        fruitBox.add(new Dog()); //컴파일 에러
        
//        FruitBox<Animal> animalBox = new FruitBox<>(); //컴파일 에러
        
    }
}

적용한 결과 FruitBox 인스턴스를 생성할 때는 타입변수에는 Fruit를 상속받은 타입만 올 수 있다.
만약 Fruit가 클래스가 아닌 인터페이스여도 implements가 아니라 extends이다.
두 개 이상을 상속받아야 한다면 class FruitBox<F extends Fruit & Food> {...}와 같이 & 연산자를 이용하면 된다.

0개의 댓글