likes
comments
collection
share

Java IO(上篇——输入输出流、对象序列化)

作者站长头像
站长
· 阅读数 11

在java中,将数据从外部读入到内存中的操作称为输入流,而将数据从内存中写出到外部的操作称为输出流。数据存储格式及使用方法的角度来看,又可以将流分为字节流字符流

字符流

在Java中,它内部使用的编码方式为UTF-16,而UTF-16又是Unicode的编码方案。不过这仅仅是它内部的编码方案,不代表我们在做字符流输入输出的时候要使用此种方式。现在普遍使用的编码为UTF-8,它具体的编码方式本篇不做介绍。由于字符流的存储和读取需要依赖相应的编码方式,所以一定要为其指定对应的编码,以免造成因为编码方式不同导致的乱码

所有的字符流都继承了ReaderWriter,前者是输入流,后者是输出流。我们往往使用他们的子类。

FileInputStream inputStream = new FileInputStream("/Users/c/Desktop/ceshi.txt");
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
char[] charBuff = new char[20];
int len;
while ((len = reader.read(charBuff))!=-1){
    System.out.println(new String(charBuff,0,len));
}

输出:阳光男孩小丑熊

代码解读:示例中第一行代码将文本输入到字节流中,第二行的代码将这个字节流转换为UTF-8编码的字符流,后面剩下的代码则是将这些字符转为字符串的形式打印。

上面的代码演示了如何读取文件的字节流并将其按指定编码转为字符流,下面的示例示范如何将字符写入到文件中。

FileOutputStream outputStream = new FileOutputStream("/Users/c/Desktop/ceshi.txt");
OutputStreamWriter streamWriter = new OutputStreamWriter(outputStream,StandardCharsets.UTF_8);
PrintWriter writer = new PrintWriter(streamWriter,true);
writer.println("太阳光了");

上面第一行代码将文件读入到字节输入流,第二行将这个输入流转为指定字符编码的字符流,第三行创建一个写出器指定为自动冲刷(为了将缓冲区所有剩余数据都冲刷到目的地),最后成功写入。

上面两个示例采用组合流的方式读写数据,先是处理字节,再将字节转为字符,最后输出。过程中已经囊括了字节和字符的输入输出。在日常开发中,字节流常用来处理图片、视频、大文本等对效率有要求的操作。

字节流

字节流的处理往往比字符流要高效,因为他传输数据时并不需要太繁琐的编码。

RandomAccessFile

这个类可以在文件中的任意位置查询、写入数据,他实现了DataInput和DataOutput接口,这两个接口是专门用于二进制读写的接口。 这个类的强大之处在于他有一个指针可以移动到文件的任何位置,如果想要读写哪里的数据,只需要移动指针就可以了。

//第一个参数表示文件路径,第二个参数为操作模式
public RandomAccessFile(String name, String mode)
    throws FileNotFoundException
{
    this(name != null ? new File(name) : null, mode);
}
//第一个参数表示文件对象,第二个参数为操作模式
public RandomAccessFile(File file, String mode)
    throws FileNotFoundException
{
    this(file, mode, false);
}

mode的取值范围为:r、rw、rws、rwd,分别表示只读、读写、(rws、rwd)同步读写

RandomAccessFile r = new RandomAccessFile("/Users/c/Desktop/ceshi.txt", "rw");
r.write("helloworld".getBytes());
r.seek(r.length());
r.write("你好世界".getBytes());
r.seek(0);
byte[] buff = new byte[100];
int len = r.read(buff);
System.out.println("当前指针位置:"+r.getFilePointer());
System.out.println(new String(buff,0, len));

输出: 当前指针位置:34 helloworld你好世界

示例代码第一行创建一个可读写的随机访问的类;第二行写入一个字符串的字节数组,默认编码为UTF-8;第三行将文件的指针移动到当前字节长度的位置;第四行写入一个字符串字节数组;第五行将指针移动到文件起点;后面的代码则从文件起点读数据。

RandomAccessFile是一个可读可写的IO类,并且主要针对二进制的读写操作。

ZIP文件处理

可以使用ZipInputStreamZipOutputStream来操作ZIP压缩文件

try (FileOutputStream o = new FileOutputStream("/Users/c/Desktop/ceshi.zip");
     ZipOutputStream zipOput = new ZipOutputStream(o)) {
    zipOput.putNextEntry(new ZipEntry("/Users/c/Desktop/66.docx"));
    zipOput.putNextEntry(new ZipEntry("/Users/c/Desktop/ceshi.txt"));
}
try (FileInputStream r = new FileInputStream("/Users/c/Desktop/ceshi.zip");
     ZipInputStream zipIput = new ZipInputStream(r)) {
    ZipEntry entry;
    while ((entry = zipIput.getNextEntry()) != null) {
        System.out.println("文件名:" + entry.getName());
    }
}

输出: 文件名:/Users/c/Desktop/66.docx 文件名:/Users/c/Desktop/ceshi.txt

上面的示例先创建一个包含两个文件的压缩包,需要把希望压缩的每个文件都放到ZipEntry对象中。然后将他们的部分信息读取出来,同样的需要从每个ZipEntry对象中读取文件信息。

对象序列化

通过ObjectInputStreamObjectOutputStream可以完成对象的序列化和反序列化,但是前提是要实例化的对象类型必须实现Serializable接口,这个接口没有任何方法

序列化和反序列化

class User implements Serializable {
    private String name;
    private int age;
    private Birthday birthday;

    public User(String name,int age,Birthday birthday){
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Birthday getBirthday() {
        return birthday;
    }
}
  1. 创建一个用户类,因为要被序列化所以必须实现Serializable接口
class Birthday implements Serializable{
    private LocalDate localDate;

    public Birthday(int year,int month,int day){
        localDate = LocalDate.of(year,month,day);
    }

    public LocalDate getLocalDate() {
        return localDate;
    }

    public void setLocalDate(LocalDate localDate) {
        this.localDate = localDate;
    }
}
  1. 创建一个出生日期类,这个对象被User对象内联,而包含在被序列化对象中的对象属性所属的类也需要被序列化,所以这个类也必须实现Serializable接口
public static void main(String[] args) throws IOException {
    User user1 = new User("张三",20,new Birthday(2023,3,3));
    System.out.println("user1:"+user1);
    System.out.println("user1.birth:"+user1.getBirthday());
    try (ObjectOutputStream out = new ObjectOutputStream(
            new FileOutputStream("/Users/c/Desktop/User.dat"))){
        out.writeObject(user1);
    }

    try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("/Users/c/Desktop/User.dat"))){
        User user2 = (User) in.readObject();
        System.out.println("user2:"+user2);
        System.out.println("user2.birth:"+user2.getBirthday());
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
}
  1. 运行程序

输出: user1:day9.demo6.User@2ed94a8b user1.birth:day9.demo6.Birthday@38082d64 user2:day9.demo6.User@56ef9176 user2.birth:day9.demo6.Birthday@4566e5bd

根据控制台输出的信息可以发现,对象的序列化可以完成深拷贝。两个Birthday不是同一个引用地址。

Java中还提供了一个transient关键可以禁止对某些对象的序列化

class User implements Serializable {
    private String name;
    private int age;
    private transient Birthday birthday;
}

birthday对象使用了此关键字再运行

user2:day9.demo6.User@3f49dace user2.birth:null

这次user对象中的birthday对象指向null,很显然无法获取到它的序列化信息,因为transient关键字的原因,再做序列化操作的时候跳过了对它的序列化。

自定义序列化机制

自定义序列化的读写方法

如果在User方法中添加两个方法,那么对象序列化操作就会被覆盖

private void readObject(ObjectInputStream in) {
    System.out.println("对象输入流");
}

private void writeObject(ObjectOutputStream out){
    System.out.println("对象输出流");
}

添加这两个方法后再次运行上面的程序

输出: 对象输出流 对象输入流 user2:null user2.birth:null

这一次对象序列化和反序列化都被替换为只输出一句话,这是序列化为单个类提供的机制。所以在这里可以编写一些自定义和序列化相关的操作。

序列化版本号

版本号的作用在于确保反序列化时,Java中类的信息与序列化文件保持一致

class User implements Serializable {
    private static final long serialVersionUID = 1;
}

先将User的序列化版本号设置为1,然后执行序列化操作。现在将类中的序列化版本号修改为2,再执行反序列化操作。

class User implements Serializable {
    private static final long serialVersionUID = 2;
}

输出: local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

可以看到,反序列化失败了,因为版本不一致!