Creating a class
A class is a decision about what an object remembers and who is allowed to change it. You pick the fields. You write the one way in. You write the ways out that you actually want, and you leave out the ones you do not. Every one of those choices says what a valid object of your class looks like, and says that nothing outside the class gets to skip the check.
This lesson builds one small class and keeps it the same the whole way through, a Student with a name and a gpa.
Fields, and why they are private
A field is a variable that belongs to an object rather than to a method. Every Student you build carries its own name and its own gpa, separate from every other one.
public class Student
{
private String name;
private double gpa;
}
Both fields are private, and that word is doing real work. Nothing outside this file can hand name or gpa a value. The only ways in are the ones written inside the class. Because those are the only ways in, the class gets to decide what counts as a valid Student and can actually hold that line, since it is the only code touching the fields. A public field has no door, so it has no promise attached to it.
Before your code runs at all, Java has already put the object in memory and given every field a default: a reference field like name starts as null, and a numeric field like gpa starts at 0.0. You did not write those values. Overwriting them with real ones, before anybody else can look, is the constructor's job.
The constructor, the one way in
A constructor runs once, at the moment new Student(...) executes, and it has one job: leave the object in a state you are willing to stand behind.
public Student(String name, double gpa)
{
if (name == null)
{
throw new IllegalArgumentException("name cannot be null");
}
this.name = name;
this.gpa = gpa;
}
The check comes before either assignment, and the reason is the word "before". If the name really is null, the throw happens while the object is still half built, and the half-built object never reaches the caller. new hands back nothing at all when the constructor throws. That is what a constructor promises: every object of this class that exists anywhere passed this check.
gpa gets no null check and does not need one. A double is a primitive, so it always holds a number, never the absence of one. name is a reference, so it can point at nothing, and null would otherwise sail straight into the field and sit there until something reads it and dies a long way from here.
name = name; and losing an afternoon
The parameter and the field have the same name on purpose, and that is exactly why you need this.. Inside the constructor the bare word name means the parameter, because the parameter is closer in scope and covers the field. this.name reaches past it to the field on the object being built. Drop the this. and write name = name; and the line still compiles, because assigning a parameter to itself is legal Java. It just does nothing at all. The field keeps its default forever, printing the object shows null, and no warning ever fires. Put this. in front of a field every time, even where nothing is covering it, because the day it matters is the day you will not be looking.
The ways out, and the ones you leave out
public String getName()
{
return this.name;
}
public double getGpa()
{
return this.gpa;
}
public void setGpa(double gpa)
{
this.gpa = gpa;
}
The 2 getters are read doors. They hand back a value without letting outside code touch the field. setGpa is a write door, and it exists because you decided a gpa is allowed to move over a student's career. There is no setName, and that absence is a decision, not an oversight: once a Student is built its name does not change, so there is a way to read it and no way to change it.
If you did add a setName later, it would need the same null check the constructor has. A promise about a field is only as strong as the weakest door into that field. A setter that skips the check is a second way in that ignores what the first way in guaranteed, and from then on the class promises nothing.
Putting the rule in one place
When a field has a rule, write the rule in the setter and make the constructor go through the setter instead of around it.
public class Circle
{
private double radius;
public Circle(double radius)
{
setRadius(radius);
}
public void setRadius(double radius)
{
if (radius > 0.0)
{
this.radius = radius;
}
else
{
throw new IllegalArgumentException("radius must be greater than 0");
}
}
}
Now there is one line in the whole class that decides what a legal radius is. Change the rule and you change it once. Write this.radius = radius; in the constructor instead and you have 2 places that both claim to guard the field, one of which does not, and new Circle(-1.0) builds a circle with a negative radius that nothing will ever complain about. It will not throw. It will just quietly report an area for a shape that cannot exist.
What printing an object shows
If you never say otherwise, printing a Student gives you something like Student@1a2b3c, which is the class name and a number about where the object sits in memory. It tells you nothing about the student. Replacing that with something readable is toString, and it has a lesson of its own: Overriding a method. Write one on every class you build. It costs 5 minutes and it pays for itself the first time you print an object while chasing a bug.
In the practice APE
The practice exam posted on the official site, the one described in the Practice APE Exam Instructions, hands you a Circle with the shape above already half built. The field and the setter are written. The constructors are not.
public class Circle extends Shape
{
private double radius;
public Circle(double radius)
{
}
public double area()
{
return 0.0f;
}
public double circumference()
{
return 0.0f;
}
public void setRadius(double radius)
{
if (radius > 0.0)
this.radius = radius;
else
throw new IllegalArgumentException("radius must be greater than 0");
}
}
The comment at the top of the file is the specification, and it words the rule for you: the constructor must set the radius and the name using the tools provided in Circle and in Shape, and must not assign into the radius or the name field directly. That sentence is this whole lesson in one line. The door exists, it holds the rule, and going around it puts the rule somewhere it can be forgotten.
The name half of that sentence belongs to the parent class and is covered in Reaching the super class. The radius half is setRadius, which is already written and already correct.
The other practice problem on this topic hands you a Student whose fields, constructor and toString are all written for you, so you can read a finished small class before you write one. Its fields are private String first, last; and private int studentId;, its constructor takes all 3, and its toString returns the last name, a comma, the first name, a colon and the id. That file is the worked example inside Implementing Comparable.
Readiness check
Work each one on paper and write the answer down before you open it.
Use the Student class from the top of this lesson, with the null check and both assignments.
Student s = new Student("Ada", 3.9);What doess.getName()return, and what doess.getGpa()return?- Someone deletes the
this.from both assignments, so the body readsname = name;andgpa = gpa;. Does it compile? After the same call, what doess.getName()return, and what doess.getGpa()return? Student s = new Student(null, 3.9);What happens, and does aStudentobject reach the variables?
Answers
1. Ada and 3.9.
2. It compiles, because assigning a parameter to itself is legal. Both fields keep the defaults Java gave them, so getName() returns null and getGpa() returns 0.0. Nothing warns you.
3. An IllegalArgumentException with the message name cannot be null. No object reaches s. The throw happens before either assignment, so there is no half-built Student anywhere for anyone to find.
Use the Circle above, whose setRadius throws when the radius is not greater than 0.
- The constructor is
setRadius(radius);. What doesnew Circle(-1.0)do? - The constructor is changed to
this.radius = radius;instead. What doesnew Circle(-1.0)do now, and what wouldarea()return for that object ifareais PI times the radius squared? - With the good constructor back, a caller writes
c.setRadius(0.0);on a circle that was built with radius 2.0. What happens, and what is the radius afterward?
Answers
1. It throws an IllegalArgumentException whose message is radius must be greater than 0, and no Circle is handed back.
2. It builds a Circle with a radius of -1.0 and throws nothing. area() returns 3.141592653589793, because squaring the radius throws the sign away. A circle that cannot exist reports a perfectly ordinary area, which is the kind of bug that shows up 3 methods later with nothing pointing back here.
3. It throws, because 0.0 is not greater than 0.0, and the radius is still 2.0 afterward. The assignment is inside the if, so a rejected value never reaches the field. That is the whole reason the check and the assignment live in the same method.
A class declares private String name; and private double gpa; and its constructor throws before it assigns anything.
- At the instant the constructor's first line begins running, what is in
nameand what is ingpa? - Two students are built with the same name and the same gpa. Is there 1 object or 2, and does each one have its own pair of fields?
- The class has a getter for both fields and a setter for neither. Name every piece of code that is still able to change
gpa.
Answers
1. null and 0.0. Java assigns those defaults as part of putting the object in memory, before any line you wrote runs.
2. Two objects, each with its own name and its own gpa. Fields belong to the object, not to the class, so the 2 students share nothing.
3. Only code inside the class itself, which right now means only the constructor. With no setter there is no way in from outside, and that is a promise the class can keep, not a feature it forgot.
Further reading
- Object (Java SE 19 & JDK 19). Every class you write already extends this one, which is where the default
toString,equalsandhashCodecome from. - IllegalArgumentException (Java SE 19 & JDK 19). The exception a rejected argument raises, and the constructor that takes the message.
- Lesson: Classes and Objects (The Java Tutorials). Oracle's own trail on building a class, start to finish.
- Declaring Member Variables (The Java Tutorials). Field declarations, access modifiers and the default values Java gives before your code runs.
- Providing Constructors for Your Classes (The Java Tutorials). What a constructor is, and what happens when you write none.
- Controlling Access to Members of a Class (The Java Tutorials). The table that says exactly who can see private, package, protected and public.
- Using the this Keyword (The Java Tutorials). Why
this.name = name;reads the way it does. - Class (programming) (Wikipedia). The general idea, across languages, not only Java.
- Encapsulation (computer programming) (Wikipedia). Why the fields are private, stated as a principle rather than as a Java rule.
- Reading 10: Abstract Data Types, from MIT 6.031, Software Construction. The strongest argument anywhere for choosing operations before choosing fields.
- Reading 11: Abstraction Functions & Rep Invariants, MIT 6.031. Names the thing a constructor is protecting: the condition your fields must always satisfy.
- Lecture 4: Classes and Objects, MIT 6.092, on MIT OpenCourseWare. Slides covering the same ground in about 20 pages.
- Chapter 8. Classes (The Java Language Specification, Java SE 19). The rules themselves, if you want to see what the compiler is actually reading.
The one thing to keep
Private fields, a constructor that checks what it must before it assigns anything, and only the doors out you actually chose to build. Every field either gets a setter because you decided it should change or it does not, because you decided it should not. When a field has a rule, the rule lives in one method and everything else calls that method, so there is exactly one line to get right and exactly one line to change.