什么是IO流?
byte序列的读写,Java中的IO流是实现输入/输出的基础.
Java将数据从源(文件、内存、键盘、网络)读入到内存 中,形成了流,然后将这些流还可以写到另外的目的地(文件、内存、控制台、网络),之所以称为流,是因为这个数据序列在不同时刻所操作的是源的不同部分。按照不同的分类标准,IO流分为不同类型。主要有以下几种方式:按照数据流方向、数据处理的单位和功能。
不管流的分类是多么的丰富和复杂,其根源来自于四个基本的类。这个四个类的关系如下:
| 字节流 | 字符流 |
输入流 | InputStream | Reader |
输出流 | OutputStream | Writer |
输入流与输出流:
数据从内存到硬盘,通常认为是输出流,即写操作;相反,从硬盘到内存,通常认为是输入流,即读操作;这里的输入、输出是从内存的角度划分的。
字节(byte 1字节)流和字符(char 2字节)流:
字节流和字符流区别非常简单,它们的用法几乎一样。区别在于字节流和字符流所处理的最小数据单元不同。
| 处理最小数据单元 | 基类 |
字节流 byte | 8 | In/OutStream |
字符流 char | 16 | Reader/writer |
节点流和处理流:
节点流是可以从或向一个特定的地方(节点)读写数据,也叫 低级流。如FileReader。
处理流是在对节点流封装的基础上的一种流,通过封装后来实现数据的读写功能,也叫高级流。
|
|
节点流 | 未经封装,low level stream |
处理流 | 封装过, high level stream |
常用节点流
父 类 InputStream OutputStream Reader Writer
文 件 FileInputStream FileOutputStrean FileReader FileWriter 文件进行处理的节点流
数 组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter 对数组进行处理的节点流(对应的不再是文件,而是内存中的一个数组)
字符串 StringReader StringWriter 对字符串进行处理的节点流
管 道 PipedInputStream PipedOutputStream PipedReader PipedWriter 对管道进行处理的节点流
常用处理流(关闭处理流使用关闭里面的节点流)
父 类 InputStream OutputStream Reader Writer
缓冲流 BufferedImputStrean BufferedOutputStream BufferedReader BufferedWriter
----需要父类作为参数构造,增加缓冲功能,避免频繁读写硬盘,可以初始化缓冲数据的大小,由于带了缓冲功能,所以就写数据的时候需要使用flush 方法咯
转换流 *InputStreamReader OutputStreamWriter- 要inputStream 或OutputStream 作为
参数,实现从字节流到字符流的转换数据流 *DataInputStream DataOutputStream -提供将基础数据类型写入到文件中,或者
读取出来,为什么要有这个流呢?看这样的分析,如果没有这种流的话,有一个long,本身只占8 个字节,如果我要写入到文件,需要转成字符串,然后在转成字符数组,那空间会占用很多,但是有了这种流之后就很方便了,直接将这8 个字节写到文件就完了。。是不是既节约了内存空间有让程序写起来更加方便简单了呐。写倒是很简单,但是读取的时候就注意了,根据读取的数据类型,指针会往下移,所以你写的顺序必须要和读的顺序一致才能完成正确的需求.
Java输入输出流总结:
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
访问字符串 |
|
| StringReader | StringWriter |
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 |
|
| InputStreamReader | OutputStreamWriter |
对象流 | ObjectInputStream | ObjectOutputStream |
|
|
抽象基类 | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
打印流 |
| PrintStream |
| PrintWriter |
推回输入流 | PushbackInputStream |
| PushbackReader |
|
特殊流 | DataInputStream | DataOutputStream |
|
|
Java是一种完全面向对象的高级语言,所以在编写程序的时候数据大都存放在对象当中。我们有时会需要将内存中的整个对象都写入到文件中去,然后在适当的时候再从文件中将对象还原至内存。我们可以使用java.io.ObjectInputStream和java.io.ObjectOutputStream类来完成这个任务。
1、什么是对象的序列化(Serialize)?为什么要实现对象的序列化?
序列化是指将对象的状态信息转换为可以存储或传输的形式(2进制数据)的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
序列化的目的:
1)永久的保存对象,保存对象的字节序列到本地文件中;
2)通过序列化对象在网络中传递对象;
3)通过序列化对象在进程间传递对象。
2、ObjectInputStream类 和ObjectOutputStream类
如果我们想要序列化一个对象,如我们自定义的User类的对象,那么这个对象必须实现Serializable接口。Serializable接口没有任何的抽象方法,实现这个接口仅仅是为了通知编译器已这个对象将要被序列化,所以此接口仅仅是一个表示接口。类似的用法还有Cloneable接口,实现这个接口也只是起到通知编译器的作用。
3.对象的序列化和反序列化
想要完成对象的输入输出,还必须依靠ObjectInputStream和ObjectOutputStream;
使用对象输出流输出序列化对象的步骤,也称为序列化,而使用对象输入流读入对象的过程,也称为反序列化。
为了演示如何进行对象的序列化,我们先设计一个User类:
<span style="color:#333333;">package cls;
import java.io.*;
public class User implements Serializable // 实现Serializable接口,</span><span style="color:#ff0000;">仅仅起到标识这个类可被序列化的作用</span><span style="color:#333333;">
{
// 可序列化对象的版本
private static final long serialVersionUID = 1L;
private String name;
private int num;
public User(String name,int num)
{
this.name = name;
this.num = num;
}
public String getName()
{
return name;
}
public int getNum()
{
return num;
}
} </span>
4.serialVersionUID
在对象进行序列化或者反序列化操作的时候,要考虑JDK版本的问题,如果序列化的JDK版本和反序列化的JDK版本不统一则就可能造成异常,所以在序列化操作中引入了一个serialVersionUID的常量,可以通过此常量来验证版本的一致性,在进行反序列化时,JVM会将传过来的字节流中的serialVersionUID与本地相应实体的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就抛出不一致的异常。
5.到底序列化了哪些东西呢?
所有的对象拥有各自的属性值,但是所有的方法都是公共的,所以序列化对象的时候实际上序列化的就是属性。
下面我们使用ObjectInputStream类 和ObjectOutputStream类 向文件中写入3个User对象,追加1个User对象,最后再从文件中读回对象。
package cls;
import java.io.*;
import java.util.*;
import cls.User;
public class ObjectStreamDemo
{
public static void main(String[] args)
{
User[] user = new User[]{new User("dogg",1),new User("catt",2),new User("pigg",3)};
// 向文件中写入对象
try
{
ObjectStreamDemo.writeObj(user,args[0]);//args[0]传入文件名
}
catch(Exception e)
{
System.out.println(e.toString());
}
// 向文件中追加对象
try
{
// 要追加的对象
User[] u = new User[]{new User("append1",4),new User("append2",5)};
ObjectStreamDemo.appendObj(u,args[0]);
}
catch(Exception e)
{
System.out.println(e.toString());
}
// 读取对象
try
{
List<User> list = ObjectStreamDemo.readObj(args[0]);
// 输出对象信息
Iterator<User> it = list.iterator();
while(it.hasNext())
{
User temp = it.next();
System.out.println(temp.getName() + "," + temp.getNum());
}
}
catch(Exception e)
{
System.out.println(e.toString());
}
}
static private void appendObj(Object[] objs,String fileName) throws Exception
{
File file = new File(fileName);
// 以追加模式创建文件流对象
FileOutputStream fis = new FileOutputStream(file,true);
ObjectOutputStream oos = new ObjectOutputStream(fis)
{
// 重写 writeStreamHeader()方法,空实现
protected void writeStreamHeader(){};
};
// 写入数据
for(Object o : objs)
{
oos.writeObject(o);
}
// 关闭流
oos.close();
}
static private List<User> readObj(String fileName) throws Exception
{
File file = new File(fileName);
// 使用List保存读取出来的对象
ArrayList<User> list = new ArrayList<User>();
// 创建流对象
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
// 读取对象并放入List容器中
while(fis.available() > 0)
{
list.add((User)ois.readObject());
}
ois.close();
return list; // 返回List
}
static private void writeObj(Object[] objs,String fileName) throws Exception
{
// 使用命令行参数中指定的文件名
File file = new File(fileName);
// 创建流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
// 写入对象
for(Object o : objs)
{
oos.writeObject(o);
}
// 关闭流
oos.close();
}
}
注意,
当我们想要向一个已经存在的文件中追加对象时,应该重写ObjectOutputStream的writeStreamHeader()方法,并空实现。
因为,ObjectOutputStream在写入数据的时候会加上一个特别的
流头(Stream Header)
,在读取数据的时候会先检查这个流头。所以我们在向文件中追加对象的时候ObjectOutputStream就会再次向文件中写入流头,这样在读取对象的时候会发生StreamCorrupedException异常。
6.Externalzable接口
写入:void writeExternal(ObjectOutput out) throws IOException
读取:void readExternal(ObjectInput in)throws IOException,ClassNotFoundException
- import java.io.Externalizable ;
- public class Person implements Externalizable{
- private static final long serialVersionUID = 1l;
- private String name ; // 声明name属性
- private int age ; // 声明age属性
- public Person(String name,int age){ // 通过构造设置内容
- this.name = name ;
- this.age = age ;
- }
- public String toString(){ // 覆写toString()方法
- return "姓名:" + this.name + ";年龄:" + this.age ;
- }
- public void writeExternal(ObjectOutput out) throws IOException{
- out.writeObject(this.name); //保存姓名属性
- out.writeInt(this.age); //保age属性
- }
- public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException{
- this.name = in.readObject(); //读取姓名
- this.age = in.readInt(); //读取年龄
- }
- };
为了方便测试,现在将,现在序列化及反序列化操作形成方法调用的形式。
- import java.io.File ;
- import java.io.IOException ;
- import java.io.FileOutputStream ;
- import java.io.OutputStream ;
- import java.io.ObjectOutputStream ;
- import java.io.FileInputStream ;
- import java.io.InputStream ;
- import java.io.ObjectInputStream ;
- public class SerDemo03{
- public static void main(String args[]) throws Exception{
- //ser() ;
- dser() ;
- }
- public static void ser() throws Exception {
- File f = new File("D:" + File.separator + "test.txt") ; // 定义保存路径,参考附录1。
- ObjectOutputStream oos = null ; // 1.声明对象输出流
- OutputStream out = new FileOutputStream(f) ; // 2.文件输出流
- oos = new ObjectOutputStream(out) ; //3.用文件输出流实例化对象输出流
- oos.writeObject(new Person("张三",30)) ; //4.保存对象
- oos.close() ; //5. 关闭
- }
- public static void dser() throws Exception {
- File f = new File("D:" + File.separator + "test.txt") ; // 定义保存路径
- ObjectInputStream ois = null ; // 声明对象输入流
- InputStream input = new FileInputStream(f) ; // 文件输入流
- ois = new ObjectInputStream(input) ; // 实例化对象输入流
- Object obj = ois.readObject() ; // 读取对象
- ois.close() ; // 关闭
- System.out.println(obj) ;
- }
- };
以上程序执行的时候出现了一个错误:
在使用Externalizable接口的时候需要在被序列化的类中定义一个无参构造,因为此接口在进行反序列化的时候,会先使用
类中的无参构造方法为其进行实例化,之后再将内容分别设置到属性之中,因此,修改Person类:
- import java.io.Externalizable;
- import java.io.*;
- public class Person implements Externalizable{
- private static final long serialVersionUID = 1L;
- private String name; //声明name属性
- private int age; //声明age属性
- public Person(){} //无参构造
- public Person(String name, int age){
- this.name = name;
- this.age = age;
- }
- public String toString(){ //覆写toString()方法
- return "姓名:" + this.name + ":年龄:" + this.age;
- }
- public void writeExternal(ObjectOutput out)
- throws IOException{
- out.writeObject(this.name); //保存姓名属性
- out.writeInt(this.age); //保存age属性
- }
- public void readExternal(ObjectInput in)
- throws IOException, ClassNotFoundException{
- this.name = (String)in.readObject(); //读取姓名
- this.age = in.readInt(); //读取年龄
- }
- }
可以看出,序列化的一般步骤可以分为5步:
1.声明对象输出流
2.声明文件输出流,并实例化
3.用文件输出流对象实例化对象输出流
4.调用对象输出流的writeObject函数保存对象
5.关闭对象输出流
反序列化步骤:
1.声明对象输入流
2.声明文件输入流
3.用文件输入流对象实例化对象输入流
4.调用对象输入流的readObject函数读取对象,打印读取到的对象内容
5.关闭对象输入流
Externalizable和Serializable接口实现序列化的区别:
transient关键字:
在序列化操作的时候,如果某个属性不希望被序列化下来,则可以直接使用transient 关键字声明。
- import java.io.Serializable ;
- public class Person implements Serializable{
- private transient String name ; // 声明name属性,但是此属性不被序列化
- private int age ; // 声明age属性
- public Person(String name,int age){ // 通过构造设置内容
- this.name = name ;
- this.age = age ;
- }
- public String toString(){ // 覆写toString()方法
- return "姓名:" + this.name + ";年龄:" + this.age ;
- }
- };
- import java.io.File ;
- import java.io.IOException ;
- import java.io.FileOutputStream ;
- import java.io.OutputStream ;
- import java.io.ObjectOutputStream ;
- import java.io.FileInputStream ;
- import java.io.InputStream ;
- import java.io.ObjectInputStream ;
- public class SerDemo04{
- public static void main(String args[]) throws Exception{
- ser() ;
- dser() ;
- }
- public static void ser() throws Exception {
- File f = new File("D:" + File.separator + "test.txt") ; // 定义保存路径
- ObjectOutputStream oos = null ; // 声明对象输出流
- OutputStream out = new FileOutputStream(f) ; // 文件输出流
- oos = new ObjectOutputStream(out) ;
- oos.writeObject(new Person("张三",30)) ; // 保存对象
- oos.close() ; // 关闭
- }
- public static void dser() throws Exception {
- File f = new File("D:" + File.separator + "test.txt") ; // 定义保存路径
- ObjectInputStream ois = null ; // 声明对象输入流
- InputStream input = new FileInputStream(f) ; // 文件输入流
- ois = new ObjectInputStream(input) ; // 实例化对象输入流
- Object obj = ois.readObject() ; // 读取对象
- ois.close() ; // 关闭
- System.out.println(obj) ;
- }
- };
如果要保存多个对象,则最好使用对象数组的形式完成。
- import java.io.File ;
- import java.io.IOException ;
- import java.io.FileOutputStream ;
- import java.io.OutputStream ;
- import java.io.ObjectOutputStream ;
- import java.io.FileInputStream ;
- import java.io.InputStream ;
- import java.io.ObjectInputStream ;
- public class SerDemo05{
- public static void main(String args[]) throws Exception{
- Person per[] = {new Person("张三",30),new Person("李四",31),
- new Person("王五",32)} ;
- ser(per) ; //Object obj[]可以接收对象数组
- Object o[] = (Object[])dser() ;
- for(int i=0;i<o.length;i++){
- Person p = (Person)o[i] ;
- System.out.println(p) ;
- }
- }
- public static void ser(Object obj[]) throws Exception {
- File f = new File("D:" + File.separator + "test.txt") ; // 定义保存路径
- ObjectOutputStream oos = null ; // 声明对象输出流
- OutputStream out = new FileOutputStream(f) ; // 文件输出流
- oos = new ObjectOutputStream(out) ;
- oos.writeObject(obj) ; // 保存对象
- oos.close() ; // 关闭
- }
- public static Object[] dser() throws Exception {
- File f = new File("D:" + File.separator + "test.txt") ; // 定义保存路径
- ObjectInputStream ois = null ; // 声明对象输入流
- InputStream input = new FileInputStream(f) ; // 文件输入流
- ois = new ObjectInputStream(input) ; // 实例化对象输入流
- Object obj[] = (Object[])ois.readObject() ; // 读取对象
- ois.close() ; // 关闭
- return obj ;
- }
- };
拓展:1、对象序列化的作用,对象序列化并不一定都向文件中保存,也有可能面向于其他的输入或输出2、被序列化的对象的类必须实现Serializable 接口,如果某个属性不希望被保存下来,则可以使用transient 关键字声明。3、ObjectOutputStream 序列化对象,ObjectInputStream 反序列化对象4、Externalizable 接口作用: 开发人员手式实现序列化的操作5、使用序列化保存一组对象的时候要使用对象数组的形式操作
保存的数据有限,所以为了解决这样的问题,Java 中引入了类集 框架解决数组的存储限制问题。不过已经超出本文的讨论范围,以后有机会将会为大家总结。
附录1:
在Windows下的路径分隔符和Linux下的路径分隔符是不一样的,当直接使用绝对路径时,跨平台会暴出“No such file or diretory”的异常。
比如说要在temp目录下建立一个test.txt文件,在Windows下应该这么写:
File file1 = new File ("C:\tmp\test.txt");
在Linux下则是这样的:
File file2 = new File ("/tmp/test.txt");
如果要考虑跨平台,则最好是这么写:
File myFile = new File("C:" + File.separator + "tmp" + File.separator, "test.txt");
File类有几个类似separator的静态字段,都是与系统相关的,在编程时应尽量使用。
separatorChar
public static final char separatorChar
与系统有关的默认名称分隔符。此字段被初始化为包含系统属性 file.separator 值的第一个字符。在 UNIX 系统上,此字段的值为 '/';在 Microsoft Windows 系统上,它为 '\'。
separator
public static final String separator
与系统有关的默认名称分隔符,为了方便,它被表示为一个字符串。此字符串只包含一个字符,即 separatorChar。
pathSeparatorChar
public static final char pathSeparatorChar
与系统有关的路径分隔符。此字段被初始为包含系统属性 path.separator 值的第一个字符。此字符用于分隔以路径列表 形式给定的文件序列中的文件名。在 UNIX 系统上,此字段为 ':';在 Microsoft Windows 系统上,它为 ';'。
pathSeparator
public static final String pathSeparator
与系统有关的路径分隔符,为了方便,它被表示为一个字符串。此字符串只包含一个字符,即 pathSeparatorChar。
附录2:
Android中实现序列化有两种选择:一是实现Serializable接口,二是实现Parcelable接口(android特有的功能,效率比实现Serializable接口高效,可用于Intent数据传递,也可以用于IPC)。
前面介绍了Serializable接口,实现Serializable接口来实现对象的序列化很简单,但是性能没有Parcelable接口高。所以建议使用Parcelable 。
1.什么是Parcelable接口:
Parcelable接口定义了将数据写入Parcel和从Parcel读出的接口。一个对象(实例),如果需要封装到消息中去,就必须实现这一接口,实现了这一接口,该实体就称为可打包的了。
2.应用场景:
需要在多个部件(Activity或Service)之间通过Intent传递一些数据时,简单类型的可以直接放入Intent,复杂类型的必须实现Parcelable接口。
3.Parcelable接口的定义:
public interface Parcelable
{
//内容描述接口,基本不用管
public int describeContents();
//写入接口函数,打包
public void writeToParcel(Parcel dest, int flags);
//读取接口,目的是要从Parcel中构造一个实现了Parcelable的类的实例处理。因为实现类在这里还是不可知的,所以需要用到模板的方式,继承类名通过模板参数传入
//为了能够实现模板参数的传入,这里定义Creator嵌入接口,内含两个接口函数分别返回单个和多个继承类实例
public interface Creator<T>
{
public T createFromParcel(Parcel source);
public T[] newArray(int size);
}
}
通过实现Parcelable接口序列化对象的步骤:
1、实现Parcelable接口。
2、并且实现Parcelable接口的public void writeToParcel(Parcel dest, int flags)方法 。将你的对象序列化为一个Parcel对象,即:将类的数据写入外部提供的Parcel中,打包需要传递的数据到Parcel容器保存,以便从 Parcel容器获取数据
3、重写describeContents方法,内容接口描述,默认返回0就可以
4、自定义类型中必须含有一个名称为CREATOR的静态成员,该成员对象要求实现Parcelable.Creator接口及其方法。
注:其中public static final一个都不能少,内部对象CREATOR的名称也不能改变,必须全部大写。需重写本接口中的两个方法:createFromParcel(Parcel in) 实现从Parcel容器中读取传递数据值,封装成Parcelable对象返回逻辑层,newArray(int size) 创建一个类型为T,长度为size的数组,仅一句话即可(return new T[size]),供外部类反序列化本类数组使用。
简而言之:通过writeToParcel将你的对象映射成Parcel对象,再通过createFromParcel将Parcel对象映射成你的对象。也可以将Parcel看成是一个流,通过writeToParcel把对象写到流里面,在通过createFromParcel从流里读取对象,只不过这个过程需要你来实现,因此写的顺序和读的顺序必须一致。
4、Serializable实现与Parcelabel实现的区别:
1)Serializable的实现,只需要implements Serializable 即可。这只是给对象打了一个标记,系统会自动将其序列化。
2)Parcelabel的实现,不仅需要implements Parcelabel,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现 Parcelable.Creator 接口。
5、两者代码比较:
1)创建Person类,实现Serializable
public class Person implements Serializable
{
private static final long serialVersionUID = -7060210544600464481L;
private String name;
private int age;
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;
}
}
2)创建Book类,实现Parcelable
public class Book implements Parcelable
{
private String bookName;
private String author;
private int publishDate;
public Book()
{
}
public String getBookName()
{
return bookName;
}
public void setBookName(String bookName)
{
this.bookName = bookName;
}
public String getAuthor()
{
return author;
}
public void setAuthor(String author)
{
this.author = author;
}
public int getPublishDate()
{
return publishDate;
}
public void setPublishDate(int publishDate)
{
this.publishDate = publishDate;
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags)
{
//必须按成员变量声明的顺序封装数据,不然会出现获取数据出错
out.writeString(bookName);
out.writeString(author);
out.writeInt(publishDate);
}
//必须实现Parcelable.Creator接口,否则会在获取Book数据的时候报错。
public static final Parcelable.Creator<Book> CREATOR = new Creator<Book>()//实现Parcelable.Creator接口对象名必须为CREATOR,不然同样会报错上面所提到的错;
//这个接口实现了从Percel容器读取Book数据,并返回Book对象给逻辑层使用
{
@Override
public Book[] newArray(int size)
{
return new Book[size];
}
@Override
public Book createFromParcel(Parcel in)//从Percel容器读取Book数据,并返回Book对象给逻辑层使用
{
return new Book(in);
}
};
//这是一个Book的构造函数,通过Parcel对象构造Book对象。
public Book(Parcel in) {
//在读取Parcel容器里的数据时,必须按成员变量声明的顺序读取数据,不然会出现获取数据出错bookName = in.readString();
author = in.readString();
publishDate = in.readInt(); }
}