1

I am trying to make a generic function that builds a List of any type based on a string that holds values of that type delimited by let's say a comma. I have done this:

public static <T> List<T> stringToList(String listStr, Class<T> itemType)
{
    return Arrays.asList(listStr.split(",")).stream().map(x -> itemType.cast(x.replaceAll("\\s+|\"|\t","")))).collect(Collectors.toList());
}

When I try to test it with:

String listStringsStr  = "\"Foo\", \"Bar\"";
List<String> resS = stringToList(listStringsStr, String.class);

String listIntegersStr = "1,10,-1,0";
List<Integer> resI = stringToList(listStringsStr, Integer.class);

I have two problems.

  1. In the first case (String) I get an extra double quote around each string item: ""Foo"", ""Bar"".
  2. In the second case (Integer) I get a java.lang.ClassCastException: Cannot cast java.lang.String to java.lang.Integer which means that it can't convert "1" to 1. I know this works with Integer::parseInt, but I want to make a generic method.

Any Ideas?

[EDIT] - Because I caused confusion the way I posted it, I add the test code:

String listStringsStr  = "\"Foo\", \"Bar\"";
List<String> listStrings = Arrays.asList("Foo", "Bar");
String listIntegersStr = "1,10,-1,0";
List<Integer> listIntegers = Arrays.asList(1, 10, -1, 0);

List<String> resS = stringToList(listStringsStr, String.class);
System.out.println(resS);
System.out.println(listStrings);
assert (resS.containsAll(listStrings));

List<Integer> resI = stringToList(listIntegersStr, Integer.class);
System.out.println(resI);
System.out.println(listIntegers);
assert (resI.containsAll(listIntegers));

After including the x.replaceAll("\\s+|\"|\t","") the first assertion now pass, the second fails. The console output is

[Foo, Bar]
[Foo, Bar]
[1, 10, -1, 0]
[1, 10, -1, 0]

listIntegers holds Integer thus I suppose resI holds ints, or I just broke java's type safety :D

stelios
  • 2,679
  • 5
  • 31
  • 41
  • No, you don't have extra quotes. The only quotes you have are the ones that are in the string. Printing resS shows `resS = ["Foo", "Bar"]`. – JB Nizet Jan 27 '16 at 17:09
  • I have. When you print a String in the console the quotes are removed: List listStrings = Arrays.asList("Foo", "Bar"); assert (resS.containsAll(listStrings)); raises an Exception showing that listStrings contain ["Foo", "Bar"] where resS contains [""Foo"", ""Bar""]. If you print listStrings you will see no quotes – stelios Jan 27 '16 at 17:19
  • @chefarov If your definition is that values are comma-separated, and nothing else, then the quotes are *part of* the value. This even includes the leading space before `"Bar"`. If you want quotes to be used to allow value to contain commas, then you need to CSV Parser, not a plain `split()`, since that is CSV syntax, which is more than just comma-separating. – Andreas Jan 27 '16 at 17:26
  • @Andreas you are right I added a regex to remove unwanted characters – stelios Jan 27 '16 at 18:19
  • 1
    Don’t use Chad Dienhart’s solution, it still returns a list of String, just pretending to be a `List` due to the unchecked type cast. The expression `itemType.toString().valueOf(…)` is pure nonsense. And, further, *don’t* copy answers into your question. The answer are already there, nobody needs a copy of them inside your question. – Holger Jan 27 '16 at 18:42
  • @Holger you are right I understood that afterwards – stelios Jan 28 '16 at 09:28

1 Answers1

6

For problem #2: A simple modification would be to pass a Function<String, T> instead of Class<T>. The function would define how to parse the string as the class of the desired type. Some possible values:

Function<String, String> stringParser = s -> s;
Function<String, Integer> intParser = s -> Integer.parseInt(s);

For problem #1: In the example you gave the input strings do contain quotes. You could have made the input string "Foo,Bar" to avoid having quotes in the output. If you don't have control over the input, you can trim quotes/whitespace/etc. as part of the parsing function, or separate the words by something more advanced than splitting by commas - is the input a CSV file?

  • 1
    Additional suggestion: To allow string representation of object to include comma, use a CSV parser instead of `split()`, so quoting can be used to support embedded commas. – Andreas Jan 27 '16 at 17:17
  • There should be a simpler solution than passing a conversion function as argument to another conversion function. At least there is in any other language I know – stelios Jan 27 '16 at 17:24
  • Well, it depends on the contents of your input string. Clearly it can contain strings. What about numbers, lists, maps, or more complex objects? Are they lines from a CSV file? – Stephen Rosenthal Jan 27 '16 at 17:28
  • 1
    Well, first you are converting a `String` to an array of Strings and then you are converting a `String` to a specific type. There are two different conversions that have to be addressed and are necessary. Also, the solution is not that hard, you just do a `function.apply(x)` and then pass a Lambda expression as a call, like: `stringToList(listStringsStr, (s) -> { return Integer.parseInt(s); });` – Roberto Linares Jan 27 '16 at 17:30
  • 2
    @chefarov Your question said *"any type"*, so how would you expect generic code to know how to create an instance of my custom `Train` class (as an example) from a `String`, unless you either explicitly allow me to provide a parser, or assume that my class has a `String` constructor? – Andreas Jan 27 '16 at 17:31
  • 2
    @guitarsteve Suggestion for answer (problem #2): Show the effect of your suggestion, i.e. the second argument of `stringToList` in the two examples would be `String::toString` and `Integer::parseInt`, respectively. – Andreas Jan 27 '16 at 17:36
  • @Andreas sry I meant the simple classes representing primitive types – stelios Jan 27 '16 at 17:42
  • @Andreas thanks - I added examples. I figured explicit lambdas make it more clear how to extend the examples than method references. – Stephen Rosenthal Jan 27 '16 at 17:42
  • 1
    @chefarov: making the function work for a predefined set of classes is possible, but where’s the sense in that premise? For the caller, `Integer.class` is not simpler than `Integer::valueOf`, and semantically, you are passing a function in both cases, just using different forms, whereas the `Class` form requires some kind of Reflection to invoke it. – Holger Jan 27 '16 at 18:52
  • If I pass intParser to stringToList, replacing x -> itemType.cast(x) with map(intParser) , I get an "Error:(26, 32) java: incompatible types: inferred type does not conform to equality constraint(s) inferred: T equality constraints(s): T,java.lang.Integer " – stelios Jan 28 '16 at 10:02
  • @Holger. Yeah I suppose that finally this function may not be much meaningfull at all – stelios Jan 28 '16 at 10:26