【问题标题】:Why is String.equalsIgnoreCase is so slow为什么 String.equalsIgnoreCase 这么慢
【发布时间】:2014-07-16 02:24:29
【问题描述】:

我在面试中遇到了一个问题,要写一个方法来检查相似词,而不考虑字符大小写。

我通过使用每对字符的 ASCII 值的差异来回答它。但是在家里,当我在 String.class 中进行它的实际实现时,我感到很不安 - 为什么它是这样实现的!

我试图在内置方法和我的自定义方法之间进行比较,这种方式-

public class EqualsIgnoreCase {

    public static void main(String[] args) {
        String str1 = "Srimant @$ Sahu 959s";
        String str2 = "sriMaNt @$ sAhu 959s";

        System.out.println("Avg millisecs with inbuilt () - " + averageOfTenForInbuilt(str1, str2));
        System.out.println("\nAvg millisecs with custom () - " + averageOfTenForCustom(str1, str2));
    }

    public static int averageOfTenForInbuilt(String str1, String str2) {
        int avg = 0;
        for (int itr = 0; itr < 10; itr++) {
            long start1 = System.currentTimeMillis();
            for (int i = 0; i < 100000; i++) {
                str1.equalsIgnoreCase(str2);
            }
            avg += System.currentTimeMillis() - start1;
        }
        return avg / 10;
    }

    public static int averageOfTenForCustom(String str1, String str2) {
        int avg = 0;
        for (int itr = 0; itr < 10; itr++) {
            long start2 = System.currentTimeMillis();
            for (int i = 0; i < 100000; i++) {
                isEqualsIgnoreCase(str1, str2);
            }
            avg += System.currentTimeMillis() - start2;
        }
        return avg / 10;
    }

    public static boolean isEqualsIgnoreCase(String str1, String str2) {
        int length = str1.length();
        if (str2.length() != length) {
            return false;
        }

        for (int i = 0; i < length; i++) {
            char ch1 = str1.charAt(i);
            char ch2 = str2.charAt(i);

            int val = Math.abs(ch1 - ch2);
            if (val != 0) {
                if (isInAlphabetsRange(ch1, ch2)) {
                    if (val != 32) {
                        return false;
                    }
                } else {
                    return false;
                }
            }
        }
        return true;
    }

    public static boolean isInAlphabetsRange(char ch1, char ch2) {
        return (((ch1 <= 122 && ch1 >= 97) || (ch1 <= 90 && ch1 >= 65)) && ((ch2 <= 122 && ch2 >= 97) || (ch2 <= 90 && ch2 >= 65)));
    }

}

输出-

内置 () 的平均毫秒数 - 14

自定义 () 的平均毫秒数 - 5

我发现内置方法正在提高效率,因为有很多检查和方法调用。 这种实施背后有什么具体原因吗?还是我的逻辑中遗漏了什么?

任何建议,将不胜感激!

【问题讨论】:

  • 尝试先调用averageOfTenForCustom,再调用averageOfTenForInbuilt:由于JVM启动,实际结果可能存在差异。
  • 小写和大写之间的转换不是一件简单的事情。您只使用基本的 ASCII 拉丁字符(字符距离 32)。但是,当您使用整个 Unicode 集时,问题就相当复杂了。 unicode.org/faq/casemap_charprop.html
  • @sp00m 交替调用它们没有显着差异!我之前已经查过了。
  • 还将 0 和 32 的差异视为“匹配”意味着 * (42) 和 J (74) 相等 :-) 所以你会返回匹配 "Jump" 和 "*转"
  • 即使在 ASCII 中,您的代码对于不在 Laitn 字母表中的字符也会运行错误。例如,它会为 "0" 和 "P" 或 "0" 和 "\x10' 返回 true

标签: java string performance ignore-case


【解决方案1】:

您的例程只处理 ASCII 字符。系统一处理所有的unicode字符。

考虑以下示例:

public class Test {

    public static void main(String[] args) {
        System.out.println((int) 'ě'); // => 283
        System.out.println((int) 'Ě'); // => 282 
    }

}

【讨论】:

  • @53by97 小心过早和幼稚的优化。我很确定无论您的代码做什么,尝试优化 equalsIgnoreCase 都不会带来任何显着的改进。
  • @53by97:“但在几乎所有情况下,我们都不会经常遇到 Unicode”——你多么与世隔绝。这在美国部分地区可能是正确的;但这在整个世界上是完全不真实的。您的玩具实现可能适合您需要它的特定狭窄领域,但库设计人员需要更健壮地构建他们的代码。
  • @53by97 我的一位教授曾经说过“如果它不必是正确的,我可以让它任意快”。真的,任何天真到认为英语只包含 ASCII 字符的人都会大吃一惊;)
  • 即使是英文也有很多带有变音符号的字符,比如上面的naïve这个词,或者ressumé、exposé...
  • if (str2.length() != length) 还有一个错误的快捷方式。 "weißbier".length() != "WEISSBIER".length().
【解决方案2】:

您的方法在很多方面都不正确。例如,它认为“!”等于“B”,“B”等于“1”,但是“!”不等于“1”(因此它不像我们期望的 equals 方法那样具有传递性)。

是的,为该方法编写错误的实现是很容易的,它既快速又简单。一个公平的挑战是编写一个正确的,即正确处理 JDK 实现所做的所有参数。

您可能还希望查看How do I write a correct micro-benchmark in Java? 以获得更可靠的性能测量结果。

【讨论】:

  • +1 - 让我想起了我曾经读过的一个讨论:“a:啊,但是你的方法比我的慢一倍”。 "b: 如果允许我返回不正确的结果,我可以让我的方法在恒定时间内运行"
  • @LievenKeersmaekers 请检查编辑后的代码,它占字母范围!
  • 好的,现在您正在处理 ASCII。其他字符集(例如 ISO-8869-1,更不用说 Unicode)中的字符呢?
【解决方案3】:

这可能不是唯一的原因,但您的解决方案实际上并不适用于所有可能的字符串这一事实绝对是一个因素。

在某些(烦人的)语言环境中,两个字符可能具有相同的大写字母但不同的小写字母。出于这个原因,为了工作(大多数情况下,请参阅土耳其语),规范实现必须比较字符串的小写和大写 char-for-char。

您的实现可能在 99% 的情况下都是完美的,特别是如果您只需要处理英语语言环境,但不幸的是核心库实现不能做出这样的假设。

【讨论】:

  • “两个小写,一个大写”的问题在土耳其语中不存在,但在希腊语 (sigma) 中存在。相比之下,土耳其语相当简单,只是没有 I 作为大写 i。
  • 抱歉,我的意思是 equalsToIgnoreCase 无法在某些特定语言环境下工作(例如土耳其语,因为 i 问题),并不是说它遇到了小写/大写问题 - 虽然我可以看到我在表达我的句子的方式中并不清楚这一点。感谢您提供该问题的实际示例:我在理智上知道该问题,但以前从未真正见过它。
【解决方案4】:

我认为检查

String1.equalsIgnoreCase(String2)

提供的字符接受度要好得多,它接受 Unicode 中包含的所有类型的字符值;但是;您试图通过自定义代码弄清楚的是您只比较英文字母字符。

所以,我认为,根据您帖子的评论员 Pavel Horel 的说法,由于它提供了各种 Unicode 字符之间比较的复杂性,因此可能需要更多时间.

【讨论】:

  • 我认为如果 String Class/methods 的创建者创建了 2 个单独/重载的方法来区分它们会更好,这不会影响性能。
  • 我认为如果您学会仅在应用程序中的(可测量的)问题时才考虑性能会更好。如果高性能 ASCII 字符串操作对应用程序至关重要,那么一开始就不会选择 java.lang.String。请了解premature optimization。除此之外,较小的 API 总是更好的设计,除非您有充分的理由不这样做。
  • @ignis,OP 在 1)收到面试问题,2)回答,3)回家并更彻底地研究问题之后来到这里。他并没有试图过早地优化一些生产代码,而是试图了解为什么这个库是这样的。
  • @BrianS 这仍然是过早的优化,因为他还没有测量和验证 equalsIgnoreCase 的现有实现是实际应用程序中的一个重要性能瓶颈。
  • @53by97 是的,这是对 Java 的 String 类非常普遍的批评。 String 应该是具有 2 个子类(ASCII 和 UTF-8)的抽象类。现在,它是一个无法扩展的final UTF-16 类。哦,好吧,这对大多数应用程序来说都没有关系。
【解决方案5】:

我认为 String.java 的这段摘录是相关的:

if (ignoreCase) {
    // If characters don't match but case may be ignored,
    // try converting both characters to uppercase.
    // If the results match, then the comparison scan should
    // continue.
    char u1 = Character.toUpperCase(c1);
    char u2 = Character.toUpperCase(c2);
    if (u1 == u2) {
        continue;
    }
    // Unfortunately, conversion to uppercase does not work properly
    // for the Georgian alphabet, which has strange rules about case
    // conversion.  So we need to make one last check before
    // exiting.
    if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
        continue;
    }
}

【讨论】:

    猜你喜欢
    • 2021-09-03
    • 2016-09-28
    • 2020-02-08
    • 2012-07-17
    • 2011-11-07
    • 2015-08-24
    • 2013-08-06
    • 2011-01-02
    • 2019-06-16
    相关资源
    最近更新 更多