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 2022/11/13 16:30:43 UTC

[sis] 02/02: Save the "TypeName to Java class" association in `DefaultTypeName`.

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 7b995facc0023ba36a3fb1785be1b49e2ffcd29a
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Nov 13 17:27:51 2022 +0100

    Save the "TypeName to Java class" association in `DefaultTypeName`.
---
 .../apache/sis/util/iso/DefaultNameFactory.java    |  76 +++++---
 .../org/apache/sis/util/iso/DefaultTypeName.java   | 195 ++++++++++++---------
 .../main/java/org/apache/sis/util/iso/Names.java   |  43 +++--
 .../java/org/apache/sis/util/iso/TypeNames.java    |  33 ++--
 .../java/org/apache/sis/util/iso/NamesTest.java    |  35 ++--
 .../org/apache/sis/util/iso/TypeNamesTest.java     |  10 +-
 .../org/apache/sis/util/UnknownNameException.java  |   7 +-
 7 files changed, 218 insertions(+), 181 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultNameFactory.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultNameFactory.java
index a7557d787a..c093d2d7b4 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultNameFactory.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultNameFactory.java
@@ -22,6 +22,7 @@ import java.util.Arrays;
 import java.util.Locale;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.lang.reflect.Type;
 import org.opengis.metadata.Identifier;
 import org.opengis.util.TypeName;
 import org.opengis.util.NameSpace;
@@ -32,6 +33,7 @@ import org.opengis.util.NameFactory;
 import org.opengis.util.InternationalString;
 import org.apache.sis.util.SimpleInternationalString;
 import org.apache.sis.util.DefaultInternationalString;
+import org.apache.sis.util.UnknownNameException;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.collection.WeakHashSet;
@@ -70,7 +72,7 @@ import org.apache.sis.internal.util.Strings;
  * from multiple threads.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.5
+ * @version 1.3
  *
  * @see Names
  * @see DefaultNameSpace
@@ -198,27 +200,50 @@ public class DefaultNameFactory extends AbstractFactory implements NameFactory {
     }
 
     /**
-     * Creates a type name from the given character sequence.
+     * Creates a type name from the given character sequence and automatically inferred Java type.
      * The default implementation returns a new or an existing {@link DefaultTypeName} instance.
+     * See {@link DefaultTypeName} javadoc for the list of recognized type names.
      *
-     * @param  scope  the {@linkplain AbstractName#scope() scope} of the type name to be created,
+     * @param  scope  the {@linkplain AbstractName#scope() scope} of the type name create,
      *                or {@code null} for a global namespace.
      * @param  name   the type name as a string or an international string.
-     * @return the type name for the given character sequence.
+     * @return the type name for the given scope and character sequence.
+     * @throws UnknownNameException if a mapping from the name to a Java class was expected to exist
+     *         (because the specified scope is "OGC" or "class") but the associated Java class can not be found.
      *
      * @see #toTypeName(Class)
+     * @see DefaultTypeName#DefaultTypeName(NameSpace, CharSequence)
      * @see Names#createTypeName(CharSequence, String, CharSequence)
      */
     @Override
-    public TypeName createTypeName(final NameSpace scope, final CharSequence name) {
+    public TypeName createTypeName(final NameSpace scope, final CharSequence name) throws UnknownNameException {
         return pool.unique(new DefaultTypeName(scope, name));
     }
 
+    /**
+     * Creates a type name from the given character sequence and explicit Java type.
+     * The default implementation returns a new or an existing {@link DefaultTypeName} instance.
+     *
+     * @param  scope     the {@linkplain AbstractName#scope() scope} of the type name to create,
+     *                   or {@code null} for a global namespace.
+     * @param  name      the type name as a string or an international string.
+     * @param  javaType  the value type to be returned by {@link #toJavaType()}, or {@code null} if none.
+     * @return the type name for the given scope, character sequence and Java type.
+     *
+     * @see #toTypeName(Class)
+     * @see DefaultTypeName#DefaultTypeName(NameSpace, CharSequence, Type)
+     *
+     * @since 1.3
+     */
+    public TypeName createTypeName(final NameSpace scope, final CharSequence name, final Type javaType) {
+        return pool.unique(new DefaultTypeName(scope, name, javaType));
+    }
+
     /**
      * Creates a member name from the given character sequence and attribute type.
      * The default implementation returns a new or an existing {@link DefaultMemberName} instance.
      *
-     * @param  scope  the {@linkplain AbstractName#scope() scope} of the member name to be created,
+     * @param  scope  the {@linkplain AbstractName#scope() scope} of the member name to create,
      *                or {@code null} for a global namespace.
      * @param  name   the member name as a string or an international string.
      * @param  attributeType  the type of the data associated with the record member.
@@ -235,7 +260,7 @@ public class DefaultNameFactory extends AbstractFactory implements NameFactory {
      * Creates a local name from the given character sequence.
      * The default implementation returns a new or an existing {@link DefaultLocalName} instance.
      *
-     * @param  scope  the {@linkplain AbstractName#scope() scope} of the local name to be created,
+     * @param  scope  the {@linkplain AbstractName#scope() scope} of the local name to create,
      *                or {@code null} for a global namespace.
      * @param  name   the local name as a string or an international string.
      * @return the local name for the given character sequence.
@@ -261,7 +286,7 @@ public class DefaultNameFactory extends AbstractFactory implements NameFactory {
      * array is 1, or an instance of {@link DefaultScopedName} if the length of the array is 2
      * or more.
      *
-     * @param  scope        the {@linkplain AbstractName#scope() scope} of the generic name to be created,
+     * @param  scope        the {@linkplain AbstractName#scope() scope} of the generic name to create,
      *                      or {@code null} for a global namespace.
      * @param  parsedNames  the local names as an array of {@link String} or {@link InternationalString} instances.
      *                      This array shall contain at least one element.
@@ -283,7 +308,7 @@ public class DefaultNameFactory extends AbstractFactory implements NameFactory {
      * This method splits the given name around a separator inferred from the given scope, or the
      * {@link DefaultNameSpace#DEFAULT_SEPARATOR ':'} separator if the given scope is null.
      *
-     * @param  scope  the {@linkplain AbstractName#scope() scope} of the generic name to be created,
+     * @param  scope  the {@linkplain AbstractName#scope() scope} of the generic name to create,
      *                or {@code null} for a global namespace.
      * @param  name   the qualified name, as a sequence of names separated by a scope-dependent separator.
      * @return a name parsed from the given string.
@@ -409,15 +434,12 @@ public class DefaultNameFactory extends AbstractFactory implements NameFactory {
     /**
      * Suggests a type name for the given class. Apache SIS provides a mapping between {@code Class}
      * and {@code TypeName} objects as documented in the {@link DefaultTypeName} javadoc.
-     *
-     * <p>In order to protect against potential changes in the {@code Class} ↔ {@code TypeName} mapping, users are
-     * encouraged to retrieve the {@code valueClass} by invoking the {@link Names#toClass(TypeName)} method instead
-     * than parsing the name.</p>
+     * The given {@code valueClass} can be fetched back by {@link DefaultTypeName#toJavaType()}.
      *
      * @param  valueClass  the Java class for which to get a type name, or {@code null}.
      * @return a suggested type name, or {@code null} if the given class was null.
      *
-     * @see DefaultTypeName#toClass()
+     * @see DefaultTypeName#toJavaType()
      * @see Names#toClass(TypeName)
      * @see Names#createTypeName(Class)
      *
@@ -431,26 +453,26 @@ public class DefaultNameFactory extends AbstractFactory implements NameFactory {
          * Note: we do not cache the TypeName for the valueClass argument because:
          *
          *  - It is not needed (at least in the default implementation) for getting unique instance.
-         *  - It is not the best place for performance improvement, since TypeName are usually only
-         *    a step in the creation of bigger object (typically AttributeType). Users are better to
-         *    cache the bigger object instead.
+         *  - It is not the best place for performance improvement, because `TypeName` is usually
+         *    only a step in the creation of bigger object (typically `AttributeType`).
+         *    Callers should cache the bigger object instead.
          */
-        TypeNames t = typeNames;
-        if (t == null) {
+        TypeNames mapper = typeNames;
+        if (mapper == null) {
             /*
-             * Create TypeNames outide the synchronized block because the TypeNames constructor will call back
-             * methods from this class. Since those methods are overrideable, this could invoke user's code.
-             * Note also that methods in this class use the `pool`, which is itself synchronized, so we are
-             * better to avoid double synchronization for reducing the risk of dead-lock.
+             * Create TypeNames outside the synchronized block because the TypeNames constructor will call back
+             * methods from this class. Since those methods are overrideable, they could invoke user's code.
+             * Note also that methods in this class use the `pool`, which is itself synchronized,
+             * so we are better to avoid double synchronization for reducing the risk of dead-lock.
              */
             final TypeNames c = new TypeNames(this);
             synchronized (this) {                       // Double-check strategy is ok if `typeNames` is volatile.
-                t = typeNames;
-                if (t == null) {
-                    typeNames = t = c;
+                mapper = typeNames;
+                if (mapper == null) {
+                    typeNames = mapper = c;
                 }
             }
         }
-        return t.toTypeName(this, valueClass);
+        return mapper.toTypeName(this, valueClass);
     }
 }
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultTypeName.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultTypeName.java
index deaca620e4..7e00cfc999 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultTypeName.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/DefaultTypeName.java
@@ -16,6 +16,9 @@
  */
 package org.apache.sis.util.iso;
 
+import java.util.Objects;
+import java.util.Optional;
+import java.lang.reflect.Type;
 import javax.xml.bind.annotation.XmlType;
 import javax.xml.bind.annotation.XmlRootElement;
 import org.opengis.util.TypeName;
@@ -33,7 +36,7 @@ import org.apache.sis.util.UnknownNameException;
  * </ul>
  *
  * <h2>Mapping Java classes to type names</h2>
- * It is sometime useful to establish a mapping between {@link Class} and {@code TypeName}.
+ * A bidirectional mapping is defined between {@code TypeName} and Java {@link Class}.
  * When an UML identifier from an OGC standard exists for a given {@code Class},
  * Apache SIS uses that identifier prefixed by the {@code "OGC"} namespace.
  * Note that this is <strong>not</strong> a standard practice.
@@ -114,7 +117,7 @@ import org.apache.sis.util.UnknownNameException;
  *
  * The mapping defined by Apache SIS may change in any future version depending on standardization progress.
  * To protect against such changes, users are encouraged to rely on methods or constructors like
- * {@link DefaultNameFactory#toTypeName(Class)} or {@link #toClass()} instead of parsing the name.
+ * {@link #toJavaType()} or {@link DefaultNameFactory#toTypeName(Class)} instead of parsing the name.
  *
  *
  * <h2>Immutability and thread safety</h2>
@@ -125,7 +128,7 @@ import org.apache.sis.util.UnknownNameException;
  * @author  Guilhem Legal (Geomatys)
  * @author  Cédric Briançon (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.5
+ * @version 1.3
  *
  * @see DefaultMemberName
  * @see DefaultNameFactory
@@ -139,33 +142,85 @@ public class DefaultTypeName extends DefaultLocalName implements TypeName {
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = 7182126541436753582L;
+    private static final long serialVersionUID = 7571710679743017926L;
 
     /**
-     * The value class to be returned by {@link #toClass()}, or {@code null} if not yet computed.
-     * {@link Void#TYPE} is used as a sentinel value meaning explicit {@code null}.
+     * The value returned by {@link #toJavaType()}, or {@code null} if none.
+     * This is usually a {@link Class}, which is serializable.
+     */
+    @SuppressWarnings("serial")         // Not statically typed as Serializable.
+    private final Type javaType;
+
+    /**
+     * Constructs a type name from the given character sequence and infers automatically a Java type.
+     * The scope and name arguments are given unchanged to the
+     * {@linkplain DefaultLocalName#DefaultLocalName(NameSpace,CharSequence) super-class constructor}.
+     * Then the Java type is inferred in a way that depends on the specified scope:
+     *
+     * <ul>
+     *   <li>If the scope is {@code "OGC"}, then:
+     *     <ul>
+     *       <li>If the name is {@code "CharacterString"}, {@code "Integer"}, {@code "Real"} or other recognized names
+     *           (see {@linkplain DefaultTypeName class javadoc}),
+     *           then the corresponding Java class is associated to this type name.</li>
+     *       <li>Otherwise {@link UnknownNameException} is thrown.</li>
+     *     </ul>
+     *   </li>
+     *   <li>Else if the scope is {@code "class"}, then:
+     *     <ul>
+     *       <li>If the name is accepted by {@link Class#forName(String)},
+     *           then that Java class is associated to this type name.</li>
+     *       <li>Otherwise {@link UnknownNameException} is thrown.</li>
+     *     </ul>
+     *   </li>
+     *   <li>Else if the scope {@linkplain DefaultNameSpace#isGlobal() is global}, then:
+     *     <ul>
+     *       <li>If the name is one of the names recognized in {@code "OGC"} scope (see above),
+     *           then the corresponding class is associated to this type name.</li>
+     *       <li>Otherwise no Java class is associated to this type name.
+     *           No exception is thrown because names in the global namespace could be anything;
+     *           this constructor can not know if the given name was wrong.</li>
+     *     </ul>
+     *   </li>
+     *   <li>Otherwise no Java class is associated to this type name,
+     *       because this method can not check the validity of names in other namespaces.</li>
+     * </ul>
      *
-     * <p>This value is only computed. We do not allow the user to explicitly specify it, because we
-     * need that {@code DefaultTypeName}s having identical name also have the same {@code valueClass}.
-     * This is necessary {@link DefaultNameFactory#pool} cache integrity. Users who want to explicitly
-     * specify their own value class can override {@link #toClass()} instead.</p>
+     * @param  scope  the scope of this name, or {@code null} for a global scope.
+     * @param  name   the local name (never {@code null}).
+     * @throws UnknownNameException if a mapping from this name to a Java class was expected to exist
+     *         (because the specified scope is "OGC" or "class") but the associated Java class can not be found.
      *
-     * @see #setValueClass(NameSpace, String, Class)
-     * @see #toClass()
+     * @see DefaultNameFactory#createTypeName(NameSpace, CharSequence)
      */
-    private transient Class<?> valueClass;
+    protected DefaultTypeName(final NameSpace scope, final CharSequence name) throws UnknownNameException {
+        super(scope, name);
+        try {
+            javaType = TypeNames.toClass(TypeNames.namespace(scope), super.toString());
+        } catch (ClassNotFoundException e) {
+            throw new UnknownNameException(TypeNames.unknown(super.toFullyQualifiedName()), e);
+        }
+        if (javaType == Void.TYPE) {
+            throw new UnknownNameException(TypeNames.unknown(super.toFullyQualifiedName()));
+        }
+    }
 
     /**
-     * Constructs a type name from the given character sequence. The argument are given unchanged to the
+     * Constructs a type name from the given character sequence and explicit Java type.
+     * The scope and name arguments are given unchanged to the
      * {@linkplain DefaultLocalName#DefaultLocalName(NameSpace,CharSequence) super-class constructor}.
      *
-     * @param scope  the scope of this name, or {@code null} for a global scope.
-     * @param name   the local name (never {@code null}).
+     * @param scope     the scope of this name, or {@code null} for a global scope.
+     * @param name      the local name (never {@code null}).
+     * @param javaType  the value type to be returned by {@link #toJavaType()}, or {@code null} if none.
      *
-     * @see DefaultNameFactory#createTypeName(NameSpace, CharSequence)
+     * @see DefaultNameFactory#createTypeName(NameSpace, CharSequence, Type)
+     *
+     * @since 1.3
      */
-    protected DefaultTypeName(final NameSpace scope, final CharSequence name) {
+    protected DefaultTypeName(final NameSpace scope, final CharSequence name, final Type javaType) {
         super(scope, name);
+        this.javaType = javaType;
     }
 
     /**
@@ -190,88 +245,59 @@ public class DefaultTypeName extends DefaultLocalName implements TypeName {
         if (object == null || object instanceof DefaultTypeName) {
             return (DefaultTypeName) object;
         }
-        return new DefaultTypeName(object.scope(), object.toInternationalString());
+        return new DefaultTypeName(object.scope(), object.toInternationalString(), object.toJavaType().orElse(null));
     }
 
     /**
-     * Sets {@link #valueClass} to the given value, only if the scope and the name of this {@code TypeName}
-     * are equal to the given values. The check for scope and name is a protection against renaming that user
-     * could apply if they subclass {@link DefaultNameFactory}. If the user performed such renaming, then the
-     * value class may be wrong, so we will ignore the given value class and let {@link #toClass()} computes
-     * the class itself.
+     * Returns the Java type represented by this name.
+     * This is the type either specified explicitly at construction time or inferred from the type name.
+     *
+     * @return the Java type (usually a {@link Class}) for this type name.
+     *
+     * @see Names#toClass(TypeName)
+     *
+     * @since 1.3
      */
-    final void setValueClass(final NameSpace scope, final String name, final Class<?> valueClass) {
-        if (scope == super.scope() && name.equals(super.toString())) {
-            this.valueClass = valueClass;
-        }
+    @Override
+    public Optional<Type> toJavaType() {
+        return Optional.ofNullable(javaType);
     }
 
     /**
      * Returns the Java class associated to this type name.
-     * The default implementation parses this name in different ways depending on the {@linkplain #scope() scope}:
      *
-     * <ul>
-     *   <li>If the scope is {@code "OGC"}, then:
-     *     <ul>
-     *       <li>If the name is {@code "CharacterString"}, {@code "Integer"}, {@code "Real"} or other recognized names
-     *           (see {@linkplain DefaultTypeName class javadoc}), then the corresponding class is returned.</li>
-     *       <li>Otherwise {@link UnknownNameException} is thrown.</li>
-     *     </ul>
-     *   </li>
-     *   <li>Else if the scope is {@code "class"}, then:
-     *     <ul>
-     *       <li>If the name is accepted by {@link Class#forName(String)}, then that class is returned.</li>
-     *       <li>Otherwise {@link UnknownNameException} is thrown.</li>
-     *     </ul>
-     *   </li>
-     *   <li>Else if the scope {@linkplain DefaultNameSpace#isGlobal() is global}, then:
-     *     <ul>
-     *       <li>If the name is one of the names recognized in {@code "OGC"} scope (see above),
-     *           then the corresponding class is returned.</li>
-     *       <li>Otherwise {@code null} is returned. No exception is thrown because names in the global namespace
-     *           could be anything, so we can not be sure that the given name was wrong.</li>
-     *     </ul>
-     *   </li>
-     *   <li>Otherwise {@code null} is returned, since this method can not check the validity of names in other
-     *       namespaces.</li>
-     * </ul>
+     * @deprecated Replaced by {@link #toJavaType()}.
      *
      * @return the Java class associated to this {@code TypeName},
      *         or {@code null} if there is no mapping from this name to a Java class.
-     * @throws UnknownNameException if a mapping from this name to a Java class was expected to exist
-     *         (typically because of the {@linkplain #scope() scope}) but the operation failed.
-     *
-     * @see Names#toClass(TypeName)
-     * @see DefaultNameFactory#toTypeName(Class)
      *
      * @since 0.5
      */
-    public Class<?> toClass() throws UnknownNameException {
-        /*
-         * No synchronization, because it is not a problem if two threads compute the same value concurrently.
-         * No volatile field neither, because instances of Class are safely published (well, I presume...).
-         */
-        Class<?> c = valueClass;
-        if (c == Void.TYPE) {
-            return null;
-        }
-        if (c == null) {
-            /*
-             * Invoke super.foo() instead of this.foo() because we do not want to invoke any overridden method.
-             * This is for ensuring that two TypeNames constructed with the same name will map to the same class.
-             * See `valueClass` javadoc for more information.
-             */
-            try {
-                c = TypeNames.toClass(TypeNames.namespace(super.scope()), super.toString());
-            } catch (ClassNotFoundException e) {
-                throw new UnknownNameException(TypeNames.unknown(super.toFullyQualifiedName()), e);
-            }
-            if (c == null) {
-                throw new UnknownNameException(TypeNames.unknown(super.toFullyQualifiedName()));
-            }
-            valueClass = c;
+    @Deprecated
+    public Class<?> toClass() {
+        return (javaType instanceof Class<?>) ? (Class<?>) javaType : null;
+    }
+
+    /**
+     * Compares this type name with the specified object for equality.
+     *
+     * @param  object  the object to compare with this type for equality.
+     * @return {@code true} if the given object is equal to this name.
+     */
+    @Override
+    public boolean equals(final Object object) {
+        if (super.equals(object)) {
+            return Objects.equals(javaType, ((DefaultTypeName) object).javaType);
         }
-        return (c != Void.TYPE) ? c : null;
+        return false;
+    }
+
+    /**
+     * Invoked by {@link #hashCode()} for computing the hash code value when first needed.
+     */
+    @Override
+    int computeHashCode() {
+        return super.computeHashCode() ^ Objects.hashCode(javaType);
     }
 
 
@@ -293,5 +319,6 @@ public class DefaultTypeName extends DefaultLocalName implements TypeName {
      * the {@link #name} field will be set by JAXB during unmarshalling.
      */
     private DefaultTypeName() {
+        javaType = null;
     }
 }
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java
index cdce2bb011..8e80469e5a 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java
@@ -17,6 +17,7 @@
 package org.apache.sis.util.iso;
 
 import java.util.Collections;
+import java.lang.reflect.Type;
 import org.opengis.util.TypeName;
 import org.opengis.util.LocalName;
 import org.opengis.util.MemberName;
@@ -292,7 +293,7 @@ public final class Names extends Static {
         ensureNonNull("valueClass", valueClass);
         final DefaultNameFactory factory = DefaultFactories.forBuildin(NameFactory.class, DefaultNameFactory.class);
         return factory.createMemberName(createNameSpace(factory, namespace, separator), localPart,
-                factory.toTypeName(valueClass));    // SIS-specific method.
+               factory.toTypeName(valueClass));     // SIS-specific method.
     }
 
     /**
@@ -338,8 +339,7 @@ public final class Names extends Static {
      *
      * <ul>
      *   <li>If the given type name is {@code null}, then this method returns {@code null}.</li>
-     *   <li>Else if the given type name is an instance of {@code DefaultTypeName},
-     *       then this method delegates to {@link DefaultTypeName#toClass()}.</li>
+     *   <li>Else if the value returned by {@link DefaultTypeName#toJavaType()} is a {@link Class}, returns that class.</li>
      *   <li>Else if the type name {@linkplain DefaultTypeName#scope() scope} is {@code "OGC"}, then:
      *     <ul>
      *       <li>If the name is {@code "CharacterString"}, {@code "Integer"}, {@code "Real"} or other recognized names
@@ -358,21 +358,21 @@ public final class Names extends Static {
      *       <li>If the name is one of the names recognized in {@code "OGC"} scope (see above),
      *           then the corresponding class is returned.</li>
      *       <li>Otherwise {@code null} is returned. No exception is thrown because names in the global namespace
-     *           could be anything, so we can not be sure that the given name was wrong.</li>
+     *           could be anything; this method can not be sure that the given name was wrong.</li>
      *     </ul>
      *   </li>
-     *   <li>Otherwise {@code null} is returned, since this method can not check the validity of names in other
-     *       namespaces.</li>
+     *   <li>Otherwise {@code null} is returned,
+     *       because this method can not check the validity of names in other namespaces.</li>
      * </ul>
      *
      * @param  type  the type name from which to infer a Java class.
      * @return the Java class associated to the given {@code TypeName},
      *         or {@code null} if there is no mapping from the given name to a Java class.
      * @throws UnknownNameException if a mapping from the given name to a Java class was expected to exist
-     *         (typically because of the {@linkplain DefaultTypeName#scope() scope}) but the operation failed.
+     *         (typically because of the {@linkplain DefaultTypeName#scope() scope}) but the lookup failed.
      *
      * @see #createTypeName(Class)
-     * @see DefaultTypeName#toClass()
+     * @see DefaultTypeName#toJavaType()
      *
      * @since 0.5
      */
@@ -380,21 +380,18 @@ public final class Names extends Static {
         if (type == null) {
             return null;
         }
-        Class<?> c;
-        if (type instanceof DefaultTypeName) {
-            c = ((DefaultTypeName) type).toClass();
-        } else {
-            try {
-                c = TypeNames.toClass(TypeNames.namespace(type.scope()), type.toString());
-            } catch (ClassNotFoundException e) {
-                throw new UnknownNameException(TypeNames.unknown(type), e);
-            }
-            if (c == null) {
-                throw new UnknownNameException(TypeNames.unknown(type));
-            }
-            if (c == Void.TYPE) {
-                c = null;
-            }
+        final Type t = type.toJavaType().orElse(null);
+        if (t instanceof Class<?>) {
+            return (Class<?>) t;
+        }
+        final Class<?> c;
+        try {
+            c = TypeNames.toClass(TypeNames.namespace(type.scope()), type.toString());
+        } catch (ClassNotFoundException e) {
+            throw new UnknownNameException(TypeNames.unknown(type), e);
+        }
+        if (c == Void.TYPE) {
+            throw new UnknownNameException(TypeNames.unknown(type));
         }
         return c;
     }
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/TypeNames.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/TypeNames.java
index 2c28bad824..356d520f14 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/TypeNames.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/TypeNames.java
@@ -127,34 +127,29 @@ search: if (CharSequence.class.isAssignableFrom(valueClass)) {
                 name = valueClass.getName();    // See above comment.
             }
         }
-        /*
-         * Now create the name and remember the `valueClass` for that name if the implementation allows that.
-         */
-        final TypeName t = factory.createTypeName(ns, name);
-        if (t instanceof DefaultTypeName) {
-            ((DefaultTypeName) t).setValueClass(ns, name, valueClass);
+        if (factory instanceof DefaultNameFactory) {
+            return ((DefaultNameFactory) factory).createTypeName(ns, name, valueClass);
         }
-        return t;
+        return factory.createTypeName(ns, name);
     }
 
     /**
      * Returns the class for a {@code TypeName} made of the given scope and name.
      * This method is the converse of {@link #toTypeName(NameFactory, Class)}.
-     * This method returns 3 kind of values:
+     * There is 3 kinds of return value:
      *
      * <ul>
-     *   <li>{@code Void.TYPE} if the namespace or the name is unrecognized, without considering that as an error.
-     *       This is a sentinel value expected by {@link DefaultTypeName#toClass()} for such case.</li>
-     *   <li>{@code null} if {@code namespace} is recognized, but not the {@code name}.
-     *       This will be considered as an error by {@link DefaultTypeName#toClass()}.</li>
+     *   <li>{@code null} if the namespace or the name is unrecognized, without considering that as an error.</li>
+     *   <li>{@code Void.TYPE} if {@code namespace} is recognized, but not the {@code name}.
+     *       This is a sentinel value to be considered as an error by {@link DefaultTypeName} constructor.</li>
      *   <li>Otherwise the class for the given name.</li>
      * </ul>
      *
      * @param  namespace  the namespace, case-insensitive. Can be any value, but this method recognizes
      *         only {@code "OGC"}, {@code "class"} and {@code null}. Other namespaces will be ignored.
-     * @param  name  the name, case-sensitive.
-     * @return the class, or {@code Void.TYPE} if the given namespace is not recognized,
-     *         or {@code null} if the namespace is recognized but not the name.
+     * @param  name  the type name, case-sensitive.
+     * @return the class, or {@code null} if the given namespace is not recognized,
+     *         or {@code Void.TYPE} if the namespace is recognized but not the type name.
      * @throws ClassNotFoundException if {@code namespace} is {@code "class"} but {@code name} is not
      *         the name of a reachable class.
      */
@@ -164,20 +159,20 @@ search: if (CharSequence.class.isAssignableFrom(valueClass)) {
             c = MAPPING.get(name);
             if (c == null) {
                 c = Types.forStandardName(name);
-                if (c == null && namespace == null) {
-                    c = Void.TYPE;          // Unknown name not considered an error if not in "OGC" namespace.
+                if (c == null && namespace != null) {
+                    c = Void.TYPE;          // Unknown name in OGC namespace.
                 }
             }
         } else if (namespace.equalsIgnoreCase("class")) {
             c = Class.forName(name);
         } else {
-            c = Void.TYPE;                  // Not an "OGC" or "class" namespace.
+            c = null;                   // Not an "OGC" or "class" namespace.
         }
         return c;
     }
 
     /**
-     * Ensures that the given class is not {@link Void#TYPE}.
+     * Ensures that the given class is non-null and not {@link Void#TYPE}.
      * This is a helper method for callers of {@link #toTypeName(NameFactory, Class)}.
      */
     static boolean isValid(final Class<?> valueClass) {
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/util/iso/NamesTest.java b/core/sis-metadata/src/test/java/org/apache/sis/util/iso/NamesTest.java
index 454f71e0b2..2463f844de 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/util/iso/NamesTest.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/util/iso/NamesTest.java
@@ -39,7 +39,7 @@ import static org.junit.Assert.*;
  * Tests the {@link Names} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.5
  * @module
  */
@@ -84,8 +84,12 @@ public final strictfp class NamesTest extends TestCase {
         assertValueClassEquals(Random.class, type);
         assertValueClassEquals(DefaultNameFactoryTest.class,
                 new DefaultTypeName(type.scope(), DefaultNameFactoryTest.class.getName()));
-        assertValueClassEquals(UnknownNameException.class,
-                new DefaultTypeName(type.scope(), "org.apache.sis.Dummy"));
+        try {
+            new DefaultTypeName(type.scope(), "org.apache.sis.Dummy");
+            fail("Expected UnknownNameException.");
+        } catch (UnknownNameException e) {
+            assertTrue(e.getMessage().contains("org.apache.sis.Dummy"));
+        }
     }
 
     /**
@@ -100,7 +104,12 @@ public final strictfp class NamesTest extends TestCase {
         assertValueClassEquals(String.class,               type);
         assertValueClassEquals(Double.class,               new DefaultTypeName(type.scope(), "Real"));
         assertValueClassEquals(InternationalString.class,  new DefaultTypeName(type.scope(), "FreeText"));
-        assertValueClassEquals(UnknownNameException.class, new DefaultTypeName(type.scope(), "Dummy"));
+        try {
+            new DefaultTypeName(type.scope(), "Dummy");
+            fail("Expected UnknownNameException.");
+        } catch (UnknownNameException e) {
+            assertTrue(e.getMessage().contains("OGC:Dummy"));
+        }
     }
 
     /**
@@ -116,29 +125,15 @@ public final strictfp class NamesTest extends TestCase {
         assertValueClassEquals(null,         Names.createTypeName(null,   null, "Dummy"));
     }
 
-    /**
-     * Invokes {@link Names#toClass(TypeName)}, but catch {@link UnknownNameException}.
-     * If the latter exception is caught, then this method returns {@code UnknownNameException.class}.
-     */
-    private static Class<?> toClass(final TypeName type) {
-        try {
-            return Names.toClass(type);
-        } catch (UnknownNameException e) {
-            final String message = e.getMessage();
-            assertTrue(message, message.contains(type.toFullyQualifiedName().toString()));
-            return UnknownNameException.class;
-        }
-    }
-
     /**
      * Asserts that calls to {@link Names#toClass(TypeName)} returns the expected value class.
      */
     private static void assertValueClassEquals(final Class<?> expected, final TypeName type) {
-        assertEquals(expected, toClass(type));
+        assertEquals(expected, Names.toClass(type));
         /*
          * Tests detection with an implementation which is not the SIS one.
          */
-        assertEquals(expected, toClass(new TypeName() {
+        assertEquals(expected, Names.toClass(new TypeName() {
             @Override public int                       depth()                  {return type.depth();}
             @Override public List<? extends LocalName> getParsedNames()         {return type.getParsedNames();}
             @Override public LocalName                 head()                   {return type.head();}
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/util/iso/TypeNamesTest.java b/core/sis-metadata/src/test/java/org/apache/sis/util/iso/TypeNamesTest.java
index c217af0338..c15347b357 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/util/iso/TypeNamesTest.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/util/iso/TypeNamesTest.java
@@ -38,7 +38,7 @@ import static org.apache.sis.internal.util.Constants.OGC;
  * Tests are performed through the {@link DefaultNameFactory#toTypeName(Class)} method.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.5
+ * @version 1.3
  * @since   0.5
  * @module
  */
@@ -57,7 +57,7 @@ public final strictfp class TypeNamesTest extends TestCase {
         final DefaultNameFactory factory = DefaultFactories.forBuildin(NameFactory.class, DefaultNameFactory.class);
         final TypeName type = factory.toTypeName(valueClass);
         assertNotNull(name, type);
-        assertSame   (name, valueClass, ((DefaultTypeName) type).toClass());
+        assertSame   (name, valueClass, type.toJavaType().get());
         assertEquals (name, namespace,  type.scope().name().toString());
         assertEquals (name, name,       type.toString());
         assertEquals (name, valueClass, TypeNames.toClass(namespace, name));
@@ -127,10 +127,10 @@ public final strictfp class TypeNamesTest extends TestCase {
      */
     @Test
     public void testInvalidNames() throws ClassNotFoundException {
-        assertEquals("Dummy:Real", Void.TYPE,    TypeNames.toClass("Dummy", "Real"));
+        assertNull  ("Dummy:Real",               TypeNames.toClass("Dummy", "Real"));
         assertEquals(OGC+":Real",  Double.class, TypeNames.toClass(OGC,     "Real"));
         assertEquals("Real",       Double.class, TypeNames.toClass(null,    "Real"));
-        assertEquals("Dummy",      Void.TYPE,    TypeNames.toClass(null,    "Dummy")); // Considered not an error.
-        assertNull  (OGC+":Dummy",               TypeNames.toClass(OGC,     "Dummy")); // Considered an error.
+        assertNull  ("Dummy",                    TypeNames.toClass(null,    "Dummy"));    // Considered not an error.
+        assertEquals(OGC+":Dummy", Void.TYPE,    TypeNames.toClass(OGC,     "Dummy"));    // Considered an error.
     }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/UnknownNameException.java b/core/sis-utility/src/main/java/org/apache/sis/util/UnknownNameException.java
index 824a817fa8..6ce8c284ab 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/UnknownNameException.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/UnknownNameException.java
@@ -24,14 +24,15 @@ package org.apache.sis.util;
  * or any other objects with similar purpose.
  *
  * <p><b>Note:</b> in the particular case of objects created from a {@link org.opengis.util.Factory},
- * the exception for unrecognized identifiers is rather {@link org.opengis.util.NoSuchIdentifierException}.</p>
+ * the exception for unrecognized identifiers is rather {@link org.opengis.util.NoSuchIdentifierException}.
+ * This {@code UnknownNameException} differs in being an unchecked exception.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.5
+ * @version 1.3
  * @since   0.5
  * @module
  */
-public class UnknownNameException extends RuntimeException {
+public class UnknownNameException extends IllegalArgumentException {
     /**
      * For cross-version compatibility.
      */