Repository: apache/jena (java)

Issue:
Equality of RDFDatatypes is by reference only --> Should it stay that way?
### Version

5.3.0-SNAPSHOT

### Question

In [apache/jena#2795](https://github.com/apache/jena/issues/2795), I discovered that no implementation of `RDFDatatype` provides an override for `Object#equals` or `Object#hashCode`, resulting in all comparisons being performed by reference only.

For instance, these tests fail:

```java
@Test public void baseDataTypeEquality() {
    var rdfDataType1 = new BaseDatatype("urn:x-hp-dt:unknown");
    var rdfDataType2 = new BaseDatatype("urn:x-hp-dt:unknown");

    assertEquals(rdfDataType1, rdfDataType2);
}

@Test public void xsdDoubleEquality() {
    var rdfDataType1 = new XSDDouble("double", Double.class);
    var rdfDataType2 = XSDDatatype.XSDdouble;

    assertEquals(rdfDataType1, rdfDataType2);
}
```

To further illustrate the inconsistency, here’s a scenario where the first test fails, but the second one succeeds:

```java
@Test
public void testEqualityA() {
    var l1 = LiteralLabelFactory.create("foo", new BaseDatatype("urn:x-hp-dt:unknown"));
    var l2 = LiteralLabelFactory.create("foo", new BaseDatatype("urn:x-hp-dt:unknown"));

    assertEquals(l1, l2);
}

@Test
public void testEqualityB() {
    var m = ModelFactory.createDefaultModel();
    var l1 = m.createTypedLiteral("foo", "urn:x-hp-dt:unknown");
    var l2 = m.createTypedLiteral("foo", "urn:x-hp-dt:unknown");

    assertEquals(l1, l2);
}
```

I propose implementing `Object#equals` and `Object#hashCode` in `BaseDatatype` to compare instances by their class and URI, as shown below:

```java
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    final BaseDatatype that = (BaseDatatype) o;
    return Objects.equals(uri, that.uri);
}

@Override
public int hashCode() {
    return Objects.hashCode(uri);
}
```

I couldn’t find any subclass of `BaseDatatype` where uniqueness isn’t defined by the URI, so this approach should cover all cases without needing additional overrides.

Pull request:
GH-2808: Implemented Object#equals and #hashCode in BaseDatatype
GitHub issue resolved #2808

Pull request Description:

- Added implementations for `org.apache.jena.datatypes.BaseDatatype#equals` and `#hashCode` based on `RDFDatatype.getURI`, aligning with the principle: "Same URI, same datatype" for RDF datatypes.
-  Added corresponding tests in `org.apache.jena.datatypes.TestDatatypes` to verify equality and hash code behavior.

----

 - [x] Tests are included.
 - no Documentation change and no updates are needed for the [Apache Jena website](https://github.com/apache/jena-site/)
 - [x] Commits have been squashed to remove intermediate development commit messages.
 - [x] Key commit messages start with the issue number (GH-xxxx)

By submitting this pull request, I acknowledge that I am making a contribution to the Apache Software Foundation under the terms and conditions of the [Contributor's Agreement](https://www.apache.org/licenses/contributor-agreements.html).

----

See the [Apache Jena "Contributing" guide](https://github.com/apache/jena/blob/main/CONTRIBUTING.md).


Patch:
diff --git a/jena-core/src/main/java/org/apache/jena/datatypes/BaseDatatype.java b/jena-core/src/main/java/org/apache/jena/datatypes/BaseDatatype.java
index 11a69bd96c..c315666955 100644
--- a/jena-core/src/main/java/org/apache/jena/datatypes/BaseDatatype.java
+++ b/jena-core/src/main/java/org/apache/jena/datatypes/BaseDatatype.java
@@ -247,4 +247,31 @@ public class BaseDatatype implements RDFDatatype {
               + (getJavaClass() == null ? "" : " -> " + getJavaClass())
               + "]";
     }
+
+    /**
+     * Same URI, same datatype.
+     * <p />
+     * See <a href="https://www.w3.org/TR/rdf11-concepts/#section-Graph-Literal">RDF 1.1 Concepts and Abstract Syntax - 3.3 Literals</a>
+     * and <a href="https://www.w3.org/TR/rdf11-concepts/#section-Datatypes">RDF 1.1 Concepts and Abstract Syntax - 5. Datatypes</a>
+     * @param other
+     * @return true if the URIs are the same
+     */
+    @Override
+    public final boolean equals(Object other) {
+        if (this == other) return true;
+        if (other instanceof RDFDatatype that) {
+            return Objects.equals(this.getURI(), that.getURI());
+        }
+        return false;
+    }
+
+    /**
+     * The hash code of a datatype is the hash code of its URI.
+     * @see #equals(Object)
+     * @return the hash code of the URI
+     */
+    @Override
+    public final int hashCode() {
+        return Objects.hashCode(this.getURI());
+    }
 }


Test patch:
diff --git a/jena-core/src/test/java/org/apache/jena/datatypes/TestDatatypes.java b/jena-core/src/test/java/org/apache/jena/datatypes/TestDatatypes.java
index dffc15a5f2..37315a2fdd 100644
--- a/jena-core/src/test/java/org/apache/jena/datatypes/TestDatatypes.java
+++ b/jena-core/src/test/java/org/apache/jena/datatypes/TestDatatypes.java
@@ -20,6 +20,7 @@ package org.apache.jena.datatypes;
 
 import static org.junit.Assert.assertEquals ;
 import static org.junit.Assert.assertFalse ;
+import static org.junit.Assert.assertNotEquals ;
 import static org.junit.Assert.assertNotNull ;
 import static org.junit.Assert.assertTrue ;
 
@@ -27,6 +28,7 @@ import java.math.BigDecimal ;
 import java.util.UUID;
 
 import org.apache.jena.datatypes.xsd.XSDDatatype ;
+import org.apache.jena.datatypes.xsd.impl.XSDDouble ;
 import org.apache.jena.graph.Node ;
 import org.apache.jena.graph.NodeFactory ;
 import org.apache.jena.rdf.model.Resource ;
@@ -197,6 +199,71 @@ public class TestDatatypes {
         testLiteralIsCorrectType("5.55", XSDDatatype.XSDfloat) ;
     }
 
+    @Test public void baseDataTypeEquality() {
+        var rdfDataType1 = new BaseDatatype("urn:x-hp-dt:unknown");
+        assertEquals(rdfDataType1, rdfDataType1);
+
+        var rdfDataType2 = new BaseDatatype("urn:x-hp-dt:unknown");
+        assertEquals(rdfDataType1, rdfDataType2);
+        assertEquals(rdfDataType2, rdfDataType1);
+
+        assertEquals(rdfDataType1.hashCode(), rdfDataType2.hashCode());
+    }
+
+    @Test public void baseDataTypeEmptyEquality() {
+        var rdfDataType1 = new BaseDatatype("");
+        assertEquals(rdfDataType1, rdfDataType1);
+
+        var rdfDataType2 = new BaseDatatype("");
+        assertEquals(rdfDataType1, rdfDataType2);
+        assertEquals(rdfDataType2, rdfDataType1);
+
+        assertEquals(rdfDataType1.hashCode(), rdfDataType2.hashCode());
+    }
+
+    @Test public void baseDataTypeNullEquality() {
+        var rdfDataType1 = new BaseDatatype(null);
+        assertEquals(rdfDataType1, rdfDataType1);
+
+        var rdfDataType2 = new BaseDatatype(null);
+        assertEquals(rdfDataType1, rdfDataType2);
+        assertEquals(rdfDataType2, rdfDataType1);
+
+        assertEquals(rdfDataType1.hashCode(), rdfDataType2.hashCode());
+    }
+
+    @Test public void xsdDoubleEquality() {
+        var rdfDataType1 = new XSDDouble("double", Double.class);
+        assertEquals(rdfDataType1, rdfDataType1);
+
+        var rdfDataType2 = XSDDatatype.XSDdouble;
+        assertEquals(rdfDataType1, rdfDataType2);
+        assertEquals(rdfDataType2, rdfDataType1);
+
+        assertEquals(rdfDataType1.hashCode(), rdfDataType2.hashCode());
+    }
+
+    @Test public void baseDataTypeNotEquals() {
+        var rdfDataType1 = new BaseDatatype("urn:x-hp-dt:unknownA");
+        var rdfDataType2 = new BaseDatatype("urn:x-hp-dt:unknownB");
+
+        assertNotEquals(rdfDataType1, rdfDataType2);
+        assertNotEquals(rdfDataType2, rdfDataType1);
+    }
+
+    @Test public void baseDataTypeNullNotEquals() {
+        var rdfDataType1 = new BaseDatatype("urn:x-hp-dt:unknownA");
+        var rdfDataType2 = new BaseDatatype(null);
+
+        assertNotEquals(rdfDataType1, rdfDataType2);
+        assertNotEquals(rdfDataType2, rdfDataType1);
+    }
+
+    @Test public void hashCodeEqualsUriHashCode() {
+        var rdfDataType = new BaseDatatype("urn:x-hp-dt:unknown");
+        assertEquals(rdfDataType.getURI().hashCode(), rdfDataType.hashCode());
+    }
+
     private void testValueToLex(Object value, XSDDatatype datatype) {
         Node node = NodeFactory.createLiteralByValue(value, datatype) ;
         assertTrue("Not valid lexical form "+value+" -> "+node, datatype.isValid(node.getLiteralLexicalForm())) ;