首页 > 编程语言 >有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误

有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误

时间:2024-10-14 22:48:51浏览次数:3  
标签:Java 常犯 开发人员 hats System add new hat

Java 是一种复杂的编程语言,很长一段时间以来一直主导着许多生态系统。可移植性、自动垃圾回收及其温和的学习曲线是使其成为软件开发的绝佳选择的一些因素。但是,与任何其他编程语言一样,它仍然容易受到开发人员错误的影响。

本文探讨了 Java 开发人员最常犯的 10 大错误以及避免这些错误的一些方法。

Java 是一种编程语言Oracle,但随着时间的推移,它已经广泛存在于可以使用软件的任何地方。Java 采用面向对象编程的概念设计,消除了其他语言(如 C 或 C++)、垃圾回收和架构无关的虚拟机的复杂性,创造了一种新的编程方式。此外,它的学习曲线很平缓,并且似乎成功地遵守了自己的座右铭——一次编写,到处运行,这几乎总是正确的; 但 Java 问题仍然存在。我将解决我认为是最常见错误的 10 个 Java 问题。

常见错误 #1:忽略现有库

Java 开发人员忽视无数用 Java 编写的库绝对是一个错误。在重新发明轮子之前,请尝试搜索可用的库 - 其中许多库已经在其存在的多年中得到了完善,并且可以免费使用。这些可以是日志库(如 logback 和 Log4j),也可以是网络相关库(如 NettyNIO)。一些库(如 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");
    	}
}

在大多数情况下,更简洁的解决方案是使用多态性并将具有特定行为的代码移动到单独的类中。可以使用静态代码分析器(例如 FindBugsPMD)来检测诸如此类的 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

相关文章

  • java实现 已知一颗树的层序遍历和中序遍历 输出树的先序遍历和后序遍历
    给定树的节点数,在给出这棵树的层序遍历和中序遍历输出这棵树的先序遍历和后序遍历输入735426712536471输出35246712561743importjava.util.ArrayList;importjava.util.HashMap;importjava.util.List;importjava.util.Scanner;classN......
  • java数组讲解
    前言:由上两章,我们已经了解关于java的基础语法,这章我们将讲解数组的相关语法,坐好了没,我们准备要发车啦!!!我们将从五部分给大家讲解:1数组的基本概念2.数组是引用类型3.数组的应用场景4.数组的练习5.二维数组1数组的基本概念:1.1 为什么要使用数组1.存储多个相同类型的......
  • 【油猴脚本】00027 案例 Tampermonkey油猴脚本, 仅用于学习,不要乱搞。添加标题为网页数
    前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦......
  • java计算机毕业设计OA办公自动化系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着信息技术的迅猛发展,企业办公模式正经历着深刻的变革。传统的纸质化、人工化的办公方式已难以满足现代企业高效、协同、信息化的管理需求。OA(Offi......
  • java计算机毕业设计城市天然气管理系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着城市化进程的加速,天然气作为一种清洁、高效的能源,在城市能源供应中占据了越来越重要的地位。然而,传统的天然气管理方式存在诸多不足,如信息孤岛、......
  • 今日一学,5道Java基础面试题(附Java面试题及答案整理)
    前言马上国庆了,本来想着给自己放松一下,刷刷博客,慕然回首,自动拆装箱?equals?==?HashCode?instanceof?似乎有点模糊了,那就大概看一下5道Java基础面试题吧。好记性不如烂键盘~***12万字的java面试题整理***instanceof关键字的作用instanceof严格来说是Java中的一个双目运算符,用......
  • JavaScript 代码能够成功进行树摇(Tree Shaking),代码规范
    要确保JavaScript代码能够成功进行树摇(TreeShaking),你可以遵循以下几个实践:1.使用ES6模块树摇主要依赖于ES6的模块语法(import和export)。确保你的代码使用这种模块系统,而不是CommonJS的require和module.exports。//正确的ES6模块语法exportconstfoo=()......
  • Java二维数组
    Java中的二维数组是一个存储多个一维数组的数组。它可以被看作是一个表格或者矩阵。声明一个二维数组的方法如下:dataType[][]arrayName;其中,dataType是指定数组元素类型的数据类型,arrayName是数组的名称。初始化二维数组的方法有两种:指定数组的大小,并逐个赋值:dataType......
  • Javaweb之SpringBootWeb案例之 登录功能的详细解析
     1.登录功能1.1需求编辑在登录界面中,我们可以输入用户的用户名以及密码,然后点击"登录"按钮就要请求服务器,服务端判断用户输入的用户名或者密码是否正确。如果正确,则返回成功结果,前端跳转至系统首页面。1.2接口文档我们参照接口文档来开发登录功能基本信息请求路径:/login请......
  • java中,深克隆和浅克隆怎么用,有什么应用场景?-----面试题分享
    在Java中,对象的克隆可以分为浅克隆(ShallowClone)和深克隆(DeepClone)。这两种克隆方式的主要区别在于它们如何处理对象内部的引用类型字段。浅克隆(ShallowClone)定义:浅克隆创建一个新对象,然后将原始对象中的非静态字段复制到新对象中。如果字段是基本类型,则直接复制其值;如......