1)什么是IO流?
I 是 input(输入)的缩写,O 是 output(输出)的缩写,流是指数据。
2)什么是输入,什么是输出?
我们的计算机上,有内存和硬盘两个可以存放数据的地方,内存中的数据的临时的,硬盘中的数据的持久的。当我们从硬盘中“读取”(read)数据到内存中使用时,称为“输入”(input);当我们将内存中的数据“写入”(write)到硬盘中存储时,称为“输出”(output)。【注意:“读取”对应“输入”,“写入”对应“输出”。背好这句,后面贼好用!】
3)IO流有哪些分类?
根据流的方向进行分类:输入流和输出流。
根据数据的读取方式进行分类:字节流和字符流。
4)什么是字节流,什么是字符流?
如果对输入和输出理解了,那么对输入流和输出流这种分类方式应该不难理解。那么什么是字节流和字符流呢?
字节流,是指数据的读取方式是一次读取1个字节byte,这种流能够读取任何类型的文件,比如音频、视频、图片等都可以;字符流,是指数据的读取方式是一次读取一个字符,是只为了读取文本文件(仅仅只对 【.txt】 文件)而存在的,因此不能读取音频、视频等类型的文件。举个例子:
假设有一个文本文件,是a.txt,文件中的内容如下:a张三12李四
在windows系统的文本文件中,一个数字、符号或者是字母,其大小是1个字节;一个汉字的大小则会因为编码格式的
不同而占用不同的字节。
假如用字节流读取:第一次读取'a',第二次读取'张'的一部分,第三次读取'张'的另一部分......
假如用字符流读取:第一次读取'a',第二次读取'张',第三次读取'三'......
5)IO流怎么学?
java中IO流对应的多种类都已经写好了,初学者不需要关心其底层原理,只需要掌握java为我们提供了哪些流,流的特点是什么,常用方法有哪些即可。且要知道java中所有的流都是在 java.io.* 下面。
6)先看一下大概的IO流家族,如下图,有加中文的,本篇博客都会介绍到。
上图中,带“File”的是文件流,带“Buffered”的是缓冲流,带“StreamReader”的是转换流,带“Data”的是数据流,
带“Print”的是打印流,带“Object”的是对象流,我们会在后面一一介绍到。
注意:
1)在java中,类名以“Stream”结尾的就都是字节流;以“Reader/Writer”结尾的就都是字符流。
2)InputStream、OutputStream、Reader、Writer都是抽象类。所有的流都实现了Closeable接口,都有close()方法,都是可以关闭的,且流在使用之后是必须关闭的。
3)所有的输出流还实现了Flushable接口,都有flush()方法,每一次使用完输出流之后,先flush()后,再close()。flush()方法可以将没有输出的数据强行输出,防止丢失数据。
1、FileInputStream
FileInputStream,包含“Stream”,说明它是一个字节流;包含“File”,说明它是专门用于读取文件的流。
现在,我们直接用代码来表述,说明这个类的使用。我们创建一个文件 F:\a.txt,文本内容是 1a.中 。注意这个文本内容很具有代表性,1是数字,a是字母,. 是符号,中是汉字。
然后我们尝试将该硬盘文件中的内容读取并打印出来,看看我们能打印出什么。
public class IOTest01 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
//1)先创建流对象,构造方法 FileInputStream() 会抛出异常,需要处理
fis = new FileInputStream("F:\\a.txt");
//4)reaf()方法是读取一个字节。
//在ASCII编码表中,字符'1'对应49,字符'a'对应97,字符'.'对应46
//可以看到,汉字'中'对应3个字节,分别是228、184、173
//当读取到-1后,表示文本已经读取完了,没有其他内容了
int readData = fis.read();
System.out.println(readData); //49
readData = fis.read();
System.out.println(readData); //97
readData = fis.read();
System.out.println(readData); //46
readData = fis.read();
System.out.println(readData); //228
readData = fis.read();
System.out.println(readData); //184
readData = fis.read();
System.out.println(readData); //173
readData = fis.read();
System.out.println(readData); //-1
readData = fis.read();
System.out.println(readData); //-1
//2)FileInputStream() 抛出的异常在这里处理
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) { //5)read()方法也存在异常,因此也需要捕捉
e.printStackTrace();
} finally {
//3)流使用之后,需要关闭,在finally中进行关闭
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
对上述代码的解释,按注释给的顺序阅读代码,很容易懂
对 if(fis!=null) 的解释:
这个判断条件很有必要加,加完之后。
假设fis为空,那么就不需要关闭了,就不会执行 if{ } 中关闭流这些代码;如果fis不为空,就关闭流。
这样做的目的是防止空指针异常。
你想想,如果不加的话,fis为空,即fis这个引用没有指向任何对象,那它怎么调用close()方法,这样就会发空指
针异常。
对read()方法的解释:
read()方法就是“读取”,每次读取一个字节大小的数据,返回一个int类型的字面量。
每读取一个,光标就会往下移动一个字节,以便读取下一个字节。
当读取的返回值是-1时,表示文本已经读完了。
对’中’读取了三个字节的解释:
字母,英文符号,数字只占一个字节。但是汉字的字节数是不确定的,编码格式不同,汉字的字节数也可能会不同。
细心的小伙伴应该有发现到,上面读一个字节,打印一个字节,代码大量重复,因此我们优化一下代码,用循环来读取。代码如下:
public class IOTest01 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("F:\\a.txt");
//1)我们定义一个变量readData,来存放读取到的字节
int readData = 0;
//2)以下代码的意思是,只要读取到的字节不为-1,也就是文本没有读完,就一直循环地读,并把
//读到的值赋值给readData,同时打印readData。
while((readData=fis.read())!=-1){
System.out.println(readData);
}
//!!!注意从现在开始,这行以下代码基本都是相同的,我们不需要再关注它们
//重要事情再说一遍,接下来的重复代码,我们看try{}中的代码即可
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
学会了怎么使用输入流之后,我们反过来,再来看看JDK帮助文档,我们发现,read()方法还有另一个重载方法,如下:
read()方法:这个方法,是将一个字节从硬盘读取内存,再继续读下一个,依次往返读取。这就好比是送外卖,外卖
小哥一次从饭店拿一个饭盒,送给客户,然后又回到饭店,再拿一个饭盒....显然这样做的效率十分低下。
read(byte[] b)方法:这个方法,是将读取到的多个字节存放到字节数组中,然后一次性搬到内存。这就好比外卖小
哥学聪明了,他用大袋子装了很多饭盒,这就不用往复跑很多次了。这样,效率就变高了。
注意:read()的返回值是一个小于256的int类型的数字;read(byte[] b)的返回值表示读到了几个字节数量,比如我们
定义一个byte[1024],也就是能存1024个字节的数组,然后我们的文件大小是1025个字节,那么使用循环读取,第
一次返回的int是1024,第二次是1,第三次是-1。具体看下面的代码。
我们把 a.txt 文件的内容改成 abcde,把文件放到项目下
我们来看一下代码是怎样的:
public class IOTest01 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("a.txt");
//注意这里的变量名从readData变成了readCount,因为使用read(byte[] b)方法,其返回值是
//字节数量,而不是字节这个数据本身
int readCount = 0;
//1)定义一个字节数组,长度为4
byte[] bytes = new byte[4];
//2)将字节4个4个地读取到bytes中
readData = fis.read(bytes);
System.out.println(readData); //4
readData = fis.read(bytes);
System.out.println(readData); //1
readData = fis.read(bytes);
System.out.println(readData); //-1
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
同样的,上述代码也可以用循环进行优化
public class IOTest01 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("a.txt");
int readData = 0;
//1)定义一个字节数组,长度为4
byte[] bytes = new byte[4];
//2)将字节4个4个地读取到bytes中
while((readData=fis.read(bytes))!=-1){
//3)这一步稍作修改,使用String类的构造方法传入一个字节数组,将字节数组中的数据转换成
//字符串,从下标0读到下标为readData的位置
System.out.println(new String(bytes,0,readData));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2、FileInputStream的其他方法
在前面的学习中,我们只学习了FileInputStream的两个方法,read()和close()。现在,我们来了解另外的方法,可能以后会用到。
1)avaliable()方法:该方法返回文件中剩下的字节数量,注意是数量
我们的文本内容依然是 abcde,来看看avaliable()方法有什么妙用
public class IOTest01 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("a.txt");
//1)先不读,光标落在文件的初始位置,因此available()会返回文件的总字节数量,将其作为
//byte[]数组的大小,我们就不需要循环读取了
byte[] bytes = new byte[fis.available()];
fis.read(bytes);
System.out.println(new String(bytes));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2)skip()方法:跳过指定数量的字节,不读
public class IOTest01 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("a.txt");
fis.skip(3); //跳过3个,我们推测接下来读到的是d,而不是a
System.out.println(fis.read()); //100.ASCII编码表中,字符'd'对应100.
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3、相对路径和绝对路径
绝对路径:指从盘符开始的路径
相对路径:指当前项目的路径
这样说简直不是人话,我们用图说话吧。
首先是绝对路径:我们在javase这个项目下面新建一个a.txt文件,那么这个文件的绝对路径就是 F:\software\IDEA\project\javase\a.txt
然后是相对路径,a.txt 的相对路径是什么呢?上面是说,当前项目的路径。诶,我们的 a.txt 不就是刚好就在 javase 这个项目下吗?所以相对路径就是 a.txt 。如下代码,盘符和前面的各层文件夹都不用写出来了。
FileInputStream fis = new FileInputStream("a.txt");
来,考一考各位,以下将 a.txt 放在src文件夹下,该怎么写相对路径呢?如果会的话,说明你就掌握了。
//同样的,盘符和前面的各层文件夹都不用写
FileInputStream fis = new FileInputStream("IO流\\src\\a.txt");
另外值得一提的是,在java中,以下路径写法都是对的
FileInputStream fis = new FileInputStream("IO流\\src\\a.txt");
// 使用两个 \ ,是因为java中的字符串中,\是转义字符,在字符串中两个 \ 才能表示一个 \
FileInputStream fis = new FileInputStream("IO流/src/a.txt");
4、FileOutputStream
学明白FileInputStream之后,FileOutputStream就简单了。我们直接看代码
public class IOTest01 {
public static void main(String[] args) {
FileOutputStream fos = null;
try {
//同样的,我们关注try{}中的代码即可
//1)假设我们的new.txt文件是不存在。那它会像FileInputStream那样出现找不到文件的异常吗?
//答案是不会,如果new.txt文件不存在,由于这是个相对路径,因此会在当前项目创建一个new.txt
//文件
fos = new FileOutputStream("new.txt");
//2)我们要想 new.txt 中写入数据,先定义一个有数据的字节数组。97、98、99、100分别对应字
//符'a'、'b'、'c'、'd'
byte[] bytes = {97,98,99,100};
//3)调用write()方法将数组bytes中的数据写到文件 new.txt 中
fos.write(bytes);
//4)前面已经有提到,输出流在关闭之前,需要调用flush()方法清空数据流。防止数据缺漏
fos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fos!=null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
执行以上代码之后,我们可以看到在 javase 文件夹下,多了一个新的 new.txt 文件,且文本内容是 abcd 。
关于write()方法
输出流的write()方法和输入流的read()方法其实是对应的。
read()方法有几个重载方法,read(byte[] b),read(byte[],int off,int length)(off是起始位置,length)
是所要读取的字节数。
类比过去,write()方法也是有write(byte[] b),write(byte[],int off,int length),他们的功能也都是类似
的。只不过read()是读取,write()是写入而已。也就是说write()是一个字节一个字节地写入,write(byte[] b)
是将字节数组中的多个字节同时写入。
此时,我们想在 new.txt 中,再加上 efg,使文本内容变成 abcdefg,我们将上面代码修改一下
byte[] bytes = {101,102,103};
继续执行,然后看看我们的文件。发现文本内容竟然是 efg,并没有达到我们想要的结果。我们去看看JDK的帮助文档。
发现还有另外一个构造方法,第二个参数 boolean append,当为true时,表示在文本内容末尾加上新的内容;当为false时,表示将文本内容覆盖掉,写入新的内容。这里就不再演示了,大家改改代码验证一下就行。
FileOutputStream fos = new FileOutputStream("new.txt",true);
接着,上面的代码中,我们想要向文件中写入 abcd ,这是因为我们知道在ASCII码表中,他们分别对应97、98、99、100,那要是我想写入汉字呢,比如我想写入“我爱中国”,有什么办法将这些汉字转换成字节吗?有的,我们可以使用getBytes()方法。
我直接写try中的内容。
try{
fos = new FileOutputStream("new.txt",true);
byte[] bytes = "我爱中国".getBytes();
fos.write(bytes);
fos.flush();
}
//当然,还能简化一点
try{
fos = new FileOutputStream("new.txt",true);
fos.write("我爱中国".getBytes());
fos.flush();
}
5、文件复制
已经学习了FileInputStream和FileOutputStream,输入流是将数据从硬盘读取到内存,输出流是将数据从内存写入到硬盘,那这样的话,我们就能实现文件的复制了。这次不操作文本文件,我们来试一下图片的复制。
如上,将图片 01.jpg 放到项目 javase 中,我们将它复制到 IO流 模块的目录下。
public class IOTest01 {
public static void main(String[] args) {
//1)声明两个对象,要在try{}以外声明,否则不能在finally{}中关闭
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("01.jpg");
fos = new FileOutputStream("IO流\\01.jpg");
byte[] bytes = new byte[fis.available()];
fis.read(bytes);
fos.write(bytes);
fos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//2)注意这里两个流都要关闭
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fos!=null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
有了前面的学习,这里的代码就不难了。执行程序,图片就复制成功了。
注意这里前面说过,字节流能操作任何类型的文件,所以这里除了复制图片,复制其他类型的文件也是可以的,只不过如果是比较大的视频,效率可能会比较低。
在第一部分 IO简介 中就有指出,类名包含 Reader 和 Writer 的,就是专门用来操作文本文件的。而且学懂前面的字节流,字符流就会相当简单。
字节流的read()和write(),是一个字节一个字节地读取和写入,那么字符流的read()和write(),自然就是一个字符一个字符地读取和写入。比如我们读取一个内容为 我爱abc 的文本文件,会先读 ‘我’ ,然后光标移动一位,下次调用read()方法的时候,就是读 ‘爱’。
这里口水比较多,大家估计都已经懂了,但我还是要啰嗦一下,同样的,字符流也有read(char[] c)和write(char[] c)方法来一次性读取或者写入多个字符。
已经没有多的知识点了,直接贴代码吧。
1、FileReader
public class IOTest01 {
public static void main(String[] args) {
FileReader fr = null;
try {
fr = new FileReader("a.txt");
//1)字节流就要定义字节数组,字符流就要定义字符数组。
//注意字节流中有avaliable()方法得到剩下的字节数,而字符流没有对应的方法
char[] chars = new char[1024];
//2)与字节流相同。同样读取到数组中。
fr.read(chars);
System.out.println(chars);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//3)这里的变量名要修改成fr喽,别忘了
if(fr!=null){
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2、FileWriter
public class IOTest01 {
public static void main(String[] args) {
FileWriter fw = null;
try {
fw = new FileWriter("a.txt",true);
char[] chars = {'我','爱','中','国'};
fw.write(chars);
fw.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fw!=null){
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
在前面那个说要介绍哪些流的图中,有四个缓冲流,分别是 BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream ,和文件流中的字节流和字符流类似,缓冲流中的字节流和字符流之间基本的方法也都是类似的,因此这里只介绍 BufferedReader和BufferedWriter。
所谓缓冲流,就是自带字节数组或者字符数组,我们不需要像之前那样去定义字节数组或者字符数组了,因为缓冲流中是自带的。
另外,很多人死记硬背使用缓冲流的代码,这样子一旦太久没有敲IO流的代码,就会很容易忘记。
所以接下来。我们一起去看帮助文档,理解了,就容易记了。
我们看到这两个构造方法,第二个参数是 sz,即 size(大小) 的缩写,这个就是用来指定字符数组的大小的;关键是第一个参数 Reader in,我们貌似还学习过这个类,继续查这个类,得到以下重要信息。
绕了一圈,因为Reader是抽象类不能实例化,因此我们要实例化其子类,查看其子类,最终找到其子类InputStreamReader的子类FileReader是我们学过的,所以,我们应该用FileReader对象来作为参数。
现在,我们来读取一个文本,文本内容如下图:
看代码。
public class IOTest01 {
public static void main(String[] args) {
BufferedReader br = null;
try {
//1)用FileReader对象作为参数传入
br = new BufferedReader(new FileReader("a.txt"));
//2)使用缓冲流的好处,就是其有一个方法readLine(),可以读取文本的一行。
//当文本读完时,返回的不是-1,而是null
String str = br.readLine();
System.out.println(str); //我爱中国
str = br.readLine();
System.out.println(str); //
str = br.readLine();
System.out.println(str); //我爱中国
str = br.readLine();
System.out.println(str); //null
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(br!=null){
try {
//3)注意这里,只有最外层的包装流BufferedReader需要关闭
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
同样的,我们用循环进行优化
try {
br = new BufferedReader(new FileReader("a.txt"));
String str = null;
//只要读到的字符串不为null,也就是文本没有结束,就一直一行一行地往下读
while((str=br.readLine())!=null){
System.out.println(str);
}
}
这里还有两个概念,就是节点流和包装流,很好理解。当我们将一个流对象A作为参数传给另一个流B的构造方法时,
A就是节点流,B就是包装流。
节点流和包装流是相对而言的,比如下面的代码中,InputStreamReader对于FileInputStream来说是包装流,对于
BufferedReader来说是节点流。
现在,假设我们用一个FileInputStream对象作为参数传入会怎样呢?会报错,因为FileInputStream是字节流,而我们所需要的参数是Reader,是字符流,因此,我们需要用到转换流,也就是我们在找Reader的子类时,找到的那个InputStreamReader。
用法如下:
try {
//1)套娃行为,就是现将字节流转换成字符流,再传给BufferedReader的构造方法
br = new BufferedReader(new InputStreamReader(new FileInputStream("a.txt")));
String str = null;
while((str=br.readLine())!=null){
System.out.println(str);
}
}
BufferedWriter就不多介绍了,你想想,BufferedReader可以读取一行,且读取到的一行是字符串,那么相对的,BufferedWriter的write()方法,也可以写入一行字符串,直接给代码,试试效果。
BufferedWriter br = null;
try {
br = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("a.txt")));
br.write("张三\n");
br.write("李四");
br.flush();
}
到这里内容优点多,我们做个简单回顾,前面已经学习了文件流、转换流、缓冲流,其实最重要的两个流,是FileInputStream、FileOutputStream,以下是杜聚宾老师的原话
我们以前在开发中,最常用的是FileInputStream、FileOutputStream,偶尔会用到缓冲流,转换流基本没用过。
这里你会觉得很坑,那前面不是都白学了吗?并没有,因为这些是基本功,把基础打牢固了,后面接触某些新知识就会容易了。
回到数据流,我们来介绍一下什么是数据流?
数据流在将数据写入到文件的时候,除了数据本身之外,数据的类型也会被一起写入到文件中。因此这个文件是特殊
,不是普通的文本文件,不能被直接打开。
以下是DataOutputStream的构造方法
同样的,OutputStream是一个抽象类,不能实例化,我们用其子类FileOutputStream
public class IOTest01 {
//这里的throws抛出异常是不规范写法,应该要用try catch捕捉处理
public static void main(String[] args) throws IOException {
//1)定义数据流,构造方法的参数传一个FileOutputStream对象。
//注意这里的文件名没有后缀,不是 .txt 文件
DataOutputStream dos = new DataOutputStream(new FileOutputStream("b"));
//2)定义各种数据类型的变量
byte b = 100;
short s = 200;
int i = 300;
long l = 400L;
float f = 1.0f;
double d = 2.0;
boolean bo = true;
char c = 'a';
//3)调用相对应的方法将变量写入到文件中
dos.writeByte(b);
dos.writeShort(s);
dos.writeInt(i);
dos.writeLong(l);
dos.writeFloat(f);
dos.writeDouble(d);
dos.writeBoolean(bo);
dos.writeChar(c);
//4)流的刷新和关闭
dos.flush();
dos.close();
}
}
执行以上程序之后,我们来打开,看看这个文件,发现是一堆乱码。
那如何重新查看这些数据呢?
[ 只能用数据输入流重新读取,且读取的顺序,必须和存入的时候一模一样,才能使读到的数据完全正确 ]
代码如下:
public class IOTest02 {
public static void main(String[] args) throws IOException {
DataInputStream dis = new DataInputStream(new FileInputStream("b"));
byte b = dis.readByte();
short s = dis.readShort();
int i = dis.readInt();
long l = dis.readLong();
float f = dis.readFloat();
double d = dis.readDouble();
boolean bo = 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(d);
System.out.println(bo);
System.out.println(c);
}
}
数据流也用的很少,以下仍是杜聚宾老师的原话
在开发生涯中,只用过一次
你还记得我们学习java的第一个程序“Hello World”吗?打印字符串的语句:
System.out.println("Hello world");
这我们都会写。但是你有没有去想过,为什么System.out.println(),就能将字符串打印在控制台上吗?我来解释给你听。
1)首先了解System,它也是一个类,在java.lang包下面,java规定,lang包下的类都是不用导入就能直接使用的。因此我们不导包,也能直接使用System类。
2)其次了解System.out。我们来看System类的部分源代码。System类中有一个引用out,其类型是就是PrintStream,我们还发现,这是一个用static修饰的引用,因此我们能直接用 System.out 进行访问。
这里的 out 是 null ,但往下的代码中会对其进行赋值,这里涉及到的有些方法是用C++或者C代码写的,我们没有必要再深究了。只要知道,其最后会被赋值一个PrintStream对象即可。
3)接着了解整句,System.out.println()。我们看PrintStream的源码。我们发现,原来println()是PrintStream类的一个方法。关于这个方法为什么能将字符串打印到控制台上,我们依旧没有必要再深究了。
来,也就是说我们得到以下的代码,两者是完全等价的。
public class IOTest02 {
public static void main(String[] args) throws IOException {
System.out.println("张三");
PrintStream ps = System.out;
ps.println("张三");
}
}
理解了System.out.println()之后,先转个脑回路,我们思考一个问题:凭什么只能输出在控制台上,我输出到其他地方不可以吗?我们来看看System类的源码。我们发现一个setOut()方法,根据字面意思,就是设置输出,这应该是就是我们要用到的方法了。
setOut()方法调用了两个方法,我们来看setOut0()方法。发现它是个本地方法(用关键字 native 修饰的方法称为本地方法,底层是用C++写的),那就不深究了,我们会用setOut()方法就行。
setOut()方法需要传入一个PrintStream对象,因此我们需要先创建一个PrintStream对象,PrintStream的构造方法又需要传入一个OutputStream,OutputStream是抽象类,因此我们要使用其子类FileOutputStream来创建(利用多态特性)。
万事俱备了,我们来看代码
public class IOTest01 {
public static void main(String[] args) throws IOException {
PrintStream ps = new PrintStream(new FileOutputStream("a.txt"));
System.setOut(ps);
//以下语句会打印到当前项目下的a.txt文件中
System.out.println("张三");
System.out.println("李四");
System.out.println("王五");
}
}
那输出到文件有什么作用呢?作用大着呢,我们可以利用上述代码来写一个日志类,通过日志类来生成日志。(日志文件,就是专门用来记录程序执行情况的文件)
日志类的代码如下:(这个类的代码是垃圾代码,不规范,只是想让大家知道日志是个什么东西)
public class Logger {
public static void log(String str){
try {
//1)创建一个打印流,打印在log.txt文件上
PrintStream ps = new PrintStream(new FileOutputStream("log.txt",true));
//2)通过System类修改输出方向
System.setOut(ps);
//3)创建日期对象
Date date = new Date();
//4)格式化日期:年-月-日 时-分-秒 毫秒
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String time = sdf.format(date);
//5)将日期和参数str打印到log.txt文件中
System.out.println(time + ":" + str);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
创建好Logger类之后,我们执行以下程序。日志文件就生成了,打开看文件中的内容,你就了解日志文件是个什么玩意儿了。
public class IOTest01 {
public static void main(String[] args) throws IOException {
Logger.log("调用了login()方法,用户开始登陆");
Logger.log("用户登陆成功");
}
}
在前面的所有所学到的流中,我们向文件中写入的数据,要么是字节,要么是字符,要么是字符串,最后一部分,我们要来学习怎么向文件中写入java对象。
1)为什么我们要向文件中写入java对象呢?
java对象中可以保存数据,假设我们定义了一个User类(用户)来存放用户的一些信息,比如账号、密码,以及个人
信息等,这些在用户注册完账号后,都要先把数据作为参数保存在User对象中,再将User对象写入到文件中,这样就
能将用户信息永久地保存在硬盘中了。
当下一次用户登陆的时候,我们就去文件中找到对应的账号,只有用户输入的密码是正确的,才能成功地登陆。
2)什么是“序列化”和“反序列化”?
这两个流因为能写入和读取文件中的java对象,因此称为“对象流”。其中,ObjectOutputStream又称为“序列化流”,
ObjectInputStream又称为“反序列化流”。
序列化是指将对象从内存写入到硬盘的过程,反序列化是指将对象从硬盘读取到内存的过程。
我们仍然先看看帮助文档,其构造方法仍然是需要传入FileOutputStream和FileInputStream(多态,再次强调!)
我们先创建一个User类,等会就来存这个类的对象(直接复制代码即可)
public class User {
private long account; //账号
private String password; //密码
public User(long account, String password) {
this.account = account;
this.password = password;
}
public long getAccount() {
return account;
}
public void setAccount(long account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"account=" + account +
", password='" + password + '\'' +
'}';
}
}
1、序列化流将对象写入文件
现在,我们来实际操作一下,首先是序列化流ObjectOutputStream
public class IOTest01 {
public static void main(String[] args) throws IOException {
//1)创建一个序列化对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user"));
//2)创建user对象
User user = new User(123456, "abc");
//3)调用方法将user对象写入
oos.writeObject(user);
oos.flush();
oos.close();
}
}
执行以上程序,结果出异常了,如下
解决方法很简单,我们修改一下User类如下:也就是去实现Serializable这个接口
public class User implements Serializable{
}
我们查看这个Serializable接口,发现这个接口里面竟然什么都没有写!
这里扩展一下,向Serializable这种什么内容都没有的接口,称为'标识接口'。
实现Serializable这个接口,并不是想要实现它的某些方法,或者遵从它的某些规范,它仅仅只是一个标识的作用,
它仅仅只是要告诉程序员,实现了它的类,就是可以序列化的类。
也就是说,实现了Serializable之后,现在我们的User对象可以序列化了。
继续执行将其序列化的代码,发现成功了,而且生成了一个user文件,且打开之后都是乱码。想要知道里面存的是什么,方法就是使用反序列化流来读取。
2、反序列化流读取文件中的对象
其实,我们发现,加上序列化流中上述代码,和之前相比只多了两个新的方法,writeObject()和readObject(),所以要掌握的新内容也不多,下面直接给代码,很容易看懂。
接下来,开始反序列化流ObjectInputStream的代码:
public class IOTest02 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//1)定义一个反序列化对象,指向user文件
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user"));
//2)调用readObject()方法,读取文件中的对象
Object obj = ois.readObject();
System.out.println(obj); //打印一个对象,其实是调用该对象的toString()方法,我们已经重写了该方法
ois.close();
}
}
运行结果如下,也就是说我们成功读取出了对象。
3、存入和读取多个对象
向文件中写入多个对象,不能向其他流一样,通过 new FileOutputStream(" ",true) 的方式,也就是在文件末继续写入对象来实现(原因比较复杂,我会重新开坑,写一篇专门介绍这个原因的博客)。
而是采用集合的方式,向集合对象中添加入多个User对象,再将集合对象写入到文件中,来实现一次性存入多个User对象。如果你还没学过集合,就先理解成集合是一个容器,可以存放对象。当然,这不是唯一的实现方式,且这是较差的一种方式,我都会在新坑里做介绍的。
我们先看看代码,注意要先把原来的 user 文件删除掉。
写入集合对象的代码如下:
public class IOTest01 {
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user"));
List<User> list = new ArrayList<>(); //创建一个集合
list.add(new User(111,"abc")); //add()方法是,向集合中添加对象,我们这里直接传入一个匿名对象
list.add(new User(222,"abc"));
list.add(new User(333,"abc"));
oos.writeObject(list); //将集合写入文件
oos.flush();
oos.close();
}
}
读取集合对象
public class IOTest02 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user"));
//将集合读取出来,由于readObject()方法是返回一个Object对象,因此我们这里要将Object强转成List
List list = (ArrayList) ois.readObject();
User user1 = (User) list.get(0); //集合的get()方法,可以通过传入索引来获取到对象
User user2 = (User) list.get(1);
User user3 = (User) list.get(2);
System.out.println(user1); //打印取到的User对象
System.out.println(user2);
System.out.println(user3);
ois.close();
}
}
这样,我们就取出了User对象,如下图:
这一小节的开头我有说到,使用 new FileOutputStream(" ",true) 作为参数传给 ObjectOutputStream 的构造方法,来实现添加对象的方式是行不通的。因此,如果我们还要写入更多的User对象,只能将集合读取出来,往集合中存入新的User对象,再将集合继续写入文件来实现。我们来看下面的代码,稍微修改一下IOTest02即可。
public class IOTest02 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user"));
//ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user"));
List list = (ArrayList) ois.readObject();
System.out.println(list); //这里的打印,是为了确认一下user文件集合中只有3个User对象
list.add(new User(444,"abc")); //然后我们往读取出来的集合中,再添加2个User对象
list.add(new User(555,"abc"));
ois.close();
//注意以下细节,不能在上面那个位置中定义,会报错
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user"));
oos.writeObject(list);
oos.flush();
oos.close();
}
}
再执行一下以下程序,验证我们的文件中的集合中已经有5个User对象了。
public class IOTest03 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user"));
List list = (ArrayList) ois.readObject();
System.out.println(list);
}
}
这种方式,显然过于繁琐,所以我才说这是一种比较差劲的方式。当然,这里你也可以不用深究,也没有必要深究。如果想深究的话,可以去看看其他博客,或者等我把填坑的博客写完,我会在这里加上链接。
4、transient关键字
很简单,比如你给 User 类中的 password 变量加上 transient,(private transient String password;)那么序列化 User 类对象的时候,变量 password 将不会被写入到文件中。
以下代码可以直接复制粘贴(记得删除 user 文件),没有学习的必要,主要是要知道 transient 关键字的作用即可。
public class IOTest01 {
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user"));
oos.writeObject(new User(111,"abc"));
oos.flush();
oos.close();
}
}
public class IOTest02 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user"));
User user = (User) ois.readObject();
System.out.println(user); //输出结果 User{account=111, password='null'}
}
}
我们取出来的 User 对象,其变量 password 的值是null,说明我们在给 User 类的 password 加上关键字 transient 之后,其就没有被写入到文件了。
5、序列化版本号
了解序列化版本号之前,我们还是先来试试代码。
我们把 User 类中的 transient 去掉,然后运行第4节中的两段代码,此时我们的 user 文件中就有一个对象,User{account=111, password=‘abc’}。现在,我们修改一下 User 类,给它新增一个成员变量 email,如下,请大家直接复制代码试验即可:
public class User implements Serializable {
private long account; //账号
private String password; //密码
private String email;
public User(long account, String password, String email) {
this.account = account;
this.password = password;
this.email = email;
}
public long getAccount() {
return account;
}
public void setAccount(long account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "User{" +
"account=" + account +
", password='" + password + '\'' +
", email='" + email + '\'' +
'}';
}
}
改完之后,我们再去执行第4节中的第二段代码,来读取我们的 User 类对象,发现报错了。错误如下:
java.io.InvalidClassException: User; local class incompatible: stream classdesc serialVersionUID = 3094711356857122676, local class serialVersionUID = 4110144175829258817
很长,但是我们不慌,拆开来看就清晰了,如下图:
原来报错的根本原因是类的序列化版本号不一致。
那么这个序列化版本号究竟是什么呢?
Java虚拟机在看到 User 类实现了 Serializable 这个标识接口之后,就会给这个类自动生成一个序列化版本号。这个版本号的作用,是确保流中的类和文件中的类是同一个类,因为只有同一个类,序列化之后,才能被反序列化。
由于我们修改了 User 类,导致 Java虚拟机 重新为 User 类生成了一个新的序列化版本号,所以导致不一致。
但是,我们自己清楚,这两个类确实都是 User 类。所以,我们不需要 Java虚拟机 为我们自动生成序列化版本号,我们可以自己定义,且当我们自己定义的时候,便不会再自动生成。
自定义序列化版本号非常简单,只需要在类中加上 serialVersionUID 这个常量即可,如下:
public static final long serialVersionUID = 16516516543215L; //可以随便写
【小总结】:我们想要序列化和反序列化的对象,都要实现 Serializable 这个接口,且最好手动给它加上一个自定义的序列化版本号。
另外,我们在 IDEA 中也可以设置自动生成序列化对象的代码,设置方法如下:将√取消掉,然后apply,OK。
设置完之后,只要实现了 Serializable 接口的类,可以将光标停在该类上,然后按 “Alt + Enter”,就能手动添加序列化版本号了。
第八章很重要,也许你现在学完不能理解有什么用,但是你要记住有这么个事,就是后面的重点代码我会强调,你至少要看得懂,因为学完JavaSE之后,这个知识点还会用得到,而且用得很多。
1、Properties
如果你还没有学习过集合,那也没有关系,我简单跟你介绍一下Properties是个什么玩意儿。
我们知道,数组是一种容器,比如一个 char[ ] 数组,那么它可以用来存放多个字符;同样的,集合也是一种容器,不过它是专门用来存放对象(引用数据类型)的容器。
Properties也是集合当中的一种,不过它是专门存字符串的,它可以存一个“键”,和一个“值”,键和值构成“键值对”,将键值对存入Properties对象之后,我们可以再根据“键”,来获取“值”。你先了解这些,也它有知道setProperty()和getProperty()两个方法就行,因为这是我们接下来需要用到的知识。
这么说可能有点抽象,看看代码你就很容易理解了。
public class PropertiesTest01 {
public static void main(String[] args) {
//1)创建一个Properties对象
Properties pro = new Properties();
//2)设置键值对。左边的参数就是键,右边的参数就是值。这样,键值对就存入到pro容器中了、
pro.setProperty("username","zhangsan");
pro.setProperty("password","123");
//3)然后我们从容器中,就可以通过键作为参数,来读取到值。
String username = pro.getProperty("username"); //这里用字符串类型变量来接收读取到的值
String password = pro.getProperty("password");
//4)我们将他们打印出来
System.out.println(username); //zhangsan
System.out.println(password); //123
}
}
2、属性配置文件
我们准备一个文件,文件名为 userinfo.properties,userinfo是“用户信息”的意思,但这不是我们关注的重点;我们要关注的是文件后缀.properties,我们称其为配置文件(当然,不一定是这个文件后缀才叫配置文件,配置文件还有很多种)。
我们还注意到,红框中的图标发生了改变,这是因为IDEA识别到你的文件后缀是 .properties,它就知道是个配置文件,为了与其他文件进行区分,它就修改了图标。
我们可以打开这个文件,然后直接编写以下内容。在这里,等号左边的,我们称为“键”,英文为“key”;等号右边的,我们称为“值”,英文为“value”。
且如果内容格式为如下格式,也就是:
key1=value1
key2=value2
的配置文件,我们称为属性配置文件。
通过1、2节的学习,我们就能将其结合起来运用。我们运用 IO流 来获得文件的数据,再运用 Properties集合 来将数据存到集合对象中,再通过集合对象获取数据。
你会觉得,这不是多此一举吗?我直接用 IO流 读取数据不就得了吗,为什么还要先将数据存到集合,再读取出来?这是因为,我们编写的文件内容格式是固定的,是 “键=值” 的格式,我们在将数据存入集合后,就能通过集合,来通过键,获取值。
还是不理解?看第三节的内容。
3、结合IO流和Properties集合读取文件内容
public class IOPropertiesTest {
public static void main(String[] args) throws IOException {
//1)将文件中的数据放到流当中
FileReader reader = new FileReader("userinfo.properties");
//2)创建一个Properties对象
Properties pro = new Properties();
//3)将流中的数据加载到pro集合中,其中等号左边称为“key”,等号右边称为“value”
pro.load(reader);
//4)通过getProperty()方法,传入键,获取返回值
String username = pro.getProperty("username"); //通过传入键username,来获得值zhangsan,然后赋值给变量username
String password = pro.getProperty("password");
System.out.println(username); //输出结果是 zhangsan
System.out.println(password);
}
}
嗯,我们确实是通过键拿到了值,但是有什么用呢?
4、第八章的重要性
以后的实际开发中,一旦应用程序开发后上线供用户使用,就基本上不能再对定义好的类进行修改了。因为类一旦改变,就会相当麻烦,比如源代码需要重新编译,项目需要重新部署,服务器需要重启,且如果该类关联了其他类,其他类也可能要修改等等…
此时,我们可以在配置文件上修改啊!我们修改了配置文件上的值,就相当于修改了传入方法的字符串,我们就不需要修改代码了。
还是不理解有什么用的话,你学到后面的知识,就会慢慢理解了。