Java 파트13-1. 컬렉션 프레임워크

1. 시작하기 전에


  • 프레임 워크 : 자바는 자료구조를 사용해서 객체들을 효율적으로 추가, 삭제, 검색할 수 있도록 인터페이스와 구현 클래스를 java.util 패키지에서 제공한다. 이들을 총칭해서 컬렉션 프레임 워크라고 한다.
  • List, Set, Map : 컬렉션 프레임워크의 주요 인터페이스, 이것들은 컬렉션 클래스를 사용하는 방법을 정의한 것
  • 컬렉션 요소는 객체들만 가능하다. -> 기본 타입 int, char, double은 불가능


2. List 컬렉션


List 컬렉션은 배열과 비슷하게 객체를 인덱스로 관리한다. 차이점은 저장 용량이 자동으로 증가, 객체 저장시 자동 인덱스 부여, 추가 삽입 삭제 검색 등을 위한 다양한 메소드 제공이다.
List 컬렉션은 객체 자체 저장이 아니라 객체의 번지를 참조한다. 따라서 동일한 객체를 중복 저장할 경우 동일한 번지를 참조한다. List 컬렉션에는 ArrayList, Vector, LinkedList 등이 있는데 다음은 List 컬렉션에서 공통적으로 사용 가능한 List 인터페이스의 메소드이다.

  • 객체 추가
    • boolean add(E e) : 주어지 객체를 맨 끝에 추가
    • void add(int index,E element) : 주어진 인덱스에 객체를 추가
    • E set(int index, E element) : 주어진 인덱스에 저장된 객체를 주어진 객체로 교체
    • boolean addAll(Collection<? extendsE> c) : 컬렉션 c의 모든 요소를 맨 뒤에 추가
  • 객체 검색
    • boolean contains(Object o) : 주어진 객체가 저장되어있는지 조사
    • E get(int index) : 주어진 인덱스에 저장된 객체를 리턴
    • boolean isEmpty() : 컬렉션이 비어있는지 조사
    • int size() : 저장되어 있는 전체 객체 수 리턴
    • int capacity() : 현재 용량을 리턴
    • int indexOf(Object o) : o와 같은 첫 번째 요소의 인덱스 리턴, 없으면 -1 리턴
  • 객체 삭제
    • void clear() : 저장된 모든 객체 삭제
    • E remove(int index) : 주어진 인덱스에 저장된 객체를 삭제
    • boolean remove(Object o) : 주어진 객체를 삭제

위에서 E는 저장되는 객체의 타입을 List 컬렉션을 생성할 때 결정하라는 뜻이다.

import java.util.List;

public class Test {
    public static void main(String[] args) {
        List<String> list...;
        list.add(1,"홍길동");
        String str = list.get(1);
    }
}

List list = ..; 는 List 컬렉션에 저장되는 객체를 String 타입으로 하겠다는 뜻이다. 위에서 언급한 E는 String 타입이 되는 것이다. 따라서 add의 파라미터와 get의 리턴값은 문자열이 된다. 다음은 List 컬렉션에 저장된 모든 객체 대상을 하나씩 가져와보자. 일반 for문은 쉽게 이해할 수 있지만, 향상된 for문은 코드를 줄일 수 있으므로 알아두자.

List<String> list = ...;
for (int i=0;i<list.size();i++){
    String str = list.get(i);
}

//향상된 for문
// 자동적으로 list에 저장된 총 객체 수만큼 루핑하고
// 객체를 하나씩 str에 대입
for (String str : list){

}


ArrayList

ArrayList는 List 인터페이스의 대표적인 구현 클래스이다.

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

// 예시
package selftest;

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

public class codetest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("홍길동");
        list.add("장보고");
        list.add("해신");
        list.add(1,"메롱");
        list.remove(1);
        System.out.println("객체 수 :"+ list.size());
        System.out.println("1인덱스값 :"+ list.get(1));
    }
}

두 번째 코드와 같이 E타입 파라미터를 생략하면 앞에 List로 지정된 E타입 파라미터를 따라간다. ArrayList 객체를 생성하면 기본으로 10개의 객체를 저장할 수 있는 용량이 생성되고 객체 수가 늘어나면 자동으로 증가한다.
ArrayList는 파이썬의 배열과 같다. 0번 인덱스부터 차례대로 저장되고 특정 인덱스 객체 제거시 바로 뒤에서 부터 인덱스는 당겨지고, 추가시 바로 뒤에서부터 인덱스는 증가한다. 따라서, 객체 삭제와 삽입이 자주 일어나는 경우에는 사용하지 않는게 좋다.

Vector

Vector은 ArrayList와 동일한 내부 구조를 가지고 있으며 선언방식도 같다. 차이점은 Vector은 동기화된 메소드로 구성되어 있기 때문에 멀티 스레드가 동시에 Vector메소드를 실행할 수 없다. 그래서 멀티 스레드 환경에서 안전하게 객체를 추가, 삭제할 수 있다.
추가로 lastElement() 메서드로 마지막 인덱스의 값을 가져올 수도 있다.

// 클래스 파일
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;
    }
}

// 메인
public class codetest {
    public static void main(String[] args) {
        List<Board> list = new Vector<>();
        
        // 객체 삽입
        list.add(new Board("제목1","내용1","글쓴이1"));
        list.add(new Board("제목2","내용2","글쓴이2"));
        list.add(new Board("제목3","내용3","글쓴이3"));
        
        // 객체 제거
        list.remove(1);
        Board board = list.get(1); // Board 객체 하나 꺼내기
        System.out.println(board.subject);// 꺼낸 객체 사용
    }
}


LinkedList

LinkedList는 List 구현 클래스로 ArrayList와 사용 방법은 같으나 내부 구조가 다르다. LinkedList는 인접 참조를 링크해서 체인처럼 관리한다. ArrayList의 경우 중간에 객체를 지우면 인덱스를 다 땡겨와야 했다. 하지만 LinkedList는 양옆의 인덱스에 저장되있는 객체번지를 서로 참조하면서 어떤 인덱스가 제거되면 앞뒤의 링크만 수정되기 때문에 전부를 수정하지 않아도 된다. 내부적 동작 방식만 다르고 사용법은 똑같다.

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

        // 삽입
        list.add("장보고");
        list.add("해신");
        list.add("쭉쩡이");

        // 제거
        list.remove(1);
        String str = list.get(1); // 불러오기
        System.out.println(str);
    }
}


3. Set 컬렉션


List 컬렉션은 객체의 저장 순서를 유지하지만, Set 컬렉션은 저장 순서가 유지되지 않는다. 또한 객체를 중복해서 저장할 수 없고, 하나의 null만 저장할 수 있다. 파이썬의 집합과 비슷하다.
Set 컬렉션에는 HashSet, LinkedHashSet, TreeSet 등이 있는데 다음은 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<String> set = ...;
// iterator로 얻은 Iterator 구현 클라스 객체를 Iterator<String>타입 인터페이스 변수에 대입
Iterator<String> iterator = set.iterator();

Iterator< String > 타입의 iterator 변수에 대입한 이유는 반복해서 가져올 객체가 String타입이기 때문이다.
다음은 Iterator 인터페이스에 선언된 메소드이다.

리턴 타입메소드설명
booleanhasNext()가져올 객체가 있으면 true, 없으면 false 리턴
Enext()컬렉션에서 하나의 객체를 가져옴
voidremove()Set 컬렉션에서 객체를 제거

Iterator에서 하나의 객체를 가져올 때는 next를 사용하는데 사용하기 전에 hasNext를 이용해서 true를 반환할 경우 사용하는 것이 좋다. 사실 이렇게 iterator을 복잡하게 사용하지 않고 향상된 for문으로 하면 전체 객체를 대상으로 반복이 가능하다.

Set<String> set = ...;
Iterator<String> iterator = set.iterator();
while(iterator.hasNext()){
    String str = iterator.next();
}

// iterator을 사용하지 않고 향상된 for문을 이용해 전체 객체를 대상으로 반복 가능하다.
for(String str : set){

}


HashSet

HashSet은 Set 인터페이스의 구현 클래스이다. 선언하는 방법은 다른 것들과 똑같다.
HashSet 객체들은 순서 없이 저장하고 동일한 객체는 중복저장하지 않는다. HashSet이 판단하는 동일한 객체란 같은 인스턴스를 의미하는 것이 아니다. HashSet은 객체를 저장하기 전에 hashCode()메소드를 호출해서 이미 저장되어 있는 객체들의 해시코드와 비교한다. 여기서 동일하다면 equals() 메소드로 두 객체를 다시 비교하고 true가 나오면 동일한 객체로 판단하고 중복 저장을 하지 않는다. 즉, hashCode값이 같고 equals도 true면 동등 객체로 판단하여 저장하지 않고 이외의 경우는 저장한다고 보면 된다.
예외적으로 문자열이 같은 String 객체는 동등한 객체로 간주한다. String 클래스가 hashCode와 equals 메소드를 재정의해서 같은 문자열의 경우 hashCode의 리턴값이 같게, equals도 true로 나오도록 했기 때문이다.

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

        set.add("가");
        set.add("나");
        set.add("다");
         set.add("다"); // 동일해서 하나만 저장된다

        // 객체들이 순서 없이 저장된다.
        System.out.println("객체 개수 : "+set.size());
        Iterator<String> iterator = set.iterator();
        // 하나씩 꺼래서 살펴보기
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
        // 하나씩 꺼래서 살펴보기
        for (String str : set){
            System.out.println(str);
        }
        set.clear(); // 전체 비우기
        System.out.println("객체 개수 : "+set.size());
    }


}


hashCode는 객체의 해시코드를 반환하고, equals는 동일한 객체인지 아닌지를 판단한다. 그런데 만약 서로 다른 객체 즉, 인스턴스가 달라도 안에 정보가 같다면 동일한 객체로 간주하여 중복 저장이 되지 않도록 하는 경우가 필요할 때가 있다. 이때는 equals와 hashcode를 재정의하면 된다.

// 클래스
public class Member {
    String name;
    int age;

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

    @Override
    public boolean equals(Object obj){
        if (obj instanceof Member){// obj가 Member의 객체이면
            Member member = (Member) obj;
            return member.name.equals(this.name) && member.age==this.age;
        } else{
            return false;
        }

    }

    @Override
    // name과 age값이 같으면 동일한 hash값 리턴
    public int hashCode(){
        return name.hashCode()+age; // String의 hashCode 이용
   }
}

// 메인
public class HashCodeExam {
    public static void main(String[] args) {
        Set<Member> set = new HashSet<>();

        // 서로 다른 인스턴스지만 재정의한것에 의해 동일한 인스턴스로 판단
        // 중복처리하여 한 개만 저장된다.
        set.add(new Member("메시",30));
        set.add(new Member("메시",30));

        System.out.println(set.size()); // 1

    }
}


4. Map 컬렉션


Set(HashSet), List(ArrayList, Vector, Stack), Queue(LinkedList)는 Collection을 상속받아 단일 클래스의 객체만을 요소로 다룬다는 공통점이 있지만, 이와 달리 Map은 Collection을 상속받지 않는다. Map 컬렉션은 key와 value로 구성된 Map.Entry 객체를 저장하는 구조를 가지고 있다. Entry는 Map 인터페이스 내부에 선언된 중첩 인터페이스이다. key와 value는 모두 객체이다. key값은 중복될 수 없고, value값은 중복이 가능하다. 만일 동일한 key로 저장될 경우 새로운 값으로 대체된다.
Map 컬렉션에는 HashMap, Hashtable, LinkedHashMap, Properties, TreeMap 등이 있다. 다음은 Map 컬렉션에서 공통적으로 사용 가능한 Map 인터페이스 메소드이다.

  • 객체 추가
    • 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를 삭제하고 값을 리턴

위에서 K와 V는 저장되는 키와 객체의 타입을 Map 컬렉션을 생성할 때 결정하라는 뜻이다. Map< String, Integer > map =…; 처럼 말이다.
저장된 전체 객체를 대상으로 하나씩 얻고 싶은 경우에는 두 가지 방법이 있다. 첫 번째는 keySet()메소드로 모든 키를 Set 컬렉션으로 얻은 다음 반복자를 통해 키를 하나씩 받아서 get() 메소드로 값을 얻는 방법이다. 두 번째는 entrySet() 메소드로 모든 Map.Entry를 Set 컬렉션으로 얻은 다음 반복자를 통해 getKey()와 getValue()를 통해 얻는 방법이다. 그런데 Set 컬렉션으로 뽑은 순간 iterator을 사용하지 않고 향상된 for문으로 돌려도 된다.


HashMap

HashMap은 Map 인터페이스를 구현한 Map 컬렉션이다. HashMap의 키로 사용할 객체는 hashCode와 equals 메소드를 재정의해서 동등 객체가 될 조건을 정해야 한다. 객체가 달라도 동등 객체라면 같은 키로 간주하고 중복 저장이 되지 않도록 하기 위해서이다. 주로 key 타입으로 String을 많이 사용하는데 String 문자열 같은 경우에는 앞서 말했듯이 이미 hashcode와 equals가 재정이되어 있다. key와 value의 타입은 기본 타입(byte, short, int, float, double, boolean, char) 은 사용할 수 없고 반드시 클래스 및 인터페이스 타입만 사용 가능 하다.

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

        map.put("key1",10); // value값은 같아도 가능
        map.put("key2",10);
        map.put("key3",20);

        // 방법 1
        Set<String> keyset = map.keySet(); // 모든 key를 set 컬렉션으로 반환
        Iterator<String> iterator = keyset.iterator(); // 반복자 뽑기
        while(iterator.hasNext()){
            String key = iterator.next();
            System.out.println("key값 : " + key+ ", value값 : "+ map.get(key));
        }

        System.out.println();

        // 향상된 for문
        for (String key :keyset){
            System.out.println("key값 : " + key+ ", value값 : "+ map.get(key));
        }

        System.out.println();

        // 방법 2
        // Map.Entry<String,Integer> 형태로 Set 컬렉션 받기
        Set<Map.Entry<String,Integer>> set = map.entrySet();
        // set이 <Map.Entry<String,Integer>> 형식으로 저장되어 있으니
        // 반복자도 <Map.Entry<String,Integer>> 형식의 변수에 받아야함
        Iterator<Map.Entry<String,Integer>> iterator1 = set.iterator(); // set 반복자 받기
        while(iterator1.hasNext()){
            // Map.Entry<String,Integer> 형식의 반복자가 들어있으니
            // Map.Entry<String,Integer 형식의 변수에 받는다.
            Map.Entry<String,Integer> entry = iterator1.next();
            String key = entry.getKey();
            Integer value = entry.getValue();
            System.out.println("key값 : " + key+ ", value값 : "+ value);
        }


        // set에 두 개가 들어있어서 향상된 for문 사용못함
    }
}


key값을 사용자 정의 객체인 Student로 하고 싶다면 hashcode와 equals를 재정의 한다.

// 클래스
package selftest;

public class Student {
    String name;
    int id ; // 학번
    
    public Student(String name, int id){
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Student){
            Student student = (Student) obj;
            return student.id == this.id && student.name.equals(this.name);
        } else{
            return false;
        }        
    }

    @Override
    public int hashCode() {
        // String의 hashcode를 이용(문자열이 같으면 해쉬코드가같으니)
        return id + name.hashCode();
    }
}
// 메인
public class HashMapExam {
    public static void main(String[] args) {
        Map<Student,Integer> map = new HashMap<>();
        // 서로 다른 객체지만 재정의로 인해 하나만 들어간다.
        map.put(new Student("장광남",11),95); 
        map.put(new Student("장광남",11),96);

        System.out.println(map.size()); // 1
    }
}


Hashtable

HashMap과 동일한 내부 구조를 가지며 마찬가지로 키로 사용할 객체에 대해 hashCode와 equals를 재정의하여 동등 객체가 될 조건을 재정의해야한다. HashMap과 차이점은 동기화된 메소드로 구성되어 있어 멀티 스레드가 동시에 Hashtable의 메소드를 실행할 수 없다. Vector와 ArrayList의 관계와 비슷하다. 결론적으로 멀티 스레드 환경에서는 안전하게 객체를 추가, 삭제할 수 있으므로 Hashtable은 스레드에 안전하다. 사용법은 HashMap과 똑같다.


5. Iterator


vector, ArrayList, LinkedList, Set과 같이 요소가 순서대로 저장된 컬렉션에서 요소를 순차적으로 검색할 때는 java.util 패키지의 Iterator< E > 인터페이스를 사용한다. 다음과 같은 메서드를 가지고 있다.

  • boolean hasNext() : 방문할 요소가 남아 있으면 true 리턴
  • E next() : 다음 요소 리턴
  • void remove() : 마지막으로 리턴된 요소 제거
Vector<Integer> v = new Vector<>();
Iterator<Integer> it = v.iterator(); // 벡터의 타입에 맞게 iterator 타입 설정

while (it.hasNext()){
    int n = it.next(); 
    ...
}


6. Collections 활용 메서드


  • Collections 클래스는 static 타입으로 여러 메서드를 지원한다.
    • sort() : 정렬
    • reverse() : 요소를 반대 순으로 정렬
    • max(), min() : 최대 최소값
    • binarySearch() : 이진 검색, 반환값이 양수이면 찾고자 하는 원소의 인덱스값, 음수이면 탐색 실패, 반환되는 음수는 삽입되어야 하는 위치 -1 을 반환. 즉, 반환된 음수를 양수로 바꾸고 +1 해주면 삽입되어야 할 위치
LinkedList<String> myList = new LinkedList<>();

Collections.sort(myList);
Collections.reverse(myList);
int idx = Collections.binarySearch(myList,"아바타")


7. 제네릭 만들기


지금까지 JDK에 제네릭으로 구현된 컬렉션을 사용하는 방법을 알아봤고, 직접 만드는 것도 어렵지 않다.

public class GStack<T>{
    int tos;
    Object [] stck;
    public GStack(){
        tos =0;
         // 제네릭 매개변수로 객체를 생성하거나 배열 생성 불가능
         // Object 배열을 생성하여 실제 타입의 객체를 요소로 삽입해야함
        stck = new Object[10];
    }

    public T pop(){
        return (T)stck[tos]; // 타입 매개변수로 캐스팅후 반환
    }
    
    // 주의점
    T a = new T() ; // 제네릭 타입의 객체 생성 불가능 , 오류 발생
    // 제너릭은 배열로 선언할 수 없음
    Gstack<Integer>[] gs = new GStack<>[10]; // 컴파일 오류
    // 재너릭 타입의 배열 선언은 허용
    public void myArray(T[] a){...} // 정상
}

// 주의점

위 처럼 하면 ArrayList 같이 하나의 컬렉션을 만든 것이다.
주의점

  • 제네릭 클래스 내에서 제네릭 타입을 가진 객체 생성은 불가능
  • 제네릭 클래스는 배열로 선언할 수 없음 -> 필요하다면 Object 배열을 생성하여 실제 타입 T의 객체를 요소로 삽입하고 리턴 시에는 반드시 T로 캐스팅 한 뒤에 반환


제너릭 메서드

class GenericMethod{
    static <T> void toStack(T[] a, GStack<T> gs){
        for (int i =0;i<a.length;i++){
            gs.push(a[i]);
        }
    }
}

// String, object 타입이 들어왔다면 컴파일러는 슈퍼클래스인 object 타입으로 유추
GenericMethod.toStack(sArray,objectStack); 

타입 매개변수는 메서드의 리턴 타입 앞에 선언된다. toStack()에서 < T >가 타입 매개변수의 선언이다. 메서드를 호출할 때는 컴파일러가 메서드의 인자를 통해 타입을 유추하기 때문에 인자로 타입명을 명시하지 않아도 된다.


8. 정리하기


  • 컬렉션 프레임워크 : 자료구조들을 사용해서 객체들을 효율적으로 추가, 삭제, 검색할 수 있도록 인터페이스와 구현 클래스들을 java.util 패키지에서 제공하는데 이들을 총칭한 말
  • List 컬렉션 : 배열과 비슷하게 객체를 인덱스로 관리한다. 차이점은 저장용량 자동증가, 자동 인덱스 부여, 다양한 메소드 제공이다. List 컬렉션은 동일한 객체를 중복 저장할 수 있고(객체 번지 저장) null도 저장 가능하다.
  • Set 컬렉션 : 저장순서가 유지되지 않고, 객체 중복저장이 불가능하며, 하나의 null만 저장 가능하다.
  • Map 컬렉션 : key과 value로 구성된 Map.Entry 객체를 저장하는 구조를 가진다. Entry는 Map 인터페이스 내부에 선언된 중첩인터페이스이다. key와 value는 모두 객체이고, key값은 중복이 불가능하며, value는 중복 가능하다. 중복된 key값이 들어오면 새로 온 것으로 대체된다.



본 포스팅은 ‘혼자 공부하는 자바’를 읽고 공부한 내용을 바탕으로 작성하였습니다.


© 2021. By Backtony