Java基础-IO学习之字符流
2016-08-30 19:32
337 查看
字符流是什么
字符流是可以直接读写字符的IO流字符流读取字符, 就要先读取到字节数据, 然后转为字符. 如果要写出字符, 需要把字符转为字节再写出.
在前面简单介绍了IO和学习了字节流(点此传送门),学习完字节流后你会发现基本上字符流可以自学了,因为许多用法都非常相似。
两图看完字符流
(注:图片来源于:http://ankonlili.iteye.com)
其中深色的为节点流,浅色的为处理流。在此简单介绍下两种区别
按照流是否直接与特定的地方(如磁盘、内存、设备等)相连,分为节点流和处理流两类。
节点流:可以从或向一个特定的地方(节点)读写数据。如FileReader.
处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader.
字符流 FileReader
FileReader类的read()方法可以按照字符大小读取public static void main(String[] args) throws IOException { FileReader fr = new FileReader("read.txt"); int ch; //根据项目默认的码表一次读取一个字符 while((ch = fr.read()) != -1) { System.out.println((char)ch); } fr.close(); }
字符流 FileWriter
FileWriter类的write()方法可以自动把字符转为字节写出public static void main(String[] args) throws IOException { FileWriter fw = new FileWriter("write.txt"); fw.write("world你好"); fw.close(); }
万一你忘记关闭流了这么办?结果会一样吗?(将fw.close() 注释)
你会发现,什么都没有,在讲BufferedOutputStream时说过,该close方法带自动刷新缓冲区功能,难道FileWriter里也有缓冲区?
答案是明显的,该缓冲区是在Writer里的(继承下来),查看该源码
public abstract class Writer implements Appendable, Closeable, Flushable { private char[] writeBuffer;//缓冲区数组 private static final int WRITE_BUFFER_SIZE = 1024;//缓冲区大小 //相当于容量为2k的缓冲区 }
字符流的拷贝
public static void main(String[] args) throws IOException { FileReader fr = new FileReader("read.txt"); FileWriter fw = new FileWriter("write.txt"); int ch; while((ch = fr.read()) != -1) { fw.write(ch); } fr.close(); fw.close(); }
你会发现向上面的这样单纯拷贝文本文件,是不用字符流拷贝也是可以的(即便里面有中文字符)。为什么?
首先来讲讲码表,计算机内保存在内存、硬盘的数据都是二进制的,为什么我们能看到字符,就是码表的功能。
码表其实就是一个字符和其对应的二进制相互映射的一张表。这张表中规定了字符和二进制的映射关系。计算机存储字符时将字符查询码表,然后存储对应的二进制计算机;取出字符时将二进制查询码表,然后转换成对应的字符显示。
当使用字节流时,他会一个字节一个字节不赖的copy过去,最后根据其码表显示。而字符的拷贝还是要先将字节形式读取,在根据码表转换为字符,最后再根据码表转换为字节写入,最后根据其码表显示。这样看来,使用字符的copy是不是挺多余的。
字符流拷贝简单原理图:
假设使用GBK码表(中文占两个字节,第一个字节为负数,第二个字节为正数和负数),由于中文第一个字节肯定是负数,在读取的时候,若发现第一个字节为负数,则一起读两个字节,这样就通过码表读取了一个中文字符了
Q:那什么情况下该使用字符流呢?
上面说copy使用字符流是多余的,下面讲什么情况需要使用字符流程序就只需要读取一段文本, 或者就只需要写出一段文本的时候可以使用字符流,因为这个字节流不好操作(可以看前一篇字节流的中文乱码问题)
读取的时候是按照字符的大小读取的,不会出现半个中文;写出的时候可以直接将字符串写出,不用转换为字节数组
Q:字符流是否可以拷贝非纯文本的文件?
不可以拷贝非纯文本的文件(音频,视频,图片等)因为在读的时候会将字节转换为字符,在转换过程中,可能找不到对应的字符(在对应的码表中找不到),就会用?代替,写出的时候会将字符转换成字节写出去。但是如果是?就直接写出进去,所以使用字符流拷贝后打开会发现提示文件已损坏。
自定义字符数组的拷贝
public static void main(String[] args) throws IOException { FileReader fileReader = new FileReader("read.txt"); FileWriter fileWriter = new FileWriter("write.txt"); int len; char[] arr = new char[1024*8]; while((len = fileReader.read(arr)) != -1) { fileWriter.write(arr,0,len); } fileReader.close(); fileWriter.close(); }
带缓冲的字符流
BufferedReader的read()方法读取字符时会一次读取若干字符到缓冲区, 然后逐个返回给程序, 降低读取文件的次数, 提高效率BufferedWriter的write()方法写出字符时会先写到缓冲区, 缓冲区写满时才会写到文件, 降低写文件的次数, 提高效率
public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new FileReader("read.txt")); BufferedWriter bw = new BufferedWriter(new FileWriter("write.txt")); int ch; //read一次,会先将缓冲区读满,从缓冲去中一个一个的返给临时变量ch while((ch = br.read()) != -1) { bw.write(ch);//write一次,是将数据装到字符数组,装满后再一起写出去 } br.close(); bw.close();//带刷新缓冲区 }
其原理性是和字节流(BufferedInputStream,BufferedOutputStream)是一样的,只不过一个操作着byte,一个操作着char
readLine()和newLine()方法
BufferedReader的readLine()方法可以读取一行字符(不包含换行符号)BufferedWriter的newLine()可以输出一个跨平台的换行符号"\r\n"(win下)
public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new FileReader("read.txt")); BufferedWriter bw = new BufferedWriter(new FileWriter("write.txt")); String str; while((str = br.readLine()) != null) { bw.write(str); //bw.write("\r\n"); //只支持windows系统 bw.newLine(); //跨平台的 } br.close(); bw.close(); }read文件
write文件
如何判断一行读取完毕?
查看API可得:
读取一个文本行。通过下列字符之一即可认为某行已终止:换行 ('\n')、回车 ('\r')
Q:前面讲了FileWriter带有缓冲功能,那么为什么还需要BufferedWriter
它们不同在于缓冲区的大小。BufferedWriter更合理的使用缓冲区,在你对大量的数据时,FileWrite的效率明显不如BufferedWriter。BufferedReader源码分析(JDK1.8)
在前一篇分析过 BufferedInputStream 的源码,感觉BufferedReader的源码和BufferedInputStream其原理性很多东西都相同,若上一篇有解释过,这里就不做太多的解释了相同的,在BufferedReader中也有重复读(re-readthe same bytes)
//缓冲区 private char cb[]; //nChars为缓冲区内字符个数,nextChar为当前缓冲区位置(pos) private int nChars, nextChar; //标记失效的情况(当nextChar-markedChar超过readAheadLimit标记失效) private static final int INVALIDATED = -2; //未mark的情况 private static final int UNMARKED = -1; //标记的位置 private int markedChar = UNMARKED; //最多能mark的字节长度,也就是从markedChar位置到当前nextChar的最大长度 private int readAheadLimit = 0; public void mark(int readAheadLimit) throws IOException { if (readAheadLimit < 0) { throw new IllegalArgumentException("Read-ahead limit < 0"); } synchronized (lock) { ensureOpen(); this.readAheadLimit = readAheadLimit;//记录最多能mark的字节长度 markedChar = nextChar;//markedChar标记使用当前缓冲区位置 markedSkipLF = skipLF;//skipLF这个后面说 } } public void reset() throws IOException { synchronized (lock) { ensureOpen(); if (markedChar < 0) throw new IOException((markedChar == INVALIDATED) ? "Mark invalid" : "Stream not marked"); nextChar = markedChar;//使用markedChar还原当前位置 skipLF = markedSkipLF; } }
再来分析下fill()方法,这里对于每种情况就不分开详细说明,直接在源码中说明,详细可以看我的前一篇(字节流),因为其原理性都是一样的
private void fill() throws IOException { int dst;//需要填充缓冲区的起始位置 if (markedChar <= UNMARKED) { //这是未mark的情况,直接将缓冲区清空即可 dst = 0;//起始位置为0 } else { /* Marked */ int delta = nextChar - markedChar; if (delta >= readAheadLimit) {//这里判断标记是否失效 /* Gone past read-ahead limit: Invalidate mark */ markedChar = INVALIDATED; readAheadLimit = 0; dst = 0; } else {//这里如果标记未失效且readAheadLimit小于缓冲区大小 if (readAheadLimit <= cb.length) { //那么我们可以将需要markedChar指针移动到0位置 //即将markedChar之前的缓冲区内容清空(并往前移动),因为reset不需要 System.arraycopy(cb, markedChar, cb, 0, delta); markedChar = 0;//markedChar已经被移动到cb[0] dst = delta;//填充的起始位置为delta } else { //否则将扩容cb,扩容大小到readAheadLimit char ncb[] = new char[readAheadLimit]; System.arraycopy(cb, markedChar, ncb, 0, delta); cb = ncb; markedChar = 0; dst = delta; } nextChar = nChars = delta; } } int n; do {//这里往缓冲区内从dst开始将缓冲区读满 n = in.read(cb, dst, cb.length - dst); } while (n == 0); if (n > 0) {//当n等于-1时说明已经读取完毕,否则出现设置nChars和nextChar nChars = dst + n; nextChar = dst; } }
最后分析下读取方法
public class BufferedReader extends Reader { //底层需要包装的Reader private Reader in; //缓冲区 private char cb[]; //nChars为缓冲区内字符个数,nextChar为当前缓冲区位置(pos) private int nChars, nextChar; //默认缓冲区的大小,这里说明缓冲区大小为16k private static int defaultCharBufferSize = 8192; //默认一行的字符个数 private static int defaultExpectedLineLength = 80; //用于是否忽略换行符的标记(readLine方法是不读取换行符的) private boolean skipLF = false; public int read() throws IOException { synchronized (lock) { ensureOpen(); //判断其底层包装的Reader是否关闭 for (;;) { if (nextChar >= nChars) {//当前缓冲区位置大于等于缓冲区内字符个数 fill();//再次从文件中读取字符 if (nextChar >= nChars)//若是还是大于那就说明已经到文件末尾了 return -1;//返回 int -1 } if (skipLF) {//判断是否需要忽略换行符 skipLF = false; if (cb[nextChar] == '\n') { nextChar++; continue; } } //返回nextChar当前指向的位置字符,自动提升为int return cb[nextChar++]; } } } //readLine方法 public String readLine() throws IOException { return readLine(false); } //ignoreLF始终为false String readLine(boolean ignoreLF) throws IOException { StringBuffer s = null; int startChar; synchronized (lock) { ensureOpen();//判断其底层包装的Reader是否关闭 boolean omitLF = ignoreLF || skipLF;//这里就看skipLF的 bufferLoop: for (;;) { if (nextChar >= nChars) fill();//这里和上面一样从缓冲区内读取字符 if (nextChar >= nChars) { //到达文件末尾 if (s != null && s.length() > 0) return s.toString(); else//这里可以说明readLine方法读取到末尾返回null return null; } boolean eol = false; char c = 0; int i; //这里进行了判断后nextChar++,很明显是为了忽略'\n' //看完下面你会明白omitLF的作用 if (omitLF && (cb[nextChar] == '\n')) nextChar++; //下面都进行重新初始化 skipLF = false; omitLF = false; charLoop://从当前缓冲区位置开始,到nChars结束遍历缓冲区 for (i = nextChar; i < nChars; i++) { c = cb[i]; //下面都拿win(\r\n)来说,当遇到\r时便退出了循环此时c = '\r' if ((c == '\n') || (c == '\r')) { eol = true;//当等于'\n'或者'\r'便退出循环,并设置eol标志为true break charLoop;//退出循环 } } startChar = nextChar;//记录nextChar为一行的开始位置 nextChar = i;//将i赋值给nextChar,此时 cb[nextChar] = '\r' if (eol) {//此时说明遇到了'\n'或者'\r'了 String str; if (s == null) {//将这一行在缓冲区中的字符数据转换为String str = new String(cb, startChar, i - startChar); } else {//s不为null则往后追加再转化为字符串 s.append(cb, startChar, i - startChar); str = s.toString(); } nextChar++;//这里++后 此时 cb[nextChar] = \n' if (c == '\r') {//前面有判断过此时c = '\r' skipLF = true;//将skipLF设置为true } //说明了读到"\r"字符之后skipLF都会被设置为true,通过这样来忽略掉之后的"\n" return str;//返回str } //如果没有遇到'\n'或者'\r',则需要继续循环并fill(),在进行判断 if (s == null) s = new StringBuffer(defaultExpectedLineLength); s.append(cb, startChar, i - startChar); } } } //close方法 public void close() throws IOException { synchronized (lock) { if (in == null)//判断in是否为null,不为null则将其关闭 return; try { in.close(); } finally { in = null; cb = null; } } } }
BufferedWriter源码分析(JDK1.8)
public class BufferedWriter extends Writer { //需要包装的Writer private Writer out; //缓冲区字符数组 private char cb[]; //缓冲区字符总数和缓冲区当前需写位置 private int nChars, nextChar; //默认缓冲区大小,总共大小为16k private static int defaultCharBufferSize = 8192; // 行分割符 private String lineSeparator; public BufferedWriter(Writer out, int sz) { super(out);//传入需要包装的Writer和需要设置的缓冲区大小 if (sz <= 0) throw new IllegalArgumentException("Buffer size <= 0"); this.out = out; cb = new char[sz]; nChars = sz; nextChar = 0; //行分割符根据系统平台决定 lineSeparator = java.security.AccessController.doPrivileged( new sun.security.action.GetPropertyAction("line.separator")); } //刷新缓冲区 void flushBuffer() throws IOException { synchronized (lock) { ensureOpen(); if (nextChar == 0) return; out.write(cb, 0, nextChar);//将nextChar前的所有字符写入 nextChar = 0;//将nextChar归0 } } private void ensureOpen() throws IOException { if (out == null) throw new IOException("Stream closed"); } public void write(int c) throws IOException { synchronized (lock) { ensureOpen();//判断底层的Writer是否打开 if (nextChar >= nChars)//此时缓冲区数据已满,需刷新写入 flushBuffer(); cb[nextChar++] = (char) c;//强转为char后直接写入缓冲区 } } //写入一个换行符 public void newLine() throws IOException { write(lineSeparator); } //关闭流 public void close() throws IOException { synchronized (lock) { if (out == null) { return; } try (Writer w = out) {//刷新流后自动关闭 flushBuffer(); } finally { out = null; cb = null; } } } }相比之下,BufferedWriter简单许多
LineNumberReader
LineNumberReader是BufferedReader的子类, 具有相同的功能, 并且可以统计行号调用getLineNumber()方法可以获取当前行号
调用setLineNumber()方法可以设置当前行号
public static void main(String[] args) throws IOException { LineNumberReader lnr = new LineNumberReader(new FileReader("read.txt")); String line; lnr.setLineNumber(100); while((line = lnr.readLine()) != null) { System.out.println(lnr.getLineNumber() + ": " + line); } lnr.close(); } /* * outPut: * 101: 世界你好 * 102: 你好世界 */
源码简单分析(JDK1.8)
public String readLine() throws IOException { synchronized (lock) { String l = super.readLine(skipLF); skipLF = false; if (l != null)//每readLine一次后便会 ++ lineNumber++; return l; } }看其源码可得知每readLine后便++,所以设置lineNumber为100后第一行输出的为101
转换流
转换流是指将字节流与字符流之间的转换,包含两个类:InputStreamReader和OutputStreamWriter。假设从UTF-8的文件中读取数据并将其写入到GBK文件中,该如何操作?
FileInputStream是使用默认码表读取文件, 如果需要使用指定码表读取, 那么可以使用InputStreamReader(字节流,编码表)
FileOutputStream是使用默认码表写出文件, 如果需要使用指定码表写出, 那么可以使用OutputStreamWriter(字节流,编码表)
public static void main(String[] args) throws IOException { // InputStreamReader isr = new InputStreamReader(new FileInputStream("utf-8.txt"), "UTF-8"); // OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("gbk.txt"), "GBK"); //更高效的读写,使用Buffer进行包装 BufferedReader br = new BufferedReader( new InputStreamReader(new FileInputStream("utf-8.txt"), "UTF-8")); BufferedWriter bw = new BufferedWriter( new OutputStreamWriter(new FileOutputStream("gbk.txt"), "GBK")); int ch; while((ch = br.read()) != -1) { bw.write(ch); } bw.close(); br.close(); }
上面的2为 回车(\r\n)网上说GBK不论中、英文字符均使用双字节来表示,那么我这里的英文明明只占1个字节?
这里其实汉字、全角字符以及其它扩展字符才是双字节编码,而半角字符还在只占一个字节(兼容ASCII码表)
简单原理图:
IO小习
尝试进行如下简单模拟操作:当我们下载一个试用版软件,没有购买正版的时候,每执行一次就会提醒我们还有多少次使用机会,用学过的IO流知识,模拟试用版软件,试用10次机会,执行一次就提示一次您还有几次机会,如果次数到了提示请购买正版public static void main(String[] args) throws IOException { //config.txt 存放软件剩余次数 BufferedReader br = new BufferedReader(new FileReader("config.txt")); String line = br.readLine(); br.close(); int times; try { times = Integer.parseInt(line); } catch (NumberFormatException e) { System.out.println("系统配置文件有损坏!"); times = -1; } if(times != -1) { if(times > 0) { System.out.println("您还有" + times-- + "次机会"); FileWriter fw = new FileWriter("config.txt"); fw.write(times+"");//将剩余次数重新写入 fw.close(); } else { System.out.println("您的试用次数已到,请购买正版"); } } }
相关文章推荐
- java基础学习之io读写--字符流
- Java IO学习基础之读写文本文件
- Java IO学习基础之读写文本文件
- java 基础之 IO(字节流和字符流)
- Java IO学习笔记(三):字节流与字符流
- Java IO学习3:字节-字符转换流
- Java IO学习基础之读写文本文-Java基础-Java-编程开发
- Java基础17-IO之字符流
- Java IO学习笔记(三):字节流与字符流
- java io学习基础之 文本文件与二进制文件的区别(转)
- Java IO学习基础之读写文本文件
- Java IO学习基础之读写文本文件
- Java IO学习基础之读写文本文件
- Java IO学习基础之读写文本文件
- Java IO学习基础之读写文本文件
- Java IO学习笔记:字符流
- Java IO学习笔记:字符流
- 黑马程序员________Java中IO技术字节流字符流的应用及File类学习笔记
- Java IO学习基础之读写文本文件
- 黑马程序员---java IO-字符流 学习笔记