一、Java流式输入/输出原理
在Java程序中,对于数据的输入/输出操作以“流”(stream)方式进行;JDK提供了各种各样的“流”类,用以获取不同种类的数据。程序通过标准的方法输入或输出数据。
原理:
流是用来读写数据的,这里以文件为例,在Java中有个File类,它封装的是文件的名称,是内存里面的一个对象,真正的文件是硬盘上的一块空间,在这个空间里存储者着各种数据,如果我们要读取文件中的数据,就是通过流的方式。我们知道计算机存储各种类型的数据采用的是二进制010101这样的形式,那么程序是如何通过流的方式读取这些数据的呢?
举个生活中的例子,想象一下我们是如何用一根管道从桶中取水的,我们将管道插到水桶上,然后在管道的另一边打开水龙头,水会从管道中流出来,我们把这根管道称为流,而Java里面的流式输入/输出跟这个原理是一样的。假如,我们要从文件中读取数据时,把一根管道插进文件里,然后文件里面的数据就会流进管道,在管道另一边我们就可以读取文件流出来的各种数据了,当你要往文件写入数据时,也是通过管道让要写入的数据流进文件中。除了读写文件以外,还可以通过网络的形式,刚才我们把文件个流比喻成桶和管道,这里网络我们可以想象成家里的自来水管流出来的水,原理是类似的(只不过我们不能往自来水里面灌水,而网络不仅能读取还能写入)。
我们知道计算机的世界是二进制的世界,数据的存储和传输都是通过二进制的形式,因此从文件中读取的数据都是10101,看不懂怎么办?这里就像桶中流出的水有杂质一样,要去除这些杂志 ,我们可以在管道上添加一层过滤层,把杂志过滤掉,同理,我们可以在读取文件的管道上在套一层更加强大的管道,将二进制转换成字符串,这样我们就可以看懂这些字符串了。
二、流的分类
Java中可以从不同的角度对流进行分类:
根据数据流的方向
- 输入流:程序可以从中读取数据的流。
- 输出流:程序能向其中写入数据的流。
- 字节流:是最原始的流,读取出来的是10101这种底层的数据,它是按字节来读取的(1字节8bit)。
- 字符流:一个字符一个字符的读取数据,一个字符是2个字节。
- 节点流:用于直接操作目标设备的流。
- 处理流:是对一个已存在的流封装,通过对数据进行处理为程序提供灵活读写的功能。
JDK所提供的所有流类型位于包Java.io内都分别继承以下四种抽象类。
这里我们如何判定流式输入流还是输出流的?记住一点,输入流和输出流是站在程序的角度来说的,就是说流入程序的是输入流,流出程序的是输出流。
三、节点流和处理流
注意,不管外面包裹多少层处理流,其最终都是建立在节点流之上的。
1.节点流类型
节点流是直接插到数据源上面的,直接读取数据源里面的数据,或者直接写入数据源。
2.处理流
处理流是包在别的流上面的流,提供各种便捷功能的流。
四、InputStream(输入流)
继承自InputStream的流都是用于向程序输入数据,且数据是以字节为单位。
凡是以InputStream结尾的管道,都是以字节形式读取数据。
基本方法:
五、OutputStream(输出流)
继承自OutputStream的流是用于程序中输出数据的,且数据是以字节为单位。
基本方法:
六、Reader流
继承自Reader的流都是用于向程序中输入数据,且数据以字符为单位(2字节)
基本方法:
七、Writer流
继承自Writer的流都是用于程序中输出数据的,且数据以字符为单位(2字节)。
基本方法:
八、节点流讲解
示例:使用FileInputStream流来读取文件内容
package test;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class TestFileInputStream {
public static void main(String[] args) {
FileInputStream fis = null;
try{
//获取文件对象
File file = new File("D:/tts9/workspace/test/src/test/TestFileInputStream.java");
//创建文件输入流
fis = new FileInputStream(file);
//记录字节总数
int total = 0;
//使用变量len来装调用read()方法时返回的整数,没有读取到返回数据返回-1
int len = -1;
//读取缓存大小
byte[] b = new byte[1024];
while((len=fis.read(b))!=-1){
for(byte b1 : b){
System.out.print((char)b1);
}
total +=len;
}
System.out.println("\r文件字节数:"+total);
}catch(Exception e){
e.printStackTrace();
}finally {
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
示例:使用FileOutputStream流往一个文件里面写入数据。
package test;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class TestFileInputStream {
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try{
//输入文件对象
File fileIn = new File("D:/tts9/workspace/test/src/test/TestFileInputStream.java");
//输出文件对象
File fileOut = new File("D:/NewTestFileInputStream.java");
//如果文件不存在,新建文件
if(!fileOut.exists()){
fileOut.createNewFile();
}
//创建文件输入流
fis = new FileInputStream(fileIn);
//创建文件输出流
fos = new FileOutputStream(fileOut);
//记录字节总数
int total = 0;
//使用变量len来装调用read()方法时返回的整数,没有读取到返回数据返回-1
int len = -1;
//读取缓存大小
byte[] b = new byte[1024];
while((len=fis.read(b))!=-1){
fos.write(b);
total +=len;
}
System.out.println("文件复制完毕");
System.out.println("文件字节数:"+total);
}catch(Exception e){
e.printStackTrace();
}finally {
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
查看D盘,D:/NewTestFileInputStream.java已经生成了。
FileInputStream和FileOutputStream这两个流都是字节流,都是以一个字节为单位进行输入和输出的。所以对于占用2个字节存储空间的字符来说读取出来时就会显示成乱码。
示例:使用FileWriter向文件中写入数据
package test;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class TestFileInputStream {
public static void main(String[] args) {
FileReader fr = null;
FileWriter fw = null;
try{
//输入文件对象
File fileIn = new File("D:/tts9/workspace/test/src/test/TestFileInputStream.java");
//输出文件对象
File fileOut = new File("D:/NewTestFileInputStream.java");
//如果文件不存在,新建文件
if(!fileOut.exists()){
fileOut.createNewFile();
}
//创建文件输入流
fr = new FileReader(fileIn);
//创建文件输出流
fw = new FileWriter(fileOut);
//记录字节总数
int total = 0;
//使用变量len来装调用read()方法时返回的整数,没有读取到返回数据返回-1
int len = -1;
//读取缓存大小
char[] c = new char[1024];
while((len=fr.read(c))!=-1){
fw.write(c);
total +=len;
}
System.out.println("文件复制完毕");
System.out.println("文件字符数:"+total);
}catch(Exception e){
e.printStackTrace();
}finally {
if(fw!=null){
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fr!=null){
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
FileReader和FileWriter这两个流都是字符流,都是以一个字符为单位进行输入和输出的。所以读取和写入占用2个字节的字符时都可以正常地显示出来,以上是以File(文件)这个类型为例对节点流进行了讲解,所谓的节点流指定就是直接把输入流或输出插入到数据源上,直接往数据源里面写入数据或读取数据。
九、处理流讲解
1.缓冲流(Buffering)
带有缓冲区的,缓冲区(Buffer)就是内存里面的一小块区域,读写数据时都是先把数据放到这块缓冲区域里面,减少io对硬盘的访问次数,保护我们的硬盘。可以把缓冲区想象成一个小桶,把要读写的数据想象成水,每次读取数据或者是写入数据之前,都是先把数据装到这个桶里面,装满了以后再做处理。这就是所谓的缓冲。先把数据放置到缓冲区上,等到缓冲区满了以后,再一次把缓冲区里面的数据写入到硬盘上或者读取出来,这样可以有效地减少对硬盘的访问次数,有利于保护我们的硬盘。
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
public class TestBufferStream {
public static void main(String[] args) {
FileReader fr = null;
FileWriter fw = null;
BufferedReader br = null;
BufferedWriter bw = null;
try{
//获取输入流
fr = new FileReader(new File("D:/tts9/workspace/test/src/test/TestBufferStream.java"));
//获取输出流
fw = new FileWriter(new File("D:/NewTestBufferStream.java"));
//获取缓冲输入流
br = new BufferedReader(fr);
//获取缓冲输出流
bw = new BufferedWriter(fw);
String s = null;
/*
* 在字符缓冲中提供了一个新的读取方法:readLine(),这个方法可以一次性读取一行,当读取结尾时,返回null
*/
while((s=br.readLine())!=null){
System.out.println(s);
bw.write(s);
}
System.out.println("文件复制完毕");
}catch(Exception e){
e.printStackTrace();
}
}
}
转换流非常的有用,它可以把一个字节流转换成一个字符流,转换流有两种,一种叫InputStreamReader,另一种叫OutputStreamWriter。InputStream是字节流,Reader是字符流,InputStreamReader就是把InputStream转换成Reader。OutputStream是字节流,Writer是字符流,OutputStreamWriter就是把OutputStream转换成Writer。把OutputStream转换成Writer之后就可以一个字符一个字符地通过管道写入数据了,而且还可以写入字符串。我们如果用一个FileOutputStream流往文件里面写东西,得要一个字节一个字节地写进去,但是如果我们在FileOutputStream流上面套上一个字符转换流,那我们就可以一个字符串一个字符串地写进去。
public class TestTransform1 {
public static void main(String args[]) {
try {
OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("D:/java/char.txt"));
osw.write("MircosoftsunIBMOracleApplet");// 把字符串写入到指定的文件中去
System.out.println(osw.getEncoding());// 使用getEncoding()方法取得当前系统的默认字符编码
osw.close();
osw = new OutputStreamWriter(new FileOutputStream(
"D:\\java\\char.txt", true), "ISO8859_1");
// 如果在调用FileOutputStream的构造方法时没有加入true,那么新加入的字符串就会替换掉原来写入的字符串,在调用构造方法时指定了字符的编码
osw.write("MircosoftsunIBMOracleApplet");// 再次向指定的文件写入字符串,新写入的字符串加入到原来字符串的后面
System.out.println(osw.getEncoding());
osw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
import java.io.*;public class TestTransform2{ public static void main(String args[]){ try{ InputStreamReader isr = new InputStreamReader(System.in); //System.in这里的in是一个标准的输入流,用来接收从键盘输入的数据 BufferedReader br = new BufferedReader(isr); String s = null; s = br.readLine();//使用readLine()方法把读取到的一行字符串保存到字符串变量s中去 while(s != null){ System.out.println(s.toUpperCase());//把保存在内存s中的字符串打印出来 s = br.readLine();//在循环体内继续接收从键盘的输入 if(s.equalsIgnoreCase("exit")){ //只要输入exit循环就结束,就会退出 break; } } }catch(Exception e){ e.printStackTrace(); } }}
3.数据流
DataInputStream和DataOutputStream分别继承自InputStream和OutputStream,属于处理流,提供了存取与机器无关的Java基本数据类型。
package test;通过bais这个流往外读取数据的时候,是一个字节一个字节地往外读取的,因此读出来的数据无法判断是字符串还是bool类型的值,因此要在它的外面再套一个流,通过dataInputStream把读出来的数据转换就可以判断了。注意了:读取数据的时候是先写进去的就先读出来,因此读ByteArray字节数组数据的顺序应该是先把占8个字节的double类型的数读出来,然后再读那个只占一个字节的boolean类型的数,因为double类型的数是先写进数组里面的,读的时候也要先读它。这就是所谓的先写的要先读。如果先读Boolean类型的那个数,那么读出来的情况可能就是把double类型数的8个字节里面的一个字节读了出来。
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public class TestDataStream {
public static void main(String[] args) {
ByteArrayInputStream bais = null;
ByteArrayOutputStream baos = null;
DataInputStream dis = null;
DataOutputStream dos = null;
try{
//在调用构造方法时,首先会在内存里面创建一个ByteArray字节数组
baos = new ByteArrayOutputStream();
//在输出流的外面套上一层数据流,用来处理int,double类型的数
dos = new DataOutputStream(baos);
dos.writeInt(10);
dos.writeBoolean(true);
bais = new ByteArrayInputStream(baos.toByteArray());
dis = new DataInputStream(bais);
/*
* 先写进去的就先读出来,调用readInt()方法读取出写入的随机数
* 后写进去的就后读出来,这里面的读取顺序不能更改位置,否则会打印出不正确的结果
*/
System.out.println(dis.readInt());
System.out.println(dis.readBoolean());
}catch(Exception e){
e.printStackTrace();
}finally{
if(dis!=null){
try {
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(dos!=null){
try {
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4.打印流——print
/*这个小程序是重新设置打印输出的窗口,
* 把默认在命令行窗口输出打印内容设置成其他指定的打印显示窗口
*/
import java.io.*;
public class TestPrintStream{
public static void main(String args[]){
PrintStream ps = null;
try{
FileOutputStream fos = new FileOutputStream("D:/java/log.txt");
ps = new PrintStream(fos);//在输出流的外面套接一层打印流,用来控制打印输出
if(ps != null){
System.setOut(ps);//这里调用setOut()方法改变了输出窗口,以前写System.out.print()默认的输出窗口就是命令行窗口.
//但现在使用System.setOut(ps)将打印输出窗口改成了由ps指定的文件里面,通过这样设置以后,打印输出时都会在指定的文件内打印输出
//在这里将打印输出窗口设置到了log.txt这个文件里面,所以打印出来的内容会在log.txt这个文件里面看到
}
for(char c=0;c<=60000;c++){
System.out.print(c+"\t");//把世界各国的文字打印到log.txt这个文件中去
}
}catch(Exception e){
e.printStackTrace();
}
}
}
package cn.gacl.test;
import java.io.*;
public class TestObjectIo {
public static void main(String args[]) {
T t = new T();
t.k = 8;// 把k的值修改为8
try {
FileOutputStream fos = new FileOutputStream(
"D:/java/TestObjectIo.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
// ObjectOutputStream流专门用来处理Object的,在fos流的外面套接ObjectOutputStream流就可以直接把一个Object写进去
oos.writeObject(t);// 直接把一个t对象写入到指定的文件里面
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream(
"D:/java/TestObjectIo.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
// ObjectInputStream专门用来读一个Object的
T tRead = (T) ois.readObject();
// 直接把文件里面的内容全部读取出来然后分解成一个Object对象,并使用强制转换成指定类型T
System.out.print(tRead.i + "\t" + tRead.j + "\t" + tRead.d + "\t"
+ tRead.k);
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
* 凡是要将一个类的对象序列化成一个字节流就必须实现Serializable接口
* Serializable接口中没有定义方法,Serializable接口是一个标记性接口,用来给类作标记,只是起到一个标记作用。
* 这个标记是给编译器看的,编译器看到这个标记之后就可以知道这个类可以被序列化 如果想把某个类的对象序列化,就必须得实现Serializable接口
*/
class T implements Serializable {
// Serializable的意思是可以被序列化的
int i = 10;
int j = 9;
double d = 2.3;
int k = 15;
// transient int k = 15;
// 在声明变量时如果加上transient关键字,那么这个变量就会被当作是透明的,即不存在。
}
直接实现Serializable接口的类是JDK自动把这个类的对象序列化,而如果实现public interface Externalizable extends Serializable的类则可以自己控制对象的序列化,建议能让JDK自己控制序列化的就不要让自己去控制。