![[Java 03] 연산자와 제어문, 문자열](https://img.thenullpage.com/posts/6447/6447_1_9820aa.webp)
연산자는 값으로 무언가를 하는 기호다. 산술 + - * / %, 비교 == != < > <= >=, 논리 && || !, 대입 =과 축약형 += -=가 있다. C 계열과 거의 같지만 자바만의 함정 몇 개를 짚는 게 이번 편의 목적이다.
비교의 결과는 boolean이고 조건 자리엔 반드시 boolean이 와야 한다. a > b는 boolean을 내놓고 이게 if나 반복 조건이 된다. C나 자바스크립트처럼 0을 거짓으로 치는 느슨함이 없어서 if (count)는 에러이고 if (count > 0)이라 또렷이 적어야 한다. 덕분에 대입과 비교를 헷갈린 if (x = 5)도 x = 5의 결과가 int지 boolean이 아니라 애초에 막힌다.
논리 연산은 단축 평가로 동작한다. &&는 왼쪽이 거짓이면 오른쪽을 보지 않고, ||는 왼쪽이 참이면 건너뛴다. 성능을 넘어 안전장치로 쓰인다. if (s != null && s.length() > 0)은 s가 null이면 오른쪽 s.length()를 실행하지 않아 널 참조 예외가 안 난다. 순서를 뒤집으면 null일 때 곧장 터진다.
삼항 연산자는 값을 고르는 짧은 if다. 조건 ? 참 : 거짓 꼴이다. 증감 i++와 ++i는 둘 다 1 늘리지만 표현식이 내놓는 값이 다르다. i++는 늘리기 전 값을, ++i는 늘린 값을 돌려줘서 arr[i++]처럼 값을 쓰며 증가시키는 코드에서 이 차이가 버그가 된다.
int max = (a > b) ? a : b;
String label = (score >= 60) ? "합격" : "불합격";
switch는 문장형과 표현식형이 있다. 옛 문장형은 case 끝에 break를 빠뜨리면 아래로 흘러내리는 폴스루가 있어 실수가 잦았다. JDK 14부터 정식이 된 표현식형은 화살표 ->로 폴스루를 없애고 값을 바로 돌려준다. case 6, 7처럼 콤마로 묶고, 여러 줄이면 중괄호와 yield로 값을 낸다. 모든 경우를 다뤘는지 컴파일러가 챙겨 줘 요즘은 이쪽이 기본이다.
String kind = switch (day) {
case 6, 7 -> "주말";
default -> "평일";
};
반복문은 for, while, do-while 세 가지다. for는 횟수가 정해질 때, while은 조건이 참인 동안, do-while은 최소 한 번 돌 때 쓴다. 배열이나 리스트를 훑을 땐 인덱스를 직접 다루는 대신 향상된 for(for-each)가 경계 실수가 없고 뜻도 또렷하다. 다만 순회 중 원소를 지우거나 인덱스가 필요하면 일반 for를 써야 한다.
int[] nums = {10, 20, 30};
int sum = 0;
for (int x : nums) {
sum += x;
}
System.out.println(sum); // 60
break와 continue로 흐름을 조절한다. break는 반복을 즉시 끝내고 continue는 이번 회차만 건너뛰고 다음으로 넘어간다. 중첩 반복에서 바깥까지 한 번에 빠지려면 outer: 레이블을 붙여 break outer;로 지목하지만, 흐름을 따라가기 어려워 아껴 쓰는 게 좋다.
문자열은 불변이라 한 번 만들면 바뀌지 않는다. s.toUpperCase()는 s를 바꾸는 게 아니라 새 문자열을 만들어 돌려주니 결과를 어딘가 받아야 쓸 수 있다. 이 불변성 때문에 +로 이을 때마다 새 문자열이 생겨서, 반복문 안에서 +=로 붙이면 버려지는 중간 문자열이 산더미처럼 쌓인다. 이럴 땐 StringBuilder에 쌓다가 마지막에 한 번 굳힌다. 상수 몇 개를 잇는 정도라면 +로 충분하다.
// 나쁨: 반복마다 새 문자열 생성
String r = "";
for (int i = 0; i < 1000; i++) r += i + ",";
// 좋음: 버퍼에 쌓고 마지막에 한 번 굳힘
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) sb.append(i).append(",");
String result = sb.toString();
문자열 비교는 예외 없이 equals를 쓴다. 같은 리터럴 "hi"를 두 번 쓰면 문자열 풀이라는 공용 창고에서 공유돼 a == b가 우연히 참이 되지만, new String("hi")는 풀을 거치지 않은 새 객체라 ==가 거짓이다. ==는 같은 객체인지를 볼 뿐이니, 값이 같은지는 언제나 equals로 봐야 한다.
String a = "hi", b = "hi";
System.out.println(a == b); // true (풀 공유)
String c = new String("hi");
System.out.println(a == c); // false (새 객체)
System.out.println(a.equals(c)); // true (값 비교)
자주 쓰는 문자열 메서드는 손에 익혀 둔다. length(), charAt(i), substring(a, b), indexOf, contains, split, trim과 strip, replace, toUpperCase와 toLowerCase가 있고 모두 원본을 바꾸지 않고 새 값을 돌려준다. 숫자 변환은 Integer.parseInt, String.valueOf를 쓰며 파싱은 숫자 모양이 아니면 예외를 던지니 못 믿을 입력엔 예외 처리를 곁들인다. 순서를 따질 땐 부등호가 아니라 compareTo를 쓰는데, 부등호 비교는 원시 타입에만 되고 객체인 문자열에는 안 되기 때문이다.
String csv = "kim,30,seoul";
String[] parts = csv.split(",");
int age = Integer.parseInt(parts[1]); // 30
System.out.println(String.format("나이 %s세", age));
제어문에서 흔한 실수를 정리해 둔다. 문자열을 ==로 비교하는 것, 문장형 switch에서 break를 빠뜨리는 것, 정수 나눗셈으로 소수점을 잃는 것, 반복 조건의 부등호를 <와 <= 중 잘못 골라 한 번 더 돌거나 덜 도는 하나 차이 오류가 대표적이다. 배열을 도는 표준형 for (int i = 0; i < length; i++)를 몸에 새겨 두면 이 실수가 크게 준다.
새 switch는 패턴 매칭으로 더 나아간다. JDK 21부터 case Integer i ->, case String s ->처럼 들어온 값의 타입으로 분기하며 그 타입 변수로 바로 받는다. 예전이라면 instanceof로 확인하고 다시 형변환하던 장황한 코드가 한 줄로 준다. 자세한 건 열거형과 sealed 편에서 이어 간다.
다음 편은 배열과 컬렉션이다. 고정 크기 배열부터 크기가 늘어나는 List, 중복을 거르는 Set, 키로 값을 찾는 Map까지 코드로 훑는다. 이번 편에서 본 향상된 for와 StringBuilder가 그 자리에서 곧바로 다시 쓰이니 익혀 두면 좋다.