자바의 프리미티브 타입, 변수 그리고 배열을 사용하는 방법을 익힙니다.
자바에서 모든 변수들은 사용되기 전에 선언되어야 한다. 자바는 8가지의 premitive data type을 제공한다.
primitive type은 자바에서 제공하는 키워드를 통해 미리 선언된다. primitive type은 다른 privitive type의
value와 상태를 공유하지 않는다. primitive type은 JVM language stack에 저장된다.
primitive types는 다음과 같다.
bite
,short
,int
,long
,float
,double
,boolean
,char
8 bit(1 byte)의 부호가 있는 2's complement 정수이다. 최소값은 -128이며 최대값은 127이다. byte
타입은
메모리를 아끼는 것이 중요한 큰 배열에서 메모리를 아끼는데 유용하다. 또한 코드에서 값의 범위를 정의하고
싶을 때 int
를 대신하여 사용하는 것이 좋을 수 있다. 이는 변수의 값 범위를 문서를 제공하는 것과 같은
정보를 제공하는 것이다.
2's complement는 2의 보수라는 의미로 지정된 비트 수에서 음수를 표현하기 위한 방법을 말한다.
예를 들어, 1을 4 bit로 표현하면 `0001`이다. -1을 표현하기 위해서는 양수 1의 비트들을 반전한뒤 +1하면 된다.
`0001` -> `1110` -> `1111`(-1)
16 bit(2 byte)의 부호가 있는 2's complement 정수이다. 최소값은 -32,768이고 최대값은 23,767이다.
32 bit(4 byte)의 부호가 있는 2's complement 정수이다. 최소값은 -231이고 최대값은
231-1이다. Java 8부터 int
타입은 unsigned 32-bit integer을 표현하는데 사용될 수 있다.
64 bit(8 btye)의 2's complement 정수이다. 최소값은 -263이고 최대값은
263-1이다. int와 마찬가지로 unsigned long 타입을 지원하다.
32 bit(4 byte)의 부동소수점을 표현하는 단정밀도 실수이다. byte에서와 유사하게 float은 배열에서 double대신
사용하여 메모리를 아낄 수 있게 한다. float은 통화와 같은 정확한 값을 요구할 때는 사용하지 말아야 한다. 그럴 경우에는 java.meth.BigDecimal을 사용해야 한다.
64 bit(8 byte)의 부동소수점을 표현하는 배정밀도 실수이다. 소수점 값에 대해서 일반적으로 float이 아닌 double이 사용된다. double도 float과 마찬가지로 정확한 값을 요구하는 경우에는 사용하지 말아야 한다.
boolean은 1비트의 정보를 나타내지만, 그 크기는 정확하게 정의되어 있지 않다.
16 bit(2 byte)이며 유니코드 문자를 나타낸다. 최소값은 '\u0000'
(0) 이고 최대값은 '\uffff'
(65,535)이다.
각각의 primitive data type은 default value를 가지고 있다. 그러나 default value에 의존해서 프로그래밍하는 것은 바람직하지 않은 방법이다. 또한 기본값이 존재한다고해서 해당 기본값이 지역변수에 적용되는 것은 아니다. 만약 지역변수를 초기화하지 않고 사용하려고 한다면 기본값을 얻는 것이 아닌 컴파일 에러를 발생시킬 것이다.
primitive type size 출력
public class PrimitiveTest {
public static void main(String[] args) {
System.out.println("byte size : " + Byte.BYTES);
System.out.println("short size : " + Short.BYTES);
System.out.println("int size : " + Integer.BYTES);
System.out.println("long size : " + Long.BYTES);
System.out.println("float size : " + Float.BYTES);
System.out.println("double size : " + Double.BYTES);
// System.out.println("boolean size : " + Boolean.BYTES);
System.out.println("byte size : " + Character.BYTES);
}
}
byte size : 1
short size : 2
int size : 4
long size : 8
float size : 4
double size : 8
byte size : 2
Data Type | 크기(byte) | 최소값 | 최대값 | 기본값 |
---|---|---|---|---|
byte | 1 | -128 | 127 | 0 |
short | 2 | -32,768 | 32,767 | 0 |
int | 4 | -231 | 231-1 | 0 |
long | 8 | -263 | 263-1 | 0L |
float | 4 | (+/-)1.4 x 10-45 | (+/-)3.4 x 1038 | 0.0f |
double | 8 | (+/-)4.9 x 10-324 | (+/-)1.8 x 10308 | 0.0d |
char | 2 | ‘\u0000’(0) | ‘\uffff’(65.535) | ‘\u0000’ |
boolean | not defined | 0(false) | 1(true) | false |
래퍼런스 타입은 클래스 타입의 변수를 말한다. 동적으로 생성된 객체의 주소를 담고 있다. 래퍼런스 타입의 변수는 pre-defined되지 않으며 프로그래머에 의해서 생성된다.
래퍼런스 타입은 java.lang.Object
의 서브클래스이다. Object
클래스는 메모리에 저장된 객체에 접근할 수 있도록 한다.
객체의 주소는 java language stack에 저장되고, 실제 객체는 heap에 저장된다. 래퍼런스 타입은 c의 포인터와 유사하지만 포인터는 아니며,
객체를 다루는데 사용되는 타입이다.
래퍼런스 타입으로는 대표적으로 Annotation
,Array
,Class
,Enumeration
,Interface
가 있다.
Annotation
: 메타데이터를 프로그램의 요소와 연결할 수 있는 방법을 제공한다.Array
: 동일한 데이터 타입의 데이터들이 저장된, 고정된 크기의 데이터 구조를 제공한다.Class
: 상속, 다형성, 캡슐화를 제공하기 위해 만들어졌다. 일반저그올 실제 세계의 무언가를 모델링하며,Enumeration
: 선택할 수 있는 값들을 묶어놓은 집합의 래퍼런스 이다.Interface
: 자바 클래스에 의해 구현되는 public API를 제공한다.래퍼런스 타입에 대해 설명하기 위해 프리미티브 타입과 래퍼런스 타입을 비교하고자 한다.
두 타입의 가장 큰 차이는, 프리미티브 타입은 항상 value를 갖지만 래퍼런스 타입은 null을 가질 수 있다는 것이다. 만약, 프리미티브 타입 변수를
선언하고 초기화하지 않는다면 default value가 저장될 것이다. 그러나 래퍼런스 타입 변수는 초기화하지 않는다면 null 값을 갖게 된다.
프리미티브 타입 변수간에 =
연산자를 사용하면 해당 변수의 값이 복사될 것이다. 그러나 래퍼런스 타입의 변수간에 =
연산자를 사용하면
객체가 복사되는 것이 아닌 객체의 주소가 복사된다. 이는 마치 하나의 객체에 대해 두 개의 래퍼런스 타입 변수가 가리키는 것과 같이 작동한다.
프리미티브 변수에서는 각각의 변수가 가지고 있는 값을 비교한다. 하지만 래퍼런스 변수에서는 해당 변수들이 가리키고 있는 객체를 비교하지 않고,
래퍼런스 변수가 가리키는 주소를 비교한다. 따라서, 두 객체가 논리적으로 완전히 동일하더라도 객체가 위치하고 있는 메모리 주소가 다르기 때문에
래퍼런스 변수에서 ==
를 통한 비교는 false
로 나올 것이다. 래퍼런스 타입 변수간에 비교를 수행하려면 equals()
메소드를 사용해야 한다.
해당 메소드는 두 객체가 논리적으로 동일한지 확인한다.
프리미티브 변수를 메소드의 전달인자로 넘겨주면 해당 변수의 값이 전달된다. 따라서 메소드에서 넘겨받은 전달인자를 수정하더라도 실제 프리미티브 변수는 수정되지 않는다.
즉, 메소드의 local scope에 해당하는 것이다. 하지만, 래퍼런스 변수를 넘겨줄 경우 래퍼런스 변수에 담긴 객체의 주소가 넘어가게 된다. 메소드에서 래퍼런스 타입 전달인자를
통해 해당 객체를 수정할 경우 실제 객체 또한 수정된다. 하지만 래퍼런스 타입 전달인자가 가리키는 주소를 수정할 경우 래퍼런스 변수에 담긴 주소값은 수정되지 않는다.
프리미티브 변수를 메소드의 리턴값으로 반환하면 해당 변수의 값이 전달된다. 그러나 래퍼런스 변수의 경우 해당 변수에 담긴 주소값이 전달된다.
따라서 메소드가 종료된 이후에도 전달받은 래퍼런스 변수를 통해 객체에 접근할 수 있다. 하지만 프리미티브 변수는 지역 변수로 동작하기 떄문에 함수 종료시 사라지게 된다.
메소드에서 생성한 데이터를 전달하고 싶을 떄 래퍼런스 변수를 사용하여 이를 처리할 수 있다.
리터럴은 변수에 할당되는 상수값을 말한다.
int x = 123;
int x = 0123;
8진수 리터럴의 앞에 '0'을 붙인다.int x = 0x123;
16진수 리터럴의 앞에 '0x' or '0X'을 붙인다.int x = 0b1111;
2진수 리터럴의 앞에 '0b'를 붙인다.실수형 리터럴은 기본적으로 double d = 123.456;
표현식을 사용한다. 해당 리터럴이 double인지 float인지 표기하지 않아도되며 기본적으로 double로 인식된다.
만약 특정 데이터 타입으로 표기하고 싶을 떄는 double의 경우d
or D
를 값뒤에 붙이고, float의 경우f
or F
를 값뒤에 붙인다.
char ch = 'a';
작은 따옴표를 통해 단일 문자를 표현char ch = 062;
정수형 리터럴을 통해 유니코드의 값을 표현. 범위는 unsigned int
와 동일char ch = '\u0061';
char ch = '\n';
모든 escape 구문은 문자형 리터럴로 사용될 수 있다.큰 따옴표에 감싸진 문자들의 조합은 String 리터럴로 다뤄진다.
String s = "Hello World!";
Boolean 리터럴로는 true
와 false
만 허용된다.
선언과 초기화하는 방법은 C와 유사하다. 그래서 바이트코드를 통해 내부적으로 어떻게 동작하는지 확인하고자 한다.
public class Main {
public static void main(String[] args) {
int a;
a = 2;
int b = 3;
TestClass testClass = new TestClass();
}
}
class TestClass{
int b;
TestClass(){
b = 4;
}
}
Compiled from "Main.java"
public class Main {
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_2
1: istore_1
2: iconst_3
3: istore_2
4: new #2 // class TestClass
7: dup
8: invokespecial #3 // Method TestClass."<init>":()V
11: astore_3
12: return
}
먼저 바이트코드의 명령어에 대한 설명이 필요하다.
a
: reference typei
: primitive type - integericonst_2
: '2'를 stack에 push한다.istore_1
: stack에서 pop하여 jvm stack영역에 있는 local variable array의 1번 변수에 push한다.iconst_3
: '3'를 stack에 push한다.istore_2
: stack에서 pop하여 jvm stack영역에 있는 local variable array의 2번 변수에 push한다.new #2
: constant pool에서 2번 index에 있는 클래스의 인스턴스를 생성하고, 해당 객체의 address를 stack에 push한다.dup
: stack의 top에 있는 data를 복사하여 stack에 push한다.invokespecial #3
: stack의 top에 있는 객체를 가져오고, constant pool에서 3번 index에 있는 해당 객체의 메소드를 호출한다.astore_3
: stack에서 pop하여 jvm stack영역에 있는 local variable array의 2번 변수에 push한다.명령어에 대한 설명을 적었지만, 아직 바이트코드를 이해하기 위해서는 해결해야 할 질문이 존재한다.
new
를 통해 객체를 생성했는데, 왜 dup
를 통해 복제하는 것일까?자바의 클래스 생성자는 void를 리턴한다. 즉, 호출된 이후에 객체의 주소를 리턴하는 것이 아니라, 해당 객체를 initialize하는 작업만을 한다는 것이다.
만약 dup
를 수행하지 않고 생성자를 호출했을 떄를 생각해보자. 이 경우 invokespecial
은 객체의 주소를 필요로 하기 떄문에 stack에 있는 객체의 주소를 pop할 것이다.
또한, 생성자는 void를 리턴하기 때문에 생성자 종료 이후 stack에는 객체의 주소가 존재하지 않는다. 그렇다면 astore_3
명령어에서 객체의 래퍼런스를
loacl variable array에 저장하는 해야하는데 객체의 래퍼런스가 없기 때문에 해당 메소드를 수행할 수 없게 된다.
다시말해서, invokespecial
은 객체의 주소를 필요로 하고 void를 리턴하기 때문에 dup
를 통해 invokespecial
을 위한 객체의 주소를 복제하는 것이다.
TestClass testClass = new TestClass();
라는 코드를 봤을 때 단순히 생성자가 객체를 생성하여 리턴하는 구나라고 생각하였다.
하지만, 생성자는 단지 initialize작업만 할 뿐, 실제 객체를 생성하는 것은 new
라는것을 알게되었다.
stack은 cpu의 레지스터역할을 하고 있다. 연산을 수행하는 코드를 담은 assembly에서 연산값은 스택이 아닌 레지스터에 저장된다.
그리고 변수값을 할당할 때는 레지스터가 아닌 메모리에 저장하였다. 이와 유사하게 jvm에서도 stack은 레지스터처럼 일시적인 값을 담는 용도로 사용될 뿐이다.
하지만, 다른 점은 assembly에서는 로컬변수도 레지스터의 개수를 넘기지 않는 선에서 레지스터에 값을 저장하였지만, bytecode에서는 로컬변수를 위한
local variable array가 존재한다는 것이다.
변수의 스코프는 다음과 같이 나눌 수 있다.
Class scope
, Method Scope
, Loop Scope
, Bracket Scope
클래스의 brackets({}) 내부에 private으로 선언된 변수는 Class scope를 갖는다. Class scope는 Class 내부라면 어디서든지 해당 변수에 접근할 수 있음을 의미한다.
만약 변수앞에 접근자(access modifier)가 지정되있지 않으면 해당 변수는 동일한 패키지를 사용하는 모든 클래스에서 접근할 수 있게된다.
public class TestClass{
private int a = 0;
public void example1(){
a++;
}
public void example2(){
a--;
}
}
메소드 안에 선언된 변수는 해당 메소드 내부에서만 사용가능하다.
public void method(){
int a = 1;
}
for
,while
과 같은 Loop 내부에 선언된 변수는 해당 Loop에서만 사용가능하다.
public void method(){
while(true){
int a = 1;
}
}
{}
을 사용하여 추가적인 스코프를 정의할 수 있다.
public void method(){
int a = 1;
{
int b = 2;
}
}
만약 위 코드의 Bracket scope에서 int b
가 아닌 int a
를 선언하면 어떻게 될까?
해당 스코프내에서 a는 1이 아닌 2를 갖는 변수로 동작할 것이다. 이러한 것을 variable shadowing이라고
말한다. 이는 좋지 않는 방법이므로 this
를 사용하여 변수들을 구분하는 것이 올바른 방법이다.
자바는 boolean을 제외한 primitive type 간의 타입 변환을 허용한다. 다만, 큰 타입에서 작은 타입으로의 타입 변환은 데이터의 손실을 야기하며 이 경우 컴파일 에러가 발생한다.
자바에서 타입 변환은 묵시적 타입 변환(자동 타입 변환)과 명시적 타입 변환(강제 타입 변환)으로 나눠진다.
묵시적 타입 변환은 대입 연산이나 산술 연산에서 컴파일러가 자동으로 수행하는 형 변환이다.
double num1 = 10;
// int num2 = 3.14;
double num2 = 7.0f + 3.14
System.out.println(num1);
System.out.println(num3);
// 10.0
// 10.14
num1에는 int형 리터럴인 10
이 저장된다. Int에서 Double로 묵시적 타입 변환이 이뤄진다. 메모리 크기가 작은 타입에서 큰 타입으로 변환이 이뤄져 데이터 손실은 없다.
num2에서는 double형 리터럴이 int형에 타입 변환이 이뤄진다. 그러나 이 경우 메모리 크기가 큰 타입에서 작은 타입으로의 변환이므로 데이터 손실이 발생하며, 컴파일 에러가 발생한다.
num3에서는 float형 7.0f
와 double형 3.14
가 더해지는데, 이때 float형 리터럴이 double형으로 형 변환된다. 그러한 이유는 데이터 손실을 피하기 위함이다.
명시적 타입 변환은 타입 캐스트 연산자인 ( )
를 사용하여 강제로 타입을 변환하는 것이다.
int num1 = 1, num2 = 4;
double result1 = num1 / num2; // 0.0
double result2 = (double) num1 / num2; // 0.25
자바에서 산술 연산을 수행하고 얻은 결과값은 피연산자의 데이터 타입을 따르게 된다. num1과 num2는 int형 변수이므로 result1에는 0
이 저장되고, double형 변수에 담김으로 result1은 0.0
이 된다.
result2에서는 (double)
로 강제 형 변환을 수행하여 double형 결과값이 변수에 저장된다.
타입 캐스팅은 크기가 큰 data type을 크기가 작은 data type에 대입하는 것을 의미한다. 이 경우 데이터 손실이 발생할 수 있다.
만약 byte b = 10;
과 같은 리터럴을 이용한 대입이고 data type의 범위에 부합하면 데이터 손실 없이 정상적으로 컴파일된다. 그러나,
int a = 10;
byte b = a;
와 같이 더 큰 data type의 변수를 더 작은 data type에 대입하려고 한다면 데이터 손실 우려가 있기 때문에 컴파일 에러가 발생한다.
타입 프로모션은 크기가 작은 data type을 크기가 큰 data type에 대입하는 것을 의미하며, 타입 캐스팅과는 다르게 데이터 손실이 발생하지 않는다. 따라서 자동 형변환만으로 캐스팅이 가능하다.
형변환은 privitive type만이 아닌 reference type에서도 이뤄진다. reference type에서는 상속관계에 있는 객체간의 형변환이 이뤄진다. 이를 바인딩이라고 말하며, 바인딩은 동적 바인딩과 정적 바인딩으로 나눠진다. 동적 바인딩은 runtime에 정해지는 것이고, 정적 바인딩은 compile time에 이뤄지는 것이다.
public class SuperClass{
public void dynamicMethod(){
System.out.println("SuperClass's method");
}
public static void staticMethod(){
System.out.println("SuperClass's statc method");
}
}
public class SubClass extends SuperClass{
@Override
public void dynamicMethod(){
System.out.println("SubClass's method");
}
}
public class Main {
public static void main(String[] args) {
SuperClass superClass = new SubClass();
superClass.dynamicMethod();
}
}
위 코드의 main
메소드는 SubClass's method
를 출력한다. SuperClass형 reference 변수에 SubClass의 인스턴스가 대입되었다. superClass는 부모클래스 type의 변수이므로 SuperClass의 method를 호출할 것 같지만, 자식클래스의 메소드를 호출한다. 이러한 바인딩을 동적 바인딩이라고 말한다.
그렇다면, 동적 바인딩은 어떻게 가능한 것일까? 부모클래스 type의 변수인데 어떻게 자식클래스 type의 메소드를 호출할 수 있을까? 이는 jvm에서 reference type 변수가 어떤 변수인지 확인하면 알 수 있다.
reference type 변수는 c/c++
의 포인터와 유사하다고 설명했었다. 그런데 사실은 1개의 포인터가 아니라 2개의 포인터를 가지고 있는 것과 유사하고 동작한다. 첫번째 포인터는 reference type 변수에 담긴 객체의 클래스에 있는 메소드 테이블을 가리킨다. 두번째 포인터는 Heap에 저장된 객체를 가리킨다. JVM은 SubClass의 메소드들을 담은 테이블을 생성할 때 override된 메소드들은 그에 맞게 메소드의 address를 수정한다. 따라서 SuperClass type 변수더라도 가리키고 있는 객체는 SubClass 객체이므로 해당 객체의 클래스의 메소드 테이블에서 메소드를 탐색할 것이다. 이러한 원리를 바탕으로 동적바인딩을 수행한다.
정적 바인딩은 컴파일 타임에 실행될 메소드가 정해지는 것이다. static 메소드는 애초에 override가 불가능하며 static 메소드는 jvm의 method area에 미리 저장되기 때문에 정적 바인딩이 되는 것이다.
public static void main(String[] args) {
int[] list = new int[3];
list[0] = 0;
list[1] = 1;
list[2] = 2;
System.out.println(list.length);
for(int i = 0; i < list.length; i++){
System.out.println(list[i]);
}
}
1차 배열은 자료형[] 변수명
으로 선언할 수 있다. 배열의 길이는 고정되어있다. 따라서 new int[3]
과 같이 길이를 명시해주거나, {1,2,3}
과 같이
배열 선언시 내부 원소들도 함께 할당해줘야 한다.
int[] a = {1,2,3};
int[] b = a;
b[1] = 20;
for (int i = 0; i < a.length; i++){
System.out.println(a[i]);
}
for (int i = 0; i < a.length; i++){
System.out.println(b[i]);
}
// 1
// 20
// 3
// 1
// 20
// 3
1차 배열들 간에 =
연산자를 사용하여 할당하면 복사되는 변수에는 배열의 주소가 할당된다. 즉, b에는 a가 가리키는 배열의 주소가 담기게 된다.
따라서, 배열 b의 원소를 수정하면 a의 원소도 수정되게 된다. 사실 원소가 수정된다기 보다 a,b 모두 동일한 배열의 주소를 가리키기 떄문에 발생하는 것이다.
배열의 주소를 복사하는 것이 아니라 배열 그 자체를 복사하려고 한다면, clone()
메소드를 사용해야 한다.
int[] a = {1,2,3};
int[] b = a.clone();
b[1] = 20;
for (int i = 0; i < a.length; i++){
System.out.println(a[i]);
}
for (int i = 0; i < a.length; i++){
System.out.println(b[i]);
}
// 1
// 20
// 3
// 1
// 20
// 3
서로 타입이 다른 배열은 할당될 수 없다.
java의 2차 배열은 c/c++과는 다르게 가장 앞단에 있는 배열의 길이만 지정해주면 된다.
예를 들어, 만약 3x2 배열이라면, int[] list = new int[3][]
으로 선언해주면 된다. 이때 int[3][] 에서 1차에 해당하는 부분에는 크기의 배열이
들어와도 정상적으로 동작한다. 즉, 서로 크기 다른 배열들도 2차원 배열에 들어올 수 있다는 것이다.
int[][] plist = new int[3][];
plist[0] = new int[3];
plist[1] = new int[4];
plist[2] = new int[5];
for(int i = 0; i < plist.length; i++){
System.out.println(plist[i].length);
}
// 3
// 4
// 5
자바10부터 타입 추론을 지원한다. 타입 추론은 var
를 명시하여 사용할 수 있으며 자동으로 해당 변수의 타입을 추론하여 지정하는 것을 말한다.
타입 추론의 주요 특징은 다음과 같다.