As a kind of recursion, you can use Stream.reduce method. First prepare a list of possible combinations of characters for each character-position, and then consecutively reduce the stream of these lists to a single list, by summing the pairs of list elements.
As a list element, you can use Map<Integer,String>, where key - the position of the character in the string, value - the character itself, and summ the contents of these maps, excluding those keys that are already present.
And you get a list of permutations of characters.
For example, if you have a four-character string 𝗔𝗕𝗖𝗗, then you have to pass through three steps of reduction, consecutively accumulating the results:
| str1 + str2 = sum1 |
sum1 + str3 = sum2 |
sum2 + str4 = total |
𝗔 + 𝗕 = 𝗔𝗕 𝗔 + 𝗖 = 𝗔𝗖 𝗔 + 𝗗 = 𝗔𝗗 |
𝗔𝗕 + 𝗖 = 𝗔𝗕𝗖 𝗔𝗕 + 𝗗 = 𝗔𝗕𝗗 𝗔𝗖 + 𝗕 = 𝗔𝗖𝗕 𝗔𝗖 + 𝗗 = 𝗔𝗖𝗗 𝗔𝗗 + 𝗕 = 𝗔𝗗𝗕 𝗔𝗗 + 𝗖 = 𝗔𝗗𝗖 |
𝗔𝗕𝗖 + 𝗗 = 𝗔𝗕𝗖𝗗 𝗔𝗕𝗗 + 𝗖 = 𝗔𝗕𝗗𝗖 𝗔𝗖𝗕 + 𝗗 = 𝗔𝗖𝗕𝗗 𝗔𝗖𝗗 + 𝗕 = 𝗔𝗖𝗗𝗕 𝗔𝗗𝗕 + 𝗖 = 𝗔𝗗𝗕𝗖 𝗔𝗗𝗖 + 𝗕 = 𝗔𝗗𝗖𝗕 |
15 sums for each of the 4 characters are 60 sums, resulting in 4! = 24 permutations.
Try it online!
// original string
String str = "𝗔𝗕𝗖𝗗";
// array of characters of the string
int[] codePoints = str.codePoints().toArray();
// contents of the array
System.out.println(Arrays.toString(codePoints));
//[120276, 120277, 120278, 120279]
// list of permutations of characters
List<Map<Integer, String>> permutations = IntStream.range(0, codePoints.length)
// Stream<List<Map<Integer,String>>>
.mapToObj(i -> IntStream.range(0, codePoints.length)
// represent each character as Map<Integer,String>
.mapToObj(j -> Map.of(j, Character.toString(codePoints[j])))
// collect a list of maps
.collect(Collectors.toList()))
// intermediate output
//[{0=𝗔}, {1=𝗕}, {2=𝗖}, {3=𝗗}]
//[{0=𝗔}, {1=𝗕}, {2=𝗖}, {3=𝗗}]
//[{0=𝗔}, {1=𝗕}, {2=𝗖}, {3=𝗗}]
//[{0=𝗔}, {1=𝗕}, {2=𝗖}, {3=𝗗}]
.peek(System.out::println)
// reduce a stream of lists to a single list
.reduce((list1, list2) -> list1.stream()
// summation of pairs of maps from two lists
.flatMap(map1 -> list2.stream()
// filter out those keys that are already present
.filter(map2 -> map2.keySet().stream()
.noneMatch(map1::containsKey))
// join entries of two maps
.map(map2 -> new LinkedHashMap<Integer, String>() {{
putAll(map1);
putAll(map2);
}}))
// collect into a single list
.collect(Collectors.toList()))
// List<Map<Integer,String>>
.orElse(List.of(Map.of(0, str)));
// number of permutations
System.out.println(permutations.size()); // 24
// number of rows (factorial of string length - 1)
int rows = IntStream.range(1, codePoints.length)
.reduce((a, b) -> a * b).orElse(1);
// column-wise output
IntStream.range(0, rows)
.mapToObj(i -> IntStream.range(0, permutations.size())
.filter(j -> j % rows == i)
.mapToObj(permutations::get)
.map(map -> map.toString().replace(" ", ""))
.collect(Collectors.joining(" ")))
.forEach(System.out::println);
//{0=𝗔,1=𝗕,2=𝗖,3=𝗗} {1=𝗕,0=𝗔,2=𝗖,3=𝗗} {2=𝗖,0=𝗔,1=𝗕,3=𝗗} {3=𝗗,0=𝗔,1=𝗕,2=𝗖}
//{0=𝗔,1=𝗕,3=𝗗,2=𝗖} {1=𝗕,0=𝗔,3=𝗗,2=𝗖} {2=𝗖,0=𝗔,3=𝗗,1=𝗕} {3=𝗗,0=𝗔,2=𝗖,1=𝗕}
//{0=𝗔,2=𝗖,1=𝗕,3=𝗗} {1=𝗕,2=𝗖,0=𝗔,3=𝗗} {2=𝗖,1=𝗕,0=𝗔,3=𝗗} {3=𝗗,1=𝗕,0=𝗔,2=𝗖}
//{0=𝗔,2=𝗖,3=𝗗,1=𝗕} {1=𝗕,2=𝗖,3=𝗗,0=𝗔} {2=𝗖,1=𝗕,3=𝗗,0=𝗔} {3=𝗗,1=𝗕,2=𝗖,0=𝗔}
//{0=𝗔,3=𝗗,1=𝗕,2=𝗖} {1=𝗕,3=𝗗,0=𝗔,2=𝗖} {2=𝗖,3=𝗗,0=𝗔,1=𝗕} {3=𝗗,2=𝗖,0=𝗔,1=𝗕}
//{0=𝗔,3=𝗗,2=𝗖,1=𝗕} {1=𝗕,3=𝗗,2=𝗖,0=𝗔} {2=𝗖,3=𝗗,1=𝗕,0=𝗔} {3=𝗗,2=𝗖,1=𝗕,0=𝗔}
See also: How do you check if a word has an anagram that is a palindrome?