Back to Java: Notes from a Developer Returning to the Language

Why Return to Java?

Java never really went away. It just got quieter in my daily work as I moved toward JavaScript, TypeScript, and frontend-leaning stacks. But modern Java especially post-JDK 17 is a genuinely different language from what I remember. Records, sealed classes, pattern matching, virtual threads. It felt worth a proper revisit rather than pretending I knew it.

These are rough notes from the first few days getting back up to speed.

Day 1 Tooling and Hello World

The starting point was obvious: install JDK 21 and get something running.

# Verify the installation
java --version
javac --version

I used SDKMAN to manage the JDK install, which made switching versions trivial later. For the editor I went with IntelliJ IDEA Community for Java.

The classic first program:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Two commands to understand from the start:

  • javac HelloWorld.java compiles source to bytecode (HelloWorld.class)
  • java HelloWorld runs the bytecode on the JVM

The distinction matters: Java ships bytecode, not native binaries. The JVM interprets (and JIT-compiles) that bytecode at runtime. This is still the foundation of why Java runs everywhere.

Day 2 Primitive Types, Casting, and the Memory Model

Java has 8 primitive types. The ones that come up constantly:

TypeSizeDefaultNotes
int32-bit0Most common integer type
long64-bit0LUse L suffix for literals
double64-bit0.0Default floating-point type
boolean1-bitfalsetrue or false only
byte 8-bit-127 to 127Holds char and value less than 127
char16-bit’�‘Unsigned Unicode character

Type Casting

Widening (smaller → larger) is implicit. Narrowing requires an explicit cast and can lose data:

int x = 42;
long y = x;          // widening  implicit, safe

double d = 9.99;
int truncated = (int) d;  // narrowing  explicit, loses .99

Stack vs Heap

This tripped me up when I first learned Java years ago worth locking in early:

  • Stack stores primitive values and references. Each method call gets its own stack frame. Memory is reclaimed automatically when the frame pops.
  • Heap stores objects. Managed by the garbage collector. Lives longer than a single method call.
int a = 5;              // 'a' lives on the stack
String s = "hello";     // 's' (the reference) is on the stack; the String object is on the heap

Primitives are always on the stack. Objects are always on the heap. The reference to an object sits on the stack.

Day 3 Variables, Constants, and String Basics

Variables and Constants

Java is statically typed declare the type upfront or use var (type inferred by the compiler, not runtime):

int count = 0;
var name = "Aderemi";   // compiler infers String

Constants use final. The convention is UPPER_SNAKE_CASE:

final int MAX_RETRIES = 3;
final String API_VERSION = "v2";

Once assigned, a final variable cannot be reassigned. For class-level constants, pair it with static:

public static final double TAX_RATE = 0.075;

String Basics

String in Java is immutable operations always return a new String, they do not modify in place. The most-used methods:

String s = "Hello, Java";

s.length();               // 11
s.charAt(7);              // 'J'
s.substring(7);           // "Java"
s.substring(7, 11);       // "Java" (end index is exclusive)
s.contains("Java");       // true
s.equals("Hello, Java");  // true  always use equals(), not ==

The == vs equals() distinction is critical. == compares references (memory addresses). equals() compares content. Two String objects with identical text will fail a == check if they were allocated separately:

String a = new String("test");
String b = new String("test");

a == b;        // false  different objects
a.equals(b);   // true  same content

String literals from the pool are an exception (the JVM interns them), but relying on that is fragile. Always use equals().

Day 4 Operators

Arithmetic

Standard arithmetic works exactly as expected, with one gotcha: integer division truncates toward zero rather than rounding.

int a = 10, b = 3;

a + b;   // 13
a - b;   // 7
a * b;   // 30
a / b;   // 3  not 3.33, truncation!
a % b;   // 1  remainder

To get a decimal result from integer division, cast at least one operand to double first:

(double) a / b;   // 3.3333...

Compound assignment and increment/decrement work the same as in C-derived languages:

int x = 5;
x += 3;   // x is now 8
x++;      // x is now 9 (post-increment: returns old value, then increments)
++x;      // x is now 10 (pre-increment: increments first, then returns)

The prefix/postfix distinction only matters when the expression value is used in the same statement. Inside a standalone x++ or ++x it makes no difference.

Comparison and Logical

Comparison operators return boolean:

5 == 5    // true
5 != 3    // true
5 > 3     // true
5 <= 5    // true

Logical operators for combining booleans the important detail is short-circuit evaluation:

true && false    // false  evaluates right side only if left is true
true || false    // true   evaluates right side only if left is false
!true            // false

Because of short-circuiting, && and || are safe to use when the right-hand operand has a side effect (like a method call) you only want to run conditionally. Bitwise & and | do not short-circuit they always evaluate both sides.

Bitwise

Less common in day-to-day application code but useful when working with flags, permissions, or low-level data:

OperatorNameExampleResult
&AND5 & 31
|OR5 | 37
^XOR5 ^ 36
~NOT (unary)~5-6
<<Left shift1 << 38
>>Right shift8 >> 14
>>>Unsigned right shift8 >>> 14

The difference between >> and >>> only shows up with negative numbers. >> preserves the sign bit (arithmetic shift); >>> fills with zeros regardless (logical shift).

Ternary

A compact inline conditional. Readable when short, painful when nested:

int age = 20;
String label = age >= 18 ? "adult" : "minor";

Don’t chain ternaries. Use an if/else block the moment it takes more than one line to read.

Operator Precedence

From highest to lowest, condensed to what actually matters in practice:

PriorityOperators
1++, -- (postfix), method calls
2++, -- (prefix), !, ~, unary -
3*, /, %
4+, -
5<<, >>, >>>
6<, >, <=, >=
7==, !=
8&
9^
10|
11&&
12||
13?: (ternary)
14=, +=, -=, etc.

The practical rule: use parentheses to make intent explicit. Don’t rely on memorising every precedence level. a + b * c is fine. a | b && c is a bug waiting to happen parenthesise it.

Day 5 Control Flow

if / else if / else

Nothing unusual here. The body can be a single statement without braces, but always use braces the dangling-else ambiguity is not worth the saved characters:

int score = 74;

if (score >= 90) {
    System.out.println("A");
} else if (score >= 80) {
    System.out.println("B");
} else if (score >= 70) {
    System.out.println("C");
} else {
    System.out.println("F");
}

Nested Conditions

Nesting works, but more than two levels deep is usually a sign to extract a method or restructure with early returns:

if (user != null) {
    if (user.isActive()) {
        if (user.hasPermission("admin")) {
            // do the thing
        }
    }
}

Cleaner as guard clauses:

if (user == null) return;
if (!user.isActive()) return;
if (!user.hasPermission("admin")) return;
// do the thing

switch Statement (Traditional)

The traditional switch is still valid and widely found in existing codebases. One thing to know immediately: fall-through is the default. Without break, execution continues into the next case:

int day = 3;

switch (day) {
    case 1:
        System.out.println("Monday");
        break;
    case 2:
        System.out.println("Tuesday");
        break;
    case 3:
        System.out.println("Wednesday");
        break;
    default:
        System.out.println("Other");
}

Forgetting break is one of the classic Java bugs. The compiler won’t warn you.

switch Expression (Java 14+)

The newer arrow-syntax switch expression eliminates fall-through entirely and can return a value directly. This is the form to prefer in modern code:

int day = 3;

String name = switch (day) {
    case 1 -> "Monday";
    case 2 -> "Tuesday";
    case 3 -> "Wednesday";
    case 4 -> "Thursday";
    case 5 -> "Friday";
    default -> "Weekend";
};

Each arm is isolated no fall-through, no break required. Multiple labels can share an arm:

String type = switch (day) {
    case 1, 2, 3, 4, 5 -> "Weekday";
    case 6, 7 -> "Weekend";
    default -> "Unknown";
};

For arms that need more than one statement, use a block with yield to produce the value:

String label = switch (score) {
    case 10 -> "Perfect";
    default -> {
        String result = score > 5 ? "Good" : "Low";
        yield result + " (" + score + ")";
    }
};

yield is the switch-expression equivalent of return it sets the value produced by that arm. Using return here would exit the enclosing method entirely, which is almost never what you want inside a switch expression.

Day 6 Loops

for Loop

The classic counted loop. Three-part header: initializer, condition, update:

for (int i = 0; i < 5; i++) {
    System.out.println(i);   // 0 1 2 3 4
}

The enhanced for-each loop is cleaner for iterating over arrays and collections when you don’t need the index:

int[] numbers = {10, 20, 30, 40};

for (int n : numbers) {
    System.out.println(n);
}

while Loop

Checks the condition before each iteration. If the condition is false at the start, the body never runs:

int count = 0;

while (count < 3) {
    System.out.println(count);
    count++;
}

do-while Loop

Checks the condition after each iteration the body always runs at least once. Less common, but useful for input prompts or retry logic:

int n;

do {
    n = getUserInput();
} while (n < 0);

break and continue

break exits the loop immediately. continue skips the rest of the current iteration and jumps to the next:

for (int i = 0; i < 10; i++) {
    if (i == 5) break;         // stop at 5
    if (i % 2 == 0) continue;  // skip even numbers
    System.out.println(i);     // prints 1, 3
}

Labelled Loops

Java supports labels on loops, which lets break and continue target an outer loop directly. I had never actually used this in practice before revisiting the language:

outer:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (j == 1) continue outer;   // skip to next iteration of outer loop
        System.out.println(i + "," + j);
    }
}
// prints: 0,0  |  1,0  |  2,0

break outer would exit the outer loop entirely rather than just the inner one. It’s not something you reach for often, but when you need to escape a nested loop cleanly it’s better than a sentinel variable.

Day 7 Methods

Declaration Syntax

A Java method declaration has four required parts: access modifier, return type, name, and parameter list. The body follows in braces:

accessModifier returnType methodName(ParameterType paramName) {
    // body
    return value;
}

A concrete example:

public int add(int a, int b) {
    return a + b;
}

Parameters and Return Types

Parameters are declared with their type. Java passes primitives by value (a copy) and objects by reference (a copy of the reference not the object itself):

public String greet(String name) {
    return "Hello, " + name + "!";
}

Multiple parameters, multiple types:

public double calculateArea(double width, double height) {
    return width * height;
}

void Methods

When a method produces no return value, the return type is void. A bare return; is valid to exit early but is not required at the end:

public void printSeparator(int length) {
    for (int i = 0; i < length; i++) {
        System.out.print("-");
    }
    System.out.println();
}

Method Signature

The method signature is the name plus the parameter type list return type and parameter names are not part of it. This is what the compiler uses to distinguish overloaded methods:

// Different signatures  same name, different parameters
public int multiply(int a, int b)           { return a * b; }
public double multiply(double a, double b)  { return a * b; }
public int multiply(int a, int b, int c)    { return a * b * c; }

Overloading on return type alone is not allowed. int foo() and double foo() are indistinguishable from the call site the compiler rejects it.

Calculator Utility Class

Putting Day 7 together into a small utility class. No instance needed, so everything is static. Division and modulo include guards without them a zero denominator would throw an unchecked ArithmeticException at runtime with no helpful message:

public class Calculator {

    public static int add(int a, int b) {
        return a + b;
    }

    public static int subtract(int a, int b) {
        return a - b;
    }

    public static int multiply(int a, int b) {
        return a * b;
    }

    public static double divide(double a, double b) {
        if (b == 0) {
            throw new ArithmeticException("Division by zero");
        }
        return a / b;
    }

    public static double power(double base, int exponent) {
        double result = 1;
        for (int i = 0; i < Math.abs(exponent); i++) {
            result *= base;
        }
        return exponent < 0 ? 1.0 / result : result;
    }

    public static int modulo(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("Modulo by zero");
        }
        return a % b;
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        System.out.println(Calculator.add(10, 5));         // 15
        System.out.println(Calculator.subtract(10, 5));    // 5
        System.out.println(Calculator.multiply(4, 6));     // 24
        System.out.println(Calculator.divide(10.0, 3.0));  // 3.3333...
        System.out.println(Calculator.power(2, 8));        // 256.0
        System.out.println(Calculator.modulo(17, 5));      // 2
    }
}

The power method handles negative exponents power(2, -3) returns 0.125. Rolling it manually rather than reaching for Math.pow immediately was a useful exercise for combining a loop, a conditional, and a return value in one method.

What’s Next

Days 8 onward move into arrays, ArrayList, and then OOP fundamentals classes, constructors, encapsulation, inheritance. That’s where Java starts to feel like itself rather than a verbose scripting language. The goal is to reach a point where I can comfortably write and read modern Java (records, streams, optionals) without mentally translating from another language.

More notes to follow.