The content of this article is about summarizing four techniques for writing Java code. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.
Our usual programming tasks are nothing more than applying the same technology suite to different projects. For most cases, these technologies can meet the goals. However, some projects may require the use of some special technologies, so engineers have to delve deeper to find the simplest but most effective methods. In this article we will introduce some general design strategies and goal achievement techniques that can help solve common problems, namely:
Only do purposeful optimization
Use enumerations as much as possible for constants
Redefine the equals() method in the class
Use polymorphism as much as possible
It is important to note that the techniques described in this article may not be applicable in all situations. In addition, when and where these technologies should be used require careful consideration by the user.
Large software systems must pay great attention to performance issues. Although we hope to write the most efficient code, many times, if we want to optimize the code, we have no way to start. For example, will the following code affect performance?
public void processIntegers(List<Integer> integers) { for (Integer value: integers) { for (int i = integers.size() - 1; i >= 0; i--) { value += integers.get(i); } } }
It depends on the situation. It can be seen from the above code that its processing algorithm is O(n³) (using big O notation), where n is the size of the list collection. If n were only 5, then there would be no problem and only 25 iterations would be performed. But if n is 100,000, it may affect performance. Please note that even then we cannot be certain that there will be a problem. Although this method requires performing 1 billion logical iterations, whether it will have any impact on performance is still open to discussion.
For example, assuming the client executes this code in its own thread and waits asynchronously for the calculation to complete, its execution time may be acceptable. Similarly, if the system is deployed in a production environment, but there is no client to make calls, then we do not need to optimize this code at all, because it will not consume the overall performance of the system at all. In fact, after optimizing performance, the system will become more complex, but the tragedy is that the performance of the system does not improve as a result.
The most important thing is that there is no free lunch in the world, so in order to reduce the cost, we usually implement optimization through technologies such as caching, loop expansion, or precomputed values, which actually increases the complexity of the system. , also reduces the readability of the code. If this optimization can improve the performance of the system, then even if it becomes complicated, it is worth it, but before making a decision, you must first know these two pieces of information:
What are the performance requirements?
Where is the performance bottleneck
First we need to clearly know what the performance requirements are. If it is ultimately within the requirements and the end users do not raise any objections, then there is no need to perform performance optimization. However, when new functions are added or the data volume of the system reaches a certain scale, optimization must be performed, otherwise problems may occur.
In this case, you should not rely on intuition, nor should you rely on inspection. Because even experienced developers like Martin Fowler are prone to doing some wrong optimizations, as explained in the article Refactoring (page 70):
If enough programs are analyzed, What you'll find interesting about performance is that most of the time is wasted in a small part of the system. If the same optimization is performed on all code, the end result is that 90% of the optimization is wasted because the optimized code does not run as frequently. Any time spent optimizing without a goal is a waste of time.
As a seasoned developer, we should take this perspective seriously. Not only does the first guess not improve system performance, but 90% of development time is completely wasted. Instead, we should execute common use cases in a production environment (or a pre-production environment), find out which parts of the system are consuming system resources during execution, and then configure the system. For example, if only 10% of the code consumes most resources, optimizing the remaining 90% of the code is a waste of time.
According to the analysis results, to use this knowledge, we should start with the most common situations. Because this will ensure that the actual effort ultimately improves the performance of the system. After each optimization, the analysis steps should be repeated. Because this not only ensures that the performance of the system is really improved, but also shows where the performance bottleneck is after optimizing the system (because after one bottleneck is solved, other bottlenecks may consume more overall resources of the system) . Note that the percentage of time spent in existing bottlenecks is likely to increase because the remaining bottlenecks are temporarily unchanged, and the overall execution time should decrease as the target bottleneck is eliminated.
尽管在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设计了一个简单的货币系统,目的是使用多态类,但发现这使设计过于复杂,于是便将他的设计重新设计成一个非多态风格。经验和合理的判断将决定何时是将条件代码转换为多态代码的合适时间。
作为程序员,尽管平常所使用的常规技术可以解决大部分的问题,但有时我们应该打破这种常规,主动需求一些创新。毕竟作为一名开发人员,扩展自己知识面的的广度和深度,不仅能让我们做出更明智的决定,也能让我们变得越来越聪明。
相关推荐:
总结Java SE、Java EE、Java ME三者的区别
The above is the detailed content of Summarize four tips for writing Java code. For more information, please follow other related articles on the PHP Chinese website!