Notice
Recent Posts
Recent Comments
Link
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[Java] 예제 소스 정리 - 연산자(산술, 논리, 비트, 조건, 증감, 할당) 본문

네이버클라우드 AIaaS 개발자 양성과정 1기/Java

[Java] 예제 소스 정리 - 연산자(산술, 논리, 비트, 조건, 증감, 할당)

끈기JK 2022. 12. 29. 20:54

com.eomcs.lang.ex05

 

예제 소스 정리

 

연산자

 

# 산술 연산자 : 우선 순위

# 연산자 우선 순위
괄호: ()
후위 연산자: a++, a--
전위 연산자: ++a, --a, 단항 연산자(+, -)
*, /, %
+, -
비트이동 연산자: <<, >>, >>>
관계 연산자: <, >, <=, >=, instanceof
등위 연산자: ==, !=
&
^
|
논리 연산자 AND: &&
논리 연산자 OR: ||
삼항 연산자: (조건) ? 값 : 값
할당 연산자: =, +=, -=, *=, /=, %=, &=, ^=, |=, <<=, >>=, >>>=

 

 

# 산술 연산자 : 기본 연산 단위
byte b;
b = 5; // OK!
b = 6; // OK!
// - 리터럴 5, 6은 4바이트 정수 값이다.
// - 정수 리터럴은 기본이 4바이트 크기이지만,
// - byte 변수에 저장할 수 있다면 허락한다!

b = 5 + 6; // OK!
// - 리터럴끼리 산술 연산한 결과도 리터럴로 간주한다.
// - 그 결과 값이 변수의 범위 내의 값이면 허락한다.

System.out.println(b);
// - 이유? 리터럴 값은 컴파일 단계에서 그 값이 얼마인지 확인할 수 있기 때문이다.
// - 변수의 경우는 컴파일 단계에서 값을 확인할 수 없다.

byte x = 5, y = 6, z;
z = x; // OK!
z = y; // OK!

//    z = x + y; // 컴파일 오류!
//
// "자바의 정수 연산은 최소 단위가 4바이트이다."
// "그래서 byte나 short의 연산 단위가 기본으로 4바이트이다."
//
// - 자바는 정수 변수에 대해 산술 연산을 수행할 때,
//   그 변수의 값이 4바이트 보다 작다면(byte나 short 라면),
//   4바이트로 만들어 연산을 수행한다.
// - 즉 임시 4바이트 정수 메모리를 만든 다음에 
//   그 메모리에 값을 담은 후에 연산을 수행한다.
// - 따라서 x + y는 바로 실행하지 않고
//   임의의 4바이트 정수 메모리를 만든 다음에 
//   그 메모리에 x와 y 값을 넣고 연산을 수행한다.
// - 연산 결과도 당연히 4바이트이다.
//   그래서 4바이트 값을 1바이트 메모리에 넣지 못하기 때문에
//   컴파일 오류가 발생하는 것이다!

// short도 마찬가지이다.
short s1 = 5;
short s2 = 6;
short s3;
s3 = s1; // OK!
s3 = s2; // OK!
//    s3 = s1 + s2; // 컴파일 오류!

int s4 = s1 + s2;
System.out.println(s4);
// 이유?
// - byte 경우와 마찬가지고 short 메모리의 값을 직접 연산할 수 없다.
// - 임시 4바이트 메모리를 만든 다음에 그 메모리에 값을 저장한 후 
//   연산을 수행한다.
// - 당연히 그 연산 결과를 담은 메모리도 4바이트이기 때문에
//   short(2byte) 메모리에 저장할 수 없는 것이다.

 결론!
 - 숫자의 크기에 상관없이 작은 숫자를 다루더라도 
   정수를 다룰 때는 그냥 int를 사용하라!
 - byte는 보통 파일의 데이터를 읽어 들일 때 사용한다.

 

 

# 산술 연산자 : 연산의 결과 타입
// 연산을 수행한 후 생성된 결과도 피연산자와 같은 타입이다.

int i = 5;
int j = 2;
float r = i / j; // int와 int의 연산 결과는 항상 int이다.
// 따라서 r 변수에 넣기 전에 
// 이미 결과는 정수 2가 된다.
// 정수 2를 float 변수에 넣으면 
// 출력할 때 2.0이 된다.
System.out.println(r);

// 해결책!
// - 변수에 들어 있는 값을 다른 타입으로 바꿔라.
//   "형변환(type conversion=type casting)"하라!
r = (float)i / (float)j; // float / float = float
// i / j의 값은 2.5가 되고
// r에 저장되는 것은 2.5이다.
System.out.println(r);

// 물론 두 개의 정수 값 중 한 개만 float으로 형변환해도 된다.
// 왜? 
// => 연산을 수행할 때 나머지 변수가 암시적 형변환이 이루어지기 때문이다.
r = i / (float)j;
System.out.println(r);

 정리!
 - int와 int의 연산 결과는 int이다.
 - float과 float의 연산 결과는 float이다.
 - 즉 연산 결과는 피연산자의 타입과 같다.
 - 그래서 두 값을 계산했을 때 결과 메모리를 초과할 경우
   값이 짤릴 수 있다.
   주의하라!
 - 코드를 작성할 때 피연산자의 계산 결과가 피연산자의 메모리 크기를
   벗어날 가능성이 있다면,
   처음부터 피연산자의 값을 더 큰 메모리에 담아서 연산을 수행하라!
   

 형변환(type casting=type conversion)?
 - 변수나 리터럴을 다른 타입의 값을 바꾸는 것이다.
 - 주의!
   원래 변수의 타입을 바꾸는 것이 아니다.
   내부적으로는 변수에 들어 있는 값을 꺼내 
   지정된 타입의 임시 메모리를 만들어 저장한다.

 결론!
 1) 자바의 최소 연산 단위는 int이다.
    따라서 int 보다 작은 크기의 메모리 값을 다룰 때는
    내부적으로 int로 자동 형변환을 수행한 다음에 연산을 수행한다.
    내부적으로 자동 형변환하는 것을 
    "암시적 형변환(implicit type conversion)"이라 부른다.
 => byte + byte = int
 => short + short = int
 => byte + short = int
 
 2) 연산 결과는 항상 피연산자의 타입과 같다.
 => int + int = int
 => long + long = long
 => float + float = float
 => double + double = double

 3) 다른 타입과 연산을 수행할 때는 
    내부적으로 같은 타입으로 맞춘 다음에 실행한다.

 

 

# 산술 연산자 : 연산의 결과 타입
// int와 int의 연산 결과는 int이다.
// 다른 타입이 될 수 없다.
// => 0111 1111 1111 1111 1111 1111 1111 1111 = Integer.MAX_VALUE
//
int x = Integer.MAX_VALUE; // 0x7fffffff = 약 +21억
int y = Integer.MAX_VALUE; // 0x7fffffff = 약 +21억

int r1 = x + y; // 0x7fffffff + 0x7fffffff = 0xfffffffe = -2
//   0111 1111 1111 1111 1111 1111 1111 1111(x)
// + 0111 1111 1111 1111 1111 1111 1111 1111(y)
// ---------------------------------------------
//   1111 1111 1111 1111 1111 1111 1111 1110(r1)
System.out.println(r1); // int(4byte) + int(4byte) = int(4byte)
// => int와 int의 연산 결과가 int의 범위를 넘어가면 
//    의도한 결과가 나오지 않을 수 있다.

// 그래서 int와 int의 연산 결과를 더 큰 메모리에 담는다면 해결될까?
long r2 = x + y;  // 0x7fffffff + 0x7fffffff = 0xfffffffe = -2
System.out.println(r2); // int(4byte) + int(4byte) = int(4byte)
// 해결 안됨!
// r2의 출력 결과를 보면 42억이 출력되는 것이 아니라 -2가 출력된다.
// 이유?
// - int 와 int의 연산 결과는 피연산자와 같은 4바이트 int가 된다.
// - 그래서 8바이트 long 변수에 저장하기 전에 
// - 이미 그 결과는 int 값으로 -2가 되기 때문에 
// - long 변수의 값이 -2가 된다.

// 진정한 해결책?
// - int와 int 연산 결과가 int 크기를 넘어갈 것 같으면 
//   형변환하여 계산하라!
r2 = (long)x + (long)y;

 

 

# 산술 연산자 : 연산의 결과 타입
float f1 = 987.6543f;
float f2 = 1.111111f;
System.out.println(f1);
System.out.println(f2);

float r1 = f1 + f2;
// f1과 f2에 들어 있는 값이 유효자릿수라 하더라도
// 연산 결과가 유효자릿수가 아니라면 값이 잘리거나 반올림 된다.
// => float과 float의 연산 결과는 float이기 때문이다.
//      987.6543
//    +   1.111111
//   ---------------
//      988.765411  <=== float의 유효자릿수인 7자리를 넘어간다.
//      988.7654    <=== 유효자릿수를 넘어가는 수는 짤린다.

System.out.println(r1);
// 기대값: 987.6543 + 1.111111 = 988.765411
// 결과값: 988.7654
// 결과가 옳지 않게 나온 이유?
// => float과 float의 연산 결과는 float이다.
// => 그래서 메모리 크기를 넘어가는 뒤의 11은 짤린다.

// 그럼 결과를 담을 변수의 크기를 늘리면 되는가?
double r2 = f1 + f2;
System.out.println(r2);
// 기대값: 988.765411
// 결과값: 988.765380859375
// 기대한 결과가 나오지 않은 이유?
// => float과 float의 연산 결과는 float이다.
// => double 변수에 저장하기 전에 이미 float 값이 되면서 일부 값이 왜곡되었다.

// 그런데 r1 변수와 달리 뒤에 이상한 숫자가 많이 붙는 이유는 무엇인가?
// => IEEE 754의 이진수 변환 문제때문이다.
// => 4바이트 float 부동소수점을 8바이트 double 부동소수점 변수에 저장할 때 
//    왜곡된 값이 들어 갈 수 있다.
// => float을 double 변수에 넣을 때 왜곡이 발생하기 때문에 
//    가능한 double 변수로 값을 바꾼 다음에 연산을 수행하라.
//    더 좋은 것은 처음부터 double 변수를 사용하라!

// 다음과 같이 처음부터 double 변수를 사용하라!
double d1 = 987.6543;
double d2 = 1.111111;
double r5 = d1 + d2; // = 988.765411
System.out.println(r5);
// 그럼에도 실제 출력해보면 맨 뒤에 극한의 작은 수가 붙는다.
// 이유? IEEE 754 이진수 변환 문제이다. 고민하지 말라!
// 어떻게 처리할 건데? 맨 뒤에 붙은 극한의 작은 수는 그냥 잘라 버린다.

 

 

# 산술 연산자 : 암시적 형변환(implicit type conversion; type casting)
byte b = 1;
short s = 2;
int i = 3;
long l = 4;
float f = 5.5f;
double d = 6.6;
boolean bool = true;
char c = 7;

// byte + byte = int
// => 연산을 하기 전에 byte 값이 int로 암시적 형변환 된다.
//    byte r1 = b + b; // 컴파일 오류!

// short + short = int
// => 연산을 하기 전에 short 값이 int로 암시적 형변환 된다.
//    short r2 = s + s; // 컴파일 오류!

// byte + short = int
// => byte와 short 값은 int로 암시적 형변환 된다.
//    short r3 = b + s; // 컴파일 오류!

// byte + int = int
// => byte가 int로 암시적 형변환 한 이후 연산을 수행한다.
int r4 = b + i; // OK

// short + int = int
// => short가 int로 암시적 형변환 한 이후 연산을 수행한다.
int r5 = s + i; // OK

// int + long = long
// => int가 long으로 암시적 형변환 한 이후에 연산을 수행한다.
//    int r6 = i + l; // 컴파일 오류!

// long + float = float
// => long이 float으로 암시적 형변환 한 후에 연산을 수행한다.
//    long r7 = l + f; // 컴파일 오류!

// int + float = float
// => 정수 타입의 값과 부동소수점 타입의 값을 연산하면
//    정수 타입의 값이 암시적 형변환을 통해 부동소수점으로 바뀐다.
//    int r8 = i + f; // 컴파일 오류!

// float + double = double
//    float r9 = f + d; // 컴파일 오류!

// byte + short + int + long + float + double = double
//long r10 = b + s + i + l + f + d; // 컴파일 오류!

// float + int + long = float
//    long r11 = f + i + l; // 컴파일 오류!

// boolean + int = 컴파일 오류!
// => 산술 연산자는 정수 타입(byte, short, char, int, long)과
//    부동소수점 타입(float, double)에 대해서만 
//    실행할 수 있다. 
//    int r12 = bool + i; // 컴파일 오류!

 정리!
 - 연산은 항상 같은 타입끼리만 가능하다.
 - 다른 타입끼리 연산을 할 때는 둘 중 한개의 타입을 다른 타입을 
   바꿔야 한다.
 - 타입을 바꾸는 것을 내부적인 규칙에 따라 자동으로 처리한다고 해서
   "암시적 형변환(implicit type conversion)"이라 부른다.
 - 암시적 형변환 규칙
   다음과 같이 오른쪽 타입의 값으로 자동 변환시킨다.
   byte,short,char => int => long => float => double

 - 정수와 부동소수점에 대해서만 암시적 형변환이 일어난다.
   그 외 다른 타입은 불가능하다!

 

 

# 산술 연산자 : 암시적 형변환과 연산 우선순위
float r1 = 5 / 2 + 3.1f;
// 계산 순서:
// r1 = int(5) / int(2) + float(3.1);
// r1 = int(2) + float(3.1);
// r1 = float(2.0) + float(3.1)
// r1 = float(5.1)
//
// => 연산 우선 순위에 따라 계산하는 순간에 암시적 형변환이 이루어진다.
// => 모든 값을 최종 결과 타입으로 바꾸고 계산하지는 않는다.
System.out.println(r1);

float r2 = 3.1f + 5 / 2;
// 계산 순서
// r2 = float(3.1) + int(5) / int(2)
// r2 = float(3.1) + int(2)
// r2 = float(3.1) + float(2.0)
// r2 = float(5.1)
System.out.println(r2);

 

 

# 산술 연산자 : 명시적 형변환
byte b;

// 4바이트 크기를 갖는 정수 리터럴을 byte 변수에 저장할 수 없다.
//    b = 259; // 컴파일 오류!

// 저장하고 싶다면 형변환(type casting)을 명시적으로 지정하라!
// => 단 메모리에 들어가기에 큰 값이라면 형변환할 때 값이 잘린다.
b = (byte)259;
// int(4 byte)  => 0000 0000 0000 0000 0000 0001 0000 0011
// byte(1 byte) => ---- ---- ---- ---- ---- ---- 0000 0011
//
// => 4바이트 중에서 앞의 3바이트가 잘리고 뒤의 1바이트만 b에 저장된다.
// 
System.out.println(b); // 3

 결론!
 => 큰 메모리의 값을 작은 메모리에 넣으려고 형변환을 사용하기도 하는데
    다만 형변환하더라도(즉 작은 메모리에 넣더라도) 값이 잘리지 않을 때만 하라!
 => 형변환하더라도 값이 소실되지 않을 때만 "명시적 형변환"을 지시하라!

 

 

관계 연산자 : 부동소수점 비교
double d1 = 987.6543;
double d2 = 1.111111;
System.out.println((d1 + d2) == 988.765411);
// 결과는 false이다.
// 이유?
// - 부동소수점 값을 IEEE 754 명세에 따라 2진수로 바꿔 메모리에 담을 때
//   정규화(소수점 이하의 수를 2진수로 바꾸는) 과정에서
//   정수로 딱 떨어지지 않는 경우가 있다.
//   즉 극한의 미세 소수점이 붙을 수 있다. 
// - CPU나 OS, JVM의 문제가 아니다.
// - IEEE 754 명세에 따라 부동소수점을 처리하는 모든 
//   컴퓨터에서 발생하는 문제이다.
// - 이런 부동소수점을 계산할 때 기대하는 값과 다른 값이 나올 수 있다.
// - 또한 연산한 결과를 메모리에 담을 때도 정규화 과정에서
//   극한의 미세 소수점이 붙을 수 있다.
System.out.println(d1);
System.out.println(d2);
System.out.println(d1 + d2); 
// 987.6543 + 1.111111 = 988.7654110000001
// => 결과 뒤에 극소수의 값이 붙는다.
// => 그래서 부동 소수점의 비교를 대충 다루지 말라!
// 0 10000001000 1110110111010011110000000001101000110110111000101111 (987.6543)
// 0 01111111111 0001110001110001110001010011111100111001110100011011 (1.111111)
//
// 1.1110110111010011110000000001101000110110111000101111
// 0.0000000010001110001110001110001010011111100111001110
//---------------------------------------------------------
// 1.1110111001100001111110001111110011010110011111111101
//
// 0 10000001000 1110111001100001111110001111110011010110011111111101
// 0 10000001000 1110111001100001111110001111110011010110011111111101

double x = 234.765411;
double y = 754.0;
System.out.println((x + y) == 988.765411);

System.out.println(x);
System.out.println(y);
System.out.println(x + y);
// d1 + d2와 달리 x + y의 계산 결과는 뒤에 극소수의 값이 붙지 않는다.
// 234.765411 + 754.0 = 988.765411
// 
// 0 10000000110 1101010110000111111000111111001101011001111111110101 (234.765411)
// 0 10000001000 0111100100000000000000000000000000000000000000000000 (754.0)
//
// 0.0111010101100001111110001111110011010110011111111101
// 1.0111100100000000000000000000000000000000000000000000
// ------------------------------------------------------
// 1.1110111001100001111110001111110011010110011111111101
//
// 0 10000001000 1110111001100001111110001111110011010110011111111101
//

// IEEE 754의 변환 공식에 따라 발생되는 이런 문제를 
// 실무 프로그래밍 할 때 해결하는 방법?
//
System.out.println((d1 + d2) == (x + y)); // false

// 소수점 뒤에 붙은 극소수의 값을 무시하면 된다.
// => JVM이 자동으로 처리하지 않는다.
// => 다음과 같이 개발자가 직접 처리해야 한다.
double EPSILON = 0.00001;
System.out.println(Math.abs((d1 + d2) - (x + y)) < EPSILON);

 

 

# 논리 연산자 : &&, ||, !(not), ^(XOR; exclusive-OR)
// AND 연산자 
// - 두 개의 논리 값이 모두 true일 때 결과가 true가 된다.
System.out.println(true && true);
System.out.println(true && false);
System.out.println(false && true);
System.out.println(false && false);

System.out.println("-----------------------");

// OR 연산자 
// - 두 개의 논리 값 중 한 개라도 true이면 결과는 true가 된다.
System.out.println(true || true);
System.out.println(true || false);
System.out.println(false || true);
System.out.println(false || false);

System.out.println("-----------------------");

// NOT 연산자 
// - true는 false로 false는 true로 바꾼다.
System.out.println(!true);
System.out.println(!false);

System.out.println("-----------------------");

// exclusive-OR(XOR)연산자 
// - 배타적 비교 연산자라 부른다.
// - 두 개의 값이 다를 때 true이다.
System.out.println(true ^ true);
System.out.println(false ^ false);
System.out.println(true ^ false);

 

 

# 논리 연산자 : &&, ||, !(not), ^(XOR; exclusive-OR)
// boolean 타입이 아닌 데이터 타입에 대해서는 사용할 수 없다.
//    System.out.println(0 && 1); // 컴파일 오류!
//    System.out.println(0 || 1); // 컴파일 오류!
//    System.out.println(!0); // 컴파일 오류!

// ^ 연산자를 정수 값에 대해 사용하면 
// 논리 연산자가 아니라 비트 연산자로 동작한다.
// 비트 연산자인 ^ 은 비트 단위로 연산을 수행한다.
// 
System.out.println(2 ^ 3);  // OK!
// 00000000_00000000_00000000_00000010 (2)
// 00000000_00000000_00000000_00000011 (3)
// ---------------------------------------
// 00000000_00000000_00000000_00000001 (1)

 

 

# 논리 연산자 : && vs &
boolean a = false;
boolean b = false;
boolean r = a && (b = true); 
// 계산 순서
// r = a && (b = true)
// r = false && (b = true) 
// => && 연산에서 왼쪽 값이 이미 false이기 때문에 결과는 확정되었다. 
// => 이렇게 && 연산의 오른쪽을 실행하기 전에 결과를 알 수 있다면 
//    JVM은 실행의 효율을 위해 && 연산의 오른쪽을 실행하지 않는다.
// => 그래서 (b = true) 문장은 실행되지 않는다.
// r = false
System.out.printf("a=%b, b=%b, r=%b\n", a, b, r);

a = false;
b = false;
r = a & (b = true);
// 계산 순서
// r = a & (b = true)
// r = false & (b = true)
// => & 연산자의 경우 왼쪽 값으로 결과를 예측할 수 있다 하더라도,
//    결과에 상관없이 & 오른쪽 문장을 무조건 실행한다.
// r = false & (b 변수의 값을 true 바꾸고, b 변수의 값을 이 자리에 놓는다.)
// r = false & true
// r = false
System.out.printf("a=%b, b=%b, r=%b\n", a, b, r);

 &&, ||
 - 앞의 피연산자의 값으로 결과를 알 수 있다면 뒤의 명령은 실행하지 않는다.
 
 &, |
 - 앞의 피연산자로 결과를 알 수 있을 지라도, 
   뒤에 놓은 명령까지 모두 실행한다.

 

 

# 논리 연산자 : || vs |
boolean a = true;
boolean b = false;
boolean r = a || (b = true);
// 계산 순서:
// r = a || (b = true)
// r = true || (b = true)
// => || 왼쪽 값으로 이미 결과를 알 수 있기 때문에 
//    || 오른쪽 문장은 실행하지 않는다.
// r = true
System.out.printf("a=%b, b=%b, r=%b\n", a, b, r);

a = true;
b = false;
r = a | (b = true);
// 계산 순서: 
// r = a | (b = true)
// r = true | (b = true)
// => | 왼쪽 값으로 결과를 확정할 수 있더라도 무조건 오른쪽 문장을 실행한다.
// r = true | (b 변수에 true를 저장)
// r = true | true
// r = true
System.out.printf("a=%b, b=%b, r=%b\n", a, b, r);

 

 

# 비트 연산자(&, |, ^, ~)
int a = 0b0110_1100;
int b = 0b0101_0101;

// 정수 값에 대해서는 &&와 ||, !을 사용할 수 없다.
//System.out.println(a && b); // 컴파일 오류!
//System.out.println(a || b); // 컴파일 오류!
//System.out.println(!a); // 컴파일 오류!


// 그러나 &, |, ^, ~는 사용할 수 있다.
// => 각 비트 단위로 연산을 수행한다.
// => 1은 true, 0은 false라고 간주하고 계산한다.
// => 출력 결과도 정수이다.

System.out.println(a & b);
// a = 0000 0000 0000 0000 0000 0000 0110 1100
// b = 0000 0000 0000 0000 0000 0000 0101 0101
// --------------------------------------------
//     0000 0000 0000 0000 0000 0000 0100 0100 = 68

System.out.println(a | b);
// a = 0000 0000 0000 0000 0000 0000 0110 1100
// b = 0000 0000 0000 0000 0000 0000 0101 0101
// --------------------------------------------
//     0000 0000 0000 0000 0000 0000 0111 1101 = 125

System.out.println(a ^ b);
// a = 0000 0000 0000 0000 0000 0000 0110 1100
// b = 0000 0000 0000 0000 0000 0000 0101 0101
// --------------------------------------------
//     0000 0000 0000 0000 0000 0000 0011 1001 = 57

// 비트 연산에서 not은 ! 연산자가 아니라 ~ 연산자 이다.
System.out.println(~a);
// a = 0000 0000 0000 0000 0000 0000 0110 1100
// --------------------------------------------
//     1111 1111 1111 1111 1111 1111 1001 0011 = -109

 

 

# 비트 연산자 & 를 이용하여 % 연산 구현하기
System.out.println(54 % 2);
System.out.println(54 & 0b0000_0001);
System.out.println(54 & 1);
// % 연산은 나누기 연산을 수행해야 한다.
// 나누기 연산은 여러 번의 계산을 수행하게 된다.
// 그에 비해 & 비트 연산은 한 번만 하면 된다.
// 결론?
// - 짝수인지 홀수 인지 알아내거나
// - 2의 나머지를 구하고 싶다면 & 비트 연산을 수행하는 것이 빠르다.
//
// 어떤 값에 대해 2로 나눈 나머지 값을 구하고 싶다면,
// & 연산자를 이용하여 그 값의 하위 1비트 값만 추출하면 된다.
// 예)
//    0011 0110 (54)
//  & 0000 0001 (뒤의 1비트의 값을 추출)
// --------------
//    0000 0000 추출된 값은 0이다. 즉 나머지가 없다.
//
//    0011 0101 (53)
//  & 0000 0001
// --------------
//    0000 0001 (나머지는 1이다)
System.out.println("--------------------");


System.out.println(57 % 4);
System.out.println(57 & 0b11);
// 어떤 값에 대해 4로 나눈 나머지 값을 구하고 싶다면,
// & 연산자를 이용하여 그 값의 하위 2비트 값만 추출하면 된다.
// 주의!
// => & 연산자를 사용해서 나머지 값을 구하려면
//    나누는 값이 2의 제곱수여야 한다.
// => 즉 2의 제곱수로 나눈 나머지 값을 구하는 경우에는
//    % 대신 비트 연산자 &를 사용하면 계산 속도가 빠르다.
// 권고
// => 너무 고민 말고 그냥 % 사용하라.
// => 다른 개발자가 이해하기 편할 것이다.
// => 그럼에도 불구하고 이 연산자의 응용법을 익히는 이유는
//    가끔 오픈소스나 고급 개발자의 코드에서
//    이런 코드를 만나기 때문이다.

System.out.println(57 % 8);
System.out.println(57 & 0b111); // 57 & 7

System.out.println(57 % 16);
System.out.println(57 & 0b1111); // 57 & 15 = 57 & 0xf

 

 

# 비트 연산자 & 활용: 특정 값을 차단하고 특정 값만 통과시킬 때

 => 특정 비트의 값만 추출하고 싶을 때 사용할 수 있다.

int data = 0b1111_1001_0111_1111;
System.out.println(Integer.toBinaryString(data & 0b0000_1111_1100_0000));
//   1111_1001_0111_1111
// & 0000_1111_1100_0000
//-----------------------
//   0000_1001_0100_0000

 

 

# 비트 연산자 & 활용: 그림의 한 픽셀에서 빨강 색을 제거하고 싶다.
int pixel = 0x003f4478; // 각 바이트의 값이 '00RRGGBB' 이라 가정하다.
System.out.println(pixel & 0x0000ffff);
// pixel = 00000000_00111111_01000100_01111000
//       & 00000000_00000000_11111111_11111111
//         00000000_00000000_01000100_01111000

 

 

논리 연산자 : 조건문과 비트 연산
// &&, ||, ! 의 피연산자(operand)는 반드시 boolean 이어야 한다.
// 그리고 계산 결과는 boolean이다.
boolean r;
//    r = 10 && 20; // 컴파일 오류!
//    r = 10 || 20; // 컴파일 오류!

// &, |, ^, ~(not) 의 피연산자가 정수라면
// 비트 연산자로 동작한다.

//    r = 10 & 20; // 컴파일 오류! 비트 연산의 결과는 정수이다. 

int r2 = 10 & 20; // OK!

//    float r3 = 10.2f & 20.3f; // 컴파일 오류!

 

 

# 비트 이동 연산자 : 비트 이동의 유효 범위
System.out.println(3 << 1);
//     000000000 00000000 00000000 00000011 = 3
//   0|000000000 00000000 00000000 00000011x = 비트이동
//     000000000 00000000 00000000 000000110 = 6

System.out.println(3 << 33); // 6
System.out.println(3 << 65); // 6
System.out.println(3 << 97); // 6

값 3 에 대해 33비트를 이동하나, 65비트를 이동하나, 97 비트를 이동하나
 같은 값이 나오는 이유?
 => int 타입의 값에 대해 비트 이동을 할 때는 0 ~ 31까지만 유효하다.
    만약 31을 넘는 경우 32로 나눈 나머지 값을 비트 이동으로 간주한다.
 => long 타입의 경우 비트 이동은 0 ~ 63까지 유효하다.
    만약 63을 넘는 경우 64로 나눈 나머지 값을 비트 이동으로 간주한다.

 => 공식:
    n << s
    - n 이 int 타입이라면, 다음 계산을 통해 s의 최종 값을 결정한다.
         s % 데이터 타입 비트수 = 최종 비트이동 개수
         예1) 3 << 33 = 3 << (33 % 32) = 3 << 1 = 6
         예2) 3 << 65 = 3 << (65 % 32) = 3 << 1 = 6
         예3) 3 << 97 = 3 << (97 % 32) = 3 << 1 = 6
      나머지 값을 계산할 때 % 대신에 비트 연산자 & 를 사용하면 계산 속도가 빠르다.
         s & (데이터 타입 비트수 - 1) = 최종 비트 이동 값
         예1) 3 << 33
              = 3 << (33 & (32 - 1))
              = 3 << (33 & 31)
              = 3 << (33 & 00011111)
                     00100001
                    &00011111
                   ----------
                     00000001 = 1
              = 3 << 1 = 6
      따라서 s의 값은 무조건 0 ~ 31 이다.
      결국 s의 값은 s % 32 의 결과와 같다.

    - n 이 long 타입이라면, 다음 계산을 통해 s의 최종 값을 결정한다.
         s & 0b111111 = 최종 비트 이동 값
      따라서 s의 값은 무조건 0 ~ 63 이다.
      결국 s의 값은 s % 64 의 결과와 같다.

 => 예1)
    3 << 33
    n => 00000000 00000000 00000000 00000011 = 3
    s => 00000000 00000000 00000000 00100001 = 33
    비트이동 => s & 0b11111
         00000000 00000000 00000000 00100001 = 33
       & 00000000 00000000 00000000 00011111
       ---------------------------------------
         00000000 00000000 00000000 00000001 = 1
    최종 비트 이동 값을 계산하면 다음과 같다.
      3 << 33 ==> 3 << 1

 => 예2)
    3 << 65
    n => 00000000 00000000 00000000 00000011 = 3
    s => 00000000 00000000 00000000 01000001 = 65
    비트이동 => s & 0b111111
         00000000 00000000 00000000 01000001 = 65
       & 00000000 00000000 00000000 00111111
       ---------------------------------------
         00000000 00000000 00000000 00000001 = 1
  최종 비트 이동 값을 계산하면 다음과 같다.
      3 << 65 ==> 3 << 1

 비트 이동 계산의 근거: Java Language Specification
 If the promoted type of the left-hand operand is int,
 then only the five lowest-order bits
 of the right-hand operand are used as the shift distance.
 It is as if the right-hand operand were subjected
 to a bitwise logical AND operator & (§15.22.1)
 with the mask value 0x1f (0b11111).
 The shift distance actually used is therefore always
 in the range 0 to 31, inclusive.

 If the promoted type of the left-hand operand is long,
 then only the six lowest-order bits
 of the right-hand operand are used as the shift distance.
 It is as if the right-hand operand were subjected
 to a bitwise logical AND operator & (§15.22.1)
 with the mask value 0x3f (0b111111).
 The shift distance actually used is therefore always
 in the range 0 to 63, inclusive.

 

 

# 비트 연산자 : 응용 II

 Yes/No 또는 true/false 값을 저장할 때
 비트 연산자를 사용하면 메모리를 절약할 수 있다.

// 비트 연산자 사용 전
// => 각각의 상태를 별도의 변수에 저장해야 한다.
// => 8개의 데이터를 저장하기 위해 32바이트가 소요된다.
boolean c, cpp, java, js, python, php, html, css;
c = true;
cpp = false;
java = true;
js = false;
python = true;
php = false;
html = true;
css = false;

// => 물론 배열을 이용할 수 있다.
//    boolean 배열을 JVM에서 다룰 때는 각 boolean에 대해
//    1바이트를 사용한다.
// => 따라서 8개의 데이터를 저장하기 위해 8바이트를 사용한다.
boolean[] lang = new boolean[8];
lang[0] = true;
lang[1] = false;
lang[2] = true;
lang[3] = false;
lang[4] = true;
lang[5] = false;
lang[6] = true;
lang[7] = false;

// 비트 연산자 사용 후
// => 4바이트 변수 한 개만 있으면
//    최대 32개의 데이터를 저장할 수 있다.
// 어떻게?
int lang2 = 0;
// 00000000 00000000 00000000 00000000

// 32비트에서 뒤에 8비트를 사용하여 8개의 true/false 값을 저장할 수 있다.
// 8 비트에서 각 언어에 값을 저장할 비트를 다음과 같다고 가정하자.
// 00000000
// ||||||||- css
// |||||||- html
// ||||||- php
// |||||- python
// ||||- javascript
// |||- java
// ||- c++
// |- c

// 이렇게 준비된 32비트 메모리에서 특정 비트의 값을 1로 설정하고 싶다면
// 다음과 같이 특정 비트의 값이 1인 수를 OR(|) 연산하라!
lang2 = lang2 | 0x80; // c = true
//   00000000 00000000 00000000 00000000 00000000
// | 00000000 00000000 00000000 00000000 10000000
// --------------------------------------------------
//   00000000 00000000 00000000 00000000 10000000

//lang2 |= 0x00; // c++ = false

lang2 |= 0x20; // java = true
//   00000000 00000000 00000000 00000000 10000000
// | 00000000 00000000 00000000 00000000 00100000
// ------------------------------------------------------
//   00000000 00000000 00000000 00000000 10100000

//lang2 |= 0x00; // js = false

lang2 |= 0x08; // python = true
//   00000000 00000000 00000000 00000000 10100000
// | 00000000 00000000 00000000 00000000 00001000
// -------------------------------------------------------
//   00000000 00000000 00000000 00000000 10101000

//lang2 |= 0x00; // php = false

lang2 |= 0x02; // html = true
//   00000000 00000000 00000000 00000000 10101000
// | 00000000 00000000 00000000 00000000 00000010
// ------------------------------------------------------
//   00000000 00000000 00000000 00000000 10101010

System.out.println(Integer.toBinaryString(lang2));

// 실무에서는 이렇게 비트를 이용하여 여러 개의 true/false 상태를 저장하기도 한다.

 

 

# 비트 연산자 : 응용 III

 특정 비트의 값을 설정할 때
 0x01, 0x02, 0x04, 0x08 처럼 직접 숫자를 사용하면
 코드를 읽고 이해하기가 쉽지 않다.
 해결책?
 - 각각의 값을 의미있는 이름을 가진 변수에 저장한 후 사용하라.
 - 또한 조회용으로 사용할 변수이므로 상수로 선언하라.

final int CSS           = 0x01; // 0000 0001
final int HTML          = 0x02; // 0000 0010
final int PHP           = 0x04; // 0000 0100
final int PYTHON        = 0x08; // 0000 1000
final int JAVASCRIPT    = 0x10; // 0001 0000
final int JAVA          = 0x20; // 0010 0000
final int CPP           = 0x40; // 0100 0000
final int C             = 0x80; // 1000 0000

// Java와 C, C++, JavaScript를 할 줄 아는 개발자의 정보를 설정하라!
int lang = C | JAVA | PYTHON | HTML;
//   1000 0000 (C)
// | 0010 0000 (JAVA)
// | 0000 1000 (PYTHON)
// | 0000 0010 (HTML)
// -------------------
//   1010 1010
//

System.out.println(Integer.toBinaryString(lang));

 

 

# 비트 연산자 : 응용 IV
final int CSS           = 0x01; // 0000 0001
final int HTML          = 0x02; // 0000 0010
final int PHP           = 0x04; // 0000 0100
final int PYTHON        = 0x08; // 0000 1000
final int JAVASCRIPT    = 0x10; // 0001 0000
final int JAVA          = 0x20; // 0010 0000
final int CPP           = 0x40; // 0100 0000
final int C             = 0x80; // 1000 0000

// C, Java, Python, HTML 을 할 줄 아는 개발자의 정보를 설정하라!
int lang = C | JAVA | PYTHON | HTML; // 10101010

// 정수 값에서 특정 비트의 값만 검사하는 방법
// 예) 10101010 (C, Java, Python, HTML)
//
// CPP 언어를 할 줄 아는지 검사하기
//     10101010
//   & 01000000 (조사하려는 값과 AND 한다. 01000000)
//   ----------------------
//     00000000
//
// AND 결과 값을 검사 값과 같은지 비교하면 된다.
//     00000000 (결과값)
//     01000000 (CPP 여부를 조사하는 값)
// => 결과 값과 조사한 값이 같지 않으면 해당 비트가 0이라는 의미다.

System.out.printf("CSS        : %b\n", (lang & CSS) == CSS);
System.out.printf("HTML       : %b\n", (lang & HTML) == HTML);
System.out.printf("PHP        : %b\n", (lang & PHP) == PHP);
System.out.printf("Python     : %b\n", (lang & PYTHON) == PYTHON);
System.out.printf("JavaScript : %b\n", (lang & JAVASCRIPT) == JAVASCRIPT);
System.out.printf("Java       : %b\n", (lang & JAVA) == JAVA);
System.out.printf("C++        : %b\n", (lang & CPP) == CPP);
System.out.printf("C          : %b\n", (lang & C) == C);

System.out.println("--------------------------");
System.out.printf("CSS        : %b\n", (lang & CSS) > 0);
System.out.printf("HTML       : %b\n", (lang & HTML) > 0);
System.out.printf("PHP        : %b\n", (lang & PHP) > 0);
System.out.printf("Python     : %b\n", (lang & PYTHON) > 0);
System.out.printf("JavaScript : %b\n", (lang & JAVASCRIPT) > 0);
System.out.printf("Java       : %b\n", (lang & JAVA) > 0);
System.out.printf("C++        : %b\n", (lang & CPP) > 0);
System.out.printf("C          : %b\n", (lang & C) > 0);

 

 

# 비트 연산자 : 응용 V - 사용자 권한 관리에 적용
// 1) 사용자 권한을 값으로 정의
final int LOGOUT = 0x01;    // 00000001   - 로그아웃
final int GENERAL = 0x02;   // 00000010   - 일반 로그인
final int ADMIN = 0x04;     // 00000100   - 관리자 로그인

// 2) 메뉴의 접근 범위 설정
int menu1 = LOGOUT; // 로그아웃 상태에서만 접근 가능한 메뉴
int menu2 = GENERAL; // 일반으로 로그인 된 사용자만 접근 가능한 메뉴
int menu3 = ADMIN; // 관리자로 로그인 된 사용자만 접근 가능한 메뉴
int menu4 = LOGOUT | GENERAL | ADMIN; // 로그아웃 되었든, 일반으로 로그인 되었든, 관리자로 로그인 되었든 모두 접근 가능한 메뉴

// 3) 접근 테스트
// => menu1이 로그아웃 상태에서 접근 가능한 것인지 검사한다.
System.out.println((menu1 & LOGOUT) > 0);
// 계산 원리
//    menu1:    00000001
//    LOGOUT:   00000001
//            &-----------
//              00000001   <---- 0 보다 큰 값이 나온다.
// 해설: 
// 어떤 값에 대해 LOGOUT 값을 & 한다는 것은
// LOGOUT 비트(오른쪽에서 첫 번째 비트)가 1인지 검사한다는 뜻이다. 

System.out.println((menu1 & GENERAL) > 0);
// 계산 원리
//    menu1:    00000001
//    GENERAL:  00000010
//            &-----------
//              00000000   <---- 0 보다 큰 값이 나온다.
// 해설: 
// 어떤 값에 대해 GENERAL 값을 & 한다는 것은
// GENERAL 비트(오른쪽에서 두 번째 비트)가 1인지 검사한다는 뜻이다.

System.out.println((menu1 & ADMIN) > 0);
// 계산 원리
//    menu1:    00000001
//    ADMIN:    00000100
//            &-----------
//              00000000   <---- 0 보다 큰 값이 나온다.
// 해설: 
// 어떤 값에 대해 ADMIN 값을 & 했을 때 true 가 나온다면,
// 그 값을 2진수로 보았을 때 ADMIN 비트가 1이라는 의미다.

 

 

# 비트 연산자 : 응용 V - 사용자 권한 관리에 적용
// 1) 사용자 권한을 값으로 정의
final int LOGOUT = 0x01;    // 00000001   - 로그아웃
final int GENERAL = 0x02;   // 00000010   - 일반 로그인
final int ADMIN = 0x04;     // 00000100   - 관리자 로그인

// 2) 메뉴의 접근 범위 설정
int menu1 = LOGOUT; // 로그아웃 상태에서만 접근 가능한 메뉴
int menu2 = GENERAL; // 일반으로 로그인 된 사용자만 접근 가능한 메뉴
int menu3 = ADMIN; // 관리자로 로그인 된 사용자만 접근 가능한 메뉴
int menu4 = LOGOUT | GENERAL | ADMIN; // 로그아웃 되었든, 일반으로 로그인 되었든, 관리자로 로그인 되었든 모두 접근 가능한 메뉴
int menu5 = GENERAL | ADMIN; // 로그인 한 사용자만 접근 가능한 메뉴

// 3) 접근 테스트
// => menu1이 로그아웃 상태에서 접근 가능한 것인지 검사한다.
System.out.println((menu2 & LOGOUT) > 0);
System.out.println((menu2 & GENERAL) > 0);
System.out.println((menu2 & ADMIN) > 0);
System.out.println("----------------------------------");

System.out.println((menu3 & LOGOUT) > 0);
System.out.println((menu3 & GENERAL) > 0);
System.out.println((menu3 & ADMIN) > 0);
System.out.println("----------------------------------");

System.out.println((menu4 & LOGOUT) > 0);
System.out.println((menu4 & GENERAL) > 0);
System.out.println((menu4 & ADMIN) > 0);
System.out.println("----------------------------------");

System.out.println((menu5 & LOGOUT) > 0);
System.out.println((menu5 & GENERAL) > 0);
System.out.println((menu5 & ADMIN) > 0);

 

 

# 조건 연산자 => ? :
int age = 20;

// 조건 연산자를 수행한 후 그 결과를 받을 변수를 받드시 선언해야 한다.
// => 선언하지 않으면 문법 오류!
//    (age > 18) ? "성년" : "미성년";

 

int age = 20;

// 조건 연산자의 결과 값이 왼편의 변수 타입과 일치해야 한다.
// => 결과 값이 없으면 문법 오류!
//    String str = age > 18 ? System.out.println("성인이다.") : System.out.println("미성년자이다.");
// 표현식 자리에는 문자가 되었든 숫자가 되었든 
// 실행 결과가 놓여져야 한다.
// 위의 System.out.println(...) 문장은 결과를 리턴하지 않는다.
// 그래서 컴파일 오류이다.

// => 왼쪽 편의 변수 타입과 표현식의 결과 타입이 다르면 문법 오류!
//    int result = age > 18 ? "성년" : "미성년";

 

 

# 증감 연산자 : 후위(post-fix) 증가 연산자
int i = 2;

// 증감 연산자가 없다면,
// 기존 변수의 값을 1증가시키기 위해 다음과 같이 코딩해야 한다.
//i = i + 1;

// 증감 연산자를 사용하면 다음과 같이 간략하게 작성할 수 있다.
i++; // i => 3
// 현재 위치에 i 메모리에 들어 있는 값(2)을 꺼내 놓는다.
// 그런 다음에 i 메모리의 값을 1 증가시킨다.
// 결론:
//   ==> i++ 문장은 컴파일러가 i = i + 1 문장으로 바꾼다.
//   ==> 즉 i = i + 1 문장을 축약한 문법에 불과하다.

i++; // i => 4

System.out.println(i);   // 4

System.out.println(i++); // 4
// 위의 코드는 컴파일 할 때 다음의 코드로 바뀐다.
//
//    int temp = i; //<-- 임시 변수를 만들어 현재 i 값을 저장한다.
//    i = i + 1;
//    System.out.println(temp);

System.out.println(i); // 5

 

 

# 증감 연산자 : 후위(post-fix) 증감 연산자 응용 I
int i = 7;

int j = i++;

// 위 문장은 컴파일될 때 다음과 같은 형태로 변환된다.
// int temp = i;
// i = i + 1;
// int j = temp;
//
// 소스 코드 그대로 설명하면 다음과 같이 설명할 수 있다.
// 그러나 가능한 컴파일 후에 변환된 코드를 그대로 이해하는 게 더 낫다.
// 1) i 값을 그자리에 놓는다.
//    => j = 7;
//    => 7 값을 j에 저장할 수 없다.
//    => 왜? 아직 = 연산자 오른쪽의 모든 식이 수행되지 않았다.
// 2) ++ 연산 실행
//    => 즉 i = i + 1 실행
//    => i = 8 이 되었다.
// 3) 할당 연산자 실행
//    => j <=== 7 실행

 

 

# 증감 연산자 : 후위(post-fix) 증감 연산자 응용 I
int i = 7;

i = i++;
// 위 문장은 다음과 같이 실행된다.
//int temp = i;
//i = i + 1;
//i = temp;
//
// 1) i 값을 그자리에 놓는다.
//    => i = 7;
//    => 7 값을 i에 저장할 수 없다.
//    => 왜? 아직 = 연산자 오른쪽의 모든 식이 수행되지 않는다.
// 2) ++ 연산 실행
//    => 즉 i = i + 1 실행
//    => i = 8 이 되었다.
// 3) 할당 연산자 실행
//    => i <===== 7 실행
//    => 다시 i는 8에서 7로 바뀌었다.

 

 

# 증감 연산자 : 후위(post-fix) 증감 연산자 응용 II
int i = 2;
int result = i++ + i++ * i++;
// 연산자 우선수위: 
// 1) ++, -- 
// 2) *, /, %
// 3) +, -
// 4) =
// 
// int result = 2 + i++ * i++;  => i = 3
// int result = 2 + 3 * i++; => i = 4
// int result = 2 + 3 * 4; => i = 5
// int result = 2 + 12;
// int result = 14;
System.out.printf("%d, %d\n", i, result);

 

 

# 증감 연산자 : 전위(pre-fix) 증가 연산자
int i = 2;

++i;
// i 메모리의 값을 먼저 증가시킨다.
// 그리고 i 메모리의 값을 그 자리에 놓는다.

++i;

System.out.println(i); // 4

System.out.println(++i); 
// i = i + 1
// System.out.println(5)

System.out.println(i); // 5

 

 

# 증감 연산자 : 전위(pre-fix) 증감 연산자 응용 I
int a = 5;
int r = ++a;
// 위의 문장은 다음 두 문장으로 변경된다.
// a = a + 1;
// int r = a;

System.out.printf("%d, %d\n", a, r);
// r = ++a 연산 순서
// 1) ++a 연산을 먼저 수행 => a 변수의 값을 1 증가시킨다.
// 2) a 값을 리턴한다. => r = 6;
// 3) = 연산자 수행 => r 변수에 6 값을 넣는다.
// 결론!
// => 전위 연산자는 먼저 변수의 값을 -- 하거나 ++ 한 후에 
//    그 자리에 변수의 값을 놓는다.

 

int i = 2;
i = ++i;
// 위의 문장은 다음 두 개의 문장으로 바뀐다.
// i = i + 1;
// i = i;

System.out.println(i); // 3

 

 

# 증감 연산자 : 전위(pre-fix) 증감 연산자 응용 II
int i = 2;
int result = ++i + ++i * ++i;
// 위의 문장은 다음 문장들로 변경된다.
// i = i + 1;
// t1 = i;
// i = i + 1;
// t2 = i;
// i = i + 1;
// t3 = i;
// r = t1 + t2 * t3;
// 
// 1) int result = 3 + 4 * 5;
// 2) int result = 3 + 20;
// 3) int result = 23;

System.out.println(result); // 23

 

 

# 증감 연산자 : 전위(pre-fix) 증감 연산자 응용 II
int a = 5;
int r = --a + --a / --a;
System.out.printf("%d, %d\n", a, r);
// 연산 순서
// r = 4 + --a / --a;
// r = 4 + 3 / --a;
// r = 4 + 3 / 2;
// r = 4 + 1;
// r = 5;

 

 

# 증감 연산자 : 전위(pre-fix) 증감 연산자 응용 II
// 주의!
// 1) pre-fix 연산자나 post-fix 연산자를 리터럴에 적용할 수 없다.
//    int x = ++100; // 컴파일 오류!
//    x = 100++; // 컴파일 오류!

// 2) 변수에 동시에 적용할 수 없다.
int y = 100;
//    ++y++; // 컴파일 오류!
//    (++y)++; // 컴파일 오류!
//    ++(y++); // 컴파일 오류!

 

 

# 할당(배정,대입) 연산자 : += -= *= /= %= &= |= ^= <<= >>= >>>=
int i = 2;

i = i + 20;
i += 20; // += 연산자를 사용하면 위의 코드를 축약할 수 있다.
System.out.println(i);

i = 2;
i *= 5;
System.out.println(i);