HashMap与Switch语句性能

时间:2015-01-16 22:29:43

标签: java hashmap switch-statement

HashMap本质上具有O(1)性能,而开关状态可以具有O(1)或O(log(n)),具体取决于编译器是使用tableswitch还是查找开关。

可以理解,如果switch语句是这样编写的,

switch (int) {
    case 1:
    case 2:
    case 3:
    case 4:
    default:
}
然后它会使用一个tableswitch,并且显然比标准的HashMap具有性能优势。但是如果switch语句稀疏怎么办?这将是我要比较的两个例子:

HashMap<Integer, String> example = new HashMap<Integer, String>() {{
        put(1, "a");
        put(10, "b");
        put(100, "c");
        put(1000, "d");
}};

switch (int) {
    case 1:
        return "a";
    case 10:
        return "b";
    case 100:
        return "c";
    case 1000:
        return "d";
    default:
        return null;
}

什么能提供更多吞吐量,查找切换或HashMap? HashMap的开销是否会使lookupswitch尽早获得优势,但随着案例/条目数量的增加最终逐渐减少?

编辑:我尝试了一些使用JMH的基准测试,这里是我使用的结果和代码。 https://gist.github.com/mooman219/bebbdc047889c7cfe612 正如你们提到的,lookupswitch语句的表现优于HashTable。我仍然想知道为什么。

5 个答案:

答案 0 :(得分:22)

取决于:

  1. 如果有一些项目|固定物品。如果可以,使用开关(最坏情况为O(n))

  2. 如果有很多项目,或者你想在不修改多少代码的情况下添加未来的项目---&gt;使用hash-map(访问时间被视为常量时间)

  3. 针对您的情况。您不应该担心性能,因为不同的执行时间非常短。只关注代码的可读性/可维护性。是否值得优化一个简单的案例以提高几纳秒?

答案 1 :(得分:9)

这里接受的答案是错误的。

http://java-performance.info/string-switch-implementation/

切换速度始终与哈希映射一样快。 Switch语句将转换为直接查找表。对于整数值(ints,enums,shorts和longs),它是对语句的直接查找/ jmp。没有其他哈希需要发生。对于字符串,它将为case语句预先计算字符串哈希,并使用输入的字符串的哈希码来确定跳转到的位置。如果发生碰撞,它将执行if / else链。现在您可能会认为“这与HashMap相同,对吧?”但这不是事实。查找的哈希码是在编译时计算的,并且不会根据元素的数量而减少(冲突的可能性较小)。

开关具有O(1)查找,而不是O(n)。 (好吧,实际上对于少量项目,将开关转换为if / else语句。这提供了更好的代码局部性,并避免了额外的内存查找。但是,对于许多项目,开关被更改为我上面提到的查找表)。

您可以在此处了解更多信息 How does Java's switch work under the hood?

答案 2 :(得分:1)

TL/DR

基于代码可读性和可维护性。两者的成本都是 O(1) 并且几乎没有区别(尽管切换通常会稍微快一点)。

在这种特殊情况下,映射会更快,因为开关返回一个地址,然后必须转到该地址以识别返回值。 (一个罕见的例子)。如果您的 switch 只是调用函数,那么 Map 也会更快。

为了加快速度,我会确保使用数字大小写,并避免通过常量或枚举器(打字稿)使用字符串。

(已编辑)我按预期确认:How does Java's switch work under the hood? 带开关。

更详细的答案

杂草丛生:

switch 语句通常会获得更高的性能。它创建一个查找表并转到引用并从该点开始。不过也有例外。

当你使用一个简单的开关时,比如 return map.get(x) vs. switch(1=>'a', 2=>'b', etc)。那是因为映射可以直接返回所需的值,开关将停止映射地址并继续直到中断或结束。

无论如何,它们的执行成本应该非常相似。

考虑代码的可维护性和可读性

使用地图解耦数据,这可以获得动态创建“切换”案例的好处。详情如下。

如果您需要处理多个复杂的功能/流程,如果您改用地图,可能会更容易读/写。特别是当 switch 语句开始超过 20 或 30 个选项时。

个人使用的地图案例

一段时间以来,我一直在 React 应用程序中使用以下通量模式(Redux/useReducer)。

我创建了一个中央映射,我将触发器映射为键,值是一个功能引用。然后,我可以在有意义的时间和地点加载案例。

最初我使用它来分解用例以减少文件大小并将类似功能的用例以更有条理的方式组合在一起。虽然后来我把它发展成在域中加载并在域钩子中配置事件和数据,例如 useUser、useFeedback、useProfile 等...

这样做使我能够将默认状态、初始化函数、事件等创建到一个逻辑文件结构中,它还允许我在需要之前保持较低的占用空间。

注意事项

使用地图不允许掉线,尽管大多数人认为这种代码有异味。同时防止意外跌落。

答案 3 :(得分:0)

在您的情况下,由于您的Integer使用了HashMap密钥,而int语句使用了简单的“switch”,因此效果最佳的实施方案将是switch语句,除非通过此部分代码的次数非常高(数十或数十万)。

答案 4 :(得分:0)

如果有这样的示例,我将使用Guava ImmutableMaps(确保您也可以使用Java 9构建器)。

private static final Map<String, String> EXAMPLE = ImmutableMaps.<String, String>>builder()
    .put("a", "100")
    .put("b", "200")
    .build(); 

这样,它们是不可变的,并且只能初始化一次。

有时候我会这样使用策略模式:

private static final Map<String, Command> EXAMPLE = ImmutableMaps.<String, String>>builder()
    .put("a", new SomethingCool())
    .put("b", new BCool())
    .build(); 

private static final Command DEFAULT= new DefCommand();

使用:

EXAMPLE.getOrDefault("a", DEFAULT).execute(); //java 8

关于性能,请选择可读性。您稍后(一年后)会感谢我:D。