Recommended resources
The freely available resources this lesson will direct you to.
Hello! Last time, we traced the pressures behind early Java: changing hardware, varied operating systems, memory-safety failures, and software delivered across networks. Today we’ll compare Java’s answers with C and C++ directly. The useful question is not “Which language is better?” but which responsibilities each design gives to the programmer, compiler, binary, and runtime.
We’ll examine four choices: object lifetime, memory access, executable portability, and object orientation. The chapter breaks divide the lesson into roughly three 15-minute sessions.
Object lifetime: control versus automatic reclamation
In C, a program can allocate storage and explicitly release it. C++ adds constructors and destructors, so an object can own a resource whose cleanup is tied to a defined lifetime. That control is valuable: a file handle, lock, or memory buffer can be released at a predictable point. It also creates an obligation to get ownership right. Free storage too early and another user may access invalid memory; never free it and usage grows.
Early Java made a different bargain for object memory. Code creates objects, but ordinary Java code does not explicitly free them. The runtime can reclaim an object once it is no longer reachable from the program’s live references. Read the contemporary account for the problem the designers thought this solved.
Read this October 1995 Java design account for its comparison of explicit allocation and freeing with garbage-collected objects.
In Section 2.1.6, “Memory Management and Garbage Collection” (pp. 24–25), begin with the paragraph about C and C++ programmers managing memory. Read the comparison through the explanation of Java’s absent explicit free operation. Focus on which lifetime decision moves from application code into the runtime; you can stop before the ReverseString example.
There is an important distinction between reclaiming memory and releasing a resource on time. Garbage collection does not promise to close a file, return a database connection, or release a lock at the moment you stop using an object. Nor does it reclaim an object that remains reachable through an unintended cache or listener. Modern Java therefore still needs explicit resource-lifetime practices, even though ordinary object-memory reclamation is automatic.
C++ also should not be reduced to “manual new and delete.” Its deterministic destructors and ownership abstractions can avoid direct allocation calls in application code and handle non-memory resources particularly well. Java’s historical trade-off was to remove explicit object deallocation from the ordinary programming model, accepting runtime work and less control over reclamation timing in exchange for eliminating many use-after-free and double-free mistakes.
A 1995 document describes references as “symbolic handles.” Treat that as historical explanatory language, not a guarantee that every JVM implements each reference as a separate handle. Likewise, garbage collection is based on reachability, not simply a count of how many variables currently name an object.
Pointers and arrays: what operations can code express?
The lifetime difference becomes more consequential when combined with pointer operations. C and C++ allow code to work with addresses and, within their respective rules, perform pointer arithmetic. This is useful for systems programming, native interfaces, and compact representations. It also means an incorrect calculation or unchecked memory access can affect data unrelated to the intended operation.
Java has object references, but ordinary Java code cannot add an offset to one or cast an arbitrary integer into one. An array is an object with a length, and an indexed access is checked against that length. Java retains aliasing—two references can denote the same mutable object—without exposing general-purpose address manipulation.
Return to the design document to see how its authors distinguished Java’s arrays and objects from C-style pointer manipulation.
In Section 2.2.8, “No More Pointers” (pp. 30–31), read the pointer comparison. Start just after the subsection’s opening sentence and stop at its closing claim. Pay particular attention to the paragraph on array indexing.
“No pointers” is shorthand that needs care in an architecture discussion. References let Java code locate and share objects; what Java withholds is programmer-controlled pointer arithmetic and arbitrary address construction. An invalid array index produces a defined exception rather than permission to access adjacent storage. A bad reference can still be null, and shared mutable objects can still cause difficult bugs. Java narrows the class of possible memory-corruption failures; it does not make programs correct by construction.
This boundary applies to ordinary Java code. Native code and other low-level facilities have different safety considerations, so a system using them cannot attribute every operation’s safety to Java’s reference model.
Binary portability: what exactly gets distributed?
Both C and C++ can be used to write portable source code. The distinction is that their conventional compilation targets a particular processor, operating-system interface, and binary convention. A native executable or library built for one target is not, merely by virtue of being written in C++, an executable for another.
Java instead distributes class files containing instructions for the JVM. A compatible runtime on the destination supplies the implementation for that machine. This changes who performs the platform-specific work: rather than producing a native application binary for every target, the application producer can distribute JVM-targeted code and rely on a runtime for each supported platform.
Read the document’s distinction between machine-independent bytecode and portable program behavior. It sharpens the comparison with native C and C++ binaries.
In Chapter 4, Section 4.1.1, “Byte Codes” (pp. 47–48), start at the paragraph beginning “The Java compiler doesn’t generate” and read the bytecode explanation through the discussion of interpretation or native translation. Then, in Section 4.2, “Portable” (pp. 48–49), read the data-type comparison, including the primitive-type list. Ask why a common instruction format alone would not guarantee the same results on two machines.
For example, Java specifies the size and integer-arithmetic behavior of int. If those properties varied with the host, a class file might run on two machines yet compute differently. Binary portability and behavioral portability are related, but not identical. One concerns whether the artifact can be executed; the other concerns whether its specified operations mean the same thing.
The design document’s primitive-type list calls char a “16-bit Unicode character.” The size remains the relevant portability point here, but the phrase is imprecise for modern Unicode: a Java char is a UTF-16 code unit, and some characters require two code units.
Nor does the JVM erase the environment. A suitable runtime must exist, and a program can still depend on platform-specific files, native libraries, or unavailable APIs. The defensible claim is that Java standardized an application binary target across supported runtimes, not that any Java artifact works unchanged in every environment.
Object orientation: familiar syntax, different constraints
C can express object-like designs with data structures and functions, but classes and virtual method dispatch are not built into its language model. C++ makes classes central to many designs while retaining other approaches, including free functions and value-oriented types. It permits multiple inheritance of classes and operator overloading. Early Java kept familiar class syntax but omitted those two C++ facilities: a class has one direct superclass, while it can implement multiple interfaces.
Use the design document’s account of omitted C++ features to understand the simplicity trade-off, rather than treating omission as an absence of object orientation.
In Section 2.2.4, “No More Multiple Inheritance” (pp. 28–29), read the interface account, starting after the subsection’s first sentence. Then in Section 2.2.6, “No More Operator Overloading” (p. 29), read the operator account. The interface description reflects early Java; interfaces later gained further capabilities.
This was a choice about complexity and predictability, not a proof that the removed facilities have no legitimate uses. Multiple class inheritance can combine implementation and state, but raises questions about inherited members and their relationships. Early Java allowed multiple interface types without multiple class implementations. Named methods took the place of programmer-defined arithmetic operators, making an operation’s meaning less dependent on a class’s overloads. The cost was less expressive freedom for designs that could have used those features effectively.
Java is not literally “everything is an object”: primitives are distinct from objects, and classes can contain static methods. Where Java does support polymorphism, the receiver’s runtime class can determine which overriding instance method runs. A short bytecode excerpt makes the distinction between a method reference in a caller and the eventual target concrete.
Watch “Java Bytecode Crash Course” from Oracle Developers for a focused look at symbolic method references and virtual dispatch—not for a full bytecode tutorial yet.
Watch the call site to see a method reference in the class file and the JVM’s role in linking it. Then watch dispatch choices, focusing on why a receiver’s runtime type matters to an overriding instance method, while a static call does not use virtual dispatch.
The deeper distribution issue appears when an object-oriented library changes. A C++ client compiled with knowledge of a class’s layout or other binary details may need rebuilding when its library changes; the precise result depends on the library’s ABI design. Java class files instead retain symbolic references, and the runtime determines object layouts and resolves references when classes are linked. Read the historical argument, but resist its strongest wording.
Read the design document’s “fragile superclass” discussion to connect object orientation with independently updated binaries.
In Chapter 5, read Section 5.1.1, “The Fragile Superclass Problem” (p. 54), beginning with the library-change argument. Continue into Section 5.1.2, “Solving the Fragile Superclass Problem” (pp. 54–55), and stop at that layout sentence. Track which decisions the document says Java defers; read “interpreter” as the document’s period-specific description, not a requirement that a modern JVM always interprets execution.
The contrast is useful, but “libraries can freely change without affecting clients” would be too broad. Java supports particular kinds of binary-compatible evolution; removing a method, changing a needed signature, or introducing certain conflicts can still break clients. Its design reduces dependence on compile-time layout and supports some independent evolution. It does not eliminate API compatibility work.
Closing perspective
The four comparisons express one coherent early design:
| Question | C/C++ emphasis | Early Java choice |
|---|---|---|
| Who manages object-memory reclamation? | Programmer-controlled ownership and lifetime mechanisms | Runtime garbage collection |
| What memory access can ordinary code express? | Address-oriented operations are available | Opaque references and checked array access |
| What binary is normally shipped? | Target-specific native code | Class files for a suitable JVM |
| How are object-oriented facilities constrained? | C has no built-in classes; C++ offers broader class mechanisms | Classes with single superclass inheritance, multiple interfaces, and runtime-linked references |
Java moved work and control into the runtime to address safety and distribution costs. C and C++ retained capabilities that remain valuable where precise ownership, representation, or native integration matters. Being able to state both the problem solved and the control surrendered makes the historical comparison more useful than a language ranking.
Next, we’ll separate the specifications and components behind “Java”—the language, JVM, APIs, JDK, and runtime—so these trade-offs can be attributed to the right layer.