1. 概述

本文将对比两个 Java 生态中非常流行的开源库:Apache CommonsGoogle Guava。这两个库都提供了丰富的工具类 API,主要集中在集合、I/O 等领域。

为简洁起见,本文只聚焦于集合框架中最常用的几个功能,并结合代码示例进行讲解。同时,也会总结它们之间的差异。

此外,我们还整理了一系列深入讲解各种 CommonsGuava 工具的文章,供你进一步学习。

2. 两者的简要历史

Google Guava 是由 Google 主导开发的项目,虽然现在已开源,但其核心功能仍由 Google 工程师维护。Guava 最初的动机是将 JDK 1.5 引入的泛型特性集成到 Java 集合框架(JCF)中,并增强其功能。

随着时间的推移,Guava 的能力不断扩展,现已涵盖图结构、函数式编程、范围对象、缓存和字符串处理等功能。

Apache Commons 最初是 Jakarta 项目的一部分,旨在补充 Java 核心集合 API 的不足,后来成为 Apache 软件基金会的顶级项目。多年来,它扩展为一个庞大的可重用 Java 组件库,覆盖了图像处理、I/O、加密、缓存、网络、验证、对象池等多个领域。

作为一个开源项目,Apache 社区持续为 Commons 添加新功能。✅ 尤其值得一提的是,它对向后兼容性极为重视

3. Maven 依赖配置

使用 Guava 时,只需在 pom.xml 中添加如下依赖:

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

最新版本可在 Maven 中央仓库 查看。

对于 Apache Commons,情况略有不同。根据所需功能,需要引入不同的子模块。例如,若使用集合相关功能,需添加:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.4</version>
</dependency>

在本文示例中,我们将使用 commons-collections4

现在,我们进入正题!

4. 双向映射(Bi-directional Maps)

双向映射指的是既能通过 key 查找 value,也能通过 value 查找 key 的映射结构。JCF 并不直接支持这种结构。

我们来看 Guava 和 Apache Commons 是如何实现的。示例中,我们将使用一周七天的编号与名称之间的双向映射。

4.1. Guava 的 BiMap

Guava 提供了 BiMap 接口,用于实现双向映射。✅ 可使用 EnumBiMapEnumHashBiMapHashBiMapImmutableBiMap 等实现类实例化

这里我们使用 HashBiMap

BiMap<Integer, String> daysOfWeek = HashBiMap.create();

填充数据与普通 Map 类似:

daysOfWeek.put(1, "Monday");
daysOfWeek.put(2, "Tuesday");
daysOfWeek.put(3, "Wednesday");
daysOfWeek.put(4, "Thursday");
daysOfWeek.put(5, "Friday");
daysOfWeek.put(6, "Saturday");
daysOfWeek.put(7, "Sunday");

下面是几个 JUnit 测试验证功能:

@Test
public void givenBiMap_whenValue_thenKeyReturned() {
    assertEquals(Integer.valueOf(7), daysOfWeek.inverse().get("Sunday"));
}

@Test
public void givenBiMap_whenKey_thenValueReturned() {
    assertEquals("Tuesday", daysOfWeek.get(2));
}

4.2. Apache 的 BidiMap

Apache Commons 提供了 BidiMap 接口:

BidiMap<Integer, String> daysOfWeek = new TreeBidiMap<Integer, String>();

✅ **我们使用的是 TreeBidiMap,但也支持 DualHashBidiMapDualTreeBidiMap 等实现**。

填充方式与 Guava 的 BiMap 相同,使用方式也类似:

@Test
public void givenBidiMap_whenValue_thenKeyReturned() {
    assertEquals(Integer.valueOf(7), daysOfWeek.inverseBidiMap().get("Sunday"));
}

@Test
public void givenBidiMap_whenKey_thenValueReturned() {
    assertEquals("Tuesday", daysOfWeek.get(2));
}

在一些简单的性能测试中,Apache Commons 的双向映射在插入时稍逊于 Guava,但在查找 key 和 value 时表现更优。

5. 一个 key 映射多个 value

在购物车场景中,可能需要将水果和蔬菜分别归类,即一个 key 映射多个 value。我们来看看两个库如何处理这种情况。

5.1. Guava 的 Multimap

首先看如何创建和初始化 Multimap

Multimap<String, String> groceryCart = ArrayListMultimap.create();

groceryCart.put("Fruits", "Apple");
groceryCart.put("Fruits", "Grapes");
groceryCart.put("Fruits", "Strawberries");
groceryCart.put("Vegetables", "Spinach");
groceryCart.put("Vegetables", "Cabbage");

JUnit 测试验证功能:

@Test
public void givenMultiValuedMap_whenFruitsFetched_thenFruitsReturned() {
    List<String> fruits = Arrays.asList("Apple", "Grapes", "Strawberries");
    assertEquals(fruits, groceryCart.get("Fruits"));
}

@Test
public void givenMultiValuedMap_whenVeggiesFetched_thenVeggiesReturned() {
    List<String> veggies = Arrays.asList("Spinach", "Cabbage");
    assertEquals(veggies, groceryCart.get("Vegetables"));
}

Multimap 还支持移除某个 entry 或整个 key 对应的集合

@Test
public void givenMultiValuedMap_whenFuitsRemoved_thenVeggiesPreserved() {
    
    assertEquals(5, groceryCart.size());

    groceryCart.remove("Fruits", "Apple");
    assertEquals(4, groceryCart.size());

    groceryCart.removeAll("Fruits");
    assertEquals(2, groceryCart.size());
}

如上所示,我们先移除了 Apple,再移除了整个 Fruits 分类。

5.2. Apache 的 MultiValuedMap

初始化 MultiValuedMap

MultiValuedMap<String, String> groceryCart = new ArrayListValuedHashMap<>();

填充和使用方式与 Guava 类似:

@Test
public void givenMultiValuedMap_whenFruitsFetched_thenFruitsReturned() {
    List<String> fruits = Arrays.asList("Apple", "Grapes", "Strawberries");
    assertEquals(fruits, groceryCart.get("Fruits"));
}

@Test
public void givenMultiValuedMap_whenVeggiesFetched_thenVeggiesReturned() {
    List<String> veggies = Arrays.asList("Spinach", "Cabbage");
    assertEquals(veggies, groceryCart.get("Vegetables"));
}

⚠️ 但与 Guava 不同的是,Apache Commons 不支持移除单个 entry,只能移除整个 key 的集合

@Test
public void givenMultiValuedMap_whenFuitsRemoved_thenVeggiesPreserved() {
    assertEquals(5, groceryCart.size());

    groceryCart.remove("Fruits");
    assertEquals(2, groceryCart.size());
}

6. 多个 key 映射一个 value

我们以经纬度映射城市为例:

cityCoordinates.put("40.7128° N", "74.0060° W", "New York");
cityCoordinates.put("48.8566° N", "2.3522° E", "Paris");
cityCoordinates.put("19.0760° N", "72.8777° E", "Mumbai");

看看如何实现。

6.1. Guava 的 Table

Guava 提供了 Table 来处理这种场景:

Table<String, String, String> cityCoordinates = HashBasedTable.create();

使用示例:

@Test
public void givenCoordinatesTable_whenFetched_thenOK() {
    
    List expectedLongitudes = Arrays.asList("74.0060° W", "2.3522° E", "72.8777° E");
    assertArrayEquals(expectedLongitudes.toArray(), cityCoordinates.columnKeySet().toArray());

    List expectedCities = Arrays.asList("New York", "Paris", "Mumbai");
    assertArrayEquals(expectedCities.toArray(), cityCoordinates.values().toArray());
    assertTrue(cityCoordinates.rowKeySet().contains("48.8566° N"));
}

Table 提供了对行、列和值的 Set 视图,并支持按行或列查询

我们用电影演员与作品的例子演示:

Table<String, String, String> movies = HashBasedTable.create();

movies.put("Tom Hanks", "Meg Ryan", "You've Got Mail");
movies.put("Tom Hanks", "Catherine Zeta-Jones", "The Terminal");
movies.put("Bradley Cooper", "Lady Gaga", "A Star is Born");
movies.put("Keenu Reaves", "Sandra Bullock", "Speed");
movies.put("Tom Hanks", "Sandra Bullock", "Extremely Loud & Incredibly Close");

查询示例:

@Test
public void givenMoviesTable_whenFetched_thenOK() {
    assertEquals(3, movies.row("Tom Hanks").size());
    assertEquals(2, movies.column("Sandra Bullock").size());
    assertEquals("A Star is Born", movies.get("Bradley Cooper", "Lady Gaga"));
    assertTrue(movies.containsValue("Speed"));
}

⚠️ Table 的限制是只能用两个 key 映射一个 value,Guava 暂不支持更多 key 的映射

6.2. Apache 的 MultiKeyMap

回到城市坐标映射的例子,使用 MultiKeyMap 实现:

@Test
public void givenCoordinatesMultiKeyMap_whenQueried_thenOK() {
    MultiKeyMap<String, String> cityCoordinates = new MultiKeyMap<String, String>();

    // populate with keys and values as shown previously

    List expectedLongitudes = Arrays.asList("72.8777° E", "2.3522° E", "74.0060° W");
    List longitudes = new ArrayList<>();

    cityCoordinates.forEach((key, value) -> {
      longitudes.add(key.getKey(1));
    });
    assertArrayEquals(expectedLongitudes.toArray(), longitudes.toArray());

    List expectedCities = Arrays.asList("Mumbai", "Paris", "New York");
    List cities = new ArrayList<>();

    cityCoordinates.forEach((key, value) -> {
      cities.add(value);
    });
    assertArrayEquals(expectedCities.toArray(), cities.toArray());
}

⚠️ 要实现与 Table 相同的查询效果,需要手动遍历 MultiKeyMap

但 MultiKeyMap 的优势在于支持多个 key 映射一个 value。例如:

@Test
public void givenDaysMultiKeyMap_whenFetched_thenOK() {
    days = new MultiKeyMap<String, String>();
    days.put("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Weekday");
    days.put("Saturday", "Sunday", "Weekend");

    assertFalse(days.get("Saturday", "Sunday").equals("Weekday"));
}

7. Apache Commons Collections vs. Google Guava

据 Guava 开发者所述,✅ Guava 的诞生初衷是为了支持泛型,而 Apache Commons 在早期并不支持。Guava 严格遵循集合 API 的设计规范,且持续活跃开发,版本更新频繁。

但在性能方面,Apache Commons 在查询时表现更优,而 Guava 在插入操作上更胜一筹。

虽然本文只对比了集合相关的功能,但 ✅ Apache Commons 整体功能覆盖面比 Guava 更广

8. 总结

本文对比了 Apache Commons Collections 与 Google Guava 在集合框架方面的部分功能。

当然,这只是冰山一角。两个库都有各自的独特功能,✅ 在实际项目中,它们完全可以共存互补

源码示例可在 GitHub 获取。


原始标题:Apache Commons Collections vs Google Guava