Language/Java

[Java] 상속

sssbin 2022. 2. 23. 18:19

 

클래스 상속과 객체

- 슈퍼 클래스 객체와 서브 클래스의 객체는 별개이다.

- 서브 클래스 객체는 슈퍼 클래스 멤버를 포함한다.

- 서브 클래스는 슈퍼 클래스의 private 멤버 외 모든 멤버를 접근할 수 있다.

 

상속 선언과 객체

1. 상속 선언 - extends 키워드 사용

class ColorPoint extends Point { ... }

2. 서브 클래스 객체 생성

Point p = new Point();
ColorPoint cp = new ColorPoint(); // 서브 클래스 객체 생성

 

특징

- 자바에서는 클래스의 다중 상속을 지원하지 않는다.

- 자바의 모든 클래스는 자바에서 제공하는 Object 클래스를 자동으로 상속받도록 컴파일된다.


상속과 생성자

서브 클래스와 슈퍼 클래스의 생성자 호출 및 실행

class A {
	public A() {
		System.out.println("생성자A");
	}
}

class B extends A {
	public B() {
		System.out.println("생성자B");
	}
}

class C extends B {
	public C() {
		System.out.println("생성자C");
	}
}

public class ConstructorEx {
	public static void main(String[] args) {
		C c;
		c = new C();
	}
}

실행결과

- 서브 클래스의 객체가 생성될 때, 서브 클래스의 생성자와 슈퍼 클래스의 생성자가 모두 실행된다.

- 이때 슈퍼 클래스의 생성자가 먼저 실행된다.

- 서브 클래스의 생성자가 먼저 호출되지만, 더 나중에 실행된다.

 

서브 클래스에서 슈퍼 클래스 생성자 선택

1. 슈퍼 클래스의 기본 생성자가 묵시적으로 선택

class A2 {
	public A2() {
		System.out.println("생성자A");
	}
	public A2(int x) {
		System.out.println("생성자A2");
	}
}

class B2 extends A2 {
	public B2() {
		System.out.println("생성자B");
	}
}

public class ConstructorEx2 {
	public static void main(String[] args) {
		B2 b;
		b = new B2(); // 생성자 호출 
	}
}

실행결과

- 서브 클래스의 기본 생성자에 대해 컴파일러는 자동으로 슈퍼클래스의 기본 생성자와 짝을 맺는다.

class A3 {
	public A3() {
		System.out.println("생성자A");
	}
	public A3(int x) {
		System.out.println("매개변수생성자A");
	}
}

class B3 extends A3 {
	public B3() {
		System.out.println("생성자B");
	}
	public B3(int x) {
		System.out.println("매개변수생성자B");
	}
}

public class ConstructorEx3 {
	public static void main(String[] args) {
		B3 b;
		b = new B3(5);
	}
}

실행결과

- 서브 클래스의 매개 변수가 있는 생성자도 슈퍼 클래스의 기본 생성자와 짝을 이룬다.

2. super()를 이용하여 명시적으로 슈퍼 클래스의 생성자 선택

class A4 {
	public A4() {
		System.out.println("생성자A");
	}
	public A4(int x) {
		System.out.println("매개변수생성자A" + x);
	}
}

class B4 extends A4 {
	public B4() {
		System.out.println("생성자B");
	}
	public B4(int x) {
		super(x); // 첫 줄에 와야 함 
		System.out.println("매개변수생성자B" + x);
	}
}

public class ConstructorEx4 {
	public static void main(String[] args) {
		B4 b;
		b = new B4(5);
	}
}

실행결과


업캐스팅과 instanceof 연산자

- 캐스팅: 타입 변환 -> 업캐스팅 / 다운캐스팅업

 

업캐스팅

Person p;
Student s = new Student();
p = s; // 업캐스팅

- 서브 클래스 객체가 슈퍼 클래스 타입으로 변환되는 것

- 슈퍼 클래스의 레퍼런스가 서브 클래스 객체를 가리키도록 치환되는 것

- 업캐스팅한 레퍼런스로는 슈퍼 클래스의 멤버만 접근할 수 있다.

 

다운캐스팅

Student s = (Student)p; // 다운캐스팅, (Student)의 타입 변환을 반드시 지정

- 업캐스팅된 서브 클래스 객체를 다시 원래대로 되돌리는 것

 

instanceof 연산자와 객체 구별

객체레퍼런스 instanceof 클래스타입
// instance 연산자 활용 예제

class Person { }
class Student extends Person { }
class Researcher extends Person { }
class Professor extends Researcher { }

public class InstanceOfEx {
	static void print(Person p) {
		if (p instanceof Person)
			System.out.print("Person ");
		if (p instanceof Student) 
			System.out.print("Student ");
		if (p instanceof Researcher) 
			System.out.print("Researcher ");
		if (p instanceof Professor)
			System.out.print("Professor ");
		
		System.out.println();
	}

	public static void main(String[] args) {
		System.out.print("new Student() -> ");
		print(new Student());
		
		System.out.print("new Researcher() -> ");
		print(new Researcher());
		
		System.out.print("new Professor() -> ");
		print(new Professor());
	}
}

실행결과

- 레퍼런스가 가리키는 객체가 어떤 클래스 타입인지 구분하기 위해 사용

- '레퍼런스'가 가리키는 객체가 해당 '클래스 타입'이면 true, 아니면 false


메소드 오버라이딩

class A {
	void f() {
		System.out.println("A의 f() 호출");
	}
}
class B extends A {
	void f() { // A의 f()를 오버라이딩
		System.out.println("B의 f() 호출");
	}
}

- 서브 클래스에서 슈퍼 클래스에 선언된 메소드를 중복 작성하여 슈퍼 클래스에 작성된 메소드를 무력화시킴.

- 그러면 슈퍼 클래스의 레퍼런스를 이용하든 서브 클래스의 레퍼런스를 이용하든 항상 서브 클래스에 오버라이딩한 메소드가 실행된다.

- 서브 클래스에 오버라이딩한 메소드는 반드시 슈퍼 클래스에 작성된 메소드의 이름, 리턴 타입, 매개 변수 리스트가 모두 같도록 작성

 

오버라이딩의 목적, 다형성 실현

Shape 클래스의 draw() 메소드를 오버라이딩한 사례

- 오버라이딩은 슈퍼 클래스에 선언된 메소드를, 동일한 이름으로 각 서브 클래스에서 필요한 내용으로 새로 구현하는데 있다.

- 오버라이딩은 상속을 통해 '하나의 인터페이스에 서로 다른 내용 구현'이라는 객체 지향의 다형성을 실현하는 도구이다.

class Shape { // 도형의 슈퍼 클래스
	public void draw() {
		System.out.println("Shape");
	}
}

class Line extends Shape {
	public void draw() { // 메소드 오버라이딩
		System.out.println("Line");
	}
}

class Rect extends Shape {
	public void draw() { // 메소드 오버라이딩
		System.out.println("Rect");
	}
}

class Circle3 extends Shape {
	public void draw() { // 메소드 오버라이딩
		System.out.println("Circle");
	}
}

public class MethodOverridingEx {
	static void paint(Shape p) { // Shape을 상속받은 모든 객체들이 매개변수로 넘어올 수 있음
		p.draw(); // p가 가리키는 객체에 오버라이딩한 draw() 호출. 동적 바인딩 
	}
	public static void main(String[] args) {
		Line line = new Line();
		paint(line); // Line의 draw() 실행. "Line" 출력
		
		// 다음 호출들은 모두 paint() 메소드 내에서 Shape에 대한 레퍼런스 p로 업캐스팅됨
		paint(new Shape()); // Shape의 draw() 실행. "Shape" 출력
		paint(new Line()); // 오버라이딩된 메소드 Line()의 draw() 실행, "Line" 출력
		paint(new Rect()); // 오버라이딩된 메소드 Rect()의 draw() 실행, "Rect" 출력
		paint(new Circle3()); // 오버라이딩된 메소드 Circle()의 draw() 실행, "Circle" 출력
	}

}

 

동적 바인딩

- 실행할 메소드를 컴파일 시에 결정하지 않고 실행 시에 결정하는 것

- 자바에서는 동적 바인딩을 통해 오버라이딩된 메소드가 항상 실행되도록 보장한다.

public class SuperObject {
	
	protected String name;
	
	public void paint() {
		draw();
	}
	
	public void draw() {
		System.out.println("Super Object");
	}

	public static void main(String[] args) {
		SuperObject a = new SuperObject();
		a.paint();
	}
}

실행결과

public class SuperObject {
	protected String name;
	
	public void paint() {
		draw();
	}
	
	public void draw() {
		System.out.println("Super Object");
	}
}

public class SubObject extends SuperObject {
	public void draw() {
		System.out.println("Sub Object");
	}
	
	public static void main(String [] args) {
		SuperObject b = new SubObject();
		b.paint();
	}
}

실행결과
오버라이딩된 메소드를 호출하는 동적바인딩

 

오버라이딩과 super 키워드

- 서브 클래스에서 super 키워드를 이용하면 정적 바인딩을 통해 슈퍼 클래스의 멤버에 접근할 수 있다.

super.슈퍼클래스의멤버

 

@Override

- 오버라이딩하는 메소드 앞에 붙임

- 컴파일러에게 오버라이딩이 정확한지 확인하도록 지시

 

오버로딩(overloading) vs 오버라이딩(overriding)

비교 요소 메소드 오버로딩 메소드 오버라이딩
선언 같은 클래스나 상속 관계에서 동일한 이름의 메소드 중복 작성 서브 클래스에서 슈퍼 클래스에 있는 메소드와동일한 이름의 메소드 재작성
관계 동일한 클래스 내 혹은 상속 관계 상속 관계
목적 이름이 같은 여러 개의 메소드를 중복 선언하여 사용의 편리성 향상 슈퍼 클래스에 구현된 메소드를 무시하고 서브클래스에서 새로운 기능의 메소드를 재정의하고자 함
조건 메소드 이름은 반드시 동일함. 메소드의 인자의 개수나 인자의 타입이 달라야 성립 메소드의 이름, 인자의 타입, 인자의 개수, 리턴 타입 등이 모두 동일하여야 성립
바인딩 정적 바인딩. 컴파일 시에 중복된 메소드 중 호출되는 메소드 결정 동적 바인딩. 실행 시간에 오버라이딩된 메소드 찾아 호출

 


추상 클래스

추상 메소드

- abstract 키워드와 함께 원형만 선언되고, 코드는 작성되지 않은 메소드

abstract public String getName();	// 추상 메소드
abstract public String fail() { return "Good Bye"; }	// 추상 메소드 아님. 컴파일 오류

 

추상 클래스

- 추상 메소드를 최소 한 개 이상 가지고 abstract로 선언된 클래스

- 추상 메소드가 없어도 abstract로 선언한 클래스

// 추상 메소드를 가진 추상 클래스
abstract class Shape { // 추상 클래스 선언
	public Shape() { ... }
	public void edit() { ... }
	
	abstract public void draw(); // 추상 메소드 선언 
}
// 추상 메소드 없는 추상 클래스
abstract class Jcomponent { // 추상 클래스 선언
	String name;
	public void load(String name) {
		this.name = name;
	}
}

- 추상 메소드를 가지고 있으면 반드시 추상 클래스로 선언되어야 한다.

- 추상 클래스의 인스턴스를 생성할 수 없다.

Jcomponent p; // 오류 없음. 추상 클래스의 레퍼런스 선언
p = new JComponent(); // 컴파일 오류. 추상 클래스의 인스턴스 생성 불가
Shape obj = new Shape(); // 컴파일 오류. 추상 클래스의 인스턴스 생성 불가

 

추상 클래스의 상속

- 추상 메소드를 가진 추상 클래스를 상속받는 서브 클래스는 자동으로 추상 클래스가 된다.

- 그러므로 서브 클래스에 abstract를 붙여 추상 클래스임을 명시해야 함.

abstract class A { // 추상 클래스 
	abstract public int add(int x, int y); // 추상 메소드 
}
abstract class B extends A { // 추상 클래스. 추상 메소드 add()를 상속받기 때문
	public void show() { System.out.println("B"); }
}

 

추상 클래스의 구현

- 서브 클래스에서 슈퍼 클래스의 모든 추상 메소드를 오버라이딩하여 실행 가능한 코드로 구현하는 것

- 추상 클래스를 구현한 서브 클래스는 인스턴스를 생성할 수 있다.

class C extends A { // 추상 클래스 구현. C는 정상 클래스
	@Override
	public int add(int x, int y) { return x+y; } // 추상 메소드 구현. 오버라이딩
	public void show() { System.out.println("C"); }
}

 

추상 클래스의 목적

- 상속을 위한 슈퍼 클래스로 활용하기 위한 것

- 추상 클래스는 추상 메소드를 통해 서브 클래스가 구현할 메소드의 원형을 알려주는 인터페이스의 역할

- 서브 클래스의 다형성을 실현

abstract class Calculator {
	public abstract int add(int a, int b); // 두 정수의 합을 구하여 리턴 
	public abstract int subtract(int a, int b); // 두 정수의 차를 구하여 리턴 
	public abstract double average(int[] a); // 정수 배열의 평균 리턴 
}

public class GoodCalc extends Calculator {
	@Override
	public int add(int a, int b) { // 추상 메소드 구현
		return a+b;
	}
	@Override
	public int subtract(int a, int b) { // 추상 메소드 구현
		return a-b;
	}
	@Override
	public double average(int[] a) { // 추상 메소드 구현
		double sum = 0;
		for (int i=0; i<a.length; i++)
			sum += a[i];
		return sum/a.length;
	}
	
	public static void main(String[] args) {
		GoodCalc c = new GoodCalc();
		System.out.println(c.add(2, 3));
		System.out.println(c.subtract(2, 3));
		System.out.println(c.average(new int [] {2, 3, 4}));

	}

}

인터페이스

interface PhoneInterface { // 인터페이스 선언
	public static final int TIMEOUT = 10000; // 상수 필드. public static final 생략 가능
	public abstract void sendCall(); // 추상 메소드. public static abstract 생략 가능
	public abstract void receiveCall(); // 추상 메소드. public static abstract 생략 가능
	public default void printLogo() { // 디폴드 메소드는 public 생략 가능
		System.out.println("** Phone **");
	} // 디폴트 메소드 
}

- interface 키워드를 사용하여 선언

- 5 종류의 멤버로 구성되며, 필드(멤버 변수)를 만들 수 없다.

   · 상수, 추상 메소드, default 메소드, private 메소드, static 메소드

   · 상수: public static final 속성이며, 속성은 생략 가능

   · 추상 메소드: public abstract 속성이며, 속성은 생략 가능

   · default 메소드: 접근 지정 public으로 고정

   · private 메소드: 인터페이스 내의 다른 메소드에서만 호출 가능

   · static 메소드: 접근 지정이 생략되면 public이며, private으로 지정 가능

   · default, private, static 메소드들은 코드가 작성되어 있어야 한다.

- 인터페이스는 객체를 생성할 수 없다.

 

인터페이스 상속

- 상속을 통해 기존 인터페이스에 새로운 규격을 추가한 새로운 인터페이스를 만든다.

interface MobilePhoneInterface extends PhoneInterface {
	void sendSMS(); // 추상 메소드
	void receiveSMS(); // 추상 메소드 
}

- 이렇게 함으로써, MobilePhoneInterface 인터페이스는 총 6개의 멤버를 가지게 된다.

- 인터페이스는 콤마로 연결하여 2개 이상의 인터페이스를 다중 상속할 수 있다.

interface MusicPhoneInterface extends PhoneInterface, MP3Interface {
	...
}

 

인터페이스 구현

- implements 키워드를 사용하여 인터페이스의 모든 추상 메소드를 구현한 클래스를 작성하는 것

class SamsungPhone implements PhoneInterface { // 인터페이스 구현
	// PhoneInterface의 모든 추상 메소드 구현
	public void sendCall() { System.out.println("띠리리리링"); }
	public void receiveCall() { System.out.println("전화가 왔습니다."); }
	
	// 메소드 추가 작성
	public void flash() { System.out.println("전화기에 불이 켜졌습니다."); }
}