자바[Java] - 배열[Array]

윤여준·2023년 7월 15일
0

자바[Java]

목록 보기
1/4

1. 배열이란?

배열이란 무엇일까?

자바 공식 문서 중 튜토리얼 문서를 살펴보자. 배열을 다음과 같이 설명하고 있다. (https://docs.oracle.com/javase/tutorial/java/nutsandbolts/arrays.html)

“An array is a container object that holds a fixed number of values of a single type.”

배열은 고정된 개수의 단일 타입 값들을 담고 있는 객체이다.

이때, 배열에 들어 있는 변수들을 배열의 요소(components, 컴포넌트)라고 한다.

한 배열의 모든 요소들은 같은 타입을 갖는데, 그 타입을 배열의 요소의 타입(component type)이라고 한다.

만약 배열의 component type이 T라면, 배열의 타입은 T[]라고 쓴다. 예를 들어서 배열의 component type이 int라면, 배열의 타입은 int[]라고 쓴다.

배열의 element type은 아무 타입이나 가능하다. 기본형(primitive)과 참조형(reference) 타입 모두 가능하다. 예를 들어서 interface 타입이나 class 타입도 배열의 요소가 될 수 있다.

2. 배열의 길이와 인덱스

우리가 평소에 변수를 사용할 때는 변수의 이름을 통해서 변수에 접근한다.

하지만 배열에 들어 있는 변수는 이름이 없다. 대신 음수가 아닌 정수 인덱스 값에 의해 참조된다.

만약 배열에 n개의 요소가 있다면 각 요소는 0에서 n-1까지의 정수 인덱스를 사용하여 참조된다.

또한 배열에 n개의 요소가 있다면 n을 배열의 길이라고 한다. 배열의 길이는 배열이 생성될 때 정해진다. 배열이 생성된 이후에 배열의 길이는 고정된다.

그렇다면 배열에 들어 있는 변수의 개수, 즉 배열의 길이가 0이 될 수 있을까?

Java spec 문서에 의하면, 배열의 길이는 0일 수 있으며, 이 경우를 비었다(empty)고 표현한다.

3. 배열이 메모리 상에서 연속적으로 위치하는가?

찾아보면서 신기했던 것은, 자바에서는 배열이 메모리 상에서 연속적으로 위치하지 않아도 된다는 것이었다. (https://stackoverflow.com/questions/10224888/java-are-1-d-arrays-always-contiguous-in-memory)

다른 언어들에서 배열은 메모리 상에서 연속적으로 위치하는 경우가 많다.

하지만 자바에서는 굳이 메모리 상에서 연속적으로 위치하지 않아도 된다고 한다.

자세한 내용은 위의 링크를 참고하면 된다.

간략하게 요약하면 다음과 같다.

  1. JLS에서는 배열이 객체라고 한다.
  2. 동시에 JVMS에서는 배열과 객체가 JVM의 힙에 저장된다고 한다.
  3. 그러나 JVMS는 힙 메모리가 연속적일 필요가 없다고 한다.
  4. 모든 배열이 힙에 저장되고 힙이 연속적이지 않을 수 있으므로 배열도 연속적이지 않을 수 있다.

4. 배열 변수 (Array Variables)

JLS에 따르면, 배열 타입의 변수는 객체에 대한 참조를 담고 있다. (A variable of array type holds a reference to an object.)

즉 배열 타입 변수는 배열 객체를 참조하는 변수이다. 배열 객체와 배열 타입 변수 (Array Variables)는 다른 것이다. 배열 타입 변수는 배열 객체의 주소를 저장하는 참조형 변수이다. 따라서 배열 타입 변수를 선언하는 것은 배열 객체를 생성하거나 배열 요소를 위한 공간을 할당하지 않는다. 오직 배열에 대한 참조를 담을 수 있는, 배열을 다룰 수 있는 변수 그 자체를 만든다.

배열 타입 변수와 배열 객체와의 관계를 그림으로 나타내면 위와 같다. 배열 타입 변수는 배열 객체의 주소를 담고 있을 수 있다.

배열 변수는 다음과 같이 선언할 수 있다.

// 자료형[] 변수명;
int[] arr1;
char[] arr2;
byte[][] arr3;

배열 요소의 타입과 대괄호 [], 변수명을 적어주면 된다.

1차원 배열은 대괄호 쌍 1개([]), 2차원 배열은 대괄호 쌍 2개([][]), n차원 배열은 대괄호 쌍 n개([][]…)를 적어주면 된다.

5. 배열의 생성

배열 객체는 array creation expression 또는 array initializer를 통해서 생성할 수 있다.

array creation expression을 이용한 배열 객체 생성은 다음과 같다. 이 방법을 이용해서 배열 객체를 생성하면 객체의 각 요소는 요소 타입(component type)의 기본값(default value)으로 초기화되고 객체의 길이는 사용자가 설정한 값이 된다.

int[] arr1 = new int[100];

array initializer를 이용한 배열 객체 생성은 다음과 같다. 이렇게 생성된 객체의 길이는 중괄호{} 안에 있는 값들의 개수와 같고, 객체의 요소는 주어진 값으로 초기화된다.

	int[] arr2 = {1,2,3,4};
// arr2가 참조하는 객체의 값: 1,2,3,4
// arr2가 참조하는 객체의 길이 : 4

아래 코드를 통해 배열 변수가 선언되고 배열 객체가 생성되는 과정을 그림으로 살펴보면 다음과 같다.

int[] arr;
arr = new int[5];

int형 배열 참조 변수 arr를 선언한다. 아직 배열 객체는 생성되지 않았다.

int[] arr;

연산자 ‘new’에 의해서 메모리의 빈 공간에 5개의 int형 데이터를 저장할 수 있는 공간이 마련된다. (배열이 생성된 메모리 주소가 0x200이라고 가정)

arr = new int[5];

그리고 각 배열 요소는 자동적으로 int의 기본값인 0으로 초기화된다.

끝으로 대입 연산자 “=”에 의해 배열의 주소가 int형 배열 참조 변수 arr에 저장된다.

이렇게 생성된 배열의 길이는 final instance variable인 length를 통해 접근 가능하다. 예를 들어,

int[] arr2 = {1,2,3,4};
System.out.println(arr2.length);
// 출력 결과 : 4

개발을 하다 보면 배열의 길이를 바꿔줄 때가 있기 때문에 배열의 길이를 숫자 그대로 가져가서 사용하기보다는 배열의 length 변수를 사용하는 게 좋다. 예를 들어 다음과 같다.

int[] arr = new int[5]; // 배열 arr의 길이 : 5

for (int i = 0; i < 5; i++) { // 배열 arr의 길이를 바꾸면 5를 다른 값으로 바꿔야 함
	System.out.println(arr[i]);
}
int[] arr = new int[5];

for (int i = 0; i < arr.length; i++) { // 배열 arr의 길이를 바꿔도 코드를 바꿔주지 않아도 됨
	System.out.println(arr[i]);
}

6. 배열의 접근

배열의 요소들은 인덱스와 대괄호[]를 이용해서 접근할 수 있다. 예를 들어 아래의 코드와 같다.

int arr1 = new int[10];
System.out.println(arr1[0]);
// arr1[0] : arr1의 0번째 요소

모든 배열의 인덱스는 0부터 시작한다. 만약 배열의 길이가 n이라면, 이 배열의 인덱스는 0부터 n-1까지의 정수로 이루어져 있다.

배열은 반드시 int 값으로 인덱싱 되어야 한다. 다만, short, byte, char 값은 인덱스 값으로 사용될 수 있다. 왜냐하면 이 값들은 unary numeric promotion으로 subjected 되어 int 값이 되기 때문이다.

long 값 인덱스로 배열 요소에 접근하려고 하면 compile-time 에러가 뜬다.

배열에 대한 모든 접근은 run time에 체크한다. 0보다 작거나 배열의 길이 이상의 인덱스를 사용해서 배열에 접근하면 ArrayIndexOutOfBoundsException 에러가 발생한다.

배열의 인덱스에 들어가는 값은 실행 시에 대입된다. 따라서 컴파일러는 이 값의 범위를 체크하지 못한다. 그렇기 때문에 인덱스의 범위를 신경써서 개발해야 한다.

7. Array Store Exception

배열 타입이 A[]이고 A가 참조형일 때, 배열 요소에 적절한 값이 할당되는지 런타임에 체크된다.

타입이 A[]인 배열(여기서 A는 참조형임)의 경우 런타임 시 배열의 구성 요소에 대한 할당을 검사하여 할당되는 값이 구성 요소에 할당 가능한지 확인한다.

할당되는 값의 타입이 배열에 할당할 수 없는 타입이면 ArrayStoreException이 발생한다.

JVM(Java Virtual Machine)은 할당이 유효한지 확인하기 위해 런타임 시 확인한다. 만약 할당이 유효하지 않은 경우 ArrayStoreException이 발생한다.

8. 배열의 초기화

배열은 생성과 동시에 자동적으로 자신의 타입에 해당하는 기본값으로 초기화되므로 사용하기 전에 따로 초기화를 해주지 않아도 된다.

하지만 원하는 값을 사용하려면 따로 각 요소마다 값을 지정해주어야 한다.

int[] arr = new int[5];
arr[0] = 50;
arr[1] = 60;
arr[2] = 70;
arr[3] = 80;
arr[4] = 90; 

위와 같이 직접 하나하나 값을 지정할 수 있지만 배열의 길이가 클 경우 이렇게 할 수 없다.

반복 작업을 줄일 수 있는 간단한 도구인 for문을 사용해서 위의 코드를 바꿔보면 다음과 같다.

int[] arr = new int[5];
for(int i = 0; i < arr.length; i++) {
	arr[i] = i * 10 + 50;
}

하지만 for문으로 배열을 초기화하는 방법은 일정한 규칙이 있는 값을 저장할 때만 가능하다.

자바에서는 일정한 규칙이 없을 때도 간단하게 배열을 초기화할 수 있는 방법을 제공한다.

int[] arr = new int[]{50, 60, 70, 80, 90}; // 배열의 생성과 초기화를 동시에 진행

저장할 값들을 중괄호{} 안에 쉼표로 구분해서 나열하면 되고, 괄호{} 안의 값의 개수에 의해 배열의 길이가 자동으로 결정되기 때문에 괄호[] 안에 배열의 길이는 안 적어도 된다.

int[] arr = new int[]{50, 60, 70, 80, 90};
int[] arr = {50, 60, 70, 80, 90}; // new int[]를 생략할 수 있음

다음과 같이 배열의 선언과 생성을 따로 하는 경우에는 new int를 생략할 수 없다.

int[] arr;
arr = new int[]{50, 60, 70, 80, 90};
// arr = {50, 60, 70, 80, 90}; -> 에러. new int[]를 생략할 수 없음

(선언과 생성을 동시에 하면 컴파일러가 타입을 추론해서 배열을 만드는데 선언과 생성을 따로 하면 컴파일러가 타입을 추론하지 못하기 때문인듯하다. 정확한 이유는 알아내지 못하였다.)

9. Array Members

배열 타입의 멤버들을 알아보자.

  • length 변수
    • public final field
    • 배열의 요소의 개수가 담겨있다.
    • length 변수는 음이 아닌 정수이다.
  • clone 메소드
    • public 메소드
    • Object 클래스의 clone 메소드를 오버라이드한 메소드이다.
      • Array 타입은 Object 클래스를 상속 받는다.
    • no checked exceptions를 던진다.
    • T[] 타입 배열의 clone 메소드를 사용하면 T[] 타입 값이 반환된다.
    • 다차원 배열의 clone은 얕은 복사이다.
      • 오직 하나의 새로운 배열을 만든다는 의미이다.
      • subarray들은 공유된다.
  • clone 메소드를 제외한 모든 모든 멤버들은 Object 클래스에서 상속된다.

10. char 배열과 String 타입

C언어 같은 언어에서는 char 배열과 String 타입이 같지만, 자바에서는 char 배열과 String 타입은 서로 다르다.

char배열에는 문자열을 다룰 수 있는 메소드들이 없지만, String 클래스에는 문자열을 다룰 수 있는 메소드들이 정의되어 있다.

String 클래스의 주요 메소드는 다음과 같다.

메서드설명
char charAt(int index)문자열에서 해당 위치(index)에 있는 문자를 반환한다.
int length()문자열의 길이를 반환한다.
String substring(int from, int to)문자열에서 해당 범위(from~to)에 있는 문자열을 반환한다. (to는 범위에 포함되지 않음)
boolean equals(Object obj)문자열의 내용이 obj와 같은지 확인한다. 같으면 결과는 true, 다르면 false가 된다.
char[] toCharArray()문자열을 문자배열(char[])로 변환해서 반환한다.

또한 String 객체는 immutable하기 때문에 내용물이 절대 바뀔 수 없는 반면, char 배열은 mutable 하기 때문에 내용물이 바뀔 수 있다.

char 배열과 String 타입 모두 문자열을 다룬다는 공통점이 있기 때문에 서로 변환할 수 있다.

new String과 toCharArray를 사용하면 된다.

char[] chArr = { 'A', 'B', 'C' };
String str = new String(chArr); // char 배열 -> String
char[] tmp = str.toCharArray(); // String -> char 배열

C언어 같은 것들과 달리, 자바에서는 문자열도 char 배열도 '\u0000'(NUL 문자)로 끝나지 않는다.

11. String 배열

배열 element의 타입은 아무 타입이나 가능하다. 그래서 배열 String 타입도 가능하다.

배열 element의 타입이 String인 경우에도 선언과 생성 방법은 int 배열의 선언과 생성방법과 다르지 않다. 예를 들어 3개의 문자열(String)을 담을 수 있는 배열을 생성하는 문장은 다음과 같다.

String[] name = new String[3];

위의 문장을 수행한 결과를 그림으로 표현하면 다음과 같다. 3개의 String 타입의 참조변수를 저장하기 위한 공간이 마련되고 참조형 변수의 기본값은 null이므로 각 요소의 값은 null로 초기화 된다. 여기서 null은 어떠한 객체도 가리키고 있지 않다는 뜻이다.

이어서 String 배열의 초기화 과정을 살펴보자. String 배열의 초기화 역시 int 배열과 동일한 방법으로 한다. 아래와 같이 배열의 각 요소에 문자열을 지정하면 된다.

String[] name = new String[3];
name[0] = "Kim";
name[1] = "Park";
name[2] = "Yi"; 

중괄호 {}를 사용해서 다음과 같이 간단히 초기화 할 수도 있다.

String[] name = new String[]{"Kim", "Park", "Yi"};
String[] name = { "Kim", "Park", "Yi" }; // new String[]을 생략할 수 있음

특별히 String 클래스만 “Kim”과 같이 큰 따옴표만으로 간략히 표현하는 것이 허용된다. 원래 String은 클래스이므로 new 연산자를 통해 객체를 생성해야한다.

String[] name = new String[2];
name[0] = new String["Kim"];
name[1] = "Park";

위의 코드를 그림으로 나타내면 다음과 같다. String 변수도 참조형 변수이기 때문에 String 배열의 요소에는 String 객체의 주소가 들어간다.

참고자료

Java Language Specification SE20 version (https://docs.oracle.com/javase/specs/jls/se20/html/index.html)
Java Virtual Machine Specification SE20 version (https://docs.oracle.com/javase/specs/jvms/se20/html/index.html)
Java Tutorials (https://docs.oracle.com/javase/tutorial/java/nutsandbolts/arrays.html)
자바의 정석

profile
Junior Backend Engineer

0개의 댓글