Hello, World & Output
Hello, World
The statement syntax is the same language you already write — braces, semicolons, the same comment markers. What changed is everything around it: no
#include, and main lives inside a class and takes an array of strings rather than argc/argv.#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}System.out.println appends the newline and knows how to render whatever you hand it; printf takes a format string and believes it. Note also what is missing from the Java side: no return 0, because main returns void and the exit status comes from System.exit or from falling off the end normally. Java does have System.out.printf with C's conversion specifiers, for when you want the field widths.Arguments and the Exit Status
C's
argv[0] is the program name and argc counts it. Java's args holds only the arguments — the program name is not in it, and the count is args.length. System.exit is the counterpart of returning a value from main.#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
/* argv[0] is the program name, so a bare run has argc == 1. */
(void) argv;
printf("%d\n", argc - 1);
exit(3);
}class Main {
public static void main(String[] args) {
// args does NOT include the program name, so this is already the count.
System.out.println(args.length);
System.exit(3);
}
}The off-by-one between
argc - 1 and args.length is the kind of difference that survives a mechanical translation and then produces a wrong answer. Note too that System.exit does not run any pending finally blocks — it is the abrupt exit, the same as C's, while returning normally from main lets the JVM shut down in an orderly way.Types Have Fixed Widths Now
int Is Always 32 Bits
C guarantees only minimum widths —
int is at least 16 bits, long at least 32 — which is why stdint.h exists. Java fixes every width by specification: byte 8, short 16, int 32, long 64, on every platform.#include <stdio.h>
#include <stdint.h>
int main(void) {
/* sizeof(long) is 8 on Linux/macOS 64-bit and 4 on 64-bit Windows.
Only the stdint.h names mean the same thing everywhere. */
printf("%zu\n", sizeof(int32_t));
printf("%zu\n", sizeof(int64_t));
printf("%d\n", INT32_MAX);
return 0;
}class Main {
public static void main(String[] args) {
// No sizeof, because there is nothing to ask: the widths are
// fixed by the language specification, not by the platform.
System.out.println(Integer.BYTES);
System.out.println(Long.BYTES);
System.out.println(Integer.MAX_VALUE);
}
}Java removed
sizeof because it removed the question. The cost is the other half of the trade: Java has **no unsigned integer types** at all. A C unsigned int arriving over JNI has to be carried in a Java int and read with Integer.toUnsignedLong or Integer.compareUnsigned, and a value above 2³¹−1 looks negative until you do.A Java char Is 16 Bits
C's
char is a byte, and whether it is signed is implementation-defined. Java's char is an unsigned 16-bit UTF-16 code unit — a different thing wearing the same name, and one of the sharpest traps when porting.#include <stdio.h>
int main(void) {
char letter = 'A';
printf("%zu\n", sizeof(letter)); /* 1 — a byte */
printf("%d\n", letter); /* 65 */
return 0;
}class Main {
public static void main(String[] args) {
char letter = 'A';
// 2 bytes, not 1: a UTF-16 code unit.
System.out.println(Character.BYTES);
System.out.println((int) letter);
}
}The consequence bites in two directions. A Java
char[] is not a byte buffer, so handing one to C as a char* is wrong by a factor of two — byte[] is the counterpart. And because a UTF-16 unit cannot hold every code point, characters outside the Basic Multilingual Plane take two chars, which is why String.length() counts code units rather than characters.References Instead of Pointers
A Reference Is Not a Pointer
Java has no
*, no &, and no pointer arithmetic. A variable of class type holds a reference, which behaves like a pointer you can only follow — never offset, never compare by address for ordering, and never turn into an integer.#include <stdio.h>
typedef struct { int value; } Box;
static void modify(Box *box) {
box->value = 99;
}
int main(void) {
Box box = { 1 };
modify(&box); /* the address is taken EXPLICITLY */
printf("%d\n", box.value);
Box *cursor = &box;
printf("%d\n", cursor->value);
return 0;
}class Box {
int value;
}
class Main {
static void modify(Box box) {
box.value = 99; // no ->, and no address was taken
}
public static void main(String[] args) {
Box box = new Box();
box.value = 1;
modify(box); // the reference is passed by value
System.out.println(box.value);
Box cursor = box; // another name for the same object
System.out.println(cursor.value);
}
}Java is pass-by-value throughout, and the value being passed is the reference — which is why
modify can change the object's field but could not make the caller's variable point at a different object. That is exactly C's Box * versus Box ** distinction, with the second one simply unavailable. The absence of pointer arithmetic is what lets the collector move objects and rewrite the references.NULL Is Checked For You
Following a null pointer in C is undefined behavior — usually a segmentation fault delivered by the hardware, with no message and no line number. Java performs the comparison on every dereference and throws an exception naming the line.
#include <stdio.h>
typedef struct { int value; } Box;
int main(void) {
Box *box = NULL;
/* This check is the one the JVM performs for you. */
if (box == NULL) {
printf("null\n");
} else {
printf("%d\n", box->value);
}
return 0;
}class Box {
int value;
}
class Main {
public static void main(String[] args) {
Box box = null;
try {
System.out.println(box.value);
} catch (NullPointerException error) {
// The JVM did the comparison; this is what it found.
System.out.println("null");
}
}
}A
NullPointerException is the runtime having compared against zero and lost — which is why it arrives as a catchable object with a stack trace rather than as a crash. Since Java 14 the message also names the exact expression that was null, which is the part C can never offer: by the time the hardware faults, the information about which subexpression it was has already been thrown away.Arrays Know Their Length
The Length Travels With the Array
A C array name decays to a bare pointer the moment it is passed anywhere, losing its size — which is why every C function taking an array takes a count beside it. A Java array is an object with a
length field that goes wherever it goes.#include <stdio.h>
/* The count MUST be passed: 'values' is a pointer here, and
sizeof(values) would give the size of a pointer. */
static int sum(const int *values, size_t count) {
int total = 0;
for (size_t index = 0; index < count; index++) {
total += values[index];
}
return total;
}
int main(void) {
int numbers[3] = { 10, 20, 12 };
size_t count = sizeof(numbers) / sizeof(numbers[0]); /* works only HERE */
printf("%d\n", sum(numbers, count));
printf("%zu\n", count);
return 0;
}class Main {
static int sum(int[] values) {
int total = 0;
for (int value : values) { // no count parameter needed
total += value;
}
return total;
}
public static void main(String[] args) {
int[] numbers = { 10, 20, 12 };
System.out.println(sum(numbers));
System.out.println(numbers.length);
}
}The
sizeof(numbers) / sizeof(numbers[0]) idiom is correct in main and silently gives 1 or 2 inside sum, because there the name is a pointer. That is the single most common source of buffer overruns in C, and Java removed the possibility by making the length part of the object. The for (int value : values) form exists because the loop no longer needs an index to stay in bounds.Every Index Is Checked
C does not check, and reading past the end is undefined behavior — usually it simply returns the next bytes, which is what makes the bug so hard to find. Java checks every access and throws with both numbers in the message.
#include <stdio.h>
int main(void) {
int numbers[3] = { 10, 20, 30 };
size_t count = 3;
size_t index = 5;
/* Omit this and numbers[5] reads past the array with nothing to object. */
if (index >= count) {
printf("index %zu out of bounds for length %zu\n", index, count);
} else {
printf("%d\n", numbers[index]);
}
return 0;
}class Main {
public static void main(String[] args) {
int[] numbers = { 10, 20, 30 };
int index = 5;
try {
System.out.println(numbers[index]);
} catch (ArrayIndexOutOfBoundsException error) {
// The JVM emitted the comparison; this is its message.
System.out.println("index " + index
+ " out of bounds for length " + numbers.length);
}
}
}The C check had to be written, and had to use an unsigned type so a "negative" index wraps to something enormous and still fails. Java's check is not free either — it is a compare and a branch on every access — but the JIT removes most of them by proving the bound once, which is why the enhanced
for loop is usually faster than indexing.Strings Are Objects, Not char*
Counted, Immutable, and Not NUL-Terminated
A C string is an address whose end is the first zero byte — nothing records the length and every operation rediscovers it. A Java
String stores its length, is immutable, and may contain \0 as ordinary data.#include <stdio.h>
#include <string.h>
int main(void) {
const char *message = "Hello, World!";
printf("%zu\n", strlen(message)); /* a LOOP looking for the zero byte */
char window[6];
memcpy(window, message, 5);
window[5] = '\0'; /* a substring needs its own buffer */
printf("%s\n", window);
const char with_zero[] = "ab\0cd"; /* ends at the zero: length 2 */
printf("%zu\n", strlen(with_zero));
return 0;
}class Main {
public static void main(String[] args) {
String message = "Hello, World!";
System.out.println(message.length()); // a stored field
System.out.println(message.substring(0, 5));
String withZero = "ab\0cd"; // the zero is DATA: length 5
System.out.println(withZero.length());
}
}C reports 2 where Java reports 5, and that disagreement is the lesson: a zero byte terminates one and is data in the other. Immutability is the other half —
substring hands back a new object rather than writing into a buffer you sized, so there is no + 1 to forget and no overflow to cause. The cost is that building a string in a loop allocates repeatedly, which is what StringBuilder exists to avoid.Comparing Strings, and the == Trap
C has no
== for strings at all — comparing two char* compares addresses, so you call strcmp. Java's == compiles fine on strings and compares references, which is the same mistake with none of the friction.#include <stdio.h>
#include <string.h>
int main(void) {
char first[] = "hello";
char second[] = "hello";
/* Comparing the pointers compares ADDRESSES — always different here. */
printf("%d\n", first == second);
/* strcmp compares the contents. */
printf("%d\n", strcmp(first, second) == 0);
return 0;
}class Main {
public static void main(String[] args) {
String first = new String("hello");
String second = new String("hello");
// == compares REFERENCES, exactly like comparing two char* in C.
System.out.println(first == second);
// equals compares the contents.
System.out.println(first.equals(second));
}
}This is the one place where Java's syntax being C-like is actively dangerous:
first == second is a type error nowhere and wrong nearly everywhere. It is made worse by string interning — two identical literals are the same object, so "hello" == "hello" is true and the bug hides until a string arrives from input. The new String(...) above defeats interning to make the comparison honest.new, And No free
new Versus malloc and free
malloc returns an address or NULL, and reclaiming it is a line you write. new cannot fail in the same way — it throws OutOfMemoryError rather than returning null — and nothing reclaims it explicitly.#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t count = 4;
int *buffer = malloc(count * sizeof(int));
if (buffer == NULL) return 1; /* allocation can FAIL, visibly */
for (size_t index = 0; index < count; index++) {
buffer[index] = (int) index * 10;
}
int total = 0;
for (size_t index = 0; index < count; index++) total += buffer[index];
printf("%d\n", total);
free(buffer); /* and someone must remember this */
return 0;
}class Main {
public static void main(String[] args) {
int[] buffer = new int[4]; // zeroed, and cannot return null
for (int index = 0; index < buffer.length; index++) {
buffer[index] = index * 10;
}
int total = 0;
for (int value : buffer) total += value;
System.out.println(total);
// Nothing is freed. The collector decides when.
}
}Three things vanish at once: the failure check, the
free, and the uninitialized memory — new int[4] is guaranteed zeroed while malloc gives you whatever was there. What you give up is knowing when memory is released. That imprecision is why Java needed try-with-resources for things that are not memory: the collector handles the heap but has nothing to say about a file handle or a socket.A Struct With Functions Is a Class
The this You Passed By Hand
The C idiom is a struct plus functions whose first parameter is a pointer to it. A class is that pattern with the first parameter supplied automatically — and the functions moved inside the braces.
#include <stdio.h>
typedef struct {
long total;
} Counter;
/* The first parameter IS "this". */
static void counter_add(Counter *counter, long amount) {
counter->total += amount;
}
static long counter_doubled(const Counter *counter) {
return counter->total * 2;
}
int main(void) {
Counter counter = { 0 };
counter_add(&counter, 5);
counter_add(&counter, 16);
printf("%ld\n", counter.total);
printf("%ld\n", counter_doubled(&counter));
return 0;
}class Counter {
private long total;
// No first parameter: "this" is supplied.
void add(long amount) {
total += amount;
}
long doubled() {
return total * 2;
}
long total() {
return total;
}
}
class Main {
public static void main(String[] args) {
Counter counter = new Counter();
counter.add(5);
counter.add(16);
System.out.println(counter.total());
System.out.println(counter.doubled());
}
}The mechanical change is small —
counter_add(&counter, 5) becomes counter.add(5) — and the real addition is private. In C the struct's fields are visible to every file that includes the header, and the convention that only counter_* functions touch them is enforced by nothing. Java made that convention a compile error, which is most of what "encapsulation" buys.The Preprocessor Is Gone
No #define, No #include, No Header
Java has no preprocessor at all. A constant is a
static final field with a type; there is no textual substitution, so a "macro" cannot capture the wrong expression or evaluate its argument twice. import names a class the compiler already knows how to find rather than pasting a file in.#include <stdio.h>
/* Textual substitution: the argument is evaluated TWICE. */
#define SQUARE(x) ((x) * (x))
#define LIMIT 10
int main(void) {
int counter = 3;
printf("%d\n", SQUARE(counter));
/* The classic macro trap: counter is incremented twice. */
printf("%d\n", SQUARE(counter++));
printf("%d\n", counter);
printf("%d\n", LIMIT);
return 0;
}class Main {
// A typed constant, not a text substitution.
static final int LIMIT = 10;
// A method: its argument is evaluated exactly once.
static int square(int value) {
return value * value;
}
public static void main(String[] args) {
int counter = 3;
System.out.println(square(counter));
// Evaluated once, so counter ends at 4 rather than 5.
System.out.println(square(counter++));
System.out.println(counter);
System.out.println(LIMIT);
}
}The two columns disagree on the last two numbers, and that is the point:
SQUARE(counter++) expands to ((counter++) * (counter++)), which increments twice and is in fact undefined behavior for reading and modifying the same object twice without a sequence point. A method cannot do that. Losing #ifdef also means Java has no conditional compilation — platform differences are handled at runtime instead, which is what "write once, run anywhere" is really describing.Exceptions Instead of Return Codes
Throwing Versus Returning a Status
C signals failure with a sentinel return plus a global
errno that the next call may overwrite. Java throws an object that unwinds the stack, runs every finally on the way, and cannot be ignored by accident.#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void) {
const char *text = "123";
char *end = NULL;
errno = 0; /* MUST be cleared first */
long value = strtol(text, &end, 10);
if (errno != 0 || end == text || *end != '\0') {
printf("error: bad number\n");
return 1;
}
printf("%ld\n", value);
const char *bad = "nope";
char *bad_end = NULL;
errno = 0;
long other = strtol(bad, &bad_end, 10);
(void) other;
/* No errno is set for "not a number" — the only signal is that the
end pointer did not move. This is the trap. */
if (bad_end == bad || *bad_end != '\0') {
printf("error: bad number\n");
}
return 0;
}class Main {
public static void main(String[] args) {
System.out.println(Integer.parseInt("123"));
try {
Integer.parseInt("nope");
} catch (NumberFormatException error) {
System.out.println("error: bad number");
}
}
}The C column is not exaggerated:
strtol genuinely reports "not a number at all" only by leaving the end pointer where it started, and genuinely requires clearing errno first because it is not cleared on success. What Java adds beyond brevity is that the failure carries information — a type, a message, and the stack — and that a finally block runs on the way out, which C's goto cleanup idiom achieves only if every early return remembers to jump.Collections Instead of Hand-Rolling
A Hash Map in the Library
C's standard library has no map, no growable array, and no set, so a small table is usually an array of pairs and a loop. Java ships them, and the growable one resizes itself.
#include <stdio.h>
#include <string.h>
typedef struct {
const char *key;
long value;
} Entry;
/* Returns 1 and writes *out when found, 0 when not. */
static int lookup(const Entry *entries, size_t count,
const char *key, long *out) {
for (size_t index = 0; index < count; index++) {
if (strcmp(entries[index].key, key) == 0) {
*out = entries[index].value;
return 1;
}
}
return 0;
}
int main(void) {
Entry ages[3] = { { "alice", 30 }, { "bob", 25 }, { "carol", 41 } };
size_t count = 3;
long age = 0;
if (lookup(ages, count, "bob", &age)) printf("bob %ld\n", age);
long ignored = 0;
if (!lookup(ages, count, "dave", &ignored)) printf("dave not found\n");
printf("%zu\n", count);
return 0;
}import java.util.HashMap;
import java.util.Map;
class Main {
public static void main(String[] args) {
Map<String, Integer> ages = new HashMap<>();
ages.put("alice", 30);
ages.put("bob", 25);
ages.put("carol", 41);
Integer age = ages.get("bob");
if (age != null) System.out.println("bob " + age);
if (!ages.containsKey("dave")) System.out.println("dave not found");
System.out.println(ages.size());
}
}The C version is a linear scan — fine for three entries, wrong for three thousand — and deciding when to graduate to a real hash table is a judgment Java made once, for everyone. Note the shape that survives translation:
get returning null for a missing key is the same "found/not found" signal as the C function's return value, and it exists for the same reason, because a zero value is otherwise indistinguishable from a missing one.Undefined Behavior Is Gone
Overflow and Shifts Are Specified
Signed overflow is undefined behavior in C — the compiler may assume it never happens and optimize accordingly — and shifting by more than the width is undefined too. Java specifies both: signed arithmetic wraps two's-complement, and a shift count is taken modulo the width.
#include <stdio.h>
#include <limits.h>
int main(void) {
/* Signed overflow is UNDEFINED, so this is written to avoid it:
compute in unsigned, where wrapping IS defined. */
unsigned int wrapped = (unsigned int) INT_MAX + 1u;
printf("%d\n", (int) wrapped);
/* Shifting by >= the width is UNDEFINED in C. Kept in range here. */
unsigned int shifted = 1u << 31;
printf("%u\n", shifted);
return 0;
}class Main {
public static void main(String[] args) {
// Signed overflow is DEFINED: it wraps, two's complement.
int wrapped = Integer.MAX_VALUE + 1;
System.out.println(wrapped);
// A shift count is taken modulo 32 for int — also defined.
int shifted = 1 << 31;
System.out.println(Integer.toUnsignedString(shifted));
}
}This is the deepest difference on the page and the one with no syntax to see. In C, "it wrapped on my machine" is not a guarantee — the optimizer is allowed to delete an overflow check on the grounds that overflow cannot occur, and it does. Java has no undefined behavior as a category: evaluation order is left-to-right, integer sizes are fixed, and a program that misbehaves does so identically everywhere. That predictability is bought with the bounds checks and the null checks in the rows above.
Where They Still Meet: JNI
A JNI Function Is a C Function With a Mangled Name
This is where the two languages still meet in practice. A
native method in Java binds to a C function whose name encodes the package, class and method, and whose first two parameters are the JNI environment and the receiver. The Java column here is illustrative — there is no native library to load in the runner — while the C column compiles and runs.#include <stdio.h>
/* What a JNI implementation looks like, minus <jni.h>:
*
* JNIEXPORT jlong JNICALL
* Java_Main_combine(JNIEnv *env, jobject self,
* jlong first, jlong second, jlong third);
*
* The name is Java_<package>_<class>_<method>, and the first two
* parameters are always the environment and the receiver.
*/
long combine(long first, long second, long third) {
return first + second * third;
}
int main(void) {
printf("%ld\n", combine(2, 5, 8));
return 0;
}class Main {
// Binds to Java_Main_combine in a library loaded by System.loadLibrary.
// jlong is 64-bit, so this one is unambiguous; jint is 32.
private static native long combine(long first, long second, long third);
public static void main(String[] args) {
// Calling it needs the native library present:
// System.loadLibrary("combine");
// System.out.println(combine(2, 5, 8));
System.out.println("declaration only");
}
}Three things must agree and none is checked at compile time: the mangled symbol name, the parameter widths, and the presence of the library at run time — a mismatch surfaces as
UnsatisfiedLinkError. The widths are the merciful part: jint and jlong are fixed at 32 and 64 bits, so unlike C-to-C interop there is no platform ambiguity. The unsigned gap remains, though — a C unsigned long has no Java counterpart and must be carried in a long and read with Long.toUnsignedString.