通常,我们都希望我们的代码是高效和兼容的,但实际上,代码中经常含有一些隐藏的坑,只有在出现异常时才能解决。本文是一篇相对简短的文章,列出了开发人员在编写 Java 程序经常犯错误,以避免在线问题。
1、大量使用 Enum.valuesEnum.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
缓存它们,轻松解决这个问题。
如下代码
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 thisLocalDate.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 zonedateTime.atZone(ZoneId.of("GMT+2"));
复制
Instant我最喜欢的。本质上是这样。 LocalDateTime,但强制使用 UTC 时区。在应用程序中需要处理时区时,最好在所有服务和数据库中使用同一时区。使用时 Instant 一切都变成了 UTC,然后读者可以根据需要将其转换为不同的时区。
// Current time in UTCInstant.now();// Note the 'Z' at the end it means UTCInstant.parse("2022-06-21T12:12:12Z");// Convert instant to a different time zoneInstant.now().atZone(ZoneId.of("GMT+3"));
复制
简单来说
- 不要使用日期和日历(或任何与) java.util 相关日期)
- 不要使用 joda.time(因为它与 java.time 非常相似)
- 如果您只对某个区域的日期感兴趣,请使用它 LocalDate
- 对某一区域的日期和时间感兴趣,请使用 LocalDateTime
- 如需日期时间且不想处理时区,请使用 Instant