解決 Java 異常:無法強制轉換為 java.lang.Comparable
1. 概述
總的來說,Java 對類型、API 以及我們可以對物件執行的操作都相當保護。然而,在某些情況下,我們仍然可能會弄巧成拙,遇到意想不到的運行時錯誤。
本教學將討論在使用Comparable物件時可能遇到的ClassCastException ,因為該問題可能難以除錯。我們還將考慮已排序和未排序的集合。
2. Null比較器
在程式碼中,我們最常使用簡單的集合,例如ArrayList和LinkedList 。它們維護元素的順序,但不強制執行內部元素的任何特定順序。同時,我們可以根據需要對它們進行排序。問題在於,它們都不要求物件是可比較的,至少沒有明確地要求。 sort sort()方法接受一個Comparator作為參數:
@Test
void givenNonComparableTasksInArrayList_whenSortedWithoutComparator_thenThrowsClassCastException() {
List<NonComparableTask> tasks = new ArrayList<>();
tasks.add(new NonComparableTask("B", 2));
tasks.add(new NonComparableTask("A", 1));
ClassCastException ex = assertThrows(ClassCastException.class, () -> tasks.sort(null));
assertEquals(ClassCastException.class, ex.getClass());
}
在這種情況下,我們得到了一個合理的結果。如果程式碼不知道如何比較List,它會嘗試將清單中的物件強制轉換為Comparable ,然後呼叫compareTo方法。這就是為什麼我們可能會遇到ClassCastException異常的原因。這可能會讓人困惑,因為我們沒有任何編譯時問題(儘管 IDE 可能會標記出這個問題)。但是,考慮到我們使用了null作為參數,這種情況並非完全出乎意料。
因此,如果我們使用不可比較的對象,我們可以傳遞一個預先定義的比較器,從而確保比較不會失敗:
private static final Comparator<NonComparableTask> BY_PRIORITY_THEN_NAME =
Comparator.comparingInt(NonComparableTask::getPriority)
.thenComparing(NonComparableTask::getName);
我們來看一個實作了Comparable:
public class SimpleTask implements Comparable<SimpleTask> {
// Fields, constructors, getters, and setters
@Override
public int compareTo(SimpleTask other) {
if (other == null) {
throw new NullPointerException("other must not be null");
}
int byPriority = Integer.compare(this.priority, other.priority);
if (byPriority != 0) {
return byPriority;
}
return this.name.compareTo(other.name);
}
// Other methods
}
在這種情況下,我們採取了一種更積極的方法來處理nulls ,但這取決於程式碼的使用場景。例如,已排序的集合通常本身就不允許null值,因此在某些情況下,這種檢查可能是多餘的。此外,如果在比較過程中處理**null values**很重要,我們可以使用預先定義的**Comparators** **。 `Comparator.nullsFirst** Comparator.nullsFirst()和Comparator.nullsLast()分別用來將空值放在比較結果的開頭和結尾。
實作compareTo後,理論上我們可以傳遞null ,元素會被毫無問題地轉換為Comparable :
@Test
void givenComparableTasks_whenSortedWithoutComparator_thenNaturalOrderIsUsed() {
List<SimpleTask> tasks = new ArrayList<>();
tasks.add(new SimpleTask("Write docs", 3));
tasks.add(new SimpleTask("Fix build", 1));
tasks.add(new SimpleTask("Review PR", 2));
tasks.add(new SimpleTask("Another P1", 1));
tasks.sort(null);
assertEquals(
Arrays.asList(
new SimpleTask("Another P1", 1),
new SimpleTask("Fix build", 1),
new SimpleTask("Review PR", 2),
new SimpleTask("Write docs", 3)
),
tasks
);
}
然而,這種方法不夠明確。我們可以使用以下技巧來重複使用比較邏輯:
@Test
void givenComparableTasks_whenSortedWithoutComparator_thenNaturalOrderIsUsedExplicitly() {
List<SimpleTask> tasks = new ArrayList<>();
tasks.add(new SimpleTask("Write docs", 3));
tasks.add(new SimpleTask("Fix build", 1));
tasks.add(new SimpleTask("Review PR", 2));
tasks.add(new SimpleTask("Another P1", 1));
tasks.sort(SimpleTask::compareTo);
assertEquals(
Arrays.asList(
new SimpleTask("Another P1", 1),
new SimpleTask("Fix build", 1),
new SimpleTask("Review PR", 2),
new SimpleTask("Write docs", 3)
),
tasks
);
}
透過傳遞方法引用,我們可以讓程式碼更明確,避免傳遞null值,進而提高程式碼的健全性。如果實現的介面發生變化,我們會在編譯時立即發現。此外,我們也可以使用 `Comparator.naturalOrder()` 來避免傳遞null值。這種方法可以被認為是更符合慣用寫法的:
@Test
void givenComparableTasks_whenSortedWithNaturalOrder_thenOrderIsCorrect() {
List<SimpleTask> tasks = new ArrayList<>();
tasks.add(new SimpleTask("Write docs", 3));
tasks.add(new SimpleTask("Fix build", 1));
tasks.add(new SimpleTask("Review PR", 2));
tasks.add(new SimpleTask("Another P1", 1));
tasks.sort(Comparator.naturalOrder());
assertEquals(
Arrays.asList(
new SimpleTask("Another P1", 1),
new SimpleTask("Fix build", 1),
new SimpleTask("Review PR", 2),
new SimpleTask("Write docs", 3)
),
tasks
);
}
但是,需要記住的是,它對不可比較的物件不起作用,因為它必須退而求其次,使用真正的比較邏輯。
3. 原始類型
另一種強制比較不可比較物件並避免編譯時錯誤的方法是使用原始類型。 Collections Collections提供了一個方便的對Collection物件進行排序的方法:
@SuppressWarnings("unchecked")
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
然而,它被參數化,僅接受Comparable元素的集合。同時,如果我們有一個原始集合,則無法強制執行此規則:
@Test
void givenNonComparableTasksInArrayList_whenSortedWithCollectionsSort_thenThrowsClassCastException() {
ArrayList tasks = new ArrayList();
tasks.add(new NonComparableTask("B", 2));
tasks.add(new NonComparableTask("A", 1));
ClassCastException ex = assertThrows(ClassCastException.class, () -> Collections.sort(tasks));
assertEquals(ClassCastException.class, ex.getClass());
}
使用原始類型是一種糟糕的編碼實踐,上面的程式碼就是一個很好的例子。如果我們按照正確的方式處理,這段程式碼甚至無法編譯:
@Test
void givenNonComparableTasksInArrayList_whenSortedWithCollectionsSort_thenThrowsClassCastException() {
ArrayList<NonComparableTask> tasks = new ArrayList<>();
tasks.add(new NonComparableTask("B", 2));
tasks.add(new NonComparableTask("A", 1));
ClassCastException ex = assertThrows(ClassCastException.class, () -> Collections.sort(tasks));
assertEquals(ClassCastException.class, ex.getClass());
}
為了避免此類問題,最好始終檢查 IDE 或其他工具中的標誌和通知。
4. 有序收藏
另一種可能遇到此異常的情況是使用已排序集合,例如PriorityQueue或TreeSet.這種情況可能不太明顯,因為 Java 並不要求這些集合使用Comparable物件。它們的行為與普通集合類似。如果我們想要明確地提供排序邏輯,則需要將Comparator傳遞給建構子。否則,我們必須使用Comparable物件。讓我們來看看不這樣做的情況:
@Test
void givenNonComparableTasks_whenUsingPriorityQueueWithoutComparator_thenAddingThrowsClassCastException() {
PriorityQueue<NonComparableTask> pq = new PriorityQueue<>();
ClassCastException ex = assertThrows(ClassCastException.class,
() -> pq.add(new NonComparableTask("First", 1)));
assertEquals(ClassCastException.class, ex.getClass());
}
@Test
void givenNonComparableTasks_whenUsingTreeSetWithoutComparator_thenAddingThrowsClassCastException() {
TreeSet<NonComparableTask> set = new TreeSet<>();
ClassCastException ex = assertThrows(ClassCastException.class,
() -> set.add(new NonComparableTask("Fix build", 1)));
assertEquals(ClassCastException.class, ex.getClass());
}
這兩種情況下,都會嘗試對物件進行類型轉換,這會導致程式碼運行失敗。不過,好的一方面是,它會在第一次嘗試添加元素時就失敗。雖然問題仍然會在運行時出現,但我們或許能更早發現問題。
為了解決這個問題,我們有兩個選擇。第一種是使用Comparable元素:
@Test
void givenTasksInRandomOrder_whenAddedToPriorityQueue_thenPollingReturnsSortedOrder() {
PriorityQueue<SimpleTask> pq = new PriorityQueue<>();
pq.add(new SimpleTask("Write docs", 3));
pq.add(new SimpleTask("Fix build", 1));
pq.add(new SimpleTask("Review PR", 2));
pq.add(new SimpleTask("Another P1", 1));
List<SimpleTask> polled = new ArrayList<>();
while (!pq.isEmpty()) {
polled.add(pq.poll());
}
assertEquals(
Arrays.asList(
new SimpleTask("Another P1", 1),
new SimpleTask("Fix build", 1),
new SimpleTask("Review PR", 2),
new SimpleTask("Write docs", 3)
),
polled
);
}
第二種方法可能更靈活,即在建立排序集合時提供Comparator :
@Test
void givenNonComparableTasks_whenUsingTreeSetWithComparator_thenIterationIsSorted() {
Comparator<NonComparableTask> byPriorityThenName = Comparator.comparingInt(NonComparableTask::getPriority)
.thenComparing(NonComparableTask::getName);
TreeSet<NonComparableTask> set = new TreeSet<>(byPriorityThenName);
set.add(new NonComparableTask("Write docs", 3));
set.add(new NonComparableTask("Fix build", 1));
set.add(new NonComparableTask("Review PR", 2));
assertEquals(
Arrays.asList(
new NonComparableTask("Fix build", 1),
new NonComparableTask("Review PR", 2),
new NonComparableTask("Write docs", 3)
),
new ArrayList<>(set)
);
}
總的來說,這些解決方案與我們之前的方法非常相似。然而,主要問題在於要牢記這些事項,因為整合開發環境(IDE)和編譯器通常不會標記出這個問題。
5. 結論
本文討論了ClassCastException及其與Comparable物件的關聯。 Java在防止我們使用錯誤的類別並確保遵循既定契約方面做得相當出色。然而,由於不正確的類型轉換,仍然可能出現運行時異常。在大多數情況下,這些問題是由引入程式碼異味和不遵循最佳實踐所造成的。因此,最佳方法是配置錯誤報告和程式碼檢查工具,以免遺漏任何潛在問題。
我們還需要遵循最佳實踐,避免任何會降低類型安全性的編碼方法。此外,我們應該理解,要對物件進行排序,我們需要明確排序邏輯。
像往常一樣,本教程中的所有程式碼都可以在 GitHub 上找到。