面试:Java 输入输出流

1. Java IO 流的实现机制是什么?

流的本质是数据传输,根据处理数据类型的不同,流可以分为两大类:字节流和字符流。字节流以字节(8bit)为单位,包含两个抽象类: InputStream(输入流)和 OutputStream(输出流)。

字符流以字符(Character 16 bit)为单位,根据码表映射字符,一次可以读多个字节,它包含两个抽象类: Reader(输入流)和 Writer (输出流)。

字节流与字符流最主要的区别为:字节流在处理输入输出时不会用到缓存,而字符流用到了缓存。

1.1 字节流

字节流每个抽象类都有很多具体的实现类在这里就不详细介绍了。下图主要介绍Java中 IO 的设计理念。 Java IO类在设计时采用了Decorator 装饰器设计模式,以 InputStream为例,介绍 Decorator 设计模式在IO类中的使用如下。

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

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

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


class MyOwnInputStream extends FilterInputStream{

public MyOwnInputStream(InputStream in){

super(in);

}



public int read() throws IOException{

int c = 0;

if((c = super.read())!= -1){

if(Character.isLowerCase((char)c))

return Character.toUpperCase((char)c);

else if(Character.isUpperCase((char)c))

return Character.toLowerCase((char)c);

else

return c;

}

else{

return -1;

}

}

}



public class Test{

public static void main(String[] args){

int c;

try{

InputStream is = new MyOwnInputStream(new BufferedInputStream(new FileInputStream("test.txt")));

while((c = is.read()) >= 0){

System.out.println((char)c);

}

is.close();

}

catch(IOException e){

System.out.println(e.getMessage());

}

}

}



// test.txt: aaaBBBcccDDD123

// output: AAAbbbCCCddd123

2. 管理文件和目录的类是什么?


import java.io.File;

public class Test{

public static void main(String[] args){

File file = new File("testDir");

if(! file.exists()){

System.out.println("directory is empty");

return;

}

File[] fileList = file.listFiles();

for(int i = 0; i < fileLIst.length(); i++){

if(fileList[i].isDirectory()){

System.out.println("directory is: " + fileList[i].getName());

}

else{

System.out.println("file is: " + fileList[i].getName());

}

}

}

}

3. Java Socket 是什么?

Socket 称为套接字,用来实现计算机之间的通信,分面向连接(TCP)和面向无连接(UDP)的通信协议。任何一个 Socket 由 IP 地址和端口号唯一确定。

基于 TCP 的通信过程如下:首先, Server(服务器)端 Listen(监听)指定的某个端口(建议使用大于1024的端口)是否有连接请求;其次, Client(客户)端向Server端发出 Connect(连接)请求;最后, Server端向Client端发回 Accept(接受)消息。一个连接就建立起来了,会话随即产生。 Server端和 Client端都可以通过SendWrite等方法与对方通信。


import java.net.*;

import java.io.*;



class Server{

public static void main (String[] args) {

BufferedReader br = null;

PrintWriter pw = null;

try{

ServerSocket server = new ServerSocket(3000); // 监听 3000 端口

Socket socket = server.accept(); // 接请求

// 获取输入流

br = new BufferedReader(new InputStreamReader(socket.getInputStream()));

// 获取输出流

pw = new PrintWriter(socket.getOutputStream(), true);

String s = br.readLine(); // 获取接受的数据

pw.println(s); // 发送相同数据给客户端

}

catch(Exception e){

e.printStackTrace();

}

finally{

try{

br.close();

pw.close();

}

catch (Exception e) {

}

}

}

}

客户端程序:


import java.net.*;

import java.io.*;



class Client{

public static void main (String[] args) {

BufferedReader br = null;

PrintWriter pw = null;

try{

Socket socket = new Socket("localhost", 3000); // 客户端绑定 3000 端口

// 获取输入流

br = new BufferedReader(new InputStreamReader(socket.getInputStream()));

// 获取输出流

pw = new PrintWriter(socket.getOutputStream(), true);

pw.println("Hello"); // 向服务端发送数据

String s = null;

while(true){ // 不停地读缓冲区

// socket 的 输入流都在 BufferdReader 里面

s = br.readLine();

if(s!=null) break;

}

System.out.println(s);

}

catch(Exception e){

e.printStackTrace();

}

finally{

try{

br.close();

pw.close();

}

catch (Exception e) {

}

}

}

}

先开启服务端,再开启客户端,可以看到客户端输出其打印的 “Hello”

4. Java NIO 是什么?

NIO 是非阻塞 IO(Nonblocking IO),在它出现之前,Java 通过传统的 Socket 来通信:

如果客户端还没有对服务器端发起连接请求,那么 accept 就会阻塞(阻塞指的是暂停一个线程的执行以等待某个条件发生,例如某资源就绪)。如果连接成功,当数据还没有准备好时,对 read 的调用同样会阻塞。当要处理多个连接时,就需要采用多线程的方式,由于每个线程都拥有自己的栈空间,而且由于阻塞会导致大量线程进行上下文切换,使得程序的运行效率非常低下,因此在 J2SE1.4 中引入了NIO来解决这个问题。

NIO通过 SelectorChannelBuffer来实现非阻塞的IO操作:

NIO 非阻塞的实现主要采用了 Reactor(反应器)设计模式,这个设计模式与 Observer(观察者)设计模式类似,只不过 Observer 设计模式只能处理一个事件源Reactor 设计模式可以用来处理多个事件源。

在上图中, Channel可以被看作一个双向的非阻塞的通道,在通道的两边都可以进行数据的读写操作。 Selector实现了用一个线程来管理多个通道(采用了复用与解复用的方式使得一个线程能够管理多个通道,即可以把多个流合并成为一个流,或者把一个流分成多个流的方式),它类似于一个观察者。在实现时,把需要处理的 Channel的IO事件(例如 connectreadwrite等)注册给 SelectorSelector内部的实现原理为:对所有注册的 Channel进行轮询访问,一旦轮询到一个 Channe1有注册的事件发生,例如有数据来了,它就通过传回 Selection-Key的方式来通知开发人员对 Channe1 进行数据的读或写操作Key(由 Selection Key类表示)封装一个特定 Channe1和一个特定的 selector之间的关系。 这种通过轮询的方式在处理多线程请求时不需要上下文的切换,而采用多线程的实现方式在线程之间切换时需要上下文的切换,同时也需要进行压栈与弹栈操作。因此,NIO有较高的执行效率。

Buffer 用来保存数据,可以用来存放从 Channe1读取的数据,也可以存放使用 Channe 1进行发送的数据。Java提供了多种不同类型的 Buffer,例如 ByteBufferCharBuffer等,通过Buffer,大大简化了开发人员对流数据的管理。

NIO 在网络编程中有着非常重要的作用,与传统的 Socket方式相比,由于NIO采用了非阻塞的方式,在处理大量并发请求时,使用NIO要比使用 Socket效率高出很多

5. 什么是 Java 序列化?

Java 提供两种对象持久化方式:序列化、外部序列化。

5.1 序列化(Serialization)

所有要实现序列化的类都必须事项 Serializable 接口,其位于 Java.lang 包中,里面也没有任何方法。

使用一个输出流(如 FileOuputStream)来构造一个 ObjectOutputStream(对象流)对象,接着会用该对象的 writeObject (Object obj)方法可以将 obj 对象写入(即保存状态),要恢复时可以使用期对应的输入流。

Java提供了多个对象序列化街扩,包括 ObjectOuput, ObjectInput, ObjectOutputStreamObjectInputStream。实例:


import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.io.Serializable;



public class People implements Serializable{



private String name;

private int age;

public People(){

this.name = "lili";

this.age = 20;

}

public int getAge(){

return age;

}

public void steAge(int age){

this.age = age;

}

public String getName(){

return this.name;

}

public void setName(String name){

this.name = name;

}

public static void main(String[] args){

People p = new People();

ObjectOutputStream oos = null;

ObjectInputStream ois = null;

try{

FileOutputStream fos = new FileOutputStream("people.out");

oos = new ObjectOutputStream(fos);

oos.writeObject(p);

oos.close();

}catch(Exception e){

e.printStackTrace();

}



People p1;

try{

FileInputStream fis = new FileInputStream("people.out");

ois = new ObjectInputStream(fis);

p1= (People) ois.readObject();

System.out.println("name: " + p1.getName());

System.out.println("age: " + p1.getAge());

ois.close();

}

catch (Exception e) {

e.printStackTrace();

}

}

}



// 输出

// name:lili

// age:20

序列化会影响系统性能,除非必须使用,尽可能不用。

需要使用序列化的情况:

  1. 通过网络发送对象;或者对象状态要被持久化到数据库或文件中

  2. 序列化能实现深复制,即可以复制引用的对象。

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

5.2 外部序列化

外部序列化 Externalizable接口如下:


public interface Externalizable extends Serializable{

void readExternal(ObjectInput in);?

void writeExternal(ObjectOutput out);?

}

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

在用接口 Serializable 实现序列化时,类所有的属性都会被序列化,怎样才能实现只序列化部分属性?

答案:用 transient 修饰不想被序列化的属性。

注:序列化时不会实例化 static 变量的值(序列化的是实例,而不是类,实例不保存静态变量的值)

6. System.out.println() 方法要注意什么问题?

表达式结合顺序