Refactoring Basics in Java

Naveen AnamaneniUpdated · 7 min read

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.

AttendanceCheck.java
// 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

Small change, run tests, commit, repeat
  1. Tests pass. Never start refactoring on red tests.
  2. Make one small change. One rename, one extracted method.
  3. Run the tests.
  4. Pass: commit. Fail: undo (it was small, so undoing is cheap) and try a smaller step.

Core Refactorings

RefactoringWhat it doesIntelliJ IDEA shortcut (Windows/Linux)
RenameGives a variable, method or class a clearer name everywhere it is usedShift+F6
Extract VariableNames a complicated expressionCtrl+Alt+V
Extract MethodMoves a block into its own named methodCtrl+Alt+M
Extract ConstantTurns a magic number or string into a named constantCtrl+Alt+C
InlineThe opposite: removes a variable or method that adds nothingCtrl+Alt+N
Introduce Parameter ObjectGroups parameters that travel together into a recordRefactor menu
Move MethodMoves a method to the class whose data it usesF6
Replace Conditional with PolymorphismTurns a type switch into overridden methods or a sealed typeBy 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.

Attendance.java▶ Try it
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));
    }
}
Output
Navika: 182/200 (91%) OK
Advitha: 140/200 (70%) SHORTAGE

Now the steps, each one checked by running the program and comparing the output:

StepRefactoringChange
1Renamerep → attendanceLine, n → name, p → present, t → workingDays
2Extract Variablepresent * 100 / workingDays (written three times) → int percent
3Extract Constant75 → MIN_ATTENDANCE_PERCENT
4Extract Methodthe shortage rule → boolean isShort(int percent)
5Simplifythe if-else that only differs in one word → a conditional expression
6Introduce Parameter Objectname, present, workingDays → record AttendanceRecord

Here is the result after all six steps.

Attendance.java▶ Try it
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)));
    }
}
Output
Navika: 182/200 (91%) OK
Advitha: 140/200 (70%) SHORTAGE

Explanation:

  1. The output is byte-for-byte the same, which is the definition of a successful refactoring.
  2. Rename made the code readable without changing a single calculation.
  3. 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).
  4. Extract Constant and Extract Method turned the school rule into a named, testable piece: isShort(74) and isShort(75) can now be checked directly.
  5. 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

  1. Easy: Add a third student, "Srihitha", with 150 present out of 200 days, to both versions of the program. Predict the output before running.
  2. Harder: The school changes the rule: 65 to 74 percent is "WARNING", below 65 is "SHORTAGE". First refactor isShort into a method String statusFor(int percent) without changing the output (run it). Then, as a separate step, add the new rule.

Quick check: Refactoring

4 questions
1What is refactoring?
2What do you need before you start refactoring?
3What should you do if the tests fail after a small refactoring step?
4Which refactoring turns a repeated expression into a named local variable?

Key Takeaways

Was this tutorial helpful?

Lesson 541 of 578