------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------
IO流体验与简介
字节流
输入流:java.io.InputStream
输出流:java.io.OutputStream
特点:
字节流的抽象基类派生出来的子类名称都是以其父类名作为子类名的后缀。
如:FileInputStream, ByteArrayInputStream等。
说明:
字节流处理的单元是一个字节,用于操作二进制文件(计算机中所有文件都是二进制文件)
1.1.1. 输入流读取方式1:
read方法()
一次读取一个字节,读到文件末尾返回-1.
仔细查看api文档发现read方法如果读到文件的末尾会返回-1。那么就可以通过read方法的返回值是否是-1来控制我们的循环读取。
/** * 根据read方法返回值的特性,如果独到文件的末尾返回-1,如果不为-1就继续向下读。 * */ privatestaticvoid showContent(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
int len = fis.read(); while (len != -1) { System.out.print((char)len); len = fis.read();
} // 使用完关闭流 fis.close(); } |
我们习惯这样写:
/** * 根据read方法返回值的特性,如果独到文件的末尾返回-1,如果不为-1就继续向下读。 * */ privatestaticvoid showContent(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
int len; while ((len = fis.read()) != -1) { System.out.print((char) len); } // 使用完关闭流 fis.close(); } |
1.1.2. 输入流读取方式2:
使用read(byte[] b) 方法。使用缓冲区(关键是缓冲区大小的确定)
使用read方法的时候,流需要读一次就处理一次,可以将读到的数据装入到字节数组中,一次性的操作数组,可以提高效率。
问题1:缓冲区大小
那么字节数组如何定义?定义多大?
可以尝试初始化长度为5的byte数组。通过read方法,往byte数组中存内容
那么该read方法返回的是往数组中存了多少字节。
/** * 使用字节数组存储读到的数据 * */ privatestaticvoid showContent2(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
// 通过流读取内容 byte[] byt = newbyte[5]; int len = fis.read(byt); for (int i = 0; i < byt.length; i++) { System.out.print((char) byt[i]); }
// 使用完关闭流 fis.close(); } |
问题1: 缓冲区太小:
数据读取不完.
测试发现问题,由于数组太小,只装了5个字节。而文本的字节大于数组的长度。那么很显然可以将数组的长度定义大一些。例如1024个。
/** * 使用字节数组存储读到的数据 * */ privatestaticvoid showContent2(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
// 通过流读取内容 byte[] byt = newbyte[1024]; int len = fis.read(byt); for (int i = 0; i < byt.length; i++) { System.out.print(byt[i]); }
// 使用完关闭流 fis.close(); } |
问题三:缓冲区有默认值.
测试,打印的效果打印出了很多0,因为数组数组有默认初始化值,所以,我们将数组的数据全部都遍历和出来.现在需要的是取出数组中的部分数据.需要将循环条件修改仔细查看api文档。发现该方法read(byte[] b)返回的是往数组中存入了多少个字节。就是数组实际存储的数据个数。
/** * 使用字节数组存储读到的数据 * */ privatestaticvoid showContent2(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
// 通过流读取内容 byte[] byt = newbyte[1024]; int len = fis.read(byt); for (int i = 0; i <len; i++) { System.out.print(byt[i]); }
// 使用完关闭流 fis.close(); } |
总结:
问题一:为什么打印的不是字母而是数字,
是字母对应的码值。
如何显示字符,强转为char即可
问题二:注意:回车和换行的问题。
windows的换车和换行是"\r\n" 对应码表是13和10 。
1.1.3. 输入流读取方式3:
使用read(byte[] b,int off,int len)
查看api文档,
b显然是一个byte类型数组,当做容器来使用
off,是指定从数组的什么位置开始存字节
len,希望读多少个
其实就是把数组的一部分当做流的容器来使用。告诉容器,从什么地方开始装要装多少。
/** * 把数组的一部分当做流的容器来使用 * read(byte[] b,int off,int len) */ privatestaticvoid showContent3(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
// 通过流读取内容 byte[] byt = newbyte[1024]; // 从什么地方开始存读到的数据 int start = 5;
// 希望最多读多少个(如果是流的末尾,流中没有足够数据) int maxLen = 6;
// 实际存放了多少个 int len = fis.read(byt, start, maxLen);
for (int i = start; i < start + maxLen; i++) { System.out.print((char) byt[i]); }
// 使用完关闭流 fis.close(); } |
需求2:测试skip方法
通过Io流,读取"c:/a.txt"文件中的第9个字节到最后所有的内容并在控制台显示出来。
分析:其实就是要跳过文件中的一部分字节,需要查找API文档。可以使用skip方法skip(long n),参数跟的是要跳过的字节数。
我们要从第9个开始读,那么要跳过前8个即可。
/** * skip方法 * * */ privatestaticvoid showContent4(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
// 通过流读取内容 byte[] byt = newbyte[1024]; fis.skip(8); int len = fis.read(byt); System.out.println(len); System.out.println("**********"); for (int i = 0; i < len; i++) { System.out.println((char) byt[i]); } // 使用完关闭流 fis.close(); } |
1.1.4. 输入流读取方式4:
使用缓冲(提高效率),并循环读取(读完所有内容).
总结:读完文件的所有内容。很显然可以使用普通的read方法,一次读一个字节直到读到文件末尾。为了提高效率可以使用read(byte[] byt);方法就是所谓的使用缓冲提高效率。我们可以读取大文本数据测试(大于1K的文本文件.)
/** * 使用字节数组当缓冲 * */ privatestaticvoid showContent5(String path) throws IOException { FileInputStream fis = new FileInputStream(path); byte[] byt = newbyte[1024]; int len = fis.read(byt); System.out.println(len); String buffer = new String(byt, 0, len); System.out.print(buffer); } |
注意:如何将字节数组转成字符串? 可以通过创建字符串对象即可。
发现:一旦数据超过1024个字节,数组就存储不下。
如何将文件的剩余内容读完?
我们可以通过通过循环保证文件读取完。
/** * 使用字节数组当缓冲 * */ privatestaticvoid showContent7(String path) throws IOException { FileInputStream fis = new FileInputStream(path); byte[] byt = newbyte[1024]; int len = 0; while ((len = fis.read(byt)) != -1) { System.out.println(new String(byt, 0, len)); } } |
1.2. OutputStream
字节输出流
案例:
1,写代码实现把"Hello World!"写到"c:/a.txt"文件中。
a, c:/a.txt不存在时,测试一下。
b, c:/a.txt存在时,也测试一下。
要写两个版本:
a, 使用write(int b) 实现。
b, 使用write(byte[] b) 实现。
2,在已存在的c:/a.txt文本文件中追加内容:“Java IO”。
显然此时需要向指定文件中写入数据。
使用的就是可以操作文件的字节流对象。OutputStream。该类是抽象类,需要使用具体的实现类来创建对象查看API文档,找到了OutputStream的实现类FileOutputStream 创建FileOutputStream 流对象,必须指定数据要存放的目的地。通过构造函数的形式。创建流对象时,调用了系统底层的资源。在指定位置建立了数据存放的目的文件。
流程: 1:打开文件输出流,流的目的地是指定的文件 2:通过流向文件写数据 3: 用完流后关闭流 |
1.2.1. 输出流写出方式1:
使用write(int b)方法,一次写出一个字节.
在C盘下创建a.txt文本文件
import java.io.FileOutputStream; import java.io.IOException;
publicclass IoTest2 { publicstaticvoid main(String[] args) throws IOException { String path = "c:\\a.txt"; writeTxtFile(path); }
privatestaticvoid writeTxtFile(String path) throws IOException { // 1:打开文件输出流,流的目的地是指定的文件 FileOutputStream fos = new FileOutputStream(path);
// 2:通过流向文件写数据 fos.write('j'); fos.write('a'); fos.write('v'); fos.write('a'); // 3:用完流后关闭流 fos.close();
} }
|
当c盘下的a.txt不存在会怎么样?
测试:将c盘下的a.txt文件删除,发现当文件不存在时,会自动创建一个。
注意:使用write(int b)方法,虽然接收的是int类型参数,但是write 的常规协定是:向输出流写入一个字节。要写入的字节是参数 b 的八个低位。b 的 24 个高位将被忽略。
1.2.2. 输出流写出方式2:
使用write(byte[] b),就是使用缓冲.提高效率.
上述案例中的使用了OutputStram 的write方法,一次只能写一个字节。成功的向文件中写入了内容。但是并不高效,如和提高效率呢?是否应该使用缓冲,根据字节输入流的缓冲原理,是否可以将数据保存中字节数组中。通过操作字节数组来提高效率。查找API文档,在OutputStram类中找到了write(byte[] b)方法,将 b.length 个字节从指定的 byte 数组写入此输出流中。
如何将字节数据保存在字节数组中,以字符串为例,”hello , world” 如何转为字节数组。显然通过字符串的getBytes方法即可。
publicclass IoTest2 { publicstaticvoid main(String[] args) throws IOException { String path = "c:\\a.txt"; writeTxtFile(path); }
privatestaticvoid writeTxtFile(String path) throws IOException { // 1:打开文件输出流,流的目的地是指定的文件 FileOutputStream fos = new FileOutputStream(path);
// 2:通过流向文件写数据 byte[] byt = "java".getBytes(); fos.write(byt); // 3:用完流后关闭流 fos.close(); } }
|
仔细查看a.txt文本文件发现上述程序每运行一次,老的内容就会被覆盖掉。,那么如何不覆盖已有信息,能够往a.txt里追加信息呢。查看API文档,发现FileOutputStream类中的构造方法中有一个构造可以实现追加的功能FileOutputStream(File file, boolean append) 第二个参数,append - 如果为 true,则将字节写入文件末尾处,而不是写入文件开始处
privatestaticvoid writeTxtFile(String path) throws IOException { // 1:打开文件输出流,流的目的地是指定的文件 FileOutputStream fos = new FileOutputStream(path,true);
// 2:通过流向文件写数据 byte[] byt = "java".getBytes(); fos.write(byt); // 3:用完流后关闭流 fos.close(); } |
1.3. 字节流文件拷贝
1.3.1. 字节输入输出流综合使用
通过字节输出流向文件中写入一些信息,并使用字节输入流把文件中的信息显示到控制台上。
publicclass IoTest3 { publicstaticvoid main(String[] args) throws IOException { String path = "c:\\b.txt"; String content = "hello java";
writeFile(path, content);
readFile(path); }
publicstaticvoid writeFile(String path, String content) throws IOException { // 打开文件输出流 FileOutputStream fos = new FileOutputStream(path); byte[] buffer = content.getBytes(); // 向文件中写入内容 fos.write(buffer); // 关闭流 fos.close();
}
publicstaticvoid readFile(String path) throws IOException { FileInputStream fis = new FileInputStream(path); byte[] byt = newbyte[1024]; int len = 0; while ((len = fis.read(byt)) != -1) { System.out.println(new String(byt, 0, len)); } // 关闭流 fos.close();
} }
|
注意输出流的细节:
这个输出流显然只适合小数据的写入,如果有大数据想要写入,我们的byte数组,该如何定义?
上述案例中我们将输入流和输出流进行和综合使用,如果尝试进输出流换成文本文件就可以实现文件的拷贝了.
什么是文件拷贝?很显然,先开一个输入流,将文件加载到流中,再开一个输出流,将流中数据写到文件中。就实现了文件的拷贝。
分析: 第一步:需要打开输入流和输出流 第二步:读取数据并写出数据 第三步:关闭流 publicclass IoTest3 {
publicstaticvoid main(String[] args) throws IOException {
String srcPath = "c:\\a.txt"; String destPath = "d:\\a.txt"; copyFile(srcPath, destPath); }
publicstaticvoid copyFile(String srcPath, String destPath) throws IOException {
}
} |
1.3.2. 字节流拷贝文件实现1
读一个字节写一个字节read 和write
publicclass IoTest3 {
publicstaticvoid main(String[] args) throws IOException {
String srcPath = "c:\\a.txt"; String destPath = "d:\\a.txt"; copyFile(srcPath, destPath); }
publicstaticvoid copyFile(String srcPath, String destPath) throws IOException { // 打开输入流,输出流 FileInputStream fis = new FileInputStream(srcPath); FileOutputStream fos = new FileOutputStream(destPath);
// 读取和写入信息 int len = 0; while ((len = fis.read()) != -1) { fos.write(len); }
// 关闭流 fis.close(); fos.close(); }
} |
文本文件在计算机中是以二进制形式存在的,可以通过io流来拷贝,那么图片能不能拷贝呢?视频呢?音频呢?
publicclass IoTest3 {
publicstaticvoid main(String[] args) throws IOException {
String srcPath = "c:\\秋.jpg"; String destPath = "d:\\秋.jpg"; copyFile(srcPath, destPath); }
publicstaticvoid copyFile(String srcPath, String destPath) throws IOException { // 打开输入流,输出流 FileInputStream fis = new FileInputStream(srcPath); FileOutputStream fos = new FileOutputStream(destPath);
// 读取和写入信息 int len = 0; while ((len = fis.read()) != -1) { fos.write(len); }
// 关闭流 fis.close(); fos.close(); }
} |
测试统统通过,所以字节流可以操作所有的文件。只是发现程序很慢,需要很长时间。特别是拷贝音频和视频文件时。
为什么?因为每次读一个字节再写一个字节效率很低。很显然这样效率低下的操作不是我们想要的。有没有更快更好的方法呢,是否可以使用缓冲区来提高程序的效率呢。
1.3.3. 字节流拷贝文件实现2;
使用字节数组作为缓冲区
publicstaticvoid copyFile2(String srcPath, String destPath) throws IOException { // 打开输入流,输出流 FileInputStream fis = new FileInputStream(srcPath); FileOutputStream fos = new FileOutputStream(destPath);
// 读取和写入信息 int len = 0;
// 使用字节数组,当做缓冲区 byte[] byt = newbyte[1024]; while ((len = fis.read(byt)) != -1) { fos.write(byt); }
// 关闭流 fis.close(); fos.close(); }
|
问题1: 使用缓冲(字节数组)拷贝数据,拷贝后的文件大于源文件.
测试该方法,拷贝文本文件,仔细观察发现和源文件不太一致。
打开文件发现拷贝后的文件和拷贝前的源文件不同,拷贝后的文件要比源文件多一些内容问题就在于我们使用的容器,这个容器我们是重复使用的,新的数据会覆盖掉老的数据,显然最后一次读文件的时候,容器并没有装满,出现了新老数据并存的情况。
所以最后一次把容器中数据写入到文件中就出现了问题。
如何避免?使用FileOutputStream 的write(byte[] b, int off, int len)
b 是容器,off是从数组的什么位置开始,len是获取的个数,容器用了多少就写出多少。
publicstaticvoid copyFile2(String srcPath, String destPath) throws IOException { // 打开输入流,输出流 FileInputStream fis = new FileInputStream(srcPath); FileOutputStream fos = new FileOutputStream(destPath);
// 读取和写入信息 int len = 0;
// 使用字节数组,当做缓冲区 byte[] byt = newbyte[1024]; while ((len = fis.read(byt)) != -1) { fos.write(byt, 0, len); }
// 关闭流 fis.close(); fos.close(); }
|
使用缓冲拷贝视频,可以根据拷贝的需求调整数组的大小,一般是1024的整数倍。发现使用缓冲后效率大大提高。
1.4. 字节流的异常处理
上述案例中所有的异常都只是进行了抛出处理,这样是不合理的。所以上述代码并不完善,因为异常没有处理。
当我们打开流,读和写,关闭流的时候都会出现异常,异常出现后,后面的代码都不会执行了。假设打开和关闭流出现了异常,那么显然close方法就不会再执行。那么会对程序有什么影响?
案例:
publicclass IoTest4 { publicstaticvoid main(String[] args) throws IOException, InterruptedException { String path = "c:\\b.txt"; readFile(path); }
privatestaticvoid readFile(String path) throws IOException, InterruptedException { FileInputStream fis = new FileInputStream(path); byte[] byt = newbyte[1024]; int len = fis.read(byt); System.out.println(new String(byt, 0, len)); // 让程序睡眠,无法执行到close方法。 Thread.sleep(1000 * 10); fis.close(); } } |
在执行该程序的同时我们尝试去删除b.txt文件。如果在该程序没有睡醒的话,我们是无法删除b.txt 文件的。因为b.txt还被该程序占用着,这是很严重的问题,所以一定要关闭流。
目前我们是抛出处理,一旦出现了异常,close就没有执行,也就没有释放资源。那么为了保证close的执行该如何处理呢。
那么就需要使用try{} catch(){}finally{}语句。try中放入可能出现异常的语句,catch是捕获异常对象,fianlly是一定要执行的代码
publicclass IoTest4 { publicstaticvoid main(String[] args) throws IOException, InterruptedException { String path = "c:\\b.txt"; readFile(path); }
privatestaticvoid readFile(String path) { FileInputStream fis = null; try { fis = new FileInputStream(path); byte[] byt = newbyte[1024]; int len = fis.read(byt); System.out.println(new String(byt, 0, len)); } catch (IOException e) { // 抛出运行时异常 thrownew RuntimeException(e); } finally { // 把close方法放入finally中保证一定会执行 // 先判断是否空指针
try { if (fis != null) { fis.close(); } catch (Exception e) { thrownew RuntimeException(e); }
}
}
} } |
文件拷贝的异常处理:
publicstaticvoid copyFile(String srcPath, String destPath) {
FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(srcPath); fos = new FileOutputStream(destPath);
byte[] byt = newbyte[1024 * 1024]; int len = 0; while ((len = fis.read(byt)) != -1) {
fos.write(byt, 0, len); } } catch (IOException e) { thrownew RuntimeException(e); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { thrownew RuntimeException(e); } } if (fos != null) { try { fos.close(); } catch (IOException e) { thrownew RuntimeException(e); } }
}
} |
注意:
在最后的close代码中可能会有问题,两个close,如果第一个close方法出现了异常,并抛出了运行时异常,那么程序还是停止了。下面的close方法就没有执行到。
那么为了保证close的执行,将第二个放到fianlly中即可。
publicstaticvoid copyFile(String srcPath, String destPath) {
FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(srcPath); fos = new FileOutputStream(destPath);
byte[] byt = newbyte[1024 * 1024]; int len = 0; while ((len = fis.read(byt)) != -1) {
fos.write(byt, 0, len); } } catch (IOException e) { thrownew RuntimeException(e); } finally {
try { if (fis != null) { fis.close(); } } catch (IOException e) { thrownew RuntimeException(e); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { thrownew RuntimeException(e); } }
} }
} |
1.5. 字节缓冲流
1.5.1. 缓冲流
上述程序中我们为了提高流的使用效率,自定义了字节数组,作为缓冲区.Java其实提供了专门的字节流缓冲来提高效率.
BufferedInputStream和BufferedOutputStream
BufferedOutputStream和BufferedOutputStream类可以通过减少读写次数来提高输入和输出的速度。它们内部有一个缓冲区,用来提高处理效率。查看API文档,发现可以指定缓冲区的大小。其实内部也是封装了字节数组。没有指定缓冲区大小,默认的字节是8192。
显然缓冲区输入流和缓冲区输出流要配合使用。首先缓冲区输入流会将读取到的数据读入缓冲区,当缓冲区满时,或者调用flush方法,缓冲输出流会将数据写出。
注意:当然使用缓冲流来进行提高效率时,对于小文件可能看不到性能的提升。但是文件稍微大一些的话,就可以看到实质的性能提升了。
publicclass IoTest5 { publicstaticvoid main(String[] args) throws IOException { String srcPath = "c:\\a.mp3"; String destPath = "d:\\copy.mp3"; copyFile(srcPath, destPath); }
publicstaticvoid copyFile(String srcPath, String destPath) throws IOException { // 打开输入流,输出流 FileInputStream fis = new FileInputStream(srcPath); FileOutputStream fos = new FileOutputStream(destPath);
// 使用缓冲流 BufferedInputStream bis = new BufferedInputStream(fis); BufferedOutputStream bos = new BufferedOutputStream(fos);
// 读取和写入信息 int len = 0;
while ((len = bis.read()) != -1) { bos.write(len); }
// 关闭流 bis.close(); bos.close(); }
}
|
2. 字符流
计算机并不区分二进制文件与文本文件。所有的文件都是以二进制形式来存储的,因此,从本质上说,所有的文件都是二进制文件。所以字符流是建立在字节流之上的,它能够提供字符层次的编码和解码。例如,在写入一个字符时,Java虚拟机会将字符转为文件指定的编码(默认是系统默认编码),在读取字符时,再将文件指定的编码转化为字符。
常见的码表如下:
ASCII: 美国标准信息交换码。用一个字节的7位可以表示。
ISO8859-1: 拉丁码表。欧洲码表,用一个字节的8位表示。又称Latin-1(拉丁编码)或“西欧语言”。ASCII码是包含的仅仅是英文字母,并且没有完全占满256个编码位置,所以它以ASCII为基础,在空置的0xA0-0xFF的范围内,加入192个字母及符号,
藉以供使用变音符号的拉丁字母语言使用。从而支持德文,法文等。因而它依然是一个单字节编码,只是比ASCII更全面。
GB2312: 英文占一个字节,中文占两个字节.中国的中文编码表。
GBK: 中国的中文编码表升级,融合了更多的中文文字符号。
Unicode: 国际标准码规范,融合了多种文字。所有文字都用两个字节来表示,Java语言使用的就是unicode。
UTF-8: 最多用三个字节来表示一个字符。
(我们以后接触最多的是iso8859-1、gbk、utf-8)
查看上述码表后,很显然中文的‘中’在iso8859-1中是没有对映的编码的。或者一个字符在2中码表中对应的编码不同,例如有一些字在不同的编码中是有交集的,例如bjg5 和gbk 中的汉字简体和繁体可能是一样的,就是有交集,但是在各自码表中的数字不一样。
例如
使用gbk 将中文保存在计算机中,
中 国
对映 100 200 如果使用big5 打开
可能 ? ...
不同的编码对映的是不一样的。
很显然,我们使用什么样的编码写数据,就需要使用什么样的编码来对数据。
ISO8859-1:一个字节
GBK: 两个字节包含了英文字符和扩展的中文 ISO8859-1+中文字符
UTF-8 万国码,推行的。是1~3个字节不等长。英文存的是1个字节,中文存的是3个字节,是为了节省空间。
那么我们之前学习的流称之为字节流,以字节为单位进行操作之情的操作全是英文,如果想要操作中文呢?
测试:将指定位置的文件通过字节流读取到控制台
publicclass TestIo { publicstaticvoid main(String[] args) throws IOException { String path = "c:\\a.txt"; writFileTest(); readFileByInputStream(path); }
privatestaticvoid readFileByInputStream(String path) throws IOException { FileInputStream fis = new FileInputStream(path);
int len = 0; while ((len = fis.read()) != -1) { System.out.print((char) len); } }
privatestaticvoid writFileTest() throws FileNotFoundException, IOException { // 创建文件对象 File file = new File("c:\\a.txt"); // 创建文件输出流 FileOutputStream fos = new FileOutputStream(file); fos.write("中国".getBytes()); fos.close(); } }
|
发现控制台输出的信息:
???ú 是这样的东西,打开a.txt 文本发现汉字”中国”确实写入成功。
那么说明使用字节流处理中文有问题。
仔细分析,我们的FileInputStream输入流的read() 一次是读一个字节的,返回的是一个int显然进行了自动类型提升。那么我们来验证一下“中国”对应的字节是什么
使用:"中国".getBytes() 即可得到字符串对应的字节数组。是[-42, -48, -71, -6]
同样,将read方法返回值直接强转为byte ,发现结果也是-42, -48, -71, -6 。
代码:
publicclass TestIo { publicstaticvoid main(String[] args) throws IOException { String path = "c:\\a.txt"; writFileTest(); readFileByInputStream(path); //查看中国对应的编码 System.out.println(Arrays.toString("中国".getBytes())); }
privatestaticvoid readFileByInputStream(String path) throws IOException { FileInputStream fis = new FileInputStream(path); int len = 0; while ((len = fis.read()) != -1) { System.out.println((byte)len); } }
privatestaticvoid writFileTest() throws FileNotFoundException, IOException { // 创建文件对象 File file = new File("c:\\a.txt"); // 创建文件输出流 FileOutputStream fos = new FileOutputStream(file); fos.write("中国\r\n".getBytes()); fos.close(); }
}
|
那么中国对应的是-42, -48, -71, -6是4个字节。那就是一个中文占2个字节,(这个和编码是有关系的)
很显然,我们的中文就不能够再一个字节一个字节的读了。所以字节流处理字符信息时并不方便那么就出现了字符流。
字节流是字符流是以字符为单位。
体验字符流:
publicstaticvoid main(String[] args) throws IOException {
String path = "c:\\a.txt"; readFileByReader(path); } privatestaticvoid readFileByReader(String path) throws IOException { FileReader fr = new FileReader(path); int len = 0; while ((len = fr.read()) != -1) { System.out.print((char) len); } } |
总结:字符流就是:字节流 + 编码表,为了更便于操作文字数据。字符流的抽象基类:
Reader , Writer。
由这些类派生出来的子类名称都是以其父类名作为子类名的后缀,如FileReader、FileWriter。
2.1. Reader
方法:
1,int read():
读取一个字符。返回的是读到的那个字符。如果读到流的末尾,返回-1.
2,int read(char[]):
将读到的字符存入指定的数组中,返回的是读到的字符个数,也就是往数组里装的元素的个数。如果读到流的末尾,返回-1.
3,close()
读取字符其实用的是window系统的功能,就希望使用完毕后,进行资源的释放
由于Reader也是抽象类,所以想要使用字符输入流需要使用Reader的实现类。查看API文档。找到了FileReader。
1,用于读取文本文件的流对象。
2,用于关联文本文件。
构造函数:在读取流对象初始化的时候,必须要指定一个被读取的文件。
如果该文件不存在会发生FileNotFoundException.
publicclass IoTest1_Reader {
publicstaticvoid main(String[] args) throws Exception { String path = "c:/a.txt"; // readFileByInputStream(path); readFileByReader(path); }
/** * 使用字节流读取文件内容 * * @param path */ publicstaticvoid readFileByInputStream(String path) throws Exception { InputStream in = new FileInputStream(path);
int len = 0; while ((len = in.read()) != -1) { System.out.print((char) len); }
in.close(); }
/** * 使用字符流读取文件内容 */ publicstaticvoid readFileByReader(String path) throws Exception { Reader reader = new FileReader(path); int len = 0; while ((len = reader.read()) != -1) { System.out.print((char) len); }
reader.close(); }
} |
2.2. Writer
Writer中的常见的方法:
1,write(ch): 将一个字符写入到流中。
2,write(char[]): 将一个字符数组写入到流中。
3,write(String): 将一个字符串写入到流中。
4,flush():刷新流,将流中的数据刷新到目的地中,流还存在。
5,close():关闭资源:在关闭前会先调用flush(),刷新流中的数据去目的地。然流关闭。
发现基本方法和OutputStream 类似,有write方法,功能更多一些。可以接收字符串。
同样道理Writer是抽象类无法创建对象。查阅API文档,找到了Writer的子类FileWriter
1:将文本数据存储到一个文件中。
publicclass IoTest2_Writer {
publicstaticvoid main(String[] args) throws Exception { String path = "c:/ab.txt";
writeToFile(path); } /** * 写指定数据到指定文件中 * */ publicstaticvoid writeToFile(String path) throws Exception { Writer writer = new FileWriter(path); writer.write('中'); writer.write("世界".toCharArray()); writer.write("中国"); writer.close(); } } |
2:追加文件:
默认的FileWriter方法新值会覆盖旧值,想要实现追加功能需要
使用如下构造函数创建输出流 append值为true即可。
FileWriter(String fileName, boolean append)
FileWriter(File file, boolean append)
3:flush方法
如果使用字符输出流,没有调用close方法,会发生什么?
privatestaticvoid writeFileByWriter(File file) throws IOException { FileWriter fw = new FileWriter(file); fw.write('新'); fw.flush(); fw.write("中国".toCharArray()); fw.write("世界你好!!!".toCharArray()); fw.write("明天"); // 关闭流资源 //fw.close(); } |
程序执行完毕打开文件,发现没有内容写入.原来需要使用flush方法. 刷新该流的缓冲。
为什么只要指定claose方法就不用再flush方法,因为close也调用了flush方法.
2.3. 字符流拷贝文件
一个文本文件中有中文有英文字母,有数字。想要把这个文件拷贝到别的目录中。
我们可以使用字节流进行拷贝,使用字符流呢?肯定也是可以的。
2.3.1. 字符流拷贝文件实现1
publicstaticvoid main(String[] args) throws Exception { String path1 = "c:/a.txt"; String path2 = "c:/b.txt";
copyFile(path1, path2); }
/** * 使用字符流拷贝文件 */ publicstaticvoid copyFile(String path1, String path2) throws Exception { Reader reader = new FileReader(path1); Writer writer = new FileWriter(path2);
int ch = -1; while ((ch = reader.read()) != -1) { writer.write(ch); }
reader.close(); writer.close(); } |
但是这个一次读一个字符就写一个字符,效率不高。把读到的字符放到字符数组中,再一次性的写出。
2.3.2. 字符流拷贝文件实现2
publicstaticvoid main(String[] args) throws Exception { String path1 = "c:/a.txt"; String path2 = "c:/b.txt";
copyFile(path1, path2); }
publicstaticvoid copyFile3(String path1, String path2) throws Exception { Reader reader = new FileReader(path1); Writer writer = new FileWriter(path2);
int ch = -1; char [] arr=newchar[1024]; while ((ch = reader.read(arr)) != -1) { writer.write(arr,0,ch); }
reader.close(); writer.close(); } |
字节流可以拷贝视频和音频等文件,那么字符流可以拷贝这些吗?
经过验证拷贝图片是不行的。发现丢失了信息,为什么呢?
计算机中的所有信息都是以二进制形式进行的存储(1010)图片中的也都是二进制
在读取文件的时候字符流自动对这些二进制按照码表进行了编码处理,但是图片本来就是二进制文件,不需要进行编码。有一些巧合在码表中有对应,就可以处理,并不是所有的二进制都可以找到对应的。信息就会丢失。所以字符流只能拷贝以字符为单位的文本文件
(以ASCII码为例是127个,并不是所有的二进制都可以找到对应的ASCII,有些对不上的,就会丢失信息。)
2.4. 字符流的异常处理
publicstaticvoid main(String[] args) throws Exception { String path1 = "c:/a.txt"; String path2 = "c:/b.txt";
copyFile2(path1, path2); }
/** * 使用字符流拷贝文件,有完善的异常处理 */ publicstaticvoid copyFile2(String path1, String path2) { Reader reader = null; Writer writer = null; try { // 打开流 reader = new FileReader(path1); writer = new FileWriter(path2);
// 进行拷贝 int ch = -1; while ((ch = reader.read()) != -1) { writer.write(ch); } } catch (Exception e) { thrownew RuntimeException(e); } finally { // 关闭流,注意一定要能执行到close()方法,所以都要放到finally代码块中 try { if (reader != null) { reader.close(); } } catch (Exception e) { thrownew RuntimeException(e); } finally { try { if (writer != null) { writer.close(); } } catch (Exception e) { thrownew RuntimeException(e); } } } } |
2.5. 字符流的缓冲区
查看Reader 发现Reader,操作的是字符,我们就不需要进行编码解码操作,由字符流读到二进制,自动进行解码得到字符,写入字符自动编码成二进制.
Reader有一个子类BufferedReader。子类继承父类显然子类可以重写父类的方法,也可以增加自己的新方法。例如一次读一行就是常用的操作.那么BufferedReader 类就提供了这个方法,可以查看readLine()方法具备 一次读取一个文本行的功能。很显然,该子类可以对功能进行增强。
体验BufferedReader
publicclass IoTest_BufferedReader { publicstaticvoid main(String[] args) throws IOException { readFile("c:\\a.txt"); }
privatestaticvoid readFile(String path) throws IOException { Reader read = new FileReader(path);
BufferedReader br = new BufferedReader(read);
String line = null; while ((line = br.readLine()) != null) { System.out.println(line); }
} }
|
注意:
在使用缓冲区对象时,要明确,缓冲的存在是为了增强流的功能而存在,所以在建立缓冲区对象时,要先有流对象存在.
缓冲区的出现提高了对流的操作效率。原理:其实就是将数组进行封装。
使用字符流缓冲区拷贝文本文件.
publicclass Demo7 { publicstaticvoid main(String[] args) throws IOException { // 关联源文件 File srcFile = new File("c:\\linux大纲.txt"); // 关联目标文件 File destFile = new File("d:\\linux大纲.txt"); // 实现拷贝 copyFile(srcFile, destFile);
}
privatestaticvoid copyFile(File srcFile, File destFile) throws IOException { // 创建字符输入流 FileReader fr = new FileReader(srcFile); // 创建字符输出流 FileWriter fw = new FileWriter(destFile);
// 字符输入流的缓冲流 BufferedReader br = new BufferedReader(fr); // 字符输出流的缓冲流 BufferedWriter bw = new BufferedWriter(fw);
String line = null; // 一次读取一行 while ((line = br.readLine()) != null) { // 一次写出一行. bw.write(line); // 刷新缓冲 bw.flush(); // 进行换行,由于readLine方法默认没有换行.需要手动换行 bw.newLine(); } // 关闭流 br.close(); bw.close(); } }
|
2.6. 装饰器模式
需求:想要在读取的文件的每一行添加行号。
publicclass IoTest7_BufferedReader {
publicstaticvoid main(String[] args) throws IOException { readFile("c:\\a.txt"); }
privatestaticvoid readFile(String path) throws IOException { Reader read = new FileReader(path);
BufferedReader br = new BufferedReader(read); int count = 0; String line = null; while ((line = br.readLine()) != null) { count++; System.out.println(count+":"+line); }
} }
|
很容易的就可以实现。如果每次使用BufferedReader 输出时都需要显示行号呢? 每次都加? 很显然,我们的BufferedReader继承了Reader 对父类进行了功能的增强,那么我们也可以继承BufferedReader 重写该类的readLine方法,进行功能的增强.
publicclass IoTest_BufferedReader { publicstaticvoid main(String[] args) throws IOException { readFile("c:\\a.txt"); }
privatestaticvoid readFile(String path) throws IOException { Reader read = new FileReader(path);
BufferedReader br = new MyBufferedReader(read); String line = null; while ((line = br.readLine()) != null) { System.out.println(line); } } }
class MyBufferedReader extends BufferedReader { public MyBufferedReader(Reader read) { super(read); }
intcount;
@Override public String readLine() throws IOException { String line = super.readLine(); if (line != null) { count++; returncount + ":" + line;
} else { returnnull; }
} } |
需求:
要在输出的一行前加上引号
可以再定义一个BufferedReader的子类,继承BufferedReader增强功能.
publicclass IoTest_BufferedReader { publicstaticvoid main(String[] args) throws IOException { readFile("c:\\a.txt"); }
privatestaticvoid readFile(String path) throws IOException { Reader read = new FileReader(path); BufferedReader br = new MyQutoBufferedReader(read); int count = 0; String line = null; while ((line = br.readLine()) != null) { System.out.println(line); count++; }
} }
// quotation 引号 class MyQutoBufferedReader extends BufferedReader {
public MyQutoBufferedReader(Reader reader) { super(reader); }
public String readLine() throws IOException { String line = super.readLine(); if (line != null) {
return"\"" + line + "\"";
} else { returnnull; }
} } |
需求三:
既想要显示行号又想要显示引号
发现,就需要再定义子类,发现这样比较麻烦,代码臃肿.而且代码重复.
可以换一种方式.如下:
其实就是一个新类要对原有类进行功能增强.
1. 在增强类中维护一个被增强的父类引用变量
2. 在增强类的构造函数中初始化1中的变量
3. 创建需要增强的方法,在刚方法中调用被被增强类的方法,并加以增强。
publicclass IoTest_BufferedReader { publicstaticvoid main(String[] args) throws IOException { readFile("c:\\a.txt"); }
privatestaticvoid readFile(String path) throws IOException { Reader read = new FileReader(path); BufferedReader bufferedReader = new BufferedReader(read); BufferedReader br = new MyQutoBufferedReader2(bufferedReader); br = new MyLineBufferedReader2(br); String line = null; while ((line = br.readLine()) != null) { System.out.println(line); } } }
// quotation 引号 class MyQutoBufferedReader2 extends BufferedReader { private BufferedReader bufferedReader;
public MyQutoBufferedReader2(BufferedReader bufferedReader) { super(bufferedReader); this.bufferedReader = bufferedReader; }
public String readLine() throws IOException { String line = super.readLine(); if (line != null) {
return"\"" + line + "\"";
} else { returnnull; }
} }
class MyLineBufferedReader2 extends BufferedReader { private BufferedReader bufferedReader;
public MyLineBufferedReader2(BufferedReader bufferedReader) { super(bufferedReader); this.bufferedReader = bufferedReader; }
intcount;
@Override public String readLine() throws IOException { String line = super.readLine(); if (line != null) { count++; returncount + ":" + line;
} else { returnnull; }
} } |
这就是装饰器模式
JavaIO中的应用:
Java I/O类库需要多种不同的功能组合,所以使用了装饰器模式。
FilterXxx类是JavaIO提供的装饰器基类,即我们要想实现一个新的装饰器,就要继承这些类。
装饰器与继承:
问题:
修饰模式做的增强功能按照继承的特点也是可以实现的,为什么还要提出修饰设计模式呢?
继承实现的增强类和修饰模式实现的增强类有何区别?
继承实现的增强类:
优点:代码结构清晰,而且实现简单
缺点:对于每一个的需要增强的类都要创建具体的子类来帮助其增强,这样会导致继承体系过于庞大。
修饰模式实现的增强类:
优点:内部可以通过多态技术对多个需要增强的类进行增强
缺点:需要内部通过多态技术维护需要增强的类的实例。进而使得代码稍微复杂。