首先来首歌曲来放松一下吧!
一、IO介绍
IO是指Input/Output,即输入和输出。以内存为中心:
Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。
Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等。
IO流是一种流式的数据输入/输出模型:
二进制数据以byte
为最小单位在InputStream
/OutputStream
中单向流动;
字符数据以char
为最小单位在Reader
/Writer
中单向流动。
Java标准库的java.io
包提供了同步IO功能:
字节流接口:InputStream
/OutputStream
;
字符流接口:Reader
/Writer
。
注意:UTF-8编码下,英文字符占一个字节,中文字符占三个字节!
Java的读写数据在传输时都是byte[]
,String
这两种方式!
1、Reader 和 Writer
如果我们需要读写的是字符,并且字符不全是单字节表示的ASCII字符,那么,按照char
来读写显然更方便,这种流称为字符流 。
Reader
和Writer
本质上是一个能自动编解码的InputStream
和OutputStream
。
使用Reader
,数据源虽然是字节,但我们读入的数据都是char
类型的字符,原因是Reader
内部把读入的byte
做了解码,转换成了char
。使用InputStream
,我们读入的数据和原始二进制数据一模一样,是byte[]
数组,但是我们可以自己把二进制byte[]
数组按照某种编码转换为字符串。究竟使用Reader
还是InputStream
,要取决于具体的使用场景。如果数据源不是文本,就只能使用InputStream
,如果数据源是文本,使用Reader更方便一些。Writer
和OutputStream
是类似的。
2、同步和异步
同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。
而异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。
Java标准库的包java.io
提供了同步IO,而java.nio
则是异步IO。上面我们讨论的InputStream
、OutputStream
、Reader
和Writer
都是同步IO的抽象类,对应的具体实现类,以文件为例,有FileInputStream
、FileOutputStream
、FileReader
和FileWriter
。
二、File对象
1、创建File对象
File file = new File("路径");
1.1 路径的写法
注意Windows平台使用\
作为路径分隔符,在Java字符串中需要用\\
表示一个\
。Linux平台使用/
作为路径分隔符:
用.
表示当前目录,..
表示上级目录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.learn.file;import java.io.File;import java.io.FilenameFilter;import java.io.IOException;public class FileTest { public static void main (String[] args) throws IOException { File file = new File("." ); File file01 = new File(".\\" ); File file02 = new File("..\\" ); File file1 = new File(".." ); File file2 = new File("Settings\\setting.properties" ); File file3 = new File("E:\\MyJavaProgram\\Settings\\setting.properties" ); System.out.println(File.separator); File f = new File("/usr/bin/javac" ); } }
1.2 获取路径
getPath()
:返回构造方法传入的路径,直接输出File对象名效果一样!
getAbsolutePath()
:返回绝对路径,若传入参数有.
,则该方法也会出现.
;
getCanonicalPath()
:返回的是规范路径,若传入参数有.
,则该方法会将点翻译为正确路径;
路径如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.learn.file;import java.io.File;import java.io.FilenameFilter;import java.io.IOException;public class FileTest { public static void main (String[] args) throws IOException { File file = new File("." ); System.out.println(file); System.out.println(file.getPath()); System.out.println(file.getAbsolutePath()); System.out.println(file.getCanonicalPath()); System.out.println("---------------------------------------------" ); File file01 = new File(".\\" ); System.out.println(file01.getCanonicalPath()); System.out.println("---------------------------------------------" ); File file02 = new File("..\\" ); System.out.println(file02.getCanonicalPath()); System.out.println("---------------------------------------------" ); File file1 = new File(".." ); System.out.println(file1.getPath()); System.out.println(file1.getAbsolutePath()); System.out.println(file1.getCanonicalPath()); System.out.println("---------------------------------------------" ); File file2 = new File("Settings\\setting.properties" ); System.out.println(file2.getPath()); System.out.println(file2.getAbsolutePath()); System.out.println(file2.getCanonicalPath()); System.out.println("---------------------------------------------" ); File file3 = new File("E:\\MyJavaProgram\\Settings\\setting.properties" ); System.out.println(file3.getPath()); System.out.println(file3.getAbsolutePath()); System.out.println(file3.getCanonicalPath()); }
2、文件目录判断
File
对象既可以表示文件,也可以表示目录。
特别要注意的是,构造一个File
对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File
对象,并不会导致任何磁盘操作。只有当我们调用File
对象的某些方法的时候,才真正进行磁盘操作。
isFile()
:判断是否是已存在文件
isDirectory()
:判断是否是已存在目录
isAbsolute()
:判断是否是完整路径
isHidden()
:判断是否是隐藏文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 File file1 = new File(".." ); File file2 = new File("Settings\\setting.properties" ); File file3 = new File("E:\\MyJavaProgram\\Settings\\setting.properties" ); System.out.println(file3.isAbsolute()); System.out.println(file3.isDirectory()); System.out.println(file3.isFile()); System.out.println(file3.isHidden()); System.out.println("---------------------------------------------" ); System.out.println(file2.isAbsolute()); System.out.println(file1.isAbsolute()); System.out.println(file1.isFile()); System.out.println(file1.isDirectory());
3、File权限判断
用File
对象获取到一个文件时,还可以进一步判断文件的权限和大小:
boolean canRead()
:是否可读;
boolean canWrite()
:是否可写;
boolean canExecute()
:是否可执行;
long length()
:文件字节大小。
对目录而言,是否可执行表示能否列出它包含的文件和子目录。
1 2 3 4 5 6 System.out.println("---------------------------------------------" ); File fileExe = new File("E:\\MyJavaProgram\\Settings\\test.exe" ); System.out.println(fileExe.canRead()); System.out.println(fileExe.canWrite()); System.out.println(fileExe.canExecute());
4、创建和删除文件
先将需要创建的文件test.txt
传入File对象,再调用creatNewFile()方法来创建!
可通过createTempFile()
创建临时文件,以及deleteOnExit()
在JVM退出时自动删除该文件。
createNewFile()
:返回boolean值,若文件已存在返回false!
delete()
:返回boolean值,若文件已不存在返回false!
createTempFile()
:创建临时文件,创建位置为C盘某个位置,需要传入两个参数,一个前缀,一个后缀!
deleteOnExit()
:JVM退出时自动删除,删除后仍然可获得File对象路径!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 File file4 = new File("Settings\\test.txt" );if (file4.createNewFile()){ System.out.println("test.txt创建成功!" ); }if (file4.delete()){ System.out.println("test.txt删除成功!" ); } File file5 = File.createTempFile("temp" ,".cpp" ); System.out.println(file5.getCanonicalPath()); file5.deleteOnExit(); System.out.println(file5.getCanonicalPath());
5、创建和删除目录
boolean mkdir()
:创建当前File对象表示的目录;
boolean mkdirs()
:创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;
boolean delete()
:删除当前File对象表示的目录,当前目录必须为空才能删除成功,是能删除最内部的目录,并且只有目录为空才可删除!
创建目录使用mkdirs()
即可,mkdir()
可以做到的,mkdirs()
都可以做到!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 File file7 = new File(".\\test" );if (file7.mkdir()){ System.out.println("test目录创建成功!" ); }if (file7.delete()){ System.out.println("test目录删除成功!" ); } File file8 = new File(".\\test\\test02\\test03" );if (file8.mkdirs()){ System.out.println("嵌套目录创建成功!" ); if (file8.delete()){ System.out.println("test03目录删除成功!" ); } }
6、遍历文件和目录
使用listFiles()
获取当前目录层次关系,用File对象数组接收,可以传入FilenameFilter()
方法来过滤不需要的文件或目录!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.learn.file;import java.io.File;import java.io.FilenameFilter;import java.io.IOException;public class FileTest { public static void main (String[] args) throws IOException { System.out.println("-----------------------------------------------" ); File file6 = new File("." ); File[] files = file6.listFiles(); printFiles(files); File[] files1 = file6.listFiles(new FilenameFilter() { @Override public boolean accept (File dir, String name) { return name.endsWith(".idea" ); } }); printFiles(files1); } static void printFiles (File[] files) { System.out.println("-----------------------------------------------" ); if (files != null ){ for (var file : files){ System.out.println(file); } } } }
7、Path对象
Java标准库还提供了一个Path
对象,它位于java.nio.file
包。Path
对象和File
对象类似,但操作更加简单:
Paths.get()
:参数可以有多个,表示路径,后面的一定得是前面路径的子路径!
toAbsolutePath()
:转化为带.
的绝对路径
normalize()
:将构造路径的点去掉
toAbsolutePath().normalize()
:将带点的绝对路径去掉点,转化为完整的路径!
toFile()
:转化为File对象!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.learn.file;import java.io.File;import java.io.IOException;import java.nio.file.Path;import java.nio.file.Paths;public class PathTest { public static void main (String[] args) throws IOException { Path path = Paths.get(".\\Path" ,"PathTest\\test01" , "test02" ); System.out.println(path); System.out.println(path.toAbsolutePath()); System.out.println(path.normalize()); System.out.println(path.toAbsolutePath().normalize()); File file = path.toFile(); System.out.println(file); for (var p : path){ System.out.println(p); } System.out.println(); for (var p : path.toAbsolutePath()){ System.out.println(p); } } }
8、遍历目录例子
为了有层次关系,可进行空格的控制输出,使用getName()
方法获取目录或文件名!
使用递归去进行层次遍历!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.learn.file;import java.io.File;import java.io.IOException;public class FileWork { public static void main (String[] args) throws IOException { File file = new File("." ); ListDir(file, 0 ); } static void ListDir (File file, int level) { File[] files = file.listFiles(); if (files != null ) { for (var ls : files) { for (int i = 0 ; i < level; i++){ System.out.print(" " ); } if (ls.isFile()) { System.out.println(ls.getName()); } else { System.out.println(ls.getName() + "\\" ); } ListDir(ls, level + 1 ); } } } }
InputStream
就是Java标准库提供的最基本的输入流。它位于java.io
这个包里。java.io
包提供了所有同步IO的功能。
要特别注意的一点是,InputStream
并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read()
。
这个方法会读取输入流的下一个字节,并返回字节表示的int
值(0~255)。如果已读到末尾,返回-1
表示不能继续读取了。
FileInputStream
是InputStream
的一个子类
在计算机中,类似文件、网络端口这些资源,都是由操作系统统一管理的。应用程序在运行的过程中,如果打开了一个文件进行读写,完成后要及时地关闭,以便让操作系统把资源释放掉,否则,应用程序占用的资源会越来越多,不但白白占用内存,还会影响其他应用程序的运行。
InputStream
和OutputStream
都是通过close()
方法来关闭流。关闭流就会释放对应的底层资源。
我们还要注意到在读取或写入IO流的过程中,可能会发生错误,例如,文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成IOException
异常并抛出。因此,所有与IO操作相关的代码都必须正确处理IOException
。
1.1 手动关闭 + 未处理异常
如果读取过程中发生了IO错误,InputStream
就没法正确地关闭,资源也就没法及时释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.learn.file;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;public class InputStreamTest { public static void main (String[] args) throws IOException { InputStream input = new FileInputStream("Settings\\readme.txt" ); int n; while ((n = input.read()) != -1 ){ System.out.print(n + " " ); } input.close(); } }
1.2 使用 try finally 来处理异常
无论是否异常,总会执行finally来关闭文件!
注意:关闭流时要保证流不为null,否则会抛出NullPointerException
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.learn.file;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;public class InputStreamTest { public static void main (String[] args) throws IOException { System.out.println(); InputStream input1 = null ; try { input1 = new FileInputStream("Settings\\readme.txt" ); int n1; while ((n1 = input1.read()) != -1 ){ System.out.print(n1 + " " ); } }finally { if (input1 != null ){ input1.close(); } } } }
1.3 使用 try(resource) 实现自动关闭流(推荐)
编译器自动为我们关闭资源
实际上,编译器并不会特别地为InputStream
加上自动关闭。编译器只看try(resource = ...)
中的对象是否实现了java.lang.AutoCloseable
接口,如果实现了,就自动加上finally
语句并调用close()
方法。InputStream
和OutputStream
都实现了这个接口,因此,都可以用在try(resource)
中。
只有实现了上述接口使用该语法才会进行自动关闭!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.learn.file;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;public class InputStreamTest { public static void main (String[] args) throws IOException { System.out.println(); try (InputStream input2 = new FileInputStream("Settings\\readme.txt" )) { int n2; while ((n2 = input2.read()) != -1 ){ System.out.print(n2 + " " ); } } } }
2、读到缓存中
在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。
一次读取多个字节时,需要先定义一个byte[]
数组作为缓冲区,read()
方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。
read()
方法的返回值不再是字节的int
值,而是返回实际读取了多少个字节。如果返回-1
,表示没有更多的数据了。
InputStream
提供了两个重载方法来支持读取多个字节:
int read(byte[] b)
:读取若干字节并填充到byte[]
数组,返回读取的字节数
int read(byte[] b, int off, int len)
:指定byte[]
数组的偏移量和最大填充数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.learn.file;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;public class InputStreamTest { public static void main (String[] args) throws IOException { System.out.println(); try (InputStream input3 = new FileInputStream("Settings\\readme.txt" )) { int n2; byte [] buffer = new byte [10 ]; while ((n2 = input3.read(buffer)) != -1 ){ System.out.println("读取了" + n2 + "个字节" ); } } } }
如下:
1 2 3 4 读取了10个字节 读取了10个字节 读取了10个字节 读取了2个字节
3、阻塞
在调用InputStream
的read()
方法读取数据时,我们说read()
方法是阻塞(Blocking)的!
执行到第二行代码时,必须等read()
方法返回后才能继续。因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()
方法调用到底要花费多长时间。
1 2 3 int n; n = input.read(); int m = n;
除了FileInputStream
可以从文件获取输入流,还有ByteArrayInputStream
可以在内存中模拟一个InputStream
:
ByteArrayInputStream
实际上是把一个byte[]
数组在内存中变成一个InputStream
,虽然实际应用不多,但测试的时候,可以用它来构造一个InputStream
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.learn.file;import java.io.ByteArrayInputStream;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;public class InputStreamTest { public static void main (String[] args) throws IOException { byte [] data = { 72 , 101 , 108 , 108 , 111 , 33 }; try (InputStream input4 = new ByteArrayInputStream(data)){ int n3; while ((n3 = input4.read()) != -1 ){ System.out.print((char )n3 + " " ); } } } }
4.2 封装为函数进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package com.learn.file;import java.io.ByteArrayInputStream;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;public class InputStreamTest { public static void main (String[] args) throws IOException { byte [] data = { 72 , 101 , 108 , 108 , 111 , 33 }; try (InputStream input4 = new ByteArrayInputStream(data)){ int n3; while ((n3 = input4.read()) != -1 ){ System.out.print((char )n3 + " " ); } } System.out.println(); String s; try (InputStream input5 = new FileInputStream("Settings\\readme.txt" )) { int n5; StringBuilder sb = new StringBuilder(); while ((n5 = input5.read()) != -1 ){ sb.append((char )n5); } s = sb.toString(); } System.out.println(s); String s1; try (InputStream input6 = new FileInputStream("Settings\\readme.txt" )) { s1 = readAsString(input6); } System.out.println(s1); try (InputStream input7 = new ByteArrayInputStream(data)){ String s2 = readAsString(input7); System.out.println(s2); } } 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(); } }
四、OutputStream
和InputStream
相反,OutputStream
是Java标准库提供的最基本的输出流。
和InputStream
类似,OutputStream
也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b)
这个方法会写入一个字节到输出流。要注意的是,虽然传入的是int
参数,但只会写入一个字节,即只写入int
最低8位表示字节的部分:
OutputStream
也提供了close()
方法关闭输出流,以便释放系统资源!
1 public abstract void write (int b) throws IOException ;
1、flush方法
它的目的是将缓冲区的内容强制输出到目的地。
向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]
数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream
有个flush()
方法,能强制把缓冲区内容输出。
通常情况下,我们不需要调用这个flush()
方法,因为缓冲区写满了OutputStream
会自动调用它,并且,在调用close()
方法关闭OutputStream
之前,也会自动调用flush()
方法。
需要手动调用该方法的例子:实时聊天软件,不能等缓冲区满了在进行输出吧!
2、写入和关闭FileOutStream
如下方,只有当append参数为true时才不会进行覆盖,默认为false!
1 2 3 new FileOutputStream(File file, boolean append); new FileWriter(File file, boolean append);
2.1 一个字节的去读
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.learn.file;import java.io.*;import java.nio.charset.StandardCharsets;public class OutputStreamTest { public static void main (String[] args) throws IOException { File file = new File("Settings\\input.txt" ); if (file.createNewFile()){ System.out.println("input.txt创建成功!" ); } OutputStream output = new FileOutputStream("Settings\\input.txt" ); output.write(72 ); output.write(101 ); output.write(108 ); output.write(108 ); output.write(111 ); output.close(); } }
2.2 一次性读入若干字节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.learn.file;import java.io.*;import java.nio.charset.StandardCharsets;public class OutputStreamTest { public static void main (String[] args) throws IOException { File file = new File("Settings\\input.txt" ); if (file.createNewFile()){ System.out.println("input.txt创建成功!" ); } OutputStream output = new FileOutputStream("Settings\\input.txt" ); output.write("world" .getBytes(StandardCharsets.UTF_8)); output.close(); } }
2.3 使用try(resourse)自动关闭(推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.learn.file;import java.io.*;import java.nio.charset.StandardCharsets;public class OutputStreamTest { public static void main (String[] args) throws IOException { File file = new File("Settings\\input.txt" ); if (file.createNewFile()){ System.out.println("input.txt创建成功!" ); } try (OutputStream output1 = new FileOutputStream("Settings\\input.txt" )){ output1.write("hello world!" .getBytes(StandardCharsets.UTF_8)); } try (InputStream input = new FileInputStream("Settings\\input.txt" )){ int n; while ((n = input.read()) != -1 ){ System.out.print((char ) n); } } } }
3、阻塞
同样,只有等write方法执行完毕才能执行下一行!
所以write方法也是阻塞的!
1 2 3 int n; n = output.write(); int m = n;
4、OutputStream实现类
用FileOutputStream
可以从文件获取输出流,这是OutputStream
常用的一个实现类。此外,ByteArrayOutputStream
可以在内存中模拟一个OutputStream
ByteArrayOutputStream
实际上是把一个byte[]
数组在内存中变成一个OutputStream
,虽然实际应用不多,但测试的时候,可以用它来构造一个OutputStream
。
使用ByteArrayOutputStream模拟OutputStream
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.learn.file;import java.io.*;import java.nio.charset.StandardCharsets;public class OutputStreamTest { public static void main (String[] args) throws IOException { byte [] data; try (ByteArrayOutputStream output1 = new ByteArrayOutputStream()){ output1.write("Hello world!" .getBytes(StandardCharsets.UTF_8)); data = output1.toByteArray(); } System.out.println(new String(data)); } }
5、一个小例子
实现文件的复制!
在命令行进行运行带有参数的运行,即将source.txt 复制到copy.txt;
当然要在源文件下,或者使用相对路径绝对路径都可以!
前提是两个文件都已经存在了!
1 $ java CopyTest.java source.txt copy.txt
main函数的args参数就是用来接收命令行参数的,直接将第一个参数给了输入流,第二个参数给了输出流,即可实现将输入流复制到输出流的copy文件里!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.learn.file;import java.io.*;public class CopyTest { public static void main (String[] args) throws IOException { try (InputStream source = new FileInputStream(args[0 ]); OutputStream copyfile = new FileOutputStream(args[1 ])){ int n; while ((n = source.read()) != -1 ){ copyfile.write(n); } } } }
效果:
之前:
1 2 3 4 source.txt: I am a source file! copy.txt: (空)
之后:
1 2 3 4 source.txt: I am a source file! copy.txt: I am a source file!
五、Filter模式
InputStream
,OutputStream
都是以这种Filter模式来提供各种功能:
下面仅以InputStream
举例!
FileInputStream
:从文件读取数据,是最终数据源;
ServletInputStream
:从HTTP请求读取数据,是最终数据源;
Socket.getInputStream()
:从TCP连接读取数据,是最终数据源;
…
如果要给FileInputStream
添加各种功能:
1 2 3 4 5 6 7 BufferedFileInputStream extends FileInputStream DigestFileInputStream extends FileInputStream CipherFileInputStream extends FileInputStream
这还只是针对FileInputStream
设计,如果针对另一种InputStream
设计,很快会出现子类爆炸的情况。
因此,直接使用继承,为各种InputStream
附加更多的功能,根本无法控制代码的复杂度,很快就会失控。
为了解决依赖继承会导致子类数量失控的问题,JDK首先将InputStream
分为两大类:
无论我们包装多少次,得到的对象始终是InputStream
,我们直接用InputStream
来引用它!
通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)。它可以让我们通过少量的类来实现各种功能的组合!
FilterInputStream是其他各钟功能的父类!
一类是直接提供数据的基础InputStream
,例如:
FileInputStream
ByteArrayInputStream
ServletInputStream
…
一类是提供额外附加功能的InputStream
,例如:
BufferedInputStream
DigestInputStream
CipherInputStream
…
1 2 3 4 5 6 InputStream file = new FileInputStream("test.gz" ); InputStream buffered = new BufferedInputStream(file); InputStream gzip = new GZIPInputStream(buffered);
结构图像这样:
OutputStream
也一样!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ┌─────────────┐ │ InputStream │ └─────────────┘ ▲ ▲ ┌────────────────────┐ │ │ ┌─────────────────┐ │ FileInputStream │─┤ └─│FilterInputStream│ └────────────────────┘ │ └─────────────────┘ ┌────────────────────┐ │ ▲ ┌───────────────────┐ │ByteArrayInputStream│─┤ ├─│BufferedInputStream│ └────────────────────┘ │ │ └───────────────────┘ ┌────────────────────┐ │ │ ┌───────────────────┐ │ ServletInputStream │─┘ ├─│ DataInputStream │ └────────────────────┘ │ └───────────────────┘ │ ┌───────────────────┐ └─│CheckedInputStream │ └───────────────────┘
下面编写一个自定义的FilterInputStream来实现需要实现的功能,同时还可以使得所有InputStream都可以使用该功能:
CountInputStream实现了计数功能;
所有的功能都必须得继承自FilterInputStream!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package com.learn.file;import java.io.ByteArrayInputStream;import java.io.FilterInputStream;import java.io.IOException;import java.io.InputStream;import java.nio.charset.StandardCharsets;public class FilterWork { public static void main (String[] args) throws IOException { byte [] data = "hello world!" .getBytes(StandardCharsets.UTF_8); try (CountInputStream inputStream = new CountInputStream(new ByteArrayInputStream(data))){ int n; while ((n = inputStream.read()) != -1 ){ System.out.print((char )n); } System.out.println("\nTotal read " + inputStream.getBytesRead() + " bytes!" ); } } }class CountInputStream extends FilterInputStream { private int count; protected 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); this .count += n; return n; } }
六、操作Zip
ZipInputStream
是一种FilterInputStream
,它可以直接读取zip包的内容:
另一个JarInputStream
是从ZipInputStream
派生,它增加的主要功能是直接读取jar文件里面的MANIFEST.MF
文件。因为本质上jar包就是zip包,只是额外附加了一些固定的描述文件。
继承的结构关系:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ┌───────────────────┐ │ InputStream │ └───────────────────┘ ▲ │ ┌───────────────────┐ │ FilterInputStream │ └───────────────────┘ ▲ │ ┌───────────────────┐ │InflaterInputStream│ └───────────────────┘ ▲ │ ┌───────────────────┐ │ ZipInputStream │ └───────────────────┘ ▲ │ ┌───────────────────┐ │ JarInputStream │ └───────────────────┘
2、读取Zip包
创建一个ZipInputStream
,通常是传入一个FileInputStream
作为数据源,然后,循环调用getNextEntry()
,直到返回null
,表示zip流结束。
一个ZipEntry
表示一个压缩文件或目录,如果是压缩文件,我们就用read()
方法不断读取,直到返回-1
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.learn.file;import java.io.FileInputStream;import java.io.IOException;import java.util.zip.ZipEntry;import java.util.zip.ZipInputStream;public class ZipTest { public static void main (String[] args) throws IOException { try (ZipInputStream zip = new ZipInputStream(new FileInputStream(".\\zip.zip" ))) { ZipEntry entry = null ; while ((entry = zip.getNextEntry()) != null ) { System.out.println("文件或目录名:" + entry.getName()); if (!entry.isDirectory()) { int n; System.out.print("内容:" ); while ((n = zip.read()) != -1 ) { System.out.print((char ) n); } System.out.println(); } } } } }
结果如下:
1 2 3 4 5 文件或目录名:niub/ 文件或目录名:test.txt 内容:I am a Test file! 文件或目录名:zip.txt 内容:hello world!
3、写入Zip包
ZipOutputStream
是一种FilterOutputStream
,它可以直接写入内容到zip包。我们要先创建一个ZipOutputStream
,通常是包装一个FileOutputStream
,然后,每写入一个文件前,先调用putNextEntry()
,然后用write()
写入byte[]
数据,写入完毕后调用closeEntry()
结束这个文件的打包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package com.learn.file;import java.io.*;import java.util.zip.ZipEntry;import java.util.zip.ZipInputStream;import java.util.zip.ZipOutputStream;public class ZipTest { public static void main (String[] args) throws IOException { try (ZipInputStream zip = new ZipInputStream(new FileInputStream(".\\zip.zip" ))) { ZipEntry entry = null ; while ((entry = zip.getNextEntry()) != null ) { System.out.println("文件或目录名:" + entry.getName()); if (!entry.isDirectory()) { int n; System.out.print("内容:" ); while ((n = zip.read()) != -1 ) { System.out.print((char ) n); } System.out.println(); } } } File file1 = new File(".\\Settings" ); try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(".\\zip.zip" ))) { File[] files = file1.listFiles(); if (files != null ){ for (File file : files) { zip.putNextEntry(new ZipEntry(file.getName())); zip.write(getFileDataAsBytes(file)); zip.closeEntry(); } } } } private static byte [] getFileDataAsBytes(File file) throws IOException{ byte [] data = new byte [1024 ]; try (InputStream file1 = new FileInputStream(file)){ int n; while ((n = file1.read(data)) != -1 ){ System.out.println("read " + n + " byte" ); } } return data; } }
七、读取classpath资源
是一种与路径无关的读取文件的方式:
从classpath读取文件就可以避免不同环境下文件路径不一致的问题:如果我们把default.properties
文件放到classpath中,就不用关心它的实际存放路径。
在classpath中的资源文件,路径总是以/
开头,我们先获取当前的Class
对象,然后调用getResourceAsStream()
就可以直接从classpath读取任意的资源文件!
关于classpath目前还不太清楚,后续再进行学习!
Java存放.class
的目录或jar包也可以包含任意其他类型的文件,例如:
配置文件,例如.properties
;
图片文件,例如.jpg
;
文本文件,例如.txt
,.csv
;
……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.learn.file;import java.io.IOException;import java.io.InputStream;public class ClassPathTest { public static void main (String[] args) throws IOException { try (InputStream input = ClassPathTest.class.getResourceAsStream("/default.properties" )) { if (input != null ) { int n; while ((n = input.read()) != -1 ){ System.out.print((char ) n); } } } } }
如果我们把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置:
1 2 3 Properties props = new Properties(); props.load(inputStreamFromClassPath("/default.properties" )); props.load(inputStreamFromFile("./conf.properties" ));
八、序列化
序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]
数组。
为什么要把Java对象序列化呢?因为序列化后可以把byte[]
保存到文件中,或者把byte[]
通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
有序列化,就有反序列化,即把一个二进制内容(也就是byte[]
数组)变回Java对象。有了反序列化,保存到文件中的byte[]
数组又可以“变回”Java对象,或者从网络上读取byte[]
并把它“变回”Java对象。
一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable
接口:
Serializable
接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。
这节有点云里雾里,后续再看!
1、序列化
把一个Java对象变为byte[]
数组,需要使用ObjectOutputStream
。它负责把一个Java对象写入一个字节流。
ObjectOutputStream
既可以写入基本类型,如int
,boolean
,也可以写入String
(以UTF-8编码),还可以写入实现了Serializable
接口的Object
。
因为写入Object
时需要大量的类型信息,所以写入的内容很大。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.learn.file;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;import java.util.Arrays;public class SerializableTest { public static void main (String[] args) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); try (ObjectOutputStream output = new ObjectOutputStream(buffer)) { output.writeInt(12345 ); output.writeUTF("Hello" ); output.writeObject(123.456 ); } System.out.println(Arrays.toString(buffer.toByteArray())); } }
2、反序列化
和ObjectOutputStream
相反,ObjectInputStream
负责从一个字节流读取Java对象
除了能读取基本类型和String
类型外,调用readObject()
可以直接返回一个Object
对象。要把它变成一个特定类型,必须强制转型。
要特别注意反序列化的几个重要特点:
反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。
readObject()
可能抛出的异常有:
ClassNotFoundException
:没有找到对应的Class;
InvalidClassException
:Class不匹配。
对于ClassNotFoundException
,这种情况常见于一台电脑上的Java程序把一个Java对象,例如,Person
对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义Person
类,所以无法反序列化。
对于InvalidClassException
,这种情况常见于序列化的Person
对象定义了一个int
类型的age
字段,但是反序列化时,Person
类定义的age
字段被改成了long
类型,所以导致class不兼容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.learn.file;import java.io.*;import java.util.Arrays;public class SerializableTest { public static void main (String[] args) throws Exception { ByteArrayInputStream bufferin = new ByteArrayInputStream(buffer.toByteArray()); try (ObjectInputStream input = new ObjectInputStream(bufferin)) { int n = input.readInt(); String s = input.readUTF(); Double d = (Double) input.readObject(); System.out.println(n); System.out.println(s); System.out.println(d); } } }
为了避免这种class定义变动导致的不兼容,Java的序列化允许class定义一个特殊的serialVersionUID
静态变量,用于标识Java类的序列化“版本”,通常可以由IDE自动生成。如果增加或修改了字段,可以改变serialVersionUID
的值,这样就能自动阻止不匹配的class版本:
1 2 3 public class Person implements Serializable { private static final long serialVersionUID = 2709425275741743919L ; }
3、安全性
因为Java的序列化机制可以导致一个实例能直接从byte[]
数组创建,而不经过构造方法,因此,它存在一定的安全隐患。一个精心构造的byte[]
数组被反序列化后可以执行特定的Java代码,从而导致严重的安全漏洞。
实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。
九、Reader
Reader
是Java的IO库提供的另一个输入流接口。和InputStream
的区别是,InputStream
是一个字节流,即以byte
为单位读取,而Reader
是一个字符流,即以char
为单位读取:
java.io.Reader
是所有字符输入流的超类!
read()
方法读取字符流的下一个字符,并返回字符表示的int
,范围是0
~65535
。如果已读到末尾,返回-1
。
InputStream
Reader
字节流,以byte
为单位
字符流,以char
为单位
读取字节(-1,0~255):int read()
读取字符(-1,0~65535):int read()
读到字节数组:int read(byte[] b)
读到字符数组:int read(char[] c)
2、FileReader
FileReader
是Reader
的一个子类,它可以打开文件并获取`Reader:
FileReader()
可以接收一个编码参数!
2.1 直接创建FileReader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.learn.file;import java.io.FileReader;import java.io.IOException;import java.io.Reader;public class ReaderTest { public static void main (String[] args) throws IOException { Reader reader = new FileReader("Settings\\readme.txt" ); int n; while ((n = reader.read()) != -1 ){ System.out.print((char ) n); } reader.close(); } }
2.2 使用try(resourse)实现自动关闭资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.learn.file;import java.io.FileReader;import java.io.IOException;import java.io.Reader;public class ReaderTest { public static void main (String[] args) throws IOException { try (Reader reader = new FileReader("Settings\\readme.txt" )){ int n; while ((n = reader.read()) != -1 ){ System.out.print((char ) n); } } } }
2.3 填充字符到char数组
read(char[] c)
返回实际读入的字符个数,最大不超过char[]
数组的长度。返回-1
表示流结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.learn.file;import java.io.FileReader;import java.io.IOException;import java.io.Reader;public class ReaderTest { public static void main (String[] args) throws IOException { try (Reader reader = new FileReader("Settings\\readme.txt" )){ int n; char [] buffer = new char [100 ]; while ((n = reader.read(buffer)) != -1 ){ System.out.println("read " + n + "chars" ); } } } }
3、CharArrayReader
CharArrayReader
可以在内存中模拟一个Reader
,它的作用实际上是把一个char[]
数组变成一个Reader
:
1 2 3 4 5 6 try (Reader reader = new CharArrayReader("Hello" .toCharArray())) { int n; while ((n = reader.read()) != -1 ){ System.out.print((char ) n); } }
4、StringReader
StringReader
可以直接把String
作为数据源,它和CharArrayReader
几乎一样:
1 2 3 4 5 6 try (Reader reader = new StringReader("Hello" )) { int n; while ((n = reader.read()) != -1 ){ System.out.print((char ) n); } }
除了特殊的CharArrayReader
和StringReader
,普通的Reader
实际上是基于InputStream
构造的。
因为Reader
需要从InputStream
中读入字节流(byte
),然后,根据编码设置,再转换为char
就可以实现字符流。
InputStreamReader
就是一个转换器,它可以把任何InputStream
转换为Reader
:
两个参数,一个InputStream,一个可选的编码方式参数!
使用try (resource)
结构时,当我们关闭Reader
时,它会在内部自动调用InputStream
的close()
方法,所以,只需要关闭最外层的Reader
对象即可
1 2 3 4 5 6 7 8 9 10 InputStream input = new FileInputStream("src/readme.txt" ); Reader reader = new InputStreamReader(input, StandardCharsets.UTF_8);try (Reader reader1 = new InputStreamReader(new FileInputStream("src/readme.txt" ),StandardCharsets.UTF_8)) { System.out.println(reader1); }
十、Writer
Reader
是带编码转换器的InputStream
,它把byte
转换为char
,而Writer
就是带编码转换器的OutputStream
,它把char
转换为byte
并输出:
OutputStream
Writer
字节流,以byte
为单位
字符流,以char
为单位
写入字节(0~255):void write(int b)
写入字符(0~65535):void write(int c)
写入字节数组:void write(byte[] b)
写入字符数组:void write(char[] c)
无对应方法
写入String:void write(String s)
Writer
是所有字符输出流的超类,它提供的方法主要有:
写入一个字符(0~65535):void write(int c)
;
写入字符数组的所有字符:void write(char[] c)
;
写入String表示的所有字符:void write(String s)
。
2、FileWriter
同样可以设置第三个参数,使其不进行覆盖写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.learn.file;import java.io.*;import java.nio.charset.StandardCharsets;public class WriterTest { public static void main (String[] args) throws IOException { try (Writer writer = new FileWriter("readme.txt" , StandardCharsets.UTF_8, true )) { writer.write('H' ); writer.write("Hello" .toCharArray()); writer.write("Hello" ); } try (Reader reader = new FileReader("readme.txt" )){ int n; while ((n = reader.read()) != -1 ){ System.out.print((char ) n); } } } }
3、CharArrayWriter
CharArrayWriter
可以在内存中创建一个Writer
,它的作用实际上是构造一个缓冲区,可以写入char
,最后得到写入的char[]
数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.learn.file;import java.io.*;import java.nio.charset.StandardCharsets;public class WriterTest { public static void main (String[] args) throws IOException { try (CharArrayWriter writer = new CharArrayWriter()) { writer.write(65 ); writer.write(66 ); writer.write(67 ); char [] data = writer.toCharArray(); System.out.print(data); } } }
4、StringWriter
StringWriter
也是一个基于内存的Writer
,它和CharArrayWriter
类似。实际上,StringWriter
在内部维护了一个StringBuffer
,并对外提供了Writer
接口:
可以使用StringBuffer
的所有方法,只是实现了一个writer接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.learn.file;import java.io.*;import java.nio.charset.StandardCharsets;public class WriterTest { public static void main (String[] args) throws IOException { try (StringWriter writer = new StringWriter()) { writer.write("hello world nxd" ); writer.append(" last!" ); System.out.print(writer); } } }
5、OutputStreamWriter
除了CharArrayWriter
和StringWriter
外,普通的Writer实际上是基于OutputStream
构造的,它接收char
,然后在内部自动转换成一个或多个byte
,并写入OutputStream
。因此,OutputStreamWriter
就是一个将任意的OutputStream
转换为Writer
的转换器:
1 2 3 4 5 try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt" ), StandardCharsets.UTF_8)) { int n; writer.write("hello" ); }
十一、PrintStream和PrintWriter
1、PrintStream
PrintStream
和OutputStream
相比,除了添加了一组print()
/println()
方法,可以打印各种数据类型,比较方便外,它还有一个额外的优点,就是不会抛出IOException
,这样我们在编写代码的时候,就不必捕获IOException
。
以及对应的一组println()
方法,它会自动加上换行符。
我们经常使用的System.out.println()
实际上就是使用PrintStream
打印各种数据。其中,System.out
是系统默认提供的PrintStream
,表示标准输出.
System.err
是系统默认提供的标准错误输出。
PrintStream
是一种FilterOutputStream
,它在OutputStream
的接口上,额外提供了一些写入各种数据类型的方法:
写入int
:print(int)
写入boolean
:print(boolean)
写入String
:print(String)
写入Object
:print(Object)
,实际上相当于print(object.toString())
…
1 2 3 4 5 6 7 8 9 10 11 package com.learn.file;public class PrintStreamTest { public static void main (String[] args) { System.out.print(12345 ); System.out.print(new Object()); System.out.println("Hello" ); System.err.println("hhh" ); } }
2、PrintWriter
PrintStream
最终输出的总是byte数据,而PrintWriter
则是扩展了Writer
接口,它的print()
/println()
方法最终输出的是char
数据。两者的使用方法几乎是一模一样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.learn.file;import java.io.PrintWriter;import java.io.StringWriter;public class PrintWriterTest { 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()); } }
本章终于完结,敬请期待后续内容!