배열
- 같은 자료형의 변수를 하나의 묶음으로 다루는 것이다.
- 배열은 저장된 값마다 인덱스 번호를 부여해(Zero-base) 원하는 수만큼 값을 저장할 수 있다.
배열의 선언
- 변수와는 다르게 대괄호를 붙여 선언하며, Stack에 데이터를 저장할 수 있는 "배열명" 이름의 공간을 만들어 주는 것을 의미한다.
자료형[] 배열명;
자료형 배열명[];
배열의 할당
- Stack에 선언한 공간은 하나이기 때문에 해당 공간에는 2개 이상의 값이 들어갈 수 없다. 따라서 new 연산자를 이용해 Heap 영역에 원하는 수만큼 공간을 할당해주어야 값의 저장이 가능하다.
자료형[] 배열명 = new 자료형[배열크기];
자료형 배열명[] = new 자료형[배열크기]; - new 연산자는 Heap 영역에 공간을 만든 후 해당 공간에 대한 주소를 발생시키는 역할을 한다.
- Stack 영역 및 Heap 영역에 배열 저장에 필요한 공간들을 만들었다면 해당 공간 사이의 연결이 필요하다. 서로 연결되어 있지 않으면 해당 값을 찾아갈 수 없기 때문이다. 따라서 대입 연산자(=)를 이용해 new가 만든 주소값을 Stack의 "변수명" 공간에 저장한다.
이렇게 주소를 가리키는 것을 포인터라고 한다. - 배열은 참조 변수로 Heap 영역에 할당되며, 배열 공간의 주소를 이용해 인덱스를 참조하는 방식으로 사용된다.
참조 변수: 배열의 Stack 공간에는 주소가 들어가 있는데 이렇게 주소를 저장하여 다른 공간을 참조하는 변수를 참조 변수라고 한다.
배열 초기화
- 인덱스를 이용한 초기화 : 배열 하나하나 값을 넣는 초기화 방식이다. ex. arr[0] = 1;
- for문을 이용한 초기화 : index가 순차적으로 증가함에 따라 초기화할 리터럴 값이 규칙적인 경우 반복문을 통해 배열을 초기화할 수 있다.
반복문을 사용하여 초기화할 경우 배열의 길이는 arr.length를 사용하여 구한다.
String의 길이를 구할 때 사용되는 str.length()는 메소드이지만, arr.length는 배열 안에 있는 기본 변수이기 때문에 소괄호를 붙이지 않고 사용한다. - 선언과 동시에 초기화 : 중괄호를 이용하여 초기화하는 방식이다.
ex. int[] arr = {1, 2, 3, 4, 5};
선언과 동시에 초기화하는 경우 중괄호 안의 값의 개수에 따라 해당 개수만큼의 공간이 만들어진다.
new를 사용한 초기화도 가능하다.
ex. int[] arr = new int[] {1, 2, 3, 4, 5}; - 아래와 같이 배열의 인덱스에 접근하여 값을 출력하는 것이 아니라, 해당 배열 자체를 출력하는 경우에는 해당 배열의 주소값이 출력된다.
해당 주소값은 정확한 주소값은 아니며, 정확한 주소값은 자바에서 제공하지 않기 때문에 알 수 없다.for(int i = 0; i < 5; i++) { arr[i] = (i+1) * 10; } System.out.println(arr);
- 배열 크기를 처음과 다르게 변경하기 위해 new를 사용해 새로 할당할 경우 기존 배열이 추가/삭제 되는 것이 아니라 해당 크기만큼의 새로운 배열이 Heap 영역에 생성되고, 해당 주소를 다시 참조하게 된다.
하지만 다른 주소를 참조하게 될 경우 Stack에 있는 주소값 저장 공간에는 하나의 값만 들어갈 수 있기 때문에 원래의 주소는 지워지고, 변경된 배열의 주소만 남게 된다.
연결이 끊긴 배열의 경우 JVM이 알아서 해당 메모리를 삭제해 준다. - 배열의 연결을 아예 끊고 싶은 경우에는 null을 할당해주면 된다.
배열의 복사
배열에 있는 데이터의 손실을 방지하기 위해 배열을 복사할 수 있다.
- 얕은 복사: 객체의 주소값만 가져와 다른 참조형 변수에 저장하는 것을 말한다. 이 경우 하나의 객체를 두 변수가 참조하게 된다.
얕은 복사의 방법은 다음과 같다.- Stack 영역에 arr1을 만든 후, Heap에 만들어진 공간의 주소값을 저장한다.
- Stack 영역에 arr2를 만든 후 1의 주소값을 arr2에도 저장한다.
- 깊은 복사: 새로운 배열 객체를 생성하여 기존 배열의 데이터를 복사하는 것을 말한다.
깊은 복사의 경우 새로운 주소를 만들어 값을 복사하는 것이기 때문에 사본을 변경하여도 원본에 영향을 미치지 않는다.
깊은 복사의 방법에는 다음 세 가지 방법이 있다.
- for문을 이용해 복사
int[] originArr = {1, 2, 3, 4, 5}; int[] copyArr = new int[originArr.length]; for(int i = 0; i < originArr.length; i++) { copyArr[i] = originArr[i]; }
- System.arraycopy()를 이용해 복사
System.arraycopy() 메소드는 다음과 같이 정의되어 있다.
System.arraycopy(src, srcPos, dest, destPos, length);
src : 원본 배열
srcPos : src에 대한 포지션, 원본 배열에서 복사를 시작할 위치
dest : 복사 배열 (사본 배열)
destPos : 복사 배열에서 붙여넣기를 시작할 위치
length : 복사 길이 (얼마나 복사할 것인지)
원본 배열의 0번 인덱스부터 원본 배열의 길이만큼 복사한 후, 복사 배열의 3번 인덱스부터 붙여넣는 경우 다음과 같이 작성한다.System.arraycopy(originArr, 0, copyArr, 3, originArr.length);
- Arrays.copyOf를 사용해 복사
Arrays.copyOf는 다음과 같이 정의되어 있다.
Arrays.copyOf(original, newLength);
반환값 : 배열
original : 원본 배열
newLength : 복사할 길이
int[] originArr = {1, 2, 3, 4, 5}; int[] copyArr = new int[10]; copyArr = Arrays.copyOf(originArr, originArr.length);
위 코드의 경우 복사한 데이터를 어디에 붙여넣을지 명시해 놓지 않았기 때문에 복사된 배열을 반환하여 대입 연산자를 이용해 저장해 주어야 한다.
copyOf에서 ctrl + 클릭을 통해 볼 수 있는 메소드의 원형은 다음과 같다.
public static int[] copyOf(int[] original, int newLength) { int[] copy = new int[newLength]; System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
copyOf 메소드의 original 부분에 원본 배열을 넣어 호출하게 되면 Stack에 만들어진 original이 원본 배열의 주소를 참조하는 얕은 복사가 일어난다.
int[] copy = new int[newLength];를 통해 길이가 newLenght인 배열을 Heap 영역에 새로 생성한다.
이후 다음 줄에서 System.arraycopy()를 이용해 original 배열을 만들어둔 copy 변수에 복사한다.
이때 사용되는 Math.min() 메소드는 둘 중 최소값을 고르는 메소드로 원본 배열의 길이와 넘겨받은 길이 중 더 작은 값을 골라 사용한다. 큰 값을 사용할 경우 인덱스 범위를 초과할 수 있기 때문이다.
마지막 줄의 return은 원래 호출해 준 메소드로 돌아가는 역할을 한다. 그런데 이때 copy가 있으므로 copy의 주소 값을 가지고 돌아가 반환해준다는 것을 의미한다.
copy의 주소 값은 copyArr에 저장되며(얕은 복사), 새로운 주소값을 참조하게 되었기 때문에 copyArr이 원래 가지고 있던 배열의 주소값은 버려진다.
따라서 System.arraycopy()를 사용하는 경우에는 복사한 후 주소값이 같으나, Arrays.copyOf를 사용하는 경우에는 복사한 후 주소값이 달라지고, 길이가 다른 경우 처음 초기화했던 크기 또한 유지하지 않는다.
- for문을 이용해 복사