You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2013/03/14 16:37:02 UTC

svn commit: r1456484 - in /sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter: ClassPair.java ConverterRegistry.java SystemConverter.java

Author: desruisseaux
Date: Thu Mar 14 15:37:01 2013
New Revision: 1456484

URL: http://svn.apache.org/r1456484
Log:
Completed the port of ConverterRegistry, excepts toString() and tests.

Modified:
    sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/ClassPair.java
    sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/ConverterRegistry.java
    sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/SystemConverter.java

Modified: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/ClassPair.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/ClassPair.java?rev=1456484&r1=1456483&r2=1456484&view=diff
==============================================================================
--- sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/ClassPair.java [UTF-8] (original)
+++ sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/ClassPair.java [UTF-8] Thu Mar 14 15:37:01 2013
@@ -19,13 +19,16 @@ package org.apache.sis.internal.converte
 import java.io.Serializable;
 import net.jcip.annotations.Immutable;
 import org.apache.sis.util.ObjectConverter;
+import org.apache.sis.util.Debug;
 
 
 /**
  * Holds explicit {@link #sourceClass} and {@link #targetClass} values. Used as key in a hash
- * map of converters. Also used as the base class for subclasses working on explicit source and
- * target class. We allows this opportunist leveraging of implementation because those classes
- * are not public (otherwise a separated hierarchy may have been preferable).
+ * map of converters. Also used as the base class for converters defined in this package.
+ *
+ * <p>The only direct subtype allowed is {@link SystemConverter}.
+ * <strong>No other direct subtype shall exist</strong>.
+ * See {@link #equals(Object)} for an explanation.</p>
  *
  * @param <S> The base type of source objects.
  * @param <T> The base type of converted objects.
@@ -45,12 +48,12 @@ class ClassPair<S,T> implements Serializ
     /**
      * The source class.
      */
-    protected final Class<S> sourceClass;
+    final Class<S> sourceClass;
 
     /**
      * The target class.
      */
-    protected final Class<T> targetClass;
+    final Class<T> targetClass;
 
     /**
      * Creates an entry for the given source and target classes.
@@ -80,7 +83,7 @@ class ClassPair<S,T> implements Serializ
      *
      * @return A key for the parent source, or {@code null}.
      */
-    public final ClassPair<? super S, T> parentSource() {
+    final ClassPair<? super S, T> parentSource() {
         final Class<? super S> source;
         if (sourceClass.isInterface()) {
             @SuppressWarnings({"unchecked","rawtypes"})
@@ -118,27 +121,21 @@ class ClassPair<S,T> implements Serializ
     }
 
     /**
-     * Returns {@code true} if the source and target classes of the given converter
-     * are strictly equal to the source and target classes of this {@code ClassPair}.
-     *
-     * @param  The converter to check.
-     * @return {@code true} if the given converter is for the same source and target classes.
-     */
-    final boolean isExactlyFor(final ObjectConverter<? super S, ? extends T> converter) {
-        return converter.getSourceClass() == sourceClass &&
-               converter.getTargetClass() == targetClass;
-    }
-
-    /**
-     * Compares the given object with this entry for equality. Two entries are considered
-     * equals if they have the same source and target classes. This is required for use
-     * as {@link java.util.HashMap} keys in {@link ConverterRegistry}.
-     *
-     * @param  other The object to compare with this entry.
-     * @return {@code true} if the given object is a entry having the same source and target classes.
+     * Compares the given object with this {@code ClassPair} for equality. Two {@code ClassPair}
+     * instances are considered equal if they have the same source and target classes, ignoring
+     * all other properties eventually defined in subclasses.
+     *
+     * <p>This method is designed for use by {@link ConverterRegistry} as {@link java.util.HashMap}
+     * keys. Its primary purpose is <strong>not</strong> to determine if two objects are converters
+     * doing the same conversions. However the {@link SystemConverter} subclass overrides this
+     * method with an additional safety check.</p>
+     *
+     * @param  other The object to compare with this {@code ClassPair}.
+     * @return {@code true} if the given object is a {@code ClassPair}
+     *         having the same source and target classes.
      */
     @Override
-    public final boolean equals(final Object other) {
+    public boolean equals(final Object other) {
         if (other instanceof ClassPair<?,?>) {
             final ClassPair<?,?> that = (ClassPair<?,?>) other;
             return sourceClass == that.sourceClass &&
@@ -148,7 +145,8 @@ class ClassPair<S,T> implements Serializ
     }
 
     /**
-     * Returns a hash code value for this entry.
+     * Returns a hash code value for this {@code ClassPair}.
+     * See {@link #equals(Object)} javadoc for information on the scope of this method.
      */
     @Override
     public final int hashCode() {
@@ -159,6 +157,7 @@ class ClassPair<S,T> implements Serializ
      * Returns a string representation for this entry.
      * Used for formatting error messages.
      */
+    @Debug
     @Override
     public String toString() {
         return sourceClass.getSimpleName() + " ⇨ " + targetClass.getSimpleName();

Modified: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/ConverterRegistry.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/ConverterRegistry.java?rev=1456484&r1=1456483&r2=1456484&view=diff
==============================================================================
--- sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/ConverterRegistry.java [UTF-8] (original)
+++ sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/ConverterRegistry.java [UTF-8] Thu Mar 14 15:37:01 2013
@@ -18,24 +18,25 @@ package org.apache.sis.internal.converte
 
 import java.util.Map;
 import java.util.LinkedHashMap;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
 import net.jcip.annotations.ThreadSafe;
 import org.apache.sis.internal.util.SystemListener;
+import org.apache.sis.util.Classes;
+import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ObjectConverter;
+import org.apache.sis.util.UnconvertibleObjectException;
+import org.apache.sis.util.resources.Errors;
 
 
 /**
  * A collection of {@link ObjectConverter} instances.
  * A converter from the given <var>source type</var> to the given <var>target type</var> can be
- * obtained by a call to {@link #converter(Class, Class)}. If no converter exists for the given
+ * obtained by a call to {@link #find(Class, Class)}. If no converter exists for the given
  * source and target types, then this registry searches for a suitable converter accepting a
  * parent class of the given source type, or returning a sub-class of the given target type.
  *
  * <p>New instances of {@code ConverterRegistry} are initially empty. Custom converters must be
- * explicitly {@linkplain #register registered}. However a system-wide registry initialized
- * with default converters is provided by the {@link #SYSTEM} constant.</p>
+ * explicitly {@linkplain #register(ObjectConverter) registered}. However a system-wide registry
+ * initialized with default converters is provided by the {@link #SYSTEM} constant.</p>
  *
  * {@section Note about conversions from interfaces}
  * {@code ConverterRegistry} is primarily designed for handling converters from classes to
@@ -78,78 +79,373 @@ public class ConverterRegistry {
     }
 
     /**
-     * The map of converters of any kind. We use a single read/write lock for the whole map
-     * because write operations will be rare (so {@code ConcurrentHashMap} may be an overkill).
-     */
-    private final Map<ClassPair<?,?>, SystemConverter<?,?>> converters;
-
-    /**
-     * The locks for the {@link #converters} map.
+     * The map of converters of any kind. For any key of type {@code ClassPair<S,T>},
+     * the value shall be of type {@code ObjectConverter<? super S, ? extends T>}.
+     * To ensure this constraint, values should be read and written using only the
+     * type-safe {@link #get(ClassPair)} and {@link #put(ClassPair, ObjectConverter)}
+     * methods.
+     *
+     * <p>In the special case where the value is actually {@code SystemConverter<S,T>},
+     * then the key and the value may be the same instance (in order to save object
+     * allocations).</p>
+     *
+     * {@section Synchronization note}
+     * Synchronization if performed by {@code synchronized(converters)} statements. We tried
+     * {@code ReadWriteLock}, but this is not very convenient because read operations may be
+     * followed by write operations at any time if the requested converter is not in the cache.
+     * Furthermore profiling has not identified this class as a noticeable contention point.
      */
-    private final ReadWriteLock locks;
+    private final Map<ClassPair<?,?>, ObjectConverter<?,?>> converters;
 
     /**
      * Creates an initially empty set of object converters.
      */
     public ConverterRegistry() {
         converters = new LinkedHashMap<>();
-        locks = new ReentrantReadWriteLock();
     }
 
     /**
      * Removes all converters from this registry.
      */
     public void clear() {
-        final Lock lock = locks.writeLock();
-        lock.lock();
-        try {
+        synchronized (converters) {
             converters.clear();
-        } finally {
-            lock.unlock();
         }
     }
 
     /**
+     * Gets the value from the {@linkplain #converters} map for the given key.
+     */
+    private <S,T> ObjectConverter<? super S, ? extends T> get(final ClassPair<S,T> key) {
+        assert Thread.holdsLock(converters);
+        return key.cast(converters.get(key));
+    }
+
+    /**
+     * Puts the given value in the {@linkplain #converters} map for the given key.
+     */
+    @SuppressWarnings("unchecked")
+    private <S,T> void put(ClassPair<S,T> key, final ObjectConverter<? super S, ? extends T> converter) {
+        assert key.getClass() == ClassPair.class; // See SystemConverter.equals(Object)
+        assert Thread.holdsLock(converters);
+        if (converter instanceof SystemConverter<?,?> &&
+            converter.getSourceClass() == key.sourceClass &&
+            converter.getTargetClass() == key.targetClass)
+        {
+            /*
+             * Opportunistically share the same instance for the keys and the values, in order
+             * to reduce a little bit the amount of objects in the JVM. However we must remove
+             * any old value from the map using the old key, otherwise put operation may fail.
+             * See SystemConverter.equals(Object) for more explanation.
+             */
+            converters.remove(key);
+            key = (SystemConverter<S,T>) converter;
+        }
+        converters.put(key, converter);
+    }
+
+    /**
+     * If {@code existing} or one of its children is equals to the given {@code converter},
+     * returns it. Otherwise returns {@code null}.
+     *
+     * @param  <S> The {@code converter} source class.
+     * @param  <T> The {@code converter} target class.
+     * @param  converter The converter to replace by an existing converter, if possible.
+     * @param  existing Existing converter to test.
+     * @return A converter equals to {@code converter}, or {@code null} if none.
+     */
+    @SuppressWarnings("unchecked")
+    private static <S,T> ObjectConverter<S,T> findEquals(ObjectConverter<S,T> converter,
+            final ObjectConverter<S, ? extends T> existing)
+    {
+        if (converter instanceof FallbackConverter<?,?>) {
+            final FallbackConverter<S,T> fc = (FallbackConverter<S,T>) converter;
+            converter = findEquals(fc, fc.primary);
+            if (converter == null) {
+                converter = findEquals(fc, fc.fallback);
+            }
+        } else if (converter.equals(existing)) {
+            converter = (ObjectConverter<S,T>) existing;
+        } else {
+            converter = null;
+        }
+        return converter;
+    }
+
+    /**
+     * Returns a converter equals to the given {@code converter}, or {@code null} if none.
+     *
+     * @param  <S> The {@code converter} source class.
+     * @param  <T> The {@code converter} target class.
+     * @param  converter The converter to replace by an existing converter, if possible.
+     * @return A converter equals to {@code converter}, or {@code null} if none.
+     */
+    @SuppressWarnings("unchecked")
+    final <S,T> ObjectConverter<S,T> findEquals(final SystemConverter<S,T> converter) {
+        ObjectConverter<? super S, ? extends T> existing;
+        synchronized (converters) {
+            existing = get(converter);
+        }
+        if (existing != null && existing.getSourceClass() == converter.getSourceClass()) {
+            return findEquals(converter, (ObjectConverter<S, ? extends T>) existing);
+        }
+        return null;
+    }
+
+    /**
      * Returns an unique instance of the given converter. If a converter already exists for the
      * same source an target classes, then that converter is returned. Otherwise that converter
-     * is cached if {@code cache} is {@code true} and returned.
+     * is cached and returned.
      *
      * @param  converter The converter to look for a unique instance.
-     * @param  cache Whether to cache the given converter if there is no existing instance.
      * @return A previously existing instance if one exists, or the given converter otherwise.
      */
     @SuppressWarnings("unchecked")
-    final <S,T> ObjectConverter<S,T> unique(final SystemConverter<S,T> converter, final boolean cache) {
-        SystemConverter<S,T> existing;
-        Lock lock = locks.readLock();
-        lock.lock();
-        try {
-            existing = (SystemConverter<S,T>) converters.get(converter);
-        } finally {
-            lock.unlock();
+    final <S,T> ObjectConverter<S,T> unique(final SystemConverter<S,T> converter) {
+        ObjectConverter<S,T> existing = findEquals(converter);
+        if (existing == null) {
+            register(converter);
+            existing = findEquals(converter);
+            if (existing == null) {
+                return converter;
+            }
+        }
+        return existing;
+    }
+
+    /**
+     * Registers a new converter. This method should be invoked only once for a given converter,
+     * for example in class static initializer. For example if a {@code Angle} class is defined,
+     * the static initializer of that class could register a converter from {@code Angle} to
+     * {@code Double}.
+     *
+     * <p>This method registers the converter for the {@linkplain ObjectConverter#getTargetClass()
+     * target class}, some parents of the target class (see below) and every interfaces except
+     * {@link Cloneable} which are implemented by the target class and not by the source class.
+     * For example a converter producing {@link Double} can be used for clients that just ask
+     * for a {@link Number}.</p>
+     *
+     * {@section Which super-classes of the target class are registered}
+     * Consider a converter from class {@code S} to class {@code T} where the two classes
+     * are related in a hierarchy as below:
+     *
+     * {@preformat text
+     *   C1
+     *   └───C2
+     *       ├───C3
+     *       │   └───S
+     *       └───C4
+     *           └───T
+     * }
+     *
+     * Invoking this method will register the given converter for all the following cases:
+     *
+     * <ul>
+     *   <li>{@code S} → {@code T}</li>
+     *   <li>{@code S} → {@code C4}</li>
+     * </ul>
+     *
+     * No {@code S} → {@code C2} or {@code S} → {@code C1} converter will be registered,
+     * because an identity converter would be sufficient for those cases.
+     *
+     * {@section Which sub-classes of the source class are registered}
+     * Sub-classes of the source class will be registered on a case-by-case basis when the
+     * {@link #find(Class, Class)} is invoked, because we can not know the set of all
+     * sub-classes in advance (and would not necessarily want to register all of them anyway).
+     *
+     * @param <S> The class of source value.
+     * @param <T> The class of target (converted) values.
+     * @param converter The converter to register.
+     */
+    public <S,T> void register(final ObjectConverter<S,T> converter) {
+        ArgumentChecks.ensureNonNull("converter", converter);
+        /*
+         * If the given converter is a FallbackConverter (maybe obtained from an other
+         * ConverterRegistry), unwraps it and registers its component individually.
+         */
+        if (converter instanceof FallbackConverter<?,?>) {
+            final FallbackConverter<S,T> fc = (FallbackConverter<S,T>) converter;
+            register(fc.primary);
+            register(fc.fallback);
+            return;
         }
         /*
-         * If no instance existed before for the source and target classes, stores this
-         * instance in the pool. However we will need to check again during the write
-         * operation in case an other thread had the time to add an instance in the pool.
+         * Registers an individual converter.
          */
-        if (existing == null) {
-            if (!cache) {
+        final Class<S> sourceClass = converter.getSourceClass();
+        final Class<T> targetClass = converter.getTargetClass();
+        final Class<?> stopAt = Classes.findCommonClass(sourceClass, targetClass);
+        ArgumentChecks.ensureNonNull("sourceClass", sourceClass);
+        ArgumentChecks.ensureNonNull("targetClass", targetClass);
+        synchronized (converters) {
+            for (Class<? super T> i=targetClass; i!=null && i!=stopAt; i=i.getSuperclass()) {
+                register(new ClassPair<>(sourceClass, i), converter);
+            }
+            /*
+             * At this point, the given class and parent classes
+             * have been registered. Now registers interfaces.
+             */
+            for (final Class<? super T> i : Classes.getAllInterfaces(targetClass)) {
+                if (i.isAssignableFrom(sourceClass)) {
+                    /*
+                     * Target interface is already implemented by the source, so
+                     * there is no reason to convert the source to that interface.
+                     */
+                    continue;
+                }
+                if (Cloneable.class.isAssignableFrom(i)) {
+                    /*
+                     * Exclude this special case. If we were accepting it, we would basically
+                     * provide converters from immutable to mutable objects (e.g. from String
+                     * to Locale), which is not something we would like to encourage. Even if
+                     * the user really wanted a mutable object, in order to modify it he needs
+                     * to known the exact type, so asking for a conversion to Cloneable is too
+                     * vague.
+                     */
+                    continue;
+                }
+                if (sourceClass == Number.class && Comparable.class.isAssignableFrom(i)) {
+                    /*
+                     * Exclude this special case. java.lang.Number does not implement Comparable,
+                     * but its subclasses do. Accepting this case would lead ConverterRegistry to
+                     * offer converters from Number to String, which is not the best move if the
+                     * user want to compare numbers.
+                     */
+                    continue;
+                }
+                if (!i.isAssignableFrom(sourceClass)) {
+                    register(new ClassPair<>(sourceClass, i), converter);
+                }
+            }
+        }
+    }
+
+    /**
+     * Registers the given converter under the given key. If a previous converter is already
+     * registered for the given key, then there is a choice:
+     *
+     * <ul>
+     *   <li>If one converter is defined exactly for the {@code <S,T>} classes while the
+     *       other converter is not, then the most accurate converter will have precedence.</li>
+     *   <li>Otherwise the new converter is registered in addition of the old one in a
+     *       chain of fallbacks.</li>
+     * </ul>
+     *
+     * @param key The key under which to register the converter.
+     * @param converter The converter to register.
+     */
+    @SuppressWarnings("unchecked")
+    private <S,T> void register(final ClassPair<S,T> key, ObjectConverter<S, ? extends T> converter) {
+        final ObjectConverter<? super S, ? extends T> existing = get(key);
+        if (existing != null) {
+            /*
+             * An other converter already exists for the given key. If the converters are
+             * equal (i.e. the user registered the same converter twice), do nothing.
+             */
+            if (existing.equals(converter)) {
+                return;
+            }
+            /*
+             * FallbackConverters are created only for converters having the same source class.
+             * If this is not the case, the new converter will replace the existing one because
+             * its source is more specific:  the source of 'converter' is of type <S> while the
+             * source of 'existing' is of type <? super S>.
+             */
+            assert converter.getSourceClass() == key.sourceClass; // Enforced by parameterized type.
+            if (existing.getSourceClass() == key.sourceClass) {
+                final boolean oldIsExact = existing .getTargetClass() == key.targetClass;
+                final boolean newIsExact = converter.getTargetClass() == key.targetClass;
+                if (oldIsExact & !newIsExact) {
+                    /*
+                     * The existing converter was defined exactly for the <S,T> classes, while the
+                     * new one was defined for an other target. Do not touch the old converter and
+                     * discard the new one. The new converter is not really lost since it should
+                     * have been registered in a previous iteration for its own <S,T> classes.
+                     */
+                    return;
+                }
+                if (newIsExact == oldIsExact) {
+                    /*
+                     * If no converter is considered more accurate than the other, keep both of
+                     * them in a fallback chain. Note that the cast to <S,…> is safe because we
+                     * checked the source class in the above 'if' statement.
+                     */
+                    converter = FallbackConverter.merge((ObjectConverter<S, ? extends T>) existing, converter);
+                    assert key.targetClass.isAssignableFrom(converter.getTargetClass()) : converter; // See FallbackConverter.merge javadoc.
+                }
+            }
+        }
+        put(key, converter);
+    }
+
+    /**
+     * Returns a converter for the specified source and target classes.
+     *
+     * @param  <S> The source class.
+     * @param  <T> The target class.
+     * @param  source The source class.
+     * @param  target The target class, or {@code Object.class} for any.
+     * @return The converter from the specified source class to the target class.
+     * @throws UnconvertibleObjectException if no converter is found.
+     */
+    public <S,T> ObjectConverter<? super S, ? extends T> find(final Class<S> source, final Class<T> target)
+            throws UnconvertibleObjectException
+    {
+        final ClassPair<S,T> key = new ClassPair<>(source, target);
+        synchronized (converters) {
+            ObjectConverter<? super S, ? extends T> converter = get(key);
+            if (converter != null) {
                 return converter;
             }
-            lock = locks.writeLock();
-            lock.lock();
-            try {
-                existing = (SystemConverter<S,T>) converters.put(converter, converter);
-                if (existing != null) {
-                    converters.put(existing, existing);
-                } else {
-                    existing = converter;
+            /*
+             * At this point, no converter were found explicitly for the given key. Searches a
+             * converter accepting some super-class of S, and if we find any cache the result.
+             * This is the complement of the search performed in the register(ObjectConverter)
+             * method, which looked for the parents of the target class. Here we process the
+             * case of the source class.
+             */
+            ClassPair<? super S, T> candidate = key;
+            while ((candidate = candidate.parentSource()) != null) {
+                converter = get(candidate);
+                if (converter != null) {
+                    put(key, converter);
+                    return converter;
                 }
-            } finally {
-                lock.unlock();
+            }
+            /*
+             * No converter found. Gives a chance to subclasses to provide dynamically-generated
+             * converter. The default implementation does not provide any.
+             */
+            converter = createConverter(source, target);
+            if (converter != null) {
+                put(key, converter);
+                return converter;
             }
         }
-        return existing;
+        /*
+         * No explicit converter were found. Checks for the trivial case where an identity
+         * converter would fit. We perform this operation last in order to give a chance to
+         * register an explicit converter if we need to.
+         */
+        if (target.isAssignableFrom(source)) {
+            return key.cast(IdentityConverter.create(source));
+        }
+        throw new UnconvertibleObjectException(Errors.format(Errors.Keys.CanNotConvertFromType_2, source, target));
+    }
+
+    /**
+     * Creates a new converter for the given source and target types, or {@code null} if none.
+     * This method is invoked by <code>{@linkplain #find find}(source, target)</code> when no
+     * registered converter were found for the given types.
+     *
+     * @param  <S> The source class.
+     * @param  <T> The target class.
+     * @param  source The source class.
+     * @param  target The target class, or {@code Object.class} for any.
+     * @return A newly generated converter from the specified source class to the target class,
+     *         or {@code null} if none.
+     */
+    protected <S,T> ObjectConverter<S,T> createConverter(final Class<S> source, final Class<T> target) {
+        return null;
     }
 }

Modified: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/SystemConverter.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/SystemConverter.java?rev=1456484&r1=1456483&r2=1456484&view=diff
==============================================================================
--- sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/SystemConverter.java [UTF-8] (original)
+++ sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/internal/converter/SystemConverter.java [UTF-8] Thu Mar 14 15:37:01 2013
@@ -75,12 +75,47 @@ abstract class SystemConverter<S,T> exte
     }
 
     /**
+     * Performs the comparisons documented in {@link ClassPair#equals(Object)} with an additional
+     * check: if <strong>both</strong> objects to compare are {@code SystemConverter}, then also
+     * requires the two objects to be of the same class. We do that in order to differentiate the
+     * "ordinary" converters from the {@link FallbackConverter}.
+     *
+     * {@section Implementation note}
+     * This is admittedly a little bit convolved. A cleaner approach would have been to not allow
+     * the {@code ConverterRegister} hash map to contain anything else than {@code ClassPair} keys,
+     * but the current strategy of using the same instance for keys and values reduces a little bit
+     * the number of objects to create in the JVM. An other cleaner approach would have been to
+     * compare {@code ObjectConverter}s in a separated method, but users invoking {@code equals}
+     * on our system converters could be surprised.
+     *
+     * <p>Our {@code equals(Object)} definition have the following implications regarding
+     * the way to use the {@link ConverterRegistry#converters} map:</p>
+     * <ul>
+     *   <li>When searching for a converter of the same class than the key (as in the
+     *       {@link ConverterRegistry#findEquals(SystemConverter)} method), then there
+     *       is no restriction on the key that can be given to the {@code Map.get(K)}
+     *       method. The {@code Map} is "normal".</li>
+     *   <li>When searching for a converter for a pair of source and target classes
+     *       (as in {@link ConverterRegistry#find(Class, Class)}), the key shall be
+     *       an instance of {@code ClassPair} instance (not a subclass).</li>
+     * </ul>
+     */
+    @Override
+    public final boolean equals(final Object other) {
+        if (super.equals(other)) {
+            final Class<?> type = other.getClass();
+            return type == ClassPair.class || type == getClass();
+        }
+        return false;
+    }
+
+    /**
      * Returns an unique instance of this converter. If a converter already exists for the same
      * source an target classes, then this converter is returned. Otherwise this converter is
      * cached and returned.
      */
     final ObjectConverter<S,T> unique() {
-        return ConverterRegistry.SYSTEM.unique(this, true);
+        return ConverterRegistry.SYSTEM.unique(this);
     }
 
     /**
@@ -88,7 +123,8 @@ abstract class SystemConverter<S,T> exte
      * in the virtual machine, we do not cache the instance (for now) for security reasons.
      */
     protected final Object readResolve() throws ObjectStreamException {
-        return ConverterRegistry.SYSTEM.unique(this, false);
+        final ObjectConverter<S,T> existing = ConverterRegistry.SYSTEM.findEquals(this);
+        return (existing != null) ? existing : this;
     }
 
     /**