游戏电视苹果数码历史美丽
投稿投诉
美丽时装
彩妆资讯
历史明星
乐活安卓
数码常识
驾车健康
苹果问答
网络发型
电视车载
室内电影
游戏科学
音乐整形

Java代码优化的30个小技巧

  前言
  我之前写过两篇关于优化相关的问题:《聊聊sql优化的15个小技巧》和《聊聊接口性能优化的11个小技巧》,发表之后,在全网受到广大网友的好评。阅读量和点赞率都很高,说明了这类文章的价值。
  今天接着优化这个话题,我们一起聊聊Java中代码优化的30个小技巧,希望会对你有所帮助。
  1。用String。format拼接字符串
  不知道你有没有拼接过字符串,特别是那种有多个参数,字符串比较长的情况。
  比如现在有个需求:要用get请求调用第三方接口,url后需要拼接多个参数。
  以前我们的请求地址是这样拼接的:Stringurlhttp:susan。sc。cn?userNameuserNameageageaddressaddresssexsexroledIdroleId;
  字符串使用号拼接,非常容易出错。
  后面优化了一下,改为使用StringBuilder拼接字符串:StringBuilderurlBuildernewStringBuilder(http:susan。sc。cn?);urlBuilder。append(userName)。append(userName)。append(age)。append(age)。append(address)。append(address)。append(sex)。append(sex)。append(roledId)。append(roledId);
  代码优化之后,稍微直观点。
  但还是看起来比较别扭。
  这时可以使用String。format方法优化:StringrequestUrlhttp:susan。sc。cn?userNamesagesaddressssexsroledIds;StringurlString。format(requestUrl,userName,age,address,sex,roledId);
  代码的可读性,一下子提升了很多。
  我们平常可以使用String。format方法拼接url请求参数,日志打印等字符串。
  但不建议在for循环中用它拼接字符串,因为它的执行效率,比使用号拼接字符串,或者使用StringBuilder拼接字符串都要慢一些。2。创建可缓冲的IO流
  IO流想必大家都使用得比较多,我们经常需要把数据写入某个文件,或者从某个文件中读取数据到内存中,甚至还有可能把文件a,从目录b,复制到目录c下等。
  JDK给我们提供了非常丰富的API,可以去操作IO流。
  例如:publicclassIoTest1{publicstaticvoidmain(String〔〕args){FileInputStreamfisnull;FileOutputStreamfosnull;try{FilesrcFilenewFile(UsersdvsusanDocumentsworkspacejumpsrcmainjavacomsuejumpservicetest11。txt);FiledestFilenewFile(UsersdvsusanDocumentsworkspacejumpsrcmainjavacomsuejumpservicetest12。txt);fisnewFileInputStream(srcFile);fosnewFileOutputStream(destFile);intlen;while((lenfis。read())!1){fos。write(len);}fos。flush();}catch(IOExceptione){e。printStackTrace();}finally{try{if(fos!null){fos。close();}}catch(IOExceptione){e。printStackTrace();}try{if(fis!null){fis。close();}}catch(IOExceptione){e。printStackTrace();}}}}
  这个例子主要的功能,是将1。txt文件中的内容复制到2。txt文件中。这例子使用普通的IO流从功能的角度来说,也能满足需求,但性能却不太好。
  因为这个例子中,从1。txt文件中读一个字节的数据,就会马上写入2。txt文件中,需要非常频繁的读写文件。
  优化:publicclassIoTest{publicstaticvoidmain(String〔〕args){BufferedInputStreambisnull;BufferedOutputStreambosnull;FileInputStreamfisnull;FileOutputStreamfosnull;try{FilesrcFilenewFile(UsersdvsusanDocumentsworkspacejumpsrcmainjavacomsuejumpservicetest11。txt);FiledestFilenewFile(UsersdvsusanDocumentsworkspacejumpsrcmainjavacomsuejumpservicetest12。txt);fisnewFileInputStream(srcFile);fosnewFileOutputStream(destFile);bisnewBufferedInputStream(fis);bosnewBufferedOutputStream(fos);byte〔〕buffernewbyte〔1024〕;intlen;while((lenbis。read(buffer))!1){bos。write(buffer,0,len);}bos。flush();}catch(IOExceptione){e。printStackTrace();}finally{try{if(bos!null){bos。close();}if(fos!null){fos。close();}}catch(IOExceptione){e。printStackTrace();}try{if(bis!null){bis。close();}if(fis!null){fis。close();}}catch(IOExceptione){e。printStackTrace();}}}}
  这个例子使用BufferedInputStream和BufferedOutputStream创建了可缓冲的输入输出流。
  最关键的地方是定义了一个buffer字节数组,把从1。txt文件中读取的数据临时保存起来,后面再把该buffer字节数组的数据,一次性批量写入到2。txt中。
  这样做的好处是,减少了读写文件的次数,而我们都知道读写文件是非常耗时的操作。也就是说使用可缓存的输入输出流,可以提升IO的性能,特别是遇到文件非常大时,效率会得到显著提升。3。减少循环次数
  在我们日常开发中,循环遍历集合是必不可少的操作。
  但如果循环层级比较深,循环中套循环,可能会影响代码的执行效率。
  反例:for(Useruser:userList){for(Rolerole:roleList){if(user。getRoleId()。equals(role。getId())){user。setRoleName(role。getName());}}}
  这个例子中有两层循环,如果userList和roleList数据比较多的话,需要循环遍历很多次,才能获取我们所需要的数据,非常消耗cpu资源。
  正例:MapLong,ListRoleroleMaproleList。stream()。collect(Collectors。groupingBy(Role::getId));for(Useruser:userList){ListRolerolesroleMap。get(user。getRoleId());if(CollectionUtils。isNotEmpty(roles)){user。setRoleName(roles。get(0)。getName());}}
  减少循环次数,最简单的办法是,把第二层循环的集合变成map,这样可以直接通过key,获取想要的value数据。
  虽说map的key存在hash冲突的情况,但遍历存放数据的链表或者红黑树的时间复杂度,比遍历整个list集合要小很多。4。用完资源记得及时关闭
  在我们日常开发中,可能经常访问资源,比如:获取数据库连接,读取文件等。
  我们以获取数据库连接为例。
  反例:1。加载驱动类Class。forName(com。mysql。jdbc。Driver);2。创建连接ConnectionconnectionDriverManager。getConnection(jdbc:mysqllocalhost:3306db?allowMultiQueriestrueuseUnicodetruecharacterEncodingUTF8,root,123456);3。编写sqlStringsqlselectfromuser;4。创建PreparedStatementPreparedStatementpstmtconn。prepareStatement(sql);5。获取查询结果ResultSetrspstmt。execteQuery();while(rs。next()){intidrs。getInt(id);Stringnamers。getString(name);}
  上面这段代码可以正常运行,但却犯了一个很大的错误,即:ResultSet、PreparedStatement和Connection对象的资源,使用完之后,没有关闭。
  我们都知道,数据库连接是非常宝贵的资源。我们不可能一直创建连接,并且用完之后,也不回收,白白浪费数据库资源。
  正例:1。加载驱动类Class。forName(com。mysql。jdbc。Driver);Connectionconnectionnull;PreparedStatementpstmtnull;ResultSetrsnull;try{2。创建连接connectionDriverManager。getConnection(jdbc:mysqllocalhost:3306db?allowMultiQueriestrueuseUnicodetruecharacterEncodingUTF8,root,123456);3。编写sqlStringsqlselectfromuser;4。创建PreparedStatementpstmtconn。prepareStatement(sql);5。获取查询结果rspstmt。execteQuery();while(rs。next()){intidrs。getInt(id);Stringnamers。getString(name);}}catch(Exceptione){log。error(e。getMessage(),e);}finally{if(rs!null){rs。close();}if(pstmt!null){pstmt。close();}if(connection!null){connection。close();}}
  这个例子中,无论是ResultSet,或者PreparedStatement,还是Connection对象,使用完之后,都会调用close方法关闭资源。
  在这里温馨提醒一句:ResultSet,或者PreparedStatement,还是Connection对象,这三者关闭资源的顺序不能反了,不然可能会出现异常。5。使用池技术
  我们都知道,从数据库查数据,首先要连接数据库,获取Connection资源。
  想让程序多线程执行,需要使用Thread类创建线程,线程也是一种资源。
  通常一次数据库操作的过程是这样的:创建连接进行数据库操作关闭连接
  而创建连接和关闭连接,是非常耗时的操作,创建连接需要同时会创建一些资源,关闭连接时,需要回收那些资源。
  如果用户的每一次数据库请求,程序都都需要去创建连接和关闭连接的话,可能会浪费大量的时间。
  此外,可能会导致数据库连接过多。
  我们都知道数据库的最大连接数是有限的,以mysql为例,最大连接数是:100,不过可以通过参数调整这个数量。
  如果用户请求的连接数超过最大连接数,就会报:toomanyconnections异常。如果有新的请求过来,会发现数据库变得不可用。
  这时可以通过命令:showvariableslikemaxconnections
  查看最大连接数。
  然后通过命令:setGLOBALmaxconnections1000
  手动修改最大连接数。
  这种做法只能暂时缓解问题,不是一个好的方案,无法从根本上解决问题。
  最大的问题是:数据库连接数可以无限增长,不受控制。
  这时我们可以使用数据库连接池。
  目前Java开源的数据库连接池有:DBCP:是一个依赖Jakartacommonspool对象池机制的数据库连接池。C3P0:是一个开放源代码的JDBC连接池,它在lib目录中与Hibernate一起发布,包括了实现jdbc3和jdbc2扩展规范说明的Connection和Statement池的DataSources对象。Druid:阿里的Druid,不仅是一个数据库连接池,还包含一个ProxyDriver、一系列内置的JDBC组件库、一个SQLParser。Proxool:是一个JavaSQLDriver驱动程序,它提供了对选择的其它类型的驱动程序的连接池封装,可以非常简单的移植到已有代码中。
  目前用的最多的数据库连接池是:Druid。6。反射时加缓存
  我们都知道通过反射创建对象实例,比使用new关键字要慢很多。
  由此,不太建议在用户请求过来时,每次都通过反射实时创建实例。
  有时候,为了代码的灵活性,又不得不用反射创建实例,这时该怎么办呢?
  答:加缓存。
  其实spring中就使用了大量的反射,我们以支付方法为例。
  根据前端传入不同的支付code,动态找到对应的支付方法,发起支付。
  我们先定义一个注解。Retention(RetentionPolicy。RUNTIME)Target(ElementType。TYPE)publicinterfacePayCode{Stringvalue();Stringname();}
  在所有的支付类上都加上该注解PayCode(valuealia,name支付宝支付)ServicepublicclassAliaPayimplementsIPay{Overridepublicvoidpay(){System。out。println(发起支付宝支付);}}PayCode(valueweixin,name微信支付)ServicepublicclassWeixinPayimplementsIPay{Overridepublicvoidpay(){System。out。println(发起微信支付);}}PayCode(valuejingdong,name京东支付)ServicepublicclassJingDongPayimplementsIPay{Overridepublicvoidpay(){System。out。println(发起京东支付);}}
  然后增加最关键的类:ServicepublicclassPayService2implementsApplicationListenerContextRefreshedEvent{privatestaticMapString,IPaypayMapnull;OverridepublicvoidonApplicationEvent(ContextRefreshedEventcontextRefreshedEvent){ApplicationContextapplicationContextcontextRefreshedEvent。getApplicationContext();MapString,ObjectbeansWithAnnotationapplicationContext。getBeansWithAnnotation(PayCode。class);if(beansWithAnnotation!null){payMapnewHashMap();beansWithAnnotation。forEach((key,value){StringbizTypevalue。getClass()。getAnnotation(PayCode。class)。value();payMap。put(bizType,(IPay)value);});}}publicvoidpay(Stringcode){payMap。get(code)。pay();}}
  PayService2类实现了ApplicationListener接口,这样在onApplicationEvent方法中,就可以拿到ApplicationContext的实例。这一步,其实是在spring容器启动的时候,spring通过反射我们处理好了。
  我们再获取打了PayCode注解的类,放到一个map中,map中的key就是PayCode注解中定义的value,跟code参数一致,value是支付类的实例。
  这样,每次就可以每次直接通过code获取支付类实例,而不用if。。。else判断了。如果要加新的支付方法,只需在支付类上面打上PayCode注解定义一个新的code即可。
  注意:这种方式的code可以没有业务含义,可以是纯数字,只要不重复就行。7。多线程处理
  很多时候,我们需要在某个接口中,调用其他服务的接口。
  比如有这样的业务场景:
  在用户信息查询接口中需要返回:用户名称、性别、等级、头像、积分、成长值等信息。
  而用户名称、性别、等级、头像在用户服务中,积分在积分服务中,成长值在成长值服务中。为了汇总这些数据统一返回,需要另外提供一个对外接口服务。
  于是,用户信息查询接口需要调用用户查询接口、积分查询接口和成长值查询接口,然后汇总数据统一返回。
  调用过程如下图所示:
  调用远程接口总耗时530ms200ms150ms180ms
  显然这种串行调用远程接口性能是非常不好的,调用远程接口总的耗时为所有的远程接口耗时之和。
  那么如何优化远程接口性能呢?
  上面说到,既然串行调用多个远程接口性能很差,为什么不改成并行呢?
  如下图所示:
  调用远程接口总耗时200ms200ms(即耗时最长的那次远程接口调用)
  在java8之前可以通过实现Callable接口,获取线程返回结果。
  java8以后通过CompleteFuture类实现该功能。我们这里以CompleteFuture为例:publicUserInfogetUserInfo(Longid)throwsInterruptedException,ExecutionException{finalUserInfouserInfonewUserInfo();CompletableFutureuserFutureCompletableFuture。supplyAsync((){getRemoteUserAndFill(id,userInfo);returnBoolean。TRUE;},executor);CompletableFuturebonusFutureCompletableFuture。supplyAsync((){getRemoteBonusAndFill(id,userInfo);returnBoolean。TRUE;},executor);CompletableFuturegrowthFutureCompletableFuture。supplyAsync((){getRemoteGrowthAndFill(id,userInfo);returnBoolean。TRUE;},executor);CompletableFuture。allOf(userFuture,bonusFuture,growthFuture)。join();userFuture。get();bonusFuture。get();growthFuture。get();returnuserInfo;}
  温馨提醒一下,这两种方式别忘了使用线程池。示例中我用到了executor,表示自定义的线程池,为了防止高并发场景下,出现线程过多的问题。8。懒加载
  有时候,创建对象是一个非常耗时的操作,特别是在该对象的创建过程中,还需要创建很多其他的对象时。
  我们以单例模式为例。
  在介绍单例模式的时候,必须要先介绍它的两种非常著名的实现方式:饿汉模式和懒汉模式。8。1饿汉模式
  实例在初始化的时候就已经建好了,不管你有没有用到,先建好了再说。具体代码如下:publicclassSimpleSingleton{持有自己类的引用privatestaticfinalSimpleSingletonINSTANCEnewSimpleSingleton();私有的构造方法privateSimpleSingleton(){}对外提供获取实例的静态方法publicstaticSimpleSingletongetInstance(){returnINSTANCE;}}
  使用饿汉模式的好处是:没有线程安全的问题,但带来的坏处也很明显。privatestaticfinalSimpleSingletonINSTANCEnewSimpleSingleton();
  一开始就实例化对象了,如果实例化过程非常耗时,并且最后这个对象没有被使用,不是白白造成资源浪费吗?
  还真是啊。
  这个时候你也许会想到,不用提前实例化对象,在真正使用的时候再实例化不就可以了?
  这就是我接下来要介绍的:懒汉模式。8。2懒汉模式
  顾名思义就是实例在用到的时候才去创建,比较懒,用的时候才去检查有没有实例,如果有则返回,没有则新建。具体代码如下:publicclassSimpleSingleton2{privatestaticSimpleSingleton2INSTANCE;privateSimpleSingleton2(){}publicstaticSimpleSingleton2getInstance(){if(INSTANCEnull){INSTANCEnewSimpleSingleton2();}returnINSTANCE;}}
  示例中的INSTANCE对象一开始是空的,在调用getInstance方法才会真正实例化。
  懒汉模式相对于饿汉模式,没有提前实例化对象,在真正使用的时候再实例化,在实例化对象的阶段效率更高一些。
  除了单例模式之外,懒加载的思想,使用比较多的可能是:spring的Lazy注解。在spring容器启动的时候,不会调用其getBean方法初始化实例。mybatis的懒加载。在mybatis做级联查询的时候,比如查用户的同时需要查角色信息。如果用了懒加载,先只查用户信息,真正使用到角色了,才取查角色信息。9。初始化集合时指定大小
  我们在实际项目开发中,需要经常使用集合,比如:ArrayList、HashMap等。
  但有个问题:你在初始化集合时指定了大小的吗?
  反例:publicclassTest2{publicstaticvoidmain(String〔〕args){ListIntegerlistnewArrayList();longtime1System。currentTimeMillis();for(inti0;i100000;i){list。add(i);}System。out。println(System。currentTimeMillis()time1);}}
  执行时间:12
  如果在初始化集合时指定了大小。
  正例:publicclassTest2{publicstaticvoidmain(String〔〕args){ListIntegerlist2newArrayList(100000);longtime2System。currentTimeMillis();for(inti0;i100000;i){list2。add(i);}System。out。println(System。currentTimeMillis()time2);}}
  执行时间:6
  我们惊奇的发现,在创建集合时指定了大小,比没有指定大小,添加10万个元素的效率提升了一倍。
  如果你看过ArrayList源码,你就会发现它的默认大小是10,如果添加元素超过了一定的阀值,会按1。5倍的大小扩容。
  你想想,如果装10万条数据,需要扩容多少次呀?而每次扩容都需要不停的复制元素,从老集合复制到新集合中,需要浪费多少时间呀。10。不要满屏try。。。catch异常
  以前我们在开发接口时,如果出现异常,为了给用户一个更友好的提示,例如:RequestMapping(test)RestControllerpublicclassTestController{GetMapping(add)publicStringadd(){inta100;return成功;}}
  如果不做任何处理,当我们请求add接口时,执行结果直接报错:
  what?用户能直接看到错误信息?
  这种交互方式给用户的体验非常差,为了解决这个问题,我们通常会在接口中捕获异常:GetMapping(add)publicStringadd(){Stringresult成功;try{inta100;}catch(Exceptione){result数据异常;}returnresult;}
  接口改造后,出现异常时会提示:数据异常,对用户来说更友好。
  看起来挺不错的,但是有问题
  如果只是一个接口还好,但是如果项目中有成百上千个接口,都要加上异常捕获代码吗?
  答案是否定的,这时全局异常处理就派上用场了:RestControllerAdvice。RestControllerAdvicepublicclassGlobalExceptionHandler{ExceptionHandler(Exception。class)publicStringhandleException(Exceptione){if(einstanceofArithmeticException){return数据异常;}if(einstanceofException){return服务器内部异常;}returnnull;}}
  只需在handleException方法中处理异常情况,业务接口中可以放心使用,不再需要捕获异常(有人统一处理了)。真是爽歪歪。11。位运算效率更高
  如果你读过JDK的源码,比如:ThreadLocal、HashMap等类,你就会发现,它们的底层都用了位运算。
  为什么开发JDK的大神们,都喜欢用位运算?
  答:因为位运算的效率更高。
  在ThreadLocal的get、set、remove方法中都有这样一行代码:intikey。threadLocalHashCode(len1);
  通过key的hashCode值,与数组的长度减1。其中key就是ThreadLocal对象,与数组的长度减1,相当于除以数组的长度减1,然后取模。
  这是一种hash算法。
  接下来给大家举个例子:假设len16,key。threadLocalHashCode31,
  于是:inti311515
  相当于:inti311615
  计算的结果是一样的,但是使用与运算效率跟高一些。
  为什么与运算效率更高?
  答:因为ThreadLocal的初始大小是16,每次都是按2倍扩容,数组的大小其实一直都是2的n次方。
  这种数据有个规律就是高位是0,低位都是1。在做与运算时,可以不用考虑高位,因为与运算的结果必定是0。只需考虑低位的与运算,所以效率更高。12。巧用第三方工具类
  在Java的庞大体系中,其实有很多不错的小工具,也就是我们平常说的:轮子。
  如果在我们的日常工作当中,能够将这些轮子用户,再配合一下idea的快捷键,可以极大得提升我们的开发效率。
  如果你引入com。google。guava的pom文件,会获得很多好用的小工具。这里推荐一款com。google。common。collect包下的集合工具:Lists。
  它是在太好用了,让我爱不释手。
  如果你想将一个大集合分成若干个小集合。
  之前我们是这样做的:ListIntegerlistLists。newArrayList(1,2,3,4,5);ListListIntegerpartitionListLists。newArrayList();intsize0;ListIntegerdataListLists。newArrayList();for(Integerdata:list){if(size2){dataListLists。newArrayList();size0;}size;dataList。add(data);}
  将list按size2分成多个小集合,上面的代码看起来比较麻烦。
  如果使用Lists的partition方法,可以这样写代码:ListIntegerlistLists。newArrayList(1,2,3,4,5);ListListIntegerpartitionListLists。partition(list,2);System。out。println(partitionList);
  执行结果:〔〔1,2〕,〔3,4〕,〔5〕〕
  这个例子中,list有5条数据,我将list集合按大小为2,分成了3页,即变成3个小集合。
  这个是我最喜欢的方法之一,经常在项目中使用。
  比如有个需求:现在有5000个id,需要调用批量用户查询接口,查出用户数据。但如果你直接查5000个用户,单次接口响应时间可能会非常慢。如果改成分页处理,每次只查500个用户,异步调用10次接口,就不会有单次接口响应慢的问题。
  如果你了解更多非常有用的第三方工具类的话,可以看看我的另一篇文章《吐血推荐17个提升开发效率的轮子》。13。用同步代码块代替同步方法
  在某些业务场景中,为了防止多个线程并发修改某个共享数据,造成数据异常。
  为了解决并发场景下,多个线程同时修改数据,造成数据不一致的情况。通常情况下,我们会:加锁。
  但如果锁加得不好,导致锁的粒度太粗,也会非常影响接口性能。
  在java中提供了synchronized关键字给我们的代码加锁。
  通常有两种写法:在方法上加锁和在代码块上加锁。
  先看看如何在方法上加锁:publicsynchronizeddoSave(StringfileUrl){mkdir();uploadFile(fileUrl);sendMessage(fileUrl);}
  这里加锁的目的是为了防止并发的情况下,创建了相同的目录,第二次会创建失败,影响业务功能。
  但这种直接在方法上加锁,锁的粒度有点粗。因为doSave方法中的上传文件和发消息方法,是不需要加锁的。只有创建目录方法,才需要加锁。
  我们都知道文件上传操作是非常耗时的,如果将整个方法加锁,那么需要等到整个方法执行完之后才能释放锁。显然,这会导致该方法的性能很差,变得得不偿失。
  这时,我们可以改成在代码块上加锁了,具体代码如下:publicvoiddoSave(Stringpath,StringfileUrl){synchronized(this){if(!exists(path)){mkdir(path);}}uploadFile(fileUrl);sendMessage(fileUrl);}
  这样改造之后,锁的粒度一下子变小了,只有并发创建目录功能才加了锁。而创建目录是一个非常快的操作,即使加锁对接口的性能影响也不大。
  最重要的是,其他的上传文件和发送消息功能,任然可以并发执行。14。不用的数据及时清理
  在Java中保证线程安全的技术有很多,可以使用synchroized、Lock等关键字给代码块加锁。
  但是它们有个共同的特点,就是加锁会对代码的性能有一定的损耗。
  其实,在jdk中还提供了另外一种思想即:用空间换时间。
  没错,使用ThreadLocal类就是对这种思想的一种具体体现。
  ThreadLocal为每个使用变量的线程提供了一个独立的变量副本,这样每一个线程都能独立地改变自己的副本,而不会影响其它线程所对应的副本。
  ThreadLocal的用法大致是这样的:先创建一个CurrentUser类,其中包含了ThreadLocal的逻辑。publicclassCurrentUser{privatestaticfinalThreadLocalUserInfoTHREALOCALnewThreadLocal();publicstaticvoidset(UserInfouserInfo){THREALOCAL。set(userInfo);}publicstaticUserInfoget(){THREALOCAL。get();}publicstaticvoidremove(){THREALOCAL。remove();}}在业务代码中调用CurrentUser类。publicvoiddoSamething(UserDtouserDto){UserInfouserInfoconvert(userDto);CurrentUser。set(userInfo);。。。业务代码UserInfouserInfoCurrentUser。get();。。。}
  在业务代码的第一行,将userInfo对象设置到CurrentUser,这样在业务代码中,就能通过CurrentUser。get()获取到刚刚设置的userInfo对象。特别是对业务代码调用层级比较深的情况,这种用法非常有用,可以减少很多不必要传参。
  但在高并发的场景下,这段代码有问题,只往ThreadLocal存数据,数据用完之后并没有及时清理。
  ThreadLocal即使使用了WeakReference(弱引用)也可能会存在内存泄露问题,因为entry对象中只把key(即threadLocal对象)设置成了弱引用,但是value值没有。
  那么,如何解决这个问题呢?publicvoiddoSamething(UserDtouserDto){UserInfouserInfoconvert(userDto);try{CurrentUser。set(userInfo);。。。业务代码UserInfouserInfoCurrentUser。get();。。。}finally{CurrentUser。remove();}}
  需要在finally代码块中,调用remove方法清理没用的数据。15。用equals方法比较是否相等
  不知道你在项目中有没有见过,有些同事对Integer类型的两个参数使用号比较是否相等?
  反正我见过的,那么这种用法对吗?
  我的回答是看具体场景,不能说一定对,或不对。
  有些状态字段,比如:orderStatus有:1(未下单),0(已下单),1(已支付),2(已完成),3(取消),5种状态。
  这时如果用判断是否相等:IntegerorderStatus1newInteger(1);IntegerorderStatus2newInteger(1);System。out。println(orderStatus1orderStatus2);
  返回结果会是true吗?
  答案:是false。
  有些同学可能会反驳,Integer中不是有范围是:128127的缓存吗?
  为什么是false?
  先看看Integer的构造方法:
  它其实并没有用到缓存。
  那么缓存是在哪里用的?
  答案在valueOf方法中:
  如果上面的判断改成这样:StringorderStatus1newString(1);StringorderStatus2newString(1);System。out。println(Integer。valueOf(orderStatus1)Integer。valueOf(orderStatus2));
  返回结果会是true吗?
  答案:还真是true。
  我们要养成良好编码习惯,尽量少用判断两个Integer类型数据是否相等,只有在上述非常特殊的场景下才相等。
  而应该改成使用equals方法判断:IntegerorderStatus1newInteger(1);IntegerorderStatus2newInteger(1);System。out。println(orderStatus1。equals(orderStatus2));
  运行结果为true。16。避免创建大集合
  很多时候,我们在日常开发中,需要创建集合。比如:为了性能考虑,从数据库查询某张表的所有数据,一次性加载到内存的某个集合中,然后做业务逻辑处理。
  例如:ListUseruserListuserMapper。getAllUser();for(Useruser:userList){doSamething();}
  从数据库一次性查询出所有用户,然后在循环中,对每个用户进行业务逻辑处理。
  如果用户表的数据量非常多时,这样userList集合会很大,可能直接导致内存不足,而使整个应用挂掉。
  针对这种情况,必须做分页处理。
  例如:privatestaticfinalintPAGESIZE500;intcurrentPage1;RequestPagepagenewRequestPage();page。setPageNo(currentPage);page。setPageSize(PAGESIZE);PageUserpageUseruserMapper。search(page);while(pageUser。getPageCount()currentPage){for(Useruser:pageUser。getData()){doSamething();}page。setPageNo(currentPage);pageUseruserMapper。search(page);}
  通过上面的分页改造之后,每次从数据库中只查询500条记录,保存到userList集合中,这样userList不会占用太多的内存。
  这里特别说明一下,如果你查询的表中的数据量本来就很少,一次性保存到内存中,也不会占用太多内存,这种情况也可以不做分页处理。
  此外,还有中特殊的情况,即表中的记录数并算不多,但每一条记录,都有很多字段,单条记录就占用很多内存空间,这时也需要做分页处理,不然也会有问题。
  整体的原则是要尽量避免创建大集合,导致内存不足的问题,但是具体多大才算大集合。目前没有一个唯一的衡量标准,需要结合实际的业务场景进行单独分析。17。状态用枚举
  在我们建的表中,有很多状态字段,比如:订单状态、禁用状态、删除状态等。
  每种状态都有多个值,代表不同的含义。
  比如订单状态有:1:表示下单2:表示支付3:表示完成4:表示撤销
  如果没有使用枚举,一般是这样做的:publicstaticfinalintORDERSTATUSCREATE1;publicstaticfinalintORDERSTATUSPAY2;publicstaticfinalintORDERSTATUSDONE3;publicstaticfinalintORDERSTATUSCANCEL4;publicstaticfinalStringORDERSTATUSCREATEMESSAGE下单;publicstaticfinalStringORDERSTATUSPAY下单;publicstaticfinalStringORDERSTATUSDONE下单;publicstaticfinalStringORDERSTATUSCANCEL下单;
  需要定义很多静态常量,包含不同的状态和状态的描述。
  使用枚举定义之后,代码如下:publicenumOrderStatusEnum{CREATE(1,下单),PAY(2,支付),DONE(3,完成),CANCEL(4,撤销);privateintcode;privateStringmessage;OrderStatusEnum(intcode,Stringmessage){this。codecode;this。messagemessage;}publicintgetCode(){returnthis。code;}publicStringgetMessage(){returnthis。message;}publicstaticOrderStatusEnumgetOrderStatusEnum(intcode){returnArrays。stream(OrderStatusEnum。values())。filter(xx。codecode)。findFirst()。orElse(null);}}
  使用枚举改造之后,职责更单一了。
  而且使用枚举的好处是:代码的可读性变强了,不同的状态,有不同的枚举进行统一管理和维护。枚举是天然单例的,可以直接使用号进行比较。code和message可以成对出现,比较容易相关转换。枚举可以消除if。。。else过多问题。18。把固定值定义成静态常量
  不知道你在实际的项目开发中,有没有使用过固定值?
  例如:if(user。getId()1000L){doSamething();}
  或者:if(Objects。isNull(user)){thrownewBusinessException(该用户不存在);}
  其中1000L和该用户不存在是固定值,每次都是一样的。
  既然是固定值,我们为什么不把它们定义成静态常量呢?
  这样语义上更直观,方便统一管理和维护,更方便代码复用。
  代码优化为:privatestaticfinalintDEFAULTUSERID1000L;。。。if(user。getId()DEFAULTUSERID){doSamething();}
  或者:privatestaticfinalStringNOTFOUNDMESSAGE该用户不存在;。。。if(Objects。isNull(user)){thrownewBusinessException(NOTFOUNDMESSAGE);}
  使用staticfinal关键字修饰静态常量,static表示静态的意思,即类变量,而final表示不允许修改。
  两个关键字加在一起,告诉Java虚拟机这种变量,在内存中只有一份,在全局上是唯一的,不能修改,也就是静态常量。19。避免大事务
  很多小伙伴在使用spring框架开发项目时,为了方便,喜欢使用Transactional注解提供事务功能。
  没错,使用Transactional注解这种声明式事务的方式提供事务功能,确实能少写很多代码,提升开发效率。
  但也容易造成大事务,引发其他的问题。
  下面用一张图看看大事务引发的问题。
  从图中能够看出,大事务问题可能会造成接口超时,对接口的性能有直接的影响。
  我们该如何优化大事务呢?少用Transactional注解将查询(select)方法放到事务外事务中避免远程调用事务中避免一次性处理太多数据有些功能可以非事务执行有些功能可以异步处理
  关于大事务问题我的另一篇文章《让人头痛的大事务问题到底要如何解决?》,它里面做了非常详细的介绍,如果大家感兴趣可以看看。20。消除过长的if。。。else
  我们在写代码的时候,if。。。else的判断条件是必不可少的。不同的判断条件,走的代码逻辑通常会不一样。
  废话不多说,先看看下面的代码。publicinterfaceIPay{voidpay();}ServicepublicclassAliaPayimplementsIPay{Overridepublicvoidpay(){System。out。println(发起支付宝支付);}}ServicepublicclassWeixinPayimplementsIPay{Overridepublicvoidpay(){System。out。println(发起微信支付);}}ServicepublicclassJingDongPayimplementsIPay{Overridepublicvoidpay(){System。out。println(发起京东支付);}}ServicepublicclassPayService{AutowiredprivateAliaPayaliaPay;AutowiredprivateWeixinPayweixinPay;AutowiredprivateJingDongPayjingDongPay;publicvoidtoPay(Stringcode){if(alia。equals(code)){aliaPay。pay();}elseif(weixin。equals(code)){weixinPay。pay();}elseif(jingdong。equals(code)){jingDongPay。pay();}else{System。out。println(找不到支付方式);}}}
  PayService类的toPay方法主要是为了发起支付,根据不同的code,决定调用用不同的支付类(比如:aliaPay)的pay方法进行支付。
  这段代码有什么问题呢?也许有些人就是这么干的。
  试想一下,如果支付方式越来越多,比如:又加了百度支付、美团支付、银联支付等等,就需要改toPay方法的代码,增加新的else。。。if判断,判断多了就会导致逻辑越来越多?
  很明显,这里违法了设计模式六大原则的:开闭原则和单一职责原则。
  开闭原则:对扩展开放,对修改关闭。就是说增加新功能要尽量少改动已有代码。
  单一职责原则:顾名思义,要求逻辑尽量单一,不要太复杂,便于复用。
  那么,如何优化if。。。else判断呢?
  答:使用策略模式工厂模式。
  策略模式定义了一组算法,把它们一个个封装起来,并且使它们可相互替换。工厂模式用于封装和管理对象的创建,是一种创建型模式。publicinterfaceIPay{voidpay();}ServicepublicclassAliaPayimplementsIPay{PostConstructpublicvoidinit(){PayStrategyFactory。register(aliaPay,this);}Overridepublicvoidpay(){System。out。println(发起支付宝支付);}}ServicepublicclassWeixinPayimplementsIPay{PostConstructpublicvoidinit(){PayStrategyFactory。register(weixinPay,this);}Overridepublicvoidpay(){System。out。println(发起微信支付);}}ServicepublicclassJingDongPayimplementsIPay{PostConstructpublicvoidinit(){PayStrategyFactory。register(jingDongPay,this);}Overridepublicvoidpay(){System。out。println(发起京东支付);}}publicclassPayStrategyFactory{privatestaticMapString,IPayPAYREGISTERSnewHashMap();publicstaticvoidregister(Stringcode,IPayiPay){if(null!code!。equals(code)){PAYREGISTERS。put(code,iPay);}}publicstaticIPayget(Stringcode){returnPAYREGISTERS。get(code);}}ServicepublicclassPayService3{publicvoidtoPay(Stringcode){PayStrategyFactory。get(code)。pay();}}
  这段代码的关键是PayStrategyFactory类,它是一个策略工厂,里面定义了一个全局的map,在所有IPay的实现类中注册当前实例到map中,然后在调用的地方通过PayStrategyFactory类根据code从map获取支付类实例即可。
  如果加了一个新的支付方式,只需新加一个类实现IPay接口,定义init方法,并且重写pay方法即可,其他代码基本上可以不用动。
  当然,消除又臭又长的if。。。else判断,还有很多方法,比如:使用注解、动态拼接类名称、模板方法、枚举等等。由于篇幅有限,在这里我就不过多介绍了,更详细的内容可以看看我的另一篇文章《消除if。。。else是9条锦囊妙计》21。防止死循环
  有些小伙伴看到这个标题,可能会感到有点意外,代码中不是应该避免死循环吗?为啥还是会产生死循环?
  殊不知有些死循环是我们自己写的,例如下面这段代码:while(true){if(condition){break;}System。out。println(dosamething);}
  这里使用了while(true)的循环调用,这种写法在CAS自旋锁中使用比较多。
  当满足condition等于true的时候,则自动退出该循环。
  如果condition条件非常复杂,一旦出现判断不正确,或者少写了一些逻辑判断,就可能在某些场景下出现死循环的问题。
  出现死循环,大概率是开发人员人为的bug导致的,不过这种情况很容易被测出来。
  还有一种隐藏的比较深的死循环,是由于代码写的不太严谨导致的。如果用正常数据,可能测不出问题,但一旦出现异常数据,就会立即出现死循环。
  其实,还有另一种死循环:无限递归。
  如果想要打印某个分类的所有父分类,可以用类似这样的递归方法实现:publicvoidprintCategory(Categorycategory){if(categorynullcategory。getParentId()null){return;}System。out。println(父分类名称:category。getName());CategoryparentcategoryMapper。getCategoryById(category。getParentId());printCategory(parent);}
  正常情况下,这段代码是没有问题的。
  但如果某次有人误操作,把某个分类的parentId指向了它自己,这样就会出现无限递归的情况。导致接口一直不能返回数据,最终会发生堆栈溢出。
  建议写递归方法时,设定一个递归的深度,比如:分类最大等级有4级,则深度可以设置为4。然后在递归方法中做判断,如果深度大于4时,则自动返回,这样就能避免无限循环的情况。22。注意BigDecimal的坑
  通常我们会把一些小数类型的字段(比如:金额),定义成BigDecimal,而不是Double,避免丢失精度问题。
  使用Double时可能会有这种场景:doubleamount10。02;doubleamount20。03;System。out。println(amount2amount1);
  正常情况下预计amount2amount1应该等于0。01
  但是执行结果,却为:0。009999999999999998
  实际结果小于预计结果。
  Double类型的两个参数相减会转换成二进制,因为Double有效位数为16位这就会出现存储小数位数不够的情况,这种情况下就会出现误差。
  常识告诉我们使用BigDecimal能避免丢失精度。
  但是使用BigDecimal能避免丢失精度吗?
  答案是否定的。
  为什么?BigDecimalamount1newBigDecimal(0。02);BigDecimalamount2newBigDecimal(0。03);System。out。println(amount2。subtract(amount1));
  这个例子中定义了两个BigDecimal类型参数,使用构造函数初始化数据,然后打印两个参数相减后的值。
  结果:0。0099999999999999984734433411404097569175064563751220703125
  不科学呀,为啥还是丢失精度了?
  Jdk中BigDecimal的构造方法上有这样一段描述:
  大致的意思是此构造函数的结果可能不可预测,可能会出现创建时为0。1,但实际是0。1000000000000000055511151231257827021181583404541015625的情况。
  由此可见,使用BigDecimal构造函数初始化对象,也会丢失精度。
  那么,如何才能不丢失精度呢?BigDecimalamount1newBigDecimal(Double。toString(0。02));BigDecimalamount2newBigDecimal(Double。toString(0。03));System。out。println(amount2。subtract(amount1));
  我们可以使用Double。toString方法,对double类型的小数进行转换,这样能保证精度不丢失。
  其实,还有更好的办法:BigDecimalamount1BigDecimal。valueOf(0。02);BigDecimalamount2BigDecimal。valueOf(0。03);System。out。println(amount2。subtract(amount1));
  使用BigDecimal。valueOf方法初始化BigDecimal类型参数,也能保证精度不丢失。在新版的阿里巴巴开发手册中,也推荐使用这种方式创建BigDecimal参数。23。尽可能复用代码
  ctrlc和ctrlv可能是程序员使用最多的快捷键了。
  没错,我们是大自然的搬运工。哈哈哈。
  在项目初期,我们使用这种工作模式,确实可以提高一些工作效率,可以少写(实际上是少敲)很多代码。
  但它带来的问题是:会出现大量的代码重复。例如:ServiceSlf4jpublicclassTestService1{publicvoidtest1(){addLog(test1);}privatevoidaddLog(Stringinfo){if(log。isInfoEnabled()){log。info(info:{},info);}}}ServiceSlf4jpublicclassTestService2{publicvoidtest2(){addLog(test2);}privatevoidaddLog(Stringinfo){if(log。isInfoEnabled()){log。info(info:{},info);}}}ServiceSlf4jpublicclassTestService3{publicvoidtest3(){addLog(test3);}privatevoidaddLog(Stringinfo){if(log。isInfoEnabled()){log。info(info:{},info);}}}
  在TestService1、TestService2、TestService3类中,都有一个addLog方法用于添加日志。
  本来该功能用得好好的,直到有一天,线上出现了一个事故:服务器磁盘满了。
  原因是打印的日志太多,记了很多没必要的日志,比如:查询接口的所有返回值,大对象的具体打印等。
  没办法,只能将addLog方法改成只记录debug日志。
  于是乎,你需要全文搜索,addLog方法去修改,改成如下代码:privatevoidaddLog(Stringinfo){if(log。isDebugEnabled()){log。debug(debug:{},info);}}
  这里是有三个类中需要修改这段代码,但如果实际工作中有三十个、三百个类需要修改,会让你非常痛苦。改错了,或者改漏了,都会埋下隐患,把自己坑了。
  为何不把这种功能的代码提取出来,放到某个工具类中呢?Slf4jpublicclassLogUtil{privateLogUtil(){thrownewRuntimeException(初始化失败);}publicstaticvoidaddLog(Stringinfo){if(log。isDebugEnabled()){log。debug(debug:{},info);}}}
  然后,在其他的地方,只需要调用。ServiceSlf4jpublicclassTestService1{publicvoidtest1(){LogUtil。addLog(test1);}}
  如果哪天addLog的逻辑又要改了,只需要修改LogUtil类的addLog方法即可。你可以自信满满的修改,不需要再小心翼翼了。
  我们写的代码,绝大多数是可维护性的代码,而非一次性的。所以,建议在写代码的过程中,如果出现重复的代码,尽量提取成公共方法。千万别因为项目初期一时的爽快,而给项目埋下隐患,后面的维护成本可能会非常高。24。foreach循环中不remove元素
  我们知道在Java中,循环有很多种写法,比如:while、for、foreach等。publicclassTest2{publicstaticvoidmain(String〔〕args){ListStringlistLists。newArrayList(a,b,c);for(Stringtemp:list){if(c。equals(temp)){list。remove(temp);}}System。out。println(list);}}
  执行结果:Exceptioninthreadmainjava。util。ConcurrentModificationExceptionatjava。util。ArrayListItr。checkForComodification(ArrayList。java:901)atjava。util。ArrayListItr。next(ArrayList。java:851)atcom。sue。jump。service。test1。Test2。main(Test2。java:24)
  这种在foreach循环中调用remove方法删除元素,可能会报ConcurrentModificationException异常。
  如果想在遍历集合时,删除其中的元素,可以用for循环,例如:publicclassTest2{publicstaticvoidmain(String〔〕args){ListStringlistLists。newArrayList(a,b,c);for(inti0;ilist。size();i){Stringtemplist。get(i);if(c。equals(temp)){list。remove(temp);}}System。out。println(list);}}
  执行结果:〔a,b〕25。避免随意打印日志
  在我们写代码的时候,打印日志是必不可少的工作之一。
  因为日志可以帮我们快速定位问题,判断代码当时真正的执行逻辑。
  但打印日志的时候也需要注意,不是说任何时候都要打印日志,比如:PostMapping(query)publicListUserquery(RequestBodyListLongids){log。info(requestparams:{},ids);ListUseruserListuserService。query(ids);log。info(response:{},userList);returnuserList;}
  对于有些查询接口,在日志中打印出了请求参数和接口返回值。
  咋一看没啥问题。
  但如果ids中传入值非常多,比如有1000个。而该接口被调用的频次又很高,一下子就会打印大量的日志,用不了多久就可能把磁盘空间打满。
  如果真的想打印这些日志该怎么办?PostMapping(query)publicListUserquery(RequestBodyListLongids){if(log。isDebugEnabled()){log。debug(requestparams:{},ids);}ListUseruserListuserService。query(ids);if(log。isDebugEnabled()){log。debug(response:{},userList);}returnuserList;}
  使用isDebugEnabled判断一下,如果当前的日志级别是debug才打印日志。生产环境默认日志级别是info,在有些紧急情况下,把某个接口或者方法的日志级别改成debug,打印完我们需要的日志后,又调整回去。
  方便我们定位问题,又不会产生大量的垃圾日志,一举两得。26。比较时把常量写前面
  在比较两个参数值是否相等时,通常我们会使用号,或者equals方法。
  我在第15章节中说过,使用号比较两个值是否相等时,可能会存在问题,建议使用equals方法做比较。
  反例:if(user。getName()。equals(苏三)){System。out。println(找到:user。getName());}
  在上面这段代码中,如果user对象,或者user。getName()方法返回值为null,则都报NullPointerException异常。
  那么,如何避免空指针异常呢?
  正例:privatestaticfinalStringFOUNDNAME苏三;。。。if(nulluser){return;}if(FOUNDNAME。equals(user。getName())){System。out。println(找到:user。getName());}
  在使用equals做比较时,尽量将常量写在前面,即equals方法的左边。
  这样即使user。getName()返回的数据为null,equals方法会直接返回false,而不再是报空指针异常。27。名称要见名知意
  java中没有强制规定参数、方法、类或者包名该怎么起名。但如果我们没有养成良好的起名习惯,随意起名的话,可能会出现很多奇怪的代码。27。1有意义的参数名
  有时候,我们写代码时为了省事(可以少敲几个字母),参数名起得越简单越好。假如同事A写的代码如下:inta1;intb2;Stringcabc;booleanbfalse;
  一段时间之后,同事A离职了,同事B接手了这段代码。
  他此时一脸懵逼,a是什么意思,b又是什么意思,还有c。。。然后心里一万个草泥马。
  给参数起一个有意义的名字,是非常重要的事情,避免给自己或者别人埋坑。
  正解:intsupplierCount1;intpurchaserCount2;StringuserNameabc;booleanhasSuccessfalse;27。2见名知意
  光起有意义的参数名还不够,我们不能就这点追求。我们起的参数名称最好能够见名知意,不然就会出现这样的情况:StringyongHuMing苏三;String用户Name苏三;Stringsu3苏三;StringsuThree苏三;
  这几种参数名看起来是不是有点怪怪的?
  为啥不定义成国际上通用的(地球人都能看懂)英文单词呢?StringuserName苏三;Stringsusan苏三;
  上面的这两个参数名,基本上大家都能看懂,减少了好多沟通成本。
  所以建议在定义不管是参数名、方法名、类名时,优先使用国际上通用的英文单词,更简单直观,减少沟通成本。少用汉子、拼音,或者数字定义名称。27。3参数名风格一致
  参数名其实有多种风格,列如:字母全小写intsuppliercount1;字母全大写intSUPPLIERCOUNT1;小写字母下划线intsuppliercount1;大写字母下划线intSUPPLIERCOUNT1;驼峰标识intsupplierCount1;
  如果某个类中定义了多种风格的参数名称,看起来是不是有点杂乱无章?
  所以建议类的成员变量、局部变量和方法参数使用supplierCount,这种驼峰风格,即:第一个字母小写,后面的每个单词首字母大写。例如:intsupplierCount1;
  此外,为了好做区分,静态常量建议使用SUPPLIERCOUNT,即:大写字母下划线分隔的参数名。例如:privatestaticfinalintSUPPLIERCOUNT1;28。SimpleDateFormat线程不安全
  在java8之前,我们对时间的格式化处理,一般都是用的SimpleDateFormat类实现的。例如:ServicepublicclassSimpleDateFormatService{publicDatetime(Stringtime)throwsParseException{SimpleDateFormatdateFormatnewSimpleDateFormat(yyyyMMddHH:mm:ss);returndateFormat。parse(time);}}
  如果你真的这样写,是没问题的。
  就怕哪天抽风,你觉得dateFormat是一段固定的代码,应该要把它抽取成常量。
  于是把代码改成下面的这样:ServicepublicclassSimpleDateFormatService{privatestaticSimpleDateFormatdateFormatnewSimpleDateFormat(yyyyMMddHH:mm:ss);publicDatetime(Stringtime)throwsParseException{returndateFormat。parse(time);}}
  dateFormat对象被定义成了静态常量,这样就能被所有对象共用。
  如果只有一个线程调用time方法,也不会出现问题。
  但Serivce类的方法,往往是被Controller类调用的,而Controller类的接口方法,则会被tomcat的线程池调用。换句话说,可能会出现多个线程调用同一个Controller类的同一个方法,也就是会出现多个线程会同时调用time方法。
  而time方法会调用SimpleDateFormat类的parse方法:OverridepublicDateparse(Stringtext,ParsePositionpos){。。。DateparsedDate;try{parsedDatecalb。establish(calendar)。getTime();。。。}catch(IllegalArgumentExceptione){pos。errorIndexstart;pos。indexoldStart;returnnull;}returnparsedDate;}
  该方法会调用establish方法:Calendarestablish(Calendarcal){。。。1。清空数据cal。clear();2。设置时间cal。set(。。。);3。返回returncal;}
  其中的步骤1、2、3是非原子操作。
  但如果cal对象是局部变量还好,坏就坏在parse方法调用establish方法时,传入的calendar是SimpleDateFormat类的父类DateFormat的成员变量:publicabstractclassDateFormatextendsForma{。。。。protectedCalendarcalendar;。。。}
  这样就可能会出现多个线程,同时修改同一个对象即:dateFormat,它的同一个成员变量即:Calendar值的情况。
  这样可能会出现,某个线程设置好了时间,又被其他的线程修改了,从而出现时间错误的情况。
  那么,如何解决这个问题呢?SimpleDateFormat类的对象不要定义成静态的,可以改成方法的局部变量。使用ThreadLocal保存SimpleDateFormat类的数据。使用java8的DateTimeFormatter类。29。少用Executors创建线程池
  我们都知道JDK5之后,提供了ThreadPoolExecutor类,用它可以自定义线程池。
  线程池的好处有很多,下面主要说说这3个方面。降低资源消耗:避免了频繁的创建线程和销毁线程,可以直接复用已有线程。而我们都知道,创建线程是非常耗时的操作。提供速度:任务过来之后,因为线程已存在,可以拿来直接使用。提高线程的可管理性:线程是非常宝贵的资源,如果创建过多的线程,不仅会消耗系统资源,甚至会影响系统的稳定。使用线程池,可以非常方便的创建、管理和监控线程。
  当然JDK为了我们使用更便捷,专门提供了:Executors类,给我们快速创建线程池。
  该类中包含了很多静态方法:newCachedThreadPool:创建一个可缓冲的线程,如果线程池大小超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。newFixedThreadPool:创建一个固定大小的线程池,如果任务数量超过线程池大小,则将多余的任务放到队列中。newScheduledThreadPool:创建一个固定大小,并且能执行定时周期任务的线程池。newSingleThreadExecutor:创建只有一个线程的线程池,保证所有的任务安装顺序执行。
  在高并发的场景下,如果大家使用这些静态方法创建线程池,会有一些问题。
  那么,我们一起看看有哪些问题?newFixedThreadPool:允许请求的队列长度是Integer。MAXVALUE,可能会堆积大量的请求,从而导致OOM。newSingleThreadExecutor:允许请求的队列长度是Integer。MAXVALUE,可能会堆积大量的请求,从而导致OOM。newCachedThreadPool:允许创建的线程数是Integer。MAXVALUE,可能会创建大量的线程,从而导致OOM。
  那我们该怎办呢?
  优先推荐使用ThreadPoolExecutor类,我们自定义线程池。
  具体代码如下:ExecutorServicethreadPoolnewThreadPoolExecutor(8,corePoolSize线程池中核心线程数10,maximumPoolSize线程池中最大线程数60,线程池中线程的最大空闲时间,超过这个时间空闲线程将被回收TimeUnit。SECONDS,时间单位newArrayBlockingQueue(500),队列newThreadPoolExecutor。CallerRunsPolicy());拒绝策略
  顺便说一下,如果是一些低并发场景,使用Executors类创建线程池也未尝不可,也不能完全一棍子打死。在这些低并发场景下,很难出现OOM问题,所以我们需要根据实际业务场景选择。30。Arrays。asList转换的集合别修改
  在我们日常工作中,经常需要把数组转换成List集合。
  因为数组的长度是固定的,不太好扩容,而List的长度是可变的,它的长度会根据元素的数量动态扩容。
  在JDK的Arrays类中提供了asList方法,可以把数组转换成List。
  正例:String〔〕arraynewString〔〕{a,b,c};ListStringlistArrays。asList(array);for(Stringstr:list){System。out。println(str);}
  在这个例子中,使用Arrays。asList方法将array数组,直接转换成了list。然后在for循环中遍历list,打印出它里面的元素。
  如果转换后的list,只是使用,没新增或修改元素,不会有问题。
  反例:String〔〕arraynewString〔〕{a,b,c};ListStringlistArrays。asList(array);list。add(d);for(Stringstr:list){System。out。println(str);}
  执行结果:Exceptioninthreadmainjava。lang。UnsupportedOperationExceptionatjava。util。AbstractList。add(AbstractList。java:148)atjava。util。AbstractList。add(AbstractList。java:108)atcom。sue。jump。service。test1。Test2。main(Test2。java:24)
  会直接报UnsupportedOperationException异常。
  为什么呢?
  答:使用Arrays。asList方法转换后的ArrayList,是Arrays类的内部类,并非java。util包下我们常用的ArrayList。
  Arrays类的内部ArrayList类,它没有实现父类的add和remove方法,用的是父类AbstractList的默认实现。
  我们看看AbstractList是如何实现的:publicvoidadd(intindex,Eelement){thrownewUnsupportedOperationException();}publicEremove(intindex){thrownewUnsupportedOperationException();}
  该类的add和remove方法直接抛异常了,因此调用Arrays类的内部ArrayList类的add和remove方法,同样会抛异常。
  说实话,Java代码优化是一个比较大的话题,它里面可以优化的点非常多,我没办法一一列举完。在这里只能抛砖引玉,介绍一下比较常见的知识点,更全面的内容,需要小伙伴们自己去思考和探索。
  这篇文章写了很久,花了很多时间和心思,如果你看了文章有些收获,记得给我点赞鼓励一下喔。最后说一句(求关注,别白嫖我)
  如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

这才是最营养的早餐,我家孩子天天点名要吃,太香了,出锅就扫光生活没有彩排,美食没有美颜。大家好,今天用面粉给大家分享一道美食。面粉,日常生活中非常常见的一种食材,可以用来做成各种各样好吃的美食。万能的面粉拘不仅可以做成包子,馒头还可以做……男性衰老,头发先知,6大表现提示老了!老不可怕,早养生才长寿上一次回家,发现父亲的头发又少了不少,以前的父亲可是有着一头茂密的头发,这也一直让他引以为傲,别人年纪轻轻就开始脱发秃顶,可他到40多岁仍旧头发旺盛。可不知道从什么时候起,他的……遵义道桥百亿贷款超长展期背后地方城投市场化经营艰难一则城投公司贷款展期降息新闻,引起市场广泛关注。近日,遵义道桥建设(集团)有限公司(下称遵义道桥)在中国债券信息网公开了关于推进银行贷款重组事项的公告(下称公告),明确了……2016年,韩国乐天因反华遭抵制,被迫退出中国市场,如今怎样文影中纪实编辑影中纪实【前言】2022年3月16日,韩国电视台发布这样一则消息:乐天集团要关闭位于上海的总部。此消息一出,瞬间引起网友热议。要知道,乐天……19款迈腾330和380动力差距大吗,这两款车的具体区别是什19款迈腾330和380用的都是EA888系的发动机,但是330和380的研发初衷还是有明显差别的。330是EA888GEN3B主要针对燃油经济性优化,380是EA888GEN……用南瓜做的面包就是好吃,这配方快收藏,奶香浓郁香甜暄软家里有好几个大南瓜,可着劲儿用它做各种美食。这不,南瓜餐包又来了!模具我用的八角模具,这就使得它的颜值提升了一大截。没有这个模具就直接揉成团放在烤盘里或者其它差不多的模具……世界名车排名是怎么样的?我觉得五菱车才是弟一名神车有很多名车都败在了此车上德国的名车最多市面上常见的名车豪车就是BBA等这类车型,一般都是指百万级别的的车就可以称为名车,那么这些名车谁的排……65岁,存款15万,退休金3500,可以安享晚年吗?如果没有负担,没有拖累,没有啃老,自己则有医保,也有住房,这样的收入和存款,应当会过得不错。要知道,对一位老人来说,每月3500元过日子,已经算是不错了。即便在一线城市,……多头旋的孩子,脾气坏聪明有财气头旋的真正寓意到底是啥对于孩子的发旋,很多家长都非常好奇,因为有的孩子只有1、2个发旋,有的孩子发旋却有3个甚至是4个。但对于发旋的说法,很多地方都不一样,这也让更多家长更加不解,也害怕孩子的性格真……8月3日手机爆料汇总第一,绿厂基于台积电6nm的MariSiliconX设计有不少硬件冗沉,还会接着打磨很久。接下来粮蓝家小芯片也会集成很多硬件级新功能,制程工艺较前代升级也不小第二,欧加1……只一年时间,抛弃雪藏买断!昔日的全明星领袖,32岁就无人问津前言NBA自由市场已经开启了很长一段时间,虽说联盟绝大部分球队的引援补强工作,基本都告一段落。但我们却不得不承认,自由市场上还是有不少的自由球员,还在苦苦等待着下一份合同……三星自己都不用?GalaxyS23曝光,无缘2亿像素和屏下镜三星于6月23日推出HP3图像传感器,拥有2亿像素带来无与伦比的解析力,消息称摩托罗拉将在7月份全球首发。但韩媒爆料,由于这颗传感器的实际表现不佳,三星不会用在GalaxyS2……
燃点为啥中国市场对英菲尼迪不敢爱了?世界上没有无缘无故的爱,也没有无缘无故的恨。英菲尼迪如果能够对这句话有更深一层的感触,那它或许能更早的正视其在中国汽车市场的折戟沉沙,并早日寻得正确的应对之法。英菲……价格12万至16万,德系日系美系车,SUV车,有哪些可以推荐这个价格的合资SUV只能看看小型SUV。日系1。日产逍客,日产逍客作为一款曾经登顶欧洲SUV销量冠军的车辆,可见其实力不一般,现如今逍客也即将迎来新的一代,设计更年……这款全进口大型SUV,5。2米车长,3。6L排量V6配9AT很多车主在选购中大型SUV时,更多的可能是考虑大众途昂这款德系车型,不过今天小编倒是给大家推荐一款全进口的中大型SUV,霸气车身,5。2米车长,2。6L排量V6发动机,配有9A……手绘百米画卷萌娃眼中的消防是啥样?视频加载中。。。今天(11月9日),是第31个全国消防日。在绵阳市北川羌族自治县,萌娃们插上想象的翅膀,用手中的七彩画笔描绘出对消防的认识和理解,将消防安全的种子扎根心间……汽车养护,大保养?小保养?看完你就懂了!养车是用车生活的重要组成部分,俗话说汽车七分靠养三分靠修,把车子养成什么样取决于不同车主的养车方式。有些车子年龄不大,车况就像七老八十的干瘪老头,有些车子年龄比车主都大,车况就……等红灯被电动车追尾,致使电动车车主身亡!汽车车主有责任吗?近年来,随着你家车数量的日益增多,道路交通的压力也是越来越大,堵车现象,屡见不鲜。而这时候车身轻便小巧的电动车成为了人们的对爱,甚至不少车主马路上随意穿插,对安全行驶造成了极大……为什么蜜蜂会清理同伴的尸体?研究揭示背后机制图为研究成果。中国科学院西双版纳热带植物园供图中新网昆明3月9日电(记者胡远航)蜜蜂像人一样,也会埋葬死去的同类。甚至在热带和亚热带地区生活的穴居蜜蜂中,还有特别的搬尸蜂……想想在2000年时的街头,驾驶这样一辆玛莎拉蒂跑车,那是何等玛莎拉蒂3200GT,诞生于1998年,他的车身由大师乔治亚罗设计;由于此时的玛莎拉蒂已归于意大利的FIAT公司麾下,与法拉利同门,所以玛莎拉蒂3200GT的外形有了脱胎换骨的……2019买车别乱选,这10款全新车将国产,开3年不亏,重点关说到这个买车,现在很多人都不知道应该怎么选,是买台实实在在的合资品牌轿车呢,还是够一够买台豪华品牌SUV,许多人对此也是相当纠结,那当下又该怎么办,买车应该怎么选呢?哎,这就是……摸得着的音乐,听得见的温度人生第一次玩黑胶的心路历程我看到的文字被压缩成了字节,我看过的电影变成了资源链接,但我却看不到音乐,虽然它出现在我生活的每个角落。音乐逐渐远离实体的流媒体时代里,随便一个音乐软件就有一辈子听不完的歌。我……中大型SUV又添猛将,换代后与X5同级别,指纹解锁,还看啥汉近年来经济增长,手里都有钱了,买中型SUV的消费者越来越多,市面上的中型SUV可选择的其实并不多,基本上就是一家一个型号,像大众途观L、丰田汉兰达、本田的冠道、福特锐界,这几款……想买新车的不如再等等,这5款大7座SUV今年都将国产,款款吸新的2019年,我想很多消费者应该都对新的一年展开新的展望了吧。的确,人活着就应该有点目标、有点追求,如果在年轻气盛之时都碌碌无为,那跟咸鱼又有什么区别呢?因此,在新的一年时间……
友情链接:易事利快生活快传网聚热点七猫云快好知快百科中准网快好找文好找中准网快软网