상세 컨텐츠

본문 제목

1. 프로그래밍의 시작 Java의 자료형 (기본형, 참조형)

How To Java/Java Tutorial

by 카페코더 2020. 2. 19. 18:27

본문

반응형

취업 준비를 하며 복습한다는 마음으로, 이것을 보는 누군가에게 도움이 되었으면 하는 마음으로
Java-Tutoral을 작성해 봅니다. 입문서와 순서가 잘못되었을 수 있고, 제가 아는 정보가 틀렸을 수 있습니다.
이 글에 대한 잘못된 정보나, 오탈자 등 수정해야 할 항목 혹은 추가해야 할 항목은 댓글로 알려주시면 감사하겠습니다.

이 자료가 올라가는 저장소 :
https://github.com/hwk0911/Java-tutorial

 

hwk0911/Java-tutorial

Java tutorial. Contribute to hwk0911/Java-tutorial development by creating an account on GitHub.

github.com

 

Java의 자료형에 대해 알아본다.

기초를 쌓지 않은 프로그래머는 소금과 모래로 된 기둥 위에 지어진 성과 같다.
기초를 탄탄히 하자는 마음으로 Java 자료형을 정리해본다.

1. Java 자료형은 크게 Primitive type과 Reference type으로 나뉜다.

다음은 Java에서 제공하는 자료형의 분류이다.

Java 자료형
Java의 자료형

+ Java가 아닌 다른 대부분 언어들은 자료형의 크기를 컴파일러에 맡겨 처리한다.
   하지만 Java의 경우는 크기 자체를 언어에서 직접 고정해 사용한다.

2. 원시 자료형(Primitive Type)

  • 기본 자료형은 반드시 사용하기 전에 선언되어야 한다.
  • 자료형의 길이는 OS와는 무관하다.
  • Object 타입이 아닌 Primitive 타입이기 때문에 null 객체를 값으로 가질 수 없다.
자료형 키워드 크기 기본값(전역변수 한정) 표현범위
논리형 boolean 미정
Virtual Machine Dependent
false true, false
문자형(유니코드) char 2byte \u0000 0 ~ 65,535
정수형 byte 1byte 0 -128 ~ 127
short 2byte 0 -32768 ~ 32767
int 4byte 0 -2147483648 ~
2147483647
long 8byte 0 -9223372036854775808 ~
9223372036854775807
실수형 float 4byte 0.0 -3.4E38 ~ +3.4E38
double 8byte 0.0 -1.7E308 ~ +1.7E308

전역 변수(클래스 내의 멤버 변수)의 경우 초기화를 하지 않아도 Exception(예외)가 발생하지 않는다.
지역 변수(메소드나 생성자 내의 멤버 변수)의 경우는 초기화를 하지 않으면 Exception이 발생한다.

지역 변수 초기화 에러) Error:(5, 28) java: variable a might not have been initialized
(에러코드가 나온 김에 하는 얘기지만, 에러코드를 직접 읽어보는 것 또한 좋은 공부가 된다.)

위와 같은 차이가 나는 이유는 전역 변수는 Heap영역에 저장되는데, 이때 변수의 자동 초기화가 이뤄진다.
반대로 지역 변수의 경우 Stack영역에 저장되는데, 이때 변수의 자동 초기화가 이뤄지지 않는다.

3. 참조 자료형

java.lang.object 를 상속받으면 참조형이 된다. 선언한 자료형은 마찬가지로 기본형이 아닌 참조형이 된다.
대표적으로 Class Type, Interface Type, Array Type, Enum Type 이 있다.

3.1 클래스 타입 (Class Type)

클래스형의 가장 큰 특징은 객체를 참조하는 형태이다. 다음은 클래스형의 간단한 예제이다.

class Truck {
    private int weight;
    private int size;

    Truck(int weight, int size) {
        this.weight = weight;
        this.size = size;
    }

    public int GetWeight() {
        return this.weight;
    }

    public int GetSize() {
        return this.size;
    }


    @Override
    public String toString() {
        return "Truck{" + "weight=" + weight + ", size=" + size + '}';
    }
}

public class Main {
    public static void main(String[] args) {
        Truck truckA = new Truck(25, 10);
        Truck truckB = new Truck(10, 5);

        System.out.println(truckA.toString());
        System.out.println(truckB.toString());

        System.out.println(truckA.GetWeight() + " " + truckA.GetSize());
        System.out.println(truckB.GetWeight() + " " + truckB.GetSize());
    }
}
/*
결과 : 
Truck{weight=25, size=10}
Truck{weight=10, size=5}
25 10
10 5

Process finished with exit code 0
 */

C / C++언어를 먼저 접해 본 사람들이라면 구조체와 클래스가 상당히 비슷하게 생긴 것을 알 수 있다. 이 둘의 차이는
번외에서 서술하겠다.

몇몇 사람들이 C / C++은 어렵고 Java는 쉽다고 하는데, 대부분 이유로 포인터를 얘기한다.
필자는 C -> C++ -> Java 순으로 언어를 공부한 사람인데, Java가 제일 어렵게 느껴졌다.
이유는 포인터 때문인데, C / C++ 은 포인터를 사용자가 직접 제어가 가능하지만,
Java는 포인터가 내부에 숨겨져 있어 더 어렵게 느껴졌다.

3.1.1 래퍼 클래스 (Wrapper Class)

비객체인 원시 자료형과 대응하는 객체 자료형이다.
래퍼 클래스는 비객체인 원시 자료형을 객체로 요구할 때 사용된다.
특징으로는 내부에 멤버 변수(원시 자료형)가 final 선언이 되어있다.

주요 요구는 매개변수로 객체가 요구될 때이다. 

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;

public class Main {
    public static void main(String[] args) {
        //매개변수를 Wrapper Class 로 사용
        ArrayList<Integer> arrayListA = new ArrayList<>();
        LinkedList<Integer> linkedListA = new LinkedList<>();
        HashMap<Integer, Boolean> hashMapA = new HashMap<>();

        //매개변수를 Primitive 자료형으로 사용
        ArrayList<int> arrayListB = new ArrayList<>();
        LinkedList<int> linkedListB = new LinkedList<>();
        HashMap<int, boolean> hashMapB = new HashMap<>();
    }
}
/*
결과 :
Error:(11, 19) java: unexpected type
               required: reference
               found:    int
Error:(12, 20) java: unexpected type
               required: reference
               found:    int
Error:(13, 17) java: unexpected type
               required: reference
               found:    int
Error:(13, 22) java: unexpected type
               required: reference
               found:    boolean
 */

결과를 보면 래퍼 클래스를 매개변수로 한 선언에서는 에러가 발생하지 않았지만,
원시 자료형을 매개변수로 한 선언에서는 java : unexpected type 에러가 발생하였다.
required : reference (요구 : 참조형)
found : int or boolean (사용 : 값)

원시 자료형에 대응하는 래퍼 클래스

원시 자료형(Primitive Type) 래퍼 클래스(Wrapper Class)
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Char
boolean Boolean

+ void의 래퍼 클래스인 Void도 존재한다.

3.1.2 문자열 클래스 (String Class)

문자열 클래스인 String이다. 참조 자료형인 클래스 타입(Class Type) 중에서도 특별한 참조 자료형이다.

우선 String의 선언부터 살펴보자.

public class Main {
    public static void main(String[] args) {
        String strA = new String("CafeCoder");
        String strB = new String("CafeCoder");
        String strC = "CafeCoder";
        String strD = "CafeCoder";

        System.out.println("값");
        System.out.println("strA : " + strA);
        System.out.println("strB : " + strB);
        System.out.println("strC : " + strC);
        System.out.println("strD : " + strD);

        System.out.println();

        System.out.println("비교 결과");
        System.out.println("strA == strB : " + (strA == strB));
        System.out.println("strA == strC : " + (strA == strC));
        System.out.println("strC == strD : " + (strC == strD));
    }
}
/*
결과 :
값
strA : CafeCoder
strB : CafeCoder
strC : CafeCoder
strD : CafeCoder

비교 결과
strA == strB : false
strA == strC : false
strC == strD : true

Process finished with exit code 0
 */

String은 위의 strA, strB 처럼 new 생성자를 통해 생성하거나, strC, strD 처럼 값을 입력하여 생성할 수 있다.
위의 코드는 String 객체를 생성하여 이용한다는 것은 같지만, 생성되는 메모리 영역이 다르다.
다른 참조형과 마찬가지로 Heap 메모리에서 관리하지만, 특별하게 불변한(immutable) 특징을 가진다.
이런 특징을 가진 객체를 불변객체(immutable object)라고 한다.

위와 같은 특징들 때문에 String을 사용할 때 메모리 관리 측면에서 상당히 비효율적이다.
필요한 "CafeCoder" 문자열이 여러 개고, 위와 같이 선언했다면 4개의 CafeCoder가 메모리에 올라가게 된다.
이것을 방지하기 위해 만들어진 메모리 영역을 String Constant Pool이라 한다.

String Constant Pool을 살펴보자.

String Constant Pool
String Constatn Pool

strC와 strD는 같은 주소를 참조한다. 하지만 strA 와 strB는 String Constatnt Pool에 저장된 것이 아닌, 
Heap 메모리에 저장되어 참조하게 된다. 따라서 다음과 같은 비교 결과가 나오게 된다.

(strA == strB) : false
(strA == strC) : false
(strC == strD) : true  

String Type은 정말 강력한 기능들을 많이 지원하지만, 메모리 관리에 취약한 단점을 갖고 있다는 결론이 나온다.
이것을 그나마 해결할 수 있는 방법에 대해 번외에서 마저 설명하겠다.

3.2 인터페이스 타입 (Interface Type)

이번에 설명할 것은 인터페이스 타입의 자료형이다. 

우선 인터페이스를 만들어 보자.

interface Interace<Generic>{
	int SetValue(Generic value);
}

인터페이스의 이름인 Interface 우측에 붙은 <Generic>은 제네릭 타입이며, 우선은 인터페이스를 자료형에 얽매이지
않게 해주는 '옵션'이라고만 생각해 두자.

정의 : 인터페이스(interface)는 자바 프로그래밍 언어에서 클래스들이 구현해야 하는 동작을 지정하는 데 사용되는 
추상형이다. (참조 : 위키백과_인터페이스(자바))

인터페이스(interface)는 클래스처럼 정의하지만, 클래스처럼 객체를 만들거나 메소드를 직접 구현 할 수 없다.
쉽게 풀어쓰자면, class의 템플릿(template)이라 생각하면 된다. 

자세한 내용은 후에 interface와 abstract를 같이 다뤄 서술하겠다.

3.3 배열 타입 (Array Type)

배열은 규칙, 순서, 간격 등에 따라 나열된 요소/데이터들의 집합/모양으로 정의되어있다.
같은 자료형을 갖는 집합체로 볼 수 있다. 보통 자료형의 효율적인 관리를 목적으로 선언된다.
+같은 자료형이 아닌 여러 종류의 자료형을 가진 집합체는 레코드라 한다.

Java의 배열은 Heap 메모리에 연속적으로 직접 할당된다.

배열은 기본형으로 선언할 수 있고, 참조형으로 선언할 수 있다. 다음 코드를 보자.

public class Main {
    public static void main(String[] args) {
    	//일반형 선언
        Object[] objects = new Object[2];
        int[] ints = new int[10];
        
        //참조형 선언
        char[] chars = null;
    }
}

기본적으로 배열은 주소라고 볼 수 있다.
위에 서술했듯이 Heap 메모리에 연속적으로 할당되는데, 이때 배열의 주소는 배열의 첫 번째 인덱스의 주소와 같다.

일반형으로 선언한 두 배열의 경우는 Heap 메모리에 각 자료형의 크기 * 배열의 길이 만큼의 메모리가 할당되어 있고,
참조형으로 선언한 배열은 주소가 null 값으로 선언은 되었지만, 정의는 되어있지 않은 상태이다.
후에 다른 캐릭터 배열의 주소 값으로 정의하여 같은 배열로 사용할 수 있다.

3.4 열거 타입 (Enum Type)

Java jdk 1.5 버전 이후로 사용 가능한 자료형이다. 관련이 있는 상수(Constant)들의 집합이라 볼 수 있다.
C / C++ 역시 enum 이 존재한다. 하지만 Java의 Enum은 참조형으로 더욱 더 강력한 기능을 지원한다.
Enum은 클래스와 마찬가지로 별도의 Java파일, 클래스 안, 클래스 밖에 선언할 수 있다.

먼저 Java jdk 1.5 미만 버전의 상수를 선언하는 방법을 살펴보자.

public class Main {
    private final static int one = 1;
    private final static int two = 2;
    private final static int three = 3;

    public static void main(String[] args) {
        System.out.println(one);
        System.out.println(two);
        System.out.println(three);
    }
}
/*
결과 :
1
2
3

Process finished with exit code 0
 */

+ 기본적으로 final 즉 상수를 선언할 때는, static으로 선언하여 메모리 할당을 한 번만 하도록 설정해준다.

위와 같은 코드에서는 main의 바로 위에 상수로 정의되어있어 각각의 변수명이 어떤 것을 가리키는지 쉽게 알 수 있다.
하지만 코드가 복잡해질수록 상수를 파악하는 데 어려움을 느낄 수 있다.
이런 점을 보완하기 위해 기존에 상수들은 클래스나 인터페이스를 통해 선언되었다.
+ 어떤 클래스가 상수만으로 이뤄져 있다면, 인터페이스로 선언하는 것이 좋다.

하지만 위 방식은 몇 가지 한계가 존재한다.

  1. 타입에 대한 안정성을 보장하지 않는다.
    위 코드에서는 one, two, three를 상수 명으로 사용하였는데, 꼭 이런 경우만 있으리란 법은 없다.
    예를 들어 red라는 상수를 선언하였는데 이때 red의 값이 0으로 쓰일지 1로 쓰일지는 아무도 모른다.
    또한, red를 대표하는 값이 될 수 없다.
  2. 이름에 대한 값을 보장하지 않는다.
    예를 들어 System.out.println(one); 코드를 실행한다고 생각해보자. 결과는 1이 나올 것인데,
    위 코드에 private static final int first = 1; 한 줄을 추가하여 실행한다면, 결과로 나온 1이
    one에 대한 것인지, first에 대한 것인지 보장할 수 없다.
  3. Namespace를 보장하지 않는다.

위의 코드를 열거형으로 바꿔보면 다음과 같은 코드가 나온다. 

Java jdk 1.5이후 버전의 상수를 선언하는 방법

enum Number {
    ONE, TWO, THREE; // 1번 
    ONE(1), TWO(2), THREE(3); // 2번
}

1번의 방법은 문자열만을 상수로 선언해 준 것이고, 2번의 방법은 문자열과 대응하는 정수형 상수를 추가해 준 것이다.
두 방식 중 한 방식만을 사용하여 선언해야 하며, 선언된 상수는 처음 정의된 뒤 변경할 수 없다.

2번의 방법을 사용할 때는 생성자와 정수형 상수를 반환할 멤버 함수가 필요하다.

그렇다면 사용하는 방법을 알아보자.
다음은 1번 방식을 사용하여 선언하고, 사용하는 코드이다.

enum Number {
    ONE, TWO, THREE;
}

public class Main {
    public static void main(String[] args) {
        System.out.println("단순 상수 출력하기");
        System.out.println(Number.ONE);
        System.out.println(Number.TWO);
        System.out.println(Number.THREE);

        System.out.println("\n" + "values메소드를 통해 상수 출력하기");
        for(int index = 0, size = Number.values().length ; index < size ; index++){
            System.out.println(Number.values()[index]);
        }

        System.out.println("\n" + "values와 switch 를 통해 2번 방식 구현하기");
        for(int index = 0, size = Number.values().length ; index < size ; index++){
            switch (Number.values()[index]){
                case ONE :
                    System.out.println(1);
                    break;
                case TWO :
                    System.out.println(2);
                    break;
                case THREE :
                    System.out.println(3);
                    break;
                default:
                    System.out.println("해당 없음");
            }
        }
    }
}
/*
결과 :
ONE
TWO
THREE

ONE
TWO
THREE

1
2
3


Process finished with exit code 0
 */

위와 같이 상수 활용이 가능하며, 2번 방식을 사용하지 않고도 문자열 상수에 대응하는 상수를 지정하여 사용할 수 있다.

다음은 2번 방식을 사용하여 선언하고, 상수를 사용하는 코드이다.

enum Number {
    ONE(1), TWO(2), THREE(3);
    private int value;

    Number(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        Number one = Number.ONE;
        Number two = Number.TWO;
        Number three = Number.THREE;

        System.out.println(one + " : " + one.getValue());
        System.out.println(two + " : " + two.getValue());
        System.out.println(three + " : " + three.getValue());
    }
}

/*
결과 :
ONE : 1
TWO : 2
THREE : 3

Process finished with exit code 0
 */

 

그렇다면 Enum을 사용하기 전 Enum의 특징과 주의사항에 대해 알아보자.

특징

  1. Java의 Enum은 참조형으로, 클래스나 인터페이스처럼 생성자, 함수, 변수 등을 만들 수 있다.
    C / C++ 보다 강력한 기능을 지원하는 이유이기도 하다.
  2. 상수 비교는 기본적인 비교연산자를 사용할 수 있다.
  3. Java 컴파일러는 Enum의 상수들에 대해 제공하는 values() 메소드를 통해 상수를 배열화 할 수 있다.
  4. 함수를 오버라이드 하거나 생성할 수 있다.
  5. EnumMap, EnumSet 컬렉션 클래스가 존재한다. 따라서 더욱 다양하게 활용할 수 있다.
  6. 인스턴스는 코드상에서 처음 호출되거나, 참조될 때 생성된다.
  7. 클래스처럼 인터페이스를 구현할 수 있다.
  8. 내부에 추상함수를 정의할 수 있다.

주의사항

  1. Namespace를 보장하므로, 정의된 상수를 제외한 다른 값을 할당할 수 없다.
  2. 선언하는 방식을 위 코드의 2번 방식으로 선언한다면, 생성자, 멤버 변수를 작성해야 한다.
    생성자는 private로 선언해야한다.
  3. 상수는 처음 정의된 후 변경할 수 없다.
  4. new를 사용하여 생성할 수 없다.

4. 번외

4.1 long 보다 큰 숫자가 필요하다면 BigInteger를 사용하자.

일반적인 연산자 (+, -, *, /, %, 등...) 은 사용할 수 없다.
하지만 BigInteger를 매개변수로 하는 add(), subtract(), multiply(), divide() 메소드가 제공된다.

import java.math.BigInteger;

public class test {
    public static void main(String[] args) {
        BigInteger A = BigInteger.valueOf(555);
        BigInteger B = BigInteger.valueOf(222);

        System.out.println("A : " + A);
        System.out.println("B : " + B);
        System.out.println("A + B : " + A.add(B));

        System.out.println("A to float : " + A.floatValue());
    }
}

/*
결과 : 
A : 555
B : 222
A + B : 777
A to float : 555.0

Process finished with exit code 0
*/

 

4.2 구조체 vs 클래스 (Struct vs Class)

  • 4.2.1 구조체
    연관성 있는 여러 자료형을 하나로 묶어 새롭게 정의한 자료형이다. 예를 들어 강아지의 이름과 나이를
    같이 사용하고 싶다면, String name, int age 두 개를 묶어 하나의 변수처럼 사용할 수 있다.
  • 4.2.2 클래스
    먼저 간단하게 설명하자면, 구조체에 메소드를 추가한 것으로 생각하면 이해하기 쉽다.
    많은 Java 입문 책 혹은 기술 블로그에서 클래스를 붕어빵 틀에 비유한다.
    말 그대로 붕어빵을 만들기 위한 틀, 즉 설계도와 같다 할 수 있다.

  • 4.2.3 특징
    간단하게 둘의 특징을 비교하면 다음과 같다.
특징 구조체 클래스
그룹화 여러 자료형을 묶어 개발자의 필요에 따라 새로운 자료형으로 사용할 수 있다.
인터페이스 인터페이스를 구현할 수 있다.
구성 자료형 자료형 + 메소드
캡슐화 및 상속 불가능 가능
할당 메모리 공간 Stack Heap
생성자 필요없음 필요함

4.3 String Type 의 메모리 관리

String strA = new String("CafeCoder");
String strB = new String("CafeCoder");
String strC = "CafeCoder";
String strD = "CafeCoder";

위와 같이 4개의 String 변수를 선언했다고 가정하자.
그렇다면 위에 설명했듯이 strC와 strD는 String Constant Pool에,
strA와 strB는 Heap 메모리에 생성될 것이다.

strC와 strD는 같은 주소를 참조할 것이며, strA와 strB는 서로 다른 주소를 참조할 것이다.
이때 strA.intern() 메소드를 사용하면, Heap 메모리에 있던 String 객체는 String Constant Pool로 이동한다.
그렇다면 strA.intern()는 strC 그리고 strD는 같은 주소를 참조하게 된다.
strA = strA.intern(); 코드를 실행시켜 준다면 결과는 다음과 같이 바뀌게 된다.

(strA == strB) : false 
(strA == strC) : true 
(strC == strD) : true  

 

마치며

이 포스팅을 끝내는데 시간이 너무 오래 걸렸다. 이거 하다 보면 아 맞아 이것도 추가해야지! 하고 신나서
추가하다 보니, 뜻밖에 꽤 디테일한 자료가 되었지만, 시간이 그만큼 소요된 것이다.
(그래프나 코드를 직접 다 작성해서 가져오는것 역시 한몫 했다.)

그래도 매일 중간중간 저장을 하며 조금씩 완성되어가는 결과물을 보며 너무 행복했다.
내가 배운 이 내용이 누군가에게도 도움이 되었으면 하는 마음이다.

ONE FOR ALL,
ALL FOR ONE

반응형

관련글 더보기

GitHub 댓글

댓글 영역