您的位置:首页 > 职场人生

Java学习笔记-《Java程序员面试宝典》-第四章基础知识-4.7输入输出流(4.7.4-4.7.6)

2017-06-22 17:12 736 查看

4.7.4 Java NIO是什么

在非阻塞IO(Nonblocking IO,NIO)出现之前,Java是通过传统的Socket来实现基本的网络通信功能的。以服务器端为例,其实现基本流程如下图所示:



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

NIO通过Selector、Channel和Buffer来实现非阻塞的IO操作,其实现原理如下图所示:



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

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

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

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

4.7.5 什么是Java系列化

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

1>序列化(Serialization)

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

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

序列化有以下两个特点:

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

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

Java提供了多个对象序列化的接口,包括:ObjectOutput、ObjectInput,ObjectOutputStream和ObjectInputStream。

示例如下:

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 setAge(int age){
this.age = age;
}

public String getName(){
return 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){

}

People pl;

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

ois = new ObjectInputStream(fis);
pl = (People) ois.readObject();
System.out.println("name:"+pl.getName()
4000
);
System.out.println("age: "+pl.getAge());
ois.close();

}catch(Exception e){

}

}
}

运行结果:
name:Lili
age: 20


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

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

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

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

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

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

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

2>外部序列化

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

public interface Externalizable extends Serializable{
void readExternal(ObjectInput in);?
void writeExternal(ObjectOutput out);?
}


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

4.7.6 System.out.println()方法使用需要注意哪些问题

Java中的System.out.println()方法提供了一种非常有效简单的方法来实现控制台的输出,该方法默认接收一个字符类型的变量作为参数。当然,在使用时可以传递任意能够转换为String类型的变量作为参数(例如基本类型int,或者一个实现toString方法的自定义类等)。示例如下:

class People{

private String name;
private  int age;

public People(){
this.name = "LRD";
this.age = 21;
}

public String toString(){
return "name: "+name+" age: "+age;
}
}
public class TestPrint {

public static void main(String[] args){
System.out.println(new People());
System.out.println(1+2+"");
System.out.println(""+1+2);
}
}

运行结果:
name: LRD age: 21
3
12


对于第一个输出语句来说,由于传入的参数是一个对象,因此会调用这个对象的toString()方法,把返回的字符串打印出来。

对于第二个输出语句来说,参数中的+会由左到右顺序计算。首先计算1+2,由于他们都是整形变量,因此计算结果为3,接着计算3+”“,由于”“是字符串,因此会首先把3转换为字符串,在进行操作,计算结果为”3”,因此输出结果为3。

对于最后一个输出语句来说,首先计算”“+1,会把1转换为字符串,其次执行加操作,计算结果为”1”。同理,接着计算”1”+2结果为”12”,因此输出结果为12。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java socket 程序员 面试
相关文章推荐