Java知识点07——输入/输出(File类、IO流、序列化、NIO)
- 一、File 类
- 1.1 概述
- 1.2 构造方法
- 1.3 访问文件和目录
- 1.3.1 访问文件名相关方法
- 1.3.2 文件检测相关方法
- 1.3.3 获取常规文件信息
- 1.3.4 文件操作相关方法
- 1.3.5 目录操作相关方法
- 1.2 文件过滤器
- 1.2.1 普通方法
- 1.2.2 匿名内部类方法
- 1.2.3 Lambda表达式优化
- 1.3 案例
- 1.3.1 递归打印多级目录
- 二、IO流
- 2.1 什么是IO
- 2.2 IO分类
- 1.2.1、输入流/输出流
- 1.2.2 字节流/处理流
- 1.2.3 IO的流向说明
- 1.2.4 流的概念模型
- 1.2.5 字节流 / 字符流
- 2.3 输入流(FileInputStream、FileReader)
- 2.3.1 FileInputStream(字节输入流)
- 2.3.2 FileReader(字符输入流)
- 2.4 输出流(FileOutputStream、FileWriter)
- 2.4.1 FileOutputStream(字节输出流)
- 2.4.2 PrintStream(字节打印流)
- 2.4.3 FileWriter(字符输出流)
- 2.4.4 PrintWriter(字符打印流)
- 2.5 输入输出流体系
- 2.5.1 处理流
- 2.5.2 输入输出流中常用的流分类
- 2.5.3 转换流
- 2.5.4 缓冲流
- 2.5.4.1 字节缓冲流
- 2.5.4.2 字符缓冲流
- 2.6 重定向标准输入/输出
- 2.7 案例
- 2.7.1 图片复制
- 2.7.2 关闭和刷新
- 三、序列化
- 3.1 序列化含义
- 3.2 使用对象流实现序列化
- 3.2.1 Serializable 接口
- 3.2.2 Externalizable 接口
- 3.3 对象引用的序列化
- 3.3.1 防止重复序列化
- 3.4 自定义序列化
- 四、NIO(New IO)
- 4.1 使用 Buffer(缓冲)
- 4.2 使用 Channel(通道)
- 4.3 字符集和 Charset
- 4.4 文件锁
- 4.5 Path、Paths、Files
- 4.6 FileVisitor 遍历文件和目录
- 4.7 WatchService监控文件变化
- 4.8 访问文件属性
一、File 类
1.1 概述
java.io.File
类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。
1.2 构造方法
-
public File(String pathname)
:通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。 -
public File(String parent, String child)
:从父路径名字符串和子路径名字符串创建新的 File实例。 -
public File(File parent, String child)
:从父抽象路径名和子路径名字符串创建新的 File实例。 -
public File(URI uri)
:通过将指定的file:URI转换为抽象路径名来创建新的File实例。
举例说明:
个人的身份证号就是URN,个人的家庭地址就是URL,URN可以唯一标识一个人,而URL可以告诉邮递员怎么把货送到你手里。
- 构造举例,代码如下:
import java.io.File;
import java.io.IOException;
public class FileDemo {
public static void main(String[] args) throws IOException {
//File(String pathname) 将指定路径名转换成一个File对象
File file = new File("D:\\1.txt");//只是创建该文件对象(并没有创建该文件)!!!
System.out.println(file);
file.createNewFile();//真正根据该对象创建文件(如果路径存在的话)(如果路径不存在则必须创建,否则报错)
//创建文件夹
File fileDir = new File("D:\\a");
//判断该file1文件对象是否存在,如果不存在就创建该对象
if (!fileDir.exists()){
System.out.println("该路经是否创建成功?"+fileDir.mkdir());
}
//File(String parent,String child) 根据指定的父路径和文件路径创建File对象
File file1 = new File("D:\\a","1.txt");
System.out.println(file1);
//D盘下没有a文件夹
//file1.createNewFile();//IOException: 系统找不到指定的路径。
file1.createNewFile();
//File(File parent,String child) 根据指定的父路径对象和文件路径创建File对象
File parent = new File("D:\\a");
File file2 = new File(parent, "1.txt");
System.out.println(file2);
File file3 = new File(new File("D:\\a"),"1.txt");
System.out.println(file3);
}
}
小贴士:
- 一个File对象代表硬盘中实际存在的一个文件或者目录。
- 无论该路径下是否存在文件或者目录,都不影响File对象的创建。
1.3 访问文件和目录
1.3.1 访问文件名相关方法
import java.io.File;
import java.io.IOException;
public class FileDemo01 {
public static void main(String[] args) throws IOException {
//创建文件夹
File fileDir = new File("D:\\a");
//判断该file1文件对象是否存在,如果不存在就创建该对象
if (!fileDir.exists()){
System.out.println("该路经是否创建成功?"+fileDir.mkdir());
}
//File(String parent,String child) 根据指定的父路径和文件路径创建File对象
File file1 = new File("D:\\a","1.txt");
System.out.println(file1);
file1.createNewFile();//创建该文件 1.txt //如果该路径不存在就直接创建会报错
System.out.println("file1.getName():"+file1.getName());
System.out.println("file1.getPath():"+file1.getPath());
System.out.println("file1.getAbsoluteFile():"+file1.getAbsoluteFile());
System.out.println("file1.getAbsolutePath():"+file1.getAbsolutePath());
System.out.println("file1.getParent():"+file1.getParent());
System.out.println("file1.renameTo():"+file1.renameTo(new File("D:\\a\\2.txt")));
}
}
1.3.2 文件检测相关方法
在UNIX/Linux/BSD等系统上,如果路径名开头是一条斜线(/),则说明File对象对应一个绝对路径;在Windows等系统上,如果路径开头是盘符,则说明它是一个绝对路径。
import java.io.File;
import java.io.IOException;
public class FileCheckDemo {
public static void main(String[] args) throws Exception {
isExists();
}
//Boolean exists();判断文件是否存在
public static void isExists() throws IOException {
new File("D:\\b").mkdir();
File file = new File("D:\\b\\a.txt");
file.createNewFile();
System.out.println("file:"+file);
System.out.println("对象对应的文件是否存在?"+file.exists());//true
System.out.println("file对象对应的文件是否可读?"+file.canRead());
System.out.println("file对象对应的文件是否可写?"+file.canWrite());
System.out.println("file对象对应的文件是否是文件?"+file.isFile());
System.out.println("file对象对应的文件是否是目录?"+file.isDirectory());
System.out.println("file对象对应的文件是否是绝对路径?"+file.isAbsolute());
}
}
1.3.3 获取常规文件信息
import java.io.File;
import java.io.IOException;
public class FileCheckDemo {
public static void main(String[] args) throws Exception {
isExists();
}
//Boolean exists();判断文件是否存在
public static void isExists() throws IOException {
new File("D:\\b").mkdir();
File file = new File("D:\\b\\a.txt");
file.createNewFile();
System.out.println("file对象对应的文件的最后修改时间:"+file.lastModified());
System.out.println("file对象对应的文件内容长度:"+file.length());
}
}
1.3.4 文件操作相关方法
import java.io.File;
import java.io.IOException;
public class FileOperationDemo {
public static void main(String[] args) throws IOException {
File file = new File("D:\\a\\fileDemo.txt");
//当file对象对应的文件不存在时创建该文件(创建成功返回true,否则返回false)
if (!file.exists()){
new File("D:\\a").mkdir();
}
file.createNewFile();
//删除file对象所对应的文件或路径
System.out.println("删除file对象对应的文件是否成功?"+file.delete());
//在指定的临时文件目录中创建一个临时空文件
File.createTempFile("cht",".tmp", new File("D:\\a"));
//当Java虚拟机退出时,删除file对象所对应的文件
file.deleteOnExit();
//相对路径创建文件
System.out.println("======相对路径创建File文件======");
File file2 = new File("fileRelativeTest.txt");
file2.createNewFile();
System.out.println("file2对象的绝对路径为:"+file2.getAbsolutePath());
}
}
createNewFile()
delete()
createTempFile(String prefix,String suffix,File director)
相对路径创建文件
1.3.5 目录操作相关方法
public boolean mkdirs()
:创建由此File表示的目录,包括任何必需但不存在的父目录。
import java.io.File;
public class FileDirectorOperation {
public static void main(String[] args) {
File file = new File("D:\\a\\b");
//如果file对应的目录不存在就创建该目录
System.out.println("file对象是否存在?"+file.exists());
if (!file.exists()){
file.mkdir();
}
File file2 = new File("D:\\a");
//列出所有file对象的所有文件名和目录名
System.out.println("======"+file2.getAbsolutePath()+"路径下对应的所有文件和目录======");
String[] fileList = file2.list();
for (String fileName:fileList){
System.out.println(fileName);
}
//列出系统所有的根目录
System.out.println("======系统的所有根目录======");
File[] roots = File.listRoots();
for (File root:roots){
System.out.println(root);
}
}
}
- 当使用相对路径的File对象来获取父路径时可能引起错误,因为该方法返回File对象所对应的目录名、文件名里最后一个子目录名、子文件名删除后的结果。
- API中说明:length(),表示文件的长度。但是File对象表示目录,则返回值未指定。
- API中说明:delete方法,如果此File表示目录,则目录必须为空才能删除。
1.2 文件过滤器
1.2.1 普通方法
import java.io.File;
public class SearchFile {
public static void main(String[] args) {
File dir = new File("D:\\a");
printFilesName(dir);
}
//打印
private static void printFilesName(File dir) {
//获取子文件和目录
File[] files = dir.listFiles();
//判断是否null,防止空指针异常
if (files != null){
for (File f:files){
if (f.isFile() && f.getName().endsWith(".tmp")){
System.out.println("文件名:"+f.getName()+" ,文件绝对路径:"+f.getAbsolutePath());
}else{
printFilesName(f);
}
}
}
}
}
1.2.2 匿名内部类方法
import java.io.File;
import java.io.FilenameFilter;
public class FilenameFilterTest {
public static void main(String[] args) {
File file = new File("D:\\a");
//数据源文件必须是要.tmp类型
File[] dataFiles = file.listFiles(new FilenameFilter() {
//以a开头,后缀名为.tmp的将被选出来,其余被过滤掉
@Override
public boolean accept(File dir, String name) {
String fileName = name.toLowerCase();
if (fileName.endsWith(".tmp")){
if (fileName.startsWith("a")){
return true;
}
}
return false;
}
});
//遍历File[]
for (File f:dataFiles){
System.out.println(f.getName());
}
}
}
1.2.3 Lambda表达式优化
import java.io.File;
import java.io.FilenameFilter;
public class FilenameFilterTest {
public static void main(String[] args) {
File file = new File("D:\\a");
//Lambda表达式优化
File[] files = file.listFiles(((dir, name) -> name.endsWith(".tmp")
&& name.startsWith("a")));
for (File f:files){
System.out.println(f.getName());
}
}
}
1.3 案例
1.3.1 递归打印多级目录
分析:多级目录的打印,就是当目录的嵌套。遍历之前,无从知道到底有多少级目录,所以我们还是要使用递归实现。
代码实现:
import java.io.File;
public class RecursionPrintDirectory {
public static void main(String[] args) {
//创建file对象
File dir = new File("D:\\a");
//调用打印目录方法
printDir(dir);
}
//打印目录的方法
private static void printDir(File dir) {
//获取子文件和目录
File[] files = dir.listFiles();
/**
* 判断:当是文件时,打印该文件的绝对路径
* 当是目录时,继续调用打印目录的方法,形成递归调用
*/
for (File f:files){
if (f.isFile()){
System.out.println("文件:"+f.getName()+" ,路径为:"+f.getAbsolutePath());
}else{
System.out.println("目录:"+f.getName()+" ,路径为:"+f.getAbsolutePath());
//继续遍历,调用printDir()形成递归
printDir(f);
}
}
}
}
二、IO流
2.1 什么是IO
Java中I/O操作主要是指使用java.io
包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据。
2.2 IO分类
1.2.1、输入流/输出流
输入流 | 输出流 | |
字节流 | InputStream | OutputStream |
字符流 | Reader | Writer |
根据数据的流向分为:输入流和输出流。
- 输入流 :把数据从
其他设备
上读取到内存
中的流。 - 输出流 :把数据从
内存
中写出到其他设备
上的流。
根据数据的类型分为:字节流和字符流。
- 字节流 :以字节为单位,读写数据的流。
InputStream
orOutputStream
- 字符流 :以字符为单位,读写数据的流。
Reader
orWriter
1.2.2 字节流/处理流
1.2.3 IO的流向说明
- 输入流/输出流 以内存为基准
1.2.4 流的概念模型
1.2.5 字节流 / 字符流
- 字节流和字符流操作方式几乎完全一样,区别只在于操作的数据单元不同
字节流操作的数据单元是8位的字节,字符流操作的数据单元是16位的字符。 - 一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。
2.3 输入流(FileInputStream、FileReader)
2.3.1 FileInputStream(字节输入流)
构造方法:
-
FileInputStream(File file)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。 -
FileInputStream(String name)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出
FileNotFoundException
常用方法:
-
int read()
:从输入流中读取单个字节,返回所读取的字节数据(字节数据可直接转换为int类型)。 -
int read(byte[] b)
:从输入流中最多读取 b.length 个字节的数据,并将其存储在字节数组 b 中,返回实际读取的字节数。 -
int read(byte[] b,int offset,int lenght)
:从输入流中最多读取 length 个字节的数据,并将其存储在字符数组 b 中,放入数组 b 中时,并不是从数组起点开始,而是从 offset 位置开始,返回实际读取的字节数。
直到read(byte[] b)方法返回-1,即表明到了输入流的结束点。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class FileInputStreamTest {
public static void main(String[] args) throws IOException {
//创建字节输入流
FileInputStream fis = new FileInputStream("D:\\a\\FileInputStreamTest.txt");
//创建一个长度为1024的“容器”(可以理解为盛放字节流的容器)
byte[] b = new byte[1024];//java中一个汉字占3个字节
//用于保存实际读取的字节数
int hasRead = 0;
//使用循环来重复“取水”过程
while( (hasRead = fis.read(b)) > 0 ){
//取出 “容器” 中的水滴(字节),将字节数组转换成字符串输入
System.out.println(new String(b,0,hasRead));
}
//关闭文件输入流,放在finally块里更安全
fis.close();
}
}
2.3.2 FileReader(字符输入流)
java.io.FileReader
类是读取字符文件的类。构造时使用系统默认的字符编码和默认字节缓冲区。
小贴士:
- 字符编码:字节与字符的对应规则。Windows系统的中文编码默认是GBK编码表。
idea中UTF-8- 字节缓冲区:一个字节数组,用来临时存储字节数据。
构造方法:
-
FileReader(File file)
: 创建一个新的 FileReader ,给定要读取的File对象。 -
FileReader(String fileName)
: 创建一个新的 FileReader ,给定要读取的文件的名称。
当你创建一个流对象时,必须传入一个文件路径。类似于FileInputStream 。
常用方法:
-
int read()
:从输入流中读取单个字符,返回所读取的字符数(字节数据可直接转换为int类型)。 -
int read(char[] c)
:从输入流中最多读取 c.length 个字符的数据,并将其存储在字符数组 c 中,返回实际读取的字符数。 -
int read(char[] c,int offset,int lenght)
:从输入流中最多读取 length 个字符的数据,并将其存储在字符数组 c 中,放入数组 c 中时,并不是从数组起点开始,而是从 offset 位置开始,返回实际读取的字符数。
直到read(char[] cbuf)方法返回-1,即表明到了输入流的结束点。
import java.io.FileReader;
import java.io.IOException;
public class FileReaderTest {
/**
* 程序里打开的文件IO资源不属于内存里的资源,
* 垃圾回收机制无法回收该资源,所以应该显式地关闭文件IO资源
* Java 7 改写了所有的IO资源类,它们都实现了AutoCloseable接口,
* 因此都可以通过自动关闭资源的try语句来关闭这些IO流。
*/
public static void main(String[] args) {
try(FileReader fr = new FileReader("D:\\a\\FileReader.txt")){
char[] c = new char[32];
int hasRead = 0;
while ((hasRead = fr.read(c)) > 0){
System.out.println(new String(c,0,hasRead));
}
}catch (IOException e){
e.printStackTrace();
}
}
}
2.4 输出流(FileOutputStream、FileWriter)
构造方法:
-
public FileOutputStream(File file, boolean append)
: 创建文件输出流以写入由指定的 File对象表示的文件。 -
public FileOutputStream(String name, boolean append)
: 创建文件输出流以指定的名称写入文件。
这两个构造方法,参数中都需要传入一个boolean类型的值,
true
表示追加数据,false
表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了。默认是false
-
FileWriter(File file)
: 创建一个新的 FileWriter,给定要读取的File对象。 -
FileWriter(String fileName)
: 创建一个新的 FileWriter,给定要读取的文件的名称。
当你创建一个流对象时,必须传入一个文件路径,类似于FileOutputStream。
常用方法:
-
void write(int c)
:将指定的字节/字符输入到输出流中,其中 c 既可以代表字节,也可以代表字符。 -
void write(byte[]/char[] buf)
:将字节数组/字符数组中的数据输出到指定输出流中。 -
void write(byte[]/char[] buf,int offset,int length)
:将字节数组/字符数组中从 offset 位置开始,长度为 length的字节/字符输出到输出流中。 -
void write(String str)
:将 str 字符串里包含的字符输出到指定输出流中。 -
void write(String str,int offset,int length)
:将 str 字符串里从 offset 位置开始,长度为 length 的字符输出到指定输出流中。 -
void flush()
:刷新该流的缓冲。 -
void close()
:关闭该流(会先自动刷新该流的缓冲,然后关闭)。
2.4.1 FileOutputStream(字节输出流)
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamTest {
public static void main(String[] args) {
try(
//创建字节输入流
FileInputStream fis = new FileInputStream("D:\\a\\FileOutputStream.txt");
//创建字节输出流
//当指定路径下的文件不存在时会自动创建
FileOutputStream fos = new FileOutputStream("D:\\a\\NewFileOutputStream.txt",true);
){
byte[] b = new byte[32];//相当于一个缓冲区
int hasRead = 0;
while ((hasRead = fis.read(b)) > 0){
//每读取一次,即写入文件输出流;读了多少,就写多少
fos.write(b,0,hasRead);
}
//前提要FileOutputStream(String name,boolean append)append的参数要为true
byte[] bAppendByte = "这里是追加的信息。。。".getBytes();
fos.write(bAppendByte);
}catch (IOException e){
e.printStackTrace();
}
}
}
2.4.2 PrintStream(字节打印流)
继承树
java.lang.Object
java.io.OutputStream
java.io.FilterOutputStream
java.io.PrintStream
*java.io.PrintStream:打印流
PrintStream为其他输出流添加了功能,使他们能够方便地打印各种数据值表示形式。
PrintStream特点:
1.只负责数据的输出,不负责数据的读取
2.与其他输出流不同,PrintStream永远不会抛出异常IOException
3.有特有的方法,print,println
void print(任意类型的值)
void println(任意类型的值并换行)
构造方法:
PrintStream(File file)
:输出的目的地是一个文件PrintStream(OutputStream out)
:输出的目的地是一个字节输出流PrintStream(String fileName)
:输出的目的地是一个文件路径
继承自父类的方法:
-
public void close()
: 关闭此输出流并释放与此流相关联的任何系统资源 -
public void flush()
: 刷新此输出流并强制任何缓冲流的输出字节被写入 -
public void write(byte[] b)
: 将 b.leng 字节从指定的字节数组写入此输出流 -
public void write(byte[] b,int off,int len)
: 从指定的字节数组写入len字节,从偏移量off开始输出到此输出流。 -
public abstract void write(int b)
: 将指定的字节写入此流。
注意:
如果使用继承自父类的write方法写数据,那么查看数据的时候会查询编码表 97–>a
如果使用自己特有的方法print/println方法写数据,写的数据原样输出 97–>97
import java.io.FileNotFoundException;
import java.io.PrintStream;
public class PrintStreamTest01 {
public static void main(String[] args) {
//创建打印流PrintStream对象,构造方法中绑定要输出的目的地
try (PrintStream ps = new PrintStream("D:\\a\\PrintStream.txt")){
//如果使用继承自父类的的方法write写数据,那么查看数据的时候会查询编码表97-->a
ps.write(97);
ps.println();//换行
//如果使用自己特有的方法print/println方法写数据,写的数据原样输出 97-->97
ps.println(97);
ps.println('a');
ps.println("hello");
ps.println(true);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
2.4.3 FileWriter(字符输出流)
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterTest {
public static void main(String[] args) {
try(FileWriter fw = new FileWriter("D:\\a\\FileWriter.txt")){
fw.write("锦瑟 - 李商隐\r\n");
fw.write("锦瑟无端五十弦,一弦一柱思华年\r\n");
fw.write("庄生晓梦迷蝴蝶,望帝春心托杜鹃\r\n");
fw.write("沧海月明珠有泪,蓝田日暖玉生烟\r\n");
fw.write("此情可待成追忆,只是当时已惘然\r\n");
}catch (IOException e){
e.printStackTrace();
}
}
}
- 回车符
\r
和换行符\n
:
- 回车符:回到一行的开头(return)。
- 换行符:下一行(newline)。
- 系统中的换行:
- Windows系统里,每行结尾是
回车+换行
,即\r\n
;- Unix系统里,每行结尾只有
换行
,即\n
;- Mac系统里,每行结尾是
回车
,即\r
。从 Mac OS X开始与Linux统一。小贴士:
使用数组读取,每次读取多个字节,减少了系统间的IO操作次数,从而提高了读写的效率,建议开发中使用。
2.4.4 PrintWriter(字符打印流)
继承树:
java.lang.Object
java.io.Writer
java.io.PrintWriter
构造函数:
-
PrintWriter(File file)
:使用指定的文件创建一个没有自动行刷新的新PrintWriter。 -
PrintWriter(File file, Charset charset)
: 使用指定的文件和字符集创建一个没有自动行刷新的新PrintWriter。
常用方法:
-
print(String str)
:向文件写入一个字符串。 -
print(char[] ch)
:向文件写入一个字符数组。 -
print(char c)
:向文件写入一个字符。 -
print(int i)
:向文件写入一个int型值。 -
print(long l)
:向文件写入一个long型值。 -
print(float f)
:向文件写入一个float型值。 -
print(double d)
:向文件写入一个double型值。 -
print(boolean b)
:向文件写入一个boolean型值。
例子跟PrintStream类似
2.5 输入输出流体系
2.5.1 处理流
功能:它可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入/输出方法,让程序员只需关心高级流的操作。
import java.io.*;
public class PrintStreamTest {
public static void main(String[] args) {
try(
//创建节点流(默认不能追加输出流中的信息 append 参数为false)
FileOutputStream fos = new FileOutputStream("D:\\a\\FileOutputStream.txt");
//创建处理流
PrintStream ps = new PrintStream(fos);//参数为一个节点流
){
//使用不能追加字节流的处理流执行输出 //会覆盖文件的内容!!!!!
ps.println("输出普通字符串"); //不是在控制台输出,而是写入到指定的文件中
//直接使用处理流输出对象
ps.println(new PrintStreamTest());//不是在控制台输出,而是写入到指定的文件中
}catch (IOException e) {
e.printStackTrace();
}
}
}
2.5.2 输入输出流中常用的流分类
注意:
- 规则:如果进行输入/输出的内容时文本内容,则应该考虑使用字符流;如果进行输入/输出的内容是二进制内容,则应该考虑使用字节流。
- 增加缓冲流功能后需要使用
flush()
才可以将缓冲区的内容写入实际的物理节点。
/**
* 用字符串作为物理节点的字符输入/输出流的用法
*/
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
public class StringNodeTest {
public static void main(String[] args) {
String src = "从现在开始,做一个上进的人\n喂马,劈柴,周游世界\n告诉他们im fine";
char[] buffer = new char[32];//创建一个字符数组,作为缓冲区
int hasRed = 0;
try(StringReader sr = new StringReader(src)){
//采用循环方式读取字符串存入到buffer字符数组中
while ((hasRed = sr.read(buffer)) > 0){//判断读取到的有效字节数
System.out.println(new String(buffer,0,hasRed));
}
} catch (IOException e) {
e.printStackTrace();
}
try(
//创建StringWriter时,实际上是以一个StringBuffer作为输出节点
//使用指定的初始字符串缓冲区大小
StringWriter sw = new StringWriter(20)
){
sw.write("这是一个美丽的地方\n");//实际是将str写入到指定的字符串缓冲区中
sw.write("湖北理工学院\n");
sw.write("慈湖湖畔\n");
System.out.println("-------下面是sw字符串节点里的内容-------");
System.out.println(sw.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.5.3 转换流
-
InputStreamReader
:将字节流输入流转换成字符输入流。 -
OutputStreamWtirer
:将字节输出流转换成字符输出流。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class KeyinTest {
public static void main(String[] args) {
try (
//将System.in对象转成Reader对象
InputStreamReader reader = new InputStreamReader(System.in);
//将普通的Reader包装成BufferedReader
BufferedReader br = new BufferedReader(reader);
){
String line = null;
//采用循环方法来逐行地读取
while((line = br.readLine()) != null){
//如果读取的字符串是 exit ,则程序退出
if (line.equals("exit")){
System.exit(1);
}
//打印读取的内容
System.out.println("输入内容为:"+line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.5.4 缓冲流
缓冲流,也叫高效流,是对4个基本的FileXxx
流的增强,所以也是4个流,按照数据类型分类:
- 字节缓冲流:
BufferedInputStream
,BufferedOutputStream
- 字符缓冲流:
BufferedReader
,BufferedWriter
缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。
2.5.4.1 字节缓冲流
构造方法:
-
public BufferedInputStream(InputStream in)
:创建一个 新的缓冲输入流。 -
public BufferedOutputStream(OutputStream out)
: 创建一个新的缓冲输出流。
构造举例,代码如下:
// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("bis.txt"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("bos.txt"));
效率测试:
查询API,缓冲流读写方法与基本的流是一致的
基本流,代码如下:
public class BufferedDemo {
public static void main(String[] args) throws FileNotFoundException {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
FileInputStream fis = new FileInputStream("jdk9.exe");
FileOutputStream fos = new FileOutputStream("copy.exe")
){
// 读写数据
int b;
while ((b = fis.read()) != -1) { //从此输入流中读取一个字节的数据。
fos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("普通流复制时间:"+(end - start)+" 毫秒");
}
}
十几分钟过去了...
- 缓冲流,代码如下:
public class BufferedDemo {
public static void main(String[] args) throws FileNotFoundException {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("jdk9.exe"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.exe"));
){
// 读写数据
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("缓冲流复制时间:"+(end - start)+" 毫秒");
}
}
缓冲流复制时间:8016 毫秒
如何更快呢?
使用数组
的方式,代码如下:
public class BufferedDemo {
public static void main(String[] args) throws FileNotFoundException {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("jdk9.exe"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.exe"));
){
// 读写数据
int len;//每次读取的字节长度
byte[] bytes = new byte[8*1024];//设置一个大的数组充当(缓冲区)
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0 , len);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("缓冲流使用数组复制时间:"+(end - start)+" 毫秒");
}
}
缓冲流使用数组复制时间:666 毫秒
2.5.4.2 字符缓冲流
构造方法:
-
public BufferedReader(Reader in)
:创建一个 新的缓冲输入流。 -
public BufferedWriter(Writer out)
: 创建一个新的缓冲输出流。
构造举例,代码如下:
// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));
特有方法:
字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。
- BufferedReader:
public String readLine()
: 读一行文字。 - BufferedWriter:
public void newLine()
: 写一行行分隔符,由系统属性定义符号。
readLine
方法演示,代码如下:
public class BufferedReaderDemo {
public static void main(String[] args) throws IOException {
// 创建流对象
BufferedReader br = new BufferedReader(new FileReader("in.txt"));
// 定义字符串,保存读取的一行文字
String line = null;
// 循环读取,读取到最后返回null
while ((line = br.readLine())!=null) {
System.out.print(line);
System.out.println("------");
}
// 释放资源
br.close();
}
}
newLine
方法演示,代码如下:
public class BufferedWriterDemo throws IOException {
public static void main(String[] args) throws IOException {
// 创建流对象
BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"));
// 写出数据
bw.write("我是");
// 写出换行
bw.newLine();
bw.write("程序");
bw.newLine();
bw.write("员");
bw.newLine();
// 释放资源
bw.close();
}
}
2.6 重定向标准输入/输出
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
/**
* 重定向标准输入流,将System.out的输入重定向到文件输出,而不是在屏幕上输出
*/
public class RedirectOut {
public static void main(String[] args) {
PrintStream ps = null;
try {
//一次性创建PrintStream输出流
ps = new PrintStream(new FileOutputStream("D:\\a\\out.txt"));
//将标准输出重定向到ps输出流
System.setOut(ps);
//向标准输出输出一个字符串
System.out.println("普通字符串");
//向标准输出输出一个对象
System.out.println(new RedirectOut());
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
if (ps != null){
ps.close();
}
}
}
}
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Scanner;
/**
* 重定向标准输入,从而将System.in重定向到指定文件,而不是键盘
*/
public class RedirectIn {
public static void main(String[] args) {
FileInputStream fis = null;
Scanner sc = null;
try {
fis = new FileInputStream("D:\\a\\in.txt");
//将标准输入流重定向到fis输入流
System.setIn(fis);
//使用System.in创建Scanner对象,用于获取标准输入
sc = new Scanner(System.in);
//只把回车作为分隔符
sc.useDelimiter("\n");
//判断是否还有下一个输入项
while (sc.hasNext()){
System.out.println("键盘输入的内容是:"+sc.next());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
if (fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (sc != null){
sc.close();
}
}
}
}
2.7 案例
2.7.1 图片复制
public class Copy {
public static void main(String[] args) throws IOException {
// 1.创建流对象
// 1.1 指定数据源
FileInputStream fis = new FileInputStream("D:\\test.jpg");
// 1.2 指定目的地
FileOutputStream fos = new FileOutputStream("test_copy.jpg");
// 2.读写数据
// 2.1 定义数组(相当于缓冲区,暂时存放读取的字节)
byte[] b = new byte[1024];
// 2.2 定义长度
int len;
// 2.3 循环读取
while ((len = fis.read(b))!=-1) {
// 2.4 写出数据
fos.write(b, 0 , len);
}
// 3.关闭资源
fos.close();
fis.close();
}
}
流的关闭原则:
先开后关,后开先关
。
- 虽然参数为int类型四个字节,但是只会保留一个字符的信息写出。
- 未调用close方法,数据只是保存到了缓冲区,并未写出到文件中。
2.7.2 关闭和刷新
因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush
方法了。
-
flush
:刷新缓冲区,流对象可以继续使用。 -
close
:先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。
代码使用演示:
public class FWWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter("fw.txt");
// 写出数据,通过flush
fw.write('刷'); // 写出第1个字符
fw.flush();
fw.write('新'); // 继续写出第2个字符,写出成功
fw.flush();
// 写出数据,通过close
fw.write('关'); // 写出第1个字符
fw.close();
fw.write('闭'); // 继续写出第2个字符,【报错】java.io.IOException: Stream closed
fw.close();
}
}
小贴士:即便是flush方法写出了数据,操作的最后还是要调用close方法,释放系统资源。
三、序列化
3.1 序列化含义
建议:程序创建的每个 JavaBean 类都实现 Serializable。
3.2 使用对象流实现序列化
3.2.1 Serializable 接口
使用Serializable来实现序列化非常简单,主要让目标类实现Serializable标记接口即可,无须实现任何方法。
一旦某个类实现了Serializable接口,该类的对象就是可序列化的,程序可以通过如下两个步骤来序列化该对象:
序列化:
1.创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其它节点流的基础之上。
//创建ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
2.调用ObjectOutputStream对象的writeObject()方法输出序列化对象。
//将一个Person对象输出到输出流中
oos.writeObject(per);
反序列化:
1.创建一个ObjectInputStream输入流,这个流是一个处理流,所以必须及案例在其它节点流的基础之上。
//创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
2.调用ObjectInputStream对象的readObject()方法读取流中的对象,该方法返回一个Object类型的Java对象,如果程序知道Java对象的类型,则可以将对象强制类型转换成其真实的类型。
//从输入流中读取一个Java对象,并将其强制类型转换为Person类
Person p = (Person)ois.readObject();
序列化案例:
import java.io.Serializable;
/**
* 实例化的Person bean类
*/
@Data
public class Person implements Serializable {//必须实现Serializable接口
private String name;
private int age;
}
/**
* 使用ObjectOutputStream将一个Person对象写入磁盘
*/
public class WriteObject {
public static void main(String[] args) {
//创建一个ObjectOutputStream输出流(放入可自动关闭io资源的try语句中)
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\a\\object.txt"))){
Person per = new Person("孙悟空",500);
//将per对象写入到输出流中
oos.writeObject(per);
} catch (IOException e) {
e.printStackTrace();
}
}
}
反序列化案例:
/**
* 使用ObjectInputStream将磁盘中序列化的二进制流反序列化为Person类的实例
*/
public class ReadObject {
public static void main(String[] args) {
//创建一个ObjectInputStream输入流
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\a\\object.txt"));){
//从输入流中读取一个Java对象,并将其强制类型转换为Person类
Person p = (Person) ois.readObject();
System.out.println("名字为:"+p.getName()+",年龄为:"+p.getAge());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
3.2.2 Externalizable 接口
待完善
3.3 对象引用的序列化
@Data
public class Teacher implements Serializable {
private String name;
private Person student;
}
3.3.1 防止重复序列化
- 所有保存到磁盘中的对象都有一个序列化编号。
- 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并输出。
- 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。
public class WriteTeacher {
public static void main(String[] args) {
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\a\\teacher.txt"))){
Person per = new Person("孙悟空",500);
Teacher t1 = new Teacher("唐僧",per);
Teacher t2 = new Teacher("菩提祖师",per);
//将对象写入输出流
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(per);
oos.writeObject(t2);//重复写只会存在第一次写的那个
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class ReadTeacher {
public static void main(String[] args) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\a\\teacher.txt"))){
//依次读取 ObjectInputStream 输入流中的4个对象
Teacher t1 = (Teacher) ois.readObject();
Teacher t2 = (Teacher) ois.readObject();
Person p = (Person) ois.readObject();
Teacher t3 = (Teacher) ois.readObject();
System.out.println("t1 的 student 引用和 p 是否相同:"+(t1.getStudent() == p));
System.out.println("t2 的 student 引用和 p 是否相同:"+(t2.getStudent() == p));
System.out.println("t2 的 t3 是否是同一个对象:"+(t2 == t3));
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
注意:
如果多次序列化同一个Java对象时,只有第一次序列化时才会把Java对象转换成字节序列输出。(后续如果更改了该对象的变量的值,重新调用 writeObject()方法时,程序只会输出前面的序列化编号,即更改的实例变量的值还是之前的值,不是更改之后的值)
3.4 自定义序列化
@Data
public class TransientPerson implements Serializable {
private String name;
private transient int age;
}
//测试类
public class TransientTest {
public static void main(String[] args) {
try (
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\a\\transient.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\a\\transient.txt"))
){
TransientPerson per = new TransientPerson("孙悟空",500);
oos.writeObject(per);
TransientPerson p = (TransientPerson) ois.readObject();
System.out.println(p.getName()+","+p.getAge());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
备注:
transient 关键字只能用于修饰实例变量,不可修饰Java程序中的其他成分。
四、NIO(New IO)
4.1 使用 Buffer(缓冲)
Buffer的主要作用是装入数据,然后输出数据;
得到一个XxxBuffer对象:
Buffer 读入数据后的示意图:
4.2 使用 Channel(通道)
获取 Channel :
@Test
void fileChannelTest(){
File file = new File("/Users/jpdev/Desktop/deploy.txt");
try {
//创建 FileInputStream,以该文件输入流创建 FileChannel
FileChannel inChannel = new FileInputStream(file).getChannel();
//以文件输入流创建 FileChannel,用于控制输出
FileChannel outChannel = new FileOutputStream("/Users/jpdev/Desktop/TemporaryFiles/a.txt").getChannel();
//将FileChannel里的全部数据映射成 ByteBuffer
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
//直接将buffer 里的数据全部输出 (文件复制)
outChannel.write(buffer);
//再次调用buffer的clear()方法,复原limit、position的位置
buffer.clear();
//使用 UTF-8 的字符集来创建解码器
Charset charset = Charset.forName("UTF-8");
//创建解码器
CharsetDecoder decoder = charset.newDecoder();
//使用解码器将ByteBuffer转换成CharBuffer
CharBuffer charBuffer = decoder.decode(buffer);
//CharBuffer 的 toString 方法可以获取对应的字符串
System.out.println(charBuffer);
} catch (IOException e) {
e.printStackTrace();
}
}
4.3 字符集和 Charset
常见中文相关编码:
4.4 文件锁
待完善
4.5 Path、Paths、Files
待完善
4.6 FileVisitor 遍历文件和目录
待完善
4.7 WatchService监控文件变化
待完善
4.8 访问文件属性
FileAttributeView
是其他 XxxAttributeView
的父接口。
public class AttributeViewTest {
public static void main(String[] args) throws IOException {
//以指定的路径来创建Path对象
Path path = Paths.get("D:\\a\\AttributeViewTest.txt");
System.out.println("path里包含的路径数量:"+path.getNameCount());
System.out.println("path的根路径:"+path.getRoot());
System.out.println("path的绝对路径:"+path.toAbsolutePath());
System.out.println("====== BasicFileAttributeView ======");
//获取访问基本属性的BasicFileAttributeView
BasicFileAttributeView basicView = Files.getFileAttributeView(path,BasicFileAttributeView.class);
//获取访问基本属性的BasicFileAttributes
BasicFileAttributes basicAttribs = basicView.readAttributes();
//访问文件基本属性
System.out.println("创建时间:"+new Date(basicAttribs.creationTime().toMillis()));
System.out.println("最后访问时间:"+new Date(basicAttribs.lastAccessTime().toMillis()));
System.out.println("最后修改时间:"+new Date(basicAttribs.lastModifiedTime().toMillis()));
System.out.println("文件大小:"+basicAttribs.size()+" bytes");
System.out.println("====== FileOwnerAttributeView ======");
//访问文件属性著信息的FileOwnerAttributeView
FileOwnerAttributeView ownerView = Files.getFileAttributeView(path,FileOwnerAttributeView.class);
//获取用户所属信息
System.out.println("该文件所属用户:"+ownerView.getOwner());
}
}