HomeGuidesJava
Java13 min readCode verified

Write Your Own Java Class, One Compiler Error at a Time

The assignment is always the same: model something you can touch, store an attribute, write a method that changes it, a method that returns it, a method that describes all of them, and make two objects. Here is a draft that gets most of that right and still fails — and every fix it needs. Every error message and every line of output on this page was produced by running the code.

A draft that looks finished#

Somebody set out to model a car. They named the class, listed four attributes, commented every one, wrote a method that changes an attribute and a method that returns it, and reached for a Scanner at the end. This is a normal, honest first attempt:

public class MyCar {
    private int year;          //Year of vehicle
    private string make;       //Make of Vehicle
    private string model:      //Model of Vehicle
    private string color;      //Color of vehicle

    public void setYear(int year){
        year = 2026;
    }

    public int getYear(){
        return year;
    }

    Scanner Keyboard = new scanner(System.in);

How many things are wrong with it? Count them before you scroll. Most people find two or three. There are six, and the most expensive one compiles perfectly.

MyCar.java:4: error: ';' expected
    private string model:      //Model of Vehicle
                        ^
MyCar.java:16: error: reached end of file while parsing
    Scanner Keyboard = new scanner(System.in);
                                              ^
2 errors

Two errors, and neither one mentions string. That is the first thing worth understanding about javac: it cannot look for missing types until it has finished reading the shape of the file. A stray : and a missing } stop it at the parsing stage, so everything after that is still unexamined. Fix errors from the top down, and recompile after each one — the list you get next is a different list, not a shorter one.

“reached end of file while parsing”

This message almost always means one thing: a missing closing brace. The compiler read the whole file still waiting for a } that never arrived. It points at the last line because that is where it ran out of file, not because that line is wrong.

Fix the syntax, and the next layer appears#

Change the : to a ;, close the class with a }, and compile again. Same file, otherwise untouched:

MyCar.java:3: error: cannot find symbol
    private string make;       //Make of Vehicle
            ^
  symbol:   class string
  location: class MyCar
MyCar.java:4: error: cannot find symbol
    private string model;      //Model of Vehicle
            ^
  symbol:   class string
  location: class MyCar
MyCar.java:5: error: cannot find symbol
    private string color;      //Color of vehicle
            ^
  symbol:   class string
  location: class MyCar
MyCar.java:15: error: cannot find symbol
    Scanner Keyboard = new scanner(System.in);
    ^
  symbol:   class Scanner
  location: class MyCar
MyCar.java:15: error: cannot find symbol
    Scanner Keyboard = new scanner(System.in);
                           ^
  symbol:   class scanner
  location: class MyCar
5 errors

Five errors from two mistakes, both of them about capital letters.

String is a class, and Java is case sensitive all the way down. string is not a misspelling that the compiler forgives — it is a name for a class that does not exist, which is exactly what cannot find symbol: class string says. The same goes for new scanner(...). Convention is not decoration here: class names begin with a capital letter, variables and methods begin with a lowercase one, and the compiler enforces the first half of that rule whether you meant it or not.

Scanner on line 15 fails for a different reason. It is spelled correctly, and the compiler still cannot find it, because Scanner lives in java.util and nothing has said so. That needs a line above the class:

import java.util.Scanner;

String never needs an import because it lives in java.lang, which is imported into every file automatically. That is the whole difference between the two.

The Scanner should not be here at all#

Adding the import would make line 15 compile. It would still be the wrong line to write, and this is a design point worth more marks than the spelling ones.

MyCar models a car. A car has a year, a make, a model and a colour. A car does not have a keyboard. A Scanner field describes how one particular program happens to collect information — and the moment it is inside the class, MyCar can only be used by programs that have a console attached. Not a test, not a GUI, not a file reader.

Input belongs in the class that runs the program, next to main:

import java.util.Scanner;

public class Garage {
    public static void main(String[] args) {
        Scanner keyboard = new Scanner(System.in);
        System.out.print("Year: ");
        int year = keyboard.nextInt();
        // ...and then hand the values to the class
    }
}

The useful test, whenever you are deciding whether something belongs in a class: is this a fact about the object, or a fact about this program? Year, make, model and colour are facts about the car. The keyboard is a fact about the program.

Now it compiles, and it is still wrong#

Capitalise the types, drop the Scanner, and the class finally compiles. Here it is, clean:

public class MyCar {
    private int year;
    private String make;
    private String model;
    private String color;

    public void setYear(int year){
        year = 2026;
    }

    public int getYear(){
        return year;
    }
}

No errors. Not even a warning with javac -Xlint:all. So test it — make a car, set its year, read the year back:

public class Demo {
    public static void main(String[] args) {
        MyCar mine = new MyCar();
        mine.setYear(2020);
        System.out.println(mine.getYear());
    }
}

What prints?

0

Not 2020, and not 2026 either. Zero.

Two separate things went wrong in three lines of method body, and neither one is a typo.

1. The parameter hides the field

Inside setYear(int year) there are two things called year: the field belonging to the object, and the parameter belonging to the method. When a name is ambiguous, Java always resolves it to the closest one — the parameter. So the line

year = 2026;

assigns 2026 to the parameter, which stops existing the instant the method returns. The field is never touched, so it keeps the value it was born with: 0, the default for an int.

This is called shadowing, and it is the single most common bug in a first set of setters. The fix is the keyword this, which means the object this method was called on:

public void setYear(int year) {
    this.year = year;   // this.year is the field, year is the parameter
}

Read it left to right and it says what it does: put the parameter into the field. Nothing else in the method needs this., because nothing else is ambiguous — getYear can return a bare year and be perfectly clear, since there is no parameter competing with it.

Why nothing warns you

year = 2026; is a legal assignment to a legal variable. The compiler has no way to know you meant the other one, so it says nothing — not an error, not a warning, not with every lint check switched on. The only symptom is a getter that keeps returning 0 or null. If a setter appears to do nothing, look for a missing this. before you look anywhere else.

2. A setter that ignores its own parameter

Even with this. in place, this.year = 2026; would be wrong. A mutator's job is to store the value it was handed, not a value chosen when the code was written. Hard-coding 2026 makes setYear a method that can only ever mean one year, and the parameter becomes decoration.

The shape almost never varies:

public void setThing(Type thing) { this.thing = thing; }
public Type getThing()            { return thing; }

Accessors, mutators, and what private buys you#

The two method names have formal words attached, and exams use them:

TermAlso calledWhat it doesShape
Accessorgetterreports an attribute, changes nothingpublic int getYear()
Mutatorsetterchanges an attribute, returns nothingpublic void setYear(int y)

They only mean anything because the fields are private. Try to reach a private field from outside its class and the compiler stops you:

MyCar mine = new MyCar(2020, "Honda", "Civic", "blue");
mine.year = 1999;
P2.java:4: error: year has private access in MyCar
        mine.year = 1999;
         ^
1 error

That error is the feature. Because the only route to year runs through setYear, the class gets to have an opinion about what a year is allowed to be, and that opinion lives in exactly one place:

public void setYear(int year) {
    if (year >= 1886 && year <= 2100) {
        this.year = year;
    }
}

Make the field public and that check is unenforceable, because any line of code anywhere can walk straight past it. This is what encapsulation means in practice — not secrecy, but keeping the rules about an attribute in the same place as the attribute.

Java has four levels of access, and for a class like this one you only need the two ends:

ModifierVisible toUse it for
privatethis class onlyfields, and helper methods nobody outside should call
(none)other classes in the same packagerarely a deliberate choice; it is what you get by accident
protectedsame package, plus subclassesmembers a subclass genuinely needs
publiceverywherethe class itself, its constructors, and its accessors and mutators

The default choice for a class you are writing today: every field private, every method that the outside world is meant to call public. Deviate only when you can say why.

Not every field needs a setter

A car's colour can change; its make cannot become Toyota. Leaving out setMake is a decision, and a defensible one — a field with a getter and no setter is read-only to the outside world. If your assignment asks for a full set of accessors and mutators, write them all; otherwise, giving a setter to something that never changes is a small design mistake, not a safe default.

The constructor#

So far every MyCar is born empty and has to be filled in one setter at a time — four calls before the object is usable, and nothing stops you forgetting one. A constructor is the method that runs once, at creation, to put the object into a valid state immediately.

It has two distinguishing features and no others: its name is exactly the class name, and it declares no return type — not even void.

public MyCar(int year, String make, String model, String color) {
    this.year  = year;
    this.make  = make;
    this.model = model;
    this.color = color;
}

Same this. rule as the setter, four times over, and for the same reason. Now one line does what four did:

MyCar mine = new MyCar(2020, "Honda", "Civic", "blue");

There is a catch that surprises people, and it is worth meeting on purpose rather than at 2am. Until you wrote that constructor, new MyCar() worked — a class with no constructor gets a free no-argument one that leaves every field at its default. The moment you write any constructor, the free one disappears:

P3.java:3: error: constructor MyCar in class MyCar cannot be applied to given types;
        MyCar p = new MyCar();
                  ^
  required: int,String,String,String
  found:    no arguments
  reason: actual and formal argument lists differ in length

If you want both ways of making a car, write both — two constructors with different parameter lists is overloading, and it is normal:

public MyCar() {
    this(2026, "unknown", "unknown", "unpainted");
}

this(...) on the first line of a constructor calls another constructor of the same class, so the real work stays written once.

Those defaults matter, because they are what an uninitialised field holds: 0 for int, 0.0 for double, false for boolean, and null for every object reference — String included. A String field you forgot to set is not ""; it is null, and it will print as the word null.

toString: the method that describes everything#

Requirement five — a method that returns a String describing all of the attributes — already has a name in Java, and using that name is what makes it work everywhere. Print an object without it and you get this:

MyCar@2a139a55

That is not an error, and not a bug. Every class in Java inherits a toString from Object, and the inherited one prints the class name, an @, and the object's hash code in hexadecimal. It is a machine's answer. The number changes between runs, and it is not an address you can use for anything.

Give the class its own and the machine's answer is replaced by yours:

@Override
public String toString() {
    return year + " " + make + " " + model + " (" + color + ")";
}

The signature has to match exactly — public, returns String, named toString, no parameters. Name it describe() instead and it is a perfectly good method that Java will never call on your behalf.

And it does call it on your behalf, in two places you use constantly: System.out.println(mine), and any string concatenation such as "Repainted: " + mine. You never write mine.toString() in normal code.

What @Override is for

It is optional — the method works without it. What it does is ask the compiler to check that you really are replacing an inherited method. Write toSting by mistake and, without the annotation, you get a new method and silent wrong output; with it, you get a compile error naming the problem. Put it on every method you intend to override.

The finished class#

Every fix, assembled. This compiles clean under javac -Xlint:all on OpenJDK 21:

public class MyCar {

    // Attributes — private, so only MyCar can change them directly.
    private int year;
    private String make;
    private String model;
    private String color;

    // Constructor — runs once, when the object is created.
    public MyCar(int year, String make, String model, String color) {
        this.year  = year;
        this.make  = make;
        this.model = model;
        this.color = color;
    }

    // Mutators — change an attribute.
    public void setYear(int year)      { this.year  = year;  }
    public void setColor(String color) { this.color = color; }

    // Accessors — report an attribute.
    public int    getYear()  { return year;  }
    public String getMake()  { return make;  }
    public String getModel() { return model; }
    public String getColor() { return color; }

    // Describes every attribute as one String.
    @Override
    public String toString() {
        return year + " " + make + " " + model + " (" + color + ")";
    }
}

And the class that uses it. main lives here, not in MyCar — one class models the thing, the other runs the program:

public class Garage {
    public static void main(String[] args) {
        MyCar mine = new MyCar(2020, "Honda",  "Civic",   "blue");
        MyCar hers = new MyCar(2017, "Toyota", "Corolla", "silver");

        System.out.println(mine);
        System.out.println(hers);

        mine.setColor("red");
        mine.setYear(2021);

        System.out.println("Repainted: " + mine);
        System.out.println("Year is now " + mine.getYear());
    }
}
2020 Honda Civic (blue)
2017 Toyota Corolla (silver)
Repainted: 2021 Honda Civic (red)
Year is now 2021

Look at the third and fourth lines against the second. Changing mine did nothing at all to hers, because new was called twice and each call built a separate object with its own copy of all four fields. The class is one description; the objects are independent things made from it. That is the whole idea, and printing two of them side by side is the cheapest way to see it.

The six requirements, checked off#

The assignment on the board is nearly always worded the same way. Against the finished code:

#RequirementWhere it is
1a class that models the objectpublic class MyCar
2at least one attributefour private fields
3a method that changes an attributesetYear, setColor — with this.
4a method that returns an attributegetYear and the other three accessors
5a method returning a String describing all attributestoString
6at least two objects instantiatedmine and hers in Garage.main
appropriate access modifier on each memberfields private, class and methods public

Where these go wrong#

  • A setter without this.. Compiles, runs, warns about nothing, and does nothing. Symptom: a getter that always returns 0 or null.
  • A setter that hard-codes a value instead of storing its parameter. The parameter list is the giveaway — if the body never mentions the parameter, the method is ignoring its own input.
  • Lowercase string or scanner. cannot find symbol naming a class you did not think you wrote is nearly always a capitalisation problem.
  • A colon instead of a semicolon, or any missing brace. These stop the parser, which is why the error list afterwards looks unrelated to the mistake and gets longer once you fix them.
  • A Scanner field inside the model class. Compiles once imported, and welds the class to a console it should know nothing about.
  • Writing a constructor and then calling new MyCar(). The free no-argument constructor is gone the moment you write one of your own.
  • Naming the describe-everything method anything but toString. It works when you call it and never runs when println would have called it for you.
  • Two classes, one file, both public. Only one public class per file, and the file must be named after it — MyCar.java and Garage.java, not both in one.

Test yourself in the free Kestrel Exams app

Topic-selectable practice — offline, no ads, no account.

Practice this topic →

Frequently asked questions#

Why does my setter not change anything?

Almost always because the parameter has the same name as the field, so the assignment writes the parameter back onto itself and the field is never touched. Inside setYear(int year), the bare name year means the parameter. Write this.year = year, where this.year is the field and year is the parameter. It compiles either way and javac gives no warning, so the only symptom is a getter that keeps returning 0.

What is the difference between an accessor and a mutator?

An accessor returns the value of an attribute and changes nothing — getYear(). A mutator changes an attribute and usually returns nothing — setYear(int year). They are the same idea as getters and setters; accessor and mutator are the words most textbooks and exams use.

Why write private fields and public getters instead of just making the fields public?

Because a private field can only be changed by code inside the class, which means the class can enforce its own rules. If year is public, any code anywhere can set it to −4000 and the class cannot stop it. If year is private and the only way in is setYear, the check lives in one place. It also means you can change how the attribute is stored later without breaking every caller.

Do I have to write a constructor?

No. If you write no constructor at all, Java supplies a no-argument one that leaves every field at its default (0 for numbers, false for boolean, null for object references such as String). But the moment you write any constructor, that free one disappears. If you write MyCar(int, String, String, String) and then call new MyCar(), the compiler reports that the constructor cannot be applied to the given types.

Why does printing my object show something like MyCar@2a139a55?

That is the toString inherited from Object: the class name, an @, and the hash code in hexadecimal. It is not an error and not a memory address you can use. Write your own public String toString() that returns a readable line, and println will call it automatically.

Should a class like MyCar contain a Scanner?

No. A class that models an object should hold what the object is, not how a program gets input. Put the Scanner in the class with main, read the values there, and pass them into the constructor. A Scanner field also makes the class unusable anywhere without a console — a test, a GUI, or a file reader.

Is Java case sensitive for type names like String?

Yes, completely. String is a class and string is not a type at all, so private string make; fails with cannot find symbol: class string. The same applies to Scanner and scanner. Class names start with a capital letter by convention and the compiler treats a different capitalisation as a different, non-existent name.

Suggest a change

Something here not clear? A topic you wish we covered? Tell us. We read every message, and a request is the fastest way to get a guide written — several of these exist because somebody asked.