二、输入与输出

1、输入/输出流

抽象类InputStream和OutputStream 构成了输入/输出(I/O)类层次机构的基础。

因为面向字节的流不便于处理以Unicode形式存储的信息,所以从抽象类Reader和Writer中继承出来了一个专门用于处理Unicode字符的单独的类层次结构。这些类拥有的读入和写出操作都是基于两字节的Char值的(即,Unicode码元),而不是基于byte值的。

1.1 读写字节
· abstract int read()
  从数据中读入一个字节,并返回该字节。这个read方法在碰到输入流的结尾时返回-1。
· long skip(long n)
  在输入流中跳过n个字节,返回实际跳过的字节数(如果碰到输入流的结尾,则可能小于n)。
· int available()
  返回在不阻塞的情况下可获取的字节数
· abstract void write(int n)
  写出一个字节的数据
· void flush()
  冲刷输出流,也就是将所有缓冲的数据发送到目的地。
1.2 组合输入/输出流过滤器

与抽象类InputStream和OutputStream一样,这些类值支持在字节级别上的读写。如果我们只有DateInputSteam,那么我们就只能读入数值类型;为了从文件中读入数字,我们可以首先创建一个FileInputStrea,然后将其传递给DataInputStream的构造器:

FileInputStream fin = new FileInputStream("employee.dat");
DateInputStream din = new DateInputStream(fin);
double x = din.readDouble();

如果我们想要使用缓冲机制,以及用于文件的数据输入方式,那么就需要使用下面这种构造器序列:

DateInputStream din = new DateInputStream(
	new BufferedInputStream(
		new FileInputStream("employee.dat")));

有时当多个输入流链接在一起时,你需要跟踪各个中介输入流。例如,当读入输入时,你经常需要预览下一个字节,以了解它是否是你想要的值。
java提供了用于此目的的PushbackInputStream;

PushbackInputStream pbin = new PushbackInputStream(
	new BufferedInputStream(
		new FileInputStream("employee.bat")));
//预读下一个字节:
int b = pbin.read();
//并且再它非你所期望的值时将其推回流中。
if(b != '<'){
	pbin.unread(b);
}

2、文本输入与输出

OutputStreamWriter类将使用选定的字符编码方式,把Unicode码元的输出流转换为字节流。而InputStreamReader类将包含字节(用某种字符编码方式表示的字符)的输入流转换为可以产生Unicode码元的读入器。
Reader in = new InputStreamReader(new FileInputStream("data.txt"), StandardCharsets.UTF_8);

2.1 如何写出文本输出

对于文本输出,可以使用PrintWriter。
PrintWriter out = new PrintWriter("employee.txt", "UTF-8");
等同于:
PrintWriter out = new PrintWriter(new FileOutputStream("employee.txt"). "UTF-8");

如果写出器设置为自动冲刷模式,那么只要println被调用,缓冲区中的所有字符都会被发送到它们的目的地,可以通过使用PrintWriter(Writer out, Boolean autoFlush)来启用或禁用自动冲刷机制。

2.2 如何读入文本输入

我们可以使用Scanner类,从任何输入流中构建Scanner对象。
我们也可以将短小的文本文件像下面这样读入到一个字符串中:String content = new String(Files.readAllBytes(path), charset);
如果想要将这个文件一行行地读入,那么可以调用:List<String> lines = Files.readAllLines(path, charset);
如果文件太大,那么可以将行惰性处理为一个Stream< String>对象:

try(Stream<String> lines = Files.lines(path, charset)){
	...
}

在早期的java版本中,处理文本输入的唯一方式就是通过BufferedReader类。它的readLine方法会产生一行文本,或者在无法获得更多的输入时返回null。

InputStream inputStream = ... ;
try(BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))){
	String line;
	while((line = in.readLine()) != null){
		...
	}
}

如今,BufferedReader类又有了一个lines方法,可以产生一个Stream< String>对象

2.3 以文本格式存储对象

示例代码:

package com.java02.day02.textfile;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDate;
import java.util.Scanner;

/**
 * @description:
 * @author: juju
 * @date: 2020-07-09 09:38
 */
public class TextFileTest {
    public static void main(String[] args) throws IOException {
        Employee[] staff = new Employee[3];
        staff[0] = new Employee("Carl Cracker", (double) 75000, 1982, 4, 13);
        staff[1] = new Employee("Harry Hacker", (double) 50000, 1989, 12, 4);
        staff[2] = new Employee("Tony Tester", (double) 40000, 1990, 7, 29);

        try (PrintWriter out = new PrintWriter("employee.dat", "UTF-8")) {
            writeDate(staff, out);
        }

        try (Scanner in = new Scanner(new FileInputStream("employee.dat"), "UTF-8")) {
            Employee[] newStaff = readData(in);
            for (Employee e : newStaff){
                System.out.println(e);
            }
        }

    }

    private static void writeEmployee(PrintWriter out, Employee e) {
        out.println(e.getName() + "|" + e.getSalary() + "|" + e.getHireDate());
    }

    private 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);
    }

    private static void writeDate(Employee[] employees, PrintWriter out) {
        out.println(employees.length);

        for (Employee e : employees) {
            writeEmployee(out, e);
        }
    }

    private static Employee[] readData(Scanner in){
        int n = in.nextInt();
        in.nextLine();

        Employee[] employees = new Employee[n];
        for (int i = 0; i < n; i++){
            employees[i] = readEmployee(in);
        }
        return employees;
    }
}

执行结果:

Carl Cracker|75000.0|1982-04-13
Harry Hacker|50000.0|1989-12-04
Tony Tester|40000.0|1990-07-29

Employee{name='Carl Cracker', salary=75000.0, hireDate=1982-04-13}
Employee{name='Harry Hacker', salary=50000.0, hireDate=1989-12-04}
Employee{name='Tony Tester', salary=40000.0, hireDate=1990-07-29}
2.4 字符编码方式

平台使用的编码方式可以由静态方法Charset.defaultCharset返回。静态方法Charset.availableCharsets会返回所有可用的Charset实例,返回结果是一个从字符集的规范名称到Charset对象的映射表。

StandardCharsets类具有类型为Charset的静态变量,用来表示每种java虚拟机都必须支持的字符编码方式:StandardCharsets.UTF_8
为了获得另一种编码方式的Charset,可以使用静态的forName方法:Charset shiftJIS = Charset.forName("Shift-JIS");
在读入或写出文本时,应该使用Charset对象。例如,我们可以像下面这样将一个字节数组转换为字符串:String str = new String(bytes, StandardCharsets.UTF_8);

3、读写二进制数据

3.1 DataInput 和 DataOutput接口

DataOutput接口定义了下面用于二进制格式写数组、字符、boolean值和字符串的方法:

writeChars | writeByte | writeInt | writeShort | writeLong | writeFloat | writeDouble | writeChar | writeBoolean | writeUTF

为了读回数据,可以使用在DataInput接口中定义的下列方法:

readInt | readShort | readLong | readFloat | readDouble | readChar | readBoolean | readUTF

DataInputStream in = new DataInputStream(new FileInputStream("employee.dat"));
DataOutputStrean out = new DataOutputStream(new FileOutputStream("employee.dat"));

3.2 随机访问文件

RandomAccessFile类可以在文件中的任何位置查找或写入数据。你可以打开一个随机访问文件,只用于读入或者同事用于读写,你可以通过使用字符串“r”(用于读入访问)或“rw”(用于读入/写出访问)作为构造器的第二个参数来指定这个选项。
RandomAccessFile in = new RandomAccessFile("employee.dat", "r");
RandomAccessFile inOut = new RandomAccessFile("employee.dat", "rw");

随机访问文件有一个表示下一个将被读入或写出的字节所处位置的文件指针,seek方法可以用来将这个文件指针设置到文件中的任意字节位置,seek的参数是一个long类型的整数,它的值位于0到文件按照字节来度量的长度之间。
getFilePointer方法将返回文件指针的当前位置。

RandomAccessFile类同时实现了DataInput和DataOutput接口。为了读写随机访问文件,可以使用在前面小节中讨论过的诸如readInt/writeInt和readChar/writeChar之类的方法。

示例代码:

package com.java02.day02.randomaccess;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * @description:
 * @author: ju
 * @date: 2020-07-09 13:33
 */
public 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(int size, DataInput in) throws IOException {
        StringBuffer b = new StringBuffer(size);
        int i = 0;
        boolean more = true;
        while (more && i< size){
            char ch = in.readChar();
            i++;
            if (ch == 0){
                more = false;
            }else {
                b.append(ch);
            }
        }
        in.skipBytes(2 * (size -i));
        return b.toString();
    }
}

package com.java02.day02.randomaccess;

import com.java02.day02.textfile.Employee;

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

/**
 * @description:
 * @author: ju
 * @date: 2020-07-09 13:41
 */
public class RandomAccessTest {
    public static void main(String[] args) throws IOException {
        Employee[] staff = new Employee[3];
        staff[0] = new Employee("Carl Cracker", (double) 75000, 1982, 4, 13);
        staff[1] = new Employee("Harry Hacker", (double) 50000, 1989, 12, 4);
        staff[2] = new Employee("Tony Tester", (double) 40000, 1990, 7, 29);

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

        try (RandomAccessFile in = new RandomAccessFile("employee1.dat", "r")) {
            int n = (int) (in.length()/Employee.RECODE_SIZE);
            Employee[] newStaff = new Employee[n];
            for (int i = 0; i < n; i++){
                newStaff[i] = new Employee();
                //将文件指针设置到距文件pos个字节处
                in.seek(i * Employee.RECODE_SIZE);
                newStaff[i] = readData(in);
            }
            for (Employee e : newStaff){
                System.out.println(e);
            }
        }
    }

    private static void writeData(DataOutput out, Employee e) throws IOException {
        DataIO.writeFixedString(e.getName(), Employee.NAME_SPACE, out);
        out.writeDouble(e.getSalary());
        LocalDate hireDate = e.getHireDate();
        out.writeInt(hireDate.getYear());
        out.writeInt(hireDate.getMonthValue());
        out.writeInt(hireDate.getDayOfMonth());
    }

    private static Employee readData(DataInput in) throws IOException {
        String name = DataIO.readFixedString(Employee.NAME_SPACE, in);
        double salary = in.readDouble();
        int year = in.readInt();
        int month = in.readInt();
        int day = in.readInt();
        return new Employee(name, salary, year, month, day);
    }
}

3.3 ZIP文档

ZIP文档(通常)以压缩格式存储一个或多个文件,每个ZIP文档都有一个头,包含诸如每个文件名字和所使用的压缩方法等信息。在Java中,可以使用ZipInputStream来读入ZIP文档。你可能需要浏览文档中每个单独的项,getNextEntry方法就可以返回一个描述这些项的ZipEntry类型的对象。向ZipInputStream的getInputStream方法传递该项可以获取用于读取该项的输入流。然后调用closeEntry来读入下一项。

ZipInputStream zin = new ZipInputStream(new FileInputStream(zipname));
ZipEntry entry;
while((entry = zin.getNextEntry())!= null){
	InputStream in = zin.getInputStream(entry);
	...
	zin.closeEntry();
}
zin.close();
FileOutputStream fout = new FileOutputStream("test.zip");
ZipOutputStream zout = new ZipOutputStream(fout);
//for all files
{
	ZipEntry ze = new ZipEntry(filename);
	zout.putNextEntry(ze);
	...
	zout.closeEntry();
}
zout.close();

4、对象输入/输出流和序列化

4.1 为克隆使用序列化

示例代码:

public class SerialCloneTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        Employee harry = new Employee("Harry Hacker", (double) 50000, 1989, 12, 4);
        Employee harry2 = (Employee) harry.clone();

        harry.raiseSalary(10);

        System.out.println(harry);
        System.out.println(harry2);
    }
}
public class SerialCloneable implements Serializable, Cloneable {

    @Override
    public Object clone() throws CloneNotSupportedException {
        try {
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            try (ObjectOutputStream out = new ObjectOutputStream(bout)) {
                out.writeObject(this);
            }

            try (InputStream bin = new ByteArrayInputStream(bout.toByteArray())) {
                ObjectInputStream in = new ObjectInputStream(bin);
                return in.readObject();
            }
        } catch (IOException | ClassNotFoundException e) {
            CloneNotSupportedException e2 = new CloneNotSupportedException();
            e2.initCause(e);
            throw e2;
        }
    }
    
}
public class Employee extends SerialCloneable {
    private String name;
    private Double salary;
    private LocalDate hireDate;

    public static final int NAME_SPACE = 20;

    public static final int RECODE_SIZE = 30;

    public Employee() {
    }

    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 +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Double getSalary() {
        return salary;
    }

    public void setSalary(Double salary) {
        this.salary = salary;
    }

    public LocalDate getHireDate() {
        return hireDate;
    }

    public void setHireDate(LocalDate hireDate) {
        this.hireDate = hireDate;
    }

    public void raiseSalary(double byPercent){
        double raise = salary * byPercent / 100;
        salary += raise;
    }
}

5、操作文件

Path和Files类封装了在用户机器上处理文件系统所需的所有功能。例如,Files类可以用来移除或重命名文件,或者查询文件最后被修改的时间。

5.1 Path
Path absolute = Paths.get("/home", "harry");
Path relative = Paths.get("myprog", "conf", "user.properties");

路径不必对应着每个实际存在的文件,它仅仅只是一个抽象的名字序列。当你想要创建文件时,首先需要创建一个路径,然后才调用方法去创建对应的文件。

调用p.resolve(q) 将按照下列规则返回一个路径:
· 如果q是绝对路径,则结果就是q;
· 否则,根据文件系统的规则,将“p后面跟着q”作为结果。

还有一个很方便的方法 resolveSibling,它通过解析指定路径的父路径产生其兄弟路径。例如,如果workPath是/opt/myapp/work,那么下面的调用
Path tempPath = workPath.resolveSibling("temp");
将创建/opt/myapp/temp。

resolve 的对立面是relativize,即调用p.relativize®将产生路径q,而对q进行解析的结果正式r。
normalize方法将移除所有冗余的 . 和 … 部件,规范化/home/cay/…/fred./myprog 将产生/home/fred/myprog。
toAbsolutePath方法将产生给定路径的绝对路径,该绝对路径从根部件开始。

Path类由许多有用的方法用来将路径断开。例如:

Path p = Paths.get("/home", "fred", "myprog.properties");
Path parent = p.getParent(): // /home/fred
Path file = p.getFileName(); // myprog.properties
Path root = p.getRoot(); // /

5.2 读写文件

可以用下面的方式很容易的读取文件的所有内容:
byte[] bytes = Files.readAllBytes(path);
如果想将文件当作字符串读入,那么可以在调用readAllBytes之后执行下面的代码:
String content = new String(bytes, charset);
如果希望将文件当作行序列读入,那么可以调用:
List<String> lines = Files.readAllLines(path, charset);
如果希望写出一个字符串到文件中,可以调用:
Files.write(path, content.getBytes(charset));
向指定文件追加内容,可以调用:
Files.write(path, content.getBytes(charset), StandardOpenOption,APPEND);
还可以将一个行的集合写出到文件中:
Files.write(path, lines);

这些简便方法适用于处理中等长度的文本文件,如果要处理的文件长度比较大,或者是二进制文件,那么还是应该使用所熟知的输入/输出流或者读入器/写出器:

InputStream in = Files.newInputStream(path);
OutputStream out = Files.newOutputStream(path);
Reader in = Files.newBufferedReader(path, charset);
Writer out = Files.newBufferedWriter(path, charset);
5.3 创建文件和目录
创建新目录可以调用:
Files.createDirectory(path);

创建路径中的中间目录,应该使用:
Files.createDireacoties(path)

创建空文件
Files.createFile(path);

在给定位置或者系统指定位置创建临时文件或临时目录:
Path newPath = Files.createTempFile(dir, prefix, suffix);
Path newPath = Files.createTempFile(prefix, suffix);
Path newPath = Files.createTempDirectory(dir, prefix);
Path newPath = Files.createTempDirectory(prefix);
5.4 复制、移动和删除文件
将文件从一个位置复制到另一个位置:
Files.copy(fromPath, toPath);

移动文件(即复制并删除原文件):
Files.move(fromPath, toPath);

如果想要覆盖已有的目标路径,可以使用REPLACE_EXISTING选项。如果想要复制所有的文件属性,可以使用COPY_ATTRIBUTES选项。也可以像下面这样同时选择这两个选项:
Files.copy(fromPath, toPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);

可以将移动操作定义为原子性的,这样就可以保证要么移动操作成功完成,要么源文件继续保持在原来位置。具体可以使用ATOMIC_MOVE选项来实现:
Files.move(fromPath, toPath, StandardCopyOption.ATOMIC_MOVE);

还可以将一个输入流复制到Path中,这表示你想要将该输入流存储到硬盘上。类似地,你可以将一个Path复制到输入流中。可以使用下面的调用:
Files.copy(inputStream, toPath);
Files.copy(fromPath. outputStream);

删除文件可以调用:
Files.delete(path);
如果要删除的文件不存在,这个方法就会抛出异常。因此,可转而使用下面的方法:
boolean deleted = Files.deleteIfExists(path);
该删除方法还可以用来移除空目录。
5.5 获取文件信息
下面的方法都将返回一个boolean值,表示检查路径的某个属性的结果:
· exists
` isHidden
` isReadable, isWritable, isExecutable
` isRegularFile, isDirectory, isSymbolicLink

size方法将返回文件的字节数:
long fileSize = Files.size(path);

getOwner 方法将文件的拥有者作为java.nio.file.attribute.UserPrincipal的一个实例返回。
所有的文件系统都会报告一个基本属性集,它们被封装在BasicFileAttributes接口中:
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
5.6 ZIP文件系统

如果zipname是某个ZIP文件的名字,那么下面的调用:FileSystem fs = FileSystem.newFileSystem(Paths.get(zipname), null);
将建立一个文件系统,它包含ZIP文档中的所有文件。如果知道文件名,那么从ZIP文档中复制出这个文件就会变得很容易:Files.copy(fs.getPath(sourceName), targetPath);

要列出ZIP文档中的所有文件,可以遍历文件树:

FileSystem fs = FileSystem.newFileSystem(Paths.get(zipname), null);
Files.walkFileTree(fs.getPath("/"), new SimpleFileVisitor<Path>(){
	public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException{
		System.out.println(file);
		return FileVisitResult.CONTINUE;
	}
});

6、内存映射文件

6.1 内存映射文件的性能

从文件汇总获得一个通道(channel),通道是用于磁盘文件的一种抽象,它使我们可以访问诸如内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性。
FileChannel channel = FileChannel.open(path, options);
通过调用FileChannel类的map方法从这个通道中获得一个ByteBuffer。你可以指定想要映射的文件区域与映射模式,支持的模式有三种:
· FileChannel.MapMode.READ_ONLY: 所产生的缓冲区是只读的,任何对该缓冲区写入的尝试都会导致ReadOnlyBufferException异常。
· FileChannel.MapMode.READ_WRITE: 所产生的缓冲区是可写的,任何修改都会在某个时刻写回到文件中。注意,其他映射同一个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的确切行为是依赖于操作系统的。
· FileChannle.MapMode.PRIVATE: 所产生的缓冲区是可写的,但是任何修改对这个缓冲区来说都是私有的,不会传播到文件中。

6.2 缓冲区数据结构

在实践中,最常用的将是ByteBuffer和CharBuffer。每个缓冲区都具有:
· 一个容量,它永远不能改变
· 一个读写位置,下一个值将在此进行读写
· 一个界限,超过它进行读写是没有意义的
· 一个可选的标记,用于重复一个读入或写出操作

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

· Buffer clear()
  通过将位置复位到0,并将界限设置到容量,使这个缓冲区为写出做好准备。返回this。
· Buffer flip()
  通过将界限设置到位置,并将位置复位到0,使这个缓冲区为读入做好准备。返回this。
· Buffer rewind()
  通过将读写位置复位到0,并保持界限不变,使这个缓冲区为重新读入相同的值做好准备。返回this。
· Buffer mark()
  将这个缓冲区的标记设置到读写位置,返回this。
· Buffer reset() 
  将这个缓冲区的位置设置到标记,从而允许被标记的部分可以再次被读入或写出,返回this。
· int remaining()
  返回剩余可读入或写出的值的数量,即界限与位置之间的差异。
· int position()
· void position(int newValue)
  返回这个缓冲区的位置。
· int capacity()
  返回这个缓冲区的容量
6.3 文件加锁机制

要锁定一个文件,可以调用FileChannel类的lock或tryLock方法:
FileChannel = FileChannel.open(path);
FileLock lock = channel.lock();FileLock lock = channel.tryLock();

你还可以通过下面的调用锁定文件的一部分:
FileLock lock(long start, long size, boolean shared)FileLock tryLock(long start, long size, boolean shared)
如果shared标志为false,则锁定文件的目的是读写,而如果为true,则这是一个共享锁,它允许多个进程从文件中读入,并阻止任何进程获得独占的锁。并非所有的操作系统都支持共享锁,因此你可能会在请求共享做的时候得到的是独占的锁。调用FileLock类的isShared方法可以查询你所持有的锁的类型。

文件加锁机制是依赖于操作系统的。
· 在某些系统中,文件加锁仅仅是建议性的,它依旧可以向被另一个应用并发锁定的文件执行写操作。
· 在某些系统中,不能在锁定一个文件的同时将其映射到内存中。
· 文件锁是由整个Java虚拟机持有的。
· 在一些系统中,关闭一个通道会释放由Java虚拟机持有的底层文件上的所有锁。
· 在网络文件系统上锁定文件是高度依赖于系统的,因此应该尽量避免。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值