순간을 성실히, 화려함보단 꾸준함을

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

백기선님과 함께 하는 자바 스터디

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

폭발토끼 2021. 5. 5. 23:59

목표

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

학습할 것

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

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

자바는 크게 프리미티브 타입과 레퍼런스 타입으로 나뉩니다.
프리미티브 타입(Primitive type) 이란 원시타입이라는 뜻이고 직접 값을 담는 타입을 뜻합니다.
레버런스 타입(Reference type)은 다른 값을 참조하는 주소값을 담는 타입을 뜻합니다.

자바에서 프리미티브 타입은 8가지가 있습니다.

  타입 메모리 크기 기본값 데이터의 표현범위
논리형 boolean 1 byte FALSE true,false
정수형 byte 1 byte 0 -128 ~ 127
short 2 byte 0 -32,768 ~ 32,767  
int(default) 4 byte 0 -2,147,483,648 ~ 2,147,483,647  
long 8 byte 0L -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807  
실수형 float 4 byte 0.0F (3.4 X 10-38) ~ (3.4 X 1038) 의 근사값
double(default) 8 byte 0 (1.7 X 10-308) ~ (1.7 X 10308) 의 근사값  
문자형 char 2 byte \u0000' 0 ~ 65,535

이 모든 자료형을 전부 알아야 하는지에 대해선 각자 선택에 맡기겠습니다.
그러나 적어도 int,double,long 형은 몇바이트인지는 알고 가셨으면 하는 바람입니다.

그럼 왜 이런 자료형들은 각각의 메모리크기를 갖게 되었고 표현할 수 있는 범위가 왜 이렇게 될까요?
자세한 내용은 '컴퓨터 구조'라는 과목을 배우면 알 수 있지만 간략하게 여기서 언급하고 가겠습니다.

컴퓨터는 사람과 마찬가지로 컴퓨터만의 언어를 가지고 있습니다. 바로 이진수입니다. 0과 1로만 대화를 할 수있게끔 설계된 컴퓨터는 오로지 이진수밖에 모르는 것이죠.

바로 이 0과1을 표현할 수 있는 공간이 1bit 입니다.
1bit라는 메모리공간에선 0과 1만 쓰고 읽을 수 있으며 이러한 bit들이 8개가 모여 1byte를 이룹니다.
즉, 1byte = 8bit 라는 뜻입니다. 바로 이 1byte가 컴퓨터가 어떠한 일을 처리하기 위한 '최소 단위'가 되는 것입니다.
그래서 어떠한 언어든지 메모리의 크기를 정의할때 byte 단위로 정의하는 것입니다.

1 bit 는 0과 1을 담을 수 있으니 1bit가 표현할 수 있는 범위는 0~1입니다.

2 bit 는 00, 01, 10, 11 이렇게 4가지의 수를 표현할 수 있으니 범위는 0~3입니다.

3 bit 는 0~7 이겠죠?
즉, X bit 는 $0\sim2^{x}-1$의 수를 표현해줄 수 있는 것 입니다.

그러면 이상한 부분이 생깁니다. int형은 4byte = 32bit 이면 $0\sim 2^{32}-1$ 의 범위까지 표현되는게 정상이 아닌가요?
그러나 우리는 이미 위에서 이상함을 미리 느꼈어야 합니다. 바로 왜 0부터 시작이 되는 것 이지?라는 의문입니다.
우린 자바에선 음수를 자유자재로 사용할 수 있습니다. 즉, int형으로 선언을 하고 음수의 값을 할당할 수 있죠.
그러나 위처럼 0부터 시작하게 되면 음수의 값을 표현할 수 없습니다. 이를 위해 MSB 방법을 택하게 됩니다.

부호의 절대값 표현

가장 좌측에 있는 비트를 최상의 비트(MSB)로 설정하여 사용하는 방법을 살펴보겠습니다.
$$+18 = 00010010_{(2)}$$

$$-18 = 10010010_{(2)}$$

최상위 비트가 0이면 양수를 1이면 음수를 의미합니다. 그러나 이 방법은 치명적인 문제점을 가지고 있습니다.
1)산술 계산을 행하는데 부호와 절대값 둘다 알아야 할 필요성이 있습니다.
2)0을 표현하는데 2가지의 방법이 존재합니다(+0,-0)

특히 2번째 문제점은 굉장히 중요한 문제인데 이를 해결하려고 '2의 보수법(2’s completemt)'이 사용되는 것 입니다.

2의 보수법

글의 취지와 관계없이 멀리까지 와버린 것 같은데 이런 개념은 단지 자바를 공부하는걸 넘어서 컴퓨터공학과 연관되어 있는 내용이라 적어도 개발자들은 알고 있어야 할 내용이라고 생각하여 글을 작성한 것이니 봐주셨으면 좋겠습니다.

2의 보수법은 기존 양수를 표현하고 있는 비트들을 전부 뒤집습니다.(0은 1로,1은 0으로)
그리고 나서 +1을 더해주는 것 입니다.

$$+18 = 00010010_{(2)} $$

2의 보수를 취하면

$$-18 = 11101110_{(2)}$$

두 수를 더하면 0이 되는 걸 확인할 수 있습니다.
이런 방법의 장점은

  1. 0을 표현할 수 있는 방법이 1가지뿐이다.
  2. 음수로 만드는 방법이 쉽다.
  3. 산술 계산이 복잡하지 않다.

따라서 2의 보수법은 $-2^{n-1}\sim+2^{n-1}-1$ 의 범위를 갖게 되는 것 입니다.
왜 $+2^{n-1}-1$ 이냐면 0은 양수로 취급받기 때문입니다.
만약 0부터 2n 까지 표현하고 싶다면 unsinged 형을 사용하면 되지만 자바에서는 지원하지 않는 걸로 알고 있습니다.
(C,C++ 에서는 지원하고 있습니다.)

실수형은 '부동 소수점'방식을 사용하고 있는데 이에 관한 내용은 저보다 훨씬 훌륭한 분의 글을 올려드리겠습니다.
부동소수점1
부동소수점2

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

프리미티브 타입을 알아보았으니 레퍼런스 타입을 공부해보겠습니다.
레퍼런스 타입 : 레퍼런스 타입은 값을 직접 저장하는 것이아닌 값이 저장된 주소값을 저장합니다. 즉, 데이터를 보유하는 다른 메모리 위치에 대한 '포인터'가 포합됩니다.

간단한 예시를 들어서 설명하겠습니다.

public class Main {
    int a = 400;
    public static void main(String[] args){
        Person person = new Person();
        System.out.println(person.age);
        person.change_age();
        System.out.println(person.age);
    }
}
class Person{
    public int age=100;
    public void change_age(){
        this.age=200;
    }
}

Main 클래스 안에는 변수 a가 선언되어 있는 상태이고 main함수 내부에서는 Person 객체를 new키워드를 사용하여 동적할당 해준 후 사용해주고 있는 모습입니다.

이런 상황인거죠. 이때 person.age가 실행되는 과정을 살펴보면 먼저 Person person = new Person();을 읽어들이고 stack 영역에 person 변수를 heap영역에 Person 의 인스턴스와 메서드들을 담은 메모리 공간을 가리키게 됩니다.
그리고 나서 person.age를 읽어들일때 먼저 stack영역에서 person객체가 존재하는지 확인한 후 존재하면 heap영역으로 찾아들어가 age라는 변수가 있는지 확인하게 됩니다. 존재한다면 age를 출력하게끔 하는 것이죠.
change_age()라는 메서드도 마찬가지 입니다. (이를 잘 알아두면 상속으로 개념이 확장되면 좀 더 복잡한 구조를 쉽게 배울 수 있습니다)

이때 person이 레퍼런스 타입의 변수가 되는 것 입니다.

이 때문에 자바에서는 swap 처리를 하는게 여간 불편한게 아닙니다.

public class Main {
    public static void main(String[] args){
        int a=400,b=200;
        swap(a,b);
        System.out.println(a+" "+b);
    }
    public static void swap(int x,int y){
        int temp =x;
        x=y;
        y=temp;
    }
}

언뜻 보기에는 a와 b가 서로 swap이 될 것 처럼 보이지만 전혀 아니죠.
이유는 swap 함수에서 파라미터로 받는 x,y 의 변수는 새로운 메모리에 할당이 되어 저장되기 때문입니다.
즉, call_by_value의 함수의 호출이 일어나기 때문이죠.
그럼 무슨 방법이 없는가???자바에선 포인터의 개념이 존재하지 않기 때문에 call_by_reference를 흉내내는 C/C++와는 달리 지역변수로 선언하여 직접 교환할 수 있는 방법은 없는걸로 알고 있습니다.(틀린 내용이라면 댓글로 알려주세요)

그래서 call_by_reference의 형태의 함수 호출이 필요한 것입니다.

public class Main {
    int x;
    Main(int x){
        this.x = x;
    }
    public static void main(String[] args){
        Main a = new Main(400);
        Main b = new Main(200);
        swap(a,b);
        System.out.println(a.x+" "+b.x);
    }
    public static void swap(Main f,Main s){
        int temp =f.x;
        f.x=s.x;
        s.x=temp;
    }
}

Main 클래스 내부에 x라는 int형 변수를 선언하고 main 함수에서 이 클래스의 객체 2개를 생성합니다.
그리고 swap 함수에 객체의 인스턴스가 아닌 객체 자체를 넘겨주어 f와s 가 같은 메모리공간을 가르키도록 만들어 주었습니다.
이땐 새롭게 생성된 메모리가 아닌 동일한 메모리 공간을 가리키고 있으니 swap이 정상적으로 동작하는 걸 확인 할 수 있을겁니다.

리터럴

리터럴이란?
변수에 넣는 값 자체를 의미합니다.

int a = 4;
이런 구문이 있으면 a는 변수 4가 리터럴이 됩니다.
변하지 않는 값(int,double,long.boolean 등등)을 리터럴이라고 칭한다고 생각하시면 됩니다.
그럼 객체에 존재하는 인스턴스는 리터럴일까? 답은 아니오입니다. 이유는 언제든지 값이 바뀔지 모르기 때문입니다.

문자열 리터럴은 좀 특이한데 이 부분은 링크를 확인하세요

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

변수를 선언하는 방법은 간단합니다.

int a;

변수 타입과 변수 이름을 적어주면 됩니다.
이렇게 하면 변수 타입의 크기만큼 메모리공간이 할당되고 그 공간에 이름을 사용하여 값을 넣을 수 있습니다.

  • 변수명은 숫자로 시작할 수 없습니다.
  • _(underscore)와 $ 문자 이외의 특수문자는 사용할 수 없습니다.
  • 자바의 키워드는 변수명으로 사용할 수 없습니다.(class,void,return 등)

*변수 초기화 vs 변수 할당

초기화와 할당을 굳이 구분지어 말하지 않는 사람들도 존재하지만 공부하는 차원에서 차이점을 알아보자

int a = 4;

이렇게 선언과 동시에 할당을 하게 되면 초기화(Initialization)이라고 합니다.
그렇지 않고

int a;
a=4;

선언을 하고 난 뒤 후에 값을 변수에 대입하는 것을 할당(Assignment)라고 합니다.

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

선언 위치와 변수의 종류

public class Main {
    int x=1; //인스턴스 변수
    static int y=2; //클래스 변수
    public static void main(String[] args){
        int main_x=4; //지역 변수
        Main ins = new Main();
        System.out.println(ins.x); //1 출력
        show(main_x); //4 출력
        System.out.println(y); //3 출력
        System.out.println(++ins.y); //4 출력
    }
    public static void show(int show_x){
        System.out.println(show_x);
        y++;
    }
}

클래스 내부에서 static이 없는 변수를 인스턴스 변수라고 지칭하며, static 키워드가 붙은 변수를 클래스 변수하고 합니다.
그리고 메서드 내부에 선언된 변수를 지역변수라고 합니다.
이 변수들은 모두 스코프와 생성시기가 다릅니다.

 

변수의 종류 선언위치 생성시기
인스턴스 변수 (instance variable) 클래스 영역 인스턴스가 생성이 되었을때
클래스 변수 (class variable) 클래스가 메모리에 올라갈때  
지역 변수 (local variable) 클래스 영역 이외의 영역 (메서드,생성자,초기화 블록 내부) 변수 선언문이 수행되었을 때

자바에서는 크게 3가지의 variable이 존재합니다. 각 변수마다 스코프,라이프타임이 다른데 한번 알아보겠습니다.

 

1) 인스턴스 변수(instance variable)
클래스 영역에서 선언이 되며, 클래스의 인스턴스를 생성할 때 마다 생성됩니다. 이 때문에 인스턴스 변수값을 읽어오거나 저장하려면 먼저 인스턴스를 생성을 해야합니다.


우린 자바를 처음 공부하다보면 인스턴스 변수를 main 함수안에서 사용하려고 하면 오류가 발생하는 것을 한번쯤은 경험해 보았을 것 입니다. 그러면 대체 왜 인스턴스 변수를 main 함수안에서 직접적으로 사용하려고 하면 오류가 발생하는 걸까요?바로 '생성시기'때문에 그렇습니다. 인스턴스 변수는 인스턴스를 생성하고 나서부터 사용이 가능합니다. 즉, new키워드를 사용하여 인스턴스를 생성한 후 부터 사용이 가능한 것인데 static 함수인 main 함수를 로딩하는 도중에 아직 인스턴스가 생성되지도 않았는데 인스턴스 변수를 사용하려고 하니 당연히 에러가 발생하게 되는 것 입니다. 이건 말이 안되는 로직입니다.반대로 인스턴스에서는 클래스 레벨에 올라오는 변수들을 참조할 수 있겠죠? 클래스가 메모리에 올라온 뒤에 인스턴스가 생성되기 때문입니다.

 

2) 클래스 변수(class variable)
클래스 변수란 클래스 내부에서 static으로 선언된 변수를 뜻합니다.
클래스 변수는 선언이 되면 해당 클래스 내부에서 모든 인스턴스가 같은 메모리 공간을 가리킵니다.
클래스 변수는 인스턴스를 생성하지 않고 클래스가 메모리에 올라갔을때 생성되기 때문에 인스턴스에서는 언제든지 참조하여 사용할 수 있습니다.
그렇기에 전역변수(global variable)이라고도 불리게 됩니다.

 

3) 지역 변수(local variable)
지역 변수란 인스턴스 변수와 클래스 변수를 제외한 모든 변수를 뜻합니다.
블럭내에 선언된 변수는 블럭을 벗어나게 되면 소멸됩니다.

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

타입변환이란?

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

형변환 방법

(type)operand : 변환할 변수나 리터널 앞에 타입을 괄호와 함께 붙여주고, 이름을 적어주면 됩니다.

public class Main {
    public static void main(String[] args){
        int x=1;
        long y = 1;
        if(y==(long)x)
            System.out.println("Same"); //Same 출력
        else
            System.out.println("not Same");
    }
}

int형인 x를 long형으로 캐스팅 한 후 long형인 y와 비교해주는 소스입니다. 캐스팅이 정상적으로 된 것을 확인할 수 있습니다,

public class Main {
    public static void main(String[] args){
        int x=(int)1;
        double y = 1.1;
        if((int)y==y)
            System.out.println("Same");
        else
            System.out.println("not Same"); //not Same 출력
    }
}

그러나 작은 범위로 캐스팅을 하는 경우에는 주의해야 합니다. 작은 범위로 캐스팅을 하면 그 차이만큼 데이터가 소멸될 수 있기 때문입니다.

자동 형변환

public class Main {
    public static void main(String[] args){
        int x=1;
        long y = 1;
        if(y==x)
            System.out.println("Same"); //Same 출력
        else
            System.out.println("not Same");
    }
}

자바에서는 변수 타입이 작은 범위과 큰 범위를 비교해 줄때 자동으로 작은 범위의 변수를 큰 범위의 변수타입으로 변환을 해줍니다. 그래서 위의 소스에서 에러없이 잘 작동하는 것 입니다.

타입 캐스팅 vs 타입 프로모션

타입 프로모션이란?
크기가 더 작은 범위의 변수를 더 큰 범위의 자료형으로 대입할때, 자동으로 작은 범위가 큰 범위로 변환되는 현상입니다.
타입 캐스팅이란?
크기가 더 큰 범위의 변수를 작은 범위의 자료형으로 대입할때, 자료형을 명시하여 강제로 넣는 것을 뜻합니다.

1차 및 2차 배열 선언하기

배열(array)이란?
같은 타입의 변수들로 이루어진 유한한 집합을 의미합니다.
배열의 핵심은 같은 타입이라는 것 입니다.
(다른 타입의 변수들로 이루어진 집합은 튜플이라고 정의합니다.)

1차원 배열의 선언

int[] arr = new int[5];

2차원 배열의 선언

int[][] arr = new int[2][3];

이런 형태라고 생각하시면 됩니다.
그러나 더 자세히 들여다 보면

이런 형태로 이루어 집니다.

public class Main {
    public static void main(String[] args){
        int[][] arr = new int[2][3];
        System.out.println(arr.length); // 2 출력
        System.out.println(arr[0].length);// 3 출력
    }
}

따라서 arr 이라는 배열 자체의 길이를 찍어봤을땐 2가 출력이 되지만, 한줄을 찍어봤을땐 3이 출력이 되는 것 입니다.
(더 나아가 ArrayList 와 Vector에 대해서도 공부해볼 것을 추천드립니다. 두개의 차이점도 한번 직접 찾아보세요)

자바 타입추론, var

타입추론이란

정적 타이핑을 지원하는 언어에서, 타입이 정해져있지 않은 변수에 대해서 컴파일러가 변수의 타입을 스스로 찾아낼 수 있도록 하는 기능을 뜻합니다.

즉, 타입을 명시하지 않아도 되며,코드량을 줄이고 코드의 가독성을 높일 수 있다는 뜻입니다.
자바 10부터 var 구문이 생기고 자바 11부터는 이를 통한 람다 타입 지원도 생겼습니다.
컴파일러는 개발자가 입력한 초기화 값을 통해 타입을 유추하는데 var은 컴파일러가 타입을 유추할 수 있도록 반드시 데이터를 초기화 해야합니다.

public class Main {
    public static void main(String[] args){
        var str = "this variable VAR";
        if(str instanceof String)
            System.out.println("str type : String"); //출력
    }
}

주의사항

  • 앞에서 언급한 것 처럼 초기화가 되어 있지 않는 다면 var은 사용할 수가 없다
  • 자바 7에서 나온 다이아몬드 연산자(<> 연산자)는 var과 함께 사용하지 못합니다.

사용하는 방법

  1. foreach
    흔히 사용하는 for문에서 변수를 선언할 때 var로 선언해서 사용하면 편합니다.
  2. //Java 9 for(Person person : personList){ //... } //Java 10 for(var person : personList){ //... }
  3. 람다(Java 11)
    LTS인 자바 11은 람다 인자에도 var 키워드를 넣게 해줍니다.이게 중요한 이유는 일반 람다의 경우 파라미터 어노테이션을 집어넣지 못합니다. 만약 어노테이션을 넣고 싶으면 따로 메서드로 빼던가, 익명 클래스로 정의해야만 했죠.
    그러나 자바 8부터 람다 인자는 타입추론의 기초였던게, 자바 11부터는 타입추론의 유연성을 추가했습니다.
  4. Consumer<Person> personConsumer = (@Nonnull var person) -> { // @Nonnull 어노테이션에 의해 person에 널체크부터 하겠지? }
  5. 익명 클래스
    가독성 문제 때문에 var 사용을 지양하려고 하는 성향이 존재합니다. 이유는 IDE를 사용하지 않을때 일반 에디터로는 타입 추론이 어려워지기 때문입니다.그러나 익명클래스는 정의가 거대하고,유추가 쉽습니다. 또한 선언한 다음에 변수가 바뀌는 일도 없습니다.
    따라서 var을 활용하면 효율적입니다.
  6. var sell = new Seller<String>() { @Override public String get() { //... } }
  7. var number = 20; var str = "string"; var list = new ArrayList<Integer>();

[출처 : https://www.youtube.com/channel/UCwjaZf1WggZdbczi36bWlBA]