= 여러 형태를 가질 수 있는 능력
-> 여기서는 부모 객체의 참조변수로 자식 객체의 인스턴스를 참조!!
class Tv {
boolean power;
int channel;
void power() {power != power;}
void channelUp() {++channel;}
void channelDown() {--channel;}
}
class CapTv extends Tv {
Strint text;
void caption() {}
}
public static void main(String[] args) {
Tv t = new CapTv();
//조상 클래스 Tv의 참조변수가 CapTv의 인스턴스를 참조
//그러나 반대로 자식 클래스의 참조변수가 부모 클래스의 인스턴스를
//참조할 수는 X!!
// CapTv tv = new Tv() 절대 안돼!!
}
자손 타입의 참조변수가 조상 타입의 인스턴스 참조가 불가
하다!참조변수의 형변환
은 서로 상속관계에 있는 클래스 사이에서만 가능!Up-casting(자손->조상) : 형변환 생략가능
Down-casting(조상->자손) : 형변환 생략불가
(여기서 왜 형변환이 생략 불가 하냐면 참조변수가 다룰 수 있는 멤버변수의 개수가 늘어나기 때문에 문제가 발생할 가능성이 존재하므로)
나름 야물딱지게 정리한 casting 이걸 읽는게 더 이해 잘될듯
class Car {
String color;
int door;
void drive() {
System.out.println("drive, brrr~");
}
void stop() {
System.out.println("stop!!!");
}
}
class FireEngine extends Car {
void water() {
System.out.println("water!!!");
}
}
class CastingTest1 {
public static void main(String[] args) {
Car car = null;
//car이라는 참조변수를 선언
FireEngine fe = new FireEngine();
//FireEngine 인스턴스를 생성 -> 같은 타입의 참조변수가 참조
FireEngine fe2 = null;
//참조변수를 마찬가지로 null로 설정
fe.water();
//메서드 water가 호출되어 water가 출력된다
car = fe; //형변환이 생략
//car타입 참조변수가 자손 fe가 참조하는 인스턴스를 참조하도록
//fe가 참조하는 인스턴스의 주소가 car에 저장된다
//그러나 이는 Car타입이므로 자식 클래스의 메서드인 water는 호출 X
fe2 = (FireEngine) car; //조상 -> 자손이므로 형변환 표시
fe2.water();
}
}
//출력 결과
//water!!!
//water!!!
= for 참조변수가 참조하는 인스턴스의 실제 타입을 알아보려고!
class InstanceofTest {
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.");
}
}
= 조상 클래스에 선언된 변수와 같은 이름의 인스턴스 변수를 자손 클래스가 갖는다면, 조상 타입의 참조변수로 자손 인스턴스를 참조하는 경우
와 자손 타입의 참조변수로 자손 인스턴스를 참조하는 경우
는 다른 결과가 발생한다!
조상 타입의 참조변수 사용 = 조상 클래스에 선언된 멤버변수 사용
자손 타입의 참조변수 사용 = 자손 클래스에 선언된 멤버변수 사용
-> 멤버변수의 값은 참조하는 객체의 타입이 아니라 참조변수의 타입에 따라 결정되기 때문
(솔직히 말해서 나는 위의 문장을 한번에 받아들이기 어려웠는데, 예시를 보고 아~~ 바로 이해했다 ㅎㅎ..)
예시1. 이건 간단한 예시(참조변수의 타입과 인스턴스의 타입이 같음)
class Parent {
int value = 10;
void method() {
System.out.println("내장된 값은 "+this.value+"입니다);
}
}
class Child extends Parent {
int value = 20;
void method() {
System.out.println("힛 내장된 값은 "+this.value+"입니다");
}
}
class BindingTest {
Parent p = new Parent();
Child c = new Child();
System.out.println("parent: "+p.method);
//내장된 값은 10입니다
System.out.println("child: "+c.child);
//힛 내장된 값은 20입니다
}
예시2.
class Parent {
int value = 10;
void method() {
System.out.println("내장된 값은 "+this.value+"입니다);
}
}
class Child extends Parent {
int value = 20;
void method() {
System.out.println(super.value);
System.out.println("힛 내장된 값은 "+this.value+"입니다");
}
}
class BindingTest {
Parent p = new Child();
Child c = new Child();
System.out.println(p.value);
//10
System.out.println(c.value);
//20
System.out.println("parent: "+p.method);
//내장된 값은 10입니다
System.out.println("child: "+c.child);
//10
//힛 내장된 값은 20입니다
}
상속을 통해 다양한 형태의 자손 클래스들을 생성할 수 있다!
class Car {
String color = "red";
int money;
Car(int money) {
this.money = money;
}
void start() {
System.out.println("차가 출발합니당");
}
void stop() {
System.out.println("차가 도착했습니당");
}
}
class SportCar extends Car {
String speed = "매우 빠름";
SportCar() {
super(300);
}
}
class ForeignCar extends Car {
String speed = "그럭저럭";
ForeignCar() {
super(500);
}
}
class Buyer {
int money = 1000;
void buy(Car c) {
if(money < c.money) {
System.out.println("돈이 없어용 ㅠㅠ");
return;
}
money -= c.money;
System.out.println("남은 돈은 "+money+"입니당");
}
}
** 다른 예시로 PrintStream
클래스의 print
메서드도 다형성을 갖는데 알아두면 좋아용 (왜냐면 매개변수가 최상위 부모 클래스인 Object이므로 뭐가 들어가도 상관없으므로~)
타입이 부모 클래스인 참조 변수라면, 이 변수가 배열일 때 각기 다른 자식 클래스를 이 배열 안에 담을 수 있다.
Car[] c = new Car[3];
c[0] = new SportCar();
c[1] = new ForeignCar();
c[2] = new DomesticCar();
이때 배열은 크기를 미리 지정해줘야 하는데 얼마나 추가될 지 알 수 없다면, Vector
클래스를 활용해 크기를 동적으로 늘려준다.
class Buyer {
int money = 1000;
Vector cars = new Vector();
void buy(Car c) {
if(money < c.money) {
System.out.println("잔액이 부족해 물건을 살 수 없습니다.");
return;
}
money -= c.money;
cars.add(c);
System.out.println(c+"를 구입하셨습니다");
}
void refund(Car c) {
if(cars.remove(c)) {
money += c.money;
System.out.println(c+"를 환불하셨습니다");
} else {
System.out.println("구입하신 제품 중 해당 상품이 없습니다ㅠㅠ");
}
}
}
= 추상
: 공통된 성질을 뽑아 일반적인 개념으로 파악
= 추상화
: 공통된 성질을 찾아 조상을 만드는 작업 (<-> 구체화)
= 미완성 설계도
abstract
키워드를 통해 추상을 표현abstract
키워드가 사용되면 추상 클래스가 됨= 선언부만 남겨놓고 구현부는 작성하지 않은 것
{}
를 사용하지 않으므로 ;
를 통해 선언부를 마무리 지어야 함{}
가 있다는 자체만으로 일반 메서드로 취급받음)사실 구현부가 비어있는 일반 메서드로 구현하면 추상 메서드와 크게 다를 바 없지만, 그럼에도 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();
}
Unit[] group = new Unit[4];
group[0] = new Marine();
// 이때는 group[0].move(100, 200); 이 가능하나
Object[] group = new Object[4];
group[0] = new Marine();
// 이때는 group[0].move(100, 200); 이 불가능
//Object 클래스가 move라는 메서드를 갖지 않기 때문에!
= 일종의 추상 클래스라고도 볼 수 있으나, 추상화 정도가 추상클래스보다도 높다.
= 추상 클래스 = 미완성 설계도 | 인터페이스 = 밑그림
interface 이름 {
public static final 타입 이름 = 값;
public abstract 메서드이름
}
모든 멤버변수는 public static final로 상수여야 함
모든 메서드도 public abstract 여야 함
다중 상속
이 가능!!interface Movable {
void move(int x, int y);
}
interface Attackable {
void attack(Unit u);
}
interface Fightable extends Movable, Attackable {}
//얘는 자기가 갖고있는건 하나도 없지만 추상 메서드 두개를 가짐
implements
키워드를 사용인터페이스
는 밑그림이기 때문에 나머지 기능들을 구현
한다는 의미가 크고, 추상클래스
는 미완성되어 있으므로 나머지 기능들을 확장
한다는 의미가 강해 extends(확장)
키워드를 사용하게 된다)class 클래스명 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) {}
}
++ 인터페이스의 끝에 ~able
을 사용하는 경우가 많은데 이는 구현할 때 이런 기능을 구현할 수 있다 라는 가능의 의미를 나타내기 위함이라고 한다.
(소곤소곤) 또다른 객체지향 언어인 C++에서는 다중상속을 허용하는데, 자바는 클래스에서 다중상속이 불가능하기 때문에 인터페이스에서의 다중 상속을 허용한다고 말은 하지만 실상 인터페이스 다중상속은 그렇게 잘 활용되진 않는다.
(그래서 이 부분은 이렇게 쓰인다~~ 에 대한 이해만 하고 넘어가면 좋을듯)
그러나 두 개의 클래스에서 상속을 받아야 한다면 한쪽은 상속을 받고 하나는 필요한 부분만을 뽑아 인터페이스로 만든 후 구현한다.
public class VCR {
protected int counter;
public void play() {//재생}
public void stop() {//재생 멈춤}
public void reset() {counter = 0;}
public int getCounter() {return counter;}
public void setCounter(int c) {counter = c;}
}
public interface IVCR {
public void play();
public void stop();
public void reset();
public int getCounter();
public void setCounter(int c);
}
public class TVCR implements Tv extends IVCR {
VCR vcr = new VCR();
public void play() {vcr.play();}
public void stop() {vcr.stop();}
public void reset() {vcr.reset();}
public int getCounter() {vcr.getCounter();}
public void setCounter(int c) {vcr.setCounter(c);}
}
interface Parseable {
public abstract void parse(String fileName);
}
class ParserManager {
public static Parseable getParser(String type) {
if(type.equals("XML") return new XMLParser();
else {
Parseable p = new HTMLParser();
return p;
}
}
}
class XMLParser implements Parseable {
public void parse(String fileName) {
System.out.println(fileName+"- XML parsing complete");
}
}
class HTMLParser implements Parseable {
public void parse(String fileName) {
System.out.println(fileName+"- HTML parsing complete");
}
}
public static void main(String[] args) {
Parseable parser = ParserManager.getParser("XML");
parser.parse("document.xml");
parser = ParserManager.getParser("HTML");
parser.parse("document2.html");
}
// 출력
// document.xml - XML parsing completed.
// document2.html - HTML parsing completed.
- 개발시간을 단축시킬 수 있다
(일단 인터페이스가 작성되면, 이를 활용해 프로그램을 작성할 수 있음)- 표준화가 가능하다
(프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 뒤 구현하여 프로그램을 작성하도록 함)- 서로 관계없는 클래스들에게 관계를 맺어줄 수 있다
(하나의 인터페이스를 공통적으로 구현하도록 함으로써)- 독립적인 프로그래밍이 가능하다
(클래스의 선언과 구현을 분리시킬 수 있기 때문)
- 클래스를 사용하는 user와 클래스를 제공하는 provider가 있다.
- 메서드를 호출하는 user는 사용하려는 메서드의 선언부만 알면 된다.
이제 예를 들어보자. 클래스 A는 클래스 B의 인스턴스를 생성하고 메서드를 호출한다. 그러나 클래스 A가 클래스 B를 직접 호출하지 않고 인터페이스를 매개체로 인터페이스를 통해 클래스 B의 메서드에 접근하려고 하면, 클래스 B에 변경사항이 있거나 다른 기능의 다른 클래스로 대체 되어도 A는 전혀 영향을 받지 않을 수 있다!
이렇게 두 클래스간의 관계가 간접적
으로 변경되려면 인터페이스를 이용해 B의 선언부와 구현부를 분리해야 한다.
interface I {
public abstract void methodB();
}
class B implements I {
public void methodB() {
System.out.println("methodB in B class");
}
}
class A {
//아래의 메서드가 호출된다면 인터페이스 I를 구현한
//클래스의 인스턴스를 제공받아야 함
public void methodA(I i) {
i.methodB();
}
}
이렇게 되면 A-B간의 관계가 아닌 A-I-B간의 관계로 바뀌게 된다.
매개변수를 통해 동적으로 제공받을 수도 있지만 제 3의 클래스를 통해서 제공받을 수도 있다!
class InterfaceTest3 {
public static void main(String[] args) {
A a = new A();
a.methodA();
}
}
class A {
void methodA() {
//제 3의 클래스 메서드를 통해 인터페이스 I를 구현한 클래스의
//인스턴스를 얻어옴
I i = InstanceManager.getInstance();
i.methodB();
}
}
interface I {
public abstract void methodB();
}
class B implements I {
public void methodB {
System.out.println("methodB in B class");
}
}
class InstanceManager {
public static I getInstance() {
return new B();
}
}
자바의 정석, 2nd Edition.