• Effective Java 3


    Item 15 使类和成员的可访问性最小化

    1、一个设计的好的组件应该隐藏它的所有实现细节,清晰地将它的 API 以及具体实现分开。

    2、尽可能的使用低级别的访问级别,降低类和成员的可访问性。

    3、如果一个包私有的顶层类或接口只被一个类使用,考虑将它变为私有的静态内部类。

    3、设计类的时候首先设计你所有的公有 API,然后应该反射性的将其他剩余的类设为private,只有当它真的需要提高访问级别时,才将它提升为包私有的。

    4、protected的类是API的一部分,需要始终保证对它的支持,protected的级别在实际使用中是较少的。

    5、如果一个方法覆盖了超类的一个方法,不能使它的访问级别比超类中的低。

    6、在调试程序时,可以适当提高某些方法的访问性。

    7、public类的实体域通常是非public的。

    8、一个类拥有public static final array 是错误的,作为替代选择以下方法:

    private static final Thing[] PRIVATE_VALUES = { ... };
    public static final List<Thing> VALUES =Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
    private static final Thing[] PRIVATE_VALUES = { ... };
    public static final Thing[] values() {
        return PRIVATE_VALUES.clone();
    }

    9、在Java中,public不在是最高访问级别,如果它不在导出包中,它将是模块私有的。

    10、public类不应该有public域,除非是充当常量的public static final域。

    Item 16 在public类中 使用访问方法而不是直接使用public域

    // Encapsulation of data by accessor methods and mutators
    class Point {
        
        private double x;
        private double y;
       
         public Point(double x, double y) {
            this.x = x;
            this.y = y;
    }
    public double getX() { return x; }
    public double getY() { return y; }
    public void setX(double x) { this.x = x; }
    public void setY(double y) { this.y = y; }
    }    

    1、如果是包私有的或是静态内部类或是不可变对象则不会有什么大问题。

    2、总之、public类不要暴露可变对象,对于不可变对象是否可以暴露存疑,

     

    Item 17 最小化可变性

    1、为了确保类的不可变需要保证以下规则:

      1)不要提供能改变类的状态的方法。

      2)确保类不能被继承。

      3)使所有域都是final的。

      4)使所有域都是private的。

      5)确保对于任何可变组件的互斥访问。

    2、例子:

     1 // Immutable complex number class
     2 public final class Complex {
     3     private final double re;
     4     private final double im;
     5     public Complex(double re, double im) {
     6         this.re = re;
     7         this.im = im;
     8     }
     9 
    10     public double realPart() { return re; }
    11     public double imaginaryPart() { return im; }
    12 
    13     public Complex plus(Complex c) {
    14         return new Complex(re + c.re, im + c.im);
    15     }
    16 
    17     public Complex minus(Complex c) {
    18         return new Complex(re - c.re, im - c.im);
    19     }
    20 
    21     public Complex times(Complex c) {
    22         return new Complex(re * c.re - im * c.im,re * c.im + im * c.re);
    23     }
    24 
    25     public Complex dividedBy(Complex c) {
    26         double tmp = c.re * c.re + c.im * c.im;
    27         return new Complex((re * c.re + im * c.im) / tmp,(im * c.re - re * c.im) / tmp);
    28     }
    29 
    30     @Override public boolean equals(Object o) {
    31         if (o == this)
    32             return true;
    33         if (!(o instanceof Complex))
    34             return false;
    35         Complex c = (Complex) o;
    36 // See page 47 to find out why we use compare instead of ==
    37         return Double.compare(c.re, re) == 0&&Double.compare(c.im, im) == 0;
    38     }
    39 
    40     @Override public int hashCode() {
    41         return 31 * Double.hashCode(re) + Double.hashCode(im);
    42     }
    43 
    44     @Override public String toString() {
    45         return "(" + re + " + " + im + "i)";
    46     }
    47 }

    这里函数返回了一个新的对象,而不改变原有的操作数,称为函数式编程。而过程式或是命令式编程则会改变操作数的状态。

    3、一个不可变类的状态,就是它被刚创建时的类。

    4、不可变类是线程安全的,不需要同步。

    5、可以尽可能的自由的去复用不可变类。

    6、使用静态工厂使客户复用对象而不是创建一个新的。

    7、可以通过方法return一个不可变类的内部状态进行复用。

    8、不可变类的主要缺点,在于创建对象的开销。

    9、如果一个类的方法的多步操作是基本类型的,那么在中间步骤实际是不需要每次都创建一个新的对象的。

    10、使一个类不能被继承,可以使类的构造器private并添加public静态工厂:

    // Immutable class with static factories instead of constructors
    public class Complex {
        private final double re;
        private final double im;
        private Complex(double re, double im) {
        this.re = re;
        this.im = im;
        }
        public static Complex valueOf(double re, double im) {
            return new Complex(re, im);
        }
    ... // Remainder unchanged
    }

    11、实际中亦可适当放松限制以提高性能。

    12、对于一些常用的数据进行缓存。

    13、如果一个包含可变类的不可变类需要实现Serializable,则必须提供readObject或者readResolve方法,否则会被攻击者创建一个可变版本。

    14、不要一看到getter就反射性的写setter。

    15、小类尽可能是不可变的的,相对的大类是可变的比较好。

    16、尽可能使所有的域private final,除非有恰当的理由。

    Item 18 比起继承更支持复合

    1、跨包继承是一件危险的事。

    2、与调用方法不同。继承破坏了封装性。

    3、例子:

    // Broken - Inappropriate use of inheritance!
    public class InstrumentedHashSet<E> extends HashSet<E> {
    // The number of attempted element insertions
        private int addCount = 0;
        public InstrumentedHashSet() {
        }
        public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
        }
        @Override public boolean add(E e) {
        addCount++;
        return super.add(e);
        }
        @Override public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
        }
        public int getAddCount() {
        return addCount;
        }
    }

    错误在于 addAll方法在内部实现是基于add的。子类的addAll计算了一次count,然后调用超类addAll,再去调用add,但是add方法已经在子类被覆盖,其中又计算了一次。一次addAll方法对一个元素会计算两次。

    4、另一个子类脆弱的原因是,超类如果添加了一个新的方法,如果子类不进行相应的覆盖可能会造成严重的安全隐患。

    5、如果进行继承不进行任何覆盖,只是添加新的方法情况可能会没有那么糟,但是仍有可能与将来版本超类中的新添加的函数相同而引发冲突。以及其他问题。

    6、使用复合。在新的类中添加一个私有域,引用现有类的一个实例。新类中的每个方法都可以调用被包含的现有类实力中对应的方法,并返回它的结果。这种方法称为转发。这样会使类很稳固。

     1 // Wrapper class - uses composition in place of inheritance
     2 public class InstrumentedSet<E> extends ForwardingSet<E> {
     3     private int addCount = 0;
     4     public InstrumentedSet(Set<E> s) {
     5       super(s);
     6     }
     7     @Override public boolean add(E e) {
     8       addCount++;
     9       return super.add(e);
    10     }
    11     @Override public boolean addAll(Collection<? extends E> c) {
    12       addCount += c.size();
    13       return super.addAll(c);
    14     }
    15     public int getAddCount() {
    16       return addCount;
    17     }
    18 }
    19 
    20 // Reusable forwarding class
    21 public class ForwardingSet<E> implements Set<E> {
    22     private final Set<E> s;
    23     public ForwardingSet(Set<E> s) { this.s = s; }
    24     public void clear() { s.clear(); }
    25     public boolean contains(Object o) { return s.contains(o); }
    26     public boolean isEmpty() { return s.isEmpty(); }
    27     public int size() { return s.size(); }
    28     public Iterator<E> iterator() { return s.iterator(); }
    29     public boolean add(E e) { return s.add(e); }
    30     public boolean remove(Object o) { return s.remove(o); }
    31     public boolean containsAll(Collection<?> c){ return s.containsAll(c); }
    32     public boolean addAll(Collection<? extends E> c){ return s.addAll(c); }
    33     public boolean removeAll(Collection<?> c){ return s.removeAll(c); }
    34     public boolean retainAll(Collection<?> c){ return s.retainAll(c); }
    35     public Object[] toArray() { return s.toArray(); }
    36     public <T> T[] toArray(T[] a) { return s.toArray(a); }
    37     @Override public boolean equals(Object o){ return s.equals(o); }
    38     @Override public int hashCode() { return s.hashCode(); }
    39     @Override public String toString() { return s.toString(); }
    40 }

    这里通过转发技术,都由私有域s进行处理,因此不会出现上述的问题。

    7、包装技术的缺点在于不支持回调框架。

    8、每次使用继承的时候都要慎重地再次确认是否是 “is-a”的关系,是否真的是子类型。

    Item 19 设计被用于继承的类时需要恰当的文档说明

    1、一个类必须通过文档说明其中会被覆盖的 自使用 方法。(也就是一个方法的实现依赖于同个类中的其他方法)。

    2、惯例使用 Implementation Requirement 来说明方法的内部工作原理。

    3、构造器中不能调用能够被覆盖的方法。否则会导致程序出错。

    4、如果设计的将要被继承的类实现了 Cloneable 或者 Serializable。clone 和 readObject不能调用能被覆盖的方法。

    5、如果设计的将要被继承的类实现了Serializable,必须把readResolve 或者writeReplace 设为protected而不是private。

    6、禁止继承的两种方法一是使类final 二是构造器私有化并提供静态工厂进行代替。

    7、为了使更好的子类化,需要一个或多个protected方法。

    Item 20 优先考虑接口而不是抽象类

    1、两者最主要的区别在于,子类只能继承一个父类,却能实现多个接口。

    2、接口是理想的定义混入的办法,比如Comparable 就被广泛应用提供标准的比较功能。

    3、接口不同于继承的严格关系,可以定义非层次结构的框架。

    4、通过包装器,接口可以提供安全强力的功能增加。

    5、考虑在接口中以默认方法的形式提供实现帮助。

    6、不允许对Object的方法提供默认方法。

    7、结合接口和抽象类的优点可以提供一个 抽象骨架实现类

    8、接口定义类型、提供默认方法。 骨架实现类实现 剩下的基于基本接口方法的 非基本方法。(模版方法模式)

    9、管理上对骨架实现类命名为 AbstractInterface 其中Interface为接口名。

    10、一个例子,一个静态工厂方法包含一个 基于 AbstractList 的完整的List实现:

     1 // Concrete implementation built atop skeletal implementation
     2 static List<Integer> intArrayAsList(int[] a) {
     3   Objects.requireNonNull(a);
     4   // The diamond operator is only legal here in Java 9 and later
     5   // If you're using an earlier release, specify <Integer>
     6   return new AbstractList<>() {
     7     @Override public Integer get(int i) {
     8       return a[i]; // Autoboxing (Item 6)
     9     }
    10     @Override public Integer set(int i, Integer val) {
    11       int oldVal = a[i];
    12       a[i] = val; // Auto-unboxing
    13       return oldVal; // Autoboxing
    14     }
    15     @Override public int size() {
    16       return a.length;
    17     }
    18   };
    19 }

    11、即使因为某些原因一个类不能对抽象骨架实现类进行继承,它也总可以对原接口进行实现。

    12、实现了接口的类 可以将 接口方法的调用 传递给 它包含的 继承了骨架实现的 私有内部类的实例。这种技术也被称为模拟多重继承。

    13、写一个抽象骨架实现类的步骤:

      1)、认真研究接口,决定哪些方法是基本的,而其他方法基于这些基本方法实现。这些基本方法将是 骨架实现类中的抽象方法。

      2)、对接口中所有可以被实现的其他方法提供默认方法不要忘记继承自Object的方法是例外。

      3)、如果基本方法和默认方法包括了这个接口的一切,则就不需要编写骨架实现类。

      4)、否则写一个骨架实现类实现找个接口,这个接口包含了一切剩余方法的实现,这个类可能包括任何合适的非公有域和方法。

    例子:

    // Skeletal implementation class
    public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {
    // Entries in a modifiable map must override this method
        @Override public V setValue(V value) {throw new UnsupportedOperationException();}
    
            // Implements the general contract of Map.Entry.equals
        @Override public boolean equals(Object o) {
            if (o == this)
                return true;
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry) o;
            return Objects.equals(e.getKey(), getKey())&& Objects.equals(e.getValue(), getValue());
        }
    
        // Implements the general contract of Map.Entry.hashCode
        @Override public int hashCode() {
            return Objects.hashCode(getKey())^ Objects.hashCode(getValue());
        }
        @Override public String toString() {
            return getKey() + "=" + getValue();
        }
    }                    

    14、 因为骨架实现类用于继承,因此如上文所述需要一个好的文档说明。

    15、他一个变化是简单实现,不过他不是抽象的而是该接口最简单的一种实现。

    Item 21 为了后续实现设计接口

    1、默认方法的声明包括一个默认实现,所有实现这个接口,但不实现这个默认方法的类都将使用这个默认方法。

    2、许多被加入核心借口的默认方法,都是为了帮助lambdas表达式的使用。

    3、并不总是可能添加一个默认方法并保持,已经实现这个接口的类的内部的关系不变。

    4、例子:

     1 // Default method added to the Collection interface in Java 8
     2 default boolean removeIf(Predicate<? super E> filter) {
     3     Objects.requireNonNull(filter);
     4     boolean result = false;
     5     for (Iterator<E> it = iterator(); it.hasNext(); ) {
     6         if (filter.test(it.next())) {
     7             it.remove();
     8            result = true;
     9         }
    10     }
    11     return result;
    12 }

    5、现阶段添加默认方法,实现这个接口的类可能仍然可以正常编译,但运行时可能发生错误。

    6、 设计默认方法不支持用在接口中移除方法或改变现有方法的签名。

    7、除非非常必要,否则不要在已经设计完并被使用的接口中添加默认方法,这可能会使实现类产生不可预知的错误,尽量只在新设计的接口中使用这一技术。

    Item 22 使用接口只用于定义类型

    1、一个常见的错误例子:

    1 // Constant interface antipattern - do not use!
    2 public interface PhysicalConstants {
    3 
    4     static final double AVOGADROS_NUMBER = 6.022_140_857e23;
    5     static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
    6     static final double ELECTRON_MASS = 9.109_383_56e-31;  //_相当于,并不对数值有影响,只是更有可读性。2
    7 }

    2、正确的方式:如果该数值与类或接口的关系很紧密,直接放入;如果最好被视为枚举类型,应当视为枚举类型。或者也可以使用工具类:

    // Constant utility class
    package com.effectivejava.science;
    public class PhysicalConstants {
       
        private PhysicalConstants() { } // Prevents instantiation
    
        public static final double AVOGADROS_NUMBER =6.022_140_857e23;
        public static final double BOLTZMANN_CONST = 1.380_648_52e-23;
        public static final double ELECTRON_MASS = 9.109_383_56e-31;
    }

    3、如果经常使用这个数值,如上方式的调用可能非常繁琐,需要输入一长串字符,便于化简可以使用静态导入功能:

    1 // Use of static import to avoid qualifying constants
    2 import static com.effectivejava.science.PhysicalConstants.*;
    3 public class Test {
    4     double atoms(double mols) {
    5         return AVOGADROS_NUMBER * mols;
    6     }
    7 ...
    8 // Many more uses of PhysicalConstants justify static import
    9 }

     
    Item 23 类层次优先于标签类

    1、标签类:

     1 // Tagged class - vastly inferior to a class hierarchy!
     2 class Figure {
     3   enum Shape { RECTANGLE, CIRCLE };
     4 // Tag field - the shape of this figure
     5   final Shape shape;
     6 // These fields are used only if shape is RECTANGLE
     7   double length;
     8   double width;
     9 // This field is used only if shape is CIRCLE
    10   double radius;
    11 // Constructor for circle
    12   Figure(double radius) {
    13     shape = Shape.CIRCLE;
    14     this.radius = radius;
    15   }
    16 // Constructor for rectangle
    17   Figure(double length, double width) {
    18     shape = Shape.RECTANGLE;
    19     this.length = length;
    20     this.width = width;
    21   }
    22   double area() {
    23   switch(shape) {
    24     case RECTANGLE:
    25       return length * width;
    26     case CIRCLE:
    27       return Math.PI * (radius * radius);
    28     default:
    29       throw new AssertionError(shape);
    30     }
    31   }
    32 }

    缺点有许多:多重实现都挤在一个类中;内存增加了许多无用的东西;冗长的;易错;效率低;只是类层次的拙劣的模仿。

    2、将标签类转换为类层次:

      1)定义一个抽象类,包含所有在标签类中行为依赖标签值的方法,往往包含switch,例如上例中的area().

      2) 将所有不依赖于标签值的方法和数值放入这个类。这个类即是根。

      3)定义实体子类,新增各自的实现方法,以及实现父类中的抽象类,以对应不同标签的实现。

    // Class hierarchy replacement for a tagged class
    abstract class Figure {
        abstract double area();
    }
    
    class Circle extends Figure {
        final double radius;
        Circle(double radius) { this.radius = radius; }
        @Override double area() { return Math.PI * (radius * radius); }
    }
    
    class Rectangle extends Figure {
        final double length;
        final double width;
        Rectangle(double length, double width) {
           this.length = length;
            this.width = width;
        }
        @Override double area() { return length * width; }
    }

    Item 24 相较于非静态优先使用静态成员类

    1、一个嵌套类应该只被设计用语服务它的外围类。

    2、共有四种类型的嵌套类:静态成员类、非静态成员类、匿名类和局部类。前三种统称为内部类。

    3、静态成员类的一种常见用法是作为公有的辅助类,用于帮助它的外部类。

    4、非静态成员类隐含着一个外围类对象的实例 this。正是通过它才能访问外围类中的一切。

    5、静态内部类的实例可以不依附于外围类的实例独立存在,而非静态则不行。

    6、非静态成员类的一种常见用法是适配器:

     1 // Typical use of a nonstatic member class
     2 public class MySet<E> extends AbstractSet<E> {
     3 ... // Bulk of the class omitted
     4     @Override public Iterator<E> iterator() {
     5         return new MyIterator();
     6     }
     7     private class MyIterator implements Iterator<E> {
     8 ...
     9     }
    10 }

    7、如果成员类不需要访问它的外部对象,那它就应该是静态的。

    8、private静态成员类的一种常见用法是代表外围类所代表的对象的组件。

    9、匿名类并不是外围类的一个成员。

    10、除了常量,匿名类不能拥有静态成员。

    11、只有在外围类非静态时,匿名类才能拥有外围类的实例。

    12、不能声明匿名类实现多个接口或者继承一个类并同时实现接口。

    13、匿名类的客户端无法调用任何成员除非是从其超类继承获得。

    14、匿名类可以用于实现静态工厂。

    15、如今比起匿名类,lambdas表达式可能更好。

    16、局部类可以在任何允许局部变量存在的地方被声明。

    17、局部类有名字可以复用, 只有外围类非静态时可以获得外围类的实例,不能包含静态成员,尽可能保持短。

    18、如果一个嵌套类不止被外围类的一个方法所使用,应该使它成为成员类。如果每个成员类实例都需要其外围对象的引用,那应该是非静态;否则静态。如果只有一个地方需要创建实例,并且已经预先定义了类型,使它成为匿名类;否则作为局部类。

    Item 25 限制每个资源文件(.java)只拥有一个顶层类。

    1、如果不这样做会因为编译的顺序不同而产生不同的结果。

    2、如果需要使用多个类,考虑使用静态成员类。

  • 相关阅读:
    利用锚点制作简单索引效果
    BOM之location对象
    引入CSS
    对象继承
    indexOf、instanceOf、typeOf、valueOf详解
    JSON详解
    浏览器兼容性-JS篇
    gcc堆栈排列的建议(译文)
    VLAN 学习
    DPDK KNI 接口2
  • 原文地址:https://www.cnblogs.com/WutingjiaWill/p/9188809.html
Copyright © 2020-2023  润新知