Java判断字符串是否为数字的多种方式,你用对了吗

前言

判断一个字符串是否为数字是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.commonsgroupId>
			<artifactId>commons-lang3artifactId>
		dependency>

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

使用NumberUtils时,我们通常会选择其静态方法isParsableisCreatable其中的一种,它们的不同点在于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");//科学计数法
    }

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

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

用例 isNumberic1 isNumberic2 isNumberic3 isNumberic4 isNumberic5 isNumberic6 isNumberic7 isNumberic8
null false false false false false false false false
“” false false true true false false false true
" " false false false false false false false true
“123” true true true true true true true true
“0.123” true true true false true true false false
“-12.3” true true true false true true false false
“07” true true true true true true true true
“123.” true false true false false true false false
“.123” true false true false true true false false
“1.2.3” false false false false false false false false
“–12.3” false false false false false false false false
“-1-2.3” false false false false false false false false
“-12.3-” false false false false false false false false
" 123" true false false false false false false true
“1 23” false false false false false false false true
"123 " true false false false false false false true
“1a2b3c” false false false false false false false false
“10.0d” true false false false false true false false
“1000L” false false false false false true false false
“10.0f” true false false false false true false false
“१२३” false false true true true false true true
“١٢٣” false false true true true false true true
“Ⅲ” false false false false false false false false
“0b100” false false false false false false false false
“09” true true true true true false true true
“0xFF” false false false false false true false false
“2.99e+8” true false false false false true false false

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

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.isNumericStringUtils.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)非十进制的数字,例如二进制,八进制,十六进制,科学计数法表示的数字

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

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

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

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

小结

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

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

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

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

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

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

你可能感兴趣的:(Java基础)