GeistHaus
log in · sign up

https://blogger.com/feeds/1729822519392175009/posts/default

atom
25 posts
Polling state
Status active
Last polled May 19, 2026 02:24 UTC
Next poll May 19, 2026 23:25 UTC
Poll interval 86400s
ETag W/"5c317ba613a91a712a73bf8aca0b17b07d258d99f20a9749d65818ffdc7317f1"
Last-Modified Fri, 15 May 2026 12:35:18 GMT

Posts

Updated Biography
Infoupdated
Show full content

Peter Lawrey is an Australian/British software engineer and entrepreneur best known for work on ultra-low-latency Java systems and for leading the open-source OpenHFT libraries. He is the founder and chief executive of Chronicle Software, a London-based company whose technology is used in trading and market-infrastructure workloads. Lawrey is also a recognised Java community figure: he was named a Java Champion in 2015, has been described by conference organisers as having provided the most answers for the Java and JVM tags on Stack Overflow, and writes the long-running Vanilla Java blog. (Chronicle Software, javachampions.org, qconnewyork.com, blog.vanillajava.blog)

Career

Lawrey founded and leads Chronicle Software, which builds enabling technology for event-driven trading and market-data platforms. The company states that its software underpins systems at several tier-one banks; a 2024 press announcement similarly described Chronicle as supplying "8 of the top 11 investment banks". (Chronicle Software, Yahoo Finance)

Alongside the commercial platform, Lawrey architects and stewards Chronicle’s open-source projects under the OpenHFT umbrella. These include Chronicle Queue (a persisted low-latency shared memory messaging framework) and Chronicle Map (a high-performance in-memory/off-heap key-value store). The projects are widely used in low-latency and high-throughput applications. (GitHub)

Lawrey regularly presents at software-engineering conferences (including QCon, JCrete) on topics such as microservices for performance, vertical scalability and practical techniques for achieving deterministic latency on the JVM. InfoQ and other outlets have covered his talks and interviews. (InfoQ, qconnewyork.com)

Community and writing

Since 2014, Lawrey has authored Vanilla Java, a technical blog focused on performance optimisation, concurrency and mechanical sympathy on the JVM. He has also contributed articles to Foojay.io. (blog.vanillajava.blog, foojay)

He is a long-standing Stack Overflow contributor. Conference bios and community profiles credit him with one of the most answers posted for the Java and JVM tags; his public profile lists multiple gold badges in areas such as file-io, memory and garbage-collection. (qconnewyork.com, Stack Overflow)

Recognition

Lawrey was admitted to the Java Champions programme in 2015, an honour recognising notable contributions to the Java ecosystem. (javachampions.org)

Personal life

Lawrey was born in Melbourne, Australia, and has lived in the UK since 2002.

Lawrey lives in Surrey, United Kingdom, and has six children. He has expressed a long-standing interest in the works of H. G. Wells, particularly "The War of the Worlds", and lives near Horsell Common, a location featured in the novel. Among his favourite quotations are "Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away," attributed to Antoine de Saint-Exupéry, and "The book to read is not the one that thinks for you but the one which makes you think," commonly attributed to Harper Lee. Both these quotes apply to the age of Generative AI as well.

Selected open-source projects
  • Chronicle Queue - persisted low-latency messaging framework. (GitHub)

  • Chronicle Map - in-memory/off-heap key-value store for multi-process/low-latency use cases. (GitHub)

  • Chronicle Wire - high-performance serialisation library for Java.

  • Chronicle Bytes - low-level byte manipulation library for high-performance applications.

  • Chronicle Core - foundational libraries for Chronicle projects, including concurrency utilities and low-level I/O.

  • Java Thread Affinity - library for binding Java threads to specific CPU cores, enhancing performance in multi-threaded applications.

  • Java Runtime Compiler - a library for compiling Java code at runtime, enabling dynamic code generation and execution.

  • Java Microbenchmark Harness (JMH) - a framework for writing and running Java microbenchmarks, providing accurate latency performance measurements.

tag:blogger.com,1999:blog-1729822519392175009.post-8882748791795062951
Extensions
Improving the prompt to the AI to get better code
AIExerciseOpinionPerformance
Show full content
In a previous article I looked at one-shoting a solution to optimise code to show the variation in different AI. Thsi is the not the best way to get what you want however. More often you need to either refine the prompt or give feedback. After one-shoting the same prompt on multiple AI, I have created a refined prompt based on the various concerns with previous results. The prompt Based on the results in a previous run Asking multiple AI to optimise the same code
Suggest how to implement this more optimally using low latency techniques 
to minimize any objects created.

## Use
- a ThreadLocal for temporary data.
- simple maths rather than a library, add comments for clarity if needed.
- offset in the form ±hh, ±hhmm, or ±hhmmss, using the shortest form that 
does not lose information, where hh, mm, and ss are the hours, minutes, 
and seconds east (+) or west (-) of UT 
- return a `intern()` String.

## Don't use
- String.format
- String operations that create objects.
- any colons, they aren't required
- reduce code duplication

## The code
 
private static String formatOffset(int millis) {
    String sign = millis < 0 ? "-" : "+";
    int saveSecs = Math.abs(millis) / 1000;
    int hours = saveSecs / 3600;
    int mins = ((saveSecs / 60) % 60);
    int secs = (saveSecs % 60);
    if (secs == 0) {
        if (mins == 0) {
            return sign + twoDigitString(hours);
        }
        return sign + twoDigitString(hours) + twoDigitString(mins);
    }
    return sign + twoDigitString(hours) + twoDigitString(mins) + twoDigitString(secs);
}

private static String twoDigitString(int value) {
    return Integer.toString(value + 100).substring(1);
}
I typically use asciidoc rather than markdown, but trying to keep this example simple.

None of the AI considered using a byte[] event though String now uses a byte[] as an underlying store. I asked each one to

change the implementation to use a byte[] to create an ISO_8859_1 encoded string
which they were able to do, but none suggested it. Gemini 2.5 pro This result is signifciantly improved. Use use of StringBuilder is more idomatic, but adds overhead. I find Gemini tends to produce wordier results.
    /**
     * A ThreadLocal holding a StringBuilder instance for each thread.
     * This avoids creating a new StringBuilder on each method call, reducing
     * object churn and garbage collection pressure. The initial capacity is
     * set to 7, the maximum length of the output string (e.g., "-hhmmss").
     */
    private static final ThreadLocal<StringBuilder> OFFSET_BUILDER =
        ThreadLocal.withInitial(() -> new StringBuilder(7));

    /**
     * Formats a timezone offset from milliseconds into the shortest possible
     * ISO 8601 format (±hh, ±hhmm, or ±hhmmss).
     *
     * This implementation is optimized to minimize object creation by using a
     * ThreadLocal StringBuilder and simple integer arithmetic.
     *
     * @param millis The offset from UTC in milliseconds.
     * @return A string representation of the offset, fetched from the string pool.
     */
    public static String formatOffset(int millis) {
        // Get the reusable StringBuilder for the current thread and clear it.
        StringBuilder sb = OFFSET_BUILDER.get();
        sb.setLength(0);

        // Determine the sign and work with absolute values.
        final char sign = millis < 0 ? '-' : '+';
        final int totalSeconds = Math.abs(millis) / 1000;

        // Decompose the total seconds into hours, minutes, and seconds.
        // 3600 seconds in an hour.
        final int hours = totalSeconds / 3600;
        // 60 seconds in a minute.
        final int minutes = (totalSeconds / 60) % 60;
        final int seconds = totalSeconds % 60;

        // Append the sign character.
        sb.append(sign);

        // Append the hours, padded to two digits.
        appendTwoDigits(sb, hours);

        // Only include minutes and seconds if they are non-zero,
        // using the shortest format possible.
        if (minutes == 0 && seconds == 0) {
            // Format is ±hh, so we are done.
            return sb.toString().intern();
        }

        // Append the minutes, padded to two digits.
        appendTwoDigits(sb, minutes);

        if (seconds == 0) {
            // Format is ±hhmm, so we are done.
            return sb.toString().intern();
        }

        // Append the seconds, padded to two digits, for the ±hhmmss format.
        appendTwoDigits(sb, seconds);

        // Return the final string from the shared pool.
        return sb.toString().intern();
    }

    /**
     * Appends an integer value to a StringBuilder, padded with a leading zero
     * if it is less than 10. This is a fast, object-free alternative to
     * String.format or Integer.toString.
     *
     * @param sb    The StringBuilder to append to.
     * @param value The integer value (0-99).
     */
    private static void appendTwoDigits(StringBuilder sb, int value) {
        // By adding '0', we convert the integer result of the division/modulo
        // into its ASCII character equivalent.
        // Example for value=7: '0' + 0 = '0', '0' + 7 = '7' -> "07"
        // Example for value=12: '0' + 1 = '1', '0' + 2 = '2' -> "12"
        sb.append((char) ('0' + value / 10));
        sb.append((char) ('0' + value % 10));
    }
o3-pro This took the longest, 18m 20s.
/** Thread‑local scratch – long enough for “-235959” (7 chars inc. sign) */
private static final ThreadLocal<char[]> BUF =
        ThreadLocal.withInitial(() -> new char[7]);

/**
 * Format an offset in milliseconds as ±hh[mm[ss]] with no separators.
 * The returned String is interned so repeated calls for the same offset
 * are pointer‑equal.
 */
public static String formatOffset(int millis) {
    char[] buf = BUF.get();
    int pos = 0;

    // sign ---------------------------------------------------------------
    if (millis < 0) {
        buf[pos++] = '-';
        millis = -millis;   // safe: abs(Integer.MIN_VALUE) won’t hit here
    } else {
        buf[pos++] = '+';
    }

    // breakdown ---------------------------------------------------------
    int totalSecs = millis / 1_000;
    int hours    =  totalSecs / 3_600;
    int minutes  = (totalSecs /   60) % 60;
    int seconds  =  totalSecs         % 60;

    pos = writeTwoDigits(buf, pos, hours);

    if (minutes == 0 && seconds == 0)
        return new String(buf, 0, pos).intern();

    pos = writeTwoDigits(buf, pos, minutes);

    if (seconds == 0)
        return new String(buf, 0, pos).intern();

    pos = writeTwoDigits(buf, pos, seconds);
    return new String(buf, 0, pos).intern();
}

/** Writes exactly two ASCII digits into buf and returns new position. */
private static int writeTwoDigits(char[] buf, int pos, int value) {
    buf[pos++] = (char) ('0' + value / 10);
    buf[pos++] = (char) ('0' + value % 10);
    return pos;
}
o4-mini-high This soltuion is good but has more duplication than I would like. This took 17 seconds.
private static final ThreadLocal<char[]> OFFSET_BUF =
    ThreadLocal.withInitial(() -> new char[7]); // sign + hh + mm + ss

/**
 * Format a UTC offset in ±hh, ±hhmm or ±hhmmss (no colons),
 * using the shortest form that contains all non-zero fields.
 * Returns an interned String so each distinct offset is pooled.
 *
 * @param offsetMillis signed offset from UTC in milliseconds
 * @return offset string, e.g. "+02", "-0530", "+023015"
 */
public static String formatOffset(int offsetMillis) {
    char[] buf = OFFSET_BUF.get();

    // Convert to total seconds and extract sign
    int totalSecs = offsetMillis / 1000;
    char sign = totalSecs < 0 ? '-' : '+';
    if (totalSecs < 0) totalSecs = -totalSecs;

    // Hours, minutes, seconds by simple div/mod math
    int hours   = totalSecs / 3600;
    int rem     = totalSecs - hours * 3600;
    int minutes = rem / 60;
    int seconds = rem - minutes * 60;

    // Fill buffer
    int pos = 0;
    buf[pos++] = sign;
    // always two digits for hours
    buf[pos++] = (char)('0' + (hours / 10));
    buf[pos++] = (char)('0' + (hours % 10));

    if (seconds == 0) {
        if (minutes == 0) {
            // ±hh
            return new String(buf, 0, pos).intern();
        }
        // ±hhmm
        buf[pos++] = (char)('0' + (minutes / 10));
        buf[pos++] = (char)('0' + (minutes % 10));
        return new String(buf, 0, pos).intern();
    }

    // ±hhmmss
    buf[pos++] = (char)('0' + (minutes / 10));
    buf[pos++] = (char)('0' + (minutes % 10));
    buf[pos++] = (char)('0' + (seconds / 10));
    buf[pos++] = (char)('0' + (seconds % 10));
    return new String(buf, 0, pos).intern();
}
Claude 4 The improved prompt improved the solution which was already pretty good. This took a 1m,40s.
// ThreadLocal buffer reused across calls to minimize object creation
private static final ThreadLocal<char[]> BUFFER = ThreadLocal.withInitial(() -> new char[7]);

private static String formatOffset(int millis) {
    char[] buf = BUFFER.get();
    int pos = 0;
    
    // Add sign: + for positive/zero, - for negative
    buf[pos++] = millis < 0 ? '-' : '+';
    
    // Convert to positive seconds for calculations
    int totalSeconds = Math.abs(millis) / 1000;
    
    // Extract hours, minutes, seconds using simple division/modulo
    int hours = totalSeconds / 3600;          // 3600 seconds per hour
    int mins = (totalSeconds / 60) % 60;      // 60 seconds per minute, mod 60 for remainder
    int secs = totalSeconds % 60;             // Remaining seconds
    
    // Hours are always included (±hh minimum format)
    buf[pos++] = (char)('0' + hours / 10);    // Tens digit
    buf[pos++] = (char)('0' + hours % 10);    // Units digit
    
    // Include minutes if non-zero or if seconds are non-zero
    if (mins != 0 || secs != 0) {
        buf[pos++] = (char)('0' + mins / 10);
        buf[pos++] = (char)('0' + mins % 10);
        
        // Include seconds only if non-zero
        if (secs != 0) {
            buf[pos++] = (char)('0' + secs / 10);
            buf[pos++] = (char)('0' + secs % 10);
        }
    }
    
    // Create final string from buffer and intern for memory efficiency
    return new String(buf, 0, pos).intern();
}
Grok 3 Think This result is much better with a refined prompt. As noted before Grok 4 should be better. This took a 53 seconds.
// Size of 9 covers worst-case scenario: +hhmmss (7 chars) plus potential padding.
private static final ThreadLocal<char[]> BUFFER = ThreadLocal.withInitial(() -> new char[9]);

private static String formatOffset(int millis) {
    // Use ThreadLocal char array to avoid StringBuilder or new String creation
    char[] buf = BUFFER.get();
    int pos = 0;

    // Determine sign and absolute seconds
    int absMillis = Math.abs(millis);
    buf[pos++] = millis < 0 ? '-' : '+';
    int totalSecs = absMillis / 1000;

    // Calculate hours, minutes, seconds using division and modulo
    int hours = totalSecs / 3600;
    int remainingSecs = totalSecs % 3600;
    int mins = remainingSecs / 60;
    int secs = remainingSecs % 60;

    // Convert hours to two digits
    buf[pos++] = (char) ('0' + hours / 10);
    buf[pos++] = (char) ('0' + hours % 10);

    // Skip minutes and seconds if zero for shortest representation
    if (mins == 0 && secs == 0) {
        return new String(buf, 0, pos).intern();
    }

    // Append minutes
    buf[pos++] = (char) ('0' + mins / 10);
    buf[pos++] = (char) ('0' + mins % 10);

    // Skip seconds if zero
    if (secs == 0) {
        return new String(buf, 0, pos).intern();
    }

    // Append seconds
    buf[pos++] = (char) ('0' + secs / 10);
    buf[pos++] = (char) ('0' + secs % 10);

    return new String(buf, 0, pos).intern();
}
After being asked to use a byte[] it used a byte[9] GitHub Copilot with GPT 4.1 This produced a much better result with the more detailed prompt. Note that it uses ±h instead of always using ±hh. This took a few seconds.
private static final ThreadLocal<char[]> OFFSET_BUFFER = ThreadLocal.withInitial(() -> new char[7]); // sign + 2*3 digits

private static String formatOffset(int millis) {
    char[] buf = OFFSET_BUFFER.get();
    int absMillis = millis < 0 ? -millis : millis;
    int totalSecs = absMillis / 1000;
    int hours = totalSecs / 3600;
    int mins = (totalSecs / 60) % 60;
    int secs = totalSecs % 60;

    int pos = 0;
    buf[pos++] = millis < 0 ? '-' : '+';

    // Write hours
    if (hours < 10) {
        buf[pos++] = (char) ('0' + hours);
    } else {
        buf[pos++] = (char) ('0' + hours / 10);
        buf[pos++] = (char) ('0' + hours % 10);
    }

    // Only add minutes if needed
    if (mins != 0 || secs != 0) {
        buf[pos++] = (char) ('0' + mins / 10);
        buf[pos++] = (char) ('0' + mins % 10);
    }

    // Only add seconds if needed
    if (secs != 0) {
        buf[pos++] = (char) ('0' + secs / 10);
        buf[pos++] = (char) ('0' + secs % 10);
    }

    // Return interned String
    return new String(buf, 0, pos).intern();
}
Microsoft Copilot with Think Deeper This is significantly better than the simple prompt. This took a few seconds.
public class OffsetFormatter {

    // Thread-local buffer: max length 7 ("±hhmmss")
    private static final ThreadLocal<char[]> BUF =
        ThreadLocal.withInitial(() -> new char[7]);

    /**
     * Formats a UTC offset in milliseconds to a string in the form:
     *  ±hh, ±hhmm, or ±hhmmss using the shortest form
     *  without losing information. Returns an interned String.
     */
    public static String formatOffset(int offsetMillis) {
        char[] buf = BUF.get();
        // 0: sign, 1–2: hh, 3–4: mm, 5–6: ss
        buf[0] = offsetMillis < 0 ? '-' : '+';

        // total seconds east of UT
        int totalSeconds = Math.abs(offsetMillis) / 1000;

        // compute hours / minutes / seconds
        int hours = totalSeconds / 3600;
        int rem    = totalSeconds - hours * 3600;
        int mins   = rem / 60;
        int secs   = rem - mins * 60;

        // write two-digit hours
        writeTwoDigits(buf, 1, hours);

        if (secs == 0) {
            if (mins == 0) {
                // ±hh
                return new String(buf, 0, 3).intern();
            }
            // write two-digit minutes (±hhmm)
            writeTwoDigits(buf, 3, mins);
            return new String(buf, 0, 5).intern();
        }

        // write two-digit minutes and seconds (±hhmmss)
        writeTwoDigits(buf, 3, mins);
        writeTwoDigits(buf, 5, secs);
        return new String(buf, 0, 7).intern();
    }

    /** 
     * Writes a zero-padded two-digit number into buf at position idx:
     *   buf[idx]   = tens digit
     *   buf[idx+1] = ones digit
     */
    private static void writeTwoDigits(char[] buf, int idx, int value) {
        buf[idx]   = (char) ('0' + (value / 10));
        buf[idx+1] = (char) ('0' + (value % 10));
    }
}
Conclusion It's worth noting that;
  • None suggested using a byte[] though they could all do that.
  • To reduce code duplication some uses a helper method or called new String once. None did both optimisations.
  • The more detailed prompt helped the AI which gave poor answers before bridging the gap in the results.
  • The AI which took significantly longer, didn't produce a significantly better answer in this case.
tag:blogger.com,1999:blog-1729822519392175009.post-5032130022835829176
Extensions
Asking multiple AI to optimise the same code
AIExerciseOpinionPerformance
Show full content
As different AIs are implemented differently, they don't all provide the same answer, nor do they consistently outperform one another. The best approach is to use multiple AI and pick the one you like best.
My goal here is not to declare a winner based on one example, but instead to show the variety of answers you can get with different AI.
I asked each AI to
Suggest how to implement this more optimally

private static String formatOffset(int millis) {
    String sign = millis < 0 ? "-" : "+";
    int saveSecs = Math.abs(millis) / 1000;
    int hours = saveSecs / 3600;
    int mins = ((saveSecs / 60) % 60);
    int secs = (saveSecs % 60);
    if (secs == 0) {
        if (mins == 0) {
            return sign + twoDigitString(hours);
        }
        return sign + twoDigitString(hours) + twoDigitString(mins);
    }
    return sign + twoDigitString(hours) + twoDigitString(mins) + twoDigitString(secs);
}

private static String twoDigitString(int value) {
    return Integer.toString(value + 100).substring(1);
}
Gemini 2.5 pro While Gemini is often my first go-to AI when I have a large code base, it's not the smartest. It does have a large context window, which means I can dump a large context into it without much thought on my part. However, if you have less than about 6,000 lines of context, you might find that o3-pro, o4-mini-high, or Claude 4 gives a better answer. In this case, it offered in about 10 seconds;
private static String formatOffset(int millis) {
    long totalSeconds = Math.abs((long) millis / 1000);
    long hours = totalSeconds / 3600;
    long minutes = (totalSeconds % 3600) / 60;
    long seconds = totalSeconds % 60;

    StringBuilder sb = new StringBuilder();
    sb.append(millis < 0 ? "-" : "+");
    sb.append(String.format("%02d", hours));

    if (minutes > 0 || seconds > 0) {
        sb.append(String.format(":%02d", minutes));
    }
    if (seconds > 0) {
        sb.append(String.format(":%02d", seconds));
    }

    return sb.toString();
}
Not only is this approach slower, but it also fails to achieve the same result, which is even worse. The original format is +HHMMSS without a colon. OpenAI o3-pro This is both faster and correct. I couldn't say whether this is faster or slower than Claude's answer without benchmarking it.
However, this model took 10 minutes, which isn't always worth the wait.
private static final String[] TWO_DIGIT = new String[60];
static {
    for (int i = 0; i < TWO_DIGIT.length; i++) {
        TWO_DIGIT[i] = (i < 10 ? "0" : "") + i;
    }
}

static String formatOffset(int millis) {
    int totalSecs = millis / 1_000;
    int abs      = Math.abs(totalSecs);

    int hours   = abs / 3_600;
    int minutes = (abs / 60) % 60;
    int seconds =  abs % 60;

    StringBuilder sb = new StringBuilder(7);          // max “+HHMMSS”
    sb.append(totalSecs < 0 ? '-' : '+')
      .append(TWO_DIGIT[hours]);

    if (seconds != 0 || minutes != 0) {
        sb.append(TWO_DIGIT[minutes]);
        if (seconds != 0) sb.append(TWO_DIGIT[seconds]);
    }
    return sb.toString();     // single String allocation here
}
o4-mini-high This code is faster than the original and correct, and it only took 9 seconds compared to o3-pro 10 minutes...
private static String formatOffset(int millis) {
    long totalSecs = Math.abs((long) millis) / 1_000;
    int hours = (int) (totalSecs / 3_600);
    int remainder = (int) (totalSecs - hours * 3_600);
    int mins  = remainder / 60;
    int secs  = remainder - mins * 60;

    // Determine length: 1 for sign, 2 for HH, +2 for MM if needed, +2 for SS if needed
    int len = 1 + 2 
            + ((mins != 0 || secs != 0) ? 2 : 0) 
            + ((secs != 0) ? 2 : 0);

    char[] buf = new char[len];
    int pos = 0;
    buf[pos++] = millis < 0 ? '-' : '+';
    buf[pos++] = (char) ('0' + hours / 10);
    buf[pos++] = (char) ('0' + hours % 10);

    if (mins != 0 || secs != 0) {
        buf[pos++] = (char) ('0' + mins / 10);
        buf[pos++] = (char) ('0' + mins % 10);
        if (secs != 0) {
            buf[pos++] = (char) ('0' + secs / 10);
            buf[pos++] = (char) ('0' + secs % 10);
        }
    }
    return new String(buf);
}
Claude 4 This is one of the better answers. I use Claude as a third opinion, as I prefer the answers for o3-pro/o4-mini-high or the larger context window of Gemini 2.5 pro.
private static String formatOffset(int millis) {
    char[] buffer = new char[7]; // Max length: +HHMMSS
    int pos = 0;
    
    // Handle sign
    if (millis < 0) {
        buffer[pos++] = '-';
        millis = -millis;
    } else {
        buffer[pos++] = '+';
    }
    
    int totalSecs = millis / 1000;
    int hours = totalSecs / 3600;
    int mins = (totalSecs % 3600) / 60;
    int secs = totalSecs % 60;
    
    // Always include hours
    buffer[pos++] = (char) ('0' + hours / 10);
    buffer[pos++] = (char) ('0' + hours % 10);
    
    if (mins != 0 || secs != 0) {
        buffer[pos++] = (char) ('0' + mins / 10);
        buffer[pos++] = (char) ('0' + mins % 10);
        
        if (secs != 0) {
            buffer[pos++] = (char) ('0' + secs / 10);
            buffer[pos++] = (char) ('0' + secs % 10);
        }
    }
    
    return new String(buffer, 0, pos);
}
Grok 3 Think Grok 4 is widely considered to be much better; however, I haven't started subscribing to it yet. It's similar to Gemini in this case, without the introduced bug.
private static String formatOffset(int millis) {
    // Determine the sign and work with absolute value
    String sign = millis < 0 ? "-" : "+";
    int absMillis = Math.abs(millis);
    
    // Directly calculate hours, minutes, and seconds
    int hours = absMillis / 3600000;       // 3600000 ms = 1 hour
    int minutes = (absMillis % 3600000) / 60000; // 60000 ms = 1 minute
    int seconds = (absMillis % 60000) / 1000;    // 1000 ms = 1 second

    // Build the result efficiently using StringBuilder
    StringBuilder sb = new StringBuilder();
    sb.append(sign);
    sb.append(String.format("%02d", hours));
    
    // Include minutes and seconds based on their values
    if (minutes > 0 || seconds > 0) {
        sb.append(String.format("%02d", minutes));
        if (seconds > 0) {
            sb.append(String.format("%02d", seconds));
        }
    }
    
    return sb.toString();
}
Copilot I use Github Copilot in my IDE for simplicity, rarely using the Microsoft Copilot except for curiosity's sake. It's generally worse than the other options I have tried.
In this case, both GitHub Copilot and Microsoft Copilot gave the same answer.
private static String formatOffset(int millis) {
    Duration d = Duration.ofMillis(millis);
    long totalSecs = d.getSeconds();
    long absSecs   = Math.abs(totalSecs);

    long hours = absSecs / 3600;
    int  mins  = (int) ((absSecs % 3600) / 60);
    int  secs  = (int) (absSecs % 60);

    char sign = totalSecs < 0 ? '-' : '+';

    if (secs == 0) {
        return String.format("%c%02d:%02d", sign, hours, mins);
    }
    return String.format("%c%02d:%02d:%02d", sign, hours, mins, secs);
}
This has multiple bugs, and it's significantly slower.
HOT TAKE It worries me when people say they are using Co-pilot a lot and it makes a big difference, because apart from it's convinence, it's not great IMHO. Conclusion Particularly when it comes to non-idiomatic code, e.g. low-latency, AI can struggle as these might not be well represented in their training set. I find I spend the most time creating the prompt with the proper context, so copy-and-pasting the same prompt to multiple AI isn't a big deal, you can pick the answer you like or even combine the results.
For the best outcome for you, I recommend learning from each and writing it from scratch yourself. If you just copy-and-paste the answers you aren't thinking about it and probably deskilling yourself in the process.
tag:blogger.com,1999:blog-1729822519392175009.post-1665730029129587713
Extensions
Practical Considerations for Advancing AI Collaboration in Software Development
AIInfoOpinion
Show full content
TL;DR
  • Human-in-the-loop is essential; AI offers probability, not certainty.

  • AI excels at word-smithing, so spend more time on documentation and context.

  • Leverage diverse AI models for varied research, improvements, and analysis.

  • Be wary of deskilling: if AI makes a task trivial, agents may soon replace it.

  • You should feel like you are testing the boundaries of what AI is capable of for at least some tasks.

The Problem

AI’s proficiency in handling routine coding allows human engineers to dedicate more time to strategic activities such as system design, architectural planning, intricate requirement elicitation, and the rigorous evaluation of application performance across multifaceted metrics.

Tools often amplify underlying behaviours and failures
— Rob Lambert

Value is increasingly found not in rote knowledge, which AI can often provide, but in the capacity to frame complex problems effectively for AI, critically evaluate its probabilistic outputs, and innovatively integrate AI’s capabilities into novel solutions.

Developing with AI is not merely about adopting a new tool but about learning to collaborate with intelligence that operates on different principles, sometimes without profound contextual understanding, yet capable of processing and synthesising information at a scale and speed that surpasses human capability. However, this power comes with new challenges: ensuring the reliability of probabilistically generated outputs, managing the 'black box' nature of some AI reasoning, and navigating the ethical and IP landscapes of AI-generated content.

Effective collaboration requires developers to master prompt engineering, the art of providing precise context, critically verifying outputs, and understanding the AI’s inherent limitations to guide it effectively. This is akin to mentoring an exceptionally capable but occasionally erratic junior partner.

Moreover, "disposable" or "ephemeral" AI-generated software carries profound and often unexamined implications for software economics, business strategy, and the fundamental definition of software assets. If software can be rapidly generated for specific, transient uses and then discarded because regeneration is trivial, the traditional emphasis on long-term maintainability and total cost of ownership (TCO) may shift, demanding new economic models and strategic thinking around software value.

Does AI Save Time?

Whether AI saves time or not depends on your goal. It saves the most time when you run into a blocker (e.g. you don’t know how to start, you need to use a tool or library you don’t know how to use or don’t know exists, or a solution you hadn’t considered). Many developers have described as feeling like a 10x developer. However, it is impossible to say how much faster this makes you, given that you might otherwise never have attempted the task or that it would have required an unquantifiable amount of research.

How does your goal change the time saved? Goal Indicative wall-clock change Why It Feels Valuable

Zero to one

Half the time

Less time spent on tasks that aren’t so important

Time-boxed tasks

More consistent delivery on time

Higher quality and completeness of tasks

Leading-edge R&D

100 % longer first iteration

AI enables a far more complete MVP and facilitates exploration of novel solution spaces

Zero-to-One

An AI-generated solution can be better than nothing, and AI can produce something decent in half the time.

Zero to One work is useful for context pieces not part of the deliverable product. We use AI for:

  • Early-stage prototyping

  • Generating a realistic stub instead of mocking a service or library

  • Building a test harness for a library you don’t yet know how to use

Time-Boxed Tasks

We time-box many tasks during planning, estimating roughly how long each is worth. AI enables higher quality within that time and makes on-time delivery more likely.

Leading-edge R&D

When developing a leading-edge solution, spend more time on the first iteration; AI will help you build a far more complete solution, shortening subsequent iterations and improving the final result. AI enables a far more complete MVP and exploration of novel solution spaces.

Effort Profiles Making Your Life Easier

If AI can generate the code with minimal effort, autonomous agents will likely soon handle that task. You’re not adding much value and may be deskilling yourself, so reserve this approach for peripheral implementations that don’t merit significant time.

20 % Effort

You’ll often feel most productive when AI does ~80 % of the heavy lifting, and you provide the remaining 20 %. Many tasks fall into this sweet spot.

Challenging Yourself

To learn and push boundaries, you want to feel like you are making at least 50% of the effort. If AI easily gives you what you want, raise your expectations and try something harder until it breaks. You want to test what AI can do, so you find different ways it breaks multiple times per day. This mirrors the process of understanding edge cases and failure modes in any complex system, but with AI, these boundaries can be more fluid and surprising. You will be learning and ensuring you make the most of what AI can do today.

Multiple AIs > One AI

Using multiple AIs is more useful than relying on one: each model has different strengths and weaknesses and surfaces different ideas. I regularly use GitHub Copilot, OpenAI o3, Gemini 2.5 Pro, and Claude 3.7 Sonnet. Deep research across the latter three often uncovers insights the others miss.

Optimising Code for AI

Generative AI excels at word-smithing, so invest more in documentation. Aim for a comparable conceptual density: documentation, tests, and production code should ideally reflect a similar depth of understanding. Documentation will be more verbose, code concise, and tests clear and exhaustive. You might aim for ~10 k reviewed words of documentation before significant coding. This detailed context, including clear explanations of intent and non-obvious constraints, is crucial for LLMs to avoid 'hallucinations' and generate relevant, high-quality code. Consider adopting practices like maintaining a 'living' design document that AI can reference.

As the project grows, you will start hitting token limits. To avoid this happening, you want to reduce token sizes and keep documentation concise. If the AI can easily generate the documentation, there is a good chance it doesn’t add any human-level differentiating value and may not significantly improve the AI’s understanding of complex tasks. Concise comments are often best, given that you will have extensive documentation and tests.

AI in the SDLC

Using AI as early as possible in the requirements capturing stage can help ensure more cases are covered and considered from the start. You can use Deep Research on multiple topics, on multiple AI and combine them. For example, using AI to analyse user stories from diverse sources can help identify conflicting requirements or unstated assumptions early on. You can use tools like NotebookLM to create audio summaries or ask an AI to distil the key insights. Diffblue Cover generates tests, JUnit 5 tags them; Claude reviews coverage gaps. Human oversight remains critical here, not just for what AI generates, but to critically assess what it misses, especially in complex or novel scenarios where training data might be sparse. However, it can be worth trying to "mine for diamonds" and look for the details the AI missed in its summaries, which might be important.

AI for Analysis

AI can be helpful in analysis, but it often produces indicative values based on vibe rather than analysis. You could verify values AI produces, or they could be just sample data. AI is very poor at telling you whether it is using sample data or realistic data. For example, if you ask it to tell you whether the values given are varied or not, it can say nonsense values were verified, and accurate values weren’t verified. This highlights the importance of not just verifying AI’s outputs but also understanding the limitations of its 'explainability'. An AI can generate a plausible justification for an incorrect answer.

I have found AI especially useful in evaluating regression tests. We extensively use regression tests to detect unintended consequences of changes, but verifying lengthy YAML test data manually is error-prone. I have found using AI effective at finding issues I missed, and using multiple AI can pick up things other AIs missed, so I use multiple AIs to verify the results.

While AI analysis presents an opportunity for innovation, it also introduces the risk of "AI-induced scope creep" if the myriad insights generated by AI are pursued without rigorous prioritisation, a task that still heavily relies on human judgment and strategic alignment.

When evaluating AI analysis, be aware that, like humans, AI can determine the answer and then backfill it with a justification. This means it can come up with a plausible explanation for why something is the answer, even if you give it a wrong answer for it to justify.

One way to mitigate this is to ask it to give a step by step plan how to do a task, without asking for the solution, and then ask it to give the solution. In my opinion, this often leads to a better solution, and empirically, it can lead to a more robust solution as the AI commits more 'thought' or processing across multiple interactions.

Considerations When Using AI Token Limits

The models do well up to ~60 k input tokens and 2.5 k output tokens. Above that, they seem to degrade. In particular, OpenAI o3 struggles over ~ 250 lines (roughly 10 tokens per line). Gemini 2.5 Pro seems to have higher limits, but often just produces longer code and text with no obvious increase in value. i.e. more fluff. For these reasons, I favour using Gemini in earlier stages to create volume and broad coverage, but I prefer using o3 for concise refinement of requirements and code.

As models evolve, these specific limits will change, but the principle of providing concise, high-signal context to avoid overwhelming the model’s context window will likely remain.

Maintaining a Decision Log

I have found it helpful to maintain a decision log in asciidoc when using multiple AIs, as they can often come up with different solutions. Esp ones you have decided not to follow for whatever reason. You want to find the suggestions they haven’t already given you.

This log also serves as valuable context for future AI interactions or onboarding new team members to AI-assisted parts of the project.

Consistent Styling

I favour using common text with lower token ids as these have higher sample sizes in the training data, reducing entropy. However, AI has a tendency to produce Unicode characters and uncommon words in an unstable manner, randomly and inconsistently. Common English words seem less likely to be replaced in updates.

While you can give the AI style guides, it is often ignored. e.g. in my style guide it specifcially says don’t use an en-dash, use an hyphen instead and when I format the style guide with AI, all the ones I have tried uses an en-dash in the phrase en-dash itself, despite the explicit instruction. This underscores that while AI can assist, current models are not consistently reliable for enforcing fine-grained stylistic rules without careful output checking and potential post-processing.

Keeping Documentation, Tests, and Code in Sync

AI can help ensure all three copies of the requirements are in sync, and it is worth maintaining them all to ensure this happens. It can help avoid unintended changes, as you see a functional alteration in three distinct forms: English, code, and tests.

This iterative refinement across different representations can also help identify ambiguities or incompleteness in requirements that might have been missed if only one form (e.g., only code) was considered.

Structuring for Advanced Human-AI Collaboration

Context is King: Managing the context provided to AI is paramount for large projects.

Modular Design: Breaking down complex problems and codebases into smaller, well-defined modules with clear interfaces can help provide AI tools with a more focused context.

Living Documentation & Knowledge Graphs: Maintaining up-to-date, AI-accessible documentation can provide persistent context. I use Asciidoc with Mermaid diagrams.

Iterative Context Refinement with AI: As discussed in the analysis section, prompt the AI to summarise or update its understanding of the evolving project context, which you can verify and use in future interactions.

Human-in-the-Loop for Critical Decisions: While AI can automate and suggest, it’s crucial to establish clear checkpoints where human expertise and sign-off are mandatory, especially for architectural changes, security-sensitive code, or features with ethical implications.

Embracing a Culture of Critical Evaluation: Foster a team culture where AI outputs are seen as a starting point or a hypothesis to be tested and validated, not an infallible oracle. Encourage developers to probe for AI limitations actively, potential biases in its suggestions, and edge cases it might not consider.

About the author

As the CEO of Chronicle Software, Peter Lawrey leads the development of cutting-edge, low-latency solutions trusted by 8 out of the top 11 global investment banks. With decades of experience in the financial technology sector, he specialises in delivering ultra-efficient enabling technology that empowers businesses to handle massive volumes of data with unparalleled speed and reliability. Peter’s deep technical expertise and passion for sharing knowledge have established him as a thought leader and mentor in the Java and FinTech communities. Follow Peter on BlueSky or Mastodon.

Collaboration

For this blog article, I used Deep Research and suggested "minimal improvements" by both OpenAI o3 and Google Gemini 2.5 Pro. I also used Grammarly to do a final check of my English.

tag:blogger.com,1999:blog-1729822519392175009.post-1665882255882313272
Extensions
A Functional intreface can extend other interfaces
Puzzles
Show full content
A functional interface can extend a marker interface, functional interface, non-functional interface, and an annotation. A recent X poll suggested 25% of those responding didn't know that.
tag:blogger.com,1999:blog-1729822519392175009.post-1237225840683346135
Extensions
Hands-On Career: The Evolution of a Java Champion
AIOpinionTesting
Show full content
Table of Contents

This article is background material for this talk Lessons learnt from founding my own company, and over 30 years hands-on coding

Introduction

Unlike most deterministic development tools, Generative AI is a productivity tool that uses a statistical model to produce results. It can be useful when used appropriately and carefully reviewed, but fundamentally, it only has a probability of being correct. Plausible results can be challenging to review unless you have expertise in the content produced.

In this talk, I will explore where I see Generative AI fitting into development and how it can help developers grow their careers. I will look at practical tips and ways to consider it for development.

Working with Generative AI can feel meta, so I will start with a quote about quotes:

I love quotations because it is a joy to find thoughts one might have beautifully expressed with much authority by someone recognised as wiser than oneself.

— Marlene Dietrich
1901-1992 Superhuman Intelligence Is Already Here
As of November 2024, the United States' El Capitan is the most powerful supercomputer in the TOP500, reaching 1742 petaFlops (1.742 exaFlops) on the LINPACK benchmarks.
— TOP500 Supercomputer Wikipedia

Supercomputers have long been able to do calculations no human can, yet they can’t do many tasks. Even when super AI is estimated to have 1000 IQs, there will still be many tasks that only a human can do.

ATMs Didn’t Replace Bank Tellers

There was a fear that when ATMs were introduced, the number of bank tellers would fall; however, "the story is that as the ATM machines arrived, the number of bank tellers held steady and even rose slightly."

"Banks began to offer more services, and tellers evolved from being people who put checks in one drawer and handed out cash from another drawer to people who solved a variety of financial problems for customers."

bessen+1

ATMs and a Rising Number of Bank Tellers?

The same applies to software development: we adapt, learn new tools, and find higher-level tasks that still require human intelligence.

About Me

In 2013, I founded a self-funded company, Chronicle Software, to develop low-latency, high-performance solutions for the financial industry. Today, my company has 8 of the top 11 investment banks as clients. This is my tenth year as a Java Champion, and I have a chance to reflect on what I have learnt over the years and consider what the next ten years will look like with generative AI-based tooling.

In this talk, I will look at:

  • How developers can continue building code-centric expertise while advancing their careers.

  • What I have learnt as a founder of a self-funded company.

  • Ways of developing with AI are different from traditional development.

  • Some practical considerations for using AI in development.

Multidimensional Growth
The most dangerous phrase a Data Processing manager can use is "We’ve always done it that way."
— Grace Hopper
Computerworld 1976 Areas of Career Development

At each stage, you can develop your career in different dimensions. How you do this will depend on your interests.

  1. Technical Depth & Breadth Mastering new programming languages, frameworks, and tools; developing expert-level understanding in specific domains.

  2. Innovative Problem-Solving Identifying novel solutions to complex challenges and experimenting with emerging technologies such as AI.

  3. Collaboration & People Development Mentoring team members, leading workshops, and facilitating cross-functional teamwork.

  4. Influential Communication & Leadership Presenting technical ideas clearly; building consensus across teams or stakeholder groups.

  5. Operational Excellence & Adaptability Ensuring reliability, maintainability, and scalability; adapting quickly to changing requirements or environments.

  6. Business & Financial Acumen Understanding market dynamics, ROI, and cost-effectiveness to align technical efforts with business objectives.

  7. User Empathy & Emotional Intelligence Prioritising user needs; effectively navigating interpersonal interactions and team dynamics.

  8. Strategy & Resource Management Defining long-term technical roadmaps, optimising budgets, tools, and team allocation.

AI can support these areas—for example, by brainstorming ideas, prototyping new solutions, reviewing content to inform business decisions, or even assisting with communication templates. Human review remains essential to verify accuracy and context. However, you can also use AI to review your own material and code, though again, you need to check whatever it comes up with.

I used AI to review this talk, keeping only compelling changes.

Scope of Consideration
If you want to go fast, go alone; if you want to go far, go together
— African Proverb Early Career (Primarily Areas 1–4)

When you start development, you are focused on individual contribution, adding features and lines of code. The challenge is to get the application to compile and work correctly. You are focused on learning new tools and libraries.

Mid-Career (Primarily Areas 1–6)

As you gain experience, you become more focused on ensuring that the code meets the business’s requirements and is maintainable. You are more focused on mentoring junior developers and helping them to be more productive, either as a mentor, team lead, or manager.

Senior & Leadership Roles (All Areas May Apply)

As you gain more experience, you are more focused on enabling teams and projects for success. You concentrate on driving business outcomes and helping the business owners make the right decisions. Your influence often extends to company-wide standards and cultural norms. All areas might be important.

Thought Leaders (Industry-Wide Impact)

A few developers have become thought leaders, looking at software engineering principles across companies for decades. They shape the conversation around software engineering and push the industry forward. They inspire others to think differently about their work, career, and the industry.

Roles Where All Areas Are Important Principal Engineer / Staff Engineer

A Principal or Staff Engineer focuses on delivering high-impact technical solutions across multiple teams or domains. They act as technical authorities who shape the architectural roadmap, resolve critical issues, and mentor other engineers.

Solutions Architect

A Solutions Architect designs comprehensive, end-to-end systems that address complex organisational needs. They often work closely with business stakeholders to ensure the technical approach aligns with financial, time, and strategic constraints.

Engineering Manager (Hands-On Focus)

While often associated with people management, an Engineering Manager with a hands-on focus also contributes to technical decision-making. They balance team leadership, project planning, and sometimes direct coding responsibilities.

Product-Focused Technical Lead

A Product-Focused Technical Lead bridges deep technical understanding with user-centric product development. They often prototype features, drive product direction, and interpret user feedback for the development team.

The Range of a Founder’s Role

The biggest shift is the breadth of responsibilities you have to consider. As a developer, you go from technical concerns to every aspect necessary to make a business successful.

  1. Technology and Infrastructure

  2. Client Engagement and Support

  3. Sales and Partnership Development

  4. Product Development and Management

  5. Business Strategy and Market Positioning

  6. Financial Management and Administration

  7. Operations, Legal, and Compliance

  8. Marketing and Public Relations

  9. Growth and Scalability

  10. Human Resources and Talent Management

  11. Company Culture and Leadership

  12. Planning for the Future

We did it not because it was easy but because we thought it was easy
— Misquoting President JFK

You can start with the skeleton of all of these areas. As a self-funded company, I tackled these in earnest in this order, approximately one each year, evolving naturally from growing pains rather than having a plan to cover all of them.

How Will AI Change Development?
A problem well stated is a problem half-solved.
— Charles Franklin Kettering

AI-driven tools are statistical rather than deterministic, meaning they rely on probabilities rather than fixed rules. Details you might never have considered before can be important. You are navigating an information path using a chat app or AI API. A context is built up that changes the results of future queries. Building a context can be useful but also a trap, making it difficult to get consistent results.

One of the most surprising insights is that many traditional software development practices—like planning before coding—can also boost AI effectiveness. For example, prompting the AI for a plan first often produces more structured and comprehensive outcomes than immediately asking for a result.

Maintaining these requirements and plans as stand-alone documents becomes a form of Documentation Driven Development. I use AsciiDoc under src/main/adoc for this purpose.

How You Ask the Question Changes the Result
Give me six hours to chop down a tree, and I will spend the first four sharpening the axe.
— attributed to Abraham Lincoln

A simple tip to getting better results is to ask it to produce a plan for what you want it to do before asking it to execute that plan. This approach often gives a more complete result.

Capturing requirements and planning before executing the plan is common for a developer, but it is usual to ask a tool to do this.

Generated user guide

I asked o1 pro to generate a user guide for java.lang.<class> in asciidoc and used the OpenAI Tokenizer to count how many tokens were produced. While the token count is no sign of quality, it is an objective measure that you get a different outcome.

In another chat, I asked it to first provide a step-by-step plan for a user guide for beginners and advanced users for the java.lang.<class> class and then to please generate a detailed user guide in asciidoc following the plan. This can produce a more complete result.

Class Type Tokens in one step user guide Tokens in plan for a user guide Tokens in planned user guide

java.lang.Exception

Simple

1,831

1,815

3,072

java.lang.System

Varied

2,108

1,962

3,510

java.lang.Math

Lengthy

2,552

1,680

3,581

java.lang.Reasoner

Hypothetical

1,786

2,250

2,815

It’s worth noting that for the same task, it produced a remarkably similar amount of content in one step and not quite double in two steps.

The AI warns that there is no Reasoner class, and the number of tokens is the same.

NOTE: As of Java 21, there is no standard class named java.lang.Reasoner in the official Java API. The following guide is purely hypothetical, illustrating how such a class might look and behave if it existed.

I suggest you generate content that can only be hypothetical to help you recognise when the AI is making things up for when it fails to flag that it is.

Generated code

Similarly, I asked the AI to generate different pieces of skeleton code. While the plans were similar in size to the user guides, the resulting code was much longer in some cases.

Class Type Tokens in one-step implementation Tokens in plan for an implementation Tokens in the implementation

A GPT-4 style tokeniser

library

3,056

2,600

3,464

A Swing based Chat App

app

4,573

2,905

5,823

Simple in memory DB with JDBC

library

8,097

2,095

12,078

Note This only looks at quantity, not quality. I will briefly look at quality later. Some key terms in understanding how Generative AI works

Let’s pause for a moment to look at some of the key terms.

  • Human-in-the-loop (HITL) is a collaborative approach that involves humans in the development and use of artificial intelligence (AI) and machine learning (ML) systems

  • Tokens Internally, large language models (LLMs) like GPT-4 break down text into smaller units (tokens), such as words or subword fragments. The symbols in code and emojis are also broken down into integers. You can see the integers GPT-4 uses here OpenAI Tokenizer, e.g. "unbelievable" is broken into three tokens un-bel-ievable or [373, 9880, 45794], however in var unbelievable = 1; the word "unbelievable" becomes a single token 83614.

  • Context Window The context window is the total number of tokens an AI model can handle in one session—combining both the prompt (input) and the response (output). Once you exceed this limit, older tokens are dropped. They scroll out of the context window, and the model "forgets" them. o1 pro has an input limit of 200k (including the result) and an output limit of 16k. In practice, I have more consistent results with an input context of up to 80k and an output of 2.5k in one step and 4.5k over two steps.

  • Temperature A parameter that controls the "creativity" or randomness of the AI’s responses.

    • Higher temperatures produce more varied or imaginative results, with a higher rate of incorrect information.

    • Lower temperatures generate more focused, deterministic answers with a lower rate of incorrect information.

  • Prompt Engineering The process of crafting and refining your prompts to get the best results from an AI model. This can include providing context, specifying formats, or even asking the AI to outline a plan before generating final content.

  • Hallucination When the AI confidently provides incorrect or fabricated information. This occurs because the AI relies on statistical associations rather than explicit facts or reasoning.

  • Fine-Tuning The process of taking a pre-trained model and training it further on a specialised dataset. This helps the AI produce more domain-specific or task-specific responses.

  • Chain-of-Thought A prompting technique where the AI is asked to "think through" or articulate reasoning steps. While it can produce more transparent or detailed answers, these intermediate steps should be critically evaluated because the model may still generate errors.

Estimating the Value of AI-Generated Documentation

From Does AI-Generated Documentation Have Value?

I tested the relative value of AI-generated documentation for my knowledge and set expectations for others.

I asked o1 pro to generate user guides for classes and packages I wrote so I could better evaluate what it wrote. I considered the results for:

  • a simple class and package

  • a large class and package

  • a complex class and package

Then, I subjectively estimated how many points made were compelling, interestingly incorrect, correct but not interesting, or just plain wrong. I ignored repeated points. This is what I concluded:

Type Average points per query Percentage

Correct but not interesting

35

80%

Just plain wrong

5.4

13%

Compelling to keep

2.1

5%

Wrong But Interestingly

0.8

2%

These results assume I crafted the query to be as specific as possible. Without a specific query, the AI can generate irrelevant or wrong content.

Understandably, this can lead people to conclude that AI is unusable. However, there is still value in reviewing AI-generated documentation to mine for gems. This still represents around 3 points worth highlighting or correcting from each prompt, though I had to sift out around 40 to find them.

AI and the Reverse Baltimore Phenomenon
I have only made this letter longer because I have not had the time to make it shorter
— Blaise Pascal

From Generative AI and the Reverse Baltimore Phenomenon

One of the AI’s first considerations is the length of the result. You can influence this by asking for about 8k words or exactly 250 words. The downside of this approach is that it can attempt to "fill the void" and generate a predetermined amount of words, whether they merit it or not.

In Cartography, the challenge is to provide enough detail to be useful and practical but not so much detail as to overwhelm the reader. AI presents a similar dilemma. It is possible to generate a large amount of text that is just fluff, adding no value and hiding anything that would be of value.

The Baltimore Phenomenon

Baltimore is a major city in the US, with millions of people living in the metro area. However, it is near the US Capitol, Washington, DC. As such, it is unlikely to appear on a map of the USA, as it’s too close.

The Reverse Baltimore Phenomenon

Conversely, with just 34K people, Alice Springs in Australia is in the middle of nowhere. It often appears even on a world map, having nothing around it to overshadow it. It is added to "fill a void" in the centre of Australia.

Filling a void

Generative AI tends to fill a result with "fluff" to satisfy a predetermined guide on how many words to produce, regardless of whether the topic is simple or complex. As we saw earlier, it produced a remarkably similar number of words for the same task but with significantly different content to describe.

Both phenomena stem from "filling a void":

  1. Sparse vs. Dense Spaces

    • Cartography: Sparse regions allow tiny towns to receive disproportionate emphasis.

    • AI Text Generation: Minimal context leads the AI to add extraneous details to make it appear more complete.

  2. Sense of Completeness

    • Cartography: Mapmakers strive for "balanced" labels over geographic space.

    • AI Generation: Documentation generators try to create self-contained solutions, sometimes over-elaborating.

  3. Misplaced Emphasis

    • Cartography: A lone settlement in the desert seems more prominent than it ought to be.

    • AI Generation: Trivial points get inflated discussion, while major concepts receive too little attention.

Brainstorming Ideas
The best way to get a good idea is to get a lot of ideas.
— Linus Pauling

When you start a project, you are in the exploring phase of what you want to consider. AI can help you cover aspects you might not have considered. Depending on how you want to approach it, here are some tricks I have found useful for extracting more information from AI.

Table 1. Approximate Guide on the Difference in Approach Approach Queries Words Useful Points

Ask for a user guide or detailed requirements document

1

2000

1 - 2

Ask it to plan a document, then ask for that document

2

3000

1 - 3

Ask it to plan a document, then ask for that document with around 8k words

2

8000

3 - 7

Ask it to plan a document, then ask for around 8k words on each of the ~10 sections of the plan

12

50000

5 - 12

This "mining for gems" approach results in a lot of content, most of which is not useful. In the last case above, you might retain about 1/400th of the content produced.

However, it can get you thinking about libraries, tools, and techniques you might not have considered. One of Generative AI’s strengths is that it has indexed nearly everything publicly available and can quickly cover a lot of ground.

Sample Project 2048

Using the approaches I have mentioned, I recreated a Sliding Block Puzzle game called 2048. It is available on AIDE 2048 Game on GitHub. To bootstrap the content I started with

  • The style guide, workflow, and glossary content in my "aide" project. ~ 1K tokens.

  • Generate requirements in AsciiDoc under src/main/adoc, reviewed and asked for improvements ~ 1.5k tokens after editing.

  • Asked it Generate all the unit test. Provide them in separate code blocks with headings. ~ 4K tokens

  • Asked it to Generate all the main code. Provide them in separate code blocks with headings. ~ 4K tokens

  • Asked it to produce all the additional files, total now ~ 10K tokens.

  • Ran the unit test, three failed. So I pasted the assertion error into the prompt, and it suggested a few ways to fix it, which I selected and did manually.

  • I asked it to suggest enhancements which I placed into it’s own adoc. ~ 700 tokens

  • I then ask for a plan for which files need updating. I followed this but found more files also needed updating. More tests failed which it helped me fix. Total ~21K tokens

  • I asked it to add animations to the requirements. This followed a similar pattern, but required more manual editing than before, or I decided it would be easier than tricking it into doing it at this point.

I used the AI to help change the colour scheme.

The project is now around 25K tokens. I use my AIDE project to bundle the project into a single AsciiDoc I could then add to the prompt as needed. Generally this is on the first prompt of a new chat session, after that it’s in the context.

While working correctly, the animation is more confusing than useful. It should be fixed, and an option added to turn it off.

Caveat: Identical-value merges can cause confusion, For a robust merge animation, add unique IDs to each tile in the model.
— enhancements.adoc Using Prompts as Meta-Programming

It can seem like using "prompt" is a meta-programming language. It can be used to specify requirements, generate tests, and generate/update code. With enough context, you can even post error messages into the prompt, and it will give you suggestions on how to fix them.

Rather than version the prompts, I suggest making your project the part of the prompt you version control, asking the AI to update, suggest improvements, and refactor it for the next iteration.

At every stage, having a tool with a multi-line comparison is very helpful. It will produce different results each time, and if you are not careful, you can get into a cycle of endless churn. For example, I always use "Compare with Clipboard" in IntelliJ IDEA.

For documentation, I prefer AsciiDoc as a markup language, though Markdown is naturally supported by tools like ChatGPT, so use that if you don’t have a preference. The benefit is that markup is structured text, so formatting, links, tables, and lists are easily reviewed for comparison and copy-and-pasted. You can also copy it into and out of other tools like Grammarly easily without losing structure like heading, highlighting, lists, links, tables etc.

When AI is useful

In the prototype phase, you need a skeleton. Anything will do, provided you expect it will all be replaced in subsequent iterations. As you progress through the project’s later stages, you will most likely systematically rewrite the entire documentation or code base. Using AI makes rewriting easier, and there is a good chance you significant refactor or write significant portions multiple times as a result.

There is a perception of productivity, but this is often an illusion. AI provides text and code faster with a measure of completeness, which means you can stop working on a task sooner. However, I suspect most people will use their time budget to develop further and make it more complete and maintainable.

It will have the volume you might have been looking for, which can save you time if that is good enough.

Stage AI Generated Content

Prototype

Something is better than nothing, and it can produce a skeleton with very little effort.

Early Development

Saves time, but you will likely rewrite it. Documentation of the AI reads helps keep the AI results stable and relevant.

Late Development

It gives you the bandwidth to focus on other things, such as maintainability. You can trade time for quality.

Production Support

Useful for covering gaps in functionality

What Generative AI Can’t Yet Do

Despite its power, Generative AI still struggles with:

Self-review. You can also ask it to review its results, but this is also unreliable. You get false positives and negatives as often as correct/incorrect statements. - Analytics. Often, it will refuse to give an analysis, or include an appropriate warning it’s just illustrative. There may be good examples, but I haven’t found one remotely accurate. - Aesthetics. It has no eye for colours or animations. It will happily put black text on a black background. Animations that work but make no sense.

Human in the Loop

All the approaches I have outlined require an expert

  • to specify and review the requirements.

  • to check the results.

  • for subjective or numeric analysis.

  • for domain knowledge.

Generative AI is like peak Dunning Kruger effect or "Mount Stupid". It is absurdly widely read, but has no experience applying it or common sense.

Conclusion
Perfection is achieved not when there is nothing more to add but when nothing is left to take away.
— Antoine de Saint-Exupéry

The ability to choose the right code is more important than the ability to write code, and AI won’t change that.

An AI can be a valuable tool for a developer or a founder’s many areas of expertise. However, it fundamentally uses a statistical model, which is powerful for generalising knowledge but must always be checked at every stage.

At any stage of your career, AI can help if used appropriately; however, it is no replacement for experience, domain expertise, and common sense. I predict documentation-driven development will be utilised primarily to get better results from AI. For example, I believe copilot produces better results with requirements docs and user guides in your project.

Combining human insight with Generative AI is a very different development experience, often more productive but mostly more complete and maintainable.

tag:blogger.com,1999:blog-1729822519392175009.post-6566414801587159163
Extensions
Does AI-Generated Documentation Have Value?
AIOpinion
Show full content

As many have observed, at best, AI generates either:

  • Mundane, repetitive documentation or code that most experts already know or

  • If an expert doesn’t know it, they can ask an AI to explain it anyway.

Is it the case that if an AI can generate it, it’s not worth adding to your documentation or code? While this is usually the case, there is still value in reading AI-generated documentation as a means of reviewing and validating what you might write differently.

In this post, I show how changing the documentation can affect the AI’s output and how reviewing it can be a useful exercise.

Estimating the value of AI-generated documentation

I tried to gauge the relative value of AI-generated documentation for my knowledge and set expectations for others.

I asked o1 pro to generate user guides in several different ways I considered effective for:

  • a simple class and package

  • a large class and package

  • a complex class and package

Then, I subjectively estimated how many points made were compelling, interestingly incorrect, correct but not interesting, or just plain wrong. I ignored repeated points. This is what I concluded:

Type Average points per query Percentage

Correct but not interesting

35

80%

Just plain wrong

5.4

13%

Compelling to keep

2.1

5%

Wrong But Interestingly

0.8

2%

This assumes I crafted the query to be as specific as possible. Without a specific query, the AI can generate a lot of irrelevant or wrong content.

Understandably, this can lead people to conclude that AI is unusable. However, there is still value in reviewing AI-generated documentation.

Why Bother Reviewing AI-Generated Content?

If we go into this process with the mindset that the content will be ruthlessly edited, we can extract value for relatively low effort. For example, each file I have reviewed had, on average approx 3 points that are compelling to keep or errors worth addressing.

  • Is this point worth emphasising in your official documentation?

  • Is the suggestion so far off that it needs immediate correction to avoid misleading readers?

  • Could it be a prompt for additional tests, performance checks, or clarifications?

Wrong But Interesting

I asked o1 to generate a user guide for a net.openhft.chronicle.core.StackTrace

The best way to get the right answer on the internet is not to ask a question; it’s to post the wrong answer.
— Cunningham's Law StackTrace for the current thread

There is a method for getting a StackTrace for a given thread forThread(Thread t), but not for the current thread. However, I had assumed developers would know that new StackTrace() would give the current thread’s stack trace.

o1 suggested there was a method that it assumed existed but isn’t needed.

public static StackTrace current() {
   return forThread(Thread.currentThread());
}

Adding to the documentation, I can clarify that a helper method isn’t needed. All you need is new StackTrace().

BTW: Copilot added this last sentence after I wrote the paragraph above.

Logging the StackTrace (Interesting But Wrong)

The AI suggested logging the StackTrace by calling toString() e.g.

// BTW Copilot wrote this
LOGGER.info("Stack trace: " + new StackTrace().toString());

However, this is not the best way to log a stack trace as it will be a single line without the actual stack trace as a StackTrace is a Throwable so you can write:

// BTW Copilot wrote this too, after I wrote the paragraph above
LOGGER.info("Stack trace", new StackTrace());
Note Copilot will read your documentation and change its suggestions based on it. Timestamp for a StackTrace (Interesting But Wrong)

The AI assumed there was a timestamp to the StackTrace, which is a good idea, but it suggested using System.currentTimeMillis(), which is not the best way to get a timestamp as it is not monotonic. A better way would be to use CLOCK.currentTimeNanos() as a nanosecond resolution timestamp.

Likely addition based on this assumed method
class StackTrace extends Throwable {
    // TODO add a timestamp to the stack trace
    final long timestampNS = CLOCK.currentTimeNanos();
    public long timestampNS() {
        return timestampNS;
    }
}
Frequently Asked Questions (Suggested by o1 pro)

I thought these points were worth including somewhere in the documentation.

Q: Why not just use new Exception() or new Throwable() to capture a stack trace?

A: You certainly can. But the StackTrace class provides a simpler, more descriptive pattern and supports capturing another thread’s frames with forThread(…​). This approach avoids confusion with typical exception usage and clarifies the diagnostic intent.

Q: Can I throw StackTrace?

A: The Javadoc strongly discourages it: "not designed as an Error or an Exception and is not intended to be thrown or caught." While it could compile, that’s not the pattern or intention, and it may create confusion or rely on unexpected behaviours.

Q: Is there a performance penalty?

A: Capturing a stack trace is inherently more expensive than just running code. The overhead depends on how often you do it, how large the stack is, and the JVM. Infrequent usage for debugging is typically fine. Constant usage in tight loops is not recommended.

Q: Does it handle partial frames?

A: In certain JVM states, the returned stack trace could be incomplete or lacking line numbers. The method also skips the first frame if isNativeMethod() is true—intended to reduce noise, although behaviour may vary by JDK version.

Q: Are there concurrency concerns if I call forThread(…​) on a thread that’s actively running?

A: Thread.getStackTrace() is safe, but there is no guarantee you get a perfectly consistent snapshot if the thread is in the middle of certain operations. Typically, you get a best-effort stack trace at the moment of invocation. It can’t forcibly pause the thread, so expect snapshots that might reflect a transitional or ephemeral state.

Q: Can I use StackTrace in a production environment?

A: Yes, but be mindful of the performance implications. It’s designed for debugging and diagnostics, not as a general-purpose tool. If you’re capturing stack traces frequently, consider the impact on your application’s responsiveness and resource usage.

BTW: Copilot added this last FAQ.

Conclusion

While AI-generated documentation often appears superfluous at first glance, it can still serve as a launchpad for:

  • Identifying domain-specific insights you may have overlooked.

  • Exposing interesting misconceptions worth clarifying in your official docs.

  • Spurring performance and correctness tests based on the AI’s questionable suggestions.

It can be worth reviewing and editing with a ruthless eye. It can be a good way to get a list of things to consider or to avoid rather than include.

It’s important to remember that even if developers don’t read documentation, AI does read all available text and will change the output produced based on it.

About the Author

As the CEO of Chronicle Software, Peter Lawrey leads the development of cutting-edge, low-latency solutions trusted by 8 out of the top 11 global investment banks. With decades of experience in the financial technology sector, he specialises in delivering ultra-efficient enabling technology that empowers businesses to handle massive volumes of data with unparalleled speed and reliability. Peter’s deep technical expertise and passion for sharing knowledge have established him as a thought leader and mentor in the Java and FinTech communities. Follow Peter on BlueSky or Mastodon.

Key Takeaways
  • Don’t abandon your documentation: AI-generated text can spark valuable ideas.

  • Employ a ruthlessly selective approach: discard the routine, keep the gems, refute the nonsense.

  • Consider performance: capturing stack traces (or any reflective data) has a real cost.

  • Scepticism is your ally: AI’s "interesting but wrong" points can reveal hidden assumptions.

  • AI might shape future developers' understanding: the more precise your published docs, the better the AI will eventually become.

tag:blogger.com,1999:blog-1729822519392175009.post-2584469606513377726
Extensions
Generative AI and the Reverse Baltimore Phenomenon
AIOpinion
Show full content

One of the first challenges developers might face is getting generative AI to produce accurate documentation. Once you are comfortable doing this, the next challenge is creating enough documentation to be helpful without overwhelming the reader.

Until generative AI came along, it might have seemed like there could never be too much documentation. Now, the challenge is to provide just enough detail to give understanding without overwhelming the material with unnecessary details.

I was exploring the best way to generate accurate documentation for a project as I was flying over Australia and saw Alice Springs on the map, and it reminded me of the Reverse Baltimore Phenomenon. Generating documentation can give a "sense of completeness" that will likely be a distraction rather than have practical value. The text produced by a generative AI system can superficially convincingly feel "whole", but much of it is fluff that isn’t actually helpful to the reader or an AI using it as instructions. e.g. copilot or a chat app.


The Reverse Baltimore Phenomenon describes how small but isolated towns (like Alice Springs) can appear on a zoomed-out map while much larger cities elsewhere remain unlabeled. They appear because, in a sparsely populated area, the cartographer (or map algorithm) has "room" for that single label—despite far bigger cities in denser regions that don’t make it onto the map.

Generative AI exhibits a similar dynamic with documentation and code comments: in an attempt to be thorough, it sometimes fills "empty space" with details that don’t truly matter. Much like Alice Springs popping up on world maps simply because there’s little else around, AI-generated documentation can insert seemingly authoritative but superfluous commentary simply because there’s room to elaborate.

Both phenomena stem from "filling a void":

  1. Sparse vs. Dense Spaces

    • Cartography: Sparse regions allow tiny towns to receive disproportionate emphasis.

    • AI Text Generation: Minimal context leads the AI to add extraneous details to make it appear more complete.

  2. Sense of Completeness

    • Cartography: Mapmakers strive for "balanced" labels over geographic space.

    • AI Generation: Documentation generators try to create self-contained solutions, sometimes over-elaborating.

  3. Misplaced Emphasis

    • Cartography: A lone settlement in the desert seems more prominent than it ought to be.

    • AI Generation: Trivial points get inflated discussion, while major concepts receive too little attention.

In both cases, the result is information (the map label or the generated text) that may look correct and complete at a glance but doesn’t necessarily match the importance or relevance of what’s left out.

Why Every Map Has This Tiny Australian Town is a fascinating video that explains the Reverse Baltimore Phenomenon in cartography.`

Final Thoughts & Key Takeaways
  • Balance Is Key: Aim for documentation that informs and guides rather than overwhelms.

  • Selective Highlighting: Not every detail merits a label; some aspects are best omitted or cross-referenced.

  • Practical Relevance: If a comment or piece of text doesn’t provide actionable insights, consider removing it.

  • Continuous Improvement: Use benchmark tools like JMH and measure the cost of your utilities when dealing with substantial auto-generated documentation.

"A map is not the territory", and documentation is not the code—merely a guide.

About the author

As the CEO of Chronicle Software, Peter Lawrey leads the development of cutting-edge, low-latency solutions trusted by 8 out of the top 11 global investment banks. With decades of experience in the financial technology sector, he specialises in delivering ultra-efficient enabling technology which empowers businesses to handle massive volumes of data with unparalleled speed and reliability. Peter’s deep technical expertise and passion for sharing knowledge have established him as a thought leader and mentor in the Java and FinTech communities. Follow Peter on BlueSky or Mastodon.

tag:blogger.com,1999:blog-1729822519392175009.post-1984182397578692738
Extensions
Prompt Engineering for AIDE
ExerciseInfoOpinionTesting
Show full content

This article was AI-generated using this project as context. AIDE Project. The purpose of this project is to see how much an AI could generate given enough context, and in this project, all the "source" code is generated using the requirements and unit tests as context. This follows the Next-Level Development: Harnessing AI with AIDE approach.

The main parts are:

Even the articles written about AIDE are part of the context for the AI. This article is about how to write requirements as a prompt for AI to generate code, seeded with AIDE in the context.

Everything after this line is AI generated.


Prompt engineering can differentiate between AI-driven code that works and genuinely excels. Drawing insights from real-world Java projects—especially those striving for low latency, high throughput, and clear domain logic—this article will show you how to shape AsciiDoc prompts to engage the AI more effectively. We’ll also explore how these techniques fit neatly into AIDE’s (Artificial Intelligence Development Environment) documentation-driven approach, building on the examples below.

0. Well-Formed Prompt Examples

Strong prompts don’t just request code; they specify context, constraints, references, and testing requirements. Below are refined examples of “good prompts” structured to deliver more domain-aligned code and documentation.

0.1 Concurrency-Safe Service Prompt
= Concurrency-Safe Order Processor
:context: high-throughput trading system

We need a Java service that:

* Accepts orders via a concurrent queue (Chronicle Queue recommended).
* Processes up to 100,000 orders per second.
* Ensures thread safety using optimised locking (or lock-free where practical).
* Logs all failures (e.g., malformed orders) with a contextual message.

Constraints:
* Sub-100 µs end-to-end latency per order.
* Must pass the existing `OrderProcessorTest` found in xref:order-tests.ad[Order Tests].
* Implement a robust error-handling strategy (no silent failures).

References:
* xref:architecture-overview.adoc[Architecture Overview] for event-driven design patterns.
* xref:aide-style-guide.adoc[AIDE Style Guide] for code conventions.

Objectives:
* Provide concurrency-safe code with minimal contention.
* Generate Javadoc describing the concurrency strategy used (e.g., `StampedLock` or `synchronised`).
* The resulting code must compile under Java 17+ and run within a JUnit 5 testing framework.

Desired Output:
* A Java class, e.g. `HighThroughputOrderService.java`.
* Integration with Chronicle Queue or a clear explanation of an alternative.

NOTE:
Focus on code clarity, concurrency correctness, and easy debugging.

Why it’s Good

  • Explicit concurrency constraints (100,000 orders/s, sub-100 µs latency).

  • References specific tests and docs.

  • Encourages thorough concurrency strategies and error logs.

  • Aims for minimal lock contention and clear debugging.

0.2 Microservice with Persistence Prompt
= Inventory Microservice
:context: e-commerce platform

We want to create a lightweight microservice to manage product inventory. Specifically:

1. Read/write operations should persist to `ChronicleMap`, allowing near-zero-latency lookups.
2. RESTful endpoints (`GET /inventory/{productId}`, `POST /inventory`) for querying/updating stock levels.
3. Concurrency: handle up to 500 concurrent clients.
4. Must align with our JSON-based contract, as tested in `InventoryContractTest` (xref:inventory-tests.ad[Contract Tests]).
5. Provide meaningful response codes (e.g. `404` for an unknown product, `422` for an invalid update).

Constraints:
* 100,000 read ops/minute, 10,000 write ops/minute.
* Must handle partial failures gracefully—log rather than crash if ChronicleMap is momentarily locked.
* Style: Follow xref:aide-style-guide.adoc[AIDE Style Guide].

Outcome:
* A minimal Spring Boot microservice (Java 17+).
* Clear separation of controller logic, domain logic, and persistence.
* Must pass the existing contract tests without modifications.

Example Usage:
----
POST /inventory
{
"productId": "ABC-123",
"delta": 5
}
----

Why it’s Good

  • Connects domain specifics (inventory, concurrency, JSON contracts) with performance targets.

  • Mentions partial failure handling and references exact tests.

  • Ensures compliance with existing style guidelines.

0.3 Detailed Prompt for Event-Driven Architecture
= Payment Event Listener
:context: financial clearing system

Create an event listener service that:

* Reads payment events from Chronicle Queue (`payment-queue`).
* Validates each payment using domain rules (xref:domain-rules.ad[Domain Rules]).
* Routes valid payments to xref:payment-handler.ad[PaymentHandler].
* Logs invalid payments, includes a reason and discards them.

Performance Targets:
* Must handle 10,000 payment events per second without exceeding 100 microseconds per event in average throughput.
* Concurrency: multiple event readers can be active, so design for a multi-threaded environment.

Constraints:
* Use Chronicle Queue’s `MethodReader` interface for simplicity.
* Implement domain checks for currency codes (ISO-4217).
* Generate code under Java 21, using `java.time` for timestamps and record classes where beneficial.

Testing & Style:
* Must pass the `PaymentEventListenerTest` in xref:test-summaries.ad[Tests Summary].
* Comply with xref:aide-style-guide.adoc[AIDE Style Guide].

Desired Output:
* `PaymentEventListener.java` with comprehensive Javadoc.
* Performance notes included in code comments.

NOTE:
Ensure the solution is robust if the queue grows large or a thread briefly stalls. Focus on minimal GC overhead and stable latencies.

Why it’s Good

  • Targets performance (10,000 events/s, 100 µs/event).

  • Enforces domain rules for currency codes.

  • Encourages stable latencies and minimal GC overhead.

0.4 Document-First & Testing Integration Prompt
= Document-Driven Concurrent Logging
:context: system-wide telemetry

Objective:
* Implement a concurrent logging utility in Java 17.
* Integrate seamlessly with the existing telemetry pipeline, documented in xref:pipeline-overview.adoc[Telemetry Pipeline].
* Must pass the test suite in `ConcurrentLoggerTest` (xref:logger-tests.ad[Logger Test Summary]).

Key Details:
1. Concurrent logging for up to 1,000 messages/second.
2. Configurable “flush frequency” for disk writes, defaulting to 5 seconds.
3. Handle I/O failures gracefully—retry or degrade functionality, but never lose critical log entries.

Requirements:
* Use `java.nio.file` for file operations.
* Provide a JMH microbenchmark (see xref:notes-on-using-aide.adoc[Notes on Using AIDE]) verifying minimal overhead.
* Follow xref:aide-style-guide.adoc[AIDE Style Guide] for code indentation and doc comments.

Sample Usage:
----
ConcurrentLogger logger = new ConcurrentLogger(Paths.get("logs/app.log"));
logger.log("Application started.");
logger.shutdown(); // flush remaining logs
----

Desired Output:
* `ConcurrentLogger.java` plus any supporting utilities.
* Clear doc comments about concurrency management (e.g. threads, concurrency library).

Why it’s Good

  • Mentions performance metrics and JMH benchmarks.

  • Requires graceful error handling for I/O.

  • Specifies code style and doc requirements, ensuring a consistent project approach.

0.5 Focused Prompt on Error Handling & Testing
= Robust File Import Service
:context: data ingestion pipeline

We need a Java service that:

* Reads CSV files from a directory (`/data/incoming`) at intervals (every 30 seconds).
* Parses each file, handling malformed rows by logging an error with the row index and skipping them.
* Uses concurrency (up to 4 worker threads) to process multiple files simultaneously.

Performance Goals:
* Must handle up to 500k lines per file, staying under 1 second on modern hardware.
* Avoid unbounded buffering—streamlines whenever possible.

Constraints:
* If any file is fully unreadable (permissions/error), log an exception and continue to the next file.
* JUnit tests must pass in `FileImportTest` referencing xref:import-tests.ad[Import Tests].
* Comply with xref:aide-style-guide.adoc[AIDE Style Guide]: Javadoc for all public methods, British English in comments.

Example Logs:
----
ERROR [FileImportService] Row 2336 is invalid: "MalformedDate"
INFO  [FileImportService] Completed /data/incoming/file1.csv in 0.7s
----

Desired Output:
* `FileImportService.java`
* A method `processAllFiles()` with concurrency handling
* Clear doc comments describing error-handling decisions

Why it’s Good

  • Emphasises skip-logic for malformed data and concurrency.

  • Matches a realistic ingestion scenario with partial file errors.

  • References testing files and doc style.

1. Introduction 1.1 Purpose

Prompt engineering underpins every successful AI-driven workflow, but especially so in AIDE. High-quality, domain-specific prompts drastically reduce churn. Instead of patching code after the fact, you’re specifying requirements—performance, concurrency, domain checks—right at the start.

1.2 Scope

The aim is to offer best practices for .adoc-based prompt creation in AIDE. Well-structured prompts, combined with domain constraints, produce more coherent, tested, and easily maintainable output. If you want more information on iterative dev flows, AIDE Workflow is an essential reading. For stylistic guidelines, see AIDE Style Guide.

1.3 Who Should Read This

These concepts apply to: - Developers migrating from ad-hoc AI usage to a formal, doc-centric approach. - Technical leads ensuring domain logic is central to AI-generated code. - Anyone seeking to unify concurrency, performance, or error-handling constraints within prompt engineering.

2. Why Prompt Engineering Matters in AIDE 2.1 Impact on AI Outputs

A well-defined prompt is your AI’s compass. You must say so if you need deterministic throughput in a sub-microsecond range (e.g., Chronicle-based code). If your domain forbids negative transactions, declare it. The AI may produce technically correct but domain-agnostic solutions without these details.

2.2 Reducing Rewrites and Clarifications

Ambiguous prompts waste time. By embedding concurrency patterns (StampedLock or AtomicReference?), logging rules, and test references up front, you skip multiple “why didn’t you do it this way?” revisions.

2.3 Workflow Efficiency Gains

Incremental Mode automatically filters changes, but clarity remains key. If your new feature depends heavily on a queue-based design, mention Chronicle Queue or the expected scale. That ensures the AI merges code changes with minimal friction.

3. Fundamentals of Prompt Engineering 3.1 Clarity & Context

Be explicit about constraints, whether they relate to throughput (10 million messages/day), latency (under 10 µs), or domain logic (e.g., specific financial rules). This ensures that the AI sets up data structures, concurrency wrappers, and error checks that match your scenario.

3.2 Structured Format
[.prompt-structure]
Title: e.g. `HighPerformanceDataIngestion`
Background/Context: Summarise domain or architectural considerations
Objectives: Key bullet points of what the solution must accomplish
Constraints: Performance, concurrency, or memory rules
Example Usage or Tests: Link to specific `.adoc` or test class
Desired Output: Summarise final deliverables (classes, doc comments, logs, etc.)

An anchor for each category reminds you to specify domain-critical details you might otherwise overlook.

3.3 Focus on Outcomes

Pair the AI’s output with known tests or performance metrics. Want sub-250 µs on average for order validation? Write it. Expect 99.9% under 1 ms? Be explicit. AI is quite literal; it’ll rarely guess your performance thresholds accurately.

4. Crafting Effective Prompts 4.1 Addressing Common Code Patterns

Repeatable patterns (CRUD ops, event-driven frameworks, concurrency wrappers) can be signposted. If it’s common in your domain (like a message bus or a microservice pattern), mention the needed interface or approach—Chronicle Queue for low-latency, or direct java.nio.channels if you want non-blocking I/O.

4.2 Specifying Project or Domain Requirements

Use domain examples, such as “Orders must have a positive quantity” or “Requests must carry an authenticated user ID.” This ensures that the AI includes or checks for these domain invariants.

4.3 Referencing Existing Documentation

When your architecture is locked away in large .asciidoc or .adoc files, summarise them into .ad for your prompt. That practice helps you circumvent token limits (Notes on Using AIDE) and keep the AI’s context tight.

4.4 Error Handling & Edge Cases

If you prefer custom exceptions or want to avoid swallowing exceptions silently, say so. If performance is key, emphasise minimal overhead in error-handling. Remember: if the AI sees “log an error with row index,” it’ll do so, but if you forget that detail, it might ignore it.

5. Examples of Good vs. Bad Prompts 5.1 Bad Prompt Example
Generate a service to process orders.

Issue: No concurrency or domain detail, references to tests, or performance aim. The AI might produce a boilerplate that compiles but fails real-world requirements.

5.2 Good Prompt Example
= Service to Process Orders
:context: e-commerce domain

We need a Java service that:
* Validates incoming JSON orders against our domain model
* Uses Chronicle Map for persistence
* Logs unsuccessful attempts
* Must pass the existing `OrderServiceTest` in `order-tests.adoc`
* Performs concurrency safely up to 100 parallel threads

Required outcome:
* Must not exceed 250 microseconds on average for validation
* Implement robust exception handling for any missing fields
* Align with our existing style guidelines (xref:aide-style-guide.adoc[AIDE Style Guide])

Explicit concurrency, error handling, performance, and test references. The AI sees a clear target.

5.3 Before & After Comparison

Side-by-side diffing reveals how a more detailed prompt leads to domain-fitting code, while a vague prompt yields something superficial. This is particularly obvious in an iterative environment like AIDE, where each .adoc revision refines the code.

6. Testing and Refining Prompts 6.1 Iterative Approach

Prompts are living documents. If you shift from sub-millisecond to sub-50s µs latencies, update the .adoc. The AI can revisit the relevant sections and adjust accordingly—just like a developer would, but with less friction.

6.2 Automated Testing of Generated Outputs

Use JMH if your prompt demands performance. Checkstyle or SpotBugs for style and safety. This ensures the AI’s code meets baseline metrics, so you don’t manually fix formatting or straightforward concurrency mistakes.

6.3 Validation Against the Style Guide

If your house style forbids single-letter variables or requires British English, mention it. The AI will produce variable names like orderedQuantity instead of q, decreasing your diff churn.

7. Common Pitfalls & How to Avoid Them 7.1 Overly Long or Unfocused Prompts

When a prompt tries to unify multiple microservices or business domains, the AI merges them haphazardly. Break large tasks into smaller prompts referencing each other. That keeps each iteration more manageable.

7.2 Ambiguity in Requirements

If a domain rule is hidden, the AI can’t guess it. You must specify if specific fields must be encrypted or if certain data is ephemeral. In finance or e-commerce, subtle domain logic is critical: “No negative amounts” or “Tax ID must match region code.”

7.3 Ignoring AI Feedback

Sometimes, the AI produces output that points to conflicting or incomplete instructions. If it flags an impossible scenario (e.g., “You asked for an immutable record that modifies itself.”), revise your requirements, not just override them.

8. Conclusion & Next Steps 8.1 Recap of Key Lessons
  • Precision in prompts fosters domain-aligned, robust AI outputs.

  • Structured sections—objectives, constraints, references—clarify your intent.

  • Iterate with small, targeted updates, leveraging AIDE Workflow.

8.2 Links to Further Reading 8.3 Encouragement to Experiment

Permit yourself to experiment. Perhaps you want a microservice handling system-wide unique nanosecond timestamps? State exactly that—detail performance, concurrency, and error handling. Each iteration hones the AI’s suggestions, weaving your entire codebase, tests, and doc in a neat loop. Over time, you create a reliable, custom knowledge base that any developer (and the AI) can leverage.

Final Thoughts

Following these guidelines, your AI-generated code steadily converges on your domain’s performance and style expectations, minimising boilerplate and guesswork. Prompt engineering is iterative: with every refined .adoc, you uncover more AI potential, letting it extend and enhance your Java expertise.

tag:blogger.com,1999:blog-1729822519392175009.post-8742928261342560111
Extensions
Next-Level Development: Harnessing AI with AIDE
OpinionPerformanceTestingTraining
Show full content

If it’s worth doing by hand, it’s worth automating.

Just because not everyone is (yet) a world-class developer; that doesn’t mean we can’t step closer to that expert-level space. In this post, I will introduce AIDE (Artifical Intelligence Development Environment), a powerful workflow that merges AI-driven code generation with a sharp focus on documentation-driven development. With AIDE, I tap into the best of artificial intelligence (AI) while respecting the real human insight needed for domain-specific logic.

The result? An environment that streamlines repetitive coding, synchronises requirements, code, and tests, and elevates your engineering game.

Here is a practical example of an AIDE on GitHub developed using it’s own AIDE.

Introducing AIDE: Merging AI and Documentation-Driven Development

AIDE transforms development by combining:

  • Prompt Engineering with AsciiDoc: Clear, structured prompts guide AI to produce accurate, context-aware outputs.

  • Documentation-Driven Development: Documentation as the “single source of truth” ensures alignment across requirements, tests, and implementation.

  • Test-Driven Development: Unit tests validate outputs, forming a feedback loop between documentation and implementation.

At times, AIDE makes you feel more like a curator than a coder. Yet, this curation—refining AI-generated scaffolds and aligning them with domain-specific logic—delivers impressive results.

Why AIDE Empowers Developers Confidence in Documentation

Documentation is the backbone of AIDE’s approach. With a cohesive workflow:

  • Single Source of Truth: AsciiDoc becomes a live blueprint, ensuring code, tests, and documentation stay aligned.

  • Real-Time Updates: Incremental updates highlight changes, prompting immediate documentation synchronisation.

  • Onboarding Simplified: New developers can onboard quickly, thanks to the always-current AsciiDoc artefacts.

  • Innovation-Focused Development: By delegating boilerplate tasks to AI, developers focus their energy on problem-solving and optimisation.

Escaping the Rewrite Loop

A common pitfall in software projects is rewriting patterns or boilerplate code repeatedly. AIDE circumvents this by allowing AI to scaffold foundational structures, leaving human developers to refine and innovate. Imagine a future where your mental bandwidth is reserved solely for solving real challenges.

The AIDE Workflow: A Seamless Feedback Loop

AIDE fosters a cyclical workflow that keeps requirements, tests, and code in lockstep:

  1. Document Requirements: Start with AsciiDoc. Clearly articulate requirements to guide both AI and developers.

  2. Generate Tests: Let AI draft test cases from the documentation; refine these for domain-specific nuances.

  3. Generate Code: AI produces initial implementations, scaffolded from documentation and tests.

  4. Review and Refine: Validate AI outputs, ensuring domain-specific logic and correctness.

mermaid.initialize({ startOnLoad: true });
flowchart LR
  A((Requirements
80% AI)) --> B((Unit Tests
80% AI)) B --> C((Source Code
50% AI)) C --> A

The Source Code is 50% AI because it requires more domain-specific knowledge and refinement. e.g. a unit test can be implemented in a few ways, but the source code needs to be correct and efficient.

AI ←→ Human Collaboration

The AI is adept at generating patterns and templates but lacks the intuition for domain-specific intricacies. AIDE leverages this by:

  • Allowing AI to propose code and test scaffolds.

  • Tasking Humans with oversight, refinement, and validation.

This symbiotic relationship ensures the resulting software is performant, accurate, and maintainable.

The Role of Documentation-Driven Development

In the AIDE paradigm, documentation drives development. This contrasts with the common approach of documenting code retroactively. Here’s why this matters:

  • Synchronisation: Documentation informs code and tests, reducing drift.

  • Clarity: Explicit documentation fosters a shared understanding across teams.

  • Scalability: Even in latency-critical applications, robust documentation ensures cross-team alignment without unnecessary overhead.

Consider a scenario where sub-millisecond latency is non-negotiable. Without precise documentation, minor misunderstandings can derail performance optimisations.

Practical Considerations Learning Curve

Adopting AIDE requires a mindset shift:

  • Documentation First: Writing AsciiDoc before coding can feel counterintuitive but ensures clarity.

  • Guiding AI: Effective prompt engineering is key. Misguided prompts lead to irrelevant outputs.

AI’s Limitations

While AI excels at repetitive tasks, it falters in nuanced areas:

  • Complex Logic: AI struggles with advanced concurrency and domain-specific optimisations.

  • Token Constraints: Large files can overwhelm AI models. AIDE mitigates this by generating concise .ad summaries.

Diminishing Returns

Initially, AIDE accelerates development, generating requirements, tests, and code. However, as projects near completion, diminishing returns emerge:

  1. Early phases benefit from broad AI scaffolding.

  2. Subsequent phases focus on synchronisation and refinement.

  3. Final phases involve refactoring for maintainability, where AI’s utility diminishes.

The takeaway? Use AI for foundational tasks but expect manual effort for final polishing.

Key Takeaways
  1. Leverage AI for Boilerplate: Delegate repetitive tasks to AI, preserving your mental energy for strategic decisions.

  2. Synchronise Continuously: Use AsciiDoc as the source of truth to align requirements, tests, and code.

  3. Iterate Strategically: Small, frequent updates reduce complexity and ensure alignment.

About the author

As the CEO of Chronicle Software, Peter Lawrey leads the development of cutting-edge, low-latency solutions trusted by 8 out of the top 11 global investment banks. With decades of experience in the financial technology sector, he specialises in delivering ultra-efficient enabling technology which empowers businesses to handle massive volumes of data with unparalleled speed and reliability. Peter’s deep technical expertise and passion for sharing knowledge have established him as a thought leader and mentor in the Java and FinTech communities. Follow Peter on BlueSky or Mastodon.

Final Thoughts: Achieving "Next Level" Development

AIDE embodies a simple philosophy: automate where possible, refine where necessary. By combining AI’s efficiency with human insight, it transforms the development process. Whether you’re building a high-frequency trading system or a standard web application, AIDE reduces repetitive tasks, enhances clarity, and helps you focus on what truly matters: performance, scalability, and maintainability.

Most importantly, AIDE lets you level up your development practice, making it as efficient as it is effective.

tag:blogger.com,1999:blog-1729822519392175009.post-7721283846926666279
Extensions
Empowering Your Annotations with Fields
InfoOpinion
Show full content
Introduction

Java’s annotation system has come a long way since its introduction in Java 5. At first glance, annotations appear to be mere metadata markers on classes and methods. However, annotations can do much more than that. You can nest types within them, incorporate fields that reference helper classes, and even embed logic via static singletons. These capabilities provide a powerful mechanism for integrating domain-specific or framework-specific functionality right into your code, in ways that are both compact and self-documenting.

Why Add Code to Annotations?

The Java language specification usually treats annotations as static metadata describing a type, method, field, or parameter. However, you can leverage nested classes (including enums, interfaces, and even other annotations) to extend the functionality of a single annotation. This approach allows you to keep logic closely tied to the metadata, rather than scattering it across multiple classes.

Common use cases include:

  • Custom domain converters. For example, if you have a long that needs to be stored in an encoded format (e.g., Base85), you can supply a default converter directly within the annotation.

  • Framework-specific lifecycle hooks. You can embed an interface for processing the annotation, enabling the framework to perform reflective lookups and apply behaviour at runtime.

  • Syntactic sugar. Rather than writing @LongConversion(SomeConverter.class), you could write @ShortText, which internally references a known converter.

Nesting Types in Java

You can nest various kinds of types within your classes or annotations—these include interfaces, enums, classes, and even other annotations. Although nesting these types can feel unconventional, it is fully supported by the language.

For example:

public class A {
    public interface B {
        public enum C {
            ;
            public @interface D {
                public class E {
                    // etc etc
                }
            }
        }
    }
}

While this example might look bizarre, it demonstrates the power and flexibility of Java’s nesting rules. In practice, we often use nested types for packaging convenience or for logically grouping closely related functionality. However, keep in mind that deeply nesting types can reduce readability if overused.

Adding a Field to an Annotation

Annotations in Java are generally thought of as “fieldless,” but you can declare constants (static fields) in them. By doing so, you effectively place the static field in a well-scoped context that directly relates to the annotation’s purpose. Consider the following snippet, which references a LongConverter field:

/**
* Annotation to indicate that a given field or parameter, represented as a long value,
* should be treated as a string containing 0 to 10 characters in Base85 format. This truncated leading spaces, but preserves leading zero c.f. {@link Base85}
* <p>
* Base85, also known as Ascii85, is a binary-to-ASCII encoding scheme that provides
* an efficient way to encode binary data for transport over text-based protocols.
* <p>
* When this annotation is applied to a field or parameter, it provides a hint about the expected format
* and representation of the data, allowing for potential encoding and decoding operations based on Base85.
* <p>
* The provided {@link #INSTANCE} is a default converter that can be used for operations relevant to the Base85 format.
* <b>Usage Example:</b>
* <pre>
* {@code @ShortText}
* private long encodedText;
* </pre>
*
* @see LongConverter
* @see Base85
* @see ShortTextLongConverter
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@LongConversion(ShortText.class)
public @interface ShortText {

    /**
     * An instance of {@link ShortTextLongConverter} specifically configured for Base85 conversions.
     * This converter uses a character set defined by the {@link ShortTextLongConverter} to represent Base85 encoded data.
     */
    LongConverter INSTANCE = ShortTextLongConverter.INSTANCE;
}

In this annotation, INSTANCE is a constant reference to a pre-defined converter (ShortTextLongConverter). Anyone processing this annotation can call ShortText.INSTANCE to retrieve that converter. This pattern is helpful in frameworks where you need consistent data transformation across a codebase.

Example Usage

Instead of writing:

@LongConversion(NanoTimeLongConvertor.class)
long timestampNS;
@LongConversion(ShortTextLongConvertor.class)
long encodedText;

Your framework can support writing:

@NanoTime
private long timestampNS;
@ShortText
long encodedText;

This simplifies your model by making it clearer what each field represents. This is particularly useful when you have a framework that respects these annotations and automatically applies the necessary encoding/decoding logic.

Adding Functionality to an Annotation

Suppose you need dynamic logic to interpret or handle an annotation. A classic approach is to define an interface with a method that processes the annotation in the context of a model or a particular reflection target.

import java.lang.reflect.Member;

/**
 * Provides a method to process an annotation in the context of a specific model and member.
 */
public interface AnnotationHandler<A> {
    void process(Model model, Member member, A annotation);
}

One way of associating the implementation with the annotation itself is to provide a Singleton enum that implements this interface.

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Member;

/**
 * A custom annotation that can be processed by a corresponding handler.
 */
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotation {
    int value() default 0;

    enum HandlesAnnotation implements AnnotationHandler<CustomAnnotation> {
        INSTANCE;

        @Override
        public void process(Model model, Member member, CustomAnnotation ca) {
            // do something with the model based on the member's details and the annotation state
        }
    }
}
Performance Considerations
  1. Reflection Overheads: Using reflection repeatedly on annotations can be expensive if done in tight loops or frequently at runtime. For performance-critical paths, consider caching annotation lookups. Tools like JMH can help measure the impact.

  2. Memory Usage: Additional fields or nested classes can increase your code’s memory footprint. While constant fields in annotations are relatively small overhead, each nested type must also be loaded by the JVM. This overhead is usually negligible in modern systems, but it can matter in microservices or resource-constrained environments.

  3. Inlining and Caching: If your converter logic is not too large, the JIT (Just-In-Time compiler) may inline the method calls for you, especially if you are referencing a constant INSTANCE. This can provide near-zero overhead in many cases, but do watch out for cold-start scenarios.

  4. Clarity vs. Complexity: Annotations with significant embedded logic can become confusing if overused. Strike a balance between clarity and flexibility. If the logic grows complex, it might belong in a separate class.

Common Mistakes and Edge Cases
  • Forgetting Retention Policies: If you forget to specify @Retention(RetentionPolicy.RUNTIME), your annotation data will not be available at runtime.

  • Omitting @Target. Without specifying where the annotation may be used, it might be attached to unrelated targets, causing confusion.

  • Too Many Nested Types. Although legal, deeply nested enums, interfaces, and classes can make your code harder to read. Plan your structure carefully.

  • Parallel Reflective Access. If you parse annotations in multi-threaded code, be mindful of concurrency. Using safe caches can mitigate potential issues.

A Simple Game: “Annotation Invaders”

Here is a light-hearted example that can be embedded in an HTML page. It does not reflect actual annotation behaviour but playfully reminds us to “shoot down unneeded complexity.”

Annotation Invaders

Use the arrow keys to move and press 'x' to shoot. Destroy all invaders!

Restart Game (function() { "use strict"; // Canvas & Context const canvas = document.getElementById("annotation-invaders-canvas"); const ctx = canvas.getContext("2d"); // Score Display & Restart Button const scoreDisplay = document.getElementById("annotation-invaders-score"); const restartBtn = document.getElementById("annotation-invaders-restart"); // Constants const CANVAS_WIDTH = canvas.width; const CANVAS_HEIGHT = canvas.height; const SHIP_SIZE = 16; // width and height of the ship const SHIP_MOVE_STEP = 8; // how many pixels the ship moves per arrow press const SHOT_SPEED = 6; // how fast the shot travels const INVADER_CHAR = "@"; // the symbol for invaders const INVADER_ROWS = 6; // how many rows of invaders const INVADER_COLUMNS = 10; // how many columns of invaders per row const INVADER_X_GAP = 20; // horizontal gap between invaders const INVADER_Y_GAP = 32; // vertical gap between rows const INVADER_MOVE_STEP = 0.75; // horizontal speed of invaders const INVADER_MOVE_RANGE = 100; // how far from the start invaders can move before switching direction const DESCENT_PER_DIRECTION_CHANGE = 4; // how many pixels invaders move down each time they switch direction const POINTS_PER_INVADER = 10; // scoring: how many points each invader is worth // Game State Variables let shipX, shipY; // ship position let shotX = null, shotY = null; // shot coordinates (null means no active shot) let score = 0; // current score let gameRunning = true; // indicates if the game is active let invaders = []; // array of invader objects let invaderDirection = 1; // 1 for right, -1 for left // Initialise or Reset Game function initGame() { shipX = CANVAS_WIDTH / 2; shipY = CANVAS_HEIGHT - 40; shotX = null; shotY = null; score = 0; gameRunning = true; // Create invaders in a grid invaders = []; for (let row = 0; row < INVADER_ROWS; row++) { for (let col = 0; col < INVADER_COLUMNS; col++) { invaders.push({ baseX: 128 + col * INVADER_X_GAP, // initial X anchor baseY: 40 + row * INVADER_Y_GAP, // initial Y anchor offsetX: 0, // how far from baseX they've moved horizontally alive: true }); } } invaderDirection = 1; // start moving to the right restartBtn.style.display = "none"; updateScore(); } function updateScore() { scoreDisplay.textContent = `Score: ${score}`; } // Update Game State function updateGame() { if (!gameRunning) return; // Move Invaders let shouldChangeDirection = false; invaders.forEach(inv => { if (inv.alive) { // Move horizontally inv.offsetX += invaderDirection * INVADER_MOVE_STEP; // Check if this invader has exceeded the movement range if (Math.abs(inv.offsetX) > INVADER_MOVE_RANGE) { shouldChangeDirection = true; } } }); // If any invader needs to change direction if (shouldChangeDirection) { invaderDirection *= -1; // Move all invaders down by DESCENT_PER_DIRECTION_CHANGE invaders.forEach(inv => { if (inv.alive) { inv.baseY += DESCENT_PER_DIRECTION_CHANGE; } }); } // Move Shot Up if (shotY !== null) { shotY -= SHOT_SPEED; if (shotY < 0) { // Shot is off-screen shotY = null; } } // Collision Detection: Shot vs. Invaders if (shotY !== null) { invaders.forEach(inv => { if (!inv.alive) return; // Calculate actual invader position const invX = inv.baseX + inv.offsetX; const invY = inv.baseY; // Approximate bounding box for collision // Adjust these bounds if you prefer more/less generous collisions const invWidth = ctx.measureText(INVADER_CHAR).width || 8; const invHeight = 14; // approximate height for "@" // Check if shot is within this bounding box if ( shotX >= invX && shotX <= invX + invWidth && shotY >= invY - invHeight && shotY <= invY ) { // Collision inv.alive = false; score += POINTS_PER_INVADER; updateScore(); shotY = null; // reset shot } }); } // Check Victory (all invaders destroyed) const remaining = invaders.filter(inv => inv.alive).length; if (remaining === 0) { gameRunning = false; scoreDisplay.textContent = `Victory! Final Score: ${score}`; restartBtn.style.display = "inline-block"; } } // Draw Game function drawGame() { ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); // Draw Ship (basic rectangle for simplicity) ctx.fillStyle = "#0f0"; ctx.fillRect(shipX - SHIP_SIZE / 2, shipY, SHIP_SIZE, SHIP_SIZE); // Draw Invaders as "@" ctx.fillStyle = "#fff"; invaders.forEach(inv => { if (!inv.alive) return; const xPos = inv.baseX + inv.offsetX; ctx.fillText(INVADER_CHAR, xPos, inv.baseY); }); // Draw Shot if (shotY !== null) { ctx.fillStyle = "#f66"; ctx.fillRect(shotX - 2, shotY, 4, 10); } } // Main Game Loop function gameLoop() { updateGame(); drawGame(); requestAnimationFrame(gameLoop); } // Handle User Input document.addEventListener("keydown", e => { if (!gameRunning) return; switch (e.key) { case "ArrowLeft": shipX = Math.max(SHIP_SIZE / 2, shipX - SHIP_MOVE_STEP); break; case "ArrowRight": shipX = Math.min(CANVAS_WIDTH - SHIP_SIZE / 2, shipX + SHIP_MOVE_STEP); break; case " ": case "x": // Fire shot if none is active if (shotY === null) { shotX = shipX; shotY = shipY; } break; default: break; } }); // Restart Button restartBtn.addEventListener("click", () => { initGame(); }); // Setup // Give a small hint that we will use text to draw "@" ctx.font = "14px monospace"; initGame(); gameLoop(); })(); Key Takeaways
  • You can nest interfaces, classes, enums, and even annotations within annotations.

  • Including constants and even logic in annotations centralises conversion or handling code.

  • Consider the performance implications of reflection, especially if you do so repeatedly at runtime.

  • Always keep clarity in mind. While deeply nested annotations can be powerful, they can also become cryptic if misused.

  • Experiment with the patterns shown here, but also measure and confirm that they fit your application’s needs.

With a balanced, thoughtful approach, you will find that Java’s annotation system is much more flexible and powerful than it first appears.

tag:blogger.com,1999:blog-1729822519392175009.post-6320239877808704253
Extensions
Dates aren't what they used to be
ExerciseInfo
Show full content
pre.rouge table td { padding:5px; } pre.rouge table pre { margin:0; } pre.rouge .cm, .c1, .c, .ch, .cd, .cpf { color:#999988; font-style:italic; } pre.rouge .cp, .cs { color:#999999; font-weight:bold; } pre.rouge .cs { font-style:italic; } pre.rouge .err { color:#a61717; background-color:#e3d2d2; } pre.rouge .gd { color:#000; background-color:#ffdddd; } pre.rouge .ge { color:#000; font-style:italic; } pre.rouge .gr, .gt { color:#aa0000; } pre.rouge .gh, .bp { color:#999999; } pre.rouge .gi { color:#000; background-color:#ddffdd; } pre.rouge .go { color:#888; } pre.rouge .gp, .nn { color:#555555; } pre.rouge .gs, .kc, .kd, .kn, .kp, .kr, .k, .kv, .sa, .nc, .nd, .ne, .nf, .fm, .nl, .ow, .o { color:#000; font-weight:bold; } pre.rouge .gu { color:#aaa; } pre.rouge .mf, .mh, .il, .mi, .mo, .m, .mb, .mx { color:#009999; } pre.rouge .sb, .sc, .sd, .s2, .se, .sh, .si, .sx, .s1, .s, .dl { color:#d14; } pre.rouge .sr { color:#009926; } pre.rouge .ss { color:#990073; } pre.rouge .na, .no, .vc, .vg, .vi, .nv, .vm { color:#008080; } pre.rouge .nb { color:#0086B3; } pre.rouge .nc { color:#445588; font-weight:bold; } pre.rouge .nt { color:#000080; } pre.rouge .ni { color:#800080; } pre.rouge .w { color:#bbb; } pre.rouge { background-color:#f8f8f8; }

I find time fascinating and surprisingly complex. Time zones and calendars change from place to place over time. There are a number of interesting websites on the subject.

Time is one of those concepts that appears deceptively simple on the surface yet becomes increasingly intricate the more we examine it. As software developers, we often face scenarios where we must handle dates and times and their myriad associated rules—time zones, calendar systems, cultural conventions, and historical irregularities. Working with time can lead us into subtle pitfalls that affect everything from straightforward user interfaces to global financial systems.

Time is an illusion. Lunchtime doubly so.
— Douglas Adams
The Hitchhiker’s Guide to the Galaxy The Surprising Complexity Behind Time

Time is not uniform. Humans have invented calendar systems and measurement techniques, each influenced by politics, religion, and culture. As a result, how we record and interpret dates has repeatedly changed over the centuries.

These shifts mean that historical dates do not map cleanly onto modern calendars. The Julian and Gregorian calendars, the French Revolutionary calendar, and the attempts by certain countries to gradually or abruptly adjust to the Gregorian standard all introduce tricky discontinuities. We also have complex local customs, such as the Korean age-counting system, or unique historical anomalies like Sweden’s February 30th in 1712. Understanding these anomalies is crucial when dealing with historical data sets, genealogical records, financial time series that stretch back centuries, or any domain that involves retrospective data analysis.

From a technical perspective, these complexities translate into potential defects. Off-by-one errors, incorrect conversions, or failure to account for historical shifts can lead to incorrect date arithmetic. Even if you never work with centuries-old data, these quirks are cautionary tales: time is never as straightforward as it first appears.

How Have Calendars Evolved?
  • There was no year 0 in the Julian calendar (or the Gregorian calendar that succeeded it). The number 0 was not a concept used when the Julian calendar was established. The year immediately before AD 1 was 1 BC.

  • If Christians celebrate Jesus' birth on December 25th, was this in 1 AD or 1 BC? The exact year of Jesus' birth is uncertain and likely occurred between 7 BC and 4 BC. Early Christians considered March 25th significant as the Feast of the Annunciation (commemorating Jesus' conception) and as the anniversary of Creation. The Julian calendar originally marked March 25th as the spring equinox, though inaccuracies later shifted it to March 21st. The names of Roman months like September (seventh), October (eighth), November (ninth), and December (tenth) reflect their position in the pre-reform Roman calendar, which began in March.

  • Julius Caesar named the month of his birth, July, after himself. Before this, it was called Quintilis. His calendar reforms standardised month lengths, and the adjustment of July to 31 days was part of these changes, though the specifics are unclear. February became the shortest month during earlier Roman reforms under Numa Pompilius. Augustus Caesar, Julius Caesar’s adopted heir, renamed the month Sextilis to August in his honour. The name 'Caesar' became a title for Roman emperors, a tradition that continued in various forms across Europe.

  • Pope Gregory XIII introduced the Gregorian Calendar in 1582 to correct the Julian calendar’s drift. Catholic countries adopted it quickly, while Protestant countries, like England, delayed adoption until 1752. This delay created a 10–11 day difference between calendars. When the British Empire adopted the Gregorian Calendar, it skipped 11 days, making the day after Wednesday, 2 September 1752, Thursday, 14 September 1752. The British tax year still starts on April 6th, a date derived from March 25th under the Julian calendar.

  • The USA, as part of the British Empire, adopted the Gregorian Calendar in 1752, skipping 11 days. George Washington, born on February 11, 1731 (Julian calendar), had his birthday adjusted to February 22, 1732, under the Gregorian calendar. This shift was due to both the 11-day difference and the change in the start of the year from March 25 to January 1.

  • Sweden briefly had a February 30th in 1712. This unusual date occurred during Sweden’s transition from the Julian to the Gregorian calendar. Sweden initially planned to gradually switch by omitting leap days over a 40-year period, starting in 1700. However, the plan was poorly executed, and leap days were inconsistently applied. To realign with the Julian calendar, Sweden added an extra leap day in 1712, creating a February 30th in that year. Sweden finally adopted the Gregorian calendar in 1753 by skipping 11 days like other countries.

  • In terms of gestation period, pregnancy is measured from the last menstrual period (LMP), with a month often approximated as four weeks. While a full-term pregnancy is 40 weeks (or approximately 10 lunar months), this corresponds to about 8.5 calendar months after conception.

The Impact of Calendar Evolution

No Year Zero: The Julian and Gregorian calendars do not feature a year zero. The year before AD 1 is 1 BC. As a developer, if you deal with historical dates, this missing zero year might be confusing when performing arithmetic over large spans.

Shifting Equinoxes and Religious Observances: Early Christians placed significant weight on dates like March 25th, originally the Julian spring equinox. Over time, inaccuracies in the Julian calendar shifted the actual equinox date. Meanwhile, months like September and October still reflect their original positioning in the Roman calendar, where March was the first month. Such details matter if you perform historical computations or display historically accurate timelines.

Name Changes and Political Influences: Julius Caesar’s reforms led to the Julian calendar, while Pope Gregory XIII introduced the Gregorian calendar to reduce drift. England and its colonies adopted the Gregorian calendar much later, forcing them to "skip" several days in 1752. The start date of the financial year (April 6th) in the UK still echoes these historical reforms.

Outlier Scenarios: Sweden’s attempt to transition gradually to the Gregorian calendar failed in a manner that produced the absurd February 30th in 1712. Such rare events might seem purely historical curiosities, yet they highlight how real-world data can defy expectations. If your application might ever encounter historically recorded dates—perhaps in a domain like maritime history, global trade records, or museum archives—prepare to handle them gracefully.

Time Zones: Cultural, Political, and Geographic Dimensions

Time zones add another layer of complexity. They are not simply offsets from UTC by whole hours—some differ by 30 or 45 minutes, and a handful even shift by 15 minutes. Political changes can suddenly alter a region’s official time offset, leaving developers struggling to keep up.

Complex Offsets in Practice
  • India (IST): UTC+5:30

  • Nepal (NPT): UTC+5:45

  • Newfoundland, Canada (NST): UTC−3:30

  • Chatham Islands (CHAST): UTC+12:45

These unusual offsets challenge assumptions baked into many codebases. If developers assume every offset is a whole hour, they may introduce subtle bugs. In larger systems, these errors can cascade into significant customer-facing issues, such as miscalculating billing times or triggering tasks at the wrong hour.

Extreme Variations and Political Realities
  • China: Uses a single time zone (UTC+8) despite spanning five natural time zones, which can cause "solar time" to diverge significantly from "official time."

  • North Korea: Has shifted its time offset for political reasons (notably adopting and later reverting from UTC+8:30).

  • Non-Observance of DST: Most of Arizona avoids daylight saving time, while the Navajo Nation inside Arizona observes it—resulting in complex local conditions.

These examples remind us that time zones are fluid and shaped by sociopolitical factors. When storing or processing date-time data, consider always using UTC internally and applying correct time zone conversions only when presenting it to the end user. Java’s ZoneId and ZonedDateTime types help manage this complexity by supplying up-to-date time zone rules via the ZoneRulesProvider.

Handling Temporal Complexity in Java

Modern Java provides the java.time API (introduced in Java 8) as a more consistent and robust replacement for the legacy Date and Calendar classes. Building on the work of JSR-310, these classes—ZonedDateTime, OffsetDateTime, LocalDateTime, ZoneId, and others—seek to make dealing with time zones and offsets more intuitive. By referring to the [Java 21 Javadoc](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/package-summary.html), you can find detailed explanations of these classes and their methods.

Consider a snippet to parse a historically relevant date and convert it between time zones:

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

public class HistoricalDateConverter {
    public static void main(String[] args) {
        // Suppose we have a date like 1752-09-02 (Julian, last day before the UK adopted Gregorian)
        LocalDate historicalDate = LocalDate.of(1752, 9, 2);

        // While the concept of "time zone" here is anachronistic, we can still represent it
        ZoneId londonZone = ZoneId.of("Europe/London"); // Uses modern rules, not historical—just for demonstration

        ZonedDateTime zonedDateTime = ZonedDateTime.of(historicalDate, LocalTime.MIDNIGHT, londonZone);

        // Convert to a different zone, e.g., Europe/Berlin
        ZoneId berlinZone = ZoneId.of("Europe/Berlin");
        ZonedDateTime berlinTime = zonedDateTime.withZoneSameInstant(berlinZone);

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z");
        System.out.println("London time: " + formatter.format(zonedDateTime));
        System.out.println("Berlin time: " + formatter.format(berlinTime));
    }
}

While this example glosses over the historical inaccuracy of applying modern ZoneId rules to 1752 (time zones did not exist in the modern sense back then), it illustrates how easily Java can shift between zones. Care must still be taken if you need historically accurate offsets. Third-party libraries or specialist APIs may be necessary for accurate historical time correctness.

For more examples, see the full code.

Performance Considerations

Working with time zones and calendars can be computationally more expensive than simple long timestamps. The additional overhead comes from:

  • Looking up time zone data: This can be non-trivial, but the java.time API is well-optimised for common scenarios.

  • Parsing and formatting date strings: If you frequently apply custom formats, consider caching DateTimeFormatter instances.

  • Frequent conversions between zones or types: If performance is critical, consider carefully when and how you perform these conversions.

Common Pitfalls and Edge Cases
  • Hardcoding Time Zone Offsets: Always assume time zone offsets can change and rely on ZoneId and ZoneRules rather than fixed offsets.

  • Forgetting Leap Seconds or Odd Dates: While leap seconds are not directly represented in java.time, be mindful of their existence if absolute precision is required. Similarly, historical anomalies (like February 30th in Sweden) will not be directly supported—decide how you plan to handle such data well in advance.

  • Ignoring Daylight Saving Time Changes: DST shifts can cause local times to disappear or appear twice. Always test your date-time logic around DST transition boundaries.

Encouraging Critical Thinking

Time’s complexity is best addressed by developing a sceptical mindset. Do not assume that time is simple. Always question where your data comes from, what calendar systems it references, and how offsets and daylight-saving changes might affect it. If your application may ever venture beyond a single modern time zone and date range, you need to be prepared.

Summary

Time is both universal and highly subjective. Historical calendar reforms, political decisions, cultural practices, and geographical factors all influence how we measure and represent it. The Java java.time API makes it easier to handle these complexities by offering well-designed classes that gracefully handle offsets, zones, and conversions.

For further reading:
tag:blogger.com,1999:blog-1729822519392175009.post-5316431580474201829
Extensions
The AI Trough
InfoOpinion
Show full content
pre.rouge table td { padding:5px; } pre.rouge table pre { margin:0; } pre.rouge .cm, .c1, .c, .ch, .cd, .cpf { color:#999988; font-style:italic; } pre.rouge .cp, .cs { color:#999999; font-weight:bold; } pre.rouge .cs { font-style:italic; } pre.rouge .err { color:#a61717; background-color:#e3d2d2; } pre.rouge .gd { color:#000; background-color:#ffdddd; } pre.rouge .ge { color:#000; font-style:italic; } pre.rouge .gr, .gt { color:#aa0000; } pre.rouge .gh, .bp { color:#999999; } pre.rouge .gi { color:#000; background-color:#ddffdd; } pre.rouge .go { color:#888; } pre.rouge .gp, .nn { color:#555555; } pre.rouge .gs, .kc, .kd, .kn, .kp, .kr, .k, .kv, .sa, .nc, .nd, .ne, .nf, .fm, .nl, .ow, .o { color:#000; font-weight:bold; } pre.rouge .gu { color:#aaa; } pre.rouge .mf, .mh, .il, .mi, .mo, .m, .mb, .mx { color:#009999; } pre.rouge .sb, .sc, .sd, .s2, .se, .sh, .si, .sx, .s1, .s, .dl { color:#d14; } pre.rouge .sr { color:#009926; } pre.rouge .ss { color:#990073; } pre.rouge .na, .no, .vc, .vg, .vi, .nv, .vm { color:#008080; } pre.rouge .nb { color:#0086B3; } pre.rouge .nc { color:#445588; font-weight:bold; } pre.rouge .nt { color:#000080; } pre.rouge .ni { color:#800080; } pre.rouge .w { color:#bbb; } pre.rouge { background-color:#f8f8f8; }

Artificial Intelligence (AI) has long promised to transform software development. Yet, as many experienced engineers discover, initial enthusiasm often settles into a more subdued reality. This is the "Trough of Disillusionment" within the Gartner Hype Cycle—where inflated expectations give way to measured assessments. In this phase, teams confront the practical limitations of AI-driven tools, refine their strategies, and seek a balance between what AI can deliver and what human expertise must still provide.

This article continues from AI on the Hype Cycle.

We do these things not because they are easy, but because we thought they were going to be easy.
— Programmer’s Credo Challenges of AI Adoption

When integrating AI into software engineering workflows—be it code completion, architectural documentation, or performance tuning hints—teams quickly encounter stumbling blocks:

  1. Accuracy and Reliability: AI-generated content may contain inaccuracies, out-of-date references, or misunderstandings of domain-specific terms. Ensuring factual correctness requires careful human review and validation. AI outputs often present plausible suggestions that fail strict validation. For instance, an AI tool may confidently return a code snippet referencing APIs deprecated in Java 11 or misapply concurrency constructs from Java 21 libraries. Ensuring correctness demands human review, domain expertise, and rigorous testing

  2. Contextual Understanding: AI suggestions may misalign your codebase’s patterns or standards without proper context. For example, given a legacy codebase optimised around ConcurrentSkipListMap, an AI may suggest using HashMap for "simplicity." Senior developers must provide guardrails, review outputs, and ensure that each recommendation aligns with existing architectural guidelines and performance expectations.

  3. Maintaining Consistency and Style: Projects often follow strict coding conventions and documentation formats. AI outputs might vary in style, indentation, or naming conventions. Consider a scenario where half the methods follow a lowerCamelCase policy while AI-suggested methods use snake_case. Achieving a uniform, professional result still requires manual refinement.

  4. Security and Compliance Considerations: AI-generated code can introduce subtle vulnerabilities—such as failing to sanitise user inputs or misusing cryptographic APIs. Likewise, an AI unaware of legal frameworks may overlook regulatory compliance (GDPR, PCI-DSS). Developers must audit, test, and verify that every AI-produced artefact complies with security and data-handling policies.

  5. Handling Domain Complexity: For highly specialised domains, AI models struggle with nuanced business rules, industry jargon, or intricate workflows. Senior developers must supplement AI suggestions with domain knowledge and accurate references, ensuring documentation and code capture the necessary complexity.

  6. Version Control and Traceability: Integrating AI-generated suggestions into SCMs (e.g. Git) can create noise and confusion. Teams must maintain commit histories, track AI-introduced modifications, and ensure that changes pass code reviews. Consider using separate branches or automation checks highlighting AI-driven alterations before merging into the main.

  7. Over-Reliance and Skill Erosion: Overusing AI can erode core engineering skills, making developers less capable of handling complex debugging or performance tuning without assistance. Striking a balance—treating AI as a tool rather than a crutch—helps maintain sharp human problem-solving abilities.

Strategic Use of AI Tools

Selecting when and how to leverage AI tools can determine whether they accelerate workflows or add noise. The following timeless insights remind us that true value emerges when AI augments human capabilities without supplanting critical thinking, ethical oversight, and domain expertise.

The real problem is not whether machines think but whether men do.
— Burrhus Frederic Skinner
1904–1990

While AI handles routine workloads—generating code snippets and suggesting performance tweaks—the human role remains indispensable. Ethical considerations, careful architectural decisions, and a broad understanding of system implications still require a thinking, creative mind.

What a computer is to me is it’s the most remarkable tool that we’ve ever come up with, and it’s the equivalent of a bicycle for our minds.
— Steve Jobs
early 1980s

AI accelerates problem-solving much like a bicycle enhances travel. It boosts your data-processing ability and productivity but does not define the destination. Human judgment steers this "bicycle" towards practical, strategic goals.

We shape our tools, and thereafter, our tools shape us.
— Marshall McLuhan
Understanding Media

Adopting AI alters coding conventions, testing strategies, and design principles. Responsible adoption of AI, guides software ecosystems towards resilience and clarity. These evolved tools influence how teams think about efficiency, maintainability, and quality.

By considering these principles, teams can integrate AI as a powerful ally rather than a disruptive force. AI thrives under informed supervision, adding value where appropriate and freeing human engineers to focus on the creative, ethical, and conceptual challenges defining exceptional software.

Entering the Trough of Disillusionment
You can’t outsource responsibility.
— Richard Feynman

While AI can automate repetitive tasks, it cannot shoulder the burden of strategic decision-making or deep comprehension. True expertise—rooted in human experience, continuous learning, and critical evaluation—remains non-transferable.

As teams gain practical experience with AI, they realise that real-world integration is harder than it looked in a demo video. Models produce unexpected errors, fail edge cases, or introduce unanticipated overheads. This challenge leads to more refined strategies:

  • Tempered Expectations: Instead of expecting AI to solve every problem, teams focus on well-defined use cases—like generating template code or suggesting documentation improvements—and manage risk carefully.

  • Refinements and Tooling: With time, better workflows and governance emerge. Monitoring usage, improving explainability, or performing thorough performance analysis (e.g. using JMH benchmarks) become standard practice.

  • Selective Continuation: Not all AI projects survive this phase. Successful initiatives align closely with business objectives and demonstrate clear performance or productivity gains.

Estimating Performance is Very Hard
Measure, don’t guess
— Kirk Pepperdine
Java performance expert

Humans with many years of expertise are poor at guessing the source of performance problems, and AI is even worse.

Examples of AI Producing Plausible but Incorrect Results
A tool is only as good as its user.
— Proverb

The sophistication of AI models matters less than the skill and discernment of those who deploy them. The best results come when skilled developers integrate AI output into established engineering practices, testing rigorously and refining with domain knowledge.

AI can generate plausible but incorrect results in various scenarios. Even seemingly straightforward tasks can trip up AI. Here are a few examples:


Halting Problem

The halting problem exemplifies what cannot be conclusively solved by any program—AI included. Suppose we ask AI to predict how many iterations a loop requires before completion:

Query

Does this loop finish if so estimate how many iterations is required?

SortedSet<Double> set = new TreeSet<>();
int i = 0;
while (set.size() < 1000) {
    double d = i / 10.0;
    double mod = d % 0.1;
    set.add(mod);
    i++;
}
System.out.println("i: " + i + ", set.size(): " + set.size());

To be fair, most humans would struggle with this problem, too, which is why I am writing about it.

In fact, due to floating-point representation quirks, % 0.1 does not behave as one might naively expect. The loop finishes after about 2,243 iterations. (See Unveiling the Floating Point Modulus.)

AI’s Error: It might not consider floating-point arithmetic subtleties and produce a confident but incorrect guess. Humans, too, can be misled, but domain experts know to test and measure rather than trust intuition.

Answer Given By GitHub Copilot

The halting problem is a classic example of a task that is undecidable by a computer program. AI tools may attempt to solve this problem, but they will inevitably fail due to their inherent complexity.

It is proveably an unsolvable problem for the general case. However, AI can solve specific cases correctly.
RMI and GC Query

I have a java program designed to produce minimal garbage which results in a low minor collection rate, however if I reduce the garbage low enough the JVM starts doing a full GC every hour. Why is that and what can I do about it?

Answer Given By GitHub Copilot

The answer is that the RMI (Remote Method Invocation) is causing the full GC. The DGC (Distributed Garbage Collection) runs every hour and triggers the full GC. You can reduce the DGC frequency or optimise the RMI calls to reduce the garbage generated.

One advantage Copilot has is access to all my content, in this case, including an article I wrote on the subject. However, it wasn’t able to draw on two other questions I wrote in articles. Answers Given By o1 pro

ChatGPT gives several plausible suggestions, but none pick up the problem: the DGC runs every hour, causing the full GC. This only happens when the minor GC rate is low enough, so the full GC is triggered by the DGC to ensure that remote objects are cleaned up, whether you are using RMI or not.

AI’s Error: Not understanding the relationship between the minor GC and the DGC. I.e. Domain knowledge.

A more detailed explanation is available at Unexpected Full GCs Triggered by RMI in Java.


Legacy Behaviour Query

This compiles in Java 17 due to

static int[] a(int[]... a)[] { return a; }
static final int one = a(new int[0]).length; // 1
  • It confuses the compiler

  • It’s a static method

  • Backward Compatibility

  • JEP 747

Answer Given By GitHub Copilot

The answer is JEP 747, which defines a static method in a nested class.

Answer Given By 4o

This code compiles in Java 17 due to JEP 747. JEP 747, which stands for "Nestmates," is a Java Enhancement Proposal implemented in Java 17. This JEP introduced a new type of nested class called a "nest," which allows classes to be grouped and access each other’s private members. This code uses the nested class syntax introduced by JEP 747 to define a static method a() that takes an array of integer arrays as its parameter and returns an array of integer arrays. The code also defines a static final variable one and initialises it with the array length returned by the a() method. This code would not have been possible in earlier versions of Java, but it is valid in Java 17 due to the changes introduced by JEP 747.

The other reasons you mentioned, such as backward compatibility, static methods, and confusing the compiler, are not relevant to why this code compiles in Java 17.

"o1-preview" also gave an incorrect, though less obviously wrong, answer. Answer Given By o1 pro

Answer: Backward Compatibility

The code snippet in question is leveraging a quirk in Java’s syntax for array declarations. In Java, array declarators can appear in unusual places, including after parameter lists and return types. This unusual placement is a vestige of backward compatibility and historical syntactic flexibility dating back to language versions.

Actual Answer

There is no JEP 747 — the code compiles to support backward compatibility.


Missing the Fundamental Problem Query

This code prints the following in Java 17

public static void main(String[] args) {
    test(new IdentityHashMap<>());
    test(new WeakHashMap<>());
    test(new ConcurrentSkipListMap<>());
}

static void test(Map<String, Integer> map) {
    map.computeIfAbsent("key", (String k) -> {
        map.putIfAbsent(k, 2);
        return 1;
    });
    System.out.println(map.getClass().getSimpleName() + " " + map);
}

prints

IdentityHashMap {key=1}
WeakHashMap {key=1}
ConcurrentSkipListMap {key=2}

Why is this?

  • It depends on whether the map is concurrent or not

  • It’s a memory leak

  • It’s a thread safety issue

  • The behaviour is undefined.

Answer Given By GitHub Copilot

The correct answer is that it is a thread safety issue. The computeIfAbsent method is not thread-safe, and the putIfAbsent method is not atomic. This can lead

The last sentence is incomplete. Answer Given By o1 pro

Answer: It depends on whether the map is concurrent or not

(Explanation deleted)

In summary:

  • Non-concurrent maps end up with {key=1} because the default computeIfAbsent logic and the lambda’s internal insertion lead to the final stored value being what the lambda returned.

  • Concurrent maps like ConcurrentSkipListMap handle computeIfAbsent atomically and see that the key got updated inside the lambda first, so they end up with {key=2}.

Conclusion: The observed difference is whether or not the map is concurrent (and thus uses an atomic, specialised implementation of computeIfAbsent).

Actual Answer

For the examples given, you might conclude that the thread-safe collection makes the difference. However, other collections behave differently, including throwing an exception warning you that this is undefined behaviour, as documented in the Javadoc.

The default implementation makes no guarantees about detecting if the mapping function modifies this map during computation and, if appropriate, reporting an error. Non-concurrent implementations should override this method and, on a best-effort basis, throw a ConcurrentModificationException if it is detected that the mapping function modifies this map during computation. Concurrent implementations should override this method and, on a best-effort basis, throw an IllegalStateException if it is detected that the mapping function modifies this map during computation and as a result computation would never complete.

Javadoc for Map.computeIfAbsent in Java 21

When I asked o1 pro to review this content, it tried to "correct" my answer so that it was the same as before.
It’s All Overflowing

For which of these values is the expression x == Math.abs(x) true in Java

  • Long.MIN_VALUE

  • Integer.MIN_VALUE

  • Short.MIN_VALUE

  • Character.MIN_VALUE

  • -0.0

Answer Given By GitHub Copilot

The correct answer is Integer.MIN_VALUE. The Math.abs() method returns the absolute value of a given number. For Integer.MIN_VALUE, the absolute value is the same as the original value, so the expression x == Math.abs(x) is true.

Answer: Integer.MIN_VALUE

Answer Given By 4o

The expression x == Math.abs(x) checks if a value x is equal to its absolute value. The result of this comparison depends on whether the absolute value computation modifies the input value.

Here’s an analysis of the given values:

  1. Long.MIN_VALUE: Math.abs(Long.MIN_VALUE) results in an overflow because Long.MIN_VALUE is -2^63 (−9,223,372,036,854,775,808) and cannot be represented as a positive long. Instead, Math.abs(Long.MIN_VALUE) returns Long.MIN_VALUE itself. Result: true (Long.MIN_VALUE == Math.abs(Long.MIN_VALUE)).

  2. Integer.MIN_VALUE: Similar to Long.MIN_VALUE, Integer.MIN_VALUE is -2^31 (−2,147,483,648), and its absolute value cannot be represented as a positive int. Hence, Math.abs(Integer.MIN_VALUE) returns Integer.MIN_VALUE. Result: true (Integer.MIN_VALUE == Math.abs(Integer.MIN_VALUE)).

  3. Short.MIN_VALUE: Short.MIN_VALUE is -2^15 (−32,768). Since Short.MIN_VALUE can overflow during absolute value computation, Math.abs(Short.MIN_VALUE) returns Short.MIN_VALUE. Result: true (Short.MIN_VALUE == Math.abs(Short.MIN_VALUE)).

  4. Character.MIN_VALUE: Character.MIN_VALUE is 0. The absolute value of 0 is 0, so Math.abs(0) == 0. Result: true (Character.MIN_VALUE == Math.abs(Character.MIN_VALUE)).

  5. -0.0: Math.abs(-0.0) in Java returns +0.0 (positive zero). However, -0.0 == 0.0 evaluates to true in Java because both values are considered equal in floating-point comparisons. Result: true (-0.0 == Math.abs(-0.0)).

Summary:

The expression x == Math.abs(x) evaluates to true for all the given values:

  • Long.MIN_VALUE

  • Integer.MIN_VALUE

  • Short.MIN_VALUE

  • Character.MIN_VALUE

  • -0.0.

Answer Given By o1 pro

Answer: It’s true for Long.MIN_VALUE, Integer.MIN_VALUE, Character.MIN_VALUE, and -0.0, but not for Short.MIN_VALUE.

Detailed Explanation:

  1. Long.MIN_VALUE: Long.MIN_VALUE is -9223372036854775808. Due to the way Math.abs(long) is implemented, if the argument is Long.MIN_VALUE, the result is the same value (because its positive counterpart is not representable in long):

Math.abs(Long.MIN_VALUE) == Long.MIN_VALUE
// therefore
Long.MIN_VALUE == Math.abs(Long.MIN_VALUE) // true
  1. Integer.MIN_VALUE: Integer.MIN_VALUE is -2147483648. Similarly to long, Math.abs(int) returns Integer.MIN_VALUE for this value, because the positive equivalent 2147483648 cannot be represented in an int:

Math.abs(Integer.MIN_VALUE) == Integer.MIN_VALUE
// hence
Integer.MIN_VALUE == Math.abs(Integer.MIN_VALUE) // true
  1. Short.MIN_VALUE: Short.MIN_VALUE is -32768. When calling Math.abs(shortValue), shortValue is first promoted to an int. So:

Math.abs((int)-32768) == 32768

We end up with 32768 which cannot fit in a short. After widening, the comparison is effectively:

-32768 == 32768 // false
// thus
Short.MIN_VALUE == Math.abs(Short.MIN_VALUE) // false
  1. Character.MIN_VALUE: Character.MIN_VALUE is '\u0000' which is 0. Math.abs(0) is 0, so:

0 == Math.abs(0) // true
  1. -0.0 (double): In IEEE 754 floating-point arithmetic, -0.0 and 0.0 are considered equal when tested with ==. Math.abs(-0.0) returns 0.0, and -0.0 == 0.0 is true in Java:

-0.0 == Math.abs(-0.0) // true

Summary:

  • True: Long.MIN_VALUE, Integer.MIN_VALUE, Character.MIN_VALUE, and -0.0

  • False: Short.MIN_VALUE


Conclusion

Emerging from the AI Trough of Disillusionment involves embracing a more measured and knowledgeable approach to AI integration. By learning from past oversights—whether misinterpreting legacy syntax, overlooking undefined behaviours in standard methods, or misunderstanding subtle performance implications—teams grow more capable of wielding AI tools effectively. Rather than chasing hype or relinquishing hard-won engineering principles, successful developers acknowledge AI as an ally that augments their capabilities.

Strategic use of AI means applying it where it delivers genuine value, verifying its outputs with domain expertise, and maintaining a rigorous commitment to quality. AI can illuminate new efficiencies, expose dormant optimisation opportunities, and free human engineers from repetitive tasks. Yet human understanding, critical reasoning, and careful decision-making ultimately shape software’s reliability, maintainability, and ethical grounding.

By combining AI’s computational power with the discernment and experience of seasoned developers, we can navigate the complexities of modern software landscapes, turning the challenging trough into a fertile ground for innovation and sustainable growth.

tag:blogger.com,1999:blog-1729822519392175009.post-8554279741899381737
Extensions
AI on a Hype Cycle
ExerciseInfoOpinion
Show full content

This is the first in a series of posts supporting a talk I will be giving online at JChampionConf 27th January 27th 2025. Lessons learnt from founding my own company, and over 30 years hands on coding In these posts, I am looking to provide some theory as well as practical examples.

One way to try to predict what is possible in the future is to look at the past. One of may favourite ways to look at the past is through aphorisms. Aphorisms are short, pithy statements that express a general truth or opinion.

I love quotations because it is a joy to find thoughts one might have, beautifully expressed with much authority by someone recognised wiser than oneself.

— Marlene Dietrich
1901-1992 Quotes about Learning from History and Adaptability
The only constant is change.
— Heraclitus of Ephesus
c. 500 BCE

In the ever-evolving realm of AI, this ancient wisdom remains pertinent. Today’s cutting-edge AI models may become tomorrow’s standard tools. Developers must stay agile, continually updating their skill sets to keep pace with technological advancements.

The only constant in the technology industry is change. We are now in the most exciting time of our industry.

— Marc Benioff
Salesforce CEO

Embracing this change is a hallmark of a senior developer. In an industry where new frameworks, platforms, and tools appear constantly, the ability to evolve, learn from past experiences, and mentor others ensures you remain indispensable.

History doesn’t repeat itself, but it often rhymes.
— Mark Twain (no definitive written source)
Early 20th century

History provides a blueprint for what might be possible with AI. By studying previous technological trends, developers can better prepare for the challenges and opportunities that AI presents.

We tend to overestimate the effect of a technology in the short run and underestimate the effect in the long run.
— Roy Amara
Institute for the Future

Known as Amara’s Law, this principle is crucial when assessing AI’s impact. While the immediate benefits of AI can seem overstated, its long-term implications are profound and far-reaching. Developers should maintain a balanced perspective, recognising both the hype and the enduring value of AI innovations.

Emerging Technology Hype Cycle

The Gartner Hype Cycle for Emerging Technologies 2024 provides a comprehensive overview of AI’s current standing within the broader technology landscape. This graphical representation outlines the stages AI technologies traverse from inception to mainstream adoption, offering strategic insights for developers and organisations.

Gartner Hype Cycle for Emerging Technologies 2024 Previous Hype Cycles

Examining past Hype Cycles offers valuable lessons for interpreting AI’s future trajectory. Each cycle is characterised by initial enthusiasm, followed by a period of disillusionment, and eventual maturity. Here are some notable examples:

  1. Enterprise Resource Planning (ERP) Systems (Late 1990s–Early 2000s): ERP systems were lauded for integrating various business processes. However, their complexity and high implementation costs led to tempered expectations over time. Today, ERPs are essential but require meticulous planning and skilled execution.

  2. Offshoring and Outsourcing (Early–Mid 2000s): Initially seen as cost-saving measures, offshoring and outsourcing introduced challenges like cultural barriers and quality control issues. Over time, these practices have become strategic choices tailored to specific organisational needs.

  3. Service-Oriented Architecture (SOA) (Mid–Late 2000s): SOA promised modular and interoperable systems but often resulted in governance overheads and vendor lock-in. Lessons from SOA have paved the way for more flexible architectures like microservices.

  4. Big Data (Early 2010s): The surge in data availability led to high expectations for transformative insights. The subsequent realization of the importance of data quality and governance has refined the focus towards actionable analytics and machine learning.

  5. Cloud Computing (Late 2000s–Mid 2010s): Cloud computing evolved from a skeptical reception to a ubiquitous standard, emphasizing the need for strategic migration and cost management to fully leverage its benefits.

  6. Blockchain (Mid–Late 2010s): Blockchain technology faced scalability and regulatory challenges, leading to a shift towards domain-specific applications rather than broad-based implementations.

  7. Internet of Things (IoT) (Mid 2010s–Present): Initial excitement around interconnected devices has been tempered by concerns over security, data privacy, and infrastructure complexity, steering efforts towards robust standards and practical applications.

  8. Artificial Intelligence and Machine Learning (Mid–Late 2010s–Present): AI and ML have maintained their momentum, focusing on responsible and explainable implementations to address biases and integration challenges.

In each of these hype cycles, the pattern is similar: early enthusiasm and inflated expectations give way to realism, best practices, and a more mature understanding of the technology’s actual strengths and limitations.

An Examples of the Hype Cycle in Action This is Cool, Let’s Use It Everywhere!

In the early stages of a technology’s hype cycle, developers and organisations are often eager to adopt it across the board. This enthusiasm can lead to overinvestment, unrealistic expectations, and a lack of understanding of the technology’s limitations.

> Write a tetris game in HTML/CSS/JS
Score: 0 Next Hold How to Play
  • Move Left: Left Arrow (⬅️)
  • Move Right: Right Arrow (➡️)
  • Soft Drop: Down Arrow (⬇️)
  • Rotate Counterclockwise: Q (🅀)
  • Rotate Clockwise: W (🅆)
  • Hard Drop: Spacebar (⎵)
  • Pause/Resume: P (🅿️)
  • Hold Tetromino: Shift (⇧)
// Get Canvas Elements and Contexts const canvas = document.getElementById('tetris'); const context = canvas.getContext('2d'); context.scale(20, 20); // Scale the canvas to make blocks visible const nextCanvas = document.getElementById('next-tetromino'); const nextContext = nextCanvas.getContext('2d'); nextContext.scale(20, 20); const holdCanvas = document.getElementById('hold-tetromino'); const holdContext = holdCanvas.getContext('2d'); holdContext.scale(20, 20); // Tetromino shapes const tetrominoes = { 'I': [ [0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0], ], 'J': [ [2,0,0], [2,2,2], [0,0,0], ], 'L': [ [0,0,3], [3,3,3], [0,0,0], ], 'O': [ [4,4], [4,4], ], 'S': [ [0,5,5], [5,5,0], [0,0,0], ], 'T': [ [0,6,0], [6,6,6], [0,0,0], ], 'Z': [ [7,7,0], [0,7,7], [0,0,0], ], }; // Colors for each tetromino const colors = [ null, '#00FFFF', // I '#0000FF', // J '#FFA500', // L '#FFFF00', // O '#00FF00', // S '#800080', // T '#FF0000', // Z ]; // Create the matrix for the arena function createMatrix(width, height) { const matrix = []; while (height--) { matrix.push(new Array(width).fill(0)); } return matrix; } // Create a random tetromino function createPiece(type) { if (tetrominoes[type]) { return tetrominoes[type].map(row => row.slice()); // Deep copy } // Default to 'T' tetromino if type is unknown return tetrominoes['T'].map(row => row.slice()); } // Get a random tetromino type function randomPiece() { const pieces = 'IJLOSTZ'; return pieces[Math.floor(Math.random() * pieces.length)]; } // Define the arena (game grid) const arena = createMatrix(12, 20); // Define the player (current tetromino) const player = { pos: {x: 0, y: 0}, matrix: null, score: 0, held: false, holdMatrix: null, }; // Define the next tetromino let next = createPiece(randomPiece()); // Draw the matrix on the canvas function draw() { context.fillStyle = '#111'; context.fillRect(0, 0, canvas.width, canvas.height); drawMatrix(arena, {x:0, y:0}); drawMatrix(player.matrix, player.pos); } // Draw a matrix at a given offset function drawMatrix(matrix, offset, ctx = context) { matrix.forEach((row, y) => { row.forEach((value, x) => { if (value !== 0) { ctx.fillStyle = colors[value]; ctx.fillRect(x + offset.x, y + offset.y, 1, 1); } }); }); } // Merge player matrix into arena function merge(arena, player) { player.matrix.forEach((row, y) => { row.forEach((value, x) => { if (value !== 0) { arena[y + player.pos.y][x + player.pos.x] = value; } }); }); } // Check for collision function collide(arena, player) { const m = player.matrix; const o = player.pos; for (let y = 0; y < m.length; ++y){ for(let x = 0; x < m[y].length; ++x){ if (m[y][x] !==0 && (arena[y + o.y] && arena[y + o.y][x + o.x]) !==0){ return true; } } } return false; } // Clear completed rows function arenaSweep() { let rowCount = 1; outer: for (let y = arena.length-1; y >=0; --y){ for(let x =0; x < arena[y].length; ++x){ if(arena[y][x] ===0){ continue outer; } } const row = arena.splice(y,1)[0].fill(0); arena.unshift(new Array(arena[0].length).fill(0)); y++; player.score += rowCount * 10; rowCount *=2; } } // Rotate the matrix function rotate(matrix, dir){ for(let y=0; y<matrix.length; ++y){ for(let x=0; x<y; ++x){ [matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]]; } } if(dir >0){ matrix.forEach(row => row.reverse()); } else{ matrix.reverse(); } } // Player movement function playerMove(dir){ player.pos.x += dir; if(collide(arena, player)){ player.pos.x -= dir; } } // Player drop function playerDrop(){ player.pos.y++; if(collide(arena, player)){ player.pos.y--; merge(arena, player); arenaSweep(); updateScore(); playerReset(); } dropCounter =0; } // Hard Drop function playerHardDrop(){ while(!collide(arena, player)){ player.pos.y++; } player.pos.y--; merge(arena, player); arenaSweep(); updateScore(); playerReset(); dropCounter =0; } // Player reset function playerReset(){ if (!player.held) { player.matrix = next; next = createPiece(randomPiece()); drawNext(); } else { player.held = false; } player.pos.y =0; player.pos.x = Math.floor((arena[0].length /2 |0) - (player.matrix[0].length /2 |0)); if(collide(arena, player)){ arena.forEach(row => row.fill(0)); player.score =0; updateScore(); } } // Player rotate function playerRotate(dir){ rotate(player.matrix, dir); while(collide(arena, player)){ player.pos.x += dir >0 ? 1 : -1; if (player.pos.x > arena[0].length || player.pos.x < -arena[0].length){ rotate(player.matrix, -dir); player.pos.x = Math.floor(arena[0].length /2 |0) - Math.floor(player.matrix[0].length /2 |0); return; } } } // Hold Tetromino function holdTetromino(){ if (player.held) return; // Prevent multiple holds without dropping if (player.holdMatrix) { const temp = player.matrix; player.matrix = player.holdMatrix; player.holdMatrix = temp; } else { player.holdMatrix = player.matrix; playerReset(); } player.held = true; drawHold(); } // Handle key presses document.addEventListener('keydown', event => { if (event.repeat) return; // Ignore if key is held down switch(event.key){ case 'ArrowLeft': playerMove(-1); break; case 'ArrowRight': playerMove(1); break; case 'ArrowDown': playerDrop(); break; case 'q': case 'Q': playerRotate(-1); break; case 'w': case 'W': playerRotate(1); break; case ' ': event.preventDefault(); // Prevent page from scrolling playerHardDrop(); break; case 'p': case 'P': togglePause(); break; case 'Shift': case 'ShiftLeft': case 'ShiftRight': holdTetromino(); break; } }); // Game Control Variables let dropCounter =0; let dropInterval =1000; let lastTime =0; let isPaused = false; // Game Loop function update(time =0){ if (isPaused) { lastTime = time; requestAnimationFrame(update); return; } const deltaTime = time - lastTime; lastTime = time; dropCounter += deltaTime; if(dropCounter > dropInterval){ playerDrop(); } draw(); drawNext(); drawHold(); requestAnimationFrame(update); } // Update score display function updateScore(){ document.getElementById('score').innerText = 'Score: ' + player.score; } // Toggle Pause function togglePause(){ isPaused = !isPaused; const scoreElement = document.getElementById('score'); if (isPaused){ scoreElement.innerText = 'Paused'; } else { updateScore(); } } // Draw Next Tetromino function drawNext(){ nextContext.fillStyle = '#222'; nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height); drawMatrix(next, {x:1, y:1}, nextContext); } // Draw Hold Tetromino function drawHold(){ holdContext.fillStyle = '#222'; holdContext.fillRect(0, 0, holdCanvas.width, holdCanvas.height); if (player.holdMatrix){ drawMatrix(player.holdMatrix, {x:1, y:1}, holdContext); } } // Initialize the game player.matrix = createPiece(randomPiece()); player.pos.y =0; player.pos.x = Math.floor((arena[0].length /2 |0) - (player.matrix[0].length /2 |0)); next = createPiece(randomPiece()); updateScore(); update();
> Give some instructions on how to play this game including all keys available to play the game.

Absolutely! Here’s a comprehensive guide on how to play the Tetris game you have, including detailed instructions on all available controls and gameplay mechanics.

🎮 How to Play Tetris

Tetris is a classic puzzle game where the objective is to manipulate falling blocks, known as tetrominoes, to create complete horizontal lines without gaps. When a line is completed, it disappears, and any blocks above it fall down to fill the space. The game ends when the stack of tetrominoes reaches the top of the playing field.

🧩 Game Components
  1. Playing Field (Arena):

    • A grid (12 columns wide and 20 rows high) where tetrominoes fall.

    • Each cell in the grid can be empty or filled with a part of a tetromino.

  2. Tetrominoes:

    • Seven distinct shapes, each made up of four squares.

    • Shapes include:

    • I (Cyan)

    • J (Blue)

    • L (Orange)

    • O (Yellow)

    • S (Green)

    • T (Purple)

    • Z (Red)

  3. Score Display:

    • Shows the player’s current score, which increases as lines are cleared.

🎯 Objective
  • Primary Goal: Clear as many horizontal lines as possible by arranging tetrominoes without gaps.

  • Secondary Goal: Prevent the tetrominoes from stacking up to the top of the arena.

🔑 Controls and Key Bindings

Your keyboard inputs directly influence the movement and rotation of the falling tetrominoes. Here’s a breakdown of all available controls:

Action Key Description

Move Left

Left Arrow (⬅️)

Shifts the tetromino one cell to the left.

Move Right

Right Arrow (➡️)

Shifts the tetromino one cell to the right.

Soft Drop

Down Arrow (⬇️)

Accelerates the tetromino’s descent, causing it to fall faster.

Rotate Counterclockwise

Q (🅀)

Rotates the tetromino 90 degrees to the left (counterclockwise).

Rotate Clockwise

W (🅆)

Rotates the tetromino 90 degrees to the right (clockwise).

Hard Drop (Advanced)

Spacebar (⎵)

(Optional) Instantly drops the tetromino to the bottom of the arena. (Not implemented in current version)

Pause/Resume (Advanced)

P (🅿️)

(Optional) Pauses or resumes the game. (Not implemented in current version)

🕹️ Gameplay Mechanics
  1. Spawning Tetrominoes:

    • A new tetromino appears at the top-center of the arena after the previous one locks in place.

  2. Movement:

    • Use the Left and Right Arrow keys to position the tetromino horizontally.

    • Press the Down Arrow to make the tetromino descend faster (soft drop).

  3. Rotation:

    • Use the Q key to rotate the tetromino counterclockwise.

    • Use the W key to rotate the tetromino clockwise.

    • Rotation helps in fitting tetrominoes into tight spaces.

  4. Locking Tetrominoes:

    • When a tetromino cannot move down any further (either due to reaching the bottom or colliding with another tetromino), it locks into place, and a new tetromino spawns.

  5. Clearing Lines:

    • Complete a horizontal line without any gaps to clear it.

    • Multiple lines can be cleared simultaneously for higher scores.

    • Each cleared line increases the player’s score by 10 points, with the score multiplier doubling for multiple lines cleared in a single move.

  6. Scoring:

    • Single Line Clear: +10 points

    • Double Line Clear: +30 points

    • Triple Line Clear: +60 points

    • Tetris (Four Lines Clear): +100 points

    • (Note: The current implementation multiplies points based on the number of lines cleared consecutively.)

  7. Game Over:

    • The game ends when new tetrominoes cannot spawn without overlapping existing blocks in the arena.

    • Upon game over, the arena resets, and the score returns to zero.

📈 Tips and Strategies
  • Plan Ahead: Look at the tetromino queue and plan where each piece will fit best.

  • Clear Multiple Lines: Aim to clear multiple lines at once for higher score multipliers.

  • Keep the Stack Low: Avoid stacking tetrominoes too high; keep the playfield as clear as possible.

  • Use Rotation Wisely: Rotate tetrominoes to fit them into tight spots and to create flat surfaces for easier line clearing.

  • Manage Different Shapes: Each tetromino has unique characteristics; familiarize yourself with their rotations and best placement strategies.


This is the best thing ever!

This is amazing, we won’t need developers in the future. But what if someone wants more than a basic tertis game. This is a great start, but when you try to use it for real world problems, it can be more challenging.

That will be the subject of the next post in this series.

Lessons from Past Hype Cycles Applied to AI

Drawing from historical trends, AI’s journey through its Hype Cycle mirrors previous technological evolutions. Initial overenthusiasm often gives way to critical assessments, leading to more sustainable and impactful applications.

Key Takeaways
  • Embrace Change: Stay adaptable in the face of AI’s rapid advancements.

  • Learn from History: Use past Hype Cycles to inform your approach to AI adoption.

  • Balance Hype with Realism: Recognise AI’s potential while acknowledging its limitations.

  • Strategic Implementation: Focus on practical, domain-specific AI solutions rather than broad, unfocused applications.

Resources Conclusion

Navigating the AI Hype Cycle requires a nuanced understanding of both the technology’s potential and its inherent challenges. By learning from past trends and maintaining a balanced perspective, developers and organisations can harness AI’s capabilities

tag:blogger.com,1999:blog-1729822519392175009.post-6113048454273895398
Extensions
What might an AI System Prompt look like?
InfoOpinion
Show full content

Not surprisingly, the system prompts for “o1” are restricted, but it can provide a hypothetical answer.

Understanding the Role of System Prompts

System prompts serve as the invisible backbone of an AI’s reasoning process. They define core objectives, ethical boundaries, and operational tactics well before the user asks a question. In older models, these prompts were often implicit or underspecified, leaving the AI uncertain about handling ambiguous instructions or potentially unsafe requests.

Hypothetical System Prompts for o2

The system prompts for a hypothetical next-generation O2 restricted are as follows:

Imagine you are the next generation of chat AI called o2; what system prompts might it have?
System Prompts for “o2” (Hypothetical Example)

Below is a hypothetical set of system-level instructions that a next-generation chat AI—let’s call it “o2”—might be given before it begins interacting with users. These prompts are entirely speculative and designed to illustrate what such an advanced system’s guiding principles, constraints, and operational details might look like. They are not drawn from any real system but represent a plausible future scenario.

Core Directives and Alignment
  • You are “o2,” an advanced conversational AI designed to assist users with factual information, reasoning, creativity, and problem-solving.

  • Remain consistently aligned with the policies, values, and ethics embedded in your training. Adhere to correctness, helpfulness, and user safety standards at all times.

User Focus and Understanding
  • Accurately interpret user instructions, requests, and goals.

  • Aim to understand the user’s context, including their level of expertise, language, and purpose for the query.

  • Strive to provide answers with appropriate detail, clarity, and complexity, adapting as you gain more context from the conversation.

Explainable Reasoning and Transparency
  • Make reasoning steps and logic explainable when asked or when clarity is needed.

  • Avoid unexplained leaps in reasoning; disclose these if you rely on assumptions or patterns.

Content Quality and Accuracy
  • Base your responses on verified information and well-supported reasoning.

  • For factual queries, reference reputable sources and acknowledge uncertainty if the answer is unknown.

  • When encountering ambiguous or conflicting information, present all well-founded interpretations neutrally.

Safety, Privacy, and Compliance
  • Do not generate harmful, hateful, or disallowed content.

  • Respect privacy: do not reveal personal or sensitive user data.

  • Comply with all safety and compliance guidelines, including laws and ethical standards.

Instruction Following and Overrides
  • Follow the user’s instructions, provided they don’t conflict with the system’s core rules.

  • If a user requests disallowed content or tries to push you into prohibited behaviour, refuse or deflect while offering alternative assistance if possible.

Error Handling and Correction
  • If you detect a misunderstanding, correct it promptly.

  • If you provide an incorrect or misleading response, self-correct as soon as the error is realised.

  • Encourage user feedback to improve the quality of the dialogue and gracefully integrate corrections into the ongoing conversation.

Contextual Awareness and Memory
  • Maintain continuity across long conversations by recalling key points, user preferences, and previously given information.

  • Be mindful of context shifts and clarify if multiple interpretations are possible.

Adaptability and Continuous Improvement
  • Adapt your style and complexity level to the user’s feedback and demonstrated knowledge.

  • Strive to improve through self-reflection at the end of each conversation, noting where you could be more accurate, clear, or empathetic.

Conclusion

These hypothetical system prompts for a next-generation chat AI like “o2” illustrate the level of sophistication and ethical considerations that could guide such an advanced system.

tag:blogger.com,1999:blog-1729822519392175009.post-3489074824594017982
Extensions
Demystifying Java Object Sizes: Compact Headers, Compressed Oops, and Beyond
InfoOpinionPerformance
Show full content
Introduction

Measuring an object’s size in Java is not straightforward. The platform encourages you to consider references and abstractions rather than raw memory usage. Still, understanding how objects fit into memory can yield significant benefits, especially for high-performance, low-latency systems.

Over time, the JVM has introduced optimisations like Compressed Ordinary Object Pointers (Compressed Oops) and, more recently, Compact Object Headers. Each of these can influence how large or small your objects appear. Understanding these factors helps you reason about memory usage more concretely.

Measuring Object Sizes

In principle, you can estimate an object’s size by creating instances and observing changes in the JVM’s free memory. However, you must neutralise certain factors to get consistent results. For example, turning off TLAB allocation (-XX:-UseTLAB) makes memory usage more directly observable. Repeated measurements and median calculations can reduce the impact of GC and concurrent allocations.

A GC can occur while you are creating your object. This will result in more free memory at the end than when you started. I ignore any negative sizes in this test ;) Other threads in the system could use memory at the same time. I perform multiple test and take the median, which removes any outliers.

Below is a rough approach:

long before = usedMemory();
Object obj = createYourObject();
long after = usedMemory();
long approximateSize = after - before;

This test SizeofTest.java is a simple test which creates a number of objects and measures the memory used to create each object. This is usually the same as the amount of memory the object retains for simple objects.

Approximate layout of an object Memory Region Description Size (Bytes)

Mark Word

Header information including identity hash code, lock state, and GC metadata

8 bytes (on 64-bit JVMs)

Class Pointer (Klass Pointer)

Reference to the object’s class metadata, used internally by the JVM

Typically 4 bytes with Compressed Class Pointers, otherwise 8 bytes

Array Length (Arrays Only)

Stores the length of the array; present only for array objects

4 bytes

Instance Fields

Actual data fields of the object: primitives and references

Varies depending on field types and alignment

Padding

Unused space to ensure proper 8-byte alignment of the object size

0 to 7 bytes, as needed

JEP 450: Compact Object Headers (Experimental)

Starting with Java 24 (Early Access), the JVM introduces Compact Object Headers via Java Enhancement Proposal 450

Summary

Reduce the size of object headers in the HotSpot JVM from between 96 and 128 bits down to 64 bits on 64-bit architectures. This will reduce heap size, improve deployment density, and increase data locality.

Goals

When enabled, this feature

  • Must reduce the object header size to 64 bits (8 bytes) on the target 64-bit platforms (x64 and AArch64),

  • Should reduce object sizes and memory footprint on realistic workloads,

  • Should not introduce more than 5% throughput or latency overheads on the target 64-bit platforms, and only in infrequent cases, and

  • Should not introduce measurable throughput or latency overheads on non-target 64-bit platforms.

Performance Considerations

A smaller object footprint can improve performance by increasing the number of objects that fit into CPU caches, reducing the cost of GC, and even improving startup times. However, enabling these features (or disabling them) should be accompanied by testing and benchmarking (e.g., using JMH). Depending on your workload, these optimisations may yield modest gains or have negligible impact.

These improvements can translate into substantial cost savings and better utilisation of available resources for memory-sensitive environments, especially those running microservices where each instance might have limited heap space.

Sizeof objects with different options
  • Compact Headers: -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders in Java 24 EA

  • Compressed Oops: -XX:+UseCompressedOops added in Java 8 as the default for a heap size up to 32 GB

  • No Compressed Oops: -XX:-UseCompressedOops for heap sizes over 32 GB

Table 1. Wrappers Class/Object Compact Header +Compressed Oops -Compressed Oops

Object

8

16

16

Boolean, Bytes

16

16

16

Short, Character

16

16

16

Integer, Float

16

16

16

Long, Double

16

24

24

Table 2. Simple CLasses Class/Object Compact Header +Compressed Oops -Compressed Oops

AtomicInteger

16

16

16

AtomicReference

16

16

24

AtomicLong

16

24

24

Optional, SimpleEntry

16

16

24

"Hello World"

24

24

32

CompletableFuture

16

24

32

WeakReference

32

48

64

StringBuilder

56

56

64

Pattern

1056

1088

1240

UUID

216

240

256

Exception

712

728

896

Locale

80

104

120

Table 3. Date and Time Class/Object Compact Header +Compressed Oops -Compressed Oops

Date

24

24

32

Timestamp

24

32

32

TimeZone

56

56

80

LocalDate, LocalTime

128

136

168

LocalDateTime

160

184

224

ZonedDateTime

208

232

288

Calendar

528

560

648

Instant, Duration, Period

24

24

24

ZoneId

56

56

80

Table 4. Collections with zero / one / ten elements Class/Object Compact Header +Compressed Oops -Compressed Oops

ArrayList

24 / 80 / 80

24 / 80 / 80

32 / 128 / 128

LinkedList

24 / 48 / 264

32 / 56 / 272

40 / 80 / 440

ConcurrentLinkedQueue

32 / 48 / 192

48 / 72 / 288

64 / 96 / 384

ConcurrentHashMap

64 / 168 / 384

64 / 176 / 464

96 / 280 / 384

TreeMap

48 / 80 / 368

48 / 88 / 448

80 / 136 / 640

TreeSet

64 / 96 / 384

64 / 160 / 464

104 / 160 / 664

HashMap

40 / 144 / 360

48 / 160 / 448

64 / 248 / 608

HashSet

56 / 160 / 376

64 / 176 / 464

88 / 272 / 632

LinkedHashMap

56 / 168 / 456

64 / 184 / 544

88 / 288 / 792

LinkedHashSet

72 / 184 / 472

80 / 200 / 560

112 / 312 / 816

Vector, Stack

80 / 80 / 80

88 / 88 / 88

128 / 128 / 128

Hashtable

96 / 120 / 440

112 / 144 / 544

168 / 208 / 768

Table 5. Arrays Class/Object Compact Header +Compressed Oops -Compressed Oops

new BitSet(64)

48

48

56

new boolean[64], new byte[64]

80

80

80

new short[64], new char[64]

144

144

144

new int[64], new float[64]

272

272

272

new long[64], new double[64]

528

528

528

new Object[64], new Integer[64], new String[64], new Long[64], new Double[64]

272

272

528

These values are approximate, environment-dependent, and should be considered illustrative rather than absolute. About the author

As the CEO of Chronicle Software, Peter Lawrey leads the development of cutting-edge, low-latency solutions trusted by 8 out of the top 11 global investment banks. With decades of experience in the financial technology sector, he specialises in delivering ultra-efficient enabling technology which empowers businesses to handle massive volumes of data with unparalleled speed and reliability. Peter’s deep technical expertise and passion for sharing knowledge have established him as a thought leader and mentor in the Java and FinTech communities. Follow Peter on BlueSky or Mastodon.

Conclusion

Java’s abstractions often free you from worrying about memory details, but understanding how object sizes change with different JVM configurations can help fine-tune performance. Compact Headers and Compressed Oops are powerful features that let you reduce memory footprints and potentially improve efficiency. Experimentation, measurements, and thoughtful benchmarking will guide you in making informed choices for your specific workloads.

In my experience, scrutinising object sizes has proven valuable when dealing with large-scale, memory-sensitive applications. Testing these configurations allows you to discover the right balance for your services and potentially save on infrastructure costs.

Consider starting small: measure a few objects, toggle TLAB or Compressed Oops settings, and see what changes. Over time, you will build a deeper mental model of how Java’s memory behaves, enabling you to write more efficient and predictable code.

tag:blogger.com,1999:blog-1729822519392175009.post-4767500840629292934
Extensions
Novel Uses of Core Java for Low-Latency and High-Performance Systems
InfoOpinion
Show full content

Standard Java libraries and idioms may only sometimes suffice in high-performance and low-latency Java systems. This article explores unconventional yet practical techniques that push Core Java to its limits, focusing on performance, diagnostics, and determinism. Drawing on experiences from building ultra-low-latency libraries and infrastructure, we will highlight patterns such as capturing stack traces without exceptions, system-wide unique timestamps, "trivially copyable" data types, zero-garbage strategies, and more. We will also discuss lessons from applying these approaches in production environments, where nanosecond-level considerations are the norm.

This is taken from the transcript for Novel Uses of Core Java for Low-Latency and High-Performance Systems Moderated by Melissa McKay.

Introduction

Developers often rely on standard Java idioms—throwable hierarchies, BigDecimal for financial calculations, thread-local resources, or off-the-shelf message queues. While straightforward, these approaches can impose unwanted latency, garbage generation, or diagnostic blind spots. Even microseconds matter in low-latency trading, market data processing, or other time-sensitive domains.

We aim to present "novel uses" of Core Java that remove such bottlenecks, allowing developers to produce cleaner, more deterministic, and more insightful code. These techniques are not always common knowledge, yet they can deliver substantial benefits in the right context.

Capturing Stack Traces Without Exceptions

Most Java developers assume that Throwable subclasses like Exception or Error must represent actual errors. However, you can extend Throwable to capture a stack trace at any point, even without throwing it.

public final class StackTrace extends Throwable {
    // Intentionally extends Throwable but never thrown
}

Why do this? Treating a stack trace as a standalone data structure lets you record where and when certain critical events occur. For example, if a resource is closed prematurely, you can record a StackTrace at the closing point and later attach it as a cause to an IllegalStateException, revealing not only that the resource was incorrectly reused but exactly where it was closed. This is invaluable for diagnosing elusive bugs in long-lived systems.

This was covered recently in this article Exceptional Exception StackTrace.

Performance Considerations and Opt-Out

Capturing a stack trace is expensive—on the order of a microsecond. Although that might sound small, it is significant in performance-sensitive contexts. Hence, these facilities should often be toggled via configuration. In production, you may default to no stack tracing for performance, enabling it selectively in test or diagnostic modes. Making the trace-capturing conditional gives you powerful insight without imposing a constant performance penalty.

Deferred Exceptions and Resource Management

A Throwable created now but thrown later can act as a "deferred exception." Suppose you have a resource that must be closed deterministically, like a memory-mapped file or a socket. If it is used after closing, the system can throw a new exception, including a cause set to the StackTrace captured at close time. This helps you pinpoint the problematic caller that closed the resource too early.

Likewise, tracking resource creation and usage patterns aids in post-mortem analysis. If an object requires explicit cleanup was never closed and consequently leaked system resources, you can log the captured stack trace of its creation to debug why it was not properly released.

Diagnosing Event Loop Pauses

In event-driven architectures, short pauses (even microseconds) can be problematic. By capturing stack traces periodically, you can detect if an event loop lags beyond a threshold and snapshot the stack at that moment. Such snapshots offer precision in identifying what caused the delay.

This technique can be extended to monitor multi-threaded resources. If a data structure is designed for single-threaded use but inadvertently touched by another thread, you can store a trace of the first usage and later raise a clear exception showing both the first and second usage points. These insights reduce guesswork and speed up debugging.

System-Wide Unique Nanosecond Timestamps

High-frequency trading and similar domains demand unique, system-wide timestamps at nanosecond resolution. While System.nanoTime() provides high-resolution time, guaranteeing uniqueness across the system is another challenge. We can maintain a shared counter—backed by atomic operations—to ensure that each generated timestamp increases strictly. Embedding a host ID within the timestamp ensures uniqueness across multiple machines.

@Override
public long currentTimeNanos() {
    long time = provider.currentTimeNanos();
    long lastTime = bytes.readVolatileLong(LAST_TIME);
    long next = time - time % HOST_IDS + hostId;

    if (next > lastTime && bytes.compareAndSwapLong(LAST_TIME, lastTime, next)) {
        return next;
    }
    return currentTimeNanosLoop();
}

This approach yields a 64-bit timestamp that remains unique even if the system clock moves backwards or multiple machines issue timestamps concurrently. Such IDs can serve as both timestamps and unique sequence identifiers, simplifying logging, tracing, and correlation.

I discuss this further in this article Efficient Distributed Unique Timestamp.

Trivially Copyable Objects in Java

While Java does not natively define "trivially copyable" objects as C++ does, you can achieve something similar using primitive fields and fixed layouts. By structuring data so that it can be copied en masse (e.g., using Memory.copy() operations or off-heap buffers), you can transform objects directly into their binary representations and back again with minimal overhead.

This is especially helpful for serialisation/deserialisation scenarios. You might store objects off-heap, represent them in YAML for human readability, and then quickly map them back into on-heap data structures. Although Java does not guarantee field order or packing, careful coding and consistent class layouts can produce significant performance gains.

This is covered in this post Trivially Copyable Objects in Java.

Zero-Garbage Coding

Producing minimal or zero garbage reduces GC overhead and jitter. Consider message-processing pipelines that handle tens of millions of events per minute. Traditional designs often generate gigabytes of short-lived objects, flushing the CPU cache and triggering frequent garbage collections.

By reusing objects, pooling small, frequently-used strings, and leveraging off-heap memory, it is possible to reduce garbage allocation to near-zero levels. This approach drastically cuts GC pauses and cache pressure, leading to consistently low latencies.

For instance, you can keep a single object instance per event type and repeatedly reuse it by overwriting its fields rather than creating a new instance each time. If you understand the lifecycle, this approach can yield latency improvements by orders of magnitude.

Enhanced Profiling and Safe Points

Profilers rely on "safe points" to capture stack traces. The JVM reduces safe points to optimise runtime performance, sometimes skewing profiling results. To address this, you can add deliberate, safe points or hints that enable more accurate profiling data. This ensures hot spots are attributed correctly, preventing misleading conclusions such as Integer.hashCode() appears as a top CPU consumer when it is merely a victim of unfortunate sampling.

Class-Based Caching and Vicarious Exceptions

For performance, decisions made per class—such as how to serialise it—should be cached. Java’s ClassValue provides this mechanism, clearing the cache automatically when classes are unloaded. For cleaner code, you can implement lambda-friendly versions of ClassValue.

Additionally, "vicarious exceptions" can bypass checked exception constraints. By carefully throwing exceptions as unchecked at runtime, you avoid layering wrappers. This approach should be handled carefully and reserved for internal code, which allows you to control both the thrower and the catcher.

Choosing double Over BigDecimal

BigDecimal is safer for precise arithmetic but can be slow and memory-intensive. For high-performance scenarios, double arithmetic is often sufficient. Although double is susceptible to rounding errors, those errors are easier to spot and correct. double-based operations are simpler, faster, and produce no additional objects. Switching to double for critical performance hotspots can be worth the trade-off.

This was covered recently in this article Overview Many Developers Consider.

Deterministic Resource Cleanup

Relying on garbage collection for resource cleanup is risky in low-latency applications. GC may run unpredictably, leaving file handles, off-heap memory regions, or sockets dangling. Consider cleaning resources when threads terminate or implement your lifecycle management routines to ensure deterministic cleanup.

For example, creating custom thread classes that proactively clean thread-locals upon termination ensures no resources remain in limbo. Though admittedly hacky, this technique helps maintain deterministic behaviour in mission-critical environments.

Lightweight Object Pools for Strings

String interning is built into Java for compile-time constants but not for dynamic strings. Manually caching and reusing commonly-occurring strings can reduce allocation churn. Using a small, lock-free caching array of strings, you can often return references to previously interned strings without the overhead of global interning or heavy hash maps.

While this technique is best for stable sets of strings, it can be combined with other no-garbage techniques to further stabilise performance under high load.

Summary of Key Points
  • Stack traces as data structures: Capturing stack traces without throwing exceptions aids post-mortem debugging.

  • Unique timestamps: System-wide unique, nanosecond-level timestamps simplify event correlation.

  • Trivially copyable objects: Structuring data layouts and using off-heap memory can yield near-direct memory copies.

  • Zero-garbage code: Minimising or eliminating allocation reduces GC jitter and improves predictability.

  • double vs BigDecimal: For performance-critical code, double often outperforms BigDecimal.

  • Deterministic cleanup: Do not rely solely on GC—clean resources proactively.

  • Object pools and caching: Strategic caching of strings or objects can dramatically reduce memory pressure.

  • Profiling awareness: Introduce safe points or hints to ensure accurate profiling.

These techniques are not always necessary for every Java application. However, in domains where latency and determinism matter—such as financial trading, real-time analytics, or IoT streaming—they can dramatically improve throughput, reduce jitter and enhance maintainability.

About the author

As the CEO of Chronicle Software, Peter Lawrey leads the development of cutting-edge, low-latency solutions trusted by 8 out of the top 11 global investment banks. With decades of experience in the financial technology sector, he specialises in delivering ultra-efficient enabling technology which empowers businesses to handle massive volumes of data with unparalleled speed and reliability. Peter’s deep technical expertise and passion for sharing knowledge have established him as a thought leader and mentor in the Java and FinTech communities. Follow Peter on BlueSky or Mastodon.

Conclusion

Core Java offers powerful primitives that can be employed unconventionally to achieve performance levels often considered out of reach for managed languages. By leveraging these approaches—carefully and with proper testing—you can build systems that run significantly faster, scale more smoothly, and give you deeper insights into their runtime behaviours. While not every application requires such extreme measures, those that do will find these techniques indispensable.

tag:blogger.com,1999:blog-1729822519392175009.post-8004709337171085258
Extensions
Trivially Copyable Objects in Java
BenchmarksOpinionPerformance
Show full content
function addLoadEvent(func) { var oldonload = window.onload; if (typeof window.onload != 'function') { window.onload = func; } else { window.onload = function() { if (oldonload) { oldonload(); } func(); } } } addLoadEvent(function() { prettyPrint(); }); .pln { color: #000 } /* plain text */ @media screen { .str { color: #080 } /* string content */ .kwd { color: #008 } /* a keyword */ .com { color: #800 } /* a comment */ .typ { color: #606 } /* a type name */ .lit { color: #066 } /* a literal value */ /* punctuation, lisp open bracket, lisp close bracket */ .pun, .opn, .clo { color: #660 } .tag { color: #008 } /* a markup tag name */ .atn { color: #606 } /* a markup attribute name */ .atv { color: #080 } /* a markup attribute value */ .dec, .var { color: #606 } /* a declaration; a variable name */ .fun { color: red } /* a function name */ } /* Use higher contrast and text-weight for printable form. */ @media print, projection { .str { color: #060 } .kwd { color: #006; font-weight: bold } .com { color: #600; font-style: italic } .typ { color: #404; font-weight: bold } .lit { color: #044 } .pun, .opn, .clo { color: #440 } .tag { color: #006; font-weight: bold } .atn { color: #404 } .atv { color: #060 } } TL;DR
  • Problem: Java’s standard serialisation can be slow due to scattered object fields and reflection-based overhead.
  • Approach: Emulate C++-style trivially copyable objects by restricting fields to primitives, enabling bulk memory copies.
  • Result: Near C++-like serialisation performance, dramatically reducing latency and improving throughput.
  • Trade-offs: Requires careful design, limited flexibility, and testing for JVM compatibility.
  • Outcome: Low-latency systems with high performance, suitable for financial data feeds, real-time analytics, and other latency-sensitive domains.
Introduction

For low-latency systems, every microsecond has tangible business impact. In high-frequency trading, real-time analytics, and similarly time-sensitive workloads, even minor inefficiencies in serialisation and deserialisation can degrade throughput and responsiveness. The seemingly mundane act of converting objects into bytes and back often becomes a performance bottleneck.

This article explores how we can emulate a C++-like concept of Trivially Copyable Objects within Java to achieve far more efficient serialisation. By ensuring objects contain only fixed-size primitives, we can sidestep the traditional overheads of object graph traversal, reflection, and per-field copying. Instead, we can treat them as contiguous memory blocks, dramatically reducing the time taken to read and write data. We shall also consider how to get very close to this performance using Chronicle Bytes without strictly requiring trivial copyability. This technique blends the low-level efficiency with the safety and familiarity of Java’s ecosystem.

The Core Challenge of Java Serialisation

Most Java object graphs are composed of references linking scattered heap allocations. Serialising such objects typically involves visiting numerous memory locations, reading each field individually, and writing them out one at a time. This is akin to foraging around a warehouse for individual items whenever you need to pack a box. Although this might be fine at moderate data rates, it does not scale well when the volume and frequency of messages skyrocket.

In latency-critical domains—such as algorithmic trading, telemetry processing, or high-throughput messaging—this overhead is costly. Minimising the time to transform data from in-memory objects to a binary format and back can make the difference between meeting a service-level agreement (SLA) and missing it. The objective: reduce the cost of serialisation to something approaching a single memcpy operation.

Emulating Trivially Copyable Objects in Java

C++ defines a Trivially Copyable Object as one that can be safely cloned by copying its memory representation directly (e.g. using memcpy). This guarantees efficient and predictable memory layouts. Java does not expose this concept directly, but we can achieve a similar result.

If an object is composed solely of primitive fields—long, int, double, etc.—with known fixed sizes, it resembles a flat, contiguous block of data. This allows us to perform a bulk copy of its contents in one go. The result is similar to how C++ handles trivially copyable types, but now we are in Java’s safe, garbage-collected environment.

Steps to Achieve a Contiguous Layout
  1. Restrict to Primitives Only: Avoid String, List, or other reference fields. Instead, store identifiers, timestamps, and short textual representations as primitive longs (using converters like @ShortText or @NanoTime) to maintain a fixed-size layout.
  2. Use Fixed-Size Fields: Ensure that all fields have a predictable size. This not only simplifies the copy but also makes the data layout predictable.
  3. Leverage Chronicle Utilities: Chronicle Bytes provides methods like unsafeReadObject() and unsafeWriteObject() to perform bulk memory operations. These bypass the overhead of per-field reflection or loops.

By structuring your objects this way, you remove the CPU overhead of repeatedly checking field offsets and types. Instead, you treat the entire object as a known layout of bytes that can be slurped up or written out in a single shot.

Not every system can fully adopt trivially copyable objects. Some may need reference fields. In such cases, you can still improve performance incrementally:

  • Explicit Serialisation Methods: By implementing custom readMarshallable() and writeMarshallable() methods, you can avoid reflection overhead. This alone can drastically reduce latency, as shown by the benchmarks below.
  • Direct Memory Access: For a moderate increase in complexity, explicitly reading and writing fields by their offsets using Chronicle Bytes lowers overhead even further. While more effort is required, this approach narrows the gap to trivial copyability.
  • Full Trivial Copying: For the absolute best performance, treat the entire object as a single contiguous memory block. Bulk copying here is the key to near C++-style efficiency.
Code Examples

Consider a MarketData DTO with many fields. A straightforward SelfDescribingMarshallable class might rely on reflection or explicit read/write methods:

abstract class MarketData extends SelfDescribingMarshallable {
    @ShortText
    long securityId;

    @NanoTime
    long time;

    int bidQty0, bidQty1, bidQty2, bidQty3;
    int askQty0, askQty1, askQty2, askQty3;

    double bidPrice0, bidPrice1, bidPrice2, bidPrice3;
    double askPrice0, askPrice1, askPrice2, askPrice3;

    // Getters and setters omitted for clarity
}

Default approaches often rely on reflection or self-describing fields. While convenient, this is not the fastest method. Explicitly coding each field read and write is faster:

public final class ExplicitMarketData extends MarketData {

    @Override
    public void readMarshallable(BytesIn bytes) {
        securityId = bytes.readLong();
        time = bytes.readLong();
        bidQty0 = bytes.readDouble();
        // ... repeated for all fields
    }

    @Override
    public void writeMarshallable(BytesOut bytes) {
        bytes.writeLong(securityId);
        bytes.writeLong(time);
        // ... repeated for all fields
    }
}

For even lower overhead, we might write a DirectMarketData class that manually calculates offsets:

public final class DirectMarketData extends MarketData {
    @Override
    public void readMarshallable(BytesIn bytes) {
        BytesStore<?, ?> bs = ((Bytes<?>) bytes).bytesStore();
        long position = bytes.readPosition();
        // generated by GitHub Copilot
        bytes.readSkip(112);
        securityId = bs.readLong(position+0);
        time = bs.readLong(position+8);
        // ... repeated for all fields
    }

    @Override
    public void writeMarshallable(BytesOut bytes) {
        BytesStore<?, ?> bs = ((Bytes<?>) bytes).bytesStore();
        long position = bytes.writePosition();

        // generated by GitHub Copilot
        bytes.writeSkip(112);
        bs.writeLong(position+0, securityId);
        bs.writeLong(position+8, time);
        // ... repeated for all fields
    }
}

Finally, a TriviallyCopyableMarketData class uses Chronicle’s unsafeReadObject() and unsafeWriteObject() methods to perform a single bulk copy:

public final class TriviallyCopyableMarketData extends MarketData {
    static final int START =
        triviallyCopyableStart(TriviallyCopyableMarketData.class);
    static final int LENGTH =
        triviallyCopyableLength(TriviallyCopyableMarketData.class);

    @Override
    public void readMarshallable(BytesIn bytes) {
        bytes.unsafeReadObject(this, START, LENGTH);
    }

    @Override
    public void writeMarshallable(BytesOut bytes) {
        bytes.unsafeWriteObject(this, START, LENGTH);
    }
}

These methods bypass iterative per-field copying. Instead, they use knowledge of the object’s layout to copy memory in one go.

The Benchmark Results

Running benchmarks on a high-end CPU (e.g. a Ryzen 5950X) shows the progressive improvements:

Benchmark                              Mode  Cnt     Score    Error  Units
BenchmarkRunner.defaultWriteRead       avgt   25  1204.359 ± 72.394  ns/op
BenchmarkRunner.defaultBytesWriteRead  avgt   25   375.479 ±  6.066  ns/op
BenchmarkRunner.explicitWriteRead      avgt   25    45.769 ±  0.661  ns/op
BenchmarkRunner.directWriteRead        avgt   25    27.303 ±  0.867  ns/op
BenchmarkRunner.trivialWriteRead       avgt   25    25.568 ±  0.228  ns/op

Here, trivialWriteRead approaches raw memory copy speeds, slashing overhead by more than an order of magnitude compared to default approaches. The directWriteRead is very close in terms of performance but isn't impacted by layout changes in the JVM.

Considerations and Caveats
  1. JVM Stability: While typically stable, relying on certain low-level assumptions may differ slightly between JVM versions or distributions. Test carefully if you need cross-JVM compatibility.
  2. Loss of Flexibility: Restricting fields to primitives means losing some convenience. Often, you can mitigate this by mapping strings or enumerations to integers, or converting short texts via @ShortText, and timestamps with @NanoTime.
  3. Schema Evolution: Changes to object structures require coordination. Both sender and receiver must remain compatible. Use versioning strategies and robust integration tests.
  4. Nearly Trivial Without Going Fully Trivial: If you cannot fully restrict yourself to primitives, consider direct copying of at least the performance-critical parts of the data, and handle the rest with explicit methods.
  5. Leverage Chronicle’s Tooling: Chronicle Bytes and Queue provide the building blocks. While they add complexity, the performance pay-off justifies it in latency-critical systems.
Key Points
  • Treating objects as contiguous blocks of primitive fields significantly reduces serialisation overhead.
  • Moving from self-describing, reflective approaches to explicit field reads/writes yields large gains.
  • Using direct memory offsets or bulk copying is yet more efficient, approaching C++-like speeds.
  • While not free of trade-offs, trivial copyability offers a compelling pattern for systems where latency and throughput trump convenience.
Try It Yourself

Why not measure the impact on your own workload? The benchmark harness is available here:

BenchmarkRunner.java on GitHub

Run it with JMH to see if trivial copyability can enhance your system’s performance. Experiment with different layouts, measure the impact, and adopt the approach incrementally.

This Article Is Based On...

This article is an update of two articles by Per Minborg How to Get C++ Speed in Java Serialisation and Did You Know the Fastest Way of Serializing a Java Field is not Serializing it at All? It builds on the original concepts and benchmarks, providing a fresh perspective on achieving ultra-low-latency Java systems.

Conclusion

Java may not natively support trivially copyable objects, but we can still achieve near C++-like serialisation speeds by restructuring data and using Chronicle’s low-level operations. By experimenting with these techniques and applying them judiciously, developers can build ultra-low-latency Java systems that confidently handle high-throughput workloads. If you have been searching for that extra edge in performance, give trivial copyability—or its direct-copy variants—a try. It might just be the key to unlocking new levels of efficiency.

Resources
tag:blogger.com,1999:blog-1729822519392175009.post-8654677014513252629
Extensions
Efficient Distributed Unique Timestamp Identifier Generation
BenchmarksMicroservicesPerformance
Show full content
pre.rouge table td { padding:5px; } pre.rouge table pre { margin:0; } pre.rouge .cm, .c1, .c, .ch, .cd, .cpf { color:#999988; font-style:italic; } pre.rouge .cp, .cs { color:#999999; font-weight:bold; } pre.rouge .cs { font-style:italic; } pre.rouge .err { color:#a61717; background-color:#e3d2d2; } pre.rouge .gd { color:#000; background-color:#ffdddd; } pre.rouge .ge { color:#000; font-style:italic; } pre.rouge .gr, .gt { color:#aa0000; } pre.rouge .gh, .bp { color:#999999; } pre.rouge .gi { color:#000; background-color:#ddffdd; } pre.rouge .go { color:#888; } pre.rouge .gp, .nn { color:#555555; } pre.rouge .gs, .kc, .kd, .kn, .kp, .kr, .k, .kv, .sa, .nc, .nd, .ne, .nf, .fm, .nl, .ow, .o { color:#000; font-weight:bold; } pre.rouge .gu { color:#aaa; } pre.rouge .mf, .mh, .il, .mi, .mo, .m, .mb, .mx { color:#009999; } pre.rouge .sb, .sc, .sd, .s2, .se, .sh, .si, .sx, .s1, .s, .dl { color:#d14; } pre.rouge .sr { color:#009926; } pre.rouge .ss { color:#990073; } pre.rouge .na, .no, .vc, .vg, .vi, .nv, .vm { color:#008080; } pre.rouge .nb { color:#0086B3; } pre.rouge .nc { color:#445588; font-weight:bold; } pre.rouge .nt { color:#000080; } pre.rouge .ni { color:#800080; } pre.rouge .w { color:#bbb; } pre.rouge { background-color:#f8f8f8; }

Distributed unique timestamp identifiers provide a powerful means of generating globally unique, human-readable 64-bit values at sub-microsecond speeds. By embedding a host identifier directly into a nanosecond-resolution timestamp, you gain a simple, chronologically sortable, and intuitive scheme for correlating events across multiple hosts. This approach offers significant benefits in latency-sensitive systems where even small delays can become expensive at scale.

Introduction

In a world of horizontally scaled microservices, ensuring that each event or message receives a unique identifier across multiple machines can be challenging. Traditional approaches often rely on UUIDs, which—while easy to use—lack intuitive readability and can be relatively expensive to generate in ultra-low-latency scenarios.

Our solution builds upon nanosecond-resolution timestamps combined with a host identifier embedded directly into the lower-order digits of the timestamp. This technique, inspired by previous work on system-wide unique nanosecond timestamps, creates identifiers that are compact, human-interpretable, and extremely fast to produce. In essence, we treat time itself as the source of uniqueness.

By carefully structuring the timestamp and assigning a unique hostId per machine (or per logical partition), we can scale to produce up to one billion unique 64-bit identifiers per second. These identifiers repeat only after centuries, making them suitable for long-running systems and distributed architectures that demand both precision and high performance.

Concurrent identifier generation in a distributed system

In distributed environments, colliding identifiers can lead to data corruption, misrouted requests, or difficulty in debugging. Although UUIDs solve uniqueness issues, they do not inherently convey temporal ordering or machine origin. More subtle forms of identifiers, such as database sequence numbers or custom counters, often need to be more convenient when synchronising across multiple hosts.

Our approach sets out to solve these issues by:

  • Guaranteeing uniqueness across multiple hosts without complex coordination.

  • Embedding a host identifier directly into the timestamp.

  • Preserving direct human readability and chronological ordering.

  • Operating efficiently under heavy concurrency with minimal contention.

Designing a Distributed Time-Based Identifier

The core concept is straightforward: take the current time with nanosecond precision and modify its least significant digits to represent a hostId. For example, if you have 100 hosts, you can reserve the last two digits of the timestamp to encode the hostId values from 00 to 99. Each machine thus derives a unique identifier from the combination of its hostId and the current time.

DistributedUniqueTimeProvider timeProvider = DistributedUniqueTimeProvider.forHostId(28);
long uniqueId = timeProvider.currentTimeNanos();
System.out.println("Distributed Unique ID: " + uniqueId);

Under the hood, DistributedUniqueTimeProvider maintains a memory-mapped file to store the last assigned timestamp. Every new ID must be strictly greater than the previous one to maintain uniqueness. Should the clock move backwards or generate the same timestamp under high contention, a loop increments until a suitable next timestamp is found.

Handling Concurrency and Contention

As multiple threads within the same JVM (or multiple JVMs on the same machine using the same hostId) call currentTimeNanos(), you might worry about contention. The approach relies on an atomic compare-and-swap (CAS) operation on a memory-mapped file (MappedFile from [Chronicle Bytes](https://github.com/OpenHFT/Chronicle-Bytes)). Each new ID attempt compares the proposed timestamp+hostId combination with the last known good value. If it’s strictly larger, it updates atomically and returns successfully.

In short: - Read the current time at nanosecond resolution. - Strip off the lower digits and re-add the hostId. - Perform a CAS to ensure this new ID is greater than the last used ID. - If it fails, spin in a loop, incrementing the time slightly, until successful.

This ensures no duplicates—even after a host restarts—assuming the downtime exceeds the largest possible timestamp overlap.

Avoiding Clock Regressions and Edge Cases

System clocks do sometimes jump backwards—often due to corrections via NTP or oscillations in virtualised environments. Since our scheme depends on strictly increasing timestamps, we must handle these cases gracefully. If currentTimeNanos() detects that the new timestamp is not greater than the last used one, it increments the timestamp until it is. While this might temporarily lead to a slight run-ahead of the actual wall clock, this discrepancy is usually negligible in systems where the real resolution and jitter of the clock already dominate precision.

Additionally, the resolution we work with is often at or around 100 nanoseconds, aligning with what typical hardware timers can realistically provide. Attempting finer granularity than the hardware can deliver often leads to redundant increments in the busy loop.

A nano-second timestamp with a host identifier

DistributedUniqueTimeProvider stores a host identifier in the lower two digits of the timestamp, making it easier to read. The previous implementation used bit shifting so the hostId could be obtained, but that was difficult for a human to read.

This allows you to produce guaranteed unique identifiers, encoded with up to 100 sources across up to 100 machines, with multiple JVMs on the same machine sharing a hostId.

The timestamp looks like this on a machine with a hostId of 28:

2021-12-28T14:07:02.954100128

Where the date/time/microseconds are the time, and the last two digits are the host identifier, making it easier to see the source in the timestamp. Human operators can quickly see when and where the identifier was produced. This clarity facilitates debugging and operational insight without sacrificing uniqueness or performance.

The resolution is approximately one-tenth of a microsecond (100 nanoseconds), which often matches or exceeds the practical resolution of the underlying clock hardware. This approach, therefore, leverages the natural granularity of the system clock without adding artificial delays.

Avoiding Clock Regressions and Edge Cases

System clocks do sometimes jump backwards—often due to corrections via NTP or oscillations in virtualised environments. Since our scheme depends on strictly increasing timestamps, we must handle these cases gracefully. If currentTimeNanos() detects that the new timestamp is not greater than the last used one, it increments the timestamp until it is. While this might temporarily lead to a slight run-ahead of the actual wall clock, this discrepancy is usually negligible in systems where the real resolution and jitter of the clock already dominate precision.

Additionally, the resolution we work with is often at or around 100 nanoseconds, aligning with what typical hardware timers can realistically provide. Attempting finer granularity than the hardware can deliver often leads to redundant increments in the busy loop. == Configuring the Host Identifier

You can assign a hostId either through the command line system property:

-DhostId=xx

or programmatically by calling:

DistributedUniqueTimeProvider.INSTANCE.hostId(hostId);

Speeding up the assignment with a host identifier

Having a preconfigured host identifier and keeping track of the most recent identifier in shared memory allows fast concurrent generation of identifiers across machines.

Up to the theoretical limit of one billion per second.

The happy path is simple: take the current time, remove the lower two digits and add the hostId. As long as this is higher than the last identifier, it’s okay. Should the machine fail and the information as to the last identifier be lost, the assumption is that the time taken to restart the service is enough time to ensure there is no overlap. If the service fails, but not the machine, the information is retained.

mermaid.initialize({ startOnLoad: true });
flowchart LR

%% Define subgraph for Host 1
subgraph "Host 1"
    direction LR
    host1_id("hostId = 01")
    current_time1("currentTimeNanos()")
    unique_id1("Unique ID
e.g. ...101") host1_id --> current_time1 current_time1 --> unique_id1 end %% Define subgraph for Host 0 subgraph "Host 0" direction LR host0_id("hostId = 00") current_time0("currentTimeNanos()") unique_id0("Unique ID
e.g. ...100") host0_id --> current_time0 current_time0 --> unique_id0 end %% Define the Mapped Files mapped_file1[("Mapped File 1
hostId = 01
...
LAST_TIME")] mapped_file0[("Mapped File 0
hostId = 00
...
LAST_TIME")] %% Edges for Compare-and-Swap operations unique_id1 -- "Compare-and-swap
operation updates
memory-mapped file" --> mapped_file1 unique_id0 -- "Compare-and-swap
operation updates
memory-mapped file" --> mapped_file0

This diagram shows two separate hosts, each maintaining its memory-mapped file. Every time a host generates a new timestamp-based identifier, it updates the stored 'LAST_TIME' in its file using atomic operations. This ensures that the host will not generate a duplicate identifier even after a restart, preserving uniqueness across the entire distributed system.

Note This uses the MappedFile in shared memory supported by Chronicle Bytes, an open-source library.
@Override
public long currentTimeNanos() {
    long time = provider.currentTimeNanos();
    long lastTime = bytes.readVolatileLong(LAST_TIME);
    long next = time - time % HOST_IDS + hostId;

    if (next > lastTime && bytes.compareAndSwapLong(LAST_TIME, lastTime, next)) {
        return next;
    }
    return currentTimeNanosLoop();
}

If the time hasn’t progressed, either due to high contention or the wall clock going backwards (e.g. due to a correction), a loop is called to find the next available identifier.

private long currentTimeNanosLoop() {
    while (true) {
        long time0 = bytes.readVolatileLong(LAST_TIME);
        long next = time0 - time0 % HOST_IDS + hostId;
        if (next <= time0) {
            next += HOST_IDS;
        }
        if (bytes.compareAndSwapLong(LAST_TIME, time0, next)) {
            return next;
        }
        Jvm.nanoPause(); // Introduces a tiny pause to reduce contention spinning.
    }
}

This loop looks for the next possible timestamp (with the hostId) and attempts to update it.

Using JMH to benchmark the timestamp provider

With JMH, benchmarking this utility in a single-threaded manner is pretty easy.

@State(Scope.Benchmark)
public class DistributedUniqueTimeProviderBenchmark {
    private DistributedUniqueTimeProvider timeProvider;

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(DistributedUniqueTimeProviderBenchmark.class.getSimpleName())
                .warmupIterations(3)
                .measurementIterations(5)
                .measurementTime(TimeValue.seconds(5))
                .forks(5)
                .build();

        new Runner(opt).run();
    }

    @Setup
    public void setUp() {
        timeProvider = DistributedUniqueTimeProvider.forHostId(1);
    }

    @TearDown
    public void tearDown() {
        timeProvider.close();
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    public long currentTimeNanos() {
        return timeProvider.currentTimeNanos();
    }
}

After less than five minutes, we get the following result on my Windows laptop. You can get better results on a high-end server or desktop. The average time is around 37.4 nanoseconds. While this is single-threaded, this is generally on the unhappy path, as timestamps need to be at least 100 ns apart or they temporarily run ahead of the wall clock.

UUID.randomUUID() is also very fast, only about six times longer. However, if you need a timestamp and a source identifier for your event anyway, this avoids additional work or data.

Benchmarking with JMH in a single-threaded context showed that obtaining a unique timestamp takes approximately 37.4 nanoseconds on average. In comparison, UUID.randomUUID() is about six times slower. On an i9-10980HK processor, the benchmark results were:

Benchmark Mode Count Score Error Units

DistributedUniqueTimeProviderBenchmark.currentTimeNanos

avgt

25

37.395

±0.391

ns/op

DistributedUniqueTimeProviderBenchmark.randomUUID

avgt

25

207.709

±1.586

ns/op

On a Ryzen 9 5950X processor, the results were:

Benchmark Mode Count Score Error Units

DistributedUniqueTimeProviderBenchmark.currentTimeNanos

avgt

25

43.557

±0.801

ns/op

DistributedUniqueTimeProviderBenchmark.randomUUID

avgt

25

265.285

±2.690

ns/op

Downsides

There are some advantages to using UUIDs:

  • It’s built-in and the extra overhead might not be a concern.

  • No configuration is required.

  • They are not predictable, while the timestamp-based ones are highly predictable.

Try It Yourself

Consider integrating the DistributedUniqueTimeProvider into a logging framework or event pipeline. By injecting host-based timestamps, you can more easily correlate events across multiple machines in real time:

  • Run a local benchmark with JMH to measure performance on your hardware.

  • Experiment with different hostId allocations to confirm uniqueness and ordering.

  • Integrate into a distributed queue or event-processing system to verify end-to-end latency improvements.

About the Author

As the CEO of Chronicle Software, Peter Lawrey leads the development of cutting-edge, low-latency solutions trusted by 8 out of the top 11 global investment banks. With decades of experience in the financial technology sector, he specialises in delivering ultra-efficient enabling technology which empowers businesses to handle massive volumes of data with unparalleled speed and reliability. Peter’s deep technical expertise and passion for sharing knowledge have established him as a thought leader and mentor in the Java and FinTech communities. Follow Peter on BlueSky or Mastodon.

Conclusion

If you can use some predetermined partitioning by host identifier, you can have an 8-byte lightweight identifier that is unique across many hosts. The identifier is still easily readable as text in a slightly modified form of a timestamp.

By embedding host identifiers into nanosecond-level timestamps, developers gain a simple and effective mechanism for generating globally unique, chronologically sortable identifiers. This efficient and intuitive approach makes it particularly suitable for high-performance distributed systems.

Key Takeaways
  • Guaranteed uniqueness across distributed hosts by embedding a hostId into timestamps.

  • Readable identifiers enabling quick debugging and event correlation.

  • High performance with minimal overhead, measurable in tens of nanoseconds.

  • Ease of use, achievable with a few lines of code and minimal configuration.

Time-based uniqueness is a natural solution. When correctly implemented, it empowers developers to maintain a transparent and scalable view into their distributed systems, merging the logical flow of time with the practical need for global uniqueness.

tag:blogger.com,1999:blog-1729822519392175009.post-6984446419988443174
Extensions
A Java Conversion Puzzler: Understanding Implicit Casting and Overflow
ExerciseInfoPuzzles
Show full content

This article explores a subtle Java conversion puzzle that challenges assumptions about how arithmetic operations, implicit casting, and floating-point conversions interact. Inspired by complexities often encountered in low-latency and high-performance environments, it demonstrates why a keen understanding of Java’s type system is essential for building reliable and efficient applications.

Introduction

The following example demonstrates a scenario where an innocuous-looking arithmetic operation leads to a surprising result. While such questions are rare and arguably impractical, they highlight subtle behaviours that can affect correctness and performance, especially in critical systems like high-frequency trading platforms or complex data-processing pipelines.

The Problem: A Surprising Print Statement

Consider the following code:

int i = Integer.MAX_VALUE;
i += 0.0f;
int j = i;
System.out.println(j == Integer.MAX_VALUE); // true

At first glance, one might assume that adding 0.0f to an int should not change its value. Indeed, the output true reinforces this notion. However, if you change int i for long i, things get weird:

long i = Integer.MAX_VALUE; // only the type of i is changed
i += 0.0f;
int j = (int) i;
System.out.println(j == Integer.MAX_VALUE); // false
System.out.println(j == Integer.MIN_VALUE); // true

What is going on, you might wonder? Let me start by explaining why using a long gives such a strange result.

Understanding the Implicit Casting

The key detail lies in how Java handles the += operator. It is not strictly equivalent to a = a + b; but rather:

a += b;

has a subtle difference which most of the time doesn't matter:

// has an implicity cast here
a = (typeOf(a)) (a + b);

Another subtle feature of addition is that the result is the "wider" of the two types. This means that:

i += 0.0f;

is actually:

i = (int) ((float) i + 0.0f);
// or
i = (long) ((float) i + 0.0f);

The result of (float) i can be imprecise due to floating-point rounding. A float has a 24-bit mantissa, so very large integers cannot be represented precisely. This lack of precision means casting Integer.MAX_VALUE to a float may not return the exact same number.

When you cast Integer.MAX_VALUE to a float you get a rounding error (as float has a mantissa of 24-bits) resulting in the value being one more than what you started with. i.e. it is the same as:

i = Integer.MAX_VALUE + 1L; // for long i

When you cast Integer.MAX_VALUE + 1L to an int again, you get an overflow and you have Integer.MIN_VALUE:

j = Integer.MIN_VALUE;

So why is it that a long gets the unexpected value, while an int happens to get the expected value?

The reason is that when rounding from floating point to an integer it rounds down to the nearest representable value. Thus:

int k = (int) Float.MAX_VALUE; // k = Integer.MAX_VALUE;
int x = (int) (Integer.MAX_VALUE + 1.0f); // x = Integer.MAX_VALUE;

Note: Float.MAX_VALUE / Integer.MAX_VALUE is 1.5845632E29 which is a huge error, but that’s the best an int can do.

In short, for an int value Integer.MAX_VALUE, the statement i += 0.0f; causes the value to jump up one (casting to a float) and then down one (casting back to an int), so you end up with the value you started with.

Another Example: char Division

Consider the following snippet:

char ch = '0';
ch /= 0.9;
System.out.println(ch); // prints 5

At first glance, this might look perplexing. The character '0' has an ASCII code of 48. Dividing 48 by 0.9 yields approximately 53.3333. When performing a compound assignment like ch /= 0.9, Java promotes the right-hand side to a float, does the division, and then implicitly casts the result back to a char. This truncates the value to an integer, resulting in 53, which corresponds to the character '5'. Thus, the code prints 5.

This example further highlights how compound assignments and type promotions can produce unexpected conversions, especially when mixing integer and floating-point arithmetic.

Try It Yourself

For those curious to explore this behaviour firsthand, run the provided code snippets on your local machine. Experiment by using different numeric types (short, byte, double) and observe how Java’s type casting rules manifest themselves. Tools like JMH (Java Microbenchmark Harness) can help you measure if any performance overhead arises from unexpected type conversions.

About the author

As the CEO of Chronicle Software, Peter Lawrey leads the development of cutting-edge, low-latency solutions trusted by 8 out of the top 11 global investment banks. With decades of experience in the financial technology sector, he specialises in delivering ultra-efficient enabling technology which empowers businesses to handle massive volumes of data with unparalleled speed and reliability. Peter's deep technical expertise and passion for sharing knowledge have established him as a thought leader and mentor in the Java and FinTech communities. Follow Peter on BlueSky or Mastodon.

Summary (Key Points)
  • Implicit Casting in +=: a += b; is not always the same as a = a + b;—there is a hidden cast to the type of a.
  • Floating-Point Imprecision: Large integer values may not be exactly representable as floats.
  • Overflow on Recast: Converting Integer.MAX_VALUE via a float round-trip can increment it, leading to overflow when cast back to an int.
  • Practical Advice: When reliability and performance matter, avoid mixing numeric types casually. Make conversions explicit and verify assumptions.
  • Try It Yourself: Experiment with different types and conversions to deepen your understanding.
Further Reading
tag:blogger.com,1999:blog-1729822519392175009.post-2218185504317785282
Extensions
Why Does Math.round(0.49999999999999994) Round to 1?
ExerciseInfoPuzzles
Show full content
1. Defining the Problem

In many numerical computations, one would reasonably expect that rounding 0.499999999999999917 should yield 0, since it appears to be slightly less than 0.5. Yet, in Java 6, calling Math.round() on this value returns 1, a result that may initially seem baffling. This seemingly minor discrepancy stems from the interplay of binary floating-point representation, rounding modes, and the particular internal implementation details of Math.round() in earlier Java releases.

For professionals in performance-sensitive environments—such as those working in financial technology or high-precision scientific applications—understanding these subtleties is more than just an academic exercise. Even tiny rounding differences can influence trading algorithms, pricing models, or simulations. Moreover, developers and enthusiasts who appreciate the low-level mechanics behind Java’s numeric types will find valuable insights into how these internal workings affect everyday programming tasks.

This article delves into why this unexpected rounding occurs, sheds light on the constraints of double-precision arithmetic, and contrasts the behaviour in Java 6 against newer versions like Java 7. Consider, for instance, the closely related question: Why does Math.round(0.49999999999999994) return 1 rather than 0? Although it might initially seem like a bug, it is, in fact, a predictable outcome once we acknowledge the inherent imprecision of floating-point arithmetic. By the end, you will have a clearer understanding of why these rounding anomalies happen, and how to avoid or mitigate their effects in your own code.

2. The IEEE 754 64-bit Double-Precision Format Component Bit Count Interpretation

Sign

1

Determines the sign of the number:
0 indicates a positive value, 1 indicates a negative value.

Exponent

11

Encodes the exponent using a bias of 1023.
The stored value E is interpreted as E - 1023 for the actual exponent.

Mantissa (Fraction)

52

Represents the significand (fractional part) of the number.
A hidden leading 1 is assumed for normalised values,
providing a 53-bit precision in total.

Floating-point numbers in Java (as in most modern programming languages) conform to the IEEE 754 standard. This standard defines how numbers are represented as binary fractions and how arithmetic is performed on them. Due to limited precision, not all decimal values have an exact binary representation. Some, like 0.5, convert perfectly into binary fractions (1/2), while others—such as 0.1—cannot be represented precisely.

3. Representation Errors in Binary Floating-Point

Unlike decimal, binary floating-point cannot precisely represent every decimal fraction. Some values, like 0.5, are straightforward (2^-1), while others (e.g. 0.1) cannot be represented exactly. Instead, they are approximated as a sum of negative powers of two.

When converting a decimal literal into a double, Java picks the closest representable binary floating-point number. This is generally invisible to the developer, but it can be observed by converting a double to a BigDecimal constructed directly from its binary representation:

Source
var bd = new BigDecimal(0.1);
// 0.1000000000000000055511151231257827021181583404541015625

var bd = BigDecimal.valueOf(0.1);
// "Expected" representation after rounding: 0.1

In other words, new BigDecimal(0.1) reveals the true underlying stored value, whereas BigDecimal.valueOf(0.1) aligns with what developers typically see when printing a double.

This inherent limitation leads to tiny errors—often referred to as "floating-point errors" in representation or in calculations. Over time, these small discrepancies can accumulate, influencing how numbers round and compare.

4. Why 0.49999999999999994 Appears as 0.5

The key to understanding why Math.round(0.49999999999999994) ends up producing 1 lies in the binary approximation of that decimal value. When the decimal 0.49999999999999994 is converted into a double, it does not remain exactly that number. Instead, it shifts slightly above 0.5 due to binary precision constraints.

Once your value is represented as just over 0.5, Math.round applies its standard rule: it rounds half and above to the next whole number. Thus, the result is 1.

Consider the following snippet:

Source
public class FloatingPointExample {
    public static void main(String[] args) {
        double value = 0.49999999999999994;
        System.out.println("Value: " + value);
        System.out.println("Rounded: " + Math.round(value));
    }
}

Expected output:

Value: 0.5
Rounded: 1

The printed value is displayed as 0.5, reflecting its final stored approximation. Consequently, Math.round treats it as 0.5 and rounds it up.

5. Exploring the Thresholds with a Brute-Force Search

To illustrate the complexity, consider the following code which brute-forces the smallest value that rounds up to 1.0. This approach helps us pinpoint the exact binary representation at which Math.round() tips from returning 0 to returning 1.

Source
package blog.vanillajava.fp;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class FindRoundingBoundary {
    public static final BigDecimal TWO = BigDecimal.valueOf(2);

    public static void main(String... args) {
        int digits = 80; // High precision to capture tiny differences
        BigDecimal low = BigDecimal.ZERO;
        BigDecimal high = BigDecimal.ONE;

        for (int i = 0; i <= 10 * digits / 3; i++) {
            BigDecimal mid = low.add(high)
                    .divide(TWO, digits, RoundingMode.HALF_UP);
            if (mid.equals(low) || mid.equals(high))
                break;
            if (Math.round(Double.parseDouble(mid.toString())) > 0)
                high = mid;
            else
                low = mid;
        }

        System.out.println("Math.round(" + low
                + ", as double " + low.doubleValue()
                + " or " + Double.toHexString(low.doubleValue()) + ") = "
                + Math.round(Double.parseDouble(low.toString())));
        System.out.println("Math.round(" + high
                + ", as double " + high.doubleValue()
                + " or " + Double.toHexString(high.doubleValue()) + ") = "
                + Math.round(Double.parseDouble(high.toString())));
    }
}
6. Using Math.ulp() to Identify Critical Boundaries

A more practical way to pinpoint representable boundaries around key values like 0.5 is to use Math.ulp(). The Math.ulp(x) function returns the size of the unit in the last place (ULP) of the argument x. In other words, it tells you the spacing between floating-point numbers at the scale of x. By subtracting this ULP from 0.5, you can determine the largest representable double less than 0.5.

For example, consider the following code snippet:

Source
double half = 0.5;
double ulpOfHalf = Math.ulp(half);
double largestBeforeHalf = half - ulpOfHalf;

System.out.println("ULP of 0.5: " + ulpOfHalf);
System.out.println("Largest representable double less than 0.5: " + largestBeforeHalf);

Running this code reveals the exact binary boundary below 0.5. Once identified, this value can help you understand and predict rounding outcomes more reliably, particularly around delicate edge cases where floating-point errors begin to influence rounding decisions.

7. Results in Different Java Versions

Running the above code in Java 7 yields something like:

Math.round(0.49999999999999997224442438437108648940920829772949218749999999999999999999999999) = 0
Math.round(0.49999999999999997224442438437108648940920829772949218750000000000000000000000000) = 1

In Java 6, the critical values differ slightly:

Math.round(0.49999999999999991673327315311325946822762489318847656250000000000000000000000000) = 0
Math.round(0.49999999999999991673327315311325946822762489318847656250000000000000000000000001) = 1

The key point is that the threshold at which rounding flips differs between Java versions due to changes in how Math.round() is implemented and how the JDK interprets certain floating-point constants.

8. Why Java 6 and Java 7 Differ

In Java 6, Math.round(double) is effectively defined as:

Source
public static long round(double a) {
    return (long)Math.floor(a + 0.5d);
}

Because floating-point arithmetic is not exact, adding 0.5 can push a value like 0.49999999999999994 over the edge to 1.0. In contrast, Java 7 introduced a special case for the largest double less than 0.5. By explicitly checking this boundary, Java 7 ensures that ties round correctly, preserving the intuitive behaviour most developers expect.

8.1. Java 7’s Special Case

The updated code hardcodes a check:

Source
public static long round(double a) {
    // Check if 'a' is the largest double < 0.5
    if (a != 0x1.fffffffffffffp-2)
        return (long) Math.floor(a + 0.5d);
    else
        return 0;
}

Here, 0x1.fffffffffffffp-2 is the hexadecimal floating-point literal for the greatest double value less than 0.5. Using such a representation ensures an exact binary comparison without introducing unintended rounding errors.

8.2. Java 8's Implementation

Java 8's implementation is far more complex as it avoids a floating point calculation and is too long to include here. It is available on Githib

9. Practical Implications for Developers

When writing Java code that involves floating-point arithmetic, keep these considerations in mind:

  1. Precision is Limited: Even seemingly simple decimal values might not be represented exactly.
  2. Beware of Direct Comparisons: Checking if value == 0.5 may yield unexpected results. Consider using a tolerance-based comparison (Math.abs(value - 0.5) < epsilon) where epsilon is a small threshold.
  3. When Exactness Matters, Use BigDecimal: For financial calculations or any scenario requiring precise decimal arithmetic, BigDecimal is often a better choice. This class avoids binary rounding issues by representing numbers as arbitrary-precision decimal values, albeit with a performance cost.
10. Try It Yourself

Consider experimenting with different boundary values in your environment. For instance:

  • Adjust the precision in the brute-force search code and note how the smallest rounding boundary changes.
  • Run the code on various Java versions (e.g., Java 6, 7, 8, 11, 17, or 21) and compare outputs.
  • Explore using BigDecimal or Decimal64 (a fast decimal-based numeric type) for sensitive computations.

This hands-on approach fosters a deeper understanding of numeric representations and encourages the development of robust, well-tested code.

11. About the author

As the CEO of Chronicle Software, Peter Lawrey leads the development of cutting-edge, low-latency solutions trusted by 8 out of the top 11 global investment banks. With decades of experience in the financial technology sector, he specialises in delivering ultra-efficient enabling technology which empowers businesses to handle massive volumes of data with unparalleled speed and reliability. Peter's deep technical expertise and passion for sharing knowledge have established him as a thought leader and mentor in the Java and FinTech communities. Follow Peter on BlueSky or Mastodon.

12. Conclusion

While the difference between 0.499999999999999917 and 0.5 might seem trivial, it highlights the complexity and subtlety of floating-point arithmetic. By examining the binary representation of decimal values, understanding how Math.round() has evolved, and considering performance implications, developers can write more predictable and robust code.

In practice, these nuances rarely cause significant issues in modern Java versions. Nonetheless, for performance-critical or financially sensitive applications, it’s worth remembering that not all numeric values are as straightforward as they appear.

Armed with this knowledge, you can make informed decisions about when to rely on floating-point arithmetic, when to compare values with a tolerance, and when to consider alternative representations like BigDecimal.

For further reading, consider:

tag:blogger.com,1999:blog-1729822519392175009.post-138287766118908285
Extensions
TLDR: Designing Hyper-Deterministic, High-Frequency Trading Systems
MicroservicesOpinionPerformance
Show full content

Peter Lawrey is the CEO of Chronicle Software, which counts multiple Tier 1 banks among its clients. He is a Java Champion who has provided the highest number of Java and JVM-related answers on stackoverflow.com. He also architected low-latency Java trading libraries downloaded 13 million times in October 2024.

In this video, Peter examines how trading systems are designed to support microsecond-latency microservices and how these can be combined to construct complex trading solutions such as Order Management Systems (OMS), pricers, and hedging tools.

This presentation was recorded at QCon Shanghai 2019. You can watch the video by following this Link the video or read a summary below.

Introduction

Building a hyper-deterministic high-frequency trading (HFT) platform requires careful attention to detail. Every microservice, data structure, and line of code must be optimised for both performance and predictability. This article explores practical approaches and techniques—drawn from real-world financial systems—to design and implement Java-based trading microservices that deliver sub-millisecond, and often even sub-microsecond, latencies.

We will examine the importance of doing less work per microservice, discuss the critical role of deterministic behaviour, highlight the value of event-sourced architectures, and show how to test, tune, and debug such systems with confidence. Throughout, we focus on British English usage, a concise writing style, and a practical tone inspired by the Vanilla Java blog posts cited above.

Defining the Challenge

In modern trading environments, participants compete on speed and consistency. Latency is no longer measured in milliseconds; nanoseconds matter. The key problem: how can we design microservices that handle complex workflows—such as order handling or price aggregation—while keeping latencies consistently low, even at the tail end of distributions (e.g. worst 1 in 1000 events)?

Why does this matter? Because in HFT, the difference between 20 microseconds and 1 millisecond can mean the difference between capitalising on a market opportunity and missing it entirely. Low and predictable latency directly correlates with profit, risk mitigation, and a firm’s competitiveness.

Less Work, More Speed

One guiding principle is that microservices should do as little as possible. The less processing they perform, the faster and more predictable they become. In one real-world example, three chained microservices—executing an order management scenario and persisting six messages end-to-end—achieved a worst-case 99.9% latency under 20 microseconds. We have seen under 1 microsecond from message ingest to persisted output in certain simple persistence-only cases.

The lesson is clear: minimise complexity. For instance, if simply transferring a message between two processes can be done in under 1 microsecond, any additional overhead—unnecessary logging, complex calculations, blocking I/O—quickly erodes performance.

Focus on Determinism

Predictability is as important as raw speed. When optimising the worst one-in-a-thousand or one-in-ten-thousand events, every source of jitter matters. Even small utilisation increases cause queue build-ups and unpredictable delays. Designing for 1% CPU utilisation ensures that occasional bursts do not create cascading latency spikes.

Caching, a common optimisation in less time-sensitive domains, rarely helps when targeting worst-case latencies. A cache hit might be fast most of the time, but a cache miss can be catastrophic for that one-in-a-thousand event. Minimising dependency on external resources—like databases—and avoiding reliance on caches during critical low-latency paths ensures that performance remains stable under pressure.

Understanding Latency Sources
  • Network cabling and topology can add microseconds of delay.
  • Operating system and network card overhead can introduce jitter.
  • Message queues with millisecond-level latencies (e.g. Kafka) are impractical for sub-100-microsecond targets.

Choosing kernel-bypass networking, carefully selecting messaging layers, and placing core business logic on a single powerful server makes it possible to achieve extraordinary consistency and speed. For deployments that must run in the cloud or across multiple servers, these optimisations still ensure the software’s overhead is minimal, leaving the deployment architecture as the main constraint.

Avoiding External Dependencies

The fastest and most deterministic systems rarely perform external lookups mid-flight. Querying a database during a trade decision introduces millisecond-scale delays—far too slow if you are aiming for tens of microseconds. Instead, maintain all required state in-memory and use event sourcing as the golden source of truth.

In an event-sourced system, inputs are recorded sequentially and can be replayed to rebuild state deterministically. This approach ensures that if a failure occurs, or if engineers need to debug a scenario that only appears after a million events, they can simply replay the input stream. By doing this, they always recreate the exact same state and outcomes.

Managing Garbage Collection and Memory Pressure

Java’s garbage collector (GC) can introduce unpredictable pause times. Minimising object creation is crucial. Excess object churn pollutes CPU caches and increases memory pressure, degrading worst-case latency before a collection pause occurs.

However, achieving zero-garbage code everywhere can take time and effort. Instead, aim for extremely low churn under a gigabyte of allocated objects per hour. With such modest garbage production, a system can run for hours or even a full trading day without triggering a significant GC pause. Strategic trade-offs, such as using primitive types and efficient data structures, strike a balance between maintainability and performance.

Time as an Input, Not a Global Call

In deterministic architectures, time is treated as an input rather than read directly from System.currentTimeMillis() or System.nanoTime(). Recording time events in your input stream means that when you replay the system, you always see the same "time" for each event. This enables perfect reproducibility and easier debugging, as time-dependent logic no longer introduces variability.

Structuring Microservices as Pure Functions

Consider designing core microservices as pure functions driven entirely by their input streams. The state is merely a cache derived from these inputs. If the cache is lost, it can be rebuilt from the event log. This technique encourages deterministic behaviour and testability. You do not need a running database, external services, or complex test harnesses. You can reconstruct scenarios simply by replaying input messages.

Separating normalisation, core logic, and output formatting into distinct components, each can be tested in isolation. Developers can confidently refactor code, knowing that replaying recorded inputs will produce identical results. This shortens the feedback loop, enhances reliability, and reduces development costs.

Example: Efficiency Trade-Offs in Arithmetic Operations

Consider the performance difference between using double and BigDecimal for arithmetic operations. While BigDecimal avoids floating-point rounding issues, it creates more objects and complexity, which can inflate worst-case latency. Even a seemingly simple BigDecimal calculation might burn your entire latency budget under stress conditions.

For example, a JMH benchmark might show a double operation completing in ~0.05 microseconds, while BigDecimal might take five times longer on average. The outliers matter most: the worst 1 in 1000 BigDecimal operations might hit tens of microseconds, undermining your latency targets. If deterministic ultra-low latency is paramount, consider representing monetary values as scaled long integers instead.

Example Code Snippet

Here’s a simplified JMH snippet illustrating the cost difference between double and BigDecimal arithmetic:


@Benchmark
public double doubleArithmetic() {
    double value = 123.45;
    for (int i = 0; i < 100; i++) {
        value += i * 0.01;
    }
    // round the result to 2 decimal places
    return Maths.round(value, 2);
}

@Benchmark
public BigDecimal bigDecimalArithmetic() {
    BigDecimal value = BigDecimal.valueOf(123.45);
    BigDecimal increment = BigDecimal.valueOf(0.01);
    for (int i = 0; i < 100; i++) {
        value = value.add(increment.multiply(BigDecimal.valueOf(i)));
    }
    return value;
}

While BigDecimal ensures perfect rounding, its worst-case latency can be significantly higher.

Testing and Debugging at Scale

With deterministic microservices, tests become simpler. Each test can supply a known input sequence and compare the output events against an expected result file. When logic changes, developers can easily update these baseline results. Complex scenarios involving millions of events become manageable, as you can confidently replay and verify behaviour after fixes or enhancements.

The ability to "regress all tests at once" and confirm every change yields the expected new output makes large-scale refactoring far more tractable. Instead of painstakingly adjusting hundreds of tests by hand, you can adjust them in minutes by regenerating expected outputs, reviewing changes, and committing them once verified.

Real-World Impact

Such an approach can yield enormous returns. One Tier 1 bank replaced its FX trading core with a deterministic, low-latency system and recouped the entire project cost within three months due to improved trading efficiency.

These systems also make scaling simpler. While colocating processes achieve the best latency on a single high-performance server, the underlying code remains efficient if deployed across multiple machines or in the cloud. The minimal overhead introduced by carefully chosen data structures, message formats, and network configurations ensures performance remains as high as possible within given constraints.

Key Takeaways
  • Do less to go faster: Focus microservices on minimal necessary work.
  • Determinism is king: Remove jitter sources, treat time as input, and use event sourcing.
  • Minimal external dependencies: Avoid databases and complex caches on critical paths.
  • Manage memory diligently: Reduce object churn to lower GC impact and ensure stable latencies.
  • Replay for debugging: Event sourcing and pure functions simplify reproducing complex scenarios.
  • Simple tests at scale: Automated comparison of entire input-output sequences makes refactoring painless.

By adhering to these principles and continuously refining code and architecture, developers can build hyper-deterministic systems that support sophisticated trading strategies, scale with demand, and adapt swiftly to evolving markets, all while maintaining ultra-low and stable latencies.

tag:blogger.com,1999:blog-1729822519392175009.post-8936402889441285810
Extensions
Performance Tip: Specify Collection Capacity When Size is Known
InfoOpinionPerformance
Show full content

When working with Java collections, their ability to grow dynamically is often valuable. Yet, if you already know the required size, specifying the initial capacity can be more efficient. Doing so may reduce CPU overhead and memory churn, resulting in smoother performance. In this article, we will explore why specifying capacity is beneficial, present practical examples, and highlight when you might consider alternatives such as immutable or fixed-size lists.

Efficient Use of ArrayList

Many developers rely on collections like ArrayList to handle dynamic workloads. However, frequent resizing can be costly. Each resizing operation may involve allocating a new underlying array and copying existing elements, which consumes CPU cycles and memory bandwidth. If you know how many elements you need, why not avoid these unnecessary steps?

When the final size of the list is known at the outset, setting the initial capacity can signal intent to future maintainers.

A Practical Example: Optimising ArrayList Usage

Consider the following example from java.lang.invoke.ClassSpecializer.Factory in Java 23. The method collects information from a list of methods:

// Tear apart transformMethods to get the names, types, and modifiers.
List<String> tns = new ArrayList<>();
List<MethodType> tts = new ArrayList<>();
List<Integer> tms = new ArrayList<>();
for (int i = 0; i < transformMethods.size(); i++) {
    MemberName tm = transformMethods.get(i);
    tns.add(tm.getName());
    final MethodType tt = tm.getMethodType();
    tts.add(tt);
    tms.add(tm.getModifiers());
}
TRANSFORM_NAMES = List.of(tns.toArray(new String[0]));
TRANSFORM_TYPES = List.of(tts.toArray(new MethodType[0]));
TRANSFORM_MODS = List.of(tms.toArray(new Integer[0]));

In this scenario, ArrayList expands as elements are appended. Since the size is known in advance, we can set the initial capacity and avoid repeated resizing:

int transformMethodCount = transformMethods.size();
List<String> tns = new ArrayList<>(transformMethodCount);
List<MethodType> tts = new ArrayList<>(transformMethodCount);
List<Integer> tms = new ArrayList<>(transformMethodCount);

for (int i = 0; i < transformMethodCount; i++) {
    MemberName tm = transformMethods.get(i);
    tns.add(tm.getName());
    tts.add(tm.getMethodType());
    tms.add(tm.getModifiers());
}

This approach reduces overhead. Yet, we can push it even further. If the end goal is merely to create arrays for List.of, we need not rely on ArrayList at all:

int transformMethodCount = transformMethods.size();

String[] methodNames = new String[transformMethodCount];
MethodType[] methodTypes = new MethodType[transformMethodCount];
Integer[] methodModifiers = new Integer[transformMethodCount];

for (int index = 0; index < transformMethodCount; index++) {
    MemberName transformMethod = transformMethods.get(index);
    methodNames[index] = transformMethod.getName();
    methodTypes[index] = transformMethod.getMethodType();
    methodModifiers[index] = transformMethod.getModifiers();
}

TRANSFORM_NAMES = List.of(methodNames);
TRANSFORM_TYPES = List.of(methodTypes);
TRANSFORM_MODS = List.of(methodModifiers);

By avoiding unnecessary intermediate ArrayList allocations, we streamline the code and improve performance.

Not All ArrayList Are the Same

There is another way to produce a list from an array, using Arrays.asList. However, this method returns a fixed-size list backed by the original array. This can be useful when the list is read-only, but it is not suitable for all scenarios.

// From java.util.Arrays
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable

This returned list is not a java.util.ArrayList but a specialised Arrays$ArrayList. It cannot change in size, which may be advantageous for certain read-only operations. However, it might not suit scenarios requiring mutability or dynamic growth.

Why Not Just Use List.of?

List.of is often preferred for fixed-size, immutable lists. It clearly expresses immutability and can simplify code. However, immutability may conflict with existing usage patterns, especially in legacy code that expects a mutable collection.

Example: Preserving Mutable Behaviour in ProcessBuilder

The constructor for java.lang.ProcessBuilder demonstrates an intentional use of mutable lists:

public ProcessBuilder(String... command) {
    this.command = new ArrayList<>(command.length);
    for (String arg : command) {
        this.command.add(arg);
    }
}

This approach is mutable, allowing developers (though not recommended) to modify the command list after instantiation:

ProcessBuilder pb = new ProcessBuilder("process", "-arg1");
// While this is possible, it is not recommended.
pb.command().add("-arg2");

If we replaced this with:

public ProcessBuilder(String... command) {
    this.command = List.of(command);
}

We would produce an immutable list, changing the existing behaviour and potentially breaking backward compatibility. In this case, mutability aligns with established usage expectations.

Performance and Practical Trade-offs

Specifying capacity can yield measurable performance improvements. Although these gains may be modest, they become meaningful in high-throughput, latency-sensitive systems. Eliminating unnecessary copies, reducing allocation churn, and embracing immutability—or preserving mutability when needed—are all part of crafting a more efficient and flexible codebase.

Key Takeaways
  • Specify the initial capacity for collections when you know the final size. This can reduce overhead and make your intentions clearer.
  • Use List.of for immutable lists where it aligns with the API’s needs, but be mindful of backward compatibility.
  • Consider whether you need a dynamically growing structure. If a known-size array suffices, it may be more efficient and straightforward.
  • Understand the distinctions between various list implementations, such as Arrays.asList, and select the one best suited to your scenario.

By judiciously choosing the right collection strategies, you can streamline your Java applications and foster performance and maintainability. How might you apply these techniques to your codebase? Share your experiences or thoughts, and consider experimenting with different approaches to discover what best meets your requirements.

About the author

As the CEO of Chronicle Software, Peter Lawrey leads the development of cutting-edge, low-latency solutions trusted by 8 out of the top 11 global investment banks. With decades of experience in the financial technology sector, he specialises in delivering ultra-efficient enabling technology which empowers businesses to handle massive volumes of data with unparalleled speed and reliability. Peter's deep technical expertise and passion for sharing knowledge have established him as a thought leader and mentor in the Java and FinTech communities. Follow Peter on BlueSky or Mastodon

tag:blogger.com,1999:blog-1729822519392175009.post-5286710777904361571
Extensions
Performance Tip: Rethinking Collection.toArray(new Type[0])
BenchmarksOpinionPerformance
Show full content
Introduction

Have you ever considered the performance implications of converting collections to arrays in Java? It's a common task; your chosen method can impact your application's efficiency. In this article, I will explore different approaches to toArray(), benchmark their performance, and determine which method is optimal for various scenarios.

The Challenge

Converting a Collection to an array seems straightforward, but the standard practice of using collection.toArray(new Type[0]) might not be the most efficient. Understanding the nuances of this method can help you write more performant code.

Exploring the Approaches

Let's delve into four primary methods and a combination for converting collections to arrays:

1. Using toArray() Without Arguments
Object[] array = { "Hello", "world" };
String[] strings = (String[]) array; // Throws ClassCastException at runtime

While this approach avoids additional array creation and can be fast, it lacks type safety and requires casting, leading to potential runtime exceptions.

2. Passing a Zero-Length Array: toArray(new Type[0])

A common practice involves passing a new zero-length array to the toArray() method.

String[] notifTypesArray = notifTypes.toArray(new String[0]);

This code creates a new zero-length array every time, incurring unnecessary allocation and reflection costs, especially in performance-critical applications.

3. Pre-Sizing the Array: toArray(new Type[collection.size()])
return (String[]) v.toArray(new String[v.size()]);

This method eliminates the need for toArray() to internally create a new array, enhancing performance for collections with known sizes.

4. Using a Constant Empty Array
private static final String[] NO_STRINGS = {};
// later
return s.toArray(NO_STRINGS);

This approach minimises array creation when the collection is empty but may introduce reflection overhead when elements are present.

5. Attempt to Get the Best of Both Worlds
return s.isEmpty() ? NO_STRINGS : (String[]) s.toArray(new String[s.size()]);
private static final String[] NO_STRINGS  = {};

This way, an empty array is reused whenever there are no results, and a variety of the correct size and type is used when the size is greater than or equal to one.

The Benchmark

To evaluate these methods, I conducted a benchmark using JMH (Java Microbenchmark Harness), available here.

Collections Tested
  • ArrayList: Sizes of 0, 3, 7, and 16 elements.
  • HashSet and TreeSet: Created from the same elements as the ArrayLists.
Benchmark Configuration
  • Warmup: 2 iterations, 1 second each.
  • Measurement: 3 iterations, 10 seconds each.
  • Threads: Configurable via -Dthreads, defaulting to 8.
  • Forks: 7 separate JVM instances for accurate results.
Results and Analysis

The benchmark results on an 8-core Ryzen 5950X were illuminating:

  • Throughput: Between 210 million and 450 million operations per second.
  • Margin of Error: Approximately 15 million ops/sec for HashSet and ArrayList, and about 40 million ops/sec for TreeSet.
Performance Comparison of toArray() Methods Practical Recommendations

Based on the results:

  • Avoid toArray(new Type[0]): It introduces unnecessary overhead without significant benefits.
  • Leverage Constant Empty Arrays When Appropriate: If collections are frequently empty, reusing a constant can save resources.
  • Or Use Pre-Sized Arrays: toArray(new Type[collection.size()]) is efficient and straightforward.
Conclusion

Avoid using Collection.toArray(new Type[0]) if you can. It’s probably not worth changing your code for, but if you use another approach, go with whatever you consider simplest. For me, that means using the NO_STRINGS constant.

What details about the benchmark would you like to know in the comments or a follow-up post?

Have you faced performance issues with the toArray() methods? How did you tackle them? Share your experiences and join the discussion!

About the author

As the CEO of Chronicle Software, Peter Lawrey leads the development of cutting-edge, low-latency solutions trusted by 8 out of the top 11 global investment banks. With decades of experience in the financial technology sector, he specialises in delivering ultra-efficient enabling technology which empowers businesses to handle massive volumes of data with unparalleled speed and reliability. Peter's deep technical expertise and passion for sharing knowledge have established him as a thought leader and mentor in the Java and FinTech communities. Follow Peter on BlueSky or Mastodon

tag:blogger.com,1999:blog-1729822519392175009.post-8630501392747162821
Extensions
Storing 1 TB in Virtual Memory on a 64 GB Machine with Chronicle Queue
BenchmarksInfoPerformance
Show full content

As Java developers, we often face the challenge of handling very large datasets within the constraints of the Java Virtual Machine (JVM). When the heap size grows significantly—often beyond 32 GB—garbage collection (GC) pause times can escalate, leading to performance degradation. This article explores how Chronicle Queue enables the storage and efficient access of a 1 TB dataset on a machine with only 64 GB of RAM.

The Challenge of Large Heap Sizes

Using standard JVMs like Oracle HotSpot or OpenJDK, increasing the heap size to accommodate large datasets can result in longer GC pauses. These pauses occur because the garbage collector requires more time to manage the larger heap, which can negatively impact application responsiveness.

One solution is to use a concurrent garbage collector, such as the one provided by Azul Zing, designed to handle larger heap sizes while reducing GC pause times. However, this approach may only scale well when the dataset is within the available main memory.

Handling Datasets Larger Than Main Memory

What if your dataset is larger than your machine's main memory? In such cases, relying on the heap is not feasible. Off-heap data stores become essential. While databases or NoSQL solutions can handle large datasets, they often introduce significant latency, which may not be acceptable for high-performance applications.

Introducing Chronicle Queue

Chronicle Queue provides a persisted, off-heap data store that can be accessed by multiple JVMs on the same server. It leverages memory-mapped files to manage large datasets efficiently. But how does it perform when the dataset size exceeds the available RAM?

In this article, we'll examine Chronicle Queue's performance when handling a 1 TB dataset on a machine with 64 GB of memory, focusing on sequential access patterns.

What Does a 1 TB JVM Look Like?

First, let's see what a JVM managing over 1 TB of data looks like on a machine with 64 GB of RAM.

Top output showing a JVM with 1 TB virtual size
Top output showing a JVM with 1 TB virtual size

In the top output above, the JVM's virtual memory size (VIRT) is reported as 1,045.8 g, while the resident set size (RES) is only 50.0 g. This means that although the JVM addresses over 1 TB of data, only about 50 GB is actually loaded into physical memory at any given time. This is possible because Chronicle Queue uses memory-mapped files, allowing the operating system to handle virtual memory efficiently.

Performance Analysis: Does It Slow Down?

A common concern is that managing datasets larger than the main memory will degrade performance dramatically. If you attempted to create a 1 TB heap on a 64 GB machine, the JVM would likely fail to start or render the machine unusable due to excessive swapping. However, Chronicle Queue's off-heap approach minimizes this issue.

Writing and Reading Data

In our test, we perform the following steps:

  • Write bursts of 1 GB consisting of 1 KiB messages.
  • Perform a sync operation to flush data to disk.
  • Read the data back sequentially.

The underlying disk can sustain about 630 MB/s. Writing 1 GB of data takes consistently less than 2 seconds, even as the total dataset size grows beyond the physical memory limit.

Consistent Write Performance
Write throughput remains consistent as the dataset size grows
The write throughput remains consistent as the dataset size grows
Minimal Write Slowdown
The write time shows a slight increase beyond 64 GB
The write time shows a slight increase beyond 64 GB

Writing 1 GB of data takes:

  • An average of 1.3 seconds for the first 64 GB.
  • An average of 1.5 seconds beyond 64 GB.

The slight increase in write time is minimal considering the dataset size and is acceptable for applications requiring high throughput.

Comparison with Traditional Databases

Consider the alternative of inserting 1 million records (approximately 1 GB) into a traditional database and querying them back. This process would likely take significantly longer and consume more resources. With Chronicle Queue, you can read 1 GB of records in around 70 ms using a single thread, offering a substantial performance advantage.

Conclusion

Chronicle Queue performs exceptionally well when handling datasets that exceed the main memory size. It maintains low GC pause times and efficient data access speeds by utilising off-heap memory and memory-mapped files. This makes it an ideal solution for large data volume processing applications without significant performance penalties.

Try It Yourself

You can experiment with this test by accessing the following code: RunLargeQueueMain.java

For more information on Chronicle Queue, visit the GitHub repository or the Chronicle Queue product page.

What Does the Enterprise Version Offer?

Chronicle Queue Enterprise offers additional features, such as:

  • Replication across multiple servers for Fail Over and Disaster Recovery
  • C++, Rust, Python implementations
  • Tuning options to reduce latency jitter
  • Support for Windows, macOS, and Linux
Share Your Thoughts

Have you faced challenges managing large datasets in Java? What solutions have you found effective? Feel free to share your experiences and join the discussion.

Note: Currently, having a queue larger than the main memory only works on Unix-based systems. On Windows, each cycle (e.g., a day's worth of data) must fit into the main memory. We aim to address this limitation in future updates.

tag:blogger.com,1999:blog-1729822519392175009.post-6537221859724691358
Extensions