diff --git a/src/main/java/org/apache/commons/collections4/iterators/LexicographicPermutationIterator.java b/src/main/java/org/apache/commons/collections4/iterators/LexicographicPermutationIterator.java
new file mode 100644
index 0000000000..3389db68b2
--- /dev/null
+++ b/src/main/java/org/apache/commons/collections4/iterators/LexicographicPermutationIterator.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.collections4.iterators;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+
+/**
+ * This iterator creates permutations of an input collection, using the
+ * lexicographical order.
+ *
+ * The iterator might return fewer than n! permutations of the input collection,
+ * because duplicated permutations are skipped: equal elements are not
+ * distinguished from one another.
+ * The {@code remove()} operation is not supported, and will throw an
+ * {@code UnsupportedOperationException}.
+ *
+ *
+ * NOTE: in case an empty collection is provided, the iterator will
+ * return exactly one empty list as result, as 0! = 1.
+ *
+ *
+ * @param the type of the objects being permuted
+ * @see PermutationIterator
+ * @since 4.6.0
+ */
+public class LexicographicPermutationIterator implements Iterator> {
+
+ /**
+ * The comparator used to define order of generation,
+ * or null if it uses the natural ordering.
+ */
+ private final Comparator super E> comparator;
+
+ /**
+ * Next permutation to return. When a permutation is requested
+ * this instance is provided and the next one is computed.
+ */
+ private List nextPermutation;
+
+ /**
+ * Standard constructor for this class, using the natural ordering of the elements.
+ *
+ * @param collection The collection to generate permutations for
+ * @throws NullPointerException if collection is null
+ */
+ public LexicographicPermutationIterator(final Collection extends E> collection) {
+ this(collection, null);
+ }
+
+ /**
+ * Constructs an instance using the given comparator to order the elements.
+ *
+ * @param collection The collection to generate permutations for
+ * @param comparator The comparator used to define the order of generation,
+ * or null to use the natural ordering of the elements
+ * @throws NullPointerException if collection is null
+ */
+ public LexicographicPermutationIterator(final Collection extends E> collection, final Comparator super E> comparator) {
+ Objects.requireNonNull(collection, "collection");
+ nextPermutation = new ArrayList<>(collection);
+ this.comparator = comparator;
+ }
+
+ /**
+ * Indicates if there are more permutation available.
+ *
+ * @return true if there are more permutations, otherwise false
+ */
+ @Override
+ public boolean hasNext() {
+ return nextPermutation != null;
+ }
+
+ /**
+ * Returns the next permutation of the input collection.
+ *
+ * @return A list of the permutator's elements representing a permutation
+ * @throws NoSuchElementException if there are no more permutations
+ */
+ @Override
+ public List next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+
+ final int size = nextPermutation.size();
+ List nextP = null;
+
+ // find the pivot: the rightmost element that is smaller than its successor.
+ // if there is none the current permutation is the last one in lexicographical order
+ int i = size - 2;
+ while (i >= 0 && compareElements(nextPermutation.get(i), nextPermutation.get(i + 1)) >= 0) {
+ --i;
+ }
+
+ if (i >= 0) {
+ // find the rightmost element greater than the pivot; the tail is descending,
+ // so this is the pivot's successor in the remaining elements
+ int j = size - 1;
+ while (j >= i && compareElements(nextPermutation.get(i), nextPermutation.get(j)) >= 0) {
+ --j;
+ }
+
+ // swap the pivot with its successor, then reverse the descending tail
+ // into ascending order to obtain the smallest larger permutation
+ nextP = new ArrayList<>(nextPermutation);
+ Collections.swap(nextP, i, j);
+ final List subList = nextP.subList(i + 1, nextP.size());
+ Collections.reverse(subList);
+ }
+
+ final List result = nextPermutation;
+ nextPermutation = nextP;
+ return result;
+ }
+
+ /**
+ * Always throws {@link UnsupportedOperationException}.
+ *
+ * @throws UnsupportedOperationException Always thrown.
+ */
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("remove() is not supported");
+ }
+
+ /**
+ * Compares this iterator to another for equality. Two iterators are equal when
+ * they use equal comparators and are positioned at an equal next permutation.
+ *
+ * @param o The object to compare to this instance
+ * @return true if the given object is an equal iterator, otherwise false
+ */
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final LexicographicPermutationIterator> that = (LexicographicPermutationIterator>) o;
+ return Objects.equals(comparator, that.comparator) && Objects.equals(nextPermutation, that.nextPermutation);
+ }
+
+ /**
+ * Returns a hash code consistent with {@link #equals(Object)}. Note that the
+ * hash code changes as the iterator advances.
+ *
+ * @return A hash code for this instance
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(comparator, nextPermutation);
+ }
+
+ /**
+ * Compares two elements using the comparator, or their natural ordering if no
+ * comparator was supplied.
+ *
+ * @param e1 The first element to compare
+ * @param e2 The second element to compare
+ * @return a negative integer, zero, or a positive integer as the first element
+ * is less than, equal to, or greater than the second
+ * @throws ClassCastException if no comparator was supplied and the elements are
+ * not mutually {@link Comparable}
+ */
+ @SuppressWarnings("unchecked")
+ private int compareElements(final E e1, final E e2) {
+ return comparator == null
+ ? ((Comparable super E>) e1).compareTo(e2)
+ : comparator.compare(e1, e2);
+ }
+
+}
diff --git a/src/test/java/org/apache/commons/collections4/iterators/LexicographicPermutationIteratorTest.java b/src/test/java/org/apache/commons/collections4/iterators/LexicographicPermutationIteratorTest.java
new file mode 100644
index 0000000000..c20dd8a13b
--- /dev/null
+++ b/src/test/java/org/apache/commons/collections4/iterators/LexicographicPermutationIteratorTest.java
@@ -0,0 +1,343 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.collections4.iterators;
+
+import static java.util.Collections.emptyList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for LexicographicPermutationIterator.
+ */
+class LexicographicPermutationIteratorTest extends AbstractIteratorTest> {
+
+ /**
+ * A comparator that orders nothing, identified only by an id, used to check that
+ * equal comparators make equal iterators.
+ *
+ * @param the type of the objects compared
+ */
+ private static final class CustomComparator implements Comparator {
+
+ private final int id;
+
+ CustomComparator(final int id) {
+ this.id = id;
+ }
+
+ @Override
+ public int compare(final T o1, final T o2) {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final CustomComparator> cmp = (CustomComparator>) o;
+ return id == cmp.id;
+ }
+
+ @Override
+ public int hashCode() {
+ return id;
+ }
+ }
+
+ /**
+ * A value holder that deliberately does not implement {@link Comparable}, used to
+ * check that a supplied comparator is honored.
+ *
+ * @param the type of the wrapped value
+ */
+ private static final class NonComparableObject {
+
+ private final T value;
+
+ NonComparableObject(final T value) {
+ this.value = value;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final NonComparableObject> that = (NonComparableObject>) o;
+ return Objects.equals(value, that.value);
+ }
+
+ T getValue() {
+ return value;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(value);
+ }
+ }
+
+ @SuppressWarnings("boxing") // OK in test code
+ protected Character[] testArray = { 'A', 'B', 'C' };
+
+ protected List testList;
+
+ @Override
+ public LexicographicPermutationIterator makeEmptyIterator() {
+ return new LexicographicPermutationIterator<>(new ArrayList<>());
+ }
+
+ @Override
+ public LexicographicPermutationIterator makeObject() {
+ return new LexicographicPermutationIterator<>(testList);
+ }
+
+ @BeforeEach
+ public void setUp() {
+ testList = new ArrayList<>();
+ testList.addAll(Arrays.asList(testArray));
+ }
+
+ @Override
+ public boolean supportsEmptyIterator() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsRemove() {
+ return false;
+ }
+
+ @Test
+ void testCustomComparator() {
+ final Iterator> permutationIterator = new LexicographicPermutationIterator<>(Arrays.asList('C', 'B', 'A'),
+ Comparator.reverseOrder());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('C', 'B', 'A'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('C', 'A', 'B'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('B', 'C', 'A'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('B', 'A', 'C'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('A', 'C', 'B'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('A', 'B', 'C'), permutationIterator.next());
+
+ assertFalse(permutationIterator.hasNext());
+ }
+
+ @Test
+ void testCustomComparatorWithNonComparableObjects() {
+ final Iterator>> permutationIterator =
+ new LexicographicPermutationIterator<>(Arrays.asList(
+ new NonComparableObject<>('A'),
+ new NonComparableObject<>('B')), Comparator.comparing(NonComparableObject::getValue));
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList(
+ new NonComparableObject<>('A'),
+ new NonComparableObject<>('B')), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList(
+ new NonComparableObject<>('B'),
+ new NonComparableObject<>('A')), permutationIterator.next());
+
+ assertFalse(permutationIterator.hasNext());
+ }
+
+ @Test
+ void testDuplicatedPermutationsAreSkipped() {
+ final Iterator> permutationIterator = new LexicographicPermutationIterator<>(Arrays.asList('A', 'A', 'B', 'B'));
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('A', 'A', 'B', 'B'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('A', 'B', 'A', 'B'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('A', 'B', 'B', 'A'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('B', 'A', 'A', 'B'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('B', 'A', 'B', 'A'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('B', 'B', 'A', 'A'), permutationIterator.next());
+
+ assertFalse(permutationIterator.hasNext());
+ }
+
+ @Test
+ void testEmptyCollection() {
+ final Iterator> permutationIterator = makeEmptyIterator();
+
+ // there is one permutation for an empty set: 0! = 1
+ assertTrue(permutationIterator.hasNext());
+ assertTrue(permutationIterator.next().isEmpty());
+
+ assertFalse(permutationIterator.hasNext());
+ }
+
+ @Test
+ void testEqualsForEqualCollections() {
+ final Iterator> one = new LexicographicPermutationIterator<>(emptyList());
+ final Iterator> another = new LexicographicPermutationIterator<>(emptyList());
+
+ assertEquals(one, another);
+ }
+
+ @Test
+ void testEqualsForEqualCollectionsAndComparators() {
+ final Iterator> one = new LexicographicPermutationIterator<>(emptyList(), new CustomComparator<>(42));
+ final Iterator> another = new LexicographicPermutationIterator<>(emptyList(), new CustomComparator<>(42));
+
+ assertEquals(one, another);
+ }
+
+ @Test
+ void testHashCodeForEqualCollections() {
+ final Iterator> one = new LexicographicPermutationIterator<>(emptyList());
+ final Iterator> another = new LexicographicPermutationIterator<>(emptyList());
+
+ assertEquals(one.hashCode(), another.hashCode());
+ }
+
+ @Test
+ void testHashCodeForEqualCollectionsAndComparators() {
+ final Iterator> one = new LexicographicPermutationIterator<>(emptyList(), new CustomComparator<>(42));
+ final Iterator> another = new LexicographicPermutationIterator<>(emptyList(), new CustomComparator<>(42));
+
+ assertEquals(one.hashCode(), another.hashCode());
+ }
+
+ @Test
+ void testNonComparableElementsThrow() {
+ final Iterator>> permutationIterator = new LexicographicPermutationIterator<>(
+ Arrays.asList(
+ new NonComparableObject<>('A'),
+ new NonComparableObject<>('B')));
+
+ assertTrue(permutationIterator.hasNext());
+ assertThrows(ClassCastException.class, permutationIterator::next);
+ }
+
+ @Test
+ void testPermutationException() {
+ final Iterator> permutationIterator = new LexicographicPermutationIterator<>(Arrays.asList('A', 'B'));
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('A', 'B'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('B', 'A'), permutationIterator.next());
+
+ // asking for another permutation should throw an exception
+ assertFalse(permutationIterator.hasNext());
+ assertThrows(NoSuchElementException.class, permutationIterator::next);
+ }
+
+ /**
+ * test checking that all the permutations are returned in lexicographical order
+ */
+ @Test
+ void testPermutationExhaustivity() {
+ final Iterator> permutationIterator = makeObject();
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('A', 'B', 'C'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('A', 'C', 'B'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('B', 'A', 'C'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('B', 'C', 'A'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('C', 'A', 'B'), permutationIterator.next());
+
+ assertTrue(permutationIterator.hasNext());
+ assertEquals(Arrays.asList('C', 'B', 'A'), permutationIterator.next());
+
+ assertFalse(permutationIterator.hasNext());
+ }
+
+ @Test
+ void testRemoveThrows() {
+ final Iterator> permutationIterator = makeObject();
+
+ assertTrue(permutationIterator.hasNext());
+ assertThrows(UnsupportedOperationException.class, permutationIterator::remove);
+ }
+
+ @Test
+ void testStreamOfPermutations() {
+ final Iterable> iterable = this::makeObject;
+
+ final List> allPermutations = StreamSupport.stream(iterable.spliterator(), false)
+ .collect(Collectors.toList());
+
+ assertEquals(Arrays.asList(
+ Arrays.asList('A', 'B', 'C'),
+ Arrays.asList('A', 'C', 'B'),
+ Arrays.asList('B', 'A', 'C'),
+ Arrays.asList('B', 'C', 'A'),
+ Arrays.asList('C', 'A', 'B'),
+ Arrays.asList('C', 'B', 'A')), allPermutations);
+ }
+
+}