一、IO基础

Java中的IO可以分为两类:byte- and number-oriented I/O,这种类型由InputStream和OutputStream处理;character and text I/O,这种类型由Reader和Writer处理。两种类型都实现了对外部数据源或目的抽象,输入流就好比用吸管从容器中抽水,而输出流就好比用水管向容器中注水,流的概念屏蔽了底层的细节,使得我们可以采用相同的方法来进行读入、写出等操作,而不必关心操作的是文件还是控制台亦或是网络流。

为了理解Java IO,必须知道牢固掌握Java处理Byte,Integer,Character和其他原始类型的方式以及什么时候什么情况下,需要进行彼此之间的转换。

int占4个字节,范围-2,147,483,648 ~2,147,483,647。

long占8个字节,范围-9,223,372,036,854,775,808 ~9,223,372,036,854,775,807。结尾以L(推荐)作为标记。

short 和 byte. shorts,占两个字节,范围-32,768 ~ 32,767,不常用主要是为了兼容C。

byte一个字节,范围-128 ~ 127。注意是有符号的。

Java没有字面意义上的byte和short,什么意思呢,如果你书写下面的代码

byte b = 42;
short s = 24000;

42,24000实际是被当做int类型来处理的。这种情况下,由于数值在编译阶段就能确定,所有上面两行代码是允许的,但是如果这样写:

int i = 42;
byte b = i;
short s = i;

就是不允许的,因为int的范围比byte,short要大,面临截取的问题,需要显示的强制类型转换。

实际上,

byte b = 1 + 2;
byte b1 = 22;
byte b2 = 23;
byte b3 = b1 + b2;

这几行代码也是不允许的,因为byte类型的算术操作会自动提升为int类型。由于这些原因,Java IO流中的读写函数都没有直接处理byte类型,而是采用int类型。如:

public abstract int read() throws IOException

这里返回值是int类型的,-1代表读取到文件结束。至于-1为什么能标志文件结束,是因为读取的字节会作为int的低8位,剩余的3个字节填0,其值是一个正值,所以不会出现因为读到文件内容中的-1而误认为文件结束的情况。

write函数也是类似的,

public abstract void write(int b) throws IOException

这里的int b范围为0~255,小于int的范围,所以需要对256取余,如果赋值的话,再加上256。可以采用位运算b = b & 0x000000FF;保证。

然而,下面的情况:

public int read(byte[] data) throws IOException
 public int read(byte[] data, int offset, int length) throws IOException

这里采用的却是真正的byte,因为对于一个数来说8bit byte与32bit int转换的代价较小(针对write(int b)而言),而对于千万级别的数组来说,这个代价就会很大。

尽管byte里存储的是-128~127的带符号数,但是我们可以采用下面的公式将其转换为对应的无符号数:

int unsignedByte = signedByte >= 0 ? signedByte : 256 + signedByte;

字符数据

除了数字,Java IO还需要处理字符数据。我们知道计算机只能处理数字,所以字符就需要转换为相应的数字,如何转换?这就涉及到编码的问题。

ASCII(the American Standard Code for Information Interchange)

ASCII实际上用7bit来编码字符(最高位置0)。范围为0~127。的当把一个0~127整数强制转换为char就得到其ASCII字符。

所有的Java程序都可以用ASCII表示,不能表示为ASCII的Unicode字符,可以采用\u00A9这种形式表示。

ISO Latin-1

8bit字符集,可以表示256个不同的字符。包括了ASCII。

Unicode

2字节,16bit字符集,可以表示65536中不同的字符。它包括了ASCII和ISO Latin-1.

由于Java Stream 是一个字节一个字节的读取的,因此对于Unicode编码的字符来说,比如汉字,这就容易造成混乱。如果用Stream读取Unicode字符的话,可能会采用下面的方式:

int b1 = in.read();
int b2 = in.read();
char c = (char) (b1*256 + b2);

所以Java增加了Reader和Writer类。

UTF-8

Unicode编码的效率不高,尤其是处理内容中大量包含英文字符时,由此,引入UTF-8编码,它采用变长方式编码。出现频率越高的,编码长度越短。

UTF-8是一种变长字节编码方式。对于某一个字符的UTF-8编码,如果只有一个字节则其最高二进制位为0;如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的位数,其余各字节均以10开头。UTF-8最多可用到6个字节。 
如表: 
1字节 0xxxxxxx 
2字节 110xxxxx 10xxxxxx 
3字节 1110xxxx 10xxxxxx 10xxxxxx 
4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 
5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 
6字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 
因此UTF-8中可以用来表示字符编码的实际位数最多有31位,即上表中x所表示的位。除去那些控制位(每字节开头的10等),这些x表示的位与UNICODE编码是一一对应的,位高低顺序也相同。 实际将UNICODE转换为UTF-8编码时应先去除高位0,然后根据所剩编码的位数决定所需最小的UTF-8编码位数。 因此那些基本ASCII字符集中的字符(UNICODE兼容ASCII)只需要一个字节的UTF-8编码(7个二进制位)便可以表示。 

char 数据类型

包括char,char array,String.正如为了理解input、 output Stream必须理解byte一样,为了更好地理解Reader和Writer,必须理解好char。

char占2个字节,是Java中唯一一个无符号类型,范围0~65535.每个char都可以表示一个Unicode字符。可以直接采用0~65535范围内的int为char赋值,也可以用单引号包围的字符为其赋值。

char copyright = 169;
char copyright = '©';

字符型的算术运算会自动提升为int型,如同byte。

char c = 'a' + 'b';//错误
Readers and Writers

Input和Output Stream是基于byte的,而Reader和Writer是基于character的,比如ASCII和ISO Latin-1占一个字节,Unicode用两个字节,UTF-8用变长字节编码,Reader和Writer会根据相应的编码格式将读入的byte转换为char。这就是为什么必须为其指定编码的原因,编码不正确就会产生乱码。

两者的方法与Stream的也类似。例如,OutputStream声明了如下的函数:

public abstract void write(int i) throws IOException
public void write(byte[] data) throws IOException
public void write(byte[] data, int offset, int length) throws IOException

而,java.io.Writer有如下的函数:

public void write(int i) throws IOException
public void write(char[] data) throws IOException
public abstract void write(char[] data, int offset, int length) throws IOException

只是把byte换为char。还有一点不同,传递给OutputStream write()的int对256取余,而传入Writer write()的int对65536取余。

java.io.Writer还有如下的函数,从字符串获取数据,Stream是基于byte的因此没有相应的函数。(这也是记忆函数的一种方法)

public void write(String s) throws IOException
public void write(String s, int offset, int length) throws IOException。

二、Output/Input Streams


一、OutputStream

java.io.OutputStream包含下面几个方法:

public abstract void write(int b) throws IOException
 public void write(byte[] data) throws IOException
 public void write(byte[] data, int offset, int length) throws IOException
 public void flush() throws IOException
 public void close() throws IOException

从第一个函数可知,这是一个抽象类。抽象类相当于一种模板,所有实现它的子类必须实现它的抽象方法(强制性),这一点与普通父类是不同的,父类更多的强调的是继承关系,而抽象类是对共同特征的提取。

1.write函数只是向输出流输出特定的bit pattern,至于如何解释这个bit pattern取决于具体的终端。例如,System.out.write(65);控制台会把其解释为A,这是控制台的行为,而不是输出流的。

2.

public void write(byte[] data) throws IOException,
public void write(byte[] data, int offset, int length) throws IOException

第一个函数把全部的byte array数据写入,第二个函数从offset开始,写入length byte。

eg:

String s = "How are streams treating you?";
 byte[] data = s.getBytes();
 System.out.write(data);

这种方法可以提高效率,但是byte[]的大小选取也是有讲究的,比如对于文件,建议一次写入1024byte;对于网络连接,一次写入128byte,不合理的大小也会带来效率问题。

3.flush和close

write的数据会在缓冲区积累,直到缓冲区填满之后,才将其一次性写入到destination中。flush()方法可以强制清空缓冲区,把数据写入destination。当输出流关闭(程序退出或调用close())时,会隐式的flush缓冲区。考虑这样一种情况,程序非正常退出,此时缓冲区中可能还有数据,所以当我们调试程序时,显示调用flush()是很重要的。这告诉我们flush的使用需要根据具体的情况而定。

二、InputStream

java.io.InputStream是所有字节输入流的超类。包含以下方法:

public abstract int read() throws IOException
 public int read(byte[] data) throws IOException
 public int read(byte[] data, int offset, int length) throws IOException
 public long skip(long n) throws IOException
 public int available() throws IOException
 public void close() throws IOException
 public synchronized void mark(int readlimit)
 public synchronized void reset() throws IOException
 public boolean markSupported()


1.public abstract int read() throws IOException

read()返回的是int型,范围0~255,如果强制将其转换为byte,如:

byte[] b = new byte[10];
 for (int i = 0; i < b.length; i++) {
b[i] = (byte) System.in.read();
 }

这时byte是带符号的,范围-128~127。但是,只要我们心中有数,是没有任何问题的,如果想把它转换为int可以这样:int i = (b >= 0) ? b : 256 + b;

read()函数是阻塞的直到1byte的数据是available。读写是比较慢的,所以如果程序需要处理重要的事情,应该把读写操作单独放到自己的线程中。

2.

public int read(byte[] data) throws IOException
public int read(byte[] data, int offset, int length) throws IOException

在java.io.InputStream的默认实现中,这两个函数是通过调用数次read()函数实现的,子类一般对其进行重写,使其更加高效。一般通过native方法实现。

eg:

try {
byte[] b = new byte[100];
int offset = 0;
while (offset < b.length) {
int bytesRead = System.in.read(b, offset, b.length - offset);
if (bytesRead == -1) break; // end of stream
offset += bytesRead;
}
 }catch (IOException e) {System.err.println("Couldn't read from System.in!");
}

3.public long skip(long n) throws IOException

可以跳过指定的byte,返回值为跳过的byte数,到达文件末尾返回-1.数据类型为long,可以处理较大的输入流。skip比读入然后忽略数据效率要高的多。

4.public int available() throws IOException

InputStream的available()返回0,子类应该对其重写。但是并非所有的子类都对其进行了重写。

5.public void close() throws IOException

像是System.out可以不用关闭。但是对于文件和网络流,用完后要及时关闭,释放资源。

6.

public synchronized void mark(int readlimit)
 public synchronized void reset() throws IOException
 public boolean markSupported()

实现返回并重读的功能。markSupported()用于测试流是否支持mark操作。如果支持,可以用mark()在当前位置设置标记。并且之后可以用reset()返回这个标记位置。前提是读取的byte不超过readlimit。任何时刻,一个流中只有一个标记,后面的会擦除前面的。输入流中唯一两个永远支持mark操作的是BufferedInputStream(System.in is an instance)和ByteArrayInputStream。

三、FIleInputStream

一、FIleInputStream

作为具体实现类,它继承了InputStream的方法,并且与其他输入流使用起来别无二致。

public native int read() throws IOException
 public int read(byte[] data) throws IOException
 public int read(byte[] data, int offset, int length) throws IOException
 public native long skip(long n) throws IOException
 public native int available() throws IOException
 public native void close() throws IOException

可以说全部使用了native方法,第二个与第三个内部也是调用了read()方法。

构造函数如下:

public FileInputStream(String fileName) throws IOException
 public FileInputStream(File file) throws FileNotFoundException
 public FileInputStream(FileDescriptor fdObj)

硬编码的文件名具有平台相关性,所以如果可能的话应该尽量避免。建议使用后两个。

当给构造函数传递文件名时,Java会在当前的工作路径下寻找文件,也可以传递文件的绝对或相对路径。如果不存在指定的文件,会抛出FileNotFoundException,如果文件不可读,会抛出其他异常。

FileInputStream中有一个InputStream没有中没有声明的函数public final FileDescriptor getFD() throws IOException,该函数返回一个java.io.FileDescriptor对象,可以用该对象创建另一个文件流。

还有一个方法,

protected void finalize() throws IOException,该方法在相应的文件流对象被垃圾回收机制回收时调用,确保文件被关闭。一般,这个方法不需要显示的调用,但是如果自己写个子类,必须在finalize()中调用super.finalize()。

有可能对应一个文件有多个文件流,每个文件流保存一个当前位置指针。

二、FileOutputStream

同FileInputStream相似,FileOutputSTream继承了OutputStream的方法。

public native void write(int b) throws IOException
 public void write(byte[] data) throws IOException
 public void write(byte[] data, int offset, int length) throws IOException
 public native void close() throws IOException

上面所有方法也是native或者调用native方法。

构造函数与FileInputStream是对应的。

public FileOutputStream(String filename) throws IOException
 public FileOutputStream(File file) throws IOExc

eption
public FileOutputStream(FileDescriptor fd)

与FileInputStream不同的是,当文件不存在时,会自动创建。当文件存在时,原有内容会被覆盖。如果想采用追加的方式写入数据,可以采用这种构造函数:FileOutputStream(File file, boolean append)。

关于文件名的事项同FileInputStream是相同的,而且它也有两个父类中未声明的函数getFD()和finalize() 。

尽管只要Java虚拟机不是非正常退出,程序退出时,Java会自动关闭打开的文件。但是,像是web服务器这种长时间运行的程序,不用的时候应该尽快关闭文件流,以免影响其他进程或程序的访问。

四、网络流

一、URL

统一资源定位符,用于唯一的标识网络资源。

四个构造函数如下,

public URL(String u) throws MalformedURLException
 public URL(String protocol, String host, String file) throws MalformedURLException
 public URL(String protocol, String host, int port, String file) throws MalformedURLException
 public URL(URL context, String u) throws MalformedURLException

如果构造函数参数没有指定有效的URL,会抛出MalformedURLException.

给出一个绝对URL地址,可以直接调用第一个构造函数产生对象,如下:

URL u = null;
 try {
u = new URL("http://www.poly.edu/schedule/fall97/bgrad.html#cs");
 }catch (MalformedURLException e) { }

也可以采用第二个构造函数,如下

URL u = null;
 try {
u = new URL("http", "www.poly.edu", "/schedule/fall97/bgrad.html#cs");
 }catch (MalformedURLException e) { }

一般情况下,没有必要指定端口,大多数协议都有默认的端口,比如HTTP采用80端口。但是如果端口确实改变,则可以采用第三个构造函数:

URL u = null;
 try {
u = new URL("http", "www.poly.edu", 80,"/schedule/fall97/bgrad.html#cs");
 }catch (MalformedURLException e) { }

大多数情况下,HTML采用相对URL,这时采用第四个构造函数比较方便。

URL u1, u2;
 try {
u1 = new URL("http://metalab.unc.edu/javafaq/course/week12/07.html");
u2 = new URL(u1, "08.html");
 }catch (MalformedURLException e) { }

一旦构造出URL对象就可以使用openStream()获得输入流,剩下的操作就与操作普通流一样了。

二、URL Connections

通过调用URL对象的openConnection()方法,可以得到URLConnection的引用。URL connections在客户端与服务器交互上提供了更多的功能,它既提供了输入流用于客户端从服务器读取数据,也提供了输出流,使得客户端可以向服务器端发送数据。

URL连接中读取数据包括以下5步:

1.构造URL对象。

2.通过URL对象的openConnection()方法得到URLConnection对象。

3.设置连接参数和客户端发送给服务器端的请求属性。

4.connect()建立起到服务器的连接,对于网络连接可能使用的是Socket,对于本地连接可能使用文件输入流。从服务器获取响应头信息。

5.通过getInputStream()得到的输入流从服务器读取信息,也可以通过getOutputStream()得到的输出流给服务器发送数据。

“This scheme is very much based on the HTTP/1.0 protocol. It does not fit other schemes that have a more interactive "request, response, request, response, request, response" pattern instead of HTTP/1.0's "single request, single response, close connection" pattern. In particular, FTP and even HTTP/1.1 aren't well suited to this pattern. I wouldn't be surprised to see this replaced with something more general in a future version of Java.”--《Java IO》

向URL连接中写数据,包括下面几步:

1.构造URL对象。

2.调用openConnection()获取URLConnection对象。

3.给setDoOutput()传递true,表明这个URLConnection可用来输出。

4.如果同时想从这个流中读取数据,调用setDoInput(true),表明这个URLConnection可用于输入。

5.准备好待发送数据,最好是byte array.

6.调用getOutputStream()得到输出流,向该输出流写入第5步准备好的数据。

7.关闭输出流。

8.调用getInputStream()获取输入流,读入数据。

三、Sockets

数据在通过网络从一个主机发送到另一个主机之前会先被分割成数据报,数据报大小从几十bytes到60,000bytes。采用这种策略的优点是某个数据报丢失时,只需要重新发送这个数据报,而不需要全部重新发送;当数据报没有按顺序到达时,还可以进行排序。这些对程序员来说是透明的,主机的网络软件会自动为我们完成这些工作。Java程序员面对的是一个更高水平的抽象Socket。Socket表示两个发送数据的主机之间的可靠连接。它的四个基础的操作。

1. 连接到远程主机
2. 发送数据
3. 接收数据
4. 关闭连接。

Socket可能会向它连接的主机既发送数据,也接收数据。

为了连接到不同的主机,需要创建不同的Socket对象。构造方法如下:

public Socket(String host, int port) throws UnknownHostException,IOException
 public Socket(InetAddress address, int port) throws IOException
 public Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
 public Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException

其中host是像www.baidu.com这样的字符串,还可以传递java.net.InetAddress的对象。port是远程主机的端口。可选参数localAddress 和localPort指明了本地主机的地址和端口,Java API中的解释:

localAddr - the local address the socket is bound to, or null for the anyLocal address.

localPort - the local port the socket is bound to or zero for a system selected free port.

读写数据是通过输入输出流完成的。

public InputStream getInputStream() throws IOException
 public OutputStream getOutputStream() throws IOException

关闭Socket

public synchronized void close() throws IOException

三、Server Sockets

为了实现一个服务器,你需要写一个程序等待其他主机的连接请求。Server socket需要绑定服务器的一个特定端口,一旦绑定了端口,它会监听端口上的连接请求。当服务器监听到连接尝试,它接受这个连接,这就创建了两台主机的Socket,可以通过它进行通信。许多客户端可以累计连接服务器端口。服务器端接收到的数据可以通过它要到达的目的端口以及它来自的主机和端口区分。服务器可以通过端口信息得知数据需要的服务(HTTP、FTP),而且可以通过客户端地址和端口得知需要将响应发送何处。一个端口同一时刻最多只能由一台服务器监听,因此,它可能需要一次处理很多连接,它会把对每个连接的处理分配给不同的线程。新到达的连接会存储在队列中直到服务器处理他们。一旦队列满了,新的连接请求会被拒绝。

java.net.ServerSocket有三个构造函数,可以指定绑定端口,存储连接的队列长度、IP地址。

public ServerSocket(int port) throws IOException
 public ServerSocket(int port, int backlog) throws IOException
 public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException

通常只需要指定端口,

try {
ServerSocket ss = new ServerSocket(80);
 }catch (IOException e) {System.err.println(e);}

一旦获取ServerSocket,下一步就可以调用accept()方法,这个方法会一直阻塞,直到收到下一个连接尝试,然后返回一个Socket。可以用它来与客户端交流。close()方法关闭ServerSocket.

public Socket accept() throws IOException
 public void close() throws IOException

ServerSocket自身没有获取输入输出流的方法。下面代码演示了得到输出流的方式。

try {
ServerSocket ss = new ServerSocket(2345);
Socket s = ss.accept();
OutputStream out = s.getOutputStream();
// Send data to the client.
s.close();
 }catch (IOException e) {System.err.println(e);}

上面的程序中,s关闭后,ss仍然绑定2345端口。下面的代码可以重复的接受连接:

try {
ServerSocket ss = new ServerSocket(2345);
while (true) {
Socket s = ss.accept();
OutputStream out = s.getOutputStream();
// send data to the client
s.close();
}
 }catch (IOException e) {System.err.println(e);}

下面的例子从命令行读取端口号,并在此端口上监听到达的连接。当检测到连接后,回复客户端及其自身的端口号和地址。然后关闭连接。

import java.net.*;  
    import java.io.*;  
    public class HelloServer {  
        public final static int defaultPort = 2345;  
        public static void main(String[] args) {  
            int port = defaultPort;  
            try {  
                port = Integer.parseInt(args[0]);  
            }catch (Exception e) {}  
            if (port <= 0 || port >= 65536) port = defaultPort;  
            try {  
                ServerSocket ss = new ServerSocket(port);  
                while (true) {  
                    try {  
                        Socket s = ss.accept();  
                        String response = "Hello " + s.getInetAddress() + " on port "  
                        + s.getPort() + "\r\n";  
                        response += "This is " + s.getLocalAddress() + " on port "  
                        + s.getLocalPort() + "\r\n";  
                        OutputStream out = s.getOutputStream();  
                        out.write(response.getBytes());  
                        out.flush();  
                        s.close();  
                    }catch (IOException e) {}  
                }  
            }catch (IOException e) {System.err.println(e);}  
        }  
    }