Guava Library in the Java 25 Ecosystem: A Comprehensive Guide
A comprehensive guide to the Guava library in the Java 25 ecosystem.
Table of Contents
- Introduction
- Installation
- Immutable Collections
- New Collection Types
- Caching
- Concurrency Utilities
- String Utilities
- I/O Utilities
- Preconditions
- Functional Idioms: Optional, Predicate, Function
- Math Utilities
- Hashing
- EventBus
- Graph API
- Ordering: Ordering and ComparisonChain
- Guava and Java 25’s New Features
- Guava vs. the Java Standard Library
- Best Practices and Pitfalls
- 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
nullelements, 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 withExecutors.newVirtualThreadPerTaskExecutor()on Java 25. Even thoughListenableFuturepredatesCompletableFuture, 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(withFiles.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 itsByteSource/CharSourceabstractions.
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.Predicateand 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,EventBusremains 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 feature | Relationship with Guava |
|---|---|
| Virtual Threads | MoreExecutors.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. |
| Records | Preconditions 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 Concurrency | StructuredTaskScope 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
| Need | Recommendation |
|---|---|
| Simple immutable list/map/set | Java’s List.of()/Map.of() for simple cases, or Guava’s ImmutableList when you need a builder or richer API |
| Multimap, Multiset, BiMap, Table | Guava (no JDK equivalent) |
| In-memory caching | Guava’s LoadingCache, or Caffeine for high-performance/virtual-thread-heavy workloads |
| Optional | java.util.Optional for new code |
| String joining/splitting | Guava’s Joiner/Splitter for richer options, or String.join/split for simple cases |
| Asynchronous programming | Java’s CompletableFuture / virtual threads for new code, or Guava’s ListenableFuture for existing codebases and callback chaining |
| Hashing / Bloom Filter | Guava (no Bloom Filter in the JDK) |
| Graph data structures | Guava’s common.graph |
Best Practices and Pitfalls
- Avoid
@BetaAPIs: Classes marked@Betain Guava can change or be removed at any time. If you’re building a library, use the Guava Beta Checker. - Prefer
buildOrThrow(): UseImmutableMap.Builder.buildOrThrow()instead ofbuild()for clearer error messages on duplicate keys. - Watch the module transition: Guava 33.4.5–33.4.7 had known modularization issues; use 33.4.8 or later directly.
- Synchronization with virtual threads: Be careful with legacy Guava APIs that use
synchronizedblocks in code paths heavily exercised by virtual threads (thread-pinning risk). - 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.
- Nullness annotations: Newer Guava releases are migrating to JSpecify null-safety annotations; if you use Kotlin, pay attention to resulting compiler warnings.
References
- Guava’s official GitHub repository: https://github.com/google/guava
- Guava user guide (wiki): https://github.com/google/guava/wiki
- Latest version on Maven Central: https://mvnrepository.com/artifact/com.google.guava/guava
- Guava Javadoc: https://guava.dev/
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.