상속
class Animal {
String name;
void setName(String name) {
this.name = name;
}
}
class Dog extends Animal { // Animal 클래스를 상속한다.
}
public class Sample {
public static void main(String[] args) {
Dog dog = new Dog();
dog.setName("poppy");
System.out.println(dog.name);
}
}
poppy
클래스 상속을 위해서는 extends라는 키워드를 사용한다. 이제 Dog 클래스는 Animal 클래스를 상속하게 되었다. Dog 클래스에 객체 변수인 name과 메서드인 setName을 만들지 않았지만 Animal 클래스를 상속했기 때문에 예제에서 보듯이 그대로 사용이 가능하다.
자식 클래스의 기능 확장하기
class Animal {
String name;
void setName(String name) {
this.name = name;
}
}
class Dog extends Animal {
void sleep() {
System.out.println(this.name+" zzz");
}
}
public class Sample {
public static void main(String[] args) {
Dog dog = new Dog();
dog.setName("poppy");
System.out.println(dog.name);
dog.sleep();
}
}
poppy
poppy zzz
sleep 메서드를 추가하여 이제 Dog 클래스는 Animal 클래스보다 좀 더 많은 기능을 가지게 되었다.
IS-A 관계란?
Dog 클래스는 Animal 클래스를 상속했다. 즉, Dog는 Animal의 하위 개념이라고 할 수 있다. 이런 경우 Dog는 Animal에 포함되기 때문에 ‘개(Dog)는 동물(Animal)이다’라고 표현할 수 있다. 자바는 이러한 관계를 IS-A 관계라고 표현한다. 즉, ‘Dog is a Animal(개는 동물이다)’과 같이 말할 수 있는 관계를 IS-A 관계라고 하는 것이다. 이렇게 IS-A 관계(상속 관계)에 있을 때 자식 클래스의 객체는 부모 클래스의 자료형인 것처럼 사용할 수 있다. 그래서 다음과 같은 코딩이 가능하다.
Animal dog = new Dog(); // Dog is a Animal
하지만 이 반대의 경우, 즉 부모 클래스로 만들어진 객체를 자식 클래스의 자료형으로는 사용할 수 없다. 그러므로 다음의 코드는 컴파일 오류가 발생한다.
Dog dog = new Animal(); // 컴파일 오류
Animal dog = new Dog(); // Dog is a Animal (O)
이 코드를 읽어 보면 ‘개(Dog)로 만든 객체는 동물(Animial) 자료형이다’라고 해석할 수 있다.
또, 다음 코드를 보자.
Dog dog = new Animal(); // Animal is a Dog (X)
역시 개념적으로 읽어 보면 ‘동물(Animal)로 만든 객체는 개(Dog) 자료형이다’로 해석할 수 있을 것이다. 근데 뭔가 좀 이상하지 않은가? Animal로 만든 객체는 ‘개(Dog)’ 자료형 말고 ‘호랑이(Tiger)’ 자료형 또는 ‘사자(Lion)’ 자료형도 될 수 있지 않은가? 즉, 개념적으로 살펴 보아도 두 번째 코드는 성립할 수 없다는 것을 알 수 있다.
점프 투 자바Object 클래스란?
자바에서 만드는 모든 클래스는 Object 클래스를 상속받는다. 사실 우리가 만든 Animal 클래스는 다음 코드와 기능적으로 완전히 동일하다. 하지만 굳이 다음 코드처럼 Object 클래스를 상속하도록 코딩하지 않아도 자바에서 만들어지는 모든 클래스는 Object 클래스를 자동으로 상속받게끔 되어 있다.
class Animal extends Object {
String name;
void setName(String name) {
this.name = name;
}
}
따라서 자바에서 만드는 모든 객체는 Object 자료형으로 사용할 수 있다. 다시 말해, 다음과 같이 코딩하는 것이 가능하다.
Object animal = new Animal(); // Animal is a Object
Object dog = new Dog(); // Dog is a Object
메서드 오버라이딩
이번에는 Dog 클래스를 좀 더 구체화시키는 HouseDog 클래스를 만들어 보자. HouseDog 클래스는 Dog 클래스를 상속하여 다음과 같이 만들 수 있다.
class Animal {
String name;
void setName(String name) {
this.name = name;
}
}
class Dog extends Animal {
void sleep() {
System.out.println(this.name+" zzz");
}
}
class HouseDog extends Dog {
}
public class Sample {
public static void main(String[] args) {
HouseDog houseDog = new HouseDog();
houseDog.setName("happy");
houseDog.sleep(); // happy zzz 출력
}
}
happy zzz
HouseDog 클래스를 실행해 보면 sleep 메서드가 호출되어 이와 같은 결과가 출력된다.
그런데 HouseDog 클래스로 만들어진 객체들을 sleep 메서드 호출 시 ‘happy zzz’가 아닌 ‘happy zzz in house’로 출력하고 싶다. 어떻게 해야 할까? 다음과 같이 HouseDog 클래스를 수정해 보자.
class Animal {
String name;
void setName(String name) {
this.name = name;
}
}
class Dog extends Animal {
void sleep() {
System.out.println(this.name + " zzz");
}
}
class HouseDog extends Dog {
void sleep() {
System.out.println(this.name + " zzz in house");
}
}
public class Sample {
public static void main(String[] args) {
HouseDog houseDog = new HouseDog();
houseDog.setName("happy");
houseDog.sleep(); // happy zzz in house 출력
}
}
happy zzz in house
Dog 클래스에 있는 sleep 메서드를 HouseDog 클래스에 다시 구현하여 이와 같이 원하던 결괏값을 얻을 수 있다.
HouseDog 클래스에 Dog 클래스와 동일한 형태(즉, 입출력이 동일)의 sleep 메서드를 구현하면 HouseDog 클래스의 sleep 메서드가 Dog 클래스의 sleep 메서드보다 우선순위를 갖게 되어 HouseDog 클래스의 sleep 메서드가 호출되게 된다.
이렇게 부모 클래스의 메서드를 자식 클래스가 동일한 형태로 또다시 구현하는 행위를 메서드 오버라이딩(method overriding, 풀이하자면 메서드 덮어쓰기라고 할 수 있다.)이라고 한다.
메서드 오버로딩
void sleep(int hour) {
System.out.println(this.name+" zzz in house for " + hour + " hours");
}
이미 sleep이라는 메서드가 있지만 동일한 이름의 sleep 메서드를 또 생성할 수 있다. 단, 메서드의 입력 항목이 다를 경우만 가능하다. 새로 만든 sleep 메서드는 입력 항목으로 hour라는 int 자료형이 추가되었다. 이렇듯 입력 항목이 다른 경우 동일한 이름의 메서드를 만들 수 있는데 이를 메서드 오버로딩(method overloading)이라고 부른다.
새로 만든 sleep 메서드를 확인하기 위해 main 메서드를 다음과 같이 변경하고 실행해 보자.
class Animal {
String name;
void setName(String name) {
this.name = name;
}
}
class Dog extends Animal {
void sleep() {
System.out.println(this.name + " zzz");
}
}
class HouseDog extends Dog {
void sleep() {
System.out.println(this.name + " zzz in house");
}
void sleep(int hour) {
System.out.println(this.name + " zzz in house for " + hour + " hours");
}
}
public class Sample {
public static void main(String[] args) {
HouseDog houseDog = new HouseDog();
houseDog.setName("happy");
houseDog.sleep(); // happy zzz in house 출력
houseDog.sleep(3); // happy zzz in house for 3 hours 출력
}
}
happy zzz in house
happy zzz in house for 3 hours
다중 상속이란?
다중 상속은 클래스가 동시에 하나 이상의 클래스를 상속받는 것을 뜻한다. C++, 파이썬 등 많은 언어들이 다중 상속을 지원하지만 자바는 다중 상속을 지원하지 않는다. 만약 자바가 다중 상속을 지원한다면 다음과 같은 코드가 만들어질 수 있을 것이다.
class A {
public void msg() {
System.out.println("A message");
}
}
class B {
public void msg() {
System.out.println("B message");
}
}
class C extends A, B {
public void static main(String[] args) {
C test = new C();
test.msg();
}
}
자바가 다중 상속을 지원한다고 가정하고 C 클래스가 A, B라는 클래스를 동시에 상속(extends A, B)하도록 했다. main 메서드에서 test.msg();를 실행할 때 A 클래스의 msg 메서드를 실행해야 할까? 아니면 B 클래스의 msg 메서드를 실행해야 할까? 다중 상속을 지원하게 되면 이렇듯 애매모호한 부분이 생기게 된다. 자바는 이러한 불명확한 부분을 애초에 없앤 언어이다.
생성자
class Animal {
String name;
void setName(String name) {
this.name = name;
}
}
class Dog extends Animal {
void sleep() {
System.out.println(this.name + " zzz");
}
}
class HouseDog extends Dog {
void sleep() {
System.out.println(this.name + " zzz in house");
}
void sleep(int hour) {
System.out.println(this.name + " zzz in house for " + hour + " hours");
}
}
public class Sample {
public static void main(String[] args) {
HouseDog houseDog = new HouseDog();
houseDog.setName("happy");
houseDog.sleep();
houseDog.sleep(3);
}
}
Animal, Dog, HouseDog 클래스들이 준비되었다면 이제 main 메서드를 다음과 같이 수정한 후 실행해 보자.
(... 생략 ...)
public class Sample {
public static void main(String[] args) {
HouseDog dog = new HouseDog();
System.out.println(dog.name);
}
}
null
dog 객체의 name 변수에 아무런 값도 설정하지 않았기 때문에 null이 출력될 것이다. 이렇듯 HouseDog 클래스는 코딩하기에 따라 객체 변수 name에 값을 설정할 수도 있고 설정하지 않을 수도 있다.
그렇다면 name이라는 객체 변수에 값을 무조건 설정해야만 객체가 생성될 수 있도록 강제할수 있는 방법은 없을까? 바로 생성자를 이용하면 된다. HouseDog 클래스에 다음과 같은 메서드를 추가해 보자.
(... 생략 ...)
class HouseDog extends Dog {
HouseDog(String name) {
this.setName(name);
}
void sleep() {
System.out.println(this.name + " zzz in house");
}
void sleep(int hour) {
System.out.println(this.name + " zzz in house for " + hour + " hours");
}
}
(... 생략 ...)
이와 같이 메서드명이 클래스명과 동일하고 리턴 자료형을 정의하지 않는 메서드를 생성자라고 한다. 생성자 규칙은 다음과 같다.
- 클래스명과 메서드명이 같다.
- 리턴 타입을 정의하지 않는다(void도 사용하지 않는다.).
new 클래스명(입력인수, ...)
생성자는 메서드와 마찬가지로 다양한 입력을 받을 수 있다. 우리가 HouseDog 클래스에 만든 생성자는 다음과 같이 입력값으로 문자열을 필요로 하는 생성자이다.
HouseDog(String name) {
this.setName(name);
}
따라서 다음과 같이 new 키워드로 객체를 만들 때 문자열을 전달해야만 한다.
HouseDog dog = new HouseDog("happy"); // 생성자 호출 시 문자열을 전달해야 한다.
만약 다음처럼 코딩하면 컴파일 오류가 발생할 것이다.
HouseDog dog = new HouseDog();
오류가 발생하는 이유는 객체 생성 방법이 생성자의 규칙과 맞지 않기 때문이다. 생성자가 선언된 경우 생성자의 규칙대로만 객체를 생성할 수 있다.
(... 생략 ...)
public class Sample {
public static void main(String[] args) {
HouseDog dog = new HouseDog("happy");
System.out.println(dog.name);
}
}
happy
main 메서드를 실행하면 생성자에 의해 name 객체 변수에 값이 설정되어 이러한 결과가 출력된다.
이렇듯 생성자를 사용하면 setName("happy")와 같은 필수적인 행동을 객체 생성 시에 제어할 수 있다.
디폴트 생성자
class Dog extends Animal {
void sleep() {
System.out.println(this.name + " zzz");
}
}
class Dog extends Animal {
Dog() {
}
void sleep() {
System.out.println(this.name + " zzz");
}
}
첫 번째 코드와 두 번째 코드의 차이점은 무엇일까? 두 번째 코드에는 생성자가 구현되어 있다. 생성자의 입력 항목이 없고 생성자 내부에 아무 내용이 없는 이와 같은 생성자를 디폴트 생성자라고 부른다. 디폴트 생성자를 구현하면 new Dog()로 Dog 클래스의 객체가 만들어질때 디폴트 생성자 Dog()가 실행될 것이다.
만약 클래스에 생성자가 하나도 없다면 컴파일러는 자동으로 이와 같은 디폴트 생성자를 추가한다. 하지만 사용자가 작성한 생성자가 하나라도 구현되어 있다면 컴파일러는 디폴트 생성자를 추가하지 않는다.
생성자 오버로딩
class Animal {
String name;
void setName(String name) {
this.name = name;
}
}
class Dog extends Animal {
void sleep() {
System.out.println(this.name + " zzz");
}
}
class HouseDog extends Dog {
HouseDog(String name) {
this.setName(name);
}
HouseDog(int type) {
if (type == 1) {
this.setName("yorkshire");
} else if (type == 2) {
this.setName("bulldog");
}
}
void sleep() {
System.out.println(this.name + " zzz in house");
}
void sleep(int hour) {
System.out.println(this.name + " zzz in house for " + hour + " hours");
}
}
public class Sample {
public static void main(String[] args) {
HouseDog happy = new HouseDog("happy");
HouseDog yorkshire = new HouseDog(1);
System.out.println(happy.name); // happy 출력
System.out.println(yorkshire.name); // yorkshire 출력
}
}
happy
yorkshire
HouseDog 클래스는 두 개의 생성자가 있다. 하나는 String 자료형을 입력으로 받는 생성자이고 다른 하나는 int 자료형을 입력으로 받는 생성자이다. 두 생성자의 차이는 입력 항목이다. 이렇게 입력 항목이 다른 생성자를 여러 개 만들 수 있는데 이런 것을 생성자 오버로딩(constructor overloading)이라고 한다.
HouseDog happy = new HouseDog("happy"); // 문자열로 생성
HouseDog yorkshire = new HouseDog(1); // 숫자값으로 생성
인터페이스
인터페이스(interface)는 초보 개발자를 괴롭히는 단골손님이다. 인터페이스에 대한 개념 없이 코드로만 이해하려고 하면 곧 미궁에 빠지게 된다. 이렇게 이해하기 힘든 인터페이스는 도대체 왜 필요할까? 새로운 예제를 통해 인터페이스를 차근차근 알아보자.
인터페이스는 왜 필요한가?
난 동물원(zoo)의 사육사(zookeeper)이다.
육식동물(predator)이 들어오면 난 먹이를 던져준다(feed).
호랑이(tiger)가 오면 사과(apple)를 던져준다.
사자(lion)가 오면 바나나(banana)를 던져준다.
class Animal {
String name;
void setName(String name) {
this.name = name;
}
}
class Tiger extends Animal {
}
class Lion extends Animal {
}
class ZooKeeper {
void feed(Tiger tiger) { // 호랑이가 오면 사과를 던져 준다.
System.out.println("feed apple");
}
void feed(Lion lion) { // 사자가 오면 바나나를 던져준다.
System.out.println("feed banana");
}
}
public class Sample {
public static void main(String[] args) {
ZooKeeper zooKeeper = new ZooKeeper();
Tiger tiger = new Tiger();
Lion lion = new Lion();
zooKeeper.feed(tiger); // feed apple 출력
zooKeeper.feed(lion); // feed banana 출력
}
}
ZooKeeper 클래스는 tiger가 왔을 때, lion이 왔을 때, 각각 다른 feed 메서드가 호출된다.
프로그램을 실행하면 다음과 같은 결과가 출력된다.
feed apple
feed banana
만약 Tiger와 Lion뿐이라면 ZooKeeper 클래스는 더 이상 할 일이 없겠지만 Crocodile, Leopard 등이 계속 추가된다면 ZooKeeper는 클래스가 추가될 때마다 매번 다음과 같은 feed 메서드를 추가해야 한다.
(... 생략 ...)
class ZooKeeper {
void feed(Tiger tiger) {
System.out.println("feed apple");
}
void feed(Lion lion) {
System.out.println("feed banana");
}
void feed(Crocodile crocodile) {
System.out.println("feed strawberry");
}
void feed(Leopard leopard) {
System.out.println("feed orange");
}
}
(... 생략 ...)
인터페이스 작성하기
interface Predator {
}
class Animal {
String name;
void setName(String name) {
this.name = name;
}
}
(... 생략 ...)
이 코드와 같이 인터페이스는 class가 아닌 interface 키워드로 작성한다.
그리고 Tiger, Lion 클래스는 작성한 인터페이스를 구현하도록 다음과 같이 implements라는 키워드를 사용해 수정하자.
(... 생략 ...)
class Tiger extends Animal implements Predator {
}
class Lion extends Animal implements Predator {
}
(... 생략 ...)
이렇게 Tiger, Lion 클래스가 Predator 인터페이스를 구현하게 되면 ZooKeeper 클래스의 feed 메서드를 다음과 같이 변경할 수 있다.
feed 메서드의 입력으로 Tiger, Lion을 각각 필요로 했지만 이제 이것을 Predator라는 인터페이스로 대체할 수 있게 되었다. tiger, lion은 각각 Tiger, Lion의 객체이기도 하지만 Predator 인터페이스의 객체이기도 하기 때문에 이와 같이 Predator를 자료형으로 사용할 수 있는 것이다. 05-5절에서 공부했던 IS-A 관계가 인터페이스에도 적용된다. 즉, ‘Tiger is a Predator’, ‘Lion is a Predator’가 성립된다.
- tiger: Tiger 클래스의 객체이자 Predator 인터페이스의 객체
- lion: Lion 클래스의 객체이자 Predator 인터페이스의 객체
여기서 말하는 육식동물 클래스는 Crocodile이나 Leopard와 같이 육식 동물들의 이름을 한 클래스들을 말한다.
class Crocodile extends Animal implements Predator {
}
Crocodile 클래스는 실제 코드에 적용하지 말고 눈으로만 살펴보자.
이제 왜 인터페이스가 필요한지 감을 잡았을 것이다. 보통 중요 클래스(ZooKeeper)를 작성하는 시점에서는 클래스(Animal)의 구현체(Tiger, Lion)가 몇 개가 될지 알 수 없으므로 인터페이스(Predator)를 정의하여 인터페이스를 기준으로 메서드(feed)를 만드는 것이 효율적이다.
인터페이스의 메서드
그런데 앞서 살펴본 ZooKeeper 클래스에 약간의 문제가 발생했다. ZooKeeper 클래스의 feed 메서드를 보면 tiger가 오든지, lion이 오든지 무조건 feed apple이라는 문자열을 출력한다. tiger가 오면 feed apple을 출력하는 것이 맞지만 lion이 오면 feed banana를 출력해야 한다.
(... 생략 ...)
class ZooKeeper {
public void feed(Predator predator) {
System.out.println("feed apple"); // 항상 feed apple 만을 출력한다.
}
}
(... 생략 ...)
feed apple
feed apple
이번에도 인터페이스의 마법을 부려 보자. Predator 인터페이스에 다음과 같은 getFood 메서드를 추가해 보자.
interface Predator {
String getFood();
}
(... 생략 ...)
그런데 좀 이상하다. 메서드에 몸통이 없다?!
인터페이스의 메서드는 메서드의 이름과 입출력에 대한 정의만 있고 그 내용은 없다. 그 이유는 인터페이스는 ‘규칙’이기 때문이다. 즉, getFood 메서드는 인터페이스를 implements한 클래스들이 강제적으로 구현해야 하는 규칙이 된다.
이제 Predator 인터페이스에 getFood메서드를 추가하면 Tiger, Lion 등의 Predator 인터페이스를 구현한 클래스에서 컴파일 오류가 발생할 것이다. 오류를 해결하려면 다음처럼 Tiger, Lion 클래스에 getFood 메서드를 구현해야 한다.
(... 생략 ...)
class Tiger extends Animal implements Predator {
public String getFood() {
return "apple";
}
}
class Lion extends Animal implements Predator {
public String getFood() {
return "banana";
}
}
(... 생략 ...)
Tiger, Lion 클래스의 getFood 메서드는 각각 apple과 banana를 리턴하게 했다.
이어서 ZooKeeper 클래스도 다음과 같이 변경이 가능하다.
(... 생략 ...)
class ZooKeeper {
void feed(Predator predator) {
System.out.println("feed "+predator.getFood());
}
}
(... 생략 ...)
feed 메서드가 feed apple 대신 "feed "+predator.getFood()를 출력하도록 코드를 수정하였다. predator.getFood()를 호출하면 Predator 인터페이스를 구현한 구현체(Tiger, Lion)의 getFood() 메서드가 호출된다.
이제 프로그램을 실행해 보자. 원하던 대로 다음과 같은 결괏값이 출력되는 것을 확인할 수 있다.
feed apple
feed banana
인터페이스 더 파고들기
여기서는 왜 인터페이스가 필요한지를 이해하고 넘어가는 것이 가장 중요하다. 동물(Tiger, Lion, Crocodile 등) 클래스의 종류만큼 feed 메서드가 필요했던 ZooKeeper 클래스를 Predator 인터페이스를 이용하여 구현했더니 단 한 개의 feed 메서드로 구현이 가능해졌다. 여기서 중요한 점은 메서드의 개수가 줄어들었다는 점이 아니라 ZooKeeper 클래스가 동물 클래스에 의존적인 클래스에서 동물 클래스와 상관없는 독립적인 클래스가 되었다는 점이다. 바로 이 점이 인터페이스의 핵심이다.
물리적 세계자바 세계
컴퓨터 | ZooKeeper |
USB 포트 | Predator |
하드디스크, 메모리 스틱, 스마트폰, ... | Tiger, Lion, Crocodile, ... |
디폴트 메서드
자바 8 버전 이후부터는 디폴트 메서드(default method)를 사용할 수 있다. 인터페이스의 메서드는 구현체를 가질 수 없지만 디폴트 메서드를 사용하면 실제 구현된 형태의 메서드를 가질 수 있다. 예를 들어 Predator 인터페이스에 다음과 같은 디폴트 메서드를 추가할 수 있다.
interface Predator {
String getFood();
default void printFood() {
System.out.printf("my food is %s\n", getFood());
}
}
디폴트 메서드는 메서드명 가장 앞에 default라고 표기해야 한다. 이렇게 Predator 인터페이스에 printFood 디폴트 메서드를 구현하면 Predator 인터페이스를 구현한 Tiger, Lion 등의 실제 클래스는 printFood 메서드를 구현하지 않아도 사용할 수 있다. 그리고 디폴트 메서드는 오버라이딩이 가능하다. 즉, printFood 메서드를 실제 클래스에서 다르게 구현하여 사용할 수 있다.
다향성
객체 지향 프로그래밍의 특징 중에는 다형성(polymorphism)이 있다.
interface Predator {
(... 생략 ...)
}
class Animal {
(... 생략 ...)
}
class Tiger extends Animal implements Predator {
(... 생략 ...)
}
class Lion extends Animal implements Predator {
(... 생략 ...)
}
class ZooKeeper {
(... 생략 ...)
}
class Bouncer {
void barkAnimal(Animal animal) {
if (animal instanceof Tiger) {
System.out.println("어흥");
} else if (animal instanceof Lion) {
System.out.println("으르렁");
}
}
}
public class Sample {
public static void main(String[] args) {
Tiger tiger = new Tiger();
Lion lion = new Lion();
Bouncer bouncer= new Bouncer();
bouncer.barkAnimal(tiger);
bouncer.barkAnimal(lion);
}
}
어흥
으르렁
instanceof는 어떤 객체가 특정 클래스의 객체인지를 조사할 때 사용되는 자바의 내장 명령어이다. 여기서 animal instanceof Tiger는 ‘animal 객체는 Tiger 클래스로 만들어진 객체인가?’를 묻는 조건문이고, 조건이 참이라면 ‘어흥’을 출력하게 되는 것이다.
Animal tiger = new Tiger(); // Tiger is a Animal
Animal lion = new Lion(); // Lion is a Animal
여러분은 이 Bouncer 클래스의 barkAnimal 메서드가 마음에 드는가? 만약 Crocodile, Leopard 등의 클래스가 추가된다면 barkAnimal 메서드는 다음처럼 수정되어야 한다.
Tiger tiger = new Tiger(); // Tiger is a Tiger
Animal animal = new Tiger(); // Tiger is a Animal
Predator predator = new Tiger(); // Tiger is a Predator
Barkable barkable = new Tiger(); // Tiger is a Barkable
여기서 중요한 점은 Predator로 선언된 predator 객체와 Barkable로 선언된 barkable 객체는 사용할 수 있는 메서드가 서로 다르다는 점이다. predator 객체는 getFood() 메서드가 선언된 Predator 인터페이스의 객체이므로 getFood 메서드만 호출이 가능하다. 이와 마찬가지로 Barkable로 선언된 barkable 객체는 bark 메서드만 호출이 가능하다.
interface Predator {
(... 생략 ...)
}
interface Barkable {
void bark();
}
interface BarkablePredator extends Predator, Barkable {
}
(... 생략 ...)
기존의 인터페이스를 상속하여 BarkablePredator를 만들었다. 이와 같이 하면 BarkablePredator는 Predator의 getFood 메서드, Barkable의 bark 메서드를 그대로 사용할 수 있다. 지금까지 봐왔던 것처럼 인터페이스는 일반 클래스와는 달리 extends를 이용하여 여러 개의 인터페이스(여기서는 Predator, Barkable)를 동시에 상속할 수 있다. 즉, 다중 상속이 지원된다.
이번에는 Lion 클래스를 앞서 작성한 BarkablePredator 인터페이스를 구현하도록 수정해 보자.
(... 생략 ...)
class Lion extends Animal implements BarkablePredator {
public String getFood() {
return "banana";
}
public void bark() {
System.out.println("으르렁");
}
}
(... 생략 ...)
어흥
으르렁
이렇게 Lion 클래스를 수정하고 Bouncer 클래스를 실행하더라도 이와 같이 동일한 결괏값이 출력되는 것을 확인할 수 있다.
Bouncer 클래스의 barkAnimal 메서드의 입력 자료형이 Barkable이더라도 BarkablePredator를 구현한 lion 객체를 전달할 수 있다. 그 이유는 BarkablePredator는 Barkable 인터페이스를 상속받은 자식 인터페이스이기 때문이다. 자식 인터페이스로 생성한 객체의 자료형은 부모 인터페이스로 사용하는 것이 가능하다. 자식 클래스의 객체 자료형을 부모 클래스의 자료형으로 사용 가능하다는 점과 동일하다.
다음은 지금까지 진행했던 Sample.java의 전체 코드이다.
interface Predator {
String getFood();
default void printFood() {
System.out.printf("my food is %s\n", getFood());
}
}
interface Barkable {
void bark();
}
interface BarkablePredator extends Predator, Barkable {
}
class Animal {
String name;
void setName(String name) {
this.name = name;
}
}
class Tiger extends Animal implements Predator, Barkable {
public String getFood() {
return "apple";
}
public void bark() {
System.out.println("어흥");
}
}
class Lion extends Animal implements BarkablePredator {
public String getFood() {
return "banana";
}
public void bark() {
System.out.println("으르렁");
}
}
class ZooKeeper {
void feed(Predator predator) {
System.out.println("feed " + predator.getFood());
}
}
class Bouncer {
void barkAnimal(Barkable animal) {
animal.bark();
}
}
public class Sample {
public static void main(String[] args) {
Tiger tiger = new Tiger();
Lion lion = new Lion();
Bouncer bouncer = new Bouncer();
bouncer.barkAnimal(tiger);
bouncer.barkAnimal(lion);
}
}
추상 클래스
추상 클래스(abstract class)는 인터페이스의 역할도 하면서 클래스의 기능도 가지고 있는 자바의 ‘돌연변이’ 같은 클래스이다. 어떤 사람은 ‘추상 클래스는 인터페이스로 대체하는 것이 좋은 디자인’이라고도 얘기한다. 이러한 추상 클래스를 알아보기 위해 우리가 작성했던 Predator 인터페이스를 다음과 같이 추상 클래스로 변경해 보자.
abstract class Predator extends Animal {
abstract String getFood();
default void printFood() { // default 를 제거한다.
System.out.printf("my food is %s\n", getFood());
}
}
(... 생략 ...)
추상 클래스를 만들려면 class 앞에 abstract를 표기해야 한다. 또한 인터페이스의 메서드와 같은 역할을 하는 메서드(여기서는 getFood 메서드)에도 역시 abstract를 붙여야 한다. abstract 메서드는 인터페이스의 메서드와 마찬가지로 구현체가 없다. 즉, abstract 클래스를 상속하는 클래스에서 해당 abstract 메서드를 구현해야만 한다. 그리고 Animal 클래스의 기능을 유지하기 위해 Animal 클래스를 상속했다.
인터페이스의 디폴트 메서드는 더 이상 사용할 수 없으므로 default 키워드를 삭제하여 일반 메서드로 변경했다.
Predator 인터페이스를 이와 같이 추상 클래스로 변경하면 Predator 인터페이스를 상속했던 BarkablePredator 인터페이스는 더 이상 사용이 불가능하므로, 다음과 같이 삭제해야 한다. 그리고 Tiger, Lion 클래스도 Animal 클래스 대신 Predator 추상 클래스를 상속하도록 변경해야 한다.
abstract class Predator extends Animal {
(... 생략 ...)
}
interface Barkable {
(... 생략 ...)
}
interface BarkablePredator extends Predator, Barkable {
}
class Animal {
(... 생략 ...)
}
class Tiger extends Predator implements Barkable {
(... 생략 ...)
}
class Lion extends Predator implements Barkable {
(... 생략 ...)
}
class ZooKeeper {
(... 생략 ...)
}
class Bouncer {
(... 생략 ...)
}
public class Sample {
(... 생략 ...)
}
Predator 추상 클래스에 선언된 getFood 메서드는 Tiger, Lion 클래스에 이미 구현되어 있으므로 추가로 구현할 필요는 없다. 추상 클래스에 abstract로 선언된 메서드는 인터페이스의 메서드와 마찬가지로 반드시 구현해야 한다. 추상 클래스에는 abstract 메서드 외에 실제 메서드도 사용할 수 있다. 추상 클래스에 실제 메서드를 추가하면 Tiger, Lion 등으로 만들어진 객체에서 그 메서드들을 모두 사용할 수 있게 된다. 원래 인터페이스에서 default 메서드로 사용했던 printFood가 추상 클래스의 실제 메서드에 해당된다.
객관식 문제
- 자바에서 한 클래스가 다른 클래스를 상속받을 때 사용하는 키워드는 무엇인가요? (정답:b)
- a) super
- b) extends
- c) inherits
- d) this
- 상속을 통해 자식 클래스가 부모 클래스에서 얻을 수 있는 이점은 무엇인가요? (정답:a)
- a) 코드 재사용성
- b) 객체 간의 독립성 강화
- c) 코드 복잡도 증가
- d) 상속된 필드의 자동 초기화
- 다중 상속이 허용되지 않는 이유 중 하나는 무엇인가요? (정답:c)
- a) 코드의 재사용성
- b) 메모리 절약
- c) 다이아몬드 문제로 인한 모호성
- d) 컴파일 시간 단축
- 상속된 메서드를 자식 클래스에서 재정의하려면 어떤 키워드를 사용하나요? (정답:d)
- a) super
- b) abstract
- c) override
- d) 없음, 자동으로 재정의된다.
- 자바에서 모든 클래스의 상위 클래스는 무엇인가요? (정답:a)
- a) Object
- b) Base
- c) Class
- d) Parent
- 생성자의 기본 역할은 무엇인가요? (정답:b)
- a) 클래스의 메서드를 초기화한다
- b) 클래스의 객체를 초기화한다
- c) 클래스의 인터페이스를 정의한다
- d) 클래스의 속성을 변경한다
- 생성자는 언제 호출되나요? (정답:b)
- a) 클래스가 선언될 때
- b) 객체가 생성될 때
- c) 프로그램이 종료될 때
- d) 메서드가 호출될 때
- 생성자를 오버로딩할 때 중요한 조건은 무엇인가요? (정답:b)
- a) 같은 매개변수를 사용해야 한다
- b) 서로 다른 매개변수를 가져야 한다
- c) 이름이 달라야 한다
- d) 반환 타입이 있어야 한다
- 인터페이스의 특징으로 옳은 것은 무엇인가요? (정답:b)
- a) 필드와 메서드를 포함한다
- b) 메서드의 구현을 포함할 수 없다
- c) 객체를 생성할 수 있다
- d) final 키워드로 선언해야 한다
- 인터페이스의 메서드 구현을 클래스에서 강제하기 위한 키워드는 무엇인가요? (정답:a)
- a) implements
- b) extends
- c) interface
- d) super
- 다형성의 주요 특징은 무엇인가요? (정답:c)
- a) 여러 형태로 객체를 생성할 수 있다
- b) 여러 클래스를 상속할 수 있다
- c) 동일한 인터페이스로 다양한 구현을 사용할 수 있다
- d) 모든 필드가 자동으로 초기화된다
- 오버라이딩과 오버로딩의 차이점으로 올바른 것은 무엇인가요? (정답:c)
- a) 오버라이딩은 메서드 이름이 다르다
- b) 오버로딩은 반환 타입이 다르다
- c) 오버라이딩은 상속 관계에서 이루어진다
- d) 오버로딩은 상속 관계에서 이루어진다
- 추상 클래스의 주요 특징으로 옳은 것은 무엇인가요? (정답:c)
- a) 객체 생성이 가능하다
- b) 모든 메서드를 추상 메서드로 선언해야 한다
- c) 인스턴스를 만들 수 없다
- d) static 메서드를 가질 수 없다
- 추상 클래스와 인터페이스의 공통점은 무엇인가요? (정답:a)
- a) 둘 다 객체를 생성할 수 없다
- b) 둘 다 모든 메서드를 구현해야 한다
- c) 둘 다 필드를 가질 수 없다
- d) 둘 다 final로 선언해야 한다
- 추상 메서드는 어떤 키워드를 사용하여 선언하나요? (정답:c)
- a) static
- b) void
- c) abstract
- d) final
서술형 문제
- 추상 클래스와 일반 클래스의 차이점을 설명하세요.
추상 클래스 하나 이상의 추상 메서드를 포함할 수 있고 일반 클래스는 메서드를 포함할 수 없다.
- 추상 클래스와 인터페이스를 함께 사용하는 이유와 예를 설명하세요.
인터페이스는 클래스가 가져야 할 기능을 정의하며 추상 클래스는 기본적인 동작을 제공하면서 확장을 허용한다.
- 추상 클래스를 상속받은 자식 클래스에서 추상 메서드를 구현해야 하는 이유를 설명하세요.
추상 메서드는 선언만 되어 있고, 구현이 없기 때문에 자식 클래스에서 구체적인 동작을 정의해야 한다.
- 추상 클래스에서 선언된 일반 메서드와 추상 메서드의 차이점을 설명하세요.
추상 상속받는 모든 클래스에서 사용할 수 있고 일반 메서드는 선언만 되어 있으며, 자식 클래스에서 반드시 구현해야 한다.
- 상속과 구성(Composition)의 차이점과 각각의 장단점을 비교하세요.
상속은 코드의 재사용, 변화에 대한 유연성 등 여러 장점들을 생각하고 사용하고 구성은 결합도가 낮아지며 유연성을 높인다.
- 생성자에서 this와 super 키워드의 차이점에 대해 설명하세요.
this는 현재 객체를 참조하는 키워드이고 super는 상위 클래스의 인스턴스를 참조하는 키워드이다
- 자바에서 인터페이스와 추상 클래스의 차이점을 설명하세요.
추상 클래스는 하나 이상의 추상 메서드를 포함하는 클래스이고 추상 메서드는 선언만 있고 구현은 없는 메서드이다. 인터페이스는 상수와 추상 메서드로 구성된 집합체이고 인터페이스는 추상 클래스와 유사하지만 추상 클래스와 같이 구현된 메서드와 필드를 가질 수가 없다.
- 다형성의 개념과 객체지향 프로그래밍에서의 중요성을 설명하세요.
다형성은 한 가지 형태나 모양이 여러 가지 다른 동작을 할 수 있는 능력을 의미한다..객체지향 프로그래밍에서 다형성은 코드의 유연성과 확장성을 높이며, 동일한 인터페이스를 통해 서로 다른 구현을 쉽게 대체할 수 있게 해준다.
- 메서드 오버로딩과 오버라이딩의 차이점을 비교하세요.
오버로딩메서드는 오버로딩은 같은 이름의 메서드를 여러 개 가지면서 매개변수의 유형과 개수를 다르게 함으로써 구현된다. 오버라이딩은 상속받은 자식 클래스가 부모 클래스의 메서드를 자신의 상황에 맞게 변경해서 사용하는 것을 의미한다.
- 동적 바인딩의 개념을 설명하고 다형성과의 관계를 설명하세요.
동적 바인딩은 메서드 호출이 컴파일 시점이 아닌 런타임 시점에 실제 객체의 타입에 따라 연결되는 것을 의미한다.
코딩 테스트 문제
[1~7] 아래 코드를 실행했을 때 출력되는 결과를 적으시오.
```java
class Animal {
String sound = "generic sound";
}
class Dog extends Animal {
String sound = "bark";
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
System.out.println(a.sound);
}
}
```
결과: generic sound
```java
class Parent {
void show() {
System.out.println("Parent");
}
}
class Child extends Parent {
void show() {
System.out.println("Child");
}
}
public class Main {
public static void main(String[] args) {
Parent p = new Child();
p.show();
}
}
```
결과: Child
```java
class Parent {
int x = 10;
}
class Child extends Parent {
int x = 20;
}
public class Main {
public static void main(String[] args) {
Parent p = new Child();
System.out.println(p.x);
}
}
```
결과: 10
```java
class A {
A() {
System.out.println("A's constructor");
}
}
class B extends A {
B() {
System.out.println("B's constructor");
}
}
public class Main {
public static void main(String[] args) {
B obj = new B();
}
}
```
결과: A's constructor B's constructor
```java
class A {
A() {
System.out.println("A's no-arg constructor");
}
}
class B extends A {
B() {
System.out.println("B's no-arg constructor");
}
}
public class Main {
public static void main(String[] args) {
A a = new B();
}
}
```
결과: A's no-arg constructor B's no-arg constructor
```java
interface A {
void display();
}
interface B extends A {
void display();
}
class C implements B {
public void display() {
System.out.println("C's display");
}
}
public class Main {
public static void main(String[] args) {
A obj = new C();
obj.display();
}
}
```
결과: C's display
```java
interface A {
default void show() {
System.out.println("A");
}
}
class B implements A {
public void show() {
System.out.println("B");
}
}
public class Main {
public static void main(String[] args) {
A obj = new B();
obj.show();
}
}
```
결과: B
'WINK-(Web & App) > JAVA 스터디' 카테고리의 다른 글
[2024-2 Java 스터디] 김재승 #4주차 (2) | 2024.11.11 |
---|---|
Java[2024-2 Java 스터디] 이민형 #4주차 (5-5장) (0) | 2024.11.07 |
[2024-2 Java 스터디] 이서영 #3주차 (1) | 2024.11.07 |
[2024-2 Java 스터디] 이가인 #4주차 (0) | 2024.11.07 |
[2024-2 Java 스터디] 정채은 #4주차 (3) | 2024.11.07 |