Code Refactoring in Java: Tips, Best Practices, Techniques

Nir Shafrir
Javarevisited
Published in
13 min readSep 9, 2023

Throughout my journey as a Java developer, I have learned to acknowledge the fact that most of the code I write from the initial stages of developing a product is destined to undergo massive changes before it is done. As developers, our job does not just entail crafting new lines of code but also routinely revisiting the code we’ve previously written or inherited, not just for minor tweaks but for substantial changes and enhancements. In some cases, the whole codebase needs to be refactored. This fundamental practice in our craft is formally referred to as code refactoring.

However, code refactoring in Java isn’t always the optimal solution and can have unintended negative consequences. Many developers can share their refactoring stories and experiences of failure. “Works don’t touch” is the famous adage that comes to mind. Sometimes, there is no other choice but code refactoring, but without the right safety net, developers will find themselves dreading the need to retouch the code.

This is why careful planning is crucial to ensure its success. When undertaking a code refactoring journey, it’s essential to acknowledge that the process may be time-consuming and filled with challenges. Also, it’s important to recognize that when working in a team, each member has their perspectives and considerations that can impact the process when incorporating new features into the freshly refactored code. This requires thorough planning, documentation, and collective decision-making on the new architecture.

By following specific steps, techniques, and the correct tooling, you can sidestep potential refactoring pitfalls, preventing the emergence of “horror stories” during refactoring such as the one shared in this article. So let’s start by first exploring what Java code refactoring is:

What Is Java Code Refactoring?

Java Code refactoring is the practice of restructuring and improving existing code by making small incremental changes that do not impact the external behavior of the code. Code refactoring involves changing the internal structure of the code to improve readability, performance, maintainability, and efficiency without breaking the code’s functionality.

Martin Fowler is a software developer and author who has written extensively about design and refactoring code. In his book titled “Refactoring Improving the Design of Existing CodeHe describes refactoring as follows

“Refactoring is a controlled technique for improving the design of an existing code base. Its essence is applying a series of small behavior-preserving transformations, each of which “too small to be worth doing”. However the cumulative effect of each of these transformations is quite significant” Martin Fowler

Some of the common changes you can make when refactoring your Java code include:

  • Renaming variables, classes, functions, and other elements in your code to more readable and descriptive names.
  • Inline replacement of methods or functions that have become redundant with their content at call sites.
  • You can also extract blocks of code from functions and move them into their own separate functions to improve modularity and readability.
  • Reducing redundancy by removing pieces of code that perform the same function and replacing them with one.
  • Refactoring can also involve extracting and splitting classes or modules that handle too many responsibilities into smaller, more focused components.
  • You can also combine classes or modules with similar functionality.
  • Refactoring changes can also include making changes to improve code performance.

Java Code Refactoring Techniques

Renaming Variables and Methods

Choosing meaningful variable and method names is key to making your code more readable. Code readability is arguably one of the most important aspects of any good codebase. Readable code clearly communicates the intent to the reader, while less readable code increases the likelihood of bugs during refactoring. Using meaningful variable and method names reduces the need for comments and makes collaboration easy.

// Before Refactoring
int d = 30; // days
int h = 24; // hours in a day

// After Refactoring
int daysInMonth = 30;
int hoursInDay = 24;

Extract Method

The extract method is one of the most common Java code refactoring techniques. The extract method is useful when you have a long, complex method that performs multiple responsibilities. Extracting tasks to separate methods makes the original method less complex and readable. This method also allows you to refactor your code to be more reusable and easily maintainable. Suppose you have a simple class that processes orders and calculates subtotal cost, tax rate, and total cost.

public class OrderProcessor {
private List<Item> items;
private double taxRate;

public double processOrder() {
double subtotal = 0;
for (Item item : items) {
subtotal += item.getPrice();
}

double totalTax = subtotal * taxRate;
double totalCost = subtotal + totalTax;

return totalCost;
}
}

You can refactor the code above such that the code related to calculating the subtotal, tax, and total cost has been extracted into separate methods: calculateTax, calculateSubtotal, and calculateTotalCost as shown below. This makes the class more readable, modular, and reusable.

public class OrderProcessor {
private List<Item> items;
private double taxRate;

public double processOrder() {
double subtotal = calculateSubtotal();
double totalTax = calculateTax(subtotal);
return calculateTotalCost(subtotal, totalTax);
}

private double calculateSubtotal() {
double subtotal = 0;
for (Item item : items) {
subtotal += item.getPrice();
}
return subtotal;
}

private double calculateTax(double subtotal) {
return subtotal * taxRate;
}

private double calculateTotalCost(double subtotal, double totalTax) {
return subtotal + totalTax;
}
}

Remove Magic Numbers and Strings.

Magic numbers and strings are values hardcoded into your code. Using such values in your code makes the code less maintainable and increases inconsistencies and errors due to mistypes. Instead of using hardcoded values, refactor your Java code to use constants with descriptive names.

// Before Refactoring
if (status == 1) {
// ... code for active status ...
}

// After Refactoring
public static final int ACTIVE_STATUS = 1;

if (status == ACTIVE_STATUS) {
// ... code for active status ...
}

Code De-duplication.

Duplicate codes are similar code fragments that appear in multiple places in the codebase. Duplicate code is dreaded in software development due to its negative effects on quality, maintainability, and efficiency. It also increases bug intensity, increases inconsistencies, and bloats the size of your codebase.

Before Refactoring

public class NumberProcessor {

public int calculateTotal(int[] numbers) {
int total = 0;
for (int i = 0; i < numbers.length; i++) {
total += numbers[i];
}
return total;
}

public double calculateAverage(int[] numbers) {
int total = 0;
for (int i = 0; i < numbers.length; i++) {
total += numbers[i];
}
double average = (double) total / numbers.length;
return average;
}
}



After Refactoring

public class NumberProcessor {

public int calculateSum(int[] numbers) {
int total = 0;
for (int i = 0; i < numbers.length; i++) {
total += numbers[i];
}
return total;
}

public int calculateTotal(int[] numbers) {
return calculateSum(numbers);
}

public double calculateAverage(int[] numbers) {
int total = calculateSum(numbers);
double average = (double) total / numbers.length;
return average;
}
}

In the refactored example, we have extracted the duplicate logic for summing up the number into the calculateSum method. Both calculateTotal and calculateAverage methods now utilize the calculateSum method to calculate the sum.

Simplify Methods.

Over time, your code gets more dated and maintained by different developers it is easy to end up with complex and convoluted methods. This Java code refactoring technique lets you keep your methods easy to understand, maintain, and extend.

The simplification method may involve identifying overly complex methods that contain nested logic and too many responsibilities before applying the steps below to simplify the method.

  • Apply the Single Responsiblity Principle (SPR) to the method.
  • Extract sub-methods to standalone methods.
  • Eliminate redundant and unused code.
  • Limit the level of nesting within the method.

Here is a simple Java code refactoring example to illustrate the method simplification technique.

Before simplification

public class ShoppingCart {
private List<Item> items;

public double calculateTotalCost() {
double total = 0;
for (Item item : items) {
if (item.isDiscounted()) {
total += item.getPrice() * 0.8;
} else {
total += item.getPrice();
}
}
if (total > 100) {
total -= 10;
}
return total;
}
}

We can simplify the example above by extracting the calculateItemPrice logic to calculateItemPrice and applyDiscount and simplify the conditionals by using ternary operators instead.

public class ShoppingCart {
private List<Item> items;

public double calculateTotalCost() {
double total = 0;
for (Item item : items) {
total += calculateItemPrice(item);
}
total -= applyDiscount(total);
return total;
}

private double calculateItemPrice(Item item) {
return item.isDiscounted() ? item.getPrice() * 0.8 : item.getPrice();
}

private double applyDiscount(double total) {
return total > 100 ? 10 : 0;
}
}

Red-green refactoring

The red-green refactoring method is also commonly called the Test Driven Development (TDD). This Java code refactoring technique emphasizes writing tests before writing the actual code that passes the tests already written. This is a repetitive cycle, and on each iteration, you should write new tests before implementing just enough code to pass those tests.

The red-green Java code refactoring technique involves the following three steps.

Red: At this step in the cycle, you have no actual code. You should start by writing failing tests(Red) because there is no implementation to satisfy the test cases.

Green: In this phase, you should write the minimal code necessary to pass the failing test cases. The goal is not to write perfect or optimized code at this stage but to make the test pass (turn the test from red to green).

Refactor: Once you are confident that the code you have implemented passes the tests, you can now focus on refactoring the code to improve performance, structure, and other aspects without breaking the functionality lest the test cases fail.

The cycle repeats itself once you are done with a specific test case. In the new cycle, you can move to the next test case, which involves writing a new test case and just enough code implementation to pass the new test case before refactoring the code to add improvements.

Fix Code Classes that Violate the Single Responsibility Principle.

You’ve probably heard about the SOLID principles in object-oriented programming. SOLID is an acronym for the five principles, and one of these is the Single Responsibility principle, represented by the first letter. According to this principle, every class should have one and only one reason for change. In other words, a class should have only one responsibility.

The single responsibility principle is one of the most important aspects of keeping your code maintainable, readable, flexible, and modular. Here is an example of an OrderProcessing class that violates the single responsibility principle since it is also responsible for logging information to a file.

Before refactoring

public class OrderProcessor {
public void processOrder(Order order) {
// Processing logic

// Logging
Logger logger = new Logger();
logger.log("Order processed: " + order.getId());
}
}

According to the single responsibility principle we can refactor this class such that we have and OrderProcessor class responsible for processing and order and OrderLogger class responsible for logging.

After refactoring

public class OrderProcessor {
public void processOrder(Order order) {
// Processing logic

// Logging
OrderLogger orderLogger = new OrderLogger();
orderLogger.logOrderProcessed(order);
}
}

public class OrderLogger {
public void logOrderProcessed(Order order) {
Logger logger = new Logger();
logger.log("Order processed: " + order.getId());
}
}

13 Java code refactoring best practices.

Java code refactoring is an important practice that holds the potential to elevate the quality of your code, among other benefits we have highlighted above. However, you should also take care, especially when refactoring a large codebase or working with an unfamiliar one, lest you inadvertently alter software functionality or introduce unforeseen flaws.

Here are some tips and best practices you can follow when refactoring Java code to avoid eny potential problems.

  1. Preserver functionality: The primary aim of Java code refactoring is improving quality. However, the external behaviour of the program i.e how it responds to inputs and outputs and other user interactions should remain unchanged. Remember that with a tool such as Digma, you never have to worry about breaking stuff. Digma lets you know which services use the code you’re working on, which might break if you make changes, and what you need to test.
  2. Understand the code well: Before setting out to refactor a certain piece of code, ensure that you understand the code you’re about to refactor. This includes its functionality, interactions, and dependencies. This comprehension will guide your decisions and help you avoid making changes that may impact the functionality of your code.
  3. Break down your Java Refactoring process into small steps: Refactoring a large piece of software can be overwhelming, especially if it is new to you. However, by breaking down the refactoring process into smaller manageable steps, you make your workload easier, reduce the risk of errors, and allow you to continuously and easily validate your changes.
  4. Use version controls to create a backup: Since refactoring involves making changes to your codebase, there is a chance that things may not go as planned. It is a good idea to back up your working software in a separate branch to avoid wasting a lot of time and resources when you cannot figure out which changes have broken your software. Version control systems like Git allow you to manage different software versions concurrently.
  5. Run Frequent Tests on your changes: The last thing you want when refactoring your code is inadvertently breaking the functionality of your program or introducing bugs that affect your software. Before embarking on any significant code changes, especially refactoring, having a suite of tests in place provides a safety net. These tests validate that your code behaves as expected and that existing functionality remains intact. You can also use the Digma filtering or sorting by “impact” metric in the asset section on the sidebar and identify areas that might need attention during the refactoring. This allows you to gain insights into which parts of the codebase might have the most significant impact and could potentially benefit the most from refactoring. With Digma, you can proactively prioritize your refactoring efforts instead of blindly refactoring different sections of your code.
  6. Leverage Refactoring Tools: With modern IDEs like Eclipse and IntelliJ, Java code refactoring does not have to be stressful. For instance, the IntelliJ IDEA includes a set of robust refactoring capabilities. Some features include safe delete, extract method/parameter/field/variable, inline refactoring, copy/move, and many more. These features make your work easier and reduce the chances of you introducing bugs during refactoring.
  7. Gain insights into the code changes. Gaining insights into your code changes can help you speed up the refactoring process by identifying any issues that may arise along the way. Tools like Digma can help you do that. Digma is a runtime linter that lets you quickly identify risky code, potential errors, and bottlenecks in complex codebases. It uses OpenTelemetry behind the scenes to collect data such as traces, logs, and metrics when you run your code locally. As you embark on refactoring your Java code, you can leverage the Digma insights to identify issues, anomalies, and patterns to facilitate informed and efficient Java code refactoring. Measuring and recording various metrics related to your code’s performance and behavior also gives you a before/after picture that serves as a baseline to understand the impact of your changes. By comparing the before and after states, you can identify areas that require further optimization, and assess whether the refactoring achieved the intended goals.
  8. Leverage unit tests: As a developer, you’d want to refactor your code with confidence that you’re not going to break your application or introduce bugs. A complete unit test suite allows you to detect regressions, ensure functionality is not broken, facilitate collaboration, and ensure long-term maintainability.
  9. Keep track of performance changes with Continuous Feedback: Java code refactoring is not just about improving the structure of your but also the performance. Keeping track of performance metrics takes the guesswork out of the equation, allowing you to validate that your refactoring efforts are yielding results. Besides quantifying your improvements, tracking performance improvements allows you to compare different Java refactoring strategies. Digma gives you continuous access to evidence-based feedback, so you can continuously optimize and improve your code. You don’t need to be an observability expert or wait until there’s a production-breaking issue. With valuable data such as code execution times, scaling limitations, and N+1 query issues, you can quickly fix performance issues in your code.
  10. Peer review your Java code refactoring changes: Once you make you’re done with your refactoring changes, it is always a good idea to have another developer with a fresh perspective look at the changes. An impartial peer review from your colleague can identify issues you might have overlooked and bring new insights and valuable feedback. A peer review is a good way to foster collaboration and knowledge sharing among team members if you’re working in a team.
  11. Document the refactoring changes: If you’re working with a team of other developers, it is obvious that your changes may impact their work. To promote collaboration and enhance transparency, it is imperative that you document reasons for changes that may not be immediately obvious. Documenting changes also improves knowledge sharing and onboarding.
  12. Regression testing: Once you’re done with refactoring your Java code, you need to ensure that the existing functionalities are preserved. Regression testing allows you to ensure that your newly introduced logic does not conflict with the existing code.
  13. Keep your Team in the Loop: If you’re working on a team with other developers, communicate your refactoring changes to your colleagues. This prevents conflicts and helps them adapt to any changes you make.
  14. Revert If Necessary: If you encounter unexpected issues during refactoring, don’t hesitate to revert to a working state. It’s better to take a step back than waste a lot of time and resources when you can’t figure out what went wrong in the Java code refactoring process.

Conclusion.

Refactoring is an essential practice that contributes to the long-term success of software projects. By incorporating refactoring using the techniques we have discussed above alongside diligent adherence to the best practices into your development cycle, you can turn any complex and tangled codebase into a readable, maintainable, and extensible software solution. However, keep in mind that Java code refactoring is not a one-off activity but an ongoing process that can be integrated into your development cycle.

FAQ

When should I refactor my Java code?

Code refactoring can be done throughout the software development lifecycle. It’s often a good idea to refactor when you’re adding new features, fixing bugs, or working on a piece of code that’s hard to understand. Regularly setting aside time for refactoring prevents technical debt from accumulating and helps maintain a high-quality codebase.

How do I decide which code to refactor?

You can start by identifying areas of the codebase that are difficult to understand, contain duplicated logic, or are prone to bugs. Look for long methods, complex conditionals, and instances where following design principles like the Single Responsibility Principle would improve code organization.

How can I ensure that refactoring doesn’t introduce bugs?

A strong suite of automated tests is essential to minimize the risk of introducing bugs to your code. Before refactoring, ensure that your code has good test coverage to help you catch any regressions and ensure the code’s functionality remains unaffected.

How to refactor classes in Java?

Begin by identifying the target class that requires improvement, understanding its behaviour and analyzing its dependencies. Break down large methods, move methods to suitable classes, and leverage inheritance and interfaces for cleaner structures. Rename, reorganize, and simplify conditionals to boost readability. Thoroughly test changes to ensure that functionality is not broken.

--

--

Nir Shafrir
Javarevisited

Mountain biker! cofounder of Digma.ai, An IDE plugin that analyzes code as it runs, providing actionable insights into errors, performance, and usage.