Java编程中的一些常见问题汇总
本文列举了我在周围同事的Java代码中看到的一些比较典型的错误。显然,静态代码分析(我们团队用的是qulice)不可能发现所有的问题,这也是为什么我要在这里列出它们的原因。
如果你觉得少了什么,请不吝赐教,我会很乐意把它们加上。
下面列出的所有这些错误基本都与面向对象编程有关,尤其是Java的OOP。
类名
读下这篇短文“什么是对象”。类应该是真实生活中的一个抽象实体,而不是什么“validators”,“controller”, “managers”这些东西。如果你的类名以”er”结尾的话——那它就是个糟糕的设计。
当然了,工具类也是反模式,比如说Apache的StringUtils, FileUtils, 以及IOUtils。上面这些都是糟糕设计的代表。延伸阅读:OOP中工具类的替代方案。
当然,不要使用前缀或者后缀来区分类和接口。比方说,这些名字就是错误的:IRecord, IfaceEmployee, 或者RecordInterface。通常来说,接口名应该是真实生活中的实体的名字,类名应该可以说明它的实现细节。如果这个实现没有什么特别可说明的,可以把它叫作Default, Simple或者类似的什么。比如说:
class SimpleUser implements User {};
class DefaultRecord implements Record {};
class Suffixed implements Name {};
class Validated implements Content {};
方法名
方法可以返回值也可以返回void。如果方法返回值的话,它的名字应该能说明它返回了什么,比如说(永远也不要使用get前缀):
boolean isValid(String name);
String content();
int ageOf(File file);
如果它返回void,那么它的名字应该要说明它做了什么。比如:
void save(File file);
void process(Work work);
void append(File file, String line);
刚才提到的这些规则只有一个例外——JUnit的test方法不算。下面将会说到这个。
test方法的名字
在JUnit的测试用例中,方法名应该是没有空格的英文语句。用一个例子来说明会更清楚一些:
/**
* HttpRequest can return its content in Unicode.
* @throws Exception If test fails
*/
public void returnsItsContentInUnicode() throws Exception {
}
你的JavaDoc里的第一句话的开头应该是你要测试的那个类的名字,然后是一个can。因此,你的第一句话应该是类似于“somebody can do something”。
方法名也是一样的,只是没有主题而已。如果我在方法名中间加一个主题的话,我就能得到一个完整的句子,正如上面那个例子中那样:“HttpRequest returns its content in unicode”。
请注意test方法的名字是不以can开头的。只有JavaDoc里的的注释会以can开头。除此之外,方法名不应该以动词开头。
实践中最好将测试方法声明为抛出Exception的。
变量名
避免组合的变量名,比如说timeOfDay, firstItem,或者httpRequest。类变量及方法内的变量都是如此。变量名应该足够长,避免在它的可见作用域内产生歧义,但是如果可以的话也不要太长。名字应该是单数或复数形式的名词,或者是一个适当的缩写。比如:
List<String> names;
void sendThroughProxy(File file, Protocol proto);
private File content;
public HttpRequest request;
有的时候,如果构造方法要将入参保存到一个新初始化的对象中的时候,它的参数和类属性的名字可能会冲突。这种情况,我建议是去掉元音,使用缩写。
示例:
public class Message {
private String recipient;
public Message(String rcpt) {
this.recipient = rcpt;
}
}
很多时候,看一下变量的类名就知道变量该取什么名字了。就用它的小写形式就好了,像这样就很靠谱:
File file;
User user;
Branch branch;
然而,基础类型的话,永远不要这么做,比如Integer number或者String string。
如果存在多个不同性质的变量的话,可以考虑下使用形容词。比如:
String contact(String left, String right);
构造方法
不考虑异常的话,应该只有一个构造方法用来将数据存储到对象变量中。其它构造方法则使用不同的参数来调用这个构造方法。比如说:
public class Server {
private String address;
public Server(String uri) {
this.address = uri;
}
public Server(URI uri) {
this(uri.toString());
}
}
一次性变量
无论如何都应该避免使用一次性变量。这里我所说的“一次性“指的是只使用一次的变量。比如下面这个:
String name = "data.txt";
return new File(name);
上述的变量只会使用一次,因此这段代码可以重构成这样:
return new File("data.txt");
有的时候,比较罕见的情况中——主要是为了格式更好看些——可能会用到一次性变量。然而,还是应当尽量避免这种情况。
异常
毋庸赘言,永远不要自己吞掉异常,而是应该当它尽量往上传递。私有方法应该始终把受检查异常往外面抛。
不要使用异常来进行流程控制。比方说下面这段代码就是错误的:
int size;
try {
size = this.fileSize();
} catch (IOException ex) {
size = 0;
}
那如果IOException提示“磁盘已满”的话该怎么办?你还会认为这个文件大小为0,然后继续往下处理?
缩进
关于缩进,主要的规则就是左括号要么在该行的末尾,要么就在同一行上闭合(对于右括号来说则相反)。比如说,下面这个就不正确,因为第一个左括号没有在同一行上闭合,而它后面还有别的字符。第二个括号也有问题,因为它前面有字符,但对应的开括号又没在同一行上:
final File file = new File(directory,
"file.txt");
正确的缩进应该是这样的:
StringUtils.join(
Arrays.asList(
"first line",
"second line",
StringUtils.join(
Arrays.asList("a", "b")
)
),
"separator"
);
关于缩进,第二条重要的规则就是同时一行中应该尽量多写一些——上限是80个字符。上面的那个例子并不满足这点,它还可以收缩一下:
StringUtils.join(
Arrays.asList(
"first line", "second line",
StringUtils.join(Arrays.asList("a", "b"))
),
"separator"
);
多余的常量
当你希望在类的方法中共享信息的时候,应当使用类常量,这些信息应该是你这个类所特有的。不要把常量当作字符串或数值字面量的替代品来使用——这是非常糟糕的实践方式,它会对代码造成污染。常量(正如OOP中的任何对象一样)应当在真实世界中有它自己的含义。看下这些常量在真实生活中的意思是什么:
class Document {
private static final String D_LETTER = "D"; // bad practice
private static final String EXTENSION = ".doc"; // good practice
}
另一个常见的错误就是在单元测试中使用常量来避免测试方法中出现冗余的字符串或者数值的字面量。不要这么做!每个测试方法都应该有自己专属的输入值。
在每个新的测试方法中使用新的文本或者数值。它们是相互独立的。那么为什么它们还要共享同样的输入常量呢?
测试数据耦合
下面是测试方法中数据耦合的一个例子:
User user = new User("Jeff");
// maybe some other code here
MatcherAssert.assertThat(user.name(), Matchers.equalTo("Jeff"));
最后一行中,”Jeff”和第一行中的同一个字符串字面值发生了耦合。如果过了几个月,有人想把第三行这个值换一下,那么他还得花时间找出同一个方法中哪里也使用了这个”Jeff”。
为了避免这种情况,你最好还是引入一个变量。