Java Basics for Python Developers

Understanding WHY Java is verbose, and how that verbosity protects code consumers

The Core Philosophy Difference

Python

"We're all consenting adults here."

Trust developers to do the right thing. Conventions over enforcement.

Best for: Rapid development, scripts, small teams, prototyping

Java

"Make wrong things impossible."

The compiler enforces correctness. Mistakes caught before runtime.

Best for: Large teams, long-lived codebases, library/API design

Key Insight

When 50 developers touch the same codebase over 10 years, Java's verbosity becomes a feature. The "boilerplate" is actually encoding important constraints that protect everyone.

1. Static Typing: Catching Errors Before Runtime

def calculate_total(price, quantity):
    return price * quantity

# These all "work" until runtime
calculate_total(10, 5)        # OK: 50
calculate_total("10", 5)      # Surprise: "1010101010"
calculate_total([1,2], 3)     # Surprise: [1, 2, 1, 2, 1, 2]

# You only find out about type errors when the code runs
# Could be in production, at 3am, on a weekend...
public int calculateTotal(int price, int quantity) {
    return price * quantity;
}

// Compile-time errors - code won't even run:
calculateTotal(10, 5);        // OK: 50
calculateTotal("10", 5);      // COMPILE ERROR: incompatible types
calculateTotal(myList, 3);    // COMPILE ERROR: incompatible types

// Errors caught BEFORE deployment, in your IDE, instantly

Why This Matters for Code Consumers

IDE Autocomplete
The IDE knows exactly what types are expected and can suggest valid options
Instant Feedback
Errors caught before running tests, before deployment, before production
Self-Documenting
The method signature IS the documentation - no need to read implementation
Refactoring Safety
Rename a method, compiler finds every usage that needs updating

2. Access Modifiers: Controlling Your API Surface

This is where Java shines for library authors and API designers.

class BankAccount:
    def __init__(self, balance):
        self.balance = balance          # Public by convention
        self._internal_id = "abc123"    # "Private" by convention (_)
        self.__secret = "shhh"          # Name-mangled, but still accessible

    def deposit(self, amount):
        self.balance += amount

# Nothing stops a consumer from doing:
account = BankAccount(100)
account.balance = -999999       # Oops, invalid state
account._internal_id = "hacked" # Convention ignored
account._BankAccount__secret    # Even "private" is accessible
public class BankAccount {
    private double balance;              // TRULY private - compiler enforced
    private final String accountId;      // Private AND immutable

    public BankAccount(double initialBalance) {
        if (initialBalance < 0) {
            throw new IllegalArgumentException("Balance cannot be negative");
        }
        this.balance = initialBalance;
        this.accountId = generateId();
    }

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit must be positive");
        }
        this.balance += amount;
    }

    public double getBalance() {
        return this.balance;  // Read-only access
    }
    // No setBalance() - consumers CANNOT directly modify
}

// Consumer code:
BankAccount account = new BankAccount(100);
account.balance = -999999;    // COMPILE ERROR: balance has private access
account.getBalance();         // OK: returns 100

The Four Access Levels

Modifier Same Class Same Package Subclass Everywhere
private Yes No No No
(default) Yes Yes No No
protected Yes Yes Yes No
public Yes Yes Yes Yes

Key Insight: API Evolution

You can freely change private internals without breaking consumers. Only public is your committed API contract. This is why proper encapsulation enables safe evolution of large codebases.

3. Getters/Setters: Not Just Boilerplate

Python developers often mock Java's getters/setters as unnecessary ceremony. Here's why they exist:

class Temperature:
    def __init__(self):
        self._celsius = 0

    # Python CAN do this, but it's opt-in, not default
    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Below absolute zero!")
        self._celsius = value

# Problem: if you START with a public attribute
# and later need validation, you must refactor
# all consumer code OR add @property (breaking direct access)
public class Temperature {
    private double celsius;

    public double getCelsius() {
        return celsius;
    }

    public void setCelsius(double celsius) {
        if (celsius < -273.15) {
            throw new IllegalArgumentException("Below absolute zero!");
        }
        this.celsius = celsius;
    }

    // BONUS: Computed property without storage
    public double getFahrenheit() {
        return (celsius * 9/5) + 32;
    }

    // Read-only: no setFahrenheit() exists
    // Consumer knows this is derived, not stored
}

Why Getters/Setters Matter

Validation
Add rules anytime without changing the API signature
Computed Values
getFahrenheit() doesn't need storage - it's calculated
Lazy Loading
Load from database on first access, cache afterward
Logging/Auditing
Log every time a sensitive value changes
Thread Safety
Add synchronization later without API changes
Read-Only Fields
Provide getter without setter = immutable to consumers

The Critical Insight

If you expose public double celsius, you can NEVER add validation without breaking all consumers. With getCelsius()/setCelsius(), you can evolve the implementation freely while the API stays stable.

4. The final Keyword: Immutability and Intent

# No true constants - just convention
MAX_CONNECTIONS = 100  # SCREAMING_CASE = "please don't change this"
MAX_CONNECTIONS = 999  # But you can... nothing stops you

class User:
    def __init__(self, user_id):
        self.user_id = user_id  # Nothing stops reassignment later

user = User("U123")
user.user_id = "HACKED"  # Allowed, even if it shouldn't be
// True compile-time constant
public static final int MAX_CONNECTIONS = 100;
MAX_CONNECTIONS = 999;  // COMPILE ERROR: cannot assign to final variable

public class User {
    private final String userId;  // Must be set in constructor, never changed

    public User(String userId) {
        this.userId = userId;
    }

    public String getUserId() {
        return userId;
    }
    // No setUserId() possible - truly immutable after construction
}

User user = new User("U123");
// user.userId = "HACKED";  // Won't even compile

Three Uses of final

Usage Meaning Why It Matters
final int x = 10; Variable cannot be reassigned Safe to cache, share between threads
public final void check() Method cannot be overridden Security-critical code can't be bypassed
public final class String Class cannot be extended Prevents malicious subclasses

Key Insight

When you see final, you KNOW it won't change. No need to trace through code wondering "does anything modify this?" The compiler guarantees it.

5. Interfaces: Contracts for Consumers

This is Java's killer feature for API design and the foundation of SOLID principles.

def process_payment(payment_processor):
    # We HOPE payment_processor has a .charge() method
    # We find out at runtime if it doesn't
    payment_processor.charge(100)

# Any object might work... or might crash at runtime
process_payment(my_thing)  # Does my_thing have .charge()? We'll find out!

# Python 3.8+ has Protocol for type hints, but it's optional
from typing import Protocol

class PaymentProcessor(Protocol):
    def charge(self, amount: float) -> bool: ...

# But this is just for type checkers, not enforced at runtime
// The CONTRACT: anyone implementing this MUST provide these methods
public interface PaymentProcessor {
    boolean charge(double amount);
    void refund(String transactionId);
    String getProviderName();
}

// Implementation MUST fulfill the contract - compiler enforces
public class StripeProcessor implements PaymentProcessor {
    @Override
    public boolean charge(double amount) {
        // Stripe-specific implementation
        return true;
    }

    @Override
    public void refund(String transactionId) {
        // Stripe-specific implementation
    }

    @Override
    public String getProviderName() {
        return "Stripe";
    }

    // If you forget refund(), COMPILE ERROR - contract not fulfilled
}

// Consumer code is GUARANTEED these methods exist
public void processPayment(PaymentProcessor processor) {
    processor.charge(100);  // Compiler guarantees this exists
}

Why Interfaces Matter

Compile-Time Guarantees
Missing methods = compile error, not runtime crash in production
Multiple Implementations
StripeProcessor, PayPalProcessor, MockProcessor all work
Testing
Pass a mock implementation for unit tests
Documentation
The interface IS the contract - no ambiguity
Decoupling
Consumer depends on interface, not implementation
IDE Support
"Find all implementations" works perfectly

6. Checked Exceptions: Forcing Error Handling

def read_config(path):
    return open(path).read()  # Might raise FileNotFoundError

# Consumer might forget to handle it
config = read_config("settings.json")  # Crashes if file missing

# No indication in the function signature that this can fail
# You have to read the implementation or documentation
// Method DECLARES what can go wrong - part of the signature
public String readConfig(String path) throws IOException {
    return Files.readString(Path.of(path));
}

// Consumer MUST handle it - compiler enforces this
public void loadSettings() {
    String config = readConfig("settings.json");  // COMPILE ERROR!
}

// Option 1: Handle it
public void loadSettings() {
    try {
        String config = readConfig("settings.json");
    } catch (IOException e) {
        // You MUST decide what to do: retry, default, fail, etc.
    }
}

// Option 2: Propagate it (caller must handle)
public void loadSettings() throws IOException {
    String config = readConfig("settings.json");
}

Key Insight

The method signature tells consumers: "This can fail in these specific ways, and you MUST deal with it." No more "I didn't know that could throw an exception" bugs in production at 3am.

Quick Reference: Python → Java

Python Java Why Java Does It
def func():public void func()Explicit return type
self.x = 1this.x = 1Same concept
_privateprivateEnforced, not convention
Duck typingInterfacesCompile-time contracts
NonenullSame concept
True/Falsetrue/falseLowercase in Java
list = []List<String> list = new ArrayList<>()Type-safe collections
dict = {}Map<String, Integer> map = new HashMap<>()Type-safe maps