您的位置:首页 > 编程语言 > Java开发

Java IO 经典教程 (上) (翻译自jenkov.com)

2017-11-12 19:06 465 查看
此系列文章翻译自Jakob Jenkov的java系列教程,原文地址链接为Jakob Jenkov的教程,教程比较详细,很适合初学者!

您可以查看Java IO 经典教程 (中) (翻译自jenkov.com)

您可以查看Java IO 经典教程 (下) (翻译自jenkov.com)

如果您更喜欢简书的风格,也可以点击链接:此文章简书链接

教程介绍

Java IO是java中的相关API,主要目的为读数据与写数据(input 和 output)。大部分的应用都需要处理一些输入数据,并且根据输入数据生成一些输出数据,比如说从文件或网络读取数据,然后写回文件或通过网络响应数据。

Java IO的相关API在Java IO包中(java.io)。如果你直接去java.io包中去看源代码,那么你会因为大量的代码而感到相当困惑。这些Java类的意图是什么?哪些类可以用来完成你的任务?如何创建你自己的类去做插件?等等。那么这个教程的目的就是尝试去告诉你这些Java类是如何组织的和它们的一些背后目的,所以你不必去疑惑怎么样去选择使用合适的类,或者有没有一个现成的类去满足你的需求。

java.io包的范围

java.io包并不能解决所有的input和output。实际上从GUI程序或web页面输入或输出到他们上,这类的api并没有在java.io包中,而是在另外的一些地方。比如说,Swing工程或servlet和http相关的IO类就在javaEE中。

java.io包首先是专注于解决文件、网络流、内存缓冲区等的输入或输出。然而,java.io包并不包含socket这些的必要的网络通讯,如果需要,你可以去看Java Networking API。但是你打开一个socket连接,这时候读写数据就需要用InputStream和OutputStream等相关的类。

Java NIO - 另一种 IO API

Java也包含另一种io的API,叫做java NIO,他和Java IO与Java Networking API的java类有很多的相似之处。但是Java NIO可以以非阻塞模式来工作。非阻塞模式在高并发下读写数据的性能要远远大于堵塞的IO。

其他的Java IO工具和一些技巧等

可以点击教程链接Java How To’s and Utilities,其中包含了一些新的Java IO使用工具。

此Java IO教程的范围

首先是刚告诉你Java IO是如何功能工作的,以及告诉你如何去使用它。最后,会转向到Java IO包的核心类上。

这个教程中展示的类,不仅仅只是一个API的列表展示(你可以从java官方网站上获取这些API列表)。每段文本都是对类的简短介绍,他的目的和一些使用例子。换句话说,一些东西你没有必要去java官方文档上去找。

Java5 到 Java8

这个教程的第一个版本是基于Java5来写的,写的时候Java已经到了Java8版本,但是这些代码一样可以在Java8上面完美运行。

Java IO 概述

在这里,我会给你一个Java.io包中的类的一个概要说明。更具体的说,我回去尝试按照它们的意图来分组这些类。这些分组会放你未来会觉得它更简单,入去确定一个类的目的,或根据你的需要去找到合适的类。

Input 和Output - 源代码和目标

专业术语“input”和“output”有时候会有一些概念上的模糊。一个应用程序的输入部分往往是另一个部分的输出。
OutputStream
是一个流被写出去,还是从一个地方写进来?归根结底,
OutputStream
是把数据写入到读取的程序,不是么?以我个人经历发现,第一次学习Java IO的时候这些会让我有一些困惑。

为了解决这些困惑,我曾经尝试在input和output上添加一些不同的名称,希望让从哪里输入的,输出到哪里,有一些概念上的联系。

Java的IO包本身的主要关注点在于,从一个地方读取原始数据和向目的地写入数据。最典型的数据来源和目的地主要有:

文件(File)

管道(Pipes)

网络连接(Network Connections)

内存缓冲区,比如数组(In-memory Buffers)

System.in, System.out, System.error

下面的图说明了一个程序从一个地方读数据并且写数据到目的地的原理:



IO流是Java IO的核心概念。流是一个概念上的无休止的数据流。你既可以从一个流读取数据也可以写入到一个流。流用来连接数据源与目标地。Java IO流基于字节流和字符流。

InputStream, OutputStream, Reader and Writer

一个程序从数据源读取数据的时候,需要 InputStream或者Reader。程序写数据的时候,就需要OutputStream或Writer。具体可以参见下面图例:



InputStream和Reader连接到数据来源。OutputStream和Writer连接到目的地

Java IO的目的和特性

Java IO中有很多InputStream, OutputStream, Reader和Writer的子类,这些子类会用来处理不同的需求。这就是为什么有这么多类的原因。具体的用法主要有以下一些场景:

文件

网络流

内存缓冲区

管道

缓冲

过滤

解析

读写文本

读写原始类型的数据(long等等)

读写对象

上面的这些可以让你在阅读Java IO相关类的时候清晰一些。更能理解这些类的目的都是什么。

Java IO相关类概览

在讨论源,目的地, input,output和各式各样的Java IO类的时候,下面的表以input和output做分类,基本列出了大部分的Java IO相关的类,有字符流,也有字节流,以及更多特殊用途的如buffering,,parsing等等:

字节输入流Output字符输入流Output
基础类InputStreamOutputStreamReader/InputStreamReader
数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReader
文件FileInputStream/RandomAccessFileFileOutputStream/RandomAccessFileFileReader
管道PipedInputStreamPipedOutputStreamPipedReader
BufferBufferedInputStreamBufferedOutputStreamBufferedReader
FilterFilterInputStreamFilterOutputStreamFilterReader
解析PushbackInputStream/StreamTokenizerPushbackReader/LineNumberReader
StringStringReader
DataDataInputStreamDataOutputStream
Data - FormattedPrintStream
ObjectsObjectInputStreamObjectOutputStream
实用工具SequenceInputStream

Files

在Java应用中,文件是常见的数据源。所以这篇文章会给你一个用java处理文件的简短介绍。这里虽然不会去详细阐述每一个知识点,但是也会给你足够的知识去决定如何处理文件。后面会有一些独立的章节,更加详细的去描述这些方法或类,以及一些例子等等。

根据Java IO读取文件

如果你想从一端读取一个文件到另一端,你可以用FileInputStream或者FileReader,这取决于你是想把文件读取成字节还是字符。这两个类每次读取一个字节或者一个字符来处理整个文件,或者将字节或字符放入到字节数组或字符数组中。你没有必要读取整个文件,只需要有序的读取文件中的字符或字节就可以。

如果你需要读取文件中随机位置的部分,你可以使用RandomAccessFile。

根据Java IO写文件

如果你想从一端读取一个文件到另一端,你可以用FileOutputStream或者FileWriter,这取决于你是想根据字节还是字符来写。这两个类每次写入一个字节或者一个字符来写完整个文件,或者将字节或字符放入到字节数组或字符数组中来写。数据会按顺序存储到文件当中。

如果你需要在文件中随机的位置来写,你可以使用RandomAccessFile。

根据Java IO随机访问文件

上面的内容已经说到,你可以利用RandomAccessFile来随机访问文件。

随机的意思并是不真正意义上的随机访问文件地址,它紧紧是意味着你可以根据你的需要来访问文件的某个位置。没有一个强制的顺序让你访问文件。这就让处理已经存在的文件的某部分称为了可能,去增加内容,从中删除内容,当然了也包括去读取任何内容。

获取文件和目录信息

有时候比起文件内容,你更想去获取文件信息。比如说你想获取文件或者目录的大小,以及文件的其他属性。再比如获取某目录下的所有文件列表。获取目录和文件的信息,都可以通过Java IO的类来解决。

Pipes

Java IO中,管道用来提供同一个jvm中的两个线程间的通信。所以管道也可以是数据的源或目的地。在不同的jvm(不同的进程)下,你不可以用管道去连接线程。Java的管道概念,不同于linux或unix的管道,linux或unix中,管道可以用来连接两个运行在不同地址空间的进程。
(译者注:linux下的管道符为“ | ”,例如:ps -ef | grep java)
。Java中,通讯部分必须在同一个进程下,可以使不同线程。

利用Java IO创建管道

用Java IO创建管道,已经有线程的类可以使用:PipedOutputStream和PipedInputStream。PipedInputStream可以用来连接PipedOutputStream。一个线程把数据写入到PipedOutputStream,可以被另一个线程创建的已经连接的PipedInputStream读取。

Java IO管道示例

这里提供一个简单的例子来演示如何去连接一个PipedInputStream到一个PipedOutputStream:

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

/**
* 代码基于java8
*/
public class PipeExample {

public static void main(String[] args) throws IOException {

final PipedOutputStream pipedOutputStream = new PipedOutputStream();
final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream);

Runnable runnable1 = () -> {
try {
pipedOutputStream.write("hello kopshome".getBytes());
} catch (IOException e) {
}
};

Runnable runnable2 = () -> {
try {
int data = pipedInputStream.read();
while (data != -1) {
System.out.println((char) data);
data = pipedInputStream.read();
}
} catch (IOException e) {
}
};

new Thread(runnable1).start();
new Thread(runnable2).start();
}

}


除了利用构造方法外,你也可以用connect()方法来连接两个管道流。PipedInputStream和PipedOutputStream都有connect()方法。

管道和线程

记住这一点,当使用两个已连接的管道流时,流和线程应该是一一对应的关系。read()和write()方法是阻塞的方法,这意味着,如果你用同一个线程既读又写,那么就会造成这个线程的死锁。

管道的替代方案

在同一jvm中,线程间通讯有很多其他的办法。实际上线程间的交互,相比利用原始字节数据,更多的还是利用对象进行交互。当然了,如果你想利用原始数据进行线程间的交互,Java IO的管道流是一个不错的选择。

Networking

Java网络相关的内容或多或少在此教程的范围之外。Java网络相关的更多细节在教程Java Networking Tutorial中。由于网络连接也是常见的数据源或数据目的地,同时也是你也会用Java IO的相关API通过网络连接进行通讯,所以这里会对Java网络进行简单的介绍。

当两个进程间通过建立网络连接进行通讯,这就像他们之间会用一个文件:使用InputStream去读取数据,再使用OutputStream去写出数据。也就是说,Java网络相关API用来在连个进程间建立网络连接,Java IO利用建立的网络连接做数据交互。

这主要意味着,如果你想把一些东西写入到文件中,那么把它写入到网络连接中也是一样简单的。无非就是把FileOutputStream替换成了OutputStream。由于FileOutputStream是OutputStream的子类,所以这当然也不是什么问题。

实际上,从文件读取也一样。一个组件如果可以从文件读取数据,也同样可以从网络连接读取数据。只要确定你的读取部分组件是依赖于InputStream而非FileInputStream。

这里提供一个例子:

public class MyClass {

public static void main(String[] args) {

InputStream inputStream = new FileInputStream("c:\\myfile.txt");

process(inputStream);

}

public static void process(InputStream input) throws IOException {
//do something with the InputStream
}
}


在这个例子中,process()方法并不关心参数InputStream是从文件系统中还是网络连接读取来的数据(例子中紧紧是写成了从系统中读取的文件),这个方法仅仅是通过InputStream而已。

Byte & Char Arrays

在一个Java应用中,字节数组和字符数组经常用来在程序内部临时存储数据。它们也是常规的数据源或数据目的地。如果你需要在程序运行的时候访问文件内容,也可以把文件加载到数组中。当然,你可以通过数组的下标来访问文件的内容。如果你设计一个组件用来从InputStream或Reader读取特定的数据并且不用数组,那该怎么办?

通过InputStream或Reader读数组

去写一个从数组读取数据的组件,你需要用ByteArrayInputStream或CharArrayReader包装一下字节或字符数组。这样,数组中的字节或字符就可以通过包装的stream或reader来读取了。

这里有一个简单的例子:

byte[] bytes = new byte[1024];

//write data into byte array...

InputStream input = new ByteArrayInputStream(bytes);

//read first byte
int data = input.read();
while(data != -1) {
//do something with data

//read next byte
data = input.read();
}


这个demo也同样适用于字符数组。只是利用CharArrayReader来做这件事,你可以完成的。

通过OutputStream或Writer写入到数组

同样的去写数据,可以用ByteArrayOutputStream或CharArrayWriter。你需要做的事情就是创建一个ByteArrayOutputStream或CharArrayWriter,然后向这里面写数据,就像你用其他的stream或writer一样。当所有数据写完的时候,只是简单的调用oByteArray()或toCharArray方法,所有的数据就可以以数组的形式返回。

这里有一个简单的例子:

ByteArrayOutputStream output = new ByteArrayOutputStream();

output.write("This text is converted to bytes".getBytes("UTF-8"));

byte[] bytes = output.toByteArray();


这个demo也同样适用于字符数组。只是利用CharArrayWriter来做这件事,你可以完成的。

System.in, System.out, 和System.error

System.in,System.out和System.err这三个流也是常见的数据源或数据目的地。最常用的可能就是程序利用System.out将数据输出到控制台了。

这三个流是在jvm启动的时候被Java运行时环境初始化的,所以你并不必去实例化它们。(虽然你可以在运行时改变他们)

System.in

System.in是一个InputStream,它是控制台程序的典型的接受键盘输入的流。由于数据经常通过配置文件或命令行参数传递给Java程序,所以不会经常用它。在GUI应用中,程序的输入是GUI提供的。这是一个来自Java IO中独立的input机制。

System.out

System.out是一个PrintStream。它经常把你写入的数据输出到控制台。它经常用在命令行工具中。在debug模式打印报告中也会经常用到它(虽然它不是最好的往事去输出debug信息)

System.err

System.err是一个PrintStream。它的作用有些像System.out,不同的是它一般用来打印错误日志。一些程序(比如Eclipse)会用红色字体来显示System.err的输出日志。

简单System.out + System.err 例子

下面是一个简单的例子来使用System.out和System.err:

try {
InputStream input = new FileInputStream("c:\\data\\...");
System.out.println("File opened...");

} catch (IOException e){
System.err.println("File opening failed:");
e.printStackTrace();
}


改变System的流

即使System的三个流是java.lang.System的三个静态成员,并且在虚拟机启动的时候就已经初始化好,但是你也可以用一个流来改变它们。仅仅是给System.in或System.out创建一个新的InputStream,进而所有的数据都可以被读写到新的流中。

使用System.setIn()、System.setOut()或System.setErr()其中一个方法,创建一个新的System流,举例说明:

OutputStream output = new FileOutputStream("c:\\data\\system.out.txt");
PrintStream printOut = new PrintStream(output);

System.setOut(printOut);


现在所有写入到System.out的数据都可以被定向到目录c:\data\system.out.txt了。但是请记住,你需要去确定在JVM关闭之前要刷新System.out和关闭这个文件,确保所有的数据已经被刷新到文件中了。

Java IO: Streams

Java IO Stream是一个你既可以读也可以写的数据流。正如本教程前面提到的,Stream经常被连接到数据源或数据目的地,例如文件,网络连接等等。

Stream在读取或写入数据时,没有像数组那样的下标的概念。你不可以在一个Stream中像数组(或RandomAccessFile读取文件)一样,前或向后移动。Stream只是一个连续不断的数据流。

一些流的实现,比如PushbackInputStream,允许你把不需要的数据回退到流中,稍后可进行重新读取。但是你只可以回退一小部分的数据,并且不能像数组那样随意的遍历数据。数据只能有序的读取。

Java IO流通常基于字节或字符。基于字节的流一般叫做”stream”,例如InputStream,OutputStream等。这些流每次读写一个字节,除了DataInputStream和DataOutputStream以外,这两个流一次可以读写int,long, float和double等类型的值。

基于字符的流一般叫做”Reader”或”Writer”。字符流可以读写字符(例如Latin1或UNICODE的字符)。看
Java IO: Readers and Writers
章节可以看更多关于字符流的信息。

InputStream

java.io.InputStream是所有Java IO输入流(input stream)的基类。如果你想写一个需要从输入流读取数据的组件,可以尝试让我们的组件依赖于一个InputStream,而不是它的子类(比如FileInputStream),这么做可以让你的代码可以处理所有的InputStream,而不是具体的某一个子类。

但是依赖于InputStream并不总是可行的。如果你需要让数据回退到流,你应该去依赖PushbackInputStream,这意味着你的流参数是这个类型的。否则你无法调用PushbackInputStream的unread()方法。

从InputStream经常会调用read()方法。read()方法返回一个int数值,为读取的字节的值。如果所有的数据已经读完,就会返回-1。

下面为示例:

InputStream input = new FileInputStream("c:\\data\\input-file.txt");

int data = input.read();

while(data != -1){
data = input.read();
}


OutputStream

java.io.OutputStream是所有Java IO输出流的基类。如果你想写一个写出到流的组件,应该确认组件是依赖于OutputStream而不是他的任何子类。

下面的简单示例为将一些数据写入到文件:

OutputStream output = new FileOutputStream("c:\\data\\output-file.txt");
output.write("Hello World".getBytes());
output.close();


将流结合起来

你可以把多各流结合起来成一条链,这样可能实现更高级的操作。比如说,一次从文件读一个字节比较慢。更快的做法是一次从磁盘读取更大的一块儿数据,然后再遍历这个数据块以获得每个字节。要实现缓冲区你可以用BufferedInputStream包装你的输入流,这里有一个示例:

InputStream input = new BufferedInputStream(
new FileInputStream("c:\\data\\input-file.txt"));

...


缓冲区也可以应用到OutputStream,从而分批的写入数据到磁盘(或者目标流)。这也提供了更快的输出。这可以用BufferedOutputStream

缓冲区只是组合流的效果之一。你也可以用PushbackStream包装你的InputStream。这也你可以把数据回退到stream以晚一些重读。这有时候在解析过程中很方便。或者,用SequenceInputStream的时候你可以合并两个InputStream到一个。

通过将输出流和输入流组合到一起,还可以实现一些其他的效果。你可以写你自己的stream相关类去包装Java自带的stream类。这样你就可以创建你自己的过滤器或一些效果。

Input解析

Java IO中已经设计了一些类来帮助你解析input,这些类是:

PusbackInputStream

PusbackReader

StreamTokenizer

PushbackReader

LineNumberReader

这一节的内容目的不是给你一个完整的解析数据的过程,而是提供一个与解析数据有关的快速列表。

如果你去解析数据,会经常用上面的列表编写自己的类去解决问题。在我的解析器核心出使用PushbackInputStream,因为有时候我需要提前一两个字符,去决定即将到来的数据的含义。

我有一个使用PushbackReader的真实的例子,在文章中,使用PushbackReader在流,数组或文件中替换字符串。例子中创建了一个TokenReplacingReader,可以替换掉数据中${tokenName}格式的内容,从而换成你自己想要换的数据内容。并且这对使用者是不可见的。

Reader和Writer

Java IO中的java.io.Reader和java.io.Writer的工作原理很像InputStream和OutputStream,但不同的是reader和writer是基于字符的,他们是用来用些文本数据的。InputStream和OutputStream的基于字节的,记住了?

Reader

Java Reader是所有java reader相关类的基类。子类包括有BufferedReader、PushbackReader、InputStreamReader、StringReader和一些其他的类。

下面这有一个例子:

Reader reader = new FileReader("c:\\data\\myfile.txt");

int data = reader.read();
while(data != -1){
char dataChar = (char) data;
data = reader.read();
}


注意,当InputStream每次返回一个字节的时候,这个字节是 0 到 255 直接的值(-1代表没有数据了),Reader每次返回一个字符,意味着返回 0 到 65535 (-1代码数据已经读完)。这并不意味着Reader从已连接的数据源一次读取两个字节,它每次可以一次读取一或多个字节。这取决于文本数据的编码格式。

用InputStreams把Reader连接起来

Java Reader可以被InputStream连接起来。如果你有一个InputStream,并且向从这里读取字符,你可以用InputStreamReader包装它。把InputStream传入InputStreamReader的构造方法:

Reader reader = new InputStreamReader(inputStream);


在构造方法中,你也可以指定一个用来读取文本数据的编码。更多关于这个内容可以参考InputStreamReader章节。

Writer

Java IO API中Writer是所有writer的基类。子类主要包括BufferedWriter和PrintWriter等一些其他的类。

下面是一个Writer的例子:

Writer writer = new OutputStreamWriter(outputStream);


将Readers和Writers结合起来

像stream一样,Reader和Writer也可以组合起来用,成为更有意思的IO。它就像用InputStream把Reader结合起来,或者用OutputStream把Writer结合起来。比如,你可以用BufferedReader把Reader包装起来,或者用BufferedWriter把Writer包装起来,下面是两个这样的例子:

Reader reader = new BufferedReader(new FileReader(...));

Writer writer = new BufferedWriter(new FileWriter(...));


Java并发IO

有时候你可能需要并发的处理输入或输出。换句话说,你可能需要开启多线程去写入或产生输出。比如,有也许有一个应用需要在磁盘上处理非常多的文件。这可以并行的去处理以增加性能。或者你可能有一个服务,像web服务或聊天服务器,需要接受很多单独的连接和请求。这些也可以并行处理以获得性能上的提升。

如果你需要并发IO,这里给你一些你应该注意的一些常见问题:

如果你没有办法保证每一个线程读取多少input,或线程是按什么顺序把数据写到输出流。你就不应该在同一时间有多个线程来从InputStream读数据。也不可以在同一时间有多个线程来把数据写到OutputStream。

如果线程有序执行的那么你可以有多个线程去使用stream、reader和writer。实际上,你可以有一个线程来决定是什么样类型的请求,然后把这个请求交给其他合适的线程去做进一步处理。当有序读取stream / reader / writer时候这样的是可行的。注意,这些传递stream的线程是需要同步的。

注意:在Java NIO中,你可以有一个线程读来读取或写入多个“channel”。当你打开许多网络连接,但是每个网络连接只有少量的数据,例如在一个聊天服务器中,你可以有一个线程来调度所有的“channel”(连接)。Java NIO是另外一回事了,会在后面的教程中讲解。

Java IO: 异常处理

Streams或Readers/Writers在使用过程中需要是可关闭的。这需要调用close()方法。这需要一点儿思考,看下面的代码:

InputStream input = new FileInputStream("c:\\data\\input-text.txt");

int data = input.read();
while(data != -1) {
//do something with data...
doSomethingWithData(data);

data = input.read();
}
input.close();


这段代码看起来很好。但是如果doSomethingWithData()方法抛出异常会发生什么?对的!如果发生异常InputStream就不会被关闭了!

下面的代码避免了这样的问题:

InputStream input = null;

try{
input = new FileInputStream("c:\\data\\input-text.txt");

int data = input.read();
while(data != -1) {
//do something with data...
doSomethingWithData(data);

data = input.read();
}
}catch(IOException e){
//do something with e... log, perhaps rethrow etc.
} finally {
if(input != null) input.close();
}


注意一下现在InputStream是怎么样在finally下关闭的。不管在try代码块中发生了什么,finally中的代码都会被执行。所以InputStream总是会被关闭。

但是close()方法如果抛出异常会发生什么?是说stream已经被关闭了?好吧,你可能要用try-catch去catch住这个异常,像下面这样:

} finally {
try{
if(input != null) input.close();
} catch(IOException e){
//do something, or ignore.
}
}


这样嵌套的去处理问题,看起来非常丑陋,尽管解决了问题。这样不优雅的异常处理代码并不够好,他可以在你的代码中重复的传播。如果写代码时候比较匆忙,忘记异常处理该怎么办?

想象一下如果从doSomethingWithData()抛出一个异常,第一个catch捕捉到那个异常,InputStream在finally关闭。但是,如果从input.close()方法抛出一个异常会发生什么?两个异常中的哪一个应该被传播到调用堆栈?

幸运的是,有一个方法解决这个问题。解决的办法是 Exception Handling Templates in Java。创建一个异常处理模板在最后来关闭流。代码一次写完,可以在各处重用。可以在Exception Handling Templates in Java学习更多。

Java 7中Java IO 的异常处理

Java7开始有一个新的异常处理机制叫做“try with resources”,它是一个语法糖。这个异常处理机制特别的地方在于你使用资源时需要在使用后关闭,那么用它可以解决这个问题,例如使用InputStream,OutputStream时等等。想要学习和它更多相关的内容可以去Try With Resources in Java 7

InputStream

InputStream是Java IO中所有输入流的基类。它的子类包括FileInputStream,BufferedInputStream和PushbackInputStream等。想要查看所有详细的列表,请看“Java IO Overview”章节。

InputStreams and Sources

InputStream经常被用来连接一些数据源,例如文件,网络连接,管道等等。查看更详细的信息参考“Java IO Overview”章节

InputStream例子

它用来读取字节数据,每次读取一个字节。下面是一个相关例子:

nputStream inputstream = new FileInputStream("c:\\data\\input-text.txt");

int data = inputstream.read();
while(data != -1) {
//do something with data...
doSomethingWithData(data);

data = inputstream.read();
}
inputstream.close();


这个例子中创建了一个FileInputStream实例。FileInputStream是InputStream的子类所以它可以InputStream类型的变量。

注意:为了代码清晰,这里并没有考虑处理异常的情况。想学习更多可以看“Java IO Exception Handling”

Java7开始,你可以用try-with-resources去确保InputStream在使用后可以关闭。具体的用法可以参考上面Java异常处理的文章,但是这里有一个例子:

try( InputStream inputstream = new FileInputStream("file.txt") ) {

int data = inputstream.read();
while(data != -1){
System.out.print((char) data);
data = inputstream.read();
}
}


只要线程已经执行出try代码块,inputstream就会被关闭。

read()

InputStream的read()方法返回一个int类型的值,这个值是每次读取的字节的值。下面是一个相关的例子:

int data = inputstream.read();


你也可以把int值强转成为一个char类型的:

char aChar = (char) data;


InputStream的子类有可能扩展了read()方法。例如,DataInputStream允许读取java基本类型,像int、long、float、double、boolean等等。当然这些有相应的方法,readBoolean(),readDouble()等等。

Stream的结束

如果read()方法返回 -1,就说明流到已经全部读取完毕。这里的 -1 是int类型的,不是byte或short。这里是有些不同的!

当流去读完毕的时候,你可以关闭它了

read(byte[])

InputStream有两个read()方法,参数是一个字节数组。这些方法是:

int read(byte[])

int read(byte[], int offset, int length)

每次读取一个字节数组显然要比每次读取一个字节要快的多,所以只要有可能,你可以使用这类的方法来替代read()方法。

read(byte[])方法会读取所有数据到数组中。它会返回一个int值,来告诉实际读取的字节数。万一实际读取的字节数要比提供的字节少,那么字节数组的其余部分将包含与读取开始之前所做的相同的数据。要记住去检查返回的int值,看实际取得的字节数有多少。

read(byte[], int offset, int length)方法也是读数组到数组中,但是可以根据参数来规定开始的偏移量和一共读取多少长度。返回值和read(byte[])方法的含义相同。

两个方法相同的地方都是如果返回值是 -1 ,代表读取结束。

下面的例子为如何使用InputStream的read(byte[])方法:

InputStream inputstream = new FileInputStream("c:\\data\\input-text.txt");

byte[] data      = new byte[1024];
int    bytesRead = inputstream.read(data);

while(bytesRead != -1) {
doSomethingWithData(data, bytesRead);

bytesRead = inputstream.read(data);
}
inputstream.close();


首先,上面代码创建了一个字节数组。然后创建了一个叫bytesRead的int类型变量,去接受每次调用read(byte[])方法的返回值。

在循环里,doSomethingWithData()方法被循环调用,传递进去读了多少字节和字节数组。在循环的最后把数据重新读到数组一遍。

不用花太多精力放在研究怎么样使用read(byte[], int offset, int length)方法来替换read(byte[])。你完全可以在任何时候用read(byte[], int offset, int length)替换掉read(byte[])方法。

mark()和reset()

InputStream类中有两个方法mark()和reset(),但是它的子类不一定有支持(或者说子类有没有重写此方法)。

如果一个InputStream的子类支持这两个方法,子类会重写markSupported()方法并返回 true。相反的,如果返回false,则子类不支持这两个方法。

mark()方法在InputStream设置一个内部的标记,标记在流中哪个数据到目前为止已经被读取。使用InputStream的代码,可以根据这个标记位来继续读取数据。

在读取流的过程汇总,如果想退回到标记的那点上,可以调用reset()方法。然后InputStream退回到标记位上,开始从这个位置开始返回数据。这当然会导致多次返回一些数据

当实现一个解析器时,会经常用到这两个方法。解析器读取一个InputStream时会提前读,如果解析器没有找到它想要的,他可能需要回退并且将已读数据和其他的数据进行匹配。

OutputStream

Java IO API中,OutputStream是所有输出流的基类。子类包括BufferedOutputStream,FileOutputStream等等。想要了解所有的流相关的清单,请看“Java IO Overview”章节

OutputStream介绍和数据目的地

OutputStream经常用来连接到数据目的地,比如文件,网络连接,管道等等。OutputStream将所有数据的数据写入目的地后即结束。

write(byte)

write(byte)方法用来向流中写入单个字节。参数为int类型,也就是准备写出的数据。只有int值的第一个字节会被写入。其余的都会被忽略。

OutputStream的子类可以有更多write()方法。例如,DataOutputStream可以写java基础类型如int,long,float,double,boolean等。相对应的方法writeBoolean(),writeDouble()等

下面是OutputStream的write()例子:

OutputStream output = new FileOutputStream("c:\\data\\output-text.txt");

while(hasMoreData()) {
int data = getMoreData();
output.write(data);
}
output.close();


这个例子首先创建了一个FileOutputStream去获取要读的数据文件。然后进入一个循环,循环的条件是有数据,这只是一个伪代码,并没有去展示怎么确定有数据。

在循环里,调用了方法getMoreData()去获取下一次要往OutputStream写的数据,然后写入数据。

write(byte[])

OutputStream也有write(byte[] bytes)方法和write(byte[] bytes, int offset, int length)方法,这两个都是写入一个或部分数组内的字节到OutputStream。

write(byte[] bytes)方法是将数组内所有数据写入到OutputStream。而write(byte[] bytes, int offset, int length)可以规定从哪里开始写,一共写多少长度的数据。

flush()

OutputStream的flush()方法刷新所有已经写入OutputStream的数据到数据目的地。例如,如果OutputStream是一个FileOutputStream的话,写入FileOutputStream的的字节可能并没有全部被写入到磁盘,他们可能在内存缓冲区里,虽然你的Java代码已经将数据写入到FileOutputStream,但是通过flush()方法,你可以确保数据都已经写到磁盘(网络连接或其他的数据目标地)

close()

当你往OutputStream写完数据的时候你会去关闭掉流,那么就要利用close()方法。由于OutputStream各式的写入方法可能会抛出IOException,那么你就要在finally代码块中来关闭流。下面是一个关闭流的例子:

OutputStream output = null;

try{
output = new FileOutputStream("c:\\data\\output-text.txt");

while(hasMoreData()) {
int data = getMoreData();
output.write(data);
}
} finally {
if(output != null) {
output.close();
}
}


这个例子是在finally中调用close()方法,确保流被关闭。但是它依然没有完美解决异常问题。但是我已经在“Java IO异常处理”章节说明了这个问题的解决办法。

FileInputStream

使用FileInputStream可以以字节流的形式来读取文件内容。FileInputStream是InputStream的子类,所以你可以使用FileInputStream像InputStream一样。

FileInputStream例子

下面是一个简单的例子:

InputStream input = new FileInputStream("c:\\data\\input-text.txt");

int data = input.read();
while(data != -1) {
//do something with data...
doSomethingWithData(data);

data = input.read();
}
input.close();


注意:为了保证代码思路清晰,这里并没有考虑异常处理的情况。

FileInputStream的构造方法

FileInputStream提供了三种构造方法来创建一个实例。我会在这里先介绍前两个。

第一个构造方法有一个String类型的参数。这个参数是指你想要读取的文件路径。下面是一个例子:

String path = "C:\\user\\data\\thefile.txt";

FileInputStream fileInputStream = new FileInputStream(path);


注意路径字符串。他需要两个“\”来生成一个“\”。因为“\”在Java中是转义字符。所以你想写一个“\”就得用两个“\”来表示。

在linux或unix中,文件的路径像下面这样:

String path = "/home/jakobjenkov/data/thefile.txt";


注意使用常规的分隔符(/)来作为目录分隔符。这是如何在linux或unix上写文件路径。实际上,以我的经验,Java应该也理解你在windows上用”/”作为路径分隔符,类似这样的:c:/user/data/thefile.txt,但是别听我的,你去你的系统上测试一下。

第二个构造方式是提供一个File类型的参数。这个参数你可以传入你想要读取的文件:

String path = "C:\\user\\data\\thefile.txt";
File   file = new File(path);

FileInputStream fileInputStream = new FileInputStream(file);


具体选用哪个构造函数,这取决于你有什么。如果你已经有一个String或File,那么只要用相应的就好了。将String转为File或者将File转为String,并没有太大的区别。

read()

FileInputStream的read()方法会返回一个int值,它是读取的字节。如果返回 -1,那么说明已经读取完毕。-1是int值,而不是一个byte值,这里可是不一样的。这个方法和InputStream中的read()使用是一样的。

read(byte[])

FileInputStream也有两个read()方法,使用的的方式也是和InputStream章节是一样的。

close()

和其他的InputStream一样,FileInputStream也需要在使用后关闭,调用close()方法即可。至于异常处理,可以参考相关的异常处理章节。

FileOutputStream

FileOutputStream可以以流的形式写出一个文件。他是OutputStream的子类。

OutputStream例子

下面是FileOutputStream的简单例子:

OutputStream output = new FileOutputStream("c:\\data\\output-text.txt");

while(moreData) {
int data = getMoreData();
output.write(data);
}
output.close();


OutputStream的构造方法

OutputStream包含了一些使用的构造方法,我会介绍比较常用的一些构造方法。

第一个构造方法需要传入一个String类型的参数,即你要写出数据的目标文件路径:

String path = "C:\\users\\jakobjenkov\\data\\datafile.txt";

FileOutputStream output = new FileOutputStream(path);


第二个构造方法为传入一个File类型的参数,它指向的是系统上的一个文件:

String path = "C:\\users\\jakobjenkov\\data\\datafile.txt";
File   file = new File(path);

FileOutputStream output = new FileOutputStream(file);


覆盖文件和增量写入文件

如果你创建了一个FileOutputStream,并准备输出到一个已经存在的文件,这时你不得不去决定,是要覆盖这个文件还是增量的去写入。这取决于如何使用构造方法。

如果构造方法只传入一个参数也就是文件全名,那么就会覆盖已存在的文件:

OutputStream output = new FileOutputStream("c:\\data\\output-text.txt");


然后还有一个两个参数的构造方法,一个参数是文件名,还有一个参数为boolean类型,它表明了你是想覆盖还是想增量写入。下面是两个例子:

OutputStream output = new FileOutputStream("c:\\data\\output-text.txt", true); //增量写入文件

OutputStream output = new FileOutputStream("c:\\data\\output-text.txt", false); //覆盖文件


write()

这个方法需要传入一个int类型的你想要写出的数据。

写字节数组

由于FileOutputStream是OutputStream的子类,所以你也可以写字节数组,来替代每次仅写一个字节。

flush()

当你把数据写入FileOutputStream,数据有可能缓存在计算机的内存中,会晚一些写入磁盘。例如每当有“X”数量的数据需要写入时,或者FileOutputStream流已经关闭。

如果你想确定在流未关闭并且所有的数据已经写入磁盘,那么你可以调用flush()方法。

close()

和其他的OutputStream一样,FileOutputStream也需要早使用后关闭它。只要调用close()方法即可。

RandomAccessFile

使用RandomAccessFile可以让你在文件中来回移动进行来读写操作,也可以覆盖文件中的某部分内容。这是FileInputStream和FileOutputStream做不到的。

创建RandomAccessFile

使用之前,你必须实例化一个RandomAccessFile对象,下面展示如何撞见一个对象实例:

RandomAccessFile file = new RandomAccessFile("c:\\data\\file.txt", "rw");


注意构造方法的第二个参数为“rw”。这是打开文件的模式。“rw”代表为只读模式。更多模式请查看java API手册。

利用RandomAccessFile在文件中跳转

在特殊的位置来读写,你必须先将指针移动到指定位置,这个可以用seek()方法。利用getFilePointer()可以得到当前的文件中指针位置。下面是一个简单的例子:

RandomAccessFile file = new RandomAccessFile("c:\\data\\file.txt", "rw");
file.seek(200);
long pointer = file.getFilePointer();
file.close();


从文件中读取内容

读取内容,可以用RandomAccessFile众多read()方法中的一个,下面是一个例子:

RandomAccessFile file = new RandomAccessFile("c:\\data\\file.txt", "rw");
int aByte = file.read();
file.close();


read()方法每次读取一个字节,在当前文件中指针所指向的位置。

给你一个javaDOC没有提及的东西:read()方法运行时,文件指针增长是在字节刚读取之后。这意味着你可以继续调用方法而不用手动的指定文件指针。

写入到文件

写入到文件,可以调用其众多write()方法的其中一个,下面是一个例子:

RandomAccessFile file = new RandomAccessFile("c:\\data\\file.txt", "rw");
file.write("Hello World".getBytes());
file.close();


和read()方法一样,在调write()方法之后文件指针会自动像前移动。这样就不用手动去移动文件指针。

close()

在使用完RandomAccessFile之后,你必须调用close()方法。你可以看教程中之前的例子来学如果关闭流。

RandomAccessFile的异常处理

为了让上面的示例看起来更清晰,并没了考虑异常处理。然而,RandomAccessFile必须在使用后将其关闭,就像其他的流或reader/writer一样,流的关闭前后的异常处理,请看第十二章节。

File

Java IO 的File类可以帮助你访问底层的文件系统,使用File类你可以:

查看文件或目录是否存在

如果目录不存在,可以创建

读取文件的长度

删除或移动文件

删除文件

查看路径指向的事文件还是目录

读取目录下的文件列表

这篇文章会告诉你更多的相关操作。

注意:File只允许你访问文件和文件系统的元数据,如果你需要读写文件的内容,那么你可以使用FileInputStream,FileOutputStream和RandomAccessFile等。

注意:如果你使用的是Java NIO,你就得使用java.nio.FileChannel。(你可以两种方法都使用,但是你只是想用Java NIO)

实例化java.io.File

在你使用File类对文件系统做一些操作之前,你必须要创建一个File实例。下面是如何创建一个实例:

File file = new File("c:\\data\\input-file.txt");


很简单对么?File类也有一些其他的构造方法供你使用。

查看文件是否存在

一旦实例化了File,你就可以检查相应的文件是否存在。如果文件不存在File类的构造方法也不会执行失败。要检查文件是否存在,调用exists()方法,下面是一个例子:

File file = new File("c:\\data\\input-file.txt");

boolean fileExists = file.exists();


如果目录不存在,那么创建一个

你可以用File类创建一个目录,如果该目录不存在。相关的方法为mkdir()和mkdirs()。

利用mkdir()创建一个文件夹,如果该文件夹不存在:

File file = new File("c:\\users\\jakobjenkov\\newdir");
boolean dirCreated = file.mkdir();


假如c:\users\jakobjenkov目录已经存在,上面的代码会创建一个jakobjenkov的子目录newdir。如果目录已经创建好,mkdir()会返回true,如果没有则返回false。

mkdirs()会创建所有所有参数中不存在的路径:

File file = new File("c:\\users\\jakobjenkov\\newdir");

boolean dirCreated = file.mkdirs();


假如C盘已经存在,例子中的代码会创建所有的相关文件夹,如果创建好则返回true,反之则为not。

文件长度

以字节读取文件的长度,调用length()方法,下面是一个例子:

File file = new File("c:\\data\\input-file.txt");

long length = file.length();


重命名或移动文件

重命名或移动一个文件,调用renameTo()方法:

File file = new File("c:\\data\\input-file.txt");

boolean success = file.renameTo(new File("c:\\data\\new-file.txt"));


正如前面所简要提到的,renameTo()方法也可以用来移动文件到另一个目录。给renameTo()方法传递的新目录名称不要与文件所在的目录一样。

renameTo()方法返回布尔类型的值,表示重命名是否成功。导致重命名或移动文件失败有很多原因,比如文件已经被其他程序打开,没有足够的权限等等。

删除文件

删除文件需要调用delete() 方法:

File file = new File("c:\\data\\input-file.txt");

boolean success = file.delete();


此方法也会返回一个布尔值表示文件是否删除成功。删除文件失败的原因可能有文件已经被其他程序打开,没有足够的权限等等。

确定指向的路径是文件还是目录

File实例可以指向文件或目录。你可以检查指向的具体是什么,这里调用isDirectory()方法。如果方法返回true那么说明指向的是目录,反之则为文件:

File file = new File("c:\\data");

boolean isDirectory = file.isDirectory();


读取目录下的文件列表

你可以通过调用list()方法或listFiles() 方法获得一个目录下的文件列表,list()方法返回一个String数组,代表指向的文件或目录的绝对路径。listFiles() 方法返回一个File数组,代表着相应文件的File对象:

File file = new File("c:\\data");

String[] fileNames = file.list();

File[]   files = file.listFiles();
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐