使用guava处理文件
读写文件是一个程序员的核心能力! 令人意外的事,虽然java有非常丰富的并且强壮的I/O接口,但是却不怎么好用。 虽然在java7中已经有了一些改善。 但是我们还是要学一下guava的I/O相关的工具。 这一章我们要学习一下内容:
-- 使用Files类处理文件的移动和复制,或者从文件中读取内容到字符串中
-- Closer 类 给我们提供非常简洁干净的方式去确保文件被正确关闭
-- ByteSource 和 CharSource 类,是inputStream和readers的不可变实现类
-- ByteSink 和 CharSink 类,是 outputStreams 和 writers的不可变实现类
-- CharStreams 和 ByteStreams 类 提供了静态方法去分别处理 Readers,Writers,InputStreams,和OutputStreams
-- BaseEncoding 类,提供方法处理 byte序列和ASCII码
复制文件
Files 类提供了很多非常有用的方法处理文件对象,对于任何一个java开发者,copy文件也算是一个比较有挑战性的过程,但是我们看一下在guava的帮助下,怎样方便的额copy一个文件:
File original = new File("path/to/original");
File copy = new File("path/to/copy");
Files.copy(original, copy);
移动/重命名文件
移动文件和复制文件一样在java中也是非常笨重,但是使用guava就会变的比较简单:
public class GuavaMoveFileExample {
public static void main(String[] args) {
File original = new File("src/main/resources/copy.txt");
File newFile = new File("src/main/resources/newFile.txt");
try{
Files.move(original, newFile);
}catch (IOException e){
e.printStackTrace();
}
}
}
上面的例子中,我们将copy.txt文件重命名为newFile.txt文件
处理字符串一样处理文件
Files类可以读取将文件的内容读取到字符串数组中,并返回文件的第一行, 下面的例子中我们将看到怎样将文件读取到字符串数组中:
@Test
public void readFileIntoListOfStringsTest() throws Exception{
File file = new File("src/main/resources/lines.txt");
List<String> expectedLines = Lists.newArrayList("The quick
brown","fox jumps over","the lazy dog");
List<String> readLines = Files.readLines(file,
Charsets.UTF_8);
assertThat(expectedLines,is(readLines));
}
在这个例子中,我们使用单元测试的方式从文件中读取内容并和期望读取到的内容进行比较。 数组中字符串的分行符都已经被去除,但是其他的空白字符串都被保留。 还有另外一个版本的Files.readLines方法,接受一个LineProcessor实例作为额外的参数,每一行都会经过LineProcessor.processLine方法处理,这个方法返回一个boolean值,当processLine方法返回false,或者文件读取完成,文件处理会终止。 下面我们来看一下下面的一个CSV文件, 文件包含的内容如下:
"Savage, Tom",Being A Great Cook,Acme Publishers,ISBN-
123456,29.99,1
"Smith, Jeff",Art is Fun,Acme Publishers,ISBN-456789,19.99,2
"Vandeley, Art",Be an Architect,Acme Publishers,ISBN-
234567,49.99,3
"Jones, Fred",History of Football,Acme Publishers,ISBN-
345678,24.99,4
"Timpton, Patty",Gardening My Way,Acme Publishers,ISBN-
4567891,34.99,5
为了提取书的名称,我们可以实现下面的一个LineProcessor实例:
public class ToListLineProcessor implements
LineProcessor<List<String>>{
private static final Splitter splitter = Splitter.on(",");
private List<String> bookTitles = Lists.newArrayList();
private static final int TITLE_INDEX = 1;
@Override
public List<String> getResult() {
return bookTitles;
}
@Override
public boolean processLine(String line) throws IOException {
bookTitles.add(Iterables.get(splitter.split(line),TITLE_
INDEX));
return true;
}
这里我们以逗号分隔读取到的字符串,并将title放到List 中,这里我们都是返回true,因为我们想获得所有的书的标题。 下面是一个单元测试确保我们上面的LineProcessor的逻辑处理是正确的:
@Test
public void readLinesWithProcessor() throws Exception {
File file = new File("src/main/resources/books.csv");
List<String> expectedLines = Lists.newArrayList("Being A Great
Cook","Art is Fun","Be an Architect","History of Football","Gardening
My Way");
List<String> readLines = Files.readLines(file, Charsets.UTF_8,
new ToListLineProcessor());
assertThat(expectedLines,is(readLines));
}
Hashing a file
给一个文件产生HashCode,使用原有的java代码的情况下,会产生很多标准化的代码。 但是使用Guava就可以很容易的给文件产生一个Hash码.
public class HashFileExample {
public static void main(String[] args) throws IOException {
File file = new File("src/main/resources/sampleTextFileOne.
txt");
HashCode hashCode = Files.hash(file, Hashing.md5());
System.out.println(hashCode);
}
}
上面的例子中,我们使用的Files的hash方法,传入File对象,和 HashFunction实例,这里的HashFunction我们使用的是HashFunction的MD5实现。
写文件
处理input/output streams时,我们一般会有下面的几个步骤:
- 打开文件的输入、输出流
- 从文件中读取字节流
- 操作完成后,在finally块中保证所有的资源关闭
当我们在代码中一遍遍重复这样的过程后,代码将变得不好维护, Files类提供了非常方便的方法去写或者追加内容到文件中。 一般情况只需要一行代码就可以搞定。
写、追加 文件内容
下面是一个追加文件内容的例子:
@Test
public void appendingWritingToFileTest() throws IOException {
File file = new File("src/test/resources/quote.txt");
file.deleteOnExit();
String hamletQuoteStart = "To be, or not to be";
Files.write(hamletQuoteStart,file, Charsets.UTF_8);
assertThat(Files.toString(file,Charsets.UTF_8),is(hamletQuoteStart));
String hamletQuoteEnd = ",that is the question";
Files.append(hamletQuoteEnd,file,Charsets.UTF_8);
assertThat(Files.toString(file, Charsets.UTF_8),
is(hamletQuoteStart + hamletQuoteEnd));
String overwrite = "Overwriting the file";
Files.write(overwrite, file, Charsets.UTF_8);
assertThat(Files.toString(file, Charsets.UTF_8),
is(overwrite));
}
上面的例子中,我们使用了一个单元测试做了如下的事情:
- 创建一个文件,并且保证这个文件如果存在的话,就将其删除
- 使用File.write方法写文件,并且保证写入是成功的
- 使用File.append方法追加内容到字符串中,并且同样保证追加的内容是成功的
- 使用File.write方法覆盖之前的内容,并保证之前的内容被覆盖了
虽然这是一个比较简单的例子,但是注意到我们这里并没有任何打开或关闭文件的操作,这些基本的操作已经由guava帮助我们完成了。
InputSupplier and OutputSupplier
Guava 有InputSupplier 和 OutputSupplier 接口作为InputStreams/Readers 和 OutputStream/Writers的门面。 下面章节中,我们将看到我们是怎样从InputSuppliers和OutputSuppliers中受益,使用这些接口guava会自动帮助我们open,flush,close用到的资源。
Sources and Sinks
Guava I/O中有source和sink分别对应为reading,writing文件,Sources,Sinks不是streams readers writers 但是提供了相同的功能。 Source 和 Sink对象可以按照下面两个方式使用:
--
ByteSource
ByteSource 代表了可读的bytes类型的数据源,典型的是我们可以从文件中读取byte类型的数据,下面我们从文件中读取一个ByteSource。
@Test
public void createByteSourceFromFileTest() throws Exception {
File f1 = new File("src/main/resources/sample.pdf");
byteSource = Files.asByteSource(f1);
byte[] readBytes = byteSource.read();
assertThat(readBytes,is(Files.toByteArray(f1)));
}
这里我们使用Files.asByteSource方法创建一个ByteSource。 接着我们调用read方法读取字节数组。 最后我们假设读取到的bytes和调用的Files.toByteArray方法得到的值是一致的.
ByteSink
ByteSink类代表了一个可以写的byte source。 我们可以写入bytes到文件或者byte数组。 从文件中创建一个ByteSink.我们可以按照如下的方式:
@Test
public void testCreateFileByteSink() throws Exception {
File dest = new File("src/test/resources/byteSink.pdf");
dest.deleteOnExit();
byteSink = Files.asByteSink(dest);
File file = new File("src/main/resources/sample.pdf");
byteSink.write(Files.toByteArray(file));
assertThat(Files.toByteArray(dest),is(Files.
toByteArray(file)));
}
和上面的读取类似这里就不再描述.
Copying from a ByteSource class to a ByteSink class
下面我们将学习怎样将ByteSource和ByteSink整合起来使用,这样就可以屏蔽具体的细节,只要关注 ByteSource和ByteSink。
@Test
public void copyToByteSinkTest() throws Exception {
File dest = new
File("src/test/resources/sampleCompany.pdf");
dest.deleteOnExit();
File source = new File("src/main/resources/sample.pdf");
ByteSource byteSource = Files.asByteSource(source);
ByteSink byteSink = Files.asByteSink(dest);
byteSource.copyTo(byteSink);
assertThat(Files.toByteArray(dest),
is(Files.toByteArray(source)));
}
上面的例子中我们通过使用Files.asByteSource 和 Files.asByteSink方法 创建了 ByteSource和ByteSink实例。 然后我们调用ByteSource.copyTo方法 将bytes写入到ByteSink对象中。 然后验证一下写入的是否正确。 ByteSink 也有copyTo方法,接受一个outputStream将字节写入到目标文件中。
ByteStreams and CharStreams
ByteStreams 是与InputStream和OutputStream的工具类,CharStreams是Reader和Writer的工具类, 这两个工具类中有很多方法,我们这里只关注一些比较有趣的方法.
Limiting the size of InputStreams
ByteStreams,limit方法接受一个InputStream和一个长整形的参数,返回一个包装了好了的 InputStream 仅仅包含指定长度的的字节数。下面我们看一个例子:
@Test
public void limitByteStreamTest() throws Exception {
File binaryFile = new
File("src/main/resources/sample.pdf");
BufferedInputStream inputStream = new
BufferedInputStream(new FileInputStream(binaryFile));
InputStream limitedInputStream =
ByteStreams.limit(inputStream,10);
assertThat(limitedInputStream.available(),is(10));
assertThat(inputStream.available(),is(218882));
}
Joining CharStreams
使用CharStreams.join方法可以将多个文件的内容一起写入到一个文件中去.
@Test
public void joinTest() throws Exception {
File f1 = new
File("src/main/resources/sampleTextFileOne.txt");
File f2 = new
File("src/main/resources/sampleTextFileTwo.txt");
File f3 = new File("src/main/resources/lines.txt");
File joinedOutput = new
File("src/test/resources/joined.txt");
joinedOutput.deleteOnExit();
List<InputSupplier<InputStreamReader>> inputSuppliers() =
getInputSuppliers()(f1,f2,f3);
InputSupplier<Reader> joinedSupplier =
CharStreams.join(inputSuppliers());
OutputSupplier<OutputStreamWriter> outputSupplier =
Files.newWriterSupplier(joinedOutput, Charsets.UTF_8);
String expectedOutputString = joinFiles(f1,f2,f3);
CharStreams.copy(joinedSupplier,outputSupplier);
String joinedOutputString = joinFiles(joinedOutput);
assertThat(joinedOutputString,is(expectedOutputString));
}
private String joinFiles(File ...files) throws IOException {
StringBuilder builder = new StringBuilder();
for (File file : files) {
builder.append(Files.toString(file,Charsets.UTF_8));
}
return builder.toString();
}
private List<InputSupplier<InputStreamReader>>
getInputSuppliers()(File ...files){
List<InputSupplier<InputStreamReader>> list =
Lists.newArrayList();
for (File file : files) {
list.add(Files.newReaderSupplier(file,Charsets.UTF_8));
}
return list;
}
我们看一下上面一大段代码的意思:
- 创建了4个文件对象,其中3个位输入文件对象,1个为输出文件对象
- 使用 Files.newReaderSupplier的静态方法创建InputSupplier实例
- 将3个InputSupplier逻辑上变成一个InputSupplier
- 调用Files.newWriterSupplier方法创建OutputSupplier
- 最后调用Files.toString方法获取要比较的数据
- 调用CharStreams.copy方法将InputSupplier数据写入到OutputSupplier
- 最后验证我们写入的数据是否和想象的一样
Closer
Closer类在guava中的作用是保证所有实现了Closeable接口的对象都能够调用Closer.close方法合理的关闭。 这个功能在java7中也有类似的实现 try-with-resources. 但是使用Closer的方式更加直观,具体的例子如下:
public class CloserExample {
public static void main(String[] args) throws IOException {
Closer closer = Closer.create();
try {
File destination = new File("src/main/resources/copy.
txt");
destination.deleteOnExit();
BufferedReader reader = new BufferedReader(new
FileReader("src/main/resources/sampleTextFileOne.txt"));
BufferedWriter writer = new BufferedWriter(new
FileWriter(destination));
closer.register(reader);
closer.register(writer);
String line;
while((line = reader.readLine())!=null){
writer.write(line);
}
} catch (Throwable t) {
throw closer.rethrow(t);
} finally {
closer.close();
}
}
}
BaseEncoding
当我们处理二进制数据时,我们有时候需要把二进制数据转换成可打印的ASCII码,我们当然也需要将已经编码的数据转换成原来的编码方式,BaseEncoding是一个抽象类包含一些静态工厂方法来创建不同编码方式的实例,下面是一个简单的例子:
@Test
public void encodeDecodeTest() throws Exception {
File file = new File("src/main/resources/sample.pdf");
byte[] bytes = Files.toByteArray(file);
BaseEncoding baseEncoding = BaseEncoding.base64();
String encoded = baseEncoding.encode(bytes);
assertThat(Pattern.matches("[A-Za-z0-
9+/=]+",encoded),is(true));
assertThat(baseEncoding.decode(encoded),is(bytes));
}
上面的例子中,我们获取了一个pdf文件,并且将其用Base64编码, 我们假设所有的字节都被编辑成了ASCII码, 然后又将获得到的base64编码的数据decode,BaseEncoding 类除了给我们简单的encode和decode,我们还可以包装outputSupplier bytesink,writer实例,这样在写入时,就可以使用我们指定的编码。 一样的我们也可以包装InputStream,bytesource,reader实例,再读取文件时进行decode. 下面我们看一个具体的例子:
@Test
public void encodeByteSinkTest() throws Exception{
File file = new File("src/main/resources/sample.pdf");
File encodedFile = new
File("src/main/resources/encoded.txt");
encodedFile.deleteOnExit();
CharSink charSink = Files.asCharSink(encodedFile,
Charsets.UTF_8);
BaseEncoding baseEncoding = BaseEncoding.base64();
ByteSink byteSink = baseEncoding.encodingSink(charSink);
ByteSource byteSource = Files.asByteSource(file);
byteSource.copyTo(byteSink);
String encodedBytes = baseEncoding.encode(byteSource.read());
assertThat(encodedBytes,is(Files.
toString(encodedFile,Charsets.UTF
_8)));
}
总结
我们学习了怎样使用InputSupplier和OutputSupplier处理文件的打开和关闭,然后我们还学习了怎样是用ByteSource,ByteSink,CharSource,CharSink 类,最后我们学习了使用BaseEncoding类将二进制数据转换为文本数据,