본문 바로가기
Java/basic

자바 변수의 종류, 특징 - Primitive type, Reference type

by Ellery 2022. 10. 30.
목차
1. Primitive 변수 타입의 종류와 특징
2. 컴퓨터에서의 정수, 실수 표현법, 자바에서의 정수 실수 사용법
3. Reference type
4. 리터럴
5. 변수 타입별 스코프, 각각의 라이프 사이클
6. type casting(명시적 형변환), type promotion(자동 형변환)
7. 타입 추론 var(JDK 10+)

1. Primitive 변수 타입의 종류와 특징

타입 종류 용량 기본값  범위
byte(정수형) 1 byte 0 -128 ~ 127
short 2 byte 0 -32,748 ~ 32767
int 4 byte 0 -2,147,483,648 ~ 2,147,483,647(21억)
long 8 byte 0L -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807(922경)
float(실수형) 4 byte 0.0f (3.4 * 10 ^-38) ~ (3.4 * 10^38), precision 7
double 8 byte 0.0d (1.7 * 10 ^ -308) ~ (1.7 * 10^308), precision 15
boolean(논리형) 1 byte false true, false
char(문자형) 2 byte '\u0000' 0 ~ 65,535

자바에서 char 데이터타입은 단일 16bit-Unicode 문자이다. (It has a minimum value of '\u0000' (or 0) and a maximum value of '\uffff' (or 65,535 inclusive)). 자바에서는 유니코드를 이용하여 문자를 표현함

JVM에서 쓰는 UTF-16 인코딩는 문자 하나를 16비트(2바이트)로 표현하므로 총 2^16 = 65535개의 문자를 표현할 수 있다. (C계열은 영문 대소문자만이 표현가능한 ASCII를 이용함. 2^7=128개의 문자 표현 가능). ASCII 테이블 번호를 Unicode에서도 그대로 활용하기 때문에 호환이 된다.
문자셋(charset)-인코딩(encoding) 방식을 구분하자. 서로 다른 인코딩 방식의 문자열을 변환해야 하거나, 4바이트 이모지 등을 처리해야할 수 도 있음.

  • 문자 셋: 하나의 언어권에서 사용하는 언어를 표현하기 위한 모든 문자들 집합. SBCS(ASCII) - 각 문자를 1바이트만 사용, MBCS - 아스키코드 제외한 문자는 2바이트. , WBCS(Unicode) - 모두 2바이트 처리
  • 인코딩: 문자셋을 이해할 수 있는 바이트와의 매핑 규칙(ASCII에서 a는 65, a는 97). 유니코드 글자셋을 사용하는 UTF-8, UTF-16, ... 방식, KS X 1001, 1003을 사용하는 EUC-KR 방식(한글 완성형 인코딩), CP949(코드페이지 949, 한글 windows에서 씀), CP437(영문 윈도우)
  • UTF-16 방식은 모든 영어,숫자,문자를 2바이트로 표현, UTF-8은 가변크기로 표현(영문숫자 1바이트, 한글 3바이트). 웹에서는 UTF-8을 이용해서 문서사이즈를 줄였다.
  • 자신이 사용하고자 하는 데이터의 최대 크기를 고려하여 정수, 실수형에서 오버플로우, 실수형에서 언더플로우가 생기지 않도록 사용한다

2. 컴퓨터에서의 정수, 실수 표현법, 자바에서의 정수 실수 사용법

컴퓨터에서의 bit numbering은 이진수의 비트 위치를 식별하는 데에 사용되는 규칙이다. 이를 이용하여 정수형, 실수형을 표현한다.

https://en.wikipedia.org/wiki/Bit_numbering


1바이트 정수형 표현시 2^0~ 2^7 총 8개의 비트 시퀸스로 표현을 한다. 이 표기법에 따라 제일 중요한 비트부터 왼쪽에서 써내려가면 상대적으로 작은 값은 오른쪽에서 쓰게 된다. 제일 작은 비트인 LSB는 난수생성이나 해시함수, 체크섬 함수등에 사용되고, 제일 큰 MSB는 signed 탑의 경우 부호를 나타낸다. unsigned 타입은 이 MSB까지 숫자 표현에 사용하기 때문에 표현 가능범위가 양수방향으로 2배가 됨.

바이트 오더링(빅엔디안, 리틀엔디안)에 따라 MSB, LSB 방향은 바뀔 수 있음. LSB가 낮은 쪽의 주소에서 먼저 나오는 비트 시퀸스이면 리틀 엔디안, MSB가 먼저 나오면 빅엔디안이다.

  • 비트오버플로우가 발생하면 MSB를 벗어난 데이터가 인접 비트를 덮어쓰게 된다.
  • java 8부터 Integer, Long 래퍼 클래스에서 Integer.parseUnsignedInt() 등을 이용해서 unsigned 범위를 이용할 수 있다. 타 언어로 짠 서버의 직렬화 데이터를 받아오는 경우, 네트워크 통신 같은 특수한 경우에 사용할 수 있음
  • 돈 계산하는 로직을 짤 때는 float, double 타입으로 부동소수점 연산을 하면 안된다. 정확하지 않은 값이 나올 수 있음. 실수형은 java.math.BigDecimal을 사용해야 정확하게 짤 수 있음
  • 변수명 작성 시 숫자로 시작X, _ $외 특수문자 사용X, 자바 키워드는 변수명으로 못 쓴다.
int unsigned = Integer.parseUnsignedInt("220000000");
System.out.println(Integer.toUnsignedString(unsigned));
BigDecimal bd = BigDecimal.valueOf(2_200_000_000L);

 

https://en.wikipedia.org/wiki/Double-precision_floating-point_format

- 실수 표현 시에 IEEE 754 표준인 부동소수점을 이용하여 64비트인 double형 실수 기준 1(부호)+11(지수부)+52(가수부)비트 를 이용하여 수를 표현한다.

3. Reference type

Primitive  타입 외의 모든 클래스는 레퍼런스 타입이다. String, Scanner, Randomm, int[], String[] 등...
자바는 기본 자료형을 제외하면 모두 포인터형이고, 데이터가 저장된 객체의 주소를 저장하게 된다. 따라서 포인터 연산이 불가능하고, 또한 두 참조형 변수간의 swap 연산이 불가능하다. 자바에서의 swap 구현은 이 포스트를 참고하자.

참조형 변수는 null 또는 객체주소(OS word 사이즈에 따라 32bit JVM이면 4바이트 0x0~0xFFFFFF, 64bit이면 8바이트의 주소범위를 가진다)

public void increaseAge(Human player, int age){ // Human의 경우 원본이 바뀜, int의 경우 원본은 바뀌지 않음
	player.age += age;
	age = 0;
}

// void increaseAgeRef(Human* player, int* age){...} // C 버전

int age = 10;
Human adam = new Human(); // adam.age: 20;
increaseAge(adam, age); // adam.age: 30, age: 10

https://courses.engr.illinois.edu/cs225/sp2022/resources/stack-heap/ 일반적인 메모리 구조

Primitive 타입으로 선언된 변수는 스택에 저장되고, Reference 타입으로 선언된 객체는 힙에 저장된다. 
레퍼런스 타입 변수는 동일한 타입의 다른 데이터를 가리키도록 다른 값을 할당 받을 수 없다(C의 포인터와 다른 점이다. 참조형 변수에 담긴 주소값을 명시적으로 직접 접근하거나 연산할 수 없다.). 

참조형 변수에 동일한 타입의 다른 데이터를 할당하면 참조형 변수가 다른 데이터를 가리키는 게 아니라 다른 데이터의 얕은 복사본이 참조형 변수가 가리키던 데이터를 덮어쓴다.
자바에서는 Wrapper 클래스를 이용해서 Primitive type을 마치 객체처럼 인스턴스로 다룰 수 있고, Boxing, unboxing을 통해 Primitive 타입 변수를 래핑하거나 다시 되돌릴 수 있다. 

Boolean bA = new Boolean(true);
Boolean bB = new Boolean(“false”);

Character cA = new Character(‘a’);

Byte byA = new Byte(10);
Byte byB = new Byte(“127”);

Short sA = new Short(1234);
Short sB = new Short(“1234”);

Integer iA = new Integer(1234);
Integer iB = new Integer(“1234”);

Long lA = new Long(1234);
Long lB = new Long(“1234”);

Float fA = new Float(12.34f);
Float fB = new Float(“12.34f”);

Double dA = new Double(12.34);
Double dB = new Double(“12.34”);
  • primitive type과의 차이점
    • java.lang 패키지에 포함되어 있음(String, StringBuffer, process, Runtime, thread, Math, StrictMath, Exception, Throwable, Error, Package, Class, ClassLoader, Wrapper, System, Stream)
    • reference 타입의 변수는 생성된 객체에 접근할 수 있다
    • 리터럴은 메모리의 stack에 저장되고, 변수는 heap에 저장되서 값이 들어있는 address가 할당된다
    • Null을 다룰 수 있다. Generic 사용 가능, 별도의 hashcode가 있음. boxing, unboxing의 오버헤드가 생길 수 있음.
  • boxing ↔ unboxing: 기본형 객체를 wrapper 클래스로 바꾸면 boxing-conversion, 그 반대는 unboxing-conversion
  • 코드 작성시에 추가적인 연산이 일어나는 박싱, 언박싱(오토박싱)이 최대한 일어나지 않도록 작성하는게 좋음

4. 리터럴

  • 어떤 변수에 의해 저장되는 값 그 자체.(변수는 값을 저장하는 공간, 리터럴은 값 그 자체) int year(변수) = 2021(리터럴)
  • Integer literals
    • long 타입은 L,l(대문자로 안하면 1,i랑 햇깔리므로 대문자로 작성하자) 로 끝나는 접미사를 붙여줌. 그외 나머지 숫자는 int
    • 진법에 따라 접두사를 붙여줌. 16진수: 0x 로 시작, 2진수: 0b 로 시작, 8진수: 0으로 시작
  • floating-point literals
    • float 타입은 F,f 로 끝나는 접미사를 붙여줌
    • 그 외 나머지는 double(optional: D 또는 d로 끝나는 값)
  • 그 외 문자, 문자열 리터럴
    • 유니코드 캐릭터
    • escape character 종류 
      • \b 백스페이스 \t 탭문자 \n 새 라인 \f form feed \r 캐리지리턴 \" 큰따옴표 \' 작은따옴표 \\ 역슬래쉬

5. 변수 타입별 스코프, 각각의 라이프 사이클

  • 모든 identifier들은 코드상에서 lexically하게 스코프를 가진다. 따라서 변수의 범위는 컴파일 시간에 결정되고, 함수 콜스택과는 무관함.
    • local variables(loop, block, bracket scope) - 대괄호 내에서만 scope를 가진다. 괄호블록이 끝나면 소멸됨
    • instance variables - 클래스가 인스턴스로 생성되면서 초기화됨. 해당 클래스의 모든 메서드, 생성자, 블록 내부에서 엑세스할 수 있음. 인스턴스가 소멸하면 소멸됨
    • class(static) variables - static modifier가 붙은 변수는 클래스가 로딩될 때 JVM class area에 올라가고 종료할 때까지 모든 객체가 메모리를 공유해서 전역으로 접근할 수 있음. 프로그램이 종료되기 전까지 메모리에 계속 상주함
    → 클래스변수는 클래스 로딩되는 컴파일 시점에 초기화된다. 그래서 런타임 이후에 생성되는 인스턴스를 참조할 수 없다.
  • variable shadow: method 내부에 instance variable과 동일한 이름의 변수가 있으면 하위 scope의 변수가 상위 scope의 변수를 가리게 됨(shadowing). 그런데 람다식으로 메서드를 작성하게 되면 shadowing이 일어나지 않는다. (클래스의 스코프를 따르게 됨)

6. type casting(명시적 형변환), type promotion(자동 형변환)

  • 명시적으로 변수 앞에 (type)을 붙여서 형변환하면 type-casting
  • 크기가 다른 자료형에 어떤 자료형을 대입할 때, 자동으로 그 자료형으로 변환되면 type-promition
    • numeric promotions: 숫자 간 연산 시에 변수들의 크기가 호환되지 않는 경우, 한쪽을 다른 한쪽 타입으로 변환해서 연산
  • 타입이 바뀌면 표현범위가 바뀐다 - 표현하지 못하는 비트가 생기면 값이 변한다
    • Widening, narrowing conversion: 타입 범위가 넓어지거나 작아짐 ex) int ↔ long...
      • 넓히는 경우에는 값이 왜곡되지 않지만 좁히는 경우에는 범위 밖의 비트를 표현 못하기 때문에 값이 변한다.
      • 정수형간의 타입 캐스팅, 실수형 간의 타입 캐스팅, 정수-실수형 간의 타입캐스팅 시 표현범위를 잘 체크하자
      • 다른 타입 간 연산시에는 기존의 값을 최대한 보존할 수 있는 타입(가장 넓은 표현범위)으로 자동 형변환 된다. 범위를 좁혀야되는 경우에는 반드시 명시적으로 형변환 연산자를 써주고 표현범위, precision 오차를 체크한다
    • string conversion: Wrappeer class의 toString()을 통해서 변환, 혹은 parse메서드를 통한 경우를 뜻함(Integer.parseInt, Long.parseLong)
      • Integer.toSting()은 null이 들어오면 NPE를 반환함. String.valueOf()는 “null”이라는 문자열로 반환함
      • any type + 문자열 = 문자열. 이걸 이용해도 파싱이 가능함 ex) 7 + “” = “7”, true + “” = “true”, null + “” = “null”
      • tip
  • auto boxing, auto unboxing conversion: Wrapper class가 primitive type과의 연산이나 값 저장이 이루어지는 경우 컴파일러에서 자동적으로 변환해주는 것을 뜻함. (JDK 5+ 부터 자바 컴파일러가 자동으로 처리함) 
  • casting: 명시적인 변환(primitive 타입간, 구현-상속 관계의 class간)

7. 타입 추론 var(JDK 10+)

  • type interference: 정적언어에서 컴파일러가 컴파일 시점에서 변수를 초기화하는 리터럴을 판단하여 해당타입으로 변환하는 것 → 자바에서는 var 키워드를 사용한다. 
  • 초기값이 리터럴로 없으면 안된다. 초기화값이 있는 지역변수로만 선언이 가능함. null 로 초기화 안됨, local 변수가 아니면 안됨, 타입 없이 배열 초기값을 넘겨도 안됨
1. 리스트 순회 시에 foreach문으로 사용할 때 편리하다

for(var name: students) {...}

2. 람다 인자에도 var를 넣을 수 있다(JDK 11+). 일반 람다의 경우 파라미터 어노테이션을 넣지 못하였다. 그래서 
클래스 내의 메서드로 빼버리던가 익명 클래스로 정의해야했는데, JDK 11부터 var를 사용하여 타입 추론 기능을 사용할 수 있게 되었다. 

Consumer<Person> pConsumer = (@Nonnull var person) -> {...}

3. 익명클래스 용도로 사용할 수 있다. 일반적인 변수는 타입추론이 어려워져서 잘 사용하지 않지만, 익명클래스 선언용도로 사용하면
유추가 쉬워진다.

var supply = new Supplier<String>() {
	@Override
    // getter, setter...
}

- 백기선님의 자바 온라인 스터디 https://github.com/whiteship/live-study 주제를 정리한 내용입니다.

참고
- 자바의 정석 2판