写出优质代码的4个技巧.docx

上传人:b****8 文档编号:23950600 上传时间:2023-05-22 格式:DOCX 页数:16 大小:23.64KB
下载 相关 举报
写出优质代码的4个技巧.docx_第1页
第1页 / 共16页
写出优质代码的4个技巧.docx_第2页
第2页 / 共16页
写出优质代码的4个技巧.docx_第3页
第3页 / 共16页
写出优质代码的4个技巧.docx_第4页
第4页 / 共16页
写出优质代码的4个技巧.docx_第5页
第5页 / 共16页
点击查看更多>>
下载资源
资源描述

写出优质代码的4个技巧.docx

《写出优质代码的4个技巧.docx》由会员分享,可在线阅读,更多相关《写出优质代码的4个技巧.docx(16页珍藏版)》请在冰豆网上搜索。

写出优质代码的4个技巧.docx

写出优质代码的4个技巧

写出优质Java代码的4个技巧

设计 方法 系统 return 代码

我们平时的编程任务不外乎就是将相同的技术套件应用到不同的项目中去,对于大多数情况来说,这些技术都是可以满足目标的。

然而,有的项目可能需要用到一些特别的技术,因此工程师们得深入研究,去寻找那些最简单但最有效的方法。

在前一篇文章中,我们讨论了必要时可以使用的四种特殊技术,这些特殊技术可以创建更好的Java软件;而本文我们将介绍一些有助于解决常见问题的通用设计策略和目标实现技术,即:

1.只做有目的性的优化

2.常量尽量使用枚举

3.重新定义类里面的equals()方法

4.尽量多使用多态性

值得注意的是,本文中描述的技术并不是适用于所有情况。

另外这些技术应该什么时候使用以及在什么地方使用,都是需要使用者经过深思熟虑的。

1.只做有目的性的优化

大型软件系统肯定非常关注性能问题。

虽然我们希望能够写出最高效的代码,但很多时候,如果想对代码进行优化,我们却无从下手。

例如,下面的这段代码会影响到性能吗?

publicvoidprocessIntegers(Listintegers){

for(Integervalue:

integers){

for(inti=integers.size()-1;i>=0;i--){

value+=integers.get(i);

}

}

}

这就得视情况而定了。

上面这段代码可以看出它的处理算法是O(n³)(使用大O符号),其中n是list集合的大小。

如果n只有5,那么就不会有问题,只会执行25次迭代。

但如果n是10万,那可能会影响性能了。

请注意,即使这样我们也不能判定肯定会有问题。

尽管此方法需要执行10亿次逻辑迭代,但会不会对性能产生影响仍然有待讨论。

例如,假设客户端是在它自己的线程中执行这段代码,并且异步等待计算完成,那么它的执行时间有可能是可以接受的。

同样,如果系统部署在了生产环境上,但是没有客户端进行调用,那我们根本没必要去对这段代码进行优化,因为压根就不会消耗系统的整体性能。

事实上,优化性能以后系统会变得更加复杂,悲剧的是系统的性能却没有因此而提高。

最重要的是天下没有免费的午餐,因此为了降低代价,我们通常会通过类似于缓存、循环展开或预计算值这类技术去实现优化,这样反而增加了系统的复杂性,也降低了代码的可读性。

如果这种优化可以提高系统的性能,那么即使变得复杂,那也是值得的,但是做决定之前,必须首先知道这两条信息:

1.性能要求是什么

2.性能瓶颈在哪里

首先我们需要清楚地知道性能要求是什么。

如果最终是在要求以内,并且最终用户也没有提出什么异议,那么就没有必要进行性能优化。

但是,当添加了新功能或者系统的数据量达到一定规模以后就必须进行优化了,否则可能会出现问题。

在这种情况下,不应该靠直觉,也不应该依靠检查。

因为即使是像MartinFowler这样有经验的开发人员也容易做一些错误的优化,正如在重构(第70页)一文中解释的那样:

如果分析了足够多的程序以后,你会发现关于性能的有趣之处在于,大部分时间都浪费在了系统中的一小部分代码中里面。

如果对所有代码进行了同样的优化,那么最终结果就是浪费了90%的优化,因为优化过以后的代码运行得频率并不多。

因为没有目标而做的优化所耗费的时间,都是在浪费时间。

作为一名身经百战的开发人员,我们应该认真对待这一观点。

第一次猜测不仅没有提高系统的性能,而且90%的开发时间完全是浪费了。

相反,我们应该在生产环境(或者预生产环境中)执行常见用例,并找出在执行过程中是哪部分在消耗系统资源,然后对系统进行配置。

例如消耗大部分资源的代码只占了10%,那么优化其余90%的代码就是浪费时间。

根据分析结果,要想使用这些知识,我们应该从最常见的情况入手。

因为这将确保实际付出的努力最终是可以提高系统的性能。

每次优化后,都应该重复分析步骤。

因为这不仅可以确保系统的性能真的得到了改善,也可以看出再对系统进行优化后,性能瓶颈是在哪个部分(因为解决完一个瓶颈以后,其它瓶颈可能消耗系统更多的整体资源)。

需要注意的是,在现有瓶颈中花费的时间百分比很可能会增加,因为剩下的瓶颈是暂时不变的,而且随着目标瓶颈的消除,整个执行时间应该会减少。

尽管在Java系统中想要对概要文件进行全面检查需要很大的容量,但是还是有一些很常见的工具可以帮助发现系统的性能热点,这些工具包括JMeter、AppDynamics和YourKit。

另外,还可以参见DZone的性能监测指南,获取更多关于Java程序性能优化的信息。

虽然性能是许多大型软件系统一个非常重要的组成部分,也成为产品交付管道中自动化测试套件的一部分,但是还是不能够盲目的且没有目的的进行优化。

相反,应该对已经掌握的性能瓶颈进行特定的优化。

这不仅可以帮助我们避免增加了系统的复杂性,而且还让我们少走弯路,不去做那些浪费时间的优化。

2.常量尽量使用枚举

需要用户列出一组预定义或常量值的场景有很多,例如在web应用程序中可能遇到的HTTP响应代码。

最常见的实现技术之一是新建类,该类里面有很多静态的final类型的值,每个值都应该有一句注释,描述该值的含义是什么:

publicclassHttpResponseCodes{

publicstaticfinalintOK=200;

publicstaticfinalintNOT_FOUND=404;

publicstaticfinalintFORBIDDEN=403;

}

if(getHttpResponse().getStatusCode()==HttpResponseCodes.OK){

//DosomethingiftheresponsecodeisOK

}

能够有这种思路就已经非常好了,但这还是有一些缺点:

1.没有对传入的整数值进行严格的校验

2.由于是基本数据类型,因此不能调用状态代码上的方法

在第一种情况下只是简单的创建了一个特定的常量来表示特殊的整数值,但并没有对方法或变量进行限制,因此使用的值可能会超出定义的范围。

例如:

publicclassHttpResponseHandler{

publicstaticvoidprintMessage(intstatusCode){

System.out.println("Recievedstatusof"+statusCode);

}

}

HttpResponseHandler.printMessage(15000);

尽管15000并不是有效的HTTP响应代码,但是由于服务器端也没有限制客户端必须提供有效的整数。

在第二种情况下,我们没有办法为状态代码定义方法。

例如,如果想要检查给定的状态代码是否是一个成功的代码,那就必须定义一个单独的函数:

publicclassHttpResponseCodes{

publicstaticfinalintOK=200;

publicstaticfinalintNOT_FOUND=404;

publicstaticfinalintFORBIDDEN=403;

publicstaticbooleanisSuccess(intstatusCode){

returnstatusCode>=200&&statusCode<300;

}

}

if(HttpResponseCodes.isSuccess(getHttpResponse().getStatusCode())){

//Dosomethingiftheresponsecodeisasuccesscode

}

为了解决这些问题,我们需要将常量类型从基本数据类型改为自定义类型,并只允许自定义类的特定对象。

这正是Java枚举(enum)的用途。

使用enum,我们可以一次性解决这两个问题:

publicenumHttpResponseCodes{

OK(200),

FORBIDDEN(403),

NOT_FOUND(404);

privatefinalintcode;

HttpResponseCodes(intcode){

this.code=code;

}

publicintgetCode(){

returncode;

}

publicbooleanisSuccess(){

returncode>=200&&code<300;

}

}

if(getHttpResponse().getStatusCode().isSuccess()){

//Dosomethingiftheresponsecodeisasuccesscode

}

同样,现在还可以要求在调用方法的时候提供必须有效的状态代码:

publicclassHttpResponseHandler{

publicstaticvoidprintMessage(HttpResponseCodestatusCode){

System.out.println("Recievedstatusof"+statusCode.getCode());

}

}

HttpResponseHandler.printMessage(HttpResponseCode.OK);

值得注意的是,举这个例子事项说明如果是常量,则应该尽量使用枚举,但并不是说什么情况下都应该使用枚举。

在某些情况下,可能希望使用一个常量来表示某个特殊值,但是也允许提供其它的值。

例如,大家可能都知道圆周率,我们可以用一个常量来捕获这个值(并重用它):

publicclassNumericConstants{

publicstaticfinaldoublePI=3.14;

publicstaticfinaldoubleUNIT_CIRCLE_AREA=PI*PI;

}

publicclassRug{

privatefinaldoublearea;

publicclassRun(doublearea){

this.area=area;

}

publicdoublegetCost(){

returnarea*2;

}

}

//Createacarpetthatis4feetindiameter(radiusof2feet)

RugfourFootRug=newRug(2*NumericConstants.UNIT_CIRCLE_AREA);

因此,使用枚举的规则可以归纳为:

当所有可能的离散值都已经提前知道了,那么就可以使用枚举

再拿上文中所提到的HTTP响应代码为例,我们可能知道HTTP状态代码的所有值(可以在RFC7231中找的到,它定义了HTTP1.1协议)。

因此使用了枚举。

在计算圆周率的情况下,我们不知道关于圆周率的所有可能值(任何可能的double都是有效的),但同时又希望为圆形的rugs创建一个常量,使计算更容易(更容易阅读);因此定义了一系列常量。

如果不能提前知道所有可能的值,但是又希望包含每个值的字段或方法,那么最简单的方法就是可以新建一个类来表示数据。

尽管没有说过什么场景应该绝对不用枚举,但要想知道在什么地方、什么时间不使用枚举的关键是提前意识到所有的值,并且禁止使用其他任何值。

3.重新定义类里面的equals()方法

对象识别可能是一个很难解决的问题:

如果两个对象在内存中占据相同的位置,那么它们是相同的吗?

如果它们的id相同,它们是相同的吗?

或者如果所有的字段都相等呢?

虽然每个类都有自己的标识逻辑,但是在系统中有很多西方都需要去判断是否相等。

例如,有如下的一个类,表示订单购买…

publicclassPurchase{

privatelongid;

publiclonggetId(){

returnid;

}

publicvoidsetId(longid){

this.id=id;

}

}

……就像下面写的这样,代码中肯定有很多地方都是类似于的:

PurchaseoriginalPurchase=newPurchase();

PurchaseupdatedPurchase=newPurchase();

if(originalPurchase.getId()==updatedPurchase.getId()){

//Executesomelogicforequalpurchases

}

这些逻辑调用的越多(反过来,违背了DRY原则),Purchase类的身份信息也会变得越来越多。

如果出于某种原因,更改了Purchase类的身份逻辑(例如,更改了标识符的类型),则需要更新标识逻辑所在的位置肯定也非常多。

我们应该在类的内部初始化这个逻辑,而不是通过系统将Purchase类的身份逻辑进行过多的传播。

乍一看,我们可以创建一个新的方法,比如isSame,这个方法的入参是一个Purchase对象,并对每个对象的id进行比较,看看它们是否相同:

publicclassPurchase{

privatelongid;

publicbooleanisSame(Purchaseother){

returngetId()==other.gerId();

}

}

虽然这是一个有效的解决方案,但是忽略了Java的内置功能:

使用equals方法。

Java中的每个类都是继承了Object类,虽然是隐式的,因此同样也就继承了equals方法。

默认情况下,此方法将检查对象标识(内存中相同的对象),如JDK中的对象类定义(version1.8.0_131)中的以下代码片段所示:

publicbooleanequals(Objectobj){

return(this==obj);

}

这个equals方法充当了注入身份逻辑的自然位置(通过覆盖默认的equals实现):

publicclassPurchase{

privatelongid;

publiclonggetId(){

returnid;

}

publicvoidsetId(longid){

this.id=id;

}

@Override

publicbooleanequals(Objectother){

if(this==other){

returntrue;

}

elseif(!

(otherinstanceofPurchase)){

returnfalse;

}

else{

return((Purchase)other).getId()==getId();

}

}

}

虽然这个equals方法看起来很复杂,但由于equals方法只接受类型对象的参数,所以我们只需要考虑三个案例:

1.

另一个对象是当前对象(即originalPurchase.equals(originalPurchase)),根据定义,它们是同一个对象,因此返回true

2.

3.

另一个对象不是Purchase对象,在这种情况下,我们无法比较Purchase的id,因此,这两个对象不相等

4.

5.

其他对象不是同一个对象,但却是Purchase的实例,因此,是否相等取决于当前Purchase的id和其他Purchase是否相等

6.

现在可以重构我们之前的条件,如下:

PurchaseoriginalPurchase=newPurchase();

PurchaseupdatedPurchase=newPurchase();

if(originalPurchase.equals(updatedPurchase)){

//Executesomelogicforequalpurchases

}

除了可以在系统中减少复制,重构默认的equals方法还有一些其它的优势。

例如,如果构造一个Purchase对象列表,并检查列表是否包含具有相同ID(内存中不同对象)的另一个Purchase对象,那么我们就会得到true值,因为这两个值被认为是相等的:

Listpurchases=newArrayList<>();

purchases.add(originalPurchase);

purchases.contains(updatedPurchase);//True

通常,无论在什么地方,如果需要判断两个类是否相等,则只需要使用重写过的equals方法就可以了。

如果希望使用由于继承了Object对象而隐式具有的equals方法去判断相等性,我们还可以使用==操作符,如下:

if(originalPurchase==updatedPurchase){

//Thetwoobjectsarethesameobjectsinmemory

}

还需要注意的是,当equals方法被重写以后,hashCode方法也应该被重写。

有关这两种方法之间关系的更多信息,以及如何正确定义hashCode方法,请参见此线程。

正如我们所看到的,重写equals方法不仅可以将身份逻辑在类的内部进行初始化,并在整个系统中减少了这种逻辑的扩散,它还允许Java语言对类做出有根据的决定。

4.尽量多使用多态性

对于任何一门编程语言来说,条件句都是一种很常见的结构,而且它的存在也是有一定原因的。

因为不同的组合可以允许用户根据给定值或对象的瞬时状态改变系统的行为。

假设用户需要计算各银行账户的余额,那么就可以开发出以下的代码:

publicenumBankAccountType{

CHECKING,

SAVINGS,

CERTIFICATE_OF_DEPOSIT;

}

publicclassBankAccount{

privatefinalBankAccountTypetype;

publicBankAccount(BankAccountTypetype){

this.type=type;

}

publicdoublegetInterestRate(){

switch(type){

caseCHECKING:

return0.03;//3%

caseSAVINGS:

return0.04;//4%

caseCERTIFICATE_OF_DEPOSIT:

return0.05;//5%

default:

thrownewUnsupportedOperationException();

}

}

publicbooleansupportsDeposits(){

switch(type){

caseCHECKING:

returntrue;

caseSAVINGS:

returntrue;

caseCERTIFICATE_OF_DEPOSIT:

returnfalse;

default:

thrownewUnsupportedOperationException();

}

}

}

虽然上面这段代码满足了基本的要求,但是有个很明显的缺陷:

用户只是根据给定帐户的类型决定系统的行为。

这不仅要求用户每次要做决定之前都需要检查账户类型,还需要在做出决定时重复这个逻辑。

例如,在上面的设计中,用户必须在两种方法都进行检查才可以。

这就可能会出现失控的情况,特别是接收到添加新帐户类型的需求时。

我们可以使用多态来隐式地做出决策,而不是使用账户类型用来区分。

为了做到这一点,我们将BankAccount的具体类转换成一个接口,并将决策过程传入一系列具体的类,这些类代表了每种类型的银行帐户:

publicinterfaceBankAccount{

publicdoublegetInterestRate();

publicbooleansupportsDeposits();

}

publicclassCheckingAccountimplementsBankAccount{

@Override

publicdoublegetIntestRate(){

return0.03;

}

@Override

publicbooleansupportsDeposits(){

returntrue;

}

}

publicclassSavingsAccountimplementsBankAccount{

@Override

publicdoublegetIntestRate(){

return0.04;

}

@Override

publicbooleansupportsDeposits(){

returntrue;

}

}

publicclassCertificateOfDepositAccountimplementsBankAccount{

@Override

publicdoublegetIntestRate(){

return0.05;

}

@Override

publicbooleansupportsDeposits(){

returnfalse;

}

}

这不仅将每个帐户特有的信息封装到了到自己的类中,而且还支持用户可以在两种重要的方式中对设计进行变化。

首先,如果想要添加一个新的银行帐户类型,只需创建一个新的具体类,实现了BankAccount的接口,给出两个方法的具体实现就可以了。

在条件结构设计中,我们必须在枚举中添加一个新值,在两个方法中添加新的case语句,并在每个case语句下插入新帐户的逻辑。

其次,如果我们希望在BankAccount接口中添加一个新方法,我们只需在每个具体类中添加新方法。

在条件设计中,我们必须复制现有的switch语句并将其添加到我们的新方法中。

此外,我们还必须在每个case语句中添加每个帐户类型的逻辑。

在数学上,当我们创建一个新方法或添加一个新类型时,我们必须在多态和条件设计中做出相同数量的逻辑更改。

例如,如果我们在多态设计中添加一个新方法,我们必须将新方法添加到所有n个银行帐户的具体类中,而在条件设计中,我们必须在我们的新方法中添加n个新的case语句。

如果我们在多态设计中添加一个新的account类型,我们必须在BankAccount接口中实现所有的m数,而在条件设计中,我们必须向每个m现有方法添加一个新的case语句。

虽然我们必须做的改变的数量是相等的,但变化的性质却是完全不同的。

在多态设计中,如果我们添加一个新的帐户类型并且忘记包含一个方法,编译器会抛出一个错误,因为我们没有在我们的BankAccount接口中实现所有的

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高等教育 > 医学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1