자바(12) - 컬렉션프레임워크

우리가 사용하는 프로그램은 대부분 데이터를 사용하여 구현한다. 메일 시스템은 메일을, 채팅 앱은 친구 목록과 채팅 내용 등을 관리한다. 프로그램을 실행할 때 데이터를 효율적으로 관리하기 위해 자료 구조를 사용한다. 자바에서는 이런 자료구조들을 제공하는데 이를 컬렉션 프레임워크라고 한다.

제네릭

프로그램에서 변수를 선언할 때 모든 변수는 자료형이 있다. 메서드에서 매개변수를 사용할 때도 자료형을 가지고 있다. 대부분 하나의 자료형으로 구현하지만, 변수나 메서드의 자료형을 필요에 따라 여러 자료형으로 바꿀 수 있으면 프로그램은 훨씬 유연해질 것이다. 이처럼 어떤 값이 하나의 참조 자료형이 아닌 여러 참조 자료형을 사용할 수 있도록 프로그래밍하는 것을 제네릭(Generic) 프로그래밍이라고 한다. 제네렉 프로그램은 참조 자료형이 변환될 때 이에 대한 검증을 컴파일러가 하므로 안정적이다. 우리가 다룰 컬렉션 프레임워크도 제네릭을 저극 활용한다.

public class ThreeDPrinter {
    private Powder material;

    public void setMaterial(Powder material) {
        this.material = material;
    }

    public Powder getMaterial() {
        return material;
    }
}

public class ThreeDPrinter {
    private Plastic material;

    public void setMaterial(Plastic material) {
        this.material = material;
    }

    public Plastic getMaterial() {
        return material;
    }
}

위의 예제는 3D 프린터의 재료에 대한 클래싀이다. 3D 프린터의 재료로는 파우더가 될 수도 플라스틱이 될 수도 있다. 그런데 위와 같이 선언하면 우리는 재료가 생길 때마다 다른 클래스들을 만들어 주어야 하는 번거로움이 있다. 이런 문제점을 제네릭을 사용하면 바꿀 수 있다.

public class GenericPrinter<T> {
    private T material;

    public void setMaterial(T material) {
        this.material = material;
    }

    public T getMaterial {
        return material;
    }
}

GenericPrinter<Powder> powderPrinter = new GenericPrinter();
powderPrinter.setMaterial(new Powder())

클래스의 옆에 <T>가 보일텐데 이것이 제네릭 선언으로 자료형 매개변수라고 부른다. 자료형을 선택해서 하면 T 부분에 우리가 선택한 자료형들로 인식이 될 것이다. static 변수나 메서드는 인스턴스를 생성하지 않아도 클래스 이름으로 호출할 수 있다. static 변수는 인스턴스 변수가 생성되기 이전에 생성되며 static 메서드는 인스턴스 변수를 사용할 수 없다. 그런데 T의 자료형이 정해지는 순간은 제네릭 클래스의 인스턴스가 생성되는 순간이다. static들은 이 시점보다 빠르기 때문에 static 변수의 자료형이나 static 메서드 내부 변수의 자료형으로 사용할 수 없다.

T외에도 원하는 문자를 써도 된다. 보통 E(elment), K(key), V(value)등을 자주 쓴다.

T 자료형에 사용할 자료형을 제한하는

제네릭 클래스에서 T 자료형에 사용할 자료형에 제한을 둘 수 있다. 예를 들어 우리가 구현한 GenericPrint<T> 클래스는 사용할 수 있는 재료가 한정되어 있다. 만약 아무 제한이 없으면 아래 처럼 물을 재료로 쓰게할 수도 있기 때문이다.

GenericPrinter<Water> printer = new GenericPrinter<Water>();

이것은 논리상 말이 안되기에(언젠가는 될려나? ㅎㅎ) 자료형에 제한을 두는 방식으로 extends 예약어를 사용할 수 있다.

public abstract class Material {
    public abstract void doPrinting();
}

public class Powder extends Material {
    public void doPrinting() {
        System.out.println("Powder")
    }
}

public class Plastic extends Material {
    public void doPrinting() {
        System.out.println("Plastic")
    }
}

// extends 예약어로 사용할 수 있는 자료형에 제한을 둔다.
public class GenericPrinter<T extends Material> {
    private T material;
}

<T extends Material>로 선언하면 제네릭 클래스를 사용할 때 상위 클래스 Material에서 선언한 메서드를 사용할 수 있다. 그냥 <T>로 하면 컴파일 시 Object 클래스로 변환된다. 그러면 Object의 기본 메서드만 사용할 수 있다. 왜냐하면 자료형을 알 수 없기 때문이다. 근데 전자처럼 선언하면 Material 추상 클래스의 doPrinting()메서드를 사용할 수 있게 된다.

제네릭 메서드 활용하기

메서드의 매개변수를 자료형 매개변수로 사용하는 경우를 봐보자. 또 자료형 매개 변수가 하나 이상인 경우도 봐보겠다. 일단적인 제네릭 메서드의 형식은 아래와 같다.

public <자료형 매개변수> 반환형 메서드 이름(자료형 매개변수 ~~) {}

반환형 앞에 사용하는 <자료형 매개변수>는 여러 개일 수 있으며, 이는 메서드 내에서만 유효하다.

public class Point<T, V> {
    T x;
    V y;

    Point(T x, V y) {
        this.x = x;
        this.y = y;
    }

    // 제네릭 메서드
    public T getX() {
        return x;
    }

    public V getY() {
        return y;
    }
}

한 점을 나태는 Point 클래스의 두 좌표 x, y는 정수일 수도 있고 실수일 수도 있고 그렇기에 T와 V라는 자료형 매개변수로 표현했다. 그럼 다음과 같이 쓸 수 있다.

Point<Integer, Double> p1 = new Point<>(0, 0.0);
Point<Integer, Double> p2 = new Point<>(10, 10.0);

다음은 테스트 코드이다.

public class GenericMethod {
    public static <T, V> double makeRectangle(Point(T, V) p1, Point(T, V) p2) {
        double left = ((Number)p1.getX()).doubleValue();
        double right = ((Number)p2.getX()).doubleValue();
        double top = ((Number)p1.getY()).doubleValue();
        double bottom = ((Number)p2.getY()).doubleValue();
        double width = right - left;
        double height = bottom - top;

        return width * height;
    }

    public static void main(String[] args) {
        Point<Integer, Double> p1 = new Point<>(0, 0.0);
        Point<Integer, Double> p2 = new Point(10, 10.0);

        double rect = GenericMethod.<Integer, Doubble>makeRectangle(p1, p2);
    }
}

GenericMethod 클래스는 제네릭 클래스가 아니다. 제네릭 클래스가 아니라도 내부에 제네릭 메서드를 구현할 수 있다. 제네릭 메서드인 makeRectangle()메서드는 static으로 구현했다. makeRectangle() 메서드에서 사용하는 T와 V는 메서드 내부에서만 유효하게 사용할 수 있다.

class Shape<T> {
    public static <T, V> double makeRectangle(Point<T, V> p1, Point<T, V> p2) {}
}

위 코드에서 Shape<T>에 사용한 T와 makeRectangle()에서 사용한 T는 전혀 다른 의미이다. 즉 makeRectangle() 메서드에서 사용한 T는 메서드 내에서만 유효하다.

제네릭은 아직까지 헷갈리는 부분이 많은데 계속 써보면서 익숙해지는 수밖에 없을거 같다. 이 제네릭을 알아야 컬렉션 프레임에서도 잘 활용할 수 있기 때문이다. ArrayLists의 정의를 보면 다음과 같이 생겼다.

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}

배열은 요소를 가지므로 T보다는 Element를 의미하는 E를 많이 사용한다. 그리고 다음과 같이 자료형을 넣어 선언한다.

ArrayList<String> list = new ArrayList<String>();

// get() 메서드의 모습이다.
public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

ArrayList<String> list = new ArrayList<>();
String str = new String("abc");
list.add(str);
String s = list.get(0);

컬렉션 프레임워크(Collection Framework)

흔히 프로그램 개발을 건물 짓는 일에 비유한다. 원하는 건물을 지으려면 구조를 잘 잡아야 하듯이 프로그램을 개발할 때도 사용하는 자료를 어떤 구조로 관리할 것인지가 중요하다. 그래야 프로그램의 기능을 효과적으로 구현할 수 있기 때문이다. 이때 사용하는 것이 자료 구조(data structure)이다. 자료 구조는 프로그램 실행 중 메모리에 자료를 유지, 관리하기 위해 사용한다. 자바에서는 필요한 자료 구조를 미리 구현하여 java.util패키지에 제공하고 있는데, 이를 컬렉션 프레임워크라고 한다. 물론 자료 구조를 직접 만들 수도 있긴 하지만 컬렉션 프레임워크를 사용하면 직접 개발하는 수고도 덜고 이미 잘 만들어진 자료 구조 클래스들이기에 안정성이 좋다.

자바 컬렉션 프레임워크에는 여러 인터페이스가 정의되어 있고, 그 인터페이스를 구현한 클래스가 있다. 구조는 아래와 같다.

Collection <- List <- ArrayList, Vector, LinkedList
           <- Set  <- HashSet, TreeSet

Map <- HashTable <- Properties
    <- HashMap
    <- TreeMap

Collection 인터페이스

Collection 인터페이스 하위에 List 인터페이스와 Set 인터페이스가 있다. List를 구현한 클래스는 순차적인 자료를 관리하는 데 사용하는 클래스이고, Set 인터페이스는 우리가 수학 시간에 배운 집합을 생각하면 된다. 집합은 순서와 상관없이 중복을 허용하지 않는다. 따라서 Set 계열의 클래스는 아이디처럼 중복되지 않는 객체를 다루는 데 사용한다.

  • List 인터페이스: 순서가 있는 자료 관리, 중복 허용. ArrayList, Vector, LinkedList, Stack, Queue 등이 있다.
  • Set 인터페이스: 순서가 정해져 있지 않음. 중복을 허용하지 않음. HashSet, TreeSet 등이 있다.

Collection 인터페이스에 선언된 메서드 중 자주 사용하는 메서드는 아래와 같다.

  • boolean add(E e): Collection에 객체를 추가
  • void clear(): 모든 객체를 제거
  • Iterator<E> iterator: 순환할 반복자(Iterator)를 반환
  • boolean remove(Object o): 매개변수에 해당하는 인스턴스가 존재하면 제거
  • int size(): 요소의 개수를 반환

Map 인터페이스

Map 인터페이스는 하나가 아닌 쌍(Pair)으로 되어있는 자료를 관리하는 메서드들이 선언되어 있다. key-value 쌍이라고 표현하는데 이때 키 값은 중복될 수 없다. 학번과 학생 이름처럼 쌍으로 되어 있는 자료를 관리할 때 사용하면 편리하다. Map은 기본적으로 검색용 자료 구조이다. 즉 어떤 key값을 알고 있을 때 value를 찾기 위한 구조이다. 자주 사용하는 메서드는 다음과 같다.

  • V put(K key, V value): key에 해당하는 value값을 map에 넣는다.
  • V get(K key): key에 해당하는 value값을 반환
  • boolean isEmpty(): Map이 비었는지 여부 반환
  • boolean containsKey(Object key): Map에 해당 key가 있는지 여부를 반환
  • boolean containsValue(Object key): Map에 해당 value가 있는지 여부를 반환
  • Set keyset(): key 집합을 Set로 반환(중복 안 되므로 Set)
  • Collection values(): value를 Collection으로 반환(중복 무관)
  • V remove(key): key가 있는 경우 삭제
  • boolean remove(Object key, Object value): key가 있는 경우 key에 해당하는 value가 매개변수와 일치할 때