1. 概念
I/O:是
Intput/Output
的缩写,指的是某个设备或环境进行数据的输入和输出,java将输入输出问题(如:读写文件,网络传输等)抽象成流对象(Stream)来解决
Java环境<=>某个设备或环境
数据<=>文本、图片、音频、视频等
输入和输出<=>读取文件就是输入,写入文件就是输出
Java.io包下提供了各种流的类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出
Java中IO流使用了一种[装饰器设计模式](https://flowus.cn/squirtle/share/3cb86019-b83d-424d-aedc-03f7b8ed0bb9
【FlowUs 息流】装饰器模式),将IO流分成底层节点流和上层处理流,其中节点流用于和底层的物理存储节点直接关联,不同的物理节点获取节点流的方式可能存在一定的差异,但是程序可以把不同的物理节点流包装成统一的处理流,从而允许程序使用统一的输入、输出代码来读取不同的物理存储节点的资源
2. 流的分类
-
数据流的流向不同分类
输入流:读取外部数据(磁盘、光盘等存储设备)到程序(内存)中
输出流:把程序(内存)中的内容输出到外部设备(磁盘、光盘等存储设备)
-
按传输数据单位不同分类
字符流(8bit):字符流常用于处理文本文件
字节流(16bit):字节流常用于处理图片视频等
-
从功能分类
节点流:可以从一个特定的I/O设备读写数据的流,称为节点流(也被称为低级流)。从实际的数据源连直接连接到直接的输入/输出节点
处理流:是对已经存在的流进行连接或封装,通过封装后的流来实现数据读写功能,也称为高级流
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 | |
---|---|---|---|---|---|
抽象基类(不允许实例化) | InputStream | OutputStream | Reader | Writer | |
节点流 | 访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
节点流 | 访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
节点流 | 访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWirter |
节点流 | 访问字符串 | StringBufferInputStream | StringBufferOutputStream | StringReader | StringWriter |
处理流 | 缓冲流 | BufferedReader | BufferedWriter | ||
处理流 | 转换流 | InputStreamReader | OutputStreamWriter | ||
处理流 | 对象流 | ObjectInputStream | ObjectOutputStream | ||
处理流 | FilterIntputStream | FilterOutputStream | FilterReader | FilterWriter | |
处理流 | 打印流 | PrintStream | PrintWriter | ||
处理流 | 推回输入流 | PushbackInputStream | PushbackReader | ||
处理流 | 特殊流 | DataInputStream | DataOutputStream |
3. IO流常用类和方法
3.1 File类
File类可以使用文件路径字符串来创建File实例,该文件路径可以是绝对路径,也可以是相对路径,相对路径的解释是依据默认路径(user.dir),即用户工作路径
-
一个File对象是文件和目录的路径的抽象表现形式
-
路径分隔符
-
windows中使用的是
\\
-
Linux、Unix以及URL中使用的是
/
-
java也支持在windows中使用
/
如果为了避免出现跨平台问题,可以使用File类的一个常量public static final String separator
-
File file=new File("D:"+File.separator+"temp"+File.separator+"java.txt")//"D:\\temp\\java.txt"
3.1.1 构造器
File(String filePath)//文件路径
File(String parentPath,String childPath)//父路径拼接上子路径,表示完整指定的路径
File(File parentPath,String childPath)//父路径拼接子路径,表示完整指定路径--方便的一点就是可以将File对象直接作为父路径,减少开发量
使用示例:
File file1 = new File("E:/文档/temp/java.txt");
File file2 = new File("E:/文档/temp/","java.txt");
File file2 = new File(new File("E:/文档/temp/"),"java.txt");
3.1.2常用方法
-
访问文件名相关方法
* String getName():返回当前File对象所表示的文件名或路径名(最末级节点名称) * String getPath():返回此对象所对应的路径名 * File getAbsoluteFile():返回File对象的绝对路径 * String getAbsoluteFile():返回File对象所对应的绝对路径名 * String getParent():获取File对象所对应的目录的父目录 * boolean renameTo(File newName):重命名此File对象所对应的目录或文件,成功返回true,失败返回false
@Test public void findFile() { //并不会去访问文件的操作getName()和getPath() File file=new File("E:\\Documents");//这是一个目录 String name = file.getName();//Documents String path = file.getPath();//E:\Documents System.out.println(path); System.out.println(name);//如果是目录则返回当前最后一级目录名 File file1=new File("E:\\Documents\\IO流.xmind");//这是一个文件 String name1 = file1.getName();//IO流.xmind String path1 = file1.getPath();//E:\Documents\IO流.xmind System.out.println(path1); System.out.println(name1);//如果是目录则返回当前文件名 //getAbsoluteFile()会去访问该目录--如果访问不到则会拼接到默认路径后作为目录名存在 File file2=new File("222:\\Documents");//这是一个目录 File absoluteFile = file2.getAbsoluteFile();//D:\文档\Java_WorkSpace\JAVACore\222:\Documents //getAbsolutePath()--这个方法返回String绝对路径,不能进行File操作--可以使用new File(absolutePath,length) String absolutePath = file2.getAbsolutePath(); System.out.println(absolutePath);//D:\文档\Java_WorkSpace\JAVACore\222:\Documents System.out.println(absoluteFile);//D:\文档\Java_WorkSpace\JAVACore\222:\Documents //String getParent() String parent = file2.getParent(); System.out.println(parent);//222: //重命名 File file4=new File("E:\\tempBySRMYY\\");//这是一个目录 File file5=new File("E:\\tempBy\\"); boolean b = file4.renameTo(file5); System.out.println(b);//true修改成功 }
-
文件检测相关方法
boolean exists():判断File对象所对应的文件或目录是否存在 boolean canWrite():判断File对象所对应的文件和目录是否可写 boolean isFile():判断File对象所对应的文件和目录是否可读 boolean isDirectory():判断File对象所对应的是否是目录,而不是文件 boolean isAbsolute():判断File对象所应的文件或目录是否是绝对路径。该方法消除了不同平台的差异,可以直接判断File对象是否为绝对路径,在Unix/Linux/BSD等系统上,如果路径名开头是一条斜线(/),则表明该File对象对应一个绝对路径,在Windows系统上,如果路径开头是盘符,则说明是绝对路径
@Test public void detectFile(){ File directory = new File("E:\\文档\\temp"); File file = new File("E:\\文档\\temp\\java.txt"); //检测文件是否存在 System.out.println(file.exists());//true System.out.println(directory.exists());//true //检测文件是否可写 System.out.println(file.canWrite());//true System.out.println(directory.canWrite());//true //检测文件是否可读 System.out.println(file.canRead());//true System.out.println(directory.canRead());//true //检测是否是文件,是文件返回true,是其他的返回false System.out.println(file.isFile());//true System.out.println(directory.isFile());//false //检测是否是目录 System.out.println(file.isDirectory());//false System.out.println(directory.isDirectory());//true //检测是否是绝对路径--该方法不用考虑系统平台 System.out.println(file.isAbsolute());//true }
-
获取常规文件信息
long lastModified():返回文件的最后修改时间 long length():返回文件内容的长度
@Test public void getFileInfo(){ File directory = new File("E:\\文档\\temp"); File file = new File("E:/文档/temp/java.txt"); long l = file.lastModified(); Date date = new Date(l); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String format = simpleDateFormat.format(date); System.out.println(format);//2021-08-20 09:55:38 //文件内容长度--bit long length = file.length(); System.out.println(length);//98 3字节一个汉字 }
-
文件操作相关方法
boolean createNewFile():当此File对象所对应的文件不存在时,该方法将新建一个该File对象所指定的新文件,如果创建成功则返回true,否则返回false boolean delete():删除File对象所对应的文件或路径。 static File createTempFile(String prefix,String suffix):在默认的临时文件目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和定后缀作为文件名,这是一个静态方法,可以直接通过File类来调用。prefix参数必须至少是3字节长,suffix为空则表示默认为.tmp static File createTempFile(String prefix,String suffix,File directory):File directory用于指定创建文件的目录 void deleteOnExit():注册一个删除钩子,指定当java虚拟机退出时,删除File对象所对应的文件和目录
@Test public void fileOp() throws IOException { File directory = new File("E:\\文档\\temp"); File file = new File("E:/文档/temp/java.md"); //创建一个新文件--只能创建文件,不能创建目录 boolean newFile = file.createNewFile(); System.out.println(newFile);//true--返回true,表示创建成功,如果该文件存在,则返回true //删除文件或目录--true删除成功,false删除失败 boolean delete = file.delete(); System.out.println(delete);//true //创建一个临时文件 //prefix:文件名--这个文件名会拼接一个系统生成的随机数作为最终生成的文件名 //suffix:文件后缀--如果为null,默认后缀名为.tmp //directory:指定目录,在该目录下创建文件,如果不指定则在默认路径下 File md = File.createTempFile("xzt", ".md",new File("E:\\文档\\temp")); //注册删除的钩子 File directory1 = new File("E:\\文档\\temp\\test.md"); directory1.deleteOnExit(); System.exit(0); }
-
目录操作相关方法
boolean mkdir():试图创建一个File对象所对应的目录,如果创建成功,返回true,调用该方法时,File对象时File对象必须对应一个路径,而不是一个文件 String[] list():列出File对象的所有子文件名和目录名,返回一个String数组 File[] listFiles():列出File对象的所有子文件和路径,返回File数组 static File[] listRoot():列出系统所有的根路径,这是一个静态发给发,可以直接通过File类来调用
@Test public void directoryOp(){ File directory = new File("E:/文档/temp/"); File file = new File("E:/文档/temp/java.txt"); //mkdir创建File对象对应的目录--如果存在创建失败 //如果指定的路径是一个文件名,创建时也会当作目录来创建 //如果指定的路径是一个文件名,且文件名存在,目录也无法创建 System.out.println(directory.mkdir());//false System.out.println(file.mkdir());//true //获取当前目录下的所有文件-可以通过FilenameFilter参数添加文件过滤 String[] list = directory.list(); for (String s : list) { System.out.println(s); } //如果指定的路径是一个文件则会报错java.lang.NullPointerException // String[] list1 = file.list(); // for (String s : list1) { // System.out.println(s); // } System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); //获取当前目录下的所有文件和目录信息--绝对路径 File[] files = directory.listFiles(); for (File file1 : files) { System.out.println(file1); } System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); //根目录数 File[] roots = File.listRoots(); for (File file1 : roots) { System.out.println(file1); } }
3.1.3 File文件过滤器
@Test
public void filterFile(){
File directory = new File("E:/文档/temp/");
// File file = new File("E:/文档/temp/java.txt");
//list方法接收的是一个FilenameFilter接口参数,这个接口和FileFilter抽象累的功能非常相似,可以把FileFilter当成FilenameFilter的实现类--当成,不是真的实现类
//这个lambda实现的就是FilenameFilter接口中的accept方法,该方法将依次对指定的File的所有子目录或文件进行迭代,如果返回true则list就会列出该目录下的所有目录或文件
String[] list = directory.list((dir, name) -> name.endsWith(".md") || new File(name).isDirectory());
for (String s : list) {
System.out.println(s);
}
}
3.2 输入流和输出流
3.2.1 InputStream和Reader对比
int read();
int read(byte[] b)
int read(byte[] b,int off,int len);
int read();
int read(char[] cbuf);
int read(char[] cbuf,int off,int len);
void mark(int readAheadLimit):在记录指针当前位置记录一个标记mark
boolean markSupported():判断此输入流是否支持mark()操作,是否支持记录标
void reset():将此流的记录指针重定位到上一次记录标记mark的位置
long skip(long n):记录指针向前移动n个字节/字符
Input Stream特有的方法:
ps:Reader无特有方法
int available()
3.2.2 OutputStream和Writer对比
void write(int c);
void write(byte[] b);
void write(byte[] b,int off,int len);
void write(int c);
void write(char[] cbuf);
void write(char[] cbuf,int off,int len);
void write(String str);
void write(String,int off,int len);
void close()
void flush()
Writer特有的方法:
ps:OutputStream无特有方法
Writer append(char c)
Writer append(CharSequence csq)
Writer append(CharSequence csq,int start,int end)
3.3字节流与字符流
字符流内部实际上是使用了转换流将字节流的实例对象进行转换后的结果
-
字节流:主要用于对图片、视频等非文本文件进行操作
-
字符流:主要用于对.txt、.java、.cpp等文本文件进行操作
字节流与字符流的使用:
- 创建一个文件对象–用于指定读取或写入的文件
- 创建流对象–字节流/字符流–这里读取或写入的是文件,就需要使用访问文件的字节流/字符流
- 输入流调用read()、输出流调用write()
- 关闭流连接
字符流实现将内容读取到本地文件
//1.实例化File对象--指定需要读取的文件
File file = new File("E:\\文档\\temp\\java.txt");
//方式一:read()
//2.将指定的文件传给FileReader对象
//FileReader fr=null;
//try {
//fr=new FileReader(file);
//3.调用read()方法读取文件--如果读取到文件末尾返回-1
//int data = fr.read();
//while (data != -1) {
//不要自己使用换行符,读取文件时会读取换行符的
// System.out.print((char)data);
// data=fr.read();
//}
//} catch (IOException e) {
//try {
//throw new IOException("文件读取异常");
//} catch (IOException ioException) {
//ioException.printStackTrace();
//}
//} finally {
//4.一定要关闭流--垃圾回收机制不能清除物理连接--流,数据库连接和网络连接
//if(fr != null) {
//try {
//fr.close();
//} catch (IOException e) {
//e.printStackTrace();
//}
//}
//}
//方式二:read(char[] c)
FileReader fileReader =null;
try {
fileReader = new FileReader(file);
char[] cbuf=new char[8];
int len = 0;
while((len=fileReader.read(cbuf))!=-1){//read(char[] cbuf)返回值是读取字符的个数
//方式一的正确写法和错误写法:
//错误方式:使用char数组时,是将读取的字符依次覆盖到char数组中,未覆盖部分会保留上一次读取的内容,因此执行结果为:
//如果你是一个孩子,一定要学会坚强
//如果你是一个大人,一定要学会承担,一定要学会
// for (int i = 0; i < cbuf.length; i++) {
// System.out.print(cbuf[i]);
// }
//如果你是一个孩子,一定要学会坚强
//如果你是一个大人,一定要学会承担
// for (int i = 0; i < len; i++) {
// System.out.print(cbuf[i]);
// }
//方式二的正确写法和错误写法
//错误方式:与上同理,使用覆盖的方式,未覆盖部分保留了上一次读取的字符
//如果你是一个孩子,一定要学会坚强
//如果你是一个大人,一定要学会承担,一定要学会
// String str=new String(cbuf);
// System.out.print(str);
//正确方式:采用String构造器,设置每次转换为String的偏移量
String str = new String(cbuf,0,len);
System.out.print(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileReader != null) {
try {
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
字符流实现将内容写入到本地文件
注意:在实现内容写入的时候,如果不关闭流,不会默认调用flush()方法,数据也就无法输出到文件中–所以要么关闭流,要么flush(),但是这种情况,关闭流一定要执行,否则容易造成资源浪费,内存泄漏。
/**
* @Description:字符流写入操作
* 注意事项:
* 1.写入流一定要关闭后,文件写入才算生效,否则不生效
* 2.被写入的文件可以不存在,如果不存在则自动创建一个文件
* 3.如果调用FileWriter(File file)构造器,则默认append为false,即使用覆盖文件的方式进行写入操作
* 如果调用FileWriter(File file,boolean append)构造器,true代表向文件末尾追加内容,false代表使用文件覆盖的方式进行写入操作
*/
//1.创建一个File对象--用于指定需要写入的文件
File file=new File("E:/文档/temp/java1.md");
//2.将此文件对象File放入FileWriter中
FileWriter fw=new FileWriter(file,true);
//调用write()方法写入数据
fw.write("## IO流");
//4.关闭写入流
fw.close();
字符流实现文件复制
File readFile=new File("E:/文档/temp/java.txt");
File writeFile=new File("E:/文档/temp/java1.md");
FileReader fr= null;
FileWriter fw= null;
try {
fr = new FileReader(readFile);
fw = new FileWriter(writeFile,true);
char[] cbuf=new char[5];
int len = 0;
//方式一:推荐这种方式,减少对文件的频繁操作
while((len=fr.read(cbuf))!=-1){
String str=new String(cbuf,0,len);
System.out.print(str);
fw.write(cbuf,0,len);
}
//方式二:这种方式会频繁对文件进行操作
// while((len=fr.read(cbuf))!=-1){
// for (int i = 0; i < len; i++) {
// fw.write(cbuf[i]);
fw.flush();
// }
// }
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
字节流实现文件的读取
FileInputStream fis= null;
try {
File srcPath = new File("E:/Documents/集合框架.png");
fis = new FileInputStream(srcPath);
byte[] bbufe=new byte[1024];
int len = 0;
while((len=fis.read(bbufe))!=-1){
for(int i=0;i<len;i++){
System.out.println((char)bbufe[i]);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
字节流是实现文件的写入
FileOutputStream fos= null;
try {
File destPath = new File("E:/Documents/java.txt");
fos = new FileOutputStream(destPath);
byte[] b=new byte[]{'a','b','c'};
fos.write(b);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
字节流实现文件的复制
FileInputStream fis= null;
FileOutputStream fos= null;
try {
File srcPath = new File("E:/Documents/集合框架.png");
File destPath = new File("E:/Documents/集合框架2.png");
fis = new FileInputStream(srcPath);
fos = new FileOutputStream(destPath);
byte[] bbufe=new byte[1024];
int len = 0;
while((len=fis.read(bbufe))!=-1){
fos.write(bbufe,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
3.4.缓冲流
缓冲流:是为了提高数据的读写速度而产生的,JavaAPI提供了带缓冲功能的流类,在使用这些流类时,会创建一个内部的缓冲区数组,使用8192个字节的缓冲区
private static int DEFAULT_BUFFER_SIZE = 8192;
缓冲流需要套在相应的节点流上
-
BufferedInputStream和BufferedOutputStream
-
BufferedReader和BufferedWriter
当读取数据时,会一次性读取8192个(8KB),存在缓冲区中,直到缓冲区存满了,才会重新从文件中读取下一个块,可以使用flush()方法将未存满的缓冲区内容强制写入输出流
在关闭流的过程中,只需要关闭缓冲流的流连接,节点流的连接也会自动关闭,不需要再次关闭–当然关闭也不错
long start = System.currentTimeMillis();
BufferedInputStream bis=null;
BufferedOutputStream bos=null;
try {
//创建文件对象
File srcPath = new File("D:/BaiduNetdiskDownload/1.mp4");
File destPath = new File("D:/BaiduNetdiskDownload/2.mp4");
//创建读取和写入的流对象
FileInputStream fis = new FileInputStream(srcPath);
FileOutputStream fos = new FileOutputStream(destPath);
//创建处理流对象
bis=new BufferedInputStream(fis);
bos=new BufferedOutputStream(fos);
byte[] bbufe=new byte[1024];
int len = 0;
while((len=bis.read(bbufe))!=-1){
bos.write(bbufe,0,len);
}
System.out.println("文件传输成功");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println("文件传输速度:"+(end - start));//1849--约快了3~4倍
3.5.转换流
转换流用于在字节流和字符流之间相互转换
InputStreamReader:将InputStream转换为Reader
OutputSreamWriter:将OutputSream转换为Writer
字节流中的数据都是字符时,转成字符流操作更高效
使用转换流的目的是为了处理文件乱码,实现编码和解码的功能
常见的编码表
ASCII:美国标准信息交换码。用一个字节的7位可以表示。
ISO8859-1:拉丁码表。欧洲码表。用一个字节的8位表示。
GB2312:中国的中文编码表。最多两个字节编码所有字符
GBK(ANSI):中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。
UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。
BIG5:繁体中文字符集
字节流输入流转换为字符流输入流
//将InputStream转换为Reader
File file = new File("E:\\Documents\\java.txt");
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis,"UTF-8");//默认使用IDEA默认编码
char[] cbuf=new char[20];//FileInputStream原本时需要用byte数组承接的,但是转换流使用后只能用char承接,因为read方法没有byte[]参数
int len=0;
while((len=isr.read(cbuf)) != -1){
for (int i = 0; i < len; i++) {
System.out.print(cbuf[i]);
}
}
isr.close();
字节流输出流转换为字符流输出流
File file = new File("E:\\Documents\\java.txt");
FileOutputStream fos =new FileOutputStream(file);
OutputStreamWriter osw=new OutputStreamWriter(fos,"UTF-8");
osw.write("中国加油!");
osw.close();
转换流实现复制文件
public void conversionFile() throws IOException {
File src = new File("E:\\Documents\\java.txt");
File dest = new File("E:\\Documents\\java1.txt");
FileInputStream fis=new FileInputStream(src);
FileOutputStream fos=new FileOutputStream(dest);
InputStreamReader isr=new InputStreamReader(fis,"UTF-8");
OutputStreamWriter osw=new OutputStreamWriter(fos,"GBK");
char[] cbuf=new char[20];
int len=0;
while((len=isr.read(cbuf))!=-1){
osw.write(cbuf,0,len);
osw.flush();
}
isr.close();
osw.close();
}
3.6其他流
-
标准输入输出流
System.in\System.out\System.errInputStreamReader inputStreamReader = new InputStreamReader(System.in); BufferedReader br=new BufferedReader(inputStreamReader); while(true){ System.out.println("请输入:"); String data=br.readLine(); if ("e".equalsIgnoreCase(data)||"exit".equalsIgnoreCase(data)) { System.out.println("程序结束"); break; } String s = data.toUpperCase(); System.out.println(s); } br.close(); }
-
数据流
DataOutputStream dos=new DataOutputStream(new FileOutputStream("data.txt")); dos.writeUTF("李白"); dos.flush(); dos.writeInt(15); dos.flush(); dos.writeBoolean(true); dos.flush(); dos.close(); DataInputStream dis=new DataInputStream(new FileInputStream("data.txt")); //java.io.EOFException // boolean b = dis.readBoolean(); // int i = dis.readInt(); // String s = dis.readUTF(); //李白3841false // String s = dis.readUTF(); // boolean b = dis.readBoolean(); // int i = dis.readInt(); String s = dis.readUTF(); int i = dis.readInt(); boolean b = dis.readBoolean(); System.out.println(s+i+b); dis.close();
-
对象流
ObjectInputStream和ObjectOutputStream
作用:将Java中的对象写入到数据源中或把对象从数据源中还原出来
序列化:使用ObjectOutputStream类保存基本类型数据或对象的机制叫序列化
反序列化:
使用ObjectInputStream类读取基本类型数据或对象的机制叫反序列化
注意:ObjectOutputStream和ObjectInputStream不能序列化static和stransient修饰的成员变量
对象序列化:对象的序列化机制允许把内存中的java对象转换成平台无关的二进制流,支持保存到磁盘或传输到其他的网络节点,再通过程序获取二进制流就可以恢复原来的java对象
为了让类实现可序列化,该类必须实现以下两个接口其中的一个
-
Serializable
- 该接口是一个标记接口,无需实现任何方法,只是标注该类的实例为可序列化的
-
Externalizable
- 该接口提供了两个方法,由程序员确定存储和恢复的对象数据
@Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(new StringBuffer(name).reverse()); out.writeInt(age); out.writeObject(p); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { this.name=((StringBuffer)in.readObject()).reverse().toString(); this.age=in.readInt(); this.p=(Person) in.readObject(); }
- 该接口提供了两个方法,由程序员确定存储和恢复的对象数据
/** * @Description: 对象流--主要是对对象的序列化处理--将对象写入磁盘文件,然后读取文件中的对象信息 * @Param: * @return: void */ @Test public void objectStreamTest(){ ObjectOutputStream oos = null; try { //1.创建文件对象--需要将对象写入哪个文件 File file = new File("E:\\Documents\\java.txt"); //2.创建节点流--输出流 FileOutputStream fos=new FileOutputStream(file); //3.创建处理流-对象输出流 oos = new ObjectOutputStream(fos); //4.创建需要写入的类对象 Person p=new Person("李白",25); //5.写入到文件 oos.writeObject(p);//当文件未序列化时报错:com.carl.javaadvanced.ioclass.Person } catch (IOException e) { e.printStackTrace(); } finally { try { //关闭流 oos.close(); } catch (IOException e) { e.printStackTrace(); } } //获取文件中的对象 ObjectInputStream ois= null; try { //1.创建文件对象--需要从哪个文件读取对象 File dest=new File("E:\\Documents\\java.txt"); //2.创建节点流--输入流 FileInputStream fis=new FileInputStream(dest); //3.创建处理流--对象输入流 ois = new ObjectInputStream(fis); //4.将获取的对象信息使用类对象承接--这里会失去对象记忆,一并认为是Object Person p=(Person) ois.readObject(); System.out.println(p.toString()); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { try { //关闭流 ois.close(); } catch (IOException e) { e.printStackTrace(); } } }
/** * @Description: 对象流--当一个类中包含另一个类的引用的序列化问题测试 * 1.如果该类包含其他可序列化对象的引用,自己是不可序列化对象,是不支持序列化到文件中的 * 2.如果该类包含其他不可序列化对象的引用,即使自己是可序列化的对象,也是不支持序列化的 * 3.下述例子中Teachar和Person都必须是可序列化对象,才能满足写入磁盘文件或网络传输的需求--因此在开发中所有的javaBean都需要实现Serializable接口 * @Param: * @return: void */ @Test public void objectStreamTest2(){ ObjectOutputStream oos = null; try { //1.创建文件对象--需要将对象写入哪个文件 File file = new File("E:\\Documents\\java.txt"); //2.创建节点流--输出流 FileOutputStream fos=new FileOutputStream(file); //3.创建处理流-对象输出流 oos = new ObjectOutputStream(fos); //4.创建需要写入的类对象 Teacher t=new Teacher("李白",25,new Person("老师",30)); //5.写入到文件 oos.writeObject(t);//当文件未序列化时报错:com.carl.javaadvanced.ioclass.Person } catch (IOException e) { e.printStackTrace(); } finally { try { //关闭流 oos.close(); } catch (IOException e) { e.printStackTrace(); } } //获取文件中的对象 ObjectInputStream ois= null; try { //1.创建文件对象--需要从哪个文件读取对象 File dest=new File("E:\\Documents\\java.txt"); //2.创建节点流--输入流 FileInputStream fis=new FileInputStream(dest); //3.创建处理流--对象输入流 ois = new ObjectInputStream(fis); //4.将获取的对象信息使用类对象承接--这里会失去对象记忆,一并认为是Object Teacher t=(Teacher) ois.readObject(); System.out.println(t.toString()); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { try { //关闭流 ois.close(); } catch (IOException e) { e.printStackTrace(); } } }
/** * @Description: 对象流--当一个类创建两个对象都使用同一个其他对象问题测试 * 1.Java序列化机制--序列化算法 * 所有保存到磁盘中的对象都有一个序列化编号 * 当程序试图序列化一个对象时,程序会先检查该对象是否已经被序列化,当该对象从未被序列化,系统才会将该对象转换成字节序列并输出 * 如果某对象已经被序列化,程序将直接输出一个序列化编号,而不是再次重新序列化该对象 * 2.该结果表示: * 序列化机制会在准备序列化时检测文件中是否已经存在该对象的序列化,如果存在则不会再次序列化 * 两次序列化的地址值一致,表示并没有重新序列化 * 当文件中已经存在该对象的序列化编号,修改该对象的成员变量值并不会引起序列化文件内容发生改变,依旧是反馈编号(只有在初始调用write()方法序列化时会发生对象转字节存储,后面再修改成员对象信息的操作都是无效操作,再次使用write()方法会记录一个访问顺序,便于在读取的时候按访问顺序读取) * 对象一旦被序列化,即序列化编号一旦确定,则不能修改后重新写到相同的文件中进行覆盖,报错: * java.io.InvalidClassException: com.carl.javaadvanced.ioclass.Person; local class incompatible: stream classdesc serialVersionUID = -3758171511954800602, local class serialVersionUID = -3758171511954800601 * @Param: * @return: void */ @Test public void objectStreamTest3(){ ObjectOutputStream oos = null; try { //1.创建文件对象--需要将对象写入哪个文件 File file = new File("E:\\Documents\\teacher1.txt"); //2.创建节点流--输出流 FileOutputStream fos=new FileOutputStream(file,true); //3.创建处理流-对象输出流 oos = new ObjectOutputStream(fos); //4.创建需要写入的类对象 Person p = new Person("徒弟", 18); Teacher t1=new Teacher("李白",25,p); Teacher t2=new Teacher("杜甫",30,p); //5.写入到文件 oos.writeObject(t1);//当文件未序列化时报错:com.carl.javaadvanced.ioclass.Person oos.writeObject(t2); oos.writeObject(p); p.setAge(15); oos.writeObject(p); oos.writeObject(t2); } catch (IOException e) { e.printStackTrace(); } finally { try { //关闭流 oos.close(); } catch (IOException e) { e.printStackTrace(); } } //获取文件中的对象 ObjectInputStream ois= null; try { //1.创建文件对象--需要从哪个文件读取对象 File dest=new File("E:\\Documents\\teacher1.txt"); //2.创建节点流--输入流 FileInputStream fis=new FileInputStream(dest); //3.创建处理流--对象输入流 ois = new ObjectInputStream(fis); //4.将获取的对象信息使用类对象承接--这里会失去对象记忆,一并认为是Object Teacher t1=(Teacher) ois.readObject(); Teacher t2=(Teacher) ois.readObject(); Person p=(Person) ois.readObject(); Teacher t3=(Teacher) ois.readObject(); System.out.println(t1.toString()+">>>>\n"+t2.toString()+">>>>\n"+p.toString()+">>>>\n"+t3.toString()); System.out.println(t2==t3);//true System.out.println(t1.getP()==t2.getP());//true System.out.println(p == t1.getP());//true } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { try { //关闭流 ois.close(); } catch (IOException e) { e.printStackTrace(); } } }
序列化机制的过滤功能:
在Java9中为ObjectOutputStream增加了两个方法- setObjectInputFilter():用于为对象输入流设置过滤器,当反序列化时,过滤器会自动调用 checkInput()方法,用于检查序列化数据是否有效
- getObjectInputFilter():获取输入流的过滤器状态
- checkInput()方法检查序列化数据时有三种返回值:
- Status.REJECTED:拒绝恢复
- Status.ALLOWED:允许恢复
- Status.UNDECIDED:未决定状态,程序继续执行检查
自定义序列化:
transient:被该关键字修饰的成员变量将无法进行序列化–这样就可以控制某些成员变量不需要序列化
缺点:成员变量被该关键字修饰后将完全被隔离在序列化机制之外,无法获取
另一种自定义序列化机制:
在需要序列化的类中实现这三种方法:private void writeObject(ObjectOutputStream out):负责写入特定类的实例状态 private void readObject(ObjectInputStrean in):负责从流中读取并恢复对象的实例变量 private void readObjectNoData():用于正确的初始化反序列化的对象
通过这三种方法可以控制类中哪些实例变量需要序列化,哪些不需要序列化,以及序列化流不完整的情况下进行初始化
彻底的自定义机制:ANY-ACCESS-MODIFIER Object writeReplace():将对象转换为其他对象进行序列化-手动调用 ANY-ACCESS-MODIFIER Object readResolve():保护性复制整个对象-在readObject()之后被调用--一般用于序列化单例类、枚举类,该方法的返回值会覆盖原来反序列的对象,而原来readObject()反序列化的对象将会被立即丢弃
private void writeObject(java.io.ObjectOutputStream out) throws IOException { out.writeObject(new StringBuffer(name).reverse()); out.writeInt(age); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { this.name=new String(((StringBuffer)in.readObject()).reverse()); this.age=in.readInt(); } //这个方法实现后,在调用readObject()方法时必须使用该返回值类型接收 private Object writeReplace() throws ObjectStreamException{ ArrayList<Object> list=new ArrayList<>(); list.add(name); list.add(age); return list; }
-
4. Java虚拟机读写其他进程的数据
使用Runtime对象的exec()方法可以运行平台上的其他程序,该方法产生一个Process对象,Process对象代表的就是java程序启动的子进程
InputStream getErrorStream();获取子进程的错误流
InputStream getInputStream();获取子进程的输入流
OutputStream getOutputStream();获取子进程的输出流
public void childProcess(){
Process p= null;
BufferedReader br=null;
try {
p = Runtime.getRuntime().exec("javac");
br=new BufferedReader(new InputStreamReader(p.getErrorStream(),"GBK"));
String buff=null;
while((buff=br.readLine())!=null){
System.out.println(buff);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5. RandomAccessFile(随机访问)
RandomAccessFile是java输入/输出流中功能最丰富的文件内容访问类,它既可以读取文件内容,也可以向文件写数据。
特点:支持随机访问方式访问文件,程序可以直接跳转到文件的任意位置进行数据的读写操作
使用场景:
-
只需要访问文件的部分内容
-
在指定位置追加内容
缺点:只能操作文件
public RandomAccessFile(File file, String mode);
//使用String参数的构造器本质上还是使用的File参数的构造器--套娃
public RandomAccessFile(String name, String mode);
需要特别说明mode参数:该参数指定了RandomAccessFile对象的访问模式,共四个参数
-
“r”:以只读方式打开指定文件。如果试图对该RandomAccessFile执行写入操作,将抛出IOException
-
“rw”:以读、写方式打开指定文件,如果该文件不存在,则尝试创建该文件
-
“rws”:以读、写方式打开指定文件,同步文件内容和元数据的更新
-
“rwd”:以读、写方式打开指定文件,同步文件内容的更新
write()写数据时,"rw"模式不会将数据立即写入到磁盘中,而是在关闭close之前flush到磁盘中,"rwd"会将数据立即写入到磁盘中,如果数据写入出现异常,"rwd"会保存异常前写入的数据,"rw"则全部丢失
long getFilePointer();返回文件记录指针的当前位置
void seek(long pos);将文件记录指针定位到pos位置
三个重载的read方法与InputStream一样
三个重载的write方法与OutputSream一样
还特别提供了readXxx()和WriteXxx()来完成输入和输出
4. 实际业务中常用的IO架构
public class ConnectionPerThreadWithPool implements Runnable
{
public void run()
{
//线程池
//注意,生产环境不能这么用,具体请参考《java高并发核心编程卷2》
ExecutorService executor = Executors.newFixedThreadPool(100);
try
{
//服务器监听socket
ServerSocket serverSocket =
new ServerSocket(NioDemoConfig.SOCKET_SERVER_PORT);
//主线程死循环, 等待新连接到来
while (!Thread.interrupted())
{
Socket socket = serverSocket.accept();
//接收一个连接后,为socket连接,新建一个专属的处理器对象
Handler handler = new Handler(socket);
//创建新线程来handle
//或者,使用线程池来处理
new Thread(handler).start();
}
} catch (IOException ex)
{ /* 处理异常 */ }
}
static class Handler implements Runnable
{
final Socket socket;
Handler(Socket s)
{
socket = s;
}
public void run()
{
//死循环处理读写事件
boolean ioCompleted=false;
while (!ioCompleted)
{
try
{
byte[] input = new byte[NioDemoConfig.SERVER_BUFFER_SIZE];
/* 读取数据 */
socket.getInputStream().read(input);
// 如果读取到结束标志
// ioCompleted= true
// socket.close();
/* 处理业务逻辑,获取处理结果 */
byte[] output = null;
/* 写入结果 */
socket.getOutputStream().write(output);
} catch (IOException ex){
/*处理异常*/
}
}
}
}
}
代码说明
上述示例代码中,对于每一个新的网络连接,都会通过线程池分配一个专门的线程去处理IO。其中每个线程都会独立负责自己的socket连接的输入和输出
在这个过程中服务器的监听线程也是相互独立的,任何的Socket连接的输入和输出处理,不会阻塞到新的Socket连接的监听和建立,因此服务器的吞吐量也得到了提升
这是一个非常经典的Connection Per Thread模型,这种模型在活动连接数不超高1000的情况下是非常不错的,不需要考虑系统的过载、限流等问题
缺点:
-
这个模型非常依赖于线程,线程的创建和销毁成本很高,线程的创建和销毁都需要通过重量级的系统调用去完成
-
线程本身占用较大内存,Java中线程的栈内存一般至少分配512K~1M的空间,如果系统中的线程数超过一千以上,整个JVM的内存将占用1G以上
-
线程的切换成本也非常高,操作系统切换线程时需要保留线程的上下文,然后执行系统调用。
过多的线程频繁切换可能会导致执行线程切换的时间大于线程执行的时间,这时候带来的表现往往是系统的CPU sy值特别高,从而导致系统几乎陷入不可用状态
-
容易造成锯齿状的系统负载。
因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不稳定,就很容易造成大量请求同时到来,从而激活大量阻塞线程增加系统负载压力
为了解决这些缺点,Java在1.4版本引入了NIO,参考NIO流(多路复用技术)