判断一个字符串是否为数字是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类的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时,我们通常会选择其静态方法isParsable
和isCreatable
其中的一种,它们的不同点在于isCreatable
不仅可以接受十进制数字字符串,还可以接受八进制,十六进制以及科学计数法表示的数字字符串,isParsable
只接受十进制数字字符串。
public static boolean isNumeric5(String str) {
return NumberUtils.isParsable(str);
}
public static boolean isNumeric6(String str) {
return NumberUtils.isCreatable(str);
}
如果使用StringUtils,那么要考虑到底该使用isNumeric
还是isNumericSpace
。二者的唯一差异在于,isNumeric
会认为空字符串为非法数字,isNumericSpace
则认为空字符串也是数字。
public static boolean isNumeric7(String str) {
return StringUtils.isNumeric(str);
}
public static boolean isNumeric8(String str) {
return StringUtils.isNumericSpace(str);
}
默认情况下,文章中的数字都指的是十进制的阿拉伯数字(0 ,1,2 … 9),测试时会扩展范围。考虑以下几个方向:
主体测试方法如下:
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 |
通过这个表格,可以看出不同的判断方法,对于非常规的字符串来说,差异还是比较大的。
在处理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;
}
常规数字指业务中常用的数字,譬如用于表示金额的浮点数,用于统计数量的整数等。这种情况下,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是最优解。
这部分的用例就相对灵活很多了。极端情况下,比如多一个小数点,或者多一个负号,或者纯粹的掺入非数字字符,isNumberic1 ~ isNumberic8均能做出有效的判断。
但是,如果只有一个小数点,且小数点的位置不合时宜的情况下,比如“123.”,“.123”,使用异常处理的isNumberic1和使用NumberFormat的isNumberic3行为一致,均返回了true。
你可能惊呆了,这怎么能算是有效字符串呢,这种情况其实和07这个测试用例是一样的,Java可以将它们转换成浮点数123.0或者整数123。所以返回true对于java来说这就是合理的。如果你不满意,那只能考虑正则这条路了。
所有的判断方法,均认为罗马数字为非法数字。
使用印度数字或者阿拉伯文数字,其中 isNumberic3,isNumberic4,isNumberic5,isNumberic7,isNumberic8 能够做出有效判断。其它方案均无效。
如果是做国际业务的同学,你可能就要留意了,他们用本地语言填写的电话号码你涵盖了吗?
等等,那汉字表示的数字,“一”,“二”,“三”… 该用什么方法来判断呢? 很遗憾,本文列举的方法均不满足,需要自己开发相关工具类或查找有效资料。
前面的测试用例均是十进制数,但是一些少数场景不免会出现十进制以外的数据。二进制变量以 0b
或0B
开始,八进制以 0
开始,十六进制以0X
或0x
开始。
通过倒数第二行和倒数第三行可以看出来,只有 isNumberic6 可以准确的判断出八进制和十六进制。
通过倒数第四行可以看出来,任何方法都不能判断二进制。
通过最后一行可以看出来,isNumberic1和isNumberic6 可以用来判断科学计数法。
判断一个字符串是否为数字看起来是一项很简单的业务,但是它涉及的场景却是非常多的,从业务角度来看,没有哪个方法是完美的。
有人说异常处理的方式不好,性能低,但是它能处理开头和结尾为空字符串的输入,还能处理科学计数法。
有人说正则最好,但他们用的正则表达式基本都是从网上扒下来的吧,只能判断阿拉伯数字吧,而且不能处理以0开始的字符吧。
有人说使用数字字符的方式最好,但是它无法判断浮点数。
还有人说使用StringUtils最好,那他们有对比过NumberUtils吗?
总之,没有什么方法是最好的, 最适合的才是最好的。这就和找对象一个道理,你说刘亦菲美吧,她很美,但不适合呀。