본문 바로가기

Study/개발일지

[백엔드온라인TIL] java 학습 3일차

오버로딩(Overloading)

 

 

오버로딩(Overloading)이라는 뜻은 사전적으로 '과적하다.'라는 뜻이다. C언어에서는 함수명이 고유하게 존재해야 한다. 즉 하나의 함수가 하나의 기능만을 구현해야 한다는 것이다. 하지만 자바에서는 하나의 메소드 이름으로 여러 기능을 구현하기 때문에 '과적하다.'라는 뜻의 이름을 붙여준 것으로 보인다.

 

오버로딩의 정의는 자바의 한 클래스 내에 이미 사용하려는 이름과 같은 이름을 가진 메소드가 있더라도 매개변수의 개수 또는 타입이 다르면, 같은 이름을 사용해서 메소드를 정의할 수 있다.

 

 

오버로딩의 조건

 

 

메소드의 이름이 같고, 매개변수의 개수나 타입이 달라야 한다. 주의할 점은 '리턴 값만' 다른 것은 오버로딩을 할 수 없다는 것이다.

 

아래의 코드를 통해서 예를 들어보자.

 

<코드>

class OverloadingTest {

	public static void main(String[] args) {
		OverloadingMethods om = new OverloadingMethods();

		om.print();
		System.out.println(om.print(3));
		om.print("Hello!");
		System.out.println(om.print(4, 5));
	}
}

class OverloadingMethods {
	public void print() {
		System.out.println("오버로딩1");
	}

	String print(Integer a) {
		System.out.println("오버로딩2");
		return a.toString();
	}

	void print(String a) {
		System.out.println("오버로딩3");
		System.out.println(a);
	}

	String print(Integer a, Integer b) {
		System.out.println("오버로딩4");
		return a.toString() + b.toString();
	}

}

 

 

<결과>

오버로딩1
오버로딩2
3
오버로딩3
Hello!
오버로딩4
45

 

위 코드는 아무런 문제없이 잘 실행되고 있다. print라는 같은 이름을 가진 네개의 메소드가 매개변수의 개수와 타입를 다르게 지정하여 지정하는 것이 가능하다는 것을 보여주고 있다.

 

단, 여기서 한번 더 강조할 점은 '리턴 값'만 다르게 지정하는 것은 오버로딩할 수 없다는 것이다.

 

또한 접근 제어자도 자유롭게 지정해 줄 수 있다. 각 메소드의 접근 제어자를 public, default, protected, private으로 다르게 지정해줘도 상관없다는 것이다. 접근 제어자만 다르게한다고 오버로딩이 가능하지 않다는 것도 알아두자.

 

결국 오버로딩은 매개변수의 차이로만 구현할 수 있다는 것이다. 매개변수가 다르다면 리턴 값은 다르게 지정할 수 있다.

 

 

오버로딩을 사용하는 이유

 

 

1. 같은 기능을 하는 메소드를 하나의 이름으로 사용할 수 있다.

 

우리가 흔히 콘솔창에 텍스트를 출력할 때 사용하는 println라는 메소드를 대표적인 예로 들어볼 수 있다. 실은 이 함수가 오버로딩의 결정체이다. 우리는 println의 인자 값으로 int, double, boolean, String 등의 아주 다양한 타입의 매개변수들을 집어넣어도 우리는 그 함수들이 어떻게 실행되지는 모르지만 콘솔창에 아주 잘 출력해주는 것을 볼 수 있다.

 

이렇게 '출력하다.'라는 같은 기능을 가진 메소드들를 println이라는 하나의 이름으로 정의가 가능한 것이다.

 

2. 메소드의 이름을 절약할 수 있다.

 

이 위에서 예로 들었던 println을 생각해보자. 이 메소드를 매개변수의 종류에 따라서 다르게 지정한다고 생각해보자. printlnInt, printlnDouble, printlnBoolean 등 수많은 메소드들의 이름을 정해줘야 할 것이다. 이는 프로그래머의 입장에서는 메소드의 네이밍에 고민을 가중시킨다. 그리고 이런 이름들은 다른 곳에 사용할 가능성도 생기게 된다.

 

 

 

오버라이딩(Overriding)

 

 

부모 클래스로부터 상속받은 메소드를 자식 클래스에서 재정의하는 것을 오버라이딩이라고 한다. 상속받은 메소드를 그대로 사용할 수도 있지만, 자식 클래스에서 상황에 맞게 변경해야하는 경우 오버라이딩할 필요가 생긴다.

 

 

오버라이딩의 조건

 

오버라이딩은 부모 클래스의 메소드를 재정의하는 것이므로, 자식 클래스에서는 오버라이딩하고자 하는 메소드의 이름, 매개변수, 리턴 값이 모두 같아야 한다. 코드를 통해 좀 더 자세히 알아보자.

 

<코드>

public class OverridingTest {

	public static void main(String[] args) {
		Person person = new Person();
		Child child = new Child();
		Senior senior = new Senior();
		
		person.cry();
		child.cry();
		senior.cry();
	}
}

class Person {
	void cry() {
		System.out.println("흑흑");
	}
}

class Child extends Person {
	@Override
	protected void cry() {
		System.out.println("잉잉");
	}
}

class Senior extends Person {
	@Override
	public void cry() {
		System.out.println("훌쩍훌쩍");
	}
}

 

 

<결과>

흑흑
잉잉
훌쩍훌쩍

 

@Override는 어떤 용도인가?

 

이는 어노테이션(Annotation)이라는 것으로 직역하면 주석이라는 뜻이다. 이는 일반적인 주석과 다르게, 검증하는 기능을 한다. 여기서 사용된 @Override라는 어노테이션은 오버라이딩을 검증하는 기능을 한다. 코드상으로 검사했을 때 오버라이딩이 실제로 시행되지 않았다면 컴파일시 오류를 출력한다.

 

위 코드는 정상적으로 실행되는 것을 볼 수 있다. 부모 클래스의 메소드를 오버라이딩하는 것은 내용만을 새로 정의하는 것이므로 선언부는 부모의 것과 완벽히 동일해야 하는 것을 볼 수 있다.

 

프로그래머가 Person클래스의 cry메소드를 '흑흑'이라고 정의했다. 그런데 Child클래스와 Senior클래스를 만들면서 울음소리를 다르게 출력하고 싶은 것이다. 그래서 Chile클래스와 Senior클래스에서 부모의 메소드의 이름만 빌려와서 자기의 방식대로 '재정의'하였다. 이것이 오버라이딩이다.

 

여기서 접근 제어자를 다르게 설정해 놓은 것을 볼 수 있다. 오버라이딩에서 접근 제어자를 설정하는 규칙이 존재한다.

 

1. 자식 클래스에서 오버라이딩하는 메소드의 접근 제어자는 부모 클래스보다 더 좁게 설정할 수 없다.

 

위에서 볼 수 있듯이 부모클래스의 접근제어자는 default로 설정되어 있다. 여기서 자식 클래스들은 default보다 같거나 더 넓은 범위의 접근제어자만 설정할 수 있으므로 default, protected, public 이 세 개의 접근 제어자는 사용이 가능하다.

 

2. 예외(Exception)는 부모 클래스의 메소드 보다 많이 선언할 수 없다.

 

부모 클래스에서 어떤 예외를 throws 한다고 하면, 자식 클래스에서는 그 예외보다 더 큰 범위의 예외를 throws할 수 없다는 것이다.

 

3. static메소드를 인스턴스의 메소드로 또는 그 반대로 바꿀 수 없다.

 

부모 클래스의 static메소드를 자식에서 같은 이름으로 정의할 수 있지만 이것은 다시 정의하는 것이 아니라 같은 이름의 static메소드를 새로 정의하는 것이다.

 

 

 

오버로딩 vs 오버라이딩

 

 

이 둘은 이름만 비슷하지 사실 명백히 다른 것이다.

 

오버로딩 - 기존에 없는 새로운 메소드를 추가하는 것

오버라이딩 - 상속받은 메소드를 재정의 하는 것

 

 구분  Overriding  Overloading
 접근 제어자  부모 클래스의 메소드의 접근 제어자보다 더 넓은 범위의 접근 제어자를 자식 클래스의 메소드에서 설정할 수 있다.  모든 접근 제어자를 사용할 수 있다.
 리턴형   동일해야 한다.  달라도 된다.
 메소드명  동일해야 한다.  동일해야 한다.
 매개변수  동일해야 한다.  달라야만 한다.
 적용 범위  상속관계에서 적용된다.  같은 클래스 내에서 적용된다.

 

 

 

📌 1. 다형성이란? 

다형성(polymorphism)이란 부모-자식 상속 관계에 있는 클래스에서

상위 클래스가 동일한 메시지로 하위 클래스들을 서로 다르게 동작시키는 객체 지향 원리입니다. 

다형성을 활용하면 부모 클래스가 자식 클래스의 동작 방식을 알수 없어도 오버라이딩을 통해 자식 클래스를 접근할 수 있습니다.

그렇다면 어떻게  부모가 자식이 어떤 일을 하는 지 몰라도,  자식 멤버 함수를 호출시킬 수 있을 까요? 

이유는 동적 바인딩 때문입니다. 동적바인딩이란, 메서드가 실행 시점에서 성격이 결정되는 바인딩인데요.

프로그램의 컴파일 시점에 부모 클래스는 자신의 멤버 함수밖에 접근할 수 없으나,

실행 시점에 동적 바인딩이 일어나 부모클래스가 자식 클래스의 멤버함수를 접근하여 실행할 수 있습니다. 

 

📌 2. 다형성 장점 

1) 유지보수가 쉽다 

개발자가 여러 객체를 하나의 타입으로 관리가 가능하기 때문에 코드 관리가 편리해 유지보수가 용이합니다.  

2) 재사용성 증가

다형성을 활용하면 객체를 재사용하기 쉬워지기 때문에 개발자의 코드 재사용성이 높집니다.

3) 느슨한 결합

다형성을 활용하면 클래스간 의존성이 줄어들며 확장성이 높고 결합도가 낮아져 안전성이 높아집니다. 

 

📌 3. 다형성 필수 조건

 1) 상속 관계

다형성을 활용하기 위해서는 필수로 부모-자식 간 클래스 상속이 이루어져야 합니다. 

 2) 오버라이딩 필수 (자식 클래스에서 메소드 재정의) 

다형성이 보장되기 위해서는 하위 클래스 메소드가 반드시 재정의되어 있어야 합니다. 

 3) 업캐스팅 (자식 클래스의 객체가 부모 클래스 타입으로 형변환 되는 것)

부모 타입으로 자식클래스를 업캐스팅하여 객체를 생성해야 합니다. 

 

📌 4. 다형성 구현 방법 

1) 상속 클래스 구현 (부모-자식 클래스 구현) 

2) 메소드 오버라이딩 

3) 업캐스팅하여 객체 선언 

4) 부모 클래스 객체로 자식 메소드 호출 

 

📌 5. 다형성 예시  

 1) 상속 클래스 구현 (부모-자식 클래스 구현) 

- 부모 자식간 상속 클래스를 구현합니다. 

- 부모 클래스는 Book 클래스 이며 자식 클래스는 Novel(소설) 클래스입니다.  

- 디폴트 생성자 외 인수 있는 생성자를 추가해 생성자를 중복 정의합니다. 

class Book{
    public String name;
    public String publisher; 
    Book(){
    	this.name = "";
        this.publisher = "";
    }
    Book(String name, String publisher){
        this.name = name;
        this.publisher = publisher;
    }
    void print(){
        System.out.println("print : Book");
    }; 
}

 

 2) 메소드 오버라이딩 

- 자식 클래스인 Novel, SF  클래스에 부모 메서드를 오버라이딩하여 재정의합니다.

class Novel extends Book{
    public String name;
    public String publisher; 
    Novel(String name, String publisher){
        super(name, publisher);
    }
    @Override
    void print(){
        System.out.println("print : Novel");
    }
}
class SF extends Book{
    public String name;
    public String publisher; 
    SF(String name, String publisher){
        super(name, publisher);
    }
    @Override
    void print(){
        System.out.println("print : SF");
    }
}

 

 3) 업캐스팅하여 객체 선언 

업캐스팅(자식 클래스 객체를 부모 클래스로 형변환)하여 아래와 같이 객체를 선언합니다.

Book b = new Novel("메타버스 소설","출판사(IT)");
Book c = new SF("메타버스", "SF출판사");

 

 4) 부모 클래스 객체로 자식 메소드 호출 

부모 클래스 Book 으로 생성된 객체의 멤버 함수를 선언합니다.

Book b = new Novel("소설","소설출판사");
b.print();

Book c = new SF("메타버스", "SF출판사");
c.print();

 

결과적으로 위와 같이 다형성을 활용하면

부모 클래스로 객체를 선언했으나 실행시점에 동적 바인딩되어 자식클래스의 멤버함수가 호출되는 것을 볼 수 있습니다. 

print : Novel
print : SF

 

 

📌 6. 객체 타입 확인 : instanceof 

instanceof 는 객체 타입을 확인하는 연산자로, 객체의 실제 타입을 알아보기 위한 연산자입니다. 

아래와 같이 "객체 instanceof 타입" 과 같이 사용합니다. 

객체 instanceof 타입

 

instanceof 연산자는 해당 객체가 타입이 맞다면 true, 아니면 false를 반환합니다. 

if(c instanceof SF){
    c.print(); //true
}
if(b instanceof Novel){
    b.print(); //true
}
728x90