카테고리 없음

Java Features

머룽 2023. 4. 23. 14:06
요즘들어 Kotlin만 사용해서 Java의 새로운 문법? 기능들에 관심이 없었다. 그래서 오늘은 자바11~17까지의 문법적인 변화에 대해 알아보도록 하자. 대부분 문법적인 부분들만 살펴볼 예정이니 자세한 내용들은 해당 공식문서를 살펴보면 좋겠다.

Local-Variable Syntax for Lambda Parameters

Java 10부터 지역변수에 var 키워드를 통해 타입추론을 할 수 있었다. 그런데 Lambda 표현식의 변수엔 사용 불가 했다. 하지만 이제는 Lambda expression에서도 변수에 var 키워드를 사용가능하다.
Stream.iterate(1, (var number) -> number + 1).limit(10).collect(Collectors.toList())
만약 변수가 2개이상일 경우엔 혼합해서 사용하면 안된다.
BiConsumer<Integer, Integer> foo = (var b1, Integer b2) -> {};
해당 코드는 올바르지 않다.

http client

java.net 의 HttpClient가 java 11부터 정식으로 표준화 되었다. java9에서 인큐베이팅 된 후 두번의 업그레이드 후 출시 되었다. 기존의 HttpURLConnection들은 사용하기 어렵고 사용자 친화적이지 않다. 그래서 다른 써드 라이브러리들을 많이 사용해왔다. 그러나 이제는 조금 편리하고 사용자 친화적인 HttpClient를 사용하면 된다.
public static void main(String[] args) throws Exception {

    var httpClient = HttpClient.newBuilder()
            .build();
    var httpRequest = HttpRequest.newBuilder()
            .GET()
            .uri(URI.create("https://httpstat.us/200"))
            .build();
    var httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
    System.out.println(httpResponse.body());

}
근데 사용할 일이 있나 모르겠다.

Pattern Matching for instanceof

java 14부터 진행되고 있는 instanceof Pattern Matching 이다.

if (records instanceof Records) { var r = (Records) records; // use r }
이전에는 위와 같이 instanceof로 비교한 후 해당 타입으로 캐스팅을 했다. 어쩌면 불필요한 코드였을지도 모른다. 그러나 이제는 그럴필요 없다.
if (records instanceof Records r) {
    // use r 
} else {
    // cant use r
}
이제는 해당 타입의 캐스팅하지 않고 바로 사용할 수 있다. 만약 해당 조건에 만족하지 않으면 사용할 수 없다.
if (!(records instanceof Records r)) {
    // cant use r
} else {
    // use r 
}
해당 변수로 조건문을 추가 할 수도 있다.
if (records instanceof Records r && r.name.length() > 10) {
  // use r
}
편리한 기능이 추가 되었다.

Switch Expressions and Pattern Matching

java 12 부터 Switch Expressions을 사용할 수 있고 java17 부터 Pattern Matching을 사용할 수 있다. 이전에 switch문은 실수의 여지도 크며 불필요한 코드가 많았다.
public static void getTransportPrint(Transport transport) {

        switch (transport) {
            case BUS:
                System.out.println("take a bus");
                break;
            case TAXI:
                System.out.println("take a taxi");
                break;
            case SUBWAY:
                System.out.println("take a subway");
                break;
            default:
                System.out.println("walking");
                break;

        }
    }
break키웓드를 실수로 넣지 않으면 버그가 나오기 싶다. 또한 break키워드는 불필요한 코드일지도 모른다. 이제는 간단한 표현식으로 바꿀수도 있다.
public static void getTransportPrint(Transport transport) {

    switch (transport) {
        case BUS -> System.out.println("take a bus");
        case TAXI -> System.out.println("take a taxi");
        case SUBWAY -> System.out.println("take a subway");
        default -> System.out.println("walking");
    }
}
실수의 여지도 적으며 코드도 간결해졌다. 물론 case에 여러 조건도 가능하다.
switch (transport) {
    case BUS, TAXI -> System.out.println("take a bus and taxi");
    case SUBWAY -> System.out.println("take a subway");
    default -> System.out.println("walking");
}
표현식이라 리턴도 받을 수 있다.
public static int getTransportNumber(Transport transport) {
    return switch (transport) {
        case BUS, TAXI -> 1;
        case SUBWAY -> 2;
    };
}
추가적으로 yield 키워드도 추가되었다. case문에 추가적인 코드가 들어갈때 사용하면 된다.
public static int getTransportOrdinal(Transport transport) {
    return switch (transport) {
        case BUS -> 0;
        case TAXI -> {
            var ordinal = transport.ordinal();
            // bla bla            
            yield 1200;
        }
        case SUBWAY -> {
            // bla bla
            yield 8000;
        }
    };
}
이전에는 타입체크를 할 경우 if문과 instanceof를 사용해 체크를 했다.
public static double getTypeNumberSwitch(Object o) {
    var result = 0d;
    if (o instanceof Number) {
        var i = (Integer) o;
        result = i.doubleValue();
    } else if (o instanceof String) {
        var s = (String) o;
        result = Double.parseDouble(s);
    }
    return result;
}
불필요한 코드들도 많고 result에 계속 어싸인을 하고 있어 실수의 여지도 크다. 이제는 그럴 필요 없다.
public static double getTypeNumberSwitch(Object o) {
    return switch (o) {
        case Number i -> i.doubleValue();
        case String s -> Double.parseDouble(s);
        default -> 0;
    };
}
간단하면서 한눈에 알아 볼수 있는 코드가 되었다.

Text Blocks

java 13 부터 나온 기능이다. 다른언어는 진작에 있었긴 한데 그래도 나오니 한결 나은거 같다. 멀티 라인의 string을 손쉽게 작성할 수 있다. 이전에는 멀티라인의 string 컨트롤하기 힘들었다. 예를들어 json을 예쁘게 만들고 싶다면 아래와 같이 해야 했다.
final var text = "{\
" +
        "\\t\\"name\\" : \\"wonwoo\\",\
" +
        "\\t\\"address\\": \\"Carson, CA, 90746\\"\
" +
        "}";
System.out.println(text);
보기만 해도 힘들다. 만들긴 더 힘들다. 그러나 이제는 손쉽게 만들수 있다.
final var text = """
        {
            "name" : "wonwoo",
            "address": "Carson, CA, 90746"
        }
        """;
마지막 행으로 공백이 결정되므로 마지막행의 위치가 중요하다.
final var text = """
            {
                "name" : "wonwoo",
                "address": "Carson, CA, 90746"
            }
        """;
위와 같이 작성했다면 아래와 같이 출력된다.
|    {
|        "name" : "wonwoo",
|        "address": "Carson, CA, 90746"
|    }
| 해당 표시로 여백을 표시하였다. 만약 text block 에는 여러줄을 표시하고 한줄로 출력이 되게 하고 싶다면 \\(역슬래스)를 추가적으로 넣으면 된다.
final var text = """
        Lorem ipsum dolor sit amet, consectetur adipiscing \\
        elit, sed do eiusmod tempor incididunt ut labore \\
        et dolore magna aliqua.\\
        """;
출력시에는 한줄로 표기 된다. 여러 이스케이프문자들이 있는데 이건 문서를 찾아보자! 아쉬운게 하나 있는데 interpolation이 없다. 잠깐 보기에는 향후에 추가 될 수도 있다고 하니 기다려보자. 아쉬운대로 몇가지 방법으로 해결 할 수 있다. replace, String.format와 text block을 지원하기 위해 추가된 String.formatted을 사용하면 된다. replace와 String.format은 이전부터 있었으니 생략하고
var text = """
        red
        green
        blue
        %s
        """.formatted("white");
내부적으로는 String.format과 동일하다.

Record

java 14부터 추가된 Record는 값 타입을 쉽게 표현할 수 있는 기능이다. 자바는 쓸때없이 너무 장황하다라는 말이 많다. 값 타입의 클래스를 한번 만드려면 생성자, 접근자, equals, hashCode, toString, getter 등 너무 불필요하게 코드들을 작성해야 한다. Record를 사용하면 이제 그럴 필요 없다.
public record Records(String name, String address) {
}
이제는 위와 같이만 해도 생성자, 접근자, equals, hashCode, toString, getter? 등이 만들어 진다. 추가적으로 생성자를 작성할 수 있다.
public Records(String name) {
    this(name, "1528 Fillmore st, San Francisco");
}
Compact 생성자를 만들수 있다. Compact생성자는 지루한 변수할당없이 로직에 집중할 수 있도록 도와준다.
record Records(String name, String address) {

    public Records {
        if (name == null) {
            throw new IllegalArgumentException("name must be not null!");
        }
        if (address == null) {
            throw new IllegalArgumentException("addrss must be not null!");
        }
    }
}
예를들어 위의 코드는 name 과 address의 대한 유효성을 체크하는 부분이다. 혹은 다음과 같이 변수할당없이 로직에만 집중할 수 있게 도와주기도 한다.
record Rational(int num, int denom) {
    Rational {
        int gcd = gcd(num, denom);
        num /= gcd;
        denom /= gcd;
    }
}
이는 다음과 같은 코드이다.
record Rational(int num, int denom) {
    Rational(int num, int demon) {
        // Normalization
        int gcd = gcd(num, denom);
        num /= gcd;
        denom /= gcd;
        // Initialization
        this.num = num;
        this.denom = denom;
    }
}
몇가지 제약조건들이 있다. 예를들어 extends절이 없고, final class 이며 abstract도 할 수 없다. 더 많은 제약 조건이 있으니 문서를 참고하면 되겠다.

Sealed Class

kotlin의 Sealed 클래스랑 문법만 다르지 거의 동일하다. Pattern Matching에도 사용할 수 있다. 예제부터 보자.

sealed interface TransportSealed permits BusSealed, TaxiSealed { } sealed class BusSealed implements TransportSealed { public String getBus() { return "take a bus"; } } final class TaxiSealed implements TransportSealed { public String getTaxi() { return "take a taxi"; } } final class ExpressBus extends BusSealed { @Override public String getBus() { return super.getBus() + " Express"; } }
sealed class 는 sealed 키워드를 통해 만들수 있으니 봉인된 클래스를 permits 키워드를 통해 선언 할 수 있다. permits에 없는 클래스는 사용할 수 없다. 봉인된 클래스에는 non-sealed, sealedfinal, 세 키워드중에 하나를 선택해야만 한다. non-sealed 은 상속이 가능하다. final 은 상속이 불가하다. sealed은 하위 클래스가 있어야 한다.
static void getTransport(TransportSealed transportSealed) {

    switch (transportSealed) {
        case ExpressBus expressBus -> System.out.println(expressBus.getBus());
        case BusSealed bus -> System.out.println(bus.getBus());
        case TaxiSealed taxi -> System.out.println(taxi.getTaxi());
    }
}
위와 같이 switch을 이용해서 Pattern Matching도 할 수 있다. switch 문에 봉인된 클래스가 하나라도 없으면 컴파일에러가 발생한다. 또한 record 클래스에도 사용할수 있으니 참고하면 되겠다.

Miscellaneous

Helpful NullPointerExceptions

NullPointException의 message가 좀 더 명확하게 나온다.
private static void lowerCase(String str) {
    System.out.println(str.toLowerCase());
}

String str = null;
//Null Point Exception
lowerCase(str);
위와 같은 코드가 있을 경우 정확히 어디에서 에러가 발생한지 메시지로 통해 확인할 수 있다.
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toLowerCase()" because "str" is null
사실 위의 코드는 굳이 메시지 말고도 라인만 봐도 명확하다. 하지만 좀 더 복잡한 경우에는 도움이 많이 될 것 같다. 아래와 같이 말이다.
lowerCase(a.b.c.i);
만약 b가 null이라면 메시지는 다음과 같다.
Exception in thread "main" java.lang.NullPointerException: Cannot read field "c" because "a.b" is null

toList

Strem을 자주 사용한다면 많이 사용되는 terminal operation .collect(Collectors.toList())을 간편하게 줄일 수 있다.
Stream.of(1, 2, 3, 4, 5).toList();
귀찮게 collect를 사용하지 않아도 바로 List로 만들 수 있다. 필자가 알아본건 여기까지다. 물론 더 많은 내용이 있으니 공식문서들을 잘 살펴보면 되겠다. 사용법들을 알았으니 좀 더 각자 딥하게 들어가는 것도 좋겠다. 필자는 언젠가 다시 자바를 사용할 수 있으니 그때 더 딥하게 알아보자.