I/O 流概述
大部分程序都需要进行输入/输出处理,比如从键盘读取数据、从屏幕中输出数据、从文件中写数据等等。在 Java 中,把这些不同类型的输入、输出源抽象为流(Stream),而其中输入或输出的数据则称为数据流(Data Stream),用统一的接口表示,从而使程序设计简单明了。
流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
流一般分为输入流(Input Stream)和输出流(Output Stream)两类,但这种划分并不是绝对的。比如一个文件,当向其中写数据时,它就是一个输出流;当从其中读取数据时,它就是一个输入流。当然,键盘只是一个输入流,而屏幕则只是一个输出流。(其实我们可以通过一个非常简单的方法来判断,只要是向内存中写入就是输入流,从内存中写出就是输出流)。
基类:InputStream 和 OutputStream
字节流主要操作 byte 类型数据,以 byte 数组为准,java 中每一种字节流的基本功能依赖于基本类 InputStream 和 Outputstream,他们是抽象类,不能直接使用。字节流能处理所有类型的数据(如图片、avi 等)。
InputStream
InputStream 是所有表示字节输入流类的基类,继承它的子类要重新定义其中所定义的抽象方法。InputStream 是从装置来源地读取数据的抽象表示,例如 System 中的标准输入流 in 对象就是一个 InputStream 类型的实例。
在 InputStream 类中,方法 read() 提供了三种从流中读数据的方法:
一 int read():从输入流中读一个字节,形成一个 0~255 之间的整数返回(是一个抽象方法)。
二 int read(byte b[]):从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
三 int read(byte b[],int off,int len):从输入流中读取长度为 len 的数据,写入数组 b 中从索引 off 开始的位置,并返回读取得字节数。
对于这三个方法,若返回 -1,表明流结束,否则,返回实际读取的字符数。
OutputStream
OutputStream 是所有表示字节输出流类的基类。子类要重新定义其中所定义的抽象方法,OutputStream 是用于将数据写入目的地的抽象表示。例如 System 中的标准输出流对象 out 其类型是 java.io.PrintStream,这个类是 OutputStream 的子类。
一般来说,很少直接实现 InputStream 或 OutputStream 上的方法,因为这些方法比较低级,通常会实现它们的子类。
文件流
在 I/O 处理中,最常见的就是对文件的操作。java.io 包中所提供的文件操作类包括:
用于读写本地文件系统中的文件:FileInputStream 和 FileOutputStream
描述本地文件系统中的文件或目录:File、FileDescriptor 和 FilenameFilter
提供对本地文件系统中文件的随机访问支持:RandomAccessFile
今天我们来学习文件流的 FileInputStream 和 FileOutputStream。
FileInputStream 类用于打开一个输入文件,若要打开的文件不存在,则会产生异常
FileNotFoundException,这是一个非运行时异常,必须捕获或声明抛弃。 FileOutputStream 类用来
打开一个输出文件,若要打开的文件不存在,则会创建一个新的文件,否则原文件的内容会被新写入的内容所覆盖。
在进行文件的读/写操作时,会产生非运行时异常 IOException,必须捕获或声明抛弃(其他的输入/输出流处理时也同样需要进行输入/输出异常处理)。
缓冲流
类 BufferedInputStream 和 BufferedOutputStream 实现了带缓冲的过滤流,它提供了缓冲机制,把任意的 I/O 流“捆绑”到缓冲流上,可以提高 I/O 流的读取效率。
在初始化时,除了要指定所连接的 I/O 流之外,还可以指定缓冲区的大小。缺省时是用 32 字节大小的缓冲区;最优的缓冲区大小常依赖于主机操作系统、可使用的内存空间以及机器的配置等;一般缓冲区的大小为内存页或磁盘块等的整数倍。
BufferedInputStream 的数据成员 buf 是一个位数组,默认为 2048 字节。当读取数据来源时例如文件,BufferedInputStream 会尽量将 buf 填满。当使用 read () 方法时,实际上是先读取 buf 中的数据,而不是直接对数据来源作读取。当 buf 中的数据不足时,BufferedInputStream 才会再实现给定的 InputStream 对象的 read() 方法,从指定的装置中提取数据。
BufferedOutputStream 的数据成员 buf 是一个位数组,默认为 512 字节。当使用 write() 方法写入数据时,实际上会先将数据写至 buf 中,当 buf 已满时才会实现给定的 OutputStream 对象的 write() 方法,将 buf 数据写至目的地,而不是每次都对目的地作写入的动作。
对于 BufferedOutputStream,只有缓冲区满时,才会将数据真正送到输出流,但可以使用 flush() 方法人为地将尚未填满的缓冲区中的数据送出。
数据流
接口 DataInput 和 DataOutput,设计了一种较为高级的数据输入输出方式:除了可处理字节和字节数组外,还可以处理 int、float、boolean 等基本数据类型,这些数据在文件中的表示方式和它们在内存中的一样,无须转换,如 read(), readInt(), readByte()…; write(), writeChar(), writeBoolean()… 此外,还可以用 readLine() 方法读取一行信息。
数据流类 DataInputStream 和 DataOutputStream 的处理对象除了是字节或字节数组外,还可以实现对文件的不同数据类型的读写:
1 分别实现了 DataInput 和 DataOutput 接口。
2 在提供字节流的读写手段同时,以统一的形式向输入流中写入 boolean,int,long,double 等基本数据类型,并可以再次把基本数据类型的值读取回来。
3 提供了字符串读写的手段。
标准流
语言包 java.lang 中的 System 类管理标准输入/输出流和错误流。
System.in 从 InputStream 中继承而来,用于从标准输入设备中获取输入数据(通常是键盘) System.out 从 PrintStream 中继承而来,把输入送到缺省的显示设备(通常是显示器) System.err 也是从 PrintStream 中继承而来,把错误信息送到缺省的显示设备(通常是显示器)
每当 main 方法被执行时,就会自动生产上述三个对象。这里就不再写代码验证了。
内存读写流
为了支持在内存上的 I/O,java.io 中提供了类:ByteArrayInputStream、ByteArrayOutputStream 和 StringBufferInputStream
ByteArrayInputStream 可以从指定的字节数组中读取数据。
ByteArrayOutputStream 中提供了缓冲区可以存放数据(缓冲区大小可以在构造方法中设定,缺省为 32),可以用 write() 方法向其中写入数据,然后用 toByteArray() 方法将缓冲区中的有效字节写到字节数组中去。size() 方法可以知道写入的字节数,reset() 可以丢弃所有内容。
StringBufferInputStream 与 ByteArrayInputStream 相类似,不同点在于它是从字符缓冲区 StringBuffer 中读取 16 位的 Unicode 数据,而不是 8 位的字节数据(已被 StringReader 取代)。
顺序输入流
java.io 中提供了类 SequenceInputStream,使应用程序可以将几个输入流顺序连接起来。顺序输入流提供了将多个不同的输入流统一为一个输入流的功能,这使得程序可能变得更加简洁。
字符流基类
java.io 包中专门用于字符流处理的类,是以 Reader 和 Writer 为基础派生的一系列类。
字符流以字符为单位,根据码表映射字符,一次可能读多个字节,只能处理字符类型的数据。
同类 InputStream 和 OutputStream 一样,Reader 和 Writer 也是抽象类,只提供了一系列用于字符流处理的接口。它们的方法与类 InputStream 和 OutputStream 类似,只不过其中的参数换成字符或字符数组。
Reader 是所有的输入字符流的父类,它是一个抽象类。
Writer 是所有的输出字符流的父类,它是一个抽象类
InputStreamReader 和 OutputStreamWriter 是 java.io 包中用于处理字符流的最基本的类,用来在字节流和字符流之间作为中介:从字节输入流读入字节,并按编码规范转换为字符;往字节输出流写字符时先将字符按编码规范转换为字节。使用这两者进行字符处理时,在构造方法中应指定一定的平台规范,以便把以字节方式表示的流转换为特定平台上的字符表示。
如果读取的字符流不是来自本地时(比如网上某处与本地编码方式不同的机器),那么在构造字符输入流时就不能简单地使用缺省编码规范,而应该指定一种统一的编码规范“ISO 8859_1”,这是一种映射到 ASCCII 码的编码方式,能够在不同平台之间正确转换字符。
缓存流
同样的,为了提高字符流处理的效率,java.io 中也提供了缓冲流 BufferedReader 和 BufferedWriter。其构造方法与 BufferedInputStream 和 BufferedOutPutStream 相类似。另外,除了 read() 和 write() 方法外,它还提供了整行字符处理方法:
1 public String readLine():BufferedReader 的方法,从输入流中读取一行字符,行结束标志\n、\r或者两者一起(这是根据系统而定的)。
2 public void newLine():BufferedWriter 的方法,向输出流中写入一个行结束标志,它不是简单地换行符\n或\r,而是系统定义的行隔离标志(line separator)。
1 对字符数组进行处理: CharArrayReader、CharArrayWrite
2 对文本文件进行处理:FileReader、FileWriter
3 对字符串进行处理:StringReader、StringWriter
4 过滤字符流:FilterReader、FileterWriter
5 管道字符流:PipedReader、PipedWriter
6 行处理字符流:LineNumberReader
7 打印字符流:PrintWriter
文件操作
java.io 定义的大多数类都是流式操作,但 File 类不是。它直接处理文件和文件系统。File 类没有指定信息怎样从文件读取或向文件存储;它描述了文件本身的属性。File 对象用来获取或处理与磁盘文件相关的信息,例如权限,时间,日期和目录路径。此外,File 还浏览子目录层次结构。Java 中的目录当成 File 对待,它具有附加的属性——一个可以被 list() 方法检测的文件名列表。
对于 FileInputStream/FileOutputStream、FileReader/FileWriter 来说,它们的实例都是顺序访问流,即只能进行顺序读/写。而类 RandomAccessFile 则允许文件内容同时完成读和写操作,它直接继承 object,并且同时实现了接口 DataInput 和 DataOutput。
随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。存在指向该隐含数组的光标或索引,称为文件指针。输入操作从文件指针开始读取字节,并随着对字节的读取而前移此文件指针。如果随机访问文件以读取/写入模式创建,则输出操作也可用。输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。
RandomAccessFile 提供了支持随机文件操作的方法:
readXXX() 或者 writeXXX(): 如 ReadInt(),ReadLine(),WriteChar(),WriteDouble() 等
int skipBytes(int n): 将指针向下移动若干字节
length(): 返回文件长度
long getFilePointer(): 返回指针当前位置
void seek(long pos): 将指针调用所需位置
在生成一个随机文件对象时,除了要指明文件对象和文件名之外,还需要指明访问文件的模式。
在 java 中有数据传输的地方都用到 I/O 流(通常是文件、网络、内存和标准输入输出等)。
InputStream 和 OutputStream 是所有字节流的祖先(只有 RandAccessFile 类是一个列外),read 和 write 是它们最基本的方法,读写单位是字节。
Reader 和 Writer 是所有字符流的祖先,read 和 write 是它们最基本的方法,读写单位是字符。
在众多的流对象中,并不是每一种都单独使用,其中过滤流的子类在数据送出去之前做必要的处理。
从线程到多线程
首先你应该知道什么是线程:
线程:程序执行流的最小单元。它是进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派 CPU 的基本单位。
如同大自然中的万物,线程也有“生老病死”的过程,下图表示了一个线程从创建到消亡的过程,以及过程中的状态。
结合线程的生命周期,我们再来看看多线程的定义:
多线程:从软件或者硬件上实现多个线程并发执行的技术。在单个程序中同时运行多个线程完成不同的工作。
在 Java 中,垃圾回收机制 就是通过一个线程在后台实现的,这样做的好处在于:开发者通常不需要为内存管理投入太多的精力。反映到我们现实生活中,在浏览网页时,浏览器能够同时下载多张图片;实验楼的服务器能够容纳多个用户同时进行在线实验,这些都是多线程带来的好处。
从专业的角度来看,多线程编程是为了最大限度地利用 CPU 资源——当处理某个线程不需要占用 CPU 而只需要利用 IO 资源时,允许其他的那些需要 CPU 资源的线程有机会利用 CPU。这或许就是多线程编程的最终目的。当然,你也可以进一步了解 为什么使用多线程。
对于多线程和线程之间的关系,你可以这样理解:一个使用了多线程技术的程序,包含了两条或两条以上并发运行的线程(Thread)。
Java 中的 Thread 类就是专门用来创建线程和操作线程的类
创建线程
根据我们前面所学,我们可以自定义一个类,然后继承 Thread 类来使其成为一个线程类。
那么我们要把线程要做的事情放在哪里呢?在 Java 中,run() 方法为线程指明了它要完成的任务,你可以通过下面两种方式来为线程提供 run 方法:
继承 Thread 类并重写它的 run() 方法,然后用这个子类来创建对象并调用 start() 方法。
通过定义一个类,实现 Runnable 接口,实现 run() 方法。
概括一下,启动线程的唯一的方法便是 start(),而你需要把待完成的工作(功能代码)放入到 run() 方法中。
查看线程运行状态
线程的状态共有 6 种,分别是:新建 New、运行(可运行)Runnable、阻塞 Blocked、计时等待 Timed Waiting、等待 Waiting 和终止 Terminate。
当你声明一个线程对象时,线程处于新建状态,系统不会为它分配资源,它只是一个空的线程对象。 调用 start() 方法时,线程就成为了可运行状态,至于是否是运行状态,则要看系统的调度了。 调用了 sleep() 方法、调用 wait() 方法和 IO 阻塞时,线程处于等待、计时等待或阻塞状态。 当 run() 方法执行结束后,线程也就终止了。
sleep(),在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。填入的参数为休眠的时间(单位:毫秒)。