文章目录
- 一、IO流的概述
- 1、流
- 2、流的分类
- 3、Java IO流的四大块
- 4、流的两大特性
- 5、java.io包下的16个常用流
- 二、文件专属流
- 1、java.io.FileInputStream
- 2、java.io.FileOutputStream
- 3、java.io.FileReader
- 4、java.io.FileWriter
- 三、缓冲流与转换流
- 1、java.io.BufferedReader
- 2、java.io.BufferedWriter
- 3、java.io.InputStreamReader
- 四、数据流
- 1、java.io.DataOutputStream
- 2、java.io.DataInputStream
- 五、标准输出流
- 1、java.io.PrintStream
- 2、java.io.PrintWriter
- 六、对象专属流与File类
- 1、File类
- 2、File类的常用方法
- 3、对象流
- 4、IO与Properties联合使用
一、IO流的概述
1、流
以内存为参照,读进内存为输入,反之为输出
2、流的分类
🍁以流的方向进行分类:
- 以内存为参照物,往内存中写,称为输入、Input、读
- 从内存中出来,叫输出、Output、写
🍁按照读取数据方式不同进行分类
- 有的流按照字节的方式读取数据,一次读取一个字节byte,即8位bit,这种流是万能的,可读取任何类型的文件,如文本、图片、声音文件、视频文件。称字节流
- 有的流按照字符的方式读取数据,一次读取一个字符,这种流方便了对普通文本的读取,不能读取图片、声音、视频等文件,只能读取纯文本,Word文件都无法读取。称字符流
如:file.txt文件,内容:a中国bcdef,a在Windows中占一个字节,中占两个字节,则:
字符流: a–>中–>国…
字节流:a–>中字符的一半–>中的另一半
🍺Java中的IO流已经写好了,都在java.io.*下
3、Java IO流的四大块
类名 | 流 | 是否抽象类 |
---|---|---|
java.io.InputStream | 字节输入流 | √ |
java.io.OutputStream | 字节输出流 | √ |
java.io.Reader | 字符输入流 | √ |
java.io.Writer | 字符输出流 | √ |
在Java中,只要类名以Stream结尾,都是字节流。只要类名以Reader/Writer结尾的都是字符流。 如InputStreamReader即字符流
4、流的两大特性
🍁
- 所有流都实现了java.io.Closeable接口,都是可关闭的,都有close()方法,流就像一个管道,是内存和硬盘之间的通道,用完一定要关掉,不然会占用很多资源
- 所有的输出流都是实现了java.io.Flushable接口,都是可刷新的,都有flush()方法。输出流在最终输出后,一定要用flush()刷新下,这个刷新表示将管道/通道中剩余未输出的数据强行输出完,清空管道,没flush()可能会导致丢数据
5、java.io包下的16个常用流
二、文件专属流
1、java.io.FileInputStream
文件字节输入流,万能的,可读任何类型的文件(硬盘–>内存)
- 通过构造方法创建文件字节输入流对象
//注意路径在IDEA中\变\\,前面那个\表示转义
//你直接手输G:/JAVA/old files也行
//直接new报错,因为有异常未处理
FileInputStream fileInputStream = new FileInputStream("G:\\JAVA\\old files");
- read()方法
try{
fileInputStream = new FileInputStream("G:\\JAVA\\old files");
while(true){
int readData = fileInputStream.read();
//读完以后返回-1
if(readData == -1){
break;
}
System.out.println(readData);
}
}catch(FileNotFoundException e) {
e.printStackTrace();
//处理read方法的异常
}catch(IOException e){
e.printStackTrace();
}....
以上使用while(true) + if–break可以优化为:
int readData = 0;
while( (readData = fileInputStream.read() ) != -1){
System.out.println(readData);
}
以上,使用read()方法从输入流中一次读取一个字节,这样内存和硬盘之间的交互太频繁,耗费不必要的资源。
- read(byte[ ] b)方法
从输入流中将最多b.length个字节的数据先读入一个byte数组中
try{
fileInputStream = new FileInputStream("G:\\JAVA\\test.txt");
byte[] bytes = new byte[4];
//注意传入数组时read返回的是读取到的字节数量
//不是字节本身
int readCount1 = fileInputStream.read(bytes);
//4
System.out.println(readCount1);
//abcd
System.out.println(new String(bytes));
int readCount2 = fileInputStream.read(bytes);
//2
System.out.println(readCount2);
//efcd
System.out.println(new String(bytes));
int readCount3 = fileInputStream.read(bytes);
//-1,表示一个都没有读到
System.out.println(readCount3);
System.out.println(new String(bytes));
...
过程分析:
运行结果:
程序优化:
//利用String的构造方法,传入数组,转为String
System.out.println(new String(bytes));
//改为用另一个构造方法,传入启示下标和长度
//使用readCount,即可“读到多少个,转多少个”
System.out.println(new String(bytes,0,readCount));
FileInputStream最终版代码:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class FileInputStreamTest {
public static void main(String[] args) {
//try域中的fileInputStream变量在finally域中识别不到
//所以在外面定义
FileInputStream fis = null;
try {
fis = new FileInputStream("G:\\JAVA\\test.txt");
byte[] bytes = new byte[6];
int readCount = 0;
while((readCount = fis.read(bytes)) != -1){
System.out.println(new String(bytes,0,readCount));
}
}catch(FileNotFoundException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
//在finally语句中确保流一定被关闭
}finally{
//关闭的前提是流不为空
if(fis != null){
try{
fis.close();
//这里处理close方法的异常
}catch(IOException e){
e.printStackTrace();
}
}
}
}
}
- FileInputStream类中的其他常用方法
🍁int available()方法
返回流当中剩余的没有读到的字节数量
System.out.println("流中的总字节数:" + fileInputStream.available());
//读一个
int data = fileInputStream.read();
//流中剩余的字节数
System.out.println(fileInputStream.available());
有了总字节数,我们就可以创建一个对应长度的byte数组,这样直接一次性拿完,不用再根据read的返回值来写循环了
byte[] byte1 = new byte[fileInputStream.available()];
int readCount = fileInputStream.read(byte1);
System.out.println(new String(byte1));
//但byte数组不能太大,所以以上不适用于大文件。
🍁long skip()方法
跳过几个字节不取
fis = new FileInputStream("G:\\JAVA\\test.txt");
fis.skip(2);
2、java.io.FileOutputStream
文件字节输出流,负责写,从内存到硬盘。
通过构造方法创建文件字节输出流对象
加不加true传参,就像Linux中的 > 和 >>的区别:
//tempFile如果不存在,运行程序会自动创建
//不加参数true,调用write方法会清空文件原来的内容
fos = new FileOutputStream("tempFile");
//append参数传true,则write方法是追加而不是清空重写
fos = new FileOutputStream("tempFile",true);
write()方法
FileOutputStream fos = null;
//即abcd
byte[] bytes = {97,98,99,100};
try{
fos = new FileOutputStream("tempFile",true);
//将数组中的内容全部写到tempFile
fos.write(bytes);
//只要bytes数组中的前两位
fos.write(bytes,0,2);
fos.flush();
}catch(FileNotFoundException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(fos != null){
fos.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
String s = "这是个字符串";
//String转byte数组
byte[] bs = s.getBytes();
fos.write(bs);
fos.flush();
🍺🍺🍺综合练习:复制D盘文件1.avi到C盘
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class Copy {
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("D:\\course\\1.avi");
fos = new FileOutputStream("C:\\1.avi");
//一次读1M
byte[] bytes = new byte[1024*1024];
int readCount = 0;
while ((readCount = fis.read(bytes)) != -1) {
//在循环条件中读,在循环体中写
fos.write(bytes,0,readCount);
}
//输出流最后要刷新
fos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
//注意这里fis和fos分开try
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意最后:
√ 如果把fis和fos的关闭流分支写在一起:
...
} finally {
try {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
...
当fis出现异常的时候,fos的关闭流代码就执行不了了,所以分开try…catch
3、java.io.FileReader
文件字符输入流,只能读取普通文本。
//用法和类中的方法,类比FileInputStream
FileReader reader = null;
try{
reader = new FileReader("D:\\1.txt");
char[] chars = new char[4];
int readerCount = 0;
while( (readerCount = reader.read(chars)) != -1){
System.out.println(new String(chars,0,readerCount));
}
}catch(FileNotFoundException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(reader != null){
reader.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
4、java.io.FileWriter
文件字符输出流,将文本从内存写到磁盘,只能传输普通文本,word文件不是普通文本。
与字节输出流不同的是:字符输出流的write方法可以直接传字符串,也能写成功
FileWriter fw = null;
try{
fw = new FileWriter("temp.txt");
char[] chars = {'字','符','输','出'};
fw.write(chars);
fw.write(chars,0,2);
//write(String str)
fw.write("可以直接写字符串了");
}catch(IOException e){
e.printStackTrace();
}finally{
....
🍺🍺🍺综合练习:拷贝普通文本文件
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class TxtCopy {
public static void main(String[] args) {
FileReader fr = null;
FileWriter fw = null;
try{
fr = new FileReader("D:\\course\\HelloWorld.java");
fw = new FileWriter("E:\\HelloWorld_copy.java");
int charCount = 0;
//循环一次1M
char[] chars = new char[1024*512];
while((charCount = fr.read(chars)) != -1){
fw.write(chars,0,charCount);
}
}catch(IOException e){
e.printStackTrace();
}finally {
try{
if(fr != null){
fr.close();
}
}catch(IOException e){
e.printStackTrace();
}
try{
if(fw != null){
fw.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
}
注意:
能用记事本编辑的都是普通文本文件,如xx.java,并不是单指txt文件
三、缓冲流与转换流
1、java.io.BufferedReader
BufferedReader类的构造方法:
即带有缓冲区的字符输入流,使用这个流不用自定义char数组,自带缓冲
FileReader fileReader = new FileReader("D:\\1.txt");
BufferedReader br = new BufferedReader(fileReader);
//注意,根据源码,BufferedReader只能传字符流,不能传字节流,对应的
当一个流的构造方法中需要传入另一个流的时候,这个被传进来的流称为节点流,如上面的FileReader,外部负责包装这个流的,称包装流,也称处理流。如上面的BufferedReader
🍁
br.close();
对于包装流来说,只需要关闭最外层的流就好,里面的节点流有底层源码去自动关闭。
readLine()方法
String firstLine = br.readLine();
//readLine方法不带换行,所以这里选择println
System.out.println(firstLine);
String s =null;
//读全部
while((s = br.readLine()) != null){
System.out.println(s);
}
2、java.io.BufferedWriter
带有缓冲的字符输出流
FileWriter fileWriter = new FileWriter("temp.txt");
BufferedWriter bw = new BufferedWriter(fileWriter);
bw.write("hello world!");
bw.write("\n");
bw.flush();
//只关闭最外层
bw.close()
传入FileOutputStream时,用转换流
3、java.io.InputStreamReader
前面提到:BufferedReader的构造方法只能传字符流,不能传字节流,可通过转换流转字节流为字符流:
FileInputStream in = new FileInputStream("D:\\course\\1.txt");
//这里reader是包装流
//字节流转为字符流
InputStreamReader reader = new InputStreamReader(in);
BufferedReader bfReader = new BufferedReader(reader);
合并代码:
BufferedReader bfReader = new BufferedReader(new InputStreamReader(new FileInputStream("D:\\course\\1.txt")));
四、数据流
1、java.io.DataOutputStream
数据专属流,这个流可以将数据连同数据的类型一并写入文件(那么这个文件就不是普通的文本文件了,用记事本打开就会乱码)
//注意DataOutputStream的构造方法中传入一个字节流做为节点流
DataOutputStream dos = new DataOutputStream(new FileOutputStream("tempData"));
byte b = 100;
int i = 300;
char c = 'a';
//把数据类型一并写入文件中
dos.writeByte(b);
dos.writeInt(i);
dos.writeChar(c);
dos.flush();
2、java.io.DataInputStream
数据字输入流,DataOutputStream写的文件,只能使用DataInputStream去读,且读的时候需要提前知道写入的顺序(读的顺序和写的顺序一致,才能正常取出数据)
DataInputStream dis = new DataInputStream(new FileInputStream("tempData"));
//注意和写的顺序一致
byte b1 = dis.readByte();
int i1 = dis.readInt();
五、标准输出流
1、java.io.PrintStream
标准的字节输出流,默认输出到控制台,标准输出流不需要close()
//System.out.println
PrintStream ps = System.out;
ps.println();
总结回顾之前System类中的方法
- System.gc()
- System.currentTimeMills()
- System.exit(0);
- System.arrayCopy()
//传入一个文件字节输出流做为节点流
PrintStream printStream = new PrintStream(new FileOutputStream("log"));
//标准输出流不再指向控制台,修改成了log文件
System.setOut(printStream);
//再输出
System.out.println("hello");
System.out.println("log");
//这时候就输出到文件中去了
以上也是日志输出的一个实现思路:
/**
* 日志记录
*/
public class Logger {
public static void main(String[] args) {
Logger.log("System is ready!");
}
public static void log(String msg){
try {
PrintStream printStream = new PrintStream(new FileOutputStream("log.txt",true));
System.setOut(printStream);
//日期
Date nowTime = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(nowTime);
System.out.println(strTime + ":" + msg);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
2、java.io.PrintWriter
标准输出流之字符流,用法参照PrintStream
六、对象专属流与File类
1、File类
- java.io.File类的父类是java.lang.Object
- File对象是文件和目录路径名的抽象表现形式。如C:\course是一个File对象,也可能是文件,也可能是目录
- File类和四大流没有关系,所以File类不能完成文件的读和写
2、File类的常用方法
🍁 exists()方法
//判断文件是否存在
File f1 = new File("D:\\file");
System.out.println(f1.exists());
🍁 createNewFile()和mkdir()
//若D:\\file不存在,则以文件的形式创建
if(!f1.exists()){
f1.createNewFile();
}
//以目录的形式创建
if(!f1.exists()){
f1.mkdir();
}
注意:
当f1对象中的路径是多重目录时,mkdir方法变为mkdirs()
🍁getParent()方法
//获取文件的父路径
File f2 = new File("D:\\course\\src\\1.txt");
// D:\course\src
String parentPath = f2.getParent();
File parentPathFile = f2.getParentFile();
相对应的,有个getAbsolutePath() 方法:
//获取绝对路径
File f3 = new File("FileTest.java");
System.out.println(f3.getAbsoluteFile());
System.out.println(f3.getAbsolutePath());
🍁getName()方法
//获取文件名
File f2 = new File("D:\\course\\src\\1.txt");
//1.txt
System.out.println(f2.getName());
🍁isDirectory()和isFile()方法
//判断是文件/目录
File f2 = new File("D:\\course\\src\\1.txt");
//false
System.out.println(f2.isDirectory());
🍁lastModified()方法
File f2 = new File("D:\\course\\src\\1.txt");
//从1970年0:0到现在的总毫秒数
Long times = f2.lastModified();
Date time = new Date(times);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(time);
System.out.println(strTime);
🍁length()方法
//获取文件大小
Long size = f2.length();
🍁File[ ] listFiles()方法
//获取当前目录下的所有子文件,返回一个File[]数组
File f = new File("D:\\course");
File[] files = f.listFiles();
//增强for循环获取子文件的绝对路径
for(File fObj:files){
System.out.println(fObj.getAbsoluteFile());
}
❀❀❀练习–目录拷贝
/**
* 业务功能:实现任意两个目录间文件的拷贝
*/
import java.io.*;
public class CopyAll {
public static void main(String[] args) {
File src = new File("D:\\course");
File dest = new File("E:\\copy");
copyDir(src,dest);
}
public static void copyDir(File srcFile,File destFile){
/**
* 源文件是一个文件
*/
if(srcFile.isFile()){
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream(srcFile);
/**
* 三目运算符,判断目标路径结尾是不是\
* 不是则加\,并截取源目录除盘符以外的目录拼接给它
*/
String path = destFile.getAbsolutePath().endsWith("\\") ?
destFile.getAbsolutePath():
destFile.getAbsolutePath() + "\\" + srcFile.getAbsolutePath().substring(3);
if(!new File(path).exists()){
new File(path).getParentFile().mkdirs();
}
out = new FileOutputStream(path);
byte[] bytes = new byte[1024*1024];
int readCount = 0;
while( (readCount = in.read(bytes)) != -1 ){
out.write(bytes,0,readCount);
}
out.flush();
} catch(FileNotFoundException e) {
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(in != null){
in.close();
}
}catch(IOException e){
e.printStackTrace();
}
try{
if(out != null){
out.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
System.out.println("拷贝完成,进度100%!");
//如果源文件是文件,则一定不是目录,下面的目录复制
//下面的目录复制不必再执行
return;
}
/**
* 能执行到这儿说明源文件是一个目录
* 这里要是再加if分支判断是否为目录,则上面的return就不必了
* 但要么文件要么目录,所以直接上面return了
*/
//获取源目录下的所有子文件
File[] files = srcFile.listFiles();
for(File file:files){
if(file.isDirectory()){
String destPath = (destFile.getAbsolutePath().endsWith("\\") ?
destFile.getAbsolutePath():
destFile.getAbsolutePath() + "\\" + srcFile.getAbsolutePath().substring(3));
File newFile = new File(destPath);
if(!newFile.exists()){
newFile.mkdirs();
}
}
//执行到这里,说明源目录的子文件是一个文件,不是目录
// 递归调用copy就行
copyDir(file,destFile);
}
System.out.println("拷贝完成,进度100%!");
}
}
在刚开始调代码的时候遇到一个error:
error:
java.io.FileNotFoundException:.\xxx\xxx.txt (系统找不到指定的路径。)
java.io.FileNotFoundException: E:\xx\xx (拒绝访问。)
分析:
在构造一个File对象时,指定的文件路径是什么都可以,就算不存在也能够构造File对象,但是,现在你要对文件进行输入输出操作,也就是InputStream和OutputStream操作时,如果填写的路径不存在,那么就会报系统找不到指定路径,如果指定的是xxx\,就会报拒绝访问异常。
具体参考【BUG】
3、对象流
🍁序列化:
Serialize,Java对象存储到文件中,将Java对象的状态保存下来的过程,用ObjectOutputStream实现。
🍁反序列化:
DeSerialize,将硬盘上的数据重新恢复到内存中,恢复成Java对象
public class Students implements Serializable{
private int age;
.....
}
参与序列化和反序列化的对象,必须实现Serializable接口 ,否则报错java.io.NotSerializableException
//Serializabl接口的源码就这两行
public interface Serializable {
}
Serializable接口中没有任何方法,是一个标志性接口。标志性接口,起到标识作用,Java虚拟机看到这个类实现了这个接口,可能会对这个类进行特殊待遇,即为该类自动生成一个序列化版本号。
//定义实现序列化接口的Students类
class Students implements Serializable{
int no;
String name;
public Students(){
}
public Students(int no,String name){
this.no = no;
this.name = name;
}
}
Students s = new Students(111,"llg");
//序列化,将对象存到硬盘,文件名称students
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("students"));
oos.writeObject(s);
oos.flush();
//将硬盘中的文件students反序列化成对象,读进内存
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("students"));
Object obj = ois.readObject();
System.out.println(obj);
对象序列化后的文件,用txt查看乱码:
🍁序列化多个对象和反序列化多个对象:
把对象放到集合中,writeObject方法中传入一个集合
List<Students> studentsList = new ArrayList<>();
studentsList.add(new Students(02,"A"));
studentsList.add(new Students(03,"B"));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("studentsList"));
//传入了一个List集合
//ArrayList类已经实现了Serializable
oos.writeObject(studentsList);
反序列化多个对象:
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("studentsList"));
//readObject方法返回的是Object类型,更细分析这里是一个List,故强转
List<Students> objList = (List<Students>) ois.readObject();
for(Students objStudents:objList){
System.out.println(objStudents);
}
🍁transient关键字
transient关键字表示游离的,不参与序列化。
private transient String name;
即对象的name属性序列化的时候不会再序列化到文件中,则此后再反序列化,需要new对象时name就无值,出现默认值null
🍁IDEA生成序列化版本号
private static final Long serialVersionUID = -7876415467956846468432L;
❀自动生成序列化版本号的缺陷:
一旦代码后续修改,重新编译会生成全新的序列化版本号。而序列化版本号是用来区分类的,故JVM会认为这是一个全新的类。再反序列化的时候就会报错。
因此:实现Serializable后,手动在代码中写一个固定的序列化版本号,别自动生成(自动生成的不显示,且代码修改编译后,序列化版本号会改变)。
4、IO与Properties联合使用
//有文件userinfo.txt
username=root
password=123qweASD
将userinfo.txt文件中的数据加载到Properties对象中(温习:Properties是一个Map集合,其key和value都是String类型)
FileReader reader = new FileReader("userinfo.txt");
Properties pro = new Properties();
//Properties对象的load方法,将文件中的数据加载到Map集合中
//其中,等号左边为key,右边为value
pro.load(reader);
//由key取value
String value = pro.getProperty("username");
对于经常变动的数据,可以单独写到一个文件中(即配置文件),使用程序动态读取。后续修改只需改配置文件,不用改代码,不用重新编译。
当配置文件的内容格式是key=value的时候,称为属性配置文件,文件名常以.properties结尾。=也可用:且=左右两边最好别加空格。
Properies是专门存放属性配置文件内容的一个类。