java程序员面试笔试宝典-4.7输入输出流

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

java语言中,输入和输出都被称为抽象的流,流可以看作一组有序的字节集合,即数据再两设备之间的传输。
流的本质是数据传输,根据处理数据类型的不同,流分为字节流和字符流。
字节流以字节(8bit)为单位,包含两个抽象类:InputStream(输入流)和OutputStream(输出流)
字符流以字符(16bit)为单位,根据码表映射字符,一次可以读多个字节,包含两个抽象类:Reader(输入流),Writer(输出流)

字节流和字符流的主要区别是:字节流再处理输入输出时不会用到缓存,而字符流用到了缓存。

每个抽象类都有很多具体的实现类。Java IO在设计时采用了Decorator(装饰者)设计模式。
流的作用主要是为了改善程序性能并且使用方便。

例1:设计一个输入流的类,该类的作用是在读文件的时候把文件中的大写字母转换为小写字母,小写字母转换为大写字母。

在设计时,可以通过继承抽象装饰者类(FilterInputStream)来实现一个装饰类
通过调用InputStream类或其子类提供的一些方法再加上逻辑判断代码从而可以简单实现这个功能。

package cn.itcast.demo;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
//通过继承抽象装饰者类(FilterInputStream),来实现一个装饰类
public 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;
		}
	}	
}

package cn.itcast.demo;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

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.print((char)c);
			}
			is.close();
		} catch (IOException e) {
			System.out.println(e.getMessage());
		}
		
	}
}

在这里插入图片描述

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

Java中使用类File来管理文件和文件夹。
通过类不仅能够查看文件和目录的属性,而且还可以实现对文件或目录的创建,删除与重命名等操作。
在这里插入图片描述
在这里插入图片描述

例1:如何列出某个目录下的所有文件?

package cn.itcast.demo;

import java.io.File;

public class Test1 {

	public static void main(String[] args) {
		File file = new File("D:\\CopyTranslator_v0.0.7-Kylin-RC1");
		//判断目录是否存在
		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());
			}
		}
	}

}

在这里插入图片描述
在这里插入图片描述

4.7.3 Java Socket是什么?

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个双向链路的一端称为一个Socket.
Socket也称为套接字,可以用来实现不同虚拟机或不同计算机之间的通信。

在java中,Socket分为两种类型:

1)面向连接的Socket通信协议(TCP 传输控制协议)
2)面向无连接的Socket通信协议(UDP 用户数据报协议)

基于TCP的通信过程如下:

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

Socket的生命周期可以分为三个阶段:

打开Socket,
使用Socket收发数据
关闭Socket
在java语言中,可以使用ServerSocket来作为服务器端,Socket作为客户端来实现网络通信。

1.用Socket实现客户端和服务器端的通信,要求客户发送数据后能够回显相同的数据。

服务器端:

package cn.itcast.demo;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

//服务器端代码
public class Server {

	public static void main(String[] args) {
		BufferedReader br = null;
		PrintWriter pw = null;
		try {
			ServerSocket server = new ServerSocket(2000);
			Socket socket = server.accept();
			//获取输入流
			//InputStream getInputStream()  返回此套接字的输入流
			br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			//获取输出流
			//OutputStream getOutputStream()  返回此套接字的输出流
			//PrintWriter(OutputStream out, boolean autoFlush) 
			// 通过现有的 OutputStream 创建新的 PrintWriter
			pw = new PrintWriter(socket.getOutputStream(),true);
			String s = br.readLine();//获取接收的数据
			pw.println(s);//发送相同的数据给客户端
		} catch (Exception e) {
			// TODO: handle exception
			System.out.println(e.getMessage());
		}finally {
			try {
				br.close();
				pw.close();
			} catch (Exception e2) {
				// TODO: handle exception
				System.out.println(e2.getMessage());
			}
		}
	}	

}

客户端:

package cn.itcast.demo;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class Client {

	public static void main(String[] args) {
		BufferedReader br = null;
		PrintWriter pw = null;
		try {
			Socket socket = new Socket("localhost", 2000);
			//获取输入流与输出流
			br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			pw = new PrintWriter(socket.getOutputStream(),true);
			//向服务器发送数据
			pw.println("Hello");
			String s = null;
			while(true){
				s = br.readLine();
				if(s != null){
					break;
				}
			}
			System.out.println(s);
		} catch (Exception e) {
			// TODO: handle exception
			System.out.println(e.getMessage());
		}finally {
			try {
				br.close();
				pw.close();
			} catch (Exception e2) {
				// TODO: handle exception
				System.out.println(e2.getMessage());
			}
		}
	}

}

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

结果:在客户端控制台输出Hello
在这里插入图片描述

4.7.4 Java NIO是什么?

1)在非阻塞IO(NIO)出现之前,java是通过传统的Socket来实现基本的网络通信功能的。

以服务器端为例:
如果客户端还没有对服务器发起连接请求,则accept就会阻塞
阻塞是指:暂停一个线程的执行,以等待某个条件发生,例如资源就绪。
如果连接成功,当数据还没有准备好时,对read的调用同样会阻塞
当要处理多个连接时,就要采用多线程的方式,由于每个线程都有自己的栈空间,而且由于阻塞会进行大量的上下文切换,使得程序的运行效率非常低。

2)NIO通过Selector,Channel和Buffer来实现非阻塞的IO操作。

NIO非阻塞的实现主要采用了Reactor(反应器)设计模式,这个设计模式与Observer(观察者)设计模式类似,只不过Observer设计模式只能处理一个事件源头,而Reactor设计模式可以用来处理多个事件源。
在这里插入图片描述
在上图中,Channel可以被看做一个双向的非阻塞通道,在通道的两端都可以进行数据的读写操作。
Selector实现了用一个线程来管理多个通道(采用了复用和解复用的方式使得一个线程能够管理多个通道,
即可以把多个流合并为一个流,或者把一个流分成多个流的方式),它类似一个观察者。
在实现时,把需要处理的Channel的IO事件,(例如connect,read,或write等)注册给Selector.

Selector内部实现原理为:

对所有注册的Channel进行轮询访问,一旦沦陷到一个Channel1有注册的事件发生,例如有数据来了,它就通过传回Selection-Key的方式来通知开发人员对Channel1进行数据的读或写操作。
Key(由SelectionKey类表):封装一个特定的Channe1和一个特定的Selector之间的关系。
这种通过轮询的方式在处理多线程请求时不需要进行上下文的切换,而采取多线程实现方式在线程之间切换时需要上下文的切换,同时也需要及逆行压栈和弹栈操作。
因此,NIO有较高的效率。

3)Buffer用来保存数据,可以用来存放从Channe1读取的数据,也可以存放使用Chann1进行发送的数据。

4)Java提供了多种不同类型的Buffer,例如ByteBuffer,CharBuffer,CharBuffer等,通过Buffer大大简化了开发人员对流数据的管理。

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

4.7.5 什么是Java序列化?

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

1)序列化:

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

如何实现序列化呢?

所有实现序列化的类都必须实现Serializable接口(标识接口)
使用一个输出流(例如FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,
紧接着,使用该对象的writeObject(Object obj)方法就可以将obj对象写出(即保存其状态),
要恢复时可以使用其对应的输入流。

序列化有以下两个特点:

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

java提供了多个对象序列化的接口,包括ObjectOutput,ObjectInput,ObjectOutputStream和ObjectInputStreamm

例1:序列化的具体实例:

package cn.itcast.demo;

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 String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
	
	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 ex) {
			// TODO: handle exception
			System.out.println(ex.getMessage());
		}
		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 ex) {
			System.out.println(ex.getMessage());
		}
	}

}

在这里插入图片描述
由于序列化的使用会影响程序的性能,因此如果不是必须要使用序列化,应尽可能不要使用序列化。

需要使用序列化的情况:

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

与序列化相反的是反序列化,它将流转换为对象。

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

自定义serialVersionUID有以下3个优点:

1)提高程序的运行效率。
如果在类中未显示地声明serialVersionUID,那么在序列化时会通过计算得到serialVersionUID,如果显示声明则省去了计算的过程。
2)提高程序在不同平台的兼容性,因为可能不同平台计算serialVersionUID方式不同。
3)增强程序各个版本的兼容性。
后期对类进行修改,加入新的属性,serialVersionUID会发生变化,如果不显示声明,将会导致类在修改前对象序列化的文件在修改后无法进行反序列化操作。

2)外部序列化

java还提供了另一种方式来实现对象序列化,即外部序列化。
接口如下

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

外部序列化与序列化的区别:

外部序列化是内置的API,只需要实现Serializable接口,开发人员不需要编写任何代码就可以实现对象的序列化。
而使用外部序列化时,Externalizable 接口中的读写方法必须有开发人员来实现。难度大,灵活性高。
可以对需要持久化的那些属性进行控制。

引申:在使用接口Serializable实现序列化时,这个类中的所有属性都会被序列化,那么如何实现只序列化部分属性呢?

方法一:

实现Externalizable 接口,开发人员根据需要来实现readExternal和writeExternal

方法二:

使用关键字transient来控制序列化的属性,被transient修饰的属性是临时的,不会被序列化。

public class DataObject implements Serializable{
	public static int i = 0;
	private String word = "";
	public static void setI(int i){
		DataObject.i = i;
	}
	
	public void setWord(String word) {
		this.word = word;
	}
}
创建一个如下方式的DataObject:
        DataObject object = new DataObject();
        object.setI(2);
        object.setWord("123");
将此对象序列化文件,并在另一个JVM中读取文件,进行反序列化,请问此时读出的DataObject对象中的word和i是什么

结果:
"123"  和  0

说明:
Java在序列化时不会实例化static变量,上述代码只实例化了word,而没有实例化i
在反序列化时只能读取到word的值,i为默认值0

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

System.out.println()实现控制台的输出。
方法默认接收一个字符串类型的变量作为参数,在使用时可以传递任意能够转换为String类型的变量作为参数
例如基本类型int,或者是一个实现toString方法的自定义类等

package cn.itcast.demo;

public class People1 {
	private String name;
	private int age;
	public People1(){
		this.name = "小强";
		this.age = 18;
	}
	
	@Override
	public String toString() {
		return "People1 [name=" + name + ", age=" + age + "]";
	}

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

}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值