ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Camel][Java] Collection Framework (컬렉션 프레임워크)
    Java/개념 정리 2020. 3. 1. 17:57

    Collection Framework란?

    컬렉션 프레임워크란 쉽게 말해서 약속된 구조나 골격을 말합니다. 여기서 구조나 골격이란 Java에서 클래스를 의미합니다. 약속된, 이미 정의된 클래스라고 할 수 있는 것입니다. 하지만 이러한 특성은 라이브러리와 별반 다를바 없습니다. 그렇다면 왜 프레임워크라고 하는 것일까? 그 이유는 컬렉션과 관련된 클래스들의 정의에 적용되는 설계의 원칙 또는 구조가 존재하기 때문입니다. 

     컬렉션은 데이터의 저장, 그리고 이것과 관련 있는 알고리즘을 구조화해놓은 프레임워크입니다. 이 말이 어렵다면, 컬렉션은 자료구조와 알고리즘을 클래스로 구현한 것으로 생각하면 좀 더 와닿을 것입니다. 이러한 컬렉션 클래스는 많은 양의 인스턴스를 다양한 형태로 저장하는 기능을 제공하기 때문에 자료구조나 알고리즘을 잘 모르더라도 컬렉션 프레임워크를 활용하면 다양하고 효율적인 인스턴스의 저장이 가능합니다. 물론 자료구조나 알고리즘을 몰라도 상관없다는 것은 아닙니다 :) 

     컬렉션이 프레임워크인 이유는 인터페이스 구조를 기반으로 클래스들이 정의되어 있기 때문입니다. 컬렉션 프레임 워크의 인터페이스 구조는 아래의 그림을 통해 확인할 수 있습니다. 

    Collection Framework Interface Structure

    위 그림에서 Collection<E> 인터페이스와 Map<K,V> 인터페이스로 나뉘는 것을 확인 할 수 있습니다. 대부분의 일반적인 데이터 저장을 지원하는 제네릭 클래스들은 모두 Collection<E>를 구현하고 있습니다. 반면 Map<K,V>는 키값과 밸류 값을 기반으로 하는 데이터 저장방식입니다. 

     

     

     

     

     

    Collection<E> 인터페이스를 구현하는 Generics 클래스

    Collection<E> 인터페이스를 구현하는 Generics 클래스에는 크게 List<E>,Set<E> 그리고 Queue<E>가 있습니다. 이러한 Generics 클래스는 인스턴스의 참조 값을 저장의 대상으로 삼습니다. 다만 저장방식에 차이가 있어서 여러가지로 나뉘게 되는 것입니다. 

    List<E> 인터페이스 

    List<E> 인터페이스를 구현하는 제네릭 클래스는 동일한 인스턴스의 중복 저장을 허용하고 인스턴스의 저장순서가 유지된다는 특징이 있습니다. 이러한 List<E> 인터페이스의 대표적인 제네릭 클래스는 ArrayList<E>와 LinkedList<E>가 있습니다. 이 두 클래스의 차이점은 저장하는 방식입니다.

     

    ArrayList<E> 클래스

     ArrayList<E>는 클래스의 이름에서도 볼 수 있듯이 배열과 굉장이 유사합니다. 하지만 배열과의 차이점은 인덱스 정보를 따로 관리할 필요가 없으며 데이터 삭제를 위한 코드의 작성이 필요없다는 것입니다. 또한 저장되는 인스턴스의 수에 따라서 크기가 자동으로 확장된다는 특징 덕분에 배열처럼 그 크기를 고민할 필요가 없습니다. ArrayList<E>는 배열을 기반으로하기 때문에 저장소의 용량을 늘리는 과정에 많은 비용이 필요하다는 것과 데이터의 삭제에 필요한 연산과정이 매우 복잡하다는 단점이 있습니다. 반면 데이터의 참조가 쉽다는 장점도 존재합니다. 배열은 한번 생성되면 그 길이를 변경할 수 없기때문에 저장소의 용량을 늘리려면 새로운 배열 인스턴스를 생성하고 기존 데이터를 복사하는 과정이 필요합니다. 이런 과정은 부담이 많이 갈 수 밖에 없습니다. 데이터의 삭제과정 역시 중간 값을 삭제하게 되면 그 뒤에 저장된 데이터들을 모두 한 칸씩 이동시켜줘야 하기때문에 연산과정이 복잡해지게 되는 것입니다. 

     

    LinkedList<E> 클래스

     LinkedList<E>는 ArrayList<E>와 굉장히 유사하기 때문에 사용방법에서도 차이가 거의 없습니다. LinkedList<E>는 저장소의 용량을 늘리는 과정이 간단하고 데이터의 삭제가 간단하다는 장점이 있습니다. 반면 데이터의 참조는 다소 불편하다는 단점도 있습니다. 이러한 특징들은 ArrayList<E>의 특징과 상반된다는 점을 알 수 있을 것입니다. 

     

     ArrayList<E>와 LinkedList<E>의 차이첨은 내부적으로 인스턴스를 저장하는 방식에 차이가 있습니다. 방식에 차이가 있을 뿐이지 어떤 클래스가 좋은 클래스라고 할 수 없다는 것입니다. 상황에 맞는 클래스를 적절하게 선택해서 사용하는 것이 중요한 것입니다. 

     

    위의 두 클래스의 장단점을 요약하면 아래와 같습니다.

     

    ArrayList<E> 장점

    저장소의 용량을 늘리는데 비용이 크다.

    데이터의 삭제에 필요한 연산과정이 복잡하다.

    ArrayList<E> 단점

    데이터의 참조가 용이하다. 

     

    LinkedList<E> 장점

    저장소의 용량을 늘리는 것이 간편하다.

    데이터의 삭제가 간편하다.

    LinkedList<E> 단점

    데이터의 참조가 불편하다. 

     

     

     

     

     

     

    Set<E> 인터페이스

    이번에는 Set<E>인터페이스입니다. Set<E> 인터페이스를 구현하는 클래스의 List<E> 인터페이스를 구현하는 클래스와의 차이점은 데이터의 저장순서 유지와 중복저장 허용입니다. Set<E> 인터페이스를 구현하는 클래스는 데이터의 저장순서를 유지하지 않고, 데이터의 중복저장을 허용하지 않습니다. 
     Set<E> 인터페이스를 구현하는 클래스의 대표적인 예로는 HashSet<E> 클래스와 TreeSet<E> 클래스가 있습니다. 

     

    HashSet<E>클래스

    import java.util.Iterator;
    import java.util.HashSet;
    
    class HowToUseSetInterface {
    	public static void main ( String[] args ) {
        		HashSet<String> hSet = HashSet<String>();
            	hSet.add("One");
            	hSet.add("Two");
            	hSet.add("Three");
            	hSet.add("One");
            
            	Iterator<String> i = hSet.iterator();
                
                	while(i.hasNext()) {  
               		System.out.println(i.next());
                	}
                    //	출력 결과 
                    //	Three
                    //  	Two
                    //  	One
            
            	
        	}
    }

     위의 코드를 확인해보면 총 4번의 add 메소드의 사용을 통해 데이터를 저장하는 것을 볼 수 있습니다. 하지만 출력 결과를 확인해보면 One은 두번이 아닌 한번만 저장된 것을 알 수 있습니다. 이것은 Set<E> 인터페이스의 데이터 중복저장을 허용하지 않는 특징을 보여줍니다. 또한 한가지 더 주목할 점은 출력 순서가 역순이라는 것입니다. 하지만 입력 순서를 바꿔서 실행해보면 출력 결과가 변하지 않는 것을 확인할 수 있습니다. 이것은 Set<E> 인터페이스를 구현하는 클래스는 데이터의 저장순서를 유지하지 않는다는 특징을 통해 이해할 수 있습니다. Set이라는 것은 집합이라는 뜻이고 집합에는 순서가 존재하지 않습니다.

     

     HashSet<E> 클래스에서 동일한 데이터를 저장하지 않는다는 사실을 확인했지만, 그렇다면 어떻게 동일한 데이터인지 아닌지를 구분할 수 있을까? HashSet<E> 클래스를 좀 더 제대로 활용하고 이해하기 위해서는 Hash 알고리즘을 이해해야합니다. 동일한 값이 존재하는지 확인하는 과정은 우선 데이터의 Hash 값을 먼저 계산해야합니다. 그리고 얻어낸 Hash 값에 속하는 그룹내에 동일한 데이터가 존재하는지 하나씩 확인합니다. 이 두과정을 나누어 수행하는 이유는 검색의 효율성 그리고 이로인한 속도의 향상을 위해서 입니다. 이러한 Hash 알고리즘을 사용하는 HashSet<E> 클래스의 장점은 검색속도가 매우 빠르다는 것입니다. 

     HashSet<E> 클래스의 데이터 저장과정에서 일어나는 검색의 두 단계는 Object 클래스에 정의되어 있는 hasCode 메소드와 equals 메소드를 상황에 맞게 적절하게 오버라이딩하여 활용해야 합니다. 

     

     이렇게 검색속도가 빠르다는 장점을 가진 HashSet<E> 클래스에는 아이러니하게도 검색과 직접적으로 관련된 메소드가 존재하지 않는데, 그 이유는 HashSet<E> 의 데이터 저장과정에서 데이터의 중복을 막기 위해 항상 검색을 해주기 때문입니다. 그렇기 때문에 HashSet<E>은 매우 빠른 검색속도와 더불어 매우 빠른 데이터의 저장도 가능하게됩니다. 

     

    TreeSet<E> 클래스

     이어서 TreeSet<E> 클래스에 대해서 설명하겠습니다. 앞서 설명한 HashSet<E> 클래스가 Hash라는 자료구조를 기반으로 구현되었듯이 TreeSet<E> 클래스는 Tree라는 자료구조를 기반으로 구현되어 있습니다. Tree는 데이터를 정렬된 상태로 저장하는 자료구조입니다.

     따라서, TreeSet<E> 클래스는 데이터를 정렬된 상태로 유지합니다. 저장순서를 유지하는 HashSet<E>과는 확연하게 다른 것입니다. 또한 TreeSet<E> 클래스는 Set<E> 인터페이스를 구현하는 클래스이므로 데이터의 중복저장을 허용하지 않습니다. 

     그렇다면 TreeSet<E> 클래스는 데이터를 정렬된 상태로 저장한다고 했는데 그 정렬기준을 어떻게 정해지게 되는 것일까? 아래의 코드와 같이 데이터의 입력이 주어진다면 우리는 어떻게 정렬기준을 정해야할까? 

    Camel c1 = new Camel("Kim", 160);
    Camel c2 = new Camel("Lee", 170);
    Camel c3 = new Camel("Jeon", 180);
    Camel c4 = new Camel("Choi", 190);

     주어지는 입력에는 숫자뿐만아니라 문자열정보도 동시에 주어지고 있습니다. 이런 상황은 자주 겪는 상황으로 대부분의 경우 인스턴스의 정렬기준은 개발자가 직접 정의해줘야 합니다. 그렇기 때문에 Java에서는 아래의 인터페이스 구현을 통해 정렬의 기준을 개발자가 직접 정의할 것을 요구하고 있습니다. 

    interface Comparable <T> {
    	int compareTo(T obj);
    }

     위 인터페이스의 compareTo 메소드는 인자로 전달된 obj가 작다면 양의 정수를 반환하고, 크다면 음의 정수, 같다면 0을 반환하도록 정의되어야 합니다. 앞서 데이터의 입력의 예시로 보였던 Camel 인스턴스의 생성을 다시 예로 들어 설명하겠습니다. 정렬의 기준을 숫자의 크기가 크면 크다고 기준을 정했다고 가정하면, compareTo 메소드를 구현할 때 인자로 전달되 숫자가 작다면 양수를 반환하는 것이고, 크다면 음수, 같다면 0을 반환하는 것입니다. 이러한 Comparable<T> 인터페이스를 구현한다면 비로소 TreeSet<E> 클래스를 사용해 데이터를 저장할 수 있게 되는 것입니다. TreeSet<E> 역시 compareTo 메소드의 호출결과를 참조해 정렬하기 때문에, TreeSet<E>에 저장되는 인스턴스는 반드시 Comparable<T> 인터페이스를 구현해야하는 것입니다. Integer와 같은 일부 클래스에는 이러한 Comparable<T> 인터페이스를 이미 구현하고 있기 때문에 따로 Comparable<T> 인터페이스를 구현할 필요가 없기도 합니다. String 클래스도 역시 compareTo 메소드에 사전편찬 순서를 기준으로 정렬기준이 정의되어 있습니다. 

     

     그렇다면 여기서 한가지 추가적으로 드는 궁금한 점은 'String 같은 경우는 사전편찬 순서만을 정렬기준으로 할 수 있는가?' 입니다. 만약 문자열의 길이를 정렬기준으로 하고 싶은 경우가 있을 수 있기 때문에, 이러한 경우에는 String 클래스의 Wrapper 클래스를 정의해 정렬기준을 정하는 방식을 사용할 수 있습니다. 이를 활용한 예시를 아래에 코드로 작성했습니다. 

    import java.util.Iterator;
    import java.util.TreeSet;
    
    class WrapString implements Comaparable<WrapString> {
    		String str;
        	public WrapString ( String str ) { this.str = str }
        	public int getLength() { return str.length(); }
        	public int compareTo(WrapString wStr) {
        		if ( getLength() > wStr.getLength() ) return 1;
            	else if ( getLength() < wStr.getLength() ) return -1;
            	else return 0;
        	}
        	public String toString() { return str; }
    }
    
    class CompareWrapString {
    	public static void main ( String[] args ) {
        		TreeSet<WrapString> tS = new TreeSet<WrapString>();
            	tS.add(new WrapString("Camel"));
            	tS.add(new WrapString("Lion"));
            	tS.add(new WrapString("Pig"));
            
            	Iterator<WrapString> i = tS.iterator();
                
                	while(i.hasNext()) {  
               		System.out.println(i.next());
                	}
                    
                   // 출력결과
                   // Pig
                   // Lion
                   // Camel
        }
    }
    

     위과 같은 Wrapper 클래스를 정의해 정렬기준을 정하는 방법은 정렬기준을 변경하기 위해 별도의 클래스를 정의해야한다는 점이 다소 번거롭고 이치에 맞지않다고 느낄 수 있습니다. 그렇기에 Java에서는 Comparator<T> 인터페이스를 기반으로 TreeSet<E>의 정렬기준을 정할 수 있도록 하고 있습니다. Comparator<T> 인터페이스는 아래와 같이 정의되어 있으며, 사용예시 또한 아래에 코드로 작성했습니다. 

    interface Comparator<T> {
    	int compare(T obj1, T obj2);
        	boolean equals(Object obj);
    }
    import java.util.Iterator;
    import java.util.TreeSet;
    import java.util.Comparator;
    
    class StringComparator implements Comaparator<String> {
    		
        	public int compare(String str1, String str2) {
        		if ( str1.length() > str2.length() ) return 1;
            	else if ( str1.length() < str2.length() ) return -1;
            	else return 0;
        	}
    }
    
    class CompareStringUseComparator {
    	public static void main ( String[] args ) {
        		TreeSet<String> tS = new TreeSet<String>(new StringComparator());
            	tS.add(new WrapString("Camel"));
            	tS.add(new WrapString("Lion"));
            	tS.add(new WrapString("Pig"));
            
            	Iterator<WrapString> i = tS.iterator();
                
                	while(i.hasNext()) {  
               		System.out.println(i.next());
                	}
                    
                   // 출력결과
                   // Pig
                   // Lion
                   // Camel
        }
    }
    

     

    Map<K, V> 인터페이스

    이번에는 Map<K,V> 인터페이스를 구현하는 클래스를 설명하겠습니다. Map<K,V> 인터페이스를 구현하는 클래스의 대표적인 두 가지로는 HashMap<K,V>클래스와 TreeMap<K,V>클래스가있습니다. 이러한 클래스들의 데이터 저장방식은 Key와 Value를 사용하기때문에 Key-Value 방식이라고도 합니다. 이러한 방식의 특징은 데이터를 저장할 때 데이터만 저장하는 것이 아니라 데이터를 찾기위한 Key를 함께 저장하는 구조라는 것입니다. 

     

    HashMap<K,V> 클래스

    import java.util.HashMap;
    
    
    class UseHashMap {
    	public static void main ( String[] args ) {
        		HashMap<Integer, String> hM = new HashMap<Integer, String>();
                	hM.put( new Integer(1), "Camel");
                	hM.put( 3, "Camel");
                	hM.put( 5, "Pig");
            	hM.remove(5);
                
            	System.out.println("Key is 1 : "+ hM.get(1));
                	System.out.println("Key is 3 : "+ hM.get(new Ingeter(3)));
                	System.out.println("Key is 3 : "+ hM.get(new Ingeter(5)));
               	// 출력결과
                    // Camel
                    // Camel
                    // null
        }
    }
    

     HashMap<K,V> 클래스의 특징은 Value에 상관없이 중복된 Key의 저장을 허용하지 않는다는 점과 Value가 같더라도 Key가 다르면 둘 이상의 데이터 저장이 가능하다는 점입니다. 그리고 HashMap<K,V> 역시 HashSet<E>과 마찬가지로 Hash 알고리즘을 기반으로 하고 있기때문에 검색속도가 빠르다는 장점을 동일하게 가지고 있습니다. 

     

    TreeMap<K,V> 클래스

     TreeMap<K,V> 클래스의 경우도 Tree 자료구조를 기반으로 구현되어 있습니다. 그렇기 때문에 저장된 데이터는 정렬되어 저장됩니다. 사용방법과 데이터의 정렬을 제외한 특징은 HashMap<K,V>과 거의 동일합니다. 

     

     

     

    Collection<E> 인터페이스의 Iterator 메소드

     Collection<E> 인터페이스에는 다음과 같이 Iterator라는 메소드가 정의되어 있습니다. 

    Iterator<E> iterator()

    이 메소드의 반환형은 Iterator<E>입니다. 이 것은 iterator 메소드가 호출되면 인스턴스가 생성되고, 이 인스턴스는 Iterator<E> 인터페이스를 구현하는 클래스의 인스턴스라는 것입니다. 그리고 이 iterator 메소드는 그 인스턴스의 참조값을 반환합니다. 메소드의 반환형이 인터페이스 Iterator<E>로 선언되어 있다는 것은 필요에 따라서 Iterator<E> 인터페이스에 정의된 메소드만 호출하면 된다는 것입니다. 그렇기 때문에 Iterator<E>를 반환형으로 선언되어 있다면 우리Iterator<E>인터페이스에 정의되어 있는 메소드만 신경쓰면 되는 것입니다. 

     Iterator<E> 인터페이스에 정의되어 있는 메소드와 사용법은 다음과 같습니다. 

    Type and Method Description ( 설명 )
    boolean hasNext() 참조할 다음 번 요소가 존재하면 true를 반환한다.
    E next() 다음 번 요소를 반환한다.
    void remove() 현재 위치의 요소를 삭제한다.
    import java.util.Iterator;
    import java.util.LinkedList;
    
    class HowToUseIterator {
    	public static void main ( String[] args ) {
        		LinkedList<String> list = LinkedList<String>();
            	list.add("One");
            	list.add("Two");
            	list.add("Three");
            	list.add("Four");
            
            	
                	// iterator 메소드가 실행되면서 반복자(iterator)라는 인스턴스를 생성한다.
            	// 반복자를 생성한 후 반복자의 참조값을 i에 저장한다.
            	Iterator<String> i = list.iterator();
                
                	System.out.println("출력과 \"Three\" 삭제 ");
            	// 반복자는 hasNext를 통해 컬렉션인스턴스의 첫 공간을 탐색하고, 그곳에 유효한 값이 존재하면 true를 반환한다.
                	while(i.hasNext()) {  
                    
                    	// next 메소드는 현재 반복자가 탐색하고 있는 공간에 저장된 데이터를 반환한다.
                    	// 또한, next 메소드는 탐색 위치를 다음 번 요소로 이동시킨다.
            		String str = i.next();  
                		System.out.println(str);
                		if(str.compareTo("Three")==0) i.remove();
            	}
            
            	System.out.println("\"Three\" 삭제 후 출력");
            	i = list.iterator();
            	while(i.hasNext()) {
                		System.out.println(i.next());
            	}
        	}
    }
    실행 결과
    출력과 Three 삭제
    One
    Two
    Three
    Four
    Three 삭제 후 출력
    One
    Two
    Four

     

    Iterator(반복자)를 사용하는 이유

     반복자를 사용하는 이유는 컬렉션클래스의 종류에 상관없이 동일한 형태의 참조방식을 유지하기 위해서 입니다. LinkedList<E>클래스의 참조방식은 get 메소드를 사용하는 것입니다. 이처럼 get 메소드를 사용하는 것은 데이터의 저장순서가 유지되기 때문입니다. 하지만 데이터의 저장순서가 유지되지 않는 클래스의 경우에는 get 메소드가 정의되어 있지 않습니다. 이러한 상황에서 반복자는 저장된 데이터 전부를 참조할 때 매우 유용합니다. 그렇기 때문에 컬렉션 클래스들은 반복자를 반환하는 iterator 메소드를 구현하고 있습니다. 

     간혹 LinkedList<E> 클래스를 사용해 코드를 작성했는데, 작성하고 보니 LinkedList보다 다른 클래스가 더 적절하게 사용되는 상황이 있을 수 있습니다. 이런 상황에서 반복자를 사용해 코드를 작성했다면 컬렉션 클래스의 교체만으로 코드 수정을 완료할 수 있는 것입니다. 

     

    Collection 클래스를 사용헤 int형 정수 저장하기

    컬렉션 클래스를 사용해서 기본 자료형을 저장하는 것은 생각만큼 간단하지 않습니다. 기본 자료형을 기반으로 제네릭 인스턴스를 생성할 수 없기 때문입니다. 이런 상황에서 사용할 수 있는 것은 Wrapper 클래스와 Auto Boxing & Auto Unboxing 입니다. 

    import java.util.Iterator;
    import java.util.LinkedList;
    
    class CollectionInt {
    	public static void main ( String[] args ) {
        		
                	//아래처럼 기본 자료형을 기반으로 제네릭 인스턴스 생성은 불가능하다. 
                	LinkedList<int> list = LinkedList<int>();
        		
                	//기본자료형을 저장하는 컬렉션 인스턴스 생성 대신 int형의 Wrapper클래스인 Ingeter의 인스턴스를 저장한다.
                	LinkedList<Integer> list = LinkedList<Ingeter>();
            	list.add(1); // Auto Boxing 이 이뤄진다.
            	list.add(2); // Auto Boxing 이 이뤄진다.
            	list.add(3); // Auto Boxing 이 이뤄진다.
            	list.add(4); // Auto Boxing 이 이뤄진다.
            
            	Iterator<Integer> i = list.iterator();
                
                	while(i.hasNext()) {  
                    
                    	int num = i.next();  // Auto Unboxing 이 이뤄진다.
                		System.out.println(num);
                		
            	}
            
            	
        	}
    }

     

    댓글

Camel`s Tistory.