본문으로 건너뛰기

안동민 개발노트

본문 시작

산술 연산과 문자열 결합

정수·실수 산술과 문자열 결합의 평가 순서를 추적하고 회원 나이의 합계·평균·자격 비율을 계산합니다.

연산자는 값을 계산하지만 모든 +가 덧셈은 아닙니다.

피연산자 중 문자열이 나타난 뒤의 +는 문자열 결합으로 동작합니다.

또 정수끼리 나누면 소수 부분이 사라진 뒤 결과가 다른 타입에 대입됩니다.

회원가입의 합계와 평균을 정확히 만들려면 식의 최종 변수 타입이 아니라 각 연산이 수행되는 순간의 타입과 순서를 읽어야 합니다.

문자열을 만난 뒤의 더하기는 모두 결합이 된다

+는 왼쪽부터 평가되며 문자열 상태에 들어간 뒤의 숫자는 문자열로 붙습니다.

  1. 시작

    "total=" + 20 → "total=20"

  2. 다음 + 30

    미 문자열이므로 "total=2030"

  3. 괄호 적용

    + 30 → 50

  4. 마지막 결합

    "total=" + 50


문자열 연결 순서

lab/ConcatenationTrap.java
public final class ConcatenationTrap {
    public static void main(String[] args) {
        int firstAge = 20;
        int secondAge = 30;

        System.out.println("total=" + firstAge + secondAge);
    }
}
total=2030

식은 왼쪽부터 평가됩니다.

먼저 "total=" + firstAge가 문자열 "total=20"을 만들고, 다음 + secondAge도 문자열 결합이 되어 "total=2030"이 됩니다.

괄호로 숫자 덧셈을 먼저 계산하면 결과가 달라집니다.

System.out.println("total=" + (firstAge + secondAge));
total=50

출력 한 줄에서 모든 계산을 끝내기보다 합계에 이름을 붙이면 평가 순서와 단위를 더 분명하게 볼 수 있습니다.

src/MemberAgeArithmetic.java
public final class MemberAgeArithmetic {
    public static void main(String[] args) {
        int firstAge = 20;
        int secondAge = 30;
        int memberCount = 2;

        int totalAge = firstAge + secondAge;
        int differenceAge = secondAge - firstAge;
        int doubledAge = firstAge * 2;
        int wholeAverage = totalAge / memberCount;
        int remainder = totalAge % memberCount;
        double preciseAverage = (double) totalAge / memberCount;

        System.out.println("total=" + totalAge);
        System.out.println("difference=" + differenceAge);
        System.out.println("doubled=" + doubledAge);
        System.out.println("wholeAverage=" + wholeAverage);
        System.out.println("remainder=" + remainder);
        System.out.println("preciseAverage=" + preciseAverage);
    }
}
total=50
difference=10
doubled=40
wholeAverage=25
remainder=0
preciseAverage=25.0

+, -, *, /, %는 각각 덧셈, 뺄셈, 곱셈, 나눗셈의 몫, 나머지를 계산합니다.

음수의 나머지 규칙까지 필요한 알고리즘에서는 부호를 별도로 확인해야 하지만 현재 회원 나이는 음수를 허용하지 않을 예정입니다.

평균의 소수는 나눗셈 전에 타입을 바꿔야 남는다

정수 나눗셈에서 사라진 소수는 결과를 double에 담아도 되살아나지 않습니다.

  1. 정수 입력

    total=5, count=2

  2. 잘못된 순서

    / 2 → 2

  3. 뒤늦은 저장

    double average = 2.0

  4. 올바른 순서

    (double)5 / 2 → 2.5


정수 나눗셈

lab/IntegerDivisionLoss.java
public final class IntegerDivisionLoss {
    public static void main(String[] args) {
        int totalAge = 5;
        int memberCount = 2;
        double average = totalAge / memberCount;

        System.out.println("average=" + average);
    }
}
average=2.0

totalAge / memberCount의 두 피연산자가 모두 int이므로 먼저 정수 결과 2가 만들어집니다.

그 뒤 double 변수에 대입되면서 2.0으로 바뀝니다.

이미 버린 0.5는 대입 변환으로 복구되지 않습니다.

double average = (double) totalAge / memberCount;

왼쪽 피연산자를 double로 바꾸면 다른 피연산자도 큰 범위로 변환되고 실수 나눗셈 2.5가 됩니다.

형변환 자체는 마지막 절에서 자세히 다루지만 평균 계산에서 나눗셈 전에 타입을 바꿔야 한다는 위치는 지금 확인합니다.

0으로 정수를 나누면 실행 중 예외가 발생합니다.

lab/DivideByZero.java
public final class DivideByZero {
    public static void main(String[] args) {
        int totalAge = 50;
        int memberCount = 0;
        System.out.println(totalAge / memberCount);
    }
}
Exception in thread "main" java.lang.ArithmeticException: / by zero

컴파일러는 memberCount의 실행 시점 값이 항상 0인지 일반적으로 판단하지 않습니다.

평균을 계산하기 전에 회원 수가 0인지 조건으로 나누어야 합니다.


연산자 우선순위

곱셈·나눗셈·나머지는 덧셈·뺄셈보다 먼저 평가됩니다.

int score = 10 + 20 * 2;      // 50
int grouped = (10 + 20) * 2;  // 60

대입은 산술 계산 뒤에 일어나고 문자열 결합은 같은 + 연산자끼리 왼쪽부터 진행됩니다.

우선순위 표를 전부 외워 복잡한 식을 압축하기보다 계산의 의미 단위에 괄호를 둡니다.

src/DurationBreakdown.java
public final class DurationBreakdown {
    public static void main(String[] args) {
        int totalMinutes = 135;
        int hours = totalMinutes / 60;
        int remainingMinutes = totalMinutes % 60;
        int restoredMinutes = (hours * 60) + remainingMinutes;

        System.out.println(hours + "h " + remainingMinutes + "m");
        System.out.println("restored=" + restoredMinutes);
    }
}
2h 15m
restored=135

몫은 시간, 나머지는 남은 분입니다.

(hours * 60) + remainingMinutes는 곱셈이 먼저이므로 괄호 없이도 같지만 “시간을 분으로 환산한 값”이라는 의미를 눈에 보이게 합니다.

다시 계산한 값이 원래 합계와 같은지 확인하면 분해 공식을 검증할 수 있습니다.

135분을 몫과 나머지로 분해하고 다시 조립한다

같은 totalMinutes에서 몫은 큰 단위, 나머지는 남은 작은 단위를 만듭니다.

  1. 135 / 60 = 2시간

  2. 나머지

    135 % 60 = 15분

  3. 복원

    2×60+15 = 135

  4. 큰 단위

    hours는 완전한 60분 묶음

  5. 작은 단위

    remainingMinutes는 60 미만

  6. 검산

    복원값과 원본 비교


나이 차이와 가입 자격 비율

현재 회원 한 명의 정보에는 나이와 최소 가입 나이가 있습니다.

MemberEligibilityMath <email> <age> <minimumAge> 형식으로 받고 계산을 출력과 분리합니다.

src/MemberEligibilityMath.java
public final class MemberEligibilityMath {
    public static void main(String[] args) {
        if (args.length != 3) {
            System.out.println("사용법: MemberEligibilityMath <email> <age> <minimumAge>");
            return;
        }

        String email = args[0];
        int age = Integer.parseInt(args[1]);
        int minimumAge = Integer.parseInt(args[2]);

        int gapYears = minimumAge - age;
        double eligibilityPercent = ((double) age / minimumAge) * 100.0;

        System.out.println("email=" + email);
        System.out.println("age=" + age);
        System.out.println("gapYears=" + gapYears);
        System.out.println("eligibility=" + eligibilityPercent + "%");
    }
}

args에는 명령 뒤의 값이 문자열로 들어옵니다.

따라서 먼저 값이 정확히 세 개인지 확인한 뒤 위치 0, 1, 2를 읽습니다.

Integer.parseInt(문자열)"75"처럼 정수 모양인 문자열을 int 값으로 바꿉니다.

지금은 두 숫자 인자가 정수 모양이라고 가정하고, 잘못된 문자열을 다시 입력받는 방법은 ch3의 입력 검증에서 다룹니다.

java -cp out MemberEligibilityMath arithmetic@example.com 15 20
email=arithmetic@example.com
age=15
gapYears=5
eligibility=75.0%

eligibilityPercent 식에서는 비율을 구한 뒤 100을 곱합니다.

age * 100 / minimumAge도 정수 범위 안에서는 계산할 수 있지만 정수 나눗셈과 중간 곱셈 오버플로를 함께 검토해야 합니다.

여기서는 첫 피연산자를 double로 바꿔 소수 진행률을 보존합니다.

최소 가입 나이가 0이면 현재 코드는 실수 나눗셈 결과로 Infinity를 만들 수 있습니다.

정수 나눗셈과 달리 예외가 아닙니다.

0을 허용할지, 오류로 거절할지는 산술 연산자가 아니라 회원가입의 입력 규칙이 결정합니다.

조건문 절에서 그 경계를 닫습니다.

산술 오류는 계산 시점에 따라 다르게 나타난다

연산 직전의 타입과 값에 따라 잘못된 문자열·정보 손실·예외·Infinity가 갈립니다.

  1. "total="+20+30

    total=2030 · 결합 순서

  2. 5 / 2

    · 정수 소수 손실

  3. 5 / 0

    ArithmeticException

  4. 5.0 / 0.0

    Infinity · 부동소수점 규칙


연습 문제

25세, 40세, 50세의 회원 나이를 사용해 다음을 출력하는 MemberAgeMath를 작성하세요.

  • 총 회원 나이
  • 실수 평균
  • 가장 나이 많은 회원과 가장 어린 회원의 나이 차이
  • 30세 이상인 회원 수를 아직 조건문 없이 직접 계산한 값

문자열 결합보다 숫자 계산이 먼저 일어나도록 파생 변수를 둡니다.

해설 보기
src/MemberAgeMath.java
public final class MemberAgeMath {
    public static void main(String[] args) {
        int first = 25;
        int second = 40;
        int third = 50;
        int memberCount = 3;

        int total = first + second + third;
        double average = (double) total / memberCount;
        int ageGap = third - first;
        int qualifiedMemberCount = 2;

        System.out.println("total=" + total);
        System.out.println("average=" + average);
        System.out.println("ageGap=" + ageGap);
        System.out.println("adultMembers=" + qualifiedMemberCount);
    }
}
total=115
average=38.333333333333336
ageGap=25
adultMembers=2

평균의 긴 소수는 double의 실제 계산 결과입니다.

표시 자릿수 반올림과 계산 값 자체는 다른 문제이며 출력 형식을 다룰 때 분리합니다.

다음 절에서는 각 회원이 30세 이상인지 비교해 qualifiedMemberCount를 계산 가능한 조건으로 바꿉니다.