Let's start the seventh post of the Java 8 series about the changes that you can find from version 6 to 8, the Collections.

You will see some subject already mentioned in the stream post, but I consider important to have a separate post to this subject.

Summary


Introduction

A collection is a group of objects contained in a single object.

The main interfaces in the Collections Framework are:

  • List: an ordered collection of elements; it allows duplicate entries. It does not limit the null elements
    • ArrayList: dynamic arrays. It's not synchronized, allow null values and maintenance the insertion order. It can not be used by primitives.
    • LinkedList: the elements are not stored side-by-side. They are linked by pointers. Each element specifies previews and next elements.
  • Set: it's not ordered collection and it does not allow duplicate entries. It allows only one null element.
    • The HashSet works like HashMap but not allow duplicate elements.
  • Queue: ordered for a process (FIFO - first-in, first-out)
  • Map: it maps keys to values; it does not allow duplicate key.
    • HashMap implements Map interface. You can see the internal working in this post on GeeksforGeeks. You will see that Java 8 change the implementation to solve collision. Now Java 8 began using linked-list and as soon as the threshold is reached the hash will change to use a balanced tree.
      • LinkedHashMap: similar to the HashMap but now you can access the elements by their insertion order.
    • HashTable also implements Map interface. The main differences between the HashTable and the HashMap are that the first one does not allow to use null value to the key or to the value and it is synchronized, which is an important reason to make the performance worst comparing with HashMap. The ConcurrentHashMap (from Executor framework) is similar to HashTable, but the ConcurrentHashMap has a better performance.  ConcurrentHashMap read data using get() without locks (the locking is applied only for updates), by another hand, the HashTable is synchronized for all operation (single lock for whole data). More detail about the differences you can see on this other GeeksforGeeks post. And an implementation you can see here.
    • TreeMap implements a child of Map interface, the SortedMap. It's useful to use with unique elements and where is necessary to have sorted elements (natural order or using Comparable interface).

HowToDoInJava - Java Collection


 

Another post also addresses the topic Java collection. If you want a little more about this go to the Marcus Biel Java Collection post in DZone.

Using the Diamond Operator

From Java 7, you can omit the type of the generic class from the right side, but the diamond operator ('<>') is required yet.

# Before
Map<Map<String,Integer>, List> map1 =
       new HashMap<Map<String,Integer>, List>();

# New Version
Map<Map<String,Integer>, List> map2 = new HashMap<>();

# DOES NOT COMPILE
Map<> map2 = new HashMap<Map<String,Integer>, List>();

The compiler will fill the type with the type declared on the right side. The diamond is limited to be used only on right side.

Iterates, filters and sorts (using lambda)

To iterate through the collection you can do this using forEach loop, the index, while loop (see this HowToDoInJava post to examples). But you don't need to navigate each element in every case. If you want to search some elements it's possible to use the binarySearch method to do this. But the list needs to be sorted.

# Array
int[] n = {6,9,1,8}
Arrays.sort(n); // [1,6,8,9]
Arrays.binarySearch(n,6)); // the result is the index '1'

# List
List l = Arrays.asList(9,7,5,3);
Collections.sort(l);
Collections.binarySearch(l,3); // the result is the index '0'

# Stream
Stream.iterate(1L, n->n*2).limit(10).forEach(System.out::print);

To sort a collection you need to know how to compare the elements. So, you can use the Comparator interface with inner class or using lambda expression. It's possible to say that Comparator is a functional interface because it has a single abstract method. If it's necessary you can use a not natural order with reverseOrderMethod.

# inner class
Comparator byWeight = new Comparator() {
    public int compare(A a1, A a2) {
         return a1.getWeight() - a2.getWeight();
    }
};

# Lambda
Comparator byWeight =
    (A a1, A a2) -> {return a1.getWeight() - a2.getWeight(); };

# Reverse Order
List l = Arrays.asList(1,2,3);
Comparator c = Comparator.reverseOrder();
Collections.binarySearch(l,3,c);

Also you can use filters in the collections. The Streams accept the Predicate interface to apply in the collection.  Example you can see here, compare before and after Java 8.

Predicate pred = e -> e % 2=0;
collection.stream.filter(pred).collect(Collectors.toList());

Java SE 8 collection improvements

Java 8 introduced new methods to help us to manipulate the collections. One of them was the removeIf method to remove an element using a condition. Another one was the replaceAll, where you pass a lambda expression and to apply to each element. You can use the forEach loop to navigate by the list and do something.

List list = Arrays.asList(1,2,3,4,5);
list.removeIf( i -> s > 3);
list.replaceAll( i -> i*2);
list.forEach( i -> System.out.println(i));

The Map also added new methods such merge that add a new element with a logic.

# merge
Map<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", null);

BiFunction<String, String, String> logic =
    (v1,v2) -> v1.length() > v2.length() ? v1:v2;

String result1 = map.merge("key1", "new", logic);
// Result1 = value1;
String result2 = map.merge("key1", "newValue1", logic);
// Result2 = newValue1;
System.out.println(map.get("key1")); // {key1=newValue1}

# putIfAbsent - add new value if not exist or value is null
map.putIfAbsent("key2", "value2");
map.putIfAbsent("key1", "value3");
System.out.println(map); // {key1=newValue1, key2=value2}

The computeIfPresent is another method to map and runs only when the key isn’t present or is null. And computeIfAbsent do the opposite.

# computeIfPresent
Map<String, Integer> total = new HashMap<>();
map.put("key1", 1);
map.put("key2", null);

BiFunction<String, String, String> logic =
  (v1,v2) -> v1 + 1;

Integer k1 = total.computeIfPresent("key1", logic); // k1 = 2
Integer k3 = total.computeIfPresent("key3", logic); // k2 = null

# computeIfAbsent
Function<String, Integer> func = k -> 99;
total.computeIfAbsent("key1", func); // 1
total.computeIfAbsent("key2", func); // 99
total.computeIfAbsent("key3", func); // 99

Streams

The stream in java is a sequence of data which you can execute operation and get a result. It's not a collection. The Stream API was introduced to process elements in sequence.  The stream wraps an existing collection to support operations expressed with lambdas, so you specify what you want to do, not how to do it.

Some differences of the stream from the collection are:

  • No storage. A stream gets each element from a source (data structure, an array, a generator function) and processes through a pipeline of computational operations.
  • Functional in nature. An operation on a stream produces a result but does not modify its source.
  • Laziness-seeking. Many stream operations can be implemented lazily.
  • Possibly unbounded. It's possible to work with infinite size using streams throughout short-circuiting operations such as limit(n) or findFirst().
  • Consumable. The elements of a stream are only visited once during the life of a stream.

 The Stream pipeline is the set of operations chained, which can be the intermediate operations and terminal operations.

  • Intermediate Stream: execute operation where is possible to manipulate the stream and the result is another stream.
  • Terminal operator: execute the operator to get a result different of the stream.

Intermediate Stream Operation

  • flatMap: "It takes each element in the stream and makes any elements it contains top-level elements in a single stream". It is usual to remove empty elements of when you are using a list with a list.
Stream.of(Arrays.asList(), Arrays.asList("a"), Arrays.asList("b"))
        .flatMap (l->l.stream()).forEach(System.out::print); //ab
  • map: "It creates a one-to-one mapping from the elements in the stream to the elements of the next step in the stream"
Stream.of("a","b","c").map(String::length).forEach(System.out::print); //111

Terminal Stream Operation

Search for data:

  • findAny/findFirst: It return a Optional< T > object. To an empty stream, the return will be an empty Optional return. It works with an infinite stream. These methods do not need to process all the elements. The findAny method is useful to work with a parallel stream.
Stream.of("a","b","c")
      .findAny().ifPresent(System.out::println); //a
  • allMatch/anyMatch/noneMatch : the return is a boolean type related to the predicate passed. The allMatch and the noneMatch methods cannot be working in an infinite stream.
Predicate<String> pred = x->Character.isLetter((x.charAt(0));
Stream.of("a","bb","cc", "1").anyMatch(pred); //

Reduction operator

  • collect: it does not terminate an infinite execution. It is a reduction operator.
  • min/max: It allows find smallest or largest value. It returns an Optional< T > object to be able to represent no value. In an infinite stream, it cannot terminate the process.
Stream.of("a","bb","cc")
    .min((s1,s2)->s1.length()- s2.length()); //a
  • count: it returns a long number the represents the number of elements in a stream.  It does not terminate execution in case of the infinite stream.
Stream.of ("a","b","c").count(); // 3


Group the results

  • averagingDouble/averagingInt/averagingLong: return the average to the collect
    •   System.out.print(Stream.of ("a","b","c")
           .collect(Collectors.averagingInt(String::length))); // (1+1+1)/3
        
  • joining: it creates a single string.
    •   System.out.print(Stream.of ("a","b","c")
                .collect(Collectors.joining(","))); // a,b,c
        
  • groupingBy: it will create a map using a Function.
    •   Map<Integer, List<String>> map = Stream.of ("a","b","cc","dd")
          .collect(Collectors.groupingBy(String::length));
        System.out.print(map); // {1=[a,b], 2=[cc,dd]}
        
  • partitioningBy: it will create a map using a Predicate.
    •   Map<Boolean, List<String>> map = Stream.of ("a","b","cc","dd")
          .collect(Collectors.partitioningBy(s->s.length() <2 ));
        System.out.print(map); // {false=[cc,dd], true= [a,b]}
        

Optional With Primitive Streams

  • average: it returns a optional type to primitive (ex. OptionalDouble)
    • IntStream.rangeClosed(1,10).average();
  • sum: It does not return optional type
    •   IntStream.rangeClosed(1,10).sum();
        
  • min:/max:
    • IntStream.rangeClosed(1,10).min();

Related Posts

Reference