Ch 3.6 기타 연산자 (Other Operators)
앞서 배운 산술, 비교, 논리, 대입 연산자 외에도 실무에서 자주 사용되는 삼항 연산자, 비트 연산자, instanceof 연산자 등을 알아봅니다. 특히 비트 연산자는 성능에 민감한 시스템 프로그래밍이나 플래그 관리에 실용적으로 쓰입니다.
1. 삼항 연산자 (Ternary Operator)
삼항 연산자는 자바에서 유일하게 피연산자가 3개인 연산자 입니다. 간단한 if-else를 한 줄로 표현할 수 있습니다.
구문:
조건식 ? 참일 때 반환값 : 거짓일 때 반환값
public class TernaryBasic {
public static void main(String[] args) {
int score = 85;
// if-else 방식
String result1;
if (score >= 80) {
result1 = "합격";
} else {
result1 = "불합격";
}
// 삼항 연산자 방식 (위와 완전히 동일)
String result2 = (score >= 80) ? "합격" : "불합격";
System.out.println(result1); // 합격
System.out.println(result2); // 합격
// 숫자 반환
int a = 10, b = 20;
int max = (a > b) ? a : b;
System.out.println("최댓값: " + max); // 20
// 절댓값
int n = -15;
int abs = (n >= 0) ? n : -n;
System.out.println("절댓값: " + abs); // 15
}
}
if-else 대비 삼항 연산자 사용 기준
| 상황 | 권장 |
|---|---|
| 단순 값 선택 (한 줄로 표현 가능) | 삼항 연산자 |
| 복잡한 로직, 여러 문장 실행 | if-else |
| 중첩이 2단계 이상 필요 | if-else |
| 메서드 호출이나 예외 처리 포함 | if-else |
중첩 삼항 연산자와 가독성 문제
public class NestedTernary {
public static void main(String[] args) {
int score = 75;
// 나쁜 예: 중첩 삼항 연산자 (가독성 매우 낮음)
String grade = (score >= 90) ? "A" :
(score >= 80) ? "B" :
(score >= 70) ? "C" :
(score >= 60) ? "D" : "F";
System.out.println("등급: " + grade); // C (동작은 하지만...)
// 좋은 예: if-else로 명확하게 표현
String grade2;
if (score >= 90) grade2 = "A";
else if (score >= 80) grade2 = "B";
else if (score >= 70) grade2 = "C";
else if (score >= 60) grade2 = "D";
else grade2 = "F";
System.out.println("등급: " + grade2); // C
}
}
삼항 연산자는 단순한 2지선다 일 때 가장 효과적입니다. 3가지 이상의 선택지가 필요하거나 중첩이 필요하다면 if-else나 switch를 사용하세요.
2. 비트 연산자 완전 정리
비트 연산자는 정수를 이진수(비트) 단위 로 연산합니다. 연산 속도가 매우 빠르고 메모리 효율이 높아 시스템 프로그래밍, 암호화, 플래그 관리에 사용됩니다.
| 연산자 | 이름 | 설명 |
|---|---|---|
& | AND | 두 비트가 모두 1이면 1 |
| | OR | 두 비트 중 하나라도 1이면 1 |
^ | XOR | 두 비트가 다르면 1 |
~ | NOT | 모든 비트를 반전 (0→1, 1→0) |
<< | Left Shift | 비트를 왼쪽으로 n칸 이동 (× 2ⁿ) |
>> | Right Shift | 비트를 오른쪽으로 n칸 이동 (÷ 2ⁿ), 부호 유지 |
>>> | Unsigned Right Shift | 오른쪽으로 n칸 이동, 빈자리를 0으로 채움 |
public class BitwiseBasic {
public static void main(String[] args) {
int a = 0b1010; // 10
int b = 0b1100; // 12
System.out.printf("a = %4s (%d)%n", Integer.toBinaryString(a), a);
System.out.printf("b = %4s (%d)%n", Integer.toBinaryString(b), b);
System.out.println();
System.out.printf("a & b = %4s (%d)%n", Integer.toBinaryString(a & b), a & b); // 1000 = 8
System.out.printf("a | b = %4s (%d)%n", Integer.toBinaryString(a | b), a | b); // 1110 = 14
System.out.printf("a ^ b = %4s (%d)%n", Integer.toBinaryString(a ^ b), a ^ b); // 0110 = 6
System.out.printf("~a = %s (%d)%n", Integer.toBinaryString(~a), ~a); // -11
}
}
3. 비트 AND로 특정 비트 확인 (마스킹)
특정 비트의 값이 1인지 0인지 확인할 때 비트 AND와 마스크(mask) 를 사용합니다.
public class BitMasking {
public static void main(String[] args) {
int value = 0b10110101; // 181
// 0번째 비트(LSB) 확인: 홀수/짝수 판별
System.out.println("0번 비트: " + ((value & 0b00000001) != 0)); // true (홀수)
// 2번째 비트 확인
System.out.println("2번 비트: " + ((value & 0b00000100) != 0)); // true
// 3번째 비트 확인
System.out.println("3번 비트: " + ((value & 0b00001000) != 0)); // false
// 특정 비트 추출 (마스크 & 시프트 조합)
int rgb = 0xFF8040; // R=255, G=128, B=64
int red = (rgb >> 16) & 0xFF;
int green = (rgb >> 8) & 0xFF;
int blue = rgb & 0xFF;
System.out.printf("R=%d, G=%d, B=%d%n", red, green, blue); // R=255, G=128, B=64
}
}
4. 비트 OR로 플래그 설정
여러 플래그(옵션)를 하나의 정수로 관리할 때 비트 OR를 사용합니다.
public class BitOrFlag {
public static void main(String[] args) {
// 각 권한을 비트로 표현 (2의 거듭제곱)
final int READ = 0b0001; // 1
final int WRITE = 0b0010; // 2
final int EXECUTE = 0b0100; // 4
final int ADMIN = 0b1000; // 8
// 플래그 설정: OR로 여러 권한 합치기
int userPerms = READ | WRITE; // 0011 = 3
int adminPerms = READ | WRITE | EXECUTE | ADMIN; // 1111 = 15
System.out.println("사용자 권한: " + Integer.toBinaryString(userPerms)); // 11
System.out.println("관리자 권한: " + Integer.toBinaryString(adminPerms)); // 1111
// 권한 추가: OR로 비트 설정
userPerms |= EXECUTE; // EXECUTE 권한 추가
System.out.println("추가 후: " + Integer.toBinaryString(userPerms)); // 111
// 권한 확인: AND로 비트 체크
System.out.println("쓰기 권한: " + ((userPerms & WRITE) != 0)); // true
System.out.println("관리자 권한: " + ((userPerms & ADMIN) != 0)); // false
// 권한 제거: AND NOT으로 비트 삭제
userPerms &= ~WRITE; // WRITE 권한 제거
System.out.println("제거 후: " + Integer.toBinaryString(userPerms)); // 101
}
}
5. 비트 XOR: 토글과 간단한 암호화
public class BitXor {
public static void main(String[] args) {
// 특정 비트 토글: XOR로 비트 반전
int flags = 0b1010;
int toggleMask = 0b0110;
System.out.println("원래: " + Integer.toBinaryString(flags));
flags ^= toggleMask;
System.out.println("토글: " + Integer.toBinaryString(flags)); // 1100
flags ^= toggleMask;
System.out.println("복원: " + Integer.toBinaryString(flags)); // 1010
// XOR 암호화 아이디어 (간단한 예시)
String message = "Hello";
int key = 0x42; // 암호화 키
// 암호화: 각 문자에 XOR 적용
char[] encrypted = new char[message.length()];
for (int i = 0; i < message.length(); i++) {
encrypted[i] = (char)(message.charAt(i) ^ key);
}
System.out.println("암호화: " + new String(encrypted));
// 복호화: 동일한 키로 XOR 적용 (XOR의 역산 = XOR)
char[] decrypted = new char[encrypted.length];
for (int i = 0; i < encrypted.length; i++) {
decrypted[i] = (char)(encrypted[i] ^ key);
}
System.out.println("복호화: " + new String(decrypted)); // Hello
}
}
6. 시프트 연산자: <<, >>, >>>
public class ShiftOperator {
public static void main(String[] args) {
int n = 8; // 0000 1000
// Left Shift (<<): n * 2^k
System.out.println(n << 1); // 16 (8 * 2¹ = 16)
System.out.println(n << 2); // 32 (8 * 2² = 32)
System.out.println(n << 3); // 64 (8 * 2³ = 64)
// Right Shift (>>): n / 2^k (부호 유지)
System.out.println(n >> 1); // 4 (8 / 2¹ = 4)
System.out.println(n >> 2); // 2 (8 / 2² = 2)
System.out.println(n >> 3); // 1 (8 / 2³ = 1)
// 음수에서 >> (부호 비트 복사, 산술 시프트)
int neg = -8;
System.out.println(neg >> 1); // -4 (부호 유지)
System.out.println(neg >> 2); // -2
System.out.println(neg >>> 1); // 2147483644 (부호 무시, 0으로 채움)
// 시프트 연산은 곱셈/나눗셈보다 빠름 (최적화 목적)
int x = 100;
int doubled = x << 1; // x * 2 = 200
int halved = x >> 1; // x / 2 = 50
System.out.println("2배: " + doubled + ", 절반: " + halved);
}
}
>> vs >>> 차이
public class SignedUnsignedShift {
public static void main(String[] args) {
int negative = -1;
// -1의 이진수: 1111 1111 1111 1111 1111 1111 1111 1111
// >> (Signed): 빈자리를 부호 비트(1)로 채움 → 여전히 -1
System.out.println(negative >> 1); // -1 (11111111...1111)
// >>> (Unsigned): 빈자리를 항상 0으로 채움 → 양수
System.out.println(negative >>> 1); // 2147483647 (01111111...1111)
// 활용: 두 정수의 평균 (오버플로우 방지)
int a = Integer.MAX_VALUE;
int b = Integer.MAX_VALUE - 1;
// int wrong = (a + b) / 2; // 오버플로우 발생!
int correct = (a + b) >>> 1; // 부호 없는 우시프트로 오버플로우 방지
System.out.println("안전한 평균: " + correct); // 2147483646
}
}
>>> 의 실용적 활용Java의 HashMap이나 Arrays.sort() 같은 표준 라이브러리 내부에서도 중간값 계산에 >>> 를 사용합니다. 두 정수 a + b가 오버플로우할 수 있을 때 (a + b) >>> 1 패턴을 사용합니다.
7. instanceof 연산자
instanceof는 객체가 특정 클래스 또는 인터페이스의 인스턴스인지 확인합니다. 결과는 true 또는 false를 반환합니다.
public class InstanceofBasic {
public static void main(String[] args) {
Object obj = "Hello";
System.out.println(obj instanceof String); // true
System.out.println(obj instanceof Integer); // false
System.out.println(obj instanceof Object); // true (모든 객체는 Object의 인스턴스)
// null은 instanceof에서 항상 false
String s = null;
System.out.println(s instanceof String); // false (NPE 없음)
// 다형성과 instanceof
Object num = Integer.valueOf(42);
if (num instanceof Integer) {
int value = (Integer) num; // 명시적 캐스팅
System.out.println("정수값: " + value); // 42
}
}
}
Pattern Matching instanceof (Java 16+)
Java 16부터 instanceof 와 함께 변수 선언을 결합하는 패턴 매칭 이 도입되었습니다. 타입 체크와 캐스팅을 한 번에 처리합니다.
public class PatternMatchingInstanceof {
public static void main(String[] args) {
Object[] items = {"Hello", 42, 3.14, true, null};
for (Object item : items) {
// Java 16 이전 방식
if (item instanceof String) {
String s = (String) item; // 명시적 캐스팅 필요
System.out.println("문자열 (길이 " + s.length() + "): " + s);
}
// Java 16+ 패턴 매칭 방식 (타입 체크 + 캐스팅 동시에)
else if (item instanceof Integer i) {
System.out.println("정수: " + i);
} else if (item instanceof Double d) {
System.out.printf("실수: %.2f%n", d);
} else if (item instanceof Boolean b) {
System.out.println("불리언: " + b);
} else {
System.out.println("null 또는 알 수 없는 타입");
}
}
}
}
실행 결과:
문자열 (길이 5): Hello
정수: 42
실수: 3.14
불리언: true
null 또는 알 수 없는 타입
8. 문자열 + 연산자와 StringBuilder 성능
반복문에서 + 로 문자열을 연결하면 매번 새 String 객체가 생성되어 성능이 크게 저하 됩니다.
public class StringConcatPerformance {
public static void main(String[] args) {
int ITERATIONS = 10_000;
// 방법 1: String + (느림 - 매번 새 객체 생성)
long start1 = System.currentTimeMillis();
String s = "";
for (int i = 0; i < ITERATIONS; i++) {
s += i; // 매 반복마다 새로운 String 객체 생성
}
long time1 = System.currentTimeMillis() - start1;
// 방법 2: StringBuilder (빠름 - 내부 버퍼에 추가)
long start2 = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < ITERATIONS; i++) {
sb.append(i); // 동일 객체의 버퍼에 추가
}
String result = sb.toString();
long time2 = System.currentTimeMillis() - start2;
System.out.println("String + : " + time1 + "ms");
System.out.println("StringBuilder: " + time2 + "ms");
System.out.println("결과 길이 동일: " + (s.length() == result.length())); // true
}
}
- 단순 연결 (컴파일 타임 상수):
String s = "Hello" + " " + "World";→ 컴파일러가 자동으로"Hello World"로 최적화. OK. - 반복문 내 연결:
StringBuilder사용 (성능 중요). - 멀티스레드 환경:
StringBuffer사용 (스레드 안전).
9. 실전 예제: 비트 플래그로 사용자 권한 시스템 구현
public class PermissionSystem {
// 권한 플래그 상수 (비트 위치)
static final int PERM_READ = 1 << 0; // 0001 = 1
static final int PERM_WRITE = 1 << 1; // 0010 = 2
static final int PERM_DELETE = 1 << 2; // 0100 = 4
static final int PERM_ADMIN = 1 << 3; // 1000 = 8
// 미리 정의된 역할
static final int ROLE_GUEST = PERM_READ; // 읽기만
static final int ROLE_EDITOR = PERM_READ | PERM_WRITE; // 읽기+쓰기
static final int ROLE_MANAGER= PERM_READ | PERM_WRITE | PERM_DELETE; // 읽기+쓰기+삭제
static final int ROLE_ADMIN = PERM_READ | PERM_WRITE | PERM_DELETE | PERM_ADMIN; // 전체
// 권한 확인
static boolean hasPermission(int userPerms, int requiredPerm) {
return (userPerms & requiredPerm) != 0;
}
// 권한 부여
static int grantPermission(int userPerms, int perm) {
return userPerms | perm;
}
// 권한 회수
static int revokePermission(int userPerms, int perm) {
return userPerms & ~perm;
}
// 권한 토글
static int togglePermission(int userPerms, int perm) {
return userPerms ^ perm;
}
// 권한 출력
static void printPermissions(String name, int perms) {
System.out.println("[" + name + "] 권한 (이진수: " + String.format("%4s", Integer.toBinaryString(perms)).replace(' ', '0') + ")");
System.out.println(" 읽기: " + (hasPermission(perms, PERM_READ) ? "O" : "X"));
System.out.println(" 쓰기: " + (hasPermission(perms, PERM_WRITE) ? "O" : "X"));
System.out.println(" 삭제: " + (hasPermission(perms, PERM_DELETE) ? "O" : "X"));
System.out.println(" 관리자: " + (hasPermission(perms, PERM_ADMIN) ? "O" : "X"));
System.out.println();
}
public static void main(String[] args) {
// 초기 권한 설정
int alicePerms = ROLE_EDITOR; // 읽기 + 쓰기
int bobPerms = ROLE_GUEST; // 읽기만
int carolPerms = ROLE_ADMIN; // 전체
printPermissions("Alice (에디터)", alicePerms);
printPermissions("Bob (게스트)", bobPerms);
printPermissions("Carol (관리자)", carolPerms);
// 권한 부여: Bob에게 쓰기 권한 추가
System.out.println(">>> Bob에게 쓰기 권한 부여");
bobPerms = grantPermission(bobPerms, PERM_WRITE);
printPermissions("Bob (업그레이드)", bobPerms);
// 권한 회수: Alice에게서 쓰기 권한 제거
System.out.println(">>> Alice의 쓰기 권한 회수");
alicePerms = revokePermission(alicePerms, PERM_WRITE);
printPermissions("Alice (다운그레이드)", alicePerms);
// 삼항 연산자로 접근 제어
System.out.println("=== 파일 삭제 접근 제어 ===");
String[] users = {"Alice", "Bob", "Carol"};
int[] perms = {alicePerms, bobPerms, carolPerms};
for (int i = 0; i < users.length; i++) {
String access = hasPermission(perms[i], PERM_DELETE) ? "허용" : "거부";
System.out.println(users[i] + " 삭제 접근: " + access);
}
}
}
실행 결과:
[Alice (에디터)] 권한 (이진수: 0011)
읽기: O
쓰기: O
삭제: X
관리자: X
[Bob (게스트)] 권한 (이진수: 0001)
읽기: O
쓰기: X
삭제: X
관리자: X
[Carol (관리자)] 권한 (이진수: 1111)
읽기: O
쓰기: O
삭제: O
관리자: O
>>> Bob에게 쓰기 권한 부여
[Bob (업그레이드)] 권한 (이진수: 0011)
읽기: O
쓰기: O
삭제: X
관리자: X
>>> Alice의 쓰기 권한 회수
[Alice (다운그레이드)] 권한 (이진수: 0001)
읽기: O
쓰기: X
삭제: X
관리자: X
=== 파일 삭제 접근 제어 ===
Alice 삭제 접근: 거부
Bob 삭제 접근: 거부
Carol 삭제 접근: 허용
10. 핵심 정리
| 연산자/개념 | 설명 | 주요 활용 |
|---|---|---|
? : (삼항) | 조건에 따라 값 선택 | 단순 if-else 대체 |
& (비트 AND) | 공통 비트만 남김 | 특정 비트 확인(마스킹) |
| (비트 OR) | 합친 비트 | 플래그 설정 |
^ (비트 XOR) | 다른 비트만 1 | 토글, 암호화 |
~ (비트 NOT) | 모든 비트 반전 | 플래그 제거 (& ~mask) |
<< | 왼쪽 시프트 (× 2ⁿ) | 빠른 2의 거듭제곱 곱셈 |
>> | 오른쪽 시프트 (÷ 2ⁿ), 부호 유지 | 빠른 나눗셈 |
>>> | 오른쪽 시프트, 빈자리 0으로 채움 | 오버플로우 방지 평균 계산 |
instanceof | 타입 확인 | 다형성 코드에서 타입 분기 |
Java의 EnumSet은 비트 플래그보다 더 타입 안전하고 가독성 좋은 대안입니다. 비트 연산으로 플래그를 직접 관리하는 방식은 C/C++ 스타일이며, Java에서는 EnumSet을 우선 고려하세요. 단, 성능이 극도로 중요한 하드웨어 제어, 네트워크 패킷 처리 등에서는 비트 연산이 여전히 유용합니다.