Java编码规范

本章节目录

简介:

以标准的、规范的方式来约定Java代码,通过执行统一的Java编码规范,使开发人员养成良好的编码习惯,便于提高代码的可靠性、可读性、可维护性和一致性,以保证公司软件产品的质量。

一、目录列表

1.1 命名

2.1	基本原则
2.2	文件、包
2.3	类、接口
2.4	字段
2.4.1	    常量
2.4.2	    变量和参数
2.4.3	    集合
2.4.4	    方法
2.4.5	    异常
2.4.6	    命名约定示例

1.2 代码格式

3.1	基本原则
3.2	缩进
3.3	长度
3.4	行宽
3.5	间隔
3.6	对齐
3.7	括号

1.3 注释

4.1	基本原则
4.2	JavaDoc
4.3	类、接口
4.4	方法
4.5	其他
4.6	注释参考

1.4 声明

5.1	基本原则
5.2	包
5.3	类、接口
5.4	方法
5.5	字段
5.6	声明示例

1.5 类与接口

6.1	基本原则
6.2	抽象类与接口
6.3	继承与组合
6.4	构造方法和静态工厂方法
6.5	toString()
6.6	Singleton Class

1.6 方法

7.1	基本原则
7.2	参数和返回值
7.3	输入参数合法性判断原则

1.7 表达式和语句

8.1	基本原则
8.2	控制语句
8.3	循环语句

1.8 错误与异常

9.1	基本原则
9.2	受控异常与运行时异常
9.3	异常的传递
9.4	异常处理规范
9.5	异常处理范例

1.9 性能

10.1	基本原则
10.2	String
10.3	StringBuilder
10.4	集合
10.5	对象
10.6	同步
10.7	final
10.8	垃圾收集和资源释放
10.9	System.arraycopy()
10.1	单例
10.11	静态变量
10.12	创建Java对象
10.13	局部变量
10.14	包装类型和基本类型
10.15	慎用synchronized
10.16	finalize方法
10.17	慎用异常

1.10 安全

11.1	跨站脚本XSS
11.2	违反信任边界规则(Trust Boundary Violation)
11.3	不安全的反射(Unsafe Reflection)
11.4	SQL 注入(SQL Injection)
11.5	系统信息泄露(System Information Leakage)
11.6	资源注入(resource injection)

二、命名

2.1 基本原则

  • 使用可以准确说明变量、字段、方法、类、接口和包的完整英文描述符。例如,采用类似firstName,listAllUsers或CorporateCustomer这样的名字,严禁使用汉语拼音及不相关的单词进行命名。虽然Java支持Unicode命名,但本规范规定对变量、字段、方法、类、接口和包不得使用汉字进行命名;
  • 采用领域术语。如果用户称他们的“客户”(clients)为“顾客”(customers),那么就采用术语Customer来命名这个类,而不用Client;
  • 采用驼峰命名法(Camel-Case),名字中各单词首字母大写,其余字母小写,但是类和接口的名字首字母大写,包名全部小写;
  • 尽量少用缩写,如果一定要使用,需使用公共缩写和习惯缩写,如实现(implement)可缩写成impl,经理(manager)可缩写成mgr等,具体见附录之《常用缩写表》,严禁滥用缩写;
  • 避免使用长名字(最好不超过 25个字符);
  • 避免使用相似或者仅在大小写上有区别的名字;
  • 避免使用Java中具有特殊意义的关键字,例如equals、hashCode、clone、finalizer等。
  • 避免使用下划线作为名字的首字母;
  • 命名时应使用复数来表示它们代表多值。例如:orderItems。

2.2 文件、包

  • 文件名应与文件中对应的类名严格相同,所有单词首字母大写;
  • 包名一般以项目或模块名命名,少用缩写和长名,一律小写;
  • 严禁在基本包下直接定义类和接口,所有项目中的类和接口都应定义在各自的项目和模块包中。类和接口必须包含在包里,禁止出现无包的类和接口;

2.3 类、接口

类和接口的名字所有单词首字母大写。

使用能够准确反映该类和接口的含义、功能等的词,一般采用名词。接口名使用I为前缀。

2.4 字段

2.4.1 常量

采用完整的大写英文单词,在单词之间用下划线连接,如:DEFAULT_VALUE(严禁对变量使用这种命名方法)。

应用级(Application)的全局常量,统一定义在一个常量类中,集中管理,方便维护。

2.4.2 变量和参数

不能清楚识别类型的变量,可以使用类型缩写作其前缀,如字符串使用strXXX,boolean使用isXXX,hasXXX等等。除第一个单词外其余单词首字母大写。

避免与类名一致,避免与方法名一致。在方法中命名局部变量时,应避免与类的字段一致。

2.4.3 集合

集合应采用复数命名来表示队列中存放的对象类型。命名应采用完整的英文描述符;名字中所有非开头的单词的第一个字母应大写;适当使用集合后缀,如下:

Map servicesMap = new HashMap();   // 服务Map(集合后缀命名)
List usersList = new ArrayList();   // 用户列表(集合后缀命名)

2.4.4 方法

方法的命名方式:应采用完整的英文描述符;大小写混合使用;第一个单词首字母小写;所有中间单词的第一个字母大写。方法名的第一个单词常常采用一个有强烈动作色彩的动词。 方法名不能与类名一致。

取值方法使用get前缀,设值方法使用set前缀,判断方法使用is(has)前缀,例如:

getName()
setName()
isLogin()

2.4.5 异常

异常类名由表示该异常类型的单词和Exception组成,如ActionException。

异常实例一般使用e、ex等,在多个异常时使用该异常名或简写加E,Ex等组成,如: sqlEx、actionEx。

2.4.6 命名约定示例

三、代码格式

3.1 基本原则

  • 代码格式需可读性高,易于维护;
  • 代码格式需前后一致,并符合本规范的基本要求和原则。

3.2 缩进

子功能块应在其父功能块后缩进。当功能块过多而导致缩进过深时,应将子功能块提取出来做为子方法。代码中以4个英文空格字符缩进,在编辑器中需以4个英文空格字符替代TAB,否则在不同编辑器中TAB长度不等时会影响整个程序代码的格式。

示例:

public void methodName() {
	if (some condition) {
		for (int i = 0; i < 100; i++) {
			…
		} // end for 
	} // end if 
}

3.3 长度

为便于阅读和理解,单个方法的有效代码长度应尽量控制在100行以内(不包括注释行),当一个功能模块过大时,往往会造成阅读困难,因此需使用子方法将相应功能抽取出来,这有利于提高代码的复用度。

单个类也不宜过大,若出现此种情况时,应将相应功能的代码重构到其他类中,通过组合的方式来调用,建议单个类的长度不超过1500行(不包括注释行)。

3.4 行宽

行宽应设置为120字符。在任何情况下, 超长的语句应在一个逗号后或一个操作符前折行。长表达式要在低优先级操作符处划分新行,操作符放在新行之首,划分出的新行要进行适当的缩进,使排版整齐。

3.5 间隔

类、方法及功能块间等应以空行相隔,以增加可读性,但不能有无规则的大片空行。操作符两端应当各留一个空格以增加可读性。相互独立的功能模块之间可使用注释行间隔,并标明相应内容。

3.6 对齐

  • 关系密切的行应对齐,对齐包括类型、修饰、参数等各部分;
  • 当方法参数过多时,应在每个参数后(逗号后)换行并对齐;
  • 当判断或循环的条件比较长时,应在操作符前换行并对齐,同时注释各条件。

示例 :

// 变量对齐 
int count = 100;
int length = 0;
String strUserName = null;
Integer[] porductCode = new Integer[2];  // 产品编码数组 
// 参数对齐 
public Connection getConnection(String url,
                                String userName,
                                String password)
    throws SQLException, IOException {
}

// 换行对齐 
public static final String SQL_SELECT_PRODUCT = "SELECT * "
    + " FROM TProduct WHERE Prod_ID = "
    + prodID;
// 条件对齐 
if (Condition1 // 当条件一 
    && Condition2 // 并且条件二 
    && Condition3) {
    …
}
for (int i = 0; i < productCount.length; i++) {
    …
}

3.7 括号

{} 中的语句应单独另起一行,左括号“{”当紧跟其语句后,右括号“}”单独作为一行且与其匹配行对齐,并尽量在其后说明其匹配的功能模块。

示例:   

public void methodName() {
    if (some condition) {
    // some condition条件下逻辑注释描述 
        for (int i = 0; i < 100; i++) {
             // 循环体逻辑注释描述 
            …
        } // end for 
    } // end if 
} // end methodName() 

较长的代码块、方法、类、接口等的右括号后应使用//end …等标识其结束。

示例:

类的结束符:} // end ClassName
方法结束符:} // end methodName ()
功能块结束:} // end if…userName is null?
循环块结束:} // end for…every user in userList

不要在程序中出现多余的括号,但有时为了增加可读性和便于理解,可以用括号限定相应项。

四、注释

4.1 基本原则

  • 注释应增加代码的清晰度。代码注释的目的是使开发人员和维护人员易于理解和修改;
  • 避免使用装饰性描述;
  • 保持注释的简洁。冗余无用的注释,容易造成维护人员的误解;
  • 注释不仅要包括代码的功能,还要解释为什么如此实现;
  • 不能为了注释而注释;
  • 统一使用//形式注释;
  • 对那些临时的,短期的解决方案,或已经够好但仍不完美的代码使用TODO注释;
  • 注释代码量占总体代码25%以上。

4.2 JavaDoc

对类、接口、方法、变量等的注释需要符合JavaDoc规范。对类、接口和方法都应详细说明其功能、条件、参数等信息,并使用良好的HTML标记格式化注释,使生成的JavaDoc易阅读和理解。

4.3 类、接口

在类和接口定义之前应对其进行注释,包括类和接口的目的、作用、功能、继承于何种父类、实现的接口、实现的算法、使用方法、示例程序等。

示例:

/**
 * <p>字符串工具类。</p> 
 * 定义字符串操作时所需的方法,如转换中文、HTML标记处理、
 * 特殊字符处理等。<br>
 * 创  建  人:张三 <br>
 * 创建时间:2013-6-19 <br>
 * 修  改  人:赵五 <br>
 * 修改时间:2013-6-20 <br>
 * 修改备注:补充注释说明 <br>
 * @version 1.1
 */
public class StringUtil {
    …
}

4.4 方法

  • 依据标准JavaDoc规范对方法进行注释,以明确该方法功能、作用、各参数含义以及返回值等;
  • 复杂的业务逻辑或算法需要在方法内用//注释说明;
  • 参数的注释应注明其取值范围等;
  • 返回值的注释应注明失败、错误、异常时的返回情况;
  • 异常的注释应注明什么时候、什么条件下会引发什么样的异常。

示例:

/**
* 执行查询。
* 该方法调用Statement的executeQuery(sql)方法并返回ResultSet
* 结果集。
* 
* @param sql 标准的SQL语句
* @return ResultSet结果集,若查询失败则返回null
* @throws SQLException 当SQL执行失败时可能引发此异常
*/
public ResultSet executeQuery(String sql) throws SQLException {
    // Statement和SQL语句都不能为空
    if (null != stmt && !StringUtil.isEmpty(sql)) {
        //返回查询执行结果
        return stmt.executeQuery(sql);
    }
    …
    return resultSet;
} // end executeQuery() 

4.5 其他

  • 重要的变量应加以注释,以说明其含义、用途;
  • 不易理解的分支条件表达式应加注释;
  • 不易理解的循环,应说明退出循环的条件;
  • 过长的方法实现,应将其语句按实现的功能分段加以概括性说明;
  • 异常处理,应注明正常及异常情况的触发条件,并说明当异常发生时程序应如何处理。

4.6 注释参考

五、声明

5.1 基本原则

声明的基本原则是遵守Java语言规范,并遵从习惯用法。

5.2 包

  • 在引入包时,应完全限制到代码所使用的类,禁止使用通配符方式;
  • 引入同一包中的类时,声明需在一起,可由编辑器自动完成此功能;
  • 重要的包当添加注释;
  • 避免引入当类、接口没有用到的包。

5.3 类、接口

类、接口定义语法规范:

[可见性][(‘abstract’|’final’)] [Class|Interface] class_name [(‘extends’|’implements’)][父类或接口名] {

}

例如:


public class LoginAction extends BaseAction implemnets ActionListener {
            …
}

5.4 方法

良好的程序设计应尽可能减小类与类之间耦合。所遵循的经验法则是:尽量限制方法的可见性。如果方法没必要公有(public),就定义为保护(protected);没必要保护(protected),就定义为私有(private)。

方法定义语法规范:

[可见性][(‘abstract’|’final’)] [‘synchronized’][返回值类型] method_name(参数列表)[(‘throws’)][异常列表] {
            …
}

示例:

public List listAllUsers(String deptId) throws DAOException {
            …
}

若有toString()、equals()、hashCode()、colone()等重载自Object的方法,应将其放在类的最后。

类中各种方法的声明顺序:

  • 构造方法;
  • 静态公共方法;
  • 静态私有方法;
  • 受保护方法;
  • 私有方法;
  • 继承自Object的方法。

5.5 字段

字段定义语法规范:

[(‘public’|’private’|’protected’)][‘static’][(‘final’|’volatile’)][‘transient’]
data_type field_name [ ‘=’ expression] ‘;’

若没有足够理由,不要把类变量或实例变量声明为公有。公有和保护的可见性应当尽量避免,所有的字段都建议置为私有,由获取和设置方法(Getter、Setter)访问。

不允许“隐藏”字段,即给局部变量所取的名字,不可与另一个更大范围内定义的字段的名字相同(或相似)。例如,如果一个字段(实例或类变量)叫做 firstName,就不要再生成一个局部变量叫做 firstName,或者任何易混肴的名字,如 fistName。

数组声明时当将”[]”跟在类型后,而不是字段名后,例如:
Integer[] ai = new Integer [2];  // 规范的数组声明方式
Integer aj[] = new Integer [3];  // 不规范的数组声明方式

一行代码只声明一个变量,仅将一个变量用于一件事。

变量声明顺序:

  • 常量;
  • 类变量;
  • 实例变量。

其中,实例变量中依次为公有字段、受保护字段、私有字段。也可以将私有字段声明在类的最后。

5.6 声明示例

示例:

/* 常量 */
public static final double PI = 3.141592653589793;
/* 类变量 */
protected static String key = “Love”;
/* 实例变量 */
/* 公有字段 */
public String userName = “Tom”;
/* 受保护字段 */
protected float price = 0.0f;
/* 友元字段 */
Vector vPorducts = null;
/* 私有字段 */
private int count;

/* 构造方法 */
public Constructor() {
}
/* 公共方法 */
public String getUserName() {
}
/* 友元方法 */
void createProduct() {
}
/* 受保护方法 */
protected void convert() {
}
/* 私有方法 */
private void init() {
}
/* 重载Object方法 */
public String toString() {
}
/* main方法 */
public static void main(String[] args) {
}

六、类与接口

6.1 基本原则

  • 类的划分粒度,不可太大,太大容易造成过于庞大的单个类,也不可太细,太细容易使类的继承太深;
  • 一个类只做一件事,也可以根据每个类的职责进行划分,如用User来存放用户信息,而用UserDAO来对用户信息进行数据访问操作(比如存取数据库),用UserService来封装用户信息的业务操作等等;
  • 多使用Java设计模式,随时重构;
  • 多个类使用相同方法时,将方法提到一个接口中,尽量提高复用度;
  • 对于不希望被继承的类,将其声明为final,例如某些实用类,但不要滥用final,否则会对系统的可扩展性造成影响;
  • 对于不希望被实例化的类,将其缺省构造方法声明为private。

6.2 抽象类与接口

一般而言:接口定义行为,而抽象类定义属性和公有行为。注意两者间的取舍,在设计中,可由接口定义公用的行为,由一个抽象类来实现其部分或全部方法,以给子类提供统一的行为定义。

多使用接口,尽量做到面向接口的设计和编程,以提高系统的可扩展性。

6.3 继承与组合

尽量使用组合代替继承:一则使类的层次不至于过深;二则使类与类、包与包之间的耦合度更小,更具可扩展性。

6.4 构造方法和静态工厂方法

当需要使用多个构造方法创建类时,建议使用静态工厂方法替代这些构造方法,例如:

/**
* <p>用户类</p>
* 
* 生成用户实例对象
*/ 
public class User {
    public User() {
        super();
        // do somethings to create user instance 
    }

    /**
    * 获取用户实例
    * 
    * 该方法创建用户对象实例,并返回该实例
    * 
    * @param name 用户名
    * @param password 用户密码
    * @return User实例
    */
    public static User getInstance(String name, String password) {
        User user = new User();
        user.setName(name);
        user.setPassword(password);
        return user;
    }
}

6.5 toString()

核心的、重要的类应重载toString()方法,以使该类能输出必要和有用的信息等,例如:

/**
* @see java.lang.Object#toString()
*/
public String toString() {
    final StringBuffer sb = new StringBuffer("Actor:[");
    sb.append("ID = ").append(_id) .append(",Name = ").append(_name) .append(']');
    return sb.toString();
}

6.6 Singleton Class

单例类使用如下方式声明,并将其缺省构造方法声明成private,例如:

public class Singleton {
    private static Singleton instance = new Singleton();
    // 私有缺省构造方法,避免被其他类实例化 
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
} // end Singleton 

单例类若需要实现序列化,则必须提供readResolve()方法,确保反序列化出来的类仍然是唯一的实例。

七、方法

7.1 基本原则

  • 一个方法只完成一项功能。除定义系统的公用接口方法外,其它方法应尽可能的缩小其可见性;
  • 避免用一个类实例去访问其静态变量和方法;
  • 避免在一个方法里提供多个出口.

例如:

// 不要使用这种方式,当处理程序段很长时将很难找到出口点  
if (condition) {
    return A;
} else {
    return B;
}
//如下方式 
String result = null;
if (condition) {
    result = A;
} else {
    result = B;
}
return result;

7.2 参数和返回值

避免过多的参数列表,尽量控制在5个以内。若需要传递多个参数,应使用一个容纳这些参数的对象进行传递,以提高程序的可读性和可扩展性。

参数类型和返回值尽量接口化,以屏蔽具体的实现细节,提高系统的可扩展性。

例如:

public void joinGroup(List userList) {}   // 参数类型使用接口
public List listAllUsers() {}   // 返回值类型使用接口

7.3 输入参数合法性判断原则

除构造方法、getter、setter外,public类的public和protected方法必须判断输入参数是否为null,如果是字符串,同时要判断是否为””,文件名或File对象要判断有效性。若参数不合法则抛出IllegalArgumentException异常,并描述原因。也可以根据技术框架的特点,约定统一的实现方式,如:向上层方法抛出异常或返回错误信息。private方法不必判断输入参数合法性。

示例:

public String compare(String euspFileName, String sapFileName, String outputPath) {
    // 参数校验
    if (null == euspFileName || 0 == euspFileName.length()) {
        throw new IllegalArgumentException("euspFileName is null");
    }
    if (null == sapFileName || 0 == sapFileName.length()) {
        throw new IllegalArgumentException("sapFileName is null");
    }

    File euspFile = new File(euspFileName);
    if (!euspFile.exists() || !euspFile.isFile() || !euspFile.canRead()) {
        throw new IllegalArgumentException("file[" + euspFileName
					+ "] is not valid.");
    }
    File sapFile = new File(sapFileName);
    if (!sapFile.exists() || !sapFile.isFile() || !sapFile.canRead()) {
        throw new IllegalArgumentException("file[" + sapFileName
				+ "] is not valid.");
    }
    if (null == outputPath || 0 == outputPath.length()) {
        outputPath = "." + File.separator;
    }
    else if (!outputPath.endsWith(File.separator)) {
        outputPath += File.separator;
    }
    File outputDir = new File(outputPath);
    if (!outputDir.exists() || !outputDir.isDirectory()
				|| !outputDir.canWrite()) {
        throw new IllegalArgumentException("directory[" + outputPath
					+ "] is not valid.");
    }
    …
}

八、表达式和语句

8.1 基本原则

  • 表达式和语句应清晰、简洁、易于阅读和理解,避免使用晦涩难懂的语句;
  • 每行至多包含一条执行语句,过长应换行;
  • 避免在构造方法中执行大量耗时的初始化工作,应将这种工作下放到被使用时再创建相应资源,如果不可避免,则应使用对象池和Cache等技术提高系统性能;
  • 避免在一个语句中给多个变量赋相同的值。它很难读懂;
  • 不要将中文字符硬编码到代码中,尽量使用英文。如字符串常量值(System.out.print)、日志信息、异常的message等。目的是避免字节码(class文件)在不同操作系统下的乱码问题;
  • 不要使用内嵌(embedded)赋值运算符试图提高运行时的效率,这是编译器的工作;
  • 尽量在声明局部变量的同时初始化。唯一不这么做的理由是变量的初始值依赖于某些先前发生的计算;
  • 一般而言,在含有多种运算符的表达式中,需要使用圆括号来避免运算符优先级问题,即使运算符的优先级对你而言可能很清楚,但对其他人未必如此。你不能假设别的程序员和你一样清楚运算符的优先级;
  • 不要为了表现编程技巧而过分使用技巧,简单就好;
  • if、while、try、finally、switch、synchronized和static instantiation的代码块中应有相应的逻辑处理,不能为空。

8.2 控制语句

判断表达式中如有常量,则应将常量置于表达式的左侧。如:

if ( null == user)…

尽量不使用三目条件判断。

所有if语句必须用{}包括起来,即便是只有一句:

示例:

if (true) {
    …
}
if (true) 
    i = 0; // 不要使用这种

当有多个else分句时,应分别注明其条件,注意缩进并对齐,

示例:

// 先判断i 是否等于1 
if (i == 1) {
    …
} else if (i == 2) {
    // i == 2 说明…… 
    j = i;
} else {
    // 如果都不是(i > 2 || i < 1) 
    // 说明出错了 
} // end if  i == 1

过多的else分句请将其转成switch语句或使用子方法。

当一个case子句没有break语句时,应注释说明,如:

switch (condition) {
    case ABC:
        // statements; 
        // 继续下一个CASE 
    case DEF: 
        // statements; 
        break;
    case XYZ:
        // statements; 
        break;
    default:
        // statements; 
        break;
} // end switch 

除非有万不得已的原因和无替代办法,一般情况下,禁止使用超过3层的if/else嵌套。 if/else嵌套绝大部分情况下可以拉平为一组平行的if语句。超过3层的if/else嵌套往往难以阅读和调试,特别是采用eclipse等单步调试工具,断点的跳跃性太大。

示例:

if (conditon1) {
    expr1;
    if (conditon1) {
        expr1;
        if (conditon1) {
            expr1;
        } else {
            expr1;
        }
    } else {
        expr1;
        if (conditon1) {
            expr1;
        } else {
            expr1;
        }
    }
} else {
    expr1;
    if (conditon1) {
        expr1;
        if (conditon1) {
            expr1;
        } else {
            expr1;
        }
    } else {
        expr1;
        if (conditon1) {
            expr1;
        } else {
            expr1;
        }
    }
}

可以拉平为:

if (!conditon1) {
    // 1.output error; 
    // 2.release resource such as Stream; 
    // 3.return/exit/throw RuntimeException; 
}
expr1;

if (!conditon2) {
    // 1.output error; 
    // 2.release resource such as Stream; 
    // 3.return/exit/throw RuntimeException; 
}
expr2;
…

8.3 循环语句

循环中必须有终止循环的条件或语句,避免死循环。

循环中不能人为的改变步长。

当在for语句的初始化或更新子句中使用逗号时,避免使用三个以上变量。若需要,可以在for循环之前(为初始化子句)或for循环末尾(为更新子句)使用单独的语句。

循环条件在每次循环中都会执行一次,应尽量避免在其中调用耗时或费资源的操作。比较一下两种循环的差异:

// 不推荐方式 
while (index < products.getCount()) {
    // 每此都会执行一次getCount()方法 
    // 若此方法耗时则会影响执行效率 
    // 而且可能带来同步问题,若有同步需求,请使用同步块或同步方法 
}
// 推荐方式 
// 将操作结构保存在临时变量里,减少方法调用次数 
final int count = products.getCount();
while (index < count) {
}

九、错误与异常

9.1 基本原则

  • 异常的发生表示程序已无法继续正常执行了,只能作一些收尾处理。如记日志、释放资源等;
  • 不要使用异常实现控制结构。通常的思想是只对逻辑和编程的错误采用异常处理;
  • 通常的法则是系统在正常状态下、无重新加载和硬件失效状态下,不应产生任何异常;
  • 最小化从一个给定的抽象类中导出的异常个数;
  • 对于经常发生的可预计事件不要采用异常;
  • 确保状态码有一个正确值;
  • 在本地进行安全性、合法性检查,而不是让用户去做;
  • 若有finally子句,则不要在try块中直接返回,亦不要在finally中直接返回。

9.2 受控异常与运行时异常

受控异常必须捕捉并做相应处理,不能将受控异常抛到系统之外去处理。

对可预见的运行时异常当进行捕捉并处理,比如空指针等。通常,对空指针的判断不是使用捕捉NullPointException的方式,而是在调用该对象之前使用判断语句进行直接判断。

示例:

// 若不对list是否为null进行检查,则在其为null时会抛出空指针异常 
if (null != list && 0 < list.size()) {
    for (int i = 0; i < list.size(); i++) {
    }
}

9.3 异常的传递

避免异常淹没,错误的例子:

try {
	someMethod();
} catch(SomeException ex) {
}

禁止捕获到异常未作任何处理。应向上抛出由顶层方法统一处理异常,同时系统需要使用日志输出组件把完整的异常信息写入日志文件。

避免异常信息丢失和篡改,错误的例子:

try {
    someMethod();
} catch(SomeException excp) {
    throw new OtherException(); // or throw new therException("some message");
}

真实的异常信息丢失了,或被篡改了,上层调用程序将无法得知真实的原因,这将会耗费大量的调试工作量和时间,实乃开发中的大忌。

至少做到:

try {
    someMethod();
} catch(SomeException ex) {
    log.error(ex);
    throw new OtherException(ex.getMessage(), ex); // or throw new OtherException("some message :"+ex.getMessage(),ex); 
}

9.4 异常处理规范

避免异常在层层传递过程中丢失或被篡改的方法,就是不层层捕获和抛出异常,而将最原始的异常直接抛到最上层的处理程序中。

好处:

  • 避免了层层传递过程中的异常淹没和信息丢失。
  • 代码变简洁,减少了大量的try/catch/throw语句。

这样try/catch语句只出现在最上层的方法,如main、Runnable.run方法中了。

9.5 异常处理范例

文件、socket等资源的释放建议放在finally中,但视具体逻辑而定。

示例:

try {
    lineColumnTitles = fileReader.readLine();
    // other statement; 
} catch (IOException e) {
    throw e;
} finally {
    try {
        fileReader.close();
    } catch (IOException ex) {
        log.error(ex);
    }
}

十、性能

10.1 基本原则

性能的提升并不是一蹴而就的,而是由良好的编程习惯积累的。虽然任何良好的习惯和经验对性能的提升十分有限,甚至微乎其微,但是良好的系统性能却是由这些习惯等和经验积累而成,不积细流,无以成江海。

10.2 String

不要使用如下String初始化方法:

String str = new String(“abcdef”);

这将产生两个对象,应当直接赋值:

String str = “abcdef”;

在处理可变 String 的时候要尽量使用 StringBuilder 类,需要线程同步时才使用StringBuffer 类。

10.3 StringBuilder

StringBuilder的构造器会创建一个默认大小的字符数组。在使用中,如果超出这个大小,就会重新分配内存,创建一个更大的数组,并将原先的数组复制过来,再丢弃旧的数组。在大多数情况下,可以在创建StringBuilder的时候指定大小,这样就避免了在容量不够的时候自动增长,以提高性能。

示例:StringBuilder builder= new StringBuilder (1024);

10.4 集合

避免使用Vector和HashTable等旧的集合实现,这些实现的存在仅是为了与旧的系统兼容,而且由于这些实现是同步的,故而在大量操作时会带来不必要的性能损失。在新的系统设计中不应出现这些实现,使用ArrayList代替Vector,使用HashMap代替HashTable。

若确实需要使用同步集合类,当使用如下方式获得同步集合实例:

Map map = Collections.synchronizedMap(new HashMap());

由于数组、ArrayList与Vector之间的性能差异巨大,故在能使用数组时用ArrayList,尽量避免使用Vector。

为“ArrayList”和“HashMap”定义初始大小,JVM为Vector扩充大小的时候需要重新创建一个更大的数组,将原先数组中的内容复制过来,最后,原先的数组再被回收。可见Vector容量的扩大是一个颇费时间的事。所以最好能准确的估计需要的最佳大小:

public ArraryList arrayList = new ArraryList(20);
public HashMap map = new HashMap(10);

HashMap使用不当会造成JVM频繁进行垃圾回收,需要封装大量的数据时,应避免使用HashMap,使用特定的JavaBean对象替换HashMap。

10.5 对象

避免在循环中频繁构建和释放对象。

不再使用的对象应及时销毁。

如无必要,不要序列化对象。

避免创建无用对象(如没有必要,则不要对变量进行初始化)。

10.6 同步

在不需要同步操作时避免使用同步操作类,如能使用ArrayList时不要使用Vector。

尽量少用同步方法,避免使用太多的 synchronized 关键字。

尽量将同步最小化,即将同步作用到最需要的地方,避免大块的同步块或方法等。

10.7 final

将参数或方法声明成final可提高程序响应效率,但是绝对不要仅因为性能而将类、方法等声明成final,声明成final的类、方法一定要确信不再被继承或重载。

不需要重新赋值的变量(包括类变量、实例变量、局部变量)声明成final。

私有方法不需要声明成final。

若方法确定不会被继承,则声明成final。

10.8 垃圾收集和资源释放

不要过分依赖JVM的垃圾收集机制,因为JVM在什么时候运行GC是无法预测和知道的。

尽可能早的释放资源,不再使用的资源需立即释放。

可能有异常的操作时必须在try的finally块中释放资源,如数据库连接、socket连接、IO操作等。

示例:

Connection conn = null;
try {
    …
} catch(Exception e) {
    // 异常捕捉和处理
} finally { 
    // 判断conn等是否为null
    if (null != conn) {
        try {
            conn.close();
        } catch(Exception ex) {
        }
    }
} // end try...catch...finally 

10.9 System.arraycopy()

使用System.arraycopy ()代替通过来循环复制数组,System.arraycopy ()要比通过循环来复制数组快的多。

示例:

public class IRB {
    void method () {
        int[] array1 = new int [100];
        for (int i = 0; i < array1.length; i++) {
            array1 [i] = i;
        }
        int[] array2 = new int [100];
        for (int i = 0; i < array2.length; i++) {
            array2 [i] = array1 [i];
        }
    }
}

建议:

public class IRB {
    void method () {
        int[] array1 = new int [100];
        for (int i = 0; i < array1.length; i++) {
            array1 [i] = i;
        }
        int[] array2 = new int [100];
        System.arraycopy(array1, 0, array2, 0, 100);
    }
}

10.1 单例

需要在合适的场合使用单例。单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例。简单来说,单例主要适用于以下三个方面:

  • 控制资源的使用,通过线程同步来控制资源的并发访问;
  • 控制实例的产生,以达到节约资源的目的;
  • 控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。

10.11 静态变量

避免随意的使用静态变量,要知道,当某个对象被定义为stataic变量所引用,那么GC通常是不会回收这个对象所占有的内存,如:

public class A {
    static B b = new B();
}

此时静态变量b的生命周期与A类同步,如果A类不被卸载,那么b对象会常驻内存,直到程序终止。

10.12 创建Java对象

尽量避免过多的创建Java对象,避免在常用方法的循环中new对象。由于系统不仅要花费时间来创建对象,而且还要花时间对这些对象进行垃圾回收和处理,在我们可以控制的范围内,最大限度的重用对象,在适当的情况下用基本的数据类型或数组来替代对象。

10.13 局部变量

尽量使用局部变量,调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快。其他变量:静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。

10.14 包装类型和基本类型

虽然包装类型和基本类型在使用过程中是可以相互转换,但它们两者所产生的内存区域是完全不同的。基本类型数据的产生和处理都在栈中进行;包装类型是对象,是在堆中产生实例。

集合类中的对象,适合使用包装类型,其它情况提倡使用基本类型。

10.15 慎用synchronized

实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。使用synchronized关键字时,直接会把其修饰体(代码块、方法、类)锁住,在修饰体代码执行完之前其他线程无法调用执行该修饰体。所以synchronized修饰的范围应尽量小,并且应尽量使用方法同步代替代码块同步。

synchronized关键字的修饰体不一定是当前对象,如果是在方法上加的synchronized,则是以对象本身为锁的,如果是静态方法则锁的是类。

10.16 finalize方法

尽量不要使用finalize方法,JVM的规范并不保证何时执行该方法,所以用这个方法来释放资源很不合适,有可能造成长时间资源得不到释放。

10.17 慎用异常

当创建一个异常时,需要收集一个栈跟踪(stack track),这个栈跟踪用于描述异常是在何处创建的。构建这些栈跟踪时需要为运行时栈做一份快照,正是这一部分开销很大。当需要创建一个Exception时,JVM不得不说:先别动,对现在的样子存一份快照,所以暂时停止入栈和出栈操作。栈跟踪不只包含运行时栈中的一两个元素,而是包含这个栈中的每一个元素。

如果创建一个Exception,就得付出代价。好在捕获异常开销不大,因此可以使用 try-catch 将核心内容包起来。从技术上讲,甚至可以随意地抛出异常,而不用花费很大的代价。招致性能损失的并不是 throw 操作——尽管在没有预先创建异常的情况下就抛出异常是有点不寻常。真正要花代价的是创建异常。幸运的是,好的编程习惯已教会我们,不应该不管三七二十一就抛出异常。异常是为异常的情况而设计的,使用时也应该牢记这一原则。

十一、安全

11.1 跨站脚本XSS

风险及危害性

跨站脚本XSS指利用网站漏洞从用户那里恶意盗取信息。用户在浏览网站、使用即时通讯软件、甚至在阅读电子邮件时,通常会点击其中的链接。攻击者通过在链接中插入恶意代码,就能够盗取用户信息。攻击者通常会用十六进制(或其他编码方式)将链接编码,以免用户怀疑它的合法性。网站在接收到包含恶意代码的请求之后会产成一个包含恶意代码的页面,而这个页面看起来就像是那个网站应当生成的合法页面一样。许多流行的留言本和论坛程序允许用户发表包含HTML和 javascript的帖子。假设用户甲发表了一篇包含恶意脚本的帖子,那么用户乙在浏览这篇帖子时,恶意脚本就会执行,盗取用户乙的session信息。

如何导致XSS攻击,一般来说来自http的post,或者get方式取得参数值很可能为恶意代码,如果开发者直接用这些参数组合成http链接,用户点击该连接,就会造成XSS攻击风险。

应对措施

开发者要保证代码的安全性,使其免受XSS攻击,可采取以下措施:

  • 过滤或转换用户提交数据中的HTML代码。
  • 限制用户提交数据的长度。

非安全代码示例

<%
String mofWindowId = request.getParameter("mofWindowId");
%>
<form name="importXml"  action="mofXml.cmd?method=importMofXml&primaryKey=<%=mofWindowId%>" method="post">

安全代码示例

<%
String mofWindowId=XSSCheck.getParameter(request,”mofWindowId”);
%>
<form name="importXml" 
action="mofXml.cmd?method=importMofXml&primaryKey=<%=mofWindowId%>" method="post">

注:XSSCheck为公用工具类,用于XSS检查,其getParameter实现逻辑如下:

  • 通过参数名称,从请求中取得参数值;
  • 将&,<,>,’,”转义,如下:

3)返回安全的字符串。

11.2 违反信任边界规则(Trust Boundary Violation)

风险及危害

一个受信任的边界可以被认为是由系统划出的边境,例如session、attribute、aplication、数据库、文件等在服务端存储边界都认为是受信任的。反之来自http的post,或者get方式取得参数值是不受信任的。凡是将非受信任边界的参数转入到受信任的边界内,需要对参数值进行检查,否则造成信任边界违例。当开发者直接操作受信边界内部的参数时会认为该参数是安全的,而造成安全隐患,例如脚本注入,XSS攻击等。

应对措施

开发者要保证代码的安全性,当参数信任边界切换的时候,对参数值进行检查,检查其内容里是否用非法脚本信息:
1)过滤或转换用户提交数据中的HTML代码。
2)限制用户提交数据的长度

非安全代码示例

String dsn = request.getParameter("DSN");
String sql = request.getParameter("SQL");
if (sql == null) {
	sql = "";
}
dsn = (String) session.getAttribute("SqlHelper.DSN");
session.setAttribute("SqlHelper.DSN", dsn);

安全代码示例

String dsn = XSSCheck.getParameter(request,"DSN");
String sql = request.getParameter("SQL");
if (sql == null) {
	sql = "";
}
dsn = (String) session.getAttribute("SqlHelper.DSN");
session.setAttribute("SqlHelper.DSN", dsn);

11.3 不安全的反射(Unsafe Reflection)

风险及危害

攻击者能够建立一个不可预测的、贯穿应用程序的控制流程,使得他们可以潜在地避开安全检测。攻击者能够建立一个在开发者意料之外的、不可预测的控制流程,贯穿应用程序始终。这种形式的攻击能够使得攻击者避开身份鉴定,或者访问控制检测,或者使得应用程序以一种意料之外的方式运行。如果攻击者能够将文件上传到应用程序的classpath或者添加一个classpath的新入口,那么这将导致应用程序陷入完全的困境。无论是上面哪种情况,攻击者都能使用反射将新的、多数情况下恶意的行为引入应用程序。

应对措施

开发者可以定制一份白名单,通过关键字关联需要实例化的类,http请求中传递是不是实际的类名,而是关键字,开发者得到关键字后在白名单中寻找需要的信息,进行实例化。

非安全代码示例

String className = request.getParameter("classname");
if ((className != null)&& ((className = className.trim()).length() != 0)) {
	// Attempt to load class and get its location.
	try {
	    ProtectionDomain pd = Class.forName(className).getProtectionDomain();
	    if (pd != null) {
			CodeSource cs = pd.getCodeSource();

安全代码示例

String classNameKey = request.getParameter("classname");
String className=WhiteList.get(classNameKey);
if ((className != null)&& ((className = className.trim()).length() != 0)) {
	// Attempt to load class and get its location. 
	try {
	    ProtectionDomain pd = Class.forName(className).getProtectionDomain();
	    if (pd != null) {
			CodeSource cs = pd.getCodeSource();

注:WhiteList.get其具体实现如下,
1)从描述白名单的文件中,读出白名单列表;
2)根据传入的关键值获取白名单中的真实值;
3)返回该值,如果没有找到,抛出异常。

11.4 SQL 注入(SQL Injection)

风险及危害

SQL注入是一种常见攻击方式,由于开发者采用sql拼凑的方式,用来自网络中不安全的参数形成sql语句访问数据库,攻击者常常采用该漏洞组合成非法的sql语句,使得信息泄露,访问到本来没有权限查看的内容或者直接破坏数据库信息等。发生SQL Injection有以下几种方式:

1)进入程序的数据来自不可信赖的资源。
2)数据用于动态构造一个SQL查询。

应对措施

  • 开发者可以采用带参方式访问sql语句访问数据库,在java中即采用PreparedStatement的方式访问数据库。
  • 如果开发者一定要使用sql拼凑的方式访问数据,对字符串要检查并过滤单引号’,对于可能为整形或者浮点类型参数,要先转整形,或者浮点,再进行拼凑。

非安全代码示例

String userid = (String)session.getAttribute("classname");
String param1 = request.getParameter(“param1”);
StringBuffer strbuf = new StringBuffer();
strbuf.append(“select * from table1 where userid=”);
strbuf.append(userid);
strbuf.append(“ and param1=’”).append(param1).append(“’”);
String sql = strbuf.toString();
// 当param1为 test’ or 1=1 
那么这条语句就为 select * from table1 where userid=$userid and param1=’test’ or 1=1这样查询出来的数据就超越了这个用户访问的范围。

安全代码示例

方法一:采用PreparedStatement访问数据库。

String userid = (String)session.getAttribute("classname");
String param1= request.getParameter(“param1”);
StringBuffer strbuf = new StringBuffer();
String sql = “select * from table1 where userid=? and param1=?”;

方法二:检查并过滤特殊字符

String userid = (String)session.getAttribute("classname");
String param1= request.getParameter(“param1”);
StringBuffer strbuf = new StringBuffer();
strbuf.append(“select * from table1 where userid=”);
strbuf.append(userid);
strbuf.append(“ and param1=’”)
.append(SqlInjectCheck.checkStringValue(param1)).append(“’”);
String sql = strbuf.toString();

注:SqlInjectCheck.checkStringValue是公用方法,其实现如下,
1)将 ‘ 转化成 &acute;
2)返回字符串。

11.5 系统信息泄露(System Information Leakage)

风险及危害

JSP中出现HTML注释(System Information Leakage:HTML Comment in JSP),攻击者可以通过html的注释得到用于攻击的信息。

应对措施

应使用JSP注释代替HTML注释。(JSP注释不会被传递给用户)。

非安全代码示例

<!—此处是取得系统信息的注释 ->

安全代码示例

<% //此处是取得系统信息的注释%>

11.6 资源注入(resource injection)

风险及危害

允许用户可以通过输入来控制资源标识符,会让攻击者有能力访问或修改被保护的系统资源。

发生resource injection有以下两种方式:

  • 攻击者可以指定已使用的标识符来访问系统资源。例如,攻击者可以指定用来连接到网络资源的端口号。
  • 攻击者可以通过指定特定资源来获取某种能力,而这种能力在一般情况下是不可能获得的。

应对措施

开发者可以定制一份白名单,通过关键字关联需要资源内容,http请求中传递是不是实际的资源内容,而是关键字,开发者得到关键字后在白名单中寻找需要的信息,进行后续操作。

非安全代码示例

String dsn = request.getParameter("DSN");
DataSource ds = (DataSource) ctx.lookup(dsn);

安全代码示例

String userid = (String)session.getAttribute("classname");
String param1= request.getParameter(“param1”);
String dsnKey = request.getParameter("DSN ");
String dsn = WhiteList.get(dsnKey);
DataSource ds = (DataSource) ctx.lookup(dsn);

发表回复