JDK 14부터 record라는 녀석이 추가되었는데, getter, equals hashcode, toString 등을 자동으로 만들어주어서, 불변 데이터 객체를 쉽게 만들 수 있게 해주는 좋은 녀석이다.
하지만 조심해야 할 점이 있다.
record에 배열을 쓸 때 조심해야 한다.
public record Person(
String[] names,
int age
) {
}
바로 다음과 같이 레코드의 구성요소에 배열 array가 있을 경우이다.
레코드의 구성요소에 배열이 존재할 경우, 레코드가 보일러플레이터들을 자동으로 생성해줘서 equal, hashcode, toString을 재정의해줘야한다.
여기에 SonarQube에서의 major 이슈사례가 등록된 경우도 있다.
Java static code analysis
Unique rules to find Bugs, Vulnerabilities, Security Hotspots, and Code Smells in your JAVA code
rules.sonarsource.com
왜 이런 이슈가 존재할까?
Record (Java SE 17 & JDK 17)
Direct Known Subclasses: UnixDomainPrincipal public abstract class Record extends Object This is the common base class of all Java language record classes. More information about records, including descriptions of the implicitly declared methods synthesize
docs.oracle.com
레코드의 equals 메소드는 해당 레코드의 모든 매개변수 인스턴스가 상대 인스턴스와 동등할 때 true를 반환한다.
레코드 클래스의 동등성-Equality을 비교하는 방법은 다음과 같다.
- 레코드 구성요소 c가 참조 타입 (reference type)일 경우, Objects.equals(this.c, r.c)의 결과로 판단한다.
- 레코드 구성요소 c가 원시 타입일 경우, 래퍼 클래스로 바꿔서 Integer.compare(this.c, r.c)와 같이 compare의 결과로 판단한다.
import java.util.*;
public class Main
{
public static void main(String[] args) {
String[] tag1 = { "a", "b", "c"};
String[] tag2 = { "a", "b", "c"};
System.out.println(Objects.equals(tag1, tag2));
}
}
배열은array은 참조 타입이므로 Objects::equals의 결과로 동등성을 판단하게 되고, 배열은 참조가 같을 경우에 true를 반환하기 때문에 레코드의 equals 연산은 사용자가 바라는 결과를 얻지 못한다. 그리고 hashcode, toString에도 똑같은 논리로 적용되어 원치 않는 결과를 만들어 낼 수도 있다.
해결하는 방법은
1. 직접 equals, hashcode, toString을 재정의한다.
문제에 대한 직관적인 해답이다. 직접 배열에 대응하도록 셋 다 재정의 해준다.
public record Person(
String[] names,
int age
) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Arrays.equals(names, person.names);
}
@Override
public int hashCode() {
int result = Objects.hash(age);
result = 31 * result + Arrays.hashCode(names);
return result;
}
@Override
public String toString() {
return "Person{" +
"names=" + Arrays.toString(names) +
", age=" + age +
'}';
}
}
하지만 이렇게 되면 코드가 굉장히 복잡해진다. 자동으로 생성해주는 함수들을 직접 또 만들어야 해서 Record를 사용하는 이유가 없다.
2. 배열 대신 List를 사용한다.
훨씬 간단하고 영리한 방법이다.
Objects::equals를 제대로 지원하지 않는 배열 대신 List를 사용하면 된다.
public record Person(
List<String> names,
int age
) {
}
아래와 List 형식으로 사용하자.
import java.util.*;
public class Main
{
public static void main(String[] arges) {
List<String> tag1 = List.of("a", "b", "c");
List<String> tag2 = List.of("a", "b", "c");
System.out.println(Objects.equals(tag1, tag2));
}
}