Java Collection的多个索引 - 最基本的解决方案?

时间:2010-03-23 15:57:33

标签: java collections indexing

我正在寻找在Java Collection上创建多个索引的最基本的解决方案。

所需功能:

  • 删除值后,必须删除与该值关联的所有索引条目。
  • 索引查找必须比线性搜索更快(至少与TreeMap一样快)。

边条件:

  • 不依赖于大型(如Lucene)库。没有罕见或未经过良好测试的库。没有数据库。
  • 像Apache Commons Collections这样的库可以。
  • 更好的是,如果仅适用于JavaSE(6.0)。
  • 编辑:没有自我实施的解决方案(感谢答案提示这一点 - 最好将它们放在这里以保证完整性,但我已经有了一个非常类似于Jay的解决方案)每当有几个人们发现,他们实现了同样的事情,这应该是一些公共图书馆的一部分。

当然,我可以写一个自己管理多个地图的课程(这并不难,但感觉就像重新发明轮子一样)。所以我想知道,如果没有它可以完成 - 同时仍然得到类似于使用单个索引java.util.Map的简单用法。

谢谢,克里斯

更新

看起来好像我们没有找到任何东西。我喜欢你所有的答案 - 自我开发的版本,数据库类库的链接。

这就是我真正想要的:在(a)Apache Commons Collections或(b)Google Collections / Guava中使用这些功能。或者也许是一个非常好的选择。

其他人也会错过这些库中的这项功能吗?他们提供各种各样的东西,如MultiMaps,MulitKeyMaps,BidiMaps,......我觉得,它很适合这些库 - 它可以被称为MultiIndexMap。你觉得怎么样?

14 个答案:

答案 0 :(得分:19)

每个索引基本上都是单独的Map。您可以(也可能应该)在管理搜索,索引,更新和删除的类后面抽象它。一般来说,这并不难做到这一点。但不是,虽然它可以很容易地从Java Collections类构建,但是没有标准的开箱即用类。

答案 1 :(得分:12)

看看CQEngine (Collection Query Engine),它完全符合此类要求,基于IndexedCollection

有关更多背景信息,请参阅相关问题How do you query object collections in Java (Criteria/SQL-like)?

答案 2 :(得分:7)

我的第一个想法是为被索引的东西创建一个类,然后创建多个HashMap来保存索引,并将相同的对象添加到每个HashMaps中。对于添加,您只需将相同的对象添加到每个HashMap。删除将需要搜索每个HashMap以获取对目标对象的引用。如果删除需要很快,您可能希望为每个索引创建两个HashMaps:一个用于索引到值,另一个用于值到索引。当然,我会用一个明确定义的界面包装你在课堂上做的任何事情。

看起来这不会很难。如果您事先知道索引的数量和类型以及窗口小部件的类,那将非常简单,例如:

public class MultiIndex
{
  HashMap<String,Widget> index1=new HashMap<String,Widget>();
  HashMap<String,Widget> index2=new HashMap<String,Widget>();
  HashMap<Integer,Widget> index3=new HashMap<Integer,Widget>();

  public void add(String index1Value, String index2Value, Integer index3Value, Widget widget)
  {
    index1.put(index1Value, widget);
    index2.put(index2Value, widget);
    index3.put(index3Value, widget);
  }
  public void delete(Widget widget)
  {
    Iterator i=index1.keySet().iterator(); 
    while (i.hasNext())
    {
      String index1Value=(String)i.next();
      Widget gotWidget=(Widget) index1.get(index1Value);
      if (gotWidget.equals(widget))
        i.remove();
    }
    ... similarly for other indexes ...
  }
  public Widget getByIndex1(String index1Value)
  {
    return index1.get(index1Value);
  }
  ... similarly for other indexes ...

  }
}

如果你想让它变得通用并接受任何对象,拥有可变数量和类型的索引等,那就更复杂了,但并不多。

答案 3 :(得分:5)

您有很多非常紧缩的要求似乎对您的需求非常特别。你说的大多数事情都不可行是因为很多人都有相同的确切需求,基本上定义了一个基本的数据库引擎。这就是为什么它们是“大型”库。你说“没有数据库”,但其核心是每个索引系统都是术语和文档的“数据库”。我认为Collection是一个“数据库”。我想说看看Space4J

我会说如果你找不到你要找的东西,可以在GitHub上开始一个项目,继续自己编码并分享结果。

答案 4 :(得分:4)

Google Collections LinkedListMultimap

关于您的第一个要求

  • 删除值后,必须删除与该值关联的所有索引条目。

我认为既没有图书馆也没有帮助者支持它。

以下是我使用LinkedListMultimap

的方法
Multimap<Integer, String> multimap = LinkedListMultimap.create();

// Three duplicates entries
multimap.put(1, "A");
multimap.put(2, "B");
multimap.put(1, "A");
multimap.put(4, "C");
multimap.put(1, "A");

System.out.println(multimap.size()); // outputs 5

为了满足您的第一个要求,帮助者可以做得很好

public static <K, V> void removeAllIndexEntriesAssociatedWith(Multimap<K, V> multimap, V value) {
    Collection<Map.Entry<K, V>> eCollection = multimap.entries();
    for (Map.Entry<K, V> entry : eCollection)
        if(entry.getValue().equals(value))
            eCollection.remove(entry);
}

...

removeAllIndexEntriesAssociatedWith(multimap, "A");

System.out.println(multimap.size()); // outputs 2

Google系列

  • 轻质
  • Joshua Block(Effective Java)支持
  • 好的功能如ImmutableList,ImmutableMap等

答案 5 :(得分:4)

你需要查看Boon。 :)

http://rick-hightower.blogspot.com/2013/11/what-if-java-collections-and-java.html

您可以添加n个搜索索引和查找索引。它还允许您有效地查询原始属性。

以下是wiki(我是作者)的一个例子。

  repoBuilder.primaryKey("ssn")
          .searchIndex("firstName").searchIndex("lastName")
          .searchIndex("salary").searchIndex("empNum", true)
          .usePropertyForAccess(true);

您可以通过提供true标志作为searchIndex的第二个参数来覆盖它。

注意empNum是一个可搜索的唯一索引。

如果在运行时查询一组复杂的Java对象很容易怎么办?如果有一个API使您的对象索引(实际上只是TreeMaps和HashMaps)保持同步,该怎么办?那么你会得到Boon的数据回购。本文介绍如何使用Boon的数据存储实用程序来查询Java对象。这是第一部分。可以有很多很多部分。 :) Boon的数据仓库使得对集合的基于索引的查询变得更加容易。 为什么Boon数据回购

Boon的数据存储允许您至少在查询集合时将Java集合视为数据库。 Boon的数据仓库不是内存数据库,无法替代将对象安排到针对您的应用程序优化的数据结构中。 如果您想花时间提供客户价值并构建对象和类,并使用Collections API来处理数据结构,那么DataRepo就是您的最佳选择。这并不妨碍打破Knuth书籍并提出优化的数据结构。它只是帮助保持平凡的事物,这样你就可以花时间把困难的事情做好。 出生于需要

这个项目出于需要。我正在研究一个项目,该项目计划在内存中存储大量的域对象以提高速度,并且有人向所有人提出我忽略的重要问题。我们如何查询这些数据。我的回答是我们将使用Collections API和Streaming API。然后我试着这样做......嗯...... 我也厌倦了在大型数据集上使用JDK 8流API,而且速度很慢。 (Boon的数据仓库与JDK7和JDK8一起使用)。这是一个线性搜索/过滤器。这是设计的,但是对于我正在做的事情,它没有用。我需要索引来支持任意查询。 Boon的数据仓库增强了流API。

Boon的数据存储不会尝试替换JDK 8流API,事实上它可以很好地使用它。 Boon的数据仓库允许您创建索引集合。索引可以是任何东西(它是可插入的)。 此时,Boon的数据仓库索引基于ConcurrentHashMap和ConcurrentSkipListMap。 根据设计,Boon的数据仓库可与标准集合库配合使用。没有计划创建一组自定义集合。如果有人愿意,应该能够插入番石榴,并行树或特洛伊。 它为此提供了简化的API。它允许线性搜索完成感,但我建议主要使用它来使用索引,然后使用流API来实现其余的(对于类型安全和速度)。

逐步前进潜行峰值

假设您有一个创建200,000个员工对象的方法,如下所示:

 List<Employee> employees = TestHelper.createMetricTonOfEmployees(200_000);

所以现在我们有200,000名员工。让我们搜索一下......

首先在可搜索的查询中包装Employees:

   employees = query(employees);

现在搜索:

  List<Employee> results = query(employees, eq("firstName", firstName));

那么上面和流API之间的主要区别是什么?

  employees.stream().filter(emp -> emp.getFirstName().equals(firstName)

使用Boon的DataRepo,速度提高了约20,000%!啊HashMaps和TreeMaps的力量啊。 :) 有一个看起来就像你的内置集合的API。还有一个看起来更像DAO对象或Repo对象的API。

使用Repo / DAO对象的简单查询如下所示:

  List<Employee> employees = repo.query(eq("firstName", "Diana"));

更复杂的查询将如下所示:

  List<Employee> employees = repo.query(
      and(eq("firstName", "Diana"), eq("lastName", "Smith"), eq("ssn", "21785999")));

或者这个:

  List<Employee> employees = repo.query(
      and(startsWith("firstName", "Bob"), eq("lastName", "Smith"), lte("salary", 200_000),
              gte("salary", 190_000)));

甚至这个:

  List<Employee> employees = repo.query(
      and(startsWith("firstName", "Bob"), eq("lastName", "Smith"), between("salary", 190_000, 200_000)));

或者如果你想使用JDK 8流API,这不适用于它:

  int sum = repo.query(eq("lastName", "Smith")).stream().filter(emp -> emp.getSalary()>50_000)
      .mapToInt(b -> b.getSalary())
      .sum();

如果员工人数很多,上述情况会更快。这将缩小名字从史密斯开始并且薪水超过50,000的员工。让我们说你有10万名员工,只有50名叫史密斯,所以现在你通过使用有效地从50万名员工中抽出50名员工的指数迅速缩小到50,然后我们只用50而不是整个100,000来过滤。 / p>

以下是从线性搜索的数据回购与以纳秒为单位的索引搜索的基准测试:

Name index  Time 218 
Name linear  Time 3709120 
Name index  Time 213 
Name linear  Time 3606171 
Name index  Time 219 
Name linear  Time 3528839

有人最近对我说:&#34;但是使用流API,您可以在parralel中运行过滤器。

让我们看看数学如何成立:

3,528,839 / 16 threads vs. 219

201,802 vs. 219 (nano-seconds).

索引获胜,但这是一张照片。不! :)

仅快9,500%而不是快40,000%。太近了.....

我添加了更多功能。他们大量使用索引。 :)

repo.updateByFilter(values(&#34; firstName&#34;,&#34; Di&#34;)),                 和(eq(&#34; firstName&#34;,&#34; Diana&#34;),                         eq(&#34; lastName&#34;,&#34; Smith&#34;),                         eq(&#34; ssn&#34;,&#34; 21785999&#34;)));

上述内容相当于

更新员工e    SET e.firstName =&#39; Di&#39;   在哪里e.firstName =&#39;戴安娜&#39;     和e.lastName =&#39;史密斯&#39;     和e.ssn =&#39; 21785999&#39;

这允许您在多个记录上一次设置多个字段,以便进行批量更新。

所有基本类型都有重载方法,所以如果你有一个值来更新从过滤器返回的每个项目:

  repo.updateByFilter("firstName", "Di",
          and( eq("firstName", "Diana"),
          eq("lastName", "Smith"),
                  eq("ssn", "21785999") ) );

以下是一些基本的选择功能:

  List <Map<String, Object>> list =
     repo.query(selects(select("firstName")), eq("lastName", "Hightower"));

您可以根据需要选择多个选项。您还可以将列表重新排序:

  List <Map<String, Object>> list =
     repo.sortedQuery("firstName",selects(select("firstName")),
       eq("lastName", "Hightower"));

您可以选择相关属性的属性(即employee.department.name)。

  List <Map<String, Object>> list = repo.query(
          selects(select("department", "name")),
          eq("lastName", "Hightower"));

  assertEquals("engineering", list.get(0).get("department.name"));

以上将尝试使用类的字段。如果要使用实际属性(emp.getFoo()与emp.foo),则需要使用selectPropertyPath。

  List <Map<String, Object>> list = repo.query(
          selects(selectPropPath("department", "name")),
          eq("lastName", "Hightower"));

请注意,select(&#34; department&#34;,&#34; name&#34;)比selectPropPath快得多(&#34; department&#34;,&#34; name&#34;),这可能是一个紧密的循环。

默认情况下,所有搜索索引和查找索引都允许重复(主键索引除外)。

  repoBuilder.primaryKey("ssn")
          .searchIndex("firstName").searchIndex("lastName")
          .searchIndex("salary").searchIndex("empNum", true)
          .usePropertyForAccess(true);

您可以通过提供true标志作为searchIndex的第二个参数来覆盖它。

注意empNum是一个可搜索的唯一索引。

如果您愿意或需要,您甚至可以将简单搜索作为地图进行搜索:

  List<Map<String, Object>> employees = repo.queryAsMaps(eq("firstName", "Diana"));

我不确定这是一个功能还是一个错误。我的想法是,一旦处理数据,您需要以不将数据使用者与实际API联系起来的方式呈现数据。拥有String / basic类型的Map似乎是实现此目的的一种方法。 请注意,要映射转换的对象如下所示:

  System.out.println(employees.get(0).get("department"));

收率:

{class=Department, name=engineering}

这对于工具的调试和即席查询非常有用。我正在考虑添加支持以轻松转换为JSON字符串。

添加了查询集合属性的功能。这应该与集合和数组一起使用,如您所愿深度嵌套。再次阅读,因为它是一个真正的MF实现!

  List <Map<String, Object>> list = repo.query(
          selects(select("tags", "metas", "metas2", "metas3", "name3")),
          eq("lastName", "Hightower"));

  print("list", list);

  assertEquals("3tag1", idx(list.get(0).get("tags.metas.metas2.metas3.name3"), 0));

以上打印出来的内容如下:

list [{tags.metas.metas2.metas3.name3=[3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3]},
...

我创建了几个关系类来测试它:

public class Employee {
List <Tag> tags = new ArrayList<>();
{
    tags.add(new Tag("tag1"));
    tags.add(new Tag("tag2"));
    tags.add(new Tag("tag3"));

}
...
public class Tag {
...
List<Meta> metas = new ArrayList<>();
{
    metas.add(new Meta("mtag1"));
    metas.add(new Meta("mtag2"));
    metas.add(new Meta("mtag3"));

}

}
public class Meta {
 ...
   List<Meta2> metas2 = new ArrayList<>();
   {
       metas2.add(new Meta2("2tag1"));
       metas2.add(new Meta2("2tag2"));
       metas2.add(new Meta2("2tag3"));

   }

}

...
public class Meta2 {



List<Meta3> metas3 = new ArrayList<>();
{
    metas3.add(new Meta3("3tag1"));
    metas3.add(new Meta3("3tag2"));
    metas3.add(new Meta3("3tag3"));

}
public class Meta3 {

...

您还可以按类型搜索:

  List<Employee> results = sortedQuery(queryableList, "firstName", typeOf("SalesEmployee"));

  assertEquals(1, results.size());
  assertEquals("SalesEmployee", results.get(0).getClass().getSimpleName());

以上查找所有员工的简单类名为SalesEmployee。它也适用于全班名称,如:

  List<Employee> results = sortedQuery(queryableList, "firstName", typeOf("SalesEmployee"));

  assertEquals(1, results.size());
  assertEquals("SalesEmployee", results.get(0).getClass().getSimpleName());

您也可以按实际课程搜索:

  List<Employee> results = sortedQuery(queryableList, "firstName", instanceOf(SalesEmployee.class));

  assertEquals(1, results.size());
  assertEquals("SalesEmployee", results.get(0).getClass().getSimpleName());

您还可以查询实现某些接口的类:

  List<Employee> results = sortedQuery(queryableList, "firstName",      
                              implementsInterface(Comparable.class));

  assertEquals(1, results.size());
  assertEquals("SalesEmployee", results.get(0).getClass().getSimpleName());

您也可以索引嵌套字段/属性,它们可以是集合字段或属性非集合字段,可以根据需要进行深层嵌套:

  /* Create a repo, and decide what to index. */
  RepoBuilder repoBuilder = RepoBuilder.getInstance();

  /* Look at the nestedIndex. */
  repoBuilder.primaryKey("id")
          .searchIndex("firstName").searchIndex("lastName")
          .searchIndex("salary").uniqueSearchIndex("empNum")
          .nestedIndex("tags", "metas", "metas2", "name2");

稍后您可以使用nestedIndex进行搜索。

  List<Map<String, Object>> list = repo.query(
          selects(select("tags", "metas", "metas2", "name2")),
          eqNested("2tag1", "tags", "metas", "metas2", "name2"));

使用nestedIndex的安全方法是使用eqNested。如果你有这样的索引,你可以使用eq,gt,gte等:

  List<Map<String, Object>> list = repo.query(
          selects(select("tags", "metas", "metas2", "name2")),
          eq("tags.metas.metas2.name2", "2tag1"));

您还可以添加对子类的支持

  List<Employee> queryableList = $q(h_list, Employee.class, SalesEmployee.class,  
                  HourlyEmployee.class);
  List<Employee> results = sortedQuery(queryableList, "firstName", eq("commissionRate", 1));
  assertEquals(1, results.size());
  assertEquals("SalesEmployee", results.get(0).getClass().getSimpleName());

  results = sortedQuery(queryableList, "firstName", eq("weeklyHours", 40));
  assertEquals(1, results.size());
  assertEquals("HourlyEmployee", results.get(0).getClass().getSimpleName());

数据仓库在其DataRepoBuilder.build(...)方法中具有类似的功能,用于指定子类。这允许您在同一个repo或searchable集合中使用无缝查询字段形成子类和类。

答案 6 :(得分:2)

我编写了一个包含

等方法的Table界面
V put(R rowKey, C columnKey, V value) 
V get(Object rowKey, Object columnKey) 
Map<R,V> column(C columnKey) 
Set<C> columnKeySet()
Map<C,V> row(R rowKey)
Set<R> rowKeySet()
Set<Table.Cell<R,C,V>> cellSet()

我们希望将它包含在未来的番石榴版本中,但我不知道何时会发生这种情况。 http://code.google.com/p/guava-libraries/issues/detail?id=173

答案 7 :(得分:2)

您的主要目标似乎是当您从一个索引中移除该对象时,您将从所有索引中删除该对象。

最简单的方法是添加另一个间接层:将实际对象存储在Map<Long,Value>中,并使用双向映射(您可以在Jakarta Commons中找到它,可能还有Google代码)用于索引为Map<Key,Long>。从特定索引中删除条目时,您将从该索引中获取Long值,并使用它从主映射和其他索引中删除相应的条目。

BIDIMap的一个替代方案是将您的“索引”地图定义为Map<Key,WeakReference<Long>>;但是,这将要求您实施ReferenceQueue进行清理。


另一种方法是创建一个可以接受任意元组的密钥对象,定义其equals()方法以匹配元组中的任何元素,并将其与TreeMap一起使用。您不能使用HashMap,因为您将无法仅根据元组的一个元素计算哈希码。

public class MultiKey
implements Comparable<Object>
{
   private Comparable<?>[] _keys;
   private Comparable _matchKey;
   private int _matchPosition;

   /**
    *  This constructor is for inserting values into the map.
    */
   public MultiKey(Comparable<?>... keys)
   {
      // yes, this is making the object dependent on externally-changable
      // data; if you're paranoid, copy the array
      _keys = keys;
   }


   /**
    *  This constructor is for map probes.
    */
   public MultiKey(Comparable key, int position)
   {
      _matchKey = key;
      _matchPosition = position;
   }


   @Override
   public boolean equals(Object obj)
   {
      // verify that obj != null and is castable to MultiKey
      if (_keys != null)
      {
         // check every element
      }
      else
      {
         // check single element
      }
   }


   public int compareTo(Object o)
   {
      // follow same pattern as equals()
   }
}

答案 8 :(得分:1)

使用Prefuse Tables。它们支持尽可能多的索引,快速(索引是TreeMaps),并且有很好的过滤选项(布尔过滤器?没问题!)。无需数据库,在许多信息可视化应用程序中使用大型数据集进行测试。

在它们的原始形式中,它们不像标准容器那样方便(你需要处理行和列),但你可以肯定地写一个小包装器。此外,它们可以很好地插入到UI组件中,例如Swing的JTables。

答案 9 :(得分:1)

如果您想要数据的多个索引,您可以创建和维护多个哈希映射或使用像Data Store这样的库:

https://github.com/jparams/data-store

示例:

Store<Person> store = new MemoryStore<>() ;
store.add(new Person(1, "Ed", 3));
store.add(new Person(2, "Fred", 7));
store.add(new Person(3, "Freda", 5));
store.index("name", Person::getName);
Person person = store.getFirst("name", "Ed");

使用数据存储,您可以创建不区分大小写的索引和各种很酷的东西。值得一试。

答案 10 :(得分:0)

我不确定我是否理解这个问题,但我认为您要求的是多种方法,可以将不同的,唯一的键映射到值,并在值消失时进行适当的清理。

我看到你不想自己动手,但是有一个简单的地图和多图的组合(我使用下面的Guava多图,但Apache也可以工作)来做你想要的。我在下面有一个快速而肮脏的解决方案(跳过构造函数,因为这取决于你想要使用什么类型的底层地图/多图):

package edu.cap10.common.collect;

import java.util.Collection;
import java.util.Map;

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

public class MIndexLookupMap<T> extends ForwardingMap<Object,T>{

    Map<Object,T> delegate;
    Multimap<T,Object> reverse;

    @Override protected Map<Object, T> delegate() { return delegate; }

    @Override public void clear() {
        delegate.clear();
        reverse.clear();
    }

    @Override public boolean containsValue(Object value) { return reverse.containsKey(value); }

    @Override public T put(Object key, T value) {
        if (containsKey(key) && !get(key).equals(value)) reverse.remove(get(key), key); 
        reverse.put(value, key);
        return delegate.put(key, value);
    }

    @Override public void putAll(Map<? extends Object, ? extends T> m) {
        for (Entry<? extends Object,? extends T> e : m.entrySet()) put(e.getKey(),e.getValue());
    }

    public T remove(Object key) {
        T result = delegate.remove(key);
        reverse.remove(result, key);
        return result;
    }

    public void removeValue(T value) {
        for (Object key : reverse.removeAll(value)) delegate.remove(key);
    }

    public Collection<T> values() {
        return reverse.keySet();
    }   

}

删除是O(键的数量),但其他一切都与典型的地图实现顺序相同(一些额外的常量缩放,因为你还需要添加相反的东西)。

我刚刚使用了Object个密钥(应该可以使用equals()hashCode()的适当实现以及关键区别) - 但您也可以使用更具体的密钥类型。

答案 11 :(得分:0)

让我们看一下项目http://code.google.com/p/multiindexcontainer/wiki/MainPage 这是如何将映射用于JavaBean getter并对索引值执行查找的一般方法。 我想这就是你要找的东西。让我们试一试。

答案 12 :(得分:0)

基本上可以使用基于多个哈希映射的解决方案,但在这种情况下,所有这些都必须手动更新。可以在此处找到一个非常简单的集成解决方案:http://insidecoffe.blogspot.de/2013/04/indexable-hashmap-implementation.html

答案 13 :(得分:0)

以下是我如何实现这一点,现在只需要放置,删除和获取方法,您需要覆盖所需的方法。

示例:

MultiKeyMap<MultiKeyMap.Key,String> map = new MultiKeyMap<>();
MultiKeyMap.Key key1 = map.generatePrimaryKey("keyA","keyB","keyC");
MultiKeyMap.Key key2 = map.generatePrimaryKey("keyD","keyE","keyF");

map.put(key1,"This is value 1");
map.put(key2,"This is value 2");

Log.i("MultiKeyMapDebug",map.get("keyA"));
Log.i("MultiKeyMapDebug",map.get("keyB"));
Log.i("MultiKeyMapDebug",map.get("keyC"));

Log.i("MultiKeyMapDebug",""+map.get("keyD"));
Log.i("MultiKeyMapDebug",""+map.get("keyE"));
Log.i("MultiKeyMapDebug",""+map.get("keyF"));

输出:

MultiKeyMapDebug: This is value 1
MultiKeyMapDebug: This is value 1
MultiKeyMapDebug: This is value 1
MultiKeyMapDebug: This is value 2
MultiKeyMapDebug: This is value 2
MultiKeyMapDebug: This is value 2

MultiKeyMap.java:

/**
 * Created by hsn on 11/04/17.
 */


public class MultiKeyMap<K extends MultiKeyMap.Key, V> extends HashMap<MultiKeyMap.Key, V> {

    private Map<String, MultiKeyMap.Key> keyMap = new HashMap<>();

    @Override
    public V get(Object key) {
        return super.get(keyMap.get(key));
    }

    @Override
    public V put(MultiKeyMap.Key key, V value) {
        List<String> keyArray = (List<String>) key;
        for (String keyS : keyArray) {
            keyMap.put(keyS, key);
        }
        return super.put(key, value);
    }

    @Override
    public V remove(Object key) {
        return super.remove(keyMap.get(key));
    }

    public Key generatePrimaryKey(String... keys) {
        Key singleKey = new Key();
        for (String key : keys) {
            singleKey.add(key);
        }
        return singleKey;
    }

    public class Key extends ArrayList<String> {

    }

}