day11【功能流之缓冲流、转换流、序列化流】

day11【功能流之缓冲流、转换流、序列化流】

第一章 字符流

当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。

1.1 字节流读取字符的问题

数据在持久设备上一定是以二进制形式保存,是以字节形式保存。但是有些字节数据合在一起才表示的是字符数据。

比如:记事本中保存的数据,最后肯定是以字节形式保存在硬盘上,但是其中需要2个字节表示一个汉字。我们真正要使用流读取记事本中的数据时,不应该一个一个字节读取,而是应该把表示汉字的那几个字节一起读取,然后把这些字节合在一起表示一个汉字。

需求:使用字节流读取D:\out.txt记事本以下的内容:

abc你好

一次读一个字节数组代码演示如下所示:

分析和步骤:

1)创建FileInputStream类的对象fis,D:\out.txt作为参数;

2)定义一个字节byte数组b,先让数组长度是1024然后读取一遍后在将数组长度变为4;

3)定义一个变量len=0记录读取字节的个数;

4)使用while循环来读取数据,并使用输出语句输出String类构造函数转换后的数据

5)关闭资源流;

//一次读取一个字节数组
	public static void method_2() throws IOException {
		// 创建读取字符数据的输入流对象
		FileInputStream fis = new FileInputStream("D:\\out.txt");
		//定义一个字节数组
//		byte[] b=new byte[1024];//数组大小是1024的整数倍
		byte[] b=new byte[4];//abc  
		//定义一个变量记录读取字节的个数
		int len=0;
		while((len=fis.read(b))!=-1)
		{
			System.out.print(new String(b,0,len));
		}
		//释放资源
		fis.close();
	}

结果:

说明:会出现上述结果的原因是字母abc各占一个字节,而汉字占两个或者三个字节。上述定义的字节数组一次只能存放4个字节,所以会将你字进行字节的拆分,这样会导致后面的汉字字符对应的字节都发生了变化,所以会出现乱码。

字节流读取字符数据的问题:

汉字等字符,往往由多个字节组成。使用字节流读取由多个字节组成字符数据,发现读取到的每个字符的对应的字节数据,而不是真正的字符内容。而我们更希望看到读取的具体的内容是什么字符数据。

我们需要把读取到的某2个或者3个字节合并在一起,拼成一个汉字。这时我们在程序中并不知道应该把哪2个或者3个字节合并成一个汉字。由于在程序中,有时一个字节就表示一个字符数据,比如英文字母,有时必须是2个字节或者3个字节表示一个汉字,那么到底应该把一个字节转成字母,还是把2个字节或者3个字节转成汉字,这时我们无法对读取的数据进行控制。

例如:记事本中的数据:abc你好

我们如果使用字节流来读取上述数据,那么字节流不知道什么时候是几个字节组成字母,几个字节组成汉字,这样会出现我们不想要的结果。

但是Java提供的字符流就可以解决上述读取字符数据的问题

1.2 字符输入流【Reader】

java.io.Reader抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。

  • public void close() :关闭此流并释放与此流相关联的任何系统资源。

  • public int read(): 调用一次读取一个字符,返回字符的编码值。即读取的内容存放到返回值中。如果读取到文件末尾返回-1;

  • public int read(char[] cbuf): 调用一次读取多个字符,把这些字符保存在cbuf中,返回给字符数组中存储的字符个数,如果读取到文件末尾返回-1;

1.3 FileReader类

java.io.FileReader 类是读取字符文件的便利类。构造时使用当前环境默认的字符编码和默认字节缓冲区。

小贴士:

  1. 字符编码:字节与字符的对应规则。Windows系统的中文编码默认是GBK编码表。idea中UTF-8
  2. 字节缓冲区:一个字节数组,用来临时存储字节数据。

注意:在计算机中所有数据在底层都是以字节数据存在的,即使是字符数据在最底层也是以字节数据存在的,因为计算机只识别字节数据。所以在字符流底层使用的是字节缓冲区,其实就是一个字节数组。

构造方法

  • FileReader(File file): 创建一个新的 FileReader ,给定要读取的File对象。
  • FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文件的名称。

当你创建一个流对象时,必须传入一个文件路径。类似于FileInputStream 。

  • 构造举例,代码如下:
public class FileReaderConstructor throws IOException{
    public static void main(String[] args) {
   	 	// 使用File对象创建流对象
        File file = new File("day09\\a.txt");
        FileReader fr = new FileReader(file);
      
        // 使用文件名称创建流对象
        FileReader fr = new FileReader("day09\\b.txt");
    }
}

读取字符数据

  1. 读取字符read方法,调用一次读取一个字符,返回字符的编码值。提升为int类型。即读取的内容存放到返回值中。如果读取到文件末尾返回-1;循环读取,代码使用演示:

    案例:使用字符输入流读取D:\out.txt上的文本文件。并将数据显示到控制台中。

    读数据--输入流--FileReader

    FileReader:

    FileReader(String fileName):传递文件名称

    输入流读文件的步骤:

    A:创建输入流对象

    B:调用输入流对象的读数据方法

    C:释放资源

    package com.itheima_02;
    import java.io.FileReader;
    import java.io.IOException;
    
    public class FileReaderDemo {
    	public static void main(String[] args) throws IOException {
    		//创建输入流对象
    //指定文件不存在就会报异常:java.io.FileNotFoundException: D:\\test\\1111.txt (系统找不到指定的文件。)
    //		FileReader fr = new FileReader("D:\\test\\1111.txt");
    		FileReader fr = new FileReader("D:\\out.txt");
    		
    		//调用输入流对象的读数据方法
    		//int read():一次读取一个字符
    		int ch=0;
    		while((ch=fr.read())!=-1) {
    			System.out.print((char)ch);
    		}
    		//释放资源
    		fr.close();
    	}
    }
    
    

小贴士:虽然读取了一个字符,但是会自动提升为int类型。

  1. 使用字符数组读取read(char[] cbuf),调用一次读取多个字符,把这些字符保存在cbuf数组中,返回给字符数组中存储的字符个数,如果读取到文件末尾返回-1.代码使用演示:
public class FISRead {
    public static void main(String[] args) throws IOException {
      	// 使用文件名称创建流对象
       	FileReader fr = new FileReader("D:\\out.txt");
      	// 定义变量,保存有效字符个数
        int len =0;
        // 定义字符数组,作为装字符数据的容器
        char[] cbuf = new char[1024];
        // 循环读取
        while ((len = fr.read(cbuf))!=-1) {
            System.out.println(new String(cbuf,0,len));
        }
    	// 关闭资源
        fr.close();
    }
}

1.4 字符输出流【Writer】

java.io.Writer 抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字符输出流的基本共性功能方法。

  • public abstract void close() :关闭此输出流并释放与此流相关联的任何系统资源。

  • public abstract void flush() :刷新此输出流并强制任何缓冲的输出字符被写出。

    说明:字符输入和字符输出流都是自带缓冲区的。缓冲区就是在类的底层封装了一个数组,如果我们使用字符输出流向目的地文件中写数据的时候,数据不会立刻写到目的地文件中,而是写到自带的数组中,数据还在内存中,所以我们必须调用FileWriter类中的刷新方法flush将数组中的数据刷新到目的地硬盘文件中。

  • public void write(int b) :写出一个字符。

  • public void write(char[] cbuf):将 b.length字符从指定的字符数组写出此输出流。

  • public abstract void write(char[] b, int off, int len) :从指定的字符数组写出 len字符,从偏移量 off开始输出到此输出流。

  • public void write(String str) :写出一个字符串。

1.5 FileWriter类

java.io.FileWriter 类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

构造方法

  • FileWriter(File file): 创建一个新的 FileWriter,给定要读取的File对象。

  • FileWriter(String fileName): 创建一个新的 FileWriter,给定要读取的文件的名称。

    当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。 类似于FileOutputStream。

  • 构造举例,代码如下:

public class FileWriterConstructor {
    public static void main(String[] args) throws IOException {
   	 	// 使用File对象创建流对象
        File file = new File("day09\\a.txt");
        FileWriter fw = new FileWriter(file);
      
        // 使用文件名称创建流对象
        FileWriter fw = new FileWriter("day09\\b.txt");
    }
}

基本写出数据

写出字符write(int b) 方法,每次可以写出一个字符数据,代码使用演示:

public class FWWrite {
    public static void main(String[] args) throws IOException {
        // 使用文件名称创建流对象
        FileWriter fw = new FileWriter("day09\\fw.txt");     
      	// 写出数据
      	fw.write(97); // 写出第1个字符
      	fw.write('b'); // 写出第2个字符
      	fw.write('C'); // 写出第3个字符
      
      	/*
        【注意】关闭资源时,与FileOutputStream不同。
      	 如果不关闭,数据只是保存到缓冲区,并未保存到文件。
        */
        // fw.close();
    }
}
输出结果:
abC

小贴士:

  1. 虽然参数为int类型四个字节,但是只会保留一个字符的信息写出。
  2. 未调用close方法,数据只是保存到了缓冲区,并未写出到文件中。

关闭和刷新

因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush 方法了。

  • flush :刷新缓冲区,流对象可以继续使用。
  • close :关闭流,释放系统资源。关闭前会刷新缓冲区。

代码使用演示:

public class FWWrite {
    public static void main(String[] args) throws IOException {
        // 使用文件名称创建流对象
        FileWriter fw = new FileWriter("day09\\fw.txt");
        // 写出数据,通过flush
        fw.write('刷'); // 写出第1个字符
        fw.flush();
        fw.write('新'); // 继续写出第2个字符,写出成功
        fw.flush();
      
      	// 写出数据,通过close
        fw.write('关'); // 写出第1个字符
        fw.close();
        fw.write('闭'); // 继续写出第2个字符,【报错】java.io.IOException: Stream closed
        fw.close();
    }
}

小贴士:

1)即便是flush方法写出了数据,操作的最后还是要调用close方法,释放系统资源。

2)注意:

close()和flush()方法的区别:

flush():刷新缓冲区。流对象还可以继续使用。

close():先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。

写出其他数据

void write(String str):写一个字符串数据

void write(String str,int index,int len):写一个字符串中的一部分数据

​ 说明:

​ str 表示要写的字符串

​ index 表示从字符串哪个下标开始写

​ len  表示写的字符个数。

​ 注意:字符串可以看做是一个由多个字符组成的字符数组。

void write(int ch):写一个字符数据,这里写int类型的好处是既可以写char类型的数据,也可以写char对应的int类型的值。'a',97

void write(char[] chs):写一个字符数组数据

void write(char[] chs,int index,int len):写一个字符数组的一部分数据

​ 参数:

​ chs  表示写的字符数组

​ index 表示从数组哪个下标开始写

​ len写的字符个数

代码使用演示:

package com.itheima_01;
import java.io.FileWriter;
import java.io.IOException;

/*
 * void write(String str):写一个字符串数据
 * void write(String str,int index,int len):写一个字符串中的一部分数据
 * void write(int ch):写一个字符数据,这里写int类型的好处是既可以写char类型的数据,也可以写char对应的int类型的值。'a',97
 * void write(char[] chs):写一个字符数组数据
 * void write(char[] chs,int index,int len):写一个字符数组的一部分数据
 */
public class FileWriterDemo3 {
	public static void main(String[] args) throws IOException {
		//创建输出流对象
		FileWriter fw = new FileWriter("day09\\b.txt");
		
		//void write(String str):写一个字符串数据
		//fw.write("abcde");
		
		//void write(String str,int index,int len):写一个字符串中的一部分数据
		//fw.write("abcde",0,5);
		//fw.write("abcde",1,3);
		
		//void write(int ch):写一个字符数据,这里写int类型的好处是既可以写char类型的数据,也可以写char对应的int类型的值。'a',97
		//fw.write('a');
		//fw.write(97);
		
		//void write(char[] chs):写一个字符数组数据
		char[] chs = {'a','b','c','d','e'};
		//fw.write(chs);
		
		//void write(char[] chs,int index,int len):写一个字符数组的一部分数据
		//fw.write(chs,0,5);
		fw.write(chs,2,3);
		
		//释放资源
		fw.close();
	}
}

FileWriter写入换行以及向文本末尾追加

说明:操作类似于FileOutputStream。

操作代码演示如下:

package com.itheima_01;

import java.io.FileWriter;
import java.io.IOException;

public class FileWriterDemo4 {
	public static void main(String[] args) throws IOException {
		//创建输出流对象
		//FileWriter fw = new FileWriter("day09\\4.txt");
		FileWriter fw = new FileWriter("day09\\4.txt",true); //表示追加写入,默认是false
		
		for(int x=0; x<10; x++) {
			fw.write("hello"+x+"\r\n");
		}
		//释放资源
		fw.close();
	}
}

小贴士:字符流,只能操作文本文件,不能操作图片,视频等非文本文件。

第一章 缓冲流

前面学习了基本的一些流,作为IO流的入门,今天我们要见识一些更强大的流。比如能够高效读写的缓冲流,能够转换编码的转换流,能够持久化存储对象的序列化流等等。这些功能更为强大的流,都是在基本的流对象基础之上创建而来的,就像穿上铠甲的武士一样,相当于是对基本流对象的一种增强。

1.1 概述

缓冲流是对4个基本的FileXxx 流的增强,所以也是4个流,按照流操作数据的数据类型分类:

  • 字节缓冲流BufferedInputStreamBufferedOutputStream
  • 字符缓冲流BufferedReaderBufferedWriter

缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

1.3 字符缓冲流

构造方法

  • public BufferedReader(Reader in) :创建一个 新的字符缓冲输入流。
  • public BufferedWriter(Writer out): 创建一个新的字符缓冲输出流。

构造举例,代码如下:

// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("E:\\br.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("E:\\bw.txt"));

特有方法

字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。

  • BufferedReader:public String readLine(): 经常使用它读取一行数据。readLine方法可以按照行读取,读取的结束标记’\r’’\n’,返回的结果是读到这一行的所有文字。如果读取到文件的末尾返回 null。

  • BufferedWriter:public void newLine(): 写一行行分隔符。就是换行。

readLine方法演示,代码如下:

public class BufferedReaderDemo {
    public static void main(String[] args) throws IOException {
      	 // 创建流对象
        BufferedReader br = new BufferedReader(new FileReader("E:\\in.txt"));
		// 定义字符串,保存读取的一行文字
        String line  = null;
      	// 循环读取,读取到最后返回null
        while ((line = br.readLine())!=null) {
            System.out.print(line);
            System.out.println("------");
        }
		// 释放资源
        br.close();
    }
}

newLine方法演示,代码如下:

public class BufferedWriterDemo throws IOException {
    public static void main(String[] args) throws IOException  {
      	// 创建流对象
		BufferedWriter bw = new BufferedWriter(new FileWriter("E:\\out.txt"));
      	// 写出数据
        bw.write("黑马");
      	// 写出换行
        bw.newLine();
        bw.write("程序");
        bw.newLine();
        bw.write("员");
        bw.newLine();
		// 释放资源
        bw.close();
    }
}
输出效果:
黑马
程序
员

1.4 练习:文本排序

请将文本信息恢复顺序。

3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必得裨补阙漏,有所广益。
8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。
4.将军向宠,性行淑均,晓畅军事,试用之于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。
2.宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。
1.先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。
9.今当远离,临表涕零,不知所言。
6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。
7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐付托不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。
5.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。

准备工作:

在当前项目下面新建一个文件,叫做in.txt。然后将上述内容复制到in.txt文件中。注意每个序号后面要是一行数据。每个序号之间的数据要换行。效果如下图所示。

案例分析

  1. 逐行读取文本信息。
  2. 解析文本信息到集合中。
  3. 遍历集合,按顺序,写出文本信息。

案例实现

/*
分析:原来文本文件中的数据是乱序的,我们现在要求将文本文件in.txt中的无序的数据按照顺序写到一个文件out.txt中。
    1. 逐行读取文本信息。
        数据源文件in.txt属于文本文件,使用字符输入流---一行一行---高效的字符输入流BufferedReader 方法 readLine()
    2. 解析文本信息到集合中。
        每一行数据有个特点:  3.侍中、侍郎郭攸之、费祎
         数字+"."+文本数据
         读取到这一行数据,将其进行切割  使用String类中 split("\\.")
         注意:这里要书写  \\. 不能直接书写 . 因为在这里.具备特殊含义 表示任意的字符
         而我们这里希望是真正的 点,所以需要使用\将点进行转义,但是在java中\\表示一个真正的\
         总结:\\.表示我们生活中的一个 .

         3.侍中、侍郎郭攸之、费祎、董允等,....
         8.愿陛下托臣以讨贼兴复之效。。。
         按照上述做法切割完成之后,数组长度是2
         索引     数据
         0       3
         1       侍中、侍郎郭攸之、费祎、董允等,....
         将截取的数据存放到Map集合中
         序号作为key  文本数据作为value
         HashMap<Integer,String> hm = new HashMap<Integer,String>();
         hm.put(3,"侍中、侍郎郭攸之、费祎、董允等,....");----->hm.put(Integer.parseInt(arr[0]),arr[1]);
    3. 遍历集合,按顺序,写出文本信息。

        //遍历
        for(int i=1;i<=hm.size();i++)
        {
            //i赋值给key
            Integer key=i;
            //根据key获取值
            String value = hm.get(key);
            //写文本信息 一行一行  BufferedWriter  换行 newLine()
            每一行信息包括: bw.write(key+"."+value);
        }
 */
public class BufferedTest {
    public static void main(String[] args) throws Exception {
        //1. 逐行读取文本信息。
        //1.1 创建高效字符输入流对象关联数据源文件
        BufferedReader br = new BufferedReader(new FileReader("in.txt"));
        //1.2 调用方法读取数据
        //创建集合对象保存切割后的序号和文本数据 key : 存储序号 3   value : 存储文本数据
        HashMap<Integer, String> hm = new HashMap<>();
        String line = null;
        while ((line = br.readLine()) != null) {
            //1.3 截取读取的数据
            String[] arr = line.split("\\.");
            //2. 解析文本信息到集合中。  3.侍中、侍郎郭攸之、费祎、董允等,....
            //由于key位置是Integer类型,而 数组中第一个数据是字符串,所以需要转换
            int key = Integer.parseInt(arr[0]);
            //添加到集合中
            hm.put(key, arr[1]);//key:3 value:侍中、侍郎郭攸之、费祎、董允等,....
        }
       
        //关闭资源
        br.close();
        //创建字符高效输出流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"));
        //3. 遍历集合,按顺序,写出文本信息。
        for (int i = 1; i <= hm.size(); i++) {
            //获取i就是key,i赋值给key
            Integer key = i;
            //根据key获取value
            String value = hm.get(key);
            //将遍历出来的key和value写到out.txt中
            bw.write(key+"."+value);
            //换行
            bw.newLine();
        }
        //释放资源
        bw.close();
    }
}

第二章 转换流

3.1 字符编码和字符集

字符编码

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本f符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。

  • 字符编码Character Encoding : 就是一套我们人类语言的字符与二进制数之间的对应规则。

字符集

  • 字符集 Charset:是一个系统支持的所有字符的集合,包括各国家文字、标点符号、数字等。

计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符集等。

可见,当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。

  • ASCII字符集

    • ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字)。
    • 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。

    ASCII码表1个字节表示一个字符。

    生活中的字符 十进制 二进制

    ​ A 65 01000001

    ​ B 66 01000010

    ……………..

    补充:(了解)

    在美式英语中使用的特殊控制字符。其中:

    0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符),如控制符:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(响铃)等;通信专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等;ASCII值为8、9、10 和13 分别转换为退格、制表、换行和回车字符。它们并没有特定的图形显示,但会依不同的应用程序,而对文本显示有不同的影响。

32表示空格。

  • ISO-8859-1字符集

  • 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。

  • ISO-5559-1使用单字节编码,兼容ASCII编码。

  • ISO-8859-1编码表1个字节表示一个字符

  • ISO:International Standards Organization,国际标准化组织。

  • GBxxx字符集

    • GB就是国标的意思,是为了显示中文而设计的一套字符集。
    • GB2312:简体中文码表。叫做信息交换用汉字编码字符集。它识别六七千的文字,兼容ASCII编码表。2个字节表示一个汉字字符。
    • GBK:叫做汉字编码字符集,也可以叫做国标。是GB2312的升级版,识别2万多个中文字符。2个字节表示一个汉字字符。目前主流的编码表。
    • GB18030:是GBK的升级版,包含了大部分的中文(简体、繁体、甲骨文、象形文等等),还有少数民族的文字,识别更多。每个字可以由1个、2个 或4个字节组成。
  • Unicode字符集

    因为世界上各个国家都开发了属于自己的编码表,造成编码表不统一,世界计算机协会就为编码统一,制定了一张国际通用的编码表:unicode码表

    • Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码。标准万国码。

    • 有三种编码方案,UTF-8、UTF-16和UTF-32。最为常用的UTF-8编码。

    • UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则:

      1. 128个US-ASCII字符,只需一个字节编码。

      2. 拉丁文等字符,需要二个字节编码。

      3. 大部分常用字(含中文),使用三个字节编码。

      4. 其他极少使用的Unicode辅助字符,使用四字节编码。

可以识别汉字的编码表有哪些:

	GB2312、GBK、GB18030、UTF-8

  在开发中常用的码表:ISO-8859-1、GBK、UTF-8、ASCII。
文字--->(数字) :编码: 就是把能看懂内容,转换成看不懂的内容。

A      65      01000001

(数字)--->文字  : 解码: 就是把看不懂的内容,转换成看懂的内容。

 01000001  65    A
 乱码:编码和解码使用的编码表不一致导致的。

image-20200502082751550

3.2 编码引出的问题

在IDEA中,使用FileReader 读取项目中的文本文件。由于IDEA的设置,都是默认的UTF-8编码,所以没有任何问题。但是,当读取Windows系统中创建的文本文件时,由于Windows系统的默认是GBK编码,就会出现乱码。

需求:使用字符输入流读取E:\1.txt文件中的数据,要求:1.txt使用GBK编码方式保存。

public class ReaderDemo {
    public static void main(String[] args) throws IOException {
        FileReader fileReader = new FileReader("E:\\1.txt");
        int read;
        while ((read = fileReader.read()) != -1) {
            System.out.print((char)read);
        }
        fileReader.close();
    }
}
输出结果:
���

那么如何读取GBK编码的文件呢?

3.3 InputStreamReader类

转换流java.io.InputStreamReader,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法

  • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。
  • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。

构造举例,代码如下:

InputStreamReader isr = new InputStreamReader(new FileInputStream("in.txt"));
InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK");

指定编码读取

public class ReaderDemo2 {
    public static void main(String[] args) throws IOException {
      	// 定义文件路径,文件为gbk编码
        String FileName = "E:\\1.txt";
      	// 创建流对象,默认UTF8编码
        InputStreamReader isr = new InputStreamReader(new FileInputStream(FileName));
      	// 创建流对象,指定GBK编码
        InputStreamReader isr2 = new InputStreamReader(new FileInputStream(FileName) , "GBK");
		// 定义变量,保存字符
        int read=0;
      	// 使用默认编码字符流读取,乱码
        while ((read = isr.read()) != -1) {
            System.out.print((char)read); // ��
        }
        isr.close();
      
      	// 使用指定编码字符流读取,正常解析
        while ((read = isr2.read()) != -1) {
            System.out.print((char)read);// 你好
        }
        isr2.close();
    }
}

3.4 OutputStreamWriter类

转换流java.io.OutputStreamWriter ,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码转换为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法

  • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
  • OutputStreamWriter(OutputStream in, String charsetName): 创建一个指定字符集的字符流。

构造举例,代码如下:

OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("day10\\out.txt"));
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("day10\\out.txt") , "GBK");

指定编码写出

public class OutputDemo {
    public static void main(String[] args) throws IOException {
      	// 定义文件路径
        String FileName = "E:\\out.txt";
      	// 创建流对象,默认UTF8编码
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(FileName));
        // 写出数据
      	osw.write("你好"); 
        osw.close();
      	
		// 定义文件路径
		String FileName2 = "E:\\out2.txt";
     	// 创建流对象,指定GBK编码
        OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream(FileName2),"GBK");
        // 写出数据
      	osw2.write("你好");
        osw2.close();
    }
}

转换流理解图解

转换流是字节与字符间的桥梁!

字节存在硬盘上。字符存在内存中。

总结

转换流有两个作用:

​ 1. 指定读写的码表

​ 2.可以把字节流对象变成字符流对象

3.5.练习:把UTF-8文件转成GBK

需求:把D:\out.txt以UTF-8编码的文件转换为 以GBK编码的文件

/*
    需求:把D:\\out.txt以UTF-8编码的文件转换为 以GBK编码的文件
 */
public class Test01 {
    public static void main(String[] args) throws Exception {
        //把这个文件换成GBK编码  内容不变
        //创建输入流
        //默认按照UTF-8方式读取
        InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\out.txt"));
        //创建输出流
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\123.txt"),"GBK");
        //循环读写
        int a;
        //每次读取一个字符
        while((a=isr.read()) != -1){
            //写出一个字符
            osw.write(a);
        }
        //关流
        osw.close();
        isr.close();
    }
}

第四章 序列化

4.1 概述

Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据对象的类型对象中存储的数据等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。

反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化对象的数据对象的类型对象中存储的数据信息,都可以用来在内存中创建对象。看图理解序列化:

4.2 ObjectOutputStream类

java.io.ObjectOutputStream 类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。

构造方法

  • public ObjectOutputStream(OutputStream out) : 创建一个指定OutputStream的ObjectOutputStream。

构造举例,代码如下:

FileOutputStream fileOut = new FileOutputStream("E:\\s.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);

序列化操作

  1. 一个对象要想序列化,必须满足两个条件:
  • 该类必须实现java.io.Serializable 接口,Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException

    说明:Serializable:它是一个标记性接口。这个接口中没有任何的方法,这种接口称为标记型接口!它仅仅是一个标识。是一个标记接口为了启动一个序列化功能。只有具备了这个接口标识的类才能通过Java中的序列化和反序列化流操作这个对象。

    注意:只要一个类实现了Serializable接口,那么都会给每个实现类分配一个序列版本号作为唯一标识。

  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。

//为了保证学生对象可以被序列化,我们让Student类来实现Serializable接口
public class Student implements Serializable {
	//属性
	private String name;
	private transient int age; // transient瞬态修饰成员,不会被序列化
	public Student(String name, int age) {
		this.name = name;
		this.age = age;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
}

2.写出对象方法

  • public final void writeObject (Object obj) : 将指定的对象写出。
public class SerializeDemo{
   	public static void main(String [] args)  throws IOException {
        //创建学生对象
		Student s = new Student("黑旋风",18);
		//把创建出来的学生对象持久化保存在硬盘中
		//创建序列化对象  创建输出流对象并关联目标文件
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\s.txt"));
		//使用序列化对象中的方法持久化学生对象
		oos.writeObject(s);// 姓名被序列化,年龄没有被序列化。
		//关闭资源
		oos.close();
   	}
}

4.3 ObjectInputStream类

ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。

这种操作叫做反序列化。

反序列化:可以把序列化后的对象(硬盘上的文件中的对象数据),读取到内存中,然后就可以直接使用对象。这样做的好处是不用再一次创建对象了,直接反序列化就可以了。

构造方法

  • public ObjectInputStream(InputStream in) : 创建一个指定InputStream的ObjectInputStream。

反序列化操作1

如果能找到一个对象的class文件,我们可以进行反序列化操作,调用ObjectInputStream读取对象的方法:

  • public final Object readObject () : 读取一个对象。
public class DeserializeDemo {
   public static void main(String [] args)  throws IOException, ClassNotFoundException {
        //创建反序列化对象,指定一个字节输入流用来读取持久文件
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\s.txt"));
		//使用反序列化对象ois调用函数进行读取数据
		Student s = (Student) ois.readObject();
		System.out.println(s.getName()+"==="+s.getAge());
		//关闭资源
		ois.close();
    }
}
输出结果:
  黑旋风===0

反序列化操作2

当在反序列化对象之前,我们对Student类做了一些简单的修改,无关紧要的修改。例如给Student类添加一个属性字段或者函数都可以,再次反序列化,就出问题了,报如下图所示的异常:

分析异常:

通过以上发生的异常我们发现是由于readObject()函数发生的异常,所以我们接下来会查看readObject()函数。

接下来查看InvalidClassException 异常类:



问题一:什么是该类的序列版本号呢?

类要进行序列化操作时,需要实现Serializable接口。(Serializable接口也称为标记接口),实现了标记接口的类,该类会存在一个标记值。这个标记值就是该类的序列版本号。这个版本号和该类相关联。

说明:也就是说只要一个类实现Serializable接口,那么在编译源文件时,生成的class文件中就会生成一个和该类相关联的序列版本号。

问题二:这个版本号有什么作用呢?

在序列化时,这个版本号会随着对象一起被序列化到本地文件中。在反序列化的时候,使用流从硬盘上会读取之前序列化的文件,那么jvm会拿着使用流读取到的序列化的版本号和本地硬盘上即class文件中的序列化版本号进行匹配,如果不匹配就会抛异常。Java.io.InvalidClassException。

所以可以理解这个序列化版本号serialVersionUID是用来验证的,防止反序列化的对象和本地的类不匹配。

问题三:这个序列化版本号是如何生成的呢?

由于实现类已经实现了Serializable接口,那么只要重新编译源文件的时候,编译器就会根据类的各个方面(如类的成员变量、成员函数、修饰符、函数返回值类型等)计算成为该类的默认 serialVersionUID 值(版本号)。

serialVersionUID 称为序列版本编号(标记值)。

上述代码如果在反序列化之前修改Student类时,会在报的异常中出现如下图所示的提示信息:

cn.itcast.sh.otherstream.demo.Student; 
local class incompatible: stream classdesc serialVersionUID = -7620680200402190796, 
local class serialVersionUID = 6830451359866645744

stream classdesc serialVersionUID = -7620680200402190796, 表示从流中读取的版本号(反序列化时从硬盘文件中读取的)

local class serialVersionUID = 6830451359866645744 ,表示Student类的序列版本号(表示Student类实现Serializable接口时,编译后在生成的class文件中的序列化版本号)

上述两个版本号不一致,所以报异常。

通过以上分析,可以得到一个结论:

如果可以保证反序列化对象和序列化对象的标记值相同,就可以避免异常的发生。

那么我们如何做才能保证反序列化对象和序列化对象的标记值相同呢?

我们修改Student类是无关紧要的。在我们修改Student类的时候,我们不希望它抛异常。我们可以给类定义一个默认的版本号,即给Student类添加标记值也就是版本号serialVersionUID。这样一来,添加的标记值即版本号会随着对象的序列化持久保存。无论是序列化,还是反序列化,都不会再根据类的各个方面计算版本号了。序列化和反序列化的版本号会永远一致,所以不会抛出异常,这样就可以避免InvalidClassException异常的发生了。

但是,这样一来,类的安全问题,只能自己来维护。因为已经将类的对象序列化之后,由于类中已经显示定义了版本号,那么反序列化的时候即使修改了Student类,也不会报异常了。

在idea中给自定义类Student添加版本号方法如下所示:

1)File----->Settings

2)Editor-->inspections→serialization issues→选择图中的选项。勾上serializable class without ‘serialVersionUID’

3)点击确定,将光标放到实体类上,按alt+回车就可以出现生成序列版本ID了

总结,记住

1、当一个对象需要被序列化 或 反序列化的时候对象所属的类需要实现Serializable接口。

2、被序列化的类中需要添加一个serialVersionUID。

4.4 练习:序列化集合

  1. 将存有多个自定义对象的集合序列化操作,保存到list.txt文件中。
  2. 反序列化list.txt ,并遍历集合,打印对象信息。

案例分析

  1. 把若干学习对象 ,保存到集合中。
  2. 把集合序列化。
  3. 反序列化读取时,只需要读取一次,转换为集合类型。
  4. 遍历集合,可以打印所有的学生信息

案例实现

public class SerTest {
	public static void main(String[] args) throws Exception {
		// 创建 学生对象
		Student student = new Student("老王", "laow");
		Student student2 = new Student("老张", "laoz");
		Student student3 = new Student("老李", "laol");

		ArrayList<Student> arrayList = new ArrayList<>();
		arrayList.add(student);
		arrayList.add(student2);
		arrayList.add(student3);
		// 序列化操作
		 serializ(arrayList);
		
		// 反序列化  
		ObjectInputStream ois  = new ObjectInputStream(new FileInputStream("list.txt"));
		// 读取对象,强转为ArrayList类型
		ArrayList<Student> list  = (ArrayList<Student>)ois.readObject();
		
      	for (int i = 0; i < list.size(); i++ ){
          	Student s = list.get(i);
        	System.out.println(s.getName()+"--"+ s.getPwd());
      	}
	}

	private static void serializ(ArrayList<Student> arrayList) throws Exception {
		// 创建 序列化流 
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt"));
		// 写出对象
		oos.writeObject(arrayList);
		// 释放资源
		oos.close();
	}
}

第五章 打印流(了解)

5.1 概述

平时我们在控制台打印输出,是调用print方法和println方法完成的,这两个方法都来自于java.io.PrintStream类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。

5.2 PrintStream类

构造方法

  • public PrintStream(String fileName) : 使用指定的文件名创建一个新的打印流。

构造举例,代码如下:

PrintStream ps = new PrintStream("day10\\ps.txt");

改变打印流向

System.out就是PrintStream类型的,只不过它的流向是系统规定的,打印在控制台上。

public class Test01 {
    public static void main(String[] args) throws Exception {
        // 调用系统的打印流,控制台直接输出97
        System.out.println(97);

        // 创建打印流,指定文件的名称
        PrintStream ps = new PrintStream("ps.txt");
        //直接使用对象调用方法输出到指定文件中
        ps.println(100);

        //设置系统的打印流流向,输出到ps.txt
        System.setOut(ps);

        System.out.println("你爱我");

        //打印流是自动刷新
        System.out.println(1);
        System.out.println(true);
        System.out.println("哈哈哈");

        ps.close();
    }
}

说明:System类中的static void setOut(PrintStream out) 方法表示重新分配“标准”输出流。 改变流的输出方向。

第六章 装饰设计模式

在我们今天所学的缓冲流中涉及到java的一种设计模式,叫做装饰模式,我们来认识并学习一下这个设计模式。

1.概述

装饰模式指的是在不改变原类, 不使用继承的基础上,动态地扩展一个对象的功能,功能就是方法。

2.装饰设计模式写法

  1. 装饰类和被装饰类必须实现相同的接口 (必须有接口)
  2. 在装饰类中必须传入被装饰类的引用(对象)
  3. 在装饰类中对需要扩展的方法进行扩展
  4. 在装饰类中对不需要扩展的方法调用被装饰类中的同名方法

3.代码演示

//明星接口 共同父接口
public interface Star {
    //唱歌
    public abstract void sing();
    //演戏
    public abstract void play();
}

//刘德华 被装饰类
public class LiuDeHua implements Star {
    @Override
    public void sing() {
        System.out.println("刘德华唱冰雨");
    }

    @Override
    public void play() {
        System.out.println("刘德华演无间道");
    }
}

//刘德华增强类 装饰类
public class BufferedLiuDeHua implements Star {

    //LiuDeHua类(被装饰类)的对象作为成员变量
    private LiuDeHua ldh;

    //构造方法
    public BufferedLiuDeHua(LiuDeHua ldh) {
        this.ldh = ldh;
    }

    @Override
    public void sing() {
        System.out.println("刘德华唱rap 淡黄的长裙 蓬松的头发");
    }

    @Override
    public void play() {
        //调用原来的方法
        ldh.play();
    }
}

public class Demo测试类 {
    public static void main(String[] args) {
        //测试刘德华这个类
        LiuDeHua ldh = new LiuDeHua();
        ldh.sing();
        ldh.play();

        //-------------------------------

        //测试刘德华增强类
        BufferedLiuDeHua bl = new BufferedLiuDeHua(ldh);
        bl.sing();
        bl.play();

    }
}

说明:

image-20200502094609711

第七章 commons-io包

1.概述

commons-io是apache开源基金组织提供的一组有关IO操作的类库,可以提高IO功能开发的效率。

这些内容不在JDK里面,在使用的时候单独导入。

2.使用方式

  1. 下载commons-io相关jar包;http://commons.apache.org/proper/commons-io/

    你无需下载,直接到今日下发的资料中找到使用即可。

    image-20200502102609065

  2. 把commons-io-2.6.jar包复制到指定的项目的lib目录中

    说明:以后我们只要使用不是jdk的内容即第三方(oracle属于第一方,我们属于第二方,其他的都是第三方)的技术都需要导包。

    ​ 导包步骤:

    ​ 1)在当前项目根目录下新建一个lib(是library库的缩写)文件夹

    ​ 2)将第三方jar包放到lib文件夹下

    image-20200502102938164

  3. 将commons-io-2.6.jar加入到classpath

选中jar包右键加入到classpath下。选择add as library

image-20200502103138824

image-20200502103236223

注意:必须按照上面去做,否则无法使用。

3.方法

  • IOUtils类中

    • public static int copy(InputStream in, OutputStream out) 文件复制(适合文件大小为2GB以下)
    • public static long copyLarge(InputStream in, OutputStream out) 文件复制(适合文件大小为2GB以上)
    public class Test01 {
        public static void main(String[] args) throws Exception {
            //在IOUtils类下面
            // public static int copy(InputStream in, OutputStream out)      文件复制(适合文件大小为2GB以下)
            // public static long copyLarge(InputStream in, OutputStream out) 文件复制(适合文件大小为2GB以上)
    
            //输入流
            FileInputStream fis = new FileInputStream("3.jpg");
            //输出流
            FileOutputStream fos = new FileOutputStream("4.jpg");
    
            //复制
            IOUtils.copy(fis,fos);
    
            //关流
            fos.close();
            fis.close();
        }
    }
    
  • FileUtils类中

    • public static void copyFileToDirectory(File srcFile, File destFile) //复制文件到另外一个目录下。

    • public static void copyDirectoryToDirectory(File srcDir,File destDir);文件夹复制,将srcDir复制到destDir

      需求:将D:\1.mp3复制到E盘

      public class Test01 {
          public static void main(String[] args) throws Exception {
      //        public static void copyFileToDirectory(File srcFile, File destFile) //复制文件到另外一个目录下。
      //        public static void copyDirectoryToDirectory(File srcDir,File destDir);文件夹复制,将srcDir复制到destDir
              //需求:将D:\1.mp3复制到F盘
              //文件
      //        File f1 = new File("D:\\1.mp3");
      //        //文件夹
      //        File f2 = new File("F:\\");
      //
      //        //调用方法
      //        FileUtils.copyFileToDirectory(f1,f2);
                    //copyDirectoryToDirectory() :把文件夹复制到一个文件夹下
              //文件夹
              File f3 = new File("D:\\test");
              //文件夹
              File f4 = new File("F:\\");
              //调用方法
              FileUtils.copyDirectoryToDirectory(f3,f4);
          }
      }
      
posted @ 2023-02-15 16:26  忘了鱼尾纱的猫  阅读(157)  评论(0)    收藏  举报