Effective Java (第三版) 第二部分

正文

  • 优先考虑流中无副作用的函数

  • 优先使用 Collection 而不是 Stream 来作为方法的 返回类型

  • 谨慎使用流并行

  • 检查参数有效性

    • this.strategy = Objects.requireNonNull(strategy, "strategy");
      
      private static void sort(long a[], int offset, int length) {
          assert a != null;
          assert offset >= 0 && offset <= a.length;
          assert length >= 0 && length <= a.length - offset;
          ... // Do the computation
      }
      
  • 必要时进行防御性拷贝

    • this.strategy public final class Period {
          private final Date start;
          private final Date end;
          /**
           * @param  start the beginning of the period
           * @param  end the end of the period; must not precede start
           * @throws IllegalArgumentException if start is after end
           * @throws NullPointerException if start or end is null
           */
          public Period(Date start, Date end) {
              if (start.compareTo(end) > 0)
                  throw new IllegalArgumentException(
                      start + " after " + end);
              this.start = start;
              this.end   = end;
          }
          public Date start() {
              return start;
      	}
          public Date end() {
              return end;
      	}
      }
      
      Date start = new Date();
      Date end = new Date();
      Period p = new Period(start, end);
      end.setYear(78);  // Modifies internals of p!
      
      利用 Date 类是可变的这一事实很容易违反这个不变式
       
      
      
    • 保护性拷贝(防止成员变量被破坏)

      • 构造方法的防御性拷贝

        • public Period(Date start, Date end) {
              this.start = new Date(start.getTime());
              this.end   = new Date(end.getTime());
              if (this.start.compareTo(this.end) > 0)
                throw new IllegalArgumentException(
          }
          
      • get方法的防御性拷贝

        • public Date start() {
              return new Date(start.getTime());
          }
          public Date end() {
              return new Date(end.getTime());
          }
          
      • 成员变量定义成基本类型

  • 仔细设计方法签名

    • 仔细选择方法名名称

    • 不要过分地提供方便的方法

    • 避免过长的参数列表

      目标是四个或更少的参数

      • 一种方法是将方法分解为多个方法

      • 创建辅助类来保存参数组

      • 对象构造到方法调用采用 Builder 模式

    • 对于参数类型,优先选择接口而不是类

    • 与布尔型参数相比,优先使用两个元素枚举类型

  • 明智审慎地使用重载

    • 为什么返回值不同,不算重载

    • 编译的时候没有报错,有可能运行的时候报错

    • List<Interger> list = new ArrayList<>();
      
      void test(List<Interger> list, int e){
        	if(list.contines(e)){ // 自动装箱
            list.remove(e); // 移出的是Index 而不是对应的值
          }
      }
      
  • 明智审慎地使用可变参数

    • 性能问题

      • 每次调用数组分配和初始化
    • 可以使用重载来解决

  • 返回空的数组或集合,不要返回 null

    • 返回null

      • 性能优势,避免分配内存

        除非测量结果表明所讨论的分配是性能问题的真正原因,否则不宜担心此级别的性能

    • 返回空的数组或集合

      • 避免npe

      • 空集合 Collections.emptyList()

      • 认为分配零长度数组会损害性能,则可以重复返回相同的零长度数组,因为所有零长度数组都是不可变
        的:

        • private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
          public Cheese[] getCheeses() {
              return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
          }
          
  • 明智审慎地返回 Optional

  • 为所有已公开的 API 元素编写文档注释

  • 最小化局部变量的作用域

    • for 循环优于 while 循环

      • 局部变量最小号化
        Iterator<Element> i = c.iterator();
        while (i.hasNext()) {
            doSomething(i.next());
        }
        
        
  • for-each 循环优于传统 for 循环

  • 了解并使用库

    • static Random rnd = new Random();
      static int random(int n) {
          return Math.abs(rnd.nextInt()) % n;
      }
      
      • 如果 n 是小的平方数,随机数序列会在相当短的时间内重复

      • 如果 random 方法工作正常,程序将输出一个接近 50 万的数字,但是如果运行它,你将发现它输出一个接近 666666 的数字。随机方法生成的数字中有三分之二落在其范围的下半部分

        • public static void main(String[] args) {
              int n = 2 * (Integer.MAX_VALUE / 3);
              int low = 0;
              for (int i = 0; i < 1000000; i++)
                  if (random(n) < n/2)
              low++;
              System.out.println(low);
          }
          
      • Math.abs(rnd.nextInt()) 可能会得到负数(Integer.MIN_VALUE 的绝对值超过了 Integer.MAX_VALUE),然后对这个负数取模操作可能会得到负值

    • 选择的随机数生成器现在是 ThreadLocalRandom

    • List如何取交集

  • 若需要精确答案就应避免使用 float 和 double 类型

    • loat 和 double 类型主要用于科学计算和工程计算。它们执行二进制浮点运算,该算法经过精心设计,能够在很 大范围内快速提供精确的近似值。但是,它们不能提供准确的结果,也不应该在需要精确结果的地方使用。float 和 double 类型特别不适合进行货币计算,因为不可能将 0.1(或 10 的任意负次幂)精确地表示为 float 或 double

      • 例如,假设你口袋里有 1.03 美元,你消费了 42 美分。你还剩下多少钱?
        System.out.println(1.03 - 0.42);
        
        // out 0.6100000000000001
        
    • 使用int 和 long

      • 用明确下限来解决

        • 1030 -42
    • 使用 BigDecimal 有两个缺点:它与原始算术类型相比很不方便,而且速度要慢得多。如果你只解决一个简单的问题,后一种缺点是无关紧要的,但前者可能会让你烦恼

  • 基本数据类型优于包装类

    • 基本类型性能好

    • 将 == 操作符应用于包装类型几乎都是错误的。

      • int 1 ==1 true Integer 用equals

      • jdk -128-127 默认使用同一个缓存

    • 案例2

  • 当使用其他类型更合适时应避免使用字符串

    • 字符串是其他值类型的糟糕替代品

    • 字符串是枚举类型的糟糕替代品

    • 字符串是聚合类型的糟糕替代品

      • String compoundKey = className + "#" + i.next();
        这种方法有很多缺点。如果用于分隔字段的字符出现在其中一个字段中,可能会导致混乱。
        要访问各个字段,你 必须解析字符串,这是缓慢的、冗长的、容易出错的过程。
        你不能提供 equals、toString 或 compareTo 方法,但必须 接受 String 提供的行为。
        更好的方法是编写一个类来表示聚合,通常是一个私有静态成员类
        
    • 字符串不能很好地替代 capabilities

      • 如果两个客户端各自决定为它们的线程本地变量使用相同的名称,它们无意中就会共享一个变
        量,这通常会导致两个客户端都失败

      • 没有ThreadLocal之前
        =
        public class ThreadLocal {
            private ThreadLocal() { } // Noninstantiable
            // Sets the current thread's value for the named variable.
            public static void set(String key, Object value);
            // Returns the current thread's value for the named variable.
            public static Object get(String key);
        }
        
        
  • 当心字符串连接引起的性能问题

    • 要获得能接受的性能,请使用 StringBuilder 代替 String

      • public String statement() {
            String result = "";
            for (int i = 0; i < numItems(); i++)
                result += lineForItem(i); // String concatenation
            return result;
        }
        StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
        b.append(lineForItem(i));
        
    • 部分jdk是可以自动优化,并不保证优化到什么程度

  • 通过接口引用对象

    • 如果存在合适的接口类型,那么应该使用接口类型声明参数、返回值、变量和字段

      • 使用LinkedHashSet<Son> sonSet = new LinkedHashSet<>(); 而不是Set<Son> sonSet = new LinkedHashSet<>();
    • 如果没有合适的接口存在,那么用类引用对象是完全合适的

      • 考虑值类,如 String 和 BigInteger。值类很少在编写时考虑到多个实现。它们通常是 final 的,很少有相应的接口。使用这样的值类作为参数、变量、字段或返 回类型非常合适。而不是使用的接口

      • Integer i = Integer.valueof(10)

    • 如果没有合适的接口,就使用类层次结构中提供所需功能的最底层的类

  • 接口优于反射

  • 明智审慎地本地方法

    • 由于本地语言比 Java 更依赖于平台,因此使用本地方法的程序的可移植性较差。它们也更难调 试。如果不小心,本地方法可能会降低性能
  • 明智审慎地进行优化

  • 遵守被广泛认可的命名约定

    • Identifier TypeExample
      Package or moduleorg.junit.jupiter.api , com.google.common.collect
      Class or InterfaceStream, FutureTask, LinkedHashMap,HttpClient
      Method or Fieldremove, groupingBy, getCrc
      Constant FieldMIN_VALUE, NEGATIVE_INFINITY
      Local Variablei, denom, houseNum
      Type ParameterT, E, K, V, X, R, U, V, T1, T2
  • 只针对异常的情况下才使用异常

    • try {
          int i = 0;
          while ( true )
              range[i++].climb();
      } catch ( ArrayIndexOutOfBoundsException e ) {
      }
      不要利用异常终止循环
      
       for ( Mountain m : range )
          m.climb();
      
  • 对可恢复的情况使用受检异常,对编程错误使用运行时异常

  • 避免不必要的使用受检异常

  • 优先使用标准的异常

    • 不要直接重用 Exception、RuntimeException、Throwable 或者 Error

      • 对待这些类要像对待抽象类一样。你无法 可靠地测试这些异常,因为它们是一个方法可能抛出的其他异常的超类。
    • IllegalArgumentException
      IllegalStateException
      NullPointerException
      IndexOutOfBoundsExecption
      ConcurrentModificationException
      UnsupportedOperationExceptionnull 的参数值不正确 
      不适合方法调用的对象状态 
      在禁止使用 null 的情况下参数值为 null 
        下标参数值越界 
        在禁止并发修改的情况下,检测到对象的并发修改 
        对象不支持用户请求的方法
      
  • 抛出与抽象对应的异常

    • 更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法称为异常转译 (exception translation),如下代码所示:

      • /* Exception Translation */
        try {
            ... /* Use lower-level abstraction to do our bidding */
        } catch ( LowerLevelException e ) {
            throw new HigherLevelException(...);
        }
        
      • /**
         * Returns the element at the specified position in this list.
         * @throws IndexOutOfBoundsException if the index is out of range
         * ({@code index < 0 || index >= size()}).
         */
        public E get( int index ) {
            ListIterator<E> i = listIterator( index );
            try {
                return(i.next() );
            } catch ( NoSuchElementException e ) {
                throw new IndexOutOfBoundsException( "Index: " + index );
            }
        }
        
    • 异常链

      • 一种特殊的异常转译形式称为异常链 (exception chaining),如果低层的异常对于调试导致高层异常的问题非常有 帮助,使用异常链就很合适。低层的异常(原因)被传到高层的异常,高层的异常提供访问方法 (Throwable 的 getCause 方法)来获得低层的异常:

        • // Exception Chaining
          try {
          ... // Use lower-level abstraction to do our bidding
          } catch (LowerLevelException cause) {
              throw new HigherLevelException(cause);
          }
          
          /* Exception with chaining-aware constructor */
          class HigherLevelException extends Exception {
              HigherLevelException( Throwable cause ) {
                  super(cause);
          } }
          
    • 如有可能,处理来自低层异常的最好做法是,在调用低层方法之前确保它们会成功执行,从而避免它们抛出异常。有时候,可以在给低层传 递参数之前,检查更高层方法的参数的有效性,从而避免低层方法抛出异常。

    • 如果无法阻止来自低层的异常,其次的做法是,让更高层来悄悄地处理这些异常,从而将高层方法的调用者与低 层的问题隔离开来。在这种情况下,可以用某种适当的记录机制(如 java.util.logging) 将异常记录下来。这样有助于 管理员调查问题,同时又将客户端代码和最终用户与问题隔离开来。

  • 每个方法抛出的异常都需要创建文档

    • 始终要单独地声明受检异常, 并且利用 Javadoc 的@ throws 标签, 准确地记录下抛出每个异常的条件

    • 示例:

      • /**
             * If the specified key is not already associated with a value,
             * attempts to compute its value using the given mapping function
             * and enters it into this map unless {@code null}.  The entire
             * method invocation is performed atomically, so the function is
             * applied at most once per key.  Some attempted update operations
             * on this map by other threads may be blocked while computation
             * is in progress, so the computation should be short and simple,
             * and must not attempt to update any other mappings of this map.
             *
             * @param key key with which the specified value is to be associated
             * @param mappingFunction the function to compute a value
             * @return the current (existing or computed) value associated with
             *         the specified key, or null if the computed value is null
             * @throws NullPointerException if the specified key or mappingFunction
             *         is null
             * @throws IllegalStateException if the computation detectably
             *         attempts a recursive update to this map that would
             *         otherwise never complete
             * @throws RuntimeException or Error if the mappingFunction does so,
             *         in which case the mapping is left unestablished
             */
        
  • 在细节消息中包含失败一捕获信息

    • 当程序由于未被捕获的异常而失败的时’候,系统会自动地打印出该异常的堆栈轨迹。在堆栈轨迹中包含该异常的字符串表示法 (string representation),即它的 toString 方法的调用结果。它通常包含该异常的类名,紧随其后的是细节消息 (detail message)

    • 异常类型的 toString 方法应该尽可能多地返回有关失败原因的信息,这一点特别重要。换句话说,异常的字符串表示法应该捕获失败,以便于后续进行分析。

    • 为了捕获失败,异常的细节信息应该包含“对该异常有贡献”的所有参数和字段的值。 例如, IndexOutOfBoundsException 异常的细节消息应该包含下界、上界以及没有落在界内的下标值

      •  /**
         * Constructs an IndexOutOfBoundsException.
         *
         * @param lowerBound the lowest legal index value
         * @param upperBound the highest legal index value plus one
         * @param index the actual index value
         */
        public IndexOutOfBoundsException( int lowerBound, int upperBound,
                          int index ) {
            // Generate a detail message that captures the failure
            super(String.format(
                      "Lower bound: %d, Upper bound: %d, Index: %d",
                      lowerBound, upperBound, index ) );
            // Save failure information for programmatic access
            this.lowerBound = lowerBound;
            this.upperBound = upperBound;
            this.index = index;
        }
        
  • 保持失败原子性

    • 一般而言,失败的方法调用应该使对象保持在被调用之前的状态。 具有这种属性的方法被称为具有失败原子性 (failure atomic)

    • 在执行操作之前检查参数的有效性

      •  public Object pop() {
            if ( size == 0 )
                throw new EmptyStackException();
            Object result = elements[--size];
            elements[size] = null; /* Eliminate obsolete reference */
            return(result);
        }
        
  • 不要忽略异常

    • 不要这么做

      • try { ...
        } catch ( SomeException e ) {
          
        }
        
        try { ...
        } catch ( SomeException e ) {
           e.printStackTrace();
        }
        
    • 如果选择忽略异常, catch 块中应 该包含一条注释,说明为什么可以这么做,并且变量应该命名为 ignored:

  • 同步访问共享的可变数据

    • public class StopThread {
          private static Boolean stopRequested;
          public static void main(String[] args)
                  throws InterruptedException {
              Thread backgroundThread = new Thread(() -> {
                  int i = 0;
                  while (!stopRequested)
      				i++; 
              });
              backgroundThread.start();
              TimeUnit.SECONDS.sleep(1);
              stopRequested = true;
      	} 
      }
      
    • 你可能期待这个程序运行大约一秒钟左右,之后主线程将 stopRequested 设置为 true ,致使后台线程的循环终 止。但是在我的机器上,这个程序永远不会终止:因为后台线程永远在循环!

    • synchronizedvolatile 的区别

  • 避免过度同步

  • executor 、task 和 stream 优先于线程

    • 线程池的优势(面试重点)
  • 相比 wait 和 notify 优先使用并发工具

    • 并发集合(标准集合上提供的并发集合)ConcurrentHashMap

      • 比如, 应该优先使用 ConcurrentHashMap ,而不是使用 Collections.synchronizedMap
    • 同步器 CountDownLatch Semaphore CyclicBarrier Exchanger

  • 文档应包含线程安全属性

    • 不可变的 — 这个类的实例看起来是常量。不需要外部同步。示例包括 String、Long 和 BigInteger(详见第 17 条)。

    • 无条件线程安全 — 该类的实例是可变的,但是该类具有足够的内部同步,因此无需任何外部同步即可并发地使 用该类的实例。例如 AtomicLong 和 ConcurrentHashMap。

    • 有条件的线程安全 — 与无条件线程安全类似,只是有些方法需要外部同步才能安全并发使用。示例包括 Collections.synchronized 包装器返回的集合,其迭代器需要外部同步。

    • 非线程安全 — 该类的实例是可变的。要并发地使用它们,客户端必须使用外部同步来包围每个方法调用(或调 用序列)。这样的例子包括通用的集合实现,例如 ArrayList 和 HashMap。

    • 线程对立 — 即使每个方法调用都被外部同步包围,该类对于并发使用也是不安全的。线程对立通常是由于在不 同步的情况下修改静态数据而导致的。没有人故意编写线程对立类;此类通常是由于没有考虑并发性而导致 的。当发现类或方法与线程不相容时,通常将其修复或弃用。第 78 条中的 generateSerialNumber 方法在没有内 部同步的情况下是线程对立的

  • 明智审慎的使用延迟初始化

  • 不要依赖线程调度器

    • 任何依赖线程调度器(Thread.yield)来保证正确性或性能的程序都可能是不可移植的。
  • 优先选择 Java 序列化的替代方案

  • 非常谨慎地实现 Serializable

    • 一旦类的实现被发布,它就会降低更改该类实现的灵活性

    • 增加了出现 bug 和安全漏洞的可能性

    • 为继承而设计的类(详见第 19 条)很少情况适合实现 Serializable 接口,接口也很少情况适合扩展它

    • 内部类(详见第 24 条)不应该实现 Serializable。

      • 它们使用编译器生成的合成字段存储对外围实例的引用,并 存储来自外围的局部变量的值。这些字段与类定义的对应关系,就和没有指定匿名类和局部类的名称一样。因此,内 部类的默认序列化形式是不确定的。但是,静态成员类可以实现 Serializable 接口。
  • 考虑使用自定义的序列化形式

  • 保护性的编写 readObject 方法

    • 反序列化过程中,修改字节流后执行反序列化

    • 在readObject中检查
      private void readObject(ObjectInputStream s)
              throws IOException, ClassNotFoundException {
          s.defaultReadObject();
          // Check that our invariants are satisfied
          if (start.compareTo(end) > 0)
          throw new InvalidObjectException(start +" after "+ end);
      }
      
  • 对于实例控制,枚举类型优于 readResolve

    • 序列化是一种独立构造器的创建对象机制(序列化是一种以byte[]为参数的隐形构造方法)

    • 经过序列化和反序列化,不是同一个对象

      •  public class Elvis {
            public static final Elvis INSTANCE = new Elvis();
            private Elvis() { ... }
            public void leaveTheBuilding() { ... }
        }
        
    • readResolve 特性允许你用 创建的实例代替另外一个实例[Serialization, 3.7]。对于一个正在被 反序列化的对象,如果它的类定义了一个 方法,并且具备正确的声明,那么在反序列化之后,新建 对象上的 readResolve 方法就会被调用。然后,该方法返回的对象引用将会被回收,取代新建的对象。这个特性 在绝大多数用法中,指向新建对象的引用不会再被保留,因此成为垃圾回收的对象。

      • // readResolve for instance control - you can do better!
        private Object readResolve() {
            // Return the one true Elvis and let the garbage collector
            // take care of the Elvis impersonator.
            return INSTANCE;
        }
        
    • 引用对象必须加上transient

  • 考虑用序列化代理代替序列化实例

    • 序列化代理模式:降低序列化安全问题

      • 参考EnumSet

      • import java.io.*;
        
        class User implements Serializable {
            private String username;
            private transient String password; // 不希望序列化的字段
        
            public User(String username, String password) {
                this.username = username;
                this.password = password;
            }
        
            private Object writeReplace() throws ObjectStreamException {
                return new UserProxy(username);
            }
        
            private static class UserProxy implements Serializable {
                private String username;
        
                public UserProxy(String username) {
                    this.username = username;
                }
        
                private Object readResolve() throws ObjectStreamException {
                    // 根据用户名查找用户对象,这里简化为直接创建
                    return new User(username, "*****");
                }
            }
        }
        
        
    • 缺点:序列化代理模式会格外消耗性能

  • 29
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值