搜尋
首頁Javajava教程詳細介紹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,除非有執行緒安全的要求。 ##

StringBuilder sb = new StringBuilder(100);  
sb.append("Name: ");  
sb.append(name);  
sb.append("\n!");  
String s = sb.toString();
或這樣寫:

String s = "Name: " + name + "\n!";

測試字串相等性

錯誤的寫法:

if (name.compareTo("John") == 0) ...  
if (name == "John") ...  
if (name.equals("John")) ...  
if ("".equals(name)) ...

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

物件

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

錯誤的寫法:

if ("John".equals(name)) ...  
if (name.length() == 0) ...  
if (name.isEmpty()) ...
正確的寫法:

"" + 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类型

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


ArrayList LinkedList
add (append) O(1) or ~O(log(n)) if growing O(1)
insert (middle) O(n) or ~O(n*log(n)) if growing O(n)
remove (middle) O(n) (always performs complete copy) O(n)
iterate O(n) O(n)
get by index O(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
JVM中的類加載程序子系統如何促進平台獨立性?JVM中的類加載程序子系統如何促進平台獨立性?Apr 23, 2025 am 12:14 AM

類加載器通過統一的類文件格式、動態加載、雙親委派模型和平台無關的字節碼,確保Java程序在不同平台上的一致性和兼容性,實現平台獨立性。

Java編譯器會產生特定於平台的代碼嗎?解釋。Java編譯器會產生特定於平台的代碼嗎?解釋。Apr 23, 2025 am 12:09 AM

Java編譯器生成的代碼是平台無關的,但最終執行的代碼是平台特定的。 1.Java源代碼編譯成平台無關的字節碼。 2.JVM將字節碼轉換為特定平台的機器碼,確保跨平台運行但性能可能不同。

JVM如何處理不同操作系統的多線程?JVM如何處理不同操作系統的多線程?Apr 23, 2025 am 12:07 AM

多線程在現代編程中重要,因為它能提高程序的響應性和資源利用率,並處理複雜的並發任務。 JVM通過線程映射、調度機制和同步鎖機制,在不同操作系統上確保多線程的一致性和高效性。

在Java的背景下,'平台獨立性”意味著什麼?在Java的背景下,'平台獨立性”意味著什麼?Apr 23, 2025 am 12:05 AM

Java的平台獨立性是指編寫的代碼可以在任何安裝了JVM的平台上運行,無需修改。 1)Java源代碼編譯成字節碼,2)字節碼由JVM解釋執行,3)JVM提供內存管理和垃圾回收功能,確保程序在不同操作系統上運行。

Java應用程序仍然可以遇到平台特定的錯誤或問題嗎?Java應用程序仍然可以遇到平台特定的錯誤或問題嗎?Apr 23, 2025 am 12:03 AM

Javaapplicationscanindeedencounterplatform-specificissuesdespitetheJVM'sabstraction.Reasonsinclude:1)Nativecodeandlibraries,2)Operatingsystemdifferences,3)JVMimplementationvariations,and4)Hardwaredependencies.Tomitigatethese,developersshould:1)Conduc

雲計算如何影響Java平台獨立性的重要性?雲計算如何影響Java平台獨立性的重要性?Apr 22, 2025 pm 07:05 PM

云计算显著提升了Java的平台独立性。1)Java代码编译为字节码,由JVM在不同操作系统上执行,确保跨平台运行。2)使用Docker和Kubernetes部署Java应用,提高可移植性和可扩展性。

Java的平台獨立性在廣泛採用中扮演著什麼角色?Java的平台獨立性在廣泛採用中扮演著什麼角色?Apr 22, 2025 pm 06:53 PM

Java'splatformindependenceallowsdeveloperstowritecodeonceandrunitonanydeviceorOSwithaJVM.Thisisachievedthroughcompilingtobytecode,whichtheJVMinterpretsorcompilesatruntime.ThisfeaturehassignificantlyboostedJava'sadoptionduetocross-platformdeployment,s

容器化技術(例如Docker)如何影響Java平台獨立性的重要性?容器化技術(例如Docker)如何影響Java平台獨立性的重要性?Apr 22, 2025 pm 06:49 PM

容器化技術如Docker增強而非替代Java的平台獨立性。 1)確保跨環境的一致性,2)管理依賴性,包括特定JVM版本,3)簡化部署過程,使Java應用更具適應性和易管理性。

See all articles

熱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

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

熱工具

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

將Eclipse與SAP NetWeaver應用伺服器整合。

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

VSCode Windows 64位元 下載

VSCode Windows 64位元 下載

微軟推出的免費、功能強大的一款IDE編輯器

SublimeText3 Mac版

SublimeText3 Mac版

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