В том же духе, что и на других платформах, казалось логичным задать следующий вопрос: каковы общие неочевидные ошибки в Java? Вещи, которые кажутся обязательными, но не работают.
Я не буду давать указания относительно того, как структурировать ответы или что «слишком легко» считать ошибкой, поскольку именно для этого и проводится голосование.
Смотрите также:




Пойду первым, вот один, которого я поймал сегодня. Это было связано с путаницей Long / long.
public void foo(Object obj) {
if (grass.isGreen()) {
Long id = grass.getId();
foo(id);
}
}
private void foo(long id) {
Lawn lawn = bar.getLawn(id);
if (lawn == null) {
throw new IllegalStateException("grass should be associated with a lawn");
}
}
Очевидно, имена были изменены, чтобы защитить невиновных :)
если у вас есть метод с тем же именем, что и конструктор, НО имеет возвращаемый тип. Хотя этот метод выглядит как конструктор (для новичка), это НЕ.
передача аргументов в основной метод - нубам нужно время, чтобы привыкнуть.
прохождение. в качестве аргумента пути к классам для выполнения программы в текущем каталоге.
Понимая, что имя массива строк неочевидно
hashCode и равно: многие java-разработчики с опытом работы более 5 лет не совсем понимают это.
Установить против списка
До JDK 6 в Java не было NavigableSets, чтобы можно было легко перебирать Set и Map.
> До JDK 6 в Java не было NavigableSets, чтобы можно было легко выполнять итерацию по Set и Map. Обычные наборы в Java легко повторяются. NavigableSets просто добавляют такие операции, как «найти первый элемент, меньший или равный X». Полезно, но не обязательно просто повторять.
SortedSet (начиная с Java 1.3) имеет методы headSet и tailSet, которые обеспечивают большую часть этих функций.
Попробуйте прочитать Пазлы Java, который полон страшных вещей, даже если вы не сталкиваетесь с ними каждый день. Но это сильно подорвет ваше доверие к языку.
Это в моем списке дел, которые нужно прочитать.
Это отличная книга, но большинство «ошибок» в головоломках - это вещи, которые на практике просто не бывает. Это чрезвычайно эзотерические крайние случаи, из-за которых я сказал: «Кто бы мог написать это в любом случае!»
Большинство ошибок связано с отчетами об ошибках, поэтому очевидно, что люди действительно сталкиваются с этими вещами, а затем очень запутываются. (Я написал последнюю программу в книге (кроме названия), но намеренно.)
Не воспринимайте головоломки Java как удар по Java. Большинство вещей там больше похоже на «Java далеко не идеальна», чем «Java сломана». Другие языки могут доставить вам гораздо больше проблем, вызывающих путаницу.
Перефразируя разговор с Джошем Блошем из «Кодеры за работой». JB: Почему нет книги под названием «C Puzzlers»? Автор: Потому что это все загадки. ДБ: Да, у каждого языка есть крайние случаи. Это заслуга Java, что их можно перечислить в одной небольшой книге.
Сравнение равенства объектов с использованием == вместо .equals() - что ведет себя совершенно иначе для примитивов.
Эта проблема гарантирует, что новички будут сбиты с толку, когда "foo" == "foo", но new String("foo") != new String("foo").
Интересно, что Groovy использует '==' для сравнения значений, а не для сравнения идентичности объектов. Будьте осторожны при копировании!
Чтобы следовать из этого ... String j = "foo"; System.out.println (j == "foo"); // Выводит истину
Практическое правило: если переменная имеет метод equals(), используйте его.
Переопределение equals (), но не hashCode ()
Это может привести к неожиданным результатам при использовании карт, наборов или списков.
Я сделал что-то глупое с хэш-картами в эти дни, я использовал хеш чего-то в качестве ключа для карты, а не сам объект. При моем первом столкновении с хешем у меня возникли проблемы, поскольку метод equals для целого числа возвращал бы true, даже если метод equals для объекта возвращал бы false.
Глупо было изобретать колесо (и, возможно, преждевременно оптимизировать); java.util.HashMap эффективно использует хэш объекта в качестве ключа (для повышения эффективности), но правильно обрабатывает коллизии, используя корзины. Всегда сначала ищите существующий класс, который имеет желаемую функциональность!
Не говоря уже о замене equals неправильной подписью. public boolean equals(MyStuff other) ... вместо public boolean equals(Object other) ...
Я думаю, что очень хитрым является метод String.substring. Это повторно использует тот же базовый массив char[], что и исходная строка, с другими offset и length.
Это может привести к очень трудно заметным проблемам с памятью. Например, вы можете анализировать очень большие файлы (возможно, XML) на несколько небольших битов. Если вы преобразовали весь файл в String (вместо того, чтобы использовать Reader для «обхода» файла) и использовали substring для захвата нужных вам битов, вы все равно несете за кулисами полный массив char[] размером с файл. Я видел, как это происходило несколько раз, и заметить это бывает очень сложно.
Фактически, это прекрасный пример того, почему интерфейс нельзя полностью отделить от выполнение. И это было прекрасным введением (для меня) несколько лет назад, почему вы должны с подозрением относиться к качеству стороннего кода.
Это также очень полезно для экономии памяти, когда вы собираетесь разбить большую строку на несколько подстрок.
Исходный код библиотеки классов Java всегда интересно читать, как к лучшему, так и к худшему. ;)
Можно представить себе оптимизацию виртуальной машины, при которой такой массив может быть частично собран в сборку мусора, если будет доказано, что части недоступны.
Обратите внимание, что этот ответ теперь частично устарел. В более новой версии JDK подстрока не действует. Соответствующий ответ.
Чтобы сделать этот список полным, какая версия JDK меняет поведение?
Изменение было в JDK 7u6, помеченном как ошибка нет. 4513622.
Еще один момент, на который я хотел бы обратить внимание, - это (слишком распространенное) стремление сделать API универсальными. Использование хорошо продуманного универсального кода - это нормально. Создать свой собственный сложно. Очень сложный!
Достаточно взглянуть на функции сортировки / фильтрации в новом Swing JTable. Это полный кошмар. Очевидно, что вы, вероятно, захотите объединить фильтры в цепочку в реальной жизни, но я обнаружил, что это невозможно сделать без использования только необработанной типизированной версии предоставленных классов.
Не очень специфично для Java
Манипулирование компонентами Swing извне потока отправки событий может привести к ошибкам, которые чрезвычайно трудно найти. Это то, о чем часто забываем даже мы (как опытные программисты с 3-мя годами опыта в Java)! Иногда эти ошибки проникают внутрь после правильного написания кода и небрежного рефакторинга впоследствии ...
Смотрите этот руководство, почему вы должен.
Хэш по умолчанию не является детерминированным, поэтому, если он используется для объектов в HashMap, порядок записей на этой карте может меняться от запуска к запуску.
В качестве простой демонстрации следующая программа может давать разные результаты в зависимости от того, как она выполняется:
public static void main(String[] args) {
System.out.println(new Object().hashCode());
}
То, сколько памяти выделено для кучи или запускаете ли вы ее в отладчике, может повлиять на результат.
Это не ошибка, это ожидаемое поведение для хэш-карты. и есть класс карты (LinkedHashMap), основная цель которого - сохранить порядок итераций.
@finnw, для меня это была ошибка, когда я пытался выяснить, что сделала моя программа, и в следующий раз, когда я запустил ее с тем же входом, она выдала другой результат. В конце концов я обнаружил, что Object.hashCode() может давать разные значения даже для первого созданного мной объекта. LinkedHashMap полезен, но в моем случае мне не нужно было сохранять порядок вставки или доступа, и я пытался максимально эффективно использовать память, поэтому, хотя Object.hashCode() подходил для моего алгоритма, я переборщил с ним, чтобы просто получить детерминизм.
Даже в C# не гарантируется стабильность самого хэш-кода. Хеш-коды для одинаковых строк могут различаться в разных версиях .NET Framework и на разных платформах (например, 32-разрядных и 64-разрядных) для одной версии. NET Framework. В некоторых случаях они могут даже различаться в зависимости от домена приложения ». В результате хэш-коды никогда не должны использоваться за пределами домена приложения, в котором они были созданы, они никогда не должны использоваться в качестве ключевых полей в коллекции и они никогда не должны сохраняться ». msdn.microsoft.com/en-us/library/system.string.gethashcode.a spx Похоже на Java, тем более.
Это просто потому, что hashCode объекта является его адресом памяти (или функцией этого адреса). Это согласуется с методом класса Object equals (), который также сравнивает адреса памяти. Любая разумная реализация hashCode для подкласса не будет зависеть от расположения в памяти, а должна зависеть от содержимого объекта.
SimpleDateFormat не является потокобезопасным.
(un) Бокс и долгая / долгая путаница. В отличие от опыта до Java 5, вы можете получить исключение NullPointerException во второй строке ниже.
Long msec = getSleepMsec();
Thread.sleep(msec);
Если getSleepTime () возвращает значение null, при распаковке выполняется ошибка.
Или вы можете использовать long вместо Long, и вы получите NPE на одну строчку раньше.
List<Integer> list = new java.util.ArrayList<Integer>();
list.add(1);
list.remove(1); // throws...
Старые API-интерфейсы не разрабатывались с учетом бокса, поэтому перегружайте их примитивами и объектами.
Среди распространенных ошибок, хорошо известных, но все же иногда кусающих программистов, есть классический if (a = b), который встречается во всех C-подобных языках.
В Java это может работать, конечно, только если a и b логические. Но я слишком часто вижу, как новички тестируют if (a == true) (тогда как if (a) короче, читаемее и безопаснее ...) и иногда по ошибке пишут if (a = true), задаваясь вопросом, почему тест не работает.
Для тех, кто этого не понимает: последний оператор сначала присваивает truea, а затем выполняет тест, который всегда успешен!
-
Тот, который кусает многих новичков и даже некоторых отвлекающих более опытных программистов (нашел это в нашем коде), if (str == "foo"). Обратите внимание, что мне всегда было интересно, почему Sun отменяет знак + для строк, но не знак ==, по крайней мере, для простых случаев (с учетом регистра).
Для новичков: == сравнивает ссылки, а не содержимое строк. У вас может быть две строки с одинаковым содержимым, хранящиеся в разных объектах (разные ссылки), поэтому == будет ложным.
Простой пример:
final String F = "Foo";
String a = F;
String b = F;
assert a == b; // Works! They refer to the same object
String c = "F" + F.substring(1); // Still "Foo"
assert c.equals(a); // Works
assert c == a; // Fails
-
Еще я видел if (a == b & c == d) или что-то в этом роде. Это работает (как ни странно), но мы потеряли ярлык логического оператора (не пытайтесь писать: if (r != null & r.isSomething())!).
Для новичков: при оценке a && b Java не оценивает b, если a ложно. В a & b Java оценивает обе части, а затем выполняет операцию; но вторая часть может провалиться.
[EDIT] Хорошее предложение от J Coombs, я обновил свой ответ.
if (a = b) вызывает ошибку времени компиляции, если a не имеет типа boolean или Boolean. Ура за строгий набор текста. Бу за плохой синтаксис.
PhiLho, похоже, вы предположили, что читатель уже знает Java и знает, что вы имеете в виду. Это помогло бы немного подробнее объяснить, что здесь не так, а что правильно. (Например, успешное присвоение или сравнение; & vs. &&.)
Примечание. Что касается (str == "foo"), проблема заключается в том, что оператор ==, в отличие от C#, не перегружен для строк, поэтому == только сравнивает идентификаторы объектов, а не сравнивает строки.
@JCoombs Спасибо, что указали на это, я обновил свой ответ. Обратите внимание, что новые языки программирования, такие как Scala, Dart или Ceylon, делают правильные вещи для == (вызывая .equals за сценой).
Есть два, которые меня немного раздражают.
Во-первых, классы Java Date и Calendar серьезно испорчены. Я знаю, что есть предложения по их устранению, я просто надеюсь, что они удастся.
Calendar.get (Calendar.DAY_OF_MONTH) основан на 1
Calendar.get (Calendar.MONTH) - 0 на основе
Другой - Integer vs int (это касается любой примитивной версии объекта). Это, в частности, раздражение, вызванное тем, что вы не думаете, что Integer отличается от int (поскольку вы можете обрабатывать их одинаково большую часть времени из-за автобокса).
int x = 5;
int y = 5;
Integer z = new Integer(5);
Integer t = new Integer(5);
System.out.println(5 == x); // Prints true
System.out.println(x == y); // Prints true
System.out.println(x == z); // Prints true (auto-boxing can be so nice)
System.out.println(5 == z); // Prints true
System.out.println(z == t); // Prints SOMETHING
Поскольку z и t являются объектами, даже несмотря на то, что они имеют одинаковое значение, они (скорее всего) являются разными объектами. На самом деле вы имели в виду:
System.out.println(z.equals(t)); // Prints true
Это может быть проблемой для отслеживания. Вы что-то отлаживаете, все выглядит нормально, и вы, наконец, обнаруживаете, что ваша проблема в том, что 5! = 5, когда оба являются объектами.
Возможность сказать
List<Integer> stuff = new ArrayList<Integer>();
stuff.add(5);
так приятно. Это сделало Java настолько удобнее в использовании, что не нужно было помещать все эти строки «new Integer (5)» и «((Integer) list.get (3)). IntValue ()» повсюду. Но эти преимущества приходят с этим.
Мне потребовалось два 8-часовых дня, чтобы понять, почему все наши свидания были на месяц перерыв ... Я выдергивал волосы!
Месяц на основе 0 - это вещь POSIX (и библиотеки C). То, что это стандарт, не означает, что ему всегда нужно следовать.
z == t всегда ложно. Два отдельно новых объекта имеют разную идентичность. С другой стороны, если вы укажете Integer.valueOf (5) в обоих случаях, то это будет один и тот же объект: значения от -128 до 127 должны быть кэшированными значениями.
Если x == z или 5 == z истинно, это из-за auto-распаковка на z, а не из-за какого-либо автоматического бокса. Integer.valueOf (5)! = New Integer (5) (вспоминая мой предыдущий комментарий о новом объекте, имеющем отличную идентичность от любого другого отдельно созданного объекта).
-1. Вы ошибаетесь насчет примера z == t, как описывает @Chris Jester-Young. Возможно, вы путаете это с аналогичным примером в .NET, где z == tможет истинно.
этот несколько раз превзошел меня, и я слышал, что довольно много опытных Java-разработчиков тратят впустую много времени.
ClassNotFoundException --- вы знаете, что класс находится в пути к классам, НО вы НЕ уверены, почему класс НЕ загружается.
Собственно, у этого класса есть статический блок. Произошло исключение в статическом блоке, и кто-то съел исключение. они НЕ ДОЛЖНЫ. Они должны бросать ExceptionInInitializerError. Так что всегда ищите статические блоки, которые могут вас сбить с толку. Это также помогает переместить любой код в статических блоках в статические методы, чтобы упростить отладку метода с помощью отладчика.
Один раз меня достал, но мне потребовались минуты, чтобы разобраться в кодовой базе из сотен тысяч строк - я не знаю, как опытный Java-разработчик был озадачен этим.
Целочисленное деление
1/2 == 0 not 0.5
... наряду с общими правилами преобразования / продвижения / вывода типов с помощью чисел. Например. 1/2.0 действительно имеет значение 0,5, как и 1d/2, и я уверен, что некоторые из головоломок Java связаны именно с тем, когда происходит продвижение, и, следовательно, происходит ли переполнение или нет.
не специфично для java, хотя
Поплавки
Я не знаю, много раз я видел
floata == floatb
где должен быть "правильный" тест
Math.abs(floata - floatb) < 0.001
Я действительно хочу, чтобы BigDecimal с буквальным синтаксисом был десятичным типом по умолчанию ...
Это не относится к Java, но касается способа представления чисел с плавающей точкой. По крайней мере, у C, C++, Python и Perl есть эти проблемы, и, вероятно, у многих других они есть.
Просто потому, что все спрыгнули со скалы ...
"правильный" не совсем подходит для всех случаев (но в этом суть). В противном случае я полагаю, что Java предоставила бы для этого универсальное решение. Я имею в виду, что ваше решение не будет работать для 1.001E-15 по сравнению с 1.002E-15.
Использовать BigDecimal вместо float или даже double - ужасная идея. Использование BigDecimal сильно снижает производительность, и его следует использовать только тогда, когда вам нужна точность. float и double намного лучше, чем BigDecimal, если вы не создаете службу, которая обрабатывает деньги.
Я не верю, что вы могли бы поддержать «огромный», если бы вы провели некоторое исследование реальных, типичных Java-систем. В приложениях, где все накладные расходы на вычисления «огромны», вы не стали бы использовать java для начала, и даже если бы вы это сделали, вы всегда могли бы иметь вместо этого специальные, нестандартные типы для критических расчетов с потерей производительности. Кроме того, я уверен, что можно многое сделать, чтобы разница в производительности не была столь значительной даже для приложений, критичных к производительности. Лучшие реализации плюс аппаратное ускорение могут творить чудеса.
Подобные ситуации news.ycombinator.com/item?id=9476139 просто невозможны без реальных усилий и злого умысла.
ИМХО 1. Использование vector.add (Collection) вместо vector.addall (Collection). Первый добавляет объект коллекции в вектор, а второй добавляет содержимое коллекции. 2. Использование синтаксических анализаторов xml, которые поступают из нескольких источников, таких как xerces, jdom, не имеет прямого отношения к программированию. Полагаться на разные парсеры и иметь их jar-файлы в пути к классам - это кошмар.
Это должна быть старая проблема. Вектор фактически больше не используется. А с общими списками это невозможно.
@John, Vector также является общим
Неизменяемые строки, что означает, что некоторые методы не изменяют исходный объект, а вместо этого возвращают измененную копию объекта. Начиная с Java, я все время забывал об этом и задавался вопросом, почему метод replace не работает с моим строковым объектом.
String text = "foobar";
text.replace("foo", "super");
System.out.print(text); // still prints "foobar" instead of "superbar"
Примечание: строки неизменяемы и в Python, и в C#. Но я согласен с тем, что всякий раз, когда вы впервые сталкиваетесь с этим, эта концепция может сбивать с толку, если у вас есть опыт работы с изменяемыми строками и, следовательно, вы склонны рассматривать их в основном как массивы символов. Также обратите внимание, что по этой причине такие важные вещи, как пароли, следует обрабатывать с помощью char [], поскольку нет возможности стереть строку. stackoverflow.com/questions/8881291/…
Ваш пример довольно тривиален, хотя да, он поставит вас в тупик. Я бы сказал, что более важная вещь, которую я на самом деле видел (а это не должны быть опытные разработчики), связанная с этим, - это синхронизация с неизменяемым неокончательным объектом: <code> class Foo {Long bar = new Long (0L); public void baz () {синхронизировано (бар) {бар + = 1L; }}} </code> В этом примере два конкурирующих потока никогда не будут блокироваться на одном и том же объекте, и 'bar', скорее всего, никогда не будет тем, чем вы хотите (при условии, что два потока не имеют допустимой синхронизации до куча).
@Jon Coombs Фактически, вы можете стереть строку. Вы можете использовать Reflection или Unsafe. Это позволяет делать некоторые сумасшедшие вещи, например, изменять строковый литерал так, чтобы попытка распечатать литерал «foo» фактически выводила «bar» (из-за пула строк)
Неунифицированная система типов противоречит идее объектной ориентации. Несмотря на то, что все не обязательно должно быть объектами, размещенными в куче, программисту все равно должно быть разрешено обрабатывать примитивные типы, вызывая для них методы.
Реализация системы универсальных типов со стиранием типов ужасна и сбивает с толку большинство студентов, когда они впервые узнают о универсальных типах в Java: почему нам все еще нужно приводить типы, если параметр типа уже предоставлен? Да, они обеспечивали обратную совместимость, но довольно глупой ценой.
Я только что наткнулся на этот:
double[] aList = new double[400];
List l = Arrays.asList(aList);
//do intense stuff with l
Кто-нибудь видит проблему?
Что происходит, Arrays.asList() ожидает массив типов объектов (например, Double []). Было бы неплохо, если бы он просто выдавал ошибку для предыдущего ocde. Однако asList() также может принимать такие аргументы:
Arrays.asList(1, 9, 4, 4, 20);
Итак, код создает List с одним элементом - double[].
Я должен был догадаться, когда для сортировки массива из 750000 элементов потребовалось 0 мс ...
Doubles.asList от Guava (guava-libraries.googlecode.com/svn/trunk/javadoc/com/google /…) делает то, что вы хотите. Аналогично Ints.asList делает то же самое для int[] и т. д.
Вы также можете делать что-то, не проходя лизис: double [] sorted = aList.clone (); Arrays.sort (отсортировано); ... а Боб твой дядя. Вы также можете транслировать его и делать то же самое, если хотите. В java.util.Arrays есть много хороших вещей, это не просто asList.
"a,b,c,d,,,".split(",").length
возвращает 4, нет 7, как вы могли (и я, конечно, сделал) ожидать. split игнорирует все завершающие возвращенные пустые строки. Это значит:
",,,a,b,c,d".split(",").length
возвращает 7! Чтобы добиться того, что я бы назвал «наименее удивительным», вам нужно сделать что-нибудь весьма удивительное:
"a,b,c,d,,,".split(",",-1).length
чтобы получить 7.
также обратите внимание, что split принимает регулярное выражение, поэтому для разделения на точку вам нужно будет сделать "a.b".split("\.")
Я думаю, что у меня большая проблема, которая всегда ставила меня в тупик, когда я был молодым программистом, был исключение одновременной модификации при удалении из массива, который вы повторяли:
List list = new ArrayList();
Iterator it = list.iterator();
while(it.hasNext()){
//some code that does some stuff
list.remove(0); //BOOM!
}
Не совсем специфично для Java, поскольку многие (но не все) языки реализуют это таким образом, но оператор % не является истинным оператором по модулю, поскольку он работает с отрицательными числами. Это делает его оператором остаток и может привести к некоторым сюрпризам, если вы об этом не знаете.
Следующий код может выводить либо «четное», либо «нечетное», но это не так.
public static void main(String[] args)
{
String a = null;
int n = "number".hashCode();
switch( n % 2 ) {
case 0:
a = "even";
break;
case 1:
a = "odd";
break;
}
System.out.println( a );
}
Проблема в том, что хэш-код для «числа» отрицательный, поэтому операция n % 2 в коммутаторе также отрицательна. Поскольку в переключателе нет случая, чтобы иметь дело с отрицательным результатом, переменная a никогда не устанавливается. Программа распечатает null.
Убедитесь, что вы знаете, как оператор % работает с отрицательными числами, независимо от того, на каком языке вы работаете.
Разве вы не должны сделать ((n% 2) == 0) означает даже еще ложное?
+1 за »Убедитесь, что вы знаете, как оператор% работает с отрицательными числами, независимо от того, на каком языке вы работаете.« - Тем более, что почти каждый язык обрабатывает модуль по-разному.
Когда вы создаете duplicate или slice из ByteBuffer, он не наследует значение свойства order из родительского буфера, поэтому такой код не будет делать то, что вы ожидаете:
ByteBuffer buffer1 = ByteBuffer.allocate(8);
buffer1.order(ByteOrder.LITTLE_ENDIAN);
buffer1.putInt(2, 1234);
ByteBuffer buffer2 = buffer1.duplicate();
System.out.println(buffer2.getInt(2));
// Output is "-771489792", not "1234" as expected
Более того, они используют общий буфер. duplicate () предназначен для создания другого класса для записи, а не для создания клона и забвения его происхождения.
System.out.println(Calendar.getInstance(TimeZone.getTimeZone("Asia/Hong_Kong")).getTime());
System.out.println(Calendar.getInstance(TimeZone.getTimeZone("America/Jamaica")).getTime());
Выход такой же.
Было бы неплохо включить объяснение. Вот моя попытка: в обоих случаях текущее время в любом месте разрешается в одно и то же время в абсолютном выражении, а то, что печатается, фактически основано на вашем собственном часовом поясе (например, UTC / GMT, если вы находитесь в Англии).
Использование подстановочного знака универсальных шаблонов ?.
Люди видят это и думают, что должны, например используйте List<?>, когда им нужен List, к которому они могут добавить что угодно, не думая, что List<Object> уже делает это. Затем они задаются вопросом, почему компилятор не позволяет им использовать add(), потому что List<?> на самом деле означает «список какого-то определенного типа, которого я не знаю», поэтому единственное, что вы можете сделать с этим List, - это получить из него экземпляры Object.
Однажды мне было интересно отладить TreeSet, так как я не знал об этой информации из API:
Note that the ordering maintained by a set (whether or not an explicit comparator is provided) must be consistent with equals if it is to correctly implement the Set interface. (See Comparable or Comparator for a precise definition of consistent with equals.) This is so because the Set interface is defined in terms of the equals operation, but a TreeSet instance performs all key comparisons using its compareTo (or compare) method, so two keys that are deemed equal by this method are, from the standpoint of the set, equal. The behavior of a set is well-defined even if its ordering is inconsistent with equals; it just fails to obey the general contract of the Set interface. http://download.oracle.com/javase/1.4.2/docs/api/java/util/TreeSet.html
Объекты с правильными реализациями equals / hashcode добавлялись и никогда больше не встречались, поскольку реализация compareTo несовместима с equals.
Это помогло бы объяснить проблему и ее решение. Я предполагаю, что проблема здесь в том, что сигнатура метода foo () ожидает примитив. Код вообще компилируется?