ArrayList는 배열을 기반으로 한 컬렉션의 하나이며, 데이터를 추가, 삭제시 내부에서 동적으로 배열의 길이를 조절해준다.
Object[]
배열을 이용해 요소를 저장한다.정적할당(static allocation)
이라고 한다.Number[] r = new Number[5]; // 정적 할당(static allocation)
r[0] = 10;
r[1] = 20;
r[2] = 30;
r[3] = 40;
r[4] = 50;
r[3] = null; // 배열은 삭제 메서드가 없어서 null을 이용해 객체 요소를 삭제
System.out.println(Arrays.toString(r)); // [10, 20, 30, null, 50]
동적 할당(dynamuc allocation)
이라고 한다.List<Number> l = new ArrayList<>(); // 동적 할당(dynamic allocation)
l.add(10);
l.add(20);
l.add(30);
l.add(40);
l.add(50);
l.remove(3);
System.out.println(l); // [10, 20, 30, 50]
ArrayList를 사용하기 위해선 패키지를 import
해줘야한다.
import java.util.ArrayList;
메소드 | 설명 |
---|---|
ArrayList() | 크기가 10인 ArrayList를 생성한다. |
ArrayList(Collection c) | 주어진 컬렉션이 저장된 ArrayList를 생성한다. |
ArrayList(int initialCapacity) | 지정된 초기용량을 갖는 ArrayList를 생성한다. |
위에 소개한 메소드들을 코드로 작성해보자.
// 타입설정 Integer 객체만 적재가능
ArrayList<Integer> members = new ArrayList<>();
// 초기 용량(capacity) 지정
ArrayList<Integer> num1 = new ArrayList<>(10);
// 배열을 넣어 생성
ArrayList<Integer> list1 = new ArrayList<>(Arrays.asList(1, 2, 3));
// 다른 컬렉션으로부터 그대로 요소를 받아와 생성
// ArrayList를 인자로 받는 API를 사용하기 위해서 Collection 타입 변환이 필요할 때 많이 사용
ArrayList<Integer> list2 = new ArratList<>(list1);
ArrayList 생성 문법을 보면 <>
기호를 이용해 타입을 지정하는 걸 알 수 있으며, 저 기호가 바로 제네릭이다.
제네릭(Generics)
: 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법만약 <>
안에 String 타입명을 작성하면 ArrayList 클랙스 자료형의 타입은 String 타입으로 지정되어 문자열 데이터만 리스트에 적재할 수 있게 된다.
아래 코드의 배열과 리스트 선언문 형태를 비교하면, 선언하는 키워드나 문법 순서만 다르고, 자료형명을 선언하고 자료형의 타입을 지정하는 것은 똑같다.
ArrayList에 요소를 추가할 때 객체를 생성할 때 지정한 타입의 데이터만 추가가 가능하다.
용량(capacity)
: 리스트의 공간 용량크기(size)
: 리스트에 들어있는 요소들의 총 개수메소드 | 설명 |
---|---|
boolean add(Object obj) | ArrayList의 마지막에 객체를 추가하며, 성공하면 true를 리턴한다. |
void addAll(Collection c) | 주어진 컬렉션의 모든 객체를 저장하며, 마지막 index 뒤에 붙인다. |
위에 소개한 메소드들을 코드로 작성해보자.
ArrayList<String> list1 = new ArrayList<>(5); // 용량 5으로 지정
list1.add("A");
System.out.println(list1.add("B")); // true 출력
System.out.println(list1.size()); // 들어있는 요소들의 총 개수인 2을 출력
ArrayList<String list2 = new ArrayList<>(5);
list2.add("C");
list2.add("D");
list1.addAll(list2); // list1에 list2의 내용 추가
System.out.println(list1); // [A, B, C, D] 출력
리스트의 원하는 인덱스에 데이터를 삽입할 수 있다.
이때, 원하는 인덱스에 요소를 넣을 수 있게 기존의 요소들이 한 칸씩 뒤로 이동되면서 빈 공간을 만들어낸다.
그렇게 데이터들을 뒤로 밀어내는 동작은 비용이 크기 때문에 ArrayList의 사이즈가 커질수록 비효율적이다.
메소드 | 설명 |
---|---|
boolean add(int index, Object obj) | 지정된 index에 객체를 저장하며, 자리에 있던 기존의 데이터는 삭제되지 않고 뒤로 밀려난다. |
void addAll(Collection c) | 지정된 index부터 주어진 컬렉션의 데이터를 저장하며, 자리에 있던 기존의 데이터는 삭제되지 않고 뒤로 밀려난다. |
위에 소개한 메소드들을 코드로 작성해보자.
ArrayList<String> list = new ArrayList<>(8);
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
// 3번째 인덱스 자리에 요소 삽입
list.add(3, "A");
System.out.println(list); // [1, 2, 3, A, 4, 5]
위치를 지정하여 삽입할 때 인덱스가 리스트의 용량을 넘지 않도록 조절해야한다.
만약, 용량이 10인 리스트에 100번째 인덱스 위치에 요소를 넣으려고 한다면, IndexOutOfBoundsException
예외가 발생하게 된다.
또, 리스트의 용량에 맞춰서 요소를 넣었을 때, 연속적인 데이터가 아니게된다면 이때도 IndexOutOfBoundsException
예외가 발생하게 된다.
왜냐하면 위 사진처럼 리스트의 용량(capacity)은 8이지만, 논리적인 공간(size)는 5이므로, 7번째 인덱스에 값을 삽입하는 것은 논리적인 공간을 넘어 불가능하다.
요소의 삭제도 중간에 위치한 요소를 제거할 경우, 나머지 요소들이 빈 공간을 채우려 앞쪽으로 이동하게 된다.
메소드 | 설명 |
---|---|
Object remove(int index) | 지정된 위치(index)에 있는 객체를 제거한다. |
boolean remove(Object obj) | 지정된 객체를 제거하고, 성공하면 true를 리턴한다. |
boolean removeAll(Collection c) | 지정한 컬렉션에 저장된 것과 동일한 객체들을 ArrayList에서 제거한다. |
void clear() | ArrayList를 완전히 비운다. |
boolean retainAll(Collection c) | ArrayList에 저장된 객체 중에서 주어진 컬렉션과 공통된 것들만 남기고 제거한다.(removeAll의 반대 버전) |
위에 소개한 메소드들을 코드로 작성해보자.
ArrayList<String> list = new ArrayList<>(8);
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
// 2번째 인덱스 자리의 요소 삭제
list.remove(2);
System.out.println(list); // [1, 2, 4, 5]
// 만약 모든 값을 제거하려면 반복문을 사용하는 것이 아니라
// 간단히 clear() 메소드를 사용하면 된다.
list.clear();
System.out.println(list); // []
위의 코드의 clear()
메소드 부분을 제외하고 그림으로 표현하면 아래와 같다.
메소드 | 설명 |
---|---|
boolean isEmpty() | ArrayList가 비어있는지 확인한다. |
boolean contains(Object obj) | 지정된 객체(obj)가 ArrayList에 포함되어 있는지 확인한다. |
int indexOf(Object obj) | 지정된 객체(obj)가 저장된 위치를 찾아 반환한다. |
int lastIndexOf(Object obj) | 지정된 객체(obj)가 저장된 위치를 뒤에서부터 역방향으로 찾아 반환한다. |
위에 소개한 메소드들을 코드로 작성해보자.
ArrayList<String> list1 = new ArrayList<>();
list1.add("A");
list1.add("B");
list1.add("C");
list1.add("A");
// list에 A가 있는지 검색 : true
list1.contains("A");
// list에 A가 있는지 순차적으로 검색하고 index를 반환 (만일 없으면 -1)
list1.indexOf("A"); // 0
// list에 A가 있는지 역순으로 검색하고 index를 반환 (만일 없으면 -1)
list1.lastIndexOf("A"); // 3
메소드 | 설명 |
---|---|
Object get(int index) | 지정된 위치(index)에 저장된 객체를 반환한다. |
List subList(int from_index, int to_index) | from_index부터 to_index-1 사이에 저장된 객체를 반환한다. |
위에 소개한 메소드들을 코드로 작성해보자.
ArrayList<String> list = new ArrayList<>(18);
list.add("P");
list.add("r");
list.add("o");
list.add("g");
list.add("r");
list.add("a");
list.add("m");
// 개별 단일 요소 반환
list.get(0); // "P"
list.get(3); // "g"
// list[0] ~ list[6] 범위 반환
list.subList(0, 7); // [P, r, o, g, r, a, m]
// list[3] ~ list[6] 범위 반환
list.subList(3, 7); // [g, r, a, m]
// list[3] ~ list[5] 범위 반환
list.subList(3, 6); // [g, r, a]
리스트 범위 반환 subList()
를 그림으로 그리면 아래와 같다.
메소드 | 설명 |
---|---|
Object set(int index, Object obj) | 주어진 객체(obj)를 지정한 위치(index)에 저장한다. 자리에 있던 기존의 데이터는 삭제되고, 새로운 데이터가 대체되어 들어간다. |
위에 소개한 메소드들을 코드로 작성해보자.
ArrayList<String> list = new ArrayList<>(18);
list.add("data1");
list.add("data2");
list.add("data3");
// index 1번의 데이터를 문자열 "setData"로 변경한다.
list.set(1, "setData");
System.out.println(list); // [data1, setData, data3]
ArrayList는 생성할 때 용량을 정할 수 있지만, 데이터를 추가하면서 용량(capacity)보다 크기(size)가 커진다면, 자동으로 용량을 늘려준다.
만약 자동으로 용량을 늘려야한다면, 자체적으로 배열을 큰사이즈로 새로 만들고, 기존의 배열에서 요소들을 복사해 간접적으로 리스트의 용량을 늘린다.
하지만 이런 동작은 리스트를 다루기엔 편하지만, 배열 복사 동작 자체가 비용이 꽤 크기 때문에 오버헤드
를 발생시키게 된다.
메소드 | 설명 |
---|---|
int size() | ArrayList에 저장된 객체의 개수를 반환한다. |
void ensureCapacity(int minCapacity) | ArrayList의 용량이 최소한 minCapacity가 되도록 한다. |
void trimToSize() | 용량의 크기에 맞게 줄인다(빈 공간을 없앤다.) |
위에 소개한 메소드들을 코드로 작성해보자.
ArrayList<String> list1 = new ArrayList<>(5); // 용량(capacity)를 5으로 설정
// 용량 5을 넘은 요소 7개 추가
list1.add("A");
list1.add("B");
list1.add("C");
list1.add("D");
list1.add("E");
list1.add("F");
list1.add("G");
// 크기(size)는 7 : 자동으로 용량이 증가되어 데이터를 적재함(오버헤드 발생)
list1.size();
// --------------------------------------------------
ArrayList<String> list2 = new ArrayList<>(5); // 용량(capacity)를 5으로 설정
// 용량 5에 맞게 요소 5개 추가
list2.add("A");
list2.add("B");
list2.add("C");
list2.add("D");
list2.add("E");
// ensureCapacity() 메소드를 이용해 용량 확장
list2.ensureCapacity(10);
// 용량 확장 후 요소 추가
list2.add("F");
list2.add("G");
// 미리 확장해 오버헤드 발생하지 않았다.
// 따라서, 사용될 데이트의 개수를 미리 알고있는 경우엔 처음에 그 값으로 선언해주면 된다.
System.out.println(list); // [1, 2, 3, 4, 5, 6, 7]
ArrayList를 정렬할 때 주의할 점은 sort()
메소드는 정렬된 값을 반환하는 것이 아니라, 원본 리스트 자체를 변경시키는 점이다.
메소드 | 설명 |
---|---|
void sort(Comparator c) | 지정된 정렬기준(c)으로 ArrayList를 정렬한다. |
Comparator를 이용해 정렬하기 위해선 패키지를 import
해줘야한다.
import java.util.Comparator;
위에 소개한 메소드들을 코드로 작성해보자.
import java.util.Comparator;
ArrayList<String> list1 = new ArrayList<>();
list1.add("3");
list1.add("2");
list1.add("1");
// 오름차순 정렬
list1.sort(Comparator.naturalOrder());
System.out.println(list1); // [1, 2, 3]
// 내림차순 정렬
list1.sort(Comparator.reverseOrder());
System.out.println(list1); // [3, 2, 1]
보통 ArrayList의 요소들을 순회할 일이 있다면 다음과 같이 for문으로 처리하는 것이 일반적이다.
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
for (Integer i : list){
System.out.println(i);
}
다만 몇몇 컬렉션에서는 저장된 요소를 Iterator 인터페이스
로 읽어오도록 하는 순회 패턴을 지향하기도 한다.
메소드 | 설명 |
---|---|
Iterator iterator() | ArrayList의 Iterator 객체를 반환한다. |
ListIterator listIterator() | ArrayList의 ListIterator를 반환한다. |
ListIterator listIterator(int index) | ArrayList의 지정된 위치부터 시작하는 ListIterator를 반환한다. |
Collection 인터페이스에서는 Iterator 인터페이스를 구현한 클래스의 인스턴스를 반환하는 irerator()
메소드를 정의해 각 요소에 접근하도록 정의하고 있다.
따라서 Collection 인터페이스를 상속받는 List나 Set 인터페이스에서도 iterator()
메소드를 사용할 수 있다.
Iterator를 사용하기 위해선 패키지를 import
해줘야한다.
import java.util.Iterator;
위에 소개한 메소드들을 코드로 작성해보자.
import java.util.Iterator;
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 이터레이터 객체 반환
Iterator<Integer> iter = list.iterator();
// 만일 다음 요소가 있을 경우 반복
while(iter.hasNext()){
System.out.println(iter.next()); // 1, 2, 3를 출력하고 종료
}
또한, ArrayList에는 Iterator
뿐만 아니라 리스트 전용 이터레이터 객체인 ListIterator
도 지원한다.
ListIterator
인터페이스는 Iterator
인터페이스를 상속받아 여러 기능을 추가한 인터페이스이며, Iterator
는 컬렉션의 요소를 접근할 때 단방향으로만 이동할 수 있는 반면, ListIterator
인터페이스는 컬렉션 요소의 대체, 추가, 검색 등을 위한 작업에서 양방향으로 이동하는 것을 지원해 쓰임새가 더 넓다.
그리고 Iterator
는 Collection 인터페이스를 구현한 컬렉션에서 모두 사용할 수 있는 반면, ListIterator는 오로지 List 컬렉션에서만 사용이 가능하다.
또한, ListIterator 역시 사용하기 위해선 패키지를 import
해줘야한다.
import java.util.ListIterator;
import java.util.ListIterator;
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 이터레이터 객체 반환
ListIterator<Integer> iter = list.listIterator();
// 만일 다음 요소가 있을 경우 반복(순방향)
while(iter.hasNext()){
System.out.println(iter.next()); // 1, 2, 3를 출력하고 종료
}
// 만일 이전 요소가 있을 경우 반복(역방향)
while(iter.hasPrevious()){
System.out.println(iter.previous()); // 3, 2, 1를 출력하고 종료
}
검색하다 보니, Inpa님 글이랑 거의 똑같은거 같아요.
거기서도 뭔가 의아했는데, ArrayList는 내부적으로 연속된 주소를 가진 배열을 이용하는 컬렉션입니다.
이외에는 잘 정리된 글 같습니다