Adding types to the types
COMP16412 week 8
Stian Soiland-Reyes
# Generics
import java.util.ArrayList;
import java.util.List;
public class Listfun {
@SuppressWarnings("rawtypes")
public static void main(String[] args) {
List favourites = new ArrayList();
favourites.add("Lorelai");
favourites.add("Lasagna");
favourites.add("Looping");
for (Object obj : favourites) {
System.out.println(obj);
}
}
}
# Generics
$ java ListFun
Lorelai
Lasagna
Looping
import java.util.ArrayList;
import java.util.List;
public class Listfun {
@SuppressWarnings("rawtypes")
public static void main(String[] args) {
List favourites = new ArrayList();
favourites.add("Lorelai");
favourites.add("Lasagna");
favourites.add("Looping");
favourites.add(3.14);
favourites.add(favourites);
System.out.println(favourites);
}
}
# Generics
$ java ListFun
[Lorelai, Lasagna, Looping, 3.14, (this Collection)]
package java.util;
// ...
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// ...
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// ...
}
# Generics
List favourites = new ArrayList();
favourites.add("Lorelai");
favourites.add("Lasagna");
favourites.add("Looping");
favourites.add(3.14);
favourites.add(favourites);
Object food = favourites.get(3);
String food2 = (String)food;
# Generics
$ java ListFun
Exception in thread "main" java.lang.ClassCastException:
class java.lang.Double cannot be cast to class java.lang.String
at generics/generics.Listfun.main(Listfun.java:9)
public class Listfun {
record Food(String dish, boolean vegetarian) {};
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void main(String[] args) {
List favourites = new ArrayList();
favourites.add(new Food("Lasagna", true));
favourites.add(new Food("Steak", false));
// ...
String food = (String)favourites.getFirst();
System.out.println(food.toLowerCase());
}
}
# Generics
$ java ListFun
Exception in thread "main" java.lang.ClassCastException:
class generics.Listfun$Food cannot be cast to class java.lang.String
at generics/generics.Listfun.main(Listfun.java:13)
List<Food> favourites = new ArrayList<>();
favourites.add(new Food("Lasagna", true));
for (Food food: favourites) {
// ..
}
# Generics
List<Food> favourites = new ArrayList<>();
favourites.add(new Food("Lasagna", true));
Food food = favourites.getLast();
# Generics
Compilers and IDEs love generics! 😻
→ From runtime exception to compile time problem
String food = (String)favourites.getFirst();
System.out.println(food.toLowerCase());
Casting is implied and type safe
public interface List<E> extends SequencedCollection<E> {
int size();
boolean add(E e);
E get(int index);
E remove(int index);
// ...
}
# Generics
Within a class/interface/method, generics parameters are used in signatures, just like any other defined types
public class KeyValue<K, N extends Number> {
K key;
N value;
public KeyValue(K key, N value) {
this.key = key;
this.value = value;
}
public int asInt() {
return value.intValue();
}
public static void main(String[] args) {
KeyValue<String, Float> temperature = new KeyValue<>("Temperature", 37.7f);
System.out.println(temperature.asInt());
}
}
# Generics
By using "extends" the class can call methods on the parameterised type
# Generics
public class PairMaker {
public static <E> List<E> listOfPair(E e1, E e2) {
ArrayList<E> list = new ArrayList<E>();
list.add(e1);
list.add(e2);
return list;
}
public static void main(String[] args) {
List<String> pair = listOfPair("Romeo", "Juliet");
List<Number> numbers = listOfPair(15.21, 5);
List<Object> mixed = listOfPair("Mixed", 5);
}
}
# Generics
Method-specific generics are defined left of the return type
Method parameters are typically bound by the arguments passed in
public class EmptyMaker {
public static <E> List<E> empty() {
return new ArrayList<E>();
}
public static void main(String[] args) {
List<String> none = empty();
List<Number> noNumbers = empty();
String whenTypeCantBeInferred = EmptyMaker.<String>empty().getFirst();
}
}
# Generics
Generic types can usually be inferred from which type the returned value will be stored/passed as.
For explicit typing of method generics: TheClass.<Type>method()
Avoiding NullPointerException using the Monad pattern
# CHAPTER 2
# Optional type
Methods in Java can be declared to return:
void
)public void doStuff();
int
)public int value();
String
, List
)public String toString();
public InputStream newInputStream(Path path)
throws IOException;
# Optional type
Methods in Java can actually return:
void
)int
)String
, List
)
null
RuntimeException
Integer.parseInt("Fred")
throws NumberFormatException
null
can cause delayed errorsimport java.util.List;
import java.util.Map;
public class Recipient {
private static Map<String,String> STREETS = Map.of(
"M13 9PL", "Oxford Rd",
"M14 5TQ", "Wilmslow Rd"
);
private String name, postcode, address;
public Recipient(String name, String postcode) {
this.name = name;
this.postcode = postcode;
this.address = STREETS.get(postcode.toUpperCase());
}
public String prepareLetter() {
return name + "\n" + address.toUpperCase() + "\n" + postcode;
}
public static void main(String[] args) {
List<Recipient> recipients = List.of(
new Recipient("Alice", "M13 9PL"),
new Recipient("Bob", "M14 5UN")
);
// ..
for (Recipient r : recipients) {
System.out.println(r.prepareLetter());
}
}
}
# Optional type
null
can cause delayed errorsimport java.util.List;
import java.util.Map;
public class Recipient {
private static Map<String,String> STREETS = Map.of(
"M13 9PL", "Oxford Rd",
"M14 5TQ", "Wilmslow Rd"
);
private String name, postcode, address;
public Recipient(String name, String postcode) {
this.name = name;
this.postcode = postcode;
this.address = STREETS.get(postcode.toUpperCase());
}
public String prepareLetter() {
return name + "\n" + address.toUpperCase() + "\n" + postcode;
}
public static void main(String[] args) {
List<Recipient> recipients = List.of(
new Recipient("Alice", "M13 9PL"),
new Recipient("Bob", "M14 5UN")
);
// ..
for (Recipient r : recipients) {
System.out.println(r.prepareLetter());
}
}
}
# Optional type
Alice
OXFORD RD
M13 9PL
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.toUpperCase()" because "this.address" is null
at generics/optional.Recipient.prepareLetter(Recipient.java:21)
at generics/optional.Recipient.main(Recipient.java:31)
null
can cause delayed errorsimport java.util.List;
import java.util.Map;
public class Recipient {
private static Map<String,String> STREETS = Map.of(
"M13 9PL", "Oxford Rd",
"M14 5TQ", "Wilmslow Rd"
);
private String name, postcode, address;
public Recipient(String name, String postcode) {
this.name = name;
this.postcode = postcode;
this.address = STREETS.get(postcode.toUpperCase());
}
public String prepareLetter() {
return name + "\n" + address.toUpperCase() + "\n" + postcode;
}
public static void main(String[] args) {
List<Recipient> recipients = List.of(
new Recipient("Alice", "M13 9PL"),
new Recipient("Bob", "M14 5UN")
);
// ..
for (Recipient r : recipients) {
System.out.println(r.prepareLetter());
}
}
}
# Optional type
Alice
OXFORD RD
M13 9PL
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.toUpperCase()" because "this.address" is null
at generics/optional.Recipient.prepareLetter(Recipient.java:21)
at generics/optional.Recipient.main(Recipient.java:31)
public String getAddress() {
return address;
}
public String prepareLetter() {
String addressToPrint = getAddress();
if (addressToPrint != null) {
addressToPrint = "";
}
return name + "\n" + addressToPrint.toUpperCase() + "\n" + postcode;
}
# Optional type
Use Java identity check =! null
,
fall back to a default value
Null checks can become tedious and require extra variables
Null checks are easy to miss
import java.util.Optional;
public class Recipient {
// ...
private final String name, postcode, address;
public Recipient(String name, String postcode) {
this.name = Objects.requireNonNull(name);
this.postcode = Objects.requireNonNull(postcode);
this.address = STREETS.get(postcode.toUpperCase());
}
public Optional<String> getAddress() {
return Optional.ofNullable(address);
}
}
# Optional type
public String prepareLetter() {
Optional<String> addressToPrint = getAddress();
if (addressToPrint.isEmpty()) {
addressToPrint = Optional.of("");
}
return name + "\n" + addressToPrint.get().toUpperCase() + "\n" + postcode;
}
# Optional type
public final class Optional<T> {
// ...
public boolean isPresent();
public boolean isEmpty();
public T get();
public T orElse(T other);
public T orElseThrow();
// ...
}
Polymorphic type
T
Optional<T>
Optional.empty()
Optional.of(t)
# Optional type
// missing value
Optional.empty();
// non-null value, otherwise NullPointerException
Optional.of("");
// empty if null, otherwise value
Optional.ofNullable(address);
# Optional type
// missing value
Optional.empty();
// non-null value, otherwise NullPointerException
Optional.of("");
// empty if null, otherwise value
Optional.ofNullable(address);
Tips: Construct Optional
close to return statement
Use two return
statements to make path of empty Optional
explicit
Convention: Don't keep Optional
instances in fields.
public Optional<String> getAddress() {
if (STREETS.containsKey(postcode)) {
return Optional.of(STREETS.get(postcode));
} else {
return Optional.empty();
}
}
// Not safe unless checking isPresent() first
getAddress().get().toUpperCase();
// Always return, possibly with fall-back value.
// ..but why upper case the empty string?
getAddress().orElse("").toUpperCase();
// Transform using lambda function
getAddress().map(a -> a.toUpperCase()).orElse("");
// Transform using method reference
getAddress().map(String::toUpperCase).orElse("");
# Optional type
Optional
encourages functional paradigm.
Higher order functions return a new Optional
// Not safe unless checking isPresent() first
getAddress().get().toUpperCase();
// Always return, possibly with fall-back value.
// ..but why upper case the empty string?
getAddress().orElse("").toUpperCase();
// Transform using lambda function
getAddress().map(a -> a.toUpperCase()).orElse("");
// Transform using method reference
getAddress().map(String::toUpperCase).orElse("");
# Optional type
Optional
encourages functional paradigm.
Higher order functions return a new Optional
Monad: A wrapped value with additional semantics/constraints
MayBe monad: value may not be there
Monads can be combined (bound) to form new monads
# Optional type
Higher order functions return a new Optional
..which can be further composed
Partial computation:
Delay handling of empty values until end of pipeline
Option<String> askPostCode() { /* .. */ }
String addressLine() {
<Optional>String postcode = askPostCode();
return postcode
.map(this::findAddress)
.map(String::toUpperCase)
.orElse("");
}