Week7

Course overview

Classes and Objects

Classes

You need to understand how you write a class; you put each class in its own .java file, with the basic syntax: public class MyName … { … }. Within the class you can define variables and methods. The typical order is to first define the variables, then the constructors, and last the methods.

You need to know the distinction between instance variables and class (static) variables and local variables You can define methods to operate on objects (regular methods) and to operate without an object (static methods). Methods that only read information from the object are called accessor methods. Methods that change the state/information stored in the object are called mutator methods. Methods and constructors can be overloaded. The type system is used to figure out which one will be executed.

The Type System

Java is a strongly typed language. As a result, all variables and expressions have a type. Expressions can be all kinds of things, e.g. just a literal 36, a variable x, summing two variables x + y, a call to a method Math.min(3,5) or even a call to a constructor new ArrayList<Integer>(). There is an important distinction between primitive types and nonprimitive types, although Autoboxing converts primitive values 'automaticaly' to non-primitive values and vice-versa. To convert between types, implicit casts are allowed when the compiler can reason they are safe. Explicit casts are potentially unsafe. You have generic types, like ArrayList<Integer> or Map<String,Integer>, where generic types are denoted by a type variable (e.g. ArrayList<E> or Map<K,V> )

You also need to understand relations between types:

  • Interfaces can be implemented by classes, which implies objects of that class can also be used as the type of the interface. (subtype: class, supertype: interface)
  • When working with inheritance, subclasses also are of the type of their superclass, but the other way around needs explicit casting.
  • Relations can be visualized using a class hierarchy diagram (which can also show interfaces and abstract classes).

The compiler uses the type system to reason about the correctness of types in your code. You should be able to reason about types like the compiler does. This is an important skill when you are coding in a team, use libraries written by other people, etc.

Casting and instanceof

Suppose that we have a Herbivore which is a subclass of Creature. Which of the following assignments are okay?

Herbivore h = new Herbivore();
Creature c = h;
Herbivore h2 = c;

The third line is not, because a Herbivore is more specific than a Creature by being a subclass. Thus, the Creature object cannot be cast into a Herbivore object. The second line, however, performs an implicit cast (it is always safe, since we go from a subtype to a supertype). It is important that you can figure out these sorts of errors for Question 3 in the practice exam! If we want to convert a Creature to a Herbivore in a safe way, we have to check the type of the creature and perform a cast afterwards.

if (c instanceof Herbivore) {
    Herbivore h2 = (Herbivore) c;
}

Objects

You should be able to instantiate objects using their constructor. You should understand that you can call methods on objects.

  • Within a class, you can just call the methods of the current object without using a period to call them. (e.g. within countFile() you can call count())
  • When you call methods on other objects, you need the period.
public int sizeSquared(Collection<?> col) {
    return col.size() * col.size();
}

You can also pass objects to methods and constructor (see above). You can obtain a reference to the current object via the this keyword. You should be able to use some basic objects used during the assignments (e.g. String, BigInteger, ArrayList, PrintWriter, Scanner, etc.). Also, you should be able to call and use methods of classes you have not seen before, but for which an explanation is provided of how they work.

References and (Im)mutability

Variables of non-primitive types hold references. We can have multiple references to the same object. This can lead to confusion if you modify the object using one reference, since these modifications will also be observable via the other references. A non-primitive variable can also hold a null reference. If that is the case calling a method on in or accessing an instance variables throws a NullPointerException. Some classes have been designed such that all modifications of an object are forbidden. Notable examples are String and Integer. Objects of these classes are called immutable. With such objects you don’t have to worry about the reference issue. Other classes, however, can be modified and are called mutable. The Collections are notable examples. Reasoning about references can be made easier using Memory State Diagrams.

Exceptions

When a problematic situation is detected, programs can and should raise an exception. In Java, an exception is raised using the throw keyword and an object of type Throwable. You can indicate that a method might throw an Exception using the throws keyword in the method header. You have multiple constructions with the try keyword:

  • The try-catch construction (multiple catch clauses allowed)
  • The try-finally construction (executed as soon as the try clause is exited)
  • The try-catch-finally construction (combination of above)
  • The try-with-resources construction (no catch or finally necessary)

There are checked exceptions and unchecked exceptions. Subclasses of Error and RuntimeException are unchecked. All other exceptions are checked exceptions. Checked exceptions either need to be caught, or thrown the caller (this can be indicated with throws in the method header). This is not necessary for unchecked exceptions, but if you throw them, it is nice to still indicate this to other programmers in the method header. The only checked Exceptions that you have dealt with are the IOException and FileNotFoundException. For instance, the constructors of Scanner, PrintWriter and FileInputStream can throw a FileNotFoundException. The readLine() method of BufferedReader can throw a IOException. This is not indicated in the reference you have available at the exam, so you should remember it!

Interfaces

Interfaces are one of the ways to achieve polymorphism. An interface introduces a new type, for which you can specify methods that must be implemented by classes implementing the interface. A class indicates it is implementing an interface with the keyword implements in the class header and must implement its methods. Objects of classes implementing the interface also have the type of the interface (the interface is a supertype and the class a subtype). Classes can extend at most one class, but can have any number of interfaces as a supertype.

Inheritance

The second way to achieve polymorphism is by using inheritance. It is indicated with the keyword extends in the class header and can be either for classes or interfaces. In case of classes we have a relation between the superclass and the subclass (which is just a special type of supertype/subtype relation). Instance variables and method implementations are inherited from the super class (and can be called and used if they are public or protected). Methods can be overridden, redefining the implementation of certain methods. It is still possible to call the methods or constructors of the super class using the super keyword. A class can extend at most one class, but implement multiple interfaces.

Abstract classes

In some cases we want to implement part of a class, but leave some methods open for subclasses to implement. This can be achieved with an abstract class. Such as class has the keyword abstract in the class header. Methods without an implementation must be denoted with the abstract keyword as well. Other methods in the abstract class may call the abstract methods and operate on instance variables like normal classes. It is not possible to create objects of an abstract class by directly calling the constructor using new.

Object

If a class does not explicitly extend another class, it implicitly extends Object. You need to know about three methods in Object what they do: toString(), hashCode() and equals(). By default, these methods use the memory address of an object (e.g. to determine whether two objects are equal). So even if two separate objects have the same values stored in their instance variables, the equals() method will return false (just as the == operator does). It can be very useful to override them, but with hashCode() and equals() you have to make sure you adhere to their contract. Many classes from the standard library override these methods already.

Collections

Comparator and Comparable

Sometimes we want to sort objects according to some order. In case a natural order is available, a class can implement the Comparable<E> interface. It requires the method public int compareTo(E other). A number of classes from the standard library do this, like String, Integer, BigInteger, Double, etc. We can also use a separate class to define an ad-hoc order using the Comparator<E> interface. This requires the method public int compare(E left, E right).

We can use the static method Collections.sort() to sort a list of elements which implement the Comparable interface. It is overloaded in two versions: one where we only pass a list and the list is sorted according to the natural order of it's elements. In second option is that we pass a Comparator as a second argument to sort according to some ad-hoc order defined by that Comparator object.. Suppose we get an int c from either left.compareTo(right) or myComp.compare(left, right)

  • If c < 0 then the object left comes before right in a sorted list.
  • If c > 0 then the object left comes after right in a sorted list.
  • If c == 0 then the objects are regarded as equal by this order.

Consider a List<Integer> with numbers 1,2, … , 10. When we call Collections.sort() on this list, the order will indeed be 1,2,3,4,5,6,7,8,9,10 (this is easy to remember). Objects of the following class will be sorted in the same way:

public class MyInteger implements Comparable<MyInteger> {

    private int value;

    @Override
    public int compareTo(MyInteger other) {
        return this.value - other.value;
    }
}

Sometimes it is also useful to call another compareTo method:

public class Student implements Comparable<Student> {

    private String name;

    @Override
    public int compareTo(Student other) {
        return name.compareTo(other.name);
    }
}

Collections Framework

A very useful framework that allows you to store and access collections of objects in a number of ways. There are three different types: Lists, Sets and Maps. The Collection<E> interface extends the Iterable<E> interface. This means that you can use the enhanced for-loop on Lists and Sets.

List

A List stores data sequentially. The same element can occur multiple times and the order of elements is part of the list semantics. An ArrayList<E> uses an array to model a list. It is very efficient to access elements anywhere in the list, but adding at the front or removing an element halfway requires a lot of elements to be moved. A LinkedList<E> uses a container object for each element, with pointers to the next and previous container. This makes adding and removing at both the front and back very efficient, but access of elements in the middle is costly.

Set

A Set<E> is used to store elements if we are only interested in knowing whether an element is or is not in the set. Elements are never repeated and the order in which elements are added has no special meaning. A SortedSet<E> is a subtype of Set<E> where the elements are stored in a particular order (either a natural order, or an order defined by a Comparator) A HashSet<E> implements Set<E> stores elements using both the hashCode() and equals() methods and requires that these are consistent. A TreeSet<E> implements SortedSet<E> stores elements in a binary tree and requires a either a Comparable or Comparator interface that is consistent with equals().

Map

A Map<K,V> is used to store key-value pairs. The keys form a set (i.e. they are unique) and every key is associated with a value currently stored in the map. There is no particular order in which the keys are stored. A HashMap<K,V> implements Map<K,V> and stores elements using both the hashCode() and equals() methods and requires that these are consistent. A SortedMap<K,V> is a sub-type of Map<K,V> and stores the key-value pairs in the order of their keys. Similar to SortedSet this can be a natural order, or an order defined by an Comparator. A TreeMap<K,V> implements SorterMap<K,V> stores elements in a binary tree and requires a either a Comparable or Comparator interface that is consistent with equals(). It is

Lambda Expressions

Functional interfaces are interface with one unimplemented method. Lambda expressions are used to implement a functional interface. The syntax is something like this: input definition -> output definition. A method reference is a special lambda expression for situations where a single method is called. The syntax looks like: ClassName::methodName or expression::methodName.

For instance:

public static void sortCoursesByTeacherLambda3(List<Course> courses) {
    Comparator<Course> comp;
    comp = (o1, o2) -> o1.getTeacher().compareTo(o2.getTeacher());
    Collections.sort(courses, comp);
}

In the following table, you can review the relation between types, method references and lambda expressions:

TypeMethod referenceLambda Expression
StaticMyClass::myStaticMethod(args) -> MyClass.myStaticMethod(args)
Boundvar::myMethod(args) -> var.myMethod(args)
UnboundMyClass::myMethod(obj, args) -> obj.myMethod(args)
ConstructorMyClass::new(args) -> new MyClass(args)
Arrayint[]::new(len) -> new int[len]

Functional interfaces

With the new built-in functional interfaces we can model common patterns, such as:

  • Doing something with an object (Consumer)
  • Checking a property/condition (Predicate)
  • Transforming an object (Function, UnaryOperator)
  • Combining two objects into one (BinaryOperator)

Here are some common functional interfaces:

InterfaceInput argumentsOutputInterpretation
Comparator(T o1, T o2)intDefine an order on T's
BinaryOperator(T left, T right)TCombine two T's into one T.
Consumer(T arg)voidDo something with argument
BiConsumer(T arg1, U arg2)voidDo something with the arguments
Function(T arg)RTransform a T into an R
BiFunction(T arg1, U arg2)RTransform a T and U into an R
Predicate(T arg)booleanCheck if a T has a property
Supplier()TProduces object of type T
UnaryOperator(T arg)TTransforms a T to a T
Runnable()voidExecute a task

They are useful to construct methods that are generic. Rather than hardcoding these tasks, we can pass them as an argument. Calling is easy due to lambda expressions and method references.

Default interface methods

In Java 8, we are allowed to write method implementations in an interface (but we can not define any instance variables). In case we do, the default keyword is mandatory. Subclasses do not need to implement the default methods (but they may override them). Functional interfaces can have any number of default methods, but must have exactly one non-default (unimplemented or abstract) method. Please have a look at the example code below:

public interface Point2D {
    public double getX(); //Traditional interface method
    public double getY(); //Traditional interface method
    public default double distanceTo(Point2D other) { // Default method
        double dx = getX() - other.getX(); // Calls to unimplemented method.
        double dy = getY() - other.getY();
        return Math.sqrt(dx*dx + dy*dy);
    }
}

Static interface methods

In Java 8, it is now also possible to define static utility methods within an interface (much like Collections.sort, or Math.max). For example, the Comparator interface has a number of useful static methods:

// Function based Comparators
public static <T> Comparator<T> comparing(Function<T,Comparable> keyExtractor)
public static <T,U> Comparator<T> comparing(Function<T,U> keyExtractor, Comparator<U> keyComparator)
// Natural order based Comparators
public static <T extends Comparable> Comparator<T> naturalOrder()
public static <T extends Comparable> Comparator<T> reverseOrder()

These can be used to quickly obtain a Comparator without creating a separate class. For example, Comparator<String> strCmp = Comparator.reverseOrder() will give you a Comparator for String objects that is a reverse alphabetical order. Comparator<Student> stdCmp = Comparator.comparing(Student::getName) gives a Comparator that orders Student objects by the natural order of the String objects returned when getName() is called on a Student object.

And also some default methods:

// Composition of Comparators
public default <T> Comparator<T> thenComparing(Comparator<T> other)
public default <T> Comparator<T> thenComparing(Function<T,Comparable> keyExtractor)
public default <T,U> Comparator<T> thenComparing(Function<T,U> keyExtractor, Comparator<U> keyComparator)

These can be used to create Comparators that look at different aspects. For example,

Comparator<Student> stdCmp = Comparator.comparing(Student::getFirstName)
                                       .thenComparing(Student::getAge, Comparator.reverseOrder());

gives you a comparator that first orders Student objects based on the alphabetical order of their first names, and if two students have the same first name, orders them from old to younger.

Writing shorter Comparators

We consider more examples of writing shorter Comparators.

We can write methods rather than classes, and then use a method reference to obtain a Comparator:

public static int compareCourses(Course left, Course right) {
    if (left.getTeacher().equals(right.getTeacher())) {
        return left.getTeacher().compareTo(right.getTeacher());
    }
    if (left.getCourseYear() != right.getCourseYear()) {
        return right.getCourseYear() - left.getCourseYear();
    }
    return left.getCourseName().compareTo(right.getCourseName());
}

public static void sortCourses(List<Course> courses) {
    Collections.sort(courses, MyClass::compareCourses);
}

Some more examples based on Comparator.comparing and thenComparing:

Comparator<Course> compTeacher = Comparator.comparing(Course::getTeacher);
Comparator<Course> compYear = Comparator.comparing(Course::getCourseYear, Comparator.reverseOrder());
Comparator<Course> compName = Comparator.comparing(Course::getCourseName);
// Combine the three separate comparators into one:
Comparator<Course> comp = compTeacher.thenComparing(compYear).thenComparing(compName);
Comparator<Course> comp = Comparator.comparing(Course::getTeacher)
                                    .thenComparing(Course::getCourseYear, Comparator.reverseOrder())
                                    .thenComparing(Course::getCourseName);
Collections.sort(courses, comp);

The Optional<T> class

An object of Optional<T> either: holds a single value of type T, or holds no value at all. It can be used to avoid returning null values and forces the caller to deal with potential absence of a result. Static methods that can be used to obtain an Optional object:

public static <T> Optional<T> empty()
public static <T> Optional<T> of(T elem)
public static <T> Optional<T> ofNullable(T elem)

So for example Optional<String> name = Optional.of("Grace") gives an Optional that holds a value, while Optional<String> missing = Optional.empty() gives you an empty Optional.

The following methods are available on an Optional object and can be used to handle the object:

public T get()
public void ifPresent(Consumer<T> action)
public boolean isPresent()
public T orElse(T alternative)

The Java 8 Stream API

Java 8 introduce the Stream API that can be used to build and process data processing pipelines in a declarative fashion. The main interface is Stream<T>. It models an (unfinished) data processing pipeline that can emit objects of type T. A pipeline consists of three types of operations:

  • A single data source
  • Zero or more intermediate operations
  • A single terminal operation Objects only start flowing through the pipeline and are actually processed when they are requested by a terminal operation. The data source and intermediate operations are called lazy operations. The terminal operation is a non-lazy operation.

The most common way to obtain a Stream is by means of a new default method that was added to the Collection interface: public default Stream<T> stream(), which you can call for example on your favorite ArrayList or HashSet objects.

The following code examples are some alternative ways to obtain Stream objects:

public default Stream<T> stream()
// A stream that will emit three string objects
Stream<String> s = Stream.of("hello", "I'm", "flowing");
// The word "hello" infinitely many times.
Stream<String> tooPolite = Stream.generate(() -> "hello");
// The numbers 0, 1, 2, etcetera
Stream<BigInteger> allInts = Stream.iterate(BigInteger.ZERO, bi -> bi.add(BigInteger.ONE));
// Obtain a Stream from a List object
List<String> lst = Arrays.asList("these", "are", "list", "elements");
Stream<String> fromLst = lst.stream();

These operations are lazy, so no objects start flowing (yet)

Intermediate operation are lazy and can be recognized from the fact the return type is also a Stream. This resulting Stream represents the data processing pipeline with an additional operation attached to it.

MethodOutputDescription
distinct()Stream<T>discards duplicate elements
filter(Predicate<T> predicate)Stream<T>discards false elements
limit(long n)Stream<T>emits at most n elements
map(Function<T,R> mapper)Stream<R>converts objects to type R
skip(long n)Stream<T>discards the next n elements
sorted()Stream<T>sorts elements by their natural order
sorted(Comparator<T> comparator)Stream<T>sorts elements with the comparator

When you call a terminal operation on a Stream, it starts consuming objects from the stream and objects finally start flowing through the processing pipeline until they reach the terminal operation.

The following operations are available:

MethodOutputDescription
allMatch(Predicate<T> predicate)booleanIs predicate true for all
anyMatch(Predicate<T> predicate)booleanIs predicate true for any
count()longNumber of objects
collect(Collector<T,A,R> collector)RAggregate using the collector
findFirst()Optional<T>First object emitted
forEach(Consumer<T> action)voidPerform action on all objects
max(Comparator<T> comparator)Optional<T>Maximum according to comparator
min(Comparator<T> comparator)Optional<T>Minimum according to comparator
noneMatch(Predicate<T> predicate)booleanIs predicate false for all
reduce(BinaryOperator<T> accumulator)Optional<T>Aggregate using the accumulator

The collect operation requires an object of type Collector<T,A,R>.

  • A collector takes objects of type T.
  • Accumulates them into a (mutable) accumulator type A
  • Transforms the final accumulation into type R There is no need to implement them by yourself (unless you want to). The Collectors class provides methods to obtain many useful collectors (similar to Comparator.comparing()). Collectors.toList() and Collectors.toSet() for contructing a List or Set dataset from the elements of a Stream. Collectors.joining() is for combining strings into a single String (with or without delimiter, prefix and suffix).
You have reached the end of this section! Continue to the next section: