안녕하세요

오늘은 Java 개발자라면 많이 들어보셨을 JIT 컴파일러에 대해서 소개하려고 합니다.

 

먼저 Java 프로그램이 실행되는 과정을 설명하겠습니다.

 

Java 코드를 자바 컴파일러가 바이트코드로 컴파일합니다. 그 결과물은 클래스파일입니다.

그리고 JVM이 실행을 하면서 해당 바이트코드를 한 줄씩 읽으면서 기계어로 해석(interpret)합니다.

 

JIT 컴파일러는 자주 호출되는 메소드나 자주 실행되는 코드 블럭을 미리 기계어로 번역하여 캐싱을 해둡니다.

그리고 이 자주 실행되는 부분을 'HotSpot'이라고 합니다.

 

그럼 JIT 컴파일은 언제 수행될까요?

컴파일 타임에 수행될까요? 런타임에 수행될까요?

 

런타임에 실행됩니다.

 

보통 '컴파일'이라고 하면 말 그대로 컴파일 타임에 수행이 됩니다.

하지만 JIT 컴파일러는 런타임에 수행됩니다.

 

왜 런타임에 수행하는 것일까요?

 

그 전에 컴파일 방식과 인터프리터 방식의 장단점에 대해서 살펴보겠습니다.

 

컴파일 방식의 단점

컴파일 방식의 단점은 시작 시간이 오래 걸린다는 것입니다. 프로그램의 성능은 좋아지겠지만 시작 시간이 너무 오래 걸립니다.

실행을 하려면 컴파일(기계어로의 컴파일) 과정을 거쳐야 합니다. 개발자가 코드를 한 줄만 수정해도 다시 컴파일을 해야 하기 때문에 프로그램이 시작 시간이 느리다는 단점이 있습니다.

('개발자가 코드를 한 줄만 수정해도 자바 컴파일러의 컴파일(javac)은 해야 하는데 그럼 이거는?'이라고 생각하실 수 있습니다. 하지만 Java 코드를 바이트코드로 컴파일하는 것은 아주 빠르게 수행됩니다. 또한 자바 컴파일러는 수정된 부분의 코드만 다시 컴파일을 하고 기존 코드는 재사용하기 때문에 일반적인 컴파일(기계어로의 컴파일)보다 훨씬 빠릅니다.)

 

컴파일 방식의 장점

하지만 컴파일을 한 번만 하고 실행을 많이 한다면 컴파일 방식이 유리할 것입니다.

예를 들어, 어떤 프로그램을 CD에 담아서 대량 생산을 한다면 컴파일 방식이 유리할 것입니다.

한 번만 컴파일 해놓으면 그 실행파일을 CD에 담기만 하면 되기 때문입니다. 해당 프로그램은 실행 시간 중에 기계어로 번역하는 작업을 하지 않고 이미 번역되어 있는 기계어를 바로 실행하기 때문에, 컴파일을 할 때는 오래 걸려서 답답할 수도 있겠지만 사용자는 아주 빠른 성능의 프로그램을 사용할 수 있을 것입니다.

즉, 컴파일 방식의 장점은 실행 속도가 빠르다는 것입니다.

이러한 장점 때문에 대규모 계산이 필요한 고성능 컴퓨팅 분야에서는 여전히 Fortran과 C언어를 사용하는 것입니다.

(참고로 Fortran은 최초의 프로그래밍 언어입니다😉)

 

그렇다면 인터프리터 방식은 어떨까요?

인터프리터 방식은 실행하기 전에 전체 코드를 기계어로 번역하는 작업(컴파일)을 하지 않아도 되므로 시작 시간은 컴파일 방식에 비해서 빠를 것입니다. 하지만 실행 중에 코드를 기계어로 번역하는 작업을 해야 하기 때문에 실행 속도는 컴파일 방식보다 느릴 것입니다.

 

예를 들어서 설명하겠습니다.

예를 들어 A라는 메소드가 있습니다.

컴파일 방식일 때, A 메소드는 컴파일하는데 1초가 걸리고 실행하는데 1초가 걸린다고 합시다.

인터프리트 방식일 때, A 메소드는 인터프리터가 한 줄씩 기계어로 해석했을 때 총 해석시간이 1초이며 한 줄씩 실행할 때 총 실행 시간이 1초가 걸린다고 합시다.

(사실 이렇지 않습니다. 컴파일은 코드 전체를 번역하면서 최적화 작업을 진행하기 때문에 컴파일러가 기계어로 번역을 하는 시간은 인터프리터가 기계어로 한 줄씩 해석하는 총 시간보다 오래 걸릴 확률이 높고, 최적화가 되었기 때문에 인터프리터가 실행 중에 기계어로 해석하는 시간을 제외하더라도 실행시간은 인터프리트 방식보다 컴파일 방식이 더 빠를 확률이 높습니다. 이해를 돕기 위해, 계산을 쉽게 하기 위해 1초로 정한 점 양해 바랍니다.)

 

A 메소드가 딱 1번 호출된다면

컴파일 방식 인터프리트 방식
기계어로 번역 (1초) 기계어로 해석 (총 1초)
실행 (1초) 실행 (1초)
총 2초 총 2초

컴파일 방식이든 인터프리트 방식이든 총 시간은 2초로 똑같습니다.

 

A 메소드가 1000번 호출된다면

컴파일 방식 인터프리트 방식
기계어로 번역 (1초) 기계어로 해석 (총 1초)
실행 (1초) 실행 (1초)
실행 (1초) 기계어로 해석 ( 1초)
실행 (1초) 실행 (1초)
실행 (1초) 기계어로 해석 ( 1초)
실행 (1초) 실행 (1초)
실행 (1초) 기계어로 해석 ( 1초)
실행 (1초) 실행 (1초)
실행 (1초) 기계어로 해석 ( 1초)
실행 (1초) 실행 (1초)
실행 (1초) 기계어로 해석 ( 1초)
.
.
.
.
.
.
총 1001초 총 2000초

실행 중에 인터프리트 방식으로 해석을 하면 해당 코드를 기계어로 해석하는 똑같은 작업을 1000번 하는 것입니다.

따라서 컴파일 방식은 1001초, 인터프리트 방식은 2000초가 걸릴 것입니다.

그리고 이 차이는 A 메소드가 많이 호출될수록 점점 커질 것입니다.

 

따라서 자주 실행되는 코드를 미리 기계어로 컴파일 해두면 실행시간을 훨씬 줄일 수 있을 것입니다.

 

그래서 JIT 컴파일러가 런타임에 동작하는 이유가 뭔데?

이러한 단점을 극복하기 위해서 Java 1.3 HotSpot VM부터 JIT 컴파일러를 도입하여 자주 실행되는 메소드는 실행 중에 기계어로 번역을 하는 방법을 채택합니다. 이 방법으로는 시작 시간은 여전히 빠르면서, 실행 중에 자주 실행되는 코드를 기계어로 번역을 하여 인터프리터 방식의 실행 속도가 느리다는 단점을 어느 정도 극복할 수 있습니다.

 

JIT 컴파일러가 런타임에 실행됨으로써 얻을 수 있는 장점은 이 뿐만이 아닙니다.

아래의 코드를 봅시다.

for (int i = 0; i < 1000; i++) {
	// 코드 생략
}

위의 반복문을 보면 해당 반복문이 1000번 실행될 것이라는 것을 컴파일 시점에 알 수 있습니다.

 

int count = scanner.nextInt(); // 사용자의 입력을 받음
for (int i = 0; i < count; i++) {
	// 코드 생략
}

하지만 이 코드는 반복문이 몇 번 실행될지 예측을 할 수 없습니다.

사용자의 입력값에 따라 반복문의 실행 횟수가 달라지기 때문입니다.

 

또한 서버 애플리케이션의 경우, 클라이언트가 어떤 API를 많이 호출하는지에 따라 특정 메소드의 실행 횟수가 달라지기 때문에 컴파일 시점에 더욱 더 예측을 하기가 어렵습니다.

 

하지만 런타임에는 이러한 정보들을 알 수 있습니다.

 

이렇게 런타임에 컴파일을 하게 되면 동적 최적화를 할 수 있습니다. 컴파일 타임에는 알 수 없었던 정보들을 알 수 있기 때문에 프로그램의 실행 패턴 등 더 많은 정보를 기반으로 최적화를 할 수 있다는 장점이 있습니다.

 

따라서 자주 실행되는 코드를 미리 기계어로 컴파일 해두면 실행시간을 훨씬 줄일 수 있을 것입니다.

하지만 이 메소드가 자주 호출되는지 어떻게 아냐고!!

 

여기에서 JIT 컴파일러가 런타임에 수행됨으로써 얻을 수 있는 장점이 드러납니다.

 

그러면 특정 메소드가 어느 정도로 자주 호출되어야 JIT 컴파일러가 컴파일을 하게 되는 것일까요?

메소드가 몇 번 호출되어야 JIT 컴파일러가 컴파일을 할지 그 임계값은 -XX:CompileThreshold  옵션으로 지정할 수 있습니다.

만약 Test.class라는 클래스파일을 실행시킬 때, 메소드가 5000번 이상 호출되었을 때 JIT 컴파일을 하고 싶다면

java -XX:CompileThreshold=5000 Test

이렇게 해주면 됩니다.

Java 8 이상의 HotSpot JVM에서 CompileThreshold 옵션의 기본값은 10,000입니다.

따라서 아무런 설정을 해주지 않는다면 특정 메소드가 10,000번 호출되었을 때 JIT 컴파일러가 기계어로 번역을 해서 따로 캐싱해두는 것입니다.

 

위에서 말한 것처럼 JIT 컴파일러가 런타임에 동작하기 때문에 얻을 수 있는 장점이 많이 있습니다. 하지만 단점도 있습니다. JIT 컴파일을 한 후에는 성능이 빨라지겠지만 컴파일을 런타임에 하기 때문에 JIT 컴파일러가 컴파일을 하는 동안에는 응답시간이 지연될 수도 있습니다. 따라서 컴파일을 빠르게 하는 것도 중요합니다.

 

최적화를 잘 하면 컴파일 후의 코드는 성능이 매우 좋아지겠지만 컴파일 시간이 오래걸립니다.

최적화를 엄청 잘 하지는 않으면 컴파일 시간은 덜 걸려서 실행 중에 지연 시간은 덜 발생하겠지만 컴파일 후의 코드가 성능이 그렇게 좋아지지는 않을 것입니다.

 

지연 시간도 줄이고 최적화도 잘하면 매우 좋겠지만 하나를 개선하려면 하나를 포기해야 합니다.

 

JVM은 두 가지의 장점을 최대한 살릴 수 있도록 Tiered 컴파일이라는 방식을 채택합니다.

 

Tiered 컴파일러

Tiered 컴파일러는 한마디로 다단계 컴파일러라고 할 수 있습니다.

최적화가 아주 잘 된 코드를 '짱 좋은 코드', 그럭 저럭 최적화가 되어 컴파일된 코드를 '보통 코드'라고 하겠습니다.

(여기에서 '코드'는 기계어(machine code)를 말합니다)

특정 메소드가 CompileThreshold 만큼 호출되어 JIT 컴파일러가 실행될 때 JIT 컴파일러는 바로 짱 좋은 코드로 컴파일하지 않습니다. 일단은 보통 코드로 컴파일합니다. 그러면 짱 좋은 코드로 컴파일 하는 것보다는 실행 중에 지연 시간이 덜 발생하겠죠?

그러다가 그 메소드가 더 많이 호출되어 일정 횟수를 초과하면 그 때 짱 좋은 코드로 다시 최적화를 하는 것입니다.

바이트코드에서 바로 짱 좋은 코드로 컴파일 하는 것보다 보통 코드에서 짱 좋은 코드로 최적화 하는 것이 시간이 훨씬 덜 소요됩니다.

이렇게 하면 지연시간을 최소화하면서 많이 호출되는 코드를 최적화가 아주 잘 된 코드로 컴파일 할 수 있는 것입니다.

 

Tiered 컴파일러에서 보통 코드로 컴파일하는 컴파일러를 C1, 짱 좋은 코드로 컴파일하는 컴파일러를 C2라고 합니다.

 

Java 9 이전에는 서버 컴파일러, 클라이언트 컴파일러라는 개념이 있었습니다.

Tiered 컴파일러로 따지면 보통 코드로 컴파일하는 C1 컴파일러가 클라이언트 컴파일러, 짱 좋은 코드로 컴파일하는 C2 컴파일러가 서버 컴파일러입니다.

실행을 할 때 서버 컴파일러 또는 클라이언트 컴파일러를 지정할 수 있었습니다.

 

하지만 Java 9부터 Tiered 컴파일러가 기본 옵션이 되었고 명시적으로 서버 컴파일러만을, 또는 클라이언트 컴파일러만을 사용할 수 없습니다.

(-XX:TieredCompilation 옵션으로 Tiered 컴파일러를 비활성화하여 C2 컴파일러(서버 컴파일러)만을 사용하도록 할 수는 있습니다.)

 

Code Cache

그러면 JIT 컴파일러가 이렇게 컴파일한 기계어를 어디에 저장할까요?

JVM 안의 Code Cache라는 곳에 저장됩니다.

Code Cache의 크기는 -XX:ReservedCodeCacheSize 옵션을 통해 지정할 수 있습니다.

Code Cache의 크기를 256MB로 설정하고 싶다면 -XX:ReservedCodeCacheSize=256m이라고 하면 됩니다.

만약에 코드 캐시 영역이 꽉 차면 더 이상 JIT 컴파일러가 동작하지 않습니다. 컴파일을 하더라도 컴파일된 기계어를 저장할 공간이 없기 때문입니다.

따라서 Code Cache 영역의 크기를 적절히 설정하는 것이 중요합니다.

 

그러면 GC가 돌 때 Stop-the-world가 발생하는 것처럼 JIT 컴파일러가 실행될 때 Stop-the-world가 발생하지 않을까요?

그렇지 않습니다. GC가 동작할 때는 살아있는 객체를 판별하기 위해서는 일단 애플리케이션의 실행을 멈춰야 합니다.

인구 조사를 예로 들어봅시다.

예를 들어 총 20층이고 각 층에는 10개의 가구가 있는 아파트가 있습니다.

인구 조사 담당자는 1층부터 20층까지 순차적으로 각 집을 돌면서 인구 조사를 합니다.

그런데 1층을 이미 조사하고 2층을 조사하고 있는 중에 1층에서 아기가 태어난다면 정확한 인구 조사를 못 하겠죠?

그렇기 때문에 GC가 돌 때는 애플리케이션의 실행을 멈추고 지금 살아있는 객체를 알아내는 것이고,

그래서 Stop-the-world가 발생하는 것입니다.

 

하지만 JIT 컴파일러는 상황이 다릅니다. 굳이 애플리케이션 실행을 중단할 필요가 없습니다.

따라서 백그라운드 스레드로 애플리케이션 스레드와 동시에 실행됩니다.

 

https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html

https://www.geeksforgeeks.org/just-in-time-compiler/

https://www.baeldung.com/jvm-tiered-compilation

https://www.baeldung.com/jvm-code-cache

https://www.oreilly.com/library/view/java-performance-2nd/9781492056102/ch04.html

안녕하세요

오늘은 Java 사용자라면 자주 쓰실 enum에 대해서 알아보겠습니다.

 

enum 타입은 아래와 같이 enum이라는 키워드를 사용하여 만들 수 있습니다.

public enum Color {
    RED, GREEN, BLUE;
}

 

enum 타입을 만들 때는 class 키워드가 아니라 enum 키워드로 만들기 때문에 클래스가 아니라고 생각하실 수도 있는데요,

enum도 클래스이고, 다른 클래스들과 다를 것 없이 Object 클래스를 상속받습니다.

 

이렇게 enum 키워드를 통해 만들어진 모든 enum 타입은 java.lang.Enum 클래스를 상속받게 됩니다.

extends Enum을 안 써줘도 말이죠. (enum에는 extends를 쓸 수 없습니다.)

컴파일을 하면서 컴파일러가 extends Enum을 추가해줍니다.

이것이 바로 enum 타입에는 extends를 사용할 수 없는 이유입니다.

Java는 다중 상속을 지원하지 않기 때문에 enum 타입이 다른 클래스를 상속하고 있으면 extends Enum을 추가해줄 수 없기 때문입니다.

 

java.lang.Enum 클래스

그럼 모든 enum 타입의 부모 클래스인 java.lang.Enum 클래스에 대해서 알아보겠습니다.

Enum 클래스는 Java가 처음 나왔을 때부터 enum이 있었던 것이 아닙니다. JDK1.5부터 추가된 클래스입니다.

먼저 인스턴스 변수로 어떤 것이 있는지 보겠습니다.

name과 ordinal을 볼 수 있는데요, 두 변수 모두 final로 선언되어 있습니다.

 

그리고 이제 생성자를 봅시다.

생성자는 딱 하나가 있습니다.

유일한 생성자. 프로그래머들은 이 생성자를 호출할 수 없다.enum 클래스를 정의하면 컴파일러가 어떤 코드를 추가해주는데, 그 코드가 호출하는 생성자이다.
public enum Color {
    RED, GREEN, BLUE;
}

이 enum 클래스의 경우 생성자가 호출되면서 name과 ordinal이 어떤 값으로 세팅되는지 디버거를 통해 직접 봐볼까요?

RED, GREEN, BLUE 순으로 선언했었는데, 이 순서대로 ordinal의 값으로 0부터 순서대로 저장되는 것을 볼 수 있습니다.

그리고 name에는 "RED", "GREEN", "BLUE"와 같이 enum 상수값이 문자열로 저장되었습니다.

ordinal에는 정의한 enum 상수 순서대로 0번부터 차례대로 저장됩니다.

equals() 메소드

equals() 메소드가 final로 선언되어 있는 것을 볼 수 있는데요,

final로 선언되어 있기 때문에 저희는 equlas() 메소드를 오버라이딩할 수 없습니다.

그런데 equals() 메소드 안을 보면 그냥 this == other 이렇게 == 연산자를 통해 비교하고 있습니다.

즉, enum 상수들끼리 비교를 할 때는 equals()로 비교를 하나 ==으로 비교를 하나 똑같다는 것입니다.

그런데 equals() 메소드는 같은 객체(동일성)인지를 비교하는 것이 아니라 내용이 같은지(동등성)를 비교하도록 오버라이딩 해야 하는 거 아닌가? 하는 생각이 드셨을 수도 있습니다.

네 equals() 메소드는 동등성을 비교해야 하는 것이 맞습니다. 하지만 enum 클래스는 이렇게 == 연산자를 통해 비교를 하는 것이 동등성을 비교하는 것과 결과가 같습니다.

 

https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.9

위는 Java 공식 문서인데요,

각각의 enum 상수의 인스턴스는 딱 1개이기 때문에 비교를 하려는 두 객체 중 적어도 하나가 enum 상수라면 equals() 대신 == 연산자로 비교해도 된다.

enum 상수는 인스턴스가 단 하나만 생성됩니다. (싱글톤)

따라서 어차피 같은 enum 상수라면 같은 인스턴스이기 때문에 굳이 equals() 메소드로 비교하지 않고 == 연산자를 통해서 비교를 하면 되는 것입니다.

clone() 메소드

CloneNotSupportedException을 던집니다. 이는 enum의 '싱글톤' 상태에 필수적인 '절대 복제되지 않음'을 보장합니다.

clone 메소드는 호출하면 무조건 예외가 발생합니다.

위의 equals() 메소드에서 설명했던 것처럼, enum은 인스턴스가 딱 1개인 싱글톤이기 때문에 객체를 복사할 수 없습니다.

 

toString(), hashCode(), clone() 메소드

Object 클래스의 메소드인 equals(), toString(), hashCode(), clone() 메소드 중 toString()을 제외하고 모두 final로 선언되어있습니다.

toString()을 제외한 equals(), hashCode(), clone() 메소드는 저희가 재정의할 수 없습니다.

그리고 clone() 메소드는 아예 clone을 하지 못 하게 예외를 던지는 것을 볼 수 있습니다.

enum 상수들의 목록을 가져오려면 어떻게 해야 할까?

public enum Color {
    RED, GREEN, BLUE;
}

만약 이 Color enum 타입의 상수들인 RED, GREEN, BLUE들을 배열로 가져오고 싶으면 어떻게 해야 할까요?

values() 라는 static 메소드를 사용하면 됩니다.

예를 들어 Color.values() 이렇게 말입니다.

public class EnumDemo {

    public static void main(String[] args) {
        Color[] colors = Color.values();
        for (Color color : Color.values()) {
            System.out.println(color);
        }
    }

}

위 코드를 실행하면

이렇게 Color enum 타입에 있는 상수들이 모두 출력되는 것을 볼 수 있습니다.

 

그런데 java.util.Enum 클래스에 들어가보면 values()라는 메소드는 없습니다.

그렇다면 도대체 values() 메소드는 어디에 정의되어 있는 것일까요?

 

자, 생각을 해봅시다.

자식은 부모를 알지만 부모는 자식을 모릅니다.

이것은 당연합니다.

java.lang.Enum 클래스를 상속받아서 만들어지는 enum 타입이 무엇일지 알지 못합니다.

Color라는 enum 타입을 만들 수도 있고 Grade라는 enum 타입을 만들 수도 있고, Category라는 enum 타입을 만들 수도 있고, Season이라는 enum 타입을 만들 수도 있고.... 가능한 enum 타입은 무한할 것입니다.

뿐만 아니라 Color 클래스의 enum 상수로 RED, YELLOW, GREEN이 올지, PINK, ORANGE, BLUE, PURPLE이 올지 등등 또한 무한할 것입니다.

따라서 java.lang.Enum 클래스는 자식을 모르기 때문에 values() 메소드를 구현할 수 없습니다.

 

그럼 도대체 values()는 어떻게 쓸 수 있는 것일까요?

https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html

공식 튜토리얼 문서를 보면 values() 메소드는 컴파일러가 enum을 만들 때 추가해준다고 합니다.

 

원소들이 enum 상수들로만 이루어져있다면 어떤 자료구조를 사용하는 것이 좋을까?

Set 자료구조를 사용하고 싶은데 element들이 Enum 상수들이라면 EnumSet을 사용하는 것이 좋습니다.

Map 자료구조를 사용하고 싶은데 element들이 Enum 상수들이라면 EnumMap을 사용하는 것이 좋습니다.

EnumSet과 EnumMap에 대한 내용은 다른 글에서 자세히 다루겠습니다.

 

참고

https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.9

https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html

https://www.baeldung.com/java-enumset

https://www.baeldung.com/java-enum-map

안녕하세요

오늘은 Java의 equals() 메소드에 대해 알아보겠습니다.

 

equals()는 내용이 같은지를 비교하는 것이고 ==은 같은 객체인지 확인하는 것이라는 말을 많이 들었을 것입니다.

정말 그럴까요?

 

우선 Java에서 ==은 같은 객체인지 아닌지를 비교하는 연산자입니다. (primitive 타입의 경우 값 비교)

 

우선 Object 클래스의 equals() 메소드를 한 번 살펴봅시다.

Object 클래스의 equals() 메소드는 ==으로 비교하는 것을 볼 수 있습니다.

즉, Object 클래스는 ==으로 비교하는 것과 equals() 메소드로 비교하는 것이 똑같다는 것입니다.

정말 그런지 한 번 확인해볼까요?

public class ObjectTest {

    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = o1;
        Object o3 = new Object();
        Object o4 = new Object();
        System.out.println((o1 == o2) + " " + o1.equals(o2));
        System.out.println((o1 == o3) + " " + o1.equals(o3));
        System.out.println((o1 == o4) + " " + o1.equals(o4));
        System.out.println((o2 == o3) + " " + o2.equals(o3));
        System.out.println((o2 == o4) + " " + o2.equals(o4));
        System.out.println((o3 == o4) + " " + o3.equals(o4));
    }

}

o1과 o2는 같은 객체입니다.

 

이 코드를 실행해보면

==으로 비교한 것과 equals() 메소드로 비교한 결과가 모두 같습니다.

또한 같은 객체인 o1과 o2만 true가 나온 것을 볼 수 있습니다.

 

그럼 이제 제가 클래스를 하나 만들어보겠습니다.

 
public class Person {

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

}

이름과 나이를 상태로 가지는 Person이라는 클래스를 만들었습니다.

 

public class EqualsTest {
    public static void main(String[] args) {
        Person p1 = new Person("김땡땡", 20);
        Person p2 = new Person("김땡땡", 20);
        Person p3 = p2;
        Person p4 = new Person("이땡땡", 25);
        System.out.println(p1.equals(p2));
        System.out.println(p1.equals(p3));
        System.out.println(p1.equals(p4));
        System.out.println(p2.equals(p3));
        System.out.println(p2.equals(p4));
        System.out.println(p3.equals(p4));
    }
}

p1과 p2는 이름과 나이가 모두 같고, p2와 p3는 같은 객체입니다.

p4는 p1, p2, p3과 다른 객체이고 내용도 다릅니다.

 

그리고 이 코드를 실행하면 어떤 결과가 나올까요?

.

.

.

.

.

.

.

.

.

.

.

.

.

.

결과는 이렇습니다.

같은 객체인 p2와 p3의 비교에서만 true를 리턴했습니다.

 

p1과 p2는 나이와 이름이 모두 같은데 왜 equals()로 비교를 했을 때 다르다고 나왔을까요?

 

사실 이는 당연한 결과입니다. Object 클래스의 equals()는 ==으로 비교한 것과 똑같은 것을 확인했었죠?

제가 만든 Person 클래스는 Object 클래스를 상속받기 때문에 Object의 equals()와 동일합니다.

따라서 Person 객체들을 ==으로 비교한 것과 결과가 똑같이 같은 객체인 경우에만 true를 반환했던 것입니다.

 

그런데 저는 같은 객체일 때가 아니라 이름과 나이가 모두 동일할 때 true를 반환하고 싶습니다.

그러면 어떻게 해야 할까요?

 

이름과 나이가 같으면 true를 반환하도록 equals() 메소드를 오버라이딩(재정의) 해주면 됩니다.

@Override
public boolean equals(Object o) {
    if (o instanceof Person) {
        Person p = (Person) o;
        if (p.getName() != null && p.getName().equals(this.name) && p.getAge() == this.age) {
            return true;
        } else {
            return false;
        }
    } else {
        return false;
    }
}

저는 이렇게 작성해보았습니다.

 

equals() 메소드를 오버라이딩 했으니 이제 아까 코드를 다시 실행해볼까요?

public class EqualsTest {
    public static void main(String[] args) {
        Person p1 = new Person("김땡땡", 20);
        Person p2 = new Person("김땡땡", 20);
        Person p3 = p2;
        Person p4 = new Person("이땡땡", 25);
        System.out.println(p1.equals(p2));
        System.out.println(p1.equals(p3));
        System.out.println(p1.equals(p4));
        System.out.println(p2.equals(p3));
        System.out.println(p2.equals(p4));
        System.out.println(p3.equals(p4));
    }
}
이제 실행결과가 어떻게 나올까요?

이렇게 이름과 나이가 같은 p1과 p2의 비교에서 true를 리턴한 것을 볼 수 있습니다.
또한 p3는 p2와 동일한 객체이기 때문에 p1과 p3의 비교에서도 true가 나왔습니다.

 

그런데 equals() 메소드를 오버라이딩할 때 주의할 점이 있습니다.

이것은 Object 클래스의 equals() 메소드에 달린 주석입니다.
다른 객체가 이것과 같은지 아닌지를 나타냅니다.
equals() 메소드는 null이 아닌 참조 객체에 대해 비교를 수행합니다.
재귀적이다. null이 아닌 참조 타입 객체 x가 있을 때 x.equals(x)는 true를 반환해야 한다.
대칭적이다. null이 아닌 참조 타입 객체 x. y가 있을 때 y.equals(x)가 true일 때만 x.equals(y)가 true여야 한다.
전이적이다. null이 아닌 참조 타입 객체 x, y, z가 있을 때, x.equals(y)가 true이고 y.equals(z)가 true이면 x.equals(z)는 true를 반환해야 한다.
일관적이다. null이 아닌 참조  타입 객체 x, y가 있을 때 x.equals(y)를 여러 번 호출해도 일관적으로 계속 true를 반환하거나 일관적으로 계속 false를 반환해야 한다.
null이 아닌 참조 타입 객체 x가 있을 때, x.equals(null)은 false를 리턴해야 한다.

(의역이 있을 수 있습니다.)

equals() 메소드를 오버라이딩 할 때는 위 규칙들을 반드시 지키며 재정의해야 합니다.

 
또한 equals() 메소드를 오버라이딩할 땐 hashCode() 메소드도 오버라이딩하는 것이 좋습니다.
그렇지 않으면 HashSet이나 HashMap을 사용할 때 원치 않는 결과를 얻을 수 있습니다.

무슨 말인지 아직 와 닿지 않으시죠? 한 번 예를 들어보겠습니다.

아까 제가 구현한 Person 클래스를 key로 가지는 HashMap을 만들어보겠습니다.

import java.util.HashMap;
import java.util.Map;

public class HashMapTest {
    public static void main(String[] args) {
        Map<Person, Integer> map = new HashMap<>();
        Person p1 = new Person("김땡땡", 20);
        Person p2 = new Person("김땡땡", 20);
        map.put(p1, 1);
        map.put(p2, 2);
        System.out.println(map.size());
    }
}
아까 저희는 이름과 나이가 같으면 true를 리턴하도록 equals() 메소드를 재정의했습니다.

이름과 나이가 같은 객체 p1과 p2를 만들고 map에 한 번은 p1을 key로, 한 번은 p2를 key로 엔트리를 추가했습니다.

이 코드의 결과는 어떻게 될까요?
1을 출력할까요? 2를 출력할까요?
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2를 출력합니다.
이상하지 않나요?
key 값이 같으니 덮어써질 것 같은데, 왜 다른 key로 인식하는 것일까요?
그 이유는 hashCode()를 재정의하지 않았기 때문입니다.
 
Object 클래스의 equals() 메소드에 달린 주석
같은 객체는 같은 해시코드를 가져야 한다는 hashCode() 메소드의 일반적인 규칙을 유지하기 위해서 equals() 메소드를 오버라이딩 했을 때는 일반적으로 hashCode()를 반드시 오버라이딩해야 한다.
위에 써있는 것처럼 equals() 메소드를 오버라이딩 했다면 equals() 메소드 비교에서 true를 반환하는 두 객체는 hashCode() 메소드에서도 같은 해시코드를 반환해야 합니다.
HashMap은 equals() 메소드와 hashCode() 메소드 모두 같아야 동일한 key로 인식합니다.
따라서 위 코드의 실행 결과 1을 출력하기를 원한다면 hashCode()를 오버라이딩하여 hashCode()의 리턴값까지 같게 만들어줘야 합니다.
한 번 hashCode() 메소드를 오버라이딩해보겠습니다.
@Override
public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + age;
    return result;
}
이렇게 hashCode() 메소드까지 오버라이딩 했으니 다시 아까 코드를 실행해볼까요?
import java.util.HashMap;
import java.util.Map;

public class HashMapTest {
    public static void main(String[] args) {
        Map<Person, Integer> map = new HashMap<>();
        Person p1 = new Person("김땡땡", 20);
        Person p2 = new Person("김땡땡", 20);
        map.put(p1, 1);
        map.put(p2, 2);
        System.out.println(map.size());
    }
}

이제 1을 출력하는 것을 볼 수 있습니다.

 

모든 클래스가 가지고 있는 equals() 클래스와 주의점에 대해서 알아보았습니다.

읽어주셔서 감사합니다.

try~catch문에서 finally 블럭은 도대체 왜 필요한걸까?

try 블럭에서 예외가 발생하지 않으면 try-catch문 이후에 있는 코드가 실행이 될거고

try 블럭에서 예외가 발생해서 catch 블럭으로 갔다고 해도 가장 큰 범위인 Exception을 잡는 catch 블럭이 마지막에 무조건 있다면 어떤 예외가 발생하더라도 catch 블럭에 걸릴테니까 그 안에서 예외처리를 해주면 try-catch문 이후에 있는 코드가 실행이 될텐데

왜 굳이 finally가 필요할까? 하는 생각이 문득 들었다.

그래서 finally에 대해서 찾아보게 되었다.

 

일단 아래의 코드를 보자.

public class FinallyTest {
    public static void main(String[] args) {
        FinallyTest finallyTest = new FinallyTest();
        finallyTest.returnInFinally();
    }

    public void returnInFinally() {
        try {
            int n = 5 / 0;
            System.out.println("여기는 try 블럭");
        } catch (Exception e) {
            System.out.println("여기는 catch 블럭");
        } finally {
            System.out.println("여기는 finally 블럭");
        }
    }
}

이 코드의 결과는 어떻게 될까?

 

5를 0으로 나누는 순간 예외가 발생하여 catch 블럭으로 들어가게 되므로

System.out.println("여기는 try 블럭");

 위 코드는 실행되지 않고 catch 블럭으로 들어가게 된다.

catch블럭에서 "여기는 catch 블럭"을 출력하고 finally 블럭으로 들어가서 "여기는 finally 블럭"을 출력한다.

따라서 위 코드의 실행 결과는 아래와 같다.

여기는 catch 블럭
여기는 finally 블럭

 

그럼 이제 try 블럭과 catch 블럭에 return문을 넣어보자.

public class FinallyTest {
    public static void main(String[] args) {
        FinallyTest finallyTest = new FinallyTest();
        finallyTest.returnInFinally();
    }

    public void returnInFinally() {
        try {
            int n = 5 / 0;
            System.out.println("여기는 try 블럭");
            return;
        } catch (Exception e) {
            System.out.println("여기는 catch 블럭");
            return;
        } finally {
            System.out.println("여기는 finally 블럭");
        }
    }
}

위 코드의 실행 결과는 어떻게 될까?

1. catch 블럭에서 return을 해줬으니까 finally는 실행이 되지 않을 것이다.

2. return문을 만났어도 finally 블럭은 실행될 것이다.

.

.

.

.

.

.

.

정답은 2번이다.

위 코드를 실행하면 실행 결과는 아래와 같다.

여기는 catch 블럭
여기는 finally 블럭

try 블럭이 실행되고 나서 finally 블럭이 최종적으로 실행된다.

 

그러면 아래의 코드는 실행결과가 어떻게 될까?

public class FinallyTest2 {
    public static void main(String[] args) {
        FinallyTest2 finallyTest = new FinallyTest2();
        finallyTest.returnInFinally();
    }

    public void returnInFinally() {
        while (true) {
            try {
                System.out.println("여기는 try 블럭");
                break;
            } catch (Exception e) {
                System.out.println("여기는 catch 블럭");
            } finally {
                System.out.println("여기는 finally 블럭");
            }
        }
    }
}

while문을 들어가자마자 break;를 해줬다.

1. break문을 만났으니 바로 while문을 빠져나갈 것이다.

2. break문을 만났어도 finally 블럭은 실행될 것이다.

.

.

.

.

.

.

정답은 2번이다.

위 코드의 실행 결과는 아래와 같다.

여기는 try 블럭
여기는 finally 블럭

 

그러면 아래의 경우에는 어떨까?

public class FinallyTest3 {
    public static void main(String[] args) {
        try {
            FinallyTest3 finallyTest = new FinallyTest3();
            finallyTest.returnInFinally();
        } catch (Exception e) {
            System.out.println("여기는 main 메소드의 catch 블럭");
        }
    }

    public void returnInFinally() {
        try {
            int n = 5 / 0;
            System.out.println("여기는 try 블럭");
        } catch (Exception e) {
            System.out.println("여기는 catch 블럭");
            throw e;
        } finally {
            System.out.println("여기는 finally 블럭");
        }
    }
}

catch 블럭에서 잡은 예외 객체를 그대로 다시 던져줬다.

그렇다면 이 코드의 실행 결과는 어떻게 될까?

1. Java의 메소드에서 예외가 발생하면 바로 메소드를 빠져나가므로 finally 블럭은 실행되지 않을 것이다.

2. 예외가 발생해도 메소드를 바로 빠져나가지 않고 finally 블럭이 실행될 것이다.

.

.

.

.

.

.

.

정답은 2번이다.

위 코드의 실행 결과는 아래와 같다.

여기는 catch 블럭
여기는 finally 블럭
여기는 main 메소드의 catch 블럭

 

그럼 마지막으로 아래의 코드를 보자.

public class FinallyTest4 {
    public static void main(String[] args) {
        try {
            FinallyTest4 finallyTest = new FinallyTest4();
            int result = finallyTest.returnInFinally();
            System.out.println("result: " + result);
        } catch (Exception e) {
            System.out.println("여기는 main 메소드의 catch 블럭");
        }
    }

    public int returnInFinally() {
        try {
            int n = 5 / 0;
            System.out.println("여기는 try 블럭");
            return 0;
        } catch (Exception e) {
            System.out.println("여기는 catch 블럭");
            throw e;
        } finally {
            System.out.println("여기는 finally 블럭");
            return 1;
        }
    }
}

앞의 코드에서 catch 블럭에서 예외가 발생한 경우에도 finally 블럭이 실행되었다고 했으니 finally 블럭의 "여기는 finally 블럭"이 출력될 것이다.

그러면 메인 메소드에서 catch 블럭에 걸려서 "여기는 main 메소드의 catch 블럭"을 출력할까? 아니면 result: 1을 출력할까?

1. returnInFinally 메소드에서 예외를 던졌으므로 main 메소드의 catch 블럭이 실행될 것이다.

2. finally에서 1을 return 했으니 main 메소드에서 정상적으로 1을 return 받고 catch 블럭에 들어가지 않는다.

.

.

.

.

.

.

.

.

정답은 2번이다.

위 코드의 실행 결과는 다음과 같다.

여기는 catch 블럭
여기는 finally 블럭
result: 1

 

https://docs.oracle.com/javase/tutorial/essential/exceptions/finally.html

 

The finally Block (The Java™ Tutorials > Essential Java Classes > Exceptions)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

위 링크는 Java의 공식 튜토리얼 사이트인데

예상치 못한 예외가 발생한 경우에도 finally 블럭이 실행되는 것을 보장한다.

return, continue, break로 인해 cleanup 코드가 실행되지 않는 일을 막을 수 있다.

라고 나와있다.

 

그러니 이 글의 초반에서 finally가 왜 필요할까? 하는 의문이 해소되었다.

return, continue, break, 예외 발생 등으로 메소드가 종료되어도 finally 블럭의 코드는 실행된다. (return 이후에 finally 블록이 실행된다)

return, break 등을 만나 메소드나 블록을 빠져나가는 경우에도 무조건 실행되어야 하는 코드가 있는 경우 매우 유용할 것 같다.

 

그런데 조금 놀란 것은 마지막 코드(FinallyTest4)에서 finally에서 return을 하는 바람에 앞에서 발생했던 예외가 무시되었다는 것이다.

하나의 메소드에서는 return과 예외를 던지기 2개를 모두 할 수 없다.

return을 하거나 예외를 던지거나 둘 중 하나만 가능하다.

 

위의 코드에서는 catch 블럭에서 예외를 throw 했으나 메소드 종료 전에 finally 블럭이 실행되면서 예외를 throw 하는 것이 아니라 1을 return 하게 되었다. finally문에서 return을 하는 바람에 throw한 Exception을 잃어버리게 된 것이다.

catch 블럭에서 예외를 던진 것이 아니라 return을 한 경우에도 해당 return 값은 무시되고 finally에서 return 한 값이 최종적으로 return 될 것이다.

try문에서 예외가 발생하지 않아 return 0; 부분이 실행되었더라도 0을 반환하는 것이 아니라 finally문에 걸려서 1을 반환할 것이다.

 

따라서 finally 블럭에서 return 하는 것은 의도치 않은 결과를 얻을 수 있으니 하지 않는 것을 권장한다.

인텔리제이에서도 'finally' block can not complete normally, 'return' inside 'finally' block 이라며 경고 메시지가 뜬다.

finally 블럭 안에서 return을 하는 것은 의도했다 하더라도 던져진 예외를 가릴 수 있고 디버깅을 어렵게 할 수 있다.

이제 finally 블럭 안에서 return을 하는 것은 안 된다는 것은 알았다.

그러면 아래와 같은 코드는 어떨까?

try 블럭 이후에 finally 블럭이 실행될텐데 5를 반환할까? 10을 반환할까?

public class FinallyTest5 {
    public static void main(String[] args) {
        FinallyTest5 finallyTest = new FinallyTest5();
        int result = finallyTest.returnInFinally();
        System.out.println("result: " + result);
    }

    public int returnInFinally() {
        int n = 5;
        try {
            System.out.println("여기는 try 블럭");
            return n;
        } finally {
            System.out.println("여기는 finally 블럭");
            n = 10;
        }
    }
}

위 코드의 실행결과는 아래와 같다.

여기는 try 블럭
여기는 finally 블럭
result: 5

finally 블럭에서 n을 10으로 바꿔줘도 5를 반환한다.

인텔리제이에서도 finally 블럭에서 n에 10을 할당해주는 부분에서 n은 회색으로 표시가 되고 아래와 같은 경고 메시지가 뜬다.

그럼 아래와 같이 참조자료형의 경우는 어떻게 될까?

public class FinallyTest6 {
    public static void main(String[] args) {
        FinallyTest6 finallyTest = new FinallyTest6();
        StringBuilder result = finallyTest.returnInFinally();
        System.out.println("result: " + result);
    }

    public StringBuilder returnInFinally() {
        StringBuilder str = new StringBuilder("five");
        try {
            System.out.println("여기는 try 블럭");
            return str;
        } finally {
            System.out.println("여기는 finally 블럭");
            str.setLength(0);
            str.append("ten");
        }
    }
}

StringBuilder 객체의 특정 인스턴스 변수를 "five"로 초기화했다가 try문에서 해당 객체를 return 하고 그 이후 finally에서 해당 필드의 값을 "ten"으로 바꿔줬다. (객체를 새로 생성해서 재할당 한 것이 아니라 객체는 그대로고 특정 필드의 값만 바꿨다.)

이 경우 실행 결과는 아래와 같다.

여기는 try 블럭
여기는 finally 블럭
result: ten

finally 블럭에서 값을 바꾼 것이 영향을 주었다.

 

인텔리제이에서도 finally 블럭에서 str이 회색 처리가 되지 않았다. (물론 finally 블럭에서 str = new StringBuilder("ten"); 이렇게 재할당을 했다면 str도 회색 처리가 되었을 것이다)

 

FinallyTest5처럼 기본 자료형인 경우와 FinallyTest6처럼 참조 자료형인 경우는 또 다르다.

return도 Java의 pass by value와 똑같다고 보면 된다.

이 사실을 염두에 두고 코드를 작성할 때 주의하면 좋을 것 같다.

 

그럼 finally 블록이 실행되지 않는 경우는 언제일까?

try 블럭이나 catch 블럭에서 이렇게 System.exit()을 하는 경우에는 finally 블럭이 실행되지 않는다.

System.exit(1);

 

System.exit() 메소드를 호출하면 아예 이 프로그램 자체가 종료되기 때문이다.

finally가 실행되지 않는 경우는 이 경우가 유일하다.

 

 

출처

https://stackoverflow.com/questions/3861522/do-you-really-need-the-finally-block

https://docs.oracle.com/javase/tutorial/essential/exceptions/finally.html

https://stackoverflow.com/questions/48088/returning-from-a-finally-block-in-java

https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.20.2

https://milkye.tistory.com/367

https://stackoverflow.com/questions/7297937/does-java-return-by-reference-or-by-value

https://stackoverflow.com/questions/4205362/java-beginner-returning-an-object-is-it-returned-as-a-constant-reference-o

Java 개발을 하다가 jdbc로 MySQL DB와 연결을 하는데 the last packet sent successfully to the server was 0 milliseconds ago 이런 에러가 나는 경우가 있다.

이 경우 DB URL 뒤에 ?useSSL=false을 추가해주면 된다.  MySQL은 SSL 설정이 default가 true인데 SSL 연결을 한다는 설정을 false로 바

것이다.

 

 

예를 들어 URL이 아래와 같다면

jdbc:mysql://어쩌구저쩌구.ap-northeast-2.rds.amazonaws.com:3306/my_db

아래와 같이 뒤에 추가해주면 된다.

jdbc:mysql://어쩌구저쩌구.ap-northeast-2.rds.amazonaws.com:3306/my_db?useSSL=false

파라미터가 여러 개라면 아래와 같이 &로 연결해주면 된다.

jdbc:mysql://어쩌구저쩌구.ap-northeast-2.rds.amazonaws.com:3306/my_db?useSSL=false&characterEncoding=UTF-8&serverTimeZone=Asia/Seoul

 

오늘 어떤 라이브러리가 필요해서 Gradle 파일에 의존성을 추가해주었다.

이 코끼리를 클릭하여 Sync도 잘 해서 빌드도 성공적으로 했다.

 

그런데 프로그램을 실행하려고 하니

Execution failed for task ':compileJava'. > Could not resolve all files for configuration ':compileClasspath'. > Could not find com.github.shin285:KOMORAN:3.3.4. Searched in the following locations: - https://repo.maven.apache.org/maven2/com/github/shin285/KOMORAN/3.3.4/KOMORAN-3.3.4.pom If the artifact you are trying to retrieve can be found in the repository but without metadata in 'Maven POM' format, you need to adjust the 'metadataSources { ... }' of the repository declaration. Required by: project : Possible solution: - Declare repository providing the artifact, see the documentation at https://docs.gradle.org/current/userguide/declaring_repositories.html

 

이런 에러가 났다.

 

찾아보니 아래와 같이 Gradle 파일의 allproject의 repositories에 아래 코드를 추가해야 한다고 한다.

maven { url 'https://jitpack.io' }

 

allprojects {
    repositories {
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
    }
}

이렇게 코드를 추가하니 오류가 발생하지 않고 잘 돌아갔다.

 

출처: https://www.python2.net/questions-666798.htm

Java에는 Javadoc이라는 문서화 툴이 있어서 문서를 쉽게 만들 수 있다.
사용법은 간단하다. 변수, 메소드, 클래스 선언부 바로 위에 /**로 시작하는 여러줄 주석을 달면 된다.
/** 까지만 치면 인텔리제이가 알아서 자동완성해준다.

패키지 주석은 해당 패키지에 package-info.java라는 파일을 만들고 그 파일에 주석을 달면 된다.
인텔리제이에서는 패키지에 오른쪽 마우스를 클릭하고 New를 누르면 package-info.java를 만들 수 있게 되어있다.

그러면 이렇게 생겼을 것이다.

이 코드 위에 여러줄 주석으로 패키지에 대한 설명을 달면 된다.


클래스 주석은

이렇게 달면 된다.
만약 lombok과 같은 어노테이션이 있다면 아래와 같이 어노테이션 위에 주석을 달면 된다.

메소드 주석은

이렇게 달면 된다. 메소드를 완성하고 /** 까지만 치면 @param, @return, @throws까지 자동으로 만들어준다.
맨 위에는 메소드에 대한 설명을 적으면 된다. (급조하느라 아주 말도 안 되는 이상한 메소드이다.)
@param에는 매개변수에 대한 설명을, @return에는 반환값에 대한 설명을, @throws에는 던지는 예외에 대한 설명을 적는다.

주석을 다 달았으면 이제 문서를 생성하면 된다.
Tools에 Generate JavaDoc을 클릭한다.


그리고 한글 깨짐 방지를 위해 Other command line arguments에

-encoding UTF-8 -charset UTF-8 -docencoding UTF-8

을 입력한다.


그리고 OK를 누른다.

그러면

이렇게 생성이 되는 것을 볼 수 있다.

아까 Open generated documentation in browser에 체크를 했기 때문에 문서가 바로 웹 브라우저로 열릴 것이다.
만약 열리지 않는다면 아까 지정해준 경로에서 index.html을 열면 된다.

자바 공식 문서와 똑같이 생겨서 굉장히 그럴듯해보인다.

문서를 만드는 또 다른 방법이 있다.
build.gradle에

javadoc { 
    source = sourceSets.main.java.srcDirs 
    options.encoding = 'UTF-8' 
}

다음과 같은 코드를 적어준다.

그러고 나서 터미널을 열어준다.

경로가 해당 프로젝트 디렉터리인지 꼭 확인한다.
여기에서

이렇게 입력하고 엔터를 치면

성공적으로 문서가 만들어진다.

만들어진 문서는 build 폴더의 docs 안에 javadoc 안에 있다.

그런데 이렇게 문서를 만들고 나서 보니 lombok(롬복)의 @Getter, @Setter, @ToString, @NoArgsConstructor, @AllArgsConstructor 등으로 만들어진 메소드는 문서에 나타나지 않는 것을 알 수 있을 것이다.

이는 delombok을 이용하여 간단히 해결할 수 있다.
build.gradle에 다음과 같은 코드를 추가한다. 아까 build.gradle에 javadoc 부분을 추가했었다면 그 부분을 아래와 같이 변경한다.

javadoc {
    dependsOn delombok 
    source = file("$buildDir/delombok") 
    options.encoding = 'UTF-8' 
    failOnError = false 
}

그러고 나서

을 하면 된다.

이런 String 배열이 있을 때

public class ArrayTest {
    public static void main(String[] args) {
        String[] arr = {"apple", "banana", "cherry", "donut"};
        System.out.println(arr);
    }
}

이 배열을 출력하면

이렇게 이상한 글자가 출력된다.

public class ArrayTest {
    public static void main(String[] args) {
        String[] arr = {"apple", "banana", "cherry", "donut"};
        System.out.println(arr.toString());
    }
}

이렇게 arr.toString()을 출력해도 결과는 똑같다. toString()을 출력하는 것은 어떤 변수를 그냥 출력했을 때와 같다.

 

일반 배열은 그냥 출력하면 이렇게 이상하게 출력된다.

그럴 때 사용하는 것이 Arrays 클래스의 static 메소드 toString()이다.

import java.util.Arrays;

public class ArrayTest {
    public static void main(String[] args) {
        String[] arr = {"apple", "banana", "cherry", "donut"};
        System.out.println(Arrays.toString(arr));
    }
}

그러면 이렇게 우리가 원하는 대로 출력되는 것을 볼 수 있다.

 

Arrays와 같이 s로 끝나는 클래스들은 대부분 유용한 static 메소드들이 들어있는 유틸리티 클래스이다.

Arrays 클래스에서 자주 쓰이는 유용한 정적 메소드 중 하나가 sort() 메소드이다.

static 메소드이기 때문에 메소드 앞에 클래스이름.을 꼭 붙여서 Arrays.sort() 이렇게 사용해야 한다.

String[] arr = {"cherry", "Banana", "apple", "donut"};

이렇게 문자열들이 다 섞여있는 배열을 Arrays.sort() 메소드를 이용해서 정렬해보자.

import java.util.Arrays;

public class ArrayTest {
    public static void main(String[] args) {
        String[] arr = {"cherry", "Banana", "apple", "donut"};
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

정렬이 되긴 했지만 대문자가 소문자보다 유니코드 값이 더 작기 때문에 Banana가 가장 앞에 온다.

대소문자를 구분하지 않고 정렬하고 싶다면 Arrays.sort() 메소드의 두 번째 인자에 Comparator 객체를 비교기준으로 넣어주면 된다.

import java.util.Arrays;

public class ArrayTest {
    public static void main(String[] args) {
        String[] arr = {"cherry", "Banana", "apple", "donut"};
        Arrays.sort(arr, String::compareToIgnoreCase);
        System.out.println(Arrays.toString(arr));
    }
}

그러면 이렇게 대소문자를 무시하고 알파벳 순서대로 정렬된 것을 볼 수 있다.

위 코드는 메소드 참조를 이용한 것이고 람다식을 이용하고 싶다면 다음과 같이 하면 된다.

import java.util.Arrays;

public class ArrayTest {
    public static void main(String[] args) {
        String[] arr = {"cherry", "Banana", "apple", "donut"};
        Arrays.sort(arr, (a, b) -> a.compareToIgnoreCase(b));
        System.out.println(Arrays.toString(arr));
    }
}

이 코드를 실행해도 결과는 같다.

람다식과 메소드 참조를 잘 모른다면 아래 두 글을 먼저 읽어보는 것을 추천한다.

https://breakcoding.tistory.com/4

 

[Java] 람다식(lambda expression)과 메소드 참조(method reference)

C/C++에는 함수포인터라는 개념이 있어 함수를 다른 함수로 전달하고 싶을 때에는 함수 포인터를 사용하면 된다. 그런데 자바는 C/C++보다 더 객체지향적인 언어이기 때문에 메소드(C/C++로 따지면 함수)는 무조건..

breakcoding.tistory.com

https://breakcoding.tistory.com/183

 

[Java] 메소드 참조 (Method Reference)

이 글에 이어지는 글이다. (↓링크) [Java] 람다식(lambda expression)과 메소드 참조(method reference) C/C++에는 함수포인터라는 개념이 있어 함수를 다른 함수로 전달하고 싶을 때에는 함수 포인터를 사용..

breakcoding.tistory.com

람다식이나 메소드 참조를 이용하고 싶지 않다면 Comparator 클래스를 구현한 클래스를 만들어서 객체를 생성해서 sort() 메소드의 두 번째 인자로 넣어줄 수도 있다.

import java.util.Arrays;
import java.util.Comparator;

public class ArrayTest {
    public static void main(String[] args) {
        String[] arr = {"cherry", "Banana", "apple", "donut"};
        Arrays.sort(arr, new MyComparator());
        System.out.println(Arrays.toString(arr));
    }
}
class MyComparator implements Comparator<String> {
    @Override
    public int compare(String o1, String o2) {
        return o1.compareToIgnoreCase(o2);
    }
}

이렇게 해도 결과는 똑같다.

 

그런데 역시 정적 배열이라서 출력할 때도 귀찮고 중간에 원소를 삭제하고 싶을 때 곤란하다. 따라서 이런 정적 배열을 동적배열(리스트)로 바꾸고 싶다면 어떻게 해야 할까?

이것 역시 유틸리티 클래스인 Arrays에 있는 static 메소드 asList() 메소드를 이용하면 된다.

import java.util.Arrays;
import java.util.List;

public class ArrayTest {
    public static void main(String[] args) {
        String[] arr = {"apple", "banana", "cherry", "donut"};
        List<String> list = Arrays.asList(arr);
    }
}

이렇게 하면 배열을 동적 배열인 List로 쉽게 바꿀 수 있다.

 

이 List를 ArrayList로 바꾸고 싶다면 다음과 같이 하면 된다.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ArrayTest {
    public static void main(String[] args) {
        String[] arr = {"apple", "banana", "cherry", "donut"};
        List<String> list = Arrays.asList(arr);
        ArrayList<String> arrayList = new ArrayList<>(list);
    }
}

이렇게 ArrayList의 생성자의 인자로 넣어주면 된다.

동적 배열 ArrayList로 바꾸고 나면 정적 배열이었을 때보다 훨씬 할 수 있는 것들이 많다. 특히 스트림을 이용해서 굉장히 편리하게 처리할 수 있는 것들이 많아진다.

StringTokenizer는 구분자를 기준으로 문자열을 분리하고 싶을 때 클래스이다.

구분자란 무엇일까?

"양파,당근,마늘,오이,상추"라는 문자열이 있을 때 콤마(,)를 기준으로 나누고 싶다면 콤마(,)가 구분자이다.

"2020년 02월 15일"이라는 문자열이 있을 때 공백을 기준으로 나누고 싶다면 공백이 구분자이다.

"2020-02-15"라는 문자열이 있을 때 '-' 문자를 기준으로 나누고 싶다면 구분자는 '-'이다.

 

일단 문자열을 쪼개고 싶다면 StringTokenizer 객체를 생성해야 한다. StringTokenizer 객체를 생성할 때에는 나누고 싶은 문자열과 구분자를 적어야 한다. 구분자를 적어주지 않으면 공백을 기준으로 나눈다.

String s = "2020년 2월 15일";
StringTokenizer tokenizer = new StringTokenizer(s); //공백을 기준으로 문자열 s를 나누기 위한 객체

공백을 기준으로 문자열을 나누고 싶다면 이렇게 생성자의 인자로 문자열만 넣어주면 된다.

 

String s = "2020-02-15";
StringTokenizer tokenizer = new StringTokenizer(s, "-");

특정 문자열을 기준으로 나누고 싶다면 문자열과 구분자를 생성자의 인자로 넣어주면 된다.

 

이렇게 StringTokenizer 객체를 생성하고 나면 문자열을 쪼개기 위한 준비작업은 다 끝났다.

이제 문자열을 나누면 된다.

 

일단 StringTokenizer 클래스의 메소드부터 살펴보자.

int countTokens() 남아있는 토큰의 개수를 반환한다.
boolean hasMoreTokens() 남아있는 토큰이 있으면 true, 더 이상 토큰이 없으면 false
String nextToken() 토큰을 꺼내온다.

https://docs.oracle.com/javase/10/docs/api/java/util/StringTokenizer.html

 

StringTokenizer (Java SE 10 & JDK 10 )

Constructs a string tokenizer for the specified string. All characters in the delim argument are the delimiters for separating tokens. If the returnDelims flag is true, then the delimiter characters are also returned as tokens. Each delimiter is returned a

docs.oracle.com

이 메소드들의 사용법은 직접 코드를 보면서 배우는 것이 가장 빠를 거라고 생각한다.

import java.util.StringTokenizer;

public class StringTokenizerTest {
    public static void main(String[] args) {
        String s = "2020-02-15";
        StringTokenizer tokenizer = new StringTokenizer(s, "-");
        System.out.println("토큰의 개수: " + tokenizer.countTokens());
        while(tokenizer.hasMoreTokens()) {
            System.out.println(tokenizer.nextToken());
            System.out.println("토큰의 개수: " + tokenizer.countTokens());
        }
    }
}

 

 

 

 

import java.util.StringTokenizer;

public class StringTokenizerTest {
    public static void main(String[] args) {
        String s = "안녕? 여기는 feelcoding 블로그야";
        StringTokenizer tokenizer = new StringTokenizer(s);
        System.out.println("토큰의 개수: " + tokenizer.countTokens());
        while(tokenizer.hasMoreTokens()) {
            System.out.println(tokenizer.nextToken());
        }
    }
}

 

위 코드의 실행 결과

 

 

 

import java.util.StringTokenizer;

public class StringTokenizerTest {
    public static void main(String[] args) {
        String s = "2020-02-15";
        StringTokenizer tokenizer = new StringTokenizer(s, "-");
        System.out.println("토큰의 개수: " + tokenizer.countTokens());
        while(tokenizer.hasMoreTokens()) {
            System.out.println(tokenizer.nextToken());
        }
    }
}

 

위 코드의 실행 결과

 

 

 

import java.util.StringTokenizer;

public class StringTokenizerTest {
    public static void main(String[] args) {
        String s = "2020-02-15";
        StringTokenizer tokenizer = new StringTokenizer(s, "-");
        System.out.println("토큰의 개수: " + tokenizer.countTokens());
        while(tokenizer.hasMoreTokens()) {
            System.out.println(tokenizer.nextToken());
            System.out.println("토큰의 개수: " + tokenizer.countTokens());
        }
        tokenizer.nextToken(); //다 소모한 객체에서 토큰을 뽑아내려고 시도. 예외 발생
    }
}

StringTokenizer 객체는 소모적이라서 한 번 사용하면 다시 사용할 수 없다.

다시 사용하고 싶다면 객체를 새로 만들어야 한다. 이미 소모한 객체에서 토큰을 꺼내려고 하면 다음과 같이 NoSuchElementException 예외가 발생한다.

 

위 코드의 실행결과. 예외가 발생한다.

 

13번째 줄에서 예외가 발생했다고 하는데 그것은 마지막 코드인 tokenizer.nextToken();이다.

따라서 nextToken() 메소드를 사용하기 전에는 반드시 if(객체.hasMoreTokens()) 또는 if(객체.countTokens() > 0)으로 체크해보고 토큰을 뽑아야 한다.

이 글에 이어지는 글이다. (↓링크)

 

[Java] 람다식(lambda expression)과 메소드 참조(method reference)

C/C++에는 함수포인터라는 개념이 있어 함수를 다른 함수로 전달하고 싶을 때에는 함수 포인터를 사용하면 된다. 그런데 자바는 C/C++보다 더 객체지향적인 언어이기 때문에 메소드(C/C++로 따지면 함수)는 무조건..

breakcoding.tistory.com

람다식은 불필요한 코드를 생략하고 메소드 내부 코드만 작성해서 간단하게 인터페이스 객체를 생성하는 방법이었다. 

하지만 람다식도 불필요한 부분들이 있다. 다음 코드를 보자.

interface ReturnNumber {
    void function(int n);
}
public class LambdaTest {
    public static void main(String[] args) {
        printNumber(x -> System.out.println(x), 10); //받은 수를 그대로 반환하는 ReturnNumber 객체를 만들어서 넘겨줌
    }
    static void printNumber(ReturnNumber returnNumber, int num) {
        returnNumber.function(num);
    }
}

이 코드를 보면 x -> System.out.println(x)라는 람다식으로 ReturnNumber 객체를 만들어서 메소드의 인자로 넘겨줬다.

하지만 이 람다식은 너무 간단하다. 받은 것을 그대로 출력하라는 것인데 x를 두 번이나 썼다. 이 x를 불필요한 부분이라고 생각해서 이런 간단한 코드들은 메소드참조를 써서 더욱 더 간결하게 만들 수 있게 했다.

interface ReturnNumber {
    void function(int n);
}
public class LambdaTest {
    public static void main(String[] args) {
        printNumber(System.out::println, 10); //받은 수를를 그대로 반환하는 ChangeNumber 객체를 만들어서 넘겨줌
    }
    static void printNumber(ReturnNumber returnNumber, int num) {
        returnNumber.function(num);
    }
}

이렇게 System.out::println만 해주면 인자를 받아서 그 인자를 출력하도록 하는 메소드를 구현한 것이다.

람다식인 x -> System.out.println(x)보다 훨씬은 아니지만 메소드 참조를 사용하니 조금 더 간결해졌다.

 

다음 코드를 보자.

public class LambdaTest {
    public static void main(String[] args) {
        String[] names = {"Bill", "jane", "Anne", "billy", "Tom", "jake", "amily", "Susan"};
        Arrays.sort(names);
        for(String s : names)
            System.out.print(s + " ");
    }
}

대소문자가 구별되어 정렬됨

이렇게 Arrays.sort() 메소드를 사용해서 배열을 정렬하면 자동으로 오름차순 정렬이 되는데 문자열의 경우 유니코드의 값을 기준으로 오름차순 정렬을 하기 때문에 소문자는 무조건 대문자보다 뒤에 오게 된다.

 

만약 이것이 싫다면 내가 원하는 정렬기준으로 compare​(T o1, T o2) 메소드를 구현한 Comparator 인터페이스를 객체를 Arrays.sort() 메소드의 두 번째 인자로 넣어주면 된다. 그런데 String 클래스에 대소문자 구별없이  이미 만들어져있는 메소드가 있다. 그 메소드를 부르면 Comparator 객체를 반환하는 것은 아니고 그 메소드의 반환값을 compareTo(T o1, T o2) 메소드의 반환값으로 구현만 해주면 된다. String 클래스에 있는 compareToIgnoreCase() 메소드이다.

https://docs.oracle.com/javase/10/docs/api/java/lang/String.html

 

String (Java SE 10 & JDK 10 )

Compares two strings lexicographically. The comparison is based on the Unicode value of each character in the strings. The character sequence represented by this String object is compared lexicographically to the character sequence represented by the argum

docs.oracle.com

compareTo() 메소드는 Comparator 인터페이스의 유일한 추상메소드이기 때문에 람다식 사용이 가능하다.

(람다식은 인터페이스에 추상메소드가 딱 1개만 있을 때 사용 가능하다)

https://docs.oracle.com/javase/10/docs/api/java/util/Comparator.html

 

Comparator (Java SE 10 & JDK 10 )

Compares its two arguments for order. Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second. The implementor must ensure that sgn(compare(x, y)) == -sgn(compare(y, x)) for all x and

docs.oracle.com

import java.util.Arrays;

public class LambdaTest {
    public static void main(String[] args) {
        String[] names = {"Bill", "jane", "Anne", "billy", "Tom", "jake", "amily", "Susan"};
        Arrays.sort(names, (a, b) -> a.compareToIgnoreCase(b));
        for(String s : names)
            System.out.print(s + " ");
    }
}

이렇게 람다식으로 Arrays.sort() 메소드의 두 번째 인자에 대소문자를 구분 안 하는 Comparator 객체를 넣어주면

 

이렇게 대소문자 구별 없이 정렬된 것을 볼 수 있다.

하지만 (a, b) -> a.compareToIgnoreCase(b)에서 a, b가 두 번씩 사용되어 불필요한 코드라고 생각해서 이것도 메소드 참조로 표현할 수 있도록 했다.

import java.util.Arrays;

public class LambdaTest {
    public static void main(String[] args) {
        String[] names = {"Bill", "jane", "Anne", "billy", "Tom", "jake", "amily", "Susan"};
        Arrays.sort(names, String::compareToIgnoreCase);
        for(String s : names)
            System.out.print(s + " ");
    }
}

String::compareToIgnoreCase 이렇게 메소드 참조를 사용하면 코드가 좀 더 간결해졌다.

이 코드의 실행결과는 위 코드의 실행결과와 정확히 똑같다.

 

그리고 스트림에서 특히 람다식과 메소드 참조가 정말 유용하게 사용되는데

list.removeIf(x -> Objects.isNull(x));

이런 코드도 메소드 참조를 이용하면

list.removeIf(Objects::isNull);

이렇게 더 간결하게 줄일 수 있다.

 

list.forEach(x -> System.out.println(x));

이런 코드도 메소드 참조를 이용하면

list.forEach(System.out::println);

이렇게 줄일 수 있다.

 

메소드 참조의 규칙을 정리하자면 다음과 같다.

  람다식 메소드 참조
static 메소드 a ->클래스이름.메소드(a) 클래스이름::메소드이름
인스턴스 메소드 (a, b) -> a.메소드(b) 클래스이름::메소드이름
(a) -> 객체.메소드(a) 객체::메소드이름
생성자 (a) -> new 생성자(a) 생성자이름::new
배열 생성자 (a) -> new 타입[a] 타입::new

첫 번째 예제 코드에서 배운 a -> System.out.println(a)를 System.out::println으로 줄이는 것은 이 표에서 3번째 즉, 인스턴스 메소드에서 (a) -> 객체.메소드(a)를 객체::메소드이름으로 바꾸는 것에 해당한다. (System.out은 표준 출력을 담당하는 객체이다.)

두 번째 예제 코드에서 배운 a.compareToIgnoreCase(b)를 String::compareToIgnoreCase로 바꾸는 것은 이 표에서 두 번째 즉, (a, b) -> a.메소드(b) 에서 클래스이름::메소드이름으로 바꾸는 것에 해당한다.

 

생성자와 배열 생성자를 메소드 참조로 바꾸는 예를 살펴보자. 스트림을 배우지 않았다면 낯설 수 있지만 그래도 메소드 참조를 어떻게 사용하는지 감이 올 것이다.

import java.util.ArrayList;
import java.util.Arrays;

public class LambdaTest {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("Peter");
        names.add("Paul");
        names.add("Mary");
        Employee[] employees = names.stream().map(Employee::new).toArray(Employee[]::new);
        System.out.println(Arrays.toString(employees));
    }
}
class Employee {
    String name;

    public Employee(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }
}

String 타입의 ArrayList names를 메소드 참조를 이용해서 배열로 바꿔주었다. Employee 클래스의 생성자와 Employee[] 배열 생성자를 메소드 참조로 표현했다.

조금 어려울 수도 있는데 그러면 그냥 람다식을 사용하면 되고 사실 메소드 참조는 아까 봤던 예제 정도가 자주 쓰인다.

+ Recent posts