diff --git a/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/CoreDatatype.java b/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/CoreDatatype.java index 7ed0df4fa90..19db1269cea 100644 --- a/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/CoreDatatype.java +++ b/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/CoreDatatype.java @@ -110,7 +110,7 @@ enum XSD implements CoreDatatype { public static final String NAMESPACE = "http://www.w3.org/2001/XMLSchema#"; private static IRI iri(String localName) { - return new CoreDatatypeHelper.DatatypeIRI(NAMESPACE, localName); + return new InternedIRI(NAMESPACE, localName); } private final IRI iri; @@ -275,7 +275,7 @@ enum RDF implements CoreDatatype { public static final String NAMESPACE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; private static IRI iri(String localName) { - return new CoreDatatypeHelper.DatatypeIRI(NAMESPACE, localName); + return new InternedIRI(NAMESPACE, localName); } private final IRI iri; @@ -316,7 +316,7 @@ enum GEO implements CoreDatatype { public static final String NAMESPACE = "http://www.opengis.net/ont/geosparql#"; private static IRI iri(String localName) { - return new CoreDatatypeHelper.DatatypeIRI(NAMESPACE, localName); + return new InternedIRI(NAMESPACE, localName); } private final IRI iri; diff --git a/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/CoreDatatypeHelper.java b/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/CoreDatatypeHelper.java index 100e61264e2..6455f6707af 100644 --- a/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/CoreDatatypeHelper.java +++ b/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/CoreDatatypeHelper.java @@ -43,38 +43,4 @@ static Map getReverseLookup() { return reverseLookup; } - static class DatatypeIRI extends AbstractIRI { - - private static final long serialVersionUID = 169243624049169159L; - - private final String namespace; - private final String localName; - private final String stringValue; - - public DatatypeIRI(String namespace, String localName) { - this.namespace = namespace; - this.localName = localName; - this.stringValue = namespace.concat(localName).intern(); - } - - @Override - public String stringValue() { - return stringValue; - } - - @Override - public String getNamespace() { - return namespace; - } - - @Override - public String getLocalName() { - return localName; - } - - @Override - public String toString() { - return stringValue; - } - } } diff --git a/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/InternedIRI.java b/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/InternedIRI.java new file mode 100644 index 00000000000..f75d2ad101a --- /dev/null +++ b/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/InternedIRI.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * Copyright (c) 2022 Eclipse RDF4J contributors. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + ******************************************************************************/ + +package org.eclipse.rdf4j.model.base; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; + +import org.eclipse.rdf4j.common.annotation.InternalUseOnly; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Value; + +/** + * An IRI implementation that interns the stringValue so that two objects can be compared by their stringValue + * reference. Must only be used for IRIs that are effectively ´public static final´ and only for a very limited number + * of objects because string interning affects the GC root set + * (https://shipilev.net/jvm/anatomy-quarks/10-string-intern/). + * + */ +@InternalUseOnly +public final class InternedIRI implements IRI { + private static final long serialVersionUID = 169243429049169159L; + + private final String namespace; + private final String localName; + private final String stringValue; + private final int hashCode; + + public InternedIRI(String namespace, String localName) { + this.namespace = namespace; + this.localName = localName; + this.stringValue = namespace.concat(localName).intern(); + this.hashCode = stringValue.hashCode(); + } + + @Override + public String stringValue() { + return stringValue; + } + + @Override + public String getNamespace() { + return namespace; + } + + @Override + public String getLocalName() { + return localName; + } + + @Override + public String toString() { + return stringValue; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o instanceof InternedIRI) { + // Because the stringValue is interned we can simply compare the reference. + return stringValue == ((InternedIRI) o).stringValue; + } + + if (o instanceof Value) { + Value value = (Value) o; + if (value.isIRI()) { + return stringValue.equals(value.stringValue()); + } + } + + return false; + } + + @Override + public int hashCode() { + return hashCode; + } + + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeInt(hashCode); + out.writeUTF(namespace); + out.writeUTF(localName); + } + + private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException { + int hashCode = in.readInt(); + String namespace = in.readUTF(); + String localName = in.readUTF(); + + // Deserialization in Java typically uses Unsafe to set final fields, we need to use reflection. + writeToPrivateFinalField(namespace, "namespace"); + writeToPrivateFinalField(localName, "localName"); + writeToPrivateFinalField(hashCode, "hashCode"); + + // The main reason we need a custom deserialization is that we need to intern the stringValue field. + String stringValue = (namespace + localName).intern(); + writeToPrivateFinalField(stringValue, "stringValue"); + assert stringValue.hashCode() == hashCode; + } + + private void writeToPrivateFinalField(String value, String fieldName) { + try { + Field field = InternedIRI.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(this, value); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private void writeToPrivateFinalField(int value, String fieldName) { + try { + Field field = InternedIRI.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(this, value); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/Vocabularies.java b/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/Vocabularies.java index b4469c1d25b..1e21780c6ec 100644 --- a/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/Vocabularies.java +++ b/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/Vocabularies.java @@ -8,67 +8,57 @@ package org.eclipse.rdf4j.model.vocabulary; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; + import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.Namespace; -import org.eclipse.rdf4j.model.base.AbstractIRI; +import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.base.AbstractNamespace; +import org.eclipse.rdf4j.model.base.InternedIRI; /** * Utility methods related to RDF vocabularies. * * @author Alessandro Bollini - * @since 3.5.0 - * * @implNote To be eventually removed or merged with {@code org.eclipse.rdf4j.model.util.Vocabularies}. + * @since 3.5.0 */ class Vocabularies { - static Namespace createNamespace(String prefix, String namespace) { - return new AbstractNamespace() { - - private static final long serialVersionUID = 3913851292983866831L; - - @Override - public String getPrefix() { - return prefix; - } - - @Override - public String getName() { - return namespace; - } - - }; + private Vocabularies() { } - static IRI createIRI(String namespace, String localName) { - return new AbstractIRI() { - - private static final long serialVersionUID = 1692436252019169159L; + static Namespace createNamespace(String prefix, String namespace) { + return new VocabularyNamespace(prefix, namespace); + } - // ;( removing .toString() causes a 2x penalty in .equals() performance on Oracle JDK 1.8/11… + private static class VocabularyNamespace extends AbstractNamespace { - private final String stringValue = (namespace + localName).toString(); + private final String prefix; + private final String namespace; - @Override - public String stringValue() { - return stringValue; - } + public VocabularyNamespace(String prefix, String namespace) { - @Override - public String getNamespace() { - return namespace; - } + this.prefix = prefix; + this.namespace = namespace; + } - @Override - public String getLocalName() { - return localName; - } + @Override + public String getPrefix() { + return prefix; + } - }; + @Override + public String getName() { + return namespace; + } } - private Vocabularies() { + static IRI createIRI(String namespace, String localName) { + return new InternedIRI(namespace, localName); } }