Tag: Performance

  • Java Strings

    Strings are the backbone of many Java applications, used for everything from logging to data processing. However, Java’s String class is immutable, meaning every concatenation with the + operator creates a new object, potentially leading to performance bottlenecks. Have you ever noticed your application slowing down when handling large strings? In this post, we’ll compare three ways to concatenate strings—using the + operator, StringBuilder, and StringBuffer—and measure their impact on time and memory. By the end, you’ll know how to optimise string operations for low-latency, high-throughput systems. Let’s dive in

    Lets create Strings class with 3 static methods

    • concatenateBasic
    • concatenateStringBuilder
    • concatenateStringBuffer
    public class Strings {
        public static void concatenateBasic(int iterations) {
            String result = "";
            for (int i = 0; i < iterations; i++) {
                result = result + "word ";
            }
        }
    
        public static void concatenateStringBuilder(int iterations) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < iterations; i++) {
                sb.append("word ");
            }
            String result = sb.toString();
        }
    
        public static void concatenateStringBuffer(int iterations) {
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < iterations; i++) {
                sb.append("word ");
            }
            String result = sb.toString();
        }
    }

    All three methods concatenate a string by appending ‘word ’ for a specified number of iterations.

    The difference is minimal when this is done with a small number of iterations. But as the count of iterations grows, both the memory & time required to do the same functionality grows exponentially with the ‘+’ operator. Below is a sample code to test this

    public void testStringConcatenation() throws InterruptedException {
        // Get runtime
        Runtime runtime = Runtime.getRuntime();
        long startMemory, endMemory, startTime, endTime, duration, memoryUsed;
        for (int i = 10; i <= 1000000; i = i * 10) {
            System.out.println("With iterations: " + i);
            runtime.gc();
            startMemory = runtime.totalMemory() - runtime.freeMemory();
            startTime = System.nanoTime();
            Strings.concatenateBasic(i);
            endTime = System.nanoTime();
            endMemory = runtime.totalMemory() - runtime.freeMemory();
            duration = (endTime - startTime) / 1_000_000; // Convert to milliseconds
            memoryUsed = (endMemory - startMemory) / 1024; // in KB
            System.out.println("Time taken using '+': " + duration + " ms, Memory used: " + memoryUsed + " KB");
    
            runtime.gc();
            startMemory = runtime.totalMemory() - runtime.freeMemory();
            startTime = System.nanoTime();
            Strings.concatenateStringBuilder(i);
            endTime = System.nanoTime();
            endMemory = runtime.totalMemory() - runtime.freeMemory();
            duration = (endTime - startTime) / 1_000_000; // Convert to milliseconds
            memoryUsed = (endMemory - startMemory) / 1024; // in KB
            System.out.println("Time taken using StringBuilder: " + duration + " ms, Memory used: " + memoryUsed + " KB");
    
            runtime.gc();
            startMemory = runtime.totalMemory() - runtime.freeMemory();
            startTime = System.nanoTime();
            Strings.concatenateStringBuffer(i);
            endTime = System.nanoTime();
            endMemory = runtime.totalMemory() - runtime.freeMemory();
            duration = (endTime - startTime) / 1_000_000; // Convert to milliseconds
            memoryUsed = (endMemory - startMemory) / 1024; // in KB
            System.out.println("Time taken using StringBuffer: " + duration + " ms, Memory used: " + memoryUsed + " KB");
    
            Thread.sleep(1000); // Sleep for 1 second between iterations
        }
    }

    The results are as below.

    Iterations‘+’ Time (ms)‘+’ Memory (KB)StringBuilder Time (ms)StringBuilder Memory (KB)StringBuffer Time (ms)StringBuffer Memory (KB)
    1029010163022
    1000580906
    10003769028027
    10000673171010240199
    10000034522562882208831668
    10000004141057383207267072416426

    Note: Runtime.gc() is used to hint at garbage collection, but results may vary depending on the JVM’s behaviour.

    As you can see, while the initial difference is negligible, the performance of the + operator degrades dramatically as the number of concatenations grows, leading to significant increases in both execution time and memory consumption.

    For 1M iterations, StringBuilder is up to 59,157 times faster. StringBuffer is slightly slower than StringBuilder as it uses synchronized (Thread safe) methods.

    Why This Matters

    The performance differences highlighted above might seem trivial for a small number of string concatenations.

    Examples

    1. Imagine a high-throughput web server handling thousands of requests per second. Each request generates a log entry with details like the timestamp, user ID, and endpoint. Using the + operator to build log messages, such as

    log = timestamp + " " + userId + " " + endpoint

    creates multiple String objects per log entry. Use of StringBuilder will significantly improve the performance

    2. In a data processing pipeline, such as one generating CSV reports from a database, you might concatenate fields like

    row = id + "," + name + "," + value // for each record

    For a dataset with millions of rows, using + in a loop results in quadratic time complexity, causing delays in report generation.

    Low-Latency and High-Throughput Systems

    In low-latency systems like financial trading platforms, every millisecond counts. Concatenating strings to format trade messages using + can introduce unacceptable delays due to object creation. Similarly, high-throughput systems, such as streaming data processors, handle massive data volumes. Inefficient string operations can bottleneck these systems, reducing throughput. By using StringBuilder (or StringBuffer in thread-safe contexts), developers ensure these systems remain responsive and scalable, meeting stringent performance requirements.

    Conclusion

    Choosing the right string concatenation method can significantly impact your Java application’s performance. For single-threaded applications, StringBuilder is the go-to choice for its speed and efficiency. Use StringBuffer in multi-threaded environments requiring thread safety. Avoid + in loops to prevent performance degradation. Try running the test code yourself and share your results in the comments!

    The code is available at https://github.com/dcurioustech/java-samples/blob/master/java-samples/src/main/java/com/dcurioustech/strings/Strings.java Tests – https://github.com/dcurioustech/java-samples/blob/master/java-samples/src/test/java/com/dcurioustech/strings/StringsTest.java

    #Java #StringConcatenation #Performance