산술 연산과 문자열 결합
덧셈·뺄셈·곱셈·나눗셈·나머지 연산과 문자열을 잇는 +의 계산 순서를 따라가며, 회원 나이의 합계와 평균, 가입 기준 대비 비율을 계산합니다.
앞 글에서는 나이, 회원 번호, 비율 같은 값을 알맞은 타입의 변수에 담았습니다. 이제 그 값으로 계산을 할 차례입니다. 회원 두 명의 나이를 더해 합계를 내고, 회원 수로 나눠 평균을 구하는 것은 간단해 보입니다. 하지만 Java에서는 같은 +가 덧셈일 때도 있고 문자열을 이어 붙이는 연산일 때도 있습니다. 또 정수끼리 나누면 소수 부분이 사라집니다.
두 문제의 원인은 같습니다. 식의 결과는 마지막에 값을 담는 변수의 타입이 아니라, 연산자 하나하나가 실행되는 순간 그 연산자 양쪽에 있는 값, 즉 피연산자(operand)의 타입으로 정해집니다. 이 글에서는 식이 어떤 순서로 계산되는지, 그 순간 피연산자의 타입이 무엇인지를 따라가며 합계, 평균, 비율을 정확하게 계산해 봅니다.
같은 +인데 결과가 다른 이유
회원 두 명의 나이 20과 30을 더해 합계를 출력하려고 다음과 같이 썼습니다. println 안의 식에는 +가 두 번 나오는데, 두 번 모두 덧셈이 되어 total=50이 나오기를 기대한 코드입니다.
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=50 대신 total=2030이 나왔습니다. +는 두 피연산자가 모두 숫자이면 덧셈을 하지만, 한쪽이라도 String이면 다른 쪽을 문자열로 바꿔 이어 붙입니다. 이것을 문자열 결합(string concatenation)이라고 합니다. 그리고 +가 여러 개 이어진 식은 왼쪽부터 차례로 계산됩니다. 그래서 위 식은 다음 두 단계를 거칩니다.
"total=" + firstAge: 왼쪽이 문자열이므로 20을"20"으로 바꿔 이어 붙이고, 결과는 문자열"total=20"입니다."total=20" + secondAge: 왼쪽이 이미 문자열이므로 이번에도 결합이 일어나"total=2030"이 됩니다.
두 번째 +를 실행하는 순간 왼쪽 값은 이미 문자열이므로, 20과 30이 숫자로서 더해질 기회가 한 번도 없습니다. 숫자 덧셈을 먼저 하려면 괄호로 묶습니다. 괄호 안은 바깥보다 먼저 계산되므로, 20과 30이 숫자인 상태에서 더해져 50이 된 뒤 문자열과 결합됩니다.
System.out.println("total=" + (firstAge + secondAge));total=50아래 그림은 두 식을 나란히 놓고 + 하나하나가 무엇을 만들어 내는지 보여 줍니다.
문자열 결합은 왼쪽부터 식의 종류를 바꾼다
문자열 왼쪽 결합과 괄호 숫자합의 중간값이 2030/50 결과를 만듭니다. 각 + 평가 단계가 실제 차이의 원인입니다.
JAVA · PAIRED TRACE
문자열 결합은 왼쪽부터 식의 종류를 바꾼다
같은 20과 30도 첫 연산이 문자열 결합인지 숫자 덧셈인지에 따라 total=2030과 total=50으로 갈립니다.
TRACE A · 문자열 먼저
"total=" + 20 → String "total=20"
"total=20" + 30 → total=2030
TRACE B · 숫자 먼저
(20 + 30) → int 50
"total=" + 50 → total=50
출력 문장 안에서 계산을 압축하기보다 합계를 먼저 변수로 만들면 평가 순서와 단위를 더 쉽게 검토할 수 있습니다.
이 예의 조건
- 두 결합 식은 독립 예제입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 문자열 결합은 왼쪽부터 식의 종류를 바꾼다 문자열 total=과 20을 먼저 결합한 trace는 total=2030을 만들고, 괄호 안에서 20과 30을 먼저 더한 trace는 total=50을 만드는 왼쪽부터의 평가 비교입니다. JAVA · PAIRED TRACE 문자열 결합은 왼쪽부터 식의 종류를 바꾼다 같은 20과 30도 첫 연산이 문자열 결합인지 숫자 덧셈인지에 따라 total=2030과 total=50으로 갈립니다. TRACE A · 문자열 먼저 "total=" + 20 → String "total=20" "total=20" + 30 → total=2030 TRACE B · 숫자 먼저 (20 + 30) → int 50 "total=" + 50 → total=50 출력 문장 안에서 계산을 압축하기보다 합계를 먼저 변수로 만들면 평가 순서와 단위를 더 쉽게 검토할 수 있습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 연산 괄호 없는 식은 문자열 결합, 괄호 식은 숫자 덧셈부터 합니다. |
|
|
| 다음 결합 각 중간 결과에 나머지 결합을 적용합니다. |
|
|
| 출력 서로 다른 계산 결과를 출력합니다. |
|
|
문자열 결합은 왼쪽부터 식의 종류를 바꾼다
문자열 왼쪽 결합과 괄호 숫자합의 중간값이 2030/50 결과를 만듭니다. 각 + 평가 단계가 실제 차이의 원인입니다.
JAVA · PAIRED TRACE
문자열 결합은 왼쪽부터 식의 종류를 바꾼다
같은 20과 30도 첫 연산이 문자열 결합인지 숫자 덧셈인지에 따라 total=2030과 total=50으로 갈립니다.
TRACE A · 문자열 먼저
"total=" + 20 → String "total=20"
"total=20" + 30 → total=2030
TRACE B · 숫자 먼저
(20 + 30) → int 50
"total=" + 50 → total=50
출력 문장 안에서 계산을 압축하기보다 합계를 먼저 변수로 만들면 평가 순서와 단위를 더 쉽게 검토할 수 있습니다.
이 예의 조건
- 두 결합 식은 독립 예제입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 문자열 결합은 왼쪽부터 식의 종류를 바꾼다 문자열 total=과 20을 먼저 결합한 trace는 total=2030을 만들고, 괄호 안에서 20과 30을 먼저 더한 trace는 total=50을 만드는 왼쪽부터의 평가 비교입니다. JAVA · PAIRED TRACE 문자열 결합은 왼쪽부터 식의 종류를 바꾼다 같은 20과 30도 첫 연산이 문자열 결합인지 숫자 덧셈인지에 따라 total=2030과 total=50으로 갈립니다. TRACE A · 문자열 먼저 "total=" + 20 → String "total=20" "total=20" + 30 → total=2030 TRACE B · 숫자 먼저 (20 + 30) → int 50 "total=" + 50 → total=50 출력 문장 안에서 계산을 압축하기보다 합계를 먼저 변수로 만들면 평가 순서와 단위를 더 쉽게 검토할 수 있습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 연산 괄호 없는 식은 문자열 결합, 괄호 식은 숫자 덧셈부터 합니다. |
|
|
| 다음 결합 각 중간 결과에 나머지 결합을 적용합니다. |
|
|
| 출력 서로 다른 계산 결과를 출력합니다. |
|
|
왼쪽부터 계산한다는 규칙은 반대 방향으로도 작동합니다. System.out.println(20 + 30 + "=total");은 50=total을 출력합니다. 첫 +는 두 숫자 사이에 있으니 덧셈이 되어 50을 만들고, 그다음 +에서 비로소 문자열을 만나 결합이 일어나기 때문입니다. 즉 문자열이 식의 어디에 처음 나타나느냐에 따라, 그 앞은 덧셈으로 계산되고 그 뒤는 모두 결합이 됩니다.
char끼리 더할 때도 예상과 다른 일이 생깁니다. char에는 문자 모양이 아니라 문자마다 정해진 번호인 코드 값이 들어 있고, Java는 char를 정수 타입의 하나로 취급합니다. 그래서 'A' + 'B'는 문자열 "AB"가 아니라 두 코드 값 65와 66을 더한 131이 됩니다. 문자를 이어 붙이려면 "" + 'A' + 'B'처럼 문자열로 시작하거나 처음부터 String을 씁니다.
출력하기 전에 계산 결과에 이름 붙이기
괄호를 기억해 두는 것보다 좋은 방법은 계산을 출력문 밖으로 꺼내 변수에 담는 것입니다. 다음 프로그램은 다섯 가지 산술 연산의 결과를 각각 이름 있는 변수에 담은 뒤 출력합니다.
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+, -, *, /, %는 차례로 덧셈, 뺄셈, 곱셈, 나눗셈, 나머지를 계산합니다. 계산 결과를 먼저 totalAge 같은 변수에 담아 두면 출력문에는 "total=" + totalAge처럼 문자열과 변수 하나만 남습니다. 그래서 +가 결합인지 덧셈인지 헷갈릴 일이 없고, 각 값이 무엇을 뜻하는지도 이름으로 드러납니다.
출력을 하나씩 확인해 보겠습니다. 처음 세 줄은 20 + 30 = 50, 30 − 20 = 10, 20 × 2 = 40입니다. wholeAverage는 50을 2로 나눈 몫 25이고, remainder는 50을 2로 나눈 나머지 0입니다. 마지막 줄의 25.0은 double 값이기 때문에 소수점이 붙어 나옵니다. 50은 2로 나누어떨어지므로 wholeAverage와 preciseAverage는 값이 같습니다. 두 방식이 언제 달라지는지는 다음 절에서 봅니다.
정수끼리 나누면 소수 부분이 사라진다
회원 두 명의 나이 합이 5라면 평균은 2.5여야 합니다. 평균을 double 변수에 담으면 소수까지 나올 것 같지만, 실제로는 그렇지 않습니다.
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.0totalAge / memberCount의 두 피연산자는 모두 int입니다. 정수끼리의 나눗셈은 결과도 정수여야 하므로, Java는 몫 2.5에서 소수 부분을 버린 2를 결과로 냅니다. 대입은 그다음에 일어납니다. 정수 2가 double 변수에 들어가면서 2.0으로 바뀌지만, 나눗셈에서 이미 버린 0.5는 돌아오지 않습니다. 결과를 담을 변수가 double이라는 사실은 나눗셈이 끝난 뒤에야 고려되기 때문입니다.
이때 소수 부분은 반올림되지 않고 그냥 버려집니다. 예를 들어 5 / 3은 1.67에 가깝지만 결과는 2가 아니라 1입니다. 음수에서도 0에 가까운 쪽으로 버리므로 -5 / 2는 −2입니다.
나눗셈 결과에 소수를 남기려면 나누기 전에 피연산자 하나를 double로 바꿔야 합니다.
double average = (double) totalAge / memberCount;(double) totalAge는 totalAge의 값 5를 double 값 5.0으로 바꿉니다. 이렇게 값을 다른 타입으로 바꾸는 것을 형변환이라고 하고, 괄호 안에 타입을 적는 이 표기를 캐스트(cast)라고 합니다. 형변환은 나눗셈보다 먼저 적용되므로, 나눗셈이 일어나는 순간 피연산자는 double 5.0과 int 2입니다. 이처럼 피연산자의 타입이 서로 다르면 Java는 좁은 쪽인 int를 넓은 쪽인 double로 바꾼 뒤 계산합니다. 결국 5.0 / 2.0이 되어 결과는 2.5입니다.
형변환의 위치가 중요합니다. 같은 형변환이라도 (double) (totalAge / memberCount)라고 쓰면 괄호 안의 정수 나눗셈이 먼저 끝나 2가 되고, 그 2를 바꾼 2.0이 결과가 됩니다. 소수 부분을 살리려면 반드시 나눗셈 전에 피연산자를 바꿔야 합니다. 형변환의 규칙은 변수 스코프와 숫자 형변환에서 자세히 다룹니다.
0으로 나누면
나누는 수가 0이면 어떻게 될까요? 정수 나눗셈에서는 프로그램이 실행 중에 멈춥니다.
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
at DivideByZero.main(DivideByZero.java:5)이 코드는 컴파일은 문제없이 되고, 실행 중 5번째 줄의 나눗셈에서 ArithmeticException이라는 예외(exception)가 발생합니다. 예외는 실행 중에 생긴 오류를 알리는 신호로, 아무도 처리하지 않으면 위처럼 메시지를 출력하고 프로그램이 끝납니다. 컴파일러가 이 오류를 미리 막지 못하는 이유는 나누는 값이 실행 중에 정해지기 때문입니다. 이 예제에서는 0이 소스에 적혀 있지만, 보통은 회원 수가 입력이나 계산 결과로 정해지므로 실행해 보기 전에는 알 수 없습니다. 그래서 평균을 계산하기 전에 회원 수가 0인지 먼저 확인해야 하며, 그 방법은 if와 else로 조건 분기하기에서 배웁니다. 나머지 연산 %도 정수를 0으로 나누면 같은 예외가 발생합니다.
실수 나눗셈은 다르게 동작합니다. 5.0 / 0은 예외 없이 Infinity(무한대)가 되고, -5.0 / 0은 -Infinity, 0.0 / 0은 '숫자가 아님'을 뜻하는 NaN이 됩니다. 예외가 나지 않으니 프로그램은 계속 실행되지만, 그만큼 잘못된 값이 조용히 다음 계산으로 넘어갈 수 있습니다. 이 경우는 이 글 마지막 예제에서 다시 만납니다.
아래 그림은 지금까지 본 나눗셈을 한데 모은 것입니다. 나눗셈 직전에 피연산자 가운데 double이 있는지, 그리고 나누는 수가 0인지에 따라 결과가 네 갈래로 나뉩니다. 그림에 함께 실린 135분 분해는 바로 다음에 볼 나머지 연산의 예입니다.
JAVA · DIVISION FLOW
나눗셈 직전 타입과 0이 결과를 결정한다
이 도식은 본문의 int·double 예제를 비교합니다. 0으로 나누는 예시는 양수 5와 5.0을 사용하며, 피연산자의 타입에 따라 예외와 Infinity로 갈립니다.
정수끼리 나눔 · 먼저 정수 결과
- 제수가 0이 아님
5 / 2 → 2이 결과를 double 변수에 저장해도 2.0입니다. 이미 버린 소수 부분은 복구되지 않습니다.
- 제수가 0
5 / 0 → ArithmeticException정수 나눗셈을 실행할 때 예외가 발생합니다.
double 포함 · 실수 결과
- 나눗셈 전에 타입을 바꿈
(double) 5 / 2 → 2.5먼저 5가 double로 변환되므로 실수 나눗셈이 수행됩니다.
- 양수 5.0을 0.0으로 나눔
5.0 / 0.0 → Infinity이 실수 예제는 정수 나눗셈의 예외와 다른 결과를 만듭니다.
135분을 몫과 나머지로 분해
135 / 60 = 2시간 · 135 % 60 = 15분
2 × 60 + 15 = 135로 다시 합치면 원래 분 수가 됩니다.
나눗셈 뒤에 double 변수로 저장하는 것과 나눗셈 전에 피연산자를 double로 바꾸는 것은 서로 다른 프로그램입니다.
나머지 연산 %
%는 나눗셈의 나머지를 구합니다. 몫을 구하는 /와 짝을 이루어, 큰 단위와 작은 단위로 나누는 계산에 자주 쓰입니다. 다음 프로그램은 135분을 시간과 분으로 나눕니다.
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=135135를 60으로 나누면 몫이 2, 나머지가 15이므로 2시간 15분입니다. 첫 출력문 hours + "h " + remainingMinutes + "m"은 숫자로 시작하지만, 바로 다음 피연산자가 문자열 "h "이므로 첫 +부터 결합이 일어납니다. 숫자끼리 더해지는 +가 하나도 없으므로 2h 15m이 그대로 출력됩니다. restoredMinutes는 시간을 분으로 되돌린 2 × 60에 남은 15분을 더한 값입니다. 결과가 원래의 135와 같으므로 분해가 맞게 되었다는 것을 확인할 수 있습니다.
%는 짝수와 홀수를 가릴 때도 씁니다. 어떤 정수를 2로 나눈 나머지가 0이면 짝수이고 1이면 홀수입니다. 다만 음수에서는 주의해야 합니다. %의 결과는 나눠지는 수의 부호를 따르므로 -7 % 3은 2가 아니라 −1입니다. 음수에서도 언제나 0 이상의 나머지가 필요하다면 Math.floorMod(-7, 3)처럼 표준 라이브러리 메서드를 쓰면 2를 얻을 수 있습니다. 이 글의 회원 나이는 음수가 될 수 없으므로 %로 충분합니다.
연산자 우선순위와 괄호
식에 연산자가 여러 개 섞여 있으면 어느 연산자부터 묶어 계산할지는 우선순위(precedence)가 정합니다. 산수에서처럼 *, /, %가 +, -보다 먼저 묶입니다. 그래서 10 + 20 * 2는 10 + (20 * 2)로 묶여 곱셈이 먼저 계산됩니다.
int score = 10 + 20 * 2; // 20 * 2를 먼저 계산: 10 + 40 = 50
int grouped = (10 + 20) * 2; // 괄호 안을 먼저 계산: 30 * 2 = 60우선순위가 같은 연산자끼리는 왼쪽부터 묶어 계산합니다. 그래서 100 - 30 - 20은 (100 − 30) − 20 = 50이고, 100 / 10 / 2는 (100 / 10) / 2 = 5입니다. 앞에서 본 문자열 결합도 같은 규칙을 따릅니다. 덧셈과 결합은 같은 + 연산자이므로 왼쪽부터 하나씩 계산되고, 대입 =는 오른쪽 식 전체의 계산이 끝난 뒤에 일어납니다.
우선순위와 결합이 함께 작용하는 예를 하나 보겠습니다. "sum=" + 7 / 2 + 1은 sum=31을 출력합니다. /가 +보다 먼저이므로 7 / 2가 먼저 계산되어 3이 되고, 그다음 왼쪽부터 "sum=" + 3이 "sum=3"을, "sum=3" + 1이 "sum=31"을 만듭니다. 4를 출력하려면 "sum=" + (7 / 2 + 1)처럼 괄호로 묶어야 합니다.
우선순위표를 모두 외울 필요는 없습니다. 2장에서 쓰는 연산자의 우선순위는 다음 글에서 비교·논리 연산자와 함께 정리합니다. 실무에서는 우선순위를 외워 괄호를 줄이기보다, 계산의 의미가 한 덩어리인 곳에 괄호를 둡니다. 앞의 (hours * 60) + remainingMinutes가 그런 예입니다. 곱셈이 먼저이므로 괄호가 없어도 결과는 같지만, 괄호가 '시간을 분으로 바꾼 값'이라는 의미 단위를 보여 줍니다.
나이와 가입 기준 비교하기
이제 실행 인자로 이메일, 나이, 최소 가입 나이를 받아 두 가지를 계산해 보겠습니다. 하나는 최소 가입 나이까지 남은 햇수이고, 다른 하나는 나이가 최소 가입 나이의 몇 퍼센트인지입니다. 프로그램은 java -cp out MemberEligibilityMath <email> <age> <minimumAge> 형식으로 실행합니다.
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에는 명령 뒤에 적은 값이 문자열로 들어옵니다. 그래서 먼저 인자가 정확히 세 개인지 확인하고, 아니라면 사용법을 출력한 뒤 return으로 끝냅니다. !=는 '같지 않다'는 뜻으로, 다음 글에서 다른 비교 연산자와 함께 배웁니다. 인자가 세 개라면 0, 1, 2번 위치의 값을 읽고, 나이와 최소 가입 나이는 Integer.parseInt로 int로 바꿉니다. 지금은 두 숫자 인자가 정수 모양의 문자열이라고 가정합니다. 숫자가 아닌 입력을 다시 받는 방법은 3장의 입력 검증과 재입력 반복에서 다룹니다.
[email protected]
age=15
gapYears=5
eligibility=75.0%나이 15세, 최소 가입 나이 20세이므로 gapYears는 20 − 15 = 5입니다. eligibilityPercent는 괄호 안의 (double) age / minimumAge가 먼저 계산되어 15.0 / 20 = 0.75가 되고, 여기에 100.0을 곱해 75.0이 됩니다. 마지막 줄은 그 값 뒤에 문자열 "%"를 이어 붙인 것입니다.
같은 계산을 age * 100 / minimumAge처럼 정수로만 할 수도 있습니다. 15 × 100 / 20 = 1500 / 20 = 75이므로 이 입력에서는 결과가 같습니다. 하지만 이 방식은 나누어떨어지지 않을 때 소수 부분을 버립니다. 나이가 15이고 최소 가입 나이가 22라면 정확한 비율은 약 68.18%인데, 정수 계산은 68을 냅니다. 곱셈과 나눗셈의 순서를 바꾼 age / minimumAge * 100은 더 나쁩니다. 15 / 22가 먼저 계산되어 0이 되므로 결과도 0입니다. 정수 계산에서는 큰 값끼리 곱한 중간 결과가 int 범위를 넘을 수 있다는 점도 따져야 합니다. 그래서 여기서는 첫 피연산자를 double로 바꿔, 소수 부분까지 담은 비율을 계산했습니다.
최소 가입 나이로 0을 주면 어떻게 될까요? java -cp out MemberEligibilityMath [email protected] 15 0의 마지막 줄은 eligibility=Infinity%입니다. (double) age가 double이므로 나눗셈은 실수 나눗셈이고, 앞에서 본 대로 예외 대신 Infinity가 됩니다. 프로그램은 멈추지 않지만 의미 없는 값을 출력합니다. 최소 가입 나이로 0을 허용할지, 오류로 거절할지는 산술 연산자가 정할 수 있는 문제가 아닙니다. 회원가입의 입력 규칙으로 정해야 하며, 그런 규칙을 코드로 옮기는 방법은 if와 else로 조건 분기하기에서 배웁니다.
더 깊이: 문자열 결합은 실제로 어떻게 실행될까
"total=" + totalAge를 실행하면 Java는 totalAge의 값을 문자열로 바꾼 뒤 두 문자열을 이은 새 문자열을 만듭니다. int 50은 "50", double 25.0은 "25.0", boolean은 "true"처럼 바뀌는데, 기본형 값은 String.valueOf로 바꾼 것과 같은 문자열이 됩니다. 원래 문자열은 그대로 두고 결과를 담은 새 문자열 객체를 만든다는 점도 기억해 둘 만합니다.
javac가 결합 식을 어떤 바이트코드로 바꾸는지는 Java 버전에 따라 달라졌습니다. Java 8까지는 StringBuilder의 append 호출을 이어 붙이는 코드로 바꿨습니다. Java 9부터는 JEP 280에 따라 invokedynamic 명령으로 바꾸고, 실제로 어떻게 이어 붙일지는 실행 중에 JDK의 StringConcatFactory가 정합니다. 그래서 프로그램을 다시 컴파일하지 않아도, 더 새로운 JDK에서 실행하는 것만으로 결합 성능이 좋아질 수 있습니다. 어느 방식이든 한 식 안의 결합은 충분히 효율적이지만, 반복문 안에서 문자열을 계속 이어 붙이는 경우는 사정이 다릅니다. 그런 경우와 StringBuilder는 문자열 서식과 StringBuilder에서 다룹니다.
정리
+는 두 피연산자가 모두 숫자이면 덧셈, 한쪽이라도 문자열이면 결합입니다.+가 이어진 식은 왼쪽부터 계산되므로, 문자열이 처음 나온 뒤의+는 모두 결합이 됩니다.- 출력문 안에서 계산하기보다 계산 결과를 먼저 이름 있는 변수에 담으면, 계산 순서와 의미가 분명해집니다.
- 정수끼리 나누면 결과도 정수이고 소수 부분은 버려집니다. 결과를
double변수에 담아도 이미 버린 값은 돌아오지 않으므로, 나누기 전에(double)로 피연산자를 바꿔야 합니다. - 정수를 0으로 나누면
ArithmeticException이 발생합니다. 실수 나눗셈은 예외 대신Infinity나NaN을 만듭니다. %는 나머지를 구합니다. 결과의 부호는 나눠지는 수를 따르므로-7 % 3은 −1입니다.*,/,%가+,-보다 먼저 계산되고, 우선순위가 같으면 왼쪽부터 계산합니다. 의미가 한 덩어리인 곳에는 괄호를 둡니다.
연습 문제
1. 출력 예측하기
다음 세 줄의 출력을 실행하기 전에 예측하세요. 줄마다 어느 연산이 먼저 계산되는지 정하고, 계산이 한 단계씩 끝날 때 그 값이 정수인지 실수인지 문자열인지를 적어 가며 따라가면 됩니다.
System.out.println("sum=" + 7 / 2 + 1);
System.out.println(7 / 2 * 2.0);
System.out.println(1 + 2 + "3" + 4 + 5);해설 보기
sum=31
6.0
3345첫 줄은 /가 먼저 계산되어 7 / 2가 정수 3이 됩니다. 그다음 왼쪽부터 "sum=" + 3이 "sum=3"을, "sum=3" + 1이 "sum=31"을 만듭니다.
둘째 줄은 /와 *의 우선순위가 같으므로 왼쪽부터 계산합니다. 7 / 2는 정수끼리의 나눗셈이라 3이 되고, 3 * 2.0에서 3이 double로 바뀌어 6.0이 됩니다. 2.0이 식 안에 있어도, 그 앞의 정수 나눗셈은 이미 끝난 뒤입니다.
셋째 줄은 1 + 2가 숫자 덧셈이라 3이 되고, 3 + "3"부터는 문자열 결합이 되어 "33", "334", "3345" 순서로 만들어집니다.
2. 세 회원의 나이 계산하기
25세, 40세, 50세 회원의 나이로 다음 값을 출력하는 src/MemberAgeMath.java를 작성하세요. 문자열 결합보다 숫자 계산이 먼저 끝나도록 계산 결과를 변수에 담은 뒤 출력합니다.
- 나이의 합계
- 소수까지 계산한 평균
- 가장 나이 많은 회원과 가장 어린 회원의 나이 차이
- 30세 이상인 회원 수 (아직 조건문을 배우지 않았으므로, 나이를 30으로 나눈 몫이 0이면 30세 미만이고 1이면 30세 이상이라는 점을 이용하세요. 나이가 60세 미만이라고 가정합니다.)
해설 보기
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 thirtyOrOlderCount = first / 30 + second / 30 + third / 30;
System.out.println("total=" + total);
System.out.println("average=" + average);
System.out.println("ageGap=" + ageGap);
System.out.println("thirtyOrOlder=" + thirtyOrOlderCount);
}
}total=115
average=38.333333333333336
ageGap=25
thirtyOrOlder=2합계는 25 + 40 + 50 = 115입니다. 평균은 (double)로 나누기 전에 115를 double로 바꿨기 때문에 115.0 / 3 = 38.333…이 됩니다. 형변환 없이 total / memberCount라고 썼다면 정수 38이 나왔을 것입니다.
평균의 끝자리 …336은 계산 실수가 아니라 double의 실제 값입니다. 38.333…처럼 끝없이 이어지는 소수는 이진수로 정확히 저장할 수 없어 가장 가까운 값으로 저장되고, Java는 그 값을 구별할 수 있는 만큼의 자릿수로 출력합니다. 소수 둘째 자리까지만 보여 주는 것처럼 출력 형식을 정하는 문제는 계산과 따로 다루며, 문자열 서식과 StringBuilder에서 배웁니다.
30세 이상인 회원 수는 정수 나눗셈이 소수 부분을 버린다는 성질을 이용했습니다. 60세 미만의 나이라면 30으로 나눈 몫은 30세 미만일 때 0, 30세 이상일 때 1입니다. 그래서 25 / 30 + 40 / 30 + 50 / 30 = 0 + 1 + 1 = 2가 됩니다. 60세 이상이 섞이면 몫이 2 이상이 되어 이 방법은 틀린 값을 내므로, 실제 프로그램에서는 쓰지 않습니다. 다음 글에서 배울 비교 연산자를 쓰면 age >= 30처럼 조건을 그대로 코드에 적을 수 있습니다.
다음 글 증감·비교·논리·대입 연산자에서는 값을 1씩 늘리는 ++, 두 값을 비교하는 >=와 ==, 조건을 잇는 &&와 ||를 배우고, 이를 이용해 회원가입 조건을 boolean 값으로 계산합니다.