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);
}
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 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 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 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
}
}
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
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());
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.
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:
filter()
andforEach()
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();
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());
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 |