Unix使用斜杆/ 作为路径分隔符,而web应用最新使用在Unix系统上面,所以目前所有的网络地址都采用 斜杆/ 作为分隔符。
Windows由于使用 斜杆/ 作为DOS命令提示符的参数标志了,为了不混淆,所以采用 反斜杠\ 作为路径分隔符。所以目前windows系统上的文件浏览器都是用 反斜杠\ 作为路径分隔符。随着发展,DOS系统已经被淘汰了,命令提示符也用的很少,斜杆和反斜杠在大多数情况下可以互换,没有影响。而网址开头的双斜杠表示这是这串地址的开始,从这里开始解析。在shell等里面,开头的斜杠就表示根目录。
1.浏览器地址栏中使用“/” 出现在html url()中也是"/";
2.windows文件浏览器上使用 反斜杠\ ;
3.编程语言中“\”表示转义,所以路径中用"\\",转义后被理解为"\"
4.java中的文件路径可以使用“/”或者“\\”
5.Unix使用斜杆/ 作为路径分隔符。
概述:流是一组有序的数据序列,分为输入和输出,I(input)/O(output) ,它提供了一条通道程序,可以使这条通道把源中的字节序列送到目的地,虽然I/O流经常与磁盘文件存取有关,但是程序的源和目的地也可以是键盘、鼠标、内存、显示器等。
负责输入输出的类在java。io中,所有输入流类都是抽象类 InputStream(字节输入流)或者抽象类Reader(字符输入流)的子类,而所有输出流都是抽象类OutputStream(字节输出流)或者抽象类Writer(字符输出流)的子类。(输入输出读写可以以程序为参照),如果操作的数据用文本记事本打开,打开的数据是可以看懂的就可以用字符流,不能用字符流,如果不知道就可以直接用字节流。输入流和输出流的读写都是阻塞的。
输入流:
并不是所有的InputStream类的子类都支持InputStream中定义的方法,如skip()、mark()、reset()只对某些可用,java中的字符是Unicode编码,是双字节的,InputStream是用来处理字节的,在处理字符文本时不是很方便,所以java为字符文本提供了一套单独的类Reader,它是字符输入流的抽象类,所有字符输入流的实现类都是它的子类,
字符输入流:Reader
输出流:OutStream
outputStream类中的所有方法均返回void
字符输出流:Writer
File类:IO中唯一代表磁盘文件本身的对象,File类定义了一些与平台无关的方法来操作文件,可以通过调用File类中的方法,实现创建、删除、重命名文件等,他主要用来获取文件本身的一些信息,数据流可以将数据写入文件中,
文件的创建与删除:创建主要有3个构造函数,(1)new File(string 路径名称并包含文件扩展名)(2)new File(string 父路径字符串, string 子路径字符串并包含文件扩展名)(3)new File(File 父路径对象, string 子路径字符串).对于MircroSoft windows平台,包含盘的路径名前缀由驱动器和一个“:”组成,如果路径是绝对路径 还可能跟“\\”.(绝对路径就是从盘符开始,不用拼接的路径)这里有一个用File.exists()和file。delete()做了个小练习。"\"为转义字符所以用两个。如果没有写盘符路径,默认为项目路径下。java的删除不走回收站,文件夹下有内容时,delete方法删除文件夹失败。renameTo方法,如果路径名相同就是改名,路径名不同就是剪切+改名。list()方法为获取该目录下一级的文件和文件夹的名字, listFiles()方法获取该目录下一级的文件和文件夹的对象。也可以传一个filenameFilter接口的参数,用来过滤想要找的文件名字,只需要用一个内部类,并且编写accept方法即可(File参数为父File对象,String参数为文件名)。
文件字节输入输出流:操作后记得释放资源。也就是关闭输入输出流。调用close()方法,作用:让流对象变成垃圾,这样可以被垃圾回收器回收,通知系统去释放跟该文件相关的资源。两者先关谁都行。
构造方法:new FileInputStream(string name)用给定的文件名创建一个对象或者new FileInputStream(File file)用给定的File对象创建一个对象,FileOutputStream 类有与FileInputStream类相同的构造方法,创建一个 FileOutputStream 对象时,可以指定一个不存在的文件名(不存在的时候系统会创建一个),但是此文件不能是一个已经被其他程序打开的文件,虽然Java语言在程序结束时自动关闭所有打开的流,但是当使用完后,主动关闭是个好习惯,如果没有关闭,另一个程序访问的时候可能访问不到。不同的系统不同的换行符,一般高级编辑器的换行符是\n,windows的换行符是\r\n。在创建自己输出流对象的时候可以用有两个参数的构造方法确定是否追加。FileInputStream的read()方法在不关闭字节流的时候每次都是从下一字节开始读结束时返回-1,但是-1也可以读,但是此方法太慢,可以用一次读取一个字节数组,用字节数组读取写入的时候 要用输入字节流缓冲区的字节长度来输出字节流,因为最后有肯能没有占满你定义的字节数组,会有默认值,所以要用write(byte【】,0,length(这个为read(byte【】)方法返回的值))。
FileInputStream和FileOutputStream 两个类只提供了对字节或字节数组的读取方法,由于汉子占两个字节,如果使用字节流,读取不好可能会出现乱码现象,此时采用FileReader和FileWriter即可避免这种现象。FileReader能流顺的读取文件,只要不关闭流,每次调用read()方法就会顺序的读取源中其余的内容,直到源的末尾或者流关闭(就是一个字符一个字符的读,返回unicode值,可以用char强转为字符)
OutputStreamWriter和InputStreamReader
字符输出流和字符输入流, = 字节输出流和字节输入流 + 编码表。 再输入和输出的时候可以指定编码。输入和输出的编码表要一样。不给的话给默认的。字符输出流与字节输出流不一样的是字符流需要调用flush()刷新,字符输出流的close方法默认刷新一次。close后对象不可再使用,flush()后对象可以再用。字符输入流和字节输出流的区别是 字节输入用字节和byte数组,字符输入流用的是字符和字符数组。
FileReader和FileWriter类:InputStreamReader和OutputStreamWriter的子类,为了方便写入输出字符的便捷类,此类的构造方法默认字符编码和默认自己缓冲区大小都是可接受的。
计算机是如果识别什么时候把字节转换为一个中文呢?
在计算机中,中文的存储分为两个字节,第一个字节肯定是负的,第二个字节无所谓,所以两个一拼。
带缓存的输入/输出流(由于上面需要自定义字节数组,所以java提供了这个自缓冲区的字节类)
缓存可以说是I/O的一种性能优化,缓存流为I/O流增加了内存缓存区,有了缓存区,使得流在执行skip()、mark()、reset()成为可能。
bufferedInputStream和bufferedOutStream 类
bufferedInputStream类可以对任意的InputStream类进行带缓存区的包装以达到性能的优化,bufferedInputStream有两个构造函数,bufferedInputStream(InputStream in)和bufferedInputStream(InputStream in, int size)第一种形式是创建了一个带有32个字节的缓存流,第二种是指定大小的缓存流,
bufferedOutStream 输出信息和向OutputStream输入信息完全一样,只不过bufferedOutStream 有一个flush方法,它用来将缓存区的数据强制输出完,bufferedOutStream 有两个构造函数,bufferedOutStream (OutputStream out)和bufferedOutStream (OutputStream out, int size)第一种创建一个默认个字节的缓存区,第二种为制定大小的缓存区,每一次的write其实是将内容写入byte[] 当buffer容量达到上线时,会真正出发写入。flush方法就是用于即使该缓存区没有满的情况下,也强制将缓存区的内容强制写入外设,习惯上称这个过程为刷新,flush只对使用缓存区的OutputStream类的子类有效,flush方法是有Flushable接口实现得来的。当调用close()方法时,系统在关闭流前,也会将缓冲区中的信息刷新到磁盘中。两种缓冲流的构造方法都需要一个对象的原因是因为两个类都起到缓冲作用,而实际操作文件还是实质上的类,它的作用就相当于人拿水杯去接水环节中的水杯。
bufferedReader和bufferedWriter类 :他们分别继承了 Reader和writer ,是字符输入和输出流的缓冲流,
bufferedReader 的几个方法: read()读取单个字符 readline()读取一行文本,并返回字符串,若无数据返回null
如果不指定buffer大小,则readLine()使用的buffer有8192个字符,在达到buffer大小之前,只有遇到"/r"、"/n"、"/r/n"才会返回,否则一直阻塞。readLine方法在遇到终止符、数据流发生异常或者另一端被close()掉时,才会返回null值。
write(string s, int off, int leng)写入字符串的某一部分。 flush()刷新该流的缓存。newline()根据系统写入一个换行号符。再使用bufferedWriter的write方法时,数据并没有立刻写入输出流中,而是先进入缓存区中,如果想立刻将缓存区中的数据写入输出流中,一定要用flush方法,
数据输入输出流:DataInputStream类与DataOutputStream :当读取一个数据时,不必再关心这个数值应当是什么字节,可以读写基本数据类型的数据。
DataInputStream(InputStream in)使用指定的基础InputStream 创建一个对象。读的顺序和格式要与写的顺序和格式一样。
DataOutputStream (OutputStream out)创建一个新的数据输出流,将数据写入指定基础输出流。有三个方法可以使用(1)writeByetes(string s)将字符串中每一个字符的低字节内容写入目标设备中,(2) writeChars将字符串中每一个字符的两个字节的内容都写到目标设备中(string s)(3) writeUTF(string s)将字符串按照UTF编码后的字节长度写入目标设备,然后才是每个字节的UTF编码,
DataInputStream提供了一个readUTF方法返回一个字符串。这个方法读取的是用writeUTF(string s)写入的字符串,其他两种不行。
内存操作流:用来处理临时信息,程序运行完毕从内存清掉,分为三种:操作字节数组(ByteArrayOutputStream/ByteArrayInputStream),操作字符数组(CharArrayReader/CharArrayWriter),操作字符串(StringReader/StringWriter)。3种类操作类似。
ByteArrayOutputStream:该类实现了将数据写入字节数组的输出流。 当数据写入缓冲区时,缓冲区会自动增长。 数据可以使用toByteArray()
和toString()
。关闭ByteArrayOutputStream没有任何效果。 该流中的方法可以在流关闭后调用,而不生成IOException 。
ByteArrayInputStream:ByteArrayInputStream
包含一个内部缓冲区,其中包含可以从流中读取的字节。 内部计数器跟踪read
方法要提供的下一个字节。关闭ByteArrayInputStream没有任何效果。 在关闭流之后,可以调用此类中的方法,没有IOException 。ByteArrayInputStream的构造函数的参数为一个byte数组,这个byte数组需要用ByteArrayOutputStream的toByteArray方法获得。
打印流:字节打印流(PrintStream)和字符打印流(PrintWriter),特点:1.只有写数据的没有读取数据的(只能操作目的地不能操作数据源)。2.可以操作任意类型的数据。3.如果启动了自动刷新,则能够自动刷新,构造函数要传自动刷新参数并且调用带ln的方法。4.该流是可以操作文本文件的,
标准输入输出流:System- in/out.。out实质就是PrintStream对象,in是InputStream对象。可以通过字符缓冲流包装标注输入流,可以实现键盘输入。
RandomAccessFile:随机访问流,直接继承自object,它融合了输入和输出功能,支持对随机访问文件的读取和写入。构造方法的参数mode可以决定打开模式,如"rw" 既可以读也可以写。getFilePointer
方法可以获取文件指针,设置指针位置用seek方法,这个指针的位置实际上就是占用字节数,在读取UTF时有些不同,所以字节数(索引对不上)。可以通过设置指针位置来选择读取的位置。
SequenceInputStream:合并流:表示其他输入流的逻辑串联,用于读取两个或多个文件的内容复制到一个文件中。创建对象后跟别的字节输入类一样的读,写的话用字节输出流照常写。构造方法的参数为指定两个InputStream参数或者是用InputStream的Vector数组对象通过elements方法返回的Enumeration对象。
ObjectOutputStream:序列化流:将Java对象写入文件,或网络中传输, 对象--到--流数据。ObjectInputStream:反序列化流:把文件中的流对象数据或者网络中的流对象数据还原成对象,流数据--到--对象。写入的对象需要实现Serializable序列化接口,此接口没有可实现,被称为标记接口。两者都要close。如果写入文件后,改动对象内容,会出错,因为对象类继承Serializable的时候它本身也有一个标记值,改动类的内容会改动这个标记值,所以再读取会报错,解决方法就是点类名上的黄色警告线生成一个固定的serialID, 这时类里自动生成一个serialVersionUID字段,第二种解决办法,把你需要改变的字段前加上关键字“transient”表示不想被序列化,这样改变这个字段不会影响类的serialID值。
Properties:表示了一个持久的属性集,可以与IO流相结合使用,Properties可以保存在流中或从流中加载, 每个键和对应的值都为字符串,是hashtable的子类,说明键值不能是null值。并且该类没有泛型。load(Reader obj)和store(writer wri, String x)方法分别是把文件中的数据读取到Properties中和把Properties中的数据存储到文件中.这个文件必须是键值对形式“key” = “value”。
ZIP压缩输入/输出流 :java.util.zip包里的ZipOutputStream类与ZipInputputStream类,如果要从zip压缩管理文件内读取某个文件,要先找到对应该文件的“目录进入点”(从它可知该文件在ZIP文件内的位置)才能读取这个文件内容。如果想要将文件写入zip文件内,必须先写入对应于该文件的“目录进入点”并且把要写入文件内容的位置移到此进入点所指的位置,然后在写入文件内容。java实现了I/O数据流与网络数据流的单一接口,因此数据的压缩、网络传输和解压缩实现比较容易。ZIPentry类产生的对象用来代表一个ZIP压缩文件内的进入点(entry)。zipInputStream类用来读取ZIP压缩格式文件,所支持的包括已压缩及未压缩的进入点(entry)。zipOutputStream类用来写出ZIP压缩格式文件,而且所支持的包括已压缩及未压缩的进入点(entry)。
压缩文件:利用zipoutputStream类对象,可将文件压缩为.zip文件。ZipOutputStream类的构造函数 ZipOutputStream(outputstream out)
每一个ZIP文件中可能包含多个文件,使用ZipOutputStream类将文件写入目标ZIP文件时,必须先使用ZipOutputStream对象
的putNextEntry()方法,写入个别文件的entry,将流内目前指到的位置移到该entry所指的开头位置,
解压缩ZIP
zipInputStream 类可读取ZIP压缩格式的文件,包括对已压缩和未压缩条目的支持(entry)。构造函数:zipInputStream (InputStream in)
使用zipInputStream来解压文件,必须先使用zipInputStream类的getnextentry()方法来取得其内的第一个zipentry
package io;
import javax.xml.crypto.Data;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
public class IOSever {
public static void main(String [] args) throws IOException {
int port = 8080;
ServerSocket server = null;
try
{
server = new ServerSocket(port);
System.out.println("The time server is start in port :" + port);
Socket socket = null;
while(true)
{
socket = server.accept();
new Thread(new TimeServerHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
finally {
if (server != null)
{
System.out.println("The time server close");
server.close();
server = null;
}
}
}
static class TimeServerHandler implements Runnable
{
Socket client = null;
public TimeServerHandler(Socket socket) {
this.client = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
//BufferedWriter out = null;
//BufferReader的read方法和readLine方法在任何情况下都是阻塞的
//如果不指定buffer大小,则readLine()使用的buffer有8192个字符,在达到buffer大小之前,只有遇到"/r"、"/n"、"/r/n"才会返回,
//否则一直阻塞。readLine方法在遇到终止符、数据流发生异常或者另一端被close()掉时,才会返回null值。
//所以用printWriter
try{
in = new BufferedReader(new InputStreamReader(this.client.getInputStream()));
out = new PrintWriter(this.client.getOutputStream(), true);
//out = new BufferedWriter(new OutputStreamWriter(this.client.getOutputStream()));
String currentTime = null;
String body = null;
while(true)
{
body = in.readLine();
if(body == null)
break;
System.out.println("The time server receive order :" + body);
currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
out.println(currentTime);
//out.write(currentTime);
}
} catch (IOException e) {
e.printStackTrace();
}
finally {
if(in != null)
{
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
if(out != null);
{
out.close();
out = null;
}
if(this.client != null){
try {
this.client.close();
} catch (IOException e) {
e.printStackTrace();
}
this.client = null;
}
}
}
}
}
}
package io;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class IOClient {
public static void main(String [] args) throws IOException {
int port = 8080;
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try{
socket = new Socket("127.0.0.1", port);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
out.println("QUERY TIME ORDER");
System.out.println("Send order 2 server succeed");
String res = in.readLine();
System.out.println("now is : " + res);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
finally {
if(in != null)
{
in.close();
in = null;
}
if(socket != null)
{
try{
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
socket = null;
}
}
}
}
一、同步阻塞I/O(BIO):
同步阻塞I/O,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务端资源要求比较高,并发局限于应用中,在jdk1.4以前是唯一的io现在,但程序直观简单易理解
二、同步非阻塞I/O(NIO):
同步非阻塞I/O,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,jdk1,4开始支持
三、异步非阻塞I/O(AIO):
异步非阻塞I/O,服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由操作系统先完成了再通知服务器用其启动线程进行处理。AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,jdk1.7开始支持。
ASCII编码表:由现实世界的字符和对应的数值组成的表,最高位为符号位,其余为数值位,a = 97, A = 65, 0 = 48,
ISO-8859-1:拉丁码表,8位表示一个数据,
GB2312:简体中文,
GBK:中国的中文编码表升级,融合和了更多的中文文字符号,
GB18030:GBK的取代版本,
BIG-5码:通行于台湾,香港地区的繁体编码方案,俗称“大五码”。
Unicode:国际标准吗,融合了多种文字,所有文字都用两个字节来表示,java使用的就是unicode。
String类的构造方法和getbytes的方法为解码和编码,可以指定编码表("utf-8" "GBK等")。只要用相同的编码表九不会出问题, 不同的话会出问题。
1. ASCII码
我们知道,在计算机内部,所有的信息最终都表示为一个二进制的字符串。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从0000000到11111111。
上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码,一直沿用至今。
ASCII码一共规定了128个字符的编码,比如空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。
2、非ASCII编码
英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。
但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0—127表示的符号是一样的,不一样的只是128—255的这一段。
至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示256x256=65536个符号。
中文编码的问题需要专文讨论,这篇笔记不涉及。这里只指出,虽然都是用多个字节表示一个符号,但是GB类的汉字编码与后文的Unicode和UTF-8是毫无关系的。
3.Unicode
正如上一节所说,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。
可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是Unicode,就像它的名字都表示的,这是一种所有符号的编码。
Unicode当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字“严”。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表。
4. Unicode的问题
需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
比如,汉字“严”的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。
这里就有两个严重的问题,第一个问题是,如何才能区别unicode和ascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
它们造成的结果是:1)出现了unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示unicode。2)unicode在很长一段时间内无法推广,直到互联网的出现。
5.UTF-8
互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。
UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
UTF-8的编码规则很简单,只有二条:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
下表总结了编码规则,字母x表示可用编码的位。
Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
下面,还是以汉字“严”为例,演示如何实现UTF-8编码。
已知“严”的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“11100100 10111000 10100101”,转换成十六进制就是E4B8A5。
6. Unicode与UTF-8之间的转换
通过上一节的例子,可以看到“严”的Unicode码是4E25,UTF-8编码是E4B8A5,两者是不一样的。它们之间的转换可以通过程序实现。
在Windows平台下,有一个最简单的转化方法,就是使用内置的记事本小程序Notepad.exe。打开文件后,点击“文件”菜单中的“另存为”命令,会跳出一个对话框,在最底部有一个“编码”的下拉条。
里面有四个选项:ANSI,Unicode,Unicode big endian 和 UTF-8。
1)ANSI是默认的编码方式。对于英文文件是ASCII编码,对于简体中文文件是GB2312编码(只针对Windows简体中文版,如果是繁体中文版会采用Big5码)。
2)Unicode编码指的是UCS-2编码方式,即直接用两个字节存入字符的Unicode码。这个选项用的little endian格式。
3)Unicode big endian编码与上一个选项相对应。我在下一节会解释little endian和big endian的涵义。
4)UTF-8编码,也就是上一节谈到的编码方法。
选择完”编码方式“后,点击”保存“按钮,文件的编码方式就立刻转换好了。
7. Little endian和Big endian
上一节已经提到,Unicode码可以采用UCS-2格式直接存储。以汉字”严“为例,Unicode码是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,就是Big endian方式;25在前,4E在后,就是Little endian方式。
这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。
因此,第一个字节在前,就是”大头方式“(Big endian),第二个字节在前就是”小头方式“(Little endian)。
那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?
Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格“(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。
如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。
8. 实例
下面,举一个实例。
打开”记事本“程序Notepad.exe,新建一个文本文件,内容就是一个”严“字,依次采用ANSI,Unicode,Unicode big endian 和 UTF-8编码方式保存。
然后,用文本编辑软件UltraEdit中的”十六进制功能“,观察该文件的内部编码方式。
1)ANSI:文件的编码就是两个字节“D1 CF”,这正是“严”的GB2312编码,这也暗示GB2312是采用大头方式存储的。
2)Unicode:编码是四个字节“FF FE 25 4E”,其中“FF FE”表明是小头方式存储,真正的编码是4E25。
3)Unicode big endian:编码是四个字节“FE FF 4E 25”,其中“FE FF”表明是大头方式存储。
4)UTF-8:编码是六个字节“EF BB BF E4 B8 A5”,前三个字节“EF BB BF”表示这是UTF-8编码,后三个“E4B8A5”就是“严”的具体编码,它的存储顺序与编码顺序是一致的。
Unicode 可以使用的编码有三种,分别是:UTF-8、UTF-16、UTF-32 都是 Unicode 的一种实现。
UFT-8:一种变长的编码方案,使用 1~4个字节来存储;由于 UTF-8 的处理单元为一个字节(也就是一次处理一个字节),所以处理器在处理的时候就不需要考虑这一个字节的存储是在高位还是在低位,直接拿到这个字节进行处理就行了,因为大小端是针对大于一个字节的数的存储问题而言的。
UFT-32:一种固定长度的编码方案,不管字符编号大小,始终使用 4 个字节来存储;需要标记字节的顺序,UTF-32BE 和 UTF-32LE,分别对应大端和小端
UTF-16:介于 UTF-8 和 UTF-32 之间,使用 2 个或者 4 个字节来存储,长度既固定又可变
只有 UTF-8 兼容 ASCII,UTF-32 和 UTF-16 都不兼容 ASCII,因为它们没有单字节编码。需要标记字节的顺序,UTF-16BE 表示大端,UTF-16LE 表示小端。
java中默认是大端
int x = 0x01020304;
ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[4]);
byteBuffer.asIntBuffer().put(x);
String before = Arrays.toString(byteBuffer.array());
System.out.println("默认字节序:" + byteBuffer.order().toString() + "," + "内存数据:" + before);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
byteBuffer.asIntBuffer().put(x);
String after = Arrays.toString(byteBuffer.array());
System.out.println("小端字节序:" + byteBuffer.order().toString() + "," + "内存数据:" + after);
在String类和继承抽象类AbstractStringBuilder的StringBuilder和StringBuffer类中,length()和codePointCount()方法都是计算字符串长度。
测试运行的长度值是相等的,那为什么要设定两个方法呢?
经过研究发现:对于普通字符串,这两种方法得到的值是一样的,但对于UniCode编码来说,还是有一点区别。
实际情况是length()方法返回的是使用的是UTF-16编码的字符代码单元数量,不一定是实际上我们认为的字符个数。同理codePointCount()方法返回的是Unicode代码点个数,是实际上的字符个数。
因为常用的uniCode字符使用一个代码单元就可以表示,但有些辅助字符需要一对代码单元表示。length()方法计算的是代码单元的数量,codePointCount()方法计算的是代码点数。
比如整数集合的数学符号”Z”(没办法打出来),它的代码点是U+1D56B,但它的代理单元是U+D835和U+DD6B,如果令字符串str = “/u1D56B”,机器识别的不是”Z”,而是一个代码点”/u1D56“和字符”B“,所以会得到它的代码点数是2,代码单元数也是2。
但如果令字符str = “/uD835/uDD6B”,那么机器会识别它是2个代码单元代理的1个代码点”Z“,故而,length的结果是代码单元数量2,而codePointCount()的结果是代码点数量1.
但平常我们使用时,这两种求字符串长度的方法还是通用的,不用加以区别。不过建议多用codePointCount()方法。
Java中,char[]、String、StringBuilder和StringBuffer类中采用了UTF-16编码,使用U+0000~U+FFFF来表示一个基本字符(BMP字符),但是位于U+D800到U+DBFF和U+DC00到U+DFFF的char被视为无定义字符。大多数的常用Unicode字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。即:基本字符用一个char表示,辅助字符使用一对char表示。
Java使用代码点(Unicode code pointer)这个概念来表示范围在U+0000与U+10FFFF之间的字符值(int型),代码单元(Unicode code unit)表示作为UTF-16编码的代码单元的 16位char值(char型)。也就是说,可能存在一个字符,它的代码点数量是1,而代码单元数量是2。所以,代码单元的数量并不一定是字符的数量。其中U+0000到U+FFFF为基本字符,U+10000到U+10FFFF为增补字符。