'컬렉션 프레임워크'가 뭔지 모른다면, 아래 포스트부터 확인할 것
목차
<List 인터페이스>
'List 인터페이스'에는 객체를 순서에 따라 저장하고 유지하는 데 필요한 메서드가 선언되어 있다.
우리가 알고 있는 순차 자료 구조의 대표적인 예는 '배열'이다. 배열은 앞에서 다뤘었는데 자바에서 배열을 구현한 대표적인 클래스는 ArrayList와 Vector가 있고, 구현 방식은 다르지만 순차 자료 구조를 구현한 LinkedList가 있다.
그럼 객체 배열로 가장 많이 사용하고, 자주 활용한 ArrayList부터 보자.
<ArrayList 클래스>
'ArrayList'는 객체 배열을 구현한 클래스이며 컬렉션 인터페이스와 그 하위 List 인터페이스를 구현했다.
객체 순서를 기반으로 순차적으로 자료를 관리하는 프로그램을 구현할 때 사용한다.
collection 패키지 하위에 arraylist패키지를 만들고 MemberArrayList.java 클래스를 생성하면 패키지의 계층 구조가 아래처럼 보인다.
공통으로 사용할 Member클래스는 collection패키지 하위에 있고, MemberArrayList 클래스를 활용할 관리 클래스는 collection.arraylist 패키지에 만든다.
ArrayList를 활용해 회원 관리 프로그램 구현하기
ArrayList를 활용한 MemeberArrayList 클래스에서는 메서드를 3개 제공한다.
- addMember() 메서드 : 회원을 추가하는 메서드
- removeMember() 메서드 : 회원을 삭제하는 메서드
- showAllMember() 메서드 : 전체 회원을 출력하는 메서드
각 메서드의 코드를 보면 Collection 인터페이스에서 선언하고 ArrayList에서 구현한 add(), get() 등의 메서드를 사용한 것을 알 수 있다.
ArrayList를 사용하여 회원을 추가하고, 삭제하고, 회원 정보를 출력해보자.
package collection.arraylist;
import java.util.ArrayList;
import collection.Member;
public class MemberArrayList {
private ArrayList<Member> arrayList; //ArrayList 선언
public MemberArrayList() {
arrayList = new ArrayList<Member>();
} //Member형으로 선언한 ArrayList 생성
public void addMember(Member member) {
arrayList.add(member);
} //ArrayList에 회원을 추가하는 메서드
public boolean removeMember(int memberId) {
for(int i = 0; i < arrayList.size(); i++) {
Member member = arrayList.get(i); //get() 메서드로 회원을 순차적으로 가져옴
int temId = member.getMemberId();
if(temId == memberId) { //회원 아이디가 매개변수와 일치하면
arrayList.remove(i); //해당 회원을 삭제
return true;
}
}
System.out.println(memberId + "가 존재하지 않습니다.");
return false; //반복문이 끝날 때 까지 해당 아이디를 찾지 못한 경우
}
public void showAllMember() {
for(Member member : arrayList) {
System.out.println(member);
}
System.out.println();
} //전체 회원을 출력하는 메서드
}
- ArrayList를 사용하려면 import java.util.ArrayList를 선언해 주어야 한다. ArrayList를 선언하고 MemberArrayList() 생성자에서 ArrayList를 생성한다.
- addMember() 메서드에서는 매개변수로 전달된 회원을 ArrayList의 맨 뒤에 추가한다.
- removeMember() 메서드에서는 매개변수로 전달받은 아이디(memberId)를 ArrayList에서 찾아 제거한다.
아마 removeMember() 메서드의 코드가 어렵게 느껴질 수도 있다.
'get(i) 메서드'는 순차 관리를 하는 배열에서 사용하는 메서드고 이 예제에서는 매개변수 i에 해당하는 객체를 ArrayList에서 반환해 준다.
예를 들어 get(0)은 ArrayList에 저장된 객체 중 첫 번째 객체를 반환한다.
회원 아이디가 매개변수로 전달받은 아이디와 같으면 해당 회원을 배열에서 삭제한다. 성공적으로 삭제한 경우 true를, 해당 아이디를 못 찾은 경우 출력문이 나오며 false를 반환. 따라서 메서드의 반환형이 boolean이다.
- showAllMember() 메서드에서는 모든 회원을 출력한다.
향상된 for문을 사용하여 배열에 있는 회원을 하나씩 가져와 출력하면 Member 클래스에 재정의한 toString()이 호출되면서 회원 정보가 출력된다.
MemberArrayList 테스트 클래스 구현하기
이제 이렇게 만든 클래스에 직접 회원을 추가하고 삭제해 구현이 잘 되었는지 확인해보자.
다음처럼 collection.arraylist패키지 하위에 MamberArrayListTest클래스를 만들자.
package collection.arraylist;
import collection.Member;
public class MemberArrayListTest {
public static void main(String[] args) {
MemberArrayList memberArrayList = new MemberArrayList();
Member memberLee = new Member(1001, "이순신");
Member memberKim = new Member(1002, "김유신");
Member memberPark = new Member(1003, "박혁거세");
Member memberShin = new Member(1004, "신사임당");
//새로운 회원 인스턴스 생성
memberArrayList.addMember(memberLee);
memberArrayList.addMember(memberKim);
memberArrayList.addMember(memberPark);
memberArrayList.addMember(memberShin);
//ArrayList에 회원 추가
memberArrayList.showAllMember(); //전체 회원 출력
memberArrayList.removeMember(memberPark.getMemberId());
//박혁거세의 아이디를 호출하여 해당 회원 삭제
memberArrayList.showAllMember(); //전체 회원 출력
}
}
ArrayList에 추가한 회원 전체가 나타나고,
memberPark의 아이디를 매개변수로 removeMember()메서드를 호출하여 배열에서 해당 회원이 삭제됨을 알 수 있다.
연습하기 :
[ArrayList의 특정 위치에 회원 추가하기!]
회원을 추가할 때 맨 뒤가 아닌 특정 위치에 추가하는 메서드를 만들고, MemberArrayListTest 클래스에 코드를 추가하여 테스트 해보자.
MemberArrayList클래스
package collection.arraylist;
import java.util.ArrayList;
import collection.Member;
public class MemberArrayList {
private ArrayList<Member> arrayList;
public MemberArrayList() {
arrayList = new ArrayList<Member>();
}
...
public void insertMember(Member member, int index) {
if(index < 0 || index > arrayList.size()+1) {
System.out.println("지정한 index에 추가할 수 없습니다.");
System.out.println();
return ;
} //매개변수 index가 0보다 작거나, 배열길이+1보다 크면 출력문 실행
arrayList.add(index-1, member);
} // ArrayList의 add(int index, E) 메서드 활용
}
MemberArrayListTest 클래스
package collection.arraylist;
import collection.Member;
public class MemberArrayListTest {
public static void main(String[] args) {
MemberArrayList memberArrayList = new MemberArrayList();
Member memberLee = new Member(1001, "이순신");
Member memberKim = new Member(1002, "김유신");
Member memberPark = new Member(1003, "박혁거세");
Member memberShin = new Member(1004, "신사임당");
//새로운 회원 인스턴스 생성
memberArrayList.addMember(memberLee);
memberArrayList.addMember(memberKim);
memberArrayList.addMember(memberPark);
memberArrayList.addMember(memberShin);
//ArrayList에 회원 추가
...
Member memberKing = new Member(1005, "킹콩");
memberArrayList.insertMember(memberKing, 6);
memberArrayList.showAllMember(); //전체 회원 출력
}
}
<ArrayList와 Vecor 클래스>
'Vector'는 자바2 이전부터 제공되었으며 ArrayList처럼 배열을 구현한 클래스이다.
ArrayList와 Vector의 가장 큰 차이는 '동기화의 지원 여부'다.
'동기화(synchronization)'란 두 개 이상의 스레드가 동시에 Vector를 사용할 때 오류가 나지 않도록 실행 순서를 보장하는 것이다.
스레드와 멀티스레드 프로그래밍
'스레드'는 작업 단위라고 할 수 있다. 프로그램이 메모리에서 수행되려면 스레드 작업이 생성되어야 하는데, 이때 하나의 스레드만 수행되면 단일 스레드(single thread)라고 부르며, 두 개 이상의 스레드가 동시에 실행되는 경우 멀티스레드(multi-thread)라고 한다.
두 개 이상의 스레드가 동시에 실행되면 같은 메모리 공간(리소스)에 접근하기 때문에 변수 값이나 메모리 상태에 오류가 생길 수 있는데, 이때 메모리에 동시에 접근하지 못하도록 순서를 맞추는 것이 '동기화'다.
두 작업이 동시에 실행되는 멀티스레드 환경이 아닌 경우 ArrayList를 사용하도록 권장한다.
그 이유는 동기화를 구현하기 위해서는 동시에 작업이 이루어지는 자원에 대해 잠금(lock)을 수행하기 때문.
즉 메서드를 호출할 때 배열 객체에 잠금을 하고, 메서드 수행이 끝나면 잠금을 해제한다. 이렇게 Vector의 모든 메서는 호출될 때마다 잠금과 해제가 일어나 ArrayList보다 수행 속도가 느린 편이다.
ArrayList를 사용해서 구현했는데, 나중에 동기화가 필요하다면 Vector로 바꾸지 않고 다음처럼 생성 코드를 쓰면 된다.
Collection.synchronizedList(new ArrayList<String>());
<LinkedList 클래스>
배열은 처음 배열을 생성할 때 정적 크기로 선언하고, 물리적 순서와 논리적 순서가 동일하다.
배열은 중간에 자료를 삽입하거나 삭제할 때 나머지 자료를 이동시켜 빈 공간을 만들지 않고 연속된 자료 구조를 구현한다.
또한 처음 선언한 배열 크기 이상으로 요소가 추가되는 경우에는 크기가 더 큰 배열을 새로 생성하여 각 요소를 복사해야 하는 번거로움이 있다.
이러한 배열의 특징을 개선한 자료 구조를 '링크드 리스트(linked list)'라고 한다.
자바의 LinkedList 클래스가 이를 구현한다.
링크드 리스트 구조
'링크드 리스트'의 각 요소는 다음 요소를 가리키는 주소 값을 가진다.
따라서 물리적인 메모리는 떨어져 있어도, 논리적으로는 앞뒤 순서가 있다.
같은 List 인터페이스를 구현한 ArrayList에 비해 중간에 자료를 넣고 제거하는 데 시간이 적게 걸리는 장점이 있고, 크기를 동적으로 증가시킬 수 있다.
링크드 리스트의 각 요소는 요소의 자료와 다음 요소의 주소를 저장하는 부분으로 구현된다.
링크드 리스트의 각 요소는 물리적으로 다른 메모리에 생성되어 있지만, 다음 요소를 가리키는 순서에 따라 A 다음은 B, 그 다음은 D가 된다.
D의 다음으로 가리키는 요소가 없기 때문에 널(null)값이나 0을 저장한다.
링크드 리스트에 요소 추가하기
링크드 리스트 3번째 위치에 'C' 요소를 추가해 보자.
배열이라면 D 요소를 뒤로 밀고 공간을 비워 그 자리에 C를 놓게 된다. 하지만 링크드 리스트는 서로 가리키고 있는 주소 값을 변경해 주면 된다. 자료 이동이 발생하는 배열에 비해 훨씬 효율적인 편.
B가 가리키던 다음 위치를 C로 변경하고 C는 D를 가리키면 끝.
그러면 논리적으로 A-B-C-D 순서가 된다.
링크드 리스트의 요소 제거하기
제거해야 하는 요소가 있을 경우에도 각 요소가 가리키는 주소 값을 변경하면 된다.
B를 제거한다고 할 때 A의 다음 요소를 C로 변경하기만 하면, A-C-D순서가 된다.
이때 제거된 B의 메모리는 나중에 자바의 가비지 컬렉터에 의해 수거가된다.
배열과 링크드 리스트의 다른 점
배열은 생성이될 떄 용량을 지정하고, 용량보다 더 많은 요소가 추가된 경우에 용량을 늘려가며 수행을 한다.
그러나 '링크드 리스트'는 요소를 추가할 때마다 동적으로 요소의 메모리를 생성하기 때문에 배열처럼 용량을 늘리고 요소 값을 복사하는 번거로움이 없다. 또한 링크드 리스트는 자료를 중간에 추가하거나 삭제할 때 자료의 이동이 배열보다 적다.
하지만 배열이 링크드 리스트보다 효율적인 경우도 있다.
어떤 요소의 위치(i번째)를 찾을 때를 생각해보면, 배열은 물리적으로 연결된 자료 구조이므로 i번째 요소 메모리 위치를 바로 계산할 수 있어 접근이 빠르다. 그리고 배열이 링크드 리스트보다 구현하기가 쉽다.
따라서 사용하는 자료의 변동(삽입, 삭제)이 많은 경우에는 링크드 리스트를,
자료 변동이 거의 없는 경우에는 배열을 사용하는 것이 효율적.
LinkedList 클래스 사용하기
LinkedList는 ArrayList보다 다양한 메서드를 제공한다.
여기서는 LinkedList 클래스에서만 제공하는 메서드를 사용한다.
package collection;
import java.util.LinkedList;
public class LinkedListTest {
public static void main(String[] args) {
LinkedList<String> myList = new LinkedList<String>();
myList.add("A");
myList.add("B");
myList.add("C");
//링크드 리스트에 요소 추가
System.out.println(myList); //리스트 전체 출력
myList.add(1,"D"); //링크드 리스트의 첫 번째 위치에 D추가
System.out.println(myList);
myList.addFirst("0"); //맨 앞에 0추가
System.out.println(myList);
System.out.println(myList.removeLast());
// 연결 리스트의 맨 뒤 요소 삭제 후, 해당요소를 출력.
System.out.println(myList);
}
}
LinkedList 클래스에는 링크드 리스트의 맨 앞 또는 맨 뒤에 요소를 추가·삭제하는 addFirst(), addLast(), removeFirst(), removeLast() 등의 메서드가 있다. ArrayList 클래스보다 훨씬 다양한 편.
이들 메서드는 스택(stack)이나 큐(queue)에서 다양하게 활용될 수 있다.
<ArrayList로 스택과 큐 구현>
그러면 프로그램을 개발할 때 가장 많이 사용하는 자료 구조인 스택(stack)과 큐(queue)에 대해 알아보자.
'스택(stack)'은 상자를 쌓듯 자료를 관리하는 방식.
상자가 쌓인 상태에서 하나의 상자를 꺼내려면, 맨 나중에 올린 상자를 먼저 꺼내야 한다. 이처럼 스택은 맨 나중에 추가된 데이터를 먼저 꺼내는 LIFO(Last In First Out)방식이다.
'큐(queue)'는 일상 생활에서 가장 많이 사용하는 방식의 자료 구조로 '선착순'을 생각하면 된다.
줄은 선 대기열처럼 먼저 추가된 데이터부터 꺼내어 사용하는 FIFO(First In First Out)방식이다.
Stack 클래스는 자바1부터 제공했다. Queue는 인터페이스로 정의되어 있고 Priority Queue등이 구현되어 있다. 하지만 자신이 직접 구현하면 사용하기 편리할 때가 있어 ArrayList나 LinkedList 클래스를 활용하여 구현하는 경우도 종종 있다.
ArrayList로 스택 구현하기
스택(stack)은 가장 최근에 추가된 자료부터 반환해준다. 그러므로 가장 최근에 검색한 단어를 찾는다던지, Undo를 구현할 때 응용할 수 있다.
스택에 자료를 추가하는 것을 push()라고 하고, 자료를 꺼내는 것을 pop()이라고 한다. 그리고 스택에 가장 최근에 추가한 자료의 위치를 top으로 부른다.
다음과 같이 MyStack 클래스를 만들고 ArrayList를 생성하여 push(), pop()을 간단하게 구현하자.
package collection.arraylist;
import java.util.ArrayList;
class MyStack{
private ArrayList<String> arrayStack = new ArrayList<String>();
public void push(String data) {
arrayStack.add(data);
} //스택의 맨 뒤에 요소를 추가
public String pop() {
int len = arrayStack.size(); //ArrayList에 저장된 유효한 자료의 개수
if (len == 0) {
System.out.println("스택이 비었습니다.");
return null;
}
return(arrayStack.remove(len-1)); //맨 뒤에 있는 자료를 반환하고 제거.
} //스택의 맨 뒤에서 요소를 꺼내는 메서드
}
public class StackTest {
public static void main(String[] args) {
MyStack stack = new MyStack();
stack.push("A");
stack.push("B");
stack.push("C");
System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack.pop()); //스택이 비었을 경우
}
}
- push()에서는 add()메서드를 사용해 ArrayList 맨 뒤에 요소를 추가한다.
- pop() 메서드에서 arrayStack.remove(len-1)을 사용해 가장 최근에 추가된 마지막 항목(요소)을 ArrayList에서 제거 후 반환한다.
ArrayList로 큐 구현하기
이번에는 ArrayList로 큐(queue를 구현해보자
package collection.arraylist;
import java.util.ArrayList;
class MyQueue{
private ArrayList<String> arrayQueue = new ArrayList<String>();
public void enQueue(String data) {
arrayQueue.add(data);
} //큐의 맨 뒤에 추가
public String deQueue() {
int len = arrayQueue.size();
if (len == 0) {
System.out.println("큐가 비었습니다.");
return null;
}
return(arrayQueue.remove(0)); //맨 앞의 자료를 반환하고 배열에서 제거
} //큐의 맨 앞에서 꺼내는 메서드
}
public class QueueTest {
public static void main(String[] args) {
MyQueue queue = new MyQueue();
queue.enQueue("A");
queue.enQueue("B");
queue.enQueue("C");
System.out.println(queue.deQueue());
System.out.println(queue.deQueue());
System.out.println(queue.deQueue());
System.out.println(queue.deQueue());
}
}
- enQueue() 에서는 add()를 사용하여 ArrayList 맨 뒤에 요소를 추가한다
- 그리고 자료를 꺼내는 deQueue()메서드는 ArrayList의 맨 앞에있는 요소부터 제거하고 반환.
출력 결과를 보면 추가된 순서대로 요소가 반환되고 제거되는 모습을 확인할 수 있다.
<Collection 요소를 순회하는 Iterator>
MemberArrayList.java의 removeMember() 메서드를 보면 for문과 get(i) 메서드를 사용하여 회원을 순차적으로 하나씩 꺼내면서 매개변수와 같은 아이디를 찾는다.
그런데 순서가 없는 Set인터페이스를 구현한 경우 get(i) 메서드를 사용할 수 없다.
이럴 때 'Iterator'를 사용한다.
'Iterator'는 Collection 인터페이스를 구현한 객체에서 미리 정의되어 있는 iterator()메서드를 호출하여 참조한다.
예를 들어 Collection을 구현한 ArrayList에 iterator() 메서드를 호출하면 Iterator 클래스가 반환되므로 다음처럼 Iterator형 변수에 대입하여 사용한다.
Iterator ir = memberArrayList.iterator();
Iterator를 사용할 때 필요한 메서드
Iterator를 사용하여 모든 요소를 순회할 때 다음 두 가지 메서드를 쓴다.
메서드 | 설명 |
boolean hasNext() | 이후에 요소가 더 있는지 체크하는 메서드, 요소가 있다면 true를 반환 |
E next() | 다음에 있는 요소를 반환. |
그럼 이 두 메서드로 MemberArrayList 클래스의 removeMember() 메서드를 수정하자.
public boolean removeMember(int memberId) {
Iterator<Member> ir = arrayList.iterator(); //Iterator 반환
while(ir.hasNext()) { //요소가 있는 동안
Member member = ir.next(); //다음 회원을 반환
int tempId = member.getMemberId();
if (tempId == memberId) { //회원 아이디가 매개변수와 일치하면
arrayList.remove(member);//해당 회원 삭제
return true; //true반환
}
}
//순회가 끝날 때까지 삭제하려는 값을 찾지 못한 경우
System.out.println(memberId + "가 존재하지 않습니다.");
return false;
- arrayList.iterator() 메서드를 호출해 Iterator를 가져온다. Iterator<Member>와 같이 제네릭 자료형으로 Iterator가 순회할 요소의 자료형을 지정한다.
- Iterator는 각 요소를 순회하기 때문에 hasNext()의 결과가 true면 다음 요소를 가져오는 next() 메서드를 호출한다.
- 비교부분은 for문과 get(i)메서드를 사용하는 경우와 같다.
이렇게 순서가 없는 클래스도 Iterator를 사용하면 요소를 순회할 수 있다.