Oracle Java application performance, part 1: Creating dynamic strings

Tips about using dynamic strings in Oracle and how they can improve Java application performance.

[This tip was excerpted from the new Wrox Press book Oracle 9i Java Programming.]

Performance optimization is a big topic; big enough to fill an entire book of information on how to make Oracle and Java perform better. In this and subsequent tips, we will look at just a few ways to improve the performance of your Java programs.

Since String objects are immutable, the creators of Java included the StringBuffer class in the core Java API. The StringBuffer class is a class that acts as a buffer for the creation and manipulation of dynamic strings. In fact, even if you don't explicitly use the StringBuffer, the compiler uses it for you whenever you concatenate string references. If you have source code like this:

 html += "<td>" + rset.getString(1) + "</td>";

A new String is created and the reference is assigned to the variable html. The Java compiler creates the new String by treating the source code as if you had coded this:

 StringBuffer sb = new StringBuffer(html); sb.append("<td>").append(rset.getString(1)).append("</td>"); html = sb.toString();

The StringBuffer(String) constructor creates a StringBuffer that is sized to the same number of characters as are contained in the string argument. As soon as append() is called, the StringBuffer object must resize its internal buffer to hold additional characters. Due to the additional method calls, additional buffer objects, and additional memory allocation, string concatenation tends to be very inefficient. In the next example, we'll see how to use the StringBuffer to dramatically improve the performance of Java code that dynamically creates strings.

By taking control of the StringBuffer creation, we can eliminate all the waste caused by string concatenation and caused by allowing the compiler to create StringBuffer method calls. The StringBuffer has a constructor that takes an integer parameter:

 StringBuffer html = new StringBuffer(300200);

This parameter instructs the StringBuffer object to initialize its internal buffer to be the specified number of characters. As long as we do not append more than that many characters to the StringBuffer, there will be no additional method calls to resize the buffer, and no reallocation of memory. The performance improvement is impressive, as we will see in the StringDemo2 class.

Modify the StringDemo1 class as shown below to create the StringDemo2 class:

 SQL> create or replace and compile 2 java source named "StringDemo2" 3 as 4 import java.sql.*; 5 public class StringDemo2 { 6 public static void getAllObs() { 7 long start = System.currentTimeMillis(); 8 System.out.println("Entered StringDemo2.getAllObs() at " + start 9 + " milliseconds"); 10 Connection conn = null; 11 StringBuffer html = new StringBuffer(300200); 12 html.append("<html><body><table>"); 13 try { 14 Class.forName("oracle.jdbc.driver.OracleDriver"); 15 String url = "jdbc:default:connection:"; 16 conn = DriverManager.getConnection(url, "username","password"); 17 String sql = "select * from all_objects"; 18 Statement stmt = conn.createStatement(); 19 ResultSet rset = stmt.executeQuery(sql); 20 while (rset.next()) { 21 html.append("<tr>"); 22 html.append("<td>").append(rset.getString(1)); 23 html.append("</td>"); 24 html.append("<td>").append(rset.getString(2)); 25 html.append("</td>"); 26 html.append("<td>").append(rset.getString(3)); 27 html.append("</td>"); 28 html.append("<td>").append(rset.getString(4)); 29 html.append("</td>"); 30 html.append("<td>").append(rset.getString(5)); 31 html.append("</td>"); 32 html.append("<td>").append(rset.getString(6)); 33 html.append("</td>"); 34 html.append("<td>").append(rset.getString(7)); 35 html.append("</td>"); 36 html.append("<td>").append(rset.getString(8)); 37 html.append("</td>"); 38 html.append("<td>").append(rset.getString(9)); 39 html.append("</td>"); 40 html.append("<td>").append(rset.getString(10)); 41 html.append("</td>"); 42 html.append("<td>").append(rset.getString(11)); 43 html.append("</td>"); 44 html.append("<td>").append(rset.getString(12)); 45 html.append("</td>"); 46 html.append("<td>").append(rset.getString(13)); 47 html.append("</td>"); 48 html.append("</tr>"); 49 if (html.length() > 300000) { 50 break; 51 } 52 } 53 html.append("</table></body></html>"); 54 } catch (Exception e) { 55 e.printStackTrace(); 56 } 57 finally { 58 try { 59 conn.close(); 60 } catch (Exception ignored) {} 61 } 62 long end = System.currentTimeMillis(); 63 System.out.println("Complete StringDemo2.getAllObs() at " + end 64 + " milliseconds"); 65 System.out.println("Total milliseconds=" + (end - start)); 66 System.out.println("final length = " + html.length()); 67 System.out.println(); 68 } 69 } 70 71 /

When the Java class is successfully compiled, create a call specification:

 SQL> create or replace procedure string_demo2 2 as language java name 'StringDemo2.getAllObs()'; 3 /

Use the following PL/SQL statements to set the environment so that output is directed to the console:

 SQL> set serveroutput on SQL> call dbms_java.set_output(2000);

Now, call the procedure:

 SQL> exec string_demo2; Entered StringDemo2.getAllObs() at 1003419696403 milliseconds Complete StringDemo2.getAllObs() at 1003419699107 milliseconds Total milliseconds=2704 final length = 300158

As I said, the improvement is dramatic, from 132 seconds (131860 milliseconds) to 3 seconds (2704 milliseconds). That's more than a 40X improvement. No matter how easy it is to do string concatenation:

 String a = "a"; String b = "b"; String s = a + b;

Avoid the temptation! As we saw in the example above, sometimes a little work on the part of the programmer can yield huge performance improvements.

In this case, we created a StringBuffer object that was large enough to hold the string we wanted to create. As we append to the StringBuffer, the object never needs to resize its buffer, so all the expensive memory reallocations and object creation that occurs with string concatenation is eliminated.

Owing to this dramatic difference in execution times, it makes sense to keep this in mind whenever you are concatenating String references in Java. If the method is going to be called rarely, you can choose to use the '+' operator to concatenate strings. If however, the method will be called a lot, you will definitely want to consider using a StringBuffer in the code.

To do this in your own code, you'll need to estimate how large the string is that you want to create could possibly be. In the example above it was easy because I somewhat arbitrarily sized the StringBuffer to 300,200 characters without worrying about how many rows would be returned. I chose this value because I knew, from runs of the previous example, that this size would be large enough to hold the string that was created by the code. In your own code, you'll need to look at how many rows you want to display to the user, and how large each row could be. You can test that estimate by printing out the size of each completed StringBuffer.

If you're getting strings from some source other than a table, it will be harder to make the estimate, but even a rough estimate will be better than starting with the default buffer size. When you use the constructor:

 StringBuffer sb = new StringBuffer();

The default size is 16 characters. Every time the buffer resizes, it adds 1 to the current size and then doubles it. So the progression of buffer sizes is 16, 34, 70, 142, 286, 574, 1150, and so on. Suppose the actual string size is going to be 1000 characters, but your rough estimate comes out to 500. Even with your estimate being 50% too low, you've still eliminated 5 calls to the resize method.

Keep in mind that using StringBuffers is not a cure all. You can still consume all the memory in the system and cause the program to crash by appending enough characters to the StringBuffer. In the application I worked on, our second step in the solution was to page all queries. When a query returned rows, we only showed a certain number (it was a user configurable parameter) to the user. If the number of rows returned was more than the number displayed, we gave the user a Next button to get the next set of rows from the query. Using these techniques, we eliminated all memory problems from the application.

Next Steps

What's in store for Java programming as Java turns 20?

Dig Deeper on Oracle Java and J2EE