入门程序员很快发现,数字的文本表示形式与程序可以在其上执行数学运算的数字变量明显不同。 例如, "123"
与真实数值123
或十六进制0x7B
。 程序必须使用算法或转换例程来从文本中获取数字-尤其是当文本使用分组或小数点分隔符(例如美国数字格式的逗号和小数点)格式化时。 文本到数字的转换首先是交互式编程的一个问题,但是HTML,XML以及许多其他将数据作为文本处理的文件和通信格式也经常遇到这种情况。
在Java SE API提供了类似的方法Integer.parseInt()
和Double.parseDouble()
进行转换,但这些方法预计在Java语言规范文本(见定义的形式他们的论据相关主题 )。 就本文而言,它主要查看整数和双精度数,该格式基本上仅由以下字符组成:
- 前导减号(ASCII值
45
或十六进制0x2D
) - 数字0到9(ASCII值
48
到57
或十六进制0x30
到0x39
) - 对于浮点值,用点或句点表示的小数点(ASCII值
46
或十六进制0x2E
)
该要求对于程序员和代码是合理的,但是用户希望以其本地文化的通用格式输入和查看数字。 Java SE API的java.text.NumberFormat
类包含一个方便的parse(String source)
方法,大多数程序员都使用该方法将特定于语言环境的格式化文本解析为数字值。 不幸的是,该方法可能会产生意想不到的错误结果。 本文介绍了NumberFormat
的原理,回顾了其功能,公开了类的解析陷阱,并提供了可靠使用它的指导原则。
无需验证即可解析
本文的示例程序(请参阅下载 ) NumberInput
(图1所示)是一个Swing应用程序,可让您探索几种将文本输入转换为数值的方法。 除输入字段外,该程序还显示默认的语言环境名称,原始键值,原始长度和已解析的位置数(如果适用)。 在启动时,它将分别使用双123456.7
值123456.7
和整数值1234567
加载输入字段。 这两个值均按照本机用户期望的格式设置为默认语言环境。 因为我住在美国,程序会显示"123,456.7"
的双重价值, "1,234,567"
的整数值。
图1. NumberInput初始显示
当您单击NoCheck按钮时,该程序使用Double.parseDouble()
和Integer.parseInt()
进行直接解析,而无需进行验证。 请注意,在调用任何其他方法之前,会在actionPerformed()
从输入字符串中删除前导和尾随空格。 图2显示了结果:
图2. Double.parseDouble()为格式化的文本抛出NumberFormatException
错误的原因是逗号用作美国语言环境的分组分隔符。 除去逗号后,输入为"123456.7"
,程序将接受double值。
负值呢? 键入前导符号(并除去逗号)可使程序满意,但后缀符号的结果为: 输入字符串"123456.7-"
NumberFormatException 。
出于相同的原因,整数分析显示类似的行为。 为NoCheck按钮调用的代码在NumberInput
的noCheckInput()
方法中。 它使用Double.parseDouble()
从JTextField jtD
输入,并使用Integer.parseInt()
从JTextField jtI
输入。 根据规则,这些结果是正常的,并且超出绝对初学者阶段,对于大多数Java程序员来说,应该是预期的行为。
抢救NumberFormat
除了错别字和其他用户输入错误外,在显示带格式的数字文本和接收来自同一字段的数字文本输入之间始终存在不安的关系。 我们可能已经知道所有程序员,他们认为解决方案仅是显示一条消息,上面写着:“没有逗号且前导减号的关键数字”。 头脑冷静的数据输入人员通常对此方法没有太大的问题,但是用户通常希望看到格式化的数字,然后通常直接在格式化的显示器上键入内容,从而保持分隔符和分组。 通常,经过一番抱怨之后,美国程序员解决该问题的第一步是编写一个例程,该例程去除逗号并将任何尾随的减号移动到输入值的前面。 以这种方式编写的许多程序在生产中都有很长的生命。 从某种意义上说,这是连程序员首次进军国际化(I18N)和本地化(localization)(见相关主题 )。 问题在于,这种代码只能针对一个或一组有限的语言环境有效地本地化程序。
Java程序被誉为能够在任何启用的平台上运行,并且许多人也以熟悉的方式将其表示为任何国家和语言。 Java SE SDK提供了API来使这种期望成为现实。 但是,像我刚才描述的第一个努力那样编写的程序在其假定范围之外使用时很快就会崩溃。 在其他国家/地区中,值123456.7
可以设置为格式或键为"123.456,7"
, "123456,7"
或"123'456,7"
或其他键。 假设所有区域设置都使用相同的分组分隔符和十进制分隔符(同样,在美国示例中分别使用“,”和“。”)的任何程序都将不起作用。 预料到此问题,API包含java.text.NumberFormat
。 该类提供了外部简单的parse()
和format()
方法,这些方法可以自动进行区域设置,包括格式化符号的知识。 实际上, NumberInput
使用NumberFormat
来格式化输入字段中显示的值。
Java Locale
对象表示并标识语言和地区或国家/地区的特定组合。 它本身不提供局部行为; 类必须自己提供本地化。 但是,Java平台确实支持一致的语言环境集,并且许多标准类都实现一致的本地化行为。 这些类通常具有两种版本的方法:一种采用Locale
参数,而另一种采用默认方法。 默认语言环境在程序启动时自动确定,或由传递给Java运行时的参数覆盖。
NumberFormat
是一个抽象类,但它提供了静态的工厂get XXX Instance()
方法,用于获取具有预定义的本地化格式的具体实现。 基础实现通常是java.text.DecimalFormat
的实例。 本文中的代码和讨论使用NumberFormat.getNumberInstance()
返回的默认值来格式化和解析双NumberFormat.getIntegerInstance()
值,使用NumberFormat.getIntegerInstance()
来获取整数值。
值得一提的是,完全本地化分析所需的代码很少。 这些步骤是:
- 获取一个
NumberFormat
实例。 - 将
String
解析为Number
。 - 抓住适当的数值。
付出很少的努力的好处是巨大的,每个Java程序员都应该使用NumberFormat
来处理格式化的数字转换。 要在不同的语言环境中进行尝试,请使用以下命令行调用NumberInput
应用程序,其中lc
是ISO-639语言代码, cc
是ISO-3166国家代码:
java -Duser.language=lc -Duser.region=cc NumberInput
从JDK 1.4开始,可以使用user.country
系统属性代替user.region
。 要确定Java平台支持的语言环境,请参阅JDK文档的国际化部分中的支持的语言环境(请参阅参考资料 )。 程序可以使用java.util.Locale
的静态getAvailableLocales()
方法在运行时确定语言环境支持。
清单1显示了NumberInput
的NFInput()
方法的相关代码,单击NF按钮时将调用该代码。 该方法使用NumberFormat.parse(String)
进行验证和转换。
清单1. NFInput()方法使用NumberFormat.parse(String)
...
NumberFormat nfDLocal =
NumberFormat.getNumberInstance(),
nfILocal =
NumberFormat.getIntegerInstance();
...
public void NFInput( String sDouble, String sInt )
{ // "standard" NumberFormat parsing
double d;
int i;
Number n;
try
{
n = nfDLocal.parse( sDouble );
d = n.doubleValue();
...
n = nfILocal.parse( sInt );
i = n.intValue();
...
}
catch( ParseException pe )
{
...
}
} // end NFInput
NumberFormat
实例的实现
在这一点上,简要回顾一下NumberFormat
被要求get XXX Instance()
时发生的情况以及DecimalFormatSymbols
类的角色都是有用的。 讨论基于对J2SE 1.4附带的参考实现源代码的回顾,并且可能会发生变化。
事件的基本流程是NumberFormat
基于关联的语言环境向内部ListResourceBundle
咨询适当的模式,并返回使用该模式创建的DecimalFormat
对象。 如果没有显式的负模式,则将正负号与正模式组合使用。 在此过程中,将创建适合区域设置的DecimalFormatSymbols
对象,并且DecimalFormat
实例将获得对该对象的引用。 因为NumberFormat.get XXX Instance()
方法基于工厂模式,所以其他实现或将来的引用实现可能返回不同的类。 因此,任何自定义代码必须确保DecimalFormat
尝试访问相关的前实际返回DecimalFormatSymbols
实例。
DecimalFormatSymbols
对象包含信息,例如适当的十进制和分组分隔符以及减号。 单击“信息”按钮时, NumberInput
收集大量此类信息并将其显示在对话框中。 图3显示了使用en_US
语言环境的示例。 此信息对于解析和验证本地化格式的数字至关重要。
图3. DecimalFormatSymbols数据
尝试单击NF按钮,您将看到即使使用逗号或其他本地分组分隔符也可以正确接受这些值。 根据当前样式放置减号也可以接受。 减号在另一个位置时该怎么办? 这以及其他几个问题是下一节的主题,也是本文的动力。
UnexpectedResults.equals(bigTrouble)
关于Java国际化的大多数文章都将重点放在NumberFormat
的格式化功能上,并在到目前为止我给出的信息有所变化之后结束对解析的任何讨论。 不幸的是,对该类(实际上是从NumberFormat.get XXX Instance()
返回的具体DecimalFormat
子类)进行的测试和实验显示了解析异常,这可能令人惊讶:在许多常见的字段条件下, NumberFormat.parse(String)
欣喜地截断数据并在没有向程序员指示的情况下丢失信号。 以下情况显示了此行为(除非另有说明,否则使用en_US
语言环境):
- 十进制分隔符之前的多个连续或不规则插入的分组分隔符将被忽略。
例如,"123,,,456.7"
"123,45,6.7"
和"123,45,6.7"
被接受,并且都返回123456.7
。
经过深思熟虑,我得出的结论是,尽管从技术上讲,这种行为是错误的,但不会丢失任何数据,任何解决方案都会导致超出其价值的工作。 您应该了解该行为,但是NumberInput
应用程序无法纠正它,并且在本文中我将不再对其进行详细介绍。 - 在十进制分隔符之后出现的分组分隔符会导致截断。
"123,456.7,85"
被接受为123456.7
。 - 多个十进制分隔符会导致截断。
"123,456..7"
被接受为123456.0
;"12.3.456.7"
被接受为12.3
。 - 对于带有前减号(负前缀)的模式,截断会在非数字字符的点处发生,包括嵌入的负号。
"123,4r56.7"
被接受为1234.0
;"12-3,456.7"
被接受为12.0
(正值)。 - 对于带有尾随减号(负后缀)的模式,截断会在非数字字符的点处发生,不包括嵌入的减号。 可接受嵌入的负号,但任何其他数据都将被截断。
对于沙特阿拉伯语言环境(ar_SA
),"123,4r56.7"
被接受为1234.0
; 接受"12-3,456.7"
作为-12.0
(负值)。 - 如果模式为负输入指定前导负号,则尾随负号将被忽略。
接受"123,456.7-"
作为123456.7
(正值),接受"-123.456,7-"
(荷兰语区域设置nl_NL
)作为-123456.7
(负值)。
图4和图5显示了单击NF按钮时其中一些行为的示例。 尽管可能难以解释原始条目的意图,但可以肯定的是,没有预料到会出现1234.0的双重结果,用户也不想从整数输入中删除最后两位。 同样,不会引发异常,也没有迹象表明输入的部分被忽略了。
图4. NumberFormat.parse(String)的意外结果
图5. NumberFormat.parse(String)接受截断的值
考虑到实现NumberFormat
和DecimalFormat
类所需的工作量,这些结果在许多使用JDK 1.4和5.0进行的测试中都是一致的。 另一方面,代码比将参数传递给方法并检查结果要简单得多。 唯一的真实线索是在NumberFormat.parse(String source)
的JDK文档中,该文档没有进一步说明就说:“该方法可能不会使用给定字符串的整个文本。”
看起来像这样的异常是很麻烦的,乍一看,似乎最好返回编程的“按自己的方式来做”。 “垃圾进,垃圾出”在计算领域是老生常谈,但这仅意味着程序无法保证数据正确无误 。 程序员的义务是尽可能确保所有输入均有效 。 与其说它是一个错误,不如说是一种NumberFormat.parse(String)
的设计,它尽可能地从输入字符串的某个部分返回一个数字。 不幸的是,该行为包括一个未声明的假设,即数据已经通过验证。 最终结果是程序员无法确定何时输入无效,这破坏了与用户和数据本身的隐式契约。
几年前发现这些问题时,我的第一React是为parse(String)
方法编写相当于前端预处理器的内容。 那行得通,但是代价是额外的,部分冗余的代码和更多的时间来处理数据。 幸运的是,事实证明,谨慎使用现有的NumberFormat
方法可以解决此问题。
使用ParsePosition进行验证
parse(String source, ParsePosition parsePosition)
方法是不寻常的,因为它不会引发任何异常。 它通常用于从单个字符串中解析多个数字时使用。 但是,返回方法时, ParsePosition.getIndex()
的值是输入字符串中最后一个解析的位置加一个。 如果代码始终以索引设置为零开始,则在处理后,索引值将等于已解析字符的数量。 使用验证方法的关键是将更新后的索引与原始输入字符串的长度进行比较。
为避免混淆,我应该提到ParsePosition
也有一个getErrorIndex()
方法。 此方法对于此处讨论的条件基本上没有用,因为没有检测到错误。 另外,在使用它时,必须在每次解析操作之前将错误索引重置为-1; 否则结果可能会产生误导。
单击NF或NFPP按钮时, NumberInput
应用程序将在Length / PP列下显示ParsePosition
索引。 如果原始值的长度大于零且与索引值匹配,则两者均显示为绿色;否则,两者均显示为绿色。 否则,这些值将显示为红色。 此操作与特定的验证方法分开完成。 如果再次查看图4 ,即使与NF按钮关联的NFInput()
方法接受了数据,该值也将显示为红色,表明存在错误。
对于最终的验证版本,当NFPPInput()
NFPP按钮时,将调用NFPPInput()
方法。 此方法使用parse(String, ParsePosition)
验证输入并获取数字值。 图6和图7显示在NFPPInput()
检测到来自图4的无效输入。 在我的测试中,该方法正确处理了NumberFormat.parse(String)
遗漏的所有条件。
图6.检测无效的重复条目
图7.检测无效的整数条目
您必须遵循一些准则,以确保使用parse(String, ParsePosition)
正确的结果:
- 请记住,该方法永远不会引发异常。
为了清楚和演示起见,此处的代码仅显示“ 可接受 / 不可接受”对话框。 在通用情况下,应该抛出ParseException
使其更符合正常期望。 - 始终在调用
parse(String, ParsePosition)
之前将ParsePosition
索引重置为零。
必须进行重置,因为使用此方法,解析ParsePosition
输入字符串中的ParsePosition
索引开始。 - 使用
NumberFormat.getNumberInstance()
解析双NumberFormat.getIntegerInstance()
值,使用NumberFormat.getIntegerInstance()
解析整数值。
如果您不对整数使用整数实例(或者将setParseIntegerOnly(true)
应用于数字实例),则该方法将解析所有小数点分隔符,直到输入字符串的末尾。 结果是长度和索引匹配,并且您接受了无效的输入。 - 除了比较长度和索引值是否相等之外,还必须在解析后检查是否为空
Number
或输入字符串为空(“”或长度为零)。
清除输入字段将导致一个空字符串。 在这种情况下,长度和索引值均为零,因此它们匹配。 对于空字符串输入,parse方法返回null。 此行为与使用NumberFormat.parse(String source)
空字符串的结果不同,后者会抛出“无法解析的数字”ParseException
。 请记住,parse(String source, ParsePosition parsePosition)
永远不会引发异常! 在NumberInput
,清单2中的代码段用于处理各种可能性:清单2.检查错误情况
if( sDouble.length() != pp.getIndex() || n == null ) { /* error */ }
总而言之,正确输入处理的步骤为:
- 获取适当的
NumberFormat
并定义一个ParsePosition
变量。 - 将
ParsePosition
索引设置为零。 - 使用
parse(String source, ParsePosition parsePosition)
输入值。 - 如果输入长度和
ParsePosition
索引值不匹配或解析的Number
为null,请执行错误操作。 - 否则,该值将通过验证。
清单3显示了相关代码:
清单3. NFPPInput()方法
...
NumberFormat nfDLocal =
NumberFormat.getNumberInstance(),
nfILocal =
NumberFormat.getIntegerInstance();
ParsePosition pp;
...
public void NFPPInput( String sDouble,
String sInt )
{ // validate NumberFormat with ParsePosition
Number n;
double d;
int i;
pp.setIndex( 0 );
n = nfDLocal.parse( sDouble, pp );
if( sDouble.length() != pp.getIndex() ||
n == null )
{
showErrorMsg(
"Double Input Not Acceptable\n" +
"\"" + sDouble + "\"");
}
else
{
d = n.doubleValue();
jtD.setText( nfDLocal.format( d ) );
showInfoMsg( "Double Accepted \n" + d );
}
pp.setIndex( 0 );
n = nfILocal.parse( sInt, pp );
if( sInt.length() != pp.getIndex() ||
n == null )
{
showErrorMsg(
"Int Input Not Acceptable \n" +
"\"" + sInt + "\"");
}
else
{
i = n.intValue();
jtI.setText( nfILocal.format( i ) );
showInfoMsg( "Int Accepted \n" + i );
}
} // end NFPPInput
结论
Java SE API中已进行了大量工作,不仅允许在字节码级别“写一次,在任何地方运行”,而且还可以容纳国际化和本地化的应用程序。 NumberFormat
和DecimalFormat
是打算编写世界一流应用程序的Java程序员不能没有的类。 但是,如本文所示,开发人员也无法使用parse(String source)
方法,除非可以假定完美的输入-在现实世界中很少出现这种情况。 我在本文中提供的信息和代码为您提供了另一种使用parse(String source, ParsePosition parsePosition)
来确定条目何时无效并获得正确结果的技术。
翻译自: https://www.ibm.com/developerworks/java/library/j-numberformat/index.html