Котлин. Попытка использовать reified-типы для разбора списков и массивов - PullRequest
2 голосов
/ 30 октября 2019

Я пытаюсь использовать reified-тип при разборе json. Он отлично работает с одной записью json, но не работает со списком.

ВОПРОСЫ:

  1. Чего мне не хватает в методе String.parseList ()?
  2. Как получилосьClassCastException при .first () несмотря на то, что присвоение прошло на одну строку раньше?
    package qa

    import com.fasterxml.jackson.databind.ObjectMapper
    import org.slf4j.LoggerFactory
    import org.testng.Assert
    import org.testng.annotations.Test

    class ReifiedParseListTest {

        data class User(var name: String = "userName", var age: Int = 0)

        val log = LoggerFactory.getLogger(this.javaClass.name)
        val objectMapper = ObjectMapper()
        val json: String = """[{"name":"Alice","age":1},{"name":"Bob","age":2}]"""
        val expected: String = "[User(name=Alice, age=1), User(name=Bob, age=2)]"


        inline fun <reified V> String.parseList(): List<V> = objectMapper
                .readValue(this, Array<V>::class.java).toList()


        @Test
        fun checkParseList_OK() {
            val actual: List<User> = objectMapper
                    .readValue(json, Array<User>::class.java).toList()

            log.info("actual.first() is of type: {}", actual.first().javaClass)
            Assert.assertEquals(actual.toString(), expected)
        }

        @Test
        fun checkParseListReified_FAILS() {
            val actual: List<User> = json.parseList<User>()
            Assert.assertEquals(actual.toString(), expected)
            // java.lang.AssertionError:
            // Expected :[User(name=Alice, age=1), User(name=Bob, age=2)]
            // Actual   :[{name=Alice, age=1}, {name=Bob, age=2}]
        }

        @Test
        fun checkParseListReifiedClassCast_FAILS() {
            val actual: List<User> = json.parseList<User>()
            log.info("actual.first() is of type: {}", actual.first().javaClass)
            // java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to qa.ReifiedParseListTest$User
        }

    }

Ответы [ 4 ]

2 голосов
/ 30 октября 2019

В этом случае reified помогает распространять класс типа, но все еще есть стирание типа.
Чтобы избежать этого, вы можете использовать что-то вроде JavaType:

inline fun <reified V> String.parseList(): List<V> {
    return objectMapper.readValue(this, objectMapper.getTypeFactory()
        .constructCollectionType(List::class.java, V::class.java))
}

Обратите внимание, чтобез reified мы не смогли бы использовать V::class.java

Теперь, чтобы ответить на ваш второй вопрос, как получается, что, хотя val actual равно List<User>, вы получаете ClassCastException - ответ сновастирание типов с некоторым запутыванием типов платформ.

Если вы посмотрите, что возвращает эта функция (это ваша функция без asList() вызова:

inline fun <reified V> String.parseList() = 
        objectMapper.readValue(this, Array<V>::class.java)

Вы заметите, что она возвращает Array<???>!, который является способом Котлина сказать "это что-то из Java, я надеюсь, что это будет работать, но я не могу обещать". Теперь, вызывая toList(), это расслабляет компилятор, говоря: "да, в конце мы возвращаемТип Kotlin, все будет хорошо ". Но на самом деле это ложное обещание.

То, что вы получите, будет Array<Any> заполнено LinkedHashMap, что, конечно, не удастся, когда их приводят к Userосновываясь на ложном обещании, которое мы дали компилятору.

1 голос
/ 01 ноября 2019

я наконец-то получил еще одно решение, которое, кажется, обрабатывает как отдельные объекты, так и списки

    inline fun <reified V> String.parse(): V = objectMapper.readValue(this, object : TypeReference<V>() {})

@Test
fun checkParseSingle() {
    val jsonSingle: String = """{"name":"Carol","age":3}"""
    val expectedSingle: String = "User(name=Carol, age=3)"

    val actual: User = jsonSingle.parse<User>()
    Assert.assertEquals(actual.toString(), expectedSingle)
}

@Test
fun checkParseList() {
    val jsonList: String = """[{"name":"Alice","age":1},{"name":"Bob","age":2}]"""
    val expectedList: String = "[User(name=Alice, age=1), User(name=Bob, age=2)]"

    val actual: List<User> = jsonList.parse<List<User>>()
    Assert.assertEquals(actual.toString(), expectedList)
}
1 голос
/ 30 октября 2019

Сбой из-за того, что Array<V>::class.java всегда возвращает класс Array<Any>. Вы можете увидеть это, выполнив следующий код:

printReifiedArr<String>() // prints `class [Ljava.lang.Object;`

inline fun <reified V> printReifiedArr() {
    println(Array<V>::class.java)
}

Ваша функция может быть исправлена ​​путем замены Array<V>::class.java на ручной класс массива с получением :

inline fun <reified V> String.parseList(): List<V> = objectMapper
                .readValue(this, Class.forName("[L${V::class.java.name};") as Class<Array<V>>).toList()

Примечание: этот подход использует коробочную версию массива примитивов, другие подходы можно найти здесь .

0 голосов
/ 10 ноября 2019

Вам нужно захватить общий тип, который T:class.java не даст. Но следующие будут работать для любого универсального типа

inline fun <reified T> jacksonTypeRef(): TypeReference<T> = object: TypeReference<T>() {}

inline fun <reified T : Any> String.parseJson(): T {
    return objectMapper.readValue(this, jacksonTypeRef<T>())
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...