Refactoring Basics in Java
Tidying a cupboard doesn't add any new clothes. You fold, sort and move things so that tomorrow morning you find your uniform in five seconds instead of five minutes. Refactoring is tidying for code: the program does exactly the same thing afterwards, but it is easier to read and easier to change. The trick is to tidy without losing anything, and that takes a method. In this lesson, you will learn what refactoring is, the safety net you need before you start, the small-steps loop, and the refactorings you will use most, with an attendance report cleaned up one step at a time.
What Is Refactoring?
Refactoring in Java is the process of changing the internal structure of code without changing its external behavior. The inputs and outputs stay identical; names, methods and classes are rearranged so the code is clearer.
- It isn't adding features. A refactoring step never changes what the program prints, returns or saves.
- It isn't fixing bugs. If you find a bug while refactoring, note it, finish the refactoring, then fix the bug as a separate change.
- It is done in small steps. Each step is so small that if something breaks, you know exactly what caused it.
The Safety Net: Tests First
You can only be sure behavior didn't change if you check it. Before refactoring, make sure tests cover the code. If there are none, write a quick characterization test: a test that records what the code does today, right or wrong, so you notice if it changes.
// A tiny characterization test: run it before and after every step
static void check(String actual, String expected) {
if (!actual.equals(expected)) {
throw new AssertionError(
"Expected:\n" + expected + "\nGot:\n" + actual);
}
}In real projects, these are JUnit tests (see "Unit Testing with JUnit in Java").
The Refactoring Loop
- Tests pass. Never start refactoring on red tests.
- Make one small change. One rename, one extracted method.
- Run the tests.
- Pass: commit. Fail: undo (it was small, so undoing is cheap) and try a smaller step.
Core Refactorings
| Refactoring | What it does | IntelliJ IDEA shortcut (Windows/Linux) |
|---|---|---|
| Rename | Gives a variable, method or class a clearer name everywhere it is used | Shift+F6 |
| Extract Variable | Names a complicated expression | Ctrl+Alt+V |
| Extract Method | Moves a block into its own named method | Ctrl+Alt+M |
| Extract Constant | Turns a magic number or string into a named constant | Ctrl+Alt+C |
| Inline | The opposite: removes a variable or method that adds nothing | Ctrl+Alt+N |
| Introduce Parameter Object | Groups parameters that travel together into a record | Refactor menu |
| Move Method | Moves a method to the class whose data it uses | F6 |
| Replace Conditional with Polymorphism | Turns a type switch into overridden methods or a sealed type | By hand |
On macOS, use Cmd in place of Ctrl. Eclipse and VS Code have the same refactorings under a Refactor menu.
A Worked Example: The Attendance Report
Here is the starting code. It works, and the test at the bottom proves what it prints.
public class Attendance {
static String rep(String n, int p, int t) {
String s = n + ": " + p + "/" + t;
if (p * 100 / t < 75) {
s = s + " (" + p * 100 / t + "%) SHORTAGE";
} else {
s = s + " (" + p * 100 / t + "%) OK";
}
return s;
}
public static void main(String[] args) {
System.out.println(rep("Navika", 182, 200));
System.out.println(rep("Advitha", 140, 200));
}
}Navika: 182/200 (91%) OK
Advitha: 140/200 (70%) SHORTAGENow the steps, each one checked by running the program and comparing the output:
| Step | Refactoring | Change |
|---|---|---|
| 1 | Rename | rep → attendanceLine, n → name, p → present, t → workingDays |
| 2 | Extract Variable | present * 100 / workingDays (written three times) → int percent |
| 3 | Extract Constant | 75 → MIN_ATTENDANCE_PERCENT |
| 4 | Extract Method | the shortage rule → boolean isShort(int percent) |
| 5 | Simplify | the if-else that only differs in one word → a conditional expression |
| 6 | Introduce Parameter Object | name, present, workingDays → record AttendanceRecord |
Here is the result after all six steps.
public class Attendance {
private static final int MIN_ATTENDANCE_PERCENT = 75;
record AttendanceRecord(String name, int present, int workingDays) {
int percent() {
return present * 100 / workingDays;
}
}
static boolean isShort(int percent) {
return percent < MIN_ATTENDANCE_PERCENT;
}
static String attendanceLine(AttendanceRecord r) {
int percent = r.percent();
String status = isShort(percent) ? "SHORTAGE" : "OK";
return r.name() + ": " + r.present() + "/" + r.workingDays()
+ " (" + percent + "%) " + status;
}
public static void main(String[] args) {
System.out.println(attendanceLine(
new AttendanceRecord("Navika", 182, 200)));
System.out.println(attendanceLine(
new AttendanceRecord("Advitha", 140, 200)));
}
}Navika: 182/200 (91%) OK
Advitha: 140/200 (70%) SHORTAGEExplanation:
- The output is byte-for-byte the same, which is the definition of a successful refactoring.
- Rename made the code readable without changing a single calculation.
- Extract Variable removed the triple calculation. The record's
percent()method then gave it a home next to the data it uses (a small Move Method). - Extract Constant and Extract Method turned the school rule into a named, testable piece:
isShort(74)andisShort(75)can now be checked directly. - The record means the next field (say,
className) is added in one place, not to every method signature.
When to Refactor
- Before adding a feature, if the code makes the feature hard to add. "Make the change easy, then make the easy change."
- While fixing a bug, if understanding the code took too long. Refactor so the next person understands it faster, then fix the bug in a separate commit.
- During code review, when a reviewer says "I had to read this twice".
- Not the day before a release, and not on code without any tests unless you write the tests first.
Common Mistakes
Refactoring and changing behavior in the same step. If the output changes, you can't tell whether it was the refactoring or the new feature.
Big-bang rewrites. Rewriting a whole class at once and "testing it at the end" is not refactoring. Small steps, tested each time.
Refactoring without tests. Without a safety net, you are just editing and hoping.
Mixing refactoring commits with feature commits. Keep them separate so reviewers can check each one quickly.
Practice
- Easy: Add a third student, "Srihitha", with 150 present out of 200 days, to both versions of the program. Predict the output before running.
- Harder: The school changes the rule: 65 to 74 percent is "WARNING", below 65 is "SHORTAGE". First refactor
isShortinto a methodString statusFor(int percent)without changing the output (run it). Then, as a separate step, add the new rule.
Key Takeaways
Was this tutorial helpful?