本文探讨了在java中对形如"x.y"的数字序列进行排序的正确方法,特别是当期望的排序结果是基于版本号语义而非纯数值大小时。针对常见的将此类数据误用为bigdecimal进行排序的问题,文章强调了其潜在的语义混淆。我们提出并详细介绍了一种更健壮、更清晰的解决方案:通过创建自定义的version类来封装版本逻辑,实现comparable接口,从而确保排序结果符合版本号的预期。
在Java开发中,我们经常会遇到需要对带有小数点的数字进行排序的场景。然而,当这些数字实际上代表版本号(例如“3.2”、“3.9”、“3.10”、“3.12”、“3.17”)时,如果直接使用标准的数值类型(如BigDecimal)进行排序,可能会得到与预期不符的结果。这是因为数值类型会按照其数学值进行比较,而版本号则有其独特的比较规则。
考虑一个包含以下“数字”的列表:[3.2, 3.10, 3.12, 3.17, 3.9]。 如果按照数值大小进行排序,期望的结果可能是:[3.2, 3.9, 3.10, 3.12, 3.17]。 但如果将这些字符串转换为BigDecimal,并使用其默认的compareTo方法进行排序,3.10会被视为与3.1等价(或在某些情况下,其精度可能导致意外行为,但核心问题是它不会像版本号那样将10视为大于9)。在版本号的语义中,3.10显然应该排在3.9之后,因为它的小版本号10大于9。
BigDecimal类设计用于处理任意精度的十进制数字,其compareTo方法严格遵循数值大小比较规则。这意味着:
这与我们期望的版本号排序逻辑完全相悖。版本号的比较通常是逐级进行的:首先比较主版本号(major),如果相同,则比较次版本号(minor),依此类推。因此,将版本号字符串直接映射到BigDecimal并进行排序是一种常见的误用,容易导致逻辑混乱和错误结果。
为了避免上述问题,最健壮和清晰的方法是创建一个专门的类来表示版本号,并实现Comparable接口,从而定义其正确的比较逻辑。
我们可以使用Java 16引入的record类型来简洁地定义一个不可变的Version类。它将包含主版本号(major)和次版本号(minor)两个整数组件。
public record Version(int major, int minor) implements Comparable{ /** * 从字符串解析版本号。 * 支持 "X" (次版本号默认为0) 或 "X.Y" 格式。 * * @param s 版本号字符串 * @return Version 对象 * @throws NumberFormatException 如果字符串格式不正确 */ public static Version parse(String s) { int dot = s.indexOf('.'); if (dot < 0) { // 如果没有小数点,视为只有主版本号,次版本号为0 return new Version(Integer.parseInt(s), 0); } else { // 解析主版本号和次版本号 int major = Integer.parseInt(s.substring(0, dot)); int minor = Integer.parseInt(s.substring(dot + 1)); return new Version(major, minor); } } /** * 实现版本号的比较逻辑。 * 首先比较主版本号,如果相同则比较次版本号。 * * @param v 另一个 Version 对象 * @return 负数、零或正数,表示当前对象小于、等于或大于指定对象 */ @Override public int compareTo(Version v) { // 首先比较主版本号 if (this.major != v.major) { return Integer.compare(this.major, v.major); } // 如果主版本号相同,则比较次版本号 return Integer.compare(this.minor, v.minor); } /** * 返回版本号的字符串表示形式。 * * @return "major.minor" 格式的字符串 */ @Override public String toString() { return major + "." + minor; } }
代码解释:
nor。record会自动生成构造函数、equals()、hashCode()和toString()方法。有了自定义的Version类,我们就可以轻松地对版本号字符串进行正确的排序了。
import java.util.List;
import java.util.stream.Stream;
public class VersionSorter {
public static void main(String[] args) {
// 原始的版本号字符串列表
List versionStrings = List.of("3.2", "3.10", "3.12", "3.17", "3.9");
System.out.println("原始版本号列表: " + versionStrings);
// 使用Stream API和自定义Version类进行排序
System.out.println("\n排序后的版本号列表:");
versionStrings.stream()
.map(Version::parse) // 将字符串映射为 Version 对象
.sorted() // 使用 Version 类的 compareTo 方法进行排序
.forEachOrdered(System.out::println); // 顺序打印结果
}
} 运行上述代码,将得到以下输出:
原始版本号列表: [3.2, 3.10, 3.12, 3.17, 3.9] 排序后的版本号列表: 3.2 3.9 3.10 3.12 3.17
这正是我们期望的版本号排序结果。
通过采用这种方法,我们不仅解决了特定场景下的排序问题,还遵循了良好的面向对象设计原则,使代码更具健壮性、可维护性和可扩展性。