Unit 2

Class Structure and Design

Test prepUnit 2 FRQ Test — what to study

The format. You'll write one complete Java class by hand, from scratch — the class header, instance variables, and every method the prompt asks for. Unlike the Unit 1 FRQ Test, you are not given the scoring guidelines during this test. You may use your own hand-written notes, though, so a checklist like the one below is worth copying into them.

The scoring. The test is worth 18 points — nine scored tasks at 2 points each. Work that's attempted but not fully correct still earns 1 point — half credit — so attempt every task. A constructor with one mistake in it is worth a point; a blank space is worth nothing.

What you need to be able to write

Treat this as your checklist — it groups the test's nine scored tasks into eight things to practice (each instance variable is its own scored task on the real test, but they're one skill to drill here). Practice each one by hand, on paper.

  1. The class header.
  2. The instance variables, declared private.
  3. A no-argument constructor that sets every instance variable to a sensible default.
  4. A one-parameter constructor that sets one instance variable from its argument and sets the other to a default — the actual shape used on the Unit 2 FRQ Test and Practice Test.
  5. A two-parameter constructor that sets both instance variables from arguments.
  6. Accessor method(s) ("getters") — one per instance variable, returning that variable's value.
  7. Mutator method(s) ("setters") — void, take a parameter, and reassign an instance variable.
  8. A toString method that returns a String with each instance variable's information on its own line — the exact format the Unit 2 FRQ Test and Practice Test both ask for.

An example with the same shape

Your test will use different names and different values. Study the shapes here, not this code.

public class ScoreBoard {

  private String teamName;
  private int totalPoints;

  public ScoreBoard() {
    teamName = "Unknown";
    totalPoints = 0;
  }

  public ScoreBoard(String startName) {
    teamName = startName;
    totalPoints = 0;
  }

  public ScoreBoard(String startName, int startPoints) {
    teamName = startName;
    totalPoints = startPoints;
  }

  public String getTeamName() {
    return teamName;
  }

  public void setTotalPoints(int points) {
    totalPoints = points;
  }

  public String toString() {
    return "Team: " + teamName + "\nPoints: " + totalPoints;
  }
}

Mistakes that cost points

  • Forgetting the class header or its closing brace
  • Writing teamName = teamName; inside a constructor or mutator instead of this.teamName = teamName; — or sidestepping the problem entirely by naming the parameter something else, like startName
  • Giving an accessor the wrong return type, or forgetting to return at all
  • Forgetting that a mutator method is void — it reassigns, it doesn't return
  • Forgetting to set the other instance variable to its default inside the one-parameter constructor — every constructor has to leave both variables initialized
  • Giving two constructors the same parameter list, which won't compile
  • Missing semicolons, or braces that don't close

How to practice

  • Write a whole class like the one above from memory, on paper, then check it line by line against this checklist.
  • Do it more than once — writing a full class out three times from scratch is worth more than re-reading these notes once.
  • We'll do the Unit 2 FRQ Practice Test together in class — it asks for the same nine pieces with different data, so it's the closest rehearsal there is. You don't need to wait for it to start practicing on your own.
  • Ask the tutor to quiz you on any single piece — a no-argument constructor, an accessor, a toString — so you can drill the ones you're least sure of.

Select a topic to expand it.

2.1Instance Variables/Attributes

A class is a programmer-defined blueprint from which objects are created. An object is an instance of a class.

An instance variable is a variable defined in a class that represents an attribute of an object.

public class Painter {
  private int xLocation;
  private int yLocation;
  private String direction;
  private int remainingPaint;
}

Those four variables are what a Painter knows about itself. The methods (move(), turnLeft(), paint()) are what it can do.

Every object gets its own copy

When an object is instantiated, it gets its own copy of the instance variables. Two Painter objects made from the same class hold completely separate values:

rosieamy
xLocation23
yLocation40
direction"east""south"
remainingPaint510

Changing rosie's paint level does nothing to amy.

If a constructor doesn't directly assign a value to an instance variable, that variable still gets a default value based on its data type — 0 for an int, 0.0 for a double, false for a boolean, and null for a String. An instance variable is never simply empty.

Declaring an instance variable

private int xLocation;

Read it in three parts:

PartWhat it does
privateThe access modifier — sets visibility. private means it can't be accessed from outside the class.
intThe data type — what kind of value it holds.
xLocationThe name of the instance variable.

An access modifier sets the visibility of classes, variables, constructors, and methods:

  • public — visible to all classes in a program
  • private — visible only inside the class

Why instance variables should be private

Encapsulation is an object-oriented programming concept where the instance variables of a class are hidden from other classes and can be accessed only through the methods of the class.

Marking every instance variable private is how you accomplish it.

Make instance variables private. Make methods that other classes need public.

The point isn't secrecy — it's control. If remainingPaint were public, any code anywhere could set it to -500 and nothing would stop it. Keeping it private means every change has to go through a method you wrote, so you decide what a legal change looks like.

That also means you can change how the class works internally without breaking every other class that uses it, because they were never touching your variables directly in the first place.

2.2Constructors

A constructor is a block of code that has the same name as the class and tells the computer how to create a new object.

Painter alice = new Painter();

That line calls the constructor in the Painter class. When a constructor is called, Java looks for it in the class and runs the code inside it before moving on to the next statement.

A constructor sets the state of the object — the attributes represented by its instance variables.

No-argument constructors

A no-argument constructor is a constructor with no parameters. The empty parentheses after the class name are what make it one.

public class Painter {
  public Painter() {
    xLocation = 0;
    yLocation = 0;
    direction = "East";
    remainingPaint = 0;
  }
}

Building it, piece by piece:

  1. public — so it can be called from outside the class
  2. Painter — a constructor must have the same name as the class
  3. () — empty, since there are no parameters
  4. The body assigns starting values to the instance variables

That first line — public Painter() — is the constructor signature: the public keyword, the constructor name, and any parameters.

Default values

"Default value" gets used two ways here, and they are worth keeping apart.

The values Java supplies. Any instance variable you don't assign gets a default based on its data type. This happens for every object, whether or not you wrote a constructor:

Data typeDefault value
int0
double0.0
booleanfalse
Stringnull

The values you choose. A no-argument constructor often assigns its own starting values — predefined values used when the user doesn't provide one:

public Painter() {
  direction = "East";   // your choice, not Java's
  remainingPaint = 0;
}

A separate rule covers the constructor itself: if a class contains no constructors at all, Java automatically supplies an empty no-argument one, so new Painter() still compiles. That object's instance variables simply keep the type defaults from the table above.

To clarify — As soon as you write any constructor, Java stops supplying the automatic no-argument constructor.

Parameterized constructors

A parameterized constructor takes a specific number of arguments, passed in to assign values to the object's instance variables.

public Painter(int x, int y, String dir, int paint) {
  xLocation = x;
  yLocation = y;
  direction = dir;
  remainingPaint = paint;
}

The variables in the header (int x, int y, …) are the parameters — the type and name of the values expected when the constructor is called. Inside the constructor they behave as local variables.

Calling it:

Painter katie = new Painter(2, 3, "North", 4);

The values you pass in — 2, 3, "North", 4 — are the arguments. Their values are copied into the parameters, which is called call by value.

Overloading

Overloading is defining two or more constructors with the same name but different signatures.

public class Painter {
  private int xLocation;
  private int yLocation;
  private String direction;
  private int remainingPaint;

  public Painter() {
    xLocation = 0;
    yLocation = 0;
    direction = "East";
    remainingPaint = 0;
  }

  public Painter(int x, int y, String dir, int paint) {
    xLocation = x;
    yLocation = y;
    direction = dir;
    remainingPaint = paint;
  }
}

Now both of these work, and Java picks the right one by matching the arguments you pass:

Painter a = new Painter();
Painter b = new Painter(2, 3, "North", 4);

Avoiding errors when overloading

Java tells overloaded constructors apart by their signature — the number, types, and order of the parameters. The names of the parameters don't matter.

That's the rule behind almost every overloading error:

public Painter(int x, int y) { ... }
public Painter(int startX, int startY) { ... }   // ERROR

Both take two ints. Renaming the parameters changes nothing — the signatures are identical, so Java can't tell which one you meant and the class won't compile.

These, on the other hand, are all fine — each has a different parameter list:

public Painter() { ... }
public Painter(int x, int y) { ... }
public Painter(int x, int y, String dir) { ... }

Check the parameter list, not the parameter names. If two constructors take the same types in the same order, they conflict no matter what you called the parameters.

The other common error is calling a constructor that doesn't exist. If a class only defines Painter(int, int, String, int), then new Painter() fails — Remember, writing any constructor means Java stops supplying the automatic no-argument constructor.

2.3Methods

A method is a named block of code you can call. Unit 2 is about the methods that make a well-built class: the ones that let other classes work with your object without touching its private instance variables directly.

Void methods

A void method performs an action but does not give a value back. void is the return type, and it means "returns nothing."

public void move() {
  xLocation = xLocation + 1;
}

You call it as a statement on its own line:

alice.move();

How NOT to call a void method

This is where most mistakes happen. A void method produces no value, so it cannot be used anywhere a value is expected.

System.out.println(alice.move());   // ERROR — nothing to print
int x = alice.move();               // ERROR — nothing to assign
String s = "Moved: " + alice.move(); // ERROR — nothing to concatenate

All three fail for the same reason: there is no value coming back.

A void method call is a complete statement. If you find yourself putting one inside a print, an assignment, or a bigger expression, it belongs on its own line instead.

alice.move();                    // do the action
System.out.println(alice.getX()); // then ask for a value

Methods that return a value

A method that returns a value hands something back to the caller. The return type in the header says what kind, and return exits the method and hands the value back to wherever it was called.

public int getX() {
  return xLocation;
}

Because it produces a value, you have to use that value:

int x = alice.getX();               // assign it
System.out.println(alice.getX());   // print it
if (alice.getX() > 5) { ... }       // test it

How calling a method that returns a value can go wrong

The errors are the mirror image of the void ones — the value must fit the context:

int x = alice.getDirection();   // ERROR — returns a String, not an int
if (alice.getX()) { ... }       // ERROR — an int is not a boolean condition
alice.getX();                   // legal, but pointless — value thrown away

That last one compiles and runs. It just wastes the answer, which makes it a favorite on tests.

Accessor methods

An accessor method gives back the value currently assigned to an instance variable. It's how other classes read private data.

public int getX() {
  return xLocation;
}
  • public so it can be reached from outside the class
  • The return type matches the instance variable's type
  • Named get followed by the variable name, by convention
  • return provides the value back to the caller

Accessors always return a value — the whole point is handing something back. Java gives you a copy of the value, which is called return by value.

Mutator methods

A mutator method changes the value assigned to an instance variable.

public void setAge(int age) {
  this.age = age;
}
  • public so it can be reached from outside the class
  • Return type is void — it changes something rather than reporting something
  • Named set followed by the variable name, by convention
  • Takes a parameter matching the instance variable's type
  • Assigns the parameter's value to the instance variable

Calling it:

lisa.setAge(17);

Accessors and mutators together are what make encapsulation practical. The variables stay private, and these methods are the controlled doorway in and out.

The toString method

When you print an object directly, Java calls that object's toString method:

Pizza cheesePizza = new Pizza("cheese");
System.out.print(cheesePizza);
Pizza@3c717ef2

That's the class name, an @, and the object's hash code — the value Java associates with each object. Technically correct, and useless to a human.

Writing your own toString replaces that output:

public String toString() {
  return topping + " pizza";
}
cheese pizza

The header has to match exactly: public, returns String, named toString, no parameters. In the body, return a String describing the object.

Java calls it automatically in two situations:

  • when an object reference is passed to a print statement
  • when an object reference is concatenated with a String
System.out.println(cheesePizza);              // both call toString()
System.out.println("Order: " + cheesePizza);

Escape sequences

An escape sequence starts with a \ and tells Java how to display something inside a String:

SequenceMeaning
\nnew line
\"double quote
\\backslash
System.out.print("\"Best cookie shop!\" - Anonymous");
"Best cookie shop!" - Anonymous
System.out.println("C\nA\nK\nE");
C
A
K
E
System.out.println("Laugh \\S'more\\ worry less.");
Laugh \S'more\ worry less.

Calling a method from its signature

A method signature is the method's name plus the ordered list of its parameter types. To call a method correctly, read its signature and match it exactly:

public void paint(String color)

That signature tells you everything you need:

  • the name is paint
  • it needs one argument
  • that argument must be a String
  • it returns nothing, so the call stands on its own line
alice.paint("purple");   // correct
alice.paint();           // ERROR — missing the argument
alice.paint(7);          // ERROR — wrong type
int c = alice.paint("purple");  // ERROR — void returns nothing

Overloading applies to methods just as it does to constructors: same name, different signatures. Java picks which one to run by matching the arguments you actually pass.

2.4Local Variables vs. Instance Variables

A variable is a container that stores a value in memory. A data type is the format of the data that can be stored in it.

Data typeHoldsExamples
intwhole numbers7, 6784
doubledecimal numbers2.5, 92.81
booleanone of two valuestrue, false
Stringa sequence of characters"purple"

Java programs use variables everywhere — to hold a running total, a user's answer, a loop counter. This unit focuses on two kinds, told apart by where they are declared:

  • A local variable is declared inside a block — a method, a constructor, a loop, or an if statement.
  • An instance variable is declared at the top of a class, outside every method, to represent an attribute of an object.

Everything below applies to both kinds unless it says otherwise.

Declaring and initializing a local variable

public static void main(String[] args) {
  int score = 125;
}

score is a local variable — it is declared inside a block, the body of main.

That one line does three things:

  • Declaration — giving a name and data type to a variable (int score)
  • Assignment — using the assignment operator = to set or change the value
  • Initialization — giving a starting value using =

The 125 is a literal: a source code representation of a value.

You can also split declaration and initialization apart, then reassign as often as you like:

int score;         // declare
score = 125;       // initialize
score = 200;       // reassign — the old value is gone

A variable holds exactly one value at a time. Reassigning replaces what was there.

A local variable has no default value. Declaring int score; and then trying to use score before assigning it is a compile error.

Declaring and initializing an instance variable

Review from 2.1. Instance variables were introduced there as an object's attributes. This is the same idea, seen from the variable side.

An instance variable is declared at the top of the class, outside every method, and is normally marked private:

public class Painter {
  private String color;          // declared here...
  private int remainingPaint;

  public Painter() {
    color = "none";              // ...initialized here
    remainingPaint = 3;
  }
}

The same three words apply — declaration, assignment, initialization — but they usually happen in two different places: the variable is declared at the top of the class, and given its starting value in the constructor.

Two things follow from that:

  • Every object gets its own copy. Two Painter objects each have their own color and their own remainingPaint.
  • Instance variables have default values. One that is never assigned still holds something: 0 for int, 0.0 for double, false for boolean, and null for any reference type. Local variables have no such default.

Storing primitive and reference data

Both kinds of variable can store either kind of data.

A primitive type is a basic data type Java predefines — int, double, and boolean in this course. The variable holds the value itself.

A reference type holds a pointer to the memory location of an object. String and any class you write are reference types.

int age = 16;                    // holds 16
Painter alice = new Painter();   // holds a reference to a Painter object

Put the two ideas together and every variable falls into one of four boxes:

PrimitiveReference
Local (inside a block)int bonus = 5;String answer = "yes";
Instance (top of a class)private int remainingPaint;private String color;

The two questions are independent. Where a variable is declared makes it local or instance; what it holds makes it primitive or reference. So answer above is a local variable and a reference variable at the same time, with no contradiction — one label describes its scope, the other describes its data.

Variable scope

Scope refers to where a variable can be used. A variable exists only inside the region of code it was declared in — outside that region, the name means nothing.

Scope is decided by where you declare the variable, and the dividing line is the block. A block is a section of code inside a pair of braces { } — the body of a class, a method, a loop, or an if statement.

Local variables

A local variable can only be used inside the block that declares it. Once the block ends, the variable is gone:

public class PainterTester {
  public static void main(String[] args) {
    int totalCoats = 3;

    if (totalCoats > 1) {
      int bonus = 5;                    // local to the if block
      totalCoats = totalCoats + bonus;  // fine — still inside the block
    }

    System.out.println(bonus);          // ERROR — 'bonus' does not exist out here
  }
}

bonus was declared inside the if block, so it stops existing at that block's closing brace. totalCoats is still fine on that last line, because its block — the body of main — hasn't ended yet.

The rule is the same every time: a variable exists from the line that declares it until the closing brace of the block that declares it. Find the braces and you have the answer.

Instance variables

An instance variable reaches much further. Here is the same Painter class, now with its methods:

public class Painter {
  private String color;                   // usable in every method below
  private int remainingPaint;

  public Painter() {
    color = "none";                       // usable in the constructor
    remainingPaint = 3;
  }

  public boolean hasPaint() {
    return remainingPaint > 0;            // usable in this method
  }

  public void paint(String newColor) {
    color = newColor;                     // and in this one
    remainingPaint = remainingPaint - 1;
  }
}

color and remainingPaint are declared outside every method, so the constructor, hasPaint, and paint can all read and change them. And unlike a local variable, an instance variable lasts as long as the object does — it survives from one method call to the next, which is how an object remembers anything.

Where a variable can be used

DeclaredUsableGone when
In the main methodFrom its declaration to the end of main, including any loops and if blocks insidemain finishes
As an instance variableEvery constructor and method in that class, no matter the orderThe object is no longer used
Inside a method or constructorFrom its declaration to the end of that method or constructorThe method or constructor finishes
Inside a loop or if blockFrom its declaration to the end of that blockThe block ends
As a parameterThe whole body of that method or constructorThe method or constructor finishes

Every row except the instance variable one depends on order: the variable has to be declared before the line that uses it. Instance variables are the exception — a method can use an instance variable declared below it in the class, because the object's variables all exist before any method runs.

Parameters are local variables

A parameter is a local variable. It is declared in the method or constructor header, and it exists only while that method runs.

That single fact causes one of the most common bugs in Unit 2:

public class Pet {
  private String name;
  private int age;

  public Pet(String name, int age) {
    name = name;     // does nothing useful
    age = age;       // does nothing useful
  }
}

This compiles and looks right. It's broken.

When a local variable/parameter and an instance variable share a name, the parameter wins. So name = name assigns the parameter to itself. The instance variables are never touched — they keep their default values, null and 0.

Scope also explains this error:

public Pet(String newName, int newAge) {
  name = newName;
  age = newAge;
}

public void printPetInfo() {
  System.out.println("Name: " + newName);   // ERROR
}

newName was a parameter of the constructor, so it exists only inside the constructor. By the time printPetInfo runs, it's gone. The instance variable name is what that method should use.

The this keyword

The this keyword refers to the current object. Putting this. in front of a name forces Java to use the instance variable rather than the local variable/parameter:

public Pet(String name, int age) {
  this.name = name;   // instance variable = parameter
  this.age = age;
}

Read this.name = name; as "this object's name gets the value of the parameter name."

There are two ways to avoid the naming bug, and both are correct:

public Pet(String newName, int newAge) {   // different names
  name = newName;
  age = newAge;
}

public Pet(String name, int age) {         // same names + this
  this.name = name;
  this.age = age;
}

The this version is more common in professional code, because the parameter names match the attributes they set and you don't have to invent a second set of names.

2.5Evaluating Expressions

An expression is a combination of data and operators that evaluates to a single value. An operand is the data being operated on.

ExpressionValueType
4 + 610int
10 * 2.020.0double
"Hi " + "there""Hi there"String

The arithmetic operators are +, -, *, /, and %.

Value and type

When you evaluate an expression, pay attention to both the value and the type. The type of the result depends on the types of the operands.

21 - 3 * 2.0

3 * 2.0 mixes an int and a double. Java promotes the int, giving 6.0, and 21 - 6.0 gives 15.0 — a double, not an int.

The rule: if any operand in an arithmetic operation is a double, the result is a double.

int a = 5;
double b = 2.0;

a + b      // 7.0   double
a * 2      // 10    int
a / b      // 2.5   double

Integer division truncates

Dividing two int values gives an int. The decimal portion is truncated — cut off, not rounded.

int x = 8;
int y = 3;
int z = x / y;    // 2, not 2.67 and not 3

Make one side a double and the division behaves the way you'd expect:

double z = 8 / 3;      // 2.0  — division happened FIRST, in ints
double z = 8.0 / 3;    // 2.666...  — one operand is a double

That first line catches people constantly. The 8 / 3 is evaluated as integer division before anything is assigned, so widening it afterward can't recover the lost decimal.

double a = 4.0;
int b = 2;
double result = a * b;    // 8.0 — a double, so a double result

Evaluating in order

Java evaluates the expression on the right, then assigns the result to the variable on the left.

int x = 0;
x = 15 - 5 * 2;    // 5 * 2 = 10, then 15 - 10 = 5

When several expressions run in a row, each one uses the updated values from the lines before it:

int x = 3;
int y = 7;
x = x + y;    // x = 3 + 7  →  10
y = x + y;    // y = 10 + 7 →  17   (uses the NEW x)

Java follows this order of operations:

PrecedenceOperators
highest!
* / %
+ -
< > <= >=
== !=
lowest= += -= *= /= %=

Dividing by zero

The two cases behave completely differently, and both are testable.

Dividing an int by 0 throws an ArithmeticException — the program crashes.

int a = 7;
int b = 0;
int c = a / b;
Exception message: java.lang.ArithmeticException: / by zero

Dividing a double by 0 gives Infinity — no crash, and the program keeps running.

double a = 7.0;
double b = 0.0;
double c = a / b;
Infinity

The difference comes from how Java handles decimal numbers, which follows the IEEE 754 standard.

int divided by 0 → ArithmeticException. double divided by 0 → Infinity.

Check the types before you decide which one happens.

The mod operator

The mod operator % gives the remainder of a division problem.

ExpressionResult
15 % 21
12 % 102
12 % 60
9 % 21

A result of 0 means the division came out even — which is how you test whether a number is even, or a multiple of anything.

Compound assignment operators

A compound assignment operator is shortcut syntax that performs an operation on both operands and assigns the result to the variable on the left.

+= -= *= /= %=

int a = 4;
a += 6;        // 10 — same as a = a + 6

The right side is evaluated completely first:

int a = 7;
int b = 2;
b *= a + 1;    // b = 2 * (7 + 1) = 16, not 2 * 7 + 1

Strings and the + operator

Concatenation joins two Strings with +:

System.out.print("Hello " + "World");
Hello World

+ is the only arithmetic operator that works with Strings:

System.out.print("Hello " - "World");
error: bad operand types for binary operator '-'
  first type:  java.lang.String
  second type: java.lang.String

Boolean expressions

A relational operator compares values or expressions:

OperatorMeaning
==is equal to
!=is not equal to
>greater than
<less than
>=greater than or equal to
<=less than or equal to

Using them produces a Boolean expression — a logical statement that evaluates to true or false:

ExpressionValue
3 + 4 == 7true
32 / 4 + 5 >= 20false
5 + (11 % 2) != 3 * 2 - 1true

Work these the same way as any other expression: evaluate the arithmetic on both sides first, then compare.