자바는 항상 Call-By-Value(Pass-By-Value)로만 동작한다고 한다.

분명 자바를 처음 배울때는 Call-By-Value와 Reference-By-Value를 모두 사용한다고 배웠던거 같은데 어떻게 된건지 알아보겠다.

(Call-By-Value와 Pass-By-Value에서 Call과 Value는 거의 동일하게 사용한다고 하니 Call-By-Value로 통일하여 작성하기로 한다.)


일단 처음 Call-By-Value와 Reference-By-Value를 학습할 때 배우는 swap 예제를 살펴본다.

 

아래는 Call-By-Value의 예시로 작성된 코드이다.

a,b 변수를 swap 함수의 인자로 건네줘서 값을 바꿨지만 a, b 변수의 값은 그대로이다. 즉, a, b의 값만 복사(Call-By-Value)하여 x, y 변수가 지역변수로 새로 생성됐기 때문에 x와 y를 스왑하던, 값을 바꾸던 a, b 변수에는 영향이 없는 것이다. 

public class CallByTest {
    @Test
    public void callbyvalue(){
        int a = 10;
        int b = 20;

        System.out.println("swap() 호출 전 : a = " + a + ", b = " + b); // a = 10, b = 20
        swap(a, b);
        System.out.println("swap() 호출 후 : a = " + a + ", b = " + b); // a = 10, b = 20
    }

    public void swap(int x, int y){
        int temp = x;
        x = y;
        y = temp;
    }
}

 

그럼 Call-By-Reference 예제로 배웠던 swap 예제코드를 살펴본다.

swap 호출전후로 인자로 넘겼던 객체의 상태값이 바꼈으므로 Call-By-Reference의 예시로 알았던 코드이다.

public class CallByTest {
    @Test
    public void CallByReference(){
        Game a = new Game(10);
        Game b = new Game(20);

        System.out.println("swap() 호출 전 : a.star = " + a.star + ", b.star = " + b.star); // a.star = 10, b.star = 20
        swap(a, b);
        System.out.println("swap() 호출 후 : a.star = " + a.star + ", b.star = " + b.star); // a.star = 20, b.star = 10
    }

    public void swap(Game x, Game y){
        int temp = x.star;
        x.star = y.star;
        y.star = temp;
    }

    class Game {
        int star;
        public Game(int star) {
            this.star = star;
        }
    }
}

 

하지만, 진정한 Call-By-Reference는 참조하는 객체의 상태값을 변경하는게 아니라 실제 x,y 변수가 참조하는 객체를 변경하여 외부의 a 변수가 참조하는 객체를 바꿀수 있는 경우를 말한다.

자바는 Call-By-Value만 지원하므로 아래 코드에서는 a가 참조하는 객체가 realCallByReference() 함수에서 변경한 객체(star 값이 20인)로 변경되지 않고 여전히 10이 출력되는 걸 볼 수있다.

public class CallByTest {
    @Test
    public void test(){
        Game a = new Game(10);

        realCallByReference(a);

        System.out.println("a가 참조하는 객체는 바뀌지 않음 : " + a.star); // 10
    }

    public void realCallByReference(Game x){
        x = new Game(20);
    }
}

1. 함수 내에서 지역변수 x가 새로 생성되며 함수인자인 a와 같은 객체를 참조함.

    > Call-By-Value에 의해 a가 참조하는 객체에 대한 위치값만 전달해줬음.

2. realCallByReference 함수내에서 x가 참조하는 객체를 신규로 생성한 객체로 변경함.

    > a와 같은 객체를 참조하다가 새로운 객체로 변경되었으며 a에는 전혀 영향없음.

3. realCallByReference 함수 종료후 출력결과를 보면 a는 그대로 기존 객체를 참조하고 있음.

 


정리해보면

자바는 Call-By-Value만을 지원한다. 

처음 학습시 Call-By-Reference의 예제로 알고있던 swap코드는 참조하는 객체의 상태값을 바꾸는 것이지 참조하는 객체 자체를 바꾸는 것이 아니므로 진정한 Call-By-Reference의 예제는 아니다.

 

참고 StackOverflow

stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value

 

Is Java "pass-by-reference" or "pass-by-value"?

I always thought Java uses pass-by-reference. However, I've seen a couple of blog posts (for example, this blog) that claim that it isn't (the blog post says that Java uses pass-by-value). I don't ...

stackoverflow.com

 


 

 

 

자바에서 코드를 작성하며 String을 비교하는 경우는 매우 많습니다.

보통 int나 char 타입의 경우 '==' 비교를 많이 사용합니다만, String의 경우 대부분 == 비교를 사용하면 안됩니다.

 

== 과 .equals() 의 차이

  • == 비교는 참조하는 객체가 동일한지를 비교합니다. 
  • .equals() 비교는 값 자체가 동일한지를 비교합니다.

String 비교시 대부분의 경우는 값 자체가 동일한지를 비교하므로 == 비교가 아닌 .equals() 비교를 해야합니다.

 

간단한 테스트 코드를 보겠습니다.

// 값에 대한 비교이므로 동일
new String("test").equals("test") // true 

// new String()으로 새로운 객체를 생성하여 같은 객체끼리 동일한지 비교
// 서로 참조하는 객체가 다르므로 false
new String("test") == "test" // false 
new String("test") == new String("test") // false 

// "test" 스트링 리터럴은 컴파일러에 의해 interned 되므로
// 같은 객체끼리 비교하는 것이 됨
"test" == "test" // true 

// 스트링 리터럴은 컴파일러에 의해 조립되어("te" + "st" = "test") interend 되므로 
// 같은 객체끼리 비교하는 것이 됨
"test" == "te" + "st" // true

// Objects.equals()로 null을 포함하여 값 비교가 가능함
Objects.equals("test", new String("test")) // true
Objects.equals(null, "test") // false
Objects.equals(null, null) // true

string의 값 비교를 위해서는 .equals()나 Object.equals()를 사용할 수도 있고,

대소문자를 신경쓰지 않고 비교하는 String.equalsIgnoreCase() 같이 추가기능이 있는 비교메서드를 사용할 수 있습니다.

 

String intern

위 내용 중 string intern에 대해 추가설명을 드리자면,

자바는 JVM에 문자열 풀(pool)을 생성 후, 새로운 문자열이 사용되면 그 풀에 등록하여 놓습니다.

만일 그 문자열이 다시 사용되는 경우 새로 생성하는 것이 아닌 문자열 풀에서 꺼내 사용하는 것이지요. 왜냐하면 String 객체는 불변이기 때문에 동일한 객체를 계속 생성하는 것이 아니라 생성 후 공유하는 특징을 가지기 때문입니다.

 

String 객체에 대해 intern된 값을 가져올 수도 있는데요 이를위해 String 클래스의 intern()을 사용할 수 있습니다.

아래 테스트 코드를 보시죠.

 

str1과 str2는 String intern에 의해 JVM 문자열 풀의 같은 String 객체를 참조합니다. 

str1.intern() 은 JVM 문자열 풀에서 str1과 값이 동일한 String 객체를 가져옵니다. 만일 풀에 저장되어 있지 않다면 신규로 등록후 가져오게 되지요. 

str1.intern() 과 str2.intern()은 JVM 문자열 풀의 같은 객체를 가져오므로 동일합니다. 

 

str3과 str4는 new String()으로 생성된 각기 다른 String 객체를 참조합니다.

하지만 str1 ~ str4 의 intern() 호출은 결국 다 같은 JVM 문자열 풀의 같은 객체를 반환합니다.

public class StringComparison {
    @Test
    public void StringIntern(){
        String str1 = "하이";
        String str2 = "하이";

        System.out.println(str1 == str2); // true
        System.out.println(str1.equals(str2)); // true
        System.out.println(str1.intern() == str2.intern()); // true

        String str3 = new String("하이");
        String str4 = new String("하이");

        System.out.println(str3 == str4); // false
        System.out.println(str3.equals(str4)); // true
        System.out.println(str3.intern() == str4.intern()); // true
        System.out.println(str1.intern() == str4.intern()); // true
    }
}

 


 

 

자바 변수에는 생성한 오브젝트(객체)를 가리키는 참조형 타입의 변수가 있습니다.

아래 Integer 타입의 num변수를 선언 후 할당하는 경우입니다.

Integer num; // Integer 타입의 변수 num을 선언, 초기화 하지 않음
num = new Integer(10); // new 키워드를 사용해 Integer 객체를 생성후 num에게 참조시킴

첫째 줄은 Integer 타입의 num 변수를 선언했습니다. 참조형 변수를 선언만 했으므로 'null'로 초기화됩니다.

null 이란 참조형 변수가 아무것도 가리키지 않는 상태라는 의미입니다. 

두번째 줄에서 num에 세로운 Integer 객체를 생성하여 참조시켰습니다. 이로써 num은 null 에서 새로운 Integer 객체를 가리키도록(Integer 객체의 주소값이 할당) 변경되었습니다.

 

NullPointerException(아래부터는 줄여서 NPE라고 하겠습니다.)은 첫 번째 줄처럼 선언만 하고 아무것도 참조하지 않는 null 상태인 변수에서 내용을 참조하려 할 때 발생합니다.

 

내용을 참조한다는 것은 점(.)으로 객체 변수에 접근하는 것을 말합니다.

아래 코드는 null인 변수 k에 점(.)으로 indexOf() 함수를 호출하려 할 때 NPE가 발생합니다.

아무것도 가리키지 않는 상태인데 내부 내용에 접근하려 하니 오류가 나는 것이지요.

public void NPE_method1(){
    String k = null;
    k.indexOf(1);
}

 

컴파일러가 해당 참조변수는 아직 초기화 되지 않았다는 메시지를 표출하기도 하지만 아래와 같은 코드에서는 null이 넘어올지 초기화된 참조변수가 넘어올지 알 수 없습니다. 만일 doSomething(null); 로 호출하게 되면 NPE 오류가 나게됩니다. 

public void doSomething(SomeObject obj) {
   obj.myMethod();
}

 

이런 경우에는 어떻게 하여야 할까요?

제일 좋은 방법은 메서드 시작전에 obj가 초기화 되었는지 검증하는 것입니다.

doSomething 함수의 내부로직이 obj가 초기화 되었다는 것을 전제로 작성되었다면, 첫 줄에 null 값에 대한 검증 후 만일 null이 넘어온 경우 처리할 수 없다는 명시적인 메시지와 함께 종료하는 것이죠.

import java.util.Objects;

public class Test {
    public static void main(String[] args) {
        new Test().doSomething(null);
    }

    public void doSomething(Object obj){
        Objects.requireNonNull(obj, "obj는 null값이 아니여야 합니다.");
    }
}
Exception in thread "main" java.lang.NullPointerException: obj는 null값이 아니여야 합니다.

 

doSomething 내에서 obj에 null값을 허용하며 분기하여 처리할 수도 있습니다. 다음 코드를 참조해주세요.

public void doSomething(Object obj){
    if(obj == null){
        // obj가 null일 때의 로직
    }else {
        // obj를 이용한 로직
    }
}

 

 


 

 

프로그래밍 언어에는 크게 두가지 타입이 있습니다.

1. 정적인 언어 : 컴파일 때 이미 변수에 대한 데이터 타입이 확정되어 다른 데이터 타입을 할당할 수 없는 언어입니다.

예를들어 C, C++, JAVA와 같은 언어가 여기에 속합니다.

2. 동적인 언어 : 여기 언어들은 변수에 다른 데이터 타입을 할당할 수 있습니다. Ruby, Python 등의 언어가 있죠.

 

자바는 정적인 언어이기 때문에 변수를 사용하려면 데이터 타입을 꼭 지정해줘야 합니다. 자바에서 사용되는 타입은 다음과 같습니다.

https://www.geeksforgeeks.org/data-types-in-java/?ref=lbp

자바의 데이터 타입은 크게 두 가지 Primitive 와 Non-Primitive 타입으로 나눌 수 있습니다. 

- Primitive Data Type : boolean, char, int, short, byte, long, float, double

- Non-Primitive Type : String, Array 등


Primitive Data Type

Primitive 데이터는 오직 한 값만 가지며 다른 특수한 기능이 없습니다. 그 값 자체라고 생각하면 됩니다.

boolean, byte, short, int, long, float, double 이렇게 8가지 데이터 타입이 있으며 하나씩 살펴보겠습니다.

 

먼저 각 자료형마다 표현할 수 있는 범위가 정해져 있습니다. boolean은 1bit, byte는 8bit 처럼 말이죠.

여기서 1bit는 0 또는 1로 표현되는 한자리 값입니다. 즉, 2^1 = 2 가지 값을 표현할 수 있습니다.

마찬가지로 1byte(= 8bit)는 2진수로 00000000 ~ 11111111 사이의 값을 가지며 2^8 = 256 가지 값을 표현합니다.

데이터의 표현 범위는 여기까지 하고 한 자료형씩 보겠습니다.

 

1. boolean : 1bit의 값으로 참/거짓(true/false)만을 나타내는 자료형입니다. 

True는 1, False는 0의 값으로 표현합니다. 두 가지 값만 필요하니 1bit로 모든 값을 표현할 수있습니다.

class Test {
    public static void main(String args[])
    {
        boolean b = true;
        if (b == true)
            System.out.println("Hi boolean"); // Hi boolean 출력
    }
}

 

 

2. byte : 8 bit(1 byte)의 값으로 -128 ~ 127 범위의 값을 표현할 수 있습니다.

정수 자료형 중 가장 작은 범위를 지니고 있습니다. 8bit 이므로 2^8=256 개의 값인 -128~127을 표현합니다. 범위안에 0이 있어 128까지가 아님에 유의합니다.  

class Test {
    public static void main(String args[])
    {
        byte a = 126;
 
        System.out.println(a); // 126 출력
 
        a++;
        System.out.println(a); // 127 출력
 
        // 표현 가능한 범위(-128 ~ 127)를 넘었으므로 -128로 돌아갑니다.
        a++;
        System.out.println(a); // -128 출력
 
        // -128에서 1이 증가합니다.
        a++;
        System.out.println(a); // -127 출력
    }
}

 

3. short : 16 bit(2 byte)의 값으로 -32,768 ~ 32,767 범위의 값을 표현합니다.

2^16= 65,536개의 값을 표현하며, byte와 마찬가지로 중간에 0을 포함하여 양수의 갯수가 하나 더 적습니다. 

 

4. int : 32 bit(4 byte)의 값으로 -2,147,483,648 to 2,147,483,647 범위의 값을 표현합니다.

정수형 중 가장 많이 사용되는 자료형입니다. 범위를 봤을 때 대부분의 변수사용이 이 범위로 커버가 가능하므로 가장 많이 사용됩니다. 만일 사용하는 변수가 21억 이상의 값이 할당될 가능성이 있다면 더 큰 자료형인 long을 사용해야 합니다.

 

5. long : 64 bit(8 byte)의 값으로 -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 범위의 값을 표현합니다.

2^64개의 변수를 표현합니다. 값이 엄청나게 커진걸 볼 수 있습니다. 변수를 선언하게 되면 그 공간만큼 메모리가 할당되므로 사용하는 변수에 맞는 자료형을 선택해야 합니다.

물론 요즘 사용하는 하드웨어들이 메모리가 빵빵하기 때문에 자료형 몇개로 부족해지지는 않겠지만 습관을 들이는게 좋습니다.

 

6. float : 32 bit(4 byte)의 값으로 실수를 표현합니다.

위 5개 자료형과는 다르게 실수형 데이터 타입입니다. 실수형이란 소수점을 가진 값으로 이 소수점을 표현하기 위해 가진 bit중 일부를 사용합니다. 소수임을 알리기 위해 값에 f를 붙혀 할당합니다.

public class Main {
    public static void main(String[] args) {
        float number = 0.9f;
        System.out.println(number); // 0.9 출력
    }
}

 

7. double : 64 bit(8 byte)의 값으로 실수를 표현합니다.

float와 마찬가지로 소수점을 표현하기 위해 bit 일부를 사용하지만 f를 붙혀주지 않아도 됩니다.

public class Main {
    public static void main(String[] args) {
        double number = 0.09;
        System.out.println(number); // 0.09 출력
    }
}

 

8. char : 16 bit(2 byte)의 유니코드 문자를 표현합니다.

표현범위로는 '\u0000' (0) to '\uffff' (65535) 입니다. '\u' 는 유니코드라는 의미이고 16진수로 '0000'(0) ~ 'ffff'(65535) 까지 값이 할당된다는 의미입니다.  

자바에서는 문자를 표현할 때 2바이트를 사용하고 있지만 C, C++은 1바이트를 사용합니다. C, C++은 아스키(ASCII)로 문자를 표현하여 일부 문자가 표현이 불가능 합니다. 하지만 자바는 문자(Character)에 Unicode를 사용하므로 대부분 나라의 언어를 포함하여 표현이 가능합니다.

 

아래 테스트를 보면 char형으로 문자 한개를 표현할 수 있고, int 자료형이 4byte로 더 크므로 할당이 가능한 걸 볼 수 있습니다. 

public class Test {
    public static void main(String[] args) {
        char a = 'a';
        char b = 'b';
        System.out.println(a); // a 
        System.out.println(b); // b

        int intFromChar1 = a;
        int intFromChar2 = b;
        System.out.println(intFromChar1); // 97
        System.out.println(intFromChar2); // 98
        
        char x = 'ㄱ';
        char y = 'ㄲ';
        System.out.println(x); // ㄱ
        System.out.println(y); // ㄲ

        int intFromChar3 = x;
        int intFromChar4 = y;
        System.out.println(intFromChar3); // 12593
        System.out.println(intFromChar4); // 12594
    }
}

 

 


 

 

 

람다표현에서는 바깥쪽 스코프의 변수(static 변수, 지역 변수, 인스턴스 변수)를 참조 할 수 있으며 이를 capturing lambda 라고 한다. 

그 중 지역 변수를 참조하는 경우에만 컴파일러는 해당 변수가 final 또는 effectively final 인지 체크하여 아니라면 컴파일 오류를 표출하게 되는데 이에 대해서 알아보고자 한다.

 

아래 메서드는 람다를 리턴한다. 하지만 람다표현식에서 함수의 파라미터(지역변수)를 사용하고 있으며 이를 증가시키고 있으므로 컴파일 오류를 표출한다.

Supplier<Integer> incrementer(int start) {
    return () -> start++; // local variables referenced from a lambda expression must be final or effectively final
}

위 코드의 start 지역변수는 final로 선언되지 않았으며 값을 변경했으므로 effectively final이 아니다.

 > effective final : final은 붙지 않았지만 할당 후 값을 변경하지 않아 final과 같은 효과

 

컴파일 되지 않는 기본적인 이유는 람다는 참조하는 지역변수에 대한 복사본을 가지고 동작하기 때문이다.


람다는 왜 참조하는 외부스코프 지역변수에 대해 복사본을 가지고 동작할까?

위 코드에서 incrementer 함수는 실행되면서 start를 증가시키지 않고 람다를 리턴할 뿐이다.

incrementer 함수의 실행이 끝난 후의 다른 시점에서 리턴한 람다표현식이 실행되어 start 지역변수가 증가할 수 있으며, 그 시점이 오기전에 GC에 의해서 start 지역변수가 정리될 수도 있다.(함수의 실행이 끝나면 지역변수는 GC에 의해 정리대상이 된다.) 그러므로 실행 시점에 지역변수가 사라질 것을 방지하지 위해서 람다는 복사본을 생성하여 동작하게 된다.(또한 지역변수는 Stack영역에 저장되기 때문에 공유자원이 아니므로 다른 thread에서 람다가 실행된다면 변수에 접근할 수 없다.)

 

복사본을 가지고 미래에 동작할 예정인데 복사본에서 값이 바뀌어 버리면(= final or effective final이 아니면, = 최종상태값과 복사본값이 다르면) 오류를 발생할 여지가 생기므로 컴파일 단계에서 막는 것이다.


static 변수와 인스턴스 변수의 경우에는?

private int start = 0;
Supplier<Integer> incrementer() {
    return () -> start++;
}

지역변수가 아닌 인스턴스 변수는 컴파일 에러도 나지 않으며 정상 동작한다. 인스턴스 변수는 Heap영역에 저장되고 static 변수는 Method영역에 저장되며 두 영역모두 공유자원이므로 다른 thread에서 접근이 가능하다. 자유롭게 접근이 가능하므로 실행되는 시점에 최종상태값을 읽어 증가시킬 수 있다. 그러니 변수에 대한 제약조건이 없는 것이다.

 

이에대한 간단한 예제 코드이다.

변수 holder가 참조하는 배열객체는 heap 영역에 저장되어 자유롭게 참조가 가능하다. 그러므로 sums.sum()로 람다가 실행되는 시점에 holder[0]의 최종상태값을 읽어들여 계산하므로 6이 리턴된다.

public int workaroundSingleThread() {
    int[] holder = new int[] { 2 };
    IntStream sums = IntStream
      .of(1, 2, 3)
      .map(val -> val + holder[0]);

    holder[0] = 0;

    return sums.sum();
}

 

 


읽어주셔서 감사합니다. 도움이 되셨다면 광고 클릭 부탁드립니다.

모두 힘내세요! : )

 

 

 

'개발 > JAVA' 카테고리의 다른 글

NullpointerException 과 해결법  (0) 2020.12.09
[Java 기본] Java Primitive 데이터 타입  (0) 2020.12.09
equals, hashcode  (0) 2020.12.07
JVM, JRE, JDK 간단 개념  (0) 2020.12.07
ExecutorService 사용법  (0) 2020.12.01

롬복의 @EqualsAndHashCode 애노테이션을 보다가 문득 equals()와 hashCode() 메서드에 대해서 잘 모른다는 사실을 알고 한번 찾아보고 정리한다.

Object 클래스에 정의된 equals(Object obj) 메서드를 보면 ‘==’ 비교결과를 반환한다.

class Object{
    public boolean equals(Object obj) {
        return this == obj;
    }    
}

그럼 ‘==’ 비교는 무엇을 뜻하는 것일까? 참조형 변수끼리 ‘==’ 비교를 한다면 ‘동일’ 객체를 참조하고 있는지 확인하는 것이다.
(논리적으로 같은 객체가 아니다.)

class Game{
    int star;

    public Game(int star) {
        this.star = star;
    }
}
Game game1 = new Game(1);
Game game2 = game1;

// '동일' 객체를 참조하므로 game1 == game2
Game game1 = new Game(1);
Game game2 = new Game(1);

// 같은 프로퍼티를 가지므로 논리적으로 동일한 객체지만 
// '동일' 객체를 참조하지 않으므로 game1 != game2

하지만 개발을 하다보면 ‘동일’한 객체인지 체크하는 것보다 논리적으로 ‘동등’한지(모든 프로퍼티가 같은 객체인지) 체크할 상황이 더 많다.
이런 경우에는 보통 클래스에 Object.equals(Object obj)를 재정의한다.

class GameWithEquals{
    int star;

    public GameWithEquals(int star) {
        this.star = star;
    }

    @Override
    public boolean equals(Object game) {
        if(!(game instanceof GameWithEquals)) return false;
        return ((GameWithEquals) game).star == this.star;
    }
}
@Test
public void 재정의한_equals_동등비교(){
    GameWithEquals game1 = new GameWithEquals(1);
    GameWithEquals game2 = new GameWithEquals(1);

    assertTrue(game1.equals(game2));
}

단순히 ‘==’ 비교를 하던 Object.equals(Object obj)를 재정의 했고, 객체간의 동등비교가 필요하면 이 함수를 활용하면 되니 모든 문제가 해결되었다고 생각할 수 있지만 equals()와 짝으로 hashcode()메서드도 재정의 되어야 한다.

왜냐하면 컬렉션들(HashSet, HashMap, HashTable …)은 객체가 동등한지 비교할 때 equals()와 hashcode()를 둘다 체크하기 때문이다. 예를들어 equals()만 재정의한 경우에는 다음 테스트를 통과하지 못한다.

@Test
public void Map은_키값비교를_equals만으로_하지않는다(){
    HashMap<GameWithEquals, String> map = new HashMap<>();
    GameWithEquals game1 = new GameWithEquals(1);
    map.put(game1, "HI");

    GameWithEquals game2 = new GameWithEquals(1);
    assertTrue(game1.equals(game2)); // True
    assertEquals(map.get(game2), "HI"); // null반환
}

컬렉션은 키값 객체를 비교할 때 equals()와 hashcode()를 모두 체크하므로 재정의 하지 않은 hashcode()에 의해 테스트는 실패하게 된다.

 

Object.hashCode()는 native로 정의되어 있으며 실제 참조하는 객체에 대한 교유값이므로 ‘==’ 비교와 동일하게 다른객체를 참조한다면 서로 다른값이 반환된다. 논리적인 비교가 되기 위해서는 equals와 마찬가지로 가진 프로퍼티를 이용하여 hashCode를 반환하도록 오버라이드 한다.

 

class GameWithEqualsAndHashCode{
    int star;

    public GameWithEqualsAndHashCode(int star) {
        this.star = star;
    }

    @Override
    public boolean equals(Object game) {
        if(!(game instanceof GameWithEqualsAndHashCode)) return false;
        return ((GameWithEqualsAndHashCode) game).star == this.star;
    }

    @Override
    public int hashCode() {
        return Objects.hash(star);
    }
}
@Test
public void Map은_키값비교를_equals와_hashCode로_한다(){
    HashMap<GameWithEqualsAndHashCode, String> map = new HashMap<>();
    GameWithEqualsAndHashCode game1 = new GameWithEqualsAndHashCode(1);
    map.put(game1, "HI");

    GameWithEqualsAndHashCode game2 = new GameWithEqualsAndHashCode(1);
    assertTrue(game1.equals(game2)); // True
    assertEquals(map.get(game2), "HI"); // HI반환, True
}

정리하면,
객체간의 논리적인 비교를 위해서는 equals()와 hashCode()를 모두 재정의 해야한다.
재정의는 개발자가 작성해도 되지만 IDE의 자동완성이나 Lombok(@EqualsAndHashCode)의 도움으로 자동작성 할 수도 있다.

 

JVM

  • 컴파일된 바이트 코드(.class)파일을 OS에서 실행 가능한 코드로 변환하여 실행한다. 코드 변환을 위해서 인터프리터와 JIT 컴파일러가 동작한다.
  • JVM은 표준스팩이 존재하며 스팩에 따라 구현한 여러 구현체들이 있다.
  • OS에 종속적이기 때문에 윈도우용 JVM, 맥용 JVM 이런식으로 여러 종류가 있다.(밴더에 따라서도 구분됨)
  • JVM은 단독으로 배포되지 않고 JRE에 포함되어 배포된다.

JRE

  • Library를 포함하여 자바프로그램 실행에 필요한 것들을 모아둔 배포판이다.
  • JVM과 핵심 라이브러리, 프로퍼티, 리소스 파일 등을 가지도 있다.

JDK

  • JRE + 개발에 필요한 툴

JVM Architecture

JVM은 컴파일된 클래스파일(바이트 코드)을 OS 위에서 실행하며 실행을 위해 다음 3가지 파트로 나눠진다.

1. 클래스로더 시스템

Loading, Linking, Initialization 3가지 동작으로 클래스를 로드하고 정보를 저장한다.

  • Loading : 클래스 파일을 읽고 클래스 정보를 메소드 영역에 저장한다. 로딩이 완료된 클래스는 해당 클래스 타입의 Class 객체를 생성하여 힙 영역에 저장한다.
  • Linking : Verify, Prepare, Resolve 3가지 단계로 이루어져 있다.
  • Initialization : static 변수의 값을 할당하고 static 블록을 실행한다.

클래스파일(.class)을 로드하는 클래스로더(ClassLoader)는 계층형 구조를 가진다.

  • Bootstrap ClassLoader : $JAVA_HOME/jre/lib/rt.jar 의 클래스 파일들을 로드한다. JVM을 실행하기 위한 핵심 파일들이 로드된다.
  • Extension ClassLoader : $JAVA_HOME/jre/lib/ext 또는 java.ext.dirs 옵션에 정의된 디렉터리의 클래스 파일들을 로드한다.
  • Application ClassLoader : Classpath로 지정한 디렉터리의 클래스 파일을 로드한다. 보통 개발한 클래스 파일들이 여기서 로드된다.

로드한 클래스를 찾을 때는 Application ClassLoader를 시작으로 해서 못찾으면 상위 ClassLoader에게 찾는 클래스를 위임한다. 최상단의 Bootstrap ClassLoader에서도 클래스를 찾지 못하면 익숙한 ClassNotFoundException 이 발생한다.


2. 다양한 메모리

  • 메소드 영역

클래스 레벨의 정보들이 저장된다.(클래스 이름, 부모 클래스 이름, 메서드, 변수, static 변수 등의 정보)
JVM마다 오직 하나의 메소드 영역을 가지며 공유자원이다.
공유자원 : 다른 영역의 메모리에서도 참조가 가능하다.


  • 힙 영역

모든 객체(인스턴스)들의 정보가 힙에 저장된다.
JVM마다 하나의 힙 영역을 가지며 메소드 영역과 같이 공유자원이다. 메소드, 힙 영역은 공유자원으로 여러 쓰레드에서 접근하여 데이터를 가져갈 수 있다. (thread-safe 하지 않는다.)


  • 스택 영역

JVM은 쓰레드마다 하나의 런타임 스택을 생성해주며 이 스택들이 스택 영역에 저장된다.
쓰레드가 가진 스택은 메서드 콜들이 스택 프레임이라 부르는 블럭으로 쌓여있다.(오류가 발생하면 콘솔에 찍히는 그 형태)
공유자원이 아니므로 Thread-safe하며 Thread가 종료될 때 해당 스택은 JVM에 의해 제거된다.


  • PC 레지스터 영역

각 쓰레드마다 분리된 PC 레지스터를 가지며 현재 실행중인 스택 프레임을 가리키는 포인터가 저장된다. 쓰레드가 다음 실행 스택 프레임으로 넘어가면 PC 레지스터도 업데이트 된다.


  • 네이티브 메소드 스택 영역

각 쓰레드마다 분리된 네이티브 메소드 스택 영역을 가진다. 네이티브 메소드 정보가 저장된다.
네이티브 메소드 : 메소드 선언에 native 키워드가 붙어있고 구현은 C나 C++같이 자바가 아닌 다른 언어로 구현되어 있는 메소드


3. 실행 영역

인터프리터, JIT 컴파일러, Garbage Collector로 구성되어 있다.

  • 인터프리터 : 바이트코드는 인터프리터로 한줄씩 네이티브 코드로 바꿔가며 어플리케이션이 실행된다.
  • JIT 컴파일러 : 매번 라인을 새로 읽으며 실행하지 않고 반복되는 코드는 JIT 컴파일러가 모두 네이티브 코드로 바꿔둔다. (효율성을 위해서)
  • GC : 더이상 사용되지 않는(참조되지 않는) 객체들은 GC에 의해 정리된다.

 


읽어주셔서 감사합니다. 도움이 되셨다면 광고 클릭 부탁드립니다.

모두 힘내세요! : )

 

 

 

Thread를 구현할 때 Runnable 인터페이스를 구현하거나 Thread 클래스를 상속하여 구현한다. 구현한 Thread를 new MyThread().start() 와 같이 호출하여 직접적으로 실행할 수도 있지만 기본 JDK의 java.util.concurrent패키지의 ExcecutorService를 이용하여 실행할 수도 있다.


ExecutorService

ExecutorService는 재사용이 가능한 ThreadPool로 Executor 인터페이스를 확장하여 Thread의 라이프사이클을 제어한다.
Thread를 활용하여 다수의 작업(Task)들을 비동기로 수행한다는 것은 간단하지 않다. Thread의 라이프사이클(생성과 제거 등)이나 발생할 수 있는 여러가지 low level의 고려사항들이 존재하는데 이를 개발자가 신경쓰지 않도록 편리하게 추상화한 것이 ExecutorService이다.

ExecutorService에 Task(작업)를 지정해주면 가진 ThreadPool을 이용하여 Task를 실행한다. Task는 큐(Queue)로 관리되기 때문에 ThreadPool의 Thread 갯수보다 실행할 Task가 많은경우 미실행된 Task는 큐에 저장되어 실행을 마친 Thread가 생길 때까지 기다린다.

 


ExecutorService 초기화

ExecutorService를 초기화 하는 방법에는 2 가지 방법이 있다.

1. 직접 new 키워드를 사용

ExecutorService는 인터페이스이기 때문에 구현체인 ThreadPoolExecutor를 new키워드로 초기화한다. (필요에 따라 다른 구현체를 초기화해도 된다.)

아래의 초기화 코드에서 10개의 core thread, 10개의 max thread, 0 밀리세크의 keepAliveTime, 작업 큐로는 LinkedBlockingQueue가 초기화되었다. Task(작업)을 위한 Queue에는 Runnable과 Callable 인터페이스를 구현한 클래스를 받을 수 있는데 return값이 있냐(Callable) 없냐(Runnable)에 따라 선택하면 된다.

ExecutorService executorService = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());

2. Executors 클래스에서 제공하는 Factory method를 사용

제공되는 3가지의 factory method를 이용한 초기화이다. 메서드명에서 생성되는 ThreadPool의 성향을 유추할 수 있으며 실행하고자 하는 Task에 따라 선택하여 사용한다.

// 1. 10개 고정 사이즈의 ThreadPool 생성
ExecutorService executorService = Executors.newFixedThreadPool(10);

// 2. 1개 고정 사이즈의 ThreadPool 생성
ExecutorService executorService = Executors.newSingleThreadExecutor();

// 3. 유동적으로 증가하고 줄어드는 ThreadPool 생성
ExecutorService executorService = Executors.newCachedThreadPool();

new 키워드를 사용하는 것이 좀 더 세부적인 설정이 가능하지만 Executors를 사용하는 것이 더 간편하다. 대부분의 경우 간편한 설정으로 원하는 작업이 가능하다.


ExecutorService Task 할당

ExecutorService를 초기화 했다면 ThreadPool에 원하는 Task(작업)을 할당해야 한다. 일단 Task를 Callable / Runnable 인터페이스를 구현하여 생성하고, ExecutorService의 메서드를 호출하여 실행한다.

// Runnable 인터페이스로 Task 정의
Runnable runnableTask = () -> {
	try{
		System.out.println(Thread.currentThread().getName() + " start");
		TimeUnit.MILLISECONDS.sleep(500);
		System.out.println(Thread.currentThread().getName() + " end");
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
};

// Callable 인터페이스로 Task 정의
Callable callableTask = () -> {
	TimeUnit.MILLISECONDS.sleep(5000);
	return "Task's execution";
};

// 정의한 Task를 List에 추가
List<Callable> callableTasks = new ArrayList<>();
callableTasks.add(callableTask);
callableTasks.add(callableTask);
callableTasks.add(callableTask);

 

아래는 위에서 정의한 작업(Task)을 할당(실행)하기 위해서 제공되는 메서드들이다.

 

1. execute() : 리턴타입이 void로 Task의 실행결과나 Task의 상태(실행중 or 실행완료)를 알 수 없다.

executorService.execute(runnableTask);

 

2. submit() : Task를 할당하고 Future 타입의 결과값을 받는다. 결과가 리턴되어야 하므로 주로 Callable을 구현한 Task를 인자로 준다.

Future future = executorService.submit(callableTask);

 

3. invokeAny() : Task를 Collection에 넣어서 인자로 넘겨줄 수 있다. 실행에 성공한 Task 중 하나의 리턴값을 반환한다.

String result = executorService.invokeAny(callableTasks);

 

4. invokeAll() : Task를 Collection에 넣어서 인자로 넘겨줄 수 있다. 모든 Task의 리턴값을 List<Future<>> 타입으로 반환한다.

List<Future> futures = executorService.invokeAll(callableTasks);

ExcecutorService 종료

실행 명령한 Task가 모두 수행되어도 ExecutorService는 자동으로 종료되지 않는다. 앞으로 들어올 Task를 처리하기 위해 Thread는 wait 상태로 대기한다. 그러므로 종료를 위해서는 제공되는 shutdown() 이나 shutdownNow() API를 사용해야 한다.

 

1. executorService.shutdown()

실행중인 모든 Task가 수행되면 종료한다.

 

2. List<Runnable> notExecutedTasks = executorService.shutDownNow()

실행중인 Thread들을 즉시 종료시키려고 하지만 모든 Thread가 동시에 종료되는 것을 보장하지는 않고 실행되지 않은 Task를 반환한다.

여기에 추가로 두 개의 shutdown 메서드가 결합된 awaitTermination()을 사용하는 것이 추천된다. 이 메서드는 먼저 새로운 Task가 실행되는 것을 막고, 일정 시간동안 실행중인 Task가 완료되기를 기다린다. 만일 일정 시간동안 처리되지 않은 Task에 대해서는 강제로 종료시킨다.

executorService.shutdown();
try {
	if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
		executorService.shutdownNow();
	} 
} catch (InterruptedException e) {
	executorService.shutdownNow();
}

Future 인터페이스

submit()invokeAll() 메서드를 호출할때 반환하는 Future 객체로 Task의 결과값이나 상태(실행중 또는 실행완료)를 알 수 있다. 또한 Future 인터페이스는 Blocking method인 get()을 제공하는데 Task 실행 결과를 얻을 수 있다.(Runnable을 구현한 Task라면 null이 반환된다.)

Blocking이기 때문에 실행중에 get()이 호출되는 경우 실행이 끝날 때까지 대기한다. 이는 성능저하를 불러올 수 있으므로 Timeout을 설정하여 일정 시간이 지나면 TimeoutException이 발생하도록 유도할 수 있다.

Future<String> future = executorService.submit(callableTask);
String result = null;
try {
    result = future.get(); // Task가 실행중이면 여기서 대기한다.
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

// Timeout 설정, 지정된 시간이 지나면 TimeoutException이 발생한다.
String result = future.get(200, TimeUnit.MILLISECONDS); 

이 밖에도 isDone(), cancel(), isCancelled() 메서드가 있다.

boolean isDone = future.isDone(); // Task가 실행되었는지?
boolean canceled = future.cancel(true); // Task를 취소
boolean isCancelled = future.isCancelled(); // Task가 취소되었는지?

'개발 > JAVA' 카테고리의 다른 글

equals, hashcode  (0) 2020.12.07
JVM, JRE, JDK 간단 개념  (0) 2020.12.07
Comparator, Comparable 어떻게 사용할까?  (0) 2020.11.29
LocalDateTime, ZonedDateTime  (0) 2019.10.11
Collections를 이용한 정렬(sort method) 활용  (0) 2017.05.09

자바에서 정렬에 관련된 작업을 할 경우 떠올릴 수 있는 대표적인 인터페이스가 두가지 있다.

Comparator와 Comparable이다.

두 가지 인터페이스의 차이점과 사용법에 대해 알아보자.

 

두 인터페이스가 사용되는 이유는 공통적이다.

Primitive 타입의 숫자나 문자를 비교할 경우 명확한 기본 비교로직이 존재한다. 오름차순이라면 '1'보단 '2'가 크고, '가' 보다는 '나'가 크다. 하지만 비교하는 대상이 객체라면 기본 비교로직으로는 비교가 불가능하다.

예를들어 Game 클래스로 만든 객체들을 정렬한다고 가정한다면 여러가지 정렬을 위한 기준이 있을 수 있다. (발매년도, 타이틀명 등등) 이 때, 정렬 기준에 대한 명확한 로직을 정의할 수 있도록 도와주는 것이 Comparator와 Comparable 인터페이스이다. 

 

하나씩 살펴보자.

 

Comparator

'비교기'라는 이름에서 알 수 있듯이 비교를 위한 보조로직을 따로 작성하여 정렬시 인자로 건네준다.

보조로직 작성은 Comparator의 int compare(obejct args1, object args2) 메서드에 작성한다.

리스트의 정렬은 Collections.sort() 메서드를 사용하는데, 여기의 인자로 전달한다.

public class GameComparatorTest {
    @Test
    public void test1(){
        Comparator<Game> gameComparator = new Comparator<Game>() {
            // 비교를 위한 로직을 작성
            public int compare(Game game1, Game game2) {
                // 발매년도는 내림차순으로 하면서, 같은 발매년도에서는 타이틀명을 오름차순으로 하고싶다.
                if(game1.year != game2.year){
                    return Integer.compare(game1.year, game2.year) * (-1);
                }else {
                    return game1.title.compareTo(game2.title);
                }
            }
        };
 
        List<Game> games = new ArrayList<>();
        games.add(new Game(2000"Assassin Creed"));
        games.add(new Game(1990"Overwatch"));
        games.add(new Game(2000"League Of Legends"));
        games.add(new Game(1990"A Power"));
 
        Collections.sort(games, gameComparator);
 
        System.out.println(games);
    }
}
 
class Game {
    int year;
    String title;
 
    public Game(int year, String title) {
        this.year = year;
        this.title = title;
    }
 
    @Override
    public String toString() {
        return "Game{" +
                "year=" + year +
                ", title='" + title + '\'' +
                '}';
    }
}

// 출력결과

[Game{year=2000, title='Assassin Creed'}, Game{year=2000, title='League Of Legends'}, Game{year=1990, title='A Power'}, Game{year=1990, title='Overwatch'}]

발매년도로 내림차순 되었고, 발매년도가 같은경우 타이틀명으로 오름차순 되었다. 

 

 

Comparable

Comparator와 유사하게 비교를 위한 보조로직을 정의하지만 작성하는 위치가 다르다. '비교할 수 있는'이라는 뜻과 같이 비교하고자 하는 클래스에 비교로직을 정의한다. 마찬가지로 Collections.sort() 메서드로 정렬할 수 있고 이 때는 두번째 인자가 필요하지 않는다.

public class GameComparableTest {
    @Test
    public void test1(){
        List<Game> games = new ArrayList<>();
        games.add(new Game(2000"Assassin Creed"));
        games.add(new Game(1990"Overwatch"));
        games.add(new Game(2000"League Of Legends"));
        games.add(new Game(1990"A Power"));
 
        Collections.sort(games);
 
        System.out.println(games);
    }
}
 
class Game implements Comparable<Game> {
    int year;
    String title;
 
    public Game(int year, String title) {
        this.year = year;
        this.title = title;
    }
 
    @Override
    public String toString() {
        return "Game{" +
                "year=" + year +
                ", title='" + title + '\'' +
                '}';
    }
 
    @Override
    public int compareTo(Game game1) {
        // 발매년도는 내림차순으로 하면서, 같은 발매년도에서는 타이틀명을 오름차순으로 하고싶다.
        if(this.year != game1.year){
            return Integer.compare(this.year, game1.year) * (-1);
        }else {
            return this.title.compareTo(game1.title);
        }
    }
}

출력결과는 Comparator의 출력결과와 동일하다.

 

즉, 두 가지 인터페이스 모두 객체 비교를 위한 보조로직을 정의하는데 사용되는 인터페이스이다. 상황에 맞게 선택하여 사용하자.

 

 

 

 

'개발 > JAVA' 카테고리의 다른 글

JVM, JRE, JDK 간단 개념  (0) 2020.12.07
ExecutorService 사용법  (0) 2020.12.01
LocalDateTime, ZonedDateTime  (0) 2019.10.11
Collections를 이용한 정렬(sort method) 활용  (0) 2017.05.09
BigDecimal & BigInteger  (0) 2017.04.18

목표 

  • Java8부터 추가된 LocalDateTime, ZonedDateTime에 대한 기본이해 및 간단한 활용

날짜와 시간을 나타내는 자바 API는 여러가지가 있었지만 기능의 부족함으로 사용이 권장되지는 않았다.

(Calendar, Date 등)

하지만 자바8부터 등장한 java.time API는 이전 API의 결함을 상당부분 해결했고, 앞으로 제 역할을 할 것이라 기대된다. 

 

Instant Class

Instant 클래스는 타임라인의 한 점을 나타낸다.

Immutable, thread-safe 하며 now()로 생성된 인스턴스를 리턴받을 수 있다.

equals(), compareTo() 처럼 비교를 위한 함수를 제공하므로 인스턴스끼리 시간비교가 가능하다.

 
    @Test
    public void Instant_Duration_기본() throws InterruptedException {
        Instant start = Instant.now();
        System.out.println("현재시각(인스턴스) : " + start); // 현재시각(인스턴스) : 2019-10-11T04:50:51.759Z
 
        someComplicatedJob(); // 시간텀을 두기 위해서 임의의 JOB을 수행
 
        // Instant는 한 점을 나타내므로 비교가 가능하다.
        Instant end = Instant.now();
        System.out.println("Instant 사이의 비교 - equals() : " + start.equals(end));
        System.out.println("Instant 사이의 비교 - compareTo() : " + start.compareTo(end));
 
        // 비교를 위해서 Duration 클래스를 사용할 수 있다.
        // Duration : 두 Instant 사이에 있는 시간의 양, 불변 클래스
        Duration timeElapsed = Duration.between(start, end);
        System.out.println("JOB 수행에 걸린시간(밀리초) : " + timeElapsed.toMillis());
        System.out.println("JOB 수행에 걸린시간(초) : " + timeElapsed.getSeconds());
        System.out.println("JOB 수행시간이 마이너스인지 ? " + timeElapsed.isNegative());
    }
    
    
 
cs

 

Duration Class

시간차에 대한 작업을 위해 제공되는 클래스이다.

@Test
public void Duration_활용(){
    Duration duration1 = Duration.ofHours(1); // 1시간 차이
    Duration duration3 = Duration.ofHours(3); // 3시간 차이
 
    // 두 Duration이 2배 이상 차이가 나는지 체크하는 코드
    boolean overDoubleFaster1 = duration1.multipliedBy(2).minus(duration3).isNegative();
    System.out.println("duration3이 duration1 보다 2배 더 빠른가 ? " + overDoubleFaster1);
 
    boolean overDoubleFaster2 = duration1.toNanos() * 2 < duration3.toNanos();
    System.out.println("duration3이 duration1 보다 2배 더 빠른가 ? " + overDoubleFaster2);
}
cs

 

LocalDateTime

실제 사람이 사용하는 시간을 나타내는 API이다. Local(지역)이 접두어로 붙은 것에서 유추할 수 있듯이 여러 지역의 시간대(Time Zone) 정보는 포함하지 않는다.

KST나 UTC처럼 시간대를 지정하는 것이 정확한 표현이지만, 실제 활용에서 이러한 시간대는 잘 사용되지 않고 오히려 방해가 될 수 있으므로 만일 시간대를 필요로 하지 않는 작업이라면 LocalDateTime 사용을 고려한다.

 

LocalDate, LocalTime처럼 날짜, 시간에 특화된 API도 있지만 이 둘을 합쳐서 표현할 수 있는 LocalDateTime이 주로 사용된다.

@Test
public void LocalDate_LocalTime_LocalDateTime_기본(){
    // 구역(Zone) 구분이 없는 지역날짜로 now()나 of()로 생성한다.
    LocalDate nowDate = LocalDate.now();
    LocalDate myBirthDay = LocalDate.of(1988, Month.MAY, 26); // '월'을 enum으로 줄 수도 있다.
 
    System.out.println("현재날짜 : " + nowDate); // 2019-10-11
    System.out.println("현재날짜 + 7일 : " + nowDate.plusDays(7L)); // 2019-10-18
    System.out.println("현재날짜 + 7일 : " + nowDate.plusWeeks(1L)); // 2019-10-18
    System.out.println("현재날짜 - 7일 : " + nowDate.minus(Period.ofDays(7))); // 2019-10-04
 
    LocalTime nowtime = LocalTime.now();
    System.out.println("현재시각 : " + nowtime); // 15:16:22.909
    System.out.println("현재시각 + 1시간 : " + nowtime.plusHours(1L)); // 16:16:22.909
    System.out.println("현재시각에서 시간만 3으로 변경 : " + nowtime.withHour(3)); // 03:16:22.909
 
 
    LocalDateTime nowDateTime = LocalDateTime.now();
    System.out.println("현재시각 : " + nowDateTime); // 2019-10-11T15:16:22.909
    System.out.println("현재시각 + 1시간 : " + nowDateTime.plusHours(1L)); // 2019-10-11T16:16:22.909
    System.out.println("현재시각에서 시간만 3 : " + nowDateTime.withHour(3)); // 2019-10-11T03:16:22.909
}
cs

 

ZonedDateTime

LocalDateTime에서 시간대(Time Zone) 개념이 추가된 클래스로 기본적인 사용방법은 비슷하다.

다음과 같이 사용가능한 TimeZoneId를 출력해보면 많은 수가 있는것을 알 수 있다.

@Test
public void ZoneId(){
    // 각 시간대는 America/New_York, Europe/Berlin 등 ID가 존재한다.
    Set<String> availableZoneIds = ZoneId.getAvailableZoneIds(); // 이용할 수 있는 모든 시간대를 얻는다.
    for(String str : availableZoneIds){
        System.out.println(str); // Asia/Aden, America/Chicago, Europe/Luxembourg ...
    }
}
cs

많은 ZoneId 중에서도 아마 많이 사용할 시간대는 Asia/Seoul 과 UTC 일 것이다.

해당 타임존을 인자로하여 ZonedDateTime을 생성 후 출력해보면 인자로 준 시간대 정보가 같이 출력된다. 

UTC의 경우 표준이니 별다른 내용이 없지만, Asia/Seoul의 경우는 표준 UTC 06시23분에 9시간이 더해져서 15시23분임을 알 수 있다.

LocalDateTime에 타임존을 추가하거나 ZonedDateTime에 타임존을 제거하면서 상호변환이 가능하다.

@Test
public void ZonedDateTime(){
    ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));
    System.out.println(now); // 2019-10-11T15:23:08.605+09:00[Asia/Seoul]
 
    ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("UTC"));
    System.out.println(now2); // 2019-10-11T06:23:08.605Z[UTC]
 
}
cs
@Test
public void ZonedDateTime_to_LocalDateTime(){
    ZonedDateTime nowUTC = ZonedDateTime.now(ZoneId.of("UTC"));
    System.out.println(nowUTC);
 
    LocalDateTime nowSeoul = nowUTC.withZoneSameInstant(ZoneId.of("Asia/Seoul")).toLocalDateTime();
    System.out.println(nowSeoul);
 
    ZonedDateTime nowSeoulZonedTime = nowSeoul.atZone(ZoneId.of("Asia/Seoul"));
    System.out.println(nowSeoulZonedTime);
}
cs

ZonedDateTime 인스턴스를 문자열(String)로 변경하거나 반대의 경우도 가능하다. 

변환시 DateTimeFormatter를 인자로하여 원하는 패턴으로 변환한다.

@Test
public void formatting(){
    ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));
 
    String nowStr1 = now.format(DateTimeFormatter.ISO_DATE_TIME); // 2019-10-11T15:48:07.039+09:00[Asia/Seoul]
    String nowStr2 = now.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss z")); // 2019/10/11 15:48:07 KST
    String nowStr3 = now.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)); // 2019년 10월 11일 금요일 오후 3시 48분 07초 KST
    String nowStr4 = now.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(Locale.US)); // Friday, October 11, 2019 3:48:07 PM KST
 
    ZonedDateTime now1 = ZonedDateTime.parse(nowStr1);
    ZonedDateTime now2 = ZonedDateTime.parse(nowStr2, DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss z"));
    ZonedDateTime now3 = ZonedDateTime.parse(nowStr3, DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL));
    ZonedDateTime now4 = ZonedDateTime.parse(nowStr4, DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(Locale.US));
 
    System.out.println(now1);
    System.out.println(now2);
    System.out.println(now3);
    System.out.println(now4);
}
cs

 

EpochTime -> LocalDateTime 변환

시간을 표현하는 Long값인 EpochTime을 LocalDateTime으로 변환할 수 있다.

@Test
public void Epoch_to_LocalDateTime(){
    // Epoch Time을 LocalDateTime으로 변경한다.
    LocalDateTime now3 = LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.of("Asia/Seoul"));
    System.out.println(now3);
 
    LocalDateTime now4 = LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.of("UTC"));
    System.out.println(now4);
}
cs

 


읽어주셔서 감사합니다. 도움이 되셨다면 광고 클릭 부탁드립니다.

모두 힘내세요! : )

 

 

 

'개발 > JAVA' 카테고리의 다른 글

ExecutorService 사용법  (0) 2020.12.01
Comparator, Comparable 어떻게 사용할까?  (0) 2020.11.29
Collections를 이용한 정렬(sort method) 활용  (0) 2017.05.09
BigDecimal & BigInteger  (0) 2017.04.18
Array 와 ArrayList 사이의 변환  (0) 2017.03.28

+ Recent posts