Q: Is there any way to simplify this stream expression? protected static String paramsAsString(Object[] args) {
    return Arrays.stream(args).map((o) -> o != null && o.getClass().isArray() ? ArrayUtils.toString(o) : o)
            .collect(Collectors.toList()).toString();
}

unit test
public void paramsAsString() throws Exception {
    String[] strings = {"1" , "2"};
    int[] ints = {3,4};
    int[] intsEmpty = {3,4};
    Object[] args = {"aaa" ,"zzz" , ints , strings, intsEmpty, null};
    String paramsAsString = paramsAsString(args);
    assertEquals("[aaa, zzz, {3,4}, {1,2}, {3,4}, null]", paramsAsString);
}

I'm just learning stream api. 
I wonder is there there any way to simplify this stream expression and remove the complex if?

A: If you just reference ArrayUtils::toString, you avoid the conditional:
protected static String paramsAsString(Object[] args) {
    return Arrays.stream(args)
                 .map(ArrayUtils::toString)
                 .collect(Collectors.toList()).toString();
}

but your test fails beause you get:
[aaa, zzz, {3,4}, {1,2}, {3,4}, {}]
instead of
[aaa, zzz, {3,4}, {1,2}, {3,4}, null]
You could "hack" this with a regex:
 return Arrays.stream(args)
              .map(ArrayUtils::toString)
              .collect(Collectors.toList())
              .toString().replaceAll("\\{\\}" , "null");
 }


But personally, i would keep the conditional and convert the lambda to another utility method:
public class MyArrayUtils {
     protected static String paramsAsString(Object[] args) {
         return Arrays.stream(args)
                      .map(MyArrayUtils::objectToString)
                      .collect(Collectors.toList()).toString();
     }

     private static String objectToString(Object object) {
         return object == null ? null : ArrayUtils.toString(object);
     }
}


A: It's not clear to me whether the output you expect is really what you want or if it is just an artifact of your current code (i.e. combining the output of the third-party library method ArrayUtils.toString(o) with List.toString).
When you simply use Arrays.deepToString you already get
"[aaa, zzz, [3, 4], [1, 2], [3, 4], null]"

If you really want to use the Stream API as an exercise or want to have real control over the format, you may do it this way:
using java.lang.reflect.Array
public class MyArrayUtil {
    static String paramsAsString(Object o) {
        if(o==null) return "null";
        if(!o.getClass().isArray()) return o.toString();
        return IntStream.range(0, Array.getLength(o))
            .mapToObj(i->Array.get(o, i)).map(MyArrayUtil::paramsAsString)
            .collect(Collectors.joining(", ", "{", "}"));
    }
}

This does not hide any utility method usage but is capable of formatting any array on its own. And it doesn't create an intermediate List to use its toString() method. Instead, it uses the appropriate Collector which allows you to control the output. In the example above, I used {, } so the result will be:
{aaa, zzz, {3, 4}, {1, 2}, {3, 4}, null}

If you want higher performance you may sacrifice readability and use the following derived code:
static String paramsAsString(Object o) {
    if(o==null) return "null";
    if(!o.getClass().isArray()) return o.toString();
    Stream<String> stream;
    if (o instanceof Object[]) {
        stream=Arrays.stream((Object[]) o).map(MyArrayUtil::paramsAsString);
    }
    else if (o instanceof int[]) {
        stream=Arrays.stream((int[])o).mapToObj(String::valueOf);
    }
    else if (o instanceof long[]) {
        stream=Arrays.stream((long[])o).mapToObj(String::valueOf);
    }
    else if (o instanceof double[]) {
        stream=Arrays.stream((double[])o).mapToObj(String::valueOf);
    }
    else stream=IntStream.range(0, Array.getLength(o))
        .mapToObj(i->Array.get(o, i)).map(Object::toString);
    return stream.collect(Collectors.joining(", ", "{", "}"));
}

