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 traditionally lives inside a class and takes an array of strings rather than argc/argv. (Java 25 also accepts a source file with a bare void main() and no class, but every Java example on this page uses the classic form.)#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 many of C's conversion specifiers (%d, %s, %x, %5.2f), for when you want the field widths — though C's %u, %ld and %zu throw an exception there.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. ./main alpha beta
#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);
} java Main alpha beta
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.printf Is Still Here
The format string survives almost intact —
%d, %s, %-10s and %5.2f all mean what they mean in C, because Java copied the syntax deliberately.#include <stdio.h>
int main(void) {
const char *name = "widget";
int count = 7;
printf("%d x %s\n", count, name);
printf("%-10s|%5.2f|\n", name, 12.5);
return 0;
}class Main {
public static void main(String[] args) {
String name = "widget";
int count = 7;
System.out.printf("%d x %s%n", count, name);
System.out.printf("%-10s|%5.2f|%n", name, 12.5);
}
}Two differences.
%n rather than a backslash-n escape emits the platform line separator, which is what printf in Java is documented to prefer. And the format string is checked at run time, not compile time — a mismatched conversion throws IllegalFormatConversionException rather than being caught by -Wformat, so a C habit of trusting the compiler here does not transfer. String.format is the same machinery returning a string instead of printing.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: apart from the 16-bit char, Java has no unsigned integer types. 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.There Are No Unsigned Types
🚨 Apart from
char, an unsigned 16-bit type meant for UTF-16 code units, Java has no unsigned integer types. A value that needed uint32_t needs a long, and one that needed uint64_t needs care.#include <stdint.h>
#include <stdio.h>
int main(void) {
uint32_t large = 4000000000u; /* fits comfortably */
printf("%u\n", large);
/* And the classic: mixing signed with unsigned converts the
* signed operand, so this comparison is FALSE. */
printf("%d\n", -1 < 1u);
return 0;
}class Main {
public static void main(String[] args) {
// No uint32_t. The value does not fit in a signed int, so it
// needs the next size up.
long large = 4000000000L;
System.out.println(large);
// -1 < 1 is simply true: there is no unsigned int to convert
// toward, so the classic C surprise cannot happen.
System.out.println(-1 < 1);
}
}The gain is that the usual-arithmetic-conversions trap disappears:
-1 < 1u is false in C because the signed operand converts to unsigned — GCC's -Wextra warns about comparing the two, but it compiles — and there is no way to write that mistake in Java. The cost shows up at a byte boundary — a byte is signed, so a byte read from a file arrives as −1 rather than 255, and value & 0xFF is the idiom you will write constantly. Integer.toUnsignedString, Long.compareUnsigned and Integer.divideUnsigned exist for when the bits really are unsigned.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.No sizeof, and No typedef
Neither
sizeof nor typedef exists. Every numeric primitive has one size given by the specification (boolean's size is left to the JVM), and an object's layout — padding, header, alignment — belongs to the JVM.#include <stdio.h>
typedef struct { int x; int y; } Point;
int main(void) {
printf("%zu %zu %zu\n", sizeof(int), sizeof(long), sizeof(Point));
/* Layout is yours to know about: padding, alignment, and a struct
* whose size depends on the platform. */
return 0;
}class Point { int x; int y; }
class Main {
public static void main(String[] args) {
// There is no sizeof, because every numeric primitive has one
// fixed size and an object's layout is the JVM's business.
System.out.println(Integer.BYTES + " " + Long.BYTES);
// typedef is gone too: a class or interface IS the name.
Point point = new Point();
point.x = 1;
System.out.println(point.x);
}
}That is mostly a relief and occasionally a loss. You cannot compute a buffer size, cannot control padding, and cannot know how much an object costs without a profiler — which matters when you are used to laying out a struct deliberately.
Integer.BYTES and friends give you the primitive widths as constants. The nearest thing to control over layout is choosing primitive arrays over object graphs, and Project Valhalla is the long-term answer.References Instead of Pointers
No Arithmetic, No Address-Of
A reference can be followed and nothing else — no arithmetic, no
&, no cast to an integer, and no way to ask where an object lives. (Memory outside the heap is another matter: the Foreign Function and Memory API's MemorySegment.address() hands back a raw address.)#include <stdio.h>
int main(void) {
int values[] = {10, 20, 30, 40};
int *walker = values;
int total = 0;
while (walker < values + 4) {
total += *walker;
walker++; /* moves sizeof(int) bytes */
}
printf("%d\n", total);
printf("%td\n", (values + 3) - values);
return 0;
}class Main {
public static void main(String[] args) {
int[] values = {10, 20, 30, 40};
// No pointer to move, so the walk is by index. There is no &,
// no *, and no way in ordinary Java code to hold an address.
int total = 0;
for (int index = 0; index < values.length; index++) {
total += values[index];
}
System.out.println(total);
System.out.println(3);
}
}The reason is the collector: it moves objects to compact the heap and rewrites the references it knows about, which it could only do if no program can hold a raw address. That single constraint is what buys the whole safety story, and it is why every C idiom built on address arithmetic — walking a buffer with a moving pointer, packing a tagged pointer, casting a struct to
char* to serialize it — needs rethinking rather than translating. ByteBuffer and the Foreign Function and Memory API are where that work goes.No Out Parameters
With no address-of operator there is no way to pass somewhere to write, so a function that produces two values returns an object holding both. A
record makes that one line.#include <stdio.h>
/* The C way to return two things: one through the return value,
* one through a pointer the caller supplies. */
static int divide(int numerator, int denominator, int *remainder) {
*remainder = numerator % denominator;
return numerator / denominator;
}
int main(void) {
int remainder;
int quotient = divide(17, 5, &remainder);
printf("%d %d\n", quotient, remainder);
return 0;
}record Division(int quotient, int remainder) { }
class Main {
static Division divide(int numerator, int denominator) {
return new Division(numerator / denominator, numerator % denominator);
}
public static void main(String[] args) {
Division result = divide(17, 5);
System.out.println(result.quotient() + " " + result.remainder());
}
}A
record generates its constructor, accessors, equals, hashCode and toString, so the C-style pattern of an out parameter — which exists mostly to avoid the cost of returning a struct — becomes cheaper to write than it was in C. Note that Java can modify what a reference points at, so a method taking an array or a list can fill it in; what it cannot do is replace the caller's variable, which is what int ** is for in C.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 15 the message also names the expression that was null (a local variable appears as <local1> unless the class was compiled with -g), 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
Two Dimensions Are Arrays of Arrays
A C two-dimensional array is one rectangular block; a Java one is an array of references to rows, each separately allocated and possibly a different length.
#include <stdio.h>
int main(void) {
/* ONE contiguous block of 6 ints. The row length is part of
* the type, which is why a function taking it needs the second
* dimension spelled out. */
int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
printf("%d\n", grid[1][2]);
printf("%zu\n", sizeof(grid) / sizeof(grid[0][0]));
int *flat = &grid[0][0];
printf("%d\n", flat[5]); /* the memory really is one run */
return 0;
}class Main {
public static void main(String[] args) {
// An array OF ARRAYS: one object holding two references to
// separately allocated rows.
int[][] grid = {{1, 2, 3}, {4, 5, 6}};
System.out.println(grid[1][2]);
System.out.println(grid.length * grid[0].length);
// The rows may be different lengths, which a C rectangular
// array cannot be:
int[][] ragged = new int[2][];
ragged[0] = new int[]{1};
ragged[1] = new int[]{1, 2, 3};
System.out.println(ragged[1].length);
}
}The performance consequence is the one to carry:
grid[1][2] in C is one multiplication and one load, and in Java it is two loads with a bounds check each, into memory that may be anywhere. For numeric work the usual answer is a flat int[] with your own index arithmetic — which is what C was doing for you. What Java gains is ragged arrays, and that a row can be passed on its own because it carries its own 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 would count 2 inside sum — a pointer's 8 bytes over an int's 4 — because there the name is a pointer. GCC warns that such a division does not compute the number of array elements, but builds it anyway. 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 made the comparison; this is its own message.
System.out.println(error.getMessage());
}
}
}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*
Building a String
StringBuilder grows as needed, so the buffer size, the running length and the truncation arithmetic all disappear — along with the overflow they invite.#include <stdio.h>
#include <string.h>
int main(void) {
/* The buffer, its size, and the truncation are all yours. */
char buffer[64];
buffer[0] = '