[자바의 정석 기초편] 컬렉션 프레임워크 1

JEREGIM·2023년 3월 6일
0

자바의 정석 기초편

목록 보기
14/23

📌컬렉션 프레임워크(Collections framework)

컬렉션(collection) : 여러 객체(데이터)를 모아 놓은 것

프레임워크(framework) : 표준화, 정형화된 체계적인 프로그래밍 방식

컬렉션 프레임워크(collections framework)

  • 컬렉션(다수의 객체)를 다루기 위한 표준화된 프로그래밍 방식
  • 컬렉션을 쉽고 편리하게 다룰 수 있는 다양한 클래스들을 제공
  • java.util 패키지에 포함

컬렉션 클래스(collection class)

  • 다수의 데이터를 저장할 수 있는 클래스(Vector, ArrayList, HashSet 등)

컬렉션 프레임워크의 핵심 인터페이스

List 인터페이스 - 저장 순서O, 중복O

: 저장 순서가 있는 데이터의 집합. 데이터의 중복을 허용한다.

  • 예시) 대기자 명단

  • 구현 클래스 : ArrayList, LinkedList, Stack, Vector 등

List 인터페이스 계층 구조

- 핵심 : ArrayList, LinkedList

List 인터페이스의 메서드

  • get() : 읽기, set() : 변경, indexOf() : 검색, sort() : 정렬

Set 인터페이스 - 저장 순서X, 중복X

: 저장 순서를 유지하지 않는 데이터의 집합. 데이터의 중복을 허용하지 않는다.

  • 예시) 소수의 집합, 육식동물의 집합

  • List와 특징이 정반대

  • 구현 클래스 : HashSet, TreeSet 등

Set 인터페이스 계층 구조

- 핵심 : HashSet, TreeSet

Set 인터페이스의 메서드

Collection 인터페이스의 메서드와 동일

집합과 관련된 메서드들(Collection에 변화가 있으면 true, 아니면 false를 반환)

메서드설명
boolean addAll(Collection c)저장된 Collection의 객체들을 Collection에 추가한다.(합집합)
boolean containsAll(Collection c)저장된 Collection의 객체들이 Collection에 포함되어 있는지 확인한다.(부분집합)
boolean removeAll(Collection c)저장된 Collection에 포함된 객체들을 삭제한다.(차집합)
boolean retainAll(Collection c)저장된 Collection에 포함된 객체만을 남기고 나머지는 Collection에서 삭제한다.(교집합)

Map 인터페이스 - 저장 순서X, 중복(키X, 값O)

: 키(key)와 값(value)의 쌍(pair)으로 이루어진 데이터의 집합. 저장 순서는 유지하지 않으며, 키는 중복을 허용하지 않고 값은 중복을 허용한다.

  • 예시) 아이디(key)-패스워드(value)

  • 구현 클래스 : HashMap, TreeMap, Hashtable, Properties 등

Map 인터페이스 계층 구조

- 핵심 : HashMap, TreeMap
  • LinkedHashMap : 저장 순서가 있음

  • Hashtable과 HashMap은 유사. 동기화 유무 차이(Hashtable 동기화O, HashMap 동기화X)

Map 인터페이스의 메서드

  • put(), putAll() : 추가, containsKey(), containsValue(), get() : 검색, remove() : 삭제

  • Set keySet() : 키만 읽기, Collection values() : 값만 읽기, Set entrySet() : Entry(키-값) 읽기

  • Collection values() : 리턴 타입이 Collection이기 때문에 순서와 중복이 상관 없음

  • Set keySet(), Set entrySet() : 리턴 타입이 Set이기 때문에 순서X, 중복X

Collection 인터페이스

Collection 인터페이스 계층 구조

- List 인터페이스와 Set 인터페이스의 공통 부분을 뽑아서 Collection 인터페이스를 만들었다.

Collection 인터페이스의 메서드

  • add() : 추가, contains() : 검색, remove() : 삭제, clear() : 전체 삭제

📌ArrayList

ArrayList는 기존의 Vector를 개선한 것으로 구현 원리와 기능적으로 동일. ArrayList는 동기화X, Vector는 동기화O

List 인터페이스를 구현하므로, 저장 순서가 유지되고 중복을 허용한다.

데이터의 저장 공간으로 배열을 사용한다.(배열 기반)

ArrayList의 메서드

생성자

  • ArrayList() : 기본 생성자

  • ArrayList(Collection c) : 컬렉션끼리 변환할 때 사용

  • ArrayList(int initialCapacity) : 배열의 길이 지정

추가

  • boolean add(Object o) : 객체를 추가. 성공하면 true, 실패하면 false

  • void add(int index, Object element) : 저장 위치(index) 지정 가능

  • boolean addAll(Collection c) : 컬렉션의 모든 요소를 추가

  • boolean addAll(int index, Collection c) : 컬렉션의 모든 요소를 저장 위치(index)에 추가

삭제

  • boolean remove(Object o) : 객체 삭제

  • Object remove(int index) : index의 객체 삭제

  • boolean removeAll(Collection c) : 지정한 컬렉션의 객체들을 삭제

  • void clear() : 모든 객체 삭제

검색

  • int indexOf(Object o) : 객체가 몇 번째 index에 있는지 반환, 못 찾으면 -1 반환

  • int lastindexOf(Object o) : 끝에서부터 객체를 찾음.

  • boolean contains(Object o) : 객체가 존재하는지 물어봄. 있으면 true, 없으면 false

  • Object get(int index) : index 위치의 객체를 읽음

  • Object set(int index, Object element) : index의 위치에 있는 객체를 변경

기타

  • List subList(int from, int to) : from부터 to까지의 인덱스를 뽑아내서 새로운 리스트를 만듬(to는 포함x)

  • Object[] toArray() : ArrayList의 객체 배열을 반환

  • boolean isEmpty() : ArrayList가 비어있으면 true, 아니면 false

  • void trimToSize() : 빈 공간을 제거

  • int size() : 저장된 객체의 개수를 반환

실습 예제

ArrayList list1 = new ArrayList(10);
list1.add(new Integer(5));
list1.add(4);
list1.add(new Integer(2));
list1.add(new Integer(0));
list1.add(new Integer(1));
list1.add(new Integer(3))

ArrayList list2 = new ArrayList(list1.subList(1,4)); 

list1 = [5, 4, 2, 0, 1, 3]
list2 = [4, 2, 0]

  • ArrayList list1 = new ArrayList(10); : 기본 길이(용량, Capacity)가 10인 ArrayList 생성

  • list1.add(4); : 이렇게 해도 추가가 된다. 컴파일러가 오토 박싱을 통해서 list1.add(new Integer(4)); 이렇게 자동으로 변환해준다.

  • list1.subList(1,4) : 인덱스 1부터 4 전까지의 객체를 뽑아 새로운 ArrayList를 만든다. 인덱스 4는 포함x

  • ArrayList list2 = new ArrayList(list1.subList(1,4)); : ArrayList(Collection c) 생성자를 통해 list2를 만들어준다.

Collections.sort(list1);
Collections.sort(list2);

list1 = [0, 1, 2, 3, 4, 5]
list2 = [0, 2, 4]

  • 오름차순으로 정렬
  • Collections 는 유틸 클래스이다. 헷갈리지 말자. Collection 이 인터페이스
System.out.println("list1.containsAll(list2) = " 
+ list1.containsAll(list2));

list1.containsAll(list2) = true

  • list1이 list2의 모든 요소를 포함하고 있는지
list2.add("B");
list2.add("C");
list2.add(3, "A");

list2 = [0, 2, 4, A, B, C]

  • list2.add("B"); : list2의 맨 뒤에 추가된다.
  • list2.add(3, "A"); : 인덱스가 3인 위치에 "A"가 추가된다.
list2.set(3, "AA");

list2 = [0, 2, 4, AA, B, C]

  • list2.set(3, "AA"); : 인덱스가 3인 위치의 객체가 "AA"로 변경된다.
list1.add(0, "1");
int result1 = list1.indexOf("1");
int result2 = list1.indexOf(1);

list1 = [1, 0, 1, 2, 3, 4, 5]
result1 = 0
result2 = 2

  • list1에서 인덱스가 0인 첫 번째 1은 String 타입의 "1" 이고 인덱스가 2인 두 번째 1은 Integer 타입의 new Integer(1) 이다.
list1.remove(1);

list1 = [1, 1, 2, 3, 4, 5]

  • 왜 0이 없어진걸까? 객체 new Integer(1) 이 아닌 인덱스가 1인 new Integer(0) 객체가 제거됐기 때문이다. 따라서 new Integer(1) 객체를 삭제하고 싶다면 정확하게 list1.remove(new Integer(1)); 라고 해줘야 한다.
System.out.println("list1.retainAll(list2) = " 
+ list1.retainAll(list2));

list1.retainAll(list2) = true
list1 = [2, 4]
list2 = [0, 2, 4, AA, B, C]

  • list1.retainAll(list2) : list1에서 list2와 겹치는 부분만 남기고 나머지는 모두 삭제
for (int i = list2.size() - 1; i >= 0; i--) {
    if (list1.contains(list2.get(i)))
        list2.remove(i);
}

list1 = [2, 4]
list2 = [0, AA, B, C]

  • list2에서 list1에 포함된 객체들을 삭제

    1. get(i)로 list2에서 객체를 하나씩 꺼낸다.
    2. contains()로 꺼낸 객체가 list1에 있는지 확인한다.
    3. remove(i)로 해당 객체를 list2에서 삭제한다.

ArrayList에 저장된 객체의 삭제 과정

ArrayList인 alphabet에서 "C"를 삭제하는 과정

ArrayList<String> alphabet = new ArrayList<>(7); // 배열의 길이 7
alphabet.add("A");
alphabet.add("B");
alphabet.add("C");
alphabet.add("D");
alphabet.add("E");

alphabet.remove(2); // 인덱스가 2인 위치의 "C" 삭제

1. 삭제할 데이터의 다음 데이터부터 마지막 데이터까지 한 칸씩 위로 복사해서 삭제할 데이터를 덮어쓴다.

System.arraycopy(alphabet, 3, alphabet, 2, 2);
  • alphabet[3] 부터 2(마지막 인자)만큼의 길이를 alphabet[2]에 복사한다는 뜻이다.

2. 데이터가 모두 한 칸씩 이동했으므로 마지막 데이터를 null로 변경한다.

alphabet[size - 1] = null;

3. 데이터가 삭제되어 데이터의 개수가 줄었으므로 size의 값을 감소시킨다.

size--;

[참고] 마지막 데이터를 삭제하는 경우는 1번의 과정(배열 복사)이 필요 없다.

결국, 배열의 복사가 일어나는 1번 과정이 부담이 많이 가는 과정이기 때문에 되도록이면 1번 과정이 없는 것이 좋다.

ArrayList에 저장된 첫 번째 객체부터 삭제하는 경우(배열의 복사 발생)

for(int i = 0; i < alphbet.size(); i++) {
	alphabet.remove(i);
}

  • 첫 번째 객체부터 삭제하면 배열의 복사(1번 과정)가 반복되기 때문에 전부 삭제가 되지 않는다.

ArrayList에 저장된 마지막 객체부터 삭제하는 경우(배열의 복사 발생X)

for(int i = alphbet.size() -1; i >= 0; i--) {
	alphabet.remove(i);
}
  • 마지막부터 삭제하면 배열의 복사가 발생하지 않기 때문에 제대로 전부 삭제가 된다.
  • 배열의 복사가 발생하지 않기 때문에 부담이 안가서 성능도 더 좋다.

📌LinkedList

배열의 장단점
장점 : 배열은 구조가 간단하고 데이터를 읽는데 걸리는 시간(접근 시간, access time)이 짧다.

단점1 : 크기를 변경할 수 없다.

  • 크기를 변경해야 하는 경우 새로운 배열을 생성 후 데이터를 복사해야 한다.
    (더 큰 배열 생성 -> 복사 -> 참조 변경)
  • 크기 변경을 피하기 위해 충분히 큰 배열을 생성하면 메모리가 낭비된다.

단점2 : 비순차적인 데이터의 추가, 삭제에 시간이 많이 걸린다.

  • 중간 데이터를 추가, 삭제하기 위해 다른 데이터를 옮겨야 한다.
  • 순차적인 데이터 추가(끝에 추가), 삭제(끝에 삭제)는 빠르다.

배열의 단점을 복사하기 위해 나온게 LinkedList 이다.

  • 배열과 달리 LinkedList는 불연속적으로 존재하는 데이터를 연결(link)한다.
class Node {
	Node next;
    Object data;
}
  • next : 다음 노드의 주소

  • data : 데이터 값

  • 데이터 삭제 : 단 한번의 참조 변경만으로 가능

  • 데이터 추가 : 한번의 Node 객체 생성과 두번의 참조 변경만으로 가능

LinkedList - 이중 연결 리스트

LinkedList : 연결리스트. 데이터 접근성이 나쁘다.

더블리 링크드 리스트(doubly linked list) : 이중 연결 리스트. 접근성 향상

더블리 써큘러 링크드 리스트(doubly circular linked list) : 이중 원형 연결 리스트

ArrayList vs. LinkedList 성능 비교

  • 순차적으로 데이터 추가/삭제 : ArrayList가 빠르다.

  • 비순차적으로 데이터 추가/삭제 : LinkedList가 빠르다.

  • 접근 시간(access time) : ArrayList가 빠르다.

    인덱스가 n인 데이터의 주소 = 배열의 주소 + n * 데이터 타입의 크기

ArrayList : 배열의 복사를 줄이기 위해 배열을 크게 설정하게 되면 비효율적인 메모리 사용이 된다.(배열 기반 - 연속적)

LinkedList : 데이터가 많을수록 접근성이 떨어진다.(연결 기반 - 불연속적)


📌스택과 큐(Stack & Queue)

스택(Stack) : LIFO(Last In First Out) 구조. 마지막에 저장된 것을 제일 먼저 꺼낸다. ArrayList로 구현하는게 유리

큐(Queue) : FIFO(First In First Out) 구조. 제일 먼저 저장한 것을 제일 먼저 꺼낸다. LinkedList로 구현하는게 유리

스택(Stack)의 메서드

메서드설명
boolean empty()Stack이 비어있는지 알려준다.
Object peek()Stack 맨 위에 저장된 객체를 반환한다. pop()과 달리 객체를 꺼내지는 앟는다.(비어있는 Stack이면 EmptyStackException 발생)
Object pop()Stack 맨 위에 저장된 객체를 꺼낸다.(비어있는 Stack이면 EmptyStackException 발생)
Object push(Object item)Stack에 item을 저장한다.
int search(Object o)Stack에서 주어진 객체(o)를 찾아서 그 위치를 반환한다. 못 찾으면 -1 반환(배열과 달리 위치가 0이 아닌 1부터 시작한다.)

Stack 클래스가 있기 때문에 Stack st = new Stack(); 사용 가능

큐(Queue)의 메서드

메서드설명
boolean add(Object o)저장된 객체를 Queue에 추가한다. 성공하면 true, 저장 공간이 부족하면 IlliegalStateException 발생
Object remove()Queue에서 객체를 꺼내 반환한다. 비어있으면 NoSuchElementException 발생
Object element()삭제 없이 요소를 읽어온다. peek()과 달리 Queue가 비어있으면 NoSuchElementException 발생
boolean offer(Object o)Queue에 객체를 저장한다. 성공하면 true. 실패하면 false 반환. 예외 발생x
Object poll()Queue에서 객체를 꺼내서 반환한다. 비어있으면 null 반환. 예외 발생x
Object peek()삭제 없이 요소를 읽어온다. Queue가 비어있으면 null 반환. 예외 발생x

Queue는 인터페이스이기 때문에 Queue를 구현한 클래스로 객체를 생성해야 한다.

  • Queue를 구현한 클래스 목록
Queue q = new LinkedList();
q.offer("A");
  • 참조변수 타입을 Queue로 하게 되면 나중에 객체를 다른 클래스로 변경해도 그 아래의 코드들을 검토할 필요가 없다. 참조 변수 타입이 Queue라면 Queue에 있는 메서드들만 사용했다는 의미가 되기 때문이다.

스택과 큐의 활용

스택의 활용 예 : 수식 계산, 수식 괄호 검사, 워드프로세서의 undo/redo, 웹브라우저의 뒤로/앞으로

수식 괄호 검사

Stack<String> st = new Stack<>();
String expression = "((2+4)*7)/2";
System.out.println("expression = " + expression);

try {
    for (int i = 0; i < expression.length(); i++) {
       if (expression.charAt(i) == '(')
            st.push(expression.charAt(i) + "");
        else if (expression.charAt(i) == ')')
            st.pop();
    }
    if (st.empty())
        System.out.println("괄호가 일치합니다.");
    else
        System.out.println("괄호가 불일치합니다.");

} catch (EmptyStackException e) {
    System.out.println("괄호가 불일치합니다. 예외 발생");
}

expression = ((2+4)*7)/2
괄호가 일치합니다.

expression = ((2+4)*7/2
괄호가 불일치합니다.

  • 스택 안에 '('가 아직 남아있어서 괄호가 불일치합니다 라고 출력된다.

expression = ((2+4)*7))))/2
괄호가 불일치합니다. 예외 발생

  • 스택이 비어있는데도 st.pop()이 실행되서 EmptyStackExcpetion 예외가 발생

큐의 활용 예 : 최근 사용 문서, 인쇄 작업 대기 목록, 버퍼(buffer)

최근 사용 명령어 출력

public static void save(String input) {
    if (!"".equals(input))
        q.offer(input);

    if (q.size() > MAX_SIZE)
        q.poll();
}
  • if (!"".equals(input)) : 빈 문자열이 입력 받은 것과 같지 않다면 동작. !input.equals("") 이렇게 적어도 되지만 이러면 input이 null 일때 NullPointerException이 발생한다. !"".equals(input) 이렇게 작성해주면 input이 null 이어도 예외가 발생하지 않고 빈 문자열인 "" 도 null이 될수 없기 때문에 이렇게 작성해준다.
final int SIZE = list.size();
for (int i = 0; i < SIZE; i++) {
	System.out.println((i + 1) + ". " + list.get(i));
}
  • 범위를 i < list.size() 이렇게 지정해주면 for 문을 돌 때 마다 list.size()를 호출하게 된다. for 문을 돌 때 size()는 바뀌지 않으므로 final int SIZE = list.size(); 로 size()를 한번만 알아내고 for 문을 돌 때는 i < SIZE 이렇게 상수를 이용하는게 좋은 코드이다.

📌Iterator

컬렉션에 저장된 데이터를 접근하는데 사용되는 인터페이스

메서드설명
boolean hasNext()읽어올 요소가 남아있는지 확인한다. 있으면 true, 없으면 false 반환
Object next()다음 요소를 읽어온다.

컬렉션에 저장된 요소들을 읽어오는 방법을 표준화한 것

List list = new ArrayList(); // 다른 컬렉션으로 바꿀 때 이 부분만 고치면 된다.
Iterator it = list.iterator();

while (it.hasNext())
	System.out.println(it.next());
  • 컬렉션마다 저장된 객체들을 읽어오는 방법이 다르다. Iterator 인터페이스를 쓰게 되면 다른 컬렉션으로 변경되도 읽어오는 방법은 다 똑같기 때문에 코드의 변경이 줄어든다.
public interface Collection {
	...
    public Iterator iterator();
    ...
}    
  • iterator() 메서드는 Collection 인터페이스 안에 정의되어 있기 때문에 List 인터페이스와 Set 인터페이스에 전부 구현 되어있는 메서드이다.

실습 예제

import java.util.*;

class Ex11_5 {
    public static void main(String[] args) {
		Collection<Integer> c = new ArrayList<>();
//		Collection<Integer> c = new LinkedList<>();
        c.add(1);
        c.add(2);
        c.add(3);
        c.add(4);

        Iterator<Integer> it = c.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}
  • Collection<Integer> c = new ArrayList<>(); : 참조변수 타입을 Collecion으로 하게 되면 컬렉션을 LinkedList 로 바꿔도 그 다음의 코드를 바꾸거나 검토할 필요가 없게 된다. 참조 변수 타입이 Collecion 이기 때문에 그 다음의 코드는 Collection에 있는 메서드를 사용했음을 의미하고 따라서 객체를 다른 컬렉션으로 바꿔도 동작한다는 의미이기 때문이다.

Map과 Iterator

Map에는 iterator()가 없다.

Set keySet(), Set entrySet(), Collection values()로 키 또는 값을 Set, Collecion 형식으로 바꾼 후에 iterator() 사용

짝수의 값(value)을 전부 더하는 예제

Collection values() 사용

HashMap<Character, Integer> hashMap = new HashMap<Character, Integer>(){{
    put('a', 1);
    put('b', 4);
    put('c', 6);
    put('d', 9);
}};

int sum = 0;
Collection<Integer> c = hashMap.values();
Iterator<Integer> it = c.iterator();

while (it.hasNext()) {
int i = it.next();
if (i % 2 == 0)
	sum += i;
}
System.out.println("sum = " + sum);

sum = 10

Set keySet() 사용

HashMap<Character, Integer> hashMap = new HashMap<Character, Integer>(){{
    put('a', 1);
    put('b', 4);
    put('c', 6);
    put('d', 9);
}};

int sum = 0;
Set<Character> keySet = hashMap.keySet();
Iterator<Character> it = keySet.iterator();

while (it.hasNext()) {
int i = hashMap.get(it.next());
if (i % 2 == 0)
	sum += i;
}
System.out.println("sum = " + sum);

sum = 10

Set entrySet() 사용

HashMap<Character, Integer> hashMap = new HashMap<Character, Integer>(){{
    put('a', 1);
    put('b', 4);
    put('c', 6);
    put('d', 9);
}};

int sum = 0;
Set<Map.Entry<Character, Integer>> entrySet = hashMap.entrySet();
Iterator<Map.Entry<Character, Integer>> it = entrySet.iterator();

while (it.hasNext()) {
int i = it.next().getValue();
if (i % 2 == 0)
	sum += i;
}
System.out.println("sum = " + sum);

sum = 10

  • getValue() : Entry 객체의 Value 객체를 반환

0개의 댓글