Contents

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可以使用这些选项,然后将出现一个弹出窗口:

/uploads/intellij_refactoring/1.png

在注释和字符串中搜索选项可用于任何重命名。至于 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 + VCtrl + Alt + PCtrl + Alt + FCtrl + Alt + C

IntelliJ 将根据表达式返回的内容尝试为我们提取的表达式猜测一个名称。如果它不符合我们的需求,我们可以在确认提取之前随意更改它。

让我们用一个例子来说明。我们可以想象在我们的SimpleClass类中添加一个方法来告诉我们当前日期是否在两个给定日期之间:

public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
    return LocalDate.now().isAfter(startingDate) && LocalDate.now().isBefore(endingDate);
}

假设我们想要更改我们的实现,因为我们使用*LocalDate.now()*两次,我们想确保它在两次评估中具有完全相同的值。现在让我们选择表达式并将其提取到局部变量中:

/uploads/intellij_refactoring/3.png

然后,我们的*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 已经找到了三个需要的参数:startingDateendingDatenow。由于我们希望此方法尽可能通用,因此我们将now参数重命名为date。出于凝聚力的目的,我们将其作为第一个参数。

最后,我们给我们的方法命名,isDateBetween,并完成提取过程:

/uploads/intellij_refactoring/5.png

然后我们将获得以下代码:

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类:

/uploads/intellij_refactoring/7.png

触发该功能将产生以下代码:

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用在 SimpleClassmain方法中:

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变量,它现在看起来有点矫枉过正:

/uploads/intellij_refactoring/9.png

通过内联这个变量,我们将获得以下结果:

public static boolean isNowBetween(LocalDate startingDate, LocalDate endingDate) {
    return isDateBetween(LocalDate.now(), startingDate, endingDate);
}

在我们的例子中,唯一的选择是删除所有引用并删除元素。但是假设我们也想摆脱 isDateBetween调用并选择内联它。然后,IntelliJ 会为我们提供我们之前谈到的三种可能性:

/uploads/intellij_refactoring/11.png

选择第一个将用方法主体替换所有调用并删除该方法。至于第二个,它会将所有调用替换为方法体,但保留方法。最后,最后一个只会用方法体替换当前调用

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类中。所以我们决定移动它:

/uploads/intellij_refactoring/13.png

我们的方法现在位于DateUtils类中。我们可以看到方法中对DateUtils的引用 已经消失,因为它不再需要了:

public static boolean isDateOutside(LocalDate date, LocalDate startingDate, LocalDate endingDate) {
    return !isDateBetween(date, startingDate, endingDate);
}

我们刚刚做的例子工作得很好,因为它涉及一个静态方法。然而,在实例方法的情况下,事情并不是那么简单。

如果我们想要移动实例方法IntelliJ 将搜索在当前类的字段中引用的类,并提供将方法移动到其中一个类的方法(前提是它们是我们要修改的)。

如果字段中没有引用可修改的类,则 IntelliJ 建议在移动方法之前将其设为static

6. 更改方法签名

最后,我们将讨论允许我们更改方法签名 的功能。此功能的目的是操纵方法签名的各个方面。

像往常一样,我们必须通过几个步骤来触发该功能:

  • 选择要更改的方法
  • 右键单击该方法
  • 触发重构 > 更改签名选项
  • 对方法签名进行更改
  • 按 回车

如果我们更喜欢使用键盘快捷方式,也可以使用Ctrl + F6

此功能将打开一个与方法提取功能非常相似的对话框。因此,我们拥有与提取方法相同的可能性:更改其名称、可见性以及添加/删除参数并对其进行微调。

假设我们想要更改isDateBetween的实现,以将日期范围视为包含或排除日期范围。为了做到这一点,我们想 在我们的方法中添加一个布尔参数:

/uploads/intellij_refactoring/15.png

通过改变方法签名,我们可以添加这个参数,给它命名并给它一个默认值:

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会发生什么?

/uploads/intellij_refactoring/17.png

当我们在上面的对话框中按下“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类:

/uploads/intellij_refactoring/19.png

这是  在上面的对话框中按下“重构”后的派生类。*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注解