Java, from nothing · chapter 2 of 33
Fields, methods, and constructors
Chapter 1.2 · Part 1, Java from nothing · about 30 minutes
What you need before this chapter: chapter 1.1, A class, an object, and main(). You should be able to write a class with fields, create an object with new, compile it with javac, and run it with java.
When you finish this chapter you will be able to:
- Write a constructor that sets up every field of an object from the arguments passed to
new - Mark a field
private, and explain exactly what that keyword stops other code from doing - Write a method that takes parameters, does work, and returns a value
- Build a class where it is not possible to end up with an object holding invalid data
1. The one idea in this chapter
Go back to the Ticket class from chapter 1.1:
public class Ticket {
String plate;
int arrivedAtMinute;
}
Nothing stops the following code from compiling and running:
Ticket t = new Ticket();
t.arrivedAtMinute = -400;
A ticket that arrived at minute -400 makes no sense, but Java has no objection. Worse, this compiles too:
Ticket t = new Ticket();
System.out.println(t.plate);
This prints null. Nobody set t.plate, so it still holds the value Java gives an unset String field, and the program carries on with a ticket that has no plate at all. Nothing in the class stopped either mistake, because nothing in the class does anything at the moment new Ticket() runs. The two fields just sit there, empty, waiting for whoever wrote main to remember to fill them in correctly. On a two-field class with one main method, that is a small risk. On a class used from fifty places across a real program, "whoever creates one remembers to set it up correctly" is not a plan.
The fix is to give the class its own setup code, so that creating a Ticket and getting a valid one are the same act. That setup code is called a constructor: a block that runs automatically every time new builds an object, before that object is handed back to whoever asked for it. A constructor looks like a method with no return type, and its name must be exactly the class name.
Here is the same class with one:
public class Ticket {
String plate;
int arrivedAtMinute;
public Ticket(String plate, int arrivedAtMinute) {
this.plate = plate;
this.arrivedAtMinute = arrivedAtMinute;
}
}
Now new Ticket() with no arguments does not compile at all. The class has a constructor that demands a plate and an arrival minute, so new Ticket("KA-01-4432", 555) is the only way to build one, and both fields are set the moment the object exists. That already removes the "forgot to set it" mistake. It does not yet stop new Ticket("", -400), because the constructor is only copying whatever it was given. The second half of this chapter makes the constructor check what it is given, and refuse to build the object at all when the values are wrong. Once that is in place, holding a Ticket is proof its fields are valid. Not because you trust whoever created it. Because there was no way to create it otherwise.
2. Type this
Make a new file, Ticket.java. Type this in fully. It builds on the version from 1.1, so if the shape looks familiar in places, that is deliberate.
public class Ticket {
private final String plate;
private final int arrivedAtMinute;
public Ticket(String plate, int arrivedAtMinute) {
if (plate == null || plate.isBlank()) {
throw new IllegalArgumentException("plate cannot be blank");
}
if (arrivedAtMinute < 0) {
throw new IllegalArgumentException("arrivedAtMinute cannot be negative");
}
this.plate = plate;
this.arrivedAtMinute = arrivedAtMinute;
}
public String getPlate() {
return plate;
}
public int getArrivedAtMinute() {
return arrivedAtMinute;
}
public int minutesSince(Ticket other) {
return this.arrivedAtMinute - other.arrivedAtMinute;
}
public static void main(String[] args) {
Ticket first = new Ticket("KA-01-4432", 555);
Ticket second = new Ticket("MH-12-9001", 600);
System.out.println("first plate: " + first.getPlate());
System.out.println("second plate: " + second.getPlate());
System.out.println("minutes apart: " + second.minutesSince(first));
}
}
3. Run it
javac Ticket.java
java Ticket
You should see exactly this:
first plate: KA-01-4432
second plate: MH-12-9001
minutes apart: 45
Same three lines as chapter 1.1, but every value in them was set by the constructor. There is no line anywhere in this file that reaches into a Ticket and pokes a field directly from the outside.
4. What just happened, line by line
private final String plate; has two new words on it, and each does a separate job.
private means this field can only be read or written by code inside the Ticket class itself. Code in another class, main inside a different file, for instance, cannot write someTicket.plate = "x" at all; the compiler refuses it. In chapter 1.1, plate had no access keyword, which in Java defaults to letting any class in the same package read and write it directly. private closes that door. The only way to get a value into plate from outside the class is through code the class itself controls, which is exactly the constructor you are about to read.
final means this field can be assigned exactly once, inside the constructor, and never reassigned after that. Try to write this.plate = plate; a second time anywhere else in the class and the compiler stops you. Combined with private, final means nobody outside can set this field, and nothing inside can change it after construction either. The full weight of why that matters is chapter 2.2's subject, in full. For now, take the mechanical fact: a final field is set once and stays set.
public Ticket(String plate, int arrivedAtMinute) { is the constructor. Three things mark it as one rather than an ordinary method: its name is exactly Ticket, matching the class, and it has no return type at all — not even void. Java runs this block automatically, once, every time `new Ticket(...)` appears in code, before the new object is handed back to the caller.
if (plate == null || plate.isBlank()) { throw new IllegalArgumentException("plate cannot be blank"); } is the check that makes a broken Ticket impossible to build. plate.isBlank() asks the String whether it is empty or made only of whitespace. throw stops the constructor immediately and reports a failure, using a mechanism called an exception, instead of letting the object finish being built with a bad value inside it. IllegalArgumentException is a class the Java standard library already provides for exactly this situation: an argument to a method is not acceptable. Exceptions get a full chapter later, chapter 1.9. What matters here is the effect. If this line runs, this.plate = plate; never runs, no Ticket object is produced, and whatever called new Ticket(...) has to deal with the failure instead of getting a bad ticket back.
this.plate = plate; copies the constructor's parameter into the object's field. this refers to the object being built right now. It is needed here because the parameter and the field have the same name, plate — a deliberate and common style, since the parameter's whole job is to become the field. this.plate means "the field on this object"; the bare plate on the right means "the parameter that was passed in." Without this, plate = plate; would just assign the parameter to itself and the field would stay whatever it was before, which for a field that has never been set is not what you want.
public String getPlate() { is a method: a named, reusable block of code that belongs to the class. public means outside code can call it. String before the name is the method's return type — the kind of value this method hands back to whoever calls it. return plate; is that handoff: execution stops here and the value of plate travels back to the caller. A method with a return type must return a value of that type on every path through it. A method that returns nothing at all is declared void instead, the way main was in chapter 1.1.
Methods like getPlate() and getArrivedAtMinute() exist because the fields are now private — this is the only way outside code can read them. They are called getters, and the name is exactly that literal: get, plus the field name with a capital first letter. Notice there is no setPlate(String). That is not an oversight. Nothing in this class needs to change a ticket's plate after creation, and not writing a method you do not need is the cheapest way to keep a class hard to misuse.
public int minutesSince(Ticket other) { is a method that takes a parameter of type Ticket, does work with it, and returns an int. this.arrivedAtMinute - other.arrivedAtMinute reads the arrival time of the object the method was called on (this) and subtracts the arrival time of the ticket passed in (other). Called as second.minutesSince(first), this is second and other is first, so the result is second's arrival minute minus first's: 600 - 555, which is 45.
5. Errors you are likely to hit
You call the old no-argument constructor. If you still have code lying around from chapter 1.1 that wrote new Ticket(), it now fails to compile, because that constructor no longer exists:
NoArgTest.java:3: error: constructor Ticket in class Ticket cannot be applied to given types;
Ticket t = new Ticket();
^
required: String,int
found: no arguments
reason: actual and formal argument lists differ in length
This is not a bug in your code. Once a class declares any constructor, Java stops providing the argument-free one it silently supplies to a class with no constructors at all. Every Ticket now has to say what its plate and arrival time are at the moment it is created — which is exactly the point.
You reach for a private field from outside the class. Add a second file in the same folder:
public class PrivateFieldTest {
public static void main(String[] args) {
Ticket t = new Ticket("KA-01-4432", 555);
System.out.println(t.plate);
}
}
PrivateFieldTest.java:4: error: plate has private access in Ticket
System.out.println(t.plate);
^
The fix is t.getPlate(). This error is private doing its job: PrivateFieldTest is a different class, so it cannot see plate directly, only through a method Ticket chooses to expose.
You pass a value the constructor rejects. Try building a ticket with a negative arrival time:
public class BadTicket {
public static void main(String[] args) {
Ticket t = new Ticket("KA-01-4432", -5);
System.out.println(t.getPlate());
}
}
Exception in thread "main" java.lang.IllegalArgumentException: arrivedAtMinute cannot be negative
at Ticket.<init>(Ticket.java:11)
at BadTicket.main(BadTicket.java:3)
Ticket.<init> is Java's internal name for a constructor; the line points straight at the throw statement that rejected the value. println on the line after never runs — the program stopped when the constructor threw, so t was never actually a Ticket in the first place. This is the whole mechanism working exactly as intended: a bad ticket cannot come into existence, it can only fail loudly on the way in.
6. Your turn
Add a method to Ticket:
public int elapsedMinutes(int nowMinute)
It should return how long the ticket has been parked, given the current time in minutes. It should also reject a nowMinute earlier than the ticket's arrival, because that combination cannot happen in reality, the same way the constructor refuses bad input. Call it on second with nowMinute = 700 and print the result. Do it before reading on.
The answer.
public int elapsedMinutes(int nowMinute) {
if (nowMinute < arrivedAtMinute) {
throw new IllegalArgumentException("nowMinute cannot be before arrivedAtMinute");
}
return nowMinute - arrivedAtMinute;
}
Called as second.elapsedMinutes(700) and printed as `System.out.println("elapsed: " + second.elapsedMinutes(700));`, this prints:
elapsed: 100
second arrived at minute 600, so 700 - 600 is 100. If you got 100, the method is correct. Notice also that arrivedAtMinute is read directly here, without this., because no parameter shares its name in this method. this. is only required when a name is ambiguous, as it was for plate inside the constructor.
7. Going deeper: the order a constructor actually runs in
Here is something that surprises people who already know another language. One class can be built on top of another using the keyword extends; full treatment is chapter 1.4, but one sentence of it is enough here. A class that extends another one inherits its fields and methods, and can replace, or override, an inherited method by writing a method with the same name in the subclass. Watch what happens when a constructor calls a method that gets overridden:
class Vehicle {
Vehicle() {
System.out.println("category during Vehicle constructor: " + category());
}
String category() {
return "vehicle";
}
}
class Car extends Vehicle {
private String model = "sedan";
@Override
String category() {
return model;
}
}
public class ConstructorOrder {
public static void main(String[] args) {
new Car();
}
}
Compile and run it, and the output is:
category during Vehicle constructor: null
Not "sedan". Here is why. When new Car() runs, Java first runs Vehicle's constructor, because a subclass's constructor always runs its superclass's constructor before doing anything else. An object is built from the top of its family tree downward. Vehicle's constructor calls category(), and Java looks up which category() to run based on the object's real type, which is Car, not Vehicle. So Car's version runs, and it returns model. But model = "sedan" is a field initializer on Car, and field initializers run after the superclass constructor finishes, not before. At the moment Vehicle's constructor calls category(), Car's part of the object has not been set up yet, so model is still sitting at Java's empty-String-field default, null.
The practical rule this proves: never call a method that a subclass might override from inside a constructor, unless the method touches nothing that depends on the subclass's own fields. Those fields are not there yet. This is a real, well-known Java pitfall, not specific to this course. Being able to explain why it happens, rather than avoiding it by habit alone, is the kind of thing an interviewer notices.
8. Why this matters in an interview
A design round almost always has a class whose whole purpose is to guarantee an invariant, a rule that must always be true about every object of that type. A Ticket that always has a plate and a non-negative arrival time is a small version of a bigger need. An Account can never go negative. A Reservation can never end before it starts. The technique is the same one you just used: make the fields private, do every check inside the constructor, and throw rather than build a broken object. An interviewer who reaches for this by default, without being asked, has internalized encapsulation rather than memorized the word. Encapsulation itself is chapter 2.1, the very next stop after Part 1.
The second thing worth naming is what you did not write: a setPlate method. Every setter you add is another place an invariant can be broken after construction, by code far away from the constructor that first checked it. A class with no setters and only a validating constructor cannot be corrupted once built. That is a stronger guarantee than "we remember to check before every mutation," and it costs nothing here because nothing in the program ever needed to change a ticket's plate.
Next: chapter 1.3, References, null, and what equals really compares. first and second stop being "two tickets" there and become what they actually are in Java: two variables holding references to two separate objects. That distinction is behind a huge share of the bugs beginners hit for years.
← 1.1 A class, an object, and main() · All chapters · 1.3 References, null, and what equals really compares →