수학과의 좌충우돌 프로그래밍

[JAVA] 05. 객체지향 프로그래밍 (3) 본문

프로그래밍 언어/Java

[JAVA] 05. 객체지향 프로그래밍 (3)

ssung.k 2020. 12. 27. 22:13

1. 다형성(polymorphism)

다형성이란 여러 가지 형태를 가질 수 있는 능력을 의미하며 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 구현하였습니다.

이로서 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였습니다.

 

아래 예시를 봅시다.

class Tv {
	int channel;
	
	void channelUp() {
		++channel;
	}
	
	void channelDown() {
		--channel;
	}
}

class CaptionTv extends Tv {
	String text;
}

 

일반적으로 그 동안 각 클래스의 인스턴스를 생성하기 위해 아래와 같이 인스턴스의 타입과 일치하는 타입의 참조변수만을 사용했습니다.

Tv t = new Tv();
CaptionTv c = new CaptionTv();

 

하지만 상속관계에 있는 경우, 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조하도록 하는 것이 가능합니다.

Tv t2 = new CaptionTv();

 

위 c와 t2는 실제 인스턴스는 CaptionTv이지만 참조변수가 다릅니다.

그렇기 때문에 사용할 수 있는 멤버의 개수가 차이가 납니다.

c는 CaptionTv의 모든 멤버를 사용하지만,

t2는 Tv의 멤버만을 사용할 수 있어 text 멤버는 사용할 수 없습니다.

 

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

CaptionTv c2 = new Tv(); // 컴파일 에러

 

 

참조변수의 형변환

기본형 변수와 같이 참조변수도 형변환이 가능합니다.

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

기본형 변수의 형변환에서 작은 자료형에서 큰 자료형의 형변환은 생략이 가능하듯이, 참조형 변수의 형변환에서는 자손타입의 참조변수를 조상타입으로 형변환하는 경우에는 형변환을 생략할 수 있습니다.

자손 -> 조상 (Up-casting) : 형변환 생략가능
조상 -> 자손 (Down-casting) : 형변환 생략불가

 

 

instanceof 연산자

참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof 연산자를 사용합니다.

instanceof 연산의 결과로 true를 얻었다는 것은 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 의미합니다.

 

아래 예시를 봅시다.

public class HelloWorld {

	public static void main(String[] args) {
		CaptionTv c = new CaptionTv();
		
		if (c instanceof CaptionTv) {
			System.out.println("c는 CaptionTv의 인스턴스");
      // c는 CaptionTv의 인스턴스
		}
		
		if (c instanceof Tv) {
			System.out.println("c는 Tv의 인스턴스");
      // c는 Tv의 인스턴스
		}
		
		if (c instanceof Object) {
			System.out.println("c는 Object의 인스턴스");
      // c는 Object의 인스턴스
		}
	}
}

class Tv {}

class CaptionTv extends Tv {}

CaptionTv의 인스턴스 임에도 불구하고 다른 두 클래스의 인스턴스라고 출력이 됩니다.

CaptionTv는 Tv와 Object를 상속받았기 때문에 둘을 포함하고 있기 때문입니다.

 

 

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

조상 타입의 참조변수와 자손 타입의 참조변수의 차이점이 사용할 수 있는 멤버의 개수에 있다고 했습니다.

그 외에는 또 어떤 차이가 있을까요?

 

메서드의 경우에는 조상 클래스의 메서드를 자손의 클래스에서 오버라이딩한 경우에도 참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드(오버라이딩된 메서드)가 호출되지만 멤버 변수는 참조변수에 따라 타입이 달라집니다.

 

아래 예시를 봅시다.

public class HelloWorld {

	public static void main(String[] args) {
		Tv t = new Tv();
		CaptionTv c = new CaptionTv();
		
		System.out.println(t.x);
		t.method();
    // 100
    // child method
		
		System.out.println(c.x);
		c.method();
    // 200
    // child method
	}
}

class Tv {
	int x = 100;
	
	void method() {
		System.out.println("parent method");
	}
}

class CaptionTv extends Tv {
	int x = 200;
	
	void method() {
		System.out.println("child method");
	}
}

 

하지만 CaptionTv에 x 멤버변수가 정의되어 있지 않다고 하면 조상으로부터 멤버를 상속받아 100을 출력하게 됩니다.

 

 

매개변수의 다형성

아래와 같은 예제를 생각해봅시다.

class Product {
	int price;
}

class Tv extends Product {}
class CaptionTv extends Product {}

class Buyer {
	int money = 1000;
	
	void buy(Product p) {
		money -= p.price;
	}
}

 

지금은 buy의 매개변수가 Product 이기 때문에 Product를 상속받는 여러 클래스들을 범용적으로 사용할 수 있습니다.

만약 매개변수의 다형성이 불가능하다면 각 클래스에 대한 buy 함수를 각각 만들어야 하는 불편함이 생깁니다.

 

 

여러 종류의 객체를 배열로 다루기

조상타입의 참조변수로 자손타입의 객체를 참조하는 것이 가능하므로 아래와 같이 가능합니다.

Product p1 = new Tv();
Product p2 = new CaptionTv();

 

그렇기 때문에 아래와 같이 한 배열에 담을 수 있습니다.

Product p[] = new Product[2];
p[0] = new Tv();
p[1] = new CaptionTv();

 

 

2. 추상클래스(abstract class)

추상클래스

추상클래스는 미완성된 클래스를 나타냅니다.

미완성된 메서드(추상메서드)를 포함하고 있는 클래스를 추상클래스라고 합니다.

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

 

추상클래스를 만들기 위해서는 class 앞에 abstract를 붙이기만 하면 됩니다.

abstract class 클래스이름 {
  
}

 

추상메서드(abstract method)

메서드는 선언부와 구현부로 구성되어 있습니다.

추상메서드는 선언부만 작성하고 구현부는 작성하지 않은 체로 남겨둡니다.

abstract 리턴타입 메서드이름();

 

추상클래스로부터 상속받는 자손클래스는 오버라이딩을 통해 조상인 추상클래스의 추상메서드를 모두 구현해야합니다.

만약 하나라도 구현하지 않는다면 자손클래스 역시 추상클래스로 지정해 주어야 합니다.

 

 

3. 인터페이스(interface)

인터페이스

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

인터페이스는 추상클래스처럼 추상메서드를 갖지만 추상화 정도가 높아서 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없습니다.

오직 추상메서드와 상수만을 멤버로 가질 수 있으며 그 외에 다른 것은 허용되지 않습니다.

 

인터페이스를 작성하기 위해서는 class라는 키워드 대신 interface 키워드를 사용합니다.

inferface 인터페이스이름 {
  public static final 타입 상수이름 = 값;
  public abstract 메서드이름(매개변수목록); 
}

 

일반적인 클래스들의 멤버와 달리 인터페이스의 멤버들은 제약사항이 있습니다.

  • 모든 멤버변수는 public static final 이어야하며 이를 생략 가능
  • 모든 메서드는 public abstract 이어야 하며, 이를 생략 가능

 

JDK 1.8 부터는 인터페이스에 static 메서드와 디폴트 메서드의 추가를 허용하였습니다.

 

 

인터페이스의 상속

인터페이스는 인터페이스로부터만 상속받을 수 있으며 클래스와는 달리 다중 상속이 가능합니다.

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

interface Attackble {
  void attack(Unit u);
}

interface Fightable extends Movable, Attackble {}

 

인터페이스의 구현

클래스는 확장한다는 의미에서 extends를 쓰면 반면 인터페이스는 구현한다는 의미로 implements를 사용합니다.

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

 

또한 상속과 구현을 동시에 할 수 있습니다.

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

 

 

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

두 조상으로부터 상속받는 멤버 중에서 멤버변수의 이름이 같거나 메서드이 선언부가 일치하고 구현 내용이 다르면 어느 조상의 것을 상속받게 되는 것인지 알 수 없습니다.

 

인터페이스는 static 상수만 정의할 수 있으므로 조상클래스의 멤버변수와 충돌하는 경우는 거의 없고 충돌된다 하더라도 클래스 이름을 붙여서 구분이 가능합니다.

그리고 추상메서드는 구현내용이 없으므로 조상 클래스의 메서드와 선언부가 일치하는 경우에는 당연히 조상 클래스 쪽의 메서드를 상속받으면 되므로 문제가 되지 않습니다.

 

그러나 이렇게 되면 다중 상속의 장점을 잃게 되므로 두 개의 클래스로부터 상속을 받아야할 상황이면 비중이 높은 쪽을 상속받고 다른 한 쪽은 클래스 내부에 멤버로 포함시키는 방식으로 처리하거나 인터페이스로 구현하도록 합니다.

 

예를 들어 Tv 클래스와 VCR 클래스가 있을 때 TVCR 클래스를 작성하기 위해 한 쪽을 상속받고 나머지 한 쪽은 내부적으로 인스턴스를 생성하여 사용합니다.

public class Tv {
  protected boolean power;
  
  public void power() { power = !power; }
}

public class VCR {
  public void play() {
    /* Tape를 재생하는 로직 */
  }
}

public interface IVCR {
  public void play();
}

public class TVCR extends Tv implements IVCR {
  VCR vcr = new VCR();
  
  public void play(){
    vcr.play();
  }
}

 

VCR 클래스에 정의된 메서드와 일치하는 추상메서드를 갖는 인터페이스를 작성하여 사용합니다.

 

 

인터페이스를 이용한 다형성

자손클래스의 인스턴스를 조상타입의 참조변수로 참조하는 것이 가능했습니다.

인터페이스 역시 해당 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있습니다.

 

아래 예시를 봅시다.

public class ParserTest {

	public static void main(String[] args) {
		Parseble parser = ParserManager.getParser("XML");
		parser.parse("document.xml");

	}
}

interface Parseble {
	public abstract void parse(String fileName);
}

class ParserManager {
	public static Parseble getParser(String type) {
		if (type.equals("XML")) {
			return new XMLParser();
		} 
		return null;
	}
}

class XMLParser implements Parseble {
	public void parse (String fileName) {
		System.out.println(fileName + " XML parsing completed");
	}
}

 

Parseble 인터페이스의 객체 parser에 대해서 parse 메서드를 실행하였습니다.

Parseble 인터페이스의 객체는 parse 메서드의 구현부가 없지만 이 경우, parser가 참조하고 있는 XMLParser의 parse 메서드가 실행이 됩니다.

 

 

인터페이스의 장점

인터페이스를 사용하는 이유와 그 장점을 정리해봅시다.

  • 개발시간을 단축 메서드를 호출하는 쪽에서는 메서드의 내용에 관계없이 선언부만 알면 됩니다. 그렇기 때문에 메서드를 호출하는 쪽의 코드와 인터페이스를 구현하는 클래스의 코드를 동시에 개발이 가능합니다.
  • 표준화 가능 프로젝트에 사용되는 기본 틀을 인터페이스로 작성하여 표준화가 가능합니다.
  • 서로 관계없는 클래스 간의 관계 형성 상속관계에 있지도 않고, 같은 조상클래스를 가지고 있지 않아도 인터페이스를 통해서 관계를 맺어줄 수 있습니다.
  • 독립적인 프로그래밍 인터페이스를 통해 클래스의 선언과 구현을 분리할 수 있습니다. 클래스와 클래스간의 직접적인 관계를 인터페이스를 통해 간접적으로 변경하면 독립적인 프로그래밍이 가능해집니다.

 

 

디폴트 메서드와 static 메서드

인터페이스에는 추상 메서드만 선언할 수 있었는데 JDK 1.8부터 디폴트 메서드와 static 메서드도 추가할 수 있게 되었습니다.

디폴트 메서드

디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 됩니다.

디폴트 메서드는 앞에 키워드 default를 붙이며 추상 메서드와 달리 일반 메서드처럼 몸통이 있어야 합니다.

디폴트 메서드 역시 접근제어자가 public이며 생략가능합니다.

interface MyInterface {
  default void defaultMethod() {}
}

 

대신 새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우가 발생합니다.

이 충돌을 해결하는 규칙은 다음과 같습니다.

  • 여러 인터페이스의 디폴트 메서드 간의 충돌 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 합니다.
  • 디폴트 메서드와 조상 클래스의 메서드 간의 충돌 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시됩니다.

 

 

 

4. 내부 클래스(inner class)

내부 클래스는 클래스 내에 선언된다는 점을 제외하고는 일반적인 클래스와 다르지 않습니다.

클래스 내에서 클래스를 선언하는 이유는 두 클래스가 서로 긴밀한 관계가 있기 때문입니다.

 

한 클래스를 다른 클래스의 내부 클래스로 선언하면 두 클래스의 멤버들 간에 서로 쉽게 접근할 수 있고 외부에는 불필요한 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있다는 장점이 있습니다.

class A { // 외부 클래스
  class B { // 내부 클래스
    
  }
}

 

내부 클래스의 종류와 특징

내부 클래스의 종류는 변수의 선언위치에 따른 종류와 같습니다.

변수가 선언 위치에 따라 구분되는 것과 같습니다.

내부 클래스 특징
인스턴스 클래스 외부 클래스의 멤버변수 선언 위치에 선언하며, 외부 클래스의 인스턴스멤버들과 관련된 작업
스태틱 클래스 외부 클래스의 멤버변수 선언 위치에 선언하며, 외부 클래스의 static 멤버들과 관련된 작업
지역 클래스 외부 클래스의 메서드나 초기화 블럭 안에 선언하며 선언된 영역 내부에서만 사용
익명 클래스 클래스의 선언ㅇ과 객체의 생성을 동시에 하는 이름없는 클래스

 

내부 클래스의 선언

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

 

 

익명 클래스(anonymous class)

익명 클래스는 다른 내부 클래스들과는 달리 이름이 없습니다.

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

이름이 없기 때문에 생성자도 가질 수 없으며 조상클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 하나의 클래스로 상속받는 동시에 인터페이스를 구현하거나 둘 이상의 인터페이스를 구현할 수 없습니다.

오로지 단 하나의 클래스를 상속받거나 단 하나의 인터페이스만을 구현할 수 있습니다.

class AnonymousClass {
  Object iv = new Object(){ void method() };
}

 

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

[JAVA] 07. java.lang 패키지  (0) 2021.01.13
[JAVA] 06. 예외처리  (0) 2021.01.07
[JAVA] 04. 객체지향 프로그래밍 (2)  (0) 2020.12.27
[JAVA] 03. 객체지향 프로그래밍 (1)  (0) 2020.12.27
[JAVA] 02. 변수  (0) 2020.11.27
Comments