Guava Library in the Java 25 Ecosystem: A Comprehensive Guide

A comprehensive guide to the Guava library in the Java 25 ecosystem.

🌱 Seedling·created: ·category:Java

Table of Contents

  1. Introduction
  2. Installation
  3. Immutable Collections
  4. New Collection Types
  5. Caching
  6. Concurrency Utilities
  7. String Utilities
  8. I/O Utilities
  9. Preconditions
  10. Functional Idioms: Optional, Predicate, Function
  11. Math Utilities
  12. Hashing
  13. EventBus
  14. Graph API
  15. Ordering: Ordering and ComparisonChain
  16. Guava and Java 25’s New Features
  17. Guava vs. the Java Standard Library
  18. Best Practices and Pitfalls
  19. References

Introduction

Guava is a set of “core libraries” for Java (com.google.guava:guava), maintained by Google and used across the Java ecosystem for decades. It bundles collections, caching, concurrency utilities, string helpers, I/O helpers, hashing, graph structures, and more.

Over time, Java has adopted several Guava-inspired ideas - java.util.Optional, List.of(), Map.of() - but Guava still offers a richer, more mature API surface in many scenarios. With Java 25 (LTS, released September 2025) bringing virtual threads, pattern matching, records, and structured concurrency, Guava continues to complement the JDK rather than compete with it, filling gaps the standard library doesn’t cover.

Current version: As of this writing, the current Guava release is 33.6.0-jre (the JRE flavor; an -android flavor also exists). Guava requires Java 8+, so it is fully compatible with Java 25.

Note: Guava releases frequently. Always check Maven Central for the latest version before pinning a dependency in production.


Installation

Maven

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.6.0-jre</version>
</dependency>

Gradle (Kotlin DSL)

dependencies {
    // If Guava is only used internally:
    implementation("com.google.guava:guava:33.6.0-jre")

    // If Guava types leak into your public API:
    api("com.google.guava:guava:33.6.0-jre")
}

Using Guava with the Java Platform Module System (JPMS) on Java 25

Since 33.4.8, Guava ships as a proper Java module. You can declare it in module-info.java:

module com.example.myapp {
    requires com.google.common;
}

Immutable Collections

Java’s own List.of() and Map.of() were inspired by Guava’s ImmutableList/ImmutableMap family, but Guava still offers richer builder APIs and more flexibility.

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;

// Simple construction
ImmutableList<String> colors = ImmutableList.of("red", "green", "blue");

// Using a builder
ImmutableMap<String, Integer> ages = ImmutableMap.<String, Integer>builder()
        .put("Alice", 30)
        .put("Bob", 25)
        .buildOrThrow(); // preferred over build() in 33.x

// Copying from an existing collection
List<String> source = new ArrayList<>(List.of("a", "b", "c"));
ImmutableSet<String> copy = ImmutableSet.copyOf(source);

// Collecting from a stream
ImmutableList<Integer> squares = Stream.of(1, 2, 3, 4)
        .map(x -> x * x)
        .collect(ImmutableList.toImmutableList());

Why use ImmutableList/Map?

  • Thread-safe by construction - safe to share across threads.
  • Disallows null elements, catching bugs early.
  • More memory-efficient than wrapping with Collections.unmodifiableList(...).
  • A natural fit for sharing constant data across the massively concurrent code Java 25’s virtual threads enable.

New Collection Types

Guava provides several collection types with no JDK equivalent:

Multimap - one key, multiple values

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;

Multimap<String, String> courseStudents = ArrayListMultimap.create();
courseStudents.put("Math", "Alice");
courseStudents.put("Math", "Bob");
courseStudents.put("Physics", "Carol");

Collection<String> mathStudents = courseStudents.get("Math");
// [Alice, Bob]

Multiset - a collection that tracks element counts

import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;

Multiset<String> wordCounts = HashMultiset.create();
for (String word : "apple pear apple banana apple pear".split(" ")) {
    wordCounts.add(word);
}
System.out.println(wordCounts.count("apple")); // 3

BiMap - bidirectional mapping

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;

BiMap<String, Integer> nameToRank = HashBiMap.create();
nameToRank.put("Alice", 1);
nameToRank.put("Bob", 2);

Integer rank = nameToRank.get("Alice");           // 1
String name = nameToRank.inverse().get(2);        // Bob

Table - a two-dimensional key-value structure

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;

Table<String, String, Double> grades = HashBasedTable.create();
grades.put("Alice", "Math", 85.0);
grades.put("Alice", "Physics", 90.0);
grades.put("Bob", "Math", 70.0);

Double aliceMath = grades.get("Alice", "Math"); // 85.0
Map<String, Double> aliceAllGrades = grades.row("Alice");

RangeSet / RangeMap - range-based data structures

import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;

RangeSet<Integer> ranges = TreeRangeSet.create();
ranges.add(Range.closed(1, 5));
ranges.add(Range.closed(10, 15));
ranges.add(Range.closed(4, 11)); // merges into [1, 15]

boolean contains = ranges.contains(7); // true

Caching

Guava’s LoadingCache was the go-to in-memory caching solution in Java long before Caffeine became popular, and it’s still actively used in many old and new codebases alike.

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.util.concurrent.TimeUnit;

LoadingCache<String, ExpensiveObject> cache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .expireAfterAccess(5, TimeUnit.MINUTES)
        .recordStats()
        .build(new CacheLoader<String, ExpensiveObject>() {
            @Override
            public ExpensiveObject load(String key) {
                return computeExpensiveValue(key);
            }
        });

ExpensiveObject value = cache.getUnchecked("key1");
System.out.println(cache.stats()); // hit rate, miss count, etc.

Relevance to Java 25: In highly concurrent I/O workloads built on virtual threads, some of LoadingCache’s internal synchronized-based mechanisms (especially timestamp bookkeeping for expireAfterAccess) can trigger thread pinning. For projects that lean heavily on virtual threads, migrating to Caffeine is worth evaluating; for moderate, mostly-synchronous workloads, Guava Cache remains perfectly performant.


Concurrency Utilities

ListenableFuture and Futures

import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;

import java.util.concurrent.Executors;

ListeningExecutorService pool =
        MoreExecutors.listeningDecorator(Executors.newVirtualThreadPerTaskExecutor());

ListenableFuture<String> future = pool.submit(() -> {
    Thread.sleep(100);
    return "result";
});

Futures.addCallback(future, new FutureCallback<String>() {
    @Override
    public void onSuccess(String result) {
        System.out.println("Success: " + result);
    }

    @Override
    public void onFailure(Throwable t) {
        System.err.println("Failure: " + t.getMessage());
    }
}, pool);

Note: MoreExecutors.listeningDecorator(...) composes cleanly with Executors.newVirtualThreadPerTaskExecutor() on Java 25. Even though ListenableFuture predates CompletableFuture, its callback-based chaining API is still a reasonable choice in codebases already built around it.

RateLimiter - throttling requests

import com.google.common.util.concurrent.RateLimiter;

RateLimiter limiter = RateLimiter.create(5.0); // 5 requests per second

for (int i = 0; i < 10; i++) {
    limiter.acquire(); // blocks as needed
    callApi();
}

Striped - per-key locking

import com.google.common.util.concurrent.Striped;
import java.util.concurrent.locks.Lock;

Striped<Lock> locks = Striped.lock(16);

Lock lock = locks.get(userId);
lock.lock();
try {
    updateUserBalance(userId);
} finally {
    lock.unlock();
}

Monitor - a modern alternative to synchronized

import com.google.common.util.concurrent.Monitor;

Monitor monitor = new Monitor();

monitor.enter();
try {
    // critical section
} finally {
    monitor.leave();
}

String Utilities

Joiner and Splitter

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;

String joined = Joiner.on(", ").skipNulls().join("a", null, "b", "c");
// "a, b, c"

List<String> parts = Splitter.on(",")
        .trimResults()
        .omitEmptyStrings()
        .splitToList(" apple, , pear ,banana");
// [apple, pear, banana]

CharMatcher

import com.google.common.base.CharMatcher;

String trimmed = CharMatcher.whitespace().trimFrom("   hello world   ");
String digitsOnly = CharMatcher.inRange('0', '9').retainFrom("Product-Code-4521-AX");
// "4521"

The Strings class

import com.google.common.base.Strings;

String safe = Strings.nullToEmpty(null); // ""
boolean isEmpty = Strings.isNullOrEmpty(""); // true
String padded = Strings.padStart("42", 5, '0'); // "00042"

I/O Utilities

import com.google.common.io.Files;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;

import java.io.File;
import java.nio.charset.StandardCharsets;

// Reading a file's content
String content = Files.asCharSource(new File("data.txt"), StandardCharsets.UTF_8).read();

// Writing to a file
Files.asCharSink(new File("output.txt"), StandardCharsets.UTF_8).write("hello world");

// Traversing a directory
Files.fileTraverser().depthFirstPreOrder(new File("./project"))
        .forEach(file -> System.out.println(file.getName()));

Note: On Java 25, java.nio.file.Files (with Files.readString, Files.writeString, etc.) covers most everyday needs. Guava’s I/O package still earns its place for more complex, composable data-flow scenarios via its ByteSource/CharSource abstractions.


Preconditions

import static com.google.common.base.Preconditions.*;

public void createUser(String name, int age) {
    checkNotNull(name, "Name must not be null");
    checkArgument(age >= 0, "Age cannot be negative: %s", age);
    checkState(isDatabaseConnected(), "No database connection");
    // ...
}

When combined with Java 25’s records and pattern matching, Preconditions remains a handy way to validate invariants inside a record’s compact constructor:

public record User(String name, int age) {
    public User {
        checkArgument(age >= 0, "Age cannot be negative: %s", age);
        checkNotNull(name);
    }
}

Functional Idioms

Guava’s Optional vs. java.util.Optional

Before Java 8, Guava shipped its own com.google.common.base.Optional. In modern code, java.util.Optional should be preferred; Guava’s Optional is still around for interop with legacy APIs, and conversion between the two is straightforward:

import com.google.common.base.Optional;

Optional<String> guavaOpt = Optional.of("value");
java.util.Optional<String> javaOpt = guavaOpt.toJavaUtil();

Predicates and functional idioms

import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;

Predicate<String> isLong = s -> s.length() > 5;
Collection<String> longOnes = Collections2.filter(List.of("short", "quite a long word"), isLong);

Note: java.util.function.Predicate and the Stream API should be your first choice for everyday filtering and transformation in Java 25 code. Guava’s functional helpers still matter when integrating with legacy Guava-based APIs (e.g., Multimaps.filterValues).


Math Utilities

import com.google.common.math.IntMath;
import com.google.common.math.LongMath;
import com.google.common.math.DoubleMath;

int gcd = IntMath.gcd(48, 18);                 // 6
boolean isPrime = IntMath.isPrime(17);         // true
int ceilDiv = IntMath.divide(7, 2, java.math.RoundingMode.CEILING); // 4

long factorial = LongMath.factorial(10);
boolean approxEqual = DoubleMath.fuzzyEquals(0.1 + 0.2, 0.3, 1e-9); // true

Hashing

import com.google.common.hash.Hashing;
import com.google.common.hash.HashFunction;
import com.google.common.hash.HashCode;

HashFunction hf = Hashing.sha256();
HashCode hash = hf.hashString("hello world", StandardCharsets.UTF_8);
System.out.println(hash.toString());

// Bloom Filter
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

BloomFilter<String> filter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), 1_000_000, 0.01);
filter.put("user123");
boolean mightContain = filter.mightContain("user123"); // true (false positives possible, false negatives not)

EventBus

Guava’s EventBus offers a simple in-process publish/subscribe model:

import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;

class OrderListener {
    @Subscribe
    public void onOrderCreated(OrderEvent event) {
        System.out.println("New order: " + event.getOrderId());
    }
}

EventBus bus = new EventBus();
bus.register(new OrderListener());
bus.post(new OrderEvent("ORD-1001"));

Note: New projects may prefer reactive streams or Java 25’s structured concurrency (StructuredTaskScope) for coordinating tasks, but for simple, single-JVM event handling, EventBus remains a lightweight, convenient option.


Graph API

The com.google.common.graph package models directed/undirected graph structures:

import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;

MutableGraph<String> cityGraph = GraphBuilder.undirected().build();
cityGraph.putEdge("Istanbul", "Ankara");
cityGraph.putEdge("Ankara", "Izmir");

Set<String> neighbors = cityGraph.adjacentNodes("Ankara");
// [Istanbul, Izmir]

Ordering: Ordering and ComparisonChain

import com.google.common.collect.Ordering;
import com.google.common.collect.ComparisonChain;

Ordering<String> byLength = Ordering.natural().onResultOf(String::length);
List<String> sorted = byLength.sortedCopy(List.of("long phrase", "sm", "medium"));

public int compareTo(User other) {
    return ComparisonChain.start()
            .compare(this.lastName, other.lastName)
            .compare(this.firstName, other.firstName)
            .compare(this.age, other.age)
            .result();
}

Guava and Java 25’s New Features

Java 25 (LTS) finalized several previously-preview features and introduced new ones. Here’s how they interact with Guava:

Java 25 featureRelationship with Guava
Virtual ThreadsMoreExecutors.listeningDecorator() composes with Executors.newVirtualThreadPerTaskExecutor(). Legacy Guava APIs relying on synchronized blocks (e.g., Striped locks) can risk thread pinning under heavy virtual-thread load; consider ReentrantLock-based alternatives on hot paths.
RecordsPreconditions pairs well with compact constructors; ImmutableMap/ImmutableList reinforce immutability for record fields.
Pattern Matching (switch expressions)Combines cleanly with Guava types like Range and Optional for readable switch blocks.
Structured ConcurrencyStructuredTaskScope is a more modern alternative to Guava’s callback-based ListenableFuture model; new code may prefer structured concurrency while legacy code continues using ListenableFuture.
Module System (JPMS)Since 33.4.8, Guava ships as a real Java module, enabling clean integration via module-info.java.

Guava vs. the Java Standard Library

NeedRecommendation
Simple immutable list/map/setJava’s List.of()/Map.of() for simple cases, or Guava’s ImmutableList when you need a builder or richer API
Multimap, Multiset, BiMap, TableGuava (no JDK equivalent)
In-memory cachingGuava’s LoadingCache, or Caffeine for high-performance/virtual-thread-heavy workloads
Optionaljava.util.Optional for new code
String joining/splittingGuava’s Joiner/Splitter for richer options, or String.join/split for simple cases
Asynchronous programmingJava’s CompletableFuture / virtual threads for new code, or Guava’s ListenableFuture for existing codebases and callback chaining
Hashing / Bloom FilterGuava (no Bloom Filter in the JDK)
Graph data structuresGuava’s common.graph

Best Practices and Pitfalls

  1. Avoid @Beta APIs: Classes marked @Beta in Guava can change or be removed at any time. If you’re building a library, use the Guava Beta Checker.
  2. Prefer buildOrThrow(): Use ImmutableMap.Builder.buildOrThrow() instead of build() for clearer error messages on duplicate keys.
  3. Watch the module transition: Guava 33.4.5–33.4.7 had known modularization issues; use 33.4.8 or later directly.
  4. Synchronization with virtual threads: Be careful with legacy Guava APIs that use synchronized blocks in code paths heavily exercised by virtual threads (thread-pinning risk).
  5. Avoid unnecessary dependency bloat: Don’t pull in all of Guava for a couple of helper methods - evaluate whether the modern JDK already covers your need.
  6. Nullness annotations: Newer Guava releases are migrating to JSpecify null-safety annotations; if you use Kotlin, pay attention to resulting compiler warnings.

References


This document is based on Guava 33.6.0-jre and Java 25 (LTS). Since library versions change frequently, always check the latest release notes before shipping to production.

This note is part of the Digital Garden — a collection of connected, evolving thoughts.