Статьи

Даже в JDK есть плохой код

Java 7, TreeSet и NullPointerException.

Недавно я попытался скомпилировать с помощью java 7 проект, разработанный с использованием java 6. Во время выполнения тестов произошло много веселья, тесты, которые в java 6 работали без сбоев, с java 7 они странным образом терпели неудачу! Итак, я должен был понять, почему, и это то, что я обнаружил … Сначала контекст: в этом проекте у меня есть простой Hibernate Entity, более или менее похожий на следующий.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.marco.test;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.hibernate.validator.NotNull;
@Entity
@Table(...)
public class ABean {
 
        ...
 
        private String name;
 
        @Column(name = "name", nullable = false)
        @NotNull
        public String getName() {
                return name;
        }
 
        public void setName(String name) {
                this.name = name;
        }
}

обратите внимание, что поле «имя» имеет значение nullable = false и помечено @NotNull . Это указывает Hibernate на сбой проверки в случае, если пользователь пытается создать или обновить этот столбец до Null. У меня также есть компаратор для этой организации. Этот компаратор использует поле имени для сравнения сущности (это просто упрощенная версия того, что у меня есть в проекте, конечно, я не заказываю бин на основе длины строки)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
package com.marco.test;
import java.util.Comparator;
public class ABeanComparator implements Comparator<ABean> {
 
        @Override
        public int compare(ABean o1, ABean o2) {
                if (o1.getName().length() > o2.getName().length()) {
                        return 1;
                } else if (o1.getName().length() < o2.getName().length()) {
                        return -1;
                } else {
                        return 0;
                }
        }
}

обратите внимание, что в названии поля нет нулевой проверки, в моем проекте Hibernate уже позаботился об этом. Теперь у меня есть тест, который создает одну пустую сущность и сохраняет ее в TreeSet, а затем делает другие вещи, которые нам здесь безразличны. Начало теста похоже на код ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package com.marco.test;
import java.util.SortedSet;
import java.util.TreeSet;
public class SortedTestTest {
 
        public static void main(String[] args) {
 
                ABean aBean = new ABean();
 
                SortedSet<ABean> sortedSet = new TreeSet<ABean>(new ABeanComparator());
 
                sortedSet.add(aBean);
        }
}

Если я запускаю это с Java 6, все в порядке. Но с Java 7 у меня есть исключение NullPointerException.

1
2
3
4
5
6
7
Exception in thread "main" java.lang.NullPointerException
        at com.marco.test.ABeanComparator.compare(ABeanComparator.java:9)
        at com.marco.test.ABeanComparator.compare(ABeanComparator.java:1)
        at java.util.TreeMap.compare(TreeMap.java:1188)
        at java.util.TreeMap.put(TreeMap.java:531)
        at java.util.TreeSet.add(TreeSet.java:255)
        at com.marco.test.SortedTestTest.main(SortedTestTest.java:14)

Почему? Вот почему:

01
02
03
04
05
06
07
08
09
10
    public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check
 
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }

В Java 7, когда первый объект добавляется (if (t == null)) в TreeSet , выполняется сравнение с самим собой (сравнение (ключ, ключ)). Затем метод сравнения вызовет компаратор (если он есть), и у нас будет свойство NullPointerException для свойства name.

1
2
3
4
5
6
7
8
9
    // Little utilities
 
    /**
     * Compares two keys using the correct comparison method for this TreeMap.
     */
    final int compare(Object k1, Object k2) {
        return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
            : comparator.compare((K)k1, (K)k2);
    }

Это вызывает больше вопросов, чем ответов:

  • Зачем проводить сравнение, если вы знаете, что Объект в TreeSet является первым и единственным?
    • Я предполагаю, что они хотели выполнить простую проверку Null.
  • Почему бы не создать правильный метод проверки нуля?
    • Нет ответа
  • Зачем тратить процессор и память на сравнение, которое не нужно?
    • Нет ответа
  • Зачем сравнивать объект с самим собой (сравнивать (ключ, ключ)) ??
    • Нет ответа

Это метод размещения TreeSet в Java 6, и, как вы можете видеть, сравнение было закомментировано.

01
02
03
04
05
06
07
08
09
10
11
12
13
public V put(K key, V value) {
                Entry<K, V> t = root;
                if (t == null) {
                        // TBD:
                        // 5045147: (coll) Adding null to an empty TreeSet should
                        // throw NullPointerException
                        //
                        // compare(key, key); // type check
                        root = new Entry<K, V>(key, value, null);
                        size = 1;
                        modCount++;
                        return null;
                }

Вы видите комментарий? Добавление нуля в пустой TreeSet должно вызвать исключение NullPointerException. Так что просто проверьте, является ли ключ нулевым, не запускайте бесполезное сравнение! Вывод? Всегда старайтесь анализировать код, который вы используете, потому что даже в JDK есть плохой код!