I/O简介
流的概念的引入
写Java程序一定会用到下面这一行代码:
System.out.println("hello world");
这行代码可以输出 “hello world” 在控制台上,这行代码有什么可看的呢?
一共三个元素,挨个看:
第一个:System,系统类,是个很有用的类,里面有一些实用的方法,这个后面会专门写一期介绍一些常用的类.
第二个元素:out,这个out是什么东西呢?点进去看,可以看到它在System类里面的定义:
public final static PrintStream out = null;
这个PrintStream就是一个I/O流了.往控制台输出内容就使用到了I/O流.
最后看一下println()方法,继续点进去看,这个方法由很多重载的方法.带参数的方法里面
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
它居然还是个线程同步的方法,可以看到里面有两行代码,newLine()意思很明显就是换行的意思了,着重要看一下这个print()方法,继续点进去
public void print(String s) {
if (s == null) {
s = "null";
}
write(s);
}
就暂时追到这儿,这里调用了输出流的write()方法向控制台输出内容.
那么,什么是流呢?举个栗子,假设现在有两个独立的水池,一空一满,现在需要把一个水池的水转移到另一个水池.如何做呢?
一种方法是找个容器,以这个容器作为水的运输单元进行转移,这里容器大小决定了运水的效率.
于是,聪明人会直接扔个水泵进去,通过水管直接抽水,这里水管大小和水泵功率则决定了运水效率.
因此,Java中的I/O流就相当于这里的水泵和水管.
其中I指input,O指output,即输入输出流.但是其实吧,输入输出其实是个相对的概念.比如,使用Java程序从磁盘读取一个文件到内存,此时对于Java程序来说是输入,但是在磁盘的角度又是输出了.于是,在Java中的输入输出都是相对Java程序来说的.
流的分类
可以从不同角度对I/O流进行分类,如上面,从数据传输方向上可以分为输入输出流.从作用对象则可以分为节点流和处理流.从传输的数据类型上则可以分为字节流和字符流.
顾名思义,字节流就是用来传输字节数据,而字符流用来输出字符数据,不过他们的使用也不绝对,是可以相互转化的.Java中有许多类是专门用来处理IO的,如何区分哪个是字符流哪个是字节流呢?可以很简单的看后缀名就可以.是输出流还是输入流同样也可以看类名,输出流一般会带有output关键字,输出流一般会带有input关键字.而字节流一般会带有stream后缀,字符流的话一般后缀是reader或者writer,reader表示输入流,writer表示输出流.有四个基类:
/**
* I/O流有三个分类
* 从作用对象分为 节点流和处理流
* 从方向上分为 输出流和输入流
* 从传输的数据类型分为 字节流和字符流
* 有以下四个基类
* 字节流 字符流
* 输入流 InputStream Reader
* 输出流 OutputStream Writer
*/
下面主要举栗子来看下I/O流的应用.
文件I/O相关
/**
* @Description
* @Author hyc
* @Date 2022/5/5
*/
public class IOTest {
public static void main(String[] args) {
// 演示文件的输出
// 创建文件/目录
File file = new File("test.txt");
// 创建文件输出流,可以使用字节流,也可以使用字符流,这里使用 FileOutputStream
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
// 输出 "hello world" 到文件
String hello = "helle world";
// 可以看到这里使用了字节流,无法直接输出字符串,需要将字符串转换为字节
fos.write(hello.getBytes());
// 刷新缓存
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 流资源使用完以后要关闭,并处理异常
if (fos != null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
程序运行以后可以找一下这个test.txt文件在哪个地方,因为我这里使用的是maven构建的springboot项目,代码写在test目录下,文件生成的目录可能会有不一致,这里不指定路径,使用默认的就可以,一般在程序根目录下,或者是在src目录下,我这里是在程序根目录下.
接下来读取刚刚创建的文件,将文件的内容输出到控制台上.
public class FileReaderTest {
public static void main(String[] args) {
// 使用文件输入流读入刚才生成的文件
File file = new File("test.txt");
FileReader fileReader = null;
try {
fileReader = new FileReader(file);
// ,使用一个char数组作为缓存,一次读入50个字符
char[] re = new char[50];
// 循环读入
while (fileReader.read(re) > 0){
// 如果读取到内容会返回1,如果文件内容已经读取完,则返回-1
String s = String.valueOf(re);
System.out.println(s);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileReader != null){
try {
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
结合前面给出的总结,可以分析下上面两个例子中使用到的流应该怎么分类呢?
序列化与反序列化
现在有一个问题,如果是字符串我们可以输出到文件进行持久化保存,那么如果有一个对象,比如一个User对象,是否也可以进行像字符串那样输出到文件进行持久化保存呢?答案当然是可以的,只需要将对象序列化,然后保存到文件即可.需要用到时,再读取文件,将对象反序列化,即可恢复原来的对象.下面还是直接上代码,边进行解释和用法.
首先准备需要进行序列化的类,如User类.此类中除了常用的三个属性之外,可以看到还多了一个serialVersionUID,并且还实现了Serializable接口.不一样的地方这就来了.
public class User implements Serializable {
private static final long serialVersionUID = -684979441254667710L;
// 用户名
private String username;
// 账户号
private String userAccount;
// 联系方式
private String tel;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getUserAccount() {
return userAccount;
}
public void setUserAccount(String userAccount) {
this.userAccount = userAccount;
}
public String getTel() {
return tel;
}
public void setTel(String tel) {
this.tel = tel;
}
public User(String username, String userAccount, String tel) {
this.username = username;
this.userAccount = userAccount;
this.tel = tel;
}
public User() {
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", userAccount='" + userAccount + '\'' +
", tel='" + tel + '\'' +
'}';
}
}
先来解释一下Serializable接口,可以点进去看一下这个接口
public interface Serializable {
}
可以发现,这个接口里面除了一大堆的注释之外,就只有一个接口的定义,啥也没有.因此,这个接口仅用来标识此类是可以被序列化的,可以被转化成二进制流.重点要看一下User类里面的这个serialVersionUID字段,除了后面的数字值不一样之外,它的写法是固定的,字段名也固定,可以去String类里面去抄过来,然后随便改一下不和其他类一样就行.
String类也实现了这个接口,里面也有serialVersionUID这个字段,其实这个字段就是用来标识这个类,相当于给这个类给了一个标签,在进行序列化与反序列化的时候可以定位到该类和序列化版本.其实不写这个serialVersionUID字段也可以,Java程序在序列化时会默认分配一个.但是如果在网络中经过多次转化,就有可能发生数据错误,因此最好还是写上.比如两个端点使用的Java版本不一样,在一端进行序列化时另一端反序列化时就有可能因为序列化版本不一致而导致反序列化失败或者数据错误.
下面演示一下对象的序列化与反序列化
public class SerializableTest {
public static void main(String[] args) {
// 对象的序列化与反序列化
User user = new User("zhangsan","testuser01","114118");
// 先将对象序列化,然后存入文件,再反序列化,还原为对象
// 文件名以及后缀名无所谓,能找到就行
File file = new File("filetest.io");
// 文件输出流
FileOutputStream fos = null;
// 对象输出流
ObjectOutputStream oos = null;
try {
fos = new FileOutputStream(file);
oos = new ObjectOutputStream(fos);
// 将对象序列化写入文件
oos.writeObject(user);
oos.flush();
fos.flush();
} catch (Exception e){
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
这里用到了两个流,FileOutputStream 和 ObjectOutputStream ,FileOutputStream 用来输出文件,而ObjectOutputStream 用来将对象序列化到FileOutputStream,ObjectOutputStream 不能直接直接输出对象到文件,需要借助文件输出流.* 这里需要特别注意的是流资源的关闭,这里的关闭是有顺序的,创建的时候要先创建里面的再创建外面的,但是关闭的时候要先关闭外面的,再关闭里面的,顺序是确定的*.
然后再看看读取文件将文件里的内容还原为对象
public class FileIOTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 读取之前的文件,转换为对象,这里直接将异常抛出了.
File file = new File("filetest.io");
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
Object object = ois.readObject();
User user = null;
if (object instanceof User){
user = (User)object;
}
System.out.println(user);
ois.close();
fis.close();
}
}
事实上,不止是文本文件,Java中的File类可以指代任何文件,同样I/O流也可以同样处理任何类型的文件,对于程序而言,所有文件不过是一串二进制罢了,什么类型的文件根本无所谓,字符流字节流也就是读取方式不一样而已.栗子举完了,下面举个花生,看一下I/O流对于文件的简单处理.
public class FileExportTest {
public static void main(String[] args) throws IOException {
File inputFile = new File("C:\\Users\\SC5776\\Videos\\Captures\\picture.png");
FileInputStream fileInputStream = new FileInputStream(inputFile);
File outputFile = new File("C:\\Users\\SC5776\\Videos\\Captures\\picture2.png");
FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
byte[] buffer = new byte[100];
while (fileInputStream.read(buffer) > 0){
fileOutputStream.write(buffer);
fileOutputStream.flush();
}
fileOutputStream.close();
fileInputStream.close();
}
}
这里的代码呢,完整演示了读取一个文件,然后再输出这个文件,相当于是将文件复制粘贴了一份,没什么实际意义,仅仅演示文件读取与输出.关于IO流的简单使用就先这样了.诸如其他转换流,缓冲流等,后面有机会再说.最后再回过来看一下开头引入的问题,是不是感觉就不一样了呢?