`
weimou66
  • 浏览: 1241884 次
文章分类
社区版块
存档分类
最新评论

提高编程思想

 
阅读更多

虚函数和抽象函数有什么区别

虚函数是有代码的并明确允许子类去覆盖,但子类也可不覆盖,就是说可以直接用,不用重写
抽象函数是没有代码,子类继承后一定要重写

******************************************************************

在一个类中用虚函数:
是因为在超类中的有实际代码的方法,但明确允许子类可以作重写
而且当子类重写后,可以用子类实例超类;如果这样,超类变量调用虚函数时,执行的是子类的方法

在一个类中用抽象函数
是在写超类时不确定函数的代码,让子类去实现

******************************************************************

抽象函数没有方法体。

c+=4和c=c+4的区别。

String abc="aaa";
char c=abc.charAt(i);
c+=4;

以上如果把:c+=4; 改成:c=c+4;
就不正确。


********************************************************************************************************************************************************************************************************************************************************************

c+=4;
c=c+4;

当c是int类型的时候,这两个表达式是一样的,但是c不是int时,这两个表达式是不一样的。

这两个表达式都被称为赋值表达式。第二条语句使用的是简单赋值操作符(=),而第一条语句使用的是复合赋值操作符。(复合赋值操作符包括 +=、-=、*=、/=、%=、<<=、>>=、>>>=、&=、^=和|=)Java语言规范中讲到,复合赋值 E1 op= E2等价于简单赋值E1 = (T)((E1)op(E2)),其中T是E1的类型,除非E1只被计算一次。
换句话说,复合赋值表达式自动地将它们所执行的计算的结果转型为其左侧变量的类型。


所以要让c=c+4 编译能通过,得
int a=c;
c=(char)(a+4);

System.out.println(c);



字符,字节和编码

[原创文章,转载请保留或注明出处:http://www.regexlab.com/zh/encoding.htm]

级别:初级

摘要:本文介绍了字符与编码的发展过程,相关概念的正确理解。举例说明了一些实际应用中,编码的实现方法。然后,本文讲述了通常对字符与编码的几种误解,由于这些误解而导致乱码产生的原因,以及消除乱码的办法。本文的内容涵盖了“中文问题”,“乱码问题”。

引言

“字符与编码”是一个被经常讨论的话题。即使这样,时常出现的乱码仍然困扰着大家。虽然我们有很多的办法可以用来消除乱码,但我们并不一定理解这些办法的内在原理。而有的乱码产生的原因,实际上由于底层代码本身有问题所导致的。因此,不仅是初学者会对字符编码感到模糊,有的底层开发人员同样对字符编码缺乏准确的理解。

回页首

1. 编码问题的由来,相关概念的理解

1.1 字符与编码的发展

从计算机对多国语言的支持角度看,大致可以分为三个阶段:

系统内码 说明 系统
阶段一 ASCII 计算机刚开始只支持英语,其它语言不能够在计算机上存储和显示。 英文 DOS
阶段二 ANSI编码
(本地化)
为使计算机支持更多语言,通常使用 0x80~0xFF 范围的 2 个字节来表示 1 个字符。比如:汉字 '中' 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。

不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。

不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。
中文 DOS,中文 Windows 95/98,日文 Windows 95/98
阶段三 UNICODE
(国际化)
为了使国际间信息交流更加方便,国际组织制定了 UNICODE 字符集,为各种语言中的每一个字符设定了统一并且唯一的数字编号,以满足跨语言、跨平台进行文本转换、处理的要求。 Windows NT/2000/XP,Linux,Java

字符串在内存中的存放方法:

在 ASCII 阶段,单字节字符串使用一个字节存放一个字符(SBCS)。比如,"Bob123" 在内存中为:

42 6F 62 31 32 33 00
B o b 1 2 3 /0

在使用 ANSI 编码支持多种语言阶段,每个字符使用一个字节或多个字节来表示(MBCS),因此,这种方式存放的字符也被称作多字节字符。比如,"中文123" 在中文 Windows 95 内存中为7个字节,每个汉字占2个字节,每个英文和数字字符占1个字节:

D6 D0 CE C4 31 32 33 00
1 2 3 /0

在 UNICODE 被采用之后,计算机存放字符串时,改为存放每个字符在 UNICODE 字符集中的序号。目前计算机一般使用 2 个字节(16 位)来存放一个序号(DBCS),因此,这种方式存放的字符也被称作宽字节字符。比如,字符串 "中文123" 在 Windows 2000 下,内存中实际存放的是 5 个序号:

2D 4E 87 65 31 00 32 00 33 00 00 00 ← 在 x86 CPU 中,低字节在前
1 2 3 /0

一共占 10 个字节。

回页首

1.2 字符,字节,字符串

理解编码的关键,是要把字符的概念和字节的概念理解准确。这两个概念容易混淆,我们在此做一下区分:

概念描述 举例
字符 人们使用的记号,抽象意义上的一个符号。 '1', '中', 'a', '$', '¥', ……
字节 计算机中存储数据的单元,一个8位的二进制数,是一个很具体的存储空间。 0x01, 0x45, 0xFA, ……
ANSI
字符串
在内存中,如果“字符”是以 ANSI 编码形式存在的,一个字符可能使用一个字节或多个字节来表示,那么我们称这种字符串为ANSI 字符串或者多字节字符串 "中文123"
(占7字节)
UNICODE
字符串
在内存中,如果“字符”是以在 UNICODE 中的序号存在的,那么我们称这种字符串为 UNICODE 字符串或者宽字节字符串 L"中文123"
(占10字节)

由于不同 ANSI 编码所规定的标准是不相同的,因此,对于一个给定的多字节字符串,我们必须知道它采用的是哪一种编码规则,才能够知道它包含了哪些“字符”。而对于UNICODE 字符串来说,不管在什么环境下,它所代表的“字符”内容总是不变的。

回页首

1.3 字符集与编码

各个国家和地区所制定的不同 ANSI 编码标准中,都只规定了各自语言所需的“字符”。比如:汉字标准(GB2312)中没有规定韩国语字符怎样存储。这些 ANSI 编码标准所规定的内容包含两层含义:

  1. 使用哪些字符。也就是说哪些汉字,字母和符号会被收入标准中。所包含“字符”的集合就叫做“字符集”。
  2. 规定每个“字符”分别用一个字节还是多个字节存储,用哪些字节来存储,这个规定就叫做“编码”。

各个国家和地区在制定编码标准的时候,“字符的集合”和“编码”一般都是同时制定的。因此,平常我们所说的“字符集”,比如:GB2312, GBK, JIS 等,除了有“字符的集合”这层含义外,同时也包含了“编码”的含义。

UNICODE 字符集”包含了各种语言中使用到的所有“字符”。用来给 UNICODE 字符集编码的标准有很多种,比如:UTF-8, UTF-7, UTF-16, UnicodeLittle, UnicodeBig 等。

回页首

2. 字符与编码在程序中的实现

2.1 程序中的字符与字节

在 C++ 和 Java 中,用来代表“字符”和“字节”的数据类型,以及进行编码的方法:

类型或操作 C++ Java
字符 wchar_t char *
字节 char byte
ANSI 字符串 char[] byte[]
UNICODE 字符串 wchar_t[] String
字节串→字符串 mbstowcs(), MultiByteToWideChar() * string = new String(bytes, "encoding")
字符串→字节串 wcstombs(), WideCharToMultiByte() bytes = string.getBytes("encoding")

以上需要注意几点:

  1. Java 中的 char 代表一个“UNICODE 字符(宽字节字符)”,而 C++ 中的 char 代表一个字节。
  2. MultiByteToWideChar() 和 WideCharToMultiByte() 是 Windows API 函数。

回页首

2.2 C++ 中相关实现方法

声明一段字符串常量:

// ANSI 字符串,内容长度 7 字节
char
sz[20] ="中文123";

// UNICODE 字符串,内容长度 5 个 wchar_t(10 字节)
wchar_t wsz[20] = L"/x4E2D/x6587/x0031/x0032/x0033";

UNICODE 字符串的 I/O 操作,字符与字节的转换操作:

// 运行时设定当前 ANSI 编码,VC 格式
setlocale(LC_ALL, ".936");

// GCC 中格式
setlocale(LC_ALL, "zh_CN.GBK");

// Visual C++ 中使用小写 %s,按照 setlocale 指定编码输出到文件
// GCC 中使用大写 %S

fwprintf(fp, L"%s/n", wsz);

// 把 UNICODE 字符串按照 setlocale 指定的编码转换成字节
wcstombs(sz, wsz, 20);
// 把字节串按照 setlocale 指定的编码转换成 UNICODE 字符串
mbstowcs(wsz, sz, 20);

在 Visual C++ 中,UNICODE 字符串常量有更简单的表示方法。如果源程序的编码与当前默认 ANSI 编码不符,则需要使用 #pragma setlocale,告诉编译器源程序使用的编码:

// 如果源程序的编码与当前默认 ANSI 编码不一致,
// 则需要此行,编译时用来指明当前源程序使用的编码

#pragma setlocale
(".936")

// UNICODE 字符串常量,内容长度 10 字节
wchar_t wsz[20] = L"中文123";

以上需要注意 #pragma setlocale 与 setlocale(LC_ALL, "") 的作用是不同的,#pragma setlocale 在编译时起作用,setlocale() 在运行时起作用。

回页首

2.3 Java 中相关实现方法

字符串类 String 中的内容是 UNICODE 字符串:

// Java 代码,直接写中文
String
string = "中文123";

// 得到长度为 5,因为是 5 个字符
System.out.println(string.length());

字符串 I/O 操作,字符与字节转换操作。在 Java 包 java.io.* 中,以“Stream”结尾的类一般是用来操作“字节串”的类,以“Reader”,“Writer”结尾的类一般是用来操作“字符串”的类。

// 字符串与字节串间相互转化

// 按照 GB2312 得到字节(得到多字节字符串)

byte
[] bytes = string.getBytes("GB2312");

// 从字节按照 GB2312 得到 UNICODE 字符串
string = new String(bytes, "GB2312");

// 要将 String 按照某种编码写入文本文件,有两种方法:

// 第一种办法:用 Stream 类写入已经按照指定编码转化好的字节串

OutputStream os = new FileOutputStream("1.txt");
os.write(bytes);
os.close();

// 第二种办法:构造指定编码的 Writer 来写入字符串
Writer ow = new OutputStreamWriter(new FileOutputStream("2.txt"),"GB2312");
ow.write(string);
ow.close();

/* 最后得到的 1.txt 和 2.txt 都是 7 个字节 */

如果 java 的源程序编码与当前默认 ANSI 编码不符,则在编译的时候,需要指明一下源程序的编码。比如:

E:/>javac -encoding BIG5 Hello.java

以上需要注意区分源程序的编码与 I/O 操作的编码,前者是在编译时起作用,后者是在运行时起作用。

回页首

3. 几种误解,以及乱码产生的原因和解决办法

3.1 容易产生的误解
对编码的误解
误解一 在将“字节串”转化成“UNICODE 字符串”时,比如在读取文本文件时,或者通过网络传输文本时,容易将“字节串”简单地作为单字节字符串,采用每“一个字节”就是“一个字符”的方法进行转化。

而实际上,在非英文的环境中,应该将“字节串”作为 ANSI 字符串,采用适当的编码来得到 UNICODE 字符串,有可能“多个字节”才能得到“一个字符”。

通常,一直在英文环境下做开发的程序员们,容易有这种误解。
误解二 在 DOS,Windows 98 等非 UNICODE 环境下,字符串都是以 ANSI 编码的字节形式存在的。这种以字节形式存在的字符串,必须知道是哪种编码才能被正确地使用。这使我们形成了一个惯性思维:“字符串的编码”。

当 UNICODE 被支持后,Java 中的 String 是以字符的“序号”来存储的,不是以“某种编码的字节”来存储的,因此已经不存在“字符串的编码”这个概念了。只有在“字符串”与“字节串”转化时,或者,将一个“字节串”当成一个 ANSI 字符串时,才有编码的概念。

不少的人都有这个误解。

第一种误解,往往是导致乱码产生的原因。第二种误解,往往导致本来容易纠正的乱码问题变得更复杂。

回页首

3.2 常用的编码简介

简单介绍一下常用的编码规则,为后边的章节做一个准备。在这里,我们根据编码规则的特点,把所有的编码分成三类:

分类 编码标准 说明
单字节字符编码 ISO-8859-1 最简单的编码规则,每一个字节直接作为一个 UNICODE 字符。比如,[0xD6, 0xD0] 这两个字节,通过 iso-8859-1 转化为字符串时,将直接得到 [0x00D6, 0x00D0] 两个 UNICODE 字符,即 "ÖÐ"。

反之,将 UNICODE 字符串通过 iso-8859-1 转化为字节串时,只能正常转化 0~255 范围的字符。
ANSI 编码 GB2312,
BIG5,
Shift_JIS,
ISO-8859-2 ……
把 UNICODE 字符串通过 ANSI 编码转化为“字节串”时,根据各自编码的规定,一个 UNICODE 字符可能转化成一个字节或多个字节。

反之,将字节串转化成字符串时,也可能多个字节转化成一个字符。比如,[0xD6, 0xD0] 这两个字节,通过 GB2312 转化为字符串时,将得到 [0x4E2D] 一个字符,即 '中' 字。

“ANSI 编码”的特点:
1. 这些“ANSI 编码标准”都只能处理各自语言范围之内的 UNICODE 字符。
2. “UNICODE 字符”与“转换出来的字节”之间的关系是人为规定的。
UNICODE 编码 UTF-8,
UTF-16, UnicodeBig ……
与“ANSI 编码”类似的,把字符串通过 UNICODE 编码转化成“字节串”时,一个 UNICODE 字符可能转化成一个字节或多个字节。

与“ANSI 编码”不同的是:
1. 这些“UNICODE 编码”能够处理所有的 UNICODE 字符。
2. “UNICODE 字符”与“转换出来的字节”之间是可以通过计算得到的。

在这里,我们可以看到,前面所讲的“误解一”,即采用每“一个字节”就是“一个字符”的转化方法,实际上也就等同于采用 iso-8859-1 进行转化。因此,我们常常使用 bytes = string.getBytes("iso-8859-1") 来进行逆向操作,得到原始的“字节串”。然后再使用正确的 ANSI 编码,比如 string = new String(bytes, "GB2312"),来得到正确的“UNICODE 字符串”。

回页首

3.3 非 UNICODE 程序在不同语言环境间移植时的乱码

非 UNICODE 程序中的字符串,都是以某种 ANSI 编码形式存在的。如果程序运行时的语言环境与开发时的语言环境不同,将会导致 ANSI 字符串的显示失败。

比如,在日文环境下开发的非 UNICODE 的日文程序界面,拿到中文环境下运行时,界面上将显示乱码。如果这个日文程序界面改为采用 UNICODE 来记录字符串,那么当在中文环境下运行时,界面上将可以显示正常的日文。

由于客观原因,有时候我们必须在中文操作系统下运行非 UNICODE 的日文软件,这时我们可以采用一些工具,比如,南极星,AppLocale 等,暂时的模拟不同的语言环境。

回页首

3.4 网页提交字符串

当页面中的表单提交字符串时,首先把字符串按照当前页面的编码,转化成字节串。然后再将每个字节转化成 "%XX" 的格式提交到 Web 服务器。比如,一个编码为 GB2312 的页面,提交 "中" 这个字符串时,提交给服务器的内容为 "%D6%D0"。

在服务器端,Web 服务器把收到的 "%D6%D0" 转化成 [0xD6, 0xD0] 两个字节,然后再根据 GB2312 编码规则得到 "中" 字。

在 Tomcat 服务器中,request.getParameter() 得到乱码时,常常是因为前面提到的“误解一”造成的。默认情况下,当提交 "%D6%D0" 给 Tomcat 服务器时,request.getParameter() 将返回 [0x00D6, 0x00D0] 两个 UNICODE 字符,而不是返回一个 "中" 字符。因此,我们需要使用 bytes = string.getBytes("iso-8859-1") 得到原始的字节串,再用 string = new String(bytes, "GB2312") 重新得到正确的字符串 "中"。

回页首

3.5 从数据库读取字符串

通过数据库客户端(比如 ODBC 或 JDBC)从数据库服务器中读取字符串时,客户端需要从服务器获知所使用的 ANSI 编码。当数据库服务器发送字节流给客户端时,客户端负责将字节流按照正确的编码转化成 UNICODE 字符串。

如果从数据库读取字符串时得到乱码,而数据库中存放的数据又是正确的,那么往往还是因为前面提到的“误解一”造成的。解决的办法还是通过 string = new String( string.getBytes("iso-8859-1"), "GB2312") 的方法,重新得到原始的字节串,再重新使用正确的编码转化成字符串。

回页首

3.6 电子邮件中的字符串

当一段 Text 或者 HTML 通过电子邮件传送时,发送的内容首先通过一种指定的字符编码转化成“字节串”,然后再把“字节串”通过一种指定的传输编码(Content-Transfer-Encoding)进行转化得到另一串“字节串”。比如,打开一封电子邮件源代码,可以看到类似的内容:

Content-Type: text/plain;
charset="gb2312"
Content-Transfer-Encoding: base64

sbG+qcrQuqO17cf4yee74bGjz9W7+b3wudzA7dbQ0MQNCg0KvPKzxqO6uqO17cnnsaPW0NDEDQoNCg==

最常用的 Content-Transfer-Encoding 有 Base64 和 Quoted-Printable 两种。在对二进制文件或者中文文本进行转化时,Base64 得到的“字节串”比 Quoted-Printable 更短。在对英文文本进行转化时,Quoted-Printable 得到的“字节串”比 Base64 更短。

邮件的标题,用了一种更简短的格式来标注“字符编码”和“传输编码”。比如,标题内容为 "中",则在邮件源代码中表示为:

// 正确的标题格式
Subject: =?GB2312?B?1tA=?=

其中,

  • 第一个“=?”与“?”中间的部分指定了字符编码,在这个例子中指定的是 GB2312。
  • “?”与“?”中间的“B”代表 Base64。如果是“Q”则代表 Quoted-Printable。
  • 最后“?”与“?=”之间的部分,就是经过 GB2312 转化成字节串,再经过 Base64 转化后的标题内容。

如果“传输编码”改为 Quoted-Printable,同样,如果标题内容为 "中":

// 正确的标题格式
Subject: =?GB2312?Q?=D6=D0?=

如果阅读邮件时出现乱码,一般是因为“字符编码”或“传输编码”指定有误,或者是没有指定。比如,有的发邮件组件在发送邮件时,标题 "中":

// 错误的标题格式
Subject: =?ISO-8859-1?Q?=D6=D0?=

这样的表示,实际上是明确指明了标题为 [0x00D6, 0x00D0],即 "ÖÐ",而不是 "中"。

回页首

4. 几种错误理解的纠正

误解:“ISO-8859-1 是国际编码?”

非也。iso-8859-1 只是单字节字符集中最简单的一种,也就是“字节编号”与“UNICODE 字符编号”一致的那种编码规则。当我们要把一个“字节串”转化成“字符串”,而又不知道它是哪一种 ANSI 编码时,先暂时地把“每一个字节”作为“一个字符”进行转化,不会造成信息丢失。然后再使用 bytes = string.getBytes("iso-8859-1") 的方法可恢复到原始的字节串。

误解:“Java 中,怎样知道某个字符串的内码?”

Java 中,字符串类 java.lang.String 处理的是 UNICODE 字符串,不是 ANSI 字符串。我们只需要把字符串作为“抽象的符号的串”来看待。因此不存在字符串的内码的问题。


Java代码编写的30条建议

Java代码编写的30条建议

(1) 类名首字母应该大写。字段、方法以及对象(句柄)的首字母应小写。对于所有标识符,其中包含的所有单词都应紧靠在一起,而且大写中间单词的首字母。例如:
ThisIsAClassName
thisIsMethodOrFieldName
若在定义中出现了常数初始化字符,则大写static final基本类型标识符中的所有字母。这样便可标志出它们属于编译期的常数。
Java包(Package)属于一种特殊情况:它们全都是小写字母,即便中间的单词亦是如此。对于域名扩展名称,如com,org,net或者edu等,全部都应小写(这也是Java 1.1和Java 1.2的区别之一)。

(2) 为了常规用途而创建一个类时,请采取"经典形式",并包含对下述元素的定义:

equals()
hashCode()
toString()
clone()(implement Cloneable)
implement Serializable

(3) 对于自己创建的每一个类,都考虑置入一个main(),其中包含了用于测试那个类的代码。为使用一个项目中的类,我们没必要删除测试代码。若进行了任何形式的改动,可方便地返回测试。这些代码也可作为如何使用类的一个示例使用。

(4) 应将方法设计成简要的、功能性单元,用它描述和实现一个不连续的类接口部分。理想情况下,方法应简明扼要。若长度很大,可考虑通过某种方式将其分割成较短的几个方法。这样做也便于类内代码的重复使用(有些时候,方法必须非常大,但它们仍应只做同样的一件事情)。

(5) 设计一个类时,请设身处地为客户程序员考虑一下(类的使用方法应该是非常明确的)。然后,再设身处地为管理代码的人考虑一下(预计有可能进行哪些形式的修改,想想用什么方法可把它们变得更简单)。
(6) 使类尽可能短小精悍,而且只解决一个特定的问题。下面是对类设计的一些建议:
■一个复杂的开关语句:考虑采用"多形"机制
■数量众多的方法涉及到类型差别极大的操作:考虑用几个类来分别实现
■许多成员变量在特征上有很大的差别:考虑使用几个类

(7) 让一切东西都尽可能地"私有"--private。可使库的某一部分"公共化"(一个方法、类或者一个字段等等),就永远不能把它拿出。若强行拿出,就可能破坏其他人现有的代码,使他们不得不重新编写和设计。若只公布自己必须公布的,就可放心大胆地改变其他任何东西。在多线程环境中,隐私是特别重要的一个因素--只有private字段才能在非同步使用的情况下受到保护。

(8) 谨惕"巨大对象综合症"。对一些习惯于顺序编程思维、且初涉OOP领域的新手,往往喜欢先写一个顺序执行的程序,再把它嵌入一个或两个巨大的对象里。根据编程原理,对象表达的应该是应用程序的概念,而非应用程序本身。

(9) 若不得已进行一些不太雅观的编程,至少应该把那些代码置于一个类的内部。

(10) 任何时候只要发现类与类之间结合得非常紧密,就需要考虑是否采用内部类,从而改善编码及维护工作(参见第14章14.1.2小节的"用内部类改进代码")。

(11) 尽可能细致地加上注释,并用javadoc注释文档语法生成自己的程序文档。

(12) 避免使用"魔术数字",这些数字很难与代码很好地配合。如以后需要修改它,无疑会成为一场噩梦,因为根本不知道"100"到底是指"数组大小"还是"其他全然不同的东西"。所以,我们应创建一个常数,并为其使用具有说服力的描述性名称,并在整个程序中都采用常数标识符。这样可使程序更易理解以及更易维护。

(13) 涉及构建器和异常的时候,通常希望重新丢弃在构建器中捕获的任何异常--如果它造成了那个对象的创建失败。这样一来,调用者就不会以为那个对象已正确地创建,从而盲目地继续。

(14) 当客户程序员用完对象以后,若你的类要求进行任何清除工作,可考虑将清除代码置于一个良好定义的方法里,采用类似于cleanup()这样的名字,明确表明自己的用途。除此以外,可在类内放置一个boolean(布尔)标记,指出对象是否已被清除。在类的finalize()方法里,请确定对象已被清除,并已丢弃了从RuntimeException继承的一个类(如果还没有的话),从而指出一个编程错误。在采取象这样的方案之前,请确定finalize()能够在自己的系统中工作(可能需要调用System.runFinalizersOnExit(true),从而确保这一行为)。

(15) 在一个特定的作用域内,若一个对象必须清除(非由垃圾收集机制处理),请采用下述方法:初始化对象;若成功,则立即进入一个含有finally从句的try块,开始清除工作。

(16) 若在初始化过程中需要覆盖(取消)finalize(),请记住调用super.finalize()(若Object属于我们的直接超类,则无此必要)。在对finalize()进行覆盖的过程中,对super.finalize()的调用应属于最后一个行动,而不应是第一个行动,这样可确保在需要基础类组件的时候它们依然有效。

(17) 创建大小固定的对象集合时,请将它们传输至一个数组(若准备从一个方法里返回这个集合,更应如此操作)。这样一来,我们就可享受到数组在编译期进行类型检查的好处。此外,为使用它们,数组的接收者也许并不需要将对象"造型"到数组里。

(18) 尽量使用interfaces,不要使用abstract类。若已知某样东西准备成为一个基础类,那么第一个选择应是将其变成一个interface(接口)。只有在不得不使用方法定义或者成员变量的时候,才需要将其变成一个abstract(抽象)类。接口主要描述了客户希望做什么事情,而一个类则致力于(或允许)具体的实施细节。

(19) 在构建器内部,只进行那些将对象设为正确状态所需的工作。尽可能地避免调用其他方法,因为那些方法可能被其他人覆盖或取消,从而在构建过程中产生不可预知的结果(参见第7章的详细说明)。

(20) 对象不应只是简单地容纳一些数据;它们的行为也应得到良好的定义。

(21) 在现成类的基础上创建新类时,请首先选择"新建"或"创作"。只有自己的设计要求必须继承时,才应考虑这方面的问题。若在本来允许新建的场合使用了继承,则整个设计会变得没有必要地复杂。

(22) 用继承及方法覆盖来表示行为间的差异,而用字段表示状态间的区别。一个非常极端的例子是通过对不同类的继承来表示颜色,这是绝对应该避免的:应直接使用一个"颜色"字段。

(23) 为避免编程时遇到麻烦,请保证在自己类路径指到的任何地方,每个名字都仅对应一个类。否则,编译器可能先找到同名的另一个类,并报告出错消息。若怀疑自己碰到了类路径问题,请试试在类路径的每一个起点,搜索一下同名的.class文件。

(24) 在Java 1.1 AWT中使用事件"适配器"时,特别容易碰到一个陷阱。若覆盖了某个适配器方法,同时拼写方法没有特别讲究,最后的结果就是新添加一个方法,而不是覆盖现成方法。然而,由于这样做是完全合法的,所以不会从编译器或运行期系统获得任何出错提示--只不过代码的工作就变得不正常了。

(25) 用合理的设计方案消除"伪功能"。也就是说,假若只需要创建类的一个对象,就不要提前限制自己使用应用程序,并加上一条"只生成其中一个"注释。请考虑将其封装成一个"独生子"的形式。若在主程序里有大量散乱的代码,用于创建自己的对象,请考虑采纳一种创造性的方案,将些代码封装起来。

(26) 警惕"分析瘫痪"。请记住,无论如何都要提前了解整个项目的状况,再去考察其中的细节。由于把握了全局,可快速认识自己未知的一些因素,防止在考察细节的时候陷入"死逻辑"中。

(27) 警惕"过早优化"。首先让它运行起来,再考虑变得更快--但只有在自己必须这样做、而且经证实在某部分代码中的确存在一个性能瓶颈的时候,才应进行优化。除非用专门的工具分析瓶颈,否则很有可能是在浪费自己的时间。性能提升的隐含代价是自己的代码变得难于理解,而且难于维护。

(28) 请记住,阅读代码的时间比写代码的时间多得多。思路清晰的设计可获得易于理解的程序,但注释、细致的解释以及一些示例往往具有不可估量的价值。无论对你自己,还是对后来的人,它们都是相当重要的。如对此仍有怀疑,那么请试想自己试图从联机Java文档里找出有用信息时碰到的挫折,这样或许能将你说服。

(29) 如认为自己已进行了良好的分析、设计或者实施,那么请稍微更换一下思维角度。试试邀请一些外来人士--并不一定是专家,但可以是来自本公司其他部门的人。请他们用完全新鲜的眼光考察你的工作,看看是否能找出你一度熟视无睹的问题。采取这种方式,往往能在最适合修改的阶段找出一些关键性的问题,避免产品发行后再解决问题而造成的金钱及精力方面的损失。

(30) 良好的设计能带来最大的回报。简言之,对于一个特定的问题,通常会花较长的时间才能找到一种最恰当的解决方案。但一旦找到了正确的方法,以后的工作就轻松多了,再也不用经历数小时、数天或者数月的痛苦挣扎。我们的努力工作会带来最大的回报(甚至无可估量)。而且由于自己倾注了大量心血,最终获得一个出色的设计方案,成功的快感也是令人心动的。坚持抵制草草完工的诱惑--那样做往往得不偿失

(3) 对于自己创建的每一个类,都考虑置入一个main(),其中包含了用于测试那 个类的代码。为使用一个项目中的类,我们没必要删除测试代码。若进行了任 何形式的改动,可方便地返回测试。这些代码也可作为如何使用类的一个示例 使用。

this is absolutly bad!

(4) 应将方法设计成简要的、功能性单元,用它描述和实现一个不连续的类接 口部分。理想情况下,方法应简明扼要。若长度很大,可考虑通过某种方式将 其分割成较短的几个方法。这样做也便于类内代码的重复使用(有些时候,方 法必须非常大,但它们仍应只做同样的一件事情)。

(5) 设计一个类时,请设身处地为客户程序员考虑一下(类的使用方法应该是 非常明确的)。然后,再设身处地为管理代码的人考虑一下(预计有可能进行 哪些形式的修改,想想用什么方法可把它们变得更简单)。
(6) 使类尽可能短小精悍,而且只解决一个特定的问题。下面是对类设计的一 些建议:
■一个复杂的开关语句:考虑采用"多形"机制
■数量众多的方法涉及到类型差别极大的操作:考虑用几个类来分别实现
■许多成员变量在特征上有很大的差别:考虑使用几个类

(7) 让一切东西都尽可能地"私有"--private。可使库的某一部分"公共化"(一个 方法、类或者一个字段等等),就永远不能把它拿出。若强行拿出,就可能破 坏其他人现有的代码,使他们不得不重新编写和设计。若只公布自己必须公布 的,就可放心大胆地改变其他任何东西。在多线程环境中,隐私是特别重要的 一个因素--只有private字段才能在非同步使用的情况下受到保护。

not necessary , pretotect or package level also fine in most case

(8) 谨惕"巨大对象综合症"。对一些习惯于顺序编程思维、且初涉OOP领域的新 手,往往喜欢先写一个顺序执行的程序,再把它嵌入一个或两个巨大的对象 里。根据编程原理,对象表达的应该是应用程序的概念,而非应用程序本身。

(9) 若不得已进行一些不太雅观的编程,至少应该把那些代码置于一个类的内 部。

(10) 任何时候只要发现类与类之间结合得非常紧密,就需要考虑是否采用内部 类,从而改善编码及维护工作(参见第14章14.1.2小节的"用内部类改进代 码")。

(11) 尽可能细致地加上注释,并用javadoc注释文档语法生成自己的程序文档。

(12) 避免使用"魔术数字",这些数字很难与代码很好地配合。如以后需要修改 它,无疑会成为一场噩梦,因为根本不知道"100"到底是指"数组大小"还是"其 他全然不同的东西"。所以,我们应创建一个常数,并为其使用具有说服力的描 述性名称,并在整个程序中都采用常数标识符。这样可使程序更易理解以及更 易维护。

(13) 涉及构建器和异常的时候,通常希望重新丢弃在构建器中捕获的任何异常- -如果它造成了那个对象的创建失败。这样一来,调用者就不会以为那个对象已 正确地创建,从而盲目地继续。

(14) 当客户程序员用完对象以后,若你的类要求进行任何清除工作,可考虑将 清除代码置于一个良好定义的方法里,采用类似于cleanup()这样的名字,明确 表明自己的用途。除此以外,可在类内放置一个boolean(布尔)标记,指出 对象是否已被清除。在类的finalize()方法里,请确定对象已被清除,并已丢弃 了从RuntimeException继承的一个类(如果还没有的话),从而指出一个编程 错误。在采取象这样的方案之前,请确定finalize()能够在自己的系统中工作 (可能需要调用System.runFinalizersOnExit(true),从而确保这一行为)。

(15) 在一个特定的作用域内,若一个对象必须清除(非由垃圾收集机制处 理),请采用下述方法:初始化对象;若成功,则立即进入一个含有finally从 句的try块,开始清除工作。

(16) 若在初始化过程中需要覆盖(取消)finalize(),请记住调用 super.finalize()(若Object属于我们的直接超类,则无此必要)。在对finalize() 进行覆盖的过程中,对super.finalize()的调用应属于最后一个行动,而不应是第 一个行动,这样可确保在需要基础类组件的时候它们依然有效。

(17) 创建大小固定的对象集合时,请将它们传输至一个数组(若准备从一个方 法里返回这个集合,更应如此操作)。这样一来,我们就可享受到数组在编译 期进行类型检查的好处。此外,为使用它们,数组的接收者也许并不需要将对 象"造型"到数组里。

(18) 尽量使用interfaces,不要使用abstract类。若已知某样东西准备成为一个 基础类,那么第一个选择应是将其变成一个interface(接口)。只有在不得不 使用方法定义或者成员变量的时候,才需要将其变成一个abstract(抽象) 类。接口主要描述了客户希望做什么事情,而一个类则致力于(或允许)具体 的实施细节。

they are total diffrent ,

(19) 在构建器内部,只进行那些将对象设为正确状态所需的工作。尽可能地避 免调用其他方法,因为那些方法可能被其他人覆盖或取消,从而在构建过程中 产生不可预知的结果(参见第7章的详细说明)。

(20) 对象不应只是简单地容纳一些数据;它们的行为也应得到良好的定义。

(21) 在现成类的基础上创建新类时,请首先选择"新建"或"创作"。只有自己的设 计要求必须继承时,才应考虑这方面的问题。若在本来允许新建的场合使用了 继承,则整个设计会变得没有必要地复杂。

(22) 用继承及方法覆盖来表示行为间的差异,而用字段表示状态间的区别。一 个非常极端的例子是通过对不同类的继承来表示颜色,这是绝对应该避免的: 应直接使用一个"颜色"字段。

(23) 为避免编程时遇到麻烦,请保证在自己类路径指到的任何地方,每个名字 都仅对应一个类。否则,编译器可能先找到同名的另一个类,并报告出错消 息。若怀疑自己碰到了类路径问题,请试试在类路径的每一个起点,搜索一下 同名的.class文件。

classpath is not that simple

(24) 在Java 1.1 AWT中使用事件"适配器"时,特别容易碰到一个陷阱。若覆盖了 某个适配器方法,同时拼写方法没有特别讲究,最后的结果就是新添加一个方 法,而不是覆盖现成方法。然而,由于这样做是完全合法的,所以不会从编译 器或运行期系统获得任何出错提示--只不过代码的工作就变得不正常了。

(25) 用合理的设计方案消除"伪功能"。也就是说,假若只需要创建类的一个对 象,就不要提前限制自己使用应用程序,并加上一条"只生成其中一个"注释。 请考虑将其封装成一个"独生子"的形式。若在主程序里有大量散乱的代码,用 于创建自己的对象,请考虑采纳一种创造性的方案,将些代码封装起来。

(26) 警惕"分析瘫痪"。请记住,无论如何都要提前了解整个项目的状况,再去 考察其中的细节。由于把握了全局,可快速认识自己未知的一些因素,防止在 考察细节的时候陷入"死逻辑"中。

(27) 警惕"过早优化"。首先让它运行起来,再考虑变得更快--但只有在自己必须 这样做、而且经证实在某部分代码中的确存在一个性能瓶颈的时候,才应进行 优化。除非用专门的工具分析瓶颈,否则很有可能是在浪费自己的时间。性能 提升的隐含代价是自己的代码变得难于理解,而且难于维护。

but know early and design better at first is always necesary, or else
you die

(28) 请记住,阅读代码的时间比写代码的时间多得多。思路清晰的设计可获得 易于理解的程序,但注释、细致的解释以及一些示例往往具有不可估量的价 值。无论对你自己,还是对后来的人,它们都是相当重要的。如对此仍有怀 疑,那么请试想自己试图从联机Java文档里找出有用信息时碰到的挫折,这样 或许能将你说服。

(29) 如认为自己已进行了良好的分析、设计或者实施,那么请稍微更换一下思 维角度。试试邀请一些外来人士--并不一定是专家,但可以是来自本公司其他 部门的人。请他们用完全新鲜的眼光考察你的工作,看看是否能找出你一度熟 视无睹的问题。采取这种方式,往往能在最适合修改的阶段找出一些关键性的 问题,避免产品发行后再解决问题而造成的金钱及精力方面的损失。

(30) 良好的设计能带来最大的回报。简言之,对于一个特定的问题,通常会花 较长的时间才能找到一种最恰当的解决方案。但一旦找到了正确的方法,以后 的工作就轻松多了,再也不用经历数小时、数天或者数月的痛苦挣扎。我们的 努力工作会带来最大的回报(甚至无可估量)。而且由于自己倾注了大量心 血,最终获得一个出色的设计方案,成功的快感也是令人心动的。坚持抵制草 草完工的诱惑--那样做往往得不偿失


Java性能优化技巧集锦

Java性能优化技巧集锦

一、通用篇
  1.1 不用new关键词创建类的实例
  1.2 使用非阻塞I/O
  1.3 慎用异常
  1.4 不要重复初始化变量
  1.5 尽量指定类的final修饰符
  1.6 尽量使用局部变量
  1.7 乘法和除法
二、J2EE篇
  2.1 使用缓冲标记
  2.2 始终通过会话Bean访问实体Bean
  2.3 选择合适的引用机制
  2.4 在部署描述器中设置只读属性
  2.5 缓冲对EJB Home的访问
  2.6 为EJB实现本地接口
  2.7 生成主键
  2.8 及时清除不再需要的会话
  2.9 在JSP页面中关闭无用的会话
  2.10 Servlet与内存使用
  2.11 HTTP Keep-Alive
  2.12 JDBC与Unicode
  2.13 JDBC与I/O
  1.14 内存数据库
三、GUI篇
  3.1 用JAR压缩类文件
  3.2 提示Applet装入进程
  3.3 在画出图形之前预先装入它
  3.4 覆盖update方法
  3.5 延迟重画操作
  3.6 使用双缓冲区
  3.7 使用BufferedImage
  3.8 使用VolatileImage
  3.9 使用Window Blitting
四、补充资料
===================================
正文:
===================================
一、通用篇

“通用篇”讨论的问题适合于大多数Java应用。

1.1 不用new关键词创建类的实例

用new关键词创建类的实例时,构造函数链中的所有构造函数都会被自动调用。但如果一个对象实现了Cloneable接口,我们可以调用它的clone()方法。clone()方法不会调用任何类构造函数。

在使用设计模式(Design Pattern)的场合,如果用Factory模式创建对象,则改用clone()方法创建新的对象实例非常简单。例如,下面是Factory模式的一个典型实现:


public static Credit getNewCredit() {
return new Credit();
}

改进后的代码使用clone()方法,如下所示:


private static Credit BaseCredit = new Credit();
public static Credit getNewCredit() {
return (Credit) BaseCredit.clone();
}

上面的思路对于数组处理同样很有用。

1.2 使用非阻塞I/O

版本较低的JDK不支持非阻塞I/O API。为避免I/O阻塞,一些应用采用了创建大量线程的办法(在较好的情况下,会使用一个缓冲池)。这种技术可以在许多必须支持并发I/O流的应用中见到,如Web服务器、报价和拍卖应用等。然而,创建Java线程需要相当可观的开销。

JDK 1.4引入了非阻塞的I/O库(java.nio)。如果应用要求使用版本较早的JDK,在这里有一个支持非阻塞I/O的软件包。

请参见Sun中国网站的《调整Java的I/O性能》。

1.3 慎用异常

异常对性能不利。抛出异常首先要创建一个新的对象。Throwable接口的构造函数调用名为fillInStackTrace()的本地(Native)方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,VM就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。

异常只能用于错误处理,不应该用来控制程序流程。

1.4 不要重复初始化变量

默认情况下,调用类的构造函数时, Java会把变量初始化成确定的值:所有的对象被设置成null,整数变量(byte、short、int、long)设置成0,float和 double变量设置成0.0,逻辑值设置成false。当一个类从另一个类派生时,这一点尤其应该注意,因为用new关键词创建一个对象时,构造函数链中的所有构造函数都会被自动调用。

1.5 尽量指定类的final修饰符

带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String。为String类指定final防止了人们覆盖length()方法。

另外,如果指定一个类为final,则该类所有的方法都是final。Java编译器会寻找机会内联(inline)所有的final方法(这和具体的编译器实现有关)。此举能够使性能平均提高50%。

1.6 尽量使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快。其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。另外,依赖于具体的编译器/JVM,局部变量还可能得到进一步优化。请参见《尽可能使用堆栈变量》。

1.7 乘法和除法

考虑下面的代码:


for (val = 0; val < 100000; val +=5) { alterX = val * 8; myResult = val * 2; }

用移位操作替代乘法操作可以极大地提高性能。下面是修改后的代码:


for (val = 0; val < 100000; val += 5) { alterX = val << 3; myResult = val << 1; }

修改后的代码不再做乘以8的操作,而是改用等价的左移3位操作,每左移1位相当于乘以2。相应地,右移1位操作相当于除以2。值得一提的是,虽然移位操作速度快,但可能使代码比较难于理解,所以最好加上一些注释。

二、J2EE篇

前面介绍的改善性能技巧适合于大多数Java应用,接下来要讨论的问题适合于使用JSP、EJB或JDBC的应用。

2.1 使用缓冲标记

一些应用服务器加入了面向JSP的缓冲标记功能。例如,BEA的WebLogic Server从6.0版本开始支持这个功能,Open Symphony工程也同样支持这个功能。JSP缓冲标记既能够缓冲页面片断,也能够缓冲整个页面。当JSP页面执行时,如果目标片断已经在缓冲之中,则生成该片断的代码就不用再执行。页面级缓冲捕获对指定URL的请求,并缓冲整个结果页面。对于购物篮、目录以及门户网站的主页来说,这个功能极其有用。对于这类应用,页面级缓冲能够保存页面执行的结果,供后继请求使用。

对于代码逻辑复杂的页面,利用缓冲标记提高性能的效果比较明显;反之,效果可能略逊一筹。

请参见《用缓冲技术提高JSP应用的性能和稳定性》。

2.2 始终通过会话Bean访问实体Bean

直接访问实体Bean不利于性能。当客户程序远程访问实体Bean时,每一个get方法都是一个远程调用。访问实体Bean的会话Bean是本地的,能够把所有数据组织成一个结构,然后返回它的值。

用会话Bean封装对实体Bean的访问能够改进事务管理,因为会话Bean只有在到达事务边界时才会提交。每一个对get方法的直接调用产生一个事务,容器将在每一个实体Bean的事务之后执行一个“装入-读取”操作。

一些时候,使用实体Bean会导致程序性能不佳。如果实体Bean的唯一用途就是提取和更新数据,改成在会话Bean之内利用JDBC访问数据库可以得到更好的性能。

2.3 选择合适的引用机制

在典型的JSP应用系统中,页头、页脚部分往往被抽取出来,然后根据需要引入页头、页脚。当前,在JSP页面中引入外部资源的方法主要有两种:include指令,以及include动作。


include指令:例如<%@ include file="copyright.html" %>。该指令在编译时引入指定的资源。在编译之前,带有include指令的页面和指定的资源被合并成一个文件。被引用的外部资源在编译时就确定,比运行时才确定资源更高效。
include动作:例如<jsp:include page="copyright.jsp" />。该动作引入指定页面执行后生成的结果。由于它在运行时完成,因此对输出结果的控制更加灵活。但时,只有当被引用的内容频繁地改变时,或者在对主页面的请求没有出现之前,被引用的页面无法确定时,使用include动作才合算。
2.4 在部署描述器中设置只读属性

实体Bean的部署描述器允许把所有get方法设置成“只读”。当某个事务单元的工作只包含执行读取操作的方法时,设置只读属性有利于提高性能,因为容器不必再执行存储操作。

2.5 缓冲对EJB Home的访问

EJB Home接口通过JNDI名称查找获得。这个操作需要相当可观的开销。JNDI查找最好放入Servlet的init()方法里面。如果应用中多处频繁地出现EJB访问,最好创建一个EJBHomeCache类。EJBHomeCache类一般应该作为singleton实现。

2.6 为EJB实现本地接口

本地接口是EJB 2.0规范新增的内容,它使得Bean能够避免远程调用的开销。请考虑下面的代码。


PayBeanHome home = (PayBeanHome)
javax.rmi.PortableRemoteObject.narrow
(ctx.lookup ("PayBeanHome"), PayBeanHome.class);
PayBean bean = (PayBean)
javax.rmi.PortableRemoteObject.narrow
(home.create(), PayBean.class);


第一个语句表示我们要寻找Bean的Home接口。这个查找通过JNDI进行,它是一个RMI调用。然后,我们定位远程对象,返回代理引用,这也是一个 RMI调用。第二个语句示范了如何创建一个实例,涉及了创建IIOP请求并在网络上传输请求的stub程序,它也是一个RMI调用。

要实现本地接口,我们必须作如下修改:


方法不能再抛出java.rmi.RemoteException异常,包括从RemoteException派生的异常,比如 TransactionRequiredException、TransactionRolledBackException和 NoSuchObjectException。EJB提供了等价的本地异常,如TransactionRequiredLocalException、 TransactionRolledBackLocalException和NoSuchObjectLocalException。
所有数据和返回值都通过引用的方式传递,而不是传递值。
本地接口必须在EJB部署的机器上使用。简而言之,客户程序和提供服务的组件必须在同一个JVM上运行。
如果Bean实现了本地接口,则其引用不可串行化。
请参见《用本地引用提高EJB访问效率》。

2.7 生成主键

在EJB之内生成主键有许多途径,下面分析了几种常见的办法以及它们的特点。

利用数据库内建的标识机制(SQL Server的IDENTITY或Oracle的SEQUENCE)。这种方法的缺点是EJB可移植性差。

由实体Bean自己计算主键值(比如做增量操作)。它的缺点是要求事务可串行化,而且速度也较慢。

利用NTP之类的时钟服务。这要求有面向特定平台的本地代码,从而把Bean固定到了特定的OS之上。另外,它还导致了这样一种可能,即在多CPU的服务器上,同一个毫秒之内生成了两个主键。

借鉴Microsoft的思路,在Bean中创建一个GUID。然而,如果不求助于JNI,Java不能确定网卡的MAC地址;如果使用JNI,则程序就要依赖于特定的OS。

还有其他几种办法,但这些办法同样都有各自的局限。似乎只有一个答案比较理想:结合运用RMI和JNDI。先通过RMI注册把RMI远程对象绑定到JNDI树。客户程序通过JNDI进行查找。下面是一个例子:


public class keyGenerator extends UnicastRemoteObject implements Remote {
private static long KeyValue = System.currentTimeMillis();
public static synchronized long getKey() throws RemoteException { return KeyValue++; }


2.8 及时清除不再需要的会话

为了清除不再活动的会话,许多应用服务器都有默认的会话超时时间,一般为30分钟。当应用服务器需要保存更多会话时,如果内存容量不足,操作系统会把部分内存数据转移到磁盘,应用服务器也可能根据“最近最频繁使用”(Most Recently Used)算法把部分不活跃的会话转储到磁盘,甚至可能抛出“内存不足”异常。在大规模系统中,串行化会话的代价是很昂贵的。当会话不再需要时,应当及时调用HttpSession.invalidate()方法清除会话。HttpSession.invalidate()方法通常可以在应用的退出页面调用。

2.9 在JSP页面中关闭无用的会话

对于那些无需跟踪会话状态的页面,关闭自动创建的会话可以节省一些资源。使用如下page指令:


<%@ page session="false"%>

2.10 Servlet与内存使用

许多开发者随意地把大量信息保存到用户会话之中。一些时候,保存在会话中的对象没有及时地被垃圾回收机制回收。从性能上看,典型的症状是用户感到系统周期性地变慢,却又不能把原因归于任何一个具体的组件。如果监视JVM的堆空间,它的表现是内存占用不正常地大起大落。

解决这类内存问题主要有二种办法。第一种办法是,在所有作用范围为会话的Bean中实现HttpSessionBindingListener接口。这样,只要实现valueUnbound()方法,就可以显式地释放Bean使用的资源。

另外一种办法就是尽快地把会话作废。大多数应用服务器都有设置会话作废间隔时间的选项。另外,也可以用编程的方式调用会话的 setMaxInactiveInterval()方法,该方法用来设定在作废会话之前,Servlet容器允许的客户请求的最大间隔时间,以秒计。

2.11 HTTP Keep-Alive

Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。市场上的大部分Web服务器,包括iPlanet、IIS和Apache,都支持HTTP Keep-Alive。对于提供静态内容的网站来说,这个功能通常很有用。但是,对于负担较重的网站来说,这里存在另外一个问题:虽然为客户保留打开的连接有一定的好处,但它同样影响了性能,因为在处理暂停期间,本来可以释放的资源仍旧被占用。当Web服务器和应用服务器在同一台机器上运行时,Keep- Alive功能对资源利用的影响尤其突出。

2.12 JDBC与Unicode

想必你已经了解一些使用JDBC时提高性能的措施,比如利用连接池、正确地选择存储过程和直接执行的SQL、从结果集删除多余的列、预先编译SQL语句,等等。

除了这些显而易见的选择之外,另一个提高性能的好选择可能就是把所有的字符数据都保存为Unicode(代码页13488)。Java以Unicode形式处理所有数据,因此,数据库驱动程序不必再执行转换过程。但应该记住:如果采用这种方式,数据库会变得更大,因为每个Unicode字符需要2个字节存储空间。另外,如果有其他非Unicode的程序访问数据库,性能问题仍旧会出现,因为这时数据库驱动程序仍旧必须执行转换过程。

2.13 JDBC与I/O

如果应用程序需要访问一个规模很大的数据集,则应当考虑使用块提取方式。默认情况下,JDBC每次提取32行数据。举例来说,假设我们要遍历一个5000 行的记录集,JDBC必须调用数据库157次才能提取到全部数据。如果把块大小改成512,则调用数据库的次数将减少到10次。

在一些情形下这种技术无效。例如,如果使用可滚动的记录集,或者在查询中指定了FOR UPDATE,则块操作方式不再有效。

1.14 内存数据库

许多应用需要以用户为单位在会话对象中保存相当数量的数据,典型的应用如购物篮和目录等。由于这类数据可以按照行/列的形式组织,因此,许多应用创建了庞大的Vector或HashMap。在会话中保存这类数据极大地限制了应用的可伸缩性,因为服务器拥有的内存至少必须达到每个会话占用的内存数量乘以并发用户最大数量,它不仅使服务器价格昂贵,而且垃圾收集的时间间隔也可能延长到难以忍受的程度。

一些人把购物篮/目录功能转移到数据库层,在一定程度上提高了可伸缩性。然而,把这部分功能放到数据库层也存在问题,且问题的根源与大多数关系数据库系统的体系结构有关。对于关系数据库来说,运行时的重要原则之一是确保所有的写入操作稳定、可靠,因而,所有的性能问题都与物理上把数据写入磁盘的能力有关。关系数据库力图减少I/O操作,特别是对于读操作,但实现该目标的主要途径只是执行一套实现缓冲机制的复杂算法,而这正是数据库层第一号性能瓶颈通常总是 CPU的主要原因。

一种替代传统关系数据库的方案是,使用在内存中运行的数据库(In-memory Database),例如TimesTen。内存数据库的出发点是允许数据临时地写入,但这些数据不必永久地保存到磁盘上,所有的操作都在内存中进行。这样,内存数据库不需要复杂的算法来减少I/O操作,而且可以采用比较简单的加锁机制,因而速度很快。

三、GUI篇

这一部分介绍的内容适合于图形用户界面的应用(Applet和普通应用),要用到AWT或Swing。

3.1 用JAR压缩类文件

Java档案文件(JAR文件)是根据JavaBean标准压缩的文件,是发布JavaBean组件的主要方式和推荐方式。JAR档案有助于减少文件体积,缩短下载时间。例如,它有助于Applet提高启动速度。一个JAR文件可以包含一个或者多个相关的Bean以及支持文件,比如图形、声音、HTML 和其他资源。

要在HTML/JSP文件中指定JAR文件,只需在Applet标记中加入ARCHIVE = "name.jar"声明。

请参见《使用档案文件提高 applet 的加载速度》。

3.2 提示Applet装入进程

你是否看到过使用Applet的网站,注意到在应该运行Applet的地方出现了一个占位符?当Applet的下载时间较长时,会发生什么事情?最大的可能就是用户掉头离去。在这种情况下,显示一个Applet正在下载的信息无疑有助于鼓励用户继续等待。

下面我们来看看一种具体的实现方法。首先创建一个很小的Applet,该Applet负责在后台下载正式的Applet:


import java.applet.Applet;
import java.applet.AppletStub;
import java.awt.Label;
import java.awt.Graphics;
import java.awt.GridLayout;
public class PreLoader extends Applet implements Runnable, AppletStub {
String largeAppletName;
Label label;
public void init() {
// 要求装载的正式Applet
largeAppletName = getParameter("applet");
// “请稍等”提示信息
label = new Label("请稍等..." + largeAppletName);
add(label);
}
public void run(){
try {
// 获得待装载Applet的类
Class largeAppletClass = Class.forName(largeAppletName);
// 创建待装载Applet的实例
Applet largeApplet = (Applet)largeAppletClass.newInstance();
// 设置该Applet的Stub程序
largeApplet.setStub(this);
// 取消“请稍等”信息
remove(label);
// 设置布局
setLayout(new GridLayout(1, 0));
add(largeApplet);
// 显示正式的Applet
largeApplet.init();
largeApplet.start();
}
catch (Exception ex) {
// 显示错误信息
label.setText("不能装入指定的Applet");
}
// 刷新屏幕
validate();
}
public void appletResize(int width, int height) {
// 把appletResize调用从stub程序传递到Applet
resize(width, height);
}
}

编译后的代码小于2K,下载速度很快。代码中有几个地方值得注意。首先,PreLoader实现了AppletStub接口。一般地,Applet从调用者判断自己的codebase。在本例中,我们必须调用setStub()告诉Applet到哪里提取这个信息。另一个值得注意的地方是, AppletStub接口包含许多和Applet类一样的方法,但appletResize()方法除外。这里我们把对appletResize()方法的调用传递给了resize()方法。

3.3 在画出图形之前预先装入它

ImageObserver接口可用来接收图形装入的提示信息。ImageObserver接口只有一个方法imageUpdate(),能够用一次repaint()操作在屏幕上画出图形。下面提供了一个例子。


public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) {
if ((flags & ALLBITS) !=0 {
repaint();
}
else if (flags & (ERROR |ABORT )) != 0) {
error = true;
// 文件没有找到,考虑显示一个占位符
repaint();
}
return (flags & (ALLBITS | ERROR| ABORT)) == 0;
}

当图形信息可用时,imageUpdate()方法被调用。如果需要进一步更新,该方法返回true;如果所需信息已经得到,该方法返回false。

3.4 覆盖update方法

update()方法的默认动作是清除屏幕,然后调用paint()方法。如果使用默认的update()方法,频繁使用图形的应用可能出现显示闪烁现象。要避免在paint()调用之前的屏幕清除操作,只需按照如下方式覆盖update()方法:

public void update(Graphics g) {
paint(g);
}

更理想的方案是:覆盖update(),只重画屏幕上发生变化的区域,如下所示:

public void update(Graphics g) {
g.clipRect(x, y, w, h);
paint(g);
}


3.5 延迟重画操作

对于图形用户界面的应用来说,性能低下的主要原因往往可以归结为重画屏幕的效率低下。当用户改变窗口大小或者滚动一个窗口时,这一点通常可以很明显地观察到。改变窗口大小或者滚动屏幕之类的操作导致重画屏幕事件大量地、快速地生成,甚至超过了相关代码的执行速度。对付这个问题最好的办法是忽略所有“迟到” 的事件。

建议在这里引入一个数毫秒的时差,即如果我们立即接收到了另一个重画事件,可以停止处理当前事件转而处理最后一个收到的重画事件;否则,我们继续进行当前的重画过程。

如果事件要启动一项耗时的工作,分离出一个工作线程是一种较好的处理方式;否则,一些部件可能被“冻结”,因为每次只能处理一个事件。下面提供了一个事件处理的简单例子,但经过扩展后它可以用来控制工作线程。


public static void runOnce(String id, final long milliseconds) {
synchronized(e_queue) { // e_queue: 所有事件的集合
if (!e_queue.containsKey(id)) {
e_queue.put(token, new LastOne());
}
}
final LastOne lastOne = (LastOne) e_queue.get(token);
final long time = System.currentTimeMillis(); // 获得当前时间
lastOne.time = time;
(new Thread() {public void run() {
if (milliseconds > 0) {
try {Thread.sleep(milliseconds);} // 暂停线程
catch (Exception ex) {}
}
synchronized(lastOne.running) { // 等待上一事件结束
if (lastOne.time != time) // 只处理最后一个事件
return;
}
}}).start();
}
private static Hashtable e_queue = new Hashtable();
private static class LastOne {
public long time=0;
public Object running = new Object();
}


3.6 使用双缓冲区

在屏幕之外的缓冲区绘图,完成后立即把整个图形显示出来。由于有两个缓冲区,所以程序可以来回切换。这样,我们可以用一个低优先级的线程负责画图,使得程序能够利用空闲的CPU时间执行其他任务。下面的伪代码片断示范了这种技术。


Graphics myGraphics;
Image myOffscreenImage = createImage(size().width, size().height);
Graphics offscreenGraphics = myOffscreenImage.getGraphics();
offscreenGraphics.drawImage(img, 50, 50, this);
myGraphics.drawImage(myOffscreenImage, 0, 0, this);


3.7 使用BufferedImage

Java JDK 1.2使用了一个软显示设备,使得文本在不同的平台上看起来相似。为实现这个功能,Java必须直接处理构成文字的像素。由于这种技术要在内存中大量地进行位复制操作,早期的JDK在使用这种技术时性能不佳。为解决这个问题而提出的Java标准实现了一种新的图形类型,即BufferedImage。

BufferedImage子类描述的图形带有一个可访问的图形数据缓冲区。一个BufferedImage包含一个ColorModel和一组光栅图形数据。这个类一般使用RGB(红、绿、蓝)颜色模型,但也可以处理灰度级图形。它的构造函数很简单,如下所示:
public BufferedImage (int width, int height, int imageType)

ImageType允许我们指定要缓冲的是什么类型的图形,比如5-位RGB、8-位RGB、灰度级等。

3.8 使用VolatileImage

许多硬件平台和它们的操作系统都提供基本的硬件加速支持。例如,硬件加速一般提供矩形填充功能,和利用CPU完成同一任务相比,硬件加速的效率更高。由于硬件加速分离了一部分工作,允许多个工作流并发进行,从而缓解了对CPU和系统总线的压力,使得应用能够运行得更快。利用VolatileImage可以创建硬件加速的图形以及管理图形的内容。由于它直接利用低层平台的能力,性能的改善程度主要取决于系统使用的图形适配器。VolatileImage的内容随时可能丢失,也即它是“不稳定的(volatile)”。因此,在使用图形之前,最好检查一下它的内容是否丢失。VolatileImage有两个能够检查内容是否丢失的方法:

public abstract int validate(GraphicsConfiguration gc);
public abstract Boolean contentsLost();

每次从VolatileImage对象复制内容或者写入VolatileImage时,应该调用validate()方法。contentsLost()方法告诉我们,自从最后一次validate()调用之后,图形的内容是否丢失。

虽然VolatileImage是一个抽象类,但不要从它这里派生子类。VolatileImage应该通过 Component.createVolatileImage()或者 GraphicsConfiguration.createCompatibleVolatileImage()方法创建。

3.9 使用Window Blitting

进行滚动操作时,所有可见的内容一般都要重画,从而导致大量不必要的重画工作。许多操作系统的图形子系统,包括WIN32 GDI、MacOS和X/Windows,都支持Window Blitting技术。Window Blitting技术直接在屏幕缓冲区中把图形移到新的位置,只重画新出现的区域。要在Swing应用中使用Window Blitting技术,设置方法如下:

setScrollMode(int mode);

在大多数应用中,使用这种技术能够提高滚动速度。只有在一种情形下,Window Blitting会导致性能降低,即应用在后台进行滚动操作。如果是用户在滚动一个应用,那么它总是在前台,无需担心任何负面影响。

Java编程思想读书笔记(对象)

对象的存储:

对象的存储区域有:寄存器(Registers)、栈(Stack)、堆(Heap)、静态存储空间(Static Storage)、常量存储空间(Constant storage)、Non-RAM存储空间。

  • 寄存器:寄存器位于处理器内部,由于寄存器个数有限,编译器根据本身需求适当地分配寄存器使用。
  • 栈:此里用来存储对像的引用和基本型别的变量。基本型别包括:boolean,char,byte,short,int,long,float,doule,void。
  • 堆:此里用来存储所有的Java对象。栈里的所有关于对像的引用均指定堆里的具体对象。
  • 静态存储空间:用来存储对象内的特定静态成员,此静态成员是用static变量声明的。但Java对象绝无可能置于静态存储空间中。
  • 常量存储空间:用来存储常量,常量也可存于ROM只读内存中。
  • Non-RAM存储空间:用来存储串流化对象(streamed objects)和持久性对象(persistent objects)。

基本数据类型:

基本型别包括:boolean,char,byte,short,int,long,float,doule,void。(string 属于对象,不属于基本类别。)而其对应的外覆型分别是:Boolean,Character,Byte,Short,Integer,Long,Folat,Double,Void。
别外,Java还提供了两个高精度计算的Classes:BigInteger(可以精确表示任意长度整数数值,不会在运算过程中丧失任何信息)和BigDecimal(提供任意精度的定点数)。虽然它们也可以视为外覆类,但两者都没有对应的基本型别。
缺省值:当Class的某个成员属于基本型别时,即使没有为它提供初值,Java仍保证它有一个缺省值,缺省值如下:
boolean: false
char :'/u0000/(null)
byte : (byte)0
short : (short)0
int : 0
long : 0L
float : 0.0f
double 0.0d
但是:只有当变量身份是“Class内的成员”时,Java才保证为该变量提供初值。但当变量属于局域变量(如位于某个函数内时)时,Java并不提供初始值。如:int x 。x可能是任意值(和C/C++)中的一样,不会被自动设为0.
所有数组的初始值为null。

垃圾回收:
当一个对象不再被引用后,其Reference会在栈内消失,当垃圾收集器在堆内检测对象,发现有的对象不再有Reference引用指向它时,就会把它销毁。


游戏框架设计Ⅰ—— 游戏中的事件机制

游戏框架设计Ⅰ—— 游戏中的事件机制
 
 
  事件机制在很多高级程序设计语言中都有支持。譬如VB、C#(delegate)、C++Builder(并不属于C++的范畴。C++Builder中的事件处理器必须用关键字closure<闭包>修饰)等等,甚至在HTML中也可以见到它的身影。事件机制的引入使软件系统变得更加易于理解——它使一种语言(平台)更加接近于这个世界的真相。事情的发展变得像现实世界中那样顺理成章。某一事件的产生引发了一系列其他事件的产生,这些事件要么是结果要么又会引发一系列事件的产生......如此这般,信息才得以在事件的新陈代谢中延续,世界才得以向前发展。在某些游戏设计过程中的一项重要任务就是模拟现实世界的某些特征,以期实现机器与用户的更加亲密的沟通。事件机制就是很好的一例。我们需要事件来使我们的系统更加人性化。

  我想,在我继续进行下面对讨论之前,先简单介绍一下"事件"这个东东。




1. 游戏中的事件机制

  联系是普遍存在的。事事有联系、时时有联系,整个世界是一个相互联系的统一整体。一个人的行为、物的状态的改变或事的进展过程的某一阶段可以引发一个事件。一个事件的发生或许会引发另外的事件——通过人的感知、大脑的反映,然后作出决策,付诸行动——也或许,就这么蒸发掉,无人知晓。但无论如何,在这一过程中,我们总能抽象出一些实质性的东西来。就像下面的图示:
 

在游戏中:

  事件源——表示任何可以引发事件的对象。譬如,一个"人"、"坦克"、"建筑物"、"地面"。

  事件——表示任何可以处理的事件。譬如,"感冒"、"射击"、"倒塌"、"有对象经过"。

  响应者——表示任何对某事件感兴趣的对象。

  响应器——表示对某事件感兴趣的对象对某一确定事件作出的反应。

特别的,对于过程:

  通知——发生在事件与响应者之间。我们把它分为两种方式:有限听众式、广播式。对事件感兴趣的对象(响应者)只有确定的有限个(只有一个的情况下,可以叫做点对点式)的情况就是有限听众式。而对于广播式,事件并不知道会有哪些(个)对象对自己感兴趣。它向所有可以接收事件通知的对象广播事件。

  触发——响应者发现自己对特定事件需要做出相应的行动时就会触发事件处理器,并同时传递需要的事件信息给它。对于响应者,它也可以选择沉默——自己了解事件但并不作出行动。因此这个过程的决定权在响应者手上。


2. 万事之鼻祖 Event

  我们需要一个类来表示所有事件的普遍性质。

public class Event {

// 属性

public string Name { get;set; }// 获取或设置事件的名称

public string Message { get;set; }// 获取或设置事件的简单描述

EventTypes EventType { get;set; }// 获取或设置事件类型(枚举EventTypes)

ListenerCollection Listeners { get; } // 获取响应者的集合

public bool PoolEvent { get;set; }// 获取或设置事件的简单描述

// 方法

void RaiseEvent(); // 通知响应者事件的发生

void AbandonListener( int index ); // 抛弃一个事件响应者,并把它从 Listeners 中移除。

void AbandonListener(); // 抛弃所有的事件响应者

}


3. 枚举类型 EventTypes

  这个枚举类型指示事件通知过程的类型:有限听众式、广播式。

public enum EventTypes {

LimitedListener ,

Broadcast

}


4. 响应者接口 IListener

  该接口只有唯一的方法 EventArrived() 。事件发生时会调用这个方法并传递相关参数。这个参数必须是 EventArgs 或由它派生而来。

public interface IListener {

// 通知一个响应者事件的到达。

void EventArrived( EventArgs args );

}


5. EventPool

  一个事件池。当且仅当需要事件广播时我们才需要它。需要注意的是 AddEvent 方法。它把一个事件添加到池中,第二个参数指定是否将该事件已经指定的响应者亦添加到广播的响应者中。事件添加后,其 Event::EventType 属性会被设置为 EventTypes.Broadcast。

public class EventPool {

// 属性

public ArrayList Events { get; }// 获取池中所有的事件的集合

public ListnerCollection Listners { get; }// 获取池中所有的响应者的集合

// 方法

void AddEvent( Event obj ,bool copyListners ); // 添加一个事件并把它作为广播式事件

void RemoveEventAt( int index ); // 将一个事件从列表中移除

void RemoveEvent( Event listener ); // 将一个事件从列表中移除

void Broadcast( Event event ); // 向列表中的所有响应者广播指定事件(可以是非池中的事件)

void BroadcastItemAt( int index ); // 向列表中的所有响应者广播池中的指定事件

}


6. EventArgs

public class EventArgs {

public Event Event { get; } // 获取传递这个参数的事件

public object Sender { get; } // 获取事件源

}


7. UML Diagram



8. 响应者行为

  响应者实现 IListener 接口后就可以响应事件了。在 EventArrived() 方法中,你可以直接处理事件,抑或是调用其它的事件处理器(响应器)。C#中有很好的解决方案——委托——替代函数指针的最有效的方法。在C++中也可以用虚拟函数表来模拟委托机制。总之,在响应器上的解决方案是很灵活的。在实际开发中,可以根据不同的环境做出不同的选择。


9. 扩展机制

  在一个游戏中,除了已经定义好的事件外,其剧情或功能可能会要求玩家自行定义一些事件。这就需要一种可扩展的方案。我们引入了 CustomEvent 类——继承自 Event,以及 Condition 类。

public class CustomEvent : Event {

public CustomEvent( Condition condition ) {

_Condition = condition;

}

public Condition TestCondition { get{ return _Condition; } }

Condition _Condition = null;

}

public abstract class Condition {

public Condition() {}

bool abstract Test();

}

  初始化一个 CustomEvent 类时必须同时传入一个 Condition 类。Condition 类必须被继承。Test()方法在适当的时候被调用以检测是否可以引发这个事件。


10. 后记

  以上谈到的只是一个简单的模型,是否实用还要等待实践的检验。欢迎读者的批评与建议



使用Java NIO提高服务端程序的性能

在前面的章节里,我们讨论了Java NIO的基本概念,在这一节里,我们将结合具体的Java Socket编程,讨论使用NIO提高服务端程序的性能的问题。

Java NIO增加了新的SocketChannel、ServerSocketChannel等类来提供对构建高性能的服务端程序的支持。 SocketChannel、ServerSocketChannel能够在非阻塞的模式下工作,它们都是selectable的类。在构建服务器或者中间件时,推荐使用Java NIO。

在传统的网络编程中,我们通常使用一个专用线程(Thread)来处理一个Socket连接,通过使用NIO,一个或者很少几个Socket线程就可以处理成千上万个活动的Socket连接。

通常情况下,通过ServerSocketChannel.open()获得一个ServerSocketChannel的实例,通过SocketChannel.open或者serverSocketChannel.accept()获得一个SocketChannel实例。要使ServerSocketChannel或者SocketChannel在非阻塞的模式下操作,可以调用
serverSocketChannel.configureBlocking (false);
或者
socketChannel.configureBlocking (false);

语句来达到目的。通常情况下,服务端可以使用非阻塞的ServerSocketChannel,这样,服务端的程序就可以更容易地同时处理多个socket线程。

下面我们来看一个综合例子,这个例子使用了ServerSocketChannel、SocketChannel开发了一个非阻塞的、能处理多线程的Echo服务端程序,见示例12-14。
【程序源代码】

1	// ==================== Program Discription =====================
2	// 程序名称:示例12-14 : SocketChannelDemo.java
3	// 程序目的:学习Java NIO#SocketChannel
4	// ==============================================================
5	
6	
7	import java.nio.ByteBuffer;
8	import java.nio.channels.ServerSocketChannel;
9	import java.nio.channels.SocketChannel;
10	import java.nio.channels.Selector;
11	import java.nio.channels.SelectionKey;
12	import java.nio.channels.SelectableChannel;
13	
14	import java.net.Socket;
15	import java.net.ServerSocket;
16	import java.net.InetSocketAddress;
17	import java.util.Iterator;
18
19	public class SocketChannelDemo
 
20	
21	{
22		public static int PORT_NUMBER = 23;//监听端口
23		ServerSocketChannel serverChannel;
24		ServerSocket serverSocket ;
25		Selector selector ;
26		private ByteBuffer buffer = ByteBuffer.allocateDirect (1024);
27	
28		public static void main (String [] args)
29			throws Exception
30		{
31			SocketChannelDemo server=new SocketChannelDemo();
32			server.init(args);
33			server.startWork();
34		}
35	
36	
37		public void init (String [] argv)throws Exception
38		{
39			int port = PORT_NUMBER;
40	
41			if (argv.length > 0) {	
42				port = Integer.parseInt (argv [0]);
43			}
44	
45			System.out.println ("Listening on port " + port);
46	
47			// 分配一个ServerSocketChannel
48			serverChannel = ServerSocketChannel.open();
49			// 从ServerSocketChannel里获得一个对应的Socket
50			serverSocket = serverChannel.socket();
51			// 生成一个Selector
52			selector = Selector.open();
53	
54			// 把Socket绑定到端口上
55			serverSocket.bind (new InetSocketAddress (port));
56			//serverChannel为非bolck
57			serverChannel.configureBlocking (false);
58	
59			// 通过Selector注册ServerSocetChannel
60			serverChannel.register (selector, SelectionKey.OP_ACCEPT);	
61			
62		}
63	
64	    public void startWork()throws Exception
 
65	
66	    {
67	    	while (true) {
68				
69				int n = selector.select();//获得IO准备就绪的channel数量
70	
71				if (n == 0) {
72					continue;	// 没有channel准备就绪,继续执行
73				}
74	
75				// 用一个iterator返回Selector的selectedkeys
76				Iterator it = selector.selectedKeys().iterator();
77	
78				// 处理每一个SelectionKey
79				while (it.hasNext()) {
80					SelectionKey key = (SelectionKey) it.next();
81
82					// 判断是否有新的连接到达
83	if (key.isAcceptable()) {
84           //返回SelectionKey的ServerSocketChannel
85						ServerSocketChannel server =
(ServerSocketChannel) key.channel();
86						SocketChannel channel = server.accept();
87	
88						registerChannel (selector, channel,
89							SelectionKey.OP_READ);
90	
91						doWork (channel);
92					}
93	
94					// 判断是否有数据在此channel里需要读取
95					if (key.isReadable()) {
96					   
97						processData (key);
98	
99					}
100	
101					//删除 selectedkeys
102					it.remove();
103				}
104			}
105		}
106		protected void registerChannel (Selector selector,
107			SelectableChannel channel, int ops)
108			throws Exception
109		{
 
110			if (channel == null) {
111				return;		
112			}
113	
114		
115			channel.configureBlocking (false);
116	
117			channel.register (selector, ops);
118		}
119	
120	    //处理接收的数据
121		protected void processData (SelectionKey key)
122			throws Exception
123		{
124			
125		
126			SocketChannel socketChannel = (SocketChannel) key.channel();
127			int count;
128	
129			buffer.clear();			// 清空buffer
130	
131			// 读取所有的数据
132			while ((count = socketChannel.read (buffer)) > 0) {
133				buffer.flip();		
134
135				// send the data, don′t assume it goes all at once
136				while (buffer.hasRemaining())
137				{
138					//如果收到回车键,则在返回的字符前增加[echo]$字样
139					if(buffer.get()==(char)13)
140					{
141						buffer.clear();
142						buffer.put("[echo]___FCKpd___0quot;.getBytes());
143						buffer.flip();
144						
145					}
146					socketChannel.write (buffer);//在Socket里写数据
147				}
148			
149				buffer.clear();		// 清空buffer
150			}
151	
152			if (count < 0) {
153				// count<0,说明已经读取完毕
154				socketChannel.close();
 
155			}
156		}
157	
158	
159		private void doWork (SocketChannel channel)throws Exception
160		{
161			buffer.clear();
162			buffer.put ("
Hello,I am working,please input some thing,and i will echo to you!
[echo] 
___FCKpd___0quot;.getBytes());
163			buffer.flip();
164			channel.write (buffer);
165		}
166	
167	}


使用:运行此程序,然后在控制台输入命令telnet localhost 23。

【程序输出结果】如图12-1所示。






图12-1 输出结果

【程序注解】
关于程序的解释已经包含在程序里面了,在这里我们总结以下使用ServerSocket Channel开发服务端程序的过程:
(1)分配一个ServerSocketChannel。
(2)从ServerSocketChannel里获得一个对应的ServerSocket。
(3)生成一个Selector实例。
(4)把ServerSocket绑定到端口上。
(5)设置ServerSocketChannel为非block模式(可选)。
(6)在Selector里注册ServerSocetChannel。
(7)用一个无限循环语句始终查看Selector里是否有IO准备就绪的channel。如果有,就执行对应的处理,如果没有,继续循环。

小 结

在本章我们主要介绍了Java中的网络编程。Java一开始就是一种网络编程语言,到后来才应用到各个方面,所以在Java中进行网络编程远比在C/C++中方便。

我们介绍了几个在网络编程中很重要的类,如InetAddress、URL、URLConnection、Socket、 ServerSocket、DatagramSocket、DatagramPacket、MulticastSocket等。这些类包含了进行基本网络编程的所有内容。要熟练地应用这些类,关键还是要多多练习。

基于套接字的编程基本上是客户/服务器模式,我们具体介绍了编写这种模式的步骤。在实例方面,我们给出了一个基于TCP的套接字客户/服务器程序,与此相对应,还给出了基于UDP的客户/服务器程序。两者的模式是很相似的,其实这也就是编写客户/服务器程序的一般模式。 (T111)


虚函数是有代码的并明确允许子类去覆盖,但子类也可不覆盖,就是说可以直接用,不用重写
抽象函数是没有代码,子类继承后一定要重写

******************************************************************

在一个类中用虚函数:
是因为在超类中的有实际代码的方法,但明确允许子类可以作重写
而且当子类重写后,可以用子类实例超类;如果这样,超类变量调用虚函数时,执行的是子类的方法

在一个类中用抽象函数
是在写超类时不确定函数的代码,让子类去实现

******************************************************************

抽象函数没有方法体。

[原创文章,转载请保留或注明出处:http://www.regexlab.com/zh/encoding.htm]

级别:初级

摘要:本文介绍了字符与编码的发展过程,相关概念的正确理解。举例说明了一些实际应用中,编码的实现方法。然后,本文讲述了通常对字符与编码的几种误解,由于这些误解而导致乱码产生的原因,以及消除乱码的办法。本文的内容涵盖了“中文问题”,“乱码问题”。

引言

“字符与编码”是一个被经常讨论的话题。即使这样,时常出现的乱码仍然困扰着大家。虽然我们有很多的办法可以用来消除乱码,但我们并不一定理解这些办法的内在原理。而有的乱码产生的原因,实际上由于底层代码本身有问题所导致的。因此,不仅是初学者会对字符编码感到模糊,有的底层开发人员同样对字符编码缺乏准确的理解。

回页首

1. 编码问题的由来,相关概念的理解

1.1 字符与编码的发展

从计算机对多国语言的支持角度看,大致可以分为三个阶段:

系统内码 说明 系统
阶段一 ASCII 计算机刚开始只支持英语,其它语言不能够在计算机上存储和显示。 英文 DOS
阶段二 ANSI编码
(本地化)
为使计算机支持更多语言,通常使用 0x80~0xFF 范围的 2 个字节来表示 1 个字符。比如:汉字 '中' 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。

不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。

不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。
中文 DOS,中文 Windows 95/98,日文 Windows 95/98
阶段三 UNICODE
(国际化)
为了使国际间信息交流更加方便,国际组织制定了 UNICODE 字符集,为各种语言中的每一个字符设定了统一并且唯一的数字编号,以满足跨语言、跨平台进行文本转换、处理的要求。 Windows NT/2000/XP,Linux,Java

字符串在内存中的存放方法:

在 ASCII 阶段,单字节字符串使用一个字节存放一个字符(SBCS)。比如,"Bob123" 在内存中为:

42 6F 62 31 32 33 00
B o b 1 2 3 /0

在使用 ANSI 编码支持多种语言阶段,每个字符使用一个字节或多个字节来表示(MBCS),因此,这种方式存放的字符也被称作多字节字符。比如,"中文123" 在中文 Windows 95 内存中为7个字节,每个汉字占2个字节,每个英文和数字字符占1个字节:

D6 D0 CE C4 31 32 33 00
1 2 3 /0

在 UNICODE 被采用之后,计算机存放字符串时,改为存放每个字符在 UNICODE 字符集中的序号。目前计算机一般使用 2 个字节(16 位)来存放一个序号(DBCS),因此,这种方式存放的字符也被称作宽字节字符。比如,字符串 "中文123" 在 Windows 2000 下,内存中实际存放的是 5 个序号:

2D 4E 87 65 31 00 32 00 33 00 00 00 ← 在 x86 CPU 中,低字节在前
1 2 3 /0

一共占 10 个字节。

回页首

1.2 字符,字节,字符串

理解编码的关键,是要把字符的概念和字节的概念理解准确。这两个概念容易混淆,我们在此做一下区分:

概念描述 举例
字符 人们使用的记号,抽象意义上的一个符号。 '1', '中', 'a', '$', '¥', ……
字节 计算机中存储数据的单元,一个8位的二进制数,是一个很具体的存储空间。 0x01, 0x45, 0xFA, ……
ANSI
字符串
在内存中,如果“字符”是以 ANSI 编码形式存在的,一个字符可能使用一个字节或多个字节来表示,那么我们称这种字符串为ANSI 字符串或者多字节字符串 "中文123"
(占7字节)
UNICODE
字符串
在内存中,如果“字符”是以在 UNICODE 中的序号存在的,那么我们称这种字符串为 UNICODE 字符串或者宽字节字符串 L"中文123"
(占10字节)

由于不同 ANSI 编码所规定的标准是不相同的,因此,对于一个给定的多字节字符串,我们必须知道它采用的是哪一种编码规则,才能够知道它包含了哪些“字符”。而对于UNICODE 字符串来说,不管在什么环境下,它所代表的“字符”内容总是不变的。

回页首

1.3 字符集与编码

各个国家和地区所制定的不同 ANSI 编码标准中,都只规定了各自语言所需的“字符”。比如:汉字标准(GB2312)中没有规定韩国语字符怎样存储。这些 ANSI 编码标准所规定的内容包含两层含义:

  1. 使用哪些字符。也就是说哪些汉字,字母和符号会被收入标准中。所包含“字符”的集合就叫做“字符集”。
  2. 规定每个“字符”分别用一个字节还是多个字节存储,用哪些字节来存储,这个规定就叫做“编码”。

各个国家和地区在制定编码标准的时候,“字符的集合”和“编码”一般都是同时制定的。因此,平常我们所说的“字符集”,比如:GB2312, GBK, JIS 等,除了有“字符的集合”这层含义外,同时也包含了“编码”的含义。

UNICODE 字符集”包含了各种语言中使用到的所有“字符”。用来给 UNICODE 字符集编码的标准有很多种,比如:UTF-8, UTF-7, UTF-16, UnicodeLittle, UnicodeBig 等。

回页首

2. 字符与编码在程序中的实现

2.1 程序中的字符与字节

在 C++ 和 Java 中,用来代表“字符”和“字节”的数据类型,以及进行编码的方法:

类型或操作 C++ Java
字符 wchar_t char *
字节 char byte
ANSI 字符串 char[] byte[]
UNICODE 字符串 wchar_t[] String
字节串→字符串 mbstowcs(), MultiByteToWideChar() * string = new String(bytes, "encoding")
字符串→字节串 wcstombs(), WideCharToMultiByte() bytes = string.getBytes("encoding")

以上需要注意几点:

  1. Java 中的 char 代表一个“UNICODE 字符(宽字节字符)”,而 C++ 中的 char 代表一个字节。
  2. MultiByteToWideChar() 和 WideCharToMultiByte() 是 Windows API 函数。

回页首

2.2 C++ 中相关实现方法

声明一段字符串常量:

// ANSI 字符串,内容长度 7 字节
char
sz[20] ="中文123";

// UNICODE 字符串,内容长度 5 个 wchar_t(10 字节)
wchar_t wsz[20] = L"/x4E2D/x6587/x0031/x0032/x0033";

UNICODE 字符串的 I/O 操作,字符与字节的转换操作:

// 运行时设定当前 ANSI 编码,VC 格式
setlocale(LC_ALL, ".936");

// GCC 中格式
setlocale(LC_ALL, "zh_CN.GBK");

// Visual C++ 中使用小写 %s,按照 setlocale 指定编码输出到文件
// GCC 中使用大写 %S

fwprintf(fp, L"%s/n", wsz);

// 把 UNICODE 字符串按照 setlocale 指定的编码转换成字节
wcstombs(sz, wsz, 20);
// 把字节串按照 setlocale 指定的编码转换成 UNICODE 字符串
mbstowcs(wsz, sz, 20);

在 Visual C++ 中,UNICODE 字符串常量有更简单的表示方法。如果源程序的编码与当前默认 ANSI 编码不符,则需要使用 #pragma setlocale,告诉编译器源程序使用的编码:

// 如果源程序的编码与当前默认 ANSI 编码不一致,
// 则需要此行,编译时用来指明当前源程序使用的编码

#pragma setlocale
(".936")

// UNICODE 字符串常量,内容长度 10 字节
wchar_t wsz[20] = L"中文123";

以上需要注意 #pragma setlocale 与 setlocale(LC_ALL, "") 的作用是不同的,#pragma setlocale 在编译时起作用,setlocale() 在运行时起作用。

回页首

2.3 Java 中相关实现方法

字符串类 String 中的内容是 UNICODE 字符串:

// Java 代码,直接写中文
String
string = "中文123";

// 得到长度为 5,因为是 5 个字符
System.out.println(string.length());

字符串 I/O 操作,字符与字节转换操作。在 Java 包 java.io.* 中,以“Stream”结尾的类一般是用来操作“字节串”的类,以“Reader”,“Writer”结尾的类一般是用来操作“字符串”的类。

// 字符串与字节串间相互转化

// 按照 GB2312 得到字节(得到多字节字符串)

byte
[] bytes = string.getBytes("GB2312");

// 从字节按照 GB2312 得到 UNICODE 字符串
string = new String(bytes, "GB2312");

// 要将 String 按照某种编码写入文本文件,有两种方法:

// 第一种办法:用 Stream 类写入已经按照指定编码转化好的字节串

OutputStream os = new FileOutputStream("1.txt");
os.write(bytes);
os.close();

// 第二种办法:构造指定编码的 Writer 来写入字符串
Writer ow = new OutputStreamWriter(new FileOutputStream("2.txt"),"GB2312");
ow.write(string);
ow.close();

/* 最后得到的 1.txt 和 2.txt 都是 7 个字节 */

如果 java 的源程序编码与当前默认 ANSI 编码不符,则在编译的时候,需要指明一下源程序的编码。比如:

E:/>javac -encoding BIG5 Hello.java

以上需要注意区分源程序的编码与 I/O 操作的编码,前者是在编译时起作用,后者是在运行时起作用。

回页首

3. 几种误解,以及乱码产生的原因和解决办法

3.1 容易产生的误解
对编码的误解
误解一 在将“字节串”转化成“UNICODE 字符串”时,比如在读取文本文件时,或者通过网络传输文本时,容易将“字节串”简单地作为单字节字符串,采用每“一个字节”就是“一个字符”的方法进行转化。

而实际上,在非英文的环境中,应该将“字节串”作为 ANSI 字符串,采用适当的编码来得到 UNICODE 字符串,有可能“多个字节”才能得到“一个字符”。

通常,一直在英文环境下做开发的程序员们,容易有这种误解。
误解二 在 DOS,Windows 98 等非 UNICODE 环境下,字符串都是以 ANSI 编码的字节形式存在的。这种以字节形式存在的字符串,必须知道是哪种编码才能被正确地使用。这使我们形成了一个惯性思维:“字符串的编码”。

当 UNICODE 被支持后,Java 中的 String 是以字符的“序号”来存储的,不是以“某种编码的字节”来存储的,因此已经不存在“字符串的编码”这个概念了。只有在“字符串”与“字节串”转化时,或者,将一个“字节串”当成一个 ANSI 字符串时,才有编码的概念。

不少的人都有这个误解。

第一种误解,往往是导致乱码产生的原因。第二种误解,往往导致本来容易纠正的乱码问题变得更复杂。

回页首

3.2 常用的编码简介

简单介绍一下常用的编码规则,为后边的章节做一个准备。在这里,我们根据编码规则的特点,把所有的编码分成三类:

分类 编码标准 说明
单字节字符编码 ISO-8859-1 最简单的编码规则,每一个字节直接作为一个 UNICODE 字符。比如,[0xD6, 0xD0] 这两个字节,通过 iso-8859-1 转化为字符串时,将直接得到 [0x00D6, 0x00D0] 两个 UNICODE 字符,即 "ÖÐ"。

反之,将 UNICODE 字符串通过 iso-8859-1 转化为字节串时,只能正常转化 0~255 范围的字符。
ANSI 编码 GB2312,
BIG5,
Shift_JIS,
ISO-8859-2 ……
把 UNICODE 字符串通过 ANSI 编码转化为“字节串”时,根据各自编码的规定,一个 UNICODE 字符可能转化成一个字节或多个字节。

反之,将字节串转化成字符串时,也可能多个字节转化成一个字符。比如,[0xD6, 0xD0] 这两个字节,通过 GB2312 转化为字符串时,将得到 [0x4E2D] 一个字符,即 '中' 字。

“ANSI 编码”的特点:
1. 这些“ANSI 编码标准”都只能处理各自语言范围之内的 UNICODE 字符。
2. “UNICODE 字符”与“转换出来的字节”之间的关系是人为规定的。
UNICODE 编码 UTF-8,
UTF-16, UnicodeBig ……
与“ANSI 编码”类似的,把字符串通过 UNICODE 编码转化成“字节串”时,一个 UNICODE 字符可能转化成一个字节或多个字节。

与“ANSI 编码”不同的是:
1. 这些“UNICODE 编码”能够处理所有的 UNICODE 字符。
2. “UNICODE 字符”与“转换出来的字节”之间是可以通过计算得到的。

在这里,我们可以看到,前面所讲的“误解一”,即采用每“一个字节”就是“一个字符”的转化方法,实际上也就等同于采用 iso-8859-1 进行转化。因此,我们常常使用 bytes = string.getBytes("iso-8859-1") 来进行逆向操作,得到原始的“字节串”。然后再使用正确的 ANSI 编码,比如 string = new String(bytes, "GB2312"),来得到正确的“UNICODE 字符串”。

回页首

3.3 非 UNICODE 程序在不同语言环境间移植时的乱码

非 UNICODE 程序中的字符串,都是以某种 ANSI 编码形式存在的。如果程序运行时的语言环境与开发时的语言环境不同,将会导致 ANSI 字符串的显示失败。

比如,在日文环境下开发的非 UNICODE 的日文程序界面,拿到中文环境下运行时,界面上将显示乱码。如果这个日文程序界面改为采用 UNICODE 来记录字符串,那么当在中文环境下运行时,界面上将可以显示正常的日文。

由于客观原因,有时候我们必须在中文操作系统下运行非 UNICODE 的日文软件,这时我们可以采用一些工具,比如,南极星,AppLocale 等,暂时的模拟不同的语言环境。

回页首

3.4 网页提交字符串

当页面中的表单提交字符串时,首先把字符串按照当前页面的编码,转化成字节串。然后再将每个字节转化成 "%XX" 的格式提交到 Web 服务器。比如,一个编码为 GB2312 的页面,提交 "中" 这个字符串时,提交给服务器的内容为 "%D6%D0"。

在服务器端,Web 服务器把收到的 "%D6%D0" 转化成 [0xD6, 0xD0] 两个字节,然后再根据 GB2312 编码规则得到 "中" 字。

在 Tomcat 服务器中,request.getParameter() 得到乱码时,常常是因为前面提到的“误解一”造成的。默认情况下,当提交 "%D6%D0" 给 Tomcat 服务器时,request.getParameter() 将返回 [0x00D6, 0x00D0] 两个 UNICODE 字符,而不是返回一个 "中" 字符。因此,我们需要使用 bytes = string.getBytes("iso-8859-1") 得到原始的字节串,再用 string = new String(bytes, "GB2312") 重新得到正确的字符串 "中"。

回页首

3.5 从数据库读取字符串

通过数据库客户端(比如 ODBC 或 JDBC)从数据库服务器中读取字符串时,客户端需要从服务器获知所使用的 ANSI 编码。当数据库服务器发送字节流给客户端时,客户端负责将字节流按照正确的编码转化成 UNICODE 字符串。

如果从数据库读取字符串时得到乱码,而数据库中存放的数据又是正确的,那么往往还是因为前面提到的“误解一”造成的。解决的办法还是通过 string = new String( string.getBytes("iso-8859-1"), "GB2312") 的方法,重新得到原始的字节串,再重新使用正确的编码转化成字符串。

回页首

3.6 电子邮件中的字符串

当一段 Text 或者 HTML 通过电子邮件传送时,发送的内容首先通过一种指定的字符编码转化成“字节串”,然后再把“字节串”通过一种指定的传输编码(Content-Transfer-Encoding)进行转化得到另一串“字节串”。比如,打开一封电子邮件源代码,可以看到类似的内容:

Content-Type: text/plain;
charset="gb2312"
Content-Transfer-Encoding: base64

sbG+qcrQuqO17cf4yee74bGjz9W7+b3wudzA7dbQ0MQNCg0KvPKzxqO6uqO17cnnsaPW0NDEDQoNCg==

最常用的 Content-Transfer-Encoding 有 Base64 和 Quoted-Printable 两种。在对二进制文件或者中文文本进行转化时,Base64 得到的“字节串”比 Quoted-Printable 更短。在对英文文本进行转化时,Quoted-Printable 得到的“字节串”比 Base64 更短。

邮件的标题,用了一种更简短的格式来标注“字符编码”和“传输编码”。比如,标题内容为 "中",则在邮件源代码中表示为:

// 正确的标题格式
Subject: =?GB2312?B?1tA=?=

其中,

  • 第一个“=?”与“?”中间的部分指定了字符编码,在这个例子中指定的是 GB2312。
  • “?”与“?”中间的“B”代表 Base64。如果是“Q”则代表 Quoted-Printable。
  • 最后“?”与“?=”之间的部分,就是经过 GB2312 转化成字节串,再经过 Base64 转化后的标题内容。

如果“传输编码”改为 Quoted-Printable,同样,如果标题内容为 "中":

// 正确的标题格式
Subject: =?GB2312?Q?=D6=D0?=

如果阅读邮件时出现乱码,一般是因为“字符编码”或“传输编码”指定有误,或者是没有指定。比如,有的发邮件组件在发送邮件时,标题 "中":

// 错误的标题格式
Subject: =?ISO-8859-1?Q?=D6=D0?=

这样的表示,实际上是明确指明了标题为 [0x00D6, 0x00D0],即 "ÖÐ",而不是 "中"。

回页首

4. 几种错误理解的纠正

误解:“ISO-8859-1 是国际编码?”

非也。iso-8859-1 只是单字节字符集中最简单的一种,也就是“字节编号”与“UNICODE 字符编号”一致的那种编码规则。当我们要把一个“字节串”转化成“字符串”,而又不知道它是哪一种 ANSI 编码时,先暂时地把“每一个字节”作为“一个字符”进行转化,不会造成信息丢失。然后再使用 bytes = string.getBytes("iso-8859-1") 的方法可恢复到原始的字节串。

误解:“Java 中,怎样知道某个字符串的内码?”

Java 中,字符串类 java.lang.String 处理的是 UNICODE 字符串,不是 ANSI 字符串。我们只需要把字符串作为“抽象的符号的串”来看待。因此不存在字符串的内码的问题。

Java性能优化技巧集锦

一、通用篇
  1.1 不用new关键词创建类的实例
  1.2 使用非阻塞I/O
  1.3 慎用异常
  1.4 不要重复初始化变量
  1.5 尽量指定类的final修饰符
  1.6 尽量使用局部变量
  1.7 乘法和除法
二、J2EE篇
  2.1 使用缓冲标记
  2.2 始终通过会话Bean访问实体Bean
  2.3 选择合适的引用机制
  2.4 在部署描述器中设置只读属性
  2.5 缓冲对EJB Home的访问
  2.6 为EJB实现本地接口
  2.7 生成主键
  2.8 及时清除不再需要的会话
  2.9 在JSP页面中关闭无用的会话
  2.10 Servlet与内存使用
  2.11 HTTP Keep-Alive
  2.12 JDBC与Unicode
  2.13 JDBC与I/O
  1.14 内存数据库
三、GUI篇
  3.1 用JAR压缩类文件
  3.2 提示Applet装入进程
  3.3 在画出图形之前预先装入它
  3.4 覆盖update方法
  3.5 延迟重画操作
  3.6 使用双缓冲区
  3.7 使用BufferedImage
  3.8 使用VolatileImage
  3.9 使用Window Blitting
四、补充资料
===================================
正文:
===================================
一、通用篇

“通用篇”讨论的问题适合于大多数Java应用。

1.1 不用new关键词创建类的实例

用new关键词创建类的实例时,构造函数链中的所有构造函数都会被自动调用。但如果一个对象实现了Cloneable接口,我们可以调用它的clone()方法。clone()方法不会调用任何类构造函数。

在使用设计模式(Design Pattern)的场合,如果用Factory模式创建对象,则改用clone()方法创建新的对象实例非常简单。例如,下面是Factory模式的一个典型实现:


public static Credit getNewCredit() {
return new Credit();
}

改进后的代码使用clone()方法,如下所示:


private static Credit BaseCredit = new Credit();
public static Credit getNewCredit() {
return (Credit) BaseCredit.clone();
}

上面的思路对于数组处理同样很有用。

1.2 使用非阻塞I/O

版本较低的JDK不支持非阻塞I/O API。为避免I/O阻塞,一些应用采用了创建大量线程的办法(在较好的情况下,会使用一个缓冲池)。这种技术可以在许多必须支持并发I/O流的应用中见到,如Web服务器、报价和拍卖应用等。然而,创建Java线程需要相当可观的开销。

JDK 1.4引入了非阻塞的I/O库(java.nio)。如果应用要求使用版本较早的JDK,在这里有一个支持非阻塞I/O的软件包。

请参见Sun中国网站的《调整Java的I/O性能》。

1.3 慎用异常

异常对性能不利。抛出异常首先要创建一个新的对象。Throwable接口的构造函数调用名为fillInStackTrace()的本地(Native)方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,VM就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。

异常只能用于错误处理,不应该用来控制程序流程。

1.4 不要重复初始化变量

默认情况下,调用类的构造函数时, Java会把变量初始化成确定的值:所有的对象被设置成null,整数变量(byte、short、int、long)设置成0,float和 double变量设置成0.0,逻辑值设置成false。当一个类从另一个类派生时,这一点尤其应该注意,因为用new关键词创建一个对象时,构造函数链中的所有构造函数都会被自动调用。

1.5 尽量指定类的final修饰符

带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String。为String类指定final防止了人们覆盖length()方法。

另外,如果指定一个类为final,则该类所有的方法都是final。Java编译器会寻找机会内联(inline)所有的final方法(这和具体的编译器实现有关)。此举能够使性能平均提高50%。

1.6 尽量使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快。其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。另外,依赖于具体的编译器/JVM,局部变量还可能得到进一步优化。请参见《尽可能使用堆栈变量》。

1.7 乘法和除法

考虑下面的代码:


for (val = 0; val < 100000; val +=5) { alterX = val * 8; myResult = val * 2; }

用移位操作替代乘法操作可以极大地提高性能。下面是修改后的代码:


for (val = 0; val < 100000; val += 5) { alterX = val << 3; myResult = val << 1; }

修改后的代码不再做乘以8的操作,而是改用等价的左移3位操作,每左移1位相当于乘以2。相应地,右移1位操作相当于除以2。值得一提的是,虽然移位操作速度快,但可能使代码比较难于理解,所以最好加上一些注释。

二、J2EE篇

前面介绍的改善性能技巧适合于大多数Java应用,接下来要讨论的问题适合于使用JSP、EJB或JDBC的应用。

2.1 使用缓冲标记

一些应用服务器加入了面向JSP的缓冲标记功能。例如,BEA的WebLogic Server从6.0版本开始支持这个功能,Open Symphony工程也同样支持这个功能。JSP缓冲标记既能够缓冲页面片断,也能够缓冲整个页面。当JSP页面执行时,如果目标片断已经在缓冲之中,则生成该片断的代码就不用再执行。页面级缓冲捕获对指定URL的请求,并缓冲整个结果页面。对于购物篮、目录以及门户网站的主页来说,这个功能极其有用。对于这类应用,页面级缓冲能够保存页面执行的结果,供后继请求使用。

对于代码逻辑复杂的页面,利用缓冲标记提高性能的效果比较明显;反之,效果可能略逊一筹。

请参见《用缓冲技术提高JSP应用的性能和稳定性》。

2.2 始终通过会话Bean访问实体Bean

直接访问实体Bean不利于性能。当客户程序远程访问实体Bean时,每一个get方法都是一个远程调用。访问实体Bean的会话Bean是本地的,能够把所有数据组织成一个结构,然后返回它的值。

用会话Bean封装对实体Bean的访问能够改进事务管理,因为会话Bean只有在到达事务边界时才会提交。每一个对get方法的直接调用产生一个事务,容器将在每一个实体Bean的事务之后执行一个“装入-读取”操作。

一些时候,使用实体Bean会导致程序性能不佳。如果实体Bean的唯一用途就是提取和更新数据,改成在会话Bean之内利用JDBC访问数据库可以得到更好的性能。

2.3 选择合适的引用机制

在典型的JSP应用系统中,页头、页脚部分往往被抽取出来,然后根据需要引入页头、页脚。当前,在JSP页面中引入外部资源的方法主要有两种:include指令,以及include动作。


include指令:例如<%@ include file="copyright.html" %>。该指令在编译时引入指定的资源。在编译之前,带有include指令的页面和指定的资源被合并成一个文件。被引用的外部资源在编译时就确定,比运行时才确定资源更高效。
include动作:例如<jsp:include page="copyright.jsp" />。该动作引入指定页面执行后生成的结果。由于它在运行时完成,因此对输出结果的控制更加灵活。但时,只有当被引用的内容频繁地改变时,或者在对主页面的请求没有出现之前,被引用的页面无法确定时,使用include动作才合算。
2.4 在部署描述器中设置只读属性

实体Bean的部署描述器允许把所有get方法设置成“只读”。当某个事务单元的工作只包含执行读取操作的方法时,设置只读属性有利于提高性能,因为容器不必再执行存储操作。

2.5 缓冲对EJB Home的访问

EJB Home接口通过JNDI名称查找获得。这个操作需要相当可观的开销。JNDI查找最好放入Servlet的init()方法里面。如果应用中多处频繁地出现EJB访问,最好创建一个EJBHomeCache类。EJBHomeCache类一般应该作为singleton实现。

2.6 为EJB实现本地接口

本地接口是EJB 2.0规范新增的内容,它使得Bean能够避免远程调用的开销。请考虑下面的代码。


PayBeanHome home = (PayBeanHome)
javax.rmi.PortableRemoteObject.narrow
(ctx.lookup ("PayBeanHome"), PayBeanHome.class);
PayBean bean = (PayBean)
javax.rmi.PortableRemoteObject.narrow
(home.create(), PayBean.class);


第一个语句表示我们要寻找Bean的Home接口。这个查找通过JNDI进行,它是一个RMI调用。然后,我们定位远程对象,返回代理引用,这也是一个 RMI调用。第二个语句示范了如何创建一个实例,涉及了创建IIOP请求并在网络上传输请求的stub程序,它也是一个RMI调用。

要实现本地接口,我们必须作如下修改:


方法不能再抛出java.rmi.RemoteException异常,包括从RemoteException派生的异常,比如 TransactionRequiredException、TransactionRolledBackException和 NoSuchObjectException。EJB提供了等价的本地异常,如TransactionRequiredLocalException、 TransactionRolledBackLocalException和NoSuchObjectLocalException。
所有数据和返回值都通过引用的方式传递,而不是传递值。
本地接口必须在EJB部署的机器上使用。简而言之,客户程序和提供服务的组件必须在同一个JVM上运行。
如果Bean实现了本地接口,则其引用不可串行化。
请参见《用本地引用提高EJB访问效率》。

2.7 生成主键

在EJB之内生成主键有许多途径,下面分析了几种常见的办法以及它们的特点。

利用数据库内建的标识机制(SQL Server的IDENTITY或Oracle的SEQUENCE)。这种方法的缺点是EJB可移植性差。

由实体Bean自己计算主键值(比如做增量操作)。它的缺点是要求事务可串行化,而且速度也较慢。

利用NTP之类的时钟服务。这要求有面向特定平台的本地代码,从而把Bean固定到了特定的OS之上。另外,它还导致了这样一种可能,即在多CPU的服务器上,同一个毫秒之内生成了两个主键。

借鉴Microsoft的思路,在Bean中创建一个GUID。然而,如果不求助于JNI,Java不能确定网卡的MAC地址;如果使用JNI,则程序就要依赖于特定的OS。

还有其他几种办法,但这些办法同样都有各自的局限。似乎只有一个答案比较理想:结合运用RMI和JNDI。先通过RMI注册把RMI远程对象绑定到JNDI树。客户程序通过JNDI进行查找。下面是一个例子:


public class keyGenerator extends UnicastRemoteObject implements Remote {
private static long KeyValue = System.currentTimeMillis();
public static synchronized long getKey() throws RemoteException { return KeyValue++; }


2.8 及时清除不再需要的会话

为了清除不再活动的会话,许多应用服务器都有默认的会话超时时间,一般为30分钟。当应用服务器需要保存更多会话时,如果内存容量不足,操作系统会把部分内存数据转移到磁盘,应用服务器也可能根据“最近最频繁使用”(Most Recently Used)算法把部分不活跃的会话转储到磁盘,甚至可能抛出“内存不足”异常。在大规模系统中,串行化会话的代价是很昂贵的。当会话不再需要时,应当及时调用HttpSession.invalidate()方法清除会话。HttpSession.invalidate()方法通常可以在应用的退出页面调用。

2.9 在JSP页面中关闭无用的会话

对于那些无需跟踪会话状态的页面,关闭自动创建的会话可以节省一些资源。使用如下page指令:


<%@ page session="false"%>

2.10 Servlet与内存使用

许多开发者随意地把大量信息保存到用户会话之中。一些时候,保存在会话中的对象没有及时地被垃圾回收机制回收。从性能上看,典型的症状是用户感到系统周期性地变慢,却又不能把原因归于任何一个具体的组件。如果监视JVM的堆空间,它的表现是内存占用不正常地大起大落。

解决这类内存问题主要有二种办法。第一种办法是,在所有作用范围为会话的Bean中实现HttpSessionBindingListener接口。这样,只要实现valueUnbound()方法,就可以显式地释放Bean使用的资源。

另外一种办法就是尽快地把会话作废。大多数应用服务器都有设置会话作废间隔时间的选项。另外,也可以用编程的方式调用会话的 setMaxInactiveInterval()方法,该方法用来设定在作废会话之前,Servlet容器允许的客户请求的最大间隔时间,以秒计。

2.11 HTTP Keep-Alive

Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。市场上的大部分Web服务器,包括iPlanet、IIS和Apache,都支持HTTP Keep-Alive。对于提供静态内容的网站来说,这个功能通常很有用。但是,对于负担较重的网站来说,这里存在另外一个问题:虽然为客户保留打开的连接有一定的好处,但它同样影响了性能,因为在处理暂停期间,本来可以释放的资源仍旧被占用。当Web服务器和应用服务器在同一台机器上运行时,Keep- Alive功能对资源利用的影响尤其突出。

2.12 JDBC与Unicode

想必你已经了解一些使用JDBC时提高性能的措施,比如利用连接池、正确地选择存储过程和直接执行的SQL、从结果集删除多余的列、预先编译SQL语句,等等。

除了这些显而易见的选择之外,另一个提高性能的好选择可能就是把所有的字符数据都保存为Unicode(代码页13488)。Java以Unicode形式处理所有数据,因此,数据库驱动程序不必再执行转换过程。但应该记住:如果采用这种方式,数据库会变得更大,因为每个Unicode字符需要2个字节存储空间。另外,如果有其他非Unicode的程序访问数据库,性能问题仍旧会出现,因为这时数据库驱动程序仍旧必须执行转换过程。

2.13 JDBC与I/O

如果应用程序需要访问一个规模很大的数据集,则应当考虑使用块提取方式。默认情况下,JDBC每次提取32行数据。举例来说,假设我们要遍历一个5000 行的记录集,JDBC必须调用数据库157次才能提取到全部数据。如果把块大小改成512,则调用数据库的次数将减少到10次。

在一些情形下这种技术无效。例如,如果使用可滚动的记录集,或者在查询中指定了FOR UPDATE,则块操作方式不再有效。

1.14 内存数据库

许多应用需要以用户为单位在会话对象中保存相当数量的数据,典型的应用如购物篮和目录等。由于这类数据可以按照行/列的形式组织,因此,许多应用创建了庞大的Vector或HashMap。在会话中保存这类数据极大地限制了应用的可伸缩性,因为服务器拥有的内存至少必须达到每个会话占用的内存数量乘以并发用户最大数量,它不仅使服务器价格昂贵,而且垃圾收集的时间间隔也可能延长到难以忍受的程度。

一些人把购物篮/目录功能转移到数据库层,在一定程度上提高了可伸缩性。然而,把这部分功能放到数据库层也存在问题,且问题的根源与大多数关系数据库系统的体系结构有关。对于关系数据库来说,运行时的重要原则之一是确保所有的写入操作稳定、可靠,因而,所有的性能问题都与物理上把数据写入磁盘的能力有关。关系数据库力图减少I/O操作,特别是对于读操作,但实现该目标的主要途径只是执行一套实现缓冲机制的复杂算法,而这正是数据库层第一号性能瓶颈通常总是 CPU的主要原因。

一种替代传统关系数据库的方案是,使用在内存中运行的数据库(In-memory Database),例如TimesTen。内存数据库的出发点是允许数据临时地写入,但这些数据不必永久地保存到磁盘上,所有的操作都在内存中进行。这样,内存数据库不需要复杂的算法来减少I/O操作,而且可以采用比较简单的加锁机制,因而速度很快。

三、GUI篇

这一部分介绍的内容适合于图形用户界面的应用(Applet和普通应用),要用到AWT或Swing。

3.1 用JAR压缩类文件

Java档案文件(JAR文件)是根据JavaBean标准压缩的文件,是发布JavaBean组件的主要方式和推荐方式。JAR档案有助于减少文件体积,缩短下载时间。例如,它有助于Applet提高启动速度。一个JAR文件可以包含一个或者多个相关的Bean以及支持文件,比如图形、声音、HTML 和其他资源。

要在HTML/JSP文件中指定JAR文件,只需在Applet标记中加入ARCHIVE = "name.jar"声明。

请参见《使用档案文件提高 applet 的加载速度》。

3.2 提示Applet装入进程

你是否看到过使用Applet的网站,注意到在应该运行Applet的地方出现了一个占位符?当Applet的下载时间较长时,会发生什么事情?最大的可能就是用户掉头离去。在这种情况下,显示一个Applet正在下载的信息无疑有助于鼓励用户继续等待。

下面我们来看看一种具体的实现方法。首先创建一个很小的Applet,该Applet负责在后台下载正式的Applet:


import java.applet.Applet;
import java.applet.AppletStub;
import java.awt.Label;
import java.awt.Graphics;
import java.awt.GridLayout;
public class PreLoader extends Applet implements Runnable, AppletStub {
String largeAppletName;
Label label;
public void init() {
// 要求装载的正式Applet
largeAppletName = getParameter("applet");
// “请稍等”提示信息
label = new Label("请稍等..." + largeAppletName);
add(label);
}
public void run(){
try {
// 获得待装载Applet的类
Class largeAppletClass = Class.forName(largeAppletName);
// 创建待装载Applet的实例
Applet largeApplet = (Applet)largeAppletClass.newInstance();
// 设置该Applet的Stub程序
largeApplet.setStub(this);
// 取消“请稍等”信息
remove(label);
// 设置布局
setLayout(new GridLayout(1, 0));
add(largeApplet);
// 显示正式的Applet
largeApplet.init();
largeApplet.start();
}
catch (Exception ex) {
// 显示错误信息
label.setText("不能装入指定的Applet");
}
// 刷新屏幕
validate();
}
public void appletResize(int width, int height) {
// 把appletResize调用从stub程序传递到Applet
resize(width, height);
}
}

编译后的代码小于2K,下载速度很快。代码中有几个地方值得注意。首先,PreLoader实现了AppletStub接口。一般地,Applet从调用者判断自己的codebase。在本例中,我们必须调用setStub()告诉Applet到哪里提取这个信息。另一个值得注意的地方是, AppletStub接口包含许多和Applet类一样的方法,但appletResize()方法除外。这里我们把对appletResize()方法的调用传递给了resize()方法。

3.3 在画出图形之前预先装入它

ImageObserver接口可用来接收图形装入的提示信息。ImageObserver接口只有一个方法imageUpdate(),能够用一次repaint()操作在屏幕上画出图形。下面提供了一个例子。


public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) {
if ((flags & ALLBITS) !=0 {
repaint();
}
else if (flags & (ERROR |ABORT )) != 0) {
error = true;
// 文件没有找到,考虑显示一个占位符
repaint();
}
return (flags & (ALLBITS | ERROR| ABORT)) == 0;
}

当图形信息可用时,imageUpdate()方法被调用。如果需要进一步更新,该方法返回true;如果所需信息已经得到,该方法返回false。

3.4 覆盖update方法

update()方法的默认动作是清除屏幕,然后调用paint()方法。如果使用默认的update()方法,频繁使用图形的应用可能出现显示闪烁现象。要避免在paint()调用之前的屏幕清除操作,只需按照如下方式覆盖update()方法:

public void update(Graphics g) {
paint(g);
}

更理想的方案是:覆盖update(),只重画屏幕上发生变化的区域,如下所示:

public void update(Graphics g) {
g.clipRect(x, y, w, h);
paint(g);
}


3.5 延迟重画操作

对于图形用户界面的应用来说,性能低下的主要原因往往可以归结为重画屏幕的效率低下。当用户改变窗口大小或者滚动一个窗口时,这一点通常可以很明显地观察到。改变窗口大小或者滚动屏幕之类的操作导致重画屏幕事件大量地、快速地生成,甚至超过了相关代码的执行速度。对付这个问题最好的办法是忽略所有“迟到” 的事件。

建议在这里引入一个数毫秒的时差,即如果我们立即接收到了另一个重画事件,可以停止处理当前事件转而处理最后一个收到的重画事件;否则,我们继续进行当前的重画过程。

如果事件要启动一项耗时的工作,分离出一个工作线程是一种较好的处理方式;否则,一些部件可能被“冻结”,因为每次只能处理一个事件。下面提供了一个事件处理的简单例子,但经过扩展后它可以用来控制工作线程。


public static void runOnce(String id, final long milliseconds) {
synchronized(e_queue) { // e_queue: 所有事件的集合
if (!e_queue.containsKey(id)) {
e_queue.put(token, new LastOne());
}
}
final LastOne lastOne = (LastOne) e_queue.get(token);
final long time = System.currentTimeMillis(); // 获得当前时间
lastOne.time = time;
(new Thread() {public void run() {
if (milliseconds > 0) {
try {Thread.sleep(milliseconds);} // 暂停线程
catch (Exception ex) {}
}
synchronized(lastOne.running) { // 等待上一事件结束
if (lastOne.time != time) // 只处理最后一个事件
return;
}
}}).start();
}
private static Hashtable e_queue = new Hashtable();
private static class LastOne {
public long time=0;
public Object running = new Object();
}


3.6 使用双缓冲区

在屏幕之外的缓冲区绘图,完成后立即把整个图形显示出来。由于有两个缓冲区,所以程序可以来回切换。这样,我们可以用一个低优先级的线程负责画图,使得程序能够利用空闲的CPU时间执行其他任务。下面的伪代码片断示范了这种技术。


Graphics myGraphics;
Image myOffscreenImage = createImage(size().width, size().height);
Graphics offscreenGraphics = myOffscreenImage.getGraphics();
offscreenGraphics.drawImage(img, 50, 50, this);
myGraphics.drawImage(myOffscreenImage, 0, 0, this);


3.7 使用BufferedImage

Java JDK 1.2使用了一个软显示设备,使得文本在不同的平台上看起来相似。为实现这个功能,Java必须直接处理构成文字的像素。由于这种技术要在内存中大量地进行位复制操作,早期的JDK在使用这种技术时性能不佳。为解决这个问题而提出的Java标准实现了一种新的图形类型,即BufferedImage。

BufferedImage子类描述的图形带有一个可访问的图形数据缓冲区。一个BufferedImage包含一个ColorModel和一组光栅图形数据。这个类一般使用RGB(红、绿、蓝)颜色模型,但也可以处理灰度级图形。它的构造函数很简单,如下所示:
public BufferedImage (int width, int height, int imageType)

ImageType允许我们指定要缓冲的是什么类型的图形,比如5-位RGB、8-位RGB、灰度级等。

3.8 使用VolatileImage

许多硬件平台和它们的操作系统都提供基本的硬件加速支持。例如,硬件加速一般提供矩形填充功能,和利用CPU完成同一任务相比,硬件加速的效率更高。由于硬件加速分离了一部分工作,允许多个工作流并发进行,从而缓解了对CPU和系统总线的压力,使得应用能够运行得更快。利用VolatileImage可以创建硬件加速的图形以及管理图形的内容。由于它直接利用低层平台的能力,性能的改善程度主要取决于系统使用的图形适配器。VolatileImage的内容随时可能丢失,也即它是“不稳定的(volatile)”。因此,在使用图形之前,最好检查一下它的内容是否丢失。VolatileImage有两个能够检查内容是否丢失的方法:

public abstract int validate(GraphicsConfiguration gc);
public abstract Boolean contentsLost();

每次从VolatileImage对象复制内容或者写入VolatileImage时,应该调用validate()方法。contentsLost()方法告诉我们,自从最后一次validate()调用之后,图形的内容是否丢失。

虽然VolatileImage是一个抽象类,但不要从它这里派生子类。VolatileImage应该通过 Component.createVolatileImage()或者 GraphicsConfiguration.createCompatibleVolatileImage()方法创建。

3.9 使用Window Blitting

进行滚动操作时,所有可见的内容一般都要重画,从而导致大量不必要的重画工作。许多操作系统的图形子系统,包括WIN32 GDI、MacOS和X/Windows,都支持Window Blitting技术。Window Blitting技术直接在屏幕缓冲区中把图形移到新的位置,只重画新出现的区域。要在Swing应用中使用Window Blitting技术,设置方法如下:

setScrollMode(int mode);

在大多数应用中,使用这种技术能够提高滚动速度。只有在一种情形下,Window Blitting会导致性能降低,即应用在后台进行滚动操作。如果是用户在滚动一个应用,那么它总是在前台,无需担心任何负面影响。

分享到:
评论

相关推荐

    C++编程思想第一卷

    一本提高编程思想的经典书籍!内含很多经典例题!只要都上机测试下,很快可以提高编程思想!真的是不错的一本书籍!

    《java编程思想》_java编程思想_java编程思想_ThinkinginJava_mileefx_

    学习Java必看的一本书啊,看完了会提高很多

    C++编程思想 原书第二版

    该书为C++ 编程思想第二版。清晰,易于阅读。对提高C++能力有非常大的帮助。

    C++编程思想(超清).pdf

    承和初级的C++开发人员,有助于提高编程思路

    单片机高级裸编程思想_system_

    本书以单片机裸环境为基础,为编程者定义了一个微操作系统的编程环境,并面向应用中不断提高的需求对编程策略进行了深度剖析与研究,从而分离出数据驱动、并行多任务、面向对象等重要编程思想。这些思想既可独立运用...

    C++编程思想PDF教程

    C++编程思想教程,希望能帮助你理解编程的思想,提高编程水平!

    编程思维提高习题 编程试题

    编程思维提高习题\编程思维提高习题编程思维提高习题

    C++编程思想--快速提高编程水平

    此资源可以帮助大家很好的学习C++编程!

    对C++编程思想的认识

    C++编程思想,提高对c++编程的认识,提高编程能力

    Delphi面向对象编程思想

    万丈高楼根基起,练好基本功,提高综合能力,做出更好的产品

    疯狂Android讲义_4

    本书只是一本介绍Android实际开发的图书,这不是一本关于所谓“思想”的书,不要指望学习本书能提高你所谓的“Android思想”,所以奉劝那些希望提高编程思想的读者不要阅读本书。本书更不是一本看完之后可以“吹嘘、...

    C++编程思想全集

    C++编程思想第一卷;C++编程思想第二卷.实用编程技术; PDF版本

    疯狂Android讲义_1

    本书只是一本介绍Android实际开发的图书,这不是一本关于所谓“思想”的书,不要指望学习本书能提高你所谓的“Android思想”,所以奉劝那些希望提高编程思想的读者不要阅读本书。本书更不是一本看完之后可以“吹嘘、...

    疯狂Android讲义_3

    本书只是一本介绍Android实际开发的图书,这不是一本关于所谓“思想”的书,不要指望学习本书能提高你所谓的“Android思想”,所以奉劝那些希望提高编程思想的读者不要阅读本书。本书更不是一本看完之后可以“吹嘘、...

    疯狂Android讲义_2

    本书只是一本介绍Android实际开发的图书,这不是一本关于所谓“思想”的书,不要指望学习本书能提高你所谓的“Android思想”,所以奉劝那些希望提高编程思想的读者不要阅读本书。本书更不是一本看完之后可以“吹嘘、...

    疯狂Android讲义(part03)

    本书只是一本介绍Android实际开发的图书,这不是一本关于所谓“思想”的书,不要指望学习本书能提高你所谓的“Android思想”,所以奉劝那些希望提高编程思想的读者不要阅读本书。本书更不是一本看完之后可以“吹嘘、...

    疯狂Android讲义(part06)

    本书只是一本介绍Android实际开发的图书,这不是一本关于所谓“思想”的书,不要指望学习本书能提高你所谓的“Android思想”,所以奉劝那些希望提高编程思想的读者不要阅读本书。本书更不是一本看完之后可以“吹嘘、...

    疯狂Android讲义(part04)

    本书只是一本介绍Android实际开发的图书,这不是一本关于所谓“思想”的书,不要指望学习本书能提高你所谓的“Android思想”,所以奉劝那些希望提高编程思想的读者不要阅读本书。本书更不是一本看完之后可以“吹嘘、...

    疯狂Android讲义(part05)

    本书只是一本介绍Android实际开发的图书,这不是一本关于所谓“思想”的书,不要指望学习本书能提高你所谓的“Android思想”,所以奉劝那些希望提高编程思想的读者不要阅读本书。本书更不是一本看完之后可以“吹嘘、...

    疯狂Android讲义(part01)

    本书只是一本介绍Android实际开发的图书,这不是一本关于所谓“思想”的书,不要指望学习本书能提高你所谓的“Android思想”,所以奉劝那些希望提高编程思想的读者不要阅读本书。本书更不是一本看完之后可以“吹嘘、...

Global site tag (gtag.js) - Google Analytics