⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 chapter17.htm

📁 一本Java由初级到高级的编程书籍
💻 HTM
📖 第 1 页 / 共 3 页
字号:
3. 程序列表的存取<br>
接下来的一系列方法是简单的访问器:directory()、filename()(注意方法可能与字段有相同的拼写和大小写形式)和contents()。而hasFile()用于指出这个对象是否包含了一个文件(很快就会知道为什么需要这个)。<br>
最后三个方法致力于将这个代码列表写进一个文件——要么通过writePacked()写入一个打包文件,要么通过writeFile()写入一个Java源码文件。writePacked()需要的唯一东西就是DataOutputStream,它是在别的地方打开的,代表着准备写入的文件。它先把头信息置入第一行,再调用writeBytes()将contents(内容)写成一种“通用”格式。<br>
准备写Java源码文件时,必须先把文件建好。这是用IO.psOpen()实现的。我们需要向它传递一个File对象,其中不仅包含了文件名,也包含了路径信息。但现在的问题是:这个路径实际存在吗?用户可能决定将所有源码目录都置入一个完全不同的子目录,那个目录可能是尚不存在的。所以在正式写每个文件之前,都要调用File.mkdirs()方法,建好我们想向其中写入文件的目录路径。它可一次性建好整个路径。<br>
<br>
4. 整套列表的包容<br>
以子目录的形式组织代码列表是非常方便的,尽管这要求先在内存中建好整套列表。之所以要这样做,还有另一个很有说服力的原因:为了构建更“健康”的系统。也就是说,在创建代码列表的每个子目录时,都会加入一个额外的文件,它的名字包含了那个目录内应有的文件数目。<br>
DirMap类可帮助我们实现这一效果,并有效地演示了一个“多重映射”的概述。这是通过一个散列表(Hashtable)实现的,它的“键”是准备创建的子目录,而“值”是包含了那个特定目录中的SourceCodeFile对象的Vector对象。所以,我们在这儿并不是将一个“键”映射(或对应)到一个值,而是通过对应的Vector,将一个键“多重映射”到一系列值。尽管这听起来似乎很复杂,但具体实现时却是非常简单和直接的。大家可以看到,DirMap类的大多数代码都与向文件中的写入有关,而非与“多重映射”有关。与它有关的代码仅极少数而已。<br>
可通过两种方式建立一个DirMap(目录映射或对应)关系:默认构建器假定我们希望目录从当前位置向下展开,而另一个构建器让我们为起始目录指定一个备用的“绝对”路径。<br>
add()方法是一个采取的行动比较密集的场所。首先将directory()从我们想添加的SourceCodeFile里提取出来,然后检查散列表(Hashtable),看看其中是否已经包含了那个键。如果没有,就向散列表加入一个新的Vector,并将它同那个键关联到一起。到这时,不管采取的是什么途径,Vector都已经就位了,可以将它提取出来,以便添加SourceCodeFile。由于Vector可象这样同散列表方便地合并到一起,所以我们从两方面都能感觉得非常方便。<br>
写一个打包文件时,需打开一个准备写入的文件(当作DataOutputStream打开,使数据具有“通用”性),并在第一行写入与老的分隔符有关的头信息。接着产生对Hashtable键的一个Enumeration(枚举),并遍历其中,选择每一个目录,并取得与那个目录有关的Vector,使那个Vector中的每个SourceCodeFile都能写入打包文件中。<br>
用write()将Java源码文件写入它们对应的目录时,采用的方法几乎与writePackedFile()完全一致,因为两个方法都只需简单调用SourceCodeFile中适当的方法。但在这里,根路径会传递给SourceCodeFile.writeFile()。所有文件都写好后,名字中指定了已写文件数量的那个附加文件也会被写入。<br>
<br>
5. 主程序<br>
前面介绍的那些类都要在CodePackager中用到。大家首先看到的是用法字串。一旦最终用户不正确地调用了程序,就会打印出介绍正确用法的这个字串。调用这个字串的是usage()方法,同时还要退出程序。main()唯一的任务就是判断我们希望创建一个打包文件,还是希望从一个打包文件中提取什么东西。随后,它负责保证使用的是正确的参数,并调用适当的方法。<br>
创建一个打包文件时,它默认位于当前目录,所以我们用默认构建器创建DirMap。打开文件后,其中的每一行都会读入,并检查是否符合特殊的条件:<br>
(1) 若行首是一个用于源码列表的起始标记,就新建一个SourceCodeFile对象。构建器会读入源码列表剩下的所有内容。结果产生的句柄将直接加入DirMap。<br>
(2) 
若行首是一个用于源码列表的结束标记,表明某个地方出现错误,因为结束标记应当只能由SourceCodeFile构建器发现。<br>
<br>
提取/释放一个打包文件时,提取出来的内容可进入当前目录,亦可进入另一个备用目录。所以需要相应地创建DirMap对象。打开文件,并将第一行读入。老的文件路径分隔符信息将从这一行中提取出来。随后根据输入来创建第一个SourceCodeFile对象,它会加入DirMap。只要包含了一个文件,新的SourceCodeFile对象就会创建并加入(创建的最后一个用光输入内容后,会简单地返回,然后hasFile()会返回一个错误)。<br>
<br>
17.1.2 检查大小写样式<br>
尽管对涉及文字处理的一些项目来说,前例显得比较方便,但下面要介绍的项目却能立即发挥作用,因为它执行的是一个样式检查,以确保我们的大小写形式符合“事实上”的Java样式标准。它会在当前目录中打开每个.java文件,并提取出所有类名以及标识符。若发现有不符合Java样式的情况,就向我们提出报告。<br>
为了让这个程序正确运行,首先必须构建一个类名,将它作为一个“仓库”,负责容纳标准Java库中的所有类名。为达到这个目的,需遍历用于标准Java库的所有源码子目录,并在每个子目录都运行ClassScanner。至于参数,则提供仓库文件的名字(每次都用相同的路径和名字)和命令行开关-a,指出类名应当添加到该仓库文件中。<br>
为了用程序检查自己的代码,需要运行它,并向它传递要使用的仓库文件的路径与名字。它会检查当前目录中的所有类和标识符,并告诉我们哪些没有遵守典型的Java大写写规范。<br>
要注意这个程序并不是十全十美的。有些时候,它可能报告自己查到一个问题。但当我们仔细检查代码的时候,却发现没有什么需要更改的。尽管这有点儿烦人,但仍比自己动手检查代码中的所有错误强得多。<br>
下面列出源代码,后面有详细的解释:<br>
<br>
974-980页程序<br>
<br>
MultiStringMap类是个特殊的工具,允许我们将一组字串与每个键项对应(映射)起来。和前例一样,这里也使用了一个散列表(Hashtable),不过这次设置了继承。该散列表将键作为映射成为Vector值的单一的字串对待。add()方法的作用很简单,负责检查散列表里是否存在一个键。如果不存在,就在其中放置一个。getVector()方法为一个特定的键产生一个Vector;而printValues()将所有值逐个Vector地打印出来,这对程序的调试非常有用。<br>
为简化程序,来自标准Java库的类名全都置入一个Properties(属性)对象中(来自标准Java库)。记住Properties对象实际是个散列表,其中只容纳了用于键和值项的String对象。然而仅需一次方法调用,我们即可把它保存到磁盘,或者从磁盘中恢复。实际上,我们只需要一个名字列表,所以为键和值都使用了相同的对象。<br>
针对特定目录中的文件,为找出相应的类与标识符,我们使用了两个MultiStringMap:classMap以及identMap。此外在程序启动的时候,它会将标准类名仓库装载到名为classes的Properties对象中。一旦在本地目录发现了一个新类名,也会将其加入classes以及classMap。这样一来,classMap就可用于在本地目录的所有类间遍历,而且可用classes检查当前标记是不是一个类名(它标记着对象或方法定义的开始,所以收集接下去的记号——直到碰到一个分号——并将它们都置入identMap)。<br>
ClassScanner的默认构建器会创建一个由文件名构成的列表(采用FilenameFilter的JavaFilter实现形式,参见第10章)。随后会为每个文件名都调用scanListing()。<br>
在scanListing()内部,会打开源码文件,并将其转换成一个StreamTokenizer。根据Java帮助文档,将true传递给slashStartComments()和slashSlashComments()的本意应当是剥除那些注释内容,但这样做似乎有些问题(在Java 
1.0中几乎无效)。所以相反,那些行被当作注释标记出去,并用另一个方法来提取注释。为达到这个目的,'/'必须作为一个原始字符捕获,而不是让StreamTokeinzer将其当作注释的一部分对待。此时要用ordinaryChar()方法指示StreamTokenizer采取正确的操作。同样的道理也适用于点号('.'),因为我们希望让方法调用分离出单独的标识符。但对下划线来说,它最初是被StreamTokenizer当作一个单独的字符对待的,但此时应把它留作标识符的一部分,因为它在static 
final值中以TT_EOF等等形式使用。当然,这一点只对目前这个特殊的程序成立。wordChars()方法需要取得我们想添加的一系列字符,把它们留在作为一个单词看待的记号中。最后,在解析单行注释或者放弃一行的时候,我们需要知道一个换行动作什么时候发生。所以通过调用eollsSignificant(true),换行符(EOL)会被显示出来,而不是被StreamTokenizer吸收。<br>
scanListing()剩余的部分将读入和检查记号,直至文件尾。一旦nextToken()返回一个final 
static值——StreamTokenizer.TT_EOF,就标志着已经抵达文件尾部。<br>
若记号是个'/',意味着它可能是个注释,所以就调用eatComments(),对这种情况进行处理。我们在这儿唯一感兴趣的其他情况是它是否为一个单词,当然还可能存在另一些特殊情况。<br>
如果单词是class(类)或interface(接口),那么接着的记号就应当代表一个类或接口名字,并将其置入classes和classMap。若单词是import或者package,那么我们对这一行剩下的东西就没什么兴趣了。其他所有东西肯定是一个标识符(这是我们感兴趣的),或者是一个关键字(对此不感兴趣,但它们采用的肯定是小写形式,所以不必兴师动众地检查它们)。它们将加入到identMap。<br>
discardLine()方法是一个简单的工具,用于查找行末位置。注意每次得到一个新记号时,都必须检查行末。<br>
只要在主解析循环中碰到一个正斜杠,就会调用eatComments()方法。然而,这并不表示肯定遇到了一条注释,所以必须将接着的记号提取出来,检查它是一个正斜杠(那么这一行会被丢弃),还是一个星号。但假如两者都不是,意味着必须在主解析循环中将刚才取出的记号送回去!幸运的是,pushBack()方法允许我们将当前记号“压回”输入数据流。所以在主解析循环调用nextToken()的时候,它能正确地得到刚才送回的东西。<br>
为方便起见,classNames()方法产生了一个数组,其中包含了classes集合中的所有名字。这个方法未在程序中使用,但对代码的调试非常有用。<br>
接下来的两个方法是实际进行检查的地方。在checkClassNames()中,类名从classMap提取出来(请记住,classMap只包含了这个目录内的名字,它们按文件名组织,所以文件名可能伴随错误的类名打印出来)。为做到这一点,需要取出每个关联的Vector,并遍历其中,检查第一个字符是否为小写。若确实为小写,则打印出相应的出错提示消息。<br>
在checkIdentNames()中,我们采用了一种类似的方法:每个标识符名字都从identMap中提取出来。如果名字不在classes列表中,就认为它是一个标识符或者关键字。此时会检查一种特殊情况:如果标识符的长度等于3或者更长,而且所有字符都是大写的,则忽略此标识符,因为它可能是一个static 
final值,比如TT_EOF。当然,这并不是一种完美的算法,但它假定我们最终会注意到任何全大写标识符都是不合适的。<br>
这个方法并不是报告每一个以大写字符开头的标识符,而是跟踪那些已在一个名为reportSet()的Vector中报告过的。它将Vector当作一个“集合”对待,告诉我们一个项目是否已在那个集合中。该项目是通过将文件名和标识符连接起来生成的。若元素不在集合中,就加入它,然后产生报告。<br>
程序列表剩下的部分由main()构成,它负责控制命令行参数,并判断我们是准备在标准Java库的基础上构建由一系列类名构成的“仓库”,还是想检查已写好的那些代码的正确性。不管在哪种情况下,都会创建一个ClassScanner对象。<br>
无论准备构建一个“仓库”,还是准备使用一个现成的,都必须尝试打开现有仓库。通过创建一个File对象并测试是否存在,就可决定是否打开文件并在ClassScanner中装载classes这个Properties列表(使用load())。来自仓库的类将追加到由ClassScanner构建器发现的类后面,而不是将其覆盖。如果仅提供一个命令行参数,就意味着自己想对类名和标识符名字进行一次检查。但假如提供两个参数(第二个是&quot;-a&quot;),就表明自己想构成一个类名仓库。在这种情况下,需要打开一个输出文件,并用Properties.save()方法将列表写入一个文件,同时用一个字串提供文件头信息。<br>
<br>

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -