Repository: apache/jena (java)

Issue:
Slight performance improvements to org.apache.jena.atlas.lib.cache.CacheSimple
### Change

Slight performance improvements to org.apache.jena.atlas.lib.cache.CacheSimple:
- removed already deprecated `BiConsumer<K, V> dropHandler)`, which is used nowhere
- removed `Objects.requireNonNull()` - checks --> because they throw a NullPointerException anyway
- optimized bit-operations by using a cache size, that is always a power of 2
- added more tests for `CacheSimple`
- fixed a small bug, where putting a key with the same hash as an existing one does not overridde the existing entry, when the values are equal.  

Downside: The fixed cache size must always be a power of 2. If the given size is already a power of two it will be used as fixed size for the cache, otherwise the next larger power of two will be used. (e. g. minimumSize = 10 results in 16 as fixed size for the cache)

In my experimental [branch](https://github.com/arne-bdt/jena/tree/using-string-index-for-serialization), I created a variant "SimpleFast" to benchmark the changes:
```
Benchmark                               (param0_GraphUri)  (param1_Cache)  Mode  Cnt  Score   Error  Units
TestCaches.createAndFillCacheByGet          citations.rdf          Simple  avgt   15  1,805 ± 0,147   s/op
TestCaches.createAndFillCacheByGet          citations.rdf      SimpleFast  avgt   15  1,612 ± 0,083   s/op
TestCaches.createAndFillCacheByPut          citations.rdf          Simple  avgt   15  1,818 ± 0,149   s/op
TestCaches.createAndFillCacheByPut          citations.rdf      SimpleFast  avgt   15  1,724 ± 0,126   s/op
TestCaches.getFromFilledCache               citations.rdf          Simple  avgt   15  1,551 ± 0,055   s/op
TestCaches.getFromFilledCache               citations.rdf      SimpleFast  avgt   15  1,367 ± 0,122   s/op
TestCaches.getIfPresentFromFilledCache      citations.rdf          Simple  avgt   15  0,705 ± 0,084   s/op
TestCaches.getIfPresentFromFilledCache      citations.rdf      SimpleFast  avgt   15  0,641 ± 0,061   s/op
```

Is the downside of the possibly larger cache size a problem or does it outweigh the benefits?

### Are you interested in contributing a pull request for this task?

Yes

Pull request:
GH-2738: Slight performance improvements to CacheSimple
Slight performance improvements to org.apache.jena.atlas.lib.cache.CacheSimple:
- optimized bit-operations by using a cache size, that is always a power of 2
- removed already BiConsumer<K, V> dropHandler), which is used nowhere
- added more tests for CacheSimple
- fixed a small bug, where putting a key with the same hash as an existing one does not overridde the existing entry, when the values are equal.
- added new module jena-benchmarks-shadedJena510 to be able to perform regression tests againt the previous release
- added JHM test org.apache.jena.atlas.lib.cache.TestCaches in jena-benchmarks-jmh
- added more detailed javadoc comments to Cache interface

Benchmark results:
```
Benchmark                                           (param0_GraphUri)  (param1_Cache)  Mode  Cnt  Score   Error  Units
TestCaches.createAndFillCacheByGet      ../testing/BSBM/bsbm-5m.nt.gz          Simple  avgt   15  0,206 ± 0,009   s/op
TestCaches.createAndFillCacheByGet      ../testing/BSBM/bsbm-5m.nt.gz  Jena510.Simple  avgt   15  0,228 ± 0,006   s/op
TestCaches.createAndFillCacheByPut      ../testing/BSBM/bsbm-5m.nt.gz          Simple  avgt   15  0,257 ± 0,009   s/op
TestCaches.createAndFillCacheByPut      ../testing/BSBM/bsbm-5m.nt.gz  Jena510.Simple  avgt   15  0,293 ± 0,013   s/op
TestCaches.getFromFilledCache           ../testing/BSBM/bsbm-5m.nt.gz          Simple  avgt   15  0,277 ± 0,008   s/op
TestCaches.getFromFilledCache           ../testing/BSBM/bsbm-5m.nt.gz  Jena510.Simple  avgt   15  0,290 ± 0,007   s/op
TestCaches.getIfPresentFromFilledCache  ../testing/BSBM/bsbm-5m.nt.gz          Simple  avgt   15  0,226 ± 0,006   s/op
TestCaches.getIfPresentFromFilledCache  ../testing/BSBM/bsbm-5m.nt.gz  Jena510.Simple  avgt   15  0,262 ± 0,009   s/op
```

Downside: The fixed cache size must always be a power of 2. If the given size is already a power of two it will be used as fixed size for the cache, otherwise the next larger power of two will be used. (e. g. minimumSize = 10 results in 16 as fixed size for the cache)

GitHub issue resolved #2738

Pull request Description:



----

 - [x] Tests are included.
 - No Documentation change and updates needed
 - [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-base/src/main/java/org/apache/jena/atlas/lib/Cache.java b/jena-base/src/main/java/org/apache/jena/atlas/lib/Cache.java
index c794616df6..46dd53c63c 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/lib/Cache.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/lib/Cache.java
@@ -26,6 +26,7 @@ import org.apache.jena.atlas.lib.cache.CacheInfo;
 
 /**
  * An abstraction of a cache for basic use.
+ * This cache does not support null as keys or values.
  * <p>
  * For more complex configuration of the
  * cache, use the cache builder of the implementation of choice.
@@ -34,14 +35,23 @@ import org.apache.jena.atlas.lib.cache.CacheInfo;
  */
 public interface Cache<Key, Value>
 {
-    /** Does the cache contain the key? */
+    /**
+     *  Does the cache contain the key?
+     * @param key The key to find. The key must not be null.
+     * @return True, if the cache contains the key, otherwise false.
+     */
     public boolean containsKey(Key key) ;
 
-    /** Get from cache - or return null. */
+    /**
+     * Get from cache - or return null.
+     * @param key The key for which the value is requested. The key must not be null.
+     * @return If the cache contains an entry for the given key, the value is returned, otherwise null.
+     */
     public Value getIfPresent(Key key) ;
 
     /** Get from cache; if not present, call the {@link Callable}
      *  to try to fill the cache. This operation should be atomic.
+     *  The 'key' and 'callcable' must not be null.
      *  @deprecated Use {@link #get(Object, Function)}
      */
     @Deprecated(forRemoval = true)
@@ -55,15 +65,31 @@ public interface Cache<Key, Value>
         });
     }
 
-    /** Get from cache; if not present, call the {@link Function}
-     *  to fill the cache slot. This operation should be atomic.
+    /**
+     * Get from cache; if not present, call the {@link Function}
+     * to fill the cache slot. This operation should be atomic.
+     * @param key The key, for which the value should be returned or calculated. The key must not be null.
+     * @param callable If the cache does not contain the key, the callable is called to calculate a value.
+     *                 If the callable returns null, the key is not associated with hat value,
+     *                 as nulls are not accepted as values.
+     *                 The callable must not be null.
+     * @return Returns either the existing value or the calculated value.
+     *         If callable is called and returns null, then null is returned.
      */
     public Value get(Key key, Function<Key, Value> callable) ;
 
-    /** Insert into the cache */
+    /**
+     * Insert into the cache
+     * @param key The key for the 'thing' to store. The key must not be null.
+     * @param thing If 'thing' is null, it will not be used as value,
+     *              instead any existing entry with the same key will be removed.
+     */
     public void put(Key key, Value thing) ;
 
-    /** Remove from cache - return true if key referenced an entry */
+    /**
+     * Remove from cache - return true if key referenced an entry
+     * @param key The key, which shall be removed along with its value. The key must not be null.
+     */
     public void remove(Key key) ;
 
     /** Iterate over all keys. Iterating over the keys requires the caller be thread-safe. */
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/lib/CacheFactory.java b/jena-base/src/main/java/org/apache/jena/atlas/lib/CacheFactory.java
index 44a0945e06..7ab9f09f60 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/lib/CacheFactory.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/lib/CacheFactory.java
@@ -18,9 +18,9 @@
 
 package org.apache.jena.atlas.lib ;
 
-import java.util.function.BiConsumer;
+import org.apache.jena.atlas.lib.cache.*;
 
-import org.apache.jena.atlas.lib.cache.* ;
+import java.util.function.BiConsumer;
 
 public class CacheFactory {
     /**
@@ -80,7 +80,7 @@ public class CacheFactory {
      * This cache is not thread-safe.
      */
     public static <Key, Value> Cache<Key, Value> createSimpleCache(int size) {
-        return new CacheSimple<>(size, null) ;
+        return new CacheSimple<>(size) ;
     }
 
     /** One slot cache */
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/lib/cache/CacheSimple.java b/jena-base/src/main/java/org/apache/jena/atlas/lib/cache/CacheSimple.java
index b339e16440..30a8059dc2 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/lib/cache/CacheSimple.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/lib/cache/CacheSimple.java
@@ -23,16 +23,14 @@ import static java.util.Arrays.asList;
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.Objects;
-import java.util.concurrent.Callable;
-import java.util.function.BiConsumer;
 import java.util.function.Function;
 
-import org.apache.jena.atlas.AtlasException;
 import org.apache.jena.atlas.iterator.Iter;
 import org.apache.jena.atlas.lib.Cache;
 
 /**
  * A simple fixed size cache that uses the hash code to address a slot.
+ * The size is always a power of two, to be able to use optimized bit-operations.
  * The clash policy is to overwrite.
  * <p>
  * The cache has very low overhead - there is no object creation during lookup or insert.
@@ -42,15 +40,23 @@ import org.apache.jena.atlas.lib.Cache;
 public class CacheSimple<K, V> implements Cache<K, V> {
     private final V[] values;
     private final K[] keys;
-    private final int size;
+    private final int sizeMinusOne;
     private int currentSize = 0;
-    private BiConsumer<K, V> dropHandler = null;
 
-    public CacheSimple(int size) {
-        this(size, null);
-    }
+    /**
+     * Constructs a fixes size cache.
+     * The size is always a power of two, to be able to use optimized bit-operations.
+     * @param miniumSize  If the size is already a power of two it will be used as fixed size for the cache,
+     *                    otherwise the next larger power of two will be used.
+     *                    (e.g. minimumSize = 10 results in 16 as fixed size for the cache)
+     */
+    public CacheSimple(int miniumSize) {
+        var size = Integer.highestOneBit(miniumSize);
+        if (size < miniumSize){
+            size <<= 1;
+        }
+        this.sizeMinusOne = size-1;
 
-    public CacheSimple(int size, BiConsumer<K, V> dropHandler) {
         @SuppressWarnings("unchecked")
         V[] x = (V[])new Object[size];
         values = x;
@@ -58,8 +64,6 @@ public class CacheSimple<K, V> implements Cache<K, V> {
         @SuppressWarnings("unchecked")
         K[] z = (K[])new Object[size];
         keys = z;
-        this.dropHandler = dropHandler;
-        this.size = size;
     }
 
     @Override
@@ -73,129 +77,95 @@ public class CacheSimple<K, V> implements Cache<K, V> {
     @Override
     public boolean containsKey(K key) {
         Objects.requireNonNull(key);
-        return index(key) >= 0 ;
+        return key.equals(keys[calcIndex(key)]);
     }
 
-    // Return key index (>=0): return -(index+1) if the key slot is empty.
-    private final int index(K key) {
-        int x = (key.hashCode() & 0x7fffffff) % size;
-        if ( key.equals(keys[x]) )
-            return x;
-        return -x - 1;
-    }
-
-    // Convert to a slot index.
-    private final int decode(int x) {
-        if ( x >= 0 )
-            return x;
-        return -(x+1);
+    private int calcIndex(K key) {
+        return key.hashCode() & sizeMinusOne;
     }
 
     @Override
     public V getIfPresent(K key) {
         Objects.requireNonNull(key);
-        int x = index(key);
-        if ( x < 0 )
-            return null;
-        return values[x];
+        final int idx = calcIndex(key);
+        if (key.equals(keys[idx])) {
+            return values[idx];
+        }
+        return null;
     }
 
     @Override
     public V get(K key, Function<K, V> function) {
-        return getOrFillNoSync(this, key, function);
-    }
-
-    /**
-     * Implementation of getOrFill based on Cache.get and Cache.put
-     * This function is not thread safe.
-     */
-    public static <K,V> V getOrFillNoSync(Cache<K,V> cache, K key, Function<K,V> function) {
-        V value = cache.getIfPresent(key) ;
-        if ( value == null ) {
-            try { value = function.apply(key) ; }
-            catch (RuntimeException ex) { throw ex; }
-            catch (Exception e) {
-                throw new AtlasException("Exception on cache fill", e) ;
-            }
-            if ( value != null )
-                cache.put(key, value) ;
-        }
-        return value ;
-    }
-
-    /**
-     * Implementation of getOrFill based on Cache.get and Cache.put
-     * This function is not thread safe.
-     */
-    public static <K,V> V getOrFillNoSync(Cache<K,V> cache, K key, Callable<V> callable) {
-        V value = cache.getIfPresent(key) ;
-        if ( value == null ) {
-            try { value = callable.call() ; }
-            catch (RuntimeException ex) { throw ex; }
-            catch (Exception e) {
-                throw new AtlasException("Exception on cache fill", e) ;
+        Objects.requireNonNull(key);
+        Objects.requireNonNull(function);
+        final int idx = calcIndex(key);
+        final boolean isExistingKeyNotNull = keys[idx] != null;
+        if(isExistingKeyNotNull && keys[idx].equals(key)) {
+            return values[idx];
+        } else {
+            final var value = function.apply(key);
+            if(value != null) {
+                values[idx] = value;
+                if(!isExistingKeyNotNull) {
+                    currentSize++;
+                }
+                keys[idx] = key;
             }
-            if ( value != null )
-                cache.put(key, value) ;
+            return value;
         }
-        return value ;
     }
 
 
     @Override
     public void put(K key, V thing) {
-        // thing may be null.
-        int x = index(key);
-        x = decode(x);
-        V old = values[x];
-        // Drop the old K->V
-        if ( old != null ) {
-            if ( old.equals(thing) )
-                // Replace like-with-like.
-                return;
-            if ( dropHandler != null )
-                dropHandler.accept(keys[x], old);
-            currentSize--;
-            //keys[x] = null;
-            //values[x] = null;
+        Objects.requireNonNull(key);
+        final int idx = calcIndex(key);
+        if(thing == null) { //null value causes removal of entry
+            if (keys[idx] != null) {
+                keys[idx] = null;
+                values[idx] = null;
+                currentSize--;
+            }
+            return;
         }
-
-        // Already decremented if we are overwriting a full slot.
-        values[x] = thing;
-        if ( thing == null ) {
-            // put(,null) is a remove.
-            keys[x] = null;
-        } else {
-            currentSize++;
-            keys[x] = key;
+        if(!thing.equals(values[idx])) {
+            values[idx] = thing;
+        }
+        if(!key.equals(keys[idx])) {
+            if(keys[idx] == null) { //add value
+                currentSize++;
+            }
+            keys[idx] = key;
         }
     }
 
     @Override
     public void remove(K key) {
-        put(key, null);
+        Objects.requireNonNull(key);
+        final int idx = calcIndex(key);
+        if (key.equals(keys[idx])) {
+            keys[idx] = null;
+            values[idx] = null;
+            currentSize--;
+        }
     }
 
     @Override
     public long size() {
         return currentSize;
-//        long x = 0;
-//        for ( int i = 0 ; i < size ; i++ ) {
-//            K key = keys[i];
-//            if ( key != null )
-//                x++;
-//        }
-//        return x;
     }
 
     @Override
     public Iterator<K> keys() {
-        Iterator<K> iter = asList(keys).iterator();
-        return Iter.removeNulls(iter);
+        return Iter.iter(asList(keys)).filter(Objects::nonNull);
     }
 
     @Override
     public boolean isEmpty() {
         return currentSize == 0;
     }
+
+    int getAllocatedSize() {
+        return keys.length;
+    }
 }
diff --git a/jena-benchmarks/jena-benchmarks-jmh/pom.xml b/jena-benchmarks/jena-benchmarks-jmh/pom.xml
index df68195caa..39891d5f4a 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/pom.xml
+++ b/jena-benchmarks/jena-benchmarks-jmh/pom.xml
@@ -86,6 +86,13 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.jena</groupId>
+            <artifactId>jena-benchmarks-shadedJena510</artifactId>
+            <version>5.2.0-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.logging.log4j</groupId>
             <artifactId>log4j-slf4j2-impl</artifactId>
diff --git a/jena-benchmarks/jena-benchmarks-shadedJena510/pom.xml b/jena-benchmarks/jena-benchmarks-shadedJena510/pom.xml
new file mode 100644
index 0000000000..e2f8cf9da8
--- /dev/null
+++ b/jena-benchmarks/jena-benchmarks-shadedJena510/pom.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.jena</groupId>
+        <artifactId>jena-benchmarks</artifactId>
+        <version>5.2.0-SNAPSHOT</version>
+    </parent>
+
+    <name>Apache Jena - Benchmarks Shaded Jena 5.1.0</name>
+    <artifactId>jena-benchmarks-shadedJena510</artifactId>
+    <description>Shaded Apache Jena 5.1.0 for regression benchmarks</description>
+    <packaging>jar</packaging>
+
+    <url>https://jena.apache.org/</url>
+
+    <organization>
+        <name>Apache Jena</name>
+        <url>https://jena.apache.org/</url>
+    </organization>
+
+    <licenses>
+        <license>
+            <name>Apache 2.0 License</name>
+            <url>https://www.apache.org/licenses/LICENSE-2.0</url>
+        </license>
+    </licenses>
+
+    <properties>
+        <build.time.xsd>${maven.build.timestamp}</build.time.xsd>
+        <automatic.module.name>org.apache.jena.benchmarks.shadedJena480</automatic.module.name>
+        <maven.test.skip>true</maven.test.skip>
+        <scope>test</scope>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.jena</groupId>
+            <artifactId>jena-base</artifactId>
+            <version>5.1.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jena</groupId>
+            <artifactId>jena-iri</artifactId>
+            <version>5.1.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jena</groupId>
+            <artifactId>jena-core</artifactId>
+            <version>5.1.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jena</groupId>
+            <artifactId>jena-arq</artifactId>
+            <version>5.1.0</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <createDependencyReducedPom>false</createDependencyReducedPom>
+                            <transformers>
+                              <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
+                              <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer" />
+                              <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer">
+                                <addHeader>false</addHeader>
+                              </transformer>
+                            </transformers>
+                            <artifactSet>
+                                <includes>
+                                    <include>*:*</include>
+                                </includes>
+                            </artifactSet>
+                            <relocations>
+                                <relocation>
+                                    <pattern>org.apache.jena</pattern>
+                                    <shadedPattern>org.apache.shadedJena510</shadedPattern>
+                                </relocation>
+                            </relocations>
+                            <filters>
+                                <filter>
+                                    <artifact>*:*</artifact>
+                                    <excludes>
+                                        <exclude>META-INF/*.SF</exclude>
+                                        <exclude>META-INF/*.DSA</exclude>
+                                        <exclude>META-INF/*.RSA</exclude>
+                                        <exclude>META-INF/DEPENDENCIES</exclude>
+                                        <exclude>META-INF/MANIFEST.MF</exclude>
+                                        <exclude>**/module-info.class</exclude>
+                                    </excludes>
+                                </filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/jena-benchmarks/pom.xml b/jena-benchmarks/pom.xml
index b7dacaa83c..81226cd19f 100644
--- a/jena-benchmarks/pom.xml
+++ b/jena-benchmarks/pom.xml
@@ -49,6 +49,7 @@
   <modules>
     <module>jena-benchmarks-jmh</module>
     <module>jena-benchmarks-shadedJena480</module>
+    <module>jena-benchmarks-shadedJena510</module>
   </modules>
 
 </project>
diff --git a/pom.xml b/pom.xml
index 81a386e202..f033bae824 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,7 +60,7 @@
 
     <!-- Logging -->
     <ver.slf4j>2.0.16</ver.slf4j>
-    <ver.log4j2>2.24.0</ver.log4j2>
+    <ver.log4j2>2.24.1</ver.log4j2>
 
     <!-- JSON-LD 1.1 -->
     <ver.titanium-json-ld>1.4.1</ver.titanium-json-ld>
@@ -77,16 +77,16 @@
 
     <ver.caffeine>3.1.8</ver.caffeine>
     <!-- Most of Jena uses other libraries for equivalent functionality. -->
-    <ver.guava>33.3.0-jre</ver.guava>
+    <ver.guava>33.3.1-jre</ver.guava>
 
     <ver.gson>2.11.0</ver.gson>
-    <ver.lucene>9.11.1</ver.lucene>
+    <ver.lucene>9.12.0</ver.lucene>
 
     <ver.commons-io>2.17.0</ver.commons-io>
     <ver.commons-cli>1.7.0</ver.commons-cli>
     <ver.commons-lang3>3.17.0</ver.commons-lang3>
     <ver.commons-rdf>0.5.0</ver.commons-rdf>
-    <ver.commons-csv>1.11.0</ver.commons-csv>
+    <ver.commons-csv>1.12.0</ver.commons-csv>
     <ver.commons-codec>1.17.1</ver.commons-codec>
     <ver.commons-compress>1.27.1</ver.commons-compress>
     <ver.commons-collections>4.4</ver.commons-collections>
@@ -98,10 +98,10 @@
 
     <!-- Testing -->
     <ver.junit4>4.13.2</ver.junit4>
-    <ver.junit5>5.11.0</ver.junit5>
+    <ver.junit5>5.11.1</ver.junit5>
     <ver.junit5-platform>1.10.2</ver.junit5-platform>
  
-    <ver.mockito>5.13.0</ver.mockito>
+    <ver.mockito>5.14.0</ver.mockito>
     <ver.awaitility>4.2.2</ver.awaitility>
     <ver.contract.tests>0.2.0</ver.contract.tests>
 
@@ -125,7 +125,7 @@
     <ver.plugin.war>3.4.0</ver.plugin.war>
     <ver.plugin.shade>3.6.0</ver.plugin.shade>
     <ver.plugin.enforcer>3.5.0</ver.plugin.enforcer>
-    <ver.plugin.cyclonedx>2.8.1</ver.plugin.cyclonedx>
+    <ver.plugin.cyclonedx>2.8.2</ver.plugin.cyclonedx>
     <ver.plugin.spdx>0.7.0</ver.plugin.spdx>
 
     <!--


Test patch:
diff --git a/jena-base/src/test/java/org/apache/jena/atlas/lib/cache/TestCacheSimple.java b/jena-base/src/test/java/org/apache/jena/atlas/lib/cache/TestCacheSimple.java
index 90d7330485..81c0c0f427 100644
--- a/jena-base/src/test/java/org/apache/jena/atlas/lib/cache/TestCacheSimple.java
+++ b/jena-base/src/test/java/org/apache/jena/atlas/lib/cache/TestCacheSimple.java
@@ -36,7 +36,7 @@ public class TestCacheSimple {
      */
     @Test
     public void testFixedSize() {
-        final int maxSize = 5;
+        final int maxSize = 8;
         final int submittedEntries = 10;
         final Cache<Integer, Object> testCache = new CacheSimple<>(maxSize);
         rangeClosed(1, submittedEntries)
@@ -82,6 +82,249 @@ public class TestCacheSimple {
         assertTrue("Equal key, expected to be found", cache.containsKey(key2));
     }
 
+    @Test
+    public void testPutSameHashOverridesValue() {
+        CompoundKey key1 = new CompoundKey(1, 1);
+        CompoundKey key2 = new CompoundKey(1, 2);
+        assertEquals(key1.hashCode(), key2.hashCode());
+        assertNotEquals(key1, key2);
+
+        var value1 = "value1";
+        var value2 = "value2";
+
+        Cache<CompoundKey, String> cache = new CacheSimple<>(10);
+        assertEquals(0, cache.size());
+        cache.put(key1, value1);
+        assertEquals(1, cache.size());
+        assertEquals(value1, cache.getIfPresent(key1));
+        assertNull(cache.getIfPresent(key2));
+
+        //this should override the slot
+        cache.put(key2, value2);
+        assertEquals(1, cache.size());
+        assertNull(cache.getIfPresent(key1));
+        assertEquals(value2, cache.getIfPresent(key2));
+    }
+
+    @Test
+    public void testPutSameHashOverridesSameValue() {
+        CompoundKey key1 = new CompoundKey(1, 1);
+        CompoundKey key2 = new CompoundKey(1, 2);
+        assertEquals(key1.hashCode(), key2.hashCode());
+        assertNotEquals(key1, key2);
+
+        var value1 = "value";
+        var value2 = "value";
+
+        Cache<CompoundKey, String> cache = new CacheSimple<>(10);
+        assertEquals(0, cache.size());
+        cache.put(key1, value1);
+        assertEquals(1, cache.size());
+        assertEquals(value1, cache.getIfPresent(key1));
+        assertNull(cache.getIfPresent(key2));
+
+        //this should override the slot
+        cache.put(key2, value2);
+        assertEquals(1, cache.size());
+        assertNull(cache.getIfPresent(key1));
+        assertEquals(value2, cache.getIfPresent(key2));
+    }
+
+    @Test
+    public void testGetSameHashOverridesValue() {
+        CompoundKey key1 = new CompoundKey(1, 1);
+        CompoundKey key2 = new CompoundKey(1, 2);
+        assertEquals(key1.hashCode(), key2.hashCode());
+        assertNotEquals(key1, key2);
+
+        var value1 = "value1";
+        var value2 = "value2";
+
+        Cache<CompoundKey, String> cache = new CacheSimple<>(10);
+        assertEquals(0, cache.size());
+        cache.get(key1, k -> value1);
+        assertEquals(1, cache.size());
+        assertEquals(value1, cache.getIfPresent(key1));
+        assertNull(cache.getIfPresent(key2));
+
+        //this should override the slot
+        cache.get(key2, k -> value2);
+        assertEquals(1, cache.size());
+        assertNull(cache.getIfPresent(key1));
+        assertEquals(value2, cache.getIfPresent(key2));
+    }
+
+    @Test
+    public void removeKeyByPutingNullValue() {
+        Cache<String, String> cache = new CacheSimple<>(10);
+        assertEquals(0, cache.size());
+
+        {
+            final var key = "key0";
+            final var value = "value0";
+
+            cache.put(key, value);
+
+            assertTrue(cache.containsKey(key));
+            assertEquals(value, cache.getIfPresent(key));
+            assertEquals(1, cache.size());
+
+            //removing entry by writing null value
+            cache.put(key, null);
+
+            assertEquals(0, cache.size());
+            assertFalse(cache.containsKey(key));
+            assertNull(value, cache.getIfPresent(key));
+        }
+    }
+
+    @Test
+    public void testRemove() {
+        Cache<String, String> cache = new CacheSimple<>(10);
+        assertEquals(0, cache.size());
+
+        {
+            final var key = "key0";
+            final var value = "value0";
+
+            //trying to remove non-existing key
+            cache.remove(key);
+            assertEquals(0, cache.size());
+            assertFalse(cache.containsKey(key));
+            assertNull(value, cache.getIfPresent(key));
+
+            cache.put(key, value);
+
+            assertTrue(cache.containsKey(key));
+            assertEquals(value, cache.getIfPresent(key));
+            assertEquals(1, cache.size());
+
+            //removing entry by writing null value
+            cache.remove(key);
+
+            assertEquals(0, cache.size());
+            assertFalse(cache.containsKey(key));
+            assertNull(value, cache.getIfPresent(key));
+        }
+    }
+
+    @Test
+    public void testGet() {
+        Cache<String, String> cache = new CacheSimple<>(10);
+        assertEquals(0, cache.size());
+
+        final var key = "key0";
+        final var value = "value0";
+
+        assertFalse(cache.containsKey(key));
+        assertNull(cache.getIfPresent(key));
+
+        assertEquals(value, cache.get(key, k -> value));
+
+        assertTrue(cache.containsKey(key));
+        assertEquals(value, cache.getIfPresent(key));
+        assertEquals(1, cache.size());
+    }
+
+    @Test
+    public void testGetWithSameValue() {
+        Cache<String, String> cache = new CacheSimple<>(10);
+        assertEquals(0, cache.size());
+
+        final var key = "key1";
+        final var value = "value1";
+
+        assertFalse(cache.containsKey(key));
+        assertNull(cache.getIfPresent(key));
+
+        cache.put(key, value);
+        //get with same value
+        assertEquals(value, cache.get(key, k -> value));
+
+        assertTrue(cache.containsKey(key));
+        assertEquals(value, cache.getIfPresent(key));
+        assertEquals(1, cache.size());
+    }
+
+    @Test
+    public void testGetWithDifferentValue() {
+        Cache<String, String> cache = new CacheSimple<>(10);
+        assertEquals(0, cache.size());
+
+        final var key = "key2";
+        final var value = "value2";
+        final var differentValue = "differentValue2";
+
+        assertFalse(cache.containsKey(key));
+        assertNull(cache.getIfPresent(key));
+
+        cache.put(key, value);
+        //get with different value should not override existing value
+        assertEquals(value, cache.get(key, k -> differentValue));
+
+        assertTrue(cache.containsKey(key));
+        assertEquals(value, cache.getIfPresent(key));
+        assertEquals(1, cache.size());
+    }
+
+    @Test
+    public void testGetExistingWithNullValue() {
+        Cache<String, String> cache = new CacheSimple<>(10);
+        assertEquals(0, cache.size());
+
+        final var key = "key3";
+        final var value = "value3";
+        assertFalse(cache.containsKey(key));
+        assertNull(cache.getIfPresent(key));
+
+        cache.put(key, value);
+
+        assertEquals(1, cache.size());
+        //returning null should not change anyting
+        assertEquals(value, cache.get(key, k -> null));
+        assertEquals(1, cache.size());
+
+        assertTrue(cache.containsKey(key));
+        assertEquals(value, cache.getIfPresent(key));
+    }
+
+    @Test
+    public void testGetNonExistingWithNullValue() {
+        Cache<String, String> cache = new CacheSimple<>(10);
+        assertEquals(0, cache.size());
+
+        final var key = "key4";
+
+        assertFalse(cache.containsKey(key));
+        assertNull(cache.getIfPresent(key));
+        assertEquals(0, cache.size());
+        assertNull(cache.get(key, k -> null));
+        assertEquals(0, cache.size());
+        assertFalse(cache.containsKey(key));
+        assertNull(cache.getIfPresent(key));
+    }
+
+    @Test
+    public void testAllocatedSize() {
+        var cache = new CacheSimple<>(2);
+        assertEquals(2, cache.getAllocatedSize());
+
+        cache = new CacheSimple<>(3);
+        assertEquals(4, cache.getAllocatedSize());
+
+        cache = new CacheSimple<>(4);
+        assertEquals(4, cache.getAllocatedSize());
+
+        cache = new CacheSimple<>(6);
+        assertEquals(8, cache.getAllocatedSize());
+
+        cache = new CacheSimple<>(8);
+        assertEquals(8, cache.getAllocatedSize());
+
+        cache = new CacheSimple<>(10);
+        assertEquals(16, cache.getAllocatedSize());
+    }
+
     // Compound key for tests. 
     private static final class CompoundKey {
         private final int a;
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/atlas/lib/cache/TestCaches.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/atlas/lib/cache/TestCaches.java
new file mode 100644
index 0000000000..08d37f4b54
--- /dev/null
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/atlas/lib/cache/TestCaches.java
@@ -0,0 +1,249 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.atlas.lib.cache;
+
+import org.apache.jena.atlas.lib.Cache;
+import org.apache.jena.atlas.lib.CacheFactory;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.mem.graph.helper.JMHDefaultOptions;
+import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.sparql.graph.GraphFactory;
+import org.junit.Assert;
+import org.junit.Test;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.runner.Runner;
+
+import java.util.Iterator;
+import java.util.function.Function;
+
+@State(Scope.Benchmark)
+public class TestCaches {
+
+    final static int DEFAULT_CACHE_SIZE = 1_024_000;
+
+    @Param({
+            "../testing/cheeses-0.1.ttl",
+            "../testing/pizza.owl.rdf",
+//            "../testing/BSBM/bsbm-5m.nt.gz",
+    })
+    public String param0_GraphUri;
+
+    @Param({
+            "Caffeine",
+            "Simple",
+            "Jena510.Caffeine",
+            "Jena510.Simple"
+    })
+    public String param1_Cache;
+
+    private Graph graph;
+
+    private Cache<String, Node> cache;
+
+    private static int calculateRealCacheSize(int minCacheSize) {
+        // the cache size is a power of 2 --> that is a requirement for the CacheSimpleFast
+        // to start with fair conditions for the caches, we use the same size for all caches
+        var cacheSize = Integer.highestOneBit(minCacheSize);
+        if (cacheSize < minCacheSize) {
+            cacheSize <<= 1;
+        }
+        return cacheSize;
+    }
+
+    private static Cache<String, Node> createCache(String cacheName) {
+        var cacheSize = calculateRealCacheSize(DEFAULT_CACHE_SIZE);
+        switch (cacheName) {
+            case "Caffeine":
+                return CacheFactory.createCache(cacheSize);
+            case "Simple":
+                return CacheFactory.createSimpleCache(cacheSize);
+            case "Jena510.Caffeine":
+                return new CacheFromJena510Wrapped<>(org.apache.shadedJena510.atlas.lib.CacheFactory.createCache(cacheSize));
+            case "Jena510.Simple":
+                return new CacheFromJena510Wrapped<>(org.apache.shadedJena510.atlas.lib.CacheFactory.createSimpleCache(cacheSize));
+            default:
+                throw new IllegalArgumentException("Unknown Cache: " + cacheName);
+        }
+    }
+
+    @Benchmark
+    public int getFromFilledCache() {
+        final int[] hash = {0};
+        graph.find().forEachRemaining(t -> {
+            if(t.getSubject().isURI()) {
+                hash[0] += cache.get(t.getSubject().getURI(),
+                        s -> t.getSubject()).getURI().hashCode();
+
+            }
+            if(t.getPredicate().isURI()) {
+                hash[0] += cache.get(t.getPredicate().getURI(),
+                        s -> t.getPredicate()).getURI().hashCode();
+
+            }
+            if(t.getObject().isURI()) {
+                hash[0] += cache.get(t.getObject().getURI(),
+                        s -> t.getObject()).getURI().hashCode();
+
+            }
+        });
+        return hash[0];
+    }
+
+    @Benchmark
+    public int getIfPresentFromFilledCache() {
+        final int[] hash = {0};
+        graph.find().forEachRemaining(t -> {
+            if(t.getSubject().isURI()) {
+                final var value = cache.getIfPresent(t.getSubject().getURI());
+                if(value != null)
+                    hash[0] += value.getURI().hashCode();
+
+            }
+            if(t.getPredicate().isURI()) {
+                final var value = cache.getIfPresent(t.getPredicate().getURI());
+                if(value != null)
+                    hash[0] += value.getURI().hashCode();
+            }
+            if(t.getObject().isURI()) {
+                final var value = cache.getIfPresent(t.getObject().getURI());
+                if(value != null)
+                    hash[0] += value.getURI().hashCode();
+            }
+        });
+        return hash[0];
+    }
+
+    @Benchmark
+    public Cache<String, Node> createAndFillCacheByGet() {
+        var c = createCache(param1_Cache);
+        fillCacheByGet(c, graph);
+        return c;
+    }
+
+    @Benchmark
+    public Cache<String, Node> createAndFillCacheByPut() {
+        var c = createCache(param1_Cache);
+        fillCacheByPut(c, graph);
+        return c;
+    }
+
+    private static void fillCacheByGet(Cache<String, Node> cacheToFill, Graph g) {
+        g.find().forEachRemaining(t -> {
+            if(t.getSubject().isURI()) {
+                cacheToFill.get(t.getSubject().getURI(), s -> t.getSubject());
+            }
+            if(t.getPredicate().isURI()) {
+                cacheToFill.get(t.getPredicate().getURI(), s -> t.getPredicate());
+            }
+            if(t.getObject().isURI()) {
+                cacheToFill.get(t.getObject().getURI(), s -> t.getObject());
+            }
+        });
+    }
+
+    private static void fillCacheByPut(Cache<String, Node> cacheToFill, Graph g) {
+        g.find().forEachRemaining(t -> {
+            if(t.getSubject().isURI()) {
+                cacheToFill.put(t.getSubject().getURI(), t.getSubject());
+            }
+            if(t.getPredicate().isURI()) {
+                cacheToFill.put(t.getPredicate().getURI(), t.getPredicate());
+            }
+            if(t.getObject().isURI()) {
+                cacheToFill.put(t.getObject().getURI(), t.getObject());
+            }
+        });
+    }
+
+    private static class CacheFromJena510Wrapped<K, V> implements Cache<K, V> {
+
+        private final org.apache.shadedJena510.atlas.lib.Cache<K, V> wrappedCache;
+
+        public CacheFromJena510Wrapped(org.apache.shadedJena510.atlas.lib.Cache<K, V> cacheFromJena510) {
+            this.wrappedCache = cacheFromJena510;
+        }
+
+
+        @Override
+        public boolean containsKey(K k) {
+            return wrappedCache.containsKey(k);
+        }
+
+        @Override
+        public V getIfPresent(K k) {
+            return wrappedCache.getIfPresent(k);
+        }
+
+        @Override
+        public V get(K k, Function<K, V> callable) {
+            return wrappedCache.get(k, callable);
+        }
+
+        @Override
+        public void put(K k, V thing) {
+            wrappedCache.put(k, thing);
+        }
+
+        @Override
+        public void remove(K k) {
+            wrappedCache.remove(k);
+        }
+
+        @Override
+        public Iterator<K> keys() {
+            return wrappedCache.keys();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return wrappedCache.isEmpty();
+        }
+
+        @Override
+        public void clear() {
+            wrappedCache.clear();
+        }
+
+        @Override
+        public long size() {
+            return wrappedCache.size();
+        }
+    }
+
+    @Setup(Level.Trial)
+    public void setupTrial() throws Exception {
+        this.graph = GraphFactory.createGraphMem();
+        RDFDataMgr.read(this.graph, this.param0_GraphUri);
+    }
+
+    @Setup(Level.Iteration)
+    public void setupIteration() {
+        this.cache = createCache(param1_Cache);
+        fillCacheByGet(this.cache, this.graph);
+    }
+
+    @Test
+    public void benchmark() throws Exception {
+        var opt = JMHDefaultOptions.getDefaults(this.getClass())
+                .build();
+        var results = new Runner(opt).run();
+        Assert.assertNotNull(results);
+    }
+}