객체지향 프로그래밍 2

LeeKyoungChang·2022년 2월 18일
0
post-thumbnail

Java의 정석 의 책을 읽고 정리한 내용입니다.

 

📚 1. 상속(inheritance)

  • 상속 : 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다.
  • 새로작성하고자 하는 클래스의 이름 뒤에 상속받고자 하는 클래스의 이름을 키워드 extends와 함께 써 주기만 하면 된다.
class Parent { }
class Child extends Parent { }

조상 클래스

  • 상속해주는 클래스
  • 부모(parent)클래스, 상위(super)클래스, 기반(base)클래스

자손 클래스

  • 상속받는 클래스
  • 자식(child)클래스, 하위(sub)클래스, 파생된(derived)클래스

 

class Parent { }
class Child extends Parent {
	void play(){
		System.out.println("놀자~");
	}
}
클래스클래스의 멤버
Parentage
Childage, play()
  • 상속을 받는다는 것은 조상 클래스를 확장(extend)한다는 의미로 해석할 수도 있으며 이것이 상속에 사용되는 키워드가 extends인 이유이기도 하다.
  • 같은 내용의 코드를 한 곳에서 관리함으로써 코드의 중복이 줄어든다.

✏️ 상속 정리

  • 생성자와 초기화 블럭은 상속되지 않는다. 멤버만 상속된다.
  • 자손 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 많다.

 

💡 참고
접근 제어자(access modifier)가 private 또는 default인 멤버들은 상속되지 않는다기보다 상속은 받지만 자손 클래스로부터의 접근이 제한되는 것이다.

 

스크린샷 2022-02-13 오후 4 40 00
  • Child 클래스는 GrandChild클래스의 직접 조상이다.
  • Parent클래스는 GrandChild클래스의 간접 조상이 된다.
  • GrandChild클래스는 Parent클래스와 간접적인 상속관계에 있다고 할 수 있다!

 

📌 정리
자손 클래스의 인스턴스를 생성하면 조상 클래스의 멤버와 자손 클래스의 멤버가 합쳐진 하나의 인스턴스로 생성된다.

 

✔️ 클래스간의 관계 - 포함관계

  • 상속 이외에도 클래스를 재사용하는 또 다른 방법이 있는데, 그것은 클래스 간에 포함(Composite) 관계를 맺어 주는 것이다.
  • 클래스 간의 포함관계를 맺어 주는 것은 한 클래스의 멤버변수로 다른 클래스 타입의 참조 변수를 선언하는 것을 뜻한다.
class Circle {
	int x;	// 원점 x
	int y;	// 원점 y
	int r;	// 반지름
}


class Point {
	int x;
	int y;
}

➡️

class Point {
	int x;
	int y;
}

class Circle { 
	Point c = new Point();   // 원점
	irt r;
}

 

✔️ 클래스간의 관계 결정하기

✏️ 클래스간의 관계
상속관계 : ~은 ~이다.(is-a)

  • 원은(Circle)은 점(Point)이다. - Circle is a Point

포함관계 : ~은~을 가지고 있다.(has-a)

  • 원(Circle)은 점(Point)을 가지고 있다. - Circle has a Point

 

public class DrawShape {
	public static void main(String[] args) {
		Point[] p = {	
				new Point(100, 100),
				new Point(140, 50),
				new Point(200, 100)
		};
		Triangle t = new Triangle(p);
		Circle c = new Circle(new Point(150, 150), 50);
		
		t.draw();
		c.draw();
	}

}

class Shape {
	String color = "black";
	void draw() {
		System.out.printf("[color=%s]%n", color);
	}
}

class Point {
	int x;
	int y;

	Point(int x, int y) {
		this.x = x;
		this.y = y;
	}

	Point() {
		this(0, 0);
	}

	String getXY() {
		return "(" + x + "," + y + ")"; // x와 y의 값을 문자열로 반환
	}
}

class Circle extends Shape {
	Point center;
	int r;

	Circle() {
		this(new Point(0, 0), 100);
}
	Circle(Point center, int r) {
		this.center = center;
		this.r = r;
	}
	
	void draw() {
		System.out.printf("[center=(%d, %d), r=%d, color=%s]%n", center.x, center.y, r, color);
	}
}

class Triangle extends Shape {
	Point[] p = new Point[3];
	
	Triangle(Point[] p) {
		this.p = p;
	}
	
	void draw() {
		System.out.printf("[p1=%s, p2=%s, p3=%s, color=%s]%n",
				p[0].getXY(),p[1].getXY(), p[2].getXY(), color);
	}
}
[p1=(100,100), p2=(140,50), p3=(200,100), color=black]
[center=(150, 150), r=50, color=black]

🔊 Circle, Point, Shape의 관계

  • A Circle is a Shape. // 1. 원은 도형이다.
  • A Circle has a Point. // 2. 원은 점을 가지고 있다.

CircleShape와 상속관계
CirclePoint와는 포함관계

class Circle extends Shape{ // Circle과 Shape는 상속관계
	Point center;	// Circle과 Point는 포함관계
	int r;
	
	// ...
	
}

 

복잡한 문장을 만났을 때는 이처럼 여러 문장으로 분해해보자!

Circle c = new Circle(new Point(150, 150), 50);

➡️

Point P = new Point(150, 150);
Circle c = new Circle(P, 50);

 

💡 참고
참조 변수의 출력이나 덧셈연산자를 이용한 참조변수와 문자열의 결합에는 toString()이 자동적으로 호출되어 참조변수를 문자열로 대치한 후 처리한다.

 

✔️ 단일 상속(single inheritance)

자바에서는 오직 단일 상속만을 허용한다.

class Tv {
	boolean power;	// 전원상태(on/off)
	int channel;	// 채널
	
	void power()	{ power = !power; }
	void channelUP() { ++channel; }
	void channelDown() { --channel; }
}

class VCR {
	boolean power;	// 전원상태(on/off)
	int counter = 0;
	void power() {	power = !power; }
		void play() { /*내용 생략*/ }
		void stop() { /*내용 생략*/ }
		void rew() { /*내용 생략*/ }
		void ff() { /*내용 생략*/ }
	}

class TVCR extends Tv {
	VCR vcr = new VCR();	// VCR클래스를 포함시켜서 사용한다.
	
	void play() {
		vcr.play();
	}
	
	void stop() {
		vcr.stop();
	}
	void rew() {
		vcr.rew();
	}
	void ff() {
		vcr.ff();
	}

}
  • 자바는 다중 상속을 허용하지 않으므로 Tv클래스를 조상으로 하고, VCR클래스는 TVCR클래스에 포함시켰다.
  • TVCR클래스에 VCR클래스의 메서드와 일치하는 선언부를 가진 메서드를 선언하고 내용은 VCR클래스의 것을 호출해서 사용하도록 했다.
  • 외부적으로는 TVCR클래스의 인스턴스를 사용하는 것처럼 보이지만 내부적으로는 VCR클래스의 인스턴스를 생성해서 사용하는 것이다.
  • 이렇게 함으로써 VCR클래스의 메서드의 내용이 변경되더라도 TVCR클래스의 메서드들 역시 변경된 내용이 적용되는 결과를 얻을 수 있을 것이다.

 

✔️ Object클래스 - 모든 클래스의 조상

스크린샷 2022-02-13 오후 5 08 11
  • 자바의 모든 클래스들을 Object클래스의 멤버들을 상속 받기 때문에 Object클래스에 정의된 멤버들을 사용할 수 있다!

 

📚 2. 오버라이딩(overriding)

오버 라이딩 : 조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것
override : ~위에 덮어쓰다(overwrite)

 

✔️ 오버라이딩의 조건

✏️ 오버라이딩이 성립하기 위한 조건
자손 클래스에서 오버라이딩하는 메서드는 조상 클래스의 메서드와

  • 이름이 같아야 한다.
  • 매개변수가 같아야 한다.
  • 반환타입이 같아야 한다.

 

접근 제어자(access modifier)와 예외(exception)는 제한된 조건 하에서만 다르게 변경할 수 있다.
(1) 접근 제어자는 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.

  • 만일 조상 클래스에 정의된 메서드의 접근 제어자가 protected라면, 이를 오버 라이딩하는 자손 클래스의 메서드는 접근 제어자가 protectedpublic이어야 한다.

(2) 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.

  • 아래의 코드를 보면 Child클래스의 parentMethod()에 선언된 예외의 개수가 조상인 Parent클래스의 parentMethod()에 선언된 예외의 개수보다 적으므로 바르게 오버라이딩되었다.
class Parent {
	void parentMethod() throws IOException, SQLException {
		// ...
	}
}
class Child extends Parent {
	void parentMethod() throws IOException {
		// ...
	}	
	// ...
}

음? 조상클래스에 정의된 메서드보다 적은 개수의 예외를 선언한 것 같다?
➡️ 아니다! Exception은 모든 예외의 최고 조상이므로 가장 많은 개수의 예외를 던질 수 있도록 선언한 것이다.
➡️ 그래서 예외의 개수는 적거나 같아야 한다는 조건을 만족시키지 못하는 잘못된 오버라이딩인 것이다.

 

✏️ 조상 클래스의 메서드를 자손 클래스에서 오버라이딩할 때
1. 접근 제어자를 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.
2. 예외는 조상 클래스의 메서드보다 많이 선언할 수 없다.
3. 인스턴스메서드를 static메서드로 또는 그 반대로 변경할 수 없다.

 

💡 참고
Q. 조상 클래스에 정의된 static 메서드를 자손 클래스에서 똑같은 이름의 static 메서드로 정의할 수 있을까?

A. 대답

  • 가능하다! 하지만, 이것은 각 클래스에 별개의 static메서드를 정의한 것일 뿐 오버라이딩이 아니다!
  • 각 메서드는 클래스이름으로 구별될 수 있으며, 호출할 때는 클래스이름.메서드이름()으로 하는 것이 바람직하다!
  • static멤버들은 자신들이 정의된 클래스에 묶여있다고 생각하면 된다!

 

✔️ 오버로딩 vs 오버라이딩

오버로딩(overloading) : 기존에 없는 새로운 메서드를 정의하는 것(new)
오버라이딩(overriding) : 상속받은 메서드의 내용을 변경하는 것(change, modify)

class Parent {
		void parentMethod() {}
}

class Child extends Parent {
		void parentMethod() {}	// 오버라이딩
		void parentMethod(int i) {}	// 오버로딩

		void childMethod() {}
		void childMethod(int i) {} // 오버로딩
		void childMethod() {} // 에러, 함수가 중복정의 되어있다.
}

 

✔️ super

super : 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는 데 사용되는 참조 변수이다.

  • 상속받은 멤버와 자신의 멤버와 이름이 같을 때는 super를 붙여서 구별할 수 있다.
  • this와 마찬가지로 super역시 static메서드에서는 사용할 수 없고 인스턴스메서드에서만 사용할 수 있다.
class SuperTest{
	public static void main(String args[]){
		Child c = new Child();
		c.method();
	}
}

class Parent{
	int x= 10 ;
}
  
class Child extends Parent{
	void method(){
    System.out.println("x=" + x);
    System.out.println("this.x=" + this.x);
    System.out.println("super.x=" + super.x);
	}
}
x=10
this.x=10
super.x=10
  • 모두 같은 변수를 의미, 같은 값이 출력된다.

 

class SuperTest2{
	public static void main(String args[]){
		Child c = new Child();
		c.method();
	}
}

class Parent{
	int x= 10 ;
}
  
class Child extends Parent{
	int x = 20;
	
	void method(){
    System.out.println("x=" + x);
    System.out.println("this.x=" + this.x);
    System.out.println("super.x=" + super.x);
	}
}
x=20
this.x=20
super.x=10
  • 서로 다른 값이 출력된다.
  • super.x : 조상 클래스로부터 상속받은 멤버변수 x를 뜻한다.
  • this.x : 자손 클래스에 선언된 멤버변수를 뜻한다.

➡️ 변수만 아니라 메서드 역시 super를 써서 호출할 수 있다.
➡️ 자식클래스에서 조상클래스의 메서드의 내용에 추가적으로 작업을 덧붙이는 경우라면 super를 사용해서 조상클래스의 메서드를 포함시키는 것이 좋다.

 

✔️ super() - 조상 클래스의 생성자

  • this()는 같은 클래스의 다른 생성자를 호출하는 데 사용되지만, super()는 조상 클래스의 생성자를 호출하는데 사용된다.

✏️ this()와 super() 호출?

  • Object클래스를 제외한 모든 클래스의 생성자 첫 줄에 생성자 this() 또는 super()를 호출해야 한다.
  • 그렇지 않으면 컴파일러가 자동적으로 super();를 생성자의 첫 줄에 삽입한다.

 

✏️ 인스턴스를 생성할 때 클래스를 선택, 생성자를 선택
1. 클래스 - 어떤 클래스의 인스턴스를 생성할 것인가?
2. 생성자 - 선택한 클래스의 어떤 생성자를 이용해서 인스턴스를 생성할 것인가?

 

⚠️ 주의
생성자가 정의되어 있는 클래스에는 컴파일러가 기본 생성자를 자동적으로 추가하지 않는다.

 

조상 클래스의 멤버변수는 이처럼 조상의 생성자에 의해 초기화되도록 해야 하는 것이다.

public class PointTest2 {

	public static void main(String[] args) {
		Point3D p3 = new Point3D();
		System.out.println("p3.x= "+p3.x);
		System.out.println("p3.y= "+p3.y);
		System.out.println("p3.z= "+p3.z);
	}
}

class Point{
	int x=10;
	int y=20;
	
	Point(int x, int y){
		this.x = x;
		this.y = y;
	}
}

class Point3D extends Point{
	int z = 30;
	
	Point3D(){
		this(100, 200, 300); // Point3D(int x, int y, int z)를 호출한다.
	}
	
	Point3D(int x, int y, int z){
		super(x,y); // Point(int x, int y)를 호출한다.
		this.z = z;
	}
}
p3.x= 100
p3.y= 200
p3.z= 300
  • Point클래스의 생성자 Point(int x, int y)는 어떠한 생성자도 호출하고 있지 않기 때문에 컴파일 후에 다음과 같은 코드로 변경된다.
	Point(int x, int y){
		super();	// 조상인 Object클래스의 생성자 Object()를 호출한다.
		this.x = x;
		this.y = y;
	}

생성자 호출 순서 : Point3D() → Point3D(int x, int y, int z) → Point(int x, int y) → Object()

  • 어떤 클래스의 인스턴스를 생성하면, 클래스 상속관계의 최고조상인 Object클래스까지 거슬러 올라가면서 모든 조상클래스의 생성자가 순서대로 호출된다는 것을 알 수 있다.

 

📚 3. package와 import

패키지 : 클래스의 묶음
패키지 : 클래스 또는 인터페이스를 포함시킬 수 있으며, 서로 관련된 클래스들끼리 그룹 단위로 묶어 놓음으로써 클래스를 효율적으로 관리할 수 있다.

  • String의 실제 이름 : java.lang.String
  • 클래스가 물리적으로 하나의 클래스파일(.class)인 것과 같이 패키지는 물리적으로 하나의 디렉토리이다.
    • 디렉터리가 하위 디렉토리를 가질 수 있는 것처럼, 패키지도 다른 패키지를 포함할 수 있으며 점 .으로 구분한다.

 

🔔 정리

  • 하나의 소스파일에는 첫 번째 문장으로 단 한 번의 패키지 선언만을 허용한다.
  • 모든 클래스는 반드시 하나의 패키지에 속해야 한다.
  • 패키지는 점 .을 구분자로 하여 계층구조로 구성할 수 있다.
  • 패키지는 물리적으로 클래스 파일(.class)을 포함하는 하나의 디렉토리이다.

 

✔ 패키지의 선언

package 패키지명;
  • 해당 소스파일에 포함된 모든 클래스나 인터페이스는 선언된 패키지에 속한다.
  • 소문자로 하는 것을 원칙으로 하고 있다.
  • 패키지를 지정하지 않는 모든 클래스들은 같은 패키지에 속하는 것이다.

 

✔ import 문

  • import문으로 사용하고자 하는 클래스의 패키지를 미리 명시해주면 소스코드에 사용되는 클래스 이름에서 패키 지명은 생략할 수 있다.
  • import문의 역할은 컴파일러에게 소스파일에 사용된 클래스의 패키지에 대한 정보를 제공하는 것이다.
  • 컴파일 시, 컴파일러는 import문을 통해 소스파일에 사용된 클래스들의 패키지를 알아낸 다음, 모든 클래스 이름 앞에 패키지명을 붙여준다.

 

✔ import문의 선언
import문은 package문과 달리 한 소스파일에 여러 번 선언할 수 있다.

일반적인 소스파일(*. java)의 구성은 다음의 순서로 되어 있다.
(1) package문
(2) import문
(3) 클래스 선언

선언하는 방법

import 패키지명.클래스명;
또는
import 패키지명.*;
  • 같은 패키지에서 여러 개의 클래스가 사용될 때, 패키지명.*을 이용해서 지정된 패키지에 속하는 모든 클래스를 패키지명 없이 사용할 수 있다.
  • 실행 시 성능상의 차이는 전혀 없다.

 

📢 주의할 점

  • import하는 패지키의 수가 많을 때는 어느 클래스가 어느 패키지에 속하는지 구별하기 어렵다.
  • import문에서 클래스의 이름 대신 *을 사용하는 것이 하위 패키지의 클래스까지 포함하는 것은 아니다.
import java.util.*;
import java.text.*;

=>
	
import java.*;  // 위 2개는 이걸로 대체 불가능

 

  • import문으로 패키지를 지정하지 않으면 모든 클래스이름 앞에 패키지명을 반드시 붙여야 한다.
  • 같은 패키지 내의 클래스들은 import문을 지정하지 않고도 패키지명을 생략할 수 있다.
  • 패키지명 없이 사용할 수 있는 String 와 같이 가능했던 이유 : 모든 소스파일에는 묵시적으로 import java.lang.*;이 선언되어 있기 때문이다.

 

✔ static import문

  • static import문을 사용하면 static멤버를 호출할 때 클래스 이름을 생략할 수 있다.
  • 특정 클래스의 static멤버를 자주 사용할 때 편리하다.

 

📚 4. 제어자(modifier)

제어자(modifier)는 클래스, 변수 또는 메서드의 선언부에 함께 사용되어 부가적인 의미를 부여한다.

접근 제어자 : public, protected, default, private
그 외 : static, final, abstract, native, transient, synchronized, volatile, strictfp

 

✔ static - 클래스의, 공통적인

static이 사용될 수 있는 곳 - 멤버변수, 메서드, 초기화블럭

📝 static(제어자)
(1) 멤버변수(대상)

  • 모든 인스턴스에 공통적으로 사용되는 클래스변수가 된다.
  • 클래스변수는 인스턴스를 생성하지 않고도 사용 가능하다.
  • 클래스가 메모리에 로드될 때 생성된다.

(2) 메서드(대상)

  • 인스턴스를 생성하지 않고도 호출이 가능한 static 메서드가 된다.
  • static메서드 내에서는 인스턴스멤버들을 직접 사용할 수 없다.
  • 인스턴스 멤버를 사용하지 않는 메서드는 static을 붙여서 static메서드로 선언하는 것이 좋다.
  • 가능하다면 static메서드로 하는 것이 인스턴스를 생성하지 않고도 호출이 가능해서 더 편리하고 속도도 더 빠르다.

 

💡 참고
static초기화 블록은 클래스가 메모리에 로드될 때 단 한 번만 수행되며, 주로 클래스 변수(static변수)를 초기화하는데 주로 사용된다.

class StaticTest{
	static int width = 200;	// 클래스 변수(static변수)
	static int height = 120;	// 클래스 변수(static변수)
	
	static{  // 클래스 초기화 블럭
		// static 변수의 복잡한 초기화 수행
	}
	static int max(int a, int b){ // 클래스 메서드(static 메서드)
		return a > b ? a : b;
		
	}
}

 

✔ final - 마지막의, 변경될 수 없는

final이 사용될 수 있는 곳 - 클래스, 메서드, 멤버변수, 지역변수

🔔 final(제어자)
(1) 클래스(대상)

  • 변경될 수 없는 클래스, 확장될 수 없는 클래스가 된다.
  • 그래서 final로 지정된 클래스는 다른 클래스의 조상이 될 수 없다.

(2) 메서드(대상)

  • 변경될 수 없는 메서드. final로 지정된 메서드는 오버라이딩을 통해 재정의 될 수 없다.

(3) 멤버변수, 지역변수(대상)

  • 변수 앞에 final이 붙으면, 값을 변경할 수 없는 상수가 된다.

 

final class FinalTest {	// 조상이 될 수 없는 클래스
	final int MAX_SIZE = 10;	// 값을 변경할 수 없는 멤버변수(상수)

	final void getMaxSize() {	// 오버라이딩할 수 없는 메서드(변경불가)
		final int LV = MAX_SIZE;	// 값을 변경할 수 없는 지역변수(상수)
		return MAX_SIZE;
	}
}

 

생성자를 이용한 final멤버 변수의 초기화할 때는?

  • final 인스턴스 변수의 경우 생성자에서 초기화되도록 할 수 있다.

 

✔ abstract - 추상의, 미완성의

abstract가 사용될 수 있는 곳 - 클래스, 메서드

🔔 abstract(제어자)
(1) 클래스(대상)

  • 클래스 내에 추상 메서드가 선언되어 있음을 의미한다.

(2) 메서드(대상)

  • 선언부만 작성하고 구현부는 작성하지 않은 추상 메서드임을 알린다.

 

abstract는 인스턴스를 생성할 수 없다.

abstract class AbstarctTest{	// 추상 클래스 (추상메서드를 포함한 클래스)
	abstract void move();	// 추상 메서드 (구현부가 없는 메서드)
}

 

✔ 접근 제어자(access modifier)

  • 접근 제어자는 멤버 또는 클래스에 사용되어, 해당하는 멤버 또는 클래스를 외부에서 접근하지 못하도록 제한하는 역할을 한다.
  • 클래스나 멤버변수, 메서드, 생성자에 접근 제어가 지정되어 있지 않다면, 접근 제어가 default임을 뜻한다.

🔔 접근 제어자가 사용될 수 있는 곳 - 클래스, 멤버 변수, 메서드, 생성자

제어자같은 클래스같은 패키지자손 클래스전체
publicoooo
protectedooo
(default)oo
privateo

 

💡 참고

  • 접근제어자가 default라는 것은 아무런 접근 제어자도 붙이지 않는 것을 의미한다.
대상사용가능한 접근 제어자
클래스public, (default)
메서드, 멤버변수public, protected, (default), private
지역변수없음

 

✔ 접근 제어자를 이용한 캡슐화

  • 데이터가 유효한 값을 유지하도록, 또는 비밀번호와 같은 데이터를 외부에서 함부로 변경하지 못하도록 하기 위해서는 외부로부터의 접근을 제한하는 것이 필요하다.
  • 데이터 감추기(data hiding)라고 하며, 객체지향 개념의 캡슐화(encapsulation)에 해당하는 내용이다.
  • 외부에서 접근할 필요가 없는 멤버들을 private으로 지정하여 외부에 노출시키지 않음으로써 복잡성을 줄일 수 있다.
접근 제어자를 사용하는 이유
- 외부로부터 데이터를 보호하기 위해서
- 외부에는 불필요한, 내부적으로만 사용되는, 부분을 감추기 위해서

 

  • get멤버변수이름 : 멤버변수의 값을 읽는 메서드의 이름
  • set멤버변수이름 : 멤버 변수의 값을 변경하는 메서드의 이름
  • getter : get으로 시작하는 메서드
  • setter : set으로 시작하는 메서드

 

💡 참고
하나의 소스파일(*.java)에는 public 클래스가 단 하나만 존재할 수 있으며, 소스파일의 이름은 반드시 public클래스의 이름과 같아야 한다.

 

✔ 생성자의 접근 제어자

  • 생성자의 접근제어자를 private으로 지정하면, 외부에서 생성자에 접근할 수 없으므로 인스턴스를 생성할 수 없게 된다.
  • 그래도 클래스 내부에서는 인스턴스를 생성할 수 있다.
class Singleton{
	private Singleton(){
		...
	}
	...
}
  • public메서드를 제공함으로써 외부에서 이 클래스의 인스턴스를 사용하도록 할 수 있다.
  • 이 메서드는 public인 동시에 static 이어야 한다.
class Singleton{
	...
	private static Singleton s = new Singleton();
	private Singleton(){
		...
	}
	// 인스턴스를 생성하지 않고도 호출할 수 있어야 하므로 static이어야 한다.
	public static Singleton getInstance(){
		return s;
	}
	...
}
  • 생성자가 private인 클래스는 다른 클래스의 조상이 될 수 없다.
  • 클래스 앞에 final을 추가하여 상속하지 못하는 클래스를 알리는 것이 좋다.

 

✔ 제어자(modifier)의 조합

대상사용가능한 제어자
클래스public, (default), final, abstract
메서드모든 접근제어자, final, abstract, static
멤버변수모든 접근 제어자, final, static
지역변수final

 

🔔 제어자를 조합해서 사용할 때 주의해야 할 사항
1. 메서드에 staticabstract를 함께 사용할 수 없다.

  • static메서드는 몸통이 있는 메서드에만 사용할 수 있기 때문이다.
  1. 클래스에 abstractfinal을 동시에 사용할 수 없다.
  • 클래스에 사용되는 final은 클래스를 확장할 수 없다는 의미
  • abstract는 상속을 통해서 완성되어야 한다는 의미이므로 서로 모순되기 때문이다.
  1. abstract메서드의 접근 제어자가 private일 수 없다.
  • abstract메서드는 자손클래스에서 구현해주어야 하는데 접근 제어자가 private이면, 자손클래스에서 접근할 수 없기 때문이다.
  1. 메서드에 privatefinal을 같이 사용할 필요는 없다.
  • 접근제어자가 private인 메서드는 오버라이딩될 수 없기 때문이다.
  • 이 둘 중 하나만 사용해도 의미가 충분하다.

 

📚 5. 다향성(polymorphism)

  • 객체지향 개념에서 다형성이란 여러 가지 형태를 가질 수 있는 능력을 의미하며, 자바에서는 한 타입의 참조 변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다.
  • 조상 클래스 타입의 참조 변수로 자손 클래스의 인스턴스를 참조할 수 있도록 하였다.
class Tv {
	boolean power; // 전원상태
	int channel;	// 채널
	
	void power(){power= !power;}
	void channelUp(){++channel;}
	void channelDown(){--channel;}
	
}
class CaptionTv extends Tv{
	String text; // 캡션을 보여 주기 위한 문자열
	void caption(){/* 내용 생략 */}
}
  • 클래스 TvCaptionTv는 서로 상속관계에 있다.
  • TvCaptionTv클래스가 서로 상속관계에 있을 경우, 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조하도록 하는 것이 가능하다.
Tv t = new CaptionTv();
  • 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.
  • 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다.

 

조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있다.
반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다.

&nbps;

✔ 참조 변수의 형 변환

형 변환 : 서로 상속관계에 있는 클래스사이에서만 가능하기 때문에 자손타입의 참조변수를 조상타입의 참조변수로, 조상타입의 참조변수를 자손타입의 참조변수로 형변환만 가능하다.

자손타입 ➡ 조상타입(Up-casting) : 형변환 생략가능
자손타입 ⬅ 조상타입(Down-casting) : 형변환 생략불가
class Car {
	String color;
	int door;
	void drive() {
		System.out.println("drive, Brrrr~");
	}
	void stop() {
		System.out.println("Stop!!!");
	}
}
class FireEngine extends Car {
	void water() {
		System.out.println("water!!");
	}
}
class Ambulance extends Car {
	void siren() {
		System.out.println("siren~~~");
	}
}
  • Car타입의 참조변수 cCar타입의 조상인 Object타입의 참조변수로 형변환 하는 것은 참조변수가 다룰 수 있는 멤버의 개수가 실제 인스턴스가 갖고 있는 멤버의 개수보다 적을 것이 분명하므로 문제가 되지 않는다.
  • 그래서 형변환을 생략할 수 있도록 한 것이다.
  • 형 변환은 참조 변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다.
  • 단지 참조변수의 형 변환을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것뿐이다.
  • 조상타입의 인스턴스를 자손타입의 참조변수로 참조하는 것은 허용되지 않는다.
서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다.
그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.

 

✔ instanceof연산자

  • instanceof의 왼쪽에는 참조 변수를 오른쪽에는 타입(클래스명)이 피연산자로 위치한다.
  • instanceof를 이용한 연산결과로 true가 나왔다면 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 의미한다.
public class InstanceofTest {
	public static void main(String[] args) {
		FireEngine fe = new FireEngine();

		if (fe instanceof FireEngine) {
			System.out.println("This is a FireEngine instance.");
		}
		if (fe instanceof Car) {
			System.out.println("This is a Car instance.");
		}
		if (fe instanceof Object) {
			System.out.println("This is a Object instance.");
		}
		System.out.println(fe.getClass().getName());

	}
}
This is a FireEngine instance.
This is a Car instance.
This is a Object instance.
FireEngine
어떤 타입에 대한 instanceof연산의 결과가 true라는 것은 검사한 타입으로 형변환이 가능하다는 것을 뜻한다.

 

✔ 참조변수와 인스턴스의 연결

  • 멤버변수의 경우 참조변수의 타입에 따라 달라진다.
  • 멤버변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우, 조상타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버변수가 사용되고, 자손타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버변수가 사용된다.
  • 참조변수의 타입에 따라 결과가 달라지는 경우는 조상 클래스의 멤버변수와 같은 이름의 멤버변수를 자손 클래스에 중복해서 정의한 경우뿐이다.

 

public class BindingTest3 {
	public static void main(String[] args){
  
  BindingTest3 test = new BindingTest3();
  
  Parent p = test.new Child();
  Child c  = test.new Child();
  
  System.out.println("p.x="+p.x);
  p.method();
  System.out.println("-----------------------------");
  System.out.println("c.x="+c.x);
  c.method();
 }
 
 class Parent{
  
  int x =100;
  
  void method(){
	  System.out.println("Parent Method");
  }
 }
 
 class Child extends Parent{
  
  int x=200;
  
  void method(){
	  System.out.println("x="+x);    //this.x 와 같다.
	  System.out.println("super.x="+super.x);
	  System.out.println("this.x="+this.x);
  }
 }
}
p.x=100
x=200
super.x=100
this.x=200

c.x=200
x=200
super.x=100
this.x=200
  • super.x는 조상 클래스인 Parent에 선언된 인스턴스변수 x를 뜻하며, this.x 또는 xChild클래스의 인스턴스변수 x를 뜻한다.
  • 인스턴스변수에 직접 접근하면, 참조변수의 타입에 따라 사용되는 인스턴스변수가 달라질 수 있으므로 주의해야 한다!

 

✔ 매개변수의 다향성

class Product{
	int price;
	int bonusPoint;
	
	Product(int price) {
		this.price = price;
		bonusPoint = (int)(price/10.0);
	}
}
class Tv extends Product {
	Tv() {
		super(100);
	}
	public String toString() { return "Tv"; }
}
class Computer extends Product {
	Computer() { super(200); }
	
	public String toString() { return "Computer"; }
}

class Buyer {
	int money = 1000;
	int bonusPoint = 0;
	
	void buy(Product p) {
		if(money < p.price) {
			System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
			return;
		}
		
		money -= p.price;
		bonusPoint += p.bonusPoint;
		System.out.println(p + "을/를 구입하셨습니다.");
	}
}
class PolyArgumentTest {
	public static void main(String[] args) {
		Buyer b = new Buyer();
		
		b.buy(new Tv());
		b.buy(new Computer());
		
		System.out.println("현재 남은 돈은 " + b.money + "만원입니다.");
		System.out.println("현재 보너스 점수는" + b.bonusPoint + "점입니다.");
		
	}
}
Tv을/를 구입하셨습니다.
Computer을/를 구입하셨습니다.
현재 남은 돈은 700만원입니다.
현재 보너스 점수는30점입니다.
	void buy(Product p) {
		money = money - p.price;
		bonusPoint = bonusPoint + p.bonusPoint;
	}
  • 메서드의 매개변수에 다향성을 적용하면 하나의 메서드로 간단히 처리할 수 있다.
  • 매개변수가 Product타입의 참조변수라는 것은, 메서드의 매개변수로 Product클래스의 자손타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다는 뜻이다.

 

Object 클래스
ex) print(Object obj)

public void print(Object obj){
	write(String,valueOf(obj)); // valueOf()가 반환한 문자열을 출력한다.
}

public static String valueOf(Object obj){
	return (obj == null) ? "null" : obj.toString(); // 문자열을 반환한다.
}
  • Object는 모든 클래스의 조상이므로 이 메서드의 매개변수로 어떤 타입의 인스턴스도 가능하므로, 이 하나의 메서드로 모든 타입의 인스턴스를 처리할 수 있는 것이다.

 

✔ 여러 종류의 객체를 배열로 다루기
Product타입의 참조변수 배열로 처리

Product p[] = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio();
  • 조상타입의 참조변수 배열을 사용하면, 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다.
  • 묶어서 다루고싶은 객체들의 상속관계를 따져서 가장 가까운 공통조상 클래스 타입의 참조변수 배열을 생성해서 객체들을 저장하면 된다.

 

class Buyer {
	int money = 1000;
	int bonusPoint = 0;
	Product[] item = new Product[10]; // 구입한 제품을 저장하기 위한 배열
	int i = 0;  // Product배열 item에 사용될 index
	
	void buy(Product p) {
		if(money < p.price) {
			System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
			return;
		}
		money -= p.price;	// 가진 돈에서 제품가격을 뺀다.
		bonusPoint += p.bonusPoint;	// 제품의 보너스포인트를 더한다.
		item[i++] = p;	// 제품을 Product[] item에 저장한다.
		System.out.println(p + "을/를 구입하셨습니다.");
	}
}
  • 모든 제품클래스의 조상인 Product클래스 타입의 배열을 사용함으로써 구입한 제품을 하나의 배열로 간단하게 다룰 수 있게 된다.

 

class Buyer {
	int money = 1000;
	int bonusPoint = 0;
	Vector<Product> item = new Vector<Product>();
	
	void buy(Product p) {
		if(money < p.price) {
			System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
			return;
		}
		money -= p.price;
		bonusPoint += p.bonusPoint;
		item.add(p);
		System.out.println(p + "을/를 구입하셨습니다.");
	}
}
  • 배열의 크기를 무조건 크게 설정할 수만도 없는 일이다!
  • Vector클래스를 사용하면 된다.
  • Vector클래스는 내부적으로 Object타입의 배열을 가지고 있어서, 이 배열에 객체를 추가하거나 제거할 수 있게 작성되어 있다.
  • 그리고 배열의 크기를 알아서 관리해주기 때문에 저장할 인스턴스의 개수에 신경 쓰지 않아도 된다.

 

Vector 클래스

동적으로 크기가 관리되는 객체배열

메서드/생성자설명
Vector()10개의 객체를 저장할 수 있는 Vector인스턴스를 생성한다. 10개 이상의 인스턴스가 저장되면, 자동적으로 크기가 증가된다.
boolean add(Object o)Vector에 객체를 추가한다. 추가에 성공하면 결과값으로 true, 실패하면 false를 반환한다.
boolean remove(Object o)Vector에 저장되어있는 객체를 제거한다. 제거에 성공하면 true, 실패하면 false를 반환한다.
boolean isEmpty()Vector가 비어있는지 검사한다. 비어있으면 true, 비어있지 않으면 false를 반환한다.
Object get(int index)지정된 위치(index)의 객체를 반환한다. 반환타입이 Object타입이므로 적잘한 타입으로의 형변환이 필요하다.
int size()Vector에 저장된 객체의 개수를 반환한다.

 

📚 6. 추상클래스(abstract class)

추상클래스로 인스턴스는 생성할 수 없다. 추상 클래스는 상속을 통해서 자손 클래스에 의해서만 완성될 수 있다.

abstract class 클래스이름{
	...
}
  • 추상클래스에도 생성자가 있으며, 멤버변수와 메서드도 가질 수 있다.
  • 추상 메서드를 포함하고 있지 않은 클래스에도 키워드 abstract를 붙여서 추상 클래스로 지정할 수도 있다.
  • 추상클래스로 지정되면 클래스의 인스턴스를 생성할 수 없다.

 

✔ 추상 메서드(abstract method)

  • 선언부만 작성하고 구현부는 작성하지 않은 채로 남겨 둔 것이 추상 메서드이다.
  • 추상메서드 역시 키워드 abstract를 앞에 붙여 주고, 추상메서드는 구현부가 없으므로 괄호 {} 대신 문장의 끝을 알리는 ;을 적어준다.
/* 주석을 통해 어떤 기능을 수행할 목적으로 작성하였는지 설명한다 */
abstract 리턴타입 매서드이름();
  • 추상 클래스로부터 상속받는 자손 클래스는 오버라이딩을 통해 조상인 추상 클래스의 추상 메서드를 모두 구현해주어야 한다.
abstract class Player{	// 추상클래스
	abstract void play(int pos);	// 추상메서드
	abstract void stop();	// 추상메서드
	
}

class AudioPlayer extends Player{
	void play(int pos){ /* 내용 생략 */} // 추상메서드를 구현
	void stop(){ /* 내용 생략 */}	// 추상메서드를 구현
}

abstract class AbstractPlayer extends Player{
	void play(int pos){ /* 내용 생략 */}	// 추상메서드를 구현
}
  • 메서드를 작성할 때 실제 작업내용인 구현부보다 더 중요한 부분이 선언부이다.

 

✔ 추상클래스의 작성

추상 : 낱낱의 구체적 표상이나 개념에서 공통된 성질을 뽑아 이를 일반적인 개념으로 파악하는 정신 작용
추상화 : 기존의 클래스의 공통부분을 뽑아내서 조상 클래스를 만드는 것이다.

추상화 : 클래스간의 공통점을 찾아내서 공통의 조상을 만드는 작업
구체화 : 상속을 통해 클래스를 구현, 확장하는 작업

 

class Player{
	...
	void play(int pos){}
	void stop(){}
	...
}
  • 만일 추상 메서드로 정의되어 있지 않고 위와 같이 빈 몸통만 가지도록 정의되어 있다면, 상속받는 자손 클래스에서는 이 메서드들이 온전히 구현된 것으로 인식하고 오버라이딩을 통해 자신의 클래스에 맞도록 구현하지 않을 수도 있기 때문이다.
  • 하지만 abstract를 사용해서 추상 메서드로 정의해놓으면, 자손 클래스를 작성할 때 이들 이 추상 메서드이므로 내용을 구현해주어야 한다는 사실을 인식하고 자신의 클래스에 알맞게 구현할 것이다.

 

abstract class Unit {
      int x, y;
      abstract void move(int x, int y);
      void stop() { /* 현재 위치에 정지 */ }
}

class Marine extends Unit {  // 보병
     void move(int x, int y) { /* 지정된 위치로 이동 */     }
     void stimPack()         { /* 스팀팩을 사용한다. */     }
}

class Tank extends Unit {    // 탱크
     void move(int x, int y) { /* 지정된 위치로 이동 */     }
     void changeMode()       { /* 공격모드를 변환한다. */   }
}

class Dropship extends Unit { // 수송선
     void move(int x, int y) { /* 지정된 위치로 이동 */     }
     void load()             { /* 선택된 대상을 태운다. */  }
     void unload()           { /* 선택된 대상을 내린다. */  }
}
main에서
Unit[] group = new Unit[4];
group[0] = new Marine();
group[1] = new Tank();
group[2] = new Marine();
group[3] = new Dropship();

for(int i = 0; i < group.length; i++) 
     group[i].move(100, 200);          // Unit 배열의 모든 유닛을 좌표 (100, 200)의 위치로 이동
  • group[i].move(100, 200)과 같이 호출하는 것이 Unit 클래스의 추상 메서드인 move를 호출하는 것 같이 보이지만 실제로는 이 추상 메서드가 구현된 Marine, Tank, Dropship 인스턴스의 메서드가 호출되는 것이다.

 

만약 추상화 Unit이 아닌 Object클래스를 사용시

Object[] group = new Object[4];
group[0] = new Marine();
group[1] = new Tank();
group[2] = new Marine();
group[3] = new Dropship();

for(int i = 0; i < group.length; i++) 
     group[i].move(100, 200);         // 에러발생, Object 클래스에 move 메서드가 정의되어 있지 않다.
  • 모든 클래스의 조상인 Object 클래스 타입의 배열로도 서로 다른 종류의 인스턴스를 하나의 묶음으로 다룰 수 있지만, Object 클래스에는 move 메서드가 정의되어 있지 않기 때문에 move 메서드를 호출하는 부분에서 에러가 발생한다.

 

📚 7. 인터페이스(interface)

  • 추상 메서드와 상수만을 멤버로 가질 수 있으며, 그 외의 다른 어떠한 요소도 허용하지 않는다.
  • 다른 클래스를 작성하는데 도움 줄 목적으로 작성된다.

 

✔ 인터페이스의 작성

  • interface에도 클래스와 같이 접근제어자로 public 또는 default를 사용할 수 있다.
interface 인터페이스이름 {
	public static final 타입 상수이름 =;
	public abstract 메서드 이름(매개 변수 목록);
}

 

🔔 인터페이스의 멤버들 제약사항

  • 모든 멤버 변수는 public static final 이어야 하며, 이를 생략할 수 있다.
  • 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다. 단, static 메서드와 디폴트 메서드는 예외 (JDK1.8부터)
  • 원래는 인터페이스의 모든 메서드는 추상 메서드이어야 하는데, JDK 1.8부터 인터페이스에 static 메서드와 디폴트 메서드 (default method)의 추가를 허용하는 방향으로 변경되었다.

 

✔ 인터페이스의 상속

  • 인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와는 달리 다중상속, 즉 여러 개의 인터페이스로부터 상속을 받는 것이 가능하다.
  • 인터페이스는 클래스와 달리 Object 클래스와 같은 최고 조상이 없다.

 

✔ 인터페이스의 구현

  • 인터페이스도 자신에 정의된 추상메서드의 몸통을 만들어주는 클래스를 작성해야 하는데, 그 방법은 추상 클래스가 자신을 상속받는 클래스를 정의하는 것과 거의 같다.
  • 다른 점 : 추상클래스는 extends, 인터페이스(implements)
class 클래스 이름 implements 인터페이스 이름 {
     // 인터페이스에 정의된 추상 메서드를 구현해야 한다.
}

class Fighter implements Fightable {
	public void move(int x, int y) { /* 내용 생략 */ }
	public void attack(Unit u)     { /* 내용 생략 */ }
}
  • 이 때 "Fighter 클래스는 Fightable 인터페이스를 구현한다." 라고 한다.

만일 구현하는 인터페이스의 메서드 중 일부만 구현한다면, abstract를 붙여서 추상 클래스로 선언해야 한다.

abstract class Fighter implements Fightable {
	public void move(int x, int y) { /* 내용 생략 */ }
}

그리고 상속과 구현을 동시에 할 수도 있다.

class Fighter extends Unit implements Fightable {
	public void move(int x, int y) { /* 내용 생략 */ }
	public void attack(Unit u)     { /* 내용 생략 */ }

}

 

💡 참고

  • 인터페이스의 이름에는 주로 Fightable과 같이 '~을 할 수 있는'의 의미인 'able'로 끝나는 것들이 많은데, 그 이유는 어떠한 기능 또는 행위를 하는데 필요한 메서드를 제공한다는 의미를 강조하기 위해서이다.
  • 또한 그 인터페이스를 구현한 클래스는 '~를 할 수 있는' 능력을 갖추었다는 의미이기도 하다.

 

인터페이스는 상속 대신 구현이라는 용어를 사용한다.

interface Movable {
	void move(int x, int y);
}

class Fighter extends Unit implements Fightable {
	public void move(int x, int y) { /* 실제구현내용 생략 */ }
	public void attack(Unit u)     { /* 실제구현내용 생략 */ }
}
  • Movable 인터페이스에 void move(int x, int y)와 같이 정의되어 있지만 사실 public abstract가 생략된 것이기 때문에 실제로 public abstract void move(int x, int y)이다.
  • 그래서, 이를 구현하는 Fighter 클래스에서는 void move(int x, int y)접근 제어자를 반드시 public으로 해야 하는 것이다.

 

✔ 인터페이스를 이용한 다중상속

  • '자바도 인터페이스를 이용하면 다중 상속이 가능하다.' 라고 하는 것일 뿐 자바에서 인터페이스로 다중 상속을 구현하는 경우는 거의 없다.

 

✔ 인터페이스를 이용한 다향성

인터페이스 Fightable을 클래스 Fighter 가 구현했을 때, 다음과 같이 Fighter 인스턴스를 Fightable 타입의 참조 변수로 참조하는 것이 가능하다.

Fightable f = (Fightable) new Fighter();

또는

Fightable f = new Fighter();

따라서 인터페이스는 다음과 같이 메서드의 매개 변수의 타입으로 사용될 수 있다.

void attack(Fightable f) {
     // ...
}
  • 인터페이스 타입의 매개변수가 갖는 의미 : 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야한다는 것
  • 그래서 attack 메서드를 호출할 때는 매개 변수로 Fightable 인터페이스를 구현한 클래스의 인스턴스를 넘겨주어야 한다.

 

다음과 같이 메서드의 리턴타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다.

Fightable method() {
      ...
	Fighter f = new Fighter();
	return f;                   
	// 이 두 문장을 한 문장으로 바꾸면 다음과 같다. return new Fighter(); 
}
  • 리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다. (매우 중요)
  • 위의 코드에서는 method()의 리턴 타입이 Fightable 인터페이스이기 때문에 메서드의 return 문에서 Fightable 인터페이스를 구현한 Fighter 클래스의 인스턴스를 반환한다.

 

✔ 인터페이스의 장점

(1) 개발시간을 단축시킬 수 있다.

  • 일단 인터페이스가 작성되면, 이를 사용해서 프로그램을 작성하는 것이 가능하다.
  • 메서드를 호출하는 쪽에서는 메서드의 내용에 관계없이 선언부만 알면 되기 때문이다.
  • 그리고 동시에 다른 한 쪽에서는 인터페이스를 구현하는 클래스를 작성하게 하면, 인터페이스를 구현하는 클래스가 작성될 때까지 기다리지 않고도 양쪽에서 동시에 개발을 진행할 수 있다.

(2) 표준화가 가능하다.

  • 프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음, 개발자들에게 인터페이스를 구현하여 프로그램을 작성하도록 함으로써 보다 일관되고 정형화된 프로그램의 개발이 가능하다.

(3) 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.

  • 서로 상속관계에 있지도 않고, 같은 조상클래스를 가지고 있지 않은 서로 아무런 관계도 없는 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어 줄 수 있다.

(4) 독립적인 프로그래밍이 가능하다.

  • 인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제구현에 독립적인 프로그램을 작성하는 것이 가능하다.
  • 클래스와 클래스간의 직접적인 관계를 인터페이스를 이용해서 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능하다.

 

게임에 나오는 유닛을 클래스로 표현 및 관계를 상속계층도로 표현 예시

스크린샷 2022-02-20 오전 10 49 23

SCV에게 Tank와 Dropship과 같은 기계화 유닛을 수리할 수 있는 기능을 제공하기 위해 repair 메서드를 정의한다면 다음과 같을 것이다.

void repair(Tank t) {
     // Tank를 수리한다.
}

void repair(Dropship d) {
     // Dropship을 수리한다.
}

이때, 인터페이스를 이용하면 기존의 상속체계를 유지하면서 이들 기계화 유닛에 공통점을 부여할 수 있다.

interface Repairable {}

class SCV extends GroupUnit implements Repairable {
     // ...
}

class Tank extends GroundUnit implements Repairable {
     // ...
}

class Dropship extends AirUnit implements Repairable {
     // ...
}
  • 이제 이 3개의 클래스에는 같은 인터페이스를 구현했다는 공통점이 생긴다.
  • 인터페이스 Repairable에 정의된 것은 아무것도 없고, 단지 인스턴스의 타입체크에만 사용될 뿐이다.
스크린샷 2022-02-20 오전 10 58 31

그리고 repair 메서드의 매개 변수의 타입을 Repariable로 선언하면, 이 메서드의 매개 변수로 Repairable 인터페이스를 구현한 클래스의 인스턴스만 받아들여질 것이다.

void repair(Repairable r) {
     // 매개 변수로 넘겨받은 유닛을 수리한다.
}

➡️ 앞으로 새로운 클래스가 추가될 때, SCVrepair 메서드에 의해서 수리가 가능하도록 하려면 Repairable 인터페이스를 구현하도록 하면 될 것이다.

class RepairableTest {

   public static void main(String[] args) {
       Tank tank = new Tank();

       Dropship dropship = new Dropship();

       Marine marine = new Marine();
   
       SCV scv = new SCV();

       scv.repair(tank);       // SCV가 Tank를 수리하도록 한다.
       scv.repair(dropship);
//     scv.repair(marine);     // 에러! repair (Repairable) in SCV cannot be applied to (Marine)
   }

}

class Unit {

   int hitPoint;
   final int MAX_HP;
   Unit(int hp) {
      Max_HP = hp;
   }

   // ...

}

class GroundUnit extends Unit {

   GroundUnit(int hp) {
       super(hp);
   }

}

class AirUnit extends Unit {

   AirUnit(int hp) {
       super(hp);
   }

}

class Tank extends GroundUnit implements Repairable {

   Tank() {
       super(150);     // Tank의 HP는 150이다.
       hitPoint = MAX_HP;
   }

   public String toString() {
    
       return "Tank";
   }

   // ...

}

class Dropship extends AirUnit implements Repairable {

   Dropship() {
       super(125);     // Dropship의 HP는 125이다.
       hitPoint = MAX_HP;
   }

   public String toString() {
 
       return "Dropship";
   }

   // ...

}

class Marine extends GroundUnit {

   Marine() {
       super(40);
       hitPoint = MAX_HP;
   }
  
   // ...

}

class SCV extends GroundUnit implements Repairable {

   SCV() {
       super(60);
       hitPoint = MAX_HP;
   }

   void repair(Repairable r) {
       if ( r instanceof Unit) {
            Unit u = (Unit) r;
            while(u.hitPoint!=u.MAX_HP) {
                /* Unit의 HP를 증가시킨다. */
                u.hitPoint++;
            }

            System.out.println( u.toString() + "의 수리가 끝났습니다.");
       }
   }

   // ...

}
Tank의 수리가 끝났습니다.
Dropship의 수리가 끝났습니다.
  • repair 메서드의 매개 변수 r은 Repairable 타입이기 때문에 인터페이스 Repairable에 정의된 멤버만 사용할 수 있다.
  • 그러나 Repairable에는 정의된 멤버가 없으므로 이 타입의 참조 변수로는 할 수 있는 일은 아무 것도 없다.
  • 그래서 instanceof 연산자로 타입을 체크한 뒤 캐스팅하여 Unit 클래스에 정의된 hitPointMAX_HP를 사용할 수 있도록 하였다.
  • 그 다음엔 유닛의 현재 체력 (hitPoint)이 유닛이 가질 수 있고 최고 체력 (MAX_HP)이 될 때 까지 체력을 증가시키는 작업을 수행한다.
  • Marine은 Repairable 인터페이스를 구현하지 않았으므로 SCV 클래스의 repair 메서드의 매개 변수로 Marine을 사용하면 컴파일 시에 에러가 발생한다.

 

또 다른 예시 건물들을 표현하는 클래스
스크린샷 2022-02-20 오전 11 15 40

이때


void liftOff()          { /* 내용 생략 */ }

void move(int x, int y) { /* 내용 생략 */ }

void stop()             { /* 내용 생략 */ }

void land()             { /* 내용 생략 */ }

를 추가하려고 한다.

이럴 경우, 인터페이스를 통해 해결할 수 있다.

  • 새로 추가하고자하는 메서드를 정의하는 인터페이스를 정의하고 이를 구현하는 클래스를 작성한다.
interface Liftable {
     /** 건물을 들어 올린다. */
     void liftOff();
     /** 건물을 들어 올린다. */
     void liftOff();
     /** 건물을 이동한다. */
     void move(int x, int y);
     /** 건물을 정지시킨다. */
     void stop();
     /** 건물을 착륙시킨다. */
     void land();
}

class LiftableImpl implements Liftable {
     public void liftOff()          { /* 내용 생략 */ }
     public void move(int x, int y) { /* 내용 생략 */ }
     public void stop()             { /* 내용 생략 */ }
     public void land()             { /* 내용 생략 */ }
}

마지막으로 새로 작성된 인터페이스와 이를 구현한 클래스를 BarrackFactory클래스에 적용하면 된다.

스크린샷 2022-02-20 오전 11 16 13
  • Barrack 클래스가 Liftable 인터페이스를 구현하도록 하고, 인터페이스를 구현한 LiftableImpl 클래스를 Barrack 클래스에 포함시켜서 내부적으로 호출해서 사용하도록 한다.
  • 이렇게 함으로써 같은 내용의 코드를 Barrack 클래스와 Factory 클래스에서 각각 작성하지 않고 LiftableImpl 클래스 한 곳에서 관리할 수 있다.
  • 그리고 작성된 Liftable 인터페이스와 이를 구현한 LiftableImpl 클래스는 후에 다시 재사용될 수 있을 것이다!
class Barrack extends Building implements Liftable {
     LiftableImpl l = new LiftableImpl();
     void liftOff() { l.liftOff(); }
     void move(int x, int y) { l.move(x, y); }
     void stop() { l.stop(); }
     void land() { l.land(); }
     void trainMarine() { /* 내용 생략 */ }
     ...

}

class Factory extends Building implements Liftable {
     LiftableImpl l = new LiftableImpl();
     void liftOftt() { l.liftOff(); }
     void move(int x, int y) { l.move(x, y); }
     void stop() { l.stop(); }
     void land() { l.land(); }
     void makeTank() { /* 내용 생략 */ }
     ...

}

 

✔ 인터페이스의 이해

  • 클래스를 사용하는 쪽 (User)과 클래스를 제공하는 쪽 (Provider)이 있다.
  • 메서드를 사용(호출)하는 쪽 (User)에서는 사용하려는 메서드 (Provider)의 선언부만 알면 된다. (내용은 몰라도 된다.)
class A {
      public void methodA(B b) {
            b.methodB();
      }
}

class B {
      public void methodB() {
		  System.out.println("methodB()");
      }
}

class InterfaceTest {
      public static void main(String[] args) {
            A a = new A();
            a.methodA(new B());
      }
}

A(User) ➡ B(Provider)

현재는?

  • 클래스 A(User)는 클래스 B(Provider)의 인스턴스를 생성하고 메서드를 호출한다.
  • 클래스 A를 작성하려면 클래스 B가 이미 작성되어 있어야 한다.
  • 클래스 B의 methodB()의 선언부가 변경되면, 이를 사용하는 클래스 A도 변경되어야 한다.

이와 같이 직접적인 관계의 두 클래스는 한 쪽 (Provider)이 변경되면 다른 한 쪽 (User)도 변경되어야 한다는 단점이 있다.

🔔 해결책

  • 클래스 A가 클래스 B를 호출하지 않고 인터페이스를 매개체로 해서 클래스 A가 인터페이스를 통해서 클래스 B의 메서드에 접근하도록 하면, 클래스 B에 변경사항이 생기거나 클래스 B와 같은 기능의 다른 클래스로 대체 되어도 클래스 A는 전혀 영향을 받지 않도록 하는 것이 가능하다.
interface I {

     public abstract void methodB();

}

// methodA가 호출될 떄 인터페이스 I를 구현한 클래스의 인스턴스 (클래스 B의 인스턴스)를 제공받아야 한다.
class A {
    public void methodA(I i) {
         i.methodB();
    }
}

class B implements I {
     public void methodB() {
           System.out.println("methodB in B class");
     }
}

class InterfaceTest {
      public static void main(String[] args) {
            A a = new A();
            a.methodA(new B());
      }
}

주목할 점 : 클래스 A를 작성하는데 있어서 클래스 B가 사용되지 않았다.
A-B 직접적인 관계 ↔ A-I-B 간접적인 관계

  • 클래스 A는 인터페이스를 통해 실제로 사용하는 클래스의 이름을 몰라도 되고 심지어는 실제로 구현된 클래스가 존재하지 않아도 문제되지 않는다.
  • 클래스 A는 오직 직접적인 관계에 있는 인터페이스 I의 영향만 받는다.
  • 비유
    • 인터페이스 I : 실제구현 내용 (클래스 B)을 감싸고 있는 껍데기
    • 클래스 A : 껍데기안에 어떤 알맹이 (클래스)가 들어 있는지 몰라도 된다.

 

class A {
   void autoPlay(I i) {
         i.play();
   }
}

interface I {
     public abstract void play();
}

class B implements I {
   public void play() {
        System.out.println("play in B class");
   }
}

class C implements I {
   public void play() {
        System.out.println("play in B class");
   }
}

class InterfaceTest2 {
   public static void main(String[] args) {
       A a = new A();
       a.autoPlay(new B());  // void autoPlay(I i) 호출
       a.autoPlay(new C());  // void autoPlay(I i) 호출
   }
}
play in B class
play in C class
  • 클래스 A를 작성하는데 클래스 B가 관련되지 않았다.
  • 클래스 A와 같이 매개 변수를 통해서 인터페이스 I를 구현한 클래스의 인스턴스를 동적으로 제공받아야 한다.

 

제3의 클래스를 통해서 제공받기

class InterfaceTest3 {
   public static void main(String[] args) {
       A a = new A();
       a.methodA();
   }
}

class A {
   void methodA() {
       I i = InstanceManager.getInstance();  /* 제 3의 클래스의 메서드를 통해서 인터페이스 I를 구현한 클래스의 인스턴스를 얻어온다. */
       i.methodB();
       System.out.println(i.toString());  // i로 Object 클래스의 메서드 호출 가능 
   }

}

interface I {
     public abstract void methodB();
}

class B implements I {
   public void methodB() {
       System.out.println("methodB in B class");
   }
   public String toString() {  return "class B";  }
}

class InstanceManager() {
   public static I getInstance() {
       return new B();
   }
}
methodB in B class
class B
  • 인스턴스를 직접 생성하지 않고, getInstance()라는 메서드를 통해 제공받는다.
  • 이렇게 하면, 나중에 다른 클래스의 인스턴스로 변경되어도 A 클래스의 변경 없이 getInstance() 만 변경하면 된다는 장점이 생긴다.
class A {
     void methodA() {
          I i = InstanceManager.getInstance();
          i.methodB();
          System.out.println(i.toString());  // i로 Object의 메서드 호출가능
     }
}
  • 인터페이스 I 타입의 참조 변수 i로도 Object 클래스에 정의된 메서드들을 호출할 수 있다.
  • i에 toString()이 정의되어 있지 않지만, 모든 객체는 Object 클래스에 정의된 메서드를 가지고 있을 것이기 때문에 허용하는 것이다.

 

✔ 디폴트 메서드와 static 메서드

  • 디폴트 메서드 : 디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다.
  • 디폴트 메서드는 앞에 키워드 default를 붙이며, 추상 메서드와 달리 일반 메서드 처럼 몸통 { } 이 있어야 한다.
  • 디폴트 메서드 역시 접근 제어자가 public 이며, 생략가능하다.
interface MyInterface {
	void method();
	void newMethod();  // 추상 메서드
	default void newMethod2() {}	// default 메서드

}
  • 디폴트 메서드를 추가했다고 기존의 MyInterface를 구현한 클래스를 변경하지 않아도 된다.
  • 조상 클래스에 새로운 메서드를 추가한 것과 동일해 지는 것이다.
  • 대신, 새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우가 발생한다.

 

🔔 충돌을 해결하는 규칙
(1) 여러 인터페이스의 디폴트 메서드 간의 충돌

  • 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.

(2) 디폴트 메서드와 조상 클래스의 메서드 간의 충돌

  • 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.

만약 충돌 해결 규칙 외우기 귀찮을 시 : 필요한 쪽의 메서드와 같은 내용으로 오버라이딩 해버리면 그만이다.

class DefaultMethodTest {

   public static void main(String[] args) {
       Child c = new Child();
       c.method1();
       c.method2();
       MyInterface.staticMethod();
       MyInterface2.staticMethod();
   }

}

class Child extends Parent implements MyInterface, MyInterface2 {
   public void method1() {
       System.out.println("method1()  in Child");  // 오버라이딩 
   }
}

class Parent {
   public void method2() {
       System.out.println("method2()  in Parent"); 
   }
}

interface MyInterface {
   default void method1() {
       System.out.println("method1()  in MyInterface");
   }

   default void method2() {
       System.out.println("method2()  in MyInterface");
   }

   static  void staticMethod() {
       System.out.println("staticMethod()  in MyInterface");
   }

}

interface MyInterface2 {
   default void method1() {
      System.out.println("method1()  in MyInterface2");
   }
   static  void staticMethod() {
       System.out.println("staticMethod()  in MyInterface2");
   }
}
method1() in Child
method2() in Parent
staticMethod() in MyInterface
staticMethod() in MyInterface2

 

📚 8. 내부 클래스(inner class)

  • 내부 클래스는 사용빈도가 높지 않으므로 내부 클래스의 기본 원리와 특징을 이해하는 정도까지만 학습해도 충분하다.
  • 내부 클래스는 클래스 내에 선언된 클래스이다.
내부 클래스의 장점
- 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
- 코드의 복잡성을 줄일 수 있다(캡슐화).
class A {
   ...
}

class B {
   ...
}
class A {     // 외부 클래스
   ...
   class B {  // 내부 클래스
   ...
   }
   ...
}
  • 위1의 A와 B 두 개의 독립적인 클래스를 위2 그림과 같이 바꾸면 B는 A의 내부 클래스 (inner class)가 되고 A는 B를 감싸고 있는 외부 클래스 (outer class)가 된다.
  • 이 때 내부 클래스인 B는 외부 클래스인 A를 제외하고는 다른 클래스에서 잘 사용되지 않는 것이어야 한다.

 

✔️ 내부 클래스의 종류와 특징

내부 클래스특징
인스턴스 클래스(instance class)외부 클래스의 멤버 변수 선언위치에 선언하며, 외부 클래스의 인스턴스 멤버처럼 다루어진다. 주로 외부 클래스의 인스턴스 멤버들과 관련된 작업에 사용될 목적으로 선언된다.
스태틱 클래스(static class)외부 클래스의 멤버 변수 선언위치에 선언하며, 외부 클래스의 static 멤버처럼 다루어진다. 주로 외부 클래스의 static 멤버, 특히 static 메서드에서 사용될 목적으로 선언된다.
지역 클래스(local class)외부 클래스의 메서드나 초기화 블럭 안에 선언하며, 선언된 영역 내부에서만 사용될 수 있다.
익명 클래스(anonymous class)클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스 (일회용)

 

✔️ 내부 클래스의 선언

  • 내부 클래스의 선언 위치가 변수의 선언 위치와 동일함을 알 수 있다.
  • 변수가 선언된 위치에 따라 인스턴스 변수, 클래스 변수 (static 변수), 지역 변수로 나뉘듯이 내부 클래스도 이와 마찬가지로 선언된 위치에 따라 나뉜다.
  • 그리고, 각 내부 클래스의 선언 위치에 따라 같은 선언 위치의 변수와 동일한 유효 범위 (scope)와 접근성 (accessibility)을 갖는다.
class Outer {
   int iv = 0;
   static int cv = 0;
   void myMethod() {
      int lv = 0;
   }
}

↔️

class Outer {
   class InstanceInner {}
   static class StaticInner {}
   void myMethod() {
      class LocalInner {}
   }
}

 

✔️ 내부 클래스의 제어자와 접근성
멤버 변수와 같은 성질을 갖는다.

  • 내부 클래스가 외부 클래스의 멤버와 같이 간주되고, 인스턴스 멤버와 static 멤버 간의 규칙이 내부 클래스에도 똑같이 적용된다.
class Outer {
   private int iv = 0;
   protected static int cv = 0;
   void myMethod() {
     int lv = 0;
   }
}

↔️

class Outer {
   private class InstanceInner {}
   protected static class StaticInner {}
   void myMethod() {
      class LocalInner {}
   }
}

내부 클래스도 클래스이기 때문에 abstractfinal과 같은 제어자를 사용할 수 있을 뿐만 아니라, 멤버 변수들처럼 private, protected과 접근 제어자도 사용이 가능하다.

class InnerEx1 {
   class InstanceInnr {
      int iv = 100;
//    static int cv = 100;            // 에러! static 변수를 선언할 수 없다.
      final static int CONST = 100;   // final static은 상수이므로 허용
   }

   static class StaticInner {
       int iv = 200;
       static int cv = 200;     // static 클래스만 static 멤버를 정의할 수 있다.

   }

   void myMethod() {
       class LocalInner {
           int iv = 300;
//         static int cv = 300;                   // 에러! static 변수를 선언할 수 없다.
           final static int CONST = 300;          // final static은 상수이므로 허용
       }
   }

   public static void main(String[] args) {
       System.out.println(InstanceInner.CONST);
       System.out.println(StaticInner.cv);
   }

}
100
200
  • 내부 클래스 중에서 스태틱 클래스 (StaticInner)만 static 멤버를 가질 수 있다.
  • finalstatic이 동시에 붙은 변수는 상수 (constant) 이므로 모든 내부 클래스에서 정의가 가능하다.
class InnerEx2 {
   class InstanceInner {}
   static class StaticInner {}
	
   // 인스턴스 멤버 간에는 서로 직접 접근이 가능하다.
   InstanceInner iv = InstanceInner();
   
	// static 멤버 간에는 서로 직접 접근이 가능하다.
	static StaticInner cv = new StaticInner();

   static void staticMethod() {

      // static 멤버는 인스턴스 멤버에 직접 접근할 수 없다.
	//    InstanceInner obj1 = new InstanceInner();
      StaticInner obj2 = new StaticInner();
	   
      // 굳이 접근하려면 아래와 같이 객체를 생성해야 한다.
      // 인스턴스 클래스는 외부 클래스를 먼저 생성해야만 생성할 수 있다.
      InnerEx2 outer = new InnerEx2();
      InstanceInner obj1 = outer.new InstanceInner();
   }

   void instanceMethod() {

      // 인스턴스 메서드에서는 인스턴스 멤버오 static 멤버 모두 접근 가능하다.
      InstanceInner obj1 = new InstanceInner();
      StaticInner obj2 = new StaticInner();
      // 메서드 내에 지역적으로 선언된 내부 클래스는 외부에서 접근할 수 없다.
      LocalInner lv = new LocalInner();
   }

   void myMethod() {
       class LocalInner {}
       LocalInner lv = new LocalInner(); 
   }

}
  • 인스턴스 클래스는 외부 클래스의 인스턴스 멤버를 객체 생성 없이 바로 사용할 수 있지만, 스태틱 클래스는 외부 클래스의 인스턴스 멤버를 객체생성 없이 사용할 수 없다.
  • 마찬가지로 인스턴스 클래스는 스태틱 클래스의 멤버들을 객체 생성 없이 사용할 수 있는지만, 스택틱 클래스에서는 인스턴스 클래스의 멤버들을 객체 생성 없이 사용할 수 없다.

 

class InnerEx3 {
   private int outerIv = 0;
   static  int outerCv = 0;

   class InstanceInner {
      int iiv  = outerIv;   // 외부 클래스의 private 멤버도 접근가능하다.
      int iiv2 = outerCv;
	   
   }

   static class StaticInner {
// 스태틱 클래스는 외부 클래스의 인스턴스 멤버에 접근할 수 없다.
//     int siv = outerIv;
       static int scv = outerCv;
   }

   void myMethod() {
       int lv = 0;
       final int LV = 0;   // JDK1.8 부터 final 생략 가능

       class LocalInner {
          int liv  = outerIv;
          int liv2 = outerCv;

// 외부 클래스의 지역 변수는 final이 붙은 변수 (상수)만 접근 가능하다.
//        int liv3 = lv;                          // 에러!!! (JDK1.8부터 에러 아님)
          int liv4 = LV;                          // OK

       }
   }

}
  • 내부 클래스에서 외부 클래스의 변수들에 대한 접근성을 보여 주는 예제이다.
  • 인스턴스 클래스 (InstanceInner)는 외부 클래스 (InnerEx3)의 인스턴스 멤버이기 때문에 인스턴스 변수 outerIv와 static 변수 outerCv를 모두 사용할 수 있다.
  • 심지어는 outerIv의 접근 제어자가 private 일지라도 사용가능하다.
  • 스태틱 클래스 (StaticInner)는 외부 클래스 (InnerEx3)의 static 멤버이기 때문에 외부 클래스의 인스턴스 멤버인 outerIvInstanceInner를 사용할 수 없다.
  • 단지 static 멤버인 outerCv 만을 사용할 수 있다.
  • 지역 클래스 (LocalInner)는 외부 클래스의 인스턴스 멤버와 static 멤버를 모두 사용할 수 있으며, 지역 클래스가 포함된 메서드에 정의된 지역 변수도 사용할 수 있다.
  • 단, final이 붙은 지역변수만 접근가능한데 그 이유는 메서드가 수행을 마쳐서 지역변수가 소멸된 시점에도, 지역 클래스의 인스턴스가 소멸된 지역변수를 참조하려는 경우가 발생할 수 있기 때문이다.

 

외부 클래스명$내부 클래스명.class 형식

class Outer {
   void myMethod() {
      int lv = 0;
   }
    void myMethod2() {
       int lv = 0;
    }
}

↔️

class Outer {
   void myMethod() {
      class LocalInner {}
   }
   void myMethod2() {
      class LocalInner {} 
   }
}
Outer.class
Outer$1LocalInner.class
Outer$2LocalInner.class
  • 지역 내부 클래스는 다른 메서드에 같은 이름의 내부 클래스가 존재할 수 있기 때문에 내부 클래스명 앞에 숫자가 붙는다.

 

class Outer {
   int value = 10;  // Outer.this.value

   class Inner {
      int value = 20;  // this.value

      void method1() {
          int value = 30;
          System.out.println("           value : " + value);
          System.out.println("      this.value : " + this.value);
		  System.out.println("Outer.this.value : " + Outer.this.value);
      }
   }  // Inner 클래스의 끝
}  // Outer 클래스의 끝

class InnerEx5 {
   public static void main(String[] args) {
       Outer outer = new Outer();
       Outer.Inner inner = outer.new Inner();
       inner.method1();
   }
}  // InnerEx5 끝
           value : 30
      this.value : 20
Outer.this.value : 10
  • 위의 예제는 내부 클래스와 외부 클래스에 선언된 변수의 이름이 같을 때 변수 앞에 this 또는 외부 클래스명.this 를 붙여서 서로 구별할 수 있다는 것을 보여준다.

 

✔️ 익명 클래스 (anonymous class)

익명 클래스 : 클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한번만 사용될 수 있고 오직 하나의 객체만을 생성할 수 있는 일회용 클래스이다.

new 조상 클래스 이름() {
    // 멤버 선언
}

    또는

new 구현 인터페이스 이름() {
    // 멤버 선언
}
  • 이름이 없기 때문에 생성자도 가질 수 없으며, 조상 클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 하나의 클래스로 상속받는 동시에 인터페이스를 구현하거나 둘 이상의 인터페이스를 구현할 수 없다.
  • 오로지 단 하나의 클래스를 상속받거나 단 하나의 인터페이스만을 구현할 수 있다.

 

class InnerEx6 {
   Object iv = new Object() {  void method() {}  };         // 익명 클래스
   static Object cv = new Object() {  void method() {}  };  // 익명 클래스
	
   void myMethod() {
       Object lv = new Object() {  void method() {}  };     // 익명 클래스
   } 
}
InnerEx6.class
InnerEx6$1.class ← 익명 클래스
InnerEx6$2.class ← 익명 클래스
InnerEx6$3.class ← 익명 클래스
  • 익명 클래스는 이름이 없기 때문에 외부 클래스명$숫자.class 의 형식으로 클래스파일명이 결정된다.

 

먼저 두 개의 독립된 클래스를 작성한 다음에, 다시 익명클래스를 이용하여 변경하면 보다 쉽게 코드를 작성할 수 있을 것이다.

두 개의 독립된 클래스

import java.awt.*;

import java.awt.event.*;

class InnerEx7 {

   public static void main(String[] args) {
       Button b = new Button("Start");
       b.addActionListener(new EventHandler());
   }
}

class EventHandler implements ActionListener {
   public void actionPerformed(ActionEvent e) {
       System.out.println("ActionEvent occurred!!!");
   }
}

 

익명클래스를 이용하여 변경

import java.awt.*;
import java.awt.event.*;

class InnerEx8 {
   public static void main(String[] args) {
       Button b = new Button("Start");
       b.addActionListener(new ActionListener() {
               // 버튼이 클릭되면 호출되는 actionPerformed() 메서드를 오버라이딩 
               public void actionPerformed(ActionEvent e) {
				   System.out.println("ActionEvent occurred!!!");
               }
           }  // 익명 클래스의 끝
       );
   }  // main의 끝
}  // InnerEx8의 끝
profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글

관련 채용 정보