如何在 Java 中处理 StackOverflowError?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/951635/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to handle StackOverflowError in Java?
提问by Silent Warrior
How to handle StackOverflowError
in Java ?
StackOverflowError
在Java中如何处理?
采纳答案by Huxi
I'm not sure what you mean with "handle".
我不确定你说的“把手”是什么意思。
You can certainly catch that error:
您当然可以捕获该错误:
public class Example {
public static void endless() {
endless();
}
public static void main(String args[]) {
try {
endless();
} catch(StackOverflowError t) {
// more general: catch(Error t)
// anything: catch(Throwable t)
System.out.println("Caught "+t);
t.printStackTrace();
}
System.out.println("After the error...");
}
}
but that is most likely a bad idea, unless you know exactly what you are doing.
但这很可能是一个坏主意,除非您确切地知道自己在做什么。
回答by Andrew Bullock
You probably have some infinite recursion going on.
您可能正在进行一些无限递归。
I.e. a method that calls itself over and over
即一个反复调用自己的方法
public void sillyMethod()
{
sillyMethod();
}
One to handle this is to fix your code so that the recursion terminates instead of continuing forever.
处理此问题的方法是修复您的代码,以便递归终止而不是永远继续。
回答by Tobias Langner
I guess you can't - or it at least depends on the jvm you use. Stack overflow means, that you have no room to store local variables and return adresses. If your jvm does some form of compiling, you have the stackoverflow in the jvm as well and that means, you can't handle it or catch it. The jvm has to terminate.
我想你不能 - 或者至少取决于你使用的 jvm。堆栈溢出意味着,您没有空间来存储局部变量和返回地址。如果您的 jvm 进行某种形式的编译,那么 jvm 中也会有 stackoverflow,这意味着您无法处理或捕获它。jvm 必须终止。
There could be a way to create a jvm that allows for such behavior, but it would be slow.
可能有一种方法可以创建一个允许这种行为的 jvm,但它会很慢。
I have not tested the behavior with the jvm, but in .net you just can't handle the stackoverflow. Even try catch won't help. Since java and .net rely on the same concept (virtual machines with jit) I suspect java would behave the same. The presence of a stackoverflow-exception in .NET suggests, there might be some vm that does enable the program to catch it, the normal does not though.
我还没有用 jvm 测试过这种行为,但是在 .net 中你就是无法处理 stackoverflow。即使尝试捕获也无济于事。由于 java 和 .net 依赖于相同的概念(带有 jit 的虚拟机),我怀疑 java 的行为会相同。.NET 中存在 stackoverflow-exception 表明,可能有一些 vm 确实使程序能够捕获它,但正常情况下不会。
回答by Neal Maloney
The stack trace should indicate the nature of the problem. There should be some obvious looping when you read the stack trace.
堆栈跟踪应表明问题的性质。当您阅读堆栈跟踪时,应该有一些明显的循环。
If it's not a bug, you need add a counter or some other mechanism to halt the recursion before the recursion goes so deep it causes a stack overflow.
如果它不是错误,您需要添加一个计数器或其他一些机制来在递归深入到导致堆栈溢出之前停止递归。
An example of this might be if you're handling nested XML in a DOM model with recursive calls and the XML is nested so deep it causes a stack overflow with your nested calls (unlikely, but possible). This would have to be pretty deep nesting to cause a stack overflow though.
这方面的一个例子可能是,如果您正在使用递归调用处理 DOM 模型中的嵌套 XML,并且 XML 嵌套如此之深,以至于您的嵌套调用会导致堆栈溢出(不太可能,但可能)。但是,这必须非常深的嵌套才能导致堆栈溢出。
回答by VHF
As mentioned by many in this thread, the common cause for this is a recursive method call that doesn't terminate. Where possible avoid the stack overflow and if you this in testing you should consider this in most cases to be a serious bug. In some cases you can configure the thread stack size in Java to be larger to handle some circumstances ( large data sets being managed in local stack storage, long recursive calls) but this will increase the overall memory footprint which can lead to issues in the number of threads available in the VM. Generally if you get this exception the thread and any local data to this thread should be considered toast and not used( ie suspect and possibly corrupt).
正如该线程中的许多人所提到的,造成这种情况的常见原因是不终止的递归方法调用。在可能的情况下避免堆栈溢出,如果您在测试中这样做,在大多数情况下您应该认为这是一个严重的错误。在某些情况下,您可以将 Java 中的线程堆栈大小配置为更大以处理某些情况(在本地堆栈存储中管理大型数据集,长递归调用),但这会增加整体内存占用,从而导致数量问题VM 中可用的线程数。通常,如果您遇到此异常,则应将线程和该线程的任何本地数据视为 toast 而非使用(即可疑且可能已损坏)。
回答by Michael Myers
Take a look at Raymond Chen's post When debugging a stack overflow, you want to focus on the repeating recursive part. An extract:
看一下Raymond Chen的帖子当调试堆栈溢出时,您要专注于重复递归部分。摘录:
If you go hunting through your defect tracking database trying to see whether this is a known issue or not, a search for the top functions on the stack is unlikely to find anything interesting. That's because stack overflows tend to happen at a random point in the recursion; each stack overflow looks superficially different from every other one even if they are the same stack overflow.
Suppose you're singing the song Frère Jacques, except that you sing each verse a few tones higher than the previous one. Eventually, you will reach the top of your singing range, and precisely where that happens depends on where your vocal limit lines up against the melody. In the melody, the first three notes are each a new "record high" (i.e., the notes are higher than any other note sung so far), and new record highs appear in the three notes of the third measure, and a final record high in the second note of the fifth measure.
If the melody represented a program's stack usage, a stack overflow could possibly occur at any of those five locations in the program's execution. In other words, the same underlying runaway recursion (musically represented by an ever-higher rendition of the melody) can manifest itself in five different ways. The "recursion" in this analogy was rather quick, just eight bars before the loop repeated. In real life, the loop can be quite long, leading to dozens of potential points where the stack overflow can manifest itself.
If you are faced with a stack overflow, then, you want to ignore the top of the stack, since that's just focusing on the specific note that exceeded your vocal range. You really want to find the entire melody, since that's what's common to all the stack overflows with the same root cause.
如果您在缺陷跟踪数据库中寻找这是否是一个已知问题,那么搜索堆栈中的顶级函数不太可能找到任何有趣的东西。那是因为堆栈溢出往往发生在递归的随机点;每个堆栈溢出看起来都与其他每个堆栈溢出表面上不同,即使它们是相同的堆栈溢出。
假设您正在唱Frère Jacques歌曲,但您每节都比前一节高出几个音调。最终,您将达到歌唱范围的顶峰,而发生这种情况的确切位置取决于您的声乐极限与旋律的对齐位置。在旋律中,前三个音符每个都是一个新的“最高纪录”(即该音符比迄今为止所唱的任何其他音符都要高),并且在第三小节的三个音符中出现新的最高纪录,以及一个最终的纪录在第五小节的第二个音符中高。
如果旋律代表程序的堆栈使用情况,则在程序执行的这五个位置中的任何一个位置都可能发生堆栈溢出。换句话说,相同的潜在失控递归(以更高的旋律演绎在音乐上代表)可以以五种不同的方式表现出来。这个类比中的“递归”相当快,在循环重复之前只有 8 个小节。在现实生活中,循环可能会很长,从而导致堆栈溢出可能出现的数十个潜在点。
如果您面临堆栈溢出,那么您想忽略堆栈的顶部,因为它只关注超出您音域的特定音符。您真的很想找到整个旋律,因为这是所有具有相同根本原因的堆栈溢出的共同点。
回答by TMN
You might want to see if the "-Xss" option is supported by your JVM. If so, you might want to try setting it to a value of 512k (default is 256k under 32-bit Windows and Unix) and see if that does anything (other than make you sit longer until your StackOverflowException). Note that this is a per-thread setting, so if you've got a lot of threads running you also might want to bump up your heap settings.
您可能想查看您的 JVM 是否支持“-Xss”选项。如果是这样,您可能想尝试将其设置为 512k(在 32 位 Windows 和 Unix 下默认为 256k)并查看是否有任何作用(除了让您在 StackOverflowException 之前坐得更久)。请注意,这是一个每线程设置,因此如果您有很多线程正在运行,您可能还想提高堆设置。
回答by Avrom
Simple,
简单的,
Look at the stack trace that the StackOverflowError produces so you know where in your code it occurs and use it to figure out how to rewrite your code so that it doesn't call itself recursively (the likely cause of your error) so it won't happen again.
查看 StackOverflowError 产生的堆栈跟踪,这样您就知道它发生在代码中的哪个位置,并使用它来找出如何重写您的代码,以便它不会递归调用自身(可能是您的错误的原因),因此它不会“不会再发生了。
StackOverflowErrors are not something that needs to be handled via a try...catch clause but it points to a basic flaw in the logic of your code that needs to be fixed by you.
StackOverflowErrors 不是需要通过 try...catch 子句处理的东西,但它指出了代码逻辑中需要由您修复的基本缺陷。
回答by jpangamarca
java.lang.Error javadoc:
java.lang.Error javadoc:
An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions. The ThreadDeath error, though a "normal" condition, is also a subclass of Error because most applications should not try to catch it. A method is not required to declare in its throws clause any subclasses of Error that might be thrown during the execution of the method but not caught, since these errors are abnormal conditions that should never occur.
Error 是 Throwable 的一个子类,它指示合理的应用程序不应尝试捕获的严重问题。大多数此类错误是异常情况。ThreadDeath 错误虽然是“正常”情况,但也是 Error 的一个子类,因为大多数应用程序不应该尝试捕获它。一个方法不需要在它的 throws 子句中声明在方法执行期间可能抛出但没有被捕获的 Error 的任何子类,因为这些错误是不应该发生的异常情况。
So, don't. Try to find what's wrong in the logic of your code. This exception ocurrs very often because of infinite recursion.
所以,不要。尝试找出代码逻辑中的问题。由于无限递归,这种异常经常发生。
回答by Derek Elkins
The correct answer is the one already given. You likely either a) have a bug in your code leading to an infinite recursion which is usually quite easy to diagnose and fix, or b) have code which can lead to very deep recursions for example recursively traversing an unbalanced binary tree. In the latter situation, you need to alter your code to not allocate the information on the stack (i.e. to not recurse) but to instead allocate it in the heap.
正确答案是已经给出的答案。您可能 a) 代码中有错误导致无限递归,这通常很容易诊断和修复,或者 b) 代码可能导致非常深的递归,例如递归遍历不平衡二叉树。在后一种情况下,您需要更改代码以不在堆栈上分配信息(即不递归),而是在堆中分配信息。
For example, for an unbalanced tree traversal, you could store the nodes that will need to be revisited in a Stack data structure. For an in order traversal you would loop down the left branches pushing each node as you visited it until you hit a leaf, which you would process, then pop a node off the top of the stack, process it, then restart your loop with the right child (by just setting your loop variable to the right node.) This will use a constant amount of stack by moving everything that was on the stack to the heap in the Stack data structure. Heap is typically much more plentiful than stack.
例如,对于不平衡的树遍历,您可以将需要重新访问的节点存储在 Stack 数据结构中。对于有序遍历,您将在访问它时推动每个节点的左分支向下循环,直到遇到一个叶子,您将对其进行处理,然后从堆栈顶部弹出一个节点,处理它,然后使用右孩子(只需将循环变量设置为正确的节点。)这将通过将堆栈上的所有内容移动到堆栈数据结构中的堆来使用恒定数量的堆栈。堆通常比堆栈丰富得多。
As something that is usually an extremely bad idea, but is necessary in cases where memory use is extremely constrained, you can use pointer reversal. In this technique, you encode the stack into the structure you are traversing, and by reusing the links you are traversing, you can do this with no or significantly less additional memory. Using the above example, instead of pushing nodes when we loop, we just need to remember our immediate parent, and at each iteration, we set the link we traversed to the current parent and then the current parent to the node we are leaving. When we get to a leaf, we process it, then go to our parent and then we have a conundrum. We don't know whether to correct the left branch, process this node, and continue with the right branch, or to correct the right branch and go to our parent. So we need to allocate an extra bit of information as we iterate. Typically, for low-level realizations of this technique, that bit will be stored in the pointer itself leading to no additional memory and constant memory overall. This is not an option in Java, but it may be possible to squirrel away this bit in fields used for other things. In the worst-case, this is still at least 32 or 64 times reduction in the amount of memory needed. Of course, this algorithm is extremely easy to get wrong with completely confusing results and would raise utter havoc with concurrency. So it's almost never worth the maintenance nightmare except where allocating memory is untenable. The typical example being a garbage collector where algorithms like this are common.
这通常是一个非常糟糕的主意,但在内存使用受到极大限制的情况下是必需的,您可以使用指针反转。在这种技术中,您将堆栈编码到您正在遍历的结构中,并且通过重用您正在遍历的链接,您可以在没有或显着减少额外内存的情况下执行此操作。使用上面的例子,当我们循环时,不需要推送节点,我们只需要记住我们的直接父节点,并且在每次迭代时,我们将遍历的链接设置为当前父节点,然后将当前父节点设置为我们要离开的节点。当我们到达一片叶子时,我们对其进行处理,然后去找我们的父母,然后我们就有了一个难题。我们不知道是纠正左分支,处理这个节点,然后继续右分支,还是纠正右分支并转到我们的父节点。所以我们需要在迭代时分配一些额外的信息。通常,对于此技术的低级实现,该位将存储在指针本身中,从而导致总体上没有额外的内存和常量内存。这在 Java 中不是一个选项,但可以在用于其他事物的字段中删除这一位。在最坏的情况下,这仍然至少减少了所需内存量的 32 或 64 倍。当然,这个算法非常容易出错,结果完全混乱,并发性会造成彻底的破坏。因此,除非分配内存站不住脚,否则几乎永远不值得维护噩梦。典型的例子是垃圾收集器,这种算法很常见。对于这种技术的低级实现,该位将存储在指针本身中,从而导致总体上没有额外的内存和常量内存。这在 Java 中不是一个选项,但可以在用于其他事物的字段中删除这一位。在最坏的情况下,这仍然至少减少了所需内存量的 32 或 64 倍。当然,这个算法非常容易出错,结果完全混乱,并发性会造成彻底的破坏。因此,除非分配内存站不住脚,否则几乎永远不值得维护噩梦。典型的例子是垃圾收集器,其中像这样的算法很常见。对于这种技术的低级实现,该位将存储在指针本身中,从而导致总体上没有额外的内存和常量内存。这在 Java 中不是一个选项,但可以在用于其他事物的字段中删除这一位。在最坏的情况下,这仍然至少减少了所需内存量的 32 或 64 倍。当然,这个算法非常容易出错,结果完全混乱,并发性会造成彻底的破坏。因此,除非分配内存站不住脚,否则几乎永远不值得维护噩梦。典型的例子是垃圾收集器,这种算法很常见。但是在用于其他事物的字段中可能会松散掉这一点。在最坏的情况下,这仍然至少减少了所需内存量的 32 或 64 倍。当然,这个算法非常容易出错,结果完全混乱,并发性会造成彻底的破坏。因此,除非分配内存站不住脚,否则几乎永远不值得维护噩梦。典型的例子是垃圾收集器,这种算法很常见。但是在用于其他事物的字段中可能会松散掉这一点。在最坏的情况下,这仍然至少减少了所需内存量的 32 或 64 倍。当然,这个算法非常容易出错,结果完全混乱,并发性会造成彻底的破坏。因此,除非分配内存站不住脚,否则几乎永远不值得维护噩梦。典型的例子是垃圾收集器,这种算法很常见。除非分配内存站不住脚,否则几乎从不值得维护噩梦。典型的例子是垃圾收集器,这种算法很常见。除非分配内存站不住脚,否则几乎从不值得维护噩梦。典型的例子是垃圾收集器,这种算法很常见。
What I really wanted to talk about, though, is when you might want to handle the StackOverflowError. Namely to provide tail call elimination on the JVM. One approach is to use trampoline style where instead of performing a tail call you return a nullary procedure object, or if you are just returning a value you return that. [Note: this requires some means of saying a function returns either A or B. In Java, probably the lightest way to do this is to return one type normally and throw the other as an exception.] Then whenever you call a method, you need to do a while loop calling the nullary procedures (which will themselves return either a nullary procedure or a value) until you get a value. An endless loop will become a while loop that is constantly forcing procedure objects that return procedure objects. The benefits of trampoline style is that it only uses a constant factor more stack than you would use with an implementation that properly eliminated all tail calls, it uses the normal Java stack for non-tail calls, the translation simple, and it only grows the code by a (tedious) constant factor. The drawback is you allocate an object on every method call (which will immediately become garbage) and consuming these objects involves a couple of indirect calls per tail call.
不过,我真正想谈论的是您何时可能想要处理 StackOverflowError。即在 JVM 上提供尾调用消除。一种方法是使用蹦床样式,而不是执行尾调用,而是返回一个空过程对象,或者如果您只是返回一个值,则返回该值。[注意:这需要某种方式来表示函数返回 A 或 B。在 Java 中,最简单的方法可能是正常返回一种类型并将另一种作为异常抛出。] 然后,无论何时调用一个方法,你需要做一个 while 循环调用 nullary 过程(它本身将返回一个 nullary 过程或一个值),直到你得到一个值。一个无限循环将变成一个不断强制返回过程对象的过程对象的 while 循环。蹦床风格的好处是它只使用一个常数因子比正确消除所有尾调用的实现更多的堆栈,它使用普通的 Java 堆栈进行非尾调用,翻译简单,它只会增加由(乏味的)常数因子编码。缺点是您在每个方法调用上分配一个对象(这将立即成为垃圾)并且使用这些对象涉及每个尾调用的几个间接调用。
The ideal thing to do would be to never allocate those nullary procedures or anything else in the first place, which is exactly what tail call elimination would accomplish. Working with what Java provides though, what we could do is run the code as normal and only make these nullary procedures when we run out of stack. Now we still allocate those useless frames, but we do so on the stack rather than the heap and deallocate them in bulk, also, our calls are normal direct Java calls. The easiest way to describe this transformation is to first rewrite all multi-call-statement methods into methods that have two call statements, i.e. fgh() { f(); g(); h(); } becomes fgh() { f(); gh(); } and gh(){ g(); h(); }. For simplicity, I'll assume all methods end in a tail call, which can be arranged by just packaging the remainder of a method into a separate method, though in practice, you'd want to handle these directly. After these transformations we have three cases, either a method has zero calls in which case there is nothing to do, or it has one (tail) call, in which case we wrap it in a try-catch block in the same we will for the tail call in the two call case. Finally, it may have two calls, a non-tail call and a tail call, in which case we apply the following transformation illustrated by example (using C#'s lambda notation which could easily be replaced with an anonymous inner class with some growth):
理想的做法是从一开始就永远不要分配那些空过程或其他任何东西,这正是尾调用消除将要完成的。尽管使用 Java 提供的功能,我们可以做的是正常运行代码,并且仅在堆栈用完时才执行这些无效过程。现在我们仍然分配那些无用的帧,但我们在堆栈而不是堆上分配并批量释放它们,而且我们的调用是正常的直接 Java 调用。描述这种转换的最简单方法是首先将所有多调用语句的方法重写为具有两个调用语句的方法,即 fgh() { f(); G(); H(); } 变成 fgh() { f(); gh(); } 和 gh(){ g(); H(); }. 为简单起见,我假设所有方法都以尾调用结束,这可以通过将方法的其余部分打包到一个单独的方法中来安排,但在实践中,您希望直接处理这些。在这些转换之后,我们有三种情况,一个方法有零次调用,在这种情况下没有什么可做的,或者它有一个(尾)调用,在这种情况下,我们将它包装在一个 try-catch 块中,我们将这样做两个调用案例中的尾调用。最后,它可能有两个调用,一个非尾调用和一个尾调用,在这种情况下,我们应用以下示例所示的转换(使用 C# 的 lambda 表示法,它可以很容易地被一个匿名内部类替换,并有一些增长):或者它有一个(尾)调用,在这种情况下,我们将它包装在一个 try-catch 块中,我们将在两个调用的情况下进行尾调用。最后,它可能有两个调用,一个非尾调用和一个尾调用,在这种情况下,我们应用以下示例所示的转换(使用 C# 的 lambda 表示法,它可以很容易地被一个匿名内部类替换,并有一些增长):或者它有一个(尾)调用,在这种情况下,我们将它包装在一个 try-catch 块中,我们将在两个调用的情况下进行尾调用。最后,它可能有两个调用,一个非尾调用和一个尾调用,在这种情况下,我们应用以下示例所示的转换(使用 C# 的 lambda 表示法,它可以很容易地被一个匿名内部类替换,并有一些增长):
// top-level handler
Action tlh(Action act) {
return () => {
while(true) {
try { act(); break; } catch(Bounce e) { tlh(() => e.run())(); }
}
}
}
gh() {
try { g(); } catch(Bounce e) {
throw new Bounce(tlh(() => {
e.run();
try { h(); } catch(StackOverflowError e) {
throw new Bounce(tlh(() => h());
}
});
}
try { h(); } catch(StackOverflowError e) {
throw new Bounce(tlh(() => h()));
}
}
The main benefit here is if no exception is thrown, this is the same code as we started with just with some extra exception handlers installed. Since tail calls (the h() call) don't handle the Bounce exception, that exception will fly through them unwinding those (unnecessary) frames from the stack. The non-tail calls catch the Bounce exceptions and rethrow them with the remaining code added. This will unwind the stack all the way up to the top level, eliminating the tail call frames but remembering the non-tail call frames in the nullary procedure. When we finally execute the procedure in the Bounce exception at the top-level, we will recreate all the non-tail call frames. At this point, if we immediately run out of stack again, then, since we don't reinstall the StackOverflowError handlers, it will go uncaught as desired, since we really are out of stack. If we get a little further, a new StackOverflowError will be installed as appropriate. Furthermore, if we do make progress, but then do run out of stack again, there is no benefit re-unwinding the frames we already unwound, so we install new top-level handlers so that the stack will only be unwound up to them.
这里的主要好处是如果没有抛出异常,这与我们开始时安装的代码相同,只是安装了一些额外的异常处理程序。由于尾调用(h() 调用)不处理 Bounce 异常,该异常将通过它们从堆栈中展开那些(不必要的)帧。非尾调用捕获 Bounce 异常并在添加剩余代码的情况下重新抛出它们。这将一直展开堆栈直到顶层,消除尾调用帧但记住空过程中的非尾调用帧。当我们最终在顶层执行 Bounce 异常中的过程时,我们将重新创建所有非尾调用帧。在这一点上,如果我们立即再次用完堆栈,那么,由于我们没有重新安装 StackOverflowError 处理程序,它将根据需要未被捕获,因为我们真的出栈了。如果我们再进一步,将适当安装一个新的 StackOverflowError。此外,如果我们确实取得了进展,但随后再次耗尽堆栈,重新展开我们已经展开的帧没有任何好处,因此我们安装新的顶级处理程序,以便堆栈只会展开到它们。
The biggest problem with this approach is that you'll probably want to call normal Java methods and you may have arbitrarily little stack space when you do, so they may have enough space to start but not finish and you can't resume them in the middle. There are at least two solutions to this. The first is to ship all such work to a separate thread which will have it's own stack. This is pretty effective and pretty easy and won't introduce any concurrency (unless you want it to.) Another option is simply to purposely unwind the stack before calling any normal Java method by simply throwing a StackOverflowError immediately before them. If it still runs out of stack space when you resume, then you were screwed to begin with.
这种方法的最大问题是您可能想要调用普通的 Java 方法,并且在调用时可能有任意小的堆栈空间,因此它们可能有足够的空间开始但未完成,并且您无法在中间。对此,至少有两种解决方案。第一种是将所有此类工作发送到一个单独的线程,该线程将拥有自己的堆栈。这非常有效且非常简单,并且不会引入任何并发性(除非您想要它)。另一种选择是在调用任何普通 Java 方法之前通过在它们之前直接抛出 StackOverflowError 来故意展开堆栈。如果在您恢复时它仍然用完堆栈空间,那么您一开始就被搞砸了。
A similar thing can be done to make continuations just-in-time too. Unfortunately, this transformation isn't really bearable to do by hand in Java, and is probably borderline for languages like C# or Scala. So, transformations like this tend to be done by languages that target the JVM and not by people.
也可以做类似的事情来及时进行延续。不幸的是,这种转换在 Java 中手动完成并不真正可以忍受,并且对于像 C# 或 Scala 这样的语言来说可能是边缘。因此,像这样的转换往往是由面向 JVM 的语言完成的,而不是由人来完成的。