Skip to content

Instantly share code, notes, and snippets.

@hschafer
Created November 29, 2018 00:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hschafer/098bbb5285d2b4b9d066e8010404b142 to your computer and use it in GitHub Desktop.
Save hschafer/098bbb5285d2b4b9d066e8010404b142 to your computer and use it in GitHub Desktop.
Overview of Java features we talked about on 11/28

Java Collections Library and Java Features

Today we talked about a ton of details about Java's collections and many of the Java features that get used there. Here is a short overview of the things we discussed since there were many small details.

Generics

Before we had to write classes like ArrayIntList where we would write

public class ArrayIntList {
    private int[] elementData;
    private int size;
    
    ...
}

but as we saw earlier, we can make our class "generic" by taking a type parameter for what types we want to have in the array. To do this we transformed ArrayIntList to ArrayList<E> and changed all the int data to E data.

public class ArrayList<E> {
    private E[] elementData;
    private int size;
}

Interfaces

We have seen interfaces before, but as a reminder, you can say something like

public interface List<E> {
    public boolean add(E value);
    public boolean contains(E value);
    public int size();
    public int isEmpty();
    ...
}

and now whenever a class implements the List<E> interface, it must provide at least all of the methods defined in the interface. You may also implement more than one interface like in

public class LinkedList<E> implements List<E>, Queue<E> {
    // must implement all methods in List and all methods in Queue to compile
}

Inner Classes

One thing that has been weird all quarter is the fact that for our classes that need node objects (e.g. LinkedList and SearchTree), we would have to define the node in a separate class and make the fields public. This was a bit awkward, and it would be much better to use the construct from Java known as "inner classes" to define the node class within the LinkedList class. Here is an example

public class LinkedList<E> implements List<E>, Queue<E> {
    ...
    
    private class ListNode<E> {
        private E data;
        private ListNode<E> next;
        
        private ListNode(E data) {
            this(data, null);
        }
        
        private ListNode(E data, ListNode<E> next) {
            this.data = data;
            this.next = next;
        }
    }
}

Now the ListNode class is entirely contained with the LinkedList and the clients will never know it exists! Functionally this behaves the same way for the rest of the LinkedList where it just constructs ListNodes wherever it wants, without the hassle of having a whole separate file.

By default, each inner class has a secret field that references the outer class that constructed it so you can access the LinkedList's front by doing something like

public class LinkedList<E> implements List<E>, Queue<E> {
    ...
    
    private class ListNode<E> {
        private E data;
        private ListNode<E> next;
        
        private ListNode(E data) {
            this(data, null);
        }
        
        private ListNode(E data, ListNode<E> next) {
            this.data = data;
            this.next = next;
            
            // LinkedList.this is a reference to the LinkedList that constructed this ListNode
            LinkedList.this.front = null;
        }
    }
}

It's not super common that you want this access (since it takes up space in the ListNode class), so we generally make the inner classes static; when you make an inner class static it removes this hidden reference.

public class LinkedList<E> implements List<E>, Queue<E> {
    ...
    
    private static class ListNode<E> {
        private E data;
        private ListNode<E> next;
        
        private ListNode(E data) {
            this(data, null);
        }
        
        private ListNode(E data, ListNode<E> next) {
            this.data = data;
            this.next = next;
            
            // No longer has the outer class reference so following doesn't compile
            // LinkedList.this.front = null;
        }
    }
}

Abstract Classes

In Java, we have interfaces to make requirements about which methods classes that implement that interface need to have and have classes and inheritance to share code between classes. Ideally, we want something in the middle that can partially implement some methods but leave other implementations to sub-classes; this concept is called an abstract class.

An abstract class is much like a class in the sense you can define behaviors (methods), but you are not allowed to construct an instance of the abstract class. We saw an example abstract class

public abstract class AbstractList<E> implements List<E> {
    public boolean add(E value) {
        return add(size(), value);
    }
    
    public boolean contains(E value) {
        return indexOf(value) >= 0;
    }
    
    public boolean isEmpty() {
        return size() == 0;
    }
}

Before we made it an abstract class, we ran into an issue where it wouldn't compile because it was a class that implemented the List interface but didn't implement all the methods; by making the class abstract, we make it so no one can construct an AbstractList and force all sub-classes of AbstractList to implement the remaining methods for us.

You can also require subclasses of AbstractList to have more methods than List by adding "abstract methods" that must be implemented in subclasses. For example, if we added to AbstractList

public abstract class AbstractList<E> implements List<E> {
    public abstract void destroy();
    
    public boolean add(E value) {
        return add(size(), value);
    }
    
    public boolean contains(E value) {
        return indexOf(value) >= 0;
    }
    
    public boolean isEmpty() {
        return size() == 0;
    }
}

Any sub-class of AbstractList must also implement a destroy method (as well as all other methods in List that weren't implemented by AbstractList)

Final Methods

We saw that you are able to make methods final in Java. What this means is that sub-classes of the class are unable to override the implementation of a final method. This can prevent users from violating some important behaviors of your class by extending it and doing something malicious in the sub-class by overriding a method.

Sharing methods between interfaces

Just like how we use inheritance to share methods between classes, we can also use inheritance to share method headers between interfaces. We saw that many of the methods required in List, Set, and Queue were similar so it would be nice to make a "super-interface" that had those commonalities; this super-interface in our case is the Collection interface

public interface Collection<E> {
    public boolean add(E value);
    public boolean remove(E value);
    public boolean contains(E value);
    public int size();
    public boolean isEmpty();
}

public interface List<E> extends Collection<E> {
    public E get(int index);
    public boolean add(int index, E value);
    public E remove(int index);
    public int indexOf(E value);
}

public interface Queue<E> extends Collection<E> {
    public E peek();
    public E remove();
}

// A collection that contains no duplicate elements
public interface Set<E> extends Collection<E> {
    // Technically has no new methods from Collections
    // Java still has the Set interface and "redefines" the method headers
    // in this interface to provide more specific documentation and specification
    // for Set-like operations. Example:
    
    // Adds the given value to this set. Returns true if value 
    // was distinct from all other values in the set (therefore was added successfuly)
    // and returns false if value is a duplicate with any value in the set currently.
    public boolean add(E value);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment