읽는데 약 12분
자바 데이터 타입 및 변수
프리미티브 타입 종류와 값의 범위 그리고 기본 값
타입 | 크기(byte) | 최솟값 | 최댓값 | 기본값 |
---|---|---|---|---|
boolean | 1 | false | ||
byte | 1 | -27 | -27-1 | 0 |
char | 2 | 0 | 216-1 | ‘\u0000’ |
short | 2 | -215 | 215 -1 | 0 |
int | 4 | -231 | 231 -1 | 0 |
long | 8 | -263 | 263 -1 | 0L |
float | 4 | -3.4 * 1038 | 3.4 * 1038 | 0.0F |
double | 8 | -1.7 * 10308 | 1.7 * 10308 | 0.0 |
byte, short, int, long, float, double, boolean, char
기본값이 존재하기 때문에 Null로 초기화 할수 없다.
stack에 실제 값을 저장한다.
제네릭 타입에서 사용할 수 없다.
왜 제네릭 타입에서 사용할 수 없을까?
제네릭은 JDK1.5에 도입이 되었습니다. 이 때문에 하위 버전과의 호환성있는 개발을 위해 소거(erasure) 방식을 통해 제네릭을 구현했습니다. Type erasure은 원소 타입을 컴파일 타임에만 검사를 하고 런타임에는 해당 타입 정보를 제거하게 됩니다. 이 부분은 나중에 제네릭을 다룰때 자세히 다루겠습니다.
간단히 말해서 아래처럼 제네릭을 작성하게 되면
public class Container<T> {
private T data;
public T getData() {
return data;
}
}
컴파일 이후 Object로 변하게 됩니다.
public class Container {
private Object data;
public Object getData() {
return data;
}
}
이러한 제네릭의 타입을 non-reifiable type라고 부르기도 합니다. 컴파일러를 통해서 런타임에 발생할 수 있는 타입 관련 에러들을 처리할 수 있어 디버깅시 장점이 생깁니다. 즉 primitive type은 Object의 하위 클래스가 아니므로 사용할 수 없습니다.
Size of boolean
Java Tutorial에 따르면 boolean의 크기를 정확하게 정의하고 있지 않습니다.
boolean: The boolean data type has only two possible values: true and false. Use this data type for simple flags that track true/false conditions. This data type represents one bit of information, but its “size” isn’t something that’s precisely defined.
논리형 데이터 타입인 boolean은 true 혹은 false의 값을 가지고 이는 1bit로 충분히 표현가능합니다. 하지만 컴퓨터에서 정보를 처리하는 기본단위는 byte이므로 boolean을 처리하기 위해서는 최소 1바이트를 사용해야합니다.
Instead, expressions in the Java programming language that operate on boolean values are compiled to use values of the Java Virtual Machine int data type.
표현을 1바이트가 아닌 최소 1바이트라고 말한 이유는 만약 이를 통해 비교연산을 한다면 4byte로 형변환이 일어난 후 int처럼 해석을 하기 때문입니다. 자바는 연산을 수행할 때 스택을 기반으로 수행하기 때문에 만일 피연산자가 32bit보다 작다면 나머지 부분을 채우게 되고 32비트보다 크다면 2개의 cell을 사용합니다. cell 단위로 operation을 진행하면 연산시 사용되는 opcode가 줄어드는 장점이 있습니다.
Reference type
- Primitive type을 제외한 타입들을 의미한다.
- String, array, enum, class, interface
- Stack의 참조변수는 객체에 대한 주소를 저장한다. 기본값은 null
- Heap에 실제 객체를 저장한다.
- 제네릭타입에서 사용할 수 있다. ex)
List<Integer> list;
리터럴
- 컴파일 타임에 프로그램 안에 정의되어 있는 값
- 코드 내에서 직접 쓴 값
리터럴의 종류는 정수, 실수, 문자, 논리, 문자열 리터럴이 존재합니다. 아래 사각형에서 빨간색 부분이 리터럴이라고 할 수 있습니다.
정수 및 실수형 리터럴의 뒷 부분을 살펴보면 L
,F
,D
와 같은 접미사들이 있는 것을 확인할 수 있습니다. 이 접미사는 왜 사용하는 것일까요?
정수형 리터럴은 변수의 데이터 타입에 관계없이 기본적으로 JVM으로부터 4바이트를 할당받습니다. 정수형 리터럴이 4바이트보다 크다면 컴파일 에러가 발생하게 됩니다.
4바이트보다 큰 정수를 리터럴로 표현하기 위해서 JVM에게 8바이트를 할당 받아야하고 이를 명시적으로 알려주어야 합니다. 이를 위해 L
접미사를 사용합니다.
실수형 리터럴은 operand stack에 double 자료형 크기의 8바이트를 할당 받습니다. 그래서 D
접미사는 생략가능합니다.
자료형을 float으로 변경한다면 에러가 발생하게 됩니다. 실수형 리터럴을 위해 할당된 기본크기는 double 자료형의 8바이트이기 때문입니다.
기본적으로 할당받은 8바이트를 4바이트로 변경하려면 f
접미사를 붙이면 됩니다.
요약하자면 자바는 문자열 리터럴을 제외한 byte, short, int, long, float, double, boolean, char 리터럴은 operand stack에 적재 되었다가 사라지게 됩니다.
문자열 리터럴
그렇다면 문자열 리터럴은 어디에 저장이 되는 걸까요? 문자열 리터럴은 다른 리터럴과 다르게 사라지지 않고 string constant pool
영역에 남아있습니다. 좀 더 자세히 확인해보겠습니다.
자바에서 문자열을 생성하는 방법은 new
를 사용하는 방법과 문자열 리터럴을 사용하는 방법이 있습니다.
String s1 = new String("hello, world!");
String s2 = "hello, world!";
new
연산자를 사용할 경우 Heap
에 문자열이 저장되지만 리터럴을 사용하게 되면 string constant pool
에 저장됩니다.
이러한 차이를 확인하기 위해 .equals()
메서드와 ==
연산자를 사용해보겠습니다.
public void test() {
String s1 = new String("hello, world!");
String s2 = "hello, world!";
System.out.println(s1.equals(s2));
System.out.println(s1==s2);
}
실행결과
이러한 결과는 자바에서 ==
연산자를 사용할 때 객체의 값을 비교하는 것이 아닌 객체의 레퍼런스를 비교하기 때문입니다. Heap
과 string constant pool
의 주소가 다르므로 false
가 출력되는 모습을 확인할 수 있습니다.
변수의 스코프와 라이프타임
scope
란 해당 변수에 접근 가능한 범위를 의미하고 lifetime
이란 메모리에서 변수가 언제까지 살아있는가를 의미합니다.
변수의 종류에 따른 각각의 scope
와 lifetime
을 살펴보도록 하겠습니다.
Instance variable
클래스내에서 선언되지만 메서드 및 블록 외부에 선언되는 변수를 인스턴스 변수라고 합니다.
scope
: 정적 메서드를 제외한 모든 클래스lifetime
: 클래스의 객체가 메모리에 남아있을 때까지 남아있습니다.
main 메서드는 static
키워드가 존재하므로 Instance vaiable인 result
에 접근하지 못합니다. static
메서드는 객체의 생성없이 접근하는 함수인데 내부에서 사용하는 변수가 만약 Instance variable이라면 객체 생성을 요구하므로 모순이 됩니다.
Class variable
클래스 내에 선언되지만 메서드 및 블록 외부에 선언되면서 static
keyword를 포함하는 변수를 class variable이라고 합니다. 이러한 이유로 static variable이라고 부르기도 합니다.
scope
: 클래스 전체lifetime
: 프로그램 종료시까지
lifetime
이 프로그램 종료시까지 라는 것은 당연한 일입니다. Class variable은 인스턴스를 생성하지 않고도 프로그램 어디에서나 접근가능하기 때문에 프로그램 종료시까지 해당 variable을 알고 있어야 하기 때문입니다.
Local variable
Instance variable, Class variable을 제외한 모든 변수를 의미합니다.
scope
: 변수가 선언된 block 내부lifetime
: block의 실행이 종료될 때 까지
정리하면 아래와 같습니다.
public class scope_and_lifetime {
int num1, num2; //Instance Variables
static int result; //Class Variable
int add(int a, int b){ //Local Variables
num1 = a;
num2 = b;
return a+b;
}
public static void main(String args[]){
scope_and_lifetime ob = new scope_and_lifetime();
result = ob.add(10, 20);
System.out.println("Sum = " + result);
}
}
타입 변환, 캐스팅 그리고 타입 프로모션
primitive type
casting과 promotion 둘다 피연산자나 argument의 타입을 변환하는 것을 의미합니다. primitive type의 캐스팅 및 프로모션부터 알아보겠습니다.
double result = 9.4 / 2;
위 예제는 프로그래밍을 하다보면 우리 자연스럽게 사용하는 프로모션의 예시입니다. java operator은 동일한 타입의 operand를 요구합니다. 9.4는 double
에 해당하고 2는 int
에 해당하므로 타입을 일치시키기 위해 프로모션이 발생하게 됩니다. 즉, 실제로 이루어지는 연산은 아래와 같습니다.
double result = 9.4 / 2.0;
프로모션이 발생하여 2는 int
에서 double
로 형변환 되었고 operator은 동일한 타입을 가지고 연산을 수행하게 됩니다. 이처럼 promotion
은 암시적 타입 변환을 의미합니다. 프로그래머가 특별한 코드를 작성을 하지 않아도 발생합니다.
하나의 타입은 화살표가 가리키는 방향으로 프로모션이 발생할 수 있습니다. 특이한 점은 long
이 float
로 프로모션이 발생할 수 있다는 점입니다. long
은 8바이트이고 float
는 4바이트이지만 표현할 수 있는 실수의 범위가 float
이 훨씬 크기 때문에 float으로 프로모션이 가능합니다.
다른 예시를 살펴보겠습니다.
double result = 9 / 2;
답을 보기 전 이 계산식에 대한 결과값은 무엇일까요?
답안 보기 (👈 Click)
4.0
operator는 동일한 타입의 operand에 대해 연산을 수행하고 결과값은 4
가 나오게 됩니다. 결과값 int 4는 변수 double과 타입이 다르므로 int는 double로 프로모션이 발생하여 result의 값은 4.0이 됩니다.
casting은 프로모션과 달리 프로그래머가 명시적으로 syntax를 사용해 형변환을 하는것을 의미합니다. syntax는 아래와 같이 소괄호 안에 원하는 타입을 명시하면 됩니다.
int a = 65;
byte b = (byte)a;
큰 자료형이 작은 자료형으로 바뀌는 것이므로 데이터 손실이 일어날 수도 있습니다.
reference type
다음으로는 Object들의 type casting에 대해 알아보겠습니다. 상속관계에 있는 부모와 자식 클래스간에는 서로간에 형변환이 가능합니다. Object도 primitive 타입과 마찬가지로 프로모션과 캐스팅이 존재합니다.
아래는 프로모션, 다른 말로는 업캐스팅이라고 합니다. 자식 클래스가 부모클래스 타입으로 바뀌는 것을 의미합니다.
class Human{
// 생략
}
class Student extends Human{
// 생략
}
public class CastingTest {
public static void main(String[] args) {
Student student = new Student();
Human human = student;
// 자식 클래스(Student)가 부모 클래스(Human)타입으로 캐스팅
}
}
반대로 부모 클래스가 자식 클래스로 바뀌는 것은 다운 캐스팅이라고 합니다. 다운캐스팅은 업캐스팅이 먼저 선행된 후 진행되어야합니다. 아래는 업캐스팅 없이 다운캐스팅을 시도하는 예시입니다.
class Human{
// 생략
}
class Student extends Human{
// 생략
}
public class CastingTest {
public static void main(String[] args) {
Human human = new Human();
Student student = (Student) human; // 업캐스팅 없이 다운캐스팅 진행
}
}
class Human{
// 생략
}
class Student extends Human{
// 생략
}
public class CastingTest {
public static void main(String[] args) {
Human human = new Student(); // 업캐스팅
Student student = (Student) human; // 업캐스팅 이후 다운캐스팅 진행
}
}
1차 및 2차 배열 선언하기
class ArrayExample {
public static void main(String[] args) {
//1차원 배열
int[] array = {1, 2, 3, 4, 5};
int[] array2;
array2 = new int[10];
//2차원 배열
int[][] array3 = { {1, 2 }, {3, 4}};
int[][] array4;
array4 = new int[10][10];
}
}
변수 array
는 Heap에서 배열이 시작하는 주소를 저장하고 있습니다. 해당 변수들에 접근하기 위해서는 대괄호를 통해 접근하여 사용할 수 있습니다.
2차원 배열 또한 마찬가지 입니다. 변수 array3은 또한 1차원 배열이라고 생각하면 쉽습니다. 1차원 배열인데 가지고 있는 값들이 또 다른 배열의 시작주소들입니다.
타입 추론, var
var의 공식명칭은 local variable type inference로 소개합니다. Java 10에서 도입된 변수 var은 컴파일러가 타입을 추론합니다. 이 덕분에 런타임에서 추가적인 오버헤드를 필요로 하지 않습니다.
var str1 = "hello, world!"
유의할 점은 local variable에서만 사용가능하다는 점입니다. 멤버 변수, 메서드 파라미터, 리턴타입으로 사용될 수는 없습니다. var의 주된 장점은 boilerplate code를 없애준다는 점입니다.
사용 불가능한 곳
- method parameters
- method return type
- fields
사실 컴파일러상으로 구현이 가능은 하지만 그렇게 안하기로 결정을 했다고 하는데요. 구현하기 위해서는 컴파일러의 재설계를 요구하고 만약 method signature을 변경하면 이전 버전의 클래스 파일들과 source compatibility나 binary compatibility 문제를 야기하게 된다고 합니다.
필드도 마찬가지입니다. 자바에서는 라이브러리 작성 시 리플렉션을 통해 클래스의 정보를 읽어오는 경우가 많습니다. 스프링의 여러 어노테이션들의 내부 구현과정을 살펴보면 리플렉션을 활용하는 것을 알 수 있습니다. 이 때 필드의 타입이 var
라면 여러 문제가 발생할 수 있다고 합니다. 즉 이러한 이유로 local varaible에서 사용하도록 한정하고 있습니다.
사용가능한 곳
- Local variable within a method
- resource variable for
try-with-resource
try-catch-finally의 문제점을 보안하기 위해 나온 자바 문법으로, 사용한 자원에 대해 자동으로 반납을 해주는 구문입니다. 클래스가 autoclosable 인터페이스를 구현했다면 사용가능합니다.
try(var reader = new FileReader(...)){
...
}
- loop variable in for-each loop
for(var string : stringList){
...
}
사용법
var을 사용할때는 무조건 initializer을 사용해야하고 initializer은 well known type 이여야합니다.
var customer = new Customer();
var name = "Terry"
아래는 well known type이 아닌 예시입니다.
var customer;
var customer = null;
var func = (int a,int b) -> a+b;
null은 특정한 타입을 의미 하는것이 아니므로 위에서 언급한 well known type이 아닙니다.
reassigned?
compatible type에 관해서는 reaasign이 가능합니다
var name = "terry"
name = "dana"
name = List.of("Robin","Kim") // error
var은 initializer로부터 타입을 추론한 이후에는 type check가 string을 기준으로 이루어 집니다. 만약 reassigned을 허용하고 싶지 않다면 final keyword를 사용할 수 있습니다.
final var name = "Madison"
guideline
var을 사용하면 편리해보이지만 오히려 코드 가독성을 해칠 수도 있습니다.. 예를 들어 var result = obj.process
와 같은 코드입니다. var이 local variable이여서 사용가능하지만 이 문장을 보면 .process()
가 어떤 것을 리턴하는 알 수가 없어 가독성을 해치게 됩니다. 하지만 이것은 var에서 비롯되는 문제만은 아닙니다. 변수명과 메서드명으로부터 충분한 정보를 얻지 못한채 무분별하게 var
을 사용했기 때문입니다.
var
의 올바른 사용 예시라고 할 수 있습니다. 변수명과 메서드명으로부터 어떤 객체가 리턴되는지 이미 알고 있으므로 굳이 변수타입에 InputStream
을 반복할 필요가 없습니다.
다음은 try with resource예시입니다.
try-with-resource는 AutoClosable
인터페이스를 구현한 자원에 대해 자동으로 자원을 해제하는 기능을 합니다. try 안에 3개의 closable한 자원들이 들어있습니다. 이 경우 변수가 어디있는지 찾기도 복잡할 뿐더러 위에서 언급했듯 이미 변수명과 메서드에 어떤 역할을 하는지 충분히 파악이 되는 상태입니다. 즉, 이 경우 var
을 사용하기 좋은 케이스입니다.
다음은 Generic에서 wildcard를 사용하는 케이스를 살펴보겠습니다. 자바 라이브러리 코드를 작성한다면 메서드 시그니처안에 wild card를 사용해서 method implemetation에서 제한을 할 수 있습니다.
기존의 for loop구문은 시작조건, 종료조건, increment로 이루어져있습니다. 지금 이 사진으로만 봐서는 한눈에 파악이 되지 않습니다. 이러한 코드들은 실제 라이브러리에서 작성된 코드를 일부 변경한 실제 예시입니다.
제네릭에서의 wildcard의 사용은 소스코드를 보기 어렵게 합니다. 하지만 우리는 이 타입이 실제로 어떤 것을 의미하는지 정확히 알 필요가 없습니다. 이러한 경우 정확한 타입을 기재하는것이 부담스럽기 때문에 var을 활용하면 가독성이 증가하게 됩니다.
조심해야할것은 <>
operator와 var
을 함께 사용하는것입니다. initializer에서 diamond operator또한 type inference를 사용하기 때문입니다. 즉 <>
또한 type information을 얻어 와야하는데 생성자의 argument에서 얻어올수 있지만 수식의 left hand side에서 얻어올수도 있습니다.
마지막 경우는 <>는 어느 방향에서든 정보를 얻어올수가 없으므로 <>는 Object로 판단하고 ArrayList<Object>
로 만들게 됩니다. 이러한 케이스는 worst case이므로 사용해서는 안됩니다.
장점
- reduce verbosity and clutter
가끔은 어떤 변수들은 type information이 그렇게 중요하지 않을 수 있습니다. 그러한 경우 var로 교체하게 되면 타입 대신 다른 정보들에 집중할 수 있어 가독성이 증가하게 됩니다. var에 대해 비판하는 사람은 IDE의 도움을 받으면 된다고 하지만 해당 소스코드를 읽는 사람은 긴 타입 정보때문에 가독성이 낮아질 수 있으므로 var은 이러한 문제를 일으키지 않습니다.
shortcut circuit
자바의 logical operator중에는 논리곱(&&
)과 논리합(||
) 연산자가 있습니다. 이러한 연산자는 만약 우측항이 필요하지 않다면 실행하지 않는 것을 의미합니다.
false && ...
의 경우 우측항이 true, false에 관계없이 항상 false를 리턴하므로 우측항을 계산할 필요가 없습니다.true && ...
의 경우 우측항이 true, false에 관계없이 항상 true를 리턴하므로 우측항을 계산할 필요가 없습니다.
instanceof
instanceof는 객체가 주어진 타입에 속하는지 확인하는 binary operator입니다. 결과값은 true이거나 false입니다.
syntax
object instanceOf type
작동방식
instanceof operator은 is-a
원칙에 따라 작동하게 됩니다. is-a
관계는 클래스 상속이나 인터페이스 구현과 밀접한 관련이 있습니다.
public interface Shape {
}
public class Circle extends Round implements Shape {
// implementation details
}
circle is a circle은 참이므로 아래 결과는 True를 반환합니다.
public void test() {
Circle circle = new Circle();
System.out.println(circle instanceof Circle);
}
circle is round 역시 참이므로 아래 결과는 True를 반환합니다. 여기서 object는 type의 subclass여도 True를 반환하는 것을 알 수 있습니다.
public void test() {
Circle circle = new Circle();
System.out.println(circle instanceof Round);
}
circle is shape 역시 참입니다. object가 interface를 구현했다면 object는 interface type으로도 표현가능하므로 True를 리턴합니다.
public void test() {
Circle circle = new Circle();
System.out.println(circle instanceof Shape);
}
만약 object가 null이라면 false를 리턴합니다.
public void test() {
Circle circle = null;
System.out.println(circle instanceof Shape);
}
instanceof and Generics
object에 대한 검사는 런타임에 type information을 살펴보는 것과 관련이 있습니다. instanceof type에 generics을 사용하면 컴파일 에러가 발생하게 됩니다.
public static <T> void sort(List<T> collection) {
if (collection instanceof List<String>) {
// sort strings differently
}
// omitted
}
error: illegal generic type for instanceof
if (collection instanceof List<String>) {
^
이는 java에서 instanceof는 reifiable type과 사용하도록 정의했기 때문입니다. 글 초반부에서도 언급했듯이 제네릭은 non-reifiable type이기 입니다. reifiable type과 non-reifiable type의 차이점은 다음과 같습니다.
non-reifiable type
: type erasure에 의해 컴파일 타임에 타입정보가 사라지는 것. 런타임에 구체화 되지 않는다.reifiable type
: 자신의 타입정보를 런타임시에 알고 지키게 하는 것. 런타임 시에 완전하게 오브젝트 정보를 표현할 수 있다.
아래는 reifiable type의 종류입니다.
- Primitive types, like int
- Non-generic classes and interfaces, like String or Random
- Generic types in which all types are unbounded wildcards, like Set or Map
- Raw types, like List or HashMap
- Arrays of other reifiable types, like String[], List[], or Map[]
세 번째 케이스처럼 모든 제네릭이 non-reifiable type은 아닙니다. 모든 타입이 unbound wildcard인 경우 사용가능합니다.
public static <T> void sort(T test) {
if (test instanceof List<?>) {
System.out.println("hi");
}
// omitted
}
이를 실행해보면 hi가 출력됨을 확인할 수 있습니다.
화살표 연산자
자바에서 화살표 연산자는 람다를 의미합니다. 람다는 함수를 하나의 식으로 표현한 것을 의미합니다. 이 덕분에 메서드의 이름이 필요없어 익명함수 라고 부르기도 합니다.
이러한 익명함수들은 자바에서 일급 객체로 다루어집니다. 일급 객체란 무엇일까요?
- 변수나 데이터구조 안에 담을 수 있다.
- 파라미터로 전달할 수 있다
- 반환값으로 사용할 수 있다
syntax
(parameters) -> expression
(parameters) -> {statements;}
특징
- 반환값으로 함수형 인터페이스를 사용한다. 즉, 람다식에 접근하기 위해선 함수형 인터페이스를 사용해야 한다.
함수형 인터페이스
함수형 인터페이스란 1개의 추상 메서드를 가지고 있는 인터페이스를 의미합니다. 이를 선언하기 위해서는 @FunctionalInterface
어노테이션과 1개의 추상 메서드를 선언하면 됩니다.
만일 두개 이상의 메서드를 선언하면 오류가 발생합니다.
이를 이용해서 람다에 접근해보겠습니다. 람다는 일급 객체이므로 변수처럼 사용할 수 있는데 사용하는 타입을 명시할 때 함수형 인터페이스를 사용하면 됩니다.
자바에서 Stream은 파라미터로 함수형 인터페이스를 요구하고 람다는 함수형 인터페이스를 리턴함을 이해하고 있습니다. 하지만 람다식을 매번 정의할 때마다 함수형 인터페이스를 정의하기는 어려우므로 자바에서는 기본적으로 제공하는 함수형 인터페이스가 있습니다.
- 람다에서 final이거나 final처럼 쓰인 지역변수에만 접근할 수 있다.
출처
- https://www.codelatte.io/courses/java_programming_basic/U4QLQ40D1OO82S93
- https://madplay.github.io/post/java-string-literal-vs-string-object
- https://www.swtestacademy.com/primitive-and-reference-types-in-java/
- https://www.learningjournal.guru/article/programming-in-java/scope-and-lifetime-of-a-variable/
- https://coding-nyan.tistory.com/93
- https://www.youtube.com/watch?v=786iemaCJHU
- https://www.baeldung.com/java-instanceof
- https://applefarm.tistory.com/153
- https://8iggy.tistory.com/229