Patterns

Coding Style

The Java conventions L2JMobius enforces. Read this before you submit.

Code must be understandable without IDE assistance. The reader is more important than the writer.

Core principles

1. Type inference is forbidden

The var keyword is completely prohibited. Always use explicit types.

// CORRECT - explicit type visible.
AudioManager manager = new AudioManager();
HashMap<String, Block> blockMap = new HashMap<>();
List<Player> players = new ArrayList<>();

// WRONG - hides the type.
var manager = new AudioManager();
var blockMap = new HashMap<String, Block>();

Why? Code must be understandable without IDE assistance. The reader is more important than the writer.

2. Single-line code - no wrapping

Control flow, conditions and signatures stay on single lines. If it does not fit on one line, it does not fit in the head either.

// GOOD - all parameters visible.
public void processData(String source, String target, boolean validate, int quality, ProcessingMode mode)
{
    // You can see everything. No hidden coupling.
}

// WRONG - wrapping hides complexity.
public void processData(
    String source,
    String target,
    boolean validate
)
{
    // Context is distributed vertically.
}

// CORRECT - condition visible.
if (condition1 && condition2 && condition3)
{
    doSomething();
}

Don't wrap. Don't hide. If it's long, it's long.

3. Allman braces - always on a new line

// WRONG - K&R style.
if (condition) {
    doSomething();
}

// CORRECT - Allman style.
if (condition)
{
    doSomething();
}

Why? Visual symmetry makes code easier to scan and spot errors.

4. Tabs for indentation, not spaces

A tab character is the unambiguous representation of one indentation level. Spaces are an approximation.

5. Complete sentences in comments

Comments start with a capital letter and end with a period.

// WRONG.
// calculate average value

// CORRECT.
// Calculate the average value.

Why? Professional code looks professional. We're not writing text messages.

Naming conventions

Private fields - _lowerCamelCase with leading underscore

Use final where possible.

private final int _maxHealth;
private float _currentSpeed;
private final AudioManager _audioManager;

Public members and methods - lowerCamelCase, no underscore

public void processData(String input)
{
    // Implementation.
}

public boolean validateInput(String data)
{
    // Implementation.
}

Local variables and parameters - lowerCamelCase, no underscore

Use final for locals where possible. Do NOT use final on parameters.

Constants - UPPER_CASE with underscores

public static final int CHUNK_SIZE = 16;
public static final float GRAVITY = -9.81f;
private static final String GAME_TITLE = "SimpleCraft";

Interfaces - PascalCase, no I prefix

public interface FileProcessor
{
    void process(String filePath);
    boolean validate(String data);
}

Control structures

If / else

Always use braces - even for single statements. Prefer early returns over nested else.

// CORRECT - early return.
if (!player.isAlive())
{
    return;
}

player.update(deltaTime);

// AVOID - unnecessary else.
if (player.isAlive())
{
    player.update(deltaTime);
}
else
{
    return;
}

Switch - traditional only

Use switch for 3+ cases; if/else for 1-2. Always include default and break. No arrow-syntax / switch expressions.

// CORRECT - traditional switch.
String name;
switch (blockType)
{
    case GRASS:
    {
        name = "Grass";
        break;
    }
    case DIRT:
    {
        name = "Dirt";
        break;
    }
    default:
    {
        name = "Unknown";
        break;
    }
}

// WRONG - new switch expressions.
String name = switch (blockType)
{
    case GRASS -> "Grass";
    case DIRT  -> "Dirt";
    default    -> "Unknown";
};

Why? The new switch expression hides control flow behind syntactic sugar - you lose explicit `break` statements, brace-scoped variable declarations, and the visual symmetry that lets a reader scan branches at a glance. It also silently forbids fallthrough, so the moment two cases need to share logic you're stuck duplicating code.

Language features

Avoid the Streams API

Streams introduce hidden allocations, GC pressure and obscure call sites. Use traditional loops.

// CORRECT - traditional loop.
final List<Enemy> alive = new ArrayList<>();
for (Enemy enemy : enemies)
{
    if (enemy.isAlive())
    {
        alive.add(enemy);
    }
}

// WRONG - streams.
List<Enemy> alive = enemies.stream()
    .filter(Enemy::isAlive)
    .collect(Collectors.toList());

No fully qualified class names inline

Always use imports. Never write java.util.Collections.emptyList() in a method body.

No null annotations

Do not use @NonNull / @Nullable. Use explicit null checks. Annotations are not enforced at runtime - they only provide compile-time hints in some IDEs and add visual noise. An explicit if (x == null) branch is unambiguous, debuggable, and works in every environment without configuration.

Performance

Garbage collection is not free

Avoid allocations in hot paths. Prefer object pooling or array reuse where possible.

Cache repeated getter calls

When the same getter (or any side-effect-free zero-arg call) is invoked more than once in the same scope, assign it to a final local on first use and reuse the local.

// WRONG - three calls, hides the invariant.
if (player.getInventory().getSize() > 0)
{
    sendMessage(player.getInventory().getSize() + " items");
    process(player.getInventory().getSize());
}

// CORRECT - one call, intent explicit, JIT-friendly.
final int inventorySize = player.getInventory().getSize();
if (inventorySize > 0)
{
    sendMessage(inventorySize + " items");
    process(inventorySize);
}

Exceptions to the caching rule:

• Different receiver (a.getId() vs b.getId()) - not the same call.

• Methods that return fresh state or mutate (iterator next(), poll(), time-of-day) - do NOT cache.

• Volatile fields where the freshest read is required.

• Singleton accessors like SomeManager.getInstance() - the method already returns the cached instance, so calling it repeatedly is effectively free and a local alias just adds noise.

Prefer primitives over wrappers

Avoid Integer / Float / Boolean when int / float / boolean work. Mind autoboxing.

Use StringBuilder

For more than two concatenations, build with StringBuilder.

Anti-patterns

  • No var - explicit types only.
  • No multiple variables on one line - int a, b = 0; is wrong. One per line.
  • No single-use constants - inline values used in exactly one place.
  • No magic numbers - if a literal has meaning, name it.
  • No deeply nested code - flatten with early returns.

Quick reference checklist

Before submitting code, verify:

  • No var keyword - always use explicit types.
  • No Streams API - use traditional loops.
  • No fully qualified class names inline - use imports.
  • No null annotations - use explicit null checks.
  • No new switch expressions - traditional switch only.
  • GC awareness - avoid allocations in hot paths, prefer pooling.
  • Prefer primitives - avoid boxing / unboxing.
  • Single-line code - all control flow, conditions, signatures on single lines.
  • Allman braces - opening { on a new line.
  • Tabs for indentation - not spaces.
  • Complete sentences in comments - capital letter, period.
  • _lowerCamelCase for private fields (final where possible).
  • lowerCamelCase for public methods, locals, parameters.
  • UPPER_CASE for constants.
  • One blank line between methods. Never more than one blank line anywhere.
  • No trailing spaces.
  • One variable per line.
  • Switch for 3+ cases, if/else for 1-2.
  • Always use braces on switch / if / else / loops.
  • StringBuilder for string concatenation.
  • Cache repeated getters - same getX() called 2+ times in one scope goes into a final local.