Java – Common Gotchas

本着其他平台的精神,跟进这个问题似乎是合乎逻辑的:Java中常见的非显而易见的错误是什么? 似乎他们应该工作的东西,但不要。

我不会给出如何构build答案的指导方针,也不会给出指导方针,因为这就是投票的目的。

也可以看看:

  • Perl – 常见的陷阱
  • .NET – 共同的陷阱

使用==而不是.equals()来比较对象的相等性 – 对于基元,其行为完全不同。

这个gotcha确保当"foo" == "foo"但是new String("foo") != new String("foo")时,新手被弄糊涂了。

 "a,b,c,d,,,".split(",").length 

返回4, 而不是 7你可能(我当然 )期望。 split忽略所有返回的空尾string。 这意味着:

 ",,,a,b,c,d".split(",").length 

返回7! 为了得到我所认为的“最不可思议”的行为,你需要做一些相当惊人的事情:

 "a,b,c,d,,,".split(",",-1).length 

得到7。

重写equals()而不是hashCode()

使用地图,集合或列表时,可能会有非常意想不到的结果。

我认为一个非常偷偷摸摸的是String.substring方法。 这重新使用与原始string具有不同的offsetlength的相同的基础char[]数组。

这可能会导致很难看到的内存问题。 例如,你可能会parsing一些非常大的文件(可能是XML )。 如果你已经把整个文件转换为一个String (而不是用Reader来“遍历”这个文件)并且使用substring来获取你想要的位,那么你仍然在整个文件大小的char[]数组中场景。 我已经看到这种情况发生了很多次,可能很难发现。

事实上,这是一个完美的例子,为什么接口永远不能与实现完全分离。 对于为什么你应该怀疑第三方代码的质量,这是一个很好的介绍(对我来说)几年前。

SimpleDateFormat不是线程安全的。

有两个让我烦恼不less。

date/日历

首先,Javadate和日历类严重混乱。 我知道有build议来解决他们,我只是希望他们成功。

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 

这可能是一个痛苦追查。 你去debugging一些东西,一切都看起来很好,最后你最终发现你的问题是当两个对象都是5!= 5。

可以说

 List<Integer> stuff = new ArrayList<Integer>(); stuff.add(5); 

太好了。 它让Java变得更加实用,不必把所有这些“新的Integer(5)”和“((Integer)list.get(3))。intValue()”都放在这个地方。 但是这些好处来自这个问题。

尝试阅读充满可怕东西的Java Puzzlers ,即使这些东西大部分都不是你每天碰到的东西。 但是这会破坏你对语言的大部分信心。

 List<Integer> list = new java.util.ArrayList<Integer>(); list.add(1); list.remove(1); // throws... 

旧的API并没有考虑到拳击的devise,所以过载了基元和对象。

我刚刚遇到的这一个:

 double[] aList = new double[400]; List l = Arrays.asList(aList); //do intense stuff with l 

任何人都看到问题?


会发生什么事, Arrays.asList()需要一个对象types的数组(例如Double [])。 如果它只是抛出了以前的错误,这将是很好的。 但是, asList()也可以像这样使用参数:

 Arrays.asList(1, 9, 4, 4, 20); 

所以代码是用一个元素创build一个List – 一个double[]

我应该想出了花费0毫秒sorting一个750000元素arrays…

花车

我不知道我见过很多次

 floata == floatb 

在哪里“正确的”testing应该是

 Math.abs(floata - floatb) < 0.001 

我真的希望BigDecimal与文字语法是默认的十进制types…

这一个已经超过了我几次,我听说了不less经验丰富的java开发人员浪费了很多时间。

ClassNotFoundException —你知道类是在类path中,但是你不知道为什么类没有被加载。

其实这个类有一个静态块。 在静态块中有一个例外,有人吃了这个例外。 他们不应该。 他们应该抛出ExceptionInInitializerError。 所以,总是寻找静态块来绊倒你。 它还有助于将静态块中的任何代码移动到静态方法中,以便使用debugging器debugging该方法变得更容易。

对于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 ); } 

问题是“number”的散列码是负数,所以交换机中的n % 2操作也是负数。 由于交换机没有处理负面结果的情况,所以variablesa永远不会被设置。 该程序打印出null

确保你知道%运算符如何处理负数,不pipe你在用什么语言。

我认为当我是一个年轻的程序员时,我总是会陷入困境,是从一个数组中移除的并发修改exception

  List list = new ArrayList(); Iterator it = list.iterator(); while(it.hasNext()){ //some code that does some stuff list.remove(0); //BOOM! } 

从事件调度线程外部操作Swing组件可能会导致难以发现的错误。 即使我们(经验丰富的程序员,拥有3年6年Java经验)经常忘记这件事情! 有时候这些错误会在写完代码之后溜进去,然后不小心重构

看到这个教程为什么你必须

整数划分

 1/2 == 0 not 0.5 

不可变的string,这意味着某些方法不会更改原始对象,而是返回修改的对象副本。 当我用Java开始时,我总是忘记这一点,并想知道为什么replace方法似乎不能在我的string对象上工作。

 String text = "foobar"; text.replace("foo", "super"); System.out.print(text); // still prints "foobar" instead of "superbar" 

如果你有一个与构造函数同名的方法,那么BUT有一个返回types。 尽pipe这个方法看起来像一个构造函数(对于noob),但它不是。

将主要parameter passing给主要方法 – 新手需要一些时间才能习惯。

传球。 作为在当前目录中执行程序的类path的参数。

意识到一个string数组的名称并不明显

hashCode和equals:很多拥有5年以上经验的java开发人员都不太清楚。

设置vs列表

直到JDK 6,Java没有NavigableSets让你轻松地遍历Set和Map。

(联合)拳击和长期/长期混乱。 与之前的Java 5体验相反,您可以在下面的第二行获得NullPointerException。

 Long msec = getSleepMsec(); Thread.sleep(msec); 

如果getSleepTime()返回null,则取消装箱操作。

默认散列是非确定性的,因此如果用于HashMap中的对象,则该映射中条目的sorting可能会从运行变为运行。

作为一个简单的演示,下面的程序可以根据运行方式给出不同的结果:

 public static void main(String[] args) { System.out.println(new Object().hashCode()); } 

多less内存分配给堆,或者是否在debugging器中运行,都可以改变结果。

使用? generics通配符。

人们看到它,并认为他们必须,例如,当他们想要一个List他们可以添加任何东西,而不是停下来认为一个List<Object>已经做到这一点,使用List<?> 。 然后他们想知道为什么编译器不会让它们使用add() ,因为List<?>真的意味着“我不知道的某种特定types的列表”,所以你可以用List来做的唯一事情就是获取来自它的Object实例。

当您创build一个ByteBufferduplicateslice时,它不会从父缓冲区inheritanceorder属性的值 ,所以像这样的代码不会达到您的预期:

 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 

非统一型制度与客体导向思想相矛盾。 尽pipe一切都不一定是堆分配的对象,程序员仍然应该允许通过调用它们的方法来处理基本types。

types擦除的genericstypes系统实现是可怕的,并且当他们在Java中学习generics时抛弃了大多math生:为什么如果已经提供了types参数,我们仍然需要进行types转换? 是的,他们确保向后兼容,但成本相当低。

在常见的陷阱中,众所周知但偶尔也会咬人程序员,在所有C语言中都有经典的if (a = b)

在Java中,当然,只有a和b是布尔值才能工作。 但是我经常看到新手testingif (a == true)if (a)更短,更可读和更安全…),偶尔会写错if (a = true) ,为什么testing不会,工作。
对于那些没有得到它的人:最后的声明首先分配给a true a ,然后做testing,总是成功的!

一个咬了很多新手,甚至有些分心的更有经验的程序员(在我们的代码中find它), if (str == "foo") 。 请注意,我总是想知道为什么Sun会覆盖string的+号而不是==号,至less对于简单的情况(区分大小写)。

对于新手来说: ==比较引用,而不是string的内容。 你可以有两个相同内容的string,存储在不同的对象(不同的引用),所以==将是错误的。

简单的例子:

 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 ,如果a是false,Java不会评估b。 在a & b ,Java评估两个部分然后执行操作; 但第二部分可能会失败。

[编辑]来自J库姆斯的好build议,我更新了我的答案。

首先,这是我今天发现的一个。 这与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"); } } 

显然,这些名字已经改变,以保护无辜的:)

另一个我想指出的是(通俗)推动API通用。 使用精心devise的通用代码是很好的。 devise你自己很复杂。 非常复杂!

只要看看新的Swing JTable中的sorting/筛选function。 这是一个完整的噩梦。 很明显,你可能想要在现实生活中连锁filter,但是我发现如果不使用所提供的原始types版本,就不可能这样做。

  System.out.println(Calendar.getInstance(TimeZone.getTimeZone("Asia/Hong_Kong")).getTime()); System.out.println(Calendar.getInstance(TimeZone.getTimeZone("America/Jamaica")).getTime()); 

输出是一样的。

恕我直言1.使用vector.add(集合),而不是vector.addall(集合)。 第一个将集合对象添加到vector,第二个添加集合的内容。 2.虽然与编程无关,但使用来自多个来源(如xerces,jdom)的xmlparsing器。 依靠不同的parsing器,并在类path中有他们的jar子是一场噩梦。

我曾经有过一次debuggingTreeSet的乐趣,因为我没有意识到API中的这些信息:

请注意,如果要正确实现Set接口,则由集合(不论是否提供显式比较器)维护的sorting必须与equals保持一致。 (请参阅Comparable或Comparator以获得与equals一致的精确定义)。这是因为Set接口是根据equals操作定义的,但TreeSet实例使用compareTo(或compare)方法执行所有关键比较,所以两个从这个方法来看,被这个方法视为相等的密钥是相等的。 即使sorting与等号不一致,集合的行为也是明确的。 它只是不服从Set接口的总体合同。 http://download.oracle.com/javase/1.4.2/docs/api/java/util/TreeSet.html

具有正确的equals / hashcode实现的对象正在被添加,并且再也没有看到compareTo实现与equals不一致的情况。