Skip to content

Navigation Menu

Sign in
Sign up

Feat: add ImmutableMap.sortedCopyOf #8591

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
DerrickUnleashed wants to merge 5 commits into google:master
base: master
Choose a base branch
Loading
from DerrickUnleashed:immutable-map-sortedcopyof
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,19 @@

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.Serializable;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collector;
Expand Down Expand Up @@ -406,6 +410,54 @@ public static <K, V> ImmutableMap<K, V> copyOf(
}
}

public static <K extends Comparable<? super K>, V> ImmutableMap<K, V> sortedCopyOf(
Map<? extends K, ? extends V> map) {
return sortedCopyOf(Ordering.natural(), map);
}

public static <K, V> ImmutableMap<K, V> sortedCopyOf(
Comparator<? super K> keyComparator, Map<? extends K, ? extends V> map) {
checkNotNull(keyComparator);
@SuppressWarnings({"unchecked", "rawtypes"}) // entries are non-null
Entry<K, V>[] entryArray = map.entrySet().toArray((Entry<K, V>[]) new Entry<?, ?>[map.size()]);
switch (entryArray.length) {
case 0:
return of();
case 1:
Entry<K, V> onlyEntry = entryArray[0];
return of(onlyEntry.getKey(), onlyEntry.getValue());
default:
@SuppressWarnings("nullness") // entries 0..entryArray.length-1 are non-null
Entry<K, V>[] nonNullEntries = entryArray;
Entry<K, V>[] lastEntryForEachKey = lastEntryForEachKey(nonNullEntries, entryArray.length);
Entry<K, V>[] toSort = (lastEntryForEachKey == null) ? nonNullEntries : lastEntryForEachKey;
Arrays.sort(toSort, Ordering.from(keyComparator).onResultOf(Entry::getKey));
return new RegularImmutableMap<K, V>(/* throwIfDuplicateKeys= */ false, toSort);
}
}

private static <K, V> Entry<K, V> @Nullable [] lastEntryForEachKey(
Entry<K, V>[] entries, int size) {
Set<K> seen = new HashSet<>();
BitSet dups = new BitSet(); // slots that are overridden by a later duplicate key
for (int i = size - 1; i >= 0; i--) {
if (!seen.add(entries[i].getKey())) {
dups.set(i);
}
}
if (dups.isEmpty()) {
return null;
}
@SuppressWarnings({"rawtypes", "unchecked"})
Entry<K, V>[] newEntries = new Entry[size - dups.cardinality()];
for (int inI = 0, outI = 0; inI < size; inI++) {
if (!dups.get(inI)) {
newEntries[outI++] = entries[inI];
}
}
return newEntries;
}

abstract boolean isPartialView();

@Override
Expand Down
109 changes: 109 additions & 0 deletions guava-tests/test/com/google/common/collect/ImmutableMapTest.java
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -784,6 +786,83 @@ public void testCopyOf() {
assertThat(ImmutableMap.copyOf(copy)).isSameInstanceAs(copy);
}

public void testSortedCopyOf_natural() {
Map<String, Integer> map = new LinkedHashMap<>();
map.put("banana", 3);
map.put("apple", 1);
map.put("cherry", 4);
map.put("avocado", 2);
ImmutableMap<String, Integer> copy = ImmutableMap.sortedCopyOf(map);
assertMapEquals(copy, "apple", 1, "avocado", 2, "banana", 3, "cherry", 4);
}

public void testSortedCopyOf_natural_empty() {
ImmutableMap<String, Integer> copy =
ImmutableMap.sortedCopyOf(Collections.<String, Integer>emptyMap());
assertThat(copy).isEmpty();
}

public void testSortedCopyOf_natural_singleton() {
ImmutableMap<String, Integer> copy = ImmutableMap.sortedCopyOf(singletonMap("one", 1));
assertMapEquals(copy, "one", 1);
}

public void testSortedCopyOf_natural_containsNullValue() {
Map<String, @Nullable Integer> map = new LinkedHashMap<>();
map.put("one", 1);
map.put("two", null);
assertThrows(NullPointerException.class, () -> ImmutableMap.sortedCopyOf(map));
}

public void testSortedCopyOf_natural_singleton_containsNullValue() {
Map<String, @Nullable Integer> map = new LinkedHashMap<>();
map.put("one", null);
assertThrows(NullPointerException.class, () -> ImmutableMap.sortedCopyOf(map));
}

public void testSortedCopyOf_comparator() {
Map<String, Integer> map = new LinkedHashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("A", 3);
map.put("c", 4);
ImmutableMap<String, Integer> copy =
ImmutableMap.sortedCopyOf(String.CASE_INSENSITIVE_ORDER, map);
assertMapEquals(copy, "a", 1, "A", 3, "b", 2, "c", 4);
}

public void testSortedCopyOf_comparator_empty() {
ImmutableMap<String, Integer> copy =
ImmutableMap.sortedCopyOf(
String.CASE_INSENSITIVE_ORDER, Collections.<String, Integer>emptyMap());
assertThat(copy).isEmpty();
}

public void testSortedCopyOf_comparator_singleton() {
ImmutableMap<String, Integer> copy =
ImmutableMap.sortedCopyOf(String.CASE_INSENSITIVE_ORDER, singletonMap("a", 1));
assertMapEquals(copy, "a", 1);
}

public void testSortedCopyOf_comparator_containsNullValue() {
Map<String, @Nullable Integer> map = new LinkedHashMap<>();
map.put("one", 1);
map.put("two", null);
assertThrows(
NullPointerException.class,
() -> ImmutableMap.sortedCopyOf(String.CASE_INSENSITIVE_ORDER, map));
}

public void testSortedCopyOf_duplicateKeys_keepLast() {
assertMapEquals(ImmutableMap.sortedCopyOf(mapWithDuplicateKeys()), "a", 4, "b", 3);
}

public void testSortedCopyOf_comparator_duplicateKeys_keepLast() {
assertMapEquals(
ImmutableMap.sortedCopyOf(String.CASE_INSENSITIVE_ORDER, mapWithDuplicateKeys()),
"a", 4, "b", 3);
}

public void testToImmutableMap() {
Collector<Entry<String, Integer>, ?, ImmutableMap<String, Integer>> collector =
toImmutableMap(Entry::getKey, Entry::getValue);
Expand Down Expand Up @@ -859,6 +938,36 @@ private static <K, V> void assertMapEquals(Map<K, V> map, Object... alternatingK
assertThat(map).containsExactlyEntriesIn(expected).inOrder();
}

/**
* Returns a map whose {@code entrySet()} returns entries that contain duplicate keys. This is not
* a well-behaved map, but it lets us test {@link ImmutableMap#sortedCopyOf}'s handling of
* duplicate keys.
*/
private static Map<String, Integer> mapWithDuplicateKeys() {
List<Entry<String, Integer>> entries =
asList(
immutableEntry("b", 1),
immutableEntry("a", 2),
immutableEntry("b", 3),
immutableEntry("a", 4));
return new AbstractMap<String, Integer>() {
@Override
public Set<Entry<String, Integer>> entrySet() {
return new AbstractSet<Entry<String, Integer>>() {
@Override
public Iterator<Entry<String, Integer>> iterator() {
return entries.iterator();
}

@Override
public int size() {
return entries.size();
}
};
}
};
}

private static class IntHolder implements Serializable {
private int value;

Expand Down
132 changes: 105 additions & 27 deletions guava/src/com/google/common/collect/ImmutableMap.java
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -645,33 +645,6 @@ ImmutableMap<K, V> buildJdkBacked() {
}
}

/**
* Scans the first {@code size} elements of {@code entries} looking for duplicate keys. If
* duplicates are found, a new correctly-sized array is returned with the same elements (up to
* {@code size}), except containing only the last occurrence of each duplicate key. Otherwise
* {@code null} is returned.
*/
private static <K, V> Entry<K, V> @Nullable [] lastEntryForEachKey(
Entry<K, V>[] entries, int size) {
Set<K> seen = new HashSet<>();
BitSet dups = new BitSet(); // slots that are overridden by a later duplicate key
for (int i = size - 1; i >= 0; i--) {
if (!seen.add(entries[i].getKey())) {
dups.set(i);
}
}
if (dups.isEmpty()) {
return null;
}
@SuppressWarnings({"rawtypes", "unchecked"})
Entry<K, V>[] newEntries = new Entry[size - dups.cardinality()];
for (int inI = 0, outI = 0; inI < size; inI++) {
if (!dups.get(inI)) {
newEntries[outI++] = entries[inI];
}
}
return newEntries;
}
}

/**
Expand Down Expand Up @@ -742,6 +715,111 @@ public static <K, V> ImmutableMap<K, V> copyOf(
}
}

/**
* Returns an immutable map containing the same entries as {@code map}, sorted according to the
* natural ordering of the keys. The sorting algorithm used is stable, so entries whose keys
* compare as equal will stay in the order in which they appear in the input. In addition, if two
* or more entries have keys that are equal, the value associated with the <i>last</i> such entry
* is kept.
*
* <p>If you want a map whose entries are always sorted, even under further iteration, use {@link
* ImmutableSortedMap} instead: In exchange for {@code O(log N)} {@link Map#get} lookups (instead
* of the {@code O(1)} lookups of the map returned by this method), {@code ImmutableSortedMap}
* guarantees that its {@code entrySet()}, {@code keySet()}, and {@code values()} views iterate in
* sorted order forever.
*
* <p><b>Java 8+ users:</b> If you want to convert a {@link java.util.stream.Stream} of {@link
* Entry} instances to a sorted {@code ImmutableMap}, use {@code
* stream.sorted(Map.Entry.comparingByKey()).collect(toImmutableMap(Entry::getKey,
* Entry::getValue))}. (Unlike this method, that {@code Collector} does not tolerate duplicate
* keys.)
*
* @throws NullPointerException if any key or value in {@code map} is null
* @since 33.7.0
*/
public static <K extends Comparable<? super K>, V> ImmutableMap<K, V> sortedCopyOf(
Map<? extends K, ? extends V> map) {
return sortedCopyOf(Ordering.natural(), map);
}

/**
* Returns an immutable map containing the same entries as {@code map}, sorted according to the
* specified {@code Comparator} applied to the keys. The sorting algorithm used is stable, so
* entries whose keys compare as equal will stay in the order in which they appear in the input.
* In addition, if two or more entries have keys that are equal, the value associated with the
* <i>last</i> such entry is kept.
*
* <p>If you want a map whose entries are always sorted, even under further iteration, use {@link
* ImmutableSortedMap} instead: In exchange for {@code O(log N)} {@link Map#get} lookups (instead
* of the {@code O(1)} lookups of the map returned by this method), {@code ImmutableSortedMap}
* guarantees that its {@code entrySet()}, {@code keySet()}, and {@code values()} views iterate in
* sorted order forever.
*
* <p><b>Java 8+ users:</b> If you want to convert a {@link java.util.stream.Stream} of {@link
* Entry} instances to a sorted {@code ImmutableMap}, use {@code
* stream.sorted(comparator).collect(toImmutableMap(Entry::getKey, Entry::getValue))}. (Unlike
* this method, that {@code Collector} does not tolerate duplicate keys.)
*
* @throws NullPointerException if {@code keyComparator} is null, or if any key or value in {@code
* map} is null
* @since 33.7.0
*/
public static <K, V> ImmutableMap<K, V> sortedCopyOf(
Comparator<? super K> keyComparator, Map<? extends K, ? extends V> map) {
checkNotNull(keyComparator);
@SuppressWarnings("unchecked") // we'll only be using getKey and getValue, which are covariant
Entry<K, V>[] entryArray = (Entry<K, V>[]) map.entrySet().toArray(EMPTY_ENTRY_ARRAY);
switch (entryArray.length) {
case 0:
return of();
case 1:
// requireNonNull is safe because the first `size` elements have been filled in.
Entry<K, V> onlyEntry = requireNonNull(entryArray[0]);
return of(onlyEntry.getKey(), onlyEntry.getValue());
default:
/*
* We want to retain only the last value for any given key, before sorting. This gives us
* well-defined behavior for duplicate keys, even when the keyComparator is not consistent
* with equals.
*/
@SuppressWarnings("nullness") // entries 0..entryArray.length-1 are non-null
Entry<K, V>[] nonNullEntries = entryArray;
Entry<K, V>[] lastEntryForEachKey = lastEntryForEachKey(nonNullEntries, entryArray.length);
Entry<K, V>[] toSort = (lastEntryForEachKey == null) ? nonNullEntries : lastEntryForEachKey;
int size = toSort.length;
sort(toSort, 0, size, Ordering.from(keyComparator).onResultOf(Entry::getKey));
return RegularImmutableMap.fromEntryArray(size, toSort, /* throwIfDuplicateKeys= */ false);
}
}

/**
* Scans the first {@code size} elements of {@code entries} looking for duplicate keys. If
* duplicates are found, a new correctly-sized array is returned with the same elements (up to
* {@code size}), except containing only the last occurrence of each duplicate key. Otherwise
* {@code null} is returned.
*/
private static <K, V> Entry<K, V> @Nullable [] lastEntryForEachKey(
Entry<K, V>[] entries, int size) {
Set<K> seen = new HashSet<>();
BitSet dups = new BitSet(); // slots that are overridden by a later duplicate key
for (int i = size - 1; i >= 0; i--) {
if (!seen.add(entries[i].getKey())) {
dups.set(i);
}
}
if (dups.isEmpty()) {
return null;
}
@SuppressWarnings({"rawtypes", "unchecked"})
Entry<K, V>[] newEntries = new Entry[size - dups.cardinality()];
for (int inI = 0, outI = 0; inI < size; inI++) {
if (!dups.get(inI)) {
newEntries[outI++] = entries[inI];
}
}
return newEntries;
}

static final Entry<?, ?>[] EMPTY_ENTRY_ARRAY = new Entry<?, ?>[0];

abstract static class IteratorBasedImmutableMap<K, V> extends ImmutableMap<K, V> {
Expand Down

AltStyle によって変換されたページ (->オリジナル) /