※ Java의 정석 기초편을 공부하며 기억하고 싶은 특이점들만 기록, 정리해 놓은 노트이다.
※ 노션 페이지를 복사해오다 보니 식과 포맷이 엉성한 곳이 있다.
변수의 초기화
: 변수를 선언하고 처음으로 값을 저장하는 것.
멤버변수와 배열은 컴파일러가 초기화를 해주지만, 지역변수는 내가 반드시 초기화를 해주고 사용해야 한다.
각 타입의 기본값(default value)은:
자료형 | 기본값 |
boolean | false |
char | '\u0000’ |
byte, short, int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d (또는 0.0) |
참조형 | null |
멤버변수의 초기화 법칙
- 클래스 변수가 인스턴스 변수보다 먼저 초기화된다.
- 초기화가 덮어씌워지는 순서는:💡 (기본값으로)자동 초기화 → 명시적 초기화 → 초기화 블록 → 생성자
- 명시적 초기화(Explicit initialization) : 변수를 선언함과 동시에 초기화하는 것. 간단히 원시타입이나 참조타입 하나를 넣어줄 때.
class Car {
int door = 4; // 기본형 변수 초기화 예시
Engine 3 = new Engine(); // 참조형 변수 초기화 예시
}
- 초기화 블록(Initialization block) : 클래스 초기화 블록 static {...} 이나 인스턴스 초기화 블록 {...}에 초기화할 내용을 넣어주는 것. 배열이나 예외처리가 필요한 초기화 같이 더 복잡한 초기화를 수행할 때.
- 초기화가 실행되는 순서 예시:
// 예시 1
class Car {
static int[] arr = new int[10];
static {
for(int i = 0; i < arr.length; i++) {
// 1과 10 사이의 임의의 값을 배열 arr에 저장한다.
arr[i] = (int)(Math.random()*10) + 1;
}
}
public static void main(String[] args) {
...
}
}
// 예시 2
class Car {
static {
System.out.println("static {우선순위 3번}");
}
{
System.out.println("{우선순위 3.5번}");
}
public Car() {
System.out.println("생성자: 우선순위 4번");
}
public static void main(String[] args) {
System.out.println("인스턴스 Car 1 생성함: ");
Car c1 = new Car();
System.out.println("인스턴스 Car 2 생성함: ");
Car c2 = new Car();
}
}
// 결과 (순서대로):
static {우선순위 3번}
// => 클래스 Car가 메모리에 로딩될 때 **클래스 초기화 블록**이 가장 먼저 수행됨.
인스턴스 Car 1 생성함:
{우선순위 3.5번}
생성자: 우선순위 4번
// => 인스턴스 생성시에는 **인스턴스 초기화 블록**이 **생성자**보다 먼저 수행됨.
인스턴스 Car 2 생성함:
{우선순위 3.5번}
생성자: 우선순위 4번
this() : 자기 자신 클래스의 다른 생성자를 호출하는 생성자
조건이 두 가지 있다.
- 생성자의 이름으로 클래스 이름 대신 this를 사용해야 한다.
- this()를 호출할 때는 반드시 첫 줄에서 호출해야 한다.
Car(String color) {
door = 5;
Car(color, "auto", 4); // this(color...); 여야 함.
}
this : 인스턴스 객체 자신을 가리키는 참조변수
- this에는 인스턴스 자신의 주소가 저장되어 있다. 즉, this는 인스턴스 자신을 가리키는 참조변수이다.
- : 참조변수를 통해 인스턴스의 멤버에 접근할 수 있는 것처럼, ‘this.변수명’으로 인스턴스변수에 접근할 수 있다.
- this를 사용할 수 있는 것은 인스턴스 멤버뿐이다. 즉 static메서드에서는 this를 사용할 수 없다.
- : static메서드가 호출되었을 때 인스턴스가 존재하지 않을 수도 있어서 그렇다. 인스턴스가 존재하지 않는데 ‘인스턴스 객체 자신을 가리키는’ 참조변수 this는 성립할 수 없는 것이다.
- 모든 인스턴스 메서드(생성자 포함)에는 this가 지역변수로 숨겨진 채로 존재한다.
- : 그래서 따로 선언하지 않고 막 쓸 수 있나보다.
기본 생성자(Default constructor)
모든 클래스에는 반드시 하나 이상의 생성자가 정의되어 있어야 한다.
컴파일 할 때 소스파일(.java)의 클래스에 생성자가 하나도 정의되어 있지 않은 경우에(만) 컴파일러가 자동적으로 기본 생성자를 추가하여 컴파일 한다.
기본 생성자는 다음과 같은 것이다:
// 기본 생성자 예시
Car() {};
클래스의 접근 제어자가 public인 경우에는 기본 생성자도 public Car() {}가 된다.
생성자
기본을 잘 짚고 넘어가자:
- 생성자는 인스턴스가 생성될 때 호출되는 ‘인스턴스 초기화 메서드’이다. (멤버변수 초기화 법칙에서 생성자가 가장 마지막 순위인 게 이해가 된다.)
- 생성자의 이름은 클래스의 이름과 같아야 한다.
- 생성자도 메소드이지만 리턴 값이 없다.
- 리턴 값이 없으니 void가 되어야 하지만 모든 생성자가 그러하니 생략이 가능하게 했다.
컴파일 때 컴파일러가 자동으로 해주는 일:
- 멤버변수를 기본값으로 초기화한다. (초기화가 안됐을 경우)
- 생성자가 하나도 없는 클래스에 기본 생성자를 추가해서 컴파일한다.
- void 메서드의 마지막에 return;을 추가한다.
- 인터페이스 안의 멤버변수와 메소드의 생략된 제어자를 채워 넣어 모두 public static final과 public abstract로 만든다.
변수의 종류는 모두 세 가지이다.
- 클래스 변수
- 인스턴스 변수
- 지역변수: 메소드 내에 선언된 지역변수는 메소드가 종료되면 소멸되고, 반복문 등의 블록 내에 선언된 지역변수는 그 블록{}을 벗어나면 소멸된다.
- : 메소드나 생성자 안에 직접 선언된 변수와 매개변수, 그리고 모든 인스턴스 메소드 안에 기본으로 딸려오는 this 참조변수, 초기화 블록 내부의 변수, 반복문에 선언된 변수 등등
Car car1 = new Car();의 실체..!
이 구문이 실제로 어떻게 수행되냐면:
- Car 타입의 참조변수 car1을 선언함. 메모리에 참조변수 car1을 위한 공간이 마련되고, 그 공간에는 이후에 주소가 담길 것이다.
- 연산자 new에 의해 Car 클래스의 인스턴스가 메모리의 빈 공간에 생성됨.
- 대입연산자 =에 의해 방금 만든 Car 인스턴스의 주소가 참조변수 car1에 저장됨.
- 이제 참조변수 car1으로 인스턴스에 언제든 접근할 수 있게 되었다.예를 들면 Car[] arr = new Car[3];는 세 칸짜리 배열 인스턴스를 만든 것뿐이고,
- Car[] arr = { new Car(), new Car(), new Car() }; 이렇게 해야 실제로 배열 한 칸 한 칸마다 Car 인스턴스가 만들어져 주소가 저장된 것이다.
- 기억할 것은 new가 나왔을 때만이 실제로 인스턴스가 만들어진다는 사실이다.
Static을 붙이는 간단한 룰
- 멤버변수의 경우: 모든 인스턴스에 공통된 값을 유지해야 하는 변수는 static을 붙여준다.
- 메소드의 경우: 인스턴스가 만들어지지 않았어도 언제든 불려나갈 수 있다는 것만 기억하면 된다. 즉, 인스턴스 변수나 인스턴스 메소드를 사용하지 않는 메소드의 경우에만 static을 붙일 것을 고려한다.
- (인스턴스 변수나 인스턴스 메서드에서는 static 멤버들을 얼마든지 사용 가능하다. 인스턴스 멤버들이 존재하는 시점에 static 멤버들은 언제나 먼저 메모리에 존재할 것이기 때문에.)
- static 메소드는 호출된 메소드를 찾는 과정이 필요 없어서 빠르기 면에서 성능이 향상된다.
- 기본 룰은, 같은 클래스에 속한 멤버들 간에는 별도의 인스턴스를 생성하지 않고도 서로 호출해대는 게 가능하다는 것이다. 단, 앞서 말한 것처럼 클래스 멤버가 인스턴스 멤버를 호출하는 것만 안하면 된다.
호출 스택(Call stack)
이 파트는 모든 문장이 중요하기 때문에 최대한 그대로 적겠다.
호출스택은 메소드의 작업에 필요한 메모리 공간을 제공한다. 메소드가 호출되면, 호출스택에 호출된 메소드를 위한 메모리가 할당되며, 이 메모리는 메소드가 작업을 수행하는 동안 지역변수(매개변수 포함)들과 연산의 중간 결과 등을 저장하는데 사용된다. 그리고 메소드가 작업을 마치면 할당되었던 메모리 공간은 반환되어 비워진다.
⇒ 지역변수가 왜 메소드 안에서만 생존이 가능한지가 설명된다. 한 메소드가 작업을 마치면, 사용하던 공간도 싹 비워지니까.
⇒ 메소드 별로 같은 이름의 지역변수를 사용해도 괜찮은 이유도 설명된다. 메소드 별로 공간이 따로 할당되니까.
// 호출스택 예시:
class CallStack {
public static void main(String[] args) {
int a = 0;
firstMethod();
}
public static fisrtMethod() {
secondMethod();
}
public static secondMethod() {
System.out.println("Hello");
int b = 0;
}
}
1 ~ 2. 먼저 JVM에 의해서 main메소드가 호출됨으로써 프로그램이 시작된다. 이 때, 호출스택에는 main메소드를 위한 메모리공간이 할당되고 main메소드의 코드가 수행되기 시작한다.
3. main메소드에서 firstMethod()를 호출한 상태이다. 아직 main메소드가 끝난 것은 아니므로 main메소드는 호출스택에 대기상태로 남아있고 firstMethod()의 수행이 시작된다.
4 ~ 5. 마찬가지로 firstMethod메소드에서 secondMethod메소드를, secondMethod메소드에서 println()을 호출한다. 다들 호출스택에 대기상태로 남아있는 상태이다. println메소드에 의해 ‘Hello’가 화면에 출력된다.
6. println의 수행이 완료되어 호출스택에서 사라지고 자신을 호출한 secondMethod메소드로 되돌아간다. 대기 중이던 secondMethod메소드는 prinln을 호출한 이후부터 수행을 재개한다.
7 ~ 8. 차례로 secondMethod메소드와 firstMethod메소드가 수행 완료되어 호출스택에서 사라지고 대기 상태에 있던 자신의 호출 메소드(caller)로 되돌아가기를 반복한다.
9. main메소드에도 더 이상 수행할 코드가 없으므로 종료되어, 호출스택은 완전히 비워지게 되고 프로그램은 종료된다.
여기서 호출스택의 특징을 짚어낼 수 있다:
- 메소드가 호출되면 수행에 필요한 만큼의 메모리를 스택에 할당받는다.
- 메소드가 수행을 마치고나면 사용했던 메모리를 반환하고 스택에서 제거된다.
- 호출스택의 제일 위에 있는 메소드가 현재 실행 중인 메소드이다.
- 아래에 있는 메소드가 바로 위의 메소드를 호출한 메소드이다.
- 호출스택이 완전히 비워졌을 때가 프로그램이 종료되는 시점이다(?)
- 메소드마다 자기 공간을 할당 받으니 메소드끼리 같은 이름의 지역변수가 있어도 문제 없다.
- 메소드가 자기 일을 다하면 공간이 비워지기 때문에 지역변수의 수명이 딱 메소드 내인 것이다.
오버로딩(Overloading)
기본 확실히 정리:
- 메소드를 오버로딩 하는 것이다.
- 메소드 선언부는: 제어자 반환타입 메소드이름(매개변수 1, 매개변수 2...) 여기까지를 말한다.
- 메소드의 시그니처는: 메소드이름과 매개변수들 까지를 말한다.
- 메소드 오버로딩은: 메소드이름이 같은데 매개변수의 개수나 타입, 순서가 다른 메소드를 같은 클래스 내에 정의하는 것을 말한다. 이 때 반환타입은 관계가 없다. 다른 말로 하면, 같은 시그니처로 매개변수만 다르게 해서 메소드를 여럿 만드는 것을 뜻한다.
- 메소드가 구별되는 방식: “메소드가 호출 될 때 어떤 메소드를 부르는 건지 특정할 수 있어야 한다”가 기본 원칙. 그래서 메소드이름과 매개변수의 개수, 순서(타입)이 메소드를 구별하는 기준, 즉 메소드의 시그니처가 되는 것이다.
- ⇒ 예를 들면 add(3, 3L)과 add(3L, 3)은 서로 다른 메소드로 취급되고 어떤 메소드를 불러와야 할지 컴파일러가 잘 안다. 전자는 add(int a, long b)메소드임을, 후자는 add(long a, int b)메소드임을 유추하고 구분할 수 있다.
- 결국 public이냐 아니냐 등도 메소드의 시그니처가 달라지는 데는 영향을 미치지 않으므로, 접근제어자만 다르게 해서는 오버로딩이 성립되지 않는다.
생성자는 static이 되면 절대 안된다.
인스턴스 초기화 메소드니까. 인스턴스 만들 때 불러야 하니까
main에서 직접 부르는 (같은 클래스 내의) 메소드들은 static이어야만 한다.
main이 static인데, static 메소드에서 인스턴스 멤버를 사용할 수 없으므로
참고: