본문 바로가기
Java

[자바 라이브 스터디] 02. 자바 데이터 타입, 변수와 배열

by 매트(Mat) 2021. 9. 16.

2주차 과제: 자바 데이터 타입, 변수 그리고 배열

목표

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

 

학습할 것

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




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

자바는 총 8가지의 기본 타입을 정의하여 제공하며, 크게 정수형, 실수형, 문자형, 논리형으로 나눌 수 있습니다.

 

논리형

  • Boolean
    • 참(true), 거짓(false)를 표현할 때 사용합니다.
    • 기본값: false
    • 할당되는 메모리 크기: 1byte
    • 데이터의 표현 범위: 소문자 true, false
boolean isChecked = true;
boolean isExist = false;

정수형

  • byte
    • 기본값: 0
    • 할당되는 메모리 크기: 1byte
    • 데이터의 표현 범위: -128 ~ 127
  • short
    • 기본값: 0
    • 할당되는 메모리 크기: 2byte
    • 데이터의 표현 범위: -32,768 ~ 32,767
  • int (기본 타입)
    • 기본값: 0
    • 할당되는 메모리 크기: 4byte
    • 데이터의 표현 범위: -2,147,483,648 ~ 2,147,483,647
    • 컴파일러는 기본적으로 정수 리터럴을 int 타입으로 간주
    • 정수 리터럴이 int 타입의 허용 범위를 초과할 경우 long 타입임을 컴파일러에게 명시해줘야 합니다.
  • long
    • 기본값: 0L
    • 할당되는 메모리 크기: 8byte
    • 데이터의 표현 범위: -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
    • long 타입일 경우에는 마지막에 'L' 을 꼭 붙여야 합니다.
byte a = -128;
short b = -32768;
int c = -2147483648;
long d = -9223372036854775808L;

실수형

  • float
    • 기본값: 0.0F
    • 할당되는 메모리 크기: 4 byte
    • 데이터의 표현 범위: (3.4 X 10^-38) ~ (3.4 X 10^38) 의 근사값
    • long 타입과 비슷하게 마지막에 'f' 또는 'F'를 붙여야 합니다.
  • double (기본 타입)
    • 기본값: 0.0
    • 할당되는 메모리 크기: 8 byte
    • 데이터의 표현 범위: (1.7 X 10^-308) ~ (1.7 X 10^308) 의 근사값
    • float 타입보다 2배의 정밀도를 갖는 타입입니다.
    • 실수 리터럴을 기본적으로 double 타입으로 해석합니다.
float flo1 = 0.1234567890123456789f; 
double doub1 = 0.1234567890123456789;

문자형

  • char
    • 기본값: '\u0000'
    • 할당되는 메모리 크기: 2 byte(유니코드)
    • 데이터의 표현 범위: 0 ~ 65,535
    • 작은 따옴표('')로 감싼 문자가 유니코드로 저장됩니다.
char var1 = 'A'; //유니코드: 65




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

프리미티브 타입

  • CPU나 OS에 따라 변하지 않습니다.
  • 비객체 타입으로 null값을 가질 수 없습니다. (기본값이 존재.)
  • stack 메모리에 바로 사용할 수 있는 값(실제 값)을 바이트 단위로 저장합니다.
  • 타입마다 저장되는 값의 범위가 다르고 이 범위를 초과하게 되면 오버플로우 현상이 발생합니다.
  • 각 Thread는 자신만의 stack을 갖습니다.
  • 지역 변수들은 scope에 따른 visibility를 갖습니다.
//기본 타입의 실제 값들이 stack 메모리에 올라가게 됩니다.
public class Main {
    public static void main(String[] args) {
        int num = 4;
        num = sum(num);
    }

    private static int sum(int param) {
        return param + 10;
    }
}

레퍼런스 타입

  • 레퍼런스 타입(Object, Integer, String, ArrayList, ...)은 heap 영역에 저장합니다.
  • 몇 개의 스레드가 존재하든 상관없이 단 하나의 heap 영역만 존재합니다.
  • Heap 영역에 있는 오브젝트들을 가리키는 레퍼런스 변수가 stack에 올라가게 됩니다.
  • https://yaboong.github.io/java/2018/05/26/java-memory-management/ 이 블로그를 참고하시면 정말 자세하게 stack 영역과 heap 영역에 대해 설명하고 있으니 꼭 보셔야 합니다!
//String은 heap 영역에 할당되고, stack에 host라는 이름으로 생성된 변수는 heap에 있는 "localhost" 라는 스트링을 레퍼런스 하게 됩니다.
public class Main {
    public static void main(String[] args) {
        String host = "localhost";
    }
}




리터럴

리터럴이란 데이터 그 자체를 뜻합니다. 변수에 넣는 변하지 않는 데이터를 의미합니다. 아래의 예시를 보죠

int a = 10;

여기서 리터럴은 10입니다. 즉, 1과 같이 변하지 않는 데이터(boolean, char, double, ...)를 리터럴(literal)이라고 부릅니다. 리터럴 종류로는 정수형 리터럴, 실수형 리터럴, 문자형 리터럴, 논리형 리털이 있습니다. 인스턴스는 동적으로 사용하기 위해 쓰임으로 리터럴이 될 수 없습니다.

 

리터럴과 헷갈리는 상수는 리터럴이 변하지 않는 데이터라면 상수는 변하지 않는 변수를 뜻합니다.

final int a = 10;

final 키워드를 붙이고 상수는 a가 됩니다.




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

int a = 10;

변수를 선언하는 방법은 어렵지 않습니다. a 라는 변수에 10이라는 값을 할당했습니다. 두 개로 쪼개보겠습니다.

int a; //변수 선언
a = 10;

변수를 선언하기 위해서는 변수타입 변수이름;으로 정의하셔야 합니다. 초기화하는 법도 간단합니다.

int a = 10;

이 코드가 초기화입니다. 즉, 변수타입 변수이름 = 초기화할 값;으로 정의하시면 됩니다. 초기화란 초기에 값을 설정하는 것을 말합니다. 자바에서 이런 초기화 선언을 반드시 해주셔야 합니다.

public class Main {
    static int a;
    public static void main(String[] args) {
        int a;
        System.out.println(a); //error!
    }
}

public class Main {
    static int a;
    public static void main(String[] args) {
        System.out.println(Main.a); //success!
    }
}

지역 변수는 초기화를 안해줄시 에러가 발생하고, 전역 변수는 기본값인 0이 출력이 됩니다. 만약 지역 변수를 초기화를 해주지 않으면 쓰레기값이 될 수 있기 때문에 초기화는 반드시 해주어야 합니다. 즉 예측할 수 없는 값이 들어가는 것을 방지하기 위해서 라고 추측이 됩니다. (확실X)




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

변수를 선언하고 사용할 때는 그 변수의 사용 가능한 범위를 갖습니다. 그 범위를 스코프라고 합니다.

public class Main {
    int globalScope = 10; //전역변수
    public static void main(String[] args) {
        Main m = new Main();
        m.scopeTest(3);
        System.out.println(localScope); //error!
        System.out.println(globalScope); //error!
    }

    public void scopeTest(int param) {
        int localScope = 10; //지역변수
        System.out.println(globalScope);
        System.out.println(localScope);
        System.out.println(param);
    }
}
  • globalScope는 클래스 내에서 선언된 변수이므로 globalScope의 사용 범위는 클래스 전체입니다.
  • param은 블럭 바깥에 존재하기는 하지만, 메소드 선언부에 존재하므로 사용범위는 해당 메소드 블럭내입니다.
  • localScope은 메소드 블럭내에서 선언되었기 때문에 역시 사용범위는 해당 메소드 블럭내입니다.

여기서 의문점이 들 수 있습니다. globalScope는 전역 변수인데 왜 main 메소드에서는 사용하지 못할까요?
그 이유는 static 메소드에서는 static 한 필드만 접근이 가능하기 때문입니다.

System.out.println(m.globalScope); //이런식으로 접근할 수 있습니다.

static

잠깐 static 에 대해 간략히 알아보겠습니다. static한 필드나 static한 메소드는 Class가 인스턴스화 되지 않아도 사용할 수 있습니다. 또한 static 변수는 값을 저장할 수 있는 공간이 하나만 생성됩니다. 따라서, 인스턴스가 여러 개 생성되도 static 변수는 하나입니다.

public class Main {
    int globalScope;
    static int staticVal;
    public static void main(String[] args) {
        Main m1 = new Main();
        Main m2 = new Main();

        m1.globalScope = 20; //20
        m2.globalScope = 30; //30

        System.out.println(m1.globalScope);
        System.out.println(m2.globalScope);

        m1.staticVal = 10; //20
        m2.staticVal = 20; //20

        System.out.println(m1.staticVal);
        System.out.println(m2.staticVal);
    }

}

코드를 보면 static 하지 않은 변수는 각 인스턴스별로 값이 다르게 출력되는게 확인되는데, static 한 변수는 자원을 공유하기 때문에 값이 20으로 똑같이 출력되는 것을 확인할 수 있습니다.

변수의 생명주기

변수의 생명주기란 변수를 선언함과 동시에 이 변수가 언제 생성되고 언제 죽는지를 말합니다.

public class Main {
    int globalScope; //인스턴스 변수, 필드, 전역 변수

    static int staticScope; //클래스 변수, 정적 변수

    void foo(int param) { //매개 변수, 파라미터
        int localScope; //지역 변수
    }
}
  1. 인스턴스 변수: 객체가 생성될 때 변수가 생성됩니다. 즉, 현재 Main 클래스를 static main 메소드나 다른 클래스에서 인스턴스를 생성할 때 변수가 생성이 됩니다. 인스턴스 변수는 참조가 없을 때 가비지 컬렉션이 객체를 지워버리는데 인스턴스 변수가 같이 소멸됩니다.
  2. 클래스 변수: 클래스 로드시에 생성이 되고, 자바 어플리케이션이 종료되는 시점에 소멸됩니다. 대부분의 경우 자바의 프로세스 시작시에 생성되고, 프로세스 종료시에 소멸됩니다.
  3. 매개 변수는 foo 메소드가 호출될 때 param 변수가 생성되고, foo 메소드가 종료 시점에 param 변수가 소멸됩니다.
  4. 지역 변수는 블록내에서만 사용이 가능합니다. 변수를 선언한 곳에서 생성되어 블록이 종료한 시점에서 소멸됩니다.

https://frontierdev.tistory.com/144 이 글 보시면 성능에 관해서도 어느정도 설명되어 있으니 참고하시면 좋을 것 같습니다.




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

타입 변환이란 현재 변수의 타입을 다른 타입으로 변환시키는 것을 말합니다. 타입 변환을 타입 캐스팅이라고도 합니다. (캐스팅이란 말을 자주 쓰죠.)

 

프리미티브 타입의 타입 캐스팅은 모두 서로 변환 가능합니다. 데이터 크기에 따라 데이터 손실이 발생할 수 있지만 결국엔 모두 캐스팅이 가능합니다. 여기서 데이터 손실이라함은 예를 들어 int 형의 데이터를 long 타입으로 변환할 경우에는 데이터 손실이 발생하지 않습니다. 왜냐하면 작은 데이터 -> 큰 데이터로 변환하는 경우에는 문제가 없습니다. 하지만 반대로 long 타입을 int 타입으로 변환할 경우에는 큰 데이터 -> 작은 데이터로 변환하려면 당연히 메모리 공간이 부족하기 때문에 데이터 손실이 날 수 밖에 없습니다.

 

레퍼런스 타입의 타입 캐스팅의 경우는 다릅니다. 이 때는 클래스 간의 형변환을 말하는 것이며, 반드시 상속 관계에서만 변환이 가능합니다. 즉, 상속 관계가 아니라면 변환이 불가능합니다. 캐스팅의 종류는 업캐스팅(Up Casting)과 다운캐스팅(Down Casting)이 있습니다.

 

업캐스팅(Up Casting)

업캐스팅은 자식클래스 인스턴스를 부모클래스 타입으로 변환하는 것을 말합니다. 이 때는 묵시적(자동) 형변환이 일어나고 부모클래스 타입으로 변환하기 때문에 부모클래스에서 선언된 변수나 메소드만 접근이 가능합니다. (참조 가능한 영역이 축소.)

public class Parent {
    public void parentPrint() {
        System.out.println("부모클래스의 출력 메소드");
    }

    public static void main(String[] args) {
        Child child = new Child();
        child.parentPrint(); //부모클래스로부터 상속받은 메소드
        child.childPrint(); //자식메소드

        Parent parent;
        parent = child;

        //Parent parent = new Child(); //업캐스팅 (이런식으로 표현이 가능합니다.)

        parent.parentPrint(); //부모메소드만 접근 가능
        //parent.childPrint(); //error!
    }
}

class Child extends Parent {
    public void childPrint() {
        System.out.println("자식클래스의 출력 메소드");
    }
}
  • Parent parent; : 먼저 부모클래스 타입의 레퍼런스 변수를 선언합니다.
  • parent = child; : 자식클래스 인스턴스를 부모클래스 타입의 레퍼런스 변수에 저장합니다. 이 때 자식 -> 부모 이므로 자동형변환이 일어납니다. 이것을 업캐스팅 이라고 합니다.
문법
부모클래스 부모 인스턴스;
자식클래스 = 자식 인스턴스;

부모클래스 부모 인스턴스 = new 자식클래스();



다운캐스팅(Down Casting)

업캐스팅과 반대로 부모클래스 타입의 인스턴스를 자식클래스 타입으로 변환하는 것을 말합니다. 이 때는 자동 형변환이 일어나지 않고 명시적(강제)으로 형변환을 해주어야 합니다. 안그러면 에러가 발생합니다. 자식클래스 타입으로 변환하기 때문에 부모, 자식클래스의 변수 메소드 모두 접근이 가능합니다. (참조 가능한 영역이 확대.) 단, 실제 존재하지 않은 영역에 대한 접근 위험성이 발생합니다.

 

명시적으로 형변환을 했는데도 에러가 발생할 수 있습니다.

Parent cannot be cast to com.azurealstn.sociallogin.Child

이런 에러가 발생할 수 있는데, 안전하게 하려면 업캐스팅된 인스턴스를 다시 다운캐스팅하는 경우에 형변환을 사용해주시면 됩니다.

public class Parent {
    public void parentPrint() {
        System.out.println("부모클래스의 출력 메소드");
    }

    public static void main(String[] args) {
        Parent parent = new Parent(); //업캐스팅이 안됐기에 에러 발생!
        Parent parent = new Child(); //업캐스팅

        Child child = (Child) parent; //명시적 형변환

        child.parentPrint(); //부모메소드
        child.childPrint(); //자식메소드

    }
}

class Child extends Parent {
    public void childPrint() {
        System.out.println("자식클래스의 출력 메소드");
    }
}
  • Parent parent = new Child(); : 자식 -> 부모 타입으로 업캐스팅을 시켜줍니다. (참조가능한 영역 축소)
  • Child child = (Child) parent; : 부모 -> 자식 이므로 명시적으로 형변환을 해줘야 합니다. 이것을 다운캐스팅 이라고 합니다.
문법
자식클래스 자식인스턴스 = (자식클래스) 부모인스턴스;

결론
이미 업캐스팅되어 참조 영역이 축소되었던 인스턴스를 다시 다운캐스팅을 통해 참조 영역이 확대되면 아무런 문제가 없으므로 사용이 가능합니다. 따라서, 레퍼런스 형변환시에는 반드시 상속 관계를 고려하여 알맞은 형변환 방식을 선택해서 사용해야 합니다.

 

위의 형변환이 가능한지 확인할 수 있는 메소드가 있습니다. 바로 instanceof 입니다.

System.out.println(parent instanceof Child);

이미 업캐스팅을 했기 때문에 이 실행 결과는 true가 반환될 것입니다. 하지만 업캐스팅을 안하면 false가 반환될 것입니다.

프로모션

캐스팅은 알겠는데 프로모션은 뭔가요?
간단하게 정리하면 다음과 같습니다.

  • 프로모션(묵시적, 자동 형변환) : 작은 데이터 -> 큰 데이터로 타입 형변환
  • 캐스팅(명시적, 강제 형변환) : 큰 데이터 -> 작은 데이터로 타입 형변환




1차 및 2차 배열 선언하기

먼저 배열의 특징은 메모리 구조가 정해져있기 때문에 데이터 삽입시 공간을 늘릴 수 없고, 데이터 삭제시에는 삭제된 데이터 공간에는 빈공간으로 그대로 남아있기 때문에 메모리 낭비가 발생합니다. 하지만 배열은 인덱스가 있기 때문에 조회시에는 속도 O(1)이라는 상당히 좋은 메리트를 가지고 있습니다. (보통은 ArrayList를 많이 사용합니다.)

public class Parent {
    public static void main(String[] args) {
        int[] arr; //배열 선언

        int[] arr1 = new int[5]; //배열 선언과 동시에 크기 할당

        int[] arr2;
        arr2 = new int[5]; //두 줄로 표현 가능

        int[] arr3 = {1, 2, 3, 4, 5}; //배열 초기화

        int[] arr4 = new int[] {1, 2, 3, 4, 5}; //배열 초기화

        //2차원 배열 선언
        int[][] arr5 = new int[3][4]; //3행 4열의 크기를 갖는 2차원 배열

        int[][] arr6 = { {1,2,3}, {4,5,6}, {7,8,9} }; //3행 3열의 크기를 갖는 2차원 배열 초기화
    }
}




타입 추론, var

타입 추론은 말 그대로 개발자가 변수의 타입을 명시적으로 적어주지 않아도 컴파일러가 알아서 이 변수의 타입을 대입된 리터럴로 추론하는 것입니다. 자바스크립트의 var라고 생각하시면 될 것 같아요.

public class Parent {
    public static void main(String[] args) {
        var a = "a의 타입은 String입니다.";
        System.out.println(a.getClass());
    }
}

이처럼 타입을 명시하지 않아도 변수 a의 타입 String이 나옵니다. 이런 var를 사용하는데 조건이 있습니다.
Var는 초기화값이 있는 지역 변수로만 선언이 가능합니다. 멤버변수, 메소드 파라미터, 리턴 타입으로는 사용이 불가능합니다.
더 자세히 알고싶으시다면 https://catch-me-java.tistory.com/19 이 글을 보시는 것을 추천합니다. 사용시 주의사항, 잘못된 사용법 등 많은 내용을 다루고 있습니다.




References

댓글