From 73200d0414d53c42904a11e5384b71e75b54e2ae Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Sun, 31 Jan 2021 17:44:31 -0800 Subject: [PATCH] Improved generic types and checker framework conformance (#337) Additional wildcards are used throughput the APIs to more flexibly accept parameters. For example this allows a wider range of method references to be used as load functions. The generic types now match the Checker Framework's rules [1]. This should improve usage of the cache apis in projects that run the checker. This project does not and its implementation classes are not compliant for the nullness checker. [1] https://checkerframework.org/manual/#generics-instantiation --- build.gradle | 1 - .../benmanes/caffeine/cache/BasicCache.java | 3 +- .../benmanes/caffeine/cache/AsyncCache.java | 14 +++--- .../caffeine/cache/AsyncCacheLoader.java | 27 +++++++----- .../caffeine/cache/AsyncLoadingCache.java | 5 ++- .../caffeine/cache/BoundedLocalCache.java | 4 +- .../github/benmanes/caffeine/cache/Cache.java | 7 +-- .../benmanes/caffeine/cache/CacheLoader.java | 26 ++++++----- .../benmanes/caffeine/cache/CacheWriter.java | 3 +- .../benmanes/caffeine/cache/Caffeine.java | 7 +-- .../benmanes/caffeine/cache/CaffeineSpec.java | 8 ++-- .../benmanes/caffeine/cache/Expiry.java | 3 +- .../benmanes/caffeine/cache/LoadingCache.java | 4 +- .../caffeine/cache/LocalAsyncCache.java | 44 ++++++++++++------- .../cache/LocalAsyncLoadingCache.java | 9 ++-- .../caffeine/cache/LocalLoadingCache.java | 4 +- .../caffeine/cache/LocalManualCache.java | 6 +-- .../benmanes/caffeine/cache/Policy.java | 3 +- .../benmanes/caffeine/cache/Weigher.java | 3 +- .../caffeine/cache/stats/CacheStats.java | 42 +++++++----------- .../caffeine/cache/testing/CacheSpec.java | 9 ++-- .../cache/testing/GuavaCacheFromContext.java | 10 +++-- checksum.xml | 8 +++- gradle/codeQuality.gradle | 15 ++++++- gradle/dependencies.gradle | 5 ++- simulator/build.gradle | 4 -- 26 files changed, 159 insertions(+), 115 deletions(-) diff --git a/build.gradle b/build.gradle index c16fb0c9db..bfea95c95c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,5 @@ import net.ltgt.gradle.errorprone.CheckSeverity -apply plugin: 'com.github.mjdetullio.gradle.coverity' apply plugin: 'com.github.ben-manes.versions' apply plugin: 'com.github.kt3k.coveralls' apply plugin: 'jacoco' diff --git a/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/BasicCache.java b/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/BasicCache.java index d65f20dcc8..295017c5bf 100644 --- a/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/BasicCache.java +++ b/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/BasicCache.java @@ -15,6 +15,7 @@ */ package com.github.benmanes.caffeine.cache; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -22,7 +23,7 @@ * * @author ben.manes@gmail.com (Ben Manes) */ -public interface BasicCache { +public interface BasicCache { /** Returns the value stored in the cache, or null if not present. */ @Nullable V get(K key); diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncCache.java index ce4b0135de..eddbb92ed1 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncCache.java @@ -23,6 +23,7 @@ import java.util.function.BiFunction; import java.util.function.Function; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -37,7 +38,7 @@ * @param the type of keys maintained by this cache * @param the type of mapped values */ -public interface AsyncCache { +public interface AsyncCache { /** * Returns the future associated with {@code key} in this cache, or {@code null} if there is no @@ -92,8 +93,8 @@ public interface AsyncCache { * @throws RuntimeException or Error if the mappingFunction does when constructing the future, * in which case the mapping is left unestablished */ - CompletableFuture get(K key, - BiFunction> mappingFunction); + CompletableFuture get(K key, BiFunction> mappingFunction); /** * Returns the future of a map of the values associated with {@code keys}, creating or retrieving @@ -119,7 +120,7 @@ CompletableFuture get(K key, * left unestablished */ CompletableFuture> getAll(Iterable keys, - Function, Map> mappingFunction); + Function, ? extends Map> mappingFunction); /** * Returns the future of a map of the values associated with {@code keys}, creating or retrieving @@ -145,7 +146,8 @@ CompletableFuture> getAll(Iterable keys, * left unestablished */ CompletableFuture> getAll(Iterable keys, - BiFunction, Executor, CompletableFuture>> mappingFunction); + BiFunction, ? super Executor, + ? extends CompletableFuture>> mappingFunction); /** * Associates {@code value} with {@code key} in this cache. If the cache previously contained a @@ -159,7 +161,7 @@ CompletableFuture> getAll(Iterable keys, * @param valueFuture value to be associated with the specified key * @throws NullPointerException if the specified key or value is null */ - void put(K key, CompletableFuture valueFuture); + void put(K key, CompletableFuture valueFuture); /** * Returns a view of the entries stored in this cache as a thread-safe map. Modifications made to diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncCacheLoader.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncCacheLoader.java index ae5e09aa47..d7e269dd43 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncCacheLoader.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncCacheLoader.java @@ -24,6 +24,8 @@ import java.util.function.BiFunction; import java.util.function.Function; +import org.checkerframework.checker.nullness.qual.NonNull; + /** * Computes or retrieves values asynchronously, based on a key, for use in populating a * {@link AsyncLoadingCache}. @@ -42,7 +44,7 @@ */ @FunctionalInterface @SuppressWarnings("PMD.SignatureDeclareThrowsException") -public interface AsyncCacheLoader { +public interface AsyncCacheLoader { /** * Asynchronously computes or retrieves the value corresponding to {@code key}. @@ -55,7 +57,7 @@ public interface AsyncCacheLoader { * treated like any other {@code Exception} in all respects except that, when it is * caught, the thread's interrupt status is set */ - CompletableFuture asyncLoad(K key, Executor executor) throws Exception; + CompletableFuture asyncLoad(K key, Executor executor) throws Exception; /** * Asynchronously computes or retrieves the values corresponding to {@code keys}. This method is @@ -79,7 +81,7 @@ public interface AsyncCacheLoader { * treated like any other {@code Exception} in all respects except that, when it is * caught, the thread's interrupt status is set */ - default CompletableFuture> asyncLoadAll( + default CompletableFuture> asyncLoadAll( Set keys, Executor executor) throws Exception { throw new UnsupportedOperationException(); } @@ -102,7 +104,8 @@ default CompletableFuture> asyncLoadAll( * treated like any other {@code Exception} in all respects except that, when it is * caught, the thread's interrupt status is set */ - default CompletableFuture asyncReload(K key, V oldValue, Executor executor) throws Exception { + default CompletableFuture asyncReload( + K key, V oldValue, Executor executor) throws Exception { return asyncLoad(key, executor); } @@ -122,14 +125,15 @@ default CompletableFuture asyncReload(K key, V oldValue, Executor executor) t * @return an asynchronous cache loader that delegates to the supplied {@code mappingFunction} * @throws NullPointerException if the mappingFunction is null */ - static AsyncCacheLoader bulk(Function, Map> mappingFunction) { + static AsyncCacheLoader bulk( + Function, ? extends Map> mappingFunction) { return CacheLoader.bulk(mappingFunction); } /** * Returns an asynchronous cache loader that delegates to the supplied mapping function for - * retrieving the values. Note that {@link #asyncLoad} will discard any additional mappings - * loaded when retrieving the {@code key} prior to returning to the value to the cache. + * retrieving the values. Note that {@link #asyncLoad} will silently discard any additional + * mappings loaded when retrieving the {@code key} prior to returning to the value to the cache. *

* Usage example: *

{@code
@@ -142,8 +146,9 @@ static  AsyncCacheLoader bulk(Function, Map>
    * @return an asynchronous cache loader that delegates to the supplied {@code mappingFunction}
    * @throws NullPointerException if the mappingFunction is null
    */
-  static  AsyncCacheLoader bulk(
-      BiFunction, Executor, CompletableFuture>> mappingFunction) {
+  static  AsyncCacheLoader bulk(
+      BiFunction, ? super Executor,
+      ? extends CompletableFuture>> mappingFunction) {
     requireNonNull(mappingFunction);
     return new AsyncCacheLoader<>() {
       @Override public CompletableFuture asyncLoad(K key, Executor executor) {
@@ -154,7 +159,9 @@ static  AsyncCacheLoader bulk(
           Set keys, Executor executor) {
         requireNonNull(keys);
         requireNonNull(executor);
-        return mappingFunction.apply(keys, executor);
+        @SuppressWarnings("unchecked")
+        var future = (CompletableFuture>) mappingFunction.apply(keys, executor);
+        return future;
       }
     };
   }
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncLoadingCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncLoadingCache.java
index 4e1110e855..da2c56f92b 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncLoadingCache.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/AsyncLoadingCache.java
@@ -18,6 +18,8 @@
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 
+import org.checkerframework.checker.nullness.qual.NonNull;
+
 /**
  * A semi-persistent mapping from keys to values. Values are automatically loaded by the cache
  * asynchronously, and are stored in the cache until either evicted or manually invalidated.
@@ -29,7 +31,8 @@
  * @param  the type of keys maintained by this cache
  * @param  the type of mapped values
  */
-public interface AsyncLoadingCache extends AsyncCache {
+public interface AsyncLoadingCache
+    extends AsyncCache {
 
   /**
    * Returns the future associated with {@code key} in this cache, obtaining that value from
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java
index 3d54ee8477..baa61c4975 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java
@@ -1224,7 +1224,7 @@ void refreshIfNeeded(Node node, long now) {
         && !refreshes().containsKey(keyReference)) {
       long[] startTime = new long[1];
       @SuppressWarnings({"unchecked", "rawtypes"})
-      CompletableFuture[] refreshFuture = new CompletableFuture[1];
+      CompletableFuture[] refreshFuture = new CompletableFuture[1];
       refreshes().computeIfAbsent(keyReference, k -> {
         try {
           startTime[0] = statsTicker().read();
@@ -3976,7 +3976,7 @@ static final class AsyncLoader implements CacheLoader {
         V newValue = (V) loader.asyncReload(key, oldValue, executor);
         return newValue;
       }
-      @Override public CompletableFuture asyncReload(
+      @Override public CompletableFuture asyncReload(
           K key, V oldValue, Executor executor) throws Exception {
         return loader.asyncReload(key, oldValue, executor);
       }
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Cache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Cache.java
index 8e97515ea6..5655b8cddb 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Cache.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Cache.java
@@ -21,6 +21,7 @@
 import java.util.function.Function;
 
 import org.checkerframework.checker.index.qual.NonNegative;
+import org.checkerframework.checker.nullness.qual.NonNull;
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.checkerframework.checker.nullness.qual.PolyNull;
 
@@ -38,7 +39,7 @@
  * @param  the type of keys maintained by this cache
  * @param  the type of mapped values
  */
-public interface Cache {
+public interface Cache {
 
   /**
    * Returns the value associated with the {@code key} in this cache, or {@code null} if there is no
@@ -118,7 +119,7 @@ public interface Cache {
    *         left unestablished
    */
   Map getAll(Iterable keys,
-      Function, Map> mappingFunction);
+      Function, ? extends Map> mappingFunction);
 
   /**
    * Associates the {@code value} with the {@code key} in this cache. If the cache previously
@@ -144,7 +145,7 @@ Map getAll(Iterable keys,
    * @throws NullPointerException if the specified map is null or the specified map contains null
    *         keys or values
    */
-  void putAll(Map map);
+  void putAll(Map map);
 
   /**
    * Discards any cached value for the {@code key}. The behavior of this operation is undefined for
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CacheLoader.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CacheLoader.java
index 984d8002e6..134bbd9e2e 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CacheLoader.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CacheLoader.java
@@ -24,6 +24,7 @@
 import java.util.concurrent.Executor;
 import java.util.function.Function;
 
+import org.checkerframework.checker.nullness.qual.NonNull;
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
@@ -43,7 +44,8 @@
  */
 @FunctionalInterface
 @SuppressWarnings({"PMD.SignatureDeclareThrowsException", "FunctionalInterfaceMethodChanged"})
-public interface CacheLoader extends AsyncCacheLoader {
+public interface CacheLoader
+    extends AsyncCacheLoader {
 
   /**
    * Computes or retrieves the value corresponding to {@code key}.
@@ -83,7 +85,7 @@ public interface CacheLoader extends AsyncCacheLoader {
    *         treated like any other {@code Exception} in all respects except that, when it is
    *         caught, the thread's interrupt status is set
    */
-  default Map loadAll(Set keys) throws Exception {
+  default Map loadAll(Set keys) throws Exception {
     throw new UnsupportedOperationException();
   }
 
@@ -95,7 +97,7 @@ default Map loadAll(Set keys) throws Exception {
    * @return the future value associated with {@code key}
    */
   @Override
-  default CompletableFuture asyncLoad(K key, Executor executor) {
+  default CompletableFuture asyncLoad(K key, Executor executor) {
     requireNonNull(key);
     requireNonNull(executor);
     return CompletableFuture.supplyAsync(() -> {
@@ -128,7 +130,8 @@ default CompletableFuture asyncLoad(K key, Executor executor) {
    *         that key; may not contain null values
    */
   @Override
-  default CompletableFuture> asyncLoadAll(Set keys, Executor executor) {
+  default CompletableFuture> asyncLoadAll(
+      Set keys, Executor executor) {
     requireNonNull(keys);
     requireNonNull(executor);
     return CompletableFuture.supplyAsync(() -> {
@@ -159,8 +162,7 @@ default CompletableFuture> asyncLoadAll(Set keys, Executo
    *         treated like any other {@code Exception} in all respects except that, when it is
    *         caught, the thread's interrupt status is set
    */
-  @Nullable
-  default V reload(K key, V oldValue) throws Exception {
+  default @Nullable V reload(K key, V oldValue) throws Exception {
     return load(key);
   }
 
@@ -179,7 +181,8 @@ default V reload(K key, V oldValue) throws Exception {
    *         {@code null} if the mapping is to be removed
    */
   @Override
-  default CompletableFuture asyncReload(K key, V oldValue, Executor executor) throws Exception {
+  default CompletableFuture asyncReload(
+      K key, V oldValue, Executor executor) throws Exception {
     requireNonNull(key);
     requireNonNull(executor);
     return CompletableFuture.supplyAsync(() -> {
@@ -195,8 +198,8 @@ default CompletableFuture asyncReload(K key, V oldValue, Executor executor) t
 
   /**
    * Returns a cache loader that delegates to the supplied mapping function for retrieving the
-   * values. Note that {@link #load(} will discard any additional mappings loaded when retrieving
-   * the {@code key} prior to returning to the value to the cache.
+   * values. Note that {@link #load} will silently discard any additional mappings loaded when
+   * retrieving the {@code key} prior to returning to the value to the cache.
    * 

* Usage example: *

{@code
@@ -208,13 +211,14 @@ default CompletableFuture asyncReload(K key, V oldValue, Executor executor) t
    * @return a cache loader that delegates to the supplied {@code mappingFunction}
    * @throws NullPointerException if the mappingFunction is null
    */
-  static  CacheLoader bulk(Function, Map> mappingFunction) {
+  static  CacheLoader bulk(
+      Function, ? extends Map> mappingFunction) {
     requireNonNull(mappingFunction);
     return new CacheLoader() {
       @Override public @Nullable V load(K key) {
         return loadAll(Set.of(key)).get(key);
       }
-      @Override public Map loadAll(Set keys) {
+      @Override public Map loadAll(Set keys) {
         return mappingFunction.apply(keys);
       }
     };
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CacheWriter.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CacheWriter.java
index 2a7890bdf3..ea668c403d 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CacheWriter.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CacheWriter.java
@@ -15,6 +15,7 @@
  */
 package com.github.benmanes.caffeine.cache;
 
+import org.checkerframework.checker.nullness.qual.NonNull;
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
@@ -28,7 +29,7 @@
  * @param  the most general type of values this write can write; for example {@code Object} if
  *        any value is acceptable
  */
-public interface CacheWriter {
+public interface CacheWriter {
 
   /***
    * Writes the value corresponding to the {@code key} to the external resource. The cache will
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java
index d9a5070fe9..47ca33ff62 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java
@@ -36,6 +36,7 @@
 import java.util.function.Supplier;
 
 import org.checkerframework.checker.index.qual.NonNegative;
+import org.checkerframework.checker.nullness.qual.NonNull;
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 import com.github.benmanes.caffeine.cache.Async.AsyncExpiry;
@@ -134,7 +135,7 @@
  *     normally {@code Object} unless it is constrained by using a method like {@code
  *     #removalListener}
  */
-public final class Caffeine {
+public final class Caffeine {
   static final Logger logger = System.getLogger(Caffeine.class.getName());
   static final Supplier ENABLED_STATS_COUNTER_SUPPLIER = ConcurrentStatsCounter::new;
 
@@ -1075,8 +1076,8 @@ public  AsyncLoadingCache buildAsync(
     @SuppressWarnings("unchecked")
     Caffeine self = (Caffeine) this;
     return isBounded() || refreshAfterWrite()
-        ? new BoundedLocalCache.BoundedLocalAsyncLoadingCache<>(self, loader)
-        : new UnboundedLocalCache.UnboundedLocalAsyncLoadingCache<>(self, loader);
+        ? new BoundedLocalCache.BoundedLocalAsyncLoadingCache(self, loader)
+        : new UnboundedLocalCache.UnboundedLocalAsyncLoadingCache(self, loader);
   }
 
   void requireNonLoadingCache() {
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CaffeineSpec.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CaffeineSpec.java
index 80e911dc24..df1244fe1a 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CaffeineSpec.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CaffeineSpec.java
@@ -269,6 +269,7 @@ void recordStats(@Nullable String value) {
   }
 
   /** Returns a parsed int value. */
+  @SuppressWarnings("nullness")
   static int parseInt(String key, @Nullable String value) {
     requireArgument((value != null) && !value.isEmpty(), "value of key %s was omitted", key);
     try {
@@ -280,6 +281,7 @@ static int parseInt(String key, @Nullable String value) {
   }
 
   /** Returns a parsed long value. */
+  @SuppressWarnings("nullness")
   static long parseLong(String key, @Nullable String value) {
     requireArgument((value != null) && !value.isEmpty(), "value of key %s was omitted", key);
     try {
@@ -294,7 +296,7 @@ static long parseLong(String key, @Nullable String value) {
   static Duration parseDuration(String key, @Nullable String value) {
     requireArgument((value != null) && !value.isEmpty(), "value of key %s omitted", key);
 
-    @SuppressWarnings("NullAway")
+    @SuppressWarnings({"NullAway", "nullness"})
     boolean isIsoFormat = value.contains("p") || value.contains("P");
     if (isIsoFormat) {
       Duration duration = Duration.parse(value);
@@ -312,7 +314,7 @@ static Duration parseDuration(String key, @Nullable String value) {
   /** Returns a parsed {@link TimeUnit} value. */
   static TimeUnit parseTimeUnit(String key, @Nullable String value) {
     requireArgument((value != null) && !value.isEmpty(), "value of key %s omitted", key);
-    @SuppressWarnings("NullAway")
+    @SuppressWarnings({"NullAway", "nullness"})
     char lastChar = Character.toLowerCase(value.charAt(value.length() - 1));
     switch (lastChar) {
       case 'd':
@@ -330,7 +332,7 @@ static TimeUnit parseTimeUnit(String key, @Nullable String value) {
   }
 
   @Override
-  public boolean equals(Object o) {
+  public boolean equals(@Nullable Object o) {
     if (this == o) {
       return true;
     } else if (!(o instanceof CaffeineSpec)) {
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Expiry.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Expiry.java
index 9d982de2a9..2256eb3f58 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Expiry.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Expiry.java
@@ -16,6 +16,7 @@
 package com.github.benmanes.caffeine.cache;
 
 import org.checkerframework.checker.index.qual.NonNegative;
+import org.checkerframework.checker.nullness.qual.NonNull;
 
 /**
  * Calculates when cache entries expire. A single expiration time is retained so that the lifetime
@@ -23,7 +24,7 @@
  *
  * @author ben.manes@gmail.com (Ben Manes)
  */
-public interface Expiry {
+public interface Expiry {
 
   /**
    * Specifies that the entry should be automatically removed from the cache once the duration has
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LoadingCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LoadingCache.java
index 6eaace5ca9..0229894320 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LoadingCache.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LoadingCache.java
@@ -19,6 +19,7 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 
+import org.checkerframework.checker.nullness.qual.NonNull;
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
@@ -32,7 +33,8 @@
  * @param  the type of keys maintained by this cache
  * @param  the type of mapped values
  */
-public interface LoadingCache extends Cache {
+public interface LoadingCache
+    extends Cache {
 
   /**
    * Returns the value associated with the {@code key} in this cache, obtaining that value from
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncCache.java
index 0e823c80d8..f618a90044 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncCache.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncCache.java
@@ -74,20 +74,22 @@ default CompletableFuture get(K key, Function mapping
   }
 
   @Override
-  default CompletableFuture get(K key,
-      BiFunction> mappingFunction) {
+  default CompletableFuture get(K key, BiFunction> mappingFunction) {
     return get(key, mappingFunction, /* recordStats */ true);
   }
 
   @SuppressWarnings({"FutureReturnValueIgnored", "NullAway"})
   default CompletableFuture get(K key,
-      BiFunction> mappingFunction, boolean recordStats) {
+      BiFunction> mappingFunction, boolean recordStats) {
     long startTime = cache().statsTicker().read();
     @SuppressWarnings({"unchecked", "rawtypes"})
-    CompletableFuture[] result = new CompletableFuture[1];
+    CompletableFuture[] result = new CompletableFuture[1];
     CompletableFuture future = cache().computeIfAbsent(key, k -> {
-      result[0] = mappingFunction.apply(key, cache().executor());
-      return requireNonNull(result[0]);
+      @SuppressWarnings("unchecked")
+      var castedResult = (CompletableFuture) mappingFunction.apply(key, cache().executor());
+      result[0] = castedResult;
+      return requireNonNull(castedResult);
     }, recordStats, /* recordLoad */ false);
     if (result[0] != null) {
       handleCompletion(key, result[0], startTime, /* recordMiss */ false);
@@ -97,7 +99,7 @@ default CompletableFuture get(K key,
 
   @Override
   default CompletableFuture> getAll(Iterable keys,
-      Function, Map> mappingFunction) {
+      Function, ? extends Map> mappingFunction) {
     requireNonNull(mappingFunction);
     return getAll(keys, (keysToLoad, executor) ->
         CompletableFuture.supplyAsync(() -> mappingFunction.apply(keysToLoad), executor));
@@ -106,7 +108,8 @@ default CompletableFuture> getAll(Iterable keys,
   @Override
   @SuppressWarnings("FutureReturnValueIgnored")
   default CompletableFuture> getAll(Iterable keys,
-      BiFunction, Executor, CompletableFuture>> mappingFunction) {
+      BiFunction, ? super Executor,
+          ? extends CompletableFuture>> mappingFunction) {
     requireNonNull(mappingFunction);
     requireNonNull(keys);
 
@@ -168,7 +171,7 @@ default CompletableFuture> composeResult(Map>
 
   @Override
   @SuppressWarnings("FutureReturnValueIgnored")
-  default void put(K key, CompletableFuture valueFuture) {
+  default void put(K key, CompletableFuture valueFuture) {
     if (valueFuture.isCompletedExceptionally()
         || (valueFuture.isDone() && (valueFuture.join() == null))) {
       cache().statsCounter().recordLoadFailure(0L);
@@ -176,12 +179,15 @@ default void put(K key, CompletableFuture valueFuture) {
       return;
     }
     long startTime = cache().statsTicker().read();
-    cache().put(key, valueFuture);
+
+    @SuppressWarnings("unchecked")
+    var castedFuture = (CompletableFuture) valueFuture;
+    cache().put(key, castedFuture);
     handleCompletion(key, valueFuture, startTime, /* recordMiss */ false);
   }
 
   @SuppressWarnings("FutureReturnValueIgnored")
-  default void handleCompletion(K key, CompletableFuture valueFuture,
+  default void handleCompletion(K key, CompletableFuture valueFuture,
       long startTime, boolean recordMiss) {
     AtomicBoolean completed = new AtomicBoolean();
     valueFuture.whenComplete((value, error) -> {
@@ -200,8 +206,11 @@ default void handleCompletion(K key, CompletableFuture valueFuture,
           cache().statsCounter().recordMisses(1);
         }
       } else {
+        @SuppressWarnings("unchecked")
+        var castedFuture = (CompletableFuture) valueFuture;
+
         // update the weight and expiration timestamps
-        cache().replace(key, valueFuture, valueFuture);
+        cache().replace(key, castedFuture, castedFuture);
         cache().statsCounter().recordLoadSuccess(loadTime);
         if (recordMiss) {
           cache().statsCounter().recordMisses(1);
@@ -211,7 +220,8 @@ default void handleCompletion(K key, CompletableFuture valueFuture,
   }
 
   /** A function executed asynchronously after a bulk load completes. */
-  final class AsyncBulkCompleter implements BiConsumer, Throwable> {
+  final class AsyncBulkCompleter
+      implements BiConsumer, Throwable> {
     private final LocalCache> cache;
     private final Map> proxies;
     private final long startTime;
@@ -224,7 +234,7 @@ final class AsyncBulkCompleter implements BiConsumer, Throwable>
     }
 
     @Override
-    public void accept(@Nullable Map result, @Nullable Throwable error) {
+    public void accept(@Nullable Map result, @Nullable Throwable error) {
       long loadTime = cache.statsTicker().read() - startTime;
 
       if (result == null) {
@@ -245,7 +255,7 @@ public void accept(@Nullable Map result, @Nullable Throwable error) {
     }
 
     /** Populates the proxies with the computed result. */
-    private void fillProxies(Map result) {
+    private void fillProxies(Map result) {
       proxies.forEach((key, future) -> {
         V value = result.get(key);
         future.obtrudeValue(value);
@@ -259,7 +269,7 @@ private void fillProxies(Map result) {
     }
 
     /** Adds to the cache any extra entries computed that were not requested. */
-    private void addNewEntries(Map result) {
+    private void addNewEntries(Map result) {
       if (proxies.size() == result.size()) {
         return;
       }
@@ -506,7 +516,7 @@ public V get(K key, Function mappingFunction) {
 
     @Override
     public Map getAll(Iterable keys,
-        Function, Map> mappingFunction) {
+        Function, ? extends Map> mappingFunction) {
       return resolve(asyncCache().getAll(keys, mappingFunction));
     }
 
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java
index ce502d62fe..1fd43054f1 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java
@@ -41,9 +41,10 @@ abstract class LocalAsyncLoadingCache
     implements LocalAsyncCache, AsyncLoadingCache {
   static final Logger logger = System.getLogger(LocalAsyncLoadingCache.class.getName());
 
-  @Nullable
-  final BiFunction, Executor, CompletableFuture>> bulkMappingFunction;
-  final BiFunction> mappingFunction;
+  final @Nullable BiFunction, ? super Executor,
+      ? extends CompletableFuture>> bulkMappingFunction;
+  final BiFunction> mappingFunction;
   final AsyncCacheLoader loader;
 
   @Nullable LoadingCacheView cacheView;
@@ -56,7 +57,7 @@ abstract class LocalAsyncLoadingCache
   }
 
   /** Returns a mapping function that adapts to {@link AsyncCacheLoader#asyncLoad}. */
-  BiFunction> newMappingFunction(
+  BiFunction> newMappingFunction(
       AsyncCacheLoader cacheLoader) {
     return (key, executor) -> {
       try {
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.java
index ecf93497bb..9064273415 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.java
@@ -98,7 +98,7 @@ default CompletableFuture refresh(K key) {
     @SuppressWarnings("unchecked")
     V[] oldValue = (V[]) new Object[1];
     @SuppressWarnings({"unchecked", "rawtypes"})
-    CompletableFuture[] reloading = new CompletableFuture[1];
+    CompletableFuture[] reloading = new CompletableFuture[1];
     Object keyReference = cache().referenceKey(key);
 
     var future = cache().refreshes().compute(keyReference, (k, existing) -> {
@@ -109,7 +109,7 @@ default CompletableFuture refresh(K key) {
       try {
         startTime[0] = cache().statsTicker().read();
         oldValue[0] = cache().getIfPresentQuietly(key, writeTime);
-        CompletableFuture refreshFuture = (oldValue[0] == null)
+        CompletableFuture refreshFuture = (oldValue[0] == null)
             ? cacheLoader().asyncLoad(key, cache().executor())
             : cacheLoader().asyncReload(key, oldValue[0], cache().executor());
         reloading[0] = refreshFuture;
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalManualCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalManualCache.java
index 4e81348bc7..fbf17c49d9 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalManualCache.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalManualCache.java
@@ -69,7 +69,7 @@ default Map getAllPresent(Iterable keys) {
 
   @Override
   default Map getAll(Iterable keys,
-      Function, Map> mappingFunction) {
+      Function, ? extends Map> mappingFunction) {
     requireNonNull(mappingFunction);
 
     Set keysToLoad = new LinkedHashSet<>();
@@ -95,11 +95,11 @@ default Map getAll(Iterable keys,
    * during the load are replaced when the loaded entries are inserted into the cache.
    */
   default void bulkLoad(Set keysToLoad, Map result,
-      Function, Map> mappingFunction) {
+      Function, ? extends Map> mappingFunction) {
     boolean success = false;
     long startTime = cache().statsTicker().read();
     try {
-      Map loaded = mappingFunction.apply(keysToLoad);
+      var loaded = mappingFunction.apply(keysToLoad);
       loaded.forEach((key, value) ->
           cache().put(key, value, /* notifyWriter */ false));
       for (K key : keysToLoad) {
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Policy.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Policy.java
index 8db305e4a9..4ffe09f475 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Policy.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Policy.java
@@ -23,6 +23,7 @@
 import java.util.concurrent.TimeUnit;
 
 import org.checkerframework.checker.index.qual.NonNegative;
+import org.checkerframework.checker.nullness.qual.NonNull;
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
@@ -32,7 +33,7 @@
  *
  * @author ben.manes@gmail.com (Ben Manes)
  */
-public interface Policy {
+public interface Policy {
 
   /**
    * Returns whether the cache statistics are being accumulated.
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Weigher.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Weigher.java
index 3901e92622..fd529967ff 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Weigher.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Weigher.java
@@ -20,6 +20,7 @@
 import java.io.Serializable;
 
 import org.checkerframework.checker.index.qual.NonNegative;
+import org.checkerframework.checker.nullness.qual.NonNull;
 
 /**
  * Calculates the weights of cache entries. The total weight threshold is used to determine when an
@@ -30,7 +31,7 @@
  * @author ben.manes@gmail.com (Ben Manes)
  */
 @FunctionalInterface
-public interface Weigher {
+public interface Weigher {
 
   /**
    * Returns the weight of a cache entry. There is no unit for entry weights; rather they are simply
diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/stats/CacheStats.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/stats/CacheStats.java
index a0a5d937f4..65b0a462d5 100644
--- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/stats/CacheStats.java
+++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/stats/CacheStats.java
@@ -18,6 +18,7 @@
 import java.util.Objects;
 
 import org.checkerframework.checker.index.qual.NonNegative;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 import com.github.benmanes.caffeine.cache.Cache;
 import com.github.benmanes.caffeine.cache.LoadingCache;
@@ -128,8 +129,7 @@ public static CacheStats empty() {
    *
    * @return the {@code hitCount + missCount}
    */
-  @NonNegative
-  public long requestCount() {
+  public @NonNegative long requestCount() {
     return saturatedAdd(hitCount, missCount);
   }
 
@@ -138,8 +138,7 @@ public long requestCount() {
    *
    * @return the number of times {@link Cache} lookup methods have returned a cached value
    */
-  @NonNegative
-  public long hitCount() {
+  public @NonNegative long hitCount() {
     return hitCount;
   }
 
@@ -150,8 +149,7 @@ public long hitCount() {
    *
    * @return the ratio of cache requests which were hits
    */
-  @NonNegative
-  public double hitRate() {
+  public @NonNegative double hitRate() {
     long requestCount = requestCount();
     return (requestCount == 0) ? 1.0 : (double) hitCount / requestCount;
   }
@@ -165,8 +163,7 @@ public double hitRate() {
    * @return the number of times {@link Cache} lookup methods have returned an uncached (newly
    *         loaded) value, or null
    */
-  @NonNegative
-  public long missCount() {
+  public @NonNegative long missCount() {
     return missCount;
   }
 
@@ -181,8 +178,7 @@ public long missCount() {
    *
    * @return the ratio of cache requests which were misses
    */
-  @NonNegative
-  public double missRate() {
+  public @NonNegative double missRate() {
     long requestCount = requestCount();
     return (requestCount == 0) ? 0.0 : (double) missCount / requestCount;
   }
@@ -198,8 +194,7 @@ public double missRate() {
    *
    * @return the {@code loadSuccessCount + loadFailureCount}
    */
-  @NonNegative
-  public long loadCount() {
+  public @NonNegative long loadCount() {
     return saturatedAdd(loadSuccessCount, loadFailureCount);
   }
 
@@ -212,8 +207,7 @@ public long loadCount() {
    *
    * @return the number of times {@link Cache} lookup methods have successfully loaded a new value
    */
-  @NonNegative
-  public long loadSuccessCount() {
+  public @NonNegative long loadSuccessCount() {
     return loadSuccessCount;
   }
 
@@ -226,8 +220,7 @@ public long loadSuccessCount() {
    *
    * @return the number of times {@link Cache} lookup methods failed to load a new value
    */
-  @NonNegative
-  public long loadFailureCount() {
+  public @NonNegative long loadFailureCount() {
     return loadFailureCount;
   }
 
@@ -242,8 +235,7 @@ public long loadFailureCount() {
    *
    * @return the ratio of cache loading attempts which threw exceptions
    */
-  @NonNegative
-  public double loadFailureRate() {
+  public @NonNegative double loadFailureRate() {
     long totalLoadCount = saturatedAdd(loadSuccessCount, loadFailureCount);
     return (totalLoadCount == 0) ? 0.0 : (double) loadFailureCount / totalLoadCount;
   }
@@ -255,8 +247,7 @@ public double loadFailureRate() {
    *
    * @return the total number of nanoseconds the cache has spent loading new values
    */
-  @NonNegative
-  public long totalLoadTime() {
+  public @NonNegative long totalLoadTime() {
     return totalLoadTime;
   }
 
@@ -270,8 +261,7 @@ public long totalLoadTime() {
    *
    * @return the average time spent loading new values
    */
-  @NonNegative
-  public double averageLoadPenalty() {
+  public @NonNegative double averageLoadPenalty() {
     long totalLoadCount = saturatedAdd(loadSuccessCount, loadFailureCount);
     return (totalLoadCount == 0) ? 0.0 : (double) totalLoadTime / totalLoadCount;
   }
@@ -282,8 +272,7 @@ public double averageLoadPenalty() {
    *
    * @return the number of times an entry has been evicted
    */
-  @NonNegative
-  public long evictionCount() {
+  public @NonNegative long evictionCount() {
     return evictionCount;
   }
 
@@ -293,8 +282,7 @@ public long evictionCount() {
    *
    * @return the sum of weights of evicted entities
    */
-  @NonNegative
-  public long evictionWeight() {
+  public @NonNegative long evictionWeight() {
     return evictionWeight;
   }
 
@@ -378,7 +366,7 @@ public int hashCode() {
   }
 
   @Override
-  public boolean equals(Object o) {
+  public boolean equals(@Nullable Object o) {
     if (o == this) {
       return true;
     } else if (!(o instanceof CacheStats)) {
diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java
index ed8e99f96a..ca7e041d41 100644
--- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java
+++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/CacheSpec.java
@@ -508,7 +508,8 @@ enum Loader implements CacheLoader {
       @Override public Integer load(Integer key) {
         throw new UnsupportedOperationException();
       }
-      @Override public Map loadAll(Set keys) throws Exception {
+      @Override public Map loadAll(
+          Set keys) throws Exception {
         Set moreKeys = new LinkedHashSet<>(keys.size() + 10);
         moreKeys.addAll(keys);
         for (int i = 0; i < 10; i++) {
@@ -569,7 +570,8 @@ private static class SeriazableAsyncCacheLoader
       SeriazableAsyncCacheLoader(Loader loader) {
         this.loader = loader;
       }
-      @Override public CompletableFuture asyncLoad(Integer key, Executor executor) {
+      @Override
+      public CompletableFuture asyncLoad(Integer key, Executor executor) {
         return loader.asyncLoad(key, executor);
       }
       private Object readResolve() throws ObjectStreamException {
@@ -586,7 +588,8 @@ private static final class BulkSeriazableAsyncCacheLoader extends SeriazableAsyn
       @Override public CompletableFuture asyncLoad(Integer key, Executor executor) {
         throw new IllegalStateException();
       }
-      @Override public CompletableFuture> asyncLoadAll(
+      @Override
+      public CompletableFuture> asyncLoadAll(
           Set keys, Executor executor) {
         return loader.asyncLoadAll(keys, executor);
       }
diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/GuavaCacheFromContext.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/GuavaCacheFromContext.java
index 951d1cf0d3..fd8e9659d3 100644
--- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/GuavaCacheFromContext.java
+++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/testing/GuavaCacheFromContext.java
@@ -193,7 +193,7 @@ public Map getAllPresent(Iterable keys) {
 
     @Override
     public Map getAll(Iterable keys,
-        Function, Map> mappingFunction) {
+        Function, ? extends Map> mappingFunction) {
       keys.forEach(Objects::requireNonNull);
       requireNonNull(mappingFunction);
 
@@ -205,7 +205,7 @@ public Map getAll(Iterable keys,
 
       long start = ticker.read();
       try {
-        Map loaded = mappingFunction.apply(keysToLoad);
+        var loaded = mappingFunction.apply(keysToLoad);
         loaded.forEach(cache::put);
         long end = ticker.read();
         statsCounter.recordLoadSuccess(end - start);
@@ -586,10 +586,12 @@ static class BulkLoader extends SingleLoader {
     }
 
     @Override
+    @SuppressWarnings("unchecked")
     public Map loadAll(Iterable keys) throws Exception {
+      var keysToLoad = (keys instanceof Set) ? (Set) keys : ImmutableSet.copyOf(keys);
       @SuppressWarnings("unchecked")
-      var keysToLoad = (keys instanceof Set) ? (Set) keys : ImmutableSet.copyOf(keys);
-      return delegate.loadAll(keysToLoad);
+      var loaded = (Map) delegate.loadAll(keysToLoad);
+      return loaded;
     }
   }
 
diff --git a/checksum.xml b/checksum.xml
index b74fec1acf..8b08addf19 100644
--- a/checksum.xml
+++ b/checksum.xml
@@ -222,8 +222,8 @@
   
   
     
-      F8FEFDB21CB04D9C229D3C27DC03BDF141A943AED99D29B1349CAFB10758901AE8A564C0C00FF55F574D1CBCE4BEF9E33B6F6CF92B8ABD31A8D98009870DB800
       6CB83B6D5054C63C13AF5FD39F11065556137EDD423385F5D960A656FAFDDF5A5DDCCD1DDEDDAFDCBD511D0FEF005C58FAC9E3FB0BDF1D469AF24450DFBC2325
+      F8FEFDB21CB04D9C229D3C27DC03BDF141A943AED99D29B1349CAFB10758901AE8A564C0C00FF55F574D1CBCE4BEF9E33B6F6CF92B8ABD31A8D98009870DB800
     
     
       311C3115F9F6651D1711C52D1739E25A70F25456CACB9A2CDDE7627498C30B13D721133CC75B39462AD18812A82472EF1B3B9D64FAB5ABB0377C12BF82043A74
@@ -363,6 +363,9 @@
     
       1A47AAF2442159C1CBD22521F31C74B4C71C4168AF5B22D04B4691FDD286E90F02B2792DEDAD3EEEC12B5034ADA1A9EE751C975B5A169AE0B33EE800A8D96E7F
     
+    
+      4C8808B0607564006379FBEB63BCEFC03A0F5FE83F307E455EE66B0B40AC238D14388CEA3C1D883835AF089238F824037A423124348571085C6D5415AB3981CF
+    
     
       E126B7CCF3E42FD1984A0BEEF1004A7269A337C202E59E04E8E2AF714280D2F2D8D2BA5E6F59481B8DCD34AAF35C966A688D0B48EC7E96F102C274DC0D3B381E
     
@@ -423,6 +426,9 @@
     
       9D311F26017C6849918444C38211BDACA292DE1177BA9819E0658D2D273C0ADD0B0E50F13088C495440B530566F595CE8D1C8DAC4FBEA2E98006FC0741A9FC2D
     
+    
+      A2001C5E2F3D7EB6FFF5DD19E92925114DF28AE0E23357D811E7C82955751220C39AE73BFEB0EA0BC34C3AF95E27A1D39EBB9E7F5F9522F3957D269F72FD920E
+    
     
       BC7BC2A514F8CA104A392ECF8736F4A3D316EE988FA91299D33B0AF46134B38E14E4A5061449D17B2DF7A521643E6C02DFA37CC277ED7CAB7CE83C28C00E9719
     
diff --git a/gradle/codeQuality.gradle b/gradle/codeQuality.gradle
index f8001619d6..83291bb160 100644
--- a/gradle/codeQuality.gradle
+++ b/gradle/codeQuality.gradle
@@ -5,13 +5,14 @@ import static org.gradle.util.GradleVersion.version
 
 apply plugin: 'org.kordamp.gradle.jandex'
 apply plugin: 'org.kordamp.gradle.stats'
+apply plugin: 'org.checkerframework'
 apply plugin: 'com.github.spotbugs'
 apply plugin: 'net.ltgt.errorprone'
 apply plugin: 'net.ltgt.nullaway'
 apply plugin: 'org.sonarqube'
+apply plugin: 'java-library'
 apply plugin: 'checkstyle'
 apply plugin: 'jacoco'
-apply plugin: 'java'
 apply plugin: 'pmd'
 
 configurations {
@@ -25,6 +26,7 @@ repositories {
 }
 
 dependencies {
+  checkerFramework libraries.checkerFramework
   checkstyleConfig gradlePlugins.checkstyle
   errorproneJavac libraries.errorproneJavac
   errorprone libraries.errorproneCore
@@ -56,6 +58,14 @@ jar {
   }
 }
 
+checkerFramework {
+  checkers = [
+    'org.checkerframework.checker.nullness.NullnessChecker',
+  ]
+  extraJavacArgs = [ '-Werror' ]
+  excludeTests = true
+}
+
 checkstyle {
   showViolations = true
   toolVersion = pluginVersions.checkstyle
@@ -107,7 +117,7 @@ sonarqube {
 }
 tasks.sonarqube.dependsOn(jacocoMerge)
 
-tasks.withType(JavaCompile) {
+tasks.withType(JavaCompile).configureEach {
   dependsOn downloadCaffeineLocal
   options.compilerArgs << '-Xlint:all,-auxiliaryclass'
   options.encoding = 'UTF-8'
@@ -138,6 +148,7 @@ tasks.withType(JavaCompile) {
       annotatedPackages.add('com.github.benmanes.caffeine')
     }
   }
+  checkerFramework.skipCheckerFramework = true
 }
 
 tasks.named('compileTestJava').configure {
diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle
index 19fa5fd29f..b10316f97e 100644
--- a/gradle/dependencies.gradle
+++ b/gradle/dependencies.gradle
@@ -84,9 +84,9 @@ ext {
   ]
   pluginVersions = [
     bnd: '5.2.0',
+    checkerFramework: '0.5.15',
     checkstyle: '8.39',
     coveralls: '2.8.4',
-    coverity: '1.0.10',
     errorprone: '1.3.0',
     jacoco: '0.8.6',
     jandex: '0.8.0',
@@ -111,6 +111,7 @@ ext {
     akka: "com.typesafe.akka:akka-actor_2.12:${versions.akka}",
     cache2k: "org.cache2k:cache2k-core:${versions.cache2k}",
     checkerAnnotations: "org.checkerframework:checker-qual:${versions.checkerFramework}",
+    checkerFramework: "org.checkerframework:checker:${versions.checkerFramework}",
     collision: "systems.comodal:collision:${versions.collision}",
     commonsCompress: "org.apache.commons:commons-compress:${versions.commonsCompress}",
     commonsLang3: "org.apache.commons:commons-lang3:${versions.commonsLang3}",
@@ -189,11 +190,11 @@ ext {
   ]
   gradlePlugins = [
     bnd: "biz.aQute.bnd:biz.aQute.bnd.gradle:${pluginVersions.bnd}",
+    checkerFramework: "org.checkerframework:checkerframework-gradle-plugin:${pluginVersions.checkerFramework}",
     checkstyle: dependencies.create("com.puppycrawl.tools:checkstyle:${pluginVersions.checkstyle}") {
       transitive = false
     },
     coveralls: "gradle.plugin.com.github.kt3k.coveralls:coveralls-gradle-plugin:${pluginVersions.coveralls}",
-    coverity: "gradle.plugin.com.github.mjdetullio.gradle:coverity-plugin:${pluginVersions.coverity}",
     errorprone: "net.ltgt.gradle:gradle-errorprone-plugin:${pluginVersions.errorprone}",
     jandex: "org.kordamp.gradle:jandex-gradle-plugin:${pluginVersions.jandex}",
     jmh: [
diff --git a/simulator/build.gradle b/simulator/build.gradle
index 6ee3f0ae99..bf5b78daf0 100644
--- a/simulator/build.gradle
+++ b/simulator/build.gradle
@@ -50,10 +50,6 @@ if (!(gradle.startParameter.taskNames ==~ /.*uploadArchives.*/)) {
   }
 }
 
-coverity {
-  skip = true
-}
-
 sonarqube {
   skipProject = true
 }