标签归档:Java

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

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

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

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

File file = new File(sourcePath);

try {
lineNumberReader = new LineNumberReader(new FileReader(file));
String line = lineNumberReader.readLine();
fileWriter = new FileWriter(file);
while (line != null) {
line = line.replaceAll("\\.xxx", ".XXX");
fileWriter.write(line+"\r\n");
line = lineNumberReader.readLine();
fileWriter.flush();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
lineNumberReader.close();
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}

执行的结果是,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,我们谁都不想书写平淡,总想语不惊人死不休。作为博客的人们可能这种感觉更为强烈,我是一名独立博客耶,我不能人云亦云啊,我得从我的文章里抒发我的思想啊。艹,你娘的思想也就大街上小摊上的胡萝卜包子一般廉价,还装。所以呢,我们还是需要不时的刺激一下自己的神经,同时呢,也给我们伟大的祖国发育不良的国联网添加一些有趣或无趣的素材资料吧,也算是为我国早日实现四个现代化添砖加瓦了。

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

public class JTree
extends JComponent
implements Scrollable, Accessible
How to Use Trees 一节。

树中特定的节点可以由 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,

                                       Object value,
                                       boolean selected,
                                       boolean expanded,
                                       boolean leaf,
                                       int row,
                                       boolean hasFocus)

在这个方法中,它的返回值为一个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文档在你家里叫你回家翻翻呢。