前言
軟件開發方法學的泰斗肯特·貝克(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提供的元組類:
-
- Pair,KeyValue
隨著元組的元數不斷地增加,代碼的閱讀性也逐漸地下降。當元組的元數超過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);
}
/** 原有靜態方法 */
......
}
六 各種方法綜合點評
下面,針對以上各種實現方法進行一個綜合點評:
總結如下:
- 各種實現方法有利有弊,應當根據具體的使用場景,來選擇最適合的實現方法。
- 根據參數和返回值的類型選擇實現方法:輸入輸出參數儘量使用方法參數實現,返回值儘量使用返回值實現。
- 根據參數和返回值的數量選擇實現方法:數量少的儘量使用支撐類和元組類,數量多的儘量使用自定義類。
- 不建議使用一些取巧的實現方法,比如:3.2.利用單值數組實現、5.1.利用線程本地變量實現。
- 不推薦使用對象數組和對象Map,Java是強類型定義語言,不建議使用強制數據類型轉化。
- 最適合本文中案例的實現方法是——3.4.利用支撐類實現。
後記
《莊子·養生主》有言:
吾生也有涯,而知也無涯。以有涯隨無涯,殆已!
意思是:我的生命是有限的,但知識卻是無限的。用有限的生命去追求無限的知識,必然會失敗的。
所以,知識並不是越多越好,而是“學而精之,精而深之,深而新之 ”。