Java Bridge · J1
File and class layout — no headers, and the filename is part of the code
- Java Bridge
- 7 min read
The idea
One public type, one file, one matching name
This lesson's program is four types: Greeting, Greeter, Shouter and Punctuation. In C++ you would spread them across headers and translation units, naming each file whatever reads best. Java has no headers, no translation units and no forward declarations.
Try naming one for what it does. Rename Greeter.java to MyGreeter.java and the build stops:
MyGreeter.java:6: error: class Greeter is public, should be declared in a file named Greeter.java
Read it as a rename instruction. It names the file it found the class in and the file it wanted. A public type is located by computing a filename from the type name, so the two cannot disagree. Interfaces, enums and records obey the same rule, because it is about public types, not classes.
Which suggests four types means four files. It does not. The constraint covers public types only, so Punctuation lives at the bottom of Greeter.java:
class Punctuation {
static String calm() { return "."; }
}
No public, so no filename claims it. This is what a C++ anonymous namespace becomes, and Shouter.java then calls Punctuation.loud() with no import and no include, because the two share a package. Compiling the four sources gives five class files: one per class, always, however the sources happen to be grouped.
Declaration order buys you nothing either. javac reads every file you hand it and resolves all type and member signatures before any method body, so a class may use a type declared below it. There is no class Punctuation; syntax to reach for, which is why hunting for one wastes ten minutes the first time.
Coming from C++
From C++ — the same four types, laid out both ways
You already know what this code is for. What changes is where it goes.
The interface
C++ — Greeting.h
#pragma once
#include <string>
class Greeting {
public:
virtual ~Greeting() = default;
virtual std::string greet(const std::string& name) const = 0;
};
Java — Greeting.java
public interface Greeting {
String greet(String name);
}
Three things vanished, and none of them are optional in C++:
- The include guard. There is nothing to guard.
javacreads a type declaration once no matter how many files mentionGreeting. - The virtual destructor. There are no destructors. Nothing owns anything.
= 0,virtual, andpublic. Every method on an interface is already public and abstract. Writingpublic abstract String greet(String name);compiles, and every reviewer will read it as someone who has not written Java before.
const on the method has no Java equivalent either. Immutability is a property of the class you write, never a qualifier on the reference — that is J6 and J4 territory.
The implementation
C++ — Greeter.h plus Greeter.cpp, two files, one class
// Greeter.h
#pragma once
#include "Greeting.h"
class Greeter : public Greeting {
public:
std::string greet(const std::string& name) const override;
};
// Greeter.cpp
#include "Greeter.h"
namespace {
std::string calm() { return "."; }
}
std::string Greeter::greet(const std::string& name) const {
return "Hello, " + name + calm();
}
Java — Greeter.java, one file, two classes
public class Greeter implements Greeting {
@Override
public String greet(String name) {
return "Hello, " + name + Punctuation.calm();
}
}
class Punctuation {
static String calm() { return "."; }
static String loud() { return "!"; }
}
The ratio inverts. In C++ one class routinely spans two files. In Java one file can hold several classes but only one public one, so a class never spans files.
| C++ | Java |
|---|---|
Greeter.h + Greeter.cpp | Greeter.java — declaration and body together |
#include "Greeting.h" | nothing. Same package, so the name is enough |
: public Greeting | implements Greeting (extends is for classes) |
override — optional keyword, checked | @Override — optional annotation, checked |
| anonymous namespace for file-local helpers | a non-public class in the same file |
filename is free; greeter.cpp is fine | filename is the public class name |
@Override is worth the same reflex override earns you in C++: it turns "I typed the signature wrong and silently added a new method" into a compile error.
The second implementation, and why no include appears
C++ — Shouter.cpp must say where Punctuation came from
#include "Punctuation.h" // and Punctuation must have been given a header
Java — Shouter.java says nothing
public class Shouter implements Greeting {
@Override
public String greet(String name) {
return "HELLO " + name.toUpperCase() + Punctuation.loud();
}
}
Punctuation is declared in Greeter.java, a file with a different name, and Shouter.java references it with no import and no include. Same package, so javac already knows it.
This is the load-bearing difference: in C++ you tell the compiler where a type is; in Java you tell it the name and it searches. That search is J2.
Order, and the forward declaration you keep almost writing
C++ — mutual reference needs a declaration first
class Punctuation; // forward declaration, or Greeter will not compile
class Greeter {
Punctuation* p;
};
Java — no such construct exists
public class Main {
public static void main(String[] args) {
System.out.println(describe(new Greeter(), "Pratyush"));
}
// Declared below its caller. Fine.
static String describe(Greeting greeting, String name) {
return greeting.getClass().getSimpleName() + " says: " + greeting.greet(name);
}
}
javac reads every file you hand it, builds the full set of type and member signatures, and then resolves bodies. So a method may call one declared beneath it, two classes may refer to each other freely, and the order of files on the command line is meaningless. There is no class Punctuation; syntax to reach for, which is exactly why hunting for it wastes ten minutes the first time.
The one C++ habit that will bite today
Naming the file after what it does rather than after the class. greeter.java, greeting_impl.java, types.java — all fine in C++, all fatal here:
greeter.java:1: error: class Greeter is public, should be declared in a file named Greeter.java
Read that message as a rename instruction, and note it names the file it wanted. It is the most common Java error a C++ developer produces on day one. Two fixes exist and they are not equivalent: rename the file, or rename the class. Pick on which name is right, not on which is less typing.
Worked walkthrough
NOTES — four files, and the four errors that teach them
Four files, one program. Compile and run it exactly like this, from the directory holding the sources:
.toolchain\jdk-21\bin\javac.exe -d out *.java
.toolchain\jdk-21\bin\java.exe -cp out Main
Real output, from this code:
Greeter says: Hello, Pratyush.
Shouter says: HELLO PRATYUSH!
Note what out/ contains afterwards:
out/Greeter.class
out/Greeting.class
out/Main.class
out/Punctuation.class
out/Shouter.class
Five class files from four source files. Punctuation.class exists on its own even though Punctuation never had a file of its own. That is the first thing to internalise: the class file is always one-per-class. Only the source file is allowed to hold more than one, and only for non-public types.
Greeting.java
public interface Greeting {
String greet(String name);
}
public interface Greeting — the filename rule is not about classes, it is about public types. An interface, an enum, a record and a class are all types, and all four obey it. The invariant: given a public type name, javac can compute the file to open without searching. That is why the rule exists, and why the search in J2 is cheap.
String greet(String name); — no public, no abstract, no virtual, no = 0, no trailing const. Interface methods are already public and abstract. Writing public abstract String greet(String name); compiles and is redundant; a reviewer reads it as "has not written Java before". Nothing technical is avoided by leaving them off. The signal you send is.
There is also no virtual destructor, because there are no destructors. In C++ omitting virtual ~Greeting() on a polymorphic base leaks. Here there is nothing to leak; cleanup of resources (not memory) is try-with-resources, which is J9.
Greeter.java — the file that holds two classes
public class Greeter implements Greeting {
private final String punctuation;
implements, not extends. extends is for the one class you inherit from; implements is for the interfaces you satisfy, and there may be many. Writing extends Greeting here gives you error: no interface expected here, which reads as nonsense until you know the two keywords are not interchangeable.
private final String punctuation; — final says this reference is assigned exactly once and never reassigned. It is narrower than C++ const: it constrains the variable, not the object. private final List<String> xs still lets anyone holding the reference call xs.add(...). Here the field is a String, which is immutable, so final is enough. Reading final as const is one of the traps in the bridge table; J4 and J6 return to it.
public Greeter() {
this.punctuation = Punctuation.calm();
}
Punctuation is declared at the bottom of this same file, below the class using it. No forward declaration, no reordering, no header. The invariant javac maintains: every type and member signature is known before any method body is resolved. Order carries no information. The C++ bug that cannot happen here is the compile that succeeds or fails depending on include order.
this. is optional but not noise. Java has no m_ convention and no initialiser list. In a constructor whose parameter and field share a name, this. is the only thing distinguishing them, so the habit is worth keeping.
@Override
public String greet(String name) {
@Override earns its keystrokes. It is checked, exactly like C++11 override. Without it, misspelling greet as greeting silently adds a new method and leaves Greeting.greet unimplemented. The error you then get points at the class declaration, seventeen lines away from the actual typo:
Greeter.java:6: error: Greeter is not abstract and does not override abstract method greet(String) in Greeting
public class Greeter implements Greeting {
^
With @Override present, the error lands on the typo instead: method does not override or implement a method from a supertype.
class Punctuation {
static String calm() { return "."; }
A second class in the same file, deliberately not public. This is the direct replacement for a C++ anonymous namespace. Punctuation is package-private: visible to every class in the same package, invisible outside it. Because it is not public, the filename rule does not apply, so it may share Greeter.java.
static, because Java has no free functions. Every method belongs to a class. A namespace of helpers becomes a class of static methods.
private Punctuation() { } — the way to say "never instantiate this". Without it Java supplies a public no-argument constructor for free, and new Punctuation() compiles, which is meaningless for a holder of static helpers.
Shouter.java — the file with no imports
public class Shouter implements Greeting {
@Override
public String greet(String name) {
return "HELLO " + name.toUpperCase() + Punctuation.loud();
}
}
One line here is the whole lesson. Punctuation is declared in Greeter.java, a different file with a different name, and Shouter.java names it with no import and no #include. They are in the same package, so javac already has it.
Shouter is public and therefore needs its own file. That is not a style guideline you could choose to break; it is the compiler.
Main.java
public static void main(String[] args) {
System.out.println(describe(new Greeter(), "Pratyush"));
The launcher matches this signature exactly. public, static, void, one String[]. String... args also works (same erasure). Anything else compiles fine and then fails at run time, which is the surprising part — see error 4 below.
describe is called from above its own declaration. Same rule as Punctuation, now inside a single class.
static String describe(Greeting greeting, String name) {
The parameter type is the interface. It is the one design decision in the lesson, and it is here so the mechanics have a point. describe was compiled once and works for implementations that do not exist yet. Shouter was written after it and needed no change to it.
The four errors, produced for real
Every one of these was run against exactly the code above. Recognising them on sight is half of what fluency means. Read the message, not the colour.
1 · Filename does not match the public class
Rename Greeter.java to MyGreeter.java and recompile:
MyGreeter.java:6: error: class Greeter is public, should be declared in a file named Greeter.java
public class Greeter implements Greeting {
^
1 error
Read it as a rename instruction. It names both the file it found the class in and the file it wanted. Two fixes exist and they are not equivalent: rename the file, or rename the class. Pick based on which name is right, not on which is less typing.
This is the single most likely error on your first Java day, because C++ imposes no such link and greeter.cpp is perfectly ordinary there.
2 · Two public classes in one file
Add public to class Punctuation in Greeter.java:
Greeter.java:33: error: class Punctuation is public, should be declared in a file named Punctuation.java
public class Punctuation {
^
1 error
Same error text, different cause. There is no separate "two public classes" diagnostic. The rule is not "one public class per file"; it is "a public class lives in its matching file". Two classes cannot both match one filename, so the second one is reported the same way as a mismatched name.
The fix that keeps the design: leave Punctuation package-private. The fix that changes the design: give it its own file, and now it is part of your public surface.
3 · A type the compiler cannot find
Compile Shouter.java and Greeting.java without Greeter.java present:
Shouter.java:13: error: cannot find symbol
return "HELLO " + name.toUpperCase() + Punctuation.loud();
^
symbol: variable Punctuation
location: class Shouter
1 error
cannot find symbol is Java's undeclared identifier, and it will be your most frequent error for a week. Read all three lines: the caret column, then symbol:, then location:.
Note it says variable Punctuation, not class Punctuation. javac cannot tell which you meant — Punctuation.loud() is equally consistent with a variable named Punctuation holding an object with a loud() method. So do not be thrown by the word "variable"; it means "this name resolves to nothing", nothing more.
The cause is almost never a missing import. It is a missing file — the type is not on the sourcepath or classpath at all. J2 is about where those paths point.
4 · A main the launcher will not accept
Rename main to start. This compiles cleanly and then:
Error: Main method not found in class Main, please define the main method as:
public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application
Compile-time silence, run-time failure. main is an ordinary method to javac. Only the java launcher cares, and it looks the name up by reflection. So every variation you might reach for is a legal method and a broken entry point: void main(), static int main(String[] args), public void main(String[] args).
The message is worth reading in full: it prints the signature it wanted. Ignore the JavaFX line; it is a legacy fallback the launcher mentions unconditionally.
Worked source
The 4 files of the worked design
Every file below is the one the app opens, verbatim. This is the part worth reading slowly: the prose above argues for a shape, and these are the lines that have it.
worked/src/Greeter.java42 linesworked/src/Greeting.java16 linesworked/src/Shouter.java15 linesworked/src/Main.java27 lines
worked/src/Greeter.java42 lines
// Greeter.java
//
// One public class, named exactly like the file. Rename either half of that pair and the
// build stops on the line below with:
// error: class Greeter is public, should be declared in a file named Greeter.java
public class Greeter implements Greeting {
// `final` here means the reference is assigned exactly once. It is not C++ `const`:
// it says nothing about the object it points at. Enough for a String, which is
// already immutable — J4 and J6 cover the cases where it is not enough.
private final String punctuation;
public Greeter() {
// Punctuation is declared at the BOTTOM of this file, below the class using it.
// No forward declaration and no reordering: javac reads every file it was given
// before it resolves a single name in a method body.
this.punctuation = Punctuation.calm();
}
// @Override is checked by the compiler. Misspell the name or the parameter type and
// you get "method does not override or implement a method from a supertype" instead
// of a silently-added new method that leaves Greeter abstract.
@Override
public String greet(String name) {
return "Hello, " + name + punctuation;
}
}
// A second class in the same file. This is legal, and it is the Java answer to a C++
// anonymous namespace: Punctuation is package-private, so the filename rule has nothing
// to say about it. Add `public` to the line below and the build fails with:
// error: class Punctuation is public, should be declared in a file named Punctuation.java
class Punctuation {
static String calm() { return "."; }
static String loud() { return "!"; }
// A private constructor is how you say "never instantiate this". There is no
// free-function alternative in Java — every method belongs to some class.
private Punctuation() { }
}
worked/src/Greeting.java16 lines
// Greeting.java
//
// The filename rule is about PUBLIC TYPES, not about classes. An interface is a type,
// so a public interface called Greeting has to live in a file called Greeting.java.
// There is no header to declare it in and no .cpp to define it in — this file is the
// whole thing.
public interface Greeting {
/**
* Every greeting turns a name into a line of text.
*
* No `public`, no `abstract`, no `virtual`, no `= 0`. Interface methods are already
* public and abstract; spelling that out compiles but marks you as new to Java.
*/
String greet(String name);
}
worked/src/Shouter.java15 lines
// Shouter.java
//
// The second implementation. It gets its own file for exactly one reason: it is public,
// and a public type must live in a file bearing its name. Nothing about "one class per
// file" is a style rule here — it is the compiler.
public class Shouter implements Greeting {
@Override
public String greet(String name) {
// Punctuation is declared in Greeter.java — a different file, with a different
// name, and no import or include anywhere in sight. Same package, so the plain
// name is all javac needs. This is the whole difference from C++ in one line.
return "HELLO " + name.toUpperCase() + Punctuation.loud();
}
}
worked/src/Main.java27 lines
// Main.java
//
// The entry point. `java` looks for exactly this signature — public, static, void, one
// String[] parameter. Get any part of it wrong and the launcher tells you so at run time,
// not compile time:
// Error: Main method not found in class Main, please define the main method as:
// public static void main(String[] args)
public class Main {
public static void main(String[] args) {
// describe() is declared BELOW this call. In C++ that needs a declaration first;
// here the compiler has already read the whole class.
System.out.println(describe(new Greeter(), "Pratyush"));
System.out.println(describe(new Shouter(), "Pratyush"));
}
/**
* Takes the interface, never a concrete class. That is the point of extracting
* Greeting at all: this method compiles once and keeps working for every
* implementation added later, including ones that do not exist yet.
*/
static String describe(Greeting greeting, String name) {
// getSimpleName() is reflection, used here only so the output shows which
// implementation ran. Real code would not ask.
return greeting.getClass().getSimpleName() + " says: " + greeting.greet(name);
}
}
The faded stage is not here, on purpose
In the app, the third stage of a lesson hands you the worked design with a few lines
replaced by // GAP: markers, then compiles your completion and runs a JUnit suite
against it. That needs javac, and a static site has no compiler — so rather than show a
control that cannot work, this page stops at the worked source.
Run the app for the drill: it is the download in the header, and it works offline once unpacked.