Advanced Java - Interfaces, Inner/Nested/Local Classes, Generics, Lambdas, and Streams
Table of Contents
1 Everything Goes Everywhere!
We discuss 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 typeList<T>
is acceptable. Note that the type argumentT
must match in both places. - This does create a subtype relationship. For instance,
List<T>
is a subtype ofCollection<T>
.- When something implements
List<T>
, such asArrayList<T>
, it also "is-a"List<T>
, and therefore also "is-a"Collection<T>
.
- When something implements
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); }
- if we just want to select one of the acquired implementations,
we just write the method that calls it:
- gets rid of the need for "base class" implementations, such as
AbstractList
(whichArrayList
extends, rather than only implementingList<T>
). These classes tend to show up devoted to some interface that has some obvious implementations, from beforedefault
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 ofinterface Collection
values. At the time, thesestatic
methods had no better home, so the pairing ofinterface Collection<E>
andclass Collections
was chosen. Now, theCollections
class could be eradicated and all those static methods could be placed directly inside theCollection
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.
- 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 someNode
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; } }
- 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!
- 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 xInner.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.
- we can now have shadowing between definitions from Outer, Inner,
and method parameters (and parent class's defn's as usual)
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)
- accessing items of this type yields
- subtyping is more limited than you might expect.
- even if
B
is a subtype ofA
,List<B>
is not a subtype ofList<A>
(nor vice versa).
- even if
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 fromSomething
, e.g. a child class. This helps return types not forget e.g. that we actually usedHuskyDog
even though we only required<T extends Animal>
, saving us from having to cast back toHusky
after doing something in this function and getting back a<T>
value (which happens to be aHuskyDog
value).
- lets us use all of the functionality of
- 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 ofT
values, but we actually accept a list of values that implementComparable<T>
, and it turns out a class may implement comparable for a different type, e.g.class Bar implements Comparable<Foo>
. Now we can haveBar
values and (assumedly)Foo
values in ourComparable<T>[]
array, which isn't allowed in the original version.
- we are guaranteed the same type on the way out when
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 a lower bound:
<? super HuskyDog>
We can use any type that can accept
HuskyDog
values: assumedly, this would allow usage ofHuskyDog
,Dog
, andAnimal
. - 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 allowComparator<HuskyDog>
values to sort a list ofHuskyDog
objects. What if we also had aComparator<Dog>
, or aComparator<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 returnsT
, 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 thatX
"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 typeX
as if it is something of typeY
."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 asT
is the same, this interface's signature is explicitly claiming thatList<T>
does in fact extendCollection<T>
, giving us our subtyping relationship.
- this makes sense: look at
- 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 asArrayList<Animal>
, but the variable truly refers to an object whose type isArrayList<Dog>
, and onlyDog
objects may be stored there. We've forgotten that fact, and now we're wrongly trying to put aParakeet
in a list ofDog
values because we thoughtArrayList<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.
- If we thought that ArrayList<Dog> is a subtype of
ArrayList<Animal>, the following code would be dangerous:
- any class, generic or not, that doesn't specify a parent class,
still just has
Object
as its parent class.
- when a generic interface extends another, we have a subtyping
relationship when the type parameter(s) stay(s) the same.
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! It's all smoke and mirrors
(there's a specific implementation occurring under the hood that is
not actually abstracted away), but it works.
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:
- 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
- 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
- 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)
- from the
- 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)
- from
- to convert to an
IntStream
orDoubleStream
(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 asorted
stream operator that also gives back anIntStream
.
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 atinterface Collector<T,A,R>
andclass 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 aCollector
(a functional interface needingvoid accept(T t)
definition)- "print each of these items"
- "add each of these items to my
sum
variable"
IntStream
provides theseIntStream
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. UsesBinaryOperator<T>
orBiFunction
.
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>)
- Some useful things here:
- lastly, we want to somehow stop using stream operations, and have a
stream terminator operation.
forEach
: call the methodvoid accept(T t)
on each item; thevoid
's cause us to stop streaming any furtherreduce
: keeps folding the stream together as a monoid via aBinaryOperator<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 aT
and returns aboolean
. 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 whatfilter
actually needs is an instance ofPredicate<T>
, which is a functional interface, so it needs one method with the same arguments and return type as the signatureboolean test(T t)
. In our example above,T
is already constrained toInteger
, so the lambda syntax means thatn
must be anInteger
. That's enough to find that \(n%2==0\) isboolean
, meaning we can construct thePredicate<Integer>
instance, andfilter
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.