java IO流(2)
1、缓冲区
在前面文件读写的时候,我们是在使用自己的定义的数组作为缓冲区,其实在IO流体系中有相应的缓冲区对象。
public class BufferedDemo {
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
BufferedInputStream bufIn = new BufferedInputStream(new FileInputStream("d:\\1.mp3"));
BufferedOutputStream bufOut = new BufferedOutputStream(new FileOutputStream("d:\\2.mp3"));
// FileInputStream fis = new FileInputStream("E:\\1.mp3");
// FileOutputStream fos = new FileOutputStream("e:\\2.mp3");
byte[] buf = new byte[1024];
int len = 0;
while((len = bufIn.read())!=-1){
bufOut.write(len);
bufOut.flush();
}
bufIn.close();
bufOut.close();
long end = System.currentTimeMillis();
System.out.println(end-start);
}
}
2、字符流
2.1、字节流读取字符的问题
通过以下程序读取带有中文件的文件。
public class CharStreamDemo {
public static void main(String[] args) throws IOException {
//给文件中写中文
writeCNText();
//读取文件中的中文
readCNText();
}
//读取中文
public static void readCNText() throws IOException {
FileInputStream fis = new FileInputStream("D:\\test\\cn.txt");
int ch = 0;
while((ch = fis.read())!=-1){
System.out.println(ch);
}
}
//写中文
public static void writeCNText() throws IOException {
FileOutputStream fos = new FileOutputStream("D:\\test\\cn.txt");
fos.write("欢迎你".getBytes());
fos.close();
}
}
上面程序在读取含有中文的文件时,我们并没有看到具体的中文,而是看到一些数字既然看不到中文,就必须研究下字符的编码过程。
2.2、编码表
计算机底层数据存储的都是二进制数据,而我们生活中的各种各样的数据,每一个字符和一个整数对应起来,就形成了一张编码表,这张编码表就是ASCII表。其中就是各种英文字符对应的编码。
编码表:其实就是生活中文件和计算机二进制的对应关系表。
- ascii: 一个字节中的 7位就可以表示。对应的字节都是正数。
0-xxxxxxx
- iso8859-1: 拉丁码表 latin,用了一个字节用的8位。
1-xxxxxxx 负数
。 - GB2312: 简体中文码表。6,7仟的中文和符号。用两个字节表示。两个字节都是开头为1 两个字节都是负数。
- GBK: 目前最常用的中文码表,2万多的中文和符号。用两个字节表示,一部分文字,第一个字节开头是 1,第二字节开头是0。
- GB18030: 最新的中文码表,目前还没有正式使用。
- unicode: 国际标准码表,无论是什么文字,都用两个字节存储。Java中的char类型用的就是这个码表。
char c = 'a';
占两个字节。在Java中,字符串是按照系统默认码表来解析的。简体中文版字符串默认的码表是GBK
。 - UTF-8: 基于unicode,一个字节就可以存储数据,不要用两个字节存储,而且这个码表更加的标准化,在每一个字节头加入了。编码信息(后期到api中查找)。
能识别中文的码表: GBK、UTF-8;正因为识别中文码表不唯一,涉及到了编码解码问题。
对于我们开发而言;常见的编码 GBK
UTF-8
ISO8859-1
文字—>二进制(数字) :编码。
二进制(数字)—>文字 : 解码。
2.3、FileReader类介绍
FileInputStream 用于读取诸如图像数据之类的原始字节流,FileReader读取字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是适当的。
**Reader:**读取字符流的抽象超类。read():读取单个字符并返回, read(char[]):将数据读取到数组中,并返回读取的个数。
public class CharStreamDemo {
public static void main(String[] args) throws IOException {
//给文件中写中文
writeCNText();
//读取文件中的中文
readCNText();
}
//读取中文
public static void readCNText() throws IOException {
FileReader fr = new FileReader("D:\\test\\cn.txt");
int ch = 0;
while((ch = fr.read())!=-1){
//输出的字符对应的编码值
System.out.println(ch);
//输出字符本身
System.out.println((char)ch);
}
}
//写中文
public static void writeCNText() throws IOException {
FileOutputStream fos = new FileOutputStream("D:\\test\\cn.txt");
fos.write("欢迎你".getBytes());
fos.close();
}
}
2.4、FileWriter类介绍
Writer是写入字符流的抽象类。其中描述了相应的写的动作。
public class FileWriterDemo {
public static void main(String[] args) throws IOException {
//演示FileWriter 用于操作文件的便捷类。
FileWriter fw = new FileWriter("d:\\text\\fw.txt");
fw.write("你好谢谢再见");//这些文字都要先编码。都写入到了流的缓冲区中。
fw.flush();
fw.close();
}
}
2.5、flush()和close()的区别
flush(): 将流中的缓冲区缓冲的数据刷新到目的地中,刷新后,流还可以继续使用。
close(): 关闭资源,但在关闭前会将缓冲区中的数据先刷新到目的地,否则丢失数据,然后在关闭流。流不可以使用。如果写入数据多,一定要一边写一边刷新,最后一次可以不刷新,由close完成刷新并关闭。
3、转换流
3.1、OutputStreamWriter类
需求:既然识别中文的码表有两个,GBK
、UTF-8
等,将中文数据按照utf-8的方式进行文件的存储。因为FileWriter
中默认的是GBK
。通过FileWriter的api描述,要指定编码表这些值,需要使用OutputStreamWriter
。
OutputStreamWriter 是字符流通向字节流的桥梁,可使用指定的 charset
将要写入流中的字符编码成字节。它的作用的就是,将字符串按照指定的编码表转成字节,在使用字节流将这些字节写出去。
public static void writeCN() throws Exception {
//创建与文件关联的字节输出流对象
FileOutputStream fos = new FileOutputStream("D:\\test\\cn8.txt");
//创建可以把字符转成字节的转换流对象,并指定编码
OutputStreamWriter osw = new OutputStreamWriter(fos,"utf-8");
//调用转换流,把文字写出去,其实是写到转换流的缓冲区中
osw.write("你好");//写入缓冲区。
osw.close();
}
OutputStreamWriter流对象,维护自己的缓冲区,当我们调用OutputStreamWriter对象的write
方法时,会拿着字符到指定的码表中进行查询,把查到的字符编码值转成字节数存放到OutputStreamWriter缓冲区中。然后再调用刷新功能或者关闭流,或者缓冲区存满后会把缓冲区中的字节数据使用字节流写到指定的文件中。
3.2、InputStreamReader类
InputStreamReader 是字节流通向字符流的桥梁:它使用指定的 charset
读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。
public class InputStreamReaderDemo {
public static void main(String[] args) throws IOException {
//演示字节转字符流的转换流
readCN();
}
public static void readCN() throws IOException{
//创建读取文件的字节流对象
InputStream in = new FileInputStream("D:\\test\\cn8.txt");
//创建转换流对象
//InputStreamReader isr = new InputStreamReader(in);这样创建对象,会用本地默认码表读取,将会发生错误解码的错误
InputStreamReader isr = new InputStreamReader(in,"utf-8");
//使用转换流去读字节流中的字节
int ch = 0;
while((ch = isr.read())!=-1){
System.out.println((char)ch);
}
//关闭流
isr.close();
}
}
注意: 在读取指定的编码的文件时,一定要指定编码格式,否则就会发生解码错误,而发生乱码现象。
3.3、转换流和子类区别
父类和子类的功能区别:
OutputStreamWriter和InputStreamReader是字符和字节的桥梁;也可以称之为字符转换流。字符转换流原理:字节流+编码表
。
FileWriter和FileReader:作为子类,仅作为操作字符文件的便捷类存在。当操作的字符文件,使用的是默认编码表时可以不用父类,而直接用子类就完成操作了,简化了代码。
InputStreamReader isr = new InputStreamReader(new FileInputStream(“a.txt”));//默认字符集。
InputStreamReader isr = new InputStreamReader(new FileInputStream(“a.txt”),“GBK”);//指定GBK字符集。
FileReader fr = new FileReader(“a.txt”);
这三句代码的功能是一样的,其中第三句最为便捷。
注意: 一旦要指定其他编码时,绝对不能用子类,必须使用字符转换流。
使用子类的条件:
1、操作的是文件。2、使用默认编码。
3.4、复制文本文件
思路:
- 既然是文本涉及编码表。需要用字符流。
- 操作的是文件,涉及硬盘。
- 有指定码表吗?没有,默认就行。
操作的是文件,使用的默认码表。使用哪个字符流对象。直接使用字符流操作文件的便捷类。FileReader FileWriter
public class CopyTextFileTest {
public static void main(String[] args) throws IOException {
copyTextFile();
}
public static void copyTextFile() throws IOException {
//1,明确源和目的。
FileReader fr = new FileReader("D:\\test\\cn.txt");
FileWriter fw = new FileWriter("D:\\test\\copy.txt");
//2,为了提高效率。自定义缓冲区数组。字符数组。
char[] buf = new char[1024];
int len = 0;
while((len=fr.read(buf))!=-1){
fw.write(buf,0,len);
}
/*2,循环读写操作。效率低。
int ch = 0;
while((ch=fr.read())!=-1){
fw.write(ch);
}
*/
//3,关闭资源。
fw.close();
fr.close();
}
}
3.5、字符流缓冲区对像赋值文本文件
自定义数组就可以解决问题缓冲区问题并提高效率。为什么还要使用流中的缓冲区对象呢?因为缓冲区对象中除了封装数组以外,还提供了更多的操作缓冲区数据的方法。
字符流缓冲区中的特有方法:操作字符数据时,有一个文本特有的表形实行 :行(hang)
操作行的方法:BufferedReader:readLine()
:一次读取一行。
BufferedWriter:newLine()
;写出去新的一行。
public class CharStreamBufferedTest {
public static void main(String[] args) throws IOException {
copyTextByBuffer();
}
public static void copyTextByBuffer() throws IOException {
BufferedReader bufr = new BufferedReader(new FileReader("Test24.java"));
BufferedWriter bufw = new BufferedWriter(new FileWriter("tempfile\\test24_bufcopy.txt"));
//循环读写一行数据。
String line = null;
while((line=bufr.readLine())!=null){
bufw.write(line);
bufw.newLine();
bufw.flush();
}
bufw.close();
bufr.close();
}
}
4、字符流缓冲区原理
4.1、字符流缓冲区以及readLine方法原理
public class BufferedStreamDemo {
public static void main(String[] args) throws IOException {
String line = null;
while((line=bufr.readLine())!=null){
System.out.println(line);
}
bufr.close();
}
}
上述代码在读取文件时,使用BufferedReader对象,BufferedReader是如何读取:
当使用BufferedReader读取文件时,在BufferedReader中维护了一个数组
,这个数组中存放的数据是由底层直接读取存放进去的。当我们调用BufferedReader的read
方法时,这个read
方法会到BufferedReader的缓冲区中读取数据。
由于BufferedReader读取的都是文本数据,文本数据可以按行的方式进行读取的,调用BufferedReader的readLine
方法,readLine
方法中维护了一个容器,当使用readLine
读取数据,仍然会使用BufferedReader的read
方法到缓冲区中去读数据,并存放到readLine
自己的容器中,读取时,如果读取到了’\r’
时,会继续往下读,读到’\n’
时,表示一行读取结束了。readLine
会把自己容器中的数据变成字符串返回给调用readLine
的调用者。
总结:缓冲区原理:
1、使用了底层流对象从具体设备上读取数据,并将读取到的数据存储到缓冲区的数组中。
2、通过缓冲区的read方法从缓冲区来获取具体的字符数据,这样就提高了效率。
3、如果read方法读取字符数据,并存储到另外一个容器中,直到读取到了换行符时,另一个容器中临时存储的数据转成字符串返回,就形成了readLine的功能。
4.2、字符缓冲区read()方法实现
自定义一个字符流缓冲区。 用于缓冲字符数据,从而提高操作效率。并提供了更多操作缓冲区数据的方法。 需要使用具体的流对象来完成数据的获取。
分析: 缓冲区应该具备 1,必须要有数组。 2,需要对数组进行操作,对数组操作一定要有角标。
public class MyBufferedReader {
// 底层流对象
private Reader r;
// 定义一个字符数组,作为缓冲区。
private char[] buf = new char[1024];
// 定义了一个索引,用于操作数组中的元素。
private int index = 0;
// 定义了一个变量,用于记录读取字符的个数。
private int count = 0;
// 需要一初始化就具备一个流对象。
public MyBufferedReader(Reader r) {// 可以对Reader的所有子类进行高效读取。
this.r = r;
}
//提供一个可以从缓冲区中读取一个字符的方法。 高效方法。
public char read() throws IOException {
// 1,需要先通过流对象从底层设备上获取一定数据的数据到缓冲区数组中。 使用流对象read(char[]);
// 如果count记录字符个数的变量为0,说明缓冲区已经没有字符数据。
if(count==0){
//需要从设备上获取一定数量的数据存储到缓冲区中,并用count记录存储字符的个数。
count = r.read(buf);
//每取一次新的数据,就需要将角标归0.
index = 0;
}
//如果count小于0,说明到-1,没有数据了,程序直接返回-1.
if(count<0){
return -1;
}
//从缓冲区中取出一个字符。
char ch = buf[index];
//角标自增。
index ++;
//计数器要自减。
count --;
return ch;
}
// 关闭流资源。
public void close() throws IOException {
// 其实内部就是关闭具体的流。
r.close();
}
}
4.3、readLine方法实现
思路:
从缓冲区中一次获取一个字符,并将这个字符存储到临时容器中。每获取一个字符都要进行判断,只要不是行终止符都进行存储。一旦读取到行终止符,就将临时容器中的数据转成字符串返回。
//基于高效的read方法,建立一个一次可以读取一行的数据的方法。 将行终止符前的数据转成字符串返回。
public String readLine() throws IOException{
//1,定义一个临时容器。
StringBuilder sb = new StringBuilder();
//2,调用本类中的read方法,从缓冲区中读取一个字符,存储到临时容器中。
//存的时候要注意:必须判断,如果是行终止符就不要存储了。就将临时容器中的
//字符转成字符串返回。
int ch = 0;
while((ch=this.read())!=-1){
if(ch=='\r'){
continue;
}
if(ch=='\n'){
return sb.toString();
}
sb.append((char)ch);//将读取到的字符数字转成char类型,存储到sb中。
}
//万一文本中最后一行没有行终止符,判断一下sb中是否有内容,如果有则返回。
if(sb.length()!=0){
return sb.toString();
}
return null;
}
5、装饰设计模式
5.1、装饰设计模式
装饰设计模式: 顾名思义就是对原有事物进行包装打扮,增强原有事物的功能。但是事物的本质是不变的。比如大家去礼品店买礼品,都会对礼品进行相应的包装,但不管怎么包装,礼品本身是不会发生改变的。但让我们感觉礼品更加具有吸引力和价值的体现。
在Java中也有大量使用装饰这种设计模式,比如前面学习字节流对象或者字符流对象,使用他们就可以完成字节、字符的读取操作,可是直接使用发现不管是读、还是写效率都比较低,于是就对原有的字节流和字符流进行包装,使用Buffered对其进行功能的扩展,发现读写效率都高了很多。
举例说明:
假设读取图片
使用ImageReader可以完成,为了提高效率,可以使用BufferedImageReader继承ImageReader,并增强其读数据功能,即对图片数据读取提高了效率。
假设读取媒体数据
使用MediaReader可以完成,为了提高效率,可以使用BufferedMediaReader继承MediaReader,并增强其读数据功能,即对媒体数据读取提高了效率。
总结:
读取文件 Reader
读取图片 ImageReader
提高效率 BufferedImageReader
读取媒体 MediaReader
提高效率 BufferedMediaReader
发现了一个小问题,如果Reader中还有读取其他数据的子类,如果要高效,那岂不是还要给这个子类添加一个高效子类?是的。为了给具体的读取数据的对象增加一些功能,是需要通过子类来完成的。但是这样做,会导致这个继承体系很臃肿!仅仅为了增加一些功能,而进行继承,不建议的。
这些子类无非就是需要高效,而且这些高效的功能实现是一致的。就是提供了一个缓冲区而已。没有必要每一个对象都存在一个功能重复的子类。
干脆,单独定义一个具备这个缓冲功能的对象,哪个子类需要被缓冲,就将哪个子类传递进来。
class BufferedReader extends Reader{
private Reader r;
BufferedReader(Reader r){// 对Reader高效就哦了 。
}
read(){操作的是数组}//高效的读取动作。
}
继承体系:
Reader
ImageReader
MediaReader
BufferedReader
发现这种设计方式减少了继承体系的臃肿,增减了功能,比继承更为灵活。这种设计方式单独定义一个名称:装饰设计模式
。
解决问题:给一组类增加功能, 避免继承的臃肿,提高灵活。
注意:装饰类和被装饰类必须所属于同一个体系
,通常装饰类都会提供构造函数
接收被装饰类对象。装饰类通常不单独存在。
5.2、键盘录入
目前我们学习IO的对象基本都是在操作文件和文件中的数据,但是数据来源不全都是文件,也可以使键盘或者其他地方。那么怎么读取键盘数据呢?
思路:
1、将数据存储到的文件,没有问题的。
2、怎么获取数据来源呢?键盘录入怎么弄呢?键盘录入是输入,系统中应该具备的一部分。在System类找到了标准输入流 属性 in。System.in 对应的类型是InputStream。字节输入流。
public class ReadKeyDemo {
public static void main(String[] args) throws IOException {
//获取了键盘录入的输入流对象。可以不用关闭。
InputStream in = System.in;
int ch = in.read();
System.out.println(ch);
}
}
上述程序只能读取一个字符,当要读取多个字符怎么办呢?
思路:
读取一个字节先不要操作,将其存储起来,转成一个字符串。能不能一次就读取一行字符串呢?readLine();可是readLine()是BufferedReader方法。BufferedReader使用时必须接收字符流对象。键盘录入是字节流。要是将字节流转成字符流是不是就哦了呢?咋转呢?节流—桥梁InputStreamReader—>字符流
public class ReadKeyDemo2 {
public static void main(String[] args) throws IOException {
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
String line = null;
while((line=bufr.readLine())!=null){//键盘录入记住定义结束标记。强制结束。
if("over".equals(line)){
break;
}
System.out.println(line);
}
}
}
记住:以后但凡提到了键盘录入就写这句,一行一行的读取,除非要对读取每一个字节操作。
5.3、键盘录入数据存储到文件中
思路:
1、键盘录入。
2、目的是文件。
3、这个示例中既要用到输入流,也要用到输出流。而且操作的数据都是文本数据,可以使用字符流。而且目的是文件可以使用操作文件的字符输出流。
public class KeyDataToFileTest {
public static void main(String[] args) throws IOException {
//键盘录入。
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
//目的是文件。
BufferedWriter bufw = new BufferedWriter(new FileWriter("tempfile\\key.txt"));
String line = null;
while((line=bufr.readLine())!=null){
if("over".equals(line)){
break;
}
bufw.write(line);
bufw.newLine();
bufw.flush();
}
bufw.close();
}
}
举例
// Person类
class Person
{
public void chiFan()
{
//吃饭
System.out.println("吃饭");
}
}
class SuperPerson
{
private Person person;
public SuperPerson(Person _person)
{
this.person=_person;
}
// 增强
public void superChiFan()
{
System.out.println("开胃菜");
this.person.chiFan();//原始功能
System.out.println("抽根烟");
}
}
class PersonDemo
{
public static void main(String[] args)
{
Person person=new Person();
SuperPerson sp=new SuperPerson(person);
sp.superChiFan();
}
}
6、对象序列化流
用于操作对象的流对象。对象的序列化。ObjectOutputStream
特点:用于操作对象。
解决问题:可以将对象进行序列化和反序列化。
注意:对象序列化一定要实现Serializable接口。为了给类定义一个serialVersionUID。
功能:ObjectInputStream readObject() ObjectOutputStream writeObject()
关键字:瞬态:transient
public class ObjectStreamDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
/*
* 将一个对象存储到持久化(硬盘)的设备上。
*/
writeObj();//对象的序列化。
}
public static void writeObj() throws IOException {
//1,明确存储对象的文件。
FileOutputStream fos = new FileOutputStream("tempfile\\obj.object");
//2,给操作文件对象加入写入对象功能。
ObjectOutputStream oos = new ObjectOutputStream(fos);
//3,调用了写入对象的方法。
oos.writeObject(new Person("wangcai",20));
//关闭资源。
oos.close();
}
}
7、流的操作规律
IO流中对象很多,解决问题(处理设备上的数据时)到底该用哪个对象呢?
把IO流进行了规律的总结(四个明确):
明确一 :要操作的数据是数据源还是数据目的。
源: InputStream Reader
目的: OutputStream Writer
先根据需求明确要读,还是要写。
明确二: 要操作的设备上的数据是字节还是文本呢?
源:
字节: InputStream
文本: Reader
目的:
字节: OutputStream
文本: Writer
已经明确到了具体的体系上。
明确三: 明确数据所在的具体设备。
源设备:
硬盘: 文件 File开头。
内存: 数组,字符串。
键盘: System.in;
网络: Socket
目的设备:
硬盘: 文件 File开头。
内存: 数组,字符串。
屏幕: System.out
网络: Socket
完全可以明确具体要使用哪个流对象。
明确四: 是否需要额外功能呢?
额外功能:
转换吗?转换流:InputStreamReader OutputStreamWriter
高效吗?缓冲区对象:BufferedXXX