bsj805 / study_doc

java 8버전에서 17버전으로의 변화
0 stars 0 forks source link

java 17버전 개요 #1

Open bsj805 opened 2 years ago

bsj805 commented 2 years ago

1. JAVA 17이 무엇인가

java 8버전, 11버전 이후 2021년 9월 출시된 LTS 버전.

2. 실제로 자바 8 에서 17로 바뀐 것은?

velog- java17로의 전환을 고려하는 이유

3. java 8 에서 java 17로 변경해야 하는가?

장점

bsj805 commented 2 years ago

java 17 vs java 8 - the changes 실제로 모든 변화를 파악하고 싶다면 JEP를 보라. (JDK Enhancement Proposals) https://openjdk.java.net/jeps/0

API 변화를 찾으려면 https://javaalmanac.io/ 를 참고하라.

1. 새로 등장한 var keyworld가 존재한다.

// java 8 way
Map<String, List<MyDtoType>> myMap = new HashMap<String, List<MyDtoType>>();
List<MyDomainObjectWithLongName> myList = aDelegate.fetchDomainObjects();

// java 10 way
var myMap = new HashMap<String, List<MyDtoType>>();
var myList = aDelegate.fetchDomainObjects()

좀 더 간결하게 변수를 선언하는 방법을 제시한다. 다만, 타입을 숨기는것이 안좋은 때도 있을 것이다. 잘 가려서 사용해야 한다. 항상 불편했던 건 사실 람다인데,

// causes compilation error: 
//   method reference needs an explicit target-type
var fun = MyObject::mySpecialFunction;

람다 함수를 담아 놓으려면 에러가 난다. method에 대한 reference는 target-type이 있어야 한다고 한다.

boolean isThereAneedle = stringsList.stream()
  .anyMatch((@NonNull var s) -> s.equals(“needle”));

람다 함수 내에서 var을 사용하는 것은 문제가 없다.

2. 새로 등장한 Records

어떤 이들은 롬복을 대체하기 위해 자바가 Records를 만들었다고 한다. Record는 데이터를 저장하기 위한 타입이다.

[…] a record acquires many standard members automatically:

  • A private final field for each component of the state description; == 모든 필드는 private final
  • A public read accessor method for each component of the state description, with the same name and type as the -component; == getName() 이런식의 getter가 생기는 것이 아니라, name() 이런식으로 `필드명()` 형식의 `getter`가 생성된다
  • A public constructor, whose signature is the same as the state description, which initializes each field from the - corresponding argument; 모든 필드를 초기화할 수 있는 생성자가 생성된다. record이름(필드1, 필드2, 필드3 ...) 과 같이 -Implementations of equals and hashCode that say two records are equal if they are of the same type and contain the same state; and 같은 record인지 확인할 수 있는 hashCode랑 equals 함수를 만든다
  • An implementation of toString that includes the string representation of all the record components, with their names. ''' 모든 record 필드가 가지는 값의 string representation과 그 필드 이름을 가진다 `` 읽어보면,enum과 비슷하고, LOMBOK의@value` 와도 비슷하다고 한다. record 설명
public class SampleRecord {
   private final String name;
   private final Integer age;
   private final Address address;
 
   public SampleRecord(String name, Integer age, Address address) {
      this.name = name;
      this.age = age;
      this.address = address;
   }
 
   public String getName() {
      return name;
   }
 
   public Integer getAge() {
      return age;
   }
 
   public Address getAddress() {
      return address;
   }
}

이런 코드가 있으면 우리는 record로 저장하는 것이 더 편하다.

public record SampleRecord(
   String name,
   Integer age,
   Address address
) {}

근데 이렇게 해두면 @ResponseBody같이 JSON으로 serialize가 안된다.

public record SampleRecord(
   @JsonProperty("name") String name,
   @JsonProperty("age") Integer age,
   @JsonProperty("address") Address address
) {}

record는 static 변수, static, public 메서드를 가질 수 있다. Enum과 다른 부분은

record BankAccount (String bankName, String accountNumber) implements HasAccountNumber {
  public BankAccount { // <-- this is the constructor! no () !
    if (accountNumber == null || accountNumber.length() != 26) {
      throw new ValidationException(“Account number invalid”);
    }
    // no assignment necessary here!
  }
}

그냥 알아서 값이 들어간다고 한다.

그래서

public record UserRecord(
        String name,
        Integer age,
        EmailRecord email
) {
    public String getInfo() {
        return this.email().id();
    }
}
### EmailRecord.java
public record EmailRecord(
        String id,
        String corporate
) {}

이런식으로 record안의 record를 만들어두고,

 @PostMapping("/helloPost")
    public String getRecordPost(UserRecord userRecord) {
        System.out.println(userRecord.getInfo());
        return "index";
    }

이런식으로 form을 받을 수 있게 하고,

image

<input>

이런식으로 해서 submit을 하게 했더니,

java.lang.NullPointerException: Cannot invoke "com.example.java17practice.records.EmailRecord.id()" because the return value of "com.example.java17practice.records.UserRecord.email()" is null] with root cause

즉, email이라는 record 객체가 비어있어서 어쩔수가 없다.

Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported]

이번엔 또 이런 에러

알고보니, @RequestBody annotation같은 것들은 json 형식의 content-type application/json 과 같이 받아야 한다. 그래서 나처럼 email record를 설정해둬도 채울수도 있는거고. 그래서 post 요청을 보내는 툴을 써보면 {"name":"qwe1","age":"15","id":"1qwe","corporate":"naver"}에 대응해서 UserRecord[name=qwe1, age=15, id=1qwe, corporate=naver] 이런식으로 채워져있는 것을 발견할 수 있다. 또는, {"name":"qwe1","age":"15", "email":{"id":"1qwe","corporate":"naver"}}에 대응해서 UserRecord[name=qwe1, age=15, email=EmailRecord[id=1qwe, corporate=naver]] 간단하게 커맨드객체를 채울 수 있게 된다.

bsj805 commented 2 years ago

3. Extended switch expressions

DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek();
boolean freeDay = switch (dayOfWeek) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> false;
    case SATURDAY, SUNDAY -> true;
};

이런식으로, case문에서 return을 시킬 수 있다.

@GetMapping("/apis/{id}")
    public String findId(@PathVariable String id) {
        String name = switch (id) {
            case "a", "b", "c" -> {
                System.out.println("hello" + id);
                yield "case" + id;
            }
            case "sd" -> "sd";
            default -> "default" + id;
        };
        name += " manm";
        return name;
    }

block statement도 쓸 수 있다. 다만, 블록 안에서 return할 때에는 yield 키워드를 사용한다. switch에 대한 결과물을 yield로 정할 수 있다.

4. Instanceof 사용시 편의 추가

if (obj instanceof MyObject) {
  MyObject myObject = (MyObject) obj;
  // … further logic
}

이제 local variable을 if문 안에서 생성할 수 있게 되면서

if (obj instanceof MyObject myObject) {
  // … the same logic
}

이런식으로 사용할 수 있게 된다. myObject = obj 를 해주는 구조다.

if (obj instanceof MyObject myObject && myObject.isValid()) {
  // … the same logic
}

이런식으로 같은 줄에서 먼저 실행되기만 하면 생성된 지역변수를 사용할 수 있다.

5. Sealed classes

switch문에서 받을 수 있는 모든 value에 대해 case 처리를 했는데도 default 안 썼다고 워닝이 난다. did the "no default" warning in switch ever annoy you?

Sealed Class는 instanceof 타입 체킹에 대한 워닝을 없애준다.

public abstract sealed class Animal  permits Dog, Cat {
}
public final class Dog extends Animal {
}
public final class Cat extends Animal {
}

이런 코드를 구성하면, #4, #5 를 합쳐서

if (animal instanceof Dog d) {
    return d.woof();
} 
else if (animal instanceof Cat c) {
    return c.meow();
}

사실 java단에서 부모 클래스는 어떤 자식 클래스들이 있는지 알지 못한다. 따라서 상속받는 클래스들을 Dog , Cat으로 제한 하면서

    public String getAnimalName(Animal animal) {
        if (animal instanceof Cat cat) {
            return "cat";
        }
        return "dog";
    }

이런 로직이 가능하다. 즉 Animal 클래스는 무조건 cat이나 dog를 준다는 것이 보장되어 있다는 것.

bsj805 commented 2 years ago

5. Long Texts

String myWallOfText = ”””
______         _   _           
| ___ \       | | (_)          
| |_/ / __ ___| |_ _ _   _ ___ 
|  __/ '__/ _ \ __| | | | / __|
| |  | | |  __/ |_| | |_| \__ \
\_|  |_|  \___|\__|_|\__,_|___/
”””

원래는 이게 한줄마다 \ 와 같이 escaping quotes 나 newline이 들어갔어야 했는데, """ 으로 해결하게 되었다.

6. NullPointerException 처리 개선

company.getOwner().getAddress().getCity(); 이런식의 코드를 짜게 되었을 때, NPE가 났을 때 어떤 오브젝트가 NULL인지 찾기 어려웠는데, 이 경우에도 JVM이 "cannot invoke Person.getAddress()" 이런식으로 문제를 정확히 보여준다.

7. HttpClient

fully non-asynchronous 한 HttpClient를 이용하는 것이 가능하다.

8. 새 Optional.orElseThrow() 메서드

Optionalget() 메서드는 Optional로 정의되어 있는 value를 얻기 위해 사용되었다. 만약 그래서 값이 저장 안되어 있다면 Exception을 throw하게 된다.

MyObject myObject = myList.stream()
  .filter(MyObject::someBoolean)
  .filter((b) -> false)
  .findFirst()
  .get();

이 경우에는 Exception Handling이 필요하다. 그런데 일반적으로 코드만 봤을 때 throw하는 코드인지 알기 어렵다

아래 코드는 위 코드랑 똑같지만, object가 발견 안되면 throw한다는 것을 알 것이다.

MyObject myObject = myList.stream()
  .filter(MyObject::someBoolean)
  .filter((b) -> false)
  .findFirst()
  .orElseThrow();