Lesson: Classes and Objects 2

2024. 5. 27. 10:14High Level Programming Language/Learning the Java Language

Objects

일반적인 자바 프로그램은 많은 객체를 생성하며, 이 객체들은 메서드를 호출하여 상호 작용합니다. 이러한 객체 상호 작용을 통해 프로그램은 GUI 구현, 애니메이션 실행, 네트워크를 통한 정보 송수신 등 다양한 작업을 수행할 수 있습니다. 객체가 생성된 목적을 다하면, 해당 객체의 자원은 다른 객체에서 재사용할 수 있도록 회수됩니다.

다음은 CreateObjectDemo라는 작은 프로그램으로, 이 프로그램은 하나의 Point 객체와 두 개의 Rectangle 객체를 생성합니다. 이 프로그램을 컴파일하려면 세 개의 소스 파일이 모두 필요합니다.

public class Point {
    public int x = 0;
    public int y = 0;
	// a constructor!
    public Point(int a, int b) {
	x = a;
	y = b;
    }
}

 

public class Rectangle {
    public int width = 0;
    public int height = 0;
    public Point origin;

    // four constructors
    public Rectangle() {
	origin = new Point(0, 0);
    }
    public Rectangle(Point p) {
	origin = p;
    }
    public Rectangle(int w, int h) {
	origin = new Point(0, 0);
	width = w;
	height = h;
    }
    public Rectangle(Point p, int w, int h) {
	origin = p;
	width = w;
	height = h;
    }

    // a method for moving the rectangle
    public void move(int x, int y) {
	origin.x = x;
	origin.y = y;
    }

    // a method for computing the area of the rectangle
    public int getArea() {
	return width * height;
    }
}
public class CreateObjectDemo {

    public static void main(String[] args) {

        // 하나의 Point 객체와 두 개의 Rectangle 객체를 선언하고 생성합니다.
        Point originOne = new Point(23, 94);
        Rectangle rectOne = new Rectangle(originOne, 100, 200);
        Rectangle rectTwo = new Rectangle(50, 100);

        // rectOne의 너비, 높이 및 면적을 출력합니다.
        System.out.println("Width of rectOne: " + rectOne.width);
        System.out.println("Height of rectOne: " + rectOne.height);
        System.out.println("Area of rectOne: " + rectOne.getArea());

        // rectTwo의 위치를 설정합니다.
        rectTwo.origin = originOne;

        // rectTwo의 위치를 출력합니다.
        System.out.println("X Position of rectTwo: " + rectTwo.origin.x);
        System.out.println("Y Position of rectTwo: " + rectTwo.origin.y);

        // rectTwo를 이동시키고 새로운 위치를 출력합니다.
        rectTwo.move(40, 72);
        System.out.println("X Position of rectTwo: " + rectTwo.origin.x);
        System.out.println("Y Position of rectTwo: " + rectTwo.origin.y);
    }
}


이 프로그램은 다양한 객체를 생성, 조작 및 정보를 출력합니다. 출력 결과는 다음과 같습니다:

Width of rectOne: 100
Height of rectOne: 200
Area of rectOne: 20000
X Position of rectTwo: 23
Y Position of rectTwo: 94
X Position of rectTwo: 40
Y Position of rectTwo: 72


다음 세 섹션에서는 위의 예제를 사용하여 프로그램 내에서 객체의 생명 주기를 설명합니다. 이를 통해 자신의 프로그램에서 객체를 생성하고 사용하는 코드를 작성하는 방법을 배울 수 있습니다. 또한 객체의 생명 주기가 끝났을 때 시스템이 어떻게 정리하는지도 배울 것입니다.

 

Creating Objects

알다시피, 클래스는 객체의 설계도을 제공하며, 클래스에서 객체를 생성합니다. CreateObjectDemo 프로그램에서 가져온 다음 각 statements은 객체를 생성하고 변수를 할당합니다:

Point originOne = new Point(23, 94);
Rectangle rectOne = new Rectangle(originOne, 100, 200);
Rectangle rectTwo = new Rectangle(50, 100);


첫 번째 코드 라인은 Point 클래스의 객체를 생성하고, 두 번째와 세 번째 코드 라인은 각각 Rectangle 클래스의 객체를 생성합니다.

이러한 각 문장은 세 부분으로 구성됩니다(아래에서 자세히 설명):
1. 선언(Declaration): Point originOne, Rectangle rectOne, Rectangle rectTwo 코드들은 변수 선언으로, 변수 이름을 객체 타입과 연관시킵니다.
2. 인스턴스화(Instantiation): new 키워드는 객체를 생성하는 자바 연산자입니다.
3. 초기화(Initialization): new 연산자 뒤에는 생성자를 호출하여 새 객체를 초기화합니다.

 

Declaring a Variable Refer to an Object

앞서 배운 것처럼 변수를 선언하려면 다음과 같이 작성합니다:

type name;


이는 컴파일러에게 name을 사용하여 type 타입의 데이터를 참조할 것임을 알립니다. 기본형 변수의 경우, 이 선언은 변수에 필요한 적절한 메모리 양을 예약하기도 합니다.

참조 변수를 별도의 코드 라인에 선언할 수도 있습니다. 예를 들어:

Point originOne;


위와 같이 originOne을 선언하면, 객체가 실제로 생성되어 할당될 때까지 그 값은 결정되지 않습니다. 참조 변수를 단순히 선언하는 것으로는 객체가 생성되지 않습니다. 객체를 생성하려면 다음 섹션에서 설명하는 것처럼 new 연산자를 사용해야 합니다. 코드를 사용하기 전에 originOne에 객체를 할당해야 합니다. 그렇지 않으면 컴파일 오류가 발생합니다.

현재 어떤 객체도 참조하지 않는 상태의 변수는 다음과 같이 설명할 수 있습니다 (변수 이름 originOne과 아무것도 가리키지 않는 참조):

 

originOne은 null을 참조함.


Instantiating a Class

new 연산자는 새로운 객체를 위한 메모리를 할당하고 해당 메모리에 대한 참조를 반환함으로써 클래스를 인스턴스화합니다. new 연산자는 객체 생성자도 호출합니다.

참고: "클래스를 인스턴스화한다"는 표현은 "객체를 생성한다"는 것과 동일한 의미입니다. 객체를 생성하면 클래스의 "인스턴스"를 생성하는 것이므로, 클래스를 "인스턴스화"한다고 합니다. 

new 연산자는 하나의 후위 아규먼트, 즉 생성자 호출을 필요로 합니다. 생성자 이름은 인스턴스화할 클래스의 이름을 제공합니다.

new 연산자는 생성한 객체에 대한 참조를 반환합니다. 이 참조는 일반적으로 적절한 타입의 변수에 할당됩니다. 예를 들어:

Point originOne = new Point(23, 94);


new 연산자가 반환한 참조는 변수에 할당되지 않아도 됩니다. expression에서 직접 사용할 수도 있습니다. 예를 들어:

int height = new Rectangle().height;

 

이 문장은 다음 섹션에서 다룰 것입니다.

 

Initializing an Object

다음은 Point 클래스의 코드입니다:

public class Point {
    public int x = 0;
    public int y = 0;
    // 생성자
    public Point(int a, int b) {
        x = a;
        y = b;
    }
}


이 클래스에는 단일 생성자가 포함되어 있습니다. 생성자는 클래스 이름과 동일한 이름을 사용하며 리턴 타입이 없다는 점에서 인식할 수 있습니다. Point 클래스의 생성자는 두 개의 정수 아규먼트(int a, int b)를 취합니다. 다음 문장은 이 아규먼트들에 23과 94라는 값을 제공합니다:

Point originOne = new Point(23, 94);


이 statement을 실행한 결과는 다음 그림으로 설명할 수 있습니다:

 

 

다음은 네 개의 생성자를 포함하는 Rectangle 클래스의 코드입니다:

public class Rectangle {
    public int width = 0;
    public int height = 0;
    public Point origin;

    // 네 개의 생성자
    public Rectangle() {
        origin = new Point(0, 0);
    }
    public Rectangle(Point p) {
        origin = p;
    }
    public Rectangle(int w, int h) {
        origin = new Point(0, 0);
        width = w;
        height = h;
    }
    public Rectangle(Point p, int w, int h) {
        origin = p;
        width = w;
        height = h;
    }

    // 사각형을 이동시키는 메서드
    public void move(int x, int y) {
        origin.x = x;
        origin.y = y;
    }

    // 사각형의 면적을 계산하는 메서드
    public int getArea() {
        return width * height;
    }
}


각 생성자는 기본 타입과 참조 타입을 모두 사용하여 사각형의 원점, 너비 및 높이에 대한 초기 값을 제공할 수 있게 합니다. 클래스에 여러 생성자가 있는 경우, 이들은 서로 다른 시그니처를 가져야 합니다. 자바 컴파일러는 아규먼트의 개수와 타입을 기반으로 생성자를 구분합니다. 자바 컴파일러가 다음 코드를 만나면, Point 아규먼트 하나와 두 개의 정수 아규먼트를 필요로 하는 Rectangle 클래스의 생성자를 호출할 것을 알게 됩니다:

Rectangle rectOne = new Rectangle(originOne, 100, 200);


이 코드는 origin을 originOne으로 초기화하는 Rectangle의 생성자 중 하나를 호출합니다. 또한, 생성자는 width를 100으로, height를 200으로 설정합니다. 이제 동일한 Point 객체에 대한 두 개의 참조가 존재하게 됩니다. 객체는 여러 참조를 가질 수 있습니다. 이는 다음 그림에서 설명됩니다.

 

다음 코드 라인은 너비와 높이에 대한 초기 값을 제공하는 두 개의 정수 아규먼트를 필요로 하는 Rectangle 생성자를 호출합니다. 생성자 내부 코드를 검사하면 x와 y 값이 0으로 초기화된 새로운 Point 객체를 생성하는 것을 볼 수 있습니다:

Rectangle rectTwo = new Rectangle(50, 100);


다음 statement에서 사용된 Rectangle 생성자는 아규먼트를 받지 않으므로, 파라미터가 없는 생성자라고 부릅니다:

Rectangle rect = new Rectangle();


모든 클래스에는 적어도 하나의 생성자가 있습니다. 클래스가 명시적으로 생성자를 선언하지 않으면, 자바 컴파일러는 자동으로 디폴트 생성자(파라미터가 없는 생성자)를 제공합니다. 이 디폴트 생성자는 클래스 부모의 파라미터가 없는 생성자를 호출합니다. 클래스에 다른 부모가 없으면 Object 생성자를 호출합니다. 부모 클래스에 생성자가 없는 경우(그러나 Object 클래스에는 생성자가 있습니다), 컴파일러는 프로그램을 거부할 것입니다.

 

Using Objects

객체를 생성한 후에는 아마도 이를 무언가에 사용하고 싶을 것입니다. 객체의 필드 값 중 하나를 사용하거나, 필드 중 하나를 변경하거나, 또는 객체의 메서드 중 하나를 호출하여 어떤 작업을 수행할 필요가 있을 수 있습니다.

 

Referencing an Object's Fields

객체 필드는 이름을 통해 접근합니다. 명확한 이름을 사용해야 합니다.

필드를 그 클래스 내에서 사용할 때는 단순히 이름을 가지고 사용할 수 있습니다. 예를 들어, Rectangle 클래스 내에 너비와 높이를 출력하는 statement을 추가할 수 있습니다:

System.out.println("Width and height are: " + width + ", " + height);


이 경우, widthheight는 간단한 이름입니다.

객체의 클래스 외부에 있는 코드는 객체 참조나 expression을 사용하고, 그 뒤에 dot (.) 연산자와 간단한 필드 이름을 사용해야 합니다. 예를 들어:

objectReference.fieldName


CreateObjectDemo 클래스의 코드는 Rectangle 클래스의 코드 외부에 있습니다. 따라서 CreateObjectDemo 클래스에서 rectOne이라는 이름의 Rectangle 객체 내의 origin, width 및 height 필드를 참조하려면 각각 rectOne.origin, rectOne.width 및 rectOne.height라는 이름을 사용해야 합니다. 프로그램은 rectOne의 너비와 높이를 출력하기 위해 이들 이름 중 두 개를 사용합니다:

System.out.println("Width of rectOne: "  + rectOne.width);
System.out.println("Height of rectOne: " + rectOne.height);

 

CreateObjectDemo 클래스의 코드에서 widthheight라는 간단한 이름을 사용하려고 하면 의미가 없습니다. 해당 필드들은 객체 내부에서만 존재하며, 결과적으로 컴파일러 오류가 발생합니다.

나중에, 프로그램은 유사한 코드를 사용하여 rectTwo에 대한 정보를 출력합니다. 동일한 유형의 객체는 동일한 인스턴스 필드의 복사본을 가지고 있습니다. 따라서 각 Rectangle 객체는 origin, widthheight라는 필드를 가지고 있습니다. 객체 참조를 통해 인스턴스 필드에 접근할 때, 해당 특정 객체의 필드를 참조합니다. CreateObjectDemo 프로그램의 두 객체인 rectOnerectTwo는 각각 다른 origin, width 및 height 필드를 가지고 있습니다.

필드에 접근하려면, 이전 예제와 같이 객체에 대한 명명된 참조를 사용할 수 있으며, 또는 객체 참조를 반환하는 표현식을 사용할 수 있습니다. new 연산자가 객체에 대한 참조를 반환한다는 것을 상기하십시오. 따라서 new로부터 반환된 값을 사용하여 새 객체의 필드에 접근할 수 있습니다:

int height = new Rectangle().height;


이 문장은 새로운 Rectangle 객체를 생성하고 즉시 그 높이를 가져옵니다. 본질적으로, 이 문장은 Rectangle의 기본 높이를 계산합니다. 이 문장이 실행된 후, 프로그램은 생성된 Rectangle에 대한 참조를 더 이상 가지지 않습니다. 참조를 어디에도 저장하지 않았기 때문입니다. 이 객체는 참조되지 않으며, 그 자원은 자바 가상 머신에 의해 재활용될 수 있습니다.

 

Calling an Object's Methods

객체의 메서드를 호출하기 위해서는 객체 참조를 사용합니다. 객체 참조에 메서드의 간단한 이름을 dot(.) 연산자로 연결하고, 괄호 안에 메서드의 인수를 제공합니다. 메서드에 아규먼트가 필요하지 않은 경우에는 빈 괄호를 사용합니다.

objectReference.methodName(argumentList);


또는:

objectReference.methodName();


Rectangle 클래스에는 두 개의 메서드가 있습니다: 사각형의 면적을 계산하는 getArea() 메서드와 사각형의 원점을 변경하는 move() 메서드입니다. 다음은 이 두 메서드를 호출하는 CreateObjectDemo 코드입니다:

System.out.println("Area of rectOne: " + rectOne.getArea());
...
rectTwo.move(40, 72);



첫 번째 statement은 rectOnegetArea() 메서드를 호출하여 결과를 출력합니다. 두 번째 라인은 rectTwo를 이동시키는데, 이는 move() 메서드가 객체의 origin.xorigin.y에 새로운 값을 할당하기 때문입니다.

인스턴스 필드와 마찬가지로, objectReference는 객체에 대한 참조여야 합니다. 변수 이름을 사용할 수 있지만, 객체 참조를 반환하는 표현식을 사용할 수도 있습니다. new 연산자는 객체 참조를 반환하므로, new로부터 리턴된 값을 사용하여 새 객체의 메서드를 호출할 수 있습니다:

new Rectangle(100, 50).getArea()


new Rectangle(100, 50) expression은 Rectangle 객체를 참조하는 객체 참조를 반환합니다. 이처럼 dot 표기법을 사용하여 새로운 Rectangle의 getArea() 메서드를 호출하여 새 사각형의 면적을 계산할 수 있습니다.

일부 메서드, 예를 들어 getArea()는 값을 리턴합니다. 값을 리턴하는 메서드의 경우, 메서드 호출을 expression에 사용할 수 있습니다. 리턴값을 변수에 할당하거나, 결정을 내리거나, 루프를 제어하는 데 사용할 수 있습니다. 이 코드는 getArea()가 리턴한 값을 areaOfRectangle 변수에 할당합니다:

int areaOfRectangle = new Rectangle(100, 50).getArea();


특정 객체의 메서드를 호출하는 것은 그 객체에 메시지를 보내는 것과 동일합니다. 이 경우, getArea()가 호출되는 객체는 생성자에 의해 반환된 사각형 객체입니다.

 

The Garbage Collector

일부 객체 지향 언어에서는 생성한 모든 객체를 추적하고 더 이상 필요하지 않을 때 명시적으로 파괴해야 합니다. 메모리를 명시적으로 관리하는 것은 번거롭고 오류가 발생하기 쉽습니다. 자바 플랫폼에서는 원하는 만큼 객체를 생성할 수 있으며(물론 시스템이 감당할 수 있는 범위 내에서), 객체를 파괴하는 것에 대해 걱정할 필요가 없습니다. 자바 런타임 환경은 더 이상 사용되지 않는 객체를 자동으로 삭제합니다. 이 과정을 가비지 컬렉션(Garbage Collection)이라고 합니다.

객체에 대한 모든 참조가 없어지면 그 객체는 가비지 컬렉션 대상이 됩니다. 변수에 보관된 참조는 변수가 스코프를 벗어날 때 일반적으로 제거됩니다. 또는, 변수를 특별한 값인 null로 설정하여 객체 참조를 명시적으로 제거할 수도 있습니다. 하나의 프로그램이 동일한 객체에 대한 여러 참조를 가질 수 있다는 점을 기억하세요. 객체가 가비지 컬렉션 대상이 되려면 해당 객체에 대한 모든 참조가 제거되어야 합니다.

자바 런타임 환경에는 가비지 컬렉터가 있어서 더 이상 참조되지 않는 객체가 사용하는 메모리를 주기적으로 해제합니다. 가비지 컬렉터는 적절한 시점이라고 판단되면 자동으로 작업을 수행합니다.

 

More on Classes

이 섹션에서는 객체 참조와 dot(.) 연산자를 사용하는 클래스의 더 많은 측면을 다룹니다. 이는 이전 섹션에서 객체에 대해 배운 내용을 바탕으로 합니다:

 메서드에서 값 리턴하기
 this 키워드
 클래스 멤버와 인스턴스 멤버의 차이
 접근 제어

 

Returning a Value from a Method

메서드는 다음 상황 중 하나가 발생하면 이를 호출한 코드로 반환됩니다:

 메서드 내의 모든 statements을 완료했을 때,
 return statement에 도달했을 때,
 예외를 던졌을 때 (나중에 다룹니다),

package com.intheeast.java;


// public : access modifier
public class Main {
	// 디폴트로 0이라는 값으로 설정된다. 
	int gear; // 최초의 필드...

	// JVM이 호출한다..
	public static void main(String[] args) {
		// TODO Auto-generated method stub
				
		for (String arg : args) {
			if (arg == "exit")
				return ;
		}
		
		System.out.println("+Main:main");
		
		int world = Person.HELLO;
		
		Person sung = new Person(); // 
		Person lee = new Person(
				"taehum",
				31,
				"Man",
				"daegu",
				"0102223333"
				);
		//lee.
		Person Kim = null;
		Kim.getName(); // NullPointerException...		
		
		// Object method
		String name = lee.getName();
		
		// Class static Method
		Person.helloWorld();
				
		System.out.println("-Main:main");		
		
		return ;
		
		// 아래 statement 코드는 신택스 에러(컴파일 에러)
		//System.arraycopy(lee, world, name, world, world);
	}
}

예제

 


어느 것이든 가장 먼저 발생하는 경우입니다.

메서드 선언에서 메서드의 리턴 타입을 선언합니다. 메서드 본문 내에서 return statement을 사용하여 값을 리턴합니다.

void로 선언된 메서드는 값을 리턴하지 않습니다. 반드시 return 문을 포함할 필요는 없지만, 포함할 수도 있습니다. 이 경우, return statement은 제어 흐름 블록에서 벗어나 메서드를 종료하는 데 사용되며, 단순히 다음과 같이 사용됩니다:

return;

 

void로 선언된 메서드에서 값을 반환하려고 하면 컴파일 오류가 발생합니다.

void로 선언되지 않은 메서드는 해당하는 반환 값을 가진 return 문을 포함해야 합니다. 예를 들어:

return returnValue;


리턴 값의 데이터 타입은 메서드의 선언된 리턴 타입과 일치해야 합니다. 예를 들어, boolean을 리턴하도록 선언된 메서드에서 정수 값을 리턴할 수 없습니다.

객체에 대한 섹션에서 논의된 Rectangle 클래스의 getArea() 메서드는 정수 값을 리턴합니다:

// 사각형의 면적을 계산하는 메서드
public int getArea() {
    return width * height;
}


이 메서드는 width * height expression이 계산하는 정수를 반환합니다.

getArea 메서드는 기본 타입을 반환합니다. 메서드는 참조 타입도 반환할 수 있습니다. 예를 들어, Bicycle 객체를 조작하는 프로그램에서 다음과 같은 메서드가 있을 수 있습니다:

public Bicycle seeWhosFastest(Bicycle myBike, Bicycle yourBike,
                              Environment env) {
    Bicycle fastest;
    // 각 자전거의 기어와 케이던스, 환경(지형과 바람)을 고려하여
    // 어떤 자전거가 더 빠른지 계산하는 코드
    fastest = new Bicycle();
    
    return fastest;
}


이 메서드는 가장 빠른 Bicycle 객체에 참조(값)를 반환합니다.

 

Returning a Class or Interface

이 섹션이 혼란스럽다면, 인터페이스와 상속에 대한 레슨을 마친 후에 다시 돌아오세요.

메서드가 whosFastest처럼 클래스 이름을 리턴 타입으로 사용하는 경우, 리턴되는 객체의 클래스 타입은 리턴 타입의 서브클래스이거나 정확히 같은 클래스여야 합니다. 예를 들어, ImaginaryNumber가 java.lang.Number의 서브클래스이고, Number는 Object의 서브클래스인 클래스 계층이 있다고 가정해 보겠습니다. 다음 그림과 같이 나타낼 수 있습니다.

 

The class hierarchy for ImaginaryNumber

 

이제 Number를 반환하도록 선언된 메서드가 있다고 가정해 봅시다:

public Number returnANumber() {
    ...
}

 

returnANumber 메서드는 ImaginaryNumber를 리턴할 수 있지만, Object를 반환할 수는 없습니다. ImaginaryNumber는 Number의 서브클래스이므로 Number입니다. 하지만 Object는 반드시 Number일 필요는 없습니다. Object는 String이나 다른 타입일 수 있습니다.

메서드를 재정의하여 원래 메서드의 서브클래스를 반환하도록 정의할 수도 있습니다. 예를 들어:

public ImaginaryNumber returnANumber() {
    ...
}


이 기술을 공변 반환 타입(covariant return type)이라고 하며, 리턴 타입이 서브클래스의 방향으로 변하는 것이 허용된다는 것을 의미합니다.
참고: 인터페이스 이름도 리턴 타입으로 사용할 수 있습니다. 이 경우, 리턴되는 객체는 지정된 인터페이스를 구현해야 합니다.

 

예제

상위 클래스

class Animal {
    public Animal getInstance() {
        return new Animal();
    }

    public void makeSound() {
        System.out.println("Some generic animal sound");
    }
}

 

하위 클래스

class Dog extends Animal {
    @Override
    public Dog getInstance() {
        return new Dog();  // 코바리언트 반환 타입 사용
    }

    @Override
    public void makeSound() {
        System.out.println("Woof");
    }
}

 

메인 클래스

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Animal();
        myAnimal.makeSound();  // 출력: Some generic animal sound

        Dog myDog = new Dog();
        myDog.makeSound();  // 출력: Woof

        Animal animalInstance = myAnimal.getInstance();
        animalInstance.makeSound();  // 출력: Some generic animal sound

        Dog dogInstance = myDog.getInstance();
        dogInstance.makeSound();  // 출력: Woof
    }
}

 

설명

  • Animal 클래스의 getInstance 메서드는 Animal 타입을 반환합니다.
  • Dog 클래스는 Animal 클래스를 상속받아 getInstance 메서드를 오버라이드하며, 이 메서드는 Dog 타입을 반환합니다. 이는 코바리언트 반환 타입의 예입니다.
  • 메인 클래스에서는 Animal과 Dog 객체를 생성하고, 각 객체의 getInstance 메서드를 호출하여 반환된 인스턴스의 타입에 따라 올바른 메서드가 호출되는 것을 확인할 수 있습니다.

이 예제는 코바리언트 반환 타입이 자바에서 어떻게 사용되는지를 보여줍니다. Dog 클래스의 getInstance 메서드는 Animal 클래스의 getInstance 메서드를 오버라이드하지만, 반환 타입이 Dog로 변경되었습니다. 이는 반환 타입이 부모 클래스의 반환 타입의 서브타입일 경우에만 가능합니다.

 

Using the this Keyword

인스턴스 메서드나 생성자 내에서 this는 현재 객체, 즉 메서드나 생성자가 호출되고 있는 객체에 대한 참조입니다. 인스턴스 메서드나 생성자 내에서 this를 사용하여 현재 객체의 모든 멤버에 접근할 수 있습니다.

 

Using this with a Field

this 키워드를 사용하는 가장 일반적인 이유는 필드가 메서드나 생성자의 파라미터에 의해 가려지기 때문입니다.

예를 들어, Point 클래스는 다음과 같이 작성되었습니다:

public class Point {
    public int x = 0;
    public int y = 0;
        
    // 생성자
    public Point(int a, int b) {
        this.x = a;
        this.y = b;
    }
    
    public int getX() {
    	return this.x;
    }
    
    // ... 생략
}



그러나 다음과 같이 작성할 수도 있습니다:

public class Point {
    public int x = 0;
    public int y = 0;
        
    // 생성자
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}


생성자의 각 아규먼트는 객체의 필드 중 하나를 가립니다. 생성자 내부에서 x는 생성자의 첫 번째 아규먼트의 로컬 복사본입니다. Point 필드 x를 참조하기 위해서는 생성자가 this.x를 사용해야 합니다.

 

Using this with a Constructor

생성자 내에서 this 키워드를 사용하여 같은 클래스 내의 다른 생성자를 호출할 수도 있습니다. 이를 명시적 생성자 호출(explicit constructor invocation)이라고 합니다. 다음은 객체 섹션에서 다룬 것과 다른 구현을 가진 Rectangle 클래스입니다.

public class Rectangle {
    private int x, y;
    private int width, height;
        
    public Rectangle() {
        this(0, 0, 1, 1);
    }
    public Rectangle(int width, int height) {
        this(0, 0, width, height);
    }
    public Rectangle(int x, int y, int width, int height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }
    ...
}


이 클래스는 일련의 생성자를 포함하고 있습니다. 각 생성자는 사각형의 멤버 변수 중 일부 또는 전부를 초기화합니다. 생성자는 아규먼트로 초기값이 제공되지 않은 멤버 변수에 대해 기본값을 제공합니다. 예를 들어, 매개변수가 없는 생성자는 0,0 좌표에 1x1 사각형을 생성합니다. 두 개의 아규먼트를 받는 생성자는 네 개의 아규먼트를 받는 생성자를 호출하여 너비와 높이를 전달하지만 항상 0,0 좌표를 사용합니다. 이전과 마찬가지로, 컴파일러는 아규먼트의 수와 유형에 따라 어떤 생성자를 호출할지 결정합니다.

다른 생성자를 호출하는 구문이 존재한다면, 이는 생성자 내에서 첫 번째 라인에 있어야 합니다.

 

Controlling Access to Members of a Class

액세스 수정자[Access Modifier]는 다른 클래스가 특정 필드를 사용할 수 있는지 또는 특정 메서드를 호출할 수 있는지를 결정합니다. 접근 제어에는 두 가지 수준이 있습니다:

  Top Level[class 지정] 수준: public 또는 package-private[패키지 전용](명시적 수정자가 없음).

 

※ 위 이미지처럼 클래스 생성 위자드에서 class access modifer로 선택할 수 있는 것은,

public 과 package-private 뿐입니다.

 

예제

package com.intheeast.hello;  ///////////////////

// 디폴트 : package-private -> 패키지 전용...
class TopLevelPackage {
	
	int a;
	int b;
	
	// 디폴트 컨스트럭터는 컴파일러에 의해 생성됨.
	public static void helloWorld() {
		
	}
}

 

TopLevelPackage 클래스를 다른 패키지의 클래스에서 액세스할 수 없습니다.

클래스는 public 수정자로 선언될 수 있으며, 이 경우 해당 클래스는 모든 곳에서 모든 클래스에 보입니다. 클래스에 수정자가 없으면(디폴트, package-private으로도 알려짐), 해당 클래스는 자신의 패키지 내에서만 보입니다(패키지는 관련된 클래스의 이름이 지정된 그룹입니다 — 나중에 이에 대해 배우게 될 것입니다).

 Member Level: public, private, protected 또는 패키지 전용[package-private](명시적 수정자가 없음).

 

예제

mypackage 패키지에서 구현된 Parent 클래스

 

mysubpackage.mysubpackage에서 구현된 Child(mypackage 패키지의 Parent 클래스 상속) 클래스

 

또 다른 패키지(anotherpackage)에서 구현된 AnotherPackageClass

 

예제

X 클래스 (com.intheeast.hello 패키지)

package com.intheeast.hello;

public class X {
    protected String protectedField = "Protected Field in X";

    protected void protectedMethod() {
        System.out.println("Protected Method in X");
    }
}

 

Y 클래스 (com.intheeast.world 패키지)

package com.intheeast.world;

import com.intheeast.hello.X;

public class Y extends X {
    public void accessProtectedMembers() {
        // 서브클래스 내에서는 protected 멤버에 접근 가능
        System.out.println(protectedField);
        protectedMethod();
    }

    public static void main(String[] args) {
        Y y = new Y();
        y.accessProtectedMembers();

        // 다른 패키지에서 상속받지 않은 클래스는 protected 멤버에 접근 불가
        // X x = new X();
        // System.out.println(x.protectedField); // 컴파일 오류
        // x.protectedMethod(); // 컴파일 오류
    }
}

 

서브클래스에서의 접근

  • Y 클래스는 X 클래스의 protected 멤버를 상속받으며, 자신의 메서드 내에서 protected 멤버에 액세스할 수 있습니다.
  • Y 클래스의 인스턴스는 y는 accessProtectedMembers 메서드를 호출하여, X 클래스의 protected 멤버에 접근할 수 있습니다.

멤버 수준에서는 public 수정자나 명시적 수정자가 없는 경우(패키지 전용) 최상위 클래스와 동일한 의미로 사용할 수 있습니다. 멤버에 대해서는 두 개의 추가적인 접근 수정자가 있습니다: private와 protected. private 수정자는 멤버가 자신의 클래스 내에서만 접근할 수 있음을 지정합니다. protected 수정자는 멤버가 자신의 패키지 내에서만 접근할 수 있음을 지정하며(package-private과 동일), 추가적으로 다른 패키지에 있는 해당 클래스의 하위 클래스에서도 접근할 수 있습니다.

다음 표는 각 수정자가 허용하는 멤버 접근을 보여줍니다:

Access Levels

Modifier Class Package Subclass World
public Y Y Y Y
protected Y Y Y N
no modifier Y Y N N
private Y N N N

 

첫 번째 데이터 열은 클래스 자체가 해당 접근 수준에 따라 정의된 멤버에 접근할 수 있는지를 나타냅니다. 보시다시피, 클래스는 항상 자신의 멤버에 접근할 수 있습니다. 두 번째 열은 클래스와 같은 패키지에 있는 다른 클래스들이 해당 멤버에 접근할 수 있는지를 나타냅니다(상속 관계와 상관없이). 세 번째 열은 해당 패키지 외부에 선언된 하위 클래스들이 멤버에 접근할 수 있는지를 나타냅니다. 네 번째 열은 모든 클래스가 멤버에 접근할 수 있는지를 나타냅니다.

액세스 레벨은 두 가지 방식으로 영향을 미칩니다. 첫째, 자바 플랫폼의 클래스와 같이 다른 소스에서 온 클래스를 사용할 때, 액세스 레벨이 해당 클래스의 멤버를 자신의 클래스에서 사용할 수 있는지를 결정합니다. 둘째, 클래스를 작성할 때, 클래스의 각 멤버 변수와 메서드에 어떤 액세스 레벨을 부여할지를 결정해야 합니다.

다음은 클래스 컬렉션을 통해 액세스 레이 가시성에 어떻게 영향을 미치는지를 살펴보겠습니다. 다음 그림은 이 예제의 네 가지 클래스와 그들 간의 관계를 보여줍니다.

 

Classes and Package of the Example Used to Illustrate Access Levels

 

다음 테이블은 Alpha 클래스의 멤버가 적용될 수 있는 각 액세스 한정자에 대해 표시되는 위치를 보여줍니다.

 

Visibility

Modifier Alpha Beta Alphasub Gamma
public Y Y Y Y
protected Y Y Y N
no modifier Y Y N N
private Y N N N

 

 


액세스 레벨 선택에 대한 팁:
다른 프로그래머가 여러분의 클래스를 사용할 경우, 오용으로 인한 오류가 발생하지 않도록 해야 합니다. 액세스 레벨은 이를 도울 수 있습니다.

  • 특정 멤버에 대해 의미가 있는 가장 제한적인 접근 수준을 사용하세요. 특별한 이유가 없다면 private를 사용하세요.
  • 상수를 제외하고는 public 필드를 피하세요. (튜토리얼의 많은 예제에서는 public 필드를 사용합니다. 이는 몇 가지 포인트를 간결하게 설명하는 데 도움이 될 수 있지만, 실제 코드에서는 권장되지 않습니다.) public 필드는 특정 구현에 묶이기 쉬워 코드 변경 시 유연성이 제한됩니다.

 

Understanding Class Members

이 섹션에서는 클래스의 인스턴스가 아니라 클래스 자체에 속하는 필드와 메서드를 생성하기 위해 static 키워드를 사용하는 것에 대해 논의합니다.

 

Class Variables

동일한 클래스에서 여러 객체를 생성할 때, 각 객체는 인스턴스 변수의 고유한 복사본을 갖습니다. Bicycle 클래스의 경우, 인스턴스 변수는 cadence, gear, speed입니다. 각 Bicycle 객체는 이 변수들에 대한 고유한 값을 가지며, 이는 다른 메모리 위치에 저장됩니다.

때로는 모든 객체에 공통된 변수를 가지고 싶을 때가 있습니다. 이는 static 수정자를 사용하여 달성할 수 있습니다. 선언에 static 수정자가 있는 필드는 정적 필드 또는 클래스 변수라고 불립니다. 이들은 어떤 객체와도 관련되지 않고 클래스와 연관됩니다. 클래스의 모든 인스턴스는 하나의 고정된 메모리 위치에 있는 클래스 변수를 공유합니다. 어떤 객체든 클래스 변수의 값을 변경할 수 있지만, 클래스 변수를 조작하기 위해 객체를 생성할 필요는 없습니다.

예를 들어, 여러 개의 Bicycle 객체를 생성하고 각 객체에 일련 번호를 할당한다고 가정해 봅시다. 첫 번째 객체의 ID 번호는 1부터 시작합니다. 이 ID 번호는 각 객체에 고유하므로 인스턴스 변수입니다. 동시에 몇 개의 Bicycle 객체가 생성되었는지 추적하는 필드가 필요합니다. 이는 다음 객체에 어떤 ID를 할당할지 알기 위해 필요합니다. 이러한 필드는 개별 객체와 관련이 없고 클래스 전체와 관련이 있습니다. 이를 위해 numberOfBicycles라는 클래스 변수가 필요합니다. 다음과 같이 작성할 수 있습니다:

public class Bicycle {
        
    private int cadence;
    private int gear;
    private int speed;
        
    // 객체 ID를 위한 인스턴스 변수 추가
    private int id;
    
    // 생성된 Bicycle 객체의 수를 추적하는 클래스 변수 추가
    private static int numberOfBicycles = 0;
        ...
}


클래스 변수는 클래스 이름 자체로 참조됩니다. 예를 들어:

Bicycle.numberOfBicycles


이렇게 하면 클래스 변수임이 명확해집니다.

참고: 정적 필드는 다음과 같이 객체 참조로도 참조할 수 있습니다.

myBike.numberOfBicycles


그러나 이는 클래스 변수임을 명확히 하지 않기 때문에 권장되지 않습니다.

Bicycle 생성자를 사용하여 id 인스턴스 변수를 설정하고 numberOfBicycles 클래스 변수를 증가시킬 수 있습니다:

public class Bicycle {
        
    private int cadence;
    private int gear;
    private int speed;
    private int id;
    private static int numberOfBicycles = 0;
        
    public Bicycle(int startCadence, int startSpeed, int startGear){
        gear = startGear;
        cadence = startCadence;
        speed = startSpeed;

        // Bicycle 수를 증가시키고 ID 번호를 할당
        id = ++numberOfBicycles;
    }

    // ID 인스턴스 변수를 반환하는 새로운 메서드 추가
    public int getID() {
        return id;
    }
        ...
}


이와 같이 작성하면 각 Bicycle 객체에 고유한 ID를 할당하면서도, 생성된 전체 Bicycle 객체의 수를 추적할 수 있습니다.

 

Class Static Methods

자바 프로그래밍 언어는 정적 변수뿐만 아니라 정적 메서드도 지원합니다. 선언에 static 수정자가 있는 정적 메서드는 클래스의 인스턴스를 생성할 필요 없이 클래스 이름으로 호출해야 합니다. 예를 들어:

ClassName.methodName(args)


참고: 정적 메서드는 다음과 같이 객체 참조로도 호출할 수 있습니다:

instanceName.methodName(args)


그러나 이는 클래스 메서드임을 명확히 하지 않기 때문에 권장되지 않습니다.

정적 메서드의 일반적인 사용 사례는 정적 필드에 접근하는 것입니다. 예를 들어, Bicycle 클래스에 numberOfBicycles 정적 필드에 접근하기 위한 정적 메서드를 추가할 수 있습니다:

public static int getNumberOfBicycles() {
    return numberOfBicycles;
}


다음과 같은 인스턴스 변수와 클래스 변수 및 메서드의 조합은 허용되지 않습니다:

  • 인스턴스 메서드는 인스턴스 변수와 인스턴스 메서드에 직접 접근할 수 있습니다.
  • 인스턴스 메서드는 클래스 변수와 클래스 메서드에 직접 접근할 수 있습니다.
  • 클래스 메서드는 클래스 변수와 클래스 메서드에 직접 접근할 수 있습니다.
  • 클래스 메서드는 인스턴스 변수나 인스턴스 메서드에 직접 접근할 수 없습니다. 객체 참조를 사용해야 합니다. 또한, 클래스 메서드는 this 키워드를 사용할 수 없습니다. this는 인스턴스를 참조하기 때문입니다.

이 규칙들을 준수하면, 정적 메서드와 정적 변수를 효과적으로 사용할 수 있습니다.

 

Constants

static 수정자는 final 수정자와 결합하여 상수를 정의하는 데에도 사용됩니다. final 수정자는 이 필드의 값이 변경될 수 없음을 나타냅니다.

예를 들어, 다음 변수 선언은 원주율의 근사값인 PI라는 상수를 정의합니다:

static final double PI = 3.141592653589793;

 

이렇게 정의된 상수는 재할당할 수 없으며, 프로그램에서 이를 시도하면 컴파일 타임 오류가 발생합니다. 관례적으로 상수 값의 이름은 대문자로 작성됩니다. 이름이 여러 단어로 구성된 경우 단어는 밑줄(_)로 구분됩니다.

참고: 기본 타입 또는 문자열이 상수로 정의되고 그 값이 컴파일 타임에 알려진 경우, 컴파일러는 코드 내에서 상수 이름을 값으로 대체합니다. 이를 컴파일 타임 상수라고 합니다. 상수의 값이 외부 세계에서 변경되는 경우(예: 법적으로 원주율이 실제로 3.975여야 한다고 결정되는 경우), 이 상수를 사용하는 모든 클래스를 다시 컴파일하여 현재 값을 가져와야 합니다.

 

The Bicycle Class

이 섹션에서 모든 수정을 마친 후 Bicycle 클래스는 이제 다음과 같습니다.

public class Bicycle {
        
    private int cadence;
    private int gear;
    private int speed;
        
    private int id;
    
    private static int numberOfBicycles = 0;

        
    public Bicycle(int startCadence,
                   int startSpeed,
                   int startGear) {
        gear = startGear;
        cadence = startCadence;
        speed = startSpeed;

        id = ++numberOfBicycles;
    }

    public int getID() {
        return id;
    }

    public static int getNumberOfBicycles() {
        return numberOfBicycles;
    }

    public int getCadence() {
        return cadence;
    }
        
    public void setCadence(int newValue) {
        cadence = newValue;
    }
        
    public int getGear(){
        return gear;
    }
        
    public void setGear(int newValue) {
        gear = newValue;
    }
        
    public int getSpeed() {
        return speed;
    }
        
    public void applyBrake(int decrement) {
        speed -= decrement;
    }
        
    public void speedUp(int increment) {
        speed += increment;
    }
}

 

 

Initializing Fields

보시다시피, 필드 선언 시 초기 값을 제공할 수 있습니다:

public class BedAndBreakfast {

    // 10으로 초기화
    public static int capacity = 10;

    // false로 초기화
    private boolean full = false;
}


이 방법은 초기화 값이 존재하고 하나의 라인으로 초기화할 수 있을 때 잘 작동합니다. 그러나 이러한 초기화 방식은 그 단순성 때문에 한계가 있습니다. 초기화에 논리가 필요할 경우(예: 오류 처리 또는 복잡한 배열을 채우기 위한 for 루프 등) 간단한 할당은 불충분합니다. 인스턴스 변수는 생성자에서 초기화할 수 있으며, 이 경우 오류 처리나 다른 논리를 사용할 수 있습니다. 클래스 변수에 대해서도 동일한 기능을 제공하기 위해 자바 프로그래밍 언어는 정적 초기화 블록(static initialization blocks)을 포함합니다.

참고: 필드를 클래스 정의의 시작 부분에 선언할 필요는 없지만, 이는 가장 일반적인 관행입니다. 필드는 사용되기 전에 선언되고 초기화되기만 하면 됩니다.

 

Static Initialization Blocks

정적 초기화 블록(static initialization block)은 중괄호 {}로 둘러싸인 일반적인 코드 블록이며, static 키워드가 앞에 붙습니다. 예를 들면 다음과 같습니다:

static {
    // 초기화에 필요한 코드는 여기에 작성합니다.
}


클래스는 정적 초기화 블록을 여러 개 가질 수 있으며, 이들은 클래스 본문 내 어디에든 나타날 수 있습니다. 런타임 시스템은 정적 초기화 블록이 소스 코드에 나타나는 순서대로 호출됨을 보장합니다.

정적 블록에 대한 대안으로, private 정적 메서드를 작성할 수 있습니다:

class Whatever {
    public static varType myVar = initializeClassVariable();
        
    private static varType initializeClassVariable() {
        // 초기화 코드가 여기에 작성됩니다.
    }
}


private 정적 메서드의 장점은 클래스 변수를 다시 초기화해야 할 경우 나중에 재사용할 수 있다는 점입니다.

 

Initializing Instance Members

보통 인스턴스 변수를 초기화하는 코드는 생성자에 넣습니다. 인스턴스 변수를 초기화하기 위해 생성자를 사용하는 것에 대한 두 가지 대안이 있습니다: 초기화 블록과 final 메서드입니다.

인스턴스 변수의 초기화 블록은 정적 초기화 블록과 비슷하지만, static 키워드가 없습니다:

{
    // 초기화에 필요한 코드는 여기에 작성합니다.
}

 

자바 컴파일러는 초기화 블록을 각 생성자에 복사합니다. 따라서 이 방법은 여러 생성자 간에 코드를 공유하는 데 사용할 수 있습니다.

final 메서드는 서브클래스에서 오버라이드될 수 없습니다. 이는 인터페이스와 상속에 대한 레슨에서 논의됩니다. 다음은 인스턴스 변수를 초기화하기 위해 final 메서드를 사용하는 예입니다:

class Whatever {
    private varType myVar = initializeInstanceVariable();
        
    protected final varType initializeInstanceVariable() {

        // 초기화 코드는 여기에 작성됩니다.
    }
}


이 방법은 특히 서브클래스가 초기화 메서드를 재사용하려고 할 때 유용합니다. 메서드는 final로 선언되는데, 이는 인스턴스 초기화 중에 final이 아닌 메서드를 호출하면 문제가 발생할 수 있기 때문입니다.