问题 检查String是否可以在Java中解析为Double的最快方法


我知道有一百万种方法可以做到这一点,但最快的是什么?这应该包括科学记数法。

注意:我对将值转换为Double不感兴趣,我只想知道它是否可能。即 private boolean isDouble(String value)


11819
2017-12-19 17:14


起源

AFAIK,在它上面执行一个Double.parseDouble(String),如果它不以数字开头,则抛出异常。 (在这里概括)。如果你想做regExs并删除那些不同故事的主要非数字字符。 - Rcunn87
好吧,AFAIK,try-catch往往相当慢。 - JHollanti
我将在正则表达式的想法上第二次使用Rcunn87,但请确保将其编译并静态存储,以便您可以一次又一次地重复使用它。 - Thomas Dignan
@JHollanti肯定是,我想这里的某些人是否在考虑“开发时间”而不是CPU时间。 - Thomas Dignan
@JHollanti相当慢可能仍然足够快。 - helpermethod


答案:


您可以使用Double类使用的相同正则表达式来检查它。这里有很好的记录:

http://docs.oracle.com/javase/6/docs/api/java/lang/Double.html#valueOf%28java.lang.String%29

这是代码部分:

为了避免在无效字符串上调用此方法并抛出NumberFormatException,可以使用下面的正则表达式来筛选输入字符串:

  final String Digits     = "(\\p{Digit}+)";
  final String HexDigits  = "(\\p{XDigit}+)";

        // an exponent is 'e' or 'E' followed by an optionally 
        // signed decimal integer.
        final String Exp        = "[eE][+-]?"+Digits;
        final String fpRegex    =
            ("[\\x00-\\x20]*"+  // Optional leading "whitespace"
             "[+-]?(" + // Optional sign character
             "NaN|" +           // "NaN" string
             "Infinity|" +      // "Infinity" string

             // A decimal floating-point string representing a finite positive
             // number without a leading sign has at most five basic pieces:
             // Digits . Digits ExponentPart FloatTypeSuffix
             // 
             // Since this method allows integer-only strings as input
             // in addition to strings of floating-point literals, the
             // two sub-patterns below are simplifications of the grammar
             // productions from the Java Language Specification, 2nd 
             // edition, section 3.10.2.

             // Digits ._opt Digits_opt ExponentPart_opt FloatTypeSuffix_opt
             "((("+Digits+"(\\.)?("+Digits+"?)("+Exp+")?)|"+

             // . Digits ExponentPart_opt FloatTypeSuffix_opt
             "(\\.("+Digits+")("+Exp+")?)|"+

       // Hexadecimal strings
       "((" +
        // 0[xX] HexDigits ._opt BinaryExponent FloatTypeSuffix_opt
        "(0[xX]" + HexDigits + "(\\.)?)|" +

        // 0[xX] HexDigits_opt . HexDigits BinaryExponent FloatTypeSuffix_opt
        "(0[xX]" + HexDigits + "?(\\.)" + HexDigits + ")" +

        ")[pP][+-]?" + Digits + "))" +
             "[fFdD]?))" +
             "[\\x00-\\x20]*");// Optional trailing "whitespace"

  if (Pattern.matches(fpRegex, myString))
            Double.valueOf(myString); // Will not throw NumberFormatException
        else {
            // Perform suitable alternative action
        }

6
2017-12-19 17:28



实际上在我的情况下,最快的解决方案就是if-else通过整个String使用flags和whatnots。但那是因为在我的情况下,String通常是非常小的(如3或4个字符)。但作为一般解决方案,我认为这是最好的。 - JHollanti


有一个方便的 NumberUtils#isNumber 在 Apache Commons Lang。这有点牵强:

有效数字包括用0x限定符标记的十六进制,科学记数法和用类型限定符标记的数字(例如123L)。

但我想它可能比正则表达式或抛出和捕获异常更快。


5
2017-12-19 17:25



你看过那个方法的源代码了吗?我不明白为什么它会比正则表达式更快 - 它是一个混乱的循环,比较,标志......可能是在正则引擎盖下发生的事情,但看起来肯定是丑陋的。 - Paul
@Paul:我快速看一下(我现在后悔;-))但只要它有效,我就不在乎了。我也不知道它是否比正则表达式更快。请记住,正则表达式是一个动态生成的状态机(尽管可能是 非常 优化)。 - Tomasz Nurkiewicz


Apache Commons NumberUtil实际上非常快。我猜它比任何一个都快 正则表达式实现。


2
2018-03-02 10:14



你能提供一个基准来代替这个猜测吗? - joergl
我也明白了 isDigits 和 isNumber 在 org.apache.commons.lang.math.NumberUtils,但没有什么可检查的 isDouble。那么你建议用什么方法? - David Dossot
isNumber检查所有数字(查看文档...) Valid numbers include hexadecimal marked with the 0x qualifier, scientific notation and numbers marked with a type qualifier (e.g. 123L) - Seega
如果您只需要双打,那么您不希望验证器对所有其他类型的数字都返回true。 - Asu


我使用以下代码来检查字符串是否可以解析为double:

public static boolean isDouble(String str) {
    if (str == null) {
        return false;
    }
    int length = str.length();
    if (length == 0) {
        return false;
    }
    int i = 0;
    if (str.charAt(0) == '-') {
        if (length == 1) {
            return false;
        }
        ++i;
    }
    int integerPartSize = 0;
    int exponentPartSize = -1;
    while (i < length) {
        char c = str.charAt(i);
        if (c < '0' || c > '9') {
            if (c == '.' && integerPartSize > 0 && exponentPartSize == -1) {
                exponentPartSize = 0;
            } else {
                return false;
            }
        } else if (exponentPartSize > -1) {
            ++exponentPartSize;
        } else {
            ++integerPartSize;
        }
        ++i;
    }
    if ((str.charAt(0) == '0' && i > 1 && exponentPartSize < 1)
            || exponentPartSize == 0 || (str.charAt(length - 1) == '.')) {
        return false;
    }
    return true;
}

我知道输出与Double类中的正则表达式不完全相同,但这种方法更快,结果足以满足我的需要。这些是我对该方法的单元测试。

@Test
public void shouldReturnTrueIfStringIsDouble() {
    assertThat(Utils.isDouble("0.0")).isTrue();
    assertThat(Utils.isDouble("0.1")).isTrue();
    assertThat(Utils.isDouble("-0.0")).isTrue();
    assertThat(Utils.isDouble("-0.1")).isTrue();
    assertThat(Utils.isDouble("1.0067890")).isTrue();
    assertThat(Utils.isDouble("0")).isTrue();
    assertThat(Utils.isDouble("1")).isTrue();
}

@Test
public void shouldReturnFalseIfStringIsNotDouble() {
    assertThat(Utils.isDouble(".01")).isFalse();
    assertThat(Utils.isDouble("0.1f")).isFalse();
    assertThat(Utils.isDouble("a")).isFalse();
    assertThat(Utils.isDouble("-")).isFalse();
    assertThat(Utils.isDouble("-1.")).isFalse();
    assertThat(Utils.isDouble("-.1")).isFalse();
    assertThat(Utils.isDouble("123.")).isFalse();
    assertThat(Utils.isDouble("1.2.3")).isFalse();
    assertThat(Utils.isDouble("1,3")).isFalse();
}

2
2018-02-10 15:10



谢谢!我实现了这种方法,而不是reg exp版本,并且性能有了很大的提升。使用java profiler我可以看到,我已经从使用你的reg exp isDouble函数到97ms的调用后的27,000ms - 调用相同的数量。 - Martin Holland


我认为尝试将其转换为double并捕获异常将是检查的最快方式...我能想到的另一种方式是将字符串拆分为句点('。')然后检查每个部分split数组只包含整数...但我认为第一种方式会更快


0
2017-12-19 17:20



如何快速投掷和捕捉?更不用说不好的做法了?使用句点不是语言环境安全。 - JHollanti


我尝试了下面的代码块,似乎更快地抛出异常

String a = "123f15512551";
        System.out.println(System.currentTimeMillis());
        a.matches("^\\d+\\.\\d+$");
        System.out.println(System.currentTimeMillis());

        try{
            Double.valueOf(a);
        }catch(Exception e){
            System.out.println(System.currentTimeMillis());
        }

输出:

1324316024735
1324316024737
1324316024737

0
2017-12-19 17:34



你不能依靠它做一次来确定一个基准。可能发生的变化太大,而且你不知道毫秒钟的分辨率。 - corsiKa
@glowcoder你是太多可能的变化,也可能是硬件。关于milli`s:自1970年1月1日起,它不是一个很长的价值,包括所有的millis? - HRgiger
@glowcoder说的话 - 用预先编译的模式做了一百万次并回到我们身边。 - Paul
尝试使用 System.nanoTime() 代替 currentTimeMillis()。 - Paul
是的,在爪哇,它是毫米的时代。但这不是我所说的决议。考虑以下: ideone.com/KOOP3 请注意时间毫米是如何上升1?现在复制该代码并在您的机器上运行它。在我的上面,它们每蜱上升15-16。 - corsiKa


不应该将异常用于流控制,尽管Java的作者很难不使用它 NumberFormatException 那样。

班上 java.util.Scanner 有一个方法 hasNextDouble 检查一下 String 可以读作双。

在引擎盖下 Scanner 使用正则表达式(通过预编译模式)来确定是否 String 可以转换为整数或浮点数。模式在该方法中编译 buildFloatAndDecimalPattern 你可以在哪里查看 GrepCode在这里

预编译模式具有比使用try / catch块更快的额外好处。

以下是GrepCode有一天消失的上述方法:

private void buildFloatAndDecimalPattern() {
    // \\p{javaDigit} may not be perfect, see above
    String digit = "([0-9]|(\\p{javaDigit}))";
    String exponent = "([eE][+-]?"+digit+"+)?";
    String groupedNumeral = "("+non0Digit+digit+"?"+digit+"?("+
                            groupSeparator+digit+digit+digit+")+)";
    // Once again digit++ is used for performance, as above
    String numeral = "(("+digit+"++)|"+groupedNumeral+")";
    String decimalNumeral = "("+numeral+"|"+numeral +
        decimalSeparator + digit + "*+|"+ decimalSeparator +
        digit + "++)";
    String nonNumber = "(NaN|"+nanString+"|Infinity|"+
                           infinityString+")";
    String positiveFloat = "(" + positivePrefix + decimalNumeral +
                        positiveSuffix + exponent + ")";
    String negativeFloat = "(" + negativePrefix + decimalNumeral +
                        negativeSuffix + exponent + ")";
    String decimal = "(([-+]?" + decimalNumeral + exponent + ")|"+
        positiveFloat + "|" + negativeFloat + ")";
    String hexFloat =
        "[-+]?0[xX][0-9a-fA-F]*\\.[0-9a-fA-F]+([pP][-+]?[0-9]+)?";
    String positiveNonNumber = "(" + positivePrefix + nonNumber +
                        positiveSuffix + ")";
    String negativeNonNumber = "(" + negativePrefix + nonNumber +
                        negativeSuffix + ")";
    String signedNonNumber = "(([-+]?"+nonNumber+")|" +
                             positiveNonNumber + "|" +
                             negativeNonNumber + ")";
    floatPattern = Pattern.compile(decimal + "|" + hexFloat + "|" +
                                   signedNonNumber);
    decimalPattern = Pattern.compile(decimal);
}

0
2017-12-19 17:41