Generics and Collections

Create and use a generic class

Generics introduce the concept of a type variable. Type variables are delimited by angle brackets and follow the class (or the interface) name:

class Test<T> {
  void m(T arg) { /** Do something with the argument of type T */ }
}

Type variables are parameters that provide information to the compiler so it can check types. In the example above, class Test works with an object of type T, the same type that takes method m. Here's how we will use it:

Test<String> t1 = new Test<>();
t1.m("a");

Test<Integer> t2 = new Test<>();
t2.m(1);

Notice that the generic parameter on the right is optional, it can be inferred by the compiler using the reference declaration.

So generics are a mechanism for type checking, for example, when we create an instance of List<String>:

List<String> l = new ArrayList<String>();
l.add("a");
l.add("b");

If we tried to put some other type of object different than String, the compiler would raise an error:

l.add(1); //Compile-time error

To get a value from the collection:

String s = l.get(0);

To iterate the collection:

for(String s : l) {
    System.out.println(s);
}

Now consider this object hierarchy:

public class Animal { }
public class Dog extends Animal { }
public class Cat extends Animal { }

There are three generic wildcard operators:

List<?>           l1 = new ArrayList<>();
List<? extends Animal> l2 = new ArrayList<>();
List<? super   Animal> l3 = new ArrayList<>();

The unknown wildcard (List<?>) means the list is typed to an unknown type, so the list could hold any type. Since we don't know the type of the List, you can't add elements to the collection, you can only read from the collection and treat the objects as Object instances:

List<?> l = new ArrayList<>();
for(Object o : l){
      System.out.println(o);
}

The extends wildcard (List<? extends Animal>) means the list can hold objects that are instances of Animal, or subclasses of Animal (e.g. Dog and Cat). Again, you cannot insert elements into the list, because you don't know the exact type of the list. But you can get the elements from the collection because you can safely say that its elements are of instances or subclasses of Animal:

List<? extends Animal> l = new ArrayList<>();
for(Animal a : l){
      System.out.println(a);
}

The super wildcard (List<? super Animal>) means the list is typed to either the Animal class, or a superclass of Animal. This time, you can insert elements into the list because you know that the list is typed to either Animal or a superclass of Animal, so it is safe to insert instances of Animal or subclasses of Animal (like Dog or Cat) because they are an Animal*:

List<? super Animal> l = new ArrayList<>();
l.add(new Animal());
l.add(new Dog());
l.add(new Cat());

However, to get elements from the list, you must cast the result to Object, since the elements could be of any type that is either an Animal or a superclass, but it is not possible to know exactly which. The only thing you know for sure is that any class is a subclass of Object:

List<? super Animal> l = new ArrayList<>();
for(Object o : l){
      System.out.println(o);
}

 

Create and use ArrayList, TreeSet, TreeMap, and ArrayDeque objects

ArrayList

ArrayList is an implementation of the List interface that internally uses an Array to store the elements. This implementation is not synchronized.

Each ArrayList instance has a capacity. The capacity is the size of the array used to store the elements in the list. It is always at least as large as the list size. As elements are added to an ArrayList, its capacity grows automatically.

Here's the javadoc and an example:

public class Test {
  public static void main(String[] args) {
      List<Integer> l = new ArrayList<>();

      // Add elements
      l.add(1);
      l.add(2);
      l.add(3);

      // Get size.
      int count = l.size();

      // Get an element with the zero-based index.
      Integer i = l.get(0);
  }
}

TreeSet

TreeSet is an implementation of the Set interface that uses a tree for storage, which makes access time very fast. The elements are ordered using their natural ordering, or by a Comparator provided at set creation time, depending on which constructor is used. This implementation is not synchronized.

Here's the javadoc and an example:

public class Test {
  public static void main(String[] args) {
      Set<Integer> ts = new TreeSet<>();

      // Add elements
      ts.add(2);
      ts.add(1);
      ts.add(3);

      // Get size.
      System.out.println(ts); // Prints [1,2,3]
  }
}

TreeMap

TreeMap is an implementation of the Map interface that uses a tree for storage key/value pairs, which makes access time very fast. The elements are ordered using the natural ordering or their keys, or by a Comparator provided at map creation time, depending on which constructor is used. This implementation is not synchronized.

Here's the javadoc and an example:

public class Test {
  public static void main(String[] args) {
        Map<String, Integer> tm = new TreeMap<>();
      // Put elements to the map
      tm.put("A", 10);
      tm.put("C", 40);
      tm.put("B", 20);

        // Get a set of the entries
      Set<Entry<String, Integer>> set = tm.entrySet();
      // Get an iterator
      Iterator<Entry<String, Integer>> i = set.iterator();
      // Display elements
      while(i.hasNext()) {
         Entry<String, Integer> me = i.next();
         System.out.print(me.getKey() + ": ");
         System.out.println(me.getValue());
      }

      // Get an element
      Integer i = tm.get("C"));
  }
}

ArrayDeque

ArrayDeque is an implementation of the Deque interface. Array deques have no capacity restrictions; they grow as necessary to support usage. They are not thread-safe. Null elements are prohibited. This class and its iterator implement all of the optional methods of the Collection and Iterator interfaces. Elements are stored in the order (first or last) in which they are inserted.

Here's the javadoc and an example:

public class Test {
  public static void main(String[] args) {
      Deque<Integer> d = new ArrayDeque();

    //Add elements
    d.add(1); //add element at tail
    d.addFirst(2); //add element at head
    d.addLast (3); //add element at tail

    //Get elements
    Integer firstElement1 = d.element(); //peek at the element at the head without taking the element out of the queue (throws exception is the queue is empty)
    Integer firstElement2 = d.peek(); //peek at the element at the head without taking the element out of the queue (returns null is the queue is empty)
    Integer firstElement3 = d.getFirst();//get first element (throws exception is the queue is empty)
    Integer firstElement4 = d.peekFirst();//get first element (returns null is the queue is empty)
    Integer lastElement1  = d.getLast();//get last element (throws exception is the queue is empty)
    Integer lastElement2  = d.peekLast();//get last element (returns null is the queue is empty)

    //Remove elements
    Integer element1 = d.remove(); //retrieves and removes the head of the queue
    Integer element2 = d.removeFirst(); //retrieves and removes the first element of the queue
    Integer element3  = d.removeLast(); //retrieves and removes the last element of the queue
  }
}

 

Use java.util.Comparator and java.lang.Comparable interfaces

compareTo(obj) is the method of the Comparable interface that is called on one object, to compare it to another object, so the object to be compared has to implement this interface.

String s = "hello";
int result = s.compareTo("world");

compare(obj1, obj2) is the method of the Comparator interface that is called on some object to compare two other objects, so a utility class has to implement this interface to be used somewhere else.

Comparator<String> comp = new MyComparator<>();
int result = comp.compare("hello", "world");

Java classes that have a natural ordering implement the Comparable interface (like String, Integer, BigInteger, etc).

The Comparator interface is typically used for sorting data structures such as TreeMap and TreeSet or to be passed to a sort method (such as Collections.sort or Arrays.sort).

Both methods return: 0 if the objects are equal (this have to be consistent with the equals() method) -1 if the first object (or the object making the comparison) is "less" than the other object 1 if the first object (or the object making the comparison) is "greater" than the other object

Here's and example:

class Person implements Comparable<Person> {
    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int compareTo(Person p) {
        if (this.getAge() > p.getAge())
            return 1;
        else if (this.getAge() < p.getAge())
            return -1;
        else
            return 0;
    }
}

class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        int age1 = p1.getAge();
        int age2 = p2.getAge();

        if (age1 > age2) {
            return 1;
        } else if (age1 < age2) {
            return -1;
        } else {
            return 0;
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Pesron p1 = new HDTV(60, "James");
        Person p2 = new HDTV(55, "Bryan");

        if (p1.compareTo(p2) > 0) {
            System.out.println(p1.getName() + " is older.");
        } else {
            System.out.println(p2.getName() + " is older.");
        }

        //Sorted by age
        List<Person> l = new ArrayList<>();
        l.add(p1);
        l.add(p2);

        Collections.sort(l, new AgeComparator());
        for (Person p : l)
          System.out.println(p.getName());
    }
}

Output:

James is older
Bryan
James

 

Collections Streams and Filters

A stream represents a sequence of elements. It supports different kind of operations to perform computations upon those elements.

In Java 8, collections have methods that return a stream, for example, List and Set support the new methods stream() and parallelStream() to either create a sequential or a parallel stream.

But we don't have to create collections in order to work with streams:

Stream.of("a1", "a2", "a3")
    .forEach(System.out::println);

Stream.of() will create a stream from object references.

Besides regular object streams, Java 8 brings special kinds of streams for working with the primitive data types, such as IntStream, LongStream and DoubleStream.

IntStreams for example, can replace the regular for loop using IntStream.range():

IntStream.range(1, 10)
    .forEach(System.out::println);

Streams cannot be reused. As soon as you call any terminal operation the stream is closed. Terminal operations return either a void or non-stream result, like the forEach() method. To overcome this limitation, we have to create a new stream chain for every terminal operation we want to execute.

Before Java 8, we used for loops or iterators to iterate through the collections and filter them. In Java 8, stream operations have methods like foreach, map, filter, etc. which internally iterates through the elements. For example:

List<String> names = newArrayList<>();
for(Employee e : employees) {
  if(e.getName().startsWith("C")) {
    names.add(e.getName());
  }
}

Now, the line below is doing exactly the same thing but using stream and a filter:

List<String> names = employees.stream().filter(e -> e.getName().startsWith("A")).collect(Collectors.toList());

 

Iterate using forEach methods of Streams and List

In Java 8, collections that implement Iterable (such as List and Set) now have a forEach() method. This method takes as a parameter a functional interface, therefore, the parameter can be a lambda expression:

List<String> l = new ArrayList<>();

l.add("A");
l.add("B");
l.add("C");
l.add("D");

l.forEach(i -> System.out.println(i));

Using Stream's forEach() method is almost the same:

List<String> l = new ArrayList<>();

l.add("A");
l.add("B");
l.add("C");
l.add("D");

l.stream().forEach(i -> System.out.println(i));

The advantage of using a stream is that we can perform operations on the elements of the collection before the iteration.

 

Describe Stream interface and Stream pipeline

A stream is a sequence of elements supporting sequential and parallel aggregate operations. A stream is not a data structure that stores elements. Instead, it just carries values from a source through a pipeline. Here's the javadoc.

A stream pipeline is just a sequence of aggregate operations. A stream pipeline consists of:

  • A stream source, like a collection
  • Intermediate operations that transform the stream and produce a new stream, like filter() and
  • A terminal operation that either produces a result or calls the forEach() method.

An example of a pipeline that consists of the aggregate operations filter and count:

long count = words.stream()
                  .filter(w -> w.endsWith("ly"))
                  .count();

 

Filter a collection by using lambda expressions

The way to filter collections is through the use of a Predicate, which is basically something that returns a boolean value.

Predicate is a functional interface, which means that wherever an implementation of this interface is expected, we can pass a lambda expression.

For example, consider a list of objects of this class:

class Person {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

To filter the list, first we need to convert it to a stream and then pass a lambda expression that returns a boolean value to its filter() method:

List filterd = l.stream().filter(p -> p.getAge() > 20).collect(Collectors.toList());

But what if we want to use multiple conditions to filter the list? The Predicate interface has some methods to join conditions:

Predicate<Person> nameNotNull = p -> p.getName() != null;
Predicate<Person> ageAbove20 = p -> p.getAge() > 20;

Predicate<Person> multipleConditions = nameNotNull.and(ageAbove20);
List filterd = l.stream().filter(multipleConditions).collect(Collectors.toList());

 

Use method references with Streams

When the body of a lambda expression is used to execute a method, like in this example:

List<String> l = new ArrayList<>();
l.stream().forEach(s -> System.out.println(s));

We can substitue the lambda expression with a method reference like this:

List<String> l = new ArrayList<>();
l.stream().forEach(System.out::println);

Notice how :: is used to call the method and that no parameters are passed, since the compiler infers them along with their type.

There are four types of method references (assuming a class named Person with a getName() method and a variable named p of that type):

Type Example
Reference to a static method Math::square
Reference to a constructor Integer::new
Reference to an instance method of an arbitrary object of a particular type Person::getName
Reference to an instance method of a particular object p::getName

Here's a comparison between a lambda expression and the equivalent method expression:

Lambda Expression Method Reference
n -> Math.square(n) Math::square
i -> new Integer(i) Integer::new
p -> p.getName() Person::getName
p -> p.getName() p::getName

 

Content