Advanced Java Concepts

Table of Contents

1 Everything Goes Everywhere!

We discussed how many definitions can go inside of classes, and generally how different concepts can be mixed and intermingled together in interesting ways. We catalog those points made in class here.

1.1 Interfaces: More than just a Bag of Abstract Methods

1.1.1 Extending other Interfaces

Interfaces can extend other interfaces. This means they inherit all the definitions of the parent interface, and may add or replace definitions in similar ways to how a child class can do so with its parent class definitions.

  • Example: Lists and Collections, from the Java Collections Framework:
    interface List<T> extends Collection<T>
    

    Whenever a value of type Collection<T> is needed, a value of type List<T> is acceptable. Note that the type argument T must match in both places.

  • This does create a subtype relationship. For instance, List<T> is a subtype of Collection<T>.
    • When something implements List<T>, such as ArrayList<T>, it also "is-a" List<T>, and therefore also "is-a" Collection<T>.

1.1.2 Variables

Interfaces can have variables, but they must be static and final. If you leave off either modifier, it is still assumed of all variables defined inside the interface.

1.1.3 Default Methods

Interfaces can provide methods that have body implementations, by including the default modifier. Any implementer (or child-interface) will "inherit" this definition, but they are also free to @Override it.

  • when multiple interfaces are implemented, and they have an overlapping signature where they each provided a default, we are required to have an @Override version in the implementer class; this version will be used at all times.
    • if we just want to select one of the acquired implementations, we just write the method that calls it:
      @Override .... method(..){  PreferredInterfaceName.super.method(args); }
      
  • gets rid of the need for "base class" implementations, such as AbstractList (which ArrayList extends, rather than only implementing List<T>). These classes tend to show up devoted to some interface that has some obvious implementations, from before default methods were in the language. We can still use them if we so choose.

1.1.4 Static Methods

Interfaces can have static methods (body-implementation present). They are a great place to collect static methods that have no obvious home.

  • For instance, class java.util.Collections exclusively defines static methods (and fields) for many of the various kinds of interface Collection values. At the time, these static methods had no better home, so the pairing of interface Collection<E> and class Collections was chosen. Now, the Collections class could be eradicated and all those static methods could be placed directly inside the Collection interface.

1.2 Classes: Exotic Members

1.2.1 enumeration members

We can define an enumeration inside a class.

  • access values by ClassName.EnumName.ValueName (as long as it's visible)

1.2.2 interface members

We can define an interface inside a class.

1.2.3 Nested classes: inner and static classes

We can define a class as a member of another class. This is useful for grouping definitions together, for instance when one class is only needed in another class, or perhaps to specifically hide the class (making it private). These classes are called "nested classes", and they can be static or non-static. Only the non-static classes have access to the instance variables of the enclosing ("outer") class.

  1. Static Classes

    We can have static classes defined in a class:

    • can't access any instance variables of the outer class, but both can see each other's private members.
    • creation:
      • from somewhere outside, we can do this:
      OuterClass.StaticClass sc = new OuterClass.StaticClass(args)
      
      • from inside the outer class, we can just use:
      StaticClass sc = new StaticClass(args)
      
    • good example: when we implement a linked list, we often use a LinkedList<T> class to represent the entire structure (remembering the size, head item, and providing list-wide methods), but we tend to use some Node class to represent the individual spots in the list. This can be a static class:
      class LinkedList<T> {
         private int size;
         public LinkedList(){...}
         public void add(T t){...}
         public T get(int i){...}
         
         // nested class,  only used within LinkedList.
         private static class Node {
            int val;
            Node next, prev;
         }
      }   
      
  2. Inner Classes

    Inner classes are nested classes that are attached to a specific instance of the outer class, thus gaining access to the instance variables of that particular object.

    • can access instance members of the outer class.
    • creation: must first have an object of the Outer class.
      Outer out = new Outer();
      Outer.Inner inn = out.new Inner();  // weird placement of new!
      
  3. Shadowing Issues

    Interesting shadowing issues arise when we have nested classes.

    • we can now have shadowing between definitions from Outer, Inner, and method parameters (and parent class's defn's as usual)
      • x means the parameter (closest definition)
      • this.x means Inner's x
        • Inner.this.x also means Inner's x
      • Outer.this.x means Outer's x
      • we can nest inner classes as deeply as we want, and keep fully-qualifying names enough to disambiguate. As usual, though, try not to shadow names if you can help it.

1.3 Local Classes

We can put an entire class definition inside of a method! It's only available inside that method.

  • it can't have any static members inside it (can't define or declare them).
  • it can access members of the surrounding class
  • it can even access locals in that method, but only when they are final.

2 Advanced Generics

There are a few situations where generics can be used in ways beyond a vanilla, no-information type argument. We can either require that the supplied type argument is a type that implements (or extends) some particular type, or alternatively we can require that the type is a supertype of some particular type. And if we don't actually care about a particular type argument, we can ignore it with a wildecard. We give examples of each situation below.

2.1 Wildcards

When we don't need to use a particular type argument (but it still needs to be present for completeness' sake), we can use the wildcard to indicate this don't-care situation.

  • Wildcard type argument, "?": represents an unknown type
    • accessing items of this type yields Object-typed values
    • only nulls may be added, no other values. (regardless of casting attempts)
  • subtyping is more limited than you might expect.
    • even if B is a subtype of A, List<B> is not a subtype of List<A> (nor vice versa).

2.1.1 Wildcard Examples

If we have a Pair<T,U> type and we write the getFirst method, we really don't care what the second type is. Here is the full definition without wildcards, followed by the wildcard version:

public <T,U> T getFirstLame(Pair<T,U> p) { return p.first; }
public <T>   T getFirstWild(Pair<T,?> p) { return p.first; }

Next example: counting how many items are in a list, we really don't care about the kind of items inside. (size method already exists for collections so this method isn't needed, but we can call it without caring what's inside as an example).

public static int countItems(Collection<?> c) { return c.size(); }

2.2 Upper Bounds

Sometimes we want to know more than nothing about the values coming in to a generic function, but pegging it to a specific type is still not satisfactory. We want an upper bound on the type while still letting it remember what specific type was applied.

  • setting upper bounds: the type parameter list includes an extends clause: (below, Something is either a class or an interface)
    <T extends Something> 
    
    • lets us use all of the functionality of Something
    • but T is still remembered as something that may be distinct from Something, e.g. a child class. This helps return types not forget e.g. that we actually used HuskyDog even though we only required <T extends Animal>, saving us from having to cast back to Husky after doing something in this function and getting back a <T> value (which happens to be a HuskyDog value).
  • can set multiple upper bounds: up to one class (listed first), and any number of interfaces.
    <T extends SomeClass & Interface1 & Interface2 & ... & InterfaceN>
    
  • why not just use Something, with no generic type?
    • we are guaranteed the same type on the way out when T is used as a return type; we might need that extra strength of information.
    • we might want to involve multiple hardly-related interfaces, and can't do so without a single anchor type to use if we don't use generics this way.
    • even giving up on the extends lingo and trying to simplify the types will let more things in:
      public class Go {
        
        public static <T extends Comparable<T>> int numBigger(T[] anArray, T elem) {
          int count = 0;
          for (T e : anArray)
            if (e.compareTo(elem) > 0)
            ++count;
          return count;
        }
        
        // slightly simpler type
        public static <T> int numBiggerV2(Comparable<T>[] anArray, T elem) {
          int count = 0;
          for (Comparable<T> e : anArray)
            if (e.compareTo(elem) > 0)
            ++count;
          return count;
        }
      }
      

      With numBiggerV2, it may feel like we still just accept a list of T values, but we actually accept a list of values that implement Comparable<T>, and it turns out a class may implement comparable for a different type, e.g. class Bar implements Comparable<Foo>. Now we can have Bar values and (assumedly) Foo values in our Comparable<T>[] array, which isn't allowed in the original version.

2.3 Lower Bounds

We can also use generics with a lower bound, indicating that it's acceptable for a type parameter to be any type that is a supertype of something particular.

  • representing an lower bound:
    <? super HuskyDog>
    

    We can use any type that can accept HuskyDog values: assumedly, this would allow usage of HuskyDog, Dog, and Animal.

  • example in practice: Look at Collections.sort:
    public static <T> void sort(List<T> list, Comparator<? super T> c)
    

    Though we could have limited ourselves to Comparator<T> c, that would only allow Comparator<HuskyDog> values to sort a list of HuskyDog objects. What if we also had a Comparator<Dog>, or a Comparator<Animal>? These make perfect sense as another way to sort dogs or animals, which certainly includes huskies. Only by using the lower bound can we accept all these different comparators when sorting a list of huskies.

2.4 Combining Upper and Lower Bounds

An individual type parameter may not be given upper and lower bounds. However, we can mix them in some subtle ways. Let's look at Collections.sort(List<T>) as our example:

public static <T extends Comparable<? super T>> sort(List<T> list)

We will give it a list of T values to sort, but there needs to be some sort of natural ordering (Comparable implementation is needed).

For this specific example, consider these classes:

class Animal implements Comparable<Animal> { ... }
class Dog extends Animal { ... }

Dog inherits an implementation of Comparable<Animal>, but doesn't then provide Comparable<Dog>. We'd really like to be able to sort dogs based on this more general animal-comparing approach.

First Attempt We could have tried to just allow any old T:

public static <T> void sortBAD(List<T> list)

but then Java would rightly complain that there might not be a Comparable<T> instance available for class T.

Second Attempt Okay, so we need to know that T has a Comparable<T> instance available. That requires an upper bound:

public static <T extends Comparable<T>> void sortBrittle(List<T> list)

This is better, but we're still required to supply a type for T that is comparable to itself. Dog objects are only comparable to Animal objects, so we can't sort a list of dogs yet. Comparing with any parent type, for example Object or Animal, would still be sufficient.

Incidentally, if we have a List<Animal>, it can be sorted by sortBrittle, because Animal implements Comparable<Animal> (and not Comparable<SomeParentOfAnimal>).

Third Attempt Let's relax the requirement that our type T implements Comparable of any supertype of T:

public static <T extends Comparable<? super T>> void sort(List<T> list)

Finally, we've got what we need! Even though our Dog class only implements Comparable<Animal>, we can still feed a List<Dog> to this sort method.

2.4.1 Another example

It turns out a similar example is given here in the java tutorials, on the Collections.max method. Follow the link and search the page for Collections.max: Java Tutorials: More Fun with Wildcards

2.4.2 Rule of Thumb:

There's a great summary quote from the Java tutorial on Wildcards:

  • "In general, if you have an API that only uses a type parameter T as an argument, its uses should take advantage of lower bounded wildcards (? super T). Conversely, if the API only returns T, you'll give your clients more flexibility by using upper bounded wildcards (? extends T)."

2.5 Subtyping Issues

  • given:
    interface ParentI<T>{}
    interface ChildI<T> extends ParentI<T> {}
    class A {}
    class B extends A{}
    class Gen<T> implements ChildI<T>{}
    
  • We define subtype(X,Y) to imply that X "is-a" Y (whether from class extending or interface implementing or what have you). It is also useful to think of this relationship as stating, "it's acceptable in all circumstances to view something of type X as if it is something of type Y."

    Here are some subtyping results:

    subtype(B,A)
    subtype(Gen<T>,Object)
    ! subtype(Gen<B>, Gen<A>)      // the relation of B and A doesn't suffice, here.
    subtype(Gen<T>, ChildI<T>)     // for any T
    subtype(ChildI<T>, ParentI<T>) // for any T
    subtype(Gen<T>, ParentI<T>)    // for any T
    
  • takeaways:
    • when a generic interface extends another, we have a subtyping relationship when the type parameter(s) stay(s) the same.
      • this makes sense: look at interface List<T> extends Collection<T>. As long as T is the same, this interface's signature is explicitly claiming that List<T> does in fact extend Collection<T>, giving us our subtyping relationship.
    • existing subtyping relationships between two different types are irrelevant when they are both plugged into the same generic class or interface.
      • If we thought that ArrayList<Dog> is a subtype of ArrayList<Animal>, the following code would be dangerous:
        // BAD CODE EXAMPLE
        ArrayList<Dog> dogs = new ArrayList<Dog>();
        ArrayList<Animal> animals = dogs; // this line doesn't compile
        animals.add(new Parakeet());      // this line therefore not allowed either
        

        Uh-oh! In line three, animals may be typed as ArrayList<Animal>, but the variable truly refers to an object whose type is ArrayList<Dog>, and only Dog objects may be stored there. We've forgotten that fact, and now we're wrongly trying to put a Parakeet in a list of Dog values because we thought ArrayList<Animal> was enough information. It's not, and this is why line two will be a compilation error.

        This comes down to a pair of conflicting needs: we want to be guaranteed what kinds of values we can read from the list, and we want to know what kinds of values we can write into the list. We don't get the subtyping relationship, because it's not safe to write the parent type's values into the sneaky/hidden child type's slots. That would break what kinds of values we could read form the list somewhere else where we happen to remember that it only claims to have child objects in it.

    • any class, generic or not, that doesn't specify a parent class, still just has Object as its parent class.

3 Lambda expressions

We can create "anonymous functions" that embody a bit of calculation, and pass them around much like any other value. Instead of a variable that stores int values, we can create a variable that stores int \(\rightarrow\) int functions!

3.1 Focus on Functional Interfaces

We achieve this by using interfaces that only need a single method to be implemented. Anywhere something of the interface type is needed, we can use the special lambda syntax to directly provide the single method implementation rather than the alternative longer paths:

  1. Using a Single-Purpose class:
    • make a class that implements the interface
    • make an object of that class type
    • use that object where the interface's type was needed
  2. Using an anonymous class:
    • use special anonymous class syntax to create a single object of an un-named, in-place class definition that extends the interface (or abstract class)
    • use that object where the interface's type was needed
  3. Just directly use a lambda expression where the interface's type was needed!

This is always tied to implementing a one-method-needed interface. These are called functional interfaces.

  • When creating such an interface (with only one abstract method) that we expect to use as a functional interface, we can ask Java to confirm that it truly has that shape by using the @FunctionalInterface annotation immediately preceding the interface definition itself. Like @Override, it's not actually required, but helps the compiler to give more informative error messages.
  • allows us to avoid creating an anonymous class (bulky syntax).
  • some common functional interfaces to use:
    public interface Predicate <T> { public boolean test   (T t);      }
    public interface Consumer  <T> { public void    accept (T t);      }
    public interface Comparator<T> { public int     compare(T a, T b); }
    public interface Function<T,R> { public R       apply  (T t);      }
    interface BiFunction   <T,U,R> { public R       apply  (T t, U u); } // implements 2-arg functions
    interface BinaryOperator<T> extends BiFunction<T,T,T>{ (also apply)} // implements 2-arg functions with same-type arguments and return type 
    
    (also: default compose, before, and identity functions exist.)
    

3.2 Storing Lambdas

Let's learn the syntax for lambda expressions.

Given a functional interface, you can store a lambda to a variable of that type. For example, using the Predicate<T> functional interface, which has the public boolean test(T t) method:

Predicate<Integer> canVote = (Integer x) -> x>=18;

To really get use out of them, we should be writing methods that expect arguments of these functional-interface types. Streams provide a great opportunity to use many functional interfaces by defining "stream operator" methods that heavily utilize these functional interface types as their parameter types. So let's learn about streams, in order to get practice using lambda expressions every time we need an object that implements some functional interface.

4 Streams

A stream represents a sequence of values, like a collection or an array. We can then use various stream operators that accept an incoming stream of values, performs some modification on them, and emit another stream of values. On its own, the stream operator doesn't actually have the incoming values yet - it represents the transformation from input stream to output stream. It's like a little factory that has a function embedded in it, which it will automatically apply over and over for each value of its input stream that it is fed.

We can connect multiple stream operators into a stream pipeline, describing the entire process of effects that will be performed on an incoming stream. The very end of the pipeline may be a terminal stream operation, which is not required to generate a sequence of values, and may instead do something such as calculating a single value, performing printing, updating some structure, or whatever else you have in mind that needs a sequence of values to consume in the process.

4.1 Example Stream Operations

  • to map a function across the values, emitting the results. For instance, we might want to multiply all incoming items by 2, or we might want to answer whether each item was a multiple of 5, or we might want to .lowerCase() each incoming string.
    • from the Stream interface:
      <R> Stream<R> map(Function<? super T,? extends R> mapper)
      
    • the interface Function<T,R> lacks this one definition:
      R apply(T t)
      
  • to filter out items so we only keep ones that meet some criterion. Perhaps we only keep integers that are positive, or even; perhaps we only accept Person's that are over 65. The values themselves don't get changed, but the number of items emitted by this stream can be less than the number given to it.
    • from interface Stream<T>:
      Stream<T> filter(Predicate<? super T> predicate)
      
    • the interface Predicate<T> lacks this one definition:
      boolean test(T t)
      
  • to convert to an IntStream or DoubleStream (and others); when each item will be converted to the specific type of items, we have some operators that would be common for them. These are just non-generic streams of a particular type. For instance, IntStream provides a sorted stream operator that also gives back an IntStream.

4.2 Example Terminal Stream Operations

  • check if all items in the stream satisfy some predicate, via allMatch:
    • "are all of these numbers even?"
    • "are all of these list items non-null?"
    • "are all of these people over 65?"
  • collect all values to be combined into a single answer. Take a look at interface Collector<T,A,R> and class Collectors for many options. Example things we'd want to do with a collector:
    • "add all these numbers together"
    • "find the max value"
  • forEach item, run some statements. Needs a Collector (a functional interface needing void accept(T t) definition)
    • "print each of these items"
    • "add each of these items to my sum variable"
  • IntStream provides these IntStream terminal operators among others: max, min, sum
  • reduce by combining all values into a single value. For instance, we might want to add all ints together, or concatenate all strings together from the stream. Uses BinaryOperator<T> or BiFunction.

4.3 Using Streams

We can create interface Stream<T> values, and then feed lots of lambdas with the provided methods.

We should build a stream (from an array, list, Collection, others). Stream is an interface:

interface Stream<T> {...}
source Stream creation:
arrays Arrays.stream(arrayExpr)
collections(e.g. ArrayList) collectionExpr.stream()

There are plenty of methods to implement, but we can often just use Collection.stream(Collection) to start using streams.

  • next, we perform any number of stream transformations (like map or filter), and then perform some terminal operation (do printing, sum up the ints, collect or reduce all our values togehter, or some value-consuming operation on each item in the stream).
    • Some useful things here:
      map apply a function to each item in the given stream
      filter only pass through values in the stream that meet some predicate
      generate create an endless stream of the same value
      sorted sorted(Comparator<? super T>)
  • lastly, we want to somehow stop using stream operations, and have a stream terminator operation.
    • forEach: call the method void accept(T t) on each item; the void's cause us to stop streaming any further
    • reduce: keeps folding the stream together as a monoid via a BinaryOperator<T>
    • IntStream.sum (when we have a stream of ints, we can sum them all togeher)

4.3.1 Stream Examples

For each one, we will show both a non-stream block of code for familiarity, and then the stream version. The streams are using functional interface types in lambda expressions. You may want/need to look up the stream operations we use to understand their argument/return types more.

  • Let's take an array of ints, make a stream out of it, keep only the primes, and get back to an array. This uses a simpler/restricted type of stream called an IntStream.
    int[] xs = {1,2,3,4,5,6,7,8,9};
    
    int[] keepers = Arrays.stream(xs) . filter(n -> prime(n)) . toArray();
    

    We really just needed the prime method, since it's already a method that accepts a T and returns a boolean. We can name the method directly as in the following variation of the answer. (It also shows a nice multi-line style):

    int[] keepers = Arrays.stream(xs)
                  . filter(TheClassName :: prime)
                  . toArray();
    
  • Let's take an array of ints, make a stream out of it, add one to each, only keep the evens, and then sum the results.
    int total = Arrays.stream(xs)
              . map(n->n+1)
              . filter(n -> n%2==0)
              . sum();
    

    It's worth noting that Java is able to find the type of something like the argument to filter, because what filter actually needs is an instance of Predicate<T>, which is a functional interface, so it needs one method with the same arguments and return type as the signature boolean test(T t). In our example above, T is already constrained to Integer, so the lambda syntax means that n must be an Integer. That's enough to find that \(n%2==0\) is boolean, meaning we can construct the Predicate<Integer> instance, and filter is correctly called. It's impressive that we don't have to ascribe the types!

    How would this look without streams? Here are two similar solutions.

    // option A
    // make the list of all incremented values.
    int[] upOnes = new int[xs.length];
    for (int i=0; i<xs.length; i++){
       upOnes[i] = xs[i]+1;
    }
    
    find out how many evens there are.
    int numEvens = 0;
    for (int i=0;i<upOnes.length; i++){
       if (upOnes[i]%2==0){
          numEvens++;
    }
    
    // build the list of evens.
    int[] theEvens = new int[numEvens];
    int i=0;
    int ei = 0;
    while (ei<theEvens.length){
       if (upOnes%2==0){
          theEvens[ei] = upOnes[i];
          ei++;
       }
       i++;
    }
    
    // calculate the sum.
    int total = 0;
    for (int j=0; j<theEvens.length; j++){
       total += theEvens[j];
    }
    

    Here's another, which fuses the loops together carefully:

    // option B
    // check if the values (+1) are even, and add them (+1) if so.
    int total = 0;
    for (int i=0; i<xs.length; i++){
       if ((xs[i]+1)%2==0){
          total += (xs[i]+1);
       }
    }
    

    Both options suffer from having to describe how to calculate what we want; the streams version gets to describe what we want.