http://geek.csdn.net/news/detail/238243
Original: 4 more techniques for Writing Better Java
Justin Albano
Translation: Vincent
Translator: What would you do if you wanted to optimize the Java code you wrote now? This article describes four ways to improve system performance and code readability, and if you're interested, let's take a look. The translation is as follows.
Our usual programming task is to apply the same technology suite to different projects, and for the most part, these technologies can meet the goals. However, some projects may require some special techniques, so engineers have to delve into the simplest but most effective way to find out. In the previous article, we discussed the four special techniques that can be used when necessary, which can create better Java software, and this article introduces common design strategies and target implementation techniques that help solve common problems, namely:
- Only purposeful optimization.
- Constants use enumerations as much as possible
- Redefine the methods inside the class
equals()
- Use polymorphism as much as possible
It is important to note that the techniques described in this article do not apply to all situations. In addition, when and where these technologies should be used, they need to be considered by the user.
1. Only purposeful optimization
Large software systems are definitely concerned with performance issues. Although we want to be able to write the most efficient code, many times, if we want to optimize the code, we can not do it. For example, does 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); } }}
This is subject to the circumstances . The above code shows that its processing algorithm is O (N3) (using the large o symbol), where n is the size of the list collection. If n is only 5, then there will be no problem and only 25 iterations will be performed. But if n is 100,000, that might affect performance. Please note that even then we cannot determine that there are certain problems. Although this method needs to perform 1 billion logical iterations, it remains to be seen whether the performance impact will occur.
For example, assuming that the client executes this code in its own thread and waits for the computation to complete asynchronously, its execution time may be acceptable. Similarly, if the system is deployed in a production environment, but no client calls, there is no need to optimize the code because it does not consume the overall performance of the system at all. In fact, the system becomes more complex after optimizing performance, and the tragedy is that the performance of the system does not improve.
The most important thing is that there is no free lunch, so in order to reduce the cost, we usually use a technique similar to caching, cyclic expansion, or expected value to achieve optimization, which increases the complexity of the system and reduces the readability of the code. If this optimization can improve the performance of the system, even if it becomes complex, it is worthwhile, but before making a decision, you must first know these two messages:
- What are the performance requirements
- Where are the performance bottlenecks?
First, we need to know exactly what the performance requirements are. If the end result is within the requirements and the end user does not raise any objections, then there is no need for performance optimization. However, when a new feature is added or the amount of data on the system reaches a certain scale, it must be optimized, otherwise problems may occur.
In this case, it should not rely on intuition, nor should it rely on censorship. Because even experienced developers like Martin Fowler are prone to do some wrong optimizations, as explained in the refactoring (page 70th) article:
If you analyze enough programs, you'll find that the interesting thing about performance is that most of the time is wasted in a fraction of the code in the system. If the same optimizations were made for all the code, the end result would be to waste 90% of the optimizations, since the code that was optimized would not run much more frequently. The time it takes to optimize for no purpose is a waste of time.
As a seasoned developer, we should take this view seriously. The first guess not only did not improve the performance of the system, but 90% of the development time is completely wasted. Instead, we should perform common use cases in a production environment (or in a pre-production environment) and find out which part of the process is consuming system resources and then configuring the system. For example, the code that consumes most of the resources accounts for only 10%, so optimizing the remaining 90% of the code is a waste of time.
Based on the results of the analysis, we should start with the most common situations in order to use this knowledge. Because this will ensure that the actual effort is ultimately able to improve 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 that after the system is optimized, the performance bottleneck is in which part (because after a bottleneck is resolved, other bottlenecks may consume more of the overall resources of the system). It is important to note that the percentage of time spent in existing bottlenecks is likely to increase as the remaining bottlenecks are temporary and the overall execution time should be reduced as the target bottleneck is eliminated.
While it takes a lot of capacity to do a full review of the profile in a Java system, there are some common tools that can help you discover the performance hotspots of your system, including JMeter, AppDynamics, and Yourkit. Also, refer to the Dzone Performance Monitoring Guide for more information on Java program Performance optimizations.
While performance is a very important part of many large software systems, it is also part of the automated test suite in the product delivery pipeline, but it cannot be optimized blindly and without purpose. Instead, specific optimizations should be made to the performance bottlenecks already mastered. Not only does this help us avoid adding complexity to the system, but it also allows us to take fewer detours and not do the time-wasting optimizations.
2. Constants use enumerations as much as possible
There are many scenarios that require users to list a set of predefined or constant values, such as HTTP response codes that may be encountered in a Web application. One of the most common implementation techniques is to create a new class that has a number of static final type values, each of which should have a comment that describes what the value means:
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 }
It's good to have this idea, but there are some drawbacks:
- No strict validation of incoming integer values
- The method on the status code cannot be called because it is a basic data type
In the first case, simply creating a specific represented represents a special integer value, but there is no restriction on the method or variable, so the value used may exceed the defined range. For example:
public class HttpResponseHandler { public static void printMessage(int statusCode) { System.out.println("Recieved status of " + statusCode); }}HttpResponseHandler.printMessage(15000);
Although 15000
it is not a valid HTTP response code, there is no restriction on the server side that the client must provide a valid integer. In the second case, we have no way to define the method for the status code. For example, if you want to check whether a given status code is a successful code, you must define a separate function:
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 }
To solve these problems, we need to change the constant type from the base data type to the custom type and allow only the specific objects of the custom class. This is the purpose of the Java Enumeration (enum). With enum, we can solve both of these problems at once:
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 }
Again, it is now possible to require that a valid status code be provided when the method is called:
public class HttpResponseHandler { public static void printMessage(HttpResponseCode statusCode) { System.out.println("Recieved status of " + statusCode.getCode()); }}HttpResponseHandler.printMessage(HttpResponseCode.OK);
It is worth noting that this example shows that if you are a constant, you should use enumerations as much as possible, but not that you should use enumerations. In some cases, you might want to use a constant to represent a particular value, but it also allows you to provide additional values. For example, we may all know that pi, we can use a constant to capture this value (and reuse it):
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);
Therefore, the rules that use enumerations can be summed up as:
When all possible discrete values have been known in advance, you can use the enumeration
Taking the HTTP response code mentioned above as an example, we may know all the values of the HTTP status code (which can be found in RFC 7231, which defines the HTTP 1.1 protocol). Therefore, an enumeration is used. In the case of pi, we do not know all possible values of pi (any possible double is valid), but at the same time want to create a constant for the circular rugs, making the calculation easier (easier to read), and therefore defining a series of constants.
If you don't know all the possible values in advance, but want to include a field or method for each value, the simplest way is to create a new class to represent the data. Although it is not said that the scenario should absolutely not enumerate, the key to knowing where and when not to use the enumeration is to be aware of all the values in advance and prohibit any other values.
3. Redefine the Equals () method inside the class
Object recognition can be a difficult problem: if two objects occupy the same position in memory, are they the same? If they have the same ID, are they the same? or if all the fields are equal? Although each class has its own identity logic, But there are many Western countries in the system that need to determine whether they are equal. For example, like the next class, which represents order purchase ...
public class Purchase { private long id; public long getId() { return id; } public void setId(long id) { this.id = id; }}
...... As written below, there must be many places in the code that are similar:
Purchase originalPurchase = new Purchase();Purchase updatedPurchase = new Purchase();if (originalPurchase.getId() == updatedPurchase.getId()) { // Execute some logic for equal purchases }
The more these logical calls are (in turn, against the dry principle), Purchase
the identity information for the class becomes more and more. If, for some reason, the identity logic of the class has changed Purchase
(for example, the type of the identifier has changed), there must be a lot more where the identity logic needs to be updated.
We should initialize this logic within the class, rather than Purchase
propagating the class's identity logic too much through the system. At first glance, we can create a new method, such as Issame, in which the parameter is an Purchase
object, and the ID of each object is compared to see if they are the same:
public class Purchase { private long id; public boolean isSame(Purchase other) { return getId() == other.gerId(); }}
Although this is an effective solution, it ignores the built-in functionality of Java: Using the Equals method. Each class in Java inherits the Object
class, although it is implicit, so it inherits the same equals
method. By default, this method checks the object identity (the same object in memory), as shown in the following code snippet in the object class definition (version 1.8.0_131) in the JDK:
public boolean equals(Object obj) { return (this == obj);}
This equals
method acts as a natural place to inject the identity logic (by overriding the default equals
implementation):
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(); } }}
Although this equals
method may seem complicated, we only need equals
to consider three cases because the method accepts only the parameters of the type object:
The other object is the current object (that is originalPurchase.equals(originalPurchase)
), which, by definition, is the same object and therefore returns true
Another object is not an Purchase
object, in which case we cannot compare Purchase
the IDs, so the two objects are not equal
Other objects are not the same object, but are Purchase
instances, so whether they are equal depends on Purchase
the current ID and other Purchase
equality
We can now refactor our previous conditions, as follows:
Purchase originalPurchase = new Purchase();Purchase updatedPurchase = new Purchase();if (originalPurchase.equals(updatedPurchase)) { // Execute some logic for equal purchases }
In addition to reducing replication in the system, there are other advantages to refactoring the default Equals method. For example, if you construct a Purchase
list of objects and check if the list contains another object with the same ID (different objects in memory), Purchase
then we get a true value because the two values are considered equal:
List<Purchase> purchases = new ArrayList<>();purchases.add(originalPurchase);purchases.contains(updatedPurchase); // True
In general, wherever you need to determine whether two classes are equal, you only need to use the overridden equals
method. We can also use the = = operator If you want to use an implicitly-inherited Object
equals
method to determine equality, as follows:
if (originalPurchase == updatedPurchase) { // The two objects are the same objects in memory }
It is also important to note that the equals
method should be rewritten when the method is rewritten hashCode
. For more information about the relationship between the two methods, and how to correctly define the hashCode
method, see this thread.
As we can see, the overriding equals
method not only initializes the identity logic within the class, but reduces the proliferation of this logic throughout the system, it also allows the Java language to make informed decisions about the class.
4. Use polymorphism as much as possible
For any programming language, conditional sentences are a very common structure, and there are certain reasons for its existence. Because different combinations can allow the user to change the behavior of the system based on the given value or the instantaneous state of the object. Assuming that the user needs to calculate the balance of each bank account, the following code can be developed:
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 (); } }}
Although the above code satisfies the basic requirements, there is an obvious flaw: the user simply determines the behavior of the system based on the type of account given. This not only requires the user to check the account type each time they make a decision, but also to repeat the logic when making a decision. For example, in the above design, the user must be checked in both ways. This can lead to runaway situations, especially when the need to add new account types is received.
We can use polymorphism to make decisions implicitly, rather than using account types to differentiate between them. To do this, we convert the specific class of bankaccount into an interface and pass the decision-making process into a series of specific classes that represent each type of bank account:
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; }}
This not only encapsulates the information specific to each account into its own class, but also enables users to change the design in two important ways. First of all, if you want to add a new bank account type, just create a new concrete class, the implementation BankAccount
of the interface, give two methods of the specific implementation of it. In a conditional structure design, we must add a new value to the enumeration, add a new case statement to the two methods, and insert the logic for the new account under each cases statement.
Second, if we want to add a new method to the BankAccount interface, we just need to add a new method to each specific class. In the conditional design, we must copy the existing switch statement and add it to our new method. In addition, we must add the logic for each account type in each case statement.
Mathematically, when we create a new method or add a new type, we must make the same number of logical changes in the polymorphic and conditional designs. For example, if we add a new method to a polymorphic design, we must add the new method to the specific class of all n bank accounts, and in the conditional design, we must add n new case statements in our new method. If we add a new account type to the polymorphic design, we must implement all the M numbers in the BankAccount interface, and in the conditional design, we must add a new case statement to each m existing method.
Although the number of changes we have to make is equal, the nature of the change is completely different. In a polymorphic design, if we add a new account type and forget to include a method, the compiler throws an error because we do not implement all the methods in our BankAccount interface. In a conditional design, there is no such check to ensure that each type has a case statement. If you add a new type, we can simply forget to update each switch statement. The more serious the problem is, the more we repeat our switch statement. We are human, and we tend to make mistakes. So, whenever we can rely on the compiler to remind us of the error, we should do so.
The second important consideration with respect to both designs is that they are externally equivalent. For example, if we want to check the interest rate for a checking account, the condition design would look like this:
BankAccount checkingAccount = new BankAccount(BankAccountType.CHECKING);System.out.println(checkingAccount.getInterestRate()); // Output: 0.03
Instead, the polymorphic design would look similar to the following:
BankAccount checkingAccount = new CheckingAccount();System.out.println(checkingAccount.getInterestRate()); // Output: 0.03
From an external point of view, we just call Getintereunk () on the BankAccount object. If we were to abstract the creation process into a factory class, it would be more obvious:
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 factoryBankAccount conditionalCheckingAccount = ConditionalAccountFactory.createCheckingAccount();BankAccount polymorphicCheckingAccount = PolymorphicAccountFactory.createCheckingAccount();// In both cases, the call to obtain the interest rate is the sameSystem.out.println(conditionalCheckingAccount.getInterestRate()); // Output: 0.03System.out.println(polymorphicCheckingAccount.getInterestRate()); // Output: 0.03
Replacing conditional logic with polymorphic classes is very common, so the method of refactoring conditional statements into polymorphic classes has been published. Here is a simple example. In addition, Martin Fowler Martin Fowler's refactoring (see page 255) also describes the detailed process of performing this refactoring.
As with other techniques in this article, there is no hard rule as to when to perform transition from conditional logic to polymorphic classes. In fact, we do not recommend the use of such cases. In a test-driven design: For example, Kent Beck designed a simple currency system to use polymorphic classes, but found that it made the design too complex, and then redesigned his design into a non-polymorphic style. Experience and reasonable judgment will determine when the conditional code is converted to polymorphic code at the appropriate time.
Conclusion
As programmers, although the usual techniques used to solve most of the problems, sometimes we should break this routine and proactively demand some innovation. After all, as a developer, expanding the breadth and depth of your knowledge will not only allow us to make smarter decisions, but also make us smarter.
4 Tips for writing high-quality Java code (RPM)