공부방/JAVA

자바 데이터 타입, 변수 그리고 배열 - 백기선 자바라이브스터디

EVO. 2023. 7. 22. 17:03

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

총 8가지의 기본타입이 있습니다.

  Type Default Value(기본값) Size(할당크기) Range of values(범위)
Integer(정수형) byte 0 1 byte -128~127
short 0 2 byte -32,768 ~ 32,767
int 0 4 byte -2,147,483,648 ~
2,147,483,647
long 0 8 byte -9,223,372,036,854,775,808 ~
9,223,372,036,854,775,807
Floating-point(실수형) float 0.0 4 byte 32-bit IEEE 754 floating-point 
double 0.0 8 byte 64-bit IEEE 754 floating -point
Char(문자형) char \u0000 2 byte (유니코드) 0 ~ 65,535
Bool(논리형) boolean false 1 byte true, false

int 형의 범위는 왜 저렇게 나온 걸까

int형은 메모리크기가 4byte로 정해져 있습니다. 그리고 1byte = 8bit이므로 총 32비트로 int형을 표현하게 됩니다.

그렇다면 0과 1로 32칸을 채울 수 있다는 것과 같은데 0~2의 32 제곱 (0~4,294,967,296)가 아니라 음수로 표현되는 걸까요

 

여기에 대한 답은 컴퓨터에서 음수를 표현하기 위해 MSB를 사용합니다. MSB란 Most Significant Bit 의 줄임말로 최상위 비트를 뜻합니다. 최상위 비트란 일반적으로 가장 왼쪽에 위치한 비트를 뜻합니다.

 

int는 비트수가 매우 크므로 byte형의 1byte(= 8bit)를 살펴보겠습니다.

8비트는 다음과 같이 나타날 수 있습니다.

x 0 0 0 0 0 0 0

x 표시한 가장 왼쪽에 나타낸 비트를 MSB라고 부르고 부호 비트라고 부릅니다. 이 값이 1이면 음수, 0이면 양수라고 판단합니다. 즉, 부호가 있는 자료형인 경우 1비트는 부호, 나머지 7비트로만 활용하기 때문에

2의 7승(=128, 하지만 양수는 0도 포함하기 때문에 127)  범위가 -128~127 입니다.

0~255까지 표현을 하고 싶다면 C에서 사용한것처럼 unsigned 자료형을 사용하면 되겠지만 JAVA의 같은 경우 primitive type에 unsigned를 지원하지 않습니다. 보통 큰 범위를 가진 자료형을 사용합니다.

 

하지만 JAVA 8 버전부터 다른 방법으로 사용을 할 수 있게 되었습니다.

예를들어 unsigned int 처럼 쓰고 싶으면 기본형이 아닌 BigInteger 클래스가 지원하는 static 메서드가 있습니다.  

BigInteger bigInteger = BigInteger.valueOf(22000000L);

결론적으로 int형은 8byte(=32bit)이고 2의 32제곱이 아닌 2의 31제곱까지만 양수로 표현함으로써

범위가(-2,147,483,648~2,147,483,647)을 이해할 수 있고, 또한 unsigned를 쓰고 싶다면 위의 코드와 같이 사용하면 됩니다.


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

primitive type 

  • byte, short, int, long, float, double, char, boolean
  • 기본타입으로 선언된 변수는 실제 값을 저장합니다.

 

reference type 

  • 기본타입을 제외한 모든 type은 reference type입니다.
  • 객체를 참조합니다.
  • 참조타입으로 선언된 변수는 참조하는 객체의 주소를 저장합니다.

리터럴

boolean result = true;
char capitalC = 'C';
byte b = 100;
short s = 10000;
int i = 100000;
  • 기본타입의 변수를 초기화할 때 new 키워드를 사용하지 않습니다.
  • 리터럴은 고정 값의 소스코드 표현으로, 연산할 필요 없이 코드에서 직접적으로 보입니다.
  • 위의 소스코드처럼, 기본 유형의 변수에 리터럴을 할당할 수 있습니다. (Stirng 클래스에 문자열 리터럴 할당가능)

 Integer 리터럴

  • 정수 리터럴은 문자 L 또는 l로 끝나는 경우 long 유형이고, 그렇지 않으면 int 유형입니다.
  • 소문자 l은 숫자 1과 구분하기 어렵기 때문에 대문자 L을 사용하는 것이 좋습니다.
  • 정수형인  byte, short, int, long 타입의 값은 int 리터럴에서 생성할 수 있습니다.
  • int의 범위를 초과하는 long 타입의 값은 long 리터럴에서 생성할 수 있습니다. 
  • 정수형 리터럴은 10진수, 16진수, 2진수로 표현이 가능합니다.
// The number 26, in decimal
int decVal = 26;
//  The number 26, in hexadecimal
int hexVal = 0x1a;
// The number 26, in binary
int binVal = 0b11010;

Floating-Point 리터럴

  • floating-point 리터럴이 float 타입일 경우 F 혹은 f로 값이 끝납니다.
  • double 타입일 경우엔 D 또는 d로 끝나며 관례상 생략합니다.
  • E와 e와 같이 과학적 표기법으로 표현하는 경우도 있습니다.
double d1 = 123.4;
// same value as d1, but in scientific notation
double d2 = 1.234e2;
float f1  = 123.4f;

Character and String 리터럴

  • 문자 및 문자열 유형의 리터럴에는 모든 유니코드(UTF-16) 문자가 포함될 수 있습니다. 
  • 문자 리터럴에는 항상 '작은따옴표'를, 문자열 리터럴에는 '큰따옴표'를 사용합니다.
  • Java 프로그래밍 언어는 문자 및 문자열 리터럴에 대한 몇 가지 특수 이스케이프 시퀀스도 지원합니다 
    • \b(백스페이스)
    • \n(줄 바꿈)
    • \r(캐리지 리턴)
    • \t(탭)
    • \f(폼 피드)
    • \"(큰따옴표)
    • \\(백슬래시)
  • 모든 참조 유형의 값으로 null 리터럴도 있습니다. null은 primitive 타입 변수를 제외한 모든 변수에 할당할 수 있습니다.
char test = 'a';
String cTest = "hello";
//참조타입에 null 저장
Student x = null;

 

참고

JDK 1.7부터 정수형 리터럴의 중간에 구분자 '_'를 넣을 수 있게 되어서 큰 숫자를 편하게 읽을 수 있게 되었습니다.

long big = 100_000_000L; // long big = 100000000L;
long hex = 0xFFFF_FFFF_FFFF_FFFFL; // long hex = 0xFFFFFFFFFFFFFFFL;

 


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

변수선언과 초기화 한 번에

public class CreateVariable {  
    public static void main(String[] args)   
{  
//기본 타입 variable declaration + initialization 
        int student_id = 10;  
        double numbers = 3.21;  
    }  
}

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

변수의 범위(scope)란 변수에 액세스 할 수 있는 프로그램 영역 또는 섹션을 의미합니다.

변수의 수명(life time)은 변수가 메모리에서 얼마나 오래 살아남아 있는지를 나타냅니다.

 

변수의 유형 및 범위

변수에는 세 가지 유형이 있습니다. 

  1. 인스턴스 변수
  2. 클래스 변수
  3. 지역 변수

이제 위에서 언급한 각 유형의 범위와 수명에 대해 자세히 살펴보겠습니다.

 

 

인스턴스 변수

클래스 내부에서 선언되지만 메서드 및 블록 외부에서 선언되는 변수를 인스턴스 변수라고 합니다.


범위: static 메서드를 제외한 클래스 전체에 걸쳐 있습니다. 
수명: 인스턴스가 생성될 때 인스턴스 변수가 메모리에 올라오며 클래스의 객체가 메모리에 남아있을 때까지 살아있습니다.

 

클래스 변수

클래스 내부, 모든 블록 외부에 선언되고 static으로 선언된 변수를 클래스 변수라 합니다.

 

범위 : 클래스 전체에 걸쳐 있습니다.

수명 : 클래스가 메모리에 올라갈 때 클래스 변수도 메모리에 올라가며 프로그램이 끝날 때까지 살아있습니다.

 

지역 변수

인스턴스나 클래스 변수가 아닌 모든 변수를 로컬 변수라고 합니다.


범위: 블록 내에서 선언됩니다.
수명: 변수선언문이 수행되었을 때 생성되며 선언된 블록을 떠날 때까지 살아있습니다.


이제 변수의 범위와 수명에 대한 개념을 보다 명확하게 이해할 수 있도록 예제 코드를 살펴보겠습니다.

public class scope_and_lifetime {
    int num1, num2;   //인스턴스 변수
    static int result;  //클래스 변수
    int add(int a, int b){  //지역 변수
        num1 = a;
        num2 = b;
        return a+b;
    }
    public static void main(String args[]){
        scope_and_lifetime ob = new scope_and_lifetime();
        result = ob.add(10, 20);
        System.out.println("Sum = " + result);
    }
}

위의 예제 코드에서 num1과 num2는 인스턴스 변수입니다. 변수 result는 클래스 변수입니다. 정상적으로 30이 출력이 됩니다.

 

이번엔 저 코드에서 static을 지워보겠습니다.

public class scope_and_lifetime {
    int num1, num2;   //Instance Variables
    int result; 
    int add(int a, int b){  //Local Variables
        num1 = a;
        num2 = b;
        return a+b;
    }
    public static void main(String args[]){
        scope_and_lifetime ob = new scope_and_lifetime();
        result = ob.add(10, 20);
        num1 = 10;
        System.out.println("Sum = " + result);
    }
}

이 코드를 실행하면 오류가 발생하는 것을 알 수가 있습니다.

아까 코드와 달라진 부분은 static키워드를 제거해서 result변수가 클래스변수=> 인스턴스변수로 바꿨습니다. staitc main 메서드 내부에서는 인스턴스 변수를 사용할 수 없으므로, 정적 메서드 내에서 result와 num1을 사용하면 에러가 발생합니다.


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

형변환(캐스팅)이란 변수 또는 상수의 타입을 다른 타입으로 변환하는 것을 말합니다.

 

primitive 간 형변환 

  • 정수형간의 형변환
    큰 타입에서 작은 타입으로의 변환할 때 크기의 차이만큼 잘려나가 값 손실이 날 수 있습니다.
    값 손실 없는 경우
    int : 00000000000000000000000000001010 (10진수 : 10)
    byte : 00001010 (10진수 : 10)

    값 손실 있는 경우
    int : 00000000000000000000000100101100 (10진수 : 300)
    byte : 00101100 (10진수 : 44)

    작은 타입에서 큰 타입으로의 변환되는 경우는 저장공간의 부족으로 잘려나가는 일이 없으므로 값 손실이 일어나지 않습니다.

  • 실수형 간의 형변환
    실수형에서도 정수형처럼 작은 타입에서 큰 타입으로 변환하는 경우, 빈 공간으로 0으로 채웁니다. float타입의 값을 double타입으로 변환하는 경우, 가수(M)는 float의 가수 23자리를 채우고 남은 자리를 0으로 채웁니다.

    반대로 double 타입에서 float 타입으로 변환하는 경우, 가수(M)는 double의 가수 52자리 중 23자리만 저장되고 나머지는 버려집니다. 그런데 버려질 때 가수의 24번째 자리의 값이 1이면 반올림이 발생합니다. 

    참고로, float의 범위를 초가 한 값을 float로 저장한 상태에서 이 변수를 double타입으로 형변환하면 0으로 채워지기 때문에 값의 오차가 발생합니다.
float f = 9.1234567f;
// 9.1234567 = 1.0010001111110011010101101011.... x 2^3
// float f에 저장되기위해 가수부분 23자리만 저장
// 0|10000010|00100011111100110101110

double d = 9.1234567;
// 0|10000010|0010001111110011010111011011101110110110001110010101

// f와 d는 같은 값을 저장해도 float와 double의 정밀도 차이 때문에 서로 다른 값이 저장됩니다.

double d2 = (double)f;
// 0|10000010|0010001111110011010111000000000000000000000
// 저장할 때 이미 값이 달라졌기 때문에, 형변환을 해도 값이 같아지지 않습니다.

해당 바이너리 코드는 길어져서 무작위로 적었습니다.

  • 정수형과 실수형 간의 형변환
    정수형을 실수형으로 변환할 때는 정수는 소수점이하의 값이 없으므로 비교적 변환이 간단합니다. 그저 정수를 2진수로 변환한 다음 정규화를 거쳐 실수의 저장형식으로 저장될 뿐입니다. 다만 오차를 발생하지 않게 하려면 double에서 정수형으로 형변환해야 오차가 발생하지 않을 것입니다.

    실수형에서 정수형으로 변환할 때는 실수형의 소수점이하 값은 버려집니다. 정수형의 표현형식으로는 소수점 이하의 값은 표현할 수 없기 때문입니다. 

자동 형변환(타입 프로모션)

편의상인 이유로 형변환을 생략하면 컴파일러가 생략된 형변환을 자동적으로 추가합니다. 다만 타입 프로모션은 저장하려는 값이 변환하려는 형의 값의 범위보다 작을 때만 가능합니다.

 

자동 형변환 규칙

byte < short < int < long < float < double

char < int

왼쪽에서 오른쪽으로의 변환은 형변환 연산자를 사용하지 않아도 자동 형변환이 되며, 그 반대 방향으로의 변환은 반드시 형변환 연산자를 써줘야 합니다.

byte a = 40;
byte b = 50;
byte c = 100;
int d = a * b / c;

byte b = 5;/* java2s.com*/
b = b * 2; // Error! Cannot assign an int to a byte!

1차 및 2차 배열 선언하기

class ArrayExample {
	public static void main(String[] args) {
        //1차원 배열
        int[] oneDimensionArrayEx1 = {1, 2, 3, 4, 5};
        

        //2차원 배열
        int[][] twoDimensionArrayEx1 = {{1, 2}, {3, 4}};
        
    }
}

1차원 배열

 oneDimensionArrayEx1는 []을 통해 배열 참조 변수로 선언을 했습니다. 그러고 나서 1,2,3,4,5를 런타임 시점에 힙영역에 차곡차곡 만듭니다. 그다음 1이 들어간 힙영역 주소값을 참조변수에 저장합니다.

 

2차원 배열

  • Runtime Stack 영역에 twoDimensionArrayEx1 은 2개의 요소 크기(2개 요소에 주소값을 가지고 있음)를 가진 힙 영역 주소값을 가집니다.
  • 힙 영역에는 실제 값이 들어있는 요소들과 주소값이 들어있는 요소들로 존재하게 됨

출처 :&nbsp;https://www.notion.so/2-38b5d67c7f5a48238529bb8f1617ea0d


타입추론, var

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

추론 알고리즘을 통해 인수타입을 결정하고 가능하다면 반환 타입도 추론합니다. 

 

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입니다. 

pick메서드를 호출하면서 첫 번째 인자로 String 타입, 두 번째 인자로 ArrayList <String> 타입을 전달합니다. 이런 경우 모든 인자의 공통 부모인 Serailizable로 T를 결정하게 됩니다.

 

타입 추론의 다양한 유형에 대해서 설명해 드리겠습니다.

 타입추론과 제너릭 메서드

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

public class BoxDemo {

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

  public static <U> void outputBoxes(java.util.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) {
    java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
      new java.util.ArrayList<>();
    BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
    BoxDemo.outputBoxes(listOfIntegerBoxes);
  }
}

해당 예시 출력 값

Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]

Java 컴파일러는 일반 메서드 호출의 type 매개변수를 유추할 수 있습니다. 

BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

<Integer> 부분을 생략해도 추론이 가능합니다.

 

 

 

 

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

Generic 클래스의 생성자를 호출하기 위해 필요한 type arguments를 비어있는 <>로 대체할 수 있습니다.

// For example, consider the following variable declaration:

Map<String, List<String>> myMap = new HashMap<String, List<String>>();

// You can substitute the parameterized type of the constructor with an empty set of type parameters (<>):

Map<String, List<String>> myMap = new HashMap<>();

 

 

 

 

타입추론과 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이 들어갑니다.

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

 

 

Target Types

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

 

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

위 코드는 Collections API의 emptyList 메서드를 이용해 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 SE 7 컴파일러에서는 type argument T를 위한 value가 필요한데 아무것도 주어지지 않아 Object로 결정합니다. 그 결과 Collections.emptyList()는 List<Object> 객체를 리턴합니다.

processStringList메서드 인자가 인터페이스인 List는 해당 객체를 받을 수 없어 이러한 오류메시지가 발생합니다.

 

그래서 Java SE 7에서는 type witness를 명시해줘야 합니다.

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

 

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

 

 

 

var

Java 10부터 추가된 지역변수 타입 추론은 로컬 변수 선언을 var을 이용하여 기존의 엄격한 타입 선언 방식에서 컴파일러에게 타입추론 책임을 위임할 수 있게 되었습니다. 

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

 

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));
}

2. forEach

var numbers = Arrays.asList(1, 2, 3, 4, 5);

for (var number : numbers) {
    System.out.println(number);
}

⇒ 기존에는 Object 타입으로 받아서 형변환을 하거나  직접 타입추론해 명시적 타입선언을 해줬는데 var를 사용하여 훨씬 편하게 타입선언이 가능해집니다.

 

 


참고