Java程序员面试笔试宝典(第2版)
上QQ阅读APP看书,第一时间看更新

2.1 输入输出流

从外部设备流向中央处理器的数据流被称为“输入流”,反之被称为“输出流”。由此可见,只要涉及文件的读写或者网络数据的收发,都会涉及输入、输出流。

2.1.1 Java IO流的实现机制

在Java语言中,输入和输出都被称为抽象的流,流可以被看作一组有序的字节集合,即数据在两个设备之间的传输。

流的本质是数据传输,根据处理数据类型的不同,流可以分为两大类:字节流和字符流。其中字节流以字节(8bit)为单位,读到一个字节就返回一个字节,包含两个抽象类:InputStream(输入流)和OutputStream(输出流)。而字符流使用了字节流读到一个或多个字节(中文对应的字节数是两个,在UTF-8码表中是3个字节)时,先去查指定的编码表,将查到的字符返回,它包含两个抽象类:Reader(输入流)和Writer(输出流)。其中字节流和字符流最主要的区别为:字节流在处理输入输出的时候不会用到缓存,而字符流用到了缓存。每个抽象类都有很多具体的实现类,在这里就不详细介绍了。图2-1主要介绍Java中IO的设计理念。Java IO类在设计的时候采用了Decorator(装饰者)设计模式,以InputStream为例,介绍Decorator设计模式在IO类中的使用如下。

图2-1 IO设计类图

其中ByteArrayInputStream、StringBufferInputStream、FileInputStream和PipedInputStream是Java提供的最基本的对流进行处理的类,FilterInputStream为一个包装类的基类,可以对基本的IO类进行包装,通过调用这些类提供的基本的流操作方法来实现更复杂的流操作。

使用这种设计模式的好处是,可以在运行时动态地给对象添加一些额外的职责,与使用继承的设计方法相比,该方法具有很好的灵活性。

假如现在要设计一个输入流的类,该类的作用为在读文件的时候把文件中的大写字母转换成小写字母,把小写字母转换为大写字母。在设计的时候,可以通过继承抽象装饰者类(FilterInputStream)来实现一个装饰类,通过调用InputStream类或其子类提供的一些方法再加上逻辑判断代码从而可以很简单地实现这个功能,示例代码如下:

当文件test.txt中的内容为aaaBBBcccDDD123时,程序输出为:

Java10中给InputStream新增加了一个方法transferTo:用来把数据从InputStream中直接传输到OutputStream,示例代码如下:

2.1.2 管理文件和目录的类

对文件或目录进行管理与操作在编程中有着非常重要的作用,Java提供了一个非常重要的类(File)来管理文件和文件夹,通过File类不仅能够查看文件或目录的属性,而且还可以实现对文件或目录的创建、删除与重命名等操作。下面主要介绍File类中常用的几个方法。见表2-1。

表2-1 File类常用的方法

常见笔试题:

如何列出某个目录下的所有目录和文件?

假设目录“C:\\testDir1”下有两个文件夹(dir1和dir2)和一个文件file1.txt。实现代码如下:

程序运行结果为:

2.1.3 Java Socket

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个双向链路的一端称为一个Socket。Socket也称为套接字,可以用来实现不同虚拟机或不同计算机之间的通信。在Java语言中,Socket可以分为两种类型:面向连接的Socket(TCP,Transmission Control Protocol,传输控制协议)通信协议和面向无连接的Socket(UDP,User Datagram Protocol,用户数据报协议)通信协议。任何一个Socket都是由IP地址和端口号唯一确定的。如图2-2所示。

图2-2 Socket原理图

基于TCP协议的通信过程如下:首先,Server端Listen(监听)指定的某个端口(建议使用大于1024的端口)是否有连接请求,然后Client端向Server端发出Connect(连接)请求,紧接着Server端向Client端发回Accept(接收)消息。一个连接就建立起来了,会话随即产生。Server端和Client端都可以通过Send、Write等方法与对方通信。

Socket的生命周期可以分为三个阶段:打开Socket、使用Socket收发数据和关闭Socket。在Java语言中,可以使用ServerSocket作为服务端,Socket作为客户端来实现网络通信。

2.1.4 Java序列化

Java提供了两种对象持久化的方式,分别为序列化和外部序列化。

(1)序列化(Serialization)

在分布式环境下,当进行远程通信时,无论是何种类型的数据,都会以二进制序列的形式在网络上传送。序列化是一种将对象以一连串字节描述的过程,用于解决在对对象流进行读写操作时所引发的问题。序列化可以将对象的状态写在流里进行网络传输,或者保存到文件、数据库等系统里,并在需要的时候把该流读取出来重新构造一个相同的对象。

如何实现序列化呢?其实,所有要实现序列化的类都必须实现Serializable接口,Serializable接口位于java.lang包中,它里面没有包含任何方法。实现序列化的方法为:使用一个输出流(例如FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,紧接着,使用该对象的writeObject(Object obj)方法就可以将obj对象写出(即保存其状态),要恢复的时候可以使用其对应的输入流。

序列化有如下几个特点:

1)如果一个类能被序列化,那么它的子类也能够被序列化。

2)由于static(静态)代表类的成员,transient(Java语言关键字,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持)代表对象的临时数据,因此被声明为这两种类型的数据成员是不能够被序列化的。

3)Java提供了多个对象序列化的接口:ObjectOutput、ObjectInput、ObjectOutputStream、ObjectInputStream。

下面给出一个序列化的具体实例:

上面程序的运行结果为:

由于序列化的使用会影响系统的性能,因此如果不是必须要使用序列化,尽可能不要使用序列化。那么在什么情况下会使用该序列化呢?

1)需要通过网络来发送对象,或对象的状态需要被持久化到数据库或文件中。

2)序列化能实现深拷贝,即可以拷贝引用的对象。

与序列化相对的是反序列化,它将流转换为对象。在序列化与反序列化的过程中,serialVersionUID起着非常重要的作用,每个类都有一个特定的serialVersionUID,在反序列化的过程中通过serialVersionUID来判定类的兼容性。如果待序列化的对象与目标对象的serialVersionUID不同,那么在反序列化的时候就会抛出InvalidClassException异常。作为一个好的编程习惯,最好在被序列化的类中显式地声明serialVersionUID(该字段必须定义为static final)。自定义serialVersionUID主要有如下三个优点。

1)提高程序的运行效率。如果在类中未显式声明serialVersionUID,那么在序列化的时候会通过计算得到一个serialVersionUID值。通过显式声明serialVersionUID的方式省去了计算的过程,因此提高了程序的运行效率。

2)提高程序在不同平台上的兼容性。由于各个平台的编译器在计算serialVersionUID的时候完全有可能会采用不同的计算方式,这就会导致在一个平台上序列化的对象在另外一个平台上将无法实现反序列化的操作。通过显式声明serialVersionUID的方法完全可以避免该问题的发生。

3)增强程序各个版本的可兼容性。在默认情况下,每个类都有唯一的serialVersionUID,因此当后期对类进行修改的时候(例如加入新的属性),类的serialVersionUID值将会发生变化,这将会导致类在修改前对象序列化的文件在修改后无法进行反序列化操作。同样通过显式声明serialVersionUID也会解决这个问题。

(2)外部序列化

此外,Java语言还提供了另外一种方式来实现对象持久化,即外部序列化。其接口如下:

外部序列化与序列化主要的区别在于序列化是内置的API,只需要实现Serializable接口,开发人员不需要编写任何代码就可以实现对象的序列化,而使用外部序列化时,Externalizable接口中的读写方法必须由开发人员来实现。因此与实现Serializable接口的方法相比,使用Externalizable编写程序的难度更大,但是由于把控制权交给了开发人员,在编程的时候有更多的灵活性,对需要持久化的那些属性进行控制,可能会提高性能。

引申:在用接口Serializable实现序列化的时候,这个类中的所有属性都会被序列化。怎样才能实现只序列化部分属性呢?

一种方法为实现Externalizable接口,开发人员可以根据实际需求来实现readExternal与writeExternal方法,从而控制序列化与反序列化所使用的属性,这种方法的缺点为增加了编程的难度。

另外一种方法为使用关键字transient来控制序列化的属性。被transient修饰的属性是临时的,不会被序列化。因此,可以通过把不需要被序列化的属性用transient来修饰来实现。

常见笔试题:

创建一个如下方式的DataObject:DataObject object=new DataObject();object.setWord(“123”);object.setI(2);将此对象序列化文件,并在另一个JVM中读取文件,进行反序列化,请问此时读出的DataObject对象中的word和i的值分别是( )

A.“”,0

B.“”,2

C.“123”,2

D.“123”,0

答案:D。Java在序列化的时候不会实例化static变量,因此上述代码只实例化了word,而没有实例化i。在反序列化的时候只能读取到word的值,i为默认值。