티스토리 뷰

JAVA/JAVA Theory

[JAVA] 15. Collection

쟌쥰 2019. 11. 17. 17:22

컬렉션 프레임워크

객체를 그룹 단위의 데이터로 저장하는 기본적인 자료구조들의 모음

다수의 데이터를 그룹으로 묶어 관리할 수 있으므로 프로그래밍이 보다 편리해지고, 코드의 재사용성을 늘릴 수 있다.

 

컬렉션 프레임워크 종류

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

인터페이스 분류 특징 구현 클래스
Collection List

순서를 유지하고 저장

중복 저장 가능

ArrayList, Vector

LinkedList

Set

순서를 유지하지 않고 저장

중복 저장 안됨

HashSet, TreeSet
Map

키와 값의 쌍으로 저장

키는 중복 안됨

값은 중복 저장 가능

HashMap, Hashtable

TreeMap, LinkedHashMap

1. List

객체를 일렬로 늘어놓은 구조

객체를 인덱스로 관리

객체를 저장하면 자동 인덱스가 부여되고 인덱스로 객체를 검색, 삭제할 수 있는 기능을 제공

 

List 인터페이스로 구현된 클래스

  • ArrayList
  • Vector
  • LinkedList

List 인터페이스 구현된 메소드

  • 객체추가
    • boolean add(E element) : 주어진 객체를 맨 끝에 추가
    • void add(int index, E element) : 주어진 인덱스에 객체 추가
    • 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.1 ArrayList - 가변 배열

일반 배열과 같이 인덱스로 객체를 관리

배열은 생성될 때 크기가 고정되어 변경을 할 수 없지만 ArrayList는 저장 용량을 초과하면 자동으로 저장 용량을 늘림

상당히 빠르고 크기를 마음대로 조절할 수 있는 배열

단방향 포인터 구조로 자료에 대한 순차적인 접근에 강점이 있다.

 

- 기본 생성자로 ArrayList 객체를 생성

List<기본타입> list = new ArrayList<기본타입>();

 

- 초기화 : 고정된 객체들로 구성된 List를 생성하고자 할 때 사용

List<String> list = ArrayList.asList("Apple", "Banana", "Orange");

 

다음 코드를 통해 ArrayList의 사용 예를 살펴보도록 하겠다.

package com.test02;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ListTest03 {

	public static void main(String[] args) {
		List<String> list = new ArrayList<String>();		

		list.add("홍길동");
		list.add("이순신");
		list.add("강호동");
		list.add("유재석");
		list.add("신동엽");
		list.add("조세호");
		
		list.set(list.indexOf("신동엽"), "이수근");
				
		list.remove(0);			  
		list.remove("조세호");		

		System.out.println(list.get(0));
	}
}
package com.test02;

import java.util.ArrayList;
import java.util.List;

public class ListTest01 {

	public static void main(String[] args) {
		List<String> list = new ArrayList<String>();

		list.add("홍길동");
		list.add("이순신");
		list.add("강호동");
		list.add("유재석");
		list.add("신동엽");
		list.add("조세호");

		// prn(list);

		transEelement(list);
	}

	private static void transEelement(List<String> list) {
		// 1. 홍길동을 김길동으로 바꾸자
		for (String s : list) {
			if (s.equals("홍길동")) {
				list.set(list.indexOf(s), "김길동");
			}
		}
		prn(list);

		// 2. "신"으로 끝나는 객체를 찾아서, 만일 있다면 "신"을 "자" 로 바꾸자
		for (String s : list) {
			if (s.endsWith("신")) {
				list.set(list.indexOf(s), s.replace("신", "자"));
			}
		}
		prn(list);

		// 3. "호"로 끝나는 객체를 찾아서, 만일 있다면 삭제하자.
		for (String s : list) {
			if (s.endsWith("호")) {
				list.remove(list.indexOf(s));
				break;
			}
		}
		prn(list);
	}

	public static void prn(List<String> list) {

		// {이름, 이름, 이름, ...}
		System.out.print("{ ");
		for (String s : list) {
			System.out.print(s);
			if (list.indexOf(s) != list.size() - 1)
				System.out.print(", ");
		}
		System.out.println(" }");
	}
}

 

 

1.2 Vector

ArrayList의 구 버전

ArrayList와 동일한 내부 구조를 가지고 있다.

모든 메소드가 동기화 되어있고 잘 쓰이지 않음

 

- 기본 생성자로 Vector 객체를 생성

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

ArrayList와는 달리 동기화된 메소드로 구성되어 있어 멀티 스레드 환경에서 동시에 메소드들을 실행할 수 없고, 하나의 스레드의 실행이 완료되어야만 다른 스레드에서 실행할 수 있다.

 

다음 코드를 통해 Vector 의 사용 예시를 살펴 보겠다.

package com.test01;

import java.util.Vector;

/*
 * 배열과의 가장 큰 차이점은 크기가 가변이라는 것이다!
 */
public class VectorTest01 {

	public static void main(String[] args) {
		
		Vector<Integer> v = new Vector<Integer>(10,5);	//초기크기 10, capacity increments = 5
		
		System.out.println(v.size() + " : " + v.capacity());
		for(int i=0;i<10;i++) {
			v.add(i);
			System.out.println(v.size() + " : " + v.capacity());
		}
		
		System.out.println("================");
		v.add(10);
		System.out.println(v.size() + " : " + v.capacity());	
		//용량이 늘어나야 할 때 capacity increments 크기 만큼 늘어남!
	}
}
package com.test01;

import java.util.Vector;

public class VectorTest02 {

	public static void main(String[] args) {
		Vector<String> v = new Vector<String>();

		v.add("홍길동");
		v.add("이순신");
		v.add("강호동");
		v.add("유재석");
		v.add("신동엽");
		v.add("조세호");
		
		v.set(0, "홍상삼");
		
		v.remove(0);
		v.remove("조세호");
		
		System.out.println(v.get(0));

//		prn(v);
//		System.out.println(v);	// 아주 이쁘게 잘나옴 -> @Override toString 했구나~~~

		transElement(v);
	}

	private static void transElement(Vector<String> v) {
		// 1. 홍길동의 인덱스를 찾아서, 해당 인덱스에 있는 값을 홍길순 으로 바꾸자.
//		for (String s : v) {
//			if (s.contentEquals("홍길동")) {
//				v.setElementAt("홍길순", v.indexOf(s));
//			}
//		}	
		v.set(v.indexOf("홍길동"), "홍길순");	//이거는 홍길동 없으면 에러나요~!
		System.out.println(v);
	

		// 2. "신" 으로 끝나는 객체를 찾아서, 만일 있다면 "신"을 "자"로 바꾸자.
		for (String s : v) {
			if (s.endsWith("신")) {
				v.setElementAt(s.replace('신', '자'), v.indexOf(s));
			}
		}
		System.out.println(v);

		// 3. "호" 으로 끝나는 객체를 찾아서, 만일 있다면 삭제하자.
		Vector<Integer> del = new Vector<Integer>();		
		for (String s : v) {
			if (s.endsWith("호")) {
				//여기서 바로지우면 ConcurrentModificationException : 향상된 for문에서 loop 중간에 v의 size가 줄어들면 문제가 생긴다!~
				del.add(v.indexOf(s));
			}
		}		
		for (int i : del) {
			v.remove(i);
		}

		//이렇게 하면 에러가 안나지요~!
//		for(int i=0;i<v.size();i++) {
//			if(v.get(i).endsWith("호"))
//				v.remove(i);
//		}
		
		System.out.println(v);

	}

	private static void prn(Vector<String> v) {
		// TODO Auto-generated method stub
		// 향상된 for문 (forEach) for(
		for (String s : v) {
			System.out.printf("%s\t", s);
		}
	}

}

 

 

 

1.3 LinkedList

양방향 포인터 구조로 데이터의 삽입, 삭제가 빈번할 경우 빠른 성능을 보장

ArrayList와 사용 방법은 똑같지만 내부 구현이 인접 참조 링크를 체인처럼 관리

ArrayList는 객체가 중간 인덱스에 삭제되거나 삽입될 때 뒤의 객체들의 인덱스가 변경되지만, LinkedList는 중간 인덱스의 객체가 변경되더라도 앞뒤 링크만 변경되고 나머지 링크는 변경되지 않음

삭제와 삽입이 빈번히 일어나는 경우 ArrayList보다 LinkedList가 좋은 성능을 발휘한다.

스택, 큐, 양뱡향 큐(dequeue) 등을 만들기 위한 용도로 쓰인다.

 

- 기본 생성자로 LinkedList 객체를 생성

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

 

 

 

2. Set

저장 순서가 유지되지 않으며, 객체를 중복해서 저장할 수 없음

인덱스로 관리하지 않기 때문에 인덱스를 매개 변수로 갖는 메소드가 없음

 

Set 인터페이스로 구현된 클래스

  • HashSet
  • LinkedHashSet
  • TreeSet

Set 인터페이스 구현된 메소드

  • 객체추가
    • boolean add(E e) : 주어진 객체 e 를 추가
  • 객체검색
    • boolean contains(Object o) : 주어진 객체가 저장되어 있는지 여부 확인
    • boolean isEmpty() : 컬렉션이 비어 있는지 조사
    • int size() : 저장되어 있는 전체 객체 수를 리턴
    • Object[] toArray() : 배열로 반환
    • Object[] toArray(Object[] a) : 지정한 배열의 클래스 타입으로 배열로 반환
  • 객체삭제
    • void clear() : 저장된 모든 객체를 삭제
    • boolean remove(Object o) : 주어진 객체를 삭제
    • boolean removeAll(Collection c) : 저장된 모든 객체를 삭제

 

2.1 HashSet

내부적으로 HashMap을 사용

객체들을 순서 없이 저장하고 동일한 객체는 중복 저장하지 않음

저장하려는 객체의 해시코드와 저장된 객체들의 해시코드를 비교해 같은 값이 없다면 객체 추가

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

 

다음 코드를 통해 HashSet 의 사용 예를 살펴보겠다.

package com.test03;

import java.util.HashSet;
import java.util.Set;

public class SetTest01 {

	public static void main(String[] args) {

		/*
		 * Set
		 * 1. 순서가 없다 -> add() 시 자동 정렬 
		 * 2. 중복이 불가능 하다 -> 중복해 입력해도 unique 하게 나타남
		 */
		Set<String> set = new HashSet<String>();

		set.add("1");
		set.add("5");
		set.add("2");
		set.add("7");
		set.add("4");
		set.add("4");
		set.add("4");
		set.add("3");

		System.out.println(set);
		setFind(set, "5");
		setDelete(set,"5");
	}

	public static void setDelete(Set<String> set, String del) {
		for(String d : set) {
			if(d.equals(del)) {
				System.out.println(d + " 지웠다아아아아아아아ㅏㅇ");
				set.remove(d);
				return;
			}
		}
	}

	public static void setFind(Set<String> set, String find) {
		for(String f : set) {
			if(f.equals(find)) {
				System.out.println(find + " 찾았다아아아아아앙!!!");
			}
		}
	}
	
	
}

 

다음 코드는 HashSet(Set : 집합)에서 유용하게 사용할 수 있는 집합과 관련된 메서드들입니다.

package com.test03;

import java.util.HashSet;

public class SetMethods {

	public static void main(String[] args) {
		HashSet<Integer> A = new HashSet<Integer>();
		A.add(1);
		A.add(2);
		A.add(3);
		
		HashSet<Integer> B = new HashSet<Integer>();
		B.add(3);
		B.add(4);
		B.add(5);
				
		HashSet<Integer> C = new HashSet<Integer>();
		C.add(1);
		C.add(2);
		
		HashSet<Integer> M = new HashSet<Integer>();

		// 부분집합(Subset)
		System.out.println(A.containsAll(B));	//f
		System.out.println(A.containsAll(C));	//t
		
		// 합집합(Union)
		A.addAll(B);
		
		// 교집합(Intersect)
		A.retainAll(B);
		
		// 차집합(Difference)
		A.removeAll(B);
		
	}
}

 

2.2 LinkedHashSet

해시 테이블에 데이터를 저장

저장된 순서에 따라 순서가 결정된다.

추가된 순서, 또는 가장 최근에 접근한 순서대로 접근 가능

 

2.3 TreeSet

정렬기능이 추가되었고 동일한 객체는 중복 저장하지 않음

정렬된 순서대로 보관하며 정렬 방법을 지정할 수 있음

범위 검색 작업에 효과적

레드블랙트리 구조를 사용

HashSet보다 성능상 느리지만 삽입과 동시에 정렬할 때 사용

 

 

3. Map

키와 값으로 구성된 엔트리 객체를 저장하며, 키와 값 모두 객체로, 키는 중복될 수 없지만 값은 중복이 가능하다. 

기존에 저장된 키로 값을 저장하면 덮어씌워진다.

 

Map 인터페이스로 구현된 클래스

  • HashMap
  • Hashtable
  • Properties

Map 인터페이스 구현된 메소드

  • 객체추가
    • V put(K key, V value) : 주어진 키와 값을 추가
  • 객체검색
    • V get(Object key) : 주어진 키의 값을 리턴
    • boolean contains(Object key) : 주어진 키가 있는지 여부
    • boolean containsValue(Object value) : 주어진 값이 있는지 여부
    • Set<Map.Entry<K, V>> entrySet() : 주어진 키와 값의 쌍으로 구성된 모든 엔트리 객체를 Set에 담아 리턴
    • boolean isEmpty() : 컬렉션이 비어 있는지 조사
    • int size() : 저장되어 있는 전체 객체 수를 리턴
    • Set<K> keySet() : 모든 키를 Set 객체에 담아 리턴
    • Collection<V> values() : 모든 값을 Collection 객체에 담아 리턴
  • 객체삭제
    • void clear() : 저장된 엔트리 객체를 삭제
    • V remove(Object key) : 주어진 키와 일치하는 엔트리객체를 삭제

 

3.1 HashMap

Map 인터페이스를 구현한 대표 Map 컬렉션

키는 중복 저장을 허용하지 않으며, 값은 중복을 허용

동일한 키가 될 조건은 hashCode()의 리턴값이 같고, equals() 메소드의 결과가 true 이어야 함

 

3.2 Hashtable

HashMap과 동일한 내부 구조를 가지고 있음

HashMap과 달리 동기화된 메소드로 구성되어 있어 밀티 스레드 환경에서 동시에 구현된 메소드들을 실행할 수 없다.

 

3.3 Properties

Hashtable의 하위 클래스로, 키와 값을 String 타입으로 제한

애플리케이션의 옵션 정보, 데이터베이스 연결 정보, 다국어 정보가 저장된 프로퍼티(*.properties) 파일을 읽을 때 주로 사용하며, 예제에 관련 IO 클래스를 포함하여 작성하였다.

 

4. Stack 과 Queue

stack : 후입선출(LIFO : Last In First Out) 자료구조

queue : 선입선출(FIFO : First In First Out) 자료구조

 

4.1 Stack

LIFO 자료구조를 구현한 클래스

 

- Stack 객체 생성

Stack<E> stack = new Stack<E>();

 

- Stack 클래스 주요 메소드

  • push(E item) : E
    • 주어진 객체를 스택에 추가
  • peek() : E
    • 스택의 최상단(top) 객체를 가져온다. 객체를 제거하지 않음
  • pop() : E
    • 스택의 최상단(top) 객체를 가져온다. 가져온 객체를 제거함

 

4.2 Queue

FIFO 자료구조를 구현한 클래스

 

- Queue 객체 생성

Queue<E> queue = new Queue<E>();

 

- Queue 클래스 주요 메소드

  • offer(E e) : boolean
    • 주어진 객체를 추가
  • peek() : E
    • 가장 처음 추가된 객체(front)를 가져온다. 객체를 제거하지 않음
  • pop() : E
    • 가장 처음 추가된 객체(front)를 가져온다. 가져온 객체를 제거함

 

 

5. 동기화된 컬렉션

대부분의 컬렉션 클래스들은 싱글 스레드 환경에서 사용할 수 있도록 설계

멀티 스레드환경에서 무결성을 보장하지 못 함

Vector와 Hashtable은 동기화된 메소드로 구성되어 있어 멀티 스레드 환경에서 안전하게 사용 가능

싱글 스레드 환경에 맞게 설계된 컬렉션 객체들을 동기화된 메소드로 래핑할 수 있도록 Collections가 메소드들을 제공한다.

 

- Collections의 동기화 래핑 메소드

  • synchronizedList(List<T> list) : List<T>
    • List를 동기화된 List로 리턴
  • synchronizedMap(Map<K, V> m) : Map<K, V>
    • Map을 동기화된 Map으로 리턴
  • synchronizedSet(Set<T> s) : Set<T>
    • Set을 동기화된 Set으로 리턴

- Collections의 동기화 객체 생성

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

Map<K, V> syncMap = Collections.synchronizedMap(map);

Set<T> syncSet = Collections.synchronizedSet(set);

 

6. 제네릭 (Generic)

다양한 타입의 객체들을 다루는 메소드나 클래스의 타입을 미리 명시해 줌으로써 컴파일 시 타입을 체크할 수 있도록 하고, 형변환을 하는 번거로움 없이 사용하게 해주는 것으로 배열이나 컬렉션 사용 시 데이터 타입의 안정성을 높일 수 있다.

(* 타입 안정성 : 의도하지 않는 타입의 객체가 저장되는 것을 막고, 저장된 객체를 의도하지 않은 타입으로 사용되는 것을 막는 것)

 

6.1 제네릭 선언

제네릭은 클래스와 메소드에 선언 가능

 

- 제네릭 클래스 선언

[접근제한자] class [클래스이름] <Type1, Type2, ...> {}

클래스를 선언할 때 클래스 이름 옆에 < > 를 이용하여 데이터 타입이나 타입 변수를 지정해주면 된다. 제네릭 타입 변수는 식별자 명명 규칙에 따라 어느 것이든 가능하지만 일반적으로 T를 많이 사용한다. 이후 클래스 내부에 변수나 메소드를 선언할 때 일반 데이터 타입과 동일하게 사용하면 되며, 타입 변수를 특정 데이터 타입으로 지정해주면 해당 타입으로 메모리에 할당하여 사용 가능하다.

만약 두 개 이상의 타입으로 제네릭 타입 변수를 사용하고 싶다면 콤마로 구분하여 다른 이름으로 타입 변수를 지정해주면 된다.

 

6.2 제네릭 메소드

메소드에서 사용할 타입을 제네릭을 이용하여 선언할 수 있다.

[접근제한자] <Type1, Type2, ...> [리턴타입] [메소드이름] (매개변수, ...) {...}

제네릭 메소드를 호출할 때에는 타입 변수를 명시적으로 지정할 수도 있고, 컴파일러가 매개값의 타입을 보고 추정할 수 있도록 할 수도 있다.

 

6.3 제네릭 제한하기

일반적인 방법으로 제네릭을 이용햇을 경우 타입에 대한 제한이 없다. 하지만 제네릭 타입 변수에 extends 키워드를 사용하면 타입의 종류를 제한할 수 있다.

class Point {
	int x;
    	int y;
}

class Triangle<T extends Point> {
	T pos1, pos2, pos3;
}

 

6.4 와일드카드

타입 변수를 매개변수나 리턴 타입으로 사용할 때 구체적인 타입 대신 와일드 카드를 사용할 수 있다. 타입 변수를 대치할 수 있는 구체적인 타입으로 제한을 두지 않거나 extends 키워드를 이용해 해당 타입과 자손 타입들만 가능하도록 제한하거나 super키워드를 이용해 해당 타입 부모 타입들만 가능하도록 제한하는 방법이 있다.

 

- 와일드카드 이용 3가지 방법

<?> : 제한 없음

<? extends 상위타입> : 상위 클래스 제한

<? super 하위타입> : 하위 클래스 제한

 

6.5 제네릭 상속

제네릭 클래스도 다른 클래스와 마찬가지로 부모 클래스가 될 수 있다.

부모 클래스의 제네릭 타입을 상속해서 자식 클래스에서도 제네릭 타입을 상속할 수 있다.

public class ChildProduct<T, K> extends Product<T, K> {...}

 

자식 제네릭 클래스는 추가적인 타입 변수를 가질 수 있다.

public class ChildProduct<T, K, S> extends Product<T, K> {...}

 

코드

 

다음 코드를 통해 비 제네릭 코드를 살펴보고 이에 따른 치명적인 문제점에 대해 살펴보겠다.

package com.generic;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Generic<T> {

	/*
	 * <> : generic -> collection 안에 들어가는 값의 타입을 강제한다. 
	 * -> 만약 타입이 여러가지 종류일 경우 sort() 등의 메서드에서 의도치 않은 오류가 발생할 수 있습니다.
	 */
	public static void main(String[] args) {
		List list = new ArrayList();
		list.add("1");
		list.add(2);
		list.add('c');
		list.add(null);
		list.add(3);
		
		System.out.println(list);

		Collections.sort(list);	// 비 제네릭
		System.out.println(list);	
	}
}

타입이 분명하지 않은 list를 정렬하려 할 때 java.lang.CalssCastException 이 발생한 것을 볼 수 있다.

 

다음은 제네릭 타입을 이용해 class 를 구성한 모습을 살펴 보겠다.

package com.generic;

/*
 * <T> : Type
 * <E> : Element
 * <K> : Key
 * <V> : Value
 */
public class Emp<T> {

	private T empno;
	private String ename;

	public Emp() {

	}

	public Emp(T empno, String ename) {
		super();
		this.empno = empno;
		this.ename = ename;
	}

	public T getEmpno() {
		return empno;
	}

	public void setEmpno(T empno) {
		this.empno = empno;
	}

	public String getEname() {
		return ename;
	}

	public void setEname(String ename) {
		this.ename = ename;
	}

	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return "[" + empno + "] " + ename;
	}
}
package com.generic;

public class MTest {

	public static void main(String[] args) {

		Emp<String> samsong = new Emp<String>();
		samsong.setEname("홍길동");
		samsong.setEmpno("dev0015");

		Emp<Integer> ig = new Emp<Integer>(19019, "이순신");

		Emp dhit = new Emp("abcd", "김선달");

		System.out.println(samsong);
		System.out.println(ig);
		System.out.println(dhit);

	}
}

클래스의 제네릭 타입을 T 로 설정하고 생성자에 Integer타입을 기입해서 empno Field에 Integer값을 문제없이 초기화한 모습을 볼 수 있다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함