본문 바로가기

Java/Java

컬렉션 프레임워크_컬렉션 프레임워크

1. 컬렉션 프레임워크

자바는 널리 알려져 있는 자료구조(Data Structure)를 통해 객체들을 효율적으로 추가, 삭제, 검색할 수 있는 인터페이스와 구현 클래스를 java.util 패키지에서 제공한다.

그리고 이러한 인터페이스와 구현 클래스를 컬렉션 프레임워크(Collection Framework)라고 부른다.

 

컬렉션 프레임워크라는 명칭의 의미를 살펴보면 컬렉션 프레임워크가 무엇인지 알 수 있다.

컬렉션은 객체의 저장을 뜻한다.

프레임워크는 사용방법을 정의한 라이브러리를 말한다.

즉 컬렉션 프레임워크는 실제 객체를 저장할 수 있는 컬렉션 클래스(구현 클래스), 그리고 컬렉션 클래스의 사용방법을 정의한 인터페이스를 통칭하는 말이다.

 

컬렉션 프레임워크의 주요 인터페이스로는 List, Set, Map이 있다.

그리고 해당 인터페이스들에 대한 다양한 컬렉션 클래스(구현 클래스)가 존재한다.

 

 

2. List 컬렉션

List 컬렉션은 배열과 유사한 방법으로 객체를 인덱스로 관리한다.

배열과의 차이점은 아래와 같다.

  • 저장 용량이 자동으로 증가한다.
  • 객체를 저장할 때 자동 인덱스가 부여된다.

 

또한 List 컬렉션은 객체 자체를 저장하는 것이 아니라, 객체의 번지를 참조한다.

그래서 동일한 객체를 중복 저장할 수 있으며, 인덱스는 동일한 번지를 참조한다.

null도 저장 가능하며, 이 경우 인덱스는 객체를 참조하지 않는다.

 

List 컬렉션에는 구현 클래스 ArrayList, Vector, LinkedList 등이 존재한다.

아래의 메소드들은 List 컬렉션에서 공통적으로 사용 가능한 List인터페이스의 메소드이다.

리턴 타입과 매개변수 타입의 E는 객체의 타입을 의미한다.

기능 메소드 설명
객체 추가 boolean add(E e) 주어진 객체를 맨 끝에 추가한다.
void add(int index, E element) 주어진 인덱스에 객체를 추가한다.
E 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) 주어진 객체를 삭제한다.

 

1) ArrayList

ArrayList는 List인터페이스의 대표적인 컬렉션 클래스(구현 클래스)이다.

ArrayList의 생성방법은 아래와 같다.

두 코드의 의미는 동일하며, E타입 객체를 저장하는 ArrayList를 생성한다.

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

 

ArrayList 객체를 기본 생성자로 생성하게 되면, 해당 객체는 10개의 객체를 저장할 수 있는 초기 용량를 가지게 된다.

저장된 객체 수가 늘어나면 용량은 자동으로 증가한다.

 

ArrayList에 객체를 추가하면 0번 인덱스부터 차례대로 저장된다.

만약 ArrayList에 저장된 객체 중 특정한 객체를 제거하면, 바로 뒤의 객체부터 마지막 객체까지 인덱스가 1씩 당겨진다.

반대로 ArrayList의 특정 인덱스에 객체를 저장하면, 해당 인덱스부터 마지막 인덱스까지 1씩 밀려난다.

 

이러한 특징 때문에 저장된 객체 수가 많고, 특정 인덱스에 객체를 추가하거나 제거하는 일이 빈번하다면 ArrayList보다는 LinkedList를 사용하는 것이 좋다.

하지만 인덱스를 이용해 객체를 찾거나, 맨 마지막에 객체를 추가하는 경우에는 ArrayList가 더 좋은 성능을 발휘한다. 

 

아래의 예제 코드는 ArrayList 사용의 예를 보여준다.

package sec01.exam01;

import java.util.*;

public class ArrayListExample {

	public static void main(String[] args) {
		List<String> list = new ArrayList<String>();
		
		list.add("Java"); //String 객체를 저장
		list.add("JDBC");
		list.add("Servlet/JSP");
		list.add(2, "Database");
		list.add("iBATIS");
		
		int size = list.size(); //저장된 총 객체 수 반환
		System.out.println("총 객체 수: " + size);
		System.out.println();
		
		String skill = list.get(2); //2번 인덱스의 객체 얻기
		System.out.println("2: " + skill);
		System.out.println();
		
		for(int i = 0; i < list.size(); i++) { //저장된 총 객체 수만큼 루핑
			String str = list.get(i);
			System.out.println(i + ": " + str);
		}
		System.out.println();
		
		list.remove(2); //2번 인덱스 객체(Database) 삭제
		list.remove(2); //2번 인덱스 객체(Servlet/JSP) 삭제
		list.remove("iBATIS");
		
		for(int i = 0; i < list.size(); i++) { //저장된 총 객체 수만큼 루핑
			String str = list.get(i);
			System.out.println(i + ": " + str);
		}
		
				

	}

}

 

2) Vector

Vector는 ArrayList와 동일한 내부 구조를 가진 컬렉션 클래스이다. 

Vector 객체 또한 아래와 같이 생성할 수 있다.

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

Vector가 ArrayList랑 다른 점은, Vector가 동기화 메소드로 구성되어있다는 점이다.

이러한 특성 때문에 여러 스레드가 Vector의 메소드를 동시에 실행할 수 없다.

그래서 멀티 스레스 환경에서 Vector는 안전하게 실행될 수 있다.

 

아래는 Vector의 사용 예를 보여준다.

package sec01.exam02;

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

}

 

package sec01.exam02;

import java.util.*;

public class VectorExample {

	public static void main(String[] args) {
		List<Board> list = new Vector<Board>();
		
		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"));
		
		list.remove(2);
		list.remove(3);
		
		for(int i = 0; i < list.size(); i++) {
			Board board = list.get(i);
			System.out.println(board.subject + "\t" + board.content + "\t" + board.writer);
		}

	}

}

 

3) LinkedList

LinkedList는 ArrayList와 사용 방법은 동일하지만, 내부 구조는 전혀 다르다.

ArrayList는 내부 배열에 객체를 저장하여 관리한다.

하지만 LinkedList는 인접 참조를 링크해서 체인 처럼 관리한다.

 

LinkedList의 특정 인덱스 객체를 제거하면, 앞뒤 링크만 변경되고 나머지 링크는 변경되지 않는다.

특정 인덱스에 객체를 삽입할 때도 마찬가지이다.

 

LinkedList는 아래와 같이 생성 가능하다.

생성된 LinkedList는 어떠한 링크도 만들어져있지 않기 때문에 내부는 비어있다고 생각하면 된다.

  • List<E> list = new LinkedList<E>();
  • List<E> list = new LinkedList<>();

 

아래의 예제 코드는 ArrayList와 LinkedList에 객체를 삽입하는데 걸린시간을 측정한다.

0번 인덱스에 String 객체를 10000번 추가하기 위해 add메소드를 사용한다.

실행 결과, LinkedList가 훨씬 빠른 성능을 발휘한다.

 

맨 끝 인덱스부터 차례대로 추가 또는 삭제하는 작업의 경우 ArrayList가 더 빠르다. 

변경할 추가적인 인덱스가 없기 때문이다.

 

그러나 중간에 추가, 삭제하는 경우, ArrayList는 그 뒤의 인덱스를 모두 변경해야하므로 더 많은 시간이 소요된다.

LinkedList의 경우 추가 또는 삭제되는 인덱스의 앞뒤 인덱스만 변경하면 되기 때문에 상대적으로 더 적은 시간이 소요된다.

package sec01.exam03;

import java.util.*;

public class LinkedListExample {

	public static void main(String[] args) {
		List<String> list1 = new ArrayList<String>();
		List<String> list2 = new LinkedList<String>();
		
		long startTime;
		long endTime;
		
		startTime = System.nanoTime();
		for(int i = 0; i < 10000; i++) {
			list1.add(0, String.valueOf(i));
		}
		endTime = System.nanoTime();
		System.out.println("ArrayList 소요시간: " + (endTime - startTime) + " ns");
		
		startTime = System.nanoTime();
		for(int i = 0; i < 10000; i++) {
			list2.add(0, String.valueOf(i));
		}
		endTime = System.nanoTime();
		System.out.println("LinkedList 소요시간: " + (endTime - startTime) + " ns");

	}

}

 

 

3. Set 컬렉션

List 컬렉션과 다르게 Set 컬렉션은 객체 저장의 순서가 존재하지 않는다.

또한 객체를 중복 저장할 수 없고, 하나의 null만 저장할 수 있다.

 

Set 컬렉션에는 HashSet, LinkedHashSet, TreeSet 등의 컬렉션 클래스가 있다.

아래의 메소드들은 Set 컬렉션에서 공통적으로 사용 가능한 Set 인터페이스의 메소드이다.

Set 컬렉션은 객체의 저장 순서가 존재하지 않기 때문에, 인덱스를 매개 변수로 사용하는 메소드가 없다.

기능 메소드 설명
객체 추가 boolean add(E e) 주어긴 객체를 저장한다. 성공적으로 저장되면 true, 중복 객체를 저장하려 한다면 false를 반환한다.
객체 검색 boolean contains(Object o) 주어진 객체가 저장되어 있는지 조사한다.
boolean isEmpty() 컬렉션이 비어 있는지 조사한다.
Iterator<E> iterator() 저장된 객체를 한번씩 가져오는 반복자를 반환한다.
int size() 저장되어 있는 전체 객체 수를 반환한다.
객체 삭제 void clear() 저장된 모든 객체를 삭제한다.
boolean remove(Object o) 주어진 객체를 삭제한다.

 

보는 바와 같이 Set 컬렉션은 객체 저장 순서를 고려하지 않기 때문에 인덱스를 통해 객체를 불러오는 메소드가 없다.

대신 전체 객체를 한 번씩 반복해서 가져오는 반복자(Iterator)를 제공한다.

반복자는 Iterator인터페이스를 구현한 객체을 말한다.

반복자는 아래와 같이 호출 가능하다.

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

아래는 Iterator인터페이스에 선언된 메소드들이다.

리턴 타입 메소드 설명
boolean hasNext() 가져올 객체가 있으면 true, 없으면 false를 반환한다.
E next() 컬렉션에서 하나의 객체를 가져온다.
void remove() 컬렉션에서 객체를 제거한다.

 

반복자를 통해 하나의 객체를 가져올 때는 next메소드를 사용하면 된다.

그리고 next메소드를 사용하기 전에 hasNext메소드를 통해 가져올 객체가 있는지 사전에 확인 해주는 것이 좋다.

 

1) HashSet

HashSet은 Set인터페이스의 컬렉션 클래스이다.

HashSet은 아래와 같이 생성 가능하다.

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

 

HashSet은 Set인터페이스의 구현 클래스이기 때문에 객체들을 순서 없이 저장하고, 중복된 객체를 저장하지 않는다.

중복 객체를 판단하는 HashSet의 기준이 꼭 같은 인스턴스를 뜻하지는 않는다.

 

HashSet은 객체를 저장하기 전에 먼저 객체의 hashCode메소드를 호출하여 해시코드를 얻어낸다.

그 후 이미 저장되어 있는 객체들의 해시코드와 비교하여 동일한 해시코드의 객체가 있는지 확인한다.

만약 동일한 해시코드의 객체가 있다면 2차 확인을 실시한다.

 

동일한 해시코드가 있는 경우 equals메소드를 통해 두 객체를 비교하고, true면 동일한 객체로 판단하고 중복 저장하지 않는다.

 

String 타입 객체를 저장하는 HashSet이 있다고 가정하자.

이 경우 String 객체의 문자열이 동일하다면 String 객체는 동일한 객체로 간주된다.

 

그 이유는 String클래스가 hashCode, equals메소드를 재정의하여 문자열이 동일할 경우 각각 동일한 해시코드, true를 반환하도록 했기 때문이다.

 

아래 예제 코드는 HashSet에 String 객체를 추가, 검색, 제거하는 예를 보여준다.

package sec01.exam04;

import java.util.*;

public class HashSetExample {

	public static void main(String[] args) {
		Set<String> set = new HashSet<String>();
		
		set.add("Java");
		set.add("JDBC");
		set.add("Servlet/JSP");
		set.add("Java"); //저장 X
		set.add("iBATIS");
		
		int size = set.size();
		System.out.println("총 객체수: " + size);
		
		Iterator<String> iterator = set.iterator();
		while(iterator.hasNext()) {
			String element = iterator.next();
			System.out.println("\t" + element);
		}
		
		set.remove("JDBC");
		set.remove("iBATIS");
		
		System.out.println("총 객체수: " + set.size());
		
		iterator = set.iterator();
		for(String element : set) {
			System.out.println("\t" + element);
		}
		
		set.clear();
		if(set.isEmpty()) {
			System.out.println("비어 있음");
		}

	}

}

 

그리고 다음 예제 코드는 사용자 정의 클래스인 Member클래스를 통해 HashSet에서 객체의 추가, 제거, 검색의 예를 보여준다.

Member클래스의 경우 name, age필드의 값이 동일하다면 동일한 객체로 간주할 수 있도록 hashCode, equals메소드가 재정의 되어있다.

package sec01.exam04;

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 obj) {
		if(obj instanceof Member) {
			Member member = (Member) obj;
			return member.name.equals(name) && (member.age == age);
		} else {
			return false;
		}
	}
	
	@Override
	public int hashCode() {
		return name.hashCode() + age;
	}

}

 

package sec01.exam04;

import java.util.*;

public class HashSetExample2 {

	public static void main(String[] args) {
		Set<Member> set = new HashSet<Member>();
		
		set.add(new Member("홍길동", 30));
		set.add(new Member("홍길동", 30)); //동일 객체로 판단되어 저장 X
		
		System.out.println("총 객체수 :" + set.size());

	}

}

 

 

4. Map 컬렉션

Map 컬렉션은 키(key)와 값(value)으로 구성된 Map.Entry 객체의 형태로 데이터를 저장한다.

Entry는 Map인터페이스 내부에 선언된 중첩 인터페이스이다.

그리고 키와 값은 모두 객체이다.

 

Map.Entry 형태로 데이터를 저장할 때 키는 중복될 수 없지만 값은 중복될 수 있다.

만약 기존에 존재하는 키와 동일한 키로 새로운 데이터를 저장하면 기존의 데이터는 사라지고 새로운 데이터로 대체된다.

기능 메소드 설명
객체 추가 V put(K key, V value) 주어진 키로 값을 저장한다. 새로운 키일 경우 null을 반환한다. 동일한 키가 있었을 경우 값을 대체하고 이전 값을 반환한다.
객체 검색 boolean containsKey(Object key) 주어진 키가 있는지 여부를 확인한다.
boolean containsValue(Object value) 주어진 값이 있는지 여부를 확인한다.
Set<Map.Entry<K, V>> entrySet() 키와 값으로 구성된 모든 Map.Entry 객체를 Set에 담아서 반환한다.
V get(Object key) 주어진 키에 대한 값을 반환한다.
boolean isEmpty() 컬렉션이 비어 있는지 여부를 확인한다.
Set<K> keySet() 모든 키를 Set에 담아서 반환한다.
int size() 저장된 키의 총 수를 반환한다.
Collection<V> values() 저장된 모든 값을 Collection에 담아서 반환한다.
객체 삭제 void clear() 모든 Map.Entry를 삭제한다.
V remove(Object key) 주어진 키와 일치하는 Map.Entry를 삭제하고 값을 반환한다.

 

Map 컬렉션에 저장된 전체 객체를 대상으로 하나씩 얻고 싶은 경우에는 두 가지 방법을 사용할 수 있다.

첫 번째 방법은 다음과 같다.

keySet메소드로 모든 키를 Set 컬렉션에 저장한다.

그리고 반복자를 통해 키를 하나씩 얻고, get메소드를 통해 값을 얻는 방법이다.

  • Map<K, V> map = ...;
  • Set<K> keySet = map.keySet();
  • Iterator<K> keyIterator = keySet.iterator();
  • while(keyIterator.hasNext()) { K key = keyIterator.next(); V value = map.get(key); }

 

두 번째 방법은 다음과 같다.

entrySet메소드로 모든 Map.Entry를 Set 컬렉션에 저장한다.

그리고 반복자를 통해 Map.Entry를 하나씩 얻고, getKey, getValue메소드를 이용해 키와 값을 얻는 방법이다.

  • Set<Map.Entry<K, V>> entrySet = map.entrySet();
  • Iterator<Map.Entry<K, V>> entryIterator = entrySet.iterator();
  • while(entryIterator.hasNext()) { Map.Entry<K, V> entry = entryIterator.next(); K key = entry.getKey(); V value = entry.getValue(); }

 

1) HashMap

HashMap은 Map 인터페이스를 구현하는 대표적인 Map 컬렉션이다.

Set과 마찬가지로 HashMap의 키로 사용할 객체는 hashCode, equals메소드를 재정의하여 키들 간의 동등성을 판단하기 위한 조건을 정해야한다.

 

키와 값의 타입은 기본 타입이 사용될 수 없으며, 클래스 및 인터페이스 타입만 가능하다.

아래는 String 타입의 키, Integer 타입의 값을 사용하는 HashMap을 생성하는 코드이다.

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

 

아래의 예제 코드는 이름을 키로, 점수를 값으로 사용하는 HashMap의 예를 보여준다.

package sec01.exam06;

import java.util.*;

public class HashMapExample {

	public static void main(String[] args) {
		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("\t홍길동 :" + map.get("홍길동"));
		System.out.println();
		
		
		Set<String> keySet = map.keySet();
		Iterator<String> keyIterator = keySet.iterator();
		while(keyIterator.hasNext()) { 
			String key = keyIterator.next();
			Integer value = map.get(key);
			System.out.println("\t " + key + " : " + value);
		}
		System.out.println();
		
		map.remove("홍길동");
		System.out.println("총 Entry 수:" + map.size());
		
		
		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("\t" + key + " : " + value);
		}
		System.out.println();
		
		map.clear();
		System.out.println("총 Entry 수: " + map.size());

	}

}

 

그리고 다음 예제는 사용자 정의 객체인 Student를 키로 사용하고, 점수를 값으로 사용하는 HashMap의 예를 보여준다.

Student클래스에는 학번과 이름이 동일한 Student를 동등한 키로 간주하기 위해 hashCode, equals메소드가 재정의 되어 있다.

package sec01.exam07;

public class Student {
	public int sno;
	public String name;
	
	public Student(int sno, String name) {
		this.sno = sno;
		this.name = name;
	}
	
	@Override
	public boolean equals(Object obj) { //학번과 이름이 같다면 true 반환
		if(obj instanceof Student) {
			Student student = (Student) obj;
			return (sno == student.sno) && (name.equals(student.name));
		} else {
			return false;
		}
	}
	
	@Override
	public int hashCode() { //학번과 이름이 같다면 동일한 값 반환
		return sno + name.hashCode();
	}

}

 

package sec01.exam07;

import java.util.*;

public class HashMapExample {

	public static void main(String[] args) {
		Map<Student, Integer> map = new HashMap<Student, Integer>();
		
		map.put(new Student(1, "홍길동"), 95); //동일한 키로 객체 저장
		map.put(new Student(1, "홍길동"), 95); //동일한 키로 객체 저장
		
		System.out.println("총 Entry 수:" +  map.size()); //1

	}

}

 

2) Hashtable

HashTable은 HashMap과 동일한 구조를 가진 Map 인터페이스의 컬렉션 클래스이다.

 

HashMap과의 차이점은 Hashtable의 메소드들이 동기화된 메소드라는 것이다.

그렇기 때문에 멀티 스레드 환경에서 Hashtable의 메소드들은 병렬로 처리 될 수 없다.

 

String을 키로 사용하고 Integer를 값으로 사용하는 Hashtable은 아래와 같이 생성 가능하다.

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

 

출처: 혼자 공부하는 자바(신용권)

'Java > Java' 카테고리의 다른 글

컬렉션 프레임워크_LIFO와 FIFO 컬렉션  (0) 2022.04.12
스레드_스레드 제어  (0) 2022.04.03
스레드_멀티 스레드  (0) 2022.03.28
예외 처리_예외 처리  (0) 2022.03.23
예외 처리_예외 클래스  (0) 2022.03.19