# PCollections and the producer approach to immutable Java collections

> PCollections is a persistent, immutable Java collections library whose main distinction is that its values can produce new versions instead of forcing copies. It suits code that shares state across threads, but it carries costs and gaps worth weighing first.

**hrldcpr/pcollections** — A Persistent Java Collections Library

- Repository: https://github.com/hrldcpr/pcollections
- Stars: 790 · Forks: 85
- Language: Java
- License: NOASSERTION
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/hrldcpr-pcollections

## Java's unmodifiable collections have no producers

Collections.unmodifiableCollection() hands back a view that refuses mutation. The PCollections README draws a precise line between that and what this library offers: Java's unmodifiable collections have no producers, while PCollections values do. That single difference changes what an update costs. With an unmodifiable collection x, adding an element e means copying the contents into a new HashSet and adding there, which the README describes as linear in time and space. With a PCollection y, the same operation is y.plus(e). The original y is left untouched and the copy is generally little or none. For code that threads one shared value through a pipeline and never mutates it, that difference decides whether an update is cheap or proportional to the size of the collection.

## One concrete implementation per abstract type

The library defines abstract types such as PMap, PVector, PSet and PBag, and the README states that each abstract type currently has exactly one concrete implementation. HashTreePMap fills the PMap role the way HashMap does in the standard library, and TreePMap backs PSortedMap the way TreeMap does. ConsPStack plays the PStack role that LinkedList plays, TreePVector the PVector role that ArrayList plays, HashTreePSet the PSet role of HashSet, and TreePSet the PSortedSet role of TreeSet. HashTreePBag supplies PBag, an unordered collection that tolerates duplicate elements where a set cannot. A narrow implementation list keeps the API surface small, but it also means there is no second engine to swap in if the tree based structure suits your access pattern badly.

## Every PCollection is a java.util.Collection

Interoperability is the practical payoff of the design. Every PCollection implements java.util.Collection. Every PMap implements java.util.Map. Every PSequence implements java.util.List, which covers PStack and PVector. Every PSet implements java.util.Set, and the sorted variants implement SortedMap, NavigableMap, SortedSet and NavigableSet as well. Existing code that already accepts java.util interfaces can therefore take PCollections values without conversion or copying. That is the feature the README contrasts with the persistent collections offered by other JVM languages, which do not sit on the same Java interfaces and therefore need conversion at the boundary.

## Producers and static factories in a first program

Values are created through static factory methods and then extended through producers. The factory names are consistent across the implementations: empty() returns an empty instance, singleton(e) returns one holding a single element, and from(collection) returns one holding the same elements as an existing java.util collection. The README's example starts from HashTreePSet.empty() and uses plus to grow the value. The first printed line holds one element, the second holds two, and the third shows that the original set was never changed. Notice that the second print produces a value without assigning it, which is the point of the whole library: intermediate versions cost almost nothing.

```java
import org.pcollections.*;

public class Example {
  public static void main(String... args) {
    PSet<String> set = HashTreePSet.empty();
    set = set.plus("something");

    System.out.println(set);
    System.out.println(set.plus("something else"));
    System.out.println(set);
  }
}
```

```text
[something]
[something else, something]
[something]
```

## Adding the library to a Maven or Gradle build

Distribution runs through Maven Central under the org.pcollections group. The README gives the coordinates with version 5.0.0 for Maven, and the equivalent Gradle declaration, so adding the library is a dependency block rather than any setup script. The project can also be built from a checkout. The README instructs you to clone the repository and run the Gradle wrapper script at the root, and the repository root does contain build.gradle, settings.gradle, gradlew and gradlew.bat next to src/. There are no GitHub releases published for the repository, and the README does not document a release procedure, so the changelog files sitting in the repository are the visible record of what changed between versions.

```xml
<dependency>
    <groupId>org.pcollections</groupId>
    <artifactId>pcollections</artifactId>
    <version>5.0.0</version>
</dependency>
```

```groovy
compile 'org.pcollections:pcollections:5.0.0'
```

## Semantic versioning, the VERSION file and an unclassifiable licence

The README states that PCollections uses Semantic Versioning, which is meant to give a clear correspondence between API changes and version numbers. The dependency snippets show version 5.0.0, and the repository root carries a VERSION file alongside CHANGELOG.md and CHANGING.md. The licence is the unusual part. GitHub cannot classify it and reports the licence as Other, with the actual terms in the LICENSE file, so a licence badge on a package page tells you nothing you can rely on. If you intend to ship PCollections inside a commercial product, read that file yourself and check what it permits and requires. The README does not describe any deprecation policy or migration path, so an upgrade means reading the changelog and comparing the API by hand.

## Where persistent collections are the wrong tool

Immutability is not free, and the cases where it loses are predictable. A PVector and a PSet are tree structures rather than flat arrays, so a plus produces a new root and allocates, while the same operation on a mutable list writes into existing memory. Code that appends in a tight loop or depends on list identity will pay for the guarantee. Random access into a tree backed vector also does not carry the same cost profile as ArrayList.get. The catalogue of types is narrow too. There is no persistent queue, deque or concurrent collection here, only stacks, vectors, maps, sets and bags, so a task that needs a persistent version of anything else gets no answer from this project. The README also does not document how the collections compare against the standard library on raw throughput, so a performance claim has to be measured on your own workload.

## How PCollections differs from Clojure, Scala, kotlinx and Guava

The README names its related work and draws one specific line through it. Clojure, Scala and kotlinx.collections.immutable also provide persistent collections on the JVM, but the README notes that they are less interoperable with Java than PCollections is. The reason is structural: their collection types do not implement the java.util interfaces, so passing one into code written against the standard library needs an adapter. Guava and java.util.Collections sit on the other side of the comparison, sharing the unmodifiable approach with no producers, so growing a collection means copying it. The practical choice is therefore between a persistent library that keeps java.util interop and one that gives up that interop for a different runtime model, with the language ecosystem you already use deciding which side you land on.

## Conclusion

PCollections fits services that pass shared configuration and lookup tables across threads, where a copy on every update is the real cost. It does not fit code that mutates lists in hot loops or needs a persistent type outside stacks, vectors, maps, sets and bags. Before adopting it, read the LICENSE file, since GitHub classifies it only as Other, and check the changelog files in the repository, since the project publishes no GitHub releases.

## FAQ

### How do I add an element to a PCollections PSet without mutating it?

Use the producer method plus. The README example calls set.plus('something') and assigns the result, leaving the original value untouched. The PCollections types are immutable, so there is no mutating add operation.

### Which Maven dependency should I use for PCollections?

The README lists org.pcollections:pcollections with version 5.0.0 in Maven Central, and the Gradle form is compile 'org.pcollections:pcollections:5.0.0'.

### What is the difference between PCollections and java.util.Collections.unmodifiableCollection()?

The README says Java's unmodifiable collections have no producers, so adding an element means copying the whole collection, while a PCollection produces a new value with plus and generally little or no copying.

### How do I build PCollections from source?

The README instructs you to clone the repository and run the Gradle wrapper script at the root. The repository root contains build.gradle, settings.gradle, gradlew and gradlew.bat next to the src directory.

## Sources

- [hrldcpr/pcollections on GitHub](https://github.com/hrldcpr/pcollections)
- [Issues](https://github.com/hrldcpr/pcollections/issues)
- [README](https://github.com/hrldcpr/pcollections/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hrldcpr-pcollections
