目录
1、IO流,什么是IO?
I :Input ,O :Output
通过IO可以完成硬盘文件的读和写。
2、IO流的分类?
有多种分类方式:
一种方式是按照流的方向进行分类:
以内存作为参照物,
往内存中去,叫做输入(Input),或者叫做读(Read)。
从内存中出来,叫做输出(Output),或者叫做写(Write)。
另一种方式是按照读取数据方式不同进行分类:
有的流是按照字节的方式读取数据,一次读取1个字节byte,等同于一次读取8个二进制位。这种流是万能的,什么类型的文件都可以读取。包括:文本文件、图片、声音文件、视频文件等.....
假设文件file.txt,采用字节流的话是这样读的:
a中国ba张三fe
第一次读:一个字节,正好读到'a'
第二次读:一个字节,正好读到‘中’字符的一半。
第三次读:一个字节,正好读到‘中’字符的另外一半。
有的流是按照字符的方式读取数据的,一次读取一个字符,这种流是为了方便普通文本文件而存在的,这种流不能读取:图片、声音、视频等文件。只能读取文本文件,连word文件都无法读取。
假设文件file.txt,采用字符流的话是这样读的:
a中国ba张三fe
第一次读:‘a’字符(‘a’字符在Windows系统中占用1个字节。)
第二次读:‘中’字符(‘中’字符在Windows系统中占用2个字节。)
【综上所述】:流的分类
输入流、输出流、字节流、字符流。
3、java中所有的流都是在:java.io.*;下。
java主要研究:
怎么new流对象。调用流对象的哪个方法是读,哪个方法是写。
4、IO流共有四大类:(这四个都是抽象类【abstract class】)
java.io.InputStream 字节输入流
java.io.OutputStream 字节输出流
java.io.Reader 字符输入流
java.io.Writer 字符输出流
所有的流都实现了:
java.io.Closeable接口,都是可以关闭的,都是close()方法。流毕竟是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,不然会浪费(占用)很多资源。养成好习惯,用完流一定要关闭。
所有的输出流都实现了:
java.io.Flushable接口,都是可刷新的,都有flush()方法,养成一个好习惯,输出流在最终输出之后,一定要及得flush()刷新一下。这个刷新表示将通道/管道当中剩余未输出数据强行输出完(清空管道!)。刷新的作用就是清空管道。
【注意】:如果没有flush()可能会导致丢失数据。
【注意】:在java中只要“类名”以Stream结尾的都是字节流,以“Reader/Writer”结尾的都是字符流。
5、java.io包下需要掌握的流有16个:
文件专属
java.io.FileInputStream
1、文件字节输入流,万能的,任何类型的文件都可以采用这个流来读。
2、字节的方式,完成输入的操作,完成读的操作(硬盘 --> 内存)
FileInputStream fis = null;
try {
fis = new FileInputStream("src/2.txt");
// 开始读
int readData = fis.read(); // 这个方法的返回值是:读取到的“字节”本身。
System.out.println(readData); // 97
System.out.println(fis.read()); // 98
System.out.println(fis.read()); // 99
// 已经读取到文件的末尾了,再读的时候取不到任何数据,返回-1
System.out.println(fis.read()); // -1
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 在finally语句块当中确保流一定关闭!!!
if (fis != null) { // 避免空指针异常!!!
// 关闭流的前提是:流不是空。流是null的时候没必要关闭。
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
解析:上面2.txt文件,只包括abc这三个字母。
abc
read();这个方法的返回值是:读取到的“字节”本身。读取到文件的末尾了,再读的时候取不到任何数据,返回-1。记住要关闭流,避免浪费资源。
读取数据时,可以使用while语句改进上面的程序
int readData = 0;
while ((readData = fis.read()) != -1) {
System.out.println(readData);
}
分析上面程序存在的缺点:
一次读取一个字节byte,这样内存和硬盘交互太频繁。基本上时间/资源都耗费在交互上面了。可以的一次读取多个字节。
int read(byte[] b); 一次最多读取 b.length 个字节。
减少硬盘和内存的交互,提高程序的执行效率。往byte[]数组当中读。
FileInputStream fis = null;
try {
fis = new FileInputStream("src/2.txt");
// 开始读,采用byte数组,一次读取多个字节。最多读取“数组.length”个字节。
byte[] bytes = new byte[2]; // 准备一个2个长度的byte数组,一次最多读取2个字节。
// 这个方法的返回值是:读取到的字节数量。(不是字节本身。)
int readCount = fis.read(bytes);
System.out.println(readCount); // 2 第一次读到了2个字节。
// 不应该全部转换,应该是读取了多少个字符,转换多少个。
System.out.println(new String(bytes, 0, readCount)); // ab
System.out.println(fis.read(bytes)); // 1
System.out.println(new String(bytes, 0, readCount)); // c
// 1个字节都没有读取到返回-1
System.out.println(fis.read(bytes)); // -1
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
【注意】:将数组中字节转换的时候,不能new String(bytes)全部转换,应该是读取到多少字节转换多少。
读取数据时,可以使用while语句改进上面的程序
int readCount = 0;
while ((readCount = fis.read(bytes)) != -1) {
System.out.print(new String(bytes, 0, readCount));
}
附加内存图
FileInputStream类的其他常用方法:
int available(); 返回流当前中剩余的没有读到的字节数量
long skip(long n); 跳过几个字节不读。
java.io.FileOutputStream
文件字节输出流,负责写。从内存到硬盘。
FileOutputStream fos = null;
try {
// 已追加的方式在文件末尾写入,不会清空文件内容。
fos = new FileOutputStream("src/myfile", true);
byte[] bytes = {97, 98, 99, 100};
// 将byte数组全部写出!
fos.write(bytes); // abcd
// 将byte数组的一部分写出!
fos.write(bytes, 0, 2); // ab
String s = "我是一个中国人!";
// 将字符串转换为byte数组
byte[] bs = s.getBytes();
fos.write(bs); // 写
fos.flush();// 写完之后,最后一定要刷新!
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
也可以使用下面这种方式new FileOutputStream对象
FileOutputStream fos = new FileOutputStream("src/myfile");
但是不推荐使用,这种方式会先将原文件清空,然后重新输入!myfile文件不存在的时候会自动新建!
【注意】:写完之后,最后一定要刷新!
java.io.FileReader
文件字符输入流,只能读取普通文本。读取文本内容时,比较方便,快捷。
FileReader reader = null;
try {
// 创建文件字符输入流
reader = new FileReader("src/2.txt");
char[] chars = new char[2]; // 一次读取2个字符
int readCount = 0;
while ((readCount = reader.read(chars)) != -1) {
System.out.print(new String(chars, 0, readCount));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
read(chars); 往char数组中读,按照字符的方式读取:第一次a,第二次b
java.io.FileWriter
文件字符输出流,写。只能输出普通文本。
FileWriter out = null;
try {
// 创建文件字符输出流对象
out = new FileWriter("src/myfile", true);
out.write("我是好人!"); // 开始写
out.flush(); // 刷新
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
缓存流和数据流
转换流:(将字节流转换成字符流)
java.io.InputStreamReader
java.io.OutputStreamWriter
缓冲流专属:
java.io.BufferWriter
带缓冲的字符输出流
public class BufferedWriterTest {
public static void main(String[] args) {
BufferedWriter out = null;
try {
// 带缓冲区的字符输出流
out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("src/myfile")));
// 开始写。
out.write("hello world!");
out.write('\n');
out.write("hello kitty!");
out.flush(); // 刷新
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
java.io.BufferReader
带有缓冲流的字符输入流。使用这个流的时候不需要自定义char数组,或者byte数组,自带缓冲。
public class BufferedReaderTest {
public static void main(String[] args) {
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(new FileInputStream("src/2.txt")));
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
解析:当一个流的构造方法需要一个流的时候,这个被传进来的流叫做:节点流。
外部负责包装的这个流,叫做:包装流,还有一个名字叫做,处理流。
对于上面代码来说,可以拆分为
FileInputStream fis = new FileInputStream("src/2.txt");
InputStreamReader reader = new InputStreamReader(fis); // fis是节点流,reader是包装流。
br = new BufferedReader(reader);// reader是节点流,br是包装流。
fis是节点流,br是包装流,而reader是包装流也是节点流。
java.io.BufferInputStream
java.io.BufferOutputStream
数据流
java.io.DataInputStream
数据字节输入流。DataOutputStream写的文件,只能使用DataOutputStream去读,并且读的时候需要提前知道写入的顺序。读的顺序需要和写的顺序一致,才可以正常读取出数据。
public class DataInputStreamTest {
public static void main(String[] args) {
DataInputStream dis = null;
try {
dis = new DataInputStream(new FileInputStream("src/data"));
// 开始读
byte b = dis.readByte();
short s = dis.readShort();
int i = dis.readInt();
long l = dis.readLong();
float f = dis.readFloat();
boolean sex = dis.readBoolean();
char c = dis.readChar();
System.out.println(b);
System.out.println(s);
System.out.println(i);
System.out.println(l);
System.out.println(f);
System.out.println(sex);
System.out.println(c);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (dis != null) {
try {
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
java.io.DataOutputStream
数据字节输出流,这个流可以将数据连同数据的类型一并写入文件。
【注意】:这个文件不是普通文本文件。(这个文件使用记事本打不开。)
public class DataOutputStreamTest {
public static void main(String[] args) {
DataOutputStream dos = null;
try {
dos = new DataOutputStream(new FileOutputStream("src/data"));
// 写数据
byte b = 100;
short s = 200;
int i = 300;
long l = 400L;
float f = 3.0F;
boolean sex = false;
char c = 'c';
// 写
dos.writeByte(b); // 把数据以及数据的类型一并写入到文件中。
dos.writeShort(s);
dos.writeInt(i);
dos.writeLong(l);
dos.writeFloat(f);
dos.writeBoolean(sex);
dos.writeChar(c);
dos.flush(); // 刷新
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (dos != null) {
try {
dos.close(); // 关闭最外层
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
标准输出流
java.io.printWriter
java.io.printStream
标准的字节输出流,默认输出到控制台,可以改变标准输出流的输出方向!
System.out.println("hello world!");
拆开写
PrintStream ps = System.out;
ps.println("hello world!");
【注意】:标准输出流不需要手动close()关闭。
标准输出流不再指向控制台,指向“myfile”文件
PrintStream printStream = null;
try {
printStream = new PrintStream(new FileOutputStream("sec/myfile"));
// 修改输出方向,将输出方向修改到“myfile”文件。
System.setOut(printStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 输出
System.out.println("hello world!");
模拟日志工具
public class Logger {
public static void log(String msg){
PrintStream printStream = null;
try {
// 指向一个日志文件
printStream = new PrintStream(new FileOutputStream("E:/Java/T/log.txt", true));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 改变输出方向
System.setOut(printStream);
// 日期当前时间
Date nowDate = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(nowDate);
System.out.println(strTime + " : " + msg);
}
}
测试
Logger.log("调用了UserService的doSome()方法");
对象专属流
序列化及反序列化
1、java.io.NotSerializableException:对象未序列化!
2、参与序列化和反序列化的对象,必须实现Serializable接口。
3、注意:通过源代码发现,Serializable接口只是一个标志接口:
public interface Serializable {
}
这个接口当中什么代码都没有。那么它起什么作用呢?
起到标志作用,标志的作用,java虚拟机看到这个类实现了这个接口,可能会对这个类进行特殊待遇。Serializable这个标志接口是给Java虚拟机参考的,java虚拟机看到这个接口之后,会为该类自动生成一个序列化版本号。
4、序列化版本号有什么作用呢?
java语言中要是采用什么机制来区分类的?
第一:首先通过类名进行比对的,如果类名不一样,肯定不是同一个类。
第二:如果类名一样,接下来靠序列化版本号进行区分。
不同的人编写了同一个类,但是“这两个类确实不是同一个类”。这时候序列化版本就起上作用了。对于java虚拟机来说,java虚拟机是可以区分这两个类的,因为这两个类都是实现了Serializable接口,都有默认的序列化版本号,他们的序列化版本号不一样,所以区分开了。(这是自动生成序列化版本号的好处。)
这种自动生成的序列化版本号有什么缺陷?
这种自动生成的序列化版本号的缺点是:一旦代码确定之后,不能进行后续的修改,因为只要修改,必然会重新编译,此时会生成全新的序列化版本号,这个时候java虚拟机会认为这是一个全新的类。(这样就不好了!)
【结论】:
凡是一个类实现了Serializable接口,建议给该类提供一个固定的序列化版本号。这样,以后这个类即使代码修改了,但是版本号不变,java虚拟机会认为是同一个类。
public class Student implements Serializable {
private static final long serialVersionUID = 1L; // java虚拟机识别一个类的时候先通过类名,如果类名一致,再通过序列化版本号。
private int in;
private String name;
public Student() {
}
public Student(int in, String name) {
this.in = in;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"in=" + in +
", name='" + name + '\'' +
'}';
}
}
java.io.ObjectInputStream
反序列化
public class ObjectInputStreamTest {
public static void main(String[] args) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("src/myfile"));
// 开始序列化,读
Object obj = ois.readObject();
// 反序列化回来是一个学生对象,所以会调用学生对象的toString()方法。
System.out.println(obj);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
java.io.ObjectOutputStream
序列化
public class ObjectOutputStreamTest {
public static void main(String[] args) {
// 创建java对象
Student s = new Student(1111, "zhangsan");
// 序列化
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("src/myfile"));
oos.writeObject(s); // 序列化对象
oos.flush(); // 刷新
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();// 关闭
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
一次序列化多个对象呢?可以,可以将对象放到集合当中,序列化集合。
提示:参与序列化的ArrayList集合以及集合中的元素User都需要实现java.io.Serializable接口。
附加序列化及反序列化内存图
File类
1、File类和IO流的四大类没有关系,所以File类不能完成文件的读和写。
2、File对象代表什么?
文件的目录路径名的抽象表示形式。
一个Fiel对象有可能对应的是目录,也有可能是文件。File只是一个路径名的抽象表示形式。
3、需要掌握File类中的常用的方法。
boolean exists() 测试此抽象路径名表示的文件或目录是否存在。
File f1 = new File("E:\\Java\\J");
System.out.println(f1.exists()); // false
boolean createNewFile() 有该名称的文件不存在时,创建一个由该抽象路径名命名的新的空文件。
f1.createNewFile();
boolean mkdir() 创建由此抽象路径名命名的目录。
f1.mkdir();
boolean mkdirs() 创建由此抽象路径名命名的目录,包括任何必需但不存在的父目录。(多重目录。)
File f2 = new File("E:/Java/a/b/c/d");
if (!f2.exists()) {
f2.mkdirs();
}
String getParent() 返回此抽象路径名的父(上一级)的路径名字符串,如果此路径名未命名为父目录,则返回null。
File f3 = new File("E:\\Java\\T\\2.txt");
System.out.println(f3.getParent()); // E:\Java\T
File getParentFile() 返回此抽象路径名的父(上一级)或抽象路径名,如果此路径名没有指定父目录。
File parentFile = f3.getParentFile();
System.out.println("获取绝对路径:" + parentFile.getAbsolutePath()); // 获取绝对路径:E:\Java\T
String getAbsolutePath() 返回此抽象路径名的绝对路径名字符串
File f4 = new File("src/myfile");
System.out.println("获取绝对路径:" + f4.getAbsolutePath()); // 获取绝对路径:E:\Java\IO流\src\myfile
String getName() 返回由此抽象路径名表示的文件或目录的名称。
File f1 = new File("E:\\Java\\T\\2.txt");
System.out.println("文件名:" + f1.getName()); //文件名:2.txt
boolean isDirectory() 测试此抽象路径名表示的文件是否为目录。
System.out.println(f1.isDirectory()); // false
boolean isFile() 测试此抽象路径名表示的文件是否为普通文件。
System.out.println(f1.isFile()); // true
long lastModified() 返回此抽象路径名表示的文件上次修改的时间。
long haoMiao = f1.lastModified(); // 这个毫秒是从1970年到现在的总毫秒数。
Date time = new Date(haoMiao);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(time);
System.out.println(strTime);
这个方法获取的是毫秒数,从1970年到现在的总毫秒数。
long length() 返回由此抽象路径名表示的文件的长度。
System.out.println(f1.length());
返回的是文件的字节长度。
File[] listFiles() 返回一个抽象路径名数组,表示由该抽象路径名表示的目录中的文件。
File f = new File("E:\\Java\\example");
File[] files = f.listFiles();
IO + Properties 的联合应用
以后进程改变的数据,可以单独写到一个文件中,是程序动态读取。将来只需要修改这个文件的内容,java代码不需要改动,不需要重新编译,服务器也不需要重启,就可以拿到动态的信息。类似于以上机制的这种文件被称为配置文件。
并且当配置文件中的内容格式是:
key1=value
key2=value
的时候,我们把这种配置文件叫做属性配置文件。
java规范中有要求:属性配置文件建议以.properties结尾,但是这不是必须的。这种以.properties结尾的文件在java中被称为:属性配置文件。其中Properties是专门存放属性配置文件内容的一个类。
创建userinfo.properties文件
username=admin
格式:建议key和value之间使用=的方式。在属性配置文件中井号(#)是注释。
测试
public class IoPropertiesTest {
public static void main(String[] args) {
// 新建一个输入流对象
FileReader reader = null;
try {
reader = new FileReader("src/userinfo.properties");
// 新建一个Map集合
Properties pro = new Properties();
pro.load(reader);
// 通过key来获取value
String username = pro.getProperty("username");
System.out.println(username);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
【注意】:Properties是一个Map集合,key和value都是String类型。想将userinfo文件中的数据加载到Properties对象当中。
Properties对象的load方法将文件中的数据加载到Map集合中,其中等号=左边做key,右边做value。