top of page

Week 13

In Week 13 we will discuss generic programming, wrapper types.

Generic Programming

Introduction

Generic programming is a concept in programming languages that allows you to write classes, interfaces, methods, and algorithms in a type-neutral way, such that they can be used with any data type, including user-defined types, without the need for any type-specific code. In Java, generics introduce the concept of type parameters to Java classes, interfaces, and methods.

Generics provide stronger type checks at compile time. They eliminate the use of type casting, making the code cleaner and more readable. Additionally, generics provide a way to implement generic algorithms that operate on different types of data in a type-safe manner.

Generic Programming

Usage 

GenericProgramming.png

Generics provide stronger type checks at compile time. They eliminate the use of type casting, making the code cleaner and more readable. Additionally, generics provide a way to implement generic algorithms that operate on different types of data in a type-safe manner. In generics, you define type parameters within angle brackets (< >). These type parameters act as placeholders for actual types that will be used to instantiate an object or call a method.

In the code to the left, T is a type parameter, and GenericBox is a generic class. When you create an instance of this class, you can specify an actual type for T.

Generic Programming

Key Properties

GenericExample.png

The following features are given to us directly by Java by making use of generic programming.

  • Type Safety: Generics enhance type safety by ensuring that you only insert the correct type of objects into collections or data structures, reducing the risk of runtime type-casting errors.

  • Bounded Type Parameters: You can restrict the types that can be used as type arguments by using bounds. For example, to ensure a type parameter is a subclass of Number within a generic class NumericBox you would write   

    • public class NumericBox<T extends Number>

  • Type Erasure: To maintain compatibility with older versions of Java that do not support generics, Java implements generics through a process called type erasure. This means that during compilation, generic type information is removed, and generic classes or methods are converted to their raw type equivalents. The compiler then inserts type casts as needed.

Generic Programming

Wildcards

WildcardExample.png

​Wildcards are used to represent an unknown type in generics. When you're unsure of the specific type, but still want to ensure type safety, wildcards come in handy. The most common wildcards are given below.

  • <?> Unbounded wildcard: This wildcard represents an unknown type. It is often used when the actual type doesn't matter or isn't known. For example, when you need a method to print out all elements in a list, regardless of their type:​

  • <? extends T> Bounded wildcard: This is used when you want to limit the unknown type to a particular type or its subclasses. It's often used when you want to read items from a structure, but you don't know the exact type (it might be T or a subtype).

  • <? super T> Lower bounded wildcard: This is used when you want to ensure that an unknown type is a superclass of T (or T itself). It's often used when you want to write items to a structure.

Even though you will very likely never use wildcards here are some examples of where to use which type.

  • Reading from a list: If you only intend to read items from a list and you need items of type T or its subtypes, then List<? extends T> is a good choice.

  • Writing to a list: If you intend to put items into a list and you need to store items of type T or its supertypes, then List<? super T> is apt.

  • Both reading and writing: In general, if you need to both read and write, you'll likely want to use exact type parameters rather than wildcards.

Generic Programming

Advantages and Takeaways

The following are some of many advantages of generic programming.

  • Type Safety: Generics enable stronger type checks at compile time. By using generics, you can ensure that the code will not cause unexpected exceptions at runtime due to type casting. For instance, using a non-generic List, you can insert any type of objects, leading to a potential ClassCastException at runtime. Generics prevent such scenarios.

  • Elimination of Casts: Before generics, you'd retrieve objects from collections like List, and then you'd need to cast them to the appropriate type. With generics, the casting is done automatically, reducing runtime errors and making the code cleaner.

  • Code Reuse: Generic algorithms, classes, and methods can be defined once and can work on various types, leading to greater code reusability. For instance, a single Sort algorithm can sort List<String>, List<Integer>, or any other object types, without the need for multiple overloaded methods or duplicate logic.

Generic Programm

Type Inference

TypeInferenceExample.png

The diamond notation (<>) in Java is a shorthand introduced in Java 7 to simplify the use of generics. It allows you to create an instance of a generic class without explicitly specifying its type between the angle brackets if the compiler can infer the type from the context. This feature is also called type inference. There are some things one must keep in mind however when relying on type inference.

  • Diamond notation is useful mainly for local variable instantiation. In more complex scenarios, such as anonymous inner classes, the compiler might not always be able to determine the intended type. In those cases, you'll need to explicitly specify the type.

  • While the diamond notation simplifies code writing, it's essential to be aware that it relies on the compiler's ability to infer the correct type. Always ensure the inferred type matches your expectations.

In almost all cases you could make use of type inference this should however not pose a problem. The example code to the left contains useful features not covered in the lecture. 

  • Lambda Expressions: Java 8 introduced lambda expressions, which allow for concise representation of methods. Here, the type of item is inferred to be String based on the type of items in the list.

  • var keyword: Java 10 introduced the var keyword to infer the type of local variables from the context. The type of anotherList is inferred to be ArrayList<String> based on the right side of the assignment.

Generic Programming

Generics Convetions 

In Java, the use of single uppercase letters as generic type parameter names has become a widely accepted convention. These names aren't enforced by the Java compiler but are conventions established over time for clarity and readability. Here are the common conventions.

  • E - Element: Used extensively by the Java Collections Framework to represent elements in a collection, such as List<E>, Set<E>, etc.

  • K - Key: Used in key-value pair collections like Map<K, V>.

  • V - Value: Also used in key-value pair collections alongside K.

  • T - Type: A general-purpose placeholder representing any type.

  • N - Number: Sometimes used when a numeric type is expected, although T is also commonly used for this purpose.

Java Collections Framwork

Introduction

The Java Collections Framework (JCF) is a set of classes and interfaces that provide a standardized system to handle and manipulate groups of objects. The framework offers data structures and algorithms to deal with data efficiently and coherently. Here are the key classes and interfaces of the Java Collections Framework. I recommend looking at the Java documentation for further information.

  • Interface: These define the standard operations for collections. The main interfaces are stated below. Note that ordered and sorted are not synonyms. 

    • Collection: The root interface in the collection hierarchy.​

    • List: An ordered collection that can contain duplicates.

    • Set: A collection that cannot contain duplicate elements.

    • Queue: A collection designed for elements to be processed in a specific order.

    • Deque: Extends the Queue for handling elements at both ends.

    • Map: An object that stores associations between keys and values (key-value pairs).

  • Classes: Each of the core interfaces has several implementations. Some of them are stated below.

    • List: ArrayList, LinkedList ​

    • Set: HashSet, LinkedHashSet, TreeSet

    • Queue: PriorityQueue

    • Deque: ArrayDeque

    • Map: HashMap, LinkedHashMap, TreeMap, Hashtable

In the figure to the left interfaces are listed in blue colour, their implementations are listed in black colour. A indentation represents an implements or extends relation.

Java Collections Framwork

Collections Class

CollectionsExample.png

The Collections (note that this is not the Collection interface) class provides static methods that are algorithms to operate on collections, such as sorting and searching. Some important examples include the following.

  • Sorting: Collections.sort(List<T> list) sorts the specified list into ascending order.​

  • Searching: Collections.binarySearch(List<? extends Comparable<? super T>> list, T key) searches for the specified key in the provided list using binary search algorithm.

  • Shuffling: Collections.shuffle(List<?> list) randomly permutes the elements in the provided list.

  • Reversing: Collections.reverse(List<?> list) reverses the order of the elements in the provided list.

  • Filling: Collections.fill(List<? super T> list, T obj): Replaces all elements of the specified list with the given object.

  • Copying: Collections.copy(List<? super T> dest, List<? extends T> src) copies the source list into the destination list.

  • Minimum / Maximum: Collections.min(Collection<? extends T> coll) returns the minimum element of the given collection. Collections.max(Collection<? extends T> coll) returns the maximum element.

  • Rotating: Collections.rotate(List<?> list, int distance) rotates the elements in the list by the specified distance.

  • Swapping: Collections.swap(List<?> list, int i, int j) swaps the elements at the specified positions in the list.

  • Frequency: Collections.frequency(Collection<?> c, Object o) returns the number of elements in the specified collection equal to the specified object.

Wrapper Types

Introduction

In Java, every primitive data type has a corresponding "wrapper class", which is an object-oriented representation of that primitive. These wrapper classes are part of the Java standard library and are used for various purposes, including working with data structures that require objects. We need wrapper classes in the following context.

  • Object representation of primitives: There are situations in Java programming where we need an object representation of a primitive, for example, in data structures or certain methods that require objects. The common data structures that require object representation of primitives are the ones in the Collections Framework.

  • Null Value: Primitive types can never be null, but there are circumstances when you might need to represent a "no value" state. Wrapper classes can have a null value.

  • Additional utility methods: Wrapper classes come with a variety of utility methods that allow for operations such as converting to and from strings, comparing values, and other common tasks.

Wrapper Types

ArrayList Example

WrapperExample.png

Java's ArrayList is a dynamic array implementation of the List interface. It can grow and shrink in size dynamically. The important point to note here is that ArrayList can only store objects; it cannot store primitive data types.

For instance, if you want to create an ArrayList to store integers, you can't use the primitive int type. Instead, you'll use the Integer wrapper class.

In the code to the left, you can see that when we add an int value to the ArrayList, Java automatically "autoboxes" it to the Integer wrapper class. Similarly, when we retrieve the value using the get method, it is "unboxed" back to an int. This autoboxing and unboxing process is seamless and allows you to work with wrapper classes as if they were primitives in many scenarios.

Wrapper Types

Boxing / Unboxing

Boxing is the process of converting a primitive data type into its corresponding wrapper class object. This is done automatically by the Java compiler. It's essentially the act of "wrapping" a primitive value inside an object so it can be used in places that expect an object, like in collections.

Conversely, unboxing is the process of converting a wrapper class object back into its corresponding primitive data type. Like boxing, unboxing is also done automatically by the Java compiler.

Wrapper Types

Generics Convetions 

In Java, the use of single uppercase letters as generic type parameter names has become a widely accepted convention. These names aren't enforced by the Java compiler but are conventions established over time for clarity and readability. Here are the common conventions.

  • E - Element: Used extensively by the Java Collections Framework to represent elements in a collection, such as List<E>, Set<E>, etc.

  • K - Key: Used in key-value pair collections like Map<K, V>.

  • V - Value: Also used in key-value pair collections alongside K.

  • T - Type: A general-purpose placeholder representing any type.

  • N - Number: Sometimes used when a numeric type is expected, although T is also commonly used for this purpose.

Wrapper Types

Integer Example

WrapperExample.png

The Integer class is one of the eight wrapper classes in Java's standard library that provide an object representation for the eight primitive data types. In the case of Integer, it's the object representation for the primitive int. Let us summarise the most important things to know.

  • Purpose: Primarily, it allows primitive int values to be used in contexts requiring objects, like Java collections.

  • Package: It's part of the java.lang package, so you don't need to import it explicitly.

  • Immutability: Integer objects are immutable, meaning once an Integer object is created, its value cannot be changed.

Let us discuss some of the most important methods, where for all other wrapper types similar methods exist with the corresponding name.

  • Integer.valueOf(int i): Returns an Integer instance representing the specified int value.

  • Integer.valueOf(String s): Returns an Integer object holding the value of the specified String. Throws NumberFormatException if the string does not contain a parsable integer.

  • Integer.toString(int i): Returns a String object representing the specified integer. One can also pass an Integer object which will work due to autoboxing.

  • Integer.MAX_VALUE: This constant holds the maximum value an int can have, 2^31 - 1.

  • Integer.MIN_VALUE: This constant holds the minimum value an int can have, -2^31.

That is it.

You are done with theory. To make sure you understand the theory you need to apply what you learnt in practice. Solve the exercises.

bottom of page