2004年08月23日

UNIX系统为程序员提供了许多子程序,这些子程序可存取各种安全属性.有些是信息子程序,返回文件属性,实际的和有效的UID,GID等信息.有些子程序可 改变文件属性.UID,GID等有些处理口令文件和小组文件,还有些完成加密和解密.

本文主要讨论有关系统子程序,标准C库子程序的安全,如何写安全的C程序并从root的角度介绍程序设计(仅能被root调用的子程序).

1.系统子程序

(1)I/O子程序

*creat():建立一个新文件或重写一个暂存文件.

需要两个参数:文件名和存取许可值(8进制方式).如:

creat(“/usr/pat/read_write”,0666) /* 建立存取许可方式为0666的文件 */

调用此子程序的进程必须要有建立的文件的所在目录的写和执行许可,置

给creat()的许可方式变量将被umask()设置的文件建立屏蔽值所修改,新

文件的所有者和小组由有效的UID和GID决定.

返回值为新建文件的文件描述符.

*fstat():见后面的stat().

*open():在C程序内部打开文件.

需要两个参数:文件路径名和打开方式(I,O,I&O).

如果调用此子程序的进程没有对于要打开的文件的正确存取许可(包括文

件路径上所有目录分量的搜索许可),将会引起执行失败.

如果此子程序被调用去打开不存在的文件,除非设置了O_CREAT标志,调用

<> 将不成功.此时,新文件的存取许可作为第三个参数(可被用户的umask修改).

当文件被进程打开后再改变该文件或该文件所在目录的存取许可,不影响

对该文件的I/O操作.

*read():从已由open()打开并用作输入的文件中读信息.

它并不关心该文件的存取许可.一旦文件作为输入打开,即可从该文件中读

取信息.

*write():输出信息到已由open()打开并用作输出的文件中.同read()一样

它也不关心该文件的存取许可.

(2)进程控制

*exec()族:包括execl(),execv(),execle(),execve(),execlp()和execvp()

可将一可执行模快拷贝到调用进程占有的存贮空间.正被调用进

程执行的程序将不复存在,新程序取代其位置.

这是UNIX系统中一个程序被执行的唯一方式:用将执行的程序复盖原有的

程序.

安全注意事项:

. 实际的和有效的UID和GID传递给由exec()调入的不具有SUID和SGID许

可的程序.

. 如果由exec()调入的程序有SUID和SGID许可,则有效的UID和GID将设

置给该程序的所有者或小组.

. 文件建立屏蔽值将传递给新程序.

. 除设了对exec()关闭标志的文件外,所有打开的文件都传递给新程序.

用fcntl()子程序可设置对exec()的关闭标志.

*fork():用来建立新进程.其建立的子进程是与调用fork()的进程(父进程)

完全相同的拷贝(除了进程号外)

安全注意事项:

. 子进程将继承父进程的实际和有效的UID和GID.

. 子进程继承文件方式建立屏蔽值.

. 所有打开的文件传给子进程.

*signal():允许进程处理可能发生的意外事件和中断.

需要两个参数:信号编号和信号发生时要调用的子程序.

信号编号定义在signal.h中.

信号发生时要调用的子程序可由用户编写,也可用系统给的值,如:SIG_IGN

则信号将被忽略,SIG_DFL则信号将按系统的缺省方式处理.

如许多与安全有关的程序禁止终端发中断信息(BREAK和DELETE),以免自己

被用户终端终止运行.

有些信号使UNIX系统的产生进程的核心转储(进程接收到信号时所占内存

的内容,有时含有重要信息),此系统子程序可用于禁止核心转储.

(3)文件属性

*access():检测指定文件的存取能力是否符合指定的存取类型.

需要两个参数:文件名和要检测的存取类型(整数).

存取类型定义如下:

0: 检查文件是否存在

1: 检查是否可执行(搜索)

2: 检查是否可写

3: 检查是否可写和执行

4: 检查是否可读

5: 检查是否可读和执行

6: 检查是否可读可写可执行

这些数字的意义和chmod命令中规定许可方式的数字意义相同.

此子程序使用实际的UID和GID检测文件的存取能力(一般有效的UID和GID

用于检查文件存取能力).

返回值: 0:许可 -1:不许可.

*chmod():将指定文件或目录的存取许可方式改成新的许可方式.

需要两个参数:文件名和新的存取许可方式.

*chown():同时改变指定文件的所有者和小组的UID和GID.(与chown命令不

同).

由于此子程序同时改变文件的所有者和小组,故必须取消所操作文件的SUID

和SGID许可,以防止用户建立SUID和SGID程序,然后运行chown()去获得别

人的权限.

*stat():返回文件的状态(属性).

需要两个参数:文件路径名和一个结构指针,指向状态信息的存放

的位置.

结构定义如下:

st_mode: 文件类型和存取许可方式

st_ino: I节点号

st_dev: 文件所在设备的ID

st_rdev: 特别文件的ID

st_nlink: 文件链接数

st_uid: 文件所有者的UID

st_gid: 文件小组的GID

st_size: 按字节计数的文件大小

st_atime: 最后存取时间(读)

st_mtime: 最后修改时间(写)和最后状态的改变

st_ctime: 最后的状态修改时间

返回值: 0:成功 1:失败

*umask():将调用进程及其子进程的文件建立屏蔽值设置为指定的存取许可.

需要一个参数: 新的文件建立屏值.

(4)UID和GID的处理

*getuid():返回进程的实际UID.

*getgid():返回进程的实际GID.

以上两个子程序可用于确定是谁在运行进程.

*geteuid():返回进程的有效UID.

*getegid():返回进程的有效GID.

以上两个子程序可在一个程序不得不确定它是否在运行某用户而不是运行

它的用户的SUID程序时很有用,可调用它们来检查确认本程序的确是以该

用户的SUID许可在运行.

*setuid():用于改变有效的UID.

对于一般用户,此子程序仅对要在有效和实际的UID之间变换的SUID程序才

有用(从原有效UID变换为实际UID),以保护进程不受到安全危害.实际上该

进程不再是SUID方式运行.

*setgid():用于改变有效的GID.

2.标准C库

(1)标准I/O

*fopen():打开一个文件供读或写,安全方面的考虑同open()一样.

*fread(),getc(),fgetc(),gets(),scanf()和fscanf():从已由fopen()打

开供读的文件中读取信息.它们并不关心文件的存取许可.这一点

同read().

*fwrite(),put(),fputc(),puts,fputs(),printf(),fprintf():写信息到

已由fopen()打开供写的文件中.它们也不关心文件的存取许可.

同write().

*getpass():从终端上读至多8个字符长的口令,不回显用户输入的字符.

需要一个参数: 提示信息.

该子程序将提示信息显示在终端上,禁止字符回显功能,从/dev/tty读取口

令,然后再恢复字符回显功能,返回刚敲入的口令的指针.

*popen():将在(5)运行shell中介绍.

(2)/etc/passwd处理

有一组子程序可对/etc/passwd文件进行方便的存取,可对文件读取到入口

项或写新的入口项或更新等等.

*getpwuid():从/etc/passwd文件中获取指定的UID的入口项.

*getpwnam():对于指定的登录名,在/etc/passwd文件检索入口项.

以上两个子程序返回一指向passwd结构的指针,该结构定义在

/usr/include/pwd.h中,定义如下:

struct passwd {

char * pw_name; /* 登录名 */

char * pw_passwd; /* 加密后的口令 */

uid_t pw_uid; /* UID */

gid_t pw_gid; /* GID */

char * pw_age; /* 代理信息 */

char * pw_comment; /* 注释 */

char * pw_gecos;

char * pw_dir; /* 主目录 */

char * pw_shell; /* 使用的shell */

};

*getpwent(),setpwent(),endpwent():对口令文件作后续处理.

首次调用getpwent(),打开/etc/passwd并返回指向文件中第一个入口项的

指针,保持调用之间文件的打开状态.

再调用getpwent()可顺序地返回口令文件中的各入口项.

调用setpwent()把口令文件的指针重新置为文件的开始处.

使用完口令文件后调用endpwent()关闭口令文件.

*putpwent():修改或增加/etc/passwd文件中的入口项.

此子程序将入口项写到一个指定的文件中,一般是一个临时文件,直接写口

令文件是很危险的.最好在执行前做文件封锁,使两个程序不能同时写一个

文件.算法如下:

. 建立一个独立的临时文件,即/etc/passnnn,nnn是PID号.

. 建立新产生的临时文件和标准临时文件/etc/ptmp的链,若建链失败,

则为有人正在使用/etc/ptmp,等待直到/etc/ptmp可用为止或退出.

. 将/etc/passwd拷贝到/etc/ptmp,可对此文件做任何修改.

. 将/etc/passwd移到备份文件/etc/opasswd.

. 建立/etc/ptmp和/etc/passwd的链.

. 断开/etc/passnnn与/etc/ptmp的链.

注意:临时文件应建立在/etc目录,才能保证文件处于同一文件系统中,建

链才能成功,且临时文件不会不安全.此外,若新文件已存在,即便建

链的是root用户,也将失败,从而保证了一旦临时文件成功地建链后

没有人能再插进来干扰.当然,使用临时文件的程序应确保清除所有

临时文件,正确地捕捉信号.

(3)/etc/group的处理

有一组类似于前面的子程序处理/etc/group的信息,使用时必须用include

语句将/usr/include/grp.h文件加入到自己的程序中.该文件定义了group

结构,将由getgrnam(),getgrgid(),getgrent()返回group结构指针.

*getgrnam():在/etc/group文件中搜索指定的小组名,然后返回指向小组入

口项的指针.

*getgrgid():类似于前一子程序,不同的是搜索指定的GID.

*getgrent():返回group文件中的下一个入口项.

*setgrent():将group文件的文件指针恢复到文件的起点.

*endgrent():用于完成工作后,关闭group文件.

*getuid():返回调用进程的实际UID.

*getpruid():以getuid()返回的实际UID为参数,确定与实际UID相应的登录

名,或指定一UID为参数.

*getlogin():返回在终端上登录的用户的指针.

系统依次检查STDIN,STDOUT,STDERR是否与终端相联,与终端相联的标准输

入用于确定终端名,终端名用于查找列于/etc/utmp文件中的用户,该文件

由login维护,由who程序用来确认用户.

*cuserid():首先调用getlogin(),若getlogin()返回NULL指针,再调用

getpwuid(getuid()).

*以下为命令:

*logname:列出登录进终端的用户名.

*who am i:显示出运行这条命令的用户的登录名.

*id:显示实际的UID和GID(若有效的UID和GID和实际的不同时也显示有效的

UID和GID)和相应的登录名.

(4)加密子程序

1977年1月,NBS宣布一个用于美国联邦政府ADP系统的网络的标准加密法:数

据加密标准即DES用于非机密应用方面.DES一次处理64BITS的块,56位的加

密键.

*setkey(),encrypt():提供用户对DES的存取.

此两子程序都取64BITS长的字符数组,数组中的每个元素代表一个位,为0

或1.setkey()设置将按DES处理的加密键,忽略每第8位构成一个56位的加

密键.encrypt()然后加密或解密给定的64BITS长的一块,加密或解密取决

于该子程序的第二个变元,0:加密 1:解密.

*crypt():是UNIX系统中的口令加密程序,也被/usr/lib/makekey命令调用.

crypt()子程序与crypt命令无关,它与/usr/lib/makekey一样取8个字符长

的关键词,2个salt字符.关键词送给setkey(),salt字符用于混合encrypt()

中的DES算法,最终调用encrypt()重复25次加密一个相同的字符串.

返回加密后的字符串指针.

(5)运行shell

*system():运行/bin/sh执行其参数指定的命令,当指定命令完成时返回.

*popen():类似于system(),不同的是命令运行时,其标准输入或输出联到由

popen()返回的文件指针.

二者都调用fork(),exec(),popen()还调用pipe(),完成各自的工作,因而

fork()和exec()的安全方面的考虑开始起作用.

3.写安全的C程序

一般有两方面的安全问题,在写程序时必须考虑:

(1)确保自己建立的任何临时文件不含有机密数据,如果有机密数据,设置

临时文件仅对自己可读/写.确保建立临时文件的目录仅对自己可写.

(2)确保自己要运行的任何命令(通过system(),popen(),execlp(),

execvp()运行的命令)的确是自己要运行的命令,而不是其它什么命

令,尤其是自己的程序为SUID或SGID许可时要小心.

第一方面比较简单,在程序开始前调用umask(077).若要使文件对其他人可

读,可再调chmod(),也可用下述语名建立一个”不可见”的临时文件.

creat(“/tmp/xxx”,0);

file=open(“/tmp/xxx”,O_RDWR);

unlink(“/tmp/xxx”);

文件/tmp/xxx建立后,打开,然后断开链,但是分配给该文件的存储器并未删

除,直到最终指向该文件的文件通道被关闭时才被删除.打开该文件的进程

和它的任何子进程都可存取这个临时文件,而其它进程不能存取该文件,因

为它在/tmp中的目录项已被unlink()删除.

第二方面比较复杂而微妙,由于system(),popen(),execlp(),execvp()执行

时,若不给出执行命令的全路径,就能”骗”用户的程序去执行不同的命令.因

为系统子程序是根据PATH变量确定哪种顺序搜索哪些目录,以寻找指定的命

令,这称为SUID陷井.最安全的办法是在调用system()前将有效UID改变成实

际UID,另一种比较好的方法是以全路径名命令作为参数.execl(),execv(),

execle(),execve()都要求全路径名作为参数.有关SUID陷井的另一方式是

在程序中设置PATH,由于system()和popen()都启动shell,故可使用shell句

法.如:

system(“PATH=/bin:/usr/bin cd”);

这样允许用户运行系统命令而不必知道要执行的命令在哪个目录中,但这种

方法不能用于execlp(),execvp()中,因为它们不能启动shell执行调用序列

传递的命令字符串.

关于shell解释传递给system()和popen()的命令行的方式,有两个其它的问

题:

*shell使用IFS shell变量中的字符,将命令行分解成单词(通常这个

shell变量中是空格,tab,换行),如IFS中是/,字符串/bin/ed被解释成单词

bin,接下来是单词ed,从而引起命令行的曲解.

再强调一次:在通过自己的程序运行另一个程序前,应将有效UID改为实际的

UID,等另一个程序退出后,再将有效UID改回原来的有效UID.

SUID/SGID程序指导准则

(1)不要写SUID/SGID程序,大多数时候无此必要.

(2)设置SGID许可,不要设置SUID许可.应独自建立一个新的小组.

(3)不要用exec()执行任何程序.记住exec()也被system()和popen()调用.

. 若要调用exec()(或system(),popen()),应事先用setgid(getgid())

将有效GID置加实际GID.

. 若不能用setgid(),则调用system()或popen()时,应设置IFS:

popen(“IFS=\t\n;export IFS;/bin/ls”,”r”);

. 使用要执行的命令的全路径名.

. 若不能使用全路径名,则应在命令前先设置PATH:

popen(“IFS=\t\n;export IFS;PATH=/bin:/usr/bin;/bin/ls”,”r”);

. 不要将用户规定的参数传给system()或popen();若无法避免则应检查

变元字符串中是否有特殊的shell字符.

. 若用户有个大程序,调用exec()执行许多其它程序,这种情况下不要将

大程序设置为SGID许可.可以写一个(或多个)更小,更简单的SGID程序

执行必须具有SGID许可的任务,然后由大程序执行这些小SGID程序.

(4)若用户必须使用SUID而不是SGID,以相同的顺序记住(2),(3)项内容,并

相应调整.不要设置root的SUID许可.选一个其它户头.

(5)若用户想给予其他人执行自己的shell程序的许可,但又不想让他们能

读该程序,可将程序设置为仅执行许可,并只能通过自己的shell程序来

运行.

编译,安装SUID/SGID程序时应按下面的方法

(1)确保所有的SUID(SGID)程序是对于小组和其他用户都是不可写的,存取

权限的限制低于4755(2755)将带来麻烦.只能更严格.4111(2111)将使

其他人无法寻找程序中的安全漏洞.

(2)警惕外来的编码和make/install方法

. 某些make/install方法不加选择地建立SUID/SGID程序.

. 检查违背上述指导原则的SUID/SGID许可的编码.

. 检查makefile文件中可能建立SUID/SGID文件的命令.

4.root程序的设计

有若干个子程序可以从有效UID为0的进程中调用.许多前面提到的子程序,

当从root进程中调用时,将完成和原来不同的处理.主要是忽略了许可权限的检

查.

由root用户运行的程序当然是root进程(SUID除外),因有效UID用于确定文

件的存取权限,所以从具有root的程序中,调用fork()产生的进程,也是root进程.

(1)setuid():从root进程调用setuid()时,其处理有所不同,setuid()将把有

效的和实际的UID都置为指定的值.这个值可以是任何整型数.而对非root

进程则仅能以实际UID或本进程原来有效的UID为变量值调用setuid().

(2)setgid():在系统进程中调用setgid()时,与setuid()类似,将实际和有效

的GID都改变成其参数指定的值.

* 调用以上两个子程序时,应当注意下面几点:

. 调用一次setuid()(setgid())将同时设置有效和实际UID(GID),独立分

别设置有效或实际UID(GID)固然很好,但无法做到这点.

. setuid()(setgid())可将有效和实际UID(GID)设置成任何整型数,其数

值不必一定与/etc/passwd(/etc/group)中用户(小组)相关联.

. 一旦程序以一个用户的UID了setuid(),该程序就不再做为root运行,也

不可能再获root特权.

(3)chown():当root进程运行chown()时,chown()将不删除文件的SUID和/或

SGID许可,但当非root进程运行chown()时,chown()将取消文件的SUID和/

或SGID许可.

(4)chroot():改变进程对根目录的概念,调用chroot()后,进程就不能把当前

工作目录改变到新的根目录以上的任一目录,所有以/开始的路径搜索,都

从新的根目录开始.

(5)mknod():用于建立一个文件,类似于creat(),差别是mknod()不返回所打开

文件的文件描述符,并且能建立任何类型的文件(普通文件,特殊文件,目录

文件).若从非root进程调用mknod()将执行失败,只有建立FIFO特别文件

(有名管道文件)时例外,其它任何情况下,必须从root进程调用mknod().由

于creat()仅能建立普通文件,mknod()是建立目录文件的唯一途径,因而仅

有root能建立目录,这就是为什么mkdir命令具有SUID许可并属root所有.

一般不从程序中调用mknod().通常用/etc/mknod命令建立特别设备文件而

这些文件一般不能在使用着时建立和删除,mkdir命令用于建立目录.当用

mknod()建立特别文件时,应当注意确从所建的特别文件不允许存取内存,

磁盘,终端和其它设备.

(6)unlink():用于删除文件.参数是要删除文件的路径名指针.当指定了目录

时,必须从root进程调用unlink(),这是必须从root进程调用unlink()的唯

一情况,这就是为什么rmdir命令具有root的SGID许可的原因.

(7)mount(),umount():由root进程调用,分别用于安装和拆卸文件系统.这两

个子程序也被mount和umount命令调用,其参数基本和命令的参数相同.调

用mount(),需要给出一个特别文件和一个目录的指针,特别文件上的文件

系统就将安装在该目录下,调用时还要给出一个标识选项,指定被安装的文

件系统要被读/写(0)还是仅读(1).umount()的参数是要一个要拆卸的特别

文件的指针.

本文由isbase成员编译或原创,如要转载请保持文章的完整性

欢迎访问我们的站点http://www.isbase.com

2004年08月22日

1.源程序的编译
在Linux下面,如果要编译一个C语言源程序,我们要使用GNU的gcc编译器. 下面我们以一个实例来说明如何使用gcc编译器.
假设我们有下面一个非常简单的源程序(hello.c):
int main(int argc,char **argv)
{
printf(“Hello Linux\n”);
}

要编译这个程序,我们只要在命令行下执行:
gcc -o hello hello.c
gcc 编译器就会为我们生成一个hello的可执行文件.执行./hello就可以看到程序的输出结果了.命令行中 gcc表示我们是用gcc来编译我们的源程序,-o 选项表示我们要求编译器给我们输出的可执行文件名为hello 而hello.c是我们的源程序文件.
gcc编译器有许多选项,一般来说我们只要知道其中的几个就够了. -o选项我们已经知道了,表示我们要求输出的可执行文件名. -c选项表示我们只要求编译器输出目标代码,而不必要输出可执行文件. -g选项表示我们要求编译器在编译的时候提供我们以后对程序进行调试的信息.
知道了这三个选项,我们就可以编译我们自己所写的简单的源程序了,如果你想要知道更多的选项,可以查看gcc的帮助文档,那里有着许多对其它选项的详细说明.
2.Makefile的编写
假设我们有下面这样的一个程序,源代码如下:

/* main.c */
#include “mytool1.h”
#include “mytool2.h”

int main(int argc,char **argv)
{
mytool1_print(“hello”);
mytool2_print(“hello”);
}

/* mytool1.h */
#ifndef _MYTOOL_1_H
#define _MYTOOL_1_H

void mytool1_print(char *print_str);

#endif

/* mytool1.c */
#include “mytool1.h”
void mytool1_print(char *print_str)
{
printf(“This is mytool1 print %s\n”,print_str);
}

/* mytool2.h */
#ifndef _MYTOOL_2_H
#define _MYTOOL_2_H

void mytool2_print(char *print_str);

#endif

/* mytool2.c */
#include “mytool2.h”
void mytool2_print(char *print_str)
{
printf(“This is mytool2 print %s\n”,print_str);
}

当然由于这个程序是很短的我们可以这样来编译
gcc -c main.c
gcc -c mytool1.c
gcc -c mytool2.c
gcc -o main main.o mytool1.o mytool2.o
这 样的话我们也可以产生main程序,而且也不时很麻烦.但是如果我们考虑一下如果有一天我们修改了其中的一个文件(比如说mytool1.c)那么我们难 道还要重新输入上面的命令?也许你会说,这个很容易解决啊,我写一个SHELL脚本,让她帮我去完成不就可以了.是的对于这个程序来说,是可以起到作用 的.但是当我们把事情想的更复杂一点,如果我们的程序有几百个源程序的时候,难道也要编译器重新一个一个的去编译?
为此,聪明的程序员们想出 了一个很好的工具来做这件事情,这就是make.我们只要执行以下make,就可以把上面的问题解决掉.在我们执行make之前,我们要先编写一个非常重 要的文件.–Makefile.对于上面的那个程序来说,可能的一个Makefile的文件是:
# 这是上面那个程序的Makefile文件
main:main.o mytool1.o mytool2.o
gcc -o main main.o mytool1.o mytool2.o
main.o:main.c mytool1.h mytool2.h
gcc -c main.c
mytool1.o:mytool1.c mytool1.h
gcc -c mytool1.c
mytool2.o:mytool2.c mytool2.h
gcc -c mytool2.c

有了这个Makefile文件,不过我们什么时候修改了源程序当中的什么文件,我们只要执行make命令,我们的编译器都只会去编译和我们修改的文件有关的文件,其它的文件她连理都不想去理的.
下面我们学习Makefile是如何编写的.
在Makefile中也#开始的行都是注释行.Makefile中最重要的是描述文件的依赖关系的说明.一般的格式是:
target: components
TAB rule

第一行表示的是依赖关系.第二行是规则.
比如说我们上面的那个Makefile文件的第二行
main:main.o mytool1.o mytool2.o
表 示我们的目标(target)main的依赖对象(components)是main.o mytool1.o mytool2.o 当倚赖的对象在目标修改后修改的话,就要去执行规则一行所指定的命令.就象我们的上面那个Makefile第三行所说的一样要执行 gcc -o main main.o mytool1.o mytool2.o 注意规则一行中的TAB表示那里是一个TAB键
Makefile有三个非常有用的变量.分别是$@,$^,$<代表的意义分别是:
$@–目标文件,$^–所有的依赖文件,$<–第一个依赖文件.
如果我们使用上面三个变量,那么我们可以简化我们的Makefile文件为:
# 这是简化后的Makefile
main:main.o mytool1.o mytool2.o
gcc -o $@ $^
main.o:main.c mytool1.h mytool2.h
gcc -c $<
mytool1.o:mytool1.c mytool1.h
gcc -c $<
mytool2.o:mytool2.c mytool2.h
gcc -c $<

经过简化后我们的Makefile是简单了一点,不过人们有时候还想简单一点.这里我们学习一个Makefile的缺省规则
.c.o:
gcc -c $<

这个规则表示所有的 .o文件都是依赖与相应的.c文件的.例如mytool.o依赖于mytool.c这样Makefile还可以变为:
# 这是再一次简化后的Makefile
main:main.o mytool1.o mytool2.o
gcc -o $@ $^
.c.o:
gcc -c $<

好了,我们的Makefile 也差不多了,如果想知道更多的关于Makefile规则可以查看相应的文档.
3.程序库的链接
试着编译下面这个程序

/* temp.c */
#include

int main(int argc,char **argv)
{
double value;
printf(“value:%f\n”,value);
}

这个程序相当简单,但是当我们用 gcc -o temp temp.c 编译时会出现下面所示的错误.
/tmp/cc33Kydu.o: In function `main:
/tmp/cc33Kydu.o(.text+0xe): undefined reference to `log
collect2: ld returned 1 exit status

出 现这个错误是因为编译器找不到log的具体实现.虽然我们包括了正确的头文件,但是我们在编译的时候还是要连接确定的库.在Linux下,为了使用数学函 数,我们必须和数学库连接,为此我们要加入 -lm 选项. gcc -o temp temp.c -lm这样才能够正确的编译.也许有人要问,前面我们用printf函数的时候怎么没有连接库呢?是这样的,对于一些常用的函数的实现,gcc编译器会自 动去连接一些常用库,这样我们就没有必要自己去指定了. 有时候我们在编译程序的时候还要指定库的路径,这个时候我们要用到编译器的 -L选项指定路径.比如说我们有一个库在 /home/hoyt/mylib下,这样我们编译的时候还要加上 -L/home/hoyt/mylib.对于一些标准库来说,我们没有必要指出路径.只要它们在起缺省库的路径下就可以了.系统的缺省库的路径/lib /usr/lib /usr/local/lib 在这三个路径下面的库,我们可以不指定路径.
还有一个问题,有时候我们使用了某个函数,但 是我们不知道库的名字,这个时候怎么办呢?很抱歉,对于这个问题我也不知道答案,我只有一个傻办法.首先,我到标准库路径下面去找看看有没有和我用的函数 相关的库,我就这样找到了线程(thread)函数的库文件(libpthread.a). 当然,如果找不到,只有一个笨方法.比如我要找sin这个函数所在的库. 就只好用 nm -o /lib/*.so|grep sin>~/sin 命令,然后看~/sin文件,到那里面去找了. 在sin文件当中,我会找到这样的一行libm-2.1.2.so:00009fa0 W sin 这样我就知道了sin在 libm-2.1.2.so库里面,我用 -lm选项就可以了(去掉前面的lib和后面的版本标志,就剩下m了所以是 -lm). 如果你知道怎么找,请赶快告诉我,我回非常感激的.谢谢!
4.程序的调试
我们编写的程序不太可能一次性就会成功的,在我们的程序当中,会出现许许多多我们想不到的错误,这个时候我们就要对我们的程序进行调试了.
最 常用的调试软件是gdb.如果你想在图形界面下调试程序,那么你现在可以选择xxgdb.记得要在编译的时候加入 -g选项.关于gdb的使用可以看gdb的帮助文件.由于我没有用过这个软件,所以我也不能够说出如何使用. 不过我不喜欢用gdb.跟踪一个程序是很烦的事情,我一般用在程序当中输出中间变量的值来调试程序的.当然你可以选择自己的办法,没有必要去学别人的.现 在有了许多IDE环境,里面已经自己带了调试器了.你可以选择几个试一试找出自己喜欢的一个用.

5.头文件和系统求助
有时候我们只知道一个函数的大概形式,不记得确切的表达式,或者是不记得着函数在那个头文件进行了说明.这个时候我们可以求助系统.
比 如说我们想知道fread这个函数的确切形式,我们只要执行 man fread 系统就会输出着函数的详细解释的.和这个函数所在的头文件说明了. 如果我们要write这个函数的说明,当我们执行man write时,输出的结果却不是我们所需要的. 因为我们要的是write这个函数的说明,可是出来的却是write这个命令的说明.为了得到write的函数说明我们要用 man 2 write. 2表示我们用的write这个函数是系统调用函数,还有一个我们常用的是3表示函数是C的库函数.
记住不管什么时候,man都是我们的最好助手.

—————————————————————-
好了,这一章就讲这么多了,有了这些知识我们就可以进入激动人心的Linux下的C程序探险活动.

2004年05月21日

by mvm


这七十五条,是我这些年来,尤其是在微软工作两年来的体会的总结,关于如何用正确的方法来写出质量好的软件的体会的总结。或许看似平淡无奇,但大音希声,这七十五条的效用,未必及不上那几十页几百页的体系,却远远比那好用:


1. 你们的项目组使用源代码管理工具了么?
2. 你们的项目组使用缺陷管理系统了么?
3. 你们的测试组还在用Word写测试用例么?
4. 你们的项目组有没有建立一个门户网站?
5. 你们的项目组用了你能买到最好的工具么?
6. 你们的程序员工作在安静的环境里么?
7. 你们的员工每个人都有一部电话么?
8. 你们每个人都知道出了问题应该找谁么?
9. 你遇到过有人说“我以为…”么?
10. 你们的项目组中所有的人都坐在一起么?
11. 你们的进度表是否反映最新开发进展情况?
12. 你们的工作量是先由每个人自己估算的么?
13. 你们的开发人员从项目一开始就加班么?
14. 你们的项目计划中Buffer Time是加在每个小任务后面的么?
15. 值得再多花一些时间,从95%做到100%好
16. 登记新缺陷时,是否写清了重现步骤?
17. 写新代码前会把已知缺陷解决么?
18. 你们对缺陷的轻重缓急有事先的约定么?
19. 你们对意见不一的缺陷有三国会议么?
20. 所有的缺陷都是由登记的人最后关闭的么?
21. 你们的程序员厌恶修改老的代码么?
22. 你们项目组有Team Morale Activity么?
23. 你们项目组有自己的Logo么?
24. 你们的员工有印有公司Logo的T-Shirt么?
25. 总经理至少每月参加一次项目组会议
26. 你们是给每个Dev开一个分支么?
27. 有人长期不Check-In代码么?
28. 在Check-In代码时都填写注释了么?
29. 有没有设定每天Check-In的最后期限?
30. 你们能把所有源码一下子编译成安装文件吗?
31. 你们的项目组做每日编译么?
32. 你们公司有没有积累一个项目风险列表?
33. 设计越简单越好
34. 尽量利用现有的产品、技术、代码
35. 你们会隔一段时间就停下来夯实代码么?
36. 你们的项目组每个人都写Daily Report么?
37. 你们的项目经理会发出Weekly Report么?
38. 你们项目组是否至少每周全体开会一次?
39. 你们项目组的会议、讨论都有记录么?
40. 其他部门知道你们项目组在干什么么?
41. 通过Email进行所有正式沟通
42. 为项目组建立多个Mailing Group
43. 每个人都知道哪里可以找到全部的文档么?
44. 你做决定、做变化时,告诉大家原因了么?
45. Stay agile and expect change
46. 你们有没有专职的软件测试人员?
47. 你们的测试有一份总的计划来规定做什么和怎么做么?
48. 你是先写Test Case然后再测试的么?
49. 你是否会为各种输入组合创建测试用例?
50. 你们的程序员能看到测试用例么?
51. 你们是否随便抓一些人来做易用性测试?
52. 你对自动测试的期望正确么?
53. 你们的性能测试是等所有功能都开发完才做的么?
54. 你注意到测试中的杀虫剂效应了么?
55. 你们项目组中有人能说出产品的当前整体质量情况么?
56. 你们有单元测试么?
57. 你们的程序员是写完代码就扔过墙的么?
58. 你们的程序中所有的函数都有输入检查么?
59. 产品有统一的错误处理机制和报错界面么?
60. 你们有统一的代码书写规范么?
61. 你们的每个人都了解项目的商业意义么?
62. 产品各部分的界面和操作习惯一致么?
63. 有可以作为宣传亮点的Cool Feature么?
64. 尽可能缩短产品的启动时间
65. 不要过于注重内在品质而忽视了第一眼的外在印象
66. 你们根据详细产品功能说明书做开发么?
67. 开始开发和测试之前每个人都仔细审阅功能设计么?
68. 所有人都始终想着The Whole Image么?
69. Dev工作的划分是单纯纵向或横向的么?
70. 你们的程序员写程序设计说明文档么?
71. 你在招人面试时让他写一段程序么?
72. 你们有没有技术交流讲座?
73. 你们的程序员都能专注于一件事情么?
74. 你们的程序员会夸大完成某项工作所需要的时间么?
75. 尽量不要用Virtual Heads

2004年05月02日

基本篇


作者:flyingwcy 转载自:Java研究组织

本文介绍的JAVA规则的说明分为5个级别,级别1是最基本也是最重要的级别,在今后将陆续写出其他的规则。遵守了这些规则可以提高程序的效率、使代码有更好的可读性等。

(1) 避免使用NEW关键字来创建String对象。
把一个String常量copy到String 对象中通常是多余、浪费时间的
Public class test{
Public void method(){
System.out.print (str);
}
private String str = new String (“1″); //这里新建对象是完全没有必要的
private String str2=”2” //正确的应该如此
}
参考:Joshua Bloch: “Effective Java – Programming Language Guide”


(2) 避免使用不必要的嵌套。
过多的嵌套会使你的代码复杂化,减弱可读性。
Public class test {
String add (){
Int c=(a=a+b)+b; //过于复杂
Return c
}

}

参考:http://java.sun.com/docs/codeconv/html/CodeConventions.doc9.html#177

(3) 避免在同一行声明不同类型的多个变量
这样可以使程序更加清晰,避免混乱
private int index, index1[];
正确的应该如此:
private int index;
private int index1[];


参考:http://java.sun.com/docs/codeconv/html/CodeConventions.doc5.html#2992

(4) 在每一行里写一条语句
这条规则不包括for语句:比如:´for (int I = 0; I < 10; i++) x--;’可以增加代码的可读性。
public class OSPL {
int method (int a, int b) {
int I = a + b; return I; // 可读性不强
}
正确的:
public class OSPLFixed {
int method (int a, int b) {
int I = a + b;
return I;
}
}
参考:Section 7.1 of http://java.sun.com/docs/codeconv/html/CodeConventions.doc6.html#431


(5)经常从finalize ()中调用super.finalize ()
这里的finalize ()是java在进行垃圾收集的时候调用的,和finally不一样。如果你的父类没有定义finally()的话,你也应该调用。这里有两个原因:(1)在不改变代码的情况下能够将父类的finally方法加到你的类中。 (2)以后你会养成习惯调用父类的finally方法,即使父类没有定义finally方法的时候。
正确的方法应该如此:
public class parentFinalize {
protected void finalize () throws Throwable {
super.finalize(); // FIXED
}

参考:”The Java Programming Language” by Ken Arnold and James Gosling, page 49.

(6) 不要在finalize ()中注销listeners
不要再finalize ()方法中中注销listeners,finalize ()只有再没有对象引用的时候调用,如果listeners从finalize()方法中去除了,被finalize的对象将不会在垃圾收集中去除。
public void finalize () throws Throwable {
bButton.removeActionListener (act);
}

(7) 不要显式的调用finalize ()方法
虽然显式的调用这个方法可以使你确保你的调用,但是当这个方法收集了以后垃圾收集会再收集一次。

public class T7 { public void finalize() throws Throwable { close_resources (); super.finalize (); } public void close_resources() {}}class Test { void cleanup () throws Throwable { t71.finalize(); // 调用 t71 = null; } private t71 = new T7 ();}

对于这样的调用我们应该自己创建一个释放的方法,做最初finalize ()所作的事情,当你每次想显式的调用finalize ()的时候实际上调用了释放方法。然后再使用一个判断字段来确保这个方法只执行一次,以后再调用就没关系了。

public class T7 { public synchronized void release () throws Throwable{ if (!_released) { close_resources (); // do what the old ´finalize ()´ did _released = true; } } public void finalize () throws Throwable { release (); super.finalize (); } public void close_resources() {} private boolean _released = false;}class TestFixed { void closeTest () throws Throwable { t71 .release (); // FIXED t71 = null; } private T7 t71 = new T7 ();}


参考:Nigel Warren, Philip Bishop: “Java in Practice – Design Styles and Idioms
for Effective Java”. Addison-Wesley, 1999. pp.110-111


(8)不要使用不推荐的API
尽量使用JDK1.3推荐的API。在类和方法或者java组件里有很多方法是陈旧的或者是可以选择的。有一些方法SUN用了”deprecated“标记。最好不要使用例如:
private List t_list = new List ();
t_list.addItem (str);
如果查一下javadoc的话,会发现建议用add()来代替addItem()。
参考:http://java.sun.com/j2se/1.3/docs/api/index.html

(9)为所有序列化的类创建一个´serialVersionUID´
可以避免从你各种不同的类破坏序列的兼容性。如果你不特别制订一个UID的话,那么系统为自动产生一个UID(根据类的内容)。如果UID在你新版本的类中改变了,即使那个被序列化的类没改变,你也不能反序列化老的版本了。

public class DUID implements java.io.Serializable { public void method () {}}
在里面加一个UID,当这个类的序列化形式改变的时候,你也改变这个UID就可以了。

public class DUIDFixed implements java.io.Serializable { public void method () {} private static final long serialVersionUID = 1; }


参考:Joshua Bloch: “Effective Java – Programming Language Guide”
Addison Wesley, 2001, pp. 223

(10)对于private常量的定义
比较好的做法是对于这样的常量,加上final标记,这样的常量从初始化到最后结束值都不会改变。
private int size = 5;
改变后的做法是:
private final int size = 5;

(11)避免把方法本地变量和参数定义成和类变量相同的名字。
这样容易引起混扰,建议把任何的变量字都定义成唯一的。这样看来,SCJP里的那些题目在现实中就用不到了:)

public void method (int j) { final int I = 5; // VIOLATION } private int j = 2;


建议:
public void method (int j1) { final int I = 5; // VIOLATION } private int j = 2;

参考:Michael Daconta, Eric Monk, J Keller, Keith Bohnenberger: “Java Pitfalls”
John Wiley & Sons, ISBN: 0-471-36174-7 pp.17 – 25


 


中级篇

作者:flyingwcy 转载自:Java研究组织
本文介绍的JAVA规则的说明分为3个主要级别,中级是平时开发用的比较多的级别,在今后将陆续写出其他的规则。遵守了这些规则可以提高程序的效率、使代码又更好的可读性等。
(1) 在finally方法里关掉input或者output 资源
再方法体里面定义了input或者output流的话,需要在finally里面把它关掉。
以下这几种调用不需要遵守这条规则,因为colse()方法不起作用:)
java.io.StringWriter java.io.ByteArrayOutputStream java.io.ByteArrayInputStream
如果再方法返回的时候没有调用close()方法来释放input()和output()的资源的话,会导致一个系统资源泄漏。而且在任何情况下都要确定在返回全调用了close() 方法,包括出现异常的时候。所以需要在finally方法里面加入这个方法。这样就保证了在任何情况下都会关闭资源。
错误示例:
public class CIO {
public void method (java.io.File f) {
java.io.FileInputStream fis = null;
try {
fis = new java.io.FileInputStream (f);
fis.read ();
fis.close ();
} catch (java.io.FileNotFoundException e1) {
System.out.println(“File not found”);
} catch (java.io.IOException e2) {
System.out.println(“I/O Exception”);
}
// 如果出现异常,这里就不能保证关闭资源。
}
}
修正后的代码:
public class CIOFixed {
public void method (java.io.File f) {
java.io.FileInputStream fis = null;
try {
fis = new java.io.FileInputStream (f);
fis.read ();
} catch (java.io.FileNotFoundException e1) {
System.out.println(“File not found”);
} catch (java.io.IOException e2) {
System.out.println(“I/O Exception”);
} finally {
if (fis != null) {
try {
fis.close ();
} catch (java.io.IOException e) {
System.out.println(“I/O Exception”);
}
}
}
}
}

(2) else的注意问题.
一般总认为如果if语句只有一句的话,那么{}就是可要可不要的了。可是如果if有else嵌套的话,就不一样了,{}是必需的
错误示例:
if (I < 5)
if (I < 2)
i++;
else
i–;
修改后:
if (I < 5) {
if (I < 2)
i++;
}
else {
i–;
}

(3) 不要再catch()块里什么代码也不放
在catch()块里面放入一些错误处理代码是一个好的习惯。但是如果catch()里面有有关javadoc 的代码,那也是可以的。
错误示例:
try {
System.in.read ();
} catch (java.io.IOException e) {
// 错误
}

正确:
try {
System.in.read ();
} catch (java.io.IOException e) {
System.out.println(“Descriptive error”);
}
参考:Joshua Bloch: “Effective Java – Programming Language Guide”.
Addison-Wesley, 2001, pp. 187

(4)不要在if条件里面附值
如果这样做的话,系统会报告错误。在java的很多条件声明里面用附值是很不明智的,而且系统也会报告错误。很容易引起异常。遵守这条规者能够使维护简单,避免不一致。
错误示例:
if (b = true)
正确的:
if (b == true)
参考:Section 10.4 of http://java.sun.com/docs/codeconv/html/CodeConventions.doc9.html#547

(5) for语句需要循环体。
如果没有{}的话,for语句只会执行一次!
错误示例:
for (I = 0; I < 10; i++) ;
System.out.println (i);
这里print() 只会执行一次。
正确:
for (I = 0; I < 10; i++) { // FIXED
System.out.println (i);
}

(5) 不要把方法定义成main().
在java里,main()方法是一个特别的方法。所以在自己定义方法的时候不要定义这样的名字,以免引起混扰。

(6)不要直接或者间接的定义´Error´和´Throwable´的子类
´java.lang.Error´只在JVM出现反常的时候覆盖这个方法,如果你定义了直接或者不直接的类继承了类´Error´,也就指出了这个错误是JVM内部的,而不是这个类的。所以对于java编译器来说是不可见的,这样就不能检查错误的异常处理了。
´java.lang.Throwable´是´java.lang.Exception´和´java.lang.Error´的上级类,用户如果象定义异常类的话应该继承´java.lang.Exception´。
错误示例:public class ABC extends Error
正确:public class ABC extends Exception

(7)有关”switch”语句里面的”case”问题
最好在每一个 “case”里都定义一个”return”或者“break”来控制不要走到下面的 “case”里去。如果一个”case”语句在代码的最后没有一个”break”或者”return”句,程序就会走到下一个”case”。如果这个”case”是最后一个的话,那就没什么问题,如果后面还有”case” 的话,看起来就不太安全了。
错误示例:
switch (i) {
case 1:
x = 10;
break;
case 2:
x = 20;
default:
a = 40;
break;
正确:
switch (i) {
case 1:
x = 10;
break;
case 2: // VIOLATION
x = 20;
break;
default:
x = 40;
break;

(8)建议不要使用´System.getenv ()´
不建议使用´System.getenv ()´,这个方法看起来很好用,不过并不是所有的系统都有环境变量的。不用这个方法也可能带来一些不方便。
错误示例:
void method (String name) {
System.getenv (name); // 可以用其他方法来代替
}
如果不用这个方法,我们可以用其它的方法来代替。比如:´System.getProperty ()’,´getTypeName ()´等,这也可以找到java的系统属性。
参考:David Flanagan: “Java in a Nutshell”. O´Reilly
November, 1999: Third Edition, pp.190-192

(9)不要使用’\n’或者´\r´来分行
这两个标记看来很普遍,特别是’\n’。我们经常用来作为分行用。但是不同的系统用不同的分行字符,所以这些字符在某些意义上违背了java的平台无关性。
错误示例:
System.out.println(“Hello\n” + name);
我们可以用其它的一些方法来代替,比如println(),这个方法在不同的系统平台上都起到相同的作用。后者推荐大家用这个方法:System.getProperty(“line.separator”)
参考:David Flanagan: “Java in a Nutshell”. O´Reilly,
November 1999: Third Edition, pp. 191-192

(10) 使所有的内部类”private”.
Java允许一个类包含另外一个类,带是Java byte code没有这个概念。类被编译器解释成package-private类。从更深的程度来说,包含类的任何内部私有对象能被内部类访问的也能被同一个包内的其他类访问。
错误示例:
public class INNER {
class INNER_Class {
void setValue(int i) {
_value = I; // 现在包就可以访问了
}
}
private int _value;
}
所以需要加上private class INNER_Class
参考:Statically Scanning Java Code: Finding Security Vulnerabilities.
John Viega, Gary McGraw, Tom Mutdosch, and Edward W. Felten
IEEE SOFTWARE September/October 2000

(11)不要使接口序列化
如果一个字节数组包含了一个被序列化的对象。攻击者就能读到这个对象的内部状态合字段(包括private的)。
错误示例:
public interface sample extends java.io.Serializable


 


开发篇

作者:flyingwcy 转载自:Java研究组织
本文介绍的JAVA规则的说明分为3个主要级别,本篇抛弃了平时开发中很少遇到的情况,那些用得比较少的以后再高级篇里面出现。并有六个有用的国际软件开发重要注意的有关String的问题,遵守了这些规则可以提高程序的效率、使代码又更好的可读性等。
(1) 如果有JDBC连接没有关掉的话,需要在”finally”方法中关掉
如果数据库连接失败或者是没有释放连接,看上去无关紧要。但是其他的用户就需要用更长的时间等待连接,这样数据库利用效率就会下降。确保你的代码在任何情况下,包括出错或者程序异常终止的情况下都释放数据库连接。在”finally”方法中关掉连接,就可以确保这一点。
错误示例:
try {
Statement stmt = con.createStatement();
} catch(SQLException e) {
e.printStackTrace();
}
正确示例:
try {
Statement stmt = con.createStatement();
} finally {
if (con != null && !con.isClosed()) {
con.close();
}
}

(2) 尽量避免使用´Thread.resume ()´, ´Thread.stop ()´, ´Thread.suspend ()´和 ´Runtime.runFinalizersOnExit ()´ 方法。
这些方法在平时的开发或者是教科书里面也有用到过,但是这些方法会导致四锁的倾向。一下有充足的资料来说明为什么不建议用上述方法。
参考:1.”java.lang.Thread” in the JDK API documentation
2.http://java.sun.com/j2se/1.3/docs/guide/misc/threadPrimitiveDeprecation.html
3.Paul Hyde: “Java Thread Programming”
Sams, ISBN: 0-672-31585-8 pp. 270

(3) 在表示长整常量的时候,用L来代替l.
因为l很容易和1混一起。
错误示例:
long temp = 23434l;
正确示例:
long temp = 23434L;
参考:Ken Arnold, James Gosling: “The Java Programming Language Second Edition”Addison Wesley, 1997, pp.108

(4) 最好在jsp开头写一条注释
在 jsp文件头上面写一条注释,这样可以帮助别人来理解你的代码。这条规则不仅适用于jsp,更是用于任何开发的文档。
正确示例:<%-- JSP comment --%>


(5)明确的初始化一个构造类里面的所有的字段
因为没有初始化的字段会是一个潜在的bug,所以最好初始化类里面的所有的字段。特别是静态的字段,最好在一开始就分配一个初始值
错误示例:
public class CSI {
public CSI () {
this (12);
k = 0;
}

public CSI (int val) {
j = val;
}

private int I = 5;
private int j;
private int k;
}

正确示例:
public class CSIFixed {
public CSIFixed () {
this (12);
}

public CSIFixed (int val) {
j = val;
k = 0;
}

private int I = 5;
private int j;
private int k;
}
参考:http://www.ambysoft.com/javaCodingStandards.pdf

(5) 国际化开发建议:逻辑操作符不要再一个单个的字符的前面或者后面
一个单个字符的前后不要用逻辑操作符,如果代码要在一个国家环境中运行的话。我们可以使用字符比较方法,这些方法使用统一字符比较标准来定义字符的属性的。
错误示例:public class CLO {
public boolean isLetter (char ch) {
boolean _isLetter = ( ch >= ´a´ && ch <= ´z´) //错误
|| (ch >= ´A´ && ch <= ´Z´);
return _isLetter;
}
}

正确示例:
public class CLOFixed {
public boolean isLetter (char ch) {
boolean _isLetter = Character.isLetter(ch);
return _isLetter;
}
}
参考: http://java.sun.com/docs/books/tutorial/i18n/intro/checklist.html
更多的字符比较方法请参考:http://java.sun.com/docs/books/tutorial/i18n/text/charintro.html


(6) 国际化开发建议:不要对日期对象使用´Date.toString ()´
不要使用´Date.toString ()´方法,日期格式对于地区和语言不同的国家来说是不一样的,务必不要使用。
错误示例:´DateFormat´类提供了一个预定义的格式类型来指定本地的格式。
public void printToday () {
Date today = new Date ();
String todayStr = today.toString ();
System.out.println (todayStr);
}
正确示例:
public void printToday () {
Locale currentLocale = Locale.getDefault ();
DateFormat dateFormatter = DateFormat.getDateInstance (
DateFormat.DEFAULT, currentLocale);
Date today = new Date ();
String todayStr = dateFormatter.format (today);
System.out.println (todayStr);
}
参考:http://java.sun.com/docs/books/tutorial/i18n/intro/checklist.html
http://java.sun.com/docs/books/tutorial/i18n/format/dateFormat.html

(7) 国际化开发建议:不要对数字变量使用´toString ()´方法
在全球化的开发中,不要对数字变量使用´toString ()´方法,对于java.lang.Number的任何子类都适用。包括:BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, and Short.对于这样的情况,java里也与定义了”NumberFormat”方法来格式化。
错误示例:
public class NTS {
public void method (Double amount) {
String amountStr = amount.toString ();
System.out.println (amountStr);
}
}
正确示例:
public class NTSFixed {
public void method (Double amount) {
Locale currentLocale = Locale.getDefault ();
NumberFormat numberFormatter =
NumberFormat.getNumberInstance (currentLocale);
String amountStr = numberFormatter.format (amount); //
System.out.println (amountStr + ´ ´ + currentLocale.toString ());
}
}
参考:http://java.sun.com/docs/books/tutorial/i18n/intro/checklist.html
http://java.sun.com/docs/books/tutorial/i18n/format/numberFormat.html


(8) 国际化开发建议:不要使用´String.equals ()´方法
建议不要使用´String.equals ()´方法,因为在统一字符比较标准中不一定按照相关的顺序来比较。´Collator´提供的预定义整理规则来排序string,Collator类调用´getInstance ()´方法,一般来说,可以为默认的本地创建一个Collator。例如:Collator myCollator = Collator.getInstance ();创建Collator的时候你也可以指定一个特殊的locale。例如:Collator myFrenchCollator = Collator.getInstance (Locale.FRENCH);然后就可以调用´Collator.compare ()´来执行一个本地的字符比较myCollator.compare (s1,s2);从这里可以了解更多的有关Collator类的信息:http://java.sun.com/docs/books/tutorial/i18n/text/collationintro.html



错误示例:
public class SE {
public boolean compstr (String s1, String s2) {
boolean b = (s1.equals (s2));
return b;
}
}
正确示例:
public class SEFixed {
public boolean compstr (String s1, String s2) {
Collator myCollator = Collator.getInstance ();
boolean b = (myCollator.compare(s1,s2) == 0);
return b;
}
}

参考:http://java.sun.com/docs/books/tutorial/i18n/intro/checklist.html
http://java.sun.com/docs/books/tutorial/i18n/text/locale.html

(9) 国际化开发建议:不要使用´StringTokenizer()´方法
错误示例:StringTokenizer st = new StringTokenizer(str);
可以从这里得到更多的信息:‘
参考:http://java.sun.com/docs/books/tutorial/i18n/intro/checklist.html


(10) 国际化开发建议:不要使用´Time.toString ()´方法
因为时间的格式各个国家也不一样。如果你使用日期格式类,你的应用就能够在世界上各个地方正确的显示时间和日期了。首先,用´getTimeInstance ()´方法创建一个formatter。然后,调用´format ()´方法。
错误示例:
public class TTS {
public void printTime (Time t1) {
String timeStr = t1.toString ();
System.out.println (timeStr);
}
}
正确示例:
import java.sql.Time;
import java.text.DateFormat;
import java.util.Locale;

public class TTSFixed {
public void printTime (Time t1) {
DateFormat timeFormatter = DateFormat.getTimeInstance(
DateFormat.DEFAULT, Locale.getDefault ());
String timeStr = timeFormatter.format(t1);
System.out.println (timeStr);
}
}

在我们学习Java的过程中,掌握其中的基本概念对我们的学习无论是J2SE,J2EE,J2ME都是很重要的,J2SE是Java的基础,所以有必要对其中的基本概念做以归纳,以便大家在以后的学习过程中更好的理解java的精髓,在此我总结了30条基本的概念.

  Java概述:

  目前Java主要应用于中间件的开发(middleware)—处理客户机于服务器之间的通信技术,早期的实践证明,Java不适合pc应用程序的开发,其发展逐渐变成在开发手持设备,互联网信息站,及车载计算机的开发.Java于其他语言所不同的是程序运行时提供了平台的独立性,称许可以在windows,solaris,linux其他操作系统上使用完全相同的代码.Java的语法与C++语法类似,C++/C程序员很容易掌握,而且Java是完全的彻底的面向对象的,其中提出了很好的GC(Garbage Collector)垃圾处理机制,防止内存溢出.

  Java的白皮书为我们提出了Java语言的11个关键特性.

  (1)Easy:Java的语法比C++的相对简单,另一个方面就是Java能使软件在很小的机器上运行,基础解释其和类库的支持的大小约为40kb,增加基本的标准库和线程支持的内存需要增加125kb.

  (2)分布式:Java带有很强大的TCP/IP协议族的例程库,Java应用程序能够通过URL来穿过网络来访问远程对象,由于servlet机制的出现,使Java编程非常的高效,现在许多的大的web server都支持servlet.

  (3)OO:面向对象设计是把重点放在对象及对象的接口上的一个编程技术.其面向对象和C++有很多不同,在与多重继承的处理及Java的原类模型.

  (4)健壮特性:Java采取了一个安全指针模型,能减小重写内存和数据崩溃的可能性。

  (5)安全:Java用来设计网路和分布系统,这带来了新的安全问题,Java可以用来构建防病毒和防攻击的System.事实证明Java在防毒这一方面做的比较好.
(6)中立体系结构:Java编译其生成体系结构中立的目标文件格式可以在很多处理器上执行,编译器产生的指令字节码(Javabytecode)实现此特性,此字节码可以在任何机器上解释执行.

  (7)可移植性:Java中对基本数据结构类型的大小和算法都有严格的规定所以可移植性很好.

  (8)多线程:Java处理多线程的过程很简单,Java把多线程实现交给底下操作系统或线程程序完成.所以多线程是Java作为服务器端开发语言的流行原因之一

  (9)Applet和servlet:能够在网页上执行的程序叫Applet,需要支持Java的浏览器很多,而applet支持动态的网页,这是很多其他语言所不能做到的.

  基本概念:

  1.OOP中唯一关系的是对象的接口是什么,就像计算机的销售商她不管电源内部结构是怎样的,他只关系能否给你提供电就行了,也就是只要知道can or not而不是how and why.所有的程序是由一定的属性和行为对象组成的,不同的对象的访问通过函数调用来完成,对象间所有的交流都是通过方法调用,通过对封装对象数据,很大限度上提高复用率.

  2.OOP中最重要的思想是类,类是模板是蓝图,从类中构造一个对象,即创建了这个类的一个实例(instance)

  3.封装:就是把数据和行为结合起在一个包中)并对对象使用者隐藏数据的实现过程,一个对象中的数据叫他的实例字段(instance field)

  4.通过扩展一个类来获得一个新类叫继承(inheritance),而所有的类都是由Object根超类扩展而得,根超类下文会做介绍.

  5.对象的3个主要特性
  behavior—说明这个对象能做什么.
  state—当对象施加方法时对象的反映.
  identity—与其他相似行为对象的区分标志.
  每个对象有唯一的indentity 而这3者之间相互影响.
6.类之间的关系:
  use-a :依赖关系
  has-a :聚合关系
  is-a :继承关系–例:A类继承了B类,此时A类不仅有了B类的方法,还有其自己的方法.(个性存在于共性中)

  7.构造对象使用构造器:构造器的提出,构造器是一种特殊的方法,构造对象并对其初始化.
  例:Data类的构造器叫Data
  new Data()—构造一个新对象,且初始化当前时间.
  Data happyday=new
  Data()—把一个对象赋值给一个变量happyday,从而使该对象能够多次使用,此处要声明的使变量与对象变量二者是不同的.new返回的值是一个引用.
  构造器特点:构造器可以有0个,一个或多个参数
  构造器和类有相同的名字
  一个类可以有多个构造器
  构造器没有返回值
  构造器总是和new运算符一起使用.

  8.重载:当多个方法具有相同的名字而含有不同的参数时,便发生重载.编译器必须挑选出调用哪个方法.

  9.包(package)Java允许把一个或多个类收集在一起成为一组,称作包,以便于组织任务,标准Java库分为许多包.java.lang java.util java,net等,包是分层次的所有的java包都在java和javax包层次内.

  10.继承思想:允许在已经存在的类的基础上构建新的类,当你继承一个已经存在的类时,那么你就复用了这个类的方法和字段,同时你可以在新类中添加新的方法和字段.

  11.扩展类:扩展类充分体现了is-a的继承关系. 形式为:class (子类) extends (基类).

  12.多态:在java中,对象变量是多态的.而java中不支持多重继承.
13.动态绑定:调用对象方法的机制.
  (1)编译器检查对象声明的类型和方法名.
  (2)编译器检查方法调用的参数类型.
  (3)静态绑定:若方法类型为priavte static final 编译器会准确知道该调用哪个方法.
  (4)当程序运行并且使用动态绑定来调用一个方法时,那么虚拟机必须调用x所指向的对象的实际类型相匹配的方法版本.
  (5)动态绑定:是很重要的特性,它能使程序变得可扩展而不需要重编译已存代码.

  14.final类:为防止他人从你的类上派生新类,此类是不可扩展的.

  15.动态调用比静态调用花费的时间要长,

  16.抽象类:规定一个或多个抽象方法的类本身必须定义为abstract例: public abstract string getDescripition

  17.Java中的每一个类都是从Object类扩展而来的.

  18.object类中的equal和toString方法.equal用于测试一个对象是否同另一个对象相等.toString返回一个代表该对象的字符串,几乎每一个类都会重载该方法,以便返回当前状态的正确表示.(toString 方法是一个很重要的方法)

  19.通用编程:任何类类型的所有值都可以同object类性的变量来代替.

  20.数组列表:ArrayList动态数组列表,是一个类库,定义在java.uitl包中,可自动调节数组的大小.

  21.class类 object类中的getclass方法返回ckass类型的一个实例,程序启动时包含在main方法的类会被加载,虚拟机要加载他需要的所有类,每一个加载的类都要加载它需要的类.

  22.class类为编写可动态操纵java代码的程序提供了强大的功能反射,这项功能为JavaBeans特别有用,使用反射Java能支持VB程序员习惯使用的工具.能够分析类能力的程序叫反射器,Java中提供此功能的包叫Java.lang.reflect反射机制十分强大.
  1.在运行时分析类的能力.
  2.在运行时探察类的对象.
  3.实现通用数组操纵代码.
  4.提供方法对象.
  而此机制主要针对是工具者而不是应用及程序.
  反射机制中的最重要的部分是允许你检查类的结构.用到的API有:
  java.lang.reflect.Field 返回字段.
  java.reflect.Method 返回方法.
  java.lang.reflect.Constructor 返回参数.
  方法指针:java没有方法指针,把一个方法的地址传给另一个方法,可以在后面调用它,而接口是更好的解决方案.
23.接口(Interface)说明类该做什么而不指定如何去做,一个类可以实现一个或多个interface.

  24.接口不是一个类,而是对符合接口要求的类的一套规范.若实现一个接口需要2个步骤:
  1.声明类需要实现的指定接口.
  2.提供接口中的所有方法的定义.
  声明一个类实现一个接口需要使用implements 关键字class actionB implements Comparable 其actionb需要提供CompareTo方法,接口不是类,不能用new实例化一个接口.

  25.一个类只有一个超类,但一个类能实现多个接口.Java中的一个重要接口Cloneable

  26.接口和回调.编程一个常用的模式是回调模式,在这种模式中你可以指定当一个特定时间发生时回调对象上的方法.例:ActionListener 接口监听.
  类似的API有:java.swing.JOptionPane
        java.swing.Timer
        java.awt.Tookit

  27.对象clone:clone方法是object一个保护方法,这意味着你的代码不能简单的调用它.

  28.内部类:一个内部类的定义是定义在另一个内部的类
  原因是:1.一个内部类的对象能够访问创建它的对象的实现,包括私有数据
  2.对于同一个包中的其他类来说,内部类能够隐藏起来.
  3.匿名内部类可以很方便的定义回调.
  4.使用内部类可以非常方便的编写事件驱动程序.

  29.代理类(proxy):1.指定接口要求所有代码 2.object类定义的所有的方法(toString equals)

  30.数据类型:Java是强调类型的语言,每个变量都必须先申明它都类型,java中总共有8个基本类型.4种是整型,2种是浮点型,一种是字符型,被用于Unicode编码中的字符,布尔型.

2004年05月01日

NULL….

在语言级支持锁定对象和线程间发信使编写线程安全类变得简单。本文使用简单的编程示例来说明开发高效的线程安全类是多么有效而直观。

Java 编程语言为编写多线程应用程序提供强大的语言支持。但是,编写有用的、没有错误的多线程程序仍然比较困难。本文试图概述几种方法,程序员可用这几种方法来创建高效的线程安全类。

并发性
只有当要解决的问题需要一定程度的并发性时,程序员才会从多线程应用程序中受益。例如,如果打印队列应用程序仅支持一台打印机和一台客户机,则不应该将它编写为多线程的。一般说来,包含并发性的编码问题通常都包含一些可以并发执行的操作,同时也包含一些不可并发执行的操作。例如,为多个客户机和一个打印机提供服务的打印队列可以支持对打印的并发请求,但向打印机的输出必须是串行形式的。多线程实现还可以改善交互式应用程序的响应时间。

Synchronized 关键字
虽然多线程应用程序中的大多数操作都可以并行进行,但也有某些操作(如更新全局标志或处理共享文件)不能并行进行。在这些情况下,必须获得一个锁来防止其他线程在执行此操作的线程完成之前访问同一个方法。在 Java 程序中,这个锁是通过 synchronized 关键字提供的。清单 1 说明了它的用法。

清单 1. 使用 synchronized 关键字来获取锁 public class MaxScore {
int max;
public MaxScore() {
max = 0;
}

public synchronized void currentScore(int s) {
if(s> max) {
max = s;
}
}

public int max() {
return max;
}
}


这里,两个线程不能同时调用 currentScore() 方法;当一个线程工作时,另一个线程必须阻塞。但是,可以有任意数量的线程同时通过 max() 方法访问最大值,因为 max() 不是同步方法,因此它与锁定无关。

试考虑在 MaxScore 类中添加另一个方法的影响,该方法的实现如清单 2 所示。


清单 2. 添加另一个方法  public synchronized void reset() {
max = 0;
}


这个方法(当被访问时)不仅将阻塞 reset() 方法的其他调用,而且也将阻塞 MaxScore 类的同一个实例中的 currentScore() 方法,因为这两个方法都访问同一个锁。如果两个方法必须不彼此阻塞,则程序员必须在更低的级别使用同步。清单 3 是另一种情况,其中两个同步的方法可能需要彼此独立。

清单 3. 两个独立的同步方法 import java.util.*;

public class Jury {
Vector members;
Vector alternates;

public Jury() {
members = new Vector(12, 1);
alternates = new Vector(12, 1);
}

public synchronized void addMember(String name) {
members.add(name);
}

public synchronized void addAlt(String name) {
alternates.add(name);
}

public synchronized Vector all() {
Vector retval = new Vector(members);
retval.addAll(alternates);
return retval;
}
}


此处,两个不同的线程可以将 members 和 alternates 添加到 Jury 对象中。请记住,synchronized 关键字既可用于方法,更一般地,也可用于任何代码块。清单 4 中的两段代码是等效的。

清单 4. 等效的代码
synchronized void f() {              
void f() {      
// 执行某些操作                                              
synchronized(this) {
}                                                    // 执行某些操作

      }
}  



所以,为了确保 addMember() 和 addAlt() 方法不彼此阻塞,可按清单 5 所示重写 Jury 类。

清单 5. 重写后的 Jury 类 import java.util.*;

public class Jury {
Vector members;
Vector alternates;

public Jury() {
members = new Vector(12, 1);
alternates = new Vector(12, 1);
}

public void addMember(String name) {
synchronized(members) {
members.add(name);
}
}

public void addAlt(String name) {
synchronized(alternates) {
alternates.add(name);
}
}

public Vector all() {
Vector retval;
synchronized(members) {
retval = new Vector(members);
}

synchronized(alternates) {
retval.addAll(alternates);
}

return retval;
}
}


请注意,我们还必须修改 all() 方法,因为对 Jury 对象同步已没有意义。在改写后的版本中,addMember()、addAlt() 和 all() 方法只访问与 members 和 alternates 对象相关的锁,因此锁定 Jury 对象毫无用处。另请注意,all() 方法本来可以写为清单 6 所示的形式。

清单 6. 将 members 和 alternates 用作同步的对象  public Vector all() {
synchronized(members) {
synchronized(alternates) {
Vector retval;
retval = new Vector(members);
retval.addAll(alternates);
}
}
return retval;
}


但是,因为我们早在需要之前就获得 members 和 alternates 的锁,所以这效率不高。清单 5 中的改写形式是一个较好的示例,因为它只在最短的时间内持有锁,并且每次只获得一个锁。这样就完全避免了当以后增加代码时可能产生的潜在死锁问题。

同步方法的分解
正如在前面看到的那样,同步方法获取对象的一个锁。如果该方法由不同的线程频繁调用,则此方法将成为瓶颈,因为它会对并行性造成限制,从而会对效率造成限制。这样,作为一个一般的原则,应该尽可能地少用同步方法。尽管有这个原则,但有时一个方法可能需要完成需要锁定一个对象几项任务,同时还要完成相当耗时的其他任务。在这些情况下,可使用一个动态的“锁定-释放-锁定-释放”方法。例如,清单 7 和清单 8 显示了可按这种方式变换的代码。

清单 7. 最初的低效率代码 public synchonized void doWork() {
         unsafe1();
write_file();
unsafe2();
}


清单 8. 重写后效率较高的代码 public void doWork() {
synchonized(this) {
                 unsafe1();
}
write_file();
synchonized(this) {
unsafe2();
}
}


清单 7 和清单 8 假定第一个和第三个方法需要对象被锁定,而更耗时的 write_file() 方法不需要对象被锁定。如您所见,重写此方法以后,对此对象的锁在第一个方法完成以后被释放,然后在第三个方法需要时重新获得。这样,当 write_file() 方法执行时,等待此对象的锁的任何其他方法仍然可以运行。将同步方法分解为这种混合代码可以明显改善性能。但是,您需要注意不要在这种代码中引入逻辑错误。

嵌套类
内部类在 Java 程序中实现了一个令人关注的概念,它允许将整个类嵌套在另一个类中。嵌套类作为包含它的类的一个成员变量。如果定期被调用的的一个特定方法需要一个类,就可以构造一个嵌套类,此嵌套类的唯一任务就是定期调用所需的方法。这消除了对程序的其他部分的相依性,并使代码进一步模块化。清单 9,一个图形时钟的基础,使用了内部类。

清单 9. 图形时钟示例 public class Clock {
protected class Refresher extends Thread {
int refreshTime;
public Refresher(int x) {
super(“Refresher”);
refreshTime = x;
}

public void run() {
while(true) {
try {
sleep(refreshTime);
}
catch(Exception e) {}
repaint();
}
}
}

public Clock() {
Refresher r = new Refresher(1000);
r.start();
}

private void repaint() {
// 获取时间的系统调用
// 重绘时钟指针
}
}

清单 9 中的代码示例不靠任何其他代码来调用 repaint() 方法。这样,将一个时钟并入一个较大的用户界面就相当简单。

事件驱动处理
当应用程序需要对事件或条件(内部的和外部的)作出反映时,有两种方法或用来设计系统。在第一种方法(称为轮询)中,系统定期确定这一状态并据此作出反映。这种方法(虽然简单)也效率不高,因为您始终无法预知何时需要调用它。

第二种方法(称为事件驱动处理)效率较高,但实现起来也较为复杂。在事件驱动处理的情况下,需要一种发信机制来控制某一特定线程何时应该运行。在 Java 程序中,您可以使用 wait()、notify() 和 notifyAll() 方法向线程发送信号。这些方法允许线程在一个对象上阻塞,直到所需的条件得到满足为止,然后再次开始运行。这种设计减少了 CPU 占用,因为线程在阻塞时不消耗执行时间,并且可在 notify() 方法被调用时立即唤醒。与轮询相比,事件驱动方法可以提供更短的响应时间。

创建高效的线程安全类的步骤
编写线程安全类的最简单的方法是用 synchronized 声明每个方法。虽然这种方案可以消除数据损坏,但它同时也会消除您预期从多线程获得的任何收益。这样,您就需要分析并确保在 synchronized 块内部仅占用最少的执行时间。您必须格外关注访问缓慢资源 — 文件、目录、网络套接字和数据库 — 的方法,这些方法可能降低您的程序的效率。尽量将对这类资源的访问放在一个单独的线程中,最好在任何 synchronized 代码之外。

一个线程安全类的示例被设计为要处理的文件的中心储存库。它与使用 getWork() 和 finishWork() 与 WorkTable 类对接的一组线程一起工作。本例旨在让您体验一下全功能的线程安全类,该类使用了 helper 线程和混合同步。请注意继续添加要处理的新文件的Refresher helper 线程的用法。本例没有调整到最佳性能,很明显有许多地方可以改写以改善性能,比如将 Refresher 线程改为使用 wait()/notify() 方法事件驱动的,改写 populateTable() 方法以减少列出磁盘上的文件(这是高成本的操作)所产生的影响。

小结
通过使用可用的全部语言支持,Java 程序中的多线程编程相当简单。但是,使线程安全类具有较高的效率仍然比较困难。为了改善性能,您必须事先考虑并谨慎使用锁定功能。

Vector 类提供了实现可增长数组的功能,随着更多元素加入其中,数组变的更大。在删除一些元素之后,数组变小。
Vector 有三个构造函数,
public Vector(int initialCapacity,int capacityIncrement)
         public Vector(int initialCapacity)
         public Vector()
  Vector 运行时创建一个初始的存储容量initialCapacity,存储容量是以capacityIncrement 变量定义的增量增长。初始的存储容量和capacityIncrement 可以在Vector 的构造函数中定义。第二个构造函数只创建初始存储容量。第三个构造函数既不指定初始的存储容量也不指定capacityIncrement。
  Vector 类提供的访问方法支持类似数组运算和与Vector 大小相关的运算。类似数组的运算允许向量中增加,删除和插入元素。它们也允许测试矢量的内容和检索指定的元素,与大小相关的运算允许判定字节大小和矢量中元素不数目。
  现针对经常用到的对向量增,删,插功能举例描述:
addElement(Object obj)  
  把组件加到向量尾部,同时大小加1,向量容量比以前大1
 
insertElementAt(Object obj, int index)  
  把组件加到所定索引处,此后的内容向后移动1 个单位
 
setElementAt(Object obj, int index)
  把组件加到所定索引处,此处的内容被代替。
  removeElement(Object obj) 把向量中含有本组件内容移走。
  removeAllElements() 把向量中所有组件移走,向量大小为0。
  例如:
 
     import java.lang.System;
     import java.util.Vector;
     import java.util.Emumeration;
     public class Avector{
                 public static void main(String args[])
                    {
0.   Vector v=new Vector();
1. v.addElement(“one”);
2. addElement(“two”);
3. v.addElement(“three”);
4. v.insertElementAt(“zero”,0);
5. v.insertElementAt(“oop”,3);
6. v.setElementAt(“three”,3);
7. v.setElementAt(“four”,4);
8. v.removeAllElements();
}
}
Vector中的变化情况:
1. one   2. one   3. one   4. zero   5.zero   6. zero  7. zero
8.
          two   two  one   one  one   one
             three  two   two  two   two
                 three  oop  three  three
                     three   three  four
 
  另外,Vector 在参数传递中发挥着举足轻重的作用。
  在Applet 中有一块画布(Canvas) 和一个(Panel), 而Panel 中放着用户要输入的信息,根据这些信息把参数传递到canvas 中,这时在Java 中用一个接口(Interface), 而在接口中需用一个Vector 去传递这些参数。另外,在一个类向另一个类参数传递就可以用这种方法。
  例如:
 
import java.util.Vector
interface codeselect{
           Vector codeselect=new Vector();
             }
显示数学信息
Vector(0)存入学生编号
Vector(1)存入学科
 
  在Panel 中当用户在TextField 和Choice 中选择自己所要求的内容,程序中
  通过事件响应把值传到向量Vector 中。
  假若在Panel 类中:
 
public void  codepanel extends Panel{
  public void init()
  {
    **.
TextField  s=new TextField();
Choice c=new Choice();
c. addItem(“语文”);
c.addItem(“数学”);
c.addItem(“政治”);
add(s);
add (c);
**
}
 
public boolean handleEvent(Event event){
if(event.id==Event.ACTION_EVENT){
if(event.target.instanceof Textfield)
{
coderesult.setElementAt(s.getText(),0);
}
else if(event.target intanceof Choice)
{
coderesult.setElementAt(new Integer(c.getSelectedIndex()),1);
}
}
}
}
 
 
 
  这时,向量中已经存入学生编号和学科索引号(0 为语文,1 为数学,2 为政治)。
  而在Canvas 中得到此值,
 
public class codecanvas extends Canvas{
 
public void code{
            }
public void paint{
 
String str;
int t;
str=(String)coderesult.elementAt(0);
t=(new Integer(codeselect.elementAt(1).toString())).intValue();
if(t==0)
{
显示语文信息
}
else if(t==1)
{
显示数学信息
 
}
else if(t==2)
{
显示政治信息
}
}
}

北京大学信息管理系 黄剑

使用Java语言编写应用程序最大的优点在于“一次编译,处处运行”,然而这并不是说所有的Java程序都具有跨平台的特性,事实上,相当一部分的Java程序是不能在别的操作系统上正确运行的,那么如何才能编写一个真正的跨平台的Java程序呢?下面是在编写跨平台的Java程序是需要注意的一些事情:

1. 编写Java跨平台应用程序时,你可以选择JDK1.0,1.1,1.2或支持它们的GUI开发工具如:Jbuilder,Visual Age for Java 等等,但是必须注意你的Java程序只能使用Java核心API包,如果要使用第三方的类库包,则该类库包也要由Java核心包开发完成,否则在发布你的程序的时候还得将支持该Java类库包的JVM发布出去。也就是说,你的程序需要是100%纯Java的。举一个例子,Visual J++ 就不是纯Java的,由Visual J++编写的程序也就不具有平台无关性。

2. 无论你使用的是JDK或其他开发工具,在编译时都要打开所有的警告选项,这样编译器可以尽可能多的发现平台相关的语句,并给出警告。虽然不能保证没有编译时警告错误的程序一定是跨平台的,但含有警告错误的程序却很有可能是非平台无关的。

3. 在程序中使用任何一个方法的时候,要详细察看文档,确保你使用的方法不是在文档中已经申明为过时的方法(Deprecated method),也不是文档中未标明的隐含方法(Undocumented method)。

4. 退出Java程序时尽量不要使用java.lang.System的exit方法。Exit 方法可以终止JVM,从而终止程序,但如果同时运行了另一个Java程序,使用exit方法就会让该程序也关闭,这显然不是我们希望看到的情况。事实上要退出Java程序,可以使用destory()退出一个独立运行的过程。对于多线程程序,必须要关闭各个非守护线程。只有在程序非正常退出时,才使用exit方法退出程序。

5. 避免使用本地方法和本地代码,尽可能自己编写具有相应功能的Java类,改写该方法。如果一定要使用该本地方法,可以编写一个服务器程序调用该方法,然后将现在要编写的程序作为该服务器程序的客户程序,或者考虑CORBA(公共对象请求代理)程序结构。

6. Java中有一个类似于Delphi中的winexec的方法,java.lang.runtime类的exec方法,作为该方法本身是具有平台无关性的,但是给方法所调用的命令及命令参数却是与平台相关的,因此,在编写程序时要避免使用,如果一定要调用其他的程序的话,必须要让用户自己来设置该命令及其参数。比如说,在windows中可以调用notepad.exe程序,在linux 中就要调用vi程序了。

7. 程序设计中的所有的信息都要使用ASCII码字符集,因为并不是所有的操作系统都支持Unicode字符集,这对于跨平台的Java中文软件程序不能不说是一大噩耗。

8. 在程序中不要硬性编码与平台相关的任何常量,比如行分隔符,文件分隔符,路径分隔符等等,这些常量在不同的平台上是不同的,比如文件分隔符,在UNIX和MAC中是“/”,在windows中是“\”,如果要使用这些常量,需要使用jdava.util.Properties类的getProperty方法,如java.util.Properties.getProperty(“file.separator”)可以获得文件分隔符,getProperty (“line.separator”)返回行分隔符,getProperty(“path.separator”)返回路径分隔符。

9. 在编写跨平台的网络程序时,不要使用java.net.InetAddress类的getHostName方法得到主机名,因为不同的平台的主机名格式是不同的,最好使用getAddress得到格式相同的IP地址,另外,程序中所有的主机名都要换成IP地址,比如www.263.net就要换成相应的IP地址。

10. 涉及文件操作的程序需要注意:不要在程序中硬性编码文件路径,理由和8中一样,只是这一点特别重要,因此单独提出。而且,不同平台对于文件名使用的字符及最大文件名长度的要求不同,编写你的程序的时候要使用一般的ASCII码字符作为文件的名字,而且不能与平台中已存在的程序同名,否则会造成冲突。

11. 如果您写的程序是GUI程序,在使用AWT组件时不能硬性设置组件的大小和位置而应该使用Java的布局管理器(layout manager)来设置和管理可视组件的大小和位置,否则有可能造成布局混乱。

12. 由于不同的操作系统,不同的机器,系统支持的颜色和屏幕的大小和分辨率都不同,如何获得这些属性呢?使用java.awt.Systemcolor类可以获得需要的颜色,如该类的inactiveCaption 就是窗口边框中活动标题的背景颜色,menu则是菜单的背景颜色。使用java.awt.Toolkit的getScreenResolution可以以“象素每英寸”为单位显示屏幕的分辨率。该类的getScreenSize可以得到屏幕大小(英寸),loadSystemColors可以列出所有的系统颜色。

- 这些注意事项有些是来自参考文献,有些是自己长期编写Java程序的经验所得,相信对你的程序设计会有所帮助。

山东科技大学计算中心 李鲁群

在TCP/IP 互联网时,经常会需要查询自己主机的IP地址和www服务器的IP地址。虽然,我们可以使用IPCONFIG 和PING 进行IP地址查询,但是如果在应用程序或APPLET中使用此命令回破坏我们应用程序界面。

为此本人使用JAVA 做了一个简单的程序可以直接查询自己主机的IP地址和www服务器的IP地址。

// 文件名为 NetTool.java

(注意:在JAVA 语言中大小写敏感)

import java.net.*;

public class NetTool{ InetAddress myIPaddress=null;

InetAddress myServer=null;

public static void main( String args[]){

NetTool mytool;

mytool=new NetTool();

System.out.println(“Your host IP is: ” + mytool.getMyIP());

System.out.println(“The Server IP is :”

+mytool.getServerIP());

}

//取得LOCALHOST的IP地址

public InetAddress getMyIP() {

try { myIPaddress=InetAddress.getLocalHost();}

catch (UnknownHostException e) {}

return (myIPaddress); }

//取得 www.abc.com 的IP地址

public InetAddress getServerIP(){

try {myServer=InetAddress.getByName(

“www.abc.com”);}

catch (UnknownHostException e) {}

return (myServer); } }

由于JAVA语言的跨平台特性,以上程序编译后可直接在任何装有JVM系统的机器上运行。以上程序旨在抛砖引玉,读者可将上述代码稍加变换转化成APPLET加到你的homepage中,或将地址查询结果写到一个文件中去,建立自己本地的hosts文件。