第4章 代码重构

在程序开发中经常需要修改代码,例如修改变量名。如果一个变量在不同文件的不同地方被使用,那么每个地方都需要修改。进行手工修改很容易发生错误,NetBeans提供了代码重构的功能,可以帮助开发人员迅速地修改代码而不发生错误。

所谓代码重构,是指使用较小的转换来重新构造代码,而不会改变任何程序行为。NetBeans集成开发环境的重构功能通过完成以下操作来简化代码重构的过程:确定要进行的更改,预览应用程序中受影响的部分(此时用户可以选择更改哪些内容),然后对代码进行必要的更改。例如,如果使用“重命名”操作来更改类名,集成开发环境将在代码中查找该名称的所有使用实例,并更改每个出现的名称。本章将主要介绍几种常用的代码重构方法,包括重命名、提取方法、更改参数、封装字段、移动类等。

本章主要内容

● 重命名类、接口、字段和方法

● 提取方法

● 更改参数

● 提取接口和超类

本章亮点

● 重命名类、接口、字段和方法

● 移动类以及类成员

● 封装字段

4.1 重命名

在开发中,如果需要对变量、方法等的名称进行修改,可以使用重命名操作。重命名操作包括重命名变量、重命名字段、重命名方法、重命名类或者接口、重命名包、重命名文件、重命名项目等。

4.1.1 重命名项目

要重命名一个项目,可以在“项目”窗口中右键单击项目节点,然后选择弹出菜单中的“重命名项目”命令,打开“重命名项目”对话框,如图4-1所示。

图4-1 “重命名项目”对话框

在“项目名称”文本框中输入项目的新名字。如果要同时更改项目所在文件夹的名字,则应该选中“同时重命名项目文件夹”复选框。单击“重命名”按钮,就会把选中的项目修改为指定的名字。

4.1.2 重命名其他元素

重命名变量一般是在局部进行,因此也可以使用查找替换功能实现。一个类的某个字段或者变量有可能在不同的文件中被引用,因此如果使用查找替换功能,可能会产生错误,也比较麻烦。这时使用重命名操作会比较安全和快捷。

其他元素的重命名操作比较类似,因此这里只以重命名字段或者方法为例进行介绍。对字段或者方法进行重命名操作时,首先要在源代码编辑器中选中要更改的内容,如图4-2所示。然后通过下面几种方式打开重命名对话框,如图4-3所示:

图4-2 在源代码编辑器中选中要更改的内容

图4-3 重命名对话框

● 选中“重构”→“重命名”命令;

● 在源代码编辑器中单击右键,选中弹出上下文菜单中的“重构”→“重命名”命令。

重命名对话框的标题可能会因为修改内容的不同而有所变化。这里把方法的名字由name修改为bean_name。如果要同时修改注释中的名字,则要选中“对注释应用重命名”复选框。

对于一些复杂的项目,这里强烈建议开发人员先预览重命名的结果,因此单击“预览”按钮,打开“重构”窗口的物理视图,显示预览结果,如图4-4所示。

图4-4 “重构”窗口的物理视图

图4-5为NetBeans 5.5中“重构”窗口的物理视图,通过比较可以看到,在NetBeans 6.0中,“重构”窗口的功能有所增强,提供了在实际代码中的修改效果预览。

图4-5 NetBeans 5.5中“重构”窗口的物理视图

在“重构”窗口的物理视图中,给出了树形结构可能进行的重构操作。通过选择或清除相应的复选框,指定要应用重构更改的文件或代码元素。双击某行可以在源代码编辑器中打开它所引用的文件,并定位到相应的行上。

“重构”窗口左边的工具栏中有一些操作的按钮。“刷新重构数据”按钮 可以显示启动重构命令后已经执行的所有更改;“折叠/展开树中的所有节点”按钮 用于折叠或者展开树中的所有节点;“显示逻辑视图”按钮 用于显示如图4-6所示的逻辑视图;“显示物理视图”按钮 用于显示如图4-5所示的物理视图。

图4-6 “重构”窗口的逻辑视图

“上一个实例”按钮 用于定位到上一个重构实例,并在源代码编辑器中打开相应的代码行;“下一个实例”按钮 用于定位到下一个重构实例,并在源代码编辑器中打开相应的代码行。

在完成预览后,单击“重构”按钮后,将应用重构更改;单击“取消”按钮则取消重构操作。

4.2 引入方法

在实际开发中,可以把一些常用的代码写成方法,在需要的时候进行调用。NetBeans支持把方法中的一些代码进行提取,并将这些代码变为一个方法。例如,对于下面的writeProperties方法:

                protected void writeProperties() throws M3gException {
                  super.writeProperties();
                  Object o = this.properites.get("Property ID");
                  if (o != null && o instanceof Long) {
                        this.propertyID = ((Long)o).longValue();
                  }
                }

如果希望把如图4-7所示的代码变为一个方法调用,那么可以执行如下操作:

图4-7 提取方法

① 选中要提取的代码,然后选择“重构”→“引入方法”命令,或者在源代码编辑器中单击右键,选择弹出菜单中的“重构”→“引入方法”命令,打开“引入方法”对话框,如图4-8所示。

图4-8 “提取方法”对话框

② 在“引入方法”对话框的“名称”文本框中输入新引入的方法名。在“访问权限”中可以选择新方法的可访问性。

③ 单击“确定”按钮,执行重构。重构完毕后,代码会变为如下所示,其中writePropertyID就是提取后的方法。

                protected void writeProperties() throws M3gException {
                  super.writeProperties();
                  writePropertyID();
                }
                private void writePropertyID() {
                  Object o = this.properites.get("Property ID");
                  if (o != null && o instanceof Long) {
                        this.propertyID = ((Long)o).longValue();
                  }
                }

4.3 更改方法的参数

在开发过程中,有时候需要修改某个方法的参数。如果进行手工修改,那么就必须把所有涉及这个方法的代码都进行修改。例如在类A中有一个aaa方法,类B继承于类A,并覆盖了aaa方法。类C从类B派生,也覆盖了aaa方法。现在要把类A的aaa方法修改为new_aaa,那么就需要同时修改类B和类C中的aaa方法。当然从语法上讲,如果不修改类B和类C中的aaa方法,也不会出现任何的语法错误。但是,逻辑上类B和类C中的aaa方法就不再和类A中的new_aaa有覆盖关系,从而导致意外的错误。这种错误一旦发生,很不容易找到原因。对于一个大的系统,如果手工修改方法的参数,将很容易发生问题。

NetBeans的重构技术中提供了更改方法参数的功能,使得这个操作变得非常容易。例如要修改某个子类中的write方法,原代码如下所示。

                protected void write(M3gOutputStream mos,
                        java.util.Vector ref_table) throws java.io.IOException {
                  uper.write(mos, ref_table);
                  …
                }

write方法覆盖了父类的write方法。并且在整个类家族中,都实现了这个write方法。现在要为这个方法添加一个参数,则具体的操作步骤介绍如下。

① 在源代码编辑器中选中write方法,然后选择“重构”→“更改方法参数”命令,或者在源代码编辑器中单击右键,选择弹出菜单中的“重构”→“更改方法参数”命令,打开“更改方法参数”对话框,如图4-9所示。在参数列表中,选中某个参数,单击“删除”按钮即可删除该参数。单击“上移”按钮可以把参数的位置提前一个位置;单击“下移”按钮可以把参数的位置置后一个位置。

图4-9 “更改方法参数”对话框

② 这里单击“添加”按钮,然后在“参数”列表中添加如图4-10所示的参数。在“名称”列中输入参数的名字;在“类型”列中输入参数的数据类型;在“值”列中添加参数的默认值。

图4-10 为方法添加一个参数

③ 在“可视性修饰符”下拉列表框中修改方法的可访问性。如果单击“重构”按钮,集成开发环境将跳过预览并自动应用更改。这里单击“预览”按钮,预览所有更改,打开“重构”窗口,如图4-11所示。

图4-11 预览更改方法参数

④ 从图4-11中可以看出,集成开发环境把所有要进行修改的write方法都列了出来。请检查列表,并清除任何不想更改的代码前的复选框(重构时将不操作该行的代码)。单击“执行重构”按钮完成方法参数的添加。

4.4 封装字段

字段封装是重构代码的一种操作,是通过一对存取方法来访问字段。存取方法也称为读/写方法或getter和setter方法。

通常,在封装字段时,会将字段的访问修饰符更改为private,这样就无法从类外部直接引用该字段。如果其他类要引用该字段,则它们必须使用存取方法。下面的MyClass类中,包含一个共有的name属性。

            public class MyClass {
                public String name = "";
                int age;
                public MyClass() {
                }
            }

在源代码编辑器(或者“项目”窗口)中右键单击name字段。然后选择“重构”→“封装字段”命令,或者在源代码编辑器中单击右键,选择弹出菜单中的“重构”→“封装字段”命令,打开“封装字段”对话框,如图4-12所示。

图4-12 “封装字段”对话框

在“要封装的字段的列表”中,包含了类中字段的列表,选定要封装的字段的复选框。这里,要修改name字段,因此把name字段的getter和setter复选框都选中。

“字段的可视性”下拉列表框用于选择字段的可访问性修饰符,一般情况下都选择private修饰符。

“存取方法的可视性”下拉列表框用于选择存取方法(getter和setter)的可访问性修饰符。

如果选中“使用存取方法(即使字段可以存取)”复选框,那么将更新代码中对字段的所有直接引用,以使用存取方法。如果未选定此复选框,则不会替换代码中已包含的对字段的任何直接引用。

单击“预览”按钮,打开“重构”窗口,如图4-13所示。

图4-13 预览封装字段

单击“执行重构”按钮完成字段的封装。封装name字段后的MyClass类代码如下所示。

            public class MyClass {
                private String name = "";
                int age;
                public MyClass() {
                }
                public String getName() {
                  return name;
                }
                public void setName(String name) {
                  this.name = name;
                }
            }

注意:封装字段只能对任何非静态字段进行操作。

4.5 移动类

NetBeans支持两种移动类的方法:第一种为复制/粘贴法;第二种是使用重构的移动类命令。下面分别进行介绍。

4.5.1 使用复制/粘贴法移动类

在“项目”窗口中选中要移动的类,然后右键单击该类,选择弹出菜单中的“剪切”命令(如果要复制该类,则应该选择“复制”命令)。

在要被复制到的项目包上单击右键(注意,一定是某个目标包上),选择弹出菜单中的“移动”命令,则直接会把类移动到目标包中。如果选择弹出菜单中的“重构移动”命令,会打开“移动类”对话框,如图4-14所示。

图4-14 复制/粘贴类

如果选中“移动而不重构”复选框,那么将不执行重构操作,直接把类移动到目标项目的目标包中。单击“重构”按钮即可完成移动类的操作。

4.5.2 重构的移动类命令

重构的移动类命令可以将类移动到其他包并更改引用该类的代码。在“项目”窗口或“源代码编辑器”窗口中选中要移动的类,然后选择“重构”→“移动”命令,或者在源代码编辑器”窗口中单击右键,选择弹出菜单中的“重构”→“移动”命令,打开“移动类”对话框,如图4-15所示。

图4-15 “移动类”对话框

在“项目”下拉列表框中,可以选择要将类移动到的项目名称。在“位置”下拉列表框中可以选择要将类移动到目标项目的哪个部分,此处的正确值通常为“源包”,但也可以在此字段中表示其他源根目录。“目标包”下拉列表框用于选择要将类移动到的包。当要移动多个类时(执行移动类命令前选中了多个类),则会显示如图4-16所示的对话框。

图4-16 移动多个类时的“移动类”对话框

在“类的列表”中显示了要移动的类的名称。如果只移动一个类,则不会显示此列表。单击“预览”按钮显示如图4-17所示的“重构”窗口。

图4-17 预览类的移动

在“重构”窗口中,确认重构的操作没有问题后,单击“执行重构”按钮,即可完成类的移动操作。

4.6 从内层移至外层

重构的“从内层移至外层”命令,可以将内部类在分层结构中上移一级。如果选定的类直接嵌套在顶层类中,则会创建一个新的顶层类。如果选定的类嵌套在内部类中,则将选定类移动到嵌套了该类的内部类的层中。例如,下面的代码中MySubClass类为MyClass的内部类。

            public class MyClass {
                public String name = "";
                class MySubClass {
                }
            }

在源代码编辑器中选中MySubClass类,然后选择上下文菜单中的“重构”→“从内层移至外层”命令,打开“从内层移至外层”对话框,如图4-18所示。

图4-18 “从内层移至外层”对话框

如有必要可以在“类名”文本框中更改类的名字。如果要为当前外部类生成实例字段并将外部类传递给构造方法,请选择“为当前外部类声明字段”复选框。在选中此复选框后,还要在“字段名称”文本框中输入外部类实例字段的名称。单击“预览”按钮,打开“重构”窗口,如图4-19所示。

图4-19 预览把类从内层移至外层

在“重构”窗口中,确认操作无误后,单击“执行重构”命令。因为MySubClass类包含在顶层类中,因此这样将创建一个MySubClass.java文件,里面包含MySubClass类:

                class MySubClass {
                }

如果在如图4-18所示的“从内层移至外层”对话框中选中“为当前外部类声明字段”复选框,并在“字段名称”文本框中输入“test”,那么创建的MySubClass类代码如下所示:

            class MySubClass {
                MyClass test;
                MySubClass(MyClass test) {
                  super();
                  this.test = test;
                }
            }

4.7 将匿名类转换为外部类

重构的将匿名类转换为外部类命令,可以把一个匿名类提取出来,并保存为一个外部类。在使用此操作时,将创建一个新的内部类,并将匿名内部类替换为对新内部类的调用。例如在如下的方法中,包含了一个MouseAdapter的匿名类。

            import java.awt.event.MouseAdapter;
            import java.awt.event.MouseEvent;
            import javax.swing.JPanel;
            public class MyClass extends JPanel {
                void addListener() {
                  addMouseListener(new MouseAdapter() {
                        public void mouseClicked(MouseEvent e) {
                            System.out.println(e.getX());
                        }
                  });
                }
            }

在源代码编辑器中,右键单击MouseAdapter类,然后选择弹出菜单中的“重构”→“将匿名类转换为外部类”命令,NetBeans集成开发环境会自动把MouseAdapter改变为一个内部类,代码如下所示:

            import java.awt.event.MouseAdapter;
            import java.awt.event.MouseEvent;
            import javax.swing.JPanel;
            public class MyClass extends JPanel {
                void addListener() {
                  addMouseListener(new MouseAdapterImpl());
                }
                private static class MouseAdapterImpl extends MouseAdapter {
                  public MouseAdapterImpl() {
                  }
                  public void mouseClicked(MouseEvent e) {
                        System.out.println(e.getX());
                  }
                }
            }

4.8 提取接口

重构的提取接口命令可以通过某个类或接口中的选定非静态公共方法来创建新接口。例如,下面的类中包含一个构造函数和三个公有的方法:getName、setName和getTransparency。其中,getTransparency是java.awt.Transparency接口中定义的方法。

            public class MyClass implements java.awt.Transparency {
                private String name = "";
                public MyClass() {
                }
                public String getName() {
                  return name;
                }
                public void setName(String name) {
                  this.name = name;
                }
                public int getTransparency() {
                  return 0;
                }
            }

在源代码编辑器的文件中单击鼠标右键,然后选择“重构”→“提取接口”命令,打开“提取接口”对话框,如图4-20所示。

图4-20 “提取接口”对话框

在“接口名称”文本框中输入接口的名称,这里输入MyNewInterface。在“要提取的成员”列表中,选择要提取到新接口中的成员。

如果从中提取接口的类已实现某个接口,这里实现了Transparency接口。在“要提取的成员”列表中,还会包含实现该接口的项。如果选中implements java.awt.Transparency复选框,则新提取的接口会继承这个接口。

单击“预览”按钮,打开“重构”窗口,如图4-21所示。

图4-21 预览提取接口

在“重构”窗口中,确认操作无误后,单击“执行重构”命令完成接口的提取。集成开发环境将创建一个新的文件MyNewInterface.java,里面包含MyNewInterface接口:

            import java.awt.Transparency;
            public interface MyNewInterface extends Transparency {
                String getName();
                int getTransparency();
                void setName(String name);
            }

MyClass类则变为如下形式的代码,其实现接口由Transparency变为MyNewInterface。

            public class MyClass implements MyNewInterface {
                private String name = "";
                public MyClass() {
                }
                public String getName() {
                  return name;
                }
                public void setName(String name) {
                  this.name = name;
                }
                public int getTransparency() {
                  return 0;
                }
            }

在如图4-20所示的“提取接口”对话框中,如果不选中implements java.awt.Transparency复选框,那么最后生成的MyNewInterface接口代码如下所示:

            public interface NewInterface {
                String getName();
                int getTransparency();
                void setName(String name);
            }

MyClass类的代码如下所示:

            public class MyClass implements java.awt.Transparency, NewInterface {
                private String name = "";
                public MyClass() {
                }
                public String getName() {
                  return name;
                }
                public void setName(String name) {
                  this.name = name;
                }
                public int getTransparency() {
                  return 0;
                }
            }

4.9 小结

本章主要介绍如何在NetBeans集成开发环境中使用重构命令完成代码的快速修改。本章介绍的内容不是编程必需的,但是学习掌握后可以大大提高编程的效率。对于有一定编程基础的读者,这部分应该作为提高部分好好掌握。通过本章的学习,读者应该掌握以下内容:

● 如何重命名项目、字段、方法等

● 如何利用提取方法命令,把一部分代码提取为方法

● 如何快速安全地修改方法的参数

● 如何对字段进行封装,生成字段的读/写方法

● 如何把一个类移动到另外的项目或者包中

● 如何把类由内部移动到上一层

● 如何把匿名类提取为一个内部类

● 如何从一个类中提取接口