35 Java Code Performance Optimization Tips for Android Developers

35 Java Code Performance Optimization Tips for Android Developers

Related Articles:

Awesome! Complete Source Code for 74 Apps!

Implementing Water Ripple Effect in Android: Detailed Explanation of Android.Path Bezier Drawing and Operations

[Essentials] The Most Comprehensive Summary of 2017 – These Android Interview Questions You Definitely Need

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, questioning what difference it makes to modify small details and how it affects the running efficiency of the code. I think of it this way: just like a whale in the ocean, does it make sense for it to eat a small shrimp? Not really, but once it eats enough small shrimps, it gets fed. Code optimization is similar; if a project aims to go live quickly with no bugs, then it’s okay to overlook small details. However, if there’s enough time to develop and maintain the code, each small optimization detail must be considered. Accumulating these small optimizations can significantly improve the code’s running efficiency.

The goals of code optimization are:

  • Reduce the size of the code

  • Improve the efficiency of code execution

Details of code optimization

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

Classes with the final modifier cannot be inherited. There are many examples of using final in Java’s core API, such as java.lang.String, which is a final class. Specifying a class as final means that all methods in that class are also final. The Java compiler will look for opportunities to inline all final methods, which significantly improves Java’s running efficiency. This can average a performance increase of 50%.

2. Try to reuse objects.

Especially for String objects, when string concatenation occurs, StringBuilder/StringBuffer should be used instead. Since the Java Virtual Machine not only takes time to generate objects but may also need time to garbage collect and handle these objects later, creating too many objects can significantly impact the program’s performance.

3. Use local variables as much as possible.

Parameters passed when calling methods and temporary variables created during the call are stored on the stack, which is faster. Other variables like static variables and instance variables are created in the heap, which is slower. Additionally, variables created on the stack disappear once the method execution ends, requiring no extra garbage collection.

4. Close streams promptly.

When programming in Java, be careful with database connections and I/O stream operations. Always close them promptly after use to release resources. Operations on these large objects can incur significant system overhead, and carelessness can lead to severe consequences.

5. Minimize repeated calculations of variables.

It’s important to understand that calling a method incurs a cost, even if the method contains only one statement, including creating stack frames, protecting the state during method calls, and restoring the state after the call. For example, replace the following operation:

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

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. Adopt lazy loading strategies, creating objects only when needed.

For example:

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

should be replaced with:

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

7. Use exceptions cautiously.

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

8. Do not use try…catch… inside loops; place it at the outermost level.

Unless absolutely necessary. If written without reason, your senior leader is likely to 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 and utility classes implemented in arrays.

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

(1) StringBuilder() // Default allocates space for 16 characters

(2) StringBuilder(int size) // Default allocates space for size characters

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

You can set the initial capacity of classes (not just the above StringBuilder), which can significantly enhance performance. For instance, for StringBuilder, length indicates the number of characters that the current StringBuilder can hold. When StringBuilder reaches its maximum capacity, it doubles its capacity plus 2. Whenever StringBuilder reaches its maximum capacity, it must create a new character array and copy the old character array content to the new one—this is a very performance-intensive operation. Imagine if you can estimate that about 5000 characters will be stored in the character array without specifying the length; the closest power of 2 to 5000 is 4096. Assuming each expansion adds 2, then:

(1) Based on 4096, applying for a character array of size 8194 results in a total allocation of 12290 characters at once, while specifying an initial size of 5000 saves more than half the space.

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

This results in wasted memory space and reduced code execution efficiency. Therefore, setting a reasonable initial capacity for collections and utility classes implemented with arrays is a good practice, which will yield immediate results. However, note that for collections like HashMap, which are implemented with arrays + linked lists, do not set the initial size equal to your estimate, as the likelihood of only one object being linked on a table is almost zero. It is recommended to set the initial size to a power of 2; if you estimate 2000 elements, set it to new HashMap(128) or new HashMap(256).

10. Use System.arraycopy() when copying large amounts of data.

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 because, at the computer level, bit operations are the most convenient and fastest. It is recommended to modify it to:

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

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

12. Do not continuously create object references within loops.

For example:

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

This practice will 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(); }

This way, there is only one Object object reference in memory; each time new Object() is called, the Object reference points to a different Object, significantly saving memory space.

13. Use arrays whenever possible for efficiency and type checking; use ArrayList only when the array size cannot be determined.

14. Use HashMap, ArrayList, StringBuilder as much as possible; unless thread safety is required, do not use Hashtable, Vector, StringBuffer as the last three have performance overhead due to synchronization mechanisms.

15. Do not declare arrays as public static final.

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

16. Use singletons appropriately.

Using singletons can reduce loading burdens, shorten loading times, and improve loading efficiency, but they are not suitable for all situations. In simple terms, singletons are mainly suitable for the following three aspects:

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

(2) Controlling the generation of instances to save resources.

(3) Controlling data sharing, allowing multiple unrelated processes or threads to communicate without establishing direct associations.

17. Avoid using static variables indiscriminately.

When an object is referenced by a static variable, the garbage collector typically does not reclaim the heap memory occupied by that object, as shown in:

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

At this point, the static variable b has the same lifecycle as class A. If class A is not unloaded, the B object referenced by B will remain in memory until the program terminates.

18. Clear unnecessary sessions promptly.

To clear inactive sessions, many application servers have a default session timeout, usually 30 minutes. When an application server needs to save more sessions and memory is insufficient, the operating system may move some data to disk. The application server may also transfer some inactive sessions to disk based on the MRU (Most Recently Used) algorithm, which could even throw a memory insufficient exception. If a session is to be transferred to disk, it must first be serialized, which can be very costly in large-scale clusters. Therefore, when sessions are no longer needed, the HttpSession’s invalidate() method should be called promptly to clear the session.

19. When traversing collections that implement the RandomAccess interface, such as ArrayList, use the most common for loop instead of foreach.

This is recommended by the JDK. The JDK API explains that the RandomAccess interface indicates support for fast random access. The main purpose of this interface is to allow general algorithms to change their behavior, thus providing good performance when applied to random or sequential access lists. Practical experience shows that instances of classes implementing the RandomAccess interface, when accessed randomly, will have higher efficiency using a regular for loop compared to using foreach; conversely, if accessed sequentially, using Iterator will be more efficient. You can use the following code to make this judgment:

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 of the foreach loop is the Iterator. Therefore, the latter sentence means that for instances of classes accessed sequentially, using foreach is more efficient.

20. Use synchronized blocks instead of synchronized methods.

This has been clearly stated in the article about synchronized lock method blocks in the multithreading module. Unless it is certain that an entire method needs to be synchronized, it is best to use synchronized blocks to avoid synchronizing code that does not require it, which would impact code execution efficiency.

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

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

22. Do not create unused objects or import unused classes.

This is meaningless; if the code shows “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 details, refer to the reflection section. Reflection provides users with a powerful function, but powerful features often mean low efficiency. It is not recommended to use the reflection mechanism frequently during program execution, especially the Method’s invoke method. If necessary, a recommended 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 endpoint and do not care how long the endpoint takes to start.

24. Use connection pools and thread pools.

These two 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 I/O operations.

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

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

This is understood by grasping the principles of ArrayList and LinkedList.

27. Do not have too many parameters in public methods.

Public methods are those provided externally. If these methods have too many parameters, it can lead to two main issues:

1. It violates the object-oriented programming principle; Java emphasizes that everything is an object. Too many parameters do not align with this principle.

2. Too many parameters increase the likelihood of errors in method calls.

As for what constitutes “too many,” it is around 3 or 4. For example, when writing an insertStudentInfo method with 10 fields to insert into a Student table using JDBC, these 10 parameters can be encapsulated in an entity class and passed as a parameter to the insert method.

28. When comparing string variables and string constants, write the string constant first.

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

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

It is recommended to modify it to:

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

This approach mainly avoids null pointer exceptions.

29. Be aware that in Java, if (i == 1) and if (1 == i) are no different, but from a reading habit perspective, it is recommended to use the former.

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 evaluated based on 0 and non-zero, where 0 represents false and non-zero represents true. If there is a piece of code:

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

C/C++ evaluates “i==1” as false (0). However, if:

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

There’s a chance the programmer mistakenly writes “if (i == 1)” as “if (i = 1)”. This would cause issues because it assigns 1 to i and evaluates as true, even though i is 2. This situation is likely to occur in C/C++ development and can lead to hard-to-understand errors. Therefore, to avoid incorrect assignment operations in if statements, it is recommended to write:

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

This way, even if developers mistakenly write “1 = i”, the C/C++ compiler will catch the error immediately, as we can assign a variable i to 1 but cannot assign a constant 1 to i.

However, in Java, the syntax “if (i = 1)” cannot occur as it will compile with an error “Type mismatch: cannot convert from int to boolean.” Nevertheless, although there is no semantic difference between Java’s “if (i == 1)” and “if (1 == i)”, it is better to use the former for reading habits.

30. Do not use the 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 will print their contents, as the parent class AbstractCollections overrides Object’s toString() method.

31. Do not forcefully downcast basic data types beyond their range.

This will never yield the expected results:

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

We might expect to get certain digits, but the result is:

1942892530

To explain, a long in Java 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 is 4 bytes (32 bits), so from the lower bits, the first 32 bits of the above binary data are:

0111 0011 1100 1110 0010 1111 1111 0010

This binary represents the decimal 1942892530, which is the output displayed in the console. From this example, we can also draw two conclusions:

1. The default data type for integers is int; thus, long l = 12345678901234L indicates that this number exceeds the int range, hence the L.

2. Writing “int ii = l + i;” will cause an error, as long + int results in a long, which cannot be assigned to int.

32. Remove data that is no longer used from shared collection classes in a timely manner.

If a collection class is shared (meaning it is not a property within a method), the elements within this collection will not be released automatically, as there will always be references pointing to them. Therefore, if some data in a shared collection is no longer used and is not removed, it will cause the shared collection to continuously grow, leading to potential memory leak issues.

33. Use the fastest way 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.

There are three ways to convert a basic data type to a string. For example, if I have an Integer type data i, I can use i.toString(), String.valueOf(i), or i + “”. Here’s a test of 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 + "": 25ms

So, when converting a basic data type to String, prioritize using the toString() method. The reasons are:

1. The String.valueOf() method internally calls the Integer.toString() method, but it performs a null check before calling.

2. The Integer.toString() method directly calls.

3. The i + “” method uses StringBuilder under the hood, first using the append method to concatenate, then calling toString() to get the string.

Comparing the three, it’s clear that method 2 is the fastest, method 1 is second, and method 3 is the slowest.

34. Use the most efficient way to traverse a Map.

There are many ways to traverse a Map. In typical scenarios, we need to traverse the keys and values in the Map. The recommended and most efficient way to do this is:

public static void main(String[] args) { HashMap hm = new HashMap(); hm.put("111", "222"); Set> entrySet = hm.entrySet(); Iterator> iter = entrySet.iterator(); while (iter.hasNext()) { Map.Entry entry = iter.next(); System.out.println(entry.getKey() + "\t" + entry.getValue()); } }

If you just want to traverse the keys of this Map, using “Set keySet = hm.keySet();” will be more appropriate.

35. It is advisable to separate the close() operations for resources.

This means, for example, if I have the following code:

try { XXX.close(); YYY.close(); } catch (Exception e) { ... }

It is recommended to modify it to:

try { XXX.close(); } catch (Exception e) { ... } try { YYY.close(); } catch (Exception e) { ... }

Although it may seem cumbersome, it can prevent resource leaks. If the code remains unchanged, if XXX.close() throws an exception, then the catch block will be entered, and YYY.close() will not execute; that resource will not be released, occupying space unnecessarily. If this pattern occurs frequently, it can lead to resource handle leaks. By rewriting it as shown, we ensure that both XXX and YYY will be closed regardless.

Did you gain something from this article? Please share it with more people!

Java and Android Experts Channel

We welcome you to follow us and discuss technology together. Scan and long press the QR code below for quick access. Or search for WeChat public account: JANiubility.

35 Java Code Performance Optimization Tips for Android Developers

Public Account:JANiubility

35 Java Code Performance Optimization Tips for Android Developers

Leave a Comment

×