一、概述
大多数java程序员都曾经使用过java.util.stringtokenizer类。它是一个很方便的字符串分解器,主要用来根据分隔符把字符串分割成标记(token),然后按照请求返回各个标记。这个过程称为tokenization,实际上就是把字符序列转换成应用程序能够理解的多个标记。
虽然stringtokenizer用起来很方便,但它的功能却很有限。这个类只是简单地在输入字符串中查找分隔符,一旦找到了分隔符就分割字符串。它不会检查分隔符是否在子串之中这类条件,当输入字符串中出现两个连续的分隔符时,它也不会返回""(字符串长度为0)形式的标记。
为了突破这些局限,java 2平台提供了breakiterator类,它是在stringtokenizer之上改进的字符串分解器。由于jdk 1.1.x没有提供这个类,为了满足自己的需要,开发者经常花费很多时间从头开始编写分解器。在涉及到数据格式化处理的大型工程中,这类定制的字符串分解器有时随处可见,而且这种情况并不罕见。
本文的目标是帮助你利用现有的stringtokenizer类,编写一个高级字符串分解器。
二、stringtokenizer的局限
你可以用以下三种构造函数中的任意一种创建stringtokenizer分解器:
stringtokenizer(string sinput):以空白字符(“ ”,“/t”,“/n”)为分隔符分割字符串。
stringtokenizer(string sinput, string sdelimiter):以sdelimiter为分隔符分割字符串。
stringtokenizer(string sinput, string sdelimiter, boolean breturntokens):以sdelimiter为分隔符分割字符串,但如果breturntokens为true,则分隔符也作为标记返回。
第一个构造函数不检查输入字符串是否包含子串。例如,如果以空白字符为分隔符分割“hello. today /"i am /" going to my home town”,则字符串分解结果是hello.、today、"i、am、"、going等,而不是hello.、today、"i am "、going等。
第二个构造函数不检查两个分隔符连续出现的情况。例如,如果以“,”为分隔符分割“book, author, publication,,,date published”这个字符串,则stringtokenizer返回book、author、publication和date published这四个标记,而不是book、author、publication、""、""和date published这6个标记(其中""表示0长度字符串)。要得到6个标记的答案,你必须把stringtokenizer的breturntokens参数设置为true。
允许设置值为true的breturntokens参数是一个重要的功能,因为它考虑到了分隔符连续出现的情况。例如,使用第二个构造函数时,如果数据是动态收集得到而且要用来更新数据库中的表,输入字符串中的标记对应着表里面列的值,那么当我们不能确定哪一个列应该设置为""时,我们就无法把输入串中的标记映射到数据库列。假设我们要把记录插入到一个有6个列的表,而输入数据中包含两个连续的分隔符。此时,stringtokenizer的分解结果是5个标记(两个连续的分隔符代表""标记,它将被stringtokenizer忽略),而我们却有6个字段需要设置。同时,我们也不知道连续分隔符在哪里出现,所以也就不知道哪一个列应该设置成""。
当标记本身等同于分隔符(无论是长度还是值)且位于子串之内时,第三个构造函数无效。例如,如果我们要以“,”为分隔符分解字符串“book, author, publication,/",/",date published”(这个字符串包含一个“,”标记,它与分隔符一样),结果是book、author、publication、"、"、date published这六个标记,而不是book、author、publication、,(逗号字符)、date published这五个标记。再提醒一下,即使我们把stringtokenizer的breturntokens参数设置设置成了true,在这种情况下也没有什么帮助。
三、高级字符串分解器
在编写代码之前,你必须搞清楚一个好的分解器有哪些基本要求。因为java开发者已经习惯于使用stringtokenizer类,所以一个好的分解器应该提供stringtokenizer类提供的所有实用方法,比如hasmoretokens()、nexttoken()、counttokens()。
本文提供的代码很简单,而且大部分代码足以自我解释。在这里,我主要利用了stringtokenizer类(创建类实例时breturntokens参数设置为true),并提供了上面提到的几个方法。大多数时候标记与分隔符不同,有些时候分隔符却要作为标记输出(尽管非常罕见),此时如果出现了对标记的请求,分解器要把分隔符作为标记输出。创建powerfultokenizer对象时,你只需要提供输入字符串和分隔符这两个参数,powerfultokenizer将在内部使用breturntokens设置成true的stringtokenizer。(这么做的原因在于,如果不是用breturntokens设置成true的方式创建stringtokenizer,那么它将在解决先前提出的问题时受到限制)。为了正确地控制分解器,代码在几个地方(计算标记的总数量以及nexttoken())检查breturntokens是否设置成了true。
你可能已经发现,powerfultokenizer实现了enumeration接口,从而也就实现了hasmoreelements()和nextelement()这两个方法,而这两个方法又分别把调用直接委托给hasmoretokens()和nexttoken()。(由于实现了enumeration接口,powerfultokenizer实现了与stringtokenizer的向后兼容。)
我们来看一个例子,假设输入字符串是“hello, today,,, /"i, am /", going to,,, /"buy, a, book/"”,分隔符是“,”。用分解器分割这个字符串时返回结果如表1所示:
表1:字符串分解结果
输入字符串包含11个逗号(,)字符,其中3个在子串里面、4个连续出现(“today,,,”中包含两个连续逗号,第一个逗号是today的分隔符)。下面是powerfultokenizer计算标记总数的算法:
如果breturntokens=true,把子串中的分隔符数量乘以2,再从实际总数量减去该数字,就得到了标记的总数。理由是,对于子串“buy, a, book”,stringtokenizer将返回5个标记(即“buy:,:a:,:book”),而powerfultokenizer将返回一个标记(即“buy, a, book”),两者的差值是4(即,2乘以子串中的分隔符数量)。这个公式对于所有包含分隔符的子串都有效。
类似地,对于breturntokens=false的情形,我们从实际总数(19)减去表达式[分隔符总数(11)- 连续分隔符数量(4) + 子串中的分隔符数量(3)]。由于这时我们不返回分隔符,它们(非连续出现或在子串内部)对我们来说没有用,上面的公式为我们返回了标记的总数量(9)。
请记住这两个公式,它们是powerfultokenizer的核心。这两个公式适用于几乎所有它们各自条件下的情形。但是,如果你有更复杂的要求,不能使用这两个公式,那么你应该在编写代码之前分析各种可能出现的情况,并设计出自己的公式。
// 检查分隔符是否位于子串之内
for (int i=1; i
{
iindex = sinput.indexof(sdelim, iindex+1);
if (iindex == -1)
break;
// 如果分隔符位于子串之内,则向前分析直至子串结束
while (sinput.substring(iindex-ilen, iindex).equals(sdelim))
{
inextindex = sinput.indexof(sdelim, iindex+1);
if (inextindex == -1)
break;
iindex = inextindex;
}
aiindex[i] = iindex;
//system.out.println("aiindex[" + i + "] = " + iindex);
if (iswithinquotes(iindex))
{
if (bincludedelim)
itokens -= 2;
else
itokens -= 1;
}
}
counttokens()方法检查子串是否包含双引号。如果包含,那么它减少总数并把索引值修改为字符串中下一个双引号出现的位置(如上面的代码片断所示)。如果breturntokens是false,那么它从总数减去输入字符串中出现的非连续分隔符的数量。
// 如发现多个连续的分隔符,则返回""作为标记
if ( (sprevtoken.equals(sdelim)) && (stoken.equals(sdelim)) ) {
sprevtoken = stoken;
itokenno++;
return "";
}
// 检查标记本身是否等于分隔符
if ( (stoken.trim().startswith("/"")) && (stoken.length() == 1) ) {
// 标记本身等于分隔符的特殊情况
string snexttoken = otokenizer.nexttoken();
while (!snexttoken.trim().endswith("/"")) {
stoken += snexttoken;
snexttoken = otokenizer.nexttoken();
}
stoken += snexttoken;
sprevtoken = stoken;
itokenno++;
return stoken.substring(1, stoken.length()-1);
}
// 检查字符串中是否包含子串
else if ( (stoken.trim().startswith("/""))
&& (!((stoken.trim().endswith("/""))
&& (!stoken.trim().endswith("/"/"")))) )
{
if (otokenizer.hasmoretokens())
{
string snexttoken = otokenizer.nexttoken();
// 检查"/"/""
while (!((snexttoken.trim().endswith("/""))
&& (!snexttoken.trim().endswith("/"/""))) )
{
stoken += snexttoken;
if (!otokenizer.hasmoretokens())
{
snexttoken = "";
break;
}
snexttoken = otokenizer.nexttoken();
}
stoken += snexttoken;
}
}
nexttoken()方法通过stringtokenizer.nexttoken方法获取标记,并检查标记中的双引号字符。如果发现了这些字符,它继续获取标记直至不能再找到带有双引号的标记。另外,它还把标记保存到一个变量(sprevtoken,参见本
闽公网安备 35060202000074号