Introduction to refactoring
IntelliJ IDEA offers a lot of automatic refactoring capabilities, but as developers it is not enough to know how to perform them, we need to understand what these refactorings are, when we would want to apply them, and any possible downsides or things to consider before using them.
Refactoring, as defined by Martin Fowler, "...is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior". Hence, it is important before performing any refactoring in production code to have comprehensive test coverage to prove that you haven't inadvertently changed the behaviour.
The goal of this tutorial is to introduce those who may be new to the idea of refactoring, particularly automatic refactoring, to IntelliJ IDEA's capabilities and show when you might want to apply three of the basic types of refactoring: Renaming, Extracting and Deleting.
Renaming
Renaming may seem like a trivial refactoring, but renaming via a simple find-and-replace often means unrelated items with the same name are unintentionally changed. Using IntelliJ IDEA's rename refactorings minimises these errors.
Why Rename?
Renaming is one of the simplest things you can do to improve the readability of the code. When a class, method or variable name doesn't match what it appears to do, it can cause a lot of confusion. Some reasons why you might want to rename something:
The name is not descriptive enough.
The class/method/variable name doesn't match what it really is.
Something new has been introduced, requiring existing code to have more specific name.
Renaming as you code
Imagine you come across the following code as you're implementing some feature or fixing some bug.
Let's assume that we want to:
Rename
endpoint
, a field, to describe what sort of endpoint it is.Rename
init()
, a method onServer
, to something that describes that method more accurately.Rename
Server
, a class, to something more specific.
To rename the
endpoint
field, place your caret at the word endpoint and press Shift+F6. IntelliJ IDEA will pop up a list of suggestions, based on the class name and other aspects. In this case, the name of the parameter this field is used in is also suggested.Select one of these options or type your own. If the field has a getter, IntelliJ IDEA will ask if you want to rename this as well.
You'll notice that all uses of this field are changed to the new name, and if you've chosen to rename the getter, other classes in your project will be updated to use the new name. See the next step for more information on method renaming.
To rename the method, the process is the same: place your caret at
init
and press Shift+F6. Here you'll have fewer suggestions, so type the new name:As well as renaming the method, this renames all calls of the method and all overridden/implemented methods in subclasses. IntelliJ IDEA can also rename non-code uses of the name too, which is useful if you have XML configuration or other non-Java files which refer to classes or methods. You can configure what gets renamed if you press Shift+F6 a second time to bring up the rename dialog
If the rename will apply to more than just code, IntelliJ IDEA will preview the refactoring for you, so you can select which changes you want to make. Often in these cases you may choose not to rename occurrences in comments, especially if the original method name was a common word like
name
.If you don't want to make some of these changes, press Backspace on the usages you do not want to change.
Renaming a class is similar, but can also be performed via the Project tool window. In this case, because we've discovered we want to rename the class where we use it, we're going to use Shift+F6 on the class name in the code.
Of course, any code that uses this class will also be renamed, but you also have the option of renaming variables, inheritors and other parts of the code, so they're aligned with the new name. Again, these options can be set by pressing Shift+F6 a second time.
Impact of renaming
Renaming local variables or private methods can be done fairly safely on-the-fly. For example, while you're working on a piece of functionality that touches this area of code, you can perform this refactoring knowing the impact is limited in scope.
Renaming classes or public methods could potentially impact a lot of files. If this is the case, this sort of refactoring should, at the very least, be in its own separate commit so the changes are clearly separated from any changed or additional functionality that you may have been working on at the time.
Extracting
IntelliJ IDEA's extract refactorings give developers the power to reshape their code when it becomes clear the current design, whether on a small or large scale, is no longer fit for purpose.
Extract variable
Extract variable is a low-impact change to make your code code self-documenting. It can also be used to reduce code duplication.
Imagine you come across the following code
Removing the duplication of
message.indexOf("\"screen_name\":\"") + 15)
Introducing variables to describe what each of the
indexOf
calls representRemoving the magic number 15
First, let's reduce the duplication and introduce a variable that describes what this operation is doing. Place your caret anywhere in the expression
message.indexOf("\"screen_name\":\"") + 15)
and press Control+Alt+V. IntelliJ IDEA will suggest a context for this refactoring, and you want to choose the one that encapsulates this expression:Next, if IntelliJ IDEA has detected this expression occurs more than once, you have the option to replace all the occurrences or just the one you selected.
Once the variable is extracted, IntelliJ IDEA suggests possible names based on things like the parameter the expression was used in.
We're going to use our own name,
indexOfFieldValue
, to describe what this really represents. Note that you can decide whether or not you wish this variable to be final.Next we're going to introduce a variable for the String value. There are two reasons for this: firstly, to document what the String value represents, and secondly because it will help us to remove the magic number.
Place your caret somewhere on
screen_name
and press Control+Alt+V.We're going to give this a more meaningful name,
fieldName
.Now, we're going to create a variable for the other expression used as a parameter to
substring()
, using the same process, and we'll call thisindexOfEndOfFieldValue
.Finally, we can remove the magic number, as this is just the length of the field name. The final code looks like:
static String getUsernameFromMessage(String message) { final String fieldName = "\"screen_name\":\""; final int indexOfFieldValue = message.indexOf(fieldName) + fieldName.length(); final int indexOfEndOfFieldValue = message.indexOf("\"", indexOfFieldValue); return message.substring(indexOfFieldValue, indexOfEndOfFieldValue); }It's longer than the original, but it's much more descriptive, which is particularly important in code like this where it's not clear what each expression represents. The choice of applying
final
or not is up to you, and depends upon your coding standards.
Extract parameter
Extracting or adding a parameter allows a developer to change a method so that it's easier to use. You may want to change the parameters, for example, by passing in some values from an Object rather than the object itself, or you may want to introduce a value from the method body as a parameter to allow the method to be used in more places. We're going to look at an example of the latter.
For this example, we're going to use the same code as the last example, after it has been refactored, and extend it slightly to show another method in the same class:
Our goal is to remove the duplication of code that we see in these two methods. To do that, we're going to:
Change
fieldName
into a parameter so that we can make thegetUsernameFromMessage
method apply to any field.Rename
getUsernameFromMessage
to something that represents its more general natureRemove the duplication of code in
getTextFromMessage
.
Place your caret at
fieldName
and press Control+Alt+PAs with the other refactorings, you can type a new name for the parameter if you wish. IntelliJ IDEA also previews the updated method signature. Press Enter to approve the changes.
This particular issue tells us the method is being used as a method reference, and this change will result in the method reference being converted into a lambda expression. This message could be a sign that this is not the refactoring you wish to perform. If this is the case, the next example shows an approach we could take using Extract Method. However, for this example we'll assume we're happy with the consequences of introducing a new parameter, so we'll just select Continue.
Next, IntelliJ IDEA will detect any code that can now be replaced with a call to the new method signature.
If you select Replace in this case, all the duplicate code will be replaced, and IntelliJ IDEA will select the appropriate value to pass in for the new parameter.
At this point, the original method
getUsernameFromMessage
is more general than it was, so we should rename it. We put our caret at the name and use Shift+F6, as shown in the previous section.We can simplify the code further. Inline is the inverse of extract, and in the code we have here it may be appropriate to inline our temporary variable, as the variable name gives us little more than having the value passed directly into the method. Or, given that getTextFromMessage really is a simple delegation to
getValueForField
, we can use inline to remove this method completely.To inline, place your caret at the
getTextFromMessage
variable and press Control+Alt+NNow our final code looks like:
static String getValueForField(String message, String fieldName) { final int indexOfFieldValue = message.indexOf(fieldName) + fieldName.length(); final int indexOfEndOfFieldValue = message.indexOf("\"", indexOfFieldValue); return message.substring(indexOfFieldValue, indexOfEndOfFieldValue); }Our code that was calling the original getUsernameFromMessage method was:
Parser::getUsernameFromMessage
and is now
(message) -> Parser.getValueForField(message, "\"screen_name\":\"")
Our code that was calling the original getTextFromMessage method was:
String[] wordsInMessage = Parser.getTextFromMessage(message).split("\\s");
and is now
String[] wordsInMessage = Parser.getValueForField(message, "\"text\":\"").split("\\s");
Note that the way we applied this refactoring forces all callers to pass in the field name and a) spreads the use of a String value around your code and b) may introduce duplication of one or more of these String values. This may be appropriate for your code, especially if the String duplication is dealt with, or the method is not frequently used. However, if this is not a tradeoff you wish to make for reducing the duplication of code, see the next chapter for an alternative approach.
Extract parameter can be very powerful, so it's worth reading the more detailed help page.
Extract method
One way to aid code readability is to have it in small, understandable sections. Extract method allows a developer to do just that, moving segments of code into their own, descriptively-named, method when appropriate.
Some developers may find themselves writing long methods that perform the operation they have in mind, and when they've completed (and tested) the functionality, look at the code to see where it can be refactored and simplified and break up these longer methods. Or, as a developer comes across code when they're implementing a new feature, they realise extracting some code into its own method lets them reuse existing functionality.
We're going to look at the same example as in the previous section, but take a slightly different approach to before.
As we saw earlier, the previous refactoring had some trade-offs: a method reference needed to be converted to a lambda expression, and all calling code needed to know the field name that was required. We may choose to remove the code duplication between the two methods in a different way:
Extracting the common code into its own method.
Inlining variables to simplify the remaining code.
First, highlight the code that's common between the two methods:
Pressing Control+Alt+M will bring up the Extract Method Dialog.
Type the name of the new method,
getValueForField
, and check the parameter names and order. In this case, we're going to swap the order of the parameters because we prefer thefieldName
parameter to be closer to the name of the method. This will depend upon your code style and team preferences, you might like to read the name and parameters aloud to see if it makes sense as a statement in natural language.When you press OK, IntelliJ IDEA will detect code that can be replaced with a call to this new method, and will offer to refactor that too. We're going to select Yes.
At this point, our
getTextFromMessage
andgetUsernameFromMessage
methods are two lines of simple code, and here it makes sense to inline the fieldName variable, as the method name is descriptive enough to remove the temporary variable. Press Control+Alt+N onfieldName
and select Refactor.As a last touch, you may want all similar methods grouped together. Depending upon your settings, IntelliJ IDEA may have placed the new method directly under the method you were in when you chose to extract the method, as in our case here. To put the helper methods next to each other, place the caret at the
getValueForField
method name and press Control+Shift+ArrowDown. This will place your new method,getValueForField
, under the existing getUsernameFromMessage method.Our final code looks like:
static String getTextFromMessage(String message) { return getValueForField("\"text\":\"", message); } static String getUsernameFromMessage(String message) { return getValueForField("\"screen_name\":\"", message); } static String getValueForField(String fieldName, String message) { final int indexOfFieldValue = message.indexOf(fieldName) + fieldName.length(); final int indexOfEndOfFieldValue = message.indexOf("\"", indexOfFieldValue); return message.substring(indexOfFieldValue, indexOfEndOfFieldValue); }Now we have two very specific helper methods which get the message body and the username, and a more general method that can be used to get the value of any field from the message. Additional helper methods can be added when there are other fields that are frequently needed.
Note that the Extract Parameter and Extract Method examples start with the same code, but end with code that looks very different. This is not just because we used a different refactoring, but because we made different decisions – in the first example, we chose to remove duplication completely, and move some of the decision-making into the caller of the method. In the second example, we chose to provide an API which hid the details of the field name behind small helper methods but still provide the more general method as well. We also could have mixed and matched the approaches, the refactoring we chose to begin with may have led us in a particular direction, but we can dictate our final destination. We should remember the goal of our refactoring (in this case, reduce duplication) and understand the tradeoffs we're making when we choose one direction over another, for example deciding whether we want calling code to know which field name they're asking for.
Impact of extracting
The good news is that you can undo an extract fairly easily. Not only by selecting Control+Z, of course, but by inlining the created method so that the code is back where it used to be.
The extract refactorings we've mentioned here are used regularly by experienced developers to shape the code as it evolves, and it would not be uncommon to use them to some greater or lesser extent every time the code is touched. Some of the ones that were not covered, like extract interface and extract superclass, may have a wider impact on the overall design, and more care should be taken with them.
Deleting
Sometimes when you've refactored code in several steps, you can end up with code that is no longer used, or ideally should not be used. As the goal of refactoring is simplification, you should always aim to remove unused code where you can - regardless of any impact (or not) unused code has on the performance of your application, unused code definitely takes a toll on developers working with and trying to understand the application.
Safe delete
IntelliJ IDEA lets you safe delete unused code fragments or whole files, informing you if it's safe to delete the code, and giving you the option to preview the changes before you make them. The fastest way to identify and deal with unused code is to make sure the relevant inspections are enabled, which they usually are by default:
Let's continue the example of our previous refactoring. Assuming we ended up with this code:
It's possible that some time later, when we come back to this code, the getUsernameFromMessage
method is no longer used – maybe it's no longer required, or maybe people are comfortable calling getValueForField
with the relevant parameter. So assuming we're comfortable with these reasons, we can go ahead and remove this method.
If the unused declaration inspection is turned on, the method name will be in grey to represent that it is unused.
Place the caret in
getUsernameFromMessage
and press Alt+Enter. This will give you the option to delete this method.Selecting safe delete will pop up the safe delete dialog, allowing you to search for usages of this method.
Press OK to go ahead and do the search. In our case, it was completely safe to delete, so the method is removed.
It's possible that our "unused" method is not flagged as unused, as it may be covered by a test. But if we still know that it's unused, or have checked it via Alt+F7, we can still safely delete it.
Place the caret at the method name and press Alt+Delete. This will pop up the safe delete dialog as before, and this time when you press OK IntelliJ IDEA will warn you that this method has usages
Press View Usages to check what these are
Use the results panel to navigate to the usages by double-clicking on each usage. In our case, we see there's a test that calls the method we want to delete.
Since this test is there to ensure the correctness of a method we no longer want, we can delete this test too. In the editor window, press Alt+Delete on the test method name and say OK in the Safe Delete dialog. The test method will be removed.
Now we'll see in our Safe Delete Conflicts window that this code is no longer valid.
Since this was our only usage of the method we originally wanted to delete, we can select the Rerun Safe Delete button. This time when you press OK on the Safe Delete dialog, the
getUsernameFromMessage
method will have been removed.
Impact of deleting
IntelliJ's inspections can show you code that is unused, but if your code is going to be packaged as a library for others to use, or exposes a public API in some other way, it's possible that some public symbols may be marked as unused when in actual fact they are used by code that you do not control. If public symbols appear to be unused, you should check if these are used by other systems in some way.
Unused parameters, local variables, and private fields are good candidates for deletion as it should be easy to see that deleting them does not impact any functionality.
Using safe delete to remove symbols, whether they were unused or not, lets you check before you perform the refactoring that the areas impacted are the ones you expect, and gives you control over the changes you wish to apply. However, still be aware of the caveat that public symbols may be used by systems out of your control, so always exercise caution when deleting these.
Conclusion
IntelliJ IDEA has a number of automatic refactorings available, all of which aim to let you, the developer, reshape your code in as low-impact way as possible. The aim is to make small, incremental changes, all the time keeping the code in a state that compiles. The power of the refactoring capabilities lies in chaining smaller changes together to move the code in the direction of some goal that you have in mind: reducing duplication, removing unnecessary code, striving for simplicity, improving readability, or some larger re-shaping of the design.
Small, simple changes are possible, even desirable, while working on new features or bug fixing, but remember that larger changes may need to be applied separately to differentiate between refactoring that should not impact existing functionality and functional changes.