关于java:编写Java代码时应该避免的6个坑

37次阅读

共计 4826 个字符,预计需要花费 13 分钟才能阅读完成。

通常状况下,咱们都心愿咱们的代码是高效和兼容的,然而理论状况下代码中经常含有一些暗藏的坑,只有等出现异常时咱们才会去解决它。本文是一篇比拟简短的文章,列出了开发人员在编写 Java 程序时常犯的谬误,防止线上问题。

1、大量应用 Enum.values

Enum.Values() 的问题在于,依照标准它的返回必须是一个不可变的列表。为了实现这一点,它在每次调用时返回一个带有枚举值的新数组实例。

public enum Fruits {
    APPLE, PEAR, ORANGE, BANANA;

    public static void main(String[] args) {System.out.println(Fruits.values());
        System.out.println(Fruits.values());
    }
}
// output
[Lcom.test.Fruits;@7ad041f3
[Lcom.test.Fruits;@251a69d7

它们是内存中的两个独立对象,这如同也没啥事,然而如果在解决大量申请时应用 Fruit.values() 并且机器负载很高,这可能会导致内存升高等问题。

public class Main {public static final Fruits[] values = Fruits.values();

    public static void main(String[] args) {System.out.println(values);
        System.out.println(values);
    }
}
// output
[Lcom.wayn.data.elastic.config.Fruits;@4534b60d
[Lcom.wayn.data.elastic.config.Fruits;@4534b60d

如上咱们能够通过引入公有动态最终变量 values 来缓存它们来轻松解决此问题。

2、将 Optional 作为办法参数传递

如下代码

LocalDateTime getCurrentTime(Optional<ZoneId> zoneId) {return zoneId.stream()
        .map(LocalDateTime::now)
        .findFirst()
        .orElse(LocalDateTime.now(ZoneId.systemDefault()));
}

咱们传递可选的 zoneId 参数,并依据它的存在来决定是在零碎时区中给出工夫还是应用指定的时区。然而,这不是正确应用 Optional 的形式。咱们应该防止将它们用作参数,而是应用办法重载。

LocalDateTime getCurrentTime(ZoneId zoneId) {return LocalDateTime.now(zoneId);
}

LocalDateTime getCurrentTime() {return getCurrentTime(ZoneId.systemDefault());
}

如上代码显著更易于浏览和调试。

3、应用字符拼接

Java 中的字符串是不可变的。这意味着一旦创立它们就不再可编辑。JVM 保护一个字符串池,在创立一个新字符串之前,它调用 String.intern() 办法,该办法从字符串池中返回一个与值匹配的实例(如果存在)。

假如咱们想通过连贯货色来创立一个长字符串

String longString = "";
longString +="start";
longString +="middle";
longString +="middle";
longString +="middle";
longString +="end";

不久前,咱们被告知这是一个十分蹩脚的主见,因为 Java 的旧版本执行以下操作

  • 在第 1 行中,字符串 “start” 被插入到字符串池中,longString 指向它
  • 在第 2 行中,字符串 “startmiddle” 被增加到池中,longString 指向它
  • 在第 3 行,咱们有 “startmiddlemiddle”
  • 在第 4 行 “startmiddlemiddlemiddle”
  • 最初,在第 5 行,咱们将 “startmiddlemiddlemiddleend” 增加到池中并将 longString 指向它

所有这些字符串都保留在池中并且从不应用,这会节约大量 RAM。

为了防止这种状况,咱们能够应用 StringBuilder

String longString = new StringBuilder()
  .append("start")
  .append("middle")
  .append("middle")
  .append("middle")
  .append("end")
  .toString();

调用 toString 办法时,StringBuilder 仅创立一个字符串,从而为咱们保留了最后增加到池中的所有两头字符串。然而,在 Java 5 之后,编译器会主动为咱们实现此操作,并且能够平安地应用带有 “+” 的字符串连贯。

此规定有一个例外,那就是在循环中进行字符串连贯时

String message = "";
for (int i = 0; i < 10; i++) {message += "msg" + i;}

System.out.println(message);

这段代码不会被 JIT 优化,每次迭代都会将新的字符串插入到字符串池中,这里咱们必须应用 StringBuilder

StringBuilder msgB = new StringBuilder();
for (int i = 0; i < 10; i++) {msgB.append("msg").append(i);
}

System.out.println(msgB);

这里还有几件事要留神

即时编译器有时会从新组织代码。

String s = "1" + "2" + "3";

转换成

String s = "123";

从 Java 15 开始,能够应用文本块解决多行字符串:

String sql = """
  SELECT * FROM users as u
  WHERE u.name = 'John'
  AND u.age > 34
""";

4、适度应用原始包装器

思考以下两个片段

int sum = 0;
for (int i = 0; i < 1000 * 1000; i++) {sum += i;}
System.out.println(sum);

// ----------------------

Integer sum = 0;
for (int i = 0; i < 1000 * 1000; i++) {sum += i;}
System.out.println(sum);

在我的机器上,第一个比第二个快 6 倍。惟一的区别是咱们应用包装器 Integer 类。这样做的起因是,在第 3 行中,运行时必须将 sum 变量转换为原始 int(主动拆箱),并且在执行增加后,后果将包装在一个新的 Integer 类中(主动装箱)。这意味着咱们创立了 100 万个 Integer 类并执行了 200 万个装箱操作,这解释了速度急剧下降的起因。

仅当须要将包装类存储在汇合中时才应应用包装类。然而,将来的 Java 版本将反对原始类型的汇合,这将使包装器过期。

5、本人编写哈希函数

当咱们想将对象存储在 HashMap 中时,通常会实现对象的哈希函数。该 HashMap 由带有数字的 “ 桶 ” 组成,每个哈希码都调配给一个特定的桶。如果存入 “ 桶 ” 对象的哈希函数没有正确编写,HashMap 的性能将显着升高。一个写得很好的散列函数将确保所有键的平均分配。

在个别状况下咱们须要本人编写哈希函数,但在大多数状况下,应用内置的 Objects.hash(...) 办法就行,该办法为一系列输出值生成哈希代码,生成散列代码的形式就像将所有输出值都放入一个数组中一样,并且通过调用 Arrays.hashCode(Object[]) 对该数组进行散列。

public class Car {
    private final String model;
    private final Integer year;
    private final Instant manufactureDate;

    public Car(String model, Integer year, Instant manufactureDate) {
        this.model = model;
        this.year = year;
        this.manufactureDate = manufactureDate;
    }

    @Override
    public int hashCode() {return Objects.hash(model, year, manufactureDate);
    }

    @Override
    public boolean equals(Object obj) {// 在实现 hashCode 时,不要遗记实现 equals}
}

6、应用 java.util.Date

咱们甚至应该防止 java.util 中的所有工夫类改用 java.time 包。

Date 类已被弃用,起因有很多,它有很多设计缺点。

  • 它不是无奈被批改的
  • 它无奈解决时区
  • 充斥已弃用但仍在应用的遗留代码

当程序中呈现对日期反对的需要时,util 包中的 Date、Calendar 和 rest time 类就呈现了。鉴于如上缺点,程序界有几次修复它们的尝试,但最初他们决定引入一个新的包 java.time。java.time 包与第三方的 joda.time 十分类似,这意味着咱们不须要在应用 joda.time,Jdk8 曾经有了内置反对。

咱们列出 java.time 中应用的三个最重要的类

LocalDate

示意特定时区的日期(不包含一天中的工夫)。

LocalDate.of(2022, 6, 12);
LocalDate.parse("2022-06-12");

// The Date/Time API in Java works with the ISO 8601 format by default, which is (yyyy-MM-dd)
// We can overwrite it like this
LocalDate.parse("2022.06.12", DateTimeFormatter.ofPattern("yyyy.MM.dd"));

LocalDateTime

与 LocalDate 雷同,但它有一天中的工夫。

LocalDateTime.of(2022, 6, 12, 10, 34, 18);
var dateTime = LocalDateTime.parse("2022-06-23T10:34:18");

// it's easy to get the time in a different zone
dateTime.atZone(ZoneId.of("GMT+2"));

Instant

我最喜爱的。它实质上是 LocalDateTime,但强制应用 UTC 时区。在应用程序中须要解决时区时,最好在所有服务和数据库中应用同一个时区。当应用 Instant 时,所有都变成了 UTC,而后读者能够依据须要将其转换为不同的时区。

// Current time in UTC
Instant.now();

// Note the 'Z' at the end it means UTC
Instant.parse("2022-06-21T12:12:12Z");

// Convert instant to a different time zone
Instant.now().atZone(ZoneId.of("GMT+3"));

简略来说

  • 不要应用日期和日历(或任何与 java.util 相干的日期)
  • 不要应用 joda.time(因为它与 java.time 十分类似)
  • 如果只对某个区域的日期感兴趣,请应用 LocalDate
  • 如果对某个区域的日期和工夫感兴趣,请应用 LocalDateTime
  • 如果须要日期工夫并且不想解决时区,请应用 Instant

本文翻译自国外论坛 medium,原文地址:https://medium.com/@b.stoilov/things-to-avoid-while-writing-j…

关注公众号【waynblog】每周分享技术干货、开源我的项目、实战经验、高效开发工具等,您的关注将是我的更新能源!

正文完
 0