Lab 18: Student Class
Fields, constructors, encapsulation, toString, equals, and testing
After this lab, you will be able to:
- Define a class with private fields, a constructor, and getter methods
- Override
toStringto produce a formatted string representation - Override
equalsandhashCodefor value-based equality - Write JUnit tests that verify constructor behavior, equality, and edge cases
What You’re Building
You will design a Student class from scratch: private fields for name, ID, and GPA; a constructor that initializes all three; getters (no setters — immutable design); a toString override; and an equals/hashCode pair that compares students by ID. Then you will write JUnit tests that verify your class behaves correctly. This is a full-week lab because it covers the core OOP skill set you will use for the rest of the quarter.
Timeline: This lab is assigned Monday and due Friday. Start early — the testing checkpoint takes time to think through.
Concepts and Misconceptions
| Concept | Common Mistake | What the Test Catches |
|---|---|---|
| Private fields | Declaring fields as public, bypassing encapsulation |
Test accesses fields via getters only; public fields are flagged by style check |
| Constructor | Assigning parameter to itself (name = name; instead of this.name = name;) |
Getter returns null after construction |
toString |
Returning null or printing inside toString instead of returning a String |
Test calls toString() and compares the returned value |
equals |
Comparing with == instead of overriding equals, or forgetting the Object parameter type |
Two students with the same ID are not considered equal |
hashCode |
Overriding equals without overriding hashCode |
Autograder checks that equal objects produce equal hash codes |
| JUnit | Writing tests that always pass (e.g., assertTrue(true)) instead of testing real behavior |
Test quality check verifies assertions reference your Student methods |
Checkpoints
Checkpoint 1: Private Fields, Constructor, Getters, toString
What to do: Create a Student class with three private fields: String name, int id, and double gpa. Write a constructor that takes all three as parameters and assigns them using this. Write getter methods for each field. Override toString to return a formatted string like "Student{name='Ada', id=12345, gpa=3.95}".
What the test checks: Constructing a Student and calling each getter returns the correct value. Calling toString returns the expected format.
Debugging tip: If getters return null or 0, you probably wrote name = name; in the constructor. The parameter shadows the field. Use this.name = name; to disambiguate. If toString fails, check the exact format — spaces, quotes, commas, and braces all matter.
Checkpoint 2: equals and hashCode
What to do: Override equals(Object obj) so two Student objects are equal if they have the same id. Follow the standard pattern: check for null, check getClass(), cast, and compare the id field. Override hashCode to return Integer.hashCode(id) so that equal objects always produce the same hash code.
What the test checks: Two students with the same ID are .equals(), two with different IDs are not. A student is not equal to null or a non-Student object. Equal students have equal hash codes.
Debugging tip: If equals always returns false, check that your parameter type is Object, not Student. Writing equals(Student other) is overloading, not overriding — the test calls equals(Object). If the null check fails, make sure you handle null before casting.
Checkpoint 3: Write 3 JUnit Tests
What to do: In the test file, write at least three meaningful JUnit tests. Test (a) that the constructor and getters work correctly, (b) that two students with the same ID are equal and two with different IDs are not, and (c) an edge case such as toString format or equality with null.
What the test checks: Your test file compiles, all your tests pass, and each test contains assertions that call Student methods.
Debugging tip: If your tests compile but do not run, check that your test methods are annotated with @Test and that you imported org.junit.jupiter.api.Test and static org.junit.jupiter.api.Assertions.*. If a test fails, fix your Student class — the goal is to write tests that catch real bugs, so a failing test means your implementation has an issue.
How to Debug
-
Test one method at a time. Write the constructor and getters first, then run the checkpoint 1 tests. Do not move to
equalsuntil the basics work. Layering code on a broken foundation wastes time. -
Use
System.out.printlnin main. Before running JUnit, create a fewStudentobjects in amainmethod and print them. VerifytoStringoutput visually. Callequalsand print the result. This quick check catches obvious issues before the test framework is involved. -
Read the
equalscontract.equalsmust be reflexive (a.equals(a)), symmetric (a.equals(b) == b.equals(a)), and handlenull(returnfalse). If any of these fail, check your implementation against the standard pattern shown in lecture.
Scoring
| Component | Points | Criteria |
|---|---|---|
| Checkpoints | 3 | 1 pt each. Binary: the checkpoint test passes or it does not. |
| Autograder | 5 | Correctness across all test cases. Partial credit by proportion of tests passed. |
| Timeliness | 2 | Full credit if submitted by the due date. 0 if late. |
| Total | 10 |