<------------------------------廖雪峰学Java---------------------------------->
- 1. File对象
- 2. InputStream
- 3. OutputStream
- 4. Filter模式(装饰者模式)
- 5. 直接读取Zip压缩文件的内容
- 6. 读取classpath资源
- 7. 序列化
- 8. Reader
- 9. Writer
- 10. PrintStream以及PrintWriter
- 11. 使用Files工具类
- Java的标准库java.io提供了File对象来 *** 作文件和目录
- File对象有3种形式表示的路径,一种是getPath(),返回构造方法传入的路径,一种是getAbsolutePath(),返回绝对路径,一种是getCanonicalPath,它和绝对路径类似,但是返回的是规范路径
- File对象有一个静态变量用于表示当前平台的系统分隔符:
System.out.println(File.separator); // 根据当前平台打印""或"/"
- 构造一个File对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File对象,并不会导致任何磁盘 *** 作。只有当我们调用File对象的某些方法的时候,才真正进行磁盘 *** 作
- 调用isFile(),判断该File对象是否是一个已存在的文件,调用isDirectory(),判断该File对象是否是一个已存在的目录
- 判断文件的权限和大小:
boolean canRead():是否可读; boolean canWrite():是否可写; boolean canExecute():是否可执行; long length():文件字节大小。
对目录而言,是否可执行表示是否能够列出其子文件夹及子文件
- 当File对象表示一个文件时,可以通过createNewFile()创建一个新文件,用delete()删除该文件:
File file = new File("/path/to/file"); if (file.createNewFile()) { // 文件创建成功: // TODO: if (file.delete()) { // 删除文件成功: } }
- File对象提供了createTempFile()来创建一个临时文件,以及deleteonExit() 在JVM退出时自动删除该文件
- 当File对象表示一个目录时,可以使用list() 和listFiles() 列出目录下的文件和子目录名。listFiles()提供了一系列重载方法,可以过滤不想要的文件和目录:
public class Main { public static void main(String[] args) throws IOException { File f = new File("C:\Windows"); File[] fs1 = f.listFiles(); // 列出所有文件和子目录 printFiles(fs1); File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件 public boolean accept(File dir, String name) { return name.endsWith(".exe"); // 返回true表示接受该文件 } }); printFiles(fs2); } static void printFiles(File[] files) { System.out.println("=========="); if (files != null) { for (File f : files) { System.out.println(f); } } System.out.println("=========="); } }
- File对象如果表示一个目录,可以通过以下方法创建和删除目录:
boolean mkdir():创建当前File对象表示的目录; boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来; boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功。
- Java标准库还提供了一个Path对象,它位于java.nio.file包。Path对象和File对象类似,但 *** 作更加简单:
public class Main { public static void main(String[] args) throws IOException { Path p1 = Paths.get(".", "project", "study"); // 构造一个Path对象 System.out.println(p1); Path p2 = p1.toAbsolutePath(); // 转换为绝对路径 System.out.println(p2); Path p3 = p2.normalize(); // 转换为规范路径 System.out.println(p3); File f = p3.toFile(); // 转换为File对象 System.out.println(f); for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍历Path System.out.println(" " + p); } } }2. InputStream
- InputStream是Java标准库提供的最基本的输入流。它位于java.io这个包里。java.io包提供了所有同步IO的功能
- InputStream是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read(),签名如下:
public abstract int read() throws IOException; // 这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了
- FileInputStream是InputStream的一个子类。顾名思义,FileInputStream就是从文件流中读取数据
- InputStream和OutputStream都是通过close()方法来关闭流。关闭流就会释放对应的底层资源
- 在读取或写入IO流的过程中,可能会发生错误,例如,文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成IOException异常并抛出。因此,所有与IO *** 作相关的代码都必须正确处理IOException
- 利用Java 7引入的新的try(resource) 的语法,只需要编写try语句,让编译器自动为我们关闭资源:
public void readFile() throws IOException { try (InputStream input = new FileInputStream("src/readme.txt")) { int n; while ((n = input.read()) != -1) { System.out.println(n); } } // 编译器在此自动为我们写入finally并调用close() }
编译器只看try(resource = …)中的对象是否实现了java.lang.AutoCloseable接口,如果实现了,就自动加上finally语句并调用close()方法。InputStream和OutputStream都实现了这个接口,因此,都可以用在try(resource)中
- 很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节:
int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数 int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数 //利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区, //read()方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。 //read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。 //如果返回-1,表示没有更多的数据 public void readFile() throws IOException { try (InputStream input = new FileInputStream("src/readme.txt")) { // 定义1000个字节大小的缓冲区: byte[] buffer = new byte[1000]; int n; while ((n = input.read(buffer)) != -1) { // 读取到缓冲区 System.out.println("read " + n + " bytes."); } } }
- 在调用InputStream的read()方法读取数据时,read()方法是阻塞(Blocking)的,即必须等待read()方法返回才能执行下一行代码
- ByteArrayInputStream可以在内存中模拟一个InputStream:
public class Main { public static void main(String[] args) throws IOException { byte[] data = { 72, 101, 108, 108, 111, 33 }; try (InputStream input = new ByteArrayInputStream(data)) { int n; while ((n = input.read()) != -1) { System.out.println((char)n); } } } }
- ByteArrayInputStream实际上是把一个byte[]数组在内存中变成一个InputStream,虽然实际应用不多,但测试的时候,可以用它来构造一个InputStream:
public class Main { public static void main(String[] args) throws IOException { byte[] data = { 72, 101, 108, 108, 111, 33 }; try (InputStream input = new ByteArrayInputStream(data)) { // 不需要真的提供一个FileInputStream,用ByteArrayInputStream来模拟即可 String s = readAsString(input); System.out.println(s); } } public static String readAsString(InputStream input) throws IOException { int n; StringBuilder sb = new StringBuilder(); while ((n = input.read()) != -1) { sb.append((char) n); } return sb.toString(); } }3. OutputStream
- OutputStream是Java标准库提供的最基本的输出流,是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b),这个方法会写入一个字节到输出流。要注意的是,虽然传入的是int参数,但只会写入一个字节,即只写入int最低8位表示字节的部分
- OutputStream也提供了close()方法关闭输出流,以便释放系统资源。要特别注意:OutputStream还提供了一个flush()方法,它的目的是将缓冲区的内容真正输出到目的地。
一般来说,系统会等到缓冲区写满时再一次 性输出,flush()方法的作用是强制输出缓冲区的内容
- 通常情况下,我们不需要调用这个flush()方法,因为缓冲区写满了OutputStream会自动调用它,并且,在调用close()方法关闭OutputStream之前,也会自动调用flush()方法,但是在要求快速响应的场景(如聊天服务)下就需要手动调用flush方法
- FileOutputStream为例,演示如何将若干个字节写入文件流:
public void writeFile() throws IOException { OutputStream output = new FileOutputStream("out/readme.txt"); output.write(72); // H output.write(101); // e output.write(108); // l output.write(108); // l output.write(111); // o output.close(); } public void writeFile() throws IOException { try (OutputStream output = new FileOutputStream("out/readme.txt")) { output.write("Hello".getBytes("UTF-8")); // Hello } // 编译器在此自动为我们写入finally并调用close() }
- OutputStream的write()方法是阻塞的
- FileOutputStream可以从文件获取输出流,这是OutputStream常用的一个实现类。此外,ByteArrayOutputStream可以在内存中模拟一个OutputStream
- 同时 *** 作多个AutoCloseable资源时,在try(resource) { … }语句中可以同时写出多个资源,用;隔开。例如,同时读写两个文件:
// 读取input.txt,写入output.txt: try (InputStream input = new FileInputStream("input.txt"); OutputStream output = new FileOutputStream("output.txt")) { input.transferTo(output); // transferTo的作用是? }4. Filter模式(装饰者模式)
InputStream/OutputStream的功能并不是一成不变的,例如我们想要给流添加上“缓存”,“签名”,“加密”等功能时,如果通过直接创建各类型子类的话会出现“子类爆炸”的问题(即:子类数目过多,可想而知,各功能的Stream成一个子类,不同功能之间的组合同样也是子类,而这样的组合数量过多)。因此,Java主张使用Filter模式(“装饰者模式”,decorator)来对Stream对象进行包装。
- 为了解决依赖继承会导致子类数量失控的问题,JDK首先将InputStream分为两大类:
一类是直接提供数据的基础InputStream,例如: FileInputStream ByteArrayInputStream ServletInputStream 一类是提供额外附加功能的InputStream,例如: BufferedInputStream DigestInputStream CipherInputStream
- 当我们需要给一个“基础”InputStream附加各种功能时,我们先确定这个能提供数据源的InputStream,如FileInputStream等:
InputStream file = new FileInputStream("test.gz");
- 紧接着,我们希望FileInputStream能提供缓冲的功能来提高读取的效率,因此我们用BufferedInputStream包装这个InputStream,得到的包装类型是BufferedInputStream,但它仍然被视为一个InputStream:
InputStream buffered = new BufferedInputStream(file);
- 最后,假设该文件已经用gzip压缩了,我们希望直接读取解压缩的内容,就可以再包装一个GZIPInputStream:
InputStream gzip = new GZIPInputStream(buffered);
- 无论包装多少次,得到的对象始终是InputStream,我们直接用InputStream来引用它:
- 编写FilterInputStream:
public class Main { public static void main(String[] args) throws IOException { byte[] data = "hello, world!".getBytes("UTF-8"); try (CountInputStream input = new CountInputStream(new ByteArrayInputStream(data))) { int n; while ((n = input.read()) != -1) { System.out.println((char)n); } System.out.println("Total read " + input.getBytesRead() + " bytes"); } } } class CountInputStream extends FilterInputStream { private int count = 0; CountInputStream(InputStream in) { super(in); } public int getBytesRead() { return this.count; } public int read() throws IOException { int n = in.read(); if (n != -1) { this.count ++; } return n; } public int read(byte[] b, int off, int len) throws IOException { int n = in.read(b, off, len); if (n != -1) { this.count += n; } return n; } }
注意到在叠加多个FilterInputStream,我们只需要持有最外层的InputStream,并且,当最外层的InputStream关闭时(在try(resource)块的结束处自动关闭),内层的InputStream的close()方法也会被自动调用,并最终调用到最核心的“基础”InputStream,因此不存在资源泄露
5. 直接读取Zip压缩文件的内容-
ZipInputStream是一种FilterInputStream,它可以直接读取zip包的内容:
另一个JarInputStream是从ZipInputStream派生,它增加的主要功能是直接读取jar文件里面的MANIFEST.MF文件。因为本质上jar包就是zip包,只是额外附加了一些固定的描述文件。 -
读取Zip文件:创建一个ZipInputStream,通常是传入一个FileInputStream作为数据源,然后,循环调用getNextEntry(),直到返回null,表示zip流结束;一个ZipEntry表示一个压缩文件或目录,如果是压缩文件,我们就用read()方法不断读取,直到返回-1:
try (ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) { ZipEntry entry = null; while ((entry = zip.getNextEntry()) != null) { String name = entry.getName(); if (!entry.isDirectory()) { int n; while ((n = zip.read()) != -1) { ... } } } }
- 创建Zip文件:ZipOutputStream是一种FilterOutputStream,它可以直接写入内容到zip包。我们要先创建一个ZipOutputStream,通常是包装一个FileOutputStream,然后,每写入一个文件前,先调用putNextEntry(),然后用write()写入byte[]数据,写入完毕后调用closeEntry()结束这个文件的打包:
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(...))) { File[] files = ... for (File file : files) { zip.putNextEntry(new ZipEntry(file.getName())); zip.write(getFileDataAsBytes(file)); zip.closeEntry(); } }
上面的代码没有考虑文件的目录结构。如果要实现目录层次结构,new ZipEntry(name)传入的name要用相对路径
6. 读取classpath资源- 从classpath读取文件可以避免不同环境下文件路径不一致的问题:如果我们把properties文件放到classpath中,就不用关心它的实际存放路径
- 在classpath中的资源文件,路径总是以/开头,我们**先获取当前的Class对象,然后调用getResourceAsStream()**就可以直接从classpath读取任意的资源文件:
try (InputStream input = getClass().getResourceAsStream("/default.properties")) { // TODO: }
- 调用getResourceAsStream()需要特别注意的一点是,如果资源文件不存在,它将返回null
- 把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置:
Properties props = new Properties(); props.load(inputStreamFromClassPath("/default.properties")); props.load(inputStreamFromFile("./conf.properties"));7. 序列化
- 序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组
- 一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为 “标记接口”(Marker Interface) ,实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法
- 把一个Java对象变为byte[]数组,需要使用ObjectOutputStream。它负责把一个Java对象写入一个字节流:
ublic class Main { public static void main(String[] args) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); try (ObjectOutputStream output = new ObjectOutputStream(buffer)) { // 写入int: output.writeInt(12345); // 写入String: output.writeUTF("Hello"); // 写入Object: output.writeObject(Double.valueOf(123.456)); } System.out.println(Arrays.toString(buffer.toByteArray())); } }
- 和ObjectOutputStream相反,ObjectInputStream负责从一个字节流读取Java对象:
try (ObjectInputStream input = new ObjectInputStream(...)) { int n = input.readInt(); String s = input.readUTF(); Double d = (Double) input.readObject(); }
- readObject()可能抛出的异常有:
ClassNotFoundException:没有找到对应的Class; InvalidClassException:Class不匹配。 对于ClassNotFoundException,这种情况常见于一台电脑上的Java程序把一个Java对象, 例如,Person对象序列化以后,通过网络传给另一台电脑上的另一个Java程序, 但是这台电脑的Java程序并没有定义Person类,所以无法反序列化。 对于InvalidClassException,这种情况常见于序列化的Person对象定义了一个int类型的age字段, 但是反序列化时,Person类定义的age字段被改成了long类型,所以导致class不兼容。
- 反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行
- Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息
- Reader是Java的IO库提供的另一个输入流接口。和InputStream的区别是,InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取
- java.io.Reader是所有字符输入流的超类,read()方法读取字符流的下一个字符,并返回字符表示的int,范围是0~65535。如果已读到末尾,返回-1
- FileReader是Reader的一个子类,它可以打开文件并获取Reader,FileReader默认的编码与系统相关(例如如果Windows的默认编码时GBK,那么读取UTF-8编码的文件就会出现乱码),要避免乱码问题,我们需要在创建FileReader时指定编码:
Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);
- 用**try (resource)**来保证Reader在无论有没有IO错误的时候都能够正确地关闭
- CharArrayReader可以在内存中模拟一个Reader,它的作用实际上是把一个char[]数组变成一个Reader:
try (Reader reader = new CharArrayReader("Hello".toCharArray())) { }
- StringReader可以直接把String作为数据源,它和CharArrayReader几乎一样:
try (Reader reader = new StringReader("Hello")) { }
- Reader和InputStream有什么关系?
除了特殊的CharArrayReader和StringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。如果我们查看FileReader的源码,它在内部实际上持有一个FileInputStream
- InputStreamReader可以把任何InputStream转换为Reader(转换器)。示例代码如下:
// 持有InputStream: InputStream input = new FileInputStream("src/readme.txt"); // 变换为Reader: Reader reader = new InputStreamReader(input, "UTF-8");9. Writer
- Writer就是带编码转换器的OutputStream,它把char转换为byte并输出
- Writer是所有字符输出流的超类,它提供的方法主要有:
写入一个字符(0~65535):void write(int c); 写入字符数组的所有字符:void write(char[] c); 写入String表示的所有字符:void write(String s)。
- FileWriter就是向文件中写入字符流的Writer。它的使用方法和FileReader类似:
try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) { writer.write('H'); // 写入单个字符 writer.write("Hello".toCharArray()); // 写入char[] writer.write("Hello"); // 写入String }
- CharArrayWriter可以在内存中创建一个Writer,它的作用实际上是构造一个缓冲区,可以写入char,最后得到写入的char[]数组,这和ByteArrayOutputStream非常类似:
try (CharArrayWriter writer = new CharArrayWriter()) { writer.write(65); writer.write(66); writer.write(67); char[] data = writer.toCharArray(); // { 'A', 'B', 'C' } }
- StringWriter也是一个基于内存的Writer,它和CharArrayWriter类似。实际上,StringWriter在内部维护了一个StringBuffer,并对外提供了Writer接口
- 除了CharArrayWriter和StringWriter外,普通的Writer实际上是基于OutputStream构造的,它接收char,然后在内部自动转换成一个或多个byte,并写入OutputStream。因此,OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器:
try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) { // TODO: }10. PrintStream以及PrintWriter
- PrintStream是一种FilterOutputStream,它在OutputStream的接口上,额外提供了一些写入各种数据类型的方法:
写入int:print(int) 写入boolean:print(boolean) 写入String:print(String) 写入Object:print(Object),实际上相当于print(object.toString()) ...
- 我们经常使用的System.out.println()实际上就是使用PrintStream打印各种数据。其中,System.out是系统默认提供的PrintStream,表示标准输出
- System.err是系统默认提供的标准错误输出
- PrintStream和OutputStream相比,除了添加了一组print()/println()方法,可以打印各种数据类型,比较方便外,它还有一个额外的优点,就是不会抛出IOException,这样我们在编写代码的时候,就不必捕获IOException
- PrintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,它的print()/println()方法最终输出的是char数据。两者的使用方法几乎是一模一样的:
public class Main { public static void main(String[] args) { StringWriter buffer = new StringWriter(); try (PrintWriter pw = new PrintWriter(buffer)) { pw.println("Hello"); pw.println(12345); pw.println(true); } System.out.println(buffer.toString()); } }11. 使用Files工具类
- 从Java 7开始,提供了Files和Paths这两个工具类,能极大地方便我们读写文件,Files和Paths是java.nio包里面的类,他俩封装了很多读写文件的简单方法
- 要把一个文件的全部内容读取为一个byte[],可以这么写:
byte[] data = Files.readAllBytes(Paths.get("/path/to/file.txt"));
- 如果是文本文件,可以把一个文件的全部内容读取为String:
// 默认使用UTF-8编码读取: String content1 = Files.readString(Paths.get("/path/to/file.txt")); // 可指定编码: String content2 = Files.readString(Paths.get("/path/to/file.txt"), StandardCharsets.ISO_8859_1); // 按行读取并返回每行内容: Listlines = Files.readAllLines(Paths.get("/path/to/file.txt"));
- 写入文件:
// 写入二进制文件: byte[] data = ... Files.write(Paths.get("/path/to/file.txt"), data); // 写入文本并指定编码: Files.writeString(Paths.get("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1); // 按行写入文本: Listlines = ... Files.write(Paths.get("/path/to/file.txt"), lines);
- Files工具类还有copy()、delete()、exists()、move()等快捷方法 *** 作文件和目录
- Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。读写大型文件仍然要使用文件流,每次只读写一部分文件内容
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)