优化 Java 代码的技巧

Posted

技术标签:

【中文标题】优化 Java 代码的技巧【英文标题】:Tips optimizing Java code 【发布时间】:2011-11-03 17:07:38 【问题描述】:

所以,我用 Java 编写了一个拼写检查器,一切正常。唯一的问题是,如果我使用的最大允许编辑距离太大(比如 9),那么我的代码就会内存不足。我已经分析了我的代码并将堆转储到一个文件中,但我不知道如何使用它来优化我的代码。

谁能提供帮助?我非常愿意提供文件/使用人们可能拥有的任何其他方法。

-编辑-

许多人在 cmets 中询问了更多详细信息。我认为其他人会发现它们很有用,它们可能会被埋在 cmets 中。他们在这里:

我正在使用 Trie 来存储单词本身。

为了提高时间效率,我不预先计算 Levenshtein 距离,而是边走边计算。我的意思是我在内存中只保留了两行 LD 表。由于 Trie 是前缀树,这意味着每次我向下递归一个节点时,单词的前一个字母(因此这些单词的距离)保持不变。因此,我只计算包含那个新字母的距离,前一行保持不变。

我生成的建议存储在 HashMap 中。 LD 表的行存储在 ArrayLists 中。

这是 Trie 中导致问题的函数的代码。构建 Trie 非常简单,这里我没有包含相同的代码。

/*
 * @param letter: the letter that is currently being looked at in the trie
 *        word: the word that we are trying to find matches for
 *        previousRow: the previous row of the Levenshtein Distance table
 *        suggestions: all the suggestions for the given word
 *        maxd: max distance a word can be from th query and still be returned as suggestion
 *        suggestion: the current suggestion being constructed
 */


public void get(char letter, ArrayList<Character> word, ArrayList<Integer> previousRow, HashSet<String> suggestions, int maxd, String suggestion)

// the new row of the trie that is to be computed.
ArrayList<Integer> currentRow = new ArrayList<Integer>(word.size()+1);
currentRow.add(previousRow.get(0)+1);

int insert = 0;
int delete = 0;
int swap = 0;
int d = 0;

for(int i=1;i<word.size()+1;i++)
    delete = currentRow.get(i-1)+1;
    insert = previousRow.get(i)+1;

    if(word.get(i-1)==letter)
    swap = previousRow.get(i-1);
    else
    swap = previousRow.get(i-1)+1;

    d = Math.min(delete, Math.min(insert, swap));
    currentRow.add(d);


// if this node represents a word and the distance so far is <= maxd, then add this word as a suggestion
if(isWord==true && d<=maxd)
    suggestions.add(suggestion);
    

// if any of the entries in the current row are <=maxd, it means we can still find possible solutions. 
// recursively search all the branches of the trie
for(int i=0;i<currentRow.size();i++)
    if(currentRow.get(i)<=maxd)
    for(int j=0;j<26;j++)
        if(children[j]!=null)
        children[j].get((char)(j+97), word, currentRow, suggestions, maxd, suggestion+String.valueOf((char)(j+97))); 
        
    
    break;
       


【问题讨论】:

首先要告诉我们的是你用来实现拼写检查器的算法和数据结构,代码可能很好。 由于内存不足,您需要找出哪些数据结构使用的内存最多,并对其进行优化以减少使用量。为了更具体,我们需要更多细节。 @efficiencyIsBliss:拼写检查器通常应使用多种方法生成“候选人”列表,包括使用“双变位”方法或类似方法(查找听起来像您的单词的有效单词),生成许多可能的变体(插入任何一个字母,删除每个字母,反转字母等,并保留你得到的有效单词)。然后你把所有的候选词都计算出来并计算编辑距离(使用极其快速的 DP 算法来计算 LED)并首先建议具有最低编辑距离的单词。您如何生成高达 9 的编辑距离? @efficiencyIsBliss:我的观点是:你是产生候选人的人,因此你应该能够躲避任何组合爆炸。用于计算 Levenhstein 编辑距离的动态编程算法非常快,并且可以实现,因此它不会创建单个对象。这是一篇关于您可能喜欢的主题的古老但很酷的文章(包括动态编程 LED):ibm.com/developerworks/java/library/j-jazzy @efficiencyIsBliss:看到你发布的代码我认为你以某种方式颠倒了你的逻辑。我将发布一些(工作)代码,概述一个简单的拼写检查器是如何工作的。 【参考方案1】:

这是我快速编写的一些代码,展示了一种生成候选对象并对其进行“排名”的方法。

诀窍是:你永远不会“测试”一个无效的候选人。

对我来说:“当我的编辑距离为 9 时,我的内存不足” 尖叫着“组合爆炸”。

当然,要避免组合爆炸,您不会尝试从拼写错误的作品中生成所有与“9”相距甚远的单词。您从拼写错误的单词开始并生成(相当多)可能的候选者,但不要创建太多候选者,否则会遇到麻烦。

(另请注意,计算最大为 9 的 Levenhstein 编辑距离没有多大意义,因为从技术上讲,任何少于 10 个字母的单词都可以转换为最多 9 个少于 10 个字母的任何其他单词转换)

这就是为什么您只是无法测试所有距离为 9 以内的单词而不会出现 OutOfMemory 错误或只是程序永远不会终止:

为单词 “ptmizing” 生成所有 LED,最多为 1 个,只需添加一个字母(从 a 到 z)即可生成 9*26 个变体(即 324 个变体)[有 9 个可以插入 26 个字母中的一个的位置) 生成所有 LED 最多 2 个,只需在我们知道的内容中添加一个字母即可生成 10*26*324 种变化 (60840) 生成最多 3 个 LED 会产生:17 400 240 种变化

考虑我们加一、加二或加三字母的情况(我们不计算删除、交换等)。那是在一个只有九个字符长的拼写错误的单词上。在“真实”的词中,它的爆发速度更快。

当然,您可以变得“聪明”并以一种不会有太多欺骗等的方式生成它,但重点是:这是一个快速爆炸的组合爆炸。

无论如何...这里有一个例子。我只是将有效单词的字典(在这种情况下仅包含四个单词)传递给相应的方法以保持简短。

您显然希望用您自己的 LED 实现替换对 LED 的调用。

双变位音只是一个例子:在真正的拼写检查器中,“听起来很像”的词 尽管进一步的 LED 应被视为“更正确”,因此通常首先建议。例如,“优化”和“自动优化”与 LED 的观点相去甚远,但使用双变音位时,您应该将“优化”作为第一个建议。

(免责声明:以下内容是在几分钟内完成的,它没有考虑大写、非英语单词等:它不是真正的拼写检查器,只是一个示例)

   @Test
    public void spellCheck() 
        final String src = "misspeled";
        final Set<String> validWords = new HashSet<String>();
        validWords.add("boing");
        validWords.add("Yahoo!");
        validWords.add("misspelled");
        validWords.add("***");
        final List<String> candidates = findNonSortedCandidates( src, validWords );
        final SortedMap<Integer,String> res = computeLevenhsteinEditDistanceForEveryCandidate(candidates, src);
        for ( final Map.Entry<Integer,String> entry : res.entrySet() ) 
            System.out.println( entry.getValue() + " @ LED: " + entry.getKey() );
        
    

    private SortedMap<Integer, String> computeLevenhsteinEditDistanceForEveryCandidate(
            final List<String> candidates,
            final String mispelledWord
    ) 
        final SortedMap<Integer, String> res = new TreeMap<Integer, String>();
        for ( final String candidate : candidates ) 
            res.put( dynamicProgrammingLED(candidate, mispelledWord), candidate );
        
        return res;
    

    private int dynamicProgrammingLED( final String candidate, final String misspelledWord ) 
        return Levenhstein.getLevenshteinDistance(candidate,misspelledWord);
    

在这里,您可以使用多种方法生成所有可能的候选者。我只实现了一种这样的方法(而且很快,所以它可能是假的,但这不是重点;)

    private List<String> findNonSortedCandidates( final String src, final Set<String> validWords ) 
        final List<String> res = new ArrayList<String>();
        res.addAll( allCombinationAddingOneLetter(src, validWords) );
//        res.addAll( allCombinationRemovingOneLetter(src) );
//        res.addAll( allCombinationInvertingLetters(src) );
        return res;
    

    private List<String> allCombinationAddingOneLetter( final String src, final Set<String> validWords ) 
        final List<String> res = new ArrayList<String>();
        for (char c = 'a'; c < 'z'; c++) 
            for (int i = 0; i < src.length(); i++) 
                final String candidate = src.substring(0, i) + c + src.substring(i, src.length());
                if ( validWords.contains(candidate) ) 
                    res.add(candidate); // only adding candidates we know are valid words
                
            
            if ( validWords.contains(src+c) ) 
                res.add( src + c );
            
        
        return res;
    

【讨论】:

+1 以获得如此快速的响应。我自己编写了这样的代码,我同意它的优雅并且比我的方法需要更少的内存。但是,如果要查找距离 2 内的所有建议,则必须对距离 src 距离为 1 的所有元素运行 allCombinationAddingOneLetter。这样,如果我想允许最多 9 的距离,我将不得不一遍又一遍地反复调用allCombinationAddingOneLetter,事情可能会变得非常缓慢。你怎么看? @efficiencyIsBliss:您可以编写更简洁的方法:我只是快速编写了一些代码……更棘手的是:我只生成有效的单词。要查找距离为 2 的元素,您必须编写另一种方法,该方法也仅查找有效单词。否则,您会从 "mispeled" 开始,并且您将 not 返回“mispelled”,因为您不会从第一次打电话。有一些方法可以编写相对干净的生成所有组合的代码,但是......你不能达到 9,组合爆炸太大了。我将编辑更多以举例说明。 您能否在上面的代码中包含一些关于如何减少内存占用的提示?感谢所有的帮助和讨论! @efficiencyIsBliss:这完全取决于你的字典有多大。哎呀,现在为您的字典提供 300 000 个 String 对象并不完全是个问题。此外,您只会生成有效单词,因此上面的代码不应该有任何内存问题,因为您只会创建相对较小的列表/集合/映射。唯一真正使用的内存应该是字典本身使用的内存。 (我进行了编辑以说明为什么会出现组合爆炸). 不,我的意思是在我发布的代码中。我可能最终会使用您发布的内容,但我仍然想知道如何改进自己的代码。【参考方案2】:

您可以尝试的一件事是,增加 Java 的堆大小,以克服“内存不足错误”。

以下文章将帮助您了解如何在 Java 中增加堆大小

http://viralpatel.net/blogs/2009/01/jvm-java-increase-heap-size-setting-heap-size-jvm-heap.html

但我认为解决您的问题的更好方法是,找出比当前算法更好的算法

【讨论】:

我知道如何增加 Java 堆大小,但我不认为我想这样做。我想让它更高效,以便它可以在默认堆空间中运行。【参考方案3】:

如果没有关于该主题的更多信息,社区无法为您做很多事情......您可以从以下内容开始:

    查看 Profiler 的内容(运行一段时间后):有什么东西堆积起来了吗?是否有很多对象 - 这通常会提示您代码有什么问题。

    在某处发布您保存的转储并将其链接到您的问题中,以便其他人可以查看它。

    告诉我们您正在使用哪个分析器,然后有人可以提示您在哪里寻找有价值的信息。

    在您将问题缩小到代码的特定部分后,您无法弄清楚为什么您的内存中有这么多 $FOO 的对象,请发布相关部分的 sn-p。

【讨论】:

以上是关于优化 Java 代码的技巧的主要内容,如果未能解决你的问题,请参考以下文章

编写高效的Java代码:常用的优化技巧之并发编程技巧

优化 Java 代码的技巧

代码优化:Java编码技巧之高效代码50例

JAVA中如何实现代码优化(技巧讲解)

Java 性能优化技巧集锦

Java性能优化技巧集锦