【Java学习笔记】API:I/O流
File类
File类中每一个实例可以表示硬盘(文件系统)中的一个文件或目录(实际上表示的是一个抽象路径)
使用File可以做到:
(1)访问其表示的文件或目录的属性信息,例如:名字,大小,修改时间等等
(2)创建和删除文件或目录
(3)访问一个目录中的子项
但是File不能访问文件数据
快捷键:光标指向->Alt+回车->自动import
ctrl+alt+L 自动格式化(自动对齐代码)
使用File访问文件及其属性信息
创建File时要指定路径,而路径通常使用相对路径。
相对路径的好处在于有良好的跨平台性。
实际开发中我们很少使用绝对路径,虽然清晰明了,但是不利于跨平台;绝对路径是从根开始的(E盘、D盘等盘符开始的:"C:/Users/TEDU/IdeaProjects/CGB2202_SE/demo.txt")
".\\demo.txt"写法与"./demo.txt"相同,但是为了更好的跨平台性,使用后面一种写法(因为linux只支持"/"普通斜杠)
"./"或"."是相对路径中使用最多的,表示“当前目录”,而当前目录是哪里取决于程序运行环境而定,在idea中运行java程序时,这里指定的当前目录就是当前程序所在的项目目录。
"./"在相对路径中是可以忽略不写的,默认就是从"./"开始的
无参方法
getName():获取名字------输出String类型
length():获取长度------输出long类型
补充:在一个文件中,字母数字占用1个字节,汉字占用3个字节
在java的char数据类型中,1个字符占用2个字节
canRead():是否可读----输出boolean类型
canWrite():是否可写----输出boolean类型
isHidden():是否隐藏----输出boolean类型
import java.io.File;
File file1 = new File("c:/xxx/xxx/xx/xxx.txt");
//新建了一个File实例,表示一个绝对路径下的文件(引用名file1装的是一个地址)
File file = new File("./demo.txt");
//新建了一个File实例,表示一个相对路径下的文件(引用名file的地址指向./demo.txt文件)
//获取名字
String name = file.getName();
System.out.println(name);
//获取文件大小(单位是字节)
long len = file.length();
System.out.println(len+"字节");
//是否可读可写
boolean cr = file.canRead();
boolean cw = file.canWrite();
System.out.println("是否可读"+cr);
System.out.println("是否可写"+cw);
//是否隐藏
Boolean ih = file.isHidden();
System.out.println("是否隐藏"+ih);
创建一个新文件
无参方法:
creatNewFile():可以创建一个新文件(文件类型看引用类型)
exists():判断当前File表示的位置是否已经实际存在该文件或目录----返回boolean
import java.io.File;
import java.io.IOException;
File file = new File("./test.txt")
//新建了一个File实例,引用指向了一个当前目录下的test.txt文件
/*
创建文件的前提是该文件所在的目录必须存在,如果目录不存在则创建时会抛出异常:
java.io.IOException: 系统找不到指定的路径。
File file = new File("./mydir/test.txt");
所以先用exists来判断*/
if(file.exists()){
System.out.println("该文件已存在!");
}else{
try{
file.creatNewFile();//此步骤需要用快捷键Alt+enter,
//下面的代码系统会帮我们直接补充
System.out.println("文件已创建!");
}catch (IOException e) {
e.printStackTrace();//自动补充
}
//执行过程:
该目录下没有test.txt文件时:调用creaNewFile()方法,创建该文件,并输出"文件已创建"
若该目录已有test.txt文件,输出:"该文件已存在"
快捷键补全:Alt+enter>>enter
创建一个目录
mkDir():创建当前File表示的空目录
注:mkdir是linux中的一条命令。就是make directory的简写,意思是创建目录dir.mkdir();//创建目录时要求所在的目录必须存在,否则创建失败
mkDirs():创建当前File表示的目录,同时将所有不存在的父目录一同创建(推荐使用该方法)
File dir1 = new File("demo");//表示没有子目录,是一个空目录
File dir2 = new File("./a/b/c/d");//表示有多个父目录
if(dir1.exists){
System.out.println("该目录已存在!");
}else{
dir1.mkdir();//创建一个空目录
System.out.println("目录已创建!")
}
if(dir2.exists){
System.out.println("该目录已存在!");
}else{
dir2.mkdirs();//创建一个目录同时将父目录一并创建
System.out.println("目录已创建!")
}
删除一个文件或目录
delete():可以将File表示的文件或目录(只能是空目录)删除
File file = new File("test.txt");//文件
File dir = new File("demo");//目录
//删除文件
if(file.exists()){
file.delete();
System.out.println("文件已删除!");
}else{
System.out.println("文件不存在!");
}
//删除目录(只能删除空目录)
if(dir.exists){
dir.delete();//
System.out.println("目录已删除!");
}else{
System.out.println("目录不存在!");
}
访问一个目录中的所有子项
无参方法:
isFile():判断当前File表示的是否为一个文件------输出boolean类型
isDirectory():判断当前File表示的是否为一个目录------输出boolean类型
listFiles():可以访问一个目录中的所有子项,并将一个目录下的所有子项都返回填充到File的数组
中-------输出的是File数组类型
补充:isDirectory()和exists()方法的区别
isDirectory()可以判断当前File是否是一个目录,因为只有是目录,才能访问它的各个子项
exists()可以判断当前File是否存在,它不区分是不是文件或者是目录
例如上面两个:一个是目录(可以有后缀,但没有特别意义),一个是.txt的文件
exists()两个都为true,而isDirectory()第1个为true,第2个为false,这样可以剔除掉带有视觉干扰的文件
Flie dir = new File(".");//表示的是在一个当前目录下(一个项目目录中)
if(dir.isDirectory()){//一种固定的写法,判断File指向的是不是一个目录
//因为在目录下才可以获取多个它的子项
File[] subs = dir.listFiles();
System.out.println("当前目录包含"+subs.length()+"个子项");
for(int i = 0;i<subs.length;i++){//遍历目录中的所有子项
File sub = subs[i];
System.out.println(sub.getName());
}
}
获取目录中符合特定条件的子项
有参:
File[] listFiles(FileFilter filter):重载的方法,要求传入一个文件过滤器,并在该过滤器上定义过滤条件,之后listFilesz执行完毕仅将满足该过滤要求的子项返回
FileFilter是一个接口:public interface FileFilter {
boolean accept(File pathname);
}参数要求传入一个FileFilter接口的实例:可以采用匿名类(适用于仅创一个实例)
也可以新建一个类实现该接口
listFiles(FileFilter filter):该方法会将目录中每一个子项都作为参数先传给filter的accept方法,只有accept方法返回为true的子项最终才会被包含在返回的File[]数组中进行返回。
补充:源代码::将接口accept返回为true的文件返回,同时添加到数组中,false的舍弃不添加
public File[] listFiles(FileFilter filter) { String ss[] = list(); if (ss == null) return null; ArrayList<File> files = new ArrayList<>(); for (String s : ss) { File f = new File(s, this); if ((filter == null) || filter.accept(f)) files.add(f); } return files.toArray(new File[files.size()]); }
//要求:获取当前目录中所有名字以"."开始的子项:startwith()方法
// 获取当前目录中所有名字含有"o"的子项:contains()方法或使用正则表达式
File dir = new File(".");
if(dir.isDirectory()){
//接口实现方式一:适用匿名内部类创建过滤器
FileFilter filter = new FileFilter(){
//FileFilter是一个接口,创建了一个接口匿名类的对象,引用名是filter
//FileFilter中有一个抽象方法accept(File file),因此匿名类中必须重写该方法
public boolean accept(File file){
String name = file.getName();
boolean starts = name.contains("o");//名字是否含"o"
return starts;
}
};
File[] subs = dir.listFiles(filter);//方法内部会调用accept方法
//最终为true的子项才能被listFiles返回
//将上面的代码简化
File[] subs = dir.listFiles(new FileFilter(){
public boolean accept(File file){
return file.getName().contains("o");
}
});
System.out.println(subs.length);
/*补充:接口实现方式二:新建一个类实现FileFilter接口
class MyFilter implements FileFilter{
public boolean accept(File file) {
String name = file.getName();//获取该file表示的文件或目录的名字
//过滤条件是名字中含有"o"的
//方法1: String regex = ".*o.*";//正则表达式写法
// boolean match = name.matches(regex);
// return match;
//方法2: return name.indexOf("o")>=0;
//方法3: return name.contains("o");
}
}
// 获取./src/file目录下所有名字以"D"开头的子项
public class Test {
public static void main(String[] args) {
File dir = new File("./src/file");
if(dir.isDirectory()){
FileFilter filter = new FileFilter() {
public boolean accept(File file) {
String name = file.getName();
System.out.println("正在过滤:"+name);
return name.startsWith("D");
}
};
File[] subs = dir.listFiles(filter);
for(int i=0;i<subs.length;i++){
System.out.println(subs[i].getName());
}
}
}
}
/*
正在过滤:CreateNewFileDemo.java
正在过滤:DeleteDirDemo.java
正在过滤:DeleteFileDemo.java
正在过滤:FileDemo.java
正在过滤:ListFilesDemo.java
正在过滤:ListFilesDemo2.java
正在过滤:MkDirDemo.java
正在过滤:Test.java
DeleteDirDemo.java
DeleteFileDemo.java
*/
Lambda表达式
JDK1.8之后,java支持了lambda表达式这个特性
lambda:直观感受可以用更精简的代码创建匿名内部类,但是该匿名内部类实现的接口只能有一个抽象方法,否则无法使用!
lambda表达式是编译器认可的,最终会将其改为内部类编译到class文件中
语法:
(参数列表)->{
方法体
}
//匿名内部类形式创建FileFilter
FileFilter filter = new FileFilter(){
public boolean accept(File file){
return file.getName().startsWith(".");
}
};
//使用lambda表达式
第一步:可以将new FileFilter()及抽象方法省略
FileFilter filter1 = (File file)->{return file.getName().startsWith(".");};
进一步简化:参数的类型可以忽略不写
FileFilter filter1 = (file)->{return file.getName().startsWith(".");};
进一步简化:一个参数小括号也可以不写
FileFilter filter1 = file->{return file.getName().startsWith(".");};
注:两个参数及两个以上,小括号不可以省略
进一步简化:若方法体只有一句话,大括号{}可以省略
若这句话有return关键字,那么return也要一并省略
FileFilter filter1 = file->file.getName().startsWith(".");
import java.io.File;
import java.io.FileFilter;
// 列出当前目录中所有名字包含s的子项。
// 使用匿名内部类和lambda两种写法
//使用匿名类
File file = new File(".");
if(file.isDirectory()){
File[] subs = file.listFiles(new FileFilter() {
public boolean accept(File file) {
return file.getName().contains("s");
}
});
System.out.println("共有"+subs.length+"个子项");
for(int i = 0;i<subs.length;i++){
System.out.println(subs[i].getName());
}
}
//使用lambda表达式
File file = new File(".");
if(file.isDirectory()){
File[] subs = file.listFiles(file1 -> file1.getName().contains("s"));
System.out.println("共有"+subs.length+"个子项");
for(int i = 0;i<subs.length;i++){
System.out.println(subs[i].getName());
}
}
IO流
io可以让我们用标准的读写操作来完成堆不同设备的读写数据工作,io按照方向划分为输入与输出,参照点是我们写的程序
输入:用来读取(read)数据的,是从外界到程序的方向,用于获取数据
输出:用来写出(write)数据的,是从程序到外界的方向,用于发送数据
java将IO比喻为“流”,即stream,就像生活中的“电流”,“水流”一样,它是以同一个方向顺序移动的过程,只不过这里流动的是字节(2进制数据),所以在IO中有输入流和输出流之分,我们理解它们是连接程序与另一端的“管道”,用于获取或发送数据的另一端。
两个超类(抽象类)
java.io.InputStream:所有字节输入流的超类,其中定义了读取数据的方法,因此将来不管读取的是什么设备(连接该设备的流),都有这些读取的方法,同样的方法可以读取不同设备中的数据
java.io.OutputStream:所有字节输出流的超类,其中定义了写出数据的方法。
两种流:节点流与处理流
节点流:也称为低级流,节点流的另一端是明确的,是实际读写数据的流,读写一定是建立在节点
流基础上进行的。
处理流:也称为高级流,处理流不能独立存在,必须连接在其他流上,目的是当数据流经当前流
时对数据进行加工处理来简化我们对数据的操作。
实际应用中,我们通过串联一组高级流到某个低级流上以流水线式的加工处理对某设备的数据进行读写,这个过程也称为流的连接,这也是IO的精髓。
public abstract class OutputStream implements Closeable, Flushable {...} public interface Flushable { void flush() throws IOException; } 字节输出流的超类java.io.OutputStream都实现了java.io.Flushable接口,该接口定义了flush方法。所以所有字节输出流都有该方法。
文件流
java.io.FileInputStream和FileOutputStream
这是一对低级流,继承自InputStream和OutputStream,用于读写硬盘上文件的流
补充:获取当前系统时间的毫秒值(从1970年1月1日0时起到----当前)
System.currentTimeMillis()---------返回一个long类型的值
FileOutputStream
常用构造器:FileOutputStream(String path)、FileOutputStream(File file)
向当前目录中的文件写入数据
void write(int d):向文件中写入1个字节,写入的内容是给定的int值对应的2进制的“低八位”。
//向当前目录下的demo.dat文件中写入数据
FileOutputStream fos = new FileOutputStream("./demo.txt");
//若目录中没有demo.dat,系统会创建一个
//若目录中有,在写入前会将之前的文件内容全部清空,再重新写入
fos.write(1);
/*将整数int:1 写入(32位)
1的2进制:00000000 00000000 00000000 00000001
读取“低八位”
写入demo.txt文件中
*/ 00000001
fos.write(2);
//此时,文件中数据为:00000001 00000010
fos.close();
System.out.println("执行完了");
FileInputStream
int read():无参,读取1个字节,并以int型返回,将读取的1个字节放到int型的最后8位,前面补0
如果返回值为整数-1,则表示流读取到了末尾,对于读取文件而言就是EDF(end of file)
//读取fos.dat文件内容:00000001 00000011
FielInputStream fis = new FileInputStream("fos.dat");
int d = fis.read();//第1次调用read
System.out.println(d);
/*
先读取第1个字节:00000001
返回int值: 00000000 00000000 00000000 00000001
----------补0------------- 放到最后8位
返回的int值就是d的内容
*/
d = fis.read();//第2次调用read
System.out.println(d);
/*
读取第2个字节:00000011
返回int值: 00000000 00000000 00000000 00000011
*/
d = fis.read();//第3次调用
System.out.println(d);
//到文件末尾了,int值返回-1: 11111111 11111111 11111111 11111111
//d = -1
fis.close();//关闭输入流
文件的复制
1、单字节复制
//复制一张图片:图片名----image.png
FileInputStream fis = new FileInputStream("image.png");
FileOutputStream fos = new FileOutputStream("image_cp.png");//复制后的文件名
int d;//复制是先读后写,先保存每次读到的字节
while((d=fis.read())!=-1){
fos.write(d);
}
fis.close();
fos.close();
/*
第1轮循环:先读取一次,将int值存到变量d中,判断是不是末尾,不是则写入
原文件:11000111 11000010 ...
读取第1个字节:00000000 00000000 00000000 11000111(放到后8位,前面补0)
写入第1个字节到复制后的文件中:取当前要写入字节的低8位:11000111
所以第1次复制后的文件中:11000111
第2轮循环,接着在第1次的数据后面写:11000111 11000010 ...
依次循环,知道读取的int值返回到-1,跳出循环
2、块字节复制
块读操作:
InputStream中提供了块读操作
int read(byte[] data):有参,一次性从文件中读取给定的字节数组总长度的字节量,并存入到该数
数组中。
返回值为实际读取到的字节量,若返回值为-1则表示读取到了文件末尾
int read()和int read(byte[] data)的区别:
两者返回的都是int类型,但是表达的意义不同
read()返回的是每次读取到的是文件中的值(二进制)
read(byte[] data)返回的是读取一个byte数组的实际长度
块写操作
OutputStream的块写方法:
void write(byte[] data):一次性将给定的字节数组所有字节写入到文件中
void write(byte[] data,int offset,int len):一次性将给定的字节数组从下标offset处开始的连续len个字节写入文件
//复制一个程序:wnwb.exe---使用块读写形式复制文件
FileInputStream fis = new FileInputStream("wnwb.exe");
//创建文件输入流读取原文件
FileOutputStream fos = new FileOutputStream("wnwb_cp.exe");
//创建文件输出流写出文件
//假设程序中有9个字节
int d;
byte[] data = new byte[5];
//块读取操作
d = fis.read(data);//第一次调用read
//d = 5 本次读取到了5个字节
d = fis.read(data);//第二次调用
//d = 4 本次仅读取到了4个字节
d = fis.read(data);//第三次调用
//d = -1 本次一个字节都没有读取到
/*
若每次调用后都执行这两步:
System.out.println(Arrays.toString(data));
System.out.println(d);
输出:
[49, 50, 51, -27, -83]
5
[-90, -28, -71, -96, -83]
4
[-90, -28, -71, -96, -83]
-1
*/
//块读取及写入操作(即整个复制过程)
int len;
byte[] data = new byte[1024*10];
while((len=fis.read(data))!=-1){
fos.write(data,0,len);//读取多少就写多少
}
fis.close();
fos.close();
/*比较:直接写10240和1024*10在运算效率是一样的,没有性能损耗
在编译时,带有表达式的如果可以直接算出来,编译器就会直接算出结果
所以在运行时,它俩的值完全一样
但是用表达式意义:可读性好
*/
/*将当前目录下的所有文件都复制一份,复制的文件命名为:原文件名_cp.后缀
* 比如原文件为:test.dat
* 复制后的文件为:test_cp.dat*/
public class Test{
public static void main(String[] args) throws IOException {
File dir = new File(".");
if(dir.isDirectory()){
File[] subs = dir.listFiles(file->file.isFile());//将目录中的文件过滤出来
for(int i=0;i<subs.length;i++){
File file = subs[i];
String fileName = file.getName();//原文件的名字
//名字部分 test
String name1 = fileName.substring(0,fileName.lastIndexOf("."));
//后缀部分 dat
String name2 = fileName.substring(fileName.lastIndexOf(".")+1);
//复制的文件的名字
String newFileName =name1+"_cp."+name2;
FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(newFileName);
byte[] data = new byte[1024*10];
int len;
while((len = fis.read(data))!=-1){//块读
fos.write(data,0,len);//块写
}
fis.close();
fos.close();
}
}
System.out.println("全部复制完毕!");
}
}
字符集
支持中文的常见字符集有:
GBK:国标编码。英文数字等:1个字节,中文字符:2个字节
UTF-8:内部都是unicode编码,在这个基础上不同了,加入了少部分的2进制信息作为长度描述
英文数字等:1个字节,中文字符:3个字节
英文:2进制一定是以0开始的,就可以识别出是占用1个字节,长度固定为1
但中文字符不一样:在Unicode中:2个字节,长度为2;在UTF-8中:3个字节,长度为3
那么当中英文混杂UTF-8是怎么识别出中文长度并读取的?
UTF-8中解码规则:英文2进制开头为0,可以直接识别(长度为1)
中文字符(假设Unicode二进制为:11110000 11001100)
将2字节变为3字节 :1110 10 10 ------每个字节开头加的识别码
1111 000011 001100 -----拼起来就是原Unicode编码
StandardCharsets.UTF_8就是"UTF-8";出现乱码:字符集不对(搭配)
解码:
String line = "/myweb/login?username=%E6%AD%A6%E9%9B%AA%E9%A3%9E&password=123&nickname=11000&age=18";
try {
line = URLDecoder.decode(line,"UTF-8");//将十六进制的信息转换为UTF-8可以识别的信息
System.out.println(line);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//decode只对%敏感,返回的还是字符串
//将浏览器发送过来的加密信息(汉字在传输时不能用字符集8859-1的编码格式发送过来,需要加密)
//加密方式:%XX(XX是十六进制)
写文本数据及追加模式
String提供方法:byte[] getbytes(String charsetName):将当前字符串转换为一组字节
参数为字符集的名字,常用的UTF-8。
字符集参数不分大小写,但是拼写错误会引发异常:UnsupportedEncodingException(不支持字符集异常)
追加模式:
FileOutputStream(String path,boolean append)
FileOutputStream(File file,boolean append):也可以直接传入File类型的文件路径名
当第二参数传入true时,文件流为追加模式,即:指定的文件若存在,则原有数据保留,新写入的数据会被顺序的追加到文件中
不写第二参数时,系统默认为false,没有追加模式
//无追加模式
FileOutputStream fos = new FileOutputStream("demo.txt");
String line = "轻轻敲击沉睡的心灵,慢慢张开你的眼睛";
byte[] data = line.getBytes("UTF-8");//将line字符串以UTF-8的格式解码为二进制
fos.write(data);//写到目标文件中
System.out.println("执行完毕");
fos.close();
//有追加模式
FileOutputStream fos = new FileOutputStream("demo.txt",true);
fos.write("轻轻敲击沉睡的心灵,慢慢张开你的眼睛".getBytes("UTF-8"));//编码合写到一句
fos.write("伸出你的双手,带着你的笑容".getBytes("UTF-8"));//追加的第二句
System.out.println("执行完毕");
fos.close();
从文件中读取文本数据
String中提供了将字节数组转换为字符串的构造方法:
String(byte[] data,String charsetName):可以将给定的字节数组中所有字节按照指定的字符集转换为字符串
String(byte[] data,int offset,int len,String charsetName):将给定的字节数组从下标offset处开始的连续len个字节按照指定的字符集转换为字符串
FileInputStream fis = new FileInputStream("fos.txt");
byte[] data = new byte[1024*10];
int len = fis.read(data);//块读操作,读出来是字节
//将字节转为字符串
String line0 = new String(data,"UTF-8");//此种方法读出来的文件最终大小会大于原文件
String line = new String(data,0,len,"UTF-8");//此种方法读出来文件与原文件相等
System.out.println(line);
System.out.println(line.length);
fis.close();
ByteArrayStream:字节数组流
java.io.ByteArrayOutputStream和ByteArrayInputStream
* 字节数组输出与输入流
* 它们是一对低级流,内部维护一个字节数组。
* ByteArrayOutputStream通过该流写出的数据都会保存在内部维护的字节数组中。
ByteArrayOutputStream内部提供的方法:
Byte[] toByteArray():该方法返回的字节数组包含这目前通过这个流写出的所有字节
int size():返回的数字表示已经通过当前流写出了多少字节(在流自行维护的字节数组中实际
保存了多少字节)
//用途:
//FileOutputStream fos = new FileOutputStream("pw2.txt",true);
ByteArrayOutputStream baos = new ByteArrayOutputStream();//也是一条低级流
//它是内存的字节流,没有真正的写入到文件中
OutputStreamWriter osw = new OutputStreamWriter(baos, StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw,true);
//内部连接字节数组输出低级流,此种连接并没有真正的按行写出到磁盘中,只是在内存中
byte[] data = baos.toByteArray();
System.out.println("内部数组长度:"+data.length);//内部数组长度为:0
System.out.println("内部数组内容:"+ Arrays.toString(data));//内部数组内容:[]
System.out.println("内部缓冲大小:"+baos.size());//内部缓冲大小:0
pw.println("helloworld");//当写出一行
data = baos.toByteArray();
System.out.println("内部数组长度:"+data.length);//内部数组长度为:12
System.out.println("内部数组内容:"+ Arrays.toString(data));
//内部数组内容:[104, 101, 108, 108, 111, 87, 111, 114, 108, 100, 13, 10]
System.out.println("内部缓冲大小:"+baos.size());//内部缓冲大小:12
pw.println("think in java");//又写出一行
data = baos.toByteArray();//内部数组长度为:29
System.out.println("内部数组内容:"+ Arrays.toString(data));
//内部数组内容:[104, 101, 108, 108, 111, 87, 111, 114, 108, 100, 13, 10, 72, 105, 44, 104, 111, 119, 32, 97, 114, 101, 32, 121, 111, 117, 33, 13, 10]
System.out.println("内部缓冲大小:"+baos.size());//内部缓冲大小:29
pw.close();
高级流
java将流分为节点流与处理流两类
节点流:也称为低级流,是真实连接程序与另一端的“管道”,负责实际读写数据的流,读写一定是建立在节点流的基础上进行的。它好比家里的“自来水管”,连接我们的家庭与自来水厂,负责搬运水。
处理流:也称为高级流,不能独立存在,必须连接在其他流上,目的是当数据经过当前流时对其进行某种加工处理,简化我们对数据的同等操作。它好比家里常用对水做加工的设备,比如“净水器”,“热水器”,有了它们我们就不必再自己对水进行加工了。
流的连接:实际开发中我们经常会串联一组高级流最终连接到低级流上,在读写操作时以流水线式的加工完成复杂IO操作,这个过程称为"流的连接"。
缓冲流
缓冲流时一对高级流,作用是提高读写数据的效率。
缓冲流内部有一个字节数组,默认长度是8K,缓冲流读写数据时一定是将数据的读写方式转换为块读写来保证读写效率。
缓冲流进行复制
FileInputStream fis = new FileInputStream("demo.txt");
BufferedInputStream bis = new BufferdeInputStream(fis);//连接低级输入流
FileOutputStream fos = new FileOutputStream("demo_cp.txt");
BufferedOutputStream bos = new BufferedOutputStream(fos);//连接低级输出流
int d;
while((d=bis.read())!=-1){
bos.write(d);
}
System.out.println("执行完毕");
bis.close();
bos.close();
flush():解决缓冲输出流写出数据的缓冲区问题
问题:通过缓冲流写出的数据会被临时存入缓冲流内部的字节数组,直到数组存满数据才会真实写出一次
解决:调用flush()方法------缓冲流的flush方法用于强制将缓冲区中已经缓存的数据一次性写出
注:该方法实际上存在字节输出流的超类OutputStream上定义的,并非只有缓冲输出流有这个方法,但是实际上只有缓冲输出流的该方法有实际意义,其他的流实现该方法的目的仅仅是为了在流连接过程中传递flush动作给缓冲输出流。
当流结束调用close()时,它的源程序也会调用flush(),保证在结束前清空一次
FileOutputStream fos = new FileOutputStream("demo.txt");
BufferedOutputStream bos = new BufferedOutputStream(fos);
String line = "奥利给!";
byte[] data = line.getBytes(StandardCharsets.UTF_8);
bos.write(data);
System.out.println("写出完毕");
bos.flush();
bos.close();
对象流
对象流是一对高级流,在流连接中的作用是进行对象的序列化与反序列化。
对象序列化:将一个java对象按照其结构转换为一组字节的过程。
对象反序列化:将一组字节还原为java对象(前提是这组字节是一个对象序列化得到的字节)
对象序列化的流连接操作及原理图:
void writeObject():对象流的写出方法----输出的对象是Object类型
将给定的对象转换为一组字节并写出,但是需要注意:写出的对象所属的类必须实现接口:java.io.Serializable,否则该方法会抛出异常:java.io.NotSerializableException
Object readObject():对象流的读取方法----输出的对象同样是Object类型,若想得到之前类型,需
要强转。该方法会进行对象的反序列化,如果对象流通过其连接的流读取的
字节分析并非是一个java对象,会抛出异常:ClassNotFoundException
Serializable
需要进行序列化的类必须实现接口:java.io.Serializable
实现序列化接口最好主动定义序列化版本号这个常量,这样一来对象序列化时就不会根据类的结构生成一个版本号,而是使用该固定值。那么反序列化时,只要还原的对象和当前类的版本号一致就可以进行还原。
若不自己定义序列化版本号,在后期若更改了对象的结构,在反序列化时,自动生成的版本号就与之前随机生成的不一样,无法完成发序列化。
序列号只跟类的结构有关,跟对象的信息无关。
将一个Person对象写入文件 1:先将Person对象转换为一组字节 2:将字节写入文件 流连接: 序列化 持久化 v v 对象---->对象流------->文件流-------->文件
//创建一个Person类
public class Person implements Serializable{//将Person类实现Serializable接口
public static final long SERIALIZABLE_UID = 1;//自己定义一个版本号
private String name;
private String gender;
private int age;
private transient String[] otherInfo;
/*private transient String[] otherInfo;
当一个属性被关键字transient修饰后,那么当进行对象序列化时,该属性值会被忽略
忽略不必要的属性可以达到对象瘦身的目的,减少资源开销。
*/
//Alt + insert 快捷键
//引入Person构造方法(4个参数)、getter和setter、重写toString()
}
//先将对象序列化
public class OOSDemo{
String name = "李小龙";
String gender = "男";
int age = "18";
Stirng[] otherInfo = {"是一名动作明星","来自香港","创建了截拳道","功夫巨星"};
//将一个Person对象写入文件person.obj(后缀可以随意写)
Person p = new Person(name,gender,age,otherInfo);//同时调用构造方法,传参
System.out.println(p);
FileOutputStream fos = new FileOutputStream("person.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);//连接低级流
oos.writeObject(p);//写出对象,调用writeObject(),写出来的对象是Object类型----向上造型
System.out.println("写出完毕!")
oos.close();
}
//将对象反序列化
public class OISDemo{
public static void main(String[] args){
//将person.obj文件中将对象反序列化回来
FileInputStream fis = new FileInputStream("person.obj");
ObjectInputStream ois = new ObjectInputStream(fis);//连接低级流
Person p = (Person)ois.readObject();
//readObject读出来的是一个Object类型,此时需要强转---向下造型
System.out.println(p);
字符流
java将流按照读写单位划分为字节流与字符流。
java.io.InputStream和OutputStream是所有字节流的超类
java.io.Reader和Writer则是所有字符流的超类
注:这两对超类是平级关系,互相没有继承关系
Reader和Writer是两个抽象类,里面规定了所有字符流都必须具备的读写字符的相关方法
字符流最小读写单位为字符(char),但是底层实际还是读写字节,只是字符与字节的转换工作由字符流完成。
转换流(用到了第二参数:字符集格式)
java.io.InputStreamReader和OutputStreamWriter(单词拆开来看,分别是字节流与字符流的超类)
它们是字符流非常常用的一对实现类同时也是一对高级流,实际开发中我们不直接操作它们,但它们在流连接中是非常重要的一环。
作用:实际开发中我们还有功能更好用的字符高级流,但是其他的字符高级流都有一个共同点:不
能直接连接在字节流上,而实际操作设备的流都是低级流同时也都是字节流,因此不能直接
在流连接中串联起来,转换流是一对可以连接在字节流上的字符流,其他的高级字符流可以
连接在转换流上,在流连接中起到“转换器”的作用(负责字符与字节的实际转换)
OutputStreamWriter/InputStreamRead(第一参数,第二参数):
第一参数:传入低级流引用名 第二参数:传入字符集类型(不写默认为当前系统的字符集)
转换输出流----向文件中写入文本数据
//向文件demo.txt中写入文字
FileOutputStream fos = new FileOutputStream("demo.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos,"UTF-8");//连接低级流
osw.write("轻轻的我将离开你");
osw.write("请把眼角的泪拭去");
System.out.println("执行完毕");
osw.close();
转换输入流------读取文本文件
可以将读取的字节按照指定的字符集转换为字符
字符流读一个字符的方法:int read()------若想返回char类型,需要强转
读取一个字符,返回的int值实际上表示的是一个char(低16位有效)
如果返回的int值表示的是-1则说明读取到了末尾
//将demo.txt文件中的所有文字读取回来
FileInputStream fis = new FileInputStream("demo.txt");
InputStreamReader isr = new InputStreamReader(fis,"UTF-8");
int d;
while((d=isr.read())!=-1){
System.out.println((char)d);
}
isr.close();
/*
第一轮读取的字符:
int d = isr.read();
char c = (char)d;//返回的是int值,显示char需要强转
System.out.println(c);
*/
缓冲字符流
缓冲字符流是一对高级流,内部也有一个缓冲区,读写文本数据以块读写形式加快效率,并且缓冲流有一个特别的功能:可以按行读写字符串。
缓冲字符输出流:java.io.PrintWriterr
具有自动行刷新的缓冲字符输出流,实际开发中更常用,它内部总是会自动连接BufferedWriter作为块写加速使用(缓冲读取和写出流:java.io.BufferedReader和BufferedWriter)
PrintWriter提供了对文件操作的构造方法:
PrintWriter(String path)
PrintWriter(File file)
//向文件中导入字符串
PrintWriter pw = new PrintWriter("pw","UTF-8");
pw.println("我看过沙漠下暴雨");
pw.println("看过大海亲吻鲨鱼");
pw.println("看过黄昏追逐黎明");
pw.println("没看过你");
System.out.println("执行完毕");
pw.close();
使用PrintWriter追加模式及自动行刷新功能
若想实现追加模式:只能自己创建各个流连接
自动行刷新功能:若在实例化PrintWriter时,第一参数传入的是一个流,此时若再传入一个
boolean型的参数,此值为true是就打开了自动行刷新功能
//完成简易记事本,控制台输入的每行字符串都按行写入文件,单独输入exit时退出
FileOutputStream fos = new FileOutputStream("note.txt",true);//第二参数true
//文件字节输出流(是一个低级流),向文件中写入字节数据
OutputStreamWriter osw = new OutputStreamWriter(fos,StandardCharsets.UTF-8);
//转换输出流(是一个高级流,且是一个字符流):1、衔接字符与字节流2、将写出的字符转换为字节
BufferedWriter bw = new BufferedWriter(osw);
//缓冲输出流(是一个高级流,且是一个字符流)块写文本数据加速
PrintWriter pw = new PrintWriter(bw);
//PrintWriter pw = new PrintWriter(bw,true);
//具有自动行刷新的缓冲字符输出流(第二参数:true)
Scanner scan = new Scanner(System.in);
while(true){
String line = scanner.nextLine();
if("exit".equals(line)){
break;
}
pw.println(line);
}
pw.close();
缓冲字符输入流:java.io.BufferedReader
是一个高级的字符流,特点:块读写文本数据,并且可以按行读取字符串
BufferedReader提供了一个读取一行字符串的方法:
String readLine():该方法会连续读取若干字符,当遇到换行符停止,然后将换行符之前的内容以一个字符串形式返回。返回的字符串不含有最后的换行符。
当某一行是空行时(该行内容只有一个换行符)则返回值为空字符串
注:这是内存操作,因为第一次调用readLine时,缓冲流会将数据先一次性读取到
内部的char数组中(8K的字符),然后返回内部的一行字符串。
如果流读取到了末尾,则返回值为null。
//将当前(正在编写的)源程序读取出来并输出到控制台上
FileInputStream fis = new FileInputStream(./src/io/demo.java);//当前源程序的相对路径
InputStreamReader isr = new InputStreamReader(fis);//将读取的字节转换为字符
BufferedReader br = new BufferedReader(isr);//缓冲读取到的字符
String line;
while((line=br.readLine())!=null){
System.out.println(line);//将每次读取的每一行输出
}
br.close();
异常处理
java中所有错误的超类为:Throwable,其下有两个子类:Error和Exception
Error:子类描述的都是系统错误,比如虚拟机内存溢出等
Exception:子类描述的都是程序错误,比如空指针,下标越界等
通常我们程序中处理的异常都是Exception。
异常处理机制中的try-catch
语法:try{
可能出现异常的代码片段
}catch(XXXException e){
try中出现XXXException后的处理代码
}
try语句块不能独立存在,后面必须跟catch语句块或finally语句块
//快捷键:Alt+enter->使用try-catch环绕
System.out.println("程序开始了");
try{
//当JVM执行程序出现了某个异常时就会实例化这个异常并将其抛出
//如果该异常没有被异常机制控制,则JVM会将异常隐式抛出方法外(这里指main方法外)
String line = null;//(1)
System.out.println(line.length());//(1)空指针异常
String line = "";//(2)
System.out.println(line.charAt(0));//(2)字符串下标越界异常
String line = "abc";//(3)
System.out.println(Integer.parseInt(line));//(3)数字转换异常
//若try的语句块((1)或(2)或(3)某句出错了,则try{}中语句块中剩下的代码都不会执行)
System.out.println("!!!!!");//测试语句块
//若出现多种异常,catch有多个写法
}catch(NullPointException e){System.out.println("出现了空指针");
}catch(StringIndexOutOfBoundsException e){System.out.println("出现了下标越界");
...//第一种写法
}catch(NullPointException|StringIndexOutOfBoundsException e){
System.out.println("统一的处理方法");}
//若某些异常的处理方式相同时,可以合并在一个catch来处理:第二种
}catch(Exception e){System.out.println("总归就是出错了");}
//可以在catch超类异常来捕获并处理这一类异常:Exception所有异常的超类:第三种
System.out.println("程序结束了");
异常处理机制中的finally
finally块定义在异常处理机制中的最后一块,它可以直接跟在try之后,或者最后一个catch之后。
finally可以保证只要程序执行到了try语句块中,无论try语句块中的代码是否出现异常,最终finally都会执行。
通常我们将释放资源这类操作放在finally中确保运行,例如:IO操作后最终的close()调用。
System.out.println("程序开始了");
try{
String line = "abc";//(1)
System.out.println(line.length());
String line = null;//(2)
System.out.println(line.length());//(2)空指针异常
System.out.println("!!!!!");//测试语句块
return;//return用在方法中,表示跳出方法,如果使用在main,表示退出程序。
}catch(Exception e){
System.out.println("出错了");
}finally{
System.out.println("finally中的代码执行了");
}
System.out.println("程序结束了");
/*(1):程序开始了 (2):程序开始了
3 出错了
!!!!! finally中的代码执行了
finally中的代码执行了 程序结束了
*/
IO操作时的异常处理机制应用
//编译后的完整版
FileOutputStream fos = null;
try {
fos = new FileOutputStream("demo11.txt");
fos.write(1);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
fos.close();//为了在最后一次写出---关流
} catch (IOException e) {
e.printStackTrace();
}
}
//为了写close()方法,重复使用了try--catch,但是又不可避免
//此种写法严重加大代码量,可读性也不好,所以我们用了另一种写法
自动关闭特性:省略了close()方法
JDK7之后,java提供了一个新的特性:自动关闭,旨在IO操作中可以更简洁的使用异常处理机制完成最后的close操作。
语法:
try(定义需要在finally中调用close()方法关闭的对象)
{
IO操作
}catch(XXXException e){
...
}只有实现了java.io.AutoCloseable接口的类才可以在这里定义并初始化
编译器在编译代码的时候最终会将在这里定义的类在finally中调用close()关闭
该代码是编译器认可的,而不是虚拟机。编译器在编译上述代码后会在编译后的class文件中改回成完整版
//精简版(自动关闭特性)
try(
FileOutputStream fos = new FileOutputStream("demo.txt");
){
fos.write(1);
}catch(IOException e){
e.printStackTrace();//向控制台输出当前异常的错误信息
}
throw关键字
throw用来对外主动抛出一个异常,通常下面两种情况我们主动对外抛出异常:
1、当程序遇到一个满足语法,但是不满足业务要求时,可以抛出一个异常告知调用者
2、程序执行遇到一个异常,但是该异常不应当在当前代码片段被解决时可以抛出给调用者
当我们调用一个含有throws声明异常抛出的方法时,编译器要求我们必须添加处理异常的手段,否则编译不通过,而处理手段有两种:
1、使用try-catch捕获并处理异常
2、在当前方法上继续使用throws声明该异常的抛出
具体用哪种取决于异常处理的责任问题
注:永远不应当在main方法上使用throws!
public class Person{
private int age;
public int getAge(){
return age;
}
//使用throws对外抛出一个异常(可以自定义一个异常)
public void setAge(int age) throws IllegalAgeException{
if(age<0||age>100){
throw new IllegalAgeException("年龄不合法");
}
this.age = age;
}
或者:
//直接throw一个RuntimeException
public void setAge(int age){
if(age<0||age>100){
throw new RuntimeException("年龄不合法");
}
this.age = age;
}
//自定义异常(可使用快捷键):得继承超类Exception
public class IllegalAgeException extends Exception{
public IllegalAgeException() {
}
public IllegalAgeException(String message) {
super(message);
}
public IllegalAgeException(String message, Throwable cause) {
super(message, cause);
}
public IllegalAgeException(Throwable cause) {
super(cause);
}
public IllegalAgeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
测试:
public class ThrowDemo {
public static void main(String[] args){
System.out.println("程序开始了...");
try {
Person p = new Person();
p.setAge(100000);//典型的符合语法,但是不符合业务逻辑要求
System.out.println("此人年龄:"+p.getAge()+"岁");
return;
} catch (IllegalAgeException e) {
e.printStackTrace();
}
System.out.println("程序结束了...");
/*
输出:
exception.IllegalAgeException: 年龄不合法
at exception.Person.setAge(Person.java:13)
at exception.ThrowDemo.main(ThrowDemo.java:10)
程序开始了...
程序结束了...
*/
含有throws的方法被子类重写时的规则
//子类重写超类含有throws声明异常抛出的方法时对throws的几种特殊的重写规则
//超类
public class ThrowsDemo {
public void dosome()throws IOException, AWTException {}
}
//子类
class SubClass extends ThrowsDemo{
//可以抛出超类的所有异常
// public void dosome()throws IOException, AWTException {}
//可以不再抛出任何异常
// public void dosome(){}
//可以仅抛出部分异常
// public void dosome()throws IOException {}
//可以抛出超类方法抛出异常的子类型异常
// public void dosome()throws FileNotFoundException {}
//不允许抛出额外异常(超类方法中没有的,并且没有继承关系的异常)
// public void dosome()throws SQLException {}
//不可以抛出超类方法抛出异常的超类型异常
// public void dosome()throws Exception {}
}
java异常
可以分为可检测异常,非检测异常:
可检测异常:可检测异常经编译器验证,对于声明抛出异常的任何方法,编译器将强制执行处理或
声明规则,不捕捉这个异常,编译器就通不过,不允许编译
非检测异常:非检测异常不遵循处理或者声明规则。在产生此类异常时,不一定非要采取任何适当
操作,编译器不会检查是否已经解决了这样一个异常
RuntimeException 类:属于非检测异常,因为普通JVM操作引起的运行时异常随时可能发生,此
类异常一般是由特定操作引发。但这些操作在java应用程序中会频繁出现。
因此它们不受编译器检查与处理或声明规则的限制。
异常处理机制是用来处理那些可能存在的异常,但是无法通过修改逻辑完全规避的场景。
而如果通过修改逻辑可以规避的异常是bug,不应当用异常处理机制在运行期间解决!应当在编码时及时修正。
常见的RuntimeException子类
1、IllegalArgumentException:抛出的异常表明向方法传递了一个不合法或不正确的参数
2、NullPointerException:当应用程序试图在需要对象的地方使用 null 时,抛出该异常
3、ArrayIndexOutOfBoundsException:当使用的数组下标超出数组允许范围时,抛出该异常
4、ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出该异常
5、NumberFormatException:当应用程序试图将字符串转换成一种数值类型,但该字符串不能转
换为适当格式时,抛出该异常。
异常常见的方法
//异常最常用的方法,用于将当前错误信息输出到控制台
System.out.println("程序开始了");
try {
String str = "abc";
System.out.println(Integer.parseInt(str));
}catch(Exception e){
e.printStackTrace();//输出错误信息有助于我们修补bug
//获取错误消息,一般用于提示给用户或者记录日志的时候使用
String message = e.getMessage();
System.out.println(message);
System.out.println("出错了,正在解决。。。");
}
System.out.println("程序结束了");
// 程序开始了
For input string: "abc"
出错了,正在解决。。。
程序结束了
java.lang.NumberFormatException: For input string: "abc"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at exception.ExceptionAPIDemo.main(ExceptionAPIDemo.java:11)
自定义异常
通常用于那些满足语法但是不满足业务场景时的错误。
定义自定义异常需要注意以下问题:
1、 异常的类名要做到见名知义
2、 要继承自Exception(直接或间接继承都可以)
3、提供超类异常提供的所有种类构造器
//测试异常的抛出
public class Person {
private int age;
public int getAge() {
return age;
}
// 当一个方法使用throws声明异常抛出时,调用此方法的代码片段就必须处理这个异常
(1)
public void setAge(int age) throws IllegalAgeException {
if(age<0||age>100){
//抛出自定义异常
throw new IllegalAgeException("年龄超范围:"+age);
}
this.age = age;
}
(2)
public void setAge(int age) throws Exception {
if(age<0||age>100){
//使用throw对外抛出一个异常
throw new Exception("年龄不合法!");
}
this.age = age;
}
(3)
public void setAge(int age){
if(age<0||age>100){
//除了RuntimeException之外,抛出什么异常就要在方法上声明throws什么异常
throw new RuntimeException("年龄不合法!");
}
this.age = age;
}
}