什么是IO
输入(Input)指的是:可以让程序从外部系统获得数据(核心含义是“读”,读取外部数据)。
输出(Output)指的是:程序输出数据给外部系统从而可以操作外部系统(核心含义是“写”,将数据写出到外部系统)。
java.io包为我们提供了相关的API,实现了对所有外部系统的输入输出操作,这就是我们这章所要学习的技术。
什么是数据源
数据源data source,提供数据的原始媒介。常见的数据源有:数据库、文件、其他程序、内存、网络连接、IO设备。如图所示。
数据源分为:源设备、目标设备。
流的概念
流是一个抽象、动态的概念,是一连串连续动态的数据集合。
对于输入流而言,数据源就像水箱,流(stream)就像水管中流动着的水流,程序就是我们最终的用户。我们通过流(A Stream)将数据源(Source)中的数据(information)输送到程序(Program)中。
对于输出流而言,目标数据源就是目的地(dest),我们通过流(A Stream)将程序(Program)中的数据(information)输送到目的数据源(dest)中。
流与源数据源和目标数据源之间的关系:
Oldlu提示
输入/输出流的划分是相对程序而言的,并不是相对数据源。
当程序需要读取数据源的数据时,就会通过IO流对象开启一个通向数据源的流,通过这个IO流对象的相关方法可以顺序读取数据源中的数据。
使用流读取文件内容(不规范的写法,仅用于测试)
import java.io.*;
public class TestI01 {
public static void main(String[] args) {
try {
//创建输入流
FileInputStream fis = new FileInputStream("d:/a.txt"); // 文件内容是:abc
//一个字节一个字节的读取数据
int s1 = fis.read(); // 打印输入字符a对应的ascii码值97
int s2 = fis.read(); // 打印输入字符b对应的ascii码值98
int s3 = fis.read(); // 打印输入字符c 对应的ascii码值99
int s4 = fis.read(); // 由于文件内容已经读取完毕,返回-1
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
System.out.println(s4);
// 流对象使用完,必须关闭!不然,总占用系统资源,最终会造成系统崩溃!
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
以上案例我们要注意以下几点:
使用流读取文件内容(经典代码,一定要掌握)
import java.io.*;
public class Test2 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("d:/a.txt"); // 内容是:abc
StringBuilder sb = new StringBuilder();
int temp = 0;
//当temp等于-1时,表示已经到了文件结尾,停止读取
while ((temp = fis.read()) != -1) {
sb.append((char) temp);
}
System.out.println(sb);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
//这种写法,保证了即使遇到异常情况,也会关闭流对象。
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
在JDK7以及以后的版本中可以使用try-with-resource语法更优雅的关闭资源。
java.lang.AutoCloseable接口:
在java.lang.AutoCloseable接口中包含了一个close方法,该方法用于关闭资源。
只要是实现了java.lang.AutoCloseable接口的对象,都可以使用try-with-resource关闭资源。
使用最新的try-with-resource简化(经典代码,一定要掌握)
public class Test3 {
public static void main(String[] args) {
//使用try-with-resource方式关闭资源。
//在try中打开资源,不需要在代码中添加finally块关闭资源。
try(FileInputStream fis = new FileInputStream("d:/a.txt");){
StringBuilder sb = new StringBuilder();
int temp=0;
while((temp = fis.read()) != -1){
sb.append((char) temp);
}
System.out.println(sb);
}catch(Exception e){
e.printStackTrace();
}
}
Oldlu建议
如上代码是一段非常典型的IO流代码,其他流对象的使用也基本是同样的模式!
按流的方向分类:
按处理的数据单元分类:
按处理对象不同分类:
节点流处于IO操作的第一线,所有操作必须通过它们进行;处理流可以对节点流进行包装,提高性能或提高程序的灵活性。
从上图发现,很多流都是成对出现的,比如:FileInputStream/FileOutputStream,显然是对文件做输入和输出操作的。我们下面简单做个总结:
InputStream/OutputStream
字节流的抽象类。
Reader/Writer
字符流的抽象类。
FileInputStream/FileOutputStream
节点流:以字节为单位直接操作“文件”。
ByteArrayInputStream/ByteArrayOutputStream
节点流:以字节为单位直接操作“字节数组对象”。
ObjectInputStream/ObjectOutputStream
处理流:以字节为单位直接操作“对象”。
DataInputStream/DataOutputStream
处理流:以字节为单位直接操作“基本数据类型与字符串类型”。
FileReader/FileWriter
节点流:以字符为单位直接操作“文本文件”(注意:只能读写文本文件)。
BufferedReader/BufferedWriter
处理流:将Reader/Writer对象进行包装,增加缓存功能,提高读写效率。
BufferedInputStream/BufferedOutputStream
处理流:将InputStream/OutputStream对象进行包装,增加缓存功能,提高读写效率
InputStreamReader/OutputStreamWriter
处理流:将字节流对象转化成字符流对象。
PrintStream
处理流:将OutputStream进行包装,可以方便地输出字符,更加灵活。
Oldlu建议
上面的解释,一句话就点中了流的核心作用。大家在后面学习的时候,用心体会。
InputStream/OutputStream和Reader/writer类是所有IO流类的抽象父类,我们有必要简单了解一下这个四个抽象类的作用。然后,通过它们具体的子类熟悉相关的用法。
InputStream
此抽象类是表示字节输入流的所有类的父类。InputSteam是一个抽象类,它不可以实例化。 数据的读取需要由它的子类来实现。根据节点的不同,它派生了不同的节点流子类 。
继承自InputSteam的流都是用于向程序中输入数据,且数据的单位为字节(8 bit)。
常用方法:
方法名 | 使用说明 |
---|---|
int read() | 读取一个字节的数据,并将字节的值作为int类型返回(0-255之间的一个值)。如果未读出字节则返回-1(返回值为-1表示读取结束) |
void close() | 关闭输入流对象,释放相关系统资源 |
OutputStream
此抽象类是表示字节输出流的所有类的父类。输出流接收输出字节并将这些字节发送到某个目的地。
常用方法:
方法名 | 使用说明 |
---|---|
void write(int n) | 向目的地中写入一个字节 |
void close() | 关闭输出流对象,释放相关系统资源 |
Reader
Reader用于读取的字符流抽象类,数据单位为字符。
方法名 | 使用说明 |
---|---|
int read() | 读取一个字符的数据,并将字符的值作为int类型返回(0-65535之间的一个值,即Unicode值)。如果未读出字符则返回-1(返回值为-1表示读取结束) |
void close() | 关闭流对象,释放相关系统资源 |
Writer
Writer用于输出的字符流抽象类,数据单位为字符。
方法名 | 使用说明 |
---|---|
void write(int n) | 向输出流中写入一个字符 |
void close() | 关闭输出流对象,释放相关系统资源 |
https://www.itbaizhan.com/course/id/39500.html
FileInputStream通过字节的方式读取文件,适合读取所有类型的文件(图像、视频、文本文件等)。
FileOutputStream 通过字节的方式写数据到文件中,适合所有类型的文件(图像、视频、文本文件等)。
FileInputStream文件输入字节流
public class TestFileInputStream {
public static void main(String[] args) {
//使用try-with-resource方式关闭资源。
//在try中打开资源,不需要在代码中添加finally块关闭资源。
//后开先关,栈顺序,先创建的流后关闭
try(FileInputStream fis = new FileInputStream("d:/a.txt");){
StringBuilder sb = new StringBuilder();
int temp=0;
while((temp = fis.read()) != -1){
sb.append((char) temp);
}
System.out.println(sb);
}catch(Exception e){
e.printStackTrace();
}
}
FileOutputStream文件输出字节流
public class TestFileOutputStream {
public static void main(String[] args) {
String str = "Old Lu";
// true表示内容会追加到文件末尾;false表示重写整个文件内容。
try(FileOutputStream fos = new FileOutputStream("d:/a.txt",true)){
//将整个字节数组写入到文件中。
fos.write(str.getBytes());
//将数据从内存中写入到磁盘中。
fos.flush();
}catch (IOException e){
e.printStackTrace();
}
}
}
通过创建一个指定长度的字节数组作为缓冲区,以此来提高IO流的读写效率。该方式适用于读取较大文件时的缓冲区定义。注意:缓冲区的长度一定是2的整数幂。一般情况下1024长度较为合适。
public class TestFileByteBuffer{
public static void main(String[] args) {
long time1 = System.currentTimeMillis();
copyFile("d:/1.jpg", "d:/2.jpg");
long time2 = System.currentTimeMillis();
System.out.println(time2 - time1);
}
/**
*
* @param src 源文件
* @param desc 目标文件
*/
public static void copyFile(String src,String desc){
//“后开的先关闭!”按照他们被创建顺序的逆序来关闭
try(FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(desc)){
//创建一个缓冲区,提高读写效率
byte[] buffer = new byte[1024];
int temp = 0;
while ((temp = fis.read(buffer)) != -1){
//将缓存数组中的数据写入文件中,注意:写入的是读取的真实长度;
//temp是读取字节的数量
fos.write(buffer,0,temp);
}
//将数据从内存中写入到磁盘中。
fos.flush();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
注意 在使用字节缓冲区时,我们需要注意:
获取程序运行时间
long time1=System.currentTimeMillis();
copyFile("d:/1.jpg","d:2.jpg");
long time2=System.currentTimeMillis();
System.pringln(time1-time2);
//加入字节缓冲区可以提高200倍左右的速度
Java缓冲流本身并不具有IO流的读取与写入功能,只是在别的流(节点流或其他处理流)上加上缓冲功能提高效率,就像是把别的流包装起来一样,因此缓冲流是一种处理流(包装流)。
BufferedInputStream和BufferedOutputStream这两个流是缓冲字节流,通过内部缓存数组来提高操作流的效率。
使用缓冲流实现文件的高效率复制
下面我们通过两种方式(普通文件字节流与缓冲文件字节流)实现一个文件的复制,来体会一下缓冲流的好处。
public class TestFileBufferStream {
public static void main(String[] args) {
long time1 = System.currentTimeMillis();
copyFile("d:/1.jpg","d:/2.jpg");
long time2 = System.currentTimeMillis();
System.out.println(time2 - time1);
}
public static void copyFile(String source,String destination){
//实例化节点流
try(FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(destination);
//实例化处理流
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos)){
int temp = 0;
while ((temp = bis.read()) != -1){
bos.write(temp);
}
bos.flush();
}catch(IOException e){
e.printStackTrace();
}
}
}
注意
前面介绍的文件字节流可以处理所有的文件,如果我们处理的是文本文件,也可以使用文件字符流,它以字符为单位进行操作。
文件字符输入流
public class TestFileReader {
public static void main(String[] args) {
//创建文件字符输入流对象
try(FileReader fr = new FileReader("d:/a.txt")){
StringBuilder sb = new StringBuilder();
//读取文件
int temp = 0;
while((temp = fr.read()) != -1){
sb.append((char)temp);
}
System.out.println(sb);
}catch (IOException e){
e.printStackTrace();
}
}
}
文件字符输出流
public class TestFileWriter {
public static void main(String[] args) {
//创建文件字符输出流对象
try(FileWriter fw = new FileWriter("d:/aa.txt")){
fw.write("您好尚学堂\r\n");
fw.write("您好Old Lu\r\n");
fw.flush();
}catch (IOException e){
e.printStackTrace();
}
}
}
https://www.itbaizhan.com/course/id/39504.html
BufferedReader/BufferedWriter增加了缓存机制,大大提高了读写文本文件的效率。
字符输入缓冲流
BufferedReader是针对字符输入流的缓冲流对象,提供了更方便的按行读取的方法:readLine(); 在使用字符流读取文本文件时,我们可以使用该方法以行为单位进行读取。
public class TestBufferedReader {
public static void main(String[] args) {
//创建文件字符输入流对象
try(FileReader fr = new FileReader("d:/aa.txt");
//创建字符缓冲处理流。缓冲区默认大小为8192个字符。
BufferedReader br = new BufferedReader(fr)){
//操作流
String temp = "";
//readLine():读取一行文本。
while((temp = br.readLine()) != null){
System.out.println(temp);
}
}catch(IOException e){
e.printStackTrace();
}
}
}
注意
public class TestLineNumber {
public static void main(String[] args) {
//创建字符输入缓冲流与文件字符输入流
try(BufferedReader br = new BufferedReader(new FileReader("d:/sxt.txt"));
//创建字符输出缓冲流与文件字符输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("d:/sxt2.txt"))){
String temp ="";
//定义序号变量
int i = 1;
while((temp = br.readLine()) != null){
//将读取到的内容添加序号,并输出到指定文件中。
bw.write(i+","+temp);
//换行处理
bw.newLine();
//序号变量累加
i++;
}
//刷新
bw.flush();
}catch(IOException e){
e.printStackTrace();
}
}
}
InputStreamReader/OutputStreamWriter用来实现将字节流转化成字符流。
通过转换流解决乱码
ANSI(American National Standards Institute)美国国家标准协会
public class TestInputStreamReader {
public static void main(String[] args) {
//创建文件字节输入流对象
try(FileInputStream fis = new FileInputStream("d:/sxt.txt");
//创建转换流(字节到字符的转换)流对象,并在该对象中指定编码。
InputStreamReader isr = new InputStreamReader(fis,"gbk")){
StringBuilder sb = new StringBuilder();
//操作流对象
int temp = 0;
while((temp = isr.read()) != -1){
sb.append((char) temp);
}
System.out.println(sb);
}catch(IOException e){
e.printStackTrace();
}
}
}
public class TestLineNumber2 {
public static void main(String[] args) {
//创建字符输入缓冲流、输入字节到字符转换流、文件字节输入流对象
try(BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("d:/sxt.txt")));
//创建字符输出缓冲流、输出字符到字节转换流、文件字节输出流对象
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("d:/sxt4.txt")))){
//操作流
String temp = "";
//序号变量
int i = 1;
//按照行读取
while((temp = br.readLine()) != null){
bw.write(i+","+temp);
//换行
bw.newLine();
//序号累加
i++;
}
//刷新
bw.flush();
}catch(IOException e){
e.printStackTrace();
}
}
}
通过转换流实现键盘输入屏幕输出
System.in是字节流对象,代表键盘的输入。
System.out是字节流对象,代表输出到屏幕。
public class TestKeyboardInput {
public static void main(String[] args) {
//创建键盘输入相关流对象
try(BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
//创建向屏幕输出相关流对象
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out))){
while(true){
bw.write("请输入:");
bw.flush();
//获取键盘输入的字符串
String input = br.readLine();
//判断输入的内容是否含有退出关键字。
if("exit".equals(input) || "quit".equals(input)){
bw.write("Bye Bye !");
bw.flush();
break;
}
//将读取到键盘输入的字符串,输出到屏幕。
bw.write("您输入的是:"+input);
bw.newLine();
bw.flush();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
import java.io.*;
public class TestConvertStream {
public static void main(String[] args) {
// 创建字符输入和输出流:使用转换流将字节流转换成字符流
BufferedReader br = null;
BufferedWriter bw = null;
try {
br = new BufferedReader(new InputStreamReader(System.in));
bw = new BufferedWriter(new OutputStreamWriter(System.out));
// 使用字符输入和输出流
String str = br.readLine();
// 一直读取,直到用户输入了exit为止
while (!"exit".equals(str)) {
// 写到控制台
bw.write(str);
bw.newLine();// 写一行后换行
bw.flush();// 手动刷新
// 再读一行
str = br.readLine();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭字符输入和输出流
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bw != null) {
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
在Java的IO流中专门提供了用于字符输出的流对象PrintWriter。该对象具有自动行刷新缓冲字符输出流,特点是可以按行写出字符串,并且可通过println();方法实现自动换行。
public class TestPrintWriter {
public static void main(String[] args) {
//创建字符输出流对象
try(PrintWriter pw = new PrintWriter("d:/sxt5.txt")){
//调用不带换行方法完成内容的输出
pw.print("abc");
pw.print("def");
//调用带有自动换行方法完成内容的输出
pw.println("Oldlu");
pw.println("sxt");
pw.flush();
}catch(IOException e){
e.printStackTrace();
}
}
}
public class TestLineNumber3 {
public static void main(String[] args) {
//创建字符输入缓冲流对象与文件字符输入流对象
try(BufferedReader br = new BufferedReader(new FileReader("d:/sxt.txt"));
//创建字符输出流对象
PrintWriter pw = new PrintWriter("d:/sxt6.txt")){
//操作流
String temp = "";
//定义序号变量
int i = 1;
while((temp = br.readLine()) != null){
pw.println(i+","+temp);
//序号累加
i++;
}
//刷新
pw.flush();
}catch(IOException e){
e.printStackTrace();
}
}
}
数据流将“基本数据类型与字符串类型”作为数据源,从而允许程序以与机器无关的方式从底层输入输出流中操作Java基本数据类型与字符串类型。
DataInputStream和DataOutputStream提供了可以存取与机器无关的所有Java基础类型数据(如:int、double、String等)的方法。
public class TestDataStream {
public static void main(String[] args) {
//创建数据输出流对象与文件字节输出流对象
try(DataOutputStream dos = new DataOutputStream(new FileOutputStream("d:/data"));
//创建数据输入流对象与文件字节输入流对象
DataInputStream dis = new DataInputStream(new FileInputStream("d:/data"))){
//将如下数据写入到文件中
dos.writeChar('a');
dos.writeInt(10);
dos.writeDouble(Math.random());
dos.writeBoolean(true);
dos.writeUTF("北京尚学堂");
//手动刷新缓冲区:将流中数据写入到文件中
dos.flush();
//直接读取数据:读取的顺序要与写入的顺序一致,否则不能正确读取数据。
System.out.println("char: " + dis.readChar());
System.out.println("int: " + dis.readInt());
System.out.println("double: " + dis.readDouble());
System.out.println("boolean: " + dis.readBoolean());
System.out.println("String: " + dis.readUTF());
}catch(IOException e){
e.printStackTrace();
}
}
}
Oldlu提示:
使用数据流时,读取的顺序一定要与写入的顺序一致,否则不能正确读取数据。
我们前边学到的数据流只能实现对基本数据类型和字符串类型的读写,并不能读取对象(字符串除外),如果要对某个对象进行读写操作,我们需要学习一对新的处理流:ObjectInputStream/ObjectOutputStream。
处理基本数据类型数据
ObjectInputStream/ObjectOutputStream处理基本数据类型。
ipublic class TestObjectStreamBasicType {
public static void main(String[] args) {
//创建对象输出流对象与文件字节输出流对象
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/data2"));
//创建对象输入流对象与文件字节输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/data2"))){
//将如下数据写入到文件中
oos.writeInt(10);
oos.writeDouble(Math.random());
oos.writeChar('a');
oos.writeBoolean(true);
oos.writeUTF("你好Oldlu");
oos.flush();
//必须要按照写入的顺序读取数据
System.out.println("int: "+ois.readInt());
System.out.println("double: "+ois.readDouble());
System.out.println("char: "+ois.readChar());
System.out.println("boolean: "+ois.readBoolean());
System.out.println("String: "+ois.readUTF());
}catch(IOException e){
e.printStackTrace();
}
}
}
注意
序列化和反序列化是什么
当两个进程远程通信时,彼此可以发送各种类型的数据。 无论是何种类型的数据,都会以二进制序列的形式在网络上传送。比如,我们可以通过http协议发送字符串信息;我们也可以在网络上直接发送Java对象。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象才能正常读取。
把Java对象转换为字节序列的过程称为对象的序列化。把字节序列恢复为Java对象的过程称为对象的反序列化。
序列化涉及的类和接口
ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
只有实现了Serializable接口的类的对象才能被序列化。 Serializable接口是一个空接口,只起到标记作用。
ObjectOutputStream可以将一个内存中的Java对象通过序列化的方式写入到磁盘的文件中。被序列化的对象必须要实现Serializable序列化接口,否则会抛出异常。
创建对象
public class Users implements Serializable {
private int userid;
private String username;
private String userage;
public Users(int userid, String username, String userage) {
this.userid = userid;
this.username = username;
this.userage = userage;
}
public Users() {
}
public int getUserid() {
return userid;
}
public void setUserid(int userid) {
this.userid = userid;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getUserage() {
return userage;
}
public void setUserage(String userage) {
this.userage = userage;
}
@Override
public String toString() {
return "Users{" +
"userid=" + userid +
", username='" + username + '\'' +
", userage='" + userage + '\'' +
'}';
}
序列化对象
public class TestObjectOutputStream {
public static void main(String[] args) {
//创建对象输出字节流与文件输出字节流对象
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/data3"))){
//创建Users对象
Users users = new Users(1,"Oldlu","18");
//将对象序列化到文件中
oos.writeObject(users);
//刷新
oos.flush();
}catch(IOException e){
e.printStackTrace();
}
}
}