font color=black size=3 face=“微软雅黑”>
1、转换流的字符编码
字符流的出现为了方便操作字符,更重要是的加入了编码转换。通过2个转换流,在两个对象进行构造的时候可以加入字符集(也就是编码表)。
- InputStreamReader
- OutputStreamWriter
另外,PrintStream与PrintWriter也可以加入编码表,但是它们只能打印而不能读取。
计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字,就将各个国家的文字用数字来表示,并一一对应,形成一张表,这就是编码表。
常见的编码表(见21-06,5.00解析)
相同的字符在不同的编码表中用不同的数字表示,因此会涉及到编码转换的问题。相应的转换流的字符编码示例如下:
package pack;
import java.io.*;
import java.util.*;
public class EncodeStream
{
public static void main(String[] args) throws IOException
{
// write();
//new FileOutputStream("G:\\gbk.txt"),"GBK":产生一个4字节的文件
//new FileOutputStream("G:\\utf.txt"),"UTF-8":产生一个6字节的文件
//上面这里的解释见——21-06,15.00解析
read();
//用GBK的文件取查UTF-8的表:??
//用UTF-8的文件取查GBK的表:浣犲ソ,读取到3个字符,因为“你好”在utf.txt文件里面有6个字节,查GBK是2个字节2个字节查询的!
}
//先用UTF-8写入字符
public static void write() throws IOException
{
//如果不指定编码表,直接FileWriter就可以
OutputStreamWriter osw =
new OutputStreamWriter(new FileOutputStream("G:\\utf.txt"),"UTF-8");//转变为字符写出流
osw.write("你好");
osw.close();
}
//再用GBK的编码方式读取
public static void read() throws IOException
{
InputStreamReader isr =
new InputStreamReader(new FileInputStream("G:\\utf.txt"),"GBK");//转变为字符读取流
//用字符数组的方式读取
char[] buf = new char[10];//注意,这里是字符读取,应该使用char数组读取
int len = isr.read(buf);
String str = new String(buf,0,len);
System.out.println(str);
isr.close();
}
}
2、字符编码
所谓的编码与接码,指的是什么呢?
默认使用GBK进行编码解码
编码:字符串变成字节数组。
String-->byte[]; str.getBytes(charsetName);
解码:字节数组变成字符串。
byte[] -->String: new String(byte[],charsetName);
编码接码的最小单位是字节数组。
编码解码的示例如下:
package pack;
import java.io.*;
import java.util.*;
public class EncodeDemo
{
public static void main(String[] args) throws UnsupportedEncodingException
{
String s = "你好";
//如果编码正确,解码错误,如何解决(见21-07,13.00解析)——开发中容易遇到这种情况,见(21-07,19.00解析)
byte[] b1 = s.getBytes("GBK");
System.out.println(Arrays.toString(b1));//[-60, -29, -70, -61]
String s1 = new String(b1,"ISO8859-1");//编码正确解码错误
System.out.println(s1);//????:没有找到对应的拉丁字符,就用位置字符“?”来表示
//我们再将s5用ISO8859-1编码,获取其在内存中对应的数值,这数值与s用GBK编码获得的数值相同
byte[] b2 = s1.getBytes("ISO8859-1");
System.out.println(Arrays.toString(b2));//[-60, -29, -70, -61]
String s2 = new String(b2,"GBK");//然后用GBK解码,就获得原来的字符,正确解码
System.out.println(s2);//你好
//另一种情况通过GBK编码,默认使用java语言默认编码表——GBK,用UTF-8解码错误
byte[] b3 = s.getBytes("GBK");//此方法会抛出UnsupportedEncodingException
System.out.println(Arrays.toString(b3));//将数组用字符串的形式表示(区别于解码)
//[-60, -29, -70, -61]:4个字节
String s4 = new String(b3,"UTF-8");//按UTF-8解码,将字节数组的内容解码为字符串
System.out.println(s4);//???:GBK字符按照UTF-8解码
//我们同样将“???”用UTF-8编码再用GBK解码
byte[] b4 = s4.getBytes("UTF-8");//按UTF-8编码
System.out.println(Arrays.toString(b4));//[-17, -65, -67, -17, -65, -67, -17, -65, -67]:发现往回编码的时候没有获得原码
String s5 = new String(b4,"GBK");//按GBK8解码
System.out.println(s5);//锟斤拷锟?:发现这种方法出错(21-07,25.50解析)
//因为GBK与UTF-8都识别中文造成的,既用UTF-8解码GBK编码的数据的时候,就会出现“?”。
//具体见文章:https://blog.csdn.net/luckarecs/article/details/7363943
}
}
3、字符编码——联通
我们新建一个文本文档,输入“哈哈”,保存后再打开就是“哈哈”,改为“联通”,保存再打开就变乱码了?(21-08)
关于上面这个问题,先说一下关于UTF-8编码表与GBK编码表的关系(21-08,2.25)。GBK编码表是2个字节代表一个字符(中文在GBK表中都是用负数表示,二进制开头为1,而英文字母用正数表示,二进制开头为0),而UTF-8可以使用1-3个字节代表一个字符。看一下下面这张UTF-8编码规律图:
UTF-8码表对于字节都加了一个标识头信息,就是通过判断字符的每一个字节对应的二进制数字开头部分的值来判断这个字符需要用几个字节来表示的。(具体的判断方法见20-08,6.20)
对于我们所说的“联通”二字,我们看一下它在GBK编码方式下的二进制表示形式
package pack;
import java.io.*;
import java.util.*;
public class EncodeDemo
{
public static void main(String[] args) throws UnsupportedEncodingException
{
String s = "联通";
byte[] by = s.getBytes("GBK");
for(byte b : by)
{//因为Integer中用32位二进制表示一个数字,我们只取一个字节的最低的有效八位
System.out.println(Integer.toBinaryString(b&255));//b先与255再转换为二进制是一样的
}
}
}
结果为:
11000001
10101010
11001101
10101000
我们看到,在使用GBK编码的时候,“联通”编码后的二进制表现符合UTF-8的编码规则,而文本在用GBK解码的时候,发现“联通”每个字节的开头“110”与“10”符合UTF-8的规则,直接查了UTF-8的表,结果没有查到相应的数字,显示乱码。我们只需要在联通前面加一个中文就可以,不可以加英文,因为英文GBK二进制开头也是0,符合UTF-8的编码,识别英文后中文还是没办法识别。
4、练习
首先,练习的需求与解法如下:
有五个学生,每个学生有3门课的成绩,从键盘输入以上数据(包括姓名,三门课成绩),
输入的格式如:zhagnsan,30,40,60计算出总成绩,并把学生的信息和计算出的总分数高低顺序存放在磁盘文件"stud.txt"中。
我们需要:
1、描述学生对象的类。
2、定义一个可操作学生对象的工具类。
思想:
1、通过获取键盘录入一行数据,并将该行中的信息取出封装成学生对象。
2、因为学生有很多,那么就需要存储,使用到集合进行存储。因为每个学生对象不一样(不重复:Set),而且要对学生的总分排序(可排序TreeSet),所以可以使用TreeSet。
3、将集合的信息写入到一个文件中。
代码如下:
package pack;
import java.io.*;
import java.util.*;
public class StudentInfoTest
{
public static void main(String[] args) throws IOException
{
//static <T> Comparator<T> reverseOrder(Comparator<T> cmp) 返回一个比较器,它强行逆转指定比较器的顺序。
Comparator<Student> cmp = Collections.reverseOrder();//传入将原始顺序转换的新顺序的比较器
// Set<Student> stus = StudentInfoTool.getStudents();//先不转变顺序
// StudentInfoTool.write2File(stus);//按照总成绩从小到大排序
Set<Student> stus1 = StudentInfoTool.getStudents(cmp);//转换顺序
StudentInfoTool.write2File(stus1);//按照总成绩从小到大排序
}
}
//首先创建一个学生类,并使其实现Comparable接口,具有自然比较性,这个比较性用泛型限定为只能是学生之间的比较
class Student implements Comparable<Student>
{
private String name;
private int cn,ma,en;//三个变量代表语数英三科
private int sum;
Student(String name , int cn , int ma , int en)
{
this.name = name;
this.cn = cn;
this.ma = ma;
this.en = en;
sum = cn + ma + en;
}
//首先,复写Comparable的compareTo方法
public int compareTo(Student obj) {
//先将sum转变为Integer包装类,比较总成绩
int num = new Integer(this.sum).compareTo(new Integer(obj.sum));
//成绩相同的情况下比较姓名
if(num == 0)
return this.name.compareTo(obj.name);
return num;//sum相同返回比较结果
}
//同时复写hashCode()方法与equals方法
public int hashCode()
{
return name.hashCode()+sum*73;
}
public boolean equals(Object obj)
{
//由于equals方法从Object继承来的,需要比较其是否属于Student
if(!(obj instanceof Student))
throw new RuntimeException("输入类型错误");
//如果输入类型是Student,将obj向下转型为Student
Student s = (Student)obj;
return this.name.equals(s.name) && this.sum == s.sum;//返回sum与姓名比较的结果
}
//接下来就是各类get方法
public int getSum()
{
return this.sum;
}
public String getName()
{
return this.name;
}
//重写toString方法
public String toString()
{
return "学生姓名:"+name+"\t"+"语文成绩:"+cn+"\t"+"数学成绩:"+ma+"\t"+"英语成绩:"+en+"\t";
}
}
//接下来设计一个用于获取并存储键盘输入学生信息的工具类
class StudentInfoTool
{
//首先设计一个类,返回一个Set<Student>的集合,该集合中存储从键盘输入的内容;
//另一方面,我们可以向该方法传入一个比较器,以实现我们想比较的内容,该比较器用泛型限定为Student
public static Set<Student> getStudents(Comparator<Student> cmp) throws IOException
{
//获取键盘输入的字符——System.in返回InputStream流,需要使用转换流转换为字符流
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
String line = null;
//先创建一个TreeSet集合的引用,可以根据传入的比较器为其赋予对象——注意,这里集合都用泛型指定其存储对象
TreeSet<Student> stus = null;
if(cmp == null)
stus = new TreeSet<Student>();
else
stus = new TreeSet<Student>(cmp);
while((line = bufr.readLine()) != null)
{
if(line.equals("over"))
break;
//先将读进来的字符串用“,”分割
String[] str = line.split(",");
//添加一个对象到TreeSet集合中,将从键盘输入获取的信息全部转换添加到这个对象中
Student stu = new Student(
str[0] , Integer.parseInt(str[1]) , Integer.parseInt(str[2]) , Integer.parseInt(str[3]));//注意将字符串的成绩转换为int类型
stus.add(stu);
}
bufr.close();
return stus;//返回添加完毕的TreeSet集合
}
//重载getStudents,这样可以使得不需要添加Comparator参数的getStudents可以表现为不添加,否则每次都要写getStudents(null)
public static Set getStudents() throws IOException
{
return getStudents(null);
}
//添加将集合中的stus中的内容写入相应文件的方法
public static void write2File(Set<Student> stus) throws IOException
{
//首先创建字符串缓冲区
BufferedWriter bufw = new BufferedWriter(new FileWriter("G:\\stud.txt"));
//接下来遍历TreeSet集合中的每一个Student对象
for(Student stu : stus)
{
bufw.write(stu.toString()+"\t");//先添加姓名
//再添加成绩,注意将sum转换为字符串,否则只会读取sum的最低8位,是按void write(int c)写入单个字符。方法写入的,这样便无法正确显示。
bufw.write("总分:"+stu.getSum()+"");//getSum返回一个整数,直接写只有最后8位,我们必须将其转换为字符串写
bufw.newLine();
bufw.flush();
}
bufw.close();
}
}