Skip to main content
Object Oriented Software Design
Object Oriented Software Design

Lecture #3: Java Fundamentals

Introduction

Java was released by Sun Microsystems in 1995 (the language is now stewarded by Oracle) as a deliberate compromise between two very different traditions. From C++ it borrows a familiar C-like syntax, static typing, a set of primitive types (int, double, boolean, …), and structured exception handling. From Smalltalk it borrows the idea that almost everything is an object, running inside a virtual machine with automatic memory management. There are no explicit pointers and no manual free: a garbage collector reclaims memory for you.

The portability story is central to why Java succeeded. Source code is compiled to an intermediate form called bytecode, which runs unchanged on any machine that has a Java Virtual Machine (JVM) — this is the “write once, run anywhere” promise. Portability goes further than just the language: Java standardizes floating-point arithmetic (IEEE 754) and text representation (16-bit Unicode characters), and even its graphics libraries (java.awt, javax.swing, Java2D) behave consistently across platforms. The language was also built with networking in mind from day one, which historically enabled things like servlets (server-side code) and mobile/distributed code. Today Java ships with a vast standard library — the JDK (Java Development Kit, also called Java SE) — covering everything from collections and I/O to database access and graphics.

Loosely speaking, languages you’ve likely encountered cluster into two execution styles: ahead-of-time compiled languages (C, C++, Fortran, Ada) that are translated directly to native machine code before running, and interpreted languages (Python, JavaScript, shell scripts) whose source (or a close derivative of it) is read and executed by a runtime as the program goes. This is really an independent axis from static vs. dynamic typing — Java, for instance, is statically typed like C, while the runtimes for languages like JavaScript now also compile hot code to machine code just-in-time, blurring the “interpreted” label. Java’s own execution model sits in between the two styles just described: javac compiles .java source files into .class bytecode files, and the java launcher then runs that bytecode inside a JVM, which interprets it (with just-in-time compilation for speed). This two-step pipeline trades a bit of raw execution speed for strong portability and strong compile-time checking — a trade-off that has proven worthwhile for the vast majority of application software.

This chapter walks through the concrete, practical building blocks you need to start writing Java programs: how a program is structured and launched, the type system, object creation and constructors, arrays, strings, and the static keyword, finishing with a quick tour of syntax and control structures.

Compiling and Running a Standalone Application

A Java program is launched directly from the command line (or from an IDE) by the java command. Every standalone application needs exactly one class containing a specially-shaped method named main, which is the entry point the JVM looks for:

// file: Greeter.java
public class Greeter {
    public static void main(String[] args) {
        System.out.println("Hello from Java!");
    }
}

Compiling and running it looks like this:

$ javac Greeter.java     // produces Greeter.class
$ java Greeter           // note: no ".class" suffix

A few structural rules matter here. A single .java source file may contain several classes, and javac will happily generate one .class bytecode file per class. However, a source file can contain at most one public class, and if it does, the file name must match that class’s name exactly (Greeter.java for class Greeter). In practice, the convention — and the safest habit — is one class per file.

The signature of main is fixed by the JVM’s specification and must be respected to the letter: it must be public (the JVM, from outside the class, needs to be able to call it), static (it is invoked before any object of the class exists, so it cannot be an instance method), return void, and take a single String[] parameter. Unlike C, there is no int main() returning an exit status by default (you can call System.exit(code) explicitly if you need one).

Command-Line Arguments

The single parameter of main is an array of String. Unlike C’s argv, this array does not include the program name itself — it only contains the arguments the user typed after the class name. Its length (the equivalent of C’s argc) is obtained the same way as for any array in Java: via its length field (not a method call, no parentheses).

public class ArgsPrinter {
    public static void main(String[] args) {
        for (int i = 0; i < args.length; i++) {
            System.out.print(args[i] + " ");
        }
        System.out.print("\n");
    }
}
$ java ArgsPrinter red green blue
red green blue

Since Java 5, the same loop can be written more concisely with the for-each construct, which iterates over any array or collection without needing an explicit index variable:

public class ArgsPrinter {
    public static void main(String[] args) {
        for (String word : args) {
            System.out.printf("%s ", word);
        }
        System.out.print("\n");
    }
}

Note the use of System.out.printf, also introduced in Java 5, which accepts C-style format specifiers (%s, %d, %f, …).

Standard Input, Output, and Error

Java models file and stream I/O as a hierarchy of classes living in the java.io package. The three standard streams every C programmer knows are exposed as public static fields of the System class:

public class System {
    public static final PrintStream out; // like C's stdout, text output
    public static final PrintStream err; // like C's stderr, text output
    public static final InputStream in;  // like C's stdin, raw byte input
}

They are static because there is exactly one of each per running program (there is no sense in having “several” standard outputs), and public because code anywhere needs to be able to reach them by writing System.out, System.err, System.in.

System.out.print and System.out.println are overloaded: there is a version accepting each primitive type (int, double, boolean, …) as well as one accepting any Object. When you print an object, Java automatically calls that object’s toString() method to obtain text. Every class inherits a default toString() from Object, but you will usually want to override it:

public class Fraction {
    private int numerator, denominator;

    public Fraction(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    @Override
    public String toString() {
        return numerator + "/" + denominator;
    }
}
Fraction half = new Fraction(1, 2);
System.out.println("Value: " + half);      // "+" triggers Fraction.toString()
System.out.printf("Value: %s%n", half);     // "%s" also triggers toString()

For input, System.in is a raw InputStream of bytes, which is inconvenient to use directly. Since Java 5, the Scanner class (in java.util) wraps any input source and offers convenient, formatted reading methods similar to C’s scanf:

import java.util.Scanner;

public class ReadExample {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter your age and your name: ");
        int age = scanner.nextInt();
        String name = scanner.next();
        System.out.printf("age=%d name=%s%n", age, name);
    }
}

A classic gotcha: nextInt() and next() read only the token itself and leave the trailing newline sitting unread in the input buffer. If you follow either of them with nextLine() expecting to read “the rest of the line,” you’ll instead get back that leftover empty string immediately, before the user even gets a chance to type anything. The fix is to insert an extra scanner.nextLine() right after nextInt()/next() purely to consume the leftover newline, then call nextLine() again for the line you actually want.

A Scanner can also wrap a plain String instead of System.in, which is the Java analogue of C’s sscanf.

Variables and Types

Java variables fall into exactly two mutually exclusive categories. A variable of a primitive type directly holds a value. A variable of an object type (a class or an interface) holds a reference to an object that lives elsewhere; the variable itself is never the object.

Primitive types resemble those of C/C++, plus boolean (a genuine boolean type, not encoded as an integer) and byte. Their size is fixed by the language specification and does not vary with the machine — a key portability guarantee absent from C. Primitives are handled by value (copying a primitive copies its value, not a reference) and are not objects, though each has a corresponding wrapper class that lets it be “boxed” into an object when needed (for example when storing values in a collection). char values are 16-bit Unicode code units, upward-compatible with ASCII; the same escape sequences as C are available (\n, \t, \b, …).

TypeValuesDefaultSizeWrapper class
booleantrue, falsefalse1 bit (conceptually)Boolean
charUnicode'\u0000'16 bitsCharacter
bytesigned integer08 bitsByte
shortsigned integer016 bitsShort
intsigned integer032 bitsInteger
longsigned integer0L64 bitsLong
floatIEEE 7540.0F32 bitsFloat
doubleIEEE 7540.0D64 bitsDouble

Wrapper classes are the bridge between the world of raw values and the world of objects: they let a primitive value be treated as an object (necessary for collections, which only store references), and they also carry useful static utility methods (Integer.parseInt, Double.MAX_VALUE, and so on).

Object-typed variables initially hold null, meaning “no object.” An object comes into existence only through explicit instantiation with new:

new <ClassName>(<arguments>)

<ClassName>(...) is a call to a constructor. Its job is to initialize the freshly-allocated instance: by default it sets every instance field either to the value given in its declaration, if any, or otherwise to the standard default for its type (as listed in the table above for primitives, null for object references). A class may define its own constructor(s) to run custom initialization logic; this is the very first code executed on a new instance.

Constructors

A constructor is written inside the class and must be named exactly like the class. It never declares a return type (not even void) — it is not a regular method, it is the object’s own initialization procedure.

public class Point {
    double x, y;

    Point(double x, double y) {
        this.x = x;   // "this" distinguishes the field from the parameter
        this.y = y;
        log();        // shorthand for this.log()
    }

    private void log() {
        System.out.println("Created point at (" + x + ", " + y + ")");
    }
}
Point p = new Point(3.0, 4.0);

Unlike C++, Java has no destructor. There is no manual free/delete: unreachable objects are reclaimed automatically by the garbage collector, on its own schedule. For the rare cases where an object holds an external resource (a file handle, a socket) that needs cleanup, Java offers a finalization protocol via finalize() (largely superseded today by try-with-resources, but the historical mechanism is worth knowing about).

Building Composite Objects

Objects are frequently built out of other objects. Consider a segment defined by two endpoints:

class Point {
    double x, y;
    Point(double x, double y) { this.x = x; this.y = y; }
}

class Segment {
    Point start, end;

    // Without a constructor, start and end would default to null.
    Segment(double x1, double y1, double x2, double y2) {
        start = new Point(x1, y1);
        end = new Point(x2, y2);
    }

    Segment(Point start, Point end) {   // an overloaded constructor
        this.start = start;
        this.end = end;
    }
}

Here Segment is overloaded: two constructors coexist, offering two different, equally valid ways to build the same kind of object (from four raw coordinates, or from two already-built Point objects). Note also that constructors are not inherited by subclasses — each class that needs one must declare its own (possibly delegating to another constructor of the same class with this(...), or to the parent class’s constructor with super(...)).

Arrays

In Java, arrays are genuine objects, not a thin syntactic layer over a memory block as in C. They are created dynamically, carry their own length, are reclaimed automatically by the garbage collector, are manipulated by reference (an array variable holds a reference, and assigning it or passing it to a method shares the same underlying array), and are compatible with Object (so any Object method — including toString()-adjacent behavior — is technically applicable to them).

That said, the syntax for arrays remains distinctly C-flavored: element access uses [], and there is no user-visible “Array class” to instantiate directly — you use dedicated syntax instead. Elements can be of a primitive type (a homogeneous array) or an object type (potentially holding a mix of subtypes — a heterogeneous array from the point of view of the actual runtime types). Multidimensional arrays are true arrays-of-arrays, not a single contiguous block.

int[] scores = new int[10];              // 10 ints, all initialized to 0

int[] primes = {2, 3, 5, 7, 11};         // declaration with initializer

scores = primes;                          // both are just reference variables

int[][] grid = new int[50][100];          // 50 rows of 100 ints each
// int[][] bad = new int[][100];          // ILLEGAL: the first dimension
                                           // must be given

Because a Java array variable carries no fixed size of its own (only the array object it points to does), the same variable can be reassigned to hold arrays of different lengths over its lifetime. Passing an array to a method passes the reference, so the callee can mutate the caller’s array in place:

public class ArrayDemo {
    static void fillWithOnes(int[] row) {
        for (int i = 0; i < row.length; i++) {
            row[i] = 1;
        }
    }

    public static void main(String[] args) {
        int[][] triangle = new int[3][];       // an array of 3 array *references*
        triangle[0] = new int[10];              // each row can have its own length
        triangle[1] = new int[20];
        triangle[2] = new int[30];

        fillWithOnes(triangle[0]);
        fillWithOnes(triangle[1]);
        fillWithOnes(triangle[2]);

        for (int[] row : triangle) {            // for-each over the rows
            for (int value : row) {              // for-each over one row
                System.out.print(value);
            }
            System.out.print("\n");
        }
    }
}

One more consequence of arrays being real, bounds-aware objects: accessing an index outside [0, length)triangle[0][10] on a row of length 10, for instance — never silently reads or corrupts adjacent memory the way an out-of-bounds C array access can. Instead, the JVM throws an ArrayIndexOutOfBoundsException at the moment of the illegal access, immediately and loudly, rather than quietly returning garbage or overwriting an unrelated variable.

Arrays of Objects

Allocating an array of an object type only creates the array itself — the slots start out as null references, not as actual objects:

Segment[] segments = new Segment[10];   // 10 slots, each initialized to null

segments[0] = new Segment(0, 0, 3, 4);  // now slot 0 holds a real Segment
Segment shared = new Segment(new Point(1, 1), new Point(2, 2));
segments[1] = shared;                    // slot 1 shares the same object as "shared"

There is no reason for the JVM to construct ten Segment objects just because you asked for ten slots — each slot is created and filled independently, exactly when you decide it should hold something.

Strings

Character strings in Java are full-fledged objects, instances of the String class, even though the language also allows a convenient literal notation:

String greeting = "two\nlines";

Two classes cover most string needs. String objects are immutable — once created, their content never changes, and any “modification” (concatenation, substring extraction) actually produces a brand-new String. StringBuffer (and its more modern, single-threaded cousin StringBuilder) represents a genuinely mutable character buffer whose content and size can change in place.

Useful String operations include the + concatenation operator, the family of String.valueOf(...) conversions from a primitive value to text, and instance methods such as:

String s = "hello";
int n = s.length();                 // 5
int cmp = s.compareTo("world");     // like C's strcmp
boolean same = s.equals("hello");   // logical (content) equality
char c = s.charAt(1);               // 'e'; throws StringIndexOutOfBoundsException if out of range
String sub = s.substring(1, 3);     // "el"

It is essential to use equals rather than == to compare string content== tests whether two references point to the exact same object in memory, which is rarely what you want:

Scanner input = new Scanner(System.in);
String command = input.nextLine();
while (!command.equals("quit")) {     // command == "quit" would almost always be false
    // ... process the command ...
    command = input.nextLine();
}

StringBuffer/StringBuilder supports in-place mutation:

StringBuilder buf = new StringBuilder();
buf.append("hello");
buf.insert(5, " world");
buf.setCharAt(0, 'H');
System.out.println(buf);   // "Hello world"

static: Class-Level Variables and Methods

The static keyword marks a field or method as belonging to the class itself rather than to any particular instance. A static member exists in exactly one copy, shared by every instance of the class, and — when the class is accessible — can even be reached from outside using the class name, exactly as we already saw with System.out or System.in.

Adding final to a static field makes it unmodifiable after initialization, which is how Java expresses named constants:

public class Circle {
    public static final double PI = 3.14159265;   // one shared constant

    double radius;                                  // one value per instance

    Circle(double radius) {
        this.radius = radius;
    }

    double circumference() {
        return 2 * PI * radius;   // same as 2 * Circle.PI * radius from outside
    }
}

Static methods work the same way — they belong to the class, not to an instance, which is exactly why main must be static: the JVM calls it before any object of your class exists. The standard library relies heavily on static utility methods, for instance:

Math.min(3.0, 7.0);      // static method of class Math
Math.sin(angle);
System.exit(0);          // static method of class System

enum: Typed Sets of Named Constants

C’s enum is just a set of named int constants — you can compare, print, or accidentally arithmetic your way into a nonsense value, because underneath it’s still an integer. Java’s enum is a completely different, much stronger tool: it declares a small, fixed class, whose only instances are the named constants you list, and nothing else can ever be an instance of it.

public enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

Day today = Day.MONDAY; is fully type-checked — you cannot assign an arbitrary integer to it, cannot accidentally compare a Day to an unrelated enum, and a switch over a Day lets the compiler warn you if you forgot a case. == works correctly on enum constants (there is exactly one MONDAY object, ever, so identity comparison is content comparison here), and every enum gets a sensible toString() for free ("MONDAY", not Day@1a2b3c).

Because an enum is a class, it can carry fields, a constructor, and methods, giving each constant its own associated data — something plain int constants in C could never do:

public enum Planet {
    MERCURY(3.303e+23, 2.4397e6),
    VENUS(4.869e+24, 6.0518e6),
    EARTH(5.976e+24, 6.37814e6);

    private final double mass;   // kilograms
    private final double radius; // meters

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }

    double surfaceGravity() {
        final double G = 6.67300E-11;
        return G * mass / (radius * radius);
    }
}

double g = Planet.EARTH.surfaceGravity();

Use enum any time a value should be one of a small, closed set of named alternatives (a status, a day of the week, a card suit, a direction) — it replaces the C idiom of #defined or enum-typed integer constants with something the compiler can actually enforce.

Syntax Basics

Java’s lexical syntax is close to C’s. Comments come in two forms — /* a block comment, possibly spanning several lines */ and // a single-line comment. Identifiers (for classes, variables, methods, …) start with a letter, $, or _, followed by any number of letters, digits, or other Unicode “identifier” characters; there is no length limit, and case matters. In practice, starting an identifier with $ or _ is discouraged (those are conventionally reserved for generated or library code). The community convention is camelCase for variables, parameters and methods (totalScore), PascalCase for class names (BankAccount), and ALL_CAPS_WITH_UNDERSCORES for constants (MAX_RETRIES).

Expressions and Control Structures

Expressions and control flow will feel immediately familiar if you know C or C++. A method call plays the role that a function call plays in C — it behaves as a statement when the method returns void, and as an expression producing a value otherwise. A few C operators disappear because they no longer make sense in a managed, object-oriented language: pointer dereference *, address-of &, the arrow ->, and sizeof. Two operators are added: instanceof, which tests an object’s runtime type, and the overloaded + for string concatenation. Logical operators (&&, ||, !) work strictly on boolean values (there is no implicit conversion from int to boolean as there was loosely in C). Operator precedence and associativity otherwise match C closely.

The control-flow statements are the same set you already know: if/else, while, do/while, switch, for, and break/continue. Every predicate (in if, while, the middle clause of a for, …) must be a genuine boolean expression:

for (int i = 0; i < 10; i++) {   // "i" is local to the loop
    if (i % 2 == 0) {
        continue;
    }
    System.out.println(i);
}

Since Java 5, the for-each form (for (Type element : iterableThing)) lets you iterate over any array or any object implementing Iterable, which in practice means arrays and essentially every collection type in the standard library.

Reserved Words

Java reserves a set of keywords that cannot be used as identifiers, among them: abstract, assert, boolean, break, byte, case, catch, char, class, continue, default, do, double, else, enum, extends, false, final, finally, float, for, if, implements, import, instanceof, int, interface, long, native, new, null, package, private, protected, public, return, short, static, super, switch, synchronized, this, throw, throws, transient, true, try, void, volatile, while. You are not expected to memorize this list — just recognize the words when the compiler complains that one of them cannot be used as a variable name.

Recap

  • A Java program is compiled by javac into portable bytecode, then executed by a JVM via the java launcher; a runnable class needs a public static void main(String[] args) entry point.
  • args never includes the program name; use args.length and, since Java 5, the for-each loop to walk through it.
  • System.out, System.err, and System.in are static fields of System; Scanner wraps System.in (or a String) for convenient formatted reading, and printf/String.format give C-style formatted output.
  • Variables are either primitive (holding a value directly, fixed size, non-object) or object-typed (holding a reference, initially null). Every primitive has a corresponding wrapper class.
  • new ClassName(...) allocates an object and runs a constructor — a same-named, no-return-type method that performs first-time initialization; constructors can be overloaded and are never inherited.
  • Arrays are real objects with a length field, created with new Type[size], handled by reference, and possibly multidimensional as true arrays-of-arrays; allocating an array of objects does not allocate the objects themselves.
  • String is immutable; use equals for content comparison, never ==. StringBuffer/StringBuilder provides a mutable alternative.
  • static members belong to the class as a whole, shared by all instances; combined with final they express constants.
  • enum declares a fixed, type-checked set of named constant instances (unlike C’s plain-int enums), and can carry its own fields/constructor/methods.
  • Java syntax, expressions, and control structures closely mirror C, with instanceof and string + added, and pointer-related operators removed.

Object-typed variables were introduced here only as “holding a reference” — how references actually behave (aliasing, pass-by-value-of-reference, == vs .equals, and why you almost always want to override equals/hashCode/toString) is its own chapter: see References, Aliasing, and Object Identity, meant to be read next, before Collections.