This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit dd3ddafcb2200935158aa3d217b704df93d32ace Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Mon Aug 28 16:06:11 2023 +0200 Use deterministic dimension order for user-specified units. This commit fixes a random test failure where the order depended on which tests were executed first, because the dimension order of the first cached unit was the order used for other compatible units. --- .../main/org/apache/sis/measure/UnitDimension.java | 73 +++++++++-- .../main/org/apache/sis/measure/UnitRegistry.java | 55 +++++++- .../org/apache/sis/util/collection/WeakEntry.java | 14 +- .../sis/util/collection/WeakValueHashMap.java | 145 ++++++++++++--------- .../org/apache/sis/measure/UnitFormatTest.java | 4 +- 5 files changed, 215 insertions(+), 76 deletions(-) diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitDimension.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitDimension.java index 49e03937a9..93e339f1f5 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitDimension.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitDimension.java @@ -242,7 +242,7 @@ final class UnitDimension implements Dimension, Serializable { * is safe if we use the `components` map as a read-only map (no put operation allowed). */ @SuppressWarnings("unchecked") - Map<Dimension,Integer> components = (Map<Dimension,Integer>) dimension.getBaseDimensions(); + final var components = (Map<Dimension,Integer>) dimension.getBaseDimensions(); if (components == null) { return Map.of(dimension, new Fraction(1,1)); } @@ -273,6 +273,7 @@ final class UnitDimension implements Dimension, Serializable { /** * Returns the product or the quotient of this dimension with the specified one. + * This method may return a cached instance. * * @param other the dimension by which to multiply or divide this dimension. * @param divide {@code false} for a multiplication, {@code true} for a division. @@ -350,17 +351,49 @@ final class UnitDimension implements Dimension, Serializable { } if (other instanceof UnitDimension) { final UnitDimension that = (UnitDimension) other; - if (symbol == that.symbol) { - /* - * Do not compare `components` if `symbols` is non-zero because in such case - * the components map contains `this`, which would cause an infinite loop. - */ - return (symbol != 0) || components.equals(that.components); + if ((symbol | that.symbol) != 0) { + return symbol == that.symbol; } + /* + * Do not compare `components` if `symbols` is non-zero because in such case + * the components map contains `this`, which would cause an infinite loop. + */ + return components.equals(that.components); } return false; } + /** + * Compares this dimension with the given object for equality, taking element order in account. + * + * @param that the other object to compare with. + * @return whether the two objects are equal with elements in the same order. + */ + final boolean equalsOrdered(final UnitDimension that) { + if ((symbol | that.symbol) != 0) { + return symbol == that.symbol; + } + return equalsOrdered(components, that.components); + } + + /** + * Compares the given map in a way that take order in account. + * + * @param m1 the first map to compare. + * @param m2 the second map to compare. + * @return whether the two maps contain the same element in the same order. + */ + static boolean equalsOrdered(final Map<?,?> m1, final Map<?,?> m2) { + final var i1 = m1.entrySet().iterator(); + final var i2 = m2.entrySet().iterator(); + while (i1.hasNext()) { + if (!(i2.hasNext() && i1.next().equals(i2.next()))) { + return false; + } + } + return !i2.hasNext(); + } + /** * Returns a hash code value for this dimension. */ @@ -370,7 +403,31 @@ final class UnitDimension implements Dimension, Serializable { * Do not use `components` in hash code calculation if `symbols` is non-zero * beause in such case the map contains `this`, which would cause an infinite loop. */ - return (symbol != 0) ? symbol ^ (int) serialVersionUID : components.hashCode(); + return (symbol != 0 ? symbol : components.hashCode()) ^ (int) serialVersionUID; + } + + /** + * {@return returns a hash code value which takes element order in account}. + * If the map is empty or contains only one element, then the returned value + * is the same as {@link #hashCode()}. + */ + final int hashCodeOrdered() { + int code = symbol; + if (code == 0) code = hashCodeOrdered(components); + return code ^ (int) serialVersionUID; + } + + /** + * {@return an hash code value of the given map computed in an order-sensitive was}. + * + * @param components the map for which to compute hash code. + */ + static int hashCodeOrdered(final Map<?,?> components) { + int code = 0; + for (final Map.Entry<?,?> entry : components.entrySet()) { + code = code * 31 + entry.hashCode(); + } + return code; } /** diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitRegistry.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitRegistry.java index f410a6ba00..0d1dc55919 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitRegistry.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/measure/UnitRegistry.java @@ -98,6 +98,11 @@ final class UnitRegistry implements SystemOfUnits, Serializable { * <tr><td>{@link String}</td> <td>{@link AbstractUnit}</td> <td>Key is the unit symbol.</td></tr> * <tr><td>{@link Short}</td> <td>{@link AbstractUnit}</td> <td>Key is the EPSG code.</td></tr> * </table> + * + * <h4>Dimension order</h4> + * The search for an existing unit in this map is <strong>not</strong> sensitive to dimension order. + * N⋅m is considered equivalent to m⋅N, and both of them are associated to the symbol "J" (Joule). + * We ignore dimension order because it has no incidence on the unit symbol shown to user. */ private static final Map<Object,Object> HARD_CODED = new HashMap<>(256); @@ -106,12 +111,56 @@ final class UnitRegistry implements SystemOfUnits, Serializable { * Values are stored by weak references and garbage collected when no longer used. * Key and value types are the same than the one described in {@link #HARD_CODED}. * + * <h4>Dimension order</h4> + * Contrarily to {@link #HARD_CODED}, the user-specified map of units is sensitive to dimension order. + * kg∕(m⋅s³) is not considered the same as kg∕(s³⋅m) for formatting purpose (but still considered the + * same for unit conversions purpose). This distinction is applied because the unit may have no label + * associated to it. The only label may be the list of dimensions, so we try to show them in the same + * order as specified by the users when they constructed their units. + * * <h4>Implementation note</h4> * We separate hard-coded values from user-defined values because the amount of hard-coded values is relatively - * large, using weak references for them is useless, and most applications will not define any custom values. - * This map will typically stay empty. + * large, using weak references for them is useless, and most applications will not define any custom values so + * the user-defined map will typically stay empty. This separation avoids synchronization of hard-coded values. + * Furthermore the two maps have a different policy on whether to consider dimension order as significant. + */ + private static final WeakValueHashMap<Object,Object> USER_DEFINED = new WeakValueHashMap<>(Object.class, + UnitRegistry::hashCodeOrdered, + UnitRegistry::equalsOrdered); + + /** + * Returns a hash code value for the specified key, taking dimension order in account. + * This is used for the policy of keys in the {@link #USER_DEFINED} map. + * + * @param key a key of one of the types defined in {@link #HARD_CODED}. + * @return hash code value for the given key. Never null. */ - private static final WeakValueHashMap<Object,Object> USER_DEFINED = new WeakValueHashMap<>(Object.class); + private static int hashCodeOrdered(final Object key) { + if (key instanceof UnitDimension) return ((UnitDimension) key).hashCodeOrdered(); + if (key instanceof Map<?,?>) return UnitDimension.hashCodeOrdered((Map<?,?>) key); + return key.hashCode(); + } + + /** + * Compares the given keys, taking dimension order in account. + * Shall be consistent with {@link #hashCode(Object)}. + * + * @param key a key of one of the types defined in {@link #HARD_CODED}. + * @param object an object to compare with the key. Never null. + * @return whether the given object are equal. + */ + private static boolean equalsOrdered(final Object key, final Object other) { + if (key instanceof UnitDimension) { + if (other instanceof UnitDimension) { + return ((UnitDimension) key).equalsOrdered((UnitDimension) other); + } + } else if (key instanceof Map<?,?>) { + if (other instanceof Map<?,?>) { + return UnitDimension.equalsOrdered((Map<?,?>) key, (Map<?,?>) other); + } + } + return key.equals(other); + } /** * Adds the given {@code components}, {@code dim} pair in the map of hard-coded values. diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakEntry.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakEntry.java index a58665f063..fde9942372 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakEntry.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakEntry.java @@ -34,7 +34,7 @@ import org.apache.sis.math.MathFunctions; * from the enclosing collection. * * @author Martin Desruisseaux (MPO, IRD, Geomatys) - * @version 0.3 + * @version 1.4 * * @param <E> the type of elements in the collection. * @@ -204,4 +204,16 @@ abstract class WeakEntry<E> extends WeakReference<E> implements Disposable { static int upperCapacityThreshold(final int capacity) { return capacity - (capacity >>> 2); } + + /** + * Compares the given objects using the {@code ==} operator. + * This is a convenience method for use as lambda function. + * + * @param o1 the first object to compare. + * @param o2 the second object to compare. + * @return whether the two objects are the same instance. + */ + static boolean identityEqual(final Object o1, final Object o2) { + return o1 == o2; + } } diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakValueHashMap.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakValueHashMap.java index 463b93639b..5ffff63ca2 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakValueHashMap.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakValueHashMap.java @@ -24,7 +24,9 @@ import java.util.Iterator; import java.util.Objects; import java.util.Arrays; import java.util.function.Function; +import java.util.function.ToIntFunction; import java.lang.ref.WeakReference; +import java.util.function.BiPredicate; import org.apache.sis.util.Debug; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.Utilities; @@ -86,21 +88,25 @@ import static org.apache.sis.util.collection.WeakEntry.*; */ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { /** - * Comparison mode for key objects. The standard mode is {@code EQUALS}, which means that keys are compared - * using their {@link Object#equals(Object)} method. But {@code WeakValueHashMap} will automatically select - * {@code DEEP_EQUALS} if there is a chance that some keys are arrays. In the latter case, comparisons will - * be done by the more costly {@link Objects#deepEquals(Object, Object)} method instead. + * The function to invoke for computing hash codes. + * This is usually one of the following: * - * <p>The {@code IDENTITY} mode is rarely used, and is selected only if the user explicitly asks for this mode - * at construction time. This mode is provided because reference-equality semantic is sometimes required, and - * hard to simulate if not supported natively by the hash map. See {@link java.util.IdentityHashMap} javadoc - * for some examples of cases where reference-equality semantic is useful.</p> + * <ul> + * <li>{@link Objects#hashCode(Object)} for the {@link java.util.HashMap} behavior.</li> + * <li>{@link System#identityHashCode(Object)} for the {@link java.util.IdentityHashMap} behavior.</li> + * <li>{@link Utilities#deepHashCode(Object)} for a map working with arrays.</li> + * </ul> * - * @see #comparisonMode - * @see #keyEquals(Object, Object) - * @see #keyHashCode(Object) + * The argument given in calls to {@code applyAsInt(Object)} will never be null. */ - private static final byte IDENTITY = 0, EQUALS = 1, DEEP_EQUALS = 2; + private final ToIntFunction<Object> hashFunction; + + /** + * The function to invoke for comparing two objects. + * Shall be consistent with {@link #hashFunction}. + * The arguments given in calls to {@code test(Object, Object)} will never be null. + */ + private final BiPredicate<Object,Object> comparator; /** * An entry in the {@link WeakValueHashMap}. This is a weak reference @@ -108,7 +114,7 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { */ private final class Entry extends WeakEntry<V> implements Map.Entry<K,V> { /** - * The key. + * The key. Shall never be null. */ final K key; @@ -168,7 +174,7 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { public boolean equals(final Object other) { if (other instanceof Map.Entry<?,?>) { final Map.Entry<?,?> that = (Map.Entry<?,?>) other; - return keyEquals(key, that.getKey()) && Objects.equals(get(), that.getValue()); + return comparator.test(key, that.getKey()) && Objects.equals(get(), that.getValue()); } return false; } @@ -179,7 +185,7 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { */ @Override public int hashCode() { - int code = keyHashCode(key); + int code = hashFunction.applyAsInt(key); final V val = get(); if (val != null) { code ^= val.hashCode(); @@ -204,17 +210,6 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { */ private final Class<K> keyType; - /** - * Whether keys shall be compared by reference-equality ({@link #IDENTITY}), by shallow object-equality - * ({@link #EQUALS}) or by deep object-equality ({@link #DEEP_EQUALS}). The {@code DEEP_EQUALS} mode is - * selected only if the keys in this map may be arrays. If the keys cannot be arrays, then we select the - * {@code EQUALS} mode for avoiding calls to the costly {@link Objects#deepEquals(Object, Object)} method. - * - * @see #keyEquals(Object, Object) - * @see #keyHashCode(Object) - */ - private final byte comparisonMode; - /** * The set of entries, created only when first needed. */ @@ -253,9 +248,64 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { */ @SuppressWarnings({"rawtypes", "unchecked"}) // Generic array creation. public WeakValueHashMap(final Class<K> keyType, final boolean identity) { - this.keyType = keyType; - comparisonMode = identity ? IDENTITY : - (keyType.isArray() || keyType.equals(Object.class)) ? DEEP_EQUALS : EQUALS; + this.keyType = Objects.requireNonNull(keyType); + if (identity) { + hashFunction = System::identityHashCode; + comparator = WeakEntry::identityEqual; + } else if (keyType.isArray()) { + hashFunction = Utilities::deepHashCode; + comparator = Objects::deepEquals; + } else { + hashFunction = Object::hashCode; + comparator = Object::equals; + } + lastTimeNormalCapacity = System.nanoTime(); + table = new WeakValueHashMap.Entry[MIN_CAPACITY]; + } + + /** + * Creates a new {@code WeakValueHashMap} using the given functions for computing hash code and equality. + * Commonly used functions are: + * + * <table class="sis"> + * <caption>Frequently used hash and comparison functions</caption> + * <tr> + * <th>Desired behavior</th> + * <th>Hash function</th> + * <th>Comparator</th> + * </tr><tr> + * <td>Standard (like {@link java.util.HashMap})</td> + * <td>{@link Object#hashCode()}</td> + * <td>{@link Object#equals(Object)}</td> + * </tr><tr> + * <td>Robust to arrays</td> + * <td>{@link Utilities#deepHashCode(Object)}</td> + * <td>{@link Objects#deepEquals(Object, Object)}</td> + * </tr><tr> + * <td>Identity (like {@link java.util.IdentityHashMap})</td> + * <td>{@link System#identityHashCode()}</td> + * <td>{@code (o1,o2) -> o1 == o2}</td> + * </tr> + * </table> + * + * The functions do not need to be null-safe. {@code WeakValueHashMap} will never invoke + * {@link ToIntFunction#applyAsInt(Object)} or {@link BiPredicate#test(Object, Object)} + * with null arguments. + * + * @param keyType the type of keys in the map. + * @param hashFunction the function to invoke for computing hash codes. + * @param comparator the function to invoke for comparing two objects. + * + * @since 1.4 + */ + @SuppressWarnings({"rawtypes", "unchecked"}) // Generic array creation. + public WeakValueHashMap(final Class<K> keyType, + final ToIntFunction<Object> hashFunction, + final BiPredicate<Object,Object> comparator) + { + this.keyType = Objects.requireNonNull(keyType); + this.hashFunction = Objects.requireNonNull(hashFunction); + this.comparator = Objects.requireNonNull(comparator); lastTimeNormalCapacity = System.nanoTime(); table = new WeakValueHashMap.Entry[MIN_CAPACITY]; } @@ -312,35 +362,6 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { return count; } - /** - * Returns the hash code value for the given key. - * - * @param key the key (cannot be null). - */ - private int keyHashCode(final Object key) { - switch (comparisonMode) { - case IDENTITY: return System.identityHashCode(key); - case EQUALS: return key.hashCode(); - case DEEP_EQUALS: return Utilities.deepHashCode(key); - default: throw new AssertionError(comparisonMode); - } - } - - /** - * Returns {@code true} if the two given keys are equal. - * - * @param k1 the first key (cannot be null). - * @paral k2 the second key. - */ - private boolean keyEquals(final Object k1, final Object k2) { - switch (comparisonMode) { - case IDENTITY: return k1 == k2; - case EQUALS: return k1.equals(k2); - case DEEP_EQUALS: return Objects.deepEquals(k1, k2); - default: throw new AssertionError(comparisonMode); - } - } - /** * Locates the entry for the given key and, if present, invokes the given getter method. * @@ -355,9 +376,9 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { assert isValid(); if (key != null) { final Entry[] table = this.table; - final int index = (keyHashCode(key) & HASH_MASK) % table.length; + final int index = (hashFunction.applyAsInt(key) & HASH_MASK) % table.length; for (Entry e = table[index]; e != null; e = (Entry) e.next) { - if (keyEquals(key, e.key)) { + if (comparator.test(key, e.key)) { return getter.apply(e); } } @@ -478,10 +499,10 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { */ V oldValue = null; Entry[] table = this.table; - final int hash = keyHashCode(key) & HASH_MASK; + final int hash = hashFunction.applyAsInt(key) & HASH_MASK; int index = hash % table.length; for (Entry e = table[index]; e != null; e = (Entry) e.next) { - if (keyEquals(key, e.key)) { + if (comparator.test(key, e.key)) { oldValue = e.get(); if (condition != null && !condition.equals(oldValue)) { return oldValue; diff --git a/endorsed/src/org.apache.sis.util/test/org/apache/sis/measure/UnitFormatTest.java b/endorsed/src/org.apache.sis.util/test/org/apache/sis/measure/UnitFormatTest.java index db7fa86b24..c2a42f2552 100644 --- a/endorsed/src/org.apache.sis.util/test/org/apache/sis/measure/UnitFormatTest.java +++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/measure/UnitFormatTest.java @@ -720,7 +720,7 @@ public final class UnitFormatTest extends TestCase { roundtrip(f, "(m2.s.sr)-1", "1∕(m²⋅s)"); // Too aggressive simplification bug (SIS-378) roundtrip(f, "(kg.m-3).(m.s-1)", "kg∕(m²⋅s)"); // Too aggressive simplification bug (SIS-378) roundtrip(f, "cm/day", "cm∕d"); - roundtrip(f, "W.m-2.nm-1", "10⁹⋅kg∕(m⋅s³)"); // Too aggressive simplification bug (SIS-378) + roundtrip(f, "W.m-2.nm-1", "10⁹⋅kg∕(s³⋅m)"); // Too aggressive simplification bug (SIS-378) } /** @@ -763,7 +763,7 @@ public final class UnitFormatTest extends TestCase { roundtrip(f, "W.m-3.sr-1", "W∕m³"); roundtrip(f, "m3.s-1.m-1", "m²∕s"); roundtrip(f, "(kg.m-3)*(m.s-1)", "kg∕(m²⋅s)"); - roundtrip(f, "W.m-2.nm-1", "10⁹⋅kg∕(m⋅s³)"); + roundtrip(f, "W.m-2.nm-1", "10⁹⋅kg∕(s³⋅m)"); } /**