在Java中比较版本字符串的高效方法

64

可能有重复:
如何在Java中比较两个版本字符串?


我有两个包含版本信息的字符串,如下所示:

str1 = "1.2"
str2 = "1.1.2"

现在,有人能告诉我在Java中比较这些字符串版本的有效方法,并且如果它们相等,则返回0;如果str1<str2,则返回-1;如果str1>str2,则返回1吗?


2
1.2.0 和 1.2 相比有什么区别? - Thilo
此外,1.12应该如何与1.2进行比较(即是指1.12相对于1.2,还是仅比较.1子字符串和.2子字符串)? - GreenMatt
5
请查看 https://dev59.com/YnVC5IYBdhLWcg3wvT1a。 - g051051
我可以直接使用字符串的compareTo方法吗?这也是一道面试题。那么什么是预期的高效答案呢? - Mike
1
1.2.0 = 1.2 & 1.12 > 1.2 - Mike
请检查我的解决方案https://dev59.com/YnVC5IYBdhLWcg3wvT1a#36987530 - Prateek Mishra
10个回答

151

需要 commons-lang3-3.8.1.jar 用于字符串操作。

/**
 * Compares two version strings. 
 * 
 * Use this instead of String.compareTo() for a non-lexicographical 
 * comparison that works for version strings. e.g. "1.10".compareTo("1.6").
 * 
 * @param v1 a string of alpha numerals separated by decimal points. 
 * @param v2 a string of alpha numerals separated by decimal points.
 * @return The result is 1 if v1 is greater than v2. 
 *         The result is 2 if v2 is greater than v1. 
 *         The result is -1 if the version format is unrecognized. 
 *         The result is zero if the strings are equal.
 */

public int VersionCompare(String v1,String v2)
{
    int v1Len=StringUtils.countMatches(v1,".");
    int v2Len=StringUtils.countMatches(v2,".");

    if(v1Len!=v2Len)
    {
        int count=Math.abs(v1Len-v2Len);
        if(v1Len>v2Len)
            for(int i=1;i<=count;i++)
                v2+=".0";
        else
            for(int i=1;i<=count;i++)
                v1+=".0";
    }

    if(v1.equals(v2))
        return 0;

    String[] v1Str=StringUtils.split(v1, ".");
    String[] v2Str=StringUtils.split(v2, ".");
    for(int i=0;i<v1Str.length;i++)
    {
        String str1="",str2="";
        for (char c : v1Str[i].toCharArray()) {
            if(Character.isLetter(c))
            {
                int u=c-'a'+1;
                if(u<10)
                    str1+=String.valueOf("0"+u);
                else
                    str1+=String.valueOf(u);
            }
            else
                str1+=String.valueOf(c);
        }            
        for (char c : v2Str[i].toCharArray()) {
            if(Character.isLetter(c))
            {
                int u=c-'a'+1;
                if(u<10)
                    str2+=String.valueOf("0"+u);
                else
                    str2+=String.valueOf(u);
            }
            else
                str2+=String.valueOf(c);
        }
        v1Str[i]="1"+str1;
        v2Str[i]="1"+str2;

            int num1=Integer.parseInt(v1Str[i]);
            int num2=Integer.parseInt(v2Str[i]);

            if(num1!=num2)
            {
                if(num1>num2)
                    return 1;
                else
                    return 2;
            }
    }
    return -1;
}    

6
当涉及到1.3a.1和1.3b :D以及1.3-SNAPSHOT时,它将变得更加有趣 ;D。 - Angel O'Sphere
我将其编辑为使用Integer.valueOf,因为我们的目标是性能,版本号往往较低,会命中缓存而不是创建新对象。 - Johnco
1
有人能解释一下最后一部分吗?为什么要比较vals1和vals2的长度?谢谢。 - whiteElephant
1
@whiteElephant 如果你把最后一个return放在一个else子句中,可能更容易理解。这个else是用于字符串相同或一个字符串是另一个字符串的子集的情况。例如,"1.2" = "1.2" 或 "1.2" < "1.2.3"。在这种情况下,您可以根据长字符串是更高版本的假设来比较长度。但是,如果您期望"1.2"等于"1.2.0",则会出现问题。否则,这是一个很好且简洁的解决方案。 - Lucas
3
请注意,如果版本字符串格式不正确,Integer.valueOf() 可能会抛出 NumberFormatException 异常。 - Incinerator
显示剩余14条评论

15

正如其他人所指出的那样,String.split()是实现你想要比较的非常简单的方法。Mike Deck指出,由于这些(可能很短的)字符串,它可能不会有太大影响,但无论如何!如果您想在不手动解析字符串的情况下进行比较,并具有提前退出的选项,则可以尝试使用java.util.Scanner类。

public static int versionCompare(String str1, String str2) {
    try ( Scanner s1 = new Scanner(str1);
          Scanner s2 = new Scanner(str2);) {
        s1.useDelimiter("\\.");
        s2.useDelimiter("\\.");

        while (s1.hasNextInt() && s2.hasNextInt()) {
            int v1 = s1.nextInt();
            int v2 = s2.nextInt();
            if (v1 < v2) {
                return -1;
            } else if (v1 > v2) {
                return 1;
            }
        }

        if (s1.hasNextInt() && s1.nextInt() != 0)
            return 1; //str1 has an additional lower-level version number
        if (s2.hasNextInt() && s2.nextInt() != 0)
            return -1; //str2 has an additional lower-level version 

        return 0;
    } // end of try-with-resources
}

这段代码无法正确比较 1.0.11。返回值是 0,但应该返回 1 - Saurav Sahu

10

这几乎肯定不是最有效的方法,但考虑到版本号字符串通常只有几个字符长,我认为没有必要进行进一步的优化:

public static int compareVersions(String v1, String v2) {
    String[] components1 = v1.split("\\.");
    String[] components2 = v2.split("\\.");
    int length = Math.min(components1.length, components2.length);
    for(int i = 0; i < length; i++) {
        int result = new Integer(components1[i]).compareTo(Integer.parseInt(components2[i]));
        if(result != 0) {
            return result;
        }
    }
    return Integer.compare(components1.length, components2.length);
}

当一个字符串是另一个字符串的左子串时会失败。 - Ed Staub
每次都返回0。v1.split和v1.split将始终相同 :) - jt-gilkeson
3
修复了Ed Staub和@RedLenses指出的错误。只用了2年时间才正确回答! - Mike Deck
2
点赞因为简洁,但它未能正确比较“1.0.0”和“1.0”。返回的值是“1”,而应该是“0”。 - Saurav Sahu
调用 Integer 上的 .compareTo() 现在已经被弃用。该行可以更改为: int result = Integer.compare(Integer.parseInt(components1[i]), Integer.parseInt(components2[i])); - David

8
我本想自己完成这个任务,但是我看到了三种不同的方法来完成这个任务,目前几乎每个人都在分割版本字符串。我认为这样做并不高效,尽管从代码大小方面来看,它读起来很好,看起来也不错。
方法如下:
1. 假设版本字符串中部分(序数)的数量有一个上限,并且表示的值也有一个限制。通常最多只有4个点,每个序数的最大值为999。你可以看到这是什么意思,它正朝着将版本转换为适合于像“1.0”=>“001000000000”这样的字符串的方向发展,使用字符串格式或其他方式填充每个序数。然后进行字符串比较。
2. 在序数分隔符(“.”)上拆分字符串,并迭代它们,并比较解析出的版本。这是Alex Gitelman很好地演示的方法。
3. 在解析出问题的版本字符串时比较序数。如果所有字符串实际上只是指向字符数组的指针,就像在C语言中一样,那么这将是明显的方法(在找到'.'时将其替换为空终止符,并移动一些2或4个指针)。
对这三种方法的想法:
  1. 有一个博客帖子链接,展示了如何处理1的限制。版本字符串长度、部分数量和部分最大值都有限制。我认为有些字符串在某一点会突破10,000的限制。此外,大多数实现仍然会拆分字符串。
  2. 提前拆分字符串很容易阅读和思考,但我们需要两次处理每个字符串。我想比较一下它与下一种方法的时间。
  3. 在拆分字符串的同时进行比较,可以让你在比较"2.1001.100101.9999998"和"1.0.0.0.0.0.1.0.0.0.1"时能够非常早地停止拆分。如果这是C而不是Java,优势可能会继续限制为每个版本的每个部分分配新字符串的内存量,但这并不是这样。

我没有看到任何人给出这第三种方法的例子,所以我想把它作为一个回答添加在这里,以追求效率。

public class VersionHelper {

    /**
     * Compares one version string to another version string by dotted ordinals.
     * eg. "1.0" > "0.09" ; "0.9.5" < "0.10",
     * also "1.0" < "1.0.0" but "1.0" == "01.00"
     *
     * @param left  the left hand version string
     * @param right the right hand version string
     * @return 0 if equal, -1 if thisVersion &lt; comparedVersion and 1 otherwise.
     */
    public static int compare(@NotNull String left, @NotNull String right) {
        if (left.equals(right)) {
            return 0;
        }
        int leftStart = 0, rightStart = 0, result;
        do {
            int leftEnd = left.indexOf('.', leftStart);
            int rightEnd = right.indexOf('.', rightStart);
            Integer leftValue = Integer.parseInt(leftEnd < 0
                    ? left.substring(leftStart)
                    : left.substring(leftStart, leftEnd));
            Integer rightValue = Integer.parseInt(rightEnd < 0
                    ? right.substring(rightStart)
                    : right.substring(rightStart, rightEnd));
            result = leftValue.compareTo(rightValue);
            leftStart = leftEnd + 1;
            rightStart = rightEnd + 1;
        } while (result == 0 && leftStart > 0 && rightStart > 0);
        if (result == 0) {
            if (leftStart > rightStart) {
                return containsNonZeroValue(left, leftStart) ? 1 : 0;
            }
            if (leftStart < rightStart) {
                return containsNonZeroValue(right, rightStart) ? -1 : 0;
            }
        }
        return result;
    }

    private static boolean containsNonZeroValue(String str, int beginIndex) {
        for (int i = beginIndex; i < str.length(); i++) {
            char c = str.charAt(i);
            if (c != '0' && c != '.') {
                return true;
            }
        }
        return false;
    }
}

单元测试展示预期输出。

public class VersionHelperTest {

    @Test
    public void testCompare() throws Exception {
        assertEquals(1, VersionHelper.compare("1", "0.9"));
        assertEquals(1, VersionHelper.compare("0.0.0.2", "0.0.0.1"));
        assertEquals(1, VersionHelper.compare("1.0", "0.9"));
        assertEquals(1, VersionHelper.compare("2.0.1", "2.0.0"));
        assertEquals(1, VersionHelper.compare("2.0.1", "2.0"));
        assertEquals(1, VersionHelper.compare("2.0.1", "2"));
        assertEquals(1, VersionHelper.compare("0.9.1", "0.9.0"));
        assertEquals(1, VersionHelper.compare("0.9.2", "0.9.1"));
        assertEquals(1, VersionHelper.compare("0.9.11", "0.9.2"));
        assertEquals(1, VersionHelper.compare("0.9.12", "0.9.11"));
        assertEquals(1, VersionHelper.compare("0.10", "0.9"));
        assertEquals(0, VersionHelper.compare("0.10", "0.10"));
        assertEquals(-1, VersionHelper.compare("2.10", "2.10.1"));
        assertEquals(-1, VersionHelper.compare("0.0.0.2", "0.1"));
        assertEquals(1, VersionHelper.compare("1.0", "0.9.2"));
        assertEquals(1, VersionHelper.compare("1.10", "1.6"));
        assertEquals(0, VersionHelper.compare("1.10", "1.10.0.0.0.0"));
        assertEquals(1, VersionHelper.compare("1.10.0.0.0.1", "1.10"));
        assertEquals(0, VersionHelper.compare("1.10.0.0.0.0", "1.10"));
        assertEquals(1, VersionHelper.compare("1.10.0.0.0.1", "1.10"));
    }
}

0

将字符串按 "." 或其他分隔符拆分,然后解析每个令牌为整数值并进行比较。

int compareStringIntegerValue(String s1, String s2, String delimeter)  
{  
   String[] s1Tokens = s1.split(delimeter);  
   String[] s2Tokens = s2.split(delimeter);  

   int returnValue = 0;
   if(s1Tokens.length > s2Tokens.length)  
   {  
       for(int i = 0; i<s1Tokens.length; i++)  
       {  
          int s1Value = Integer.parseString(s1Tokens[i]);  
          int s2Value = Integer.parseString(s2Tokens[i]);  
          Integer s1Integer = new Integer(s1Value);  
          Integer s2Integer = new Integer(s2Value);  
          returnValue = s1Integer.compareTo(s2Value);
          if( 0 == isEqual)  
           {  
              continue; 
           }  
           return returnValue;  //end execution
        }
           return returnValue;  //values are equal
 } 

我会把另一个if语句留给大家做练习。


0

改编自Alex Gitelman的答案。

int compareVersions( String str1, String str2 ){

    if( str1.equals(str2) ) return 0; // Short circuit when you shoot for efficiency

    String[] vals1 = str1.split("\\.");
    String[] vals2 = str2.split("\\.");

    int i=0;

    // Most efficient way to skip past equal version subparts
    while( i<vals1.length && i<val2.length && vals[i].equals(vals[i]) ) i++;

    // If we didn't reach the end,

    if( i<vals1.length && i<val2.length )
        // have to use integer comparison to avoid the "10"<"1" problem
        return Integer.valueOf(vals1[i]).compareTo( Integer.valueOf(vals2[i]) );

    if( i<vals1.length ){ // end of str2, check if str1 is all 0's
        boolean allZeros = true;
        for( int j = i; allZeros & (j < vals1.length); j++ )
            allZeros &= ( Integer.parseInt( vals1[j] ) == 0 );
        return allZeros ? 0 : -1;
    }

    if( i<vals2.length ){ // end of str1, check if str2 is all 0's
        boolean allZeros = true;
        for( int j = i; allZeros & (j < vals2.length); j++ )
            allZeros &= ( Integer.parseInt( vals2[j] ) == 0 );
        return allZeros ? 0 : 1;
    }

    return 0; // Should never happen (identical strings.)
}

所以你可以看到,这并不是那么简单。而且当你允许前导0时,它会失败,但我从未见过版本“1.04.5”或其他类似的。你需要在while循环中使用整数比较来解决这个问题。当你在版本字符串中混合字母和数字时,这变得更加复杂。


0

将它们分割成数组,然后进行比较。

// check if two strings are equal. If they are return 0;
String[] a1;

String[] a2;

int i = 0;

while (true) {
    if (i == a1.length && i < a2.length) return -1;
    else if (i < a1.length && i == a2.length) return 1;

    if (a1[i].equals(a2[i]) {
       i++;
       continue;
    }
     return a1[i].compareTo(a2[i];
}
return 0;

0
我会将问题分为两部分,格式化和比较。如果您可以假设格式是正确的,那么仅比较数字版本就非常简单:
final int versionA = Integer.parseInt( "01.02.00".replaceAll( "\\.", "" ) );
final int versionB = Integer.parseInt( "01.12.00".replaceAll( "\\.", "" ) );

然后将两个版本作为整数进行比较。所以"大问题"就是格式,但它可以有许多规则。在我的情况下,我只需要完成两对数字的最小值,因此格式始终为"99.99.99",然后我执行上面的转换;所以在我的情况下,程序逻辑在于格式化,而不是版本比较。现在,如果您正在做某些非常特定的事情,也许您可以信任版本字符串的来源,那么您只需检查版本字符串的长度,然后进行int转换即可......但我认为最好的做法是确保格式符合预期。


如果您假设格式是规则的,那么简单的词典比较就可以了,您不需要拆分。但在实践中,您会想要拆分:人们总是从1.0开始(因为当时没有人考虑版本号比较),然后他们到达1.10。或者他们从1.05开始,因为两个数字,然后到达1.99 - 或者他们到达9.x并想升级到10。 - toolforger

0

比较版本字符串可能会很麻烦;你得到的答案不太有用,因为使其工作的唯一方法是非常明确地说明你的排序约定。我在博客文章中看到了一个相对简短而完整的版本比较函数,代码放在公共领域中-虽然它不是Java,但应该很容易看出如何进行适应。


0

步骤1:使用Java中的StringTokenizer,以点作为分隔符

StringTokenizer(String str, String delimiters) 或者

您可以使用String.split()Pattern.split(),以点为分隔符,然后使用Integer.parseInt(String str)将每个字符串转换为整数

步骤2:从左到右比较整数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接