JavaRush /Java 博客 /Random-ZH /喝咖啡休息#56。Java 最佳实践快速指南

喝咖啡休息#56。Java 最佳实践快速指南

已在 Random-ZH 群组中发布
来源:DZone 本指南包含最佳 Java 实践和参考,以提高代码的可读性和可靠性。开发人员每天都有做出正确决策的重大责任,而能够帮助他们做出正确决策的最好的东西就是经验。尽管并非所有人都拥有丰富的软件开发经验,但每个人都可以借鉴他人的经验。我根据自己的 Java 经验为您准备了一些建议。我希望它们能帮助您提高 Java 代码的可读性和可靠性。喝咖啡休息#56。 Java 最佳实践快速指南 - 1

编程原理

不要编写只管用的代码。努力编写可维护的代码——不仅可以由您维护,还可以由将来可能最终使用该软件的任何其他人维护。开发人员 80% 的时间用于阅读代码,20% 的时间用于编写和测试代码。因此,专注于编写可读的代码。您的代码不需要任何人都可以理解它的作用的注释。要编写好的代码,我们可以使用许多编程原则作为指导。下面我将列出最重要的一些。
  • • KISS – 代表“保持简单,愚蠢”。您可能会注意到,开发人员在他们的旅程开始时尝试实现复杂、模糊的设计。
  • • DRY - “不要重复自己。” 尽量避免任何重复,而是将它们放入系统或方法的单个部分中。
  • YAGNI - “你不需要它。” 如果您突然开始问自己,“添加更多内容(功能、代码等)怎么样?”,那么您可能需要考虑是否真的值得添加它们。
  • 干净的代码而不是智能代码——简单地说,把你的自我放在门口,忘记编写智能代码。您需要干净的代码,而不是智能代码。
  • 避免过早优化——过早优化的问题是,在瓶颈出现之前,您永远不知道程序中的瓶颈在哪里。
  • 单一职责- 程序中的每个类或模块应该只关心提供特定功能的一位。
  • 组合而不是实现继承——具有复杂行为的对象应该包含具有单独行为的对象实例,而不是继承类并添加新行为。
  • 物体体操是一种编程练习,设计有9 条规则
  • 快速失败,快速停止- 该原则意味着当发生任何意外错误时停止当前操作。遵循这一原则可以使运行更加稳定。

套餐

  1. 按主题领域而不是技术水平优先构建包。
  2. 支持促进封装和信息隐藏的布局,以防止误用,而不是出于技术原因组织类。
  3. 将包视为具有不可变的 API - 不要公开仅用于内部处理的内部机制(类)。
  4. 不要公开仅在包内使用的类。

课程

静止的

  1. 不允许创建静态类。始终创建私有构造函数。
  2. 静态类应该保持不可变,不允许子类化或多线程类。
  3. 应保护静态类免受方向变化的影响,并应作为列表过滤等实用程序提供。

遗产

  1. 选择组合而不是继承。
  2. 不要设置受保护的字段。相反,请指定安全访问方法。
  3. 如果类变量可以标记为Final,请这样做。
  4. 如果不需要继承,请将类设为Final
  5. 如果不希望子类重写某个方法,则将其标记为Final 。
  6. 如果不需要构造函数,请勿创建没有实现逻辑的默认构造函数。如果没有指定,Java 会自动提供一个默认构造函数。

接口

  1. 不要使用常量接口模式,因为它允许类实现并污染 API。请改用静态类。这样做的另一个好处是允许您在静态块中进行更复杂的对象初始化(例如填充集合)。
  2. 避免过度使用界面
  3. 拥有一个且只有一个实现接口的类可能会导致接口的过度使用,并且弊大于利。
  4. “为接口编程,而不是实现”并不意味着您应该将每个域类与或多或少相同的接口捆绑在一起,这样做会破坏YAGNI
  5. 始终保持接口小而具体,以便客户只知道他们感兴趣的方法。从 SOLID 检查 ISP。

终结器

  1. #finalize()对象应谨慎使用,并且仅作为清理资源(例如关闭文件)时防止失败的一种手段。始终提供显式的清理方法(例如close())。
  2. 在继承层次结构中,始终在try块中调用父级的Finalize()。类清理应该在finally块中。
  3. 如果未调用显式清理方法并且终结器关闭了资源,请记录此错误。
  4. 如果记录器不可用,请使用线程的异常处理程序(最终会传递在日志中捕获的标准错误)。

一般规则

声明

断言通常采用前提条件检查的形式,强制执行“快速失败,快速停止”契约。它们应该被广泛使用来识别尽可能接近原因的编程错误。对象条件:
  • • 永远不应创建对象或将其置于无效状态。
  • • 在构造函数和方法中,始终使用测试来描述和执行契约。
  • • 应避免使用Java 关键字assert,因为它可能被禁用并且通常是一个脆弱的结构。
  • • 使用Assertions实用程序类可以避免前置条件检查中出现冗长的if-else条件。

泛型

Java泛型常见问题解答中提供了完整、极其详细的解释。以下是开发人员应注意的常见场景。
  1. 只要有可能,最好使用类型推断而不是返回基类/接口:

    // MySpecialObject o = MyObjectFactory.getMyObject();
    public  T getMyObject(int type) {
    return (T) factory.create(type);
    }

  2. 如果无法自动确定类型,请将其内联。

    public class MySpecialObject extends MyObject {
     public MySpecialObject() {
      super(Collections.emptyList());   // This is ugly, as we loose type
      super(Collections.EMPTY_LIST();    // This is just dumb
      // But this is beauty
      super(new ArrayList());
      super(Collections.emptyList());
     }
    }

  3. 通配符:

    当您仅从结构中获取值时,请使用扩展通配符;当您仅将值放入结构中时,请使用超级通配符;当您同时执行这两种操作时,请不要使用通配符。

    1. 每个人都喜欢PECS!(生产者扩展,消费者超级
    2. 使用Foo作为生产者 T。
    3. 使用 Foo作为消费者 T。

单例

单例永远不应该用经典的设计模式风格编写,这在 C++ 中很好,但在 Java 中不合适。即使它是正确的线程安全的,也永远不要实现以下内容(这将是性能瓶颈!):
public final class MySingleton {
  private static MySingleton instance;
  private MySingleton() {
    // singleton
  }
  public static synchronized MySingleton getInstance() {
    if (instance == null) {
      instance = new MySingleton();
    }
    return instance;
  }
}
如果确实需要延迟初始化,那么这两种方法的组合将起作用。
public final class MySingleton {
  private MySingleton() {
   // singleton
  }
  private static final class MySingletonHolder {
    static final MySingleton instance = new MySingleton();
  }
  public static MySingleton getInstance() {
    return MySingletonHolder.instance;
  }
}
Spring:默认情况下,bean 注册为单例范围,这意味着容器只会创建一个实例并连接到所有消费者。这提供了与常规单例相同的语义,没有任何性能或绑定限制。

例外情况

  1. 对可纠正的条件使用检查异常,对编程错误使用运行时异常。示例:从字符串中获取整数。

    不好:NumberFormatException 扩展了 RuntimeException,因此它旨在指示编程错误。

  2. 不要执行以下操作:

    // String str = input string
    Integer value = null;
    try {
       value = Integer.valueOf(str);
    } catch (NumberFormatException e) {
    // non-numeric string
    }
    if (value == null) {
    // handle bad string
    } else {
    // business logic
    }

    正确使用:

    // String str = input string
    // Numeric string with at least one digit and optional leading negative sign
    if ( (str != null) && str.matches("-?\\d++") ) {
       Integer value = Integer.valueOf(str);
      // business logic
    } else {
      // handle bad string
    }
  3. 您必须在正确的位置、域级别的正确位置处理异常。

    错误的方式 - 当数据库异常发生时,数据对象层不知道该怎么做。

    class UserDAO{
        public List getUsers(){
            try{
                ps = conn.prepareStatement("SELECT * from users");
                rs = ps.executeQuery();
                //return result
            }catch(Exception e){
                log.error("exception")
                return null
            }finally{
                //release resources
            }
        }}
    

    推荐方式- 数据层应该简单地重新抛出异常,并将处理或不处理异常的责任传递给正确的层。

    === RECOMMENDED WAY ===
    Data layer should just retrow the exception and transfer the responsability to handle the exception or not to the right layer.
    class UserDAO{
       public List getUsers(){
          try{
             ps = conn.prepareStatement("SELECT * from users");
             rs = ps.executeQuery();
             //return result
          }catch(Exception e){
           throw new DataLayerException(e);
          }finally{
             //release resources
          }
      }
    }

  4. 异常通常不应在发出时记录,而应在实际处理时记录。当抛出或重新抛出异常时,日志记录往往会在日志文件中充满噪音。另请注意,异常堆栈跟踪仍然记录抛出异常的位置。

  5. 支持使用标准异常。

  6. 使用异常而不是返回代码。

等于和哈希码

编写正确的对象和哈希码等效方法时需要考虑许多问题。为了使其更易于使用,请使用 java.util.Objects 的equalshash
public final class User {
 private final String firstName;
 private final String lastName;
 private final int age;
 ...
 public boolean equals(Object o) {
   if (this == o) {
     return true;
   } else if (!(o instanceof User)) {
     return false;
   }
   User user = (User) o;
   return Objects.equals(getFirstName(), user.getFirstName()) &&
    Objects.equals(getLastName(),user.getLastName()) &&
    Objects.equals(getAge(), user.getAge());
 }
 public int hashCode() {
   return Objects.hash(getFirstName(),getLastName(),getAge());
 }
}

资源管理

安全释放资源的方法:try-with-resources语句确保每个资源在语句结束时关闭。任何实现 java.lang.AutoCloseable 的对象(包括实现java.io.Closeable 的所有对象)都可以用作资源。
private doSomething() {
try (BufferedReader br = new BufferedReader(new FileReader(path)))
 try {
   // business logic
 }
}

使用关闭挂钩

使用JVM 正常关闭时调用的关闭挂钩。(但它无法处理突然中断,例如由于断电)这是推荐的替代方案,而不是声明仅在 System.runFinalizersOnExit() 为true时才会运行的Finalize()方法(默认为 false) 。
public final class SomeObject {
 var distributedLock = new ExpiringGeneralLock ("SomeObject", "shared");
 public SomeObject() {
   Runtime
     .getRuntime()
     .addShutdownHook(new Thread(new LockShutdown(distributedLock)));
 }
 /** Code may have acquired lock across servers */
 ...
 /** Safely releases the distributed lock. */
 private static final class LockShutdown implements Runnable {
   private final ExpiringGeneralLock distributedLock;
   public LockShutdown(ExpiringGeneralLock distributedLock) {
     if (distributedLock == null) {
       throw new IllegalArgumentException("ExpiringGeneralLock is null");
     }
     this.distributedLock = distributedLock;
   }
   public void run() {
     if (isLockAlive()) {
       distributedLock.release();
     }
   }
   /** @return True if the lock is acquired and has not expired yet. */
   private boolean isLockAlive() {
     return distributedLock.getExpirationTimeMillis() > System.currentTimeMillis();
   }
 }
}
通过在服务器之间分配资源,使资源变得完整(以及可更新)。(这将允许从突然中断(例如停电)中恢复。)请参阅上面使用 ExpiringGeneralLock(所有系统通用的锁)的示例代码。

约会时间

Java 8 在 java.time 包中引入了新的日期时间 API。Java 8 引入了新的 Date-Time API,以解决旧 Date-Time API 的以下缺点:非线程、设计不佳、复杂的时区处理等。

并行性

一般规则

  1. 请注意以下库,它们不是线程安全的。如果对象被多个线程使用,则始终与对象同步。
  2. 日期(不是不可变的)- 使用新的日期时间 API,它是线程安全的。
  3. SimpleDateFormat - 使用新的日期时间 API,它是线程安全的。
  4. 更喜欢使用java.util.concurrent.atomic类而不是使变量成为volatile
  5. 原子类的行为对于普通开发人员来说更为明显,而volatile则需要了解 Java 内存模型。
  6. 原子类将易失性变量包装到更方便的接口中。
  7. 了解volatility适合的用例。(见文章
  8. 使用可调用 当需要检查异常但没有返回类型时。由于 Void 无法实例化,因此它传达意图并可以安全地返回 null

  1. java.lang.Thread应该被弃用。尽管官方情况并非如此,但在几乎所有情况下,java.util.concurrent包都提供了更清晰的问题解决方案。
  2. 扩展 java.lang.Thread被认为是不好的做法 -改为实现 Runnable并在构造函数中使用实例创建一个新线程(组合规则优于继承)。
  3. 当需要并行处理时,优先选择执行器和线程。
  4. 始终建议指定您自己的自定义线程工厂来管理创建的线程的配置(更多详细信息请参见此处)。
  5. 对于非关键线程,在 Executors 中使用 DaemonThreadFactory,以便在服务器关闭时可以立即关闭线程池(更多详细信息请参见此处)。
this.executor = Executors.newCachedThreadPool((Runnable runnable) -> {
   Thread thread = Executors.defaultThreadFactory().newThread(runnable);
   thread.setDaemon(true);
   return thread;
});
  1. Java 同步不再那么慢(55-110 ns)。不要使用双重检查锁定等技巧来避免它。
  2. 更喜欢与内部对象而不是类同步,因为用户可以与您的类/实例同步。
  3. 始终以相同的顺序同步多个对象以避免死锁。
  4. 与类同步本质上不会阻止对其内部对象的访问。访问资源时始终使用相同的锁。
  5. 请记住,synchronized 关键字不被视为方法签名的一部分,因此不会被继承。
  6. 避免过度同步,这可能导致性能不佳和死锁。对于需要同步的代码部分,严格使用synchronized关键字。

收藏

  1. 尽可能在多线程代码中使用 Java-5 并行集合。它们安全且具有优良的特性。
  2. 如有必要,请使用 CopyOnWriteArrayList 而不是synchronizedList。
  3. 使用 Collections.unmodifying list(...) 或在将集合作为参数接收时将其复制到new ArrayList(list)。避免从类外部修改本地集合。
  4. 始终返回集合的副本,避免使用new ArrayList (list)从外部修改列表。
  5. 每个集合必须包装在一个单独的类中,因此现在与集合关联的行为有了一个家(例如过滤方法,对每个元素应用规则)。

各种各样的

  1. 选择 lambda 而不是匿名类。
  2. 选择方法引用而不是 lambda。
  3. 使用枚举代替 int 常量。
  4. 如果需要精确的答案,请避免使用 float 和 double,而是使用 BigDecimal,例如 Money。
  5. 选择原始类型而不是装箱原始类型。
  6. 您应该避免在代码中使用幻数。使用常数。
  7. 不要返回 Null。使用“Optional”与您的方法客户端进行通信。对于集合也是如此 - 返回空数组或集合,而不是空值。
  8. 避免创建不必要的对象,重用对象,避免不必要的GC清理。

延迟初始化

延迟初始化是一种性能优化。当数据由于某种原因被认为“昂贵”时使用它。在 Java 8 中,我们必须为此使用函数提供者接口。
== Thread safe Lazy initialization ===
public final class Lazy {
   private volatile T value;
   public T getOrCompute(Supplier supplier) {
       final T result = value; // Just one volatile read
       return result == null ? maybeCompute(supplier) : result;
   }
   private synchronized T maybeCompute(Supplier supplier) {
       if (value == null) {
           value = supplier.get();
       }
       return value;
   }
}
Lazy lazyToString= new Lazy<>()
return lazyToString.getOrCompute( () -> "(" + x + ", " + y + ")");
这就是现在的全部内容,我希望这对您有所帮助!
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION