目錄
上面的程式碼沒有錯,但是不夠好。不夠簡潔,==原義是比較兩個
錯誤的寫法:
String xml = FileUtils.readTextFile("my.xml");
登入後複製
" >
String xml = FileUtils.readTextFile("my.xml");
登入後複製
檔案系統
byte[] pdf = toPdf(file);
登入後複製
" >
byte[] pdf = toPdf(file);
登入後複製
捕获所有的异常
忽略所有异常
重复包装RuntimeException
不正确的传播异常
用日志记录异常
异常处理不彻底" >异常处理不彻底
捕获不可能出现的异常
transient的误用
不必要的初始化
最好用静态final定义Log变量
选择错误的类加载器
反射使用不当
不必要的同步
错误的选择List类型
HashMap size陷阱
对Hashtable, HashMap 和 HashSet了解不够
对List的误用
用数组来描述一个结构
对方法过度限制
对POJO的setter方法画蛇添足
日历对象(Calendar)误用
TimeZone的误用
时区(Time Zone)调整的误用
Calendar.getInstance()的误用
Date.setTime()的误用
SimpleDateFormat非线程安全误用
使用全局参数配置常量类/接口
overflow)" >忽略造型溢出(cast overflow)
对float和double使用==操作
用浮点数来保存money
不使用finally块释放资源
finalize方法误用
Thread.interrupted方法误用
在静态变量初始化时创建线程
定时器任务依然持有状态" >已取消的定时器任务依然持有状态
首頁 Java java教程 詳細介紹Java程式設計常見問題總結

詳細介紹Java程式設計常見問題總結

Mar 28, 2017 pm 03:36 PM
java

每天都在寫Java程序,其實裡面有一些細節大家可能沒怎麼注意,這不,有人總結了一個我們程式設計中常見的問題。

字串連接誤用

錯誤的寫法:

String s = "";  
for (Person p : persons) {  
    s += ", " + p.getName();  
}  
s = s.substring(2); //remove first comma
登入後複製

正確的寫法:

StringBuilder sb = new StringBuilder(persons.size() * 16); // well estimated buffer
for (Person p : persons) {
    if (sb.length() > 0) sb.append(", ");
    sb.append(p.getName);
}
登入後複製

錯誤的使用StringBuffer

錯誤的寫法:

StringBuffer sb = new StringBuffer();  
sb.append("Name: ");  
sb.append(name + '\n');  
sb.append("!");  
...  
String s = sb.toString();
登入後複製

問題在第三行,append char比String效能好,另外就是初始化StringBuffer沒有指定size,導致中間append時可能重新調整內部

陣列大小。最好用StringBuilder取代StringBuffer,除非有執行緒安全的要求。 ##

或這樣寫:

String s = "Name: " + name + "\n!";
登入後複製

測試字串相等性

錯誤的寫法:

if (name.compareTo("John") == 0) ...  
if (name == "John") ...  
if (name.equals("John")) ...  
if ("".equals(name)) ...
登入後複製

上面的程式碼沒有錯,但是不夠好。不夠簡潔,==原義是比較兩個

物件

是否一樣。 ##數字轉換成字串

錯誤的寫法:

正確的寫法:

"" + set.size()  
new Integer(set.size()).toString()
登入後複製

利用不可變物件(Immutable)

錯誤的寫法:

String.valueOf(set.size())
登入後複製

正確的寫法:

zero = new Integer(0);  
return Boolean.valueOf("true");
登入後複製

請使用XML解析器

錯誤的寫法:

zero = Integer.valueOf(0);  
return Boolean.TRUE;
登入後複製

正確的寫法:

int start = xml.indexOf("<name>") + "<name>".length();  
int end = xml.indexOf("</name>");  
String name = xml.substring(start, end);
登入後複製

請使用JDom組裝XML

錯誤的寫法:

SAXBuilder builder = new SAXBuilder(false);  
Document doc = doc = builder.build(new StringReader(xml));  
String name = doc.getRootElement().getChild("name").getText();
登入後複製

正確的寫法:

String name = ...  
String attribute = ...  
String xml = "<root>" 
            +"<name att=\""+ attribute +"\">"+ name +"</name>" 
            +"</root>";
登入後複製

XML編碼陷阱

錯誤的寫法:

Element root = new Element("root");  
root.setAttribute("att", attribute);  
root.setText(name);  
Document doc = new Documet();  
doc.setRootElement(root);  
XmlOutputter out = new XmlOutputter(Format.getPrettyFormat());  
String xml = out.outputString(root);
登入後複製

因為xml的編碼在檔案中指定的,而在讀取檔案的時候必須指定編碼。的做法用InputStream來邊讀取邊處理。

這樣的程式碼主要不具有跨平台可移植性。因為不同的平台可能使用的是不同的預設字元編碼。

正確的寫法:

String xml = FileUtils.readTextFile("my.xml");
登入後複製

未對資料流進行快取

錯誤的寫法:

Reader r = new FileReader(file);  
Writer w = new FileWriter(file);  
Reader r = new InputStreamReader(inputStream);  
Writer w = new OutputStreamWriter(outputStream);  
String s = new String(byteArray); // byteArray is a byte[]  
byte[] a = string.getBytes();
登入後複製

上面的程式碼是一個byte一個byte的讀取,導致頻繁的本地JNI

檔案系統

訪問,非常低效,因為呼叫本地方法是非常耗時的。最好用BufferedInputStream包裝一下。曾經做過一個測試,從/dev/zero下讀取1MB,大概花了1s,而用BufferedInputStream包裝之後只需要60ms,性能提高了94%! 這個也適用於output stream操作以及socket操作。

正確的寫法:

Reader r = new InputStreamReader(new FileInputStream(file), "ISO-8859-1");  
Writer w = new OutputStreamWriter(new FileOutputStream(file), "ISO-8859-1");  
Reader r = new InputStreamReader(inputStream, "UTF-8");  
Writer w = new OutputStreamWriter(outputStream, "UTF-8");  
String s = new String(byteArray, "ASCII");  
byte[] a = string.getBytes("ASCII");
登入後複製

無限使用heap記憶體

錯誤的寫法:

InputStream in = new FileInputStream(file);   
int b;   
while ((b = in.read()) != -1) {   
...   
}
登入後複製

這裡有一個前提,就是檔案大小不能講JVM的heap撐爆。否則就等著OOM吧,尤其是在高並發的伺服器端程式碼。最好的做法是採用Stream的方式邊讀取邊儲存(本地檔案或database)。

正確的寫法:

InputStream in = new BufferedInputStream(new FileInputStream(file));
登入後複製
另外,對於伺服器端程式碼來說,為了系統的安全,至少需要對檔案的大小進行限制。

不指定逾時時間

錯誤的程式碼:

byte[] pdf = toPdf(file);
登入後複製

這種情況在工作中已經碰到不只一次了。個人經驗一般超時不要超過20s。這裡有一個問題,connect可以指定超時時間,但是read無法指定逾時時間。但是可以設定阻塞(block)時間。

正確的寫法:

File pdf = toPdf(file);
登入後複製

另外,檔案的讀取(FileInputStream, FileChannel, FileDescriptor, File)沒法指定逾時時間, 而且IO操作均涉及到本地方法呼叫, 這個更操作了JVM的控制範圍,在分散式檔案系統中,對IO的操作內部其實是網路呼叫。一般情況下操作60s的操作都可以認為已經逾時了。為了解決這些問題,一般採用快取和非同步/訊息

佇列

處理。

頻繁使用計時器

錯誤代碼:

Socket socket = ...   
socket.connect(remote);   
InputStream in = socket.getInputStream();   
int i = in.read();
登入後複製

每次new一個Date或Calendar都會涉及一次本地呼叫來取得當前時間(儘管這個本地呼叫相對其他本地方法呼叫要快)。

如果對時間不是特別敏感,這裡使用了clone方法來新建一個Date實例。這樣相對直接new要高效一些。

正確的寫法:

Date d = new Date();   
for (E entity : entities) {   
entity.doSomething();   
entity.setUpdated((Date) d.clone());   
}
登入後複製

如果循环操作耗时较长(超过几ms),那么可以采用下面的方法,立即创建一个Timer,然后定期根据当前时间更新时间戳,在我的系统上比直接new一个时间对象快200倍:

private volatile long time;   
Timer timer = new Timer(true);   
try {   
time = System.currentTimeMillis();   
timer.scheduleAtFixedRate(new TimerTask() {   
public void run() {   
time = System.currentTimeMillis();   
}   
}, 0L, 10L); // granularity 10ms   
for (E entity : entities) {   
entity.doSomething();   
entity.setUpdated(new Date(time));   
}   
} finally {   
timer.cancel();   
}
登入後複製

捕获所有的异常

错误的写法:

Query q = ...   
Person p;   
try {   
p = (Person) q.getSingleResult();   
} catch(Exception e) {   
p = null;   
}
登入後複製

这是EJB3的一个查询操作,可能出现异常的原因是:结果不唯一;没有结果;数据库无法访问,而捕获所有的异常,设置为null将掩盖各种异常情况。

正确的写法:

Query q = ...   
Person p;   
try {   
p = (Person) q.getSingleResult();   
} catch(NoResultException e) {   
p = null;   
}
登入後複製

忽略所有异常

错误的写法:

try {   
doStuff();   
} catch(Exception e) {   
log.fatal("Could not do stuff");   
}   
doMoreStuff();
登入後複製

这个代码有两个问题, 一个是没有告诉调用者, 系统调用出错了. 第二个是日志没有出错原因, 很难跟踪定位问题。

正确的写法:

try {   
doStuff();   
} catch(Exception e) {   
throw new MyRuntimeException("Could not do stuff because: "+ e.getMessage, e);   
}
登入後複製

重复包装RuntimeException

错误的写法:

try {   
doStuff();   
} catch(Exception e) {   
throw new RuntimeException(e);   
}
登入後複製

正确的写法:

try {   
doStuff();   
} catch(RuntimeException e) {   
throw e;   
} catch(Exception e) {   
throw new RuntimeException(e.getMessage(), e);   
}   
try {   
doStuff();   
} catch(IOException e) {   
throw new RuntimeException(e.getMessage(), e);   
} catch(NamingException e) {   
throw new RuntimeException(e.getMessage(), e);   
}
登入後複製

不正确的传播异常

错误的写法:

try {   
} catch(ParseException e) {   
throw new RuntimeException();   
throw new RuntimeException(e.toString());   
throw new RuntimeException(e.getMessage());   
throw new RuntimeException(e);   
}
登入後複製

主要是没有正确的将内部的错误信息传递给调用者. 第一个完全丢掉了内部错误信息, 第二个错误信息依赖toString方法, 如果没有包含最终的嵌套错误信息, 也会出现丢失, 而且可读性差. 第三个稍微好一些, 第四个跟第二个一样。

正确的写法:

try {   
} catch(ParseException e) {   
throw new RuntimeException(e.getMessage(), e);   
}
登入後複製

用日志记录异常

错误的写法:

try {   
...   
} catch(ExceptionA e) {   
log.error(e.getMessage(), e);   
throw e;   
} catch(ExceptionB e) {   
log.error(e.getMessage(), e);   
throw e;   
}
登入後複製

一般情况下在日志中记录异常是不必要的, 除非调用方没有记录日志。

异常处理不彻底

错误的写法:

try {   
is = new FileInputStream(inFile);   
os = new FileOutputStream(outFile);   
} finally {   
try {   
is.close();   
os.close();   
} catch(IOException e) {   
/* we can&#39;t do anything */   
}   
}
登入後複製

is可能close失败, 导致os没有close

正确的写法:

try {   
is = new FileInputStream(inFile);   
os = new FileOutputStream(outFile);   
} finally {   
try { if (is != null) is.close(); } catch(IOException e) {/* we can&#39;t do anything */}   
try { if (os != null) os.close(); } catch(IOException e) {/* we can&#39;t do anything */}   
}
登入後複製

捕获不可能出现的异常

错误的写法:

try {   
... do risky stuff ...   
} catch(SomeException e) {   
// never happens   
}   
... do some more ...
登入後複製

正确的写法:

try {   
... do risky stuff ...   
} catch(SomeException e) {   
// never happens hopefully   
throw new IllegalStateException(e.getMessage(), e); // crash early, passing all information   
}   
... do some more ...
登入後複製

transient的误用

错误的写法:

public class A implements Serializable {   
private String someState;   
private transient Log log = LogFactory.getLog(getClass());   

public void f() {   
log.debug("enter f");   
...   
}   
}
登入後複製

这里的本意是不希望Log对象被序列化. 不过这里在反序列化时, 会因为log未初始化, 导致f()方法抛空指针, 正确的做法是将log定义为静态变量或者定位为具备变量。

正确的写法:

public class A implements Serializable {   
private String someState;   
private static final Log log = LogFactory.getLog(A.class);   

public void f() {   
log.debug("enter f");   
...   
}   
}   
public class A implements Serializable {   
private String someState;   

public void f() {   
Log log = LogFactory.getLog(getClass());   
log.debug("enter f");   
...   
}   
}
登入後複製

不必要的初始化

错误的写法:

public class B {   
private int count = 0;   
private String name = null;   
private boolean important = false;   
}
登入後複製

这里的变量会在初始化时使用默认值:0, null, false, 因此上面的写法有些多此一举。

正确的写法:

public class B {   
private int count;   
private String name;   
private boolean important;   
}
登入後複製

最好用静态final定义Log变量

private static final Log log = LogFactory.getLog(MyClass.class);
登入後複製

这样做的好处有三:

  • 可以保证线程安全

  • 静态或非静态代码都可用

  • 不会影响对象序列化

选择错误的类加载器

错误的代码:

Class clazz = Class.forName(name);   
Class clazz = getClass().getClassLoader().loadClass(name);
登入後複製

这里本意是希望用当前类来加载希望的对象, 但是这里的getClass()可能抛出异常, 特别在一些受管理的环境中, 比如应用服务器, web容器, Java WebStart环境中, 最好的做法是使用当前应用上下文的类加载器来加载。

正确的写法:

ClassLoader cl = Thread.currentThread().getContextClassLoader();   
if (cl == null) cl = MyClass.class.getClassLoader(); // fallback   
Class clazz = cl.loadClass(name);
登入後複製

反射使用不当

错误的写法:

Class beanClass = ...   
if (beanClass.newInstance() instanceof TestBean) ...
登入後複製

这里的本意是检查beanClass是否是TestBean或是其子类, 但是创建一个类实例可能没那么简单, 首先实例化一个对象会带来一定的消耗, 另外有可能类没有定义默认构造函数. 正确的做法是用Class.isAssignableFrom(Class) 方法。

正确的写法:

Class beanClass = ...   
if (TestBean.class.isAssignableFrom(beanClass)) ...
登入後複製

不必要的同步

错误的写法:

Collection l = new Vector();   
for (...) {   
l.add(object);   
}
登入後複製

Vector是ArrayList同步版本。

正确的写法:

Collection l = new ArrayList();   
for (...) {   
l.add(object);   
}
登入後複製

错误的选择List类型

根据下面的表格数据来进行选择


ArrayListLinkedList
add (append)O(1) or ~O(log(n)) if growingO(1)
insert (middle)O(n) or ~O(n*log(n)) if growingO(n)
remove (middle)O(n) (always performs complete copy)O(n)
iterateO(n)O(n)
get by indexO(1)O(n)

HashMap size陷阱

错误的写法:

Map map = new HashMap(collection.size());  
for (Object o : collection) {  
  map.put(o.key, o.value);  
}
登入後複製

这里可以参考guava的Maps.newHashMapWithExpectedSize的实现. 用户的本意是希望给HashMap设置初始值, 避免扩容(resize)的开销. 但是没有考虑当添加的元素数量达到HashMap容量的75%时将出现resize。

正确的写法:

Map map = new HashMap(1 + (int) (collection.size() / 0.75));
登入後複製

对Hashtable, HashMap 和 HashSet了解不够

这里主要需要了解HashMap和Hashtable的内部实现上, 它们都使用Entry包装来封装key/value, Entry内部除了要保存Key/Value的引用, 还需要保存hash桶中next Entry的应用, 因此对内存会有不小的开销, 而HashSet内部实现其实就是一个HashMap. 有时候IdentityHashMap可以作为一个不错的替代方案. 它在内存使用上更有效(没有用Entry封装, 内部采用Object[]). 不过需要小心使用. 它的实现违背了Map接口的定义. 有时候也可以用ArrayList来替换HashSet.

这一切的根源都是由于JDK内部没有提供一套高效的Map和Set实现。

对List的误用

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

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

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

  • 需要对数字进行包装(主要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 = { 10, 20, 30, 40 };
登入後複製

错误的写法:

// 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方法来执行排序。

用数组来描述一个结构

错误用法:

/**   
* @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;   
}
登入後複製

对方法过度限制

错误用法:

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;   
}   
}
登入後複製

对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());
登入後複製

日历对象(Calendar)误用

错误的写法:

Calendar cal = new GregorianCalender(TimeZone.getTimeZone("Europe/Zurich"));   
cal.setTime(date);   
cal.add(Calendar.HOUR_OF_DAY, 8);   
date = cal.getTime();
登入後複製

这里主要是对date, time, calendar和time zone不了解导致. 而在一个时间上增加8小时, 跟time zone没有任何关系, 所以没有必要使用Calendar, 直接用Date对象即可, 而如果是增加天数的话, 则需要使用Calendar, 因为采用不同的时令制可能一天的小时数是不同的(比如有些DST是23或者25个小时)

正确的写法:

date = new Date(date.getTime() + 8L * 3600L * 1000L); // add 8 hrs
登入後複製

TimeZone的误用

错误的写法:

Calendar cal = new GregorianCalendar();   
cal.setTime(date);   
cal.set(Calendar.HOUR_OF_DAY, 0);   
cal.set(Calendar.MINUTE, 0);   
cal.set(Calendar.SECOND, 0);   
Date startOfDay = cal.getTime();
登入後複製

这里有两个错误, 一个是没有没有将毫秒归零, 不过最大的错误是没有指定TimeZone, 不过一般的桌面应用没有问题, 但是如果是服务器端应用则会有一些问题, 比如同一时刻在上海和伦敦就不一样, 因此需要指定的TimeZone.

正确的写法:

Calendar cal = new GregorianCalendar(user.getTimeZone());   
cal.setTime(date);   
cal.set(Calendar.HOUR_OF_DAY, 0);   
cal.set(Calendar.MINUTE, 0);   
cal.set(Calendar.SECOND, 0);   
cal.set(Calendar.MILLISECOND, 0);   
Date startOfDay = cal.getTime();
登入後複製

时区(Time Zone)调整的误用

错误的写法:

public static Date convertTz(Date date, TimeZone tz) {   
Calendar cal = Calendar.getInstance();   
cal.setTimeZone(TimeZone.getTimeZone("UTC"));   
cal.setTime(date);   
cal.setTimeZone(tz);   
return cal.getTime();   
}
登入後複製

这个方法实际上没有改变时间, 输入和输出是一样的. 这里主要的问题是Date对象并不包含Time Zone信息. 它总是使用UTC(世界统一时间). 而调用Calendar的getTime/setTime方法会自动在当前时区和UTC之间做转换。

Calendar.getInstance()的误用

错误的写法:

Calendar c = Calendar.getInstance();   
c.set(2009, Calendar.JANUARY, 15);
登入後複製

Calendar.getInstance()依赖local来选择一个Calendar实现, 不同实现的2009年是不同的, 比如有些Calendar实现就没有January月份。

正确的写法:

Calendar c = new GregorianCalendar(timeZone);   
c.set(2009, Calendar.JANUARY, 15);
登入後複製

Date.setTime()的误用

错误的写法:

account.changePassword(oldPass, newPass);   
Date lastmod = account.getLastModified();   
lastmod.setTime(System.currentTimeMillis());
登入後複製

在更新密码之后, 修改一下最后更新时间, 这里的用法没有错,但是有更好的做法: 直接传Date对象. 因为Date是Value Object, 不可变的. 如果更新了Date的值, 实际上是生成一个新的Date实例. 这样其他地方用到的实际上不在是原来的对象, 这样可能出现不可预知的异常. 当然这里又涉及到另外一个OO设计的问题, 对外暴露Date实例本身就是不好的做法(一般的做法是在setter方法中设置Date引用参数的clone对象). 另外一种比较好的做法就是直接保存long类型的毫秒数。

正确的做法:

account.changePassword(oldPass, newPass);   
account.setLastModified(new Date());
登入後複製

SimpleDateFormat非线程安全误用

错误的写法:

public class Constants {   
public static final SimpleDateFormat date = new SimpleDateFormat("dd.MM.yyyy");   
}
登入後複製

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

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

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

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

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

忽略造型溢出(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));
登入後複製

对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);   
}
登入後複製

用浮点数来保存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");
登入後複製

不使用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: 在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&#39;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 != null) try { rs.close(); } catch (SQLException e) { }   
if (s != null) try { s.close(); } catch (SQLException e) { }   
if (c != null) try { c.close(); } catch (SQLException e) { }   
}   
return car;   
}
登入後複製

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);   
}
登入後複製

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;   
}
登入後複製

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

错误的写法:

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();   
}   
}
登入後複製

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

错误的写法:

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 == null) return;   
callback.timeout();   
}   
}
登入後複製

相关文章:

Java的实现得到,PUT,POST,删除请求

Java多线程基础详解

Java远程通讯技术及原理分析的图文介绍

以上是詳細介紹Java程式設計常見問題總結的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

Java Spring 面試題 Java Spring 面試題 Aug 30, 2024 pm 04:29 PM

在本文中,我們保留了最常被問到的 Java Spring 面試問題及其詳細答案。這樣你就可以順利通過面試。

突破或從Java 8流返回? 突破或從Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一種強大且表達力豐富的處理數據集合的方式。然而,使用Stream時,一個常見問題是:如何從forEach操作中中斷或返回? 傳統循環允許提前中斷或返回,但Stream的forEach方法並不直接支持這種方式。本文將解釋原因,並探討在Stream處理系統中實現提前終止的替代方法。 延伸閱讀: Java Stream API改進 理解Stream forEach forEach方法是一個終端操作,它對Stream中的每個元素執行一個操作。它的設計意圖是處

PHP:網絡開發的關鍵語言 PHP:網絡開發的關鍵語言 Apr 13, 2025 am 12:08 AM

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

PHP與Python:了解差異 PHP與Python:了解差異 Apr 11, 2025 am 12:15 AM

PHP和Python各有優勢,選擇應基於項目需求。 1.PHP適合web開發,語法簡單,執行效率高。 2.Python適用於數據科學和機器學習,語法簡潔,庫豐富。

Java程序查找膠囊的體積 Java程序查找膠囊的體積 Feb 07, 2025 am 11:37 AM

膠囊是一種三維幾何圖形,由一個圓柱體和兩端各一個半球體組成。膠囊的體積可以通過將圓柱體的體積和兩端半球體的體積相加來計算。本教程將討論如何使用不同的方法在Java中計算給定膠囊的體積。 膠囊體積公式 膠囊體積的公式如下: 膠囊體積 = 圓柱體體積 兩個半球體體積 其中, r: 半球體的半徑。 h: 圓柱體的高度(不包括半球體)。 例子 1 輸入 半徑 = 5 單位 高度 = 10 單位 輸出 體積 = 1570.8 立方單位 解釋 使用公式計算體積: 體積 = π × r2 × h (4

PHP與其他語言:比較 PHP與其他語言:比較 Apr 13, 2025 am 12:19 AM

PHP適合web開發,特別是在快速開發和處理動態內容方面表現出色,但不擅長數據科學和企業級應用。與Python相比,PHP在web開發中更具優勢,但在數據科學領域不如Python;與Java相比,PHP在企業級應用中表現較差,但在web開發中更靈活;與JavaScript相比,PHP在後端開發中更簡潔,但在前端開發中不如JavaScript。

PHP與Python:核心功能 PHP與Python:核心功能 Apr 13, 2025 am 12:16 AM

PHP和Python各有優勢,適合不同場景。 1.PHP適用於web開發,提供內置web服務器和豐富函數庫。 2.Python適合數據科學和機器學習,語法簡潔且有強大標準庫。選擇時應根據項目需求決定。

創造未來:零基礎的 Java 編程 創造未來:零基礎的 Java 編程 Oct 13, 2024 pm 01:32 PM

Java是熱門程式語言,適合初學者和經驗豐富的開發者學習。本教學從基礎概念出發,逐步深入解說進階主題。安裝Java開發工具包後,可透過建立簡單的「Hello,World!」程式來實踐程式設計。理解程式碼後,使用命令提示字元編譯並執行程序,控制台上將輸出「Hello,World!」。學習Java開啟了程式設計之旅,隨著掌握程度加深,可創建更複雜的應用程式。

See all articles