클래스
클래스는 객체를 정의해놓은 공간이다. 즉, 클래스는 객체를 생성하기 위해서 사용한다.
클래스를 보통 설계도에 비유를 하고 객체를 제품에 비유를 한다. 클래스라는 설계도 안에 객체(제품)을 만들기 위해 여러가지를 선언해두고 클래스(설계도)로 객체를 생성하면 객체(제품)이 생성되는 것이다.
객체
그럼 객체는 무엇일까? 객체의 정의를 현실 세계에 많이 비유를 하곤 하는데, 객체란 실제로 존재하는 것, 사물 또는 개념이라고 한다. 위에서 객체를 제품에 비유했는데 제품마다 쓰이는 용도가 다른 것처럼 객체는 객체가 가지고 있는 기능과 속성에 따라 다르다. 기능은 메소드, 속성이 변수이다.
class Tv {
String color;
boolean power;
int channel;
void power() {
power = !power;
}
void channelUp() {
channel++;
}
void channelDown() {
channel--;
}
}
TV라는 간단한 클래스를 선언해두었다. 여기서 속성이 color(tv의 색깔), power(전원), channel(채널) 이 있고, power() → 전원껏다켜기, channelUp() → 채널 올리기, channelDown() → 채널 내리기 기능이 있다.
객체와 인스턴스
객체와 인스턴스라는 용어가 생각보다 많이 헷갈리는데, 객체가 더 큰 의미로 보면 된다. 객체는 어떤 설명을 할 때 있어서, 인스턴스를 설명할 때 일반적으로 합쳐서 사용하는 언어가 객체이고, 기본적으로 어떤 클래스가 객체를 생성하는 순간, 생성된 객체를 인스턴스라고 한다.
객체의 생성과 사용
객체의 생성
// 타입에서 얘기했듯이, 타입이 클래스인 변수들은 전부 참조변수이다.
클래스명 변수명; // 클래스의 객체를 참조하기 위한 참조변수를 선언한다.
변수명 = new 클래스명(); // 클래스를 이용하여 객체를 생성한다. 해당 객체의 주소를 참조 변수에 저장한다.
// 예시 1
Tv t; // Tv 타입의 참조변수 t를 선언한다.
t = new Tv(); // Tv 인스턴스를 생성하고, 참조변수 t에 Tv 인스턴스의 주소를 저장한다.
// 예시 2
Tv t = new Tv(); // 다음과 같이 한 줄로 하는 경우가 더 많다.
객체의 사용
t.channel = 8; // 참조변수 t에 있는 멤버 변수 channel에 접근하여 값을 8로 설정한다.
t.channelDown(); // 기능인 channelDown() 을 호출한다.
System.out.println(t.channel + "입니다.");
어느 컴퓨터나 데이터를 적재할 때에는 메모리에 적재를 해야한다. 코드 영역은 컴파일 단계에서 변하지 않기 때문에 클래스를 적재하는 과정에 있어서 미리 크기를 계산하고 저장할 수 있다. 우리가 위에서 설계한 Tv 클래스의 객체에 메모리이다. 0x100이라는 논리 주소를 저장하고, 해당 논리 주소를 통해서 접근하도록 한다. 이때, 그 구역안에 메소드들은 스택영역에 메소드를 참조하기 위한 주소를 담고 있고, 각 멤버 변수들 또한 스택에 동일하게 담기게 된다. 그러나 객체 그 자체는 힙 영역에 적재된다는 것을 꼭 기억해야 한다. 그런데 자바는 객체에 메모리를 적재하는 과정에서 8바이트 단위로 저장한다고 한다. 예를 들어서 int 타입의 멤버 변수가 4개 있고 boolean형 변수가 1개있어서 17 byte라고 하더라도, 메모리 공간을 24바이트를 할당한다.
추가적으로 String 형식의 객체는 저장 방식이 두 가지가 있다. 위처럼 String 객체를 생성해서 힙 영역에 저장하는 방식과, 리터럴 방식을 사용하여 String constant pool 에 저장하는 방식 이렇게 두 가지이다.
String s = "sss"; // 리터럴 방식
String ab = new String("sss"); // 객체 생성 방식
String a = "sss" // String constant pool에 저장된 sss 가져옴, 따라서 새로운 객체 생성X
String ac = new String("sss"); // 객체를 새로 생성 함 힙 영역에 하나의 객체 추가로 생성
컴퓨터 구조 적으로 생각하더라도 하나의 객체가 어떠한 간접 주소 방식으로 메모리 주소를 가리키는 방식이기 때문에, 하나의 객체는 한 번에 힙 영역에 있는 두 개의 객체 메모리 주소에 접근을 할 수 가 없다. 그러나 다음처럼 2개의 객체가 한 메모리 영역에 접근하는 것은 가능하다.
public class main
{
// tip: arguments are passed via the field below this editor
public static void main(String[] args)
{
Test t1 = new Test();
Test t2 = new Test();
t1.a = 3;
t2.a = 4;
System.out.println("t1 출력");
t1.print();
System.out.println("t2 출력");
t2.print();
t2 = t1;
System.out.println("t1의 참조 주소를 t2에 대입 : t2 는 t1 을 가리키게 됨");
System.out.println("t1 출력");
t1.print();
System.out.println("t2 출력");
t2.print();
}
}
class Test {
int a;
public void print(){
System.out.println("a 의 값 : " + a + " 를 출력");
}
}
결과
t1 출력
a 의 값 : 3 를 출력
t2 출력
a 의 값 : 4 를 출력
t1의 참조 주소를 t2에 대입 : t2 는 t1 을 가리키게 됨
t1 출력
a 의 값 : 3 를 출력
t2 출력
a 의 값 : 3 를 출력
다음처럼 참조변수의 주소를 다른 참조변수에 대입함으로써 서로 같은 객체를 가리키도록 설정할 수 있다.
추가로 궁금했던 부분은 객체가 담기는 주소를 직접 C언어처럼 출력함으로써 확인해보고 싶었으나, 자바는 Garbage Collector를 사용함으로써, 사용하지 않는 메모리 주소는 계속 비우면서 동적으로 메모리 주소가 바뀌기 때문에 주소가 고정되어있지 않아 확인하는데 도움이 되지 않는다고 한다.
객체 배열
객체의 배열은 어렵게 생각하지 않고 간단하게 생각하면 참조 변수의 배열이다. 기존의 기본형 타입의 변수들은 동일한 자료형의 변수들이 저장되는 공간이였다면, 참조주소를 가지고 있는 Object의 배열은 각 배열이 참조변수의 주소를 가지고 있는 것이다.
int[] arr = new int[3];
System.out.print(arr[0]); // 기본 값인 0출력
Test[] arr2 = new Test[3];
System.out.print(arr2[0]); // 객체가 생성되지 않았기 때문에 주소 값이 없는 null이 출력
예외가 발생한다고 해서 NullPointerException이 발생하는건가? 라고 생각을 했다. 그러나 NullPointerException이 발생하려면 null을 가지고 있는 객체에서 어떤 것을 시도를 해야 발생하는데 그저 출력만 하는건데 NullPointerException이 발생할 리가 없었다. 아니나 다를까 테스트 코드를 적어봤더니 null이 출력됐다.
클래스의 정의
클래스의 발전을 살펴보자. 기존에 우리는 변수라는 하나의 데이터를 저장할 수 있는 메모리 공간에 값을 저장해서 사용했다. 그러나 변수를 여러 군데 담고 싶은 구역이 생겼고, 이를 배열로 활용해서 해내었다. 그러나 배열의 단점은 같은 자료형만 담을 수 있다는 문제가 있었다. 이를 해결하기 위해 구조체가 탄생했고, 구조체에 다른 자료형을 가진 변수들을 저장할 수 있었다. 하지만 그 변수들을 이용해서 메소드를 만들기 위해서는 계속 빼내서 써야하는 메소드의 재사용을 할 수 없는 문제가 있었는데, 이를 해결하기 위해서 마지막으로 클래스가 탄생했다. 클래스에는 다른 자료형의 변수를 선언할 수 있고, 클래스 내부의 기능을 하는 메소드까지 생성할 수 있었다. 정리하면 다음과 같이 이해하면 되겠다.
- 변수 : 하나의 데이터를 저장할 수 있는 공간
- 배열 : 같은 종류의 여러 데이터를 하나로 저장할 수 있는 공간
- 구조체 : 서로 관련된 여러 데이터(종류 관계X)를 하나로 저장할 수 있는 공간
- 클래스 : 데이터와 함수의 결합(구조체 + 함수)
선언위치에 따른 변수의 종류
처음에 공부할 때 가장 어려웠던 파트였다. 클래스 변수, 지역 변수, 인스턴스 변수 아무래도 인스턴스와 클래스 그 차이를 이해하지 못하고 지역 변수의 범위가 어디까지 인지 아마 개념 공부를 러프하게 한 것이 문제였지 않을까 생각한다.
class Variables {
int iv; // 인스턴스 변수
static int cv; // 클래스 변수(static변수, 공유변수)
void method() {
int lv = 0; // 지역 변수
}
}
위 처럼 공간은 저렇게 되어있다. 메서드 영역에서 선언된 변수를 지역 변수라고 하고 그 외에 클래스 공간에 선언된 변수중에 static이 달려있지 않은 변수를 인스턴스 변수, 왜냐하면 객체가 가지고 있는 변수 → 클래스가 생성한 객체를 인스턴스 라고 부르기로 한 걸 기억하자. 마지막으로 클래스 변수이다. 클래스 변수를 정적 변수라고 부르기도 하는데 그 이유는 앞에 static이 있기 때문이고, 클래스가 직접 가지고 있고 클래스를 통해 직접 접근이 가능한 변수라고 해서 클래스 변수라고 기억하는 것도 도움이 될 것 같다.
해당 부분을 이해하기 전에, 클래스 변수가 언제 담기는지에 대한 시점부터 미리 아는 것이 좋을 것 같은데, 지역변수는 메소드가 실행되는 시점에 해당 메소드가 콜스택 영역에 담기고 그와 동시에 지역변수도 콜스택에 담기는 형식이다.
그와 달리 정적 변수는 애플리케이션이 메모리에 올라가는 순간에 메소드 영역에 저장된다.
마지막으로 인스턴스 변수는 객체는 힙 영역에 저장되어야 하고 클래스에 있는 인스턴스 변수들은 객체가 생성될 때 생성이 되니, 힙 영역에 저장이 되어있다고 보는게 맞을 것 같다.
참고로 클래스 변수는 클래스를 공유하는 모든 객체들이 공유하는 변수이기 때문에, 선언하는데 있어서 굉장히 조심해야 된다. 예를 한 가지 들어보자.
public class main
{
public static void main(String[] args)
{
Monitor lg = new Monitor();
lg.name = "LG223U";
lg.brand = "LG";
Monitor.weight = 200;
Monitor.height = 100;
lg.print();
Monitor samsung = new Monitor();
samsung.name = "SS360U";
samsung.brand = "samsung";
samsung.print();
}
}
class Monitor {
static int weight = 300;
static int height = 200;
String name;
String brand;
public void print() {
System.out.println("모니터가" + calcInch() + "인치 이고, 브랜드와 이름은" + this.brand + "," + this.name + "입니다.");
}
private int calcInch() {
return (int)Math.sqrt(Math.pow(Monitor.weight, 2) + Math.pow(Monitor.height, 2));
}
}
모니터가223인치 이고, 브랜드와 이름은LG,LG223U입니다.
모니터가223인치 이고, 브랜드와 이름은samsung,SS360U입니다.
기본적으로 모니터의 기본 사이즈는 360인치로 나온다고 가정하자. 근데 LG쪽에서 이번에는 223인치짜리 모니터를 내보내달라고 해서 static 변수를 변경하였다. 그 상태로 새로운 default 사이즈로 새로운 삼성 모니터를 만들려고 해서 만들었는데, 이럴수가 삼성의 모니터 사이즈도 223인치로 변경이 되었다. 이렇듯 static 변수는 클래스를 통해서 값을 공유하는 변수이기 때문에 섣불리 수정을 하면 안된다. 강의에서는 참조변수.클래스변수로 값을 설정하는 모습을 보여주기도 하였으나, 현재는 안된다.
메서드란?
- 어떠한 명령어들을 묶어놓은 것이다.
- 값을 받아서 처리하고 결과를 반환한다.
// 여기서 선언부에 있는 int는 반환타입
// add는 함수명
// 괄호안에 있는 변수는 매개변수이다. 매개변수 또한 지역변수이다.
// 참고로 매개변수는 값을 전달하는 매개체 라고 해서 매개변수라고 한다고 한다.
int add(int x, int y){
// 여기는 구현부이다.
int result = x + y;
return result;
// 여기까지
}
여기서 헷갈릴 수 잇는 부분은 어떤 함수를 사용할 때 전달하는 값을 Arguments(인자) 라고 부르고, 함수에 필요한 변수가 선언되어있는 것을 parameter(매개변수) 라고 부른다. 인자와 매개변수가 무엇인지 헷갈리지 말도록 하자.
참고로 메소드는 클래스 영역에서만 정의가 가능하다. 메소드 내부에서 메소드를 선언하려고 하지 말자.
메소드의 장점
메소드의 장점은 필수적으로 알아두어야 한다.
- 재사용이 용이하다.
- 코드의 중복을 줄일 수 있다.
- 함수만 쳐다보면 되기 때문에 관리가 쉽다.
- 코드가 간결해서 이해하기 쉬워진다.
메소드의 호출
메소드의 호출이 어떻게 이루어지는지 확실히 이해해야 나중에 재귀함수도 확실하게 이해를 할 수 있다.
public class Main {
public static void main(String[] args) {
int result = add(3, 5);
}
public static int add(int a, int b){
return a + b;
}
}
다음 코드를 살펴보자. 먼저 흐름에 따라 메소드를 호출하는 코드가 실행이 되고 인자가 매개 변수에 전달이된다. 지역변수인 매개변수를 이용하여 함수 내부에서 어떠한 연산이 이루어지고 return을 통해 반환이 된다. 반환이 된 값은 메소드가 호출되는 부분에서 반환된 값으로 전환되고 해당 반환값이 필요한 변수에 저장이 된다.
그러나 반환타입이 void인 경우에도 return을 사용할 수 있는데 void 타입에서의 return은 값을 반환하는 것이 아닌 그저 메소드를 종료한다는 것을 의미한다. 또한 다른 타입은 무조건 return을 해줘야하는데 반해 void는 return을 생략할 수 있다. 컴파일러가 void 타입의 메소드라면 자동으로 return을 추가해주기 때문이다.
여기서 중요한 것은 메소드는 작업을 마치면 작업을 호출한 곳으로 되돌아간다는 것이다. 이것을 이후에 콜스택이라는 것을 배우게 되는데 거기서 메소드의 흐름을 자세히 살펴볼 수 있다.
위에서 한 얘기를 다음과 같이 그림으로 보면 더 정리가 잘되기때문에 그림으로 보고 눈에 익히도록 하자.
콜 스택
메서드 수행에 필요한 메모리가 제공되는 공간 메서드가 호출되면 호출스택에 메모리가 할당, 종료되면 스택에서 빠지게 된다.
다음 그림을 살펴보자. 먼저 메인 메소드가 실행되고 메인 메소드 내부에 있는 println 메소드가 실행이 되었다. 메인 메소드의 Hello 출력이 완료되고 return이 되었다.
public void println(Object x) {
String s = String.valueOf(x);
synchronized (this) {
print(s);
newLine();
}
}
println의 메소드는 다음과 같은 것을 볼 수 있다. 실제로는 println 메소드가 호출이 되고 내부에 print() 메소드가 호출되고 print() 메소드 내부에 있는 메소드가 차례차례로 콜스택에 쌓여서 마지막으로 return이 되면 하나 하나 차례대로 빠져서 println이 리턴되고 main이 리턴되는 형식인 것이다. 이렇게 메소드는 메소드를 호출하고 마지막 메소드가 리턴됐을 때 하나 하나 빠진다.
기본형 매개변수
- 기본형 매개변수는 주소 값을 가진게 아닌 데이터 그 자체를 가지고 있기 때문에, 매개변수 데이터가 변형되지 않는다, 즉 매개변수의 값을 읽기만 할 수 있다.
참조형 매개변수
- 그러나 참조형 매개변수에 참조 변수는 그 데이터로 주소값을 보내준다. 해당 주소 값에는 기존에 참조변수가 가리키고 있는 참조주소의 데이터들이 담겨있기 때문에 참조 매개변수에 있는 값을 수정하게 되면 자연스럽게 기존의 참조 주소에 있는 값도 바뀌기 때문에 변형이 일어날 수 있다.
따라서 기본형 매개변수는 읽기만 가능하고 참조형 매개변수는 읽기와 쓰기가 둘 다 가능하다.
static 메서드와 인스턴스 메서드
class MyMath2 {
long a, b; // 인스턴스 변수
long add() { // 인스턴스 메서드
return a + b;
}
// 클래스 메서드에 있는 매개변수 a, b는 인스턴스 변수 a, b와 다르다.
static long add(long a, long b) { // 클래스 메서드(static 메서드)
return a + b;
}
}
다음과 같이 구성된 클래스를 구성하였다. 먼저 인스턴스 메서드 부터 살펴보자.
1. 인스턴스 메서드
인스턴스 메서드는 인스턴스 변수와 같이 인스턴스가 생성될 때 같이 생성된다. 사용을 하기 위해서는 기존에 배웠던 것 처럼 참조변수.메서드이름() 으로 호출이 가능하다.
2.static 메서드(클래스 메서드)
static 변수를 클래스 변수라고 했던 것 처럼 static 메서드도 클래스 메서드라고 부른다. 클래스 메서드는 클래스 변수와 같이 애플리케이션이 메모리에 올라갈 때 동시에 메소드 영역에 적재되며 클래스이름.메서드이름() 으로 호출이 가능하다. 단, 인스턴스 메서드와 달리 주의해야 할 점이 있다.
— 굉장히 중요 — 인스턴스 메서드는 객체가 생성되는 시점에 생긴다. 객체가 생성되는 시점에 인스턴스 변수도 생성되기 때문에 인스턴스 메서드는 클래스 내부에 있는 인스턴스 변수의 존재를 알 수 있지만, static 메서드는 객체를 생성하기 전에도 사용이 가능하다. 클래스의 인스턴스 변수는 클래스를 인스턴스화 해서 인스턴스가 생성되어야 메모리에 적재되는데 static 메서드 입장에서는 아직 메모리에 적재되지 않은 인스턴스 변수를 당연하게도 참조할 수 없다. 그럼 인스턴스 변수와 메서드는 클래스 변수, 메서드를 사용할 수 있을까? 당연하게도 가능하다. 객체가 생성되기 전에 우선 애플리케이션이 메모리에 올라와있어야 하고, 그 시점에 클래스 변수와 메서드가 생성되기 때문이다. 결론, 클래스 메서드는 인스턴스 변수, 메서드와 관련한 작업을 해서는 안되며 메서드 내부에서 인스턴스 변수를 가지고 무언가를 할 수 없다. 이제, 해당 클래스를 사용하는 것을 예시를 한 번 살펴보자.
class MathTest2 {
System.out.println(MyMath2.add(200L, 100L); // 클래스 메서드 호출
MyMath2 mm = new MyMath2(); // 인스턴스 생성
mm.a = 200L;
mm.b = 100L;
System.out.println(mm.add()); // 인스턴스 메서드 호출
}
둘은 물론 값은 값을 반환하지만, 차이를 살펴보면 MyMath2에 있는 static 메소드를 사용할 때에는 객체를 생성하지 않고 사용할 수 있었고, 인스턴스 메서드는 객체를 생성한 후 사용한 것을 볼 수 있다. 이 코드를 짜면서 머리 속에 메모리가 적재되는 흐름이 떠오르면 가장 베스트 일 것 같다.
가장 중요한 것은 그럼 static을 언제 쓰는 것이 좋을까? 공통 속성을 가지고 있고, 인스턴스 멤버를 사용하지 않는 변수에 static을 사용해야 한다. 그러나 static은 클래스 내부에서 공유되는 변수이기때문에, 대부분은 상수 전용 클래스를 만들고 상수 전용 클래스에서 static으로 상수 변수를 만들어서 사용하는 것이 대부분이다.
오버로딩
오버로딩(overloading) 이란 한 클래스 안에 같은 이름의 메서드를 여러 개 정의하는 것이다.
오버로딩이 성립하기 위해서는 다음과 같은 두 가지 조건을 충족시켜야 한다.
- 메서드의 이름이 같아야 한다.
- 매개변수의 개수 또는 타입이 달라야 한다.
여기서 예전에 헷갈렸던 부분은 매개변수의 반환타입은 전~혀 아무 관계가 없다.
int add(int x, int y);
String add(int x, int y); // 오류 발생
위 처럼 반환타입만 다르면 메소드를 중복 정의 한 것으로 안다.
생성자
생성자는 어렵게 생각할 것 없이 인스턴스가 생성될 때마다 호출되는 ‘인스턴스 초기화 메서드’ 이다. 여기서 주목해야 할 부분은 인스턴스 초기화 메서드라는 것이다. 생성자가 왜 생겼는지에 대해서 부터 주목해야 될 것 같다. 이를 알면 이해하기 쉬울 것이다.
Time t = new Time();
t.hour = 12;
t.minute = 34;
t.second = 56;
이처럼 하나의 객체를 생성할 때마다 그 객체의 인스턴스 변수를 초기화 하기 위해 값을 대입하기란 너무 귀찮은 작업이다. 지금은 인스턴스 변수가 3개 밖에 없어서 괜찮지만, 20개라고 생각했을 때 20개의 객체를 생성하면 무려 400줄의 코드를 입력해야한다. for문 이용할 수도 있지만 요지는 그 만큼 귀찮은 작업이라는 것이다.
하지만 생성자를 사용하면 다음과 같이 작성할 수 있다.
Time t = new Time(12, 34, 56);
위에는 4줄인데에 반해, 생성자를 사용하면 단 한 줄 만에 객체가 생성되는 모습을 볼 수 있다.
생성자와 관련된 객체의 특징은 3가지가 있다.
- 생성자의 이름은 클래스 이름과 같아야 한다.
- 생성자에는 return 이 없다. (그렇다고 void를 붙이면 안된다)
- 모든 클래스는 반드시 생성자를 가져야 한다.
- 위에서 배운 오버로딩이 생성자에게 적용된다. 한 가지 예를 살펴보자.
Class Card {
String kind;
int number;
Card() {
// 인스턴스 초기화
}
Card(String kind, int number){
// 인스턴스 초기화
}
}
이 처럼 오버로딩이 가능하다는 말이다.
세 번째 특징에서 다음과 같은 의문을 가질 수 있다.
“나는 클래스를 생성하면서 생성자라는 걸 만들어본적이 없는데 잘못된 특징 아닌가?”
이는 컴파일러가 클래스 내부에 단 한개의 생성자도 존재하지 않으면 기본 생성자를 만들어주기 때문이다. 다음 코드를 보자.
class Food {
String name;
int price;
}
위와 같은 클래스가 선언되어 있을 때, 컴파일러는 클래스를 확인하고 다음과 같은 생성자를 추가해준다. Food(){} 위에서 본 매개변수가 없는 생성자이다. 이를 기본 생성자라고 부른다. 꼭 잊어버리지 말아야 할 점은 위에서 얘기했듯이 클래스에 단 한 개의 생성자도 없어야 한다는 것이다. 다음 예시를 살펴보자.
class Food {
String name;
int price;
public Food(String name, int price){
this.name = name;
this.price = price;
}
}
public class Main {
Food food = new Food(); // 에러 발생
}
위 처럼 매개변수가 들어있는 생성자를 하나 만들었고, 기본 생성자를 이용해서 객체를 생성하려고 하였으나, 매개변수가 있는 생성자가 있기 때문에 컴파일러는 자동으로 기본 생성자를 만들지 않았다. 때문에 다음과 같은 오류가 발생한다. error: constructor Food in class Food cannot be applied to given types;
이를 해결하는 법은 오버로딩을 사용하면 된다.
class Food {
String name;
int price;
public Food(){}
public Food(String name, int price){
this.name = name;
this.price = price;
}
}
public class Main {
Food food = new Food(); // 객체 생성 완료
}
생성자 this()
일단 공부를 어느정도 한 상태에서 보면 this() 와 this.인스턴스변수 에 대한 차이에 대해서 자세히 모를 수 있다. 우선 결론부터 얘기하자면 생성자 this()와 참조변수 this는 아예 다른 내용이다.
**생성자 this()**는 다른 생성자에서 생성자를 호출 할 때 사용 할 수 있는 메서드이다.
class Car {
String color;
String gearType;
int door;
Car(){
this("red", "auto", 4); // 매개변수 3개가 있는 생성자 호출
}
Car(String color, String gearType, int door){
this.color = color;
this.gearType = gearType;
this.door = door;
}
}
위와 같은 예시로 사용할 수 있다.
단, 조건이 있는데 다른 생성자에서 호출시에 첫 줄에서만 사용가능 하다는 것이다.
class Car {
String color;
String gearType;
int door;
Car(){
door = 3;
this("red", "auto", 4); // 에러 발생
}
Car(String color, String gearType, int door){
this.color = color;
this.gearType = gearType;
this.door = door;
}
}
error: call to this must be first statement in constructor
친절하게도 컴파일러가 첫줄에 와야한다고 설명까지 해준다.
참조변수 this
**생성자 this()**와 다른 참조변수 this는 메모리 구조를 이해하면 간단하다. 참조변수 this는 객체 자신의 메모리 주소를 가리키는 변수이다.
따라서 생성자를 포함한 인스턴스 메서드에서 사용이 가능하다. 주로 위에서 사용했던 것처럼 지역변수와 인스턴스 변수를 구별할 때 많이 사용한다. 물론, 매개변수와 지역변수를 구별하지 않아도 될 상황에는 this를 생략해도 되지만, 대부분은 this를 사용하는 것을 지향한다.
예전에도 얘기했지만 클래스 메서드에서는 객체가 초기화되어있지 않은 상태이기 때문에 this 변수를 사용할 수 없다. 당연스럽게도 객체의 주소가 없는 상태이기 때문이다.
변수의 초기화
가장 중요한 부분이라고 생각한다. 그냥 아무 생각없이 넘어가지 말고, 자세히 보고 어떤 과정으로 어떤 순서로 변수가 초기화 되는지 꼭 기억해야 한다.
- 지역변수는 사용하기 전에 명시적으로 초기화를 해줘야 한다.
- 이런 일이 발생하는 것은 지역변수는 메소드가 호출될 때마다 메소드가 콜 스택에 담기게 되는데 이때 마다 변수를 기본값을 찾아서 호출 한다면 메모리의 낭비가 발생한다. 콜스택이 종료되게 되면 가비지 콜렉터에 의해서 메소드는 메모리에서 사라지게 될 테니 말이다.
- 인스턴스 변수와 클래스 변수는 자동 초기화가 된다.
- 지역 변수와 달리 인스턴스 변수와 클래스 변수는 자동으로 초기화가 된다. 가령 인스턴스 변수 혹은 클래스 변수가 참조 변수라면 null로 초기화가 되는 것이다.
그럼 이제 초기화의 종류를 살펴보자. 초기화의 종류는 크게 3가지로 분류한다.
(기본값)
- 명시적 초기화
- 초기화 블럭
- 생성자
우선 위에서 얘기했듯이 초기화를 하지 않으면 인스턴스 변수 및 클래스 변수는 기본값으로 초기화가 되는데, 이는 초기화라고 얘기하기가 애매해서 제외했다.
먼저 명시적 초기화는 우리가 자주 사용하는 a = 1; 처럼 직접 대입 연산자를 통해서 명시해주는 것을 명시적 초기화라고 한다.
두번째로 초기화 블럭이 있는데, 초기화 블럭은 좀 생소할 수도 있다. 자주 사용하지 않기 때문에 눈에 많이 안 들어올것인데, 패턴을 한 번 살펴보자.
class Car {
String color;
String gearType;
int door;
static int[] func = new int[4];
{
color = "blue";
gearType = "auto";
door = 2;
}
static {
for(int i = 0; i < 4; i++) {
func[i] = (int)(Math.random() * 10) + 1;
}
}
Car(){
door = 3;
this("red", "auto", 4); // 에러 발생
}
Car(String color, String gearType, int door){
this.color = color;
this.gearType = gearType;
this.door = door;
}
}
위의 예시를 보고 다음과 같은 생각이 들면 정말 좋겠다.
“어? 일반적인 초기화 블럭은 생성자랑 비슷하지 않나요? 클래스 변수는 초기화를 해줄 메서드가 없지만 인스턴스 변수는 생성자라는 메서드가 있잖아요!”
그렇다. 그런 이유로 인스턴스 변수를 초기화하는 초기화 블럭은 잘 사용하지 않고 종종 복잡한 static 변수를 초기화를 해야 될 때, static 초기화 블럭을 사용해서 초기화를 한다.
그럼 이제 초기화가 되는 순서만 파악하면 전체적인 변수의 초기화를 이해 할 수 있을 것이다.
- 클래스 초기화
- 기본값
- 명시적 초기화
- 클래스 초기화 블럭
- 인스턴스 초기화
- 기본값
- 명시적 초기화
- 초기화 블럭
- 생성자
이와 같은 순서대로 초기화가 되며, 예전에도 배웠지만 클래스 초기화는 애플리케이션이 메모리에 올라가는 순간 초기화가 되기 때문에, 한 번 초기화 되고 이후로 초기화되지 않지만 인스턴스 초기화는 객체를 생성할 때마다 초기화가 되기 때문에 이를 유의해야 한다.
'Language > Java' 카테고리의 다른 글
7-2 참조변수 super, 생성자 super() (0) | 2023.03.22 |
---|---|
7-1 상속과 오버라이딩 (0) | 2023.03.22 |
3. 연산자 (0) | 2023.03.21 |
2. 변수 (0) | 2023.03.21 |
1. 자바 (0) | 2023.03.21 |