본문 바로가기
자바/이것이 자바다

15. 컬렉션 자료구조

by 989898 2023. 11. 18.

15.1 컬렉션 프레임워크

컬렉션(자바가 다양한 객체를 효율적으로 저장하기 위해서 이미 정해놓은 인터페이스, 클래스)

프레임워크(미리 짜여져 있는 틀을 말함)

 

자바는 널리 알려져 있는 자료구조를 바탕으로 객체들을 효율적으로 추가, 삭제, 검색할 수 있도록 관련된 인터페이스와 클래스들을 java.util 패키지에 포함시켜 놓았다. 이들을 총칭해서 컬렉션 프레임워크라고 부른다.

 

컬렉션 프레임워크는 몇 가지 인터페이스를 통해서 다양한 컬렉션 클래스를 이용할 수 있도록 설계되어 있다.

주요 인터페이스로는 List, Set, Map이 있는데, 이 인터페이스로 사용 가능한 컬렉션 객체의 종류는 다음과 같다.

List와 Set은 객체를 추가, 삭제, 검색하는 방법에 있어서 공통점이 있기 때문에 공통된 메소드만 따로 모아 Collection 인터페이스로 정의해 두고 이것을 상속하고 있다.

 

Map은 키와 값을 하나의 쌍으로 묶어서 관리하는 구조로 되어 있어 List 및 Set과는 사용 방법이 다르다.

 

다음은 각 인터페이스 별로 사용할 수 있는 컬렉션의 특징을 정리한 것이다.


15.2 List 컬렉션

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

 

List 컬렉션에는 ArrayList, Vector, LinkedList 등이 있는데, List 컬렉션에서 공통적으로 사용 가능한 List 인터페이스 메소드는 다음과 같다. 인덱스로 객체들이 관리되기 때문에 인덱스를 매개값으로 갖는 메소드들이 많다.

 

ArrayList

 

ArrayList는 List 컬렉션에서 가장 많이 사용하는 컬렉션이다. ArrayList에 객체를 추가하면 내부 배열에 객체가 저장된다. 일반 배열과의 차이점은 ArrayList는 제한 없이 객체를 추가할 수 있다는 것이다.

List 컬렉션은 객체 자체를 저장하는 것이 아니라 객체의 번지를 저장한다. 또한 동일한 객체를 중복 저장할 수 있는데, 이 경우에는 동일한 번지가 저장된다. null 또한 저장이 가능하다.

 

ArrayList 컬렉션은 다음과 같이 생성할 수 있다.

타입 파라미터 E에는 ArrayList에 저장하고 싶은 객체 타입을 지정하면 된다. List에 지정한 객체 타입과 동일하다면 ArrayList<>와 같이 객체 타입을 생략할 수도 있다. 객체 타입을 모두 생략하면 모든 종류의 객체를 저장할 수 있다.

 

ArrayList 컬렉션에 객체를 추가하면 인덱스 0번부터 차례대로 저장된다. 특정 인덱스의 객체를 제거하면 바로 뒤 인덱스부터 마지막 인덱스까지 모두 앞으로 1씩 당겨진다. 마찬가지로 특정 인덱스에 객체를 삽입하면 해당 인덱스부터 마지막 인덱스까지 모두 1씩 밀려난다. 다음 그림은 4번 인덱스가 제거되었을 때 인덱스가 모두 앞으로 1씩 당겨지는 모습을 나타낸 것이다.

따라서 빈번한 객체 삭제와 삽입이 일어나는 곳에서는 ArrayList를 사용하는 것은 바람직하지 않다.

대신 이런 경우라면 LinkedList를 사용하는 것이 좋다. 다음은 ArrayList에 객체를 추가, 검색, 삭제하는 방법을 보여준다.

package ch15.sec02.exam01;

public class Board {
	private String subject;
	private String content;
	private String writer;

	public Board(String subject, String content, String writer) {
		this.subject = subject;
		this.content = content;
		this.writer = writer;
	}

	public String getSubject() {
		return subject;
	}

	public void setSubject(String subject) {
		this.subject = subject;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}

	public String getWriter() {
		return writer;
	}

	public void setWriter(String writer) {
		this.writer = writer;
	}

}

 

package ch15.sec02.exam01;

import java.util.*;

public class ArrayListExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		// ArrayList 컬렉션 생성
		List<Board> list = new ArrayList<>();

		// 객체 추가
		list.add(new Board("제목1", "내용1", "글쓴이1"));
		list.add(new Board("제목2", "내용2", "글쓴이2"));
		list.add(new Board("제목3", "내용3", "글쓴이3"));
		list.add(new Board("제목4", "내용4", "글쓴이4"));
		list.add(new Board("제목5", "내용5", "글쓴이5"));

		// 저장된 총 객체 수 얻기
		int size = list.size();
		System.out.println("총 객체 수 : " + size);
		System.out.println();

		// 특정 인덱스의 객체 가져오기
		Board board = list.get(2);
		System.out.println(board.getSubject() + "\t" + board.getContent() + "\t" + board.getWriter());
		System.out.println();

		for (int i = 0; i < list.size(); i++) {
			Board b = list.get(i);
			System.out.println(b.getSubject() + "\t" + b.getContent() + "\t" + b.getWriter());
		}

		System.out.println();

		list.remove(2);
		list.remove(2);
		for (Board b : list) {
			System.out.println(b.getSubject() + "\t" + b.getContent() + "\t" + b.getWriter());
		}

	}

}

Vector

 

Vector는 ArrayList와 동일한 내부 구조를 가지고 있다. 차이점은 Vector는 동기화된 메소드로 구성되어 있기 때문에 멀티 스레드가 동시에 Vector() 메소드를 실행할 수 없다는 것이다. 그렇기 때문에 멀티 스레드 환경에서는 안전하게 객체를 추가 또는 삭제할 수 있다.

Vector 컬렉션은 다음과 같이 생성할 수 있다.

타입 파라미터 E에는 Vector에 저장하고 싶은 객체 타입을 지정하면 된다. List에 지정한 객체 타입과 동일하다면 Vector<>와 같이 객체 타입을 생략할 수도 있다. 객체 타입을 모두 생략하면 모든 종류의 객체를 저장할 수 있다.

 

다음은 ThreadA와 ThreadB에서 동시에 Board 객체를 Vector에 각각 1000개씩 추가한 후, 전체 저장된 수를 출력하는 예제이다.

package ch15.sec02.exam02;

import java.util.*;

import ch15.sec02.exam01.*;

public class VectorExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

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

		Thread threadA = new Thread() {
			@Override
			public void run() {
				for (int i = 1; i <= 1000; i++) {
					list.add(new Board("제목" + i, "내용" + i, "글쓴이" + i));
				}
			}
		};

		Thread threadB = new Thread() {
			@Override
			public void run() {
				for (int i = 1; i <= 1000; i++) {
					list.add(new Board("제목" + i, "내용" + i, "글쓴이" + i));
				}
			}
		};

		threadA.start();
		threadB.start();
		
        // 작업 스레드들이 모두 종료될 때까지 메인 스레드를 기다리게 함
		try {
			threadA.join();
			threadB.join();
		} catch (Exception e) {
			// TODO: handle exception
		}

		int size = list.size();
		System.out.println("총 객체 수 :" + size);

	}

}

실행 결과를 보면 정확하게 2000개가 저장되었음을 알 수 있다.

 

List<Board> list = new Vector<>(); 를 밑에 코드와 같이 변경하고 실행해보자.

 

실행 결과는 2000개가 나오지 않거나, PC에 따라 에러가 발생할 수 있다. 그 이유는 ArrayList는 두 스레드가 동시에 add() 메소드를 호출할 수 있기 때문에 경합이 발생해 결국은 하나만 저장되기 때문이다. 반면에 Vector의 add()는 동기화 메소드이므로 한 번에 하나의 스레드만 실행할 수 있어 경합이 발생하지 않는다.

 

LinkedList

 

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

LinkedList는 특정 위치에서 객체를 삽입하거나 삭제하면 바로 앞뒤 링크만 변경하면 되므로 빈번한 객체 삭제와 삽입이 일어나는 곳에서는 ArrayList보다 좋은 성능을 발휘한다. 다음은 중간에 객체를 제거할 경우 앞뒤 링크의 수정이 일어나는 모습을 보여준다.

LinkedList 컬렉션은 다음과 같이 생성할 수 있다.

다음 예제는 ArrayList와 LinkedList에 10000개의 객체를 삽입하는데 걸린 시간을 측정한 것이다.

0번 인덱스에 String 객체를 10000번 추가하기 위해 List 인터페이스의 add(int index, E element) 메소드를 이용하였다.

package ch15.sec02.exam03;

import java.util.*;

public class LinkedListExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		List<Integer> list1 = new ArrayList<>();
		List<Integer> list2 = new LinkedList<>();

		long startTime;
		long endTime;

		startTime = System.nanoTime();
		for (int i = 0; i < 10000; i++) {
			list1.add(0, i);
		}
		endTime = System.nanoTime();
		System.out.println("ArrayList 걸린시간 " + (endTime - startTime) + " ns");

		startTime = System.nanoTime();
		for (int i = 0; i < 10000; i++) {
			list2.add(0, i);
		}
		endTime = System.nanoTime();
		System.out.println("LinkedList 걸린시간 " + (endTime - startTime) + " ns");
	}

}

실행 결과를 보면  LinkedList가 훨씬 빠른 성능을 낸다. ArrayList가 느린 이유는 0번 인덱스에 새로운 객체가 추가되면서 기존 객체의 인덱스를 한 칸씩 뒤로 미는 작업을 하기 때문이다.


15.3 Set (집합) 컬렉션

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

 

집합은 순서와 상관없고 중복이 허용되지 않기 때문이다.

 

Set 컬렉션은 또한 구슬 주머니와도 같다. 동일한 구슬을 두 개 넣을 수 없으며, 들어갈(저장할) 때와 나올(찾을) 때의 순서가 다를 수도 있기 때문이다.

Set 컬렉션에는 HashSet, LinkedHashSet, TreeSet 등이 있는데, Set 컬렉션에서 공통적으로 사용 가능한 Set 인터페이스의 메소드는 다음과 같다. 인덱스로 관리하지 않기 때문에 인덱스를 매개값으로 갖는 매소드가 없다.

HashSet

 

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

타입 파라미터 E에는 HashSet에 저장하고 싶은 객체 타입을 지정하면 된다. Set에 지정한 객체 타입과 동일하다면 HashSet<>과 같이 객체 타입을 생략할 수도 있다. 객체 타입을 모두 생략하면 모든 종류의 객체를 저장할 수 있다.

 

HashSet은 동일한 객체는 중복 저장하지 않는다. 여기서 동일한 객체란 동등 객체를 말한다. HashSet은 다른 객체라도 HashCode() 메소드의 리턴값이 같고, equals() 메소드가 true를 리턴하면 동일한 객체라고 판단하고 중복 저장하지 않는다.

문자열을 HashSet에 저장할 경우, 같은 문자열을 갖는 String 객체는 동등한 객체로 간주한다. 같은 문자열이면 hashCode()의 리턴값이 같고 equals()의 리턴값이 true가 나오기 때문이다.

package ch15.sec03.exam01;

import java.util.*;

public class HashSetExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		// HashSet 컬렉션 생성
		Set<String> set = new HashSet<>();

		// 객체 저장
		set.add("Java");
		set.add("JDBC");
		set.add("JSP");
		set.add("Java");
		set.add("Spring");

		// 저장된 객체 수 출력
		int size = set.size();
		System.out.println("총 객체 수 : " + size);
	}

}

다음 예제는 이름과 나이가 동일한 경우 Member 객체를 HashSet에 중복 저장하지 않는다.

 

Member 클래스를 선언할 때 이름과 나이가 동일하다면 동일한 해시코드가 리턴되도록 hashCode()를 재정의하고, equals() 메소드가 true를 리턴하도록 재정의했기 때문이다.

package ch15.sec03.exam02;

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

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

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

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

 

package ch15.sec03.exam02;

import java.util.*;

public class HashSetExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		// HashSet 컬렉션 생성
		Set<Member> set = new HashSet<>();

		// Member 객체 저장
		set.add(new Member("홍길동", 30));
		set.add(new Member("홍길동", 30));

		System.out.println("총 객체 수 : " + set.size());
	}

}

Set 컬렉션은 인덱스로 객체를 검색해서 가져오는 메소드가 없다. 대신 객체를 한 개씩 반복해서 가져와야 하는데, 여기에는 두 가지 방법이 있다. 하나는 다음과 같이 for문을 이용하는 것이다.

다른 방법은 다음과 같이 Set 컬렉션의 iterator() 메소드로 반복자를 얻어 객체를 하나씩 가져오는 것이다. 타입 파라미터 E는 Set 컬렉션에 저장되어 있는 객체의 타입이다.

iterator는 Set 컬렉션의 객체를 가져오거나 제거하기 위해 다음 메소드를 제공한다.

사용 방법은 다음과 같다.

hasNext() 메소드로 가져올 객체가 있는지 먼저 확인하고, true를 리턴할 때만 next() 메소드로 객체를 가져온다.

만약 next() 로 가져온 객체를 컬렉션에서 제거하고 싶다면 remove() 메소드를 사용한다.

package ch15.sec03.exam03;

import java.util.*;

public class HashSetExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Set<String> set = new HashSet<>();

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

		Iterator<String> iterator = set.iterator();
		while (iterator.hasNext()) {
			String element = iterator.next();
			System.out.println(element);
			if (element.equals("JSP")) {
				iterator.remove();
			}
		}
		System.out.println();

		for (String element : set) {
			System.out.println(element);
		}

	}

}


15.4 Map 컬렉션

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

Map 컬렉션에는 HashMap, Hashtable, LinkedHashMap, Properties, TreeMap 등이 있다.

Map 컬렉션에서 공통적으로 사용 가능한 Map 인터페이스 메소드는 다음과 같다. 키로 객체들을 관리하기 때문에 키를 매개값으로 갖는 메소드가 많다.

앞의 표에서 메소드의 매개변수 타입과 리턴 타입에 K와 V라는 타입 파라미터가 있는데, K는 키 타입, V는 값 타입을 말한다.

 

HashMap

 

HashMap은 키로 사용할 객체가 hashCode() 메소드의 리턴값이 같고 equals() 메소드가 true를 리턴할 경우, 동일 키로 보고 중복 저장을 허용하지 않는다.

다음은 HashMap 컬렉션을 생성하는 방법이다. K와 V는 각각 키와 값의 타입을 지정할 수 있는 타입 파라미터이다.

키는 String 타입, 값은 Integer 타입으로 갖는 HashMap은 다음과 같이 생성할 수 있다. Map에 지정된 키와 값의 타입이 HashMap과 동일한 경우, HashMap<>을 사용할 수 있다.

모든 타입의 키와 객체를 저장할 수 있도록 HashMap을 다음과 같이 생성할 수 있지만, 이런 경우는 거의 없다.

다음 예제는 이름을 키로, 점수를 값으로 저장하는 HashMap 사용 방법을 보여준다.

package ch15.sec04.exam01;

import java.util.*;
import java.util.Map.*;

public class HashMapExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		// 엔트리저장
		Map<String, Integer> map = new HashMap<String, Integer>();

		// 객체저장
		map.put("신용권", 85);
		map.put("홍길동", 90);
		map.put("동장군", 80);
		map.put("홍길동", 95);
		System.out.println("총 Entry 수: " + map.size());
		System.out.println();

		// 키로 값 얻기
		String key = "홍길동";
		int value = map.get(key);
		System.out.println(key + ": " + value);
		System.out.println();

		// 키 Set 컬렉션을 얻고, 반복해서 키와 값을 얻기
		Set<String> keySet = map.keySet();
		Iterator<String> keyIterator = keySet.iterator();
		while (keyIterator.hasNext()) {
			String k = keyIterator.next();
			Integer v = map.get(k);
			System.out.println(k + " : " + v);
		}
		System.out.println();

		// 엔트리 Set 컬렉션을 얻고, 반복해서 키와 값을 얻기
		Set<Entry<String, Integer>> entrySet = map.entrySet();
		Iterator<Entry<String, Integer>> entryIterator = entrySet.iterator();
		while (entryIterator.hasNext()) {
			Entry<String, Integer> entry = entryIterator.next();
			String k = entry.getKey();
			Integer v = entry.getValue();
			System.out.println(k + " : " + v);
		}
		System.out.println();

		// 키로 엔트리 삭제
		map.remove("홍길동");
		System.out.println("총 Entry 수 : " + map.size());
		System.out.println();
	}

}

 

Hashtable

 

Hashtable은 HashMap과 동일한 구조를 가지고 있다. 차이점은 Hashtable은 동기화된(synchronized) 메소드로 구성되어 있기 때문에 멀티 스레드가 동시에 Hashtable의 메소드들을 실행할 수 없다는 것이다. 따라서 멀티 스레드 환경에서도 안전하게 객체를 추가, 삭제할 수 있다.

다음은 키 타입으로 String을, 값 타입으로 Integer를 갖는 Hashtable을 생성한다.

모든 타입의 키와 객체를 저장할 수 있는 Hashtable은 다음과 같이 생성할 수 있지만, 이런 경우는 거의 없다.

다음은 ThreadA와 ThreadB에서 동시에 각각 1000개씩 엔트리를 Hashtable에 추가한 후, 전체 저장된 수를 출력하는 예제이다.

package ch15.sec04.exam02;

import java.util.*;

public class HashtableExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Map<String, Integer> map = new Hashtable<String, Integer>();

		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) {
			// TODO: handle exception
		}

		int size = map.size();
		System.out.println("총 엔트리 수 : " + size);

	}

}

실행 결과를 보면 정확하게 2000개의 엔트리가 저장되어 있는 것을 확인할 수 있다. 

Hashtable을 HashMap으로 변경하고, 다시 실행해보자.

 

실행 결과는 2000개가 나오지 않거나, PC에 따라 에러가 발생할 수 있다. 왜냐하면 HashMap은 두 스레드가 동시에 put() 메소드를 호출할 수 있기 때문에 경합이 발생하고 결국은 하나만 저장되기 때문이다.

 

반면에 Hashtable의 put()은 동기화 메소드이므로 한 번에 하나의 스레드만 실행할 수 있어 경합이 발생하지 않는다.

 

Properties

 

Properties는 Hashtable의 자식 클래스이기 때문에 Hashtable의 특징을 그대로 가지고 있다. Properties는 키와 값을 String 타입으로 제한한 컬렉션이다. Properties는 주로 확장자가 .properties인 프로퍼티 파일을 읽을 때 사용한다.

 

프로퍼티 파일은 다음과 같이 키와 값이 = 기호로 연결되어 있는 텍스트 파일이다. 일반 텍스트 파일과는 다르게 ISO 8559-1 문자셋으로 저장되며, 한글일 경우에는 \u+ 유니코드로 표현되어 저장된다.

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

.class는 리플렉션을 말한다. class 객체를 얻는 방법임

 

일반적으로 프로퍼티 파일은 클래스 파일(~.class)들과 함께 저장된다. 따라서 클래스 파일을 기준으로 상대 경로를 이용해서 읽는 것이 편리하다. Class 객체의 getResourceAsStream() 메소드는 주어진 상대 경로의 리소스 파일을 읽는 InputStream을 리턴한다.

package ch15.sec04.exam03;

import java.util.*;

public class PropertiesExample {

	public static void main(String[] args) throws Exception {
		// TODO Auto-generated method stub
		Properties properties = new Properties();

		// PropertiesExample.class 와 동일한 ClassPath에 있는 database.properties 파일을 로드
		properties.load(PropertiesExample.class.getResourceAsStream("database.properties"));

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

		System.out.println("driver : " + driver);
		System.out.println("url : " + url);
		System.out.println("username : " + username);
		System.out.println("password : " + password);
		System.out.println("admin : " + admin);
	}

}

 


15.5 검색 기능을 강화시킨 컬렉션

컬렉션 프레임워크는 검색 기능을 강화시킨 TreeSet과 TreeMap을 제공한다. 이름에서 알 수 있듯이 TreeSet은 Set 컬렉션이고, TreeMap은 Map 컬렉션이다.

 

TreeSet

 

TreeSet은 이진트리를 기반으로 한 Set 컬렉션이다. 이진 트리는 여러 개의 노드가 트리 형태로 연결된 구조로, 루트 노드라고 불리는 하나의 노드에서 시작해 각 노드에 최대 2개의 노드를 연결할 수 있는 구조를 가지고 있다.

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

다음은 TreeSet 컬렉션을 생성하는 방법을 보여준다.

Set 타입 변수에 대입해도 되지만 TreeSet 타입으로 대입한 이유는 검색 관련 메소드가 TreeSet에만 정의되어 있기 때문이다. 다음은 TreeSet이 가지고 있는 검색 관련 메소드들이다.

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

package ch15.sec05.exam01;

import java.util.*;

public class TreeSetExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TreeSet<Integer> scores = new TreeSet<Integer>();

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

		for (Integer s : scores) {
			System.out.print(s + " ");
		}
		System.out.println();
		System.out.println();

		// 특정 Integer 객체를 가져오기
		System.out.println("가장 낮은 점수 : " + scores.first());
		System.out.println("가장 높은 점수 : " + scores.last());
		System.out.println("95점 아래 점수 : " + scores.lower(95));
		System.out.println("95점 위의 점수 : " + scores.higher(95));
		System.out.println("95점이거나 바로 아래 점수 : " + scores.floor(95));
		System.out.println("85점이거나 바로 위의 점수 : " + scores.ceiling(85));

		System.out.println();

		NavigableSet<Integer> descendingScores = scores.descendingSet();
		for (Integer s : descendingScores) {
			System.out.print(s + " ");
		}
		System.out.println();

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

		NavigableSet<Integer> rangeSet2 = scores.subSet(80, true, 90, false); // 80<= x
		for (Integer s : rangeSet2) {
			System.out.print(s + " ");
		}
		System.out.println();

	}

}

 

TreeMap

 

TreeMap은 이진 트리를 기반으로 한 Map 컬렉션이다. TreeSet과의 차이점은 [키와 값]이 저장된 Entry를 저장한다는 점이다. TreeMap에 엔트리를 저장하면 키를 기준으로 자동 정렬되는데, 부모 키 값과 비교해서 낮은 것은 왼쪽, 높은 것은 오른쪽 자식 노드에 Entry 객체를 저장한다.

다음은 TreeMap 컬렉션을 생성하는 방법이다.

Map 타입 변수에 대입해도 되지만 TreeMap 타입으로 대입한 이유는 검색 관련 메소드가 TreeMap에만 정의되어 있기 때문이다. 다음은 TreeMap이 가지고 있는 검색 관련 메소드들이다.

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

package ch15.sec05.exam02;

import java.util.*;
import java.util.Map.*;

public class TreeMapExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TreeMap<String, Integer> treeMap = new TreeMap<String, Integer>();

		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<Entry<String, Integer>> entrySet = treeMap.entrySet();
		for (Entry<String, Integer> entry : entrySet) {
			System.out.println(entry.getKey() + "-" + entry.getValue());
		}
		System.out.println();

		Entry<String, Integer> entry = null;
		entry = treeMap.firstEntry();
		System.out.println("제일 앞 단어 : " + entry.getKey() + "-" + entry.getValue());
		entry = treeMap.lastEntry();
		System.out.println("제일 뒤 단어 : " + entry.getKey() + "-" + entry.getValue());
		entry = treeMap.lowerEntry("ever");
		System.out.println("ever 앞 단어 : " + entry.getKey() + "-" + entry.getValue());

		System.out.println();

		// 내림차순으로 정렬하기
		NavigableMap<String, Integer> descendingMap = treeMap.descendingMap();
		Set<Entry<String, Integer>> descendingEntrySet = descendingMap.entrySet();
		// map을 set으로 저장 -> set은 iterable이기 때문에 for문에 들어갈 수 있음.
		for (Entry<String, Integer> e : descendingEntrySet) {
			System.out.println(e.getKey() + "-" + e.getValue());
		}
		System.out.println();

		// 범위 검색
		System.out.println("[c~h 사이의 단어 검색]");
		NavigableMap<String, Integer> rangeMap = treeMap.subMap("c", true, "h", false);
		for (Entry<String, Integer> e : rangeMap.entrySet()) {
			System.out.println(e.getKey() + "-" + e.getValue());
		}
	}

}

Comparable과 Comparator

 

TreeSet에 저장되는 객체와 TreeMap에 저장되는 키 객체는 저장과 동시에 오름차순으로 정렬되는데, 어떤 객체든 정렬될 수 있는 것은 아니고 객체가 Comparable 인터페이스를 구현하고 있어야 가능하다.

=> TreeSet과 TreeMap에 들어가는 객체가 Comparable 인터페이스를 구현하고 있어야 한다.

 

Integer, Double, String 타입은 모두 Comparable을 구현하고 있기 때문에 상관없지만, 사용자 정의 객체를 저장할 때에는 반드시 Comparable을 구현하고 있어야 한다.

 

Comparable 인터페이스에는 comparableTo() 메소드가 정의되어 있다. 따라서 사용자 정의 클래스에서 이 메소드를 재정의해서 비교 결과를 정수 값으로 리턴해야 한다.

다음 예제는 나이를 기준으로 Person 객체를 오름차순으로 정렬하기 위해 Comparable 인터페이스를 구현하고 있다.

나이가 적으면 -1을, 같으면 0을, 크면 1을 리턴하도록 comparableTo() 메소드를 재정의하였다.

 

package ch15.sec05.exam03;

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 o) {
		if (age < o.age)
			return -1;
		else if (age == o.age)
			return 0;
		else
			return 1;
	}

}

 

package ch15.sec05.exam03;

import java.util.*;

public class ComparableExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TreeSet<Person> treeSet = new TreeSet<Person>();

		treeSet.add(new Person("홍길동", 45));
		treeSet.add(new Person("김자바", 25));
		treeSet.add(new Person("박지원", 31));

		for (Person e : treeSet) {
			System.out.println(e.name + "-" + e.age);
		}
	}

}

비교 기능이 있는 Comparable 구현 객체를 TreeSet에 저장하거나 TreeMap의 키로 저장하는 것이 원칙이지만, 비교 기능이 없는 Comparable 비구현 객체를 저장하고 싶다면 방법은 없진 않다.

 

TreeSet과 TreeMap을 생성할 때 비교자를 다음과 같이 제공하면 된다.

 

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

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

package ch15.sec05.exam04;

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

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

 

package ch15.sec05.exam04;

import java.util.*;

public class FruitComparator implements Comparator<Fruit> {
	@Override
	public int compare(Fruit o1, Fruit o2) {
		if (o1.price < o2.price)
			return -1;
		else if (o1.price == o2.price)
			return 0;
		else
			return 1;
	}
}

 

package ch15.sec05.exam04;

import java.util.*;

public class ComparatorExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TreeSet<Fruit> treeSet = new TreeSet<Fruit>(new FruitComparator());

		treeSet.add(new Fruit("포도", 3000));
		treeSet.add(new Fruit("수박", 10000));
		treeSet.add(new Fruit("딸기", 6000));

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

}


15.6 LIFO와 FIFO 컬렉션

후입선출(LIFO / Last In First Out)은 나중에 넣은 객체가 먼저 빠져나가고, 선입선출(FIFO / First  In First Out)은 먼저 넣은 객체가 먼저 빠져나가는 구조를 말한다.

스택을 응용한 대표적인 예가 JVM 스택 메모리이다. 스택 메모리에 저장된 변수는 나중에 저장된 것부터 제거된다.

큐를 응용한 대표적인 예가 스레드풀(ExecutorService)의 작업 큐이다. 작업 큐는 먼저 들어온 작업부터 처리한다.

 

Stack

 

stack 클래스는 LIFO 자료구조를 구현한 클래스이다. 다음은 Stack 객체를 생성하는 방법이다.

다음은 Stack 클래스의 주요 메소드이다.

다음은 동전 케이스를 Stack 클래스로 구현한 예제이다. 동전 케이스는 위에만 오픈되어 있는 스택 구조를 가지고 있다.

먼저 넣은 동전은 제일 밑에 깔리고 나중에 넣은 동전이 위에 쌓이기 때문에 제일 위의 동전부터 빼낼 수 있다.

package ch15.sec06.exam01;

public class Coin {
	private int value;

	public Coin(int value) {
		this.value = value;
	}

	public int getValue() {
		return value;
	}
}

 

package ch15.sec06.exam01;

import java.util.*;

public class StackExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Stack<Coin> coinBox = new Stack<Coin>();

		coinBox.push(new Coin(100));
		coinBox.push(new Coin(50));
		coinBox.push(new Coin(500));
		coinBox.push(new Coin(10));

		while (!coinBox.isEmpty()) {
			Coin coin = coinBox.pop();
			System.out.println("꺼내온 동전 : " + coin.getValue());
		}
	}

}

Queue

 

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

다음은 Queue를 이용해서 간단한 메시지를 큐를 구현한 예제이다. 먼저 넣은 메시지가 반대쪽으로 먼저 나오기 때문에 넣은 순서대로 메시지가 처리된다.

package ch15.sec06.exam02;

public class Message {
	public String command;
	public String to;

	public Message(String command, String to) {
		this.command = command;
		this.to = to;
	}
}

 

package ch15.sec06.exam02;

import java.util.*;

public class QueueExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Queue<Message> messageQueue = new LinkedList<>();

		messageQueue.offer(new Message("sendMail", "홍길동"));
		messageQueue.offer(new Message("sendSMS", "신용권"));
		messageQueue.offer(new Message("sendKaKaotalk", "김자바"));

		while (!messageQueue.isEmpty()) {
			Message message = messageQueue.poll();
			switch (message.command) {
			case "sendMail":
				System.out.println(message.to + "님에게 메일을 보냅니다.");
				break;
			case "sendSMS":
				System.out.println(message.to + "님에게 SMS을 보냅니다.");
				break;
			case "sendKaKaotalk":
				System.out.println(message.to + "님에게 카카오톡을 보냅니다.");
				break;
			}
		}
	}
}


15.7 동기화된 컬렉션 (멀티스레드 환경에서 사용할 수 있게 하기)

컬렉션 프레임워크의 대부분의 클래스들은 싱글 스레드 환경에서 사용할 수 있도록 설계되었다.

그렇기 때문에 여러 스레드가 동시에 컬렉션에 접근한다면 의도하지 않게 요소가 변경될 수 있는 불안전한 상태가 된다.

 

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

 

경우에 따라서는 ArrayList, HashSet, HashMap을 멀티 스레드 환경에서 사용하고 싶을 때가 있을 것이다. 이런 경우를 대비해서 컬렉션 프레임워크는 비동기화된 메소드를 동기화된 메소드로 래핑하는 Collections의 synchronizedXXX() 메소드를 제공한다.

이 메소드들은 매개값으로 비동기화된 컬렉션을 대입하면 동기화된 컬렉션을 리턴한다.

다음 코드는 ArrayList를 Collections.synchronizedList()를 메소드를 사용해서 동기화된 List로 변환한다.

다음 코드는 HashSet을 Collections.synchronizedSet() 메소드를 사용해서 동기화된 Set으로 변환한다.

다음 코드는 HashMap을 Collections.synchronizedMap() 메소드를 사용해서 동기화된 Map으로 변환한다.

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

package ch15.sec07;

import java.util.*;

public class SynchronizedMapExample {

	public static void main(String[] args) {
		// Map 컬렉션 생성
		Map<Integer, String> map = Collections.synchronizedMap(new HashMap<>());

		// 작업 스레드 객체 생성
		Thread threadA = new Thread() {
			@Override
			public void run() {
				// 객체 1000개 추가
				for (int i = 1; i <= 1000; i++) {
					map.put(i, "내용" + i);
				}
			}
		};

		// 작업 스레드 객체 생성
		Thread threadB = new Thread() {
			@Override
			public void run() {
				// 객체 1000개 추가
				for (int i = 1001; i <= 2000; i++) {
					map.put(i, "내용" + i);
				}
			}
		};

		// 작업 스레드 실행
		threadA.start();
		threadB.start();

		// 작업 스레드들이 모두 종료될 때까지 메인 스레드를 기다리게 함
		try {
			threadA.join();
			threadB.join();
		} catch (Exception e) {

		}

		int size = map.size();
		System.out.println("총 객체 수 " + size);
	}

}

실행 결과를 보면 정확하게 2000개가 저장되었음을 알 수 있다. 

다음과 같이 변경하고 실행해보면 실행 결과는 2000개가 나오지 않는다. 왜냐하면 HashMap은 두 스레드가 동시에 put() 메소드를 호출할 수 있기 때문에 경합이 발생하고 결국은 하나만 저장되기 때문이다. 하지만 동기화된 Map은 한 번에 하나의 스레드만 put() 메소드를 호출할 수 있기 때문에 경합이 발생하지 않는다.


15.8 수정할 수 없는 컬렉션

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

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

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

Arrays.asList()

 

다음 예제는 수정할 수 없는 컬렉션을 생성하는 다양한 방법을 보여준다.

package ch15.sec08;

import java.util.*;

public class ImmutableExample {

	public static void main(String[] args) throws Exception {
		// TODO Auto-generated method stub

		// List 불변 컬렉션 생성
		List<String> immutableList1 = List.of("A", "B", "c");

		// Set 불변 컬렉션 생성
		Set<String> immutableSet1 = Set.of("A", "B", "c");

		// Map 불변 컬렉션 생성
		Map<Integer, String> immutableMap1 = Map.of(1, "A", 2, "B", 3, "C");

		// List 컬렉션을 불변 컬렉션으로 복사
		List<String> list = new ArrayList<>();
		list.add("A");
		list.add("B");
		list.add("C");
		List<String> immutableList2 = List.copyOf(list);

		// Set 컬렉션을 불변 컬렉션으로 복사
		Set<String> set = new HashSet<>();
		set.add("A");
		set.add("B");
		set.add("C");
		Set<String> immutableSet2 = Set.copyOf(list);

		// Set 컬렉션을 불변 컬렉션으로 복사
		Map<Integer, String> map = new HashMap<>();
		map.put(1, "A");
		map.put(2, "B");
		map.put(3, "C");
		Map<Integer, String> immutableMap2 = Map.copyOf(map);

		// 배열로부터 List 불변 컬렉션 생성
		String[] arr = { "A", "B", "C" };
		List<String> immutableList3 = Arrays.asList(arr);

	}

}

'자바 > 이것이 자바다' 카테고리의 다른 글

17. 스트림 요소 처리  (0) 2023.11.21
16. 람다식  (1) 2023.11.20
14. 멀티 스레드  (1) 2023.11.17
13. 제네릭  (1) 2023.11.17
12. java.base 모듈  (1) 2023.11.17