1. File
1.1 概述
java.io.File
:文件和文件目录路径的抽象表示形式,与平台无关File
能新建、删除、重命名文件和目录,但File
不能访问文件内容本身。 如果需要访问文件内容本身,则需要使用输入/输出流- 想要在 Java 程序中表示一个真实存在的文件或目录,那么必须有一个
File
对象;但是 Java 程序中的一个File
对象,也可能没有一个真实存在的文件或目录 File
对象可以作为参数传递给流的构造器
1.2 构造器
File(File parent, String child)
File(String pathname)
File(String parent, String child)
File(URI uri)
1.3 路径
- 路径表示方式
- 绝对路径:包含盘符在内的文件或文件目录
- 相对路径:相较于某个路径下,指明的路径
@Test
方法,是相对于当前 Modulemain
方法,是相对于当前 Project
- 路径分隔符
- 路径中的每级目录之间用一个 [路径分隔符] 隔开
- 路径分隔符和系统有关
- Windows 和 DOS系统 默认使用
来表示
- UNIX 和 URL 使用
/
来表示
- Windows 和 DOS系统 默认使用
- Java程序支持跨平台运行,因此 [路径分隔符] 要慎用。为了解决这个隐患,
File
类提供了一个常量:public static final String separator
← 根据操作系统,动态的提供分隔符
1.4 常用方法
- 获取功能
public String getAbsolutePath()
:获取绝对路径public String getPath()
:获取路径public String getName()
:获取名称public String getParent()
:获取上层文件目录路径。若无,返回null
public long length()
:获取文件长度(字节数)。不能获取目录的长度public long lastModified()
:获取最后一次的修改时间,毫秒值public String[] list()
:获取指定目录下的所有文件或者文件目录的名称数组public File[] listFiles()
:获取指定目录下的所有文件或者文件目录的File
数组
- 重命名功能
public boolean renameTo(File dest)
把文件重命名为指定的文件路径;如果dest
已存在,false
- 判断功能
public boolean isDirectory()
:判断是否是文件目录public boolean isFile()
:判断是否是文件public boolean exists()
:判断是否存在public boolean canRead()
:判断是否可读public boolean canWrite()
:判断是否可写public boolean isHidden()
:判断是否隐藏
- 创建功能
public boolean createNewFile()
:创建文件。若文件存在,则不创建,返回 falsepublic boolean mkdir()
:创建文件目录。如果此文件目录存在,就不创建了。 如果此文件目录的上层目录不存在,也不创建public boolean mkdirs()
:创建文件目录。如果上层文件目录不存在,一并创建
- 删除功能
public boolean delete()
:删除文件或者文件夹- Java中的删除不走回收站。 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录
File
类中涉及到关于文件或目录的创建、删除、重命名、修改时间、文件大小等方法,并未涉及到写入或读取文件内容的操作,如果需要读取或写入文件内容,必须使用 IO 流来完成。后续 File
类的对象常会作为参数传递到流的构造器中,指明读取或写入的"端点"。
2. 流原理及分类
- I/O 是
Input/Output
的缩写, I/O 技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。 - Java 程序中,对于数据的输入/输出操作以“流(stream)” 的方式进行。
java.io
包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。- 流的方向是相对的
- 输入 input:读取外部数据(磁 盘、光盘等存储设备的数据)到 程序(内存)中
- 输出 output:将程序(内存) 数据输出到磁盘、光盘等存储设备中。
- 可以从不同的角度对流类型(类/抽象类)进行分类
- 按操作数据单位不同
- 字节流(8 bit)
- 字符流(16 bit)
- 按数据流的流向不同
- 输入流
- 输出流
- 按流的角色的不同
![](_v_images/20200713145219974_27691.png =400x)- 节点流(原始流):直接从数据源或目的地读写数据
- 处理流(包裹流):不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。
- 按操作数据单位不同
3. IO流体系
- Java 的 IO 流共涉及 40 多个类,实际上非常规则,都是从如下 4 个抽象基类派生的。
- 由这 4 个类派生出来的子类名称都是以其父类名作为子类名后缀
4. 节点流的使用
4.1 使用说明
4.1.1 构造器
InputStream
和Reader
是所有输入流的基类FileInputStream
和FileReader
则是InputStream
和Reader
的典型实现public class FileInputStream extends InputStream
构造器public FileInputStream(String name) throws FileNotFoundException
public FileInputStream(File file) throws FileNotFoundException
public class FileReader extends InputStreamReader
构造器public FileReader(String fileName) throws FileNotFoundException
public FileReader(File file) throws FileNotFoundException
OutputStream
和Writer
也非常相似,是所有输出流的基类- 典型实现分别是:
FileOutputStream
和FileWriter
public class FileOutputStream extends OutputStream
构造器public FileOutputStream(String name) throws FileNotFoundException
public FileOutputStream(String name, boolean append) throws FileNotFoundException
public FileOutputStream(File file) throws FileNotFoundException
public FileOutputStream(File file, boolean append) throws FileNotFoundException
public class FileWriter extends OutputStreamWriter
构造器public FileWriter(String fileName) throws IOException
public FileWriter(String fileName, boolean append) throws IOException
public FileWriter(File file) throws IOException
public FileWriter(File file, boolean append) throws IOException
4.1.2 常用方法
4.1.3 Tips
- 在读取文件时,必须保证该文件已存在,否则报异常
- 在写入一个文件时,如果使用构造器
FileOutputStream(file)
,则目录下有同名文件将被覆盖。如果使用构造器FileOutputStream(file, true)
,则目录下的同名文件不会被覆盖, 会在文件内容末尾追加内容。 - 程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件 IO 资源
FileInputStream
从文件系统中的某个文件中获得输入字节。FileInputStream
用于读取非文本数据之类的原始字节流。要读取字符流,需要使用FileReader
FileOutputStream
从文件系统中的某个文件中获得输出字节。FileOutputStream
用于写出非文本数据之类的原始字节流。要写出字符流,需要使用FileWriter
- 字节流操作字节,比如:
.mp3 / .avi / .rmvb / .mp4 / .jpg / .doc / .ppt
- 字符流操作字符,只能操作普通文本文件。最常见的文本文件:
.txt / .java / .c / .cpp
等语言的源代码。尤其注意.doc / .excel / .ppt
这些不是文本文件
4.2 字节流和字符流的区别
- 字节流可以从所有格式的设备中读写数据,但字符流只能从文本格式的设备中读写数据
- 字节流可以完成所有格式文件的复制
- 字符流只能完成文本文件的复制,却无法完成视频、音频、图片等格式的文件的复制
- 因为字节是不需要解码和编码的,将字节转化为字符才存在解码和编码的问题
- 字节流在操作时本身不会用到缓冲区(内存),是文件本身直接操作的,而字符流在操作时使用了缓冲区,通过缓冲区再操作文件
- 提问:什么叫缓冲区?
- 缓冲区可以简单地理解为一段特殊的内存区域
- 某些情况下,如果一个程序频繁地操作一个资源(如文件或数据库),则性能会很低,此时为了提升性能,就可以将一部分数据暂时读入到内存的一块区域之中,以后直接从此区域中读取数据即可,因为读取内存速度会比较快,这样可以提升程序的性能
- 在字符流的操作中,所有的字符都是在内存中形成的,在输出前会将所有的内容暂时保存在内存之中,所以使用了缓冲区暂存数据
- 如果想在不关闭时也可以将字符流的内容全部输出,则可以使用
Writer
类中的flush()
完成
- 提问:使用字节流好还是字符流好?
- 先讲这样的一个概念,所有的文件在硬盘或在传输时都是以字节的方式进行的,包括图片等都是按字节的方式存储的,而字符是只有在内存中才会形成,所以在开发中,字节流使用较为广泛
- 字节流与字符流主要的区别是他们的的处理方式
字节流是最基本的,所有的 InputStream
和 OutputStream
的子类都是主要用在处理二进制数据,它是按字节来处理的。但实际中很多的数据是文本,又提出了字符流的概念,它是按虚拟机的 encode 来处理,也就是要进行字符集的转化。这两个之间通过 InputStreamReader
,OutputStreamWriter
来关联,实际上是通过 byte[]
和 String
来关联。
在实际开发中出现的汉字问题实际上都是在字符流和字节流之间转化不统一而造成的。在从字节流转化为字符流时,实际上就是 byte[] → String
时,public String(byte bytes[], String charsetName)
。
有一个关键的参数字符集编码,通常我们都省略了,那系统就用操作系统的 lang。而在字符流转化为字节流时,实际上是 String → byte[]
时,byte[] String.getBytes(String charsetName)
也是一样的道理。至于 java.io
中还出现了许多其他的流,按主要是为了提高性能和使用方便,如BufferedInputStream
,PipedInputStream
等。
4.3 为什么 FileReader 不能用来拷贝图片
截取:《Java语言程序设计(基础篇)》
FileReader
是输入字符流,拷贝文件没问题,但拷贝图片就有问题了。
假设是在 Windows 下,FileReader
用的是 GBK 码表,一个字符最多用2个字节代表。2 个字节就是 2 的 16 次方,即有 65536 个格子范围,但 GBK 码表并没有将这些格子都用完,当读到某个二进制,假设是12421(我这里用二进制的十进制说明,二进制写起来太长)对应有码值“中”,那就读到完整的 2 个字节,数据是完整的。
但如果是另一个数字 21232 没有对应字符(码值),FileReader
读到这样的数据对应码表,找不到对应的字符,就会返回一个未知字符所对应的数字,占1个字节(返回值就是测试代码中的 content)。既然字节大小读不完整,FileWriter 写的时候还能正确吗?数据就是这样丢失的。
我在说“中”的时候大家不要蒙圈,读图片为什么要谈到码表对应的汉字。汉字只是图片中二进制数据在码表上的对应字符,它可以是汉字以外的其它字符代表都可以,对于 GBK 码表没有用完的格子,FileReader
读到的 content 就不是真实的数据。
由此我们也能知道字节流为什么能读取完整,因为它不需要码表,读到啥就得到啥,不会因为码表上没有对应字符就丢弃。
打一个很好的比方,用 FileReader
读图片,就像用记事本打开图片,因为记事本一遇到二进制数据就拿码表来“翻译”,可码表并不是每个格子都用到,容易导致数据丢失。
4.4 测试代码
@Test
public void TestReaderAndWriter() {
// 1. File
File src = new File("src.txt");
File dest = new File("dest.txt");
// 2. FileReader, FileWriter
FileReader fr = null;
FileWriter fw = null;
try {
fr = new FileReader(src);
fw = new FileWriter(dest);
// 3. transfer
int len;
char[] cbuf = new char[1024];
while((len = fr.read(cbuf)) != -1)
fw.write(cbuf, 0, len);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4. close
if(fw != null)
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
if(fr != null)
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Test
public void copyByStream() {
// 1. File
File src = new File("src.png");
File dest = new File("dest.png");
// 2. FileReader, FileWriter
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(src);
fos = new FileOutputStream(dest);
// 3. transfer
int len;
byte[] cbuf = new byte[1024];
while((len = fis.read(cbuf)) != -1)
fos.write(cbuf, 0, len);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4. close
if(fos != null)
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
if(fis != null)
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5. 缓冲流
在我们学习字节流与字符流的时候,大家都进行过读取文件中数据的操作,读取数据量大的文件时,读取的速度会很慢,很影响我们程序的效率,那么,我想提高速度,怎么办?Java 中提高了一套缓冲流,它的存在,可提高 IO 流的读写速度。
因为缓冲区技术是为流技术存在的,所以建立缓冲区之前必须先有流对象。然后把流对象作为参数传给缓冲对象的构造器。注意,缓冲类是没有空参构造器的,它必须在流对象的前提下创建。
缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:
- 字节缓冲流
BufferedInputStream
— 字节输入缓冲流public BufferedInputStream(InputStream in)
public BufferedInputStream(InputStream in, int size)
BufferedOutputStream
— 字节输出缓冲流public BufferedOutputStream(OutputStream out)
public BufferedOutputStream(OutputStream out, int size)
- 字符缓冲流
BufferedReader
— 字符输入缓冲流public BufferedReader(Reader in)
public BufferedReader(Reader in, int sz)
BufferedWriter
— 字符输出缓冲流public BufferedWriter(Writer out)
public BufferedWriter(Writer out, int sz)
缓冲区有:BufferedOutputStream/BufferedWriter
写入流的缓冲区和 BufferedInputStream/BufferedReader
读取流的缓冲区,因为 Writer
流对象和 Reader
流对象操作数据时要读一份写一份,而缓冲区能够把每次读入的数据存着,写的时候一次写出去。所以他们能够提高效率,原因是它底层会创建一个内部缓冲区数组。
BufferedInputStream 部份源码:
public class BufferedInputStream extends FilterInputStream {
private static int DEFAULT_BUFFER_SIZE = 8192;
// The internal buffer array where the data is stored.
protected volatile byte buf[];
// This is the index of the next character to be read from the buf array.
protected int pos;
// The index one greater than the index of the last valid byte in the buffer.
protected int count;
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
}
缓冲区对象也一样可以操作 write()
,flush()
,close()
,并且当缓冲区 close()
时,它对应的流对象也会关闭。
特殊的是:BufferedWrite
缓冲区有 newLine()
,可以跨平台使用。Windows 的换行是
,而 Linux 是
,为了方便程序员写入统一的换行命令,Java 就封装了这个方法。相对应的,BufferReader
缓冲区有 readLine()
,可以读取一行的字符串,不含任何终止符。当读到文件末尾行以后,返回 null
,可以作为循环的控制条件。
测试代码1:
public void test() {
// 1. File
File srcFile = new File("src.png");
File destFile = new File("dest.png");
// 2. 节点流
FileInputStream fis = null;
FileOutputStream fos = null;
// 3. 处理流
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
// 4. transfer
byte[] buffer = new byte[1024];
int len;
while((len = bis.read(buffer)) != -1)
bos.write(buffer, 0, len);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 5. close
if(bos != null)
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
if(bis != null)
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
// fis.close(); // 处理流底层会去关闭节点流
// fos.close();
}
}
测试代码2:
public void test2() {
// 1. File
File srcFile = new File("BiTree.java");
File destFile = new File("二叉树.java");
// 2. 节点流
FileReader fr = null;
FileWriter fw = null;
// 3. 处理流
BufferedReader br = null;
BufferedWriter bw = null;
try {
fr = new FileReader(srcFile);
fw = new FileWriter(destFile);
br = new BufferedReader(fr);
bw = new BufferedWriter(fw);
// 4. transfer
/*
char[] cbuf = new char[1024];
int len;
while((len = br.read(cbuf)) != -1)
bw.write(cbuf, 0, len);
*/
String line;
while((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 5. close
if(br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bw != null) {
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
小结:
- 当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区
- 当使用
BufferedInputStream
读取字节文件时,BufferedInputStream
会一次性从文件中读取8192个字节(8KB)
,存在缓冲区中,直到缓冲区装满了,才重新从文件中读取下一个 8192 个字节数组 - 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,
BufferedOutputStream
才会把缓冲区中的数据一次性写到文件里。使用flush()
可以强制将缓冲区的内容全部写入输出流 - 关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也 会相应关闭内层节点流
flush()
的使用:手动将 buffer 中内容写入文件- 如果是带缓冲区的流对象的
close()
,不但会关闭流,还会在关闭流之前刷新缓冲区,关闭后不能再写出
6. 转换流
[转换流] 提供了在字节流和字符流之间的转换
6.1 说明
- 字节流中的数据都是字符时,转成字符流操作更高效
- 很多时候我们使用 [转换流] 来处理文件乱码问题。实现编码和解码的功能
- 编码:字符串 → 字节数组
- 解码:字节数组 → 字符串
- Java API 提供了两个转换流:
InputStreamReader
,OutputStreamWriter
public class InputStreamReader extends Reader
- 实现将字节的输入流按指定字符集转换为字符的输入流
- 需要和
InputStream
套接 - 构造器
public class OutputStreamWriter extends Writer
- 实现将字符的输出流按指定字符集转换为字节的输出流
- 需要和
OutputStream
套接 - 构造器
- 转换流的编码应用
- 可以将字符按指定编码格式存储
- 可以对文本数据按指定编码格式来解读
- 指定编码表的动作由构造器完成
6.2 测试代码
@Test
public void transfer() throws IOException {
File srcFile = new File("utf-8.txt");
File destFile = new File("gbk.txt");
FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(destFile);
// 具体选用哪个字符集读入,取决于 File 保存时使用的字符集
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
// 读入 UTF-8 编码的 File 将其copy一份再以 GBK 编码存储起来
OutputStreamWriter osw = new OutputStreamWriter(fos, "GBK");
char[] cbuf = new char[1024];
int len;
while((len = isr.read(cbuf)) != -1)
osw.write(cbuf, 0, len);
isr.close();
osw.close();
}
6.3 字符编码
6.3.1 编码表的由来
计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识 别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。 这就是编码表。
6.3.2 常见的编码表
- ASCII:美国标准信息交换码,用一个字节的 7 位可以表示
- ISO8859-1:拉丁码表。欧洲码表,用一个字节的 8 位表示
- GB2312:中国的中文编码表,最多两个字节编码所有字符
- GBK:中国的中文编码表升级,融合了更多的中文文字符号,最多两个字节编码
- Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示
- UTF-8:变长的编码方式,可用 1~4 个字节来表示一个字符
对于可变长度的编码集:当本字节最高位是0,说明这个字符就只用一个字节编码;如果字节最高位是1,说明这个字节和下一个字节合一起是一个字符的编码。
Unicode 不完美,这里就有三个问题,一个是,我们已经知道,英文字母只用 一个字节表示就够了,第二个问题是如何才能区别 Unicode 和 ASCII?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?第三个,如果和 GBK 等双字节编码方式一样,用最高位是 1 或 0 表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符。Unicode 在很长一段时间内无法推广,直到互联网的出现。
6.3.3 UTF-8
面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF-8 就是每次 8 个位传输数据,而 UTF-16 就是每次 16 个位。这是为传输而设计的 编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。
Unicode 只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯 一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的 Unicode 编码是 UTF-8 和 UTF-16。
在标准 UTF-8 编码中,超出基本多语言范畴(BMP) 的字符被编码为 4 字节格式,但是在修正的 UTF-8 编码中,他们由 [代理编码对(surrogatepairs)] 表示,然后这些代理编码对在序列中分别重新编码。结果标准 UTF-8 编码中需要 4 个字节的字符,在修正后的 UTF-8 编码中将需要 8 个字节。
7. 标准输入、输出流
- System 类字段摘要如下,其中
in
和out
分别代表了系统标准的输入和输出设备static PrintStream err
“标准”错误输出流static InputStream in
“标准”输入流;默认从键盘输入static PrintStream out
“标准”输出流;默认从控制台输出
- 重定向:通过 System 类的如下两个方法对默认设备进行改变
static void setIn(InputStream in)
重新分配“标准”输入流static void setOut(PrintStream out)
重新分配“标准”输出流
练习:从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续进行输入操作,直至当输入“e”或者“exit”时,退出程序
public static void main(String[] args) {
BufferedReader br = null;
try {
// System.in → 转换流 → BufferedReader
br = new BufferedReader(new InputStreamReader(System.in));
String line;
while(true) {
System.out.print("Enter String: ");
line = br.readLine();
if("e".equalsIgnoreCase(line) ||
"exit".equalsIgnoreCase(line)) break;
String upperCase = line.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
的实例
@Test
public void test() {
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream(new File("u:\text.txt"));
// 创建打印输出流,设置为自动刷新模式(写入换行符或字节'
'时都会刷新输出缓冲区)
ps = new PrintStream(fos, true);
if (ps != null) // 把标准输出流(控制台输出)改成文件
System.setOut(ps);
for (int i = 0; i <= 255; i++) {
// 输出ASCII字符
System.out.print((char) i);
if (i % 50 == 0) // 每50个数据一行
System.out.println(); // 换行
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (ps != null)
ps.close();
}
}