1.抛出异常

本节课我们使用上一节创建的ArrayMap进行讲解,假设你在main()中get()一个并不存在的key,如:

public static void main(String[] args) {
   ArrayMap<String, Integer> am = new ArrayMap<String, Integer>();
   am.put("hello", 5);
   System.out.println(am.get("yolp"));
}

那么当你运行时,会得到如下报错信息:

$ java ExceptionDemo
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1
    at ArrayMap.get(ArrayMap.java:38)
    at ExceptionDemo.main(ExceptionDemo.java:6)

依据报错信息我们可以得知在ArrayMap.java代码第38行与ExceptiomDemo.java第6行出错,出错原因是数组越界。
当遇到错误时,Java能够自动抛出异常并停止程序的运行。有时候,Java给出的报错信息并不明显,不容易得出错误,我们可以手动添加报错信息,也就是抛出一些异常。
使用关键字 throw 我们可以抛出自定义的异常,具体使用格式为:

throw new ExceptionObject(parameter1, ...)

举例:

public V get(K key) {
   int location = keyIndex(key);
   if (location < 0) { throw new IllegalArgumentException("Key " + 
                            key + " does not exist in map.");  }
   return values[keyIndex(key)];
}

一些容易出现异常的例子:

  • You try to use 383,124 gigabytes of memory.
  • You try to cast an Object as a Dog, but dynamic type is not Dog.
  • You try to call a method using a reference variable that is equal to null.
  • You try to access index -1 of an array.
    抛出异常其实是创建一个异常类型的Object,也就是相当于实例化一个异常类,所以即使程序本身并无错误,你也可以无缘无故地抛出一个异常:
public static void main(String[] args) {
    throw new RuntimeException("For no reason.");
}

得到:

Exception in thread "main" java.lang.RuntimeException: For no reason.

2.捕获异常

假如我们仅仅只是在程序的某处抛出异常而不做任何处理,程序便会崩溃。但是我们可以通过捕获异常从而让程序继续运行下去,使用

try {
   throw new SomeException();
} catch (Exception e) {
   doSomething;
}

例如:

        try {
            throw new RuntimeException("for no reason");
        } catch (Exception e) {
            System.out.println(e);
        }
        System.out.println("actually it's still running");

可以看到即使抛出RuntimeException,程序并没有崩溃:

java.lang.RuntimeException: for no reason
actually it's still running

除了打印出异常信息 Excepetion e 之外,也可以在 catch 里面纠正程序错误

优化语法

image.png
假设我们写了一个读入文件的函数 readFile 那么需要考虑:

  • 文件是否存在
  • 内存是否足以读入全部字节
  • 读入失败怎么样

那么按照常规的使用 if 判断这些特殊情况:

func readFile: {
    open the file;
    if (theFileIsOpen) {
        determine its size;
        if (gotTheFileLength) {
            allocate that much memory;
         } else {
                 return error("fileLengthError");
         }
          if (gotEnoughMemory) {
           read the file into memory;
               if (readFailed) {
                return error("readError");
           }
              ...
        } else {
             return error("memoryError");
        }
     } else {
       return error("fileOpenError")
    } 
}

可见当 if else 很多时影响可读性,此时可以使用 try catch 进行优化语法:

func readFile: {
    try {
           open the file;
           determine its size;
           allocate that much memory;
           read the file into memory;
           close the file;
    } catch (fileOpenFailed) {
          doSomething;
    } catch (sizeDeterminationFailed) {
           doSomething;
    } catch (memoryAllocationFailed) {
       doSomething;
    } catch (readFailed) {
       doSomething;
    } catch (fileCloseFailed) {
           doSomething;
    }
}

Exceptions and the Call Stack

假设某main()中方法调用是

GuitarHeroLite.main()-->GuitarString.sample()-->ArrayRingBuffer.peek()
image.png
假设peek()方法执行的时候抛出了异常,那么异常就会从栈顶向下追踪(类似于逐步pop栈元素),寻找其他方法里是否会有 catch 异常,如果到达栈底都没找到,程序就会崩溃并且Java会打印出栈的追踪信息
image.png

java.lang.RuntimeException in thread “main”: 
    at ArrayRingBuffer.peek:63 
    at GuitarString.sample:48 
    at GuitarHeroLite.java:110

Checked Exceptions

我们平时写的代码大多都没有使用 try catch 捕获异常(unchecked exceptions),但是有时候,如果你不进行异常检查,编译器会给出"Must be Caught or Declared to be Thrown" 这样的报错信息
基本的思想是
编译器需要这些异常被catch 或被确定,其被编译器认为是可避免的程序崩溃

比如,当我们 throw new IOException()的话:

public class Eagle {
    public static void gulgate() {
       if (today == “Thursday”) { 
          throw new IOException("hi"); }
    }
}
public static void main(String[] args) {
    Eagle.gulgate();
}

以上代码便会报错:

$ javac What.java
What.java:2: error: unreported exception IOException; must be caught or declared to be thrown
        Eagle.gulgate();

但是简单改一下,当我们 throw new RuntimeException():

public class UncheckedExceptionDemo {
    public static void main(String[] args) {
       if (today == “Thursday”) { 
          throw new RuntimeException("as a joke"); }
    }
}

那么报错就会消失,因为 RuntimeException()是无需检查的异常,而 IOException()是需检查的异常,一些具体的异常区分如图:
image.png
对比RuntimeException()与IOException()
image.png
关于 RuntimeException(),再具体介绍一下:
image.png
对于这些需要检查的异常,我们的办法是

  1. Catch Exception
    使用 try catch 语法捕获异常:
public static void gulgate() {
    try {
        if (today == “Thursday”) { 
          throw new IOException("hi"); }
    } catch (Exception e) {
        System.out.println("psych!");
    }
}
  1. 以关键字 throws 在method的header末尾声明异常的:
public static void gulgate() throws IOException {
   ... throw new IOException("hi"); ...
}

相当于告诉编译器 I'm dangerous method
如果一个method 调用一个 dangerous method,那么需要小心该方法本身也会变成 dangerous方法,正如

“He who fights with monsters should look to it that he himself does not become a monster. And when you gaze long into an abyss the abyss also gazes into you.” - Beyond Good and Evil (Nietzsche)

当我们在main()中调用 dangerous method,main()本身也变成了 dangerous method,需要对main()进行修正:

public static void main(String[] args) {
    Eagle.gulgate();
}

image.png


3.Iteration

以前我们有在List中使用过 加强循环去迭代其中的元素( called the “foreach” or “enhanced for” loop):

List<Integer> friends =
  new ArrayList<Integer>();
friends.add(5);
friends.add(23);
friends.add(42);
for (int x : friends) {
   System.out.println(x);
}

本小节的目标是构建一个属于我们自己的加强循环( enhance loop )
先介绍一下Java内置的List interface iterator() method:

public Iterator<E> iterator();

因为iterator()的返回值是Iterator<E>类型,所以我们定义一个Iterator<Integer>类型的seer去接收:

List<Integer> friends =
  new ArrayList<Integer>();
...
Iterator<Integer> seer
     = friends.iterator();

while (seer.hasNext()) {
  System.out.println(seer.next());
}

可见iterator接口包含两个方法:

  • hasNext():检查是否存在下一项
  • next():返回当前项的值并将next指针后移一位

The Secret of the Enhanced For Loop

image.png
如上图所示,左边的代码是增强循环,其原理等效于右边的迭代器版本,也即是说增强循环的源码即等同于图中右边的代码

  1. 首先编译器检查 Lists 是否有 iterator()方法并返回 Iterator<Integer>.
    How:
    List接口 extends Iterable 接口,继承了 Iterable接口的抽象方法 iterator()
    (实际上,List extends Collection,而Collection extends Iterable,但这已经足够接近事实了。另外我还省略了Iterable接口中的一些默认方法)
    image.png
  2. 接着,编译器检查 Iterator 是否有hasNext() 和 next()

    How: Iterator接口明确定义了这些抽象的方法
    image.png

因此,为了实现增强循环,即需要迭代器满足上述两个步骤(这里是fall2020的slides):
image.png

构建我们自己的KeyIterator()

在实现我们自己的增强循环之前,让我们先简单地构建属于我们自己的迭代器KeyIterator
在ArrayMap()中定义内部类 KeyIterator,并声明hasNext()和next():

    public class KeyIterator {

        public boolean hasNext() {
            return false;
        }

        public K next() {
            return null
        }
    }

接下来按照上面所说的 hasNext()与next()的功能完成KeyIterator:

    public class KeyIterator {

        private int Position;

        public KeyIterator() {
            Position = 0;
        }

        public boolean hasNext() {
            return Position < size;
        }

        public K next() {
            K value = keys[Position];
            Position = Position + 1;
            return value;
        }
    }

在IteratorDemo.java中测试:

public class IterationDemo {
    public static void main(String[] args) {
        ArrayMap<String, Integer> am = new ArrayMap<String, Integer>();

        am.put("hello", 5);
        am.put("syrups", 10);
        am.put("kingdom", 10);

        ArrayMap.KeyIterator ami = am.new KeyIterator();

        while(ami.hasNext()) {
            System.out.println(ami.next());
        }
    }
}

打印结果:

hello
syrups
kingdom

至此完成了我们的KeyIterator
值得注意的是

ArrayMap.KeyIterator ami = am.new KeyIterator();

以上是演示如何使用嵌套类
如果我们要创造一个非静态的嵌套类,必须有一个特定的实例,如果我们创造的KeyIterator没有与ArrayMap相关联,那将没有意义,本质上KeyIterator的职能是迭代访问ArrayMap的keys[]数组。
接下来准备实现增强循环

        ArrayMap<String, Integer> am = new ArrayMap<String, Integer>();
        for (String s : am) {
            System.out.println(s);
        }
    

因为我们刚才说了,增强循环的源码即image.png

按照两个步骤:

  1. 需要将KeyIterator class封装成一个Iterator方法,该方法的返回类型是Iterator<K>
    public Iterator<K> iterator() {
        return new KeyIterator();
    }
  1. Iterator()方法返回的Iterator<K>类型的Object应该拥有hasNext()和next(),所以需要声明我们的KeyIterator 继承了 Iterator<K> interface,建立"is-a"关系,即KeyIterator是Iterator,且具备hasNext()和next()方法
    public class KeyIterator implements Iterator<K> {
            ......
       }

然而,程序仍然不能运行,
因为Java并不知道我们的ArrayMap class里面有iterator()方法,你可能会疑惑,这么大的方法不就明摆着在那里吗?怎么会不知道。
要知道,这是我们人看见的,编译器不是人(= =)
为了让Java知道我们的ArrayMap里面实现了iterator()方法,让ArrayMap 继承 iterable<T> 接口,建立"is-a"关系,即ArrayMap是iterable,承诺ArrayMap实现了iterable里面的iterator方法

public class ArrayMap<K, V>implements Map61B<K, V>, Iterable<K> {
   ....... }

至此实现了用我们自己写的迭代器KeyIterator使用加强循环
总结:

  1. Iterable 接口拥有Iterator()方法 返回一个 Iterator<T> Object
  2. Iterator<T> 接口 拥有 hasnext()和next()方法,也即实例化的Iterator<T> Object也拥有这两个方法

To support the enhanced for loop:

  • Add an iterator() method to your class that returns an Iterator<T>.
  • The Iterator<T> returned should have a useful hasNext() and next() method.
  • Add implements Iterable<T> to the line defining your class.

当然,你也可以使用List内置的Iterator:

    public Iterator<K> iterator() {
        List<K> keylist = keys();
        return keylist.iterator();
    }

以上三行代码等效于使用内部类实现iterator:

    public Iterator<K> iterator() {
        return new KeyIterator();
    }

    public class KeyIterator implements Iterator<K> {

        private int Position;

        public KeyIterator() {
            Position = 0;
        }

        public boolean hasNext() {
            return Position < size;
        }

        public K next() {
            K value = keys[Position];
            Position = Position + 1;
            return value;
        }
    }
    

Fallenpetals
4 声望9 粉丝