程序出错该返回啥?NULL、异常、错误码、空对象?
public class RandomIdGenerator implements IdGenerator { private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class); @Override public String generate() { String substrOfHostName = getLastFiledOfHostName(); long currentTimeMillis = System.currentTimeMillis(); String randomString = generateRandomAlphameric(8); String id = String.format("%s-%d-%s", substrOfHostName, currentTimeMillis, randomString); return id; } private String getLastFiledOfHostName() { String substrOfHostName = null; try { String hostName = InetAddress.getLocalHost().getHostName(); substrOfHostName = getLastSubstrSplittedByDot(hostName); } catch (UnknownHostException e) { logger.warn("Failed to get the host name.", e); } return substrOfHostName; } @VisibleForTesting protected String getLastSubstrSplittedByDot(String hostName) { String[] tokens = hostName.split("\\."); String substrOfHostName = tokens[tokens.length - 1]; return substrOfHostName; } @VisibleForTesting protected String generateRandomAlphameric(int length) { char[] randomChars = new char[length]; int count = 0; Random random = new Random(); while (count < length) { int maxAscii = 'z'; int randomAscii = random.nextInt(maxAscii); boolean isDigit= randomAscii >= '0' && randomAscii <= '9'; boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z'; boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z'; if (isDigit|| isUppercase || isLowercase) { randomChars[count] = (char) (randomAscii); ++count; } } return new String(randomChars); } }
- 对于 generate() 函数,如果本机名获取失败,函数返回什么?这样的返回值是否合理?
- 对于 getLastFiledOfHostName() 函数,是否应该将 UnknownHostException 异常在函数内部吞掉(try-catch 并打印日志)?还是应该将异常继续往上抛出?如果往上抛出的话,是直接把 UnknownHostException 异常原封不动地抛出,还是封装成新的异常抛出?
- 对于 getLastSubstrSplittedByDot(String hostName) 函数,如果 hostName 为 NULL 或者是空字符串,这个函数应该返回什么?
- 对于 generateRandomAlphameric(int length) 函数,如果 length 小于 0 或者等于 0,这个函数应该返回什么?
函数出错返回什么?
函数出错一般有四种情况:异常对象、空、null、错误码
1. 返回错误码
C语言中没有异常处理机制,所以一般用错误码处理异常。Java个Python因为有异常处理机制所以不用错误码。
C中错误码返回有两种:一种是直接作为函数返回返回,另一种是错误码定义为全局变量,执行出错时函数调用者用这个全局变量获取错误码。
// 错误码的返回方式一:pathname/flags/mode为入参;fd为出参,存储打开的文件句柄。 int open(const char *pathname, int flags, mode_t mode, int* fd) { if (/*文件不存在*/) { return EEXIST; } if (/*没有访问权限*/) { return EACCESS; } if (/*打开文件成功*/) { return SUCCESS; // C语言中的宏定义:#define SUCCESS 0 } // ... } //使用举例 int fd; int result = open(“c:\test.txt”, O_RDWR, S_IRWXU|S_IRWXG|S_IRWXO, &fd); if (result == SUCCESS) { // 取出fd使用 } else if (result == EEXIST) { //... } else if (result == EACESS) { //... } // 错误码的返回方式二:函数返回打开的文件句柄,错误码放到errno中。 int errno; // 线程安全的全局变量 int open(const char *pathname, int flags, mode_t mode){ if (/*文件不存在*/) { errno = EEXIST; return -1; } if (/*没有访问权限*/) { errno = EACCESS; return -1; } // ... } // 使用举例 int hFile = open(“c:\test.txt”, O_RDWR, S_IRWXU|S_IRWXG|S_IRWXO); if (-1 == hFile) { printf("Failed to open file, error no: %d.\n", errno); if (errno == EEXIST ) { // ... } else if(errno == EACCESS) { // ... } // ... }
2. 返回null值
不太建议:
- 如果某个函数有可能返回null值而调用者在使用的时候没有做null判断处理,可能会导致空指针异常NPE;
- 如果我们定义了这种函数,代码中会充斥着大量NULL值判断逻辑,写起来繁琐而且和业务代码耦合,影响可读性。
public class UserService { private UserRepo userRepo; // 依赖注入 public User getUser(String telephone) { // 如果用户不存在,则返回null return null; } } // 使用函数getUser() User user = userService.getUser("18917718965"); if (user != null) { // 做NULL值判断,否则有可能会报NPE String email = user.getEmail(); if (email != null) { // 做NULL值判断,否则有可能会报NPE String escapedEmail = email.replaceAll("@", "#"); } }
那是否可以用异常代替呢?
对于get、find、query之类的函数,没找到就是返回null并不是异常。null应该更合理。当然这个事情还是要整个项目的规范一致。
对于查找这种函数,处理返回数据对象,还会返回下标位置。比如Java的indexOf函数。这个时候就不好用null表示值不存在的情况(对比找数据,这个是找位置);我们对于这种情况两种处理:1. 返回位置没找到的异常 2. 是返回一个特殊值,例如-1。-1更合理一点因为没找到不是异常而是正常情况。
3. 返回空对象
一个经典的策略是应用空对象设计模式,后面讲。
对于比较简单的情况,当返回的数据是字符串或者是空集合类型的时候, 可以用空字符串和空集合。这样我们在使用函数的时候就不需要做null值判断。
// 使用空集合替代NULL public class UserService { private UserRepo userRepo; // 依赖注入 public List<User> getUsers(String telephonePrefix) { // 没有查找到数据 return Collections.emptyList(); } } // getUsers使用示例 List<User> users = userService.getUsers("189"); for (User user : users) { //这里不需要做NULL值判断 // ... } // 使用空字符串替代NULL public String retrieveUppercaseLetters(String text) { // 如果text中没有大写字母,返回空字符串,而非NULL值 return ""; } // retrieveUppercaseLetters()使用举例 String uppercaseLetters = retrieveUppercaseLetters("wangzheng"); int length = uppercaseLetters.length();// 不需要做NULL值判断 System.out.println("Contains " + length + " upper case letters.");
4. 抛出异常对象
最常见的还是抛出异常。异常会带有更多的信息,比如函数调用栈,而且异常处理和正常的逻辑还是分开的,所以可读性会好点。
大部分动态语言只有运行时异常(非受检异常),而Java还有编译时异常,也叫受检异常。
对于代码bug以及不可恢复的异常(数据库连接失败),即使我们捕获了也做不了什么,我们就用非受检异常;对于可恢复异常,比如提额现金大于余额,我们使用受检异常明确告知调用者需要捕获处理。
当 Redis 的地址(参数 address)没有设置的时候,我们直接使用默认的地址(比如本地地址和默认端口);当 Redis 的地址格式不正确的时候,我们希望程序能 fail-fast,也就是说,把这种情况当成不可恢复的异常,直接抛出运行时异常,将程序终止掉。
// address格式:"192.131.2.33:7896" public void parseRedisAddress(String address) { this.host = RedisConfig.DEFAULT_HOST; this.port = RedisConfig.DEFAULT_PORT; if (StringUtils.isBlank(address)) { return; } String[] ipAndPort = address.split(":"); if (ipAndPort.length != 2) { throw new RuntimeException("..."); } this.host = ipAndPort[0]; // parseInt()解析失败会抛出NumberFormatException运行时异常 this.port = Integer.parseInt(ipAndPort[1]); }
如何处理抛出的异常
- 直接吞掉
public void func1() throws Exception1 { // ... } public void func2() { //... try { func1(); } catch(Exception1 e) { log.warn("...", e); //吐掉:try-catch打印日志 } //... }
- 原封不动re-throw
public void func1() throws Exception1 { // ... } public void func2() throws Exception1 {//原封不动的re-throw Exception1 //... func1(); //... }
- 包装成新的异常然后抛出
public void func1() throws Exception1 { // ... } public void func2() throws Exception2 { //... try { func1(); } catch(Exception1 e) { throw new Exception2("...", e); // wrap成新的Exception2然后re-throw } //... }
当我们面对异常的时候,
- 如果func1抛出的异常是可恢复的,而且func2的调用方不关心此异常,那么我们可以在func2内将func1的异常吞掉;
- 如果func1的异常对于func2的调用方来说,可以理解、关心的,并且在业务概念上有一定的相关性,可以选择继续抛出;
- 如func1的异常太底层,func2的调用方缺乏背景去理解、业务无关,我们就把他包装成调用方可以理解的新异常,re-throw。
是否往上继续抛出,要看上层代码是否关心这个异常。关心就将它抛出,否则就直接吞掉。是否需要包装成新的异常抛出,看上层代码是否能理解这个异常、是否业务相关。如果能理解、业务相关就可以直接抛出,否则就封装成新的异常抛出。
重构ID生成器项目中各函数的异常处理代码
public class RandomIdGenerator implements IdGenerator { private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class); @Override public String generate() { String substrOfHostName = getLastFiledOfHostName(); long currentTimeMillis = System.currentTimeMillis(); String randomString = generateRandomAlphameric(8); String id = String.format("%s-%d-%s", substrOfHostName, currentTimeMillis, randomString); return id; } private String getLastFiledOfHostName() { String substrOfHostName = null; try { String hostName = InetAddress.getLocalHost().getHostName(); substrOfHostName = getLastSubstrSplittedByDot(hostName); } catch (UnknownHostException e) { logger.warn("Failed to get the host name.", e); } return substrOfHostName; } @VisibleForTesting protected String getLastSubstrSplittedByDot(String hostName) { String[] tokens = hostName.split("\\."); String substrOfHostName = tokens[tokens.length - 1]; return substrOfHostName; } @VisibleForTesting protected String generateRandomAlphameric(int length) { char[] randomChars = new char[length]; int count = 0; Random random = new Random(); while (count < length) { int maxAscii = 'z'; int randomAscii = random.nextInt(maxAscii); boolean isDigit= randomAscii >= '0' && randomAscii <= '9'; boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z'; boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z'; if (isDigit|| isUppercase || isLowercase) { randomChars[count] = (char) (randomAscii); ++count; } } return new String(randomChars); } }
- 对于 generate() 函数,如果本机名获取失败,函数返回什么?这样的返回值是否合理?
- 对于 getLastFiledOfHostName() 函数,是否应该将 UnknownHostException 异常在函数内部吞掉(try-catch 并打印日志)?还是应该将异常继续往上抛出?如果往上抛出的话,是直接把 UnknownHostException 异常原封不动地抛出,还是封装成新的异常抛出?
- 对于 getLastSubstrSplittedByDot(String hostName) 函数,如果 hostName 为 NULL 或者是空字符串,这个函数应该返回什么?
- 对于 generateRandomAlphameric(int length) 函数,如果 length 小于 0 或者等于 0,这个函数应该返回什么?
重构generate函数
如何处理空字符和null,本身是要看业务是否允许。不过就个人判断来讲,更倾向于抛出异常给调用者。
public String generate() throws IdGenerationFailureException { String substrOfHostName = getLastFieldOfHostName(); if (substrOfHostName == null || substrOfHostName.isEmpty()) { throw new IdGenerationFailureException("host name is empty."); } long currentTimeMillis = System.currentTimeMillis(); String randomString = generateRandomAlphameric(8); String id = String.format("%s-%d-%s", substrOfHostName, currentTimeMillis, randomString); return id; }
重构getLastFieldOfHostName函数
private String getLastFieldOfHostName() throws UnknownHostException{ String substrOfHostName = null; String hostName = InetAddress.getLocalHost().getHostName(); substrOfHostName = getLastSubstrSplittedByDot(hostName); return substrOfHostName; }
当主机名获取失败的时候,返回null还是会影响后续的逻辑,所以抛出一个异常会比较好。而上层调用者和他业务相关,所以也不需要对这个异常进行重新包装。
这个函数的调用者(generate
)获取到的UnknownHostException
我们需要包装:
- 上层调用者调用
generate
的时候,不知道细节,面向接口而非实现。所以在出异常的时候也不能暴露细节。 - 从封装的角度,我们不希望低层的异常暴露给上层,调用者拿到这异常并不知道如何处理。
UnknownHostException
和generate
在业务上没有相关性。
public String generate() throws IdGenerationFailureException { String substrOfHostName = null; try { substrOfHostName = getLastFieldOfHostName(); } catch (UnknownHostException e) { throw new IdGenerationFailureException("host name is empty."); } long currentTimeMillis = System.currentTimeMillis(); String randomString = generateRandomAlphameric(8); String id = String.format("%s-%d-%s", substrOfHostName, currentTimeMillis, randomString); return id; }
重构 getLastSubstrSplittedByDot() 函数
@VisibleForTesting protected String getLastSubstrSplittedByDot(String hostName) { String[] tokens = hostName.split("\\."); String substrOfHostName = tokens[tokens.length - 1]; return substrOfHostName; }
理论上hostName参数传递应该程序员来保证没有code bug。
所以如果是私有方法你自己调用,你可以把握传递的参数是ok的,那就不需要做空值或者null的判断;反之是public,别人可以调用,他就有可能会会发生这种误用的情况。所以为了提高健壮性,需要加上。
@VisibleForTesting protected String getLastSubstrSplittedByDot(String hostName) { if (hostName == null || hostName.isEmpty()) { throw IllegalArgumentException("..."); //运行时异常 } String[] tokens = hostName.split("\\."); String substrOfHostName = tokens[tokens.length - 1]; return substrOfHostName; }
在调用者时候,我们也需要保证传参不是null或者空值,所以在getLastFieldOfHostName() 函数的代码也要作相应的修改
private String getLastFieldOfHostName() throws UnknownHostException{ String substrOfHostName = null; String hostName = InetAddress.getLocalHost().getHostName(); if (hostName == null || hostName.isEmpty()) { // 此处做判断 throw new UnknownHostException("..."); } substrOfHostName = getLastSubstrSplittedByDot(hostName); return substrOfHostName; }
重构 generateRandomAlphameric() 函数
@VisibleForTesting protected String generateRandomAlphameric(int length) { char[] randomChars = new char[length]; int count = 0; Random random = new Random(); while (count < length) { int maxAscii = 'z'; int randomAscii = random.nextInt(maxAscii); boolean isDigit= randomAscii >= '0' && randomAscii <= '9'; boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z'; boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z'; if (isDigit|| isUppercase || isLowercase) { randomChars[count] = (char) (randomAscii); ++count; } } return new String(randomChars); } }
对于入参length,非正数的时候应该怎么处理,首先负数肯定是不合理的,所以可以抛出异常,而0可以看业务需要,不过这种corner case不管是把他当成异常还是正常输入,在注释说明是很有必要的。
重构之后的完整代码
public class RandomIdGenerator implements IdGenerator { private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class); @Override public String generate() throws IdGenerationFailureException { String substrOfHostName = null; try { substrOfHostName = getLastFieldOfHostName(); } catch (UnknownHostException e) { throw new IdGenerationFailureException("...", e); } long currentTimeMillis = System.currentTimeMillis(); String randomString = generateRandomAlphameric(8); String id = String.format("%s-%d-%s", substrOfHostName, currentTimeMillis, randomString); return id; } private String getLastFieldOfHostName() throws UnknownHostException{ String substrOfHostName = null; String hostName = InetAddress.getLocalHost().getHostName(); if (hostName == null || hostName.isEmpty()) { throw new UnknownHostException("..."); } substrOfHostName = getLastSubstrSplittedByDot(hostName); return substrOfHostName; } @VisibleForTesting protected String getLastSubstrSplittedByDot(String hostName) { if (hostName == null || hostName.isEmpty()) { throw new IllegalArgumentException("..."); } String[] tokens = hostName.split("\\."); String substrOfHostName = tokens[tokens.length - 1]; return substrOfHostName; } @VisibleForTesting protected String generateRandomAlphameric(int length) { if (length <= 0) { throw new IllegalArgumentException("..."); } char[] randomChars = new char[length]; int count = 0; Random random = new Random(); while (count < length) { int maxAscii = 'z'; int randomAscii = random.nextInt(maxAscii); boolean isDigit= randomAscii >= '0' && randomAscii <= '9'; boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z'; boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z'; if (isDigit|| isUppercase || isLowercase) { randomChars[count] = (char) (randomAscii); ++count; } } return new String(randomChars); } }