Java编程常见"坑"汇总(下)

本文列举了Java编程中常见的误区和陷阱,包括List误用、数组不当使用、方法过度限制等,提供了正确做法以避免这些问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

                                         Java编程常见"坑"汇总(下)

30.对List的误用

建议下列场景用Array来替代List:

1.list长度固定,比如一周中的每一天

2.对list频繁的遍历,比如超过1w次

3.需要对数字进行包装(主要JDK没有提供基本类型的List)

比如下面的代码。

错误的写法:

List<Integer> codes = new ArrayList<Integer>();  
codes.add(Integer.valueOf(10));  
codes.add(Integer.valueOf(20));  
codes.add(Integer.valueOf(30));  
codes.add(Integer.valueOf(40));


正确的写法:

int[] codes = { 10203040 };


错误的写法:

// horribly slow and a memory waster if l has a few thousand elements (try it yourself!)  
List<Mergeable> l = ...;  
for (int i=0; i < l.size()-1; i++) {  
   Mergeable one = l.get(i);  
   Iterator<Mergeable> j = l.iterator(i+1); // memory allocation!  
   while (j.hasNext()) {  
       Mergeable other = l.next();  
       if (one.canMergeWith(other)) {  
           one.merge(other);  
           other.remove();  
       }  
   }  
}


正确的写法:

// quite fast and no memory allocation  
Mergeable[] l = ...;  
for (int i=0; i < l.length-1; i++) {  
   Mergeable one = l[i];  
   for (int j=i+1; j < l.length; j++) {  
       Mergeable other = l[j];  
       if (one.canMergeWith(other)) {  
           one.merge(other);  
           l[j] = null;  
       }  
   }  
}

实际上Sun也意识到这一点, 因此在JDK中, Collections.sort()就是将一个List拷贝到一个数组中然后调用Arrays.sort方法来执行排序。

31.用数组来描述一个结构

错误用法:

/**   
* @returns [1]: Location, [2]: Customer, [3]: Incident   
*/
   
Object[] getDetails(int id{...

这里用数组+文档的方式来描述一个方法的返回值. 虽然很简单, 但是很容易误用, 正确的做法应该是定义个类。

正确的写法:

Details getDetails(int id) {...}   
private class Details {   
public Location location;   
public Customer customer;   
public Incident incident;   
}


32.对方法过度限制

错误用法:

public void notify(Person p) {   
...   
sendMail(p.getName(), p.getFirstName(), p.getEmail());   
...   
}   
class PhoneBook {   
String lookup(String employeeId) {   
Employee emp = ...   
return emp.getPhone();   
}   
}

第一个例子是对方法参数做了过多的限制, 第二个例子对方法的返回值做了太多的限制。

正确的写法:

public void notify(Person p) {   
...   
sendMail(p);   
...   
}   
class EmployeeDirectory {   
Employee lookup(String employeeId) {   
Employee emp = ...   
return emp;   
}   
}


33.对POJO的setter方法画蛇添足

错误的写法:

private String name;   
public void setName(String name) {   
this.name = name.trim();   
}   
public void String getName() {   
return this.name;   
}

有时候我们很讨厌字符串首尾出现空格, 所以在setter方法中进行了trim处理, 但是这样做的结果带来的副作用会使getter方法的返回值和setter方法不一致, 如果只是将JavaBean当做一个数据容器, 那么最好不要包含任何业务逻辑. 而将业务逻辑放到专门的业务层或者控制层中处理。

正确的做法:

person.setName(textInput.getText().trim());


34.SimpleDateFormat非线程安全误用

错误的写法:

public class Constants {   
public static final SimpleDateFormat date = new SimpleDateFormat("dd.MM.yyyy");   
}

SimpleDateFormat不是线程安全的. 在多线程并行处理的情况下, 会得到非预期的值. 这个错误非常普遍! 如果真要在多线程环境下公用同一个SimpleDateFormat, 那么做好做好同步(cache flush, lock contention), 但是这样会搞得更复杂, 还不如直接new一个实在。

35.使用全局参数配置常量类/接口

public interface Constants {   
String version = "1.0";   
String dateFormat = "dd.MM.yyyy";   
String configFile = ".apprc";   
int maxNameLength = 32;   
String someQuery = "SELECT * FROM ...";   
}

很多应用都会定义这样一个全局常量类或接口, 但是为什么这种做法不推荐? 因为这些常量之间基本没有任何关联, 只是因为公用才定义在一起. 但是如果其他组件需要使用这些全局变量, 则必须对该常量类产生依赖, 特别是存在server和远程client调用的场景。

比较好的做法是将这些常量定义在组件内部. 或者局限在一个类库内部。

36.忽略造型溢出(cast overflow)

错误的写法:

public int getFileSize(File f) {   
long l = f.length();   
return (int) l;   
}

这个方法的本意是不支持传递超过2GB的文件. 最好的做法是对长度进行检查, 溢出时抛出异常。

正确的写法:

public int getFileSize(File f) {   
long l = f.length();   
if (l > Integer.MAX_VALUE) throw new IllegalStateException("int overflow");   
return (int) l;   
}

另一个溢出bug是cast的对象不对, 比如下面第一个println. 正确的应该是下面的那个。

long a = System.currentTimeMillis();   
long b = a + 100;   
System.out.println((int) b-a);   
System.out.println((int) (b-a));


37.对float和double使用==操作

错误的写法:

for (float f = 10f; f!=0; f-=0.1) {   
System.out.println(f);   
}

上面的浮点数递减只会无限接近0而不会等于0, 这样会导致上面的for进入死循环. 通常绝不要对float和double使用==操作. 而采用大于和小于操作. 如果java编译器能针对这种情况给出警告. 或者在java语言规范中不支持浮点数类型的==操作就最好了。

正确的写法:

for (float f = 10f; f>0; f-=0.1) {   
System.out.println(f);   
}


38.用浮点数来保存money

错误的写法:

float total = 0.0f;   
for (OrderLine line : lines) {   
total += line.price * line.count;   
}   
double a = 1.14 * 75// 85.5 将表示为 85.4999...   
System.out.println(Math.round(a)); // 输出值为85   
BigDecimal d = new BigDecimal(1.14); //造成精度丢失

这个也是一个老生常谈的错误. 比如计算100笔订单, 每笔0.3元, 最终的计算结果是29.9999971. 如果将float类型改为double类型, 得到的结果将是30.000001192092896. 出现这种情况的原因是, 人类和计算的计数方式不同. 人类采用的是十进制, 而计算机是二进制.二进制对于计算机来说非常好使, 但是对于涉及到精确计算的场景就会带来误差. 比如银行金融中的应用。

因此绝不要用浮点类型来保存money数据. 采用浮点数得到的计算结果是不精确的. 即使与int类型做乘法运算也会产生一个不精确的结果.那是因为在用二进制存储一个浮点数时已经出现了精度丢失. 最好的做法就是用一个string或者固定点数来表示. 为了精确, 这种表示方式需要指定相应的精度值.

BigDecimal就满足了上面所说的需求. 如果在计算的过程中精度的丢失超出了给定的范围, 将抛出runtime exception.

正确的写法:

BigDecimal total = BigDecimal.ZERO;   
for (OrderLine line : lines) {   
BigDecimal price = new BigDecimal(line.price);   
BigDecimal count = new BigDecimal(line.count);   
total = total.add(price.multiply(count)); // BigDecimal is immutable!   
}   
total = total.setScale(2, RoundingMode.HALF_UP);   
BigDecimal a = (new BigDecimal("1.14")).multiply(new BigDecimal(75)); // 85.5 exact   
a = a.setScale(0, RoundingMode.HALF_UP); // 86   
System.out.println(a); // correct output: 86   
BigDecimal a = new BigDecimal("1.14");


39.不使用finally块释放资源

错误的写法:

public void save(File f) throws IOException {   
OutputStream out = new BufferedOutputStream(new FileOutputStream(f));   
out.write(...);   
out.close();   
}   
public void load(File f) throws IOException {   
InputStream in = new BufferedInputStream(new FileInputStream(f));   
in.read(...);   
in.close();   
}

上面的代码打开一个文件输出流, 操作系统为其分配一个文件句柄, 但是文件句柄是一种非常稀缺的资源, 必须通过调用相应的close方法来被正确的释放回收. 而为了保证在异常情况下资源依然能被正确回收, 必须将其放在finally block中. 上面的代码中使用了BufferedInputStream将file stream包装成了一个buffer stream, 这样将导致在调用close方法时才会将buffer stream写入磁盘. 如果在close的时候失败, 将导致写入数据不完全. 而对于FileInputStream在finally block的close操作这里将直接忽略。

如果BufferedOutputStream.close()方法执行顺利则万事大吉, 如果失败这里有一个潜在的bug(http://bugs.sun.com/view_bug.do?bug_id=6335274): 在close方法内部调用flush操作的时候, 如果出现异常, 将直接忽略. 因此为了尽量减少数据丢失, 在执行close之前显式的调用flush操作。

下面的代码有一个小小的瑕疵: 如果分配file stream成功, 但是分配buffer stream失败(OOM这种场景), 将导致文件句柄未被正确释放. 不过这种情况一般不用担心, 因为JVM的gc将帮助我们做清理。

// code for your cookbook   
public void save() throws IOException {   
File f = ...   
OutputStream out = new BufferedOutputStream(new FileOutputStream(f));   
try {   
out.write(...);   
out.flush(); // don't lose exception by implicit flush on close   
finally {   
out.close();   
}   
}   
public void load(File f) throws IOException {   
InputStream in = new BufferedInputStream(new FileInputStream(f));   
try {   
in.read(...);   
finally {   
try { in.close(); } catch (IOException e) { }   
}   
}

数据库访问也涉及到类似的情况:

Car getCar(DataSource ds, String plate) throws SQLException {   
Car car = null;   
Connection c = null;   
PreparedStatement s = null;   
ResultSet rs = null;   
try {   
c = ds.getConnection();   
s = c.prepareStatement("select make, color from cars where plate=?");   
s.setString(1, plate);   
rs = s.executeQuery();   
if (rs.next()) {   
car = new Car();   
car.make = rs.getString(1);   
car.color = rs.getString(2);   
}   
finally {   
if (rs != nulltry { rs.close(); } catch (SQLException e) { }   
if (s != nulltry { s.close(); } catch (SQLException e) { }   
if (c != nulltry { c.close(); } catch (SQLException e) { }   
}   
return car;   
}


40.finalize方法误用

错误的写法:

public class FileBackedCache {   
private File backingStore;   

...   

protected void finalize() throws IOException {   
if (backingStore != null) {   
backingStore.close();   
backingStore = null;   
}   
}   
}

这个问题Effective Java这本书有详细的说明. 主要是finalize方法依赖于GC的调用, 其调用时机可能是立马也可能是几天以后, 所以是不可预知的. 而JDK的API文档中对这一点有误导:建议在该方法中来释放I/O资源。

正确的做法是定义一个close方法, 然后由外部的容器来负责调用释放资源。

public class FileBackedCache {   
private File backingStore;   

...   

public void close() throws IOException {   
if (backingStore != null) {   
backingStore.close();   
backingStore = null;   
}   
}   
}

在JDK 1.7 (Java 7)中已经引入了一个AutoClosable接口. 当变量(不是对象)超出了try-catch的资源使用范围, 将自动调用close方法。

try (Writer w = new FileWriter(f)) { // implements Closable   
w.write("abc");   
// w goes out of scope here: w.close() is called automatically in ANY case   
catch (IOException e) {   
throw new RuntimeException(e.getMessage(), e);   
}

41.Thread.interrupted方法误用

错误的写法:

try {   
Thread.sleep(1000);   
catch (InterruptedException e) {   
// ok   
}   
or   
while (true) {   
if (Thread.interrupted()) break;   
}

这里主要是interrupted静态方法除了返回当前线程的中断状态, 还会将当前线程状态复位。

正确的写法:

try {   
Thread.sleep(1000);   
catch (InterruptedException e) {   
Thread.currentThread().interrupt();   
}   
or   
while (true) {   
if (Thread.currentThread().isInterrupted()) break;   
}


42.在静态变量初始化时创建线程

错误的写法:

class Cache {   
private static final Timer evictor = new Timer();   
}

Timer构造器内部会new一个thread, 而该thread会从它的父线程(即当前线程)中继承各种属性。比如context classloader, ThreadLocal以及其他的安全属性(访问权限)。 而加载当前类的线程可能是不确定的,比如一个线程池中随机的一个线程。如果你需要控制线程的属性,最好的做法就是将其初始化操作放在一个静态方法中,这样初始化将由它的调用者来决定。

正确的做法:

class Cache {   
private static Timer evictor;   
public static setupEvictor() {   
evictor = new Timer();   
}   
}


43.已取消的定时器任务依然持有状态

错误的写法:

final MyClass callback = this;   
TimerTask task = new TimerTask() {   
public void run() {   
callback.timeout();   
}   
};   
timer.schedule(task, 300000L);   
try {   
doSomething();   
finally {   
task.cancel();   
}

上面的task内部包含一个对外部类实例的应用, 这将导致该引用可能不会被GC立即回收. 因为Timer将保留TimerTask在指定的时间之后才被释放. 因此task对应的外部类实例将在5分钟后被回收。

正确的写法:

TimerTask task = new Job(this);   
timer.schedule(task, 300000L);   
try {   
doSomething();   
finally {   
task.cancel();   
}   

static class Job extends TimerTask {   
private MyClass callback;   
public Job(MyClass callback) {   
this.callback = callback;   
}   
public boolean cancel() {   
callback = null;   
return super.cancel();   
}   
public void run() {   
if (callback == nullreturn;   
callback.timeout();   
}   
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值