이번 포스팅은 Java의 기초 개념 중 하나인 변수의 종류와 초기화 방법에 대해서 작성해 보았습니다. 클래스를 이용하여 객체를 생성하고, 객체마다 속성을 정의해 주는 멤버변수(클래스 영역에 선언된 변수)를 이용할 때 어떤 종류의 변수를 이용하는 것이 좋은지 숙지하여 적재적소에 변수의 종류를 잘 선택하여 사용하는 것이 중요할 것 같습니다.
자바에서는 변수의 종류가 크게 3가지가 있습니다. 이 변수들은 선언 위치에 따라 구분할 수 있는데 아래에서 살펴보겠습니다.
선언 위치에 따른 변수 종류
1. 클래스 변수
멤버변수 중 static이 붙은 변수
즉, 인스턴스 변수 앞에 static을 붙여서 선언합니다. 클래스 변수는 클래스가 메모리에 로딩될 때 생성되므로, 인스턴스를 생성하지 않아도 "클래스 이름.클래스 변수"의 형태로 언제든지 사용할 수 있습니다.
또한 모든 인스턴스는 공통된 저장공간(클래스 변수)을 공유하게 됩니다. 따라서, 한 클래스의 모든 인스턴스들이 동일한 값을 유지해야 하는 속성일 경우 클래스 변수로 선언합니다.
2. 인스턴스 변수
멤버변수 중 클래스 변수가 아닌 변수
클래스의 인스턴스를 생성할 때 만들어지므로, 인스턴스 변수를 사용하기 위해서는 우선 객체(인스턴스)를 생성해야 합니다.
인스턴스는 독립적인 저장공간을 가지므로 인스턴스 변수는 객체마다 서로 다른 값을 가질 수 있습니다. 따라서 객체마다 고유한 상태/값을 가져야 하는 속성의 경우 인스턴스 변수로 선언합니다.
3. 지역변수
멤버변수를 제외한 나머지 변수
메서드 내에 선언되어 메서드 내에서만 사용이 가능합니다.
선언된 블럭블록 { }내에서만 사용이 가능하고, 블록 { }을 벗어나면 소멸되어 사용할 수 없습니다.
public class Variables {
int instanceVar; // 인스턴스 변수
static int classVar; // 클래스 변수, 공유 변수
void method(){
int iv; // 지역변수
}
}
여기서 잠깐, static과 관련해서 알아두어야 하는 내용을 간단하게 짚고 넘어가겠습니다.
1. static 변수나 메서드는 모든 인스턴스에 공통적으로 사용해야 하는 경우에 사용하면 좋습니다.
2. 클래스 변수는 인스턴스를 생성하지 않아도 사용 가능합니다.
3. 클래스 메서드는 인스턴스 변수 및 메서드를 사용할 수 없습니다.
- 클래스 메서드 호출시, 인스턴스가 존재하지 않을 수도 있기 때문에 인스턴스 변수에 접근하면 오류가 발생할 수 있기 때문입니다.
- 반대로, 인스턴스 메서드 내에서는 클래스 변수 및 매서드를 사용할 수 있습니다.
4. 메서드 내에서 인스턴스 변수를 사용하지 않는다면, 메서드 앞에 static을 붙이는 것이 좋습니다.
- static 메서드는 호출시간이 짧아지기 때문에 성능이 향상되기 때문입니다.
변수의 종류에 대해서 알아봤으니 변수의 초기화에 대해서도 공부해 보겠습니다.
변수의 초기화
변수를 선언하고 처음으로 값을 저장하는 것을 '변수의 초기화'라고 알고 계실 것입니다. 변수를 선언한 후 사용하기 전에는 변수를 초기화해주는 것이 좋습니다. 그렇지 않으면 일명 쓰레기 값이라고 불리는 값이 들어가서 의도하지 않은 결과가 나타날 수 있기 때문이죠.
변수의 초기화는 아래 코드와 같이, 위에서 배운 변수의 종류에 따라 개발자가 직접 해야 하는 경우도 있고, 직접 하지 않아도 디폴트 값으로 초기화가 되는 경우도 있습니다.
public class Variables {
static int classVar; // 클래스 변수
int instanceVar; // 인스턴스 변수
void method() {
int iv = 0; // 지역변수: 명시적 초기화 필요 !!
System.out.println(iv);
System.out.println(instanceVar);
System.out.println(classVar);
}
}
/* 결과
0
0
0
*/
즉, 위에서 보이는 바와 같이 멤버변수는 우리가 초기화하지 않아도 변수의 자료형에 맞는 디폴트 값으로 초기화가 이루어지지만, 지역변수는 사용하기 전에 반드시 초기화를 해주어야 합니다.
자료형에 따른 디폴트 값 (참고)
자료형 | 기본값 |
boolean | false |
char | '\u0000' |
byte, short, int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d 혹은 0.0 |
참조형 변수 | ㅜㅕㅣㅣ |
지역변수의 경우 위와 같이 간단하게 초기화가 가능합니다. 멤버변수는 지역변수와 달리 다양한 초기화 방법이 있습니다. 이에 대해서 알아보겠습니다.
멤버변수의 초기화 방법
1. 명시적 초기화 (Explicit Initialization)
지역변수의 초기화처럼 변수의 선언과 동시에 초기화하는 방법입니다. 통상적으로 가장 먼저 고려되는 기본적인 초기화 방법이라고 할 수 있습니다.
public class Variables {
static int classVar = 0;
Member member = new Member();
...
}
2. 초기화 블럭 (Initialization Block)
복잡한 초기화 작업이 필요할 때는 명시적 초기화로는 한계가 있는 경우가 있습니다. 이럴 때 사용하는 초기화 방법이 초기화 블록을 이용하는 방법입니다.
초기화 블럭의 종류에는 클래스 변수를 초기화하는 블록과 인스턴스 변수를 초기화하는 블록이 있습니다. 즉, 클래스 변수 초기화 블록은 클래스 변수의 복잡한 초기화에, 인스턴스 변수 초기화 블록은 인스턴스 변수의 복잡한 초기화에 사용됩니다.
아래와 같이 클래스 변수 초기화 블록은 인스턴스 변수 초기화 블록에 static 키워드를 붙여서 정의할 수 있습니다.
class Variables{
...
/**
* 클래스 변수 초기화 블럭
*/
static {
}
/**
* 인스턴스 변수 초기화 블럭
*/
{
}
...
}
클래스 초기화 블럭은 클래스 변수와 마찬가지로 클래스가 초기에 메모리에 로딩될 때 한 번만 수행이 되고, 인스턴스 초기화 블록은 생성자와 같이 인스턴스를 생성할 때마다 수행됩니다.
예시를 보겠습니다.
class Variables {
static int classVar;
static String classStr;
int instanceVar;
Member member;
/**
* 클래스 변수 초기화 블럭
*/
static {
classStr = "abc";
System.out.printf("class 변수1: %d\nclass 변수2: %s\n", classVar, classStr);
}
/**
* 인스턴스 변수 초기화 블럭
*/
{
instanceVar++;
classVar++;
member = new Member();
System.out.printf("Member%d가 생성되었습니다.\n", classVar);
System.out.printf("Instance 변수: %d\n", instanceVar);
}
}
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 3; ++i) {
new Variables();
}
}
}
main 함수에서 Variables 클래스 객체를 3개 생성하였습니다.
클래스 변수 초기화 블럭은 처음에 한 번만 실행된다고 했으므로, classVar의 값을 출력하면 0(디폴트 값)으로, classStr의 값은 초기화 블록에서 초기화 한 'abc'로 나올 것입니다.
인스턴스 변수 초기화 블럭은 인스턴스가 만들어질 때마다 실행된다고 하였습니다. 우선 instanceVar은 인스턴스 변수이므로 인스터스마다 독립적으로 가질 것입니다. 따라서 생성될 때마다 instanceVar은 디폴트 값(0)에서 1 증가한 값을 각 객체마다 동일하게 가지게 될 것입니다. 그런데, 인스턴스 변수 초기화 블록 안에 클래스 변수의 증가도 있습니다. 클래스 변수는 인스턴스가 공유하는 변수이기 때문에 인스턴스 변수 초기화 블럭 내 클래스 변수는 이전에 증가했던 상태에서 계속 증가하게 될 것입니다.
직접 결과를 예상해보고 결과의 정답을 확인해 보세요!
class 변수2: abc
Member1가 생성되었습니다.
Instance 변수: 1
Member2가 생성되었습니다.
Instance 변수: 1
Member3가 생성되었습니다.
Instance 변수: 1
주의할 점은, 위 코드에서와 같이 인스턴스 변수 초기화 블록 내에서는 클래스 변수에 접근이 가능하지만, 클래스 변수 초기화 블록 내에서는 인스턴스 변수에 접근이 불가능합니다. 그 이유는 무엇일까요?
네, 맞습니다. 클래스 변수는 프로그램 실행 중에 클래스에 대한 정보가 필요할 때 (예를 들면 클래스 멤버 사용 혹은 인스턴스 생성 시) 메모리에 로딩이 됩니다. 따라서 인스턴스 변수 초기화 블럭에서의 클래스 변수는 이미 메모리에 로딩이 된 상태이기 때문에 사용할 수 있습니다. 하지만 인스턴스 변수는 인스턴스가 생성될 때 메모리에 로딩되므로 클래스 변수 초기화 블럭 내에서는 인스턴스 변수가 메모리가 로딩이 되지 않은 상태이므로 인스턴스 변수를 사용할 수 없습니다.
초기화 블럭은 이와 같이 배열이나 예외처리가 필요한 초기화 등 복잡한 초기화가 요구되어질 때, 명시적 초기화만으로는 초기화를 할 수가 없을 때 유용하게 사용할 수 있습니다.
3. 생성자 (Constructor)
생성자는 잘 아시다시피 객체가 생성될 때 호출되는 '인스턴스 초기화 메서드' 입니다. 따라서 보통 인스턴스 변수 초기화 작업이나 인스턴스 생성 시 실행이 필요한 작업을 위해서 사용됩니다.
생성자가 있는데 인스턴스 변수 초기화 블록이 왜 필요한지 의문이 들 수 있습니다.
인스턴스 변수의 초기화는 보통 생성자를 통해 이루어집니다. 하지만 생성자가 여러 개 존재하고, 각 생성자 내에서 중복되는 코드가 있다면, 즉 모든 생성자가 공통적으로 수행해야 하는 역할이 있다면 이 부분만 따로 추출하여 인스턴스 변수 초기화 블록으로 구성하면 코드가 깔끔해 질 것입니다.
그럼, 생성자와 인스턴스 변수 초기화 블록 중 어느 것이 먼저 호출되는 것일까요?
인스턴스 변수 초기화 블럭이 생성자 보다 먼저 수행됩니다. 이 부분을 기억하고 초기화 코드를 작성해야 합니다.
아래의 코드에서 생성자 내에 중복되는 코드가 있음을 확인할 수 있습니다.
class Animal{
static int count;
String type;
// 생성자 1
Animal(){
count++;
this.type = "etc";
System.out.printf("Animal %d is me!",count);
}
// 생성자 2
Animal(String type){
count++;
this.type = type;
System.out.printf("Animal %d is me!",count);
}
}
이 중복코드를 인스턴스 변수 초기화 블록으로 추출해서 코드를 작성할 수 있습니다.
class Animal {
static int count;
String type;
{
count++;
System.out.printf("Animal %d is me!", count);
}
// 생성자 1
Animal() {
this.type = "etc";
}
// 생성자 2
Animal(String type) {
this.type = type;
}
}
이렇게 코드의 중복을 제거하는 것은 코드의 신뢰성을 높여주고, 오류의 발생가능성을 줄여 준다는 장점이 있습니다. 그리고 이렇게 재사용성을 높이고 중복을 제거하는 것이 객체 지향 프로그래밍이 궁극적으로 추구하는 바입니다.
하지만 주의할 점이 있습니다. 생성자 내에서 코드에 중복이 있다고 무조건 인스턴스 초기화 블럭으로 추출하는 것이 바람직하지 않은 경우도 있습니다. 위에서 말씀드렸다시피 인스턴스 초기화 블럭이 생성자보다 먼저 호출됩니다. 따라서 아래와 같이 코드를 작성하면 의도하지 않는 결과가 나오게 됩니다.
class Animal {
static int count;
String type;
{
count++;
System.out.printf("Animal %d: my type is %s\n", count, type); // type: null로 출력
}
// 생성자 1
Animal() {
this.type = "etc";
//System.out.printf("Animal %d: my type is %s\n", count, type);
}
// 생성자 2
Animal(String type) {
this.type = type;
//System.out.printf("Animal %d: my type is %s\n", count, type);
}
}
아직 인스턴스 변수 type이 생성자 내에서 초기화 되지 않았기 때문에 null로 출력이 됩니다. 따라서 이 부분을 주의해서 인스턴스 변수 초기화 블록을 적절하게 사용하는 것이 중요합니다.
'Java' 카테고리의 다른 글
다형성(Polymorphism) (2) | 2023.10.15 |
---|---|
추상클래스(Abstract)과 인터페이스(Interface) (0) | 2023.10.15 |
제어자 (static과 final) (0) | 2023.10.15 |
JVM의 메모리 구조 (0) | 2023.10.14 |
가변인자(varargs) (0) | 2023.10.14 |