List 내부 모든 항목 중복 검사를 위한 패턴과 비교
List의 원소 내부에 공통으로 중복되는 필드값이 있는 경우 에러를 반환해야되는 상황을 생각해보자. 아래 코드와 같이 SchoolDto가 level과 code가 모두 같은 경우 에러를 던져야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
public class SchoolService {
public void checkOverrode() {
List<SchoolDto> schoolDtos = IntStream.range(0, 5).boxed().map(i -> {
SchoolDto build = SchoolDto.builder()
.name("학교..." + i)
.phone("전화번호..." + i)
.level(i % 3 + 1)
.code(100 + i % 3 + 1)
.build();
return build;
}).collect(Collectors.toList());
//schoolDtos에서 level과 code가 모두 중복되는 항목이 있는 경우 예외처리
///1. 이중 포문
for(int i = 0; i < schoolDtos.size() - 1; i++) {
for(int j = i + 1; j < schoolDtos.size(); j++) {
SchoolDto dto1 = schoolDtos.get(i);
SchoolDto dto2 = schoolDtos.get(j);
if(dto1.getCode().equals(dto2.getCode()) && dto1.getLevel().equals(dto2.getLevel())) {
throw new IllegalArgumentException("level과 code가 모두 중복되는 항목이 존재합니다");
}
}
}
///2. List<String> 처리 후 hashSet 처리
ArrayList<String> list = new ArrayList<>();
schoolDtos.forEach(dto -> {
StringBuffer sb = new StringBuffer();
sb.append(dto.getLevel());
sb.append("-");
sb.append(dto.getCode());
list.add(sb.toString());
}); // {1-101, 2-102, 3-103, 1-101, 2-102}
HashSet<String> hs = new HashSet<>();
for (String s : list) {
if(!hs.add(s)) { // HashSet에 순차적으로 add하다가, 네번째 1-101이 들어가는 순간 에러
throw new IllegalArgumentException("level과 code가 모두 중복되는 항목이 존재합니다");
}
}
//3. List<String> 처리 후 stream.distinct 처리
ArrayList<String> list2 = new ArrayList<>();
schoolDtos.forEach(dto -> {
StringBuffer sb = new StringBuffer();
sb.append(dto.getLevel());
sb.append("-");
sb.append(dto.getCode());
list2.add(sb.toString());
}); // {1-101, 2-102, 3-103, 1-101, 2-102}
int originalSize = list2.size();
int changedSize = list2.stream().distinct().collect(Collectors.toList()).size();
if(originalSize != changedSize) {
throw new IllegalArgumentException("level과 code가 모두 중복되는 항목이 존재합니다");
}
}
@Getter
@Setter
@Builder
public static class SchoolDto {
private String name;
private String phone;
private Integer level;
private Integer code;
}
}
|
cs |
이중포문 방식 : 개념이 간단함, 성능 저하 우려
세 가지 방식 중, 1번 이중 포문 방식은 가장 간단하고 String 변환이 필요없는 방식이다. 그런데 혹시 예제 코드와 같이 level, code 2가지 항목만 비교하는 것이 아니라 많은 종류의 항목을 동시에 비교해야 한다면, if문을 작성하는 것이 좀 피곤해질 수 있겠다. 또한 이중 포문의 특성상, 비교해야하는 모수가 엄청 많은 경우 성능이 저하될 수 있을 것이다. n^2 성능.
HashSet 방식 : String 변환 시 성능 저하, 리스크 우려됨, String이 아니면 equals, hashcode 메서드 Override
List를 한 번 순회하며 모든 항목을 String으로 만들고, HashSet에 순차적으로 집어넣으면서 중복 발생을 체크한다(참조2). 항목들을 String으로 변환해야하기 때문에 추가적인 성능 저하 요소가 되고, String 비교이기 때문에 알게 모를? 위험성을 내포하는 것 같다. 그리고 String이 아닌 다른 객체를 비교할때는 HashSet의 equals와 hashcode 메서드를 Override 해주어야 한다. 왜냐하면 equals와 hashcode 메서드는 비교를 원하는 객체의 메모리 주소 및 메모리 값인 hashCode까지 모두 같은지 비교하게 되어있는데, 값만 같은지 비교하고 싶을때는 이렇게 되면 안되므로 Override가 필요하다. 상세한 내용은 참조1. String 변환 시의 단점이 있지만, String으로 처리하면 hashCode가 같고 2n의 성능이 필요하며 가독성이 좋으므로 이 방법이 제일 낫지 않을까?
stream.distinct 방식 : String 변환 문제, 결국 이중 순회?
요소들을 String으로 변환하여 리스트를 만들고, 해당 리스트에 stream.distinct를 적용하여 중복 제거한다. 중복 제거 전과 후의 list 크기를 비교한다. String 변환으로 인한 문제는 위 hashSet방식과 동일하다. 그리고 distinct의 작동방식은 결국 Object.equals를 사용하는데, 이 방식대로라면 리스트의 모든 항목들을 이중 순회하며 비교해야할 것이다. 그리고 참조 3에 따르면 stream이 일반적인 메서드 방식보다 느리다고 한다. n^2 성능인거 같다.
참조
1) sonypark.log - [Java] equals() & hascode() 메서드는 언제 재정의해야 할까?
2) NEWBEDEV
https://newbedev.com/how-to-check-if-exists-any-duplicate-in-java-8-streams
3) Java8 Stream과 기존 코드 작성시 성능 비교 연구
출처: https://roka88.dev/33 [이병록의 개발 블로그]
'Programming-[Backend] > Java' 카테고리의 다른 글
[TIL][TDD] 테스트 코드 기반으로 짜되, service 코드에서 의미적으로(기능적으로) 분리해가며 작성하기 (0) | 2021.11.09 |
---|---|
[TIL]자바 리스트 중복 항목만 추출 : indexOf, lastIndexOf, HashSet 활용 (0) | 2021.10.13 |
[TIL][링크] Predicate, BooleanExpression의 개념과 차이점 (2) | 2021.08.25 |
[링크] 자바 final 요약 (0) | 2021.05.30 |
[TIL] ENUM 값 정의하고 불러오기 (2) | 2021.05.17 |