关闭

Java基础-IO学习之字符流

标签: java源码io流字符流
247人阅读 评论(0) 收藏 举报
分类:


字符流是什么

字符流是可以直接读写字符的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("您的试用次数已到,请购买正版");
			}
		}
	}


0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:18818次
    • 积分:839
    • 等级:
    • 排名:千里之外
    • 原创:67篇
    • 转载:7篇
    • 译文:0篇
    • 评论:7条