Java
Java IO

Java IO 27 - RandomAccessFile详解

简介:流是一组有顺序的,有起点和终点的字节集合。是对设备文件间数据传输的总称和抽象。

1. RandomAccessFile简介

RandomAccessFile是随机访问文件(包括读/写)的类,它支持对文件随机访问的读取和写入,即可以从指定的位置读取/写入文件数据。需要注意的是,RandomAccessFile虽然属于java.io包,但它不是InputStream或者OutputStream的子类,它也不同于FileInputStream和FileOutputStream。FileInputStream只能对文件进行读操作,FileOutputStream只能对文件进行写操作,而RandomAccessFile同时支持文件的读和写,并且它支持随机访问。

2. RandomAccessFile函数列表

  • RandomAccessFile(File file, String mode)
  • RandomAccessFile(String fileName, String mode)
  • synchronized final FileChannel getChannel()
  • final FileDescriptor getFD()
  • long getFilePointer()
  • long length()
  • int read(byte[] buffer, int byteOffset, int byteCount)
  • int read(byte[] buffer)
  • int read()
  • final boolean readBoolean()
  • final byte readByte()
  • final char readChar()
  • final double readDouble()
  • final float readFloat()
  • final void readFully(byte[] dst)
  • final void readFully(byte[] dst, int offset, int byteCount)
  • final int readInt()
  • final String readLine()
  • final long readLong()
  • final short readShort()
  • final String readUTF()
  • final int readUnsignedByte()
  • final int readUnsignedShort()
  • void seek(long offset)
  • void setLength(long newLength)
  • int skipBytes(int count)
  • void write(int oneByte)
  • void write(byte[] buffer, int byteOffset, int byteCount)
  • void write(byte[] buffer)
  • final void writeBoolean(boolean val)
  • final void writeByte(int val)
  • final void writeBytes(String str)
  • final void writeChar(int val)
  • final void writeChars(String str)
  • final void writeDouble(double val)
  • final void writeFloat(float val)
  • final void writeInt(int val)
  • final void writeLong(long val)
  • final void writeShort(int val)
  • final void writeUTF(String str)
  • void close()

3. RandomAccessFile模式说明

RandomAccessFile共有4种模式:rrwrwsrwd,解释如下:

  • r:以只读方式打开。调用结果对象的任何write方法都将导致抛出IOException。
  • rw:打开以便读取和写入。
  • rws:打开以便读取和写入,相对于rwrws还要求对文件的内容或元数据的每个更新都同步写入到基础存储设备。
  • rwd:打开以便读取和写入,相对于rwrwd还要求对文件的内容的每个更新都同步写入到基础存储设备。

对于上述模式,有以下的两个问题:

  1. 问题一:什么是元数据,即metadata?英文解释如下:

The definition of metadata is “data about other data.” With a file system, the data is contained in its files and directories, and the metadata tracks information about each of these objects: Is it a regular file, a directory, or a link? What is its size, creation date, last modified date, file owner, group owner, and access permissions?

解释:metadata是关于数据的数据,可以理解为数据的属性。在文件系统中,数据被包含在文件和文件夹中;metadata信息包括:数据的类型(是一个文件,一个目录还是一个链接)、数据的创建时间(简称ctime)、最后一次修改时间(简称mtime)、数据拥有者、数据拥有群组、访问权限等等。

  1. 问题二:rwrwsrwd 的区别。

当操作的文件是存储在本地的基础存储设备上时(如硬盘, NandFlash等),rwsrwd, rw 才有区别。

  • 当模式是rws并且操作的是基础存储设备上的文件;每次更改文件内容时,如write()写入数据,或修改文件元数据时,如文件的mtime,都会将这些改变同步到基础存储设备上。
  • 当模式是rwd并且操作的是基础存储设备上的文件;每次更改文件内容时,如write()写入数据,都会将这些改变同步到基础存储设备上。
  • 当模式是rw并且操作的是基础存储设备上的文件;关闭文件时,会将文件内容的修改同步到基础存储设备上。至于更改文件内容时是否会立即同步取决于系统底层实现。

4. RandomAccessFile源码解析

下面是RandomAccessFile的源码,基于JDK 1.7.0_07:

  • package java.io;
  • import java.nio.channels.FileChannel;
  • import sun.nio.ch.FileChannelImpl;
  • public class RandomAccessFile implements DataOutput, DataInput, Closeable {
  • // 文件描述符
  • private FileDescriptor fd;
  • // 文件通道
  • private FileChannel channel = null;
  • // 记录是否是rw模式
  • private boolean rw;
  • private Object closeLock = new Object();
  • // 是否已关闭
  • private volatile boolean closed = false;
  • // 四类模式的常量表示
  • private static final int O_RDONLY = 1;
  • private static final int O_RDWR = 2;
  • private static final int O_SYNC = 4;
  • private static final int O_DSYNC = 8;
  • public RandomAccessFile(String name, String mode)
  • throws FileNotFoundException
  • {
  • this(name != null ? new File(name) : null, mode);
  • }
  • public RandomAccessFile(File file, String mode)
  • throws FileNotFoundException
  • {
  • String name = (file != null ? file.getPath() : null);
  • // 处理模式
  • int imode = -1;
  • if (mode.equals("r"))
  • imode = O_RDONLY;
  • else if (mode.startsWith("rw")) {
  • imode = O_RDWR;
  • rw = true;
  • if (mode.length() > 2) {
  • if (mode.equals("rws"))
  • imode |= O_SYNC;
  • else if (mode.equals("rwd"))
  • imode |= O_DSYNC;
  • else
  • imode = -1;
  • }
  • }
  • if (imode < 0)
  • throw new IllegalArgumentException("Illegal mode \"" + mode
  • + "\" must be one of "
  • + "\"r\", \"rw\", \"rws\","
  • + " or \"rwd\"");
  • SecurityManager security = System.getSecurityManager();
  • if (security != null) {
  • security.checkRead(name);
  • if (rw) {
  • security.checkWrite(name);
  • }
  • }
  • if (name == null) {
  • throw new NullPointerException();
  • }
  • fd = new FileDescriptor();
  • fd.incrementAndGetUseCount();
  • // 以指定模式打开文件
  • open(name, imode);
  • }
  • public final FileDescriptor getFD() throws IOException {
  • if (fd != null) return fd;
  • throw new IOException();
  • }
  • // 获取文件通道
  • public final FileChannel getChannel() {
  • synchronized (this) {
  • if (channel == null) {
  • channel = FileChannelImpl.open(fd, true, rw, this);
  • /**
  • * 将文件描述符的使用计数加1
  • * 文件描述符可以被多个流共享,
  • * 为了保证文件描述符在流或通道全部完成使用时才能够正确地被GC回收
  • * 使用一个计数器来记录当前正在使用文件描述符的实例个数
  • * 当流或通道关闭时会将计数器减1
  • */
  • fd.incrementAndGetUseCount();
  • }
  • return channel;
  • }
  • }
  • // 打开文件,本地方法
  • private native void open(String name, int mode)
  • throws FileNotFoundException;
  • // 读取一个字节,本地方法
  • public native int read() throws IOException;
  • // 读取len个字节到b中off开始的位置,本地方法
  • private native int readBytes(byte b[], int off, int len) throws IOException;
  • // 读取len个字节到b中off开始的位置
  • public int read(byte b[], int off, int len) throws IOException {
  • return readBytes(b, off, len);
  • }
  • // 读取多个字节填满b
  • public int read(byte b[]) throws IOException {
  • return readBytes(b, 0, b.length);
  • }
  • // 读取多个字节填满b
  • public final void readFully(byte b[]) throws IOException {
  • readFully(b, 0, b.length);
  • }
  • // 读取len个字节到b中off开始的位置
  • public final void readFully(byte b[], int off, int len) throws IOException {
  • int n = 0;
  • do {
  • int count = this.read(b, off + n, len - n);
  • if (count < 0)
  • throw new EOFException();
  • n += count;
  • } while (n < len);
  • }
  • // 跳过n个字节
  • public int skipBytes(int n) throws IOException {
  • long pos;
  • long len;
  • long newpos;
  • if (n <= 0) {
  • return 0;
  • }
  • pos = getFilePointer();
  • len = length();
  • newpos = pos + n;
  • if (newpos > len) {
  • newpos = len;
  • }
  • seek(newpos);
  • /* return the actual number of bytes skipped */
  • return (int) (newpos - pos);
  • }
  • // 写出一个字节,本地方法
  • public native void write(int b) throws IOException;
  • // 将b中从off位置开始的len个字节进行写出,本地方法
  • private native void writeBytes(byte b[], int off, int len) throws IOException;
  • // 将b中所有字节进行写出
  • public void write(byte b[]) throws IOException {
  • writeBytes(b, 0, b.length);
  • }
  • // 将b中从off位置开始的len个字节进行写出
  • public void write(byte b[], int off, int len) throws IOException {
  • writeBytes(b, off, len);
  • }
  • // 获取文件中指针的当前位置,本地方法
  • public native long getFilePointer() throws IOException;
  • // 定位到文件的某个位置,本地方法
  • public native void seek(long pos) throws IOException;
  • // 文件长度,本地方法
  • public native long length() throws IOException;
  • /**
  • * 设置文件长度:
  • * 如果设置的长度大于原始长度,将扩展文件长度,新增加的长度使用默认值填充;
  • * 如果设置的长度小于原始长度,将截取文件的前newLength字节,且如果文件的偏移量大于newLength则将文件的偏移量设为newLength;
  • */
  • public native void setLength(long newLength) throws IOException;
  • public void close() throws IOException {
  • synchronized (closeLock) {
  • if (closed) {
  • return;
  • }
  • closed = true;
  • }
  • if (channel != null) {
  • // 将文件描述符计数器减1
  • fd.decrementAndGetUseCount();
  • channel.close();
  • }
  • // 将文件描述符计数器减1
  • fd.decrementAndGetUseCount();
  • close0();
  • }
  • // 读取bool值,底层是以0或大于0的值来存储的
  • public final boolean readBoolean() throws IOException {
  • int ch = this.read();
  • if (ch < 0)
  • throw new EOFException();
  • return (ch != 0);
  • }
  • // 读取一个字节
  • public final byte readByte() throws IOException {
  • int ch = this.read();
  • if (ch < 0)
  • throw new EOFException();
  • return (byte)(ch);
  • }
  • // 读取一个无符号字节
  • public final int readUnsignedByte() throws IOException {
  • int ch = this.read();
  • if (ch < 0)
  • throw new EOFException();
  • return ch;
  • }
  • // 读取一个短整型,底层使用大端存储法
  • public final short readShort() throws IOException {
  • int ch1 = this.read();
  • int ch2 = this.read();
  • if ((ch1 | ch2) < 0)
  • throw new EOFException();
  • return (short)((ch1 << 8) + (ch2 << 0));
  • }
  • // 读取一个无符号短整型,底层使用大端存储法,2个字节表示
  • public final int readUnsignedShort() throws IOException {
  • int ch1 = this.read();
  • int ch2 = this.read();
  • if ((ch1 | ch2) < 0)
  • throw new EOFException();
  • return (ch1 << 8) + (ch2 << 0);
  • }
  • // 读取一个字符,底层使用大端存储法,2个字节表示
  • public final char readChar() throws IOException {
  • int ch1 = this.read();
  • int ch2 = this.read();
  • if ((ch1 | ch2) < 0)
  • throw new EOFException();
  • return (char)((ch1 << 8) + (ch2 << 0));
  • }
  • // 读取一个整型,底层使用大端存储法,4个字节表示
  • public final int readInt() throws IOException {
  • int ch1 = this.read();
  • int ch2 = this.read();
  • int ch3 = this.read();
  • int ch4 = this.read();
  • if ((ch1 | ch2 | ch3 | ch4) < 0)
  • throw new EOFException();
  • return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
  • }
  • // 读取一个长整型,两次读取int值然后拼接在一起,底层使用大端存储法,8个字节表示
  • public final long readLong() throws IOException {
  • return ((long)(readInt()) << 32) + (readInt() & 0xFFFFFFFFL);
  • }
  • // 读取一个浮点型,底层使用大端存储法,以int的形式存储,4个字节表示
  • public final float readFloat() throws IOException {
  • return Float.intBitsToFloat(readInt());
  • }
  • // 读取一个双精度浮点型,底层使用大端存储法,以long的形式存储,8个字节表示
  • public final double readDouble() throws IOException {
  • return Double.longBitsToDouble(readLong());
  • }
  • // 读取一行
  • public final String readLine() throws IOException {
  • StringBuffer input = new StringBuffer();
  • int c = -1;
  • boolean eol = false;
  • while (!eol) {
  • switch (c = read()) {
  • case -1:
  • case '\n':
  • eol = true;
  • break;
  • case '\r':
  • eol = true;
  • long cur = getFilePointer();
  • if ((read()) != '\n') {
  • seek(cur);
  • }
  • break;
  • default:
  • input.append((char)c);
  • break;
  • }
  • }
  • if ((c == -1) && (input.length() == 0)) {
  • return null;
  • }
  • return input.toString();
  • }
  • // 读取字符串
  • public final String readUTF() throws IOException {
  • return DataInputStream.readUTF(this);
  • }
  • // 写出bool值
  • public final void writeBoolean(boolean v) throws IOException {
  • write(v ? 1 : 0);
  • //written++;
  • }
  • // 写出一个字节
  • public final void writeByte(int v) throws IOException {
  • write(v);
  • //written++;
  • }
  • // 写出一个短整型
  • public final void writeShort(int v) throws IOException {
  • write((v >>> 8) & 0xFF);
  • write((v >>> 0) & 0xFF);
  • //written += 2;
  • }
  • // 写出一个字符,2个字节
  • public final void writeChar(int v) throws IOException {
  • write((v >>> 8) & 0xFF);
  • write((v >>> 0) & 0xFF);
  • //written += 2;
  • }
  • // 写出一个整型
  • public final void writeInt(int v) throws IOException {
  • write((v >>> 24) & 0xFF);
  • write((v >>> 16) & 0xFF);
  • write((v >>> 8) & 0xFF);
  • write((v >>> 0) & 0xFF);
  • //written += 4;
  • }
  • // 写出一个长整型
  • public final void writeLong(long v) throws IOException {
  • write((int)(v >>> 56) & 0xFF);
  • write((int)(v >>> 48) & 0xFF);
  • write((int)(v >>> 40) & 0xFF);
  • write((int)(v >>> 32) & 0xFF);
  • write((int)(v >>> 24) & 0xFF);
  • write((int)(v >>> 16) & 0xFF);
  • write((int)(v >>> 8) & 0xFF);
  • write((int)(v >>> 0) & 0xFF);
  • //written += 8;
  • }
  • // 写出一个浮点型
  • public final void writeFloat(float v) throws IOException {
  • writeInt(Float.floatToIntBits(v));
  • }
  • // 写出一个双精度浮点型
  • public final void writeDouble(double v) throws IOException {
  • writeLong(Double.doubleToLongBits(v));
  • }
  • // 将字符串以字节形式写出
  • public final void writeBytes(String s) throws IOException {
  • int len = s.length();
  • byte[] b = new byte[len];
  • s.getBytes(0, len, b, 0);
  • writeBytes(b, 0, len);
  • }
  • // 将字符串以字符形式写出
  • public final void writeChars(String s) throws IOException {
  • int clen = s.length();
  • int blen = 2*clen;
  • byte[] b = new byte[blen];
  • char[] c = new char[clen];
  • s.getChars(0, clen, c, 0);
  • for (int i = 0, j = 0; i < clen; i++) {
  • b[j++] = (byte)(c[i] >>> 8);
  • b[j++] = (byte)(c[i] >>> 0);
  • }
  • writeBytes(b, 0, blen);
  • }
  • // 写出字符串
  • public final void writeUTF(String str) throws IOException {
  • DataOutputStream.writeUTF(str, this);
  • }
  • // 初始化ID
  • private static native void initIDs();
  • private native void close0() throws IOException;
  • static {
  • initIDs();
  • }
  • }

从源码可以得知,RandomAccessFile的大部分实现都使用了本地方法,在可知的源码中可以得知,RandomAccessFile在读写数据时都是以字节的方式进行操作,并且在存储数据时使用大端存储法。针对RandomAccessFile的某些本地方法,这里给出OpenJDK提供的RandomAccessFile.c源文件(位于)的代码,这些本地方法都是以C的方式来操作文件IO的:

注:该文件的路径为./openjdk/jdk/src/share/native/java/io/RandomAccessFile.c

  • /*
  • * Copyright (c) 1997, 2010, Oracle and/or its affiliates. All rights reserved.
  • * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  • *
  • * This code is free software; you can redistribute it and/or modify it
  • * under the terms of the GNU General Public License version 2 only, as
  • * published by the Free Software Foundation. Oracle designates this
  • * particular file as subject to the "Classpath" exception as provided
  • * by Oracle in the LICENSE file that accompanied this code.
  • *
  • * This code is distributed in the hope that it will be useful, but WITHOUT
  • * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  • * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  • * version 2 for more details (a copy is included in the LICENSE file that
  • * accompanied this code).
  • *
  • * You should have received a copy of the GNU General Public License version
  • * 2 along with this work; if not, write to the Free Software Foundation,
  • * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  • *
  • * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  • * or visit www.oracle.com if you need additional information or have any
  • * questions.
  • */
  • #include "jni.h"
  • #include "jni_util.h"
  • #include "jlong.h"
  • #include "jvm.h"
  • #include "io_util.h"
  • #include "io_util_md.h"
  • #include "java_io_RandomAccessFile.h"
  • #include <fcntl.h>
  • /*
  • * static method to store field ID's in initializers
  • */
  • jfieldID raf_fd; /* id for jobject 'fd' in java.io.RandomAccessFile */
  • JNIEXPORT void JNICALL
  • Java_java_io_RandomAccessFile_initIDs(JNIEnv *env, jclass fdClass) {
  • raf_fd = (*env)->GetFieldID(env, fdClass, "fd", "Ljava/io/FileDescriptor;");
  • }
  • JNIEXPORT void JNICALL
  • Java_java_io_RandomAccessFile_open(JNIEnv *env,
  • jobject this, jstring path, jint mode)
  • {
  • int flags = 0;
  • if (mode & java_io_RandomAccessFile_O_RDONLY)
  • flags = O_RDONLY;
  • else if (mode & java_io_RandomAccessFile_O_RDWR) {
  • flags = O_RDWR | O_CREAT;
  • if (mode & java_io_RandomAccessFile_O_SYNC)
  • flags |= O_SYNC;
  • else if (mode & java_io_RandomAccessFile_O_DSYNC)
  • flags |= O_DSYNC;
  • }
  • fileOpen(env, this, path, raf_fd, flags);
  • }
  • JNIEXPORT jint JNICALL
  • Java_java_io_RandomAccessFile_read0(JNIEnv *env, jobject this) {
  • return readSingle(env, this, raf_fd);
  • }
  • JNIEXPORT jint JNICALL
  • Java_java_io_RandomAccessFile_readBytes0(JNIEnv *env,
  • jobject this, jbyteArray bytes, jint off, jint len) {
  • return readBytes(env, this, bytes, off, len, raf_fd);
  • }
  • JNIEXPORT void JNICALL
  • Java_java_io_RandomAccessFile_write0(JNIEnv *env, jobject this, jint byte) {
  • writeSingle(env, this, byte, JNI_FALSE, raf_fd);
  • }
  • JNIEXPORT void JNICALL
  • Java_java_io_RandomAccessFile_writeBytes0(JNIEnv *env,
  • jobject this, jbyteArray bytes, jint off, jint len) {
  • writeBytes(env, this, bytes, off, len, JNI_FALSE, raf_fd);
  • }
  • JNIEXPORT jlong JNICALL
  • Java_java_io_RandomAccessFile_getFilePointer(JNIEnv *env, jobject this) {
  • FD fd;
  • jlong ret;
  • fd = GET_FD(this, raf_fd);
  • if (fd == -1) {
  • JNU_ThrowIOException(env, "Stream Closed");
  • return -1;
  • }
  • if ((ret = IO_Lseek(fd, 0L, SEEK_CUR)) == -1) {
  • JNU_ThrowIOExceptionWithLastError(env, "Seek failed");
  • }
  • return ret;
  • }
  • JNIEXPORT jlong JNICALL
  • Java_java_io_RandomAccessFile_length(JNIEnv *env, jobject this) {
  • FD fd;
  • jlong cur = jlong_zero;
  • jlong end = jlong_zero;
  • fd = GET_FD(this, raf_fd);
  • if (fd == -1) {
  • JNU_ThrowIOException(env, "Stream Closed");
  • return -1;
  • }
  • if ((cur = IO_Lseek(fd, 0L, SEEK_CUR)) == -1) {
  • JNU_ThrowIOExceptionWithLastError(env, "Seek failed");
  • } else if ((end = IO_Lseek(fd, 0L, SEEK_END)) == -1) {
  • JNU_ThrowIOExceptionWithLastError(env, "Seek failed");
  • } else if (IO_Lseek(fd, cur, SEEK_SET) == -1) {
  • JNU_ThrowIOExceptionWithLastError(env, "Seek failed");
  • }
  • return end;
  • }
  • JNIEXPORT void JNICALL
  • Java_java_io_RandomAccessFile_seek(JNIEnv *env,
  • jobject this, jlong pos) {
  • FD fd;
  • fd = GET_FD(this, raf_fd);
  • if (fd == -1) {
  • JNU_ThrowIOException(env, "Stream Closed");
  • return;
  • }
  • if (pos < jlong_zero) {
  • JNU_ThrowIOException(env, "Negative seek offset");
  • } else if (IO_Lseek(fd, pos, SEEK_SET) == -1) {
  • JNU_ThrowIOExceptionWithLastError(env, "Seek failed");
  • }
  • }
  • JNIEXPORT void JNICALL
  • Java_java_io_RandomAccessFile_setLength(JNIEnv *env, jobject this,
  • jlong newLength)
  • {
  • FD fd;
  • jlong cur;
  • fd = GET_FD(this, raf_fd);
  • if (fd == -1) {
  • JNU_ThrowIOException(env, "Stream Closed");
  • return;
  • }
  • if ((cur = IO_Lseek(fd, 0L, SEEK_CUR)) == -1) goto fail;
  • if (IO_SetLength(fd, newLength) == -1) goto fail;
  • if (cur > newLength) {
  • if (IO_Lseek(fd, 0L, SEEK_END) == -1) goto fail;
  • } else {
  • if (IO_Lseek(fd, cur, SEEK_SET) == -1) goto fail;
  • }
  • return;
  • fail:
  • JNU_ThrowIOExceptionWithLastError(env, "setLength failed");
  • }

5. RandomAccessFile示例

  • package com.coderap;
  • import java.io.File;
  • import java.io.IOException;
  • import java.io.RandomAccessFile;
  • public class RandomAccessFileTest {
  • public static void main(String[] args) throws IOException {
  • RandomAccessFile writeRAF = new RandomAccessFile(new File("file.data"), "rw");
  • // 写入字符串
  • writeRAF.writeUTF("hello, 中国\n");
  • // 循环写入单个字节
  • for (int i = 0; i < 26; i++) {
  • writeRAF.write('a' + i);
  • }
  • writeRAF.write('\n');
  • // 写入多个字节
  • String str = "This is a String\n";
  • writeRAF.write(str.getBytes());
  • // 写入浮点数
  • writeRAF.writeDouble(3.1415926);
  • writeRAF.close();
  • RandomAccessFile readRAF = new RandomAccessFile(new File("file.data"), "r");
  • // 读取字符串
  • System.out.print("readUTF: " + readRAF.readUTF());
  • // 循环读取单个字节
  • System.out.print("read: ");
  • for (int i = 0; i < 26; i++) {
  • System.out.print(String.valueOf((char) readRAF.read()));
  • }
  • System.out.print(String.valueOf((char) readRAF.read()));
  • System.out.print("read: ");
  • // 读取多个字节
  • byte[] bytes = new byte[str.getBytes().length];
  • readRAF.read(bytes);
  • System.out.print(new String(bytes, 0, bytes.length));
  • // 读取浮点数
  • System.out.print("readDouble: " + readRAF.readDouble());
  • readRAF.close();
  • }
  • }

运行之后会创建一个file.data文件,该文件有款吗内容,其二进制内容如下:

  • 00000000: 000e 6865 6c6c 6f2c 20e4 b8ad e59b bd0a 6162 6364 6566 6768 :..hello, .......abcdefgh
  • 00000018: 696a 6b6c 6d6e 6f70 7172 7374 7576 7778 797a 0a54 6869 7320 :ijklmnopqrstuvwxyz.This
  • 00000030: 6973 2061 2053 7472 696e 670a 4009 21fb 4d12 d84a :is a String.@.!.M..J

代码运行的打印结果如下:

  • readUTF: hello, 中国
  • read: abcdefghijklmnopqrstuvwxyz
  • read: This is a String
  • readDouble: 3.1415926