99% 的 Java 开发人员常犯的 5 个错误
来源:
Medium 在这篇文章中,您将了解许多 Java 开发人员最常犯的错误。 作为一名 Java 程序员,我知道花费大量时间修复代码中的错误是多么糟糕。有时这需要几个小时。然而,许多错误的出现是由于开发人员忽略了基本规则——也就是说,这些都是非常低级的错误。今天我们将看看一些常见的编码错误,然后解释如何修复它们。我希望这可以帮助您避免日常工作中出现问题。
使用 Objects.equals 比较对象
我假设您熟悉这种方法。许多开发人员经常使用它。这项技术是在 JDK 7 中引入的,可以帮助您快速比较对象并有效避免烦人的空指针检查。但这种方法有时会被错误地使用。这就是我的意思:
Long longValue = 123L;
System.out.println(longValue==123);
System.out.println(Objects.equals(longValue,123));
为什么用
Objects.equals() 替换
==会产生错误的结果?这是因为
==编译器将获取longValue包装类型对应的底层数据类型,然后将其与该底层数据类型进行比较。这相当于编译器自动将常量转换为底层比较数据类型。使用
Objects.equals()方法后,编译器常量的默认基本数据类型为
int。
下面是Objects.equals() 的源代码,其中
a.equals(b)使用
Long.equals()并确定对象的类型。发生这种情况是因为编译器假定常量的类型为
int,因此比较结果必定为 false。
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}
知道了原因,修复错误就非常简单了。只需声明常量的数据类型,例如
Objects.equals(longValue,123L)。如果逻辑严格的话,就不会出现上述问题。我们需要做的是遵循明确的编程规则。
日期格式不正确
在日常开发中,经常需要更改日期,但是很多人使用了错误的格式,从而导致出现意想不到的事情。这是一个例子:
Instant instant = Instant.parse("2021-12-31T00:00:00.00Z");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
System.out.println(formatter.format(instant));
这使用
YYYY-MM-dd格式将日期从 2021 年更改为 2022 年。你不应该这样做。为什么?这是因为
Java DateTimeFormatter “YYYY”模式基于 ISO-8601 标准,该标准将年份定义为每周的星期四。但 2021 年 12 月 31 日是星期五,因此该程序错误地指示了 2022 年。
为了避免这种情况,您必须使用yyyy-MM-dd格式来格式化日期。这种错误很少发生,只有新年到来时才会出现。但在我的公司却导致了生产失败。
在ThreadPool中使用ThreadLocal
如果创建
一个 ThreadLocal 变量,那么访问该变量的线程将创建一个线程局部变量。这样就可以避免线程安全问题。但是,如果您在
线程池上使用ThreadLocal,则需要小心。您的代码可能会产生意外的结果。举个简单的例子,假设我们有一个电子商务平台,用户需要发送电子邮件来确认已完成购买产品。
private ThreadLocal<User> currentUser = ThreadLocal.withInitial(() -> null);
private ExecutorService executorService = Executors.newFixedThreadPool(4);
public void executor() {
executorService.submit(()->{
User user = currentUser.get();
Integer userId = user.getId();
sendEmail(userId);
});
}
如果我们使用
ThreadLocal来保存用户信息,就会出现隐藏的错误。因为使用了线程池,并且线程可以复用,所以使用
ThreadLocal获取用户信息时,可能会错误显示别人的信息。为了解决这个问题,你应该使用会话。
使用HashSet去除重复数据
在编码的时候,我们经常会有去重的需求。当您想到重复数据删除时,许多人首先想到的是使用
HashSet。然而,不小心使用
HashSet可能会导致重复数据删除失败。
User user1 = new User();
user1.setUsername("test");
User user2 = new User();
user2.setUsername("test");
List<User> users = Arrays.asList(user1, user2);
HashSet<User> sets = new HashSet<>(users);
System.out.println(sets.size());
细心的读者应该能猜到失败的原因。
HashSet使用哈希码来访问哈希表,并使用equals方法来判断对象是否相等。如果用户自定义对象没有重写hashcode方法和
equals方法,那么默认使用父对象的hashcode方法和
equals方法。这将导致
HashSet假设它们是两个不同的对象,从而导致重复数据删除失败。
消除“被吃掉”的池线程
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(()->{
double result = 10/0;
});
上面的代码模拟了线程池抛出异常的场景。业务代码必须假设各种情况,因此很有可能会因为某种原因抛出
RuntimeException。但如果这里没有特殊处理的话,那么这个异常就会被线程池“吃掉”。而且您甚至没有办法检查异常的原因。因此,最好在进程池中捕获异常。
Java 中的字符串 - 内部视图
来源:
Medium 本文作者决定详细了解 Java 中字符串的创建、功能和特性。
创建
Java 中的字符串可以通过两种不同的方式创建:隐式(作为字符串文字)和显式(使用
new关键字)创建。字符串文字是用双引号括起来的字符。
String literal = "Michael Jordan";
String object = new String("Michael Jordan");
尽管这两个声明都创建了一个字符串对象,但这两个对象在堆内存上的定位方式有所不同。
内部代表
以前,字符串以char[]形式存储,这意味着每个字符都是字符数组中的单独元素。
由于它们以UTF-16字符编码格式表示,这意味着每个字符占用两个字节的内存。这不是很正确,因为使用统计显示大多数字符串对象仅包含
Latin-1字符。Latin-1 字符可以使用单个字节的内存来表示,这可以显着减少内存使用量 - 减少多达 50%。作为 JDK 9 版本的一部分,基于
JEP 254实现了一个新的内部字符串功能,称为紧凑字符串。在此版本中,
char[]更改为
byte[],并添加了编码器标志字段来表示所使用的编码(Latin-1 或 UTF-16)。之后,根据字符串的内容进行编码。如果值仅包含 Latin-1 字符,则使用 Latin-1 编码(
StringLatin1类)或使用 UTF-16 编码(
StringUTF16类)。
内存分配
如前所述,在堆上为这些对象分配内存的方式有所不同。使用显式 new 关键字非常简单,因为 JVM 在堆上为变量创建并分配内存。因此,使用字符串文字需要遵循一个称为实习的过程。字符串驻留是将字符串放入池中的过程。它使用一种仅存储每个单独字符串值的一个副本的方法,该副本必须是不可变的。各个值存储在 String Intern 池中。该池是一个
哈希表存储,它存储对使用文字及其哈希创建的每个字符串对象的引用。虽然字符串值在堆上,但它的引用可以在内部池中找到。使用下面的实验可以轻松验证这一点。这里我们有两个具有相同值的变量:
String firstName1 = "Michael";
String firstName2 = "Michael";
System.out.println(firstName1 == firstName2);
在代码执行期间,当 JVM 遇到
firstName1时,它会在内部字符串池Michael中查找该字符串值。如果找不到它,则会在内部池中为该对象创建一个新条目。当执行到
firstName2时,该过程再次重复,这次可以根据firstName1变量在池中找到该值。这样,就不会重复并创建新条目,而是返回相同的链接。因此,满足平等条件。另一方面,如果使用 new 关键字创建值为
Michael 的变量,则不会发生驻留并且不满足相等条件。
String firstName3 = new String("Michael");
System.out.println(firstName3 == firstName2);
实习可以与firstName3 intern()方法 一起使用,尽管这通常不是首选。
firstName3 = firstName3.intern();
System.out.println(firstName3 == firstName2);
使用+运算符 连接两个字符串文字时也可能发生实习。
String fullName = "Michael Jordan";
System.out.println(fullName == "Michael " + "Jordan");
在这里我们看到,在编译时,编译器添加两个文字并从表达式中删除
+运算符以形成单个字符串,如下所示。在运行时,
fullName和“添加的文字”都会被保留,并且满足相等条件。
System.out.println(fullName == "Michael Jordan");
平等
从上面的实验中,您可以看到默认情况下仅保留字符串文字。然而,Java 应用程序肯定不会只有字符串文字,因为它可能会从不同的源接收字符串。因此,不建议使用相等运算符,并且可能会产生不良结果。相等测试只能通过
equals方法执行。它根据字符串的值而不是存储字符串的内存地址来执行相等性。
System.out.println(firstName1.equals(firstName2));
System.out.println(firstName3.equals(firstName2));
equals 方法还有一个稍微修改过的版本,称为
equalsIgnoreCase。它对于不区分大小写的目的可能很有用。
String firstName4 = "miCHAEL";
System.out.println(firstName4.equalsIgnoreCase(firstName1));
不变性
字符串是不可变的,这意味着它们的内部状态一旦创建就无法更改。您可以更改变量的值,但不能更改字符串本身的值。
String类处理对象操作的每个方法(例如
concat、
substring)都会返回值的新副本,而不是更新现有值。
String firstName = "Michael";
String lastName = "Jordan";
firstName.concat(lastName);
System.out.println(firstName);
System.out.println(lastName);
正如您所看到的,任何变量都没有发生变化:
firstName和
lastName都没有发生变化。
String类方法不会更改内部状态,它们会创建结果的新副本并返回结果,如下所示。
firstName = firstName.concat(lastName);
System.out.println(firstName);
GO TO FULL VERSION