この記事でわかること
subListで先頭2件を切り出し、表示用に並べ替えたら、処理待ちの元のリストまで順序が変わりました。同じ範囲を独立したリストへ移し、表示順と処理順を分ける方法をJUnitで確かめます。


注文の発送キューは受付順で、O-003、O-001、O-002が待っています。管理画面の確認欄には、その先頭2件を注文番号順に表示したいとします。先頭2件の選び方はそのまま、表示の順番だけ変えるのが仕様です。
この例で扱うのは、nullを含まない、桁をそろえた文字列の注文番号です。受付順はリストの並びに保持されています。
実行サンプル
元の発送キューが変わるコードと修正版、9件のJUnit、pom.xml、READMEを収録。JDK 17とMavenで、展開したディレクトリから mvn clean test を実行できます。
表示用のsubListを並べ替えると、元の発送キューも並び替わる
次は問題が起きるBrokenDispatchPreview.javaの全文です。pendingには、変更可能なArrayListを渡します。pending.subList(0, 2)で、先頭のO-003とO-001を指定します。
package example.dispatch;
import java.util.Comparator;
import java.util.List;
final class BrokenDispatchPreview {
static List<String> sortedCodes(List<String> pending, int from, int to) {
List<String> preview = pending.subList(from, to);
preview.sort(Comparator.naturalOrder());
return preview;
}
}preview.sortの後、表示は予定どおり[O-001, O-003]になります。表示だけ見れば成功です。ただし元のpendingを読むと、[O-001, O-003, O-002]です。元の先頭がO-003からO-001に変わり、受付順を使う次の発送処理にも影響します。
subListは、元の一部分を別のListとして扱うビュー
subList(from, to)が返すListは、元のListの範囲を参照します。今回のArrayListでは、ビュー側で要素をsetすると元の対応位置に反映され、元の要素をsetするとビューからも変わって見えます。sortがビューの要素の並びを変えると、元の範囲の並びも変わります。Java 17のList.subList。
元の範囲を意図的に削除したいなら、この性質を使えます。pending.subList(0, 2).clear()を実行すると、ArrayListの先頭2件が消え、[O-002]だけが残ります。先頭2件を元の処理待ちから消す仕様ならこの動きでよく、表示だけを変える仕様ならコピーします。
ビューを持ったまま、ビュー経由以外で元のListの要素数などを変えると、そのビューの後の動作はList.subListの契約では定まりません。後続処理に範囲だけ渡し、元のListが別の場所で編集されるなら、使う範囲をコピーしてから渡します。
表示用にコピーし、そのコピーだけを並べ替える
次は修正版DispatchPreview.javaの全文です。new ArrayList<>(pending.subList(from, to))で、その時点の範囲の要素を新しいリストへ移します。その後に並べ替えるのは、新しいリストだけです。
package example.dispatch;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
final class DispatchPreview {
static List<String> sortedCodes(List<String> pending, int from, int to) {
List<String> preview = new ArrayList<>(pending.subList(from, to));
preview.sort(Comparator.naturalOrder());
return preview;
}
}| 先頭2件を並べ替えた結果 | 表示用の順 | 元の発送キュー |
|---|---|---|
| ビューを並べ替える | [O-001, O-003] | [O-001, O-003, O-002] |
| コピーを並べ替える | [O-001, O-003] | [O-003, O-001, O-002] |
独立するのは、リストの順番と、どの要素を持つかです。この例の要素は変更できないStringなので、リストを分ければ表示順を変える目的を満たせます。要素が変更できるOrderで、表示用の処理が注文の中身まで変更する場合は、同じオブジェクトを参照しているかも確認します。
終了位置は含まない。sizeは指定できる
subList(0, 2)の範囲は添字0と1です。要素数3のリストではsubList(0, 3)が全件、subList(3, 3)が空です。終了位置3を範囲の境目として指定できることと、get(3)で要素を取得できることは別です。このサンプルのArrayListでは、subList(0, 4)なら範囲外でIndexOutOfBoundsException、subList(2, 1)なら前後が逆でIllegalArgumentExceptionになります。ArrayList.subListの仕様。
開始・終了が正しくても、元のリストを編集してよいかは別の条件です。取得する添字やページ範囲が原因のときは、範囲外の診断を先に確認できます。
表示結果だけでなく、元の処理順もテストする
配布ZIPを展開し、pom.xmlがある場所でmvn clean testを実行します。DispatchPreviewTest.javaの9件で、表示の順と元の順、ビューでの要素置換・範囲削除、範囲の境界を確認できます。
DispatchPreviewTest.javaの全文を見る
package example.dispatch;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class DispatchPreviewTest {
private static final List<String> PROCESS_ORDER = List.of("O-003", "O-001", "O-002");
@Test
void sortingTheViewAlsoReordersTheProcessingQueue() {
List<String> pending = new ArrayList<>(PROCESS_ORDER);
List<String> preview = BrokenDispatchPreview.sortedCodes(pending, 0, 2);
assertEquals(List.of("O-001", "O-003"), preview);
assertEquals(List.of("O-001", "O-003", "O-002"), pending);
}
@Test
void sortingTheCopyKeepsTheProcessingQueueInOrder() {
List<String> pending = new ArrayList<>(PROCESS_ORDER);
List<String> preview = DispatchPreview.sortedCodes(pending, 0, 2);
assertEquals(List.of("O-001", "O-003"), preview);
assertEquals(PROCESS_ORDER, pending);
}
@Test
void previewAndQueueCanBeEditedIndependently() {
List<String> pending = new ArrayList<>(PROCESS_ORDER);
List<String> preview = DispatchPreview.sortedCodes(pending, 0, 2);
preview.set(0, "O-009");
assertEquals(PROCESS_ORDER, pending);
pending.clear();
assertEquals(List.of("O-009", "O-003"), preview);
}
@Test
void settingAnElementThroughEitherViewIsReflectedInTheOther() {
List<String> pending = new ArrayList<>(PROCESS_ORDER);
List<String> view = pending.subList(0, 2);
view.set(0, "O-008");
assertEquals("O-008", pending.get(0));
pending.set(1, "O-009");
assertEquals(List.of("O-008", "O-009"), view);
}
@Test
void clearingTheViewRemovesTheSelectedRangeFromTheQueue() {
List<String> pending = new ArrayList<>(PROCESS_ORDER);
pending.subList(0, 2).clear();
assertEquals(List.of("O-002"), pending);
}
@Test
void fromEqualToToIsAnEmptyRangeEvenAtSize() {
List<String> pending = new ArrayList<>(PROCESS_ORDER);
assertEquals(List.of(), DispatchPreview.sortedCodes(pending, 3, 3));
}
@Test
void sizeIsAValidExclusiveEnd() {
List<String> pending = new ArrayList<>(PROCESS_ORDER);
assertEquals(List.of("O-001", "O-002", "O-003"),
DispatchPreview.sortedCodes(pending, 0, pending.size()));
}
@Test
void endPastSizeIsRejected() {
List<String> pending = new ArrayList<>(PROCESS_ORDER);
assertThrows(IndexOutOfBoundsException.class,
() -> DispatchPreview.sortedCodes(pending, 0, 4));
}
@Test
void reversedRangeIsRejected() {
List<String> pending = new ArrayList<>(PROCESS_ORDER);
assertThrows(IllegalArgumentException.class,
() -> DispatchPreview.sortedCodes(pending, 2, 1));
}
}pom.xmlの全文を見る
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>java-sublist-view-copy</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.12.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
</plugin>
</plugins>
</build>
</project>通常は9件が成功します。修正版からnew ArrayList<>(...)を外すと、元の受付順が残るテストと、元・表示を別々に編集できるテストの2件が失敗します。表示用の結果は同じなので、assertEqualsを表示のリストだけに書くと今回の不具合を見逃します。
レビューでは、並べ替えが影響してよいリストを確認する
レビューでは「先頭2件の選び方はそのままでよいです。ここでsortすると、発送処理が使う受付順も変わります。表示用のコピーだけを並べ替え、元が[O-003, O-001, O-002]のままか確かめられますか?」と伝えます。
- 範囲の終了位置が、最後に含める要素の一つ先になっている。
- ビューを変更したとき、元の順序や要素が変わってよいか決めている。
- 元のListを残す仕様なら、表示結果と元の状態を両方テストした。
- 要素の中身も変更する場合は、要素の参照が共有されるか確認した。
Listの操作を基礎から復習するなら
Listの追加、取得、走査を自分のコードで書くところから復習したい場合は、『スッキリわかるJava入門 第5版』が候補です。基礎を一通り動かしてから今回の例に戻ると、どの変数を通してリストの中身を読んでいるかを整理しやすくなります。
スッキリわかるJava入門 第5版
変数・条件分岐から、クラスの基本まで学ぶ一冊。
豊富な図とサンプルで、変数、条件分岐、クラス、例外処理まで順番に学べます。Javaの最初の教科書を探している人向けです。
- Javaを基礎から体系的に学びたい
- Java 25 LTS対応の入門書を選びたい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
subListは元のListの一部分を扱うため、ビューの変更は元の範囲にも影響します。表示順だけ変えたいなら、範囲をnew ArrayList<>(...)でコピーしてから並べ替えます。今回の受付順のように、後続処理が使う元の値もテストに残します。



