// 기본형 자료형으로 만든 배열 예시
// 다른 자료형을 넣을 수 없음
int [] iArr = {1, 2, 3, 4, 5, "인프라"}; // 오류 발생
// A 자료형의 힙 주소를 배열로 저장 가능한 배열 생성
A[] aArr = new A[2];
A a1 = new A();
A a2 = new A();
aArr[0] = a1;
aArr[1] = a2;
위와 같이 동종모음인 경우에만 배열 생성이 가능하다.
☆ 다형성을 이용하면 이종모음 구조의 객체 배열 생성이 가능하다.
// 모든 클래스의 부모클래스인 Object 배열을 선언하면
// 다형성 원리(부모 객체를 요구하는 자리에 자식타입을 대입 가능함)에 의해
// 모든 자료를 다 대입할 수 있음.
Object[] oArr = new Object[3];
B b1 = new B();
// aArr[1] = b1; // A타입을 요구하는 배열에 B타입 대입 불가
oArr[0] = b1;
oArr[1] = a2;
oArr[2] = 100; // Object 배열에는 기본형 자료도 대입 가능
위와 같이 다형성 원리에 의해 이종모음 구조의 객체 배열 생성이 가능한 것을 확인할 수 있다.
그러나 상속을 통한 인스턴스 생성 시 부모 영역과 자식 영역이 구분되어 저장되기 때문에 일부 영역은 통제 가능한 상태가 된다.
ex) 부모클래스 사람(이름,나이,자기소개()) & 자식클래스 학생(전공, 공부하기())
→ 부모타입인 사람 타입의 변수에 학생 인스턴스를 대입하는 것이 가능
☆ 부모쪽에 존재하는 함수를 오버라이딩한 경우만 자식의 함수로 호출이 가능하므로 인터페이스를 이용해 다형성을 구현한다.
2) 다형성(Polymorphism)
- 하나의 객체가 여러가지 유형으로 사용되는 것
- 상속을 전제조건으로 함
- 자식클래스가 부모클래스의 타입을 가질 수 있도록 허용 → 부모 타입에 모든 자식객체가 대입될 수 있음
① 다형성이 적용되지 않은 코드 예시
package noinheri;
public class Rabbit {
// 토끼 몬스터 hp, atk, def
private int hp;
private int atk;
private int def;
// 생성자에서 void 파라미터로 각각 3, 0, 0으로 초기화
public Rabbit() {
this.hp = 3;
this.atk = 0;
this.def = 0;
}
// setter / getter 자동생성
// alt + shift + s 후 Generate getters and setters 선택
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
package noinheri;
public class Warrior {
// 정보은닉 적용
private String id;
private int hp;
private int atk;
private int def;
private int exp;
// 생성자 id 입력받고 나머지 필드 초기화
public Warrior(String id) {
this.id = id;
this.hp = 20;
this.atk = 3;
this.def = 1;
this.exp = 0;
}
// 캐릭터 상태 조회
public void showStatus() {
System.out.println("아이디 : " + this.id);
System.out.println("체력 : " + this.hp);
System.out.println("공격력 : " + this.atk);
System.out.println("방어력 : " + this.def);
System.out.println("경험치 : " + this.exp);
System.out.println("------------------");
}
// 단독 사냥을 하도록 메서드 생성
public void huntRabbit(Rabbit rabbit) {
if(rabbit.getHp() <= 0) {
System.out.println("이미 죽은 토끼입니다.");
return; // 죽은 토끼에 대해서는 추가 로직이 필요없음
}
// 1. 내가 공격한 토끼의 체력 -3
rabbit.setHp(rabbit.getHp() - this.atk);
// 2. 방금 공격으로 죽었다면 경험치 5 증가
if(rabbit.getHp() <= 0) {
System.out.println("토끼를 죽였습니다.");
this.exp += 5;
} else {
System.out.println("토끼를 공격했습니다.");
}
}
}
package noinheri;
public class MainClass1 {
public static void main(String[] args) {
// 전사 하나 생성
Warrior w1 = new Warrior("전사1");
// 생성 직후 정보 조회
w1.showStatus();
// 토끼 생성
Rabbit r1 = new Rabbit();
// 토끼와 교전
w1.huntRabbit(r1);
// 죽은 토끼 한 번 더 공격
w1.huntRabbit(r1);
// 사냥 후 정보 조회
w1.showStatus();
}
}
위 상황에서 huntRabbit() 함수는 Rabbit 자료형만을 받아올 수 있게 되어 있으므로 Rat 자료형 요구 시 오류가 발생한다.
package noinheri;
public class Rat {
// 쥐 몬스터 hp, atk, def
private int hp;
private int atk;
private int def;
// 생성자에서 void 파라미터로 각각 3, 0, 0으로 초기화
public Rat() {
this.hp = 5;
this.atk = 1;
this.def = 0;
}
// setter / getter 자동생성
// alt + shift + s 후 Generate getters and setters 선택
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
package noinheri;
public class Warrior {
// 정보은닉 적용
private String id;
private int hp;
private int atk;
private int def;
private int exp;
// 생성자 id 입력받고 나머지 필드 초기화
public Warrior(String id) {
this.id = id;
this.hp = 20;
this.atk = 3;
this.def = 1;
this.exp = 0;
}
// 캐릭터 상태 조회
public void showStatus() {
System.out.println("아이디 : " + this.id);
System.out.println("체력 : " + this.hp);
System.out.println("공격력 : " + this.atk);
System.out.println("방어력 : " + this.def);
System.out.println("경험치 : " + this.exp);
System.out.println("------------------");
}
// 단독 사냥을 하도록 메서드 생성
public void huntRabbit(Rabbit rabbit) {
if(rabbit.getHp() <= 0) {
System.out.println("이미 죽은 토끼입니다.");
return; // 죽은 토끼에 대해서는 추가 로직이 필요없음
}
// 1. 내가 공격한 토끼의 체력 -3
rabbit.setHp(rabbit.getHp() - this.atk);
// 2. 방금 공격으로 죽었다면 경험치 5 증가
if(rabbit.getHp() <= 0) {
System.out.println("토끼를 죽였습니다.");
this.exp += 5;
} else {
System.out.println("토끼를 공격했습니다.");
}
}
// Rat은 공격을 받고 죽지 않으면 1회 반격
// 경험치는 80
public void huntRat(Rat rat) {
if(rat.getHp() <= 0) {
System.out.println("이미 죽은 쥐입니다.");
return;
}
rat.setHp(rat.getHp() - this.atk);
if(rat.getHp() <= 0) {
System.out.println("쥐를 죽였습니다.");
this.exp += 80;
}
else {
System.out.println("쥐를 공격했습니다.");
System.out.println("쥐가 반격합니다.");
this.hp = this.hp - rat.getAtk();
}
}
}
package noinheri;
public class MainClass2 {
public static void main(String[] args) {
// 전사 생성
Warrior w1 = new Warrior("전사2");
// 상태 조회
w1.showStatus();
// 쥐 생성
Rat rat1 = new Rat();
// 쥐와 교전 (3번)
for(int i = 0; i < 3; i++) {
w1.huntRat(rat1);
}
// 상태조회
w1.showStatus();
}
}
② 다형성이 적용된 코드 예시
package inheri;
public class Monster {
// 모든 몬스터 클래스의 부모타입으로 설계된 클래스
private String name; // 다형성 특성상 몬스터 종류를 식별하기 위한 변수
private int hp;
private int atk;
private int def;
private int exp;
// 부모쪽 생성자로 초기화할 때 어떤 몬스터가 생성될지 사전에 알 수 없으므로
// 고정값이 아닌 자식쪽에서 입력받은 자료를 대입하도록 생성자 설계
public Monster(String name, int hp, int atk, int def, int exp) {
this.name = name;
this.hp = hp;
this.atk = atk;
this.def = def;
this.exp = exp;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
public int getExp() {
return exp;
}
public void setExp(int exp) {
this.exp = exp;
}
}
package inheri;
public class Warrior {
// 정보은닉 적용
private String id;
private int hp;
private int atk;
private int def;
private int exp;
// 생성자 id 입력받고 나머지 필드 초기화
public Warrior(String id) {
this.id = id;
this.hp = 20;
this.atk = 3;
this.def = 1;
this.exp = 0;
}
// 캐릭터 상태 조회
public void showStatus() {
System.out.println("아이디 : " + this.id);
System.out.println("체력 : " + this.hp);
System.out.println("공격력 : " + this.atk);
System.out.println("방어력 : " + this.def);
System.out.println("경험치 : " + this.exp);
System.out.println("------------------");
}
// 파라미터로 모든 몬스터의 부모타입인 Monster 인스턴스를 요구하면
// 다형성 원리에 의해 상속받은 모든 몬스터를 다 대입할 수 있음
public void hunt(Monster monster) {
// 죽은 몬스터는 교전 불가 및 메서드 즉시 종료
if(monster.getHp() <= 0) {
System.out.println(monster.getName() + "은 이미 죽어서 교전할 수 없습니다.");
return;
}
// 다음 공격으로 몬스터가 죽는 경우
if(monster.getHp() - (this.atk - monster.getDef()) <= 0){
// 경험치부여
monster.setHp(0);
System.out.println(monster.getName() + "(이)가 죽었습니다.");
this.exp += monster.getExp();
} else {
// 다음 공격으로 몬스터가 죽지 않아서 반격을 받는 경우
// 몬스터 체력을 (내 공격력 - 몬스터 방어력)만큼 차감
monster.setHp(this.atk - monster.getDef());
// 내 체력을 (몬스터 공격력 - 내 방어력)만큼 차감
this.hp -= monster.getAtk() - this.def;
// **가 반격했습니다.
System.out.println(monster.getName() + "(이)가 반격했습니다.");
}
}
}
// 해당 클래스의 인스턴스는 Monster 타입 변수에도 저장할 수 있음.
public class Rabbit extends Monster {
public Rabbit() {
// 부모인 Monster에 이름, 체력, 공격력, 방어력, 경험치 전달
super("토끼", 3, 0, 0, 5);
}
}
package inheri;
public class Rat extends Monster {
public Rat() {
// 부모인 Monster에 이름, 체력, 공격력, 방어력, 경험치 전달
super("쥐", 5, 0, 0, 5);
}
}
package inheri;
public class MainClass1 {
public static void main(String[] args) {
// 전사 생성
Warrior w1 = new Warrior("전사1");
// 상태 조회
w1.showStatus();
// 토끼 생성
Rabbit r1 = new Rabbit();
// 전사와 교전 2회
w1.hunt(r1);
w1.hunt(r1);
// 쥐 생성
Rat r2 = new Rat();
w1.hunt(r2);
w1.hunt(r2);
w1.hunt(r2);
w1.showStatus();
}
}
위와 같이 상속을 통해 Monster 범주에 들어가는 객체를 모두 호출 가능하다.
원래라면 토끼나 쥐 객체를 생성한 후 Warrior 파일에서 hunt() 함수를 생성해줘야 했는데,
다형성을 이용하여 Monster 타입을 처리하는 hunt로 재설계함으로써 코드의 확장성을 높일 수 있다.
- 공개 메서드를 이용하여 데이터 변경 시 메서드 내에 데이터 유효성을 검증할 수 있는 루틴을 넣을 수 있음
- 접근 권한 체크 로직을 통해 인가되지 않은 사용자에게 중요한 데이터나 로직을 숨길 수도 있고, 제어할 수도 있음
- 외부에 공개하고 싶지 않은 메서드도 private으로 선언 가능
2) 예시
① badcase
package encapsulation.bad;
public class MyBirthday {
int year;
int month;
int day;
void showDateInfo() {
System.out.println("내 생일은");
System.out.println(year + "년");
System.out.println(month + "월");
System.out.println(day + "일");
System.out.println("이니까 선물을 준비하세요!");
}
}
package encapsulation.bad;
public class MainClass {
public static void main(String[] args) {
// 같은 패키지 내부 클래스파일을 가져다 쓸 때는 import 필요 없음
MyBirthday b = new MyBirthday();
b.year = 2024;
b.month = 13; // 13월을 넣어도 그냥 동작
b.day = 32; // 32일을 넣어도 그냥 동작
b.showDateInfo();
}
}
위와 같이 누구나 정보에 접근할 수 있기 때문에 아무런 값을 넣어도 그냥 동작한다.
② goodcase
※ 리팩토링이란?
→ 기능을 유지하면서 코드의 구조를 유지보수하기 좋게 개선하는 것
package encapsulation.good;
public class MyBirthday {
// 은닉(캡슐화) 시 변수는 무조건 private으로 처리
private int year;
private int month;
private int day;
// alt+shift+s 혹은 마우스 우클릭 후 source
// generate constructor using fields 선택
public MyBirthday(int year, int month, int day) {
this.year = year;
setMonth(month);
setDay(day);
}
// 은닉된 변수에 접근하기 위해서는
// 클래스 설계 시 미리 설정해둔 setter/getter 메서드를 이용해 접근
// setter 메서드 선언
// 1. setter 메서드는 은닉변수에 값을 저장(세팅)하기 위해 선언
// 2. 메서드의 접근제한자는 public으로 설정하고, 이름은 일반적으로 set+변수명으로 지정
// setMonth를 설계하여 오로지 1~12 중 하나만 받아서 저장하도록 지정
public void setMonth(int month) {
if(month < 1) {
this.month = 1;
} else if (month > 12) {
this.month = 12;
} else {
this.month = month;
}
}
public void setDay(int day) {
if(day < 1 || day > 31) {
this.day = 1; // 범위를 벗어나는 값이 들어올 경우 1로 고정
} else {
this.day = day; // 범위 내의 값이면 그대로 지정
}
}
void showDateInfo() {
System.out.println("내 생일은");
System.out.println(year + "년");
System.out.println(month + "월");
System.out.println(day + "일");
System.out.println("이니까 선물을 준비하세요!");
}
}
package encapsulation.good;
public class MainClass {
public static void main(String[] args) {
// 12월 32일같은 없는 날짜를 걸러주는지 체크
MyBirthday b = new MyBirthday(2024, -37, 50);
// b.day = 50; // private이므로 외부인 main에서 직접 주입 불가능
b.showDateInfo();
}
}
위와 같이 잘못된 값을 넣어도 setter로 정의해둔 값에 따라 결과가 잘 출력되는 것을 확인할 수 있다.
생성자 생성 후 메인함수에서 값을 변경할 수 없도록 하고 싶다면 setter 함수를 private 접근 지정자로 설정해두면 된다.