Java 길찾기/이것이 자바다

[Java] 참조 타입 (2)

Kindbeeeear_ 2022. 1. 3. 21:21

배열 타입

배열이란?

변수는 한 개의 데이터만 저장할 수 있다. 따라서 저장해야 할 데이터의 수가 많아지면 그만큼 많은 변수가 필요하다. 예를들어 학생 30명의 성적을 저장하고, 평균값을 구한다고 가정해보다. 먼저 학생 30명의 성적을 저장하기 위해 변수 30개를 선언해야 한다.

int score1 = 83;
int score2 = 90;
int score3 = 87;
...
int score30 = 75;

그리고 평점을 구하기 위해 변수들을 모두 더해야한다.

int sum = score1;
sum += score2;
sum += score3;
...
sum += score30;
int avg = sum / 30;

위와 같은 방법은 매우 비효율적이고 지루한 코딩이 된다. 만약 더 많은 학생들의 성적을 처리하게 된다면 코드는 끔찍해질것이다. 같은 타입의 많은 양의 데이터를 다루는 프로그램에서는 좀 더 효율적인 방법이 필요한데 이것이 배열이다. 배열을 같은 타입의 데이터를 연속된 공간에 나열시키고, 각 데이터에 인덱스(index)를 부여해 놓은 자료구조이다.

 

score 배열의 각 인덱스는 각항목의 데이터를 읽거나 저장하는데 사용되며 다음과 같이 배열이름 옆에 대괄호 [ ] 에 기입된다.

score[index]

 

예를들어 score[0]dms 83, score[1]은 90, score[2]는 87의 값을 가진다. 이렇게 성적을 배열로 만들면 성적의 평균값은 배열의 인덱스를 이용해서 for문으로 쉽게 구할 수 있다.

int sum = 0;
for(int i = 0; i < 30; i++){
	sum += score[i]
}
int avg = sum / 30;

for문이 30번 반복실행하면서 i가 0부터 29까지 변한다. 따라서 sum 변수에는 score[0] ~ score[29]까지 더해지고, 마지막으로 얻은 sum을 30으로 나누어 평균 avg를 얻는다.

 

배열은 같은 타입의 데이터만 저장할 수 있다. int 배열은 int 값만 저장 가능하고, String 배열은 문자열만 저장 가능하다. 배열은 선언과 동시에 저장할 수 있는 데이터 타입이 결정된다. 만약 다른 타입의 값을 저장하려고 하면 타입 불일치 컴파일 오류가 발생한다. 배열의 또 다른 특징은 한 번 생성된 배열은 길이를 늘리거나 줄일 수 없다. 3개의 값을 저장하는 배열을 생성했다고 가정하자. 프로그램 실행 도중에 5개의 값을 저장하는 배열로 수정할 수 없고, 반대로 2개의 값만 저장하는 배열로 수정할 수도 없다. 만약 5개의 값을 저장해야 하는 경우가 발생한다면, 길이 5의 새로운 배열을 생성하고, 기존 배열 항목을 새 배열로 복사해야한다.

 

배열선언

배열을 사용하기 위해서는 우선 배열 변수를 선언해야 한다. 배열 변수 선언을 다음과 같이 두 가지 형태로 작성할 수 있다.

// 1
타입[ ] 변수;

// 2
타입 변수[ ];

대괄호 [ ]는 배열 변수를 선언하는 기호로 사용되는데, 타입 뒤에 붙을 수도 있고 변수 뒤에 붙을 수도 있다. 타입은 배열에 저장될 데이터의 타입을 말한다. 다음은 각 타입별로 배열을 선언하는 예를 보여준다.

int[] intArray;
int intArray[];

double[] doubleArray;
double doubleArray;

String[] strArray;
String strArray[];

 

배열 변수는 참조 변수에 속한다. 배열도 객체이므로 힙 영역에 생성되고 배열 변수는 힙 영역의 배열 객체를 참조하게 된다. 참조할 배열 객체가 없다면 배열 변수는 null 값으로 초기화될 수 있다.

타입[ ] 변수 = null;

만약 배열 변수가 null 값을 가진 상태에서 변수[인덱스]로 값을 읽거나 저장하게 되면 NullPointerException이 발생한다. 배열 변수는 배열을 생성하고 참조하는 상태에서 값을 저장하거나 읽어야 한다.

 

값 목록으로 배열 생성

배열 항목에 저장될 값의 목록이 있다면, 다음과 같이 간단하게 배열 객체를 만들 수 있다.

데이터타입[] 변수 = { 값0, 값1, 값2, 값3, ...};

중괄호 { }는 주어진 값들을 항목으로 가지는 배열 객체를힙에 생성하고, 배열 객체의 번지를 리턴한다. 배열 변수는 리턴된 번지를 저장함으로써 참조가 이루어진다. 아래 예를 보자.

String[] names = { "홍길동", "김자바", "곰탱이"};

이렇게 생성된 배열에서 "홍길동"은 names[0], "김자바"는 names[1], "곰탱이"는 names[2]로 읽을 수 있다. names[1]의 "김자바"를 "박자바"로 바꾸고 싶다면 다음과 같이 대입 연산자를 사용하면 된다.

names[1] = "박자바";

 

값의 목록으로 배열 객체를 생성할 때 주의할 점이 있는데, 배열 변수를 이미 선언한 후에 다른 실행문에서 중괄호를 사용한 배열 생성은 허용되지 않는다.

타입[ ] 변수;
변수 = { 값0, 값1, 값2, 값3, ...}; // 컴파일 에러

 

배열 변수를 미리 선언한 후, 값 목록들이 나중에 결정되는 상황이라면 다음과 같이 new 연산자를 사용해서 값 목록을 지정해주면 된다. new 연산자 바로 뒤에는 배열 변수 선언에서 사용한 "타입[ ]"을 붙여주고 중괄호 { }에는 값들을 나열해주면 된다.

변수 = new 타입[ ] { 값0, 값1, 값2, 값3, ... };

예를 들어 배열 names를 다음과 같이 생성할 수 있다.

String[] names = null;
names = new String[] { "홍길동", "김자바", "곰탱이"};

 

메소드의 매개값이 배열일 경우에도 마찬가지이다. 아래와 같이 매개 변수로 int[ ] 배열이 선언된 add( ) 메소드가 있을 경우, 값 목록으로 배열을 생성함과 동시에 add( ) 메소드의 매개값으로 사용하고자 할 때는 반드시 new 연산자를 사용해야 한다.

int add(int[] scores) { ... }
------------------------------
int result = add( { 95, 85, 90} ); // 컴파일 에러
int result = add( new int[] { 95, 85, 90 } );

 

new 연산자로 배열 생성

값의 목록을 가지고 있지 않지만, 향후 값들을 저장할 배열을 미리 만들고 싶다면 new 연산자로 다음과 같이 배열 객체를 생성시킬 수 있다.

타입[ ] 변수 = new 타입[길이];

길이는 배열이 저장할 수 있는 값의 수를 말한다. new 연산자로 배열을 생성할 경우에는 이미 배열 변수가 선언된 후에도 가능하다.

타입[ ] 변수 = null;
변수 = new 타입[길이];

다음은 길이 5인 int[ ] 배열을 생성한다.

int[] intArray = new int[5];

 

new 연산자로 배열을 처음 생성할 경우, 배열은 자동적으로 기본값으로 초기화된다. 학생 30명의 점수를 저장할 배열을 다음과 같이 생성한다고 가정해보자.

int[] scores = new int[30];

scores 배열은 int 타입 배열이므로 scores[0] ~ scores[29]까지 모두 기본값 0으로 초기화된다.

만약 String 배열을 생성했다면 names[0] ~ names[29]까지 모두 null 값으로 초기화 된다.

String[] names = new String[30];

 

배열이 생성되고 나서 새로운 값을 저장하려면 대입 연산자를 사용하면 된다.

변수[인덱스] = 값;

예를 들어 배열 scores의 0, 1, 2 인덱스에 각각 83, 90, 75를 저장하는 코드는 다음과 같다.

in[] scores = new int[3];
scores[0] = 83;
scores[1] = 90;
scores[2] = 75;

 

배열 길이

배열의 길이란 배열에 저장할 수 있는 전체 항목 수를 말한다. 코드에서 배열의 길이를 얻으려면 다음과 같이 배열 객체의 length필드를 읽으면 된다. 참고로 필드는 객체 내부의 데이터를 말한다. 배열의 length 필드를 읽기 위해서는 배열 변수에 도트( . ) 연산자를 붙이고 length를 적어주면 된다.

배열변수.length;

다음은 배열 intArray의 길이를 알아보는 코드이다. 배열 intArray가 3개의 값을 가지고 있기 때문에 변수 num에는 3이 저장된다.

int[] intArray = { 10, 20, 30 };
int num = intArray.length;

length 필드는 읽기 전용 필드이기 때문에 값을 바꿀 수가없다. 그래서 다음과 같이 작성하면 안된다.

intArray.length = 10; // 잘못된 코드

 

배열의 length 필드는 for문을 사용해서 배열 전체를 루핑할 때 매우 유용하게 사용할 수도 있다.

public class ArrayLengthExample {
	public static void main(String[] args) {
		int[] scores = { 83, 90, 97 };
		
		int sum = 0;
		for(int i=0; i<scores.length; i++) {
			sum += scores[i];
		}
		System.out.println("총합 : " + sum);
		
		double avg = (double) sum / scores.length;
		System.out.println("평균 : " + avg);
	}
}

 

커맨드 라인 입력

우리는 이제 프로그램 실행을 위해 main() 메소드가 필요하다는 것을 알고 있다. 하지만 main() 메소드의 매개값인 String[] args가 왜 필요한 것인지 궁금하였을 것이다. 이제 이궁금증을 풀어보자.

public static void main(String[] args) { ... }

"java 클래스"로 프로그램을 실행하면 JVM은 길이가 0인 String 배열을 먼저 생성하고 main() 메소드를 호출할 때 매개값으로 전달한다. 만약 다음과 같이 "java 클래스" 뒤에 공백으로 구분된 문자열 목록을 주고 실행하면, 문자열 목록으로 구성된 String[] 배열이 생성되고 main() 메소드를 호출할 때 매개값으로 전달된다.

java 클래스 문자열0 문자열1 문자열2 ... 문자열n-1

main() 메소드는 String[] args 매개변수를 통해서 커맨드 라인에서 입력된 데이터의 수(배열의 길이)와 입력된 데이터(배열의 항목 값)을 알 수 있게 된다. 다음 예제는 프로그램을 실행할 때 2개의 문자열을 주지 않으면 프로그램의 사용법을 출력하고 강제 종료하도록 하였다. 만약 프로그램을 실행할 때 2개의 문자열이 정확히 입력되었다면 2개의 문자열을 int 타입 숫자로 변환하고 덧셈 연산을 수행한다.

public class MainStringArrayArgument {
	public static void main(String[] args) {
		if(args.length != 2) {
			System.out.println("프로그램의 사용법");
			System.out.println("java MainStringArrayArgument num1 num2");
			System.exit(0);
		}
		
		String strNum1 = args[0];
		String strNum2 = args[1];
		
		int num1 = Integer.parseInt(strNum1);
		int num2 = Integer.parseInt(strNum2);
		
		int result = num1 + num2;
		System.out.println(num1 + " + " + num2 + " = " + result);
	}
}

위 예제를 그냥 실행하면

 

프로그램의 사용법

java MainStringArrayArgument num1 num2

 

와 같은 결과를 얻는다. 그 이유는 실행할 때 매개값을 주지 않았기 때문에 길이 0인 String 배열이 매개값으로 전달된다. 따라서 args.length는 0이므로 3라인의  if 조건식이 true가 되어 if문의 블록이 실행된 것이다. 이클립스 에서 프로그램을 실행할 때 매개값을 주고 실행하려면 메뉴에서 [Run -> Run Configurations...]를 선택하면 된다.

[Run Configurations] 대화상자의 [Main] 탭에서 [Project]와 [Main Class]를 확인한다. [Arguments] 탭을 클릭하고 [Program arguments] 입력란에 10을 입력한 후 빈 칸을 띄우고 다시 20을 입력한다. 그리고 [Run] 버튼을 클릭한다. 이것은 명령 프롬프트에서 다음과 같이 실행하는 것과 동일하다.

java MainStringArrayArgument 10 20

이렇게 실행하면 args는 { "10", "20" } 배열을 참조하게 되고 args[0]는 "10", args[1]은 "20"을 얻을 수 있다. 문자열은 산술 연산을 할 수 없기 때문에 이 문자열들을 Integer.parseInt() 메소드를 이용해서 정수로 변환시킨다.

만약 정수로 변환할 수 없는 문자열이 주어졌을 경우에는 NumberFormatException 실행 예뢰가 발생한다.

 

다차원 배열

지금까지 살펴본 배열은 값 목록으로 구성된 1차원 배열이다. 이와는 달리 값들이 행과 열로서 구성된 배열을 2차원 배열이라고 한다. 2차원 배열은 수학의 행렬을 떠올리면 되는데, 가로 인덱스와 세로 인덱스를 사용한다. 자바는 2차원 배열을 중첩 배열 방식으로 구현한다. 예를 들어 2(행) x 3(열) 행렬을 만들기 위해 다음과 같은 코드를 사용한다.

int[][] scores = new int[2][3];

배열 변수인 scores는 길이 2인 배열A를 참조한다. 배열 A의 scores[0]은 다시 길이 3인 배열을 참조한다. 그리고 scores[1] 역시 길이 3인 배열을 참조한다. scores[0]과 scores[1] 이 참조하는 길이 3인 배열은 서로 다른 배열이다. 각 배열의 길이는 다음과 같이 얻을 수 있다.

scores.length    // 2
scores[0].length // 3
scores[1].length // 3

 생성 원리는 수학 행렬과는 근본적으로 다르지만 사용 방식은 동일하다. scores[0][1]은 수학 행렬에서의 (0, 1) 값이라고 볼 수 있다. 마찬가지로 scores[1][0]은 수학 행렬에서의 (1, 0) 값이다. 자바는 일차원 배열이 서로 연결된 구조로 다차원 배열을 구현하기 때문에 수학 행렬 구조가 아닌 계단식 구조를 가질 수 있다. 다음 코드를 보자.

int[][] scores = new int[2][];
scores[0] = new int[2];
scores[1] = new int[3];

이 경우 배열 항목의 수를 조사해보면 다음과 같다.

scores.length    // 2
scores[0].length // 2
scores[1].length // 3

이런 형태의 배열에서 주의할 점은 정확한 배열의 길이를 알고 인덱스를 사용해야 한다. scores[0][2]는 ArrayIndexOutOfBoundsException을 발생시킨다. scores[0]의 마지막 인덱스는 1이기 때문이다.

 

만약 그룹화된 값 목록을 가지고 있다면 다음과 같이 중괄호 안에 다시 중괄호를 사용해서 값 목록을 나열하면 된다.

타입[][] 변수 = { {값1, 값2, ...}, {값1, 값2, ...}, ...};

예를 들어 그룹화된 성적 점수를 이용해서 다음과 같은 배열을 만들 수 있다. scores[0]에는 { 95, 80 } 배열을 참조하고, scores[1]에는 { 92, 96 } 배열을 참조한다. 

int[][] scores = { {95, 80}, {92, 96} };

위 코드로 생성된 scores 배열에서 항목 값을 다음과 같이 읽을 수 있다.

int score = scores[0][0]; // 95
int score = scores[1][1]; // 96

 

객체를 참조하는 배열

기본타입(byte, char, short, int, long, float, double, boolean) 배열은 각 항목에 직접 값을 갖고 있지만 ,참조타입(클래스, 인터페이서) 배열은 각 항목에 객체의 번지를 가지고 있다. 예를 들어 String은 클래스 타입이므로 String[] 배열은 각 항목에 문자열이 아니라, String 객체의 주소를 가지고 있다. 즉 String 객체를 참조하게 된다.

String[] strArray = new String[3];
strArray[0] = "Java";
strArray[1] = "C++";
strArray[2] = "C#";

위 코드는 배열 변수 strArray를 선언하고 3개의 문자열을 참조하는 배열을 생성한다. strArray[0], strArray[1], strArray[2] 모두 String 객체이므로 String[] 배열의 항목도 결국 String 변수와 동일하게 취급되어야 한다. 예를들어 String[] 배열 항목 간에 문자열을 비교하기 위해서는 == 연산자 대신 equals() 메소드를 사용해야 한다. == 는 객체의 번지 비교이기 때문에 문자열 비교에 사용할 수 없다.

String[] strArray = new String[3];
strArray[0] = "Java";
strArray[1] = "Java";
strArray[2] = new String("Java");

System.out.println( strArray[0] == strArray[1] );      // true (같은 객체 참조)
System.out.println( strArray[0] == strArray[2] );      // false (다른 객체 참조)
System.out.println( strArray[0].equals(strArray[2]) ); // true (문자열이 동일)

 

배열 복사

배열은 한 번 생성하면 크기를 변경할 수 없기 때문에 더 많은 저장 공간이 필요하다면 보다 큰 배열을 새로 만들고 이전 배열로부터 항목 값들을 복사해야 한다. 배열 간의 항목 값들을 복사하려면 for문을 사용하거나 Ststem.arraycopy() 메소드를 사용하면 된다. for문으로 배열을 복사하는 코드는 다음과 같다.

public class ArrayCopyByForExample {
	public static void main(String[] args) {
		int[] oldIntArray = { 1, 2, 3 };
		int[] newIntArray = new int[5];
		
		for(int i = 0; i < oldIntArray.length; i++) {
			newIntArray[i] = oldIntArray[i];
		}
		
		for(int i = 0; i < newIntArray.length; i++) {
			System.out.print(newIntArray[i] + ", ");
		}
	}
}

만약 복사되지 않은 항목이 존재한다면 int[] 배열의 기본 초기값 0이 그대로 유지된다.

 

이번에는 System.arraycopy() 메소드를 이용해서 복사해보자. System.arraycopy() 를 호출하는 방법은 다음과 같다.

System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

src 매개값은 원본 배열이고, srcPos는 원본 배열에서 복사할 항목의 시작 인덱스이다. dest 매개값은 새 배열이고, destPos는 새 배열에서 붙여넣을 시작 인덱스이다. 마지막으로 length는 복사할 개수이다. 예를 들어 원본 배열이 arr1이고 새 배열이 arr2일 경우 arr1의 모든 항목을 arr2에 복사하려면 다음과 같이 System.arraycopy() 메소드를 호출하면 된다.

System.arraycopy(arr1, 0, arr2, 0, arr1.length);
public class ArrayCopyByForExample {
	public static void main(String[] args) {
		String[] oldStrArray = { "java", "array", "copy" };
		String[] newStrArray = new String[5];
		
		System.arraycopy( oldStrArray, 0, newStrArray, 0, oldStrArray.length);
		
		for(int i = 0; i < newIntArray.length; i++) {
			System.out.print(newIntArray[i] + ", ");
		}
	}
}

참조 타입 배열일 경우, 배열 복사가 되면 복사되는 값이 객체의 번지이므로 새 배열의 항목은 이전 배열의 항목이 참조하는 객체와 동일하다. 이것을 얕은 복사라고 한다. 반대로 깊은 복사는 참조하는 객체도 별도로 생성하는 것을 말한다.

 

향상된 for문

자바 5부터 배열 및 컬렉션 객체를 좀 더 쉽게 처리할 목적으로 향상된 for문을 제공한다. 여기서 컬렉션 객체는 나중에 배울 예정이다. 향상된 for문은 반복 실행을 하기 위해 카운터 변수와 증감식을 사용하지 않는다. 배열 및 컬렉션 항목의 개수만큼 반복하고 자동적으로 for문을 빠져나간다. 다음은 향상된 for문을 작성하는 형식과 실행 흐름을 보여준다.

향상된 for문 실행 흐름

for문의 괄호( ) 에는 배열에서 꺼낸 항목을 저장할 변수 선언과 콜론( : ) 그리고 배열을 나란히 작성한다. for문이 처음 실행될 때 "배열"에서 가져올 첫 번째 값이 존재하는지 평가한다. 가져올 값이 존재하면 해당 값을 "변수"에 저장한다. 그리고 "실행문"을 실행한다. 블록 내부의 "실행문"이 모두 실행되면 다시 루프를 돌아 "배열"에서 가져올 다음 값이 존재하는지 평가한다. 만약 다음 항목이 존재하면 2 -> 3 -> 1 로 다시 진행하고, 다음 항목이 없으면 for문이 종료된다. 따라서 for문의 반복 횟수는 배열의 항목 수가 된다.

public class AdvancedForExample {
	public static void main(String[] args) {
		int[] scores = { 95, 71, 84, 93, 87 };
		
		int sum = 0;
		for (int score : scores) {
			sum = sum + score;
		}
		System.out.println("점수 총합 = " + sum);
		
		double avg = (double) sum / scores.length;
		System.out.println("점수 평균 = " + avg);
	}
}