Post

Guava в 2025 году: устаревшие, поглощенные и актуальные подсистемы

Guava в 2025 году: устаревшие, поглощенные и актуальные подсистемы

Обзор библиотеки Guava: как набор разнородных подсистем с различной эволюционной судьбой и областью применимости в современных Java-проектах.

При анализе Java-проектов с длительной историей развития довольно часто можно встретить зависимость от Guava. При этом она появляется не только в legacy-коде, но и в относительно новых модулях, где используется вполне осознанно.

Это делает Guava удобным примером библиотеки, вокруг которой полезно задать несколько практических вопросов:

  • какие проблемы она решала изначально;
  • какие ее возможности остаются актуальными сегодня;
  • где ее применение все еще оправдано;
  • и в каких случаях от нее разумнее отказаться.

Далее — без апологетики и без категоричных выводов, с позиции практического использования и архитектурной целесообразности.

Исторический контекст

Guava появилась в эпоху Java 6–7 как набор core-утилит, призванных компенсировать ограничения стандартной библиотеки того времени. В частности, она предлагала:

  • immutable-коллекции;
  • более выразительные абстракции над коллекциями;
  • удобный механизм in-memory-кэширования;
  • средства для fail-fast-проверок и контрактов;
  • вспомогательные средства для concurrency и I/O.

С выходом Java 8 и последующих версий значительная часть этих пробелов была закрыта на уровне JDK. Тем не менее Guava не исчезла полностью из практики — и это требует отдельного объяснения.

На практике Guava стоит рассматривать не как единую библиотеку, а как набор относительно независимых подсистем. С течением времени их судьба сложилась по-разному:

  • порядка 30–40% возможностей Guava сегодня можно считать устаревшими;
  • еще около 30% были в том или ином виде поглощены стандартной библиотекой Java;
  • и лишь 20–30% остаются действительно востребованными в современных Java-проектах.

Именно эта неоднородность делает Guava показательной библиотекой с точки зрения эволюции Java-экосистемы и инженерных решений.

Подсистемы Guava, утратившие актуальность

Часть функциональности Guava сегодня представляет преимущественно исторический интерес. Эти компоненты активно использовались до появления Java 8, но в современных проектах применяются все реже.

К таким подсистемам относятся:

EventBus и AsyncEventBus

Исторически EventBus и AsyncEventBus использовались для снижения связности между компонентами внутри одного процесса. В современных системах их применение ограничено: для модульных приложений чаще используются явные интерфейсы и dependency injection, а для распределенных сценариев — брокеры сообщений или специализированные event-driven фреймворки.

EventBus предоставляет простую модель внутрипроцессных событий: объекты публикуются в шину, а подписчики получают их через методы, помеченные аннотацией @Subscribe.

1
2
3
4
5
6
7
8
9
10
EventBus eventBus = new EventBus();

eventBus.register(new Object() {
    @Subscribe
    public void handle(UserCreatedEvent event) {
        System.out.println(event.getUserId());
    }
});

eventBus.post(new UserCreatedEvent("10"));

В данном примере обработчик синхронно вызывается при публикации события. AsyncEventBus отличается лишь тем, что доставка событий происходит через Executor.

1
2
3
4
5
6
7
8
9
10
11
12
13
Executor executor = Executors.newFixedThreadPool(4);
AsyncEventBus asyncEventBus = new AsyncEventBus(executor);

asyncEventBus.register(new Object() {
    @Subscribe
    public void handle(UserCreatedEvent event) {
        System.out.println(
            Thread.currentThread().getName() + ": " + event.getUserId()
        );
    }
});

asyncEventBus.post(new UserCreatedEvent("10"));

В этом случае обработчики выполняются в потоках из Executor, а вызов post не блокирует текущий поток. При этом сохраняется та же семантика подписки и доставки событий, что и у EventBus.

Падение подписчика и влияние на EventBus

Важно учитывать, что EventBus не изолирует обработчиков друг от друга. Исключение, выброшенное в одном подписчике, прерывает доставку события.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
eventBus.register(new Object() {
    @Subscribe
    public void first(UserCreatedEvent event) {
        throw new RuntimeException("Unexpected error");
    }
});

eventBus.register(new Object() {
    @Subscribe
    public void second(UserCreatedEvent event) {
        System.out.println("This will not be printed");
    }
});

eventBus.post(new UserCreatedEvent("42"));

В этом примере второй обработчик не будет вызван, поскольку исключение из первого подписчика прерывает цепочку доставки события. Таким образом, надежность обработки событий напрямую зависит от корректности всех подписчиков.

В AsyncEventBus исключения не блокируют вызывающий поток, однако они по-прежнему приводят к потере события для конкретного обработчика и не имеют встроенного механизма повторной доставки или компенсации.

Отсутствие backpressure и гарантий доставки

Ни EventBus, ни AsyncEventBus не предоставляют механизмов управления нагрузкой или гарантий доставки событий.

В частности:

  • отсутствует backpressure — при публикации большого числа событий они без ограничений передаются подписчикам;
  • нет очередей с контролем размера или стратегии переполнения;
  • отсутствуют гарантии доставки (at-least-once / exactly-once);
  • нет встроенной поддержки повторной обработки при ошибках.

AsyncEventBus лишь смещает выполнение обработчиков в Executor, но не решает системных проблем надежности. При перегрузке или ошибках подписчиков события могут быть silently потеряны.

По этой причине EventBus исторически применялся только для локальных, некритичных сценариев внутри одного процесса и не рассматривается как основа для event-driven архитектуры в современных системах.

Функциональные абстракции и FluentIterable

До появления Stream API Guava предлагала собственные функциональные интерфейсы (Function, Predicate) и обертки над коллекциями.

1
2
3
Iterable<String> result = FluentIterable.from(list)
    .filter(s -> s.length() > 3)
    .transform(String::toUpperCase);

С появлением Streams необходимость в FluentIterable и собственных функциональных интерфейсах Guava отпала. Однако подобный код все еще часто встречается в проектах, начатых до Java 8.

ListenableFuture и concurrency-утилиты

ListenableFuture расширяет стандартный Future, добавляя возможность регистрации callback-ов без блокирующего вызова get().

1
2
3
4
5
6
7
8
9
10
11
12
13
ListenableFuture<User> future = service.loadUserAsync(id);

Futures.addCallback(future, new FutureCallback<>() {
    @Override
    public void onSuccess(User user) {
        log.info("Loaded {}", user);
    }

    @Override
    public void onFailure(Throwable t) {
        log.error("Failed to load user", t);
    }
}, executor);

Таким образом Guava позволяла описывать асинхронную логику в callback-стиле. В современных версиях Java аналогичные задачи решаются с помощью CompletableFuture, а в более сложных сценариях — с помощью реактивных библиотек.

В новом коде ListenableFuture используется редко, однако в legacy-системах он остается важной частью асинхронного слоя.

Помимо ListenableFuture, Guava включала набор concurrency-утилит: Futures для композиции асинхронных операций, ListeningExecutorService и MoreExecutors для интеграции с ExecutorService, а также модель управляемых сервисов (Service, AbstractService) и дополнительные синхронизационные примитивы. Эти решения во многом предвосхитили появление CompletableFuture и современных lifecycle-фреймворков, но сегодня в основном представляют исторический интерес.

I/O-утилиты

Guava предоставляла компактные утилиты для работы с файлами, потоками и ресурсами.

1
2
String content =
    Files.asCharSource(file, StandardCharsets.UTF_8).read();

Этот код читает содержимое файла целиком в строку. В современных версиях Java аналогичная задача решается средствами NIO.2:

1
String content = java.nio.file.Files.readString(path);

С развитием стандартного I/O API потребность в Guava-утилитах существенно снизилась, однако в legacy-коде они по-прежнему встречаются достаточно часто.

Использование перечисленных подсистем в новом коде, как правило, не рекомендуется. Однако в legacy-проектах они продолжают встречаться и требуют понимания их первоначального назначения.

Подсистемы, идеи которых были поглощены JDK

Значительная часть Guava оказала прямое влияние на развитие стандартной библиотеки Java. Некоторые ее концепции со временем были реализованы непосредственно в JDK.

Наиболее показательные примеры:

Immutable-коллекции

Исторически immutable-коллекции были одной из ключевых причин использования Guava. В отличие от Collections.unmodifiable*, они обеспечивали глубокую неизменяемость и не зависели от исходных изменяемых коллекций.

1
ImmutableList<String> list = ImmutableList.of("a", "b", "c");

Попытка изменить такую коллекцию приводит к исключению, а сама коллекция не содержит внутренних ссылок на изменяемые структуры.

До появления Java 9 стандартная библиотека не предоставляла сопоставимых по удобству и надежности средств. Ситуация изменилась с введением фабричных методов:

1
2
List<String> list = List.of("a", "b", "c");
Map<String, Integer> map = Map.of("a", 1, "b", 2);

Эти коллекции также являются неизменяемыми и удовлетворяют большинству практических сценариев. В новом коде это делает использование Guava immutable-коллекций необязательным, однако в legacy-проектах они остаются распространенными и хорошо интегрированными.

Optional и fail-fast-подход

Guava предложила Optional и развитую модель предусловий задолго до появления Java 8. Сегодня эти идеи реализованы в стандартной библиотеке, хотя отдельные утилиты Guava по-прежнему выигрывают в читаемости.

1
2
3
4
5
Optional<User> user = Optional.fromNullable(loadUser(id));

if (user.isPresent()) {
    process(user.get());
}

С появлением java.util.Optional аналогичная концепция была реализована в стандартной библиотеке:

1
2
Optional<User> user = Optional.ofNullable(loadUser(id));
user.ifPresent(this::process);

Сегодня использование Optional из Guava в новом коде не рекомендуется, однако в старых проектах обе реализации могут сосуществовать, что требует внимательного отношения при рефакторинге.

Preconditions и fail-fast-проверки

Guava одной из первых предложила компактный и выразительный API для проверки предусловий.

1
2
Preconditions.checkArgument(timeout > 0, "timeout must be positive");
Preconditions.checkNotNull(user, "user is required");

Этот подход способствовал распространению fail-fast-практик в Java-коде, особенно в инфраструктурных и базовых слоях.

Впоследствии часть этой функциональности была включена в JDK:

1
Objects.requireNonNull(user, "user is required");

Подсистемы Guava, остающиеся актуальными в 2025 году

Несмотря на значительные изменения в экосистеме Java, ряд подсистем Guava остается практически полезным и сегодня.

In-memory cache

Наиболее весомым аргументом в пользу Guava остается ее реализация in-memory-кэша.

1
2
3
4
5
LoadingCache<String, User> cache =
    CacheBuilder.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .maximumSize(10_000)
        .build(this::loadUser);

Данный механизм предоставляет:

  • потокобезопасность;
  • управление временем жизни записей (TTL);
  • ограничение размера;
  • ленивую загрузку значений.

При этом он не требует подключения внешней инфраструктуры (Redis, Hazelcast и тп) и не привязан к Spring-экосистеме. Для локальных оптимизаций доступа к данным внутри одной JVM это остается простым и надежным решением.

Расширенные коллекции

Стандартная библиотека Java по-прежнему не покрывает ряд распространенных моделей данных. Guava предлагает для них специализированные абстракции.

Multimap

Multimap естественно отображает структуры вида «ключ → несколько значений» и позволяет избежать вспомогательных Map<K, List<V>>.

1
2
3
Multimap<String, String> headers = ArrayListMultimap.create();
headers.put("Accept", "application/json");
headers.put("Accept", "text/plain");

В этом примере одному HTTP-заголовку соответствует несколько значений. Использование Multimap избавляет от необходимости инициализировать списки, проверять их наличие и контролировать модификации.

1
Collection<String> acceptValues = headers.get("Accept");

Важно отметить, что get всегда возвращает коллекцию, даже если ключ отсутствует, что упрощает клиентский код и снижает количество проверок.

Multiset

Multiset моделирует множество с учетом количества элементов и часто оказывается удобнее ручной агрегации через Map<T, Integer>.

1
2
3
Multiset<String> words = HashMultiset.create();
words.add("java");
words.add("java");

Использование:

1
int javaCount = words.count("java"); // 2

Такая структура естественно ложится на задачи подсчета, статистики и агрегации. При использовании стандартных коллекций аналогичный код требовал бы явного управления счетчиками и проверок наличия ключей.

BiMap

BiMap используется для двусторонних соответствий, когда отображение должно быть обратимым.

1
2
3
BiMap<Integer, String> statusCodes = HashBiMap.create();
statusCodes.put(200, "OK");
statusCodes.put(404, "NOT_FOUND");

Использование:

1
2
String status = statusCodes.get(200);
Integer code = statusCodes.inverse().get("OK");

В отличие от обычных Map, BiMap гарантирует уникальность значений, что позволяет безопасно использовать обратное отображение без дополнительной логики.

Table

Table позволяет выразить двухмерные структуры данных в виде (rowKey, columnKey) → value.

1
2
3
4
Table<String, String, Integer> scores = HashBasedTable.create();
scores.put("Alice", "Math", 90);
scores.put("Alice", "Physics", 85);
scores.put("Bob", "Math", 78);

Использование:

1
2
Map<String, Integer> aliceScores = scores.row("Alice");
Integer mathScore = scores.get("Bob", "Math");

Использование Table избавляет от вложенных Map<K, Map<K, V>> и делает структуру данных более явной и читаемой.

Preconditions и Splitter / Joiner

Preconditions, Splitter и Joiner не предоставляют принципиально новой функциональности, но позволяют описывать типовые операции более декларативно. В инфраструктурном и интеграционном коде это иногда повышает читаемость и снижает уровень шума.

Ограничения и издержки использования

При принятии решения о подключении Guava стоит учитывать следующие факторы:

  • значительная часть функциональности дублируется средствами JDK;
  • библиотека достаточно объемна и увеличивает граф зависимостей;
  • ее использование нередко продолжается по инерции, без явной необходимости.

В новых проектах это требует особенно аккуратной оценки.

Заключение

В 2025 году Guava нельзя рассматривать как монолитную библиотеку, которую следует либо целиком принять, либо полностью исключить из проекта.

Это зрелый набор подсистем с различной эволюционной судьбой. В legacy-коде ее присутствие, как правило, естественно. В новых проектах она может использоваться точечно — при наличии конкретной, технически обоснованной необходимости.

Практическая ценность Guava определяется не фактом наличия зависимости, а тем, какие именно ее подсистемы используются и по каким причинам.

This post is licensed under CC BY 4.0 by the author.