데이터 타입 분류
프로그램이 하는 일은 결국 데이터를 처리하는 것 이다. 따라서 데이터를 얼마나 잘 다루느냐가 좋은 프로그램을 작성할 수 있는 관건이 된다. 데이터를 잘 다루기 위해서는 자바에서 지원하는 데이터 타입에 대해서 제대로 이해할 필요가 있다. 자바의 데이터 타입에는 크게 기본 타입(원시 타입)과 참조 타입으로 분류된다. 기본 타입이란 ,정수, 실수, 문자, 논리 리터럴을 저장하는 타입을 말한다. 지금까지 우리는 기본 타입으로 변수를 선언하고 데이터를 저장하는 연습을 했다. 이번 장부터는 참조 타입에 중점을 두려고 한다. 참조 타입이란 객체의 번지를 참조하는 타입으로 배열, 열거, 클래스, 인터페이스 타입을 말한다.
기본 타입으로 선언된 변수와 참조 타입으로 선언된 변수의 차이점은 저장되는 값이 무엇이냐이다. 기본 타입을 이용해서 선언된 변수는 실제 값을 변수안에 저장하지만, 참조타입인 배열, 열거, 클래스, 인터페이스를 이용해서 선언된 변수는 메모리의 번지를 값으로 갖는다. 번지를 통해 객체를 참조한다는 뜻에서 참조 타입이라고 부른다.
예를 들어 int와 double로 선언된 변수 age와 price가 있고, String 클래스로 선언된 name과 hobby가 다음과 같이 선언되어 있다고 가정해보자.
[기본 타입 변수]
int age = 25;
double price = 100.5;
[참조 타입 변수]
String name = "홍길동";
String hobby = "독서";
메모리 상에서 이 변수들이 갖는 값은 힙에 생성된다. 자바에서 사용하는 메모리 영역에 대해서는 이후에 학습하기로 하고, 지금은 변수가 스택 영역에 생성되고 객체는 힙 영역에 생성된다는 것만 알아두자.
int와 double 변수인 age와 price는 직접 값을 저장하고 있지만, String 클래스 변수인 name과 hobby는 힙 영역의 String 객체 주소 값을 가지고 있다. 주소를 통해 객체를 참조한다는 뜻에서 String 클래스 변수를 참조 타입 변수라고 한다.
메모리 사용 영역
본격적으로 참조 타입을 알아보기 전에 우선 JVM이 사용하는 메모리 영역에 대해서 알아보기로 하자. java.exe로 JVM이 시작되면 JVM은 운영체제에서 할당받은 메모리 영역을 세부영역으로 구분해서 사용한다.
메소드 영역
메소드 영역에는 코드에서 사용되는 클래스들을 클래스 로더로 읽어 클래스별로 런타임 상수풀, 필드 데이터, 메소드 데이터, 메소드 코드, 생성자 코드 등을 분류해서 저장한다. 메소드 영역은 JVM이 시작할 때 생성되고 모든 스레드가 공유하는 영역이다.
힙(Heap) 영역
힙 영역은 객체와 배열이 생성되는 영역이다. 힙 영역에 생성된 객체와 배열은 JVM 스택 영역의 변수나 다른 객체의 필드에서 참조한다. 참조하는 변수나 필드가 없다면 의미 없는 객체가 되기 때문에 이것을 쓰레기로 취급하고 JVM은 쓰레시 수집기(Garbage Collector)를 실행시켜 쓰레기 객체를 힙 영역에서 자동으로 제거한다. 그렇기 때문에 개발자는 객체를 제거하기 위해 별도의 코드를 작성할 필요가 없다.
JVM 스택(Stack) 영역
JVM 스택 영역은 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 할당된다. 자바 프로그램에서 추가적으로 스레드를 생성하지 않았따면 main 스레드만 존재하므로 JVM 스택도 하나이다. JVM 스택은 메소드를 호출할 때마다 프레임(Frame)을 추가(push)하고 메소드가 종료되면 해당 프레임을 제거(pop)하는 동작을 수행한다. 예외발생시 printStackTrace() 메소드로 보여주는 Stack Trace의 각 라인은 하나의 프레임을 표현한다. printStackTrace() 메소드는 예외 처리에서 설명한다.
프레임 내부에는 로컬 변수 스택이 있는데, 기본 타입 변수와 참조 타입 변수가 추가(push)되거나 제거(pop)된다. 변수가 이 영역에서 생성되는 시점은 초기화가 될 때, 즉 최초로 변수에 값이 저장될 때이다. 변수는 선언된 블록 안에서만 스택에 존재하고 블록을 벗어나면 스택에서 제거된다. 다음 코드를 보자.
// 1
char v1 = 'A';
// 2
if(v1=='A') {
int v2 = 100;
double v3 = 3.14;
}
// 3
boolean v4 = true;
선언된 변수는 실행 순서에 따라 스택에 생성되고 소멸된다. 1번이 실행되면 v1이 스택에 생성되고, v1이 'A' 이므로 2번이 실행된다. 2번이 실행되면 v2와 v3가 생성되고 if문을 빠져나오게 되면 v2와 v3가 소멸한다. 그 이후에 3번이 실행되고 v4개 스택에 생성된다.
기본 타입 변수는 스택 영역에 직접 값을 가지고 있지만, 참조 타입 변수는 값이 아니라 힙 영역이나 메소드 영역의 객체 주소를 가진다. 다음과 같이 배열 변수인 scores는 스택 영역에 생성되지만 실제 10, 20, 30을 갖는 배열은 힙 영역에 생성된다. 배열 변수 scores에는 배열의 힙 영역의 주소가 저장된다. 참고로 자바는 배열을 객체로 취급한다.
int[] scores = {10, 20, 30};
참조 변수의 ==, != 연산
기본 타입 변수의 ==, != 연산은 변수의 값이 같은지, 아닌지를 조사하지만 참조 타입 변수들 간의 ==, != 연산은 동일한 객체를 참조하는지, 다른 객체를 참조하는지 알아볼 때 사용된다. 참조 타입 변수의 값은 힙 영역의 객체 주소이므로 결국 주소 값을 비교하는 것이 된다. 동일한 주소 값을 갖고 있다는 것은 동일한 객체를 참조한다는 의미이다. 따라서 동일한 객체를 참조하고 있을 경우 == 연산의 결과는 true이고, != 연산의 결과는 false이다.
==와 != 연산자로 객체를 비교하는 코드는 일반적으로 if문에서 많이 사용된다. 다음은 변수 refVar1과 refVar2가 같은 객체를 참조할 경우 if 블록을 실행하도록 코딩한 것이다.
if( refVar1 == refVar2) {...}
null과 NullPointerException
참조 타입 변수는 힙 영역의 객체를 참조하지 않는다는 뜻으로 null 값을 가질 수 있다. null 값도 초기값으로 사용할 수 있기 때문에 null로 초기화된 참조 변수는 스택 영역에 생성된다.
참조 타입 변수가 null 값을 가지는지 확인하려면 ==, != 연산을 수행하면 된다.
// refVa1 이 객체를 참조한다면
refVar1 == null 결과 : false
refVar1 != null 결과 : true
프로그램 실행 도중에 발생하는 오류를 예외(Exception)라고 부른다. 예외는 사용자의 잘못된 입력으로 발생할 수도 있고, 프로그래머가 코드를 잘못 작성해서 발생할 수도 있다. 참조 변수를 사용하면서 가장 많이 발생하는 예외 중 하나로 NullPointerException이 있다. 이 예외는 참조 타입 변수를 잘못 사용하면 발생한다. 참조 타이 변수가 null을 가지고 있을 경우, 참조 타입 변수는 사용할 수 없다. 참조 타입 변수를 사용하는 것은 곧 객체를 사용하는 것을 의미하는데, 참조할 객체가 없으므로 사용할 수 없는 것이다. 그러나 프로그래머의 실수로 null 값을 가지고 있는 참조 타입 변수를 사용하면 NullPointerException이 발생한다.
int[] intArray = null;
intArray[10] = 10; // NullPointerException
String은 클래스 타입이므로 참조 타입이다. 따라서 str 변수도 null로 초기화가 가능하다. 이 상태에서 String 객체의 length() 라는 메소드를 호출하면 NullPointerException이 발생한다.
String str = null;
System.out.println("총 문자수: " + str.length()); // NullPointerException
String 타입
자바는 문자열을 String 변수에 저장하기 때문에 다음과 같이 String 변수를 우선 선언해야 한다.
String 변수;
String 변수에 문자열을 저장하려면 큰 따옴표로 감싼 문자열 리터럴을 대입하면 된다.
변수 = "문자열";
변수 선언과 동시에 문자열을 저장할 수도 있다.
String 변수 = "문자열";
사실 문자열을 String 변수에 저장한다는 말은 틀린 표현이다. 문자열이 직접 변수에 저장되는 것이 아니라, 문자열은 String 객체로 생성되고 변수는 String 객체를 참조한다. 하지만 일반적으로 String 변수에 저장한다는 표현을 사용한다. 변수는 스택 영역에 생성되고, 문자열 리터럴은 힙 영역에 String 객체로 생성된다. 그리고 변수에는 String 객체의 주소 값이 저장된다.
자바는 문자열 리터럴이 동일하다면 String 객체를 공유하도록 되어 있다. 다음과 같이 name1과 name2 변수가 동일한 문자열을 참조할 경우, name1과 name2는 동일한 String 객체를 참조하게 된다.
String name1 = "홍길동";
String name2 = "홍길동";
일반적으로 변수에 문자열을 저장할 경우에는 문자열 리터럴을 사용하지만, new 연산자를 사용해서 직접 String 객체를 생성시킬 수도 있다. new 연산자는 힙 영역에 새로운 객체를 만들 때 사용하는 연산자로 객체 생성 연산자라고 한다.
String name1 = new String("홍길동");
String name2 = new String("홍길동");
이 경우 name1과 name2는 서로 다른 String 객체를 참조한다.
따라서 문자열 리터럴로 생성하느냐 new 연산자로 생성하느냐에 따라 비교 연산자의 결과가 달라질 수 있다.
String name1 = "홍길동";
String name2 = "홍길동";
String name3 = new String("홍길동");
name1과 name2는 동일한 문자열 리터럴로 생성된 객체를 참조하기 때문에 name1 == name2 의 결과는 true가 나온다. 그러나 name3는 new 연산자로 String 객체를 별도로 생성했기 때문에 name1 == name3 의 결과는 false가 나온다.
동일한 객체이건 상관없이 '문자열'만을 비교할 때에는 String 객체의 equals() 메소드를 사용해야 한다. equals() 메소드는 원본 문자열과 매개값으로 주어진 비교 문자열이 동일한지 비교한 후 true 혹은 false를 리턴한다.
// str1과 str2가 같은 문자열인지 비교
boolean result = str1.equals(str2)
String 변수는 참조 타입이므로 초기값으로 null을 대입할 수 있다. null은 String 변수가 참조하는 String 객체가 없다는 뜻이다. 일반적인 참조 타입과 동일한 시스템을 갖는다. 그렇가면 참조를 잃은 String 객체는 어떻게 될까? JVM은 참조되지 않은 객체를 쓰레기 객체로 취급하고 쓰레기 수집기(Garbage Collector)를 구동시켜 메모리에서 자동 제거한다.
'Java 길찾기 > 이것이 자바다' 카테고리의 다른 글
[Java] 참조 타입 (3) (0) | 2022.01.04 |
---|---|
[Java] 참조 타입 (2) (0) | 2022.01.03 |
[Java] 반복문 (0) | 2021.12.30 |
[Java] 조건문 (0) | 2021.12.29 |
[Java] 연산자 (0) | 2021.12.28 |