欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java IO 核心操作(五)

程序员文章站 2022-07-15 13:00:21
...

Java常见编码简介

在计算机的世界里,任何的文字都是以指定的编码方式存在的,在Java程序的开发中最常见的是ISO8859-1,GBK/GB2312,unicode,UTF编码。

ISO8859-1:属于单字节编码,最多只能表示表示0~255的字符范围,主要在英文上的应用。

GBK/GB2312:中文的国际编码,专门用来表示文字,是双字节编码,如果在此编码中出现中文,则使用ISO8859-1编码,GBK可以表示简体中文和繁体中文,而GB2312只能表示简体的中文,GBK兼容GB2312.

unicode:Java中使用此编码方式,是最标准得的一种编码,使用十六进制表示编码,但此编码不兼容ISO8859-1编码。

UTF:由于unicode不支持ISO8859-1编码,而且容易占用更多的空间 ,而且对于英文字母也需要使用使用两个字节编码,这样使用unicode不便于传输和存储,因此产生了UTF编码。UTF编码兼容了ISO8859-1编码,同时也可以用来表示所有的语言字符,不过UTF编码是不定长编码,每一个字符的长度为1~6个字符不等。一般在中文网页中使用此编码,可以节省空间。

在程序中如果处理不好字符的编码,则就有可能出现乱码问题。如果现在本机的默认编码是GBK,但在程序中使用了ISO8859-1编码则会出现字符的乱码问题,就像两个人交谈,一个人说中文,另外一个人说的是其他语言,如果语言不同,则肯定无法沟通,如图所示:
Java IO 核心操作(五)

所以如果要避免产生乱码,则程序的编码与本地的默认编码保持一致即可,而如果要知道本地的默认编码,则可以使用System类完成。

得到本机的编码显示

使用System类可以直接取得与系统有关的信息,所以直接使用此类即可找到系统的默认编码,使用如下方法:

public static Properites getProperty()

使用上述方法得到JVM的默认编码

public class CharSetDemo01{
    public static void main(String args[]){
        System.out.println("系统默认编码:" +
            System.getProperty("file.encoding")) ;  // 获取当前系统编码
    }
};

程序运行结果:

系统默认编码GBK

可以发现现在操作系统的默认编码是GBK,所以,如果此时使用了ISO8859-1编码,则肯定出现乱码。

乱码产生

下面通过一个范例简介乱码的产生,现在本地的默认编码是GBK,下面通过对文字进行编码转换,如果要实现编码的转换可以使用String类中的getBytes(String charset)方法,此方法可以设置指定的编码,该方法的格式如下:

使用ISO8859-1编码

import java.io.OutputStream ;
import java.io.FileOutputStream ;
import java.io.File ;
public class CharSetDemo02{
    public static void main(String args[]) throws Exception {
        File f = new File("D:" + File.separator + "test.txt") ; // 实例化File类
        OutputStream out = new FileOutputStream(f) ;    // 实例化输出流
        byte b[] = "中国,你好!".getBytes("ISO8859-1") ; // 转码操作
        out.write(b) ;  // 保存
        out.close() ;   // 关闭
    }
};

程序运行结果:

???

可以发现,因为编码不一致,所以在保存时出现了乱码。在Java的开发中,乱码是一个比较常见的问题,乱码的产生就只有一个原因,即输出内容的编码与接收内容的编码不一致。

对象序列化

基本概念与Serializable接口

对象序列化就是把一个对象变为二进制的数据流的一种方式,如图所示,通过对象序列化可以方便地实现对象的传输或存储。
Java IO 核心操作(五)

如果一个类想被序列化,则对象的类必须实现java.io.Serializable接口。此接口的定义如下:

public interface Serializable{}

可以发现在接口中没有定义任何的方法,所以此接口是一个标识接口。表示一个具备被序列化的能力。

定义一个可被序列化的类

import java.io.Serializable ;
public class Person implements Serializable{
    private String name ;   // 声明name属性,但是此属性不被序列化
    private int age ;       // 声明age属性
    public Person(String name,int age){ // 通过构造设置内容
        this.name = name ;
        this.age = age ;
    }
    public String toString(){   // 覆写toString()方法
        return "姓名:" + this.name + ";年龄:" + this.age ;
    }
};

以上的Person类已经完成了序列化接口,所以此类的对象可以通过二进制数据流进行传输的。而如果要完成对象的输入或输出,还必须依靠对象输出流(ObjectOutputStream)和对象输入流(ObjectInputStream)。

使用对象输出流输出序列化对象的步骤优势也称为序列化,而使用对象输入流读入对象的过程有时也被称为反序列化,如图所示:
Java IO 核心操作(五)

提示

对象序列化和对象反序列化操作时的版本兼容问题

在对象进行序列化或反序列化的操作时,要考虑序列化JDK的版本和反序列化的JDK版本不统一则就有可能造成异常,所以在在序列化操作中引入了一个serialVersionUID的常量,可以通过此常量来验证版本的一致性。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。

当实现java.io.Serializable接口的实体类没有显示的定义一个名为SerialVersionUID,类型为long的变量时,Java序列化机制在编译时会自动生成一个此版本的serialVersionUID。当然,如果不希望通过编译来自动生成,也可以直接显示地定义一个名为serialVersionUID,类型为long的变量,只要不修改这个变量值的序列化实体,都可以进行串行化和反串行化。
本程序直接在Person中加入以下的常量即可:
private static final long serialVersionUID=1L;
SerialVersionUID的具体内容由用户指定。

对象输出流ObjectOutputStream

一个对象如果要进行输出,则必须使用ObjectOutputStream类,此类的定义如下:

public class ObjectOutputStream extends OutputStream
implements ObjectOutput,ObjectStreamConstant

ObjectOutputStream类属于OutputStream类的子类,此类的常用方法如表所示:
Java IO 核心操作(五)

此类的使用形式与PrintStream非常相似,在实例化时也需要传入一个OutputStream的子类对象,然后根据传入的OutputStream子类的对象不同,输出的位置也不同。

将Person类对象保存在文件中

import java.io.File ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
public class SerDemo01{
    public static void main(String args[]) throws Exception {
        File f = new File("D:" + File.separator + "test.txt") ; // 定义保存路径
        ObjectOutputStream oos = null ; // 声明对象输出流
        OutputStream out = new FileOutputStream(f) ;    // 文件输出流
        oos = new ObjectOutputStream(out) ;
        oos.writeObject(new Person("张三",30)) ;  // 保存对象
        oos.close() ;   // 关闭
    }
};

提出问题

到底序列化了哪些内容?

答:只有属性被序列化。因为每个对象都具有相同的方法,但是每个对象的属性不一定相同,也就是说,对象只保存了属性信息,那么在序列化操作时也是那么个道理。

对象输入流ObjectInputStream

使用ObjectInputStream可以直接把序列化好的对象反序列化。ObjectInputStream的定义如下

public class ObjectInputStream extends InputStream implements ObjectInput,ObjectStreamConstants

ObjectInputStream类也是InputStream的子类,与PrintStream类的使用类似。此类型同样需要接收InputStream类的实例才可以实例化。主要操作方法如表所示:
Java IO 核心操作(五)

下面使用对象输入流将上面保存在文件中的对象读取出来,此过程也称为反序列化。

从文件中将Person对象反序列化(读取)

import java.io.File ;
import java.io.FileInputStream ;
import java.io.InputStream ;
import java.io.ObjectInputStream ;
public class SerDemo02{
    public static void main(String args[]) throws Exception {
        File f = new File("D:" + File.separator + "test.txt") ; // 定义保存路径
        ObjectInputStream ois = null ;  // 声明对象输入流
        InputStream input = new FileInputStream(f) ;    // 文件输入流
        ois = new ObjectInputStream(input) ;    // 实例化对象输入流
        Object obj = ois.readObject() ; // 读取对象
        ois.close() ;   // 关闭
        System.out.println(obj) ;
    }
};

程序运行结果:

姓名:张三; 年龄: 30

Externalizable接口

被Serializable接口声明的类的对象的内容都将被序列化,如果现在用户希望自己指定序列化的内容,则可以让一个类实现Externalizable接口,此接口定义如下:

public interface Externalizable extends Serializable{
public void writeExternal(ObjectOutput out)throws IOException;
public void readExternal(ObjectInput in)throws IOException,ClassNotFountException;
}

Externalizable接口是Serializable接口的子接口,此接口中定义了两个方法,这两个方法的作用如下:

writeExternal(ObjectOutput out):在此接口中指定要保存的属性信息,对象序列化时调用。

readExternal(ObjectInput in):在此方法中读取被保存的信息,对象反序列化时调用。这两个方法的参数类型是ObjectOutput和ObjectInput,两个接口的定义如下:

ObjectOutput接口定义:
public interface ObjectOutput extends DataOutput

ObjectInput接口定义:
public interface ObjectInput extends DataInput

可以发现两个接口分别继承DataOutput和DataInput,这样在这两个方法中就可以像DataOutputStream和DataInputStream那样直接输出和读取各种类型的数据。

注意:

如果一个类要使用Externalizable实现序列化时,在此类中必须存在一个无参构造方法,因为在反序列化时会默认调用无参构造实例化对象,如果没有此无参构造,则运行时将会出现异常,这一点的实现机制与Serializable接口是不同的。

下面我们就具体操作一下

  • 序列化和反序列化Person对象
import java.io.File ;
import java.io.IOException ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
import java.io.FileInputStream ;
import java.io.InputStream ;
import java.io.ObjectInputStream ;
public class SerDemo03{
    public static void main(String args[]) throws Exception{
        //ser() ;
        dser() ;
    }
    public static void ser() throws Exception {
        File f = new File("D:" + File.separator + "test.txt") ; // 定义保存路径
        ObjectOutputStream oos = null ; // 声明对象输出流
        OutputStream out = new FileOutputStream(f) ;    // 文件输出流
        oos = new ObjectOutputStream(out) ;
        oos.writeObject(new Person("张三",30)) ;  // 保存对象
        oos.close() ;   // 关闭
    }
    public static void dser() throws Exception {
        File f = new File("D:" + File.separator + "test.txt") ; // 定义保存路径
        ObjectInputStream ois = null ;  // 声明对象输入流
        InputStream input = new FileInputStream(f) ;    // 文件输入流
        ois = new ObjectInputStream(input) ;    // 实例化对象输入流
        Object obj = ois.readObject() ; // 读取对象
        ois.close() ;   // 关闭
        System.out.println(obj) ;
    }
};

从以上代码可以发现,使用Externalizable接口实现序列化明显要比使用Serializable接口实现序列化麻烦的多,除此之外,两者的实现还有不同,如表所示:
Java IO 核心操作(五)
一般在开发中,Serializable接口比较常用。

transient关键字

Serializable接口实现的操作实际上是将一个对象中的全部属性进行序列化,当然也可以使用Externalizable接口以实现部分属性的序列化,但这样操作比较麻烦。
当使用Serializable接口实现序列化操作时,如果一个对象中的某个属性不希望被序列化,则可以使用transient关键字进行声明,代码如下所示:

Person类中的name属性不希望被序列化

import java.io.Serializable ;
public class Person implements Serializable{
    private transient String name ; // 声明name属性,但是此属性不被序列化
    private int age ;       // 声明age属性
    public Person(String name,int age){ // 通过构造设置内容
        this.name = name ;
        this.age = age ;
    }
    public String toString(){   // 覆写toString()方法
        return "姓名:" + this.name + ";年龄:" + this.age ;
    }
};

重新保存再读取对象

import java.io.File ;
import java.io.IOException ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
import java.io.FileInputStream ;
import java.io.InputStream ;
import java.io.ObjectInputStream ;
public class SerDemo04{
    public static void main(String args[]) throws Exception{
        ser() ;
        dser() ;
    }
    public static void ser() throws Exception {
        File f = new File("D:" + File.separator + "test.txt") ; // 定义保存路径
        ObjectOutputStream oos = null ; // 声明对象输出流
        OutputStream out = new FileOutputStream(f) ;    // 文件输出流
        oos = new ObjectOutputStream(out) ;
        oos.writeObject(new Person("张三",30)) ;  // 保存对象
        oos.close() ;   // 关闭
    }
    public static void dser() throws Exception {
        File f = new File("D:" + File.separator + "test.txt") ; // 定义保存路径
        ObjectInputStream ois = null ;  // 声明对象输入流
        InputStream input = new FileInputStream(f) ;    // 文件输入流
        ois = new ObjectInputStream(input) ;    // 实例化对象输入流
        Object obj = ois.readObject() ; // 读取对象
        ois.close() ;   // 关闭
        System.out.println(obj) ;
    }
};

程序运行结果:

姓名:null 年龄:30

以上程序中的姓名为null,表示内容没有被序列化保存下来。这样在对象反序列化后,输出时姓名就是默认值null。