
💡 오버라이딩: 상위 객체가 가진 메서드를 하위 객체가 재정의 하는 것(다형성, 확장성)
오버라이딩의 조건
class Animal {
void sound() {
System.out.println("동물이 소리를 냅니다");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("멍멍!");
}
}
Animal a = new Dog();
a.sound(); // "멍멍!"
// 추상메서드 상속
abstract class Animal {
abstract void sound(); // 구현 없음
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("야옹!");
}
}
Animal a = new Cat();
a.sound(); // "야옹!"
@Override 어노테이션
어노테이션 없어도 오버라이딩 가능, 그럼 @Override는 왜 씀?
시스템에서 오버라이딩한 메서드라고 알리는 역할로 오버라이딩이 잘못된 경우 컴파일 오류 발생.
@Override를 표시함으로써 해당 메서드가 오버라이딩하였다는 것을 쉽게 파악 가능

💡 객체지향 설계원칙, SOLID – 리스코프 치환 원칙(LSP)
하위 타입은 상위 타입을 언제나 대체할 수 있어야 한다.
→ 즉, 부모 타입으로 호출하던 기능이 자식 타입으로도 문제없이 동작해야 한다.
규칙
class Parent {
// 부모 메서드는 protected
protected void doSomething() {
System.out.println("Parent: 작업 수행");
}
}
class Child1 extends Parent {
//기본(default) 접근자로 오버라이딩 → protected보다 좁아짐 → 컴파일 에러
@Override
void doSomething() { // default
System.out.println("Child1: 기본 접근자는 더 좁아서 override 불가");
}
}
class Child2 extends Parent {
// private으로 오버라이딩 → 접근 범위가 부모보다 좁아져서 컴파일 에러
@Override
private void doSomething() {
System.out.println("Child2: private은 더 좁아서 override 불가");
}
}
class Child3 extends Parent {
// OK — 부모(protected)와 동일한 접근 제어자
@Override
protected void doSomething() {
System.out.println("Child3: 동일 접근 제어자 사용");
}
}
class Child4 extends Parent {
// OK — 부모보다 더 넓은 접근 범위(public)
@Override
public void doSomething() {
System.out.println("Child4: public으로 접근 범위 확장");
}
}
💡 오버로딩: 같은 이름의 함수에 매개변수를 다르게 주는 것. 사용성, 편의성 확보.
오버로딩의 조건
void add(int num_1, int num_2)
{
printf("%d", num_1 + num_2);
return 0;
}
void add(float num_1, float num_2)
{
printf("%f", num_1 + num_2);
return 0;
}
public class MyPrintStream {
public void println(int x) {
print(String.valueOf(x));
}
public void println(boolean x) {
print(String.valueOf(x));
}
public void println(char x) {
print(String.valueOf(x));
}
public void println(String x) {
print(x);
}
public void println(double x) {
print(String.valueOf(x));
}
public void println(Object obj) {
print(String.valueOf(obj));
}
// 실제 출력은 여기서 처리 (공통 로직)
private void print(String s) {
System.out.println("[MyPrintStream] " + s);
}
}
public class Main {
public static void main(String[] args) {
MyPrintStream out = new MyPrintStream();
out.println(10); // int
out.println("Hello"); // String
out.println(true); // boolean
out.println('A'); // char
out.println(3.14); // double
out.println(new int[]{1,2,3}); // Object
}
}
컴파일 타임 (Compile-time): 소스 코드가 컴파일될 때 발생하는 시간. 주로 구문 오류 및 타입 오류를 검사.
주요 활동: 구문 분석, 타입 체크, 코드 최적화, 바이트코드 생성
오류 유형: 문법 오류, 타입 오류
런타임 (Run-time): 프로그램이 실제로 실행되는 시간. 실행 중에 발생하는 오류를 처리.
주요 활동: 프로그램 실행, 동적 할당, 입출력 작업, 예외 처리
오류 유형: NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException
메서드는 객체의 실제 타입(런타임 타입)을 따르고, 필드는 선언된 타입(컴파일 시점)을 따라감.
class Parent {
String str = "Parent";
String getStr() {
return str;
}
}
class Child extends Parent {
String str = "Child";
String getStr() {
return str;
}
}
public class Main {
public static void main(String[] args) {
// 선언된 타입, 객체의 실제 타입
Parent parent = new Child();
Child child = new Child();
System.out.println(parent.getStr());
System.out.println(child.getStr());
System.out.println(parent.str);
System.out.println(child.str);
}
}
/*
출력 결과
Child
Child
Parent
Child
*/
메서드 호출과 필드 접근이 서로 다른 방식으로 동작하는 이유
메서드 오버라이딩과 다형성
필드 숨김(Shadowing)
즉, 메서드는 런타임의 실제 타입을 반영하여 프로그램의 유연성을 제공, 필드는 컴파일 시점에서 정적으로 결정되어 성능과 명확성을 유지.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
public class Overload {
// 오버로딩.
public static void get(List<?> list) {
System.out.println("List");
}
public static void get(ArrayList<?> list) {
System.out.println("ArrayList");
}
public static void get(LinkedList<?> list) {
System.out.println("LinkedList");
}
public static void main(String[] args) {
// 배열 원소의 정적 타입 List<?>
List<?>[] listArray = { new ArrayList<>(), new LinkedList<>(), new Vector<>() };
Arrays.stream(listArray).forEach(list -> get(list));
}
}
/*
모두 List 타입으로 보이기 때문에 결과는 모두 "List" 출력
*/
import java.util.ArrayList;
import java.util.Arrays;
// 오버라이딩
public class MyList<E> extends ArrayList<E> {
private static final long serialVersionUID = 1L;
/* (non-Javadoc)
* @see java.util.ArrayList#get(int)
*/
@Override
public E get(int index) {
System.out.print("MyList get method returns = ");
return super.get(index);
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
ArrayList<String>[] listArray = new ArrayList[2];
// 컴파일러는 배열 원소를 ArrayList<String>으로 봄
// 오버라이딩된 메서드는 실제 타입 기준
listArray[0] = new ArrayList<String>();
listArray[1] = new MyList<String>();
Arrays.stream(listArray).forEach(list -> list.add("first object"));
Arrays.stream(listArray).forEach(list -> System.out.println(list.get(0)));
}
}
/*
ArrayList → "first object"
MyList → "MyList get method returns = first object"
*/
오버로딩·오버라이딩의 결정 시점 차이
컴파일 시점에는 listArray의 각 요소가 ArrayList인지 LinkedList인지 알 수 없고, 오직 정적 타입(List) 만 알고 있다. 즉, 오버로딩에서는 컴파일러가 List 타입 파라미터를 받는 메서드를 선택.
런타임 시점에는 new ArrayList(), new MyList() 로 생성된 실제 객체 타입을 알 수 있음. 이때 오버라이딩된 메서드는 동적 바인딩을 통해 실제 객체의 메서드가 실행된다