IntelliJ 的重构简介
1. 概述
保持代码整洁并不总是那么容易。对我们来说幸运的是,我们的 IDE 现在非常智能,可以帮助我们实现这一目标。在本教程中,我们将重点介绍 JetBrains Java 代码编辑器 IntelliJ IDEA。
我们将看到编辑器提供的一些功能来重构代码 ,从重命名变量到更改方法签名。
2. 重命名
2.1. 基本重命名
首先,让我们从基础开始:重命名 。IntelliJ 为我们提供了重命名代码的不同元素的可能性:类型、变量、方法,甚至包。
要重命名元素,我们需要遵循以下步骤:
- 右键单击该元素
- 触发重构 > 重命名选项
- 输入新元素名称
- 按
Enter
顺便说一句,我们可以通过选择元素并按 Shift + F6
来替换前两个步骤。
触发后,重命名操作将在代码中搜索元素的每次使用,然后使用提供的值更改它们。
让我们想象一个SimpleClass 类,它有一个命名不当的加法方法someAdditionMethod,在main方法中调用:
public class SimpleClass {
public static void main(String[] args) {
new SimpleClass().someAdditionMethod(1, 2);
}
public int someAdditionMethod(int a, int b) {
return a + b;
}
}
因此,如果我们选择将此方法重命名为 add,IntelliJ 将生成以下代码:
public class SimpleClass() {
public static void main(String[] args) {
new SimpleClass().add(1, 2);
}
public int add(int a, int b) {
return a + b;
}
}
2.2. 高级重命名
然而,IntelliJ 所做的不仅仅是搜索我们元素的代码用法并重命名它们。事实上,还有更多选项可用。IntelliJ还可以搜索注释和字符串中的匹配项,甚至可以搜索不包含源代码的文件中的匹配项。至于参数,它可以在重写方法的情况下在类层次结构中重命名它们。
在重命名我们的元素之前再次按Shift + F6
可以使用这些选项,然后将出现一个弹出窗口:
在注释和字符串中搜索选项可用于任何重命名。至于 Search for text occurrences选项,它不适用于方法参数和局部变量。最后,Rename parameters in hierarchy选项仅适用于方法参数。
因此,如果找到与这两个选项之一的任何匹配项,IntelliJ 将显示它们并为我们提供选择退出某些更改的可能性(例如,以防它匹配与我们的重命名无关的内容)。
让我们向方法中添加一些 Javadoc,然后重命名其第一个参数a:
/**
* Adds a and b
* @param a the first number
* @param b the second number
*/
public int add(int a, int b) {...}
通过检查确认弹出窗口中的第一个选项,IntelliJ 匹配该方法的 Javadoc 注释中提到的任何参数,并提供重命名它们的方法:
/**
* Adds firstNumber and b
* @param firstNumber the first number
* @param b the second number
*/
public int add(int firstNumber, int b) {...}
最后,我们必须注意到**IntelliJ 很聪明,主要搜索重命名元素范围内的用法。**在我们的例子中,这意味着位于方法之外的注释(Javadoc 除外)并且包含对 a 的提及不会被考虑重命名。
3. 提取
现在,让我们谈谈提取 。**提取使我们能够抓取一段代码并将其放入变量、方法甚至类中。**IntelliJ 非常聪明地处理了这个问题,因为它搜索相似的代码片段并提供以相同的方式提取它们。
因此,在本节中,我们将学习如何利用 IntelliJ 提供的提取功能。
3.1. 变量
首先,让我们从变量提取开始。**这意味着局部变量、参数、字段和常量。**要提取变量,我们必须遵循以下步骤:
- 选择适合变量的表达式
- 右键单击所选区域
- 触发Refactor > Extract > Variable/Parameter/Field/Constant选项
- 如果建议,请在仅替换此事件或替换所有 x 事件选项之间进行选择
- 为提取的表达式输入一个名称(如果选择的名称不适合我们)
- 按回车
至于重命名,可以使用键盘快捷键而不是使用菜单。默认快捷键分别是Ctrl + Alt + V
、Ctrl + Alt + P
、Ctrl + Alt + F
和Ctrl + Alt + C
。
IntelliJ 将根据表达式返回的内容尝试为我们提取的表达式猜测一个名称。如果它不符合我们的需求,我们可以在确认提取之前随意更改它。
让我们用一个例子来说明。我们可以想象在我们的SimpleClass类中添加一个方法来告诉我们当前日期是否在两个给定日期之间:
public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
return LocalDate.now().isAfter(startingDate) && LocalDate.now().isBefore(endingDate);
}
假设我们想要更改我们的实现,因为我们使用*LocalDate.now()*两次,我们想确保它在两次评估中具有完全相同的值。现在让我们选择表达式并将其提取到局部变量中:
然后,我们的*LocalDate.now()*调用被捕获在局部变量中:
public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
LocalDate now = LocalDate.now();
return now.isAfter(startingDate) && now.isBefore(endingDate);
}
通过选中Replace all选项,我们确保同时替换了两个表达式。
3.2. 方法
现在让我们检查如何使用 IntelliJ 提取方法:
- 选择适合我们要创建的方法的表达式或代码行
- 右键单击所选区域
- 触发 Refactor > Extract > Method选项
- 输入方法信息:名称、可见性和参数
- 按 回车
选择方法主体后按Ctrl + Alt + M
也可以。*
让我们重用我们之前的示例,并假设我们想要一个方法来检查是否有任何日期介于其他日期之间。然后,我们只需选择 isNowBetween方法中的最后一行并触发方法提取功能。
在打开的对话框中,我们可以看到 IntelliJ 已经找到了三个需要的参数:startingDate、endingDate和now。由于我们希望此方法尽可能通用,因此我们将now参数重命名为date。出于凝聚力的目的,我们将其作为第一个参数。
最后,我们给我们的方法命名,isDateBetween,并完成提取过程:
然后我们将获得以下代码:
public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
LocalDate now = LocalDate.now();
return isDateBetween(now, startingDate, endingDate);
}
private static boolean isDateBetween(LocalDate date, LocalDate startingDate, LocalDate endingDate) {
return date.isBefore(endingDate) && date.isAfter(startingDate);
}
正如我们所见,该操作触发了新isDateBetween方法的创建 ,该方法也在isNowBetween方法中调用。默认情况下,该方法是私有的。当然,这可以使用可见性选项进行更改。
3.3. 类
毕竟,我们可能希望在特定类中获取与日期相关的方法,专注于日期管理,比方说:DateUtils。同样,这很简单:
- 右键单击包含我们要移动的元素的类
- 触发 Refactor > Extract > Delegate选项
- 键入类信息:它的名称、它的包、要委托的元素、这些元素的可见性
- 按 回车
默认情况下,此功能没有可用的键盘快捷键。
比方说,在触发该功能之前,我们在 main方法中调用与日期相关的方法:
isNowBetween(LocalDate.MIN, LocalDate.MAX);
isDateBetween(LocalDate.of(2019, 1, 1), LocalDate.MIN, LocalDate.MAX);
然后我们 使用委托选项将这两个方法委托给DateUtils类:
触发该功能将产生以下代码:
public class DateUtils {
public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
LocalDate now = LocalDate.now();
return isDateBetween(now, startingDate, endingDate);
}
public static boolean isDateBetween(LocalDate date, LocalDate startingDate, LocalDate endingDate) {
return date.isBefore(endingDate) && date.isAfter(startingDate);
}
}
我们可以看到 isDateBetween方法已经被public了。这是可见性选项的结果,默认设置为Escalate,意味着将更改可见性,以确保当前对委托元素的调用仍在编译。**
在我们的例子中,isDateBetween用在 SimpleClass 的main方法中:
DateUtils.isNowBetween(LocalDate.MIN, LocalDate.MAX);
DateUtils.isDateBetween(LocalDate.of(2019, 1, 1), LocalDate.MIN, LocalDate.MAX);
因此,在移动方法时,有必要使其不私有。
但是,可以通过选择其他选项来为我们的元素提供特定的可见性。
4. 内联
既然我们介绍了提取,让我们来谈谈它的对应物:内联 。内联就是获取代码元素并将其替换为它的组成部分。对于一个变量,这将是它被赋值的表达式。对于一个方法,它将是它的主体。之前,我们看到了如何创建一个新类并将我们的一些代码元素委托给它。但有时我们可能希望将方法委托给现有类 。这就是本节的内容。
为了内联一个元素,我们必须右键单击该元素——它的定义或对它的引用——然后触发Refactor > Inline选项。我们还可以通过选择元素并按下Ctrl + Alt + N
键来实现此目的。
此时,IntelliJ 将为我们提供多种选择,无论我们是希望内联变量还是方法,无论我们选择的是定义还是引用。这些选项是:
- 内联所有引用并删除元素
- 内联所有引用,但保留元素
- 只内联选中的引用,保留元素
让我们使用 isNowBetween方法去掉 now变量,它现在看起来有点矫枉过正:
通过内联这个变量,我们将获得以下结果:
public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
return isDateBetween(LocalDate.now(), startingDate, endingDate);
}
在我们的例子中,唯一的选择是删除所有引用并删除元素。但是假设我们也想摆脱 isDateBetween调用并选择内联它。然后,IntelliJ 会为我们提供我们之前谈到的三种可能性:
选择第一个将用方法主体替换所有调用并删除该方法。至于第二个,它会将所有调用替换为方法体,但保留方法。最后,最后一个只会用方法体替换当前调用:
public class DateUtils {
public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
LocalDate date = LocalDate.now();
return date.isBefore(endingDate) && date.isAfter(startingDate);
}
public static boolean isDateBetween(LocalDate date, LocalDate startingDate, LocalDate endingDate) {
return date.isBefore(endingDate) && date.isAfter(startingDate);
}
}
我们 在SimpleClass中的main方法 也保持不变。
5. 移动
早些时候,我们看到了如何创建一个新类并将我们的一些代码元素委托给它。但有时我们可能希望将方法委托给现有类 。这就是本节的内容。
为了移动一个元素,我们必须遵循以下步骤:
- 选择要移动的元素
- 右键单击该元素
- 触发 重构 > 移动选项
- 选择收件人类别和方法可见性
- 按回车
我们也可以通过在选择元素后按F6来实现 。
假设我们向 SimpleClass添加了一个新方法isDateOutstide(),它将告诉我们日期是否位于日期间隔之外:
public static boolean isDateOutside(LocalDate date, LocalDate startingDate, LocalDate endingDate) {
return !DateUtils.isDateBetween(date, startingDate, endingDate);
}
然后我们意识到它的位置应该在我们的DateUtils类中。所以我们决定移动它:
我们的方法现在位于DateUtils类中。我们可以看到方法中对DateUtils的引用 已经消失,因为它不再需要了:
public static boolean isDateOutside(LocalDate date, LocalDate startingDate, LocalDate endingDate) {
return !isDateBetween(date, startingDate, endingDate);
}
我们刚刚做的例子工作得很好,因为它涉及一个静态方法。然而,在实例方法的情况下,事情并不是那么简单。
如果我们想要移动实例方法 ,IntelliJ 将搜索在当前类的字段中引用的类,并提供将方法移动到其中一个类的方法(前提是它们是我们要修改的)。
如果字段中没有引用可修改的类,则 IntelliJ 建议在移动方法之前将其设为static。
6. 更改方法签名
最后,我们将讨论允许我们更改方法签名 的功能。此功能的目的是操纵方法签名的各个方面。
像往常一样,我们必须通过几个步骤来触发该功能:
- 选择要更改的方法
- 右键单击该方法
- 触发重构 > 更改签名选项
- 对方法签名进行更改
- 按 回车
如果我们更喜欢使用键盘快捷方式,也可以使用Ctrl + F6
。
此功能将打开一个与方法提取功能非常相似的对话框。因此,我们拥有与提取方法相同的可能性:更改其名称、可见性以及添加/删除参数并对其进行微调。
假设我们想要更改isDateBetween的实现,以将日期范围视为包含或排除日期范围。为了做到这一点,我们想 在我们的方法中添加一个布尔参数:
通过改变方法签名,我们可以添加这个参数,给它命名并给它一个默认值:
public static boolean isDateBetween(LocalDate date, LocalDate startingDate,
LocalDate endingDate, boolean inclusive) {
return date.isBefore(endingDate) && date.isAfter(startingDate);
}
之后,我们只需根据需要调整方法体即可。
如果我们愿意,我们可以通过重载方法选项检查 委托,以便使用参数创建另一个方法而不是修改当前方法。
7. 上拉和下压
我们的 Java 代码通常具有类层次结构——派生类扩展基类。
有时我们想在这些类之间移动成员(方法、字段和常量)。这就是最后一个重构派上用场的地方:它允许我们将成员从派生类中拉到基类中,或者将它们从基类中向下推到每个派生类中。
7.1. 拉起
首先,让我们拉起 一个方法到基类:
- 选择一个派生类的成员上拉
- 右键单击该成员
- *触发*Refactor > Pull Members Up…选项
- 按下重构按钮
默认情况下,此功能没有可用的键盘快捷键。
假设我们有一个名为Derived 的派生类。它使用私有的doubleValue() 方法:
public class Derived extends Base {
public static void main(String[] args) {
Derived subject = new Derived();
System.out.println("Doubling 21. Result: " + subject.doubleValue(21));
}
private int doubleValue(int number) {
return number + number;
}
}
基类Base为空。
那么,当我们将doubleValue()拉入 Base时会发生什么?
当我们在上面的对话框中按下“Refactor”时,*doubleValue()*会发生两件事:
- 它移动到基类
- 它的可见性从private变为受protected,以便派生类仍然可以使用它
之后的基类现在有方法:
public class Base {
protected int doubleValue(int number) {
return number + number;
}
}
拉起成员的对话框(如上图)为我们提供了更多选项:
- 我们可以选择其他成员并一次将它们拉起来
- 我们可以使用“预览”按钮预览我们的更改
- 只有方法在“Make abstract”列中有一个复选框。如果选中,此选项将在上拉期间为基类提供抽象方法定义。实际方法将保留在派生类中,但会获得一个*@Override*注解。**因此,其他派生类将不再编译,**因为它们缺少新的抽象基方法的实现
7.2. 向下推
最后,让我们将一个成员下推 到派生类。这与我们刚刚执行的上拉相反:
- 选择要下推的基类成员
- 右键单击该成员
- *触发*Refactor > Push Members Down…选项
- 按下重构按钮
与拉动成员一样,默认情况下没有键盘快捷键可用于此功能。
我们把刚才拉上来的方法再下推一下。 上一节末尾的Base类如下所示:
public class Base {
protected int doubleValue(int number) {
return number + number;
}
}
现在,让我们将doubleValue()下推到Derived类:
这是 在上面的对话框中按下“重构”后的派生类。*doubleValue()*方法回来了:
public class Derived extends Base {
private int theField = 5;
public static void main(String[] args) {
Derived subject = new Derived();
System.out.println( "Doubling 21. Result: " + subject.doubleValue(21));
}
protected int doubleValue(int number) {
return number + number;
}
}
现在Base类和 Derived 类都回到了它们在之前的“Pull Up”部分中开始的位置。差不多,也就是说——doubleValue()保留了 它在Base中的protected可见性(它最初是private)。
IntelliJ 2019.3.4在按下doubleValue()时实际上会发出警告:“从某些调用站点看不到推送的成员”。但是正如我们在上面的Derived类中看到的那样,*doubleValue()确实对main()*方法可见。
下推成员的对话框(如上图)也为我们提供了更多选项:
- 如果我们有多个派生类,那么 IntelliJ 会将成员推送到每个派生类中
- 我们可以推下多个成员
- 我们可以使用“预览”按钮预览我们的更改
- only methods在“Keep abstract”栏中有一个复选框——这类似于拉起成员:如果选中,该选项将在基类中保留一个抽象方法。与拉起成员不同,此选项会将方法实现放入所有派生类中。这些方法还将获得 @Override注解