Java Fundamentals
A Complete 8-Chapter Programming Course
Table of Contents
- Getting Started
- Variables & Basic Types
- Operators & Control Flow
- Classes & Objects
- Inheritance & Polymorphism
- Interfaces & Abstract Classes
- Exception Handling
- Collections Framework I
Getting Started
Every language covered so far — C, C++, Rust — compiles source directly to native machine code, tied to one specific OS and CPU. Java does something structurally different, and this chapter is about that difference before anything else.
The JVM — A New Compilation Model
C, C++, and Rust all compile in one step: source → machine code, specific to the target platform. Java compiles to bytecode instead — an intermediate, platform-independent format — and the Java Virtual Machine (JVM) then interprets or compiles that bytecode to real machine code at runtime, on whatever platform it's actually running on. A genuinely different, two-stage model.
"Write Once, Run Anywhere"
The practical payoff: the exact same compiled .class bytecode file runs unmodified on Windows, Linux, or macOS, as long as each has its own JVM installed. Compare this against C/C++/Rust, where the source can be portable, but the compiled binary is not — recompiling separately for each target platform is required. Java shifts the portability boundary from source code to compiled bytecode.
javac and java
A two-command workflow: javac compiles .java source into .class bytecode; java then runs the JVM against that bytecode. Genuinely a two-step process, like c1-1's own compile-then-link model — but with a different split: not compile-then-link, but compile-then-run-on-a-VM.
JIT Compilation, Briefly
The JVM doesn't just interpret bytecode slowly forever. A Just-In-Time (JIT) compiler inside the JVM identifies "hot" code paths — the parts actually executed frequently — and compiles those specific parts to real machine code at runtime, for near-native performance where it matters most. A genuine, real engineering answer to "isn't interpreting bytecode slow?"
The Smallest Java Program
A real, immediate ceremony worth naming directly: unlike C/C++'s free-standing main() function (c1-1, cpp1-1), Java requires every piece of code, even the entry point, to live inside a class. There is no such thing as a free function in Java at all — a genuinely different, stricter OOP-first philosophy from the very first line.
Compiling & Running
A real, Java-specific compiler-enforced rule: the class name must match the filename exactly, for any public class.
| Concept | C / C++ / Rust | Java |
|---|---|---|
| Compilation target | native machine code, platform-specific | bytecode, platform-independent |
| Portability | source portable, binary is not | compiled bytecode itself is portable |
| Entry point | a free-standing main() function | must live inside a class — no free functions at all |
| File-to-class naming | no relationship required | public class name must match filename exactly |
javac itself rejects a file where a public class's name doesn't match the filename — a real, immediate compile error, worth knowing from the very first program written.
Coding Challenges
Write a Java program in a file named Greeting.java with a public class Greeting whose main method prints two separate lines using two separate System.out.println calls. Compile and run it.
📄 View solutionDeliberately name a file Wrong.java but declare the public class inside it as public class Right. Attempt to compile it with javac and report the exact error.
📄 View solutionExplain, in your own words, why "write once, run anywhere" is a genuinely different claim than C's own "the source code is portable" — what specifically is portable in each case, and what still needs to happen on the target machine either way.
📄 View solutionChapter 1 Quick Reference
- Java compiles to bytecode, not native machine code — the JVM runs it at runtime, on any platform with a JVM installed
javaccompiles.java→.class;javaruns the JVM against the compiled bytecode- The JIT compiler compiles "hot" bytecode paths to native code at runtime for near-native performance
- Every piece of Java code lives inside a class — there are no free-standing functions, unlike C/C++
- A
publicclass's name must match its filename exactly — enforced byjavacitself - Next chapter: variables and basic types — primitives vs. reference types, and boxing/unboxing
Variables & Basic Types
java1-1 showed every value living inside a class. This chapter shows the one deliberate exception — and the real trap it creates.
Primitive Types
Java has exactly eight primitives — byte, short, int, long, float, double, char, boolean — each with an exact, guaranteed size across every platform, per the JVM specification itself. This is genuinely Rust-like, not C-like: c1-2's own material established that C only guarantees a minimum size for int. Java's primitives agree with Rust here, not with C.
Reference Types
Everything else — objects, arrays, String — is a reference type, stored as a reference to heap-allocated data, not the raw value itself. Conceptually similar to C's own pointer/value distinction (c1-7), but Java references cannot be manipulated with pointer arithmetic at all — no c1-7-style arithmetic; dereferencing is always implicit.
Primitives vs. Objects — Why Both Exist
A real, practical reason: primitives are stored directly — on the stack, or inline within an object — avoiding the overhead a full object/reference would carry for something as simple as an int. A deliberate performance-vs-uniformity tradeoff; making everything a full object, as some "purer" OOP languages do, has real, measurable overhead.
Boxing & Unboxing
Integer is the object wrapper class for the primitive int. Autoboxing (a primitive silently becoming a wrapper object) and auto-unboxing (the reverse) happen automatically in many contexts since Java 5.
A Real Boxing Gotcha — Integer Caching
A genuinely surprising, real, documented JVM behavior: Java caches small boxed Integer values, -128 to 127. Trips up even experienced Java programmers, since small test values often happen to fall inside the cached range and hide the bug.
== vs. .equals()
The general rule this gotcha is an instance of: == compares references for objects (are these the same object in memory) but compares values for primitives (since there's no reference to compare at all). .equals() is the method for genuine value comparison between objects.
| Concept | C (c1-2) | Rust | Java |
|---|---|---|---|
| Integer type size | minimum only, platform-dependent | exact, guaranteed | exact, guaranteed |
| Value vs. reference default | everything is a value; pointers are explicit | explicit ownership/borrowing | primitives are values; everything else is a reference |
| Object equality operator | n/a — no objects | == is value equality (PartialEq) | == is reference identity for objects, not value equality |
== for reference identity checks (or genuine primitive comparisons) — .equals() is the everyday habit for comparing what two objects actually contain.
== on boxed Integer values outside -128..127 will not reliably behave the way small test values might suggest — a genuine, real source of confusing bugs that only manifest once real data exceeds the cached range.
Coding Challenges
Declare two Integer variables both set to 100, compare them with ==, then declare two more both set to 500 and compare those with ==. Print both results and explain the difference.
📄 View solutionRepeat Challenge 1's 500-value comparison, but use .equals() instead of ==. Report the result and explain why it now behaves correctly regardless of the cache range.
📄 View solutionExplain why Java's primitive types are described as "Rust-like" rather than "C-like" in terms of size guarantees, tying your answer directly back to c1-2's own material on C's int.
📄 View solutionChapter 2 Quick Reference
- Eight primitives — exact, guaranteed sizes, unlike C's own minimum-only guarantee
- Everything else is a reference type — no pointer arithmetic, unlike C's pointers
- Autoboxing/auto-unboxing convert transparently between a primitive and its wrapper class (e.g. int/Integer)
- Integer caching (-128..127) means small boxed values happen to share references — a real, documented gotcha
==is reference identity for objects;.equals()is genuine value comparison — use.equals()by default- Next chapter: operators, control flow, switch expressions, and pattern matching
Operators & Control Flow
java1-2 covered the eight primitives and the reference/value split. This chapter puts them to work — most of it will feel immediately familiar coming from C, C++, or Rust, with two genuine exceptions worth slowing down for.
Standard Operators
Arithmetic (+ - * / %), relational (== != < > <= >=), logical (&& || !), and compound assignment (+= -= *= /=) all behave exactly as they do in C/C++. Nothing new to learn here — genuinely shared syntax across all four languages this site has covered.
Integer Division & the Common C-Style Trap
Division between two int values truncates toward zero, exactly the same trap c1-3 covered for C. Java doesn't fix this — it's the same behavior, inherited from the same C-family lineage, and it requires the same explicit cast to force floating-point division.
Traditional switch — Fallthrough, the Same Footgun
Java's traditional switch falls through by default, identical to C's own switch statement covered in c1-3 — omitting break is a genuine, real bug class in both languages, not just a Java quirk.
Switch Expressions — Java's Real Fix
Java 14 introduced switch expressions: the arrow form (->) has no fallthrough at all — each branch is isolated — and the whole expression can produce a value directly, assigned straight into result. Multi-statement branches use a block with yield to supply the value.
Pattern Matching — A Light Touch
Since Java 16, instanceof can bind the checked-and-cast value directly to a new variable in one step, removing the old two-step check-then-cast pattern. Java 21 extends this into switch itself, matching directly on an object's type. This course won't go deep on pattern matching — it's flagged here mainly so the syntax isn't a surprise when it appears in later chapters.
| Behavior | C (c1-3) | Java traditional switch | Java switch expression |
|---|---|---|---|
| Fallthrough between cases | yes, by default | yes, by default | no — never falls through |
| Produces a value directly | no | no | yes, via -> or yield |
| Multiple labels per branch | stacked case labels | stacked case labels | comma-separated on one line |
switch fallthrough isn't a Java-specific gotcha — it's the identical bug class c1-3 covered for C's own switch, inherited unchanged. Missing one break silently executes the next case's body too.
Coding Challenges
Write a program that divides 7 by 2 using int division, then again using an explicit cast to force floating-point division. Print both results and label which is truncated.
📄 View solutionWrite a traditional switch statement over an int month (1-12) that deliberately omits a break in one case to demonstrate fallthrough, then rewrite the same logic as a switch expression using the arrow form and show it no longer falls through.
📄 View solutionWrite a method that accepts an Object and uses instanceof pattern matching to print its length if it's a String, or its value doubled if it's an Integer, with no explicit cast in either branch.
📄 View solutionChapter 3 Quick Reference
- Arithmetic/relational/logical/compound-assignment operators are shared syntax with C/C++
- int/int division truncates toward zero — identical to C's own behavior (c1-3), fixed the same way: cast one operand to double
- Traditional switch falls through by default — the same real bug class as C's switch
- Switch expressions (->) never fall through and can produce a value directly; yield supplies a value from a multi-statement block
- instanceof pattern matching binds the cast value directly, removing the old check-then-cast two-step
- Next chapter: classes, objects, and Java's four access levels
Classes & Objects
Every program since java1-1 has lived inside a class out of necessity. This chapter explains why — fields, constructors, methods, and the access-control model that governs who can touch what.
Fields & Constructors
A constructor shares the class's exact name, has no return type, and runs once when new Account(...) is called. this refers to the current instance — needed here specifically because the parameter names shadow the field names.
Java's Four Access Levels
Where C++ has three access levels (public, protected, private), Java has a genuine fourth: package-private, the default when no modifier is written at all. A member with no modifier is visible to any class in the same package, but nowhere else — a level of granularity C++ has no direct equivalent for.
| Modifier | Visible from | C++ equivalent |
|---|---|---|
| public | anywhere | public |
| protected | same package + subclasses anywhere | protected (subclasses only, no package concept) |
| (none) — package-private | same package only | no direct equivalent |
| private | same class only | private |
Why a Fourth Level Exists
C++ has no built-in concept of a "package" the way Java does, so it has nothing to attach a package-scoped access level to in the first place. Java's package system gives classes in the same package a middle ground: more open than private, but without exposing a member to every caller everywhere the way public does. It's a genuinely different granularity, not just a naming difference.
Instance Methods vs. Static Methods
main, back in java1-1, was already static — this is why: a static method belongs to the class itself, not to any particular instance, which is exactly why the JVM can call main without ever constructing an object first.
private and every method as narrowly scoped as the design allows — widen to package-private, then protected, then public only when a real caller outside that scope genuinely needs access.
Coding Challenges
Write a class Rectangle with private width and height fields, a constructor that sets both using this, and a public method area() that returns their product.
📄 View solutionWrite a class with one package-private field (no modifier) and explain in a comment exactly which callers can and cannot access it, contrasting it with what private and public would each allow.
📄 View solutionWrite a static factory method inside a class called origin() that returns a new instance representing coordinates (0, 0), and explain why it must be static in order to be called before any instance exists.
📄 View solutionChapter 4 Quick Reference
- A constructor shares the class name exactly, has no return type, and runs once per new instance
- this disambiguates a field from a same-named parameter inside a constructor or method
- Java has four access levels: public, protected, package-private (the default, no modifier), and private — a genuine fourth level C++'s three don't have
- Package-private exists because Java has a real package concept C++ doesn't — a middle ground between private and public
- static methods belong to the class itself, not an instance — exactly why main can run with no object constructed first
- Next chapter: inheritance, polymorphism, and Java's single-inheritance-of-classes model
Inheritance & Polymorphism
java1-4 built one class in isolation. Real programs build hierarchies — this chapter covers how, and where Java deliberately draws a harder line than C++ ever did.
extends & Single Inheritance
A Java class can extends exactly one other class. This is a hard rule, not a convention — where C++ allows a class to inherit from several base classes at once (the exact mechanism behind cpp3-1's diamond problem), Java simply never lets that ambiguity arise in the first place.
super — Reaching Into the Parent
super(...) calls the superclass's constructor and must be the first statement in a subclass constructor if written explicitly. super.methodName() calls the superclass's version of an overridden method from inside the override itself.
Method Overriding — Virtual by Default
A genuine, real difference from C++: every Java instance method is virtual by default — dynamically dispatched based on the object's actual runtime type — unless explicitly marked final, private, or static. cpp1-7's own material required the explicit virtual keyword in C++ for this exact behavior; Java simply never offers a non-virtual instance method as the default at all.
Why Single Inheritance of Classes
Java's restriction to one extends per class is a structural choice, not a limitation nobody thought to lift — it's the same underlying goal cpp3-1's own virtual-inheritance fix pursued for C++, achieved here by never allowing a class to inherit state from more than one place to begin with. Behavior from multiple sources is still possible in Java — through interfaces, covered next chapter — but state never comes from more than one superclass.
| Behavior | C++ (cpp1-7 / cpp3-1) | Java |
|---|---|---|
| Classes a class can inherit from | multiple, directly | exactly one (extends) |
| Diamond problem | possible — needs virtual inheritance to fix | structurally impossible for state |
| Virtual dispatch default | opt-in via the virtual keyword | on by default for every instance method |
| Multiple sources of behavior | multiple inheritance | interfaces (next chapter) |
@Override doesn't change runtime behavior — it asks the compiler to verify the method actually overrides something in a superclass, catching a typo'd method signature (which would otherwise silently become an unrelated overload) at compile time instead of leaving a hard-to-notice bug.
super(...) explicitly, Java inserts a no-argument super() call automatically. This silently works right up until the superclass has no no-argument constructor at all — at which point the subclass fails to compile until super(...) is written explicitly with matching arguments.
Coding Challenges
Write a Shape class with a protected name field and an area() method returning 0.0, then a Circle subclass that extends it, calls super() in its constructor, and overrides area() correctly using @Override.
📄 View solutionStore a Circle in a variable declared as type Shape, call area() on it, and explain in a comment why the Circle version runs even though the variable's declared type is Shape — referencing what makes this possible by default in Java but not in C++.
📄 View solutionWrite a superclass with only a constructor that takes an argument (no no-arg constructor), then write a subclass whose constructor forgets to call super(...) explicitly. Show the resulting compile error and explain why it happens.
📄 View solutionChapter 5 Quick Reference
- extends allows exactly one superclass — Java never permits multiple inheritance of state
- super(...) calls the superclass constructor; super.method() calls the superclass's version of an overridden method
- Every instance method is virtual/dynamically-dispatched by default — no virtual keyword needed, unlike C++'s opt-in model
- @Override doesn't change behavior — it makes the compiler verify a real override exists, catching typo'd overloads
- Single inheritance of classes is Java's structural answer to the diamond problem cpp3-1 covered for C++
- Next chapter: interfaces and abstract classes — Java's own way to get behavior from multiple sources
Interfaces & Abstract Classes
java1-5 closed with a promise: state can only ever come from one superclass, but behavior can still come from multiple sources — through interfaces. This chapter delivers on that.
Interfaces — The Contract
An interface declares method signatures with no body and no instance fields — a pure contract. A class implements it and must supply real bodies for every abstract method it declares.
A Class Can implements Many Interfaces
This is exactly where "multiple sources of behavior" comes back from java1-5. extends is capped at one superclass; implements has no such cap at all — a single class can satisfy as many interface contracts as it genuinely needs to.
Default Methods (Java 8+)
Since Java 8, an interface method can carry a default body — implementing classes inherit it automatically and may override it if they need different behavior. This is a genuine convergence point with Rust: rust2-2's own trait default methods work the same way, behavior supplied by the contract itself without requiring every implementer to repeat it.
Abstract Classes
Unlike an interface, an abstract class can hold real fields, constructors, and fully concrete methods alongside abstract ones. It still follows java1-5's own single-inheritance rule — a class extends at most one abstract class, exactly the same as any other class.
Interface vs. Abstract Class — When to Use Which
An interface answers "can this do X?" with no shared state at all. An abstract class answers "is this genuinely a kind of Y?" and gets to share real implementation and state across every subclass. A class can implement many interfaces but extend only one abstract class — the choice usually follows from that asymmetry directly.
The Diamond Problem, Returning For Behavior Alone
Two interfaces can both supply conflicting default implementations of the same method — a real, narrow reappearance of cpp3-1's diamond problem, scoped to behavior only, since interfaces never carry state. Java doesn't guess which one wins: the implementing class is required to override the method itself and resolve the conflict explicitly, or the code simply fails to compile.
| Mechanism | C++ (cpp3-1) | Rust (rust2-2) | Java |
|---|---|---|---|
| Multiple inheritance of state | allowed — causes the diamond problem | never possible — structs don't inherit | never possible — single extends only |
| Multiple sources of behavior | multiple inheritance (same mechanism as state) | traits, no state carried | interfaces, no state carried |
| Conflicting default behavior | virtual inheritance resolves it structurally | compile error — must be resolved explicitly | compile error — must be overridden explicitly |
default method with the same signature, Java refuses to guess which one applies — the class must override that method itself, even if only to explicitly pick one interface's version via InterfaceName.super.methodName().
Coding Challenges
Write an interface Printable with an abstract method print() and a default method printTwice() that calls print() twice, then a class implementing Printable that only supplies print().
📄 View solutionWrite an abstract class Employee with a protected name field, a constructor, an abstract method pay(), and a concrete method summary() that calls pay(). Then write a Manager subclass that supplies pay().
📄 View solutionWrite two interfaces that each declare a default method with the identical signature but different bodies, then a class that implements both. Show the resulting compile error, then fix it by overriding the method explicitly.
📄 View solutionChapter 6 Quick Reference
- Interfaces declare method contracts with no state; a class can implements as many as it needs, unlike extends's single-superclass cap
- default methods (Java 8+) give an interface a real method body — a genuine convergence point with Rust's own trait default methods
- Abstract classes can hold state, constructors, and concrete methods alongside abstract ones — but still follow single inheritance
- Interface = "can do," no shared state; abstract class = "is a," shared state and implementation
- Conflicting default methods from two interfaces force an explicit override — Java never silently picks a winner
- Next chapter: exception handling — checked vs. unchecked exceptions
Exception Handling
Every prior chapter assumed things go right. This one covers what Java does when they don't — and a genuinely distinct position between two approaches already covered on this site.
try/catch/finally — The Basics
Code that might fail goes in try; a specific exception type is handled in catch; finally runs unconditionally, whether an exception was thrown or not.
The Exception Hierarchy
Every exception is a Throwable. Below that: Error (serious JVM-level problems, not meant to be caught) and Exception, which splits again into RuntimeException and everything else — that split is exactly where checked and unchecked diverge.
Checked Exceptions — Compiler-Enforced Acknowledgment
A checked exception — anything that's an Exception but not a RuntimeException, like IOException — must be either caught or declared with throws in the method signature. The compiler enforces this and refuses to compile otherwise. This is genuinely new territory versus C++: cpp2-6's own C++ exceptions carry no compiler-enforced acknowledgment requirement at all — a function that throws can be called with zero handling and zero declaration, compiling cleanly, only to crash at runtime if the exception is never caught anywhere up the call stack.
Unchecked Exceptions
RuntimeException and its subclasses — NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException — carry no such requirement. They can be thrown and left completely unhandled, compiling fine, just like every exception in cpp2-6's C++ model. Unchecked exceptions are typically reserved for programming errors — bugs — rather than expected, recoverable failure conditions.
The Real Middle Ground vs. Rust's Result<T, E>
Rust encodes failure directly in a function's return type — the signature itself says "this can fail," and the caller must handle the Result via match, ?, or an explicit unwrap() or the code won't compile clean. Java's checked exceptions force a genuinely comparable acknowledgment, but through a side-channel — the throws clause — rather than the return type itself. It's a real middle position: more compiler enforcement than C++ ever offers, but less structurally integrated into the type system than Rust's approach.
try-with-resources
Any resource implementing AutoCloseable can be declared in the try(...) parentheses — its close() is called automatically once the block exits, exception or not. It's Java's own answer to the resource-cleanup problem cpp1-5's RAII solves via destructors and Rust's Drop solves automatically — genuinely less automatic than either, since it still requires the explicit try(...) syntax rather than firing on scope exit alone.
| Approach | C++ (cpp2-6) | Java checked | Java unchecked | Rust |
|---|---|---|---|---|
| Compiler-enforced handling | no | yes — catch or declare | no | yes — built into the return type |
| Where the requirement lives | nowhere | throws clause (side-channel) | nowhere | the function signature itself |
| Resource cleanup | RAII — automatic on scope exit | try-with-resources — automatic within the block, syntax required | Drop — automatic on scope exit | |
Exception type hides which failures a piece of code actually expects and silently swallows unrelated bugs along with it — catch the narrowest type that's genuinely recoverable.
catch (Exception e) {} compiles fine and looks like handling — but it silently discards every failure, including ones that indicate a genuine bug elsewhere. At minimum, log or rethrow; never swallow silently.
Coding Challenges
Write a method that deliberately divides by zero inside a try block, catch ArithmeticException specifically, and use a finally block to print a message that always runs regardless.
📄 View solutionWrite a method that calls Files.readAllBytes without catching IOException and without declaring throws. Show the resulting compile error, then fix it using throws in the method signature.
📄 View solutionWrite a class implementing AutoCloseable with a close() method that prints a message, then use it in a try-with-resources block alongside code that throws an exception, showing that close() still runs.
📄 View solutionChapter 7 Quick Reference
- try/catch/finally — finally always runs, exception or not
- Checked exceptions (Exception minus RuntimeException) must be caught or declared with throws — compiler-enforced, unlike C++'s cpp2-6 model
- Unchecked exceptions (RuntimeException and subclasses) carry no such requirement — same as C++'s exceptions
- Java's checked exceptions are a real middle ground: more enforcement than C++, less structurally integrated than Rust's Result<T, E> in the return type itself
- try-with-resources auto-closes any AutoCloseable — Java's own, less-automatic answer to RAII/Drop
- Next chapter: the Collections Framework — Java's own answer to cpp2-2's STL containers
Collections Framework I
Fundamentals closes with the tool used constantly in real Java code: growable, generic collections — starting with List, the interface Course 2's own Collections Framework II picks up from with Map/Set/Queue.
ArrayList — A Growable Array
A plain Java array's size is fixed the moment it's created. ArrayList is backed internally by an array too, but resizes itself automatically as elements are added — the size a caller actually deals with is never fixed at all.
Generics in Collections
List<String> tells the compiler exactly what type this list holds, catching a type mismatch at compile time rather than at some later runtime cast. Before generics existed, Java collections held plain Object references, requiring an explicit, unchecked cast on every read — a real source of runtime ClassCastExceptions generics eliminate outright. Course 2's own Generics In Depth chapter goes further into how generics work themselves; this chapter only needs the collection-facing side.
Programming to the Interface
The variable is declared as List<String>, not ArrayList<String>, even though an ArrayList is what's actually constructed. This mirrors java1-6's own interface-vs-implementation split directly: code written against the List interface keeps working unchanged if the concrete implementation is later swapped for a LinkedList or any other List.
Iterating — for-each, Iterator, and Safe Removal
The for-each form is the everyday choice for read-only iteration. Removing an element while iterating requires the explicit Iterator and its own remove() — removing directly from the list during a for-each is not safe, covered next.
Java's Own Answer to the STL Containers
ArrayList is directly comparable to cpp2-2's own std::vector — both are growable, contiguous, general-purpose sequence containers. The real difference traces straight back to java1-2's boxing material: a C++ std::vector<int> stores raw int values directly, no wrapping required, since C++ templates generate real code specialized for the exact type. Java generics can't hold a primitive int at all — List<int> doesn't compile — so List<Integer> is required instead, and every element is autoboxed on the way in.
| Behavior | C++ std::vector (cpp2-2) | Java ArrayList |
|---|---|---|
| Growable | yes | yes |
| Holding a primitive/raw int | yes — stored directly, no wrapping | no — must box to Integer (java1-2) |
| Declared-type convention | the concrete container type itself | the interface (List), not ArrayList |
List<String> list = new ArrayList<>(); — this single habit keeps calling code decoupled from exactly which List implementation is in use, the same interface-over-implementation instinct from java1-6.
list.remove(...) directly on the list while a for-each loop is iterating over it throws ConcurrentModificationException — the for-each's hidden iterator detects the list changed underneath it. Use Iterator.remove() instead, the only safe way to remove elements mid-iteration.
Coding Challenges
Create a List<Integer> declared using the List interface, add five numbers to it via an ArrayList, then print them using a for-each loop and explain in a comment where autoboxing happens.
📄 View solutionWrite code that removes every element equal to a target value from a List<String> directly during a for-each loop, showing the resulting ConcurrentModificationException, then fix it correctly using an explicit Iterator's remove() method.
📄 View solutionExplain why List<int> does not compile in Java, but List<Integer> does, tying your answer back to java1-2's own primitives-vs-reference-types material and to how C++'s std::vector<int> differs.
📄 View solutionChapter 8 Quick Reference — Course 1 Complete
- ArrayList resizes automatically, unlike a plain Java array's fixed size
- Generics catch type mismatches at compile time, replacing the pre-generics Object-cast pattern
- Declare with the interface (List), construct with the implementation (ArrayList) — mirrors java1-6's interface-vs-implementation split
- Removing during a for-each throws ConcurrentModificationException — use Iterator.remove() instead
- ArrayList is Java's answer to cpp2-2's std::vector, but genuinely can't hold primitives directly — everything boxes, per java1-2
- Java Fundamentals is now complete. Course 2 (Intermediate/Advanced) begins with Generics In Depth — Java's type-erasure strategy, contrasted directly against C++ and Rust's shared monomorphization approach.