cs61b week5 -- Exceptions, Iterators, Iterables

Posted by Dave96 on Fri, 24 Dec 2021 16:30:41 +0100

1. Throw an exception

In this lesson, we will use the ArrayMap created in the previous section to explain. Suppose you get() a nonexistent key in main(), such as:

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

Then when you run, you will get the following error message:

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

According to the error information, we can know that in arraymap Line 38 of the Java code is the same as exceptiomdemo Error in line 6 of Java. The reason for the error is that the array is out of bounds.
When an error is encountered, Java can automatically throw an exception and stop the program. Sometimes, the error message given by Java is not obvious and it is not easy to get an error. We can manually add the error message, that is, throw some exceptions.
Using the keyword throw, we can throw custom exceptions:

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)];
}

Some examples of exceptions:

  • 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.
    Throwing an exception is actually creating an Object of exception type, which is equivalent to instantiating an exception class. Therefore, even if the program itself has no error, you can throw an exception for no reason:
public static void main(String[] args) {
    throw new RuntimeException("For no reason.");
}

Get:

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

2. Catch exception

If we just throw an exception somewhere in the program without doing anything, the program will crash. However, we can catch exceptions so that the program can continue to run

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

For example:

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

You can see that even if RuntimeException is thrown, the program does not crash:

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

In addition to printing the exception message exception e, you can also correct program errors in catch

Optimize syntax


Suppose we write a function readFile to read files, then we need to consider:

  • Does the file exist
  • Is there enough memory to read all bytes
  • How about reading failure

Then judge these special cases according to the conventional use of 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")
    } 
}

It can be seen that many if else affect readability. At this time, try catch can be used to optimize the syntax:

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

Suppose a method call in main() is

GuitarHeroLite.main()-->GuitarString.sample()-->ArrayRingBuffer.peek()

Assuming that an exception is thrown when the peek() method is executed, the exception will be tracked down from the top of the stack (similar to step-by-step pop stack elements) to find out whether there will be catch exceptions in other methods. If it is not found at the bottom of the stack, the program will crash and Java will print the tracking information of the stack

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

Checked Exceptions

Most of the code we usually write doesn't use try catch to catch unchecked exceptions, but sometimes, if you don't check the exceptions, the compiler will give an error message such as "Must be Caught or Declared to be Thrown"
The basic idea is
The compiler needs these exceptions to be caught or identified, which is considered by the compiler as an avoidable program crash

For example, when we 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();
}

The above code will report an error:

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

But simply change it. When we throw new RuntimeException():

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

The error will disappear because RuntimeException() is an exception that does not need to be checked, while IOException() is an exception that needs to be checked. Some specific exception differences are shown in the figure:

Compare RuntimeException() with IOException()

About RuntimeException(), let's introduce it in detail:

For these exceptions that need to be checked, our method is

  1. Catch Exception
    Catch exceptions using try catch syntax:
public static void gulgate() {
    try {
        if (today == "Thursday") { 
          throw new IOException("hi"); }
    } catch (Exception e) {
        System.out.println("psych!");
    }
}
  1. Declare the exception at the end of the header of the method with the keyword throws:
public static void gulgate() throws IOException {
   ... throw new IOException("hi"); ...
}

This is equivalent to telling the compiler I'm dangerous method
If a method calls a dangerous method, be careful that the method itself will become a dangerous method, as shown in

"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)

When we call dangerous method in main(), main() itself becomes dangerous method. We need to modify main().

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

3.Iteration

In the past, we used enhanced loops to iterate over the elements in the 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);
}

The goal of this section is to build our own enhancement loop
First, let's introduce the iterator() method of Java's built-in List interface:

public Iterator<E> iterator();

Since the return value of iterator() is of type iterator < E >, we define a seer of type iterator < integer > to receive:

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

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

It can be seen that the iterator interface contains two methods:

  • hasNext(): check whether the next item exists
  • next(): returns the value of the current item and moves the next pointer back one bit

The Iterable Interface

We just briefly introduced iterator(), and we are not clear about its internal principle. "Just like at the other end of the wall, some monks move with torches, all we can see is a shadow."

As shown in the figure above, the code on the left is an enhanced loop, and its principle is equivalent to the iterator version on the right, that is, the enhanced loop is related to the iterator
The principle is
First, the compiler checks whether Lists has an iterator() method and returns iterator < integer >
How:
List interface extends iteratable interface, which inherits the abstract method iterator() of iteratable interface
(in fact, List extends Collection, and collection extends iteratable, but this is close enough to the fact.
In addition, I omitted some default methods in the Iterable interface)

Next, the compiler checks whether the Iterator has hasNext() and next()

How: the iterator interface clearly defines these abstract methods

Build our own KeyIterator()

Define the internal class KeyIterator in ArrayMap() and declare hasNext() and next():

    public class KeyIterator {

        public boolean hasNext() {
            return false;
        }

        public K next() {
            return null
        }
    }

Next, complete the KeyIterator according to the functions of hasNext() and next()

    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;
        }
    }

In iteratordemo Test in 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());
        }
    }
}

Print results:

hello
syrups
kingdom

So far, we have completed our KeyIterator. It is worth noting that

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

The above shows how to use nested classes
If we want to create a non static nested class, we must have a specific instance. If the KeyIterator we create is not associated with the ArrayMap, it will be meaningless. In essence, the function of the KeyIterator is to iteratively access the keys [] array of the ArrayMap.
Even though we have successfully created our own KeyIterator class, we still can't achieve the enhanced loop we expect:

        ArrayMap<String, Integer> am = new ArrayMap<String, Integer>();

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

        for (String s : am) {
            System.out.println(s);
        }
    

Because Java doesn't know how to get it, and Java doesn't know how to instantiate the iterator, what we need to do in this case is to ensure that our class has the iterator() method (translated according to josh's video original words, I don't understand...)

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

    public class KeyIterator implements Iterator<K> {
     ......}

However, the program still can't run because Java refuses to perform a for each loop on the data structure unless you also declare that the data structure implements an iterative interface, that is, inheriting iteratable < k > in the header of ArrayMap:

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

So far, we have realized the use of the iterator KeyIterator written by ourselves to strengthen the loop
Summary:
Implement iterable interface to support enhanced for loop.
iterator() method must return an object that implements the Iterator interface.
Of course, you can also use the built-in Iterator:

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

The above three lines of code are equivalent to using an internal class to implement the 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;
        }
    }
    

Topics: Java