接上节,这节开始来聊聊以流的方式完成对数据读写。
说到这里可能就有点疑惑了。
流?流是什么东东啊??
如果一拿“流”字来组词,脑海里第一反应就是什么“水流”,“河流”之类的词。那么“水流”,“河流”,都有什么特征呢?
第一反应就是只要是流,就绝对会有方向啊,绝对还会有源头啊目的地啊,一去不复返(只能覆盖写或追加写)
那么在java中,将输入输出都抽象为流,将产出数据的对象(水流的源头)和接收数据的对象(水流的目的地)统称为数据源。一个对象可以打开一个数据源上的流,然后按顺序读取这个流中的数据到对象中,这样的流称为输入流。一个对象也可以打开一个目的地的流,然后按顺序的把对象中的数据写入到这个目地中,这样的流称为输出流。该对象可以是文件,网络,内存等。
这里有个问题还要再细说一下。输出流和输入流是相对的概念。举个现实中的例子,一个人,他既可以做输入流也可以做输出流。当他吃东西的时候就是输入流,当他在厕所排泄的时候,他又变成输出流。
说清楚什么是流后,我们再来说说流的分类。
1.按照流的方向,分为输出流和输入流。
2.按照流的功能,分为节点流(低级流)和处理流(高级流)
3.按照流的每次处理数据单位的不同,分为字节流和字符流
第一种分类我们已经在谈论什么是流的时候说清楚了。
我们再来说说第二种分类。什么是按照流的功能?举个显示生活中的例子。水龙头,是一条输出流,水龙头是在直接在处理数据(水),这就是节点流(低级流)。如果我想要水龙头流出的水加热,我就在水龙头上装上一个加热器,这个加热器,这个加热器并不是直接处理水,而是在处理水龙头流出的水。如果我把水龙头关了,并不会有任何热水流出。向这种不直接处理数据,而是对已经存在的低级流(节点流)升级,使本来的低级流具有跟多的功能的流,称为处理流(过滤流,高级流)。高级流(过滤流,处理流)单独存在没有任何意义,必须抵赖低级流的存在才会有意义。
这里需要注意一点,高级流并不是一定要处理低级流。接着上面的例子,我如果想要热的纯净水怎么办?我就再在热会器上再组装一个过滤器,这个过滤器是一个高级流,他是处理另一个高级流(热水器)。
再来说说第三种分类:每次处理数据单位的不同:字节流,字符流
这个又是什么意思呢?
字节流以字节(8bit)为读写单位,字符流以字符为读写单位。
大家都知道计算机的所有内容,实质上都是用字节表示的,只不过是解码方式不同,人们才看到不同的内容。那么也就是说,字符流实际上是对字节流的升级,因为读取字符的操作特别多,所以,java专门为了读取字符的方便,创建了字符流,方便读取字符。也就是说,字符流实际上是一个高级流,必须依赖于字节流才有意义
这里就要强调一点了,字节流可以读取任何内容,比如字符,图片,音频等。但是字符流只能读取字符!
在这里再解释两个专业名词:
序列化:将对象转换为一组字节的过程
持久化:将数据从内存搬到硬盘的过程
讲了这么多的概念。现在用代码来演示
字节流——低级流——输出流
java.io.FileOutputStream
向文件fos.txt中写出数据
写出一个字符串:
摩擦摩擦,是魔鬼的步伐
过程:
将String->一组字节Byte[]->利用重载的write方法写入一组字节(FileOutputStream和FileRandomStream的写入方法类似)
这里需要注意:将字符串转为Byte[]数组的方法是String类的getBytes()方法,不写参数默认为系统编码,它有个重载方法,可以传入一个给定的字符集,按照这个字符集将当前字符串转换为一组字节
在用RandomAcceesFile覆盖写时,只会覆盖需要写入的的那部分字节,而FileOutputStream不论需要写入多少字节,都会全部覆盖
demo1
public static void main(String[] args) throws IOException {
String str="摩擦摩擦,是魔鬼的步伐";
/*
* String->Byte[]
*
* byte[] getBytes()
* 按照当前字符串按照系统默认的字符集转换为对应的一组字节
* (不推荐使用,和当前系统产生耦合关系)
*
* byte[] getBytes(String csn)
* 根据给定的字符集(不区分大小写)将当前字符串转换为一组字节,常见的字符集:
* GBK:国标编码,中文占2个字节(window默认的字符集)
* UTF-8:unicode的一个子集,其中中文占3个字节(Unix默认的字符集)
* ISO8859-1:欧洲编码集,不支持中文
*/
byte[] bytes=str.getBytes("GBK");
/*
* 向文件fos.txt中写出数据
* 写出一个字符串:
* 摩擦摩擦,是魔鬼的步伐
*
* !注意:这里如果再用RandomAccessFile写“你好”
* 不会全部覆盖原来的“摩擦摩擦,是魔鬼的步伐”,只会覆盖头两个字
* 这是FileOutputStreanm干不了的(只能全部覆盖或者追加)
*
* RandomAccessFile的追加操作(把指正移动到文件末尾)
* raf.seek(raf.length());
* 再进行写入操作就是追加操作
*
*/
/*
* RandomAccessFile形式
*/
// RandomAccessFile raf=new RandomAccessFile("fos.txt","rw");
// raf.write(bytes);
// raf.writeChar('李');
// raf.close();
/*
* FileOutputStream形式
*/
FileOutputStream fos=new FileOutputStream("fos.txt");
fos.write(bytes);
fos.close();
}
Fos的覆盖写模式与追加写模式
demo2
public static void main(String[] args) throws UnsupportedEncodingException, IOException {
/*
* 以默认形式创建的FOS,若写出的文件已经存在,
* 这会将该文件所有内容全部清除后重写开始写操作
*
* 覆盖写操作
*/
// FileOutputStream fos=new FileOutputStream("fos.txt");
//
// fos.write("你好".getBytes("GBK"));
//
// //!!现在的fos.txt文本中只有“你好”没有“摩擦摩擦,是魔鬼的步伐”(覆盖写操作)
// System.out.println("写出完毕!");
// fos.close();
/*
* 追加写操作:在当前文件末尾开始写入内容。只需要
* 使用重载构造方法,第二个参数传入true即可
*/
FileOutputStream fos=new FileOutputStream("fos.txt",true);
fos.write("你好".getBytes("GBK"));
//现在的fos.txt文本中“你好”后又加了“你好”(追加写操作)
System.out.println("写出完毕!");
fos.close();
}
字节流——低级流——输入流
java.io.FileInputStream
注意:
byte[]—>String
String(Byte[] b,int start,int end)
String的构造方法可以将给定的字符数组中指定范围内的字节按照系统默认字符集转换为字符串
String(Byte[] b)
若不指定范围,则是将字节数组中所有字节进行转换
String(Byte[] b,int start,int end,String code)
最后可以添加一个字符串参数,指定字符集
demo3
public static void main(String[] args) throws IOException {
FileInputStream fis=new FileInputStream("fos.txt");
byte[] date=new byte[100];
int len=fis.read(date);
System.out.println("读取到了"+len+"个字节");
/*
* byte[]->String
* String(Byte[] b,int start,int end)
* String的构造方法可以将给定的字符数组中指定范围内的字节按照系统默认字符集转换为字符串
* String(Byte[] b)
* 若不指定范围,则是将字节数组中所有字节进行转换
* String(Byte[] b,int start,int end,String code)
* 最后可以添加一个字符串参数,指定字符集
*/
String str=new String(date,0,len);
System.out.println(str);
fis.close();
}
使用文件低级流进行复制
demo4
public static void main(String[] args) throws IOException {
FileInputStream fis=new FileInputStream("music.flac");
FileOutputStream fos=new FileOutputStream("nusic_copy.mp3");
int flag=-1;
byte[] bytes=new byte[1024*10];
long start=System.currentTimeMillis();
while((flag=fis.read(bytes))!=-1){
fos.write(bytes, 0, flag);
}
long end=System.currentTimeMillis();
System.out.println("复制完毕");
System.out.println("耗时:"+(end-start)+"ms");
fis.close();
fos.close();
}
使用缓冲流提高读写效率,完成复制文件操作,缓冲流是高级流
字符流——高级流——输入流
BufferedInputStream
字符流——高级流——输入流
BufferedOutputStream
demo5
/**
* 使用缓冲流提高读写效率
*
* 缓冲流是高级流
*
* 高级流:高级流数用来处理其他流的,目的是简化我们的读写操作。
* 高级流不独立存在,因为没有意义
* 低级流:低级流是真实负责读写数据的流。数据源明确
*
* @author Analyze
*
*/
public class CopyDemo2 {
public static void main(String[] args) throws IOException {
FileInputStream fis=new FileInputStream("music.flac");
BufferedInputStream bis=new BufferedInputStream(fis);
FileOutputStream fos=new FileOutputStream("music_copy.mp3");
BufferedOutputStream bos=new BufferedOutputStream(fos);
/*
* 缓冲流内部有一个字节数组,作为缓冲区,当第一次调用read方法读取一个字节时
* 缓冲流实际上读取了若干字节,并存入其内部的字节数据中,
* 然后返回第一个字节,当我们再次调用read方法时,不就不会再去读取了,而是将字节数据中的第二个字节返回。
* 知道当前自己数组中所有自己全部返回后,曹辉在下次read方法时再次读取若干字节并存入字节数组。
* 所以缓冲流也是依靠一次读写多个字节,减少读写次数来提高读写效率
*/
int d=-1;
long start=System.currentTimeMillis();
while((d=bis.read())!=-1){
bos.write(d);
}
long end=System.currentTimeMillis();
System.out.println("复制完毕!耗时:"+(end-start)+"ms");
fis.close();
fos.close();
}
使用缓冲流写出数据的注意事项
如果使用缓冲流写出少量字节(调用write()方法),不flush不会写出,因为Byte[]数字没有装满,缓冲流不会写出这些少量字节,会继续等待接收字节,直到装满后写出
close会前自动flush,关流只需要关最外层的高级流
demo6
public static void main(String[] args) throws IOException {
FileOutputStream fos=new FileOutputStream("bos.txt");
BufferedOutputStream bos=new BufferedOutputStream(fos);
bos.write("你好".getBytes());
/*
* 强制将当前缓冲流的缓冲区内容全部写出
* 需要注意,频繁flash会提高写出次数,降低写出效率。
* 但是写出的及时性得以保证
*/
bos.flush();
/*
* close会前自动flush,关流只需要关最外层的高级流
*/
bos.close();
}
字符流——高级流——输出流
java.io.ObjectOutputStream
对象输出流。是一个高级流。使用该流可以很方便的将Java中任意对象转换为字节后写出。
写出类Person
import java.io.Serializable;
import java.util.List;
/**
* 使用该类测试对象流的对象读写操作
*
* 所有需要别ObjectOutputStream写出的对象都必须实现Serializable;
*
* Serializable:可序列化
* @author Analyze
*
*/
public class Person implements Serializable {
/**
* 当一个类实现了Serializalble接口后,应当定义一个常量serialVersionUID.该常量的值表示当前类的版本号
* 该版本号的值直接影响着反序列化的结果
*
* 若不指定该常量,那么当前类的实例在被ObjectOutputStream进行序列化时会自动生成一个版本号并包含在序列化后的字节中。
*而生成的版本号是根据当前列的结构,只要当前类的属性以及类型没有发生变化,版本号不会变得。但是一旦改变那么版本号是会变化的
*
*为了可以控制反序列化的结果,版本号应当自行定义
*
*版本号对于反序列化的影响
*当前使用ObjectInputStream进行对象反序列化时,首先会检查该字节中描述的对象版本号是多少
*然后在和该对象所属类当前的版本号对比,若不一致,则可以进行反序列化,若不不一致则反序列化会抛出异常,不在允许进行反序列化
*
*若待反序列化的字节描述的对象的结构与其所属类有的结构不一致时,
*若版本号一致,则会启动兼容模式,即:仍然有的属性就还原,已经没有的属性则忽略
*/
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String gender;
/*
* 一个对象在进行可序列化后得到的字节量往往比当前对象本身存储的内容要大很多
* 当该对象的某些属性在序列化时不需要,那么可以使用transient关键字将其忽略
* 这样当前对象在序列化后的字节中就不在包含这些属性的值了。从而达到了对象“瘦身”的效果
* transient关键字除了序列化时标示属性忽略外,没有其他任何作用
*
* transient:短暂的,流动的
*/
private transient List<String> otherInfo;
public Person(){
}
public Person(String name, int age, String gender, List<String> otherInfo) {
super();
this.name = name;
this.age = age;
this.gender = gender;
this.otherInfo = otherInfo;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public List<String> getOtherInfo() {
return otherInfo;
}
public void setOtherInfo(List<String> otherInfo) {
this.otherInfo = otherInfo;
}
public String toString() {
return "Person [name=" + name + ", age=" + age + ", gender=" + gender + ", otherInfo=" + otherInfo + "]";
}
}
将person对象写出
public static void main(String[] args) throws IOException {
Person p=new Person();
p.setName("苍老师");
p.setAge(22);
p.setGender("女");
List<String> otherInfo=new ArrayList<String>();
otherInfo.add("是一名演员");
otherInfo.add("喜欢写毛笔字");
otherInfo.add("促进中日文化交流");
otherInfo.add("擅长爱情电影和动作电影");
p.setOtherInfo(otherInfo);
System.out.println(p);
FileOutputStream fos=new FileOutputStream("person.txt");
/*
* 对象输出流。
* 可以方便的将对象转换为字节后写出
* 这里由于该对象流处理的是文件输出流,所以经过该流写出的对象首先会转换为一组字节,
* 然后再经由文件输出流写入到文件中
*
* 对象->oos->变为一组字节->fos->写入到文件
*/
ObjectOutputStream oos=new ObjectOutputStream(fos);
/*
* 该方法就是将对象写出的。
* 会将该对象转换为一组字节,这就是OOS的独有方法
*
* 将对象转换为一组字节的过程:对象序列化
* 将数据从内存搬到硬盘的过程称为:持久化
*/
oos.writeObject(p);
System.out.println("写出完毕!");
oos.close();
}
字符流——高级流——输出流
java.io.OutputStreamWriter
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
/**
* 字符流:读写单位是字符
* 字符流只适合读写文本数据!
* 字符流都是高级流,因为读写本质都是字节,字符流只是对字符串进行了相应的处理
* 最终还是要转换为字节写出的
*
* java.io.OutputStreamWriter
* 字符输出流,特点:可以按照指定的字符集,讲写出的字符
* 转换为对应的字节
* @author Analyze
*
*/
public class OSWDemo {
public static void main(String[] args) throws IOException {
/*
* 向文件osw.txt中写出字符串
*/
FileOutputStream fos=new FileOutputStream("osw.txt");
fos.write("李岳泽".getBytes("GBK"));
fos.write("阿泽".getBytes("GBK"));
/*
* OutputStreamWriter(OutputStream out)
* 默认创建出来的OSW是使用当前系统默认字符集将指定的字符串转换为字节后写出的
*
* 若希望按照指定字符集,OSW还支持一个重载的构造方法
* 支持第二个参数,该参数是一个字符串,用来指定特定的字符集
*/
OutputStreamWriter osw=new OutputStreamWriter(fos,"GBK");
osw.write("李二");
osw.write("33");
osw.close();
}
}
字符流——高级流——输如流
java.io.InputStreamReader
字符输入流,特点:可以按照指定的字符集读取字符
public static void main(String[] args) throws IOException {
FileInputStream fis=new FileInputStream("osw.txt");
/*
* 与输出流一样,也可以通过第二个参数指定字符集
* 否则就是按照系统默认字符集读取
*/
InputStreamReader isr=new InputStreamReader(fis,"GBK");
int d=-1;
while((d=isr.read())!=-1){
System.out.print((char)d);
}
isr.close();
}
字符流——高级流——输出流
java.io.BufferdeWriter
特点是内部维护一个缓冲区,以达到一次写入多个字符,
减少写出次数提高写出字符效率
现在更常使用java.io.PrintWriter
特点是具有自动行刷新的缓冲字符输出流。其内部的缓冲是
依靠BufferedWriter实现的
缓冲字符可以按行读写字符串
public static void main(String[] args) throws FileNotFoundException {
/*
* PrintWriter针对文件写操作提供了
* 比较方便的构造方法:
* PrintWriter(File file)
* PrintWriter(String path)
*
* 向pw.txt文件中写出字符串
*
* 上述两种构造方法都支持第二个参数,是一个字符串。
* 可以指定字符集
*/
PrintWriter pw=new PrintWriter("pw.txt");
pw.println("写点啥好呢?");
pw.println("我已经词穷了");
pw.println("那就啥也别写了");
//!!!!注意:不写flush或者close是不会写去的,会存在缓冲区里。与BufferedInputStream类似
pw.flush();
pw.close();
}
PrintWriter构造方法源码:
public PrintWriter(String fileName) throws FileNotFoundException {
this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName))),
false);
}
我们可以从源码中看到,其实在new PrintWrite()时,是先创建了字节低级输出流FileOutputStream,再升级为字符高级输出OutputStreamWriter,再升级为PrintWriter
使用PrintWriter处理其他流时,它提供的构造方法既可以允许我们直接处理字节流,也可以处理其他字符流
并且,当使用处理其他流的这些构造方法时,还可以传入第二个参数是一个boolean值,当该值为true时,则具有自动行刷新功能
即:每当使用println方法写出字符串时会自动flush
演示代码:
public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException {
/*
* 向文件pw1.txt中写入字符串
*/
FileOutputStream fos=new FileOutputStream("pw1.txt");
// PrintWriter pw=new PrintWriter(fos);
/*
* 若直接处理的是字节流,那么只能按照系统默认字符集写出字符串
*/
// pw.println("如果能重来");
// pw.println("我要做李白");
// pw.println("几百年前做的好坏没那么多人猜");
/*
*若希望按照指定字符集写出字符串则需要在中间再加一个高级刘OutputStreamWriter来指定字符集
*/
OutputStreamWriter osw=new OutputStreamWriter(fos,"UTF-8");
PrintWriter pw=new PrintWriter(osw,true);
pw.println("如果能重来");
pw.println("我要做李白");
pw.println("几百年前做的好坏没那么多人猜");
pw.close();
}
使用PrintWriter实现记事本功能
即:在控制台输出的每一个字符串都可以保存到文件中
public static void main(String[] args) throws FileNotFoundException {
Scanner scan=new Scanner(System.in);
System.out.println("请输入文件名");
String name =scan.nextLine();
/*
* 若第一个参数为流,可以使用第二个参数来指定是否自动行刷新
*/
PrintWriter pw=new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(name)
),true
);
System.out.println("请开始编写内容:(若输入exit则退出)");
String line=null;
while(!"exit".equals(line)){
line=scan.nextLine();
/*
* 当PrintWriter被指定为自动行刷新以后
* 那么每当通过Println方法写出的字符串后都会自动Flush
* 注意:print方法不会自动行刷新
*/
pw.println(line);
}
pw.close();
}
字符流——高级流——输入流
java.io.BufferedReader
缓冲字符输入流,特点:读取速度快,并且可以按行读取字符串。
public static void main(String[] args) throws IOException {
/*
* 将当前程序的所有代码输出到控制台
*/
FileInputStream fis=new FileInputStream("src"+File.separator+
"se"+File.separator+"day08"+File.separator
+"BRDemo.java");;
InputStreamReader isr=new InputStreamReader(fis);
BufferedReader br=new BufferedReader(isr);//必须在字符流的基础上
/*
* String ReadLine()
* 该方法会连续读取若干字符,直到遇到换行符为止
* 然后将之前的所有字符组成个字符串返回
* 需要注意,该字符串中不包含换行符
* 若读取到文件末尾,则返回Null
*/
String line=null;
while((line=br.readLine())!=null){
System.out.print(line);
}
br.close();
}
使用OSW与ISR实现文本数据转码
public static void main(String[] args) throws IOException {
/*
* ose.txt文件现在是一个gbk编码的文件。
* 需要将其转换为一个utf-8编码的文件
*/
FileInputStream fis=new FileInputStream("osw.txt");
InputStreamReader isr=new InputStreamReader(fis,"GBK");
FileOutputStream fos=new FileOutputStream("ose_utf-8.txt");
OutputStreamWriter osw=new OutputStreamWriter(fos,"UTF-8");
int d=-1;
while((d=isr.read())!=-1){
osw.write(d);
}
System.out.println("转码完毕");
isr.close();
osw.close();
}