ArrayList는 다른 자료구조와 달리 Object[] 배열(객체 배열)을 두고 사용한다
모든 자료구조는 '동적 할당'을 전제로한다
동적 할당을 안하고 사이즈를 정해놓고 구현한다면 메인함수에서 정적배열을 선언하는 것과 차이가 없다
데이터의 개수를 알 수 없을 경우 ArrayList나 LinkedList등의 자료를 선택할 수 있다,
사이즈를 정하지 않고 동적으로 활용할 수 있기 때문이다
리스트 계열 자료구조는 데이터 사이에 빈 공간을 허락하지 않는다.
Object[] a = {"a","b","c","d"};
라는 배열이 있고 만약 "c"라는 데이터를 삭제하게 되면
Object[] a = {"a","b",null,"d"};
이 될 것이다. null 뒤 모든 데이터를 한칸 씩 끌어와야한다
Object[] a = {"a","b","d",null};
이렇게 항상 리스트 계열 자료구조는 데이터들이 연속되어 있어야한다
생성
ArrayList를 사용하기 위해서는 우선 ArrayList객체를 만들어야한다
ArrayList<Integer> numbers = new ArrayList<>();
추가
엘리먼트를 추가할 때는 add메소드를 사용한다. add는 배열에 단순히 더해지는 것이기 때문에 빠르게 동작한다
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);
특정 위치에 추가하고 싶다면 메소드 add의 첫번째 인자로 인덱스를 지정한다
numbers.add(1, 50);
자바의 배열은 크기가 고정되어 있다. 데이터를 추가하는 과정에서 내부적으로 사용하는 배열이 꽉차면 기존의 배열 대비 크기가 2배 큰 새로운 배열을 만들고 기존의 데이터를 새로운 배열로 복제한다
삭제
특정 인덱스에 위치하는 엘리먼트를 삭제할 때는 remove를 사용한다
numbers.remove(2);
가져오기
엘리먼트를 가져올 때는 get을 사용한다. 이 때 내부적으로 배열을 이용하기 때문에 빠르게 엘리먼트를 가져올 수 있다
numbers.get(2);
반복
자바에서는 ArrayList를 탐색하기 위한 방법으로 iterator을 제공한다. 이것은 주로 객체지향 프로그래밍에서 사용하는 반복기법이다.
우한 Iterator 객체를 만들어야한다
Iterator it<Integer> = numbers.iterator();
Iterator 객체는 numbers 객체 내부에 저장된 값을 하나씩 순회하면서 탐색할 수 있도록 돕는 객체다
while(it.hasNext()){
System.out.println(it.next());
}
it.next() 메소드를 호출할 때마다 엘리먼트를 순서대로 리턴한다. 만약 더 이상 순회할 엘리먼트가 없다면 it.hasNext()의 값은 false가 되면서 while문이 종료된다.
순회 과정에서 필요에 따라서는 엘리먼트를 삭제/추가하는 작업을 할 수 있다.
while(it.hasNext()){
int value = it.next();
if(value == 30){
it.remove();
}
}
it.remove()는 it.next()를 통해서 반환된 numbers의 엘리먼트를 삭제하는 명령이다.
조금 더 편리한 방법도 있다.
for(int value : numbers){
System.out.println(value);
}
ArrayList 구현하기
오픈튜토리얼스
인덱스 값을 찾아내는 방법은 유용하게 쓸 수 있을 것 같다
public class ArrayList {
private int size = 0;
private Object[] elementData = new Object[100];
public Object indexOf(Object o){
for(int i=0; i < size; i++){
if(o.equals(elementData[i])){
return i;
}
}
return -1;
}
특정한 값을 가진 엘리먼트의 인덱스 값을 알아내는 방법
값이 있다면 그 값이 발견되는 첫번째 인덱스 값을 리턴하고 없다면 -1리턴
ArrayList numbers = new ArrayList();
numbers.addLast(10);
numbers.addLast(20);
numbers.addLast(30);
System.out.println(numbers.indexOf(20));
System.out.println(numbers.indexOf(40));
// 1
// -1
<필수>
ST님 티스토리를 보고 클론코딩을 하며 이해해보려고한다
>>바로가기<<
[ArrayList 클래스 및 생성자 구성하기]
List인터페이스를 implements해준다
import Interface.List;
import java.util.Arrays;
public class ArrayList<E> implements List<E> {
private static final int DEFAULT_CAPACITY = 10; // 최소(기본) 용적 크기
private static final Object[] EMPTY_ARRAY = {}; // 빈 배열
private int size; // 요소 개수
Object[] array; // 요소를 담을 배열
// 생성자 1 (초기 공간 할당 X)
public ArrayList() {
this.array = EMPTY_ARRAY;
this.size = 0;
}
// 생성자2 (초기 공간 할당 O)
public ArrayList(int capacity) {
this.array = new Object[capacity];
this.size = 0;
}
DEFAULT_CAPACITY : 배열이 생성 될 때의 최초 할당 크기(용적)이자 최소 할당 용적 변수로 기본값은 10으로 설정해두었다. (capacity가 용적이라는 의미다)
EMPTY_ARRAY : 아무 것도 없는 빈 배열 (용적이 0인 배열)
size : 배열에 담긴 요소(원소)의 개수 변수 (용적 크기가 아니다. 절대 헷갈리지 말자)
array : 요소들을 담을 배열
그리고 DEFAULT_CAPACITY 변수와 EMPTY_ARRAY 변수는 상수로 쓸 것이기 때문에 static final 키워드를 붙인다.
생성자 1의 경우 사용자가 공간 할당을 미리 안하고 객체만 생성하고 싶을 때
사용자가 공간 할당을 명시하지 않았기 때문에 array변수를 EMPTY_ARRAY로 초기화 시켜 놓는다
ArrayList<Integer> list = new ArrayList<>();
반면에 사용자가 데이터의 개수를 예측할 수 있어서 미리 공간 할당을 해놓고 싶을 경우, array의 공간 할당을 입력된 수 만큼 (여기선 array=new Object[100])배열로 만든다. 그리고 size를 0으로 초기화 시켜 놓는다
ArrayList<Integer> list = new ArrayList<>(100);
size는 요소(원소)의 개수를 의미하는 것이다. 공간을 할당해놓는 것과는 다른 개념이다
[동적할당을 위한 resize 메소드 구현]
데이터는 적은데 용적이 크면 메모리가 낭비되고, 용적이 적은데 데이터가 많으면 넘치는 데이터를 보관할 수 없다.
그래서 size(요소의 개수)가 용적(capacity)에 얼마나 차있는지 확인하고, 적절한 크기에 맞춰 배열의 용적을 변경해야한다. 용적은 외부에서 마음대로 접근하면 데이터의 손상을 야기할 수 있기 때문에 private로 접근을 제한해준다
private void resize() {
int array_capacity = array.length;
// if array's capacity is 0
if (Arrays.equals(array, EMPTY_ARRAY)) {
array = new Object[DEFAULT_CAPACITY];
return;
}
// 용량이 꽉 찰 경우
if (size == array_capacity) {
int new_capacity = array_capacity * 2;
// copy
array = Arrays.copyOf(array, new_capacity);
return;
}
// 용적의 절반 미만으로 요소가 차지하고 있을 경우
if (size < (array_capacity / 2)) {
int new_capacity = array_capacity / 2;
// copy
array = Arrays.copyOf(array, Math.max(new_capacity, DEFAULT_CAPACITY));
return;
}
}
현재 array의 용적(=array 길이)과 데이터의 개수(size)를 비교한다
// 참고할 내용.
private static final int DEFAULT_CAPACITY = 10; // 최소(기본) 용적 크기
private static final Object[] EMPTY_ARRAY = {}; // 빈 배열
// if array's capacity is 0
if (Arrays.equals(array, EMPTY_ARRAY)) {
array = new Object[DEFAULT_CAPACITY];
return;
}
조건문 1 : if(Arrays.equals(array,EMPTY_ARRAY))
생성자에서 사용자가 용적을 별도로 설정하지 않은 경우 EMPTY_ARRAY로 초기화되어 있어 용적이 0인 상태다
이 경우를 고려하여 새로 array의 용적을 할당하기 위해 최소 용적으로 설정해두었던 DEFAULT_CAPACITY의 크기만큼 배열을 생성해주고 메소드를 종료한다
또한 주소가 아닌 값을 비교해야하기 때문에 Arrays.equals() 메소드를 사용한다
// 용량이 꽉 찰 경우
if (size == array_capacity) {
int new_capacity = array_capacity * 2;
// copy
array = Arrays.copyOf(array, new_capacity);
return;
}
조건문 2 : if(size==array_capacity)
데이터가 꽉 찰 경우에는 데이터(요소)를 더 받아오기 위해서 용적을 늘려야 한다
즉, 데이터의 개수가 용적과 같을 경우는 꽉 차있는 경우를 말한다. 이 때 새롭게 용적을 늘릴 필요가 있으므로 새로운 용적을 현재 용적의 2배로 설정한다
또한 기존에 담겨있던 요소들을 새롭게 복사하는데 Arrays.copyOf() 메소드를 사용한다
Arrays.copyOf()는 특정 배열의 원하는 길이만큼 새로운 배열로 복사하는 메소드 함수이다.
새로운 배열 생성이 가능하고, 전부 복사하거나 복사 대상의 객체를 유지시킬 필요가 없을때 사용한다
(참고로 System.arraycopy() : 복사 길이를 명시해야 하거나, 객체를 유지하고자 할때 사용하는 메소드도 있다.)
새로운 배열 = Arrays.copyOf(원본 배열, 원본 배열에서 복사하고 싶은 요소들의 길이);
첫 번째 파라미터로 복사할 배열을 넣어주고
두 번째 파라미터는 용적의 크기를 넣어준다
만약 복사할 배열보다 용적의 크기가 클 경우 먼저 배열을 복사한 뒤, 나머지 빈 공간은 null로 채워지기 때문에 편리하다.
// 용적의 절반 미만으로 요소가 차지하고 있을 경우
if (size < (array_capacity / 2)) {
int new_capacity = array_capacity / 2;
조건문 3 : if(size < (array_capacity/2))
데이터가 용적에 절반 미만으로 차지 않을 경우다. 이 경우 데이터는 적은데 빈 공간이 메모리를 차지하고 있어 불필요한 공간을 낭비하기때문에 사이즈를 줄여준다.
데이터의 개수가 용적의 절반 미만이라면 용적도 절반으로 줄여주도록 하기 위해 새로운 용적(new_capacity)을 현재 용적의 절반으로 둔 뒤, Arrays.copyOf() 메소드를 통해 새로운 용적의 배열을 생성해준다
만약 복사할 배열보다 용적의 크기가 작을 경우, 새로운 용적까지만 복사하고 반환하기 때문에 예외발생에 대해 안전하다
[add 메소드 구현]
ArrayList에 데이터를 추가한다.
리스트 인터페이스에 있는 add()메소드를 반드시 구현해야 하기 때문에 오버라이드(재정의)한다
add 메소드는 크게 3가지로 분류한다.
자바에 내장되어 있는 ArrayList에서는 addLast() 역할을 add()메소드가 하고
특정 위치 추가는 add(int index, E element)로 오버로딩된 메소드가 하며
addFirst()라는 함수는 없지만 편의상 구현한다.
@Override
public boolean add(E value) {
addLast(value);
return true;
}
public void addLast(E value) {
// 꽉차있는 상태라면 용적 재할당
if (size == array.length) {
resize();
}
array[size] = value; // 마지막 위치에 요소 추가
size++; // 사이즈 1 증가
}
add를 호출하면 자동으로 파라미터로 넘어온 value는 addLast로 보내진다. 그리고 데이터를 넣기 전 현재 용적이 꽉 차있는지 검사한다
만약 꽉차있다면 resize()메소드를 호출해서 array가 더 큰 용적을 갖도록 만들어준다
그리고 마지막 위치(size)에 value를 추가해주고 size를 1 증가시킨다.
size = 요소의 개수
index = 0 부터 시작
array[size] = value;를 해줘야한다
@Override
public void add(int index, E value) {
if (index > size || index < 0) { // 영역을 벗어날 경우 예외 발생
throw new IndexOutOfBoundsException();
}
if (index == size) { // index가 마지막 위치라면 addLast 메소드로 요소추가
addLast(value);
}
else {
if(size == array.length) { // 꽉차있다면 용적 재할당
resize();
}
// index 기준 후자에 있는 모든 요소들 한 칸씩 뒤로 밀기
for (int i = size; i > index; i--) {
array[i] = array[i - 1];
}
array[index] = value; // index 위치에 요소 할당
size++;
}
}
먼저 index로 들어오는 값이 size를 벗어나는지(빈 공간을 허용하지 않기때문에), 또는 음수가 들어오는지 확인하고 만약 범위를 벗어나거나 음수가 들어오면 예외를 발생시킨다
사용자가 넘겨준 index와 size가 같다는 것은 마미작에 추가하는 것과 같은 의미이기 때문에
addLast() 메소드를 호출한다
else부터는 중간에 삽입되는 경우인데
size가 배열의 용적과 같다는 것은 꽉차서 더이상 들어올 공간이 없다는 뜻이므로 resize()를 호출해 용적을 늘리고 index와 그 뒤의 데이터들을 한 칸씩 뒤로 밀어야하기 때문에 반복문을 사용해서 밀어준다.
그리고 array[index]에는 새로운 value를 넣어준다
public void addFirst(E value) {
add(0, value);
}
size와 index 참조 부분을 항상 생각해줘야한다.
**[get,set,indexOf,contains 메소드 구현]
@SuppressWarnings("unchecked")
@Override
public E get(int index) {
if(index >= size || index < 0) { // 범위 벗어나면 예외 발생
throw new IndexOutOfBoundsException();
}
// Object 타입에서 E타입으로 캐스팅 후 반환
return (E) array[index];
}
@SuppressWarnings("unchecked")를 붙이지 않으면 타입 안정성에 대해 경고를 보낸다
반환되는 것을 보면 E타입으로 캐스팅을 하고 있고 그 대상이 되는 것은 Object[]배열의 오브젝트 데이터다.
즉 Object > E 타입으로 변환을 하는 것인데 이 과정에서 변활할 수 없는 타입을 가능성이 있따는 경고로 메소드 옆에 경고표시가 뜨는데, 우리가 add하여 받아들이는 데이터 타입은 유일하게 E 타입만 존재한다.
형 안정성이 보장되어 ClassCastException이 뜨지 않으니 경고를 무시하겠다는 뜻이다.
형변환시 예외 가능성이 없을 확실한 경우에 최소한의 범위에서 사용하는 것이 좋다.
그렇지 않으면 중요한 경고 메세지를 놓칠 수 있다
get 메소드도 index가 음수이거나 size와 같거나 큰 수가 들어올 경우 잘못된 참조를 하고있기 때문에
예외를 발생시킨다.
만약 index가 정상적인 참조가 가능한 값일 경우 해당 인덱스의 요소를 반환해준다. 이 때 원본 데이터 타입으로 반환하기 위해 E 타입으로 캐스팅한다
@Override
public void set(int index, E value) {
if (index >= size || index < 0) { // 범위를 벗어날 경우 예외 발생
throw new IndexOutOfBoundsException();
}
else {
// 해당 위치의 요소를 교체
array[index] = value;
}
}
index를 참조하는 모든 메소드들은 반드시 예외 처리를 해서 검사를 해야한다.
객체끼리 비교할때는 동등연산자(==)이 아니라 반드시 .equals()로 비교해야한다.
객체끼리 비교할때 동등연산자를 쓰면 값을 비교하는 것이 아닌 주소를 비교하는 것이기 때문이다
(이건 예전에 학원에서 프로젝트를 할때 실수했던 범위다.. 꼭 차이점을 알아두자)
@Override
public int indexOf(Object value) {
int i = 0;
// value와 같은 객체(요소 값)일 경우 i(위치) 반환
for (i = 0; i < size; i++) {
if (array[i].equals(value)) {
return i;
}
}
// 일치하는 것이 없을경우 -1을 반환
return -1;
}
public int lastIndexOf(Object value) {
for(int i = size - 1; i >= 0; i--) {
if(array[i].equals(value)) {
return i;
}
}
return -1;
}
위 메소드는 거꾸로 탐색하는 과정이다. 만약 찾고자하는 인덱스가 뒤쪽이라고 예상했을 때 굳이 앞부터 찾을 필요가 없기 때문이다. 나중에 Stack에서도 이용가능하니까 만들어준다
@Override
public boolean contains(Object value) {
// 0 이상이면 요소가 존재한다는 뜻
if(indexOf(value) >= 0) {
return true;
}
else {
return false;
}
}
[remove 메소드 구현]
remove의 메소드의 경우 크게 2가지로 나눌 수 있다.
자바에 내장되어 있는 ArrayList도 remove()메소드는 없다,
remove(int index)와 remove(Object value)메소드만 존재한다
index에 위치한 데이터를 삭제해버리고, 해당 위치 이후의 데이터들을 한칸씩 당겨온다
index의 요소를 임시변수에 담고 배열에서는 지운다
그 다음 데이터들을 한 칸씩 당겨온다
데이터 사이의 빈 공간을 없앴으면 size값을 줄여준다.
삭제되면서 데이터가 일정 이상 비워진 경우 용적을 줄이기 위해 resize()메소드를 마지막에 추가한다
@SuppressWarnings("unchecked")
@Override
public E remove(int index) {
if (index >= size || index < 0) {
throw new IndexOutOfBoundsException();
}
E element = (E) array[index]; // 삭제될 요소를 반환하기 위해 임시로 담아둠
array[index] = null;
// 삭제한 요소의 뒤에 있는 모든 요소들을 한 칸씩 당겨옴
for (int i = index; i < size - 1; i++) {
array[i] = array[i + 1];
// 이 부분은 구현하지 않아도 무방하다
array[i + 1] = null;
}
size--;
resize();
return element;
}
삭제된 원소를 반환해야되서 타입캐스팅을 해주면서 경고창이 뜬다.
get() 메소드에서와 같이 삭제되는 원소 또한 E 타입 외에 들어오는 것이 없기 때문에 형 안정성이 보장되므로 @SuppressWarnings("unchecked")를 붙여준다
항상 마지막 원소의 인덱스는 size보다 1작다. 그렇기 때문에 범위 체크와, 이후 배열 요소들을 한 칸씩 당겨올때 시작점과 끝 점을 잘 생각해야한다.
또한 명시적으로 null처리를 해줘야 가비지컬렉터에 의해 더이상 쓰지 않는 데이터의 메모리를 수거(반환)해주기 때문에 null 처리를 해주는 것이 좋다.
(명시적으로 안해도 문제는 없지만 가비지컬렉터가 쓰지 않는 데이터라도 나중에 참조될 가능성이 있는 데이터로 볼 가능성이 높아지기 때문에 프로그램 성능에 영향을 미친다.)
index가 정상적인 참조가 가능한 값일 경우 해당 인덱스의 요소를 반환한다.
이때 원본 데이터 타입으로 반환하기 위해 E 타입으로 캐스팅 해준다
이 메소드에서 필요한 동작은 value와 같은 요소가 존재하는지, 존재한다면 몇번째 위치에 존재하는지
그 index의 데이터를 지우고 뒤의 요소들을 하나씩 당겨오는 작업. 총 두가지가 필요하다
리스트에 특정 요소와 같은 요소의 index를 찾는 작업은 indexOf()메소드
index를 통해 삭제하는 작업은 remove(int index) 두 개의 메소드를 조합한다.
@Override
public boolean remove(Object value) {
// 삭제하고자 하는 요소의 인덱스 찾기
int index = indexOf(value);
// -1이라면 array에 요소가 없다는 의미이므로 false 반환
if (index == -1) {
return false;
}
// index 위치에 있는 요소를 삭제
remove(index);
return true;
}
indexOf 메소드를 통해 해당 value와 일치하는 요소를 찾고 -1이라면 일치하는 요소가 없다는 의미이므로 false, 반대의 경우 index에 해당하는 요소를 제거하고 true를 반환한다
[size,isEmpty,clear 메소드 구현]
@Override
public int size() {
return size; // 요소 개수 반환
}
현재 ArrayList에 요소가 단 하나도 존재하지 않고 비어있는지 알려준다.
리스트가 비어있을 경우 true, 한개라도 요소가 존재할 경우 false를 반환한다
size(요소의 개수) == 0 이면 true, 아니면 false라는 것이다.
굳이 배열울 모두 순회하여 데이터가 존재하는지 검사해줄 필요가 없다.
@Override
public boolean isEmpty() {
return size == 0; // 요소가 0개일 경우 비어있다는 의미이므로 true반환
}
초기값이 아니라 절반을 해주는 이유는 초기값으로 초기화해도 되지만 현재 배열의 용적은 결국 데이터를 해당 용적에 만족하는 조건만큰 썼다는 의미이기 때문에 앞으로 들어올 데이터를 대비해 일단 절반으로만 줄인다
모든 배열은 명시적으로 null처리해준다
@Override
public void clear() {
// 모든 공간을 null 처리 해준다.
for (int i = 0; i < size; i++) {
array[i] = null;
}
size = 0;
resize();
}
참고 사항으로 clone과 toArray메소드가 있는데
이건 나중에 사용하는 경우가 있다면 추가하도록하자
클론 코딩을 하면 시간이 오래 걸리지만 확실히 이해하기는 좋은 것 같다
이게 내가 공부하는데 도움이 됐으면 좋겠다....
이건 나중에 알고리즘 시간복잡도 공부할때 참고