以内存为参照,读进内存为输入,反之为输出
🍁以流的方向进行分类:
🍁按照读取数据方式不同进行分类
如:file.txt文件,内容:a中国bcdef,a在Windows中占一个字节,中占两个字节,则:
字符流: a–>中–>国…
字节流:a–>中字符的一半–>中的另一半
🍺Java中的IO流已经写好了,都在java.io.*下
类名 | 流 | 是否抽象类 |
---|---|---|
java.io.InputStream | 字节输入流 | √ |
java.io.OutputStream | 字节输出流 | √ |
java.io.Reader | 字符输入流 | √ |
java.io.Writer | 字符输出流 | √ |
在Java中,只要类名以Stream结尾,都是字节流。只要类名以Reader/Writer结尾的都是字符流。 如InputStreamReader即字符流
🍁
文件字节输入流,万能的,可读任何类型的文件(硬盘–>内存)
- 通过构造方法创建文件字节输入流对象
//注意路径在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();//读完以后返回-1if(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);//4System.out.println(readCount1);//abcdSystem.out.println(new String(bytes));int readCount2 = fileInputStream.read(bytes);//2System.out.println(readCount2);//efcdSystem.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);
文件字节输出流,负责写,从内存到硬盘。
通过构造方法创建文件字节输出流对象
加不加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);//将数组中的内容全部写到tempFilefos.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");//一次读1Mbyte[] 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分开trytry {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
文件字符输入流,只能读取普通文本。
//用法和类中的方法,类比FileInputStreamFileReader 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();}
}
文件字符输出流,将文本从内存写到磁盘,只能传输普通文本,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;//循环一次1Mchar[] 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文件
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);
}
带有缓冲的字符输出流
FileWriter fileWriter = new FileWriter("temp.txt");
BufferedWriter bw = new BufferedWriter(fileWriter);
bw.write("hello world!");
bw.write("\n");
bw.flush();
//只关闭最外层
bw.close()
传入FileOutputStream时,用转换流
前面提到: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")));
数据专属流,这个流可以将数据连同数据的类型一并写入文件(那么这个文件就不是普通的文本文件了,用记事本打开就会乱码)
//注意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();
数据字输入流,DataOutputStream写的文件,只能使用DataInputStream去读,且读的时候需要提前知道写入的顺序(读的顺序和写的顺序一致,才能正常取出数据)
DataInputStream dis = new DataInputStream(new FileInputStream("tempData"));
//注意和写的顺序一致
byte b1 = dis.readByte();
int i1 = dis.readInt();
标准的字节输出流,默认输出到控制台,标准输出流不需要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();}}
}
标准输出流之字符流,用法参照PrintStream
🍁 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】
🍁序列化:
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 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 objList = (List) 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后,手动在代码中写一个固定的序列化版本号,别自动生成(自动生成的不显示,且代码修改编译后,序列化版本号会改变)。
//有文件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是专门存放属性配置文件内容的一个类。