Java: If vs. Switch

17
我有一段代码,其中的a)我只是为了可读性而替换成b)。

b)

我有一段代码,我将其中的a)纯粹出于可读性的考虑替换为b)。
if ( WORD[ INDEX ] == 'A' ) branch = BRANCH.A;
/* B through to Y */
if ( WORD[ INDEX ] == 'Z' ) branch = BRANCH.Z;
b)
switch ( WORD[ INDEX ] ) {
    case 'A' : branch = BRANCH.A; break;
    /* B through to Y */
    case 'Z' : branch = BRANCH.Z; break;
}

...开关版本是否会穿过所有排列组合或跳转到一个case?



编辑:

以下一些答案涉及上述方法的替代方法。
我包括以下内容以提供其使用背景。

我之所以提出上面的问题,是因为添加单词的速度在经验上得到了改善。

这绝不是生产代码,并且很快就被拼凑在一起作为PoC。

以下似乎确认了一个思想实验失败。
虽然我可能需要比我目前使用的更大的单词语料库。
失败的原因是我没有考虑到空引用仍然需要内存。(做错了!)

public class Dictionary {
    private static Dictionary ROOT;
    private boolean terminus;
    private Dictionary A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z;
    private static Dictionary instantiate( final Dictionary DICTIONARY ) {
        return ( DICTIONARY == null ) ? new Dictionary() : DICTIONARY;
    }
    private Dictionary() {
        this.terminus = false;
        this.A = this.B = this.C = this.D = this.E = this.F = this.G = this.H = this.I = this.J = this.K = this.L = this.M = this.N = this.O = this.P = this.Q = this.R = this.S = this.T = this.U = this.V = this.W = this.X = this.Y = this.Z = null;
    }
    public static void add( final String...STRINGS ) {
        Dictionary.ROOT = Dictionary.instantiate( Dictionary.ROOT );
        for ( final String STRING : STRINGS ) Dictionary.add( STRING.toUpperCase().toCharArray(), Dictionary.ROOT , 0, STRING.length() - 1 );
    }
    private static void add( final char[] WORD, final Dictionary BRANCH, final int INDEX, final int INDEX_LIMIT ) {
        Dictionary branch = null;
        switch ( WORD[ INDEX ] ) {
        case 'A' : branch = BRANCH.A = Dictionary.instantiate( BRANCH.A ); break;
        case 'B' : branch = BRANCH.B = Dictionary.instantiate( BRANCH.B ); break;
        case 'C' : branch = BRANCH.C = Dictionary.instantiate( BRANCH.C ); break;
        case 'D' : branch = BRANCH.D = Dictionary.instantiate( BRANCH.D ); break;
        case 'E' : branch = BRANCH.E = Dictionary.instantiate( BRANCH.E ); break;
        case 'F' : branch = BRANCH.F = Dictionary.instantiate( BRANCH.F ); break;
        case 'G' : branch = BRANCH.G = Dictionary.instantiate( BRANCH.G ); break;
        case 'H' : branch = BRANCH.H = Dictionary.instantiate( BRANCH.H ); break;
        case 'I' : branch = BRANCH.I = Dictionary.instantiate( BRANCH.I ); break;
        case 'J' : branch = BRANCH.J = Dictionary.instantiate( BRANCH.J ); break;
        case 'K' : branch = BRANCH.K = Dictionary.instantiate( BRANCH.K ); break;
        case 'L' : branch = BRANCH.L = Dictionary.instantiate( BRANCH.L ); break;
        case 'M' : branch = BRANCH.M = Dictionary.instantiate( BRANCH.M ); break;
        case 'N' : branch = BRANCH.N = Dictionary.instantiate( BRANCH.N ); break;
        case 'O' : branch = BRANCH.O = Dictionary.instantiate( BRANCH.O ); break;
        case 'P' : branch = BRANCH.P = Dictionary.instantiate( BRANCH.P ); break;
        case 'Q' : branch = BRANCH.Q = Dictionary.instantiate( BRANCH.Q ); break;
        case 'R' : branch = BRANCH.R = Dictionary.instantiate( BRANCH.R ); break;
        case 'S' : branch = BRANCH.S = Dictionary.instantiate( BRANCH.S ); break;
        case 'T' : branch = BRANCH.T = Dictionary.instantiate( BRANCH.T ); break;
        case 'U' : branch = BRANCH.U = Dictionary.instantiate( BRANCH.U ); break;
        case 'V' : branch = BRANCH.V = Dictionary.instantiate( BRANCH.V ); break;
        case 'W' : branch = BRANCH.W = Dictionary.instantiate( BRANCH.W ); break;
        case 'X' : branch = BRANCH.X = Dictionary.instantiate( BRANCH.X ); break;
        case 'Y' : branch = BRANCH.Y = Dictionary.instantiate( BRANCH.Y ); break;
        case 'Z' : branch = BRANCH.Z = Dictionary.instantiate( BRANCH.Z ); break;
        }   
        if ( INDEX == INDEX_LIMIT ) branch.terminus = true;
        else Dictionary.add( WORD, branch, INDEX + 1, INDEX_LIMIT );
    }
    public static boolean is( final String STRING ) {
        Dictionary.ROOT = Dictionary.instantiate( Dictionary.ROOT );
        return Dictionary.is( STRING.toUpperCase().toCharArray(), Dictionary.ROOT, 0, STRING.length() - 1 );
    }
    private static boolean is( final char[] WORD, final Dictionary BRANCH, final int INDEX, final int INDEX_LIMIT ) {
        Dictionary branch = null;
        switch ( WORD[ INDEX ] ) {
        case 'A' : branch = BRANCH.A; break;
        case 'B' : branch = BRANCH.B; break;
        case 'C' : branch = BRANCH.C; break;
        case 'D' : branch = BRANCH.D; break;
        case 'E' : branch = BRANCH.E; break;
        case 'F' : branch = BRANCH.F; break;
        case 'G' : branch = BRANCH.G; break;
        case 'H' : branch = BRANCH.H; break;
        case 'I' : branch = BRANCH.I; break;
        case 'J' : branch = BRANCH.J; break;
        case 'K' : branch = BRANCH.K; break;
        case 'L' : branch = BRANCH.L; break;
        case 'M' : branch = BRANCH.M; break;
        case 'N' : branch = BRANCH.N; break;
        case 'O' : branch = BRANCH.O; break;
        case 'P' : branch = BRANCH.P; break;
        case 'Q' : branch = BRANCH.Q; break;
        case 'R' : branch = BRANCH.R; break;
        case 'S' : branch = BRANCH.S; break;
        case 'T' : branch = BRANCH.T; break;
        case 'U' : branch = BRANCH.U; break;
        case 'V' : branch = BRANCH.V; break;
        case 'W' : branch = BRANCH.W; break;
        case 'X' : branch = BRANCH.X; break;
        case 'Y' : branch = BRANCH.Y; break;
        case 'Z' : branch = BRANCH.Z; break;
        }
        if ( branch == null ) return false;
        if ( INDEX == INDEX_LIMIT ) return branch.terminus;
        else return Dictionary.is( WORD, branch, INDEX + 1, INDEX_LIMIT );
    }
}

你需要在其中加入一些断点。 - Mike Daniels
我的回答可能会对相关问题有所帮助:https://dev59.com/8XRC5IYBdhLWcg3wUfN2#338230 - erickson
@MikeDaniels:修复了遗漏的断点,这是一种转录遗漏。 - Ande Turner
@erickson:干杯...让我添加了一条关于在Java中切换String的帖子的评论http://blog.bpsite.net/item/71/Switch%20on%20String%20in%20Java.html - Ande Turner
我知道你的代码没有这样做,但在其他情况下很容易发生:如果你的代码有副作用(比如修改INDEX),那么使用If方法可能会运行多个分支,而Switch则不会。我认为Switch更好地传达了意图,应该被使用(或者像人们建议的字典方法)。 - Erich Mirabal
11个回答

24

不要担心性能问题;使用最能表达你的意图的语法。只有当你已经(a)证明了有性能缺陷;并且(b)把它定位到特定的例程中,然后才需要担心性能。对我来说,这里使用 case 语法更加合适。


23
在字节码中,有两种形式的switch语句:tableswitchlookupswitch。其中一种假设键是密集的,而另一种则是稀疏的。请参见JVM规范中编译switch语句的描述。对于枚举类型,首先找到序数,然后继续执行int类型的case语句。我不确定JDK7中提出的String类型的switch语句将如何实现。
然而,通常会在任何明智的JVM中编译频繁使用的代码。优化器并非完全愚蠢。不要担心它,遵循通常的优化启发式方法即可。

@TomHawtin:谢谢你,汤姆。你的评论一如既往地非常有价值。当我计时我的方法时,我正在寻找一些加速的原因。关于在字符串上切换http://blog.bpsite.net/item/71/Switch%20on%20String%20in%20Java.html 我发现这是有用的阅读,我曾经编写过一段代码,以Java惯用方式从字符串版本中元编码枚举版本。也许它将如何实现? - Ande Turner
1
尝试使用现代的Java编译器及其javap功能进行实验,字符串开关是通过对字符串的哈希码进行lookupswitch操作实现的,然后对每种情况使用String.equals()函数来生成一个int ID。该ID随后通常会在第二个lookupswitch中使用。 - p00ya

7

看起来您已经列举了这些值,也许需要进行枚举?

enum BRANCH {
  A,B, ... Y,Z;
}

然后在您的代码中:
BRANCH branch = BRANCH.valueOf( WORD[ INDEX ] );

此外,你的代码可能存在一个bug,因为"A" == "A"的结果可能是false,这取决于"A"的对象标识符。

4

虽然这不是对你问题的完全回答,但是你的代码实际上存在一个错误,在每个case之后应该有一个break:

switch ( WORD[ INDEX ] ) {
    case 'A' : branch = BRANCH.A; break;
    /* B through to Y */
    case 'Z' : branch = BRANCH.Z; break;
}

我认为性能差异在这里并不会太显著,但如果你真的关心性能,并且此代码被频繁执行,这里有另一种选择:

// Warning, untested code.
BRANCH[] branchLookUp = {BRANCH.A, BRANCH.B, ..., BRANCH.Z};

branch = branchLookUp[WORD[INDEX] - 'A'];

但请确保对其进行封装并进行充分的文档记录。


@JackLeow:修复了漏掉的换行符,这是一种转录遗漏。 - Ande Turner
建议使用数组。你应该考虑将BRANCHES存储在数组中,而不是拥有26个静态字段。 - notnoop

3
这里有一种避免所有case语句的方法。
import java.util.HashMap;

public class Dictionary {
    private static Dictionary                       ROOT;
    private boolean                                 terminus;
    private final HashMap<Character, Dictionary>    dictionaries    = new HashMap<Character, Dictionary>();

    private void ensureBranch(char c) {
        if (getBranch(c) != null)
            return;
        dictionaries.put(c, new Dictionary());
    }

    private Dictionary getBranch(char c) {
        return dictionaries.get(c);
    }

    public static boolean is(final String string) {
        ensureRoot();
        return is(chars(string), ROOT, 0, string.length() - 1);
    }

    public static void add(final String... strings) {
        ensureRoot();
        for (final String string : strings)
            add(chars(string), ROOT, 0, string.length() - 1);
    }

    private static void ensureRoot() {
        if (ROOT == null)
            ROOT = new Dictionary();
    }

    private static char[] chars(final String string) {
        return string.toUpperCase().toCharArray();
    }

    private Dictionary() {
        this.terminus = false;
    }

    private static void add(final char[] word, final Dictionary dictionary, final int index, final int limit) {
        Dictionary branch = getBranch(word, dictionary, index);
        if (index == limit)
            branch.terminus = true;
        else
            add(word, branch, index + 1, limit);
    }

    private static Dictionary getBranch(final char[] word, final Dictionary dictionary, final int index) {
        final char c = word[index];
        dictionary.ensureBranch(c);
        return dictionary.getBranch(c);
    }

    private static boolean is(final char[] word, final Dictionary dictionary, final int index, final int limit) {
        Dictionary branch = dictionary.getBranch(word[index]);
        if (branch == null)
            return false;
        if (index == limit)
            return branch.terminus;
        return is(word, branch, index + 1, limit);
    }
}

@CarlManaster:我进行了一些实验。这会增加约30%的内存使用量,添加新单词的时间略长,但条目验证稍微更快。 - Ande Turner
@Ande:非常感谢你让我(以及我们所有人)知道这一点;我很高兴你采取了实证方法,并且并不惊讶你发现了小差异。我觉得内存使用增加有点令人惊讶;你可能可以找到一个合适的哈希表初始容量来稍微降低一下内存使用率。 - Carl Manaster
@CarlManaster:在原始版本中使用了18410184字节,在使用Hashmap时使用了25296416字节。我使用的是Sun在其教程中提供的“dictionary.txt”,它在磁盘上占用了622783字节。 - Ande Turner

3
老实说,在这种情况下,我认为性能并不重要。这真的取决于编译器及其优化。

3
如果你有一个带有连续整数值的switch语句,根据语言不同,它可能会被优化为分支表,非常快速。无论如何,它不会更慢!
此外,使用if/else if会比if更好,对于这样的情况,其中你的cases是相互排斥的。匹配A之后再进行25次检查是没有意义的。
但基本上,任何性能差异都是微不足道的,你应该使用最正确的语法,而在这种情况下,最正确的语法是switch语句。但是一定要用break来分隔你的cases。

2
我知道这并不是你所询问的,但你只是在这样做吗?
public class Dictionary {
    private static final Set<String> WORDS = new HashSet<String>();

    public static void add(final String... STRINGS) {
        for (String str : STRINGS) {
            WORDS.add(str.toUpperCase());
        }
    }

    public static boolean is(final String STRING) {
        return WORDS.contains(STRING.toUpperCase());
    }
}

您是否只是想寻找更加内存高效的解决方案?


@JackLeow:有点无聊... 至于内存效率,你的这个版本获胜了。也许这应该成为一个高尔夫问题。 - Ande Turner

1

switch 应该是对数级别的,而 if 是线性的,假设编译器找不到任何聪明的东西。但是长的 switch 很难阅读,也容易出错 - 如上面所述,你现在有的 switch 没有任何 break,它将会穿过所有的 case。

为什么不预先填充一个 Map,然后只使用 Map.get() 呢?

private static final Map<Char, Whatever> BRANCHES = Collections.unmodifiableMap(new HashMap<Char, Whatever>() {{
    put('A', BRANCH.A);
    ...
    put('Z', BRANCH.Z);
}}

public void getBranch(char[] WORD, int INDEX) {
    return BRANCHES.get(WORD[INDEX]);
}

如上所述,如果BRANCH是一个Enum,这种行为应该正确地在Enum中实现。

(这里的WORDINDEXBRANCH是什么?从名称来看,它们应该是常量,但你无法真正拥有常量数组——内容总是可修改的;创建常量“结构”没有多少意义;基于常量进行条件语句或切换当然也没有多少意义......)


1

switch语句应该使用哈希表来选择要跳转到哪个case。从那里开始,如果没有break语句,每个后续的case也将被执行。例如,对于您的代码,如果您在X上进行切换,它将立即转到X,然后是Y,然后是Z。请参见Java教程


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接