안동민 개발노트

본문 시작

산술 연산과 문자열 결합

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

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

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

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

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

문자열 total=과 20을 먼저 결합한 trace는 total=2030을 만들고, 괄호 안에서 20과 30을 먼저 더한 trace는 total=50을 만드는 왼쪽부터의 평가 비교입니다.

JAVA · PAIRED TRACE

문자열 결합은 왼쪽부터 식의 종류를 바꾼다

같은 20과 30도 첫 연산이 문자열 결합인지 숫자 덧셈인지에 따라 total=2030과 total=50으로 갈립니다.

문자열 결합과 괄호 산술의 두 평가 trace 첫 trace는 문자열 total=과 20을 먼저 결합해 total=20을 만들고 30을 이어 붙여 total=2030을 만듭니다. 둘째 trace는 괄호 안에서 20과 30을 먼저 더해 50을 만든 뒤 문자열과 결합해 total=50을 만듭니다. TRACE A · STRING FIRST "total=" + 20 String "total=20" 첫 +부터 결합 "total=20" + 30 30도 문자열로 붙음 왼쪽 결과가 이미 String OBSERVED OUTPUT total=2030 20 + 30이 아님 TRACE B · NUMBER FIRST (20 + 30) int 50 괄호 안 산술 먼저 "total=" + 50 마지막에 한 번 결합 숫자 합계 보존 OBSERVED OUTPUT total=50 합계 뒤 문자열화

TRACE A · 문자열 먼저

  1. "total=" + 20 → String "total=20"

  2. "total=20" + 30 → total=2030

TRACE B · 숫자 먼저

  1. (20 + 30) → int 50

  2. "total=" + 50 → 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이 있는지와 제수가 0인지에 따라 정수 몫과 나머지, ArithmeticException, 실수 결과, Infinity로 갈리는 Java 나눗셈 흐름입니다.

JAVA · DIVISION FLOW

나눗셈 직전 타입과 0이 결과를 결정한다

이 도식은 본문의 int·double 예제를 비교합니다. 0으로 나누는 예시는 양수 5와 5.0을 사용하며, 피연산자의 타입에 따라 예외와 Infinity로 갈립니다.

피연산자 타입과 0 제수에 따른 Java 나눗셈 결과 본문의 int·double 예제에서 5를 정수 0으로 나누면 ArithmeticException이고, 5.0을 0.0으로 나누면 Infinity입니다. 5를 정수 2로 나눈 결과 2를 double로 저장하면 2.0이며, 나눗셈 전에 double로 바꾸면 2.5입니다. 135분은 몫 2시간과 나머지 15분으로 분해됩니다. 나눗셈 직전의 피연산자 이 예제의 int · double double이 포함되는가? 정수 제수가 0인가? 실수 제수가 0.0인가? INT · ZERO ArithmeticException 5 / 0 INT · NONZERO 5 / 2 → 2 → double 2.0 135 / 60 → quotient 2 135 % 60 → remainder 15 2 × 60 + 15 = 135 · 복원 DOUBLE · NONZERO 실수 결과 2.5 (double) 5 / 2 → 2.5 DOUBLE · ZERO Infinity 5.0 / 0.0 아니요 예 예 아니요 아니요 예

정수 피연산자

제수 0
ArithmeticException
제수 nonzero
5 / 2 → 2 → double 2.0
몫·나머지
135 / 60=2 · 135 % 60=15 · 2×60+15=135

double 피연산자 포함

제수 0.0 · 분자 5.0
5.0 / 0.0 → Infinity
제수 nonzero
(double) 5 / 2 → 2.5

나눗셈 뒤에 double 변수로 저장하는 것과 나눗셈 전에 피연산자를 double로 바꾸는 것은 서로 다른 프로그램입니다.


정수 나눗셈

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는 곱셈이 먼저이므로 괄호 없이도 같지만 “시간을 분으로 환산한 값”이라는 의미를 눈에 보이게 합니다.

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


나이 차이와 가입 자격 비율

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

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 [email protected] 15 20
[email protected]
age=15
gapYears=5
eligibility=75.0%

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

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

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

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

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

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

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


연습 문제

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를 계산 가능한 조건으로 바꿉니다.