Skip to content

Instantly share code, notes, and snippets.

@samuelowino
Last active November 3, 2023 20:17
Show Gist options
  • Save samuelowino/fd6fbdb55e89ec16ceac94672b4af203 to your computer and use it in GitHub Desktop.
Save samuelowino/fd6fbdb55e89ec16ceac94672b4af203 to your computer and use it in GitHub Desktop.
OG:3

Old Guard EP3: Java Advanced

History of Java Collections API

The Java Collections API is a set of interfaces and classes that provide high-quality, reusable data structures and algorithms for storing and manipulating collections of objects. It was introduced as part of the Java 2 Platform, Standard Edition (J2SE) in 1998.

Java 1.2 (December 1998)

  • The Collections API was introduced in Java 1.2.
  • It included interfaces like Collection, List, Set, Map, etc., along with their respective implementations.
  • Notable classes included ArrayList, LinkedList, HashSet, HashMap, etc.

Java 1.4 (February 2002)

  • Java 1.4 introduced the java.util.concurrent package which included concurrent versions of collections like ConcurrentHashMap and CopyOnWriteArrayList to support multithreading.

Java 5 (September 2004)

  • Java 5 (also known as J2SE 5.0 or Java 1.5) brought significant enhancements with the introduction of generics.
  • With generics, it became possible to specify the type of objects that a collection can hold. This added compile-time type safety.
  • New interfaces were introduced like Iterable, Iterator, and Enumeration.

Java 6 (December 2006)

  • Java 6 didn't introduce many changes to the Collections API. It mainly focused on other aspects of the Java platform.

Java 7 (July 2011)

  • Java 7 brought a few small improvements, such as the diamond operator (<>) which simplified the creation of generic instances.

Java 8 (March 2014)

  • Java 8 introduced the Stream API and the java.util.stream package, which provided functional-style operations for collections, such as map, reduce, filter, etc. This was a significant paradigm shift towards functional programming in Java.

Java 9 (September 2017)

  • Java 9 didn't introduce major changes to the Collections API. It was focused more on other aspects of the language and platform.

Java 10 (March 2018)

  • Java 10 didn't introduce significant changes to the Collections API.

Java 11 (September 2018)

  • Java 11, like the previous versions, did not introduce major changes to the Collections API.

Java 12, 13, 14, 15, and 16 (2019-2021)

  • These versions mainly focused on other aspects of the Java language and platform, and the Collections API remained relatively stable.

Java 17 (September 2021)

  • Java 17 continued the trend of stability for the Collections API.

Throughout its history, the Collections API has been a critical part of Java, providing a rich set of data structures and algorithms that form the backbone of many Java applications. The API has been influential in promoting good software engineering practices, such as code reusability and maintainability.


The java.util.concurrent package, introduced in Java 5 (JDK 1.5), provided enhancements to the Java Collections Framework to support concurrent programming. This package includes classes that allow safe concurrent access to collections, which is crucial in multithreaded environments where multiple threads may try to access or modify the same collection simultaneously.

  1. Thread-Safe Collections:

    • It introduced thread-safe versions of standard collections like ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteArraySet, etc.
    • These collections use various synchronization mechanisms to ensure that they can be safely accessed by multiple threads concurrently.
  2. Improved Performance in Concurrent Scenarios:

    • CopyOnWriteArrayList and CopyOnWriteArraySet provide a mechanism where the collection is copied every time it is modified. This means that while one thread is modifying the collection, other threads can still read from it without any locks. This can lead to improved performance in scenarios where reads are much more frequent than writes.
  3. Lock-Free Algorithms:

    • ConcurrentHashMap uses lock-free algorithms to allow multiple threads to read and write concurrently. This can lead to better performance in scenarios with a high level of concurrency.
  4. Atomic Operations:

    • Classes like AtomicInteger, AtomicLong, and AtomicReference were introduced, which provide atomic operations without the need for explicit synchronization.
  5. Blocking Queues:

    • Classes like LinkedBlockingQueue and ArrayBlockingQueue provide thread-safe implementations of blocking queues, which are essential in many concurrent applications.
  6. ForkJoinPool:

    • ForkJoinPool is a specialized executor service introduced in Java 7 that is designed to efficiently run tasks that can be broken down into smaller subtasks. It is especially useful in recursive algorithms like divide-and-conquer.
  7. Lock Framework:

    • The Lock interface and its various implementations (like ReentrantLock) were introduced to provide more flexible locking mechanisms compared to synchronized blocks.
  8. Atomic Variables:

    • Classes like AtomicInteger, AtomicLong, and AtomicReference provide atomic operations for variables, reducing the need for explicit synchronization.

Java Predicate is a functional interface that represents a boolean-valued function. It can be used to define conditions or filters.

  1. Filtering a List:
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("John", "Jane", "Bob", "Alice", "Eve");

        Predicate<String> startsWithJ = name -> name.startsWith("J");
        
        List<String> filteredNames = filterList(names, startsWithJ);

        System.out.println(filteredNames); // Output: [John, Jane]
    }

    public static <T> List<T> filterList(List<T> list, Predicate<T> predicate) {
        return list.stream().filter(predicate).toList();
    }
}
  1. Checking for Null:
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<String> isNotNull = s -> s != null;

        String name = "John";
        System.out.println(isNotNull.test(name)); // Output: true

        String nullName = null;
        System.out.println(isNotNull.test(nullName)); // Output: false
    }
}
  1. Combining Predicates:
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> isEven = num -> num % 2 == 0;
        Predicate<Integer> isPositive = num -> num > 0;

        int num = 6;
        boolean result = isEven.and(isPositive).test(num);

        System.out.println(result); // Output: true
    }
}
  1. Negating a Predicate:
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<String> isNotEmpty = s -> !s.isEmpty();

        String emptyString = "";
        System.out.println(isNotEmpty.test(emptyString)); // Output: false

        Predicate<String> isEmpty = isNotEmpty.negate();
        System.out.println(isEmpty.test(emptyString)); // Output: true
    }
}
  1. Using Predicates with Streams:
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        Predicate<Integer> isEven = num -> num % 2 == 0;

        List<Integer> evenNumbers = numbers.stream()
                                           .filter(isEven)
                                           .toList();

        System.out.println(evenNumbers); // Output: [2, 4, 6, 8, 10]
    }
}
  1. Filtering Elements:

    List<String> names = Arrays.asList("John", "Jane", "Bob", "Alice", "Eve");
    
    List<String> filteredNames = names.stream()
                                        .filter(name -> name.startsWith("J"))
                                        .collect(Collectors.toList());
    
    System.out.println(filteredNames); // Output: [John, Jane]

    Output: [John, Jane]

  2. Mapping Elements:

    List<String> names = Arrays.asList("John", "Jane", "Bob", "Alice", "Eve");
    
    List<Integer> nameLengths = names.stream()
                                      .map(String::length)
                                      .collect(Collectors.toList());
    
    System.out.println(nameLengths); // Output: [4, 4, 3, 5, 3]

    Output: [4, 4, 3, 5, 3]

    Cimbined 1 & 2

    public class Main {
     public static void main(String[] args) {
         List<String> names = Arrays.asList("John", "Jane", "Bob", "Alice", "Eve");
    
         List<Integer> filteredNamesLength = names.stream()
                 .filter(name -> name.startsWith("J"))
                 .map(String::length)
                 .collect(Collectors.toList());
    
         System.out.println("Filtered length: " +  filteredNamesLength);
     }

}

Output: Filtered length: [4, 4]

   

3. **Reducing Elements**:

   ```java
   List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

   int sum = numbers.stream()
                    .reduce(0, Integer::sum);

   System.out.println(sum); // Output: 15

Output: 15

  1. Collecting Elements:

    List<String> names = Arrays.asList("John", "Jane", "Bob", "Alice", "Eve");
    
    String concatenatedNames = names.stream()
                                    .collect(Collectors.joining(", "));
    
    System.out.println(concatenatedNames); // Output: John, Jane, Bob, Alice, Eve

    Output: John, Jane, Bob, Alice, Eve

  2. Finding Max/Min Element:

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    
    Optional<Integer> maxNumber = numbers.stream()
                                         .max(Comparator.naturalOrder());
    
    System.out.println(maxNumber.orElse(-1)); // Output: 5

    Output: 5

  3. Grouping Elements:

    List<String> names = Arrays.asList("John", "Jane", "Bob", "Alice", "Eve");
    
    Map<Character, List<String>> groupedNames = names.stream()
                                                     .collect(Collectors.groupingBy(name -> name.charAt(0)));
    
    System.out.println(groupedNames); // Output: {J=[John, Jane], B=[Bob], A=[Alice], E=[Eve]}

    Output: {J=[John, Jane], B=[Bob], A=[Alice], E=[Eve]}

  4. Parallel Processing:

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    
    int sum = numbers.parallelStream()
                     .reduce(0, Integer::sum);
    
    System.out.println(sum); // Output: 15

    Output: 15

  5. Creating Infinite Streams:

    Stream.iterate(1, n -> n + 1)
          .limit(10)
          .forEach(System.out::println);

    Output: This will print the numbers 1 through 10.

  6. Comparator with .thenComparing

import java.util.Arrays;
import java.util.List;
import java.util.Comparator;

class Person {
    String name;
    int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class Main {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
                new Person("John", 30),
                new Person("Jane", 25),
                new Person("Bob", 30),
                new Person("Alice", 28),
                new Person("Eve", 25)
        );

        people.stream()
                .sorted(Comparator.comparing(Person::getAge).thenComparing(Person::getName))
                .forEach(System.out::println);
    }
}

Output

Person{name='Jane', age=25}
Person{name='Eve', age=25}
Person{name='Alice', age=28}
Person{name='Bob', age=30}
Person{name='John', age=30}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment