2주차 과제: 자바 데이터 타입, 변수 그리고 배열
목표: 자바의 프리미티브 타입, 변수 그리고 배열을 사용하는 방법 공부
- 프리미티브 타입 종류와 값의 범위 그리고 기본 값
- 프리미티브 타입과 레퍼런스 타입
- 리터럴
- 변수 선언 및 초기화 하는 방법
- 변수의 스코프와 라이프타임
- 타입 변환, 캐스팅 그리고 타입 프로모션
- 1차 및 2차 배열 선언하기
- 타입 추론, var
데이터 타입이란 해당 데이터가 메모리에 어떻게 저장되고, 프로그램에서 어떻게 처리되어야 하는지를 명시적으로 알려주는 것이다. Java에서 타입은 크게 기본형(프리미티브) 타입, 참조형(레퍼런스) 타입이 존재한다. 그 중 프리미티브 타입에 대해 먼저 알아보자.
프리미티브 타입(Primitive type)이란 기본형 타입을 뜻한다. Java는 총 8가지의 기본형 타입을 제공하며 각각의 타입은 기본 값이 존재한다. 즉, Null을 가질 수 없다. (Null을 넣고 싶다면 Wrapper 클래스를 활용해야 한다) 기본형 타입은 실제 값을 저장하는 공간으로 Stack 메모리에 저장된다. 다음은 기본형 타입의 종류다.
기본형 타입이 허용할 수 있는 데이터 범위를 넘게 되면, 컴파일 시 에러가 발생한다.
프리미티브 타입은 앞서 1장에서 소개했고, 레퍼런스 타입에 대해 알아본다.
레퍼런스 타입(Reference type)이란 참조형 타입을 뜻한다. 기본형 타입과 달리 빈 객체를 의미하는 Null이 존재한다. 기본형 타입과 가장 큰 차이점은 저장하는 공간, 저장하는 값의 차이다.
- 프리미티브 타입은 실제 값을 Stack 메모리에 저장한다.
- 레퍼런스 타입은 실제 값의 주소를 Heap 메모리에 저장한다.
레퍼런스 타입의 종류는 다음과 같다.
Heap 메모리에 생성된 인스턴스는 메소드나 각종 인터페이스에서 접근하기 위해 JVM의 Stack 영역에 존재하는 Frame에 일종의 포인터(C의 포인터와는 다름)인 참조값을 가지고 있어 이를 통해 인스턴스를 핸들링한다.
리터럴(literal)이란 선언된 변수에 값을 넣어 초기화 할 때 사용하는 값이며, 즉시값(immediate value)으로도 불린다.
int number; number = 10; <-- 10이 literal이다.
이와 같은 리터럴은 다음과 같이 정의할 수 있다.
변수에 넣는 변하지 않는 데이터
인스턴스와 같은 클래스 데이터들은 값이 변할 수 있기 때문에 리터럴이 될 수 없다. 하지만 불변 클래스(immutable class)와 같이 클래스 속 데이터가 변하지 않도록 설계한 경우(Java의 Color, String 등), 인스턴스 리터럴이라고 표현할 수 있다.
변수 선언은 간단하다. 다음과 같이 사용한다.
항상 세미콜론은 필수다.
<타입> <변수명>;
int i; string name; double total;
변수 선언과 초기화를 동시에 하면 다음과 같다.
<타입> <변수명> = <리터럴 값 (변수에 넣을 값)>;
int i = 10; string name = "유재석"; double total = 10.25;
프로그램에서 사용되는 변수들은 사용 가능한 범위를 가진다.
스코프란, 변수를 사용할 수 있는 범위를 얘기한다.
public class ValableScopeExam{
int globalScope = 10; // 인스턴스 변수
public void scopeTest(int value){
int localScope = 10;
System.out.println(globalScope);
System.out.println(localScpe);
System.out.println(value);
}
}
위와 같은 코드가 있다고 생각해보자.
클래스의 속성으로 선언된 globalScope 변수의 사용범위는 코드 전체다.
매개변수로 선언된 value 변수의 사용범위는 scopeTest 메서드 내다.
메서드 안에서 선언된 localScope의 사용범위는 scopeTest 메서드 내다.
다음은 main 메서드를 추가한 코드다.
public class VariableScopeExam {
int globalScope = 10;
public void scopeTest(int value){
int localScope = 20;
System.out.println(globalScope);
System.out.println(localScope);
System.out.println(value);
}
public static void main(String[] args) {
System.out.println(globalScope); //오류
System.out.println(localScope); //오류
System.out.println(value); //오류
}
}
주석으로 표시한 것 처럼 main 메서드 안에서 실행한 3줄의 코드는 오류가 발생한다. main 메서드는 static 한 메서드이기 때문에 변수 또한 static해야 하기 때문이다.
static이란 정적, 고정된 등의 뜻을 가진 단어로, 객체(인스턴스)에 소속된 멤버가 아닌, 클래스에 고정된 멤버를 선언하는 키워드다.
그렇기에 클래스 로더가 클래스를 로딩해서 메소드 메모리 영역에 적재할 때 클래스별로 관리한다. static 키워드를 통해 생성된 정적멤버들은 Heap영역이 아닌 static영역에 할당된다. static 영역에 할당된 메모리는 모든 객체가 공유하여 하나의 멤버를 어디서든지 참조할 수 있는 장점을 가지지만 Garbage Collector의 관리 영역 밖에 존재하기에 static영역에 있는 멤버들은 프로그램의 종료시까지 메모리가 할당된 채로 존재하게 된다. 그런 문제점 때문에, static을 너무 남발하게 되면 만들고자 하는 시스템 성능에 악영향을 줄 수 있다.
새로운 static 변수를 선언한 코드다.
public class VariableScopeExam {
int globalScope = 10;
static int staticVal = 7; //static으로 선언
public void scopeTest(int value){
int localScope = 20;
}
public static void main(String[] args) {
System.out.println(staticVal); //사용가능
}
}
앞서 설명한 것 처럼, main 메서드에서는 static 한 변수를 사용할 수 있다.
main 메서드에서 staticVal 변수를 사용할 수 있다.
static한 필드나, static한 메소드는 클래스가 인스턴스화 되지 않아도 사용할 수 있다. 아래와 같은 방법으로 static을 사용할 수 있다.
- static <타입> <변수명>;
- <접근권한> static class <클래스명> {}
Java의 라이프사이클이란 객체가 생성된 후부터 폐기될 때 까지를 뜻한다.
객체는 라이프사이클과 관련하여 7가지의 상태를 갖는다.
Created
: 객체를 위한 메모리 공간을 Heap 에 할당한다. 그 후 Super class의 생성자 호출을 하면서 initializer 및 instance variable의 initialize 를 수행한 후에 객체의 생성자를 수행한다.In use or reachable
: 객체가 생성되어 다른 객체에 의해 참조되거나 사용 중인 상태를 말한다. 이 상태를 Strongly referenced 상태 라고도 한다.Invisible
: 모든 객체가 이 상태를 거치는 것은 아니다. Invisible 상태는 Strongly referenced 는 되어 있지만 직접 접근할 수 없는 상태로 GC의 대상이 되지 않는다.Unreachable
: Strong reference가 존재하지 않을 경우를 말하며 이 상태의 객체는 GC의 후보가 된다. 후보가 된다고 해서 바로 GC가 되는 것은 아니지만 GC 대상 큐에 들어간다. 명확히 설명하자면 이러한 객체들은 GC의 루트가 가지는 체인에 참조되는 형태로 GC가 수행될 때 이 루트의 체인을 따라가며 메모리 해제를 한다.Collected
: 메모리 해제단계 도입부다. 이 상태에서는 GC가 객체의 finalize() 가 정의되어 있는지 판단하게 되고 finalize 가 있다면 finalizer 라는 queue 에 넣고 없다면 바로 다음 단계인 finalized 상태로 전환 시킨다.Finalized
: finalizer 를 통해 finalize가 실행된 후의 상태다. finalize는 Collected 상태라고 해서 바로 수행되는 것이 아니라 finalizer의 Queue에 들어가는 것이기 때문에 finalize가 호출되는 시간이 보장되지 않는다. 또한 JVM에 따라 수행시간도 다르며, finalize 가 구현되어 있다면 객체의 반환시간은 그만큼 더 늦어지고, finalize를 위한 객체의 메모리도 그만큼 증가한다.Deallocated
: 메모리 반환이 끝난 상태로 GC의 동작이 마무리 된 상태다.이외에도 변수의 3가지 종류에 따라 라이프 사이클을 구별할 수 있다.
로컬 변수: 처리 블록 내에서만 생존. 변수 선언부 ~ 블록 종료 시 까지
인스턴스 변수: 부모 객체 생성 ~ 부모 객체가 Grabage Selection 시 까지
클래스 변수: 가장 오랫동안 생존, 클래스 로드 시 ~ 클래스 언 로드 시 까지
하나의 타입을 다른 타입으로 변환하는 과정을 타입 변환이라고 한다. Java는 bool type을 제외한 나머지 기본형 타입 간의 타입 변환을 자유롭게 수행할 수 있다.
Java에서 다른 타입끼리의 연산은 먼저 피연산자들을 모두 같은 타입으로 만든 후에 수행된다. 메모리에 할당받은 바이트의 크기가 상대적으로 작은 타입에서 큰 타입으로의 타입 변환은 생략할 수 있다. 하지만 메모리에 할당받은 바이트의 크기가 큰 타입에서 작은 타입으로의 타입 변환은 데이터의 손실이 발생하기에, 상대적으로 바이트의 크기가 작은 타입으로 타입 변환을 할 경우 자바 컴파일러는 오류를 발생시킨다.
타입 변환의 종류는 2가지가 있다.
먼저 묵시적 타입 변환에 대해 알아보자.
묵시적 타입 변환이란 대입 연산이나 산술 연산에서 컴파일러가 자동으로 수행해주는 타입 변환을 뜻한다. Java에서는 데이터의 손실이 발생하지 않거나, 데이터의 손실이 최소화되는 방향으로 묵시적 타입 변환을 진행한다. 또한, 자바에서는 데이터의 손실이 발생하는 대입 연산은 허용하지 않는다.
다음 코드는 묵시적 타입 변환의 예다.
double num1 = 10; // int형인 10이 double로 타입 변환
// int num2 = 3.14;
double num3 = 7.0f + 3.14; // float형인 7.0이 double로 타입 변환
System.out.println(num1);
System.out.println(num3);
int
-> double
float
-> double
두 경우 모두 자바 컴파일러가 자동으로 작은 데이터 타입에서 큰 데이터 타입으로 변환했다. 변환 과정은 언제나 데이터의 손실을 최소화 하려 노력한다.
다음으로, 명시적 타입 변환에 대해 알아보자.
명시적 타입 변환이란 사용자가 타입 캐스트 연산자(())를 사용하여 강제적으로 수행하는 타입 변환을 가리킨다.
문법
(변환할 타입) 변환할 데이터
예시
int i = 10;
double number = 5.5;
number = number + (double) i;
변수 number 값: 15.5
변환시키고자 하는 변수(데이터) 앞에 괄호(())를 넣고, 그 괄호 안에 변환할 타입을 적으면 된다. Java에서는 이 괄호를 타입 캐스트(type cast) 연산자라고 한다.
1차 배열을 선언하는 방법은 다음과 같다.
int[] <배열명>;
int []<배열명>;
int <배열명>[];
1차 배열을 초기화 하는 방법은 다음과 같다.
int[] arr1;
arr1 = new int[<배열크기>];
int[] arr2 = new int[<배열크기>];
int[] arr3 = {1,2,3,4,5} // 배열 크기 5
초기화를 진행할 때는 new int[<배열크기>] or {배열 인자}를 이용해야 한다.
2차 배열을 선언하는 방법은 다음과 같다.
int[][] <배열명>;
int [] <배열명> [];
int <배열명>[][];
2차 배열을 초기화 하는 방법은 다음과 같다.
int[][] arr4;
arr4 = new int[<배열크기>][<배열크기>];
int[][] arr5 = new int[<배열크기>][<배열크기>];
int[][] arr6 = {{1,2}, {3,4}, {5,6}}
2차 배열을 생성할 때 열의 길이를 명시하지 않음으로써 행마다 길이가 다른 가변 배열을 생성할 수 있다.
타입추론이란 정적 타이핑을 지원하는 언어에서, 타입이 정해지지 않은 변수에 대해서 컴파일러가 변수의 타입을 스스로 찾아낼수 있도록 하는 기능이다.
Java 10부터 var
구문이 생겼다. var 문법을 통해 변수를 선언하게 되면 컴파일러가 알아서 변수의 타입을 결정한다. 아래와 같이 설명할 수 있다.
var a = 1; // a has type 'int'
var b = java.util.List.of(1, 2); // b has type 'List<Integer>'
var c = "x".getClass(); // c has type 'Class <? extends String>'
// (see JLS 15.12.2.6)
var d = new Object() {}; // d has the type of the anonymous class
var e = (CharSequence & Comparable<String>) "x";
// e has type CharSequence & Comparable<String>
var f = () -> "hello"; // Illegal: lambda not in an assignment context
var g = null; // Illegal: null type
var 문법은 지역변수 내에서 사용해야 하며, 선언과 초기화를 동시에 해야한다. var 문법을 for문, lambda, 익명 클래스 등에서 사용하면 좋을 것 같다.