Java核心教程6: 文件读写

在丑陋的 Java I/O 编程方式诞生多年以后,Java终于简化了文件读写的基本操作。

很多学得比较快的同学可能学习过Java的文件读写,就是诸如 InputStream, OutputStream 一样的东西,如上面所说的一样,他们既丑陋又难用,学习路线非常曲折。不过好在 Java 设计者终于意识到了 Java 使用者多年来的痛苦,在 Java7 中对此引入了巨大的改进,这些新特性被放在 java.nio.file 包下面。除此之外,Java8 新增的 streams 与文件结合使得文件操作变得更加优雅易用,可以说从此以后我们几乎再也不需要见到 InputStream 这种难用的东西了。

首先让我们将看一下文件操作的两个基本概念:

  1. 文件的路径
  2. 文件本身

可能有同学要问我这里为什么只提到了文件而没有提到目录,我这里说的文件是指广义的文件(File),包括我们认为的文件(RegularFile)和目录(Directory),为了避免歧义,之后尽量用英文说明狭义的文件(RegularFile)。



一、文件的路径

路径,也就是 Path ,例如"C://Windows/Fonts"、"/Users/tommy0607/Downloads/InputFix.jar"等等都是路径。

java.nio.file.Paths 类包含一个重载方法 static get(String first, String... more),该方法通过输入一系列路径片段可以返回一个路径。

例如想要获得 /Users/tommy0607/Downloads 的路径,可以用下面三种方式:

  • 直接用全路径:Paths.get("/Users/tommy0607/Downloads")
  • 用父目录的路径名和文件名字:Paths.get("/Users/tommy0607","Downloads")
  • 将路径进行任意分段:Paths.get("/Users","tommy0607","Downloads")

当然除了绝对路径之外,相对路径也是可以的,例如Path.get("test/abc.png")就是一个相对路径。

它还有一个重载方法,接受一个统一资源定位符URI为参数,在本节课中用不到就直接略过。

Path 对象的主要方法如下:

  • getParent() 获取该路径的父路径
  • isAbsolute() 返回该路径是否是绝对路径
  • toAbsolutePath() 获取该路径的绝对路径
  • normalize() 将绝对路径正常化,如果当前路径是相对路径则会报错
  • toUri() 将其转换成URI
  • resolve(String/Path other) 将当前路径与参数中的路径进行拼接,一般用于获取子目录的路径
  • getFileName() 获取路径的文件名,分段路径的最右边那部分就是文件名

关于normalize()方法的正常化我要特别讲解一下:
假设当前目录是 /Users/tommy0607/Downloads,现有这样的对象Path path = Paths.get("../../test.jar")
那么path.toAbsolutePath()返回的路径是 /Users/tommy0607/Downloads/../../test.jar,而path.toAbsolutePath().normoalize()返回的路径则是 /User/test.jar,也就是它会把绝对路径里所有的"../"都去掉,这就是正常化

还有要特别注意的就是 Path 类进行的所有操作都只是基于字符串处理,也就是说如果你输入一个不存在的路径也不会有任何的报错,这点要多加注意!



二、文件本身

虽然 Java 中也有用来代表一个文件(包括RegularFile和Directory)的类,叫做 File,但这个类的设计有些问题,它与其说是文件还不如说是路径,更何况在 Java7 之后就基本用不到这个类了,现在用得比较多的是 nio 中的 Files 类。

Files 工具类包含一系列完整的方法用于获得路径对应的文件的相关信息,下面用一个简单的示例来展示:

import java.nio.file.*;
import java.io.IOException;

public class PathAnalysis {
    public static void main(String[] args) throws IOException {
        Path p = Paths.get("PathAnalysis.java").toAbsolutePath();
        say("该文件存在", Files.exists(p));
        say("该文件是目录", Files.isDirectory(p));
        say("该文件可执行", Files.isExecutable(p));
        say("该文件可读", Files.isReadable(p));
        say("该文件是RegularFile", Files.isRegularFile(p));
        say("该文件可写", Files.isWritable(p));
        say("该文件不存在", Files.notExists(p));
        say("该文件是隐藏的", Files.isHidden(p));
        say("文件尺寸", Files.size(p));
        say("文件最后修改日期: ", Files.getLastModifiedTime(p));
        say("文件拥有者", Files.getOwner(p));
    }
    
    static void say(String id, Object result) {
        System.out.print(id + ": ");
        System.out.println(result);
    }
}



三、文件查找

在查找文件方面,Java 也提供了非常方便实用的API。但在介绍文件查找之前不得不先介绍一下Files.walk()方法:它可以返回某个路径下所有子目录和文件的路径的流Stream<Path>。举个例子:

Files.walk(Paths.get("/Users/tommy0607/Downloads"))
        .forEach(System.out::println);

它输出了/Users/tommy0607/Downloads 下面所有子目录和文件的路径(包括当前目录本身)


文件查找有两种模式,globregex,这里只讲解前一种模式,后一种是使用正则表达式,感兴趣的同学可以自学。

在这里,我们使用 glob 查找以 .tmp.txt 结尾的所有路径:

import java.nio.file.*;

public class Find {
    public static void main(String[] args) throws Exception {
        Path test = Paths.get("test");
        // 创建一个叫dir.tmp的文件夹
        Files.createDirectory(test.resolve("dir.tmp"));

        
        PathMatcher matcher = FileSystems.getDefault()
          .getPathMatcher("glob:**/*.{tmp,txt}");
        Files.walk(test)
          .filter(matcher::matches)
          .forEach(System.out::println);
        System.out.println("***************");

        
        
        PathMatcher matcher2 = FileSystems.getDefault()
          .getPathMatcher("glob:*.tmp");
        Files.walk(test)
          .map(Path::getFileName)
          .filter(matcher2::matches)
          .forEach(System.out::println);
        System.out.println("***************");

        
        
        Files.walk(test) 
          .filter(Files::isRegularFile) //只查找RegularFile
          .map(Path::getFileName)
          .filter(matcher2::matches)
          .forEach(System.out::println);
    }
}
/* 输出:
test\bag\foo\bar\baz\5208762845883213974.tmp
test\bag\foo\bar\baz\File.txt
test\bar\baz\bag\foo\7918367201207778677.tmp
test\bar\baz\bag\foo\File.txt
test\baz\bag\foo\bar\8016595521026696632.tmp
test\baz\bag\foo\bar\File.txt
test\dir.tmp
test\foo\bar\baz\bag\5832319279813617280.tmp
test\foo\bar\baz\bag\File.txt
***************
5208762845883213974.tmp
7918367201207778677.tmp
8016595521026696632.tmp
dir.tmp
5832319279813617280.tmp
***************
5208762845883213974.tmp
7918367201207778677.tmp
8016595521026696632.tmp
5832319279813617280.tmp
*/

matcher 中,glob 表达式开头的 **/ 表示“当前目录及所有子目录”,这在不仅仅要匹配当前目录下的路径时非常有用。一个 * 表示“任何字符串”,然后是一个点,接着是大括号,表示一系列的可能性——我们正在寻找以 .tmp.txt 结尾的路径。

matcher2 只使用 *.tmp,按理来说应该无法匹配任何路径的,但注意看map()方法将全路径转换成了文件名

注意,在前两种情况下,输出中都会出现 dir.tmp,即使它是一个目录而不是一个文件(RegularFile)。如果只查找文件,必须像在最后的 files.walk() 中那样对其进行筛选。



四、文件读写

到了最后一部分才讲到本课程的核心主题——文件读写,不过用起来其实非常简单方便。

读文件

如果一个文件很小,也就是说就算把整个文件一次性全部读到内存中也没问题,那么可以用Files.readAllLines()Files.readAllBytes(),前者是返回所有行的字符串,后者则是获取返回整个文件的字节数组。

但如果文件很大,一次性读取到内存中不太可能,或者说你并不需要读取整个文本,Files.lines() 方便地将文件转换为行的 Stream(也就是说流中的元素是文件的每一行文本):

import java.nio.file.*;

public class ReadLineStream {
    public static void main(String[] args) throws Exception {
        Files.lines(Paths.get("PathInfo.java"))
          .skip(13)
          .findFirst()
          .ifPresent(System.out::println);
    }
}

代码很容易理解,先跳过 PathInfo.java 的前13行,然后读取接下来的一行并输出。

写文件

写文件就更简单了,直接调用Files.write()即可,支持写入字节数组和一行一行的字符串;

或者Files.writeString(),只写入一个字符串对象。这两个方法中都有的 Charset 类型的参数是写入的字符串的编码,如果不指定的话默认就是UTF-8编码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343