面试准备系列——Java基础技术篇(7)/输入输出流

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

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

流的本质是数据传输,根据处理数据类型的不同,流可以分为两大类:字节流和字符流字节流以字节(8bit) 为单位,包含两个抽象类: InputStream (输人流)和OutputStream (输出流)字符流以字符(16 bit)为单位,根据码表映射字符,一次可以读多个字节,它包含两个抽象类: Reader (输入流)和Writer (输出流)。字节流和字符流最主要的区别为:字节流
在处理输入输出时不会用到缓存,而字符流用到了缓存。每个抽象类都有很多具体的实现类,在这里就不详细介绍了。

下图主要介绍Java中I0的设计理念。Java I0类在设计时采用了Decorator (装饰者)设计模式,以InputStream为例,介绍Decorator 设计模式在I0类中的使用如下。
在这里插入图片描述
其中,ByteArrayInputStream 、StringBufferInputStream、 FileInputStream 和PipedInputStream是Java提供的最基本的对流进行处理的类,FilterInputStream为一个封装类的基类,可以对基本的I0类进行封装,通过调用这些类提供的基本的流操作方法来实现更复杂的流操作。

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

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

常见题型:
在这里插入图片描述

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

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

3.(重点)Java Socket 是什么

网络上的两个程序通过–个双向的通信连接实现数据的交换,这个双向链路的一-端称为一个Socket。Socket 也称为套接字,可以用来实现不同虚拟机或不同计算机之间的通信。在Java语言中,Socket 可以分为两种类型:面向连接的Socket 通信协议( TCP,Transmission Control Protocol,传输控制协议)和面向无连接的Socket通信协议( UDP,User Datagram Protocol,用户数据报协议)。任何一个Socket都是由IP 地址和端口号唯一确定的,如下图所示。
在这里插入图片描述
基于TCP的通信过程如下:首先,Server ( 服务器)端Listen ( 监听)指定的某个端口( 建议使用大于1024的端口)是否有连接请求;其次,Client (客户)端向Server端发出Connect ( 连接)请求;最后,Server 端向Client端发回Accept ( 接受)消息。一个连接就建立起来了,会话随即产生。Server端和Client端都可以通过Send、Write等方法与对方通信

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

常见题型:
使用Socket实现客户端与服务端的通信,要求客户发送数据之后能够回显相同的数据

答案:首先创建一个服务端

public class _7_Server {

    public static void main(String[] args) {
        BufferedReader bufferedReader = null;
        PrintWriter printWriter = null;

        try {
            //服务端监听端口
            ServerSocket server = new ServerSocket(1100);
            //服务端接受socket
            Socket socket = server.accept();
            //获取输入流
            bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            //获取输出流
            printWriter = new PrintWriter(socket.getOutputStream(),true);
            //获取从客户端传输过来的内容
            String s = bufferedReader.readLine();
            System.out.println(s);
            //将获取到的内容发一份回去给客户端
            printWriter.println(s);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                bufferedReader.close();
                printWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

再创建一个客户端:

public class _7_Client {

    public static void main(String[] args) {
        BufferedReader bufferedReader = null;
        PrintWriter printWriter = null;

        try {
            //获取客户端连接套接字
            Socket socket = new Socket("127.0.0.1",1100);
            //获取输入流
            bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            //获取输出流
            printWriter = new PrintWriter(socket.getOutputStream(),true);
            //发送消息给服务端
            printWriter.println("hello");
            //接收服务端返回的数据
            String s = null;
            while (true) {
                s = bufferedReader.readLine();
                if (s != null) {
                    break;
                }
            }
            //在控制台打印返回的消息
            System.out.println(s);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                bufferedReader.close();
                printWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

最后启动服务器端程序,然后运行客户端程序,客户端将会把从服务器端转发过来的“Hello"打印出来。

4.java 中的 NIO 是什么

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

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

NIO通过Selector、Channel 和Buffer来实现非阻塞的I0操作,其实现原理如下图所示。
在这里插入图片描述
NIO非阻塞的实现主要采用了Reactor (反应器)设计模式,这个设计模式与Observer(观察者)设计模式类似,只不过Observer设计模式只能处理一个事件源,而Reactor设计模式可以用来处理多个事件源。

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

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

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

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。

下面给出一个序列化的具体实例:
在这里插入图片描述
由于序列化的使用会影响系统的性能,因此如果不是必须要使用序列化,应尽可能不要使用序列化。那么在什么情况下需要使用该序列化呢?

  • 1)需要通过网络来发送对象,或对象的状态需要被持久化到数据库或文件中。
  • 2)序列化能实现深复制,即可以复制引用的对象。

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

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

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

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

  • (2)外部序列化
    Java语言还提供了另外一种方式来实现对象持久化,即外部序列化。其接口如下:
    在这里插入图片描述

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

引申:在用接口Serializable实现序列化时,这个类中的所有属性都会被序列化,那么怎样才能实现只序列化部分属性呢?
一种方法为实现Extermalizable接口,开发人员可以根据实际需求来实现readExternal与writeExternal方法来控制序列化与反序列化所使用的属性,这种方法的缺点为增加了编程的难度。另一种方法为使用关键字transient来控制序列化的属性。被transient修饰的属性是临时的,不会被序列化。因此,可以通过把不需要被序列化的属性用transient来修饰。

常见题型:
在这里插入图片描述

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

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

在这里插入图片描述
对于第一个输出语句来说,由于传入的参数是-一个对象,因此会调用这个对象的toString()方法,把返回的字符串打印出来。对于第二个输出语句来说,参数中的+会由左到右顺序计算。首先计算1 +2,由于它们都是整型变量,因此计算结果为3,接着计算3 +"",由于""是字符串,因此首先会把3转换为字符串,其次执行加操作,计算结果为“3”, 因此输出结果为3。对于最后一个输出语句来说,首先计算"" +1,会把1转换为字符串,其次执行加操作,计算结果为“1”,同理,接着计算“1”+2结果为“12”,因此输出结果为12。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值