Technology/Java

Java의 변수에 대해 얇고 넓게 샅샅이 뜯어보자

ikjo 2022. 8. 1. 18:11
목차

1. 변수란 무엇인가?

2. 변수의 선언 방법

3. 변수의 타입에 대해 알아보자
  3-1. 프리미티브 타입
  3-2. 레퍼런스 타입
  3-3. 프리미티브 타입과 레퍼런스 타입의 차이
  3-4. 래퍼 클래스
  3-5. 명시적 타입 변환과 묵시적 타입 변환

4. 변수의 이름은 어떻게 정하지?

5. 변수의 종류
  5-1. 지역 변수
  5-2. 클래스 변수
  5-3. 인스턴스 변수

6. 변수의 초기화
  6-1. 기본값 초기화
  6-2. 유용한 final 키워드
  6-3. 여러가지 초기화 방법들

7. 리터럴?

8. 타입 추론과 var

9. 다수의 변수를 하나로 처리하기 위한 배열

 

 

변수(variable)란 무엇인가?

프로그래밍 언어에서의 변수란 '단 하나의 값을 저장할 수 있는 메모리 공간'을 의미한다. 여기서 주목해야할 점은 하나의 변수는 하나의 값만 저장된다는 것과 변수라는 게 결국에는 컴퓨터 메모리 공간 상에 자리잡고 있다는 것이다.

 

 

변수의 선언 방법

변수를 사용하려면 먼저 변수를 선언해야한다. 이때 변수를 선언하기 위해서는 변수의 타입(type)과 변수의 이름을 지정해줘야한다. 예를 들면 다음과 같다.

 

double weight; // double이라는 변수의 타입과 weight라는 변수의 이름 선언

 

 

변수의 타입에 대해 알아보자

여기서 말하는 변수의 타입은 변수에 저장될 값이 어떤 타입(종류)인지를 지정하는 것인데, 변수에 저장하고자 하는 값이 정수라면 정수형 타입을, 실수라면 실수형 타입을 지정해야한다.

 

이처럼 변수의 타입은 변수에 저장할 값이 어떤 종류인지 등에 따라 결정되는데, 값의 종류는 크게 '문자와 숫자'로 나눌 수 있으며, 숫자는 다시 '정수와 실수'로 나눌 수 있다. 이때 값의 종류에 따라 값이 저장될 공간의 크기와 저장형식을 정의한 것을 자료형(data type)이라고 하며 이 자료형을 변수의 타입으로 선택한다.

 

값의 종류, 범위, 특성 등 고려 → 자료형 결정 → 변수의 타입으로 선언

 

이러한 자료형은 크게 '프리미티브 타입(기본형)'과 '레퍼런스 타입(참조형)'으로 나눌 수 있다. 먼저 프리미티브 타입에 대해 알아보자.

 

프리미티브 타입(primitive type, 기본형)

프리미티브 타입의 변수는 실제 값(data)을 저장한다. 프리미티브 타입에는 모두 8개의 타입이 존재하는데 다음과 같이 논리형, 문자형, 정수형, 실수형으로 구분되며 이에 따라 크기, 데이터 범위, 기본 값이 상이하다.

 

 

우선 가장 간단한 논리형을 살펴보자 논리형은 boolean으로 나타내며 true와 false 중 하나를 값으로 가지며 조건식이나 논리적 계산에 사용된다. boolean은 다른 기본형과의 연산은 불가능하며, boolean 간의 연산만 가능하다.

 

정수형은 가장 많이 사용되는 자료형으로 4가지의 타입으로 나뉘는데, 각 타입마다 저장할 수 있는 값의 범위가 다르다. 이때 int의 경우 CPU가 가장 효율적으로 처리할 수 있는 타입으로 정수형의 기본 자료형에 해당된다. 예를 들어 1이라는 값은 기본적으로 int 타입으로 인식한다는 것이다.

※ byte의 경우 주로 이진 데이터를 다룰 때 사용되며, short은 C언어와의 호환을 위해 추가되었다고 한다.

 

실수형은 float과 double 2가지의 타입으로 나뉘는데, 이 중 기본 자료형은 double이다. 이들은 실수 값을 부동소수점 방식으로 저장하는 특성을 가지는데, 이 덕분에 정수형과 같은 크기를 갖더라도 훨씬 큰 수를 표현할 수 있다. 다만, 특정 자리 수 범위를 넘어가면 오차가 발생할 수 있다.

 

부동 소수점 방식에 대해서는 아래 글을 참고해볼 수 있다.

 

 

부동 소수점 방식에 대한 이해

부동 소수점의 등장 배경 부동 소수점(floating point)는 원어 그대로 둥둥 떠다니는 소수점수를 의미한다. 이는 컴퓨터가 2진수밖에 인식하지 못하기 때문에 생긴 것이다. 사람은 직관적으로 무한

ikjo.tistory.com

 

이로인해 float의 경우는 7자리까지의 정밀도를 가지고, double의 경우 15자리까지의 정밀도를 가진다. 따라서 돈과 같이 한치의 오차도 없이 작업을 처리해야하는 경우에는 자바에서 별도로 제공해주는 BigDecimal이라는 레퍼런스 타입을 활용해야 한다.

 

문자형은 char이라는 하나의 자료형을 가지는데, 해당 자료형으로 선언된 변수는 단 하나의 문자만을 저장할 수 있다. 자바에서는 '유니코드'라는 2byte 문자체계를 사용하므로 char의 크기는 2byte이며 문자를 내부적으로 정수(유니코드)로 저장하기 때문에 정수형이나 실수형과도 연산이 가능하다는 게 특징이다. (실제로 컴퓨터는 숫자만을 처리한다.) 예를 들어 char a = 'A'라고 선언하면 실제 a 변수에는 'A'의 정수 값인 65가 할당된다.

※ 이때 작은 따옴표(')를 사용해야한다는 점 그리고 ''과 같이 빈 값을 초기화할 수 없음에 유의하자

 

레퍼런스 타입(reference type, 참조형)

프리미티브 타입이 실제 값을 저장한다면, 레퍼런스 타입은 어떤 값이 저장되어 있는 주소(memory address)를 값으로 갖는다. (메모리에는 1byte 단위로 일련번호 즉, 메모리 주소가 붙어있다.) 앞서 프리미티브 타입 8개를 다루었는데, 이들을 제외한 나머지 모든 타입이 레퍼런스 타입이다.

※ 자바는 참조형 변수 간의 연산이 불가능하여 실제 연산에 사용되는 것은 모두 기본형 변수라고 한다.

 

참조형 변수를 선언 시 변수의 타입에는 클래스의 이름을 사용하는데, 클래스의 이름이 곧 참조형 변수의 타입이 되는 것이다. 앞서 잠시 언급했었던 BigDecimal이나 BigInteger 등의 클래스뿐만 아니라 String 역시 레퍼런스 타입이라고 할 수 있다.

 

참고로 프리미티브 타입 8가지 모두 기본으로 초기화되는 값이 있었는데, 레퍼런스 타입의 경우 초기화하지 않을 경우 null 값으로 초기화된다.

 

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

프리미티브 타입의 변수(이하 '기본형 변수')와 레퍼런스 타입의 변수(이하 '참조형 변수')의 차이는 우선 기본형 변수의 경우 실제 값을 저장하고 있는 반면, 참조형 변수의 경우 힙 영역에 저장되있는 객체의 메모리 주소 값을 저장한다는 것이다. (Java에서 객체는 힙 영역에 저장된다.) 참조형 변수는 해당 객체를 사용할 때마다 이 주소를 통해 불러와 사용하는 것이다.

 

또한 기본형 변수는 null을 저장 수 없지만, 참조형 변수의 경우 null을 저장할 수 있다는 차이가 있다. 아울러, 기본형 변수는 는 지네릭스로 사용할 수 없지만 참조형 변수는 지네릭스로 사용할 수 있다.

 

뭔가 참조형 변수가 기본형 변수보다 장점이 많아 보이지만, 접근 속도에서는 불리한 부분이 있다. 앞서 언급했듯이 참조형 변수의 경우 객체의 메모리 주소 값을 저장한다. 즉, 객체의 실제 값은 힙 영역에 저장되어 있는 것이다. 이를 참조하여 실제 값을 사용하기 위해서는 객체에서 얻어내는 별도의 과정이 필요하다.

 

또한 메모리적인 부분에 있어서도 불리한 점이 있다. 예를 들어 10,000,000이라는 같은 값을 사용하려고 하더라도 기본형 변수의 경우 int와 같은 프리미티브 타입의 자료형 크기(4byte)로 설정하면되지만, 이에 대응하는 래퍼 클래스인 Integer의 크기는 16byte로, 상당히 큰 정수를 저장할 수 있는 long의 자료형 크기(8byte) 보다도 크다.

 

하지만 하드웨어의 비약적인 발전에 따라 이 두 타입의 메모리 차이는 사실상 무의미한 차이로 보는 시선도 많다. 오히려 null 값을 처리해야하는 경우가 빈번한 웹 환경에서는 일관성을 위해 레퍼런스 타입만을 사용하는 경우도 있다고 한다.

 

래퍼 클래스

앞서 Integer라는 래퍼 클래스를 잠시 언급했는데, 래퍼 클래스란 프리미티브 타입의 값을 마치 객체로 다루기 위해 존재하는 클래스이다. 래퍼 클래스를 활용한다면 해당 값을 처리하기 위한 여러 메서드를 제공받을 수 있다. 자바에서 제공하는 8개의 프리미티브 타입은 모두 각각에 대응하는 래퍼 클래스를 가진다.

 

 

참고로 모든 래퍼 클래스는 Obejct 클래스의 하위 클래스에 속하며, 숫자를 다루는 래퍼 클래스인 Byte, Short, Integer, Long, Float, Double의 바로 위 상위 클래스는 Number 클래스이다.

 

이때 프리미티브 타입의 값을 래퍼 클래스로 변환하는 과정을 Boxing(박싱)이라고 하며, 반대로 래퍼 클래스를 프리미티브 타입의 값으로 변환하는 것을 Unboxing(언박싱)이라고 한다.

 

Integer num = new Integer(10); // 수동 박싱(프리미티브 타입 -> 래퍼 클래스)
int a = num.intValue(); // 수동 언박싱(래퍼 클래스 -> 프리미티브 타입)

 

실제로는 위 작업처럼 박싱, 언박싱하는 작업이 필요하지만 JDK 1.5부터는 이러한 작업이 모두 자동화되어 다음과 같이 변수에 할당만 해주어도 자동 박싱, 자동 언박싱이 가능해졌다.

 

Integer b = a; // 자동 박싱(프리미티브 타입 -> 래퍼 클래스)
int val = b; // 자동 언박싱(래퍼 클래스 -> 프리미티브 타입)

 

명시적 타입 변환(casting)과 묵시적 타입 변환(promotion)

타입 변환은 변수나 리터럴의 타입을 다른 타입으로 변환하는 것이다. 이러한 타입 변환이 필요한 경우에는 서로 다른 타입 간의 연산을 수행해야하는 경우를 예로 들 수 있다. 만일 int 타입의 값과 double 타입의 값을 연산하는 경우, 먼저 두 값을 같은 타입으로 변환하여 처리해야한다.

 

앞서 들었던 예시를 코드로 나타내면 아래와 같다.

 

        double weight = 13.5;
        int totalWeight = 10 + (int) weight;
        System.out.println(totalWeight); // 23

 

위 예시 코드에서는 double 타입의 변수를 int 타입으로 변환하여 연산을 처리했다. 이때  double타입은 int 타입 보다 크기가 더 큰 자료형으로서 캐스트 연산자를 명시하여 강제적으로 크기가 더 작은 자료형으로 변환시킨 것이다. 이로 인해 소수점 이하의 값들이 모두 '버림' 처리된 것을 확인할 수 있다. 이러한 형변환을 명시적 타입 변환(casting, 캐스팅)이라고한다.

 

참고로 여기서 사용된 괄호 ()는 '캐스트 연산자' 또는 '형변환 연산자'라고 하며 '(타입) 피연산자' 형태로 사용된다. (피연산자는 변수나 리터럴)

 

반대로 크기가 더 작은 자료형에서 크기가 더 큰 자료형으로 타입 변환하는 것을 묵시적 타입 변환(promotion, 프로모션)이라고 한다.

 

        int weight = 10;
        double totalWeight = 13.5 + weight;
        System.out.println(totalWeight); // 26.5

 

위 예시 코드는 이전과 정반대의 상황이다. int 타입의 변수를 별다른 타입 변환 처리 없이 실수값과 연산 처리를 했는데, 최종 결과는 실수값이 출력됐다. 이 경우에는 타입 변환이 편의상 생략됐을 뿐이지 실제로는 아래와 같이 컴파일러에 의해 int 타입 변수 앞에 캐스트 연산자가 추가되어 타입 변환이 이루어진다.

 

        int weight = 10;
        double totalWeight = 13.5 + (double) weight;
        System.out.println(totalWeight); // 26.5

 

이때 컴파일러는 기존의 값을 최대한 보존할 수 있는 타입으로 자동 변환해준다. 무슨 말이냐면 표현범위가 좁은 타입에서 넓은 타입으로 변환되는 경우에는 값 손실이 없으므로 두 타입 중에서 표현범위가 더 넓은 쪽으로 변환되는 것이다.

 

맨 처음 명시적 타입 변환을 예로 들었을 때 double 타입 변수 앞에 캐스트 연산자를 붙여주지 않았다면, 컴파일 에러가 나왔을 것이다. 이는 double 타입 변수의 표현 범위가 int 타입 변수의 표현 범위 보다 더 크기 때문이다. 묵시적 타입 변환을 예로 들었던 코드에서는 그 반대의 경우이므로 컴파일러가 자동으로 타입을 변환해준 것이다.

 

 

변수의 이름은 어떻게 정하지?

변수의 이름은 말 그대로 변수에 붙인 이름이다. 변수의 이름을 통해서 변수라는 메모리 공간 상에 값을 저장하기도하고 값을 읽어오기도할 수 있다. 이때 같은 스코프(유효 범위) 내에서는 같은 이름의 변수가 2개 이상 존재해서는 안된다. 같은 스코프 내에서 같은 이름이 2개 이상 존재하는 경우 컴파일 에러가 발생한다.

 

변수의 이름을 정하는데(Naming)에는 일종의 관례가 있다. 이 관례는 강제적인 것은 아니지만 코드의 유지보수 등을 위해 개발자들간 암묵적으로 지켜지는 것이다.

 

우선, 변수의 이름에 첫 번째로 나오는 단어는 항상 소문자로 시작해야 한다. 이때 여러 개의 단어로 구성된 경우에는 두 번째로 나오는 단어부터 첫 글자를 대문자로 시작한다. 이러한 표기 방법을 (lower) camelcase convention이라고 한다. 아울러 변수의 이름은 용도를 알기 쉽게 의미있는 이름으로 하는 것이 바람직하다.

 

Good case : userName, totalCount, ...

Bad case : UserName, a, b, c, ...

 

다만 해당 변수가 상수로 이용될 경우에는 모든 단어를 대문자로 표기하며, 여러 단어로 이루어진 경우에는 '_'를 통해 구분하도록 한다. 이러한 표기 방법은 snake convention이라고 한다.

 

Good case : SECRET_KEY, SERVER_URL, ...

Bad case : secret_key, serverUrl, ...

 

참고로 변수 역시 '식별자(identifier)'이므로 기본적으로 다음 규칙은 반드시 지켜야 한다. 이는 선택이 아닌 강제적인 규칙이다.

 

1. 대소문자가 구분되며 길이에 제한이 없다.

2. 예약어를 사용해서는 안 된다.

3. 숫자로 시작해서는 안 된다.

4. 특수문자는 '_'와 '$'만을 허용한다.

 

 

변수의 종류

앞서 지역 변수, 클래스 변수, 인스턴스 변수를 잠시 언급했는데, 이들을 모두 변수의 종류에 해당된다. 이때 변수의 종류를 결정하는 것은 '변수가 선언된 위치'이다.

 

각각의 변수별로 스코프(유효 범위)와 라이프 타임(생명 주기)에 대해 살펴보자.

 

지역 변수

우선 지역 변수 메서드 생성자 그리고 초기화 블럭 내부에 선언된다. 아울러 선언된 영역에서'만' 사용이 가능하며 메서드가 종료되면 소멸되어 사용할 수 없게 된다. 이러한 지역 변수는 JVM 메모리 영역 상 '스택'에 저장되게 된다. for문이나 while문의 블럭 내에서도 지역 변수를 선언할 수 있다. 이때 해당 지역 변수는 해당 구문의 블럭 내에서만 사용 가능하며, 블럭을 벗어나면 마찬가지로 소멸되어 사용할 수 없게 된다.

 

    void getMaxTime() {
        int maxTime = 0; // 지역변수
        
        // ...
    }

 

클래스 변수

그 다음은 클래스 변수를 살펴보자. 우선 클래스 변수와 인스턴스 변수는 '멤버 변수'라고도 한다. 클래스 변수의 경우 메서드 등이 아닌 클래스 영역에서 선언되며 변수의 타입 앞에 static이 붙은 것이 특징이다. 이는 정적인 데이터라는 것을 의미하며, 최초 클래스가 메모리에 로딩될 때 생생되어 프로그램이 종료될 때까지 유지된다.

 

이러한 클래스 변수는 해당 클래스의 모든 인스턴스가 해당 변수를 공유하는 특성이 있다. 즉, 한 클래스의 모든 인스턴스들이 공통적인 값을 유지해야하는 경우 사용된다. 

 

class Example1 {

    static int time = 0; // 클래스 변수
    
    // ...
}

class Example2 {

    void example() {
        Example1.time = 10;
    }
}

 

클래스 변수는 인스턴스 변수와 달리 외부 클래스에서 인스턴스를 생성하지 않고도 바로 사용할 수 있는데, 위 예시처럼 '클래스명.클래스변수명' 같은 형식으로 사용한다. 이때 클래스 변수의 접근 제어자를 public으로 설정한다면 마치 전역 변수처럼 어느 외부 클래스에서든 접근이 가능해진다.

 

인스턴스 변수

인스턴스 변수 역시 클래스 변수와 마찬가지로 멤버 변수이며, 클래스 영역에 선언된다. 다만, 클래스 변수와 달리 static이 붙지 않는 것이 특징이다. 인스턴스 변수의 경우 클래스의 인스턴스를 생성할 때 만들어지고 인스턴스별로 독립적인 값을 가지며 해당 인스턴스와 생명 주기를 같이 한다.

 

class Example1 {

    int time = 0; // 인스턴스 변수

    // ...
}

class Example2 {

    void example() {
        Example1 ex = new Example1();
        ex.time = 10;
    }
}

 

위 예시처럼 생성한 인스턴스의 참조형 변수를 통해 '인스턴스참조형변수명.인스턴스변수명' 과 같은 형식으로 외부에서 인스턴스별 인스턴스 변수에 접근할 수 있다.

 

참고로 클래스 변수와 인스턴스 변수에 접근할 때 사용한 예시 코드에서는 각 변수에 대한 접근 제어자가 생략되어 default로 선언된 것과 같아, 하나의 자바 소스 코드 파일 상의 다른 클래스에서 접근하는 것이 자유롭다. 실제로는 접근 제어자가 private, protected, default, public에 따라 상황별 접근 유무가 상이한데, 이에 대한 내용은 다음 글을 참고해볼 수 있다.

 

 

캡슐화(정보 은닉)를 위한 접근 제어자 이해하기

캡슐화란? 객체 지향에는 대표적인 4대 특성이 있는데 바로 추상화, 상속, 다형성, 캡슐화이다. 이 중 캡슐화는 정보 은닉이라고도 하는데 객체의 속성과 메서드를 하나로 묶고(하나의 객체에)

ikjo.tistory.com

 

 

변수의 초기화

앞서 타입과 이름을 통해 변수를 선언할 수 있다. 이로써 메모리의 빈 공간에 변수의 타입에 맞는 알맞은 크기의 저장 공간이 확보되고 이 저장 공간은 변수의 이름을 통해 사용할 수 있게 되었다. 이번에는 변수를 사용해보자.

 

우선 변수로부터 값을 읽어오기 위해서는 최초 변수의 값을 초기화해주어야 한다. 메모라라는 컴퓨터 자원은 여러 프로세스들이 공유하는 자원이기 때문에 전에 다른 프로그램에 의해 저장된 알 수 없는 값이 남아있을 수 있기 때문이다.

 

변수의 초기화라는 것은 결국 변수에 값을 저장하는 것인데, 다만 최초에 값을 저장하는 것이라고 볼 수 있다. 이때 지역 변수의 경우 초기화 하지 않고 해당 변수의 값을 읽으려고 할 경우 컴파일 에러가 발생한다.

 

double weight = 62.5;

 

기본값 초기화

다만, 클래스 변수나 인스턴스 변수는 초기화를 생략할 수 있고 해당 변수의 값을 읽으려고 해도 컴파일 에러가 발생하지 않는다. 그렇다면 이때 어떤 값이 출력될까?

 

앞서 프리미티브 타입과 레퍼런스 타입을 다루면서 각각의 타입별 기본적으로 초기화되는 값이 존재한다고 했다. 이는 최초 변수를 선언했을 때 값을 별도로 할당하지 않았을 때 컴파일러에 의해 자동으로 설정되는 값들이다. 따라서 별도로 초기화하지 않았어도 클래스 변수나 인스턴스 변수는 기본 값으로 초기화되어 이 값이 출력될 것이다.

※ 하지만, 최초 변수 선언 시 값을 별도로 할당하지 않는 것은 권장되지 않는 프로그래밍이라고 한다.

 

유용한 final 키워드

어떤 변수를 상수로 사용하는 경우에는 변수의 타입 앞에 'final'이라는 키워드를 붙여줄 수 있다. 이 경우 값을 한 번만 저장할 수 있게 된다. 즉, 이후에 누군가 수정할 수 없게 된다는 것이다. 상수의 용도에 맞는 기능을 가지게 되는 것이다.

 

여러가지 초기화 방법들

앞서 지역 변수와 달리 클래스 변수와 인스턴스 변수에 대한 초기화는 생략할 수 있다고 했다. 이때 클래스 변수와 인스턴스 변수를 초기화하는 데에는 여러가지 방법들이 있는데, 하나씩 살펴보자.

 

우선 변수를 선언함과 동시에 초기화하는 것을 명시적 초기화라고 한다.

 

class Example {

    static int time = 10; // 명시적 초기화
    
}

 

그 다음에는 초기화 블럭을 이용한 초기화 방법이 있다. 초기화 블럭에는 '클래스 초기화 블럭''인스턴스 초기화 블럭' 두 가지 종류가 있다. 이때 클래스 초기화 블럭은 클래스가 메모리에 로딩될 때 한 번만 수행되며, 인스턴스 초기화 블럭은 생성자와 같이 인스턴스를 생성할 때마다 수행된다.

 

class Example {

    static int money = 1000; // 클래스 변수
    int time = 0; // 인스턴스 변수

    // 클래스 초기화 블럭
    static {
        money = 2000;
    }

    // 인스턴스 초기화 블럭
    {
        time = 10;
    }
}

 

초기화 블럭을 이용하면 마치 메서드처럼 조건문, 반복문 등을 자유롭게 사율할 수 있어 복잡한 초기화 작업 시 유용하게 사용할 수 있다. 이때 초기화 블럭에 의한 초기화는 명시적 초기화 작업 이후에 수행된다.

 

인스턴스 변수의 경우에는 '생성자'를 통해 초기화를 시켜줄 수도 있다. 이때 생성자에 의한 초기화는 초기화 블럭 이후에 수행되는데, 인스턴스 변수 초기화 자체는 생성자를 사용하고, 생성자가 여러 개 있을 때 공통적으로 수행해야하는 로직을 초기화 블럭을 통해 수행하도록 한다.

 

class Example {

    int time = 0;

    Example(int time) {
        this.time = time;
    }
}

 

※ 초기화가 일어나는 순서

1. 클래스 초기화 : '컴파일러에 의한 기본값 초기화' → '명시적 초기화' → '클래스 초기화 블럭'

2. 인스턴스 초기화 : '컴파일러에 의한 기본값 초기화' → '명시적 초기화' → '인스턴스 초기화 블럭' → '생성자 초기화'

※ 클래스 변수는 항상 인스턴스 변수 보다 먼저 생성되고 초기화된다.

 

 

리터럴?

근데 변수를 초기화할 때 의문이 하나 들 수 있다. 예를 들어 프리미티브 타입의 변수에 값을 저장한다고 하는데, 이 값의 정체는 무엇이고 그 출처는 어딘가냐는 것이다.

 

우선 이 값의 정체는 자바에서 리터럴이라고 한다. 리터럴은 고정된 값을 나타내는 소스 코드로서 별도 계산 없이 작성한 소스 코드 그 자체로 표현되는 값이다. 이러한 리터럴은 프리미티브 타입의 변수에 할당하는데 사용된다. 앞서 weight라는 double형 변수에 62.5를 할당했는데, 이 62.5라는 값 그 자체가 리터럴인 것이다.

 

이 리터럴의 출처는 JVM 내 힙 영역에 있는 상수 저장소(Constant pool)이다. 개발자가 작성한 소스 코드를 자바 컴파일러가 컴파일하게 되면 클래스 파일이 생성되는데, 이 클래스 파일에는 소스 코드에 포함된 모든 리터럴의 목록이 존재한다. 이때 해당 클래스 파일이 클래스 로더에 의해 메모리에 올라가면 이 리터럴의 목록에 있는 리터럴들이 상수 저장소에 저장되는 것이다.

 

이러한 상수 저장소는 어디서든 공유되는 저장소로 예를 들어, "Apple"이라는 리터럴이 상수 저장소에 저장되어 있으면, 어떤 변수에 "Apple"을 할당하든 이 하나의 리터럴을 참조하게 되는 것이다. 만일 이러한 리터럴을 사용할 때마다 매번 메모리에 올린다면 매우 비효율적이었을 것이다.

 

 

타입 추론과 var

지금까지 변수를 선언하는 방법, 변수의 타입, 변수의 종류, 변수의 초기화 방법 등에 대해 살펴보았다. 지금까지 변수를 다루는데 있어 우선적으로 사용하고자 하는 값의 종류, 특성 등을 고려하여 자료형을 결정한 이후에 이를 변수의 타입으로 선언해주었다.

 

하지만 자바 10부터는 type inference 타입 추론 기능이 추가되었는데, 개발자가 변수의 타입을 명시적으로 적어주지 않고도 컴파일러가 알아서 변수에 할당된 리터럴로 타입을 추론해주는 기능인 것이다. 이때 변수의 타입 자리에는 var를 작성해주면된다.

 

class Main {

    public static void main(String[] args) {
        var fruit = "APPLE";
        System.out.println(fruit); // APPLE
        System.out.println(fruit instanceof String); // true
    }
}

 

위 예시 코드에서처럼 기존 변수의 타입 자리에 var를 작성해주었을 뿐인데, 프로그램 실행 시 해당 변수를 문자열 타입으로 처리해주고 있는 것을 확인할 수 있다.

 

이때 var를 사용함에 있어 유의해야할 점들이 몇가지 있다. 우선 var는 '초기화 값이 있는 지역변수'로만 선언이 가능하다는 점이다. var는 멤버 변수나 메서드의 파라미터 및 리턴 타입으로 사용이 불가능하며 반드시 값이 초기화되어야 한다. 다만, null 값이나 배열(ex. {"A", "B", "C"})로는 초기화할 수 없다.

 

또한 var는 키워드가 아니기 때문에 var라는 단어의 변수 이름을 명명할 수도 있다. var 변수는 컴파일 시점에 초기화된 값으로 추론되어 타입이 명시가된다. 따라서 런타임 시점에서 이 변수를 사용할 때마다 타입 추론 작업이 수행되는 것은 아니다. (개인적으로 아직까지는 var를 꼭 써야만 하는 당위성을 느끼지 못했다.)

 

 

다수의 변수를 하나로 처리하기 위한 배열

만일 값만 다른 같은 타입의 데이터가 무수히 많을 경우에는 어떻게 할까? 해당 개수만큼 변수를 하나하나 선언해서 사용하는 것은 작성하기도 힘들뿐더러 사용하는데에도 매우 비효율적일 것이다. 이때 같은 타입의 여러 변수를 하나의 묶음으로 다룰 수 있는 것을 '배열(array)'라고 한다.

 

변수의 경우 메모리에 생성될 때마다 불연속적인 메모리 주소를 가지게 된다. 하지만 배열의 경우에는 각각의 요소별 저장공간이 메모리 상에 연속적으로 배치되어 있어 인덱스를 통해 각각의 요소에 쉽고 빠르게 접근하는 것이 가능해진다.

 

    String[] fruits = new String[3];

    fruits[0] = "Apple";
    fruits[0] = "Melon";
    fruits[0] = "Banana";

 

위 예시 코드는 크기가 10인 문자열 타입의 1차원 배열을 선언한 것이다. 타입명과 대괄호 [ ] 그리고 new 연산자를 통해 쉽게 생성할 수 있으며 인덱스를 통해 각 요소에 접근 및 할당이 가능하다. (배열의 인덱스는 0부터 시작한다는 점에 유의하자) 또는 다음과 같이 중괄호 { }를 이용해 배열의 크기와 요소 값을 바로 할당할 수도 있다.

 

    String[] fruits = {"Apple", "Melon", "Banana"};
    // 또는 String[] fruits = new String[]{"Apple", "Melon", "Banana"};

 

배열은 다차원으로 활용할 수도 있다. 앞서 다루었던 배열은 1차원 배열로서 대괄호를 [ ]을 추가하면 아래와 같이 2차원 배열로도 선언할 수 있다. 이러한 2차원 배열은 테이블이나 그래프 형태의 데이터를 관리하는데 사용될 수 있다.

 

    int[][] graph = new int[3][3];
    // 또는 int[][] graph = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    // 또는 int[][] graph = new int[][]{ {1, 2, 3}, {4, 5, 6}, {7, 8, 9}};

 

 

참고자료

  • 도우출판 "자바의 정석"
  • 위키북스 "스프링 입문을 위한 자바 객체 지향의 원리와 이해"
  • https://catch-me-java.tistory.com/19