Java的IO操作
最近想用Java写一个爬虫,知乎了一下,很多人推荐如果业务逻辑不太复杂,都推荐使用国内大牛写的的一个框架webmagic,这个是java实现的,思路参照谷歌的Scrapy
。但是实现爬虫需要用到很多关于IO操作和多线程,发现这两项一直都是我java比较模糊的地方,这次就顺便学习一下,我看的是《java编程思想》。
对于IO的存取,不仅存在与各种I/O源端和想与之通信的接收端(接收端包括文件、控制台、网络链接等等),而且可能还需要有多种不同的方式与它们通信(顺序、随机存取、缓冲、二进制、按字符、按字节、按字等)。对于java的类库设计来说,java通过创建了大量的类库来解决这些各种各样复杂的需求。下面根据不同的需求来慢慢分析各种类的使用。
一 . 本应该叫FilePath文件目录的File类
看着这个类名大家不要误会哟,它的作用并不是处理文件的,而是用来处理文件路径(或则叫文件目录)的实用工具类。它代一个特定文件的名称(这里的名称是包含路径的名称),又能代表一个目录下的一组文件的名称
1. 目录列表器
这个实现查看一个文件目录下的文件列表。可以使用两种方法实现:1.调用不带参数的list()方法获取目录下的全部文件的列表。2.通过一个“目录过滤器”,获取规定条件下的File对象,这里的过滤一般使用正则表达式实现。
话不多说,先贴代码:DirList3.java
package io; import java.util.regex.*; import java.io.*; import java.util.*; public class DirList3 { public static void main(final String[] args) { File path = new File("./src/io"); String[] list; if(args.length == 0) list = path.list(); else list = path.list(new FilenameFilter() { private Pattern pattern = Pattern.compile(args[0]); public boolean accept(File dir, String name) { return pattern.matcher(name).matches(); } }); Arrays.sort(list, String.CASE_INSENSITIVE_ORDER); for(String dirItem : list) System.out.println(dirItem); } }
当我们显示不受限的目录下的全部文件时,直接对于File对象调用list()方法,该方法会返回一个字符数组,该字符数组就存储了该目录下的全部文件的文件目录。当我们要显示路劲下受限制的文件目录时(比如显示后缀为.java的java源文件),可以输入参数args,该参数的第一个元素可以输入正则表达式过滤,这时调用的是带参的list()方法,输入的参数是FilenameFilter对象,这是一个文件名称过滤器的类,该类很简单,里面只有一个接口方法accept(),所以我们这里使用一个匿名内部类然后实现该接口。实现该接口的原因在于把accept()方法供list()使用,使list()方法回调accept(),进而决定哪些文件包含在目录列表中,这里的回调用到了一种称为策略模式的设计模式,关于策略模式,具体看设计模式板块。accept()方法必须接受一个代表某个特定文件所在目录的File对象和包含那个文件名的一个string。注意:list()会为此目录对象下的每个文件名调用accept(),来判断该文件是否包含在内,判断结果由布尔值表示。具体的我们可以看JDK底层源代码:
//这个是JDK底层源代码,对于带参的list()的实现:
public String[] list(FilenameFilter filter) { String names[] = list(); if ((names == null) || (filter == null)) { return names; } ArrayList v = new ArrayList(); for (int i = 0 ; i < names.length ; i++) { if (filter.accept(this, names[i])) { v.add(names[i]); } } return (String[])(v.toArray(new String[v.size()])); }
2. 目录实用工具:
下面的代码是对一些工具类的一些封装。
package utils; import java.util.regex.*; import java.io.*; import java.util.*; /** * 文件和目录的工具类 * final类:不可被继承的类 * @author Administrator * */ public final class Directory { /** * function:根据传入的file目录和正则表达式,获取该目录中的文件构成的File对象数组 * @param dir * @param regex * @return 返回满足指定过滤器的抽象路径名表示的目录下的文件和目录的数组 */ public static File[] local(File dir, final String regex) { return dir.listFiles(new FilenameFilter() { private Pattern pattern = Pattern.compile(regex);//设置正则表达式 public boolean accept(File dir, String name) { return pattern.matcher(new File(name).getName()).matches(); } }); } //Overloaded public static File[] local(String path, final String regex) { return local(new File(path), regex); } /** * 内部静态类 * @author Administrator * */ public static class TreeInfo implements Iterable<File> { public List<File> files = new ArrayList<File>(); public List<File> dirs = new ArrayList<File>(); // The default iterable element is the file list: public Iterator<File> iterator() { return files.iterator(); } void addAll(TreeInfo other) { files.addAll(other.files); dirs.addAll(other.dirs); } public String toString() { return "dirs: " + PPrint.pformat(dirs) + "\n\nfiles: " + PPrint.pformat(files); } } public static TreeInfo walk(String start, String regex) { // Begin recursion return recurseDirs(new File(start), regex); } public static TreeInfo walk(File start, String regex) { // Overloaded return recurseDirs(start, regex); } public static TreeInfo walk(File start) { // Everything return recurseDirs(start, ".*"); } public static TreeInfo walk(String start) { return recurseDirs(new File(start), ".*"); } /** * * @param startDir * @param regex * @return */ static TreeInfo recurseDirs(File startDir, String regex){ TreeInfo result = new TreeInfo(); for(File item : startDir.listFiles()) { if(item.isDirectory()) { result.dirs.add(item); result.addAll(recurseDirs(item, regex)); } else // Regular file if(item.getName().matches(regex)) result.files.add(item); } return result; } // Simple validation test: public static void main(String[] args) { if(args.length == 0) System.out.println(walk(".")); else for(String arg : args) System.out.println(walk(arg)); } }
二 . I/O的典型使用方式
java的IO操作类库众多,首先看一下类的树结构:下面删除了一些不常用的,只列出了一些常用的,主要就是:File类、 InputStream及其继承子类、 OutputStream及其继承子类、Reader及其继承子类、Writer及其继承子类、RandomAccessFile类。
- java.lang.Object
- java.io.File (implements java.lang.Comparable<T>,
java.io.Serializable)- java.io.InputStream (implements java.io.Closeable)
- java.io.ByteArrayInputStream
- java.io.FileInputStream
- java.io.FilterInputStream
- java.io.BufferedInputStream
- java.io.DataInputStream (implements java.io.DataInput)
- java.io.LineNumberInputStream
- java.io.PushbackInputStream
- java.io.ObjectInputStream (implements java.io.ObjectInput,
java.io.ObjectStreamConstants)- java.io.PipedInputStream
- java.io.SequenceInputStream
- java.io.StringBufferInputStream
- java.io.OutputStream (implements java.io.Closeable,
java.io.Flushable)
- java.io.ByteArrayOutputStream
- java.io.FileOutputStream
- java.io.FilterOutputStream
- java.io.BufferedOutputStream
- java.io.DataOutputStream (implements java.io.DataOutput)
- java.io.PrintStream (implements java.lang.Appendable,
java.io.Closeable)- java.io.ObjectOutputStream (implements java.io.ObjectOutput,
java.io.ObjectStreamConstants)- java.io.RandomAccessFile (implements java.io.Closeable,
java.io.DataInput, java.io.DataOutput)- java.io.Reader (implements java.io.Closeable,
java.lang.Readable)
- java.io.BufferedReader
- java.io.LineNumberReader
- java.io.CharArrayReader
- java.io.FilterReader
- java.io.PushbackReader
- java.io.InputStreamReader
- java.io.FileReader
- java.io.PipedReader
- java.io.StringReader
- java.io.Writer (implements java.lang.Appendable,
java.io.Closeable, java.io.Flushable)
- java.io.BufferedWriter
- java.io.CharArrayWriter
- java.io.FilterWriter
- java.io.OutputStreamWriter
- java.io.FileWriter
- java.io.PipedWriter
- java.io.PrintWriter
- java.io.StringWriter
上面这些类的使用具体可以去参考JDK的文档。我们可以发现对于文件的读写类大都是成对出现的,InputStream或则Reader实现读取(从流中读取),OutputStream或则Writer实现写入功能(输出至流)。这里的流可以是控制台,可以是文件,也可以是Internet。
Java的IO类库需要多种不同功能的组合,这也正是使用装饰器模式的理由所在。FilterInputStream和FilterOutputStream用来提供装饰器类接口以控制特定的输入流InputStream与输出流OutputStream两个类。
InputStream 和 OutputStream
主要是面向字节形式的IO中;而Reader 和Writer
主要是面向字符的IO操作。
尽管可以通过不同的方式组合使用IO类流,但是常用的就只有那么一些,下面来分别分析。
1. 缓冲方式实现输入文件
对于一个文件用于字符输入,为了提高速度,我们一般希望能够对那个文件进行缓冲,所以我们使用BufferedReader 这个类实现。具体的代码如下:
package io.input; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; /** * 缓冲输入文件 * 打开一个文件用于字符输入,读取字符 * @author Administrator */ public class BufferedInputFile { /** * @param fileName 文件名 * @return 文件内的字符串 * @throws IOException */ public static String read(String fileName) throws IOException { //inputStream输入流 //创建一个(使用默认大小输入缓冲区的)缓冲字符输入流 BufferedReader in = new BufferedReader( new FileReader(fileName)); String s= null; StringBuilder sb = new StringBuilder(); while( (s = in.readLine()) != null ) { sb.append(s + "\n"); } in.close(); return sb.toString(); } /* * testing */ public static void main(String[] args) throws IOException { System.out.println( read("./src/io/BufferedInputFile.java") ); } }
2. 从内存输入
从内存中读取数据,使用的是StringReader 这个类实现,下面的代码首先从一个文件中读取出数据,然后存放在内存中,然后通过StringReader
的read()方法每次读取一个字符来实现。
package io.input; import java.io.IOException; import java.io.StringReader; /** * 从内存输入 * @author Administrator */ public class MemoryInput { public static void main(String[] args) throws IOException { String str = BufferedInputFile.read("./src/io/MemoryInput.java"); //str这时存放在内存中 //StringReader :源为字符串的字符流 StringReader in = new StringReader(str); int c = 0; while ( (c = in.read()) != -1) { System.out.print( (char)c ); } } }
3. 格式化的内存输入
读取格式化的数据,可以使用DataInputStream 这个类,很明显这个类是面向字节的IO类读取的,所以我们不能使用Reader而必须使用InputStream。代码如下:
package io.input; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.EOFException; import java.io.IOException; /** * 格式化的内存输入:面向字节读取(8位),而我们一般的字符读取是16位,所以字节读取出来的中文是乱码 * 所以必须使用inputStream。不能使用Reader * @author Administrator * */ public class FormattedMemoryInput { public static void main(String[] args) throws IOException { /* try { //先读取java文件的字符串并转换成 字节数组 byte[] bs = BufferedInputFile.read("./src/io/FormattedMemoryInput.java").getBytes(); //创建字节数组输入流 ByteArrayInputStream ins = new ByteArrayInputStream(bs); //创建输入流 DataInputStream in = new DataInputStream(ins); while(true){ System.out.print( (char)in.readByte() ); } } catch (EOFException e) { System.err.println("end"); } */ //先读取java文件的字符串并转换成 字节数组 byte[] bs = BufferedInputFile.read("./src/io/FormattedMemoryInput.java").getBytes(); //创建字节数组输入流 ByteArrayInputStream ins = new ByteArrayInputStream(bs); //创建输入流 DataInputStream in = new DataInputStream(ins); while( in.available() != 0){ System.out.print( (char)in.readByte() ); } } }
上面的几个例子都是讲的文件的输入即文件的读取,下面来讲一下关于文件的输出即文件的写入:
4. 基本的文件输出
FileWriter对象可以向文件写入数据,通常我们会用BufferedWriter将其包装起来泳衣缓冲输出。为了提供格式化的输出,它被装饰成PrintWriter,在Java Se5中为PrintWriter 添加了一个辅助构造器,使得我们直接在PrintWriter
的构造函数中直接输入文件的路径就行了,不必去执行装饰工作。代码如下;
package io.output; import io.input.BufferedInputFile; import java.io.*; public class FileOutputShortcut { static String file = "./src/io/output/FileOutputShortcut.java"; static String fileOut = "./src/io/output/FileOutputShortcut.out"; public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader(new FileReader(file)); //此处简化了 PrintWriter out = new PrintWriter(fileOut); int lineCount = 1; String s; while((s = in.readLine()) != null ) out.println(lineCount++ + ": " + s); in.close(); out.close(); System.out.println(BufferedInputFile.read(fileOut)); } }
5. 文件读写的实用工具(把上面关于文件的读写进行封装成一个工具类)
先贴上代码:
package utils; import java.io.*; import java.util.*; /** * function:提供一些静态方法,像简单字符串一样读写文件 * //字符形式 * @author Administrator * */ public class TextFile extends ArrayList<String> { private static final long serialVersionUID = 2025881288414881601L; /** * function:读取一个文件,返回文件内容的字符串形式 * @param fileName * @return */ public static String read(String fileName) { StringBuilder sb = new StringBuilder(); try { //获取的绝对路径(磁盘名开始) BufferedReader in= new BufferedReader(new FileReader( new File(fileName).getAbsoluteFile())); try { String s; while((s = in.readLine()) != null) { sb.append(s); sb.append("\n"); } } finally { in.close(); } } catch(IOException e) { throw new RuntimeException(e); } return sb.toString(); } /** * function:往文件里面写入一个字符串 * @param fileName * @param text */ public static void write(String fileName, String text) { try { PrintWriter out = new PrintWriter( new File(fileName).getAbsoluteFile()); try { out.print(text); } finally { out.close(); } } catch(IOException e) { throw new RuntimeException(e); } } /** * function:构造器: * 读一个文件返回字符串,并以一个正则表达式分离,然后保存在一个ArrayList中 */ public TextFile(String fileName, String splitter) { super( Arrays.asList( read(fileName).split(splitter) ) ); if(get(0).equals("")) remove(0); } /** * function:构造器 * 默认以换行符为正则 * @param fileName */ public TextFile(String fileName) { this(fileName, "\n"); } public void write(String fileName) { try { PrintWriter out = new PrintWriter(new File(fileName).getAbsoluteFile()); try { for(String item : this) out.println(item); } finally { out.close(); } } catch(IOException e) { throw new RuntimeException(e); } } //main 测试 public static void main(String[] args) { String file = read("./src/utils/TextFile.java"); write("./src/utils/test.txt", file); TextFile text = new TextFile("./src/utils/test.txt"); text.write("./src/utils/test2.txt"); TreeSet<String> words = new TreeSet<String>( new TextFile("./src/utils/TextFile.java", "\\W+")); System.out.println(words.headSet("a")); } }
6. 读取二进制文件
与第5点的工具类很类似:
package utils; import java.io.*; /** * function:读取二进制格式的文件 * @author Administrator * */ public class BinaryFile { public static byte[] read(File bFile) throws IOException{ BufferedInputStream bf = new BufferedInputStream( new FileInputStream(bFile) ); try { byte[] data = new byte[bf.available()]; bf.read(data);//从此输入流中将 byte.length个字节的数据读入 data数组中。 return data; } finally { bf.close(); } } public static byte[] read(String bFile) throws IOException { return read( new File(bFile).getAbsoluteFile() ); } }
三. 标准IO
按照java的标准IO模型,java提供了System.in、System.out、System.err三种标准IO。其中System.out和System.err已经事先被包装成了printStream对象,然而System.in却是一个未经过加工和包装的InputStream。所以我们可以直接使用System.out和System.err,但是对于System.in使用前必须要先包装。
下面展示一个例子,用于包装使用System.in。回显我们在控制台输入的每一行。
package io; import java.io.*; public class Echo { public static void main(String[] args) throws IOException { BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); String s; while((s = stdin.readLine()) != null && s.length()!= 0) System.out.println(s); } }
1.标准IO的重定向
我觉得标准IO的一个最大的用处在于标准IO的重定向,比如我们可以把对于控制台的输出全部重定向输出到一个文件中,这样就类似于实现了记录日志的功能。
对于三个标准IO的重定向使用下面的三个函数:
setIn(InputStream)
setOut(PrintStream)
setErr(PrintStream)
示例代码如下:
package io; import java.io.*; /** * 标准IO重定向,可以吧控制台输出到指定文件中,类似于日志 * @author Administrator */ public class Redirecting { public static void main(String[] args) throws IOException { PrintStream console = System.out; BufferedInputStream in = new BufferedInputStream( new FileInputStream("./src/io/Redirecting.java")); PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream("./src/io/test.out"))); System.setIn(in); System.setOut(out);//标准输出输出重定向到test.out这个文件中。 System.setErr(out);//标准输出输出重定向到test.out这个文件中。 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String s; while((s = br.readLine()) != null && s.length()!= 0 ) System.out.println(s); out.close(); // Remember this! System.setOut(console); } }