본문 바로가기
Back-End/백기선님의 자바 스터디

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

by 7533ymh 2022. 3. 10.

목표

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

학습할 것

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

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

우리가 주로 사용하는 값의 종류는 크게 문자와 숫자로 나눌 수 있으며 숫자는 다시 정수와 실수로 나눌 수 있다.

기본형( primitive type )은 모두 8가지 타입( 자료형 )이 있으며, 크게 논리형, 문자형, 정수형, 실수형으로 구분된다.

타입 종류

타입 범위와 기본 값

비트와 바이트

  • 한 자리의 2진수를 비트(bit)라고 한다.
    • 1 비트는 컴퓨터가 값을 저장할 수 있는 최소 단위이다.
  • 1 비트는 너무 작은 단위이기 때문에 1비트 8개를 묶어서 바이트(byte) 라는 단위를 정의해서 데이터의 기본 단위로 사용한다.
  • 워드(word)는 CPU가 한번에 처리할 수 있는 데이터의 크기를 의미한다.
    • 워드의 크기는 CPU의 성능에 따라 달라진다.
      • 32비트 CPU는 1워드가 32비트
      • 64비트 CPU는 1워드가 64비트

비트로 표현할 수 있는 값

  • 1 bit ⇒ 0, 1 즉, 2개($2^1$)
  • 2bit ⇒ 4개 ($2^2$)
  • 즉, n비트로 $2^n$개의 값을 표현할 수 있다.
  • n비트로 10진수를 표현한다면, 표현가능한 10진수의 범위는 0 ~ $2^n$-1이 된다.

논리형

  • boolean
    • ture , false 중 하나를 저장할 수 있다.
    • 기본값은 false이다.

문자형

  • char
    • Char ch = ‘A’;
    • 단 하나의 문자만을 저장할 수 있다.
    • 변수에 문자가 저장되는 것 같지만 문자의 유니코드(정수)가 저장된다.
    • 그래서 문자 리터럴 대신 문자의 유니코드를 직접 저장할 수도 있다.
    • 음수를 나타낼 필요가 없으니 0 ~ $2^{16}$-1 표현가능

정수형

  • byte
  • short
  • int
    • 약 10자리의 수를 저장할 수 있다.
  • long

정수형의 표현형식과 범위

  • 모든 정수형은 부호있는 정수이므로 왼쪽의 첫 번째 비트를 부호 비트로 사용하고 나머지는 값을 표현하는데 사용한다.
  • 그래서 n비트로 표현할 수 있는 값의 개수인 $2^n$개에서, 절반인 ‘0’으로 시작하는 $2^{n-1}$개의 값을 양수(0도 포함)의 표현에 사용하고, 나머지 절반인 ‘1’로 시작하는 $2^{n-1}$개의 값은 음수의 표현에 사용된다.
  • 고로 정수형의 범위는 -$2^{n-1}$ ~ $2^{n-1}$ - 1 이 된다. ( -1을 빼는 이유는 0때문에 )

정수형의 선택기준

  • JVM의 피연산자 스택(operand stack)이 피연산자를 4byte단위로 저장하기 때문에 크기가 4 byte보다 작은 자료형(byte, short)을 계산할 때는 4 byte로 변환하여 연산이 수행되기 때문에 int형을 쓰는 것이 효율적이다.
  • 하지만 int형의 범위를 넘어버리게 되면 오버플로우가 발생하기 때문에 범위가 넘어간다면 long을 사용하면 된다.

unsigned

  • JAVA 8 이전까지는 unsigned는 없다.
    • char의 경우 unsigned 이긴 하다. ( 부호 없는 2byte )
  • JAVA 8부터는 부호없는 자료형을 만들 수 있는 클래스를 제공한다.
    • wrapper class인 Integer, Long에 있다.
    • unsigned이므로 더 많은 수를 저장할 수 있다.
int num = Integer.parseUnmsignedInt("4294967295");
String numString = Integer.toUnsignedString(num);
System.out.println(numString);
  • 하지만 더 많은 수를 저장하기 위해 unsigned를 사용하기 보다 BigInteger를 사용하는 것이 좋다.
BigInteger bigInteger = BigInteger.valueOf(2200000000L);

실수형

  • float
  • double

실수형의 범위와 정밀도

실수형은 정수형과 저장 방식이 다르기에 같은 크기라도 훨씬 큰 값을 표현할 수는 있지만, 오차가 발생할 수 있다.

그래서 정밀도가 중요한데, 정밀도가 높을 수록 오차의 범위가 줄어든다.

위 표를 보면 float의 정밀도는 7자리로 10진수로 7자리의 수를 오차없이 저장할 수 있다는 의미이다. 그렇기에 사용할 변수의 값의 범위가 7가지를 넘는다면 정밀도를 고려해 double 타입을 사용해야 한다.

실수형의 저장형식

  • 부동 소수점수의 형태로 저장한다.
    • S : 부호
    • E : 지수
      • 점을 찍을 위치를 정하는 부분
      • 부호있는 정수
      • 지수의 범위는 -127 ~ 128 (float), -1023 ~ 1024(double)
    • M : 가수
      • 실제값을 저장하는 부분
      • 10진수로 7자리(float), 15자리(double)의 정밀도로 저장 가능
  • 부동 소수점은 고정 소수점보단 표현 범위가 넓다 ( 가수 부분이 더 크기 때문에 )
    • 하지만 정확하지는 않다.
    • float number = 0f; for(int i = 0; i < 10; i++) { number += 0.1f; } System.out.println(number); ==================== 1.0000001
    • 정확한 계산은 BigDecimal을 사용해야 한다.
    • BigDecimal number = BigDecimal.ZERO; for(int i = 0; i < 10; i++) { number = number.add(BigDecimal.valueOf(0.1)); } System.out.println(number); ==================== 1.0

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

자료형은 크게 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있다.

  • 기본형(Primitive Type)
    • 실제 값(data)를 저장한다.
  • 참조형(Reference Type)
    • 객체의 주소를 저장한다. 기본적으로 Java.lang.Object를 상속받을 경우 참조형이 된다. 즉, 기본형을 제외하고는 모두 참조형이다.

추가적으로 기본형은 JVM 메모리의 스택영역에 실제 값들이 저장된다면, 참조형은 실제 인스턴스는 힙 영역에 생성되있고, 그 영역의 주소를 스택영역에서 저장하고 있다고 보면 된다.

int a = 2;
float b = 19.2;
Car c = new Car("kia", 100);

3. 리터럴

그 자체로 값을 의미하는 것

리터럴은 데이터 그 자체를 의미한다.

원래 12, 123와 같은 값들이 상수이지만 프로그래밍에서 상수를 값을 한번 저장하면 변경할 수 없는 저장공간으로 정의했기 때문에 이와 구분하기 위해서 리터럴이라는 용어를 사용한다.

그러니 리터럴은 단지 우리가 기존에 알고 있던 상수의 다른 이름일 뿐이다.

인스턴스는 리터럴이 될 수 있을까?

인스턴스안의 값의 불변셩이 보장된다면 객체 리터럴이 될 수 있다. ( 불변 클래스 )

하지만 이렇게 불변성을 보장하도록 설계된 클래스를 제외하고 보통의 인스턴스는 동적으로 사용되고 내용이 변할 수 있기 때문에 객체 리터럴이 될 수 없다.

리터럴의 타입과 접미사

정수 리터럴

  • 정수형 중 long타입의 리터럴에 접미사 L(l)을 붙여야 하고 없다면 int타입의 리터럴이다.
    • 10진수 이외에도 2, 8, 16진수로 표현된 리터럴을 변수에 저장할 수 있다.
    • 16진수는 접두사 0x(X), 8진수는 접두사 0을 붙인다.
    • int octNum = 010; // 8진수 10, 10진수로 8 int hexNum = 0x10; // 16진수 10, 10진수로 16 int binNum = 0b10; // 2진수 10, 10진수로 2
    • JAVA 7부터는 정수형 리터럴의 중간에 구분자’_’를 넣을 수 있다.
    • long big = 100_000_000_000;

실수 리터럴

  • 실수형 중 float타입의 리터럴에 접미사 F(f)를 붙여야 한다.
  • double타입의 리터럴에는 접미사 D(d)를 붙여야 하지만 실수형에서는 double이 기본 자료형이라서 생략이 가능하다.
double a = 0.1; // 0.1 
double b = 1e-1; // 0.1 
float c = 0.1f; // 0.1
  • 나머지는 접미사가 필요없다.

문자 리터럴

  • ‘ ’로 문자를 표현할 수 있다.
char a = 'a';

출처) https://mine-it-record.tistory.com/100

문자열 리터럴

  • 큰따옴표(””)안에 표현할 수 있다.
String a = "abc";
  • 문자열 리터럴의 경우 다른 리터럴과 다르게 String의 래퍼런스 타입이지 프리미티브 타입이 아니다.
  • 그럼에도 String 타입은 리터럴을 지원하는 데, 리터럴 방식으로 String에 값을 주면 Heap 영역에서 String constant pool이라는 특수한 영역에 값이 저장된다.
  • 그리고 동일한 값을 쓰는 경우에 다른 래퍼런스타입처럼 Heap에 다시 올라가지 않고, String constant pool에 존재하는 값을 참조하는 방식으로 작동하게 된다.
  • https://velog.io/@jaden_94/String-Class

boolean 리터럴

  • true, false로 표현할 수 있다.
boolean a = true;  
boolean b = false;  

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

변수 선언

변수를 사용하려면 먼저 변수를 선언해야 한다.

int week; 

int -> 변수 타입
week -> 변수 이름  
  • 변수 타입 : 변수에 저장될 값이 어떤 타입(type)인지 지정하는 것
  • 변수 이름 : 변수에 붙힌 이름
    • 변수가 값을 저장할 수 있는 메모리 공간을 의미하므로 변수 이름은 이 메모리 공간에 이름을 붙혀주는 것

변수를 선언하면, 메모리의 빈 공간에 변수타입에 알맞는 크기의 저장공간이 확보되고, 앞으로 이 저장공간은 변수 이름을 통해 사용할 수 있게 된다.

변수의 초기화

변수를 사용하기 전에 처음으로 값을 저장하는 것

변수를 선언하면 메모리에 변수의 저장공간이 확보되어 있지만, 이 공간 안에 어떠한 값이 저장되어 있을 지는 알 수 없다. 메모리의 경우 여러 프로그램이 공유하는 자원이므로 전에 다른 프로그램에 의해 저장된 알 수 없는 값(쓰레기 값)이 남아 있을 수 있기 때문이다.

그렇기에 초기화를 해줘야 한다.

int week = 7;

변수에 값을 저장할 때는 대입 연산자 =를 사용한다.

대입 연산자의 우측의 값을 좌측에 있는 변수에 저장하라는 뜻이다.

  • 변수의 종류에 따라 변수의 초기화를 생략할 수 있는 경우도 있지만, 변수는 사용되기 전에 적절한 값으로 초기화 하는 것이 좋다.
  • 클래스변수와 인스턴스 변수는 초기화를 생략할 수 있다.

지역변수는 사용되기 전에 초기화를 반드시 해야 한다.

바이트코드에서의 동작

public class Test {
    public static void main(String[] args) {
        int a = 3;
    }
}

iconst_3은 정수 3을 호출 스택에 올린다는 뜻이고 istore_1은 정수를 첫 번째 변수에 저장한다는 뜻이다. ( 여기서는 a )

이 말은 코드가 한줄로 되어있더라도 내부적으로는 두 번으로 나눠서 일을 처리한다는 것이다.

그렇기 때문에 멀티 쓰레드 환경에서 여러 가지 Race Condition문제가 발생한다.

public class Test {
    public static void main(String[] args) {
        BigDecimal number = BigDecimal.ZERO;
        number = number.add(BigDecimal.valueOf(0.1));
    }
}

이러한 코드가 있다고 가정해보자. ( 이해를 돕기 위한 코드이다. )

만약 A라는 쓰레드가 add라는 메서드를 처리하고 다음 할당을 해야하지만 이때 B라는 쓰레드가 add라는 메서드를 처리한 뒤 A쓰레드가 할당하게 된다면 값이 이상해지게 된다.

그러므로 변수 선언과 할당을 한번에 했다해서 한줄로 실행되는 것이 두줄로 실행되는 것이다.

그 밖의 초기화의 종류

지역변수는 변수의 초기화로 충분하지만, 멤버변수의 초기화는 몇가지 방법이 더 있다.

  1. 명시적 초기화
    • 변수 선언과 동시에 초기화하는 것을 명시적 초기화라고 한다.
    • 일반적인 변수의 초기화와 동일하며, 클래스 및 지역변수 어디서든 사용가능하며 여러 초기화 방법 중 최우선적으로 고려한다.
  2. 초기화 블럭
    • 초기화 블럭은 클래스 초기화 블럭과 인스턴스 초기화 블럭으로 나뉜다.
    • public class Test { static{ // 클래스 초기화 영역 } { // 인스턴스 초기화 영역 } }
    • 클래스 초기화 블럭
      • 클래스변수의 복잡한 초기화에 사용
      • 블럭내에서는 로직도 추가할 수 있기 때문에 명시적 초기화만으로 부족할 때 사용한다.
      • 클래스가 처음 메모리에 로딩될 때 한번만 수행된다.
      • 클래스 변수의 초기화 순서 : 기본값 → 명시적 초기화 → 클래스 초기화 블럭
    • 인스턴스 초기화 블럭
      • 인스턴스 변수의 복잡한 초기화에 사용
      • 모든 생성자가 공통으로 수행해야 하는 로직이 있을 때 사용한다. ( 생성자와 같이 인스턴스가 생성될 때 수행되기 때문에 )
      • 인스턴스 초기화 블럭이 생성자보다 먼저 수행된다.
      • 인스턴스 변수의 초기화 순서 : 기본값 → 명시적 초기화 → 인스턴스 초기화 블럭 → 생성자
  3. 생성자
    • 생성자는 말 그대로 인스턴스 생성시에 생성자 함수 안에서 명시적 초기화가 이루어 진다.

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

변수는 클래스변수, 인스턴스변수, 지역변수 모두 세 종류가 있다. 변수의 종류를 결정짓는 중요한 요소는 변수의 선언된 위치이므로 변수의 종류를 파악하기 위해서는 변수가 어느 영역에 선언되었는지를 확인하는 것이 중요하다.

선언 위치에 따른 변수의 종류

public class Test {
    int instanceValue; // 인스턴스 변수
    static int classValue; // 클래스변수( static 변수, 공유변수 )

    void method(){
        int localValue; // 지역변수
    }
}

클래스 내부에 선언되는 변수를 멤버변수라고 한다. 여기서 키워드static과 함계 선언되는 변수를 클래스 변수, 붙지 않은 것을 인스턴스 변수라고 한다. 그리고 멤버변수를 제외한 나머지 변수들은 모두 지역변수이다.

  • 변수 종류와 특징
  1. 인스턴스 변수 (instance variable)
    • 클래스 영역에 선언되며, 클래스의 인스턴스를 생성할 때 만들어진다. 그렇기에 인스턴스 변수의 값을 읽어 오거나 저장하기 위해서는 먼저 인스턴스를 생성해야 한다.
    • 인스턴스 별로 별도의 저장공간을 가지므로 인스턴스별로 다른 값을 가질 수 있다.
    • public class Car { int position; public Car(int position) { this.position = position; } } Car car1 = new Car(1); Car car2 = new Car(2);
  2. 클래스 변수 (class variable)
    • 인스턴스변수 앞에 static을 붙일 경우 클래스 변수가 되며 한 클래스의 모든 인스턴스가 값을 공유한다. (공통된 저장 공간)
    • 클래스 변수는 인스턴스를 생성하지 않고 클래스가 메모리에 올라갔을 때 (로딩) 선언되기 때문에 인스턴스에서는 언제든 바로 접근해서 사용할 수 있다. 그렇기에 어디서나 접근 할 수 있는 전역변수(global variable)의 성격을 갖는다.
    • public class Car { static int position = 3; ... } public static void main(String[] args) { System.out.println(Car.position); } ============================= 3
  3. 지역 변수 (local variable)
    • 메서드 내에 선언되어 메서드 내에서만 사용 가능하며, 메서드가 종료되면 소멸되어 사용할 수 없게 된다.
    • for문이나 while문 같은 반복문도 동일하게 블럭내에서 선언된 지역변수는, 블럭을 벗어나면 소멸되어 사용할 수 없게 된다.
    • public static void main(String[] args) { for (int i = 0; i < 10; i++){ System.out.println(i); } // i가 지역 변수! }

초기화 시기와 순서

  • 초기화 시점

프로그램 실행 도중 클래스에 대한 정보가 요구될 때, 클래스는 메모리에 로딩된다.

( 해당 클래스가 이미 메모리에 로딩되어 있다면, 또 다시 로딩하지 읺는다. )

  • 초기화 순서
public class Test {
    static int classValue = 1;
    int instanceValue = 1;

        static{
        classValue = 2;   
    }

    {
        instanceValue = 2;
    }

    public Test(){
        instanceValue = 3;
    }
}

  • 클래스변수 초기화(1~3) : 클래스가 처음 메모리에 로딩될 때 차례대로 수행됨.
  • 인스턴스변수 초기화(4~7) : 인스턴스를 생성할 때 차례대로 수행됨

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

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

타입 변환

변수 또는 상수의 타입을 다른 타입으로 변환하는 것

프로그램을 작성하다 보면 같은 타입뿐만 아니라 서로 다른 타입간의 연산을 수행햐야하는 경우도 있다. 이럴 때는 연산을 수행하기 전에 타입을 일치시켜야 하는데, 변수나 리터럴의 타입을 다른 타입으로 변환해주는 것을 형 변환이라고 한다.

캐스팅(명시적 형변환)

(타입)피연산자

변환할 변수나 리터럴 앞에 타입을 괄호와 함께 붙여주기만 하면 된다. 이 때 형 변환 연산자는 그저 피연산자의 값을 읽어서 지정된 타입으로 형변환하고 그 결과를 반환할 뿐이기에 기존의 변수나 리터럴이 변화되지는 않는다.

double d = 810.4;
int score = (int)d // 810

기본형 변수 중 boolean을 제외한 나머지 타입들은 서로 형변환이 가능하다.하지만 타입간에 각 가지고 있는 크기가 다르기 때문에 형변환을 통해 크기의 차이만큼 값이 잘려나감으로써 값 손실(loss of data)이 발생할 수 있다.

타입 프로모션(묵시적 형변환)

경우에 따라 형변환을 생략할 수 있다. 컴파일러가 생략된 형변환을 자동적으로 추가하여 생략할 수 있게 되었다.

하지만 변수가 저장할 수 있는 크기보다 더 큰 값을 저장하려는 경우에 형변환을 생략하면 에러가 발생한다. 이는 더 작은 값으로 할당되며 값 손실이 발생할 수 있기 때문에 이를 명시적 형변환으로 바꾸어 주면 에러가 발생하지 않는다.

byte b = 10000; // 에러 발생, byte의 범위를 초과한다.
byte c = (byte)10000; //명시적 형 변환으로 에러가 발생하지 않는다.

타입 프로모션규칙

기존의 값을 최대한 보존할 수 있는 타입으로 자동 형변환한다.

표현범위가 좁은 타입에서 넓은 타입으로 형변환하는 경우에는 값 손실이 없으므로 두 타입 중에서 표현범위가 더 넓은 쪽으로 형변환된다.

추가적으로 몇가지 규칙이 더 있다.

  1. byte와 short는 무조건 int로 변환된다.
  2. 만약 피연산자들 중 하나가 long 타입이라면, 최종 값은 long으로 변환된다.
  3. 만약 피연산자들 중 하나가 float 타입이라면, 최종 값은 float으로 변환된다.
  4. 만약 피연산자들 중 하나가 double 타입이라면, 최종 값은 double로 변환된다.
long a = 20L;
double b = 10.0;
// a + b -> double형 

1번 규칙에 대해 더 살펴보자면, 자바 바이트코드 opcode에는 byte와 short를 스택 메모리에 적재하는 명령어가 없기 때문에 byte와 short는 무조건 int로 메모리에 올라가게 된다.

이 말은 아래 예제를 통해 알 수 있다.

public class Test {
    public static void main(String[] args) {
        byte a = 10;
        short b = 100;
        int c = a + b;
    }
}

바이트코드 중 0번과 3번 라인의 bipush명령어는 byte, short 자료형을 int 자료형으로 스택에 푸시하는 명령어 이다. 즉, 스택에 애초에 int로 올라가게 된다는 것이다.

그래서 아래와 같은 상황이 발생한다.

public class Test {
    public static void main(String[] args) {
        byte a = 10;
        byte b = 20; 
        byte c = a + b; //error java: incompatible types: possible lossy conversion from int to byte
    }
}

컴파일 에러가 발생하게 된다. 발생하지 않게 하면 변수 c의 자료형을 int로 바꾸거나, a+b를 byte로 캐스팅 해주어야 한다.

int c = a+b; 
byte c = (byte)a+b;

오토박싱/언박싱

Java에서는 primitive 타입에 대한 Wrapper 클래스가 있다. Java 5이후로는 이러한 값 끼리 명시적인 형변환을 해줄 필요가 없는데, Java 컴파일러가 이를 대신 해주기 때문이다.

오토박싱과 언박싱을 사용하면 개발자가 더 깔끔한 코드를 작성할 수 있어 가독성이 높아지게 된다.

오토박싱

오토박싱은 primitive 타입에서 Wrapper 클래스로 자동 변환되는 것이다.

int → Integer로 변환하는 식으로 말이다.

Integer value = 1;

이 경우 컴파일러가 Interger value = Integer.valueOf(1); 로 변환한다.

언박싱

언박싱은 오토박싱과 반대로 Wrapper타입을 primitive 타입으로 변환해준다. Java에서는 Wrapper 클래스의 객체가 다음과 같은 경우 일대 언박싱을 해준다.

  • 메서드에서 primitive 타입으로 매개변수를 받을 때, 매개변수로 전달되는 경우
  • 해당 primitive 타입의 변수에 할당되는 경우
public class Test {
    public static void main(String[] args) {
        Integer value = 1;

        printValue(value); // 매개변수로 전달되는 경우

        int i = value; // primitive 타입의 변수에 할당되는 경우
    }

    public static void printValue(int value){
        System.out.println(value);
    }
}

객체의 형변환

primitive 타입의 형변환은 값 자체의 변환을 의미하지만 객체의 형변환은 참조 변수에서 객체를 바라보는 관점의 변환을 의미한다. 즉, 힙에 있는 객체 자체는 변경 되지 않다는 것이다.

업 캐스팅

자식 클래스에서 부모 클래스로 캐스팅하는 것을 업 캐스팅이라고 하고, 컴파일러에 의해 수행된다.

업 캐스팅은 상속과 밀접한 관련이 있다.

public class Animal {
    public void eat(){
        //...
    }
}

public class Cat extends Animal{
    @Override
    public void eat() {
        //...
    }

    public void meow(){
        //...
    }
}

public class Test {
    public static void main(String[] args) {
        Cat cat = new Cat();
                //새로 생성된 Cat 객체는 Animal 타입의 참조 변수에도 할당할 수 있다.
        Animal animal = cat;
                //위는 다음과 같이 컴파일러에 의해 변경된다.
        //Animal animal = (Animal)cat;
    }
}

참조변수는 선언된 타입의 하위 타입을 참조할 수 잇다. 대신, 그 타ㅣㅂ에서 사용할 수 있는 메서드는 제한될 수 있다. 하지만 인스턴스 자체는 변경되지 않는다.

cat에서는 meow()를 호출할 수 있지만, animal에서는 meow()를 호출할 수 없다.

업 캐스팅 덕분에 우리는 다형성을 활용할 수 있다.

다운 캐스팅

가끔 우리는 위와 같이 상위 타입으로 참조하는 하위 타입 객체를 활용하는 경우가 잇다. 이 때, 하위 타입에서만 제공하는 메서드를 사용하고 싶은 경우 우리는 다운 캐스팅을 활용한다.

다운 캐스팅은 부모 클래스에서 자식 클래스로 명시적 캐스팅을 하는 것이다.

((Cat) aniaml).meow();

위 코드는 명시적으로 Cat 클래스로 캐스팅했으며, 정상적으로 동작한다. 하지만 Animal클래스를 상속하는 다른 클래스의 인스턴스를 사용한다거나, Animal 클래스의 인스턴스를 사용한 경우 ClassCastException이 발생하게 된다.

문제없이 컴파일되지만, 런타임에 예외가 발생하게 된다.

다만, 관계가 없는 타입으로 캐스팅하는 경우엔 컴파일 에러가 발생한다. 즉, 서로 연관이 있어야만 컴파일이 되고, 연관이 있더라도 타입이 일치하지 않는다면, 런타임에 ClassCastException이 발생하게 된다.

String s = (String) animal 

우리는 이런 런타임 예외를 방지하기 위해 instanceof 연산자를 사용할 수 있다.

if (animal instanceof Cat) {
    ((Cat) animal).meow();
}

7. 1차 및 2차 배열 선언하기

배열의 선언과 생성

  1. 배열의 선언
    • 타입 뒤 혹은 변수이름뒤에 대괄호를 붙이면 된다.
  2. int[] score; // 타입[] 변수이름; int score[]; // 타입 변수이름[];
  3. 배열의 생성
    • 연산자 new를 사용해 배열의 타입과 길이를 지정하면 메모리에 해당 길이만큼 영역을 확보한다.
    • 길이가 0인 배열도 생성이 가능하다
    • 한번 선언되면 길이를 변경할 수 없다.
      • 변경하고 싶다면 더 큰 배열을 새로 생성한 뒤 기존의 내용을 복사한다.
  4. 타입[] 변수이름 = new 타입[길이];

1차원 배열 & 2차원 배열의 선언

  • 1차원 배열
int[] score = new int[5];
score[0] = 1;
....

or 

int[] score = new int[]{1, 2, 3, 4, 5};

or 

int[] score = {1, 2, 3, 4, 5};

  • score은 Runtime Stack 영역의 Heap 영역 주소값을 가짐
  • Heap영역에 int 타입 크기의 요소 5개를 할당하여 사용함.
  • 2차원 배열
int[][] score = new int[2][2];
score[0][0] = 1;
....

or

int[][] score = new int[][]{{1,2},{3,4}};

or

int[][] score = {{1,2},{3,4}};

  • Runtime Stack 영역의 score는 2개의 요소 크기(2개 요소에 주소값을 가지고 있음)를 가진 Heap 영역 주소값을 가짐
  • Heap영역에는 실제 값이 들어있는 요소들과 주소값이 들어있는 요소들로 존재함.

타입 추론, var

출처) https://catsbi.oopy.io/6541026f-1e19-4117-8fef-aea145e4fc1b

타입 추론

자바 컴파일러에서 타입을 추론하는 것을 Type Inference 라고 한다.

이 타입추론을 하기 위해 메서드 호출과 호출할 대 사용하는 인수을 결정하기 위한 선언부터를 살펴본다. 추론 알고리즘 (inference algorithm)을 통해 인수 타입을 결정하고 가능하다면 결과가 할당되는 타입(인수타입)이나 반환 타입도 추론한다.

public class Test {
    static <T> T pick (T a1, T a2) {
        return a2;
    }
    public static void main(String[] args) {
        Serializable d = pick("d", new ArrayList<String>());
    }
}
  • 추론 알고리즘은 모든 인자와 어울리는 선에서 가장 구체적인 타입을 찾는데, 위 코드를 보면 pick메소드의 매개변수는 T이고 메소드의 매개변수 a1,a2 둘다 T타입이다.

하지만 pick메서드를 호출하면서 첫 번째 인자로 String을 주고 두 번째 인자로 ArrayList를 주었다. 이런 경우 모든 인자에 어울리는 선(공통 부모)란 Serializable으로 StringArrayList 둘다 Serializable을 구현하고 있기 때문이다.

https://devlog-wjdrbs96.tistory.com/268

타입 추론과 Generic Methods

타입추론덕분에 generic 메서드를 사용할 때 보통의 메서드처럼 특정 타입을 명시하지 않은 채로 호출할 수 있다.

public class BoxDemo {
    public static <U> void addBox(U u, List<Box<U>> boxes) {
        Box<U> box = new Box<>();
        box.set(u);
        boxes.add(box);
    }

    public static <U> void outputBoxes(List<Box<U>> boxes) {
        int counter = 0;
        for (Box<U> box: boxes) {
            U boxContents = box.get();
            System.out.println("Box #" + counter + " contains [" +
                boxContents.toString() + "]");
            counter++;
        }
    }

    public static void main(String[] args) {
        List<Box<Integer>> listOfIntegerBoxes = new ArrayList<>();
                BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes); //---(1)
        BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);//---(2)
        BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
        BoxDemo.outputBoxes(listOfIntegerBoxes);
    }
}

(1) : addBox라는 generic 메서드를 호출할 때 <Integer> type witness와 함께 type parameter를 명시하여 사용할 수 있다.

(2) : Java 컴파일러가 메서드의 인자로부터 자동으로 Integer type임을 추론해주기 때문에 type witness를 생략할 수 도 있다.

타입 추론과 Generic 클래스의 인스턴스

Java컴파일러가 컨텍스트로부터 타입추론이 가능하다면 Generic 클래스의 생성자를 호출하기 위해 필요한 type arguments를 비어있는 type witness(<>)로 대체할 수 있다.

List<String> list1 = new ArrayList<String>();
List<String> list2 = new ArrayList<>();

타입추론과 Generic 생성자

클래스가 Generic, non-generic 인지 그 여부와 관계없이 생성자는 generic일 수 있다.

class MyClass<X> {
  <T> MyClass(T t) {
    // ...
  }
}

public static void main(String[] args) {
    MyClass<Integer> myInstance = new MyClass<Integer>("");
}
  • Myclass의 type 매개변수 X에는 Integer가 들어가지만 생성자의 type 매개변수 T에는 String이 들어간다.

Java7 이전에는 컴파일러에서 실제 type argument를 작성해 타입 추론을 할 수 있었지만, 이후로 컴파일러는 the diamond(<>)를 사용하는 경우 다음과 같이 generic 클래스의 실제 type argument까지 추론이 가능하다.

MyClass<Integer> myInstance = new MyClass<>("");

Target Types

Java컴파일러는 generic method invocationtype argument를 추론하기 위해 target typing의 이점을 이용한다. 표현식의 target type이란 표현식이 나타낸 위치에 의존하여(컨텍스트 의존) Java 컴파일러가 기대하는 데이터 타입이다.

static <T> List<T> emptyList() { return new ArrayList<>(); }
List<String> listOne = Collections.emptyList();

위 코드는 Collection APIemptyList 함수를 이용해 List<String>객체를 반환한다. 이런 데이터 타입을 Target Type이라 하는데 emptyList 함수가 List<T> 타입을 리턴하기에 컴파일러에서 type argument T가 반드시 String일 것이라고 추론한다.
물론, type witness를 사용해 명시적 선언을 해줄 수도 있지만 위 코드에서는 불필요하다.

List<String> listOne = Collections.<String>emptyList(); //불필요한 witness

하지만 type witness가 필요한 경우도 있다.

void processStringList(List<String> stringList) {
        //process stringList
}
public static void main(String[] args) {
        processStringList(Colections.emptyList());
}

Java 7컴파일러에서는 컴파일 되지 않고 에러가 발생하며 에러 메시지가 출력된다.

List<Object> cannot be converted to List<String>

이런 에러가 발생하는 이유는 컴파일러는 type argument T를 위한 value를 필요한데, 아무것도 주어지지 않았기에 Object를 value로 삼게된다. 그 결과 Collections.emptyList()List<Object> 객체를 리턴하며 이는 processStringList에서 호환하지않는 인수타입이기에 에러가 발생한다.

그렇기에 Java7에서는 type witness를 명시해줘야 한다.

processStringList(Colections.<String>emptyList());

하지만 Java 8부터는 위와 같은 경우에 type witness를 명시해주지 않아도 Tartget type을 결정할 때 메서드의 argument도 살피도록 확장되었기 때문에 에러가 발생하지 않는다.

그렇기 때문에 Java 8이상에서는 위의 type witness가 없는 메서드 호출도 정상적으로 동작한다.

Var

Java 10부터 추가된 특징 중 하나인 Local Variable Type Inference는 로컬 변수 선언을 var를 이용하여 기존의 엄격한 타입 선언방식에서 컴파일러에게 타입추론 책임을 위임할 수 있게 되었다.

var list = new ArrayList<String>(); //infers ArrayList<String>
var stream = list.stream(); //infers Stream<String>

Local Variable Type Inference 사용조건

  • 초기화된 로컬 변수 선언시
  • 반복문에서 지역 변수 선언 시 (향상된 for loop 포함)

Var 활용

  1. 지역변수 선언
var numbers = Arrays.asList(1, 2, 3, 4, 5);

for (var i = 0; i < numbers.size(); i++) {
    System.out.println("numbers = " + numbers.get(i));
}
  1. forEach
var numbers = Arrays.asList(1, 2, 3, 4, 5);

for (var number : numbers) {
    System.out.println(number);
}
  1. Lambda (Java 11)
IntBinaryOperator plus10 = (@NonNull var one, @NonNull var two) -> one + two + 10;
  • Java 11부터는 람다 인자에도 var사용이 가능해졌는데, 이를 통해 파라미터 에노테이션 사용까지 가능해졌다.

비어있는 type witness를 사용하면 Object로 추론한다.

타입 추론의 맹점

하지만 이러한 타입 추론, var이 만능은 아니다. 특히, Java에서는 객체지향 패러다임을 사용하기 때문에 문제가 될 수 있다.

예를 들어, Animal 클래스가 있고, 이 클래스의 자식 클래스인 Cat, Dog가 있다고 가정해보자.

Var v = new Cat();

v의 경우 어떻게 추론이 될까? Aniaml? Cat? 이 경우 컴파일러는 초기화한 클래스의 타입을 사용하게 된다.

따라서 다음의 코드는 컴파일되지 않는다.

v = new Dog();

즉, 다형성을 활용하는 코드는 var타입 추론과 어울리지 않는다는 것이다.

var를 사용할 수 없는 위치

  • 필드 혹은 메서드 시그니처에서 사용이 불가능하다.
public long add(var list){...} // 불가능 
  • 명시적인 초기화 없이 var만 단독으로 선언할 수 없다.
var x; // 불가능
  • var변수는 null로 초기화 할 수 없다.
var x = null; // 불가능
  • 람다식과 var는 명시적으로 타겟이 되는 타입을 알아야 하기 때문에 같이 사용할 수 없다.(Java 11부터 사용가능)

주의해야할 var선언

var list = new ArrayList<>();

해당 코드의 경우 컴파일이 되지만, 실제 list의 타입은 ArrayList<Object>로 컴파일되며, generic의 이점을 얻지 못하기 때문에 피하는 것이 좋다.

Reference

댓글