안녕하세요? Justkode 입니다. 오늘은 상속, 다형성, 인터페이스에 대해서 알아 보는 시간을 가져 보도록 하겠습니다.
먼저 상속에 대해서 알아 보겠습니다. 상속은 기존의 클래스를 재사용하여 새로운 클래스를 작성 하는 것입니다. 이를 통해 우리는 적은 양의 코드를 이용하여, 새로운 클래스를 작성 할 수 있고, 공통의 수정 사항이 있을 때 편하게 수정 할 수 있다는 장점이 있습니다. 즉, 유지 보수 측면에서 이득을 볼 수 있다는 것입니다.
어떤 것들이 상속이 될 수 있을 까요? RPG 게임을 예시로 들어 보면, Player
클래스가 있다고 가정 하겠습니다. 하지만, Player
마다, 직업을 가질 수도 있겠죠? 그러면, Player
클래스를 상속 받는 Warrior
클래스, Magician
클래스 등등이 또 생길 수도 있겠네요.
자, 그러면 한 번 위의 사례를 예시로 들어 볼까요? 저번까지 사용 했던, Player
클래스를 사용 하겠습니다. 해당 클래스에 추상 메서드인 void attackPlayer(Player p)
를 추가 하겠습니다.
Player.java
abstract public class Player {
private String userName;
private int healthPoint;
private int attackPoint;
private final int USER_ID;
static int objectNum;
static {
objectNum = 0;
}
{
USER_ID = objectNum++;
}
Player(String userName, int healthPoint, int attackPoint) {
setUserName(userName);
setHealthPoint(healthPoint);
setAttackPoint(attackPoint);
printUserInfo();
}
Player(Player p) {
this(p.userName, p.healthPoint, p.attackPoint);
}
public String getUserName() {return this.userName;};
public int getHealthPoint() {return this.healthPoint;};
public int getAttackPoint() {return this.attackPoint;};
public void setAttackPoint(int attackPoint) {
if (attackPoint < 0)
return;
this.attackPoint = attackPoint;
}
public void setHealthPoint(int healthPoint) {
if (healthPoint < 0)
return;
this.healthPoint = healthPoint;
}
public void setUserName(String userName) {
if (userName.length() < 2)
return;
this.userName = userName;
}
public void printUserInfo() {
System.out.println("========================");
System.out.println("플레이어 이름: " + this.userName);
System.out.println("체력: " + this.healthPoint);
System.out.println("공격력: " + this.attackPoint);
System.out.println("유저 아이디: " + this.USER_ID);
System.out.println("========================");
}
abstract public void attackPlayer(Player p);
public static void printCurrentObjectNum() {
System.out.println("현재 생성된 객체의 갯수는 " + objectNum + "개 입니다.");
}
}
그 다음으로 Player
를 상속 받은, Magician
, Warrior
클래스를 만들어 보겠습니다. 클래스는 class 클래스명 extends 상속받을클래스명
을 이용하여, 부모 클래스를 상속 받을 수 있습니다.
우리는 자식 클래스에서, 생성자와 추상 메서드를 오버라이딩하여 구현 한 다음에 자식 클래스 객체를 만들 수 있습니다.
super()
를 이용 하여, 부모 클래스의 생성자를 사용 할 수 있습니다. 그 전에 super
에 대해서 알아 놓을 필요가 있는데, super
는 자식 클래스에서 부모 클래스로 부터 상속받은 멤버를 참조하는데 사용되는 참조 변수 입니다. 부모 클래스의 멤버 변수도 this
를 이용하여 접근 가능 하지만, 부모 클래스와 자식 클래스의 멤버 변수 이름이 같으면, this
와 super
를 이용해 구분 합니다.
Warrior(String userName, int healthPoint, int attackPoint) {
super(userName, healthPoint, attackPoint);
System.out.println("직업 전사");
System.out.println("========================");
}
오버라이딩은 부모 클래스로부터 상속받은 메서드의 내용을 자식 클래스에서 용도에 맞게 변경하는 것 입니다.
오버라이딩이 성립하기 위해서는 다음과 같은 조건들을 만족 하여야 합니다.
오버 라이딩은 그저 자식 메서드에서 작성만 해 주면 됩니다.
@Override
public void attackPlayer(Player p) {
System.out.println("========================");
System.out.printf("Player %s가 Player %s에 강력한 물리 공격%n", this.getUserName(), p.getUserName());
System.out.printf("Player %s는 %d의 피해를 입음");
p.setHealthPoint(Math.max(0, p.getHealthPoint() - this.getAttackPoint()));
System.out.printf("Player %s의 남은 체력: %d", p.getUserName(), p.getHealthPoint());
System.out.println("========================");
}
@Override
같은 건 뭔가요?@
는 Java Annotation
으로, 자바에서 사용하는 주석 입니다. 일반 주석과 다르게, 특별한 의미 및 기능을 Java 내에서 수행 할 수 있습니다. 더 정확하게 말하면, 메타 데이터의 역할을 한다고 볼 수 있습니다. 이게 언제 컴파일이 될 지.. 서버 개발 시 이 함수를 어떤 URI에 넣을지 등.. 명시해 주는 역할도 합니다. 대부분 함수 및 클래스 앞에 작성 합니다.
다형성에 대해서 비유를 해보면, 사실 전사도, 마법사도 플레이어라는 개념으로 묶일 수 있습니다. 우리가 위에서 상속을 통해서 실제로 구현 한 것 처럼요. 다형성을 조금 더 기술적으로 풀어서 이야기 하자면, 부모 클래스 타입의 참조변수로 자식 클래스의 인스턴스를 참조 할 수 있게 하는 것입니다. 사실 위에서 public void attackPlayer(Player p)
함수도 다형성을 이용 한 것 입니다. Player
타입의 참조 번수로 Warrior
, Magician
을 참조할 수 있기 때문입니다. 다음과 같이 말이죠.
Warrior warrior = new Warrior("S2지존전사S2", 100, 10);
Magician magician = new Magician("S2최강법사S2", 50, 20);
warrior.attackPlayer(magician);
magician.attackPlayer(warrior);
위 코드를 모아서, 두 Player
가 전투를 하는 코드를 만들어 보겠습니다.
Warrior.java
public class Warrior extends Player {
Warrior(String userName, int healthPoint, int attackPoint) {
super(userName, healthPoint, attackPoint);
System.out.println("직업 전사");
System.out.println("========================");
}
Warrior(Warrior p) {
super(p);
System.out.println("직업 전사");
System.out.println("========================");
}
@Override
public void attackPlayer(Player p) {
System.out.println("========================");
System.out.printf("Player %s가 Player %s에 강력한 물리 공격%n", this.getUserName(), p.getUserName());
System.out.printf("Player %s는 %d의 피해를 입음%n", this.getUserName(), this.getAttackPoint());
p.setHealthPoint(Math.max(0, p.getHealthPoint() - this.getAttackPoint()));
System.out.printf("Player %s의 남은 체력: %d%n", p.getUserName(), p.getHealthPoint());
System.out.println("========================");
}
}
Magician.java
public class Magician extends Player {
Magician(String userName, int healthPoint, int attackPoint) {
super(userName, healthPoint, attackPoint);
System.out.println("직업 마법사");
System.out.println("========================");
}
Magician(Magician p) {
super(p);
System.out.println("직업 마법사");
System.out.println("========================");
}
@Override
public void attackPlayer(Player p) {
System.out.println("========================");
System.out.printf("Player %s가 Player %s에 치명적인 마법 공격%n", this.getUserName(), p.getUserName());
System.out.printf("Player %s는 %d의 피해를 입음%n", this.getUserName(), this.getAttackPoint());
p.setHealthPoint(Math.max(0, p.getHealthPoint() - this.getAttackPoint()));
System.out.printf("Player %s의 남은 체력: %d%n", p.getUserName(), p.getHealthPoint());
System.out.println("========================");
}
}
Main.java
public class Main {
static public void main(String[] args) {
Warrior warrior = new Warrior("S2지존전사S2", 100, 10);
Magician magician = new Magician("S2최강법사S2", 50, 20);
warrior.attackPlayer(magician);
magician.attackPlayer(warrior);
}
}
========================
플레이어 이름: S2지존전사S2
체력: 100
공격력: 10
유저 아이디: 0
========================
직업 전사
========================
========================
플레이어 이름: S2최강법사S2
체력: 50
공격력: 20
유저 아이디: 1
========================
직업 마법사
========================
========================
Player S2지존전사S2가 Player S2최강법사S2에 강력한 물리 공격
Player S2지존전사S2는 10의 피해를 입음
Player S2최강법사S2의 남은 체력: 40
========================
========================
Player S2최강법사S2가 Player S2지존전사S2에 치명적인 마법 공격
Player S2최강법사S2는 20의 피해를 입음
Player S2지존전사S2의 남은 체력: 80
========================
인터페이스는 추상 클래스 종류의 하나 입니다. 추상 클래스 중, 가장 추상화 정도가 높다 라고 이야기 할 수 있습니다. 왜냐하면, 인터페이스는 오로지 추상 메서드와 상수만을 멤버로 가질 수 있기 때문 입니다.
이를 통해 우리는 다음과 같은 장점을 꾀할 수 있습니다.
작성은 다음과 같은 형태로 작성 할 수 있습니다. 단, 다음과 같은 조건을 만족 하면서 이루어 져야 합니다.
public static final
이어야 하며, 이를 생략할 수 있음.public abstract
이어야 하며, 이를 생략할 수 있다. 단, JDK 1.8 이상에서는 static
과 default
는 예외로 둔다.interface PlayerHealthPointAct {
public static final int HEALING_HEALTH_POINT = 10;
public abstract void healing();
}
또한, 인터페이스는 상속도 가능 합니다. 클래스와 달리 다중 상속이 가능 합니다.
interface PlayerHealthPointAct {
public static final int HEALING_HEALTH_POINT = 10;
public abstract void healing();
}
interface PlayerBuffAct {
public static final int BUFF_ATTACK_POINT = 10;
public abstract void buff();
}
public interface PlayerAct extends PlayerHealthPointAct, PlayerBuffAct {
public void useAllSkill();
}
이는, 클래스의 상속과 별개로, implements
를 이용하여, 서로 관계가 없는, 다른 클래스간에 표준화를 시켜 줄 수 있습니다.
public class Warrior extends Player implements PlayerAct {
Warrior(String userName, int healthPoint, int attackPoint) {
super(userName, healthPoint, attackPoint);
System.out.println("직업 전사");
System.out.println("========================");
}
Warrior(Warrior p) {
super(p);
System.out.println("직업 전사");
System.out.println("========================");
}
@Override
public void attackPlayer(Player p) {
System.out.println("========================");
System.out.printf("Player %s가 Player %s에 강력한 물리 공격%n", this.getUserName(), p.getUserName());
System.out.printf("Player %s는 %d의 피해를 입음%n", this.getUserName(), this.getAttackPoint());
p.setHealthPoint(Math.max(0, p.getHealthPoint() - this.getAttackPoint()));
System.out.printf("Player %s의 남은 체력: %d%n", p.getUserName(), p.getHealthPoint());
System.out.println("========================");
}
@Override
public void healing() {
System.out.println("========================");
System.out.printf("Player %s가 %d 포인트 만큼 회복을 시도 합니다.%n", this.getUserName(), this.HEALING_HEALTH_POINT);
this.setHealthPoint(this.getHealthPoint() + this.HEALING_HEALTH_POINT);
System.out.printf("체력이 %d가 되었습니다.%n", this.getHealthPoint());
System.out.println("========================");
}
@Override
public void buff() {
System.out.println("========================");
System.out.printf("Player %s가 %d 포인트 만큼 공격력을 증가 시킵니다.%n", this.getUserName(), this.BUFF_ATTACK_POINT);
this.setAttackPoint(this.getAttackPoint() + this.BUFF_ATTACK_POINT);
System.out.printf("공격력이 %d가 되었습니다.%n", this.getAttackPoint());
System.out.println("========================");
}
@Override
public void useAllSkill() {
this.healing();
this.buff();
}
}
특정 인터페이스를 구현 하는 객체를, 단 한번만 사용 하는 경우가 생길 수 있습니다. 나중에 Thread에 대해서 배울 텐데, Thread을 생성할 때 사용 하는 Runnable
인터페이스가 대표적 입니다. 우리는 이를 익명 객체를 생성 하여, 단 한 번만 인터페이스 구현체를 사용 하는 경우를 커버 할 수 있습니다. 예를 들어 보여드리자면 다음과 같습니다.
interface TestInterface {
public abstract void printSomething();
}
public class Main {
public static void main(String[] args) {
TestInterface testInterface = new TestInterface() {
@Override
public void printSomething() {
System.out.println("printSomething");
}
};
testInterface.printSomething();
}
}
printSomething
다음 시간에는 Package, Import에 대해서 알아 보는 시간을 가져 보겠습니다.