[Java] 컬렉션 프레임워크(Collection Framework) List, Set, Map 각각의 특징을 알아보고 대표적인 구현클래스 사용해보기

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

    컬렉션 프레임웍이란 데이터를 저장하는 클래스들을 표준화한 설계를 뜻한다. 컬렉션은 다수의 데이터, 즉 데이터 그룹을, 프레임웍은 표준화된 프로그래밍 방식을 의미한다. 


    애플리케이션을 개발하다 보면 다수의 객체를 저장해 두고 필요할 때마다 꺼내서 사용하는 경우가 많다. 만약 10개의 Product 객체를 저장해 두고, 필요할 때마다 하나씩 꺼내서 이용한다고 가정할때 배열을 사용할 것이다 배열은 쉽게 생성하고 사용할 수 있지만, 저장할 수 있는 객체 수가 배열을 생성할 때 결정되기 때문에 불특정 다수의 객체를 저장하기에는 문제가 있다. 물론 배열의 길이를 크게 생성하면 되지만, 이것은 좋은 방법이 될 수 없다. 배열의 또 다른 문제점은 객체를 삭제했을 때 해당 인덱스가 비게 되어 낱알이 듬성듬성 빠진 옥수수가 될 수 있다. 그렇기 때문에 새로운 객체를 저장하려면 어디가 비어 있는지 확인하는 코드도 필요하다.


    자바는 배열의 이러한 문제점을 해결하고, 널리 알려져 있는 자료구조를 바탕으로 객체들을 효율적으로 추가, 삭제, 검색할 수 있도록 컬렉션과 관련된 인터페이스와 클래스들을 포함시켜 놓았다. 이들을 총칭해서 컬렉션 프레임워크라고 부른다 컬렉션이란 사전적 의미로 요소를 수집해서 저장하는 것을 말하는데, 자바 컬렉션은 객체를 수집해서 저장하는 역할을 한다. 프레임워크란 사용 방법을 미리 정해 놓은 라이브러리를 말한다. 


    컬렉션 종류

    다음 컬렉션 프레임웍의 핵심 인터페이스와 특징이다 List, Set은 Collection클래스의 하위 클래스이다. 


     인터페이스

     특징 

     List 

     순서가 있는 데이터의 집합. 데이터의 중복을 허용한다.

     예) 대기자 명단

     구현클래스 : ArrayList, LinkedList, Stack, Vector 등 

     Set 

     순서를 유지하지 않는 데이터의 집합. 데이터의 중복을 허용하지 않는다.

     예) 양의 정수집합, 소수의 집합

     구현클래스 : HashSet, TreeSet 등 

     Map 

     키(Key)와 값(Value)의 쌍(Pair)으로 이루어진 데이터의 집합

     순서는 유지되지 않으며, 키는 중복을 허용하지 않고, 값은 중복을 허용한다.

     예) 우편번호, 지역번호(전화번호)

     구현클래스 : HashMap, TreeMap, Hashtable, Properties 등


    실제 개발 시에는 다루고자 하는 컬렉션의 특징을 파악하고 어떤 인터페이스를 구현한 컬렉션 클래스를 사용해야 하는지 결정해야하므로 위에 적힌 각 인터페이스의 특징과 차이를 잘 이해하고 있어야 한다.

    컬렉션 프레임웍의 모든 컬렉션 클래스들은 List, Set, Map 중의 하나를 구현하고 있으며, 구현한 인터페이스의 이름이 클래스의 이름에 포함되어 있어서 이름만으로도 클래스의 특징을 쉽게 알 수 있다.



    List 컬렉션

    List 컬렉션은 객체를 일렬로 늘어놓은 국조를 가지고 있다. 객체를 인덱스로 관리하기 때문에 객체를 저장하면 자동 인덱스가 부여되고 인덱스로 객체를 검색, 삭제할수 있는 기능을 제공한다. List 컬렉션은 객체 자체를 저장하는 것이 아니라 객체의 번지를 참조한다. 즉 인덱스 마다 객체의 번지주소가 들어있다. 동일한 객체를 중복 저장할 수 있는데, 이 경우 동일한 번지가 참조되고 null이 저장될 경우에는 해당 인덱스는 객체를 참조하지 않는다.다음 List의 대표적인 구현 클래스 ArrayList를 보자


    ArrayList

    ArrayList는 컬렉션 프레임웍에서 가장 많이 사용되는 컬렉션 클래스일 것이다. 이 ArrayList는 List인터페이스를 구현하기 때문에 데이터의 저장순서가 유지되고 중복을 허용한다는 특징을 갖는다.

    ArrayList는 Object배열을 이용해서 데이터를 순차적으로 저장한다. 예를 들면, 첫 번째로 저장한 객체는 Object배열의 0번째 위치에 저장되고 그 다음에 저장하는 객체는 1번째 위치에 저장된다. 이런 식으로 계속 배열에 순서대로 저장되며, 배열에 더 이상 저장할 공간이 없으면 보다 큰 새로운 배열을 생성해서 기존의 배열에 저장된 내용을 새로운 배열로 복사한 다음에 저정한다. 즉, 배열은 생성할 때 크기가 고정되고 사용 중에 크기를 변경할 수 없지만 ArrayList는 저장 용량(capacity)을 초과한 객체들이 들어오면 자동적으로 저장 용량이 늘어난다는 것이다. 


    ArrayLst를 생성하기 위해서는 저장할 객체 타입을 타입 파라미터로 표기하고 기본 생성자를 호출하면된다. 예를 들어 String을 저장하는 ArrayList는 다음과 같이 생성할 수 있다. 


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

    처음 자동완성으로 만들면 List<E> 로 생성될텐데 E는 타입 파라미터를 뜻한다 List 인터페이스가 제네릭 타입이기 때문이다. 타입 파라미터로 String 으로 설정하였기 때문에 String 타입만 들어갈 수 있다. 만약 타입 파라미터를 지정해주지 않으면 모든 종류의 객체를 저장할 수 있다. 하지만 저장할 때 Object로 변환하고, 찾아올 때 원래 타입으로 변환해야 하므로 실행 성능에 좋지 못한 영향을 미친다. 일반적으로 컬렉션에는 단일 종류의 객체들만 저장된다. 그래서 제넥을 도입하여 ArrayList 객체를 생성할 때 타입 파라미터로 저장할 객체의 타입을 지정함으로써 불필요한 타입 변환을 하지 않도록 했다.


    제네릭 도입하기전(자바5 이전) 코드

    List list = new ArrayList();
    list.add("홍길동");
    Object obj = list.get(0);
    String name = (String) obj;
    cs


    제네릭 도입 이후(자바5 이후) 코드

    List<String> list = new ArrayList<String>();
    list.add("홍길동");
    String name = list.get(0);
    cs



    기본 생성자로 ArrayList 객체를 생성하면 내부에 10개의 객체를 저장할 수 있는 초기 용량을 가지게 된다. 저장되면 객체수가 늘어나면 용량이 자동으로 증가하지만, 처음부터 용량을 크게 잡고 싶다면 용량의 크기를 매개값으로 받는 생성자를 이용하면 된다.


    List<String> list = new ArrayList<String>(30);
    cs

    (30)으로 String객체 30개를 저장할 수 있는 용량을 가지게 했다.



    ArrayList와 LinkedList의 차이

    ArrayList 객체를 추가하면 인덱스 0부터 차례대로 저장된다. ArrayList에서 특정 인덱스의 객체를 제거하면 바로 뒤 인덱스부터 마지막 인덱스까지 모두 앞으로 1씩 당겨진다. 마찬가지로 특정 인덱스에 객체를 삽입하면 해당 인덱스부터 마지막 인덱스까지 모두 1씩 밀려난다. 




    따라서 빈번한 객체 삭제와 삽입이 일어나는 곳에는 ArrayList를 사용하는 것이 바람직하지 않다. 이런 경우라면 LinkedList를 사용하는 것이 좋다. 그러나 인덱스 검색이나, 맨 마지막에 객체를 추가하는 경우에는 ArrayList가 더 좋은 성능을 발휘한다. ,즉 LinkedList는 중간에 데이터를 추가 하거나 삭제할때 ArrayList보다 빠르다. 그러나 데이터를 순차적으로 추가삭제 검색하는 것은 ArrayList가 더 빠르다.다음 ArrayList 예제를 보자


    public class ArrayListExample {
        public static void main(String[] args) {
            List<String> list = new ArrayList<String>();
            
            list.add("a");
            list.add("b");
            list.add("c");
            list.add("d");
            list.add("e");
            System.out.println(list);
            System.out.println("총 객체수: " + list.size());
            System.out.println();
     
            list.add(2"g");
            System.out.println(list);
            System.out.println("총 객체수: " + list.size());
            System.out.println();
            
            list.remove(4);
            System.out.println(list);
            System.out.println("총 객체수: " + list.size());
            System.out.println();
        }
    }
    cs

    add() 메소드를 통해서 0번째 인덱스부터 차례대로 데이터를 넣어주었다. 실제로는 객체 주소를 넣어준다. 그리고 size() 메소드를 통해 객체수를 알아낼수 있고 remove(인덱스) 를 통해서 해당 인덱스를 지울수도 있다.



    Set 컬렉션 

    List 컬렉션은 저장 순서를 유지하지만, Set 컬렉션은 저장 순서가 유지되지 않는다. 또한 객체를 중복해서 저장할 수 없고, 하나의 null만 저장할 수 있다. Set 컬렉션은 수학의 집합에 비유될 수 있따. 집합은 순서와 상관없고 중복이 허용되지 않기 때문이다. 

    Set 인터페이스의 구현클래스는 HashSet, LinkedHashSet, TreeSet 등이 있는데, 먼저 대표적인 HashSet을 보자


    HashSet

    HashSet은 객체들을 순서 없이 저장하고 동일한 객체는 중복 저장하지 않는다. HashSet이 판단하는 동일한 객체란 꼭 같은 인스턴스를 뜻하지는 않는다. HashSet은 객체를 저장하기 전에 먼저 객체의 hashCode() 메소드를 호출하여 해시코드를 얻어낸다. 그리고 이미 저장되어 있는 객체들의 해시코드와 비교한다. 만약 동일한 해시코드가 있다면 다시 equals() 메소드로 두 객체를 비교해서 true가 나오면 동일한 객체로 판단하고 중복 저장을 하지 않는다.


    문자열을 HashSet에 저장할 경우, 같은 문자열을 갖는 String 객체는 동등한 객체로 간주되고 다른 문자열을 갖는 String 객체는 다른 객체로 간주되는데, 그 이유는 String 클래스가 hashCode()와 equals() 메소드를 재정의해서 같은 문자열일 경우 hashCode()의 리턴값을 같게, equals()의 리턴값은 true가 나오도록 했기 때문이다 다음은 HashSet에 String객체를 추가, 검색, 제거하는 방법을 보여준다.


    public class HashSetExample {
         public static void main(String[] args) {
            Set<String> set = new HashSet<>();
            
            set.add("a");
            set.add("b");
            set.add("c");
            set.add("d");
            set.add("e");
        
            System.out.println(set);
            System.out.println(set.size());
            
            Iterator<String> iterator = set.iterator();
            while(iterator.hasNext()) {
                String element = iterator.next();
                System.out.print(element + " ");
            }
            System.out.println();
            
            set.remove("a");
            System.out.println(set);
            
            set.clear();
            System.out.println(set);
        }
    }
    cs


    Set은 인덱스가 없기 때문에 반복을 통해서 객체를 가져올려면 Iterator 클래스를 사용해야 한다. set.iterator() 를 통해서 Iterator 클래스 iterator 변수안에 저장된 객체를 가져온다. 그리고 나서 iterator.hasNext()로 가져올 데이터가 있는지의 유무를 검사하고 데이터가 있으면 next()를 통해 하나의 객체를 가져온다. 그러면서 하나씩 데이터를 출력한 것이다. Iterator을 사용해서 데이터를 가져올수 있지만 그냥 향상된 for문을 통해서 데이터를 하나하나 가져올 수 있다.


    for (String string : set) {
        System.out.print(string + " ");
    }
    cs




    Map 컬렉션

    Map 컬렉션은 키(key)와 값(value) 쌍(pair) 으로 구성된 Entry 객체를 저장하는 구조를 가지고 있다. 여기서 키와 값은 모두 객체이다. 키는 중복 저장될 수 없지만 값은 중복 저장될 수 있다. 만약 기존에 저장된 키와 동일한 키로 값을 저장하면 기존의 값은 없어지고 새로운 값으로 대치된다.


    Map 컬렉션에는 HashMap, Hashtable, LinkedHashMap, Properties, TreeMap 등이 있다. 다음 대표적인 HashMap을 보자


    HashMap

    HashMap은 Map 인터페이스를 구현한 대표적인 Map 컬렉션이다. HashMap의 키로 사용할 객체는 hashCode()와 equals() 메소드를 재정의해서 동등 객체가 될 조건을 정해야 한다. 동등 객체, 즉 동일한 키가 될 조건은 hashCode()의 리턴값이 같아야 하고, equals() 메소드가 true를 리턴해야 한다. 주로 키타입은 String을 많이 사용하는데, String은 문자열이 같을 경우 동등 객체가 될 수 있도록 hashCode()와 equals() 메소드가 재정의되어 있다. HashMap을 생성하기 위해서는 키 타입과 값 타입을 파라미터로 주고 기본 생성자를 호출하면 된다.  다음 키는 String, 값은 Integer인 HashMap 예제를 보자


    public class HashMapExample {
        public static void main(String[] args) {
            Map<String, Integer> map = new HashMap<>();
            
            map.put("a"1);
            map.put("b"2);
            map.put("c"3);
            map.put("d"4);
            map.put("e"5);
            System.out.println(map.size());
            
            System.out.println(map.get("a"));
            
            Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
            Iterator<Map.Entry<String, Integer>> entryIterator = entrySet.iterator();
            
            while(entryIterator.hasNext()) {
                Map.Entry<String, Integer> entry = entryIterator.next();
                String key = entry.getKey();
                Integer value = entry.getValue();
                
                System.out.println("key : " + key + ", value : " + value);
            }
            System.out.println();
        }
    }
    cs


    map은 put() 메소드를 통해 데이터를 넣을수 있다. (키, 값) 순으로 넣으면 되고 데이터를 가져올 때는 get()메소드를 사용하는데 키값을 이용해서 데이터를 가져온다. 그래서 반복문을 통해 key, value 쌍을 가져올려면 entrySet() 메소드를 사용해야 한다. 먼저 Set<E> 클래스 변수에 map데이터를 담고 나서 iterator() 클래스를 사용해 반복문을 돌린다.

    키는 getKey() 메소드로 가져오고 값은 getValue() 메소드를 사용해서 가져온다. 



    KeySet()을 활용한 데이터 추출


    public class HashMapExample2 {
        public static void main(String[] args) {
            Map<String, Integer> map = new HashMap<>();
            
            map.put("a"1);
            map.put("b"2);
            map.put("c"3);
            map.put("d"4);
            map.put("e"5);
     
            System.out.println(map.keySet());
            Iterator<String> iterator = map.keySet().iterator();
              while (iterator.hasNext()) {
                String key = (String) iterator.next();
                System.out.print("key="+key);
                System.out.println(" value="+map.get(key));
            }
        }
    }
    cs


    for (String key : map.keySet()) {
            System.out.print("key="+key);
            System.out.println(" value="+map.get(key));
    }
    cs


    신용권, 이것이 자바다, 한빛미디어

    댓글

    Designed by JB FACTORY