본문 바로가기

Java/Study

[JAVA] 백기선 라이브 스터디 14주차 - Generics

제네릭


목표

자바의 제네릭에 대해 학습하세요.

학습할 것 (필수)

  • 제네릭 사용법
  • 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
  • 제네릭 메소드 만들기
  • Erasure

제네릭이 왜 필요할까

범용 클래스의 장점

💡타입을 한정하지 않고 클래스를 사용할 수 있음

ex) Box Box 는 별개의 클래스를 의미 하는 것이 아니라 매개변수 값이 다른 메서드를 호출하는 것과 같다. add(3,5) add(2,4)와 같은

매개변수를 바꿔가며 함수를 사용하는 것도 코드의 유연성이 향상된다. 이를 클래스에 접목하면 클래스의 사용이 용이할 것 같다.

그러면, 모든 자료형을 담는 구조가 있을까 ? → Object 탄생

object형식으로 참조 할 수 없는 객체는 없다. → 원시 형식의 값은 참조할 수 없다.왜냐 값은 참조하는 것이 아니라 담는것(객체와 원시 데이터는 메모리 구조상 다른 곳에 위치한다.)

primitive data type은 직접 참조가 불가능하고, Wrapper클래스로 싸서 참조하는 과정을 거쳐야 함

boxing - 지금은 직접할 필요는 없다.(알아서 해줌)

Object obj = 3; // 값은 참조되는 것이 아니라 담는것
object obj = new Integer(3);
int a = obj.intValue();

Integer가 Wrapper클래스가 된다.

boxing 한 값을 사용하려면 unboxing 하는 과정을 거쳐야하고 이 과정에서 메모리 비효율이 발생한다.

범용자료형식의 문제점

값을 꺼낼 때 타입 변환을 각각 다르게 해줘야 한다는 문제 발생

class ObjectList{
    private Object[] nums;
    private int current;
    public ObjectList(){
        nums = new Object[3];
        current = 0;
    }
    public void add(Object num){
        nums[current] = num;
        current++;

    }
    public Object get(int index){
        return nums[index];
    }
}

public class Apps {
    public static void main(String[] args) {
        ObjectList list = new ObjectList();
        list.add("박소정");
        list.add(26);
        System.out.println("넣는건 에러 안남");
        int a = (Integer)list.get(0);//
                String b = (String)list.get(1);
/*꺼낼 때 객체를 건내주고,그 안엔 여러 타입이 올 수 잇음. 그런데 String -> Integer로 하려고 하면
ClassCastException 발생!*/
    }
}

int와 같은 원시 데이터 타입은 어렵고 Integer와 같은 wrapper클래스만 변수로 올 수 있음

왜냐면 Object type 이어야 함. → 그래야 모든 자료형을 담을 수 있다.

요약

  • 범용 자료형의 장점 : 코드의 유연성을 높일 수 있음.→ 강제 형변환을 통해 모든 자료형 접근 가능
  • 모든 자료형을 담을 수 있는 범용 클래스의 탄생 : Object
  • 원시 자료형은 Object 형으로 참조 할 수 없다 : Boxing , unboxing 발생
  • 모든 자료형을 담는 경우 : 문제 없음 (원시 데이터의 경우 Boxing해서 넣음)
  • 담은 자료형을 꺼내는 경우 : 문제 발생 (타입 캐스팅 에러 발생의 가능성이 높음)
  • 범용 자료형의 장점을 취하면서 타입의 안전성을 보장받고 싶다.

제네릭 사용법

class Box {}

Box : 원시 타입

T : 매개변수 . 대입된 타입

Box : 제네릭 클래스 . T의 클래스

ArrayList<TV> test1= new ArrayList<TV>();
ArrayList<Product> test2= new ArrayList<TV>();//TV가 Product를 상속하고 있어도 다형성 X
List<TV> test1= new ArrayList<TV>();
List<TV> test2= new LinkedList<TV>();// 제네릭 클래스 단위의 다형성 허용

그럼 Product의 자손 객체만 저장하고 싶으면?

//부모 객체의 리스트로 만든 후
ArrayList<Product> product_list= new ArrayList<Product>();
product_list.add(new TV());
product_list.add(new Product());
Tv t = (TV) product_list.get(0);
Product p = product_list.get(1);//꺼낼 때 형변환 해줘야함,,

Iterator

클래스 뿐만 아니라 iterator에도 제네릭 개념이 적용될 수 있다.

부분을 Object로 바꿔보면 이해하기 쉽다.

public static void main(String[] args) {
        ArrayList<Student> friend = new ArrayList<>();
        friend.add(new Student("박소정", 26));
        friend.add(new Student("유제원", 27));
        friend.add(new Student("안지훈", 31));
        Iterator it = friend.iterator();
        while(it.hasNext()){
            Student s = (Student)it.next();// Student로 형변환 해야됨,,
            System.out.println(s.getName());
        }
        System.out.println("=================================");
        Iterator<Student> it_gen = friend.iterator();
                    // 제네릭으로 타입 매개변수를 지정해주면 형변환 생략 가능
        while(it_gen.hasNext()){
            Student s = it_gen.next();
            System.out.println(s.getName());
        }
    }

Hashmap<K,V>

데이터를 key 와 value 로 저장하는 두개의 타입을 지정해 준다

public class HashMap<k,v> extends AbstractMap<K,V>{
    public V get(Object k){}
    public V put(K key , V value){}// 넣을 때만 key, value 타입을 지정해주고
    public V remove(Object k){}// 꺼내고 지울 때는 return 값이 value로 들어온 타입이므로
// 형변환을 하지 않아도 된다.
}

제네릭 주요개념 (바운디드 타입, 와일드 카드)

api 문서를 읽으면서 어려웠던 개념들이다.

한 종류의 타입만 저장할 수 있도록 제한 할 수 있지만, 모든 종류의 타입의 저장이 가능하다

지정할 수 있는 타입을 제한하는 방법은?

-bounded type

class FruitBox{}

💡인터페이스를 구현해야 하는 제약이 필요해도 implements가 아닌 extends를 사용해야함!

인터페이스와 클래스를 동시에 상속 받는 경우 에는 <T extends Fruit && Interface>로 !

위의 예에서 타입간의 다형성은 컴파일 에러가 났고, 사용시 형변환을 해줘야 한다는 불편함이 있었다.

다형성을 사용하기 위해 와일드 카드라는 개념이 도입되었다.

-wildcard

<? extends T> : 상한 제한 T와 그 자손들만 가능

<? super T> : 하한 제한 T와 그 조상들만 가능

<?> : 제한 없음 → ? extends Object

ArrayList<? extends Product> = new ArrayList<TV>();// 가능 

제네릭 메서드

매개 타입과 리턴 타입으로 타입 파라미터를 받는 메소드

메서드의 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메소드라 한다.

static void sort(List list, Comparator<? super T> c)

주의 : 둘이 모양은 같지만 다른 값이 들어 올 수 있다.


Erasure

컴파일러는 제네릭 타입을 이용해 소스파일을 체크하고, 필요한 곳에 형 변환을 넣어준다.

즉, (*.class) 에는 제네릭 타입에 대한 정보가 없다.!

(이전 파일과의 호환성을 위함)


제거 과정

  1. → Fruit로 치환, → Object
class Box<T extends Fruit>{
    void add(T t){
}
}

    //아래로 변환

class Box {
    void add(Fruit t){
}
}
  1. 제네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.

List 의 get은 Object 타입을 반환하므로 형변환이 필요 없다.

T get(int i){
    void add(T t){
}
}
// 아래로 변환
Fruit get(int i ){
    return (Fruit)list.get(i);
}
  1. 와일드 카드가 포함되어 있는 경우
static Juice makeJuice <FruitBox<? extends Fruit> box){
    String tmp = "";
    for(Fruit f: box.getList()) tmp = += f +"";
    return new Juice(tmp)
}
static Jucie makeJuice (FruitBox box){
    String tmp = "";
    Iterator it = box.getlist().iterator()
    while(it.hasNest()){
         tmp +=  (Fruit)it.next() + " " ;
}
return new Juice(tmp);