JavaRush /Java 博客 /Random-ZH /喝咖啡休息#146。99% 的 Java 开发人员都会犯的 5 个错误。Java 中的字符串 - 内部视图

喝咖啡休息#146。99% 的 Java 开发人员都会犯的 5 个错误。Java 中的字符串 - 内部视图

已在 Random-ZH 群组中发布

99% 的 Java 开发人员常犯的 5 个错误

来源:Medium 在这篇文章中,您将了解许多 Java 开发人员最常犯的错误。 喝咖啡休息#146。 99% 的 Java 开发人员都会犯的 5 个错误。 Java 中的字符串 - 内部视图 - 1作为一名 Java 程序员,我知道花费大量时间修复代码中的错误是多么糟糕。有时这需要几个小时。然而,许多错误的出现是由于开发人员忽略了基本规则——也就是说,这些都是非常低级的错误。今天我们将看看一些常见的编码错误,然后解释如何修复它们。我希望这可以帮助您避免日常工作中出现问题。

使用 Objects.equals 比较对象

我假设您熟悉这种方法。许多开发人员经常使用它。这项技术是在 JDK 7 中引入的,可以帮助您快速比较对象并有效避免烦人的空指针检查。但这种方法有时会被错误地使用。这就是我的意思:
Long longValue = 123L;
System.out.println(longValue==123); //true
System.out.println(Objects.equals(longValue,123)); //false
为什么用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));//2022-12-31 08:00:00
这使用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());// the size is 2
细心的读者应该能猜到失败的原因。HashSet使用哈希码来访问哈希表,并使用equals方法来判断对象是否相等。如果用户自定义对象没有重写hashcode方法和equals方法,那么默认使用父对象的hashcode方法和equals方法。这将导致HashSet假设它们是两个不同的对象,从而导致重复数据删除失败。

消除“被吃掉”的池线程

ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(()->{
            //do something
            double result = 10/0;
        });
上面的代码模拟了线程池抛出异常的场景。业务代码必须假设各种情况,因此很有可能会因为某种原因抛出RuntimeException。但如果这里没有特殊处理的话,那么这个异常就会被线程池“吃掉”。而且您甚至没有办法检查异常的原因。因此,最好在进程池中捕获异常。

Java 中的字符串 - 内部视图

来源: Medium 本文作者决定详细了解 Java 中字符串的创建、功能和特性。 喝咖啡休息#146。 99% 的 Java 开发人员都会犯的 5 个错误。 Java 中的字符串 - 内部视图 - 2

创建

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);             //true
在代码执行期间,当 JVM 遇到firstName1时,它会在内部字符串池Michael中查找该字符串值。如果找不到它,则会在内部池中为该对象创建一个新条目。当执行到firstName2时,该过程再次重复,这次可以根据firstName1变量在池中找到该值。这样,就不会重复并创建新条目,而是返回相同的链接。因此,满足平等条件。另一方面,如果使用 new 关键字创建值为Michael 的变量,则不会发生驻留并且不满足相等条件。
String firstName3 = new String("Michael");
System.out.println(firstName3 == firstName2);           //false
实习可以与firstName3 intern()方法 一起使用,尽管这通常不是首选。
firstName3 = firstName3.intern();                      //Interning
System.out.println(firstName3 == firstName2);          //true
使用+运算符 连接两个字符串文字时也可能发生实习。
String fullName = "Michael Jordan";
System.out.println(fullName == "Michael " + "Jordan");     //true
在这里我们看到,在编译时,编译器添加两个文字并从表达式中删除+运算符以形成单个字符串,如下所示。在运行时,fullName和“添加的文字”都会被保留,并且满足相等条件。
//After Compilation
System.out.println(fullName == "Michael Jordan");

平等

从上面的实验中,您可以看到默认情况下仅保留字符串文字。然而,Java 应用程序肯定不会只有字符串文字,因为它可能会从不同的源接收字符串。因此,不建议使用相等运算符,并且可能会产生不良结果。相等测试只能通过equals方法执行。它根据字符串的值而不是存储字符串的内存地址来执行相等性。
System.out.println(firstName1.equals(firstName2));       //true
System.out.println(firstName3.equals(firstName2));       //true
equals 方法还有一个稍微修改过的版本,称为equalsIgnoreCase。它对于不区分大小写的目的可能很有用。
String firstName4 = "miCHAEL";
System.out.println(firstName4.equalsIgnoreCase(firstName1));  //true

不变性

字符串是不可变的,这意味着它们的内部状态一旦创建就无法更改。您可以更改变量的值,但不能更改字符串本身的值。String类处理对象操作的每个方法(例如concatsubstring)都会返回值的新副本,而不是更新现有值。
String firstName  = "Michael";
String lastName   = "Jordan";
firstName.concat(lastName);

System.out.println(firstName);                       //Michael
System.out.println(lastName);                        //Jordan
正如您所看到的,任何变量都没有发生变化:firstNamelastName都没有发生变化。String类方法不会更改内部状态,它们会创建结果的新副本并返回结果,如下所示。
firstName = firstName.concat(lastName);

System.out.println(firstName);                      //MichaelJordan
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION