Java 7源码分析第18篇 - I/O操作之NIO

在NIO.2的文件I/O中,Path表示文件系统中的位置。这个Path类似于java.io.File类对象,并不表示一个已经存在的实际文件或目录,所以如果你想创建一个Path来表示即将要创建的目录或文件是完全可以的。但是要注意,如果这个文件不存在,读取时就会出现错误。

在File中已经介绍过,File转换为Path需要调用File类中的toPath()方法,同样,Path也可以转换为File,调用toFile()方法即可。java.nio.file.Path接口中定义了一些文件操作常用的方法。如创建Path、获取路径的相关信息等,如下:

Path listing = Paths.get("\\foo\\x");// 创建一个Path
		
System.out.println(listing.isAbsolute());// false 是否为绝对路径
System.out.println(listing.toAbsolutePath());// C:\foo\x
		
		
System.out.println( listing.getFileName() );// 获取文件名
System.out.println("Number of Name Elements in the Path "+ listing.getNameCount() );// 2
System.out.println("Parent Path " + listing.getParent() );// \foo
System.out.println("Root of Path " + listing.getRoot() );// // 从根到第二个元素之间的子路径,离根最近的为0,而离根最远的元素为getNameCount-1
System.out.println("Subpath from Root, 2 elements deep "+ listing.subpath(0, 2) );// foo\x

Paths类是一个工具类,提供返回一个路径的辅助方法,源代码如下:

public final class Paths {
    private Paths() { }
    //more
    public static Path get(String first, String... more) {
        return FileSystems.getDefault().getPath(first, more);
    }

    public static Path get(URI uri) {
        String scheme =  uri.getScheme();
        if (scheme == null)
            throw new IllegalArgumentException("Missing scheme");

        // check for default provider to avoid loading of installed providers
        if (scheme.equalsIgnoreCase("file"))
            return FileSystems.getDefault().provider().getPath(uri);

        // try to find provider
        for (FileSystemProvider provider: FileSystemProvider.installedProviders()) {
            if (provider.getScheme().equalsIgnoreCase(scheme)) {
                return provider.getPath(uri);
            }
        }

        throw new FileSystemNotFoundException("Provider \"" + scheme + "\" not installed");
    }
}
可以调用如上的两个方法获取Path对象。获取到后就可以进行操作了。下面来看一下查找和遍历目录树的例子。

public class TestDirectoryStream {

     public static void main(String[] args) throws IOException {
       Path listing = Paths.get("C:\\foo\\x");
       List l=TestDirectoryStream.listSourceFiles(listing);
		for(int i=0;i<l.size();i++ ){
			System.out.println(l.get(i));// 输出
		}
		
     }

     static List<Path> listSourceFiles(Path dir) throws IOException {
		List<Path> result = new ArrayList<>();
		try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir,"*.{c,h,cpp,hpp,txt}")) {
			for (Path entry : stream) { // stream实例可以迭代
				result.add(entry);
			}
		} catch (DirectoryIteratorException ex) {
			throw ex.getCause();
		}
		return result;
     }
}
查找并输出x目录下的所有符合glob模式匹配的文件。调用的主要方法就是Files类下的newDirectoryStream()方法,源代码如下:
public static DirectoryStream<Path> newDirectoryStream(Path dir, String glob)    throws IOException  {
        // avoid creating a matcher if all entries are required.
        if (glob.equals("*"))
            return newDirectoryStream(dir);
        // create a matcher and return a filter that uses it.
        FileSystem fs = dir.getFileSystem();
        final PathMatcher matcher = fs.getPathMatcher("glob:" + glob);
        DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
            @Override
            public boolean accept(Path entry)  {
                return matcher.matches(entry.getFileName());
            }
        };
        return fs.provider().newDirectoryStream(dir, filter);
    }
或者还可以遍历目录树并查找文件:

public class TestFileVisitor {

	public static void main(String[] args) throws IOException {
		Path startingDir = Paths.get("c:\\foo");
		Files.walkFileTree(startingDir, new FindJavaVisitor());
	}

	private static class FindJavaVisitor extends SimpleFileVisitor<Path> {
		public FileVisitResult preVisitDirectory(Path file,	BasicFileAttributes attrs) {
			return FileVisitResult.CONTINUE;
		}

		public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
			if (file.toString().endsWith(".txt")) {
				System.out.println(file.getFileName());
			}
			return FileVisitResult.CONTINUE;
		}
	}
}
用到的主要方法就是Files的walkFileTree(Path startingDir,FileVisitor<? super Path> visitor)。由于FileVisitor类接口中定义的方法很多,为了减少工作量,我们可以继承SimplefileVisitor类来实现部分方法,然后在这些方法中定义我们自己的功能,如visitFile()方法中将扩展名为.txt的文件输出。

如果要针对某个文件进行如下操作时,主要是通过提供的Files工具类来完成。

1、创建、删除

Path p=Paths.get("c:\\foo\\pp.txt");
//Files.createFile(p);// 创建pp.txt文件
Files.delete(p);// 删除pp.txt文件

2、移动、复制、重命名文件

 Path p1=Paths.get("c:\\foo\\pp.txt");
       Path p2=Paths.get("c:\\foo\\x\\pp2.txt");
       Path p3=Paths.get("c:\\foo\\x\\pp3.txt");
       Files.copy(p1, p2);// 复制pp.txt文件到x目录下并且重新命名为pp2.txt
       Files.move(p1, p3);// 将pp.txt文件移动到x目录下并且重新命名为pp3.txt

3、文件属性的处理

try {
			Path zip = Paths.get("C:\\foo");
			System.out.println(zip.toAbsolutePath().toString());// C:\foo
			System.out.println(Files.getLastModifiedTime(zip));// 2014-01-26T09:46:07.696941Z
			System.out.println(Files.size(zip));// 4096
			System.out.println(Files.isSymbolicLink(zip));// false
			System.out.println(Files.isDirectory(zip));// true
			// 执行批量读取
			System.out.println(Files.readAttributes(zip, "*"));
} catch (IOException ex) {
			System.out.println("Exception" + ex.getMessage());
}
最后一行运行结果如下:

{lastModifiedTime=2014-01-26T09:46:07.696941Z, fileKey=null, isDirectory=true, lastAccessTime=2014-01-26T09:46:07.696941Z, isOther=false, isSymbolicLink=false, isRegularFile=false, creationTime=2013-12-02T11:54:55.062597Z, size=4096}

4、读取、写入文件

在新的NIO中,可以对文件进行行读取,也可以按照行进行写入,使用到的主要方法就是Files类中的newBufferedReader()和newBufferedWriter(),源代码如下:

public static BufferedReader newBufferedReader(Path path, Charset cs) throws IOException {
        CharsetDecoder decoder = cs.newDecoder();
        Reader reader = new InputStreamReader(newInputStream(path), decoder);
        return new BufferedReader(reader);
}
public static BufferedWriter newBufferedWriter(Path path, Charset cs,OpenOption... options)throws IOException {
        CharsetEncoder encoder = cs.newEncoder();
        Writer writer = new OutputStreamWriter(newOutputStream(path, options), encoder);
        return new BufferedWriter(writer);
}
可以看到,其实是通过装饰器模式对流进行一步步的封装,最后得到了缓冲流。

编写的测试程序如下:

Path logFile=Paths.get("C:\\foo\\mm.txt");
         try(BufferedReader reader=Files.newBufferedReader(logFile, StandardCharsets.UTF_8)){
        	 String line;
        	 while((line=reader.readLine())!=null){
        		 System.out.println(line);
        	 }
         }
注意,虽然StandardCharsets.UTF-8指定了编码,但是如果有汉字,仍然会报出java.nio.charset.MalformedInputException异常。

        Path logFile=Paths.get("C:\\foo\\mm.txt");
        try(BufferedWriter writer=Files.newBufferedWriter(logFile, StandardCharsets.UTF_8,StandardOpenOption.WRITE)){
        	writer.write("1234");
        	writer.write("lkajd");
        	
        }

如上演示了行的读取和写入,其实在Files类中还提供了两个辅助方法readAllLines()和readAllBytes()方法,用于读取全部行和全部字节。源代码如下:

 public static byte[] readAllBytes(Path path) throws IOException {
        long size = size(path);
        if (size > (long)Integer.MAX_VALUE)
            throw new OutOfMemoryError("Required array size too large");

        try (InputStream in = newInputStream(path)) {
             return read(in, (int)size);
        }
    }

    
    public static List<String> readAllLines(Path path, Charset cs)  throws IOException  {
        try (BufferedReader reader = newBufferedReader(path, cs)) {
            List<String> result = new ArrayList<>();
            for (;;) {
                String line = reader.readLine();
                if (line == null)
                    break;
                result.add(line);
            }
            return result;
        }
    }
可以看到readAllLines()方法其实就是按行读取内容,然后存储到List中并返回。而readAllBytes()读取的文件字节数不能大于Integer.MAX_VALUE,否则会抛出异常。调用read()方法,源代码如下:

private static byte[] read(InputStream source, int initialSize)   throws IOException{
        int capacity = initialSize;
        byte[] buf = new byte[capacity];
        int nread = 0;
        int rem = buf.length;
        int n;
        // read to EOF which may read more or less than initialSize (eg: file
        // is truncated while we are reading)
        while ((n = source.read(buf, nread, rem)) > 0) {
            nread += n;
            rem -= n;
            assert rem >= 0;
            if (rem == 0) {
                // need larger buffer
                int newCapacity = capacity << 1;
                if (newCapacity < 0) {
                    if (capacity == Integer.MAX_VALUE)
                        throw new OutOfMemoryError("Required array size too large");
                    newCapacity = Integer.MAX_VALUE;
                }
                rem = newCapacity - capacity;
                buf = Arrays.copyOf(buf, newCapacity);
                capacity = newCapacity;
            }
        }
        return (capacity == nread) ? buf : Arrays.copyOf(buf, nread);
    }
其实最终还是调用了字节流的read()方法进行读取,然后将读取到的结果拷贝到一个更大的缓存数组中。

5、灵活读取、写入文件

在NIO中,可以使用SeekableByteChannel来灵活的处理文件,类似于RandomAccessFile类提供的一些方法,但是这里提出了一种新的I/O抽象-通道。SeekableByteChannel接口的定义如下:

public interface SeekableByteChannel   extends ByteChannel{
    int read(ByteBuffer dst) throws IOException;
    int write(ByteBuffer src) throws IOException;
    /**
     * Returns this channel‘s position.
     */
    long position() throws IOException;

    /**
     * Sets this channel‘s position.
     */
    SeekableByteChannel position(long newPosition) throws IOException;

    /**
     * Returns the current size of entity to which this channel is connected.
     */
    long size() throws IOException;

    /**
     * Truncates the entity, to which this channel is connected, to the given
     * size.
     */
    SeekableByteChannel truncate(long size) throws IOException;
}
可以改变通道的位置和大小,所以处理文件内容更加灵活。这个接口的一个抽象实现类为java.nio.channels.FileChannel。通过调用open()方法来获取FileChannel的具体实例。编写一个测试实例,如下:
Path logFile=Paths.get("C:\\temp.txt");
         ByteBuffer buffer=ByteBuffer.allocate(1024);
         FileChannel channel=FileChannel.open(logFile, StandardOpenOption.READ);
         channel.read(buffer,channel.size()-100);// 读取文件最后的100个字符
         buffer.flip();//此行语句一定要有  
         while (buffer.hasRemaining()) {  
             System.out.print((char)buffer.get());  
         }  
输出文件最后100个字符。


































Java 7源码分析第18篇 - I/O操作之NIO

上一篇:查看sqlserver被锁的表以及如何解锁


下一篇:如何通过outline为SQL语句指定执行计划