Java基础——IO流(四)

序列化对象

将堆内存中的东西(对象)写到硬盘中

ObjectOutputStream(OutputStream)中的writeObject方法可以将对象写入到硬盘中来
ObjectInputStream(InputStream)中的readObject方法可以将硬盘中的对象读取出来。

先上手写程序:
先定义一个Person类,作为要操作的对象

class Person 
{
	String name;
	int age;
	Person(String name,int age)
	{
		this.name = name;
		this.age = age;
	}
	public String toString()
	{
		return name+"....."+age;
	}
}

写入对象的代码如下:

//将对象写入到硬盘中来
import java.io.*;
public class ObjectStreamDemo {
	public static void main(String[] args)  throws Exception
	{
		writeObj();
	}
	public static void writeObj() throws Exception
	{
		//将目标锁定为obj.txt文件
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.txt"));

		//开始写入文件
		oos.writeObject(new Person("zhangsan",23));
	}
}

编译后运行出现以下结果:

对象写入硬盘发生的错误

通过查找API了解该异常:

异常的详细解释

Person类需要具有序列化接口。

也就是说Person类要实现Serializable接口。
将Person类实现了Serializable接口后,再次执行程序,得到obj.txt存储对象的文件。

存储着Person对象的txt文件

那么既然能将对象写进文件中,也就能将文件中得对象读出来,

//将对象写入到硬盘中来
import java.io.*;
public class ObjectStreamDemo {
	public static void main(String[] args)  throws Exception
	{
		//writeObj();
		readObj();
	}
	public static void writeObj() throws Exception
	{
		//将目标锁定为obj.txt文件
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.txt"));

		//开始写入文件
		oos.writeObject(new Person("zhangsan",23));
		oos.close();
	}
	public static void readObj() throws Exception
	{
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.txt"));
		Person p = (Person)ois.readObject();
		System.out.println(p);
		ois.close();
	}	
}

调用读取文件的方法获取对象,得到如下结果:

读取文件获取对象

这时当我将Person类中的name属性变成私有的,再次编译,运行程序:
这时候文件中的Person对象是之前的,Person类代码是重新修改后的,

两个对象的UID不一样

结果显示,从文件获取到的对象和调用Person获取到的对象的UID不一样,

当把private去掉之后,运行程序,就又可以获取到对象了,

这说明UID序列号是根据类的成员算出来的。

如果想要加上私有之后,还可以从之前的文件对象中获取到修改后的对象。

就是不让java给我算UID。我用自己定义UID,就可以了

import java.io.*;
class Person implements Serializable
{
	//写上自己的UID
	public static final long serialVersionUID = 42L;

	private String name;
	int age;
	Person(String name,int age)
	{
		this.name = name;
		this.age = age;
	}
	public String toString()
	{
		return name+"....."+age;
	}
}

这样不管参数中有没有加私有修饰符都不会影响从文件中读取对象。

注意,静态是不能被序列化的。
因为静态成员是在方法区中存在的,是不能被序列化的
只有堆中的对象才可以被序列化。

如果内容不是静态的,但是还不想被序列化,可以在元素前添加transient标签。
保证其值在堆内存中存在,而不在文本中存在。

这是存入一个对象,如果存入多个对象的话也不用着急,因为readObject方法第一次读数据返回的是第一个对象,第二次读数据返回的就是第二个对象。

管道流

输入输出可以直接进行连接,通过结合线程使用。
PipedInputStream();
PipedOutputStream();
这里写图片描述

由图上信息可知,此处应该有多线程,

是什么原理呢?

就像是一根管子,里边同时放着输入(读取)流和输出(写入)流
当读取流没有读取到数据时,会等待,等到写入流写上数据了,读取流才会启动,将数据读出来,以上过程是由两个线程完成的。

//管道流的实例
import java.io.*;
//定义两个线程,一个为读,一个写,
//定义两个流对象,接在一块,形成管道流
//用两个线程一起操作数据
class Read implements Runnable
{
	private InputStream in;
	Read(InputStream in)
	{
		this.in = in;
	}
	public void run()
	{
		try
		{
			//线程一开始就开始读取数据
			byte[] buf = new byte[1024];
			System.out.println("没有读到数据,等待");
			int len = in.read(buf);
			System.out.println("读到数据,开始打印");
			String s = new String(buf,0,len);
			//读到数据后打印,关流
			System.out.println(s);
			in.close();
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
			
	}
}
class Write implements Runnable
{
	private OutputStream out;
	Write(OutputStream out)
	{
		this.out = out;
	}
	public void run()
	{
		try
		{
			//线程一开启就写入数据
			System.out.println("6秒后写入数据....");
			Thread.sleep(6000);
			out.write("你好,世界".getBytes());
			System.out.println("数据写入完毕");
			out.close();
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
		
	}
}
public class PipedStreamDemo {
	public static void main(String[] args) 
	{
		//创建出来两个管道流
		PipedInputStream pis = new PipedInputStream();
		PipedOutputStream pos = new PipedOutputStream();
		try
		{
			//将两个管道流连接起来的方法
			pis.connect(pos);
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		//开启两个线程
		Read r = new Read(pis);
		Write w = new Write(pos);
		new Thread(r).start();
		new Thread(w).start();
	}
}

运行结果:
管道流实例

随机访问文件RandomAccessFile

该类不算是IO体系中的子类,
而是直接继承自Object

但是他是IO包中的成员,因为他具备读取和写入的功能。
内部封装了一个数组,而且通过指针对数组的元素进行操作
可以通过getFilePointer获取指针位置。
同时可以通过seek方法改变指针的位置。

其实完成读写的原理就是内部封装了字节输入流和输出流

通过构造函数可以看出,该类只能操作文件
而且操作文件还有模式:只读r,读写rw等

如果模式为只读r,不会创建文件,会去读取一个已经存在的文件,如果该文件不存在,则会出现异常
如果模式为rw
而且该对象的构造函数要操作的文件不存在,会自动创建,如果存在不会覆盖。

既然随机访问文件对象可以对文件进行读取和写入的,我们就先演示一下写文件的方法。

import java.io.*;
public class RandomAccessFileDemo {
	public static void main(String[] args) throws IOException 
	{
		writeDemo();
	}
	public static void writeDemo() throws IOException 
	{
		//先创建一个对象,直接关联文件,并且指定文件的访问类型为可读可写
		RandomAccessFile raf = new RandomAccessFile("random.txt","rw");
		//因为他里边封装的是一个字节数组,所以这里应该使用字节数据写入
		raf.write("张三".getBytes());
		raf.write(97);

		//里边封装的是字节流对象的访问模式,所以这里也是要关闭资源的
		raf.close();
	}
}

运行结果:

写入数据实例

结果显示:可以存进去,但是写进去的97成了字符a。这是因为我写入的是字节数据,在写进记事本的时候,会按照传进来的字节数据按照查表的方式获取到相应的字符,然后写入到记事本中。

这里需要注意的是,自由访问文件流中的write方法和流中的write方法一样,也是只写八位,也就是一个字节(即使传进来的数据是4个字节或者更多),
那么就出现了局限性,要是我写入的数据长度大于一个字节的最大表示数(255)要写入的数据不仅仅是8位的数据
而write方法只能操作8位。
这样会造成数据的损失。
所以我们可以在写数据的时候用的是writeInt方法,可以一次写入4个字节的数据。

那么接下来演示读取文件中数据的方法。

重新用writInt方法写了一次人的年龄,所以人的年龄是4个字节了。

//使用自由访问文件对象获取文件中的信息
import java.io.*;
public class RandomAccessFileDemo {
	public static void main(String[] args) throws IOException 
	{
		//writeDemo();
		readDemo();
	}
	public static void readDemo()  throws IOException 
	{
		//先创建一个自由访问文件流对象,和文件关联,并且设置权限为只读
		RandomAccessFile raf = new RandomAccessFile("random.txt","r");
		//因为读到的都是字节,而且read方法一次只能读取一个字节的数据,
		//所以先将读到的数据放进自定义好的数组中
		byte[] buf = new byte[4];
		raf.read(buf);
		String name = new String(buf);
		//而readInt方法可以直接读取到四个字节的内容
		int age = raf.readInt();
		System.out.println(name+"...."+age);

	}
	public static void writeDemo() throws IOException 
	{
		//先创建一个对象,直接关联文件,并且指定文件的访问类型为可读可写
		RandomAccessFile raf = new RandomAccessFile("random.txt","rw");
		//因为他里边封装的是一个字节数组,所以这里应该使用字节数据写入
		raf.write("张三".getBytes());

		//使用write方法之后这里的97就占用了4个字节
		raf.writeInt(97);
		raf.write("李四".getBytes());
		raf.writeInt(99);

		//里边封装的是字节流对象的访问模式,所以这里也是要关闭资源的
		raf.close();
	}
}

运行结果为:

自有访问文件对象获取文件中的信息

如果要同时获取到张三李四的信息,可以直接在以上代码中将readDemo方法中的read方法和readInt方法再多写一次。

这里我想要直接获取到李四的信息,应该怎么做呢?

使用到了随机访问对象的设置指针的方法,seek方法

演示:

import java.io.*;
public class RandomAccessFileDemo {
	public static void main(String[] args) throws IOException 
	{
		//writeDemo();
		readDemo_2();
	}
	//直接跳转指针的方式跳过前边的信息获取排在后边的信息
	public static void readDemo_2() throws IOException 
	{
		//直接获取李四的信息
		RandomAccessFile raf = new RandomAccessFile("random.txt","r");
		//指针向后跳到8的位置上。从这里开始读取
		raf.seek(8);
		byte[] buf = new byte[4];
		raf.read(buf);
		String name = new String(buf);
		int age = raf.readInt();
		System.out.println(name+"...."+age);
	}
	public static void readDemo()  throws IOException 
	{
		//先创建一个自由访问文件流对象,和文件关联,并且设置权限为只读
		RandomAccessFile raf = new RandomAccessFile("random.txt","r");
		//因为读到的都是字节,而且read方法一次只能读取一个字节的数据,
		//所以先将读到的数据放进自定义好的数组中
		byte[] buf = new byte[4];
		raf.read(buf);
		String name = new String(buf);
		//而readInt方法可以直接读取到四个字节的内容
		int age = raf.readInt();
		System.out.println(name+"...."+age);

	}
	public static void writeDemo() throws IOException 
	{
		//先创建一个对象,直接关联文件,并且指定文件的访问类型为可读可写
		RandomAccessFile raf = new RandomAccessFile("random.txt","rw");
		//因为他里边封装的是一个字节数组,所以这里应该使用字节数据写入
		raf.write("张三".getBytes());

		//使用write方法之后这里的97就占用了4个字节
		raf.writeInt(97);
		raf.write("李四".getBytes());
		raf.writeInt(99);

		//里边封装的是字节流对象的访问模式,所以这里也是要关闭资源的
		raf.close();
	}
}

运行结果:

直接跳转角标获取元素的值

还有一种方法获取指定位置上的元素就是:
跳过指定的角标开始获取:skipBytes();

import java.io.*;
public class RandomAccessFileDemo2 {
	public static void main(String[] args) throws IOException 
	{
		skipBytesDemo();
	}
	//直接跳转指针的方式跳过前边的信息获取排在后边的信息
	public static void skipBytesDemo() throws IOException 
	{
		//直接获取李四的信息
		RandomAccessFile raf = new RandomAccessFile("random.txt","r");
		//指针向后跳8个角标
		raf.skipBytes(8);
		//开始读取
		byte[] buf = new byte[4];
		raf.read(buf);
		String name = new String(buf);
		int age = raf.readInt();
		System.out.println(name+"...."+age);
	}
	
}

运行结果:

使用向后跳指针的方法来获取指定位置上的元素

seek方法和skipBytes方法都可以指定指针的位置来获取指定位置上的元素。
两者有不同点,就是seek方法可以指定任意位置(可以向前也可以向后),而skipBytes方法只能向后跳,不可以再返回来。

使用seek方法除了可以获取指定位置的元素,还可以在指定位置添加元素。包括跳过中间位置。

演示:

import java.io.*;
public class RandomAccessFileDemo2 {
	public static void main(String[] args) throws IOException 
	{
		addElement();
	}
	public static void addElement() throws IOException 
	{
		RandomAccessFile raf = new RandomAccessFile("random.txt","rw");
		raf.seek(8*3);//在角标位24的位置上。开始写数据
		raf.write("王五".getBytes());
		raf.writeInt(103);
		raf.close();
	}
	
}

运行结果:

跨过区域写数据

import java.io.*;
public class RandomAccessFileDemo2 {
	public static void main(String[] args) throws IOException 
	{
		addElement();
	}
	//seek方法还可以对已有元素的位置进行数据的修改
	public static void addElement() throws IOException 
	{
		RandomAccessFile raf = new RandomAccessFile("random.txt","rw");
		raf.seek(8*0);//在角标位0的位置上。开始写数据
		//这样写一下就相当于是修改了数据了
		raf.write("周七".getBytes());
		raf.writeInt(106);
		raf.close();
	}
	
}

运行结果:

这里写图片描述

发现,在第一个位置存储的元素由张三变成了周七,seek可以实现对文件中数据的修改。

这样的原理可以用到下载视频上来,一个线程下载020M,同时另一个线程下载2040M,,,,这样下载下来的视频是有连接的,可以提高效率。

DataInputStream与DataOutputStream

可以用来操作基本数据类型的数据的流对象

直接演示方法:

//演示操作基本数据类型的流对象的方法
import java.io.*;
public class DataStreamDemo {
	public static void main(String[] args)  throws IOException
	{
		writeDemo();
	}
	public static void writeDemo() throws IOException
	{
		//先创建对象
		DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"));
		//写数据有各自的方法
		dos.writeInt(7845);
		dos.writeBoolean(false);
		dos.writeDouble(452.452);
		dos.close();
	}
}

运行结果:

基本数据对象的写入

出现乱码,正常,因为是在记事本中,会有一个查编码表的过程。

接下来读取刚刚写入的数据

//演示操作基本数据类型的流对象的方法
import java.io.*;
public class DataStreamDemo {
	public static void main(String[] args)  throws IOException
	{
		//writeDemo();
		readDemo();
	}
	public static void writeDemo() throws IOException
	{
		//先创建对象
		DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"));
		//写数据有各自的方法
		dos.writeInt(7845);
		dos.writeBoolean(false);
		dos.writeDouble(452.452);
		dos.close();
	}
	public static void readDemo()  throws IOException
	{
		DataInputStream dis = new DataInputStream(new FileInputStream("data.txt"));
		int num = dis.readInt();
		boolean b = dis.readBoolean();
		double d = dis.readDouble();
		//这里读取的顺序必须是按照之前写入的顺序来,
		dis.close();
		System.out.println("num= "+num);
		System.out.println("b= "+b);
		System.out.println("d= "+d);
	}
}

运行结果为

使用基本数据类型操作数据

可以获取到数据。

writeUTF方法的演示:

//演示操作基本数据类型的流对象的方法
import java.io.*;
public class DataStreamDemo {
	public static void main(String[] args)  throws IOException
	{
		UTFWrite();
	}
	public static void UTFWrite() throws IOException
	{
		DataOutputStream dos = new DataOutputStream(new FileOutputStream("utf.txt"));
		dos.writeUTF("你好");
		dos.close();
	}
}

运行结果:
修改后UTF编码的写入数据文件大小为8字节

使用DataOutputStream的writeUTF方法写入的数据,只有同类的readUTF方法获取到数据。

读取数据

//演示操作基本数据类型的流对象的方法
import java.io.*;
public class DataStreamDemo {
	public static void main(String[] args)  throws IOException
	{
		UTFRead();
	}
	public static void UTFWrite() throws IOException
	{
		DataOutputStream dos = new DataOutputStream(new FileOutputStream("utf.txt"));
		dos.writeUTF("你好");
		dos.close();
	}
	public static void UTFRead()  throws IOException
	{
		DataInputStream dis = new DataInputStream(new FileInputStream("utf.txt"));
		String s = dis.readUTF();
		dis.close();
		System.out.println(s);
	}
}

运行结果:
使用readUTF读取数据

获取成功

当需要使用GBK或者UTF-8编码写入数据的时候用到了转换流:

//演示操作基本数据类型的流对象的方法
import java.io.*;
public class DataStreamDemo {
	public static void main(String[] args)  throws IOException
	{
		GBKWrite();
	}
	public static void GBKWrite() throws IOException
	{
		OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("gbk.txt"),"GBK");
		osw.write("你好");
		osw.close();
	}
}

写入成功:文件大小为4字节:

使用GBK编码表

使用utf-8呢?

//演示操作基本数据类型的流对象的方法
import java.io.*;
public class DataStreamDemo {
	public static void main(String[] args)  throws IOException
	{
		GBKWrite();
	}
	public static void GBKWrite() throws IOException
	{
		OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("utf-8.txt"),"UTF-8");
		osw.write("你好");
		osw.close();
	}
}

运行结果:写入成功,文件大小为6字节
utf-8编码写入数据

当用readUTF(使用修改版的UTF_8编码表)方法读取用utf_8编码的文件时,会出现这样的效果:

抛出异常

该异常的具体信息是:

这里写图片描述

所以说用writeUTF写入的数据,只有用readUTF才能读取的出来。

那么utf-8编码表和readUTF码表中用到的utf_8修改版有什么不一样的呢?

utf-8修改版和没修改版的区别

操作字节数组

ByteArrayInputStream与ByteArrayOutputStream

用于操作字节数组的流对象

ByteArrayInputStream :在构造的时候,需要接受数据来源,而且数据源是一个字节数组。
ByteArrayOutputStream: 在构造的时候,不用定义数据目的,因为该对象中已经内部封装了可变长度的字节数组。这就是数据目的。

因为这两个流对象都操作的数组,并没有调用到系统资源
所以,不用进行close关闭。

演示一下字节数组的操作方法。

//演示字节数组流对象的操作方法

import java.io.*;
public class ByteArrayStream {
	public static void main(String[] args) 
	{
		//源设备,给定一个字节数组
		ByteArrayInputStream bais = new ByteArrayInputStream("abcdef".getBytes());
		//目的设备:里边自己封装了一个可变长度的数组,这就是目的了
		ByteArrayOutputStream baos = new ByteArrayOutputStream();

		//开始读写操作
		int by = 0;
		while((by = bais.read())!=-1)
		{
			baos.write(by);
		}
		//将目的中的东西打印出来
		System.out.println(baos.toString());
	}
}

运行结果为:

使用字节数组流进行读写操作

在流操作规律讲解时:

源设备:键盘 System.in 硬盘 FileStream 内存:ArrayStream
目的设备:控制台:System.out 硬盘:FileStream 内存:ArrayStream

有时候要将一个文件中的内容放到一个数组里先存起来,这个时候,源就是硬盘,目的就是内存。就用到了字节数组对象。

其实字节数组流对象就是用流的思想来操作数组的,
设定自己的源和目的,判断读取的值是否为-1.也就是相当于判断有没有到了数组的末尾。
一次一次的读取操作也就是数组中的遍历元素。
写入操作也就是数组中的设置某一个位置上的元素的值。

只不过是把读取的方法封装起来了。

writeTo 方法

writeTo(OutputStream os):将数组中的数据写入到硬盘中取。

那么字节数组流对象是操作字节数组的,那要是操作字符数组怎么办呢?
有CharArrayReader和CharArrayWriter

里边也存在源,就是要接收一个字符数组,
目的就是新建一个空的缓冲区。
里边的方法有toCharArray,append,write,toString

同时想操作数组的话还有StringReader和StringWriter

都是同样的道理源接收一个字符串作为源,目的是一个空的字符串。

字符编码

  • 字符流的出现是为了方便操作字符
  • 更重要的是加入了编码转换
  • 通过子类转换流来完成
    • InputStreamReader
    • OutputStreamWriter
  • 在两个对象进行构造的时候可以加入字符集

编码表的由来

  • 计算机只能识别二进制数据,早期由来是电信号
  • 为了方便应用计算机,让它可以识别各个国家的文字
  • 就将各个国家的文字用数字来表示,并一一对应。形成一张表。
  • 这就是编码表

常见的编码表

  • ASCII美国标准信息交换码
    • 用一个字节的7为就可以表示
  • ISO8859-1:拉丁码表,欧洲码表
    • 用一个字节 的8为表示
  • GB2312:中国的中文编码表
  • GBK:中国的中文编码表升级,融合了更多的中文文字符号
  • Unicode:国际标准码,融合了多种文字
    • 所有文字都用两个字节来表示,Java语言使用的就是Unicode
  • UTF-8:最多用三个字节来表示一个字符。

那么现在,问题来了,GBK可以表示汉字的编码,UTF-8也可以表示汉字编码,但是相同的字在这两个编码表中不一样,这样会出什么问题呢?

用一段代码来演示:

//字符编码——使用转换流
//使用GBK编码给文件中写入一段数据
//再用UTF-8编码给文件写入同样的数据
import java.io.*;
public class EncodeStream {
	public static void main(String[] args)  throws IOException
	{
		writeGBK();//使用GBK编码表给文件中写入你好
		writeUTF();//使用UTF-8编码表给文件中写入你好
	}
	public static void writeGBK() throws IOException
	{
		OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("gbk.txt"),"GBK");
		osw.write("你好");
		osw.close();
	}
	public static void writeUTF() throws IOException
	{
		OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("utf.txt"),"UTF-8");
		osw.write("你好");
		osw.close();
	}
}

运行结果发现,gbk.txt文件中有“你好”,文件大小4KB
utf.txt文件中有“你好”文件大小6KB

接下来使用各自的读取方式去读文件

//字符编码——使用转换流
//使用GBK编码读取用GBK码表写上去的文件
//再用UTF-8编码读取用UTF-8码表写上去的文件
import java.io.*;
public class EncodeStream {
	public static void main(String[] args)  throws IOException
	{
		readGBK();
		readUTF();
	}
	public static void readGBK() throws IOException
	{
		InputStreamReader isr = new InputStreamReader(new FileInputStream("gbk.txt"),"gbk");
		char[] chs = new char[10];//已经知道了文件的大小,就可以直接定义数组的大小,不用再去循环了
		int len = isr.read(chs);
		String str = new String(chs,0,len);
		System.out.println("GBK编码读到的结果为:"+str);
	}
	public static void readUTF() throws IOException
	{
		InputStreamReader isr = new InputStreamReader(new FileInputStream("utf.txt"),"UTF-8");
		char[] chs = new char[10];
		int len = isr.read(chs);
		String str = new String(chs,0,len);
		System.out.println("UTF-8编码读到的结果为:"+str);
	}
}

运行结果为:

使用各自的编码表读取到的数据

那如果对文件进行读取的时候指定的编码表出现了错误的时候呢?

例如,使用GBK码表去读取UTF-8编码的文件,用UTF-8码表去读取GBK编码的文件

//字符编码——使用转换流
//使用GBK编码读取用utf-8码表写上去的文件
//再用UTF-8编码读取用GBK码表写上去的文件
import java.io.*;
public class EncodeStream {
	public static void main(String[] args)  throws IOException
	{
		readGBK();
		readUTF();
	}
	public static void readGBK() throws IOException
	{
		InputStreamReader isr = new InputStreamReader(new FileInputStream("gbk.txt"),"UTF-8");
		char[] chs = new char[10];//已经知道了文件的大小,就可以直接定义数组的大小,不用再去循环了
		int len = isr.read(chs);
		String str = new String(chs,0,len);
		System.out.println("GBK编码读到的结果为:"+str);
	}
	public static void readUTF() throws IOException
	{
		InputStreamReader isr = new InputStreamReader(new FileInputStream("utf.txt"),"GBK");
		char[] chs = new char[10];
		int len = isr.read(chs);
		String str = new String(chs,0,len);
		System.out.println("UTF-8编码读到的结果为:"+str);
	}
}

运行结果:

交替编码表读取出来的数据

为什么会出来这种情况呢?

我们知道,GBK编码表中一个汉字占用的两个字节
而UTF-8码表中,一个汉字占用了三个字节
在具体读取中,过程是这样的。
使用编码表读取文件过程详解

字符编码

编码:字符串变成字节数组
解码:字节数组变成字符串

通常我们将字符串变成字节数组的方法是:str.getBytes();
通常我们将字节数组变成字符串的方法是:String s = new String(byte[]);

以上方法都是按照默认的编码表(GBK)来进行转换的。

想要使用自己的指定的编码表进行转换的话,
有重载方法:

编码:str.getBytes(String charsetName);charsetName表示字符集
解码:String s = new String(byte[] buf,String charsetName);给定字节数组,并指定编码表进行解码。

先看看两种编码形式返回来的字节数组是什么样的:

//字符编码
//看看默认编码形式获取到的字节数组是什么样的
import java.util.*;
public class EncodeDemo {
	public static void main(String[] args) 
	{
		String s = "你好";
		byte[] b1 = s.getBytes();//这里按照默认的方式进行编码
		System.out.println(Arrays.toString(b1));//将编码后返回的字节数据打印出来
	}
}

运行结果:
这里写图片描述

//字符编码
//看看GBK编码形式获取到的字节数组是什么样的
import java.util.*;
public class EncodeDemo {
	public static void main(String[] args) 
	{
		String s = "你好";
		byte[] b1 = null;
		try
		{
			b1 = s.getBytes("GBK");//这里按照GBK的方式进行编码,会发生异常,因为可能会传的参数没有这样的码表	
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println(Arrays.toString(b1));//将编码后返回的字节数据打印出来
	}
}

运行结果:
这里写图片描述

发现默认编码和指定的GBK编码出来的字节数组是一样的,也就证实了,默认的编码方式是GBK的。

接下来用utf-8的方式进行编码,看看出来的字节数组是什么样的?

//字符编码
//看看UTF-8编码形式获取到的字节数组是什么样的
import java.util.*;
public class EncodeDemo {
	public static void main(String[] args) 
	{
		String s = "你好";
		byte[] b1 = null;
		try
		{
			b1 = s.getBytes("utf-8");//这里按照UTF-8的方式进行编码,会发生异常,因为可能会传的参数没有这样的码表	
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println(Arrays.toString(b1));//将编码后返回的字节数据打印出来
	}
}

运行结果:

这里写图片描述

发现,使用utf-8编码之后返回一共有6个字节,也就是说一个汉字由三个字节组成。

以上是对汉字进行编码的过程,接下来演示解码GBK的过程。

//字符编码
//GBK码表的解码过程
import java.util.*;
public class EncodeDemo {
	public static void main(String[] args) 
	{
		//******************编码过程****************************************************
		String s = "你好";
		byte[] b1 = null;
		try
		{
			b1 = s.getBytes("GBK");//这里按照GBK的方式进行编码,会发生异常,因为可能会传的参数没有这样的码表	
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println(Arrays.toString(b1));//将编码后返回的字节数据打印出来


		//******************解码过程****************************************************

		String s2 = null;
		try
		{
			s2 = new String(b1,"GBK");//这里传进来一个字节数组,然后指定编码表,会解码成一个字符串
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("s2="+s2);//将解码后的数据打印出来

	}
}

运行结果:

这里写图片描述

接下来演示解码UTF-8文件

//字符编码
//UTF-8码表的解码过程
import java.util.*;
public class EncodeDemo {
	public static void main(String[] args) 
	{
		//******************编码过程****************************************************
		String s = "你好";
		byte[] b1 = null;
		try
		{
			b1 = s.getBytes("UTF-8");//这里按照UTF-8的方式进行编码,会发生异常,因为可能会传的参数没有这样的码表	
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println(Arrays.toString(b1));//将编码后返回的字节数据打印出来


		//******************解码过程****************************************************

		String s2 = null;
		try
		{
			s2 = new String(b1,"UTF-8");//这里传进来一个字节数组,然后指定编码表,会解码成一个字符串
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("s2="+s2);//将解码后的数据打印出来

	}
}

运行结果:

这里写图片描述

那么如果使用不同的编码表读取数据呢?

//字符编码
//GBK读取Utf-8编写的数据
import java.util.*;
public class EncodeDemo {
	public static void main(String[] args) 
	{
		GBKreadU8();
	}
	public static void GBKreadU8()
	{
		//******************编码过程****************************************************
		String s = "你好";
		byte[] b1 = null;
		try
		{
			b1 = s.getBytes("UTF-8");//这里按照UTF-8的方式进行编码,会发生异常,因为可能会传的参数没有这样的码表	
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("使用Utf-8编码之后的字节数组为:"+Arrays.toString(b1));//将编码后返回的字节数据打印出来


		//******************解码过程****************************************************

		String s2 = null;
		try
		{
			s2 = new String(b1,"GBK");//这里传进来一个字节数组,然后指定编码表,会解码成一个字符串
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("使用GBK码表去读取Utf-8码表编写的数据结果为:"+s2);//将解码后的数据打印出来

	}
	
}

运行结果:
这里写图片描述

如果在写数据的时候使用了错误的编码形式,比如说ISO8859-1(欧洲语言码表),就没有办法了,因为发送了错误的编码形式,对方不知道这到底是什么样的编码形式。

如果编写的时候用的是GBK(编对了),但是解析的时候写错了(用了ISO8859-1编码表)
会出现这样的情况:

//字符编码
//GBK读取Utf-8编写的数据
import java.util.*;
public class EncodeDemo {
	public static void main(String[] args) 
	{
		ISOreadGBK();
	}
	public static void ISOreadGBK()
	{
		//******************编码过程****************************************************
		String s = "你好";
		byte[] b1 = null;
		try
		{
			b1 = s.getBytes("GBK");//这里按照GBK的方式进行编码,会发生异常,因为可能会传的参数没有这样的码表	
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("使用Utf-8编码之后的字节数组为:"+Arrays.toString(b1));//将编码后返回的字节数据打印出来


		//******************解码过程****************************************************

		String s2 = null;
		try
		{
			s2 = new String(b1,"ISO8859-1");//这里传进来一个字节数组,然后指定编码表,会解码成一个字符串
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("使用ISO8859-1码表去读取GBK码表编写的数据结果为:"+s2);//将解码后的数据打印出来

	}
	
}

运行结果:
这里写图片描述

这是怎么样的一个过程呢?
当“你好”被解码的时候,解码成为-60,-29,-70,-61这是没有错的,
但是当解析的时候,用这些字节数组去查表的时候查的是ISO8859-1,
查一个,找不到,返回来一个?,后边的都一样找不到,所以会返回来4个?,

想要能把数据解析出来,可以这样做:

//字符编码
//GBK读取Utf-8编写的数据
import java.util.*;
public class EncodeDemo {
	public static void main(String[] args) 
	{
		ISOreadGBK();
	}
	public static void ISOreadGBK()
	{
		//******************编码过程****************************************************
		String s = "你好";
		byte[] b1 = null;
		try
		{
			b1 = s.getBytes("GBK");//这里按照GBK的方式进行编码,会发生异常,因为可能会传的参数没有这样的码表	
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("使用Utf-8编码之后的字节数组为:"+Arrays.toString(b1));//将编码后返回的字节数据打印出来


		//******************解码过程****************************************************

		String s2 = null;
		try
		{
			s2 = new String(b1,"ISO8859-1");//这里传进来一个字节数组,然后指定编码表,会解码成一个字符串
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("使用ISO8859-1码表去读取GBK码表编写的数据结果为:"+s2);//将解码后的数据打印出来

		//******************再编码*******************************************************
		byte[] b2 = null;
		try
		{
			b2 = s2.getBytes("ISO8859-1");//对获取到的错误的乱码再进行基于原码表的解码获取到字节数组
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("对乱码再进行编码后得到的字节数组为:"+Arrays.toString(b2));

		//******************再编码*******************************************************

		//针对字节数组,再指定GBK编码表进行解码数据
		String s3 = null;
		try
		{
			s3 = new String(b2,"GBK");
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("进行再编码解码之后得到的数据为:"+s3);


	}
	
}

运行结果为:

这里写图片描述

过程是这样的:

乱码转换流程图解

这种方法适用在tomcat服务器的使用中,因为服务器中的默认编码形式为ISO8859-1,所以这就涉及到一个编码转换的问题了。

同样的道理,我们可不可以用GBK编码,用UTF-8解码,得到的乱码再进行编码,解码得到原来的数据呢?

//字符编码
//GBK读取Utf-8编写的数据
import java.util.*;
public class EncodeDemo {
	public static void main(String[] args) 
	{
		ISOreadGBK();
	}
	public static void ISOreadGBK()
	{
		//******************编码过程****************************************************
		String s = "你好";
		byte[] b1 = null;
		try
		{
			b1 = s.getBytes("GBK");//这里按照GBK的方式进行编码,会发生异常,因为可能会传的参数没有这样的码表	
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("使用Utf-8编码之后的字节数组为:"+Arrays.toString(b1));//将编码后返回的字节数据打印出来


		//******************解码过程****************************************************

		String s2 = null;
		try
		{
			s2 = new String(b1,"Utf-8");//这里传进来一个字节数组,然后指定编码表,会解码成一个字符串
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("使用Utf-8码表去读取GBK码表编写的数据结果为:"+s2);//将解码后的数据打印出来

		//******************再编码*******************************************************
		byte[] b2 = null;
		try
		{
			b2 = s2.getBytes("Utf-8");//对获取到的错误的乱码再进行基于原码表的解码获取到字节数组
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("对乱码再进行编码后得到的字节数组为:"+Arrays.toString(b2));

		//******************再编码*******************************************************

		//针对字节数组,再指定GBK编码表进行解码数据
		String s3 = null;
		try
		{
			s3 = new String(b2,"GBK");
		}
		catch (Exception e)
		{
			throw new RuntimeException("指定的编码表不存在");
		}
		System.out.println("进行再编码解码之后得到的数据为:"+s3);


	}
	
}

运行结果为:

这里写图片描述

结果发现,并没有出现我们想要的结果,
用ISO8859-1就可以实现乱码的还原,为什么用Utf-8反而不行了呢?

在上边运行结果中我们发现,第一次编码没有问题,得到了GBK对应的字节数组,
之后使用Utf-8码表去解析的时候出错了,得到了乱码字符,也没有问题
到对乱码在进行编码的时候,出现了和源码不一样的字节数组,
问题就出在了这里,

从GBK的编码获得的字节数组查Utf-8码表的时候找不到,返回了“?”
这个“?”在Utf-8中是存在了未知字符区域,
当再进行编码的时候就会给?重新编一个值,编成了一个三个字节的?

为什么Utf-8会重新编值呢?而ISO8859-1就不会重新编值呢?

因为Utf-8和GBK都可以识别中文,而ISO8859-1不识别中文,不会对中文进行编码,读到什么,解出来的还是什么。

联通的示例

我在记事本上写下一个联通,然后保存再打开,结果成了这个样子:

这里写图片描述

出现了乱码,这是为什么呢?不是默认是GBK编码的吗?怎么会出现乱码呢?
一看记事本这个时候的编码方式,变成了Utf-8:

这里写图片描述

默认的GBK编码为什么会变成Utf-8编码的呢?

这个时候就要了解一下Utf-8中的编码格式了:

我们知道,Utf-8有用一个字节存储的,也有两个字节存储的,还有用三个字节存储的。
那么他是按照什么方式来判断到底要读取几个字节的呢?

Utf-8中的编码形式如下图:

这里写图片描述

解读一下就是:

当读到的字节的首位是0的时候,就只读一个字节,回去查表
当读到的字节的首位是110,第二个字节的首位是10的时候,就读取两个字节,回去查表
当读到的字节的首位是1110,第二个字节的首位是10,第三个字节的首位是10的时候,就读取三个字节,回去查表

而联通这两个字,在内存中的二进制码是这样的:

//获取联通的GBK编码的二进制表现数
public class EncodeDemo2 {
	public static void main(String[] args) throws Exception
	{
		String s = "联通";
		byte[] buf = s.getBytes("GBK");
		for(int i:buf)
		{
			System.out.println(Integer.toBinaryString(i&255));
		}
	}
}

四个字节的二进制码如下:
这里写图片描述

他是怎么样从GBK变啊变成Utf-8编码的呢?
当记事本将联通写入到文件中的时候,用GBK码表进行编码,编成如上图所示的二进制表现形式,
然后再打开记事本软件的时候,这就是一个解析编码的过程,
记事本一看,第一个字节首位是110,符合Utf-8编码的形式,继续往下读,发现是10,完全符合Utf-8编码形式,这就转变成Utf-8编码形式了。

这么多字符里边,只联通字符是这样的,其他字符并不这样,

解决方式:就是写联通的时候,在联通之前加上一个汉字,

这里写图片描述

练习:
有五个学生,每个学生有三门课程的成绩
从键盘输入以上数据,(包括姓名,三门课成绩)
输入的格式如:zhagnsanm30,40,32计算出总成绩
并把学生的信息和计算出的总分数高低顺序存放在磁盘文件“stud.txt”中

/*
有五个学生,每个学生有三门课程的成绩
从键盘输入以上数据,(包括姓名,三门课成绩)
输入的格式如:zhagnsanm30,40,32计算出总成绩
并把学生的信息和计算出的总分数高低顺序存放在磁盘文件“stud.txt”中
*/

/*
考虑到有很多学生对象,所以可以用集合来进行存学生对象
还涉及到了写入流的操作:
*/
import java.io.*;
import java.util.*;


//先来定义一个Student类:实现可以自己排序的,还要为存储在集合中做准备
class Student implements Comparable<Student>
{
	private String name;
	private int ma;
	private int cn;
	private int en;
	private int sum;
	Student(String name,int ma,int cn,int en)
	{
		this.name = name;
		this.ma = ma;
		this.cn = cn;
		this.en = en;
		sum = ma+cn+en;
	}
	public int getSum()
	{
		return sum;
	}
	public String getName()
	{
		return name;
	}
	//这是为将来写数据的时候做准备
	public String toString()
	{
		return "Student["+name+", "+ma+", "+cn+", "+en+" ]";
	}
	//说不准对象会存到HashSet中呢,所以先复写了hashCode方法和equals方法,
	//姓名和总分一样视为同一个人
	public int hashCode()
	{
		return name.hashCode()+sum*7;
	}
	public boolean equals(Object obj)
	{
		if (!(obj instanceof Student))
		{
			throw new RuntimeException("传入的类型出错");
		}
		Student s = (Student)obj;
		return this.name.equals(s.name) && this.sum==s.sum;
	}
	//实现了可比较的接口,就要复写这个方法
	public int compareTo(Student s)
	{
		int num = new Integer(this.sum).compareTo(new Integer(s.sum));
		if(num==0)
			return this.name.compareTo(s.name);
		return num;
	}
}
//接下来定义操作学生的工具类
class StudentInfoTool
{
	//如果按照自己默认排序的话这里的方法传的比较器就为空
	public Set<Student> getStudent()
	{
		return getStudent(null);
	}
	//如果想要逆转排序的话,这里该方法可以讲原始的排序方式逆转
	//该方法可以将从键盘获取到的对象存储进集合
	//返回一个Set集合。
	public Set<Student> getStudent(Comparator<Student> com)
	{
		//判断参数是否为空,空的话就创建一个不带比较器的集合,不为空的话,就创建一哥带有比较器的集合
		TreeSet<Student> ts = null;
		if(com==null)
			ts = new TreeSet<Student>();
		else
			ts= new TreeSet<Student>(com);
		BufferedReader bufr = null;
		try
		{
			bufr = new BufferedReader(new InputStreamReader(System.in));
			String line = null;
			while((line = bufr.readLine())!=null)
			{
				if("over".equals(line))
					break;
				//从键盘中获取到创建对象的各个元素
				String[] strs = line.split(",");
				Student stu = new Student(strs[0],Integer.parseInt(strs[1]),
											Integer.parseInt(strs[2]),
											Integer.parseInt(strs[3]));
				ts.add(stu);//将创建好的对象添加进集合中
			}
		}
		catch (IOException e)
		{
			throw new RuntimeException("流对象创建失败");
		}
		finally
		{
			if(bufr!=null)
			{
				try
				{
					bufr.close();
				}
				catch (IOException e)
				{
					e.printStackTrace();
				}
			}
		}
		return ts;
	}
	//该方法可以讲集合中的对象,写入到文件中
	//接收一个Set集合,
	public void Set2File(Set<Student> ts)
	{
		BufferedWriter bufw = null;
		try
		{
			bufw = new BufferedWriter(new FileWriter("stud.txt"));
			for(Student s:ts)
			{
				bufw.write(s.toString()+"\t");
				bufw.write(s.getSum()+"");
				bufw.newLine();
				bufw.flush();
			}
		}
		catch (IOException e)
		{
			throw new RuntimeException("写入流开启失败");
		}
		finally
		{
			if(bufw!=null)
			{
				try
				{
					bufw.close();		
				}
				catch (IOException e)
				{
					e.printStackTrace();
				}
			}
		}
	}
}
public class StudentInfoTest {
	public static void main(String[] args)
	{
		Comparator<Student> com = Collections.reverseOrder();
		StudentInfoTool sit = new StudentInfoTool();
		Set<Student> set = sit.getStudent(com);
		sit.Set2File(set);
	}
}

运行结果为:

这里写图片描述
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值