[JAVA] 객체지향 프로그래밍 - 오버라이딩과 조상 클래스 멤버 지정 - suepr

2023. 11. 29. 15:23프로그래밍 언어/Java

1. 오버라이딩

조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것을 오버라이딩이라 한다. 상속받은 메서드를 자신의 클래스에 맞게 변경해야 하는 경우 사용한다. 앞서 배운 오버로딩과 용어가 비슷해서 헷갈릴 수 있는데, 오버로딩은 한 메서드를 매개변수로 구분해서 여러 개로 작동시키는 기술이다. 꼭 구분하자. 

 

오버라이딩 예시를 보자. 2차원 좌표를 표현하는 Point클래스를 조상으로 하는 3차원 좌표를 표현하기 위한 Point#D클래스가 있다고 하자.

class Point{
    int x;
    int y;

    String getLocation(){
        return "x : " + x + ", y : " + y;
    }
}

class Point3D extends Point{
    int z;

    String getLocation(){
        return "x : " + x + ", y : " + y + ", z : " + z;
    }
}

이처럼 getLocation() 메서드를 오버라이딩시켜 Point3D에 맞게 변형하였다. 오버라이딩은 조상의 메서드와 선언부가 완전히 일치해야 한다. 그래서 오버라이딩이 성립하기 위해서는 다음의 조건을 만족해야 한다.

  • 이름과 매개변수가 같아야 한다.
  • 반환타입이 같아야 한다.
  • 접근 제어자는 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.
    • 만약 조상 클래스의 메서드가 protected라면 이를 오버라이딩하는 자손 클래스의 메서드는 protected나 public이어야 한다. 접근 제어자를 넓은 것에서 좁은 것 순으로 나열하면 public, protected, default, private이다.
  • 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.
    • 특히 Exception은 모든 예외의 최고 조상이므로 가장 많은 개수의 예외를 선언한 것과 같다. 이 점을 주의하자.
  • 인스턴스 메서드를 static 메서드로 또는 그 반대로 변경할 수 없다.
    • 추가로 조상 클래스에 정의된 static 메서드를 자손 클래스에서 똑같은 이름의 static 메서드로 정의할 수 있다. 하지만 static 메서드는 클래스별로 구분되고 호출도 클래스이름. 메서드이름()으로 하기에 이는 오버라이딩이 아니라 각 클래스에 별개의 static메서드를 정의한 것이다.

2. 조상 클래스의 멤버 참조하기 - super

멤버 변수와 지역 변수의 이름이 같을 때 this를 통해 구별하듯이 상속받은 멤버와 자신의 멤버가 이름이 같을 때는 super를 붙여 구별할 수 있다. this와 마찬가지로 super역시 인스턴스 메서드에서만 사용할 수 있다. 예시로 super와 this를 차이를 구분해 보자.

 

예제 1

public class SuperTest
{
  public static void main (String[]args)
  {
    Child c = new Child ();
    c.method ();
  }
}

class Parent
{
  int x = 10;
}

class Child extends Parent
{
  void method ()
  {
    System.out.println ("x=" + x);
    System.out.println ("this.x=" + this.x);
    System.out.println ("super.x=" + super.x);
  }
}

예제 1의 결과는 당연히 모두 10이다. 조상 클래스로부터 상속받은 x 또한 Child의 멤버 변수이므로 this.x의 값이 10이 된다.

 

예제 2

public class SuperTest2
{
  public static void main (String[]args)
  {
    Child c = new Child ();
    c.method ();
  }
}

class Parent
{
  int x = 10;
}

class Child extends Parent
{
  int x = 20;
  void method ()
  {
    int x = 30;
    System.out.println ("x=" + x);
    System.out.println ("this.x=" + this.x);
    System.out.println ("super.x=" + super.x);
  }
}

실행 결과는 차례로 30, 20, 10이다. 이처럼 super를 통해 조상클래스의 멤버 변수에 접근할 수 있다. 변수만이 아니라 메서드에도 super를 써서 호출할 수 있는데, 주로 조상 클래스의 메서드를 자손 클래스에서 오버라이딩 한 경우 super를 사용한다. 아까 Point 예시에서도 super를 통해 더 간결하고 좋은 코드를 만들 수 있다.

class Point{
    int x;
    int y;

    String getLocation(){
        return "x : " + x + ", y : " + y;
    }
}

class Point3D extends Point{
    int z;

    String getLocation(){
        return super.getLocation() + ", z : " + z;
    }
}

3. 조상 클래스의 생성자 호출하기 - super()

this()와 마찬가지로 super()도 super와는 전혀 다른 생성자이다. this()는 같은 클래스의 한 생성자에서 다른 생성자를 호출할 때 사용했다면, super()는 조상 클래스의 생성자를 호출하는 데 사용된다. 자손 클래스가 인스턴스를 생성하면 조상 클래스의 멤버들의 초기화가 수행되어야 하기 때문에 자손 클래스의 생성자에서 조상 클래스의 생성자가 호출되어야 한다. 지금까지는 생성자 첫 줄에 자동으로 컴파일러가 super();를 추가해 줘서 오류가 없었지만 오류가 생길 수 있었다. 아래의 예제를 보자.

class Point{
    int x;
    int y;
    
    Point(int x, int y){
        this.x = x;
        this.y = y;
    }

    String getLocation(){
        return "x : " + x + ", y : " + y;
    }
}

class Point3D extends Point{
    int z;
    
    Point3D(int x, int y, int z){
        this.x = x;
        this.y = y;
        this.z = z;
    }

    String getLocation(){
        return "x : " + x + ", y : " + y + ", z : " + z;
    }
}

Point3 D 메서드를 생성하면 컴파일러가 자동으로 super()를 삽입한다. 그런데 super()는 조상 클래스의 기본 생성자인 Point()를 의미한다. Point() 생성자는 존재하지 않기에 위의 예제는 에러가 발생한다. 따라서 아래와 같은 방법으로 생성자를 구성하는 편이 옳다.

Point3D(int x, int y, int z){
    super(x, y);
    this.z = z;
}

이처럼 조상 클래스의 멤버변수는 조상의 생성자에 의해 초기화되도록 구성해야 한다.


4. 패키지

패키지란 클래스의 묶음이다. 패키지를 통해 클래스 또는 인터페이스를 그룹 단위로 묶어 효율적으로 관리할 수 있다. 사실 모든 클래스는 패키지에 포함되어야 한다. 예를 들면 String클래스의 실제 이름은 java.lang.String이다. java.lang패키지에 속한 String라는 의미이다. 그래서 같은 이름의 클래스일지라도 서로 다른 패키지에 속하면 패키지명으로 구분이 가능하다.

클래스가 물리적으로 하나의 클래스파일(. class)인 것과 같이 패키지는 물리적으로 하나의 디렉터리이다. 예를 들어 java.lang.String클래스는 물리적으로 java의 서브디렉터리인 lang에 속한 String.class파일이다. 

패키지는 클래스나 인터페이스의 소스파일 맨 위에 package 패키지명;과 같이 선언할 수 있다.

 

한 편, 소스코드를 작성할 때 다른 패키지의 클래스를 사용하려면 패키지명이 포함된 클래스 이름을 사용해야 한다. 하지만 매번 패키지명을 붙여서 작성하기는 너무 불편하다. 클래스의 코드를 작성하기 전에 import문으로 사용하고자 하는 클래스의 패키지를 미리 명시해 주면 소스코드에 사용되는 클래스이름에서 패키지명은 생략할 수 있다.

예를 들면 java.util 패키지의 Date 클래스를 사용하고자 하면 import java.util.Date 혹은 import java.util.*로 사용할 수 있다. 후자로 사용하는 경우 java.util 패키지의 모든 클래스를 사용할 수 있는데, 실행 시 두 방법의 차이는 전혀 없다. 컴파일러가 import문을 통해 사용된 클래스들의 이름 앞에 패키지명을 자동으로 붙여주는 방식으로 작동하기 때문이다.

 

패키지는 개념과 사용법만 알아두고, 직접 만들어서 배포한다던가 큰 프로젝트에서 패키지 단위로 관리하는 등의 사용처가 많지만 필요할 때 알아보고 사용해도 될 것 같다.