我们可以把函数的运行结果分为两类。一类是预期结果,也就是正常情况下输出的结果。一类是非预期的结果,也就是函数在异常(或出错)情况下输出的结果。
在正常情况下,函数返回数据的类型非常明确,但是在异常情况下,函数的返回数据类型确非常灵活,有多种选择,比如异常(Exception)、错误码、NULL 值、特殊值(比如 -1)、空对象(比如空字符串、空集合)等。
在异常情况下,函数到底该返回什么样的数据类型,并不那么容易判断。比如,上节课中,本机名获取失败的时候, ID 生成器的 generate()
函数应该返回什么呢? 是异常?空字符串?还是 NULL 值?又或者是其他特殊值呢?
上篇《规范与重构 - 7.实践:通过一段ID生成器代码,学习如何发现代码质量问题》我们把一份 ID 生成器代码从 “能用” 重构成了 “好用”。最终给出的代码看似以及完美了,但是如果再用心推敲以下,代码中关于出错处理的方式,还有进一步优化的空间,值得我们拿出来再讨论下。
下面是上节课的代码。
public class RandomIdGenerator implements LogTraceIdIdGenerator {
private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
@Override
public String generate() {
String substrOfHostName = getLastFieldOfHostName();
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format("%s-%d-%s",
substrOfHostName, currentTimeMillis, randomString);
return id;
}
private String getLastFieldOfHostName() {
String substrOfHostName = null;
try {
String hostName = InetAddress.getLocalHost().getHostName();
substrOfHostName = getLastSubstrSplitByDot(hostName);
} catch (UnknownHostException e) {
logger.error("failed to get the host name.", e);
}
return substrOfHostName;
}
@VisibleForTesting
protected String getLastSubstrSplitByDot(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 randomAscii = random.nextInt(122);
boolean isDigit = randomAscii >= 48 && randomAscii <= 57;
boolean isUpperCase = randomAscii >= 65 && randomAscii <= 90;
boolean isLowerCase = randomAscii >= 97 && randomAscii <= 122;
if (isDigit || isUpperCase || isLowerCase) {
randomChars[count++] = (char) randomAscii;
}
}
return new String(randomChars);
}
}
这段代码中的四个函数的出错处理方式,总结出下面这样几个问题:
generate()
函数,如果本机名获取失败,函数返回什么?这样的返回值是否合理?getLastFieldOfHostName()
函数,是否应该将 UnknownHostException
异常在函数内部吞掉(try-catch 并打印日志)?还是应该将异常抛出?如果抛出的话,是直接把 UnknownHostException
原封不动的抛出,还是封装成新的异常抛出?getLastSubstrSplitByDot()
函数,如果 hostName 为 NULL 或者是空字符串,这个函数应该返回什么?generateRandomAlphameric()
函数,如果 length 小于或等于 0,这个函数应该返回什么?函数出错返回的数据,一共有四种情况:错误码、NULL 值、空对象、异常对象。接下来,我们一一分析下。
C 语言中没有异常这样的语法机制,因此,返回错误码便是最常用的出错处理方式。而在 Java 等比较新的编程语言中,大部分情况下,都用异常来处理函数出错的情况,极少会用到错误码。
在 C 语言中,错误码的返回方式有两种:一种是直接占用函数的返回值,函数正常执行的返回值方到出参中;另一种是将错误码定义为全局变量,在函数执行出错时,函数调用者通过这个全局变量来获取错误码。我举个例子进一步解释。
// 错误码的返回方式一:占用函数的返回值
int open(const char *pathname, int flags, mode_t mode, int *fd) {
if(/*文件不存在*/) {
return EEXISTS;
}
if(/*没有访问权限*/) {
return EACCESS;
}
if(/*打开文件成功*/) {
return SUCCESS;
}
// ...
}
// 使用举例
int result = open("c:\test.text", O_RDWR, S_IRWXU|S_IRWXG|S_IRWXO, &fd);
if(result == SUCCESS) {
// 取出fd使用
} else if(result == EEXISTS) {
// ...
} else if(result == EACCESS) {
// ...
}
// 错误码的返回方式二
int error; // 线程安全的全局变量
int open(const char *pathname, int flags, mode_t mode) {
if(/*文件不存在*/) {
error = EEXISTS;
return -1;
}
if(/*没有访问权限*/) {
error = EACCESS;
return -1;
}
// ...
}
// 使用举例
int hFile = open("c:\test.text", O_RDWR, S_IRWXU|S_IRWXG|S_IRWXO);
if(hFile == -1) {
if(error == EEXISTS) {
// ...
} else if(error == EACCESS) {
// ...
}
//...
}
如果你熟悉的编程语言中有异常这种语法机制,那就尽量不要使用错误码。异常相对于错误码,有诸多优势,比如可以携带详细的错误信息(exception 中可以有 message、stack trace 等信息)。关于异常,我们待会还会非常详细的讲解。
在多数编程语言中,我们用 NULL 来表示 “不存在” 这种语义。不过,很多人不建议函数返回 NULL 值,认为这是一种不好的设计思路,主要理由有以下两个。
// 使用函数 getUser()
User user = userService.getUser("18147452144");
if (user != null) { // 做NULL判断,否则可能会报 NPE
String email = user.getEmail();
if (email != null) { // 做NULL判断,否则可能会报 NPE
String escapedEmail = email.replaceAll("@", "#");
}
}
那我们是否可以用异常替代 NULL 值,在查找用户不存在的时候,让函数抛出 UserNotFoundException
异常呢?
个人觉得,尽管返回 NULL 值有诸多弊端,但是对于以 get、find、select、search、query 等单词开头的查找函数来说,数据不存在,并非是一种异常情况,这是一种正常行为。所以,返回代表不存在语义的 NULL值比返回更加合理。
其实上面将的理由,也不是特别有说服力。对于查找数据不存在的情况,函数到底是返回 NULL 还是异常,有一个比较重要的参考标准是,看项目中其他类似查找函数都是如何定义的,只要整个项目遵从两种中的任何一种都可以。你只需要在函数定义的地方解释清楚,让调用者清晰地知道数据不存在的时候会返回什么就可以了。
再补充一点,对于查找函数来说,除了返回数据对象之外,有的还会返回下标位置,比如 java 中的 indexOf()
函数,用来实现某个字符串查找另一个子串第一次出现的位置。函数的返回值类型为基本类型 int
。这个时候,我们就无法用 NULL 值来表示不存在的情况了。这种情况,我们有两种处理思路,一种是返回 NotFoundException
,一种是返回一个特殊值,比如 -1。不过,显然 -1 更加合理,理由也是同样的,也就是说,“没有查找到” 是一种正常行而非异常的行为。
刚刚降到,返回 NULL 值有各种弊端。应对这个问题有一个比较经典的策略,那就是应用空对象设计模式(Null Object Design Pattern)。关于这个设计模式,后面会讲到。不过,今天来讲比较简单、比较特殊的空对象,那就是空字符串和空集合。
当函数返回的数据是字符串类型或者集合类型的时候,我们可以用空字符串或空集合替代 NULL 值,来表示不存在的情况。这样,我们在使用函数的时候,就可以不用做 NULL 值判断。
// 使用空集合替代NULL
public class UserService {
private UserRepo userRepo; // 依赖注入
public List<User> getUsers(String telephonePrefix) {
// 没有查找到数据
return Collections.emptyList();
}
}
// 使用函数 getUser()
List<User> users = userService.getUsers("181");
for (User user : users) { // 这里不需要做NULL值判断
//...
}
// 使用空字符串替代NULL
public String retrieveUppercaseLetters(String text) {
// 如果text中没有大写字母,返回空字符串,而非NULL值
return "";
}
// retrieveUppercaseLetters() 使用举例
String uppercaseLetters = retrieveUppercaseLetters("abc");
int length = uppercaseLetters.length(); // 不需要做NULL值判断
System.out.println("Contains " + length + " upper case letters");
最常用的函数出错处理方式就是抛出异。异常可以携带更多的错误信息,比如函数调用栈信息。此外,异常可以将正常逻辑和异常逻辑的处理分离开,这样代码的可读性就会更好。
Java 除了运行时异常(Runtime Exception)外,还定义了另一种异常类型,编译时异常(Compile Exception)。
try-catch
,编译器在编译时并不会检查代码是否对运行时异常做了处理。try-catch
或者在函数定义中申明,否则编译就会报错。所以,运行时异常也叫做非受检异常(Unchecked Exception),编译时异常也叫作受检异常(Checked Exception)。
在 Java 中,定义了两种异常类型,那再异常出现时,我们应该选择抛出哪种异常类型呢?是受检异常还是非受检异常?
对于代码 bug(比如数组越界)以及不可恢复异常(比如数据库连接失败),即便捕获了也做不了太多事情,所以,我们倾向于使用非受检异常。对于可恢复异常、业务异常,比如提现金额大于余额的异常,我们更倾向于使用受检异常,明确告知调用者需要捕获处理。
举个例子来解释下,代码如下所示。当 Redis 的地址(参数 address)没有设置的时候,我们直接使用默认的地址(比如本地地址和默认端口);当 Redis 的地址格式不正确的时候,我们希望程序员能 fail-fast
,也就是说,把这种情况当成不可恢复的异常,直接抛出运行时异常,将程序终止掉。
public class RedisProcessor {
private String host;
private Integer port;
// address格式:"192.168.1.105: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]);
}
}
实际上,Java 支持的受检异常一直被人诟病,很多人主张所有的异常情况都该使用非受检异常。支持这种观点的理由主要有三个。
try-catch
处理掉为止。而新增非受检异常可以不改动调用链上的代码,我们可以灵活地选择在某个函数中集中处理,比如在 Spring 中的 AOP 切面中集中处理异常。其实,非受检异常也有弊端,它的优点其实也正是它的缺点。非受检异常非常灵活,怎么处理的主动权交给了程序员。前面我们讲过,过于灵活会带来不可控,非受检异常不需要显式地在函数中定义申明,那我们在使用函数的时候,就需要查看代码才能知道具体会抛出哪些异常。非受检异常不需要强制捕获处理,那程序员就有可能漏掉一些本应该捕获处理的异常。
对于应该用受检异常还是非受检异常,争议有很多,但并没有一个强有力的理由能够说明一个就一定比另一个更好。所以,我们只需根据团队的开发习惯,在同一个项目中,制定统一的异常处理规范即可。
一般有下面三种处理方法。
1.直接吞掉
public void func1() throws Exception1 { /*...*/ }
public void func2() {
// ...
try {
func1();
} catch (Exception1 e) {
log.warn("...", e); // 吞掉:try-catch打印日志
}
// ...
}
2.原封不动地 re-throw
public void func1() throws Exception1 { /*...*/ }
//原封不动的re-throw Exception1
public void func2() throws Exception1 {
// ...
func1();
// ...
}
3.包装成新的异常 re-throw
public void func1() throws Exception1 { /*...*/ }
public void func2() {
// ...
try {
func1();
} catch (Exception1 e) {
throw new Exception2(e); // wrap成新的Exception2,然后re-throw
}
// ...
}
当我们面对函数抛出异常时,应该选择上面的哪种处理方式呢? 我总结了下面三个参考原则:
func1()
抛出的异常是可以恢复,且 func2()
的调用方并不关心此异常,我们完全可以在 func2()
内将 func1()
抛出的异常吞掉。func1()
抛出的异常对 func2()
调用方来说,是可以理解的、关心的,并且在业务概念上有一定的相关性,我们可以直接选择将 func1()
抛出的异常 re-throw。func1()
抛出的异常太底层,对 func2()
的调用方来说,缺乏背景去理解、且业务概念上无关,我们可以将它重新包装成调用方可以理解的异常,然后 re-throw。总之,是否网上继续抛出,要看上层代码是否关心这个异常。关系就将它抛出,否则就直接吞掉。是否需要包装成新的异常抛出,看上层代码是否理解这个异常、是否业务相关。如果能理解、业务相关就可以直接抛出,否则就封装成新的异常抛出。
1.返回错误码
C语言没有异常这样的语法机制,返回错误码便是最常用的出错处理方式。而 Java 等比较新的编程语言中,大部分情况下,都抛出异常来处理程序出错的情况,极少会用到错误码。
2.返回 NULL 值
大多数编程语言中,都用 NULL 值表示 “不存在” 这种语义。对于查找函数来说,数据不存在并非一种异常情况,是一种正常行为,所以返回表示不存在语义的 NULL 值比返回异常更加合理。
3.返回空对象
返回 NULL 值有各种弊端,对此有一个比较经典的应对策略,那就是应用空对象设计模式。当函数返回的数据是字符串或集合类型时,我们可以使用空对象或空集合替代 NULL 值,来表示不存在的情况。这样,我们在使用的时候,就可以不用做 NULL 值判断。
4.抛出异常对象
尽管前面讲了很多函数出错的返回数据类型,但是,最常用的函数出错处理方式是抛出异常。异常有两种类型:受检异常和非受检异常。
对于应用受检异常还是非受检异常,争论有很多,但也并没有一个非常强有力的理由,说明一个就比另一个更好。所以,我们只需要根据团队的开发习惯,在同一个项目中,制定统一的异常处理规范即可。
对于函数抛出的异常,我们有三种处理方式:直接吞掉、直接向上抛出、包装成新的异常抛出。
我们在进行软件设计的时候,除了要保证正常情况下的逻辑运行正确之外,还需要编写大量额外的代码,来处理可能出现的异常情况,以保证代码在任何情况下,都在我们的掌控之内,不会出现非预期的运行结果。程序的 bug 往往出现在一些边界条件和异常情况下,所以说,异常处理得好坏直接影响了代码的健壮性。全面、合理地处理各种异常能有效地减少代码 bug,也是保证代码质量的一个重要手段。
对于 generate()
函数,如果本机名获取失败,函数安徽什么?这样的返回值是否合理?
public String generate() {
String substrOfHostName = getLastFieldOfHostName();
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format("%s-%d-%s",
substrOfHostName, currentTimeMillis, randomString);
return id;
}
ID 由三部分构成:本机名、时间戳和随机数。时间戳和随机数的生成函数不会出错,主机名有可能获取失败。在目前的代码中,如果主机名获取失败,substrOfHostName 为 NULL,那 generate()
会返回类似 “null-1710480822005-33Ab3uK6” 这样的数据。如果主机名获取失败, substrOfHostName 为空字符串,那 generate()
会返回类似 “-1710480822005-33Ab3uK6” 这样的数据。
在异常情况下,返回上面两种特殊的 ID 数据格式,这样的做法是否合理呢? 这其实很难讲,我们要看具体的业务是怎么设计的。不过,个人更倾向于将异常告知调用者。所以,这里最好是抛出受检异常,而非特殊值。
按照这个思路,我们对 generate()
函数进行重构。重构之后的代码如下所示:
public String generate() throws IdGenerationFailureException {
String substrOfHostName = getLastFieldOfHostName();
if (substrOfHostName == null || substrOfHostName.isEmpty()) {
throw new IdGenerationFailureException();
}
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format("%s-%d-%s",
substrOfHostName, currentTimeMillis, randomString);
return id;
}
对 getLastFieldOfHostName()
函数,是否应该将 UnknownHostException
异常在函数内部吞掉,还是应该将异常继续网上抛出?如果网上抛出的话,是直接把 UnknownHostException
异常原封不动的抛出,还是封装成新的异常抛出?
private String getLastFieldOfHostName() {
String substrOfHostName = null;
try {
String hostName = InetAddress.getLocalHost().getHostName();
substrOfHostName = getLastSubstrSplitByDot(hostName);
} catch (UnknownHostException e) {
logger.error("failed to get the host name.", e);
}
return substrOfHostName;
}
现在的处理方式是当主机名获取失败的时候,getLastFieldOfHostName()
函数返回 NULL 值。我们前面讲过,是返回 NULL 值还是异常对象,要看获取不到数据是正常行为还是异常行为。获取主机名失败会影响后续逻辑处理,并不是我们期望的,所以它是一种异常行为。这里最好是抛出异常,而非返回 NULL 值。
至于是将 UnknownHostException
异常抛出,还是重新封装成新的异常抛出,要看函数跟异常是否有业务相关性。getLastFieldOfHostName()
函数用来获取主机名的最后一个字段, UnknownHostException
异常表示主机名获取失败,两者算是业务相关,所以可以直接将 UnknownHostException
抛出,不需要重新包裹新的异常。
按照上面的思路,我们对代码进行重构。
private String getLastFieldOfHostName() throws UnknownHostException {
String substrOfHostName = null;
String hostName = InetAddress.getLocalHost().getHostName();
substrOfHostName = getLastSubstrSplitByDot(hostName);
return substrOfHostName;
}
getLastFieldOfHostName()
函数修改之后, generate()
函数也要做相应的修改。我们需要在 generate()
函数中捕获 getLastFieldOfHostName()
函数抛出的 UnknownHostException
异常。当捕获到异常之后,应该怎么处理呢?
按照之前的分析, ID 获取失败的时候,要明确的告知调用者。所以,我们不能再 generate()
函数中,将 UnknownHostException
异常吞掉。那是应该原封不动的抛出,还是封装成新的异常抛出呢?
需要选择后者。在 generate()
函数中,我们需要捕获 UnknownHostException
异常,并包裹成新的异常 IdGenerationFailureException
网上抛出。这么的原因有三个:
generate()
函数时,只需要知道它生成的是随机唯一 ID,并不关心 ID 是如何生成的。即这是依赖抽象而非实现编程。如果 generate()
函数直接抛出 UnknownHostException
异常,实际上是暴露了实现细节。UnknownHostException
异常这个比较底层的异常,暴露给更上层的代码。而且,调用者拿到这个异常时,并不能理解这个异常到底代表了什么,也不知道该如何处理。UnknownHostException
异常跟 generate()
函数,在业务概念上没有相关性。按照上面的设计思路,我们对 generate()
函数再次进行重构。
public String generate() throws IdGenerationFailureException {
String substrOfHostName = null;
try {
substrOfHostName = getLastFieldOfHostName();
} catch (UnknownHostException e) {
throw new IdGenerationFailureException();
}
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format("%s-%d-%s",
substrOfHostName, currentTimeMillis, randomString);
return id;
}
对于 getLastSubstrSplittedByDot()
函数,如果 hostName 为 NULL 或者空字符串,这个函数应该返回什么?
@VisibleForTesting
protected String getLastSubstrSplitByDot(String hostName) {
String[] tokens = hostName.split("\\.");
String substrOfHostName = tokens[tokens.length - 1];
return substrOfHostName;
}
理论上讲,参数传递的正确性应该由程序员保证,我们无需做 NULL 值或空字符串的判断。但是,话说回来,谁也保证不了程序员就一定不会传递 NULL 值或者空字符串。我们到底该不该做 NULL 值或空字符串的判断呢?
如果函数是 private 私有的,只在类内部被调用,完全在你自己的掌控之下,自己保证在调用这个 private 函数的时候,不要传递 NULL 值或者空字符串就可以了。所以,可以不在 private 函数中做 NULL 值或者空字符串的判断。如果是 public 的,你无法掌控会被谁调用以及如何调用,为了尽可能提高代码的健壮性,我们最好是在 public 函数中做 NULL 值或者空字符串的判断。
getLastSubstrSplittedByDot()
是 protected 的,既不是 private 函数,也不是 public 函数,那要不要做 NULL 值或者空字符串的判断逻辑。虽然加上有些冗余,但多加些校验总归是不错的。
按照这个设计思路,我们对 getLastSubstrSplittedByDot()
函数进行重构。
@VisibleForTesting
protected String getLastSubstrSplitByDot(String hostName) {
if (hostName == null || hostName.isEmpty()) {
throw new 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 IllegalArgumentException("..."); //此处做判断
}
substrOfHostName = getLastSubstrSplitByDot(hostName);
return substrOfHostName;
}
对于 generateRandomAlphameric()
函数,如果 length <= 0,这个函数应该返回什么?
@VisibleForTesting
protected String generateRandomAlphameric(int length) {
char[] randomChars = new char[length];
int count = 0;
Random random = new Random();
while (count < length) {
int randomAscii = random.nextInt(122);
boolean isDigit = randomAscii >= 48 && randomAscii <= 57;
boolean isUpperCase = randomAscii >= 65 && randomAscii <= 90;
boolean isLowerCase = randomAscii >= 97 && randomAscii <= 122;
if (isDigit || isUpperCase || isLowerCase) {
randomChars[count++] = (char) randomAscii;
}
}
return new String(randomChars);
}
我们先来看一下 length < 0
的情况。生成一个长度为负值的随机字符串是不合常规逻辑的,是一种异常行为。所以,当出传入的参数 length < 0 的时候,我们抛出 IllegalArgumentException
异常。
我们再来看下 length = 0
的情况。length = 0
是否是异常行为呢?这就看你自己怎么定义了。我们既可以把它定义为一种异常行为,抛出 IllegalArgumentException
异常,也可以把它定义为一种正常行为,让函数入参 length = 0
的情况下,直接返回字符串。不管选择哪种处理方式,最关键的一点是,要在函数注释中,明确告知 length = 0
的情况下,会返回什么样的数据。
@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 randomAscii = random.nextInt(122);
boolean isDigit = randomAscii >= 48 && randomAscii <= 57;
boolean isUpperCase = randomAscii >= 65 && randomAscii <= 90;
boolean isLowerCase = randomAscii >= 97 && randomAscii <= 122;
if (isDigit || isUpperCase || isLowerCase) {
randomChars[count++] = (char) randomAscii;
}
}
return new String(randomChars);
}
public class RandomIdGenerator implements LogTraceIdIdGenerator {
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();
}
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 IllegalArgumentException("..."); //此处做判断
}
substrOfHostName = getLastSubstrSplitByDot(hostName);
return substrOfHostName;
}
@VisibleForTesting
protected String getLastSubstrSplitByDot(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 randomAscii = random.nextInt(122);
boolean isDigit = randomAscii >= 48 && randomAscii <= 57;
boolean isUpperCase = randomAscii >= 65 && randomAscii <= 90;
boolean isLowerCase = randomAscii >= 97 && randomAscii <= 122;
if (isDigit || isUpperCase || isLowerCase) {
randomChars[count++] = (char) randomAscii;
}
}
return new String(randomChars);
}
}
这里总结了三点经验:
RandomIdGenerator
代码,你真的能学到它的设计精髓吗?IdGenerator
代码和最终的 RandomIdGenerator
,它们一个是能用,一个是好用,天壤之别。作为程序员,我们对代码要有追求哈。