Keeping your code up to date with the latest versions of languages and libraries is a challenging task. Fortunately, IntelliJ IDEA can make this easier, with inspections to guide your efforts, automatic fixes, and the usual refactoring tools.
Java SE 8
brings entire new concepts to the language, like lambda expressions, and adds new methods to classes that
developers have been using comfortably for years. In addition, there are new ways of doing things, including the
new
Date and Time API
,
and an Optional
type to help with null-safety.
In this tutorial we're going to show how IntelliJ IDEA can help you transition your code from Java 6 (or 7) to Java 8, using code examples to show what help is available and when you may, or may not, choose to use new features.
This tutorial assumes the following prerequisites:
- You already have an IntelliJ IDEA project for an existing codebase.
On this page:
- Approaching the problem
- Initial setup
- Configuring and running language level migration inspections
- Lambda expressions
- Impact of applying lambda expressions
- New Collection Methods
- Streams API - foreach
- Streams API - collect
- Impact of replacing foreach with Streams
- New Date and Time API
- Impact of migrating to the new Date and Time API
- Using Optional
- Impact of migrating to Optional
- Summary
- Pick a small number of changes to implement.
- Pick a section of the codebase to apply them to.
- Apply the changes in batches, running your project tests frequently and checking in to your VCS system when the tests are green.
- Make sure you're compiling with a Java 8 SDK. If you're not, change your SDK to the latest version of Java 8.
- In the project settings, you should set your language level to "8.0 - Lambdas, type annotations".
- Navigate to the inspections settings.
- Create a new inspection profile called "Java8".
-
As a starting point for this profile, deselect everything using the "reset to empty" button
.
-
We're going to select a set of language migration inspections to point out sections of the code we might
want to update:
These inspections will show us areas in your code where you may be able to use the following Java 8
features:
-
Lambda expressions
-
Method references
- New
Collection
methods
-
Streams API
-
Lambda expressions
- Click OK to save these settings to the "Java8" profile and close the settings window.
- Run the inspections, selecting the "Java8" profile and the scope to run the inspections on. If your project is small, that might be the whole codebase, but more likely you will want to select a module or package to start with.
- In the Inspection Results Tool Window, you should see results grouped under "Java
language level migration aids". Under this heading, you may see "Anonymous type can be replaced with
lambda". Open up this heading to see all the sections of the code where IntelliJ IDEA has detected you can
use a lambda. You might see something like this:
- For example, you may come across a
Runnableanonymous inner class:executorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { getDs().save(new CappedPic(title)); } }, 0, 500, MILLISECONDS);
- Many inspections suggest a fix that can be applied, and "Anonymous type can be replaced with
lambda" does have a suggested resolution. To apply the fix, either:
- Click on the Problem Resolution in the right of the inspection window, in our case this is Replace with lambda.
- Or press Alt+Enter on the grey code in the editor and select Replace with lambda.
- IntelliJ IDEA will then automatically change the code above to use a lambda expression:
executorService.scheduleAtFixedRate(() -> getDs().save(new CappedPic(title)), 0, 500, MILLISECONDS);
Impact of applying lambda expressions
You should be able to automatically apply this fix to all places where anonymous inner classes are found in your codebase without impacting the functionality in your system. Applying the change will generally also improve the readability of your code, removing lines of boilerplate like in the example above.
However, you may want to check each individual change, as:
- Larger anonymous inner classes may not be very readable in a lambda form.
- There may be additional changes and improvements you can make.
Let's address both points with an example.
We might be using a
Runnable
to group a specific set of assertions in our test:
Runnable runnable = new Runnable() { @Override public void run() { datastoreProvider.register(database); Assert.assertNull(database.find(User.class, "id", 1).get()); Assert.assertNull(database.find(User.class, "id", 3).get()); User foundUser = database.find(User.class, "id", 2).get(); Assert.assertNotNull(foundUser); Assert.assertNotNull(database.find(User.class, "id", 4).get()); Assert.assertEquals("Should find 1 friend", 1, foundUser.friends.size()); Assert.assertEquals("Should find the right friend", 4, foundUser.friends.get(0).id); } };
Converting this to a lambda results in:
Runnable runnable = () -> { datastoreProvider.register(database); Assert.assertNull(database.find(User.class, "id", 1).get()); Assert.assertNull(database.find(User.class, "id", 3).get()); User foundUser = database.find(User.class, "id", 2).get(); Assert.assertNotNull(foundUser); Assert.assertNotNull(database.find(User.class, "id", 4).get()); Assert.assertEquals("Should find 1 friend", 1, foundUser.friends.size()); Assert.assertEquals("Should find the right friend", 4, foundUser.friends.get(0).id); };
In cases like these, you may choose to use IntelliJ IDEA's extract method to pull these lines into a single method instead:
Runnable runnable = () -> { assertUserMatchesSpecification(database, datastoreProvider); };
The second reason to check all your lambda conversions is that some lambdas can be further simplified. This last example is one of them - IntelliJ IDEA will show the curly braces in grey, and pressing Alt+Enter with the cursor on the braces will pop up the suggested change Statement lambda can be replaced with expression lambda:

Accepting this change will result in:
Runnable runnable = () -> assertUserMatchesSpecification(database, datastoreProvider);
Once you've changed your anonymous inner classes to lambdas and made any manual adjustments you might want to make, like extracting methods or reformatting the code, run all your tests to make sure everything still works. If so, commit these changes to VCS. Once you've done this, you'll be ready to move to the next step.
- Back in the Inspection Results Tool Window, you should see "foreach can be collapsed
with stream api" under "Java language level migration aids". You may not realise when you're going
through all the inspections, but not all of these fixes will use the Streams API (more on Streams later).
For example:
IntelliJ IDEA suggests "Can be replaced with foreach call". Applying this inspection gives us:
for (Class<? extends Annotation > annotation : INTERESTING_ANNOTATIONS) { addAnnotation(annotation); }
Note that IntelliJ IDEA has applied all simplifications it could, going as far as using a Method ReferenceINTERESTING_ANNOTATIONS.forEach(this::addAnnotation);
rather than a lambda. Method references are another new features in Java 8, which can generally be used
where a lambda expression would usually call a single method.
-
Method references take a while to get used to, so you may prefer to expand this into a lambda to see the
lambda version:
Press Alt+Enter on the method reference and click
Replace method reference with lambda. This is especially useful as you get used to
all the new syntax. In lambda form, it looks like:
INTERESTING_ANNOTATIONS.forEach((annotation) -> addAnnotation(annotation));
- What does the Streams API give us that we can't simply get from using a
forEachmethod? Let's look at an example that's a slightly more complicated for loop than the previous one:Firstly the loop body checks some condition, then does something with the items that pass that condition.public void addAllBooksToLibrary(Set<Book> books) { for (Book book: books) { if (book.isInPrint()) { library.add(book); } } }
-
Selecting the fix
Replace with forEach will use the Streams API to do the same thing:
In this case, IntelliJ IDEA has selected a method reference for the
public void addAllBooksToLibrary(Set <Book> books) { books.stream() .filter(book -> book.isInPrint()) .forEach(library::add); }
forEachparameter. For filter, IntelliJ IDEA has used a lambda, but will suggest in the editor that this particular example can use a method reference:
- Applying this fix gives:
books.stream() .filter(Book::isInPrint) .forEach(library::add);
-
In the Inspection Results Tool Window, you should see "foreach can be replaced with
collect call" under "Java language level migration aids". Selecting one of these inspection results will
show you a for loop that might look something like:
Here, we're looping over a list of Key objects, getting the Id from each of these objects, and putting them all into a separate collection of objIds.
List <Key> keys = .... List <Key.Id> objIds = new ArrayList<Key.Id>(); for (Key key : keys) { objIds.add(key.getId()); }
- Apply the
Replace with collect
fix to turn this code into:
List<Key.Id> objIds = keys.stream().map(Key::getId).collect(Collectors.toList());
- Reformat this code so that you can see more clearly all the Stream operations:
This does exactly the same thing the original code did - takes a collection of
List<Key.Id> objIds = keys.stream() .map(Key::getId) .collect(Collectors.toList());
Keys, "maps" eachKeyto itsId, and collects those into a new list,objIds.
Impact of replacing foreach with Streams
It may be tempting to run these inspections and simply apply all fixes automatically. When it comes to converting your code to use new methods on Collections or Streams, a little care should be taken. The IDE will ensure that your code works the same way it used to, but you need to check that your code remains readable and understandable after applying the changes. If you and your team are using Java 8 features for the first time, some of the new code will be very unfamiliar and probably unclear. Take the time to look at each change individually and check you're happy you understand the new code before going ahead.
Like with lambdas, a good rule of thumb is to start with small sections of code - short for loops that translate into two or fewer stream operations, preferably with single-line lambdas. As you become more familiar with the methods, then you may want to tackle more complex code.
Let's look at an example:
IntelliJ IDEA suggests that this code:
for (Entry<Class <? extends Annotation>, List<Annotation>> e : getAnnotations().entrySet()) { if (e.getValue() != null && !e.getValue().isEmpty()) { for (Annotation annotation: e.getValue()) { destination.addAnnotation(e.getKey(), annotation); } } }
getAnnotations().entrySet() .stream() .filter(e -> e.getValue() != null && !e.getValue().isEmpty()) .forEach(e -> { for (Annotation annotation: e.getValue()) { destination.addAnnotation(e.getKey(), annotation); } });
- Despite refactoring away the outer-loop, there's still a for loop inside the
forEachmethod. This suggests that there may be a different way to structure the stream call, perhaps using flatMap
.
- The
destination.addAnnotationmethod suggests that there may be a way to restructure this to use acollectcall rather than aforEach. - It's arguably not easier to understand than the original code.
- This is a complex piece of code that is iterating through and manipulating data in a collection, therefore a move towards the Streams API is a move in the right direction. It can be further refactored or improved later when the team's developers are more familiar with the way Streams work.
- In the new code the
ifcondition has been moved into afiltercall, making clearer what purpose this section of the code is.
- You'll need to enable a new inspection to locate uses of the old Date and Time API.
Note that although many methods have been deprecated on
java.util.Datefor some time, the class itself is not deprecated, so if you use it in your code you will not receive deprecation warnings. That's why this inspection is useful to locate usages. -
Run the inspection. You should see a list of results that looks something like
this:
-
Unlike the earlier inspections, these do not have suggested fixes as they will require you and your team
to evaluate the use of the old classes and decide how to migrate them to the new API. If you have a
Datefield that represents a single date without a time, for example:you may choose to replace this with apublic class HotelBooking { private final Hotel hotel; private final Date checkInDate; private final Date checkOutDate; // constructor, getters and setters... }
LocalDate. This can be done via the context menu Refactor | Type Migration... or via Ctrl+Shift+F6. Type LocalDate in the popup and selectjava.time.LocalDate. When you press enter, this will change the type of this field and getters and setters. You may still need to address compilation errors where the field, getters or setters are used. -
For fields that are both date and time, you may choose to migrate these to
java.time.LocalDateTime. For fields that are only time,java.time.LocalTimemay be appropriate. -
If you were setting the original values with a new
Date, knowing that this is the equivalent to the date and time right now:you can instead use thebooking.setCheckInDate(new Date());
now()method:booking.setCheckInDate(LocalDate.now());
-
A common and readable way to set a value for
java.util.Datewas to usejava.text.SimpleDateFormat. You might see code that looks something like:If this check in date has been migrated to aSimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); booking.setCheckInDate(format.parse("2017-03-02"));
LocalDate, you can easily set this to the specific date without the use of a formatter:booking.setCheckInDate(LocalDate.of(2017, 3, 2));
Impact of migrating to the new Date and Time API
Updating your code to use the new Date and Time API requires much more manual intervention than
migrating anonymous inner classes to Lambda Expressions and loops to the Streams API. IntelliJ IDEA will
help you see how much and where you use the old
java.util.Date and
java.util.Calendar
classes, which will help you understand the
scope of the migration. IntelliJ IDEA's refactoring tools can help you migrate these types if necessary.
However, you will need to have a strategy on how to approach each of the changes, which new types you
want to use, and how to use these correctly. This is not a change you can apply automatically.
- There are a number of inspections that look for the use nulls in Java code, these can be useful for
identifying areas that may benefit from using
Optional. We'll look at enabling just two of these inspections for simplicity:
-
Run the code analysis. You should see a list of results that looks something like
this:
-
If you see "Assignment to null" for fields, you may want to consider turning this field into an
Optional. For example, in the code below, the line where offset is assigned will be flagged:That's because in another method, the code checks to see if this value has been set before doing something with it:private Integer offset; // code.... public Builder offset(int value) { offset = value > 0 ? value : null; return this; } // more code...
In this case, null is a valid value for offset - it indicates this has not been set, and therefore shouldn't be used. You may wish to change the field into anif (offset != null) { cursor.skip(offset); }
OptionalofIntegervia Ctrl+Shift+F6, and alter the way the value is set:Then you can use the methods onprivate Optional<Integer> offset; // code... public Builder offset(int value) { offset = value > 0 ? Optional.of(value) : Optional.empty(); return this; } // more code...
Optionalinstead of performing null-checks. The simplest solution is:But it's much more elegant to use a Lambda Expression to define what to do with the value:if (offset.isPresent()) { cursor.skip(offset); }
offset.ifPresent(() -> cursor.skip(offset));
-
The inspections also indicate places where a method returns null. If you have a method that can return a
null value, the code that calls this method should check if it returned null and take
appropriate action. It's easy to forget to do this though, especially if the developer isn't aware
the method can return a null. Changing these methods to return an
Optionalmakes it much more explicit this might not return a value. For example, maybe our inspections flagged this method as returning a null value:We could alter this method to return anpublic Customer findFirst() { if (customers.isEmpty()) { return null; } else { return customers.get(0); } }
OptionalofCustomer:public Optional<Customer> findFirst() { if (customers.isEmpty()) { return Optional.empty(); } else { return Optional.ofNullable(customers.get(0)); } }
-
You'll need to change the code that calls these methods to deal with the
Optionaltype. This might be the correct place to make a decision about what to do if the value does not exist. In the example above, perhaps the code that calls thefindFirstmethod used to look like this:But we're now returning anCustomer firstCustomer = customerDao.findFirst(); if (firstCustomer == null) { throw new CustomerNotFoundException(); } else { firstCustomer.setNewOffer(offer); }
Optional, we can eliminate the null check:Optional<Customer> firstCustomer = customerDao.findFirst(); firstCustomer.orElseThrow(() -> new CustomerNotFoundException()) .setNewOffer(offer);
Impact of migrating to Optional
Changing a field type to
Optional
can have a big impact, and it's not easy to do everything automatically. To start with, try to keep the
use of
Optional
inside the class - if you can change the field to an
Optional
try not expose this via getters and setters, this will let you do a more gradual migration.
Changing method return types to
Optional
has an even bigger impact, and you may see these changes ripple
through your codebase in an unexpected way. Applying this approach to all values that can be null could
result in
Optional
variables and fields all over the code, with multiple places to performing
isPresent
checks or using the
Optional
methods to perform an action or throw an appropriate exception.
Remember that the goal of using the new features in Java 8 is to simplify the code and aid readability,
so limit the scope of the changes to small sections of the code and check that using
Optional
is making your code easier to understand, not more difficult to maintain.
IntelliJ IDEA's inspections will identify possible places for change, and the refactoring tools can help
apply these changes, but refactoring to
Optional
has a large impact and you and your team should identify a strategy for which areas to change and how to
approach these changes. You can even use the suggested fix of "Annotate field [fieldName] as @Nullable"
to mark those fields that are candidates for migrating to
Optional, in order to take a step in that direction with a smaller impact on the code.
Summary
IntelliJ IDEA's Inspections, in particular those around language migration, can help identify areas in your code that can be refactored to use Java 8 features, and even apply those fixes automatically.
If you have applied the fixes automatically, it's valuable to look at the updated code to check it isn't harder to understand, and to help you become familiar with the new features.
This tutorial gave some pointers on how to migrate your code. We've covered
lambda expressions
and
method references
, some new methods on
Collection
,
introduced the
Streams API
,
shown how IntelliJ IDEA can help you use the new
Date and Time API
and looked at how to identify places that might benefit from using the new
Optional
type.
There are plenty of new features in Java 8 designed to make life easier for programmers - to make code more readable, and to make it easier to perform complex operations on data structures. IntelliJ IDEA of course not only supports these features, but helps developers make use of them, including migrating existing code and providing help and suggestions in the editor to guide you as you use them.