第一个问题,函数是应当返回null还是长度为0的数组(或集合)?
第二个问题,函数输入参数不当时,是异常还是返回null?
先看第一个问题
有两个约定我觉得应当遵守:
1.返回零长度的数组或集合而不是null(详见《Effective Java》)
理由就是,如果返回empty,就可以少了很多not-null判断:
List<Person> list = queryPerson();
if (list != null) {
for (Person p : list) {
//do something
}
}
如果queryPerson永不返回null,那代码可以这样:
List<Person> list = queryPerson();
for (Person p : list) {
//do something
}
遵守这个规则的一个例子就是Spring JdbcTemplate的query方法,在查询不到结果时,返回的是empty List:
RowMapperResultSetExtractor:
public List<T> extractData(ResultSet rs) throws SQLException {
List<T> results = (this.rowsExpected > 0 ? new ArrayList<T>(this.rowsExpected) : new ArrayList<T>());
int rowNum = 0;
while (rs.next()) {
results.add(this.rowMapper.mapRow(rs, rowNum++));
}
return results;
}
2. 不要区分null引用和empty值
引用:
“
7. 降低修改时的误解性,不埋雷
……
一个原则就是永远不要区分null引用和empty值。
”
详见
http://javatar.iteye.com/blog/1056664
但现实世界没那么理想
比如Spring,在这两个规则上,都没有统一的处理方式
org.springframework.util.StringUtils:
public static String[] tokenizeToStringArray(
String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {
if (str == null) {
return null;
}
}
public static String[] delimitedListToStringArray(String str, String delimiter, String charsToDelete) {
if (str == null) {
return new String[0];
}
}
两个功能相似的函数,在str==null时,一个返回null,一个返回empty
同一个class中都不统一,很奇怪
顺便说一下这两个函数的最大不同:
String str = "a,b;c;,d";
String delimiter = ";,";
//同时把";"和","当作分隔符
String[] sa = StringUtils.tokenizeToStringArray(str, delimiter);
assertEquals(4, sa.length);
assertEquals("a", sa[0]);
assertEquals("b", sa[1]);
assertEquals("c", sa[2]);
assertEquals("d", sa[3]);
//认为";,"是一个分隔符
sa = StringUtils.delimitedListToStringArray(str, delimiter);
assertEquals(2, sa.length);
assertEquals("a,b;c", sa[0]);
assertEquals("d", sa[1]);
Apache StringUtils也有类似的方法,对应关系如下:
Apache Spring
---------------------------------------------------------
split tokenizeToStringArray
splitByWholeSeparator delimitedListToStringArray
两个函数对str==null的处理是统一了,但它认为null和""是不同的情况:
public static String[] split(String str, String separatorChars) {
return splitWorker(str, separatorChars, -1, false);
}
private static String[] splitWorker(String str, String separatorChars, int max, boolean preserveAllTokens) {
if (str == null) {
return null;
}
int len = str.length();
if (len == 0) {
return ArrayUtils.EMPTY_STRING_ARRAY; //that is, new String[0]
}
}
//splitByWholeSeparatorWorker在这方面跟split一样
再看看Apache ArrayUtils:
public static int[] toPrimitive(Integer[] array) {
if (array == null) {
return null;
} else if (array.length == 0) {
return EMPTY_INT_ARRAY;
}
}
同样是区分了null和empty
所以,保险起见,对数组和集合还是要加上not-null判断,虽然代码丑了一点;除非代码都是你自己写的或者你看过源码了确保永不返回null
第二个问题
Apache FileUtils和IOUtils,对输入参数为null的处理,很多时候是抛出异常,但有时也会“静静的失败”
Apache FileUtils:
//抛出异常
public static void copyDirectoryToDirectory(File srcDir, File destDir) throws IOException {
if (srcDir == null) {
throw new NullPointerException("Source must not be null");
}
if (srcDir.exists() && srcDir.isDirectory() == false) {
throw new IllegalArgumentException("Source '" + destDir + "' is not a directory");
}
//...
}
//注意到这个方法没有考虑file==null的情况
public static void writeLines(File file, String encoding, Collection<?> lines, String lineEnding, boolean append)
throws IOException {
FileOutputStream out = null;
try {
out = openOutputStream(file, append);
final BufferedOutputStream buffer = new BufferedOutputStream(out);
IOUtils.writeLines(lines, lineEnding, buffer, encoding);
buffer.flush();
out.close(); // don't swallow close Exception if copy completes normally
} finally {
IOUtils.closeQuietly(out);
}
}
//what if "file==null" ?
public static FileOutputStream openOutputStream(File file, boolean append) throws IOException {
if (file.exists()) {
if (file.isDirectory()) {
throw new IOException("File '" + file + "' exists but is a directory");
}
//...
}
return new FileOutputStream(file, append);
}
Apache IOUtils:
//不抛异常
public static void writeLines(Collection<?> lines, String lineEnding, OutputStream output, Charset encoding)
throws IOException {
if (lines == null) {
return;
}
//...
}
Apache BeanUtils:
//不抛异常
public static void populate(Object bean, Map properties)
throws IllegalAccessException, InvocationTargetException
{
BeanUtilsBean.getInstance().populate(bean, properties);
}
public void populate(Object bean, Map properties)
throws IllegalAccessException, InvocationTargetException {
// Do nothing unless both arguments have been specified
if ((bean == null) || (properties == null)) {
return;
}
//...
}
看来这个问题也是见仁见智
如果你认为NullPointerException或者IllegalArgumentException有必要通知调用方,那就抛异常,特别是调用方想知道为什么调用失败:
e.g. File存在但没有写权限
e.g. File本应该是文件但传入的是目录
个人总结:
应该采取“防御式编程”,怀疑一切