Java核心技术·卷二·第二章笔记

第二章:输入与输出

2.1 输入/输出流

抽象类 InputStream和OutputStream构成了IO类层次结构的基础

(由于UnionCode形式储存是由多个不定的字节储存的,而不是基于byte值的,所以有从Reader和Writer中继承出来了一个专门处理UnionCode字符的单独的类层次结构)

2.1.1 读写字节

InputStream的read抽象方法为其各种类型子类的基础。readAllBytes方法可以读取流中所有字节。

IO操作在执行的时候是阻塞的,结束时应该调用close方法关闭IO,清空缓冲区(也可以使用flush方法手动的清楚缓冲区)

java.io.InputStream:
abstract int read();读入并返回一个字节,碰到输入流的结尾时返回-1;
int read(byte[] b);读入一个字节数组,返回实际读入的字节数,碰到输入流的结尾时返回-1,最多读入b.length个字节;
int read(byte[] b, int off, int len);
int readNBytes(byte[] b, int off, int len);
如果未阻塞(read),读入len个字节,或者阻塞至所有的值都被读入(readNBytes)。读入的值将置于b中从off开始的位置。返回实际读入的字节数,碰到结尾返回-1;
byte[] readAllBytes();
产生一个包含从当前流中读入的所有字节的数组;
long transferTo(OutputStream out);
将当前输入流的所有字节传送到out输出流。这两个流都应该处于开启状态;
long skip(long n);跳过n个字节;
int available() 返回在不阻塞的情况下可获取的字节数;
void close();
void mark(int readlimit);
在当前位置打一个标记,如果从输入流已经读入的字节已经多于readlimit个,则对这个流允许忽略这个标记;
void reset();返回最后一个标记,随后对read的调用会从这个标记开始;
boolean markSupported();如果支持被打标记,则返回true
java.io.OutputStream:
abstract void write(int n);写出一个字节的程序;
void write(byte[] b);
void write(byte[] b, int off, int len);
类似Input Stream的方法;
void close();
void flush();冲刷输出流,将所有缓冲发送到目的地
2.1.2 完整的流家族

读写单个字节或者字节数组为InputStream与OutputStream。对于UnionCode文本使用Wirter和Reader。

还有四个附加的接口:Closeable,Flushable,Readable,Appendable。

Closeable接口扩展自AutoCloseable接口,前者只能抛出IOException,后者可以抛出任何异常。

java.lang.Appendable:
Appendable append(char c);
Appendable append(CharSequence cs);
向整个Appendable中追加给定的码元或者给定序列中所有的码元;
2.1.3 组合输入/输出流过滤器
System.getProperty("user.dir");可以获得程序执行的根目录地址;
java.io.File.separator;可以获得当前操作系统的分隔符,使用这个可以提高可扩展性。

可以通过多重嵌套过滤器的方式赋予输入输出流各种功能,虽然繁杂,但是带来了极大的灵活性

java.io.FileInputStream:
FileInputStream(String name);
FileInputStream(File file);
创建一个新的输入流。
    
java.io.FileOutputStream:
FileOutputStream(String name);
FileOutputStream(String name, boolean append);
FileOutputStream(File file);
FileOutputStream(File file, boolean append);
如果append为true,遇到相同名字的已有文件时,将数据添加到文件尾部,如果为false,则删除。
    
java.io.BufferedInputStream:
BufferedInputStream(InputStream in);//注意参数为InputStream
创建一个带缓冲区的输入流,它不会每次都访问设备,而是一次性读取一个数据块放在缓冲区,缓冲区为空则再读取
    
java.io.BufferedOutputStream:
BufferedOutputStream(OutputStream out);
创建一个带缓冲区的输出流,先输出到缓冲区,缓冲区满或者被flus之后数据就被写出;

java.io.PushbackInputStream:
PushbackInputStream(InputStream in);
PushbackInputStream(InputStream in, int size);
创建一个可以预览一个字节或者具有指定尺寸的回推缓冲区输入流;
void unread(int b);
回推一个字节,在下次调用read时被再次读取。

java的使用某些输入流(例如 FileInputStream)来获取外部的字节,再使用其他的输入流将字节装配到更有用的数据类型中(例如DataInputStream)

var fin = new FileInputStream("employee.dat");
var din = new DataInputStream(fin);
double x = dinreadDouble();
var din = new DataInputStream(
	new BufferedInputStream(
    	new FileInputStream()));
din为有缓冲机制的的文件数据输入流
var pbin = new PushbackInputStream(
	new BufferedInputstream(
    	new FileInputStream("employee.dat")));
int b = pbin.read();
if(b != '<'){
    pbin.unread(b);
}
pbin有回推机制
var din = new DataInputStream(
	pin = new PushbackInputStream(
    	new BufferedinputStream(
        	new FileInputStream("employee.dat"))));
din为有缓冲机制和回推机制的文件数据输入流。
var zin = new ZipInputStream(new FileInputStream("employee.dat"));
var din = new DataInputStream(zin);
2.1.4 文本输入与输出

对于以文本格式储存的数据,可以使用InputStreamReader和OutputStreamWriter以指定的编码格式读取UnionCode码流为字节流

var in = new InputStreamReader(System.in);//使用默认的编码方式
var in = new InputStreamReader(new FileInputStream("data.txt"),StandardCharsets.UTF_8);
2.1.5 如何写出文本输出

使用PrintWriter来进行文本输出

public static void main(String[] args) throws IOException{
    var out = new PrintWriter("employee.txt", StandardCharsets.UTF_8);
    String name = "NJQ";
    double salary = 100000;
    out.println(name + " " + salary);
    out.flush();//注意:默认写出器不会自动冲刷,如果没冲刷,不会有任何内容
    out.close();//记得关
}
var out = new PrintWriter(new OutputStreamWriter(
                new FileOutputStream("employee.txt"),StandardCharsets.UTF_8),true);
        //通过使用PrintWriter(Writer writer, boolean autoFlush)来启用自动冲刷
java.io.PrintWriter:
PrintWriter(Writer out);
PrintWriter(String filename, String encoding);
PrinrWeiter(File file, String encoding);
void print(Object obj);
void print(String s);
//打印字符串
void println(String s);
//打印字符串,如果处于自动冲刷模式,就会自动冲刷这个流
void printf(String format, Object... args);
boolean checkError();
如果产生格式化或输出错误,则返回true
2.1.6 如何读入文本输入

Scanner类,对于短小的文本文件,也可以使用Files工具类

var content = Files.read(path, charset);
//也可以一行一行的读入
List<String> lines = Files.readAllLines(path, charset);
//也可以惰性处理为一个Stream对象
try(Stream<String> lines = Files.lines(path, charset)){
    ...
}
//还可用Scanner来读入符号(token),即为被分隔符分割的字符串
Scanner in = ...;
in.useDelimiter("\\PL+");
然后next和hasNext方法就不用说了
//早期Java版本中是通过BufferedReader类来实现文件输入的
InputStream inputStream = ...;
try(var in = new BufferedReader(new InputStreamReader(inputStream,charset))){
    String line;
    while((line = in.readLine()) != null){
        dosomething with line;
    }
}
2.1.7 以文本格式存储对象
package com.package1;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.util.Locale;
import java.util.Scanner;

public class Test {
    public static void main(String[] args) throws IOException{
        var staff = new Employee[3];
        staff[0] = new Employee("Carl Cracker",75000,1987,12,15);
        staff[1] = new Employee("Harry Hacker", 54000,1988,10,1);
        staff[2] = new Employee("Tony Tester",40000,1990,4,15);
        try(var out = new PrintWriter("employee.txt",StandardCharsets.UTF_8)){
            writeData(staff,out);
        }
        //回想:try资源再退出try块的时候会自动进行资源释放,所以这里不用手动close
        try(var in = new Scanner(
                new FileInputStream("employee.txt"),"UTF-8")
        ){
            Employee[] newStaff = readData(in);
            for(Employee e : newStaff){
                System.out.println(e);
            }
        }
    }
    private static void writeData(Employee[] employees, PrintWriter out) throws IOException{
        out.println(employees.length);
        for(Employee e : employees){
            writeEmployee(out,e);
        }
    }
    public static void writeEmployee(PrintWriter out, Employee e){
        out.println(e.getName() + "|" + e.getSalary() + "|" + e.getHireDate());
    }

    private static Employee[] readData(Scanner in){
        int n = in.nextInt();
        in.nextLine();//对nextInt的处理不会处理掉最后的换行符,再次调用nextLine处理掉这个换行符
        var employees = new Employee[n];
        for(int i = 0; i < n; i++){
            employees[i] = readEmployee(in);
        }
        return employees;
    }
    public static Employee readEmployee(Scanner in){
        String line = in.nextLine();
        String[] tokens = line.split("\\|");//因为 | 需要用\来转义,而\又需要用另一个\来转义,就形成了\\|
        String name = tokens[0];
        double salary = Double.parseDouble(tokens[1]);
        LocalDate hireDate = LocalDate.parse(tokens[2]);
        int year = hireDate.getYear();
        int month = hireDate.getMonthValue();
        int day = hireDate.getDayOfMonth();
        return new Employee(name,salary,year,month,day);
    }

}
class Employee{
    private String name;
    private double salary;
    private LocalDate hireDate;

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public LocalDate getHireDate() {
        return hireDate;
    }

    public Employee(String name, double salary, LocalDate hireDate) {
        this.name = name;
        this.salary = salary;
        this.hireDate = hireDate;
    }
    public Employee(String name, double salary, int year, int month, int day){
        this.name = name;
        this.salary = salary;
        this.hireDate = LocalDate.of(year, month, day);
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", salary=" + salary +
                ", hireDate=" + hireDate +
                '}';
    }
}
2.1.8 字符编码方式

Charset类。

//在读入或者写出文本时,应该使用Charset对象
var str = String(bytes, StardardChasets.UTF_8);

2.2 读写二进制数据

二进制数据相比于文本数据具有高效的特点

2.2.1 DataInput和DataOutput接口
DataOutput的write...方法定义了以二进制数量值写出的方法
eg: writeInt 总是将一个整数写出为4字节的二进制数量值而不管它有多少位

Java中所有的值都按照高位在前的模式写出,而不管使用何种处理器.(例如1234的16进制是4D2,对于int类型,如果使用高位储存MSB就是00 00 04 D2,使用低位LSB就是04 D2 00 00)

DataInputStream in = new DataInputStream(new FileInputStream("employee.txt"));
DataOutputStream out = new DataOutputStream(new FileOutputStream("employee.txt"));
//以二进制形式读写文件
2.2.2 随机访问文件

RandomAccessFile类

package com.package1;

import java.io.*;
import java.lang.instrument.Instrumentation;
import java.time.LocalDate;

public class Test {
    public static void main(String[] args) throws IOException{
        var staff = new Employee[3];
        staff[0] = new Employee("Carl Cracker",75000,1984,5,23);
        staff[1] = new Employee("Harry Hacker",52000,1988,5,6);
        staff[2] = new Employee("Tony Tester",40000,2001,5,26);

        try(var out = new DataOutputStream(new FileOutputStream("employee.txt"))){
            for(Employee e : staff){
                writeData(out,e);
            }
        }

        try(var in = new RandomAccessFile("employee.txt","r")){
            int n = (int)(in.length()/Employee.RECODE_SIZE);
            var newStaff = new Employee[n];

            for(int i = n-1; i >= 0; i--){
                newStaff[i] = new Employee();
                in.seek(i*Employee.RECODE_SIZE);//seek移动的是字节,在本例中,一个实例占100个字节
                newStaff[i] = readData(in);
            }
            for(Employee e : newStaff){
                System.out.println(e);
            }
        }
    }
    public static void writeData(DataOutput out,Employee e) throws IOException{
        DataIO.writeFixedString(e.getName(),Employee.NAME_SIZE,out);
        out.writeDouble(e.getSalary());

        LocalDate hireDate = e.getHireDate();
        out.writeInt(hireDate.getYear());
        out.writeInt(hireDate.getMonthValue());
        out.writeInt(hireDate.getDayOfMonth());
    }
    public static Employee readData(DataInput input) throws IOException{
        String name = DataIO.readFixedString(input,Employee.NAME_SIZE);
        double salary = input.readDouble();
        int y = input.readInt();//只会读取一个int也就是4个字节的数据,并不会读取到后面的月份和日的数据。
        int m = input.readInt();
        int d = input.readInt();
        return new Employee(name, salary, y, m-1,d);//todo ? m-1 ?
    }
}
class Employee{
    private String name;
    private double salary;
    private LocalDate hireDate;
    public static final int NAME_SIZE = 40;
    //注意,在Java中采取UnionCode编码,默认char为俩字节
    public static final long RECODE_SIZE = 100;

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public LocalDate getHireDate() {
        return hireDate;
    }

    public Employee(String name, double salary, LocalDate hireDate) {
        this.name = name;
        this.salary = salary;
        this.hireDate = hireDate;
    }
    public Employee(String name, double salary, int year, int month, int day){
        this.name = name;
        this.salary = salary;
        this.hireDate = LocalDate.of(year, month, day);
    }

    public Employee() {
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", salary=" + salary +
                ", hireDate=" + hireDate +
                '}';
    }
}
class DataIO{
    public static void writeFixedString(String s, int size, DataOutput out) throws IOException{
        for(int i = 0; i < size; i++){
            char ch = 0;
            if(i < s.length()){
                ch = s.charAt(i);
            }
            out.write(ch);
        }
    }
    public static String readFixedString(DataInput input,int size) throws IOException{
        var b = new StringBuilder(size);//StringBuilder对于连续的字符串拼接具有更高的效率
        int i = 0;
        var done = false;
        while(!done && i < size){
            char ch = input.readChar();
            i++;
            if(ch == 0){
                done = true;
            }
            else{
                b.append(ch);
            }
        }
        input.skipBytes(2*(size - i));
        return b.toString();
    }
}

存在union Code的问题,todo

2.2.3 ZIP文档
//读取ZIP文件的典型框架
var in = new ZipInputStream(new FileInputStream(zipName));
ZipEntry entry;//ZIP里面的项
while((entry = zin.getNextEntry()) != null){
    read the contents of zin;
    zin.closeEntry();
}
zin.close();
//写入ZIP文件的典型框架
var fout = new FileOutputStream("test.zip");
var zout = ne ZipOutputStream(fout);
//for all files:
{
    var ze = new ZipEntry(filename);
    zout.putNextEntry(ze);
    //send data to zout;
    zout.closeEntry();
}
zout.close();

2.3 对象输入/输出流与序列化

2.3.1 保存和加载序列化对象

使用ObjectInputStream和ObjectOutputStream类。

序列化为每一个对象保存了一个序列,使之完成了脱离了内存地址的引用,能够实现网络传输和持久化储存对象

package com.package1;

import java.io.*;
import java.lang.instrument.Instrumentation;
import java.time.LocalDate;

public class Test {
    public static void main(String[] args) throws IOException,ClassNotFoundException{

        var carl = new Employee("Carl Cracker",75000,1984,5,23);
        var harry = new Manager("Harry Hacker",52000,1988,5,6);
        var tony = new Manager("Tony Tester",40000,2001,5,26);
        harry.setSecretary(carl);
        tony.setSecretary(carl);
        var staff = new Employee[3];
        staff[0] = carl;
        staff[1] = harry;
        staff[2] = tony;

        try(var out = new ObjectOutputStream(new FileOutputStream("employee.txt"))){
            out.writeObject(staff);
        }
        try(var in = new ObjectInputStream((new FileInputStream("employee.txt")))){
            var newStaff = (Employee[]) in.readObject();
            for(Employee e : newStaff){
                System.out.println(e);
            }
        }
    }
}
class Employee implements Serializable{ //注意一定要继承Serializable接口标识这个类是交给JVM进行序列化的,否则会报错,NotSerializableException
    private String name;
    private double salary;
    private LocalDate hireDate;
    public static final int NAME_SIZE = 40;
    //注意,在Java中采取UnionCode编码,默认char为俩字节
    public static final long RECODE_SIZE = 100;

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public LocalDate getHireDate() {
        return hireDate;
    }

    public Employee(String name, double salary, LocalDate hireDate) {
        this.name = name;
        this.salary = salary;
        this.hireDate = hireDate;
    }
    public Employee(String name, double salary, int year, int month, int day){
        this.name = name;
        this.salary = salary;
        this.hireDate = LocalDate.of(year, month, day);
    }

    public Employee() {
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", salary=" + salary +
                ", hireDate=" + hireDate +
                '}';
    }
}
class Manager extends Employee{
    private Employee secretary;

    public Employee getSecretary() {
        return secretary;
    }

    public void setSecretary(Employee secretary) {
        this.secretary = secretary;
    }

    public Manager(String name, double salary, int year, int month, int day) {
        super(name, salary, year, month, day);
    }
}
2.3.2 理解对象序列化的文件格式
  • 对象流输出中包含所有对象的类型和数据域
  • 每个对象都被赋予一个序列号
  • 相同对象的重复除非将被储存为对这个对象的序列号的引用

具体实现细节详见P78页,稍复杂。

2.3.3 修改默认的序列化机制

对于某些不具有传输意义的数据域,例如只对本地方法有意义的储存文件句柄或窗口句柄,Java会将它们标记成transient(转瞬即逝的)

但是可以定义具有下列签名的方法,而且实现serializable接口,实现自定义序列化

private void readObject(ObjectInputStream in)
    throws IOException, ClassNotFoundException;
private void writeObject(ObjectOutputStream out)
    throws IOException
//例如,java.awt.geom包里的Point2D.Double是不可序列化的,但是可以修改默认的序列化机制来完成
public class LabeledPoint implements Serilizable{
    private String label;
    private transient Point2D.Double point;
    //需要将point标记为transient,否则会抛出NotSerializableException异常
    ...;
    private void writeObject(ObjectOutputStream out) throws IOException{
        out.defaultWriteObject();//写出对象描述符和String域label
        out.writeDouble(point.getX());
        out.weiteDouble(point.getY());
    }
    private void readObject(ObjectInputStream in){
        in.defaultReadObject();
        double x = in.readDouble();
        double y = in.readDoubel();
        point  = new POint2D.Double(x,y);
    }
}

在写出对象时,序列化机制在输出流中仅仅是记录该对象所属的类。在读入可外部化的类时,对象输入流将用无参构造器创建一个对象,然后调用readExternal方法。

类可以实现Externalizable接口来定义他自己的机制;
public void readExternal(ObjectInputStream in) throws IOException,ClassNotFoundException;
public void writeExtenal(ObjectOutpputStream out) throws IOException;
//相对于只能被序列化方法调用的readObject和writeObject,这俩方法是公共的,潜在的允许修改现有状态的状态
public void readExternal(ObjectInput s) throws IOException{
    name = s.readUTF();
    salary = s.readDouble();
    hireDay = LocalDate.ofEpochDay(s.readLong());
}
public void writeExternal(ObjectOutput s) throws IOException{
    s.writeUTF(name);
    s.writeDouble(salary);
    s.writeLong(hireDay.toEpochDay());
}
2.3.4序列化单例和类型安全的枚举

对于类型安全的枚举,我们无疑会使用 == 去直接比较枚举值是否相同,但是序列化机制会直接跳过类的private构造方法构造出一个新的对象。对于单例来说,无疑也会造成单例性被破坏

class Orientation{
    public  static final Orientation HORIZONTAL = new Orientation(1);
    public static final Orientation VERTICAL = new Orientation(2);
    private int value;

    private Orientation(int value){
        this.value = value;
    }
}
//对于下列看似正常的序列化与反序列化操作:
Orientation original = Origentation.HORIZONTAL;
ObjectOutputStream out = ...;
out.write(original);
out.close();
ObjectInputStream in = ...;
var saved = (Origentation)in.read();
//如果进行下列比较,结果是反常的false
if(saved == Origetation.HORIZENTAL){
    ...
}
//readResolve方法在对象被序列化之后就会调用,它必须返回一个对象,而这个对象会成为readObject的返回值。
protected Object readResolve() throws ObjectStreamException{
    if(value == 1) return Origentation.HORIZONTAL;
    if(value == 2) return Origentation.VERTICAL;
    throw new ObjectStreamException();
}
2.3.5 版本管理

不同版本的类的SHA指纹会发生变化,而对象输入流将拒绝读入具有不同指纹的对象,但是可以通过声明其对早期版本兼容来实现版本管理与兼容

调用JDK的serialver工具得到早期类的指纹,之后这个类的所有较新版本都必须把serialVersionUID常量定义为与最初版本相同。

如果一个类含有名为serialVersionUID的静态数据成员,那么他就不会再计算指纹,而会直接使用它,这就实现了兼容性

serialver com.package1.Test
com.package1.Test:    private static final long serialVersionUID = -236646717991938373L;

对于新旧版本的转化,多的会设置成null(这可能会带来安全性的考虑),少的会删去

2.3.6 为克隆使用序列化

可以基于序列化+反序列化的原理实现深拷贝。

package com.package1;

import java.io.*;
import java.time.LocalDate;

public class SerialCloneTest{
    public static void main(String[] args) throws CloneNotSupportedException{
        var harry = new Employee("Harry Hacker",60000,1978,5,2);
        var harry2 = (Employee)harry.clone();

        harry.riseSalary(10);
        //harry与harry2不同了,视为两个版本
    }
}
class Employee extends SerialCloneable{
    private String name;
    private double salary;
    private LocalDate hireDate;

    public Employee(String name, double salary, int year, int month, int day) {
        this.name = name;
        this.salary = salary;
        this.hireDate = LocalDate.of(year,month,day);
    }
    public void riseSalary(double rise){
        salary += rise;
    }
}
class SerialCloneable implements Cloneable ,Serializable{
    public Object clone() throws CloneNotSupportedException{
        try{
            var bout = new ByteArrayOutputStream();//使用ByteArrayOutputStream将数据保存到字节数组中,不必将对象写出到文件中
            try(var out = new ObjectOutputStream(bout)){
                out.writeObject(this);
            }
            try(var bin = new ByteArrayInputStream(bout.toByteArray())){
                var in = new ObjectInputStream(bin);
                return in.readObject();
            }
        }catch (IOException | ClassNotFoundException e){
            var e2 = new CloneNotSupportedException();
            e2.initCause(e);
            throw e2;
        }
    }
}

小心此方法,尽管它很灵巧,但是通常被显示得构造新对象并辅助或克隆数据域的克隆方法慢得多

2.4 操作文件

Path接口和Files类在Java 7中被添加,他们封装了在用户机器上处理文件系统所需的所有功能

2.4.1 Path
java.nio.file.Paths:
Path absolute = Paths.get("/home","harry");
Path relative = Paths.get("myprog","conf","user.properties");
Paths的get方法会将字符串用默认的文件系统分隔符连接起来(Unix/Windows是\);
absolute为/home/harry,relative为 classPath/myprog/conf/user.properties
java.nio.file.Path
Path resolve(Path other);
Path resolve(String other);
//如果other是绝对路径,就返回other,否则,返回通过连接this和other获得的路径
Path resolveSibling(Path other);
Path resolveSibling(String other);
//如果other是绝对路径,返回other,否则,返回通过连接this的父路径和other获得的路径
//例如如果workPath是/opt/myapp/work,那么调用 Path tempPath = workPath.resolveSibling("temp"),tempPath的路径为 /opt/myapp/temp
Path relativize(Path other);
//使用this进行解析,相对于other的相对路径,例如以"/home/harry"为目标,对“/home/fred/input.txt"进行relativize操作,会产生"../fred/input.txt"
Path normalize();
//移除诸如..和..等冗余的路径元素,例如对"/home/njq/../work/./com/..input.txt"进行normalize会产生"/hone/work/input.txt"
Path toAbsolutePath();
//返回与该路径等价的绝对路径。该路径从根路径开始
Path getParent();
//返回父路径,如果没有父路径返回null
Path getFileName();
//返回该路径最后一个部件,如果没有任何部件,返回null。如对"/usr/src/example.txt"返回example.txt
Path getRoot();
//返回根部件,如果没有返回null
File toFile();
//从该路径中创建一个Path对象
java.io.File:
Path  toPath();
//从该文件中创建一个Path对象
2.4.2 读写文件

Files的那些简便方法适用于短中长度的文本文件,对于二进制文件和较长的文件,应该用Files的stream相关的方法

java.nio.file.Files:
static byte[] readAllBytes(Path path);
static String readString(Path path, Charset charset);
static List<String> readAlllines(Path path, Charset charset);
读入文件的内容;
static Path write(Path path, byte[] contents, OpenOption... options);
static Path write(Path path, String contents, Charset charset, OpenOption... options);
static Path write(Path path, Iterable<? extends CharSequence> contents, OpenOption options);
将给定内容写出到文件中,并返回path;
static InputStream newInputStream(Path path, OpenOption... options);
static OutputStream newOutputStream(Path path, OpenOption... options);
static BufferedReader newBufferedReader(Path path, charset charset);
static BufferedWriter newBufferedWriter(Path path, Charset charset, OpenOption... options);
打开一个文件,用于读入或写出
2.4.3 创建文件和目录
static Path createFile(Path path, FileAttribute<?>... attrs);
static Path createDirectory(Path path, FileAttribute<?>... attrs);
static Path createDirectories(Path path, FileAttribute<?>... attrs);
创建文件或目录,...ties会创建所有的中间目录,而..tory只会根据已有的目录创建,如果没有就会抛出错误;
static Path createTempFile(String prefix, String suffix, FileAttribute<?>... attrs);
static Path createTempFile(Path parentDir, String prefix, String suffix, FileAttribute<?>.. attrs);
static Path createTempDirectory(String prefix, FileAttribute<?>... attrs);
static Path createTempDirectory(Path parentDir, String prefix, FileAttribute<?>.. attrs);
创建临时文件或目录,返回该文件的位置。
2.4.4 复制,移动和删除文件
java.nio.file.Files:
static Path copy(Path from, Path to, CopyOption... options);
static Path move(Path from, path to, CopyOption... options);
static long copy(InputStream from, Path to, CopyOption... options);
static long copy(Path from, OutputStream to, CopyOption... options);
//将输入流复制到文件中或者将文件复制到输出流中,返回复制的字节数
static void delete(Path path);
static boolean deleteIfExists(Path path);
//删除指定文件或 空 目录。

对于CopyOptions有一些常见的枚举值:

REPLACE_EXISTING:如果存在,覆盖;
COPY_ATTRIBUTES:复制文件属性(创建时间等);
ATOMIC_MOVE:原子化移动
2.4.5 获取文件信息

可以调用Files的is**返回的布尔值来知晓一部分属性

所有的文件系统都会报告一个基本属性集,它们被封装在Basic File Attributes接口中:有关时间的都表示出java.nio.file.attribute.FileTime

BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
2.4.6 访问目录中的项
try(Stream<Path> entries = Files.list(pathToDirectory)){
    可以获取目录中的各个项,但是不会进入子目录
}
try(Stream<Path> entirs = Files.walk(pathToRoot)){
    walk方法可以进入子目录
}
更高级的,可以使用find方法来指定谓语,实现条件索引

这段代码使用了Files,walk方法来将一个目录复制到另一个目录

Files.walk(source).forEach(p -> {
    try{
        Path q = target.resolve(source.relativize(p));
        if(Files.isDirectory(p))
            Files.createDirectory(q);
        else
            Files.copy(p,q);
    }
    catch(IOException ex){
        throw new UnCheackedIOException(ex);
    }
});
2.4.7 使用目录流

使用Files.newDirectoryStream来产生DirectoryStream对象,并且可以使用glob模式来过滤文件。

如果想要访问某个目录的所有子孙成员,可以转而调用walkFileTree方法,并向其传递一个FileVisitor类型的对象,来实现智能化操作。

具体详看P96

2.4.8 ZIP文件结构

paths类会在默认的文件系统里面查找目录与文件,但是还可以有其他的文件系统,例如ZIP文件系统

FileSystem fs = FileSystems.newFileSystem(Paths.get(zipname),null);
复制文件将会变得容易
Files.copy(fs.getPath(sourceName),targetPath);
//例如,列出一个ZIP文档中的所有文件
FileSystem fs = FileSystem.newFileSystem(Paths.get(zipname),null);
Files.walkFileTree(fs.getpPath("/"),new SimpleFileVisitor<Path>(){
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException{
        System.out.println(file);
        return FileVisitResult.CONTINUE;
    }
});

2.5 内存映射文件

使用虚拟内存来将一个文件或者文件的一部分映射到内存中,实现以内存数组的方式访问

2.5.1 内存映射文件的性能

对于大文件使用内存映射或者带缓存的输入流相对于随机访问或者不带缓存的输入流能显著提高效率。

首先获得一个通道channel,他是磁盘文件的一种抽象
FileChannel channel = FileChannel.open(path, options);
之后调用FileChannel类的map方法从这个通道中获得一个ByteBuffer,并可以指定映射的文件区域与映射模式;
FileChannel.MapMode.READ_ONLY:只读,任何修改都会造成ReadOnlyBufferException;
FileChannel.MapMode.READ_WRITE:可写可读,而且任何修改都会在某个时刻回到文件中;
FileChannel.MapMode.PRIVATE:可写可读,任何修改都是私有的,不会传播回文件系统中去;

使用get和put方法可以首先对内存映射文件的修改

ByteOrder b = buffer.order();
//查询缓冲区当前的字节顺序(高位、低位)
buffer.order(ByteOrder.LITTLE_ENDIAN);
//修改字节顺序为低位;

下面这个程序用于计算文件的32位循环冗余校验和(CRC32),它是用于判断文件是否损坏的

package com.package1;

import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.zip.CRC32;

public class MemoryMapTest{
    public static void main(String[] args) throws IOException {
        System.out.println("Input Stream:");
        Path filename = Paths.get(args[0]);
        long start = System.currentTimeMillis();
        long crcValue = checksumInputStream(filename);
        long end = System.currentTimeMillis();
        System.out.println((end -start)+" milliseconds");

        System.out.println("Buffed Input Stream");
        start  = System.currentTimeMillis();
        crcValue = checksumBufferedInputStream(filename);
        end = System.currentTimeMillis();
        System.out.println((end -start)+" milliseconds");

        System.out.println("Random Access Stream");
        start = System.currentTimeMillis();
        crcValue = checksumRandomAccessFile(filename);
        end = System.currentTimeMillis();
        System.out.println((end - start) + " milliseconds");

        System.out.println("Mapped file");
        start = System.currentTimeMillis();
        crcValue = checksumMappedFile(filename);
        end = System.currentTimeMillis();
        System.out.println((end - start) + " milliseconds");
    }
    public static long checksumInputStream(Path filename) throws IOException{
        try(InputStream in = Files.newInputStream(filename)){
            var crc = new CRC32();

            int c;
            while((c = in.read()) != -1){
                crc.update(c);
                //此循环完成了CRC32的计算
            }
            return crc.getValue();
        }
    }
    public static long checksumBufferedInputStream(Path filename) throws IOException{
        try(var in = new BufferedInputStream(Files.newInputStream(filename))){
            var crc = new CRC32();
            int c;
            while((c = in.read()) != -1){
               crc.update(c);
            }
            return crc.getValue();
        }
    }
    public static long checksumRandomAccessFile(Path filename) throws IOException{
        try(var file = new RandomAccessFile(filename.toFile(),"r")){
            long length = file.length();
            var crc = new CRC32();

            for(long p = 0; p < length; p++){
                file.seek(p);
                int c = file.readByte();
                crc.update(c);
            }
            return crc.getValue();
        }
    }
    public static long checksumMappedFile(Path filename) throws IOException{
        try(FileChannel channel = FileChannel.open(filename)){
            var crc = new CRC32();
            int length = (int) channel.size();
            MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY,0,length);

            for(int p =0; p < length; p++){
                int c = buffer.get(p);
                crc.update(c);
            }
            return crc.getValue();
        }
    }
}
result:
Input Stream:
116472 milliseconds
Buffed Input Stream
284 milliseconds
Random Access Stream
128975 milliseconds
Mapped file
154 milliseconds
2.5.2 缓冲区数据结构

缓冲区是有相同类型的数值构成的数组,Buffer类是一个抽象类,他有包括ByteBuffer,CharBuffer,DoubleBuffer,IntBuffer,LongBuffer,ShortBuffer这些子类(注意:StringBuffer类与这些缓冲区没有关系)

其中最常用的是ByteBUffer和CharBuffer。每个缓冲区都有如下结构:

  • 一个容量,不能改变
  • 一个读写位置,下一个值将在此进行读写
  • 一个界限,超过界限的读取的没有意义的
  • 一个可选的标记,用于重复一个读入或写出操作

0<= 标记 <= 读写位置 <= 界限 <= 容量

创建缓冲区的目的主要是执行“写,然后读入”的循环;

  1. 初始化了一个位置位0的缓冲区,界限等于容量。
  2. 不断调用put将值添加到这个缓冲区中直至数据耗尽或数据量达到容量大小
  3. 调用flip方法将界限设置到当前位置并把位置复到0。
  4. 在remaining返回整数时,不断调用get获取数据
  5. 读入所有值之后,调用clear,为下一次写循环做好准备。(clear方法将位置复位到0,并将界限复位到容量)
ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);
channel.read(buffer);
channel.position(newpos);
buffer.flip();
channel.write(buffer);
//通过这样的调用,可以将缓冲区的内容写入到通道中,可以替代随机访问文件。

2.6 文件加锁机制

对于多个程序对同一文件的修改的情况,添加文件锁可以有效的避免对文件多线程修改的损坏

FileChannel = FileChannel.open(path);
FileLock lock = channel.lock();
//or:
Filelock lock = channel.trylock();
//lock方法会一直等待,直到获得锁,而trylock方法调用后会直接返回锁或者null。
FileLock lock(long start, long size, boolean shared);
//or:
FileLock tryLock(long start, long size, boolean shared);
//锁定文件的一部分,shared值如果为true则证明这是一个共享锁,它允许多个进程从文件读入,而且阻止任何进程获得独占的锁.可以调用FileLock类的isShared方法来查询锁持有锁的类型。

2.7 正则表达式

2.7.1 正则表达式语法

???人被干麻了,先留着以后再来补

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值