Maison > Java > javaDidacticiel > le corps du texte

Introduction détaillée au résumé des problèmes courants dans la programmation Java

黄舟
Libérer: 2017-03-28 15:36:12
original
1567 Les gens l'ont consulté

Nous écrivons des programmes Java tous les jours. En fait, il contient certains détails auxquels vous ne prêtez peut-être pas beaucoup d'attention. Non, quelqu'un l'a résumé pour nous Problèmes courants en programmationBien qu'il n'y ait généralement pas de gros problèmes, il est préférable de ne pas le faire. De plus, la plupart des problèmes mentionnés ici peuvent en fait être vérifiés pour nous via Findbugs

Utilisation abusive de la concaténation de chaînes

Écriture incorrecte :

String s = "";  
for (Person p : persons) {  
    s += ", " + p.getName();  
}  
s = s.substring(2); //remove first comma
Copier après la connexion

Écriture correcte :

StringBuilder sb = new StringBuilder(persons.size() * 16); // well estimated buffer
for (Person p : persons) {
    if (sb.length() > 0) sb.append(", ");
    sb.append(p.getName);
}
Copier après la connexion

Utilisation incorrecte de StringBuffer<. 🎜>

Mauvaise écriture :

StringBuffer sb = new StringBuffer();  
sb.append("Name: ");  
sb.append(name + &#39;\n&#39;);  
sb.append("!");  
...  
String s = sb.toString();
Copier après la connexion
Le problème est dans la troisième ligne. Les performances de append char sont meilleures que celles de String. De plus, la taille n'est pas spécifiée lors de l'initialisation de StringBuffer, ce qui peut en être la cause. le

tableau interne à redimensionner lors de l'ajout . S'il s'agit de JDK1.5, il est préférable d'utiliser StringBuilder au lieu de StringBuffer, à moins qu'il n'y ait une exigence de sécurité du thread. pour connecter directement la chaîne. L'inconvénient est que la longueur ne peut pas être spécifiée lors de l'initialisation. > Manière correcte d'écrire :

Ou comme ceci :

StringBuilder sb = new StringBuilder(100);  
sb.append("Name: ");  
sb.append(name);  
sb.append("\n!");  
String s = sb.toString();
Copier après la connexion

Tester l'égalité des chaînes

String s = "Name: " + name + "\n!";
Copier après la connexion
Mauvaise façon d'écrire :

Le code ci-dessus n'est pas faux, mais il n'est pas assez bon. La signification originale de == est de comparer si deux

objets sont les mêmes. idem. De plus, il est préférable de juger si le caractère est vide.
if (name.compareTo("John") == 0) ...  
if (name == "John") ...  
if (name.equals("John")) ...  
if ("".equals(name)) ...
Copier après la connexion
Méthode d'écriture correcte :

Convertir les nombres en chaînes

Méthode d'écriture incorrecte :

if ("John".equals(name)) ...  
if (name.length() == 0) ...  
if (name.isEmpty()) ...
Copier après la connexion

Manière d'écriture correcte :

Utiliser des objets immuables (Immuable)
"" + set.size()  
new Integer(set.size()).toString()
Copier après la connexion

Manière d'écriture incorrecte :

String.valueOf(set.size())
Copier après la connexion

Manière correcte d'écrire écriture :

Veuillez utiliser un analyseur XML
zero = new Integer(0);  
return Boolean.valueOf("true");
Copier après la connexion

Manière d'écriture incorrecte :

zero = Integer.valueOf(0);  
return Boolean.TRUE;
Copier après la connexion

Manière d'écriture correcte :

S'il vous plaît utiliser JDom pour assembler du XML
int start = xml.indexOf("<name>") + "<name>".length();  
int end = xml.indexOf("</name>");  
String name = xml.substring(start, end);
Copier après la connexion

Manière d'écriture incorrecte :

SAXBuilder builder = new SAXBuilder(false);  
Document doc = doc = builder.build(new StringReader(xml));  
String name = doc.getRootElement().getChild("name").getText();
Copier après la connexion

Manière d'écriture correcte :

Piège d'encodage XML
String name = ...  
String attribute = ...  
String xml = "<root>" 
            +"<name att=\""+ attribute +"\">"+ name +"</name>" 
            +"</root>";
Copier après la connexion

Manière incorrecte d'écriture :

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);
Copier après la connexion

Parce que l'encodage du xml est spécifié dans le fichier, lors de la lecture L'encodage doit être spécifié lors de l'édition du fichier Un autre problème est que vous ne pouvez pas enregistrer un fichier xml dans String à la fois. , ce qui entraînera un gaspillage inutile de mémoire. L'approche correcte consiste à utiliser InputStream pour lire et traiter en même temps. Afin de résoudre le problème d'encodage, il est préférable d'utiliser un analyseur XML pour le gérer. 🎜>

L'encodage des caractères n'est pas spécifié

Ecriture incorrecte :
String xml = FileUtils.readTextFile("my.xml");
Copier après la connexion

Ce code n'est principalement pas portable entre les plates-formes. Différentes plates-formes peuvent utiliser des encodages de caractères par défaut différents.

Manière d'écriture correcte :

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();
Copier après la connexion
Le flux de données n'est pas mis en cache

Manière d'écriture incorrecte :

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");
Copier après la connexion
Le code ci-dessus est un octet La lecture d'un octet entraîne des accès fréquents au JNI

système de fichiers

local, ce qui est très inefficace car l'appel de méthodes locales prend beaucoup de temps. Il est préférable de l'envelopper avec BufferedInputStream. J'ai fait un test une fois. Il a fallu environ 1 seconde pour lire 1 Mo à partir de /dev/zero, mais après l'avoir empaqueté avec BufferedInputStream, cela n'a pris que 60 ms et les performances ont augmenté de 94 %. Cela s'applique également aux opérations de flux de sortie et aux opérations de socket ! .

Manière d'écriture correcte :
InputStream in = new FileInputStream(file);   
int b;   
while ((b = in.read()) != -1) {   
...   
}
Copier après la connexion

Utilisation illimitée de la mémoire tas

Manière d'écriture incorrecte :

InputStream in = new BufferedInputStream(new FileInputStream(file));
Copier après la connexion
Il y a une prémisse ici , qui correspond à la taille du fichier. On ne peut pas dire que le tas de la JVM est plein. Sinon, attendez simplement le MOO, en particulier dans le code côté serveur à haute concurrence. La meilleure approche consiste à utiliser Stream pour lire et stocker en même temps (fichier local ou base de données).

Écriture correcte :

byte[] pdf = toPdf(file);
Copier après la connexion
De plus, pour le code côté serveur, pour des raisons de sécurité du système, au moins la taille du fichier doit être limitée.

Aucun délai d'attente spécifié

Code incorrect :
File pdf = toPdf(file);
Copier après la connexion

J'ai rencontré cette situation plus d'une fois au travail. L'expérience personnelle indique généralement que le délai d'attente ne doit pas dépasser 20 secondes. Il y a un problème ici. Connect peut spécifier un délai d'attente, mais read ne peut pas spécifier de délai d'attente. Mais vous pouvez régler le temps de blocage.

Écriture correcte :

Socket socket = ...   
socket.connect(remote);   
InputStream in = socket.getInputStream();   
int i = in.read();
Copier après la connexion
De plus, la lecture de fichiers (FileInputStream, FileChannel, FileDescriptor, File) ne peut pas spécifier de délai d'attente et les opérations d'E/S impliquent des appels de méthodes locales. Cela exploite davantage le contrôle. portée de la JVM. Dans un système de fichiers distribué, les opérations d'E/S sont en fait des appels réseau. Dans des circonstances normales, les opérations qui durent 60 secondes peuvent être considérées comme ayant expiré. Afin de résoudre ces problèmes, la mise en cache et le traitement asynchrone/message

queue

sont généralement utilisés.

Minuterie fréquemment utilisée
Socket socket = ...   
socket.connect(remote, 20000); // fail after 20s   
InputStream in = socket.getInputStream();   
socket.setSoTimeout(15000);   
int i = in.read();
Copier après la connexion

Code d'erreur :

Chaque fois qu'une nouvelle date ou un nouveau calendrier implique un appel local pour obtenir l'heure actuelle (bien que cet appel local soit relativement Les autres appels de méthodes locales sont plus rapides).

Si vous n'êtes pas particulièrement sensible au temps, la méthode clone est utilisée ici pour créer une nouvelle instance de Date. C'est plus efficace que le neuf direct.

Écriture correcte :

Date d = new Date();   
for (E entity : entities) {   
entity.doSomething();   
entity.setUpdated((Date) d.clone());   
}
Copier après la connexion

如果循环操作耗时较长(超过几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();   
}
Copier après la connexion

捕获所有的异常

错误的写法:

Query q = ...   
Person p;   
try {   
p = (Person) q.getSingleResult();   
} catch(Exception e) {   
p = null;   
}
Copier après la connexion

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

正确的写法:

Query q = ...   
Person p;   
try {   
p = (Person) q.getSingleResult();   
} catch(NoResultException e) {   
p = null;   
}
Copier après la connexion

忽略所有异常

错误的写法:

try {   
doStuff();   
} catch(Exception e) {   
log.fatal("Could not do stuff");   
}   
doMoreStuff();
Copier après la connexion

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

正确的写法:

try {   
doStuff();   
} catch(Exception e) {   
throw new MyRuntimeException("Could not do stuff because: "+ e.getMessage, e);   
}
Copier après la connexion

重复包装RuntimeException

错误的写法:

try {   
doStuff();   
} catch(Exception e) {   
throw new RuntimeException(e);   
}
Copier après la connexion

正确的写法:

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);   
}
Copier après la connexion

不正确的传播异常

错误的写法:

try {   
} catch(ParseException e) {   
throw new RuntimeException();   
throw new RuntimeException(e.toString());   
throw new RuntimeException(e.getMessage());   
throw new RuntimeException(e);   
}
Copier après la connexion

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

正确的写法:

try {   
} catch(ParseException e) {   
throw new RuntimeException(e.getMessage(), e);   
}
Copier après la connexion

用日志记录异常

错误的写法:

try {   
...   
} catch(ExceptionA e) {   
log.error(e.getMessage(), e);   
throw e;   
} catch(ExceptionB e) {   
log.error(e.getMessage(), e);   
throw e;   
}
Copier après la connexion

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

异常处理不彻底

错误的写法:

try {   
is = new FileInputStream(inFile);   
os = new FileOutputStream(outFile);   
} finally {   
try {   
is.close();   
os.close();   
} catch(IOException e) {   
/* we can&#39;t do anything */   
}   
}
Copier après la connexion

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 */}   
}
Copier après la connexion

捕获不可能出现的异常

错误的写法:

try {   
... do risky stuff ...   
} catch(SomeException e) {   
// never happens   
}   
... do some more ...
Copier après la connexion

正确的写法:

try {   
... do risky stuff ...   
} catch(SomeException e) {   
// never happens hopefully   
throw new IllegalStateException(e.getMessage(), e); // crash early, passing all information   
}   
... do some more ...
Copier après la connexion

transient的误用

错误的写法:

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

public void f() {   
log.debug("enter f");   
...   
}   
}
Copier après la connexion

这里的本意是不希望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");   
...   
}   
}
Copier après la connexion

不必要的初始化

错误的写法:

public class B {   
private int count = 0;   
private String name = null;   
private boolean important = false;   
}
Copier après la connexion

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

正确的写法:

public class B {   
private int count;   
private String name;   
private boolean important;   
}
Copier après la connexion

最好用静态final定义Log变量

private static final Log log = LogFactory.getLog(MyClass.class);
Copier après la connexion

这样做的好处有三:

  • 可以保证线程安全

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

  • 不会影响对象序列化

选择错误的类加载器

错误的代码:

Class clazz = Class.forName(name);   
Class clazz = getClass().getClassLoader().loadClass(name);
Copier après la connexion

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

正确的写法:

ClassLoader cl = Thread.currentThread().getContextClassLoader();   
if (cl == null) cl = MyClass.class.getClassLoader(); // fallback   
Class clazz = cl.loadClass(name);
Copier après la connexion

反射使用不当

错误的写法:

Class beanClass = ...   
if (beanClass.newInstance() instanceof TestBean) ...
Copier après la connexion

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

正确的写法:

Class beanClass = ...   
if (TestBean.class.isAssignableFrom(beanClass)) ...
Copier après la connexion

不必要的同步

错误的写法:

Collection l = new Vector();   
for (...) {   
l.add(object);   
}
Copier après la connexion

Vector是ArrayList同步版本。

正确的写法:

Collection l = new ArrayList();   
for (...) {   
l.add(object);   
}
Copier après la connexion

错误的选择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);  
}
Copier après la connexion

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

正确的写法:

Map map = new HashMap(1 + (int) (collection.size() / 0.75));
Copier après la connexion

对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));
Copier après la connexion

正确的写法:

int[] codes = { 10, 20, 30, 40 };
Copier après la connexion

错误的写法:

// 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();  
        }  
    }  
}
Copier après la connexion

正确的写法:

// 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;  
        }  
    }  
}
Copier après la connexion

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

用数组来描述一个结构

错误用法:

/**   
* @returns [1]: Location, [2]: Customer, [3]: Incident   
*/   
Object[] getDetails(int id) {...
Copier après la connexion

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

正确的写法:

Details getDetails(int id) {...}   
private class Details {   
public Location location;   
public Customer customer;   
public Incident incident;   
}
Copier après la connexion

对方法过度限制

错误用法:

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

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

正确的写法:

public void notify(Person p) {   
...   
sendMail(p);   
...   
}   
class EmployeeDirectory {   
Employee lookup(String employeeId) {   
Employee emp = ...   
return emp;   
}   
}
Copier après la connexion

对POJO的setter方法画蛇添足

错误的写法:

private String name;   
public void setName(String name) {   
this.name = name.trim();   
}   
public void String getName() {   
return this.name;   
}
Copier après la connexion

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

正确的做法:

person.setName(textInput.getText().trim());
Copier après la connexion

日历对象(Calendar)误用

错误的写法:

Calendar cal = new GregorianCalender(TimeZone.getTimeZone("Europe/Zurich"));   
cal.setTime(date);   
cal.add(Calendar.HOUR_OF_DAY, 8);   
date = cal.getTime();
Copier après la connexion

这里主要是对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
Copier après la connexion

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();
Copier après la connexion

这里有两个错误, 一个是没有没有将毫秒归零, 不过最大的错误是没有指定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();
Copier après la connexion

时区(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();   
}
Copier après la connexion

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

Calendar.getInstance()的误用

错误的写法:

Calendar c = Calendar.getInstance();   
c.set(2009, Calendar.JANUARY, 15);
Copier après la connexion

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

正确的写法:

Calendar c = new GregorianCalendar(timeZone);   
c.set(2009, Calendar.JANUARY, 15);
Copier après la connexion

Date.setTime()的误用

错误的写法:

account.changePassword(oldPass, newPass);   
Date lastmod = account.getLastModified();   
lastmod.setTime(System.currentTimeMillis());
Copier après la connexion

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

正确的做法:

account.changePassword(oldPass, newPass);   
account.setLastModified(new Date());
Copier après la connexion

SimpleDateFormat非线程安全误用

错误的写法:

public class Constants {   
public static final SimpleDateFormat date = new SimpleDateFormat("dd.MM.yyyy");   
}
Copier après la connexion

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 ...";   
}
Copier après la connexion

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

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

忽略造型溢出(cast overflow)

错误的写法:

public int getFileSize(File f) {   
long l = f.length();   
return (int) l;   
}
Copier après la connexion

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

正确的写法:

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

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

long a = System.currentTimeMillis();   
long b = a + 100;   
System.out.println((int) b-a);   
System.out.println((int) (b-a));
Copier après la connexion

对float和double使用==操作

错误的写法:

for (float f = 10f; f!=0; f-=0.1) {   
System.out.println(f);   
}
Copier après la connexion

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

正确的写法:

for (float f = 10f; f>0; f-=0.1) {   
System.out.println(f);   
}
Copier après la connexion

用浮点数来保存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); //造成精度丢失
Copier après la connexion

这个也是一个老生常谈的错误. 比如计算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");
Copier après la connexion

不使用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();   
}
Copier après la connexion

上面的代码打开一个文件输出流, 操作系统为其分配一个文件句柄, 但是文件句柄是一种非常稀缺的资源, 必须通过调用相应的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) { }   
}   
}
Copier après la connexion

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

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;   
}
Copier après la connexion

finalize方法误用

错误的写法:

public class FileBackedCache {   
private File backingStore;   

...   

protected void finalize() throws IOException {   
if (backingStore != null) {   
backingStore.close();   
backingStore = null;   
}   
}   
}
Copier après la connexion

这个问题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;   
}   
}   
}
Copier après la connexion

在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);   
}
Copier après la connexion

Thread.interrupted方法误用

错误的写法:

try {   
Thread.sleep(1000);   
} catch (InterruptedException e) {   
// ok   
}   
or   
while (true) {   
if (Thread.interrupted()) break;   
}
Copier après la connexion

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

正确的写法:

try {   
Thread.sleep(1000);   
} catch (InterruptedException e) {   
Thread.currentThread().interrupt();   
}   
or   
while (true) {   
if (Thread.currentThread().isInterrupted()) break;   
}
Copier après la connexion

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

错误的写法:

class Cache {   
private static final Timer evictor = new Timer();   
}
Copier après la connexion

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

正确的做法:

class Cache {   
private static Timer evictor;   
public static setupEvictor() {   
evictor = new Timer();   
}   
}
Copier après la connexion

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

错误的写法:

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

上面的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();   
}   
}
Copier après la connexion

相关文章:

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

Java多线程基础详解

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal
À propos de nous Clause de non-responsabilité Sitemap
Site Web PHP chinois:Formation PHP en ligne sur le bien-être public,Aidez les apprenants PHP à grandir rapidement!