읽기 전
- 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
- 개인적으로 사용해보면서 배운 점을 정리한 글입니다.
문제 제기
오버로딩과 오버라이딩 개념은 굉장히 자주 쓰이고 중요하다고 여겨진다. 그러나 어떤 경우에 제약이 걸리고 예외에 대한 내용은 조금 더 찾아봐야 알 수 있다. 배우는 김에 정리해보려 한다.
오버로딩(overloading)
클래스 내에 이미 동일한 이름을 갖는 메소드가 있더라도 매개변수, 리턴 타입을 다르게 하여 새롭게 메소드를 정의하는 방식이다. 컴파일타임 다형성(compiletime polymorphism)/정적 바인딩(Static Binding)에 해당하며 이미 JVM에 로드될 적에는 컴파일러에 의해 메소드 시그니처 기준으로 전부 구분되므로 JVM에서 별도로 메소드 식별 작업을 수행하지 않는다. 컴파일 수행하여 ".class" 파일 생성 시 overloading된 메소드는 메소드 이름이 동일하지만 메소드 이름 + 매개변수를 바탕으로 메소드 시그니처를 생성하여 저장한다. 따라서, 클래스 메타 정보에서 메소드들이 구분된 상태로 저장되기 때문에 컴파일이 완료되면 사실상 별개의 메소드로 간주한다.
오버로딩을 수행하면 다른 매개변수를 갖지만 동작이 비슷한 메소드의 이름을 절약할 수 있고 매개변수 별로 조건문이 아니라 메소드를 완전히 구분하는 이점을 갖고 있다.
오버로딩의 조건
- 메소드 이름이 동일해야 한다.
- static 메소드도 오버로딩이 가능하다.
- 매개변수는 반드시 변화가 있어야 하며 리턴 타입은 자유롭다.
- 매개변수는 같고 리턴 타입이 다르면 오버로딩이 성립하지 않는다.
- 메소드 시그니처는 메소드의 이름과 매개변수로 작성되기에 리턴타입만 다르면 시그니처가 같아서 컴파일 오류가 발생한다.
- 오버로딩된 메소드는 매개변수에 의해서만 구분이 된다.
오버로딩 예
class Test {
public static void main(String[] args) {
Test test = new Test();
}
public void print(int val) {
System.out.println("value is = " + val);
}
public void print(String val) {
System.out.println("value is = " + val);
}
public void print(int val, String strData) {
System.out.println("value is = " + val + ", " + strData);
}
public void print(String strData, int val) {
System.out.println("value is = " + strData + ", " + val);
}
}
메소드 시그니처는 메소드 이름 + 매개변수(매개변수 순서도 다르면 다르게 인식)이므로 위와 같이 다른 메소드 시그니처를 갖게끔만 보장하면 overloading이 가능하다.
오버라이딩(overriding)
부모 클래스를 상속받은 자식 클래스에서 부모 클래스의 메소드와 메소드 이름, 매개변수의 개수와 순서, 리턴 타입이 동일하지만 메소드의 동작을 다르게 선언하는 방식이다. 런타임 다형성(runtime polymorphism)/동적 바인딩(Dynamic Binding)에 해당하며 컴파일 시 모두 동일한 시그니처를 가지고 있다. 그러므로 컴파일러는 메소드를 호출한 객체가 오버라이딩한 메소드를 가지고 있는지 알 수 없다. 따라서 메소드 호출 시 변수에 할당된 객체가 오버라이딩 메소드를 정의하지 않았다면 부모 클래스의 메소드를 호출하고 정의했다면 할당된 객체 클래스의 오버라이딩 메소드를 호출하는 등 JVM에게 별도의 동작을 요구한다.
오버라이딩을 수행하면 다른 매개변수를 갖지만 동작이 비슷한 메소드의 이름을 절약할 수 있고 매개변수 별로 조건문이 아니라 메소드를 완전히 구분하는 이점을 갖고 있다.
오버라이딩의 조건
- 메소드 이름이 동일해야 한다.
- static 메소드는 오버라이딩이 불가능하다.
- 매개변수의 개수, 타입, 순서가 같아야 한다.
- 리턴 타입은 원칙적으로 동일해야 하나 Java 5 이후로는 예외가 존재한다.
- 리턴 타입이 void인 경우 상속받는 클래스의 메소드 리턴 타입도 void여야 한다.
- 리턴 타입이 기본 자료형일 경우 상속받는 클래스의 메소드 리턴 타입은 동일해야 한다.
- 리턴 타입이 참조 자료형일 경우 상속받는 클래스의 메소드 리턴 타입은 동일하거나 부모 클래스의 메소트 리턴 타입의 하위 클래스여야 한다.
- 접근 제어자는 자식 클래스 메소드가 부모 클래스 메소드와 같거나 보다 더 공개되는 방향으로 변경되어야 한다.
오버라이딩 예
class SubTest1 {
public void printInt(int val) {
System.out.println("subTest1 class | value is = " + val);
}
public Object printval(Object val) {
System.out.println("subTest1 class | value is = " + val.toString());
return val;
}
void printString(String str) {
System.out.println("subTest1 class | value is = " + str);
}
}
class SubTest2 extends SubTest1 {
@Override
public void printInt(int val) {
System.out.println("subTest2 class | value is = " + val);
}
@Override
public String printval(Object val) {
System.out.println("subTest2 class | value is = " + val.toString());
return val.toString();
}
@Override
public void printString(String str) {
System.out.println("subTest2 class | value is = " + str);
}
}
리턴 타입이 참조 자료형일 경우 자식 클래스의 메소드의 리턴 타입은 부모 클래스 메소드의 리턴 타입의 하위 클래스 자료형을 리턴해도 성립한다.
static 메소드 오버라이딩?
class SubTest1 {
public static void printInt(int val) {
System.out.println("subTest1 class | value is = " + val);
}
}
class SubTest2 extends SubTest1 {
public static void printInt(int val) {
System.out.println("subTest2 class | value is = " + val);
}
}
public class Test {
public static void main(String[] args) {
// SubTest1 클래스에 SubTest2 클래스 객체 할당
SubTest1 subTest1 = new SubTest2();
subTest1.printInt(1);
SubTest2 subTest2 = new SubTest2();
subTest2.printInt(1);
}
}
subTest1 class | value is = 1
subTest2 class | value is = 1
위 코드는 static 메소드인 printInt를 override한 뒤 호출한 결과이다. 분명 SubTest2 클래스 객체를 할당했으니 오버라이드하게끔 작성된 메소드 호출 결과로는 SubTest2 클래스의 printInt()의 결과만 출력되어야 했으나 변수가 갖는 클래스 값에 따라 결과가 달라짐을 볼 수 있다. 이렇듯 오버라이딩이 성립하려면 우측의 변수에 할당된 클래스 객체에 따라 호출할 메소드가 결정되어야 하는데 좌측의 컴파일 시 결정된 변수 클래스가 갖는 메소드를 호출하는 문제가 발생하여 오버라이딩이 성립하지 않는다. 오버라이딩이 성립하지 않았기에 이 현상을 오버라이딩이라 부르지 않고 메소드 하이딩(Hiding)이라 부른다. 에러를 출력하지 않으나 다형성을 해칠 수 있으므로 이런 방식의 코딩은 지양하는 게 좋다.