咱们平时的编程任务不外乎就是将相同的技术套件应用到不一样的项目中去,对于大多数状况来讲,这些技术都是能够知足目标的。然而,有的项目可能须要用到一些特别的技术,所以工程师们得深刻研究,去寻找那些最简单但最有效的方法。本文咱们将介绍一些有助于解决常见问题的通用设计策略和目标实现技术,即:程序员
只作有目的性的优化web
常量尽可能使用枚举算法
从新定义类里面的
equals()
方法编程尽可能多使用多态性缓存
值得注意的是,本文中描述的技术并非适用于全部状况。另外这些技术应该何时使用以及在什么地方使用,都是须要使用者通过深思熟虑的。性能优化
大型软件系统确定很是关注性能问题。虽然咱们但愿可以写出最高效的代码,但不少时候,若是想对代码进行优化,咱们却无从下手。例如,下面的这段代码会影响到性能吗?服务器
public void processIntegers(List<Integer> integers) { for (Integer value: integers) { for (int i = integers.size() - 1; i >= 0; i--) { value += integers.get(i); } } }
这就得视状况而定了。上面这段代码能够看出它的处理算法是O(n³)(使用大O符号),其中n是list集合的大小。若是n只有5,那么就不会有问题,只会执行25次迭代。但若是n是10万,那可能会影响性能了。请注意,即便这样咱们也不能断定确定会有问题。尽管此方法须要执行10亿次逻辑迭代,但会不会对性能产生影响仍然有待讨论。异步
例如,假设客户端是在它本身的线程中执行这段代码,而且异步等待计算完成,那么它的执行时间有多是能够接受的。一样,若是系统部署在了生产环境上,可是没有客户端进行调用,那咱们根本不必去对这段代码进行优化,由于压根就不会消耗系统的总体性能。事实上,优化性能之后系统会变得更加复杂,悲剧的是系统的性能却没有所以而提升。编程语言
最重要的是天下没有免费的午饭,所以为了下降代价,咱们一般会经过相似于缓存、循环展开或预计算值这类技术去实现优化,这样反而增长了系统的复杂性,也下降了代码的可读性。若是这种优化能够提升系统的性能,那么即便变得复杂,那也是值得的,可是作决定以前,必须首先知道这两条信息:ide
性能要求是什么
性能瓶颈在哪里
首先咱们须要清楚地知道性能要求是什么。若是最终是在要求之内,而且最终用户也没有提出什么异议,那么就没有必要进行性能优化。可是,当添加了新功能或者系统的数据量达到必定规模之后就必须进行优化了,不然可能会出现问题。
在这种状况下,不该该靠直觉,也不该该依靠检查。由于即便是像Martin Fowler这样有经验的开发人员也容易作一些错误的优化,正如在重构(第70页)一文中解释的那样:
若是分析了足够多的程序之后,你会发现关于性能的有趣之处在于,大部分时间都浪费在了系统中的一小部分代码中里面。若是对全部代码进行了一样的优化,那么最终结果就是浪费了90%的优化,由于优化过之后的代码运行得频率并很少。由于没有目标而作的优化所耗费的时间,都是在浪费时间。
做为一名身经百战的开发人员,咱们应该认真对待这一观点。第一次猜想不只没有提升系统的性能,并且90%的开发时间彻底是浪费了。相反,咱们应该在生产环境(或者预生产环境中)执行常见用例,并找出在执行过程当中是哪部分在消耗系统资源,而后对系统进行配置。例如消耗大部分资源的代码只占了10%,那么优化其他90%的代码就是浪费时间。
根据分析结果,要想使用这些知识,咱们应该从最多见的状况入手。由于这将确保实际付出的努力最终是能够提升系统的性能。每次优化后,都应该重复分析步骤。由于这不只能够确保系统的性能真的获得了改善,也能够看出再对系统进行优化后,性能瓶颈是在哪一个部分(由于解决完一个瓶颈之后,其它瓶颈可能消耗系统更多的总体资源)。须要注意的是,在现有瓶颈中花费的时间百分比极可能会增长,由于剩下的瓶颈是暂时不变的,并且随着目标瓶颈的消除,整个执行时间应该会减小。
尽管在Java系统中想要对概要文件进行全面检查须要很大的容量,可是仍是有一些很常见的工具能够帮助发现系统的性能热点,这些工具包括JMeter、AppDynamics和YourKit。另外,还能够参见DZone的性能监测指南,获取更多关于Java程序性能优化的信息。
虽然性能是许多大型软件系统一个很是重要的组成部分,也成为产品交付管道中自动化测试套件的一部分,可是仍是不可以盲目的且没有目的的进行优化。相反,应该对已经掌握的性能瓶颈进行特定的优化。这不只能够帮助咱们避免增长了系统的复杂性,并且还让咱们少走弯路,不去作那些浪费时间的优化。
须要用户列出一组预约义或常量值的场景有不少,例如在web应用程序中可能遇到的HTTP响应代码。最多见的实现技术之一是新建类,该类里面有不少静态的final类型的值,每一个值都应该有一句注释,描述该值的含义是什么:
public class HttpResponseCodes { public static final int OK = 200; public static final int NOT_FOUND = 404; public static final int FORBIDDEN = 403; } if (getHttpResponse().getStatusCode() == HttpResponseCodes.OK) { // Do something if the response code is OK }
可以有这种思路就已经很是好了,但这仍是有一些缺点:
没有对传入的整数值进行严格的校验
因为是基本数据类型,所以不能调用状态代码上的方法
在第一种状况下只是简单的建立了一个特定的常量来表示特殊的整数值,但并无对方法或变量进行限制,所以使用的值可能会超出定义的范围。例如:
public class HttpResponseHandler { public static void printMessage(int statusCode) { System.out.println("Recieved status of " + statusCode); } } HttpResponseHandler.printMessage(15000);
尽管15000
并非有效的HTTP响应代码,可是因为服务器端也没有限制客户端必须提供有效的整数。在第二种状况下,咱们没有办法为状态代码定义方法。例如,若是想要检查给定的状态代码是不是一个成功的代码,那就必须定义一个单独的函数:
public class HttpResponseCodes { public static final int OK = 200; public static final int NOT_FOUND = 404; public static final int FORBIDDEN = 403; public static boolean isSuccess(int statusCode) { return statusCode >= 200 && statusCode < 300; } } if (HttpResponseCodes.isSuccess(getHttpResponse().getStatusCode())) { // Do something if the response code is a success code }
为了解决这些问题,咱们须要将常量类型从基本数据类型改成自定义类型,并只容许自定义类的特定对象。这正是Java枚举(enum)的用途。使用enum,咱们能够一次性解决这两个问题:
public enum HttpResponseCodes { OK(200), FORBIDDEN(403), NOT_FOUND(404); private final int code; HttpResponseCodes(int code) { this.code = code; } public int getCode() { return code; } public boolean isSuccess() { return code >= 200 && code < 300; } } if (getHttpResponse().getStatusCode().isSuccess()) { // Do something if the response code is a success code }
一样,如今还能够要求在调用方法的时候提供必须有效的状态代码:
public class HttpResponseHandler { public static void printMessage(HttpResponseCode statusCode) { System.out.println("Recieved status of " + statusCode.getCode()); } } HttpResponseHandler.printMessage(HttpResponseCode.OK);
值得注意的是,举这个例子事项说明若是是常量,则应该尽可能使用枚举,但并非说什么状况下都应该使用枚举。在某些状况下,可能但愿使用一个常量来表示某个特殊值,可是也容许提供其它的值。例如,你们可能都知道圆周率,咱们能够用一个常量来捕获这个值(并重用它):
public class NumericConstants { public static final double PI = 3.14; public static final double UNIT_CIRCLE_AREA = PI * PI; } public class Rug { private final double area; public class Run(double area) { this.area = area; } public double getCost() { return area * 2; } } // Create a carpet that is 4 feet in diameter (radius of 2 feet) Rug fourFootRug = new Rug(2 * NumericConstants.UNIT_CIRCLE_AREA);
所以,使用枚举的规则能够概括为:
当全部可能的离散值都已经提早知道了,那么就可使用枚举
再拿上文中所提到的HTTP响应代码为例,咱们可能知道HTTP状态代码的全部值(能够在RFC 7231中找的到,它定义了HTTP 1.1协议)。所以使用了枚举。在计算圆周率的状况下,咱们不知道关于圆周率的全部可能值(任何可能的double都是有效的),但同时又但愿为圆形的rugs建立一个常量,使计算更容易(更容易阅读);所以定义了一系列常量。
若是不能提早知道全部可能的值,可是又但愿包含每一个值的字段或方法,那么最简单的方法就是能够新建一个类来表示数据。尽管没有说过什么场景应该绝对不用枚举,但要想知道在什么地方、什么时间不使用枚举的关键是提早意识到全部的值,而且禁止使用其余任何值。
对象识别多是一个很难解决的问题:若是两个对象在内存中占据相同的位置,那么它们是相同的吗?若是它们的id相同,它们是相同的吗?或者若是全部的字段都相等呢?虽然每一个类都有本身的标识逻辑,可是在系统中有不少西方都须要去判断是否相等。例如,有以下的一个类,表示订单购买…
public class Purchase { private long id; public long getId() { return id; } public void setId(long id) { this.id = id; } }
……就像下面写的这样,代码中确定有不少地方都是相似于的:
Purchase originalPurchase = new Purchase(); Purchase updatedPurchase = new Purchase(); if (originalPurchase.getId() == updatedPurchase.getId()) { // Execute some logic for equal purchases }
这些逻辑调用的越多(反过来,违背了DRY原则),Purchase
类的身份信息也会变得愈来愈多。若是出于某种缘由,更改了Purchase
类的身份逻辑(例如,更改了标识符的类型),则须要更新标识逻辑所在的位置确定也很是多。
咱们应该在类的内部初始化这个逻辑,而不是经过系统将Purchase
类的身份逻辑进行过多的传播。乍一看,咱们能够建立一个新的方法,好比isSame,这个方法的入参是一个Purchase
对象,并对每一个对象的id进行比较,看看它们是否相同:
public class Purchase { private long id; public boolean isSame(Purchase other) { return getId() == other.gerId(); } }
虽然这是一个有效的解决方案,可是忽略了Java的内置功能:使用equals方法。Java中的每一个类都是继承了Object
类,虽然是隐式的,所以一样也就继承了equals
方法。默认状况下,此方法将检查对象标识(内存中相同的对象),如JDK中的对象类定义(version 1.8.0_131)中的如下代码片断所示:
public boolean equals(Object obj) { return (this == obj); }
这个equals
方法充当了注入身份逻辑的天然位置(经过覆盖默认的equals
实现):
public class Purchase { private long id; public long getId() { return id; } public void setId(long id) { this.id = id; } @Override public boolean equals(Object other) { if (this == other) { return true; } else if (!(other instanceof Purchase)) { return false; } else { return ((Purchase) other).getId() == getId(); } } }
虽然这个equals
方法看起来很复杂,但因为equals
方法只接受类型对象的参数,因此咱们只须要考虑三个案例:
另外一个对象是当前对象(即originalPurchase.equals(originalPurchase)
),根据定义,它们是同一个对象,所以返回true
另外一个对象不是Purchase
对象,在这种状况下,咱们没法比较Purchase
的id,所以,这两个对象不相等
其余对象不是同一个对象,但倒是Purchase
的实例,所以,是否相等取决于当前Purchase
的id和其余Purchase
是否相等
如今能够重构咱们以前的条件,以下:
Purchase originalPurchase = new Purchase(); Purchase updatedPurchase = new Purchase(); if (originalPurchase.equals(updatedPurchase)) { // Execute some logic for equal purchases }
除了能够在系统中减小复制,重构默认的equals方法还有一些其它的优点。例如,若是构造一个Purchase
对象列表,并检查列表是否包含具备相同ID(内存中不一样对象)的另外一个Purchase
对象,那么咱们就会获得true值,由于这两个值被认为是相等的:
List<Purchase> purchases = new ArrayList<>(); purchases.add(originalPurchase); purchases.contains(updatedPurchase); // True
一般,不管在什么地方,若是须要判断两个类是否相等,则只须要使用重写过的equals
方法就能够了。若是但愿使用因为继承了Object
对象而隐式具备的equals
方法去判断相等性,咱们还可使用= =操做符,以下:
if (originalPurchase == updatedPurchase) { // The two objects are the same objects in memory }
还须要注意的是,当equals
方法被重写之后,hashCode
方法也应该被重写。有关这两种方法之间关系的更多信息,以及如何正肯定义hashCode
方法,请参见此线程。
正如咱们所看到的,重写equals
方法不只能够将身份逻辑在类的内部进行初始化,并在整个系统中减小了这种逻辑的扩散,它还容许Java语言对类作出有根据的决定。
对于任何一门编程语言来讲,条件句都是一种很常见的结构,并且它的存在也是有必定缘由的。由于不一样的组合能够容许用户根据给定值或对象的瞬时状态改变系统的行为。假设用户须要计算各银行帐户的余额,那么就能够开发出如下的代码:
public enum BankAccountType { CHECKING, SAVINGS, CERTIFICATE_OF_DEPOSIT; } public class BankAccount { private final BankAccountType type; public BankAccount(BankAccountType type) { this.type = type; } public double getInterestRate() { switch(type) { case CHECKING: return 0.03; // 3% case SAVINGS: return 0.04; // 4% case CERTIFICATE_OF_DEPOSIT: return 0.05; // 5% default: throw new UnsupportedOperationException(); } } public boolean supportsDeposits() { switch(type) { case CHECKING: return true; case SAVINGS: return true; case CERTIFICATE_OF_DEPOSIT: return false; default: throw new UnsupportedOperationException(); } } }
虽然上面这段代码知足了基本的要求,可是有个很明显的缺陷:用户只是根据给定账户的类型决定系统的行为。这不只要求用户每次要作决定以前都须要检查帐户类型,还须要在作出决定时重复这个逻辑。例如,在上面的设计中,用户必须在两种方法都进行检查才能够。这就可能会出现失控的状况,特别是接收到添加新账户类型的需求时。
咱们可使用多态来隐式地作出决策,而不是使用帐户类型用来区分。为了作到这一点,咱们将BankAccount的具体类转换成一个接口,并将决策过程传入一系列具体的类,这些类表明了每种类型的银行账户:
public interface BankAccount { public double getInterestRate(); public boolean supportsDeposits(); } public class CheckingAccount implements BankAccount { @Override public double getIntestRate() { return 0.03; } @Override public boolean supportsDeposits() { return true; } } public class SavingsAccount implements BankAccount { @Override public double getIntestRate() { return 0.04; } @Override public boolean supportsDeposits() { return true; } } public class CertificateOfDepositAccount implements BankAccount { @Override public double getIntestRate() { return 0.05; } @Override public boolean supportsDeposits() { return false; } }
这不只将每一个账户特有的信息封装到了到本身的类中,并且还支持用户能够在两种重要的方式中对设计进行变化。首先,若是想要添加一个新的银行账户类型,只需建立一个新的具体类,实现了BankAccount
的接口,给出两个方法的具体实现就能够了。在条件结构设计中,咱们必须在枚举中添加一个新值,在两个方法中添加新的case语句,并在每一个case语句下插入新账户的逻辑。
其次,若是咱们但愿在BankAccount接口中添加一个新方法,咱们只需在每一个具体类中添加新方法。在条件设计中,咱们必须复制现有的switch语句并将其添加到咱们的新方法中。此外,咱们还必须在每一个case语句中添加每一个账户类型的逻辑。
在数学上,当咱们建立一个新方法或添加一个新类型时,咱们必须在多态和条件设计中作出相同数量的逻辑更改。例如,若是咱们在多态设计中添加一个新方法,咱们必须将新方法添加到全部n个银行账户的具体类中,而在条件设计中,咱们必须在咱们的新方法中添加n个新的case语句。若是咱们在多态设计中添加一个新的account类型,咱们必须在BankAccount接口中实现全部的m数,而在条件设计中,咱们必须向每一个m现有方法添加一个新的case语句。
虽然咱们必须作的改变的数量是相等的,但变化的性质倒是彻底不一样的。在多态设计中,若是咱们添加一个新的账户类型而且忘记包含一个方法,编译器会抛出一个错误,由于咱们没有在咱们的BankAccount接口中实现全部的方法。在条件设计中,没有这样的检查,以确保每一个类型都有一个case语句。若是添加了新类型,咱们能够简单地忘记更新每一个switch语句。这个问题越严重,咱们就越重复咱们的switch语句。咱们是人类,咱们倾向于犯错误。所以,任什么时候候,只要咱们能够依赖编译器来提醒咱们错误,咱们就应该这么作。
关于这两种设计的第二个重要注意事项是它们在外部是等同的。例如,若是咱们想要检查一个支票账户的利率,条件设计就会相似以下:
BankAccount checkingAccount = new BankAccount(BankAccountType.CHECKING); System.out.println(checkingAccount.getInterestRate()); // Output: 0.03
相反,多态设计将相似以下:
BankAccount checkingAccount = new CheckingAccount(); System.out.println(checkingAccount.getInterestRate()); // Output: 0.03
从外部的角度来看,咱们只是在BankAccount对象上调用getintereUNK()。若是咱们将建立过程抽象为一个工厂类的话,这将更加明显:
public class ConditionalAccountFactory { public static BankAccount createCheckingAccount() { return new BankAccount(BankAccountType.CHECKING); } } public class PolymorphicAccountFactory { public static BankAccount createCheckingAccount() { return new CheckingAccount(); } } // In both cases, we create the accounts using a factory BankAccount conditionalCheckingAccount = ConditionalAccountFactory.createCheckingAccount(); BankAccount polymorphicCheckingAccount = PolymorphicAccountFactory.createCheckingAccount(); // In both cases, the call to obtain the interest rate is the same System.out.println(conditionalCheckingAccount.getInterestRate()); // Output: 0.03 System.out.println(polymorphicCheckingAccount.getInterestRate()); // Output: 0.03
将条件逻辑替换成多态类是很是常见的,所以已经发布了将条件语句重构为多态类的方法。这里就有一个简单的例子。此外,马丁·福勒(Martin Fowler)的《重构》(p . 255)也描述了执行这个重构的详细过程。
就像本文中的其余技术同样,对于什么时候执行从条件逻辑转换到多态类,没有硬性规定。事实上,如论在何种状况下咱们都是不建议使用。在测试驱动的设计中:例如,Kent Beck设计了一个简单的货币系统,目的是使用多态类,但发现这使设计过于复杂,因而便将他的设计从新设计成一个非多态风格。经验和合理的判断将决定什么时候是将条件代码转换为多态代码的合适时间。
做为程序员,尽管日常所使用的常规技术能够解决大部分的问题,但有时咱们应该打破这种常规,主动需求一些创新。毕竟做为一名开发人员,扩展本身知识面的的广度和深度,不只能让咱们作出更明智的决定,也能让咱们变得愈来愈聪明。