Ch05. 배열(array)

ho_c·2023년 4월 2일
0

자바의 정석 3판

목록 보기
4/8
post-thumbnail

들어가는 말

  • 변수는 저장공간이고, 각 저장방식에 따라서 자료형이 존재했고, 변수 하나에는 정해진 크기 안에서 하나의 값만 저장할 수 있었다.

    만약 같은 타입의 저장공간이 10개가 필요하다면?

  • 이를 위해 변수 10개를 선언하면 되겠지만, 사용할 변수가 100개만 넘어가도 하나, 하나 값을 정하고, 반대로 수정하면 100개 중의 하나를 찾아야 한다.

  • 곧 변수가 늘어날수록 코드의 가독성도, 유지 보수성도 떨어진다.
    이 때문에 같은 타입의 변수를 여러 개를 묶어서 다른 하나의 변수에 연결해 쓰는 것이 바로 “배열”이다.
    추가로 변숫값의 입력도 반복 작업이라 반복문이나 조건문을 통해 이 배열을 관리할 수 있다.


1. 배열

  • 같은 타입의 여러 변수를 하나의 묶음으로 다루는 것
    앞서 말했듯 ‘같은 타입’의 변수를 여러 번 선언하는 건 손해다. 다루기도 어렵고 그래서 배열을 통해서 ‘인덱스’‘길이’로 데이터를 다룰 수 있다.

  • 배열 안의 데이터들은 저장공간이 연속적으로 배치된다. 곧 순서가 존재한다.

  • 애초에 배열은 포인터(메모리 주소를 저장하는 변수)로 구현이 된다. 따라서 배열을 저장하는 변수 자체는 c에서 말하는 포인터 변수이다.

  • 다만 자바는 사용자가 임의의 주소를 다룰 수 없게 했기 때문에(‘참조 변수’로만 주소를 다룰 수 있음.) 포인터 변수를 사용할 수는 없다.


배열의 선언과 생성

배열 선언

  • 타입의 변수를 선언하고 []를 붙인다.
  • [n]가 가리키는 값은 *(a+n)와 같은 의미이다.
// 타입[] 변수명
int[] score;

// 타입 변수명[]
int score[];

배열의 생성

  • 선언된 건 배열의 주소를 저장할 공간이다. 실제 배열은 메모리 상에 생성되어야 한다.
    따라서 new 연산자와 함께 배열의 타입과 길이를 지정해야한다.
int[] score;
score = new int[5]; // 길이가 5인 배열 생성

  • 위 표에서 보듯, new에 의해 배열이 생성되면 int타입 데이터를 저장할 공간이 만들어진다.
  • 저장공간은 int의 기본값 0으로 자동 초기화된다.
  • score는 배열의 주소를 저장하는 ‘참조변수’이며, 이 변수로 배열에 접근할 수 있다.

배열의 길이와 인덱스

인덱스(Index)

  • 배열의 저장공간 자체는 ‘요소’라고 하며, 그 요소에 붙은 주소를 ‘인덱스’라고 한다.
  • 인덱스는 ‘0’부터 시작한다. 따라서 범위는 '배열 길이-1'이다.
  • 따라서 배열을 다룰 때, 인덱스는 해당 요소 공간에 접근하는 ‘포인터 변수의 역참조’이다.
  • 즉, 선언된 변수 자체는 배열의 시작 주소를 저장한다.
    그리고 변수는 증감이 가능한데, 자료형에 따라 같은 +1이라 할지라도 주소의 관점에선 자료형의 크기에 따라 다음 인덱스가 가리키는 값의 시작 주소로 넘어간다.
  • 그 이유는 주소의 중간값을 저장해도 별 쓸모가 없기 때문이다. 고로 a[0]은 *(a+0)과 같은 연산이다.
int[] score = new int[50]; // 길이 : 50, 인덱스 : 0~49
score[48] = 49; // 49번째 요소에 49를 저장한다.

  • 배열의 인덱스는 상수 외에도 변수, 수식으로도 구현이 가능하다.
    그래서 for문의 변수 I를 주로 인덱스로 사용한다.
int[] score =  new int[10];

// 변수로 값 저장하기
for(int i=0; i<10; i++){
	score[i] = i * 2;
}
// score[] = {0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20};


// 수식으로 값 꺼내오기
System.out.print(“출력 :);
for(int i=0; i<5; i++){
	System.out.printf(%d ”, score[i*2]);
}
// 출력 : 0 4 12 16
  • 단, 수식이나 변수 사용 시엔 인덱스의 범위가 벗어난 값이 사용되면 안 된다.
    넘어가면 실행 시 에러가 발생한다. ArrayIndexOutOfBoundsException (컴파일러에서 걸러지지 않음)
  • 그래서 반복문의 시작도 0으로 시작해서, 배열의 길이보다 작게 끝낸다.

길이(Length)

  • 배열 생성시, []안에 들어가는게 길이, 곧 '요소의 개수'이다.
  • 길이는 0과 양의 정수만 가능하며, int의 최대값 약 20억(10자리)까지 가능하다.
  • 물론 길이가 0인 배열도 만들 수 있다.
int[] arr1 = new int[100000]; // 길이가 100000인 배열
int[] arr2 = new int[0]; // 길이가 0인 배열

배열명.length

  • JVM은 모든 배열의 길이를 별도 관리한다.
  • 따라서 .length를 통해 배열의 길이를 쉽게 구할 수 있다.
  • 단, JVM으로부터 오는 배열의 길이는 ‘상수’이다.
int[] arr1 = new int[5];
int[] arr2 = new int[5];

// arr1
for(int i=0; i<5; i++){ // arr1의 길이가 바뀌면 같이 바뀌어야 한다.
	score[i] = i * 2;
}

// arr2
for(int i=0; i<arr2.length; i++){ // arr2의 길이가 바껴도 건들지 않아도 된다.
	score[i] = i * 2;
}
  • for문으로 배열을 다루면, 반복횟수는 주로 ‘0≤ i 〈배열의 길이’의 범위이다.
  • 이를 .length를 사용하면 배열의 길이가 변해도, 반복문은 건들지 않는다.
  • 프로그램이 실행되는 동안엔 한번 선언된 배열의 길이를 변경하는 건 불가능하다.
  • 만약 공간이 부족할 시, 길이를 늘이려면 더 큰 배열에 기존 배열의 값을 복사하는 코드를 추가해둬야 한다.

배열의 초기화

  • 배열은 생성 시 타입의 기본값으로 자동 초기화된다. 그래서 원하는 값은 요소마다 직접 정해줘야 한다.
  • 규칙이 있는 값이면 반복문을 사용하면 되지만, 그렇지 않다면 생성과 초기화를 동시에 할 수 있다.
int[] score = new int[]{10, 12, 18, 15, 60}; // 길이가 5인 배열

// new int[]는 생략이 가능하다.
int[] score = {10, 12, 18, 15, 60}; // new int[] 생략 가능.

// 생략은 선언과 초기화가 동시에 할 때, 가능하다.
int[] score; 
score = new int[]{10, 12, 18, 15, 60}; 
score = {10, 12, 18, 15, 60}; // 에러, new int[] 생략 불가.

// 메서드의 인자값일 때도 new int[] 생략 불가
int result = add(new int[]{1, 2, 3});
int result = add({1, 2, 3}); // 에러.
  • {} 안에 아무것도 넣지 않으면 길이가 0인 배열이 된다.
int[] score = new int[0];
int[] score = new int[]{};
int[] score = [];

배열의 출력

  • 출력 역시도 간단하다. 반복문으로 인덱스마다 출력해주면 된다.
  • 한 번에 보고 싶다면 Array.toString(배열명)메서드를 사용하자.
int[] score = {1, 2, 3, 4, 5};
System.out.println(“출력 :+ Arrays.toString(score)); 
// 출력 : {1, 2, 3, 4, 5}
  • 여기서 score를 그대로 출력하면, 배열이 아닌 배열의 주소가 16진수로 나온다.
  • 물론 예외로 char배열은 그 자체가 String이기 때문에 순서대로 나온다.
char[] cArr = {A,B,C};
System.out.println(cArr);
// ABC 

배열의 복사

  • 한번 생성 된 배열의 길이를 변경하는 것은 어렵다. 따라서 더 큰 배열을 만들어서 요소들을 복사해야 한다.
  • 복사 방식은 반복문 사용, System.arraycopy() 사용. 이렇게 두가지 방식이 있다.

(1) for 반복문 사용

int[] arr1 = new int[10]; // 기존 배열
int[] arr2 = new int[arr1.length * 2]; // arr1보다 길이가 2배인 배열


// 반복문으로 옮겨담기
for(int j = 0; j<arr1.length; j++){
	arr2[j] = arr1[j]; 
}

// 참조변수의 주소를 옮기기
arr1 = arr2;
  • 배열에 저장된 값을 옮겨 담고, 가리키는 배열의 주소를 바꿔준 것이다.
  • ‘arr1’ 변수와 연결이 끊긴 배열은 JVM의 가비지 컬렉터가 메모리에서 자동으로 지운다.

(2) System.arraycopy()

  • 반복문과 달리 한번에 값을 복사한다.
  • 요소마다 하나씩 접근하는 반복문보다 효율적인데, 이는 요소의 연속적 나열을 응용해서 처리하는 방식이다.
  • 배열의 범위를 벗어날 경우, 에러가 발생한다.
int[] arr1 = new int[10]; // 기존 배열
int[] arr2 = new int[arr1.length * 2]; // arr1보다 길이가 2배인 배열

System.arraycopy(arr1, 0, arr2, 0, arr1.length);
/* arr1의 인덱스 0포함 arr1.length개의 데이터를 
   인덱스 arr2의 인덱스 0부터 복사해서 저장해라.
*/

배열의 활용

  • 배열을 이용해서 실습을 진행해봤다. 대략 7개가 수록되어 있지만 적당하게 간단한 것만 진행해 봤다.

(1) 최대값, 최소값

// score 안의 수는 0~1000사이의 수다.
int[] score = {100, 21, 657, 15, 132, 544, 221, 687, 54};

// 사실 인덱스 0으로 초기화 시키는게 낫다.
int max = -1; 
int min = 1001;

for(int i=0; i<score.length; i++){
	if(score[i]>max){
		max = score[i];
	}
	if(score[i]<min){
		min = score[i];
	}
}
System.out.println("최대값 :" + max); // 최대값 : 687
System.out.println("최소값 :" + min); // 최소값 : 15
  • 배열 안의 값을 모르거나, 입력을 받아서 유동적일 때는 저렇게 최소값과 최대값을 설정하면 비교가 가능하다.

(2) 로또 번호 뽑기

int[] ball new int[45]; // 1~45를 저장할 배열

// 배열 초기화 1~45
for(int i=0; i < ball.length; i++){
	ball[i] = i+1;
}

int temp = 0; // 백업 공간
int j = 0; // 임의 인덱스 값을 저장할 변수

// 섞기
for(int i=0; i < 6; i++) {
	j = (int)(Math.random()* 45); // 인덱스는 0~44이다.
	temp = ball[i];
	ball[i] = ball[j];
	ball[j] = temp;
}

// 번호 출력
for(int i=0; i < 6; i++){
	System.out.print(ball[i] + “ ”);
}
  • 반복횟수를 높여서 섞는 방식도 있지만, 그것 역시 특정 위치의 인덱스만 뽑는 것이다.
  • 따라서 Math.random() 으로 인덱스 값만 뽑아서 교환한 다음 원하는 개수만 뽑아도 섞여있다.

(3) 버블 정렬

int[] numArr =  {1, 4, 2, 6, 7, 8, 12, 34};

// 총 반복(회차)
for(int i=0; i<numArr.length-1; i++){

	// 요소간 비교
	for (int j=0;j<numArr.length-1-i; j++) {
		if(numArr[j] > numArr[j+1]) { // 왼쪽의 값이 크면 서로 바꾼다.
			int tmp = numArr[j];
			numArr[j] = numArr[j+1];
			numArr[j+1] = tmp;
		}
	}
}
  • 버블 정렬 알고리즘은 요소의 값을 크기에 따라서 정렬한다. 단, 한번에 모두 정렬되는 것이 아니다.
  • 총 정렬해야 하는 수(반복횟수)는 ‘길이-1’로, 마지막 1개는 자동으로 정렬되기 때문이다.
  • 안쪽 반복은 첫 인덱스(0)부터 마지막 인덱스(길이-1)까지의 요소에 접근해서 2개를 동시 비교한다.
  • 비교 시, 왼쪽 값이 우측 값보다 크면 서로 교환한다.
  • 결과적으로 한번 반복할 때마다 하나의 값은 정렬이 끝난다. 따라서 비교 대상도 한 회차마다 한 개씩 줄어든다. ‘길이-1 – 회차수'
  • 요소가 비교가 길이-1인 이유는 두 값을 동시 비교하기 때문이다.

○ 요약

  • 배열은 같은 타입의 변수를 하나로 묶어둔 것이다.
  • 배열을 가리키는 참조변수에는 메모리 상에 있는 변수의 주소가 저장된다.
  • 요소는 배열을 구성하며, 각 요소에는 순서(인덱스)가 있다. 인덱스는 0부터 시작한다.
  • 배열 생성 시 타입의 기본값으로 자동 초기화된다..
  • 배열의 선언과 메서드 호출로 사용시에는 new를 생략할 수 없다.
  • char배열은 출력하면 문자열로 나온다.
  • 동적으로 한번 생성된 배열의 길이는 바꿀 수 없다. 컬렉션을 쓰거나 아니면 더 큰 배열을 만들어서 복사해줘야 한다.
  • 반복문만 있으면 배열로 정렬, 섞기 등 다양한 것을 할 수 있다.

2. String배열


String배열의 선언과 생성

  • String은 char의 배열이다. 거기에 여러 기능을 추가한게 String 클래스인데, String타입 역시 배열로 다룰 수 있다.
String[] name = new String[3];
name[0] = “kim”;
name[1] = “park”;
name[2] =Lee;
  • 이렇게 되면 3개의 문자열이 저장되는 문자열 배열이 만들어진다.
  • 배열의 배열로서, 각 요소에는 문자열의 주소가 저장된다. (객체 배열)


  • 참조변수의 기본값은 null이므로, null로 초기화 된다.


초기화

  • 초기화 역시, 일반 배열이랑 별반 다르지 않다.
String[] name1 = new String[]{“kim”, “park”,Lee};
String[] name2 = {“kim”, “park”,Lee}
String[] name3 = new String[3];
name3[0] = “kim”;
name3[1] = “park”;
name3[2] =Lee;

char배열과 String클래스

  • 문자열은 char의 배열이다. 곧 문자의 배열이다. 하지만 이게 JAVA에서 String은 아니다.
  • 자바의 String은 문자배열에 이를 다루는 다양한 기능을 묶어놓은 “클래스”이다.
  • 이 둘의 차이는 char배열은 내용 수정이 가능하지만, String클래스는 변경이 불가능하다는 것이다.
  • 즉, 배열의 요소값을 바꾸는 것과 객체에 연결된 주소를 바꾸는 건 서로 다른 문제다.

String클래스의 주요 메서드


번외. 커맨드 라인을 통해 입력받기

  • Scanner 클래스 안쓰고도 커맨드라인으로 값을 입력 받을 수 있다.
  • 커맨드라인으로 입력 시, main 메서드의 인자값으로 들어간다.
  • 공백이 있는 단어는 “” 으로 묶어준다.
  • 입력한 문자열만큼 배열이 생성되어 다룰 수 있다.
C:\jdk1.8\work\ch5>java ArrayEx16 abc 123 "Hello world"
public static void main(String[] args){
		System.out.println("매개변수의 개수 :"+args.length);
		for(int i=0;i< args.length;i++){
			System.out.println("args[" + i + "] = \""+ args[i] + "\"");
		}
}

/* 출력
매개변수의 개수 : 3
args[0] = "abc"
args[1] "123"
args[2] = "Hello world"
*/

3. 다차원 배열

  • 배열은 1차원뿐만 아니라, 앞선 String배열처럼 “배열의 배열” 형태로 다룰 수 있다.
  • 이를 다차원 배열이라고 하며 이번 파트에선 “2차원 배열”을 주로 사용한 것이다.

2차원 배열의 선언과 인덱스

2차원 배열 선언

  • 선언방법은 1차원처럼 배열표시“[]”가 하나 더 들어간다.
int[][] score; // 타입[][] 변수명
int score[][]; // 타입 변수명[][]
int[] score[]; // 타입[] 변수명[] 
  • 변수가 선언되었다면, 실제 배열생성은 다음과 같다.
int[][] score =  new int[4][3] // 4(행-세로) X 3(열-가로) 배열 생성
  • 구조를 보면 다음과 같고, 생성과 함께 int의 기본값 0으로 초기화 된다.


2차원 배열의 인덱스

  • 이번에는 배열의 요소에 접근 방법을 알아보자.
  • 일단 2차워 배열 역시, 인덱스는 0~길이-1이까지의 범위이다.
  • 따라서 앞서 생성한 score[][]으로 접근을 하면 다음과 같다.

  • 위 사진처럼 2차원을 배열은 배열 안에 배열을 연결한 방식으로 구현이 되어져 있다.
  • 여기서 데이터를 추출하거나, 변경하고 싶다면 해당 인덱스를 따라서 변경하면 된다.

2차원 배열의 초기화

  • 2차원 배열도 생성과 동시에 원하는 값으로 초기화가 가능하다.
int[][] score = {
		(100, 100, 100},
		(20, 20, 20},
		(30, 30, 30),
		(40, 40, 40),
		(50, 50, 50)
	};

  • 배열의 배열이기 때문에, score.length == 5 이다. 즉, score은 배열 1개의 주소를 갖고 있다.

  • 그리고 해당 배열의 각 요소는 또 다른 배열의 주소이므로 ‘배열 주소의 배열’이다.

  • 즉, score[?] 자체가 배열 하나의 참조변수라고 이해하는 것이 편하다.
    따라서 score[0].length == 3이 된다.

  • 괄호 외에도 이중 반복문으로 2차원 배열의 초기화가 가능하다.

for(int x=0; x<score.length; x++){
	for(int y=0; y<score[i].length; y++){
		score[x][y] = 0; // 모든 요소를 0으로 초기화
	}
}
  • 향상된 for문으로 다차원 배열을 처리 시에는 값의 변경은 불가능하다. 또한 score의 각 요소는 int[] 배열의 주소를 타입으로 가지기 때문에 int타입으로 사용할 시, 에러가 일어난다.
for(int[] tmp : score){ // score의 각 요소는 int[]배열을 가리킨다.
	for(int i: tmp){ // tmp의 각 요소는 int이다.
		System.out.println(i);
	}
}

가변 배열

  • 배열에 연결된 다른 배열들의 길이가 모두 동일할 필요가 없다. 즉, 사각형의 형태를 가진 배열이 아니어도 충분히 다룰 수 있다.
  • 이를 가변 배열이라고 하는데, 가변 배열은 두 번째 차원의 길이를 지정하지 않으면 된다.
int[][] arr = new int[5][]; // 두 번째 차원의 길이는 지정하지 않았다.

// arr의 요소마다 길이가 다른 배열을 생성해서 연결해준다.
arr[0] = new int[4];
arr[1] = new int[1];
arr[2] = new int[3];
arr[3] = new int[2];
arr[4] = new int[3];

  • 배열마다 길이가 다르더라도, 초기화는 {} 을 이용해서 생성과 함께 할 수 있다.
int[][] score = {
			{100, 100, 100, 100},
			{20, 20, 20},
			{40, 40},
			{50, 50, 50}
		};

○ 요약

  • “배열의 배열”의 형태로 다차원 형태의 배열을 다룰 수 있다.
  • 다차원 배열을 사용할 시, 배열의 요소는 다른 “배열의 주소”이다.
  • 배열의 생성방법은 1차원과 다르지 않다. 그렇기 new의 생략도 마찬가지이다.
  • 다차원 배열의 초기화 역시, {}을 이용해서 가능하다.


도움이 되셨다면 '좋아요' 부탁드립니다 :)

profile
기록을 쌓아갑니다.

0개의 댓글