DEV-STUDY/Java

배열 이해하기

HwangJerry 2024. 1. 24. 14:40

배열은 자료형을 이용하는 가장 단순한 자료구조이다. 물리적으로 연속된 메모리 공간을 할당하여 여러 데이터를 한번에 저장하는 방식이다.

  • 하나의 변수에 여러개의 공간을 할당하여 사용
  • 같은 타입의 데이터만 저장할 수 있고, 공간은 idx 로 구분. idx는 0번 부터 시작.
  • 배열은 reference 타입으로, 객체를 생성해야 사용할 수 있음. 배열 객체를 생성하면 기본 값으로 초기화된다.
  • 배열의 접근은 0 ~ arr.length - 1까지 접근 가능하고, 범위를 벗어나면 "ArrayIndexOutOfBoundException" 발생
  • 배열의 최대 사이즈는 ? 자바는 기본적으로 사용하는 정수 타입이 int라 배열의 사이즈도 21억까지만 생성 가능. 따라서 2GB 이상의 파일을 로드하기 위해선 NIO라는 걸 사용해야 함.

정적 배열이 배열의 기본이며, 초기에 메모리 공간을 할당하기 위해 배열의 크기를 선언해줘야 하며, 연속된 메모리 공간에 위치하므로 자바에서 해당 데이터로 접근하는 속도가 다른 컬렉션 자료형 대비 비교적 빠르다는 것이 특징이다. (동적 배열은 Doubling을 통해 크기 갱신 + 깊은 복사를 구현하여 배열 크기를 의식하지 않고 사용 가능)

자바는 오래된 언어여서 그런지 배열 선언 문법을 두 가지로 제공한다.

  • Object[] var1; // 자바 스타일
  • Object var2[]; // C언어 스타일 (C언어 사용자들을 위해 이 방식도 지원하고 있음)
int[] a, b, c;  
int[] i, j[], k[][];  

int[] array = new int[3];  
for (int l = 0; l < array.length; l++) {  
    // %d 정수 , %.1f 실수 %c 문자 %b 논리 %S 문자열 %n 뉴라인(\n)  
    System.out.printf("array[%d]: %d %n", 1, array[1]);  
}  
// 배열을 선언, 생성, 할당을 동시에  
// Object[] var = {값1, 값2, ...}; // 값의 개수만큼 배열 크기가 만들어진다.  
int[] array2 = {1, 2, 3, 4, 5};  
System.out.println(Arrays.toString(array2)); // [1, 2, 3, 4, 5]  

int[] array3 = {3, 1, 5, 7, 4};  
Arrays.sort(array3);  

System.out.println(Arrays.toString(array3)); // [1, 3, 4, 5, 7]

여기서 arr변수는 메모리에 있는 배열의 주소값을 참조하는 reference타입의 변수이다. 해당 주소는 0번째 인덱스를 가리키며, 내부적으로는 정확한 메모리 주소값이 아닌 해시값을 가져서 바로 참조할 수 있도록 구현되어 있다.

  • C++같은 언어들은 배열을 객체로 잡지 않고 기본형 데이터와 같이 stack에 저장하기 때문에 빠르게 사용할 수 있음. 하지만 자바는 배열을 객체와 같이 참조변수로 관리함. 따라서 힙에 저장되므로 접근이 느림. 왜 자바는 배열을 이렇게 구현했을까? 자바는 포인터를 제공하지 않아서 오로지 pass by value밖에 안되기 때문이다. 자바는 포인터를 사용자에게 노출시키지 않으므로 주소값을 줄 수가 없음. 따라서 레퍼런스로 전달해야 메서드, 인자, 리턴에 사용할 수 있음.
/*  
* 자바는 대입, 리턴, 인자의 값을 전달할 때 pass by value만 한다.  
*  
* pass by value:  
* - 변수를 위해 할당된 메모리에 있는 값만을 전달한다.  
* - 기본형 : 메모리에 값이 바로 저장되어 있어서, 그 값을 바로 전달 가능.  
* - 참조형 : 참조값(hash code)이 메모리에 저장되어 있어서, 그 값을 전달하면 힙에 있는 매핑 테이블을 통해 힙 주소에서 해당 데이터를 가져오게끔 설계되어 있음  
* */
public static int plusTenToPrimitive(int copy) {  
    System.out.println("copy : " + copy);  
    copy += 10;  
    System.out.println("copy : " + copy);  
    return copy;  
}  

public static void plusFiveToArray(int[] copy) {  
    for (int i = 0; i < copy.length; i++) {  
        copy[i] += 5;  
    }  
}  

public static void main(String[] args) {  
    // 원시타입 pass by value    int primitive = 10;  
    System.out.println("primitive = " + primitive); // primitive = 10  
    plusTenToPrimitive(primitive); // copy : 10, copy : 20 -> 원시타입은 로컬변수마다 스택프레임 주소를 별개로 운영하니까 이대론 아무 일도 일어나지 않음  
    // primitive = pass1(primitive); //이렇게 하면 primitive에 10을 더해줄 수 있음  
    System.out.println("primitive = " + primitive); // primitive = 10  

    // 참조타입 pass by value    int[] arr = new int[3];  
    System.out.println(Arrays.toString(arr)); // [0, 0, 0]  
    plusFiveToArray(arr); // 참조 타입은 해시 코드가 그대로 파라미터로 들어가기 때문에 해당 주소에 있는 값을 메서드 내에서 바로 수정됨.  
    System.out.println(Arrays.toString(arr)); // [5, 5, 5]  
}

주의 : 자바 기본 제공 API 중에서 다음 두 개의 API를 봐 보자.

public static void changeArray(int[] arr) {} // 5씩 더해주는 함수
public static int[] changeArray2(int[] arr) {} // 5씩 더해주는 함수

분명 참조형 변수를 파라미터로 받고 있으니, 리턴값이 없더라도 입력되는 arr의 값을 변경할 수 있을 것이다. 근데 왜 changeArray2라는 함수는 리턴값을 가지고 있을까?
-> 실제 java 기본 제공 메서드 중에도 이렇게 설계된 메서드가 있다. 이는 깊은 복사를 통해 해당 참조변수를 복사한 뒤 원본에는 변경을 가하지 않고, 사본을 만든 뒤 변경을 가하여 제공하는 것을 의도한 것이다. 대표적인 예시로 .sort()sorted() 메서드가 있다. 이렇게 자바에서는 리턴값의 유무에 따라서도 메서드의 목적을 짐작할 수 있다.

배열과 동시에 배열 크기만큼 인덱스가 모두 주어지는데, 각 인덱스는 자료형에 따라 기본값으로 초기화된다. boolean 타입은 false로, char\u0000(공백문자)로, 그 외에 numeric 변수들은 각 타입별로 0으로 초기화된다. 참조형 변수 배열의 경우에는 (기본형 데이터와 달리 참조형 변수는 null이 가능하므로) null로 초기화된다.

  • 정수 : 0
  • 실수 : 0.0
  • 문자 : /u0000 (공백)
  • 논리 : false
  • 참조 : null
  • 배열 객체를 생성하면 length라는 속성이 추가되고, 배열의 size를 의미한다.

배열 복사

배열을 복사하는 method는 두 가지 방법이 있다.

// Arrays.copyOf(int[] original, int newLength)
int[] scores = new int[]{1,2,3,4};
scores = Arrays.copyOf(scores, 5); // 크기가 5인 배열로 새로 생성하여 복사

// System.arrayCopy(Object src, int srcPos, Object dest, int destPos, int length)
int[] scores2 = new int[5]
System.arrayCopy(scores, 0, scores2, 0, scores.length);

---

/*  
* arrayCopy(Object src, int srcPos, Object dest, int destPos, int length)  
* src : 복사할 원본  
* srcPos : 원본의 복사할 위치  
* dest : 복사한 copy본  
* destPos : copy의 복사할 위치  
* length : 복사할 개수  
* */  

// 원본 완전 복사  
int[] arr = IntStream.rangeClosed(1, 7).toArray();  
int[] copy1 = new int[10];  
System.arraycopy(arr, 0, copy1, 0, arr.length);  
System.out.println("Arrays.toString(copy1) = " + Arrays.toString(copy1));  

// 원본 부분 복사  
int[] copy2 = new int[3];  
System.arraycopy(arr, 2, copy2, 0, copy2.length);  
System.out.println("Arrays.toString(copy2) = " + Arrays.toString(copy2));  

// 전체 복사  
int[] copy3 = Arrays.copyOf(arr, arr.length + 5);  
System.out.println("Arrays.toString(copy3) = " + Arrays.toString(copy3));  

// 부분 복사  
int[] copy4 = Arrays.copyOfRange(arr, 0, 3);  
System.out.println("Arrays.toString(copy4) = " + Arrays.toString(copy4));

주의할 점

1.

배열은 초기화할 때 크기를 알 수 있어야 한다. 처음부터 크기를 명확히 알 수 있게 대입해주면 컴파일러가 알아서 해석하여 문제없이 처리해주지만, 만약 변수명만 우선 선언한 뒤 나중에 초기화하려고 하는 경우에는 메모리 할당을 위해 new 연산자를 이용하여 '새로' 참조변수를 생성한다는 걸 명시해줘야 한다.

int[] scores;
scores = new int[]{1, 2, 3, 4}; // good
scores = {1,2,3} // compile error

int[] otherScores = {1,2,3,4}; // good

2.

자바의 배열 출력은 char[]을 제외하고는 해당 변수가 갖고 있는 주소값이 출력된다는 걸 알아야 한다. 하지만 char[]의 경우에는 내부적으로 BufferWriter를 이용하여 각 character를 조합하여 한번에 출력해줄 수 있도록 println()메서드가 오버로딩되어 있다.

int[] arr = {1, 2, 3, 4, 5};  
System.out.println(arr); // [I@7ef20235  
System.out.println(arr.toString()); // [I@7ef20235  

String org = "HELLO";  
char[] charArray = org.toCharArray();  
System.out.println(charArray); // HELLO  
System.out.println(charArray.toString()); // [C@7ef20235