Java 8 reduce to 2 strings - java

Can I do this with streams?
StringBuilder text = new StringBuilder();
StringBuilder dupText = new StringBuilder();
String lastLetter = "";
for (Container cont : containersList) {
String letter = cont.getLetter();
text.append(letter);
if (letter.equals(lastLetter) == false) {
dupText.append(letter);
}
lastLetter = letter;
}
System.out.println(text);
System.out.println(dupText);
I go over list of continers, each one has a char.
I need to assemble two strings - one is all the chars combine, and the other one is all the chars but without coupled duplicates (ABABAAAB -> ABABAB)
Can this be done with streams?
I tried doing it like this:
Optional<String> text = containersList.stream()
.map(Container::getLetter)
.reduce((letter,accumalator) -> accumalator += letter);
Optional<String> dupText = session.containersList().stream()
.map(Container::getLetter)
.reduce((letter, accumalator) ->{
if ((accumalator.endsWith(letter) == false)) {
accumalator += letter;
}
return accumalator;
});

Using StreamEx library
You can do this in a single Stream pipeline using the StreamEx library.
List<Container> containersList = Arrays.asList(new Container("A"), new Container("B"), new Container("A"), new Container("A"), new Container("B"));
String[] result =
StreamEx.of(containersList)
.map(Container::getLetter)
.groupRuns(Object::equals)
.collect(MoreCollectors.pairing(
MoreCollectors.flatMapping(List::stream, Collectors.joining()),
MoreCollectors.mapping(l -> l.get(0), Collectors.joining()),
(s1, s2) -> new String[] { s1, s2 }
));
System.out.println(result[0]);
System.out.println(result[1]);
This code creates a Stream of the containers and maps each of those to their letter.
Then, the method groupRuns collapses into a List the successive elements that matches the given predicate. In this case, the predicate is the equality of the String: so if you start with the stream [A, A, B], this method will collapse it into the Stream [List(A, A), List(B)] (the first element is the list of 2 A successive elements in the input).
Finally, this is collected with the pairing collector that allows to collect into two different collector. The first one joins the flat map result of each list while the second one joins only the first element of the list (hence removing the successive elements).
The result is stored inside an array which just serves as a holder for two values.
Output:
ABAAB
ABAB
Using the Stream API directly
If you want to stay with the current API and not using a library, your best bet would be to write a custom Collector:
public static void main(String[] args) {
List<Container> containersList = Arrays.asList(new Container("A"), new Container("B"), new Container("A"), new Container("A"), new Container("B"));
String[] result = containersList.stream().parallel().map(Container::getLetter).collect(ContainerCollector.collector());
System.out.println(result[0]);
System.out.println(result[1]);
}
private static final class ContainerCollector {
private StringBuilder text = new StringBuilder();
private StringBuilder dupText = new StringBuilder();
private void accept(String letter) {
text.append(letter);
if (dupText.indexOf(letter, dupText.length() - letter.length()) < 0) {
dupText.append(letter);
}
}
private ContainerCollector combine(ContainerCollector other) {
text.append(other.text);
other.dupText.codePoints().forEach(i -> {
String letter = new String(Character.toChars(i));
if (dupText.indexOf(letter, dupText.length() - letter.length()) < 0) {
dupText.append(letter);
}
});
return this;
}
private String[] finish() {
return new String[] { text.toString(), dupText.toString() };
}
private static Collector<String, ?, String[]> collector() {
return Collector.of(ContainerCollector::new, ContainerCollector::accept, ContainerCollector::combine, ContainerCollector::finish);
}
}
This custom collector builds the text and dupText when each letter is accepted. For the text String, the letter is always appended. For the dupText, the letter is only appended if the last one is different.
The combiner code (ran in case of parallel execution) is a bit tricky for the dupText: the second one is appended if it does not start with the end of the first one. Otherwise, the first letter is dropped and the rest is appended.
The output is the same.

I would make it in two separate operations. First, to get the text with duplicates:
String dupText = containersList.stream()
.map(Container::getLetter)
.collect(Collectors.joining());
And the second to remove the duplicates using the regexp:
String text = dupText.replaceAll("(.)\\1+", "$1");
While it's technically two-pass solution, it does not traverse input container twice and, I believe, it should be quite fast, at least not slower than other proposed solutions. And it's simple and does not require third-party libraries.

Using streams is the right choice for unpacking containers. Removing repeated characters, however, is easier with loops.
I'd recommend to use the best out of both worlds:
import java.util.ArrayList;
import java.util.Collection;
import java.util.stream.Collectors;
class Container {
private char letter;
public String getLetter() {
return Character.toString(letter);
}
public static Container of(char letter) {
Container container = new Container();
container.letter = letter;
return container;
}
}
public class T {
public static void main(String[] args) {
Collection<Container> containersList = new ArrayList<>();
containersList.add(Container.of('A'));
containersList.add(Container.of('B'));
containersList.add(Container.of('A'));
containersList.add(Container.of('B'));
containersList.add(Container.of('A'));
containersList.add(Container.of('A'));
containersList.add(Container.of('A'));
containersList.add(Container.of('B'));
// at first join characters, don't bother about duplicates
String text = containersList.stream()
.map(Container::getLetter)
.collect(Collectors.joining());
// afterwards remove duplicates
StringBuilder dupText = new StringBuilder();
Character lastLetter = null;
for (Character c : text.toCharArray()) {
if (c.equals(lastLetter))
continue;
dupText.append(c);
lastLetter = c;
}
System.out.println(text);
System.out.println(dupText);
}
}
A solution without loops could look like this:
// at first join characters, don't bother about duplicates
String text = containersList.stream()
.map(Container::getLetter)
.collect(Collectors.joining());
// afterwards remove duplicates
String dupText = text.chars()
.mapToObj(i -> Character.toString((char)i))
.reduce((left,right) -> {
if (left.endsWith(right))
return left;
return left+right;
})
.get();
If you must not iterate twice, use this:
MyBuilder myBuilder = new MyBuilder();
containersList.stream()
.map(Container::getLetter)
.forEachOrdered(myBuilder::accept);
System.out.println(myBuilder.text);
System.out.println(myBuilder.dupText);
with a builder like this:
class MyBuilder {
StringBuilder text = new StringBuilder();
StringBuilder dupText = new StringBuilder();
String lastLetter;
void accept(String letter) {
text.append(letter);
if (letter.equals(lastLetter) == false) {
dupText.append(letter);
}
lastLetter = letter;
}
}

Another solution using my StreamEx library:
Collector<Entry<String, Long>, ?, String[]> collector = MoreCollectors.pairing(
Collectors.mapping(e -> StreamEx.constant(e.getKey(), e.getValue()).joining(),
Collectors.joining()),
Collectors.mapping(e -> e.getKey(), Collectors.joining()),
(s1, s2) -> new String[] { s1, s2 }
);
String[] result = StreamEx.of(containersList).map(Container::getLetter)
.runLengths().collect(collector);
System.out.println(result[0]);
System.out.println(result[1]);
It should be more performant than solution proposed by #Tunaki when long series of equal letters appear: instead of collecting them to lists (via groupRuns()) this solution just counts them (via runLengths())

Related

String data manipulation with Maps for very large data input

I have solved Two Strings problem in HackerRank
Here is the problem.
Given two strings, determine if they share a common substring. A
substring may be as small as one character.
For example, the words "a", "and", "art" share the common substring.
The words "be" and "cat" do not share a substring.
Function Description
Complete the function twoStrings in the editor below. It should return
a string, either YES or NO based on whether the strings share a common
substring.
twoStrings has the following parameter(s):
s1, s2: two strings to analyze .
Output Format
For each pair of strings, return YES or NO.
However, when extra-long strings are subjected, my code does not run within the time limit. Any suggestions to improve efficiency? I think I can improve substring finding with using the Stream API. But I'm not sure how to use it in this context. Could someone please help me to understand this better?
public static void main(String[] args) {
String s1 = "hi";
String s2 = "world";
checkSubStrings(s1, s2);
}
static void checkSubStrings(String s1, String s2) {
Map<String, Long> s1Map = new HashMap<>();
Map<String, Long> s2Map = new HashMap<>();
findAllSubStrings(s1, s1Map);
findAllSubStrings(s2, s2Map);
boolean isContain = s2Map.entrySet().stream().anyMatch(i -> s1Map.containsKey(i.getKey()) );
if (isContain) {
System.out.println("YES");
} else {
System.out.println("NO");
}
}
static void findAllSubStrings(String s, Map<String, Long> map) {
for (int i = 0; i < s.length(); i++) {
String subString = s.substring(i);
for (int j = subString.length(); j > 0; j--) {
String subSubString = subString.substring(0, j);
if (map.containsKey(subSubString)) {
map.put(subSubString, map.get(subSubString) + 1);
} else {
if (!subSubString.equals(""))
map.put(subSubString, 1L);
}
}
}
}
Update
I just solved the question using HashSets.
I optimized the code using Set. Now it runs with very large Strings.
static String twoStrings(String s1, String s2) {
String result = null;
Set<Character> s1Set = new HashSet<>();
Set<Character> s2Set = new HashSet<>();
for(char a : s1.toCharArray()){
s1Set.add(a);
}
for(char a : s2.toCharArray()){
s2Set.add(a);
}
boolean isContain = s2Set.stream().anyMatch(s1Set::contains);
if(isContain){
result = "YES";
} else {
result = "NO";
}
return result;
}
If 2 strings share an N (>=2) character substring, they also share an N-1 character substring (because you can chop a character off the end of the common substring, and this will still be found in both strings). Extending this argument, they also share a 1-character substring.
As such, all you need to check are single-character substrings.
Fill your maps with single-character substrings instead, and you will avoid creating (and checking) unnecessary substrings. (And just use a Set instead of a Map, you never use the counts).
// Yields a `Set<Integer>`, which can be used directly to check.
return s.codePoints().boxed().collect(Collectors.toSet());

How to compare and operate two adjacent elements in one list using stream in Java?

The background is I have two String type variables str1 and str2 as inputs. At last I have to return a list that contains the consecutive prefix of str1 that smaller than the related prefix in str2.
I have the Java code like this:
public List<Character> getPrefix(String str1, String str2) {
int index = 0;
List<Character> res = new ArrayList<>();
//str1 = "1243"
//str2 = "2324"
// The answer will be "12".
while (index < str1.length() && index < str2.length() && str1.charAt(index) <= str2.charAt(index)) {
res.add(str1.charAt(index));
index++;
}
return res;
}
//the return type could either be List<String> or List<Character>
I was asked to convert this code in stream without using while or for loop, just in stream method. I plan to convert this code like this
List<String> list = new ArrayList<>();
list.add(str1);
list.add(str2);
List<String> res = list.stream()
.filter()
.reduce();
I found filter() method could select the element that match the given predicate, and reduce() method could use identity and accumulator to get one final result.
But I find I could neither have a way to operate two adjacent elements in one list, or get one pointer to compare and traverse each character in each element in the list(the element is String type).
So is there any ways that I could operate two adjacent elements in one list, so that I can compare their characters which in the same position.
You can:
Generate a stream of indexes
Get characters of both strings using the index
Select chars while valid
// The magic
public static List<Character> getPrefix(String str1, String str2) {
return IntStream
.range(0, Math.min(str1.length(), str2.length()))
.mapToObj(i -> new char[] { str1.charAt(i), str2.charAt(i) })
.takeWhile(a -> a[0] < a[1])
.map(a -> a[0])
.collect(Collectors.toList());
}
Look at the code and maybe that is what you want.
Still it could be enhanced more and do not solve the case when first string start with a value greater than second. This can be implemented also but need additional work.
(could not do in one piece because the supplier consume one element for check needed to chain dropWhile and takeWhile).
Simply, with a supplier you can do comparison between elements from the stream with elements from other data-structure.
import java.util.LinkedList;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class Pre1 {
public static void main(String[] args) {
System.out.println(new Pre1().getPre("1234", "2315"));
System.out.println(new Pre1().getPre("941234", "712315"));
System.out.println(new Pre1().getPre("2345", "341"));
}
public String getPre(String s1, String s2) {
//second list is used as supplier
LinkedList<Integer> l2 = s2.chars().boxed()
.map(t->Character.getNumericValue(t))
.collect(Collectors.toCollection(LinkedList<Integer>::new));
//l2.forEach(System.out::println);
Supplier<Integer> supplier = () -> {
// System.out.println(l2.peek());
return l2.isEmpty() ? 0 : l2.pollFirst();
};
return s1.chars().boxed()
.map(t->Character.getNumericValue(t))
.takeWhile(t->t<supplier.get())
.map(t->String.valueOf(t))
.collect(Collectors.joining());
}
}
Output
12
nothing
23

Searching a List of Strings (100 000 Strings): How to search for x character if there is a %

I am programming an android app in Java and I need to show a ListView.
But before creating the ListView I need to fill my Adapter with data.
I have a ArrayList of String and I want to search that ArrayList by an EditText.
My Arraylist has 100 000+ entries.
I want to get ABC from ArrayList when typing ABC.
I want to get ABC, AAC, ACC, ADC, ... from ArrayList when typing A_C so in this case the _ needs to replace a character. It also should work for multiple underlines next to each other and not next to each other.
I want to get ABC, AAC, ACC, ADC, ABAC, ABBC,... from ArrayList when typing A%C so in this case the % needs to replace multiple characters. It also should work for multiple % not next to each other. This is I think the same thing like the star * in windows.
Hope you could help.
A serious non answer: consider not doing this within your app.
When you are dealing with such enormous amounts of data, you really don't want that processing happening on (potentially cheap) mobile phone hardware. Sooner or later, some people will use your app on inadequate hardware, and then they will complain about "the app is super slow".
Meaning: consider doing this in some sort of backend service. And then, use a technology built for dealing with large amounts of text data, such as solr. So, yes, the app might send strings to your service, as you type, and the service sends back suggestions, lists, whatever.
You can try the following. First, we are creating a regular expression that matches your search term, then we filter the list using paralellStream() as recommended by Korashen.
The regex (?<=[_%])|(?=[_%]) will split the String at _ and % while preserving the delimiters. For example, if you split "A_B" the result would be ["A","_","B"] and not ["A","B"].
private static List<String> filterByTerm(List<String> list, String term) {
StringBuilder regexBuilder = new StringBuilder();
String[] array = term.split("(?<=[_%])|(?=[_%])");
for(String s : array) {
switch(s) {
case "_":
regexBuilder.append(".");
break;
case "%":
regexBuilder.append(".*");
break;
default:
regexBuilder.append(Pattern.quote(s));
}
}
String regex = regexBuilder.toString();
return list.parallelStream().filter(s -> s.matches(regex)).collect(Collectors.toList());
}
Minimal verified example:
List<String> input = new ArrayList<>();
input.add("ABC");
input.add("AAC");
input.add("ACC");
input.add("ADC");
String term = "AB_";
filterByTerm(input, term).forEach(System.out::println);
gives the output of ABC.
An ArrayList of strings is not the right data structure for this task. The only way to search such a list is to iterate through all of them, which will be slow. You want a data structure which supports you with the kind of queries that you're doing. The cost of this will probably be an increased memory footprint.
A tree structure of characters seems like it would work well. You would represent the words AAB, ABA and ABB as the following tree:
A
/ \
A B
/ / \
B A B
I also strongly agree with GhostCat that you probably don't want to do this client-side.
Below is a quick implementation. I make no guarantees that it works perfectly or optimally - it is a demonstration of what's possible, not production-ready code. I haven't tested it with large data sets.
It only supports your underscore rule, but it should be simple enough to adapt it to also support your multicharacter matching.
A generic interface which is implemented by both the root and actual char nodes, and contains the default search implementation:
interface CharTree
{
List<CharNode> getChildren();
default Optional<CharNode> getChild(char character)
{
return getChildren().stream()
.filter(ch -> ch.getCharacter() == character)
.findFirst();
}
default void search(final String pattern, final StringBuilder builder, final Set<String> results)
{
if (pattern.isEmpty())
{
results.add(builder.toString());
return;
}
char character = pattern.toCharArray()[0];
final List<CharNode> candidates;
if (character == '_')
{
candidates = getChildren();
}
else
{
candidates = getChild(character)
.map(Collections::singletonList)
.orElse(Collections.emptyList());
}
for (final CharNode node : candidates)
{
builder.append(node.getCharacter());
node.search(pattern.substring(1, pattern.length()), builder, results);
builder.deleteCharAt(builder.length() - 1);
}
}
}
Basic root implementation, with a static method to build the tree:
class Root implements CharTree
{
private Root() { }
#Getter private List<CharNode> children = new ArrayList<>();
public static Root buildTree(final List<String> words)
{
final Root root = new Root();
for (final String word : words)
{
CharTree current = root;
for (char character : word.toCharArray())
{
Optional<CharNode> node = current.getChild(character);
if (node.isPresent())
{
current = node.get();
}
else
{
final CharNode tmp = new CharNode(character);
current.getChildren().add(tmp);
current = tmp;
}
}
}
return root;
}
}
Simple character node (annotations are from Lombok)
#Data
#ToString(of = "character")
class CharNode implements CharTree
{
private final char character;
private List<CharNode> children = new ArrayList<>();
}
Some unit tests in case anyone cares:
#Test
public void one()
{
final List<String> words = Arrays.asList("aaa", "bbb", "ccc");
final CharTree root = Root.buildTree(words);
final Set<String> results = new HashSet<>();
root.search("aaa", new StringBuilder(), results);
Assert.assertEquals(1, results.size());
Assert.assertTrue(results.contains("aaa"));
}
#Test
public void two()
{
final List<String> words = Arrays.asList("aaa", "aba", "abb");
final CharTree root = Root.buildTree(words);
final Set<String> results = new HashSet<>();
root.search("a_a", new StringBuilder(), results);
Assert.assertEquals(2, results.size());
Assert.assertTrue(results.contains("aaa"));
Assert.assertTrue(results.contains("aba"));
}
#Test
public void three()
{
final List<String> words = Arrays.asList("aaa", "aba", "abb");
final CharTree root = Root.buildTree(words);
final Set<String> results = new HashSet<>();
root.search("___", new StringBuilder(), results);
Assert.assertEquals(3, results.size());
Assert.assertTrue(results.contains("aaa"));
Assert.assertTrue(results.contains("aba"));
Assert.assertTrue(results.contains("abb"));
}

Using streams to convert a list of objects into a string obtained from the toString method

There are a lot of useful new things in Java 8. E.g., I can iterate with a stream over a list of objects and then sum the values from a specific field of the Object's instances. E.g.
public class AClass {
private int value;
public int getValue() { return value; }
}
Integer sum = list.stream().mapToInt(AClass::getValue).sum();
Thus, I'm asking if there is any way to build a String that concatenates the output of the toString() method from the instances in a single line.
List<Integer> list = ...
String concatenated = list.stream().... //concatenate here with toString() method from java.lang.Integer class
Suppose that list contains integers 1, 2 and 3, I expect that concatenated is "123" or "1,2,3".
One simple way is to append your list items in a StringBuilder
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
StringBuilder b = new StringBuilder();
list.forEach(b::append);
System.out.println(b);
you can also try:
String s = list.stream().map(e -> e.toString()).reduce("", String::concat);
Explanation: map converts Integer stream to String stream, then its reduced as concatenation of all the elements.
Note: This is normal reduction which performs in O(n2)
for better performance use a StringBuilder or mutable reduction similar to F. Böller's answer.
String s = list.stream().map(Object::toString).collect(Collectors.joining(","));
Ref: Stream Reduction
There is a collector joining in the API.
It's a static method in Collectors.
list.stream().map(Object::toString).collect(Collectors.joining(","))
Not perfect because of the necessary call of toString, but works. Different delimiters are possible.
Just in case anyone is trying to do this without java 8, there is a pretty good trick. List.toString() already returns a collection that looks like this:
[1,2,3]
Depending on your specific requirements, this can be post-processed to whatever you want as long as your list items don't contain [] or , .
For instance:
list.toString().replace("[","").replace("]","")
or if your data might contain square brackets this:
String s=list.toString();
s = s.substring(1,s.length()-1)
will get you a pretty reasonable output.
One array item on each line can be created like this:
list.toString().replace("[","").replace("]","").replaceAll(",","\r\n")
I used this technique to make html tooltips from a list in a small app, with something like:
list.toString().replace("[","<html>").replace("]","</html>").replaceAll(",","<br>")
If you have an array then start with Arrays.asList(list).toString() instead
I'll totally own the fact that this is not optimal, but it's not as inefficient as you might think and is pretty straightforward to read and understand. It is, however, quite inflexible--in particular don't try to separate the elements with replaceAll if your data might contain commas and use the substring version if you have square brackets in your data, but for an array of numbers it's pretty much perfect.
There is a method in the String API for those "joining list of string" usecases, you don't even need Stream.
List<String> myStringIterable = Arrays.asList("baguette", "bonjour");
String myReducedString = String.join(",", myStringIterable);
// And here you obtain "baguette,bonjour" in your myReducedString variable
The other answers are fine. However, you can also pass Collectors.toList() as parameter to Stream.collect() to return the elements as an ArrayList.
System.out.println( list.stream().map( e -> e.toString() ).collect( toList() ) );
StringListName = ObjectListName.stream().map( m -> m.toString() ).collect( Collectors.toList() );
List<String> list = Arrays.asList("One", "Two", "Three");
list.stream()
.reduce("", org.apache.commons.lang3.StringUtils::join);
Or
List<String> list = Arrays.asList("One", "Two", "Three");
list.stream()
.reduce("", (s1,s2)->s1+s2);
This approach allows you also build a string result from a list of objects
Example
List<Wrapper> list = Arrays.asList(w1, w2, w2);
list.stream()
.map(w->w.getStringValue)
.reduce("", org.apache.commons.lang3.StringUtils::join);
Here the reduce function allows you to have some initial value to which you want to append new string
Example:
List<String> errors = Arrays.asList("er1", "er2", "er3");
list.stream()
.reduce("Found next errors:", (s1,s2)->s1+s2);
Testing both approaches suggested in Shail016 and bpedroso answer (https://stackoverflow.com/a/24883180/2832140), the simple StringBuilder + append(String) within a for loop, seems to execute much faster than list.stream().map([...].
Example: This code walks through a Map<Long, List<Long>> builds a json string, using list.stream().map([...]:
if (mapSize > 0) {
StringBuilder sb = new StringBuilder("[");
for (Map.Entry<Long, List<Long>> entry : threadsMap.entrySet()) {
sb.append("{\"" + entry.getKey().toString() + "\":[");
sb.append(entry.getValue().stream().map(Object::toString).collect(Collectors.joining(",")));
}
sb.delete(sb.length()-2, sb.length());
sb.append("]");
System.out.println(sb.toString());
}
On my dev VM, junit usually takes between 0.35 and 1.2 seconds to execute the test. While, using this following code, it takes between 0.15 and 0.33 seconds:
if (mapSize > 0) {
StringBuilder sb = new StringBuilder("[");
for (Map.Entry<Long, List<Long>> entry : threadsMap.entrySet()) {
sb.append("{\"" + entry.getKey().toString() + "\":[");
for (Long tid : entry.getValue()) {
sb.append(tid.toString() + ", ");
}
sb.delete(sb.length()-2, sb.length());
sb.append("]}, ");
}
sb.delete(sb.length()-2, sb.length());
sb.append("]");
System.out.println(sb.toString());
}
A clean way to do this is by mapping the elements of the list to string and then using the joining operation in Collectors class.
List<Integer> ls = new ArrayList<Integer>();
ls.add(1);
ls.add(2);
ls.add(3);
String s = ls.stream().map(Object::toString).collect(Collectors.joining(","));
String actual = list.stream().reduce((t, u) -> t + "," + u).get();
I'm going to use the streams api to convert a stream of integers into a single string. The problem with some of the provided answers is that they produce a O(n^2) runtime because of String building. A better solution is to use a StringBuilder, and then join the strings together as the final step.
// Create a stream of integers
String result = Arrays.stream(new int[]{1,2,3,4,5,6 })
// collect into a single StringBuilder
.collect(StringBuilder::new, // supplier function
// accumulator - converts cur integer into a string and appends it to the string builder
(builder, cur) -> builder.append(Integer.toString(cur)),
// combiner - combines two string builders if running in parallel
StringBuilder::append)
// convert StringBuilder into a single string
.toString();
You can take this process a step further by converting the collection of object to a single string.
// Start with a class definition
public static class AClass {
private int value;
public int getValue() { return value; }
public AClass(int value) { this.value = value; }
#Override
public String toString() {
return Integer.toString(value);
}
}
// Create a stream of AClass objects
String resultTwo = Arrays.stream(new AClass[]{
new AClass(1),
new AClass(2),
new AClass(3),
new AClass(4)
})
// transform stream of objects into a single string
.collect(StringBuilder::new,
(builder, curObj) -> builder.append(curObj.toString()),
StringBuilder::append
)
// finally transform string builder into a single string
.toString();
Can we try this.
public static void main(String []args){
List<String> stringList = new ArrayList<>();
for(int i=0;i< 10;i++){
stringList.add(""+i);
}
String stringConcated = String.join(",", stringList);
System.out.println(stringConcated);
}
Also, you can do like this.
List<String> list = Arrays.asList("One", "Two", "Three");
String result = String.join(", ", list);
System.out.println(result);
With Java 8+
String s = Arrays.toString(list.stream().toArray(AClass[]::new));
Not the most efficient, but it is a solution with a small amount of code.

What's the best way to build a string of delimited items in Java?

While working in a Java app, I recently needed to assemble a comma-delimited list of values to pass to another web service without knowing how many elements there would be in advance. The best I could come up with off the top of my head was something like this:
public String appendWithDelimiter( String original, String addition, String delimiter ) {
if ( original.equals( "" ) ) {
return addition;
} else {
return original + delimiter + addition;
}
}
String parameterString = "";
if ( condition ) parameterString = appendWithDelimiter( parameterString, "elementName", "," );
if ( anotherCondition ) parameterString = appendWithDelimiter( parameterString, "anotherElementName", "," );
I realize this isn't particularly efficient, since there are strings being created all over the place, but I was going for clarity more than optimization.
In Ruby, I can do something like this instead, which feels much more elegant:
parameterArray = [];
parameterArray << "elementName" if condition;
parameterArray << "anotherElementName" if anotherCondition;
parameterString = parameterArray.join(",");
But since Java lacks a join command, I couldn't figure out anything equivalent.
So, what's the best way to do this in Java?
Pre Java 8:
Apache's commons lang is your friend here - it provides a join method very similar to the one you refer to in Ruby:
StringUtils.join(java.lang.Iterable,char)
Java 8:
Java 8 provides joining out of the box via StringJoiner and String.join(). The snippets below show how you can use them:
StringJoiner
StringJoiner joiner = new StringJoiner(",");
joiner.add("01").add("02").add("03");
String joinedString = joiner.toString(); // "01,02,03"
String.join(CharSequence delimiter, CharSequence... elements))
String joinedString = String.join(" - ", "04", "05", "06"); // "04 - 05 - 06"
String.join(CharSequence delimiter, Iterable<? extends CharSequence> elements)
List<String> strings = new LinkedList<>();
strings.add("Java");strings.add("is");
strings.add("cool");
String message = String.join(" ", strings);
//message returned is: "Java is cool"
You could write a little join-style utility method that works on java.util.Lists
public static String join(List<String> list, String delim) {
StringBuilder sb = new StringBuilder();
String loopDelim = "";
for(String s : list) {
sb.append(loopDelim);
sb.append(s);
loopDelim = delim;
}
return sb.toString();
}
Then use it like so:
List<String> list = new ArrayList<String>();
if( condition ) list.add("elementName");
if( anotherCondition ) list.add("anotherElementName");
join(list, ",");
In the case of Android, the StringUtils class from commons isn't available, so for this I used
android.text.TextUtils.join(CharSequence delimiter, Iterable tokens)
http://developer.android.com/reference/android/text/TextUtils.html
The Google's Guava library has com.google.common.base.Joiner class which helps to solve such tasks.
Samples:
"My pets are: " + Joiner.on(", ").join(Arrays.asList("rabbit", "parrot", "dog"));
// returns "My pets are: rabbit, parrot, dog"
Joiner.on(" AND ").join(Arrays.asList("field1=1" , "field2=2", "field3=3"));
// returns "field1=1 AND field2=2 AND field3=3"
Joiner.on(",").skipNulls().join(Arrays.asList("London", "Moscow", null, "New York", null, "Paris"));
// returns "London,Moscow,New York,Paris"
Joiner.on(", ").useForNull("Team held a draw").join(Arrays.asList("FC Barcelona", "FC Bayern", null, null, "Chelsea FC", "AC Milan"));
// returns "FC Barcelona, FC Bayern, Team held a draw, Team held a draw, Chelsea FC, AC Milan"
Here is an article about Guava's string utilities.
In Java 8 you can use String.join():
List<String> list = Arrays.asList("foo", "bar", "baz");
String joined = String.join(" and ", list); // "foo and bar and baz"
Also have a look at this answer for a Stream API example.
in Java 8 you can do this like:
list.stream().map(Object::toString)
.collect(Collectors.joining(delimiter));
if list has nulls you can use:
list.stream().map(String::valueOf)
.collect(Collectors.joining(delimiter))
it also supports prefix and suffix:
list.stream().map(String::valueOf)
.collect(Collectors.joining(delimiter, prefix, suffix));
You can generalize it, but there's no join in Java, as you well say.
This might work better.
public static String join(Iterable<? extends CharSequence> s, String delimiter) {
Iterator<? extends CharSequence> iter = s.iterator();
if (!iter.hasNext()) return "";
StringBuilder buffer = new StringBuilder(iter.next());
while (iter.hasNext()) buffer.append(delimiter).append(iter.next());
return buffer.toString();
}
Use an approach based on java.lang.StringBuilder! ("A mutable sequence of characters. ")
Like you mentioned, all those string concatenations are creating Strings all over. StringBuilder won't do that.
Why StringBuilder instead of StringBuffer? From the StringBuilder javadoc:
Where possible, it is recommended that this class be used in preference to StringBuffer as it will be faster under most implementations.
I would use Google Collections. There is a nice Join facility.
http://google-collections.googlecode.com/svn/trunk/javadoc/index.html?com/google/common/base/Join.html
But if I wanted to write it on my own,
package util;
import java.util.ArrayList;
import java.util.Iterable;
import java.util.Collections;
import java.util.Iterator;
public class Utils {
// accept a collection of objects, since all objects have toString()
public static String join(String delimiter, Iterable<? extends Object> objs) {
if (objs.isEmpty()) {
return "";
}
Iterator<? extends Object> iter = objs.iterator();
StringBuilder buffer = new StringBuilder();
buffer.append(iter.next());
while (iter.hasNext()) {
buffer.append(delimiter).append(iter.next());
}
return buffer.toString();
}
// for convenience
public static String join(String delimiter, Object... objs) {
ArrayList<Object> list = new ArrayList<Object>();
Collections.addAll(list, objs);
return join(delimiter, list);
}
}
I think it works better with an object collection, since now you don't have to convert your objects to strings before you join them.
Apache commons StringUtils class has a join method.
Java 8
stringCollection.stream().collect(Collectors.joining(", "));
Java 8 Native Type
List<Integer> example;
example.add(1);
example.add(2);
example.add(3);
...
example.stream().collect(Collectors.joining(","));
Java 8 Custom Object:
List<Person> person;
...
person.stream().map(Person::getAge).collect(Collectors.joining(","));
Use StringBuilder and class Separator
StringBuilder buf = new StringBuilder();
Separator sep = new Separator(", ");
for (String each : list) {
buf.append(sep).append(each);
}
Separator wraps a delimiter. The delimiter is returned by Separator's toString method, unless on the first call which returns the empty string!
Source code for class Separator
public class Separator {
private boolean skipFirst;
private final String value;
public Separator() {
this(", ");
}
public Separator(String value) {
this.value = value;
this.skipFirst = true;
}
public void reset() {
skipFirst = true;
}
public String toString() {
String sep = skipFirst ? "" : value;
skipFirst = false;
return sep;
}
}
You can use Java's StringBuilder type for this. There's also StringBuffer, but it contains extra thread safety logic that is often unnecessary.
And a minimal one (if you don't want to include Apache Commons or Gauva into project dependencies just for the sake of joining strings)
/**
*
* #param delim : String that should be kept in between the parts
* #param parts : parts that needs to be joined
* #return a String that's formed by joining the parts
*/
private static final String join(String delim, String... parts) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < parts.length - 1; i++) {
builder.append(parts[i]).append(delim);
}
if(parts.length > 0){
builder.append(parts[parts.length - 1]);
}
return builder.toString();
}
Why not write your own join() method? It would take as parameters collection of Strings and a delimiter String. Within the method iterate over the collection and build up your result in a StringBuffer.
If you are using Spring MVC then you can try following steps.
import org.springframework.util.StringUtils;
List<String> groupIds = new List<String>;
groupIds.add("a");
groupIds.add("b");
groupIds.add("c");
String csv = StringUtils.arrayToCommaDelimitedString(groupIds.toArray());
It will result to a,b,c
If you're using Eclipse Collections, you can use makeString() or appendString().
makeString() returns a String representation, similar to toString().
It has three forms
makeString(start, separator, end)
makeString(separator) defaults start and end to empty strings
makeString() defaults the separator to ", " (comma and space)
Code example:
MutableList<Integer> list = FastList.newListWith(1, 2, 3);
assertEquals("[1/2/3]", list.makeString("[", "/", "]"));
assertEquals("1/2/3", list.makeString("/"));
assertEquals("1, 2, 3", list.makeString());
assertEquals(list.toString(), list.makeString("[", ", ", "]"));
appendString() is similar to makeString(), but it appends to an Appendable (like StringBuilder) and is void. It has the same three forms, with an additional first argument, the Appendable.
MutableList<Integer> list = FastList.newListWith(1, 2, 3);
Appendable appendable = new StringBuilder();
list.appendString(appendable, "[", "/", "]");
assertEquals("[1/2/3]", appendable.toString());
If you can't convert your collection to an Eclipse Collections type, just adapt it with the relevant adapter.
List<Object> list = ...;
ListAdapter.adapt(list).makeString(",");
Note: I am a committer for Eclipse collections.
You should probably use a StringBuilder with the append method to construct your result, but otherwise this is as good of a solution as Java has to offer.
Why don't you do in Java the same thing you are doing in ruby, that is creating the delimiter separated string only after you've added all the pieces to the array?
ArrayList<String> parms = new ArrayList<String>();
if (someCondition) parms.add("someString");
if (anotherCondition) parms.add("someOtherString");
// ...
String sep = ""; StringBuffer b = new StringBuffer();
for (String p: parms) {
b.append(sep);
b.append(p);
sep = "yourDelimiter";
}
You may want to move that for loop in a separate helper method, and also use StringBuilder instead of StringBuffer...
Edit: fixed the order of appends.
With Java 5 variable args, so you don't have to stuff all your strings into a collection or array explicitly:
import junit.framework.Assert;
import org.junit.Test;
public class StringUtil
{
public static String join(String delim, String... strings)
{
StringBuilder builder = new StringBuilder();
if (strings != null)
{
for (String str : strings)
{
if (builder.length() > 0)
{
builder.append(delim).append(" ");
}
builder.append(str);
}
}
return builder.toString();
}
#Test
public void joinTest()
{
Assert.assertEquals("", StringUtil.join(",", null));
Assert.assertEquals("", StringUtil.join(",", ""));
Assert.assertEquals("", StringUtil.join(",", new String[0]));
Assert.assertEquals("test", StringUtil.join(",", "test"));
Assert.assertEquals("foo, bar", StringUtil.join(",", "foo", "bar"));
Assert.assertEquals("foo, bar, x", StringUtil.join(",", "foo", "bar", "x"));
}
}
For those who are in a Spring context their StringUtils class is useful as well:
There are many useful shortcuts like:
collectionToCommaDelimitedString(Collection coll)
collectionToDelimitedString(Collection coll, String delim)
arrayToDelimitedString(Object[] arr, String delim)
and many others.
This can be helpful if you are not already using Java 8 and you are already in a Spring context.
I prefer it against the Apache Commons (although very good as well) for the Collection support which is easier like this:
// Encoding Set<String> to String delimited
String asString = org.springframework.util.StringUtils.collectionToDelimitedString(codes, ";");
// Decoding String delimited to Set
Set<String> collection = org.springframework.util.StringUtils.commaDelimitedListToSet(asString);
You can try something like this:
StringBuilder sb = new StringBuilder();
if (condition) { sb.append("elementName").append(","); }
if (anotherCondition) { sb.append("anotherElementName").append(","); }
String parameterString = sb.toString();
So basically something like this:
public static String appendWithDelimiter(String original, String addition, String delimiter) {
if (original.equals("")) {
return addition;
} else {
StringBuilder sb = new StringBuilder(original.length() + addition.length() + delimiter.length());
sb.append(original);
sb.append(delimiter);
sb.append(addition);
return sb.toString();
}
}
Don't know if this really is any better, but at least it's using StringBuilder, which may be slightly more efficient.
Down below is a more generic approach if you can build up the list of parameters BEFORE doing any parameter delimiting.
// Answers real question
public String appendWithDelimiters(String delimiter, String original, String addition) {
StringBuilder sb = new StringBuilder(original);
if(sb.length()!=0) {
sb.append(delimiter).append(addition);
} else {
sb.append(addition);
}
return sb.toString();
}
// A more generic case.
// ... means a list of indeterminate length of Strings.
public String appendWithDelimitersGeneric(String delimiter, String... strings) {
StringBuilder sb = new StringBuilder();
for (String string : strings) {
if(sb.length()!=0) {
sb.append(delimiter).append(string);
} else {
sb.append(string);
}
}
return sb.toString();
}
public void testAppendWithDelimiters() {
String string = appendWithDelimitersGeneric(",", "string1", "string2", "string3");
}
Your approach is not too bad, but you should use a StringBuffer instead of using the + sign. The + has the big disadvantage that a new String instance is being created for each single operation. The longer your string gets, the bigger the overhead. So using a StringBuffer should be the fastest way:
public StringBuffer appendWithDelimiter( StringBuffer original, String addition, String delimiter ) {
if ( original == null ) {
StringBuffer buffer = new StringBuffer();
buffer.append(addition);
return buffer;
} else {
buffer.append(delimiter);
buffer.append(addition);
return original;
}
}
After you have finished creating your string simply call toString() on the returned StringBuffer.
Instead of using string concatenation, you should use StringBuilder if your code is not threaded, and StringBuffer if it is.
You're making this a little more complicated than it has to be. Let's start with the end of your example:
String parameterString = "";
if ( condition ) parameterString = appendWithDelimiter( parameterString, "elementName", "," );
if ( anotherCondition ) parameterString = appendWithDelimiter( parameterString, "anotherElementName", "," );
With the small change of using a StringBuilder instead of a String, this becomes:
StringBuilder parameterString = new StringBuilder();
if (condition) parameterString.append("elementName").append(",");
if (anotherCondition) parameterString.append("anotherElementName").append(",");
...
When you're done (I assume you have to check a few other conditions as well), just make sure you remove the tailing comma with a command like this:
if (parameterString.length() > 0)
parameterString.deleteCharAt(parameterString.length() - 1);
And finally, get the string you want with
parameterString.toString();
You could also replace the "," in the second call to append with a generic delimiter string that can be set to anything. If you have a list of things you know you need to append (non-conditionally), you could put this code inside a method that takes a list of strings.
//Note: if you have access to Java5+,
//use StringBuilder in preference to StringBuffer.
//All that has to be replaced is the class name.
//StringBuffer will work in Java 1.4, though.
appendWithDelimiter( StringBuffer buffer, String addition,
String delimiter ) {
if ( buffer.length() == 0) {
buffer.append(addition);
} else {
buffer.append(delimiter);
buffer.append(addition);
}
}
StringBuffer parameterBuffer = new StringBuffer();
if ( condition ) {
appendWithDelimiter(parameterBuffer, "elementName", "," );
}
if ( anotherCondition ) {
appendWithDelimiter(parameterBuffer, "anotherElementName", "," );
}
//Finally, to return a string representation, call toString() when returning.
return parameterBuffer.toString();
So a couple of things you might do to get the feel that it seems like you're looking for:
1) Extend List class - and add the join method to it. The join method would simply do the work of concatenating and adding the delimiter (which could be a param to the join method)
2) It looks like Java 7 is going to be adding extension methods to java - which allows you just to attach a specific method on to a class: so you could write that join method and add it as an extension method to List or even to Collection.
Solution 1 is probably the only realistic one, now, though since Java 7 isn't out yet :) But it should work just fine.
To use both of these, you'd just add all your items to the List or Collection as usual, and then call the new custom method to 'join' them.

Categories