티스토리 뷰

JAVA/whiteship-livestudy

상속

최진영 2021. 3. 28. 17:37

자바 상속의 특징

상속이란, 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다.

 

 상속은 그 단어의 뜻과 마찬가지로 부모 클래스를 자손 클래스에 변수와 메소드를 물려주는 것을 말한다. 지금의 상속은 class와 class 간의 상속에 대해서 이야기 할 것이므로 Interface와 implement는 다음 포스팅에서 이야기할 예정이다.

 간단하게 맛만 먼저 보자.

class Parent{
    private String name;
    private int money = 30;

    public int getMoney() {
        return money;
    }
}
class Child extends Parent {
    private String name;
    private int money;

    public Child(int money) {
        this.money = money;
    }

    public int getMoney() {
        return this.money + getMoney();
    }
}

 Child가 Parent를 상속 받았고 따라서 Childe의 메소드에서 Parent 메소드를 사용할 수 있는 것을 확인할 수 있다.

 

 "자식클래스에서 부모클래스의 변수와 메소드를 받아서 사용할 수 있다" 가 메인 베이스이지만 지켜야할 규칙들과 제한이 굉장히 많다.

 

부모 클래스는 자식 클래스에게 자신의 모든 변수와 메소드를 제공한다.

 말 그대로 부모 클래스에서 사용하고 있는 변수와 메소드는 자식 메소드에서 모두 사용할 수 있다. 하지만 접근 지시자의 특성상 사용하지 못하는 경우가 있으므로(private, default) 주의해서 사용해야 한다.

 public은 모든 위치, protected는 상속관계와 같은 패키지 및 폴더에 존재할 때, private는 같은 클래스 내에서만, default는 같은 패키지 및 폴더에 존재할 때만 클래스의 메소드를 사용할 수 있다. 따라서 private와 default로 지정된 변수와 메소드는 자식클래스에서 사용할 수 없다.

package Week6;

public class Exam1 {
    public static void main(String[] args) {
        Child child = new Child();
        System.out.printf("public : "); child.getName();
        System.out.printf("protected : "); child.getMoney();
//        child.getHouse();
        System.out.printf("default : "); child.getNameAndMoney();
    }
}
class Parent{
    public String name = "parent";
    private int money = 30;
    private String house = "seoul";

    public void getName(){
        System.out.println(this.name);
    }
    protected void getMoney(){
        System.out.println(this.money);
    }
    private void getHouse(){
        System.out.println(this.house);
    }
    void getNameAndMoney(){
        System.out.println(this.name + ", " + this.money);
    }
}
class Child extends Parent {

}
//    [출력]
//    public : parent
//    protected : 30
//    default : parent, 30

 따라서 상속관계에서 내가 원하는 값을 상속시키거나 상속시키지 않으려면 접근 제어자를 활용하여서 컨트롤 해줘야한다. (default는 같은 패키지 내에 있을때는 사용가능하다.)

 

포함관계(~은 ~을 가지고 있다.)와 상속관계(~은 ~이다.)를 구분지어서 사용해야한다.

 상속관계를 사용하는데 있어서 포함관계를 사용하는지 상속관계를 사용하는지에 대한 명확한 구분이 필수적으로 필요하다. Circle(원)과 Point(중앙 점 위치)가 있을 때 이 둘은 상속관계일까 포함관계일까?

//포함관계
class Circle {
	Point c = new Point();
	int r;
}
//상속관계
class Circle extends Point {
	int r;
}

 언뜻보면 둘다 맞는말 같기는 한데 제목에 적어놓은has -a와 is -a를 대입해서 말해보자

  • 원은 원점을 가지고 있다. (O)
  • 원은 원점이다. (X)

 상속관계가 말이 많이 안되지 않은가... 클래스간의 관계는 항상 그 목적에 맞게 해야하며 지금처럼 딱 상황에 맞게 떨어지지는 않지만 보다 더 클래스간의 관계가 명확해지는 방향대로 설계를 해야한다.

 추가로 그럼 상속관계가 되는 경우만 잠깐 알아보자

//포함관계
class Circle {
	Shape s = new Shape();
}
//상속관계
class Circle extends Shape {
}
  • 원은 도형을 가지고 있다. (X)
  • 원은 도형이다. (O)

 비로소 이런 is -a의 관계가 되었을 때 명확한 상속 클래스의 관계가 되는 것이다.

 

부모 클래스는 여러 개의 자식 클래스를 가질 수 있지만 자식 클래스가 여러 개의 부모 클래스를 가지지는 못한다.

 사람일 때도 한 부모에 여러 자식이 생기지만 자식에게는 한 부모만 있듯이 상속에서 단일 상속과 다중 상속에 대한 규칙이 엄격하다.

class Child extends Parent1, Parent2 {	// 부모는 하나만 허용된다.

}

 왜 부모는 하나만 허용이 되는가는 상속할 때 우리가 자식 클래스에서 상속을 사용하는 과정을 생각해보면 된다.

package Week6;

public class Exam2 {
    public static void main(String[] args) {
      Child child = new Child();  
      child.getMoney();
    }
}

class Parent1 {
	private int money;

	public void getMoney() {
		System.out.println(this.money);
	}
}
class Parent2 {
	private int money;

	public void getMoney() {
		System.out.println(this.money);
	}
}
class Child extends Parent1, Parent2 {
  
}

 다중 상속을 했을 때 Parent1 클래스와 Parent2 클래스 모두 getMoney() 메소드를 사용하였고 Child는 그 둘을 상속받았다고 치자. 그럼 child 객체에서 getMoney()를 사용할 때 누구의 것을 상속받아서 사용하는가? 이름이 같아서 전혀 구분할 수 없다. 다중 상속을 허용했을 때 하위 클래스에서 이를 사용하기 위한 코드를 복잡하게 만들고 에러가 발생할 확률이 올라가기 때문에 이를 금지하는 것이다.

 단 interface의 경우 override를 통해 기능에 대한 선언만 해주면 되기 때문에 interface로 다중상속을 하더라도 같은 이름의 메소드가 기능적으로 충돌하여 오류가 발생할 이유가 없다. 따라서 interface가 추가되었을 때는 다중 상속을 허용한다.

 

자식 클래스 생성자 호출 시 부모 클래스 생성자도 함께 초기화 된다.

 자식 클래스의 생성자가 호출되면 동시에 부모 클래스의 생성자도 같이 초기화된다. 어찌보면 당연한 이야기이다. 자식 클래스가 선언이 되었을 때 부모 클래스의 메소드나 변수 또한 사용이 될 여지가 있기 때문에 같이 초기화주어야 하는 것이다. 호출이 진짜 되는지 일단 보자.

package Week6;

public class Exam1 {
    public static void main(String[] args) {
        Child child = new Child();
    }
}
class Parent{
    public Parent() {
        System.out.println("parent");
    }
}
class Child extends Parent {
    public Child() {
      //super(); 생략 > 자바 컴파일러가 알아서 생성
        System.out.println("child");
    }
}
//    [출력]
//    parent
//    child

 함께 생성자가 호출된다는 건 알았으니 확인해야하는 것은 "무슨 클래스가 생성자가 먼저 호출이 되는가"이다. 항상 슈퍼(최상위)클래스부터 내려오면서 생성자가 호출된다. 자식 클래스에서 부모 클래스를 사용하는 경우가 있을 때 생성자가 이전에 만들어져있어야 하는 것이기 때문에 이런 상황이 발생한다.

 그럼 의문점은 과연 어떻게?인데 일전에 생성자를 공부할 때 기본생성자를 굳이 만들지 않아도 자바에서 컴파일 시에 기본생성자를 만들어준다는 것을 이야기했었다. 그와 마찬가지로 자바는 상속을 받았다면 상속된 생성자에 부모 클래스의 생성자를 호출할 수 있는 명령어인 super();를 자동으로 붙이는데 이 명령어가 결국 부모 클래스의 생성자를 호출하는 것과 똑같은 역할을 한다. this();를 사용하는 것과 같다.

 

 단, 이 과정에서 주의해야할 것이 있다. 본 상속의 특징은 결국 자식 클래스의 생성자는 부모 클래스의 생성자에 상속되었다라고 보면 되는데 그럼 컴파일러에서 자동으로 super를 추가해주었을 때 자식 클래스의 생성자 형태는 기본생성자든 묵시적, 명시적 생성자든 상관이 없을까?(매우 상관있다.)

 자바가 컴파일 시에 super();을 자동으로 선언해주는 것은 부모 클래스의 생성자인데, 부모 클래스의 생성자가 선언이 되지 않았을 때는 자바 컴파일러가 기본 생성자를 주기 때문에 super()에는 기본 생성자가 들어가있다.

 하지만 만약 부모클래스의 생성자가 명시적 생성자일 경우에는 이를 고려하여서 자바 컴파일러가 자동으로 생성해주는 것에 의존하지않고 신경 써야한다다. 아래 코드를 보자.

class Parent{
    private String name;

    public Parent(String name) {	//부모 클래스가 명시적 생성자를 가짐
        this.name = name;
    }
}
class Child extends Parent {
    private String name;
    private int money;

    public Child() {	//컴파일 에러

    }
    public Child(String name, int money) {	//컴파일 에러
        this.name = name;
        this.money = money;
    }
}

 자바 컴파일러가 super()를 자동으로 생성해주었던 이전과는 다르게 현재 Parent 클래스에는 생성자로 기본 생성자가 아닌 명시적 생성자가 존재한다. 따라서 이를 상속받은 Child 클래스는 각 클래스마다 부모의 명시적 생성자에 맞는 super 생성자를 넣어주어야 한다. 이는 아래에서 super 키워드를 공부하면서 해결하는 방법에 대해 더 자세하게 알아보자.

 

부모 클래스로부터 상속받은 자식 클래스를 상속받는 클래스가 존재할 수 있다.

 조부모클래스 - 부모클래스 - 자식클래스로 단일 상속을 지켜만 준다면 계속해서 클래스를 줄줄이 상속할 수 있다.

class GrandParent {
    
}
class Parent extends GrandParent {
    
}
class Child1 extends Parent {
    
}
class Child2 extends Parent {
    
}

 

부모 클래스를 새롭게 정의해서 자식 클래스에서 사용할 수 있다.

 overriding을 알고 있으면 이해하기 쉽다. 부모 클래스에 존재하는 메소드를 자식 클래스에서 그대로 사용해도 되지만 자식 클래스 안에서 그 메소드를 재정의해서 사용하기도 한다. 예제 코드만 두고 밑에 메소드 오버라이딩에서 다시 다루도록 한다.

package Week6;

public class Exam1 {
    public static void main(String[] args) {
        Child child = new Child();
        child.test();
    }
}
class Parent {
    void test() {
        System.out.println("parent");
    }
}
class Child extends Parent {
    @Override
    void test() {
        System.out.println("child");
    }
}
//    [출력]
//    child

 

super 키워드

 클래스에서 멤버변수(클래스,인스턴스 변수)와 지역변수를 this 키워드로 구분했듯이 상속에서는 자식 클래스에서 상속받은 부모클래스와 구분할 때 super 키워드를 사용한다. 즉, 부모클래스와 중복되는 변수, 메소드명을 구별할 때 super를 사용하는 것이다.

package Week6;

public class Exam1 {
    public static void main(String[] args) {
        Child child = new Child();
        child.getPrint();
    }
}
class GrandParent{
    void getPrint() {
        System.out.println("grand parent");
    }
}
class Parent extends GrandParent {
    int age = 10;
    void getPrint() {
        System.out.println("parent");
    }
}
class Child extends Parent {
    int age = 11;
    void getPrint() {
        System.out.println("age : " + super.age);
        super.getPrint();
    }
}
//    [출력]
//    age : 10
//    parent

 단 super 키워드는 바로 위에 상속된 클래스를 가르키기 때문에 super를 사용한다고 해서 최상위 부모 클래스를 바로 받아오진 못한다.

 그리고 super는 클래스 내부 생성자를 사용했던 this();와 같이 super();로 부모의 생성자를 받아오기도 한다.

 생성자를 클래스에서 선언하지 않았을 때는 기본 생성자가 자바 컴파일러가 만들어주는데 상속관계에 있을 경우 부모 클래스의 생성자도 동시에 만들어 준다.

class Parent {
    public Parent() {
    }
}
class Child extends Parent {
    public Child() {
        super();
    }
}

 이런 느낌이다. 단 부모 클래스가 기본 성자나 묵시적 생성자가 아닌 명시적 생성자인 경우 자손 클래스에도 super를 통해 동일한 명시적 생성자를 만들어주어야한다. 필요한 부모클래스의 super()를 사용해서 자신이 사용하는 모든 생성자 (자동으로 생성해주는 기본 생성자의 경우도 직접 선언해야함)에 부모클래스의 생성자를 추가해주어야한다.

 super()의 위치는 항상 자식클래스 생성자 내부의 최상단에 존재하여야 한다.

class Parent {
    int age;
    public Parent(int age) {
        this.age = age;
    }
}
class Child extends Parent {
    int age;
    public Child(int age) {
        super(age);
        this.age = age;
    }
}

 

메소드 오버라이딩

 상속 관계에 있을 때 부모 클래스의 메소드를 자식 클래스서 다시 정의하는 것을 말한다.

사전적 의미 : ~위에 덮어쓰다(overwrite)

 단 오버라이딩(Overriding)을 하기 위해서는 몇가지 정해진 규칙이 필요하다.

  • 메소드의 이름이 같아야 한다.
  • 매개변수가 같아야 한다.
  • 반환타입이 같아야 한다.
  • 접근 제어자는 부모 클래스보다 좁은 범위로 변경할 수 없다.
  • 부모 클래스의 메소드보다 많은 Exception을 선언할 수 없다.
  • 부모 클래스의 인스턴스 메소드를 static 메소드로, 부모 클래스의 static 메소드를 인스턴스 메소드로 오버라이딩 할 수 없다.

 어떻게 보면 당연한 이야기들이다. 부모 메소드를 받아서 사용하기 때문에 부모 메소드보다 넓게 설정할 수 없는 것이다.

 주의해야할 점은 가장 마지막 조건이라고 생각한다. 놓치기 쉬운 부분이고 이해하기 힘들었던 부분이다. 부모 클래스의 인스턴스 메소드를 static 메소드로 받을 때는 static 메소드가 메모리에 저장되는 시점이 인스턴스보다 빠르기 때문에 안된다고 생각했었는데 문제는 부모의 static 메소드를 인스턴스 메소드로 받을 수 없다는 것이다. 왜냐하면 static은 compile time에서 method area에 올라가기 때문에 클래스 그 자체라고 보기 때문에 static 메소드를 불러서 사용을 할 수는 있겠지만 본인 클래스에 묶여있기 때문에 상속이 되지 않는 것이다.

 한마디로 인스턴스 - 인스턴스 오버라이딩은 되지만 static 오버라이딩은 정의 되지않는다.

 

 Overriding은 위에 말대로 메소드 이름과 매개변수가 같아야하기 때문에 메소드 이름이 조금이라도 틀릴경우 Overriding이 성립되는 것이 아니다. 따라서 @Overridng 어노테이션을 통해 확인하거나 IDE에서 자동 선언하도록 도움을 받은 것이 안전하다.

package Week6;

public class Exam1 {
    public static void main(String[] args) {
        Child child = new Child();
        System.out.println(child.getMoney());
    }
}
class Parent {
    int money = 10;
    int getMoney() {
        return money;
    }
}
class Child extends Parent {
    int minus = 5;

    @Override
    int getMoney() {
        return money - minus;
    }
}
//    [출력]
//    5

 

 

다이나믹 메소드 디스패치(Dynamic Method Dispatch)

 메소드 디스패치를 알기 전 다형성(Polymorphism)을 먼저 알 필요가 있다.

다형성이란 동일한 네이밍을 가지고 여러 형태의 동작을 행동하는 것을 말한다.

 우리가 '+' 연산자를 사용한다고 했을 때 int에서는 순전히 값과 값을 더한다는 의미로, String에서는 문자열 두 개를 연결한다는 의미로 사용한다. 이처럼 한가지 네이밍으로 상황에 맞게 여러가지 동작을 취할 수 있도록 하는 것이 다형성이다.

 이를 자바에서 다형성을 활용한 코드가 생기는데 이때 발생하는 것이 다이나믹 메소드 디스패치이다.

 

 메소드 디스패치란 어떤 메소드를 호출할지 결정하여 실제 실행시키는 과정을 말한다. 메소드 디스패치는 앞서 말한대로 실행되는 위치에 따라서 크게 두가지로 결정된다.

  1. Static Method Dispatch (정적 메소드 디스패치)
  2. Dynamic Method Dispatch (동적 메소드 디스패치)

 

Static Method Dispatch

 말그대로 정적인 메소드 디스패치로 앞에서 사용했던 메소드 오버라이딩이 정적 메소드 디스패치에 해당한다. 어떤 메소드가 사용이 되는지 정확하게 알고 사용을 하며 이를 런타임에서 확인하는 것이 아닌 static, 컴파일 타임에서 확인하여 사용하기 때문에 static method dispatch라 한다.

package Week6;

public class Exam2 {
    public static void main(String[] args) {
        Parent parent = new Parent();
        Child child = new Child();

        parent.printOut();
        child.printOut();
    }
}
class Parent {
    void printOut() {
        System.out.println("Parent");
    }
}
class Child extends Parent {
    @Override
    void printOut() {
        System.out.println("Child");
    }
}
//    [출력]
//    Parent
//    Child

 지금처럼 참조 클래스로 생성한 참조변수가 가르키는 방향이 정확하고, 무엇을 사용할지 고민할 상황이 없기 떄문에 명확하게 내가 사용한 메소드에 대해서 컴파일러가 확인하고 메소드를 사용하는 모습니다.

 

Dynamic Method Dispatch

 그럼 모를때는? instance of를 배울때를 생각해보자 instance of가 true일 때는 클래스 형변환이 가능하다 했었다. 즉 Parent 클래스는 Child 클래스를 상속해주고있기 때문에 Child > Parent 형변환이 가능하다. 이 이야기를 하는 이유는 생성한 인스턴스가 참조하는 참조변수의 타입에 따라 그 활용 범위가 다르기 때문이다.

Parent child = new Child();

 지금의 이 경우에 child 객체는 클래스의 어디까지 사용할 수 있을까? Child가 Parent를 상속받았으니 Parent, Child 전부 다쓸수있을까? 결론은 Parent 클래스에 대한 변수와 메소드만 사용할 수 있다.

package Week6;

public class Exam2 {
    public static void main(String[] args) {
        Parent child = new Child();
        child.printOut();
//        System.out.println(child.childAge);
        System.out.println(child.parentAge);
    }
}
class Parent {
    int parentAge;
    void printOut() {
        System.out.println("Parent");
    }
}
class Child extends Parent {
    int childAge;
    @Override
    void printOut() {
        System.out.println("Child");
    }
}
//    [출력]
//    Child
//    0

 어찌보면 당연한 이야기이다. 조상 클래스를 참조변수로 사용했을 때 자손 클래스 객체를 만든다고해서 조상 클래스가 자손 클래스 객체를 다 포함하지 않으니 본인 클래스에서의 변수와 메소드를 객체로 사용할 수 밖에 없다. (그럼 자손 클래스를 참조 변수로 사용하고 조상 클래스를 객체화하면 조상 클래스만 있기 때문에 자손 클래스 객체를 포함하지 않아 허용되지 않는다.)

 이렇게 활용되는 것이 자바 메소드에서의 다형성이다. 객체화를 했으나 내가 선언한 참조 클래스라는 틀에 따라서 참조 변수가 가르키는 내용이 달라지게 된다.

(비유를 자주하는건 별로 안좋아하는데 과자틀이 여러개 있으면 내가 찍은 과자틀에 따라서 내용물이 달라지게 되는것과 비슷하다)

 

 개념은 알겠는데 코드가 이상하다. 개념대로라면 우리는 Child를 객체화했고, Parent를 참조 클래스로 사용을 했으니까 Parent 클래스의 변수와 메소드만 사용할 수 있다했다. 근데 출력은? Parent가 아닌 Override인 Child다. ???

 여기서 동작되는 것이 Runtime에서 메소드 디스패치된 Dynamic Method Dispatch이다. 컴파일 과정에서 이미 조상 클래스의 메소드를 확인했으나 런타임에서 자손클래스 오버라이딩된 값을 확인하여 사용한 것이다.

 다시 한번 강조하지만 컴파일 과정에서는 Parent를 사용한다고 했으나 Child의 메소드를 쓴 것이다. 아래 바이트 코드를 보자

// class version 52.0 (52)
// access flags 0x21
public class Week6/Exam2 {

  // compiled from: Exam2.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LWeek6/Exam2; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 5 L0
    NEW Week6/Child
    DUP
    INVOKESPECIAL Week6/Child.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 6 L1
    ALOAD 1
    INVOKEVIRTUAL Week6/Parent.printOut ()V
   L2
    LINENUMBER 9 L2
    RETURN
   L3
    LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
    LOCALVARIABLE child LWeek6/Parent; L1 L3 1
    MAXSTACK = 2
    MAXLOCALS = 2
}

 LINENUMBER 5 L0 에서 우리는 일단 Child 객체를 생성한 후 다형성된 것이기 때문에 LINENUMBER 6 L1에서 child.printOut();은 child의 것이 아닌 parent의 메소드를 사용한 것을 확인할 수 있다.

INVOKEVIRTUAL Week6/Parent.printOut ()V

 즉, 컴파일 과정에서는 자바의 약속대로 본인이 따르는 메소드를 사용했으나 런타임 과정에서 자손 클래스의 override된 메소드를 사용하는 것, 이것이 다이나믹 메소드 디스패치 이다. 컴파일을 했더라도 본인이 override된 메소드를 출력한다.

 

추상 클래스(abstract class)

 기존의 클래스가 완전히 "완성"된 객체를 만드는 설계도라면 추상 클래스는 아직 "미완성"된 설계도라고 할 수 있다. 클래스의 모든 것이 미완성이라는 것이 아니라 클래스 내부에 미완성인 메소드(추상 메소드)가 포함되었을 때 이를 추상 클래스라고 한다.

 미완성이기 때문에 추상클래스 그 자체로만으로는 역할을 다 할 수 없지만, 새로운 클래스를 만들어 추상클래스를 상속받았을 때 미완성인 메소드를 활용해서 자손 클래스를 만들어낼 수 있다. 즉 앞서 배운 Overriding을 통해 자손 클래스에서 그 메소드를 명확히하여 사용하는 것이다.

 따라서 추상클래스는 본인 클래스에서 미완성된 메소드가 있기 때문에 객체화를 할 수 없다. 다른 자손 클래스에서 이를 상속받아 사용하는 방식의 기생하여 사용할 수 밖에 없는것이다.

 단, 사용하는데있어서 주의해야할 점이 존재한다.

package Week6;

public class Exam4{
    public static void main(String[] args) {
        Parent child = new Child();
        child.getMoney();

        Parent parent = new Parent() {
            @Override
            void getMoney() {
                System.out.println("재정의");
            }
        };
        parent.getMoney();
    }
}
abstract class Parent {
    String name;
    int money = 20;
    abstract void getMoney();
}
class Child extends Parent {

    @Override
    void getMoney() {
        System.out.println(super.money);
    }
}
//    [출력]
//    20
//    재정의

 Child 참조변수를 Child 객체화를 통해서 받는건 재정의를 했으니까 알겠다.

 근데 Parent 참조변수에 Child 객체화를 받아서 하는건 될까? 당연히 된다. 이는 참조변수에 들어가는 과정을 생각하면 된다. Child 객체화를 하였을 때 이는 메소드를 재정의한 것까지 포함해서 객체화가 된다. 이때 객체된 객체를 참조변수 Parent에 들어갈 때 Parent 참조변수에 포함되는 메서드와 변수만 객체화값이 들어가는데 비록 getMoney()는 정의가 되지 않았지만 Child 객체가 Parent 참조변수에 들어가는 과정에서 Child 객체에서 정의한 getMoney()가 들어가기 때문에 컴파일에러없이 사용이 되는 것이다.

 굳이 추상메소드를 상속없이 상용하고 싶다면 두번째 예처럼 직접 객체화할 때 메소드 재정의를 해주면 사용 가능하다.

 

 

final 키워드

 변수에서도 같은 의미로 사용했던 final 이다. 상속하지 못함, 변경하지 못함이라는 기본 속성을 가지고 사용하는 키워드이다.

 변수, 메소드, 클래스 모두에서 사용가능하며 각각에 사용에 대한 특징이 있다.

 

변수

package Week6;

public class Exam3 {
    public static final int FINAL_NUMBER_1 = 50;
//    private static final int FINAL_NUMBER_2;
    private final int FINAL_NUMBER_3;
    public Exam3(int FINAL_NUMBER_3){
        this.FINAL_NUMBER_3 = FINAL_NUMBER_3;
    }
    public static void main(String[] args) {
//        FINAL_NUMBER_1 = 60;
        System.out.println(FINAL_NUMBER_1);
        Exam3 exam3 = new Exam3(10);
        System.out.println(exam3.FINAL_NUMBER_3);
    }
}
//    [출력]
//    50
//    10

 final을 붙인 변수를 우리는 상수라고 부르며 상수는 네이밍 컨벤션이 존재한다. 변수명은 항상 모두 대문자로 작성하며, 변수명의 연결은 언더바(_)를 사용해서 구분한다.

 final으로 지정한 상수는 선언과 동시에 값을 변경하지 못하기 때문에 선언 후 값 변경이 있어서는 안되며 선언과 동시에 변수 초기화를 진행해주어야 한다.

 가령 FINAL_NUMBER_3과 같이 변수초기화를 하지 않았더라도 생성자에서 이를 반드시 초기화하도록 만들어주는 형태로 이루어진다면 사용가능하다.

 

메소드

 메소드에서 final을 사용한다는 것은 이 메소드를 더이상 변경하지 않겠다는 의미이다. 즉, override를 허용하지 않는다.

package Week6;

public class Exam4 {
}
class Parent {
    final void getName() {
        System.out.println("Parent");
    }
}
class Child extends Parent {
    @Override
    void getName() {//컴파일에러
        System.out.println("Child");
    }
}

 따라서 아래 자손클래스에서 상속을 받았지만 getName 메소드는 컴파일 에러가 나서 Override할 수 없음을 알 수 있다. 어떻게보면 다른 사용방법으로 쓰는 private도 해당 클래스 내에서만 쓰도록 만들어 결과적으로 자식 클래스에서 그 클래스를 못쓰게 하는것과 같다.

 

클래스

 클래스에서 final을 사용한다는 것은 이 클래스를 상속시켜주지 않겠다는 의미이다.

final class Parent {
}
class Child extends Parent {//컴파일에러
}

 

Object 클래스

자바의 최상위 클래스란 무엇인가?

 의 답변이다. 모든 클래스의 최상위 클래스로 우리가 상속을 선언하지 않았더라도 자바에서 알아서 선언해주는 클래스이다.

 모든 클래스의 최상위 클래스라고 하는데 얘는 왜 Interface가 아니라 Class일까? 모든 클래스가 다쓰면 Interface로 생성해도 되지않냐? 는 Object 클래스의 메소드들을 먼저 살펴보면 알게된다.

Type Method Description
Object Clone() Object를 복사한 Object를 생성하여 반환한다.
boolean equals(Object obj) obj와 같은 지("to equals")를 판단한다.
Class getClass() 객체의 클래스명을 반환한다.
int hashCode() 객체의 해시코드 값을 반환한다.
void wait() 쓰레드를 일시적으로 중지한다
void notify() wait된 해당 쓰레드를 다시 실행한다.
void nofifyAll() wiat된 모든 쓰레드를 다시 실행한다.
String toString() 객체의 String 값을 반환한다.
protected void finalize() Deprecated.(더이상 사용되지 않음)

[출처] docs.oracle.com

 전부 사용을 하기 위한 기능 구현에 해당한다. 최상위 클래스를 Interface로 구현하였다면 아래 클래스들을 모두 하나하나 구현해야한다는 소리이기 때문에 이걸 하나하나 Override해서 기능 구현을 해야하는 끔찍한 사태가 일어난다. 따라서 최상위인 Object는 클래스로 구현이 된 것이다.

 

 

[참고 자료]

[자바의 정석]

 

'JAVA > whiteship-livestudy' 카테고리의 다른 글

인터페이스  (0) 2021.04.14
패키지  (0) 2021.04.09
클래스  (0) 2021.03.24
제어문  (0) 2021.03.17
연산자  (0) 2021.03.16
댓글
최근에 올라온 글
Total
Today
Yesterday