Java基础(十三) 文件高级技术

文件高级技术

一、常见文件类型处理

一)属性文件

属性文件很简单,一行表示一个属性,属性就是键值对,键和值用(=)或者(:)分隔。

#ready to work
name = tang
age = 22
phone = 110

Java使用专门的类java.util.Properties处理这种文件。主要方法:

public synchronized void load(InputStream inStream)
public String getProperty(String key)
public String getProperty(String key, String defaultValue)
Properties props = new Properties();
try {
    props.load(new FileReader("tang.properties"));
    System.out.println("The name is " + props.getProperty("name"));
} catch (IOException e) {
    e.printStackTrace();
}

优势:可以自动处理空格,自动忽略空行,以#或者!开头的会被视为注释。

二)压缩文件

Java SDK支持两种:gzip和zip,gzip只能压缩一个文件,而zip文件可以包含多个。

先看gzip:

java.util.zip.GZIPOutputStream
java.util.zip.GZIPInputStream

它们都是InputStream和OutputStream的子类,都是装饰类,GZIPOutputStream加到

已有的流上,就可以实现压缩,而GZIPInputStream加到已有的流上就可以实现解压。

public static void gzip(String fileName) {
    InputStream in = null;
    String gzipFileName = fileName + ".gz";
    OutputStream out = null;
    try {
        in = new FileInputStream(fileName);
        out = new GZIPOutputStream(new FileOutputStream(gzipFileName));
        copy(in, out);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
public static void unGzip(String gzipFileName, String fileName) {
    InputStream in = null;
    OutputStream out = null;
    try {
        in = new GZIPInputStream(new BufferedInputStream(
                new FileInputStream(gzipFileName)
        ));
        out = new BufferedOutputStream(
                new FileOutputStream(fileName)
        );
        copy(in, out);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
public static void copy(InputStream in, OutputStream out) throws IOException {
    try {
        byte[] buf = new byte[1024];
        int count = 0;
        while ((count = in.read(buf)) != -1) {
            out.write(buf, 0, count);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (in != null) in.close();
        if (out != null) out.close();
    }
}

zip支持压缩文件包中包含多个文件,Java API中的主要类是:

java.util.zip.ZipOutputStream
java.util.zip.ZipInputStream

ZipOutputStream可以写入多个文件,它有一个重要方法:

//在写入一个文件前,必须先调用该方法,表示准备写入一个压缩条目ZipEntry
public void putNextEntry(ZipEntry e) throws IOException
//每个压缩条目都有一个名称,这个名称是压缩文件的相对路径,如果以'/'结尾表示目录
public ZipEntry(String name)
/**
* 压缩一个文件或者目录
* @param inFile 表示输入,可以是文件或者目录
* @param zipFile 表示输出的zip文件
* */
public static void zip(File inFile, File zipFile) throws IOException {
    ZipOutputStream zipOut = new ZipOutputStream(new BufferedOutputStream(
            new FileOutputStream(zipFile)));
    try {
        //输入文件不存在抛出异常
        if (!inFile.exists())
            throw new FileNotFoundException(inFile.getAbsolutePath());
        inFile = inFile.getCanonicalFile();
        String rootPath = inFile.getParent();
        //如果根路径不是以"/"结尾
        if (!rootPath.endsWith(File.separator)) {
            rootPath += File.separator;
        }
        addFileToZipOut(inFile, zipOut, rootPath);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        zipOut.close();
    }
}
public static void addFileToZipOut(File inFile, ZipOutputStream out, String rootPath) throws IOException {
    String relativePath = inFile.getCanonicalPath()
            .substring(rootPath.length());
    if (inFile.isFile()) {
        out.putNextEntry(new ZipEntry(relativePath));
        InputStream in = new BufferedInputStream(
                new FileInputStream(inFile)
        );
        try {
            copy(in, out);
        } finally {
            in.close();
        }
        //如果是目录
    } else {
        out.putNextEntry(new ZipEntry(relativePath + File.separator));
        for (File f : inFile.listFiles()) {
            addFileToZipOut(f, out, rootPath);
}

二、随机读写文件(RandomAccessFile)

一)用法

RandomAccessFile构造方法:

public RandomAccessFile(String name, String mode) throws FileNotFoundException
public RandomAccessFile(File file, String mode) throws FileNotFoundException

其中name和file,表示文件路径和File对象。mode表示打开模式:

1)r:只读

2)rw:读和写

3)rws:读和写,另外,要求文件内容和元数据的任何更新都同步到设备上。

4)rwd:读和写,另外,文件内容的更新同步到设备上,元数据更新不同步。

该类有类似于InputStream/OutputStream类似的读写字节流的方法。

另外,它还实现了DataInput/DataOutput接口。

public int read() throws IOException
public int read(byte b[]) throws IOException
public final int readInt() throws IOException
public final void writeInt(int v) throws IOException
public void write(byte b[]) throws IOException

另外还有两个read方法:

public final void readFully(byte b[]) throws IOException
public final void readFully(byte b[], int off, int len) throws IOException

与对应的read方法的区别是,它们可以确保读够期望的长度,如果到了文件结尾也没读够,抛异常。

RandomAccessFile内部有一个文件指针,指向当前的读写位置,各种read/write操作都会自动更新

该指针。与流操作不同的是,RandomAccessFile可以获取该指针,也可以更改该指针。

//获取当前指针
public native long getFilePointer() throws IOException
//更改当前指针到pos
public native void seek(long pos) throws IOException

跳过输入流中的n个字节:

public int skipBytes(int n) throws IOException //通过更改指针实现

获取文件字节数:

public native long length() throws IOException

修改文件长度:

//如果当前文件的长度小于newLength,则文件会扩展,大于会收缩,文件指针比newLength
//大则会调整到newLength
public native void setLength(long newLength) throws IOException

请避免使用以下两个方法:

public final void writeBytes(String s) throws IOException
public final String readLine() throws IOException

三、内存映射文件

内存映射文件不是Java引入的概念,而是操作系统提供的一种功能,大部分操作系统都支持。

一)基本概念

所谓内存映射文件,就是将文件映射到内存,文件对应于内存的一个字节数组,对文件的操作

变为对这个字节数组的操作,而字节数组的操作直接映射到文件上。这种映射可以是文件的全部

区域也可以是部分区域。

内存映射文件特点:

1)使用的是操作系统内核内存空间,只有一次复制,比普通读写效率高

2)可被多个不同程序共享,一个程序对内存的修改,其他程序也可以看

到,这使得它特别适合不同程序间的通信

操作系统自身在加载可执行文件的时候,一般都利用了内存映射。

内存映射局限性:

因为是按页分配内存,对小文件来说浪费内存

二)用法

内存映射文件需要通过FileInputStream/FileOutputStream/RandomAccessFile,它们都有方法:

public FileChannel getChannel()

FileChannel都有方法:

/**
* 该方法将当前文件映射到内存,映射结果就是MappedByteBuffer对象,它代表内存中的字节数组
* 如果映射区域超过了文件的范围,文件会自动扩展
* @param mode 表示映射模式:
* READ_ONLY:只读
* READ_WRITE:读写
* PRIVATE:私有模式,更改不反映到文件,也不被其他程序看到
* @param position 表示映射的起始位置
* @param size 表示映射的长度
* @return 映射完成后,文件就可以关闭,对文件的后续读写可以通过MappedByteBuffer
* */
public MappedByteBuffer map(MapMode mode, long position,
                            long size) throws IOException{
}

MappedByteBuffer是ByteBuffer的子类,ByteBuffer可以理解为封装了一个长度不可变的字节数组,

在内存映射文件中这个长度由map方法中的size决定。ByteBuffer有一个基本属性position,表示当前

读写位置,相关方法是:

public final int position() //获取当前读写位置
public final Buffer position(int newPosition)  //修改当前读写位置

ByteBuffer中有很多基于当前读写位置的读写数据方法:

public abstract byte get()  //从当前位置获取一个字节
public ByteBuffer get(byte[] dst)  //从当前位置获取dst.length长度的字节到dst
public abstract int getInt() //从当前位置读取一个int
public final ByteBuffer put(byte[] src)  //将字节数组src写入到当前位置
public abstract ByteBuffer putLong(long value) //将value写入到当前位置

这些方法读写后都会自动增加position,与之对应的还有一组方法可以指定position:

public abstract int getInt(int index)  //从index处读取一个int
public abstract double getDouble(int index) 
public abstract ByteBuffer putDouble(int index, double value)
public abstract ByteBuffer putLong(int index, long value)

这些方法在读写时,不会改变当前的读写位置。

MappedByteBuffer自己还定义了一些方法:

//检查文件内容是否真正加载到了内存,仅供参考
public final boolean isLoaded()
//尽量将文件内容加载到内存
public final MappedByteBuffer load() 
//将对内存的修改强制同步到硬盘上
public final MappedByteBuffer force() 

四、标准序列化机制

序列化就是将对象转换为字节流,反序列化就是将字节流转换为对象。

一)基本用法

要让一个类支持序列化,只需要让这个类实现接口java.io.Serializable,该接口是一个标记接口。

读取/保存声明了Serializable接口的类可以使用ObjecOutputStream/ObjectInputStream流了。

ObjectOutputStream是OutputStream的子类,但实现了ObjectOutput接口,该接口是DataOutput

的子接口,增加了一个方法:

public void writeObject(Object obj) throws IOExceptio

 该方法能把对象obj转化为字节,写到流中。

ObjectInputStream核心方法:

public Object readObject() throws ClassNotFoundException, IOException

该方法中流中读取字节,转化为对象。

public static void writeStudents(List<Student> students)
        throws IOException {
    ObjectOutputStream out = new ObjectOutputStream(
            new BufferedOutputStream(new FileOutputStream("students.dat")));
    try {
        out.writeInt(students.size());for(Student s : students) {
            out.writeObject(s);
        }
    } finally {
        out.close();
    }
}
public static List<Student> readStudents() throws IOException,
        ClassNotFoundException {
    ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(
            new FileInputStream("students.dat")));
    try {
        int size = in.readInt();
        List<Student> list = new ArrayList<>(size);
        for(int i = 0; i < size; i++) {
            list.add((Student) in.readObject());
        }
        return list;
    } finally {
        in.close();
    }
}

二)定制序列化

主要有两种定制序列化的机制:

1)使用transient关键字  

声明为transient的字段,Java的默认序列化机制就不会保存该字段了,

但可以通过writeObject来自己保存。

2)实现writeObject和readObject方法

writeObject声明必须为:

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException

ArrayList中有:

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    //该方法必须被调用,即使类中所有的字段都是transient
    s.defaultWriteObject();
    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);
    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

readObject方法必须声明为:

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;
    // Read in size, and any hidden stuff
    s.defaultReadObject();
    // Read in capacity
    s.readInt(); // ignored
    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        ensureCapacityInternal(size);
        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

三)基本逻辑

writeObject基本逻辑:

1)如果对象没有实现Serializable,抛出NotSerializable异常

2)每个对象都有一个编号,如果之前已经写过该对象,下次写入只会写该

对象的引用,这可以解决对象引用和循环引用的问题

3)如果对象实现了writeObject方法,调用它的自定义方法

4)利用的是反射机制

readObject基本逻辑:

1)不调用任何构造方法

2)它自己就相当于一个独立的构造方法,根据字节流初始化对象,利用的也是反射机制

3)在解析流时,对于引用到的类型信息会动态加载,如果找不到,抛出ClassNotFoundException

四)版本问题

需要解决的问题:序列化到文件的对象是持久保存的,不会自己改变的,而我们

的代码是不断演变改进的,如果类的定义发生了变化,反序列化会出现问题。

解决方法:Java会给类自动定义一个版本号,这个版本号是根据类中的信息生成的。

在反序列化时,如果类的定义发生了变化,版本号就会变化,与流中的版本号就会

不匹配,反序列化就会抛出java.in.InvalidClassException.

但因为Java自动生成版本号性能较低,还有为了更好地控制,我们通常自定义这个

版本号。注意通过编辑器自动生成的版本号不会自己更新。

如果版本号一样,但实际字段不匹配:

1)字段删除了:即使流中有该字段,类定义中有,该字段会被忽略;

2)新增了字段:即类定义中有,而流中没有,该字段会被设置为默认值;

3)字段类型改变:抛出InvalidClassException;

五、使用Jackson序列化

一)基本用法

1.JSON

Student student = new Student("ୟӣ", 18, 80.9d);
//它是一个线程安全类
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
//默认情况下会保存所有声明为public或者有public getter方法的字段
String str = mapper.writeValueAsString(student);
System.out.println(str);
ObjectMapper mapper = new ObjectMapper();
//默认情况下,被反序列化的类必须要有无参构造函数
Student s = mapper.readValue(new File("student.json"), Student.class);
System.out.println(s.toString());

其他重要方法:

public byte[] writeValueAsBytes(Object value)
public void writeValue(OutputStream out, Object value)
public void writeValue(Writer w, Object value)
public void writeValue(File resultFile, Object value)

public <T> T readValue(InputStream src, Class<T> valueType)
public <T> T readValue(Reader src, Class<T> valueType)
public <T> T readValue(String content, Class<T> valueType)
public <T> T readValue(byte[] src, Class<T> valueType)

2.XML 

与序列化为JSON类似:

Student student = new Student("tom", 18, 80.9d);
ObjectMapper mapper = new XmlMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
String str = mapper.writeValueAsString(student);
mapper.writeValue(new File("student.xml"), student);
System.out.println(str);

3.MessagePack 

MessagePack是一种二进制形式的JSON编码更为精简高效,因为是二进制,因此不能写出为String。

Student student = new Student("jim", 18, 80.9d);
ObjectMapper mapper = new ObjectMapper(new MessagePackFactory());
byte[] bytes = mapper.writeValueAsBytes(student);
mapper.writeValue(new File("student.bson"), student);

Student s = mapper.readValue(new File("student.bson"), Student.class);
System.out.println(s.toString())

4.容器对象 

List<Student> students = Arrays.asList(new Student[] {
new Student("tom", 18, 80.9d), new Student("๫ ",ࢥ17, 67.5d) });
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
String str = mapper.writeValueAsString(students);
mapper.writeValue(new File("students.json"), students);
System.out.println(str);
//反序列化不同,需要新建一个TypeReference对象
List<Student> list = mapper.readValue(new File("students.json"),
new TypeReference<List<Student>>() {});
System.out.println(list.toString());

二)定制序列化

1.忽略字段 

//用于字段,getter、setter方法
@JsonIgnore
double score;
//用于类,指定忽略字段
@JsonIgnoreProperties("score")
public class Student {

2.引用同一个对象 

问题:

Dog price = new Dog("Price");
Person jim = new Person("Jim", price, price);
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
String value = mapper.writeValueAsString(jim);
System.out.println(value);
Person person = mapper.readValue(value, Person.class);
if (person.getFirst() == person.getSecond()) {
    System.out.println("Same");
} else {
    System.out.println("Different"); //different 指向了不同的对象
}

解决办法:

@JsonIdentityInfo(
        generator = ObjectIdGenerators.IntSequenceGenerator.class,
        property = "id"
)
public class Dog {

3.循环引用 

问题:

Parent parent = new Parent();
parent.name = "Father";
Child child = new Child();
child.name = "Child";
parent.child = child;
child.parent = parent;
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
String s = mapper.writeValueAsString(parent); //java.lang.StackOverflowError
System.out.println(s);

解决办法:

public class Parent {
    public String name;
    @JsonManagedReference //标记为主引用
    public Child child;
}
public class Child {
    public String name;
    @JsonBackReference //标记为反向引用
    public Parent parent;
}

4.反序列化时忽略未知字段 

问题:与Java标准序列化不同,在反序列化时,对于未知字段,Jackson默认会抛出异常:UnrecognizedPropertyException.

解决办法:

mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

或者:

@JsonIgnoreProperties(ignoreUnknown=true)
public class Student {
//...
}

5.继承和多态 

Jackson不能自动处理继承和多态:

public class Shape {
}

public class Circle extends Shape {
    private int r;
    public Circle() {
    }
    public Circle(int r) {
        this.r = r;
    }
}

public class Square extends Shape {
    private int l;
    public Square() {
    }
    public Square(int l) {
        this.l = l;
    }
}
public class ShapeManager {
    private List<Shape> shapeList;
    public List<Shape> getShapeList() {
        return shapeList;
    }
    public void setShapeList(List<Shape> shapeList) {
        this.shapeList = shapeList;
    }
}

解决办法:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
        @JsonSubTypes.Type(value = Circle.class, name = "circle"),
        @JsonSubTypes.Type(value = Square.class, name = "square")
})
public class Shape {
}

6.修改字段名称 

@JsonProperty("名称:") //改变输出
String name;
//对于xml修改根元素名称
@JsonRootName("student")
public class Student {

7.格式化日期 

默认情况下日期会被序列化为一个长整数。解决:

@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone="GMT+8")
public Date date = new Date();

8.配置构造方法 

序列化时,如果类没有无参构造函数,会抛异常。解决:

@JsonCreator
public Student(
        @JsonProperty("name") String name,
        @JsonProperty("age") int age,
        @JsonProperty("score") double score) {
    this.name = name;
    this.age = age;
    this.score = score;
}
Simple is important!
原文地址:https://www.cnblogs.com/Shadowplay/p/9999798.html