如何在Java中比较两个版本字符串?

211

比较版本号是否有标准的习语?我不能直接使用字符串compareTo,因为我还不知道最大点发布数量是多少。我需要比较版本并满足以下条件:

1.0 < 1.1
1.0.1 < 1.1
1.9 < 1.10

你尝试过去掉点号,将结果字符串解析为整数吗?我目前正在使用类似以下的方法:String version = "1.1.2".replace(".", ""); int number = Integer.parseInt(version); // = 112。你可以将这个数字与另一个数字进行比较,从而找到更近期的版本。此外,你还可以检查version字符串是否匹配某些特定模式,例如\\d+\\.\\d+\\.\\d,以确保结果至少有3个数字。 - machinateur
8
这将如何处理类似于“1.12.1”和“1.1.34”的内容? - kojow7
你需要确保每个部分的长度相同才能进行比较。因此,为了比较您示例的两个版本,它们必须像这样:1.12.01 和 1.01.34。在 Java 中,您可以通过首先在 . 字符处拆分,然后比较每个元素的长度来实现这一点。之后,将所有元素放入一个字符串中,然后将其解析为整数,并将其与以相同方式转换的其他版本进行比较。 - machinateur
只是想分享一下,这个可以在Groovy中实现得非常简短。https://dev59.com/QGsz5IYBdhLWcg3wsaAN#7737400 - rvazquezglez
33个回答

212

针对这篇旧文章的另一种解决方案(仅供有需要的人参考):

public class Version implements Comparable<Version> {

    private String version;

    public final String get() {
        return this.version;
    }

    public Version(String version) {
        if(version == null)
            throw new IllegalArgumentException("Version can not be null");
        if(!version.matches("[0-9]+(\\.[0-9]+)*"))
            throw new IllegalArgumentException("Invalid version format");
        this.version = version;
    }

    @Override public int compareTo(Version that) {
        if(that == null)
            return 1;
        String[] thisParts = this.get().split("\\.");
        String[] thatParts = that.get().split("\\.");
        int length = Math.max(thisParts.length, thatParts.length);
        for(int i = 0; i < length; i++) {
            int thisPart = i < thisParts.length ?
                Integer.parseInt(thisParts[i]) : 0;
            int thatPart = i < thatParts.length ?
                Integer.parseInt(thatParts[i]) : 0;
            if(thisPart < thatPart)
                return -1;
            if(thisPart > thatPart)
                return 1;
        }
        return 0;
    }

    @Override public boolean equals(Object that) {
        if(this == that)
            return true;
        if(that == null)
            return false;
        if(this.getClass() != that.getClass())
            return false;
        return this.compareTo((Version) that) == 0;
    }

}

Version a = new Version("1.1");
Version b = new Version("1.1.1");
a.compareTo(b) // return -1 (a<b)
a.equals(b)    // return false

Version a = new Version("2.0");
Version b = new Version("1.9.9");
a.compareTo(b) // return 1 (a>b)
a.equals(b)    // return false

Version a = new Version("1.0");
Version b = new Version("1");
a.compareTo(b) // return 0 (a=b)
a.equals(b)    // return true

Version a = new Version("1");
Version b = null;
a.compareTo(b) // return 1 (a>b)
a.equals(b)    // return false

List<Version> versions = new ArrayList<Version>();
versions.add(new Version("2"));
versions.add(new Version("1.0.5"));
versions.add(new Version("1.01.0"));
versions.add(new Version("1.00.1"));
Collections.min(versions).get() // return min version
Collections.max(versions).get() // return max version

// WARNING
Version a = new Version("2.06");
Version b = new Version("2.060");
a.equals(b)    // return false

编辑:

@daiscog:感谢您的评论,这段代码是为Android平台开发的,并且根据Google的建议,方法“matches”会检查整个字符串,而不像Java使用正则表达式。(Android文档 - JAVA文档


2
这是我认为最好的解决方案。我通过将其限制为3个元素版本代码,方法是将其更改为if (!version.matches("[0-9]+(\.[0-9]+){0,2}")并添加变量:private static final int[] PRIME = {2, 3, 5}; 我能够为上述创建缺失的hashCode:@Override public final int hashCode() {final String[] parts = this.get().split("\.");int hashCode = 0;for (int i = 0; i < parts.length; i++) {final int part = Integer.parseInt(parts[i]);if (part > 0) {hashCode += PRIME[i] ^ part;}}return hashCode;} - Barry Irvine
1
考虑到您的逻辑具有“O(N log N)”复杂度,因此您至少应该缓存对“Pattern.compile()”的隐式调用。 - Lukas Eder
1
这个实现重写了equals(Object that)方法,因此应该重写hashCode()方法。两个相等的对象必须返回相同的hashCode,否则如果您将这些对象与散列集合一起使用,可能会遇到问题。 - Colin Phillips
由于“new Version("1.0").equals(new Version("1")”将返回true,因此无法基于版本字符串进行哈希。这样做是可行的,但效率低下...//合同:任何两个相等的版本必须返回相同的哈希码。 //由于“1.0”等于“1”,我们不能返回版本字符串的哈希码。 @Override public int hashCode() { return 1; } - Colin Phillips
在 Kotlin 上转换的答案:https://dev59.com/YnVC5IYBdhLWcg3wvT1a#61795721 - Serg Burlaka
你应该解析 String 并将整数存储在构造函数中,而不是在每个 compareTo(Version) 调用中解析字符串,因为每次调用解析都非常昂贵。但是保留字符串以供 get() 使用。 - Olivier Grégoire

120

使用Maven非常简单:

import org.apache.maven.artifact.versioning.DefaultArtifactVersion;

DefaultArtifactVersion minVersion = new DefaultArtifactVersion("1.0.1");
DefaultArtifactVersion maxVersion = new DefaultArtifactVersion("1.10");

DefaultArtifactVersion version = new DefaultArtifactVersion("1.11");

if (version.compareTo(minVersion) < 0 || version.compareTo(maxVersion) > 0) {
    System.out.println("Sorry, your version is unsupported");
}

您可以从此页面获取Maven Artifact正确的依赖项字符串:

<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-artifact</artifactId>
<version>3.0.3</version>
</dependency>

5
我已经创建了一个代码片段,其中包含了如何实现这一过程的测试:https://gist.github.com/2627608 - yclian
12
太棒了,不要重复造轮子! - Lluis Martinez
11
唯一的担忧是:使用这个依赖项来处理大量文件,只为了一个类 - DefaultArtifactVersion。 - ses
7
存储空间很便宜 - 当然比编写、测试和维护原始代码要便宜。 - Alex Dean
13
请注意,Comparable.compareTo 的文档中指出该方法返回值可能是"负整数、零或正整数",因此最好养成避免检查 -1 和 +1 的习惯。 - seanf
显示剩余6条评论

69

使用点作为分隔符对字符串进行标记化,然后从左侧开始逐个比较整数翻译。


1
这正是我猜到不得已要采取的方法。这也涉及到在两个版本字符串中较短的那个中循环遍历标记。感谢您的确认。 - Bill the Lizard
45
请不要忘记,有些应用程序中可能不仅包含数字,还可能包括构建编号,例如1.0.1b表示测试版等。请将其翻译为通俗易懂的中文,但不改变原意。 - John Gardner
2
你怎么做这个? - Big McLargeHuge
你可以编写一个正则表达式,将字符串分成数字和非数字部分。然后按照数字部分的大小和非数字部分的字典序进行比较。(也许还需要在点号处进行分割。) - toolforger
1
现在是2020年,我建议使用语义化版本控制(semver)。请参考https://www.baeldung.com/java-comparing-versions。 - Jacques Koorts
显示剩余2条评论

63

使用Java 9的内置Version

import java.util.*;
import java.lang.module.ModuleDescriptor.Version;
class Main {
  public static void main(String[] args) {
    var versions = Arrays.asList(
      "1.0.2",
      "1.0.0-beta.2",
      "1.0.0",
      "1.0.0-beta",
      "1.0.0-alpha.12",
      "1.0.0-beta.11",
      "1.0.1",
      "1.0.11",
      "1.0.0-rc.1",
      "1.0.0-alpha.1",
      "1.1.0",
      "1.0.0-alpha.beta",
      "1.11.0",
      "1.0.0-alpha.12.ab-c",
      "0.0.1",
      "1.2.1",
      "1.0.0-alpha",
      "1.0.0.1",  // Also works with a number of sections different than 3
      "1.0.0.2",
      "2",
      "10",
      "1.0.0.10"
    );
    versions.stream()
      .map(Version::parse)
      .sorted()
      .forEach(System.out::println);
  }
}

在线试用!

输出:

0.0.1
1.0.0-alpha
1.0.0-alpha.1
1.0.0-alpha.12
1.0.0-alpha.12.ab-c
1.0.0-alpha.beta
1.0.0-beta
1.0.0-beta.2
1.0.0-beta.11
1.0.0-rc.1
1.0.0
1.0.0.1
1.0.0.2
1.0.0.10
1.0.1
1.0.2
1.0.11
1.1.0
1.2.1
1.11.0
2
10

10
截至2020年,这应该是被选中的答案。感谢您的发布。 - Remigius Stalder
如果我们使用的是Java版本>=9,我认为这个答案应该排在前面。 - Amol Patil
抱歉,我不确定一个应用程序或库是否需要导入java.lang.module.ModuleDescriptor.Version来进行版本比较。这是为了比较模块而创建的。 - Maarten Bodewes

52

最佳方案是重复使用现有代码,使用 Maven 的 ComparableVersion 类

优点:

  • Apache 许可证,版本 2.0,
  • 已经经过测试,
  • 在多个项目中使用(复制),例如 spring-security-core、jboss 等
  • 多种功能
  • 它已经是一个 java.lang.Comparable
  • 只需复制粘贴那个类,不需要第三方依赖

请勿包含对 maven-artifact 的依赖项,因为这会拉取各种传递依赖项


这篇文章读起来像是一则广告,对其他答案没有任何帮助。 - eddie_cat
7
这与问题相关,因为它涉及到比较版本的标准方法,而Maven版本比较基本上是标准的。 - Dileep
6
这是最好的答案。我无法相信有多少其他人(包括被接受的答案)尝试一些不可靠的字符串分割方法,而没有进行测试。使用这个类的代码示例:assertTrue(new ComparableVersion("1.1-BETA").compareTo(new ComparableVersion("1.1-RC")) < 0) - Fabian Kessler
1
@toolforger,答案的最后一行建议不要将该工件作为依赖项使用。相反,您可以只复制文件。就像优点列表中所说的那样(两次)。 - Дмитрий Кулешов
@ДмитрийКулешов 哦,我忽略了那个。虽然复制一个类意味着你失去了来自上游项目的更新,并且需要负责保持所有其他属性的完整性;这种权衡应该被提到。 - toolforger
显示剩余2条评论

52

您需要对版本号字符串进行规范化处理,以便进行比较。可以使用以下方法:

import java.util.regex.Pattern;

public class Main {
    public static void main(String... args) {
        compare("1.0", "1.1");
        compare("1.0.1", "1.1");
        compare("1.9", "1.10");
        compare("1.a", "1.9");
    }

    private static void compare(String v1, String v2) {
        String s1 = normalisedVersion(v1);
        String s2 = normalisedVersion(v2);
        int cmp = s1.compareTo(s2);
        String cmpStr = cmp < 0 ? "<" : cmp > 0 ? ">" : "==";
        System.out.printf("'%s' %s '%s'%n", v1, cmpStr, v2);
    }

    public static String normalisedVersion(String version) {
        return normalisedVersion(version, ".", 4);
    }

    public static String normalisedVersion(String version, String sep, int maxWidth) {
        String[] split = Pattern.compile(sep, Pattern.LITERAL).split(version);
        StringBuilder sb = new StringBuilder();
        for (String s : split) {
            sb.append(String.format("%" + maxWidth + 's', s));
        }
        return sb.toString();
    }
}

打印

'1.0' < '1.1'
'1.0.1' < '1.1'
'1.9' < '1.10'
'1.a' > '1.9'

2
规范化的注意事项是其中隐含的最大宽度。 - dlamblin
@IHeartAndroid 很好的观点,除非你期望'4.1' == '4.1.0',否则我认为这是一个意义上的排序。 - Peter Lawrey

37
// VersionComparator.java
import java.util.Comparator;

public class VersionComparator implements Comparator {

    public boolean equals(Object o1, Object o2) {
        return compare(o1, o2) == 0;
    }

    public int compare(Object o1, Object o2) {
        String version1 = (String) o1;
        String version2 = (String) o2;

        VersionTokenizer tokenizer1 = new VersionTokenizer(version1);
        VersionTokenizer tokenizer2 = new VersionTokenizer(version2);

        int number1 = 0, number2 = 0;
        String suffix1 = "", suffix2 = "";

        while (tokenizer1.MoveNext()) {
            if (!tokenizer2.MoveNext()) {
                do {
                    number1 = tokenizer1.getNumber();
                    suffix1 = tokenizer1.getSuffix();
                    if (number1 != 0 || suffix1.length() != 0) {
                        // Version one is longer than number two, and non-zero
                        return 1;
                    }
                }
                while (tokenizer1.MoveNext());

                // Version one is longer than version two, but zero
                return 0;
            }

            number1 = tokenizer1.getNumber();
            suffix1 = tokenizer1.getSuffix();
            number2 = tokenizer2.getNumber();
            suffix2 = tokenizer2.getSuffix();

            if (number1 < number2) {
                // Number one is less than number two
                return -1;
            }
            if (number1 > number2) {
                // Number one is greater than number two
                return 1;
            }

            boolean empty1 = suffix1.length() == 0;
            boolean empty2 = suffix2.length() == 0;

            if (empty1 && empty2) continue; // No suffixes
            if (empty1) return 1; // First suffix is empty (1.2 > 1.2b)
            if (empty2) return -1; // Second suffix is empty (1.2a < 1.2)

            // Lexical comparison of suffixes
            int result = suffix1.compareTo(suffix2);
            if (result != 0) return result;

        }
        if (tokenizer2.MoveNext()) {
            do {
                number2 = tokenizer2.getNumber();
                suffix2 = tokenizer2.getSuffix();
                if (number2 != 0 || suffix2.length() != 0) {
                    // Version one is longer than version two, and non-zero
                    return -1;
                }
            }
            while (tokenizer2.MoveNext());

            // Version two is longer than version one, but zero
            return 0;
        }
        return 0;
    }
}

// VersionTokenizer.java
public class VersionTokenizer {
    private final String _versionString;
    private final int _length;

    private int _position;
    private int _number;
    private String _suffix;
    private boolean _hasValue;

    public int getNumber() {
        return _number;
    }

    public String getSuffix() {
        return _suffix;
    }

    public boolean hasValue() {
        return _hasValue;
    }

    public VersionTokenizer(String versionString) {
        if (versionString == null)
            throw new IllegalArgumentException("versionString is null");

        _versionString = versionString;
        _length = versionString.length();
    }

    public boolean MoveNext() {
        _number = 0;
        _suffix = "";
        _hasValue = false;

        // No more characters
        if (_position >= _length)
            return false;

        _hasValue = true;

        while (_position < _length) {
            char c = _versionString.charAt(_position);
            if (c < '0' || c > '9') break;
            _number = _number * 10 + (c - '0');
            _position++;
        }

        int suffixStart = _position;

        while (_position < _length) {
            char c = _versionString.charAt(_position);
            if (c == '.') break;
            _position++;
        }

        _suffix = _versionString.substring(suffixStart, _position);

        if (_position < _length) _position++;

        return true;
    }
}

示例:

public class Main
{
    private static VersionComparator cmp;

    public static void main (String[] args)
    {
        cmp = new VersionComparator();
        Test(new String[]{"1.1.2", "1.2", "1.2.0", "1.2.1", "1.12"});
        Test(new String[]{"1.3", "1.3a", "1.3b", "1.3-SNAPSHOT"});
    }

    private static void Test(String[] versions) {
        for (int i = 0; i < versions.length; i++) {
            for (int j = i; j < versions.length; j++) {
                Test(versions[i], versions[j]);
            }
        }
    }

    private static void Test(String v1, String v2) {
        int result = cmp.compare(v1, v2);
        String op = "==";
        if (result < 0) op = "<";
        if (result > 0) op = ">";
        System.out.printf("%s %s %s\n", v1, op, v2);
    }
}

输出:

1.1.2 == 1.1.2                --->  same length and value
1.1.2 < 1.2                   --->  first number (1) less than second number (2) => -1
1.1.2 < 1.2.0                 --->  first number (1) less than second number (2) => -1
1.1.2 < 1.2.1                 --->  first number (1) less than second number (2) => -1
1.1.2 < 1.12                  --->  first number (1) less than second number (12) => -1
1.2 == 1.2                    --->  same length and value
1.2 == 1.2.0                  --->  first shorter than second, but zero
1.2 < 1.2.1                   --->  first shorter than second, and non-zero
1.2 < 1.12                    --->  first number (2) less than second number (12) => -1
1.2.0 == 1.2.0                --->  same length and value
1.2.0 < 1.2.1                 --->  first number (0) less than second number (1) => -1
1.2.0 < 1.12                  --->  first number (2) less than second number (12) => -1
1.2.1 == 1.2.1                --->  same length and value
1.2.1 < 1.12                  --->  first number (2) less than second number (12) => -1
1.12 == 1.12                  --->  same length and value

1.3 == 1.3                    --->  same length and value
1.3 > 1.3a                    --->  first suffix ('') is empty, but not second ('a') => 1
1.3 > 1.3b                    --->  first suffix ('') is empty, but not second ('b') => 1
1.3 > 1.3-SNAPSHOT            --->  first suffix ('') is empty, but not second ('-SNAPSHOT') => 1
1.3a == 1.3a                  --->  same length and value
1.3a < 1.3b                   --->  first suffix ('a') compared to second suffix ('b') => -1
1.3a < 1.3-SNAPSHOT           --->  first suffix ('a') compared to second suffix ('-SNAPSHOT') => -1
1.3b == 1.3b                  --->  same length and value
1.3b < 1.3-SNAPSHOT           --->  first suffix ('b') compared to second suffix ('-SNAPSHOT') => -1
1.3-SNAPSHOT == 1.3-SNAPSHOT  --->  same length and value

谢谢。我在我的Eclipse中使用可能是一个非常老的Java版本(即使在powershell中检查时它已经是18.0.1.1),所以这里流行的答案对我不起作用。 - MikeTheSapien
精美编写的代码。再次感谢。 - MikeTheSapien

25

不知道为什么大家都认为版本号只由整数组成 - 在我这里并不是这样。

既然版本号遵循了Semver标准,为什么还要重新发明轮子呢?

首先通过Maven安装https://github.com/vdurmont/semver4j这个库。

然后使用这个库。

Semver sem = new Semver("1.2.3");
sem.isGreaterThan("1.2.2"); // true

18
public static int compareVersions(String version1, String version2){

    String[] levels1 = version1.split("\\.");
    String[] levels2 = version2.split("\\.");

    int length = Math.max(levels1.length, levels2.length);
    for (int i = 0; i < length; i++){
        Integer v1 = i < levels1.length ? Integer.parseInt(levels1[i]) : 0;
        Integer v2 = i < levels2.length ? Integer.parseInt(levels2[i]) : 0;
        int compare = v1.compareTo(v2);
        if (compare != 0){
            return compare;
        }
    }

    return 0;
}

1
适用于简单情况。 - Christophe Roussy
根据您的想法 https://dev59.com/YnVC5IYBdhLWcg3wvT1a#62532745 - Alessandro Scarozza

6
如果你的项目中已经使用了Jackson,你可以使用com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.core.Version;
import org.junit.Test;

import static org.junit.Assert.assertTrue;

public class VersionTest {

    @Test
    public void shouldCompareVersion() {
        Version version1 = new Version(1, 11, 1, null, null, null);
        Version version2 = new Version(1, 12, 1, null, null, null);
        assertTrue(version1.compareTo(version2) < 0);
    }
}

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