Java 재활 훈련 12일차 - Collection

0

java

목록 보기
12/18

Collection

collection framework는 몇 가지 인터페이스를 통해서 다양한 collection class를 이용할 수 있도록 설계되어 있다. 주요 인터페이스로 List, Set, Map이 있는데, 이 인터페이스로 사용 가능한 collection 객체의 종류는 다음과 같다.

      collection
          ^
          |
    ------------            Map
    ^          ^             ^
    |          |             |
  List        Set            |
    |          |           HashMap
ArrayList     HashSet      Hashtable
Vector        TreeSet      TreeMap
LinikedList                Properties

ListSet interface는 사용 방법이 같다. 둘 다 추가, 삭제, 변경에 대해서 인터페이스가 같다. 단지, Set은 집합의 개념으로 중복을 허용하지 않고 List는 중복을 허용한다.

List collection

List collection은 저장하는 객체를 index로 관리하기 때문에 객체를 저장하면 인덱스가 부여되고, 인덱스로 객체를 검색, 삭제할 수 있는 기능을 제공한다.

기능메서드설명
객체 추가boolean add(E e)주어진 객체를 맨 끝에 추가
객체 추가void add(int index, E element)주어진 객체를 index에 추가
객체 추가set(int index, E element)주어진 인덱스의 객체를 새로운 객체로 바꿈
객체 검색boolean contains(Object o)주어진 객체가 저장되어 있는 지 확인
객체 검색E get(int index)주어진 인덱스에 저장된 객체를 리턴
객체 검색boolean isEmpty()컬렉션이 비어있는 지 조사
객체 검색int size()저장되어 있는 전체 객체 수를 리턴
객체 삭제void clear()저장된 모든 객체를 삭제
객체 삭제E remove(int index)주어진 인덱스에 저장된 객체를 삭제
객체 삭제boolean remove(Object o)주어진 객체를 삭제

한 가지 조심해야할 것은 객체를 저장할 때 List는 객체를 복사해서 저장하는 것이 아니라, 객체의 주소를 저장하는 것이다. 따라서, 동일한 객체를 중복 저장하면 동일한 주소가 저장되는 것이다. null 또한 저장이 가능하다.

ArrayList

ArrayList는 객체를 추가하면 내부 배열에 객체가 저장된다. 일반 배열과의 차이점은 제한 없이 객체를 추가할 수 있다는 것이다. ArrayList의 생성은 다음과 같다.

List<E> list = new ArrayList<E>();
List list = new ArrayList(); 

type parameter E에는 ArrayList에 저장하고 싶은 객체 타입을 지정하면 된다.

ArrayList는 인덱스 0번부터 차례대로 저장한다. 특정 인덱스의 객체를 제거하면 바로 뒤 인덱스부터 마지막 인덱스까지 모두 앞으로 당겨지므로 O(n)시간이 걸린다.

따라서, 빈번한 객체의 삽입, 삭제가 있다면 LinkedList를 쓰는 것이 좋다.

아래는 ArrayList의 사용 예제이다.

  • Main
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // ArrayList collection
        List<Board> list = new ArrayList<Board>();

        // add object
        list.add(new Board("0"));
        list.add(new Board("1"));
        list.add(new Board("2"));
        list.add(new Board("3"));
        list.add(new Board("4"));

        int size = list.size();
        System.out.println("Total Object: " + size); // 5
        System.out.println();

        Board board = list.get(2);
        System.out.println(board.content); // 2
        System.out.println();

        list.remove(2); // 2
        list.remove(2); // 3

        for(Board b: list) {
            System.out.println(b.content); // 0 1 4
        }
    }
}

Vector

VectorArrayList와 동일한 내부 구조를 가지지만 Vector는 동기화된 메서드로 구성되어 있기 때문에 멀티 thread가 동시에 Vector 메서드를 실행할 수 없다. 반대로 이야기하자면 ArrayList는 동기화가 안되기 때문에 멀티 thread에서 동시에 접근 시에 문제가 발생할 수 있다.

List<E> list = new Vector<E>();
List list = new Vector();

다음은 ThreadAThreadB에서 동시에 Board 객체를 접근해서 1000개씩 추가한 후, 전체 저장된 수를 출력하는 예제이다.

  • Main
public class Main {
    public static void main(String[] args) {
        // vector collection
        List<Board> list = new Vector<>();

        Thread threadA = new Thread() {
            @Override
            public void run() {
                for(int i = 1; i <= 1000; i++) {
                    list.add(new Board("content" + i));
                }
            }
        };

        Thread threadB = new Thread() {
            @Override
            public void run() {
                for(int i = 1001; i <= 2000; i++) {
                    list.add(new Board("content" + i));
                }
            }
        };

        threadA.start();
        threadB.start();

        try {
            threadA.join();
            threadB.join();
        } catch (Exception e) {
            e.printStackTrace();
        }

        int size = list.size();
        System.out.println("total object: " + size); // total object: 2000
    }
}

Vector에서 ArrayList로 바꿔서 실험해보면 total object값이 이상하게 나올 것이다.

LinkedList

LinkedListArrayList와 사용 방법은 동이하지만 내부 구조는 완전히 다르다. ArrayList는 내부 배열에 객체를 저장하는 동적 배열이지만, LinkedList는 인접 객체를 체인처럼 연결해서 관리한다.

LinkedList 특성 상 객체를 삽입하거나, 삭제하면 바로 앞 뒤 링크만 변경하면 되므로 빈번한 객체 삽입, 삭제에 있어서 O(1)시간이 걸린다.

public class Main {
    public static void main(String[] args) {
        List<String> list1 = new ArrayList<String>();
        List<String> list2 = new LinkedList<String>();

        long startTime, endTime;

        startTime = System.nanoTime();
        for(int i = 0; i < 100000; i++) {
            list1.add(0, String.valueOf(i)); // insert
        }

        endTime = System.nanoTime();
        System.out.printf("%-17s %8d ns \n", "ArrayList time: ", (endTime - startTime));

        startTime = System.nanoTime();
        for(int i = 0; i < 100000; i++) {
            list2.add(0, String.valueOf(i)); // insert
        }

        endTime = System.nanoTime();
        System.out.printf("%-17s %8d ns \n", "LinkedList time: ", (endTime - startTime));
    }
}

위 코드는 ArrayListLinkedList 둘의 차이를 확연히 보여주는 예제로, 성능 테스트 결과를 보여준다. ArrayList로 삽입을 실행 시에 매번 O(N)시간이 걸리는 반면에 LinkedListO(1)시간이 걸려서 더 빠르다.

ArrayList time:   416353140 ns 
LinkedList time:   5442651 ns 

약 8배는 더 빠른 걸로 나온다.

Set

List collection은 저장 순서를 유지하지만, Set은 저장 순서를 유지하지않는다. 또한 객체를 중복해서 저장할 수 없고, 하나의 null만 저장할 수 있다. Set은 집합 개념이기 때문이다. 따라서, 정리하자면 순서도 상관없고, 중복도 허용하지 않는다. 순서도 없기 때문에 index로 관리하지 않는다.

Set에는 HashSet, LinkedHashSet, TreeSet 등이 있는데, Set collection에서 공통으로 사용하는 인터페이스는 다음과 같다.

기능메서드설명
객체 추가boolean add(E e)주어진 객체를 성공적으로 저장하면 true를 반환하고, 중복 객체면 false를 반환
객체 검색boolean contains(Object o)주어진 객체가 저장되어 있는 지 여부
객체 검색isEmpty()collection이 비어있는 지 조사
객체 검색iterator iterator()저장된 객체를 한 번씩 가져오는 iterator 반환
객체 검색int size()저장되어 있는 전체 객체 수 반환
객체 삭제void clear()저장된 모든 객체를 삭제
객체 삭제boolean remvoe(Object o)주어진 객체를 삭제

HashSet

Set collection 중에서 가장 많이 사용되는 것이 HashSet이다. 다음은 HashSet collection을 생성하는 방법이다.

Set<E> set = new HashSet<E>();
Set set = new HashSet();

HashSet은 동일한 객체는 중복 저장하지 않는다. 동일하다는 의미의 '동등'하다는 말은 HashSet은 다른 객체라도 hashCode 메서드의 반환값이 같고, equals 메서드가 true를 반환하면 동일한 객체라고 판단하고 중복 저장하지 않는다.

  1. hashCode() 반환값이 동일
  2. equals() 반환값이 동일

문자열을 HashSet에 저장할 경우, 같은 문자열을 갖는 String 객체는 동등한 객체로 간주한다. 같은 문자열이면 hashCode의 반환값이 같고, equals의 반환값이 true로 나온다.

  • Main
public class Main {
    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();

        set.add("Java");
        set.add("JDBC");
        set.add("JSP");
        set.add("Java"); // duplicated
        set.add("Spring");

        System.out.println("total object: " + set.size()); // 4
    }
}

아래의 예제는 이름과 나이가 동일한 경우 Member객체를 HashSet에 중복 저장하지 않는다. Member class를 선언할 때 이름과 나이가 동일한 hashCode가 반환되도록 hashCode를 오버라이드하고, equals 메서드가 true를 반환하도록 오버라이드했다.

  • Member
public class Member {
    public String name;
    public int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof Member target) {
            return target.name.equals(name) && (target.age == age);
        } else {
           return false;
        }
    }

    @Override
    public int hashCode() {
        return name.hashCode() + age;
    }
}

equalshashCore를 오버라이드하여 nameage가 같은 member에 대해서는 같은 객체로 판단한다.

public class Main {
    public static void main(String[] args) {
        Set<Member> set = new HashSet<Member>();
        set.add(new Member("Hong", 30));
        set.add(new Member("Park", 30));
        set.add(new Member("Hong", 30));

        System.out.println("total object: " + set.size()); // total object: 2
    }
}

총 저장 객체 수로 2개가 나온다.

Set collection은 index로 객체를 가져오지 못하므로 검색을 위해서는 for문으로 순회를 돌아야한다.

Set<E> set = new HashSet<>();
for(E e: set) {
    ...
}

다른 방법으로는 Set collection의 iterator()메서드로 iterator를 얻어 객체를 하나씩 가져오는 것이다. type parameter E는 Set collection에 저장되어 있는 개체 타입이다.

Set<E> set = new HashSet<>();
Iterator<E> iterator = set.iterator();

iteratorSet collection의 객체를 가져오거나 제거하기 위해 다음 메서드를 제공한다.

반환 타입메서드 명설명
booleanhasNext()가져올 객체가 있으면 true, 없으면 false
Enext()객체를 하나 가져온다.
voidremove()객체를 Set에서 삭제한다.
  • Main
public class Main {
    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();

        set.add("Java");
        set.add("JDBC");
        set.add("JSP");
        set.add("Spring");

        Iterator<String> iterator = set.iterator();
        while(iterator.hasNext()) {
            String elem = iterator.next();
            System.out.println(elem);

            if(elem.equals("JSP")) {
                iterator.remove();
            }
        }
        System.out.println();

        set.remove("JDBC");
        for(String elem: set) {
            System.out.println(elem);
        }
    }
}

HashSet을 반복하여 JSQPJDBC 값을 가진 객체를 삭제한다. 결과는 다음과 같다.

Java
JSP
JDBC
Spring

Java
Spring

Map

Map collection은 key-value로 이루어진 entry에 객체를 저장한다. key는 중복 저장할 수 없지만, 값은 중복 저장할 수 있다. 즉, 기존의 저장된 key와 동일한 key로 값으 저장하면 기존의 값은 없어지고 새로운 값으로 대치된다.

Map collection은 HashMap, Hashtable, LinkedHashMap, Properties, TreeMap 등이 있다. Map collection들은 공통적으로 사용 가능한 Map interface method로 다음을 가진다.

기능메서드설명
객체 추가V put(K key, V value)주어진 키와 값을 추가, 저장이 되면 값을 리턴
객체 검색boolean containsKey(Object key)주어진 키가 있는 지 여부
객체 검색boolean containsValue(Object value)주어진 값이 있는 지 여부
객체 검색Set<Map.Entry<K,V>> entrySet()키와 값의 쌍으로된 Map.Entry 객체를 Set에 담아서 반환
객체 검색V get(Object key)주어진 키의 값을 반환
객체 검색boolean isEmpty()collection이 비어있는 지 여부
객체 검색Set keySet()모든 key를 Set객체에 담아 반환
객체 검색int size()저장된 키의 총 수 반환
객체 검색Collection values()저장된 모든 값을 Collection에 담아서 반환
객체 삭제void clear()모든 Map.Entry(키와 값)을 삭제
객체 삭제V remove(Object key)주어진 Key와 일치하는 entry 삭제, 삭제 시 값을 반환

HashMap

HashMap은 key로 사용할 객체가 hashCode 메서드의 반환값이 같고, equals 메서드가 true를 반환할 경우, 동일 key로 보고 중복 저장을 허용하지 않는다.

  1. hashCode 반환값
  2. equals 반환값

다음은 HashMap collection을 생성하는 방법이다.

Map<K,V> map = new HashMap<K,V>();

아래는 key가 String이고, 값은 Integer 타입으로 갖는 HashMap이다.

Map<String, Integer> map = new HashMap<String, Integer>();
Map<String, Integer> map = new HashMap<>();

아래는 이름을 key로 점수를 value로 저장하는 HashMap 사용 방법이다.

  • Main
public class Main {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();

        map.put("Hong", 85);
        map.put("Park", 90);
        map.put("Hong", 85); // duplicated
        map.put("Lee", 95);

        String key = "Hong";
        System.out.println(key + ": " + map.get(key));

        Set<String> keySet = map.keySet();
        for(String k : keySet) {
            System.out.println("Key: " + k + " Value: " + map.get(k));
        }
    }
}

결과는 아래와 같다.

Hong: 85
Key: Hong Value: 85
Key: Lee Value: 95
Key: Park Value: 90

참고로 HashMap은 key-value entry를 입력한 순서를 지키지 않는 특성을 가지는데, 순서를 유지하고 싶다면 LinkedHashMap을 사용하면 된다. 나머지 사용 방법은 같지만, LinkedHashMap은 linked list로 입력된 entry들의 앞 뒤를 기억한다.

Hashtable

HashtableHashMap과 동일한 내부 구조를 가진다. 차이점은 Hashtable은 동기화된 메서드로 구성되어있다는 것으로 멀티 thread가 동시에 Hashtable 메서드을 실행할 수 없다. 따라서, 멀티 thread 환경에서도 안전하게 객체를 추가, 삭제할 수 있다.

Map<String, Integer> map = new Hashtable<String, Integer>();
Map<String, Integer> map = new Hashtable<>(); 

아래는 두 개의 thread들이 Hashtable에 접근하는 코드이다.

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> map = new Hashtable<>();

        Thread threadA = new Thread() {
            @Override
            public void run() {
                for(int i = 1; i <= 1000; i++) {
                    map.put(String.valueOf(i), i);
                }
            }
        };

        Thread threadB = new Thread() {
            @Override
            public void run() {
                for(int i = 1001; i <= 2000; i++) {
                    map.put(String.valueOf(i), i);
                }
            }
        };

        threadA.start();
        threadB.start();

        try {
            threadA.join();
            threadB.join();
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("total entries: " + map.size());
    }
}

실행결과를 보면 딱 2000개가 나온다. 만약 HashMap을 사용했다면 thread들끼리의 경합 때문에 제대로 결과가 나오지 않는다.

total entries: 2000

Properties

PropertiesHashtable의 자식 클래스이기 때문에 Hashtable의 특징을 그대로 가지고 있다. Properties는 key와 value을 String으로 고정한 collection이다. Properties는 주로 .properties 인 프로퍼티 파일을 읽을 때 사용한다.

properties는 일반 파일과 다르게 ISO 8859-1문자셋으로 저장된다.

  • database.properties
driver=oracle.jdbc.OracleDriver
url=jdbc:oracle:thin:@localhost:1521:orcl
username=scott
password=tiger

Properties를 사용하면 위와 같은 properties 파일의 내용을 쉽게 읽을 수 있다. 먼저 Properties 객체를 생성하고, load 메서드로 properties 파일의 내용을 메모리로 로드한다.

Properties properties = new Properties();
properties.load(Xxx.class.getResourceAsStream("database.properties"));

일반적으로 properties 파일은 class 파일과 함께 저장된다. 따라서, class파일을 기준으로 상대 경로를 이용해서 읽는 것이 편리하다. class 객체의 getResourceAsStream 메서드는 주어진 상대 경로의 resource 파일을 읽는 InputStream을 반환한다.

  • Main
public class Main {
    public static void main(String[] args) {
        try {
            Properties properties = new Properties();
            properties.load(Main.class.getResourceAsStream("database.properties"));

            String driver = properties.getProperty("driver");
            String url = properties.getProperty("url");
            String username = properties.getProperty("username");
            String password = properties.getProperty("password");

            System.out.println("driver: " + driver); // driver: oracle.jdbc.OracleDriver
            System.out.println("url: " + url); // url: jdbc:oracle:thin:@localhost:1521:orcl
            System.out.println("username: " + username); // username: scott
            System.out.println("password: " + password); // password: tiger
        } catch (IOException ie) {
            ie.printStackTrace();
        }
    }
}

Main class를 기준으로 경로를 탐색하기 때문에 database.propertiesMain class와 같은 path에 있으면 properties를 읽어온다.

검색 기능을 강화시킨 collection

검색 기능이 매우 효율적인 collection으로 TreeSetTreeMap이 존재한다. TreeSetSet collection의 하위이고, TreeMapMap collection의 하위이다.

TreeSet

TreeSet은 이진 트리를 기반으로 한 Set collection이다. 여러 개의 node가 tree형태로 연결된 구조인 것이다.

                root node
              /          \
      left child          right child
      /        \         /          
  child1       child2  child3       

TreeSet에 객체를 저장하면 다음과 같이 자동으로 정렬된다. 부모 노드의 객체와 비교해서 낮은 것은 왼쪽 자식 노드에, 높은 것은 오른쪽 자식 노드에 저장한다.

TreeSet<E> treeSet = new TreeSet<E>();

TreeSetSet이 없는 메서드들도 많이 가지고 있는데, 다음은 TreeSet이 가지고 있는 검색 관련 메서드들이다.

메서드설명
E first()제일 낮은 객체를 반환
E last()제일 높은 객체를 반환
E lower(E e)주어진 객체보다 바로 아래 객체를 반환
E higher(E e)주어진 객체보다 바로 위 객체를 반환
E floor(E e)주어진 객체와 동등한 객체가 있으면 반환, 만약 없다면 주어진 객체의 바로 아래의 객체를 반환
E ceiling()주어진 객체와 동등한 객체가 있으면 반환, 만약 없다면 주어진 객체의 바로 위의 객체를 반환
E pollFirst()제일 낮은 객체를 꺼내오고 collection에서 제거
E pollLast()제일 높은 객체를 꺼ㅐ오고 collection에서 제거
Iterator descendingIterator()내림차순으로 정렬된 Iterator를 반환
NavigableSet descendingSet()내림차순으로 정렬된 NavigableSet 반환
NavigableSet headSet(E to, boolean inclusive)주어진 객체보다 낮은 객체들을 NavigableSet으로 반환, 주어진 객체 포함 여부는 두 번째 매개값에 따라 달라짐
NavigableSet tailSet(E from, boolean inclusive)주어진 객체보다 높은 객체들을 NavigableSet으로 반환, 주어진 객체 포함 여부는 두 번째 매개값에 따라 달라짐
NavigableSet subSet(E from, boolean frominclusive, E to, boolean toinclusive)시작과 끝으로 주어진 key 사이의 Map.Entry 들을 NavigableSet으로 반환, 시작과 끝 객체의 포함 여부는 두번째, 네번째 값에 따라 달라진다.

다음은 무작위로 저장한 점수를 검색하는 방법을 보여준다.

  • Main
public class Main {
    public static void main(String[] args) {
        TreeSet<Integer> scores = new TreeSet<>();

        scores.add(87);
        scores.add(98);
        scores.add(75);
        scores.add(95);
        scores.add(80);

        for (Integer s : scores) {
            System.out.print(s + " "); // 75 80 87 95 98
        }
        System.out.println();

        System.out.println("first score: " + scores.first()); // first score: 75
        System.out.println("high score: " + scores.last()); // high score: 98
        System.out.println("under 95 score " + scores.lower(95)); // under 95 score 87
        System.out.println("higher 95 score " + scores.higher(95)); // higher 95 score 98
        System.out.println("95거나 95보다 바로 아래 score " + scores.floor(95)); // 95 or under 95 score 95
        System.out.println("85거나 85보다 바로 위 score " + scores.ceiling(85)); // higher 85 or upper score 87

        NavigableSet<Integer> descendingScores = scores.descendingSet();
        for(Integer s : descendingScores) {
            System.out.print(s + " "); // 98 95 87 80 75
        }
        System.out.println();

        NavigableSet<Integer> rangeSet = scores.tailSet(80, true);
        for(Integer s: rangeSet) {
            System.out.print(s + " "); // 80 87 95 98
        }
        System.out.println();

        // 80 <= score < 90
        rangeSet = scores.subSet(80, true, 90, false);
        for (Integer s: rangeSet) {
            System.out.println(s + " "); // 80 87
        }
    }
}

최대, 최소, 범위 검색 등이 가능한 것을 볼 수 있다.

TreeMap

TreeMap은 이진 트리를 기반으로 한 Map collection으로 TreeSet과의 차이점은 key-value이 저장된 Entry값을 저장한다는 점이다. 이때 key를 기준으로 자동 정렬되며, 부모 키와 비교해서 낮은 것은 왼쪽, 높은 것은 오른쪽으로 정렬된다.

TreeMap<K, V> treeMap = new TreeMap<K,V>();

TreeMapMap의 child이기 때문에 Map의 특성을 가지지만, 다음과 같은 TreeMap 메서드들도 있다.

메서드설명
Map.Entry<K,V> firstEntry()제일 낮은 Map.Entry를 반환
Map.Entry<K,V> lastEntry()제일 높은 Map.Entry를 반환
Map.Entry<K,V> lowerEntry(K key)주어진 key보다 바로 아래 Map.Entry를 반환
Map.Entry<K,V> higherEntry(K key)주어진 key보다 바로 위 Map.Entry를 반환
Map.Entry<K,V> floorEntry(K key)주어진 key와 동등한 key가 있으면 해당 Map.Entry를 반환, 없다면 주어진 key 바로 아래의 Map.Entry를 반환
Map.Entry<K,V> ceilingEntry(K key)주어진 key와 동등한 key가 있으면 해당 Map.Entry를 반환, 없다면 주어진 key바로 위의 Map.Entry를 반환
Map.Entry<K,V> pollFirstEntry()제일 낮은 Map.Entry를 반환하고 제거
Map.Entry<K,V> pollLastEntry()제일 높은 Map.Entry를 반환하고 제거
NavigableSet descendingKeySet()내림차순으로 정렬된 key의 NavigableSet을 반환
NavigableMap<K,V> descendingMap()내림차순으로 정렬된 Map.Entry의 NavigableMap을 반환
NavigableMap<K,V> headMap(K to, boolean inclusive)주어진 key보다 낮은 Map.Entry들을 NavigableMap으로 반환, 주어진 key의 Map.Entry 포함 여부는 두 번째 매개값에 따라 달라짐
NavigableMap<K,V> tailMap(K from, boolean inclusive)주어진 객체보다 높은 Map.Entry들을 NavigableMap으로 반환, 주어진 객체 포함 여부는 두 번째 매개값에 따라 달라짐
NavigableMap<K,V> subMap(K from, boolean fromInclusive, K to, boolean toInclusive)시작과 끝으로 주어진 key 사이의 Map.Entry들을 NavigableMap 컬렉션으로 반환, 시작과 끝 key의 Map.Entry 포함 여부는 두 번째, 네 번째 매개값에 따라 달라짐

아래 예제는 영어 단어와 페이지 번호를 무작위로 저장하고 검색하는 방법을 보여준다.

public class Main {
    public static void main(String[] args) {
        TreeMap<String, Integer> treeMap = new TreeMap<>();

        treeMap.put("apple", 10);
        treeMap.put("forever", 60);
        treeMap.put("description", 40);
        treeMap.put("ever", 50);
        treeMap.put("zoo", 80);
        treeMap.put("base", 20);
        treeMap.put("guess", 70);
        treeMap.put("cherry", 30);

        Set<Map.Entry<String, Integer>> entrySet = treeMap.entrySet();
        for(Map.Entry<String,Integer> entry: entrySet) {
            System.out.println(entry.getKey() + "-" + entry.getValue());
        }
        System.out.println();

        Map.Entry<String, Integer> entry = null;
        entry = treeMap.firstEntry();
        System.out.println("First word: " + entry.getKey() + "-" + entry.getValue());
        entry = treeMap.lastEntry();
        System.out.println("Last word: " + entry.getKey() + "-" + entry.getValue());
        entry = treeMap.lowerEntry("ever");
        System.out.println("the word before ever: " + entry.getKey() + "-" + entry.getValue());

        NavigableMap<String, Integer> descendingMap = treeMap.descendingMap();
        Set<Map.Entry<String, Integer>> descendingEntrySet = descendingMap.entrySet();

        for(Map.Entry<String, Integer> e: descendingEntrySet) {
            System.out.println(e.getKey() + "-" + e.getValue());
        }
        System.out.println();

        System.out.println("[search between c and b]");
        NavigableMap<String, Integer> rangeMap = treeMap.subMap("c", true, "h", false);
        for(Map.Entry<String, Integer> e: rangeMap.entrySet()) {
            System.out.println(e.getKey() + "-" + e.getValue());
        }
    }
}

결과는 아래와 같다.

apple-10
base-20
cherry-30
description-40
ever-50
forever-60
guess-70
zoo-80

First word: apple-10
Last word: zoo-80
the word before ever: description-40
zoo-80
guess-70
forever-60
ever-50
description-40
cherry-30
base-20
apple-10

[search between c and b]
cherry-30
description-40
ever-50
forever-60
guess-70

Comparable과 Comparator

TreeSet에 저장되는 객체와 TreeMap에 저장되는 key 객체는 저장과 동시에 오름차순으로 정렬되는데, 어떤 객체든 정렬될 수 있는 것은 아니고, 객체가 Comparable interface를 구현하고 있어야 가능하다. Integer, Double, String 타입은 모두 Comparable을 구현하고 있기 때문에 상관없지만, 사용자 정의 객체를 저장할 때는 반드시 Comparable을 구현하고 있어야 한다.

Comparable interface는 compareTo 메서드가 정의되어 있다.
| 메서드 | 설명 |
|----------------------|--------------------|
| int compareTo(T o) | 같으면 0, 작으면 음수, 크면 양수 |

아래는 나이를 기준으로 Person 객체를 오름차순으로 정렬하기 위해 Comparable 인터페이스를 구현하고 있다. 나이가 적으면 -1, 같으면 0, 크면 1을 반환하도록 정의하였다.

  • Person
public class Person implements Comparable<Person>{
    public String name;
    public int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Person person) {
        return this.age - person.age;
    }
}

Comparable interface를 받아서 compareTo를 구현하고 있다.

  • Main
public class Main {
    public static void main(String[] args) {
        TreeSet<Person> treeSet = new TreeSet<Person>();

        treeSet.add(new Person("Hong", 45));
        treeSet.add(new Person("Java", 25));
        treeSet.add(new Person("Won", 31));

        for(Person person: treeSet) {
            System.out.println(person.name + ":" + person.age); // Java:25 Won:31 Hong:45
        }
    }
}

비교 기능이 있는 Comparable 구현 객체를 TreeSet에 저장하거나, TreeMap의 키로 저장하는 것이 원칙이지만, 비교 기능이 없는 Comparable 비구현 객체를 저장하고 싶다면 TreeSetTreeMap을 생성할 때 비교자(Comparator)를 다음과 같이 제공하면 된다.

TreeSet<E> treeSet = new TreeSet<E>(new ComparatorImpl());

비교자는 Comparator 인터페이스를 구현한 객체를 말하는데, Comparator 인터페이스에는 compare 메서드가 정의되어있다. 비교자는 이 메서드를 재정의해서 비교 결과를 정수 값으로 리턴하면 된다.

메서드설명
int compare(T o1, T o2)동등하면 0, o1이 o2보다 작으면 음수, o1이 o2보다 크면 양수

다음은 Comparable을 구현하고 있지 않은 Fruit 객체를 TreeSet에 저장하는 예제이다. TreeSet을 생성할 때 비교자가 필요한데, FruitComparator를 비교자로 사용해서 가격 기준으로 Fruit 객체를 정렬시킨다.

  • Fruit
public class Fruit {
    public String name;
    public int price;

    public Fruit(String name, int price) {
        this.name = name;
        this.price = price;
    }
}

Comparable interface를 받고 있지 않다.

  • FruitComparator
public class FruitComparator implements Comparator<Fruit> {
    @Override
    public int compare(Fruit fruit1, Fruit fruit2) {
        return fruit1.price - fruit2.price;
    }
}

FruitComparator 객체를 사용하여 TreeSet이나 TreeMap에 전달해주면 FruitcompareTo를 구현하고 있지 않아도 문제없이 비교가 가능하다.

  • Main
public class Main {
    public static void main(String[] args) {
        TreeSet<Fruit> treeSet = new TreeSet<Fruit>(new FruitComparator());

        treeSet.add(new Fruit("apple", 3000));
        treeSet.add(new Fruit("watermelon", 10000));
        treeSet.add(new Fruit("orange", 6000));

        for(Fruit fruit: treeSet) {
            System.out.println(fruit.name +": " + fruit.price);
        }
    }
}

결과는 아래와 같다.

apple: 3000
orange: 6000
watermelon: 10000

LIFO와 FIFO collection

LIFO에 대해서는 Stack을 제공하고, FIFO에 대해서는 Queue를 제공한다. 먼저 Stack에 대해서 알아보자.

메서드설명
E push(E item)주어진 객체를 스택에 넣는다.
E pop()스택의 맨 위의 객체를 빼낸다.
public class Main {
    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<Integer>();
        stack.add(10);
        stack.add(20);
        stack.add(30);
        stack.add(40);

        System.out.println("stack-pop: " + stack.pop()); // 40
        System.out.println("stack-pop: " + stack.pop()); // 30
    }
}

Queue 인터페이스는 FIFO 자료구조에서 사용되는 메서드를 정의하고 있다. 다음은 Queue 인터페이스에 정의되어 있는 메서드이다.

메서드설명
boolean offer(E e)주어진 객체를 큐에 넣는다.
E poll()큐에서 객체를 빼낸다.

Queue 인터페이스를 구현한 대표적인 클래스가 바로 LinkedList이다. 그렇기 때문에 LinkedList 객체를 Queue 인터페이스 변수에 다음과 같이 대입할 수 있다.

Queue<E> queue1 = new LinkedList<E>();

다음은 Queue를 이용한 예제이다.

  • Main
public class Main {
    public static void main(String[] args) {
        Queue<String> queue = new LinkedList<String>();

        queue.offer("Mail");
        queue.offer("Gmail");
        queue.offer("Kakao");

        while (!queue.isEmpty()) {
            String msg = queue.poll();
            System.out.println("Msg: "+ msg); 
            // Mail
            // Gmail
            // Kakao
        }
    }
}

동기화된 collection

collection들 대부분은 single thread 환경에서 사용할 수 있도록 설꼐되었다. 그렇기 때문에 여러 thread가 동시에 collection에 접근하면 원하는 상태로 동작하지 않는 문제가 있다.

Vector, Hashable은 synchronized 메서드로 구성되어 있기 때문에 멀티 thread 환경에서 안전하게 요소를 처리할 수 있지만, ArrayListHashSet, HashMap은 동기화된 메서드로 구성되어 있지 않아, 멀티 스레드 환경에 안전하지 않다.

만약 ArrayList, HashSet, HashMap을 멀티 스레드 환경에서 사용하고 싶다면, 비동기화된 메서드를 동기화된 메서드로 wrapping하는 synchronizedList, synchronizedMap, synchronizedSet 메서드를 사용하면 된다.

메서드설명
List synchronizedList(List l)List를 동기화된 List로 반환
Map<K,V> synchronizedMap(Map<K,V> m)Map을 동기화된 Map으로 반환
Set synchronizedSet(Set s)Set를 동기화된 Set로 반환

이 메서드들은 매개값으로 비동기화된 collection을 대입하면 동기화된 collection을 반환한다. 다음은 ArrayListCollections.synchromizedList 메서드를 사용해서 동기화된 List로 변환한다.

List<T> list = Collections.synchronizedList(new ArrayList<T>());

list는 이제 동기화된 동작으로 실행된다.

다음은 ThreadAThreadB에서 동시에 Board 객체를 HashMap에 각각 1000개씩 추가 후 전체 저장된 수를 출력하는 예제이다.

  • Board
public class Board {
    public String content;

    public Board(String content) {
        this.content = content;
    }
}
  • Main
public class Main {
    public static void main(String[] args) {
        Map<Integer, String> map = Collections.synchronizedMap(new HashMap<>());

        Thread threadA = new Thread() {
            @Override
            public void run() {
                for(int i =1; i <= 1000; i++) {
                    map.put(i, "content: " + i);
                }
            }
        };

        Thread threadB = new Thread() {
            @Override
            public void run() {
                for(int i = 1000; i <= 2000; i++) {
                    map.put(i, "content: " + i);
                }
            }
        };

        threadA.start();
        threadB.start();

        try {
            threadA.join();
            threadB.join();
        } catch (Exception e) {
            e.printStackTrace();
        }

        int size = map.size();
        System.out.println("total object: " + size); // 2000
    }
}

만약 Collections.synchronizedMap로 wrapping하지 않고 HashMap만 사용했다면 2000이 나오지 않았을 것이다.

수정할 수 없는 collection

수정할 수 없는(unmodified) collection이란 요소를 추가, 삭제할 수 없는 collection을 말한다. collection에 저장된 요소를 변경하고 싶지 않을 때 유용하다. 여러 가지 방법으로 만들 수 있는데, 먼저 첫번째 방법으로 List, Set, Map interface의 정적 메서드인 of()로 생성할 수 있다.

List<E> immutableList = List.of(E... elements);
Set<E> immutableSet = Set.of(E... elements);
Map<K,V> immutableMap = Map.of(K k1, V v1, K k2, V v2, ...);

두번째 방법으로는 List, Set, Map 인터페이스의 정적 메서드인 copyOf()을 이용해 기존 collection을 복사하여 수정할 수 없는 collection을 만드는 것이다.

List<E> immutableList = List.copyOf(Collection<E> coll);
Set<E> immutableSet = Set.copyOf(Collection<E> coll);
Map<K,V> immutableMap = Map.copyOf(Map<K,V> map);

세 번째 방법은 배열로부터 수정할 수 없는 List 컬렉션을 만들 수 있다.

String[] arr = {"A", "B", "C"};
List<String> immutableList = Arrays.asList(arr);

0개의 댓글