Generics and Collections

Using Method References

Like lambdas, they make code easier to read.

We have a functional interface

public interface LearnToSpeak {
	void speak(String sound);

public class DuckHelper {
	public static void teacher(String name, LearnToSpeak trainer) {

public class Duckling {
	public static void makeSound(String sound) {
		LearnToSpeak learner = s -> System.out::println;
		DuckHelper.teacher(sound, learner);

There’re four formats for methods references

Calling Static Methods

The Collection class has a staic method that can be used for sorting. The Consumer functional interface takes one param and does not return anthing.

Consumer<List<Integer>> methodRef = Collections::sort;
Consumer<List<Integer>> lambda = x -> Collections.sort(x);

Calling Instance Methods on a Particular Object

The String class has a startsWith() method that takes one param and returns a boolean.

var str = "abc";
Predicate<String> methodRef = str::startsWith;
Predicate<String> lambda = s -> str.startsWith(s);

A method reference doesn’t have to take any parameters. For the next example, we use a Supplier, which takes zero params and returns a value.

var random = new Random();
Supplier<Integer> methodRef = random::nextInt;
Supplier<Integer> lambda = () -> random.nextInt();

Calling Instance Methods on a Parameter

We call an instance method that doesn’t take any params. The trick is that we do so without knowing the instance in advance.

Predicate<String> methodRef = String::isEmpty;
Predicate<String> lambda = s -> s.isEmpty();

It looks like a static method, but it isn’t.

You can even combine the two types of instance method references with a BiPredicate, which takes two params and returns a boolean.

BiPredicate<String, String> methodRef = String::startsWith;
BiPredicate<String, String> lambda = (s, p) -> s.startsWith(p);

Calling Constructors

A constructor reference is a special type of method reference that uses new instead of a method, and it instantiates an object. It’s common for a constructor reference to use a Supplier as shown here.

Supplier<List<String>> methodRef = ArrayList::new;
Supplier<List<String>> lambda = () -> new ArrayList();

Number of Params in a Method Reference

A method reference can look the same, even when it will behave differently based on the surrounding context.

Given the following method

public class Penguin {
	public static Integer countBabies(Penguin... cuties) {
		return cuties.length;

The method can be interpreted in three ways, for zero or more values.

// no param
Supplier<Integer> methodRef = Penguin::countBabies;
Supplier<Integer> lambda = () -> Penguin.countBabies();

// one param
Function<Penguin, Integer> methodRef = Penguin::countBabies;
Function<Penguin, Integer> lambda = (x) -> Penguin.countBabies(x);

// multiple params
BiFunction<Penguin, Penguin, Integer> methodRef = Penguin::countBabies;
BiFunction<Penguin, Penguin, Integer> lambda =
	(x, y) -> Penguin.countBabies(x, y);

Using Lists, Sets, Maps, and Queues

A collection is a group of objects contained in a single object, There are four main interfaces.

  • List - Ordered collection of elements that allows duplicate entries. It can be accesed by an int index.
  • Set - collection that does not allow duplicate entries.
  • Queue - Collection that orders its elements in an specific order for processing. A typical queue is FIFO or LIFO.
  • Map - Collection that maps keys to values, with no duplicate keys allowed. The elements in it are key/value pairs.

(add image of Collections hierarchy)

Common Collections Methods


Inserts a new element into the Collection and returns whether it was successful.

boolean add(E element)


Collection<String> list = new ArrayList<>();
sout(list.add("one")); // true
sout(list.add("one")); // true

Collection<String> set = new HashSet<>();
sout(set.add("one")); // true
sout(set.add("one")); // false


Removes a single matching value and returns whether it was successful.

boolean remove(Object object)


Collection<String> birds = new ArrayList<>();
sout(birds.remove("cardinal"));  // false
sout(birds.remove("hawk")); // true
sout(birds); // hawk
Deleting while Looping

Java does not allow removing elements from a list while using the enhanced for loop.

Collection<String> birds = new ArrayList<>();

for(String bird : birds) {
	birds.remove(bird); // ConcurrentModificationException

isEmpty() and size()

They look at how many elements are in the Collection.

boolean isEmpty()
int size()


It provides an easy way to discard all elements of the Collection.

void clear()

How to use it

Collection<String> birds = new ArrayList<>();
birds.add("hawk"); [hawk]
birds.add("hawk"); [hawk, hawk]
birds.clear(); []


It checks whether a certain value is in the Collection.

boolean contains(Object object)


 Collection<String> birds = new ArrayList<>();
 sout(birds.contains("hawk")); // true
 sout(birds.contains("robin")); // false


It removes all elements that match a condition.

boolean removeIf(Predicate<? super E> filter)

It uses a Predicate.

Collection<String> list = new ArrayList<>();
list.add("Magician"); [Magician]
list.add("Assistant"); [Magician, Assistant]
list.removeIf(s -> s.startsWith("M")); [Assistant]


We use it to loop through a Collection.

void forEach(Consumer<? super T> action)


Collection<String> cats = Arrays.asList("Annie", "Ripley");

Using the List Interface

You use a list when you want an ordered collection that can contain duplicate entries.
Items can be retrieved and inserted at specific positions in the list, based on an int index. Unlike an array, many List implementations can change in size after they’re declared.

Comparing List Implementations

An ArrayList is like a resizable array. When elements are added, it automatically grows.
Its main benefit, is that you can look up any element in constant time. Adding or removing an element is slower than accessing an element. It’s a good choice when you’re reading more often than writing.

A LinkedList is special because it implements both List and Queue. It has all the methods of a List. It also has additional methods to facilitate adding or removing from the beginning and/or end of the list.
It’s mein benefits are that you can access, add, and remove from the beginning and end of the list. The trade-off is that dealing with an arbitrary index takes linear time. This makes it a good choice when you’ll be using it as a Queue.

Creating a List with a Factory

There’re a few methods that let you create a List back, but don’t know the type of it.

Method Description Can add elements? replace? delete?
Arrays.asList(varargs) Returns fixed size list backed by an array no yes no
List.of(varargs) Returns immutable list no no no
List.copyOf(collection) Returns immutable list with copy of original collection’s values no no no

Immutable lists throw an UnsupportedOperationException when adding or removing a value.

Working with List Methods

This methods are for working with indexes.

Method Description
boolean add(E element) Adds element to end
void add(int index, E element) Adds element at index
E get(int index) Returns element at index
E remove(int index) Removes element at index
void replaceAll(UnaryOperator<E> op) Replaces each element in the list with the result of the operator
E set(int index, E e) Replaces element at index and returns original. Throws IndexOutOfBoundsException if the index is larger than maximum.
List<String> list = new ArrayList<>();
list.add("SD"); // [SD]
list.add(0, "NY"); // [NY, SD]
list.set(1, "FL"); // [NY, FL]
sout(list.get(0)); // NY
list.remove("NY"); // [FL]
list.remove(0); // []
list.set(0, "?"); // IndexOutOfBoundsException

Now, let’s look at using the replaceAll() method. It takes a UnaryOperator that takes one parameter and returns a value of the same type.

List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.replaceAll(x -> x*2); // [2, 4, 6]

Using the Set Interface

You use a set when you don’t want to allow duplicate entries and you aren’t concerned with the order.

Comparing Set Implementations

A HashSet stores its elements in a hash table, which means the keys are a hash and the values are an Object. This means that it uses the hashCode() method of the objects to retrieve them more efficiently.
The main benefit is that adding elements and checking whether an element is in the set both have constant time. The trade-off is that you lose the order in which you inserted the elements.

A TreeSet stores its elements in a sorted tree structure. The main benefit is that the set is always in sorted order. The trade-off is that adding and checking whether an element exists take longer than with a HashSet.

Working with Set Methods

Like List, you can create an immutable Set in one line or make a copy of an existing one.

Set<Character> letters = Set.of('z', 'o', 'o');
Set<Character> copy = Set.copyOf(letters);
Set<Integer> set = new HashSet<>();
boolean b1 = set.add(66); // true
boolean b2 = set.add(10); // true
boolean b3 = set.add(66); // false
boolean b4 = set.add(8); // true

Using the Queue Interface

You use a queue when elements are added and removed in a specific order. They’re typically used for sorting elements prior to processing them.

Comparing Queue Implementations

We use LinkedList, as in addition to being a list, it’s a double-ended queue. It’s different from a regular queue in that you can insert and remove elements from both the front and back of the queue. It’s main benefit is that it implements both List and Queue interfaces. The trade-off is that it isn’t as efficient as a pure queue.

Working with Queue Methods

|Method|Description|Throws exception on failure| |:—:|:—:|:—:| |boolean add(E e)|Adds an element to the back of the queue and returns true or throws exception|Yes| |E element()|Returns next element or throws exception if empty|yes| |boolean offer(E e)|Adds an element to the back of the queue and returns if it was successful|No| |E remove()|Removes and returns next element or throws exception if empty|yes| |E poll()|removes and returns next element or returns null if empty|no| |E peek()|returns next element or returns null is empty|no|

Queue<Integer> queue = new LinkedList<>();
queue.offer(10); // true
queue.offer(4); // true
queue.peek(); // 10
queue.poll(); // 10
queue.poll(); // 4
queue.peek(); // null

Using the Map Interface

You use a map when you want to identify values by a key. All Map classes have keys and values.

Comparing Map Implementations

A HahsMap stores the keys in a hash table. This means that it uses the hashCode() method of the keys to retrieve their values more effitiently.
The main benefit is that adding elements and retrieving the element by key both have constant time. The trade-off is that you lose the order in which you inserted the elements.

A TreeMap stores the keys in a sorted tree structure. The main benefit is that keys are always in a sorted order. The trade-off is that adding and checking whether a key is present takes longer as the tree grows larger.

Working with Map Methods

|Method|Description| |:—:|:—:| |void clear()|Removes all keys and values| |boolean containsKey(Object key)|Returns whether key is in map| |boolean containsValue(Object value)|Returns whether value is in map| |Set<Map.Entry<K,V>> entrySet()|Returns a Set of key/value pairs| |void forEach(BiConsumer(K key, V value))|Loop through each key/value pair| |V get(Object key)|Returns the value mapped by key or null| |V getOrDefault(Object key, V defaultValue)|Returns the value mapped by the key or the default value| |boolean isEmpty()|Returns whether the map is empty| |Set<K> keySet()|Returns set of all keys| |V merge(K key, V value, Function(<V, V, V> func))|Sets value if key is not set. Runs the function if the key is set to determine the new value. Removes if null| |V put(K key, V value)|Adds or replaces key/value pair. Returns previous value or null| |V putIfAbsent(K key, V value)|Adds value if key not present and returns null. Otherwise, returns existing value.| |V remove(Object key)|Removes and returns value mapped to key or null| |V replace(K key, V value)|Replaces the value for a given key if the key is set or null| |void replaceAll(BiFunction<K,V, V> func)|Replaces each value with the results of the function| |Collection<V> values()|Returns Collection of all values|

forEach() and entrySet()

The map version has two parameters - key and value.

Map<Integer, Character> map = new HashMap<>();
map.put(1, 'a');
map.put(2, 'b');
map.put(3, 'c');
map.forEach((k, v) -> sout(v))
map.entrySet().forEach(e -> sout(e.getKey() + e.getValue());
replace() and replaceAll()
Map<Integer, Integer> map = new HashMap<>();
map.put(1, 2);
map.put(2, 4);
Integer original = map.replace(2, 10); // 4
map.replaceAll((k, v) -> k + v); // {1=3, 2=12}

This sets a value but skips it if the value is already set to a non-null value.


It adds logic of what to choose.

The next example takes two String as input, and a BiFunction which is the logic. In this case it takes the longest String.

BiFunction<String, String, String> mapper = (v1, v2) -> v1.length() > v2.length ? v1 : v2;

Map<String, String> favorites = new HashMap<>();
favorites.put("Jenny", "Bus Tour");
favorites.put("Tom", "Tram");

String jenny = favorites.merge("Jenny", "Skyride", mapper);
String tom = favorites.merge("Tom", "Skyride", mapper);
// {Tom = Skyride, Jenny = Bus Tour}

(for more information on merge() check page 130 of 2nd book)

Sorting Data

(!) Comparable and Comparator are similar enough to be tricky. The exam likes to see if it can trick you into mixing the two. (!)

Creating a Comparable Class

Comparable has just one Method.

public interface Comparable<T> {
	int compareTo(T o);

As it implements T, any object can be Comparable.

public class Duck implements Comparable<Duck> {
	private String name;

	public Duck(String name) { = name;

	public int compareTo(Duck d) {
		return name.compareTo(; // sorts ascendingly by name

	public static void main(String[] args) {
		var ducks = new ArrayList<Duck>();
		duck.add(new Duck("Quack"));
		duck.add(new Duck("Puddles"));
		Collections.sort(duck); // sort by name


Here, Duck implements compareTo(). Since Duck is comparing objects of type String, and it already has a compareTo() method, it can just delegate.

There are three rules to know about the compareTo()

  • 0 is returned, when the current object is equivalent to the argument to compareTo()
  • A negative number, when the current object is smaller than the argument to compareTo()
  • A positive number, when the current object is larger than the argument to compareTo()
public class Animal implements Comparable<Animal> {
	private int id;

	public int compareTo(Animal a) {
		return id -; // sorts ascending by id

	public static void main(String[] args) {
		var a1 = new Animal();
		var a2 = new Animal(); = 5; = 7;
		sout(a1.compareTo(a2)); // -2
		sout(a1.compareTo(a1)); // 0
		sout(a2.compareTo(a1)); // 2

Casting the compareTo() Argument

When dealing with legacy code or code that does not use generics, the compareTo() method requires a cast since it’s passed an Object.

public class LegacyDuck implements Comparable {
	private String name;

	public int compareTo(Object obj) {
		LegacyDuck d = (LegacyDuck) obj; // cast because no generics
		return name.compareTo(;

Checking for null

When writing your own compare methods, you should check the data before comparing it, if it’s not validated ahead of time.

public class MissingDuck implements Comparable<MissingDuck> {
	private String name;

	public int compareTo(MissingDuck quack) {
		if(quack == null) {
			throw new IllegalArgumentException("Poorly formed duck!");

		if( == null && == null) {
			return 0;
		} else if( == null) {
			return -1;
		} else if( == null) {
			return 1;
		} else {
			return name.compareTo(;

Keeping compareTo() and equals() Consistent

The compareTo() method returns 0 if two objects are equal, while equals() returns true if two objects are equal.

Comparing Data with a Comparator

Sometimes you want to sort an object that did not implement Comparable, or you want to sort objects in different ways at different times.

Comparator is a functional interface.

public class Duck implements Comparable<Duck> {

private String name;
	private int weight;

	// constructor
	// getters & setters...

	public String toString() {
		return name;

	public int compareTo(Duck d) {
		return name.compareTo(;

	public static void main(String[] args) {
		var ducks = new ArrayList<Duck>();
		ducks.add(new Duck("Quack", 8));
		ducks.add(new Duck("Puddles", 10));

		// Comparator<Duck> byWeight = (d1, d2) -> d1.getWeight() - d2.getWeight();
		Comparator<Duck> byWeight = Comparator.comparing(Duck::getWeight);
		Collections.sort(ducks); // [Puddles, Quack]
		Collections.sort(ducks, byWeight); // [Quack, Puddles]


(!) Comparable can be used without an import statement. Comparator cannot (!)

Comparing Comparable and Comparator

|Difference|Comparable|Comparator| |:—:|:—:|:—:| |Package name|java.lang|java.util| |Interface must be implemented by class comparing?|Yes|No| |Method name in interface|compareTo()|compare()| |Number of parameters|1|2| |Common to declare using a lambda|No|Yes|

(!) The exam will try to trick you by mixing up the two and seeing if you can catch it. It also may try to trick you with the methods’ names. Pay attention when you see Comparator and Comparable in questions (!)

var byWeight = new Comparator<Duck>() { // DOES NOT COMPILE
	public int compareTo(Duck d1, Duck d2) {
		return d1.getWeight() - d2.getWeight();

The previous code doesn’t compile. The method name is wrong. A Comparator must implement a method named compare().

Comparing Multiple Fields

When comparing multiple instance variables, the code gets messy.

public class Squirrel {
	private int weight;
	private String species;

	// constructors
	// getters / setters

We want to sort first by species name, then if they are the same species, by weight.

public class MultiFieldComparator implements Comparator<Squirrel> {
	public int compare(Squirrel s1, Squirrel s2) {
		int result = s1.getSpecies().compareTo(s2.getSpecies());
		if(result != 0) {
			return result;

		return s1.getWeight() - s2.getWeight();

This works assuming no null values.

Alternatively, we can use method references and build the comparator. This does the same

Comparator<Squirrel> c = Comparator.comparing(Squirrel::getSpecies)

Suppose we want to sort in descending order by species.

var c = Comparator.comparing(Squirrel::getSpecies).reversed();
Helper static methods for Comparator

|Method|Description| |:—:|:—:| |comparing(function)|Compare by the results of a function that returns any Object| |comparingDouble(function)|Compare by the results of a function that returns a double| |comparingInt(function)|Compare by the results of a function that returns an int| |comparingLong(function)|Compare by the results of a function that returns a long| |naturalOrder()|Sort using the order specified by the Comparable implementation on the object itself| |reverseOrder()|Sort using the reverse of the order specified by the Comparable interface on the object itself|

Helper default methods for building a Comparator

|Method|Description| |:—:|:—:| |reversed()|Reverse the order of the chained Comparator| |thenComparing(function)|If the previous Comparator returns 0, use this comparator that returns an Object| |thenComparingDouble(function)|If the previous Comparator returns 0, use this comparator that returns a double| |thenComparingInt(function)|If the previous Comparator returns 0, use this comparator that returns an int| |thenComparingLong(function)|If the previous Comparator returns 0, use this comparator that returns a long|

Sorting and Searching

Now that we’ve learned about Comparable and Comparator, we can finally do something useful, like sorting. The Collections.sort() method uses the compareTo() method to sort. It expects the objects to be sorted to be Comparable.

public class SortRabbits {
	static class Rabbit { int id; }

	public static void main(String[] args) {
		List<Rabbit> rabbits = new ArrayList<>();
		rabbits.add(new Rabbit());
		Collections.sort(rabbits); // DOES NOT COMPILE

(!) This doesn’t compile, because Rabbit is not Comparable. You can fix this by passing a Comparator to sort()(!)

public class SortRabbits {
	static class Rabbit { int id; }

	public static void main(String[] args) {
		List<Rabbit> rabbits = new ArrayList<>();
		rabbits.add(new Rabbit());
		Comparator<Rabbit> c = (r1, r2) -> ->;
		Collections.sort(rabbits, c);

The sort() and binarySearc() methods allow you to pass in a Comparator object, when you don’t want to use natural order.

Reviweing binarySearch()

The binarySearch() requires a sorted List.

List<Integer> list = Arrays.asList(6, 9, 1, 8);
Collections.sort(list); // [1, 6, 8, 9]
Collections.binarySearch(list, 6); // 1
Collections.binarySearch(list, 3); // -2

(I left info out, more on this at page 139)

Working with Generics

Generic Classes

The syntax for introducing a generic is to declare a formal type parameter in angle brackets.

public class Crate<T> {
	private T contents;

	public T emptyCrate() {
		return contents;

	public void packCrate(T contents) {
		this.contents = contents;

This are the naming conventions for generics.

Name Usage
E Element
K Map key
V Map Value
N number
T generic data type
S, U, V… multiple generic types
Elephant elephant = new Elephant();
Crate<Elephant> crateForElephant = new Crate<>();
Elephant inNewHome = crateForElephant.emptyCrate();

Generic classes aren’t limited to having a single type parameter.

public class SizeLimitedCrate<T, U> {
	private T contents;
	private U sizeLimit;

	// constructor
Elephant elephant = new Elephant();
Integer numPounds = 15_000;
SizeLimitedCrate<Elephant, Integer> c1 = new SizeLimitedCrate<>(elephant, numPounds);

Generic Interfaces

Just like a class, an interface can declare a formal type parameter.

public interface Shippable<T> {
	void ship(T t);
class ShippableRobotCrate implements Shippable<Robot> {
	public void ship(Robot t) {}
class ShippableAbstractCrate<U> implements Shippable<U> {
	public void ship(U t);

Raw Types

The final way is to not use generics at all. This is the old way of writing code. It generates a compiler warning, but it does compile.

class ShippableCrate implements Shippable {
	public void ship(Object t) {}

Generic Methods

This is often useful for static methods since they aren’t part if an instance that can declare the type.

public class Handler {
	public static <T> void prepare(T t) {}

	public static <T> Crate<T> ship(T t) {}

(!) Unless a method is obtaining the type from the class/interface, it’s specified immediately before the return type of the method. (!)

public class More {
	public static <T> void sink(T t) {}
	public tatic <T> T identity(T t) { return t; }
	public static T noGood(T t) { return t; } // DOES NOT COMPILE

When you have a method declare a generic param type, it’s independent of the class generics.

public class Crate<T> {
	public <T> T tricky(T t) {
		return t;
public static String createName() {
	Crate<Robot> crate = new Crate<>();
	return crate.tricky("bot");

Bounding Generic Types

(more on this on page 145~ from 2nd book)

They restrict what types can be used in a wildcard. A wildcard generic type is an unknown generic type represented with a question mark ?.

Type of bound Syntax Example
Unbounded ? List<?> a = new ArrayList<String>();
Upper bound ? extends type List<? extends Exception> a = new ArrayList<RuntimeException>();
Lower bound ? super type List<? super Exception> a = new ArrayList<Object>();

Unbounded Wildcards

An unbounded wildcard represents any data type. You use ? when you want to specify that any type is okay.

public static void printList(List<?> list) {
	for(Object x : list)

public static void main(String[] args) {
	List<String> keywords = new ArrayList<>();

These two statements are not equivalent

List<?> x1 = new ArrayList<>();
var x2 = new ArrayList<>();

The first is of type List, while the second is of type ArrayList. Also, we can only assign x2 to a List<Object>.

Upper-Bounded Wildcards

(!) A generic type can’t juse use a subclass, instead they use wildcards. (!)

ArrayList<Number> list = new ArrayList<Integer>(); // DOES NOT COMPILE
// instead, use
List<? extends Number> list = new ArrayList<Integer>();

The upper-bounded wildcard says that any class that extends Number or Number itself can be used as a formal param type.

public static long total(List<? extends Number> list) {
	long count = 0;
	for(Number number : list) {
		count += number.longValue();
	return count;

When we work with upper bounds or unbounded wildcards, the list becomes logically immutable and therefore cannot be modified.

static class Sparrow extends Bird {}
static class Bird {}

public static void main(String[] args) {
	List<? extends Bird> birds = new ArrayList<Bird>();
7:	birds.add(new Sparrow()); // DOES NOT COMPILE
8:	birds.add(new Bird()); // DOES NOT COMPILE

The problem stems from the fact that Java doesn’t know what type List<? extends Bird> really is. It could be List<Bird> or List<Sparrow>.
Line 7 doesn’t compile because we can’t add a Sparrow to List<? extends Bird>.
Line 8 doesn’t compile because we can’t add a Bird to List<Sparrow>.

(!) Upper bounds are like anonymous classes in that they use extends regardless of whether we’re working with a class or an interface. (!)

Lower-Bounded Wildcards

I want to give a method a param which may be a List<String> or List<Object>.

We have the following “solutions”

  Method compiles Can pass List<String> Can pass List<Object>
List<?> No (unbounded generics are immutable) Yes Yes
List<? extends Object> No (upper-bounded generics are immutable) Yes Yes
List<Object> Yes No (with generics, must pass exact match) Yes

To solve this problem, we need to use a lower bound.

public static void addSound(List<? super String> list) {

With a lower bound, we are telling Java that the list will be a list of String objects or a list of some objects that are superclass of String.

(check again page 148 from 2nd book. there’s more than this)


A method reference is a compact syntax for writing lambdas that refer to methods. There are four types:

  • static methods
  • instance methods on a particular object
  • instance methods on a parameter
  • constructor references.

Each primitive class has a corresponding wrapper class. For example, long’s wrapper class is Long. Java can automatically convert between primitive and wrapper classes when needed. This is called autoboxing and unboxing. Java will use autoboxing only if it doesn’t find a matching method signature with the primitive. For example, remove(int n) will be called rather than remove(Object o) when called with an int.

The diamond operator <> is used to tell Java that the generic type matches the declaration without specifying it again. The diamond operator can be used for local variables or instance variables as well as one-line declarations.

The Java Collections Framework includes four main types of data structures: lists, sets, queues, and maps.
The Collection interface is the parent interface of List, Set, and Queue. The Map interface does not extend Collection. You need to recognize the following:

  • List: An ordered collection of elements that allows duplicate entries
    • ArrayList: Standard resizable list
    • LinkedList: Can easily add/remove from beginning or end
  • Set: Does not allow duplicates
    • HashSet: Uses hashCode() to find unordered elements
    • TreeSet: Sorted. Does not allow null values
  • Queue: Orders elements for processing
    • LinkedList: Can easily add/remove from beginning or end
  • Map: Maps unique keys to values
    • HashMap: Uses hashCode() to find keys
    • TreeMap: Sorted map. Does not allow null keys

The Comparable interface declares the compareTo() method. This method returns a negative number if the object is smaller than its argument, 0 if the two objects are equal, and a positive number otherwise. The compareTo() method is declared on the object that is being compared, and it takes one parameter. The Comparator interface defines the compare() method. A negative number is returned if the first argument is smaller, zero if they are equal, and a positive number otherwise. The compare() method can be declared in any code, and it takes two parameters. Comparator is often implemented using a lambda.

The Arrays and Collections classes have methods for sort() and binarySearch(). Both take an optional Comparator parameter. It is necessary to use the same sort order for both sorting and searching, so the result is not undefined.

Generics are type parameters for code. To create a class with a generic parameter, add <T> after the class name. You can use any name you want for the type parameter. Single uppercase letters are common choices.

Generics allow you to specify wildcards. <?> is an unbounded wildcard that means any type. <? extends Object> is an upper bound that means any type that is Object or extends it. <? extends MyInterface> means any type that implements MyInterface. <? super Number> is a lower bound that means any type that is Number or a superclass. A compiler error results from code that attempts to add an item in a list with an unbounded or upper-bounded wildcard.

Exam Essentials

Translate method references to the “long form” lambda. Be able to convert method references into regular lambda expressions and vice versa. For example, System.out::print and x -> System.out.print(x) are equivalent. Remember that the order of method parameters is inferred for both based on usage.

Use autoboxing and unboxing. Autoboxing converts a primitive into an Object. For example, int is autoboxed into Integer. Unboxing converts an Object into a primitive. For example, Character is autoboxed into char.

Pick the correct type of collection from a description. A List allows duplicates and orders the elements. A Set does not allow duplicates. A Queue orders its elements to facilitate retrievals. A Map maps keys to values. Be familiar with the differences of implementations of these interfaces.

Work with convenience methods. The Collections Framework contains many methods such as contains(), forEach(), and removeIf() that you need to know for the exam. There are too many to list in this paragraph for review, so please do review the tables in this chapter.

Differentiate between Comparable and Comparator. Classes that implement Comparable are said to have a natural ordering and implement the compareTo() method. A class is allowed to have only one natural ordering. A Comparator takes two objects in the compare() method. Different Comparators can have different sort orders. A Comparator is often implemented using a lambda such as (a, b) -> a.num – b.num.

Write code using the diamond operator. The diamond operator (<>) is used to write more concise code. The type of the generic parameter is inferred from the surrounding code. For example, in List<String> c = new ArrayList<>(), the type of the diamond operator is inferred to be String.

Identify valid and invalid uses of generics and wildcards. <T> represents a type parameter. Any name can be used, but a single uppercase letter is the convention. <?> is an unbounded wildcard. <? extends X> is an upper-bounded wildcard and applies to both classes and interfaces. <? super X> is a lower-bounded wildcard.