Back to Spliterator
Q2Question 2 of 2

What are Spliterator characteristics?

Key Concepts
  • Spliterator characteristics are bit flags that describe properties of the data source: ORDERED, SIZED, SORTED, DISTINCT, NONNULL, IMMUTABLE, CONCURRENT, SUBSIZED.
  • SIZED means the exact element count is known upfront, enabling the stream to pre-allocate arrays for toArray() without resizing.
  • SORTED means elements are already sorted, so a sorted() operation in the stream is a no-op.
  • DISTINCT means no duplicates exist, so a distinct() operation in the stream is a no-op.
  • ORDERED means elements have a defined encounter order, which affects forEachOrdered() in parallel streams.
  • Different collections report different characteristics: ArrayList is ORDERED + SIZED, HashSet is DISTINCT + SIZED, TreeSet is ORDERED + SORTED + DISTINCT + SIZED.
  • Stream operations can add or remove characteristics: filter() removes SIZED, sorted() adds SORTED, distinct() adds DISTINCT.

Definition

Spliterator characteristics are bit flags that describe data source properties like ordering, sizing, and uniqueness, enabling the stream pipeline to skip redundant operations and pre-allocate storage.
  • ORDERED (0x10) – elements have a defined encounter order; List and LinkedHashSet report this; HashSet and HashMap do not
  • DISTINCT (0x01) – all elements are unique; Set implementations report this; the stream can skip distinct()
  • SORTED (0x04) – elements are sorted in natural or comparator order; TreeSet and TreeMap report this; the stream can skip sorted()
  • SIZED (0x40) – exact element count is known; enables pre-allocation in toArray() and collect()
  • SUBSIZED (0x4000) – splits also have known sizes; ArrayList reports this, enabling balanced parallel splits
  • NONNULL (0x100) – no null elements; ConcurrentHashMap reports this
  • IMMUTABLE (0x400) – source cannot be modified; List.of() collections report this
  • CONCURRENT (0x1000) – source supports concurrent modification safely; ConcurrentHashMap reports this

At National Bank of India, characteristics are like labels on a batch of transaction receipts. ORDERED means "these receipts are in chronological order". SORTED means "these receipts are already sorted by amount". DISTINCT means "no duplicate receipt numbers". SIZED means "we know there are exactly 1000 receipts". When the processing pipeline sees SORTED, it skips the sorting step. When it sees SIZED, it pre-allocates exactly 1000 result slots instead of resizing.

Understanding the Concepts

What are Spliterator characteristics?

Characteristics are integer bit flags returned by spliterator.characteristics(). They describe structural and semantic properties of the data source. The stream pipeline inspects these flags before executing operations to determine whether certain steps can be skipped or optimised.

JAVA
package com.bank.demo;

import java.util.*;

public class CharacteristicsDemo {
    public static void main(String[] args) {
        // ArrayList: ORDERED, SIZED, SUBSIZED
        printCharacteristics("ArrayList", new ArrayList<>(List.of("SAV2001", "CUR3001")));

        // HashSet: DISTINCT, SIZED
        printCharacteristics("HashSet", new HashSet<>(List.of("SAV2001", "CUR3001")));

        // TreeSet: ORDERED, DISTINCT, SORTED, SIZED
        printCharacteristics("TreeSet", new TreeSet<>(List.of("SAV2001", "CUR3001")));

        // LinkedHashSet: ORDERED, DISTINCT, SIZED
        printCharacteristics("LinkedHashSet", new LinkedHashSet<>(List.of("SAV2001", "CUR3001")));

        // List.of: ORDERED, SIZED, SUBSIZED, IMMUTABLE
        printCharacteristics("List.of", List.of("SAV2001", "CUR3001"));
    }

    static void printCharacteristics(String name, Collection<?> coll) {
        Spliterator<?> sp = coll.spliterator();
        int chars = sp.characteristics();
        List<String> flags = new ArrayList<>();

        if ((chars & Spliterator.ORDERED) != 0) flags.add("ORDERED");
        if ((chars & Spliterator.DISTINCT) != 0) flags.add("DISTINCT");
        if ((chars & Spliterator.SORTED) != 0) flags.add("SORTED");
        if ((chars & Spliterator.SIZED) != 0) flags.add("SIZED");
        if ((chars & Spliterator.SUBSIZED) != 0) flags.add("SUBSIZED");
        if ((chars & Spliterator.NONNULL) != 0) flags.add("NONNULL");
        if ((chars & Spliterator.IMMUTABLE) != 0) flags.add("IMMUTABLE");
        if ((chars & Spliterator.CONCURRENT) != 0) flags.add("CONCURRENT");

        System.out.println(name + ": " + flags);
    }
}
TEXT
ArrayList: [ORDERED, SIZED, SUBSIZED]
HashSet: [DISTINCT, SIZED]
TreeSet: [ORDERED, DISTINCT, SORTED, SIZED]
LinkedHashSet: [ORDERED, DISTINCT, SIZED]
List.of: [ORDERED, SIZED, SUBSIZED, IMMUTABLE]

How do characteristics enable stream optimisations?

The stream pipeline reads characteristics before executing operations. If the source already satisfies a property, the corresponding stream operation becomes a no-op.

| Stream operation | Optimisation | Required characteristic |
|---|---|---|
| sorted() | Skipped (already sorted) | SORTED |
| distinct() | Skipped (already distinct) | DISTINCT |
| toArray() | Pre-allocates exact array | SIZED |
| count() | Returns estimateSize directly | SIZED |
| forEachOrdered() | Must maintain order | ORDERED |
| unordered() | Can drop ordering guarantee | Removes ORDERED |

JAVA
package com.bank.demo;

import java.util.Set;
import java.util.TreeSet;

public class StreamOptimisationDemo {
    public static void main(String[] args) {
        // TreeSet is SORTED + DISTINCT
        Set<String> accounts = new TreeSet<>(Set.of("FD4001", "SAV2001", "CUR3001"));

        // sorted() is a no-op: TreeSet is already SORTED
        // distinct() is a no-op: TreeSet is already DISTINCT
        long count = accounts.stream()
            .sorted()     // no-op (SORTED)
            .distinct()   // no-op (DISTINCT)
            .count();     // uses SIZED: returns estimateSize directly

        System.out.println("Count: " + count);

        // ArrayList is SIZED: count() is O(1)
        var list = java.util.List.of("SAV2001", "CUR3001", "FD4001");
        long listCount = list.stream().count(); // O(1): returns size directly
        System.out.println("List count: " + listCount);
    }
}
TEXT
Count: 3
List count: 3

How do stream operations change characteristics?

Stream intermediate operations can add or remove characteristics as data flows through the pipeline.

JAVA
package com.bank.demo;

import java.util.List;
import java.util.Spliterator;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class CharacteristicsChangeDemo {
    public static void main(String[] args) {
        List<String> accounts = List.of("SAV2001", "CUR3001", "FD4001", "SAV2001");

        // Original: ORDERED, SIZED, SUBSIZED, IMMUTABLE
        System.out.println("Source SIZED: " + accounts.spliterator().hasCharacteristics(Spliterator.SIZED));

        // After filter: SIZED is REMOVED (count unknown after filtering)
        Stream<String> filtered = accounts.stream().filter(a -> a.startsWith("SAV"));
        Spliterator<String> filteredSp = filtered.spliterator();
        System.out.println("After filter SIZED: " + filteredSp.hasCharacteristics(Spliterator.SIZED));

        // After sorted: SORTED is ADDED
        Stream<String> sorted = accounts.stream().sorted();
        Spliterator<String> sortedSp = sorted.spliterator();
        System.out.println("After sorted() SORTED: " + sortedSp.hasCharacteristics(Spliterator.SORTED));

        // After distinct: DISTINCT is ADDED
        Stream<String> distinct = accounts.stream().distinct();
        Spliterator<String> distinctSp = distinct.spliterator();
        System.out.println("After distinct() DISTINCT: " + distinctSp.hasCharacteristics(Spliterator.DISTINCT));
    }
}
TEXT
Source SIZED: true
After filter SIZED: false
After sorted() SORTED: true
After distinct() DISTINCT: true

Which collections have which characteristics?

| Collection | ORDERED | SIZED | SORTED | DISTINCT | SUBSIZED | IMMUTABLE |
|---|---|---|---|---|---|---|
| ArrayList | Yes | Yes | No | No | Yes | No |
| LinkedList | Yes | Yes | No | No | No | No |
| HashSet | No | Yes | No | Yes | No | No |
| LinkedHashSet | Yes | Yes | No | Yes | No | No |
| TreeSet | Yes | Yes | Yes | Yes | No | No |
| List.of() | Yes | Yes | No | No | Yes | Yes |
| ConcurrentHashMap | No | No | No | Yes | No | No |

What are common mistakes?

Mistake: Expecting parallel forEachOrdered to be fast

JAVA
// WRONG assumption: forEachOrdered is as fast as forEach in parallel
accounts.parallelStream()
    .forEachOrdered(System.out::println); // must wait for correct order!
// Parallel threads must coordinate to maintain ORDERED sequence.
// This often makes parallel forEachOrdered SLOWER than sequential.
JAVA
// CORRECT: use forEach if order does not matter
accounts.parallelStream()
    .forEach(System.out::println); // unordered, faster

Mistake: Calling sorted() on an already-sorted TreeSet stream

JAVA
// INEFFICIENT but not wrong: the stream recognises SORTED and skips the sort
new TreeSet<>(accounts).stream()
    .sorted()   // no-op because source is SORTED
    .toList();
// But it is confusing to readers. Remove the redundant sorted() call.

What about ORDERED and parallel streams?

The ORDERED characteristic affects how parallel streams process and merge results. If a source is ORDERED (like ArrayList), the stream maintains encounter order even in parallel. This requires coordination between threads, which can reduce parallelism benefits.

JAVA
package com.bank.demo;

import java.util.List;
import java.util.Set;
import java.util.HashSet;

public class OrderedParallelDemo {
    public static void main(String[] args) {
        List<String> orderedSource = List.of("SAV2001", "CUR3001", "FD4001");
        Set<String> unorderedSource = new HashSet<>(orderedSource);

        // ORDERED source: limit() must return first N in encounter order
        System.out.println("Ordered limit(2): " +
            orderedSource.parallelStream().limit(2).toList());

        // Unordered source: limit() can return any N elements
        System.out.println("Unordered limit(2): " +
            unorderedSource.parallelStream().limit(2).toList());
    }
}
TEXT
Ordered limit(2): [SAV2001, CUR3001]
Unordered limit(2): [CUR3001, FD4001]

What should a senior engineer know?

  • SUBSIZED for balanced splitting. SUBSIZED means that splits produce Spliterators with known sizes. ArrayList reports SUBSIZED because splitting by index range always produces known-size halves. This enables the ForkJoinPool to create balanced work distribution.
  • Custom Spliterator characteristics. When implementing a custom Spliterator, report accurate characteristics. Over-reporting (claiming SORTED when not sorted) causes bugs. Under-reporting (not claiming SIZED when size is known) prevents optimisations.
  • Stream.unordered(). Calling stream.unordered() removes the ORDERED characteristic, allowing the pipeline to use order-independent optimisations like faster distinct() with ConcurrentHashMap.
  • Characteristics propagation. The stream pipeline tracks characteristics through each intermediate operation. Understanding this explains why count() is O(1) on a sized stream but O(n) after a filter().

What are common interview follow-up questions?

  • "What does SIZED enable?" : Pre-allocation of result arrays in toArray(), O(1) count(), and balanced parallel splitting.
  • "What does SORTED enable?" : The stream skips the sorted() operation because the source is already in order.
  • "Why does filter() remove SIZED?" : Because the number of elements passing the filter is unknown at pipeline construction time.
  • "What is the difference between ORDERED and SORTED?" : ORDERED means elements have a defined encounter order (like insertion order). SORTED means elements are in a comparator-defined order. A LinkedHashSet is ORDERED but not SORTED. A TreeSet is both.

Visual Overview

DIAGRAM
  Characteristics and Stream Optimizations
  ==========================================

  Collection Characteristics:
  ┌────────────────┬──────────┬───────┬────────┬──────────┐
  │ Collection      │ ORDERED  │ SIZED │ SORTED │ DISTINCT │
  ├────────────────┼──────────┼───────┼────────┼──────────┤
  │ ArrayList       │    Y     │   Y   │   N    │    N     │
  │ HashSet         │    N     │   Y   │   N    │    Y     │
  │ TreeSet         │    Y     │   Y   │   Y    │    Y     │
  │ LinkedHashSet   │    Y     │   Y   │   N    │    Y     │
  └────────────────┴──────────┴───────┴────────┴──────────┘

  Stream Operations vs Characteristics:
  ┌─────────────────────────────────────────────────────┐
  │ Source: TreeSet (ORDERED, SIZED, SORTED, DISTINCT)  │
  │                                                      │
  │ .sorted()   -> SORTED present -> NO-OP              │
  │ .distinct() -> DISTINCT present -> NO-OP            │
  │ .count()    -> SIZED present -> O(1)                │
  └─────────────────────────────────────────────────────┘

  ┌─────────────────────────────────────────────────────┐
  │ Source: ArrayList (ORDERED, SIZED)                   │
  │                                                      │
  │ .filter()   -> SIZED REMOVED                        │
  │ .sorted()   -> SORTED ADDED                         │
  │ .distinct() -> DISTINCT ADDED                       │
  │ .count()    -> SIZED gone -> O(n) must count        │
  └─────────────────────────────────────────────────────┘

Key Takeaway

Spliterator characteristics are metadata flags that the stream pipeline uses to optimise execution. SIZED enables pre-allocation and O(1) counting. SORTED means sorted() is a no-op. DISTINCT means distinct() is a no-op. ORDERED means the stream must maintain encounter order, which can slow down parallel execution. Understanding these flags explains why certain stream operations are free or expensive depending on the source collection.

Different collections report different characteristics. ArrayList is ORDERED and SIZED. HashSet is DISTINCT and SIZED. TreeSet is all four: ORDERED, SIZED, SORTED, and DISTINCT. Stream operations themselves modify characteristics: filter() removes SIZED (count becomes unknown), sorted() adds SORTED, and distinct() adds DISTINCT.

At National Bank of India, Pankaj optimised the end-of-day reporting pipeline by choosing the right source collection for each stream. Account balances stored in a TreeSet could skip sorting and deduplication. Transaction counts stored in an ArrayList could leverage SIZED for pre-allocated arrays. This understanding of characteristics turned stream operations from potentially O(n log n) to O(n) or even O(1).

Remember one sentence: Spliterator characteristics like SIZED, SORTED, and DISTINCT tell the stream pipeline which operations it can skip, making the choice of source collection a performance decision.

---

Share
Use ← → arrow keys to navigate