输入与输出
Posted yusiming
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了输入与输出相关的知识,希望对你有一定的参考价值。
在Java API中可以从其中读入一个字节序列的对象称作输入流,可以向其中写入一个字节序列的对象称作输出流,这些字节序的来源地和目的地可以是文件,也可以是网络连接,甚至是内存。
读写字节
抽象类InputStream和OutputStream是组成输入输出结构体系的基础,InputStream类中的一个抽象方法:
public abstract int read() throws IOException;
可以从输入流中读取一个字节,并返回读入的字节,或者在遇到输入源结尾时返回-1,即没有字节可以读取了就返回一个-1表示到末尾了,在设计具体的输入流的类时要覆盖这个方法,以提供合适的功能。返回值的范围从-1到255。
InputStream还有若干个非抽象方法,比如读取一个字节数组,读取时跳过若干个字节等等,这些非抽象方法,都使用了read方法,所以子类只需要重写read方法即可完成相应的功能。
与InputStream类似,OutputStream有一个抽象方法write ,它可以向某个输出的位置写出一个字节,在设计具体的输出流类时需要覆盖这个抽象方法,
public abstract void write(int b) throws IOException;
read和write方法在执行时都将阻塞,直到字节确实被读入或者写出,例如:如果网络连接繁忙,流不能立即被访问时,那么当前的线程将会被阻塞,其他线程可以去执行其他的工作了。
我们可以使用available()方法去检查当前输入流中可以读入的字节数量,这意味这我们下面这段代码不可能被堵塞,
int bytesAvailable = in.available(); if (bytesAvailable > 0) { byte[] bytes = new byte[bytesAvailable]; int read = in.read(bytes); }
close()方法
当我们使用完流之后,应该使用close()方法来关闭流,这个方法会释放掉有限的操作系统的资源,如果一个程序打开了过多的输入/输出流而没有关闭它们,那么系统的资源将会被耗尽,关闭一个输出流的同时,还会冲刷缓冲区,将缓冲区中准备合并成为更大的包发送出去的字节,一并发送出去,当我们不关闭流时,可能最后一个包永远也传递不出去,我们也可以使用flush方法来手动冲刷缓冲区,将缓冲区中积攒的字节都传递出去。注意只有输出流才有这个flush方法。
InputStream的API
abstract int read(); |
读入一个字节,返回值-1到255之间 |
int read(byte[] a) |
读入一个字节数组,返回实际读入的字节数 或者碰到输入流接为时返回-1,该方法最多读取a.length个字节 |
int read(byte[] a, int off , int len) |
读入一个字节数组, 返回实际读入的字节数 或者碰到输入流接为时返回-1,off代表第一个读入的字节应该被放置的位置在b中的偏移量,就是第一个字节放在数组什么位置, len表示最多能读取多少个字节 |
long skip(long n) |
在输入流中跳过n个字节,返回实际跳过的字节数,可能小于n |
Int availabel() |
返回在不阻塞的情况下可以获取的字节数 |
void close() |
关闭流,这个方法是 |
void mark(int readlimit) |
在输入流的当前位置打一个标记,如果从输入流中读取的字节数大于 readlimit则可以忽略这个标记 |
void reset() |
返回到最后一个标记,随后对read的调用将重新读入这个标记之后的字节,如果没有任何标记,流将不会被重置 |
boolean markSupported |
是否支持打标记 |
OutputStream的API
abstract void write(); |
写出一个字节 |
void write(byte[] a) |
写出一个字节数组 |
void write(byte[] a, int off , int len) |
写出范围内数组的某一范围的字节,off代表开始位置,len最大字节数 |
void close() |
冲刷缓冲区,并关闭流 |
void flush |
冲刷输出流,也就是将所有缓冲的数据发送到目的地 |
流家族
我们把输入/输出流家族中的类按照使用方式的不同进行划分,那么就形成了处理字节和处理字符的两类层次结构,InputStream和OutputStream类可以读写字节和字节数组,这两个类构成了处理字节的类的层次结构的基础,但是如果想要读写字符串和数字,那么就就必须使用功能更为强大的子类,比如DataInputStream和DataOutputStream,这两个类可以以二进制的形式读取所有的Java基本类型,ZipInputStream和ZipOutputStream可以以zip压缩格式读写文件。
同时可以使用抽象类Reader和Writer的子类来读写字符,这两个类与InputStream和OutputStream类似,
四个附加接口
Closeable、Flushable、Readable、Appendable四个接口,其中Closeable和 Flushable接口中只有一个方法,分别是:
-
- void close() throws IOException
- void flush();
InputStream、OutputStream、Reader、Writer都实现了Closeable接口,
OutputStream和Writer实现了 Flushable接口, Readable接口中只有一个方法:
int read(CharBuffer cb),只有 Reader实现了这个接口,
Writer还实现了Appendable接口,ChaBuffer和StringBuilder的父类AbstractStringBuilder类也实现了这个接口,其中的append方法就来自这个接口。
ChaBuffer类拥有按顺序和随机地进行读写的方法,它表示内存中的缓冲区或者一个内存映像文件,Appendable接口中有两个用于添加单个字符或者字符序列的方法:
-
- append(Char c)
- append(CharSequence s)
CharSequence接口描述了一个char值序列的基本属性,String、StringBuilder、
StringBuffer、CharBuffer都实现了它
CharSequence中的方法
charAt(int index) |
返回索引处的码元 |
In length() |
返回在这个序列中码元的数量 |
CharSequence subSequence(int startIndex ,int endIndex) |
返回由 startIndex到 endIndex之间构成的新的CharSequence |
String toString() |
返回这个序列中所有的码元构成的字符串 |
组合输入/输出流过滤器
FileInputStream和FileOutputStream提供了一个附着在磁盘文件上的输入/输出流,只需要向类提供一个文件名或者完整的文件路径即可构造一个附着在磁盘文件上的输入/输出流,需要注意的是,java.io中的类都将相对路径解释为当前的用户工作目录开始,可以使用
来获取这个用户工作目录的路径,所有的构造器中的路径都已这个路径开始,比如:
FileInputStream in = new FileInputStream("a.txt");
这段代码将会读取用户工作目录下的a.txt文件,以该文件创建一个输入流对象,也可以使用绝对路径来定位资源。
但是FileInputStream和FileOutputStream跟与它们的父类InputStream和OutputStream一样都只能读写字节或者字节数组,但是它们增加了一个附着在磁盘文件上进行读写字节的功能。
// a.txt中只有一个数字8 FileInputStream in = new FileInputStream("a.txt"); System.out.println(in.read()); // 56
文件a.txt只存放一个数字8,但是FileInputStream只能读取字节,数字8的ASCLL码的二进制表示是00111000,转换为十进制就是56,也证明了FileInputStream只能读取字节或者字节数组。
如何读入数值类型呢?我们可以使用DataInputStream,可惜的是DataInputStream不能附着在磁盘文件上,也就是说我们不能使用文件名或者完整的文件路径作为参数构造DataInputStream对象,反而DataInputStream的构造函数要求我们提供一个
public DataInputStream(@NotNull java.io.InputStream in)
·InputStream类或者其子类的实例对象,但是InputStream类是一个抽象类,所以我们应该为DataInputStream的构造函数提供一个InputStream了的子类的实例对象,正好FileInputStream就是InputStream的一个直接子类,那我们可以new一个FileInputStream的对象,然后提供给DataInputStream类的构造方法吗?答案是可以的,这样一来就可以使用DataInputStream来读取数值类型了。
FileInputStream fileInputStream = new FileInputStream("a.txt"); DataInputStream dataInputStream = new DataInputStream(fileInputStream); System.out.println(dataInputStream.readInt());
在Java中某些输入流(FileInputStream)可以从文件或者其他更外部的位置上获取字节,而有的其他流(DataInputStream)可以将字节组装成更有用的数据类型,我们就可以对这二者进行组装,这里使用到了装饰者模式,向 DataInputStream的构造器中传递了一个InputStream的子类的一个实例对象,然后在内部使用该实例对象来完成相应的功能,我们看到DataInputStream内部有一个从其父类FilterInputStream继承过来的一个对象变量,FilterInputStream直接继承于InputStream
protected volatile InputStream in;
该变量用来接收构造器传递过来的InputStream的子类的实例,然后使用in来完成相应的功能,可以看一下FilterInputStream源码中的几个方法:
public int read() throws IOException { return in.read(); } public long skip(long n) throws IOException { return in.skip(n); } public int available() throws IOException { return in.available(); } public void close() throws IOException { in.close(); }
这几个方法都只是单纯调用传递过来的in的重写了InputStream中的方法,再来看DataInputStream中的方法,
public final boolean readBoolean() throws IOException { int ch = in.read(); if (ch < 0) throw new EOFException(); return (ch != 0); } public final byte readByte() throws IOException { int ch = in.read(); if (ch < 0) throw new EOFException(); return (byte)(ch); } public final short readShort() throws IOException { int ch1 = in.read(); int ch2 = in.read(); if ((ch1 | ch2) < 0) throw new EOFException(); return (short)((ch1 << 8) + (ch2 << 0)); }
这些方法中都使用到了in的read方法,方法的功能比read方法更加强大了,
FilterInputStream和FilterOutputStream的子类就是专门用于向处理字节的输入/输出添加额外的功能,DataInputStream就是其中的一个,我们来可以还可以嵌套更多过滤器来添加多重功能,比如我们想要一个带缓冲区的可以读写其他数值类型的流,可以使用如下方法来嵌套过滤器:
DataInputStream dataInputStream = new DataInputStream(new BufferedInputStream(new FileInputStream("a.txt")));
虽然看以来代码很长,但是很好理解,我们先是创建了一个可以读入文件的字节数据的FileInputStream流,然后给这个流加上了缓冲区,然后再加上读写其他数值类型的功能,完成组装。 DataInputStream就能够使用带缓冲机制的read方法了,不用每调用一次read方法请求操作系统发一个字节,可以直接请求一个数据块,将其置于缓冲区中,这种方式更加高效。
我们还可以创建能够预览下一个字节的流PushbackInputStream,这个流同样继承于FilterInputStream,他也有一个protected volatile InputStream in; 我们也可以将其与FileInputStream组装起来:
PushbackInputStream pushbackInputStream = new PushbackInputStream(new BufferedInputStream(new FileInputStream ("a.txt"))); // 预读下一个字节 int b = pushbackInputStream.read(); if(b <= ‘<‘) { // 推回流中 pushbackInputStream.unread(b); }
我们甚至可以将PushbackInputStream与DataInputStream继续进行组装:
DataInputStream dataInputStream = new DataInputStream(pushbackInputStream);
这样我们可以构造了一个与文件关联的,带缓冲机制的,可以预览下一个字节的,可以读写其他数值类型的流。这种混合并匹配过滤器类来构建输入/输出流的能力,带来了极大的灵活性。
例如:如何从一个压缩文件中读取数字?
DataInputStream dataInputStream =
new DataInputStream(new ZipInputStream(new FileInputStream("a.zip")));
看多么轻松,我们可以组合各种过滤器来创建具有不同功能的流。
文本输入与输出
在保存数据时,我们可以选择保存的形式,可以是二进制格式,可以是文本。那么二进制格式和文本格式有什么不同?举一个例子:数字 8 如果被保存为二进制格式,那么就是将十进制的8转换为二进制的00001000来保存,这里还与机器的存储字长有关。但是如果将数字8保存为文本格式,那么就是将数字8当作字符串来看待了,我们首先要选择一种编码方式,如UTF-8,然后根据字符8的编码来保存。
所以我们在存储文本字符串时,首先要考虑使用哪一种编码方式,在Java内部使用的UFT-16的编码方式,但是我们如果想要使用其他的编码方式来存储文本字符串时该怎么办呢?
文本输出
对于文本的输出,我们可以使用PrintWriter类,这个类直接继承于Writer类,这个类拥有以文本格式打印字符串和数字的方法,还可以组合FileOutputStream,实现将数字和字符串输出到一个文件中,如:
//"a.txt"相当于组合了一个FileOutputStream PrintWriter printWriter = new PrintWriter("a.txt","utf-8"); printWriter.print(123456789); System.out.println(printWriter.checkError()); printWriter.close();
这段代码将数字123456789以文本的形式输出到了a.txt中,并且是utf-8编码方式的。
还可以设置写出器的自动冲刷模式,那么只要print被调用,缓冲区的所有内容都会被发送到目的地,默认情况下自动冲刷模式是禁用的,可以使用如下方式来开启或关闭模式:
PrintWriter printWriter =new PrintWriter(new OutputStreamWriter(new FileOutputStream("a.txt"),"utf-8"),true);
PrintWriter的print方法不抛出异常,我们可以使用checkError方法,来查看输出流是出现了某些错误。
下面是一些需要注意的方法
PrintWriter(Writer writer) |
|
PrintWriter(String filename, String encoding) |
|
PrintWriter(File file ,String encoding) |
|
print(Object obj) |
输出输出对象字符串表示,也就是toString |
println(String s) |
加行终止符 |
printf(String format,Object … args) |
以指定格式输出值 |
checkError() |
查看是否产生了错误 |
文本输入
使用Scanner类,我们可以从任何输入流中构建Scanner对象
Scanner scanner = new Scanner(new FileInputStream("a.txt"),"utf-8"); while (scanner.hasNextLine()) { System.out.println(scanner.nextLine()); }
还可以使用String类的构造方法,将短小的文本文件读入到一个字符串中,
String s = new String(Files.readAllBytes(Paths.get("a.txt")), "utf-8"); System.out.println(s);
如果想将文件一行一行地读入可以使用,
List<String> strings = Files.readAllLines(Paths.get("a.txt"), StandardCharsets.UTF_8); for (String s : strings) { System.out.println(s); }
如果文件太大,可以使用BufferedReader类,它的readLine方法会产生一行文本,或者返回null,
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("a.txt"), "utf-8"))) { String s; while ((s = reader.readLine()) != null) { System.out.println(s); } }
字符编码方式
Java针对字符使用的是Unicode标准,每个字符或者“编码点”都具有一个21位的整数,有很多种不同的编码方式,也就是说可以将这些21位数字包装成字节的方法有多种。
比如说最常见的UTF-8,它会将每个Unicode编码点编码为1到4个子节的序列,注意传统的ASCLL编码中的每一个字符都只占一个字节。
另一种常见的编码方式是UTF-16,这是一种在Java字符串中使用的编码方式,它会将每个Unicode编码点编码为1个或者2个16位的值,实际上,有两种形式的UTF-16,分别位“高位优先”和“低位”优先,比如说16的值0x2122,在“高位优先”格式中,0x21会先出现,即 0x21 0x22,“低位”优先的格式中,低位0x22会先出现,即0x22 0x21,为了表示使用的是哪一种格式,文件可以以“字节顺序标记”开头,这个标记就表明了使用的是哪一种格式,格式的值为 0xFEFF,读入器可以根据这个值来确定使用的是哪一种格式,然后再丢弃这个值,
需要注意的是,有一些软件,如记事本,在UTF-8编码的文件开头添加了“字节顺序标记”,显示UTF-8是不需要这个标记的。所以使用记事本来编码有可能会出现一些错误。
使用StandardCharsets类指定字符编码
StandardCharsets类具有类型为Charset的静态变量,用于表示Java虚拟机支持的编码方式,
public static final Charset ISO_8859_1 = sun.nio.cs.ISO_8859_1.INSTANCE; public static final Charset UTF_8 = sun.nio.cs.UTF_8.INSTANCE; public static final Charset UTF_16BE = Charset.forName("UTF-16BE"); public static final Charset UTF_16LE = Charset.forName("UTF-16LE"); public static final Charset UTF_16 = Charset.forName("UTF-16");
public static final Charset US_ASCII = sun.nio.cs.US_ASCII.INSTANCE;
还可以使用Charset.forName(“XXX”)来获得其他方式编码的Charset类型,
在读入或者写出文本时,如果需要指定编码方式,应该使用Charset对象,而不应该使用字符串来指定编码方式,这样可以减少拼写错误。如:
PrintWriter printWriter = new PrintWriter("a.txt",StandardCharsets.UTF_8);
读写二进制数据
DataInput和DataOutput接口
DataOutput接口定义了以下用二进制格式写数组,字符,boolean值和字符串的方法:
-
writeChars
-
writeByte
-
writeInt
-
将一个整数写出为4个子节的二进制数值
-
-
writeLong
-
writeFloat
-
writeDouble
-
将一个double值写出为8个字节的二进制数值
-
-
writeChar
-
writeBoolean
-
writeUTF
-
使用修订版的8位Unicode转换格式写出字符串,这种方式是为了向后兼容在Unicode还没有超过16位时构建的虚拟机
-
DataInput接口定义了一系列方法用来读入二进制数据:
-
readInt
-
readShort
-
readLong
-
readFloat
-
readDouble
-
readChar
-
readBoolean
-
readUTF
方法的作用与上面的方法的作用相反
但是这两个都是接口,我们可以使用它们的实现类来完成读写二进制数据的功能,
DataInputStream实现了DataInput接口,可以使用如下方式读入二进制数:
DataInputStream dataInputStream = new DataInputStream(new FileInputStream("a.txt")); dataInputStream.XXX // 调用一系列方法
同时DataOutputStream实现了DataOutput接口,同样我们可以使用如下方法写出数据:
DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream("a.txt")); dataOutputStream.writeInt(100); dataOutputStream.close(); DataInputStream dataInputStream = new DataInputStream(new FileInputStream("a.txt")); System.out.println(dataInputStream.readInt()); dataInputStream.close();
对象输入输出与序列化
Java支持一种被称为对象序列化的机制,它可以将任何对象写出到输入流中,并在之后将其读回。
为了保存对象数据,我们先要打开一个ObjectOutputStream流,然后调用其writeObject方法,将对象传入方法中,就可以了,不过有一个前提是,对象所属的类必须实现Serializable接口,这个接口中没有任何方法,是一个标记接口,跟Cloneable 接口相似,但是你不用重写任何方法,直接将对象传入writeObject方法即可完成对象的输出,例子:
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("a.txt")); objectOutputStream.writeObject(new People()); objectOutputStream.close();
这里的People类实现了Serializable接口
还可以使用ObjectInputStream的readObject()方法读入对象,例子:
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("a.txt")); People people = (People) objectInputStream.readObject(); objectInputStream.close(); System.out.println(people); // [email protected]
考虑一种情况,当一个对象被多个多个对象共享时,即一个对象被多个对象声明为成员变量了,应该如何存储?如一个Student对象同时被两个Teacher对象共享,两个老师可以拥有共同的学生,我们不能保存和回复Student对象的内存地址,即用地址来做标识,来区别是否是同一个Student,因为当我们将Student对象重新读入的时候,它可能占据的是完全不同的内存空间。
这里我们应该使用别的方法来保存对象,Java中的对象都是使用了一个序列号来保存的,
每一个对象在保存时序列号是不同的,若一个对象之前已经被保存过了,那么在保存这个对象时,只写出“与之前保存过的序列号为X的对象相同”,这样一来就节省了空间,不必为重复的对象再去保存一次,如在保存第一个Teacher对象时,对Teacher对象的引用关联一个序列号,由于时第一次遇到Teacher对象,所以将对象数据保存到输出流中,其中,Student对象的引用也被关联了一个序列号,在保存第二个Teacher对象时,给引用关联一个序列号,由于这个对象也是第一次遇到所以也将对象数据保存到输出流中,但是这个Teacher对象中的Student已经不是第一次遇到了,所以在保存这个Student对象时,只会向输入流中写入“与之前保存过的序列号为X的Student对象相同”。
当读入对象时,整个过程是反的:
-
对象输入流中的对象,在第一次遇到其序列号时,创建并初始化这个对象,然后记录这个新的对象与序列号之间的关联
-
当遇到一个“与之前保存过的序列号为X的Student对象相同”的标记时,获取与这个序列号相关联的对象的引用。
对象序列化还有一个重要的应用场景,就是通过网络将对象集合传送到另一台计算机中,因为Java使用了序列号来代替地址,所以方式是允许的。
关于对象的序列化记住以下几点:
-
对象流输出中包含所有对象的类型和数据域
-
每个对象都被赋予了一个序列号,
-
相同对象的重复出现将被存储为对这个对象的序列好的引用
修改默认的序列化机制
某些数据域不可以序列化的,如只对本地方法有意义的存储文件句柄或窗口句柄的整数值,这种信息都是没有用处的,甚至如果这种域的值如果不恰当,还会引起本地方法的崩溃。
在Java,我们可以使用一种简单的机制来防止这种域被序列化,即使用transient 来标记一个域,即瞬时的,瞬时的域在序列化时将会被跳过。
序列化机制还为单个类提供了一种方式,去向默认的读写行为添加验证或者是任何其他想要的行为,可序列化的类可以定义如下方法:
private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException; private void writeObject(ObjectOutputStream in) throws IOException;
之后数据域再也不会被序列化,取而代之的是调用这些在类中定义的方法,注意可以在 writeObject方法中调用defaultWriterObject()方法,这个方法只能在类中定义的writeObject调用。
除了让序列化机制来保存和回复对象的数据之外,类还可以定义自己的机制,为了实现这一功能,这个类必须实现Externalizable接口,还需重写两个方法:
这些方法将会负责包括父类在内的整个对象的存储和恢复,而序列化机制是不关心父类数据和其他任何的类的信息的,只保存和加载类自己的数据域。
void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
序列化单例和类型安全的枚举
在序列化和反序列化时,如果目标对象是唯一的(即只有一个这种类型的对象),如:单例和类型安全的枚举。
如果使用的是Java的enum结构,序列化能够正常工作,但是如果是遗留下来的代码,就有可能会出现问题,如下面的枚举结构
public class Orientation { public static final Orientation A = new Orientation(0); public static final Orientation B = new Orientation(1); private int value; private Orientation(int v) { value = v; } }
这种风格在枚举被添加到Java之前是很常见的,我们可以使用 == 操作符来测试对象是否相等,当类型安全的枚举实现Serializable接口时,默认的序列化机制是不适用的,
public static void main(String[] args) throws IOException, ClassNotFoundException { Orientation a = Orientation.A; ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("a.txt")); objectOutputStream.writeObject(a); objectOutputStream.close(); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("a.txt")); Orientation o = (Orientation) inputStream.readObject(); inputStream.close(); System.out.println(Orientation.A == o); //false }
o的值是一个全新的值,它与任何预定义的对象都不相等,显然这不是我们想要的结果,
即使构造器是私有的序列化机制也可以创建新的对象。
当然也是有解决方法的,我们可以一种称为readResolve的特殊序列化方法,如果在类中定义了readResolve方法,在对象被序列化之后就会调用这个方法,这个方法的返回值必须是一个对象,这个对象将会成为readObject方法的返回值:
protected Object readResolve() throws ObjectStreamException { if (value == 1) { return Orientation.A; } else { return Orientation.B; } }
添加了这个方法之后读入的对象就是 Orientation.A了,
所以我们要向所有类型安全的枚举类,以及所有支持单例模式的类中添加readResolve方法。
以上是关于输入与输出的主要内容,如果未能解决你的问题,请参考以下文章