🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [6]일차
🚀 6일차에서는 상속에 대해서 자세히 배우고, 상속에 관련하여 extends키워드, 메소드 오버라이딩, super키워드 사용 등
예제를 반복학습하여 상속에 익숙해지고자 하였다. 관련하여 실습 또한 진행하였다!
<생성자>
생성자의 경우 중요한 개념이므로 예제와 함께 복습하였다.
- Getter Setter는 다른 프로그래머가 접근해서 쓸 수도 있지만 프레임워크가 접근해서 사용할 수도 있는데
Getter, Setter의 일반적 형식을 벗어나게 작성할 경우에는 인식을 못하는 경우가 발생하므로
일정된 양식으로 작성해주는 것이 중요
Pen p1 = new Pen();
- Pen클래스에는 생성자를 생성하지 않았음에도 인스턴스가 생성되는 것을 확인할 수 있는데
이는 컴파일러가 자동으로 생성자를 생성한 경우라고 보면된다.
❓그렇다면 명시적으로 생성자를 정의하고자할때는 어떤 경우일까
➡️Pen이라는 클래스가 인스턴스화 될때부터 “초기값”을 갖고싶다.
이럴 경우에 쓰는 것이 명시적으로 생성자를 정의 하는 것이다.
public static void main(String[] args) {
Pen p0 = new Pen();
Pen p1 = new Pen("Red"); // Pen() = 생성자
Pen p2 = new Pen("모나미", 200);
Pen p3 = new Pen("MorningGlory", "Blue", 500);
System.out.println("1) 기본 생성자 : " + p0);
System.out.println("2) 색깔 생성자 : " + p1.getColor());
System.out.println("3) 이름, 가격 생성자 : [" + p2.getName() + "]의 가격 : " + p2.getPrice());
System.out.println("4) 모든 정보를 가진 생성자 : ([" + p3.getName() + "]의 색깔 : " + p3.getColor() + ", 가격 : " + p3.getPrice() + ")");
}
이처럼 각기 다른 속성(필드)를 가지고 생성자들을 정의할 수 있다.
<this - 예약어>
“인스턴스를 가리키는 예약어” 인 this는
this.color = color 처럼 구분이 되지 않는 것을 “내 것”이라고 표현하며 구분을 함
this() 는 메소드 안에서 가장 먼저 위에서 실행되어야한다. (super 다음으로 실행)
이 this()는 다른 생성자를 호출하는 방법이다.
// TV의 모든 정보 생성자
public TV(int channel, int volumn, boolean power) {
System.out.println("TV(int channel, int volumn, boolean power) 생성자 호출");
this.channel = channel;
this.volumn = volumn;
this.power = power;
}
// TV의 전원 정보 생성자
public TV(boolean power) {
System.out.println("TV(boolean power) 생성자 호출");
this.power = power;
}
- 생성자에서도 코드가 중복해서 나온다. 위의 코드에서
this.power = power; 중복되는 코드를 계속 똑같이 쓸것인지 생각해보아야함
생성자에서 실제로 값만 채우는 것이 아니라 객체가 생성될때 처음 해야할일들이 많아질 것이다.
❓이럴때 중복된 코드를 어떻게 처리하는 것이 좋을까
중복된 코드가 여기저기 있을때의 가장 큰 문제점은 "수정하기가 힘들다."
TV가 해야하는 공통적인 일을 정의하면서 각 생성자마다 실행하는 코드를 달리할때
추가된 일이나 수정할 일이 있으면 그것을 일일이 수정할 수는 없을 것이다.
객체지향에서는 중복된 코드를 어떻게 처리해야할까 고민하는 것이 필요
▶️메소드로 하나 뺀다던지? 의 방법을 선택해야함
▶️생성자가 다른 생성자를 호출하게 할 수 있음
this.power = power;
위의 코드를 사용하고 있는 생성자 하나를 선택해서 그 생성자를 호출하는 방식으로 반복사용하는 것이 효율적이다.
1. 반복된 코드를 호출하는 예시코드)
// TV의 모든 정보 생성자
public TV02(int channel, int volumn, boolean power) {
System.out.println("TV(int channel, int volumn, boolean power) 생성자 호출");
this.channel = channel;
this.volumn = volumn;
this.power = power;
}
// TV의 전원 정보 생성자
public TV02(boolean power) {
// this()로 묶어주는 예제 (생성자가 다른 생성자를 호출할 수 있다)
// 메소드의 위에 위치
this(0, 0, power);
System.out.println("TV(boolean power) 생성자 호출");
this.power = power;
}
2. 반복된 코드를 호출하는 예시코드)
// 1. Pen
// this.color = color;
// this.name = name;
// 위 코드들은 3번 펜과 겹치므로 이렇게 바꿔줄 수 있음
this(color, name);
this.price = price;
// 2. Pen
this.name = name;
// 3. Pen
this.color = color;
this.name = name;
이렇게 3개의 Pen 생성자가 있을 경우 1번 펜에서 this.color와 this.name대신 3번 펜의 생성자를 호출할 수 있다는 것이다.
<생성자와 Scanner>
Scanner scanner = new Scanner();
이 경우 new생성자로 데이터를 어디서 받아들일것인지 모르므로 (=생성자부분이 비어있음)
Scanner객체를 설계할때부터 만들지 않은 것이다.
하지만 Scanner같은 경우 default생성자가 없기때문에 만들어지지 않는다.
Scanner scanner = new Scanner(System.in);
생성자로 들어오는 System.in은 InputStream이므로,
API에서 확인할 수 있는대로 Scanner(InputStream source)를 받는 것을 볼 수 있다.
▶️실습 - 모험가 생성 프로그램
<Backpacker 클래스> - 모험가 별 생성자를 관리하는 클래스
// @@**@@
public class Backpacker {
// ...Fields
// 상급 모험가 생성자 (무기가 있음)
public Backpacker(String name, String weapon, int HP, int MP) {
System.out.println("[상급] 모험가가 생성되었습니다.");
this.name = name;
this.weapon = weapon;
this.HP = HP;
this.MP = MP;
}
// 중급 모험가 생성자 (무기는 없음)
public Backpacker(String name, int HP, int MP) {
System.out.println("[중급] 모험가가 생성되었습니다.");
this.name = name;
this.HP = HP;
this.MP = MP;
}
// 초급 모험가 생성자 (이름과 HP만 존재)
public Backpacker(String name, int HP) {
System.out.println("[초급] 모험가가 생성되었습니다.");
this.name = name;
this.HP = HP;
}
// 초보자 생성자 (이름없이 HP만 존재)
public Backpacker(int HP) {
System.out.println("이름 없는 모험가가 생성되었습니다.");
this.HP = HP;
}
//... Getter/Setter
}
- 초보자 / 초급 / 중급 / 상급 모험가 별로 생성할 수 있는 필드의 범위를 점차 늘려나가며, 상급 모험가의 경우
이름/무기/HP/MP 의 필드를 가질 수 있도록 생성자를 생성해보았다.
<BackpackerExam 클래스> - 모험가 인스턴스를 생성하는 클래스
// 1) 초보자 생성
Backpacker user1 = new Backpacker(10);
// 2) 초급 모험가 생성
Backpacker user2 = new Backpacker("김자바", 30);
// 3) 중급 모험가 생성
Backpacker user3 = new Backpacker("박이선", 50, 30);
// 4) 상급 모험가 생성
Backpacker user4 = new Backpacker("시언어", "목검", 100, 50);
<상속>
상속 관계 = is a 관계 혹은 kind of 관계
상속은 무조건 상속하는 것이 아니라 이러한 관계가 성립되어야한다.
예를 들어 트럭은 차다, 책상은 가구다 등의 관계가 성립이되어야한다.
상속의 장점 : 내가 다 만들지 않아도 이미 만들어진 것을 상속받는 것이기 때문에 편리하다.
상속에 관련하여 main에서부터 한줄한줄 실행하듯 프로그램은 수행흐름이 있는데
이것을 기본적으로 “단일 스레드”라고 한다.
카카오톡같은 채팅프로그램에서는 “단일 스레드”로 구현이 불가능할 것이다.
실시간으로 다른 유저가 쓰는 채팅이 업데이트 되는 것을 “멀티 스레드”라고 한다.
이처럼 동시성이 느껴지도록 프로그래밍 하는 것을 “멀티 스레드”라고 한다.
자바에서는 이미 스레드라는 객체가 구현되어있고,
스레드의 흐름을 나누려면 스레드를 상속받아 사용하면된다.
이렇게 상속에서도 스레드가 활용될 수 있는 것이다.
<상속이 잘못된 사례>
자료구조인 스택의 경우 먼저 들어간 데이터는 가장 나중에 나와야하는 FILO 구조이다.
(First In Last Out)
이처럼 스택은 중간에서 값을 꺼내는것이 불가능한 자료구조인데,
자바에서 스택이 구현될때 벡터나 리스트 처럼 배열과 비슷하게 생긴 자료구조를 상속받아
스택을 구현하게 된다. 그럼 벡터나 리스트의 메소드를 사용할 수 있어 스택이 중간에서 값을 꺼내는 것도 가능해진다.
⚠️이렇게 잘못상속받게 되면 스택의 특성과 달리 스택이 가지면 안되는 기능들을 가지게 된다.
위 사진을 보면 Stack은 Vector를 상속받고 있다. 여기서 Vector는 배열과 비슷하게 쓰이는 자료구조로
get(int index)와 같은 메소드가 있는데 인덱스로 접근하여 값을 꺼낼 수 있는 것이다.
스택에서는 peek(), pop() (=맨 위 값 꺼내기), push() (=스택에 값 넣기) 등만 구현이 되어있어야하는데
Vector를 상속받아 "상속이 잘못 구현된 경우"이다.
<상속 = 일반화 + 확장>
class Car{
String name;
int speed;
public void run(){
System.out.println("자동차가 달립니다.");
}
}
class Bus extends Car{
public void passenger(){
System.out.println("승객을 태웁니다.");
}
}
// 상속 관련 예제
public class InheritanceExam01 {
public static void main(String[] args) {
Car car = new Car();
car.name = "티코";
car.speed = 100;
System.out.println("[" + car.name + "]의 속도 : " + car.speed);
car.run();
// car.passenger(); // 사용 불가능
Bus bus = new Bus();
bus.name = "스쿨버스";
bus.speed = 70;
System.out.println("[" + bus.name + "]의 속도 : " + bus.speed);
bus.run(); // Car의 메소드를 상속받아 사용
bus.passenger();
}
}
상속은 일반화와 확장을 합한 개념이다. (상속 = 일반화 + 확장)
부모클래스를 상속받는다는 것은 부모가 가지고 있는 것을 자식이 물려받아 사용할 수 있다는 의미이다.
여기서도 Bus는 passenger()라는 메소드를 만들며 본인만의 기능을 “확장” 한 것을 알 수 있다.
여기서 Bus의 {name, speed} | Truck의 {name, speed} 처럼 공통된 것들은 묶어서 관리하는것이 편할 것이다.
이렇게 공통화된 것들을 묶어서 관리하는 것이 "일반화"이다.
위 코드에서 일반화의 사례는 Car클래스라고 볼 수 있다.
Bus와 Truck이 가진 공통적인 것들을 상위클래스인 Car로 묶어줄 수 있다.
즉, 일반화된 것들을 Car로 묶어놓고, 각자에서 구현할 기능들은 확장으로 구현할 수 있을 것이다.
<상속의 형변환>
- 부모는(조상)이며 자식(자손)을 가리킬 수 있다.
Parent p = null; // Parent를 담을(가리킬) 수 있는 타입
Child c = null; // Child를 담을(가리킬) 수 있는 타입
객체들 사이에서도 형변환이 가능하다.
p = new Child(); // 부모는 자식을 담을(가리킬) 수 있지만, (묵시적 형변환)
c = new Parent(); // 자식은 부모를 담을(가리킬) 수 없다.
여기서 p가 가리키는 것은 Child 실체를 가리키는데, 타입은 Parent이기때문에
➡️c = p 처럼 자식의 그릇에 부모를 담는 것은 불가능하다.
그렇지만 실제로는 p가 가리키고 있는 인스턴스가 Child이기때문에
c = (Child)p;
처럼 Child타입으로 형변환해주는 “명시적 형변환”이 가능하다.
p = new Child(); // 부모는 자식을 담을(가리킬) 수 있지만, (묵시적 형변환)
c = (Child)p; // 명시적 형변환
따라서
Car c = new Car();
Car c2 = new Bus();
Car c3 = new Truck();
부모클래스 타입으로 자식 인스턴스를 만드는 것은 가능하지만
Truck t = new Truck();
Truck t2 = new Bus();
Truck t3 = new Car();
자식클래스 타입으로 부모 인스턴스를 만드는 것은 불가능하다.
<자바는 단일 상속만 허용>
자바는 다중상속이 되지 않는다. 자바는 단일 상속만 허용한다.
Car sb1 = new SeatBus();
Bus sb2 = new SeatBus();
와 같은 경우는 다중상속인 경우이므로 불가능하다.
<자식클래스 타입에 부모클래스 타입으로 선언된 SeatBus 인스턴스 담기>
SeatBus sb = sb1;
sb1은 SeatBus인스턴스이지만, 부모클래스인 Car타입으로 만들어진 참조변수이기때문에
더 작은그릇에 담지 못한다.
따라서
SeatBus sb = (SeatBus) sb1;
처럼 명시적 형변환을 해주어야한다.
sb1은 부모타입이기때문에 SeatBus의 메소드인 “좌석을 예약하다” 를 사용할 수는 없다.
사용하기 위해선
SeatBus sb = (SeatBus) sb1;
sb.좌석을 예약하다();
( (SeatBus) sb1 ). 좌석을 예약하다();
처럼 써주어야한다.
<아무것도 상속받지 않을 경우>
자동으로 java.lang.Object를 상속받는다.
class Car extends Object {
…
}
처럼 생략이 된 것이다. 모든 클래스는 Object의 자손이다. (최상위 클래스)
Object obj = new Car();
Object obj2 = new Bus();
Object obj3 = new SeatBus();
그렇기때문에 부모클래스 Object타입으로 다른 클래스들을 선언하는 것이 가능해진다.
<다형성 - 메소드 오버라이딩 (Overriding)>
상위 클래스의 “메소드”를 하위클래스가 “재정의”하는 것
메소드의 이름은 물론, 파라미터의 개수나 타입도 동일해야하며,
주로 상위 클래스의 동작을 상속받은 하위 클래스에서 “변경하기 위해 사용”
👀추가로 정보 은닉은 객체지향의 중요한 기법
중요한 필드는 은닉하고 , 메소드를 통해서만 필드에 접근해서 사용하도록 한다.
👀형변환이 되지 않는 것을 강제로 형변환할때는 ClassCastException이 발생할 수 있다.
if(obj3 instanceof SeatBus){
SeatBus obj4 = (SeatBus) obj3;
obj4.좌석을예약하다();
((SeatBus) obj3).좌석을예약하다();
}
이렇게 적어주어 조건문으로 해결할 수 있다.
<부모의 메소드를 오버라이딩한 자식 메소드가 있을때 어느 것이 실행될까>
public static void main(String[] args) {
ExamParent01 p = new ExamParent01();
System.out.println(p.i); // 5
System.out.println(p.getI()); // 5
// >> ExamChild01은 인스턴스로 생성되지도 않았기에 i = 10은 출력되지 않는다.
// 이번엔 ExamChild01의 인스턴스를 생성하였기에, 10, 10이 출력됨을 확인 가능
// Child가 생길때는 Parent가 반드시 생긴 후에 생기게 된다.
ExamChild01 c = new ExamChild01();
System.out.println(c.i); // 10
System.out.println(c.getI()); // 10
// 따라서 Child타입의 getI()를 사용하면 Parent의 getI()를 쓸지, Child의 getI()를 쓸지 = 자식의 것을 실행
// 결국 오버라이딩 (재정의) 한 이유는 본인이 쓰기 위해서 재정의한 것이기때문에 본인의 것, 자식의 것을 실행한다.
}
<부모의 타입으로 자식 인스턴스를 생성하면 결과가 어떨까>
<자식 클래스>
// 자식 클래스
public class ExamChild01 extends ExamParent01 {
int i = 10; // i가 오버라이딩된 상태
public ExamChild01() {
System.out.println("Child의 기본생성자 실행");
}
public int getI(){
return i;
}
public void print(){
System.out.println(i);
}
}
<부모 클래스>
public class ExamParent01 {
int i = 5;
public ExamParent01() {
System.out.println("Parent의 기본생성자 실행");
}
public int getI() {
return i;
}
}
이렇게 클래스가 존재할때,
public static void main(String[] args) {
ExamParent01 p = new ExamChild01();
System.out.println(p.i); // 부모의 필드 값 5
System.out.println(p.getI()); // 자식의 메소드 값 10
// 부모 타입으로 접근할 수 있는 값은 필드밖에 없다.
// 메소드는 재정의 되어있기때문에 부모 단에서는 자식이 오버라이딩해놓은 메소드를 모른다.
}
⭐필드는 타입을 따른다. 필드는 타입이 무엇이냐에따라 값이 다르게 나온다.
👀 따라서
System.out.println(c.i);
를 하게되면 10이 나오게된다.
⭐필드는 오버라이딩 되었을때
- “타입을 따른다”
- 필드는 private한것이 객체지향의 기본이기때문에 필드에 접근해서 값을 꺼내쓰는 경우는 거의 없다.
⭐따라서 메소드가 오버라이딩되었을때
- 자식의 것을 쓴다는 것이 핵심
이러한 원칙때문에 다형성의 원칙이 일어난다.
<자식 인스턴스를 지정하지 않았을때 다형성의 예제>
class Bird{
// 많은 필드와 메소드가 있다고 가정
public void song(){
System.out.println("새가 노래합니다.");
}
}
// 새를 상속받은 "까마귀"
class crow extends Bird{
// 메소드를 오버라이딩
@Override
public void song() {
System.out.println("까마귀가 노래합니다 : [까악 까악]");
}
}
class duck extends Bird{
@Override
public void song() {
System.out.println("오리가 노래합니다 : [꽥 꽥]");
}
}
// 비둘기
class pigeon extends Bird{
@Override
public void song() {
System.out.println("비둘기가 노래합니다 : [구구구구]");
}
}
// 자식 클래스로 무엇을 줘야할지 모를때의 예제
public class BirdExam {
public static void main(String[] args) {
// 아직 어떤 새를 생성할지 모른다고 가정
Bird bird = null;
// 실행할때의 값이 무엇이느냐에 따라 조건문 지정
if(args[0].equals("비둘기")){
bird = new pigeon();
} else if(args[0].equals("오리")){
bird = new duck();
} else if(args[0].equals("까마귀")){
bird = new crow();
} else{
bird = new Bird();
}
bird.song();
}
}
프로그램이 실행되는 시점에 메소드가 어떤것이 실행되는지를 알 수 있어야한다는 것이다.
⭐bird.song()처럼 껍데기가 하나지만, 구현체가 여러개가 있다. 이러한 경우를 다형성이라고 한다.
⭐타입이 부모타입인 것에 상관없이 메소드가 오버라이딩되면 무조건 자식의 메소드가 사용된다.
<@Override 어노테이션>
@Override 어노테이션이 없어져도 실행은 된다.
하지만 song()를 오버라이딩해야하는데 sonng()처럼 메소드 이름을 실수 했을때, 오버라이딩이 아니게 된다.
이럴때 @Override 어노테이션은 에러를 발생하여 오버라이딩이 아님을 알려주는 역할을 해준다.
song(String msg)처럼 오버라이딩이 아닌 오버로딩 된 경우에도 어노테이션은 에러를 발생시키는 역할을 한다.
<메소드 오버라이딩과 형변환으로 인한 결과 비교>
// 메소드 오버라이딩과 형변환으로 인한 결과 비교 예제
public class ExamMain04 {
public static void test(ExamParent01 p){
System.out.println(p.i);
System.out.println(p.getI());
((ExamChild01)p).print();
}
public static void main(String[] args) {
ExamChild01 c = new ExamChild01();
test(c);
// 결과 : 5, 10, 10
ExamParent01 pc = new ExamChild01();
test(pc);
// 결과 : 5, 10, 10
ExamParent01 p = new ExamParent01();
test(p);
// 결과 : 5, 5, (에러가 발생) - 마지막은 에러가 발생한다. 형변환이 되지 않는다.
// 인스턴스가 Parent인데, 위의 test에서 (Child)로 형변환이 되지 않을 것이다.
}
}
<super 키워드>
// 도형에 생성자가 추가되자 이 도형을 상속받은 클래스가 오류가 나는 이유?
// this() : 자신의 생성자를 의미
// super : 부모를 가리키는 키워드 (ex. super.필드, super.메소드())
// super() : 내 부모의 생성자를 의미
class Diagram {
int width; // 가로
int height; // 세로
Diagram(int width, int height){
this.width = width;
this.height = height;
}
}
class Circle extends Diagram {
Circle(){
// 생략이 되었을뿐 super()가 있는 것
super();
}
}
class Rectangle extends Diagram{
Rectangle(){
// 생략이 되었을뿐 super()가 있는 것
super();
}
}
// 도형으로 알아보는 상속 예제
public class ExamMain05 {
public static void main(String[] args) {
}
}
직접 추가하지 않으면 super()와 같은 코드를 컴파일러가 자동으로 추가한다.
super() 가 지정되지 않아 에러가 뜨는 오류를 해결하기 위해서는 super에 부모가 가지고 있는 생성자를 불러줘야한다.
부모인 Diagram은 기본생성자가 없고, 초기값으로 width, height를 가지는 생성자가 있으므로
super(0, 0);
super(3, 3);
등 처럼 인자를 전달해주며 생성해야한다.
<수정코드>
class Circle extends Diagram {
Circle(){
// 생략이 되었을뿐 super()가 있는 것
super(3, 3);
}
}
class Rectangle extends Diagram{
Rectangle(){
// 생략이 되었을뿐 super()가 있는 것
super(5, 5);
}
public Rectangle(int width, int height) {
super(width, height);
}
}
⭐부모의 생성자가 기본생성자가 없다면 자식이 생성자에서 부모의 생성자를 불러줘야한다.
this 메소드 보다도 super가 가장 우선순위이다. 부모가 먼저 만들어져야 내가 만들어질 수 있다.
❓super는 언제써야할까
➡️super를 사용하지않아도 오류가 발생하지 않지만, 부모클래스인 도형을 상속받을때
자식 클래스인 사각형이 초기값으로 가로, 세로를 갖고 생성하게 하고 싶다면
부모의 생성자(가로,세로)를 호출해주어야한다.
➡️위의 코드인 경우 인스턴스가 생성할때 초기값을 주고 싶으면 super를 통해 부모 생성자를 통해 호출하는 것이다.
💡 6일차에는 상속과 메소드 오버라이딩에 대한 예제를 계속 반복하면서 관련된 키워드들도 배웠다.
상속에 관련해서만 헷갈리는 부분도 있고, 메소드 오버라이딩에서도 필드의 오버라이딩과 메소드의 오버라이딩의 경우
다르게 처리되는 방식 등에서 두 경우를 뚜렷하게 구분해야겠다고 느꼈다.
super키워드도 계속 배워도 긴가민가한 부분이 많아서 비슷한 예제를 많이 풀어보는 것이 답일 것 같다!🚀
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_8일차_'인터페이스와 예외처리' (0) | 2024.12.11 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_7일차_'String클래스와 추상클래스' (0) | 2024.12.10 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_5일차_'메소드, 필드, static' (3) | 2024.12.06 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_4일차_'객체지향프로그래밍' (1) | 2024.12.05 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_3일차_'배열과 객체' (1) | 2024.12.04 |