java

位置:IT落伍者 >> java >> 浏览文章

Java编程:常见问题汇总


发布日期:2022年08月31日
 
Java编程:常见问题汇总

字符串连接误用

错误的写法

    Strings=;

    for(Personp:persons){

    s+=+pgetName();

    }

    s=ssubstring();//removefirstcomma

正确的写法

    StringBuildersb=newStringBuilder(personssize()*);//wellestimatedbuffer

    for(Personp:persons){

    if(sblength()>)sbappend();

    sbappend(pgetName);

    }

错误的使用StringBuffer

错误的写法

    StringBuffersb=newStringBuffer();

    sbappend(Name:);

    sbappend(name+\n);

    sbappend(!);

    Strings=sbtoString();

问题在第三行append char比String性能要好另外就是初始化StringBuffer没有指定size导致中间append时可能重新调整内部数组大小如果是JDK最好用StringBuilder取代StringBuffer除非有线程安全的要求还有一种方式就是可以直接连接字符串缺点就是无法初始化时指定长度

正确的写法

    StringBuildersb=newStringBuilder();

    sbappend(Name:);

    sbappend(name);

    sbappend(\n!);

    Strings=sbtoString();

或者这样写

    Strings=Name:+name+\n!;

测试字符串相等性

错误的写法

    if(pareTo(John)==)

    if(name==John)

    if(nameequals(John))

    if(equals(name))

上面的代码没有错但是不够好compareTo不够简洁==原义是比较两个对象是否一样另外比较字符是否为空最好判断它的长度

正确的写法

    if(Johnequals(name))

    if(namelength()==)

    if(nameisEmpty())

数字转换成字符串

错误的写法

    +setsize()

    newInteger(setsize())toString()

正确的写法

    StringvalueOf(setsize())

利用不可变对象(Immutable)

错误的写法

    zero=newInteger();

    returnBooleanvalueOf(true);

正确的写法

    zero=IntegervalueOf();

    returnBooleanTRUE;

请使用XML解析器

错误的写法

    intstart=xmlindexOf(<name>)+<name>length();

    intend=xmlindexOf(</name>);

    Stringname=xmlsubstring(startend);

正确的写法

    SAXBuilderbuilder=newSAXBuilder(false);

    Documentdoc=doc=builderbuild(newStringReader(xml));

    Stringname=docgetRootElement()getChild(name)getText();

请使用JDom组装XML

错误的写法

    Stringname=

    Stringattribute=

    Stringxml=<root>

    +<nameatt=\+attribute+\>+name+</name>

    +</root>;

正确的写法

    Elementroot=newElement(root);

    rootsetAttribute(attattribute);

    rootsetText(name);

    Documentdoc=newDocumet();

    docsetRootElement(root);

    XmlOutputterout=newXmlOutputter(FormatgetPrettyFormat());

    Stringxml=outoutputString(root);

XML编码陷阱

错误的写法

    Stringxml=FileUtilsreadTextFile(myxml);

因为xml的编码在文件中指定的而在读文件的时候必须指定编码另外一个问题不能一次就将一个xml文件用String保存这样对内存会造成不必要的浪费正确的做法用InputStream来边读取边处理为了解决编码的问题 最好使用XML解析器来处理

未指定字符编码

错误的写法

    Readerr=newFileReader(file);

    Writerw=newFileWriter(file);

    Readerr=newInputStreamReader(inputStream);

    Writerw=newOutputStreamWriter(outputStream);

    Strings=newString(byteArray);//byteArrayisabyte[]

    byte[]a=stringgetBytes();

这样的代码主要不具有跨平台可移植性因为不同的平台可能使用的是不同的默认字符编码

正确的写法

    Readerr=newInputStreamReader(newFileInputStream(file)ISO);

    Writerw=newOutputStreamWriter(newFileOutputStream(file)ISO);

    Readerr=newInputStreamReader(inputStreamUTF);

    Writerw=newOutputStreamWriter(outputStreamUTF);

    Strings=newString(byteArrayASCII);

    byte[]a=stringgetBytes(ASCII);

未对数据流进行缓存

错误的写法

    InputStreamin=newFileInputStream(file);

    intb;

    while((b=inread())!=){

    }

上面的代码是一个byte一个byte的读取导致频繁的本地JNI文件系统访问非常低效因为调用本地方法是非常耗时的最好用BufferedInputStream包装一下曾经做过一个测试从/dev/zero下读取MB大概花了s而用BufferedInputStream包装之后只需要ms性能提高了%! 这个也适用于output stream操作以及socket操作

正确的写法

    InputStreamin=newBufferedInputStream(newFileInputStream(file));

无限使用heap内存

错误的写法

    byte[]pdf=toPdf(file);

这里有一个前提就是文件大小不能讲JVM的heap撑爆否则就等着OOM吧尤其是在高并发的服务器端代码最好的做法是采用Stream的方式边读取边存储(本地文件或database)

正确的写法

    Filepdf=toPdf(file);

另外对于服务器端代码来说为了系统的安全至少需要对文件的大小进行限制

不指定超时时间

错误的代码

    Socketsocket=

    nnect(remote);

    InputStreamin=socketgetInputStream();

    inti=inread();

这种情况在工作中已经碰到不止一次了个人经验一般超时不要超过s这里有一个问题connect可以指定超时时间但是read无法指定超时时间但是可以设置阻塞(block)时间

正确的写法

    Socketsocket=

    nnect(remote);//failafters

    InputStreamin=socketgetInputStream();

    socketsetSoTimeout();

    inti=inread();

另外文件的读取(FileInputStream FileChannel FileDescriptor File)没法指定超时时间 而且IO操作均涉及到本地方法调用 这个更操作了JVM的控制范围在分布式文件系统中对IO的操作内部实际上是网络调用一般情况下操作s的操作都可以认为已经超时了为了解决这些问题一般采用缓存和异步/消息队列处理

频繁使用计时器

错误代码

    for(){

    longt=SystemcurrentTimeMillis();

    longt=SystemnanoTime();

    Dated=newDate();

    Calendarc=newGregorianCalendar();

    }

每次new一个Date或Calendar都会涉及一次本地调用来获取当前时间(尽管这个本地调用相对其他本地方法调用要快)

如果对时间不是特别敏感这里使用了clone方法来新建一个Date实例这样相对直接new要高效一些

正确的写法

    Dated=newDate();

    for(Eentity:entities){

    entitydoSomething();

    entitysetUpdated((Date)dclone());

    }

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

    privatevolatilelongtime;

    Timertimer=newTimer(true);

    try{

    time=SystemcurrentTimeMillis();

    timerscheduleAtFixedRate(newTimerTask(){

    publicvoidrun(){

    time=SystemcurrentTimeMillis();

    }

    }LL);//granularityms

    for(Eentity:entities){

    entitydoSomething();

    entitysetUpdated(newDate(time));

    }

    }finally{

    timercancel();

    }

捕获所有的异常

错误的写法

    Queryq=

    Personp;

    try{

    p=(Person)qgetSingleResult();

    }catch(Exceptione){

    p=null;

    }

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

正确的写法

    Queryq=

    Personp;

    try{

    p=(Person)qgetSingleResult();

    }catch(NoResultExceptione){

    p=null;

    }

忽略所有异常

错误的写法

    try{

    doStuff();

    }catch(Exceptione){

    logfatal(Couldnotdostuff);

    }

    doMoreStuff();

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

正确的写法

    try{

    doStuff();

    }catch(Exceptione){

    thrownewMyRuntimeException(Couldnotdostuffbecause:+egetMessagee);

    }

重复包装RuntimeException

错误的写法

    try{

    doStuff();

    }catch(Exceptione){

    thrownewRuntimeException(e);

    }

正确的写法

    try{  

    doStuff();  

    }catch(RuntimeExceptione){  

    throwe;  

    }catch(Exceptione){  

    thrownewRuntimeException(egetMessage()e);  

    }  

    try{  

    doStuff();  

    }catch(IOExceptione){  

    thrownewRuntimeException(egetMessage()e);  

    }catch(NamingExceptione){  

    thrownewRuntimeException(egetMessage()e);

不正确的传播异常

错误的写法

    try{

    }catch(ParseExceptione){

    thrownewRuntimeException();

    thrownewRuntimeException(etoString());

    thrownewRuntimeException(egetMessage());

    thrownewRuntimeException(e);

    }

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

正确的写法

    try{

    }catch(ParseExceptione){

    thrownewRuntimeException(egetMessage()e);

    }

用日志记录异常

错误的写法

    try{

    }catch(ExceptionAe){

    logerror(egetMessage()e);

    throwe;

    }catch(ExceptionBe){

    logerror(egetMessage()e);

    throwe;

    }

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

异常处理不彻底

错误的写法

    try{

    is=newFileInputStream(inFile);

    os=newFileOutputStream(outFile);

    }finally{

    try{

    isclose();

    osclose();

    }catch(IOExceptione){

    /*wecantdoanything*/

    }

    }

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

正确的写法

    try{

    is=newFileInputStream(inFile);

    os=newFileOutputStream(outFile);

    }finally{

    try{if(is!=null)isclose();}catch(IOExceptione){/*wecantdoanything*/}

    try{if(os!=null)osclose();}catch(IOExceptione){/*wecantdoanything*/}

    }

捕获不可能出现的异常

错误的写法

    try{

    doriskystuff

    }catch(SomeExceptione){

    //neverhappens

    }

    dosomemore

正确的写法

    try{

    doriskystuff

    }catch(SomeExceptione){

    //neverhappenshopefully

    thrownewIllegalStateException(egetMessage()e);//crashearlypassingallinformation

    }

    dosomemore

transient的误用

错误的写法

    publicclassAimplementsSerializable{

    privateStringsomeState;

    privatetransientLoglog=LogFactorygetLog(getClass());

    publicvoidf(){

    logdebug(enterf);

    }

    }

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

正确的写法

    publicclassAimplementsSerializable{

    privateStringsomeState;

    privatestaticfinalLoglog=LogFactorygetLog(Aclass);

    publicvoidf(){

    logdebug(enterf);

    }

    }

    publicclassAimplementsSerializable{

    privateStringsomeState;

    publicvoidf(){

    Loglog=LogFactorygetLog(getClass());

    logdebug(enterf);

    }

    }

不必要的初始化

错误的写法

    publicclassB{

    privateintcount=;

    privateStringname=null;

    privatebooleanimportant=false;

    }

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

正确的写法

    publicclassB{

    privateintcount;

    privateStringname;

    privatebooleanimportant;

    }

最好用静态final定义Log变量

    privatestaticfinalLoglog=LogFactorygetLog(MyClassclass);

这样做的好处有三

可以保证线程安全

静态或非静态代码都可用

不会影响对象序列化

选择错误的类加载器

错误的代码

    Classclazz=ClassforName(name);

    Classclazz=getClass()getClassLoader()loadClass(name);

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

正确的写法

    ClassLoadercl=ThreadcurrentThread()getContextClassLoader();

    if(cl==null)cl=MyClassclassgetClassLoader();//fallback

    Classclazz=clloadClass(name);

反射使用不当

错误的写法

    ClassbeanClass=

    if(beanClassnewInstance()instanceofTestBean)

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

正确的写法

    ClassbeanClass=

    if(TestBeanclassisAssignableFrom(beanClass))

不必要的同步

错误的写法

    Collectionl=newVector();

    for(){

    ladd(object);

    }

Vector是ArrayList同步版本

正确的写法

    Collectionl=newArrayList();

    for(){

    ladd(object);

    }

错误的选择List类型

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

ArrayListLinkedListadd (append)O() or ~O(log(n)) if growingO()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()O(n)

HashMap size陷阱

错误的写法

    Mapmap=newHashMap(collectionsize());

    for(Objecto:collection){

    mapput(okeyovalue);

    }

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

正确的写法

    Mapmap=newHashMap(+(int)(collectionsize()/));

对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频繁的遍历比如超过w次

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

比如下面的代码

错误的写法

    List<Integer>codes=newArrayList<Integer>();

    codesadd(IntegervalueOf());

    codesadd(IntegervalueOf());

    codesadd(IntegervalueOf());

    codesadd(IntegervalueOf());

正确的写法

    int[]codes={};

错误的写法

    //horriblyslowandamemorywasteriflhasafewthousandelements(tryityourself!)

    List<Mergeable>l=;

    for(inti=;i<lsize();i++){

    Mergeableone=lget(i);

    Iterator<Mergeable>j=erator(i+);//memoryallocation!

    while(jhasNext()){

    Mergeableother=lnext();

    if(onecanMergeWith(other)){

    rge(other);

    otherremove();

    }

    }

    }

正确的写法

    //quitefastandnomemoryallocation

    Mergeable[]l=;

    for(inti=;i<llength;i++){

    Mergeableone=l[i];

    for(intj=i+;j<llength;j++){

    Mergeableother=l[j];

    if(onecanMergeWith(other)){

    rge(other);

    l[j]=null;

    }

    }

    }

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

用数组来描述一个结构

错误用法

    /**

    *@returns[]:Location[]:Customer[]:Incident

    */

    Object[]getDetails(intid){

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

正确的写法

    DetailsgetDetails(intid){}

    privateclassDetails{

    publicLocationlocation;

    publicCustomercustomer;

    publicIncidentincident;

    }

对方法过度限制

错误用法

    publicvoidnotify(Personp){

    sendMail(pgetName()pgetFirstName()pgetEmail());

    }

    classPhoneBook{

    Stringlookup(StringemployeeId){

    Employeeemp=

    returnempgetPhone();

    }

    }

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

正确的写法

    publicvoidnotify(Personp){

    sendMail(p);

    }

    classEmployeeDirectory{

    Employeelookup(StringemployeeId){

    Employeeemp=

    returnemp;

    }

    }

对POJO的setter方法画蛇添足

错误的写法

    privateStringname;

    publicvoidsetName(Stringname){

    thisname=nametrim();

    }

    publicvoidStringgetName(){

    returnthisname;

    }

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

正确的做法

    personsetName(textInputgetText()trim());

日历对象(Calendar)误用

错误的写法

    Calendarcal=newGregorianCalender(TimeZonegetTimeZone(Europe/Zurich));

    calsetTime(date);

    caladd(CalendarHOUR_OF_DAY);

    date=calgetTime();

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

正确的写法

    date=newDate(dategetTime()+L*L*L);//addhrs

TimeZone的误用

错误的写法

    Calendarcal=newGregorianCalendar();

    calsetTime(date);

    calset(CalendarHOUR_OF_DAY);

    calset(CalendarMINUTE);

    calset(CalendarSECOND);

    DatestartOfDay=calgetTime();

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

正确的写法

    Calendarcal=newGregorianCalendar(usergetTimeZone());

    calsetTime(date);

    calset(CalendarHOUR_OF_DAY);

    calset(CalendarMINUTE);

    calset(CalendarSECOND);

    calset(CalendarMILLISECOND);

    DatestartOfDay=calgetTime();

时区(Time Zone)调整的误用

错误的写法

    publicstaticDateconvertTz(DatedateTimeZonetz){  

    Calendarcal=CalendargetInstance();  

    calsetTimeZone(TimeZonegetTimeZone(UTC));  

    calsetTime(date);  

    calsetTimeZone(tz);  

    returncalgetTime();  

    }

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

    CalendargetInstance()的误用

    错误的写法

      Calendarc=CalendargetInstance();

      cset(CalendarJANUARY);

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

    正确的写法

      Calendarc=newGregorianCalendar(timeZone);

      cset(CalendarJANUARY);

    DatesetTime()的误用

    错误的写法

      accountchangePassword(oldPassnewPass);

      Datelastmod=accountgetLastModified();

      lastmodsetTime(SystemcurrentTimeMillis());

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

    正确的做法

      accountchangePassword(oldPassnewPass);

      accountsetLastModified(newDate());

    SimpleDateFormat非线程安全误用

    错误的写法

      publicclassConstants{

      publicstaticfinalSimpleDateFormatdate=newSimpleDateFormat(ddMMyyyy);

      }

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

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

      publicinterfaceConstants{

      Stringversion=;

      StringdateFormat=ddMMyyyy;

      StringconfigFile=apprc;

      intmaxNameLength=;

      StringsomeQuery=SELECT*FROM;

      }

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

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

    忽略造型溢出(cast overflow)

    错误的写法

      publicintgetFileSize(Filef){

      longl=flength();

      return(int)l;

      }

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

    正确的写法

      publicintgetFileSize(Filef){

      longl=flength();

      if(l>IntegerMAX_VALUE)thrownewIllegalStateException(intoverflow);

      return(int)l;

      }

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

      longa=SystemcurrentTimeMillis();

      longb=a+;

      Systemoutprintln((int)ba);

      Systemoutprintln((int)(ba));

    对float和double使用==操作

    错误的写法

      for(floatf=f;f!=;f=){

      Systemoutprintln(f);

      }

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

    正确的写法

      for(floatf=f;f>;f=){

      Systemoutprintln(f);

      }

    用浮点数来保存money

    错误的写法

      floattotal=f;

      for(OrderLineline:lines){

      total+=lineprice*unt;

      }

      doublea=*;//将表示为

      Systemoutprintln(Mathround(a));//输出值为

      BigDecimald=newBigDecimal();//造成精度丢失

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

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

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

    正确的写法

      BigDecimaltotal=BigDecimalZERO;

      for(OrderLineline:lines){

      BigDecimalprice=newBigDecimal(lineprice);

      BigDecimalcount=newBigDecimal(unt);

      total=totaladd(pricemultiply(count));//BigDecimalisimmutable!

      }

      total=totalsetScale(RoundingModeHALF_UP);

      BigDecimala=(newBigDecimal())multiply(newBigDecimal());//exact

      a=asetScale(RoundingModeHALF_UP);//

      Systemoutprintln(a);//correctoutput:

      BigDecimala=newBigDecimal();

    不使用finally块释放资源

    错误的写法

      publicvoidsave(Filef)throwsIOException{

      OutputStreamout=newBufferedOutputStream(newFileOutputStream(f));

      outwrite();

      outclose();

      }

      publicvoidload(Filef)throwsIOException{

      InputStreamin=newBufferedInputStream(newFileInputStream(f));

      inread();

      inclose();

      }

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

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

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

      //codeforyourcookbook

      publicvoidsave()throwsIOException{

      Filef=

      OutputStreamout=newBufferedOutputStream(newFileOutputStream(f));

      try{

      outwrite();

      outflush();//dontloseexceptionbyimplicitflushonclose

      }finally{

      outclose();

      }

      }

      publicvoidload(Filef)throwsIOException{

      InputStreamin=newBufferedInputStream(newFileInputStream(f));

      try{

      inread();

      }finally{

      try{inclose();}catch(IOExceptione){}

      }

      }

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

      CargetCar(DataSourcedsStringplate)throwsSQLException{

      Carcar=null;

      Connectionc=null;

      PreparedStatements=null;

      ResultSetrs=null;

      try{

      c=dsgetConnection();

      s=cprepareStatement(selectmakecolorfromcarswhereplate=?);

      ssetString(plate);

      rs=sexecuteQuery();

      if(rsnext()){

      car=newCar();

      carmake=rsgetString();

      lor=rsgetString();

      }

      }finally{

      if(rs!=null)try{rsclose();}catch(SQLExceptione){}

      if(s!=null)try{sclose();}catch(SQLExceptione){}

      if(c!=null)try{cclose();}catch(SQLExceptione){}

      }

      returncar;

      }

    finalize方法误用

    错误的写法

      publicclassFileBackedCache{

      privateFilebackingStore;

      protectedvoidfinalize()throwsIOException{

      if(backingStore!=null){

      backingStoreclose();

      backingStore=null;

      }

      }

      }

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

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

      publicclassFileBackedCache{

      privateFilebackingStore;

      publicvoidclose()throwsIOException{

      if(backingStore!=null){

      backingStoreclose();

      backingStore=null;

      }

      }

      }

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

      try(Writerw=newFileWriter(f)){//implementsClosable

      wwrite(abc);

      //wgoesoutofscopehere:wclose()iscalledautomaticallyinANYcase

      }catch(IOExceptione){

      thrownewRuntimeException(egetMessage()e);

      }

    Threadinterrupted方法误用

    错误的写法

      try{

      Threadsleep();

      }catch(InterruptedExceptione){

      //ok

      }

      or

      while(true){

      if(Threadinterrupted())break;

      }

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

    正确的写法

      try{

      Threadsleep();

      }catch(InterruptedExceptione){

      ThreadcurrentThread()interrupt();

      }

      or

      while(true){

      if(ThreadcurrentThread()isInterrupted())break;

      }

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

    错误的写法

      classCache{

      privatestaticfinalTimerevictor=newTimer();

      }

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

    正确的做法

      classCache{

      privatestaticTimerevictor;

      publicstaticsetupEvictor(){

      evictor=newTimer();

      }

      }

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

    错误的写法

      finalMyClasscallback=this;

      TimerTasktask=newTimerTask(){

      publicvoidrun(){

      callbacktimeout();

      }

      };

      timerschedule(taskL);

      try{

      doSomething();

      }finally{

      taskcancel();

      }

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

    正确的写法

      TimerTasktask=newJob(this);

      timerschedule(taskL);

      try{

      doSomething();

      }finally{

      taskcancel();

      }

      staticclassJobextendsTimerTask{

      privateMyClasscallback;

      publicJob(MyClasscallback){

      thiscallback=callback;

      }

      publicbooleancancel(){

      callback=null;

      returnsupercancel();

      }

      publicvoidrun(){

      if(callback==null)return;

      callbacktimeout();

      }

      }

                   

    上一篇:Java做的比较完善的FTP连接上传下载文件

    下一篇:Linux下java的Swing/AWT程序乱码解决