is
zhou

javaWeb中的编码解码

zhouchong阅读(98)评论(0)

对于我们从事java开发的人而言,其实最容易也是产生乱码最多的地方就是web部分。首先我们来看在javaWeb中有哪些地方存在编码转换操作。

编码&解码

通过下图我们可以了解在javaWeb中有哪些地方有转码:

201501060001

用户想服务器发送一个HTTP请求,需要编码的地方有url、cookie、parameter,经过编码后服务器接受HTTP请求,解析HTTP请求,然后对url、cookie、parameter进行解码。在服务器进行业务逻辑处理过程中可能需要读取数据库、本地文件或者网络中的其他文件等等,这些过程都需要进行编码解码。当处理完成后,服务器将数据进行编码后发送给客户端,浏览器经过解码后显示给用户。在这个整个过程中涉及的编码解码的地方较多,其中最容易出现乱码的位置就在于服务器与客户端进行交互的过程。

上面整个过程可以概括成这样,页面编码数据传递给服务器,服务器对获得的数据进行解码操作,经过一番业务逻辑处理后将最终结果编码处理后传递给客户端,客户端解码展示给用户。所以下面我就请求对javaweb的编码&解码进行阐述。

请求

客户端想服务器发送请求无非就通过四中情况:

1、URL方式直接访问。

2、页面链接。

3、表单get提交

4、表单post提交

URL方式

对于URL,如果该URL中全部都是英文的那倒是没有什么问题,如果有中文就要涉及到编码了。如何编码?根据什么规则来编码?又如何来解码呢?下面LZ将一一解答!首先看URL的组成部分:

201501060002

在这URL中浏览器将会对path和parameter进行编码操作。为了更好地解释编码过程,使用如下URL

http://127.0.0.1:8080/perbank/我是cm?name=我是cm

将以上地址输入到浏览器URL输入框中,通过查看http 报文头信息我们可以看到浏览器是如何进行编码的。下面是IE、Firefox、Chrome三个浏览器的编码情况:

201501080001

201501080002

201501080003

可以看到各大浏览器对“我是”的编码情况如下:

path部分

Query String

Firefox

E6 88 91 E6 98 AF

E6 88 91 E6 98 AF

Chrome

E6 88 91 E6 98 AF

E6 88 91 E6 98 AF

IE

E6 88 91 E6 98 AF

CE D2 CA C7

查阅上篇博客的编码可知对于path部分Firefox、chrome、IE都是采用UTF-8编码格式,对于Query String部分Firefox、chrome采用UTF-8,IE采用GBK。至于为什么会加上%,这是因为URL的编码规范规定浏览器将ASCII字符非 ASCII 字符按照某种编码格式编码成 16 进制数字然后将每个 16 进制表示的字节前加上“%”。

当然对于不同的浏览器,相同浏览器不同版本,不同的操作系统等环境都会导致编码结果不同,上表某一种情况,对于URL编码规则下任何结论都是过早的。由于各大浏览器、各个操作系统对URL的URI、QueryString编码都可能存在不同,这样对服务器的解码势必会造成很大的困扰,下面我们将已tomcat,看tomcat是如何对URL进行解码操作的。

解析请求的 URL 是在 org.apache.coyote.HTTP11.InternalInputBuffer 的 parseRequestLine 方法中,这个方法把传过来的 URL 的 byte[] 设置到 org.apache.coyote.Request 的相应的属性中。这里的 URL 仍然是 byte 格式,转成 char 是在 org.apache.catalina.connector.CoyoteAdapter 的 convertURI 方法中完成的:

protected void convertURI(MessageBytes uri, Request request)   
             throws Exception {   
                    ByteChunk bc = uri.getByteChunk();   
                    int length = bc.getLength();   
                    CharChunk cc = uri.getCharChunk();   
                    cc.allocate(length, -1);   
                    String enc = connector.getURIEncoding();     //获取URI解码集  
                    if (enc != null) {   
                        B2CConverter conv = request.getURIConverter();   
                        try {   
                            if (conv == null) {   
                                conv = new B2CConverter(enc);   
                                request.setURIConverter(conv);   
                            }   
                        } catch (IOException e) {...}   
                        if (conv != null) {   
                            try {   
                                conv.convert(bc, cc, cc.getBuffer().length - cc.getEnd());   
                                uri.setChars(cc.getBuffer(), cc.getStart(), cc.getLength());   
                                return;   
                            } catch (IOException e) {...}   
                        }   
                    }   
                    // Default encoding: fast conversion   
                    byte[] bbuf = bc.getBuffer();   
                    char[] cbuf = cc.getBuffer();   
                    int start = bc.getStart();   
                    for (int i = 0; i < length; i++) {   
                        cbuf[i] = (char) (bbuf[i + start] & 0xff);   
                    }   
                    uri.setChars(cbuf, 0, length);   
    }  

从上面的代码可知,对URI的解码操作是首先获取Connector的解码集,该配置在server.xml中

<Connector URIEncoding="utf-8"  />  

如果没有定义则会采用默认编码ISO-8859-1来解析。

对于Query String部分,我们知道无论我们是通过get方式还是POST方式提交,所有的参数都是保存在Parameters,然后我们通过request.getParameter,解码工作就是在第一次调用getParameter方法时进行的。在getParameter方法内部它调用org.apache.catalina.connector.Request 的 parseParameters 方法,这个方法将会对传递的参数进行解码。下面代码只是parseParameters方法的一部分:

//获取编码  
   String enc = getCharacterEncoding();  
  //获取ContentType 中定义的 Charset  
  boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();  
  if (enc != null) {    //如果设置编码不为空,则设置编码为enc  
      parameters.setEncoding(enc);  
      if (useBodyEncodingForURI) {   //如果设置了Chartset,则设置queryString的解码为ChartSet  
          parameters.setQueryStringEncoding(enc);      
      }  
  } else {     //设置默认解码方式  
      parameters.setEncoding(org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);  
      if (useBodyEncodingForURI) {  
          parameters.setQueryStringEncoding(org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);  
      }  
  }  

从上面代码可以看出对query String的解码格式要么采用设置的ChartSet要么采用默认的解码格式ISO-8859-1。注意这个设置的ChartSet是在 http Header中定义的ContentType,同时如果我们需要改指定属性生效,还需要进行如下配置:

<Connector URIEncoding="UTF-8" useBodyEncodingForURI="true"/>  

上面部分详细介绍了URL方式请求的编码解码过程。其实对于我们而言,我们更多的方式是通过表单的形式来提交。

表单GET

我们知道通过URL方式提交数据是很容易产生乱码问题的,所以我们更加倾向于通过表单形式。当用户点击submit提交表单时,浏览器会更加设定的编码来编码数据传递给服务器。通过GET方式提交的数据都是拼接在URL后面(可以当做query String??)来提交的,所以tomcat服务器在进行解码过程中URIEncoding就起到作用了。tomcat服务器会根据设置的URIEncoding来进行解码,如果没有设置则会使用默认的ISO-8859-1来解码。假如我们在页面将编码设置为UTF-8,而URIEncoding设置的不是或者没有设置,那么服务器进行解码时就会产生乱码。这个时候我们一般可以通过new String(request.getParameter(“name”).getBytes(“iso-8859-1″),”utf-8”) 的形式来获取正确数据。

表单POST

对于POST方式,它采用的编码也是由页面来决定的即contentType。当我通过点击页面的submit按钮来提交表单时,浏览器首先会根据ontentType的charset编码格式来对POST表单的参数进行编码然后提交给服务器,在服务器端同样也是用contentType中设置的字符集来进行解码(这里与get方式就不同了),这就是通过POST表单提交的参数一般而言都不会出现乱码问题。当然这个字符集编码我们是可以自己设定的:request.setCharacterEncoding(charset) 。

字符编码详解:ASCII + GB**

zhouchong阅读(102)评论(0)

一、基础知识

在了解各种字符集之前我们需要了解一些最基础的知识,如:编码、字符、字符集、字符编码基础知识。

编码

计算机中存储的信息都是用二进制表示的,我们在屏幕上所看到文字、图片等都是通过二进制转换的结果。编码是信息从一种形式或格式转换为另一种形式的过程,通俗点讲就是就是将我们看到的文字、图片等信息按照某种规则存储在计算机中,例如‘c’在计算机中怎么表达,‘陈’在计算机中怎么表达,这个过程就称之为编码。解码是编码的逆过程,它是将存储在计算机的二进制转换为我们可以看到的文字、图片等信息,它体现的是视觉上的刺激。

n位二进制数可以组合成2的n次方个不同的信息,给每个信息规定一个具体码组,这种过程也叫编码。

在编码和解码中,他们就如加密、解密一般,他们一定会遵循某个规则,即y  = f(x),那么x = f(y);否则在解密过程就会导致‘a’解析成‘b’或者乱码。

字符

字符是可使用多种不同字符方案或代码页来表示的抽象实体,它是一个单位的字形、类字形单位或符号的基本信息,也是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。

字符是指计算机中使用的字母、数字、字和符号,包括:1、2、3、A、B、C、~!·#¥%……—*()——+等等。在 ASCII 编码中,一个英文字母字符存储需要1个字节。在 GB 2312 编码或 GBK 编码中,一个汉字字符存储需要2个字节。在UTF-8编码中,一个英文字母字符存储需要1个字节,一个汉字字符储存需要3到4个字节。在UTF-16编码中,一个英文字母字符或一个汉字字符存储都需要2个字节(Unicode扩展区的一些汉字存储需要4个字节)。在UTF-32编码中,世界上任何字符的存储都需要4个字节。

字符集

字符是各种文字和符号的总称,而字符集则是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同。而计算机要准确的处理各种字符集文字,需要进行字符编码,以便计算机能够识别和存储各种文字。

常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等。

字符编码

计算机中的信息包括数据信息和控制信息,然而不管是那种信息,他们都是以二进制编码的方式存入计算机中,但是他们是怎么展示在屏幕上的呢?同时在展现过程中如何才能保证他们不出错?这个时候字符编码就起到了重要作用!字符编码是一套规则,一套建立在符合集合与数字系统之间的对应关系之上的规则,它是信息处理的基本技术。

使用字符编码这套规则能够对自然语言的字符的一个集合(如字母表或音节表),与其他东西的一个集合(如号码或电脉冲)进行配对。

二、ASCII

2.1、标准ASCII码

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统。它主要用于显示现代英语和其他西欧英语,它是现今最通用的单字节编码系统。

ASCII使用7位或者8位来表示128或者256种可能的字符。标准的ASCII码则是使用7位二进制数来表示所有的大小写字母、数字、标点符合和一些控制字符,其中:

0~31、127(共33个)是控制字符或者通信专用字符,如控制符:LF(换行)、CR(回车)、DEL(删除)等;通信专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等。ASCII值为8、9、10、13分别表示退格、制表、换号、回车字符。

32~126(共95个)字符,32为空格、48~57为阿拉伯数字、65~90为大写字母、97~122为小写字母,其余为一些标点符号和运算符号!

前面提过标准的ASCII码是使用七位来表示字符的,而最高位(b7)则是用作奇偶校验的。所谓奇偶校验,是指在代码传送过程中用来检验是否出现错误的一种方法,一般分奇校验和偶校验两种。奇校验规定:正确的代码一个字节中1的个数必须是奇数,若非奇数,则在最高位b7添1;偶校验规定:正确的代码一个字节中1的个数必须是偶数,若非偶数,则在最高位b7添1。 (参考百度百科)

下面是ASCII字符对照表,更多详情请关注:》》 ASCII码表 《《

2014112400001

2.2、扩展ASCII码

标准的ASCII是用七位来表示的,那么它的缺陷就非常明显了:只能显示26个基本拉丁字母、阿拉伯数目字和英式标点符号,基本上只能应用于现代美国英语,对于其他国家,128个字符肯定不够。于是,这些欧洲国家决定利用字节中闲置的最高位编入新的符号,这样一来,可以表达的字符数最多就为256个,但是随着产生的问题也就来了:不同的国家有不同的字母,可能同一个编码在不同的国家所表示的字符不同。但是不管怎么样,在这些编码中0~127所表示的字符肯定是一样的,不一样的也只是128~255这一段。

8位的ASCII在欧洲国家表现的不尽人意,那么在其他国家就更加不用说了,我们拥有五千年历史文化的中华名族所包含的汉字多大10多万,不知道是多少个256。所以一个字节8位表示的256个字符肯定是不够的,那么两个字节呢?可能够了吧!我们常见的汉字就是用两个字节表示的,如GB2312。

三、GB**

对于欧美国家来说,ASCII能够很好的满足用户的需求,但是当我们中华名族使用计算机时,ASCII明显就不满足需求了,有5000年历史文化的我们,拥有的汉字达到将近10万,所以为了显示中文,我们必须设计一套编码规则用于将汉字转换为计算机可以接受的数字系统的数。显示中文的常用字符编码有:GB2312、GBK、GB18030。

GB2312

GB2312,中国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·基本集》,由中国国家标准总局发布,1981年5月1日实施。

GB2312编码的规则:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,还把数学符号、罗马希腊的 字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的”全角”字符,而原来在127 号以下的那些就叫”半角”字符了。

在GB2312中,GB2312共收录6763个汉字,其中一级汉字3755个,二级汉字3008个,还收录了拉丁字母、希腊字母、日文等682个全角字符。由于GB2312的出现,它基本上解决了我们日常的需要,它所收录的汉子已经覆盖了中国大陆99.75%的使用平率。但是我国文化博大精深,对于人名、古汉语等方面出现的罕用字,GB2312还是不能处理,于是后面的GBK和GB18030汉字字符集出现了。

GB2312字符集库非常庞大,详情:GB2312简体中文编码表

GBK

GBK,全称《汉字内码扩展规范》,由中华人民共和国全国信息技术标准化技术委员会1995年12月1日制订,也是汉字编码的标准之一。

GBK是GB2312的扩展,他向下与GB2312兼容,,向上支持 ISO 10646.1 国际标准,是前者向后者过渡过程中的一个承上启下的标准。同时它是使用双字节编码方案,其编码范围从8140至FEFE(剔除xx7F),首字节在 81-FE 之间,尾字节在 40-FE 之间,共23940个码位,共收录了21003个汉字。

GB18030

GB18030,国家标准GB18030《信息技术 中文编码字符集》,是我国计算机系统必须遵循的基础性标准之一。它有两个版本:GB18030-2000、GB18030-2005。其中GB18030-2000仅规定了常用非汉字符号和27533个汉字(包括部首、部件等)的编码,而GB18030-2005是全文强制性标准,市场上销售的产品必须符合,它是GB18030-2000的基础上增加了42711个汉字和多种我国少数民族文字的编码。

GB18030标准采用单字节、双字节和四字节三种方式对字符编码。(码位总体结构见下图)

单字节部分采用GB/T 11383的编码结构与规则,使用0×00至0×7F码位(对应于ASCII码的相应码位)。双字节部分,首字节码位从0×81至0×FE,尾字节码位分别是0×40至0×7E和0×80至0×FE。四字节部分采用GB/T 11383未采用的0×30到0×39作为对双字节编码扩充的后缀,这样扩充的四字节编码,其范围为0×81308130到0×FE39FE39。其中第一、三个字节编码码位均为0×81至0×FE,第二、四个字节编码码位均为0×30至0×39。

关于字符集

zhouchong阅读(97)评论(0)

Java编码中的中文问题是一个老生常谈的问题了,每次遇到中文乱码LZ要么是按照以前的经验修改,要么则是baidu.com来解决问题。阅读许多关于中文乱码的解决办法的博文后,发现对于该问题我们都(更加包括我自己)没有一个清晰明了的认识,于是LZ想通过这系列博文(估计只有几篇)来彻底分析、解决java中文乱码问题,如有错误之处望各位同仁指出!当然,此系列博文并非LZ完全原创,都是在前辈基础上总结,归纳,如果雷同纯属借鉴……

问题起源

对于计算机而言,它仅认识两个0和1,不管是在内存中还是外部存储设备上,我们所看到的文字、图片、视频等等“数据”在计算机中都是已二进制形式存在的。不同字符对应二进制数的规则,就是字符的编码。字符编码的集合称为字符集。

在早期的计算机系统中,使用的字符是非常少的,他们只包括26个英文字母、数字符号和一些常用符号,对于这些字符进行编码,用1个字节就足够了,但是随着计算机的不断发展,为了适应全世界其他各国民族的语言,这些少得可怜的字符编码肯定是不够的。于是人们提出了UNICODE编码,它采用双字节编码,兼容英文字符和其他国家民族的双字节字符编码。

每个国家为了统一编码都会规定该国家/地区计算机信息交换用的字符集编码,为了解决本地字符信息的计算机处理,于是出现了各种本地化版本,引进LANG, Codepage 等概念。现在大部分具有国际化特征的软件核心字符处理都是以 Unicode 为基础的,在软件运行时根据当时的 Locale/Lang/Codepage 设置确定相应的本地字符编码设置,并依此处理本地字符。在处理过程中需要实现 Unicode 和本地字符集的相互转换。

同然,java内部采用的就是Unicode编码,所以在java运行的过程中就必然存在从Unicode编码与相应的计算机操作系统或者浏览器支持的编码格式相互转化的过程,这个转换的过程有一系列的步骤,如果某个步骤出现错误,则输出的文字就会是乱码。

所以产生java乱码的问题就在于JVM与对应的操作系统/浏览器进行编码格式转换时出现了错误。

其实要解决java乱码问题的方法还是比较简单的,但是要究其原因,理解背后的原理还是需要了解

其实解决 JAVA 程序中的汉字编码问题的方法往往很简单,但理解其背后的原因,定位问题,还需要了解现有的汉字编码和编码转换。

常见字符编码

计算机要准确的处理各种字符集文字,需要进行字符编码,以便计算机能够识别和存储各种文字。常见的字符编码主要包括:ASCII编码、GB**编码、Unicode。下面LZ就简单地介绍下!(为什么是简单介绍?因为LZ在网上查找资料想去了解字符编码时,发现这个问题比我想象的复杂太多了,所以LZ需要另起一篇详细介绍,所以各位看客就简单看看吧!!)

1.ASCII编码

ASCII,American Standard Code for Information Interchange,是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是现今最通用的单字节编码系统。

ASCII码使用指定的7位或者8为二进制数字组合表示128或者256种可能的字符。标准的ASCII编码使用的是7(2^7 = 128)位二进制数来表示所有的大小写字母、数字和标点符号已经一些特殊的控制字符,最前面的一位统一规定为0。其中0~31及127(共33个)是控制字符或通信专用字符,32~126(共95个)是字符(32是空格),其中48~57为0到9十个阿拉伯数字,65~90为26个大写英文字母,97~122号为26个小写英文字母,其余为一些标点符号、运算符号等。

2.GBK***编码

ASCII最大的缺点就是显示字符有限,他虽然解决了部分西欧语言的显示问题,但是对更多的其他语言他实在是无能为了。随着计算机技术的发展,使用范围越来越广泛了,ASCII的缺陷越来越明显了,其他国家和地区需要使用计算机,必须要设计一套符合本国/本地区的编码规则。例如为了显示中文,我们就必须要设计一套编码规则用于将汉字转换为计算机可以接受的数字系统的数。

GB2312,用于汉字处理、汉字通信等系统之间的信息交换,通行于中国大陆。它的编码规则是:小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。虽然GB2312收录了这么多汉子,他所覆盖的使用率可以达到99%,但是对于那些不常见的汉字,例如人名、地名、古汉语,它就不能处理了,于是就有下面的GBK、GB 18030的出现。(点击GB2312简体中文编码表查看)。

GB18030,全称:国家标准GB 18030-2005《信息技术 中文编码字符集》,是我国计算机系统必须遵循的基础性标准之一,GB18030有两个版本:GB18030-2000和GB18030-2005。GB18030-2000是GBK的取代版本,它的主要特点是在GBK基础上增加了CJK统一汉字扩充A的汉字。

GB 18030主要有以下特点:

与UTF-8相同,采用多字节编码,每个字可以由1个、2个或4个字节组成。

编码空间庞大,最多可定义161万个字符。

支持中国国内少数民族的文字,不需要动用造字区。

汉字收录范围包含繁体汉字以及日韩汉字

GBK,汉字编码标准之一,全称《汉字内码扩展规范》,它 向下与 GB 2312 编码兼容,向上支持 ISO 10646.1 国际标准,是前者向后者过渡过程中的一个承上启下的标准。它的编码范围如下图:

Unicode编码

正如前面前面所提到的一样,世界存在这么多国家,也存在着多种编码风格,像中文的GB232、GBK、GB18030,这样乱搞一套,虽然在本地运行没有问题,但是一旦出现在网络上,由于互不兼容,访问则会出现乱码。为了解决这个问题,伟大的Unicode编码腾空出世。

Unicode编码的作用就是能够使计算机实现夸平台、跨语言的文本转换和处理。它几乎包含了世界上所有的符号,并且每个符号都是独一无二的。在它的编码世界里,每一个数字代表一个符号,每一个符号代表了一个数字,不存在二义性。

Unicode编码又称统一码、万国码、单一码,它是业界的一种标准,是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。同时Unicode是字符集,它存在很多几种实现方式如:UTF-8、UTF-16.

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码。

java编码转换过程

zhouchong阅读(79)评论(0)

java编码转换过程

我们总是用一个java类文件和用户进行最直接的交互(输入、输出),这些交互内容包含的文字可能会包含中文。无论这些java类是与数据库交互,还是与前端页面交互,他们的生命周期总是这样的:

1、程序员在操作系统上通过编辑器编写程序代码并且以.java的格式保存操作系统中,这些文件我们称之为源文件。

2、通过JDK中的javac.exe编译这些源文件形成.class类。

3、直接运行这些类或者部署在WEB容器中运行,得到输出结果。

这些过程是从宏观上面来观察的,了解这个肯定是不行的,我们需要真正来了解java是如何来编码和被解码的:

第一步:当我们用编辑器编写java源文件,程序文件在保存时会采用操作系统默认的编码格式(一般我们中文的操作系统采用的是GBK编码格式)形成一个.java文件。java源文件是采用操作系统默认支持的file.encoding编码格式保存的。下面代码可以查看系统的file.encoding参数值。

System.out.println(System.getProperty("file.encoding"));  

第二步:当我们使用javac.exe编译我们的java文件时,JDK首先会确认它的编译参数encoding来确定源代码字符集,如果我们不指定该编译参数,JDK首先会获取操作系统默认的file.encoding参数,然后JDK就会把我们编写的java源程序从file.encoding编码格式转化为JAVA内部默认的UNICODE格式放入内存中。

第三步:JDK将上面编译好的且保存在内存中信息写入class文件中,形成.class文件。此时.class文件是Unicode编码的,也就是说我们常见的.class文件中的内容无论是中文字符还是英文字符,他们都已经转换为Unicode编码格式了。

在这一步中对对JSP源文件的处理方式有点儿不同:WEB容器调用JSP编译器,JSP编译器首先会查看JSP文件是否设置了文件编码格式,如果没有设置则JSP编译器会调用调用JDK采用默认的编码方式将JSP文件转化为临时的servlet类,然后再编译为.class文件并保持到临时文件夹中。

第四步:运行编译的类:在这里会存在一下几种情况

1、直接在console上运行。

2、JSP/Servlet类。

3、java类与数据库之间。

这三种情况每种情况的方式都会不同,

1.Console上运行的类

这种情况下,JVM首先会把保存在操作系统中的class文件读入到内存中,这个时候内存中class文件编码格式为Unicode,然后JVM运行它。如果需要用户输入信息,则会采用file.encoding编码格式对用户输入的信息进行编码同时转换为Unicode编码格式保存到内存中。程序运行后,将产生的结果再转化为file.encoding格式返回给操作系统并输出到界面去。整个流程如下:

在上面整个流程中,凡是涉及的编码转换都不能出现错误,否则将会产生乱码。

2.Servlet类

由于JSP文件最终也会转换为servlet文件(只不过存储的位置不同而已),所以这里我们也将JSP文件纳入其中。

当用户请求Servlet时,WEB容器会调用它的JVM来运行Servlet。首先JVM会把servlet的class加载到内存中去,内存中的servlet代码是Unicode编码格式的。然后JVM在内存中运行该Servlet,在运行过程中如果需要接受从客户端传递过来的数据(如表单和URL传递的数据),则WEB容器会接受传入的数据,在接收过程中如果程序设定了传入参数的的编码则采用设定的编码格式,如果没有设置则采用默认的ISO-8859-1编码格式,接收的数据后JVM会将这些数据进行编码格式转换为Unicode并且存入到内存中。运行Servlet后产生输出结果,同时这些输出结果的编码格式仍然为Unicode。紧接着WEB容器会将产生的Unicode编码格式的字符串直接发送置客户端,如果程序指定了输出时的编码格式,则按照指定的编码格式输出到浏览器,否则采用默认的ISO-8859-1编码格式。整个过程流程图如下:

3.数据库部分

我们知道java程序与数据库的连接都是通过JDBC驱动程序来连接的,而JDBC驱动程序默认的是ISO-8859-1编码格式的,也就是说我们通过java程序向数据库传递数据时,JDBC首先会将Unicode编码格式的数据转换为ISO-8859-1的编码格式,然后在存储在数据库中,即在数据库保存数据时,默认格式为ISO-8859-1。

 

java的编码与解码

zhouchong阅读(82)评论(0)

编码&解码

在上篇博客中LZ阐述了三个渠道的编码转换过程,下面LZ将结束java在那些场合需要进行编码和解码操作,并详序中间的过程,进一步掌握java的编码和解码过程。在java中主要有四个场景需要进行编码解码操作:

1:I/O操作

2:内存

3:数据库

4:javaWeb

下面主要介绍前面两种场景,数据库部分只要设置正确编码格式就不会有什么问题,javaWeb场景过多需要了解URL、get、POST的编码,servlet的解码,所以javaWeb场景下节LZ介绍。

I/O操作

在前面LZ就提过乱码问题无非就是转码过程中编码格式的不统一产生的,比如编码时采用UTF-8,解码采用GBK,但最根本的原因是字符到字节或者字节到字符的转换出问题了,而这中情况的转换最主要的场景就是I/O操作的时候。当然I/O操作主要包括网络I/O(也就是javaWeb)和磁盘I/O。网络I/O下节介绍。

首先我们先看I/O的编码操作。

InputStream为字节输入流的所有类的超类,Reader为读取字符流的抽象类。java读取文件的方式分为按字节流读取和按字符流读取,其中InputStream、Reader是这两种读取方式的超类。

按字节

我们一般都是使用InputStream.read()方法在数据流中读取字节(read()每次都只读取一个字节,效率非常慢,我们一般都是使用read(byte[])),然后保存在一个byte[]数组中,最后转换为String。在我们读取文件时,读取字节的编码取决于文件所使用的编码格式,而在转换为String过程中也会涉及到编码的问题,如果两者之间的编码格式不同可能会出现问题。例如存在一个问题test.txt编码格式为UTF-8,那么通过字节流读取文件时所获得的数据流编码格式就是UTF-8,而我们在转化成String过程中如果不指定编码格式,则默认使用系统编码格式(GBK)来解码操作,由于两者编码格式不一致,那么在构造String过程肯定会产生乱码,如下:

File file = new File("C:\\test.txt");  
InputStream input = new FileInputStream(file);  
StringBuffer buffer = new StringBuffer();  
byte[] bytes = new byte[1024];  
for(int n ; (n = input.read(bytes))!=-1 ; ){  
    buffer.append(new String(bytes,0,n));  
}  
System.out.println(buffer);  

输出结果:锘挎垜鏄?cm

test.txt中的内容为:我是 cm。

要想不出现乱码,在构造String过程中指定编码格式,使得编码解码时两者编码格式保持一致即可:

buffer.append(new String(bytes,0,n,"UTF-8"));  

按字符

其实字符流可以看做是一种包装流,它的底层还是采用字节流来读取字节,然后它使用指定的编码方式将读取字节解码为字符。在java中Reader是读取字符流的超类。所以从底层上来看按字节读取文件和按字符读取没什么区别。在读取的时候字符读取每次是读取留个字节,字节流每次读取一个字节。

字节&字符转换

字节转换为字符一定少不了InputStreamReader。API解释如下:InputStreamReader 是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。 每次调用 InputStreamReader 中的一个 read() 方法都会导致从底层输入流读取一个或多个字节。要启用从字节到字符的有效转换,可以提前从底层流读取更多的字节,使其超过满足当前读取操作所需的字节。API解释非常清楚,InputStreamReader在底层读取文件时仍然采用字节读取,读取字节后它需要根据一个指定的编码格式来解析为字符,如果没有指定编码格式则采用系统默认编码格式。

String file = "C:\\test.txt";   
         String charset = "UTF-8";   
         // 写字符换转成字节流  
         FileOutputStream outputStream = new FileOutputStream(file);   
         OutputStreamWriter writer = new OutputStreamWriter(outputStream, charset);   
         try {   
            writer.write("我是 cm");   
         } finally {   
            writer.close();   
         }   

         // 读取字节转换成字符  
         FileInputStream inputStream = new FileInputStream(file);   
         InputStreamReader reader = new InputStreamReader(   
         inputStream, charset);   
         StringBuffer buffer = new StringBuffer();   
         char[] buf = new char[64];   
         int count = 0;   
         try {   
            while ((count = reader.read(buf)) != -1) {   
                buffer.append(buf, 0, count);   
            }   
         } finally {   
            reader.close();   
         }  
         System.out.println(buffer);  

 

内存

首先我们看下面这段简单的代码

String s = "我是 cm";   
byte[] bytes = s.getBytes();   
String s1 = new String(bytes,"GBK");   
String s2 = new String(bytes);  

 

在这段代码中我们看到了三处编码转换过程(一次编码,两次解码)。先看String.getTytes():

public byte[] getBytes() {  
        return StringCoding.encode(value, 0, value.length);  
    } 

内部调用StringCoding.encode()方法操作:

static byte[] encode(char[] ca, int off, int len) {  
        String csn = Charset.defaultCharset().name();  
        try {  
            // use charset name encode() variant which provides caching.  
            return encode(csn, ca, off, len);  
        } catch (UnsupportedEncodingException x) {  
            warnUnsupportedCharset(csn);  
        }  
        try {  
            return encode("ISO-8859-1", ca, off, len);  
        } catch (UnsupportedEncodingException x) {  
            // If this code is hit during VM initialization, MessageUtils is  
            // the only way we will be able to get any kind of error message.  
            MessageUtils.err("ISO-8859-1 charset not available: "  
                             + x.toString());  
            // If we can not find ISO-8859-1 (a required encoding) then things  
            // are seriously wrong with the installation.  
            System.exit(1);  
            return null;  
        }  
    }  

encode(char[] paramArrayOfChar, int paramInt1, int paramInt2)方法首先调用系统的默认编码格式,如果没有指定编码格式则默认使用ISO-8859-1编码格式进行编码操作,进一步深入如下:

String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;

同样的方法可以看到new String 的构造函数内部是调用StringCoding.decode()方法:

public String(byte bytes[], int offset, int length, Charset charset) {  
        if (charset == null)  
            throw new NullPointerException("charset");  
        checkBounds(bytes, offset, length);  
        this.value =  StringCoding.decode(charset, bytes, offset, length);  
    }  

 

decode方法和encode对编码格式的处理是一样的。

对于以上两种情况我们只需要设置统一的编码格式一般都不会产生乱码问题。

编码&编码格式

首先先看看java编码类图[1]

首先根据指定的chart设置ChartSet类,然后根据ChartSet创建ChartSetEncoder对象,最后再调用 CharsetEncoder.encode 对字符串进行编码,不同的编码类型都会对应到一个类中,实际的编码过程是在这些类中完成的。下面时序图展示详细的编码过程:

通过这编码的类图和时序图可以了解编码的详细过程。下面将通过一段简单的代码对ISO-8859-1、GBK、UTF-8编码

public class Test02 {  
    public static void main(String[] args) throws UnsupportedEncodingException {  
        String string = "我是 cm";  
        Test02.printChart(string.toCharArray());  
        Test02.printChart(string.getBytes("ISO-8859-1"));  
        Test02.printChart(string.getBytes("GBK"));  
        Test02.printChart(string.getBytes("UTF-8"));  
    }  

    /** 
     * char转换为16进制 
     */  
    public static void printChart(char[] chars){  
        for(int i = 0 ; i < chars.length ; i++){  
            System.out.print(Integer.toHexString(chars[i]) + " ");   
        }  
        System.out.println("");  
    }  

    /** 
     * byte转换为16进制 
     */  
    public static void printChart(byte[] bytes){  
        for(int i = 0 ; i < bytes.length ; i++){  
            String hex = Integer.toHexString(bytes[i] & 0xFF);   
             if (hex.length() == 1) {   
               hex = '0' + hex;   
             }   
             System.out.print(hex.toUpperCase() + " ");   
        }  
        System.out.println("");  
    }  
}  
-------------------------outPut:  
6211 662f 20 63 6d   
3F 3F 20 63 6D   
CE D2 CA C7 20 63 6D   
E6 88 91 E6 98 AF 20 63 6D  

通过程序我们可以看到“我是 cm”的结果为:

char[]:6211 662f 20 63 6d

ISO-8859-1:3F 3F 20 63 6D
GBK:CE D2 CA C7 20 63 6D
UTF-8:E6 88 91 E6 98 AF 20 63 6D

图如下:

JSP页面编码过程

zhouchong阅读(86)评论(0)

我们知道JSP页面是需要转换为servlet的,在转换过程中肯定是要进行编码的。在JSP转换为servlet过程中下面一段代码起到至关重要的作用。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="GBK" %> 

在上面代码中有两个地方存在编码:pageEncoding、contentType的charset。其中pageEncoding是jsp文件本身的编码,而contentType的charset是指服务器发送给客户端时的内容编码。

在前面一篇博客中就提到过(Java中文乱码解决之道(四)—–java编码转换过程)jsp在转换为Servlet的过程中是需要经过主要的三次编码转换过程(除去数据库编码转换、页面参数输入编码转换):

第一次:转换为.java文件;

第二次:转换为.class文件;

第三次:业务逻辑处理后输出。

第一阶段

JVM将JSP编译为.jsp文件。在这个过程中pageEncoding就起到作用了,JVM首先会获取pageEncoding的值,如果该值存在则采用它设定的编码来编译,否则则采用file.encoding编码来编译。

第二阶段

JVM将.java文件转换为.class文件。在这个过程就与任何编码的设置都没有关系了,不管JSP采用了什么样的编码格式都将无效。经过这个阶段后.jsp文件就转换成了统一的Unicode格式的.class文件了。

第三阶段

后台经过业务逻辑处理后将产生的结果输出到客户端。在这个过程中contentType的charset就发挥了功效。如果设置了charset则浏览器就会使用指定的编码格式进行解码,否则采用默认的ISO-8859-1编码格式进行解码处理。

流程如如下:


 

重构的基本步骤,如何重构

zhouchong阅读(85)评论(0)

如何重构?

 

第一步:从分解大函数开始

随着业务逻辑越来越复杂,程序员总是就着原有的程序结构不断的添加新的代码。原有的清晰而简单的程序,随着新代码的不断添加,因此,大多数软件企业的遗留系统中,超级大函数就变成了一种通病。因此重构的第一步就是要从分解大函数开始。所以寻求一个系统的切入点,归纳整理大函数的各司逻辑,运行重构”抽取方法”进行函数分解化为多个不同逻辑的函数,从而优化原有的系统结构,提高了阅读的方便性。

第二步:拆分大对象,分解了大函数

滞留的是被分解的上百上千的方法或函数,接下来就是对大对象的分解。利用重构”抽取类”将分解的函数或方法进行整合至对象中,同时保证单一职责原则,一个对象或类完成一项业务逻辑职责。最后进行类的归并。

第三步:提高代码复用率

分解过大函数、拆分大对象,系统重构从无序的乱码变得有序井然,系统的逻辑流程也清楚明白,接下来做的就是对代码的优化,遵循”DRY”原则,同一对象中存在重复逻辑代码,运用”重构方法”抽取为对应的逻辑名函数;不同对象中村子啊重复逻辑代码,运用”抽取类”将重复部分抽取为一个公用类,方便其他类调用。当重复的所在类具有并列关系,运用”抽取父类”将相同代码抽取为共有的父类。

第四步:发现程序可扩展点

系统能应对业务多变的需求变更,提高系统的易变更性,也是重构系统的目的之一。系统应保证一般性原则:预见性可扩展设计尽量不要太多;”两顶帽子”设计模式—一顶是系统重构,一顶是面对新需求实现功能。遵循”OCP原则(开放-关闭原则)”也是可扩展点的前提根本。

第五步:降低程序依赖度

降低系统的依赖度是重构系统的最根本方法。减少功能逻辑之间的联系度,新需求建立独立的功能逻辑,满足了系统功能的耦合度,达到了系统开发低耦合高内聚。

第六步:分层

分层结构是系统开发前期所设计的系统架构,默认的架构是三层架构:数据层、业务逻辑层、应用层。分层的意义在于独立各功能逻辑关系,在面对业务需求变更时,以改变原逻辑最小的代价,提高了系统代码质量。

第七步:领域驱动设计

按照职责原则对系统进行领域划分形成领域模型。原程式结构与现程式结构大相径庭,并不是抛弃原有系统结构,只是通过”小步快跑”一点一点的演变而来,再次保证代码的质量水平和阅读、维护、变更。这就是重构的完整过程。

 

“Effective Java” 可能对 Kotlin 的设计造成了怎样的影响

zhouchong阅读(142)评论(0)

简评:作者从《Effective Java》中选出了几条项目,举例分析 Java 和 Kotlin 写法不同之处,Kotlin 从中得到启发,写法更加简洁。

Java 是一种很好的语言但是有些众所周知的瑕疵,常见的陷阱以及早期(1.0 在 1995 年发布)继承过来的不是很好的元素。一本备受推崇的关于如何写好 Java 代码的书,避免常见的编码失误,处理它的弱点的书就是 Joshua Bloch 的《Effective Java》。它包含 78 个部分,为读者提供有关语言不同方面的宝贵意见。

现代编程语言的创造者有个很大的优势,因为它们可以分析已经创立的语言的弱点,从而改进它们。Jetbrains,创建了几款非常流行的 IDE 的公司,在 2010 年决定创建 Kotlin 编程语言作为自己的开发语言。它的目标是更简洁和更清晰的表达,同时消除Java的一些弱点。他们之前写 IDE 的代码都是用 Java 写的,所以他们需要一种与 Java 高度互操作的语言,并编译成Java 字节码。他们也想要 Java 开发者非常容易上手 Kotlin。使用 Kotlin,Jetbrains 想要构建更好的 Java。

 

1. 使用 Kotlin 的默认值,不再需要 builder 模式

当你在 Java 的构造器中有很多可选的参数时,代码就会变得冗长,难读而且容易出错。为了避免这种情况,《Effective Java》的第二条项目描述了如何高效使用 Builder 模式。构建这样的对象需要很多代码,比如下面的营养成分对象代码示例。它需要两个必需参数(servingSize, servings) 以及四个可选参数 ( calories, fat, sodium, carbohydrates):

public class JavaNutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values
        private int calories      = 0;
        private int fat           = 0;
        private int carbohydrate  = 0;
        private int sodium        = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings    = servings;
        }

        public Builder calories(int val)
        { calories = val;      return this; }
        public Builder fat(int val)
        { fat = val;           return this; }
        public Builder carbohydrate(int val)
        { carbohydrate = val;  return this; }
        public Builder sodium(int val)
        { sodium = val;        return this; }

        public JavaNutritionFacts build() {
            return new JavaNutritionFacts(this);
        }
    }

    private JavaNutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

使用 Java 代码来实例化对象看起来像这样:

final JavaNutritionFacts cocaCola = new JavaNutritionFacts.Builder(240,8)
    .calories(100)
    .sodium(35)
    .carbohydrate(27)
    .build();

使用 Kotlin 你根本不需要使用 Builder 模式,因为它有默认参数的功能,允许你定义每个可选构造器参数的默认值。

class KotlinNutritionFacts(
        private val servingSize: Int,
        private val servings: Int,
        private val calories: Int = 0,
        private val fat: Int = 0,
        private val sodium: Int = 0,
        private val carbohydrates: Int = 0)

创建一个 Kotlin 对象看起来像这样:

val cocaCola = KotlinNutritionFacts(240,8,
                calories = 100,
                sodium = 35,
                carbohydrates = 27)

为了更好的可读性,你也可以声明必需的参数 servingSize 和 servings:

val cocaCola = KotlinNutritionFacts(
                servingSize = 240,
                servings = 8,
                calories = 100,
                sodium = 35,
                carbohydrates = 27)

像 Java 一样,这里对象的创建是不可改变的。

我们将 Java 所需的 47 行代码减少到 Kotlin 的 7 行,生产率有很大的提高。

提示:如果你想要在 Java 中创建 KotlinNutrition 对象,你当然可以那样做,但是你被迫为每个可选参数指定一个值。幸运的是,如果你加上 JvmOverloads 注解,多种构造器就被生成了。注意如果你想要使用一个注解,我们需要声明关键词 constructor :

class KotlinNutritionFacts @JvmOverloads constructor(
        private val servingSize: Int,
        private val servings: Int,
        private val calories: Int = 0,
        private val fat: Int = 0,
        private val sodium: Int = 0,
        private val carbohydrates: Int = 0)

2. 轻松创建单例

《Effective Java》的第三条建议展示了将一个 Java 对象设计成单例,意味着只有一个实例能被实例化。下面的代码片段显示了 “ monoelvistic” 宇宙,只有一个 Elvis 存在:

public class JavaElvis {

    private static JavaElvis instance;

    private JavaElvis() {}

    public static JavaElvis getInstance() {
        if (instance == null) {
            instance = new JavaElvis();
        }
        return instance;
    }

    public void leaveTheBuilding() {
    }
}

 

Kotlin 有对象声明的概念,给了我们一个单例行为,开箱即用:

object KotlinElvis {

    fun leaveTheBuilding() {}
}
view raw

 

再也不需要手动构建单例模式了!

3. equals() 和 hashCode() 开箱即用

一个良好的编程实践起源于函数编程,简化代码主要是使用不可变值对象。第 15 项就是“类应该是不可变的除非有一个非常好的理由让它们可变。”不可变值对象在 Java 中的创建非常乏味,因为每一个对象你都要重写 equals() 和 hashCode() 方法。这在第 8 和第 9 项中花费了 Joshua Bloch 18 页来描述如何遵循这一详尽的普遍的准则。例如,如果你重写 equals(),则必须确保灵活性,对称性,传递性,一致性和无效性的合同都得到满足。听起来更像数学而不是编程。

在 Kotlin 中,你只要使用 data 类即可。编译器自动导出像 equals() 和 hashCode() 等方法。这是可能的,因为标准函数可以从对象的属性机械地派生。只要在你的类前面输入关键字 data 即可。不需要 18 页的描述了。

提示:最近 AutoValue 在 Java 中变得很流行,这个库为 Java 1.6+ 生成不可变值类。

4. 属性替代字段

public class JavaPerson {

    // don't use public fields in public classes!
    public String name;
    public Integer age;
}

 

第 14 项建议在公共类中使用访问器方法而不是公共字段。如果你不遵循这个建议可能会有麻烦,因为如果字段可以直接访问那么就丧失了封装和灵活性带来的好处。这意味着将来你不更改其公共 API 将无法改变你的类的内部表示。例如,你无法限制字段的值,如某人的年龄等等。这是一个为什么我们总是创建冗长的 getter 和 setter 方法的原因。

最佳实践被 Kotlin 增强了,因为它使用自动生成的默认 getter 和 setter 方法的属性替代了字段。

class KotlinPerson {

    var name: String? = null
    var age: Int? = null
}

 

在语法上,你可以像使用 person.name 或者 person.age 访问 Java 的公共字段一样访问这些属性。你也可以自定义 getter 和 setter 方法而不需要改变类的 API:

class KotlinPerson {

    var name: String? = null

    var age: Int? = null
    set(value) {
        if (value in 0..120){
            field = value
        } else{
            throw IllegalArgumentException()
        }
    }
}

 

长话短说:使用 Kotlin 的属性,我们的类更加简洁也更加灵活。

5. Override 变成了强制性的关键字而不是可选的注解

注解在 Java 发布的 1.5 版本中增加了。最重要的一个就是 Override,标记着一个方法正在重写父类的方法。根据第 36 项,为了避免歹毒的 bug,可选的注解应该不断地被使用。当你认为你正在重写父类的方法而实际上没有时,编译器会抛出一个错误。只要当你没有忘记使用 Override 注解,那么就没有问题。

在 Kotlin 中,override 不再是可选的注解而是强制的关键字。所以像这类难受的 bug 不会再出现,编译器将会提前警告你。Kotlin 坚持把这些事弄清楚

原文链接:How “Effective Java” may have influenced the design of Kotlin — Part 1

重构的概念及为什么代码需要重构

zhouchong阅读(171)评论(0)



1、重构的概念:

首先,重构这个概念,不是Java所特有的,而是软件工程的一个概念。

第一个定义是名词形式:   重构(名词),对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

第一个定义是动词形式:   重构(动词),使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

在面向对象C++\C#\JAVA等语言中,重构的概念一般是指对类进行重构,一般在现有类的某些功能方法不能满足扩展需要,或者修复BUG时,就需要重构

2、如何通俗的理解重构:

假设构建一个复杂系统需要考虑10点。由于人本身的局限,你肯定会做错其中的6点,当然这6点并不影响第一版的发布。然后,你如何修正这6点?

  1. 你不能重写系统。因为重写系统你还会做错6点,虽然可能是不同的6点。
  2. 你不能在增加系统功能的同时修正这6点,因为增加一个功能,就会再增加比如说5点要考虑的东西,这个过程中你又会做错3点。
  3. 当你不增加新功能,又试图修复这6点中的某一个或者几个,这个过程就是『重构』。
  4. 重构并不能保证不会毁掉你原本做对的4点,但是你起码可以肯定这个错误是因为你的重构造成的而不是别的原因(比如第2种做法里新增加的那5点)。而且这也是鼓励你在第3种做法里尽量只修正一个错误。
  5. 你永远达不到完全做对10点,即使你有无限的时间重构。而实际上你的重构永远在和添加新功能竞争资源。
  6. 由于第5点,你不要无缘无故的重构,除非需要增加新功能。但是由于第4点,又不能和增加新功能同时进行。

3、为什么需要重构:

  • 「重构」并不是完全打翻重来,最开始的设计也并非一无是处。
  • 软件开发是一个过程,软件使用的人群、环境都可能在进行中发生变化,当初设计中的一些假设、条件都会变化,这就需要根据新的状况做出调整。「重构」是代码层面的「重设计」,代码是软件的实现方式,设计做出调整,代码当然也要调整。
  • 「重构」也是对原有代码的完善,消除代码中的腐臭味,让代码更健壮、效率更高、更易维护。这是软件开发的规律决定的,没有人能一次写出完善的代码。
  • 重构这种事情对于还有生命价值的软件而言,就是必需的。这一点我觉得可以类比人类和任何一种有生命的动物。

    任何一种生物都不是完美的,但它(他、她)却可以在一定程度上适应环境的需求,但是适应的并不完美。所以才需要重构这种不断地“新陈代谢”和“进化”,保持这个物种在大自然中的竞争力。也只有被淘汰的物种才不需要新陈代谢和进化。

4、修改软件的四种动机:

  1. 增加新功能
  2. 原有功能有bug
  3. 改善原有程序的结构
  4. 优化原有系统的性能

 



先上结论:其实软件开发并不一定需要重构
作为《重构》这本书的译者,我有一个观点:重构已死。当然这种“xx已死”的调调一听就知道是哗众取宠的。更加严格的说法应该是:《重构》书中所描述的、针对面向对象语言单一代码库的重构技术,是在一个独特历史背景下发展成型的产物;在当今的软件开发历史背景下,这种技术的适用范围正在变窄,且适用的问题域和解决方案都已相当成熟,因此不再有继续讨论的价值
=== 第一更:重构是(和不是)什么 
为了使接下来的讨论有一个可靠的基础,首先需要对“重构”(refactoring)这个词作一个明确的定义。按照《重构》书中的定义:

第一个定义是名词形式:   重构(名词),对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

第一个定义是动词形式:   重构(动词),使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

上述定义中,第一个值得注意的关键词是“不改变软件可观察行为”。按照这个关键词,

所说的情景就不是重构(当然,这还取决于他说的“做错”和“修正”如何定义,我假设“修正一个错误”意味着“改变软件可观察的行为”)。

第二个关键词是“重构手法”。一次重构之所以能成为一次重构,它必须遵循一定的重构手法,而不是随便调整。很多人在阅读这本书的时候只看前四章,真正动手改代码的时候根本不遵循重构手法,这样的行为也不是重构。我在这里采用一个较为严格的限定:只有遵循已经发表的重构手法时,才能认为是在进行重构。
第三个关键词是“软件内部结构”。这实际上是非常有玄机的一个词。玄机我们暂且按下不表,从Opdyke的论文到《重构》再到《修改代码的艺术》,重构理论的早期奠基作品谈论的都是单进程、单代码库的代码重构。因此,我倾向于将“软件内部”限定为“单进程、单代码库内”。
现在,当我讨论“软件开发是否需要重构”时,我讨论的是:
软件开发是否需要:
1. 在不改变软件可观察行为的前提下,
2. 采用已经发表的重构手法,
3. 对单进程、单代码库内的软件结构进行调整。
而我的观点是:在现代软件开发的环境下,越来越不需要。
=== 第二更:为什么重构 
以下内容出自《重构》第2.2节。我用粗体标注出了一些关键词,这些关键词将有助于我们理解重构技术背后的假设。

重构能改善软件设计

不重构,程序的设计将随着时间腐坏。如果碰到临时需求跟设计有冲突,或者对设计没有整体的理解,这时改代码很难保证程序的设计不被破坏。随着“破坏”的积累,不管谁都很难再从代码中看出设计。重构更像是在整理代码,它可以移除没待对地方的代码。程序设计的“破坏”有加速累计的效应。越难看到代码里的设计,越难保持,然后腐坏的就越快。日常的重构能保持代码的设计。

设计差的代码完成相同的功能需要更多代码,通常是因为有大量的重复,因此改善代码设计的一个重要方面就是消除代码重复。减少重复代码并不会让系统跑的更快,但是却会给将来修改代码带来很大的不同。代码重复越多,就越难保证正确修改,因为有更多代码需要阅读和理解。你改了这个地方后,系统并没有按照你认为的方式工作,因为你没有修改另外一个稍有差异但完成相同功能的地方。通过消除重复,你的代码针对每件事情,只说一遍,并且是唯一的一遍,这是好设计的基本要素。

重构使软件更易理解

编程是和计算机对话的一种方式。你写代码告诉电脑做什么,它严格执行后给予反馈。很容易的,你把“想要什么”和“告诉电脑怎么做”扯到了一起。你写的代码全是在说怎么做。但是你的代码还有另外一个用户,你的同事会在几个月后为了完成某项功能而尝试修改你的代码。我们很容易忘记这个“额外”的用户,然而这个用户才是最重要的。有谁会关心电脑是否会多花点时间做编译?重要的是你的同事可能要花一周才能看懂你的代码,然后做个小修改。如果你的代码容易看懂,他也许一个小时就可以搞定。

问题就是当你正在努力让程序工作的时候,根本就没有想到你的同事。当你的程序能工作的时候,花点时间重构可以让你的代码更有结构,更能表达出它的意图。你的代码应该总是在说你想要什么。也不是我无私,这个“同事”通常就是我自己。

我通过重构来理解不熟悉的代码。当我看到不熟悉的代码时,我必须试着理解它做了什么。通过重构,我并不只是停留在用脑子记,而是实际地把代码修改成我理解的样子,然后通过重跑程序(测试)来验证我的理解是否正确。一开始的时候,我只能修改其中一些小的细节,当代码修改的越来越清晰的时候,我发现能看到之前看不到的设计。如果没有重构过这些代码,可能永远也无法理解到这些,我还没有厉害到可以把这一切都虚拟到脑子里。当学习代码的时候,重构可以逐步让我理解高层的设计。

重构有助于找到Bug

帮助理解代码也会帮我找到Bug。我承认我并不善于找Bug,有些人能够仅仅通过读一大段代码就找出其中的Bug,我却不能。然而重构代码时,我会深入地理解代码做了些什么,然后我带着这些理解再去看新代码时,有些Bug就不可避免的自然出现了。这让我想起了Kent Back经常说的一句话“我并不是伟大的程序员,我只是有伟大习惯的好程序员。”重构让我能高效地编写高质量的代码。

重构有助于提高编程速度

从前面的观点可以看出:重构能帮助你提高编程速度。

听起来有点不可思议。当我讨论重构时,人们容易理解它能改进代码质量。改进设计,可读性,减少Bug,所有这些都是改进代码质量。难道这些没有降低了开发速度吗?

我非常肯定好的代码设计是软件快速开发的基石。实际上,之所以要好的设计,就是为了快速开发。没有好的设计,你只是快一时,不久后就会慢下来。你花费时间在找Bug,修复Bug,而不是实现新需求。当系统里面有重复代码时,你要花更长的时间理解,花更长的时间修改代码。当补丁上打补丁时,再加新需求会需要更长的时间,更多的代码。

好的设计是软件快速开发的必要条件,重构能够帮你快速开发软件,因为它不仅能防止设计腐坏,还能改善设计。

从这几个重构的原因中,我们看到了下列几个(我在引文中加粗的)关键词,我们可以来分析一下它们背后隐含的假设:
短期目的 => 编程时仅考虑短期目的是不好的,也就是说,代码库有比较长的生命周期。
另一位程序员 => 在这个代码库上工作的人数会比较多,且人员的变动比较频繁。
一大段代码 => 代码库的规模比较大。
添加新功能 => 一个代码库承担多种责任。
所以从2.2节中我们不仅可以读到为什么重构,而且可以读出作者没有明言的假设:作者在谈论的是规模较大、生命周期较长、承担了较多责任、有一个较大(且较不稳定)团队在其上工作的单一代码库。当然所有这些都是相对度量,例如多少行代码算大、生存多长时间算长,没有绝对而清晰的界定。
那么接下来我们就要问:为什么重构技术有这样一些假设?这些假设是在什么样的时代背景下形成的?这些假设在今天的时代背景下是否仍然适用?如果这些假设不再适用,重构技术是否仍然适用?
=== 第三更:重构的时代背景
可能令人吃惊:重构出现的时间相当早,比Java早,比设计模式早。William Opdyke的博士论文Refactoring Object-Oriented Frameworks下载PDF)发表于1992年,这篇论文是重构理论的奠基之作。(BTW,Opdyke这位老兄相当牛逼,他的博士导师是GoF之一的Ralph Johnson。)那么在Java都还没出现之前,重构的早期玩家们玩的是什么语言呢?答案是“Smalltalk”,传说中的“Ruby之根”——好吧,跑题了,打住。《重构》的出版是1999年,所以基本上我们可以认为,重构是上世纪90年代浮现、并在本世纪头十年引起重视的技术。
于是我们要问了:为什么在世纪之交的这十多年里,规模大、承担责任多的单一代码库成为了主流?在一个代码库、一个进程里承载一个系统所有的功能,这并不是一个古而有之的做法。例如在一个典型的基于VxWorks的电信系统中,职责划分不是以函数、而是以进程为单位。即便在Sun推荐的J2EE架构(http://docs.oracle.com/cd/E19683-01/817-2175-10/deover.html…)中,业务是分散在若干个EJB中的,每个EJB可以(尽管未必总是会)被部署为一个独立的进程,所以整个系统也不必(尽管仍然可以)存在于单一代码库中。
然而Sun推荐的J2EE架构并没有被广泛接受,反倒是以Spring为代表的“轻量级J2EE”最终成为了主流。我在《不敢止步》里讲到,“轻量级J2EE”的核心架构思想实际上就是Martin Fowler的分布式对象设计第一法则
分布式对象设计第一法则:不要分布你的对象!  另一种风格则是Apache、Jon Tirsen、Rod Johnson 等开源先锋推荐的Martin Fowler 在PoEAA 里总结的:不要分布对象,在一个Java 进程里完成所有业务逻辑, 用集群解决单台服务器负载过重的问题。这种架构风格,再加上后来的“Shared Nothing Architecture”,实际上就把Web 应用简化成了一个单进程编程的问题: 不是“使远程调用变得透明”,而是根本没有远程调用。
这个架构风格的最大价值是让开发者可以在自己的桌面电脑上复制整个生产环境,从而能够快速修改并看到反馈。“在个人电脑上复制整个生产环境”这件事,只有当个人电脑的性能高到一定程度时才有可能的。在90年代之前,电信、金融、医疗、航天、军事等主要的IT用户,其生产环境是很难复制的。所以当时的软件开发过程必须是瀑布式的,因为修改之后看到效果的反馈周期太长,只好尽量提前把设计做好,开发一次成功。而90年代个人电脑性能的提升、尤其是奔腾CPU的广泛应用,使“在个人电脑上复制整个生产环境”成为了可能。
而轻量级J2EE则是在这个时间节点上的关键一触,把可能性变成了现实。如果将业务逻辑分布在多个进程中,就不可避免地需要运行多个Java虚拟机进程(甚至多个应用服务器进程),而这些——在当时的标准下——庞大的进程会耗尽当时大多数桌面开发电脑的性能,从而使反馈周期变长。
能够在单台个人电脑上复制整个生产环境,这个能力开启了整个敏捷软件开发的想象空间。测试驱动开发成为可能,持续集成成为可能,用户代表跟开发团队的紧密沟通才有必要,每天的站会、每周的迭代展示和计划会议在这个反馈周期的节奏下才有意义,注重对话的故事卡在这个节奏下才显得重要。单一代码库、单一进程、不分布对象、服务器农场式部署的架构风格,其目的是尽可能地保障在单台个人电脑上复制整个生产环境的能力,从而显著缩短反馈周期。
这就是重构技术走进主流视野的时代背景:摩尔定律使个人电脑堪堪具备足够的计算能力来复制整个生产环境,敏捷社区迫不及待地用轻量级J2EE架构使“复制整个生产环境”成为现实,在缩短反馈周期的同时也使单一代码库不得不承载整个系统的职责,从而使整个代码库变得庞大且职责不单一。为了在这样的代码库上继续快速的“修改-反馈”循环,对这样的代码库进行内部质量保障和改进的技术——测试驱动开发、持续集成、重构——才变得重要。
那么,十多年以后,我们所处的时代背景发生了哪些变化?这些变化是否会使重构技术变得不再重要?
=== 第四更:今天的世界 
今天的商业IT环境用一个词就可以概括:不确定。不确定带来颠簸和变动,不确定滋生怀疑和恐惧。而这种不确定,从丰田决定用大规模定制的产品来服务消费者的需求、而非仅仅提供某种功能开始,就已经注定了。当产品的目的是提供某种功能,功能的要求和达成的方式是相对确定的,不确定的比例相对较小;而当产品的目的是满足消费者的需求乃至提供良好的体验,人的不理性、不逻辑性就开始占据更大的比重,从而使不确定性的比例显著增加。Martin Fowler在《企业应用架构模式》里讲,所谓“业务逻辑”,实际上是“业务不逻辑”——因为这些业务规则根本不合逻辑,所以它们才需要特别地被提出、被讨论、被理解、被确认。这个洞见反映出当代商业IT环境的特征:现在的商业IT,越来越多地是在建模与迎合人的不理性、不逻辑性。在这个背景下,不确定性的剧增就是不可避免的。
当不确定性还不算太多的时候,对软件的要求是可维护性:需要加新的功能、需要修改旧的功能,能改得进去。这种“维护”(或者叫“演化”)实际上仍然是基于预测的。整个软件系统的大致方向已经被预测了,然后在此基础上谈演化。而当不确定实在太多的时候,对软件的要求就变成了可抛弃性。整本《精益创业》讲的就是这么一回事:你构建(build)的所有东西,都是为了度量(measure)某种数据从而学习(learn)——“学习”的定义是证实或证伪某个假设(hypothesis),而一旦假设被证伪,就要立即转向(pivot)。换句话说,你开发的所有软件,都应该做好很快被抛弃的准备。
如何获得可抛弃性?很简单:少写代码。一万行代码扔掉要伤心,一千行代码随时扔掉重新写。从轻量级J2EE的胜利开始,开源软件在商用软件领域成为了绝对主流,并展现出巨大的复用优势:基础框架越来越成熟,应用编程越来越倾向于用DSL描述业务领域,代码量越来越少。Spring已经呈现出这个趋势,Ruby on Rails及之后的框架更将这个趋势推向极致:开发者可以在最短时间内以最少代码量做出一个规规矩矩的软件。开发这样的业务软件并不需要多少设计,因为大部分设计已经蕴含在框架内。代码结构简单,代码量少,决定了这样的代码库烂也烂不到哪里去。而且本来就是以可抛弃性为目标设计的软件,它的生命周期预期也不会长到让代码质量能烂到哪里去。
但是总有些系统会成功,会向着更复杂的方向演化。然而这时系统的演化就未必要在同一个代码库中进行了。在这个阶段,软件的设计者应该能区分出较为稳定的领域模型与较为易变的用户操作。领域模型与用户操作两者变化的频率不同,实现的技术也不同,完全没有理由存在于同一个代码库中。同时,多渠道、尤其是移动渠道的兴起,是另一个推动领域模型与用户操作分离的动因:领域模型最好是运行在自己的进程中,向各种渠道提供服务,从而在不同渠道之间复用领域逻辑。
于是我们再一次有了“以多个代码库、多个进程承载一个系统”的诉求。而这一次,摩尔定律的发展、特别是虚拟化技术的发展,使得“多进程”与“在个人电脑上复制整个生产环境”不再矛盾。正如Sam Newman在《Building Microservices》中所说:
Domain-driven design. Continuous delivery. On-demand virtualization. Infrastructure automation. Small autonomous teams. Systems at scale. Microservices have emerged from this world.
在同一本书里,Newman也提到了可抛弃性(他称之为“可替换性”)的问题:
Teams using microservice approaches are comfortable with completely rewriting services when required, and just killing a service when it is no longer needed. When a codebase is just a few hundred lines long, it is difficult for people to become emotionally attached to it, and the cost of replacing it is pretty small.
既然不必把整个系统塞进一个代码库、一个进程,那么自然可以让每个代码库、每个进程符合单一职责原则,做且仅做一件事。这样的一个代码库规模不会大(尽管未必像Newman所想的只有几百行代码),在上面工作的团队也会很小。这样的一个代码库,它的质量不会烂到哪里去;即使出现了质量腐化的迹象,在有必要的测试覆盖的前提下,即使不遵循严格的重构手法也足以优化其内部质量;即使——尽管相当不可能——质量腐化到了相当的程度,正如Newman所说,从头写过就是了。在这样的一个代码库中,《重构》书中所描述的严格的重构技术所能发挥的价值将相当有限。
=== 第五更:尾声,以及新的开篇 
简单总结一下前面讲的内容:
1. 《重构》所介绍的重构技术,是在不改变软件可观察行为的前提下,采用已经发表的重构手法,对单进程、单代码库内的软件结构进行调整。
2. 这种技术适用于规模较大、生命周期较长、承担了较多责任、有一个较大(且较不稳定)团队在其上工作的单一代码库。
3. 十多年前,企业应用架构转向单一代码库、单一进程、不分布对象、服务器农场式部署的架构风格,其目的是尽可能地保障在单台个人电脑上复制整个生产环境的能力,从而显著缩短反馈周期。
4. 现在,更高的不确定性使代码库生命周期显著缩短,领域驱动设计和虚拟化技术使每个代码库职责单一。因此,在今天的商业IT环境下,重构技术的价值大大降低。
或者,换回哗众取宠的调调,我说的是:十五年以后,重构已死
清晰地认识到“重构已死”这一现实很重要。因为这个认识会让我们开启一系列更为宏大、影响更为深远的讨论:
1. 虽然单一代码库的职责变得简单,然而整个IT系统的复杂度仍然在。这些复杂度以何种方式表现?
2. 虽然单一代码库的内部质量不至于严重腐化,然而整个复杂的IT系统仍然有可能腐化。一个复杂系统会以何种方式腐化?
3. Opdyke在他的论文中介绍了一组行为保持的修改手法,以这些手法对程序进行修改时,对程序外部行为的影响可以被控制在最小范围。在代码之外,这些行为保持的修改手法是否普遍适用?
4. 测试驱动开发是重构的安全网。在无法使用xUnit的情景下,如何构建安全网?
5. Kerievsky在《重构与模式》中指出,重构以设计模式为目标。在面向对象设计模式不适用的场景下,如何确定重构的终点?
我的观点是,虽然《重构》书中所介绍的重构技术已经过时,然而对坏味道重构手法的总结和抽象将指导我们得出广泛适用的重构思想,这种思想将有助于识别和解决其它复杂IT系统中的内部质量问题。并且由于康威法则(Conway’s law)的作用,这种思想也将有助于识别和解决其它复杂组织系统中的内部质量问题。
用哗众取宠的调调来说就是:重构已死,重构思想永生

 

 

 

 

 

 

十二、考虑实现 Comparable 接口

zhouchong阅读(157)评论(0)

考虑实现 Comparable 接口:

和之前提到的通用方法 equals、hashCode 和 toString 不同的是 compareTo 方法属于

 

Comparable 接口,该接口为其实现类提供了排序比较的规则,实现类仅需基于内部的逻辑,为

compareTo 返回不同的值,既 A.compareTo(B) > 0 可视为 A > B,反之则 A < B,如果 A.compareTo(B)

== 0,可视为 A == B。在 C++中由于提供了操作符重载的功能,因此可以直接通过重载操作符的方式 进行对象间的比较,事实上 C++的标准库中提供的缺省规则即为此,如 bool operator>(OneObject o)。 在 Java 中,如果对象实现了 Comparable 接口,即可充分利用 JDK 集合框架中提供的各种泛型算法,

如:Arrays.sort(a); 即可完成 a 对象数组的排序。事实上,JDK 中的所有值类均实现了该接口,如 Integer、

String 等。

Object.equals 方法的通用实现准则也同样适用于 Comparable.compareTo 方法,如对称性、传 递性和一致性等,这里就不做过多的赘述了。然而两个方法之间有一点重要的差异还是需要在这里提及的, 既 equals 方法不应该抛出异常,而 compareTo 方法则不同,由于在该方法中不推荐跨类比较,如果当 前类和参数对象的类型不同,可以抛出 ClassCastException 异常。在 JDK 1.5 之后我们实现的 Comparable<T>接口多为该泛型接口,不在推荐直接继承 1.5 之前的非泛型接口 Comparable 了,新 的 compareTo 方法的参数也由 Object 替换为接口的类型参数,因此在正常调用的情况下,如果参数类 型不正确,将会直接导致编译错误,这样有助于开发者在 coding 期间修正这种由类型不匹配而引发的异 常。

在该条目中针对 compareTo 的相等性比较给出了一个强烈的建议,而不是真正的规则。推荐

compareTo 方法施加的等同性测试,在通常情况下应该返回和 equals 方法同样的结果,考虑如下情况:

public static void main(String[] args) {
    HashSet<BigDecimal> hs = new HashSet<BigDecimal>();

    BigDecimal bd1 = new BigDecimal("1.0");

    BigDecimal bd2 = new BigDecimal("1.00");

    hs.add(bd1);

    hs.add(bd2);
    System.out.println("The count of the HashSet is " + hs.size());



    TreeSet<BigDecimal> ts = new TreeSet<BigDecimal>();

    ts.add(bd1);

    ts.add(bd2);
    System.out.println("The count of the TreeSet is " + ts.size());

    }
    /*  输出结果如下:

    The count of the HashSet is 2

    The count of the TreeSet is 1
 */

由以上代码的输出结果可以看出,TreeSet 和 HashSet 中包含元素的数量是不同的,这其中的主要 原因是 TreeSet 是基于 BigDecimal 的 compareTo 方法是否返回 0 来判断对象的相等性,而在该例中 compareTo 方法将这两个对象视为相同的对象,因此第二个对象并未实际添加到 TreeSet 中。和 TreeSet 不同的是 HashSet 是通过 equals 方法来判断对象的相同性,而恰恰巧合的是 BigDecimal 的 equals 方 法并不将这个两个对象视为相同的对象,这也是为什么第二个对象可以正常添加到 HashSet 的原因。这 样的差异确实给我们的编程带来了一定的负面影响,由于 HashSet 和 TreeSet 均实现了 Set<E>接口, 倘若我们的集合是以 Set<E>的参数形式传递到当前添加 BigDecimal 的函数中,函数的实现者并不清楚 参数 Set 的具体实现类,在这种情况下不同的实现类将会导致不同的结果发生,这种现象极大的破坏了面 向对象中的”里氏替换原则”。

在重载 compareTo 方法时,应该将最重要的域字段比较方法比较的最前端,如果重要性相同,则将

比较效率更高的域字段放在前面,以提高效率,如以下代码:

public int compareTo(PhoneNumer pn) {
    if (areaCode < pn.areaCode)

    return -1;

    if (areaCode > pn.areaCode)

    return 1;


    if (prefix < pn.prefix)

    return -1;

    if (prefix > pn.prefix)

    return 1;


    if (lineNumber < pn.lineNumer)

    return -1;

    if (lineNumber > pn.lineNumber)

    return 1;

    return 0;
    }

 

上例给出了一个标准的 compareTo 方法实现方式,由于使用 compareTo 方法排序的对象并不关 心返回的具体值,只是判断其值是否大于 0,小于 0 或是等于 0,因此以上方法可做进一步优化,然而需 要注意的是,下面的优化方式会导致数值类型的作用域溢出问题。

public int compareTo(PhoneNumer pn) {
    int areaCodeDiff = areaCode - pn.areaCode;

    if (areaCodeDiff != 0)

    return areaCodeDiff;

    int prefixDiff = prefix - pn.prefix;

    if (prefixDiff != 0)
    return prefixDiff;


        int lineNumberDiff = lineNumber - pn.lineNumber;
        if (lineNumberDiff != 0)
        return lineNumberDiff;
        return 0;
    }