7

I tried to move codebase to Kotlin from Java. But I found strange behavior in String.format.

I have both same codes (and feature, also) in Kotlin and Java.

fun callScriptMethod(methodName: String, vararg args: Any): String {
        var format = methodName
        if (!format.contains("javascript:")) {
            format = String.format("javascript:%s", format)
        }

        val objects = mutableListOf<Any>()
        for (arg in args) objects.add(arg)

        if (!objects.isEmpty()) {
            format += "("
            var i = 0
            val icnt = objects.size
            while (i < icnt) {
                format += "\'%s\'"
                if (i != icnt - 1) {
                    format += ", "
                }
                i++
            }

            format += ")"
        } else {
            format += "()"
        }

        val message = String.format(Locale.getDefault(), format, args)
        return message
    }
public static String callScriptMethod(String methodName, Object... args) {
        String format = methodName;
        if (!format.contains("javascript:")) {
            format = String.format("javascript:%s", format);
        }

        List<Object> objects = Arrays.asList(args);
        if (!objects.isEmpty()) {
            format += "(";
            for (int i = 0, icnt = objects.size(); i < icnt; i++) {
                format += "\'%s\'";
                if (i != icnt - 1) {
                    format += ", ";
                }
            }

            format += ")";
        } else {
            format += "()";
        }

        String message = String.format(format, args);
        return message;
    }

and some test code.

fun main() {
    val result = Java.callScriptMethod("nativeCallback", "1", "d8d8441n24n134n",
        "dasqhjidhkdhaskjdfhawoiudnqwaidnqwioldjnqawskld:djoashdojashdlkjasdjhas", "0")
    println(result)

    val result2 = Kotlin.callScriptMethod("nativeCallback", "1", "d8d8441n24n134n",
        "dasqhjidhkdhaskjdfhawoiudnqwaidnqwioldjnqawskld:djoashdojashdlkjasdjhas", "0")
    println(result2)
}

I can expect result is javascript:nativeCallback('1', 'd8d8441n24n134n', 'dasqhjidhkdhaskjdfhawoiudnqwaidnqwioldjnqawskld:djoashdojashdlkjasdjhas', '0').

But the version of Kotlin has exception MissingFormatArgumentException.

So, I tried to debug these codes to know the format is generated successfully.

Java: javascript:nativeCallback('%s', '%s', '%s', '%s')

Kotlin: javascript:nativeCallback('%s', '%s', '%s', '%s')

Both are the same result and have same args but has a different result.

javascript:nativeCallback('1', 'd8d8441n24n134n', 'dasqhjidhkdhaskjdfhawoiudnqwaidnqwioldjnqawskld:djoashdojashdlkjasdjhas', '0')
Exception in thread "main" java.util.MissingFormatArgumentException: Format specifier '%s'
    at java.util.Formatter.format(Formatter.java:2519)
    at java.util.Formatter.format(Formatter.java:2455)
    at java.lang.String.format(String.java:2981)
    at Kotlin.callScriptMethod(Kotlin.kt:31)
    at TestKt.main(test.kt:11)
    at TestKt.main(test.kt)

So, I want to know what is the problem. How can i do?

WindSekirun
  • 1,038
  • 11
  • 22
  • Is there a reason you're building a format string instead of concating the args directly? And what's the point of `objects`? – shmosel Apr 11 '19 at 02:54
  • @shmosel I used String.format on the assumption that the arguments could change, but now I think it would have been nice to just try concat with StringBuilder. If there is no special solution, it wouldn't be too bad to turn it that way. – WindSekirun Apr 11 '19 at 02:56
  • Whether you use StringBuilder or not is beside the point. What you're doing is just as costly. – shmosel Apr 11 '19 at 02:57
  • There's also no point in the if/else. They both produce the same result for an empty list. – shmosel Apr 11 '19 at 02:57
  • Try `String.format(format, *args)`. To pass an array as `vararg` you have to explicitly unpack it. –  Apr 11 '19 at 03:00
  • @shmosel ok, I can understand what you saying. I'll try that. – WindSekirun Apr 11 '19 at 03:02
  • @dyukha It's works, but Kotlin has 'java.lang.String(format, *args)` wrapper in String.format. it's any difference using spread operator both in my code and stdlib code? – WindSekirun Apr 11 '19 at 03:04
  • Every time you pass an array as vararg you should use spread operator: 1) when you pass it to String.format; now it's treated as an array in String.format, and this function uses * to pass it further again. So if you have a chain of such calls, you have to use * everywhere. –  Apr 11 '19 at 03:09
  • Your code is quite strange (a method name can never have a colon in it so you always need to prefix it with "javascript:" and you have the unnecessary `objects` list. In Kotlin you can replace it with a one-liner: `fun callScriptMethod(methodName: String, vararg args: Any): String = "javascript:$methodName(${args.joinToString(", ")})"` – Erwin Bolwidt Apr 11 '19 at 03:32

1 Answers1

19

As vararg becomes an array once entering the function body, you have to use the spread operator in order to pass it as vararg. https://kotlinlang.org/docs/reference/functions.html#variable-number-of-arguments-varargs

When we call a vararg-function, we can pass arguments one-by-one, e.g. asList(1, 2, 3), or, if we already have an array and want to pass its contents to the function, we use the spread operator (prefix the array with *):

val message = String.format( format, *args)

The difference with Java is that Java actually allows passing an array as vararg directly, see this SO post: Can I pass an array as arguments to a method with variable arguments in Java?

i.e. Object... in Java is technically identical to Object[], there are no "real" vararg things in Java, while vararg is a real different thing in Kotlin.

Ricky Mo
  • 6,285
  • 1
  • 14
  • 30