bdfgdfg

자바의 상속,다형성,인터페이스 본문

웹프로그래밍/Java

자바의 상속,다형성,인터페이스

marmelo12 2023. 7. 26. 12:10
반응형

상속의 의미는 기존의 클래스를 재사용(물려받아) 새로운 클래스를 작성하는 것이다.

객체지향에서 상속은 코드의 재사용성과 중복을 제거하여 프로그램의 생산성과 유지보수성을 높인다.

 

public class Child extends Parent 
{

}

자바의 상속은 클래스명 뒤에 extends 키워드와 함께 부모 클래스명을 붙여준다.

 

자바 언어의 상속에서 주요 특징은 다음과 같음.

1. C++언어에서는 다중 상속을 허용하지만 자바에서는 허용하지 않음. (인터페이스 제외)

2. 클래스의 최상위 부모는 Object라는 클래스. 클래스가 상속 받는 상태가 아니라면 컴파일러가 자동으로 extends Object를 붙여준다.

 

오버라이딩

오버라이딩은 객체지향의 다형성 키워드와 연관된 키워드이며, 부모 클래스의 메소드를 자식 클래스에서 재정의 하는것을 의미한다.

 -> ex) 부모의 void show()함수를 자식에서 다시 void show()로 작성하여 재정의.

 -> 특이하게 자바는 C++/C#과 달리 virtual, overriding키워드를 붙이지 않더라도 부모 클래스의 메소드를 자식에서 재정의 하면 자동으로 오버라이딩 된다. (그렇지만 어노테이션을 이용해 오버라이딩된 메소드라는것을 명시해주는게 좋다.)

 -> 오버라이딩의 조건은 메소드명, 매개변수(타입, 개수), 반환타입이 모두 같아야한다.

public class Point // 부모 클래스
{
	protected int x = 0;
	protected int y = 0;
	
	public String getLocation()
	{
		return "x : " + x + ", y : " + y; 
	}
}

public class Point3D extends Point // 자식 클래스
{
	private int z = 5;
	
	public String getLocation()
	{
		return "x : " + x + ", y : " + y + ", z : " + z; 
	}
}

import java.util.*;

public class Hello {
	public static void main(String[] args) 
	{
		// TODO Auto-generated method stub
		
        	// 부모 타입이 자식 객체를 가리킴.
		Point p = new Point3D();

		System.out.println(p.getLocation());
	}
}

출력은 자식 클래스의 getLocation 메소드가 호출된다. 

 

참고로. static 메소드를 오버라이딩의 조건과 동일하게 하여 각각 부모와 자식에서 정의한다해도 그것은 별개의 static 메소드를 정의한것이지 오버라이딩이 아니다.

 -> 그렇기에 static 메소드는 호출 시 클래스명.메소드명()으로 호출하는게 올바름. 

 -> 자바는 신기한게 참조변수.static메소드명() 이렇게도 static 멤버함수를 호출할 수 있다.

 

참고로 오버로딩과 오버라이딩은 비슷해보이지만 개념이 다름.

 

오버로딩은 이름이 같은 함수를 매개변수의 타입이나 갯수를 달리하여 선언하는 것. 

오버라이딩은 부모 클래스의 함수를 자식에서 재정의하여 사용하는 것.

즉 간단히 정리하면 오버로딩은 함수의 중복 정의, 오버라이딩은 부모 함수의 재정의.

 

Super 키워드

this는 현재 클래스의 인스턴스를 가리키는 것. 즉 자기 자신의 클래스의 멤버에 접근할 때 사용.

super는 조상의 멤버에 접근할 때 사용.

사실 super를 물려받은 this에서는 super의 멤버를 이미 가지고 있기에 멤버가 중복되어 서로 구별해야하는 경우에 쓰인다.

public class Point3D extends Point {
	private int z = 5;
	
	public String getLocation()
	{
    		// 사실상 같은 것
		int abcd = super.x;
		int bbbb = this.x; 
        
        
		return "x : " + x + ", y : " + y + ", z : " + z; 
	}
}

다만 부모클래스에서 int형 변수 x 멤버를 들고있고, 자식에서도 int형 변수 x 멤버를 들고있다면 구분을 위해 super.x, this.x로 접근하여야 한다.

 

또한 super() 키워드도 존재하는데 조상 클래스의 생성자를 호출하는 것.

 -> C++에서 직접 부모의 클래스명(생성자명)을 호출하는것과는 다름.

 

부모의 생성자를 호출한다는 것은, 부모의 멤버를 초기화 한다는 의미.

public class Point3D extends Point {
	private int z = 5;
	
	Point3D(int x, int y, int z)
	{
    		// 직접 넣어주기
		this.x = x;
		this.y = y;
		this.z = z;
	}
	
	public String getLocation()
	{
		return "x : " + x + ", y : " + y + ", z : " + z; 
	}
}

물론 부모의 생성자를 호출하지 않고, 어차피 자식에서 물려받으니 위와같이 자식에서 부모의 멤버까지 초기화 할 수도 있다.

다만, 위의 클래스는 내용이 매우 간단하기에 가능한 것이고 이보다 더 복잡한 클래스에서 위와 같이 초기화 한다면 초기화 가 제대로 이루어지지 않아 의도치 않은 결과나 오류가 발생할 수 있다.

 

그렇기에 가능한 부모의 멤버는 super() 키워드를 호출해서 부모 생성자에서 직접 초기화하는것이 좋다.

 -> 즉 자기자신의 멤버는 자기 자신이 초기화.

public class Point 
{
	protected int x = 0;
	protected int y = 0;
	
	Point(int x, int y)
	{
		this.x = x;
		this.y = y;
	}
	
	public String getLocation()
	{
		return "x : " + x + ", y : " + y; 
	}
}


public class Point3D extends Point {
	private int z = 5;
	
	Point3D( int x, int y, int z)
	{
		super(x,y); // super() 키워드는 무조건 첫번째 줄에서 호출되어야한다.
		this.z = z;
	}
	
	public String getLocation()
	{
		return "x : " + x + ", y : " + y + ", z : " + z; 
	}
}

부모 클래스에서 명시적으로 매개변수가 있는 생성자를 추가했기에, 자식 클래스에서 부모 클래스의 생성자를 호출해줘야만 컴파일  에러가 발생하지 않는다.

 -> 기존에는 생성자를 정의하지 않더라도 컴파일러가 자동으로 매개변수 없는 기본 생성자를 만들며, 자식에서도 자동으로 컴파일러가 super(); 를 추가해주었음.

 

또한 super()키워드는 생성자 첫번째줄에서만 호출해야한다. (다른줄에서 호출하면 에러발생)

 

참고로 부모 자식관계에서의 생성자 초기화 순서

최고로 높은 부모 생성자부터 먼저 호출 후 마지막 자식의 생성자가 호출된다. 부모->자식

 

abstract

abstract는 미완성의 의미를 가지며 추상적이라는 의미도 존재한다.

언어에서는 클래스, 메소드에 붙여 선언부만 작성하고 실제 객체생성,수행내용은 자식을 통해서만 생성,정의를 유도할 수 있다.

 

public abstract class Animal // 추상 클래스 
{
	// 추상 메소드
	public abstract void AnimalSound(); 
}

위 클래스는 추상클래스이며, 추상 메소드를 가진다.

실제로 추상클래스는 인스턴스화가 불가능하다.

 

또한 Animal을 상속받는 Cat클래스를 추가해보면

실제로 내용물을 구현하지 않을 시 에러를 띄운다.

위와 같이 에러를 내뱉는다. 

public class Cat extends Animal 
{
	public void AnimalSound()
	{
		System.out.println("meow");
	}
}

그렇기에 위와 같이 추상 메서드를 오버라이딩 해준다.

 

다형성

위에서 설명한 오버라이딩이 다형성을 위한 선행작업이다.

다형성이란 하나의 물체가 여러가지 형태를 가질 수 있다는 것을 의미하며

언어레벨에서는 한 타입의 참조변수가 여러 타입의 객체를 참조할 수 있도록 하는 것을 의미.

 -> 즉 부모 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조할 수 있다는 의미.

 -> 단 다형성이 적용된 오버라이딩된 메소드와 달리 멤버변수의 경우 참조변수의 타입에 따라 달라진다.(부모, 자식간의 중복된 멤버변수. 즉 부모타입일 경우 부모타입의 멤버변수를. 자식타입일 경우 자식타입의 멤버변수를 가리킴)

 

참조변수의 형변환

참조변수의 형변환은 서로 상속 관계에 있는 클래스 사이에서만 가능하다.

자식타입 참조변수 -> 부모타입의 참조변수

부모타입의 참조변수 -> 자식타입의 참조변수

 -> 즉 Object클래스와도 형변환이 가능.

 

기본적으로

자식 타입 -> 부모타입(Up-Casting) : 형변환 생략가능

부모 타입 -> 자식타입(Down-Casting) : 형변환 생략불가능.

 

왜 가능한지와 불가능한지는 생각해보면 알 수 있다.

 

자식 타입 -> 부모타입(Up-Casting)

더 큰 메모리(혹은 같거나)를 가지는 자식 타입에서 더 작은 메모리(혹은 같거나)를 가지는 자식 타입으로는 접근할 수 있는 정보가 줄어들분 아무 문제가 없기에 안전하게 형변환 가능한 것이지만,

부모 타입 -> 자식타입(Down-Casting)

이 경우에는 더 작은 메모리에서 더 큰 메모리의 타입으로 변환하는것이기에 안전하지 않다는 의미.

 

즉 Animal을 상속받은 Cat을 통해 생성된 Cat객체를 Animal가리키는 안전.

반대로 Cat 타입이 Animal 인스턴스를 가리키는 것은 안전하지 않음.

 

그렇기에 다운캐스팅 형변환을 안전하게 하려면 instanceof연산자를 이용하여 실제 인스턴스의 타입을 확인하는게 좋다.

 

instanceof 연산자

참조변수가 참조하는 인스턴스의 형이 맞는지를 체크하는 연산자.

맞다면 true 아니라면 false를 반환.

import java.util.*;

public class Hello {
	public static void main(String[] args) 
	{
		// TODO Auto-generated method stub
		Cat a = new Cat();
		Animal b = new Animal();
		
		System.out.println(a instanceof Animal); // true
		System.out.println(a instanceof Cat); // true
		System.out.println(b instanceof Animal); // true
		System.out.println(b instanceof Cat); // false
			
	}
}

(테스트 코드 작성을 위해 Animal 클래스의 abstract 키워드를 제거함)

cat은 Animal을 상속받았고, 실제 Cat의 인스턴스이기에 모두 true를 반환.

Animal은 실제 Animal인스턴스이기에 true를 반환하지만, 자식타입에 경우 false를 반환.

 

참고로 참조 변수가 가리키는 실제 인스턴스의 타입을 문자열로 얻는방법이 있는데

참조변수.getClass().getName()을 통해 알 수 있따.

 

인터페이스(interface)

인터페이스는 일종의 추상클래스.

인터페이스는 실제 추상클래스와 달리 일반 멤버변수나 일반 메소드를 가질 수 없다. (오직 추상메소드와 상수)

 -> C++에서 인터페이스 문법은 존재하지 않기에 실제 추상클래스로 만들고 인터페이스처럼 활용하기도 한다.

 

실제 사용법은 밑과 같다.

public interface Flyable {
	public abstract void Fly();
}


// 몬스터 클래스를 상속받으며 Flyable 인터페이스를 구현.
public class Orc extends Monster implements Flyable
{
	public void Fly()
	{
		System.out.println(this.getClass().getName() + " Fly~");
	}
}

 

인터페이스는 확장의 의미인 extends가 아닌 구현의 의미로서 implements 키워드를 사용해야한다.

 

다만 인터페이스 끼리는 extends를 사용한다.

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

interface Attackable
{
	public void attack();
}

interface Fightable extends Movable, Attackable
{
	
}

또한 인터페이스를 상속한 클래스의 인스턴스도 다형성이 가능하다. 실제로 부모 자식간 다형성과 다르지 않음.

 

 

 

 

반응형

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

제어자, 캡슐화/은닉성  (0) 2023.07.26
package와 import  (0) 2023.07.26
비동기&동기 / 블로킹&논블로킹 IO  (0) 2023.07.26
클래스 초기화  (0) 2023.07.25
자바 & JVM  (0) 2023.07.25
Comments