-
[TIL #13-1][JAVA]객체지향 프로그래밍 심화, 상속(오버라이딩, super()와 super)CodeStates_Backend/TIL (Today I Learned) 2022. 5. 18. 15:59
코드스테이츠 백엔드 부트캠프 39기 18일차, 05/12
객체지향 프로그래밍 심화
객체지향의 4대 기둥
상추 다 캐버려! 상추다캐!! 👉 상속, 추상화, 다형성, 캡슐화
📌 상속
기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다. 상속에 상속에 상속 . . . 도 가능하다. 예를 들면
interface Animal {public abstract void cry();} // 인터페이스 선언. public abstract 생략 가능. interface Pet {void play();} class Cat implements Animal, Pet { // Animal과 Pet 인터페이스 다중 구현 public void cry(){ System.out.println("야옹~!"); } public void play(){ System.out.println("쥐 잡기"); } } class Pig extends Cat{ } class Horse extends Pig{ }
위 코드를 보면 Cat 클래스를 Pig 클래스가 상속받고 다시 Horse 클래스가 Pig 클래스를 상속받는다. 상속관계가 몇 번 중첩되든 상관없이 상속관계의 원칙이 그대로 다 적용된다고 생각하면 된다. 상위 클래스들의 모든 사용가능한 멤버를 다 사용할 수 있다.
단, 하나의 클래스에는 단 하나의 클래스만 상속 가능하다는 것을 주의해야 한다. (단일상속 = single inheritance)
✏️ [참고] '인터페이스'를 통해 다중상속과 비슷하게 구현할 수 있음.
상속은 다형적 표현이 가능하다는 장점이 있다. ‘프로그래머는 프로그래머이다'라는 문장은 참이다. 동시에 ‘프로그래머는 사람이다' 또한 참이다. 즉 하나의 객체가 여러 모양으로 표현될 수 있다는 것을 우리는 다형성이라 말한다.
📌 포함 관계
포함(composite)관계는 상속처럼 클래스를 재사용할 수 있는 방법으로, 클래스의 멤버로 다른 클래스 타입의 참조변수를 선언하는 것이다.
예제코드를 보면,
public class Employee { int id; String name; Address address; public Employee(int id, String name, Address address) { this.id = id; this.name = name; this.address = address; } void showInfo() { System.out.println(id + " " + name); System.out.println(address.city+ " " + address.country); } public static void main(String[] args) { Address address1 = new Address("서울", "한국"); Address address2 = new Address("도쿄", "일본"); Employee e = new Employee(1, "김코딩", address1); Employee e2 = new Employee(2, "박해커", address2); e.showInfo(); e2.showInfo(); } } class Address { String city, country; public Address(String city, String country) { this.city = city; this.country = country; } } //Output 1 김코딩 서울 한국 2 박해커 도쿄 일본
예제 코드에서 Employee 클래스의 멤버로, Address 클래스의 참조변수인 address 가 선언되어 있다. 사실 객체지향 프로그래밍에서 상속보다 포함관계를 사용하는 경우가 더 많다.
📌 상속 vs 포함 관계 구분법
🎈 <IS-A> 관계 : 상속
🎈 <HAS-A> 관계 : 포함 관계
📌 메소드 오버라이딩
상위 클래스로부터 상속받은 메소드와 동일한 이름의 메소드를 재정의 하는 것이다. (
재정!!의라!!이딩) Override의 사전적 의미인 '덮어쓰다'의 의미대로 덮어쓰기를 하는 것을 연상하면 어울린다.class Vehicle { void run() { System.out.println("Vehicle is running"); } } public class Bike extends Vehicle { // Vehicle 클래스 상속 void run() { System.out.println("Bike is running"); // 메서드 오버라이딩 } public static void main(String[] args) { Bike bike = new Bike(); bike.run(); } } //Output "Bike is running"
🍒 오버라이딩 3가지 조건
1️⃣ 메소드 이름, 매개변수, 반환타입이 상위클래스의 그것과 동일해야 한다.
2️⃣ 접근 제어자의 범위가 상위 클래스의 그것보다 같거나 넓어야 한다.
3️⃣ 예외를 상위 클래스의 그것보다 많이 선언할 수 없다.
더보기[참고] 오버로딩과 비교
👺 오버로딩 조건 2가지
1️⃣ 메소드 명이 같다.
2️⃣ 매개변수의 개수나 타입이 다르다.
👉 매개변수 타입과 개수가 같으면 순서가 달라도 오버로딩이 안된다. 중복 선언이 된다.
👉 반환타입은 오버로딩에 영향을 미치지 않는다.
오버라이딩이 다형성과도 연결된다. 다형성은 앞서 상속에서 언급했듯이, 하나의 객체를 여러가지로 표현할 수 있는 것인데 상속을 통해 오버라이딩을 하면 각각의 객체를 상위 클래스의 타입으로 표현할 수 있게 된다.
예제 코드를 보면,
public class Main { public static void main(String[] args) { // 배열로 한번에 관리하기 Vehicle[] vehicles = new Vehicle[] { new Bike(), new Car(), new MotorBike()}; for (Vehicle vehicle : vehicles) { vehicle.run(); } } } class Vehicle { void run() { System.out.println("Vehicle is running"); } } class Bike extends Vehicle { void run() { System.out.println("Bike is running"); } } class Car extends Vehicle { void run() { System.out.println("Car is running"); } } class MotorBike extends Vehicle { void run() { System.out.println("MotorBike is running"); } } //Output Bike is running Car is running MotorBike is running
Bike 객체를 Bike 타입으로 표현할 수도 있고 상위 클래스인 Vehicle 타입으로도 표현할 수 있는 것이다. 하나의 객체를 다양하게 표현할 수 있다는 뜻이다. Car, MotorBike 객체도 마찬가지이다. 특히 위 예제 코드처럼 Bike, Car, MotorBike 객체는 메서드 오버라이딩을 통해 각각의 run() 메소드가 다른 출력값을 보여준다는 사실을 확인할 수 있고, 이렇게 모든 객체를 상위 클래스 타입 하나로 선언하면 간편하게 상위 클래스 타입의 배열로 선언하여 관리할 수 있다는 편리성이 있다.
📌 super 와 super()
this, this() 와 같은 메커니즘이다. 단, 상위 클래스를 가리킬 뿐이다. super는 상위 클래스의 객체, super()는 상위 클래스의 생성자를 의미한다. 당연히 상속관계를 전제로 한다.
먼저 super 키워드에 대한 예제코드를 보자.
public class Super { public static void main(String[] args) { Lower l = new Lower(); l.callNum(); } } class Upper { int count = 20; // super.count } class Lower extends Upper { int count = 15; // this.count void callNum() { System.out.println("count = " + count); System.out.println("this.count = " + this.count); System.out.println("super.count = " + super.count); } } //Output count = 15 count = 15 count = 20
위 예제 코드를 보면 Lower 클래스는 Upper 클래스로부터 count 를 상속받는데, 공교롭게도 자신의 인스턴스 변수 count 와 이름이 같아 둘을 구분할 방법이 필요하다. 이럴 때 사용하는 것이 super 이다. 자바 컴파일러는 상위 클래스의 변수보다 해당 객체의 멤버를 먼저 참조한다.(지역변수가 멤버변수보다 우선순위인 것처럼) 따라서 상위 클래스의 변수를 가리키려면 super.count 로 사용해야 한다.
다음으로 super() 에 대한 예제 코드를 보자.
public class Test { public static void main(String[] args) { Student s = new Student(); } } class Human { Human() { System.out.println("휴먼 클래스 생성자"); } } class Student extends Human { // Human 클래스로부터 상속 Student() { super(); // Human 클래스의 생성자 호출 System.out.println("학생 클래스 생성자"); } } //Output 휴먼 클래스 생성자 학생 클래스 생성자
super() 역시 this() 와 똑같은 두가지 조건을 가진다.
1. 생성자 내부에서만 사용
2. 생성자 내부의 첫 라인에 작성
🔥 생성자, this() , super() 관련하여 컴파일러에 의해 자동 생성 되는 경우에 대한 정리(나의 헷갈림을 위함..)
자동 생성해주는 경우는 단 한가지다.
어떤 클래스든지 생성자가 하나도 구현되어 있지 않은 경우, 컴파일러가 기본 생성자를 자동생성 해주는 것이다.
단 하나라도 생성자가 구현되어 있다면 컴파일러는 기본 생성자를 자동생성 해주지 않는다. 또한 매개변수가 있는 생성자는 자동생성 해주지 않는다.
this() 의 경우
해당 클래스 내에서 오버로딩된 다른 생성자를 호출하는 기능으로써 자동생성 같은거에 해당되는 경우가 없다. 왜냐면 this() 자체가 이미 어떠한 생성자 내부에서만 사용되니까..
super() 의 경우
상위 클래스에서 아무런 생성자도 구현되어 있지 않은 상태(위 빨간 글처럼)에서, 상위 클래스의 기본생성자를 호출하는 super() 를 사용했다면 컴파일러가 자동으로 상위 클래스에 기본 생성자를 자동생성 해주므로 에러가 발생하지 않게 되는 것이다. 당연히 상위 클래스의 기본 생성자를 호출하도록 하는 매개변수가 없는 super() 만 사용했을 때만 가능한 것이다.
마지막으로 모든 클래스는 Object 클래스를 상속받으므로, 모든 클래스의 기본생성자에는 첫 줄에 super(); 가 컴파일러에 의해 자동으로 생성된다고 한다.(직접 코드 넣어서 테스트 해봄. 오류없음.)
'CodeStates_Backend > TIL (Today I Learned)' 카테고리의 다른 글
[TIL #14-1][JAVA] 객체지향 프로그래밍 심화, 다형성 (0) 2022.05.21 [TIL #13-2][JAVA] 객체지향 프로그래밍 심화, 캡슐화(패키지, 접근 제어자, getter, setter) (0) 2022.05.21 [TIL #12][JAVA] 생성자, this 와 this() (0) 2022.05.16 [TIL #11-2][JAVA] 객체지향 프로그래밍(OOP, Object Oriented Programming) 개념, 클래스, 객체, 필드, 메서드 (0) 2022.05.16 [TIL #11-1][블로그] 블로깅에 대해 (0) 2022.05.14