几乎所有的程序都离不开信息的输入和输出,比如从键盘读取数据,从文件中获取或者向文件中存入数据,在显示器上显示数据。这些情况下都会涉及有关输入/输出的处理。
在Java中,把这些不同类型的输入、输出源抽象为流(Stream),其中输入或输出的数据称为数据流(Data Stream),用统一的接口来表示。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
数据流是指一组有顺序的、有起点和终点的字节集合。
①按照流的流向分,可以分为输入流和输出流。
注意:这里的输入、输出是针对程序来说的。
输出:把程序(内存)中的内容输出到磁盘、光盘等存储设备中。
输入:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。
②按处理数据单位不同分为字节流和字符流。
字节流:每次读取(写出)一个字节,当传输的资源文件有中文时,就会出现乱码。
字符流:每次读取(写出)两个字节,有中文时,使用该流就可以正确传输显示中文。
1字符 = 2字节; 1字节(byte) = 8位(bit); 一个汉字占两个字节长度,一个英文占1个字节长度。
③按照流的角色划分为节点流和处理流。
节点流:从或向一个特定的地方(节点)读写数据。如FileInputStream。
处理流(包装流):是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接。
编码方式 |
英文字符 |
中文字符 |
GB 2312、GBK |
1 |
2 |
UTF-8 |
1 |
3-4 |
UTF-16 |
2 |
3-4 |
UTF-32 |
4 |
4 |
1.字节流读取的时候,读到一个字节就返回一个字节; 字符流使用了字节流读到一个或多个字节(中文对应的字节数是两个,在UTF-8码表中是3个字节)时。先去查指定的编码表,将查到的字符返回。
2.字节流可以处理所有类型数据,如:图片,MP3,AVI视频文件,而字符流只能处理字符数据。只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流。
下图是Java IO 流的整体架构图:
比如什么时候用输出流?什么时候用字节流?可以根据下面三步选择适合自己的流:
InputStream有read方法,一次读取一个字节,OutputStream的write方法一次写一个int。这两个类都是抽象类。意味着不能创建对象,那么需要找到具体的子类来使用。
InputStream是所有输入字节流的父类,是一个抽象类。ByteArrayInputStream、StringBufferInputStream、FileInputStream 是三种基本的介质流,它们分别从Byte 数组、StringBuffer、和本地文件中读取数据。PipedInputStream是从与其它线程共用的管道中读取数据。
ObjectInputStream 和所有FilterInputStream 的子类都是装饰流(装饰器模式的主角)。
操作流的步骤都是:
案例一:使用 read()方法,一次读取一个字节,读到文件末尾返回-1.
private static void showContent(String path) throws IOException {
// 打开流
FileInputStream fis = new FileInputStream(path);
int len;
while ((len = fis.read()) != -1) {
System.out.print((char) len);
}
// 使用完关闭流
fis.close();
}
案例二:使用read()方法的时候,可以将读到的数据装入到字节数组中,一次性的操作数组,可以提高效率。
private static void showContent2(String path) throws IOException {
// 打开流
FileInputStream fis = new FileInputStream(path);
// 通过流读取内容
byte[] byt = new byte[1024];
int len = fis.read(byt);
for (int i = 0; i
OutputStream 是所有的输出字节流的父类,它是一个抽象类。
ByteArrayOutputStream、FileOutputStream 是两种基本的介质流,它们分别向Byte 数组、和本地文件中写入数据。PipedOutputStream 是向与其它线程共用的管道中写入数据,
ObjectOutputStream 和所有FilterOutputStream 的子类都是装饰流。
OutputStram 的write方法,一次只能写一个字节。成功的向文件中写入了内容。但是并不高效,如何提高效率呢?可以使用缓冲,在OutputStram类中有write(byte[] b)方法,将 b.length个字节从指定的 byte 数组写入此输出流中。
private static void writeTxtFile(String path) throws IOException {
// 1:打开文件输出流,流的目的地是指定的文件
FileOutputStream fos = new FileOutputStream(path,true);
// 2:通过流向文件写数据
byte[] byt = "java".getBytes();
fos.write(byt);
// 3:用完流后关闭流
fos.close();
}
输入输出流综合使用——文件拷贝实现
(1)void flush():刷新此输出流并强制写出所有缓冲的输出字节。为了加快数据传输速度,提高数据输出效率,又是输出数据流会在提交数据之前把所要输出的数据先暂时保存在内存缓冲区中,然后成批进行输出,每次传输过程都以某特定数据长度为单位进行传输,在这种方式下,数据的末尾一般都会有一部分数据由于数量不够一个批次,而存留在缓冲区里,调用 flush() 方法可以将这部分数据强制提交。
(2)缓冲流
Java其实提供了专门的字节流缓冲来提高效率。BufferedInputStream 和 BufferedOutputStream。BufferedOutputStream和BufferedOutputStream类可以通过减少读写次数来提高输入和输出的速度。它们内部有一个缓冲区,用来提高处理效率。查看API文档,发现可以指定缓冲区的大小。其实内部也是封装了字节数组。没有指定缓冲区大小,默认的字节是8192。显然缓冲区输入流和缓冲区输出流要配合使用。首先缓冲区输入流会将读取到的数据读入缓冲区,当缓冲区满时,或者调用flush方法,缓冲输出流会将数据写出。
注意:当然使用缓冲流来进行提高效率时,对于小文件可能看不到性能的提升。但是文件稍微大一些的话,就可以看到实质的性能提升了。示例:
public class Test {
public static void main(String[] args) throws IOException {
String srcPath = "c:\\a.mp3";
String destPath = "d:\\copy.mp3";
copyFile(srcPath, destPath);
}
public static void 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();
}
}
计算机并不区分二进制文件与文本文件。所有的文件都是以二进制形式来存储的,因此,从本质上说,所有的文件都是二进制文件。所以字符流是建立在字节流之上的,它能够提供字符层次的编码和解码。可以说字符流就是:字节流 + 编码表,为了更便于操作文字数据。字符流的抽象基类:Reader , Writer。由这些类派生出来的子类名称都是以其父类名作为子类名的后缀,如FileReader、FileWriter。
Java使用Unicode字符集来表示字符串和字符。为了实现与其他程序语言及不同平台的交互,Java提供一种新的数据流处理方案,称作读者(Reader)和写者(Writer)。
Reader 是所有的输入字符流的父类,它是一个抽象类。
Reader 中各个类的用途和使用方法基本和InputStream 中的类使用一致。
Writer是所有的输出字符流的父类,它是一个抽象类。
下面展示一个字符输入流和字符输出流综合使用的案例:复制文件。
InputStreamReader 是字节流通向字符流的桥梁
OutputStreamWriter 是字符流通向字节流的桥梁
转换流可以将字节转成字符,原因在于,将获取到的字节通过查编码表获取到指定对应字符。 转换流的最强功能就是基于 字节流 + 编码表 。没有转换,没有字符流
PrintWriter 和 PrintStream
注:
A:只操作目的地,不操作数据源
B:可以操作任意类型的数据
C:如果启用了自动刷新,在调用println(),printf(),format()方法的时候,能够换行并刷新
D:可以直接操作文件
把Java对象转换为字节序列的过程称为对象的序列化,也就是将对象写入到IO流中。序列化是为了解决在对对象流进行读写操作时所引发的问题。序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
要对一个对象序列化,这个对象就需要实现Serializable接口,如果这个对象中有一个变量是另一个对象的引用,则引用的对象也要实现Serializable接口,这个过程是递归的。Serializable接口中没有定义任何方法,只是作为一个标记来指示实现该接口的类可以进行序列化。
要实现序列化,只需两步即可:
序列化只能保存对象的非静态成员变量,而不能保存任何成员方法和静态成员变量,并且保存的只是变量的值,变量的修饰符对序列化没有影响。
有一些对象类不具有可持久化性,因为其数据的特性决定了它会经常变化,其状态只是瞬时的,这样的对象是无法保存去状态的,如Thread对象或流对象。对于这样的成员变量,必须用 transient 关键字标明,否则编译器将报错。任何用 transient 关键字标明的成员变量,都不会被保存。
另外,序列化可能涉及将对象存放到磁盘上或在网络上发送数据,这时会产生安全问题。对于一些需要保密的数据(如用户密码等),不应保存在永久介质中,为了保证安全,应在这些变量前加上 transient 关键字。
反序列化就是从 IO 流中恢复对象。
反序列化也只需两步即可完成:
控制台只输出了Person的信息,没有输出构造方法中的内容,说明反序列化的对象是由 JVM 自己生成的,不通过构造方法生成。
我们知道,反序列化必须拥有 class 文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢?
java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常:
序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
什么情况下需要修改serialVersionUID呢?分三种情况。
序列化使用场景
File类是对文件系统中文件以及文件夹进行封装的对象,可以通过对象的思想来操作文件和文件夹。
File类保存文件或目录的各种元数据信息,包括文件名、文件长度、最后修改时间、是否可读、获取当前文件的路径名,判断指定文件是否存在、获得当前目录中的文件列表,创建、删除文件和目录等方法。
我们在对文件的操作过程中,除了使用字节流和字符流的方式之外,我们还可以使用RandomAcessFile这个工具类来实现。
RandomAccessFile可以实现对文件的读 和 写,但是他并不是继承于以上4中基本虚拟类。而且在对文件的操作中RandomAccessFile有一个巨大的优势,他可以支持文件的随机访问,程序快可以直接跳转到文件的任意地方来读写数据。所以如果需要访问文件的部分内容,而不是把文件从头读到尾,使用RandomAccessFile将是更好的选择。
RandomAccessFile的方法虽然多,但它有一个最大的局限,就是只能读写文件,不能读写其他IO节点。RandomAccessFile的一个重要使用场景就是网络请求中的多线程下载及断点续传。
mode中,有4中启动的方式:
"rwd" 打开以便读取和写入,对于 "rw",还要求对文件内容的每个更新都同步写入到底层存储设备
RandomAccessFile使用:
读取文件内容
RandomAccessFile raf = new RandomAccessFile(file,"r");
String s = null;
while ((s = raf.readLine())!=null){
System.out.println(s);
}
raf.close();
写入文件内容
String text = "写入的内容 \n";
RandomAccessFile raf = new RandomAccessFile(file,"rw");
raf.seek(12); //改变写入偏移的位置,从地12个字节的位置开始写入
raf.write(text.getBytes());
raf.close();
注意:RandomAccessFile虽然可以设置了偏移的方法,但他不能实现中间插入的效果,如果你需要实现文本中间插入的话,要先将后面的文件内容拷贝,然后写入,最后在写入的写一行,将拷贝的东西复制回来。
[文章参考]
https://blog.csdn.net/zzy1832/article/details/48154825
https://baijiahao.baidu.com/s?id=1659851047751244423&wfr=spider&for=pc