diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/collection/MapModifier.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/collection/MapModifier.java new file mode 100644 index 0000000..5cf5e97 --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/collection/MapModifier.java @@ -0,0 +1,254 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.zhouxy.plusone.commons.collection; + +import static xyz.zhouxy.plusone.commons.util.AssertTools.checkArgumentNotNull; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.google.common.annotations.Beta; + +/** + * Map 修改器 + * + *
+ * 封装一系列对 Map 数据的修改操作,修改 Map 的数据。可以用于 Map 的数据初始化等操作。 + * + *
+ * // MapModifier + * MapModifier<String, Object> modifier = new MapModifier<String, Object>() + * .putAll(commonProperties) + * .put("username", "Ben") + * .put("accountStatus", LOCKED); + * + * // 从 Supplier 中获取 Map,并修改数据 + * Map<String, Object> map = modifier.getAndModify(HashMap::new); + * + * // 可以灵活使用不同 Map 类型的不同构造器 + * Map<String, Object> map = modifier.getAndModify(() -> new HashMap<>(8)); + * Map<String, Object> map = modifier.getAndModify(() -> new HashMap<>(anotherMap)); + * Map<String, Object> map = modifier.getAndModify(TreeMap::new); + * Map<String, Object> map = modifier.getAndModify(ConcurrentHashMap::new); + * + * // 修改已有的 Map + * modifier.modify(map); + * + * // 创建一个有初始化数据的不可变的 Map + * Map<String, Object> map = modifier.getUnmodifiableMap(); + * + * // 链式调用创建并初始化数据 + * Map<String, Object> map = new MapModifier<String, Object>() + * .putAll(commonProperties) + * .put("username", "Ben") + * .put("accountStatus", LOCKED) + * .getAndModify(HashMap::new); + *
+ * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param value 要添加的 {@code value} + * @return MapModifier + */ + public MapModifier put(@Nullable K key, @Nullable V value) { + return addOperationInternal(map -> map.put(key, value)); + } + + /** + * 添加一个键值对,如果 key 已经存在,则不添加。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param value 要添加的 {@code value} + * @return MapModifier + */ + public MapModifier putIfAbsent(@Nullable K key, @Nullable V value) { + return addOperationInternal(map -> map.putIfAbsent(key, value)); + } + + /** + * 添加多个键值对。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param otherMap 要添加的键值对集合。 + * 如果为 {@code null},则什么都不做。 + * + * @return MapModifier + */ + public MapModifier putAll(@Nullable Map extends K, ? extends V> otherMap) { + if (otherMap == null || otherMap.isEmpty()) { + return this; + } + return addOperationInternal(map -> map.putAll(otherMap)); + } + + /** + * 添加多个键值对。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param entries 要添加的键值对集合 + * @return MapModifier + */ + @SafeVarargs + public final MapModifier putAll(Map.Entry extends K, ? extends V>... entries) { + if (entries.length == 0) { + return this; + } + return addOperationInternal(map -> { + for (Map.Entry extends K, ? extends V> entry : entries) { + map.put(entry.getKey(), entry.getValue()); + } + }); + } + + /** + * 当 {@code key} 不存在时,计算对应的值,并添加到 {@code map} 中。 + * + * + * 调用 {@link Map#computeIfAbsent(Object, Function)}。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param mappingFunction 计算 {@code key} 对应的值 + * @return MapModifier + */ + public MapModifier computeIfAbsent(K key, + Function super K, ? extends V> mappingFunction) { + return addOperationInternal(map -> map.computeIfAbsent(key, mappingFunction)); + } + + /** + * 当 {@code key} 存在时,计算对应的值,并添加到 {@code map} 中。 + * + * + * 调用 {@link Map#computeIfPresent(Object, BiFunction)}。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param remappingFunction 计算 {@code key} 对应的值 + * @return MapModifier + */ + public MapModifier computeIfPresent(K key, + BiFunction super K, ? super V, ? extends V> remappingFunction) { + return addOperationInternal(map -> map.computeIfPresent(key, remappingFunction)); + } + + /** + * 删除 {@code key}。 + * + * + * 注意:key 是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要删除的 {@code key} + * @return MapModifier + */ + public MapModifier remove(K key) { + return addOperationInternal(map -> map.remove(key)); + } + + /** + * 清空 {@code map} + * + * @return MapModifier + */ + public MapModifier clear() { + return addOperationInternal(Map::clear); + } + + /** + * 修改 {@code map} + * + * @param map 要修改的 {@code map} + * @return 修改后的 {@code map}。当入参是 {@code null} 时,返回 {@code null}。 + */ + public > void modify(@Nullable T map) { + if (map != null) { + this.operators.accept(map); + } + } + + /** + * 修改 {@code map} + * + * @param mapSupplier {@code map} 的 {@link Supplier} + * @return 修改后的 {@code map}。 + * 当从 {@code mapSupplier} 获取的 {@code map} 为 {@code null} 时,返回 {@code null}。 + */ + @CheckForNull + public > T getAndModify(Supplier mapSupplier) { + checkArgumentNotNull(mapSupplier, "The map supplier cannot be null."); + T map = mapSupplier.get(); + modify(map); + return map; + } + + /** + * 创建一个有初始化数据的不可变的 {@code Map} + * + * @return 不可变的 {@code Map} + */ + public Map getUnmodifiableMap() { + return Collections.unmodifiableMap(getAndModify(HashMap::new)); + } + + private MapModifier addOperationInternal(Consumer> operator) { + this.operators = this.operators.andThen(operator); + return this; + } +} diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java new file mode 100644 index 0000000..d372d27 --- /dev/null +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java @@ -0,0 +1,316 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.zhouxy.plusone.commons.collection; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.Nullable; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableMap; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MapModifierTests { + + private static final String APP_START_ID = UUID.randomUUID().toString(); + private static final String LOCKED = "LOCKED"; + + private static final Map commonProperties = ImmutableMap.builder() + .put("channel", "MOBILE") + .put("appStartId", APP_START_ID) + .build(); + + @Test + void demo() { + Map expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + + // MapModifier + MapModifier modifier = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED); + + // 从 Supplier 中获取 Map,并修改数据 + HashMap hashMap1 = modifier.getAndModify(HashMap::new); + assertEquals(expected, hashMap1); + + // 可以灵活使用不同 Map 类型的不同构造器 + HashMap hashMap2 = modifier.getAndModify(() -> new HashMap<>(8)); + assertEquals(expected, hashMap2); + + // HashMap hashMap3 = modifier.getAndModify(() -> new HashMap<>(anotherMap)); + TreeMap treeMap = modifier.getAndModify(TreeMap::new); + assertEquals(expected, treeMap); + ConcurrentHashMap concurrentHashMap = modifier.getAndModify(ConcurrentHashMap::new); + assertEquals(expected, concurrentHashMap); + + assertNull(modifier.getAndModify(() -> (Map) null)); + + // 修改已有的 Map + Map srcMap = new HashMap<>(); + srcMap.put("srcKey1", "srcValue1"); + srcMap.put("srcKey2", "srcValue2"); + modifier.modify(srcMap); + assertEquals(new HashMap() { + { + putAll(commonProperties); + put("username", "Ben"); + put("accountStatus", LOCKED); + put("srcKey1", "srcValue1"); + put("srcKey2", "srcValue2"); + } + }, srcMap); + + assertDoesNotThrow(() -> modifier.modify((Map) null)); + + // 创建一个有初始化数据的不可变的 {@code Map} + Map unmodifiableMap = modifier.getUnmodifiableMap(); + assertEquals(expected, unmodifiableMap); + assertThrows(UnsupportedOperationException.class, + () -> unmodifiableMap.put("key", "value")); + } + + @Test + void createAndInitData() { + // 链式调用创建并初始化数据 + HashMap map = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED) + .getAndModify(HashMap::new); + + HashMap expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + assertEquals(expected, map); + } + + @Test + void put() { + Map map = new MapModifier() + .put("key1", "value0") + .put("key1", "value1") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + put("key1", "value0"); + put("key1", "value1"); + } + }, map); + + new MapModifier() + .put("key1", "newValue1") + .put("key2", null) + .modify(map); + + assertEquals("newValue1", map.get("key1")); + assertTrue(map.containsKey("key2")); + assertNull(map.get("key2")); + } + + @Test + void putIfAbsent() { + Map map = new MapModifier() + .putIfAbsent("key1", null) + .putIfAbsent("key1", "value1") + .putIfAbsent("key1", "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + putIfAbsent("key1", null); + putIfAbsent("key1", "value1"); + putIfAbsent("key1", "value2"); + } + }, map); + + new MapModifier() + .putIfAbsent("key1", "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void putAll_map() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll((Map) null) + .putAll(Collections.emptyMap()) + .putAll(entries) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll(new HashMap() { + { + put("key2", "newValue2"); + put("key3", "value3"); + } + }) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void putAll_entries() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll(new SimpleEntry<>("key1", "value1"), + new SimpleEntry<>("key2", "value2")) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll() + .putAll(new SimpleEntry<>("key2", "newValue2"), + new SimpleEntry<>("key3", "value3")) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void computeIfAbsent_keyAndFunction() { + Map map = new MapModifier() + .computeIfAbsent("key1", k -> null) + .computeIfAbsent("key1", k -> "value1") + .computeIfAbsent("key1", k -> "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + computeIfAbsent("key1", k -> null); + computeIfAbsent("key1", k -> "value1"); + computeIfAbsent("key1", k -> "value2"); + } + }, map); + + new MapModifier() + .computeIfAbsent("key1", k -> "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void computeIfPresent_keyAndBiFunction() { + Map map = new HashMap() {{ + put("key1", "value1"); + }}; + new MapModifier() + .computeIfPresent("key1", (k, v) -> k + v) + .computeIfPresent("key2", (k, v) -> k + v) + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "key1value1"); + }}, map); + } + + @Test + void remove() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .remove("key2") + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "value1"); + }}, map); + } + + @Test + void clear() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .clear() + .modify(map); + assertTrue(map.isEmpty()); + } + + @Getter + static class SimpleEntry implements Map.Entry { + private final K key; + private final V value; + + public SimpleEntry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public V setValue(@Nullable V value) { + throw new UnsupportedOperationException("Unimplemented method 'setValue'"); + } + } +}
+ * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param value 要添加的 {@code value} + * @return MapModifier + */ + public MapModifier putIfAbsent(@Nullable K key, @Nullable V value) { + return addOperationInternal(map -> map.putIfAbsent(key, value)); + } + + /** + * 添加多个键值对。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param otherMap 要添加的键值对集合。 + * 如果为 {@code null},则什么都不做。 + * + * @return MapModifier + */ + public MapModifier putAll(@Nullable Map extends K, ? extends V> otherMap) { + if (otherMap == null || otherMap.isEmpty()) { + return this; + } + return addOperationInternal(map -> map.putAll(otherMap)); + } + + /** + * 添加多个键值对。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param entries 要添加的键值对集合 + * @return MapModifier + */ + @SafeVarargs + public final MapModifier putAll(Map.Entry extends K, ? extends V>... entries) { + if (entries.length == 0) { + return this; + } + return addOperationInternal(map -> { + for (Map.Entry extends K, ? extends V> entry : entries) { + map.put(entry.getKey(), entry.getValue()); + } + }); + } + + /** + * 当 {@code key} 不存在时,计算对应的值,并添加到 {@code map} 中。 + * + * + * 调用 {@link Map#computeIfAbsent(Object, Function)}。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param mappingFunction 计算 {@code key} 对应的值 + * @return MapModifier + */ + public MapModifier computeIfAbsent(K key, + Function super K, ? extends V> mappingFunction) { + return addOperationInternal(map -> map.computeIfAbsent(key, mappingFunction)); + } + + /** + * 当 {@code key} 存在时,计算对应的值,并添加到 {@code map} 中。 + * + * + * 调用 {@link Map#computeIfPresent(Object, BiFunction)}。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param remappingFunction 计算 {@code key} 对应的值 + * @return MapModifier + */ + public MapModifier computeIfPresent(K key, + BiFunction super K, ? super V, ? extends V> remappingFunction) { + return addOperationInternal(map -> map.computeIfPresent(key, remappingFunction)); + } + + /** + * 删除 {@code key}。 + * + * + * 注意:key 是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要删除的 {@code key} + * @return MapModifier + */ + public MapModifier remove(K key) { + return addOperationInternal(map -> map.remove(key)); + } + + /** + * 清空 {@code map} + * + * @return MapModifier + */ + public MapModifier clear() { + return addOperationInternal(Map::clear); + } + + /** + * 修改 {@code map} + * + * @param map 要修改的 {@code map} + * @return 修改后的 {@code map}。当入参是 {@code null} 时,返回 {@code null}。 + */ + public > void modify(@Nullable T map) { + if (map != null) { + this.operators.accept(map); + } + } + + /** + * 修改 {@code map} + * + * @param mapSupplier {@code map} 的 {@link Supplier} + * @return 修改后的 {@code map}。 + * 当从 {@code mapSupplier} 获取的 {@code map} 为 {@code null} 时,返回 {@code null}。 + */ + @CheckForNull + public > T getAndModify(Supplier mapSupplier) { + checkArgumentNotNull(mapSupplier, "The map supplier cannot be null."); + T map = mapSupplier.get(); + modify(map); + return map; + } + + /** + * 创建一个有初始化数据的不可变的 {@code Map} + * + * @return 不可变的 {@code Map} + */ + public Map getUnmodifiableMap() { + return Collections.unmodifiableMap(getAndModify(HashMap::new)); + } + + private MapModifier addOperationInternal(Consumer> operator) { + this.operators = this.operators.andThen(operator); + return this; + } +} diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java new file mode 100644 index 0000000..d372d27 --- /dev/null +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java @@ -0,0 +1,316 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.zhouxy.plusone.commons.collection; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.Nullable; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableMap; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MapModifierTests { + + private static final String APP_START_ID = UUID.randomUUID().toString(); + private static final String LOCKED = "LOCKED"; + + private static final Map commonProperties = ImmutableMap.builder() + .put("channel", "MOBILE") + .put("appStartId", APP_START_ID) + .build(); + + @Test + void demo() { + Map expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + + // MapModifier + MapModifier modifier = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED); + + // 从 Supplier 中获取 Map,并修改数据 + HashMap hashMap1 = modifier.getAndModify(HashMap::new); + assertEquals(expected, hashMap1); + + // 可以灵活使用不同 Map 类型的不同构造器 + HashMap hashMap2 = modifier.getAndModify(() -> new HashMap<>(8)); + assertEquals(expected, hashMap2); + + // HashMap hashMap3 = modifier.getAndModify(() -> new HashMap<>(anotherMap)); + TreeMap treeMap = modifier.getAndModify(TreeMap::new); + assertEquals(expected, treeMap); + ConcurrentHashMap concurrentHashMap = modifier.getAndModify(ConcurrentHashMap::new); + assertEquals(expected, concurrentHashMap); + + assertNull(modifier.getAndModify(() -> (Map) null)); + + // 修改已有的 Map + Map srcMap = new HashMap<>(); + srcMap.put("srcKey1", "srcValue1"); + srcMap.put("srcKey2", "srcValue2"); + modifier.modify(srcMap); + assertEquals(new HashMap() { + { + putAll(commonProperties); + put("username", "Ben"); + put("accountStatus", LOCKED); + put("srcKey1", "srcValue1"); + put("srcKey2", "srcValue2"); + } + }, srcMap); + + assertDoesNotThrow(() -> modifier.modify((Map) null)); + + // 创建一个有初始化数据的不可变的 {@code Map} + Map unmodifiableMap = modifier.getUnmodifiableMap(); + assertEquals(expected, unmodifiableMap); + assertThrows(UnsupportedOperationException.class, + () -> unmodifiableMap.put("key", "value")); + } + + @Test + void createAndInitData() { + // 链式调用创建并初始化数据 + HashMap map = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED) + .getAndModify(HashMap::new); + + HashMap expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + assertEquals(expected, map); + } + + @Test + void put() { + Map map = new MapModifier() + .put("key1", "value0") + .put("key1", "value1") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + put("key1", "value0"); + put("key1", "value1"); + } + }, map); + + new MapModifier() + .put("key1", "newValue1") + .put("key2", null) + .modify(map); + + assertEquals("newValue1", map.get("key1")); + assertTrue(map.containsKey("key2")); + assertNull(map.get("key2")); + } + + @Test + void putIfAbsent() { + Map map = new MapModifier() + .putIfAbsent("key1", null) + .putIfAbsent("key1", "value1") + .putIfAbsent("key1", "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + putIfAbsent("key1", null); + putIfAbsent("key1", "value1"); + putIfAbsent("key1", "value2"); + } + }, map); + + new MapModifier() + .putIfAbsent("key1", "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void putAll_map() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll((Map) null) + .putAll(Collections.emptyMap()) + .putAll(entries) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll(new HashMap() { + { + put("key2", "newValue2"); + put("key3", "value3"); + } + }) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void putAll_entries() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll(new SimpleEntry<>("key1", "value1"), + new SimpleEntry<>("key2", "value2")) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll() + .putAll(new SimpleEntry<>("key2", "newValue2"), + new SimpleEntry<>("key3", "value3")) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void computeIfAbsent_keyAndFunction() { + Map map = new MapModifier() + .computeIfAbsent("key1", k -> null) + .computeIfAbsent("key1", k -> "value1") + .computeIfAbsent("key1", k -> "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + computeIfAbsent("key1", k -> null); + computeIfAbsent("key1", k -> "value1"); + computeIfAbsent("key1", k -> "value2"); + } + }, map); + + new MapModifier() + .computeIfAbsent("key1", k -> "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void computeIfPresent_keyAndBiFunction() { + Map map = new HashMap() {{ + put("key1", "value1"); + }}; + new MapModifier() + .computeIfPresent("key1", (k, v) -> k + v) + .computeIfPresent("key2", (k, v) -> k + v) + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "key1value1"); + }}, map); + } + + @Test + void remove() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .remove("key2") + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "value1"); + }}, map); + } + + @Test + void clear() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .clear() + .modify(map); + assertTrue(map.isEmpty()); + } + + @Getter + static class SimpleEntry implements Map.Entry { + private final K key; + private final V value; + + public SimpleEntry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public V setValue(@Nullable V value) { + throw new UnsupportedOperationException("Unimplemented method 'setValue'"); + } + } +}
+ * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param otherMap 要添加的键值对集合。 + * 如果为 {@code null},则什么都不做。 + * + * @return MapModifier + */ + public MapModifier putAll(@Nullable Map extends K, ? extends V> otherMap) { + if (otherMap == null || otherMap.isEmpty()) { + return this; + } + return addOperationInternal(map -> map.putAll(otherMap)); + } + + /** + * 添加多个键值对。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param entries 要添加的键值对集合 + * @return MapModifier + */ + @SafeVarargs + public final MapModifier putAll(Map.Entry extends K, ? extends V>... entries) { + if (entries.length == 0) { + return this; + } + return addOperationInternal(map -> { + for (Map.Entry extends K, ? extends V> entry : entries) { + map.put(entry.getKey(), entry.getValue()); + } + }); + } + + /** + * 当 {@code key} 不存在时,计算对应的值,并添加到 {@code map} 中。 + * + * + * 调用 {@link Map#computeIfAbsent(Object, Function)}。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param mappingFunction 计算 {@code key} 对应的值 + * @return MapModifier + */ + public MapModifier computeIfAbsent(K key, + Function super K, ? extends V> mappingFunction) { + return addOperationInternal(map -> map.computeIfAbsent(key, mappingFunction)); + } + + /** + * 当 {@code key} 存在时,计算对应的值,并添加到 {@code map} 中。 + * + * + * 调用 {@link Map#computeIfPresent(Object, BiFunction)}。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param remappingFunction 计算 {@code key} 对应的值 + * @return MapModifier + */ + public MapModifier computeIfPresent(K key, + BiFunction super K, ? super V, ? extends V> remappingFunction) { + return addOperationInternal(map -> map.computeIfPresent(key, remappingFunction)); + } + + /** + * 删除 {@code key}。 + * + * + * 注意:key 是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要删除的 {@code key} + * @return MapModifier + */ + public MapModifier remove(K key) { + return addOperationInternal(map -> map.remove(key)); + } + + /** + * 清空 {@code map} + * + * @return MapModifier + */ + public MapModifier clear() { + return addOperationInternal(Map::clear); + } + + /** + * 修改 {@code map} + * + * @param map 要修改的 {@code map} + * @return 修改后的 {@code map}。当入参是 {@code null} 时,返回 {@code null}。 + */ + public > void modify(@Nullable T map) { + if (map != null) { + this.operators.accept(map); + } + } + + /** + * 修改 {@code map} + * + * @param mapSupplier {@code map} 的 {@link Supplier} + * @return 修改后的 {@code map}。 + * 当从 {@code mapSupplier} 获取的 {@code map} 为 {@code null} 时,返回 {@code null}。 + */ + @CheckForNull + public > T getAndModify(Supplier mapSupplier) { + checkArgumentNotNull(mapSupplier, "The map supplier cannot be null."); + T map = mapSupplier.get(); + modify(map); + return map; + } + + /** + * 创建一个有初始化数据的不可变的 {@code Map} + * + * @return 不可变的 {@code Map} + */ + public Map getUnmodifiableMap() { + return Collections.unmodifiableMap(getAndModify(HashMap::new)); + } + + private MapModifier addOperationInternal(Consumer> operator) { + this.operators = this.operators.andThen(operator); + return this; + } +} diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java new file mode 100644 index 0000000..d372d27 --- /dev/null +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java @@ -0,0 +1,316 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.zhouxy.plusone.commons.collection; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.Nullable; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableMap; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MapModifierTests { + + private static final String APP_START_ID = UUID.randomUUID().toString(); + private static final String LOCKED = "LOCKED"; + + private static final Map commonProperties = ImmutableMap.builder() + .put("channel", "MOBILE") + .put("appStartId", APP_START_ID) + .build(); + + @Test + void demo() { + Map expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + + // MapModifier + MapModifier modifier = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED); + + // 从 Supplier 中获取 Map,并修改数据 + HashMap hashMap1 = modifier.getAndModify(HashMap::new); + assertEquals(expected, hashMap1); + + // 可以灵活使用不同 Map 类型的不同构造器 + HashMap hashMap2 = modifier.getAndModify(() -> new HashMap<>(8)); + assertEquals(expected, hashMap2); + + // HashMap hashMap3 = modifier.getAndModify(() -> new HashMap<>(anotherMap)); + TreeMap treeMap = modifier.getAndModify(TreeMap::new); + assertEquals(expected, treeMap); + ConcurrentHashMap concurrentHashMap = modifier.getAndModify(ConcurrentHashMap::new); + assertEquals(expected, concurrentHashMap); + + assertNull(modifier.getAndModify(() -> (Map) null)); + + // 修改已有的 Map + Map srcMap = new HashMap<>(); + srcMap.put("srcKey1", "srcValue1"); + srcMap.put("srcKey2", "srcValue2"); + modifier.modify(srcMap); + assertEquals(new HashMap() { + { + putAll(commonProperties); + put("username", "Ben"); + put("accountStatus", LOCKED); + put("srcKey1", "srcValue1"); + put("srcKey2", "srcValue2"); + } + }, srcMap); + + assertDoesNotThrow(() -> modifier.modify((Map) null)); + + // 创建一个有初始化数据的不可变的 {@code Map} + Map unmodifiableMap = modifier.getUnmodifiableMap(); + assertEquals(expected, unmodifiableMap); + assertThrows(UnsupportedOperationException.class, + () -> unmodifiableMap.put("key", "value")); + } + + @Test + void createAndInitData() { + // 链式调用创建并初始化数据 + HashMap map = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED) + .getAndModify(HashMap::new); + + HashMap expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + assertEquals(expected, map); + } + + @Test + void put() { + Map map = new MapModifier() + .put("key1", "value0") + .put("key1", "value1") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + put("key1", "value0"); + put("key1", "value1"); + } + }, map); + + new MapModifier() + .put("key1", "newValue1") + .put("key2", null) + .modify(map); + + assertEquals("newValue1", map.get("key1")); + assertTrue(map.containsKey("key2")); + assertNull(map.get("key2")); + } + + @Test + void putIfAbsent() { + Map map = new MapModifier() + .putIfAbsent("key1", null) + .putIfAbsent("key1", "value1") + .putIfAbsent("key1", "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + putIfAbsent("key1", null); + putIfAbsent("key1", "value1"); + putIfAbsent("key1", "value2"); + } + }, map); + + new MapModifier() + .putIfAbsent("key1", "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void putAll_map() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll((Map) null) + .putAll(Collections.emptyMap()) + .putAll(entries) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll(new HashMap() { + { + put("key2", "newValue2"); + put("key3", "value3"); + } + }) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void putAll_entries() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll(new SimpleEntry<>("key1", "value1"), + new SimpleEntry<>("key2", "value2")) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll() + .putAll(new SimpleEntry<>("key2", "newValue2"), + new SimpleEntry<>("key3", "value3")) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void computeIfAbsent_keyAndFunction() { + Map map = new MapModifier() + .computeIfAbsent("key1", k -> null) + .computeIfAbsent("key1", k -> "value1") + .computeIfAbsent("key1", k -> "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + computeIfAbsent("key1", k -> null); + computeIfAbsent("key1", k -> "value1"); + computeIfAbsent("key1", k -> "value2"); + } + }, map); + + new MapModifier() + .computeIfAbsent("key1", k -> "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void computeIfPresent_keyAndBiFunction() { + Map map = new HashMap() {{ + put("key1", "value1"); + }}; + new MapModifier() + .computeIfPresent("key1", (k, v) -> k + v) + .computeIfPresent("key2", (k, v) -> k + v) + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "key1value1"); + }}, map); + } + + @Test + void remove() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .remove("key2") + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "value1"); + }}, map); + } + + @Test + void clear() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .clear() + .modify(map); + assertTrue(map.isEmpty()); + } + + @Getter + static class SimpleEntry implements Map.Entry { + private final K key; + private final V value; + + public SimpleEntry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public V setValue(@Nullable V value) { + throw new UnsupportedOperationException("Unimplemented method 'setValue'"); + } + } +}
+ * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param entries 要添加的键值对集合 + * @return MapModifier + */ + @SafeVarargs + public final MapModifier putAll(Map.Entry extends K, ? extends V>... entries) { + if (entries.length == 0) { + return this; + } + return addOperationInternal(map -> { + for (Map.Entry extends K, ? extends V> entry : entries) { + map.put(entry.getKey(), entry.getValue()); + } + }); + } + + /** + * 当 {@code key} 不存在时,计算对应的值,并添加到 {@code map} 中。 + * + * + * 调用 {@link Map#computeIfAbsent(Object, Function)}。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param mappingFunction 计算 {@code key} 对应的值 + * @return MapModifier + */ + public MapModifier computeIfAbsent(K key, + Function super K, ? extends V> mappingFunction) { + return addOperationInternal(map -> map.computeIfAbsent(key, mappingFunction)); + } + + /** + * 当 {@code key} 存在时,计算对应的值,并添加到 {@code map} 中。 + * + * + * 调用 {@link Map#computeIfPresent(Object, BiFunction)}。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param remappingFunction 计算 {@code key} 对应的值 + * @return MapModifier + */ + public MapModifier computeIfPresent(K key, + BiFunction super K, ? super V, ? extends V> remappingFunction) { + return addOperationInternal(map -> map.computeIfPresent(key, remappingFunction)); + } + + /** + * 删除 {@code key}。 + * + * + * 注意:key 是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要删除的 {@code key} + * @return MapModifier + */ + public MapModifier remove(K key) { + return addOperationInternal(map -> map.remove(key)); + } + + /** + * 清空 {@code map} + * + * @return MapModifier + */ + public MapModifier clear() { + return addOperationInternal(Map::clear); + } + + /** + * 修改 {@code map} + * + * @param map 要修改的 {@code map} + * @return 修改后的 {@code map}。当入参是 {@code null} 时,返回 {@code null}。 + */ + public > void modify(@Nullable T map) { + if (map != null) { + this.operators.accept(map); + } + } + + /** + * 修改 {@code map} + * + * @param mapSupplier {@code map} 的 {@link Supplier} + * @return 修改后的 {@code map}。 + * 当从 {@code mapSupplier} 获取的 {@code map} 为 {@code null} 时,返回 {@code null}。 + */ + @CheckForNull + public > T getAndModify(Supplier mapSupplier) { + checkArgumentNotNull(mapSupplier, "The map supplier cannot be null."); + T map = mapSupplier.get(); + modify(map); + return map; + } + + /** + * 创建一个有初始化数据的不可变的 {@code Map} + * + * @return 不可变的 {@code Map} + */ + public Map getUnmodifiableMap() { + return Collections.unmodifiableMap(getAndModify(HashMap::new)); + } + + private MapModifier addOperationInternal(Consumer> operator) { + this.operators = this.operators.andThen(operator); + return this; + } +} diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java new file mode 100644 index 0000000..d372d27 --- /dev/null +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java @@ -0,0 +1,316 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.zhouxy.plusone.commons.collection; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.Nullable; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableMap; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MapModifierTests { + + private static final String APP_START_ID = UUID.randomUUID().toString(); + private static final String LOCKED = "LOCKED"; + + private static final Map commonProperties = ImmutableMap.builder() + .put("channel", "MOBILE") + .put("appStartId", APP_START_ID) + .build(); + + @Test + void demo() { + Map expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + + // MapModifier + MapModifier modifier = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED); + + // 从 Supplier 中获取 Map,并修改数据 + HashMap hashMap1 = modifier.getAndModify(HashMap::new); + assertEquals(expected, hashMap1); + + // 可以灵活使用不同 Map 类型的不同构造器 + HashMap hashMap2 = modifier.getAndModify(() -> new HashMap<>(8)); + assertEquals(expected, hashMap2); + + // HashMap hashMap3 = modifier.getAndModify(() -> new HashMap<>(anotherMap)); + TreeMap treeMap = modifier.getAndModify(TreeMap::new); + assertEquals(expected, treeMap); + ConcurrentHashMap concurrentHashMap = modifier.getAndModify(ConcurrentHashMap::new); + assertEquals(expected, concurrentHashMap); + + assertNull(modifier.getAndModify(() -> (Map) null)); + + // 修改已有的 Map + Map srcMap = new HashMap<>(); + srcMap.put("srcKey1", "srcValue1"); + srcMap.put("srcKey2", "srcValue2"); + modifier.modify(srcMap); + assertEquals(new HashMap() { + { + putAll(commonProperties); + put("username", "Ben"); + put("accountStatus", LOCKED); + put("srcKey1", "srcValue1"); + put("srcKey2", "srcValue2"); + } + }, srcMap); + + assertDoesNotThrow(() -> modifier.modify((Map) null)); + + // 创建一个有初始化数据的不可变的 {@code Map} + Map unmodifiableMap = modifier.getUnmodifiableMap(); + assertEquals(expected, unmodifiableMap); + assertThrows(UnsupportedOperationException.class, + () -> unmodifiableMap.put("key", "value")); + } + + @Test + void createAndInitData() { + // 链式调用创建并初始化数据 + HashMap map = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED) + .getAndModify(HashMap::new); + + HashMap expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + assertEquals(expected, map); + } + + @Test + void put() { + Map map = new MapModifier() + .put("key1", "value0") + .put("key1", "value1") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + put("key1", "value0"); + put("key1", "value1"); + } + }, map); + + new MapModifier() + .put("key1", "newValue1") + .put("key2", null) + .modify(map); + + assertEquals("newValue1", map.get("key1")); + assertTrue(map.containsKey("key2")); + assertNull(map.get("key2")); + } + + @Test + void putIfAbsent() { + Map map = new MapModifier() + .putIfAbsent("key1", null) + .putIfAbsent("key1", "value1") + .putIfAbsent("key1", "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + putIfAbsent("key1", null); + putIfAbsent("key1", "value1"); + putIfAbsent("key1", "value2"); + } + }, map); + + new MapModifier() + .putIfAbsent("key1", "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void putAll_map() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll((Map) null) + .putAll(Collections.emptyMap()) + .putAll(entries) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll(new HashMap() { + { + put("key2", "newValue2"); + put("key3", "value3"); + } + }) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void putAll_entries() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll(new SimpleEntry<>("key1", "value1"), + new SimpleEntry<>("key2", "value2")) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll() + .putAll(new SimpleEntry<>("key2", "newValue2"), + new SimpleEntry<>("key3", "value3")) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void computeIfAbsent_keyAndFunction() { + Map map = new MapModifier() + .computeIfAbsent("key1", k -> null) + .computeIfAbsent("key1", k -> "value1") + .computeIfAbsent("key1", k -> "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + computeIfAbsent("key1", k -> null); + computeIfAbsent("key1", k -> "value1"); + computeIfAbsent("key1", k -> "value2"); + } + }, map); + + new MapModifier() + .computeIfAbsent("key1", k -> "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void computeIfPresent_keyAndBiFunction() { + Map map = new HashMap() {{ + put("key1", "value1"); + }}; + new MapModifier() + .computeIfPresent("key1", (k, v) -> k + v) + .computeIfPresent("key2", (k, v) -> k + v) + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "key1value1"); + }}, map); + } + + @Test + void remove() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .remove("key2") + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "value1"); + }}, map); + } + + @Test + void clear() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .clear() + .modify(map); + assertTrue(map.isEmpty()); + } + + @Getter + static class SimpleEntry implements Map.Entry { + private final K key; + private final V value; + + public SimpleEntry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public V setValue(@Nullable V value) { + throw new UnsupportedOperationException("Unimplemented method 'setValue'"); + } + } +}
+ * 调用 {@link Map#computeIfAbsent(Object, Function)}。 + * + *
+ * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param mappingFunction 计算 {@code key} 对应的值 + * @return MapModifier + */ + public MapModifier computeIfAbsent(K key, + Function super K, ? extends V> mappingFunction) { + return addOperationInternal(map -> map.computeIfAbsent(key, mappingFunction)); + } + + /** + * 当 {@code key} 存在时,计算对应的值,并添加到 {@code map} 中。 + * + * + * 调用 {@link Map#computeIfPresent(Object, BiFunction)}。 + * + * + * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param remappingFunction 计算 {@code key} 对应的值 + * @return MapModifier + */ + public MapModifier computeIfPresent(K key, + BiFunction super K, ? super V, ? extends V> remappingFunction) { + return addOperationInternal(map -> map.computeIfPresent(key, remappingFunction)); + } + + /** + * 删除 {@code key}。 + * + * + * 注意:key 是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要删除的 {@code key} + * @return MapModifier + */ + public MapModifier remove(K key) { + return addOperationInternal(map -> map.remove(key)); + } + + /** + * 清空 {@code map} + * + * @return MapModifier + */ + public MapModifier clear() { + return addOperationInternal(Map::clear); + } + + /** + * 修改 {@code map} + * + * @param map 要修改的 {@code map} + * @return 修改后的 {@code map}。当入参是 {@code null} 时,返回 {@code null}。 + */ + public > void modify(@Nullable T map) { + if (map != null) { + this.operators.accept(map); + } + } + + /** + * 修改 {@code map} + * + * @param mapSupplier {@code map} 的 {@link Supplier} + * @return 修改后的 {@code map}。 + * 当从 {@code mapSupplier} 获取的 {@code map} 为 {@code null} 时,返回 {@code null}。 + */ + @CheckForNull + public > T getAndModify(Supplier mapSupplier) { + checkArgumentNotNull(mapSupplier, "The map supplier cannot be null."); + T map = mapSupplier.get(); + modify(map); + return map; + } + + /** + * 创建一个有初始化数据的不可变的 {@code Map} + * + * @return 不可变的 {@code Map} + */ + public Map getUnmodifiableMap() { + return Collections.unmodifiableMap(getAndModify(HashMap::new)); + } + + private MapModifier addOperationInternal(Consumer> operator) { + this.operators = this.operators.andThen(operator); + return this; + } +} diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java new file mode 100644 index 0000000..d372d27 --- /dev/null +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java @@ -0,0 +1,316 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.zhouxy.plusone.commons.collection; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.Nullable; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableMap; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MapModifierTests { + + private static final String APP_START_ID = UUID.randomUUID().toString(); + private static final String LOCKED = "LOCKED"; + + private static final Map commonProperties = ImmutableMap.builder() + .put("channel", "MOBILE") + .put("appStartId", APP_START_ID) + .build(); + + @Test + void demo() { + Map expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + + // MapModifier + MapModifier modifier = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED); + + // 从 Supplier 中获取 Map,并修改数据 + HashMap hashMap1 = modifier.getAndModify(HashMap::new); + assertEquals(expected, hashMap1); + + // 可以灵活使用不同 Map 类型的不同构造器 + HashMap hashMap2 = modifier.getAndModify(() -> new HashMap<>(8)); + assertEquals(expected, hashMap2); + + // HashMap hashMap3 = modifier.getAndModify(() -> new HashMap<>(anotherMap)); + TreeMap treeMap = modifier.getAndModify(TreeMap::new); + assertEquals(expected, treeMap); + ConcurrentHashMap concurrentHashMap = modifier.getAndModify(ConcurrentHashMap::new); + assertEquals(expected, concurrentHashMap); + + assertNull(modifier.getAndModify(() -> (Map) null)); + + // 修改已有的 Map + Map srcMap = new HashMap<>(); + srcMap.put("srcKey1", "srcValue1"); + srcMap.put("srcKey2", "srcValue2"); + modifier.modify(srcMap); + assertEquals(new HashMap() { + { + putAll(commonProperties); + put("username", "Ben"); + put("accountStatus", LOCKED); + put("srcKey1", "srcValue1"); + put("srcKey2", "srcValue2"); + } + }, srcMap); + + assertDoesNotThrow(() -> modifier.modify((Map) null)); + + // 创建一个有初始化数据的不可变的 {@code Map} + Map unmodifiableMap = modifier.getUnmodifiableMap(); + assertEquals(expected, unmodifiableMap); + assertThrows(UnsupportedOperationException.class, + () -> unmodifiableMap.put("key", "value")); + } + + @Test + void createAndInitData() { + // 链式调用创建并初始化数据 + HashMap map = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED) + .getAndModify(HashMap::new); + + HashMap expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + assertEquals(expected, map); + } + + @Test + void put() { + Map map = new MapModifier() + .put("key1", "value0") + .put("key1", "value1") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + put("key1", "value0"); + put("key1", "value1"); + } + }, map); + + new MapModifier() + .put("key1", "newValue1") + .put("key2", null) + .modify(map); + + assertEquals("newValue1", map.get("key1")); + assertTrue(map.containsKey("key2")); + assertNull(map.get("key2")); + } + + @Test + void putIfAbsent() { + Map map = new MapModifier() + .putIfAbsent("key1", null) + .putIfAbsent("key1", "value1") + .putIfAbsent("key1", "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + putIfAbsent("key1", null); + putIfAbsent("key1", "value1"); + putIfAbsent("key1", "value2"); + } + }, map); + + new MapModifier() + .putIfAbsent("key1", "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void putAll_map() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll((Map) null) + .putAll(Collections.emptyMap()) + .putAll(entries) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll(new HashMap() { + { + put("key2", "newValue2"); + put("key3", "value3"); + } + }) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void putAll_entries() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll(new SimpleEntry<>("key1", "value1"), + new SimpleEntry<>("key2", "value2")) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll() + .putAll(new SimpleEntry<>("key2", "newValue2"), + new SimpleEntry<>("key3", "value3")) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void computeIfAbsent_keyAndFunction() { + Map map = new MapModifier() + .computeIfAbsent("key1", k -> null) + .computeIfAbsent("key1", k -> "value1") + .computeIfAbsent("key1", k -> "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + computeIfAbsent("key1", k -> null); + computeIfAbsent("key1", k -> "value1"); + computeIfAbsent("key1", k -> "value2"); + } + }, map); + + new MapModifier() + .computeIfAbsent("key1", k -> "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void computeIfPresent_keyAndBiFunction() { + Map map = new HashMap() {{ + put("key1", "value1"); + }}; + new MapModifier() + .computeIfPresent("key1", (k, v) -> k + v) + .computeIfPresent("key2", (k, v) -> k + v) + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "key1value1"); + }}, map); + } + + @Test + void remove() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .remove("key2") + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "value1"); + }}, map); + } + + @Test + void clear() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .clear() + .modify(map); + assertTrue(map.isEmpty()); + } + + @Getter + static class SimpleEntry implements Map.Entry { + private final K key; + private final V value; + + public SimpleEntry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public V setValue(@Nullable V value) { + throw new UnsupportedOperationException("Unimplemented method 'setValue'"); + } + } +}
+ * 调用 {@link Map#computeIfPresent(Object, BiFunction)}。 + * + *
+ * 注意:键值对是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要添加的 {@code key} + * @param remappingFunction 计算 {@code key} 对应的值 + * @return MapModifier + */ + public MapModifier computeIfPresent(K key, + BiFunction super K, ? super V, ? extends V> remappingFunction) { + return addOperationInternal(map -> map.computeIfPresent(key, remappingFunction)); + } + + /** + * 删除 {@code key}。 + * + * + * 注意:key 是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要删除的 {@code key} + * @return MapModifier + */ + public MapModifier remove(K key) { + return addOperationInternal(map -> map.remove(key)); + } + + /** + * 清空 {@code map} + * + * @return MapModifier + */ + public MapModifier clear() { + return addOperationInternal(Map::clear); + } + + /** + * 修改 {@code map} + * + * @param map 要修改的 {@code map} + * @return 修改后的 {@code map}。当入参是 {@code null} 时,返回 {@code null}。 + */ + public > void modify(@Nullable T map) { + if (map != null) { + this.operators.accept(map); + } + } + + /** + * 修改 {@code map} + * + * @param mapSupplier {@code map} 的 {@link Supplier} + * @return 修改后的 {@code map}。 + * 当从 {@code mapSupplier} 获取的 {@code map} 为 {@code null} 时,返回 {@code null}。 + */ + @CheckForNull + public > T getAndModify(Supplier mapSupplier) { + checkArgumentNotNull(mapSupplier, "The map supplier cannot be null."); + T map = mapSupplier.get(); + modify(map); + return map; + } + + /** + * 创建一个有初始化数据的不可变的 {@code Map} + * + * @return 不可变的 {@code Map} + */ + public Map getUnmodifiableMap() { + return Collections.unmodifiableMap(getAndModify(HashMap::new)); + } + + private MapModifier addOperationInternal(Consumer> operator) { + this.operators = this.operators.andThen(operator); + return this; + } +} diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java new file mode 100644 index 0000000..d372d27 --- /dev/null +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java @@ -0,0 +1,316 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.zhouxy.plusone.commons.collection; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.Nullable; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableMap; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MapModifierTests { + + private static final String APP_START_ID = UUID.randomUUID().toString(); + private static final String LOCKED = "LOCKED"; + + private static final Map commonProperties = ImmutableMap.builder() + .put("channel", "MOBILE") + .put("appStartId", APP_START_ID) + .build(); + + @Test + void demo() { + Map expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + + // MapModifier + MapModifier modifier = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED); + + // 从 Supplier 中获取 Map,并修改数据 + HashMap hashMap1 = modifier.getAndModify(HashMap::new); + assertEquals(expected, hashMap1); + + // 可以灵活使用不同 Map 类型的不同构造器 + HashMap hashMap2 = modifier.getAndModify(() -> new HashMap<>(8)); + assertEquals(expected, hashMap2); + + // HashMap hashMap3 = modifier.getAndModify(() -> new HashMap<>(anotherMap)); + TreeMap treeMap = modifier.getAndModify(TreeMap::new); + assertEquals(expected, treeMap); + ConcurrentHashMap concurrentHashMap = modifier.getAndModify(ConcurrentHashMap::new); + assertEquals(expected, concurrentHashMap); + + assertNull(modifier.getAndModify(() -> (Map) null)); + + // 修改已有的 Map + Map srcMap = new HashMap<>(); + srcMap.put("srcKey1", "srcValue1"); + srcMap.put("srcKey2", "srcValue2"); + modifier.modify(srcMap); + assertEquals(new HashMap() { + { + putAll(commonProperties); + put("username", "Ben"); + put("accountStatus", LOCKED); + put("srcKey1", "srcValue1"); + put("srcKey2", "srcValue2"); + } + }, srcMap); + + assertDoesNotThrow(() -> modifier.modify((Map) null)); + + // 创建一个有初始化数据的不可变的 {@code Map} + Map unmodifiableMap = modifier.getUnmodifiableMap(); + assertEquals(expected, unmodifiableMap); + assertThrows(UnsupportedOperationException.class, + () -> unmodifiableMap.put("key", "value")); + } + + @Test + void createAndInitData() { + // 链式调用创建并初始化数据 + HashMap map = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED) + .getAndModify(HashMap::new); + + HashMap expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + assertEquals(expected, map); + } + + @Test + void put() { + Map map = new MapModifier() + .put("key1", "value0") + .put("key1", "value1") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + put("key1", "value0"); + put("key1", "value1"); + } + }, map); + + new MapModifier() + .put("key1", "newValue1") + .put("key2", null) + .modify(map); + + assertEquals("newValue1", map.get("key1")); + assertTrue(map.containsKey("key2")); + assertNull(map.get("key2")); + } + + @Test + void putIfAbsent() { + Map map = new MapModifier() + .putIfAbsent("key1", null) + .putIfAbsent("key1", "value1") + .putIfAbsent("key1", "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + putIfAbsent("key1", null); + putIfAbsent("key1", "value1"); + putIfAbsent("key1", "value2"); + } + }, map); + + new MapModifier() + .putIfAbsent("key1", "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void putAll_map() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll((Map) null) + .putAll(Collections.emptyMap()) + .putAll(entries) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll(new HashMap() { + { + put("key2", "newValue2"); + put("key3", "value3"); + } + }) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void putAll_entries() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll(new SimpleEntry<>("key1", "value1"), + new SimpleEntry<>("key2", "value2")) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll() + .putAll(new SimpleEntry<>("key2", "newValue2"), + new SimpleEntry<>("key3", "value3")) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void computeIfAbsent_keyAndFunction() { + Map map = new MapModifier() + .computeIfAbsent("key1", k -> null) + .computeIfAbsent("key1", k -> "value1") + .computeIfAbsent("key1", k -> "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + computeIfAbsent("key1", k -> null); + computeIfAbsent("key1", k -> "value1"); + computeIfAbsent("key1", k -> "value2"); + } + }, map); + + new MapModifier() + .computeIfAbsent("key1", k -> "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void computeIfPresent_keyAndBiFunction() { + Map map = new HashMap() {{ + put("key1", "value1"); + }}; + new MapModifier() + .computeIfPresent("key1", (k, v) -> k + v) + .computeIfPresent("key2", (k, v) -> k + v) + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "key1value1"); + }}, map); + } + + @Test + void remove() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .remove("key2") + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "value1"); + }}, map); + } + + @Test + void clear() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .clear() + .modify(map); + assertTrue(map.isEmpty()); + } + + @Getter + static class SimpleEntry implements Map.Entry { + private final K key; + private final V value; + + public SimpleEntry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public V setValue(@Nullable V value) { + throw new UnsupportedOperationException("Unimplemented method 'setValue'"); + } + } +}
+ * 注意:key 是否允许为 {@code null},由最终作用的 {@link Map} 类型决定。 + * + * @param key 要删除的 {@code key} + * @return MapModifier + */ + public MapModifier remove(K key) { + return addOperationInternal(map -> map.remove(key)); + } + + /** + * 清空 {@code map} + * + * @return MapModifier + */ + public MapModifier clear() { + return addOperationInternal(Map::clear); + } + + /** + * 修改 {@code map} + * + * @param map 要修改的 {@code map} + * @return 修改后的 {@code map}。当入参是 {@code null} 时,返回 {@code null}。 + */ + public > void modify(@Nullable T map) { + if (map != null) { + this.operators.accept(map); + } + } + + /** + * 修改 {@code map} + * + * @param mapSupplier {@code map} 的 {@link Supplier} + * @return 修改后的 {@code map}。 + * 当从 {@code mapSupplier} 获取的 {@code map} 为 {@code null} 时,返回 {@code null}。 + */ + @CheckForNull + public > T getAndModify(Supplier mapSupplier) { + checkArgumentNotNull(mapSupplier, "The map supplier cannot be null."); + T map = mapSupplier.get(); + modify(map); + return map; + } + + /** + * 创建一个有初始化数据的不可变的 {@code Map} + * + * @return 不可变的 {@code Map} + */ + public Map getUnmodifiableMap() { + return Collections.unmodifiableMap(getAndModify(HashMap::new)); + } + + private MapModifier addOperationInternal(Consumer> operator) { + this.operators = this.operators.andThen(operator); + return this; + } +} diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java new file mode 100644 index 0000000..d372d27 --- /dev/null +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/collection/MapModifierTests.java @@ -0,0 +1,316 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.zhouxy.plusone.commons.collection; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.Nullable; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableMap; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MapModifierTests { + + private static final String APP_START_ID = UUID.randomUUID().toString(); + private static final String LOCKED = "LOCKED"; + + private static final Map commonProperties = ImmutableMap.builder() + .put("channel", "MOBILE") + .put("appStartId", APP_START_ID) + .build(); + + @Test + void demo() { + Map expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + + // MapModifier + MapModifier modifier = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED); + + // 从 Supplier 中获取 Map,并修改数据 + HashMap hashMap1 = modifier.getAndModify(HashMap::new); + assertEquals(expected, hashMap1); + + // 可以灵活使用不同 Map 类型的不同构造器 + HashMap hashMap2 = modifier.getAndModify(() -> new HashMap<>(8)); + assertEquals(expected, hashMap2); + + // HashMap hashMap3 = modifier.getAndModify(() -> new HashMap<>(anotherMap)); + TreeMap treeMap = modifier.getAndModify(TreeMap::new); + assertEquals(expected, treeMap); + ConcurrentHashMap concurrentHashMap = modifier.getAndModify(ConcurrentHashMap::new); + assertEquals(expected, concurrentHashMap); + + assertNull(modifier.getAndModify(() -> (Map) null)); + + // 修改已有的 Map + Map srcMap = new HashMap<>(); + srcMap.put("srcKey1", "srcValue1"); + srcMap.put("srcKey2", "srcValue2"); + modifier.modify(srcMap); + assertEquals(new HashMap() { + { + putAll(commonProperties); + put("username", "Ben"); + put("accountStatus", LOCKED); + put("srcKey1", "srcValue1"); + put("srcKey2", "srcValue2"); + } + }, srcMap); + + assertDoesNotThrow(() -> modifier.modify((Map) null)); + + // 创建一个有初始化数据的不可变的 {@code Map} + Map unmodifiableMap = modifier.getUnmodifiableMap(); + assertEquals(expected, unmodifiableMap); + assertThrows(UnsupportedOperationException.class, + () -> unmodifiableMap.put("key", "value")); + } + + @Test + void createAndInitData() { + // 链式调用创建并初始化数据 + HashMap map = new MapModifier() + .putAll(commonProperties) + .put("username", "Ben") + .put("accountStatus", LOCKED) + .getAndModify(HashMap::new); + + HashMap expected = new HashMap() { + { + put("channel", "MOBILE"); + put("appStartId", APP_START_ID); + put("username", "Ben"); + put("accountStatus", LOCKED); + } + }; + assertEquals(expected, map); + } + + @Test + void put() { + Map map = new MapModifier() + .put("key1", "value0") + .put("key1", "value1") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + put("key1", "value0"); + put("key1", "value1"); + } + }, map); + + new MapModifier() + .put("key1", "newValue1") + .put("key2", null) + .modify(map); + + assertEquals("newValue1", map.get("key1")); + assertTrue(map.containsKey("key2")); + assertNull(map.get("key2")); + } + + @Test + void putIfAbsent() { + Map map = new MapModifier() + .putIfAbsent("key1", null) + .putIfAbsent("key1", "value1") + .putIfAbsent("key1", "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + putIfAbsent("key1", null); + putIfAbsent("key1", "value1"); + putIfAbsent("key1", "value2"); + } + }, map); + + new MapModifier() + .putIfAbsent("key1", "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void putAll_map() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll((Map) null) + .putAll(Collections.emptyMap()) + .putAll(entries) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll(new HashMap() { + { + put("key2", "newValue2"); + put("key3", "value3"); + } + }) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void putAll_entries() { + Map entries = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + Map map = new MapModifier() + .putAll(new SimpleEntry<>("key1", "value1"), + new SimpleEntry<>("key2", "value2")) + .getAndModify(HashMap::new); + assertEquals(entries, map); + new MapModifier() + .putAll() + .putAll(new SimpleEntry<>("key2", "newValue2"), + new SimpleEntry<>("key3", "value3")) + .modify(map); + assertEquals(new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + put("key2", "newValue2"); + put("key3", "value3"); + } + }, map); + } + + @Test + void computeIfAbsent_keyAndFunction() { + Map map = new MapModifier() + .computeIfAbsent("key1", k -> null) + .computeIfAbsent("key1", k -> "value1") + .computeIfAbsent("key1", k -> "value2") + .getAndModify(HashMap::new); + + assertEquals(new HashMap() { + { + computeIfAbsent("key1", k -> null); + computeIfAbsent("key1", k -> "value1"); + computeIfAbsent("key1", k -> "value2"); + } + }, map); + + new MapModifier() + .computeIfAbsent("key1", k -> "newValue1") + .modify(map); + + assertTrue(map.containsKey("key1")); + assertEquals("value1", map.get("key1")); + } + + @Test + void computeIfPresent_keyAndBiFunction() { + Map map = new HashMap() {{ + put("key1", "value1"); + }}; + new MapModifier() + .computeIfPresent("key1", (k, v) -> k + v) + .computeIfPresent("key2", (k, v) -> k + v) + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "key1value1"); + }}, map); + } + + @Test + void remove() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .remove("key2") + .modify(map); + assertEquals(new HashMap() {{ + put("key1", "value1"); + }}, map); + } + + @Test + void clear() { + Map map = new HashMap() {{ + put("key1", "value1"); + put("key2", "value2"); + }}; + new MapModifier() + .clear() + .modify(map); + assertTrue(map.isEmpty()); + } + + @Getter + static class SimpleEntry implements Map.Entry { + private final K key; + private final V value; + + public SimpleEntry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public V setValue(@Nullable V value) { + throw new UnsupportedOperationException("Unimplemented method 'setValue'"); + } + } +}