이번 포스팅에서는 자바의 스트림을 활용해 Map 인터페이스의 다양한 활용에 대한 포스팅이다.
Map의 반복과, 병합, 삭제, 교체, 캐시구현 등 다양한 예제를 작성한다.
1. 반복
Map의 요소를 반복하기 위해서 기존에는 아래와 같은 방식으로 Map을 반복했다.
Map<String, String> map = new HashMap<>();
map.put("Key1", "Value1");
map.put("Key2", "Value2");
map.put("Key3", "Value3");
for(Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key : " + entry.getKey() + " // " + "value : " + entry.getValue());
}
/*
결과
key : Key2 // value : Value2
key : Key1 // value : Value1
key : Key3 // value : Value3
*/
하지만 스트림의 forEach 메서드를 사용하면 더 간단하다
map.forEach((key, value) -> System.out.println("key : " + key + " // " + "value : " + value));
/*
결과
key : Key2 // value : Value2
key : Key1 // value : Value1
key : Key3 // value : Value3
*/
Map 인터페이스의 forEach 메서드는 아래와 같이 BiConsumer<K, V> 를 인수로 받는다
BiConsumer<String, String> biConsumerMap = (key, value) -> System.out.println("key : " + key + " // " + "value : " + value);
map.forEach(biConsumerMap);
위 forEach문을 풀면 위 코드와 같다.
biConsumerMap 변수에 람다로 동작을 구현하고 ( 복잡한 로직인 경우 다양하게 구현할 수 있다. )
할당된 동작을 forEach에 인수로 전달한다.
2. 정렬
Map의 정렬은
https://anianidindin.tistory.com/7
[JAVA] 자바8 스트림 Map.Entry를 활용해 키(key) 또는 값(value)을 기준으로 Map(맵)정렬하기 1
이번 포스팅은 자바 8 스트림(Stream)을 이용해 HashMap의 키 또는 Value를 기준으로 내림차순, 오름차순 정렬을 해본다. 테스트를 위해 Map의 key,value 타입은 Map 타입으로 설정 했으며, Key는 순번, value 0
anianidindin.tistory.com
참고하면 된다.
3. 키가 존재하지 않는 경우 NPE (NullPointException 방지)
Map의 데이터를 가공하다 보면 다음과 같은 상황이 발생한다.
Map<String, String> map = new HashMap<>();
map.put("Key1", "Value1");
map.put("Key2", "Value2");
map.put("Key3", "Value3");
String str = map.get("Key");
System.out.println(str.length()); // NullPointerException
존재하지 않는 Key로 map.get()을 하는 경우 null이 반환되고
프로세스 상에서 반환된 null로 다른 연산을 하는 경우
NullPointerException 이 발생하게 된다.
위와 같은 상황을 방지하기 위해
map.get()이 아닌 map.getOrDefault()를 사용하면 된다.
String str = map.getOrDefault("Key", "New Value");
System.out.println(str.concat("!!"));
// New Value!!
getOrDefault의 첫번째 파라미터는 찾으려는 Key를 받고
두번째 파라미터는 해당 map에 key가 존재하지 않으면 기본값을 반환한다.
( 하지만 key가 존재해도 value의 값이 null 인 경우는 null을 반환한다. )
4. Map을 이용한 캐시 구현
map의 computeIfAbsent() 메서드를 이용하면
map을 탐색해 키가 존재하면 기존 값을 반환하고
존재하지 않으면 계산된 값을 put 해준다.
computeIfAbsent의 첫번째 인수에는 key를 넣어주고
두번째 인수에는 값이 존재하지 않을때 들어갈 계산된 값을 넣어준다.
먼저 값을 계산해주는 메서드를 생성한다
private Dish getDish(String k) {
return new Dish(k, false, 800, Dish.Type.MEAT);
}
보통 Map으로 구현한 캐시에는 Value가 Object로 들어가기 때문에 Map을 Map<String, Object>로 생성한다.
이 예제에서는 Value에 Dish라는 객체를 담았고 getDish 메서드는 Dish를 리턴해준다.
그리고 보통 Key의 문자열 리스트의 반복을 돌리면서 값을 넣어주는 상황이 많기 떄문에
키가 들어있는 List를 생성 해준 뒤 forEach 스트림으로 반복을 돌렸다.
Map<String, Object> map = new HashMap<>();
map.put("salad", new Dish("salad", true, 120, Dish.Type.OTHER));
map.put("tuna", new Dish("tuna", false, 250, Dish.Type.FISH));
List<String> list = List.of("salad", "tuna", "steak");
System.out.println("수정 전 map : " + map);
list.forEach(v -> map.computeIfAbsent(v, this::getDish));
System.out.println("수정 후 map : " + map);
// 결과
수정 전 map : {tuna=Dish{name='tuna', vegetarian=false, calories=250, type=FISH}
, salad=Dish{name='salad', vegetarian=true, calories=120, type=OTHER}}
수정 후 map : {tuna=Dish{name='tuna', vegetarian=false, calories=250, type=FISH}
, salad=Dish{name='salad', vegetarian=true, calories=120, type=OTHER}
, steak=Dish{name='steak', vegetarian=false, calories=800, type=MEAT}}
list.forEach(v -> map.computeIfAbsent(v, this::getDish)); 는 map에 key가 존재하면 그대로 진행하고
존재하지 않으면 지정해 놓은 메서드를 실행해 Dish를 새로 put 해준다.
5. Map의 요소 삭제
Map의 요소는 map.remove(key) 를 사용해 삭제할 수 있다.
위에서 생성한 map을 가지고 삭제를 진행 했다.
System.out.println("삭제 전 : " + map);
map.remove("salad");
System.out.println("삭제 후 : " + map);
// 결과
삭제 전 : {tuna=Dish{name='tuna', vegetarian=false, calories=250, type=FISH}, salad=Dish{name='salad', vegetarian=true, calories=120, type=OTHER}, steak=Dish{name='steak', vegetarian=false, calories=800, type=MEAT}}
삭제 후 : {tuna=Dish{name='tuna', vegetarian=false, calories=250, type=FISH}, steak=Dish{name='steak', vegetarian=false, calories=800, type=MEAT}}
key가 salad인 Map의 요소가 삭제된 것을 확인할 수 있다.
6. Map의 요소 변경
맵의 항목을 바꾸기 위해서는 replace와 replaceAll을 사용할 수 있다.
먼저 replace의 경우에는 map에 키가 존재하면 값을 변경 그렇지 않다면 변경하지 않는다.
Map<String, String> map = new HashMap<>();
map.put("Key1", "Value1");
map.put("Key2", "Value2");
map.put("Key3", "Value3");
위와 같은 Map을 준비하고
- Map.replace
System.out.println("변경 전 : " + map);
map.replace("Key1", "ChangeValue");
map.replace("Key4", "ChangeValue");
// map.replace("Key2", "Value2", "ChangeValue2"); // replace의 오버로드 버전
System.out.println("변경 후 : " + map);
// 결과
변경 전 : {Key2=Value2, Key1=Value1, Key3=Value3}
변경 후 : {Key2=Value2, Key1=ChangeValue, Key3=Value3}
map.replace를 실행해 결과를 확인 해보니
키가 존재하면 ChangeValue로 변경해주었고, 키가 존재하지 않는 경우 아무 일도 일어나지 않았다.
또한 replace의 오버로드 버전의 경우 파라미터를 3개를 받는다.
파라미터는 Key, oldValue, newValue 세 가지로
키와 벨류까지 일치 해야 새로운 value로 바꿔준다,
하지만 참고해야 할 부분은
객체의 경우 값이 일치하더라도 dish라는 인스턴스 변수의 주소값이 다르기 때문에 비교시 false를 반환해 변환되지 않는다,
Map<String, Object> map = new HashMap<>();
map.put("salad", new Dish("salad", true, 120, Dish.Type.OTHER));
map.put("tuna", new Dish("tuna", false, 250, Dish.Type.FISH));
Dish dish = new Dish("salad", true, 120, Dish.Type.OTHER);
System.out.println("변경 전 : " + map);
map.replace("salad", dish, new Dish("salad", true, 1200000, Dish.Type.OTHER));
System.out.println("변경 후 : " + map);
변경 전 : {tuna=Dish{name='tuna', vegetarian=false, calories=250, type=FISH}, salad=Dish{name='salad', vegetarian=true, calories=120, type=OTHER}}
변경 후 : {tuna=Dish{name='tuna', vegetarian=false, calories=250, type=FISH}, salad=Dish{name='salad', vegetarian=true, calories=120, type=OTHER}}
변하지 않는다..
- Map.replaceAll
Map 인터페이스의 replaceAll의 경우 BiFunction을 인수로 받는다.
System.out.println("변경 전 : " + map);
map.replaceAll((key, value) -> value.toUpperCase());
System.out.println("변경 후 : " + map);
// 결과
변경 전 : {Key2=Value2, Key1=Value1, Key3=Value3}
변경 후 : {Key2=VALUE2, Key1=VALUE1, Key3=VALUE3}
위와 같이 람다에 key와 value를 주고 요구사항에 맞춰 람다 로직을 구현해주면
전체 Value에 대한 조작/변경 이 가능하다.
7. 맵 요소의 병합
Map을 병합 하기 위해서는 Map.merge를 사용하면 된다.
merge의 경우 키가 존재하지 않으면 추가를 해주고 키가 존재하면 대체 값으로 변경해준다.
즉 두 개의 Map을 병합 할 수 있다.
merge 메서드의 경우 key, value, 함수형 인터페이스 BiFunction 총 3개의 파라미터를 받는다.
모던 자바인 액션 8장에서는 merge에 대한 설명으로
"지정된 키와 연관된 값이 없거나 값이 널이면 merge는 키를 널이 아닌 값과 연결한다.
아니면 merge는 연결된 값을 주어진 매핑 함수의 결과 값으로 대치하거나 결과가 널이면 항목을 제거한다."
라고 한다.
merge의 내부 소스를 살펴보면 쉽게 이해 할 수 있다.
즉,
1. 기준 대상 map의 value가 Null 이라면 새로운 map의 값으로 대체 한다.
2. 기준 대상 map의 key가 없다면 주어진 새로운 value가 put 된다.
3. 기준 대상 map과 병합하려는 map 모두 key가 존재한다면 매핑 함수의 결과로 대체 된다.
4. 기준 대상 map의 key가 존재하고 값도 있지만, 람다의 리턴 값이 null 인 경우 해당 항목은 제거 된다.
1,2 번에 대한 예제
Map<String, String> map1 = new HashMap<>();
map1.put("Key1", "Value1");
map1.put("Key2", "Value2");
Map<String, String> map2 = new HashMap<>();
map2.put("Key1", null);
map2.put("Key4", "Value4");
System.out.println("변경 전 map1 : " + map1 + " map2 : " + map2);
map1.forEach((key, value) -> {
map2.merge(key, value, (map2Val, map1Val) -> "중복된 키 발견!! " + map1Val + " & " + map2Val);
});
System.out.println("변경 후 map1 : " + map1 + " map2 : " + map2);
// 결과
변경 전 map1 : {Key2=Value2, Key1=Value1} map2 : {Key1=null, Key4=Value4}
변경 후 map1 : {Key2=Value2, Key1=Value1} map2 : {Key2=Value2, Key1=Value1, Key4=Value4}
map2의 Key1 의 경우 값은 null 이기 때문에 Key1의 값은 map1의 값인 Value1이 대체되었다
그리고 map2에는 Key2가 없기 때문에 map1의 key2=Value2 가 새롭게 put 되었다.
3번 예제
Map<String, String> map1 = new HashMap<>();
map1.put("Key1", "Value1");
map1.put("Key2", "Value2");
Map<String, String> map2 = new HashMap<>();
map2.put("Key1", "Value1234567");
map2.put("Key4", "Value4");
System.out.println("변경 전 map1 : " + map1 + " map2 : " + map2);
map1.forEach((key, value) -> {
map2.merge(key, value, (map2Val, map1Val) -> "중복된 키 발견!! " + map1Val + " & " + map2Val);
});
System.out.println("변경 후 map1 : " + map1 + " map2 : " + map2);
// 결과
변경 전 map1 : {Key2=Value2, Key1=Value1} map2 : {Key1=Value1234567, Key4=Value4}
변경 후 map1 : {Key2=Value2, Key1=Value1} map2 : {Key2=Value2, Key1=중복된 키 발견!! Value1234567 & Value1, Key4=Value4}
map2의 Key1의 값만 null에서 새로운 값으로 변경했다.
이때는 모두 키가 존재하고 값도 존재하기 때문에 매핑 함수의 리턴 값이 value로 다시 put 되었다.
4번 예제
Map<String, String> map1 = new HashMap<>();
map1.put("Key1", "Value1");
map1.put("Key2", "Value2");
Map<String, String> map2 = new HashMap<>();
map2.put("Key1", "Value1234567");
map2.put("Key4", "Value4");
System.out.println("변경 전 map1 : " + map1 + " map2 : " + map2);
map1.forEach((key, value) -> {
map2.merge(key, value, (map2Val, map1Val) -> null);
});
System.out.println("변경 후 map1 : " + map1 + " map2 : " + map2);
// 결과
변경 전 map1 : {Key2=Value2, Key1=Value1} map2 : {Key1=Value1234567, Key4=Value4}
변경 후 map1 : {Key2=Value2, Key1=Value1} map2 : {Key2=Value2, Key4=Value4}
merge의 매핑함수의 경우 키가 중복되면서, 기준 Map(map2)의 값이 존재하는 경우 함수가 동작한다.
따라서 Key1인 케이스만 함수가 동작하며 Null을 리턴했기 때문에
중복된 key는 제거가 된다.
따라서 merge는 두 map의 중복된 key도 제거를 할 수 있다.
이번에는 Map 인터페이스의 활용에 대해 공부 해봤다.
실무를 하다보면 key와 value 쌍으로 이루어진 데이터를 정말 많이 다룬다. (캐시, JSON, entity 객체 등등..)
Map을 이용해 데이터의 다양한 조작을 다뤄 봤고 merge의 경우에는 잘 사용하면 유용하게 사용할 수 있을 것 같다.
'JAVA' 카테고리의 다른 글
[JAVA] 자바 java.time API로 다양한 날짜와 시간 다루기 (2) | 2023.09.25 |
---|---|
[JAVA] 자바8 Stream Collector의 사용 방법 및 다양한 예제 (6) | 2023.09.18 |
[JAVA] 자바8 스트림 Map.Entry를 활용해 키(key) 또는 값(value)을 기준으로 Map(맵)정렬하기 2 (0) | 2023.09.13 |
[JAVA] 자바8 스트림 Map.Entry를 활용해 키(key) 또는 값(value)을 기준으로 Map(맵)정렬하기 1 (0) | 2023.09.12 |
[JAVA] 자바 객체 리스트(List) 정렬하기 (오름, 내림차순) (0) | 2023.09.11 |