와드의 블로그

Chapter 6. 객체지향 프로그래밍 1 본문

프로그래밍 언어/Java

Chapter 6. 객체지향 프로그래밍 1

Ward 2022. 12. 5. 17:31

1. 객체지향 언어

1.1 객체지향 이론

객체지향 이론의 기본 개념은 '실제 세계는 객체로 이루어져 있으며, 발생하는 모든 사건들은 객체 간의 상호작용이다.'라는 것이다. 실제 사물의 속성과 기능을 분석한 다음, 데이터와 함수로 정의함으로써 실제 세계를 컴퓨터 속에 옮겨 놓은 것과 같은 가상 세계를 구현하고 이 가상세계에서 모의실험을 함으로써 많은 시간과 비용을 절약할 수 있다.

1.2 객체지향 언어의 특징

객체지향 언어는 기존의 프로그래밍 언어와 전혀 다른 것이 아니라, 기존의 프로그래밍 언어에 몇 가지 새로운 규칙을 추가한 보다 발전된 형태이다. 객체지향 언어의 주요 특징은 다음과 같다.

  • 코드의 재사용: 새로운 코드를 작성할 때 기존의 코드를 이용하여 쉽게 작성할 수 있다.
  • 편리한 코드 관리: 코드 간의 관계를 이용해서 적은 노력으로 쉽게 코드를 변경할 수 있다.
  • 높은 신뢰성: 제어자와 메서드를 이용해서 데이터를 보호하고 올바른 값으로 유지하도록 하며, 코드의 중복을 제거하여 코드의 불일치로 인한 오작동을 방지할 수 있다.

2. 클래스와 객체

2.1 클래스와 객체의 정의와 용도

클래스란 객체를 정의해 놓은 설계도라고 정의할 수 있다. 클래스는 객체를 생성하는 데 사용되며, 객체는 클래스에 정의된 대로 생성된다.

객체의 사진적인 정의는 실제로 존재하는 것, 사물 또는 개념이다. 프로그래밍에서의 객체는 클래스에 정의된 내용대로 메모리에 생성된 것을 뜻한다.

클래스를 정의하고 클래스를 통해 객체를 생성하는 이유는 한 번 클래스를 잘 만들어 놓기만 하면, 그저 클래스에 정의된 대로 객체를 생성해서 사용하기만 하면 되기 때문이다.

JDK에서는 이러한 이점을 살리기 위해 많은 수의 유용한 클래스를 기본적으로 제공한다.

2.2 객체와 인스턴스

클래스로부터 객체를 만드는 과정을 클래스의 인스턴스화라고 하며, 어떤 클래스로부터 만들어진 객체를 그 클래스의 인스턴스라고 한다. 객체와 인스턴스는 같은 의미이지만, 객체는 모든 인스턴스를 대표하는 포괄적인 의미를 갖고, 인스턴스는 어떤 클래스로부터 만들어진 것인지를 강조하는 보다 구체적인 의미를 갖고 있다.

2.3 객체의 구성요소

객체는 속성과 기능, 두 종류의 구성요소로 이루어져 있다. 이러한 속성과 기능을 객체의 멤버라고 한다.

클래스는 객체를 정의한 것이므로 클래스에는 객체의 모든 속성과 기능이 정의되어 있다. 클래스로부터 객체를 생성하면, 클래스에 정의된 속성과 기능을 가진 객체가 만들어진다.

속성과 기능을 뜻하는 여러 용어가 있다.

  • 속성: 멤버 변수, 특성, 필드, 상태
  • 기능: 메서드, 함수, 행위

예시로 Tv의 속성과 기능을 담은 Tv 클래스를 만들면 다음과 같다.

class Tv {
	String color;
	boolean power;
	int channel;
	
	void onOff() {
		power = !power;
	}
	void channelUp() {
		channel++;
	}
	void channelDown() {
		channel--;
	}
}

2.4 인스턴스의 생성과 사용

Tv 클래스를 선언한 것은 Tv의 설계도를 작성한 것에 불과하므로, Tv 인스턴스를 생성해야 한다. 클래스로부터 인스턴스를 생성하는 방법은 다음과 같다.

Tv tv = new Tv();

다음은 Tv 클래스로부터 인스턴스를 생성하고 인스턴스의 속성과 메서드를 사용하는 예제이다.

class Tv {
	String color;
	boolean power;
	int channel;
	
	void onOff() {
		power = !power;
	}
	void channelUp() {
		channel++;
	}
	void channelDown() {
		channel--;
	}
}

public class OOP {
	public static void main(String[] args) {	
		Tv tv = new Tv();
		tv.channel = 7;
		tv.channelDown();
		System.out.println("현재 체널은 " + tv.channel + "번 입니다.");
	}
}

인스턴스의 속성과 메서드에 접근하기 위해서는 인스턴스의 참조 변수와. 연산자를 사용한다. '참조변수.멤버변수(메서드)'와 같은 형태로 인스턴스의 속성과 메서드에 접근한다.

2.5 객체 배열

많은 수의 객체를 다뤄야 할 때, 배열로 다루면 편리하다. 요소가 객체인 배열을 객체 배열이라고 한다. 객체 배열에는 요소로 객체가 저장되는 것이 아니라 객체를 가리키는 참조 변수들이 저장된다.

다음은 Tv 객체로 이루어진 배열을 만들고 초기화하는 과정이다.

Tv[] tvArr = new Tv[3];
for(int i = 0; i < tvArr.length; i++)
	tvArr[i] = new Tv();

모든 배열이 그렇듯이 객체 배열도 같은 타입의 객체만 저장할 수 있다. 그러나 다음 장에서 다형성에 대해 배우면 여러 종류의 객체를 하나의 배열로 관리할 수 있다.

2.6 클래스의 또 다른 정의

클래스는 객체를 생성하기 위한 틀이며 클래스는 속성과 기능으로 정의되어 있다고 했다. 이것은 객체지향 이론의 관점에서 내린 정의이고, 프로그래밍적인 관점에서는 클래스의 정의가 다르게 해석될 수 있다.

 

※ 데이터와 함수의 결합

프로그래밍 언어에서 데이터 처리를 위한 데이터 저장 형태의 발전 과정은 다음과 같다.

  1. 변수: 하나의 데이터를 저장할 수 있는 공간
  2. 배열: 같은 종류의 여러 데이터를 하나의 집합으로 저장할 수 있는 공간
  3. 구조체: 서로 관련된 여러 데이터를 종류에 관계없이 하나의 집합으로 저장할 수 있는 공간
  4. 클래스: 데이터와 함수의 결합(구조체 + 함수)

기존의 절차 지향 프로그래밍에서는 데이터와 함수가 서로 관계없는 것처럼 데이터는 데이터끼리, 함수는 함수끼리 따로 다루어져 왔지만, 사실 함수는 주로 데이터를 가지고 작업을 하기 때문에 관계가 깊다.

그래서 객체지향 언어에서는 데이터와 함수를 하나의 클래스에 정의하여 서로 관계가 깊은 변수와 함수들을 함께 다룰 수 있게 했다.

 

※ 사용자 정의 타입

프로그래밍 언어에서 제공하는 자료형 외에 프로그래머가 서로 관련된 변수들을 하나의 타입으로 묶어서 하나의 타입으로 새로 추가하는 것을 사용자 정의 타입이라고 한다. 타 프로그래밍 언어에서도 사용자 정의 타입을 정의할 수 있는 방법을 제공하고 있다. 자바와 같은 객체지향 언어에서는 클래스가 곧 사용자 정의 타입이다.

3. 변수와 메서드

3.1 선언 위치에 따른 변수의 종류

변수는 클래스 변수, 인스턴스 변수, 지역 변수 모두 세 종류가 있다. 변수의 종류를 결정짓는 중요한 요소는 변수가 선언된 위치이다. 클래스 영역에 선언된 것을 멤버 변수라고 한다. 멤버 변수를 제외한 나머지 변수들은 모두 지역 변수이며, 멤버 변수 중 static 키워드가 붙은 것은 클래스 변수, 붙지 않은 것은 인스턴스 변수이다.

class Variables {
	int iv;  // 인스턴스 변수
	static int cv; // 클래스 변수
    
	void method() {
		int lv; // 지역 변수
    }
}

※ 인스턴스 변수

클래스 영역에 선언되며, 클래스의 인스턴스를 생성할 때 만들어진다. 인스턴스마다 고유한 상태를 유지해야 하는 속성의 경우 인스턴스 변수로 선언한다.

 

※ 클래스 변수

클래스 변수를 선언하는 방법은 인스턴스 변수 앞에 static을 붙이기만 하면 된다. 클래스 변수는 모든 인스턴스가 공통된 값을 공유하게 된다. 클래스 변수는 인스턴스 변수와 달리 인스턴스를 생성하지 않고도 언제라도 사용할 수 있다는 특징이 있다. '클래스이름.클래스변수'와 같은 형식으로 사용한다.

 

※ 지역 변수

메서드 내에서 선언되어 메서드 내에서만 사용 가능하며, 메서드가 종료되면 소멸되어 사용할 수 없게 된다. 또한 블록 내에서 선언된 변수도 지역 변수이며, 블럭 내에서만 사용 가능하다.

3.2 클래스 변수와 인스턴스 변수

다음은 포커에 사용되는 카드 클래스의 예시이다. 클래스 변수와 인스턴스 변수를 활용한다.

class Card {
	String kind;
	int number;
	
	static int width = 100;
	static int height = 250;
}

public class OOP {
	public static void main(String[] args) {	
		System.out.println("Card.width = " + Card.width);
		System.out.println("Card.height = " + Card.height);
		
		Card c1 = new Card();
		c1.kind = "Heart";
		c1.number = 7;
		
		Card c2 = new Card();
		c2.kind = "Spade";
		c2.number = 4;
		
		System.out.printf("c1은 %s, %d이며, 크기는 (%d, %d)이다.%n", c1.kind, c1.number, c1.width, c1.height);
		System.out.printf("c2는 %s, %d이며, 크기는 (%d, %d)이다.%n", c2.kind, c2.number, c2.width, c2.height);
		
		System.out.println("c1의 width와 height를 각각 50, 80으로 변경합니다.");
		c1.width = 50;
		c1.height = 80;
		
		System.out.printf("c1은 %s, %d이며, 크기는 (%d, %d)이다.%n", c1.kind, c1.number, c1.width, c1.height);
		System.out.printf("c2는 %s, %d이며, 크기는 (%d, %d)이다.%n", c2.kind, c2.number, c2.width, c2.height);
		
		
	}
}

인스턴스 변수는 인스턴스가 생성될 때마다 생성되므로 인스턴스마다 고유한 값을 유지할 수 있지만, 클래스 변수는 모든 인스턴스가 하나의 저장 공간을 공유하므로, 항상 공통된 값을  갖는다.

3.3 메서드

메서드는 특정 작업을 수행하는 일련의 문장들을 하나로 묶은 것이다. 수학의 함수와 유사하며, 어떤 값을 입력하면 이 값으로 작업을 수행해서 결과를 반환한다.

 

※ 메서드를 사용하는 이유

  • 높은 재사용성: 메서드는 한 번 만들어 놓으면 몇 번이고 호출할 수 있으며, 다른 프로그램에서도 사용이 가능하다.
  • 중복된 코드 제거: 프로그램을 작성하다 보면 반복된 부분이 나타난다. 반복되는 부분을 하나의 메서드로 묶어서 작성해 놓으면, 중복된 여러 문장을 메서드 호출 하나로 대체할 수 있다.
  • 프로그램의 구조화: 규모가 큰 프로그램을 작성하면 여러 문장들을 작업 단위로 나눠서 여러 개의 메서드에 담아 프로그램을 구조화시킨다. 그러면 프로그램을 개발하거나 유지 보수할 때 편리하다.

3.4 메서드의 선언과 구현

메서드는 크게 선언부와 구현부로 이루어져 있다. 메서드를 정의하는 것은 선언부와 구현부를 작성하는 것을 뜻한다. 다음은 두 정수를 더하는 메서드를 정의한 것이다.

int add(int x, int y) // 메서드 선언부
{                     // 메서드 구현부
	return x + y;
}

 

※ 메서드 선언부

메서드 선언부는 메서드의 이름, 매개변수 선언, 그리고 반환 타입으로 구성되어 있다. 메서드가 작업을 수행하기 위해 어떤 값들을 필요로 하고 작업의 결과로 어떤 타입을 반환하는지에 대한 정보를 제공한다.

  • 매개변수 선언: 매개변수는 메서드가 작업을 수행하는데 필요한 값들을 제공받기 위한 것이다. 필요한 값의 개수만큼 변수를 선언한다.
  • 메서드 이름: 메서드 이름은 변수의 명명 규칙대로 작성하면 된다.
  • 반환 타입: 메서드의 작업 수행 결과인 반환 값의 타입을 적는다. 반환 값이 없는 메서드의 경우 반환 타입으로 void를 적는다.

※ 메서드 구현부

메서드의 선언부 다음에 오는 블록을 메서드의 구현부라고 한다. 메서드의 구현부에는 메서드를 호출했을 때 수행될 문장들을 넣는다.

 

※ return문

메서드의 반환 타입이 void가 아닌 경우, 구현부 안에 'return 반환값;'이 반드시 포함되어 있어야 한다. 이 문장은 반환 값을 호출한 메서드로 전달한다. return문은 단 하나의 값만 반환할 수 있다.

3.5 메서드의 호출

메서드를 정의했어도 호출되지 않으면 아무 일도 일어나지 않는다. 메서드를 호출하는 방법은 다음과 같다.

메서드이름(값1, 값2, ...);

 

※ 인자와 매개변수

메서드를 호출할 때 소괄호 안에 지정해 준 값들을 인자라고 하는데, 인자의 개수와 순서는 호출된 메서드의 매개변수와 일치해야 한다. 그리고 인자는 메서드가 호출되면서 매개변수에 대입되므로, 인자의 타입이 매개변수 타입과 일치하거나 자동 형 변환이 가능한 것이어야 한다.

 

다음은 사칙연산을 하는 4개의 메서드가 정의된 클래스를 이용한 예제이다.

public class OOP {
    public static void main(String[] args) {
        MyMath mm = new MyMath();
        int a = 5, b = 3;
        long result1 = mm.add(a, b);
        long result2 = mm.sub(a, b);
        long result3 = mm.mul(a, b);
        double result4 = mm.div(a, b);

        System.out.println("add(5, 3) = " + result1);
        System.out.println("sub(5, 3) = " + result2);
        System.out.println("mul(5, 3) = " + result3);
        System.out.println("div(5, 3) = " + result4);
    }
}

class MyMath {
    long add(long a, long b) {
        return a + b;
    }
    long sub(long a, long b) {
        return a - b;
    }
    long mul(long a, long b) {
        return a * b;
    }
    double div(double a, double b) {
        return a / b;
    }
}

3.6 return문

return문은 현재 실행 중인 메서드를 종료하고 호출한 메서드로 되돌아간다. 모든 메서드에는 적어도 하나 이상의 return문이 있어야 한다. 반환 타입이 void인 경우, return문이 없어도 컴파일러가 자동으로 추가해준다.

return문은 현재 실행 중인 메서드를 종료하기 때문에 메서드의 구현부에 return문이 여러 개더라도 첫 번째 return문만 실행된다.

3.7 JVM의 메모리 구조

응용 프로그램이 실행되면 JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리르 할당받고 JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.

  • 메서드 영역: 프로그램 실행 중 어떤 클래스가 사용되면, JVM은 해당 클래스의 클래스 파일을 읽어서 분석하여 클래스 데이터를 이곳에 저장한다. 이때, 클래스 변수도 이 영역에 함께 저장된다.
  • 힙: 인스턴스가 생성되는 공간이다. 즉, 인스턴스 변수들이 이 공간에 생성된다.
  • 호출 스택: 호출 스택은 메서드의 작업을 위한 메모리 공간을 제공한다. 메서드가 호출되면, 호출 스택에 호출된 메서드를 위한 공간이 할당되며, 이 메모리는 메서드가 작업을 수행하는 동안 지역 변수들과 연산의 중간 결과 등을 저장한다.

3.8 기본형 매개변수와 참조형 매개변수

자바에서는 메서드를 호출할 때 매개변수로 지정한 값을 메서드의 매개변수로 복사해서 넘겨준다. 매개변수 타입이 기본형일 때는 기본형의 값이 복사되지만, 참조형이면 인스턴스의 주소가 복사된다. 따라서 기본형 매개변수는 전달된 값을 읽기만 할 수 있는 반면, 참조형 매개변수는 전달된 값을 직접 변경할 수 있다.

public class OOP {
    public static void main(String[] args) {
        Data d = new Data();
        d.x = 10;
        System.out.println("main(): x = " + d.x);

        change(d);
        System.out.println("After change(d)");
        System.out.println("main(): x = " + d.x);
    }

    static void change(Data d) {
        d.x = 1000;
        System.out.println("change(): x = " + d.x);
    }
}

class Data {
    int x;
}

3.9 참조형 반환타입

매개변수뿐만 아니라 반환 타입도 참조형이 될 수 있다. 반환 타입이 참조형이라는 것은 메서드가 객체의 주소를 반환한다는 것이다.

public class OOP {
    public static void main(String[] args) {
        Data d = new Data();
        d.x = 10;

        Data d2 = copy(d);
        System.out.println("d.x = " + d.x);
        System.out.println("d2.x = " + d2.x);
    }

    static Data copy(Data d) {
        Data tmp = new Data();
        tmp.x = d.x;
        return tmp;
    }
}

class Data {
    int x;
}

3.10  재귀 호출

메서드 내부에서 메서드 자기 자신을 다시 호출하는 것을 재귀 호출이라 하고, 재귀 호출을 하는 메서드를 재귀 메서드라 한다. 재귀 메서드는 무한히 자기 자신을 호출하기 때문에 조건문을 통해 재귀 호출을 종료할 조건을 적어주어야 한다.

다음은 팩토리얼을 재귀적으로 구현한 코드이다.

public class OOP {
    public static void main(String[] args) {
        System.out.println(factorial(5));
    }

    static int factorial(int n) {
        if(n == 0)
            return 1;
        return n * factorial(n - 1);
    }
}

3.11 클래스 메서드와 인스턴스 메서드

변수에서 그랬던 것처럼, 메서드 앞에 static이 붙어 있으면 클래스 메서드이고 붙어 있지 않으면 인스턴스 메서드이다.

인스턴스 메서드는 인스턴스 변수와 관련된 작업을 하는, 즉 메서드의 작업을 수행하는데 인스턴스 변수를 필요로 하는 메서드이다.

반면에 메서드 중에서 인스턴스와 관계없는 메서드를 클래스 메서드로 정의한다.

  • 클래스를 설계할 때, 멤버 변수 중 모든 인스턴스에 공통으로 사용하는 것에 static을 붙인다.
  • 클래스 변수는 인스턴스를 생성하지 않아도 사용할 수 있다.
  • 클래스 메서드는 인스턴스 변수를 사용할 수 없다.
  • 메서드 내에서 인스턴스 변수를 사용하지 않는다면, static을 붙이는 것을 고려한다.

3.12 클래스 멤버와 인스턴스 멤버 간의 참조와 호출

같은 클래스에 속한 멤버들 간에는 별도의 인스턴스를 생성하지 않고도 서로 참조 또는 호출이 가능하다. 단, 클래스 멤버가 인스턴스 멤버를 참조 또는 호출하고자 하는 경우에는 인스턴스를 생성해야 한다.

class MemberCall {
    int iv = 10;
    static int cv = 20;

    int iv2 = cv;
    // static int cv2 = iv; 에러, 클래스 변수는 인스턴스 변수를 사용할 수 없다.
    static int cv2 = new MemberCall().iv; // 객체를 생성해야 사용할 수 있다.

    static void staticMethod1() {
        System.out.println(cv);
        // System.out.println(iv); 에러, 클래스 메서드에서 인스턴스 변수를 사용할 수 없다.
        MemberCall c = new MemberCall();
        System.out.println(c.iv); // 객체를 생성한 후에야 인스턴스 변수를 참조할 수 있다.
    }

    void instaceMethod1() {
        System.out.println(cv);
        System.out.println(iv); // 인스턴스 메서드에서는 인스턴스 변수를 바로 사용할 수 있다.
    }
    
    static void staticMethod2() {
        staticMethod1();
        // instanceMethod1(); 에러, 클래스 메서드에서는 인스턴스 메서드를 호출할 수 없다.
        MemberCall c = new MemberCall();
        c.instaceMethod1(); // 인스턴스를 생성한 후에야 인스턴스 메서드를 호출할 수 있다
    }
    
    void instanceMethod2() {
        staticMethod1();
        instaceMethod1(); // 인스턴스 메서드에서는 인스턴스 메서드를 바로 호출할 수 있다.
    }
}

4. 오버로딩

4.1 오버로딩이란?

메서드도 변수와 마찬가지로 같은 클래스 내에서 서로 구별될 수 있어야 하기 때문에 각기 다른 이름을 가져야 한다. 그러나 자바에서는 한 클래스 내에 이미 사용하려는 이름과 같은 이름의 메서드가 있더라도 매개변수의 개수 또는 타입이 다르면, 같은 이름을 사용해서 메서드를 정의할 수 있다.

이처럼, 한 클래스 내에 같은 이름의 메서드를 여러 개 정의하는 것을 오버로딩이라고 한다.

4.2 오버로딩의 조건

오버로딩이 성립하기 위해서는 메서드 이름이 같아야 하고, 매개변수의 개수 또는 타입이 달라야 한다. 오버로딩된 메서드들은 매개변수에 의해서만 구별될 수 있으므로 반환 타입은 오버로딩을 구현하는 데 아무런 영향을 주지 못한다.

int add(int a, int b) { return a + b; }
long add(int a, int b) { return (long)(a + b); } // 리턴타입만 다른 경우는 오버로딩으로 간주되지 않는다.
long add(long a, long b) { return a + b; } // 매개변수의 타입이 다르므로 오버로딩으로 간주된다.

4.3 오버로딩의 장점

만일 메서드가 변수처럼 단지 이름만으로 구별된다면, 한 클래스 내의 모든 메서드들은 이름이 달라야 한다. 그렇다면 근본적으로 같은 기능을 하는 메서드들이지만, 매개변수의 개수 또는 타입만 다른 경우 각각의 메서드들이 서로 다른 이름을 가져야 하기 때문에 메서드를 작성하는 쪽에서는 이름 짓기 어렵고, 메서드를 사용하는 쪽에서는 이름을 일일이 구분해서 기억해야 하기 때문에 부담이 된다.

오버로딩을 통해 같은 기능을 하는 여러 메서드를 하나의 이름으로 정의한다면 하나의 이름만 기억하면 되므로 기억하기 쉽고 이름을 짧게 할 수 있어서 오류의 가능성을 줄일 수 있다.

4.4 가변 인자와 오버로딩

※ 가변 인자

기존에는 메서드의 매개변수 개수가 고정적이었으나, JDK1.5부터 동적으로 지정해 줄 수 있게 되었으며, 이 기능을 가변인지라고 한다.

가변 인자는 '타입... 변수명'과 같은 형식으로 선언한다.

다음은 여러 문자열을 하나로 결합하여 반환하는 concatenate 메서드이다.

String concatenate(String... strs) { ... }

이 메서드를 호출할 때는 인자의 개수를 가변적으로 할 수 있고, 심지어는 인자가 없어도 되고 배열도 인자가 될 수 있다.

※ 가변 인자의 오버로딩

가변 인자를 오버로딩할 때 주의할 점은 가변인자를 선언한 메서드를 오버로딩하면, 메서드를 호출했을 때 구별되지 못하는 경우가 쉽게 발생할 수 있다는 것이다.

아래 두 오버로딩된 메서드는 서로 구분되지 않는다.

String concatenate(String delim, String... argvs) { ... }
String concatenate(String... args) { ... }

 

따라서 가능하면 가변 인자를 사용한 메서드는 오버로딩하지 않는 것이 좋다.

5. 생성자

5.1 생성자란?

생성자는 인스턴스가 생성될 때 호출되는 인스턴스 초기화 메서드이다. 따라서 인스턴스 변수의 초기화 작업에 주로 사용되며, 인스턴스 생성 시에 실행되어야 할 작업을 위해서도 사용된다.

생성자 역시 메서드처럼 클래스 내에 선언되며, 구조도 메서드와 유사하지만 조건이 있다. 생성자의 조건은 다음과 같다.

  • 생성자의 이름은 클래스 이름과 같아야 한다.
  • 생성자는 리턴 값이 없다.

생성자는 다음과 같이 정의한다. 생성자도 오버로딩이 가능하므로 하나의 클래스에 여러 개의 생성자가 존재할 수 있다.

class Card {
    Card() {
    	...
    }
    
    Card(String s, int n) {
    	...
    }
}

연산자 new가 인스턴스를 생성하면 생성자 함수를 호출해서 인스턴스 변수를 초기화한다.

Card c = new Card();

5.2 기본 생성자

기본 생성자는 컴파일러가 기본으로 제공하는 생성자이다. 컴파일할 때, 소스파일의 클래스에 생성자가 하나도 정의되자 않은 경우 컴파일러는 자동적으로 기본 생성자를 추가하여 컴파일한다. 기본 생성자는 매개변수도 없고 아무런 내용도 없는 아주 간단한 것이다.

5.3 매개변수가 있는 생성자

생성자도 메서드처럼 매개변수를 선언하여 호출 시 값을 넘겨받아서 인스턴스의 초기화 작업에 사용할 수 있다.

class Car {
    String color;
    String gearType;
    int door;
    
    Car() {}
    Car(String c, String g, int d) {
    	color = c;
        gearType = g;
        door = d;
    }
}

클래스를 작성할 때 다양한 생성자를 제공함으로써 인스턴스 생성 후에 별도로 초기화를 하지 않아도 되도록 하는 것이 바람직하다.

5.4 생성자에서 다른 생성자 호출하기(this)

같은 클래스의 멤버들 간에 서로 호출이 가능한 것처럼 생성자 간에도 서로 호출이 가능하다. 단, 다음 조건을 만족시켜야 한다.

  • 생성자의 이름으로 클래스 이름 대신 this를 사용한다.
  • 한 생성자에서 다른 생성자를 호출할 때는 반드시 첫 줄에서만 호출이 가능하다.
class Car {
    String color;
    String gearType;
    int door;
    
    Car() {
        this("white", "auto", 4);
    }
    
    Car(String color) {
        this(color, "auto", 4);
    }
    
    Car(String color, String gearType, int door) {
        this.color = color;
        this.gearType = gearType;
        this.door = door;
    }
}

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

현재 사용하고 있는 인스턴스와 같은 상태를 갖는 인스턴스를 하나 더 만들고자 할 때 생성자를 이용할 수 있다. 두 인스턴스가 같은 상태를 갖는다는 것은 두 인스턴스의 모든 인스턴스 변수가 동일한 값을 갖는 것을 의미한다.

다음은 인스턴스를 복사하기 위한 생성자이다.

Car(Car c) {
    color = c.color;
    gearType = c.gearType;
    door = c.door;
}

6. 변수의 초기화

6.1 변수의 초기화

변수를 선언하고 처음으로 값을 저장하는 것을 변수의 초기화라고 한다. 변수의 초기화는 경우에 따라서 필수적이기도 하고 선택적이기도 하다.

멤버 변수는 초기화를 하지 않아도 자동적으로 변수의 자료형에 맞는 기본값으로 초기화가 이루어진다. 하지만 지역변수는 사용하기 전에 반드시 초기화해야 한다.

6.2 명시적 초기화

변수를 선언과 동시에 초기화하는 것을 명시적 초기화라고 한다. 

class Car {
    int door = 4;
    Engine e = new Engine();
    ...
}

6.3 초기화 블록

초기화 블록에는 클래스 초기화 블록과 인스턴스 초기화 블록이 있다. 클래스 초기화 블록은 클래스 변수의 초기화에 사용되고, 인스턴스 초기화 블럭은 인스턴스 변수의 초기화에 사용된다.

인스턴스 초기화 블록은 단순히 클래스 내에 블럭을 만들고 그 안에 코드를 작성하기만 하면 된다. 그리고 클래스 초기화 블럭은 인스턴스 초기화 블럭 앞에 static을 덧붙이기만 하면 된다.

초기화 블록 내에서는 조건문, 반복문, 예외처리구문 등을 자유롭게 사용할 수 있으므로, 초기화 작업이 복잡하여 명시적 초기화만으로 부족한 경우 사용한다.

 

※ 생성자와 인스턴스 초기화 블럭

인스턴스 초기화 블록은 생성자와 같이 인스턴스를 생성할 때 마다 수행된다. 그리고 생성자보다 인스턴스 초기화 블럭이 먼저 수행된다.

인스턴스 변수의 초기화는 주로 생성자를 사용하고, 인스턴스 초기화 블럭은 모든 생성자에서 공통으로 수행돼야 하는 코드를 넣는 데 사용한다.

class Car {
    String color;
    String gearType;
    int door;
    int serialNo;
    static int count = 0;

    {
        count++;
        serialNo = count;
    }

    Car() {
        this("white", "auto", 4);
    }

    Car(String color) {
        this(color, "auto", 4);
    }

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

6.4 멤버 변수의 초기화 시기와 순서

  • 클래스 변수의 초기화 시점: 클래스가 처음 로딩될 때 단 한 번 초기화된다.
  • 인스턴스 변수의 초기화 시점: 인스턴스가 생성될 때마다 각 인스턴스 별로 초기화가 이루어진다.
  • 클래스 변수의 초기화 순서: 기본값 -> 명시적 초기화 -> 클래스 초기화 블록
  • 인스턴스 변수의 초기화 순서: 기본값 -> 명시적 초기화 -> 인스턴스 초기화 블록 -> 생성자

 

'프로그래밍 언어 > Java' 카테고리의 다른 글

자바 String 메서드 정리  (0) 2024.10.31
Chapter 5. 배열  (0) 2022.12.03
Chapter 4. 조건문과 반복문  (0) 2022.11.28
Chapter 3. 연산자  (0) 2022.11.24
Chapter 2. 변수  (0) 2022.11.23