LinkedCaseInsensitiveMap delegates to LinkedHashMap instead of extending it

Issue: SPR-15026
This commit is contained in:
Juergen Hoeller 2016-12-23 18:57:50 +01:00
parent d3f97e3092
commit 8147c112f5
3 changed files with 123 additions and 33 deletions

View File

@ -16,10 +16,13 @@
package org.springframework.util; package org.springframework.util;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* {@link LinkedHashMap} variant that stores String keys in a case-insensitive * {@link LinkedHashMap} variant that stores String keys in a case-insensitive
@ -34,9 +37,11 @@ import java.util.Map;
* @since 3.0 * @since 3.0
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> { public class LinkedCaseInsensitiveMap<V> implements Map<String, V>, Serializable, Cloneable {
private Map<String, String> caseInsensitiveKeys; private final LinkedHashMap<String, V> targetMap;
private final HashMap<String, String> caseInsensitiveKeys;
private final Locale locale; private final Locale locale;
@ -46,7 +51,7 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
* @see java.lang.String#toLowerCase() * @see java.lang.String#toLowerCase()
*/ */
public LinkedCaseInsensitiveMap() { public LinkedCaseInsensitiveMap() {
this(null); this((Locale) null);
} }
/** /**
@ -56,9 +61,7 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
* @see java.lang.String#toLowerCase(java.util.Locale) * @see java.lang.String#toLowerCase(java.util.Locale)
*/ */
public LinkedCaseInsensitiveMap(Locale locale) { public LinkedCaseInsensitiveMap(Locale locale) {
super(); this(16, locale);
this.caseInsensitiveKeys = new HashMap<>();
this.locale = (locale != null ? locale : Locale.getDefault());
} }
/** /**
@ -81,19 +84,68 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
* @see java.lang.String#toLowerCase(java.util.Locale) * @see java.lang.String#toLowerCase(java.util.Locale)
*/ */
public LinkedCaseInsensitiveMap(int initialCapacity, Locale locale) { public LinkedCaseInsensitiveMap(int initialCapacity, Locale locale) {
super(initialCapacity); this.targetMap = new LinkedHashMap<String, V>(initialCapacity) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, V> eldest) {
boolean doRemove = LinkedCaseInsensitiveMap.this.removeEldestEntry(eldest);
if (doRemove) {
caseInsensitiveKeys.remove(convertKey(eldest.getKey()));
}
return doRemove;
}
};
this.caseInsensitiveKeys = new HashMap<>(initialCapacity); this.caseInsensitiveKeys = new HashMap<>(initialCapacity);
this.locale = (locale != null ? locale : Locale.getDefault()); this.locale = (locale != null ? locale : Locale.getDefault());
} }
/**
* Copy constructor.
*/
@SuppressWarnings("unchecked")
private LinkedCaseInsensitiveMap(LinkedCaseInsensitiveMap<V> other) {
this.targetMap = (LinkedHashMap<String, V>) other.targetMap.clone();
this.caseInsensitiveKeys = (HashMap<String, String>) other.caseInsensitiveKeys.clone();
this.locale = other.locale;
}
@Override
public int size() {
return this.targetMap.size();
}
@Override
public boolean isEmpty() {
return this.targetMap.isEmpty();
}
@Override
public boolean containsValue(Object value) {
return this.targetMap.containsValue(value);
}
@Override
public Set<String> keySet() {
return this.targetMap.keySet();
}
@Override
public Collection<V> values() {
return this.targetMap.values();
}
@Override
public Set<Entry<String, V>> entrySet() {
return this.targetMap.entrySet();
}
@Override @Override
public V put(String key, V value) { public V put(String key, V value) {
String oldKey = this.caseInsensitiveKeys.put(convertKey(key), key); String oldKey = this.caseInsensitiveKeys.put(convertKey(key), key);
if (oldKey != null && !oldKey.equals(key)) { if (oldKey != null && !oldKey.equals(key)) {
super.remove(oldKey); this.targetMap.remove(oldKey);
} }
return super.put(key, value); return this.targetMap.put(key, value);
} }
@Override @Override
@ -116,19 +168,18 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
if (key instanceof String) { if (key instanceof String) {
String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key)); String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key));
if (caseInsensitiveKey != null) { if (caseInsensitiveKey != null) {
return super.get(caseInsensitiveKey); return this.targetMap.get(caseInsensitiveKey);
} }
} }
return null; return null;
} }
// Overridden to avoid LinkedHashMap's own hash computation in its getOrDefault impl
@Override @Override
public V getOrDefault(Object key, V defaultValue) { public V getOrDefault(Object key, V defaultValue) {
if (key instanceof String) { if (key instanceof String) {
String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key)); String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key));
if (caseInsensitiveKey != null) { if (caseInsensitiveKey != null) {
return super.get(caseInsensitiveKey); return this.targetMap.get(caseInsensitiveKey);
} }
} }
return defaultValue; return defaultValue;
@ -139,7 +190,7 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
if (key instanceof String) { if (key instanceof String) {
String caseInsensitiveKey = this.caseInsensitiveKeys.remove(convertKey((String) key)); String caseInsensitiveKey = this.caseInsensitiveKeys.remove(convertKey((String) key));
if (caseInsensitiveKey != null) { if (caseInsensitiveKey != null) {
return super.remove(caseInsensitiveKey); return this.targetMap.remove(caseInsensitiveKey);
} }
} }
return null; return null;
@ -148,15 +199,28 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
@Override @Override
public void clear() { public void clear() {
this.caseInsensitiveKeys.clear(); this.caseInsensitiveKeys.clear();
super.clear(); this.targetMap.clear();
}
@Override
public LinkedCaseInsensitiveMap<V> clone() {
return new LinkedCaseInsensitiveMap<>(this);
} }
@Override @Override
@SuppressWarnings("unchecked") public boolean equals(Object obj) {
public Object clone() { return this.targetMap.equals(obj);
LinkedCaseInsensitiveMap<V> copy = (LinkedCaseInsensitiveMap<V>) super.clone(); }
copy.caseInsensitiveKeys = new HashMap<>(this.caseInsensitiveKeys);
return copy; @Override
public int hashCode() {
return this.targetMap.hashCode();
}
@Override
public String toString() {
return this.targetMap.toString();
} }
@ -172,4 +236,14 @@ public class LinkedCaseInsensitiveMap<V> extends LinkedHashMap<String, V> {
return key.toLowerCase(this.locale); return key.toLowerCase(this.locale);
} }
/**
* Determine whether this map should remove the given eldest entry.
* @param eldest the candidate entry
* @return {@code true} for removing it, {@code false} for keeping it
* @see LinkedHashMap#removeEldestEntry
*/
protected boolean removeEldestEntry(Map.Entry<String, V> eldest) {
return false;
}
} }

View File

@ -35,7 +35,7 @@ import java.util.Set;
* @author Juergen Hoeller * @author Juergen Hoeller
* @since 3.0 * @since 3.0
*/ */
public class LinkedMultiValueMap<K, V> implements MultiValueMap<K, V>, Serializable { public class LinkedMultiValueMap<K, V> implements MultiValueMap<K, V>, Serializable, Cloneable {
private static final long serialVersionUID = 3801124242820219131L; private static final long serialVersionUID = 3801124242820219131L;
@ -176,18 +176,6 @@ public class LinkedMultiValueMap<K, V> implements MultiValueMap<K, V>, Serializa
} }
/**
* Create a regular copy of this Map.
* @return a shallow copy of this Map, reusing this Map's value-holding List entries
* @since 4.2
* @see LinkedMultiValueMap#LinkedMultiValueMap(Map)
* @see #deepCopy()
*/
@Override
public LinkedMultiValueMap<K, V> clone() {
return new LinkedMultiValueMap<>(this);
}
/** /**
* Create a deep copy of this Map. * Create a deep copy of this Map.
* @return a copy of this Map, including a copy of each value-holding List entry * @return a copy of this Map, including a copy of each value-holding List entry
@ -202,6 +190,17 @@ public class LinkedMultiValueMap<K, V> implements MultiValueMap<K, V>, Serializa
return copy; return copy;
} }
/**
* Create a regular copy of this Map.
* @return a shallow copy of this Map, reusing this Map's value-holding List entries
* @since 4.2
* @see LinkedMultiValueMap#LinkedMultiValueMap(Map)
* @see #deepCopy()
*/
@Override
public LinkedMultiValueMap<K, V> clone() {
return new LinkedMultiValueMap<>(this);
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {

View File

@ -74,11 +74,28 @@ public class LinkedCaseInsensitiveMapTests {
assertEquals("N", map.getOrDefault(new Object(), "N")); assertEquals("N", map.getOrDefault(new Object(), "N"));
} }
@Test
public void computeIfAbsentWithExistingValue() {
map.put("key", "value1");
map.put("KEY", "value2");
map.put("Key", "value3");
assertEquals("value3", map.computeIfAbsent("key", key -> "value1"));
assertEquals("value3", map.computeIfAbsent("KEY", key -> "value2"));
assertEquals("value3", map.computeIfAbsent("Key", key -> "value3"));
}
@Test
public void computeIfAbsentWithComputedValue() {
assertEquals("value1", map.computeIfAbsent("key", key -> "value1"));
assertEquals("value1", map.computeIfAbsent("KEY", key -> "value2"));
assertEquals("value1", map.computeIfAbsent("Key", key -> "value3"));
}
@Test @Test
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void mapClone() { public void mapClone() {
map.put("key", "value1"); map.put("key", "value1");
LinkedCaseInsensitiveMap<String> copy = (LinkedCaseInsensitiveMap<String>) map.clone(); LinkedCaseInsensitiveMap<String> copy = map.clone();
assertEquals("value1", map.get("key")); assertEquals("value1", map.get("key"));
assertEquals("value1", map.get("KEY")); assertEquals("value1", map.get("KEY"));
assertEquals("value1", map.get("Key")); assertEquals("value1", map.get("Key"));