Using Record to Model Immutable Data
레코드를 사용하여 불변 데이터 모델링
Java 언어는 불변 클래스를 만드는 여러 가지 방법을 제공합니다. 아마도 가장 간단한 방법은 최종 필드와 이러한 필드를 초기화 할 생성자를 가진 최종 클래스를 만드는 것입니다. 다음은 그러한 클래스의 예입니다.
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
이러한 요소를 작성 했으므로 필드에 대한 액세스자를 추가해야합니다. 당신은 또한 추가 할 것입니다 toString() 방법과 아마 equals() 함께 hashCode() 방법. 이 모든 것을 손으로 쓰는 것은 매우 지루하고 오류가 발생하기 쉽습니다. 다행히도 IDE가 이러한 방법을 생성 할 수 있습니다.
네트워크를 통해 또는 파일 시스템을 통해이 클래스의 인스턴스를 한 응용 프로그램에서 다른 응용 프로그램으로 전송해야하는 경우, 이 클래스를 직렬화 할 수도 있습니다. 그렇게하면이 클래스의 인스턴스가 직렬화되는 방법에 대한 정보를 추가해야 할 수도 있습니다. JDK는 직렬화를 제어하는 여러 가지 방법을 제공합니다.
결국, 당신의 Point 클래스는 파일에 써야하는 두 개의 정수로 불변 집계를 모델링하기 위해 IDE에 의해 생성 된 코드로 채워진 백 줄 길이 일 수 있습니다.
이를 변경하기 위해 JDK에 레코드가 추가되었습니다. 레코드는이 모든 것을 한 줄의 코드로 제공합니다. 레코드의 상태를 선언하기 만하면됩니다. 나머지는 컴파일러에서 생성합니다.
구조에 기록 호출
이 코드를 훨씬 간단하게 만드는 데 도움이되는 기록이 있습니다. Java SE 14부터 다음 코드를 작성할 수 있습니다.
public record Point(int x, int y) {}
이 단일 코드 줄은 다음과 같은 요소를 만듭니다.
- 두 개의 필드가있는 불변의 클래스입니다: x 과 y, 유형의 int.
- 이 두 필드를 초기화하기위한 표준 생성자가 있습니다.
- 그만큼 toString(), equals() 과 hashCode() IDE가 생성 한 것에 해당하는 기본 동작을 가진 컴파일러가 메소드를 작성했습니다. 필요한 경우 이러한 메소드의 자체 구현을 추가하여이 동작을 수정할 수 있습니다.
- 구현할 수 있습니다 Serializable 인스턴스를 보낼 수 있도록 인터페이스 Point 네트워크 또는 파일 시스템을 통해 다른 응용 프로그램에. 레코드가 직렬화되고 직렬화 해제되는 방식은이 자습서 끝에서 다루는 몇 가지 특수 규칙을 따릅니다.
레코드는 IDE의 도움없이 불변의 데이터 집합을 훨씬 간단하게 만듭니다. 레코드의 구성 요소를 수정할 때마다 컴파일러가 자동으로 equals() 과 hashCode() 당신을위한 방법.
기록의 종류
레코드는 record 대신 키워드 class 키워드. 다음 기록을 선언하겠습니다.
public record Point(int x, int y) {}
레코드를 만들 때 컴파일러가 생성하는 클래스는 최종입니다.
이 클래스는 java.lang.Record 수업. 따라서 귀하의 기록은 어떤 수업도 연장 할 수 없습니다.
레코드는 여러 인터페이스를 구현할 수 있습니다.
레코드의 구성 요소 선언
레코드 이름 바로 뒤에 오는 블록은 (int x, int y). 그것은 선언 구성 요소 명명 된 기록 Point. 레코드의 각 구성 요소에 대해 컴파일러는이 구성 요소와 동일한 이름을 가진 개인 최종 필드를 만듭니다. 레코드에 여러 구성 요소를 선언 할 수 있습니다.
이 예에서 컴파일러는 두 가지 개인 최종 필드를 만듭니다 int: x 과 y, 선언 한 두 구성 요소에 해당합니다.
이 필드와 함께 컴파일러는 하나를 생성합니다 접근 자 각 구성 요소에 대해. 이 액세스자는 구성 요소의 이름이 같은 메소드이며 값을 리턴합니다. 이것의 경우 Point 기록적으로, 생성 된 두 가지 방법은 다음과 같습니다.
public int x() {
return this.x;
}
public int y() {
return this.y;
}
이 구현이 응용 프로그램에 효과적이라면 아무것도 추가 할 필요가 없습니다. 그래도 자신의 액세스 방법을 정의 할 수 있습니다. 특정 필드의 방어 사본을 반환해야하는 경우에 유용 할 수 있습니다.
컴파일러가 생성 한 마지막 요소는 toString(), equals() 과 hashCode() 의 방법 Object 수업. 필요한 경우 이러한 메소드의 고유 한 대체를 정의 할 수 있습니다.
레코드에 추가 할 수없는 것
레코드에 추가 할 수없는 세 가지가 있습니다:
- 레코드에서 인스턴스 필드를 선언 할 수 없습니다. 구성 요소에 해당하지 않는 인스턴스 필드를 추가 할 수 없습니다.
- 필드 이니셜 라이저를 정의 할 수 없습니다.
- 인스턴스 이니셜 라이저를 추가 할 수 없습니다.
초기화 기와 정적 초기화 기로 정적 필드를 만들 수 있습니다.
정식 생성자로 레코드 구성
컴파일러는 또한 표준 생성자. 이 생성자는 레코드의 구성 요소를 인수로 가져 와서 해당 값을 레코드 클래스의 필드에 복사합니다.
이 기본 동작을 재정의해야하는 상황이 있습니다. 두 가지 사용 사례를 살펴 보겠습니다:
- 기록 상태를 확인해야합니다
- 가변 구성 요소의 방어 사본을 만들어야합니다.
소형 컨스트럭터 사용
두 개의 다른 구문을 사용하여 레코드의 표준 생성자를 재정의 할 수 있습니다. 컴팩트 한 생성자 또는 표준 생성자 자체를 사용할 수 있습니다.
다음 기록이 있다고 가정하십시오.
public record Range(int start, int end) {}
그 이름의 기록을 위해 end 더 큰 start. 컴팩트 생성자를 레코드에 작성하여 유효성 검사 규칙을 추가 할 수 있습니다.
public record Range(int start, int end) {
public Range {
if (end <= start) {
throw new IllegalArgumentException("End cannot be lesser than start");
}
}
}
컴팩트 한 표준 생성자는 매개 변수 블록을 선언 할 필요가 없습니다.
이 구문을 선택하면 다음과 같이 레코드 필드를 직접 할당 할 수 없습니다 this.start = start -컴파일러가 추가 한 코드로 수행됩니다. 그러나 컴파일러 생성 코드가 이러한 새로운 값을 필드에 할당하기 때문에 매개 변수에 새 값을 할당 할 수 있습니다.
public Range {
// set negative start and end to 0
// by reassigning the compact constructor's
// implicit parameters
if (start < 0)
start = 0;
if (end < 0)
end = 0;
}
정식 생성자 사용
예를 들어 매개 변수를 재 할당하지 않기 때문에 비 소형 양식을 선호하는 경우 다음 예와 같이 표준 생성자를 직접 정의 할 수 있습니다.
public record Range(int start, int end) {
public Range(int start, int end) {
if (end <= start) {
throw new IllegalArgumentException("End cannot be lesser than start");
}
if (start < 0) {
this.start = 0;
} else {
this.start = start;
}
if (end > 100) {
this.end = 10;
} else {
this.end = end;
}
}
}
이 경우 작성하는 생성자는 레코드의 필드에 값을 할당해야합니다.
레코드의 구성 요소를 변경할 수없는 경우 표준 생성자와 액세스 자 모두에서 방어 사본을 만드는 것이 좋습니다.
생성자 정의
이 생성자가 레코드의 표준 생성자를 호출하는 한 생성자를 레코드에 추가 할 수도 있습니다. 구문은 다른 생성자와 생성자를 호출하는 클래식 구문과 동일합니다. 모든 수업에 관해서는 this() 생성자의 첫 번째 진술이어야합니다.
다음을 살펴 보자 State 기록. 세 가지 구성 요소로 정의됩니다:
- 이 상태의 이름
- 이주의 수도의 이름
- 비어있을 수있는 도시 이름 목록.
이 기록의 외부에서 수정되지 않도록 도시 목록의 방어 사본을 저장해야합니다. 이는 매개 변수를 방어 사본에 재 할당하는 컴팩트 한 형식으로 표준 생성자를 재정의하여 수행 할 수 있습니다.
도시를 취하지 않는 생성자를 갖는 것이 응용 프로그램에 유용합니다. 이것은 주 이름과 수도 이름 만 사용하는 다른 생성자 일 수 있습니다. 이 두 번째 생성자는 표준 생성자를 호출해야합니다.
그런 다음 도시 목록을 전달하는 대신 도시를 vararg로 전달할 수 있습니다. 그렇게하려면 올바른 목록을 가진 표준 생성자를 호출해야하는 세 번째 생성자를 만들 수 있습니다.
public record State(String name, String capitalCity, List<String> cities) {
public State {
// List.copyOf returns an unmodifiable copy,
// so the list assigned to `cities` can't change anymore
cities = List.copyOf(cities);
}
public State(String name, String capitalCity) {
this(name, capitalCity, List.of());
}
public State(String name, String capitalCity, String... cities) {
this(name, capitalCity, List.of(cities));
}
}
그 점에 유의하십시오 List.copyOf() 메소드는 인수로 얻는 콜렉션에서 널값을 허용하지 않습니다.
기록 상태 얻기
컴파일러가이를 수행하므로 레코드에 액세스자를 추가 할 필요가 없습니다. 레코드에는 구성 요소 당 하나의 액세스 방법이 있으며이 구성 요소의 이름이 있습니다.
그만큼 Point 이 자습서의 첫 번째 섹션에서 기록에는 두 가지 액세스 방법이 있습니다: x() 과 y() 해당 구성 요소의 값을 반환합니다.
그러나 자신의 액세스자를 정의해야하는 경우가 있습니다. 예를 들어 State 이전 섹션의 레코드는 수정 불가능한 방어 사본을 만들지 않았습니다 cities 건설 중 목록-발신자가 내부 상태를 변경할 수 없도록 액세스기에서이를 수행해야합니다. 다음 코드를 추가 할 수 있습니다 State 이 방어 사본을 반환하는 기록.
public List<String> cities() {
return List.copyOf(cities);
}
직렬화 기록
레코드 클래스가 구현 된 경우 레코드를 직렬화하고 직렬화 해제 할 수 있습니다 Serializable. 그러나 제한이 있습니다.
- 기본 직렬화 프로세스를 대체하는 데 사용할 수있는 시스템은 레코드에 사용할 수 없습니다. 만들기 writeObject() 과 readObject() 방법은 효과가 없으며 구현 Externalizable.
- 레코드는 프록시 객체로 사용하여 다른 객체를 직렬화 할 수 있습니다. ᅡ readResolve() 메소드는 레코드를 반환 할 수 있습니다. 추가 writeReplace() 기록에서도 가능합니다.
- 기록 탈수 항상 정식 생성자에게 전화하십시오. 따라서이 생성자에 추가 할 수있는 모든 유효성 검사 규칙은 레코드를 역 직렬화 할 때 적용됩니다.
따라서 응용 프로그램에서 데이터 전송 객체를 작성하기 위해 레코드를 매우 잘 선택합니다.
실제 사용 사례에서 레코드 사용
레코드는 여러 상황에서 사용할 수있는 다목적 개념입니다.
첫 번째는 응용 프로그램의 객체 모델에 데이터를 전달하는 것입니다. 불변의 데이터 반송파 역할을하는 레코드를 사용할 수 있습니다.
로컬 레코드를 선언 할 수 있으므로 해당 레코드를 사용하여 코드의 가독성을 향상시킬 수도 있습니다.
다음과 같은 사용 사례를 고려해 봅시다. 레코드로 모델링 된 두 개의 엔티티가 있습니다: City 과 State.
public record City(String name, State state) {}
public record State(String name) {}
도시 목록이 있고 도시 수가 가장 많은 주를 계산해야한다고 가정하십시오. Stream API를 사용하여 먼저 각 도시 수를 가진 상태의 히스토그램을 작성할 수 있습니다. 이 히스토그램은 Map.
List<City> cities = List.of();
Map<State, Long> numberOfCitiesPerState =
cities.stream()
.collect(Collectors.groupingBy(
City::state, Collectors.counting()
));
이 히스토그램의 최대 값을 얻는 것은 다음과 같은 일반 코드입니다.
Map.Entry<State, Long> stateWithTheMostCities =
numberOfCitiesPerState.entrySet().stream()
.max(Map.Entry.comparingByValue())
.orElseThrow();
이 마지막 코드는 기술적입니다. 비즈니스 의미를 갖지 않습니다. 사용하기 때문에 Map.Entry 히스토그램의 모든 요소를 모델링하는 인스턴스.
로컬 레코드를 사용하면이 상황이 크게 개선 될 수 있습니다. 다음 코드는 주와 주 수를 집계하는 새로운 레코드 클래스를 작성합니다. 인스턴스를 취하는 생성자가 있습니다 Map.Entry 키-값 쌍의 스트림을 레코드 스트림에 맵핑하는 매개 변수로서.
도시 수만큼 이러한 집계를 비교해야하므로 공장 방법을 추가하여이 비교기를 제공 할 수 있습니다. 코드는 다음과 같습니다.
record NumberOfCitiesPerState(State state, long numberOfCities) {
public NumberOfCitiesPerState(Map.Entry<State, Long> entry) {
this(entry.getKey(), entry.getValue());
}
public static Comparator<NumberOfCitiesPerState> comparingByNumberOfCities() {
return Comparator.comparing(NumberOfCitiesPerState::numberOfCities);
}
}
NumberOfCitiesPerState stateWithTheMostCities =
numberOfCitiesPerState.entrySet().stream()
.map(NumberOfCitiesPerState::new)
.max(NumberOfCitiesPerState.comparingByNumberOfCities())
.orElseThrow();
코드는 이제 의미있는 방식으로 최대 값을 추출합니다. 코드를보다 읽기 쉽고 이해하기 쉽고 오류가 적고 장기적으로 유지 관리하기가 더 쉽습니다.
※ 쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공 받을 수 있습니다
'∮explotación≒ 개발' 카테고리의 다른 글
java Interfaces (0) | 2023.03.04 |
---|---|
Oracle 오라클 조인 [ ANSI JOIN, Oracle Join ] (0) | 2023.02.28 |
java Switch 에 대한 설명 (0) | 2023.02.22 |
java if 함수란 무엇인가 알아보자 (0) | 2023.02.21 |
java Expressions (0) | 2023.02.19 |
댓글