Java 재활 훈련 1일차 - 변수와 타입

0

java

목록 보기
1/18

개발 인생 4년 간 다양한 언어들로 개발해왔다. c/c++, go, python 4가지 언어로 개발해왔고, 최근까지 go만 주구장창쓰다가 project가 java로 바뀌었다.

java를 처음 공부한 지가 이제 어언 8년이 넘어간다... 학생 때는 java랑 Spring을 그렇게 열심히했는데, 막상 그렇게까지 할 필요가 있었나 싶다.

이 post는 절대 초보자를 위한 글이 아니다. 그저 과거에 공부했던 java 문법을 쑥 훑고지나가는 지침서이다. 따라서, 자세한 설명도 없다.

변수

정수 타입

java에서의 정수 타입은 총 5개로 다음과 같다.

타입메모리 크기저장되는 값의 허용 범위
byte1byte-128~128
short2byte-32,768~ 32,767
char2byte0~65535 (유니코드)
int4byte2,147,483,648~2,147,483,647
long8byte-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807

정리하자면 short 빼고는 모두 부호가 있는 signed 정수 타입인 것으로 최상위 비트 값이 부호를 의미하하는 것이다.

음수는 1의 보수법으로 표현한다. 즉, 자연수에서 최상위 비트 값을 음수를 표현하는 1로 바꾸고, 나머지 비트를 0->1로 1->0으로 스위칭한다음에 +1을 하는 것이다.

가령, 정수 127은 비트가 다음과 같은데

[0, 1, 1, 1, 1, 1, 1, 1]

-127은 여기에서 0 -> 1로 1-> 0으로 바꾸고 +1을 해주면 된다는 것이다. 단, 최상위 비트인 부호 비트는 1로 바꾸어줘야한다.

[1, 0, 0, 0, 0, 0, 0, 1]

각 진수마다의 리터럴 표현은 다음과 같다.
1. 2진수: 0b1011 또는 0B1011
2. 8진수: 013
3. 16진수: 0xB3, 0X2A0F

기본적으로 정수 리터럴은 int를 기준으로 하고 있기 때문에 정수 리터럴을 long으로 쓰고 싶다면 L을 맨 뒤에 붙이면 된다.

long var1 = 10000000000L;

문자 타입

하나의 문자는 ''으로 표현하며, 문자 리터럴은 유니코드로 변환되어 저장된다. 이는 각국 셰계 문자를 0~65535로 맵핑한 국제 규약이다.

char var1 = 'A';
char c = 65;

char은 빈 값을 넣어서 초기화할 수 없다.

char c = ''; // compile에러
char c = ' '; // 성공

실수 타입

실수 타입에는 float, double이 있다.

타입메모리 크기유효 소수 이하 자리
float4byte7자리
double8byte15자리

참고로 float는 지수부 8bit이고, 가수부 23bit이다. double은 지수부 11bit이고 가수부 52bit이다. 둘 다 가장 최상위 비트는 부호 비트이다.

컴파일러는 실수 리터럴을 기본적으로 double 타입으로 해석한다. 따라서, `float'타입으로 쓰고 싶다면 뒤에 소문자 'f'나 대문자 'F'를 붙여야한다.

float var = 3.14f;
float var2 = 3.15F;

문자열 타입

java는 문자인 ''와 문자열인 ""을 구분한다. ""로 감싸준 문자들은 유니코드로 변환되지 않고, 문자열이라는 변수에 저장되어야 한다.

char var1 = "A"; // compile error
String var1 = "A";
String var1 = "Hello World";

참고로 String 타입은 자바 원시 타입에 속하지 않는 참조 타입(레퍼런스 타입)이다.

java 13부터는 다음과 같은 텍스트 블럭 문법을 제공한다.

String block = """
    ...
""";

이렇데 감싸면 다량의 텍스트를 하나의 String 변수에 담을 수 있다.

텍스트 블록에서 줄바꿈은 \n에 해당한다. 만약 줄바꿈을 하지 않고 그대로 이어서 한 라인을 만들고 싶다면 \을 붙여주면 된다. 이 기능은 java 14부터 제공된다.

자동 타입 변환

자동 타입 변환 또는 Implicit type conversion은 값의 허용 범위가 작은 타입이 허용 범위가 큰 타입의 변수로 대입될 때 발생한다.

byte < short, char < int < long < float < double

int type은 byte 타입보다 허용 범위가 크기 때문에 다음 코드는 자동 타입 변환이 된다.

단, 반대는 불가능하다. 즉, 크기가 큰 타입이 작은 타입에 들어갈 수는 없다. 즉, 자동 타입 변환이 안된다.

byte byteValue = 10;
int intValue = byteValue; // 자동 타입 변환

정수 타입이 실수 타입으로 대입될 때는 무조건 자동 타입 변환이 된다. 실수 타입은 정수 타입보다 허용 범위가 더 크기 때문이다.

long longValue = 5000000000L;
float floatValue = longValue; // 5.0E9f로 저장된다.

char type의 경우 int 타입으로 자동 변환되면 유니코드 값이 int값으로 대입된다.

char charValue = 'A';
int intValue = charValue; // 65

단, byte 타입은 char 타입으로 변환이 불가능하다. 이는 byte 타입은 음수도 표현 가능한데 char은 불가능하기 때문이다.

강제 타입 변환

보다 더 큰 범위를 갖는 타입이 작은 범위를 갖는 타입으로 자동 변환될 수 없다고 했다. 이는 큰 타입이 갖고 있는 값들 중 일부가 작은 타입에 들어가면서 손실되기 때문이다.

그러나, 이를 만약 허용해도된다면 다음과 같이 강제 타입 변환을 사용하면 된다.

작은 허용 범위 타입 = (작은 허용 범위 타입) 큰 허용 범위 타입

가령 intbyte로 바꾸어보도록 하자.

int intValue = 10;
byte byteValue = (byte) intValue; // 강제 타입 변환

int 타입 4 byte이지만, 1 byte인 byte 타입으로 강제변환된 것이다. 이 경우, 4byte 중 3byte를 제외하고 1byte만 가져온다. 즉, 값의 손실이 있을 수 있다.

이렇게 값의 손실을 유념하고 강제 타입 변환이 long -> int, int -> char 모두 가능하다.

실수에서 정수로 바꾸는 것은, 소수점 부분들은 모두 버려지고 정수 부분만 저장되는 특성이 있다.

double doubleValue = 3.14;
int intValue = (int) doubleValue; // intValue는 정수 부분인 3만 저장

연산식을 통한 자동 타입 변환

java에서는 약간 묘한 컴파일 동작이 있는데, int타입보다 작은 타입을 가진 변수들끼리의 연산(+, *, -, %, /)이 이루어지면 자동으로 int로 바꾼다는 것이다.

byte x = 10;
byte y = 20;
byte res = x + y; // compile error

byte타입인 res에 byte 타입인 xy의 덧셈이 적용되지 않는데, 이는 x, y 둘 다 int 보다 작은 값이고, 연산에 쓰이므로 int오 강제 형변환되어서 그렇다.

byte x = 10;
byte y = 20;
byte res = (int) x + (int) y; // compile error

위의 코드와 동일한 것이다. 개인적으로 이러한 컴파일러의 동작은 정말 마음에 안든다. 이러한 암묵적 형변환은 coding을 더욱 모호하게 만들기 때문이다.

그래서, 다음과 같이 바꿔주어야 한다.

byte x = 10;
byte y = 20;
int res = x + y;

이렇게 바꾸면 에러가 해소된다.

따라서, byte, char, short, int 타입 간의 연산들은 모두 int로 형변환되기 때문에 int만을 사용하는 것을 추천한다. 어차피 java로 개발하면서 메모리 적게쓰도록 최적화 개발한다는 말이 어불성설이다.

재밌는 것은 정수 리터럴은 무조건 int로 변환되지 않는다.

byte res = 10 + 20;

문제없이 구동되는 것을 볼 수 있다. 이는 compiler가 먼저 10과 20을 더한 결과인 30을 만들고 res에 저장하는 바이트 코드를 만들기 때문이다.

그러나, 위에서 본 것과 같이 정수 리터럴이 아니라, 변수가 연산자의 대상으로 쓰이면 실행 시에 연산이 수행되면서 int로 강제 타입 변환을 하는 것이다.

만약, 연산자의 대상으로 long이 하나라도 있다면 다른 타입들도 long으로 자동 형변환된다.

long x = 10;
int y = 20;
int res = x + y; // compile error

ylong이 아니지만 compiler에 의해서 (long) y가 된다. 따라서, 다음과 같이 되는 것이다.

long x = 10;
int y = 20;
int res = x + (long) y; // compile error

따라서, 다음과 같이 되고 싶지 않다면 resint가 아니라 long으로 바꾸는 것이 좋다.

long x = 10;
int y = 20;
long res = x + y;

실수의 경우 연산의 대상이 되는 변수들 중 하나라도 타입이 double이면 무조건 double로 치환된다.

double res = 1.2f + 3.4;

위 코드는 다음과 같이 변형된다.

double res = (doubile) 1.2f + 3.4;

정수와 실수를 같이 연산하는 경우도 double로 변환된다. 이는 실수가 더 큰 타입으로 분류되기 때문이다.

int intValue = 10;
double doubleValue = 5.5;
double res = intValue + doubleValue;

intValuedouble로 변환되는 것이다.

한 가지 조심해야하는 것은 정수끼리의 나눗셈 연산의 결과 역시 정수라는 것이다. 다음의 코드를 보도록 하자.

int x = 1;
int y = 2;
double res = x / y;
System.out.println(res); // 0.0

int type인 xy에 대한 나눗셈 연산의 결과로 0.5가 나오고 dobule로 들어갈 것 같지만 아니다. 이는 정수끼리의 연산 결과가 이미 나와버리고 double로 바뀌기 때문이다. 즉, 0이 먼저나오고 0을 double로 바꾼 0.0이 나올 뿐이다.

따라서 이러한 문제를 해결하기 위해서는 두 숫자 중 하나를 double로 바꾸어줘야 한다.

int x = 1;
int y = 2;
double res = (double) x / y;
System.out.println(res); // 0.5

결과가 0.5가 나오는 것을 볼 수 있다. (double)의 위치를 x 앞에 주었는데, y앞에 주어도 되고, 둘 다 주어도 된다. 단지 두 실수 중 하나라도 dobule로 강제 형변환을 한 다음에 정수와 실수의 연산은 실수로 바꾸도록 컴파일러에게 부탁하는 것이다.

문자열과 정수의 연산에 있어서 문자열이 더 우세하다. 이는 다음과 같은 결과가 나온다는 것이다.

int value = 3;
String str = value + "7";
System.out.println(str); // 37

value와 문자열 "7"이 연산되면 문자열인 String type이 더 우세하기 때문에 value가 문자열인 String 타입으로 형 변환되어 str로 들어가는 것이다.

문자열을 기본 타입으로 변환

문자열이 특정 숫자를 가리킬 수 있다. 가령 다음과 같다.

String str = "10";

strint로 바꾸고 싶다면 다음과 같이 쓰면 된다.

String str = "10";
int intValue = Integer.parseInt(str);
System.out.println(intValue); // 10

IntegerparseInt 메서드를 사용하는 것이다. 더불어 long으로 바꾸고 싶다면 Long.parseLong을 사용하면 되고, float로 바꾸고 싶다면 Float.parseFloat를 사용하면 된다.

다른 dobule, byte 역시 같은 매커니즘이다.

반대로 정수, 실수 타입을 문자열로 바꾸고 싶은 경우는 String.valueOf()를 사용하면 된다.

int intValue = 123;
String str = String.valueOf(intValue);
System.out.println(str); // 123

정수든, 실수든 상관없이 valueOf에 넣으면 문자열로 바뀐다.

연산자

실수 연산에서 조심해야할 것

실수 타입끼리의 연산은 정확하지 않을 수 있다. 때문에 정수로 바꾸어 연산을 실행한 다음에 실수로 바꾸는 것이 좋다.

int apple = 1;
double pieceUnit = 0.1;
int number = 7;

double result = apple - number*pieceUnit;
System.out.println(result); // 0.29999999999999993

이와 같이 0.3이 아니라 0.299999가 나오는 것을 볼 수 있다. 이는 컴퓨터 상 2진수로 소수를 표현하는데 있어서 무한 소수가 발생하면 그 값을 표현하는데 한계가 있기 때문이다. 이는 곧 계산 과정에 있어서 오차가 누적되는 것이고 누적된 오차로 인해 결과가 예상과 다르게 나오는 것이다.

int apple = 10;
double pieceUnit = 1;
int number = 7;

double result = (double)(apple - number*pieceUnit) / 10;
System.out.println(result);// 0.3

다음과 같이 10을 곱하여 정수로 계산한 다음에 10을 나누는 것이다.

나눗셈 연산 후 NaN과 Infinity 처리

정수를 0으로 나누거나 나머지 연산을 하면 Exception이 발생한다.

int a = 10 / 0;
int b = 10 % 0;

다음과 같은 Exception이 발생한다.

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Main.main(Main.java:5)

그런데 문제는 실수인 0.0으로 나누면 InfinityNaN이 발생한다는 것이다. 즉, Exception이 발생하지 않는다.

double a = 10 / 0.0;
double b = 10 % 0.0;
System.out.println(a); // Infinity
System.out.println(b); // NaN
  1. Infinity: 수학적으로 무한대를 의미한다. 크기 비교가 가능하며 음의 무한대도 존재한다. 이는 계산 결과가 무한대라는 의미이지 잘못된 결과는 아니라는 것이다.
  2. NaN: 수학적으로 의미가 없는, 정의할 수 없는 값이 나올 때 발생하는 값이다. NaN은 수학적 비교도 불가능하며 NaNNaN은 같지 않다. 단지, 수학적으로 잘못된 계산을 하여, 이상한 값이 나왔다는 것을 알려주기 위해서 사용된다.

이는 Double.isInfiniteDouble.isNaN으로 식별이 가능하다.

double a = 10 / 0.0;
double b = 10 % 0.0;
System.out.println(Double.isInfinite(a)); // true
System.out.println(Double.isNaN(b)); // true

동등 연산자 주의할 점

float로 된 값과 double로 된 값은 실수일지라도 조심해야한다. 이는 부동소수점 표기 상 가수(소수)부를 더 정확하게 표현하는 double의 비트 수가 더 많기 때문에 겉으로는 같은 값 같지만, 이진수로는 다른 값일 수도 있기 때문이다.

System.out.println(0.1f == 0.1); // false

false가 나오는 것을 볼 수 있다. 0.1을 표현하는데 있어서 float보다 double이 이진수로 더 많은 수를 표현하기 때문에 발생하는 문제이다.

따라서, 이들을 비교하기 위해서는 같은 타입으로 맞추어주는 것이 좋다.

System.out.println(0.1f == (float)0.1); // true

문자열을 비교할 때는 ==, != 대신에 equals!equals를 사용하도록 하자. 이에 대해서는 추후에 더 자세히 설명하도록 하자.

String str1 = "hello world";
String str2 = "hello world";
boolean result = str1.equals(str2);
System.out.println(result); // true

0개의 댓글