자바의 데이터 타입은 크게 기본 타입과 참조 타입으로 분류된다. 기본 타입은 2장에서 다뤘고 이번장에서는 참조 타입을 다룰 예정이다. 참조 타입이란 객체의 번지(주소)를 참조하는 타입으로 배열, 열거, 클래스, 인터페이스 타입이 있다.
다음은 자바의 데이터 타입 분류를 도식화한 것이다.
기본 타입으로 선언된 변수와 참조 타입으로 선언된 변수의 차이점은 저장되는 값이다.
기본 타입으로 선언된 변수는 값 자체를 저장하고 있지만, 참조 타입으로 선언된 변수는 객체가 생성된 메모리 번지(주소)를 저장한다.
다음은 기본 타입인 int와 double로 선언된 변수 age와 price에 값을 대입하고, 참조 타입인 String 클래스로 선언된 변수 name과 hobby에 문자열을 대입하는 예제이다.
// 기본 타입 변수
int age = 25;
double price = 100.5;
// 참조 타입 변수
String name = "신용권";
String hobby = "독서";
메모리상에서 이 변수들이 갖는 값을 그림으로 표현하면 다음과 같다.
변수들은 모두 스택(Stack)이라는 메모리 영역
에 생성된다.
기본 타입 변수인 age와 price는 직접 값을 저장하고 있지만,
참조 타입 변수인 name과 hobby는 힙(Heap) 메모리 영역의 String 객체 주소를 저장하고 이 주소를 통해 String 객체를 참조한다.
java 명령어로 JVM이 구동되면 JVM은 운영체제에서 할당받은 메모리 영역을 다음과 같이 구분해서 사용한다.
메소드 영역은 바이트코드 파일을 읽은 내용이 저장되는 영역으로 클래스별로 상수, 정적 필드, 메소드 코드, 생성자 코드 등이 저장된다.
힙 영역은 객체가 생성되는 영역이다.
객체의 주소는 메소드 영역과 스택 영역의 상수와 변수에서 참조할 수 있다.
스택 영역은 메소드를 호출할 때마다 생성되는 프레임이 저장되는 영역이다.
메소드 호출이 끝나면 프레임은 자동 제거된다.
프레임 내부에는 로컬 변수 스택이 있다. 여기에서 기본 타입 변수와 참조 타입 변수가 생성되고 제거된다.
참조 타입 변수의 값은 객체의 주소이므로 참조 타입 변수의 ==, != 연산자는 주소를 비교하는 것이 된다.
위 그림에서 refVar1과 refVar2는 서로 다른 객체를 참조하고 있으므로 == 및 != 연산의 결과는 다음과 같다.
refVar1 == refVar2; // 결과: false
refVar1 != refVar2; // 결과: true
refVar2와 refVar3는 동일한 객체2를 참조하고 있으므로 == 및 != 연산의 결과는 다음과 같다.
refVar2 == refVar3; // 결과: true
refVar2 != refVar3; // 결과: false
다음 예제는 두 배열 변수를 ==, != 연산으로 같은 배열을 참조하는지 검사한다.
package ch05.sec03;
public class ReferenceVariableCompareExample {
public static void main(String[] args) {
int[] arr1;
int[] arr2;
int[] arr3;
arr1 = new int[]{1, 2, 3};
arr2 = new int[]{1, 2, 3};
arr3 = arr2;
System.out.println(arr1 == arr2); // arr1과 arr2 변수가 같은 배열을 참조하는지 검사
System.out.println(arr2 == arr3); // arr2와 arr3 변수가 같은 배열을 참조하는지 검사
}
}
9행에서 생성한 배열과 10행에서 생성한 배열은 저장 항목은 같지만 서로 다른 배열 객체로 생성되므로 arr1과 arr2 변수에 대입되는 주소는 다르다. 따라서 13행의 결과는 false가, 14행의 결과는 true가 출력된다.
arr3은 11행에서 arr2 변수의 주소가 대입되었기 때문에 두 변수는 동일한 주소를 가지며 같은 배열을 참조한다.
참조 타입 변수는 아직 주소를 저장하고 있지 않다는 뜻으로 null값을 가질 수 있다.
null도 초기값으로 사용할 수 있기 때문에 null로 초기화된 참조 변수는 스택 영역에 생성된다.
자바는 프로그램 실행 도중에 발생하는 오류를 예외(Exception)라고 부른다.
참조 변수를 사용하면서 가장 많이 발생하는 예외 중 하나는 NullPointerException
이다.
변수가 null인 상태에서 객체의 데이터나 메소드를 사용하려 할 떄 이 예외가 발생한다.
아래 코드를 보자
int[] intArray = null;
intArray[0] = 10; // NullPointerException
따라서 앞으로 NullPointerException
이 발생하면 예외가 발생된 곳에서 null인 상태의 참조 변수가 사용되고 있음을 알아야 한다.
이것을 해결하려면 참조 변수가 객체를 정확히 참조하도록 주소를 대입해야 한다.
경우에 따라서는 참조 타입 변수에 일부러 null을 대입하기도 한다.
프로그램에서 객체를 사용하려면 해당 객체를 참조하는 변수를 이용해야 하는데, 변수에 null을 대입하면 번지를 잃게 되므로 더 이상 객체를 사용할 수 없게 된다.
String hobby = "여행";
hobby = null;
어떤 변수에서도 객체를 참조하지 않으면 해당 객체는 사용할 수 없는 객체가 된다.
즉, 힙 메모리에는 있지만 주소를 모르기 때문에 사용할 수 없게 된다.
자바는 이러한 객체를 쓰레기(Garbage)로 취급하고, 쓰레기 수집기(Garbage Collector)를 실행시켜 자동으로 제거한다.
자바는 객체를 직접 제거하는 방법을 제공하지 않는다. 객체를 제거하는 유일한 방법은 객체의 모든 참조를 없애는 것이다.
다음 코드에서 "여행"에 해당하는 String 객체는 쓰레기가 된다. hobby 변수에 "영화"가 대입되면서 다른 String 객체의 주소가 대입되어 이전 주소를 잃어버리기 때문이다.
String hobby = "여행";
hobby = "영화";
자바의 문자열은 String 객체로 생성된다.
다음은 두 개의 String 변수 name과 hobby를 선언하고 문자열 리터럴을 대입한 것이다.
String name; // String 타입 변수 name 선언
name = "홍길동"; // name 변수에 문자열 대입
String hobby = "여행"; // String 타입 변수 hobby를 선언하고 문자열 대입
자바는 문자열 리터럴이 동일하다면 String 객체를 공유하도록 설계되어 있다.
다음 예제를 보자.
String name1 = "홍길동";
String name2 = "홍길동";
String 변수에 문자열 리터럴을 대입하는 것이 일반적이지만, new 연산자로 직접 String 객체를 생성하고 대입할 수도 있다.
다음 예제를 보자.
String name1 = new String("홍길동");
String name2 = new String("홍길동");
이 경우 name1과 name2 변수는 서로 다른 객체이므로 서로 다른 String 객체의 주소를 가지게 된다.
그렇기 때문에 문자열 리터럴로 생성하느냐 new 연산자로 생성하느냐에 따라 비교 연산자의 결과가 달라질 수 있다.
다음 예제를 보자.
String name1 = "홍길동";
String name2 = "홍길동";
String name3 = new String("홍길동");
name1 == name2 // true
name1 == name3 // false
동일한 String 객체든 다른 String 객체든 상관없이 내부 문자열만을 비교할 경우에는 String 객체의 equals() 메소드를 사용한다.
이 부분이 파이썬, 자바스크립트 등의 언어와 다른 점이다.
Python 의 경우
name1 = "홍길동"
name2 = "홍길동"
name1 == name2 # true
자바스크립트의 경우
let name1 = "홍길동";
let name2 = "홍길동";
name1 == name2 // true
name1 === nam2 // true
따라서 자바에서 문자열 객체의 주소가 아닌 문자열의 값을 비교하는 경우에는 equals() 메소드를 사용한다는 사실을 꼭 기억하자!
다음은 equals()
메소드의 사용 예시이다.
boolean result = str1.equals(str2); // 문자열이 같은지 검사(대소문자 구분)
boolean result != str1.equals(str2); // 문자열이 다른지 검사
문자열에서 특정 위치의 문자를 얻고 싶다면 charAt()
메소드를 이용할 수 있다.
charAt()
메소드는 매개값으로 주어진 인덱스의 문자를 리턴한다.
다음 예제를 보자.
String subject = "자바 프로그래밍";
char charValue = subject.charAt(3); // "프"
문자열에서 문자의 개수(길이)를 얻고 싶다면 length()
메소드를 사용한다.
다음 예제를 보자.
String subject = "자바 프로그래밍";
int length = subject.length(); // 8
문자열에서 특정 문자열을 다른 문자열로 대체하고 싶다면 replace()
메소드를 사용한다.
replace()
메소드는 기존 문자열은 그대로 두고, 대체한 새로운 문자열을 리턴한다.
다음 예제를 보자
String oldStr = "자바 프로그래밍";
String newStr = oldStr.replace("자바", "JAVA");
String 객체의 문자열은 변경이 불가능한 특성을 갖기 때문에 replace() 메소드가 리턴하는 문자열은 원래 문자열의 수정본이 아니라 완전히 새로운 문자열이다.
따라서 newStr 변수는 다음 그림과 같이 새로 생덩된 "JAVA 프로그래밍" 문자열을 참조한다.
문자열에서 특정 위치의 문자열을 잘라내어 가져오고 싶다면 substring()
메소드를 사용한다.
메소드 | 설명 |
---|---|
substring(int beginIndex) | beginIndex에서 끝까지 잘라내기 |
substring(int beginIndex, int endIndex) | beginIndex에서 endIndex 앞까지 잘라내기 |
다음 예제를 보자
String ssn = "880815-1234567";
String firstNum = ssn.substring(0, 6); // 880815
String secondNum = ssn.substring(7); // 1234567
문자열에서 특정 문자열의 위치를 찾고자 할 때에는 indexOf()
메소드를 사용한다.
indexOf()
메소드는 주어진 문자열이 시작되는 인덱스를 리턴한다.
다음 예제를 보자
String subject = "자바 프로그래밍";
int index = subject.indexOf("프로그래밍"); // 3
만약 주어진 문자열이 포함되어 있지 않으면 -1
을 반환한다.
단순히 주어진 문자열이 포함되어 있는지만 조사하고 싶다면 contains()
메소드를 사용하면 편리하다.
원하는 문자열이 포함되어 있으면 true
를 리턴하고, 그렇지 않으면 false
를 리턴한다.
다음 예제를 보자
String subject = "자바 프로그래밍";
boolean result1 = subject.contains("프로그래밍"); // true
boolean result2 = subject.contains("파이썬"); // false
문자열이 구분자를 사용하여 여러 개의 문자열로 구성되어 있을 경우, 이를 따로 분리해서 얻고 싶다면 split()
메소드를 사용한다.
다음 예제를 보자
String board = "번호,제목,내용,글쓴이";
String[] arr = board.split(","); // {"번호", "제목", "내용", "글쓴이"}
board 변수는 쉼표(,)로 구분된 문자열을 가지고 있다.
split()
메소드를 호출할 때 쉼표를 제공하면 분리된 문자열로 구성된 배열(array)을 얻을 수 있다.
바로 이어서 배열에 대해 알아보자
변수는 하나의 값만 저장할 수 있다. 저장해야 할 값의 수가 많아지면 그만큼 많은 변수가 필요하다. 따라서 많은 양의 값을 다루는 좀 더 효율적인 방법이 필요한데, 이것이 배열이다.
배열은 다음과 같은 특징을 가지고 있다.
배열을 사용하기 위해서는 우선 배열 변수를 선언해야 한다.
두 가지 방법이 있지만 관례적으로 첫 번째 방법을 주로 사용한다.
// 첫 번째 방법
타입[] 변수;
// 두 번째 방법
타입 변수[];
다음 예제를 보자
int[] intArray;
double[] doubleArray;
String[] strArray;
배열 변수는 참조 변수이다. 배열고 객체이므로 힙 영역에 생성되고 배열 변수는 힙 영역의 배열 주소를 저장한다
참조할 배열이 없다면 배열 변수도 null로 초기화할 수 있다.
타입[] 변수 = null;
하지만 만약 배열 변수가 null값을 가진 상태에서 변수[인덱스]로 값을 읽거나 저장하려고 시도하면 NullPointerException이 발생한다는 사실을 잊지 말자!
배열에 저장될 값의 목록이 있다면 다음과 같이 간단하게 배열을 생성할 수 있다.
타입[] 변수 = {값0, 값1, 값2, ...};
// 사계절 배열 생성
String[] season = {"Spring", "Summer", "Fall", "Winter"};
// 1번 인덱스의 "Summer"를 "여름"으로 변경
seasion[1] = "여름";
여기서 주의할 점은 중괄호로 감싼 값의 목록을 대입할 때에는 반드시 배열 변수를 선언과 동시에 대입해야 한다는 점이다.
타입[] 변수;
변수 = {값0, 값1, 값2, ...}; // 컴파일 에러
만약 배열 변수를 선언한 시점과 값 목록이 대입되는 시점이 다르다면 다음과 같이 작성해야 한다.
타입[] 변수;
변수 = new 타입[] {값0, 값1, 값2, ...};
여기서 타입[] 변수;
의 타입과 변수 = new 타입[] {값0, 값1, 값2, ...};
의 타입은 반드시 동일해야 한다.
값의 목록은 없지만 향후 값을 저장할 목적으로 배열을 미리 생성할 수도 있다.
다음과 같이 생성할 수 있다.
타입[] 변수 = new 타입[길이];
다음 예제는 길이가 5인 int 배열을 생성하고, 배열 주소를 intArray 변수에 대입한다.
int[] intArray = new int[5];
new 연산자로 배열을 처음 생성하면 배열 항목은 기본값으로 초기화된다.
다음 표는 타입별 배열의 초기값을 보여준다.
정수 배열은 0, 실수 배열은 0.0, 논리 배열은 false, 참조 배열은 null로 초기화된다.
배열의 길이란 배열에 저장할 수 있는 항목 수를 말한다.
배열의 길이를 얻는 방법은 다음과 같다.
배열변수.length;
하지만 배열의 길이는 읽기만 가능하므로 다음과 같이 값을 변경하려고 시도하면 오류가 발생한다.
int[] intArray = new int[5];
intArray.length = 10; // 컴파일 에러 발생
배열 항목에는 또 다른 배열이 대입될 수 있는데, 이러한 배열을 다차원 배열이라고 한다.
다음은 2차원 배열과 3차원 배열의 구조를 보여준다.
위 그림에서 값1, 값3, 값6을 읽는 방법은 다음과 같다.
변수[0][0][0] // 값1
변수[0][1][0] // 값3
변수[1][0][1] // 값6
다음과 같이 생성 가능하다.
타입[][] 변수 = {
{값1, 값2, ...}, // 1차원 배열의 인덱스 0
{값3, 값4, ...}, // 1차원 배열의 인덱스 1
...
};
다음 예제는 두 반의 학생 점수를 저장하는 배열의 예제이다.
int[][] scores = {
{80, 90, 96}, // 1차원 배열의 인덱스 0: 첫 번째 반 성적
{76, 88} // 1차원 배열의 인덱스 1: 두 번째 반 성적
};
다음과 같이 생성 가능하다.
타입[][] 변수 = new 타입[1차원수][2차원수];
다음 예제는 두 반의 학생 점수를 저장하는 배열의 예제이다.
int[][] scores = new int[2][3];
다음 예제는 두 반의 학생 이름을 저장하는 배열의 예제이다.
String[][] names = new String[2][];
만약 두 반의 학생 수가 다를 경우 2차원 배열의 길이를 다르게 줄 수 있다.
intp[][] scores = new int[2][];
scores[0] = new int[3]; // 첫 번째 반의 학생 수가 3명
scores[1] = new int[2]; // 두 번째 반의 학생 수가 2명
기본 타입(byte, char, short, int, long, float, double, boolean) 배열은 각 항목에 값을 직접 저장하지만, 참조 타입(클래스, 인터페이스) 배열은 각 항목에 객체의 주소를 저장한다.
다음과 같이 String 타입의 배열을 생성하고, 각 항목에 문자열을 대입했다고 가정해보자.
String[] strArray = new String[3];
strArray[0] = "Java";
strArray[1] = "C++";
strArray[2] = "C#";
그림으로 나타내면 아래와 같다.
==, != 연산자를 사용하면 객체의 주소를 비교하고, equals() 메소드를 사용하면 문자열의 값을 비교한다.
다음 예제를 보자
String[] languages = new String[3];
languages[0] = "Java";
languages[1] = "Java";
languages[2] = new String("Java");
System.out.println(languages[0] == languages[1]); // true
System.out.println(languages[0] == languages[2]); // false
System.out.println(languages[0].equals(languages[2])); // true
그림으로 나타내면 아래와 같다.
배열은 한 번 생성하면 길이를 변경할 수 없다. 더 많은 저장 공간이 필요하다면 더 큰 길이의 배열을 새로 만들고 이전 배열로부터 항목들을 복사해야 한다.
가장 기본적이 복사 방법은 for 문을 이용해서 항목을 하나씩 읽고 새로운 배열에 저장하는 것이다.
public class ArrayCopyByForExample {
public static void main(String[] args) {
// 길이 3인 배열
int[] oldIntArray = {1, 2, 3};
// 길이 5인 배열을 새로 생성
int[] newIntArray = new int[5];
// 배열 항목 복사
System.arraycopy(oldIntArray, 0, newIntArray, 0, oldIntArray.length);
// 배열 항목 출력
for (int i = 0; i < newIntArray.length; i++) {
System.out.print(newIntArray[i] + ", ");
}
}
}
실행 결과
1, 2, 3, 0, 0,
배열 복사를 위한 좀 더 간단한 방법이 있다.
System의 arraycopy()
메소드를 이용하면 한 줄만으로도 배열 복사를 할 수 있다.
원본 배열이 arr1이고 새 배열이 arr2일 경우 다음과 같이 코드를 작성하면 된다.
// 원본 배열, 원본 배열 시작 인덱스, 새 배열, 새 배열 붙여넣기 시작 인덱스, 복사 항목 수
System.arraycopy(arr1, 0, arr2, 0, arr1.length);
중요한 점은 위 두 가지 방법 모두 1차원 배열의 복사를 위한 코드라는 것이다.
2차원 배열을 복사하기 위해서는 2중 for 문을 활용하거나 다음과 같이 for 문과 System.arraycopy를 함께 사용해야 한다.
public class Array_Copy{
public static void main(String[] args) {
int a[][] = {{1,2,3},{4,5,6,},{7,8,9}};
int b[][] = new int[a.length][a[0].length];
for(int i=0; i<b.length; i++){
System.arraycopy(a[i], 0, b[i], 0, a[0].length);
}
}
}
n차원 배열을 복사하기 위해서는 for 문을 계속 n-1번 중첩해서 사용해야 한다.
자바는 배열 및 컬렉션을 좀 더 쉽게 처리할 목적으로 향상된 for 문을 제공한다.
향상된 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);
}
}
자바 프로그램을 실행하기 위해 지금까지 main() 메소드를 작성했는데, 여기서 문자열 배열 형태인 String[] args 매개변수가 왜 필요한지 알아보자.
윈도우의 명령 프롬프트(또는 파워쉘)나 맥 OS, 리눅스의 터미널에서 프로그램을 실행할 때는 요구하는 값이 있을 수 있다.
예를 들어 두 수를 입력받고 덧셈을 수행하는 Sum 프로그램은 실행할 때 다음과 같이 두 수를 요구할 수 있다.
java Sum 10 20
공백으로 구분된 10과 20은 문자열로 취급되며 String[] 배열의 항목 값으로 구성된다.
그리고 main() 메소드 호출 시 매개값으로 전달된다.
main() 메소드 중괄호 내에서 문자열 "10"과 "20"은 다음과 같이 얻을 수 있다.
String x = args[0];
String y = args[1];
매개값이 몇 개 입력되었는지 확인하려면 다음과 같이 length()
를 활용하면 된다.
args.length;
만약 다음과 같이 값을 주지 않고 실행하면 args.length()
는 0이 된다.
데이터 중에는 몇 가지로 한정된 값을 갖는 경우가 있다. 이와 같이 한정된 값을 갖는 타입을 열거 타입(Enumeration Type)이라고 한다.
열거 타입을 사용하기 위해서는 열거 타입 이름으로 소스 파일(.java)을 생성하고 한정된 값을 코드로 정의해야 한다.
열거 타입 이름은 파스칼 케이스(Pascal Case)를 따르는 것이 관례이다.
요일로 열거 상수 목록을 작성해보자.
public enum Week {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}
열거 상수는 관례적으로 알파벳으로 정의하며, 모두 대문자로 작성한다.
만약 열거 상수가 여러 단어로 구성될 경우에는 다음과 같이 단어와 단어 사이를 언더바(_)로 연갈하는 것이 관례이다.
public enum LoginResult {
LOGIN_SUCCESS,
LOGIN_FAILED
}
열거 타입도 하나의 데이터 타입이므로 변수를 선언하고 사용해아 한다.
다음과 같이 사용한다.
Week today;
Week reservationDay;
// 열거타입.상수 형태로 사용한다.
today = Week.SUNDAY;
// 열거 타입도 참조 타입이므로 null을 대입할 수 있다.
today = null;