亲宝软件园·资讯

展开

Java判断字符串是否为数字

Alphathur 人气:2

前言

判断一个字符串是否为数字是Java开发中很常见的业务需求,实现这个判断有很多种方式,大体上分为异常处理,正则表达式,数字字符,NumberFormat工具类,外部工具类这五大类,不同类型下的实现方式其实略有不同,那么究竟选择哪种方式才是最好的呢?本文将一一列举出这5类中具体8个方案,并通过丰富的测试用例来并对比这些方案的差异,相信看完本文,你将会有自己的思考。

异常处理

使用异常处理的本质是基于java自身对于字符串定义而实现的,如果我们的字符串能够被转换为数字,那么这个字符串就是数字,如果转换失败则会抛出异常,所以我们如果能够捕获异常,就可以认为它不是数字,这个方案很简单:

    public static boolean isNumeric1(String str) {
        try {
            Double.parseDouble(str);
            return true;
        } catch(Exception e){
            return false;
        }
    }

如果我们的业务只要求判断字符串是否为整数,那么只需要将Double.parseDouble(str);换成Integer.parseInt(str);即可。但是这个方案有个致命缺陷,由于判断失败会抛异常出来,当判断失败的频率比较高,将产生较大的性能损耗。

正则表达式

使用正则表达式也是一种常见的判断方式,以下的正则表达式将判断输入字符串是否为整数或者浮点数,涵盖负数的情况。

    public static boolean isNumeric2(String str) {
        return str != null && str.matches("-?\\d+(\\.\\d+)?");
    }

当然,为了性能考量,这个方法最好优化成以下方式,因为上面的写法每次调用时都会在matches内部间接创建一个Pattern实例。

    private static final Pattern NUMBER_PATTERN = Pattern.compile("-?\\d+(\\.\\d+)?");
    public static boolean isNumeric2(String str) {
        return str != null && NUMBER_PATTERN.matcher(str).matches();
    }

使用NumberFormat

通常使用NumberFormat类的format方法将一个数值格式化为符合某个国家地区习惯的数值字符串,例如我们输入18,希望输出18¥,使用这个类再好不过了,这里可以了解它的具体用法。但是也可以用该类的parse方法来判断输入字符串是否为数字。

    public static boolean isNumeric3(String str) {
        if (str == null) return false;
        NumberFormat formatter = NumberFormat.getInstance();
        ParsePosition pos = new ParsePosition(0);
        formatter.parse(str, pos);
        return str.length() == pos.getIndex();
    }

数字字符

字符串的底层实现其实就是字符数组,如果这个字符数组中每个字符都是数字,那么这个字符串不就是数字字符串了吗?利用java.lang.Character#isDigit(int)判断所有字符是否为数字字符从而达到判断数字字符串的目的:

    public static boolean isNumeric4(String str) {
        if (str == null) return false;
        for (char c : str.toCharArray ()) {
            if (!Character.isDigit(c)) return false;
        }
        return true;
    }

如果你的java版本是8以上,以上的写法可以替换成如下Stream流的方式,从而看起来更优雅。所以,茴香豆的‘茴’又多了一种写法!

    public static boolean isNumeric4(String str) {
        return str != null && str.chars().allMatch(Character::isDigit);
    }

外部工具类

使用外部工具类通常需要引入外部jar文件,一般的依赖是apache的comons-lang:

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

在使用外部工具类时,我们通常使用NumberUtils或者StringUtils,具体如下:

使用NumberUtils时,我们通常会选择其静态方法isParsable和isCreatable其中的一种,它们的不同点在于isCreatable不仅可以接受十进制数字字符串,还可以接受八进制,十六进制以及科学计数法表示的数字字符串,isParsable只接受十进制数字字符串。

1.NumberUtils.isParsable

    public static boolean isNumeric5(String str) {
        return NumberUtils.isParsable(str);
    }

2.NumberUtils.isCreatable

    public static boolean isNumeric6(String str) {
        return NumberUtils.isCreatable(str);
    }

如果使用StringUtils,那么要考虑到底该使用isNumeric还是isNumericSpace。二者的唯一差异在于,isNumeric会认为空字符串为非法数字,isNumericSpace则认为空字符串也是数字。

3.StringUtils.isNumeric

    public static boolean isNumeric7(String str) {
        return StringUtils.isNumeric(str);
    }

4.StringUtils.isNumericSpace

    public static boolean isNumeric8(String str) {
        return StringUtils.isNumericSpace(str);
    }

测试并比较

默认情况下,文章中的数字都指的是十进制的阿拉伯数字(0 ,1,2 … 9),测试时会扩展范围。考虑以下几个方向:

1)null或者空字符串

2)常规的数字,整数,浮点数以及负数

3)包含非法的字符,例如包含多余的小数点,包含多余的负号,以及其它非常规格式

4)非阿拉伯数字,例如印度数字 १२३,阿拉伯文 ١٢٣,以及罗马数字Ⅰ、Ⅱ、Ⅲ

5)非十进制的数字,例如二进制,八进制,十六进制,科学计数法表示的数字

主体测试方法如下:

    public static void check(String str) {
        System.out.println ( "----------checking:【" + str + "】---------" );
        System.out.println(isNumeric1(str));
        System.out.println(isNumeric2(str));
        System.out.println(isNumeric3(str));
        System.out.println(isNumeric4(str));
        System.out.println(isNumeric5(str));
        System.out.println(isNumeric6(str));
        System.out.println(isNumeric7(str));
        System.out.println(isNumeric8(str));
        System.out.println( "---------------end-------------------" );
    }

测试用例:

    public static void main(String[] args) throws ParseException {
        //1)null或者空字符串
        check(null);
        check("");
        check(" ");

        //2)正常的数字,整数或者浮点数
        check("123");
        check("0.123");
        check("-12.3");
        check("07");   //普通十进制7 or 八进制7

        //3)包含非法的字符,例如包含多余的小数点,包含多余的负号,以及其它非常规格式
        check("123.");
        check(".123");
        check("1.2.3");
        check("--12.3");
        check("-1-2.3");
        check("-12.3-");
        check(" 123");
        check("1 23");
        check("123 ");
        check("1a2b3c");
        check("10.0d");  //double类型
        check("1000L");  //long类型
        check("10.0f");  //float类型

        //4)非阿拉伯数字,例如印度数字 १२३,阿拉伯文 ١٢٣,以及罗马数字Ⅰ、Ⅱ、Ⅲ
        check("१२३");
        check("١٢٣");
        check("Ⅲ");

        //5)非十进制的数字,例如二进制,八进制,十六进制,科学计数法表示的数字
        check("0b100");  //二进制
        check("09");     //十进制9 or 非法的八进制
        check("0xFF");   //十六进制
        check("2.99e+8");//科学计数法
    }

由于篇幅原因,这里就不将控制台的打印结果贴上来了,有兴趣的同学可以根据我的代码自己尝试一下。

下面是将测试用例的打印结果做了个表格汇总,表头为方法名,第一列是具体的输入字符串,表格中其它单元格表示该方法判断是否为数字字符串的结果。

用例isNumberic1isNumberic2isNumberic3isNumberic4isNumberic5isNumberic6isNumberic7isNumberic8
nullfalsefalsefalsefalsefalsefalsefalsefalse
“”falsefalsetruetruefalsefalsefalsetrue
" "falsefalsefalsefalsefalsefalsefalsetrue
“123”truetruetruetruetruetruetruetrue
“0.123”truetruetruefalsetruetruefalsefalse
“-12.3”truetruetruefalsetruetruefalsefalse
“07”truetruetruetruetruetruetruetrue
“123.”truefalsetruefalsefalsetruefalsefalse
“.123”truefalsetruefalsetruetruefalsefalse
“1.2.3”falsefalsefalsefalsefalsefalsefalsefalse
“–12.3”falsefalsefalsefalsefalsefalsefalsefalse
“-1-2.3”falsefalsefalsefalsefalsefalsefalsefalse
“-12.3-”falsefalsefalsefalsefalsefalsefalsefalse
" 123"truefalsefalsefalsefalsefalsefalsetrue
“1 23”falsefalsefalsefalsefalsefalsefalsetrue
"123 "truefalsefalsefalsefalsefalsefalsetrue
“1a2b3c”falsefalsefalsefalsefalsefalsefalsefalse
“10.0d”truefalsefalsefalsefalsetruefalsefalse
“1000L”falsefalsefalsefalsefalsetruefalsefalse
“10.0f”truefalsefalsefalsefalsetruefalsefalse
“१२३”falsefalsetruetruetruefalsetruetrue
“١٢٣”falsefalsetruetruetruefalsetruetrue
“Ⅲ”falsefalsefalsefalsefalsefalsefalsefalse
“0b100”falsefalsefalsefalsefalsefalsefalsefalse
“09”truetruetruetruetruefalsetruetrue
“0xFF”falsefalsefalsefalsefalsetruefalsefalse
“2.99e+8”truefalsefalsefalsefalsetruefalsefalse

通过这个表格,可以看出不同的判断方法,对于非常规的字符串来说,差异还是比较大的。

1)null或者空字符串

在处理null时所有方法保持一致,这也是一个工具类该满足的基本素养。

对于空字符串来说,无论空字符串长度是否大于0,基于StringUtils.isNumericSpace的isNumberic8均会返回true,因为它本身认为空字符串就是数字。

对于长度大于0的空字符串来说,基于NumberFormat的isNumberic3和基于java.lang.Character#isDigit(int)的isNumberic4 这两种判断方法都正常返回了false。

但是对于长度为0的空字符串来说,isNumberic3和isNumberic4 这两种判断方法出了点小插曲,它们返回了false。这是因为,对于isNumberic3来说,toCharArray或者chars方法返回长度为0的字符数组,它并没有做一个有效的遍历。对于isNumberic4来说,NumberFormat的起始位置和终点位置一致。

所以为了让isNumberic3和isNumberic4更加健壮,建议对其实现内部再加一层空字符串的判断,优化后的代码如下。

    public static boolean isNumeric3(String str) {
        if (str == null || str.trim ().length() == 0) return false;
        NumberFormat formatter = NumberFormat.getInstance();
        ParsePosition pos = new ParsePosition(0);
        formatter.parse(str, pos);
        return str.length() == pos.getIndex();
    }
    public static boolean isNumeric4(String str) {
        if (str == null || str.trim ().length() == 0) return false;
        for (char c : str.toCharArray ()) {
            if (!Character.isDigit (c)) return false;
        }
        return true;
    }

2)常规的数字,整数,浮点数以及负数

常规数字指业务中常用的数字,譬如用于表示金额的浮点数,用于统计数量的整数等。这种情况下,isNumberic1,isNumberic2,isNumberic3,isNumberic5,isNumberic6 均表现出一致性,它们判断出来的结果都是相同的,而且也是符合我们常规预期的,是我们认为正确的结果。

对于浮点数,isNumberic4认为这不是有效数字,因为java.lang.Character#isDigit(int)认为小数点并不是数字字符。同样的,基于StringUtils.isNumeric的 isNumberic7 和基于StringUtils.isNumericSpace的 isNumberic8 也返回了false。

如果我们查看以上两个方法的底层实现,就可以发现 isNumberic7,isNumberic8 和 isNumberic4 的底层实现逻辑都是一样的,它们都是通过判断字符是否为数字字符来实现的。以下是StringUtils.isNumeric和StringUtils.isNumericSpace的源码:

    public static boolean isNumeric(CharSequence cs) {
        if (isEmpty(cs)) {
            return false;
        } else {
            int sz = cs.length();

            for(int i = 0; i < sz; ++i) {
                if (!Character.isDigit(cs.charAt(i))) {
                    return false;
                }
            }

            return true;
        }
    }

    public static boolean isNumericSpace(CharSequence cs) {
        if (cs == null) {
            return false;
        } else {
            int sz = cs.length();

            for(int i = 0; i < sz; ++i) {
                if (!Character.isDigit(cs.charAt(i)) && cs.charAt(i) != ' ') {
                    return false;
                }
            }

            return true;
        }
    }

这里尤其注意 “07“这个字符串。在某些语境下,07是十进制的7,在另一些语境下,07是八进制的7。例如我们直接将07赋值个int变量,它确实可以通过编译,但是把07换成09呢?一定会编译失败,这是因为变量声明的场景下,07作为八进制对待,它满足八进制的范围要求,而八进制无法表示09,它太大了,所以编译失败。

但是Double.parseDouble却可以将“09”转化成9.0,因为这种场景下,输入的数字作为十进制对待,0被忽略了。

        int j = 07;
        int k = 09;   //编译失败,非法的八进制
        System.out.println (Double.parseDouble ("09")); //打印9.0,以十进制对待

尽管以0开头的数字字符串,在使用Double.parseDouble 的语境中被当作十进制对待,可以被正确解析。但是从某些业务角度或者某种特定思维上来说,数字怎么能以0开始呢?你能接受一个以0开头的整数或者浮点数吗?如果你不能接受这是一个合法的数字字符串,那么很遗憾,现有的案例均不满足需求。你似乎只能通过正则表达式来实现,重新定义你的正则表达式,来过滤掉这类不恰当的字符串。

同时还需要注意,表格倒数第三行的用例是“09”,和“07”这一行类似,但isNumberic6在这两行表现的不一致。这是由于isNumberic6使用了NumberUtils.isCreatable,它把以“0”开头的数字认为是八进制数,符合八进制范围的返回true,不符合的返回false。所以"07"会返回true,“09”会返回false。

特别注意,当输入为“10.0d”, “1000L”和“10.0f”时,在某种程度上也认为这是有效的数字,因为基本类型中声明double,long和float类型的变量时,分别在字面量后面添加一个‘d’(‘D’) ,‘l’(‘L’) 和 ‘f’(‘F’)是一个很常见的操作。 这类声明一般用来处理强制转换,但对于这类数字字符串来说,使用 isNumberic1 的局限性就出来了,本例中基于 Double.parseDouble 来做判断,它可以接受‘d’(‘D’) 和 ‘f’(‘F’) 结尾的数字字符串,但是不能接受以 ‘l’(‘L’) 结尾的数字字符串,以下是Double.parseDouble的部分源码片段。

if (var6 >= var5 || var6 == var5 - 1 && (var0.charAt(var6) == 'f' || var0.charAt(var6) == 'F' || var0.charAt(var6) == 'd' || var0.charAt(var6) == 'D')) {
    if (var13) {
        return var1 ? A2BC_NEGATIVE_ZERO : A2BC_POSITIVE_ZERO;
    }

    return new FloatingDecimal.ASCIIToBinaryBuffer(var1, var3, var21, var8);
}

那是不是意味着,如果我们将isNumberic1的内部实现换成Long.parseLong,它就可以接受 “1000L” 了呢?答案是否定的,如果我们运行以下的代码,系统将抛出异常。

System.out.println (Long.parseLong ("5562L"));

这是因为Long.parseLong的底层还是用到了Character.digit方法。以下是Long.parseLong的部分源码片段,上述的打印将在第一个”if“块抛出异常。

            while (i < len) {
                // Accumulating negatively avoids surprises near MAX_VALUE
                digit = Character.digit(s.charAt(i++),radix);
                if (digit < 0) {
                    throw NumberFormatException.forInputString(s);
                }
                if (result < multmin) {
                    throw NumberFormatException.forInputString(s);
                }
                result *= radix;
                if (result < limit + digit) {
                    throw NumberFormatException.forInputString(s);
                }
                result -= digit;
            }

因此,如果你需要接受以‘d’(‘D’),‘f’(‘F’) 和 ‘l’(‘L’)结尾的数字字符串,只有isNumberic6是最优解。

3)包含非法的字符,例如包含多余的小数点,包含多余的负号,以及其它非法格式

这部分的用例就相对灵活很多了。极端情况下,比如多一个小数点,或者多一个负号,或者纯粹的掺入非数字字符,isNumberic1 ~ isNumberic8均能做出有效的判断。

但是,如果只有一个小数点,且小数点的位置不合时宜的情况下,比如“123.”,“.123”,使用异常处理的isNumberic1和使用NumberFormat的isNumberic3行为一致,均返回了true。

你可能惊呆了,这怎么能算是有效字符串呢,这种情况其实和07这个测试用例是一样的,Java可以将它们转换成浮点数123.0或者整数123。所以返回true对于java来说这就是合理的。如果你不满意,那只能考虑正则这条路了。

4)非阿拉伯数字,例如印度数字 १२३,阿拉伯文 ١٢٣,以及罗马数字Ⅰ、Ⅱ、Ⅲ

所有的判断方法,均认为罗马数字为非法数字。

使用印度数字或者阿拉伯文数字,其中 isNumberic3,isNumberic4,isNumberic5,isNumberic7,isNumberic8 能够做出有效判断。其它方案均无效。

如果是做国际业务的同学,你可能就要留意了,他们用本地语言填写的电话号码你涵盖了吗?

等等,那汉字表示的数字,“一”,“二”,“三”… 该用什么方法来判断呢? 很遗憾,本文列举的方法均不满足,需要自己开发相关工具类或查找有效资料。

5)非十进制的数字,例如二进制,八进制,十六进制,科学计数法表示的数字

前面的测试用例均是十进制数,但是一些少数场景不免会出现十进制以外的数据。二进制变量以 0b或0B 开始,八进制以 0开始,十六进制以0X或0x开始。

通过倒数第二行和倒数第三行可以看出来,只有 isNumberic6 可以准确的判断出八进制和十六进制。

通过倒数第四行可以看出来,任何方法都不能判断二进制。

通过最后一行可以看出来,isNumberic1和isNumberic6 可以用来判断科学计数法。

小结

判断一个字符串是否为数字看起来是一项很简单的业务,但是它涉及的场景却是非常多的,从业务角度来看,没有哪个方法是完美的。

有人说异常处理的方式不好,性能低,但是它能处理开头和结尾为空字符串的输入,还能处理科学计数法。

有人说正则最好,但他们用的正则表达式基本都是从网上扒下来的吧,只能判断阿拉伯数字吧,而且不能处理以0开始的字符吧。

有人说使用数字字符的方式最好,但是它无法判断浮点数。

还有人说使用StringUtils最好,那他们有对比过NumberUtils吗?

总之,没有什么方法是最好的, 最适合的才是最好的。这就和找对象一个道理,你说刘亦菲美吧,她很美,但不适合呀。

总结

加载全部内容

相关教程
猜你喜欢
用户评论