Java核心技术 输入与输出

1.输入/输出流

从其中读入一个字节序列的对象成为输入流,而可以向其中写入一个字节序列的对象成为输出流。这些字节序列的来源地和目的地可以是文件,而且通常是文件,但也可以是网络连接,甚至是内存块。抽象类InputStream和OutPutStream构成了输入/输出(I/O)类层次结构的基础。

面向字节的流不便于处理以Unicode形式存储的信息(字符使用多个字节表示),所以从抽象类Reader和Writer中继承出来一个专门用于处理Unicode字符的单独的类层次结构,这些类基于两字节的Char值,而不是byte值。        

 字节流 字符流​
输入流InputStreamReader​
输出流OutputStreamWriter​

 

读写字节

InputStream类的抽象方法:abstract int read(),这个方法将读入一个字节,并返回读入的字节,或者在遇到输入结尾时返回-1。

FileInputStream从某文件中读入一个字节,而System.in(InputStream的一个子类的预定义对象)却是从标准输入中读入信息,即控制台或重定向文件。

InputStream类还有若干个非抽象的方法,这些方法都要调用抽象的read方法,各个子类都只需覆盖这个方法。

 

OutputStream类定义了抽象方法:abstract void write(int b),它可以向某个输出位置写出一个字节。

read和write方法在执行时都被阻塞,直至字节确实被读入或者写出(如果流不能被立即访问,当前线程将被阻塞)。

available方法可以去检查当前读入的字节数量,下面的代码片段就不会被阻塞:

int bytesAvailable = in.available();
if (bytesAvailable  > 0) {
    byte[] data = new byte[bytesAvailable];
    in.read(data);
}

当完成输入/输出流的读写时,应该通过调用close方法来关闭它,这个调用会释放掉十分有限的操作系统资源。如果打开过多的输入/输出流而没有关闭,那么系统资源将被耗尽。关闭一个输出流的同时还会冲刷用于该输出流的缓冲区:所有被临时置于缓冲区中,以便用更大的包的形式传递的字节在关闭输出流时都被送出。

特别是,如果不关闭文件,那么写出字节的最后一个包可能将永远得不到传递,可以用flush方法来认为冲刷这些输出。

尽管输入输出流都提供了原生的read和write的某些具体方法,但是很少使用它们,大家感兴趣的数据包括数字,字符串和对象,而不是字节。

FileInputStream in = new FileInputStream("1.txt");
int b;
while ((b = in.read()) != -1) {
    System.out.println(b + " " + (char) b);
}
in.close();
byte[] bytes = {122,104,97,110,103};
FileOutputStream out = new FileOutputStream("2.txt");
out.write(bytes);
out.close();

完整的流家族

按照使用方法来进行划分,处理字节和字符的两个单独的层次结构。InputStream和OutputStream可以读写单个字节或字节数组。想要读写字符串和数字,需要功能更强大的子类。

对于Unicode文本,可以使用抽象类Reader和Writer,与输入输出流类似,read方法将返回一个Unicode码元(0~65535的整数)。write需要传递一个Unicode码元。

还有四个附加的接口:Closeable、Flushable、Readable和Appendable。前面两个接口分别拥有void close() throw IOException和void flush()。

InputStream、OutputStream、Reader和Writer都实现了Closeable接口,而OutputStream和Writer还实现了Flushable接口。

Readable接口只有一个方法int read(CharBuffer cb),CharBuffer类拥有按顺序和随机地进行读写访问的方法,它表示一个内存中的缓冲区或者一个内存映像的文件。

Appendable接口有两个用于添加单个字符和字符序列的方法:

Appendable append(char c)

Appendable append(CharSequence s)

CharSequence接口描述了一个char值序列的基本属性,String、CharBuffer、StringBuilder和StringBuffer都实现了它。

在流类的家族中,只有Wirter实现了Appendable。

组合输入/输出流过滤器

FileInputStream和FileOutputStream可以提供附着在一个磁盘文件上的输入输出流,只需向构造器提供文件名:

FileInputStream fin = new FileInputStream("employee.dat");

由于反斜杠字符在java中是转义字符,Windows风格的路径名可以使用\\或者/。

与抽象类InputStream和OutputStream一样,这些类只支持字节级别的读写。

然而DataInputStream就只能读入数值类型:

DataInputStream din = ...;
double x = din.readDouble();

FileInputStream没有任何读入数值类型的方法,DataInputStream也没有任何从文件中获取数据的方法,必须对二者进行组合:

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

可以通过嵌套过滤器来添加多重功能。输入流默认情况下不被缓冲区缓存,每个对read的调用都会请求操作系统在分发一个字节。相比之下数据块置于缓冲区中会更高效:

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

 

有时当多个输入流链接在一起时,需要跟踪各个中介输入流(intermediate inputstream):

PushbackInputStream pbin = new PushbackInputStream(new BufferedInputStream(
   new FileInputStream("employee.dat")));
int b = pbin.read();
if (b != '<') {
    pbin.unread(b);
}

预读下一个字节,并非所期望的值时,将其推回流中。

但是读入和推回是可应用于可回推(pushback)输入流的仅有的方法。如果希望预先浏览并且还可以读入数字:

DataInputStream din = new DataInputStream(pbin = new PushbackInputStream(new BufferedInputStream(
        new FileInputStream("employee.dat"))));

这种混合并匹配过滤器类以构建真正有用的输入输出流序列的能力,将带来极大的灵活性。

ZipInputStream zin = new ZipInputStream(new FileInputStream("employee.zip"));
DataInputStream in = new DataInputStream(zin);

 

2.文本输入与输出

在保存数据时,可以选择二进制格式或文本格式。例如整数1234,二进制由字节00 00 04 D2构成的序列(十六进制表示法),而文本存成字符串“1234”。尽管二进制IO高效但是不宜人来阅读。

在存储文本字符串时,需要考虑字符编码(character encoding)方式。在Java内部使用UTF-16编码方式,字符串1234编码为00 31 00 32 00 33 00 34 十六进制。许多程序希望文本按照其他的编码方式编码,UTF-8是最常用的的编码方式,字符串讲写成4A 6F 73 C3 A9,其中并没有用于前3个字母的任何0字节,而字符é占用了两个字节。

OutputStreamWriter类将使用选定的字符编码方式,把Unicode码元的输出流转换为字节流。而InputStreamReader类将包含字节(用某种字符编码方式表示的字符)的输入流转换为可以产生Unicode码元的读入器。

Reader in = new InputStreamReader(System.in);

这个输入流读入器会假定使用主机系统所使用的默认字符编码方式。应该总是在InputStreamReader的构造器中选择一种具体的编码方式:

Reader in = new InputStreamReader(new FileInputStream("data.txt"), StandardCharsets.UTF_8);

如何写出文本输出

对于文本输出,可以使用PrintWriter。这个类拥有以文本格式打印字符串和数字的方法,它还有一个将PrintWriter链接到FileWriter的便捷方法:

PrintWriter out = new PrintWriter("employee.txt", "UTF-8");
// 等于
PrintWriter out_p = new PrintWriter(new FileOutputStream("employee.txt"), "UTF-8");

为了输出到打印写出器,需要使用与System.out相同的方法:

String name = "Harry Hacker";
double salary = 75000;
out.print(name);
out.print(' ');
out.println(salary);
out.close();

输出到写入器out,之后这些字符将会被转换成字节并最终写入employee.txt中。println方法在行中添加了对目标系统来说恰当的行结束符(Windows系统是“\r\n”,Unix系统是“\n”)。

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

PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream("employee.txt"), "UTF-8"), true);

print方法不抛出异常,可以调用checkError方法来查看输出流是否出现了某些错误。

如何读入文本输出

最简单的方式是使用Scanner类,可以从任意输入流中构建Scanner对象。

也可以将短小文本文件读入到一个字符串:

String content = new String(Files.readAllBytes(path), charset);

一行行读入:

List<String> lines = Files.readAllLines(path, charset);

文件过大,可以将行惰性处理为一个Stream< String >对象:

Stream<String> lines = Files.lines(path, charset);

早期的Java版本中,处理文本输入的唯一方式是通过BufferedReader类,它的readLine方法会产生一行文本:

InputStream inputStream = new FileInputStream("f.txt");
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line;
while ((line = in.readLine()) != null) {
    System.out.println(line);
}

如今,BufferedReader类又有了一个lines方法,可以产生一个Stream< String >对象。但与Scanner不同,BufferedReader没有任何用于读入数字的方法。

 

以文本格式存储对象

Employee记录数组存储成一个文本文件:

Harry Hacker|35500|2019-12-20

需要使用PrintWriter类:

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

为了读入记录,每次读入一行然后分离所有字段,用String.split方法将这一行断开成一组标记:

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);
}
public class TextFileTest {

    public static void main(String[] args) throws IOException {
        Employee[] staff = new Employee[3];
        staff[0] = new Employee("Carl Cracker", 75000, 2019, 12, 20);
        staff[1] = new Employee("Harry Hacker", 50000, 2019, 12, 19);
        staff[2] = new Employee("Tony Tester", 40000, 2019, 12, 18);

        try (PrintWriter out = new PrintWriter("employee.dat", "UTF-8")) {
            writeData(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 writeData(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();
        // nextInt读入的是数组长度,但不包括行尾的换行字符,必须调用nextLine方法获得下一行输入
        in.nextLine();

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

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

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

字符编码方式

输入流和输出流都用于字节序列,但许多情况下操作的是文本即字符序列。字符如何编码成字节?

Java字符使用Unicode标准。每个字符或编码都具有一个21位的整数。有多种不同的字符编码方式,也就是说,将这些21位数字包装成字节的方式有很多种。

最常见的编码方式是UTF-8,它将每个Unicode编码点编码为1到4个字节的序列。UTF-8的好处是传统的包含了英语中用到的所有字符的ASCⅡ字符集中的每个字符都只会占用一个字节。

UTF-16会将每个Unicode编码点编码为1个或2个16位值。这是一种在Java字符串中使用的编码方式。

ISO 8859-1是一种单字节编码,包含了西欧各种语言中用到的带有重音符号的字符。Shift-JIS是一种用于日文字符的可变长编码。

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

StandardCharsets类具有类型为Charset的静态变量,用于表示虚拟机必须支持的字符编码方式:

StandardCharsets.US_ASCII
StandardCharsets.ISO_8859_1
StandardCharsets.UTF_8
StandardCharsets.UTF_16BE
StandardCharsets.UTF_16LE
StandardCharsets.UTF_16

为了获得另一种编码方式的Charset,可以使用静态forName方法:

Charset shiftJIS = Charset.forName("Shift-JIS");

在读入或写出文本时,应该使用Charset对象:

String str = new String(bytes, StandardCharsets.UTF_8);

在不指定任何编码方式时,有些方法(例如String(byte[]))会使用默认平台编码方式,而其他方法(例如Files.readAllLines)会使用UTF-8。

 

3.读写二进制数据

DataInput和DataOutput接口

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

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

writeInt总是将一个整数写出为4字节的二进制数量值。对于给定类型的每个值,所需空间相同,而且将其读回也比解析文本要更快。

writeUFT方法用修订版的8位Unicode转换格式写出字符串(与标准UFT-8不同,Unicode码元序列首先用UTF-16表示,其结果之后用UTF-8进行编码)。为了向后兼容在Unicode还没有超过16位时构建的虚拟机。因为没有其他方法会使用UTF-8的这种修订,所以只在写出用于java虚拟机的字符串时才使用writeUTF,例如当编写一个生产字节码的程序时。其他场合都应该使用wirteChars方法。

为了读回数据,DateInput接口中定义了方法:

readByte, readInt, readShort, readLong, readFloat, readDouble, readChar, readBoolean, readUTF

DataInputStream类实现了DataInput接口,为了从文件中读入二进制数据,可以将DataInputStream与某个字节源结合:

DateInputStream in = new DateInputStream(new FileInputStream("employee.dat"));

写入二进制数据,与此类似。

 

随机访问文件

RandomAccessFile类可以在文件中的任何位置查找或写入数据。磁盘文件都是随机访问的。可以打开一个随机文件,用于读入访问(r)同时用于读写(rw)。

RandomAccessFile in = RandomAccessFile("employee.dat", "r");
RandomAccessFile inOut = RandomAccessFile("employee.dat", "rw");

当将已有文件做为RandomAccessFile打开时,这个文件并不会删除。

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

getFilePointer返回文件指针的当前位置。

RandomAccessFile同时实现了DataInput和DataOutput。

将雇员记录存储到随机访问的文件中:每条记录都有一样的大小,很容易读入任何记录。

将文件指针置于第三条纪录:

long n = 3;
in.seek((n - 1) * RECORD_SIZE);
Employee e = new Employee()
e.readDate(in);

总记录数:

long nbytes = in.length();
int nrecords = (int) (nbytes  / RECOED_SIZE);

整数和浮点数都有固定尺寸。但是字符串就需要助手方法来读写具有固定尺寸的字符串:

writeFixedString写出从字符串开头开始的指定数量的码元(如果码元过少,用0值补齐字符串)。

readFixedString从输入流中读入字符,直至读入size个码元,或遇到具有0值的字符值,然后跳过输入字段中剩余的0值。为了提高效率这个方法使用StringBuilder类来读入字符串。

使用40个字符来表示姓名,因此每条记录包含100个字节:

40字符=80字节,用于姓名

1 double=8字节,用于薪水

3 int=12字节,用于日期

public static final int NAME_SIZE = 40;
public static final int RECODE_SIZE = 2 * NAME_SIZE + 8 + 4 + 4 + 4;

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.writeChar(ch);
        }
    }
    public static String readFixedString(int size, DataInput in) throws IOException {
        StringBuilder b = new StringBuilder(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();
    }
}

public class RandomAccessTest {

    public static void main(String[] args) throws IOException {
        Employee[] staff = new Employee[3];
        staff[0] = new Employee("Carl Cracker", 75000, 2019, 12, 20);
        staff[1] = new Employee("Harry Hacker", 50000, 2019, 12, 19);
        staff[2] = new Employee("Tony Tester", 40000, 2019, 12, 18);

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

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

            for (int i = n - 1; i >= 0; i--) {
                newStaff[i] = new Employee();
                in.seek(i * Employee.RECODE_SIZE);
                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 hireDay = e.getHireDay();
        out.writeInt(hireDay.getYear());
        out.writeInt(hireDay.getMonthValue());
        out.writeInt(hireDay.getDayOfMonth());
    }

    public static Employee readData(DataInput in) throws IOException {
        String name = DataIO.readFixedString(Employee.NAME_SIZE, in);
        double salary = in.readDouble();
        int y = in.readInt();
        int m = in.readInt();
        int d = in.readInt();
        return new Employee(name, salary, y, m - 1, d);
    }
}

ZIP文档

ZIP文档以压缩格式存储了一个或多个文件,每个ZIP文档都有一个头,包含诸如每个文件名字和所使用的压缩方法等信息。

Java中,使用ZipInputStream来读入ZIP文档。getNextEntry方法可以返回一个描述文档中每个项的ZipEntry类型的对象。向ZipInputStream的geiInputStream方法传递该项可以获取用于读取该项的输入流。然后调用closeEntry来读入下一项:

ZipFile zf = new ZipFile("first.zip");
ZipInputStream zin = new ZipInputStream(new FileInputStream("first.zip"));
ZipEntry entry;
while ((entry = zin.getNextEntry()) != null) {
    InputStream inputStream = zf.getInputStream(entry);
    BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
    String line;
    while ((line = in.readLine()) != null) {
        System.out.println(line);
    }
    zin.closeEntry();
}
zin.close();

写入Zip文件使用ZipOutputStream,希望放入Zip文件都要创建一个ZipEntry对象,并将文件名传递给ZipEntry构造器,它将设置文件日期和解压方法等参数(可以覆盖这些设置)。然后调用putNextEntry来开始写出新文件,并将文件发送给ZIP输出流中。当完成时,需要调用closeEntry:

FileOutputStream fout = new FileOutputStream("haha.zip");
ZipOutputStream zout = new ZipOutputStream(fout);
for (int i = 0; i < 3; i++) {
    ZipEntry ze = new ZipEntry("hello" + i + ".txt");
    zout.putNextEntry(ze);
    byte[] bytes = {122,104,97,110,103};
    zout.write(bytes);
    zout.closeEntry();
}
zout.close();

JAR文件只是带有一个特殊项的ZIP文件,这个项称作清单(JarInput/OutputStream)。

ZIP输入流是一个能够展示流的抽象化的强大之处的实例。当读入以压缩格式存储的数据时,不必担心边请求边解压数据的问题。而且ZIP格式的字节源并非必须是文件,也可以是来自网络连接的ZIP数据。事实上,当Applet类加载器读入JAR文件时,就是在读入和解压来自网络的数据。

 

一个有趣的程序:

public class TestFileWriter {
    public static void main(String args[]) {
        FileWriter fw = null;
        try {
            fw = new FileWriter("E:/2.txt");
            for (int c = 0; c <= 50000; c++) {
                fw.write(c);
            }
            fw.close();
        } catch (IOException e1) {
            e1.printStackTrace();
            System.out.println("文件写入错误");
            System.exit(-1);
        }
    }
}
 

结果是:生成文件​B.txt,包含50000个不同地区的字符,如下:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值