IntelliJ IDEA offers a lot of
automatic refactoring capabilities, but as developers it's 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's 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 the 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
Rename endpoint, a field, to describe what sort of endpoint it is.
Rename init(), a method on Server, to something that more
accurately describes that method.
Rename Server, a class, to something more specific.
To rename the endpoint field, place your cursor on 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 cursor on 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.
We can use extract variable to improve the readability of this code by:
Removing the duplication of
message.indexOf("\"screen_name\":\"") + 15)
Introducing variables to describe what each of the
indexOf
calls represent
Removing the magic number 15
First, let's reduce the duplication and introduce a variable that describes what this operation is
doing. Place your cursor anywhere in the expression
message.indexOf("\"screen_name\":\"") + 15)
and
pressCtrl+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's 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 cursor somewhere on
screen_name
and pressCtrl+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 this
indexOfEndOfFieldValue.
Finally, we can remove the magic number, as this is just the length of the field name. The
final code looks like:
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 the
getUsernameFromMessage method apply to any field.
Rename getUsernameFromMessage to something which represents its more general nature
Remove the duplication of code in getTextFromMessage.
Place your cursor on
fieldName
and press
Ctrl+Alt+P
As 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 which 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 cursor on the name and useShift+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 cursor on the
getTextFromMessage
variable and press
Ctrl+Alt+N
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 trade off you wish to make for reducing the duplication of code, see
the next chapter for an
alternative approach.
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:
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 the
fieldName 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
and
getUsernameFromMessage
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 Ctrl+Alt+N on
fieldName
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, put your
cursor on the
getValueForField
method name and press
Ctrl+Shift+Down. This will place your new method,
getValueForField, under the
existing getUsernameFromMessage method.
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 which 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 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 lead 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 trade offs 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
Ctrl+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 cursor 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 cursor on 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 applied separately to differentiate between refactoring that
should not impact existing functionality and functional changes.