Generics and Collections
Using Method References
Like lambdas, they make code easier to read.
We have a functional interface
@FunctionalInterface
public interface LearnToSpeak {
void speak(String sound);
}
public class DuckHelper {
public static void teacher(String name, LearnToSpeak trainer) {
trainer.speak(name);
}
}
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
add()
Inserts a new element into the Collection and returns whether it was successful.
boolean add(E element)
Usage
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
remove()
Removes a single matching value and returns whether it was successful.
boolean remove(Object object)
Usage
Collection<String> birds = new ArrayList<>();
birds.add("hawk");
birds.add("hawk");
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<>();
birds.add("hawk");
birds.add("hawk");
birds.add("hawk");
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()
clear()
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(); []
contains()
It checks whether a certain value is in the Collection.
boolean contains(Object object)
Usage
Collection<String> birds = new ArrayList<>();
birds.add("hawk");
sout(birds.contains("hawk")); // true
sout(birds.contains("robin")); // false
removeIf()
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]
forEach()
We use it to loop through a Collection.
void forEach(Consumer<? super T> action)
Usage
Collection<String> cats = Arrays.asList("Annie", "Ripley");
cats.forEach(System.out::println);
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.values().forEach(System.out::println);
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}
putIfAbsent()
This sets a value but skips it if the value is already set to a non-null value.
merge()
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) {
this.name = name;
}
public int compareTo(Duck d) {
return name.compareTo(d.name); // 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 tocompareTo()
- 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 - a.id; // sorts ascending by id
}
public static void main(String[] args) {
var a1 = new Animal();
var a2 = new Animal();
a1.id = 5;
a2.id = 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(d.name);
}
}
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(this.name == null && quack.name == null) {
return 0;
} else if(this.name == null) {
return -1;
} else if(quack.name == null) {
return 1;
} else {
return name.compareTo(quack.name);
}
}
}
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(d.name);
}
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)
.thenComparingInt(Squirrel::getWeight);
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) -> r1.id -> r2.id;
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<>();
crateForElephant.packCrate(elephant);
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)
sout(x);
}
public static void main(String[] args) {
List<String> keywords = new ArrayList<>();
keywords.add("java");
printList(keywords);
}
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) {
list.add("quack");
}
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)
Summary
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.