招聘网站可做哪些推广方案,网站哪家公司做得好,wordpress显示英文版,网站建设改版虚函数和抽象函数有什么区别 虚函数是有代码的并明确允许子类去覆盖#xff0c;但子类也可不覆盖,就是说可以直接用#xff0c;不用重写 抽象函数是没有代码#xff0c;子类继承后一定要重写 ****************************************************************** 在一…虚函数和抽象函数有什么区别 虚函数是有代码的并明确允许子类去覆盖但子类也可不覆盖,就是说可以直接用不用重写 抽象函数是没有代码子类继承后一定要重写 ****************************************************************** 在一个类中用虚函数 是因为在超类中的有实际代码的方法但明确允许子类可以作重写 而且当子类重写后可以用子类实例超类如果这样超类变量调用虚函数时执行的是子类的方法 在一个类中用抽象函数 是在写超类时不确定函数的代码让子类去实现 ****************************************************************** 抽象函数没有方法体。 c4和cc4的区别。
String abcaaa; char cabc.charAt(i); c4;
以上如果把c4; 改成cc4; 就不正确。 ********************************************************************************************************************************************************************************************************************************************************************
c4; cc4;
当c是int类型的时候这两个表达式是一样的但是c不是int时这两个表达式是不一样的。
这两个表达式都被称为赋值表达式。第二条语句使用的是简单赋值操作符而第一条语句使用的是复合赋值操作符。复合赋值操作符包括 、-、*、/、%、、、、、^和|Java语言规范中讲到复合赋值 E1 op E2等价于简单赋值E1 (T)((E1)op(E2))其中T是E1的类型除非E1只被计算一次。 换句话说复合赋值表达式自动地将它们所执行的计算的结果转型为其左侧变量的类型。 所以要让cc4 编译能通过得int ac;c(char)(a4);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/XPLinuxJava 字符串在内存中的存放方法 在 ASCII 阶段单字节字符串使用一个字节存放一个字符SBCS。比如Bob123 在内存中为 426F6231323300Bob123/0 在使用 ANSI 编码支持多种语言阶段每个字符使用一个字节或多个字节来表示MBCS因此这种方式存放的字符也被称作多字节字符。比如中文123 在中文 Windows 95 内存中为7个字节每个汉字占2个字节每个英文和数字字符占1个字节 D6D0CEC431323300中文123/0 在 UNICODE 被采用之后计算机存放字符串时改为存放每个字符在 UNICODE 字符集中的序号。目前计算机一般使用 2 个字节16 位来存放一个序号DBCS因此这种方式存放的字符也被称作宽字节字符。比如字符串 中文123 在 Windows 2000 下内存中实际存放的是 5 个序号 2D4E87653100320033000000 ← 在 x86 CPU 中低字节在前中文123/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 编码标准所规定的内容包含两层含义 使用哪些字符。也就是说哪些汉字字母和符号会被收入标准中。所包含“字符”的集合就叫做“字符集”。 规定每个“字符”分别用一个字节还是多个字节存储用哪些字节来存储这个规定就叫做“编码”。 各个国家和地区在制定编码标准的时候“字符的集合”和“编码”一般都是同时制定的。因此平常我们所说的“字符集”比如GB2312, GBK, JIS 等除了有“字符的集合”这层含义外同时也包含了“编码”的含义。 “UNICODE 字符集”包含了各种语言中使用到的所有“字符”。用来给 UNICODE 字符集编码的标准有很多种比如UTF-8, UTF-7, UTF-16, UnicodeLittle, UnicodeBig 等。 回页首 2. 字符与编码在程序中的实现 2.1 程序中的字符与字节 在 C 和 Java 中用来代表“字符”和“字节”的数据类型以及进行编码的方法 类型或操作CJava字符wchar_tchar *字节charbyteANSI 字符串char[]byte[]UNICODE 字符串wchar_t[]String字节串→字符串mbstowcs(), MultiByteToWideChar() *string new String(bytes, encoding)字符串→字节串wcstombs(), WideCharToMultiByte()bytes string.getBytes(encoding) 以上需要注意几点 Java 中的 char 代表一个“UNICODE 字符宽字节字符”而 C 中的 char 代表一个字节。 MultiByteToWideChar() 和 WideCharToMultiByte() 是 Windows API 函数。 回页首 2.2 C 中相关实现方法 声明一段字符串常量 // ANSI 字符串内容长度 7 字节 char sz[20] 中文123; // UNICODE 字符串内容长度 5 个 wchar_t10 字节 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 字符串有可能“多个字节”才能得到“一个字符”。 通常一直在英文环境下做开发的程序员们容易有这种误解。误解二在 DOSWindows 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; charsetgb2312 Content-Transfer-Encoding: base64 sbGqcrQuqO17cf4yee74bGjz9W7b3wudzA7dbQ0MQNCg0KvPKzxqO6uqO17cnnsaPW0NDEDQoNCg 最常用的 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?D6D0? 如果阅读邮件时出现乱码一般是因为“字符编码”或“传输编码”指定有误或者是没有指定。比如有的发邮件组件在发送邮件时标题 中 // 错误的标题格式 Subject: ?ISO-8859-1?Q?D6D0? 这样的表示实际上是明确指明了标题为 [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属于一种特殊情况它们全都是小写字母即便中间的单词亦是如此。对于域名扩展名称如comorgnet或者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设置成0float和 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 filecopyright.html %。该指令在编译时引入指定的资源。在编译之前带有include指令的页面和指定的资源被合并成一个文件。被引用的外部资源在编译时就确定比运行时才确定资源更高效。 include动作例如jsp:include pagecopyright.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。然而如果不求助于JNIJava不能确定网卡的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 sessionfalse% 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 time0; 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还提供了两个高精度计算的ClassesBigInteger可以精确表示任意长度整数数值不会在运算过程中丧失任何信息和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、CBuilder并不属于C的范畴。CBuilder中的事件处理器必须用关键字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 SocketChannelDemo20
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 servernew 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 Exception65
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 // count0说明已经读取完毕
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)