Skip to content

Module 02: Java Language Fundamentals (for Frontend Developers)

Goal: Master the core syntax needed to read and understand Java code, building a mental mapping quickly by comparing with JavaScript/TypeScript.

Preface: The Relationship Between Java and JavaScript

Despite the similar names, Java and JavaScript are completely different languages. However, as a frontend developer, the programming concepts you already know (variables, functions, conditionals, loops, objects, async) will transfer directly.

Key differences at a glance:

FeatureJavaScript / TypeScriptJava
Type checkingRuntime (JS) / Compile-time + Runtime (TS)Compile-time (strongly typed)
ExecutionInterpreted (V8 engine)Compiled to bytecode → executed by JVM
Entry pointScripts run directly / main() not requiredMust have public static void main()
File structureOne file can export multiple thingsOne file can only have one public class
Package managementimport + package.jsonimport + package declaration + Gradle/Maven
null handlingnull / undefinedOnly null (no undefined)
ClassesOptional (functional style works too)Mandatory (everything lives inside a class)

1. Basic Syntax Comparison

1.1 Variable Declarations

java
// Java: type first, variable name second — type declaration is required
String name = "Alice";            // similar to TS: let name: string = "Alice"
int age = 25;                     // similar to TS: let age: number = 25
boolean isActive = true;          // similar to TS: let isActive: boolean = true
double price = 19.99;             // floating-point uses double (64-bit)
float ratio = 0.5f;               // floating-point can also use float (32-bit), requires f suffix
long timestamp = 1700000000L;     // long integers require the L suffix
char letter = 'A';                // single quotes for a single character (not a string)

// Constants: use final instead of const
final String APP_NAME = "MyApp";  // similar to TS: const APP_NAME = "MyApp"

// Type inference (Java 10+): similar to TS's let x = ...
var count = 42;          // compiler infers int
var message = "hello";   // compiler infers String

Key differences from JS:

  • Java doesn't have let / const / var keyword choices (before Java 10 you must write the type; Java 10+ can use var for inference)
  • Java has no undefined — uninitialized reference types default to null
  • Java's primitive types (int, double, boolean, char) are not objects; they have corresponding wrapper classes (Integer, Double, Boolean, Character)

1.2 Primitive Types vs Reference Types

This is one of the biggest differences between Java and JS. In JS, everything is an object; in Java, that's not the case:

java
// Primitive types: store the value directly, not objects
int a = 10;
int b = a;      // b is a copy of a; modifying b does not affect a
b = 20;
// now a = 10, b = 20

// Reference types: store a reference (address) to the object
int[] arr1 = {1, 2, 3};
int[] arr2 = arr1;    // arr2 points to the same array
arr2[0] = 99;
// now arr1[0] is also 99, because they point to the same object

Primitive type reference table:

Java PrimitiveWrapper ClassJS/TS EquivalentDefault Value
intIntegernumber0
longLongnumber / bigint0L
doubleDoublenumber0.0
floatFloatnumber0.0f
booleanBooleanbooleanfalse
charCharacterstring (single char)'\u0000'
byteByteNo direct equivalent0
shortShortNo direct equivalent0

Why do wrapper classes exist? Because Java generics don't support primitive types. List<int> is illegal — you must use List<Integer>. This is similar to the difference between number and Number in TypeScript, but in Java the distinction is more critical.

Stack vs. Heap

Understanding the Java memory model helps diagnose memory leaks and performance issues:

java
public void run() {
    int count = 10;              // primitive: stored on the stack frame
    User user = new User("Zhang"); // user reference on stack, object on heap
}
AreaStoresLifetimeFrontend Analogy
StackLocal variables, method parameters, primitivesReleased when method returnsFunction call stack
HeapAll new objects and arraysRecycled by GCJS heap memory

Diagnostic tip: OutOfMemoryError usually relates to the heap (too many objects or large images); StackOverflowError relates to the stack (infinite recursion).

1.3 Strings

java
// Strings are immutable (same as JS)
String greeting = "Hello";
String name = "World";

// Concatenation (use + or String.format)
String message = greeting + ", " + name + "!";
String formatted = String.format("%s, %s!", greeting, name);

// Template literals (Java has no template literals — this is the closest alternative)
// JS:  `Hello, ${name}! You are ${age} years old.`
// Java:
String result = String.format("Hello, %s! You are %d years old.", name, age);

// Common methods (compared with JS)
greeting.length();           // JS: greeting.length (note the parentheses)
greeting.toUpperCase();      // JS: greeting.toUpperCase()
greeting.contains("ell");    // JS: greeting.includes("ell")
greeting.startsWith("He");   // JS: greeting.startsWith("He")
greeting.indexOf("l");       // JS: greeting.indexOf("l")
greeting.substring(1, 3);    // JS: greeting.substring(1, 3) → "el"
greeting.replace("l", "r");  // JS: greeting.replaceAll("l", "r")
greeting.trim();             // JS: greeting.trim()
greeting.isEmpty();          // JS: greeting.length === 0
greeting.split(",");         // JS: greeting.split(",")
greeting.equals("Hello");    // ⚠️ Java uses .equals() to compare string content! Not ==

⚠️ Most common pitfall: In Java, == compares references (addresses), not values. String comparison must use .equals():

java
String a = new String("hello");
String b = new String("hello");
a == b       // false! compares reference addresses
a.equals(b)  // true! compares content

1.4 Arrays

java
// Declaration and initialization
int[] numbers = {1, 2, 3, 4, 5};        // direct initialization
String[] names = new String[3];           // specify length, elements default to null
int[] scores = new int[]{90, 85, 78};    // alternative syntax

// Access and modify
int first = numbers[0];       // same as JS
numbers[2] = 99;              // same as JS
int length = numbers.length;  // ⚠️ No parentheses! It's a property, not a method

// Iteration
for (int i = 0; i < numbers.length; i++) {
    System.out.println(numbers[i]);
}

// Enhanced for loop (similar to JS's for...of)
for (int num : numbers) {
    System.out.println(num);
}

// Multi-dimensional arrays
int[][] matrix = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

Note for frontend developers: Java arrays have a fixed length — you can't push / pop like JS arrays. For dynamically-sized collections, use ArrayList (see below).

2. Control Flow

2.1 Conditional Statements

java
// if-else: exactly the same as JS
if (age >= 18) {
    System.out.println("Adult");
} else if (age >= 12) {
    System.out.println("Teenager");
} else {
    System.out.println("Child");
}

// Ternary operator: same as JS
String status = age >= 18 ? "Adult" : "Minor";

// switch (traditional form)
switch (day) {
    case 1:
        System.out.println("Monday");
        break;   // ⚠️ break is required, otherwise execution falls through to the next case
    case 2:
        System.out.println("Tuesday");
        break;
    default:
        System.out.println("Other");
}

// switch expression (Java 14+, more modern syntax)
String dayName = switch (day) {
    case 1 -> "Monday";
    case 2 -> "Tuesday";
    case 3 -> "Wednesday";
    default -> "Other";
};

2.2 Loops

java
// for loop: same as JS
for (int i = 0; i < 10; i++) {
    if (i == 5) break;
    if (i == 3) continue;
    System.out.println(i);
}

// while: same as JS
while (condition) {
    // ...
}

// do-while: same as JS
do {
    // ...
} while (condition);

3. Object-Oriented Programming (OOP)

This is the core of Java and the area with the biggest differences from JS. JS's class is just syntactic sugar; Java's class is a first-class citizen of the language.

3.1 Classes and Objects

java
// Defining a class
public class User {
    // Fields (similar to JS class properties)
    private String name;      // private = cannot be accessed from outside
    private int age;
    private String email;

    // Constructor (similar to JS's constructor)
    public User(String name, int age) {
        this.name = name;     // this is conceptually similar to JS's this
        this.age = age;
        this.email = "";      // default value
    }

    // Methods
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getEmail() {
        return email;
    }

    // Similar to JS's toString()
    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + "}";
    }
}

// Usage
User user = new User("Alice", 25);
System.out.println(user.getName());   // "Alice"
System.out.println(user);              // automatically calls toString()

Why so many getters/setters? This is a Java coding convention (the JavaBean specification). In legacy projects you'll see tons of getXxx() / setXxx() methods. Kotlin's property syntax eliminates this boilerplate.

3.2 Access Modifiers

ModifierWithin ClassSame PackageSubclassAnywhereJS/TS Equivalent
publicpublic
protectedprotected
(no modifier)No direct equivalent
privateprivate

In legacy projects, private + getter/setter is the most common pattern, known as "encapsulation."

3.3 Inheritance

java
// Parent class
public class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void speak() {
        System.out.println(name + " makes a sound");
    }
}

// Child class: use extends (same as JS's class extends)
public class Dog extends Animal {
    private String breed;

    public Dog(String name, String breed) {
        super(name);          // call parent constructor (similar to JS's super())
        this.breed = breed;
    }

    @Override                 // override parent method (similar to JS method overriding)
    public void speak() {
        System.out.println(name + " barks");
    }

    public String getBreed() {
        return breed;
    }
}

Differences from JS:

  • Java only supports single inheritance (a class can only extends one parent class)
  • A class marked final cannot be subclassed (no direct equivalent in JS)
  • The @Override annotation is not required, but strongly recommended — the compiler will check for you

3.3.1 Polymorphism

Polymorphism is one of the three pillars of OOP (encapsulation, inheritance, polymorphism). Simply put: the same method call produces different behavior depending on the object:

java
// Parent reference pointing to child objects
Animal animal1 = new Dog("Rex", 3, "Golden Retriever");
Animal animal2 = new Cat("Whiskers", 2, true);

// Same method call — runtime determines which version to execute based on actual type
animal1.speak();  // outputs "Rex barks" (actually calls Dog.speak)
animal2.speak();  // outputs "Whiskers makes a sound...meow" (actually calls Cat.speak)

// Frontend analogy: TS interface + implementations
// interface Animal { speak(): void; }
// class Dog implements Animal { speak() { console.log('Woof!'); } }
// class Cat implements Animal { speak() { console.log('Meow!'); } }
// const animals: Animal[] = [new Dog(), new Cat()];
// animals.forEach(a => a.speak());  // each outputs differently

// Polymorphism in Android — OnClickListener pattern
View.OnClickListener listener = new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // v could be Button, ImageView, TextView, etc.
        // Runtime type determines the actual behavior
    }
};

Type casting: Polymorphism is often combined with type checking:

java
if (animal instanceof Dog) {
    Dog dog = (Dog) animal;   // downcast
    dog.getBreed();
}
// Java 16+ pattern matching (more concise)
if (animal instanceof Dog dog) {
    dog.getBreed();  // automatic casting
}

3.4 Interfaces

Interfaces are Java's way of defining "contracts," similar to TypeScript's interface:

java
// Defining an interface (similar to TS: interface Clickable { void onClick(); String getLabel(); })
public interface Clickable {
    void onClick();
    String getLabel();

    // Java 8+: default methods (with implementation)
    default void onDoubleClick() {
        onClick();
        onClick();
    }
}

// Implementing an interface (use implements, similar to TS's class Xxx implements Clickable)
public class Button implements Clickable {
    private String label;

    public Button(String label) {
        this.label = label;
    }

    @Override
    public void onClick() {
        System.out.println("Button clicked: " + label);
    }

    @Override
    public String getLabel() {
        return label;
    }
}

// A class can implement multiple interfaces (compensating for single inheritance)
public class ImageButton extends ImageView implements Clickable, LongPressable {
    // Must implement all abstract methods from Clickable and LongPressable
}

Why are interfaces important? Java doesn't allow multiple inheritance, but it does allow implementing multiple interfaces. When you see code like implements Serializable, Parcelable, OnClickListener in legacy projects, it means the class adheres to multiple contracts simultaneously.

3.5 Abstract Classes

java
// Abstract class: cannot be instantiated directly, must be subclassed
public abstract class Shape {
    protected String color;

    public Shape(String color) {
        this.color = color;
    }

    // Abstract method: no body — subclasses must implement it
    public abstract double getArea();

    // Concrete method: has implementation, subclasses can use it directly
    public String getColor() {
        return color;
    }
}

public class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

// Shape shape = new Shape("red");     // ❌ Compile error — cannot instantiate an abstract class
Shape circle = new Circle("red", 5.0); // ✅ Instantiate via a subclass

3.6 Static Members

java
public class MathUtils {
    // Static field: belongs to the class itself, not to any instance
    public static final double PI = 3.14159265;

    // Static method: can be called without creating an instance
    public static int add(int a, int b) {
        return a + b;
    }

    public static int max(int a, int b) {
        return a > b ? a : b;
    }
}

// Usage (similar to JS: Math.PI, Math.max())
double area = MathUtils.PI * r * r;
int sum = MathUtils.add(3, 5);

Frontend comparison: Math.PI, Array.isArray(), and Object.keys() in JS are all examples of static members.

4. Collections Framework

Java's collections framework is richer than JS's Array / Map / Set. This is the part you'll encounter most frequently in legacy projects.

4.1 List (Corresponds to JS Array)

java
import java.util.ArrayList;
import java.util.List;

// Declaration (program to the interface — recommended)
List<String> names = new ArrayList<>();

// Add elements (similar to JS push)
names.add("Alice");
names.add("Bob");
names.add("Charlie");

// Get elements
String first = names.get(0);        // similar to JS: names[0]

// Modify elements
names.set(1, "Bobby");              // similar to JS: names[1] = "Bobby"

// Remove elements
names.remove("Charlie");            // remove by value
names.remove(0);                     // remove by index

// Size
int size = names.size();             // similar to JS: names.length

// Contains
boolean has = names.contains("Alice"); // similar to JS: names.includes("Alice")

// Iteration
for (String name : names) {          // similar to JS: for (const name of names)
    System.out.println(name);
}

// Stream API (Java 8+, similar to JS array method chaining)
List<String> filtered = names.stream()
    .filter(name -> name.startsWith("A"))      // similar to .filter()
    .map(String::toUpperCase)                   // similar to .map()
    .sorted()                                   // similar to .sort()
    .toList();                                  // collect into a List

// Searching
boolean anyMatch = names.stream().anyMatch(n -> n.length() > 2); // similar to .some()
boolean allMatch = names.stream().allMatch(n -> n.length() > 0); // similar to .every()
Optional<String> found = names.stream().findFirst();              // similar to .find()

4.2 Map (Corresponds to JS Map / Object)

java
import java.util.HashMap;
import java.util.Map;

// Creation (similar to JS: const user = new Map() or const user = {})
Map<String, Object> user = new HashMap<>();

// Add/modify key-value pairs
user.put("name", "Alice");
user.put("age", 25);
user.put("email", "alice@example.com");

// Get values
String name = (String) user.get("name");    // type casting required!
Integer age = (Integer) user.get("age");

// Get with default value (similar to JS: obj.key ?? defaultValue)
String phone = (String) user.getOrDefault("phone", "Not set");

// Check if key exists
boolean hasName = user.containsKey("name");  // similar to JS: "name" in obj

// Remove
user.remove("email");

// Iteration
for (Map.Entry<String, Object> entry : user.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

// Typed Map (recommended — avoids type casting)
Map<String, String> config = new HashMap<>();
config.put("theme", "dark");
config.put("lang", "en-US");
String theme = config.get("theme");  // no casting needed

4.3 Set (Corresponds to JS Set)

java
import java.util.HashSet;
import java.util.Set;

Set<String> tags = new HashSet<>();
tags.add("java");
tags.add("android");
tags.add("java");     // duplicate — will not be added

int size = tags.size();               // 2
boolean hasJava = tags.contains("java"); // true

for (String tag : tags) {
    System.out.println(tag);
}

4.4 Collection Type Selection Guide

RequirementUseJS Equivalent
Ordered list, allows duplicatesArrayListArray
Key-value pairsHashMapMap / {}
Unique values onlyHashSetSet
Ordered key-value pairsLinkedHashMapNo direct equivalent
Sorted key-value pairsTreeMapNo direct equivalent
Thread-safe listCopyOnWriteArrayListNo direct equivalent

Common Stream API Pitfalls

java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// ❌ Wrong: a Stream can only be consumed once
Stream<String> stream = names.stream();
stream.forEach(System.out::println);
stream.filter(s -> s.startsWith("A")); // IllegalStateException: stream has already been operated upon

// ✅ Correct: recreate the stream when needed
names.stream().forEach(System.out::println);
List<String> aNames = names.stream()
    .filter(s -> s.startsWith("A"))
    .collect(Collectors.toList());

// ❌ Wrong: modifying an external variable inside a stream
int totalLength = 0;
names.forEach(name -> totalLength += name.length()); // compile error

// ✅ Correct: use collect to aggregate
int total = names.stream().mapToInt(String::length).sum();

// ⚠️ Note: parallel streams are uncommon on the Android main thread and can cause thread-safety issues
names.parallelStream().forEach(...); // use with caution
ScenarioRecommendedNot Recommended
Iterate oncelist.forEach(...)manual for loop (also fine)
filter + map + collectstream().filter().map().collect()multiple loops
sum / max / minmapToInt().sum() / max()manual accumulation
Android main threadavoid heavy Stream operationsprocessing large data on main thread

5. Exception Handling

java
// Similar to JS's try...catch...finally
try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.err.println("Math error: " + e.getMessage());
} catch (Exception e) {
    System.err.println("Other error: " + e.getMessage());
} finally {
    // Always executes regardless of exception (same as JS)
    System.out.println("Clean up resources");
}

// Throwing exceptions
public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("Age cannot be negative");
    }
    this.age = age;
}

// Multi-catch (Java 7+)
try {
    // ...
} catch (IOException | SQLException e) {
    System.err.println("Operation failed: " + e.getMessage());
}

Key differences from JS:

  • Java exceptions are divided into checked exceptions and unchecked exceptions
  • Checked exceptions must be declared with throws or handled with try-catch, otherwise the code won't compile
  • You'll see a lot of try-catch blocks in legacy projects, many of which exist to handle checked exceptions
java
// Checked exception: must be declared in the method signature
public void readFile(String path) throws IOException {
    // If the caller doesn't handle IOException, compilation will fail
    BufferedReader reader = new BufferedReader(new FileReader(path));
    String line = reader.readLine();
    reader.close();
}

// Unchecked exception: no declaration needed (subclass of RuntimeException)
public void divide(int a, int b) {
    // ArithmeticException is unchecked — no throws needed
    System.out.println(a / b);
}

Exception Handling Best Practices

java
// ✅ Prefer catching specific exceptions and never swallow them silently
try {
    processFile(path);
} catch (IOException e) {
    Log.e(TAG, "Failed to read file: " + path, e);  // log full stack trace
    throw new BusinessException("Unable to load config", e); // wrap and rethrow
}

// ❌ Don't do this: swallowed exceptions are hard to debug
try {
    processFile(path);
} catch (Exception e) {
    // nothing here!
}

// ✅ Use try-with-resources to auto-close resources
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
    // read operations
} catch (IOException e) {
    Log.e(TAG, "IO error", e);
}

// ✅ In Android, handle async callback exceptions on the main thread
new Thread(() -> {
    try {
        String result = fetchFromNetwork();
        runOnUiThread(() -> textView.setText(result));
    } catch (IOException e) {
        runOnUiThread(() -> Toast.makeText(this, "Network error", Toast.LENGTH_SHORT).show());
    }
}).start();
Anti-patternProblemRecommended
catch (Exception e) { e.printStackTrace(); }Logs may be invisible in productionUse Log.e(TAG, msg, e) or report
catch (Exception e) {}Silent failure, hard to locateAt least log or rethrow
return inside finallySwallows exceptionsAvoid returning in finally
Catch all exceptions on UI threadMay hide ANR root causeDistinguish recoverable vs fatal

6. Lambda Expressions and Functional Programming (Java 8+)

Java 8 introduced Lambdas, allowing Java to write functional-style code like JS:

java
// JS:  const add = (a, b) => a + b;
// Java:
// Note: Java Lambdas must be assigned to a functional interface (an interface with exactly one abstract method)

// Using built-in functional interfaces
import java.util.function.*;

Function<String, Integer> strLength = s -> s.length();
// similar to JS: const strLength = (s) => s.length;

Consumer<String> printer = s -> System.out.println(s);
// similar to JS: const printer = (s) => console.log(s);

Predicate<Integer> isPositive = n -> n > 0;
// similar to JS: const isPositive = (n) => n > 0;

Supplier<Double> random = () -> Math.random();
// similar to JS: const random = () => Math.random();

// Using with collections (most common scenario)
List<String> names = List.of("Alice", "Bob", "Charlie", "Dave");

names.stream()
    .filter(name -> name.length() > 3)      // Predicate
    .map(String::toUpperCase)                // Function (method reference)
    .forEach(System.out::println);           // Consumer

// Sorting (similar to JS sort)
names.sort((a, b) -> a.length() - b.length());
// or shorthand:
names.sort(Comparator.comparingInt(String::length));

Method References

java
// When a Lambda body just calls a method, you can use a method reference as shorthand
// s -> System.out.println(s)  is equivalent to  System.out::println
// s -> s.length()             is equivalent to  String::length
// () -> new ArrayList<>()     is equivalent to  ArrayList::new

Optional (Java 8+)

Optional is used to explicitly express that a value may be absent, avoiding scattered null checks:

java
// JS/TS analogy: optional chaining + nullish coalescing
// const name = user?.name ?? "Anonymous";

Optional<User> userOpt = findUserById(1);
String name = userOpt.map(User::getName)
                      .orElse("Anonymous");

// Don't do this (defeats the purpose)
Optional<User> user = Optional.ofNullable(getUser()); // ✅ OK to wrap
User u = user.get(); // ❌ may throw NoSuchElementException

// Recommended usage
userOpt.ifPresent(u -> System.out.println(u.getName()));

Android note: Optional is natively supported only on Android API 24+. For legacy projects (minSdk < 24), use Guava's Optional or stick to explicit null checks.

7. Generics

Generics are similar to TypeScript generics, used to create reusable, type-safe code:

java
// Similar to TS: class Box<T> { private value: T; ... }
public class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

// Usage
Box<String> stringBox = new Box<>("hello");
String val = stringBox.getValue();  // no casting needed

Box<Integer> intBox = new Box<>(42);
int num = intBox.getValue();

// Generic methods
public <T> void printArray(T[] array) {
    for (T item : array) {
        System.out.println(item);
    }
}

// Generic wildcards (you'll see these in legacy projects)
// ? extends Number  → upper bound, must be Number or a subclass
// ? super Integer   → lower bound, must be Integer or a superclass
// ?                 → unbounded, any type
public void process(List<? extends Number> numbers) {
    for (Number n : numbers) {
        System.out.println(n.doubleValue());
    }
}

Generic Type Erasure and PECS

Type Erasure

Java generics are erased to Object (or the upper bound) after compilation, for backward compatibility with older JVMs:

java
// Source
List<String> names = new ArrayList<>();

// After compilation, roughly equivalent to
List names = new ArrayList();
String name = (String) names.get(0);  // compiler inserts cast

Frontend note: TypeScript types are also erased at compile time, but Java's erasure is stricter — you cannot directly retrieve List<String>'s type parameter at runtime. That's why Gson needs a TypeToken to parse List<User>.

PECS Principle

PECS = Producer-Extends, Consumer-Super:

java
// Producer: only reads data, use ? extends T
public void printNumbers(List<? extends Number> numbers) {
    for (Number n : numbers) {
        System.out.println(n);
    }
    // numbers.add(1); // ❌ compile error, cannot guarantee type safety
}

// Consumer: only writes data, use ? super T
public void addIntegers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    // Integer i = list.get(0); // ❌ compile error, can only read as Object
}
WildcardMeaningUse Case
? extends TUpper bound, T or subclassRead-only (Producer)
? super TLower bound, T or superclassWrite-only (Consumer)
?UnboundedNeither read nor write specific type

8. Enums

java
// Similar to TS: enum Status { PENDING, ACTIVE, COMPLETED }
public enum Status {
    PENDING,
    ACTIVE,
    COMPLETED,
    CANCELLED
}

// Usage
Status currentStatus = Status.ACTIVE;

switch (currentStatus) {
    case PENDING:
        System.out.println("Waiting");
        break;
    case ACTIVE:
        System.out.println("In progress");
        break;
    case COMPLETED:
        System.out.println("Done");
        break;
    case CANCELLED:
        System.out.println("Cancelled");
        break;
}

// Enum with data (Java-specific, more powerful than TS enums)
public enum Priority {
    LOW(1, "Low"),
    MEDIUM(2, "Medium"),
    HIGH(3, "High");

    private final int level;
    private final String label;

    Priority(int level, String label) {
        this.level = level;
        this.label = label;
    }

    public int getLevel() { return level; }
    public String getLabel() { return label; }
}

Priority high = Priority.HIGH;
System.out.println(high.getLabel());  // "High"
System.out.println(high.getLevel());  // 3

9. Annotations

Annotations are metadata tags in Java code, similar to TypeScript decorators:

java
// Common built-in annotations
@Override          // marks a method that overrides a parent method
@Deprecated        // marks an API as outdated
@SuppressWarnings("unchecked")  // suppresses compiler warnings

// Common annotations in Android development
@NonNull           // parameter/return value must not be null
@Nullable          // parameter/return value may be null
@SuppressLint("...")  // suppresses Android Lint warnings
@RequiresApi(21)   // requires a specific API level

// Custom annotations you'll see in legacy projects
@BindView(R.id.tv_name)     // ButterKnife
@Inject                      // Dagger/Hilt
@SerializedName("user_name") // Gson
@Entity(tableName = "users") // Room

10. I/O Basics

java
import java.io.*;

// Reading a file
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}
// try-with-resources automatically closes resources (similar to JS's using proposal)

// Writing a file
try (FileWriter writer = new FileWriter("output.txt")) {
    writer.write("Hello, World!");
} catch (IOException e) {
    e.printStackTrace();
}

Note: In Android development, you rarely use raw Java I/O directly. Networking uses OkHttp/Retrofit, databases use Room, and file operations use APIs provided by Context. However, understanding I/O basics helps when reading legacy project code.

11. Multithreading Fundamentals

JS is single-threaded (handling async via the event loop); Java is multi-threaded. This is a key concept for frontend developers to understand.

11.1 Thread and Runnable

java
// Approach 1: Extend Thread (not recommended — Java single inheritance limitation)
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Running in thread: " + Thread.currentThread().getName());
    }
}
new MyThread().start();

// Approach 2: Implement Runnable (recommended — more flexible)
// Frontend analogy: similar to setTimeout(() => { ... }, 0) but truly parallel
Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("Thread: " + Thread.currentThread().getName());
    }
};
new Thread(task).start();

// Approach 3: Lambda shorthand (most common)
new Thread(() -> {
    System.out.println("Thread running");
}).start();

11.2 Main Thread vs Worker Threads

java
// ⚠️ Core Android rules:
// - Main thread (UI thread): handles UI updates and user interaction; never run heavy work here
// - Worker threads: execute network requests, DB operations, file I/O
// - Worker threads CANNOT directly update UI! Must switch back to main thread

// Common pattern in an Activity
new Thread(() -> {
    // Worker thread: perform heavy work
    String result = fetchDataFromNetwork();

    // Switch back to main thread to update UI
    runOnUiThread(() -> {
        textView.setText(result);
    });
}).start();

11.3 Handler Mechanism (Android-specific)

java
// Handler is the core mechanism for inter-thread communication in Android
// Frontend analogy: similar to postMessage + onmessage, but more complex

// Create a Handler on the main thread
Handler mainHandler = new Handler(Looper.getMainLooper());

// Send messages from a worker thread to the main thread
new Thread(() -> {
    String data = doHeavyWork();

    // Approach 1: post a Runnable
    mainHandler.post(() -> {
        textView.setText(data);  // runs on main thread
    });

    // Approach 2: send a Message
    Message msg = mainHandler.obtainMessage();
    msg.what = 1;
    msg.obj = data;
    mainHandler.sendMessage(msg);
}).start();

11.4 ExecutorService (Thread Pool)

java
import java.util.concurrent.*;

// Create a thread pool (frontend analogy: like Promise concurrency control)
ExecutorService executor = Executors.newFixedThreadPool(4);  // fixed 4 threads

// Submit a task
Future<String> future = executor.submit(() -> {
    Thread.sleep(2000);  // simulate heavy work
    return "Task complete";
});

// Get result (blocks current thread)
try {
    String result = future.get();  // similar to JS: await promise
    System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

// Shut down the pool
executor.shutdown();

// CompletableFuture (Java 8+, similar to Promise chains)
CompletableFuture.supplyAsync(() -> fetchUser())
    .thenApply(user -> user.getName())        // like .then()
    .thenAccept(name -> System.out.println(name))
    .exceptionally(e -> { e.printStackTrace(); return null; }); // like .catch()

11.5 Async Patterns Evolution in Android

java
// 1️⃣ Legacy: AsyncTask (deprecated, but you'll see it in old code)
public class LoadDataTask extends AsyncTask<String, Void, String> {
    @Override
    protected String doInBackground(String... params) {
        return fetchData(params[0]);  // worker thread
    }

    @Override
    protected void onPostExecute(String result) {
        textView.setText(result);      // main thread
    }
}
new LoadDataTask().execute("https://api.example.com");

// 2️⃣ Transitional: RxJava (reactive programming, like RxJS)
Observable.fromCallable(() -> fetchData(url))
    .subscribeOn(Schedulers.io())          // execute on IO thread
    .observeOn(AndroidSchedulers.mainThread())  // observe on main thread
    .subscribe(
        result -> textView.setText(result),  // success
        error -> showError(error)            // failure
    );

// 3️⃣ Modern: Kotlin Coroutines (preferred for new projects)
// viewModelScope.launch {
//     val result = withContext(Dispatchers.IO) { fetchData(url) }
//     textView.text = result
// }

Frontend developer comparison:

JS Async PatternJava/Android Equivalent
setTimeout / setIntervalHandler.postDelayed()
PromiseFuture / CompletableFuture
async/awaitKotlin Coroutines / CompletableFuture.get()
Web WorkerThread / ExecutorService
postMessageHandler.sendMessage()
RxJSRxJava

12. Android-Specific Java Knowledge

12.1 Context

Context is the most important concept in Android — nearly every operation requires it:

java
// Context is the gateway to system resources and app information
// Frontend analogy: like window + document + app config combined

// Common Context sources
Activity activity = this;                  // Activity IS a Context
Context appContext = getApplicationContext(); // app-level Context
Context context = getContext();              // get Context from a View

// Common uses
Toast.makeText(context, "Hint message", Toast.LENGTH_SHORT).show();  // show toast
context.startActivity(new Intent(context, DetailActivity.class)); // navigate
context.getSharedPreferences("prefs", Context.MODE_PRIVATE);     // store config
context.getResources().getString(R.string.app_name);              // get resources

12.2 Serializable vs Parcelable

java
// Serializable: standard Java serialization (simple but slow)
// Frontend analogy: similar to JSON.stringify / JSON.parse
public class User implements Serializable {
    private String name;
    private int age;
    // automatically supports serialization, no extra code needed
}

// Passing Serializable objects
Intent intent = new Intent(this, DetailActivity.class);
intent.putExtra("user", user);

// Parcelable: Android-specific serialization (fast but verbose)
public class User implements Parcelable {
    private String name;
    private int age;

    protected User(Parcel in) {
        name = in.readString();
        age = in.readInt();
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeInt(age);
    }

    public static final Creator<User> CREATOR = new Creator<User>() {
        @Override
        public User createFromParcel(Parcel in) { return new User(in); }
        @Override
        public User[] newArray(int size) { return new User[size]; }
    };

    @Override
    public int describeContents() { return 0; }
}

Rule of thumb: Use Parcelable for passing data between Activities/Fragments; use Serializable or JSON for persistent storage.

12.3 Anonymous Inner Classes

Legacy Android projects heavily use anonymous inner classes (frontend developers will recognize these as callback patterns):

java
// Frontend analogy: element.addEventListener('click', function(e) { ... })

// Button click listener
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // handle click
    }
});
// Lambda shorthand (Java 8+)
button.setOnClickListener(v -> {
    // handle click
});

// Text change listener (frontend analogy: input.addEventListener('input', ...))
editText.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        // handle text change
    }

    @Override
    public void afterTextChanged(Editable s) {}
});

12.4 Common Android Annotations in Detail

java
// @Override: method overrides parent (compile-time check)
@Override
protected void onCreate(Bundle savedInstanceState) { ... }

// @NonNull / @Nullable: null-safety annotations
public void setName(@NonNull String name) { ... }   // warning if caller passes null
public @Nullable String getNickname() { ... }        // return value may be null

// @SuppressLint: suppress specific Lint warnings
@SuppressLint("SetTextI18n")   // suppress hardcoded string warning
textView.setText("Hello " + name);

// @RequiresApi: marks APIs requiring specific OS versions
@RequiresApi(api = Build.VERSION_CODES.O)
public void useNotificationChannel() { ... }

// @Deprecated: marks outdated APIs
@Deprecated
public void oldMethod() { ... }

// Third-party library annotations (common in legacy projects)
@SerializedName("user_name")    // Gson: JSON field mapping
@Entity(tableName = "users")    // Room: database table mapping
@Inject                          // Dagger/Hilt: dependency injection
@BindView(R.id.tv_name)         // ButterKnife: View binding

12.5 The R File and Resource References

java
// R is an auto-generated resource index, similar to importing assets in frontend
// R.id.xxx       → View IDs in layouts
// R.string.xxx   → strings in strings.xml
// R.layout.xxx   → layout files
// R.drawable.xxx → image resources
// R.color.xxx    → color values

// In an Activity
setContentView(R.layout.activity_main);       // load layout
TextView tv = findViewById(R.id.tv_title);    // find View
String appName = getString(R.string.app_name); // get string
int color = getColor(R.color.primary);         // get color

12.6 Lifecycle and Callback Patterns

java
// Android components interact with the system via lifecycle callbacks
// Frontend analogy: similar to React's useEffect + component lifecycle

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);  // must call parent method
        // like React: componentDidMount initialization
    }

    @Override
    protected void onResume() {
        super.onResume();
        // page is visible and interactive (like page focus)
    }

    @Override
    protected void onPause() {
        super.onPause();
        // page loses focus (like page blur)
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // page destroyed (like React: componentWillUnmount)
    }
}

Common questions from frontend developers:

Frontend ConceptAndroid Equivalent
windowContext
document.getElementById()findViewById()
addEventListenersetOnXxxListener()
localStorageSharedPreferences
fetch()OkHttp / Retrofit
React component lifecycleActivity lifecycle
Router / routingIntent

13. Common Design Patterns at a Glance

You'll frequently encounter the following patterns in legacy projects:

Singleton Pattern

java
// Similar to a module singleton in JS
public class DatabaseHelper {
    private static DatabaseHelper instance;

    private DatabaseHelper() {}  // private constructor

    public static synchronized DatabaseHelper getInstance() {
        if (instance == null) {
            instance = new DatabaseHelper();
        }
        return instance;
    }

    // Business methods...
}

// Usage
DatabaseHelper db = DatabaseHelper.getInstance();

Observer Pattern

java
// Ubiquitous in Android: OnClickListener, TextWatcher, and LiveData are all observer pattern
public interface OnDataChangeListener {
    void onDataChanged(String newData);
}

public class DataManager {
    private List<OnDataChangeListener> listeners = new ArrayList<>();

    public void addListener(OnDataChangeListener listener) {
        listeners.add(listener);
    }

    public void notifyDataChanged(String data) {
        for (OnDataChangeListener listener : listeners) {
            listener.onDataChanged(data);
        }
    }
}

Builder Pattern

java
// Similar to JS chaining: new Request().url("...").method("GET").build()
AlertDialog dialog = new AlertDialog.Builder(context)
    .setTitle("Notice")
    .setMessage("Are you sure you want to exit?")
    .setPositiveButton("OK", (d, w) -> finish())
    .setNegativeButton("Cancel", null)
    .create();
dialog.show();

14. Things Java Doesn't Have That JS Does

JS FeatureJava Alternative
undefinedDoesn't exist — only null
== loose comparisonJava's == is strict reference comparison; use .equals() for value comparison
Destructuring assignmentDoesn't exist — assign one by one
Spread operator ...Doesn't exist — copy manually
Optional chaining ?.Doesn't exist — check manually: if (obj != null) { obj.xxx }
Nullish coalescing ??Doesn't exist — use ternary: obj != null ? obj : default
Promise / async-awaitJava uses CompletableFuture, ExecutorService; Android uses callbacks/coroutines
Built-in JSONRequires third-party libraries: Gson, Jackson, Moshi
typeofinstanceof or .getClass()
Array.isArray()obj instanceof List or obj.getClass().isArray()
Template literalsString.format() or StringBuilder

Quick Reference for Frontend Developers

A quick translation guide when reading Java code:

Java Code You SeeEquivalent JS/TS Mental Model
String name = "hi"const name = "hi"
new ArrayList<>()[]
new HashMap<>(){} or new Map()
.add(item).push(item)
.get(index)[index]
.size().length
.equals(other)=== other
instanceofinstanceof
nullnull (no undefined)
this.xxxthis.xxx
super.xxxsuper.xxx
staticstatic
finalconst (approximately)
@OverrideNo direct equivalent (method overriding is implicit)
try-catch-finallytry-catch-finally
implements Interfaceimplements Interface (TS)
extends Classextends Class
import com.xxx.Yyyimport { Yyy } from "com/xxx"
System.out.println()console.log()

Next Steps

Now that you've got a handle on Java fundamentals, move on to Module 03: Project Structure and Build System to learn how Android projects are organized and how the Gradle build system works.

A Java Android development tutorial for frontend developers