[Java] 자바 실전(4) - (final,상속)

wony·2024년 3월 28일

Java

목록 보기
6/30

0.개요

주제 : 김영한님의 자바 실전 강의 총 정리
내용 : final, 상속에 대해 공부!

챕터8. final

1) final 변수와 상수1

final 키워드는 이름 그대로 끝이라는 뜻이다.
변수에 final 키워드가 붙으면 더는 값을 변경할 수 없다.
참고로 finalclass, method를 포함한 여러 곳에 붙을 수 있다.

1. final - 지역 변수

public class FinalLocalMain {

    public static void main(String[] args) {
        //final 지역 변수1
        final int data1;
        data1 = 10; //최초 한번만 할당 가능
  
        //data1 = 20; //컴파일 오류
        //final 지역 변수2
        final int data2 = 10;
        //data2 = 20; //컴파일 오류
        method(10);
    }

    //final 매개변수
    static void method(final int parameter) {
        //parameter = 20; 컴파일 오류
    }
}
  • final 을 지역 변수에 설정할 경우 최초 한번만 할당할 수 있다. 이후에 변수의 값을 변경하려면 컴파일 오류가 발생한다.
  • final 을 지역 변수 선언시 바로 초기화 한 경우 이미 값이 할당되었기 때문에 값을 할당할 수 없다.
  • 매개변수에 final 이 붙으면 메서드 내부에서 매개변수의 값을 변경할 수 없다. 따라서 메서드 호출 시점에 사용된 값이 끝까지 사용된다.

2. final - 필드(멤버 변수)

// final 필드 - 생성자 초기화
public class ConstructInit {
    final int value;

    public ConstructInit(int value){
        this.value = value;
    }
}
  • final을 필드에 사용할 경우 해당 필드는 생성자를 통해서 한번만 초기화 될 수 있다.
//final 필드 - 필드 초기화
public class FieldInit {
    static final int CONST_VALUE = 10;
    final int value = 10;
}
  • final 필드를 필드에서 초기화하면 이미 값이 설정되었기 때문에 생성자를 통해서도 초기화 할 수 없다. value 필드를 참고하자.
  • 코드에서 보는 것 처럼 static 변수에도 final 을 선언할 수 있다.

2) final 변수와 상수2

상수(Constant)
상수는 변하지 않고, 항상 일정한 값을 갖는 수를 말한다. 자바에서는 보통 단 하나만 존재하는 변하지 않는 고정된 값을 상수라 한다.
이런 이유로 상수는 static final 키워드를 사용한다.

3) final 변수와 참조

final은 변수의 값을 변경하지 못하게 막는다.

  • 변수는 크게 기본형 변수와 참조형 변수가 있다.
  • 기본형 변수는 10 , 20 같은 값을 보관하고, 참조형 변수는 객체의 참조값을 보관한다.
    • final 을 기본형 변수에 사용하면 값을 변경할 수 없다.
    • final 을 참조형 변수에 사용하면 참조값을 변경할 수 없다.
public class Data {
	public int value;
}
  • int value : final이 아니다. 변경할 수 있는 변수다.
public class FinalRefMain {
	public static void main(String[] args) {
		final Data data = new Data();
        //data = new Data(); //final 변경 불가 컴파일 오류

        //참조 대상의 값은 변경 가능
		data.value = 10;
		System.out.println(data.value);
		data.value = 20;
		System.out.println(data.value);
	}
}
  • 참조형 변수 datafinal 이 붙었다. 변수 선언 시점에 참조값을 할당했으므로 더는 참조값을 변경할 수 없다.
  • 그런데 참조 대상의 객체 값은 변경할 수 있다.
    • 참조형 변수 datafinal 이 붙었다. 이 경우 참조형 변수에 들어있는 참조값을 다른 값으로 변경하지 못한다. 쉽게 이야기해서 이제 다른 객체를 참조할 수 없다. 그런데 이것의 정확한 뜻을 잘 이해해야 한다. 참조형 변수
      에 들어있는 참조값만 변경하지 못한다는 뜻이다. 이 변수 이외에 다른 곳에 영향을 주는 것이 아니다.
  • Data.valuefinal 이 아니다. 따라서 값을 변경할 수 있다.

  • 정리하면 참조형 변수에 final 이 붙으면 참조 대상을 자체를 다른 대상으로 변경하지 못하는 것이지, 참조하는 대상의 값은 변경할 수 있다. 값을 변경 못하게 해야!!

정리

  • final은 매우 유용한 제약이다. 만약, 특정 변수의 을 할당한 이후에 변경하지 않아야 한다면 final을 사용하자.

changeData() 메서드에서 finalid 값 변경을 시도하면 컴파일 오류가 발생한다.변수에 final 키워드가 붙으면 최초 한번만 할당할 수 있다. 이후에 변수의 값을 변경하려면 컴파일 오류가 발생한다!

public class Member {
    private final String id; //final 키워드 사용
    private String name;
    public Member(String id, String name) {
        this.id = id;
        this.name = name;
    }
    public void changeData(String id, String name) {
		//this.id = id; //컴파일 오류 발생
        this.name = name;
    }
    public void print() {
        System.out.println("id:" + id + ", name:" + name);
    }
}
  • changeData() 메서드에서 finalid 값 변경을 시도하면 컴파일 오류가 발생한다.
public class MemberMain {
	public static void main(String[] args) {
		Member member = new Member("myId", "kim");
		member.print();
		member.changeData("myId2","seo");
		member.print();
	}
}

챕터9. 상속

1) 상속 관계

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

용어 정리

  • 부모 클래스(슈퍼 클래스) : 상속을 통해 자신의 필드와 메서드를 다른 클래스에 제공하는 클래스
  • 자식 클래스(서브 클래스) : 부모 클래스로부터 필드와 메서드를 상속받는 클래스
// 부모 클래스
public class Car {
	public void move() {
		System.out.println("차를 이동합니다.");
	}
}
// 자식 클래스
public class ElectricCar extends Car {
	public void charge() {
		System.out.println("충전합니다.");
	}
}
// 자식 클래스
public class GasCar extends Car {
	public void fillUp() {
		System.out.println("기름을 주유합니다.");
	}
}
// 가솔린차도 전기차와 마찬가지로 `extends Car` 를 사용해서 부모 클래스인 `Car` 를 상속 받는다. 상속 덕분에 여기서도 `move()` 를 사용할 수 있다.
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();
	}
}

1-1) 단일 상속

참고로 자바는 다중 상속을 지원하지 않는다. 그래서 extends 대상은 하나만 선택할 수 있다. 부모를 하나만 선택할 수 있다는 뜻이다. 물론, 부모가 또 다른 부모를 하나 가지는 것은 괜찮다.

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

1-2) 상속과 메모리 구조

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

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

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

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

  • 상속 관계의 객체를 생성하면 그 내부에는 부모와 자식이 모두 생성된다.
  • 상속 관계의 객체를 호출할 때, 대상 타입을 정해야 한다. 이때 호출자의 타입을 통해 대상 타입을 찾는다.
  • 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행한다. 기능을 찾지 못하면 컴파일 오류가 발생한다.

2) 상속과 메서드 오버라이딩

부모 타입의 기능을 자식에서는 다르게 재정의 하고 싶을 수 있다.
예를 들어서 자동차의 경우 Car.move()라는 기능을 사용하면 단순히 "차를 이동합니다."라고 출력한다. 전기차의 경우 보통 더 빠르게 이동하기 때문에 전기차가 move()를 호출한 경우에는 "전기차를 빠르게 이동합니다."라고 출력을 변경한다.

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

public class ElectricCar extends Car {
	@Override
	public void move() {
		System.out.println("전기차를 빠르게 이동합니다.");
	}
	public void charge() {
		System.out.println("충전합니다.");
	}
}

ElectricCar는 메서드 이름은 같지만 새로운 기능인 ElectricCar의 move() 메서드를 새로 만들었다. 이렇게 부모의 기능을 자식이 새로 재정의 하는 것을 메서드 오버라이딩이라 한다. 이제 ElectricCarmove() 를 호출하면 Carmove() 가 아니라 ElectricCarmove() 가 호출된다.

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

실행 결과

전기차를 빠르게 이동합니다.
차를 이동합니다.

2-1) 오버라이딩과 메모리 구조

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

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

  • 메서드 오버로딩: 메서드 이름이 같고 매개변수(파라미터)가 다른 메서드를 여러개 정의하는 것을 메서드 오버로딩(Overloading)이라 한다. 오버로딩은 번역하면 과적인데, 과하게 물건을 담았다는 뜻이다. 따라서 같은 이름의 메서드를 여러개 정의했다고 이해하면 된다.

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

3) super - 부모 참조

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

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

public class Parent {
	public String value = "parent";

	public void hello() {
		System.out.println("Parent.hello");
	}
}
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(); //this 생략 가능
		super.hello();
	}
}
  • call() 메서드를 보자.
    • this는 자기 자신의 참조를 뜻한다. this는 생략할 수 있다.
    • super는 부모 클래스에 대한 참조를 뜻한다.
    • 필드 이름과 메서드 이름이 같지만 super를 사용해서 부모 클래스에 있는 기능을 사용할 수 있다.
public class Super1Main {
	public static void main(String[] args) {
		Child child = new Child();
		child.call();
	}
}

실행결과
결과를 보면 super 를 사용한 경우 부모 클래스의 기능을 사용한 것을 확인할 수 있다.

this value = child
super value = parent
Child.hello
Parent.hello

super 메모리 그림

3-1) super - 생성자

상속 관계의 인스턴스를 생성하면 결국 메모리 내부에는 자식과 부모 클래스가 다 만들어진다. Child를 만들면 부모인Parent까지는 함께 만들어지는 것이다. 따라서, 각각의 생성자도 모두 호출되어야 한다.

상속 관계를 사용하면 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다. 상속 관계에서 부모의 생성자를 호출할 때는 super(...)를 사용하면 된다

왜 부모 클래스의 생성자를 호출하는가? (중요)

  • 상속 관계에서 부모 클래스의 생성자를 호출하는 주된 이유는 부모 클래스의 필드를 초기화하기 위해서입니다. 이를 통해 객체가 올바르게 생성되고, 모든 필드가 적절히 초기화됩니다.
  • 이렇게 하면 객체가 완전하고 일관된 상태로 생성되며, 코드의 재사용성과 유지보수성이 높아집니다.

상속 관계에서 생성자를 어떻게 사용하는지 알아보자.

  • ClassA 는 현재 파라미터가 없는 기본 생성자이지만 ClassB는 기본 생성자가 아니기 때문에 ClassC가 상속을 받을 때, 반드시 super 를 사용해주어야 한다.
public class ClassA {

    public ClassA(){
        System.out.println("ClassA 생성자");
    }
}
  • ClassA는 최상위 부모 클래스이다.
public class ClassB extends ClassA{

    public ClassB(int a){
        super();               // 기본 생성자 생략 가능
        System.out.println("ClassB 생성자 a=" + a);
    }

    public ClassB(int a, int b){
        super();              // 기본 생성자 생략 가능
        System.out.println("ClassB 생성자 a="+a + " b=" + b);
    }
}
  • ClassBClassA 를 상속 받았다. 상속을 받으면 생성자의 첫줄super(...) 를 사용해서 부모 클래스의 생성자를 호출해야 한다.
    • 예외로 생성자 첫줄에 this(...) 를 사용할 수는 있다. 하지만 super(...) 는 자식의 생성자 안에서 언젠가는 반드시 호출해야 한다.
  • 부모 클래스의 생성자가 기본 생성자(파라미터가 없는 생성자)인 경우에는 super() 를 생략할 수 있다.
    • 상속 관계에서 첫줄에 super(...) 를 생략하면 자바는 부모의 기본 생성자를 호출하는 super() 를 자동으로 만들어준다.
    • 참고로 기본 생성자를 많이 사용하기 때문에 편의상 이런 기능을 제공한다.
public class ClassC extends ClassB{

    public ClassC(){
        super(10,20); // ClassB에는 기본 생성자가 없다. 따라서 기본 생성자를 호출하는 `super()`를 사용하거나 생략할 수 없다.
        System.out.println("ClassC 생성자");
    }
}
  • ClassCClassB를 상속 받았다. ClassB는 다음 두 생성자가 있다.
    • ClassB(int a)
    • ClassB(int a, int b)
  • 생성자는 하나만 호출할 수 있다. 두 생성자 중에 하나만 선택하면 된다.
    • super(10,20) 를 통해 부모 클래스의 ClassB(int a, int b) 생성자를 선택했다.
  • 참고로 ClassC의 부모인 ClassB에는 기본 생성자가 없다. 따라서 부모의 기본 생성자를 호출하는super() 를 사용하거나 생략할 수 없다.
public class Super2Main {

    public static void main(String[] args) {
        ClassC classC = new ClassC();
    }
}

실행 결과

ClassA 생성자
ClassB 생성자 a=10 b=20
ClassC 생성자
  • 실행해보면 ClassA ClassB ClassC 순서로 실행된다.
    • 생성자의 실행 순서가 결과적으로 최상위 부모부터 실행되어서 하나씩 아래로 내려오는 것이다. 따라서 초기화는 최상위 부모부터 이루어진다. 왜냐하면 자식 생성자의 첫 줄에서 부모의 생성자를 호출해야 하기 때문이다.

정리

  • 상속 관계의 생성자 호출은 결과적으로 부모에서 자식 순서로 진행된다. 따라서, 부모의 데이터를 먼저 초기화하고 그 다음에 자식의 데이터를 초기화한다.
  • 상속 관계에서 자식 클래스의 생성자 첫줄에 반드시 super(...)를 호출해야 한다. 단, 기본 생성자(super())인 경우 생략할 수 있다.

this(...)와 함께 사용
코드의 첫줄에 this(...)를 사용하더라도 반드시 한번은 super(...)를 호출해야 한다.

코드 변경

public class ClassB extends ClassA{

    public ClassB(int a){
        this(a,0); // 기본 생성자 생략 가능
        System.out.println("ClassB 생성자 a=" + a);
    } 

    public ClassB(int a, int b){
        super(); // 기본 생성자 생략 가능
        System.out.println("ClassB 생성자 a="+a + " b=" + b);
    }
}
public class Super2Main {
    public static void main(String[] args) {
        // ClassC classC = new ClassC();
        ClassB classB = new ClassB(100);
    }
}

문제와 풀이

문제 : 상속 관계 상품
쇼핑몰의 판매 상품을 만들어보자.

  • Book , Album , Movie 이렇게 3가지 상품을 클래스로 만들어야 한다.
  • 코드 중복이 없게 상속 관계를 사용하자.
    부모 클래스는 Item 이라는 이름을 사용하면 된다.
  • 공통 속성: name , price
    • Book : 저자( author ), isbn( isbn )
    • Album : 아티스트( artist )
    • Movie : 감독( director ), 배우( actor )

다음 코드와 실행결과를 참고해서 Item , Book , Album , Movie 클래스를 만들어보자.

public class ShopMain {
    public static void main(String[] args) {
        Book book = new Book("JAVA", 10000, "han", "12345");
        Album album = new Album("앨범1", 15000,"seo");
        Movie movie = new Movie("영화1", 18000,"감독1", "배우1");

        book.print();
        album.print();
        movie.print();

        int sum = book.getPrice() + album.getPrice() + movie.getPrice();
        System.out.println("상품 가격의 합: " + sum);
    }
}
public class Item {
    public String name;
    public int price;

    public Item(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public int getPrice() {
        return price;
    }

    public void print() {
        System.out.println("이름:" + name + ",가격:" + price);
    }
}
public class Book extends Item{
    public String author;
    public String isbn;

    public Book(String name, int price, String author, String isbn) {
        super(name, price); // 부모 클래스의 생성자 호출
        this.author = author;
        this.isbn = isbn;
    }

    @Override
    public void print(){
        super.print();    // 부모 클래스의 print 메서드 호출
        System.out.println("- 저자:" + author + ", isbn:" + isbn);
    }
}
  • super(name, price); 부모 클래스의 생성자를 호출해서 필드 초기화
  • super.print(); 부모 클래스의 print 메서드 호출
profile
안녕하세요. wony입니다.

0개의 댓글