C# wraps things in sugar. Java makes you see the machinery. This part covers what Java doesn’t protect you from — null, exceptions, and how values work.

Null Safety: You’re on Your Own

C# has evolved null safety through nullable reference types, compiler analysis, and operators like ?. and ??:

#nullable enable

string? name = GetName();
Console.WriteLine(name ?? "Unknown");
Console.WriteLine(name?.Length); // safe

Java has none of this at the language level. Nullability is convention-based:

@Nullable String name = getName();
System.out.println(name != null ? name : "Unknown");

Annotations like @Nullable / @NotNull are hints, not guarantees. The compiler won’t stop you.

Optional<T> — The Pattern That Proves the Gap

Java introduced Optional<T> as a return type to signal “might be null”:

Optional<String> name = Optional.ofNullable(getName());
System.out.println(name.orElse("Unknown"));

C# developers recognize the intent — it’s like Nullable<T> but for reference types. The difference: string? is a language feature; Optional<T> is a library class. You can ignore it, wrap it wrong, or receive null from code that doesn’t use it.

Pitfall: NullPointerException remains common in Java. @Nullable / @NotNull are hints, not enforcement. Optional helps but is itself a class you must remember to use. C#’s nullable references are compiler-enforced; Java’s null safety is willpower-enforced.

Checked Exceptions: The Compiler Makes You Deal With It

C# exceptions are all unchecked — the compiler never forces you to handle them:

string ReadFile(string path)
{
    return File.ReadAllText(path); // might throw, compiler doesn't care
}

Java has checked exceptions. If a method declares throws IOException, every caller must handle or re-declare it:

String readFile(String path) throws IOException {
    return Files.readString(Path.of(path));
}

Checked exceptions are part of the method signature. The compiler enforces that callers acknowledge them:

void load() throws IOException {
    String text = readFile("data.txt"); // must handle or propagate
}

Only Exception subclasses (except RuntimeException) are checked. NullPointerException, IllegalArgumentException, etc. are unchecked — so the same language has both models.

Why This Hurts

  • Exceptions cascade up the call stack, forcing multiple methods to add throws
  • Lambdas and streams hate checked exceptions — you can’t map() with a throws lambda
  • Many Java developers wrap checked exceptions in RuntimeException anyway, defeating the purpose
files.stream()
    .map(f -> Files.readString(f)) // won't compile (throws IOException)

Pitfall: What looks like “discipline” becomes friction. Checked exceptions push developers toward patterns (wrapping, swallowing) that undermine the very safety they promised.

try-with-resources vs using

C#’s using ensures deterministic disposal:

using var file = new StreamReader("data.txt");
var content = file.ReadToEnd();
// file closed here

Java’s try-with-resources works similarly but ties cleanup to exception handling:

try (var file = new FileReader("data.txt")) {
    var content = file.read();
}

Any resource implementing AutoCloseable works. Multiple resources can be declared in one try:

try (
    FileInputStream in = new FileInputStream("input.txt");
    FileOutputStream out = new FileOutputStream("output.txt")
) {
    // both auto-closed
}

Pitfall: Resources must be declared inside try(...) — forgetting means no auto-close. This pairs naturally with checked exceptions since close() typically throws IOException, which the try block already handles.

Primitives and Their Evil Twins

Java has two worlds for every numeric type:

int     -> primitive, stack, fast
Integer -> object, heap, nullable

This causes subtle issues. Boxing looks automatic but isn’t free:

List<Integer> numbers = new ArrayList<>();
numbers.add(5); // boxing — hidden allocation

And == on boxed types is reference comparison, with cached values masking the problem:

Integer x = 100;
Integer y = 100;
System.out.println(x == y); // true (cached)

Integer m = 1000;
Integer n = 1000;
System.out.println(m == n);      // false (not cached)
System.out.println(m.equals(n)); // true

C#’s int and int? feel unified. Java’s int vs Integer are genuinely different types with different performance characteristics and comparison semantics.

Pitfall: Using == with boxed types compares references. Java caches small values (-128 to 127), making the bug intermittent. Always use .equals() on boxed types.

No struct — Everything Goes on the Heap

C# lets you define value types with struct — stack-allocated, no GC pressure:

struct Vector3
{
    public float X, Y, Z;
}

Java has no equivalent. Everything except primitives is a heap-allocated object:

class Vector3 {
    float x, y, z;
}

Every new Vector3() is heap allocation. In hot paths — game loops, simulations, data processing — this creates significant GC pressure. Java’s escape analysis can stack-allocate in some cases, but it’s a JIT optimization, not a language guarantee.

Pitfall: Heavy numeric workloads create large numbers of short-lived objects, increasing garbage collection overhead. Patterns like object pooling are more common in Java for this reason.

String Isn’t Special in Java

C# gives strings first-class treatment: verbatim literals, interpolation, == overloaded for value comparison:

string path = @"C:\Users\file.txt";
string msg = $"Hello, {name}";
bool eq = (str1 == str2); // value comparison

Java strings get none of this. == is reference comparison (covered in Part 1). Verbatim strings don’t exist — escape backslashes manually. String interpolation was only added as a preview feature in Java 21 and still lags behind C#’s.

String path = "C:\\Users\\file.txt";
String msg = "Hello, " + name;   // no interpolation until Java 21+
String msg2 = STR."Hello, \{name}"; // Java 21+ preview

Pitfall: String feels familiar, but lacks the syntactic sugar you expect. Every escape sequence and concatenation is manual.


All these add up: checked exceptions to handle, null checks to write, boxed types to watch, every object on the heap. What looks like “explicitness” becomes tax. Coming up next: what about expressing behavior?