AI can be a fantastic coding partner—if you ask the right questions. The trick isn’t just “using AI,” it’s crafting requests that drive precise, maintainable, and secure Java code. Below are five best-practice request patterns you can use today to make your Java workflow faster and safer, while keeping code quality high.
TL;DR: Give AI the right constraints, context, and outcomes (tests!), and ask it to explain trade-offs. You’ll get better code and fewer surprises.
Why your AI requests matter
Think of AI like a very fast senior dev who only has context you give it. The more concrete your request—language version, constraints, examples, tests—the better the output. Good requests reduce back-and-forth, prevent hallucinations, and yield code that’s idiomatic for your stack.
- Be explicit about Java version and libraries.
- Share minimal reproducible context (interfaces, method signatures, failing tests).
- Demand reasoning: why these choices, complexity, trade-offs.
- Insist on secure, robust defaults.
- Ask for artifacts you’d expect from a pro: tests, docs, CI, lints.
The Top 5 Best-Practice Requests
1) Context-rich, test-first requests
Give AI failing tests and the smallest amount of code necessary. Ask it to make the tests pass and keep the public API stable. This anchors the solution and keeps it aligned with your intent.
What to include:
- Java version and build tool.
- Short, failing unit test(s).
- The minimal class or method to implement.
- Constraints (e.g., “no external deps”, “O(n) time”, “thread-safe”).
- Request a short explanation of choices.
Example request “Using Java 17 and JUnit 5, implement Slug.slugify(String) so these tests pass. No external libraries. Explain your approach and edge cases.”
// src/test/java/SlugTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class SlugTest {
@Test
void slugifiesAscii() {
assertEquals("hello-world", Slug.slugify("Hello, World!"));
}
@Test
void collapsesWhitespace() {
assertEquals("a-b", Slug.slugify(" a b "));
}
@Test
void handlesUnicode() {
assertEquals("naive", Slug.slugify("naïve"));
}
}
// src/main/java/Slug.java
import java.text.Normalizer;
public final class Slug {
private Slug() {}
public static String slugify(String input) {
if (input == null) return "";
String norm = Normalizer.normalize(input, Normalizer.Form.NFD)
.replaceAll("\\p{M}", ""); // strip diacritics
String slug = norm.toLowerCase()
.replaceAll("[^a-z0-9]+", "-") // non-alphanum -> hyphen
.replaceAll("(^-|-$)", "") // trim edge hyphens
.replaceAll("-{2,}", "-"); // collapse duplicates
return slug;
}
}
Why this works:
- Tests define the contract.
- Constraints deter needless dependencies.
- The explanation (which you should request) will cover Unicode normalization and regex choices.
2) Version-aware, idiomatic Java requests
AI can produce very different code depending on the Java version. If you’re on 17, say so. Ask for idiomatic solutions: records, pattern matching for instanceof, switch expressions (on supported types), try-with-resources, etc.
What to include:
- Exact Java version.
- Framework or no framework (e.g., “plain Java 17, no Spring”).
- Style preferences (immutability, null-handling strategy).
- Performance/complexity targets if applicable.
Example request “Write an idiomatic Java 17 utility with records and pattern matching for instanceof. No external libraries. Include a small demo.”
// Java 17 idioms: records + pattern matching for instanceof
public class CommandDemo {
interface Command {}
public record Add(int a, int b) implements Command {}
public record Echo(String message) implements Command {}
public static String execute(Command c) {
if (c instanceof Add add) {
return Integer.toString(add.a() + add.b());
} else if (c instanceof Echo echo) {
return echo.message();
} else {
throw new IllegalArgumentException("Unknown command: " + c);
}
}
public static void main(String[] args) {
System.out.println(execute(new Add(2, 3))); // 5
System.out.println(execute(new Echo("hi"))); // hi
}
}
Bonus requests to include:
- “Avoid reflection.”
- “Use Optional sparingly; prefer throwing IllegalArgumentException for invalid inputs.”
- “Follow Google Java Style or Oracle conventions.”
3) Ask for reasoning, complexity, and tests
Have AI justify decisions, state time/space complexity, and provide tests to safeguard refactors. This makes reviews easier and documents trade-offs.
Example request “Implement an O(1) LRU cache in Java 17 using LinkedHashMap. Explain the design, complexity, and alternatives. Provide JUnit 5 tests.”
import java.util.LinkedHashMap;
import java.util.Map;
public class LruCache {
private final Map map;
public LruCache(int capacity) {
if (capacity 0");
this.map = new LinkedHashMap<>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > capacity;
}
};
}
public synchronized V get(K key) {
return map.get(key);
}
public synchronized void put(K key, V value) {
map.put(key, value);
}
public synchronized int size() {
return map.size();
}
}
// JUnit 5 tests
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class LruCacheTest {
@Test
void evictsLeastRecentlyUsed() {
LruCache cache = new LruCache<>(2);
cache.put(1, "a");
cache.put(2, "b");
cache.get(1); // 1 becomes most recently used
cache.put(3, "c"); // evicts 2
assertNull(cache.get(2));
assertEquals("a", cache.get(1));
assertEquals("c", cache.get(3));
}
}
Ask AI to explain:
- Why LinkedHashMap with accessOrder=true delivers O(1) get/put.
- Why synchronized methods vs. alternatives (e.g., using Collections.synchronizedMap, ConcurrentHashMap + custom deque).
- When to choose Caffeine/Guava instead, linking to Caffeine.
This improves review transparency and helps future maintainers.
4) Request security and performance reviews (with fixes)
Ask AI to act like a security/performance reviewer. Provide a snippet and ask for concrete remediations aligned with OWASP and standard Java practices.
Example request “Review this JDBC code for SQL injection, resource leaks, and performance. Give a fixed version and explain each change. Java 17, no ORM.”
Bad code:
// Vulnerable and leaky
public User findUserByName(Connection conn, String name) throws Exception {
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT id, name FROM users WHERE name = '" + name + "'");
if (rs.next()) {
return new User(rs.getLong("id"), rs.getString("name"));
}
return null;
}
Improved code:
import java.sql.*;
public class UserDao {
public User findUserByName(Connection conn, String name) throws SQLException {
String sql = "SELECT id, name FROM users WHERE name = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, name);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return new User(rs.getLong("id"), rs.getString("name"));
}
return null;
}
}
}
public record User(long id, String name) {}
}
Why this is better:
- PreparedStatement prevents SQL injection.
- try-with-resources closes statements and result sets reliably.
- The method throws SQLException, which is more precise.
- You can also request timeouts, fetch sizes, and indexes for performance.
Helpful links:
Also ask AI for:
- Null-safety and input validation.
- Logging that avoids sensitive data.
- Threading pitfalls (shared mutable state, improper locking).
- Static analysis rules for SpotBugs or SonarLint.
5) Generate scaffolding: tests, docs, and CI
AI shines at repetitive scaffolding. Ask it for parameterized tests, Javadoc, and CI configs that match your build tool and Java version.
Example request “Given this method, write Javadoc, a JUnit 5 parameterized test, and a GitHub Actions workflow for Maven with Java 17.”
Source:
public final class MathUtils {
private MathUtils() {}
/**
- Returns the greatest common divisor of a and b using Euclid's algorithm.
- Both inputs must be non-negative. GCD(0, 0) is defined as 0.
*/
public static int gcd(int a, int b) {
if (a MathUtils.gcd(a, b));
}
}
GitHub Actions workflow:
name: Java CI
on:
pull_request:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: maven
- name: Build with Maven
run: mvn -B -ntp -DskipTests=false verify
You can also ask for:
- Checkstyle or SpotBugs configs.
- Maven/Gradle snippets for reproducible builds.
- Test coverage thresholds and badges.
Small tips that pay off big
- Be explicit about limits: “No external dependencies,” “O(n log n) time,” “Avoid recursion.”
- Lock down versions: “Java 17, Maven 3.9, JUnit 5.10, AssertJ 3.25.”
- Ask for edge cases: nulls, empty collections, Unicode, time zones, overflow.
- Request benchmarks or micro-optimizations only with justification. Over-optimizing early is a trap.
- Ask for alternatives: “Show a LinkedHashMap LRU and a Caffeine-based LRU; compare.”
- Include your architecture context: “This runs in a Spring Boot 3 app with virtual threads disabled.”
Anti-patterns to avoid
- Vague requests: “Write a cache.” Better: “LRU, O(1), capacity=1000, eviction on get/put, Java 17, no deps.”
- Hidden constraints: Don’t reveal you’re on Java 8 after asking for records.
- Accepting code without tests: Always request tests and explanations.
- Security afterthoughts: Ask for threat modeling and alignment with OWASP from the start.
- Letting AI pick libraries blindly: Specify allowed libs or prefer JDK-first solutions.
Wrap-up
AI is a power tool for Java devs—when you steer it. Use context-rich, test-first prompts; insist on idiomatic, version-aware code; demand reasoning; prioritize security and performance; and generate the boring but essential scaffolding. Do this consistently, and you’ll ship better Java faster.
Further reading:
- Oracle Java Docs: docs.oracle.com
- Effective Java notes: Effective Java (summary)
- JUnit 5: junit.org/junit5
- OWASP Cheat Sheets: cheatsheetseries.owasp.org