迁移到Java 8
使您的代码与最新版本的语言和库保持最新是一项具有挑战性的任务。幸运的是,IntelliJ IDEA可以使这变得更容易,通过检查来指导您的工作,自动修复和通常的重构工具。
Java SE 8为语言带来了全新的概念,如lambda表达式,并为开发人员多年来一直使用的类添加了新的方法。此外,还有新的工作方式,包括新的Date和Time API,以及一个可帮助null安全的Optional类型。
在本教程中,我们将展示IntelliJ IDEA如何帮助您将代码从Java 6(或7)转换为Java 8,并使用代码示例来显示可用的帮助以及何时可能或可能不会选择使用新功能。
本教程假定以下先决条件:
- 您已经有一个现有代码库的IntelliJ IDEA项目。
接近问题
IntelliJ IDEA提供的大量选项和功能可能非常庞大,尤其是在解决与尝试将整个代码库(甚至只是模块或软件包)迁移到新版本的问题时。与大多数软件开发问题一样,以迭代方式处理此问题是值得的。
- 选择要实现的少量更改。
- 选择代码库的一部分以应用它们。
- 批量应用更改,经常运行项目测试,并在测试为绿色时检入VCS系统。
为此,本教程将对更改进行分组,而不是采用大爆炸的方法。
初始设置
- 确保使用Java 8 SDK进行编译。如果不是,请将SDK更改为最新版本的Java 8。
- 在项目设置中,您应将语言级别设置为“8.0 - Lambdas,键入注释”。
如果要在CI环境中编译代码,则还需要确保使用Java 8编译新代码。
配置和运行语言级别迁移检查
您的项目可能已经使用检查来鼓励代码中的某种程度的一致性和质量。为了完全专注于进行与升级到Java 8相关的更改,我们将创建一个新的检查配置文件。
- 导航到检查设置。
- 创建一个名为“Java8”的新检查配置文件。
- 作为此配置文件的起点,使用“重置为空”按钮 来取消选择所有内容。
- 我们将选择一组语言迁移检查来指出我们可能要更新的代码部分:
- Lambda表达式
- 方法参考
- 新的Collection方法
- Streams API
- 单击“确定(OK)”将这些设置保存到“Java8”配置文件并关闭设置窗口。
- 运行检查,选择“Java8”配置文件和范围以运行检查。如果您的项目很小,那可能是整个代码库,但更有可能您想要选择一个模块或包来开始。
一旦分析代码完成运行,您将在检查结果(Inspection Results)工具窗口中看到一组结果。
Lambda表达式
检查将显示您可以自动将代码转换为使用lambda表达式的位置。在现有代码中,您可能会发现许多地方,例如,当您为以下内容创建匿名内部类时:
- Runnable, Callable
- 比较
- FileFilter, PathMatcher
- 事件处理程序(EventHandle)
- 第三方界面,如“Guava's Predicate”
- 在“检查结果(Inspection Results)”工具窗口中,您应该看到在“Java语言级别迁移辅助工具(Java language level migration aids)”下分组的结果。在此标题下,您可能会看到“匿名类型可以替换为lambda(Anonymous type can be replaced with lambda)”。打开此标题可查看IntelliJ IDEA检测到的代码的所有部分,您可以使用lambda。你可能会看到这样的内容:
- 例如,您可能会遇到一个
Runnable
匿名的内部类:executorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { getDs().save(new CappedPic(title)); } }, 0, 500, MILLISECONDS);
- 许多检查建议可以应用修复,“匿名类型可以替换为lambda(Anonymous type can be replaced with lambda)”确实有建议的解决方案。要应用此修复,请执行下列操作:
- 单击检查窗口右侧的“问题解决方案(Problem Resolution)”,在我们的示例中,这是替换为lambda。
- 或者按Alt+Enter编辑器中的灰色代码并选择Replace with lambda。
- 然后,IntelliJ IDEA将自动更改上面的代码以使用lambda表达式:
executorService.scheduleAtFixedRate(() -> getDs().save(new CappedPic(title)), 0, 500, MILLISECONDS);
您会注意到lambda表达式在类型信息方面的表达非常少。这里,这个lambda代表了Runnable
所有的实现, 但却消失了。IntelliJ IDEA将通过左侧装订线中的lambda图标为您提供有关lambda表达式类型的信息:
将鼠标悬停在此将告诉您类型,然后单击可以导航到声明。
应用lambda表达式的影响
您应该能够自动将此修复程序应用于代码库中找到匿名内部类的所有位置,而不会影响系统中的功能。应用更改通常还会提高代码的可读性,删除上面示例中的样板行。
但是,您可能需要检查每个更改,如下所示:
- 较大的匿名内部类在lambda表单中可能不是很易读。
- 您可以进行其他更改和改进。
让我们用一个例子来讨论这两点。
我们可能在我们的测试中使用Runnable对一组特定的断言进行分组:
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); } };
将其转换为lambda会导致:
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); };
这不会短得多,也不会对可读性产生太大影响。
在这些情况下,您可以选择使用IntelliJ IDEA的提取方法将这些行转换为单个方法:
Runnable runnable = () -> { assertUserMatchesSpecification(database, datastoreProvider); };
检查所有lambda转换的第二个原因是可以进一步简化一些lambdas。最后一个例子是其中之一 - IntelliJ IDEA将以灰色显示花括号,并通过Alt+Enter用光标在大括号上按下将弹出建议的更改,语句lambda可以用表达式lambda替换:
接受此更改将导致:
Runnable runnable = () -> assertUserMatchesSpecification(database, datastoreProvider);
一旦您将匿名内部类更改为lambdas并进行任何手动调整,您可能想要进行,例如提取方法或重新格式化代码,运行所有测试以确保所有内容仍然有效。如果是这样,请将这些更改提交给VCS。一旦你完成了这项工作,你就已经准备好进入下一步了。
新的集合方法
新的收集方法
Java 8通过Streams API引入了一种处理数据集合的新方法。不太为人所知的是,我们习惯使用的许多Collection
类都有新的方法,而不是通过Streams API来处理的。例如,java.util.Iterable
有一个forEach
方法允许您传入一个lambda,该lambda表示在每个元素上运行的操作。IntelliJ IDEA的检查将突出显示您可以使用此方法和其他新方法的区域。
- 回到“检查结果(Inspection Results)”工具窗口,您应该在“Java语言级别迁移辅助工具(Java language level migration aids)”下看到“foreach可以使用流api折叠(foreach can be collapsed with stream api)”。您可能没有意识到何时进行所有检查,但并非所有这些修复都将使用Streams API(稍后有关Streams的更多信息)。例如:
for (Class<? extends Annotation > annotation : INTERESTING_ANNOTATIONS) { addAnnotation(annotation); }
INTERESTING_ANNOTATIONS.forEach(this::addAnnotation);
- 方法引用需要一段时间才能习惯,因此您可能更喜欢将其扩展为lambda以查看lambda版本:
在方法引用上按Alt+Enter,然后单击“使用lambda替换方法引用(Replace method reference with lambda)”。当您习惯所有新语法时,这尤其有用。在lambda形式中,它看起来如下:INTERESTING_ANNOTATIONS.forEach((annotation) -> addAnnotation(annotation));
这两个新表单与原始代码完全相同 - 对于INTERESTING_ANNOTATIONS
列表中的每个项目 ,它都使用该项目调用addAnnotation
。
Streams API - foreach
IntelliJ IDEA的检查将建议在适当的Iterable
上使用forEach
,但它也将是新的Streams API,这是一个更好的选择。
该流API 是用于查询和操作数据的强大工具,并且使用它可以显著改变和简化您编写的代码。在本教程中,我们将介绍一些最简单的用例,以帮助您入门。一旦您能更加熟练的使用这种编码风格,您可能希望进一步使用其功能。
- Streams API为我们提供了什么,我们不能简单地使用
forEach
方法?让我们来看一个比上一个循环稍微复杂的例子:public void addAllBooksToLibrary(Set<Book> books) { for (Book book: books) { if (book.isInPrint()) { library.add(book); } } }
- 选择修复“使用forEach替换(Replace with forEach)”将使用Streams API执行相同的操作:
public void addAllBooksToLibrary(Set <Book> books) { books.stream() .filter(book -> book.isInPrint()) .forEach(library::add); }
forEach
参数选择了方法参考。对于过滤器,IntelliJ IDEA使用了lambda,但会在编辑器中建议此特定示例可以使用方法引用:
- 应用此修复程序将提供:
books.stream() .filter(Book::isInPrint) .forEach(library::add);
Streams API - collect
你可能会看到“可以替换为collect调用(can be replaced with collect call)”,而不是“可以替换为 foreach调用(can be replaced with foreach call)”。这与上面的示例非常相似,但它不是在流的末尾调用forEach
方法并执行某些操作,而是使用流的collect方法将流操作的所有结果放入新的Collection
操作中。通常会看到 for
循环遍历某个集合,执行某种过滤或操作,并将结果输出到新集合中,这就是此检查将使用Streams
API识别和迁移的代码类型。
- 在“检查结果(Inspection Results)”工具窗口中,您应该在“Java语言级别迁移辅助工具(Java language level migration aids)”下看到“foreach可以替换为collect调用(foreach can be replaced with collect call)”。选择其中一个检查结果将显示一个可能类似于以下内容的for循环:
List <Key> keys = .... List <Key.Id> objIds = new ArrayList<Key.Id>(); for (Key key : keys) { objIds.add(key.getId()); }
- 应用Replace with collect fix将此代码转换为:
List<Key.Id> objIds = keys.stream().map(Key::getId).collect(Collectors.toLis t());
- 重新格式化此代码,以便您可以更清楚地看到所有Stream操作:
List<Key.Id> objIds = keys.stream() .map(Key::getId) .collect(Collectors.toList());
Key
,将每个Key
“映射”到其Id
中,并将它们收集到新的列表objIds
中。
与forEach
示例一样 ,如果过滤器需要应用于collect语句以及映射,IntelliJ IDEA可以解决,因此它可以巧妙地将许多复杂循环转换为一组Stream操作。
使用Streams替换foreach的影响
运行这些检查可能会很诱人,而且只需自动地应用所有修复程序即可。在转换代码以在集合或流上使用新方法时,应该稍加注意。IDE将确保您的代码以与以前相同的方式工作,但您需要在应用更改后检查代码是否仍然可读且易于理解。如果您和您的团队第一次使用Java 8功能,那么一些新代码将非常陌生并且可能不清楚。花些时间单独查看每个更改,并确保您在开始之前了解新代码。
与lambdas一样,一个好的经验法则是从一小段代码开始 - 用于转换为两个或更少的流操作的循环的缩写,最好是使用单行lambdas。随着您对这些方法越来越熟悉,您可能希望解决更复杂的代码问题。
我们来看一个例子:
IntelliJ IDEA建议使用下列代码:
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); } });
撇开原始代码开始时难以理解的事实,您可以选择不应用更改,原因如下:
- 尽管重构了外循环,但在forEach方法中仍然有一个for循环 。这表明可能有不同的方式来构造流调用,可能使用flatMap。
- 该destination.addAnnotation方法表明可能有一种方法可以重新构建它以使用collect调用而不是forEach。
- 它可能比原始代码更容易理解。
但是,您可以选择接受此更改,原因如下:
- 这是一段复杂的代码,它迭代并处理集合中的数据,因此向Streams API的转变是朝着正确的方向发展。当团队的开发人员更熟悉Streams的工作方式时,可以进一步重构或改进它。
- 在新代码中,if条件已被转移到filter调用中,使代码的这一部分更清楚。
除了“保持代码(keep the code)”和“应用更改(apply the changes)”选项之外,还有第三种选择:将旧代码重构为更易读的内容,即使它不使用Java 8。这可能是一段很好的代码稍后重构的一个注释,而不是试图解决所有代码的问题,而只是尝试采用更多的Java 8约定。
新的Date和Time API
我们为“Java8”配置文件选择的检查帮助我们找到可以使用lambda表达式的位置,Collections上的新方法和Streams API,并将自动应用修复程序到这些位置。Java 8中还有许多其他新功能,在下面的部分中,我们将重点介绍IntelliJ IDEA的一些功能,这些功能也可以帮助您使用它们。
在本节中,我们将介绍如何使用新的Date和Time API来定位可能会受益的地方,而不是java.util.Date
和java.util.Calendar
。
- 您需要启用新检查以查找旧Date和Time API的使用。
java.util.Date
了一段时间,但本身并未弃用,因此如果在代码中使用它,则不会收到弃用警告。这就是为什么这种检查对于定位用法很有用。 - 运行检查。您应该看到一个结果列表,如下所示:
- 与之前的检查不同,这些检查没有建议的修复,因为它们将要求您和您的团队评估旧类的使用并决定如何将它们迁移到新API。如果您有一个表示没有时间的单个日期的
Date
字段,例如:public class HotelBooking { private final Hotel hotel; private final Date checkInDate; private final Date checkOutDate; // constructor, getters and setters... }
LocalDate
替换它,这可以通过上下文菜单中的Refactor|输入Migration ...(Refactor | Type Migration...)来完成,或者通过快捷键:Ctrl+Shift+F6。在弹出窗口中键入LocalDate并选择java.time.LocalDate
。按Enter键时,将更改此字段的类型以及getter和setter。您可能仍需要解决使用字段,getter或setter的编译错误。 - 对于同时具有日期和时间的字段,您可以选择将这些字段迁移到
java.time.LocalDateTime
。对于只有时间的字段,java.time.LocalTime
可能是合适的。 - 如果您使用new设置原始值
Date
,则知道这相当于现在的日期和时间:booking.setCheckInDate(new Date());
now()
方法:booking.setCheckInDate(LocalDate.now());
- 设置
java.util.Date
值的常用且可读的方法是使用java.text.SimpleDateFormat
。您可能会看到类似于以下内容的代码:SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); booking.setCheckInDate(format.parse("2017-03-02"));
LocalDate
,您可以轻松地将其设置为特定日期,而无需使用格式化程序:booking.setCheckInDate(LocalDate.of(2017, 3, 2));
迁移到新的Date and Time API的影响
更新代码以使用新的Date和Time API比将匿名内部类迁移到Lambda Expressions并循环到Streams API需要更多的手动干预。IntelliJ IDEA将帮助您了解使用旧java.util.Date和java.util.Calendar类的数量和位置 ,这将有助于您了解迁移的范围。IntelliJ IDEA的重构工具可以帮助您在必要时迁移这些类型。但是,您需要制定一个策略,了解如何处理每个更改,您要使用哪些新类型,以及如何正确使用这些更改。这不是一个可以自动应用的更改。
使用Optional
我们将看到的最后一个Java 8功能是新的Optional类型。java.util.Optional
为您提供了一种处理空值的方法,以及一种指定方法调用是否应返回空值的方法。与日期和时间一样,IntelliJ IDEA的功能将帮助您识别可能从使用该Optional
类型中受益的代码区域 。
- 有许多检查在Java代码中查找使用空值,这些检查可用于识别可能从
Optional
使用中受益的区域。为简单起见,我们将考虑仅启用其中两项检查:
- 运行代码分析。您应该看到一个结果列表,如下所示:
- 如果您看到字段的“Assignment to null”,您可能需要考虑将此字段转换为
Optional
。例如,在下面的代码中,将标记分配了偏移量的行:private Integer offset; // code.... public Builder offset(int value) { offset = value > 0 ? value : null; return this; } // more code...
if (offset != null) { cursor.skip(offset); }
Optional
字段更改为Integer
,可以通过:Ctrl+Shift+F6,并更改值的设置方式:private Optional<Integer> offset; // code... public Builder offset(int value) { offset = value > 0 ? Optional.of(value) : Optional.empty(); return this; } // more code...
Optional
而不是执行空检查。最简单的解决方案是:if (offset.isPresent()) { cursor.skip(offset); }
offset.ifPresent(() -> cursor.skip(offset));
- 检查还指示方法返回null的位置。如果您有一个可以返回null值的方法,则调用此方法的代码应检查它是否返回null并采取适当的操作。但是很容易忘记这样做,特别是如果开发人员不知道该方法可以返回null。更改这些方法以返回
Optional
会使其更加明确,这可能不会返回值。例如,我们的检查可能会将此方法标记为返回null值:public Customer findFirst() { if (customers.isEmpty()) { return null; } else { return customers.get(0); } }
Optional
的Customer
:public Optional<Customer> findFirst() { if (customers.isEmpty()) { return Optional.empty(); } else { return Optional.ofNullable(customers.get(0)); } }
- 您需要更改调用这些方法的代码来处理
Optional
类型。如果该值不存在,这可能是决定该怎么做的正确位置。在上面的示例中,调用findFirst
方法的代码可能如下所示:Customer firstCustomer = customerDao.findFirst(); if (firstCustomer == null) { throw new CustomerNotFoundException(); } else { firstCustomer.setNewOffer(offer); }
Optional
,我们可以消除空检查:Optional<Customer> firstCustomer = customerDao.findFirst(); firstCustomer.orElseThrow(() -> new CustomerNotFoundException()) .setNewOffer(offer);
迁移到Optional的影响
将字段类型更改为Optional可能会产生很大影响,并且自动执行所有操作并不容易。首先,尝试继续使用Optional类内部 - 如果您可以将字段更改为Optional尝试,而不通过getter和setter公开它,这将允许您进行更渐进的迁移。
将更改方法返回类型更改为Optional会产生更大的影响,您可能会发现这些更改会以意想不到的方式影响您的代码库。将此方法应用于可以为null的所有值可能会导致Optional变量和字段遍布整个代码,多个位置执行isPresent检查或使用Optional方法执行操作或抛出适当的异常。
请记住,在Java 8中使用新功能的目的是简化代码并提高可读性,因此将更改的范围限制为代码的小部分,并检查使用Optional是否使代码更易于理解,而不是更难以实现保持。
IntelliJ IDEA的检查将确定可能的变更地点,重构工具可以帮助应用这些变更,但重构会对Optional产生很大的影响,您和您的团队应该确定哪些领域需要改变以及如何处理这些变化。您甚至可以使用“Annotate field [fieldName] as @Nullable”的建议修正来标记那些可以迁移到Optional的字段, 以便朝着该方向迈出一步,对代码的影响更小。
总结
IntelliJ IDEA的检查,特别是那些围绕语言迁移的检查,可以帮助识别代码中可以重构以使用Java 8功能的区域,甚至可以自动应用这些修复。
如果您已自动应用修复程序,那么查看更新的代码以检查它是否难以理解并帮助您熟悉新功能是很有价值的。
本教程提供了有关如何迁移代码的一些指示。我们已经介绍了lambda表达式和方法引用,一些关于Collection的新方法 ,介绍了Streams API,展示了IntelliJ IDEA如何帮助您使用新的Date和Time API, 并研究了如何识别使用新的Optional类型可能受益的位置。
Java 8中有许多新功能,旨在使程序员的工作更轻松 - 使代码更具可读性,并使其更容易在数据结构上执行复杂的操作。IntelliJ IDEA当然不仅支持这些功能,还可以帮助开发人员使用它们,包括迁移现有代码并在编辑器中提供帮助和建议,以便在您使用它们时为您提供指导。