목차
- 배열과 ArrayList 다른 점
- ArrayList를 대충 알아보자
- ArrayList 사용 방법
- ArrayList를 내부적으로 알아보자
배열을 크기를 정하지 않고 사용할 수 있는 방법이 없을까 하다가 공부한게 바로 ArrayList 입니다.
배열과 다르게 ArrayList는 크기가 제한되어 있지 않아서, 값을 넣고 용량을 넘을 때마다 크기가 알아서 커집니다.
이런 편리한 기능에 대해서 한번 알아볼게요!
배열과 ArrayList 다른 점
ArrayList는 배열과 비슷합니다.
배열처럼 각 값들에 index가 있고 그 index로 값들을 관리합니다. 그래서 쉽게 값들을 찾을 수 있습니다.
배열은 크기를 정해서 크기에 맞게 사용해야 하지만, ArrayList는 배열과 달리 크기가 정해져있지 않고 데이터 간에 빈공간이 없습니다. 그래서 크기를 알 수 없는 정보를 담을 때 배열보다 더 편리하게 사용할 수 있습니다. 또, 배열은 기본형과 참조형 변수가 모두 들어갈 수 있지만, ArrayList는 참조형 변수만 들어갈 수 있습니다.
크기가 변하지 않는 경우에는 배열을 사용하는 것이 더 좋은데, 그 이유는 ArrayList는 용량을 넘어서 값을 추가 할때마다 메모리를 재할당 하기 때문에 배열보다 느릴 수 있습니다.
ArrayList를 대충 알아보자
ArratList를 내부적으로 보면 배열로 이루어져 있습니다. 크기가 커지거나 할때 내부적으로 크기가 더 큰 배열을 생성하고 기존 배열의 데이터를 복사해서 사용하는 것이죠.
기존 배열의 크기를 넘어서 값이 들어오면 기존 배열의 1.5배 크기의 임시 배열이 생성되고 데이터가 복사됩니다. 그리고 값이 복사된 배열을 기존 배열로 사용하는 것입니다.
즉, (기존 배열 = 데이터가 복사된 용량 1.5배의 임시 생성된 배열)을 사용하는 것입니다.
내부적으로 설계된 코드들로 인해 배열의 용량보다 많은 값을 넣어도 우리는 간단히 ArrayList로 사용하면 됩니다.
값을 넣으면 어떻게 동작하는지 간단히 한번 알아볼게요.
값을 추가하면 데이터들이 1칸씩 뒤로 이동합니다.
{1, 2, 3, 4}가 들어있는 리스트가 있습니다.
위에 그림처럼 index 1 위치에 5를 넣어보면,
{1, 5, 2, 3, 4}가 됩니다. index 1 위치에 있던 데이터들이 1칸씩 뒤로 이동하고, index 1 위치에 5가 저장됩니다.
값을 제거하면 데이터들이 1칸씩 앞으로 이동합니다.
{1, 5, 2, 3, 4}가 들어있는 리스트에서
index 2에 있는 값을 삭제하면,
{1, 5, 3, 4}가 됩니다. index 2 위치에 있던 데이터가 없어지면서 뒤에 있던 3, 4가 앞으로 1칸씩 이동했습니다.
근데 만약에 {1, 5, 2, 3, 4}처럼 용량이 찬 상태에서 6이 추가되면 어떻게 될까요??
지금은 더이상 값을 넣을 공간이 없습니다.
그럴때는 위에서 말한 것처럼 내부적으로 크기를 1.5배의 크기로 키워주고, 값을 복사해줍니다.
현재 크기 = 5
1.5배의 크기 = 5+(5/2) = 7
(기존 배열 = 크기가 7이고 값들이 복사된 배열) 이 됩니다. 이제는 위에서 값을 추가한 것처럼 6을 추가하면 됩니다.
크기가 변하지 않을 경우에는 ArrayList가 아닌 배열을 사용하는 것이 속도가 더 빠릅니다. ArrayList는 크기가 커질때마다 배열을 임시 생성하고 복사 후 사용하니, 크기가 정해져 있으면 배열(크기를 정해서 사용)을 사용하는 게 더 빠릅니다.
이런 장단점이 있으니,
상황에 따라 배열과 리스트를 적절히 사용하면 더 좋을 것 같아요!
내부적으로 어떻게 작동하는지에 대해서는 밑에서 알아보겠습니다.
ArrayList 사용 방법
ArrayList 인스턴스 생성하기
1. ArrayList<Integer> list = new ArrayList<Integer>(); // = new ArrayList<>();로도 사용 가능합니다.
<>(제네릭) 안에 Integer뿐만 아니라 String이나 Double, Animal 등 참조형 타입이 들어갈 수 있고, 기본형 타입은 들어갈 수 없습니다. 이때, list는 Integer형으로 Integer만 담을 수 있습니다.
제네릭스를 사용하면 명확한 타입 체크가 가능해지고 잘못된 캐스팅으로 인한 오류를 막을 수 있습니다.
2. ArrayList list = new ArrayList();
이 방법은 권하지 않는 방법입니다. 제네릭스를 명확히 지정하라는 경고가 나오고, 나중에 사용 시 캐스팅을 잘못 해주면 오류가 날 수도 있기 때문에 제네릭스를 사용하는 것이 좋습니다. 이때, list는 Object 자료형으로 인식됩니다.
//Object 자료형은 모든 객체가 상속하고 있는 가장 기본적인 자료형입니다.
제네릭스를 사용하지 않아서 경고가 나오는 것을 볼 수 있고, 제네릭스 안에 int형으로 들어가서 오류가 나는 것을 볼 수 있습니다.
저는 list1, list2를 사용하겠습니다.
아직 list1과 list2는 비어있습니다.
저는 Integer형으로 되어 있어서 모두 Integer형으로 담고 있습니다. String이면 String으로 담아야 하고, 다른 참조형으로 되어 있다면 그에 맞는 참조형으로 사용해야 합니다!
ArrayList에 값 추가하기
list1.add(10);
비어있는 list1에 10이 추가됐습니다.
list1.add(0, 5);
list1의 index 0 위치에 5가 추가됩니다. 뒤에 있는 값은 1칸씩 밀려납니다.
index 0에 5가 추가됐고, 10이 뒤로 1칸 밀려났습니다.
list1.add(null);
Integer형이기에 null 값도 담을 수 있습니다.
list1에 null 값이 추가됩니다. 따로 index를 지정해주지 않는다면 값은 맨 뒤에 추가됩니다.
ArrayList에 값 삭제하기
list1.remove(1);
list1의 index 1 위치에 있는 값이 삭제됩니다. 뒤에 값들은 1칸씩 당겨집니다. 삭제된 값이 반환됩니다.
list1의 index 1 위치에 있던 10이 삭제됐습니다. 뒤에 있던 null이 1칸 당겨졌습니다.
반환 값은 삭제된 10을 반환합니다.
list1.clear();
list1에 있는 모든 값들이 삭제됩니다.
list1에 있던 모든 값들이 삭제됐습니다.
list1, list2에 아래 값들을 넣어줄게요!
list1 = {0, 1, 2, 3, 4}
list2 = {5, 6, 7}
for문으로 list1, list2에 값들을 넣었습니다.
ArrayList의 크기 구하기
list1.size();
list1의 크기를 반환합니다.
list1의 크기를 int size에 담아서 출력했습니다. 현재 list1의 크기는 5입니다.
ArrayList안의 값을 알고 싶을때
list1.contains(1);
list1에 1이 있으면 true, 없으면 false를 반환합니다.
list1에 1이 있기 때문에 true를 반환합니다.
list1.indexOf(1);
list1에 1이 있으면 그 값의 index 반환, 없으면 -1을 반환합니다.
list1 = {0, 1, 2, 3, 4} 중 1이 있기 때문에 1의 index인 1을 반환합니다.
list1.lastIndexOf(1);
list1에 1이 있는지 뒤에서 부터 확인한다. 있으면 index 반환, 없으면 -1을 반환합니다.
잘 반환 되는지 정확하게 확인하기 위해 list1에 값을 추가했습니다.
list1 = {0, 1, 2, 3, 4, -2, -1, 0, 1, 2, 3}
뒤에서부터 1이 있는지 확인하기 때문에 1이 아닌 8을 반환합니다.
list1.isEmpty();
list1이 비어있으면 true, 아니면 false를 반환합니다.
list1에 값들이 있기 때문에 false를 반환합니다.
list1.remove(list1.indexOf(1));
1을 찾아서 반환된 index를 remove 메소드를 이용해 삭제합니다.
뒤에서 부터 확인해서 1을 삭제하고 싶으면 list1.remove(list1.lastIndexOf(1));로 하면 index 8 위치에 있는 1이 삭제됩니다. 지금은 indexOf(1)이기 때문에 index 1 위치에 있는 1이 삭제됐습니다.
ArrayList안의 값을 얻고 싶을때
list1.get(3);
list1의 index 3 위치에 있는 값을 반환한다.
list1의 index 3 위치에 있는 값 4를 반환했습니다.
ArrayList 지정한 index의 값을 바꾸고 싶을때
list1.set(3,10);
list1의 index 3 위치에 있는 값을 10으로 바꿉니다. 삭제된 값이 반환됩니다.
list1의 index 3에 위치에 있던 4가 10으로 바꼈습니다. 반환 값은 삭제된 4을 반환합니다.
list1.addAll(list2);
list2의 값이 모두 list1에 추가됩니다. list2가 비어있으면 false를 반환하고, 아니면 true를 반환합니다.
list2의 {5, 6, 7}이 모두 list1에 추가됐습니다. list2에 값이 들어있기 때문에 true를 반환합니다.
list1.addAll(3, list2);
list2의 값이 list1의 index 3 위치에서부터 추가됩니다. list2가 비어있으면 false를 반환하고, 아니면 true를 반환합니다.
list2의 {5, 6, 7}이 list1의 index 3 에서부터 추가됐습니다. list2에 값이 들어있기 때문에 true를 반환합니다.
여기까지 ArrayList의 사용 방법을 알아봤습니다.
마지막으로 ArrayList 내부 코드를 몇 개만 알아보도록 하겠습니다!
ArrayList를 내부적으로 알아보자
ArrayList에서 크기가 어떻게 커지고 어떻게 관리되는지 궁금했습니다.
아까의 그림으로 보면,
크기가 5이고 용량이 꽉찬 ArrayList가 있습니다. 이때, 값을 추가한다면 크기가 한개씩 커질지 아니면 일정한 개수만큼 커질지 알아보니, 원래 크기에 1.5배의 크기만큼 커진다는 글을 봤습니다.
이렇게 원래 크기의 1.5배가 돼서 메모리에 하나의 값을 추가하고 남은 하나의 공간은 어떻게 되는지 한번 출력을 해보니 IndexOutOfBoundsException 오류가 나왔습니다.
하지만 배열에서는 값이 들어가 있지 않아도 0이 출력되는 것을 확인하고 난 후, 도대체 뭐가 다른지 궁금했습니다. 왜 0이 출력되지 않고 오류가 나오는지 알아보기 위해 ArrayList의 내부로 들어가봤습니다.
코드를 보면 ArrayList는 Object[] elementData;로 선언되어 있는 배열 elementData로 관리가 되고 있었고, grow() 메소드가 크기를 늘려주는 역할을 하고 있었습니다.
일단 먼저 add() 메소드부터 확인을 하면,
위의 그림 1로 예시를 들면,
elementData.length = 배열의 크기, 5
size = list의 크기(데이터 개수), 5
ArrayList.add(1);을 하면 add(1, elementData, size);로 다시 호출을 하고,
만약 (elementData.length == size)가 값이 true가 되면 grow() 메소드를 호출해서 elementData에 넣어줍니다. 그리고 elementData에 값을 추가해주고 있습니다.
elementData.length와 size가 같다면 값을 저장할 공간이 없는 것이기 때문에 grow() 메소드를 호출해서 배열의 크기를 늘려주는 듯 보입니다. (그림 1을 보면 저장할 공간이 없다.)
그럼 여기서 grow() 메소드를 보면,
grow() 메소드를 호출하면 grow(size + 1)을 다시 호출합니다.
grow(size + 1) 메소드를 해석하면,
minCapacity = size + 1, 리스트의 크기(데이터 개수)에 1을 더한 값, 6
oldCapacity = elementData.length, 현재 배열의 크기, 5
ArraysSupport.newArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
=
(minCapacity - oldCapacity)와 (oldCapacity >> 1 (비트 연산자, 나누기 2와 같다)) 중 더 큰 값 + oldCapacity
즉, (6 - 5 = 1)와 (5 / 2 = 2) 중 더 큰 값 + oldCapacity = 7 이렇게 계산할 수 있다.
만약 현재 배열의 크기가 0보다 크거나 배열이 비어있지 않으면,
newCapacity(생성될 배열의 크기) = 7이 되고,
elementData = Arrays.copyOf(elementData, 7);로 인해 크기가 7이고 elementData가 복사된 배열이 만들어지는 것을 볼 수 있다.
배열의 크기가 0보다 작거나 배열이 비어있으면,
elementData = (minCapacity와 (DEFAULT_CAPACITY = 10)) 중 큰 값을 크기로 가지는 배열을 가지게 된다.
//Object[] obj1 = Arrays.copyOf(Object[] obj, int length)는 0부터 length까지 obj를 복사해서 obj1에 넣어준다. 이때, length는 obj의 길이보다 커도 되고, obj1의 길이가 된다.
이렇게 grow() 메소드로 인해 크기가 5인 배열이 7로 커지는 것을 확인했습니다. (그림 2에 있는 배열 완성)
공간이 생겼으니 이제 elementData에 값을 추가할 수 있게 됐습니다.
그럼 이제 남은 공간을 얻으면 어떻게 되는지 코드를 통해서 확인해봐요.
get(int index) 메소드를 보면,
Objects.checkIndex(index, size);로 index를 체크하는 것처럼 보입니다.
Objects.chectIndex(index, size)를 확인해 볼까요?
Preconditions.checkIndex(index, length, null);를 반환합니다.
Preconditions.checkIndex(index, length, null)를 확인해 볼까요?
index가 0보다 작거나 length보다 크면 throw outthrow outOfBoundsCheckIndex(oobef, index, length);가 되네요?
요약해서,
get(int index)에서부터 보면,
찾으려는 index가 0보다 작거나 size(리스트에 있는 데이터의 개수)보다 크면 예외가 발생하도록 되어 있습니다.
그래서! ArrayList 배열의 남은 공간을 얻으면 오류가 나오는 것이었습니다!!
이렇게 해서 ArrayList 관한 궁금증을 내부 코드를 확인하면서 해결할 수 있었습니다!
지금까지 ArrayList에 관해서 알아봤습니다.
제가 공부한 부분을 정리한 내용이기 때문에 틀린 부분 있을 수 있습니다!!
혹시 틀린 부분이 있으면 알려주세요!!
'Java' 카테고리의 다른 글
[JAVA] 자바 설치 및 이클립스 설치하기! (0) | 2023.02.23 |
---|---|
[JAVA] System.arraycopy(src, srcPos, dest, destPos, length) 사용하기 (0) | 2023.02.23 |