You were almost ready with the solution, just that you need to supply the proper arguments.
If you look at the public static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements) method signature it takes a delimiter and an iterable. You were almost close to joining the strings using this method, all you had to do is to provide an Iterable<String>.
Since you start with a List<Integer> you need to convert it to an Iterable<String> to do that you can use the below code:
Iterable<String> iterable = ids.stream().map(String::valueOf).collect(Collectors.toList());
Where ids is a List<Integer>. Note that you need to do a map(String::valueOf) as you start with a List<Integer> not with a List<String>. Now that you have an Iterable<String> you can use that as the second argument of String.join().
So the complete solution is:
List<Integer> ids = new ArrayList<>();
ids.add(1);
ids.add(2);
ids.add(3);
Iterable<String> iterable = ids.stream().map(String::valueOf).collect(Collectors.toList());
String result = String.join(" ", iterable);
System.out.println(result);
Output:
1 2 3
If the ids may contain null then you can filter it out before applying the map():
Iterable<String> iterable = ids.stream()
.filter(Objects::nonNull)
.map(String::valueOf)
.collect(Collectors.toList());