ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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(); 가 컴파일러에 의해 자동으로 생성된다고 한다.(직접 코드 넣어서 테스트 해봄. 오류없음.)

     

Designed by Tistory.