[Java] 자바 101 기초 강의 #12 ( Set형 )

    자바의 Set형은 List와 유사한 형제같은 데이터형입니다.

     

    그림 1. Collection에 묶인, Set, List, Queue

     

    그림 1에서 보다시피 Set과 List는 Collection이라는 interface의 하위 interface입니다. 그러다보니 list와 set은 사용법이 유사합니다.

     

    List와 Set의 차이점

    List와 Set의 차이점은 List는 데이터를 저장하고 출력하는 것에 포커스를 둬서 동일한 데이터도 저장할 수 있다면, Set의 경우 데이터를 탐색하고, 중복되는 데이터를 제거하는 등의 기능이 있습니다.

     

    리스트 예시

    public static void main(String[] args) {
        String[] arrs = {"블랙핑크","BTS","블랙핑크"};
        List<String> list = new ArrayList<>(Arrays.asList(arrs));
    
        System.out.println("중복된 것도 나오는 리스트 -> " + list);
    }
    
    # 중복된 것도 나오는 리스트 -> [블랙핑크, BTS, 블랙핑크]

    리스트는 위의 경우 출력 예시처럼 블랙핑크라는 동일한 값을 출력합니다. 

     

    Set 예시

    public static void main(String[] args) {
        String[] arrs = {"블랙핑크","BTS","블랙핑크"};
        Set<String> setData = new HashSet<>(Arrays.asList(arrs));
    
        System.out.println("중복된 것은 제거하는 셋 -> " + setData);
    }
    
    # 중복된 것은 제거하는 셋 -> [BTS, 블랙핑크]

    하지만 Set은 기본적으로 중복을 제거하게 됩니다.

     

     

    Set 종류

    HashSet

    HashSet은 Set에서 가장 많이 사용하며, 내부적으로 해시 알고리즘(Hash algorithm)을 사용하기 때문에 검색 속도가 빠릅니다. 이는 Map의 구현 객체중 HashMap과도 동일합니다.

     

    즉, HashSet의 사용 목적은 중복된 데이터를 저장하지 않고, 데이터를 빠르게 탐색하기 위함입니다.

     

    public static void main(String[] args) {
        List<String> list1 = new ArrayList<>();
        List<String> list2 = new ArrayList<>();
        
        for(int i = 0; i < 100000; i++) {
            list1.add("data" + i);
            list2.add("data" + i);
        }
    
        Collections.shuffle(list2);
        System.out.println("list1 n10 ->" + list1.subList(0, 10));
        System.out.println("list2 n10 ->" + list2.subList(0, 10));
    }

    우선 HashSet의 비교를 위해서, 리스트1,2 데이터를 다음과 같이 세팅하였고, list1을 loop를 돌려서 list2에 데이터를 찾는 시간을 확인해보도록 하겠습니다.

     

    list1과 list2의 top10 데이터

     

    리스트 탐색 속도

    public static void scanList(List<String> list1, List<String> list2) {
        long startTime = System.currentTimeMillis();
        int scanCount = 0;
        for(String str : list1) {
            if(list2.contains(str)) {
                scanCount++;
            }
        }
        System.out.println("scan list -> (" + scanCount + "), elapsed -> " + (System.currentTimeMillis()-startTime) + "(ms)");
    }
    
    # scan list -> (100000), elapsed -> 91816(ms)

     

    10만개의 리스트를 탐색하는데 걸린 시간은 무려 91초가 걸렸습니다. list의 경우 index가 안되어 있기 때문에 데이터를 직접 모두 뒤져야 하기 때문입니다.

     

    그럼 탐색 대상인 list2를 HashSet으로 변경해보겠습니다.

     

    HashSet 탐색 속도

    public static void main(String[] args) {
        List<String> list1 = new ArrayList<>();
        Set<String> setData = new HashSet<>();
    
        for(int i = 0; i < 100000; i++) {
            list1.add("data" + i);
            setData.add("data" + i);
        }
    
        scanList(list1, setData);
    }
    
    public static void scanList(List<String> list1, Set<String> setData) {
        long startTime = System.currentTimeMillis();
        int scanCount = 0;
        for(String str : list1) {
            if(setData.contains(str)) {
                scanCount++;
            }
        }
        System.out.println("scan list -> (" + scanCount + "), elapsed -> " + (System.currentTimeMillis()-startTime) + "(ms)");
    }
    
    # scan list -> (100000), elapsed -> 14(ms)

     

    HashSet으로 변경을 하니, 14 밀리세컨드로 6558배 빨라졌습니다. 이 속도는 데이터가 많아지면 많아질수록 차이는 더욱 커집니다.

     

     

    TreeSet

    TreeSet은 HashSet의 강점을 어느정도 가져간 상태에서 데이터를 정렬하는 기능을 추가하였습니다.

     

     

    public static void main(String[] args) {
        List<String> list1 = new ArrayList<>();
        Set<String> setData = new TreeSet<>();
    
        for(int i = 0; i < 100000; i++) {
            list1.add("data" + i);
            setData.add("data" + i);
        }
    
        scanList(list1, setData);
    }
    
    public static void scanList(List<String> list1, Set<String> setData) {
        long startTime = System.currentTimeMillis();
        int scanCount = 0;
        for(String str : list1) {
            if(setData.contains(str)) {
                scanCount++;
            }
        }
        System.out.println("scan list -> (" + scanCount + "), elapsed -> " + (System.currentTimeMillis()-startTime) + "(ms)");
    }
    
    # scan list -> (100000), elapsed -> 24(ms)

     

    HashSet에서 TreeSet으로 변경 후 위와 같은 예제를 실행하니, 14ms에서 24ms 정도로 약간 느려진 것을 확인할 수 있습니다. 하지만 list에 비해서 3825배 더 빠른 속도를 보여주고 있습니다.

     

    public static void main(String[] args) {
        Set<String> hashSetData = new HashSet<>();
        Set<String> treeSetData = new TreeSet<>();
    
        // 100만
        long startTime = System.currentTimeMillis();
        for(int i = 0; i < 1000000; i++) {
            hashSetData.add("data" + i);
        }
        System.out.println("hash set elapsed -> " + (System.currentTimeMillis()-startTime));
    
        startTime = System.currentTimeMillis();
        for(int i = 0; i < 1000000; i++) {
            treeSetData.add("data" + i);
        }
        System.out.println("tree set elapsed -> " + (System.currentTimeMillis()-startTime));
        
        int i = 0; 
        for(String hashStr : hashSetData) {
            System.out.println(hashStr);
            if(++i >= 10) break;
        }
    
        i = 0;
        for(String treeStr : treeSetData) {
            System.out.println(treeStr);
            if(++i >= 10) break;
        }
    }

     

    hashset, treeset 실행결과

    hash set elapsed -> 121
    tree set elapsed -> 259
    data581470
    data581471
    data581472
    data581473
    data581474
    data581475
    data581476
    data581477
    data581478
    data581479
    data0
    data1
    data10
    data100
    data1000
    data10000
    data100000
    data100001
    data100002
    data100003

    이번엔 10만건이 아니라 100만건으로 테스트를 하였고, 10건의 데이터를 순서대로 가져와봤습니다. HashSet의 경우 데이터의 순서를 우리가 알 수 없지만, TreeSet의 경우 데이터가 오름차순 형태로 저장이 된 것을 확인할 수 있습니다. 즉, 아래와 같이 오름차순 형태로 데이터를 확인해야 된다면 TreeSet이 유리할 것입니다.

     

    LinkedHashSet

    HashSet이 순서대로 저장이 되지 않는 것이 문제라면 사실 LinkedHashSet을 사용하면 됩니다. HashSet인 상태에서 데이터를 순서대로 저장을 한 형태라 보시면 됩니다.

     

    public static void main(String[] args) {
        Set<String> hashSetData = new HashSet<>();
        Set<String> treeSetData = new TreeSet<>();
        Set<String> linkedHashSetData = new LinkedHashSet<>();
    
        // 100만
        long startTime = System.currentTimeMillis();
        for(int i = 0; i < 1000000; i++) {
            hashSetData.add("data" + i);
        }
        System.out.println("hash set elapsed -> " + (System.currentTimeMillis()-startTime));
    
        startTime = System.currentTimeMillis();
        for(int i = 0; i < 1000000; i++) {
            treeSetData.add("data" + i);
        }
        System.out.println("tree set elapsed -> " + (System.currentTimeMillis()-startTime));
    
        startTime = System.currentTimeMillis();
        for(int i = 0; i < 1000000; i++) {
            linkedHashSetData.add("data" + i);
        }
        System.out.println("linked hash set elapsed -> " + (System.currentTimeMillis()-startTime));
    
        System.out.println("hash set data.....................................");
        int i = 0;
        for(String hashStr : hashSetData) {
            System.out.println(hashStr);
            if(++i >= 5) break;
        }
    
        System.out.println("tree set data.....................................");
        i = 0;
        for(String treeStr : treeSetData) {
            System.out.println(treeStr);
            if(++i >= 5) break;
        }
    
        System.out.println("linked hash set data.....................................");
        i = 0;
        for(String linkedStr : linkedHashSetData) {
            System.out.println(linkedStr);
            if(++i >= 5) break;
        }
    }

     

    hashset, treeset, linkedhashset 실행결과

    hash set elapsed -> 124
    tree set elapsed -> 243
    linked hash set elapsed -> 126
    hash set data.....................................
    data581470
    data581471
    data581472
    data581473
    data581474
    tree set data.....................................
    data0
    data1
    data10
    data100
    data1000
    linked hash set data.....................................
    data0
    data1
    data2
    data3
    data4

    TreeSet의 경우 데이터의 값을 기준으로 오름차순 하지만, linked hash set의 경우 데이터가 저장된 순서 그대로 가져올 수 있습니다.

     

    우리가 값을 오름차순하여 가져와야 되는 경우가 사실 많지 않기 때문에 데이터의 순서가 문제가 없다면, HashSet을 쓰거나 데이터의 순서가 중요하다면 LinkedHashSet을 사용하는 것이 일반적입니다. 

     

     

    Set 메소드 [1]

    메소드 설명
    boolean add(E)
    지정된 요소가 아직 없는 경우 이 집합에 추가합니다 (선택)
    boolean addAll(Collection)
    지정된 컬렉션의 모든 요소가 아직 없는 경우 이 집합에 추가합니다 (선택)
    void clear()
    집합에서 모든 요소를 ​​제거합니다 (선택)
    boolean contains(Object)
    집합에 지정된 요소가 포함되어 있으면 true를 반환 합니다.
    boolean containsAll(Object)
    집합에 지정된 컬렉션의 모든 요소가 포함되어 있으면 true를 반환 합니다.
    boolean equals(Object)
    지정된 개체를 이 집합과 동일한지 비교합니다.
    int hashCode()
    집합에 대한 해시 코드 값을 반환합니다.
    boolean isEmpty()
    집합에 요소가 없으면 true를 반환 합니다.
    Iterator<E> iterator()
    세트의 요소에 대한 반복자를 반환합니다.
    boolean remove(Object)
    집합에서 지정된 요소가 있는 경우 제거합니다 (선택)
    boolean removeAll(Collection)
    지정된 컬렉션에 포함된 모든 요소를 ​​집합에서 제거합니다 (선택)
    boolean retainAll(Collection) 지정된 컬렉션에 포함된 집합의 요소만 유지합니다 (선택)
    int size() 이 집합의 개수를 반환합니다.
    default Spliterator<E> spliterator()
    Spliterator 세트의 요소를 통해 생성합니다 .
    Object[] toArrays()
    세트의 모든 요소를 ​​포함하는 배열을 반환합니다.
    <T> T[] toArray(T[]) 세트의 모든 요소를 포함하는 배열을 반환합니다. 
    반환된 배열의 런타임 유형은 지정된 배열의 런타임 유형입니다.

     

    연관포스팅

    [Java] 자바 101 기초 강의 #11 ( List형 )

     

    [Java] 자바 101 기초 강의 #11 ( List형 )

    오늘은 List형에 대해서 알려드리도록 하겠습니다. 혹시 Python과 같이 자유로운 언어를 배우신 분들이라면, 자바의 List는 좀 어려울 수 있습니다. 파이썬과 다르게 좀 더 엄격하기 때문입니다. List

    needneo.tistory.com

     

    References

    [1] https://docs.oracle.com/javase/8/docs/api/java/util/Set.html

    반응형

    댓글

    Designed by JB FACTORY