Java最佳实践快速参考


编程原则

不要写只能起作用的代码。旨在编写可以维护的代码-不仅可以由您自己维护,还可以由可能在将来某个时候使用该软件的其他任何人维护。

开发人员80%的时间正在阅读代码,而20%的时间在编写和测试代码。因此,请专注于编写可读代码!

您的代码无需注释即可了解其功能!

为了帮助我们开发良好的代码,有许多编程原则可以用作指导原则。在下面,我将列出最重要的那些。

KISS—它代表“保持简单,愚蠢”。您可能会注意到,开发人员在开始旅程时便尝试实现复杂的,模棱两可的设计。 DRY-“不要重复自己”。尝试避免任何重复,而是将它们放入系统或方法的单个部分中。 YAGNI -“您将不需要它”。如果遇到自问“添加额外的功能(功能,代码等)?”的情况,您可能需要重新考虑一下。 Clean code over clever code -说到干净的代码,把我的自我遗忘在门外,而不必再编写聪明的代码了。 Avoid premature optimization—过早的优化 的问题在于,直到事后,您才真正知道程序的瓶颈在哪里。 Single responsibility-程序中的每个类或模块都应只关注提供一点特定功能。 Composition over Inheritance-具有复杂行为的对象应该通过包含具有单独行为的对象实例来做到这一点,而不是继承类并添加新行为。

Object calisthenics — Object calisthenics 是编程练习,被正式定义为一组9条规则

Fail fast, fail hard - 快速失败原则表示停止当前操作,一旦发生任何意外的错误。坚持这一原则通常会带来更稳定的解决方案

Packages

  1. 支持按领域关注而不是技术层面来构造软件包。
  2. 支持布局的信息有利于封装和信息隐藏,以防止由于技术上的考虑而在组织类时使用不当。
  3. 将软件包视为提供严格的API的视图-不会暴露仅用于内部处理的内部工作方式(类)。
  4. 对于应该仅在包内部使用的类,不是公共访问范围。

Classes

Static

  1. 不允许实例化静态类。始终创建一个私有构造函数。
  2. 静态类应该是无状态的,不可变的,不允许子类化并且是线程安全的。
  3. 静态类应无副作用,并应作为实用程序提供,例如过滤列表。

Inheritance

  1. 优先考虑组成而不是继承。
  2. 不要公开受保护的字段。而是提供一个受保护的访问器。
  3. 如果可以将class变量标记为final,则将其制成。
  4. 如果不希望继承,则将类定为final。
  5. 将方法标记为final,除非期望允许子类覆盖它。
  6. 如果不需要构造函数,请不要创建没有实现逻辑的默认构造函数。如果未指定默认构造函数,则Java将自动提供默认构造函数。

    Interfaces

  7. 不要使用常量接口模式(常量的接口),因为它允许类实现和破坏API。请改用静态类。这具有允许您在静态块中执行更复杂的对象初始化(例如填充Collection)的附加好处。

  8. 避免接口过度使用
  9. 有一个且只有一个 类实现一个接口可能过度使用界面和它弊大于利。更多的
  10. “对接口进行编程,而不是对实现进行编程” 并不意味着您应该将每个域类都与一个或多或少相同的接口配对,这样做违反了YAGNI
  11. 始终保持界面小巧和特定,以便客户端只需要了解他们感兴趣的方法。从SOLID中检出ISP。

    Finalizers

  12. 应该明智地使用Object#finalize(),并且只能将其用作故障清除,以进行资源清理(例如,关闭文件)。始终提供明确的清除方法(例如close())。

  13. 在继承层次结构中,请始终在try块中调用父级的finalize()。类的清理应该在finally块中。
  14. 如果未调用显式清理方法,并且终结器关闭了资源,则记录此错误。
  15. 如果无法访问记录器,则使用线程的异常处理程序(最终将其委派给日志中捕获的标准错误)。

General

Assertions

通常以先决条件检查的形式进行的断言以快速失败,难以失败的方式强制执行类型的约定。应该自由地使用它们,以捕获尽可能靠近源代码的编程错误。

对象状态:

  • 切勿构造对象或将其转换为无效状态。
  • 在构造函数和方法上,请始终通过验证来描述和执行合同。
  • 应该避免使用Java关键字assert,因为它可以被禁用,并且通常是一种脆弱的构造。
  • 使用Assertions实用程序类可以避免冗长的if-else条件进行前提条件检查。

Generics

Java Generics FAQ中提供了完整,非常详细的解释。以下是开发人员应注意的常见方案。

如果可能,最好使用类型推断,而不是返回基类/接口:

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

2.当无法自动推断类型时,请内联它。

public class MySpecialObject extends MyObject<SpecialType> {
  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<SpecialType>());    
   super(Collections.<SpecialType>emptyList());
  }

}

3.Wildcards:

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

  1. Everyone loves PECS! (Producer-extends, Consumer-super)
  2. Use Foo<? extends T> for a T producer.
  3. Use Foo<? super T> for a T consumer.。

    Singletons 绝对不能以经典的“设计模式”样式来编写单例,这在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;
  }
}

2.如果确实需要延迟初始化,则可以将两种方法结合使用!

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;
  }
}
  1. Spring:默认情况下,bean是在单例作用域内注册的,这意味着容器将只创建一个实例并将其连接到所有使用者。这提供了与普通Singleton相同的语义,而没有性能或耦合限制。

例外情况

1.将检查的异常用于可恢复的条件,将运行时异常用于编程错误。示例:从字符串获取整数。

  • 错误:NumberFormatException扩展了RuntimeException,因此它旨在指示编程错误。
  • 不要执行以下操作:
// 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
}

2.您应该在正确的地方处理异常,正确的地方在域级别。

  • 错误的方式—发生数据库异常时,数据对象层不知道该怎么办。
class UserDAO{
    public List<User> 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<User> getUsers(){
       try{
          ps = conn.prepareStatement("SELECT * from users");
          rs = ps.executeQuery();
          //return result
       }catch(Exception e){
        throw new DataLayerException(e);
       }finally{
          //release resources
       }
   }
}

3.通常,异常不应在引发异常时记录,而应在实际处理时记录。抛出或重新抛出异常时,记录日志往往会使日志文件杂乱无章。另外,请注意,异常堆栈跟踪会捕获生成异常的位置。

4.赞成使用标准例外

5.使用例外而不是返回码。

等于和HashCode

在编写适当的对象等效项和哈希码方法时,需要注意许多问题。为了简化用法,请使用java.util.Objects的equals和hash。

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());
  }
}

资源管理

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

提供关机钩

如果JVM正常终止,则提供要调用的关闭挂钩。(这将无法处理突然的终止,例如由于断电而导致的终止)

这是推荐的替代方法,而不是声明finalize()方法,该方法仅在System.runFinalizersOnExit()为true(默认情况下为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(在系统之间共享的锁)。

Date-Time

Java 8在包java.time下引入了新的日期时间API。在Java 8中,引入了新的日期时间API,以弥补旧日期时间API的以下缺点:线程安全,设计不良,时区处理困难等。

Concurrency

General

  1. 当心以下库,这些库令人惊讶地不是线程安全的。如果在多个线程之间共享,则始终针对对象进行同步。
  2. Date(不是不可变的)—使用线程安全的新的Date-time API;
  3. SimpleDateFormat —使用线程安全的新的Date-time API;
  4. 优先使用java.util.concurrent.atomic类,而不是使变量成为volatile。
  5. 原子类的行为对于普通开发人员而言更为明显,而volatile则需要了解Java内存模型。
  6. 原子类将易失变量包装在一个更加用户友好的界面中。
  7. 了解易失性适当的用例。(请参阅文章)
  8. 当需要检查的异常但没有返回类型时,请使用Callable <Void>。由于无法实例化Void,因此可以传达意图,并且可以安全地返回null。

Threads

  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–110ns)。不要通过使用诸如双重检查锁定之类的破坏性技巧来避免这种情况。
  2. 最好与内部对象(而不是类)同步,因为用户可能会与您的类/实例同步。
  3. 始终以相同的顺序同步多个对象,以避免死锁。
  4. 与类同步并不会固有地阻止对其内部对象的访问。访问资源时,请始终使用相同的锁。
  5. 请注意,synchronized关键字不被视为方法签名的一部分,因此不会被继承。
  6. 避免过度同步,这可能导致性能降低和死锁。严格对需要同步的代码段使用synced关键字。

Collections

  1. 如果可能,请在多线程代码中使用Java-5并发集合。这些都是安全的,并具有卓越的性能。
  2. 适当时在CopyedList上使用CopyOnWriteArrayList
  3. 使用Collections.unmodifiable list(…)或在将集合作为参数new ArrayList(list)接收时复制集合。避免从班级外部更改本地收藏集。
  4. 始终返回集合的副本,避免从新的ArrayList(list)外部更改列表
  5. 每个集合都应该包装在自己的类中,因此与集合相关的行为现在有了归属(例如,过滤器方法,将规则应用于每个元素)。

Miscellaneous

  1. 更喜欢lambdas而不是匿名类
  2. 首选方法引用为lambdas
  3. 使用枚举而不是int常量。
  4. 如果需要确切的答案,请避免使用float和double,请改用BigDecimal,例如Money
  5. 优先于盒装图元的图元类型
  6. 应避免在代码中使用幻数。使用常数
  7. 不要返回Null。与Optional方法客户端通信。集合也一样—返回空数组或集合,不为空
  8. 避免创建不必要的对象,重复使用对象以及避免不必要的GC清理

延迟初始化

延迟初始化是一种性能优化。当出于某种原因数据被认为是“昂贵的”时使用。对于Java 8,我们应该为此使用Supplier功能接口。

== Thread safe Lazy initialization ===
public final class Lazy<T> {

    private volatile T value;
    public T getOrCompute(Supplier<T> supplier) {
        final T result = value; // Just one volatile read
        return result == null ? maybeCompute(supplier) : result;
    }

    private synchronized T maybeCompute(Supplier<T> supplier) {
        if (value == null) {
            value = supplier.get();
        }
        return value;
    }
}
Lazy<String> lazyToString= new Lazy<>()
return lazyToString.getOrCompute( () -> "(" + x + ", " + y + ")");

到此为止,希望它有用!


原文链接:http://codingdict.com/