当前位置: 首页 > 科技观察

Java最常见的五个错误

时间:2023-03-12 09:22:35 科技观察

在编程的时候,开发者经常会遇到各种莫名其妙的错误。近日,SushilDas在GeekOnJava上列出了Java开发中的5个常见错误,并“免费”分享给大家。1.过度使用Null避免过度使用null值是最佳实践。例如,最好让方法返回空数组或集合而不是空值,因为这可以防止程序抛出NullPointerException。以下代码片段将从另一个方法获取集合:ListaccountIds=person.getAccountIds();for(StringaccountId:accountIds){processAccount(accountId);}当一个人没有账户时,getAccountIds()将返回null值,程序将抛出NullPointerException。因此,需要增加空检查来解决这个问题。如果将返回的null值替换为空列表,则NullPointerException也不会发生。此外,由于我们不再需要对变量accountId进行空检查,因此代码会更加简洁。当你想避免空值时,不同的场景可能会采取不同的做法。一种方法是使用Optional类型,它可以是空对象或某个值的包装器。OptionaloptionalString=Optional.ofNullable(nullableString);if(optionalString.isPresent()){System.out.println(optionalString.get());}其实Java8提供了更简洁的方法:OptionaloptionalString=Optional.ofNullable(nullableString);optionalString.ifPresent(System.out::println);Java从Java8开始就支持Optional类型,但它在函数式编程世界中早已为人所知。在此之前,它曾在GoogleGuava中用于对抗早期版本的Java。2.忽略异常我们经常会忽略异常。然而,对于初学者和有经验的Java程序员来说,最好的做法仍然是处理它们。异常的抛出通常是有目的的,所以在大多数情况下需要记录导致异常的事件。不要小看这一点,如果有必要,你可以重新抛出它,在对话框中向用户显示错误信息或者记录错误信息。至少,您应该解释为什么没有处理异常,以便其他开发人员了解前因后果。自拍=person.shootASelfie();try{selfie.show();}catch(NullPointerExceptione){//也许吧,隐形人。不管怎样,谁在乎呢?}强调异常不重要的一种简单方法是将此信息作为异常的变量名传递,如下所示:try{selfie.delete();}catch(NullPointerExceptionunimportant){}3.并发修改异常该异常发生在集合对象被修改时,同时没有使用迭代器对象提供的方法来移除更新集合的内容。比如这里有一个帽子列表,想删除所有包含耳罩的值:Listhats=newArrayList<>();hats.add(newUshanka());//那个有耳罩的帽子.add(newFedora());hats.add(newSombrero());对于(IHat帽子:帽子){如果(hat.hasEarFlaps()){hats.remove(帽子);}}如果您运行此代码,将抛出ConcurrentModificationException,因为代码在迭代集合时正在修改集合。当多个进程在同一个列表上工作时,并且当一个进程正在遍历列表时,另一个进程试图修改列表内容时,可能会出现相同的异常。在多线程中并发修改集合的内容是很常见的,所以需要用并发编程常用的方法来处理,比如同步锁,并发修改的专用集合等。Java在单线程和多线程情况下解决这个问题的方式略有不同。收集对象并在另一个循环中删除它们直接的解决方案是将带有耳罩的帽子放入一个列表中,然后用另一个循环将其删除。但这需要额外的一组来容纳将被删除的帽子。ListhatsToRemove=newLinkedList<>();对于(IHat帽子:帽子){如果(hat.hasEarFlaps()){hatsToRemove.add(帽子);}}for(IHathat:hatsToRemove){hats.remove(hat);}使用Iterator.remove方法,它更简单并且不需要创建额外的集合:IteratorhatIterator=hats.iterator();while(hatIterator.hasNext()){IHathat=hatIterator.下一个();如果(hat.hasEarFlaps()){hatIterator.remove();}}使用ListIterator的方法当要修改的集合实现了List接口时,列表迭代器是一个非常合适的选择。实现ListIterator接口的迭代器不仅支持删除操作,还支持添加和设置操作。ListIterator接口实现了Iterator接口,所以这个例子看起来很像Iterator的remove方法。唯一的区别是帽子迭代器的类型和我们获取迭代器的方式——使用listIterator()方法。下面的代码片段展示了如何使用ListIterator.remove和ListIterator.add方法将带耳罩的帽子替换为阔边帽。IHatsombrero=newSombrero();ListIteratorhatIterator=hats.listIterator();while(hatIterator.hasNext()){IHathat=hatIterator.next();如果(hat.hasEarFlaps()){hatIterator.remove();hatIterator.add(sombrero);使用ListIterator,调用remove和add方法可以通过仅调用一个set方法来代替:IHatsombrero=newSombrero();ListIteratorhatIterator=hats.listIterator();while(hatIterator.hasNext()){IHathat=hatIterator.next();如果(hat.hasEarFlaps()){hatIterator.set(sombrero);//set而不是remove和add}}使用Java8中的流方法在Java8中,开发人员可以将集合转换为流,并根据某些条件对流进行过滤。此示例描述流api如何过滤帽子并避免ConcurrentModificationException。hats=hats.stream().filter((hat->!hat.hasEarFlaps())).collect(Collectors.toCollection(ArrayList::new));Collectors.toCollection方法将创建一个新的ArrayList,它负责存储过滤掉的帽子值。如果过滤条件过滤出大量的item,这里会生成一个很大的ArrayList。因此,需要谨慎使用。在Java8中使用List.removeIf方法您可以在Java8中使用另一种更简洁明了的方法——removeIf方法:hats.removeIf(IHat::hasEarFlaps);在幕后,它使用Iterator.remove来执行此操作。使用一个特殊的集合如果你一开始决定使用CopyOnWriteArrayList而不是ArrayList,就不会有问题。因为CopyOnWriteArrayList提供了修改方法(如set、add、remove),所以不会改变原来的集合数组,而是创建一个新的修改版本。这允许在遍历原始版本集合时进行修改,而不会抛出ConcurrentModificationException。这个集合的缺点也很明显——每次修改都会生成一个新的集合。还有其他适合不同场景的集合,比如CopyOnWriteSet和ConcurrentHashMap。并发修改集合时可能出现的另一个错误是,从集合创建流,在遍历流时,同时修改了后端集合。流的一般准则是避免在查询流时修改后端集合。以下示例将展示如何正确处理流:ListfilteredHats=hats.stream().peek(hat->{if(hat.hasEarFlaps()){hats.remove(hat);}})。收集(收集器。toCollection(ArrayList::new));peek方法收集所有元素并对每个元素执行预定操作。在这里,操作是尝试从基础列表中删除数据,这显然是错误的。为避免此类操作,您可以尝试上面介绍的一些方法。4.违约有时,为了更好地合作,标准库或第三方提供的代码必须遵守共同的依赖准则。例如,必须遵循hashCode和equals的共同契约,以保证Java集合框架中的一系列集合类和其他使用hashCode和equals方法的类能够正常工作。不遵守合同不会引发异常或像错误一样破坏代码编译;它是阴险的,因为它可以在没有危险警告的情况下随时更改应用程序行为。错误的代码可能会潜入生产环境,造成一系列不良影响。这包括糟糕的UI体验、不正确的数据报告、糟糕的应用程序性能、数据丢失等等。值得庆幸的是,这些灾难性错误并不经常发生。hashCode和equals约定前面已经提到过,它出现的场景可能是:集合依赖于哈希或者比较对象,就像HashMap和HashSet一样。简单来说,这个合约有两个标准:如果两个对象相等,那么哈希码必须相等。如果两个对象具有相同的哈希码,则它们可能相等也可能不相等。当您尝试从哈希图中检索数据时,违反第一条约定将导致错误。第二个标准意味着具有相同哈希码的对象不一定相等。让我们看看违反第一条规则的后果:publicstaticclassBoat{privateStringname;船(字符串名称){this.name=name;}@Overridepublicbooleanequals(Objecto){if(this==o)returntrue;如果(o==null||getClass()!=o.getClass())返回false;小船小船=(小船)o;return!(name!=null?!name.equals(boat.name):boat.name!=null);}@OverridepublicinthashCode(){return(int)(Math.random()*5000);}}如您所见,Boat类重写了equals和hashCode方法。但是,它违反了契约,因为hashCode每次调用都会为同一对象返回一个随机值。尽管我们提前添加了这种类型的船,但以下代码很可能不会在哈希集中找到名为Enterprise的船:publicstaticvoidmain(String[]args){Setboats=newHashSet<>();boats.add(newBoat("企业"));System.out.printf("Wehaveaboatnamed'Enterprise':%b/n",boats.contains(newBoat("Enterprise")));}约定的另一个例子是finalize方法。这里引用Java官方文档对其功能的描述:finalize的一般约定是:当JavaTM虚拟机判断任何线程不能再以任何方式访问指定的对象时,就会调用该方法,并将该对象只能用于某些其他(准备终止)对象或类由于某些操作而最终确定。finalize方法有几个功能,包括使该对象再次可用于其他线程;然而,finalize的主要目的是在对象被不可撤销地丢弃之前执行清理操作。例如,表示输入/输出连接的对象的finalize方法可以执行显式I/O事务以在对象被永久丢弃之前终止连接。您可以决定在文件处理程序中使用finalize方法来释放资源,但这种用法非常糟糕。由于是在垃圾回收的时候调用的,而GC的时机是不确定的,所以finalize被调用的时机是无法保证的。5.使用原始类型而不是参数化类型。根据Java文档中的描述:原始类型要么是非参数化的,要么是R类(以及非继承的R父类或父接口)的非静态成员。在引入Java泛型之前,没有原始类型的替代品。Java从1.5版本开始支持泛型编程,毫无疑问这是一个重要的特性改进。但是,出于向后兼容的原因,这里有一个问题可能会破坏整个类型系统。考虑以下示例:ListlistOfNumbers=newArrayList();listOfNumbers.add(10);listOfNumbers.add("二十");listOfNumbers.forEach(n->System.out.println((int)n*2));这是定义为原始ArrayList的数字列表。由于它没有指定类型参数,因此可以向其中添加任何对象。但最后一行将其包含的元素映射为int类型并将其乘以2,将加倍后的数据打印到标准输出。这段代码编译没有错误,但一旦运行就会抛出运行时错误,因为它试图将字符类型映射到整数。显然,如果隐藏了必要的信息,类型系统无法帮助编写安全代码。为了解决这个问题,需要为集合中存储的对象指定一个具体的类型:ListlistOfNumbers=newArrayList<>();listOfNumbers.add(10);listOfNumbers.add("二十");listOfNumbers.forEach(n->System.out.println((int)n*2));与之前代码的唯一区别是定义集合的行:ListlistOfNumbers=newArrayList<>();修改后的代码编译无法通过,因为这是试图将字符串添加到只希望存储整数的集合中。编译器将显示一条错误消息,指向试图向列表中添加二十个字符的行。参数化泛型类型是个好主意。这样,编译器就可以检查所有可能的类型,因此由于类型不一致而导致运行时异常的几率会大大降低。