美文网首页
Effective Java英文第三版读书笔记(1) -- 科学

Effective Java英文第三版读书笔记(1) -- 科学

作者: FinJmy | 来源:发表于2018-09-18 17:37 被阅读0次

    写在前面

    《Effective Java》原书国内的翻译只出版到第二版,书籍的编写日期距今已有十年之久。这期间,Java已经更新换代好几次,有些实践经验已经不再适用。去年底,作者结合Java7、8、9的最新特性,编著了第三版(参考https://blog.csdn.net/u014717036/article/details/80588806)。当前只有英文版本,可以在互联网搜索到PDF原书。本读书笔记都是基于原书的理解。


    以下是正文部分

    如何科学地创建和销毁对象(Creating and Destroying Objects)

    实践1 抛弃构造函数,使用静态工厂方法

    什么是静态工厂方法(static factory method)

    简单讲,它就是一个返回当前对象实例的静态方法。示例如下:

    public static Boolean valueOf(boolean b) { 
      return b ? Boolean.TRUE : Boolean.FALSE;
    }
    

    1.1 优点

    1. 构造函数都以类名命名,区分度不高,而静态工厂方法可以个性化,对用户更加友好。
    2. 静态工厂方法不是必须重新创建一个对象,例如上面 Boolean 的代码中,返回的是早前已经创建好的对象。这类似于设计模式中的享元模式(Flyweight pattern),典型的,相同内容的String以及Enum就用了该模式。
    3. 静态工厂方法可以返回子类。Java8中,取消了接口不能包含static方法的限制,因此在接口上实现这种静态工厂方法类,简化了文档,用户也只需关注主类。
    4. 静态工厂方法可以根据参数而返回不同的内容。如下示例,根据参数返回不同的类,屏蔽了一些内部细节。用户只需要知道返回的是EnumSet或其子类即可,哪怕以后EnumSet进一步细分,代码也几乎不需要重构。
    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");
        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }
    
    1. 静态工厂方法返回的对象类型,甚至可以在当前位置不存在。 这在SPI中用处较多。例如JDBC服务里面,java.sql.Driver接口是对外公开的一个加载驱动接口,但Jdk中并没有相关实现,实际是由各sql厂商拿到接口后做的实现。

    1.2 不足

    1. 没有public或者protected构造函数的类是无法被继承的。
    2. 在接口中,静态工厂方法不如构造函数显眼,使用者难以发现。

    1.3 最佳实践

    Date d = Date.from(instant);
    Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
    BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
    StackWalker luke = StackWalker.getInstance(options);
    Object newArray = Array.newInstance(classObject, arrayLen);
    FileStore fs = Files.getFileStore(path);
    BufferedReader br = Files.newBufferedReader(path);
    List<Complaint> litany = Collections.list(legacyLitany);
    

    实践2 当构造函数包含过多参数时,使用builder

    在实际的业务开发中,某些类可能包含丰富多样的属性。例如一个网站用户可能包含用户名、密码、昵称、头像、手机、证件、邮箱、ID、公司名等等信息,有些是必选参数,有些是可选参数。当前端送来一个用户注册请求时,则需要创建一个用户对象。这样,可能根据参数和用户类型,需要N个复杂的构造函数。下面列出几种解决方案:

    2.1 Telescoping Constructor 模式

    难读、难用。

    public class Account {
      private final String name;
      private final String password;
      private final String phone;
      private final String email;
    
      public Account(String name, String password) {
        this(name, password, null);
      }
    
      public Account(String name, String password, String phone) {
        this(name, password, phone, null);
      }
    
      public Account(String name, String password, String phone, String email) {
        this.name = name;
        this.password = password;
        this.phone = phone;
        this.email = email;
      }
    }
    

    2.2 JavaBean 模式

    把对象初始化拆分成了几条语句,代码层面上更加清晰,阅读顺畅。但是相应的缺点是这种操作是非原子性的,在并发编程中,需要专门为此做保护。

    public class Account {
      private String name;
      private String password;
      private String phone;
      private String email;
    
      public Account() {}
    
      public String getName() { return name; }
      public String getPassword() { return password; }
      public String getPhone() { return phone; }
      public String getEmail() { return email; }
      public void setName(String name) { this.name = name; }
      public void setPassword(String password) {  this.password = password; }
      public void setPhone(String phone) {  this.phone = phone; }
      public void setEmail(String email) {  this.email = email;
      }
    

    2.3 Builder 模式

    结合了前两种方式的优点,同时安全性和可继承性得以保证。但是这种方式也有缺陷。首先,创建真正的对象前需要创建Builder对象,增大了系统开销。在参数量少时,不宜过度使用该模式。

    public class Account {
      private String name;
      private String password;
      private String phone;
      private String email;
    
      private Account(Builder builder) {
        this.name = builder.name;
        this.password = builder.password;
        this.phone = builder.phone;
        this.email = builder.email;
      }
    
      public static class Builder {
        private String name;
        private String password;
        private String phone = null;
        private String email = null;
    
        public Builder(String val1, String val2) {
          name = val1;
          password = val2;
        }
    
        public Builder phone(String val) {
          phone = val;
          return this;
        }
    
        public Builder email(String val) {
          email = val;
          return this;
        }
    
        public Account build() {
          return new Account(this);
        }
      }
    }
    //调用方式
    Account account = new Account.Builder("Amy", "123456").phone("15199998888").email("a@163.com").build();
    

    实践3 使用私有构造函数或枚举类型来强制单例

    无状态的对象通常采用单例模式。通常,有两种方式来实现单例。这两种方式都是通过私有构造函数+公有的静态实例成员实现的。

    3.1 单例实现A

    没有公有构造函数确保了只有 INSTANCE 在初始化时调用私有构造函数一次,之后,不能再创建该对象的任何实例。该方式的一个缺点是可能遭受反射攻击 ,参考:AccessibleObject.setAccessible

    // Singleton with public final field
    public class Elvis { 
      public static final Elvis INSTANCE = new Elvis();
      private Elvis() { ... }
      public void leaveTheBuilding() { ... } 
    }
    

    3.2 单例实现B

    使用了静态工厂方法,通过getInstance去获取实例。相对来说,该方式更加明晰。并且,如果以后需要改造为非单例,对于用户代码没有影响。

    // Singleton with public final field
    public class Elvis { 
      private static final Elvis INSTANCE = new Elvis();
      private Elvis() { ... }
      public static Elvis getInstance() { return INSTANCE; }
      public void leaveTheBuilding() { ... } 
    }
    

    3.3 Enum实现单例

    虽然看起来不太自然,但常常是实现单例的最佳方式。

    // Enum singleton - the preferred approach 
    public enum Elvis { 
      INSTANCE;
      public void leaveTheBuilding() { ... } 
    }
    

    实践4 使用私有构造函数来限制实例化

    有时,我们编写的对象只包含一组静态的方法和变量,这样的对象是无需实例化的。但是在Java中,编译器始终会采用默认构造函数的策略。为了避免这种情况有以下2种方法:

    4.1 抽象类

    引入abstract关键字,使得类类型为抽象类是无法实例化的。但是这种方式有个缺点是如果有其他类继承该抽象类,则继承类是可以实例化的。并且抽象类容易迷惑用户,用户会认为需要继承这个类,而不是直接使用。

    4.2 添加私有构造函数

    编译器只在没有显式构造函数时为类添加默认构造函数。只要我们在类中显式添加一个私有构造函数,则该类就没法实例化了。示例如下,AssertionError并不是必须的,它只是确保没有在类内部误调用。
    该方式也有缺点:无法继承。由于子类的构造函数总会(隐式或显式地)调用父类的构造函数,当父类构造函数为private时,将无法完成该动作。

    // Noninstantiable utility class
    public class UtilityClass { 
      // Suppress default constructor for noninstantiability 
      private UtilityClass() { throw new AssertionError(); } 
      ... // Remainder omitted 
    }
    

    实践5 使用依赖注入代替硬编码资源

    类与类之间通常都存在依赖关系。下面是两种如何添加这种依赖关系的反面示例。这两种方式下,对于多线程,多实例及参数化资源都没法很好支持。

    // Inappropriate use of static utility - inflexible & untestable! 
    public class SpellChecker { 
      private static final Lexicon dictionary = ...;
      private SpellChecker() {} // Noninstantiable
      public static boolean isValid(String word) { ... } 
      public static List<String> suggestions(String typo) { ... } 
    }
    
    // Inappropriate use of singleton - inflexible & untestable! 
    public class SpellChecker { 
      private final Lexicon dictionary = ...;
      private SpellChecker(...) {} 
      public static INSTANCE = new SpellChecker(...);
      public boolean isValid(String word) { ... } 
      public List<String> suggestions(String typo) { ... }
     }
    

    一种较好的解决方案是,在类的构造函数中传入相关资源。这就是依赖注入:在对象创建时注入。该方式在静态工厂方法,Builder模式同样适用。当工程过大时,某个类可能依赖成百上千资源,这时就需要注入框架来帮忙了,例如Dagger, Guice, Spring等。

    // Dependency injection provides flexibility and testability 
    public class SpellChecker { 
      private final Lexicon dictionary;
      public SpellChecker(Lexicon dictionary) { 
        this.dictionary = Objects.requireNonNull(dictionary); 
      }
      public boolean isValid(String word) { ... } 
      public List<String> suggestions(String typo) { ... } 
    }
    

    实践6 不要创建不必要的实例

    对于不可修改的对象,采用共享模式而不是每次新建。有助于提升程序的性能。

    6.1 示例1

    String对象的新建,此处字符串值是固定不变的。

    // 每次调用都将创建一个新的对象,浪费
    String s = new String("bikini");
    // 享元模式
    String s = "bikini";
    

    6.2 示例2

    一个正则匹配模式的例子。

    static boolean isRomanNumeral(String s) { 
      return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"); 
    }
    

    此处正则表达式^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$是固定不变的,但上述实现中,每次都会用它新建一个Pattern对象,因此可以把 Pattern 抽取出来。原书作者实测性能提升6倍多。

    On my machine, the original versiontakes 1.1 µs on an 8-character input string, while the improved version takes 0.17 µs, which is 6.5 times faster.

    public class RomanNumerals {
      private static final Pattern ROMAN = Pattern.compile( "^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
      static boolean isRomanNumeral(String s) { 
        return ROMAN.matcher(s).matches(); 
      }
    }
    

    此处有个可能的争议是,如果isRomanNumeral方法从未被调用到,那么ROMAN的初始化是浪费的。原书作者认为:虽然可以通过懒加载的方式来进一步避免该问题,但是增加了代码的复杂性,且性能实际提升价值不大。

    6.3 基础类型的使用

    对于基础类型,应尽量使用 int, long 而不是 Integer, Long。后者可能触发不必要的大量对象创建。

    private static long sum() {
      // 创建大量对象
      Long sum = 0L; 
      // 创建1个对象
      long sum = 0L;
      for (long i = 0; i <= Integer.MAX_VALUE; i++) sum += i;
      return sum; 
    }
    

    实践7 解决过时引用问题

    Java的自动垃圾回收机制,使得程序员可能产生幻觉:不需要进行内存管理。然而事实并非如此。如下就是一个内存管理不当的示例,在这个场景下,随着栈的增长,elements 可能扩张到很大,但是元素pop()size减小,但是elements 并未联动减小,那些没有被垃圾回收掉的比size标号大的对象成为了过时引用(obsolete reference,意思是再也不会用到的引用)。

    // Can you spot the "memory leak"?
    public class Stack {
      private static final int DEFAULT_INITIAL_CAPACITY = 16;
      private Object[] elements;
      private int size = 0;
    
      public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
      }
    
      public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
      }
    
      public Object pop() {
        if (size == 0) throw new EmptyStackException();
        return elements[--size];
      }
      /**
       * * Ensure space for at least one more element, roughly * doubling the capacity each time the
       * array needs to grow.
       */
      private void ensureCapacity() {
        if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1);
      }
    }
    

    7.1 解决方案

    相对来说,具有垃圾回收机制的编程语言的内存管理问题潜伏的更深,不易察觉,最终影响程序的性能。修复示例程序中这种类型的问题很简单:将引用设置为null。这样不仅使得垃圾能够尽快回收掉,并且使得随后的引用变得更安全,错误引用将触发NullPointerException

    public Object pop() {
      if (size == 0) throw new EmptyStackException();
      Object result = elements[--size];
      elements[size] = null; // Eliminate obsolete reference
      return result;
    }
    

    需要注意的是,不要过度使用这个方法,它会使编程变得复杂繁琐。通常只有在程序员自行管理内存的时候,才需要这个手段。对于其他情况,在Java中,对象的生命周期通常是在一定范围内,例如 {}中,跳出范围,则自动销毁。因此,将对象定义在最小化使用范围中是一种较好的编程习惯。

    7.2 高发场景:

    • 缓存,原书作者建议使用WeakHashMap来解决。
    • 监听与回调,如果向API注册了回调函数而忘记去注册会有问题。同样,原书作者建议使用WeakHashMap来解决。

    实践8 避免使用finalizercleaner

    (实际开发中未使用,指导意义不大,暂未阅读)

    实践9 try-with-resources优于try-finally

    对于资源,在Java程序中使用完之后需要进行关闭动作。例如文件流、socket连接等等。如果我们忽视了,则可能给程序带来不良后果,尽管这些资源有finalizer来收尾。

    9.1 try-finally方式

    一种常见的方式是try-finally来确保资源能够在正常/异常情况下也正确关闭。但是当一段try-finally要使用多个资源时,嵌套后的代码看起来会非常复杂。另外,该方式还有一个问题时,如果IO设备故障,那么 read 以及 close操作都会抛出异常,但是因为close在后,所以之前的异常被冲掉,调试时只看到最后一个异常,增大调试难度。

    // try-finally is ugly when used with more than one resource!
    static void copy(String src, String dst) throws IOException {
      InputStream in = new FileInputStream(src);
      try {
        OutputStream out = new FileOutputStream(dst);
        try {
          byte[] buf = new byte[BUFFER_SIZE];
          int n;
          while ((n = in.read(buf)) >= 0) out.write(buf, 0, n);
        } finally {
          out.close();
        }
      } finally {
        in.close();
      }
    }
    

    9.1 try-with-resources方式

    Java7引入的try-with-resources,规定了资源对象必须实现AutoCloseable接口,这个接口中仅包含了一个方法:void close()

    public class MyFile implements AutoCloseable{
      @Override
      public void close() throws Exception {...}
    }
    

    try-with-resources的调用语法如下。在这种语法下,当 read 以及 close操作都抛出异常时,close的异常被抑制掉,以确保程序员看到想要的那个异常。并且,整个代码也更加简洁。

    static String firstLineOfFile(String path) throws IOException {
      try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
      }
    }
    
    static void copy(String src, String dst) throws IOException {
      try (InputStream in = new FileInputStream(src);
          OutputStream out = new FileOutputStream(dst)) {
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) >= 0) out.write(buf, 0, n);
      }
    }
    

    (完)

    相关文章

      网友评论

          本文标题:Effective Java英文第三版读书笔记(1) -- 科学

          本文链接:https://www.haomeiwen.com/subject/qylwgftx.html