简介: Java没有直接的输入输出参数机制,无法简单地实现参数的输入输出功能,因此需要借助其它方法来实现。本文作者通过实践总结,分享利用方法参数、方法返回值、类字段等方法来实现参数的输入输出,并对比总结各自的优缺点及使用场景。

image.png

前言

软件开发方法学的泰斗肯特·贝克(Kent Beck)曾说过:

我不是一个伟大的程序员,我只是一个具有良好习惯的优秀程序员。

养成良好的习惯,尤其是不断重构的习惯,是每一个优秀程序员都应该具备的素质。重构(Refactoring)就是在不改变软件现有功能的基础上,通过调整程序的结构、提高程序的质量、优化程序的性能……使其程序的设计模式和架构更趋合理,从而提高软件的稳定性、扩展性和维护性。

一 一个需要重构的方法

需求描述:

需要把一个线串(一组经纬度坐标串),按照指定分段长度数组进行按比例划分(由于指定线串的长度较小,可以近似地认为在几何平面上,无需进行球面距离换算)。

代码实现:

/**
 * 几何辅助类
 */
public final class GeometryHelper {

    /** 常量相关 */
    /** 小数位数 */
    private static final int DIGIT_SCALE = 8;
    /** 放大比例 */
    private static final double ZOOM_SCALE = 10000000000L;
    /** 几何工厂 */
    private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(new PrecisionModel(PrecisionModel.FLOATING));

    /**
     * 构造方法
     */
    private GeometryHelper() {
        throw new UnsupportedOperationException();
    }

    /**
     * 划分线串
     * 
     * @param lineString 原始线串
     * @param segmentLengthes 分段长度数组
     * @return 线串数组
     */
    public static LineString[] splitLineString(LineString lineString, double[] segmentLengthes) {
        // 检查分段数量
        if (Objects.isNull(segmentLengthes) || segmentLengthes.length < 1) {
            return new LineString[] {lineString};
        }

        // 计算总共长度
        double totalLength = Arrays.stream(segmentLengthes)
            .map(segmentLength -> Math.max(segmentLength, 0.0D))
            .sum();

        // 计算目标长度
        double lineLength = lineString.getLength();
        long[] targetLengthes = Arrays.stream(segmentLengthes)
            .mapToLong(segmentLength -> getTargetLength(lineLength, totalLength, segmentLength))
            .toArray();

        // 初始化参数值
        int index = 1;
        Coordinate[] coordinates = lineString.getCoordinates();
        Coordinate coordinate = coordinates[0];
        int length = targetLengthes.length;
        LineString[] lineStrings = new LineString[length];

        // 添加前面N段
        for (int i = 0; i < length - 1; i++) {
            // 添加线串坐标
            long addupLength = 0L;
            List<Coordinate> coordinateList = new ArrayList<>();
            coordinateList.add(coordinate);
            for (; index < coordinates.length; index++) {
                // 计算分段长度
                long segmentLength = Math.round(coordinate.distance(coordinates[index]) * ZOOM_SCALE);

                // 根据长度处理
                boolean isBreak = true;
                int compareResult = Long.compare(addupLength + segmentLength, targetLengthes[i]);
                // 根据长度处理: 未达目标长度
                if (compareResult < 0) {
                    addupLength += segmentLength;
                    coordinate = coordinates[index];
                    coordinateList.add(coordinate);
                    isBreak = false;
                }
                // 根据长度处理: 超过目标长度
                else if (compareResult > 0) {
                    long deltaLength = targetLengthes[i] - addupLength;
                    coordinate = buildMiddleCoordinate(coordinate, coordinates[index], segmentLength, deltaLength);
                }
                // 根据长度处理: 等于目标长度
                else {
                    index++;
                    coordinate = coordinates[index];
                }

                // 是否跳出循环
                if (isBreak) {
                    break;
                }
            }
            coordinateList.add(coordinate);

            // 设置线串对象
            lineStrings[i] = buildLineString(coordinateList);
        }

        // 添加最后一段
        lineStrings[length - 1] = buildLineString(coordinates, index, coordinate);

        // 返回线串数组
        return lineStrings;
    }

    /**
     * 构建线串
     * 
     * @param coordinates 坐标数组
     * @param index 当前序号
     * @param coordinate 当前坐标
     * @return 线串
     */
    private static LineString buildLineString(Coordinate[] coordinates, int index, Coordinate coordinate) {
        List<Coordinate> coordinateList = new ArrayList<>();
        coordinateList.add(coordinate);
        coordinateList.addAll(Arrays.asList(ArrayUtils.subarray(coordinates, index, coordinates.length)));
        return buildLineString(coordinateList);
    }

    /**
     * 构建线串
     * 
     * @param coordinateList 坐标列表
     * @return 线串
     */
    private static LineString buildLineString(List<Coordinate> coordinateList) {
        return GEOMETRY_FACTORY.createLineString(coordinateList.toArray(new Coordinate[0]));
    }

    /**
     * 构建中间坐标
     * 
     * @param coordinate1 坐标1
     * @param coordinate2 坐标2
     * @param segmentLength 分段长度
     * @param deltaLength 增量长度
     * @return 中间坐标
     */
    private static Coordinate buildMiddleCoordinate(Coordinate coordinate1, Coordinate coordinate2,
        long segmentLength, long deltaLength) {
        double deltaScale = deltaLength * 1.0D / segmentLength;
        double middleX = round(coordinate1.x + (coordinate2.x - coordinate1.x) * deltaScale, DIGIT_SCALE);
        double middleY = round(coordinate1.y + (coordinate2.y - coordinate1.y) * deltaScale, DIGIT_SCALE);
        return new Coordinate(middleX, middleY);
    }

    /**
     * 获取目标长度
     * 
     * @param lineLength 线路长度
     * @param totalLength 总共长度
     * @param segmentLength 段长度
     * @return 目标长度
     */
    private static long getTargetLength(double lineLength, double totalLength, double segmentLength) {
        return Math.round(Math.max(segmentLength, 0.0D) * ZOOM_SCALE * lineLength / totalLength);
    }

    /**
     * 四舍五入
     * 
     * @param value 双精度浮点值
     * @param scale 保留小数位数
     * @return 四舍五入值
     */
    private static double round(double value, int scale) {
        return BigDecimal.valueOf(value).setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    }  
    
}

备注说明:

在超过目标长度时,获取了一个中间坐标,由于数据精度的关系,这个坐标可能跟上一坐标或下一坐标重合。这里为了降低这块逻辑的复杂度,没有进行前后坐标的去重处理。

存在问题:

在方法splitLineString(划分线串)中,存在一个两层循环,导致了方法逻辑复杂、层级较深、代码量大。如果把外层循环体提炼为一个方法,就能够使代码更简洁、更清晰、更容易维护。

二 一次失败的重构经历

理论依据:

当看到一个方法定义过长或者这段方法需要很多注释才能让人理解的时候,这时候就要考虑是不是把这个方法的部分代码提取出来,形成一个新的方法,方便调用和理解,同时也减小方法的粒度。我们把这种方法叫做提炼函数(Extract Function),在Java语言中可叫做提炼方法(Extract Method)。

重构步骤:

  • 创建一个新方法,并根据这个方法的意图来命名;
  • 将待提炼的代码段从原方法中拷贝到新方法中;
  • 检查提炼的代码段,把缺少的变量添加到方法的参数中;
  • 如果部分参数成对出现,可以把这些参数合并为一个参数;
  • 如果方法需要有返回值,确定返回值的类型,并在合适的位置返回;
  • 在原方法中,删除被提炼的代码段,替换为新方法的调用。

代码实现:

/**
 * 几何辅助类
 */
public final class GeometryHelper {

    /** 原有静态常量 */
    ......

    /**
     * 划分线串
     * 
     * @param lineString 原始线串
     * @param segmentLengthes 分段长度数组
     * @return 线串数组
     */
    public static LineString[] splitLineString(LineString lineString, double[] segmentLengthes) {
        // 原有计算逻辑
        ......

        // 初始化参数值
        int index = 1;
        Coordinate[] coordinates = lineString.getCoordinates();
        Coordinate coordinate = coordinates[0];
        int length = targetLengthes.length;
        LineString[] lineStrings = new LineString[length];

        // 添加前面N段
        for (int i = 0; i < length - 1; i++) {
            lineStrings[i] = combineLineString(coordinates, index, coordinate, targetLengthes[i]);
        }

        // 添加最后一段
        lineStrings[length - 1] = buildLineString(coordinates, index, coordinate);

        // 返回线串数组
        return lineStrings;
    }

    /**
     * 组装线串
     * 
     * @param coordinates 坐标数组
     * @param index 当前序号
     * @param coordinate 当前坐标
     * @param targetLength 目标长度
     * @return 线串
     */
    private static LineString combineLineString(Coordinate[] coordinates, int index, Coordinate coordinate, long targetLength) {
        // 添加线串坐标
        long addupLength = 0L;
        List<Coordinate> coordinateList = new ArrayList<>();
        coordinateList.add(coordinate);
        for (; index < coordinates.length; index++) {
            // 计算分段长度
            long segmentLength = Math.round(coordinate.distance(coordinates[index]) * ZOOM_SCALE);

            // 根据长度处理
            boolean isBreak = true;
            int compareResult = Long.compare(addupLength + segmentLength, targetLength);
            // 根据长度处理: 未达目标长度
            if (compareResult < 0) {
                addupLength += segmentLength;
                coordinate = coordinates[index];
                coordinateList.add(coordinate);
                isBreak = false;
            }
            // 根据长度处理: 超过目标长度
            else if (compareResult > 0) {
                long deltaLength = targetLength - addupLength;
                coordinate = buildMiddleCoordinate(coordinate, coordinates[index], segmentLength, deltaLength);
            }
            // 根据长度处理: 等于目标长度
            else {
                index++;
                coordinate = coordinates[index];
            }

            // 是否跳出循环
            if (isBreak) {
                break;
            }
        }
        coordinateList.add(coordinate);

        // 返回线串对象
        return buildLineString(coordinateList);
    }

    /** 原有静态方法 */
    ......

}

存在问题:

粗看这段代码,似乎没有什么问题。但是,通过测试发现,并没有得到正确的结果。

分析原因:

在《Thinking in Java》中有这样一段话:

When you’re passing primitives into a method,you get a distinct copy of the primitive. When you’re passing a reference into a method, you get a copy of the reference.
当您将基本类型传递到方法中时,您将得到该基本类型的副本。当您将对象引用传递到方法中时,您将得到该对象引用的副本。

原来参数index(当前序号)和coordinate(当前坐标)在方法combineLineString(组装线串)中的修改,只是对该方法中的参数副本进行修改,并没有体现到调用方法splitLineString(划分线串)中,从而导致每次调用都在使用参数的初始化值。其实,这是在提取方法的过程中,没有考虑到参数的作用域。

检查技巧:

这里给出一个作者屡试不爽的检查技巧——把提取方法的所有参数添加上final关键字,编译后观察到哪个参数出现编译错误,就说明这个参数是一个输入输出参数(Inout Parameter)。

解决方案:

在Java语言中,没有直接的输入输出参数机制,无法简单地实现参数的输入输出功能。所以,需要借助其它解决方案,来实现参数的输入输出功能。在这里,作者通过实践总结,给出了以下几种解决方案。

三 利用方法参数实现

本章将从方法参数入手,实现参数的输入输出功能。

3.1 利用参数类实现

理论依据:

引入参数对象(Introduce Parameter Object):当一个方法的参数超过3个时,就可以考虑将参数封装成一个对象类。将参数封装成对象类后,提高了代码的可读性,并且该参数对象类也可以重用。以后,如果增加或删除参数,方法本身不需要修改,只需要修改参数对象类就可以了。

这里,可以利用引入参数对象重构方法,定义一个输入输出参数类,来实现参数的输入输出功能。

代码实现:

/**
 * 几何辅助类
 */
public final class GeometryHelper {

    /** 原有静态常量 */
    ......

    /**
     * 划分线串
     * 
     * @param lineString 原始线串
     * @param segmentLengthes 分段长度数组
     * @return 线串数组
     */
    public static LineString[] splitLineString(LineString lineString, double[] segmentLengthes) {
        // 原有计算逻辑
        ......

        // 初始化参数值
        Coordinate[] coordinates = lineString.getCoordinates();
        InoutParameter inoutParameter = new InoutParameter(1, coordinates[0]);
        int length = targetLengthes.length;
        LineString[] lineStrings = new LineString[length];

        // 添加前面N段
        for (int i = 0; i < length - 1; i++) {
            lineStrings[i] = combineLineString(coordinates, inoutParameter, targetLengthes[i]);
        }

        // 添加最后一段
        lineStrings[length - 1] = buildLineString(coordinates, inoutParameter.getIndex(), inoutParameter.getCoordinate());

        // 返回线串数组
        return lineStrings;
    }

    /**
     * 组装线串
     * 
     * @param coordinates 坐标数组
     * @param inoutParameter 输入输出参数
     * @param targetLength 目标长度
     * @return 线串
     */
    private static LineString combineLineString(Coordinate[] coordinates, InoutParameter inoutParameter, long targetLength) {
        // 获取输入参数
        int index = inoutParameter.getIndex();
        Coordinate coordinate = inoutParameter.getCoordinate();

        // 添加线串坐标
        ......

        // 设置输出参数
        inoutParameter.setIndex(index);
        inoutParameter.setCoordinate(coordinate);

        // 返回线串对象
        return buildLineString(coordinateList);
    }

    /** 原有静态方法 */
    ......

    /**
     * 输入输出参数类
     */
    @Getter
    @Setter
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    private static class InoutParameter {
        /** 当前序号 */
        private int index;
        /** 当前坐标 */
        private Coordinate coordinate;
    }

}

3.2 利用单值数组实现

理论依据:

当您将对象引用传递到方法中时,您将得到该对象引用的副本。也就是说,当您将数组引用传递到方法中时,您将得到该数组引用的副本。但是,这两个数组引用都指向同一个数组,当修改副本数组引用中的值时,也能体现到原有数组引用中。

利用数组引用的这个特性,可以实现参数的输入输出功能。这里,引入了单值数组的概念,即一个数组只有一个值,用于传递输入输出参数值。

代码实现:

/**
 * 几何辅助类
 */
public final class GeometryHelper {

    /** 原有静态常量 */
    ......

    /**
     * 划分线串
     * 
     * @param lineString 原始线串
     * @param segmentLengthes 分段长度数组
     * @return 线串数组
     */
    public static LineString[] splitLineString(LineString lineString, double[] segmentLengthes) {
        // 原有计算逻辑
        ......

        // 初始化参数值
        int[] indexHolder = new int[] {1};
        Coordinate[] coordinates = lineString.getCoordinates();
        Coordinate[] coordinateHolder = new Coordinate[] {coordinates[0]};
        int length = targetLengthes.length;
        LineString[] lineStrings = new LineString[length];

        // 添加前面N段
        for (int i = 0; i < length - 1; i++) {
            lineStrings[i] = combineLineString(coordinates, indexHolder, coordinateHolder, targetLengthes[i]);
        }

        // 添加最后一段
        lineStrings[length - 1] = buildLineString(coordinates, indexHolder[0], coordinateHolder[0]);

        // 返回线串数组
        return lineStrings;
    }

    /**
     * 组装线串
     * 
     * @param coordinates 坐标数组
     * @param indexHolder 序号支撑
     * @param coordinateHolder 坐标支撑
     * @param targetLength 目标长度
     * @return 线串
     */
    private static LineString combineLineString(Coordinate[] coordinates, int[] indexHolder, Coordinate[] coordinateHolder, long targetLength) {
        // 获取支撑取值
        int index = indexHolder[0];
        Coordinate coordinate = coordinateHolder[0];

        // 添加线串坐标
        ......

        // 设置支撑取值
        indexHolder[0] = index;
        coordinateHolder[0] = coordinate;

        // 返回线串对象
        return buildLineString(coordinateList);
    }

    /** 原有静态方法 */
    ......

}

3.3 利用元组类实现

理论依据:

元组(Tuple):Java中的元组(Tuple)是一种数据结构,可以存放多个元素,并且每个元素的数据类型可以不同。Tuple与List类似,但是不同的是,List只能存储一种数据类型,而Tuple可存储多种数据类型。

可能你会质疑,Object类型的List实际也是可以存储多种类型的啊?但是,在创建List时,需要指定元素数据类型,只能指定为Object类型;在获取的元素时,只能获取到Object类型的值,需要强制转化为对应的数据类型。而Tuple在创建时,可以直接指定多个元素数据类型;在获取元素时,无需进行数据类型的强制转化。

常用的元组工具包有:

  • Apache的commons-lang3提供的元组类:
    • Pair:MutablePair,ImmutablePair
    • Triple:MutableTriple、ImmutableTriple
  • JavaTuples提供的元组类:
    • Unit
    • Pair,KeyValue
    • Triplet
    • Quartet
    • Quintet
    • Sextet
    • Septet
    • Octet
    • Ennead
    • Decade

随着元组的元数不断地增加,代码的阅读性也逐渐地下降。当元组的元数超过3个时,不如直接创建对象类,并给予合适类名和字段名,便于代码的理解和维护。所以,不建议使用JavaTuples中的元组类,而推荐使用Apache的commons-lang3提供的元组类。

代码实现:

/**
 * 几何辅助类
 */
public final class GeometryHelper {

    /** 原有静态常量 */
    ......

    /**
     * 划分线串
     * 
     * @param lineString 原始线串
     * @param segmentLengthes 分段长度数组
     * @return 线串数组
     */
    public static LineString[] splitLineString(LineString lineString, double[] segmentLengthes) {
        // 原有计算逻辑
        ......

        // 初始化参数值
        Coordinate[] coordinates = lineString.getCoordinates();
        MutablePair<Integer, Coordinate> mutablePair = MutablePair.of(1, coordinates[0]);
        int length = targetLengthes.length;
        LineString[] lineStrings = new LineString[length];

        // 添加前面N段
        for (int i = 0; i < length - 1; i++) {
            lineStrings[i] = combineLineString(coordinates, mutablePair, targetLengthes[i]);
        }

        // 添加最后一段
        lineStrings[length - 1] = buildLineString(coordinates, mutablePair.getLeft(), mutablePair.getRight());

        // 返回线串数组
        return lineStrings;
    }

    /**
     * 组装线串
     * 
     * @param coordinates 坐标数组
     * @param mutablePair 当前配对
     * @param targetLength 目标长度
     * @return 线串
     */
    private static LineString combineLineString(Coordinate[] coordinates, MutablePair<Integer, Coordinate> mutablePair,
        long targetLength) {
        // 获取配对取值
        int index = mutablePair.getLeft();
        Coordinate coordinate = mutablePair.getRight();

        // 添加线串坐标
        ......

        // 设置配对取值
        mutablePair.setLeft(index);
        mutablePair.setRight(coordinate);

        // 返回线串对象
        return buildLineString(coordinateList);
    }

    /** 原有静态方法 */
    ......

}

3.4 利用支撑类实现

理论依据:

在上一节里,把所有输入输出参数放入到一个元组里,每一个输入输出参数没有一个具体的命名,造成了代码的理解和维护困难。如果每一个输入输出参数都定义一个元组,可以让代码维护者轻松地知道每一个参数的具体含义。所以,这里定义了自己的一元元组类——ObjectHolder(对象支撑类,也可以使用javatuples的Unit类),用于传递输入输出参数值。

代码实现:

/**
 * 几何辅助类
 */
public final class GeometryHelper {
    /** 原有静态常量 */
    ......

    /**
     * 划分线串
     * 
     * @param lineString 原始线串
     * @param segmentLengthes 分段长度数组
     * @return 线串数组
     */
    public static LineString[] splitLineString(LineString lineString, double[] segmentLengthes) {
        // 原有计算逻辑
        ......

        // 初始化参数值
        Coordinate[] coordinates = lineString.getCoordinates();
        ObjectHolder<Integer> indexHolder = new ObjectHolder<>(1);
        ObjectHolder<Coordinate> coordinateHolder = new ObjectHolder<>(coordinates[0]);
        int length = targetLengthes.length;
        LineString[] lineStrings = new LineString[length];

        // 添加前面N段
        for (int i = 0; i < length - 1; i++) {
            lineStrings[i] = combineLineString(coordinates, indexHolder, coordinateHolder, targetLengthes[i]);
        }

        // 添加最后一段
        lineStrings[length - 1] = buildLineString(coordinates, indexHolder.getValue(), coordinateHolder.getValue());

        // 返回线串数组
        return lineStrings;
    }

    /**
     * 组装线串
     * 
     * @param coordinates 坐标数组
     * @param indexHolder 序号支撑
     * @param coordinateHolder 坐标支撑
     * @param targetLength 目标长度
     * @return 线串
     */
    private static LineString combineLineString(Coordinate[] coordinates, ObjectHolder<Integer> indexHolder, ObjectHolder<Coordinate> coordinateHolder, long targetLength) {
        // 获取支撑取值
        int index = indexHolder.getValue();
        Coordinate coordinate = coordinateHolder.getValue();

        // 添加线串坐标
        ......

        // 设置支撑取值
        indexHolder.setValue(index);
        coordinateHolder.setValue(coordinate);

        // 返回线串对象
        return buildLineString(coordinateList);
    }

    /** 原有静态方法 */
    ......

}

/**
 * 对象支撑类
 */
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class ObjectHolder<T> {

    /** 对象取值 */
    private T value;

}

3.5 利用其它方法实现

除此之外,还可以利用其它参数方法实现参数的输入输出功能:

利用数组实现

首先,在调用函数中,定义一个对象数组,把所有输入输出参数存入对象数组中;其次,在被调用函数中,把这些参数从对象数组中取出来使用;再次,在被调用函数中,再把这些参数值存入对象数组中;最后,在调用函数中,把这些参数值从对象数组中取出来使用。

利用对象数组的问题是——代码可读性太差,而且在参数的存入和取出过程中,需要进行数据类型的强制转化。如果所有输入输出参数的类型一致,可以直接定义该类型的数组,从而避免了数据类型的强制转化。

利用Map实现

首先,在调用函数中,定义一个对象Map,把所有输入输出参数存入对象Map中;其次,在被调用函数中,把这些参数从对象Map中取出来使用;再次,在被调用函数中,再把这些参数值存入对象Map中;最后,在调用函数中,把这些参数值从对象Map中取出来使用。

利用对象Map实现,代码的可读性比利用对象数组实现更强,但是也存在同样的问题——在参数的存入和取出过程中,需要进行数据类型的强制转化。如果所有输入输出参数的类型一致,可以直接定义该类型的Map,从而避免了数据类型的强制转化。但是,利用对象Map实现,还不如定义一个参数类更实用。

利用原子类实现

JDK中,提供了一套原子类——AtomicInteger、AtomicLong、AtomicDouble等,可用于对应的基础类型和包装类型,实现对应参数的输入输出功能。实现方法跟ObjectHolder一样,这里不再累述。

四 利用方法返回值实现

本章将从方法返回值入手,实现参数的输入输出功能。

4.1 利用结果类实现

理论依据:

引入返回值对象(Introduce Return Object):当一个方法的需要返回多个值时,就可以考虑将返回值封装成一个对象类。将返回值封装成对象类后,提高了代码的可读性,并且该返回值对象类也可以重用。以后,如果增加或删除返回值,方法本身不需要修改,只需要修改返回值对象类就可以了。

这里,可以利用引入返回值对象重构方法,定义一个返回值对象类,来实现参数的输入输出功能。

代码实现:

/**
 * 几何辅助类
 */
public final class GeometryHelper {

    /** 原有静态常量 */
    ......

    /**
     * 划分线串
     * 
     * @param lineString 原始线串
     * @param segmentLengthes 分段长度数组
     * @return 线串数组
     */
    public static LineString[] splitLineString(LineString lineString, double[] segmentLengthes) {
        // 原有计算逻辑
        ......

        // 初始化参数值
        int index = 1;
        Coordinate[] coordinates = lineString.getCoordinates();
        Coordinate coordinate = coordinates[0];
        int length = targetLengthes.length;
        LineString[] lineStrings = new LineString[length];

        // 添加前面N段
        for (int i = 0; i < length - 1; i++) {
            ReturnResult result = combineLineString(coordinates, index, coordinate, targetLengthes[i]);
            index = result.getIndex();
            coordinate = result.getCoordinate();
            lineStrings[i] = result.getLineString();
        }

        // 添加最后一段
        lineStrings[length - 1] = buildLineString(coordinates, index, coordinate);

        // 返回线串数组
        return lineStrings;
    }

    /**
     * 组装线串
     * 
     * @param coordinates 坐标数组
     * @param index 当前序号
     * @param coordinate 当前坐标
     * @param targetLength 目标长度
     * @return 返回值
     */
    private static ReturnResult combineLineString(Coordinate[] coordinates, int index, Coordinate coordinate, long targetLength) {
        // 添加线串坐标
        ......

        // 返回输出结果
        return new ReturnResult(index, coordinate, buildLineString(coordinateList));
    }

    /** 原有静态方法 */
    ......

    /**
     * 返回值类
     */
    @Getter
    @Setter
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    private static class ReturnResult {
        /** 当前序号 */
        private int index;
        /** 当前坐标 */
        private Coordinate coordinate;
        /** 线串对象 */
        private LineString lineString;
    }

}

4.2 利用元组类实现

理论依据:

参考3.3章节的元组(Tuple)的定义和特性。元组(Tuple)可以用于方法的参数值,也可以用于方法的返回值。当一个方法需要返回多个值时,又不愿意定义自己的结果类时,可以采用元组(Tuple)实现多个值的返回。

代码实现:

/**
 * 几何辅助类
 */
public final class GeometryHelper {

    /** 原有静态常量 */
    ......

    /**
     * 划分线串
     * 
     * @param lineString 原始线串
     * @param segmentLengthes 分段长度数组
     * @return 线串数组
     */
    public static LineString[] splitLineString(LineString lineString, double[] segmentLengthes) {
        // 原有计算逻辑
        ......

        // 初始化参数值
        int index = 1;
        Coordinate[] coordinates = lineString.getCoordinates();
        Coordinate coordinate = coordinates[0];
        int length = targetLengthes.length;
        LineString[] lineStrings = new LineString[length];

        // 添加前面N段
        for (int i = 0; i < length - 1; i++) {
            Triple<Integer, Coordinate, LineString> triple = combineLineString(coordinates, index, coordinate, targetLengthes[i]);
            index = triple.getLeft();
            coordinate = triple.getMiddle();
            lineStrings[i] = triple.getRight();
        }

        // 添加最后一段
        lineStrings[length - 1] = buildLineString(coordinates, index, coordinate);

        // 返回线串数组
        return lineStrings;
    }

    /**
     * 组装线串
     * 
     * @param coordinates 坐标数组
     * @param index 当前序号
     * @param coordinate 当前坐标
     * @param targetLength 目标长度
     * @return 返回值
     */
    private static Triple<Integer, Coordinate, LineString> combineLineString(Coordinate[] coordinates, int index, Coordinate coordinate, long targetLength) {
        // 添加线串坐标
        ......

        // 返回输出结果
        return ImmutableTriple.of(index, coordinate, buildLineString(coordinateList));
    }

    /** 原有静态方法 */
    ......

}

4.3 利用其它方法实现

除此之外,还可以利用其它返回值方法实现参数的输入输出功能:

利用数组实现

首先,在被调用方法中,定义一个对象数组,把多个返回值放入到对象数组中;最后,在调用函数中,把这些参数值从对象数组中取出来,并强制转化为对应的数据类型。

利用对象数组的问题是——代码可读性太差,而且在返回值的存入和取出过程中,需要进行数据类型的强制转化。如果所有返回值的数据类型一致,可以直接定义该类型的数组,从而避免了数据类型的强制转化。

利用Map实现

首先,在被调用方法中,定义一个对象Map,把多个返回值放入到对象Map中;最后,在调用函数中,把这些参数值从对象Map中取出来,并强制转化为对应的数据类型。

利用对象Map实现,代码的可读性比利用对象数组实现更强,但是也存在同样的问题——在返回值的存入和取出过程中,需要进行数据类型的强制转化。如果所有返回值的类型一致,可以直接定义该类型的Map,从而避免了数据类型的强制转化。但是,利用对象Map实现,还不如定义一个返回值类更实用。

五 利用类字段实现

本章将从类字段入手,实现参数的输入输出功能。

5.1 利用线程本地变量实现

理论依据:

线程本地变量(ThreadLocal):线程本地变量不同于它们的普通变量,因为访问某个变量的每个线程都有自己的局部变量,且独立于变量的初始化副本。线程本地变量实例通常是类中的私有静态字段,它希望将变量状态与某一个线程关联起来。

要用类字段解决参数的输入输出问题,就必须考虑方法的线程安全性。这里,利用线程本地变量(ThreadLocal)来实现线程中输入输出参数值共享。

代码实现:

/**
 * 几何辅助类
 */
public final class GeometryHelper {

    /** 属性相关 */
    /** 当前序号支撑 */
    private static final ThreadLocal<Integer> INDEX_HOLDER = new ThreadLocal<>();
    /** 当前坐标支撑 */
    private static final ThreadLocal<Coordinate> COORDINATE_HOLDER = new ThreadLocal<>();

    /** 原有静态常量 */
    ......

    /**
     * 划分线串
     * 
     * @param lineString 原始线串
     * @param segmentLengthes 分段长度数组
     * @return 线串数组
     */
    public static LineString[] splitLineString(LineString lineString, double[] segmentLengthes) {
        // 原有计算逻辑
        ......

        // 初始化参数值
        INDEX_HOLDER.set(1);
        Coordinate[] coordinates = lineString.getCoordinates();
        COORDINATE_HOLDER.set(coordinates[0]);
        int length = targetLengthes.length;
        LineString[] lineStrings = new LineString[length];

        // 添加前面N段
        for (int i = 0; i < length - 1; i++) {
            lineStrings[i] = combineLineString(coordinates, targetLengthes[i]);
        }

        // 添加最后一段
        lineStrings[length - 1] = buildLineString(coordinates, INDEX_HOLDER.get(), COORDINATE_HOLDER.get());

        // 清除支撑类
        INDEX_HOLDER.remove();
        COORDINATE_HOLDER.remove();

        // 返回线串数组
        return lineStrings;
    }

    /**
     * 组装线串
     * 
     * @param coordinates 坐标数组
     * @param targetLength 目标长度
     * @return 线串
     */
    private static LineString combineLineString(Coordinate[] coordinates, long targetLength) {
        // 获取支撑取值
        int index = INDEX_HOLDER.get();
        Coordinate coordinate = COORDINATE_HOLDER.get();

        // 添加线串坐标
        ......

        // 设置支持取值
        INDEX_HOLDER.set(index);
        COORDINATE_HOLDER.set(coordinate);

        // 返回线串对象
        return buildLineString(coordinateList);
    }

    /** 原有静态方法 */
    ......

}

5.2 利用类成员变量实现

理论依据:

在上一章节中,利用线程本地变量(ThreadLocal)来实现线程中输入输出参数值共享,让方法的封装更复杂——需要从线程本地变量(ThreadLocal)读取和存储输入输出参数值。有没有一种更简单的方法,直接利用类成员变量实现输入输出参数值的共享呢?

答案是肯定的,可以把方法的封装和变量的定义封装到一个类中。这样,在每一个类实例中,都可以利用类成员变量来实现输入输出参数值的共享。但是,这个类是线程非安全的,必须在单线程中使用。

代码实现:

/**
 * 几何辅助类
 */
public final class GeometryHelper {

    // 原有构造方法
    ......

    /**
     * 划分线串
     * 
     * @param lineString 原始线串
     * @param segmentLengthes 分段长度数组
     * @return 线串数组
     */
    public static LineString[] splitLineString(LineString lineString, double[] segmentLengthes) {
        SplitLineStringAlgorithm algorithm = new SplitLineStringAlgorithm();
        return algorithm.splitLineString(lineString, segmentLengthes);
    }

}

/**
 * 划分线串算法类
 */
public class SplitLineStringAlgorithm {

    /** 属性相关 */
    /** 当前序号 */
    private int index;
    /** 当前坐标 */
    private Coordinate coordinate;

    /** 原有静态常量 */
    ......

    /**
     * 划分线串
     * 
     * @param lineString 原始线串
     * @param segmentLengthes 分段长度数组
     * @return 线串数组
     */
    public LineString[] splitLineString(LineString lineString, double[] segmentLengthes) {
        // 原有计算逻辑
        ......

        // 初始化参数值
        index = 1;
        Coordinate[] coordinates = lineString.getCoordinates();
        coordinate = coordinates[0];
        int length = targetLengthes.length;
        LineString[] lineStrings = new LineString[length];

        // 添加前面N段
        for (int i = 0; i < length - 1; i++) {
            lineStrings[i] = combineLineString(coordinates, targetLengthes[i]);
        }

        // 添加最后一段
        lineStrings[length - 1] = buildLineString(coordinates, index, coordinate);

        // 返回线串数组
        return lineStrings;
    }

    /**
     * 组装线串
     * 
     * @param coordinates 坐标数组
     * @param targetLength 目标长度
     * @return 线串
     */
    private LineString combineLineString(Coordinate[] coordinates, long targetLength) {
        // 添加线串坐标
        ......

        // 返回线串对象
        return buildLineString(coordinateList);
    }

    /** 原有静态方法 */
    ......

}

六 各种方法综合点评

下面,针对以上各种实现方法进行一个综合点评:

image.png

总结如下:

  • 各种实现方法有利有弊,应当根据具体的使用场景,来选择最适合的实现方法。
  • 根据参数和返回值的类型选择实现方法:输入输出参数尽量使用方法参数实现,返回值尽量使用返回值实现。
  • 根据参数和返回值的数量选择实现方法:数量少的尽量使用支撑类和元组类,数量多的尽量使用自定义类。
  • 不建议使用一些取巧的实现方法,比如:3.2.利用单值数组实现、5.1.利用线程本地变量实现。
  • 不推荐使用对象数组和对象Map,Java是强类型定义语言,不建议使用强制数据类型转化。
  • 最适合本文中案例的实现方法是——3.4.利用支撑类实现。

后记

《庄子·养生主》有言:

吾生也有涯,而知也无涯。以有涯随无涯,殆已!

意思是:我的生命是有限的,但知识却是无限的。用有限的生命去追求无限的知识,必然会失败的。

所以,知识并不是越多越好,而是“学而精之,精而深之,深而新之 ”。


阿里云开发者
3.2k 声望6.3k 粉丝

阿里巴巴官方技术号,关于阿里巴巴经济体的技术创新、实战经验、技术人的成长心得均呈现于此。