Post

기강 자바-01





자바의 타입

자바에서 사용하는 변수의 타입은 크게 두 가지로 분류할 수 있다.

  • 기본형 타입 (Primitive Type)
  • 참조형 타입 (Reference Type)



기본형 타입

  • Primitive Type
    • 원시형 또는 기본형 타입이라고 부른다.
    • 자바에는 여덟 가지의 기본형 타입이 존재한다.
    • 기본형은 한 번에 한 가지 타입을 나타낼 수 있다.
    • 기본형 타입은 자바에 의해 정의된 타입이며, 사용자가 직접 정의하거나 재정의할 수 없다.
  • 기본형 타입은 리터럴을 갖는 변수의 타입을 의미한다.
    • 리터럴이란 변하지 않는 값, 그 자체를 의미한다.
    • 기본형 타입의 변수를 사용하는 것은 그 변수가 갖고있는 데이터를 그대로 사용하는 것이다.
    • 정수형, 실수형, 논리형, 문자형, null 등이 있다.
  • 기본형 타입의 값들은 JVM의 메모리 영역 중에서 Stack 영역에 저장된다.
  • == 같은 비교 연산자를 사용해 값을 비교할 수 있다.



기본형 타입메모리 크기타입값의 범위기본 값
boolean1byte논리형true, falsefalse
char2byte문자형\u0000 ~ \uffff (Unicode)\u0000
byte1byte정수형-2^7 ~ 2^7 - 10
short2byte정수형-2^15 ~ 2^15 - 10
int4byte정수형-2^31 ~ 2^31 - 10
long8byte정수형-2^63 ~ 2^63 - 10
float4byte실수형1.4E–45 ~ 3.4028235E+380.0f
double8byte실수형4.9E–324 ~ 1.7976931348623157E+3080.0d



2의 보수법

  • 자바는 부호 비트와 2의 보수법으로 음수를 표현한다.
  • 부호 비트란,
    • 숫자 타입의 MSB를 부호를 나타내는 비트로 사용한다.
    • 0이면 양수
    • 1이면 음수
  • 2의 보수법이란
    • 어떤 수 x에 y를 더했을 때, 주어진 자릿수에서 가장 큰 값이 되도록 하는 y를 보수라고 한다.
    • 주로 컴퓨터에서 음수를 나타낼 때 사용된다.
      • 2의 보수법을 사용해 부호화된 이진수에서 양수, 음수를 모두 나타낼 수 있다.
      • 1의 보수 : 각 비트를 모두 반전 시킨 것
      • 2의 보수 : 1의 보수에서 1 더한 것
    • 예를 들어 6이 있을 때, (4비트까지만 표현, 앞의 1111.. 생략)
      • 1의 보수 : 0110 -> 1001
      • 2의 보수 : 0110 -> 1010



부동 소수점

  • 실수를 표현하는 방식에는 두 가지가 있다.
    • 고정 소수점
    • 부동 소수점
  • 자바는 기본형 타입으로 실수형을 표현하는데 부동 소수점을 사용한다.



먼저 고정 소수점을 알아본다. 예시는 32비트이다.

1
0111 1111 1111 1111 2222 2222 2222 2222
  • 위의 32비트는 다음과 같이 이뤄져 있다. (소수점의 위치는 내가 임의로 정했음)
    • 부호 비트 (0) - 1bit
    • 정수 비트 (1) - 15bit
    • 실수 비트 (2) - 16bit
  • 정수와 실수 부분이 나눠져 있고, 소수점의 위치가 변하지 않는다.
    • 정수 비트에 여유가 있어도, 실수를 표현하는데 사용할 수 없다.
    • 반대의 경우도 마찬가지이다.



이번엔 부동 소수점을 알아본다.

1
0111 1111 1222 2222 2222 2222 2222 2222
  • 위의 32비트는 다음과 같이 이뤄져 있다.
    • 부호 비트 (0) - 1bit
    • 지수 비트 (1) - 8bit
    • 가수 비트 (2) - 23bit
  • 부동 소수점은 소수점의 위치가 변할 수 있다.
    • 수를 정규화하여 최대한 많은 수를 표현할 수 있도록 한다.
  • 부동 소수점으로 수를 나타내는 방법
    • 10진수인 59.125가 있다고 할 때,
    • 10진수를 2진수로 변환한다. (59.125 => 111011.001)
    • 2진수를 정규화한다. (111011.001 => 1.11011001 * 2^5)
    • 2의 지수를 지수 비트에, 나머지 실수 부분을 가수 비트에 담아 표현할 수 있다.
      • 부호 비트 : 0
      • 지수 비트 : 100 0100 1
      • 가수 비트 : 110 1100 1000 0000 0000 0000
      • 합치면 : 0100 0100 1110 1100 1000 0000 0000 0000



과제

  • 38.25를 부동 소수점으로 나타내 보세요.
  • 부동 소수점의 부정확성에 대해 알아보고 설명해 보세요.
    • 연산 순서
    • 정확한 실수 계산을 위한 대안





참조형 타입

  • Reference Type
    • 참조형 데이터 타입
    • 참조형 타입은 사용자가 정의할 수 있으므로, 그 개수가 무한하다.
    • 참조형 타입은 0개 이상의 기본형 혹은 참조형 타입을 나타낼 수 있다.
  • 참조형 타입은 객체를 참조하는 타입을 의미한다.
    • 객체의 메모리 주소를 저장하고 있고, 그 메모리 위치에 실질적인 데이터가 들어있는 모양이다.
    • 참조형 타입의 객체를 사용한다는 것은 해당 객체가 갖고있는 메모리 주소에 위치한 데이터를 참조하는 것이다.
  • 자바의 참조형 타입의 종류로는
    • 클래스, 인터페이스, 열거형, 배열 등이 있다.
  • 참조형 타입의 객체는 JVM의 메모리 영역 중에서 Heap 영역에 저장된다.
  • 참조형 타입의 인스턴스를 만들기 위해서는 new 연산자를 사용해야 한다.
    • 참조형 타입의 초기화 과정이 없다면 기본적으로 null이 들어간다.
    • new 연산자를 사용해 만든 두 인스턴스는 갖고 있는 값이 같더라도 서로 다른 인스턴스이다.
  • == 연산자를 사용하여 값을 비교할 때 주의해야 한다.
    • 위에서 말했듯이 인스턴스는 내부적인 값이 같더라도 서로 다른 존재이다.
    • 참조형 객체끼리 == 연산자를 사용하면 둘의 참조값을 비교하는 꼴이기 때문에 같은 인스턴스를 참조하지 않는 이상 무조건 false이다.
    • 값이 ‘동등한지’ 알고 싶다면 Object의 equals()를 사용해야 한다.





과제

  • 본인이 알고있는 내용만으로 기본형 타입과 참조형 타입에 대해 각각 설명해 보세요.





String

  • 자바에서 문자열을 다루기 위한 클래스이다.
  • 스트링은 다음과 같은 특징들이 있다.
    • 참조형 타입이지만, 문자열 리터럴로 초기화가 가능하다.
    • + 연산자를 사용할 수 있다.
    • 불변 클래스이다. (Immutable Class)
    • 문자열 리터럴은 상수 풀(Constant Pool)에 저장된다.
      • 상수 풀을 사용해 문자열 리터럴을 캐싱한다.





String의 초기화

  • 보통의 참조형 타입들은 new 연산자를 사용해 인스턴스를 할당한다.
  • 그러나 String은 사용상 편의를 위해 리터럴을 사용해 값을 할당할 수 있다.
1
2
3
4
5
MyClass myClass = new MyClass();    // 일반적인 참조형 타입의 인스턴스 할당

String s1 = "abc";                  // 리터럴을 직접 할당
String s2 = new String("abc");      // new 연산자를 사용한 인스턴스 생성
String s3 = String.valueOf("abc");  // String의 valueOf() 스태틱 메서드를 사용한 할당
  • 일반적으로 참조형 타입의 초기화는 위의 MyClass처럼 new 연산자를 사용해 인스턴스를 생성해 할당하는 방식이다.
  • String은 문자열 리터럴을 줘서 간단하게 초기화 할 수 있다.
    • 물론 String도 위의 s2처럼 new 연산자를 사용해서 초기화 할 수도 있다.
    • s3처럼 String의 valueOf() 스태틱 메서드를 사용할 수도 있다.
    • 그 외에도 몇 가지 방법이 있다.



String의 더하기 연산자

  • 기본형 타입에서 사용하듯이 String이나 문자열 리터럴에도 더하기 연산자가 사용 가능하다.
1
2
3
4
String s1 = "abc";
String s2 = "def";

System.out.println(s1 + s2);    // abcdef
  • 매우 편리해 보이지만, 문자열 더하기 연산은 성능에 부정적인 영향을 끼칠 수 있다.
    • 그 이유는 String이 불변 객체이기 때문인데, 정확한 이유는 뒤에서 알아보도록 한다.



String은 불변 객체

  • 불변이란 변하지 않는 것을 의미한다.
    • 즉, String은 한 번 생성하면 그 값을 변경할 수 없다.
  • 왜 굳이 불변하도록 만들었나요?
    • 보안성 향상
      • 어떤 객체를 다른 객체가 참조하고 변경해도 원래 객체에는 값의 변경이 일어나지 않음.
    • Thread-Safe
      • 상태 변경이 불가능하기 때문에, 여러 스레드에서 동시에 접근해도 안전하다.


  • 아닌데요?
  • 내가 해보니까 바뀌던데요??
  • 더하기 연산자는 그럼 뭔데요???
1
2
3
4
5
6
7
8
9
String s = "abc";

s = "abcd";                 // s가 갖고 있던 abc란 문자열이 abcd로 변한 것이 아닌,
                            // abc란 문자열 대신 abcd란 문자열을 생성해 s에게 할당한 것이다.

s = s + "e";                // abcde를 생성해 할당
s = s + "f";                // abcdef를 생성해 할당
s = s + "g";                // abcdefg를 생성해 할당
s = s + "h";                // abcdefgh를 생성해 할당
  • 모두 기존의 문자열 값을 변경하는 것이 아닌, 새로운 문자열을 만들어내는 것이다.
    • String에 새로운 문자열을 할당하는 행위도
    • 더하기 연산자를 이용해 새로운 문자열을 만드는 행위도
  • 이러면 메모리에 여러 개의 문자열이 생성되니까 성능적으로 부담되겠지?
    • 엥? 참조형은 원래 새로 생성할 때마다 메모리에 쌓이는 거 아니었어?



String Constant Pool

  • 자바에서 사용된 문자열 리터럴은 기본적으로 Constant Pool에 캐싱된다.
    • 이를 통해 문자열 리터럴을 재사용 할 수 있다.
    • 재사용을 통해 메모리 절약을 할 수 있다.
  • 문자열 리터럴 생성 방식은 다음과 같다.
    • 해당 문자열 리터럴이 상수 풀에 존재하는지 확인한다.
    • 있다면, 그걸 그대로 참조한다.
    • 없다면, 생성하여 상수 풀에 담는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
String s = "abc";
String s1 = "abc";
String s2 = new String("abc");
String s3 = String.valueOf("abc");
String s4 = s;

sout(s == s1);              // true
sout(s == s2);              // false
sout(s == s3);              // true
sout(s == s4);              // true

s4 = "abcd";

sout(s == s4);              // false
  • 위의 예시를 보면 s와 s1이 == 같냐고 물었을 때 true가 나왔다.
    • 엥 참조형인데 왜 같은거야?
    • 자바에서 문자열 리터럴은 기본적으로 Constant Pool에 캐싱하고 재사용하기 때문이다.
  • 그러나 s와 s2는 서로 다르다고 나왔다.
    • 왜? Constant Pool에서 똑같은걸 참조한다며?
    • new 연산자를 사용하면 기존의 것을 참조하지 않고 새로 생성하기 때문이다.
    • s3는 s와 같다는걸 보니, String.valueOf()는 Constant Pool에서 참조하는 것을 알 수 있다.
  • s4는 s를 그대로 참조하였기 때문에 당연히 true가 나온다.
    • 그러나 s4의 값을 변경하고 다시 비교해보면 서로 다르다고 나온다.
    • 왜?? s의 참조값을 그대로 가져왔는데 s랑 다를 수가 있나?
    • String은 불변 객체이기 때문이다.
      • s4에 다른 값을 할당해준 순간에 “abcd”라는 리터럴이 Constant Pool에 캐싱되고,
      • s와 s4는 서로 다른 값을 참조하고 있는 모양이다.



1
2
3
4
5
String s = "";

for(int i = 0 ; i < 100 ; i++) {
    s = s + i;
}
  • 만약 더하기 연산을 해서 새로운 문자열을 계속 생성한다면?
    • Constant Pool에 수많은 문자열이 캐싱될 것이고,
    • 사용되지 않는 문자열이 잔뜩 쌓인 것은 메모리 낭비이고,
    • 사용되지 않는 리터럴을 위해 가비지 컬렉터가 작동할 것이고,
    • 이 모든 것이 성능에 영향을 미칠 것이다.





과제

  • String, StringBuilder, StringBuffer 세 가지를 알아보고 설명하세요.
    • Immutable
    • Mutable
    • Thread-Safe
  • JDK8 에서 개선된 문자열 더하기 연산의 처리 방식을 알아보고 설명하세요.
    • StringBuilder
  • JDK9 에서 문자열 처리 속도를 개선하기 위해 사용된 방식을 알아보고 설명하세요.
    • Compact Strings
    • Compressed Strings





배열

  • 배열은 같은 타입의 변수를 여러 개 주주죽 늘여놓은 자료 구조이다.
    • 배열은 처음 생성 시점에 길이를 정해줘야 한다.
    • 메모리 상에서도 주주주죽 순서대로 이어져 있다.
    • 인덱스를 통해 각각의 값에 접근할 수 있다.
  • 배열은 기본적으로 참조형 타입이다.
    • 배열은 기본형 타입과 참조형 타입 둘 다 사용해 만들 수 있는데,
    • 기본형 타입의 배열도 참조형 타입이다.
  • 배열 변수는 배열의 시작점에 대한 참조값을 갖고 있다.
    • 인덱스를 통해 요소에 대한 메모리 주소값을 계산할 수 있다.
    • 실제 배열도 Stack 영역에 적재되어 있으며, 순서대로 주우욱 연속되게 메모리를 할당 받는다.
      • 기본형 타입이라면 각각 요소들이 값을 담고 있을 것이고,
      • 참조형 타입이라면 각각의 요소들이 힙 영역의 참조값을 갖고 있을 것이다.



배열의 생성

1
2
3
4
5
6
7
int[] intArray = new int[3];                                // 3개의 길이를 할당받은 int형 배열
String[] stringArray = new String[] {"a", "b", "c"};        // 값을 직접 할당받은 String 배열
char[] charArray = {'a', 'b', 'c'};                         // 배열 리터럴을 할당받은 char 배열

int[][] ints2d1 = new int[3][4];                            // 2차원 배열 3 * 4
int[][] ints2d2 = new int[2][2] {new int[2], new int[2]};   // 2차원 배열 2 * 2
int[][] ints2d3 = {{1,2,3}, {4,5,6}, {7,8,9}};              // 2차원 배열 3 * 3
  • 가장 간단한 방법으론 배열 리터럴을 할당하는 방법이다.
  • 그 다음은 new 연산자를 사용할 수 있는데,
    • 뒤에 배열 리터럴을 달아주지 않으면 길게 자리만 잡아두고 실제 값은 각 타입의 기본값이 들어간 배열이 된다.
    • 뒤에 배열 리터럴을 달아줬다면,
      • 배열 리터럴의 길이에 맞게 메모리 할당을 받고,
      • 각각의 요소가 할당된 값으로 채워진 상태의 배열이 된다.
  • 맨 아래처럼 다차원 배열을 만들 수도 있다.
    • 예시는 2차원 배열이지만 3차원, 4차원 배열을 만들 수도 있다.
    • 다차원 배열 역시 주루룩 이어진 모양으로 메모리를 할당 받는다.
  • 다차원 배열 역시 여러 가지 방식으로 생성할 수 있다.



배열 요소의 접근

  • 배열 요소에 접근은 인덱스를 사용해 할 수 있다.
  • 배열의 인덱스는 0부터 시작한다.
    • 0부터 시작하기 때문에 마지막 인덱스는 (배열의 길이 - 1)이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8};   // 길이 8의 배열

sout(arr.length);                       // 8
                                        // length를 사용해 배열의 길이를 알 수 있다.

sout(arr[0]);                           // 1
sout(arr[1]);                           // 2
sout(arr[2]);                           // 3
sout(arr[3]);                           // 4
sout(arr[4]);                           // 5
sout(arr[5]);                           // 6
sout(arr[6]);                           // 7
sout(arr[7]);                           // 8

arr[0] = 100;                           // 이런식으로 배열 요소의 값을 변경할 수도 있다.





과제

1
2
3
4
5
3, 4, 5, 6,
5, 5, 4, 1,
2, 3, 4, 7,
4, 1, 0, 9,
6, 10, 6, 8
  • 위의 모양과 같은 배열을 만들어 보세요.
    • 모든 값의 합과 평균을 구하고 출력해 보세요.
    • 배열에서 최대값과 최소값을 출력해 보세요.
    • 최대값과 최소값의 자리를 서로 바꿔 보세요.
  • 2 * 13 모양의 빈 문자형 배열을 만들어 보세요.
    • 메모리에 배치된 순서에 따라 알파벳 ‘a’부터 ‘z’까지 순서대로 할당해 보세요.





자바의 값 전달

  • 메서드 호출 시 값 전달 방식에는 세 가지 정도가 있다.
    • Call By Value
    • Call By Reference
    • Call By Name
  • 자바는 Call By Value 방식을 사용해 값을 전달한다.
    • 여기서 말하는 값의 전달이란, 메소드 호출 시 인자로 넘기는 값에 대한 것을 말한다.



1
2
3
4
5
6
7
8
9
10
11
12
13
class Main {
    public static void main(String[] args) {
        int a = 100;

        sout(a);            // 100
        add10(a);
        sout(a);            // 100
    }

    private static void add10(int num) {
        num = num + 10;
    }
}
  • 위의 예시를 보면 add10 메서드의 인자로 a를 넘겨 호출했지만
    • a의 값이 110이 아닌 100으로 변하지 않은 것을 볼 수 있다.
  • 자바는 기본적으로 Call By Value 방식을 사용한다고 했다.
    • Call By Value란 인자의 값만을 복사하여 넘기는 방식이다.
    • 따라서 100이란 정수값을 보낸 것이지 a 그 자체의 메모리 주소를 넘긴 것이 아니다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass {
    int num;

    // constructor, toString(), getter, setter
}

class Main {
    public static void main(String[] args) {
        MyClass a = new MyClass(100);

        sout(a);            // 100
        add10(a);
        sout(a);            // 110
    }

    private static void add10(MyClass o) {
        o.setNum(o.getNum() + 10);
    }
}
  • 엥? 값만 복사해서 보냈는데 왜 이번엔 값이 바꼈지?
    • 참조형 타입의 경우 그 참조값을 보내기 때문이다.
    • 참조값을 보내기 때문에 파라미터를 사용해 값을 변경하면, 해당 인스턴스의 값이 변경된다.



과제

  • 기본형 타입과 참조형 타입을 메소드의 인자로 넘겨보고, 값을 변경해본 뒤에 출력해보세요.





This post is licensed under CC BY 4.0 by the author.