Lecture 4 Java程序的Bug分析与调试

  1. 多种输入指令的处理:

    一个自然而然地想法是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import java.util.Scanner

    public class MainClass(){
    public static void main(String[] args) {
    Scanner scanner = new scanner(System.in);
    int n = scanner.nextInt(); //表示接下来有n条指令
    for (int i = 0; i < n; i++) {
    int instruction = scanner.nextInt();
    switch (instruction) {
    case 1 :
    command1();
    break;
    case 2 :
    command2();
    break;
    /*...*/
    default :
    break;
    }
    }
    }
    }
  2. Java虚拟机的内存划分:

    • 栈(Stack):中存放方法中的局部变量,方法的运行一定要在栈中进行,栈内存的数据用完就释放
    • 堆(Heap):是 Java 虚拟机中最大的一块内存区域,用于存储对象实例。通俗的理解,就是我们 new出来的 所有对象和数组,都是存放在堆中的
    • 元空间(MetaSpace):虚拟机加载的类信息、常量、各类方法的信息
    • 本地方法栈
    • 程序计数器
  3. 深克隆和浅克隆

    • 数据传递规则:基础数据类型为值传递,引用数据类型为引用传递。 基础数据类型(int、boolean)在作为参数传递时,传递的是真实的数据,形参的改变,不影响实参。 引用数据类型(类、数组、接口)作为参数传递时,传递的是堆内存中的地址(类似于C语言中的指针),形参改变,实参也改变。

    • 浅克隆:设我们有一个Dog类型的dog1对象,此时我们希望new一个dog2对象,它与dog1具有相同特征,我们写出如下代码:

      1
      2
      Dog dog1 = new Dog (/*构造参数*/);
      Dog dog2 = dog1;
    • 事实上,我们通过第一行语句创建了一个Dog实例,使用dog1来引用这个实例(类似于一颗Dog型的指针dog1来指向该Dog实例在堆中的地址),之后声明的dog2变量只是在重新引用这个实例(dog2克隆的是dog1这个引用本身),在整个运行过程中仅有一个实例。像这样只克隆引用的克隆过程被称为浅克隆

    • 深克隆:从上述浅克隆的示例中我们可以看出如果希望创造出一个真正的克隆,我们不仅需要定义一个新引用,还需要创建一个新的实例,即在堆中申请一块新的内存

      1
      2
      Dog dog1 = new Dog (/*参数1 , 参数2*/);
      Dog dog2 = new Dog (/*dog1.get参数1() , dog2.get参数2()*/);

      像这样创造了一个新的实例并将新实例的所有属性都设置为原实例属性的克隆称为深克隆

    • 对容器的克隆:由于特定实例可以被不同的容器管理,所以像ArrayListHashMap这样的容器管理的显然不能是实例本身,而是实例的引用(或者说实例的地址)。因此,如果想对容器进行深克隆,一定要遍历容器中的所有对象,逐个深克隆;

  4. “==”和“equals”区别:

    • Strings类的“==”和“equals”区别:“==”用于判断二者是否是同一实例(或者说二者地址是否相同),“equals”用于判断二者内容是否相同。事实上,字符串对象,假设不通过 new 方法创造,它是被放在一个叫做“字符串常量池”的地方,和我们创建对象所用的堆区并不是在一起的,例如:

      1
      2
      3
      String s1 = "OOpre";
      String s2 = "OOpre";
      String s3 = s1;

      在上述例子中,变量名s1s3指向的显然是同一个实例,即内存中同一块地址。我们的问题是为什么s2指向的也是这块地址,这是因为不通过new方法创建的字符串对象被储存在字符串常量池中,因而当Java虚拟机在处理第二行指令时,它会发现字符串常量池中已有该对象,于是它直接令s2指向该对象。

    • 在Java中,判断Map的key是否相等,应该使用equals()方法而不是==操作符。Map接口的实现,特别是像HashMap这样的基于哈希表的实现,需要能够准确地判断两个键是否等价(即内容相同)

  5. 迭代器删除:

    我们首先讨论一下for的增强循环:

    1
    2
    3
    4
    5
    6
    for (Adventurer adventurer : adventurer)
    //它的本质是利用迭代器实现遍历
    Iterator <Adventurer> iterator = arrayList.iterator;
    while (iterator.hasNext()) {
    Adventurer adventurer = iterator.next();
    }

    这里会出现一个很经典的错误,即一边遍历一边删除:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ArrayList <Adventurer> advList = new ArrayList<>();
    //假设该容器里有了一些对象
    for (Adventurer adventurer : advList) {
    advList.remove(adventurer);
    }
    //用迭代器翻译过来就是
    Iterator <Adventurer> iterator = arrayList.iterator();
    while (iterator.hasNext()){
    Adventurer adventurer = iterator.next();
    arrayList.remove(adventurer);
    }

    用一个浅显但不太精确的解释,这是因为在上述使用的删除中,使用的是 ArrayList,也就是容器本身的删除方法,它确实能修改容器的状态,但是它不能修改迭代器的状态。这导致,迭代器的状态和当前 ArrayList 的状态不相同了,而它在每次 调用.next() 方法的时候会判断这个状态是否相同,导致报错。

    一句话说,不要在for的增强循环里增删改变容器的状态。