1、转换流(掌握)
我们学习字符流的时候知道字符流:它的底层是字节流和编码表。
需求:在硬盘上新建一个文本文件D:\out.txt,输入”你好”两个汉字,并以UTF-8编码保存,使用程序读取文件中的数据并打印到控制台上。
代码如下:
分析和步骤:
1)创建输入流FileReader类的对象fr,D:\out.txt作为参数;
2)定义字符数组,数组名是ch,长度是1024;
3)定义一个变量len=0记录着读取的字符个数;
4)使用循环来控制次数并输出读取到的数据;
5)关闭资源;
/*
* 转换流的演示
*/
public class ExchangeDemo {
public static void main(String[] args) throws IOException {
//创建字符输入流的对象
FileReader fr = new FileReader("D:\\out.txt");
//定义数组
char[] ch=new char[1024];
int len=0;
while((len=fr.read(ch))!=-1)
{
System.out.println(new String(ch,0,len));
}
//关闭资源
fr.close();
}
}
结果如下:
问题分析:
硬盘上有个记事本文件中保存的数据是以UTF-8编码保存的,现在我们使用的FileReader在读取这个记事本中的数据,结果记事本中保存的“你好”,但使用程序读取到的“浣犲ソ”,数据读取错误了。
错误原因:
出现上述的错误数据的原因是记事本文件中保存的数据使用的编码表和我们程序中读取数据使用的编码表不一致。导致数据错误了。
而我们知道字符流在读取数据的时候,是先从底层读取字节数据,然后再结合编码表查到对应的字符数据。现在我们希望读取到正确的数据,必须使用和保存时一致编码表。不能再使用FileReader进行读取,因为FileReader读取数据底层使用的是默认的编码表GBK来读取数据的,而这里我们需要在读取数据的时候指定编码表。
那么既然不能使用FileReader来根据默认编码表进行读取数据,那么怎样通过指定的编码表来进行读取数据呢?
Java提供了一个转换流InputStreamReader可以在读取数据的时候指定编码表,然后把字节数据根据指定编码表转成字符数据。
1.1、转换流介绍
转换流:它的主要功能就是进行字节数据和字符数据之间的转换的。转换流它就是用来转换的,它不能和底层的文件进行关联,也就是说转换流不能去读写文件中的数据。读取底层的字符数据还是得需要我们之前讲过的字符流FileReader。向硬盘中的文件中写数据得需要我们之前讲过的FileWriter类。
Java中提供了2个转换流对象:
InputStreamReader:输入流(硬盘文件数据------》内存),它的功能是把读取到的字节(硬盘上的数据都是字节数据)转换转成字符数据。字节转字符输入转换流。
OutputStreamWriter:输出流(内存---->硬盘文件),它的功能是把字符数据转成字节数据并写到硬盘上指定的文件中。字符转字节输出转换流。
转换流的执行原理如下图所示:
1.2、字节数据转成字符数据的输入转换流
用来把字节数据转为字符数据的转换流。
构造函数如下图所示:
InputStreamReader构造函数中的字节流是负责从文件中读取字节数据,然后把字节数据交给转换流。
为了不出现上述乱码的结果,我们使用转换流来实现,根据指定的编码来读取文件中的数据,
代码如下所示:
分析和步骤:
1)创建字节输入流FileInputStream 类的对象fis,D:\out.txt作为参数;
2)创建字节转成字符的输入转换流InputStreamReader类的对象isr,并指定编码方式是utf-8;
3)创建字符数组ch,数组长度是1024,并同时定义一个变量len来记录读取字符的个数;
4)使用while循环控制循环次数来读取字符并输出内容;
5)关闭转换流资源;
/*
* 字节转成字符的输入转换流
*/
public class InputStreamReaderDemo {
public static void main(String[] args) throws IOException {
//创建字节输入流对象
FileInputStream fis = new FileInputStream("D:\\out.txt");
//创建字节数据转成字符数据的输入转换流 如果这里不指定编码表,那么和FileReader一样
// InputStreamReader isr = new InputStreamReader(fis,"gbk");
InputStreamReader isr = new InputStreamReader(fis,"utf-8");
//创建数组
char[] ch=new char[1024];
int len=0;
while((len=isr.read(ch))!=-1)
{
System.out.println(new String(ch,0,len));
}
//关闭资源
isr.close();//关闭转换流在底层同时就会关闭字节流
}
}
1.3、字符数据转成字节数据的输出转换流
说明:用来把字符数据转为字节数据的转换流。
构造函数如下所示:
OutputStreamWriter:构造函数中接收的这个OutputStream是负责把OutputStreamWriter转后的字节数据写到文件中的流对象。
需求:将字符串”你好”按照utf-8编码方式输出到D:\out.txt文件中。
实现代码如下所示:
分析和步骤:
1)创建一个可以给文件中写数据的字节输出流FileOutputStream类的对象fos,D:\out.txt作为参数;
2)创建字符转成字节的输出转换流OutputStreamWriter类的对象osw,并指定编码方式是utf-8;
3)使用osw对象调用write()函数将字符串对象”你好”写到指定的文件位置;
4)关闭转换流资源;
/*
* 字符转字节输出转换流
*/
public class OutputStreamWriterDemo {
public static void main(String[] args) throws IOException {
//创建一个可以给文件中写数据的字节输出流对象
FileOutputStream fos = new FileOutputStream("D:\\out.txt");
//创建字符数据转字节数据输出转换流对象
OutputStreamWriter osw = new OutputStreamWriter(fos, "utf-8");
osw.write("你好");
osw.close();
}
}
注意:这里也有个缓冲区,也就是说写数据的时候先将数据写到缓冲区中,然后必须要调用flush()或者close()函数才能将缓冲区中的数据写到文件中。
1.4、转换流的细节
1、转换流的构造函数中接收的字节流都是负责和底层文件交互的流。
2、如果使用转换流没有传递编码表,就和FileReader或FileWriter没有任何区别。
3、只有输入的字节数据转换字符数据的转换流,没有输入的字符数据转字节数据的转换流。
4、只有输出的字符数据转字节数据的转换流,没有字节数据转字符数据的输出转换流。
2、编码、解码、乱码(掌握)
在转换流中,字符数据转字节数据,把字符数据编码成字节。字节转换成字符,把字节解码成字符。
编码:把字符数据通过查阅编码表,查到对应的编码值,然后把编码值转成字节数据的过程。
字符------> 字节的过程(编码)
字符可以使用字符串表示,字节可以使用字节数组表示。
编码:把看得懂的数据变成看不懂的数据(字符 ---> 字节)。
需求:把字符串”你好”进行编码。
分析:将一个字符串变为一个字节数据需要借助String类中的getBytes()和getBytes(String charsetName)函数,不带参数的函数是按照默认的字符集将字符串编码为字节数据,而带有参数的函数是将字符串根据指定的编码编码为字节数据。
byte[] getBytes() 使用平台的默认字符集将此 String 编码为 byte 序列
byte[] getBytes(String charsetName) 使用指定的字符集将此 String 编码为 byte 序列
代码如下所示:
分析和步骤:
1)定义一个EncodeDemo 测试类;
2)在这个类中定义一个字符串str,并赋值为”你好”;
3)使用字符串str对象调用getBytes()函数根据系统默认编码表将字符串”你好”转换成字节数组;
4)使用Arrays数组工具类调用toString()函数打印生成的字节数组;
5)在使用字符串str对象调用getBytes(String charsetName)函数根据指定的编码表将字符串”你好”转换成字节数组;
/*
* 编码演示
* 编码就是把看得懂的数据变为看不懂的数据
* 字符-----》字节
* 需求:把字符串”你好”进行编码。
*/
public class EncodeDemo {
public static void main(String[] args) throws UnsupportedEncodingException {
String str="你好";
//使用getBytes()使用平台的默认字符集将此 String 编码为 byte 序列,
//并将结果存储到一个新的 byte 数组中。
byte[] bytes = str.getBytes();//默认编码表
System.out.println(Arrays.toString(bytes));//[-60, -29, -70, -61]
//getBytes(Charset charset)
//使用给定的 charset 将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。
byte[] bytes2 = str.getBytes("GBK");//GBK编码
System.out.println(Arrays.toString(bytes2));//[-60, -29, -70, -61]
byte[] bytes3 = str.getBytes("UTF-8");//UTF-8编码
System.out.println(Arrays.toString(bytes3));//[-28, -67, -96, -27, -91, -67]
}
}
说明:
1)GBK编码表:一个中文占两个字节;
2)在这里UTF-8编码表:一个中文占三个字节。
解码:把看不懂的变成看得懂的(字节 --> 字符)
需求:把上述字符串”你好”生成的字节数据解码成为字符串。
分析:
解码:
String(byte[] bytes) 通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
String(byte[] bytes, String charsetName) 通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。
代码如下所示:
分析和步骤:
1)定义一个DecodeDemo 测试类;
2)在这个类中定义一个byte类型的字节数组b,将上述字符串”你好”按照gbk和utf-8生成的编码分别作为数组中的数据;
3)使用String类中的构造函数将字节作为数组,按照系统默认编码方式和指定的编码方式utf-8进行解码生成一个新的字符串;
4)输出生成新的字符串;
/*
* 解码演示
* 需求:把上述字符串”你好”生成的字节数据解码成为字符串。
解码:把看不懂的转成看得懂的(字节-----》字符)
*/
public class DecodeDemo {
public static void main(String[] args) throws UnsupportedEncodingException {
//定义一个字节数组 你好的gbk字节形式
// byte[] b={-60, -29, -70, -61};
//你好的UTF-8字节形式
byte[] b2={-28, -67, -96, -27, -91, -67};
/*
* String(byte[] bytes)
* 通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
*/
// String s = new String(b);//默认编码表解码
// System.out.println(s);//你好
/*
* String(byte[] bytes, String charsetName)
* 通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。
*/
// String s2 = new String(b,"GBK");//你好
//解码
String s2 = new String(b2,"UTF-8");
System.out.println(s2);//你好
}
}
乱码:编码和解码的时候使用的编码表不一致。导致解码出来的数据是错误的。
代码如下所示:
分析和步骤:
1)定义一个LuanCodeDemo 测试类;
2)在这个类中定义一个字符串str,并赋值为”你好”;
3)使用字符串str对象调用getBytes(“UTF-8”)函数根据指定的UTF-8编码表将字符串”你好”转换成字节数组bytes;
4)解码 ,使用String类的构造函数以GBK的编码表进行解码,但是这样会发生乱码;
5)由于编码使用UTF-8方式进行编码,所以解码也得使用UTF-8,否则会乱码;
6)既然上述使用GBK解码方式已经发生乱码了,我们应该在此基础上进行解决乱码的问题,我们需要使用生成的字符串对象调用getBytes()函数将字符串以gbk编码表在转换为字节数组进行编码;
7)重新使用正确的编码表UTF-8进行解码;
/*
* 乱码演示
*/
public class LuanCodeDemo {
public static void main(String[] args) throws UnsupportedEncodingException {
//定义字符串对象
String str="你好";
//编码
byte[] bytes = str.getBytes("UTF-8");
//解码 由于编码采用的是UTF-8,而解码采用的是GBK这样会发生乱码
String str2 = new String(bytes,"GBK");//浣犲ソ
//解码 由于编码采用的是UTF-8,而解码采用的也是UTF-8这样不会发生乱码
// String str3 = new String(bytes,"UTF-8");//你好
//输出解码后的结果
System.out.println(str2);//浣犲ソ
// System.out.println(str3);//你好
/*
* 上述乱码的原因是将字符串str="你好"以UTF-8进行编码
* 然后以GBK进行解码
* 编码和解码的码表不一致 所以会发生乱码
* 那么这里我们只能再次按照GBK编码表再次编码
*/
//编码
byte[] bytes2 = str2.getBytes("GBK");
//[-28, -67, -96, -27, -91, -67]
System.out.println(Arrays.toString(bytes2));
//重新用正确的编码进行解码
String str4 = new String(bytes2,"UTF-8");
System.out.println(str4);//你好
}
}
上述代码的图解如下图所示:
说明:
发生乱码:按照反方向编码回去,编码的时候一定要使用导致解码错误的编码表,得到编码后的数据,再使用正确的编码表进行解码。这种方式不是万能的。一旦解码中有未知的错误字符,就无法还原。
比如某些字符串中的汉字 ”联通” 就会发生未知字符的现象。 结果是:联? 这里的?就表示未知字符。
总结:
上述对于联通解码不出来,原因是由于在GBK码表中具有未知字符,而对于未知字符,解码的时候也会按照未知字符进行处理,所以为了发生上述情况,我们以后学习的软件(比如:服务器Tomcat)中底层的编码表全部是没有未知字符的编码表ISO-8859-1。