AI 时代,读源码还有用吗?从 Kotlin 集合排序说起

各位 Kotlin 爱好者们,早上好,我相信各位已经特别精通排序了,毕竟谁没有刷过几个月 leetcode 呢。

但我依然希望你花五分钟时间,看看在 Kotlin 中,排序功能是如何完成的,在这个时代,没准你也会为你的 AI 伙伴优化几行代码,甚至在 AGENTS.md 文件中写上几句:如果使用排序,请使用如下方法。

排序

Kotlin 中给集合排序,看起来并不是什么复杂的事情:升序用 sorted(),降序用 sortedDescending(),按某个字段排序就用 sortedBy()

如果继续往下看,就会发现"调整元素顺序"其实至少包含三件完全不同的事情:

  • 排序:按照某种规则重新排列元素;
  • 反转:把集合当前的顺序倒过来;
  • 洗牌:随机打乱元素顺序。

这三类操作的结果、复杂度以及是否修改原集合都不一样。尤其是 sortedDescending()reversed(),虽然有时看起来能得到相同结果,但它们表达的含义完全不同。

这篇文章就从最常用的 API 开始,一点点看进 Kotlin 标准库的实现。

排序、反转和洗牌

假设有下面这个集合:

kotlin 复制代码
val numbers = listOf(3, 1, 2)

分别调用三个函数:

kotlin 复制代码
println(numbers.sorted())    // [1, 2, 3]
println(numbers.reversed())  // [2, 1, 3]
println(numbers.shuffled())  // 每次结果都可能不同

sorted() 会根据元素的大小重新排序;reversed() 不关心元素大小,它只做一件事儿------把原来的顺序倒过来;shuffled() 则会生成一个随机排列。

因此,如果一个集合原本已经按照升序排列:

kotlin 复制代码
val sorted = listOf(1, 2, 3)

println(sorted.sortedDescending()) // [3, 2, 1]
println(sorted.reversed())         // [3, 2, 1]

两者碰巧得到了相同的结果。但只要原集合不是升序排列,这种等价关系就不存在了。

自然顺序排序

Kotlin 提供了两个最基础的排序函数:

  • sorted():按照自然顺序升序排列;
  • sortedDescending():按照自然顺序降序排列。

例如:

kotlin 复制代码
val names = listOf("rock", "byte", "coding")

println(names.sorted())
// [byte, coding, rock]

println(names.sortedDescending())
// [rock, coding, byte]

这里的"自然顺序",来自元素自身实现的 Comparable

StringInt 等常见类型本身已经实现了 Comparable,所以可以直接调用 sorted()。如果是我们自己定义的类型,也可以通过实现 Comparable 给它定义一个默认顺序:

kotlin 复制代码
data class Developer(
    val name: String,
    val experience: Int,
) : Comparable<Developer> {

    override fun compareTo(other: Developer): Int {
        return experience.compareTo(other.experience)
    }
}

现在,Developer 的自然顺序就是按照工作年限升序排列:

kotlin 复制代码
val developers = listOf(
    Developer("compose", 5),
    Developer("kotlin", 3),
    Developer("android", 7),
)

println(developers.sorted())
// [Developer(name=kotlin, experience=3),
//  Developer(name=compose, experience=5),
//  Developer(name=android, experience=7)]

不过,实现 Comparable 意味着我们在表达:这个类型存在一个清晰、稳定并且足够通用的默认顺序。

如果排序规则只适用于某个页面或某个业务场景,就没有必要为了排序去修改数据类型。这时使用 Comparator 通常更加合适,后面会详细讲到。

还有一点需要注意:sorted()sortedDescending() 都会返回新的 List,不会修改原集合。

kotlin 复制代码
val source = mutableListOf(3, 1, 2)
val result = source.sorted()

println(source) // [3, 1, 2]
println(result) // [1, 2, 3]

如果希望直接修改 MutableList 或数组,应该使用 sort()sortDescending() 等原地排序函数。

Kotlin 的大部分函数,可以从英文命名方式来猜测其行为,例如 sorted() 表示返回的结果已经经过排序了,而 sort() 表示对当前结构进行排序。

sorted() 内部做了什么

下面是 Kotlin/JVM 标准库中 Iterable<T>.sorted() 的核心实现。为了方便阅读,这里省略了部分注解:

kotlin 复制代码
public fun <T : Comparable<T>> Iterable<T>.sorted(): List<T> {
    if (this is Collection) {
        if (size <= 1) return this.toList()

        @Suppress("UNCHECKED_CAST")
        return (toTypedArray<Comparable<T>>() as Array<T>)
            .apply { sort() }
            .asList()
    }

    return toMutableList().apply { sort() }
}

它并没有一上来就对所有 Iterable 使用同一种处理方式,而是先判断当前对象是不是 Collection

Collection:先转数组,再排序

Collection 有明确的 size,标准库可以提前知道需要多大的存储空间。因此,当元素数量大于 1 时,它会沿着下面这条路径执行:

text 复制代码
Collection -> Array -> 原地排序 -> List

具体可以分成三步:

  1. 调用 toTypedArray(),把集合复制到一个临时数组中;
  2. 对这个临时数组调用 sort(),原地完成排序;
  3. 调用 asList(),把排序后的数组包装成 List 返回。

asList() 不会再次复制数组,而是返回一个由该数组支持的只读 List 视图。由于这个临时数组没有暴露给调用者,所以从外部使用时,可以把返回值理解为一次独立的排序结果。

当集合只有 0 个或 1 个元素时,根本没有排序的必要,因此直接调用 toList() 返回。

这里仍然会创建一个结果列表,而不是简单地返回 this。这是因为 sorted() 的返回类型是只读 List,并且它不应该把原来的可变集合直接泄漏出去。

普通 Iterable:先收集到 MutableList

Iterable 只保证可以获取迭代器,并不保证能提前知道元素数量,也不保证底层数据支持随机访问。

因此,标准库会走另一条路径:

kotlin 复制代码
return toMutableList().apply { sort() }

它先完整遍历 Iterable,把所有元素收集到一个新的 MutableList 中,然后再原地排序。

无论走哪条路径,排序都不可能只看一部分数据。要确定一个元素最终应该放在哪里,就必须把参与排序的元素都拿到手。因此,排序本质上是一个需要保存全部数据的有状态操作。

Sequence

在很多情况下,Sequence 都很特殊。

Sequence<T> 并没有继承 Iterable<T>,它拥有自己的一套 sorted()sortedBy()sortedWith() 扩展函数:

kotlin 复制代码
val sequence = sequenceOf(3, 1, 2)
val result: Sequence<Int> = sequence.sorted()

Sequence.sorted() 返回的仍然是 Sequence,所以从 API 表面看,它是一个惰性的中间操作。不过,它同时也是一个有状态操作:真正开始迭代时,依旧需要先读取全部元素并完成排序,然后才能产出第一个结果。

这也意味着不要对无限序列调用 sorted()。因为它永远收集不完所有元素,自然也永远无法给出第一个排序结果。

这一点,非常重要!!!

快速排序

sorted() 并不总会进入双轴快速排序。实际上,在 JVM 上需要区分两类数组:

  • IntArrayLongArray 等基本类型数组,JDK 常使用双轴快速排序等实现;
  • Array<T> 这样的对象数组,排序要求保持稳定,常见 JDK 实现使用 TimSort 一类的稳定排序算法。

Iterable<T>.sorted() 转换出来的是对象数组,因此更接近第二种情况。

当然,标准库源码最终采用哪一种具体算法,属于平台和 JDK 实现细节,未来也可能变化。对业务代码来说,更值得依赖的是 API 明确承诺的性质:Kotlin 的这些对象排序函数是稳定排序

什么是稳定排序

稳定排序,各位可能比我懂得多了。

它的大致意思是:如果两个元素在比较器看来相等,那么排序之后,它们仍然会保持原来的相对顺序。

例如:

kotlin 复制代码
data class Developer(
    val name: String,
    val experience: Int,
)

val developers = listOf(
    Developer("Alice", 5),
    Developer("Bob", 3),
    Developer("Charlie", 5),
)

val result = developers.sortedBy { it.experience }

结果为:

text 复制代码
Bob(3), Alice(5), Charlie(5)

AliceCharlie 的工作年限相同,所以它们排序后的先后关系没有发生变化。

这一点在多级排序和分阶段排序时尤其重要。不过在 Kotlin 中,如果规则明确,通常还是建议把多个条件写进同一个 Comparator,这样更容易读懂。

使用 sortedWith()

当对象没有自然顺序,或者当前场景需要一套临时规则时,可以使用 sortedWith()

kotlin 复制代码
val sortedByMultiple = developers.sortedWith(
    compareByDescending<Developer> { it.experience }
        .thenBy { it.name }
)

这段代码表达了两个排序条件:

  1. 先按照工作年限降序排列;
  2. 如果工作年限相同,再按照名字升序排列。

输出结果类似下面这样:

text 复制代码
Developer(name=android, experience=7)
Developer(name=compose, experience=5)
Developer(name=kotlin, experience=3)

compareBy()compareByDescending()thenBy()thenByDescending() 本质上都在帮助我们组合 Comparator

如果手动编写,大概会是这样:

kotlin 复制代码
val comparator = Comparator<Developer> { left, right ->
    val experienceResult =
        right.experience.compareTo(left.experience)

    if (experienceResult != 0) {
        experienceResult
    } else {
        left.name.compareTo(right.name)
    }
}

显然,使用标准库提供的比较器工厂更简洁,也不容易把升序和降序写反。

sortedWith() 的源码

sortedWith() 的实现和 sorted() 几乎一样:

kotlin 复制代码
public fun <T> Iterable<T>.sortedWith(
    comparator: Comparator<in T>,
): List<T> {
    if (this is Collection) {
        if (size <= 1) return this.toList()

        @Suppress("UNCHECKED_CAST")
        return (toTypedArray<Any?>() as Array<T>)
            .apply { sortWith(comparator) }
            .asList()
    }

    return toMutableList().apply {
        sortWith(comparator)
    }
}

唯一真正发生变化的地方,是排序时多传入了一个 Comparator

对于 Collection,它仍然会先转成数组,然后调用数组的 sortWith(comparator);对于普通 Iterable,则先转成 MutableList,再调用列表的 sortWith(comparator)

这也是 Kotlin 标准库中很常见的一种设计方式:先提供一个能力完整的底层函数,再通过一些更易用的函数去组合它,而不是把同一套排序逻辑复制很多遍。

我们自己写的代码,最终也会形成一套基本逻辑,然后各个业务通过基本逻辑进行衍生、变化、发展。

更好用的 sortedWith() ------ sortedBy

如果只是想按照某个字段排序,直接使用 sortedWith() 会稍微有些啰嗦:

kotlin 复制代码
developers.sortedWith(
    compareBy { it.experience }
)

因此 Kotlin 又提供了 sortedBy()

kotlin 复制代码
val sortedByExperience = developers.sortedBy {
    it.experience
}

它的源码非常简单:

kotlin 复制代码
public inline fun <T, R : Comparable<R>> Iterable<T>.sortedBy(
    crossinline selector: (T) -> R?,
): List<T> {
    return sortedWith(compareBy(selector))
}

sortedBy() 自己不负责排序,它只做了两件事:

  1. 使用 compareBy(selector) 创建一个 Comparator
  2. 把这个 Comparator 交给 sortedWith()

因此,sortedBy() 会自动继承 sortedWith() 的实现和优化路径。

sortedByDescending() 也是同样的思路:

kotlin 复制代码
public inline fun <T, R : Comparable<R>>
    Iterable<T>.sortedByDescending(
        crossinline selector: (T) -> R?,
    ): List<T> {
    return sortedWith(compareByDescending(selector))
}

至于 sortedDescending(),它同样没有重新实现一套降序排序逻辑:

kotlin 复制代码
public fun <T : Comparable<T>>
    Iterable<T>.sortedDescending(): List<T> {
    return sortedWith(reverseOrder())
}

reverseOrder() 会生成一个与自然顺序相反的 Comparator,再由 sortedWith() 完成真正的排序。

可以把它们的关系简单理解为:

text 复制代码
sortedDescending() ─┐
sortedBy()          ├─> 构造 Comparator ─> sortedWith()
sortedByDescending()┘

sortedBy() 的选择器可能执行很多次

sortedBy { it.experience } 看起来像是先把每个元素转换成 experience,然后再对这些值排序,但实际并不是这样。

compareBy(selector) 创建的比较器,在每次比较两个元素时,都会分别调用一次 selector。而一个元素在完整排序过程中通常会参与多次比较,所以 selector 也可能被重复执行很多次。

如果选择器只是读取一个字段,完全不必担心:

kotlin 复制代码
developers.sortedBy { it.experience }

但如果里面包含数据库查询、文件读取、复杂解析或昂贵计算,就不适合直接这样写:

kotlin 复制代码
items.sortedBy { calculateExpensiveKey(it) }

更稳妥的做法是先计算一次排序键:

kotlin 复制代码
val result = items
    .map { item -> item to calculateExpensiveKey(item) }
    .sortedBy { (_, key) -> key }
    .map { (item, _) -> item }

这会增加一份中间数据,但能够避免排序过程中重复计算昂贵的键。

是否值得这样做,取决于集合大小和选择器的成本,这就依赖开发者经验了。

排序时如何处理 null

sortedBy() 的选择器可以返回可空类型:

kotlin 复制代码
data class User(
    val name: String,
    val score: Int?,
)

默认情况下,compareBy { it.score } 会把 null 视为比非空值更小,因此升序排列时 null 会出现在前面。

如果业务规则要求把 null 放到最后,可以显式指定:

kotlin 复制代码
val comparator = compareBy<User, Int?>(
    nullsLast(naturalOrder())
) { it.score }

val result = users.sortedWith(comparator)

比起依赖默认行为,把 null 的位置写出来往往更容易理解,尤其是在比较规则比较复杂时。

编写 Comparator 时不要直接做减法

有些代码会使用两个整数相减来实现比较器:

kotlin 复制代码
// 不推荐
val comparator = Comparator<Developer> { a, b ->
    a.experience - b.experience
}

我相信各位开发者们应该经常这样写。

对于普通的小数字,它看起来没有问题。但如果两个值接近 Int 的上下边界,减法可能溢出,最终得到错误的顺序。

应该使用 compareTo()compareValues()

kotlin 复制代码
val comparator = Comparator<Developer> { a, b ->
    a.experience.compareTo(b.experience)
}

除此之外,一个正确的比较器还应该保持规则一致。例如,如果 a > bb > c,那么结果也应该满足 a > c。否则,排序结果可能不可预测,某些平台实现甚至会直接抛出异常。

reversed():反转的是当前顺序

排序讲完之后,再来看反转。

kotlin 复制代码
val list = listOf("kotlin", "Android", "skydoves")
val reversedList = list.reversed()

println(reversedList)
// [skydoves, Android, kotlin]

reversed() 不会比较任何元素。它只关心元素当前所处的位置,然后把第一个和最后一个交换、第二个和倒数第二个交换,直到走到中间。

Iterable<T>.reversed() 的核心实现如下:

kotlin 复制代码
public fun <T> Iterable<T>.reversed(): List<T> {
    if (this is Collection && size <= 1) {
        return toList()
    }

    val list = toMutableList()
    list.reverse()
    return list
}

可以看到,它的过程非常简单直接:

  1. 先处理 0 个或 1 个元素的情况;
  2. 调用 toMutableList() 创建一个副本;
  3. 对副本调用 reverse()
  4. 返回反转后的结果。

所以 reversed() 不会修改原集合:

kotlin 复制代码
val source = mutableListOf(1, 2, 3)
val result = source.reversed()

println(source) // [1, 2, 3]
println(result) // [3, 2, 1]

如果希望直接修改可变列表,则使用 reverse()

kotlin 复制代码
val source = mutableListOf(1, 2, 3)
source.reverse()

println(source) // [3, 2, 1]

函数名只差一个字母 d,行为却完全不同:

函数 返回结果 是否修改接收者
reversed() 新的 List
reverse() Unit

JVM 上的 Collections.reverse()

在 JVM 平台,MutableList.reverse() 最终会使用 java.util.Collections.reverse()。它并不是不加判断地使用下标访问,而是会根据列表类型选择不同路径。

其核心逻辑可以简化为:

java 复制代码
public static void reverse(List<?> list) {
    int size = list.size();

    if (size < REVERSE_THRESHOLD || list instanceof RandomAccess) {
        for (int i = 0, mid = size >> 1, j = size - 1;
             i < mid;
             i++, j--) {
            swap(list, i, j);
        }
    } else {
        ListIterator front = list.listIterator();
        ListIterator back = list.listIterator(size);

        for (int i = 0, mid = size >> 1; i < mid; i++) {
            Object value = front.next();
            front.set(back.previous());
            back.set(value);
        }
    }
}

支持随机访问的列表

ArrayList 实现了 RandomAccess,按下标读取和写入元素都是常数时间,因此最适合使用左右指针交换:

text 复制代码
[A, B, C, D]
 ↑        ↑
 i        j

先交换 AD,再交换 BC,整个过程只需要遍历一半元素,时间复杂度仍然是 O(n)

不支持随机访问的列表

LinkedList 没有实现 RandomAccess。如果仍然反复调用 get(i)set(i),每一次下标访问都可能重新遍历链表,最终很容易退化为 O(n²)

所以 Collections.reverse() 会在列表较大时改用两个 ListIterator:一个从前往后走,另一个从后往前走。这样仍然只需要线性遍历。

这段实现会根据数据结构的访问特性选择合适的遍历方式,这种特殊化的做法,在前一篇文章中,亦有讲解。

数组的 reversed() 和 reverse()

数组同样提供 reversed()

kotlin 复制代码
public fun <T> Array<out T>.reversed(): List<T> {
    if (isEmpty()) return emptyList()

    val list = toMutableList()
    list.reverse()
    return list
}

注意它返回的是新的 List<T>,并不会修改原数组。

而数组的 reverse() 会直接修改数组本身。它的核心实现就是经典的双指针交换:

kotlin 复制代码
public fun <T> Array<T>.reverse() {
    val midPoint = (size / 2) - 1
    if (midPoint < 0) return

    var reverseIndex = lastIndex

    for (index in 0..midPoint) {
        val tmp = this[index]
        this[index] = this[reverseIndex]
        this[reverseIndex] = tmp
        reverseIndex--
    }
}

对于奇数长度的数组,中间元素不需要移动;对于偶数长度的数组,刚好交换 size / 2 次。

整个操作的时间复杂度是 O(n),如果忽略临时交换变量,占用的额外空间是 O(1)

asReversed():不复制数据的反向视图

如果接收者是 List,Kotlin 还提供了一个容易被忽略的函数:asReversed()

kotlin 复制代码
val source = mutableListOf(1, 2, 3)
val view = source.asReversed()

println(view) // [3, 2, 1]

source += 4
println(view) // [4, 3, 2, 1]

reversed() 会复制元素并生成一个独立结果;asReversed() 返回的则是原列表的反向视图。它不需要复制整份数据,但原列表发生变化时,视图也会随之变化。

因此两者并没有绝对的好坏:

  • 需要独立快照时,使用 reversed()
  • 需要避免复制,并且能接受结果与原列表联动时,使用 asReversed()

shuffled():随机打乱顺序

最后来看洗牌:

kotlin 复制代码
val frameworks = listOf("Compose", "Flutter", "React")

println(frameworks.shuffled())
// 每次输出都可能不同

shuffled() 的实现和 reversed() 有些相似:先创建可变副本,再在副本上执行原地操作。

kotlin 复制代码
public fun <T> Iterable<T>.shuffled(): List<T> =
    toMutableList().apply { shuffle() }

因此,它同样不会修改原集合。

如果手里本来就是 MutableList,并且希望直接打乱它,可以调用 shuffle()

kotlin 复制代码
val frameworks = mutableListOf("Compose", "Flutter", "React")
frameworks.shuffle()

shuffle and shuffled,此时此刻,恰如 sort and sorted

Fisher--Yates 洗牌算法

Kotlin 的洗牌实现采用的是现代 Fisher--Yates 算法。它的思路是从后往前遍历,每次从尚未确定的位置中随机选出一个元素,放到当前位置。

核心代码如下:

kotlin 复制代码
public fun <T> MutableList<T>.shuffle(random: Random) {
    for (i in lastIndex downTo 1) {
        val j = random.nextInt(i + 1)
        this[j] = this.set(i, this[j])
    }
}

第一次看到最后一行,可能会觉得有些绕:

kotlin 复制代码
this[j] = this.set(i, this[j])

set(i, value) 在写入新值的同时,还会返回位置 i 原来的元素。因此,这一行代码其实完成了一次交换:

kotlin 复制代码
val oldValueAtI = this.set(i, this[j])
this[j] = oldValueAtI

为了看清楚整个过程,假设现在有一个列表:

text 复制代码
[A, B, C, D]

第一次循环,i = 3,从 0..3 中随机得到 j = 1,交换 BD

text 复制代码
[A, D, C, B]

此时最后一个位置已经确定,后续不会再动它。

第二次循环,i = 2,从 0..2 中随机选择一个位置。假设选中了 j = 2,那么 C 与自己交换,列表保持不变:

text 复制代码
[A, D, C, B]

第三次循环,i = 1,从 0..1 中随机得到 j = 0,交换 AD

text 复制代码
[D, A, C, B]

循环结束后,索引 0 的元素也自然确定下来。

对于基于数组的列表,这个过程只需要从后向前遍历一次,因此时间复杂度是 O(n)。原地洗牌本身只需要常数级额外空间;而 shuffled() 还需要先复制出一个新的列表,所以整体需要 O(n) 的结果空间。

在 JVM 上,无参数的 MutableList.shuffle() 会直接委托给 java.util.Collections.shuffle()。OpenJDK 会检查列表是否支持随机访问:对于 ArrayList 这类实现了 RandomAccess 的列表,它会直接通过下标交换元素;对于元素数量达到阈值的非随机访问列表,例如 LinkedList,则会先将元素复制到数组中完成洗牌,再通过迭代器写回,从而避免频繁的下标访问导致 O(n2) 的性能问题。

数组的 shuffle()

数组的洗牌逻辑几乎一样,只是交换元素时使用了一个临时变量:

kotlin 复制代码
public fun <T> Array<T>.shuffle(random: Random) {
    for (i in lastIndex downTo 1) {
        val j = random.nextInt(i + 1)

        val copy = this[i]
        this[i] = this[j]
        this[j] = copy
    }
}

数组本身支持常数时间的下标访问,因此每次交换都是 O(1),整个算法仍然是 O(n)

Fisher--Yates 的重要性质是:只要随机数生成器本身没有偏差,每一种可能的排列出现的概率都相同。

测试时如何得到可重复的结果

默认的 shuffled() 每次都可能产生不同结果,这在实际使用中没有问题,但会让单元测试变得不稳定。

Kotlin 允许传入指定的随机数生成器:

kotlin 复制代码
val result = frameworks.shuffled(Random(42))

固定种子后,同一平台和实现下可以得到可重复的随机序列,更适合测试和问题复现。

不过,kotlin.random.Random 并不是为密码、令牌等安全场景设计的。如果随机结果涉及安全性,应该使用平台提供的密码学安全随机数生成器。

到底应该选择哪个函数

最后把这些函数放在一起看:

需求 返回新结果 原地修改
按自然顺序升序排列 sorted() sort()
按自然顺序降序排列 sortedDescending() sortDescending()
按字段排序 sortedBy() / sortedByDescending() sortBy() / sortByDescending()
使用复杂比较规则 sortedWith() sortWith()
反转当前顺序 reversed() reverse()
获取反向视图 asReversed() ---
随机打乱顺序 shuffled() shuffle()

它们的常见复杂度如下:

操作 时间复杂度 新结果所需空间 是否稳定
排序 O(n log n) O(n)
反转 O(n) O(n) 不涉及比较
洗牌 O(n) O(n) 不涉及比较

表中的空间复杂度针对 sorted()reversed()shuffled() 这类返回新结果的函数。原地版本不需要再创建一份完整结果,但具体排序算法仍可能使用额外的临时空间。

一点想法

Kotlin 提供了 sortedsortedBysortedWithreversedshuffled 等集合操作函数,可以灵活地调整集合中元素的排列顺序。它们不仅支持自定义比较器,还可以组合多个排序条件,让我们能够以函数式的方式处理数据,代码也更加简洁、清晰。

有人可能会问:现在 AI 写代码已经这么普遍,还有必要花时间去看这些标准库的内部实现吗?

我的答案是:依然有必要。AI 大多数时候只是把功能跑通,它倾向于给出一个能工作的答案,而不会主动替你思考"这个选择器会被调用很多次,是不是该先把排序键算出来""这个列表不支持随机访问,原地洗牌会不会退化"。这些细节,恰恰是工程经验真正发挥作用的地方。一个优秀的工程师,对性能和代码味道的敏感度,往往超过 AI------不是因为他记得更多 API,而是因为他知道一行代码背后发生了什么。

所以,读源码的意义不在于背诵实现,而在于建立一种判断力:知道哪些地方 AI 给的方案够用,哪些地方需要自己再推一把。这种判断力,正是 AI 时代里工程师最该保留的东西。

相关推荐
️学习的小王4 小时前
二级MySQL PHP综合应用题完整考点+历年真题风格练习题
android·mysql·php
YB137516 小时前
jetpack compose 副作用 SideEffect
android·kotlin
CHB17 小时前
uni-app x 蒸汽模式 性能测试基准报告 Benchmark【Android版】
android·ios·uni-app
带娃的IT创业者18 小时前
拆掉那堵墙:LibrePods 如何让 AirPods 在 Android 上重获新生
android·开源项目·airpods·librepods·生态解锁
_阿南_20 小时前
flutter 展示的字体突然奔放了
android·flutter·ios
唐青枫1 天前
Kotlin interface 详解:别再把接口写成空壳和万能开关
kotlin
文人sec1 天前
MySQL事务与索引做了什么?
android·笔记·mysql
刘洋浪子1 天前
AndroidStudio保存日志菜单
android
朱涛的自习室1 天前
从 Prompt 到 Graph:AI 工程的进化史
android·前端·人工智能