지난 글: [프로그래밍 언어/JAVA] - JAVA 입문 - 컬렉션 프레임워크
List 인터페이스에는 객체를 순서에 따라 저장하고 유지하는 데 필요한 메서드가 선언되어 있다. 우리가 알고 있는 순차 자료 구조의 대표적인 예는 배열이다. 자바에서 배열을 구현한 대표 클래스로는 ArrayList, Vector가 있고 배열과 구현 방식은 다르지만 순차 자료 구조를 구현한 LinkedList가 있다. 그럼 객체 배열로 가장 많이 사용하고 공부하며 자주 활용한 ArrayList에 대해 살펴보자.
ArrayList 클래스
ArrayList는 그동안 공부하며 종종 사용했다. 객체 배열을 구현한 클래스이며 컬렉션 인터페이스와 그 하위 List 인터페이스를 구현하였다. 객체 순서를 기반으로 순차적으로 자료를 관리하는 프로그램을 구현할 때 사용한다. collection 패키지 하위에 arraylist 패키지를 만들고 MemberArrayList.java 클래스를 생성하면 지난 글에서 만든 공통으로 사용할 Member 클래스는 collection 패키지 하위에 있고, MemberArrayList 클래스를 활용할 관리 클래스는 collection.arraylist 패키지에 만든다.
ArrayList를 활용해 회원 관리 프로그램 구현하기
ArrayList를 활용한 MemberArrayList 클래스에서는 메서드를 3개 제공한다. 회원을 추가하는 addMember( ) 메서드, 회원을 삭제하는 removeMember( ) 메서드, 그리고 전체 회원을 출력하는 showAllMember( ) 메서드이다. 각 메서드의 코드를 보면 Collection 인터페이스에서 선언하고 ArrayList에서 구현한 add( ), get( ) 등의 메서드를 사용한 것을 볼 수 있다. ArrayList를 사용하여 회원을 추가하고, 삭제하고, 회원 정보를 출력해보자.
7행에서 ArrayList를 선언하고 MemberArrayList( ) 생성자에서 ArrayList를 생성한다. 13~15행 addMember( ) 메서드에서는 매개변수로 전달된 회원을 ArrayList의 맨 뒤에 추가한다. 17~18행 removeMember( ) 메서드는 매개변수로 전달받은 아이디(memberID) 회원을 ArrayList에서 찾아 제거한다. 30~35행 showAllMember( ) 메서드는 모든 회원을 출력한다. 향상된 for문을 사용하여 배열에 있는 회원을 하나씩 가져와 출력하면 Member 클래스에 재정의한 toString( )이 호출되며 회원 정보가 출력된다.
MemberArrayList 테스트 클래스 구현하기
이제 이렇게 만든 클래스에 직접 회원을 추가하고 삭제하며 프로그램이 잘 구현되었는지 확인해 보자. collection.arraylist 패키지 하위에 MemberArrayListTest 클래스를 만든다.
ArrayList와 Vector 클래스
Vector는 자바 2 이전부터 제공했으며 ArrayList처럼 배열을 구현한 클래스다. ArrayList와 Vector의 가장 큰 차이는 동기화 지원 여부이다. 동기화(synchronization)란 두 개 이상의 스레드가 동시에 Vector를 사용할 때 오류가 나지 않도록 실행 순서를 보장하는 것이다.
스레드와 멀티스레드 프로그래밍
스레드란 간단히 말하면 작업 단위이다. 프로그램이 메모리에서 수행되려면 스레드 작업이 생성되어야 한다. 이때 하나의 스레드만 수행되면 단일 스레드(single thread), 두 개 이상의 스레드가 동시에 실행되는 경우를 멀티 스레드(multi-thread)라고 한다. 두 개 이상의 스레드가 동시에 실행되면 같은 메모리 공안(리소스)에 접근하기 때문에 변수 값이나 메모리 상태에 오류가 생길 수 있다. 이때 메모리에 동시에 접근하지 못하도록 순서를 맞추는 것이 동기화다. 두 작업이 동시에 실행되는 멀티스레드 환경이 아닌 경우에는 ArrayList를 사용하도록 권장한다. 이유는 동기화를 구현하기 위해서는 동시에 작업이 이루어지는 자원에 대해 잠금(lock)을 수행하기 때문이다. 즉 메서드를 호출할 때 배열 객체에 잠금을 하고, 메서드 수향이 끝나면 잠금을 해제한다. 이렇게 Vector의 모든 메서드는 호출될 때마다 잠금과 해제가 일어나므로 ArrayList보다 수행 속도가 느리다. ArrayList를 사용해서 구현했는데 나증에 프로그램에서 동기화가 필요하다면 Vector로 바꾸지 않고 아래처럼 ArrayList 생성 코드를 쓰면 된다.
Collections.synchronizedList(new ArrayList<String>());
LinkedList 클래스
배열은 처음 배열을 생성할 때 정적 크기로 선언하고, 물리적 순서와 논리적 순서가 동일하다. 배열은 중간에 자료를 삽입하거나 삭제할 때 나머지 자료를 이동시켜 빈 공간을 만들지 않고 연속된 자료 구조를 구현한다. 또 처음 선언한 배열 크기 이상으로 요소가 추가되는 경우에 크기가 더 큰 배열을 새로 생성하여 각 요소를 복사해야하는 번거로움이 있다. 이런 점을 개선한 자료 구조를 링크드 리스트(linked list)라고 한다. 자바의 LinkedList클래스가 이를 구현하고 있다. 그럼 링크드 리스트 자료 구조에 대해 간단히 살펴보고 자바에서 LinkedList 클래스를 활용한 예를 보자.
링크드 리스트 구조
링크드 리스트의 각 요소는 다음 요소를 가리키는 주소 값을 가진다. 따라서 물리적인 메모리는 떨어져 있어도 논리적으로는 앞뒤 순서가 있다. 같은 List 인터페이스를 구현한 ArrayList에 비해 중간에 자료를 넣고 제거하는 데 시간이 적게 걸리는 장점이 있고, 크기를 동적으로 증가시킬 수 있다. 링크드 리스트의 각 요소는 요소의 자료와 다음 요소의 주소를 저장하는 부분으로 구현된다. 각 요소는 물리적으로 다른 메모리에 생성되어 있지만, 다음 요소를 가리키는 포인터가 있으므로 논리적으로 순서가 있다.
링크드 리스트에 요소 추가하기
링크드 리스트의 중간 위치에 요소를 추가해보자. 배열이라면 요소를 저장할 위치에 있는 기존 요소를 뒤로 밀고 공간을 비워서 그 자리에 놓는다. 하지만 링크드 리스트는 서로 가리키고 있는 주고 값만 변경해주면 된다. 자료 이동이 발생하는 배열에 비해 훨씬 효율적이다. A-C-D라는 순서의 요소가 있다. 이때 B라는 요소를 A와 C 요소 사이에 넣고 싶다면 A의 ㅏㄷ음 요소를 B로, B의 다음 요소를 C로 변경해주면 A-B-C-D의 순서가 된다.
링크드 리스트의 요소 제거하기
제거해야 하는 요소가 있는 경우에도 각 요소가 가리키는 주소 값만 병경하면 된다. A-B-C-D 순서의 요소가 있을 때, B를 제거한다고 하면 A의 다음 요소를 C로 변경하기만 하면 된다. 이때 제거된 B의 메모리는 나중에 자바의 가비지 컬렉터에 의해 수거된다.
배열과 링크드 리스트의 다른 점
배열은 생성할 때 용량을 지정하고, 용량보다 많은 요소가 추가된 경우 용량을 늘려가며 수행한다. 그러나 링크드 리스트는 요소를 추가할 때마다 동적으로 요소의 메모리를 생성하기에 배열처럼 용량을 늘리고 요소 값을 복사하는 번거로움이 없다. 또 링크드 리스트는 자료를 중간에 추가하거나 삭제할 때 자료의 이동이 배열보다 적다. 이런 면에서 링크드 리스트가 배열에 비해 더 편리한 자료 구조라 생각할 수 있다. 하지만 배열이 링크드 리스트보다 효율적인 경우도 있다. 어떤 요소의 위치(i 번째)를 찾을 때를 생각해보자. 배열은 물리적으로 연결된 자료 구조이므로 i 번째 요소 메모리 위치를 바로 계산할 수 있어 접근이 빠르다. 그리고 배열이 링크드 리스트보다 구현하기도 쉽다. 따라서 사용하는 자료의 변동(삽입, 삭제)이 많은 경우에는 링크드 리스트를, 자료 변동이 거의 없는 경우에는 배열을 사용하는 것이 효율적이다.
LinkedList 클래스 사용하기
이제 자바 LinkedList 클래스를 살펴보자. LinkedList는 ArrayList보다 다양한 메서드를 제공한다. 이 글에서는 LinkedList 클래스에서만 제공하는 메서드를 사용해 보자.
LinkedList 클래스에는 링크드 리스트의 맨 앞 또는 맨 뒤에 요소를 추가/삭제 하는 addFirst( ), addLsat( ), removeFirst( ), removeLast( ) 등의 메서드가 있다. ArrayList 클래스보다 다양하다. 이들 메서드는 이후 이야기할 스택(stack)이나 큐(queue)에서 다양하게 활용할 수 있다고 한다.
ArrayList로 스택과 큐 구현하기
그럼 프로그램을 개발할 때 가장 많이 사용하는 자료 구조인 스택과 큐에 대해 살펴보자. 먼저 스택은 상자를 쌓듯 자료를 관리하는 방식이다. 상자가 쌓인 상태에서 중간의 상자를 꺼내려면 맨 나중에 올린 상자를 먼저 꺼내야 한다. 이처럼 스택은 나중에 추가된 데이터를 먼저 꺼내는(Last In First Out; LIFO) 방식이다. 큐는 일상 생활에서 가장 많이 사용하는 방식의 자료 구조로 '선착순'이다. 줄을 선 대기열처럼 먼저 추가된 데이터부터 꺼내서 사용하는 (First In First Out; FIFO) 방식이다. Stack 클래스는 자바 1부터 제공했다. Queue는 인터페이스로 정의되어 있고 PriorityQueue 등이 구현되어 있다. 하지만 ArrayList나 LinkedList 클래스를 활용하여 구현하는 경우도 종종 있다. 자신이 직접 구현하면 사용하기 편리할 때도 있어서이다. 그럼 ArrayList를 활용하여 스택과 큐를 구현해 보자.
ArrayList로 스택 구현하기
스택은 가장 최근에 추가된 자료부터 반환해 주므로 가장 최근에 검색한 단어를 찾는다든가 장기, 체스 같은 게임에서 수를 무를 때도 응용할 수 있다. 스택에 자료를 추가하는 것을 push( )라고 하고, 자료를 꺼내는 것을 pop( ) 이라고 한다. 그리고 스택에 가장 최근에 추가된 자료의 위치를 top이라고 한다.
아래와 같이 MyStack 클래스를 만들고 ArrayList를 생성하여 push( )와 pop( )을 간단하게 구현했다.
8~10행 push( )에서는 add( ) 메서드를 사용하여 ArrayList 맨 뒤에 요소를 추가한다. 그리고 pop( ) 메서드의 18행에서 arrayStack.remove(len - 1)을 사용해 가장 최근에 추가된 마지막 항목(요소)을 ArrayList에서 제거하고 반환해 준다. 테스트 프로그램을 통해 추가된 순서와 반대로 최근 항복부터 pop( )이 수행됨을 알 수 있다.
ArrayList로 큐 구현하기
이번에는 ArrayList로 큐를 구현해 보자.
8~10행 enQueue( )에서는 add( ) 메서드를 사용하여 ArrayList 맨 뒤에 요소를 추가한다. 그리고 큐에서 자료를 꺼내는 12~20행의 deQueue( ) 에서는 ArrayList의 맨 앞에 있는 요소부터 제거하고 반환한다. 출력 결과를 보면 추가된 순서대로 요소가 반환되는 것을 알 수 있다.
Collection 요소를 순회하는 Iterator
MemberArrayList.java의 removeMember( ) 메서드를 보면 for문과 get(i) 메서드를 사용하여 회원을 순차적으로 하나씩 꺼내면서 매개변수와 같은 아이디를 찾는다. 그런데 순서가 없는 Set 인터페이스를 구현한 경우에는 get(i) 메서드를 사용할 수 없다. 이때 Iterator를 사용한다. Iterator는 Collection 인터페이스를 구현한 객체에서 미리 정의되어 있는 iterator( ) 메서드를 호출하면 Iterator 클래스가 반환되므로 아래처럼 Iterator형 변수에 대입해 사용한다.
Iterator ir = memberArrayList.iterator( );
Iterator를 사용하여 요소를 순회할 때 사용하는 메서드
Iterator를 사용하여 모든 요소를 순회할 때 다음 두 가지 메서드를 사용한다.
이 두 메서드로 MemberArrayList 클래스의 removeMember( ) 메서드를 수정해보자.
public boolean removeMember(int memberID) {
Iterator<Member> ir = arrayList.iterator();
while(ir.hashNext()); {
Member member = ir.next( );
int tempID = member.getMemberID( );
if(tempID == memberID) {
arrayList.remove(member);
return true;
}
}
System.out.println(memberID + " 가 존재하지 않습니다.");
return false;
}
arrayList.iterator( ) 메서드를 호출하여 Iterator를 가져온다. Iterato<Member>와 같이 제네릭 자료형으로 Iterator가 순회할 요소의 자료형을 지정한다. Iterator는 각 요소를 순회하기에 hashNext( )의 결과가 true이면 다음 요소를 가져오는 next( ) 메서드를 호출한다. 나머지 부분은 for문과 get(i) 메서드를 사용하는 경우와 같다. 이렇게 순서가 없는 클래스도 Iterator를 사용하면 요소를 순회할 수 있다.
참고 서적: 자바 프로그래밍 입문 - 박은종
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
JAVA 입문 - Map 인터페이스 (0) | 2022.06.07 |
---|---|
JAVA 입문 - Set 인터페이스 (0) | 2022.06.06 |
JAVA 입문 - 컬렉션 프레임워크 (0) | 2022.06.04 |
JAVA 입문 - 제네릭 (0) | 2022.06.03 |
JAVA 입문 - Class 클래스 (0) | 2022.06.02 |