一、操作对象(对象序列化)
1.概述
将内存中的对象保存到文件并存储到硬盘中,使其持久化。以后如果用到这些对象时直接从硬盘中读取,不用再new。保存的文件后缀名是.object。
这是额外功能,其底层还是File。
2.使用到的两个类
1)ObjectInputStream:反序列化,读取ObjectOutputStream写的文件。
其特有的方法是Object readObject(),该方法实现读取对象。
注意:读取时必须有存储的文件和所存储对象对应的类文件。否则抛出ClassNotFoundException。
例如:Person p = (Person)ois.readObject();
2)ObjectOutputStream:对象的序列化。将对象写入文件。
其特有的方法是void writeObject(Objcet obj),将指定对象写入文件。
注意:被操作的对象必须实现Serializable标记接口。
3.Serializable接口
1)Serializable接口用于标记被序列化的类,判断类和对象是否是同一版本。类实现了Serializable后。类此时的版
本就有了serialVersionUID号。创建对象时,对象也跟着有了serialVersionUID号。serialVersionUID号的定
义是根据类中成员的修饰符来算的。若修改了类,会产生新的serialVersionUID号。再读取时会报错。
注意:为了保证修改类内容或者不同版本的编译器给类标记的ID不一样而引起报错,可以在类中定义一个默认
serialVersionUID号。
显式定义serialVersionUID的好处:
①如果你在对类对象进行了序列化之后,又修改了这个类,那么再次读取修改前序列化的对象时,编译器
可以识别;
②如果没有显式定义,你修改后的类经过编译器编译后会生成一个新的serialVersionUID,这个
serialVersionUID跟修改前类的serialVersionUID不同,当你再次读取时,编译器会报出
InvalidClassException异常。
2) transient和static
transient:短暂的,暂时的,修饰成员变量时,写入对象时,该变量数据将不被写入。
非静态数据不想被序列化可以使用这个关键字修饰。
static:非静态的,修饰成员变量时,写入对象时,该变量数据将不被写入。
因为writeObject()方法只能操作不是非瞬态和非静态的数据。
<span style="font-size:18px">//操作对象,对象序列化。
import io.p2.bean.Person;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class ObjectStreamDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//对象序列化。写入文件
// writeObj();
//对象反序列化。读取文件
readObj();
}
public static void readObj() throws IOException, ClassNotFoundException {
//反序列化。读取ObjectOutputStream写的文件。
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.object"));
//读取时必须有存储的文件和所存储对象对应的类文件。否则抛出ClassNotFoundException
Person p = (Person)ois.readObject();
System.out.println(p.getName()+":"+p.getAge());
ois.close();
}
public static void writeObj() throws IOException {
//存储对象到硬盘中,使其持久化。以后使用到时直接读取,不用再new。后缀名是.object。容易辨认出是存储对象。
//额外功能,基础还是File
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.object"));
oos.writeObject(new Person("小强",30));
oos.close();
}
}
</span>
二、RandomAccessFile类
1.概述
该类不是IO包中四大类的子类,而是直接继承Object类。但是它也是IO包的成员,因为它具有读写功能。完成读
写的原理是内部封装了字节输入流和输出流。
RandomAccess内部封装了一个byte数组,而且通过指针对数组的元素进行操作,可以通过getFilePointer获取指
针位置,同时可以通过seek改变指针的位置。
通过构造函数可以看出,该类只能操作文件。
2.构造函数
RandomAccessFile(File file, String mode):
创建从中读取和向其中写入(可选)的随机访问文件流,该文件由 File 参数指定。
RandomAccessFile(String name, String mode):
创建从中读取和向其中写入(可选)的随机访问文件流,该文件具有指定名称。
mode 参数值及其含意:
|--->"r" :以只读方式打开。调用结果对象的任何 write 方法或者如果该文件不存在将导致抛出
IOException。
|--->"rw" :打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。 如果文件存在不会覆盖。
|--->"rws" :打开以便读取和写入,对于 "rw",还要求对文件的内容或元数据的每个更新都同步写入到底层存
储设备。
|--->"rwd" :打开以便读取和写入,对于 "rw",还要求对文件内容的每个更新都同步写入到底层存储设备。
3.常见方法
void writeInt(int v):按四个字节将 int 写入该文件,先写高字节。
voidseek(long pos):设置到此文件开头测量到的文件指针偏移量,在该位置发生下一个读取或写入操作。
int skipBytes(int n):尝试跳过输入的 n 个字节以丢弃跳过的字节。
4.随机写入细节
若文件里有内容,如果没有设置指针位置,在写入时,将从0角标开始写入,覆盖之前的数据。可以seek方法设置指针,避免文件内容丢失。
代码示例
<span style="font-size:18px">import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileDemo {
public static void main(String[] args) throws IOException {
// writeFile();
// readFile();
randomWrite();
}
public static void randomWrite() throws IOException{
RandomAccessFile raf = new RandomAccessFile("rannacc.txt","rw");
raf.seek(3*8);
raf.write("哈哈".getBytes());
raf.writeInt(108);
raf.close();
}
public static void readFile() throws IOException {
RandomAccessFile raf = new RandomAccessFile("rannacc.txt", "r");
//通过seek设置指针位置,体现随机访问。只要指定指针的位置即可。
raf.seek(1*8);
byte[] buf = new byte[4];
raf.read(buf);
String name = new String(buf);
System.out.println("name:"+name);
int age = raf.readInt();//获取当前指针位置往后的4个字节整数
System.out.println("age="+age);
//getFilePointer获取指针位置。
System.out.println("pos:"+raf.getFilePointer());
raf.close();
}
//使用RandomAccessFile对象写入一些人员信息。
public static void writeFile() throws IOException{
/*
* 若文件不存在,则创建,文件存在,则不创建。
*/
RandomAccessFile raf = new RandomAccessFile("rannacc.txt","rw");
raf.write("张三".getBytes());
// raf.write(609);//只写最低字节,即低8位、导致数据丢失。
raf.writeInt(97);//写入四个字节,即整数。
raf.write("小强".getBytes());
raf.writeInt(99);
raf.close();
}
</span>
三、管道流(piped)
1.概述
PipedInputStream输入流提供数据到Pipe的OutputStream输出流。
但是因为输入流中的read()方法,没有数据读取时,会一直阻塞,造成死锁,所以要将输入流和输出流分开,即多线程。所以管道流是IO技术和多线程技术的结合。
通常,数据由某个线程从 PipedInputStream 对象读取,并由其他线程将其写入到相应的 PipedOutputStream。不建议对这两个对象尝试使用单个线程,因为这样可能死锁线程。管道输入流包含一个缓冲区,可在缓冲区限定的范围内将读操作和写操作分离开。如果向连接管道输出流提供数据字节的线程不再存在,则认为该管道已损坏。
2.一般操作步骤
1)分别创建读和写的类,并都实现Runable接口。分别重写run方法,需要在内部处理异常。
2)创建两个管道流,并用connect方法将其连接起来。
3)创建两个线程对象,将读写对象分别传入不同线程,调用线程的start方法启动线程。
代码示例
<span style="font-size:18px">import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
public class PipedStream {
public static void main(String[] args) throws IOException {
PipedInputStream input = new PipedInputStream();
PipedOutputStream output = new PipedOutputStream();
//输入流连接输出流
/*
* 两种方法:
* 1,输入流一初始化就明确输出流,将输出流作为参数传入构造函数。
* 2、connect。
*/
input.connect(output);
new Thread(new Input(input)).start();
new Thread(new Output(output)).start();
}
}
//读线程
class Input implements Runnable{
private PipedInputStream in;
Input(PipedInputStream in){
this.in = in;
}
public void run() {
try {
byte[] buf = new byte[1024];
int len = in.read(buf);
String s = new String(buf,0,len);
System.out.println(s);
} catch (IOException e) {
e.printStackTrace();
}
}
}
//写线程
class Output implements Runnable{
private PipedOutputStream out;
Output(PipedOutputStream out){
this.out = out;
}
public void run() {
try {
out.write("嘿!管道来啦!".getBytes());
} catch (Exception e) {
// TODO: handle exception
}
}
}
</span>
四、操作基本数据类型的流对象
1.概述
操作基本数据类型的流对象是DataInputStream和DataOutputStream。这两个读写对象,可用于操作基本数据类型的流对象,包含读写各种数据类型的方法。
2.方法
读
byte型 byte readbyte()
int型 intreadInt()
boolean型 boolean readBoolean()
double型 doublereadDouble()
String readUTF();//对应writeUTF,读取以UTF-8修改版编码写入的字符串
写
byte型 writeByte(int b);//将b的低八位写入
int型 writeInt(int n)
boolean型 writeBoolean(boolean b)
double型 writeDouble(double d)
writeUTF(String str);//以与机器无关方式使用UTF-8修改版编码将一个字符串写入基础输出流。
代码示例
<span style="font-size:18px">import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
//操作基本数据类型的流对象
public class DataStreamDemo {
public static void main(String[] args) throws IOException {
// writeData();
readData();
}
public static void readData() throws IOException {
DataInputStream dis = new DataInputStream(new FileInputStream("data.txt"));
String s = dis.readUTF();
dis.close();
System.out.println(s);
}
public static void writeData() throws IOException {
DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"));
dos.writeUTF("你好啊");//utf是utf-8编码表的修改版。写完之后,用转换流指定不了修改版的编码表,只
//相应的读取流DataInputStream读取。
dos.close();
}
</span>
<span style="font-size:18px">}</span>
五、操作字节数组
1.ByteArrayInputStream
包含一个内部缓冲区,该缓冲区包含从流中读取的字节。一创建流对象,必须要明确数据源。该源是字节数组。
2. ByteArrayOutputStream
此类实现一个输出流。其中的数据被写入一个byte数组,而且该数组会自动增长。可使用toByteArray() 和 toString()获取数据。
3.关闭ByteArrayInputStream和ByteArrayOutputStream流无效
原因:因为操作都是在内存中,即源和目的都是在内存中,没有调用底层资源,所以就没有资源释放。关闭后仍可以调用,不会产生IO异常。所以一般不用关闭该流。
4.应用
这两个对象是在用流的思想来操作数组,当我们需要把一个文件中的数据加入内存中的数组时,就可以考虑用这个两个对象。此外,它还有writeTo(OutputStream os)可以把ByteArrayOutputStream对象内部定义的缓冲区内容,一次性写入os中。操作字符数组、字符串的流对象类型与之相似,可以参与它们的使用方法。
代码示例
<span style="font-size:18px">import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
public class ByteArrayStreamDemo {
public static void main(String[] args) {
ByteArrayInputStream bis = new ByteArrayInputStream("fafera".getBytes());
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int ch = 0;
while((ch=bis.read())!=-1){
bos.write(ch);
}
//打印看看
System.out.println(bos.toString());
}
}</span>
六、字符编码
1.概述
字符流的出现为了方便操作字符。更重要的是加入了编码的转换,即转换流。通过子类转换流来完成。在两个对象进行构造的时候,可以加入字符集(即编码表)。
2.可指定编码表的有:
1)转换流:InuputStreamReader和OutputStreamWriter
2)打印流:PrintStream和PrintWriter,只有输出流。
3.编码表的由来
计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。这就是编码表。
4.常见的编码表
1)ASCII:美国标准信息交换码表。用一个字节的7位表示
2)IOS8859-1:拉丁码表;欧洲码表。用一个字节的8位表示
3)GB2312:中国的中文编码表()早期
4)GBK:中国的中文编码表升级,融合了更多的中文文字字符。打头的是两个高位为1的两个字节编码。为负数
5)Unicode:国际标准码,融合了多种文字。所有文字都用两个字节来表示,Java语言使用的就是unicode。
6)UTF-8:最多用三个字节表示一个字符的编码表,根据字符所占内存空间不同,分别用一个、两个、三个字节来编码。
UTF-8编码格式:
一个字节:0开头
两个字节:字节一 ---> 110 位数:10 ~ 6
字节二 ---> 10 位数:5 ~ 0
三个字节:字节一 ---> 110 位数:15 ~ 12
字节二 ---> 10 位数:11 ~ 6
字节三 ---> 10 位数:5 ~ 0
5.转换流的编码应用
可以将字符以指定编码格式存储,也可以指定编码格式读取数据。指定编码表的动作有构造函数来完成。
6.编码和解码
1)编码:字符串变成字节数组
①默认字符集:
String ---> byte[] :str.getBytes()
②指定字符集:
String ---> byte[] :str.getBytes(charsetName)
2)解码:字节数组变成字符串
①默认字符集:
byte[] ---> String:new String(byte[])
②指定字符集:
byte[] ---> String:newString(byte[],charsetName)
7.编码解码的注意事项
1)如果编码成功,解码出来的是乱码,,则需对乱码通过再次编码(用解错码的编码表),然后再通过正确的编码表解码。针对于
IOS8859-1是通用的。
2)如果用的是GBK编码,UTF-8解码,此时通过再次编码后解码的方式,就不能成功了,因为UTF-8也支持中文,在UTF-8解的时候,会将对应的字节数改变,所以不会成功。
3)对于中文的”联通“,这两个字比较特别,它的二进制位正好是和在UTF-8中两个字节打头的相同,所以在文本文件中,如果单独写“联通”或者和满足UTF-8编码格式的字符一起保存时,记事本就会用UTF-8来进行解码动作,这样显示的就会是乱码。
代码示例
<span style="font-size:18px">import java.io.IOException;
import java.io.UnsupportedEncodingException;
public class EncodeDemo {
public static void main(String[] args) throws IOException {
/*//编码解码
*
*文字-->二进制:编码
*二进制 -->文字解码
*
*在内存中体现:
* 字符串-->字节数组:编码 把看的懂的转成看不懂的。
* 字节数组-->字符串:解码 把看不懂得专程看得懂的。
*
*你好:GBK: -60 -29 -70 -61
* 一个中文对应两个字节,每个字节高位都是1,所以是负数。
*
*你好:UTF-8:-28 -67 -96 -27 -91 -67
*
*如果编码错误,解码不出来。
*如果编码正确,解错了,有可能有救(如发现解错,可以再解一次)。
* iso8859-1能再一次解码,因为它不是中文,而且是单字节编码。
* UTF-8编错后不可救。码很多,可以识别单字节,2字节,3字节等等。有些字节码没有对应的数据,用
*未知数来表示,未知数又对应自己的一个编码表,这样源字节码就被改变了。再编码一次结果也不对。
*以下为演示。
*
*/
jieMaAgain();
// printBytes(buf);
// jieMa(buf);
}
/**
* 如果编码正确,解错了,有可能有救(如发现解错,可以再解一次)。
* @throws UnsupportedEncodingException
*/
public static void jieMaAgain() throws UnsupportedEncodingException {
//编码
String str = "你好";
byte[] buf = str.getBytes("GBK");//可以指定编码表。
//解码
String s1 = new String(buf,"iso8859-1");
System.out.println("s1="+s1);//解码错误。编码表不对。
//解错之后,再编一次,因为得出来的字符串在原来的编码表中有对应的字节,可以获取源字节。
byte[] buf2 = s1.getBytes("iso8859-1");
String s2 = new String(buf,"gbk");//换编码表
System.out.println("s2="+s2);
}
/**
* @param buf
* @throws UnsupportedEncodingException
*/
public static void jieMa(byte[] buf) throws UnsupportedEncodingException {
//将字节数组变成字符串,解码
String s = new String(buf,"utf-8");
System.out.print("s="+s);
}
/**
* @param buf
*/
//将字符串变成字节数组,编码
public static void printBytes(byte[] buf) {
for(byte b : buf){
System.out.print(b+" ");
}
}
}
</span>
练习:
<span style="font-size:18px">import java.io.UnsupportedEncodingException;
/*
* 在Java中,字符串"abcd"与"ab你好"的长度是一样的,都是四个字符,
* 但是对应的字节数不同,一个汉字占两个字节。
*
* 定义一个方法,按照最大的字节数来取子串。
* 如:对于"ab你好",如果取三个字节,那么子串就是ab与"你"字的半个,那么半个就要舍弃。
* 如果取四个字节,就是。取五个字节还是"ab你".
*
*/
public class Test {
public static void main(String[] args) throws UnsupportedEncodingException {
String str = "ab你好cd谢谢";
//GBK:汉字中字节一个正,一个是负数的特例:"琲"。utf-8中,三个字节都是负数。
// String str = "ab琲琲cd琲琲";;
/*
* GBK编码
int len = str.getBytes("gbk").length;
for (int i = 0; i < len; i++) {
System.out.println("截取"+(i+1)+"个字节数的结果是:"+CutByBytes(str,(i+1)));
}
*/
//utf编码
int len = str.getBytes("utf-8").length;
for (int i = 0; i < len; i++) {
System.out.println("截取"+(i+1)+"个字节数的结果是:"+CutByU8Bytes(str,(i+1)));
}
}
public static String CutByU8Bytes(String str, int len) throws UnsupportedEncodingException {
//将字符串转换为字节数
byte[] buf = str.getBytes("utf-8");//一个汉字对应三个负数
int count = 0;
for (int i = len-1; i>=0; i--) {
//从最大字节数末端开始取数。
if(buf[i]<0){
count++;//记录小于0的字节数,方便读取汉字。
}
else
break;
}
//判断负数个数
if(count%3==0){//如果为偶数
return new String(buf,0,len,"utf-8");
}
//如果是奇数,舍弃最后一位负数,因为这个数是下一个汉字半个。不完整。
else if(count%3==1)//多一个
return new String(buf,0,len-1,"utf-8");
else//多两个,舍弃
return new String(buf,0,len-2,"utf-8");
}
public static String CutByBytes(String str, int len) throws UnsupportedEncodingException {
//将字符串转换为字节数
byte[] buf = str.getBytes("gbk");
int count = 0;
for (int i = len-1; i>=0; i--) {
//从最大字节数末端开始取数。
if(buf[i]<0){
count++;//记录小于0的字节数,方便读取汉字。
}
else
break;
}
//判断负数个数
if(count%2==0){//如果为偶数
return new String(buf,0,len,"gbk");
}
else//如果是奇数,舍弃最后一位负数,因为这个数是下一个汉字半个。不完整。
return new String(buf,0,len-1,"gbk");
}
</span>}