编写Java代码时应该避免的6个坑

Posted waynaqua

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编写Java代码时应该避免的6个坑相关的知识,希望对你有一定的参考价值。

通常情况下,我们都希望我们的代码是高效和兼容的,但是实际情况下代码中常常含有一些隐藏的坑,只有等出现异常时我们才会去解决它。本文是一篇比较简短的文章,列出了开发人员在编写 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-java-cd078e5aa61c

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

避免Java中NullPointerException的Java技巧和最佳实践

Java中的NullPointerException是我们最经常遇到的异常了,那我们到底应该如何在编写代码是防患于未然呢。下面我们就从几个方面来入手,解决这个棘手的?问题吧。?

值得庆幸的是,通过应用一些防御性编码技术并遵循应用程序多个部分之间的约定,您可以在一定程度上避免Java中的NullPointerException。

顺便说一下,在本文中,我们将学习一些Java的编码技术和最佳实践,这些技巧和最佳实践可用于避免的Java中的空指针异常。遵循这些Java的技巧还可以最大程度地减少很多Java代码中的 x !=NULL 检查。

作为经验丰富的Java的程序员,您可能已经知道其中一些技巧,并且已经在项目中遵循了这些技巧,但是对于新手和中级 发人员来说,这可能是个不错的学习机会。顺便说一句,如果您知道其他避免Java中的NullPointerException并减少的Java中的空检查的Java的技巧,请点击文末阅读原文与我们分享。

Java技巧和最佳实践

这些都是简单的技术,很容易遵循,但是对代码质量和健壮性有重大影响。以我的经验,仅第一个技巧就可以显着提高代码质量。如前所述,如果您知道任何其他Java技巧或最佳实践,可以帮助减少空检查,那么可以通过文末阅读原文评论本文与我们分享。

1)在已知的字符串而不是未知的对象上调用equals()和equalsIgnoreCase()方法

始终在不为null的已知字符串上调用equals()方法。由于equals()方法的方法是对称的,调用a.equals(b)与调用b.equals(a)是一样的,这就是为什么很多程序员不注意对象a和b。如果调用者为空,则此调用的一个副作用就是可能导致NullPointerException。

Object unknownObject = null;

//错误的方式 - 可能引起NullPointerException
if(unknownObject.equals("knownObject")){
   System.err.println("This may result in NullPointerException if unknownObject is null");
}

//正确的方式 - 如果unknownObject为null避免 NullPointerException
if("knownObject".equals(unknownObject)){
    System.err.println("better coding avoided NullPointerException");
}

这是避免NullPointerException的最简单的Java技巧或最佳实践,但是由于equals()是一种常见方法,因此带来了极大的改进 。

2)优先使用valueOf()而不是toString(),两个都返回相同的结果

由于在 null对象上调用toString()会引发NullPointerException ,因此,如果我们可以通过调用valueOf()获得相同的值, 则最好这样做,因为将null传递给valueOf()会返回“ null ”,特别是在诸如Integer ,Float ,Double 这样的包装类的情况下或BigDecimal 。

BigDecimal bd = getPrice();
System.out.println(String.valueOf(bd)); /不会抛出NPE
System.out.println(bd.toString()); //在main线程抛出java.lang.NullPointerException"异常

如果不确定对象是否为null,请遵循此Java技巧。

3、使用null安全的方法和库

有很多这样开源库,这些库为您检查空做了大量工作。最常见的一种来自Apache Common 的StringUtils。您可以使用StringUtils.isBlank() ,ISNUMERIC() ,isWhiteSpace()和其他实用程序方法,而不必担心NullPointerException 。

//StringUtils中的方法是空指针安全的, 它不会出现NullPointerException
System.out.println(StringUtils.isEmpty(null));
System.out.println(StringUtils.isBlank(null));
System.out.println(StringUtils.isNumeric(null));
System.out.println(StringUtils.isAllUpperCase(null));

输出结果:

true
true
false
false

但是,在使用库方法之前,请不要忘记阅读Null安全方法和类的文档。这是另一种Java最佳实践,不需要太多的时间,但可以带来很大的改进。

4、避免从方法中返回null,而应返回空集合或空数组

Joshua Bloch在他的书《Effective Java》中也提到了Java最佳实践或技巧,从这本书中你将获得更多的Java编程技巧。在公众号【Java知己】,后台回复:Effective Java,可以获得该书籍的链接。

通过返回空集合或空数组,您可以确保基本调用(如size(),length())不会因NullPointerException异常而失败。集合类提供方便的空的List, Set 和Map方法:Collections.EMPTY_LIST ,Collections.EMPTY_SET 和Collections.EMPTY_MAP ,可以相应地使用它们。这是代码示例

public List getOrders(Customer customer){
   List result = Collections.EMPTY_LIST;
   return result;
}

同样,您可以使用Collections.EMPTY_SET 和Collections.EMPTY_MAP 而不是返回null。

5、使用注释@NotNull和@Nullable

在编写方法时,可以通过使用@NotNull 和@Nullable 这样的注释来声明方法是否为null安全,从而定义有关可空性的契约。 现代的编译器,IDE或工具可以读取此批注并帮助您进行缺失的空检查,或者可以通知您不必要的空检查,这会使您的代码混乱。

IntelliJ IDE 和FindBugs的已经支持这种注释。这些注释也是JSR 305的一部分,但是即使在没有任何工具或IDE支持的情况下,此注释本身也可以作为文档使用。通过查看 @NotNull 和@Nullable ,程序员自己可以决定是否检查null。顺便说一句,对于Java程序员来说,这是相对较新的最佳实践,要花些时间才能被利用起来。

6、避免在代码中预先的自动装箱和拆箱

尽管存在其他缺点,例如创建临时对象,但如果包装类对象为null,则自动装箱也容易发生NullPointerException 。例如, 如果人员没有电话号码,则以下代码将失败,会NullPointerException ,而不是返回null 。

Person ram = new Person("ram");
int phone = ram.getPhone();

如果与自动装箱和拆箱一起使用,既也会引发NullPointerException 。

7、遵守约定并定义合理的预设值

在Java的中避免NullPointerException异常的最佳方法之一就是定义初始值并遵循约定。大多数NullPointerException异常发生的原因是使用不完整的信息创建对象或未提供所有必需的依赖关系。如果您不允许创建不完整的对象并优雅地拒绝任何此类请求,则可以防止很多NullPointerException 的出现。同样,如果 允许创建对象,则应该使用合理的替代值。例如,如果没有id 和name ,则不能创建Employee 对象 ,但是可以具有可选的电话号码。现在,如果员工没有电话号码而不是返回零,否则返回默认值,例如零,但是必须谨慎地选择该选项,踩在某些时候检查null很容易,而不是拨打无效号码。一个相同的注释,通过定义什么可以为空和什么不能为空,调用者可以做出明智的决定。选择fast-fail还是接受null也是您需要采取并坚持一致的重要设计方法。

8、如果您使用数据库来存储

客户,订单等领域对象,则应在数据库本身上定义空值约束。由于数据库可以从多个来源获取数据,因此在DB中进行空能力检查将确保数据完整性。保持数据库的空约束也将有助于减少_Java代码中的空检查_。从数据库加载对象时,您将确定其中一部分可以为null以及其中部分不为null,这将最大程度地减少代码中的的 !=null 检查。

9、使用空对象模式

这是避免Java中的NullPointerExcpetion的另一种方法。如果某个方法返回一个对象,该对象将在调用方上执行,例如Collection.iterator()方法返回Iterator,则调用方将在该迭代器上执行遍历。假设调用者没有任何继承器,则可以返回Null对象而不是null。Null对象是一个特殊的对象,在不同的其中中具有不同的含义,例如,在此处,调用hasNext()并返回false 的空Iterator 可以是null对象。类似地,对于返回容器或集合类型的方法,应使用空对象而不是返回null。我打算写一篇关于空对象模式,在这里我将分享Java中空对象的更多示例。

伙计们,这些都是容易理解的Java技巧和最佳实践,可以避免NullPointerException。您将不费吹灰之力就可以知道这些技巧有多有用。如果您要使用其他任何技巧来避免此例外(不在此列表中) ),则请通过评论与我们分享,我将在此处后续更新。


“不积跬步,无以至千里”,希望未来的你能:有梦为马 随处可栖!加油,少年!

关注公众号:「Java 知己」,每天更新Java知识哦,期待你的到来!

  • 发送「Group」,与 10 万程序员一起进步。
  • 发送「面试」,领取BATJ面试资料、面试视频攻略。
  • 发送「玩转算法」,领取《玩转算法》系列视频教程。
  • 千万不要发送「1024」...

技术图片

以上是关于编写Java代码时应该避免的6个坑的主要内容,如果未能解决你的问题,请参考以下文章

99%的Java程序员会踩的6个坑

如何学好Java?你应该躲开这几个坑

Golang 需要避免踩的 50 个坑

Java 小白成长记 · 第 6 篇「为什么说要慎用继承,优先使用组合」

我希望我的 MainActivity.java 有一个按钮,点击该按钮时应该执行用 MapsActivity.java 编写的代码

避免Java中NullPointerException的Java技巧和最佳实践