标签归档:Java

LineNumberReader 和 FileWriter 同时使用碰到的问题

今天上班有个任务就是将之前产品中的示范代码有一些不规范的地方进行修改,使得能在多平台顺利运行,主要是文件名大小写的问题。由于之前示范代码编写者在跨平台上编码经验相对不足,所以编写者在写代码的时候并没有严格的大小写意识 (主要原因还是因为 Windows 不区分大小写,而代码的测试均是在 Windows 下进行的),造成很多遗留问题。产品中有示范代码 64 个,大小写问题几乎无处不在,还有一个就是跨平台产品支持的数据引擎与 Windows 不一致,之前的 Windows 版本中有不少的示范代码使用了 Windows 平台特定的引擎类型,也需要进行修改。

修改的方法可以有很多,最为简单和直接的方法,应该是一个个程序运行,一个个文件检查,逐个进行修改。我想作为程序员的我们,肯定是不愿意做这么无聊而重复的工作的。那么搞定这个无聊而重复的工作呢?首先分析一下已有的示范代码的特征,虽然之前的代码存在诸多不规范的地方,但是在一些方面还是存在很多共性的,大小写出现错误的肯定是在设置数据路径的时候出现的,设置引擎类型的代码更是固定的,之需要使用几个非常简单的正则表达式就可以将其完全正确替换了。所以我们选择了读写文件的方法来完成这个任务。

在实现这个功能的时候,使用到了 LineNumberReader 和 FileWriter 两个类,每读取一行,使用 replaceAll(String regx,String str) 方法对该行进行替换,之后再使用 FileWriter 将替换后的文本写入文件:

执行的结果是,64 个示范代码中,总会有几个会出现一些很奇怪的问题,那就是修改后的文件丢失了很多文本,导致编译不通过。那么问题在哪儿呢?经过多次分析和调试,最终发现当 LineNumberReader 和 FileWriter 指向同一个文件的时候会出现该问题,如果将源文件处理后的结果写入到一个新的文件中便不会存在这样的问题。那么究竟是哪里出了问题呢?来看一张图

在这张图中我们能看到一些信息,LineNumberReader 对象中有一个 cb 字符数组,大小为 8192=1024*8.

在我整个的调试过程中,我一直关注着最后一个字符,在整个过程中,这个位置的字符一直没有发生改变,而最终写入到文件中的最后一个字符就是该字符,这难道只是一个巧合吗?

现在让我们更换一下代码将原本使用同一个 File 对象的代码,改成使用另一个新文件对象的代码来重新调试一下。

先执行代码,查看结果发现,文本的内容完全被写入到新文件中,并没有出现丢失文本这样的情况,那么我们再来调试一下 ,我们再次来监视 cb[8191] 处的值,这次我们可以发现在 LineNumberReader 对象 lineNumber==319 的时候,该值发生改变。

====我是分割线======

综合上面的两种情况,我们可以得出一个结论,当 LineNumberReader 和 FileWriter 同时指向一个文件的时候,系统在写入文件的时候并不是逐行逐行写入,而是先记录下来逐行的内容,并且是记录到 LineNumberReader 的 cb 字符数组中。该数组的大小固定为内存最小单位 8K,也就是大小为 8192 的数组,该数组将保存初次初始化 FileWriter 对象时读取的文件内容,在之后的每次 FileWriter 写入操作均是改变该数组中存储的字符值。这样导致的结果就是,如果当前的读取的文件较大,就会存在丢失内容的问题 (最多保存 8192 个字符, 含空格和换行)。而使用一个新的文件来进行修改后内容的写入就不存在这样的问题,当 FileWriter 需要使用到 flush() 方法来将当前缓冲区中的所有内容写入文件之后,LineNumberReader 中的 cb 数组内容也将发生改变,指针下移,读取下一个块大小为 8192 的内容,直到文件末尾。

所以在使用 LineNumberReader 和 FileWriter 的时候需要注意这一点,如果实在不愿意多生成一堆新文件的话,可以在写入完成之后,将原始文件删除,而将新文件更名一下就好了。

关于 JTree 的一些碎碎念

本博号称关注 Web2.0, Ruby/Rails,Java。但是据本人所知,到目前为止还没有任何一篇关于 Java 方面的文章和只言片语。作为本博的博主,确实有点大言不惭的感觉,一想到这个心中就颇不宁静啊。

其实作为一名博客,写东西的欲望一定要强烈,很多的时候我们并没有很多的素材可以写,因为生活几乎每天都是 Just so so,我们谁都不想书写平淡,总想语不惊人死不休。作为博客的人们可能这种感觉更为强烈,我是一名独立博客耶,我不能人云亦云啊,我得从我的文章里抒发我的思想啊。艹,你娘的思想也就大街上小摊上的胡萝卜包子一般廉价,还装。所以呢,我们还是需要不时的刺激一下自己的神经,同时呢,也给我们伟大的祖国发育不良的国联网添加一些有趣或无趣的素材资料吧,也算是为我国早日实现四个现代化添砖加瓦了。

===========我是分割线=========

树中特定的节点可以由 TreePath(封装节点及其所有祖先的对象)标识,或由其显示行(其中显示区域中的每一行都显示一个节点)标识。展开 节点是一个非叶节点(由返回 false 的 TreeModel.isLeaf(node) 标识),当 展开 其所有祖先时,该节点将显示其子节点。 折叠 节点是隐藏它们的节点。 隐藏 节点是位于折叠祖先下面的节点。所有 可查看 节点的父节点都是可以展开的,但是可以显示它们,也可以不显示它们。 显示 节点是可查看的并且位于可以看到它的显示区域。

这是 JDK 5.0 非官方中文文档中的描述,我想这个描述给我们的第一个直观印象就是这个控件应该和 Windows 资源管理器中的树状结构的表现是一致的。确实如此,JTree 确实是用来作为树形展示使用的,因为我们在很多的数据管理和业务处理上,秉承了我们自古就袭承的分类方式,所以我们有很多的业务可能用到或者说可以用到 JTree。

那么 JTree 怎么用的呢?这个问题我曾经问过自己很多次,还做过很多相关的工作来学习 JTree 的使用,记得来到北京实习做的第一个学习阶段作业就是使用公司已有的组件产品做一个叠加分析的 Demo。作为一个比较友好的叠加分析工具,就肯定需要一些 GIS 数据处理的功能,比如打开工作空间,数据源,数据集,地图,同时还应该能简单地做一些图层删除或者地图关闭之类的基础功能。这样一来,主体的工作就是基础功能模块的实现了,只是在基础功能模块的基础上,添加一个叠加分析功能模块,而在基础功能模块中的可视化显示 JTree 必然是首选控件。

在实现树状加载和显示工作空间,参考公司已有的桌面产品 UI 设计,需要再 JTree 的每个节点之前添加一些图标,用于标识当前节点的数据类型,例如线数据集和面数据集的图标是有明显差异的,这样对用户的友好度会提高很多。默认的 JTree 使用 Java 默认的 meta 风格显示文件夹 (根节点) 和文件 (叶节点) 的图标样式,那么如何添加自定义的图标显示呢?当时我就在 JTree 的文档中苦苦找寻,希望自己能找到一个 setIcon() 的方法,最终无耻的失败鸟。后来才知道之需要自己实现一个类,实现 TreeCellRenderer 就可以了,重写下面这个方法 (Sun 就是这么排版的,我觉得这样挺好的,不要怪我占太多行了)

Component getTreeCellRendererComponent(JTree tree,

在这个方法中,它的返回值为一个 Component,那么我们完全可以自由发挥了,比如返回一个 JLabel。当时欣喜若狂,赶紧将代码巴拉巴拉地敲好,一看效果显著啊,根据类型不同生成不同的 Icon 设置给将要返回的 JLabel 对象就 OK 了。

实习的工作相对简单,当时对 JTree 的理解也就点到为止了。那么此次项目组需要退出 Objects Java 的控件,其中就有一些控件需要使用到 JTree 来实现,功能要求相对就复杂了不少。

需要在一个节点文本前,显示多个图标,除了用于区别节点数据类型的图标外,还需要添加几个操作图标,用于可视化节点数据的操作,那么返回一个什么呢?JPanel 无疑是最好的选择,轻量化容器中的万金油。在 JPanel 中添加几个可视化的 Icon 并不是难事,只是 JLabel 多少的问题了,通过对节点数据类型的判断分别设置便好了。问题是,如何判断用户当前点击的位置落在 JPanel 中的那个操作图标上呢?又如何将这些操作直接反映到实际的数据上来呢?通过 JTree.getUI() 方法获取当前的 JTree 的 UI,然后调用 TreeUI.getPathBounds(JTree tree, TreePath path) 方法获取当前节点的绘制区域,之后通过鼠标事件 MouseEvent 的 getX() 和 getY() 方法获取当前鼠标的位置信息,由于控件的编写图标的大小由自己定义,所以可以确定图标的大小和 JPanel 的布局 (我使用的是 FlowLayout,并且使用 setHgap() 方法将水平控件间距设置为 0),通过像素坐标的计算便可以得出当前用户鼠标单击事件应该响应哪个对应索引处的动作。

在实现的过程中,还需要使用到编辑节点的功能,那么如何来做呢?首选实现 TreeCellEditor 接口,完全自己定制,可以避免像继承自 DefaultTreeCellEditor 的烦恼 (因为它能满足一般需求,但是会限制你拳脚)。TreeCellEditor 也有一个很嚣张的方法,号称自己要接管 TreeCellRenderer 的方法,就是这个啦

Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row)

重写这个方法,用来自定义编辑时的节点渲染,例如我的树形控件在编辑的时候只能编辑节点名称,那么前面的图标还是要保持与未编辑时一致的,编辑时只需要将后面的文本置换为一个 JTextField 就可以了。然后为该 JTextField 注册一个键盘事件监听器,当输入 Enter 的时候,调用 TreeCellEditor 的 stopEditing() 方法,触发停止编辑事件。如果我们需要将编辑保存至数据模型中的话,就在 stopEditing() 方法中编写业务代码即可。

综上 JTree 这个控件实际上是一个完全遵循 MVC 架构的设计,甚至比 MVC 还抽象一些。JTree 中只处理数据和显示的一些控制,例如判断当前节点是否能够被编辑的方法 isPathEditable(TreePath path) 等。数据模型完全可以委托为 TreeModel 来管理,之需要使用 setModel 便可以轻松地将 JTree 和 TreeModel 关联起来。而在其显示渲染上便是完全由 TreeCellRenderer 来接管的,只需实现一个类,该类实现 TreeCellRenderer 接口,重写 getTreeCellRendererComponent(…) 方法,便可实现完全自定义面板显示节点。抽象最高层的是,当节点为可编辑状态时,其渲染又交由 TreeCellEditor 接管了,在编辑节点时,TreeCellEditor 的 getTreeCellRendererComponent(…) 方法便将完全接管节点的渲染工作,并且控制编辑时的动作,例如取消编辑和停止编辑之类的种种。这么看来,其实 JTree 只是提供一个壳子,通过其内部的 TreeMod 存储数据,外部的 TreeCellRenderer 和 TreeCellEditor 来控制其显示渲染和编辑渲染,给开发人员提供了一个可高度定制化的控件接口。

==========我还是分割线=========

本文的形成是如行云流水账一般啊,我也有点不知所云了,众位看官,能看则看,不能看则拉倒吧。JDK API 文档在你家里叫你回家翻翻呢。