기초

자바 기초 복기5

Bordercolli 2024. 12. 26. 19:26
728x90

[상속-시작]

GasCar.java

package extends1.ex1;

public class GasCar {
    public void move(){
        System.out.println("차를 이동합니다.");
    }
    public void fillUp(){
        System.out.println("기름을 주유합니다.");
    }
}

ElectricCar.java

package extends1.ex1;

public class GasCar {
    public void move(){
        System.out.println("차를 이동합니다.");
    }
    public void fillUp(){
        System.out.println("기름을 주유합니다.");
    }
}

CarMain.java

package extends1.ex1;

public class CarMain {
    public static void main(String[] args) {
        ElectricCar electricCar=new ElectricCar();
        electricCar.move();
        electricCar.charge();

        GasCar gasCar=new GasCar();
        gasCar.move();
        gasCar.fillUp();
    }
}

전기차와 가솔린 차를 만들었다. 전기차는 이동/ 충전 기능이 있고, 가솔린차는 이동/ 주유 기능이 있다.

 

전기차와 가솔린차는 자동차(Car)의 좀 더 구체적인 개념이다. 반대로 자동차(Car)는 전기차와 가솔린차를 포함하는 추상적인 개념이다. 그래서인지 잘 보면 둘의 공통 기능이 보인다. 바로 이동(move())이다.

전기차든 가솔린차든 주유하는 방식이 다른 것이지 이동하는 방식은 같다. 이런 경우 상속 관계를 사용하는 것이 효과적이다.

 

[상속 관계]

상속은 객체 지향 프로그래밍의 핵심 요소 중 하나로, 기존 클래스의 필드와 메서드를 새로운 클래스에서 사용하게 해준다. 이름 그대로 기존 클래스의 속성과 기능을 그대로 물려받는 것이다. 상속을 사용하려면 Extends키워드를 사용하면 된다. 그리고 extends대상은 하나만 선택할 수 있다.

 

용어정리

- 부모 클래스(슈퍼 클래스) : 상속을 통해 자신의 필드와 메서드를 다른 클래스에 제공하는 클래스

- 자식 클래스(서브 클래스) : 부모 클래스로부터 필드와 메서드를 상속받는 클래스

 

주의

* 지금부터 코드를 작성할 때 기존 코드를 유지하기 위해, 새로운 패키지에 기존 코드를 옮겨가면서 코드를 작성. 클래스의 이름이 같기 때문에 패키지명과 import사용에 주의해야 한다.

 

상속 관계를 사용하도록 코드를 작성

 

기존 코드를 유지하기 위해 ex2패키지를 새로 만들자.

Car.java

public class Car {  // 부모 클래스
    public void move(){
        System.out.println("차를 이동합니다.");
    }
}

CarMain.java

package extends1.ex2;


public class CarMain {
    public static void main(String[] args) {
        ElectricCar electricCar=new ElectricCar();
        electricCar.move();
        electricCar.charge();

        GasCar gasCar=new GasCar();
        gasCar.move();
        gasCar.fillUp();
    }
}

ElectricCar.java

public class ElectricCar extends Car { // 이러면 부모의 기능을 모두 물려받음
    public void charge(){
        System.out.println("충전합니다");
    }
}

CarMain.java

public class CarMain {
    public static void main(String[] args) {
        ElectricCar electricCar=new ElectricCar();
        electricCar.move();
        electricCar.charge();

        GasCar gasCar=new GasCar();
        gasCar.move();
        gasCar.fillUp();
    }
}

전기차와 가솔린차가 car를 상속받은 덕분에 electricCar.move(), GasCar.move()를 사용할 수 있다.

 

대신, 반대로 부모 클래스는 자식 클래스에 접근할 수 없다. 자식 클래스는 부모 클래스의 기능을 물려받기 때문에, 접근할 수 있지만, 그 반대는 아니다. 부모 코드를 보면 자식에 대한 정보가 하나도 없다. 반면에 자식 코드는 extends Parents를 통해 부모를 알고 있다.

 

+ 자바는 다중 상속을 지원하지 않는다. 그래서 extend대상은 하나만 선택할 수 있다. 부모를 하나만 선택할 수 있다는 뜻, 물론 부모가 또 다른 부모를 하나 가지는 것은 가능

 

만약 그림과 같이 다중 상속을 사용하게 된다면 Airplane입장에서 move()를 호출할 때 어떤 부모의 move()를 사용해야 할지 애매한 문제가 발생한다. 이것을 다이아몬드 문제라고 한다. 그리고 다중 상속을 사용하면 클래스 계층 구조가 매우 복잡해질 수 있다. 이런 문제점 때문에 자바는 클래스의 다중 상속을 허용하지 않는다. 대신에 이후에 설명한 인터페이스의 다중 구현을 허용해서 이러한 문제를 피한다.

 

[상속과 메모리 구조]

상속 관계를 객체로 생성할 때 메모리 구조를 확인해보자.

new ElectricCar()를 호출하면 ElectricCar 뿐만 아니라, 상속 관계에 있는 Car까지 함꼐 포함해서 인스턴스를 생성한다. 참조값은 x001로 하나이지만, 실제로 그 안에서는 car, electricCar라는 두 가지 클래스 정보가 공존하는 것이다.

상속이라고 해서 단순하게 부모 필드와 메서드만 물려받는 것이 아니다. 상속 관계를 사용하면 부모 클래스도 함께 포함해서 생성된다. 즉, 외부에서 볼 때는 하나의 인스턴스를 생성하는 것 같지만 내부에서는 부모와 자식이 모두 생성되고 공간도 구분된다.

electricCar.charge()를 호출하면 참조값을 확인해서 x001.charge()를 호출한다. 따라서 x001을 찾아서 charge()를 호출하면 되는 것이다. 그런데 상속 관계의 경우에는 내부에 부모와 자식이 모두 존재한다. 이때 부모인 Car를 통해서 charge를 찾을 지 아니면 electricCar를 통해서 charge를 찾을지 선택해야 한다. 이때는 호출하는 변수의 타입(클래스)을 기준으로 선택한다. electricCar변수 타입이 ElectricCar이므로 인스턴스 내부에 같은 타입인 ElectricCar를 통해서 charge를 호출한다.

electricCar.move()를 호출하면 먼저 x001을 참조로 이동한다. 내부에는 Car, ElectricCar두 가지 타입이 있다. 이때 호출하는 변수인 electricCar타입이 ElectricCar이므로 이 타입을 선택한다.

그런데 ElectricCar에는 move()메서드가 없다. 상속 관계에서는 자식타입에 해당 기능이 없으면 부모 타입으로 올라가서 찾는다. 이 경우 ElectricCar의 부모인 Car로 올라가서 move()를 찾는다. 부모인 Car에 move()가 있으므로 부모에 있는 move()메서드를 호출한다.

 

만약 부모에도 해당 기능을 찾지 못하면, 더 상위 부모에 필요한 기능을 찾아본다. 물론 계속 찾아도 없으면 컴파일 오류

 

지금까지 설명한 상속과 메모리 구조는 반드시 이해해야 한다.

1. 상속 관계의 객체를 생성하면, 그 내부에는 부모와 자식이 모두 생성된다.

2. 상속 관계의 객체를 호출할 때, 대상 타입을 정해야 한다. 이때 호출자의 타입을 통해 대상 타입을 찾는다.

3. 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행한다. 기능을 찾이 못하면 컴파일 오류 발생

 

[상속과 기능추가]

상속관계의 장점을 알아보기 위해, 상속 관계에 다음 기능을 추가해보자.

모든 차량에 문열기 (openDoor())기능을 추가해야 한다.

새로운 수소차(HybrogenCar)를 추가해야 한다.

 - 수소차는 fillHydrogen()기능을 통해 수소를 충전할 수 있다.

public class Car {  // 부모 클래스
    public void move(){
        System.out.println("차를 이동합니다.");
    }

    // 추가
    public void openDoor(){
        System.out.println("문을 엽니다.");
    }
}

+ 모든 차량에 문열기 기능을 추가할 때는 상위 부모인 Car에 openDoor기능을 추가하면 된다. 이렇게 하면 Car자식들은 해당 기능을 모두 물려받게 된다. 만약 상속 관계가 아니었다면 각각의 차량에 해당 기능을 모두 추가

 

public class HydrogenCar extends Car{
    public void fillHydrogen(){
        System.out.println("수소를 충전합니다.");
    }
}

수소차를 추가했다. Car를 상속받은 덕분에 move(), openDoor()와 같은 기능을 바로 사용할 수 있다. 수소차 전용 기능은 수소 충전 fillHydrogen()기능을 제공한다.

public class CarMain {
    public static void main(String[] args) {
        ElectricCar electricCar=new ElectricCar();
        electricCar.move();
        electricCar.charge();
        electricCar.openDoor();

        GasCar gasCar=new GasCar();
        gasCar.move();
        gasCar.fillUp();
        electricCar.openDoor();

        HydrogenCar hydrogenCar=new HydrogenCar();
        hydrogenCar.move();
        hydrogenCar.fillHydrogen();
        hydrogenCar.openDoor();
    }
}

 

상속 관계 덕분에 중복은 줄어들고, 새로운 수소차를 편리하게 확장한 것을 알 수 있다.

 

[상속과 메서드 오버라이딩]

부모타입의 기능을 자식에서는 다르게 재정의하고 싶을 수 있다.

ex) 자동차의 경우 Car.move()라는 기능이 있다. 이 기능을 사용하면 단순히 "차를 이동합니다"라고 출력한다. 전기차의 경우, 보통 더 빠르기 때문에 전기차가 move()를 호출한 경우, 전기차를 빠르게 이동합니다. 라고 출력을 변경하고 싶다.

 

이렇게 부모에게서 상속받은 기능을 자식에서 재정의 하는 것을 -> 메서드 오버라이딩 이라고 한다.

public class ElectricCar extends Car { // 이러면 부모의 기능을 모두 물려받음
    public void charge(){
        System.out.println("충전합니다");
    }
    @Override
    public void move(){
        System.out.println("전기차를 빠르게 이동합니다.");
    } 
    // 이처럼 Override하는 경우, 어노테이션을 해줘야 한다.
}

 

ElectricCar는 부모인 Car의 move()기능을 그대로 사용하고 싶지 않다. 메서드 이름은 같지만 새로운 기능을 사용하고 싶다. 그래서 ElectricCar의 move메서드를 새로 만들었다.

이렇게 부모의 기능을 자식이 새로 재정의하는 것을 메서드 오버라이딩이라고 한다.

이제 ElectricCar의 move()를 호출하면 Car의 move()가 아니라, ElectricCar의 move()가 호출된다.

 

@Override

@이 붙은 부분을 애노테이션이라 한다. 애노테이션은 주석과 비슷한데, 프로그램이 읽을 수 있는 특별한 주석이라고 생각하면 된다. 

 

이 애노테이션은 상위 클래스의 메서드를 오버라이드 하는 것을 나타낸다.

이름 그대로 오버라이딩한 메서드 위에 애노테이션을 붙여야 한다.

컴파일러는 이 애노테이션을 보고, 메서드가 정확히 오버라이드 되었는지 확인한다. 오버라이딩 조건을 만족시키지 않으면, 컴파일 에러를 발생시킨다. 따라서 실수로 오버라이딩을 못하는 경우를 방지해준다. 예를 들어 이 경우에 만약 부모에 move()메서드가 없다면 컴파일 오류가 발생한다. 이 기능은 필수는 아니지만 명확성을 위해서 붙혀주는 것이 좋다.

 

public class ElectricCar extends Car { // 이러면 부모의 기능을 모두 물려받음
    public void charge(){
        System.out.println("충전합니다");
    }
    @Override
    public void moveeee(){
        System.out.println("전기차를 빠르게 이동합니다.");
    }
    // 이처럼 Override하는 경우, 어노테이션을 해줘야 한다.
}

애노테이션을 하면 좋은 점이 이처럼 moveee이렇게 적어도 부모의 move를 실행할 거라는 표식이기 때문에 컴파일 오류를 내어 오류를 고치라고 해준다.

 

[오버라이딩과 메모리 구조]

1. electricCar.move()를 호출한다.

2. 호출한 electricCar의 타입은 ElectricCar이다. 따라서 인스턴스 내부의 ElectricCar타입에서 시작한다.

3. ElectricCar타입에 move()메서드가있다. 해당 메서드를 실행한다. 이때 실행할 메서드를 이미 찾았으므로, 부모 타입을 찾지않음.

 

[오버로딩과 오버라이딩]

메서드 오버로딩: 메서드 이름이 같고 매개변수(파라미터)가 다른 메서드를 여러 개 정의하는 것을 메서드 오버로딩

메서드 오버라이딩: 하위 클래스에서 상위 클래스의 메서드를 재정의하는 과정을 의미한다. 따라서 상속 관계에서 사용한다. 부모의 기능을 자식이 다시 정의하는 것이다. 자식의 새로운 기능이 부모의 기존 기능을 넘어 타서 기존 기능을 새로운 기능으로 덮어버린다고 이해. 오버라이딩을 우리 말로 번역하면 무언가를 다시 정의한다고 해서 재정의라 한다. 상속관계에서 기존 기능을 다시 정의한다고 이해하면 된다. 실무에서는 베서드 오버라이딩, 메서드 재정의 둘 다 사용한다.

* 부모 메서드와 같은 메서드를 오버라이딩 할 수 있다. 정도로 이해

 

메서드 오버라이딩 조건

1. 메서드 이름: 메서드 이름이 같아야 한다.

2. 메서드 매개변수(파라미터): 파라미터 타입, 순서, 개수가 같아야 한다.

3. 반환 타입: 반환타입이 같아야 한다. 단 반환 타입이 하위 클래스 타입일 수 있다.

4. 접근 제어자: 오버라이딩 매서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안된다. ex) 상위 클래스의 메서드가 protected로 선언되어 있으면, 하위 클래스에서 이를 public또는 protected로 오버라이드할 수 있지만, private또는 default로 오버라이드 할 수 없다.

5. 예외: 오버라이딩 메서드는 상위 클래스 메서드보다 더 많은 체크 예외를 throws로 선언할 수 없다. 하지만, 더 적거나 같은 수의 예외, 또는 하위 타입의 예외는 선언할 수 있다. 예외를 학습해야 이해할 수 있다. 예외는 뒤에서 다룬다.

* static, final, private 키워드가 붙은 메서드는 오버라이딩 될 수 없다.

 - static은 클래스 레벨에서 작동하므로 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없다. 쉽게 이야기해서 그냥 클래스 이름을 통해 필요한 곳에 직접 접근하면 된다.

- final 메서드를 재정의를 금지한다.

public final void move(){ // 이러면 메서드를 더이상 고칠 수 없다.
    System.out.println("전기차를 빠르게 이동합니다.");
}

- private메서드는 해당 클래스에만 접근 가능하기 떄문에 하위 클래스에서 보이지 않는다. 따라서 오버라이딩할 수 없다.

private void move(){
    System.out.println("차를 이동합니다.");
    // 부모에 있는 것을 숨긴다.
}

6. 생성자 오버라이딩: 생성자는 오버라이딩 할 수 없다.

 

[상속과 접근 제어]

UML표기법

 

+: public

#: protected  -> 같은 패키지 안에서 호출은 허용한다. 패키지가 달라도 상속 관계의 호출은 허용한다.

~: default(package-private) -> 같은 패키지 안에서 호출은 허용한다.

-: private

 

printParent.java 

package extends1.access.parent;

public class Parent {
    public int publicValue;
    protected int protectedValue;
    int defaultValue;
    private int privateValue;

    public void publicMethod(){
        System.out.println("parent.publicMethod");
    }
    protected void protectedMethod(){
        System.out.println("parent.protectedMethod");
    }
    void defaultMethod(){
        System.out.println("parent.defaultMethod");
    }
    private void privateMethod(){
        System.out.println("parent.privateMethod");
    }
    public void printParent(){
        System.out.println("==Parent 메서드 안==");
        System.out.println("publicValue = "+publicValue);
        System.out.println("protectedValue = "+protectedValue);
        System.out.println("defaultValue = "+defaultValue);
        System.out.println("privateValue = " + privateValue);

        // 부모 메서드 안에서 모두 접근 가능
        defaultMethod();
        privateMethod();
    }
}

child.java

package extends1;

import extends1.access.parent.Parent;

public class Child extends Parent {

    public void call(){
        publicValue=1;
        protectedValue=1; // 상속 관계 or 같은 패키지
        //defaultValue=1; // 다른 패키지 접근 불가, 컴파일 오류
        //privateValue=1; // 접근 불가, 컴파일 오류

        publicMethod();
        protectedMethod(); // 상속 관계 or 같은 패키지
        //defaultMethod(); // 다른 패키지 접근 불가, 컴파일 오류
        //privateMethod(); // 접근 불가, 컴파일 오류

        printParent();
        // public이라서 본인꺼 호출 가능
    }
}

 

 

자식 클래스인 Child에서 부모 클래스인 Parent에 얼마나 접근할 수 있는 지 확인해보자

publicValue=1: 부모의 public필드에 접근한다. public이므로 접근할 수 있다.

protectedValue=1: 부모의 protected필드에 접근한다. 자식과 부모는 다른 패키지이지만, 상속 관계이므로 접근할 수 있다.

defaultValue=1: 부모의 default필드에 접근한다. 자식과 부모가 다른 패키지이므로 접근할 수 없다.

provateValue=1: 부모의 private필드에 접근한다. private은 모든 외부 접근을 막으니 자식이라도 호출할 수 없다.

 

접근제어와 메모리 구조

본인 타입에 없으면 부모 타입에서 기능을 찾는데, 이때 접근 제어자가 영향을 준다. 왜냐하면 객체 내부에서는 자식과 부모가 구분되어 있기 때문이다.결국 자식 타입에서 부모 타입의 기능을 호출할 때, 부모 입장에서 보면 외부에서 호출한 것과

 

[SUPER-부모참조]

부모와 자식의 필드명이 같거나 메서드가 오버라이딩 되어 있으면, 자식에서 부모의 필드나 메서드를 호출할 수 없다. 이때 super키워드를 사용하면 부모를 참조할 수 있다. super는 이름 그대로 부모 클래스에 대한 참조를 나타낸다.

 

다음 예를 보자. 부모의 필드명과 자식의 필드명 둘다 value로 같다. 메서드도 hello()로자식에서 오버라이딩이 되어 있다. 이때 자식 클래스에서 부모 클래스의 value와 hello()를 호출하고 싶다면 super키워드를 사용하면 된다.

 

Parent.java

package extends1.super1;

public class Parent {
    public String value = "parent";
    public void hello(){
        System.out.println("Parent.hello");
    }
}

child.java

package extends1.super1;

public class child extends Parent{
    public String value="child";
    @Override
    public void hello(){
        System.out.println("Child.hello");
    }
    public void call(){
        System.out.println("this value = "+this.value); // 자기자신(this없으면 내꺼에서 찾고, 내꺼에도 없으면 부모에 가서 찾음)
        System.out.println("super value = "+super.value); // 나의 부모 타입에서 찾음

        this.hello();
        super.hello();
    }
}

 

call()메서드를 보자.

1. this는 자기 자신 참조를 뜻한다. this는 생략 가능

2. super는 부모 클래스에 대한 참조를 뜻한다.

3. 필드 이름과 메서드 이름이 같지만 super를 사용해서 부모 클래스에 있는 기능을 사용할 수 있다.

내꺼 아니고 부모 메서드를 호출하고 싶을때 -> super.사용!

'기초' 카테고리의 다른 글

자바 기초 복기6  (0) 2024.12.26
자바 기초 복기5 -> 다시 정리  (2) 2024.12.19
자바 기초 복기4  (3) 2024.12.18
자바 기초 복기3  (0) 2024.12.17
자바 기초복기2  (1) 2024.12.16