封面来源:碧蓝航线 北境序曲 活动CG
本文参考:尚硅谷 宋红康 Java 零基础教程 P577-P619
1. File 类的使用
1.1 路径分隔符
路径中的每级目录之间用一个 路径分隔符 隔开。
路径分隔符与系统有关:
- windows 和 DOS 系统默认使用 “\” 来表示
- UNIX 和 URL 使用 “/” 来表示
Java 程序支持跨平台运行,因此路径分隔符要 慎用 !
为了解决这个隐患,File 类提供了一个常量:
1
| public static final String separator
|
使用这个常量可以根据操作系统,动态的提供分隔符。比如:
1 2 3
| File file1 = new File("D:\\mofan.txt"); File file2 = new File("D:" + File.separator + "mofan.txt"); File file3 = new File("d:/mofan");
|
这里出现了路径的一种写法:绝对路径,既然有绝对路径,那么也有相对路径。
- 相对路径:相较于某个路径,指明的路径
- 绝对路径:包含盘符在内的文件或文件目录的路径
相对路径的注意要点
IDEA 中:
如果开发使用 JUnit 中的单元测试方法进行测试,相对路径即为当前 Module 下。
如果使用 main() 进行测试,相对路径即为当前 Project 下。
1 2 3 4 5 6 7 8 9 10
| public static void main(String[] args) { File file = new File("hello.txt"); System.out.println(file.getAbsolutePath());
File file1 = new File("IO-Stream\\hello.txt"); System.out.println(file1.getAbsolutePath()); }
|
Eclipse 中:
不管使用单元测试方法还是使用 main() 方法,相对路径都是当前 Project 下。
1.2 常用构造器
第一种
1
| public File(String pathname)
|
以 pathname 为路径创建 File 对象,可以是 绝对路径或相对路径 ,如果 pathname 是相对路径,则默认的当前路径在系统属性 user.dir 中存储。
- 绝对路径:是一个固定的路径,从盘符开始
- 相对路径:相对于某个位置开始
第二种
1
| public File(String parent, String child)
|
以 parent 为父路径,child 为子路径创建 File 对象
第三种
1
| public File(File parent, String child)
|
根据一个父 File 对象和子文件路径创建 File 对象
1.3 常用方法
File 类的获取功能
public String getAbsolutePath():获取绝对路径
public String getPath():获取路径
public String getName() :获取名称
public String getParent():获取上层文件目录路径。若无,返回null
public long length() :获取文件长度 (即:字节数) ,但不能获取目录的长度。
public long lastModified() :获取最后一次的修改时间,毫秒值
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Test public void test2() { File file1 = new File("hello.txt"); File file2 = new File("D:\\io\\hi.txt");
System.out.println(file1.getAbsoluteFile()); System.out.println(file1.getPath()); System.out.println(file1.getName()); System.out.println(file1.getParent()); System.out.println(file1.length()); System.out.println(new Date(file1.lastModified()));
System.out.println();
System.out.println(file2.getAbsoluteFile()); System.out.println(file2.getPath()); System.out.println(file2.getName()); System.out.println(file2.getParent()); System.out.println(file2.length()); System.out.println(file2.lastModified()); }
|
如下两个方法适用于文件目录:
public String[] list() :获取指定目录下的所有文件或者文件目录的名称数组
public File[] listFiles() :获取指定目录下的所有文件或者文件目录的 File 数组
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Test public void test3() { File file = new File("D:\\io"); String[] list = file.list(); for (String s : list) { System.out.println(s); }
File[] files = file.listFiles(); for (File f : files) { System.out.println(f); } }
|
File 类的重命名功能
public boolean renameTo(File dest):把文件重命名为指定的文件路径。
针对 file1.renameTo(file2) 来说,需要 file1 存在,但是 file2 不存在,将 file1 移动到 file2 并改名为 file2 (相当于剪切与重命名)。
测试代码:
1 2 3 4 5 6 7 8 9 10 11
| @Test public void test4() { File file1 = new File("hello.txt"); File file2 = new File("D:\\io\\hi.txt");
boolean renameTo = file1.renameTo(file2); System.out.println(renameTo); }
|
File 类的判断功能
public boolean isDirectory():判断是否是文件目录
public boolean isFile():判断是否是文件
public boolean exists():判断是否存在
public boolean canRead():判断是否可读
public boolean canWrite():判断是否可写
public boolean isHidden():判断是否隐藏
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Test public void test5() { File file1 = new File("hello.txt");
System.out.println(file1.isDirectory()); System.out.println(file1.isFile()); System.out.println(file1.exists()); System.out.println(file1.canRead()); System.out.println(file1.canWrite()); System.out.println(file1.isHidden());
System.out.println("======================");
File file2 = new File("D:\\io"); System.out.println(file2.isDirectory()); System.out.println(file2.isFile()); System.out.println(file2.exists()); System.out.println(file2.canRead()); System.out.println(file2.canWrite()); System.out.println(file2.isHidden()); }
|
FIle 类的创建功能
public boolean createNewFile():创建文件。若文件存在,则不创建并返回 false 。
public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建。如果此文件目录的上层目录不存在,也不创建。
public boolean mkdirs():创建文件目录。如果上层文件目录不存在, 一并创建。
注意:如果你创建文件或者文件目录没有写盘符路径,那么默认在项目路径下。
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Test public void test7() { File file1 = new File("D:\\io\\io1\\io3"); boolean mkdir1 = file1.mkdir(); if (mkdir1) { System.out.println("创建成功1"); }
File file2 = new File("D:\\io\\io1\\io4"); boolean mkdir2 = file1.mkdirs(); if (mkdir2) { System.out.println("创建成功2"); } }
|
当硬盘中真有一个真实的文件或目录存在,创建 File 对象的时候,各个属性会被显式赋值。
当硬盘中 没有 真实的文件或目录存在时,那么创建 File 对象时,除了指定的目录和路径之外,其他的属性都是取成员变量的默认值 。
File 类的删除功能
public boolean delete():删除文件或者文件夹
删除注意事项:
Java 中的删除不走回收站 。要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。
1.4 总结
使用 File 类时,我们需要明白:
1、File 类的一个对象,代表一个文件或一个文件目录
2、File 类声明在 java.io 包下
3、File 类中设计到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法,但是并未设计到写入或读取文件内容的操作。如果需要读取或写入文件内容,就必须使用 IO 流来完成。
4、后续 File 类的对象常会作为参数传递到流的构造器中,指明读取或写入的“终点”。
2. IO 流概述
2.1 Java IO 原理
I/O 是 Input / Output 的缩写,I/O 技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。😮
Java 程序中,对于数据的输入 / 输出操作以“流(stream)”的方式进行。
java.io 包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。
输入 input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中
输出 output:将程序(内存)数据输出到磁盘、光盘等存储设备中。
输入与输出针对不同的对象,会得到不同的结果。比如站在文件的角度来看,将文件中的数据写入到内存中,那这就是输出,但从内存的角度来看,这就是输入。因为我们是编写代码的人,因此我们需要 站在内存(程序)的角度 来看待 IO。
2.2 流的分类
按操作数据单位不同可分为:字节流(8 bit)、字符流(16 bit)
按数据流的流向不同分为:输入流、输出流
按流的角色不同分为:节点流、处理流
| 抽象基类 |
字节流 |
字符流 |
| 输入流 |
InputStream |
Reader |
| 输出流 |
OutputStream |
Writer |
Java 的 IO 流共涉及到 40 多个类,实际上非常规则,都是从如下 4 个抽象基类派生的。
由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。
当我们操作文本数据时,里面的数据都是一个一个的字符,所以用字符流;当我们操作视频、音频或图片时,就需要用字节流。
针对数据流的流向,我们是一直站在程序(内存)的角度出发的,写入内存叫输入流,从内存中输出到外部存储就叫输出流。
如果流直接作用在文件上,那么这叫节点流;如果当前流作用在已有的流的基础之上,那么当前流叫做处理流(可以理解成:水流等价于节点流,管道等价于处理流)。😵
关于节点流和处理流还可以这样理解:自己就可以操作的叫节点流,依赖于别的流操作的叫处理流。😰
IO 流体系图
Java IO 流体系图参考:Java IO 流体系图
| 分类 |
字节输入流 |
字节输出流 |
字符输入流 |
字符输出流 |
| 抽象基类 |
InputStream |
OutputStream |
Reader |
Writer |
| 访问文件 |
FileInputStream |
FileOutputStream |
FileReader |
FileWriter |
| 访问数组 |
ByteArrayInputStream |
ByteArrayOutputStream |
CharArrayReader |
CharArrayWriter |
| 访问管理 |
PipedInputStream |
PipedOutputStream |
PipedReader |
PipedWriter |
| 访问字符串 |
|
|
StringReader |
StringWriter |
| 缓冲流 |
BufferedInputStream |
BufferedOuputStream |
BufferedReader |
BufferedWriter |
| 转换流 |
|
|
InputStreamReader |
OutputStreamWriter |
| 对象流 |
ObjectInputStream |
ObjectOutputStream |
|
|
|
FilterInputStream |
FilterOutputStream |
FilterReader |
FilterWriter |
| 打印流 |
|
PrintStream |
|
PrintWriter |
| 推回输入流 |
PushbackInputStream |
|
PushbackReader |
|
| 特殊流 |
DataInputStream |
DataOutputStream |
|
|
重点放在:抽象基类、访问文件、缓冲流、转换流和对象流上。
3. 字符流
3.1 数据读入
使用 FileReader 读取文件中的数据,测试代码如下:
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
|
@Test public void testFileReader() { FileReader fileReader = null; try { File file = new File("hello.txt"); fileReader = new FileReader(file);
int data; while ((data = fileReader.read()) != -1) { System.out.print(((char) data)); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fileReader != null) { fileReader.close(); } } catch (IOException e) { e.printStackTrace(); } } }
|
在上述代码中,FileReader 的构造方法、read() 和 close() 都会抛出异常,为了保证资源一定可以执行关闭操作,不建议使用 throws,采用 try-catch 更好。
被读取文件一定要存在,否则会抛出 FileNotFoundException 异常。
在上述代码中,读取文件中的内容使用了 read() 方法,这个方法可以读取单个字符 ,如果纯文本文件中有内容的话,就会返回 ASCII 码表中对应的数字值,如果 ASCII 表中不存在,就会去查找 Unicode 表。将字符读取完毕后,会将读取目标指向下一个字符,因此常与循环一起使用。
如果纯文本文件没有内容 ,或者前面的内容已经被读取完了,read() 返回的就是 -1,由于我们不知道文本内容有多少,所以我们需要使用一个循环。
read() 方法只能读取一个字符,如果读取的文本很长,那效率也太低了。😳
因此,我们想可不可以一次性多读取一点,然后放到数组或字符串中,那样多奈斯!😏
JDK 提供了一个 read() 的重载方法,可以一次性多读取一点,并将其放到 char[] 中。测试代码如下:
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
| @Test public void testFileReader1(){ FileReader fileReader = null; try { File file = new File("hello.txt"); fileReader = new FileReader(file); char[] cBuf = new char[5]; int len; while ((len = fileReader.read(cBuf)) != -1) {
String str = new String(cBuf, 0, len); System.out.print(str); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fileReader != null) { fileReader.close(); } } catch (IOException e) { e.printStackTrace(); } } }
|
read(char[] cBuf):返回每次读入 cBuf 数组中的字符的个数。如果达到文件末尾,返回 -1 。
使用 read(char[] cBuf) 方法时,将读取字符数组。假设字符数组的长度是 2,那么一口气读的是 2 个字符,但由于文本内容只有 3 个字符,那么第二次读取的时候,可读取的有效长度就是 1 ,那么存放在字符数组的第 1 位就会被覆盖掉,但是第 2 位没有被覆盖,就会保留。
如果我们直接读取字符串,而不指定有效长度的话,会按字符数组的长度读取,并将没覆盖的字符读取出来,而不是只读有效长度。
所以我们在创建一个 String 对象的时候,要如此使用。
1
| String str = new String(a, 0, len);
|
a 代表的是字符数组,0 代表起始位置,len 代表读取的长度。
参考链接:FileReader用法和问答
3.2 数据写出
有了读入,当然还有写出,字符流写出将用到 FileWriter ,测试代码如下:
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
| @Test public void testFileWriter() { FileWriter fileWriter = null; try { File file = new File("hello1.txt"); fileWriter = new FileWriter(file); fileWriter.write("I have a dream!\n"); fileWriter.write("You need to have a dream!\n"); } catch (IOException e) { e.printStackTrace(); } finally { try { if (fileWriter != null) { fileWriter.close(); } } catch (IOException e) { e.printStackTrace(); } } }
|
使用字符流写出数据时,有几点需要进行说明:
1、使用字符流进行写出操作时,内容写入的目标文件可以不存在,且不会报异常。
2、File 对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建文件。
3、File 对应的硬盘中的文件如果存在:
-
如果流使用构造器是 FileWriter(file, false) 或 FileWriter(file) 将会对原有文件进行覆盖
-
如果流使用构造器是 FileWriter(file, true) 不会对原有文件进行覆盖,而是原有文件基础上追加内容
3.3 读入与写出
前面列举了数据读入和写出,但是读入是将文本文件内容输出到控制台上,写入是使用代码的方式直接写入到文本文件中,那能否将两者结合一下?
先读入 A 文件的内容,然后将该内容写入到 B 文件中呢?(这就是传说中的文本复制! 😂)
其实也很简单,直接上测试代码:
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
| @Test public void testFileReaderFileWriter() { FileReader fileReader = null; FileWriter fileWriter = null; try { File srcFile = new File("hello.txt"); File destFile = new File("hello2.txt"); fileReader = new FileReader(srcFile); fileWriter = new FileWriter(destFile); char[] cBuf = new char[5]; int len; while ((len = fileReader.read(cBuf)) != -1) { fileWriter.write(cBuf, 0 ,len); } } catch (IOException e) { e.printStackTrace(); } finally { try { fileWriter.close(); } catch (IOException e) { e.printStackTrace(); }
try { fileReader.close(); } catch (IOException e) { e.printStackTrace(); } } }
|
3.4 拓展
我们前面编写的代码都是处理文本文件的,那么那些代码可以处理图片吗?
是否只需要将 File 构造器内的文件名修改一下就可以了呢?比如:
1 2 3
| File srcFile = new File("pic.jpg"); File desFile = new File("pic2.jpg");
|
其实测试一下就知道,压根是不行的,虽然 pic2.jpg 会出现在我们的 Module 下,但并不能打开该文件。
原因也很简单,FileReader 和 FileWriter 都是处理字符流的,或者说都是处理文本文件的,而图片并不是文本文件,属于二进制文件,要想处理图片,就需要用到字节流! 👇
4. 字节流
4.1 字节输入流初体验
上文说到,对于文本文件(.txt, .java, .c, .cpp 等)使用字符流处理,对于非文本文件(.jpg, .mp3, .mp4, .pdf 等)使用字节流处理。我们也验证了使用字符流 FileReader 和 FileWriter 处理非文本文件会出错,那么使用字节流 FileInputStream 和 FileOutputStream 来处理文本文件会怎么样呢?
不多 BB,直接上代码,步骤和格式与前面的操作差不多:
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
| @Test public void testFileInputStream() { FileInputStream fileInputStream = null; try { File file = new File("hello.txt"); fileInputStream = new FileInputStream(file); byte[] bytes = new byte[5]; int len; while ((len = fileInputStream.read(bytes)) != -1) { String s = new String(bytes, 0, len); System.out.print(s); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fileInputStream != null) { fileInputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } }
|
相信聪明的你一定看到贴出代码的首行注释了:使用字节流 FileInputStream 处理文本文件可能出现乱码。于是你一通 cv 把代码拷贝到自己的项目中,然后发现并没有出现乱码,这是怎么回事?
可以看一下 hello.txt 文件的编码,一般来说都是 UTF-8 ,在 UTF-8 中,英文字符和数字也都是一个字节,使用字节流传输是没有什么问题的,但是如果你加几个中文汉字呢?
于是你又一番操作,结果发现还是没乱码?咋回事?(第一次这么想让汉字乱码😂)
原因也很简单。 在 UTF-8 编码中,一个汉字占 3 个字节。 我们代码中设置的字节数组长度为 5,然后你编写的文本中的数据恰好也是 5 字节的整数倍,不妨再加一个汉字,看看会不会乱码?
我相信,一定会乱码的!
因此结论也得出来了:使用字节流 FileInputStream 处理文本文件可能出现乱码。(这可能二字用得就很妙!)
总的来说,操作文本文件还是使用字符流好,毕竟在字符流中,无论英文、数字,抑或汉字,都算一个字符,如此下来,使用字符流操作文本文件是不会出现乱码的情况的。
扯了这么多,接下来就该进入主题了,使用字节流操作非文本文件。
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
| @Test public void testFileInputOutputStream() {
FileInputStream fileInputStream = null; FileOutputStream fileOutputStream = null;
try { File srcFile = new File("妖梦.jpg"); File destFile = new File("魂魄妖梦.jpg");
fileInputStream = new FileInputStream(srcFile); fileOutputStream = new FileOutputStream(destFile);
byte[] bytes = new byte[10]; int len; while ((len = fileInputStream.read(bytes)) != -1) { fileOutputStream.write(bytes, 0, len); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fileOutputStream != null) { fileOutputStream.close(); } } catch (IOException e) { e.printStackTrace(); }
try { if (fileInputStream != null) { fileInputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } }
|
运行后,确实实现了图片的复制,复制得到的图片也可以正常打开。
什么?魂魄妖梦是啥?😇 老二刺猿了!
4.3 文件复制
根据 4.2 操作非文本文件 提供的代码,我们可以改写一下代码,编写一个复制文件的方法 copyFile() 。
copyFile() 方法和测试代码如下:
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
| public void copyFile(String srcPath, String destPath) { FileInputStream fileInputStream = null; FileOutputStream fileOutputStream = null;
try { File srcFile = new File(srcPath); File destFile = new File(destPath);
fileInputStream = new FileInputStream(srcFile); fileOutputStream = new FileOutputStream(destFile);
byte[] bytes = new byte[1024]; int len; while ((len = fileInputStream.read(bytes)) != -1) { fileOutputStream.write(bytes, 0, len); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fileOutputStream != null) { fileOutputStream.close(); } } catch (IOException e) { e.printStackTrace(); }
try { if (fileInputStream != null) { fileInputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } }
@Test public void testCopyFile() {
long start = System.currentTimeMillis(); String srcPath = "D:\\01-视频.mp4"; String destPath = "D:\\02-视频.mp4";
copyFile(srcPath, destPath); long end = System.currentTimeMillis(); System.out.println("复制操作花费时间为:" + (end - start)); }
|
运行上述代码,复制一个大小 164M 的视频文件耗费了 2085ms。
你会发现提供的代码中,我还尝试复制了文本文件,那么真的可以复制成功吗?不会像前面测试那样乱码吗?
不妨取消注释,自己尝试一下,最终会发现能够复制成功。
总结
针对上面的现象进行进行一个小总结:
1、 使用字节流读入文本文件并显示在控制台,可能会出现乱码,但是使用字节流复制文本文件并不会出现乱码,并且能够成功复制。 (直接看就乱码,不看就不会乱码,这是薛定谔的代码?😆)
2、不能够使用字符流来处理非文本文件,就算是复制也不行!当然对于文本文件来说,还是使用字符流处理更好。
5. 缓冲流
5.1 非文本文件的复制
缓冲流的概述
前面讲的字符流(FileReader、FileWriter)和字节流(FileInputStream、FileOutputStream) 属于节点流,而缓冲流属于处理流的一种 ,在节点流的基础上包装了一层。那么,缓冲流有什么用?
缓冲流的作用就是为了提高文件的读写效率。因此在设计开发过程中,一般不会使用前面列述的几个类,而是考虑使用缓冲流,因为它们是比较基本的几个流,效率不是很高。
有关缓冲流,存在这四个类:BufferedInputStream 、 BufferedOuputStream 、 BufferedReader 和 BufferedWriter 。根据前面的说明,我们可以很简单地明白前两个是针对字节流的,后面两个是针对字符流的。
还有一点,缓冲流是不能直接作用在源文件上的,需要作用在节点流之上。
缓冲流就是“套接”在已有流的基础上。
测试代码
编写代码,实现图片的复制:
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
| @Test public void BufferedStreamTest(){ BufferedInputStream bis = null; BufferedOutputStream bos = null;
try { File srcFile = new File("妖梦.jpg"); File destFile = new File("Youmu.jpg"); FileInputStream fis = new FileInputStream(srcFile); FileOutputStream fos = new FileOutputStream(destFile); bis = new BufferedInputStream(fis); bos = new BufferedOutputStream(fos);
byte[] bytes = new byte[10]; int len; while ((len = bis.read(bytes)) != -1) { bos.write(bytes, 0, len); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (bos != null) { bos.close(); } } catch (IOException e) { e.printStackTrace(); }
try { if (bis != null) { bis.close(); } } catch (IOException e) { e.printStackTrace(); }
} }
|
运行代码后,可以前往当前 Module 中查看复制得到的图片。
上述代码中,涉及到 4 个类,两种节点流,两种作用于节点流的处理流(缓冲流),那么最后关闭的时候要关闭 4 种?
当然不用。关闭外层的流时,内层流也会自动关闭,因此,内层流的关闭可以省略。
使用缓冲流可以实现非文本文件的复制,但是怎么体现缓冲流的作用(提高了读写效率)呢?
5.2 读写速度对比
和前面一样,提取一个 copyFileWithBuffered() 方法用于复制一个视频,然后使用与前面相同的视频进行测试,比较使用缓冲流所耗费的时间与未使用缓冲流所耗费的时间:
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
| public void copyFileWithBuffered(String srcPath, String destPath) { BufferedInputStream bis = null; BufferedOutputStream bos = null;
try { File srcFile = new File(srcPath); File destFile = new File(destPath); FileInputStream fis = new FileInputStream(srcFile); FileOutputStream fos = new FileOutputStream(destFile); bis = new BufferedInputStream(fis); bos = new BufferedOutputStream(fos);
byte[] bytes = new byte[1024]; int len; while ((len = bis.read(bytes)) != -1) { bos.write(bytes, 0, len); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (bos != null) { bos.close(); } } catch (IOException e) { e.printStackTrace(); }
try { if (bis != null) { bis.close(); } } catch (IOException e) { e.printStackTrace(); } } }
@Test public void testCopyFile() { long start = System.currentTimeMillis(); String srcPath = "D:\\01-视频.mp4"; String destPath = "D:\\02-视频.mp4";
copyFileWithBuffered(srcPath, destPath); long end = System.currentTimeMillis(); System.out.println("复制操作花费时间为:" + (end - start));
}
|
运行代码后得出使用缓冲流复制相同视频所用时间为 450ms,而未使用缓冲流时,所消耗时间为 2085ms,效率得到了明显的提升(注意测试时将字节数组的大小设置成一样,控制变量嘛)。
那么为什么使用缓冲流后读写效率会得到提升呢?
主要是因为内部提供了一个 8kb 的缓冲区,数据会先写入缓冲区,当缓冲区数据满(达到某一条件)后,刷新缓冲区,将数据进行复写到目标文件中。
缓冲流内部还有一个 flush() 方法,这个方法用于刷新缓冲区。如果每次使用缓冲流写入后,手动调用这个方法(这个时候缓冲区还没达到刷新条件),会发现耗时又增加了(我的环境下耗时为 1431ms)。
5.3 文本文件的复制
对于字符流也有对应的缓冲流,我们可以测试一下(被复制文本最好是个大文本文件):
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
| @Test public void testBufferedReaderBufferedWriter() {
BufferedReader br = null; BufferedWriter bw = null;
try { br = new BufferedReader(new FileReader(new File("training-1000.txt"))); bw = new BufferedWriter(new FileWriter(new File("result.txt")));
String data; while ((data = br.readLine()) != null) {
bw.write(data); bw.newLine(); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (bw != null) { bw.close(); } } catch (IOException e) { e.printStackTrace(); }
try { if (br != null) { br.close(); } } catch (IOException e) { e.printStackTrace(); } } }
|
在上述代码的读写操作中,使用了两种方法:一种使用 char[] 数组,一种使用了 String 类。
使用 char[] 数组时,与使用 byte[] 一样,就不赘述了。
使用 String 类时需要注意实例化的字符串中并 不包含换行符 ,如果直接写入,将无法写入换行符,导致所有内容都在一行。解决方法也有两种:
- 在字符串末尾拼接
\n 再写入
- 依旧按照原方式写入,但是后续调用
readLine() 方法
5.4 技巧练习
图片加密
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
| @Test public void test1() {
FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream("妖梦.jpg"); fos = new FileOutputStream("妖梦-secret.jpg");
byte[] bytes = new byte[20]; int len; while ((len = fis.read(bytes)) != -1) { for (int i = 0; i < len; i++) { bytes[i] = (byte) (bytes[i] ^ 5); } fos.write(bytes, 0, len); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fos != null) { fos.close(); } } catch (IOException e) { e.printStackTrace(); }
try { if (fis != null) { fis.close(); } } catch (IOException e) { e.printStackTrace(); } } }
|
使用字节流传输文件时,可以将获取的字节与某个特定的数进行异或,这样就可以实现图片的简单加密。
我们知道一个数异或的异或就是原数据,因此解密时,只需要再进行异或就可以了。
字的统计
给定一个文本,统计该文本中每次字出现的次数。代码如下:
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 64 65
| @Test public void testWordCount() { FileReader fr = null; BufferedWriter bw = null;
HashMap<Character, Integer> map = new HashMap<>(); try { fr = new FileReader("training-1000.txt"); int c = 0; while ((c = fr.read()) != -1) { char ch = (char) c; if (map.get(ch) == null) { map.put(ch, 1); } else { map.put(ch, map.get(ch) + 1); } }
bw = new BufferedWriter(new FileWriter("wordCount.txt")); Set<Map.Entry<Character, Integer>> entrySet = map.entrySet();
for (Map.Entry<Character, Integer> entry : entrySet) { switch (entry.getKey()) { case ' ': bw.write("空格= " + entry.getValue()); break; case '\t': bw.write("tab键= " + entry.getValue()); break; case '\r': bw.write("回车= " + entry.getValue()); break; case '\n': bw.write("换行= " + entry.getValue()); break; default: bw.write(entry.getKey() + "=" + entry.getValue()); break; } bw.newLine(); } } catch (IOException e) { e.printStackTrace(); } finally { if (fr != null) { try { fr.close(); } catch (IOException e) { e.printStackTrace(); } }
if (bw != null) { try { bw.close(); } catch (IOException e) { e.printStackTrace(); } } } }
|
小技巧
使用 FileInputStream 、 FileOutputStream 、 FileReader 和 FileWriter 的构造方法时,可以直接传入路径,不用再书写 new File() 获取 File 对象。
6. 转换流
6.1 转换流的概述
转换流也是处理流的一种,转换流提供了在字节流和字符流之间的转换。
Java API 提供了两个转换流:
InputStreamReader :将 InputStream 转换为 Reader ,将一个字节的输入流转换成字符的输入流
OutputStreamWriter :将 Writer 转换为 OutputStream ,将一个字符的输出流转换成字节的输出流
字节流中的数据都是字符时,转换成字符流操作更加高效。
很多时候我们使用转换流来处理文件乱码问题,实现编码和解码的功能。
解码与编码
解码:字节、字节数组 ------> 字符数组、字符串 使用 InputStreamReader
编码:字符数组、字符串 ------> 字节、字节数组 使用 OutputStreamWriter
使用字节流读取一个文本文件,然后将字节流转换成字符流,最后将文本内容输出到控制台:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Test public void test1() throws IOException { FileInputStream fis = new FileInputStream("hello.txt"); InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8); char[] chars = new char[5]; int len; while ((len = isr.read(chars)) != -1) { String s = new String(chars, 0, len); System.out.println(s); }
isr.close(); fis.close(); }
|
上述代码中的异常处理仍应该使用 try-catch-finally 来处理,下述代码仅仅是为了简便,或者说偷懒。
6.3 综合使用
综合使用 InputStreamReader 和 OutputStreamWriter 实现文本文件的解码与编码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
@Test public void test2() throws IOException{ File file1 = new File("hello.txt"); File file2 = new File("hello-gbk.txt");
FileInputStream fis = new FileInputStream(file1); FileOutputStream fos = new FileOutputStream(file2);
InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8); OutputStreamWriter osw = new OutputStreamWriter(fos, "gbk");
char[] chars = new char[5]; int len; while ((len = isr.read(chars)) != -1){ osw.write(chars, 0, len); }
isr.close(); osw.close(); }
|
运行上述代码后,可以生成 helle-gbk.txt 文本文件,这个文本文件的编码是 gbk 的,如果在 IDEA 中直接打开,将会出现乱码,因为默认使用的 UTF-8 的编码打开的,所以会出现乱码。
当然如果使用 Nodepad3 、Edit Plus 打开就不会,因为这些软件可以自动匹配字符集。
6.4 字符集说明
编码表的由来
计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。而这就是编码表。
常见的编码表
ASCII:美国标准信息交换码。用一个字节的 7 位可以表示。
IS08859-1:拉丁码表、欧洲码表。用一个字节的 8 位表示。
GB2312:中国的中文编码表。最多两个字节编码所有字符。
GBK:中国的中文编码表升级,融合了更多的中文文字符号,最多两个字节编码。中文是两个字节,但是英文、数字是一个字节的。
Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码, 所有的文字都用两个字节来表示。
UTF-8:变长的编码方式,可用 1 - 4 个字节来表示一个字符。
编码的问题
Unicode 虽然融合了人类使用的所有字,但是它不完美,这里就有三个问题,一个是我们已经知道,英文字母只用一个字节表示就够了,第二个问题是如何才能区别 Unicode 和 ASCII ?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?第三个,如果和 GBK 等双字节编码方式一样,用最高位是 1 或 0 表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符。Unicode 在很长一段时间内无法推广,直到互联网的出现。
面向传输的众多UTF (UCS Transfer Format)标准出现了,顾名思义,UTF-8 就是每次 8 个位传输数据,而 UTF-16 就是每次 16 个位。这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。
Unicode 只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的 Unicode 编码是 UTF-8 和 UTF-16 。|
补充
ANSI 编码,通常指的是平台的默认编码,例如英文操作系统中是 ISO-8859-1,中文系统是 GBK。
Unicode 字符集只是定义了字符的集合和唯一编号,Unicode 编码这是对 UTF-8、UCS-2 / UTF-16 等具体编码方案的统称而已,并不是具体的编码方案。
参考视频:多种字符编码集的说明
7. 标准的输入、输出流
7.1 概述
System.in 和 System.out 分别代表了系统标准的输入和输出设备
默认输入设备是:键盘,输出设备是:显示器
System.in 的类型是 InputStream,标准的输入流,默认从键盘输入
System.out 的类型是 PrintStream,它是 OutputStream 的子类、FilterOutputStream 的子类,标准的输出流,默认从控制台输出
重定向:通过 System 类的 setln, setOut方法对默认设备进行改变。
7.2 具体使用
从键盘上输入字符串,要求将读取到的整行字符串转成大写输出,然后继续进行输入操作,直至输入 e 或 exit 时,退出程序:
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
| public static void main(String[] args) { BufferedReader br = null; try { InputStreamReader isr = new InputStreamReader(System.in); br = new BufferedReader(isr);
while (true) { System.out.println("请输入字符串:"); String data = br.readLine(); if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) { System.out.println("程序结束!"); break; }
String upperCase = data.toUpperCase(); System.out.println(upperCase); } } catch (IOException e) { e.printStackTrace(); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } }
|
8. 打印流
打印流概要
实现将基本数据类型的数据格式转化为字符串 输出 。打印流,打印流,肯定是用来打印输出的,因此没有输入。
打印流:PrintStream 和 PrintWriter
提供了一系列重载的 print() 和 println() 方法, 用于多种数据类型的输出
PrintStream 和 PrintWriter 的输出不会抛出 IOException 异常
PrintStream 和 PrintWriter 有自动 flush 功能
PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。
System.out 返回的是 PrintStream 的实例
具体使用
实现使用打印流将 ASCII 码对应的字符打印到指定文件中:
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
| @Test public void test1() { PrintStream ps = null; try { FileOutputStream fos = new FileOutputStream(new File("D:\\io\\text.txt")); ps = new PrintStream(fos, true); if (ps != null) { System.setOut(ps); } for (int i = 0; i < 255; i++) { System.out.print(((char) i)); if (i % 50 == 0) { System.out.println(); } } } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (ps != null) { ps.close(); } } }
|
9. 数据流
为了方便地操作 Java 语言的基本数据类型和 String 的数据, 可以使用数据流。
数据流有两个类:(用于读取和写出基本数据类型、String 类的数据)
DataInputStream 中的方法
boolean readBoolean()
byte readByte()
char readChar()
float readFloat()
double readDouble()
short readShort()
long readLong()
int readInt()
String readUTF()
void readFully(byte[] b)
DataOutputStream 中的方法
将上述的方法的 read 改为相应的 write 即可。
具体使用
将内存中的基本数据类型和字符串写入文件:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Test public void test2() throws IOException { DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt")); dos.writeUTF("默烦"); dos.flush(); dos.writeInt(20); dos.flush(); dos.writeBoolean(true); dos.flush();
dos.close(); }
|
将文件中存储的基本数据类型和字符串读取到内存中,保存到变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
@Test public void test3() throws IOException{ DataInputStream dis = new DataInputStream(new FileInputStream("data.txt"));
String name = dis.readUTF(); int age = dis.readInt(); boolean isMale = dis.readBoolean();
System.out.println("name = " + name); System.out.println("age = " + age); System.out.println("isMale = " + isMale);
dis.close(); }
|
10. 对象流
10.1 对象流的概述
对象流使用到的类是: ObjectInputStream 和 OjbectOutputSteam
对象流是用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把 Java 中的对象写入到数据源中,也能把对象从数据源中还原回来。这样就涉及到序列化和反序列化的概念:
序列化:用 ObjectOutputStream 类 保存 基本类型数据或对象的机制
反序列化:用 ObjectInputStream 类 读取 基木类型数据或对象的机制
注意: ObjectOutputStream 和 ObjectInputStream 不能序列化 static 和 transient 修饰的成员变量。
对象的序列化
对象序列化机制允许把内存中的 Java 对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象。
序列化的好处在于可将任何实现了 Serializable 接口的对象转化为字节数据 ,使其在保存和传输时可被还原.
序列化是RMI(Remote Method Invoke - 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础。
如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下 两个接口之一 ,否则,会抛出 NotSerializableException 异常:
-
Serializable (常用)
-
Externalizable
10.2 字符串的序列化
将某个字符串序列化写入文件 object.dat ,然后使用反序列化将文件 object.dat 中的数据输出到控制台:
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
| @Test public void testObjectOutputStream() { ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(new FileOutputStream("object.dat"));
oos.writeObject(new String("默烦")); oos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if (oos != null) { try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } } }
@Test public void testObjectInputStream() { ObjectInputStream ois = null; try { ois = new ObjectInputStream(new FileInputStream("object.dat"));
Object obj = ois.readObject(); String str = (String) obj; System.out.println(str); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } finally { if (ois != null) { try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } } }
|
运行上述代码后,当前 Module 下生成 object.dat 文件,控制台输出字符串 “默烦” 。
10.3 自定义类的序列化
使用对象流序列化对象
若某个类实现了 Serializable 接口,那么该类的对象就是可序列化的。序列化过程如下:
1、创建一个 ObjectOutputStream 对象
2、调用 ObjectOutputStream 对象的 writeObject(对象) 方法输出可序列化对象
3、注意写出一次,就要操作 flush() 一次
那么反序列化:
1、创建一个 ObjectInputStream 对象
2、调用 readObject() 方法读取流中的对象
强调: 如果某个类的属性不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的 Field 的类也不能序列化。
序列化要点
1、如果需要让某个对象支持序列化机制,那么这个对象所属的类必须实现 Serializable 接口,在这个接口中没有任何抽象方法,我们一般称这样的接口为 标识接口。
2、需要被序列化的类必须提供一个全局常量: serialVersionUID 。
3、除了保证需要被序列化的类需要实现 Serializable 接口之外,还必须保证其内部所有属性都必须是可序列化的。(默认情况下,基本数据类型都可序列化)
自定义类序列化示例
创建一个实体类 Person ,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public class Person implements Serializable {
private static final long serialVersionUID = 4216546544L;
private String name; private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
public Person() { }
}
|
在字符串序列化的代码基础上进行修改:
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
| @Test public void testObjectOutputStream() { ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(new FileOutputStream("object.dat"));
oos.writeObject(new String("默烦")); oos.flush();
oos.writeObject(new Person("默烦", 20)); oos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if (oos != null) { try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } } }
@Test public void testObjectInputStream() { ObjectInputStream ois = null; try { ois = new ObjectInputStream(new FileInputStream("object.dat"));
Object obj = ois.readObject(); String str = (String) obj;
Person person = (Person) ois.readObject(); System.out.println(str); System.out.println(person); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } finally { if (ois != null) { try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } } }
|
运行上述代码后,控制台输出:

需要注意的有:
1、每次序列化一个对象后,记得执行 flush() 操作
2、多个对象进行反序列化时,反序列化的顺序和序列化的顺序 一致 (注意是一致),否则会报错
10.4 serialVersionUID
凡是实现 Serializable 接口的类都有一个表示序列化版木标识符的静态变量:
1
| private static final long serialVersionUID;
|
serialVersionUID 用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容。
如果类没有显式定义这个静态变量,它的值是 Java 运行时环境根据类的内部细节自动生成的。 若类的实例变量做了修改,serialVersionUID 可能发生变化。 故建议,显式声明。
简单来说,Java 的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常 InvalidCastException。
10.5 序列化要点
1、如果需要让某个对象支持序列化机制,那么这个对象所属的类必须实现 Serializable 接口,在这个接口中没有任何抽象方法,我们一般称这样的接口为 标识接口。
2、需要被序列化的类必须提供一个全局常量: serialVersionUID 。
3、除了保证需要被序列化的类需要实现 Serializable 接口之外,还必须保证其内部所有属性都必须是可序列化的(默认情况下,基本数据类型都可序列化)。
4、 ObjectOutputStream 和 ObjectInputStream 不能序列化 static 和 transient 修饰的成员变量。
5、每次序列化一个对象后,记得执行 flush() 操作
6、多个对象进行反序列化时,反序列化的顺序和序列化的顺序 一致 (注意是一致),否则会报错
序列化更多相关内容可以查阅:Lambda 与序列化
11. 随机存取文件流
11.1 RandomAccessFile 类
RandomAccessFile 类的概述
RandomAccessFile 声明在 java.io 包下, 但直接继承于 java.lang.Object 类,并
且它实现了 Datalnput、DataOutput 这两 个接口,也就意味着这个类既可以读也可以写。
RandomAccessFile 类支持“随机访问”的方式,程序可以直接跳到文件的任意地方来读、写文件,比如:
-
支持只访问文件的部分内容
-
可以向已存在的文件后追加内容
RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。RandomAccessFile 类对象可以自由移动记录指针:
RandomAccessFile 构造器
1 2 3
| public RandomAccessFile(File file, String mode)
public RandomAccessFile(String name, String mode)
|
创建 RandomAccessFile 类实例需要指定一个 mode 参数,该参数指定 RandomAccessFile 的访问模式:
如果模式为只读 r,则不会创建文件,而是会去读取一个已经存在的文件,如果读取的文件不存在则会出现异常。如果 模式为 rw 读写,且文件不存在则会去创建文件,反之则不会创建。
RandomAccessFile 实现图片的复制示例
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
| @Test public void test1() { RandomAccessFile raf1 = null; RandomAccessFile raf2 = null; try { raf1 = new RandomAccessFile(new File("妖梦.jpg"), "r"); raf2 = new RandomAccessFile(new File("魂魄妖梦.jpg"), "rw");
byte[] bytes = new byte[1024]; int len; while ((len = raf1.read(bytes)) != -1) { raf2.write(bytes, 0, len); } } catch (IOException e) { e.printStackTrace(); } finally { if (raf1 != null) { try { raf1.close(); } catch (IOException e) { e.printStackTrace(); } }
if (raf2 != null) { try { raf2.close(); } catch (IOException e) { e.printStackTrace(); } } } }
|
RandomAccessFile 作为输出流示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
@Test
public void test2() throws IOException { RandomAccessFile raf1 = new RandomAccessFile("hello.txt", "rw");
raf1.write("mofan".getBytes());
raf1.close(); }
|
我们需要知道:
1、RandomAccessFile 作为输出流时,写出到的文件如果并不存在,在执行过程中会自动创建
2、如果写出到的文件存在,则会对原有文件进行覆盖(默认情况下,从头开始覆盖)
在当个方法中调用 write() 方法后会记录角标的位置,如果再次调用该方法,将会从记录的角标位置后进行写入。
11.2 实现数据的插入
实现指定位置的复写
在前文代码的基础上指定角标的位置:
1 2 3 4 5 6 7 8 9
| @Test public void test2() throws IOException { RandomAccessFile raf1 = new RandomAccessFile("hello.txt", "rw"); raf1.seek(3); raf1.write("xyz".getBytes());
raf1.close(); }
|
实现指定位置的插入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Test public void test3() throws IOException { RandomAccessFile raf1 = new RandomAccessFile("hello.txt", "rw"); raf1.seek(3);
StringBuffer builder = new StringBuffer((int) new File("hello.txt").length()); byte[] bytes = new byte[20]; int len; while ((len = raf1.read(bytes)) != -1) { builder.append(new String(bytes, 0, len)); }
raf1.seek(3); raf1.write("xyz".getBytes());
raf1.write(builder.toString().getBytes()); raf1.close(); }
|
RandomAccessFile 并没有提供插入方法,我们实现的插入其实就是:复制指定位置后的所有数据,在指定位置后写入数据,最后再将复制的数据写入文件。
那 RandomAccessFile 有什么用呢?
我们可以用 RandomAccessFile 这个类,来实现一个 多线程断点下载 的功能,用过下载工具的朋友们都知道,下 载前都会建立 两个临时文件 ,一个是与被下载文件大小相同的空文件,另一个是记录文件指针的位置文件,每次暂停的时候,都会保存上一次的指针,然后断点下载的时候,会继续从上一次的地方下载,从而实现断点下载或上传的功能,有兴趣的朋友们可以自己实现下。
12. NIO 的简单介绍
12.1 NIO.2
NIO 的简单介绍
Java NIO(New IO,Non-Blocking I0)是从 Java 1.4 版本开始引入的一套新的 IO API,可以替代标准的 Java IO API。NIO 与原来的 IO 有同样的作用和目的,但是使用的方式完全不同,NIO 支持面向缓冲区的(IO 是面向流的)、基于通道的 IO 操作。 NIO 将以更加高效的方式进行文件的读写操作。
Java API 中提供了两套 NIO,一套是针对标准输入输出 NIO,另一套就是网络编程NIO。
1 2 3 4 5
| ➢|-----java.nio.channels.Channel |-----FileChannel : 处理本地文件 |-----SocketChannel : TCP 网络编程的客户端的Channel |-----ServerSocketChannel : TCP 网絡编程的服务器端的 Channel |-----DatagramChannel : UDP 网络编程中发送端和接收端的 Channel
|
但是 JDK 1.4 中 NIO 的使用体验并不好,随着 JDK 7 的发布,Java 对 NIO 进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称它们为 NIO.2 。因为 NIO 提供的一-些功能,NIO 已经成为文件处理中越来越重要的部分。
NIO.2 中 Path、Paths 的使用
早期的 Java 只提供了一个 File 类来访问文件系统,但 File 类的功能比较有限,所提供的方法性能也不高。而且大多数方法在出错时仅返回失败,并不会提供异常信息。
NIO. 2 为了弥补这种不足,引入了 Path 接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置。Path 可以看成是 File 类的升级版本,实际引用的资源也可以不存在。
在以前IO操作都是这样写的:
1 2
| import java.io.File; File file = new File("index.html");
|
但在Java7中,我们可以这样写:
1 2 3
| import java.nio.file.Path; import java.nio.file.Paths; Path path = Paths.get("index.html");
|
同时,NIO.2 在 java.nio.file 包下 还提供了 Files、Paths 工具类,Files 包含了大量静态的工具方法来操作文件,Paths 则包含 了两个返回 Path的静态工厂方法。
Paths 类提供的静态 get() 方法用来获取 Path 对象:
1 2 3
| static Path get(String first, String ... more): 用于将多个字符串串连成路径
static Path get(URI uri): 返回指定 uri 对应的 Path 路径
|
NIO.2 中 Files 类的使用
java.nio.file.Files 用于操作文件或目录的上具类。
Files 常用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Path copy(Path src, Path dest, CopyOption ... how): 文件的复制 Path createDirectory(Path path, FileAttribute<?> ... attr): 创建一一个目录 Path createFile(Path path, FileAttribute<?> ... arr): 创建一一个文件 void delete(Path path): 删除一个文件/目录,如果不存在,执行报错 void deletelfExists(Path path): Path对应的文件/目录如果存在,执行删除 Path move(Path src, Path dest, CopyOption... how): 将src移动到dest位置 long size(Path path): 返回path指定文件的大小 Files常用方法: 用于判断 boolean exists(Path path, LinkOption ... opts): 判断文件是否存在 boolean isDirectory(Path path, LinkOption ... opts): 判断是否是目录 boolean isRegularFile(Path path, LinkOption ... opts): 判断是否是文件 boolean isHidden(Path path): 判断是否是隐藏文件 boolean isReadable(Path path): 判断文件是否可读 boolean isWritable(Path path): 判断文件是否可写 boolean notExists(Path path, LinkOption ... opts): 判断文件是否不存在 Files常用方法: 用于操作内容 SeekableByteChannel newByteChannel(Path path, OpenOption..how): 获取与指定文件的连接,how 指定打开方式。 DirectoryStream<Path> newDirectoryStream(Path path): 打开path指定的目录 InputStream newInputStream(Path path, OpenOption... how): 获取InputStream对象 OutputStream newOutputStream(Path path, OpenOpition... how) : 获取OutputStream对象
|
到此,Java IO 流基础就基本说完了,在实际开发过程中可能会书写这些 IO 流代码,但是大概率是不会使用的,经常使用的是已经封装好的第三方库,比如 Apache 下的 commons-io。
Java IO 流基础完