본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
3장 : 입력·배열·메서드

타입 변환과 오버로딩

인수 타입과 자동 형변환이 메서드 선택에 미치는 영향을 이해하고 안전한 Study Log 오버로드를 설계합니다.

같은 의미의 작업이라도 입력 타입이 다를 수 있습니다. 자바는 메서드 이름과 매개변수 목록을 함께 보고 호출 대상을 결정합니다. 같은 이름에 서로 다른 매개변수 목록을 선언하는 기능이 오버로딩입니다. 편리하지만 자동 형변환과 후보 우선순위를 이해하지 못하면 예상과 다른 메서드가 선택되거나 호출 자체가 모호해집니다.

인수 타입과 자동 변환

src/WideningCall.java
public final class WideningCall {
    public static void main(String[] args) {
        int minutes = 40;
        long total = addBonus(minutes, 5);
        System.out.println(total);
    }

    private static long addBonus(long minutes, long bonus) {
        return minutes + bonus;
    }
}
45

호출 인수는 int 두 개지만 매개변수는 long 두 개입니다. int 범위의 모든 값을 long이 표현할 수 있으므로 자바가 자동으로 넓혀 전달합니다. 이를 자동 형변환 또는 확대 변환이라고 합니다.

반대 방향은 자동으로 허용되지 않습니다.

private static int normalize(int minutes) {
    return Math.max(minutes, 0);
}

long raw = 3_000_000_000L;
int value = normalize(raw); // 컴파일 오류

long 값 일부는 int 범위를 벗어나므로 정보 손실 가능성이 있습니다. (int) raw처럼 명시적 캐스트를 하면 컴파일되지만, 값이 잘려 전혀 다른 숫자가 될 수 있습니다. 메서드 호출을 통과시키려고 캐스트부터 추가하지 말고 도메인 범위와 매개변수 타입을 먼저 검토합니다.

숫자 확대의 대표 순서는 byte → short → int → long → float → double입니다. char도 int 이상의 숫자 타입으로 확대될 수 있습니다. boolean은 숫자와 서로 변환되지 않습니다. 확대라고 해도 큰 long을 float로 바꿀 때 정밀도는 줄 수 있으므로 “컴파일 가능”과 “정보가 완전히 보존됨”을 구분합니다.

오버로딩의 구분 기준

src/OverloadSelection.java
public final class OverloadSelection {
    public static void main(String[] args) {
        printMinutes(40);
        printMinutes(40L);
        printMinutes(40.5);
    }

    private static void printMinutes(int value) {
        System.out.println("int=" + value);
    }

    private static void printMinutes(long value) {
        System.out.println("long=" + value);
    }

    private static void printMinutes(double value) {
        System.out.println("double=" + value);
    }
}
int=40
long=40
double=40.5

세 메서드는 이름이 같지만 매개변수 타입이 다르므로 각각 다른 시그니처입니다. 호출과 정확히 일치하는 타입이 있으면 그 후보가 선택됩니다. 정확한 int 후보가 없다면 printMinutes(40)long 또는 double로 확대 가능한 후보 중 더 가까운 long을 선택합니다.

반환 타입만 바꾸는 것은 오버로딩이 아닙니다.

private static int total(int[] values) { return 0; }
private static long total(int[] values) { return 0L; } // 선언 충돌

호출문 total(values)만 보고 어느 반환값을 원하는지 항상 결정할 수 없기 때문에 매개변수 목록이 같으면 같은 메서드로 취급합니다. 매개변수 이름만 바꾸는 것도 타입과 순서가 같으므로 구분되지 않습니다.

컴파일 시점의 호출 선택

src/StaticArgumentType.java
public final class StaticArgumentType {
    public static void main(String[] args) {
        int exact = 30;
        long widenedBeforeCall = exact;

        label(exact);
        label(widenedBeforeCall);
        label((long) exact);
    }

    private static void label(int value) {
        System.out.println("int session");
    }

    private static void label(long value) {
        System.out.println("long session");
    }
}
int session
long session
long session

숫자 값 30 자체가 어느 메서드를 기억하는 것이 아닙니다. 표현식의 컴파일 시점 타입이 후보를 정합니다. widenedBeforeCall 변수의 타입은 long이고 명시적 (long) 캐스트도 long 표현식을 만들기 때문에 long 오버로드가 선택됩니다.

오버로딩 결정은 실행 중 값의 크기를 보고 바뀌지 않습니다. long 변수에 30이 들어 있어도 int 범위라는 이유로 int 오버로드를 선택하지 않습니다. 이 규칙 덕분에 컴파일러가 호출 대상을 미리 확정할 수 있습니다.

null 인수의 모호성

lab/AmbiguousNullOverload.java
public final class AmbiguousNullOverload {
    public static void main(String[] args) {
        describe(null);
    }

    private static void describe(String value) {
        System.out.println("String");
    }

    private static void describe(StringBuilder value) {
        System.out.println("StringBuilder");
    }
}
컴파일 실패 관찰
error: reference to describe is ambiguous

null은 두 참조 타입 모두에 대입할 수 있고 String과 StringBuilder 사이에는 어느 쪽이 더 구체적이라고 결정할 상속 관계가 없습니다. 컴파일러가 하나를 고를 수 없어 실패합니다. (String) null처럼 호출 의도를 명시하면 컴파일되지만, API 사용자가 자주 캐스트해야 한다면 오버로드 설계가 지나치게 비슷하다는 신호입니다.

describe(Object)describe(String)이 함께 있으면 null 호출은 더 구체적인 String 후보를 선택합니다. 하지만 비슷한 참조 타입 오버로드가 늘수록 새 메서드를 추가했을 때 기존 호출이 모호해질 수 있습니다. 의미가 실제로 다르면 describeText, describeBuilder처럼 이름을 분리하는 편이 안전합니다.

출력 메서드 오버로딩

하나의 기록 출력과 배열 전체 출력을 같은 print 이름으로 표현할 수 있습니다. 매개변수의 모양이 확실히 달라 호출이 모호하지 않습니다.

src/StudyLogOverloads.java
public final class StudyLogOverloads {
    public static void main(String[] args) {
        String[] topics = {"arrays", "methods"};
        int[] minutes = {40, 50};

        print("input", 30);
        print(topics, minutes, 2);
        print(90L);
    }

    private static void print(String topic, int minutes) {
        System.out.println(topic + "=" + minutes);
    }

    private static void print(String[] topics, int[] minutes, int size) {
        if (size < 0 || size > topics.length || size > minutes.length) {
            throw new IllegalArgumentException("invalid size");
        }
        for (int index = 0; index < size; index++) {
            print(topics[index], minutes[index]);
        }
    }

    private static void print(long totalMinutes) {
        long hours = totalMinutes / 60;
        long remain = totalMinutes % 60;
        System.out.println(hours + "h " + remain + "m");
    }
}
input=30
arrays=40
methods=50
1h 30m

배열 오버로드는 한 기록 오버로드를 반복 호출해 표현 규칙을 재사용합니다. 합계 출력은 long 하나를 받아 시간과 분으로 바꿉니다. 세 호출은 인수 개수와 타입 모양이 충분히 달라 읽는 사람도 의도를 쉽게 구분합니다.

오버로드는 “비슷해 보이는 모든 작업에 같은 이름을 붙이는 기능”이 아닙니다. 호출자가 같은 개념의 작업이라고 느끼고, 인수 차이만으로 의도가 명확할 때 사용합니다. 저장, 삭제, 요약처럼 행동 자체가 다르면 서로 다른 이름이 더 낫습니다.

오버로드 선택 순서

단순화한 판단 순서는 다음과 같습니다.

  1. 이름과 인수 개수가 맞는 후보를 모읍니다.
  2. 인수 타입이 정확히 일치하는 후보를 우선합니다.
  3. 정확한 후보가 없으면 허용되는 자동 확대 변환 후보를 찾습니다.
  4. 적용 가능한 후보 중 더 구체적인 하나를 고릅니다.
  5. 하나로 정할 수 없으면 모호한 호출로 컴파일 실패합니다.

가변 인수, 박싱, 제네릭까지 들어가면 규칙은 더 많아집니다. 현재 단계에서는 정수 타입을 불필요하게 여러 개 오버로드하거나 관계없는 참조 타입을 나란히 두지 않는 것만으로도 대부분의 모호함을 피할 수 있습니다.

연습 문제

total(int first, int second)total(int[] values, int size)를 오버로드하세요. 배열 버전은 size 범위를 검사하고 long 합계를 반환하게 합니다. main에서 두 버전을 모두 호출합니다.

해설 보기
src/TotalOverloads.java
public final class TotalOverloads {
    public static void main(String[] args) {
        int[] minutes = {20, 30, 40};

        System.out.println(total(20, 30));
        System.out.println(total(minutes, 3));
    }

    private static long total(int first, int second) {
        return (long) first + second;
    }

    private static long total(int[] values, int size) {
        if (size < 0 || size > values.length) {
            throw new IllegalArgumentException("invalid size");
        }

        long result = 0L;
        for (int index = 0; index < size; index++) {
            result += values[index];
        }
        return result;
    }
}
50
90

첫 메서드는 더하기 전에 한 피연산자를 long으로 바꿉니다. int끼리 먼저 더해 오버플로한 뒤 long에 담는 실수를 피합니다. 반환 타입은 두 메서드 모두 long이지만, 오버로드 구분은 매개변수 목록으로 이루어집니다.