Ch06. 객체지향 프로그래밍Ⅱ

ho_c·2023년 5월 3일
0

자바의 정석 3판

목록 보기
6/8
post-thumbnail

들어가는 말

변수와 메서드는 클래스의 기본 요소이다. 이번 시간엔 이 둘이 객체지향적으론 어떻게 응용되는지 알아보자.


1. 오버로딩(Overloading)

한 클래스 내의 동명의 메서드를 여러 개 정의하는 것.

  • 메서드의 기능은 같지만 사용자가 입력하는 인자가 다른 경우가 있다.

  • 예를 들면 println() 가 있다. 사용할 때는 한 가지 메서드 같지만 실제로는 매개변수가 서로 다른 동명의 메서드들이 있다.

  • 즉, 오버로딩은 같은 기능을 하지만, 사용하는 인자가 다를 경우 적용한다.


조건

이름만 같다고 해서 오버로딩이 아니다. 다음 두 조건을 충족해야된다.

  1. 메서드의 이름이 같아야 됨.

  2. 매개 변수의 개수 or 타입이 달라야됨.

* 반환타입은 영향을 미치지 못한다.


예시

1. println()

void println()
void println(boolean x)
void println(char x)
void println(char[] x)
void println(double x)
void println(float x)
void println(int x)
void println(long x)
void println(Object x)
void println(String x)
  • 실제 정의된 println()의 오버로딩 유형이다.
    각자 매개변수가 달라서 사용자가 넣는 인자에 따라 그에 맞는 메서드가 선택되어 실행된다.

2. 매개변수명은 다르지만, 타입이 같은 경우

int add(int a, int b){ return 1 }
int add(int x, int y){ return 1 }
  • 이 경우, 매개변수명만 다를 뿐이지 타입과 개수가 일치한다. 따라서 오버로딩이 적용되지 않는다.

3. 리턴 타입만 다른 경우

int add(int a, int b){ return 1 }
long add(int a, int b{ return 1 }
  • 1번과 같이 반환타입만 다를 뿐 조건에 충족하지 않아, 오버로딩이 아니다.

4. 매개변수의 순서가 다른 경우

long add(int a, long b){ return 1 }
long add(long a, int b){ return 1 }
  • 매개변수의 순서로 메서드 구분이 가능해, 오버로딩이 적용된다.
    다만, 리터럴을 정확히 구분해서 넘겨줘야 한다. (산술변환 x)
    예를 들어 (3, 3)을 넘기면 타입 불일치로 메서드 구분이 불가능해 컴파일 에러가 난다.

장점

println()을 보면, 장점은 뚜렷하다.

  • 여러 메서드를 하나의 이름으로 묶어서 사용할 수 있다.
  • 그래서 한 이름으로 여러 메서드의 기능을 유추할 수 있다.
  • 메서드 이름을 절약할 수 있다.


가변인자

메서드명(타입... 변수명);

  • 기존 메서드를 정의할 때, 매개변수의 수는 고정적이다. (유동적으로 조절 x)

  • 막상, 상황에 따라 매개변수의 수가 다르거나 아예 정할 수 없을 수도 있다.
    그러면 모든 상황에 맞게 메서드를 오버로딩을 해야된다.

  • 그래서 호출 시점에 매개변수를 동적으로 제어하는 기능인 '가변인자'가 만들어졌다.
    말 그대로 매개변수가 몇개나 들어올지 모른다.


조건

가변인자는 다른 매개변수들과의 구분이 어렵다,.
그래서 무조건 매개변수의 마지막 순서에 넣어야 된다.

// 1. 구분 가능
public static int add(int a, int... b){return 0};
/* add(3,5,6,7);
순서에 따라 3은 무조건 a의 값이 된다.
*/

// 2. 구분 불가
public static int add(int... a, int b){return 0};
/* add(3,5,6,7);
7이 가변인자인지, b의 값인지 구분할 수 없다.
때문에 가변인자가 있는 경우 오버로딩된 메서드끼리 구분이 불가능하기도 하다.
*/

예시

1. 매개변수 타입이 같은 오버로딩 메서드를 하나로 대체할 수 있다.

// 오버로딩
String concatenate(String s1, String s2) { ... }
String concatenate(String s1, String s2, String s3) { ... }
String concatenate(String s1, String s2, String s3, String s4){ ... }

// 가변인자
String concatenate(String... s) { ... }

2. 호출 시, 인자의 개수가 가변적이다.

concatenate();
concatenate("a");
concatenate(new String[]{"A", "B"};
  • 인자는 없을 수도 있고, 배열도 가능하다.

3. 가변인자는 내부적으로 배열을 사용한다.

  • 가변인자가 선언된 메서드는 호출마다 가변인자를 배열에 넣어 넘긴다.
  • 이는 매개변수를 배열로 하는 것과 차이가 있다.
    매개변수가 배열이면, 인자의 생략이 불가능하다. (null, 0인 배열을 넘겨야됨)
String concatenate1(String... s1) { ... } // 가변인자
String concatenate2(String[] s2) { ... } // 배열

public void static main(String[] args){
		concatenate1(); // 인자 생략 가능
		concatenate2(); // 인자 생략 불가
}

4. 가변인자가 있을 땐, 오버로딩을 안하는 게 좋다.

int add(int a, int... b){ ... };
int add(int... b) { ... };
  • 분명 오버로딩과 가변인자의 사용조건에 부합하지만, 컴파일 에러가 난다.
    그건 컴퓨터가 오버로딩된 메서드들을 구분할 수 없기 때문이다.

  • 1를 인자로 넘긴다 할 때, 그게 a의 값인지, 가변인자 b인지 구별할 수 없다.
    왜냐면 가변인자는 인자의 생략도 가능하기 때문이다.



2. 생성자(Constructor)

생성자는 인스턴스 생성 시, 정해둔 작업을 수행하는 메서드이다.
주로 인스턴스 변수의 초기화를 위해 사용된다.

클래스이름(타입 변수명, 타입 변수명, ...) {
	// 인스턴스 생성시 수행될 코드
}

조건

다음 조건을 충족하면 클래스의 생성자가 된다.

  1. 클래스 내에 선언된다.

  2. 리턴값은 없다.

  3. 생성자의 이름은 클래스의 이름과 같아야 한다.

  4. 생성자의 오버로딩도 가능하다.

class Card {
	// 기본 생성자
	Card() {}
    
    // 매개변수가 있는 생성자
    Card(String k, int num){
    	...
    }
}

생성자 수행 과정

public void static main(String[] args){
	Card c = new Card();
}
  1. 연산자 new에 의해 heap에 Card클래스의 인스턴스 생성
  2. 생성자 Card() 호출
  3. new의 결과로 인스턴스의 주소가 반환되어, 참조변수 c에 저장

기본 생성자

모든 클래스에는 반드시 하나 이상의 생성자가 정의되어야 한다.

  • 기본으로 컴파일러가 '기본 생성자'를 추가해주기 때문에, 따로 정의할 필요는 없다.

  • 단, 컴파일러의 생성자 추가는 '생성자가 하나도 정의되지 않은 클래스'에만 적용된다.

  • 변수의 초기화작업이 없는 경우에는 기본 생성자만 써도 괜찮다.

  • 접근제어자가 public 이면 기본 생성자로 public 클래스명(){} 이 추가된다.

class Card {
	// 기본 생성자
	Card(){ }
}

사용자 생성자

사용자가 생성자를 정의한 경우, 컴파일러는 기본생성자를 추가하지 않는다.

class Deck1{
	int card = 20;
}

class Deck2{
	int card;
    // 사용자 생성자
    Deck2(int card){
    	this.card = card; 
    }
}
/*-------------------------------------*/
public void static main(String[] args){
	Deck1 d1 = new Deck1();
    Deck2 d2 = new Deck2(); //  컴파일 에러
}
  • Deck1은 기본생성자를 컴파일러가 추가해준다.
    반면 Deck2는 사용자 생성자가 있기 때문에 기본생성자가 존재하지 않는다.

  • main() 실행 시, Deck2에는 일치하는 생성자가 없기 때문에 컴파일에러가 발생한다.
    해결하려면, 기본생성자를 정의하든지, 사용자 생성자에 맞춰서 매개변수를 전달해야 한다.


매개변수가 있는 생성자

생성자도 매개변수를 선언해서 호출 시 값을 넘겨받을 수 있다.

class Deck2{
	int card;
    Deck2(int card){
    	this.card = card; 
    }
}
/*-------------------------------------*/
public void static main(String[] args){
    Deck2 d2 = new Deck2(20);
    System.out.println(d2.card); // 20
}
  • 생성자를 사용해 초기화를 하면, 인스턴스 생성 후 하나씩 초기화하지 않아도 된다.


this(), this

this는 클래스 내에서 중복되는 생성자나 변수명을 구별하기 위해 사용한다.


this() : 생성자 내에서 다른 생성자를 호출하는 조건

  1. 생성자 이름으로 클래스명 대신 this를 사용한다.
  2. 한 생성자에서 다른 생성자를 호출할 때는, 첫 줄에서만 가능하다.
  • 위 두 조건을 만족해야지 에러 없이 생성자 간 호출이 가능하다.

  • 모든 생성자를 this()로 호출하기에, 매개변수를 기반으로 생성자를 구별한다.

  • 초기화 작업 중 다른 생성자를 호출하면 기존 초기화 작업이 무의미해질 수 있기 때문에 첫째 줄에서만 호출이 가능하다.

class Car{
	String color;
    String gearType;
    int door;
	
    // 생성자1
    Car(){
    	this("white", "auto", 4); // 생성자3 호출
    }
    
    // 생성자2
    Car(String color){
    	this(color, "auto", 4); // 생성자3 호출
    }
    
    // 생성자3
    Car(String color, String gearType, int door) {
    	this.color= color;
		this.gearType = gearType;
		this.door = door;
    }
}
  • 여기서 생성자3은 this를 통해, 동명의 인스턴스변수와 매개변수를 구분한다.

참조변수 this

  • this는 참조변수로 인스턴스 자신의 주소를 가리킨다. 이때, this()와는 다른 개념이다.

  • this로 접근할 수 있는 건 '인스턴스 멤버'만 가능하다.
    클래스 멤버는 생성 시점이 달라 인스턴스가 없을 수도 있기 때문이다.

  • 인스턴스 메서드안에 this가 지역변수로 숨겨진 채 존재한다.
    이 말은 인스턴스 메서드의 '스택 프레임' 내의 지역변수 공간에 this가 저장되는 걸 말한다.
  • 따라서 this는 인스턴스 메서드 안에서만 사용이 가능하다.

* Stack-frame : 메서드 내의 매개변수와 지역변수, 메서드의 반환 주소 등을 저장하는 공간



생성자를 이용한 인스턴스 복사

사용 중인 인스턴스와 같은 상태의 인스턴스를 더 만들 때, 생성자를 이용할 수 있다.

  • 두 인스턴스의 주소는 다르다. 전혀 다른 인스턴스지만, 갖고 있는 상태(변수)가 일치한다.

  • 방법은 두 가지이다. 1. 생성자를 이용해서 / 2. clone() 사용.


생성자 이용

Car(Car c){
	color = c.color;
    gearType = c.gearType;
    door = c.door;
} 
// 막상하면, this 참조변수 쓰는 거랑 별반 다르지 않다.

결과적으로 인스턴스를 생성할 때는 2가지를 고려해야 한다.

  • 클래스 : 어떤 클래스의 인스턴스를 만들 것인가?
  • 생성자 : 선택한 클래스의 어떤 생성자를 사용해서 인스턴스를 만들 것인가?


3. 변수의 초기화

초기화

변수를 선언하고 처음 값을 저장하는 걸 '초기화'라고 한다.

  • 멤버 변수 : 초기화를 안해도 자료형의 기본값으로 자동 초기화 된다.
  • 지역 변수 : 초기화를 안하면 쓸 수가 없다.
class InitTest {
	int x; // 인스턴스 변수, 0으로 초기화
    float y; // 인스턴스 변수, 0.0f로 초기화
    
    void method1(){
    	int i; // 지역변수
        int j = i; // 에러, i는 초기화하지 않아서 사용 불가
    }
}

따라서 멤버변수, 배열(참조변수 null)의 초기화는 선택 / 지역 변수는 초기화 필수


멤버변수의 초기화

지역변수는 그 자리에서 초기화를 해야되지만, 멤버변수는 방식은 '명시적 초기화, 생성자, 초기화 블록'으로 여러 가지이다.

1) 명시적 초기화

변수 선언과 초기화를 동시에 하는 것.

class Car{
	int door = 4; // 기본형 변수 초기화
	Engine e = new Engine(); // 참조형 변수 초기화
    
    //...
}
  • 가장 간단하지만, 변수 초기화 값이 정해져 있다.

2) 초기화 블록

초기화 블럭은 두 가지 종류가 있고, 각 종류의 변수의 초기화에 사용된다.

  • 클래스 초기화 블럭 : 클래스 변수의 초기화
  • 인스턴스 초기화 블럭 : 인스턴스 변수의 초기화 (static 키워드만 붙이면 됨)

class InitBlock{
	static { /* 클래스 초기화 블럭 */}
    
    { /* 인스턴스 초기화 블럭 */}
}
  • 초기화 블럭을 사용하면, 내부에서 제어문을 사용할 수 있다.
    더불어 복잡한 초기화가 가능해진다.
  • 클래스 초기화 블럭은 클래스가 메모리에 첫 로딩될 때 한번만 수행된다.

  • 반대로 인스턴스는 생성자처럼 인스턴스 생성마다 수행된다.
    이때, 인스턴스 초기화 블럭이 생성자보다 먼저 실행된다.

// 실행 순서를 응용해서 초기화 과정을 더 편하게 만들 수 있다.
Car(){
	count++; // 중복
	serialNo = count; // 중복
	color ="White";
	gearType ="Auto";
}

Car(String color, String gearType){
	count++; // 중복
	serialNo =count; // 중복
	this.color = color;
	this.gearType = gearType;
}

/* -----------  코드 개선  ----------- */
// 인스턴스 블럭 → 중복 코드를 하나로 처리
{
	count++;
	serialNo = count;
}

Car(){
	color ="White";
	gearType ="Auto";
}

Car(String color, String gearType){
	this.color = color;
	this.gearType = gearType;
}

초기화 시기와 순서

초기화의 수행 시기와 순서를 파악하는 것은 매우 중요하다.

초기화 시점

  • 클래스 변수 : 클래스가 메모리에 처음 로딩(선언, 생성)될 때, 단 한번 초기화
  • 인스턴스 변수 : 인스턴스가 생성될 때마다 인스턴스별 초기화

초기화 순서

  • 클래스 변수 : 기본값 → 명시적 초기화 → 클래스 초기화 블럭
  • 인스턴스 변수 : 기본값 → 명시적 초기화 → 인스턴스 초기화 블럭 → 생성자

정리

  • 클래스 변수의 초기화는 클래스 데이터가 method area에 올라올 때, 진행된다.

  • 한 번 클래스 데이터가 로딩되었다면, 재로딩되지 않는다.

  • 클래스 데이터 로딩은 JVM의 종류마다 다르게 설계되어 있다.

  • 우선 순위에 따라서 초기화 값이 변화한다.

  • 인스턴스 변수는 '생성자'가 가장 마지막에 실행된다.

  • 공통된 부분은 '초기화 블럭'으로 처리해주는 것이 효율적이고, 객체지향적이다.
    중복을 제거하고, 한 곳에서 코드를 관리하기 때문에.


도움이 되셨다면 '좋아요' 부탁드립니다 :)

profile
기록을 쌓아갑니다.

0개의 댓글