您的位置:首页 > 编程语言 > Java开发

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("您的试用次数已到,请购买正版");
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: