Java 是一种复杂的编程语言,很长一段时间以来一直主导着许多生态系统。可移植性、自动垃圾回收及其温和的学习曲线是使其成为软件开发的绝佳选择的一些因素。但是,与任何其他编程语言一样,它仍然容易受到开发人员错误的影响。
本文探讨了 Java 开发人员最常犯的 10 大错误以及避免这些错误的一些方法。
Java 是一种编程语言Oracle,但随着时间的推移,它已经广泛存在于可以使用软件的任何地方。Java 采用面向对象编程的概念设计,消除了其他语言(如 C 或 C++)、垃圾回收和架构无关的虚拟机的复杂性,创造了一种新的编程方式。此外,它的学习曲线很平缓,并且似乎成功地遵守了自己的座右铭——一次编写,到处运行
,这几乎总是正确的; 但 Java 问题仍然存在。我将解决我认为是最常见错误的 10 个 Java 问题。
常见错误 #1:忽略现有库
Java 开发人员忽视无数用 Java 编写的库绝对是一个错误。在重新发明轮子之前,请尝试搜索可用的库 - 其中许多库已经在其存在的多年中得到了完善,并且可以免费使用。这些可以是日志库(如 logback 和 Log4j),也可以是网络相关库(如 Netty
或 NIO
)。一些库(如 Joda-Time
)已成为事实上的标准。
常见错误 #2:在 switch-case 块中缺少 ‘break’ 关键字
这些 Java 问题可能非常令人尴尬,有时直到在生产环境中运行后才会被发现。switch 语句中的透传行为通常很有用;但是,如果不希望出现此类行为,则缺少 “break” 关键字可能会导致灾难性的结果。如果你忘记在下面的代码示例中的 “case 0” 中输入一个 “break”,程序将写 “Zero” 后跟 “One”,因为这里的控制流将遍历整个 “switch” 语句,直到它到达 “break”。例如:
public static void switchCasePrimer() {
int caseIndex = 0;
switch (caseIndex) {
case 0:
System.out.println("Zero");
case 1:
System.out.println("One");
break;
case 2:
System.out.println("Two");
break;
default:
System.out.println("Default");
}
}
在大多数情况下,更简洁的解决方案是使用多态性并将具有特定行为的代码移动到单独的类中。可以使用静态代码分析器(例如 FindBugs 和 PMD)来检测诸如此类的 Java 错误。
常见错误 #3:忘记释放资源
每次程序打开文件或网络连接时,Java 初学者在使用完资源后释放资源非常重要。如果在对此类资源执行操作期间引发任何异常,则应采取类似的谨慎措施。有人可能会争辩说 FileInputStream 有一个终结器,它在垃圾回收事件上调用 close() 方法;但是,由于我们无法确定垃圾回收周期何时开始,因此 Importing 流可能会无限期地消耗计算机资源。事实上,Java 7 中专门针对这种情况引入了一个非常有用且简洁的语句,称为 try-with-resources:
private static void printFileJava7() throws IOException {
try(FileInputStream input = new FileInputStream("file.txt")) {
int data = input.read();
while(data != -1){
System.out.print((char) data);
data = input.read();
}
}
}
常见错误 #4:内存泄漏
此类内存泄漏背后的另一个潜在原因是一组对象相互引用,导致循环依赖关系,因此垃圾回收器无法决定是否需要这些具有交叉依赖关系引用的对象。另一个问题是使用 JNI 时非堆内存中的泄漏。
可能如下所示:
final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
final Deque<BigDecimal> numbers = new LinkedBlockingDeque<>();
final BigDecimal divisor = new BigDecimal(51);
scheduledExecutorService.scheduleAtFixedRate(() -> {
BigDecimal number = numbers.peekLast();
if (number != null && number.remainder(divisor).byteValue() == 0) {
System.out.println("Number: " + number);
System.out.println("Deque size: " + numbers.size());
}
}, 10, 10, TimeUnit.MILLISECONDS);
scheduledExecutorService.scheduleAtFixedRate(() -> {
numbers.add(new BigDecimal(System.currentTimeMillis()));
}, 10, 10, TimeUnit.MILLISECONDS);
try {
scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
此示例创建两个计划任务。第一个任务从名为 “numbers” 的 deque 中获取最后一个数字,并打印数字和 deque 大小,以防数字能被 51 整除。第二个任务将数字放入 deque 中。这两个任务都以固定速率计划,每 10 毫秒运行一次。如果执行了代码,您将看到 deque 的大小永久增加。这最终将导致 deque 被占用所有可用堆内存的对象填充。为了防止这种情况同时保留此程序的语义,我们可以使用不同的方法从 deque 中获取数字:“pollLast”。与方法 “peekLast” 相反,“pollLast” 返回元素并将其从双端队列中删除,而 “peekLast” 仅返回最后一个元素。
要了解有关 Java 中内存泄漏的更多信息,请参阅我们揭开此问题神秘面纱的文章。
常见错误 #5:过度的垃圾分配
当程序创建大量生存期较短的对象时,可能会发生过多的垃圾分配。垃圾回收器持续工作,从内存中删除不需要的对象,这会对应用程序的性能产生负面影响。一个简单的例子:
String oneMillionHello = "";
for (int i = 0; i < 1000000; i++) {
oneMillionHello = oneMillionHello + "Hello!";
}
System.out.println(oneMillionHello.substring(0, 6));
在 Java 开发中,字符串是不可变的。因此,在每次迭代时,都会创建一个新字符串。为了解决这个问题,我们应该使用一个可变的 StringBuilder:
StringBuilder oneMillionHelloSB = new StringBuilder();
for (int i = 0; i < 1000000; i++) {
oneMillionHelloSB.append("Hello!");
}
System.out.println(oneMillionHelloSB.toString().substring(0, 6));
虽然第一个版本需要相当长的时间来执行,但使用 StringBuilder 的版本在明显更少的时间内生成结果。
常见错误 #6:无需使用 Null 引用
避免过度使用 null 是一种很好的做法。例如,最好从方法返回空数组或集合,而不是 null,因为它可以帮助防止 NullPointerException
。
List<String> accountIds = person.getAccountIds();
for (String accountId : accountIds) {
processAccount(accountId);
}
如果 getAccountIds() 在某人没有帐户时返回 null,则将引发 NullPointerException。要解决此问题,需要进行 null 检查。但是,如果返回的不是 null,而是空列表,则 NullPointerException 不再是问题。此外,代码更简洁,因为我们不需要对变量 accountId 进行 null 检查。
为了处理想要避免 null 的其他情况,可以使用不同的策略。这些策略之一是使用 Optional 类型,它可以是空对象或某个值的包装:
Optional<String> optionalString = Optional.ofNullable(nullableString);
if(optionalString.isPresent()) {
System.out.println(optionalString.get());
}
事实上,Java 8 提供了一个更简洁的解决方案:
Optional<String> optionalString = Optional.ofNullable(nullableString);
optionalString.ifPresent(System.out::println);
常见错误 #7:忽略异常
处理异常通常很诱人。但是,对于初学者和经验丰富的 Java 开发人员来说,最佳实践是处理它们。异常是故意引发的,因此在大多数情况下,我们需要解决导致这些异常的问题。不要忽视这些事件。如有必要,您可以重新引发它,向用户显示错误对话框,或向日志添加消息。至少,应该解释为什么没有处理异常,以便让其他开发人员知道原因。
selfie = person.shootASelfie();
try {
selfie.show();
} catch (NullPointerException e) {
// Maybe, invisible man. Who cares, anyway?
}
突出显示异常无关紧要的更清晰方法是将此消息编码到异常的变量名称中,如下所示:
try { selfie.delete(); } catch (NullPointerException unimportant) { }
常见错误 #8:并发修改异常
当使用迭代器对象提供的方法以外的方法迭代集合时修改集合时,会发生此异常。例如,我们有一个帽子列表,我们想删除所有有耳罩的帽子:
List<IHat> hats = new ArrayList<>();
hats.add(new Ushanka()); // that one has ear flaps
hats.add(new Fedora());
hats.add(new Sombrero());
for (IHat hat : hats) {
if (hat.hasEarFlaps()) {
hats.remove(hat);
}
}
如果我们运行此代码,将引发 “ConcurrentModificationException”,因为代码在迭代集合时修改了集合。如果处理同一列表的多个线程之一正在尝试修改集合,而其他线程正在迭代该集合,则可能会发生相同的异常。在多个线程中并发修改集合是很自然的事情,但应该使用并发编程工具箱中的常用工具来处理,例如同步锁、为并发修改采用的特殊集合等。在单线程和多线程情况下解决此 Java 问题的方式存在细微差别。下面简要讨论了在单线程方案中可以处理此问题的一些方法:
收集对象并在另一个循环中删除它们
List<IHat> hatsToRemove = new LinkedList<>();
for (IHat hat : hats) {
if (hat.hasEarFlaps()) {
hatsToRemove.add(hat);
}
}
for (IHat hat : hatsToRemove) {
hats.remove(hat);
}
使用 Iterator.remove 方法
此方法更简洁,并且不需要创建其他集合:
Iterator<IHat> hatIterator = hats.iterator();
while (hatIterator.hasNext()) {
IHat hat = hatIterator.next();
if (hat.hasEarFlaps()) {
hatIterator.remove();
}
}
使用 ListIterator 的方法
当修改后的集合实现 List 接口时,使用 list 迭代器是合适的。实现 ListIterator 接口的迭代器不仅支持 remove 操作,还支持 add 和 set 操作。ListIterator 实现了 Iterator 接口,因此该示例看起来与 Iterator remove 方法几乎相同。唯一的区别是 hat 迭代器的类型,以及我们使用 “listIterator()” 方法获取该迭代器的方式。下面的代码段显示了如何使用 “ListIterator.remove” 和 “ListIterator.add” 方法将每顶帽子替换为带有宽边帽的耳罩:
IHat sombrero = new Sombrero();
ListIterator<IHat> hatIterator = hats.listIterator();
while (hatIterator.hasNext()) {
IHat hat = hatIterator.next();
if (hat.hasEarFlaps()) {
hatIterator.remove();
hatIterator.add(sombrero);
}
}
使用 ListIterator,remove 和 add 方法调用可以替换为对 set:
IHat sombrero = new Sombrero();
ListIterator<IHat> hatIterator = hats.listIterator();
while (hatIterator.hasNext()) {
IHat hat = hatIterator.next();
if (hat.hasEarFlaps()) {
hatIterator.set(sombrero); // set instead of remove and add
}
}
使用 Java 8 中引入的流方法在 Java 8 中,程序员能够将集合转换为流,并根据某些条件筛选该流。下面是一个 stream api 如何帮助我们过滤 hat 并避免 “ConcurrentModificationException” 的示例。
hats = hats.stream().filter((hat -> !hat.hasEarFlaps()))
.collect(Collectors.toCollection(ArrayList::new));
“Collectors.toCollection” 方法将创建一个包含过滤后帽子的新 ArrayList。如果大量项目要满足过滤条件,从而导致 ArrayList 较大,则这可能是一个问题;因此,应谨慎使用。使用 Java 8 中提供的 List.removeIf 方法Java 8 中提供的另一种解决方案,显然也是最简洁的,是使用“removeIf”方法:
hats.removeIf(IHat::hasEarFlaps);
就是这样。在后台,它使用 “Iterator.remove” 来完成该行为。
使用专用集合
还有其他针对不同情况进行调整的集合,例如 “CopyOnWriteSet” 和 “ConcurrentHashMap”。
并发集合修改的另一个可能错误是从集合创建流,并在流迭代期间修改后备集合。流的一般规则是避免在流查询期间修改基础集合。以下示例将显示处理流的错误方式:
List<IHat> filteredHats = hats.stream().peek(hat -> {
if (hat.hasEarFlaps()) {
hats.remove(hat);
}
}).collect(Collectors.toCollection(ArrayList::new));
常见错误 #9:破坏契约
有时,由标准库或第三方供应商提供的代码依赖于应遵守的规则才能使工作正常。例如,它可以是 hashCode 和 equals contract,当遵循它们时,可以保证 Java 集合框架中的一组集合以及使用 hashCode 和 equals 方法的其他类的工作。不遵守 Contract 并不是那种总是导致异常或破坏代码编译的错误;这更棘手,因为有时它会在没有任何危险迹象的情况下更改应用程序行为。错误代码可能会滑入生产版本并导致一大堆不需要的效果。这可能包括不良的 UI 行为、错误的数据报告、应用程序性能不佳、数据丢失等。幸运的是,这些灾难性的错误并不经常发生。我已经提到了 hashCode 和 equals contract。它用于依赖于哈希和比较对象的集合,如 HashMap 和 HashSet。简单地说,该合约包含两个规则:
- 如果两个对象相等,则它们的哈希码应该相等。
- 如果两个对象具有相同的哈希代码,则它们可能相等,也可能不相等。
违反合约的第一条规则会导致在尝试从 hashmap 中检索对象时出现问题。第二条规则表示具有相同哈希代码的对象不一定相等。让我们来看看打破第一条规则的影响:
public static class Boat {
private String name;
Boat(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Boat boat = (Boat) o;
return !(name != null ? !name.equals(boat.name) : boat.name != null);
}
@Override
public int hashCode() {
return (int) (Math.random() * 5000);
}
}
如您所见,类 Boat 覆盖了 equals 和 hashCode 方法。但是,它破坏了协定,因为 hashCode 每次调用时都会返回同一对象的随机值。以下代码很可能在哈希集中找不到名为 “Enterprise” 的船,尽管我们之前添加了这种船:
public static void main(String[] args) {
Set<Boat> boats = new HashSet<>();
boats.add(new Boat("Enterprise"));
System.out.printf("We have a boat named 'Enterprise' : %b\n", boats.contains(new Boat("Enterprise")));
}
Contract 的另一个示例涉及 finalize 方法。以下是官方 java 文档中描述其功能的引述:
finalize 的一般约定是,当 JavaTM 虚拟机已确定任何线程(尚未终止)都无法再访问此对象时,将调用它,除非是由于某个其他对象或类的 finalization 所执行的操作已准备好终止。finalize 方法可以执行任何操作,包括使此对象再次可供其他线程使用;但是,FINALIZE 的通常目的是在对象被不可撤销地丢弃之前执行清理操作。例如,表示 input/output 连接的对象的 finalize 方法可能会执行显式 I/O 事务,以便在永久丢弃对象之前中断连接。
常见错误 #10:使用 Raw 类型而不是参数化类型
根据 Java 规范,原始类型是未参数化的类型,或者是未从 R 的超类或超接口继承的类 R 的非静态成员。在 Java 中引入泛型之前,没有原始类型的替代品。它从 1.5 版本开始支持泛型编程,泛型无疑是一个重大的改进。但是,由于向后兼容性的原因,留下了一个可能会破坏类型系统的陷阱。让我们看一下以下示例:
List listOfNumbers = new ArrayList();
listOfNumbers.add(10);
listOfNumbers.add("Twenty");
listOfNumbers.forEach(n -> System.out.println((int) n * 2));
这里我们有一个定义为原始 ArrayList 的数字列表。由于它的 type 没有用 type 参数指定,我们可以向其中添加任何对象。但在最后一行中,我们将元素转换为 int,将其加倍,并将加倍的数字打印到标准输出。此代码将编译而不会出错,但一旦运行,它将引发运行时异常,因为我们尝试将字符串强制转换为整数。显然,如果我们向类型系统隐藏必要的信息,它就无法帮助我们编写安全的代码。要解决这个问题,我们需要指定要存储在集合中的对象类型:
List<Integer> listOfNumbers = new ArrayList<>();
listOfNumbers.add(10);
listOfNumbers.add("Twenty");
listOfNumbers.forEach(n -> System.out.println((int) n * 2));
与原始版本的唯一区别是定义集合的行:
List<Integer> listOfNumbers = new ArrayList<>();
修复的代码无法编译,因为我们尝试将字符串添加到预期仅存储整数的集合中。编译器将显示一个错误,并指向我们尝试将字符串 “Twenty” 添加到列表中的行。参数化泛型类型总是一个好主意。这样,编译器就能够进行所有可能的类型检查,并将类型系统不一致导致运行时异常的可能性降至最低。
总结
Java 作为平台简化了软件开发中的许多事情,它依赖于复杂的 JVM 和语言本身。但是,它的功能,例如删除手动内存管理或体面的 OOP 工具,并不能消除普通 Java 开发人员面临的所有问题。与往常一样,知识、实践和此类 Java 教程是避免和解决应用程序错误的最佳方法 - 因此,了解您的库、请阅读 Java、阅读 JVM 文档和编写程序尤为重要。也不要忘记静态代码分析器,因为它们可以指向实际的 bug 并突出显示潜在的 bug。
标签:Java,常犯,开发人员,hats,System,add,new,hat From: https://blog.csdn.net/qq_35971258/article/details/142931125