java<Generics>

Adding types to the types

COMP16412 week 8

Stian Soiland-Reyes

# Generics

Intended Learning Objectives

  1. Understand how type casting can cause programming bugs
  2. Be aware that generics hide implicit casting in collections
  3. Build own classes and interfaces with generic types
  4. Use methods with generic parameters

Collections can hold any Object

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

Collections can hold any Object

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)]

Internally collections hold any Object

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

Casting is error-prone

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)

Casting invite future bugs

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)

Types can be parameterised

Generics realises the type parameters

List<Food> favourites = new ArrayList<>();		
favourites.add(new Food("Lasagna", true));
for (Food food: favourites) {
  // ..
}
# Generics
  1. Type parameters are realised in the variable/field/argument definition.
  2. Type parameters to functions are typically inferred by arguments.
  3. <> in the constructor call:
    use implied type parameters from variable.

Generics realises the type parameters

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

Defining and using generics parameters

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

Parameters can extend existing 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

Convention: Single letter for parameters

# Generics

Common parameter names

  • E: element in collection
  • K: key
  • V: value
  • N: extends Number
  • C: extends Collection
  • T: arbitrary type

Generic method parameters

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

Inferring from method parameters

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() 

Optional<T>

Avoiding NullPointerException using the Monad pattern

# CHAPTER 2

Intended Learning Objectives

  1. Understand how null values can cause programming bugs
  2. Make code robust using explicit null handling
  3. Constructing Optional for polymorphic return values
  4. Consuming Optional monads as alternative to null handling
  5. Composing Optional monads in a functional style

Return type refresher

# Optional type

Methods in Java can be declared to return:

  1. Nothing (void)
    public void doStuff();
  2. A primitive type (e.g. int)
    public int value();
  3. A object instance (e.g. String, List)
    public String toString();
  4. Throw a checked exception
    public InputStream newInputStream(Path path)
           throws IOException;

Implied additional return types

# Optional type

Methods in Java can actually return:

  1. Nothing (void)
  2. A primitive type (e.g. int)
  3. A object instance (e.g. String, List)
    1. May be instance of any subclass of declared class/interface
    2. Object reference may be null
  4. Throw a checked exception
    1. Any method may also throw an unchecked RuntimeException
      e.g. Integer.parseInt("Fred")throws NumberFormatException

null can cause delayed errors

import 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 errors

import 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 errors

import 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)

Traditional procedural null checks

	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

Introducing java.util.Optional

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

Optional wraps a single (or no) value

	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)

Constructing Optional values

# Optional type
// missing value
Optional.empty();  

// non-null value, otherwise NullPointerException
Optional.of("");

 // empty if null, otherwise value
Optional.ofNullable(address);

Constructing Optional values

# 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();
		}
	}

Functional use of 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

Functional use of 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

Functional use of Optional

# 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("");
}

COMP16412 Week 8 Java Generics

By Stian Soiland-Reyes

COMP16412 Week 8 Java Generics

Java Generics, Optional and Streams. Week 8 in COMP16412.

  • 19