➡️ 문법 : stract class 클래스이름
추상클래스의 역할
추상 메서드는 위 이미지의 메서드의 구성 요소 중 선언부
만 작성한다 (마칠때 세미콜론(;) 포함)
주석으로는 어떤 기능을 수행할 목적으로 작성하였는지 설명한다
구현부
인 메서드 내용은 추상클래스를 상속 받는 자손클래스에서 overriding을 통해 재정의를 해서 사용한다.
즉, 해당 추상 클래스를 상속받은 자손 클래스는 반드시 클래스 내에 있는 추상 메서드를 전부 재정의(overriding) 해줘야만 사용이 가능하다
➡️ 문법 : interface 인터페이스 이름 { 인터페이스 내용 }
인터페이스는 일종의 추상 클래스이다. 추상클래스 보다 추상화 정도가 높은 추상 메서드를 가지고 있는 클래스의 일종이라고 생각하면 된다.
인스턴스만 생성 불가이고, 생성자, 멤버변수, 메서드는 얼마든지 넣어도 되는 추상 클래스와 달리 인터페이스가 가질 수 있는 것은 단 2개이다.
어차피 인터페이스 안에는 위 2가지만 쓸 수 있기 때문에 상수랑 추상 메서드 앞에 길게 붙는 수식어들(아래 괄호 안)을 생략할 수 있다.
(public static final) [타입] [상수이름] = [값];
(public abstract) [메서드이름] ([매개변수]);
인터페이스가 특이한 점은, 다중상속이 가능하다는 것이다.
interface Movable {
void move (int x, int y);
}
interface Attackable {
void attck (Unit u);
}
interface Fightable extends Movable, Attackable {
// 위의 인터페이스 2개를 상속 받았으므로, 이 안에서는
// move(), attack() 메소드를 쓸 수 있다 (멤버로 가진다)
}
인터페이스는 추상 클래스와 마찬가지로 인스턴스를 생성할 수 없고, 다른 클래스에서 인터페이스를 받아서 메소드를 완성해 줘야 한다.
이렇게 다른 클래스에서 인터페이스를 받아 쓸 때는, 상속과 마찬가지로 클래스 이름 뒤에 implements
를 붙이고 인터페이스 이름을 넣어주면 된다. 이 때 이런 것을 [인터페이스 이름]이 [클래스 이름]을 구현한다
고 말한다.
위 이미지 같이 상속과 구현을 동시에 하는 것도 가능하다.
인터페이스 이름은 어떠한 기능 또는 행위를 하는데 필요한 메소드를 제공한다는 의미를 강조하기 위해서
Fightable
과 같이 ~할 수 있는 이라는 뜻을 가진 able을 붙이는 것이 일반적이다.
class Unit { int currentHP, x, y; }
interface Movable {
void move (int x, int y);
}
interface Attackable {
void attack (Unit u);
}
interface Fightable extends Movable, Attackable {
// 위의 인터페이스 2개를 상속 받았으므로, 이 안에서는
// move(), attack() 메소드를 쓸 수 있다 (멤버로 가진다)
}
class Fighter extends Unit implements Fightable {
public void move (int x , int y) { 함수 내용 }
public void attack (Unit u) { 함수 내용 }
}
위의 코드를 도식화 하면 아래 그림과 같다.
Fightable
인터페이스는 Movable
과 Attackable
인터페이스를 다중상속 받았기 때문에 Fighter
클래스는 Fightable
인터페이스만 구현했지만, move();
과 attack();
메소드를 모두 가져와서 완성시킬 수 있었다.
이 때, 주목해야할 점은 Fighter
클래스에서 두 메소드를 완성시킬 때 public
이 사용되었다는 것이다. 이 이유는, 인터페이스에 들어가는 모든 메소드에는 앞서 말했다시피, public abstract
이 생략 되었기 때문이다.
이와 같이 인터페이스는 다중 상속이 가능하지만 실제로 인터페이스를 통해 다중 상속을 구현하는 경우는 거의 없다. 굳이 다중 상속을 해야할 경우에는 보통 두 조상 클래스 중에서 비중이 높은 쪽을 선택하여 구현하고 다른 한 쪽은 클래스 내부에 멤버로 포함 시키거나 필요한 부분만 뽑아서 포함 시키는 것이 바람직하다.
인터페이스 타입으로 형변환 하는 것을 알아보기 전에, 며칠 전에 배웠던 다형성과 참조변수의 형번환을 다시 복습해보자.
객체지향개념에서 다형성 이란?
조상
클래스 타입의 참조변수
로 자손
클래스의 인스턴스
를 참조할 수 있는 것
class Tv {
boolean power;
int channel;
void power() { power = !power; }
void channelUp() { ++channel; }
void channelDown() { --channel; }
}
class CaptionTv extends Tv {
String text;
void caption() { /*내용생략*/ }
}
public class Exercise {
public static void main(String[] args) {
Tv t = new CaptionTv();
//조상class 참조변수 인스턴스 자손타입
// -> 조상 타입의 참조변수로 자손 인스턴스를 참조
CaptionTv c = new CaptionTv();
Tv t = (Tv) c;
// 위 두 줄을 합치면 Tv t = new CaptionTv();가 된다
}
}
위와 같이 CaptionTv
가 Tv
를 상속할 때, Tv
타입의 참조변수로 CaptionTv
타입의 인스턴스를 참조할 수 있는 것이다. 이 때, 참조변수 t
는 CaptionTv
에 있는 인스턴스 중, Tv
에서 정의된 멤버들만 사용할 수 있다. (String text
와 caption()
은 사용하지 못함)
참조변수들은 상속 관계가 있을 때, 형변환으로 부모 타입이나 자식 타입으로 변신할 수 있다. 이때, 자손타입에서 부모타입은 (부모타입)
이라는 캐스팅 연산자를 생략 가능하지만, 부모타입에서 자손타입은 생략이 불가하다.
👶🏻 ➡️ 👵🏻 : 형변환 생략가능
👵🏻 ➡️ 👶🏻 : 형변환 생략불가
🤷♀️ WHY? →
자손타입이 더 포함하고 있는 내용이 많기 때문에 (내꺼도 내꺼, 니꺼도 내꺼)
애초에 형변환은 왜 하는 걸까?
형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다.
단지 참조변수의 형변환을 통해서 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것 뿐이다.
서로 상속관계에 있는 타입 간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다. 그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.
이때 쓰는 것이 연산자 instanceof
이다. instanceof
는 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아볼 수 있다. 주로 조건문(if)에서 쓰이며, 결과값은 true/false로 반환된다.
if ( 참조변수 instanceof 타입명 ) {
타입명 변수명 = (타입명) 참조변수;
// 형변환이 가능할 때(true), 형변환을 하는 구문
}
instanceof
는 구체적으로 다음과 같은 상황일 때 쓰일 수 있다.
void doWork(Car c) {
if (c instanceof FireEngine) {
FireEngine fe = (Fireengine) c;
fe.water();
} else if (c instanceof Ambulance) {
Ambulance a = (Ambulance) c;
a.siren();
}
}
/* ... */
public class Exercise {
public static void main(String[] args) {
doWork(c);
// 여기서 매개변수를 입력 했을 때, c의 클래스가
// Car클래스일지, Car클래스의 자손클래스일지 모름
}
}
예제에서 보면, doWork
함수에서 매개변수로 Car
타입의 참조변수 c
를 받고 있다. 이 함수가 실제로 메인 함수에서 쓰였을 때, 매개변수로 입력될 수 있는 것은 Car 클래스 및 Car 클래스의 자손 클래스를 타입으로 가지는 인스턴스
이다. 그러므로, 매개변수로 입력되는 것(인스턴스)이 FireEngine
타입으로 형변환이 가능할지 모르는 것이다.
이럴 때는 무작정 형변환을 시도하는 것이 아닌, instanceof
로 형변환이 가능한지 체크를 해주고 형변환을 진행해야 한다.
class Unit { int currentHP, x, y; }
interface Movable {
void move (int x, int y);
}
interface Attackable {
void attck (Unit u);
}
interface Fightable extends Movable, Attackable {
// 위의 인터페이스 2개를 상속 받았으므로, 이 안에서는
// move(), attack() 메소드를 쓸 수 있다 (멤버로 가진다)
}
class Fighter extends Unit implements Fightable {
public void move (int x , int y) { /*함수 내용*/ }
public void attack (Unit u) { /*함수 내용*/ }
}
public class Exercise {
public static void main(String[] args) {
Fightable f = (Fightable) new Fighter();
/*또는*/
Fightable f = new Fighter();
}
}
다시 인터페이스 얘기로 돌아와서, 인터페이스를 구현한 클래스는 인터페이스를 부모 클래스로 둔 자손 클래스라고도 볼 수 있다. 따라서 위 코드에서 보면 Fightable
타입의 참조변수로 Fighter
인스턴스를 호출한 것을 알 수 있다. (단, 참조변수로는 인터페이스 Fightable
에 정의된 멤버만 가능하다.)
인스턴스를 참조할 수 있으니까, 매개변수로도 쓸 수 있다.
void attack (Fightable f) { /*함수내용*/ }
/*또는 함수 호출 시에 아래와 같이도 사용 가능*/
attack(new Fighter());
이렇게. 아래와 같이 리턴타입으로 지정하는 것도 가능하다.
Fightable method() {
/* 함수 내용 */
Fighter f = new Fighter();
return f;
// 위 두 문장을 한 문장으로 합치면
return new Fighter();
}
이런 식으로.
이는 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.
→ 풀어쓰면, Fightable
이 리턴 타입이다 = Fighter
클래스의 인스턴스를 리턴하는 거나 마찬가지라는 얘기.
이렇게 인터페이스의 다형성은 아래 예제처럼도 활용될 수 있다.
interface Parseable {
// 구문 분석 작업을 수행한다.
public abstract void parse (String fileName);
}
class ParserManager {
// 리턴 타입이 Parseable 인터페이스
public static Parseable getParser(String type) {
if (type.equals("XML")) {
return new XMLParser();
} else {
Parseable p = new HTMLParser();
return p;
// return new HTMLParser();
}
}
}
class XMLParser implements Parseable {
@Override
public void parse(String fileName) {
/* 구문 분석 작업을 수행하는 코드를 적는다 */
System.out.println(fileName + "- XML parsing completed");
}
}
class HTMLParser implements Parseable {
@Override
public void parse(String fileName) {
/* 구문 분석 작업을 수행하는 코드를 적는다 */
System.out.println(fileName + "- HTML parsing completed");
}
}
public class Interface_ParserTest {
public static void main(String[] args) {
Parseable parser = ParserManager.getParser("XML");
// parser는 XMLParser의 주소값을 갖게 된다
parser.parse("document.xml");
parser = ParserManager.getParser("HTML");
// parser는 HTMLParser의 주소값을 갖게 된다
parser.parse("document2.html");
}
}
다음과 같다. 복잡하다.
일단, 해당 예제는 파일 확장자가 xml인지, html인지 구분하는 프로그램을 구현한 것이다.
Parseable
은 인터페이스로, 파일 이름을 매개변수로 받아서 분석을 하는 함수를 포함하고 있다.ParserManager
은 클래스로, 확장자(type)을 매개변수로 받아서 파일 확장자가 XML이면 XMLParser
클래스를 리턴하고, HTML이면 HTMLParser
클래스를 리턴하는 함수(getParser
)가 들어있다.XMLParser
, HTMLParser
는 인터페이스 Parseable
를 구현하는 클래스로, 인터페이스 안에 있는 parse
함수를 재정의 하여, 파일 이름 밑 구문 분석이 끝났다는 문구를 출력하는 내용을 넣어주었다 (=구현하였다).main
함수 영역에서는 인터페이스 타입의 parser
객체를 만들고, getParser
함수를 호출하여 "XML"이라는 타입명을 매개변수로 넣어주었다.Parseable parser = ParserManager.getParser("XML");
를 실행하면, parser
에는 스태틱 함수인 getParser
의 실행 결과가 저장되게 된다. 즉, parser
는 XMLParser
의 주소값을 가지게 된 것이다.상속관계 (is-a)
우리가 여태까지 했던 부모-자식 관계
→ a가 b를 상속한다 (a extends b)
라고 했을 때, a의 객체가 생성됨과 동시에 b도 메모리에 올라간다.
포함관계 (has-a)
b는 a에 포함된다는 것은 b가 a의 부품(멤버필드)이라는 것
클래스 안에 B b;
와 같은 형식으로 선언됨
의존관계
함수(method) 내에서 사용되는 것을 말함
→ 포함과는 다르게 부품(멤버필드)로 존재하지 않는다
→ 객체를 지역변수
또는 매개변수
로 사용하는 것
→ 포함과는 다르게 함수 내에서만 사용되고 함수가 끝나면 증발
(더 이상 객체를 참조하지 않는다)