35 Java Code Performance Optimization Tips for Android Developers

Author: Java Team Leader

Original Article Link: http://www.jianshu.com/p/436943216526

Introduction

Code optimization is a very important topic. Some people may think it’s useless; what’s the point of modifying small details? What impact does it have on the efficiency of code execution? I consider it like a whale in the ocean; does it benefit from eating a small shrimp? No, it doesn’t, but when there are many small shrimps, the whale gets full. Code optimization is similar; if a project focuses on going live quickly without bugs, then it can overlook details, but if there is enough time to develop and maintain code, then every detail that can be optimized must be considered. Each small optimization point accumulates and absolutely improves the efficiency of code execution.

The goals of code optimization are:

  • Reduce the size of the code

  • Improve the efficiency of code execution

Code Optimization Details

1. Try to specify the final modifier for classes and methods

A class with the final modifier cannot be inherited. There are many examples of using final in the Java core API, such as java.lang.String, which is entirely final. Specifying a class as final prevents it from being inherited, and specifying a method as final prevents it from being overridden. If a class is declared final, then all of its methods are also final. The Java compiler looks for opportunities to inline all final methods, which significantly enhances Java’s execution efficiency. This can improve performance by an average of 50%.

2. Reuse objects as much as possible

Especially for String objects, when concatenating strings, use StringBuilder/StringBuffer instead. The Java virtual machine not only takes time to generate objects, but it may also need to spend time on garbage collection and processing these objects later; thus, generating too many objects can greatly affect the program’s performance.

3. Use local variables whenever possible

The parameters passed when calling methods and temporary variables created during the call are stored in the stack, which is faster. Other variables, such as static variables and instance variables, are created in the heap, which is slower. Additionally, variables created in the stack disappear when the method finishes running, eliminating the need for extra garbage collection.

4. Close streams promptly

During Java programming, be cautious when performing database connections and I/O stream operations; always close them promptly after use to free resources. Operations on these large objects can incur significant system overhead, and negligence can lead to serious consequences.

5. Minimize repeated calculations of variables

It’s important to understand that calling a method, even if it contains only one statement, incurs costs, including creating stack frames, saving the state when calling the method, and restoring the state after the method call. For example:

for (int i = 0; i < list.size(); i++){...}

It is recommended to replace it with:

for (int i = 0, int length = list.size(); i < length; i++){...}

This way, when list.size() is large, it reduces a lot of overhead.

6. Use lazy loading strategies whenever possible, i.e., create only when needed

For example:

String str = "aaa"; if (i == 1) { list.add(str); }

It is recommended to replace it with:

if (i == 1) { String str = "aaa"; list.add(str); }

7. Use exceptions cautiously

Exceptions are detrimental to performance. Throwing an exception first creates a new object, and the constructor of the Throwable interface calls a local synchronized method named fillInStackTrace(), which checks the stack and collects call trace information. Whenever an exception is thrown, the Java virtual machine must adjust the call stack because a new object has been created during processing. Exceptions should only be used for error handling and not for controlling program flow.

8. Avoid using try…catch… inside loops; it should be placed at the outermost level

Unless absolutely necessary. If written without reason, and if your leader is a bit senior or obsessive, they will likely scold you for writing such poor code.

9. If you can estimate the length of content to be added, specify the initial length for collections implemented with arrays, and utility classes

For example, ArrayList, LinkedList, StringBuilder, StringBuffer, HashMap, HashSet, etc. For StringBuilder:

(1) StringBuilder() // allocates space for 16 characters by default

(2) StringBuilder(int size) // allocates space for ‘size’ characters by default

(3) StringBuilder(String str) // allocates space for 16 characters + str.length() characters by default

You can set its initial capacity through classes (not just the StringBuilder mentioned above), which can significantly enhance performance. For example, for StringBuilder, ‘length’ indicates the number of characters it can hold. When StringBuilder reaches maximum capacity, it will double its capacity plus two, and whenever it reaches maximum capacity, it must create a new character array and copy the old character array’s contents to the new one, which is a performance-consuming operation. If you can estimate that around 5000 characters will be stored in the character array without specifying length, the closest power of 2 to 5000 is 4096. Each time it expands by 2, then:

(1) On the basis of 4096, it will allocate a character array of size 8194, resulting in a total allocation of 12290 characters at once if the initial size can be specified as 5000, saving more than half the space.

(2) The original 4096 characters will be copied to the new character array.

This way, it wastes memory space and reduces code execution efficiency. Therefore, setting a reasonable initial capacity for collections and utility classes implemented with arrays is always beneficial. However, note that for collections like HashMap, which are implemented with arrays + linked lists, do not set the initial size equal to your estimated size, as the probability of only one object being connected to a table is almost zero. It is recommended to set the initial size to a power of 2. If you can estimate there will be 2000 elements, set it to new HashMap(128) or new HashMap(256).

10. When copying large amounts of data, use System.arraycopy()

11. Use bitwise operations for multiplication and division

For example:

for (val = 0; val < 100000; val += 5) { a = val * 8; b = val / 2; }

Using bitwise operations can greatly improve performance since bit manipulation is the most efficient operation at the computer’s core, so it is recommended to modify it to:

for (val = 0; val < 100000; val += 5) { a = val << 3; b = val >> 1; }

While bitwise operations are fast, they may make the code less understandable, so it’s best to add corresponding comments.

12. Do not continuously create object references inside loops

For example:

for (int i = 1; i <= count; i++) { Object obj = new Object(); }

This practice can lead to count references of Object objects existing in memory, which can consume a lot of memory if count is large. It is recommended to change it to:

Object obj = null; for (int i = 0; i <= count; i++) { obj = new Object(); }

In this way, there is only one Object object reference in memory, and each time new Object() is called, the Object object reference points to a different Object, greatly saving memory space.

13. Based on efficiency and type-checking considerations, use arrays whenever possible; only use ArrayList when the array size cannot be determined

14. Use HashMap, ArrayList, and StringBuilder as much as possible; unless thread safety is needed, do not recommend using Hashtable, Vector, and StringBuffer, as the latter three incur performance overhead due to synchronization mechanisms

15. Do not declare arrays as public static final

Because this is meaningless; it only defines the reference as static final, but the contents of the array can still be changed at will. Declaring an array as public is even more of a security vulnerability, meaning this array can be altered by external classes.

16. Use singletons appropriately

Using singletons can reduce the loading burden, shorten loading time, and improve loading efficiency, but singletons are not suitable for all situations. In simple terms, singletons are mainly applicable in the following three areas:

(1) Control resource usage by synchronizing threads to control concurrent access to resources

(2) Control instance creation to save resources

(3) Control data sharing, allowing communication between unrelated processes or threads without establishing direct associations

17. Avoid using static variables indiscriminately

Be aware that when an object is referenced by a static variable, the garbage collector typically does not reclaim the heap memory occupied by that object. For example:

public class A { private static B b = new B(); }

In this case, the lifecycle of the static variable b is the same as that of class A. If class A is not unloaded, the B object referenced by b will remain in memory until the program terminates.

18. Timely clear sessions that are no longer needed

To clear inactive sessions, many application servers have a default session timeout, usually 30 minutes. When application servers need to save more sessions and memory is insufficient, the operating system may transfer some data to disk, and the application server may also use the MRU (Most Recently Used) algorithm to dump some inactive sessions to disk, possibly even throwing memory shortage exceptions. If a session is to be dumped to disk, it must first be serialized, and in large-scale clusters, the cost of serializing objects is quite high. Therefore, when a session is no longer needed, the HttpSession’s invalidate() method should be called promptly to clear the session.

19. When using collections that implement the RandomAccess interface like ArrayList, use the most basic for loop instead of foreach loop to traverse

This is recommended by the JDK. The JDK API explains the RandomAccess interface as follows: it indicates support for fast random access, and its main purpose is to allow general algorithms to change behavior, thus providing good performance when applied to random or sequential access lists. Practical experience shows that when instances of classes implementing the RandomAccess interface are accessed randomly, using a normal for loop is more efficient than using a foreach loop; conversely, if accessed sequentially, using an Iterator will be more efficient. You can use code like this to judge:

if (list instanceof RandomAccess) { for (int i = 0; i < list.size(); i++) {} } else { Iterator<?> iterator = list.iterator(); while (iterator.hasNext()) { iterator.next(); } }

The underlying implementation principle of foreach loop is the Iterator, so the latter part of the sentence means that for instances of classes accessed sequentially, using foreach loop for traversal is more efficient.

20. Use synchronized blocks instead of synchronized methods

This point has been clearly explained in the article on synchronized lock method blocks in the multithreading module; unless it is certain that an entire method requires synchronization, it is best to use synchronized blocks to avoid synchronizing code that does not need to be synchronized, which would affect code execution efficiency.

21. Declare constants as static final and name them in uppercase

This way, these contents can be placed into the constant pool during compilation, avoiding the need to calculate constant values during runtime. Additionally, naming constants in uppercase makes it easy to distinguish constants from variables.

22. Do not create unused objects or import unused classes

This is meaningless. If the code contains warnings such as “The value of the local variable i is not used” or “The import java.util is never used,” please delete these useless contents.

23. Avoid using reflection during program execution

For more information, see Reflection. Reflection is a powerful feature provided by Java, but its power often means lower efficiency. It is not recommended to use reflection frequently during program execution, especially the Method’s invoke method. If necessary, a suggested practice is to instantiate an object of the class that needs to be loaded via reflection at the start of the project and keep it in memory—users only care about getting the fastest response speed when interacting with the other end and do not care how long it takes for the other end to start the project.

24. Use connection pools and thread pools

Both pools are used for object reuse; the former can avoid frequently opening and closing connections, while the latter can avoid frequently creating and destroying threads.

25. Use buffered input and output streams for IO operations

Buffered input and output streams, such as BufferedReader, BufferedWriter, BufferedInputStream, and BufferedOutputStream, can greatly enhance IO efficiency.

26. Use ArrayList for scenarios with frequent sequential insertions and random access; use LinkedList for scenarios with frequent element deletions and insertions in the middle

This can be understood by understanding the principles of ArrayList and LinkedList.

27. Do not have too many parameters in public methods

Public methods are those provided to the outside. If these methods have too many parameters, there are two main drawbacks:

1. It violates the object-oriented programming principle, as Java emphasizes that everything is an object; too many parameters do not align with object-oriented programming principles.

2. Having too many parameters inevitably increases the probability of errors when calling methods.

As for what constitutes “too many,” about 3 or 4 is appropriate. For instance, when writing an insertStudentInfo method with 10 student information fields to insert into the Student table, these 10 parameters can be encapsulated in an entity class and passed as the parameter for the insert method.

28. When comparing string variables with string constants, write string constants first

This is a common small trick. If the following code appears:

String str = "123"; if (str.equals("123")) {...}

It is recommended to modify it to:

String str = "123"; if ("123".equals(str)) {...}

This mainly avoids null pointer exceptions.

29. Understand that in Java, if (i == 1) and if (1 == i) are indistinguishable, but from a reading habit perspective, the former is recommended

People often ask whether there is a difference between “if (i == 1)” and “if (1 == i)”. This goes back to C/C++. In C/C++, the condition of “if (i == 1)” is determined based on 0 and non-zero; 0 represents false, and non-zero represents true. If there is a piece of code:

int i = 2; if (i == 1) {...} else {...}

The condition “i==1” is false, so it returns 0, indicating false. However, if:

int i = 2; if (i = 1) {...} else {...}

There is a risk that a programmer could accidentally write “if (i = 1)” instead of “if (i == 1)”, leading to a situation where i is assigned 1, and the condition will return true, despite i being 2. This kind of error can occur frequently in C/C++ development and can lead to difficult-to-understand bugs. Therefore, to avoid incorrect assignment operations in if statements, it is recommended to write the if statement as:

int i = 2; if (1 == i) {...} else {...}

This way, even if a developer accidentally writes “1 = i”, the C/C++ compiler will catch the error immediately, as a constant cannot be assigned to a variable.

However, in Java, the syntax of “if (i = 1)” is impossible because Java will compile with an error “Type mismatch: cannot convert from int to boolean” if such syntax is written. Nevertheless, even though there is no semantic difference between “if (i == 1)” and “if (1 == i)” in Java, it is still recommended to use the former for better readability.

30. Do not use toString() method on arrays

Check what happens when using toString() on an array:

public static void main(String[] args) { int[] is = new int[]{1, 2, 3}; System.out.println(is.toString()); }

The result is:

[I@18a992f

The intention is to print the contents of the array, but it may lead to a null pointer exception if the array reference is null. However, using toString() on collections can print the contents of the collection because the parent class AbstractCollections<E> overrides the toString() method of Object.

31. Avoid downcasting basic data types that exceed their range

This will never yield the expected result:

public static void main(String[] args) { long l = 12345678901234L; int i = (int)l; System.out.println(i); }

We might expect to get some of its digits, but the result is:

1942892530

To explain, Java’s long is 8 bytes (64 bits), so 12345678901234 is represented in the computer as:

0000 0000 0000 0000 0000 1011 0011 1010 0111 0011 1100 1110 0010 1111 1111 0010

An int data type is 4 bytes (32 bits), so extracting the first 32 bits from the above binary representation yields:

0111 0011 1100 1110 0010 1111 1111 0010

This binary representation corresponds to the decimal value 1942892530, which is what we see in the console output. From this example, we can also draw two conclusions:

1. The default data type for integers is int, and since long l = 12345678901234L exceeds the range of int, we add an L to indicate that this is a long type. Similarly, the default data type for floating-point numbers is double, so defining float should be written as “float f = 3.5f”.

2. Writing “int ii = l + i;” will result in an error because long + int is a long and cannot be assigned to an int.

32. Remove unused data from public collection classes in a timely manner

If a collection class is public (meaning it is not a property inside a method), the elements in this collection will not be released automatically since there are always references pointing to them. Therefore, if some data in a public collection is not used and not removed, it can lead to the public collection continuously growing, posing a risk of memory leaks.

33. Use the most efficient way to convert basic data types to strings

There are three common ways to convert a basic data type to a string: basic data type.toString() is the fastest, String.valueOf(data) is second, and data + “” is the slowest.

For instance, if I have an Integer type data i, I can use i.toString(), String.valueOf(i), and i + “”. Let’s check the efficiency of these three methods:

public static void main(String[] args) { int loopTime = 50000; Integer i = 0; long startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++) { String str = String.valueOf(i); } System.out.println("String.valueOf():" + (System.currentTimeMillis() - startTime) + "ms"); startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++) { String str = i.toString(); } System.out.println("Integer.toString():" + (System.currentTimeMillis() - startTime) + "ms"); startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++) { String str = i + ""; } System.out.println("i + \"\":" + (System.currentTimeMillis() - startTime) + "ms"); }

The results are:

String.valueOf(): 11ms Integer.toString(): 5ms i + 

Leave a Comment