[Live Study] #2 자바 데이터 타입, 변수 그리고 배열

ohzzi·2021년 1월 12일
0

Live Study

목록 보기
2/13
post-thumbnail

목표

  • 자바의 프리미티브 타입, 변수 그리고 배열을 사용하는 방법을 익힙니다.

학습할 것

  • 프리미티브 타입 종류와 값의 범위 그리고 기본 값
  • 프리미티브 타입과 레퍼런스 타입
  • 리터럴
  • 변수 선언 및 초기화하는 방법
  • 변수의 스코프와 라이프타임
  • 타입 변환, 캐스팅 그리고 타입 프로모션
  • 1차 및 2차 배열 선언하기
  • 타입 추론, var

프리미티브 타입 종류와 값의 범위 그리고 기본 값

자바의 프리미티브 타입(Primitive type), 우리 말로 원시 자료형은 가장 기본적인 자료형으로, 자바에는 총 8가지의 프리미티브 타입이 존재한다. 이는 다음과 같다.

분류타입메모리 크기기본값표현 범위
논리형boolean1bytefalsetrue, false
정수형byte1byte0-128 ~ 127
정수형short2byte0-32,768 ~ 32,767
정수형int4byte0-2,147,483,648 ~ 2,147,483,647
정수형long8byte0L-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
실수형float4byte0.0F(3.4 X 10^-38) ~ (3.4 X 10^38) 의 근사값
실수형double8byte0.0(1.7 X 10^-308) ~ (1.7 X 10^308) 의 근사값
문자형char2byte (유니코드)'\u0000'0 ~ 65,535

각 자료형의 표현 범위를 알려면 우선 비트와 바이트에 대해서 이해해야 한다. 1byte는 8bit고, 1개의 bit는 0과 1 두 가지로 표현된다. 따라서 1 bit는 0~1의 범위를, 즉 212^1 만큼의 범위를 표현할 수 있다. 자료형의 크기가 1 bit씩 늘어날 때 마다 표현할 수 있는 경우의 수는 2배가 된다. (경우의 수 공식을 생각하면 이해가 빠를 것이다.) 예를 들어, 2bit 크기가 표현할 수 있는 범위는 00, 01, 10, 11의 총 4개이며, 4bit 크기가 표현할 수 있는 범위는 0000, 0001, 0010, 0011, ... , 1110, 1111로 총 24=162^4 = 16 만큼의 범위가 표현 가능하다.

앞서 말했듯이 1byte는 8bit이기 때문에, 1byte짜리 byte형은 28=2562^8 = 256 개의 수를 표현할 수 있다. 그런데 표현 범위를 보면 알 수 있듯이, byte 형은 0 ~ 255가 아닌 -128 ~ 127 만큼을 표현할 수 있다. 그 이유는 바로 최상위 비트가 부호를 나타내는 비트이기 때문이다. 컴퓨터 프로그래밍에서는 대체로 정수형 자료는 음수를 표현할 때 2의 보수를 사용하며, 이 때 최상위 비트를 부호 비트로 사용한다. 이를 MSB라고 하는데, 이 MSB가 0이면 양수, 1이면 음수다.

이렇게 최상위 비트가 MSB로 사용되어서 음수를 표현할 수 있는 자료형들을 signed 자료형이라고 하고, 기본적으로 자바의 정수형 자료형들은 signed이다. 만약 음수를 표현하지 않고 양수만 표현하고 싶다면, 자료형 앞에 unsigned를 붙여주면 최상위 비트를 MSB로 사용하지 않아서 표현 가능한 양수 범위가 두 배가 된다. 예를 들어, unsigned int는 0 ~ 4,294,967,295 를 표현할 수 있다.

실수형을 표현하는 방법에는 고정 소수점 방식(Fixed point system)과 부동 소수점 방식(Float point system - 不動(움직이지 않음)이 아니라 浮動(떠다님)이다!)으로 표현하는데, 자바는 실수형을 부동 소수점 방식으로 표현한다.

부동 소수점 방식이란 간단히 말해서 소수점의 위치가 고정되어 있지 않은 표현 방식인데, 부호, 지수, 가수로 구성되며 float는 부호 1비트, 지수 8비트, 가수 23비트로 총 32비트를 사용하고, double은 부호 1비트, 지수 11비트, 가수 52비트로 총 64비트를 사용한다. 그냥 실수를 나타내는 것이 아니라, 수를 이진 소수로 바꾼 뒤, 소수점을 이동 해서 가수와 지수의 자릿수를 맞추는 정규화 과정을 거쳐서 표현한다.

부동 소수점 방식에는 오차가 존재하는데, 이는 부동 소수점 방식으로 실수를 표현할 때 가수 부분을 이진 소수로 사용하기 때문이다. 즉, 1/2, 1/4, 1/8, ... 와 같이 1/2의 제곱수만 정확히 표현할 수 있고, 나머지 수들은 매우 작은 범위에서 근사해서 계산한다. 때문에 예를 들어 자바에서

public class Main {
    public static void main(String[] args) {
        float number = 0.001f + 0.001f + 0.0001f;
        System.out.println(number);
    }
}

와 같은 코드를 짠다면, 우리가 일반적으로 계산하는 실수처럼 0.0021이 아니라 0.0021000002가 계산되어 오차가 발생하게 된다.

프리미티브 타입과 레퍼런스 타입

앞서 프리미티브 타입을 이야기했는데, 프리미티브 타입 외에도 레퍼런스 타입(Reference type)도 존재한다. 프리미티브 타입과 레퍼런스 타입은 메모리 저장 방식에 있어서 차이가 있는데, 프리미티브 타입은 변수 선언시에 할당되는 메모리 공간에 직접 자료를 담는 자료형이고 레퍼런스 타입은 실제 값이 아닌 해당 값을 참조하는 주소값이 메모리 공간에 저장되는 자료형이다.

프리미티브 타입을 제외한 모든 자료형, 예를 들어 String, 프리미티브 타입의 wrapper (Byte, Short, Integer 등) 등 프리미티브 타입이 아닌 모든 자료형은 레퍼런스 타입이다. 레퍼런스 타입에는 빈 객체를 의미하는 Null이 존재한다.

앞서 프리미티브 타입이 메모리 공간에 직접 자료를 담는 자료형이라고 했다. 프리미티브 타입 변수에 값을 할당하면 이 값은 스택 영역(스택 영역과 힙 영역에 대해서는 1주차 참고)에 저장되는데, 이에 반해 레퍼런스 타입의 값은 힙 영역에 객체가 생성되고, 스택에는 해당 객체를 참조하는 주소값이 저장된다.

리터럴

리터럴(literal)은 소스 코드에 직접 입력된 값 자체로, 변수 초기화 시 저장할 데이터에 해당한다. 여기서 상수(constant)와 헷갈리면 안되는데, 상수는 값을 한 번 저장하면 변경할 수 없는 '변수' 를 의미한다. 리터럴은 변수를 의미하는 것이 아니라 변수에 저장되는 값을 의미한다. 자바의 리터럴에는 정수, 실수, 논리, 문자, 문자열 이 존재한다.

  • 정수 리터럴: byte, short, int, long 타입에 저장되는 정수 형태의 리터럴
    소수점이 없는 정수 리터럴 ex) 10, -128, 등
    0으로 시작하는 8진수 리터럴 ex) 01, -07 등
    0x로 시작하는 16진수 리터럴 ex) 0x05, 0xFF 등
    0b로 시작하는 이진수 리터럴 ex) 0b010, 0b101 등
    뒤에 L이 붙는 long 리터럴 ex) 1014L, 65328L 등

이 때, 뒤에 L을 붙여 표시하는 long 리터럴을 제외한 정수 리터럴은 int형으로 컴파일되며, long 리터럴은 long형으로 컴파일된다.

  • 실수 리터럴: float, double에 저장되는 실수 형태의 리터럴
    소수점이 있는 리터럴 (10 진수 실수) ex) 10.24, 3.141592 등
    E 또는 e 가 숫자 뒤에 존재하는 리터럴 (10 진수 지수와 가수 표현) 1234E-3 = 1.234 등

기본적으로 실수 타입 리터럴은 double로 변환되므로, float 리터럴인 경우 명시적으로 f나 F를 붙여줘야 한다.

  • 논리 문자열: boolean에 true, false 값을 저장하는 리터럴
    C언어와는 다르게 0, 1을 false, true로 boolean 타입에 사용할 수 없다.

  • 문자 리터럴: char에 저장되는 문자 형태의 리터럴
    작은 따옴표로 묶인 한 개의 텍스트 ex) 'A', 'b' 등
    특수한 이스케이프 문자 ex) '\n', '\t', '\"' 등

  • 문자열 리터럴: 문자열을 저장하는 리터럴
    큰따옴표로 묶인 문자열 ex) "Java", "Hello world!" 등

변수 선언 및 초기화하는 방법

자바에서는 변수의 타입을 적고 그 뒤에 변수의 이름을 적으면 변수를 선언 할 수 있다. 변수를 선언하면 메모리 공간이 할당되며, 만약 선언만 하고 초기화하지 않는다면 그 안에는 쓰레기 값이 들어있게 된다.

public class Main {
    public static void main(String[] args) {
        int number; // int형 변수 number 선언
        double double1, double2; // 같은 타입의 변수는 여러개를 한번에 선언 가능
    }
}

변수의 초기화에는 등호를 사용한다. 변수의 이름이 좌변에, 변수에 저장할 값이 우변에 온다.

public class Main {
    public static void main(String[] args) {
        int number1;
        number1 = 10; // 이미 선언되어 있는 int형 변수 number1에 10 저장
        double double1 = 3.14; // 선언과 동시에 초기화
        // 두 방식에는 차이가 없다.
    }
}

변수를 무조건 선언할 수 있는 것은 아니다. 변수 선언에는 몇가지 규칙이 존재한다.

  • 변수명의 길이는 제한이 없으며, 대소문자는 구분된다.
  • 변수명은 숫자로 시작해서는 안된다.
  • 변수명에는 알파벳, 일부 특수문자($, _), 숫자만 허용된다. (즉, 한글 사용 안 됨)
  • 변수명에는 공백이 들어갈 수 없다.
  • 변수명에는 예약어를 사용할 수 없다.
  • 올바른 변수명: num, value, const1, userName
  • 잘못된 변수명: int, %value, 1const, user name

변수의 스코프와 라이프타임

변수의 스코프란 변수에 접근할 수 있는 영역을 의미한다. 일반적으로 전역으로 선언되지 않은 변수는 선언된 중괄호 블록 내부에서만 접근이 가능하다. 단, 변수의 종류에 따라서 변수의 스코프가 조금 다르다.

변수의 라이프타임이란 변수가 메모리에 살아있는 기간을 의미하며, 이 또한 변수의 종류에 따라서 조금씩 다르다.

  • 인스턴스 변수: 클래스 내부와 모든 메소드 및 블록 외부에서 선언된 변수
    변수의 스코프: static 메소드를 제외한 클래스 전체에서 사용 가능
    변수의 라이프타임: 객체가 사라질 때 까지
public class Person {
    String name; // 인스턴스 변수

    public String getName() {
        return name; // 클래스 내부에서 사용 가능
    }
    
    // 사용 불가
    public static void main(String args[]) {
        System.out.println(name); // static 메소드에서는 사용할 수 없음
    }
}
  • 클래스 변수: 클래스 내부, 모든 블록 외부에서 선언되고 static으로 표시된 변수
    변수의 스코프: 클래스 전체 (static 메소드에서도 접근 가능)
    변수의 라이프타임: 프로그램이 끝날때까지 또는 클래스가 메모리에 로드 되는 동안
public class Person {
    static int ID; // 클래스 변수

    public String getID() {
        return ID; // 클래스 내부에서 사용 가능
    }
    
    public static void main(String args[]) {
        System.out.println(ID); // static 메소드에서도 사용 가능
    }
}
  • 지역 변수: 인스턴스 및 클래스 변수가 아닌 모든 변수
    변수의 스코프: 선언된 블록 ( {} )
    변수의 라이프타임: 선언된 블록 내부를 떠날 때 까지
public class Person {
    String name; // 인스턴스 변수
    static String ID; // 클래스 변수
    
    public void localTest1() {
        int a = 10;
        System.out.println(a); // 10이 정상적으로 출력
    }
    
    // 사용 불가
    public void localTest2() {
    	if (true) {
            int b = 20;
        }
        System.out.println(a); // 다른 블록의 변수를 사용할 수 없음
        System.out.println(b); // 블록 스코프 밖에서 변수를 사용할 수 없음
    }
}

주의할 점은 static하지 않은 변수들의 스코프에는 static 메소드가 들어가지 않는다는 점이다. static으로 선언된 변수 및 메소드들은 프로그램이 로드될 때 생성되며 단 하나만 존재한다. 따라서 static 키워드로 선언된 변수 및 메소드들이 생성된 시점에서 인스턴스의 변수 및 메소드를 참조할 수 없다.

즉, static 메소드, 예를 들어 public static void main(String args[]) 메소드 안에서는 static으로 선언된 변수들만 사용할 수 있다. (static 메소드 안에서 선언된 변수들 또한 static이므로 사용할 수 있다.)

타입 변환, 캐스팅 그리고 타입 프로모션

각 자료형마다 데이터를 저장하는 방식이 다르다. 예를 들어, int형은 4바이트 공간에 정수를 저장하고, float형은 4바이트 공간에 실수를 저장한다. 이런 자료형들, 특히나 프리미티브 타입 사이에는 서로 자유로운 변환이 가능한데, 이를 타입 변환이라고 한다.

이 때, 자바에서 타입이 변환되는 경우에는 두 가지가 존재한다.

  • 첫째, 크기가 더 작은 자료형을 크기가 더 큰 자료형으로 변환할 때 자동으로 형변환
  • 둘째, 크기가 더 큰 자료형을 크기가 더 작은 자료형으로 변환할 때 강제로 형변환
  • 주의할 점은, 여기서 말하는 크기는 데이터의 크기(바이트 수)가 아니라 표현형의 범위를 의미한다.

첫째의 경우를 타입 프로모션(자동 형변환), 둘째의 경우를 타입 캐스팅(명시적 형변환)이라고 한다. 타입 프로모션의 경우는 간단하다. 기존의 자료형의 표현 범위가 더 작기 때문에, 자료의 손실 없이도 형변환이 가능하다. 타입 프로모션은 말 그대로 프로그램이 실행되는 도중 자동으로 일어난다. 다음의 경우를 보자.

public class Main {
    int a = 10;
    long b = a;
}

int형 a에는 정수 리터럴 10이 저장된다. 그리고 이 a를 그대로 long형 b에 넣으면, 오류 없이 정상적으로 작동한다. int 형의 표현 범위보다 long 형의 표현 범위가 더 크기 때문에, 자료의 손실 없이 형변환이 자동으로 가능한 것이다.

그러나 표현 범위가 더 작은 자료형으로 변환을 한다면?

public class Main {
    float a = 3.14;
    int b = a;
}

아까 강조했듯이 타입 프로모션과 캐스팅을 결정하는 요소 중 '크기'는 자료형의 크기가 아니라 가능한 표현 범위다. 정확히 말하자면, 형변환 했을 때 데이터의 소실 여부다. int형과 float형 모두 4바이트의 메모리 공간을 할당받지만, float은 소수부를 표현할 수 있고, int형은 표현할 수 없다. 즉, float형의 자료를 강제로 int형에 쑤셔넣는 상황이 된다. 이런 경우에 오류가 발생하는데, 이때 "이 오류를 무시하고 형변환을 하겠다."가 타입 캐스팅이라고 보면 된다.

public class Main {
    float a = 3.14;
    int b = (int) a; // 형변환은 되지만 소수점 아래 0.14가 소실되어 3이 된다.
}

이처럼 ()안에 변환할 자료형을 넣어서 코드를 작성하게 되면, 데이터가 소실되더라도 강제로 형변환을 진행하는데, 이를 타입 캐스팅이라고 한다.

1차 및 2차 배열 선언하기

배열은 여러 개의 원소들을 저장할 수 있는 자료구조로, 레퍼런스 타입 자료형이다. 변수의 선언을 줄여주며, 자바의 배열은 한 번 크기를 지정하면 변경할 수 없다는 특징이 있다.

public class Main {
    String[] days; // 배열의 선언
}

배열은 위와 같이 배열에 들어가는 원소들의 자료형을 써 주고, 그 뒤에 []를 붙여 배열임을 표시해 준다. 이렇게 선언한 배열에는 배열 객체를 생성해서 초기화 해 주어야 한다.

public class Main {
    String[] days;
    days = new String[7]; // [] 안에 배열의 크기를 정수형으로 넣는다.
}

이렇게 생성된 배열에는 크기를 넘지 않는 선에서 데이터를 넣어줄 수 있다.

public class Main {
    String[] days;
    days = new String[7];
    days[0] = "월요일"; // 배열의 인덱스는 0부터 시작
    days[1] = "화요일";
    days[2] = "수요일";
    days[3] = "목요일";
    days[4] = "금요일";
    days[5] = "토요일";
    days[6] = "일요일";
    
    days = {"월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"};
}

아니면 배열을 선언할 때 new 키워드로 객체를 새로 생성할 필요 없이 바로 값을 넣어서 생성을 해 줄 수 도 있다.

public class Main {
	String[] days = {"월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"};
}

앞서 배열은 레퍼런스 타입이라고 했다. 따라서 이렇게 배열을 선언하고 생성하게 되면, 스택 영역에는 배열의 주소값이 저장되고, 힙 영역에 배열 안의 데이터가 생성된다.

만약 배열을 선언만 하고 값을 넣어주지 않았다면, 배열은 기본적으로 각 자료형의 기본값(예를 들어 int는 0)으로 저장된다. 다만, 선언만 하고 값을 넣어주지 않거나 new 키워드로 생성하지 않은 배열의 값은 저장되지 않기 때문에, 이 배열의 값에 접근하려면 오류가 발생한다.

그런데, 배열은 워드프로세서의 테이블처럼 2차원으로 만들 수 있다. 아니, 2차원을 넘어서 3차원, 4차원 등 n차원으로 만들 수 있다. 여기서는 2차원 배열만을 설명하도록 한다.

앞서 1차원 배열들은 String[]와 같이 인덱스가 들어가는 대괄호가 하나만 존재했다. 2차원 배열은 다음과 같이 대괄호가 두 개 붙는 배열이다.

public class Main {
    int[][] twoDimension;
}

이렇게 생성된 2차원 배열은 간단히 생각해서 배열 안에 배열이 들어가는 방식이다.

public class Main {
    int[][] twoDimension = {{1,2},{3,4}};
}

2차원 배열도 1차원 배열과 마찬가지로 초기화할 수 있다.

public class Main {
    int[][] twoDimension1 = new int[2][2];
    twoDimension1 = {{1,2},{3,4}};
    int[][] twoDimension2 = {{5,6},{7,8}};
}

앞서 배열의 주소값이 스택에 저장되고 배열의 데이터 값이 힙에 저장된다고 했는데, 2차원 배열은 조금 다르다. 2차원 배열은 1차원 배열 두 개가 합쳐진 것으로 이해할 수 있는데, 때문에 각각의 1차원 배열의 주소값도 저장해야 한다.

때문에 2차원 배열을 선언하면 힙 영역에 먼저 두 개의 1차원 배열의 주소값이 저장되고, 해당 주소에 1차원 배열의 데이터가 저장된다. 예를 들어,

public class Main {
    int[][] twoDimension = {{1,2},{3,4}};
}

와 같이 2차원 배열을 저장했다면, 스택 영역에는 twoDimension의 주소값이 저장된다. 그리고 해당 주소값의 힙 영역을 가보면, twoDimension[0][]과 twoDimension[1][]의 주소값이 저장되어 있다. 다시 해당 주소로 가 보면 각각 twoDimension[0][0], twoDimension[1][0]의 데이터가 저장되어 있다.

타입 추론, var

자바스크립트나 파이썬 같은 동적 타입 언어들은 변수를 선언할 때 따로 자료형을 명시해 주지 않는다. 예를 들어 자바스크립트는 변수 선언시 const, let, var만 구분해 주고 데이터를 넣으면 컴파일러나 인터프리터가 해당 데이터를 파악해서 자료형을 결정한다.

이렇게 개발자가 변수의 타입을 명시적으로 적어주지 않아도 컴파일러나 인터프리터가 변수의 타입을 대입된 리터럴을 통해서 추론하는 것을 타입 추론이라고 하는데, 자바에서는 제네릭과 람다에서 타입 추론을 볼 수 있다.

class MyArray<T> {
    T element;
    void setElement(T element) { this.element = element; }
    T getElement() {
        return element;
    }
}

MyArray<Integer> myArr = new MyArray<>();

위의 예시에서 new MyArray<>()의 <> 안에 타입을 명시하지 않았지만, 왼쪽에 이미 Integer라고 타입을 명시해 뒀기 때문에 컴파일러는 타입 추론을 통해 Integer 형이 들어간다고 판단하게 된다.

자바 10 이후부터는 일반 변수에도 타입 추론이 등장했다. 자바스크립트를 사용해 본 개발자라면 친숙한 var 키워드다.

public class Main {
    public static void main(String[] args) {
        var message = "Hello World!";
        System.out.println(message); // Hello World 출력
        var number = 10;
        System.out.println(number); // 10 출력
    }
}

위 예시 처럼 var는 타입 명시를 하지 않고도 할당된 리터럴에 따라 컴파일러가 타입 추론을 하게 된다.

다만 var를 사용하는데는 몇가지 제약 사항이 존재한다. 우선 자바 7에서 나온 다이아몬드 연산자 <>(제네릭의 그것)와 함께 사용할 수 없다. 또한 var는 반드시 로컬 변수로만 사용이 가능하다.

그리고 주의할 점은, 자바스크립트의 var처럼 타입이 자유로운 것이 아니라는 점이다. 예를 들어 var로 선언한 변수에 문자열 리터럴을 저장했다면, 해당 변수는 String 변수가 되기 때문에 다른 자료형을 할당하면 오류가 발생한다.

참고 자료
https://ndb796.tistory.com/5
https://wikidocs.net/81943
https://league-cat.tistory.com/411
http://www.tcpschool.com/java/java_generic_concept

profile
배울 것이 많은 초보 개발자 입니다!

0개의 댓글