和我一起学Effective Java之类和接口

类和接口

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

信息隐藏(information hiding)/封装(encapsulation):隐藏模块内部数据和其他实现细节,通过API和其他模块通信,不知道其他模块的内部工作情况。

原因:有效地解除各模块之间的耦合关系

访问控制机制(access control):决定类,接口和成员的可访问性。由声明的位置和访问修饰符共同决定。

对于顶层的类和接口,两种访问级别:

  • 包级私有的(package-private)
  • 公有的(public)

对于成员(域,方法,嵌套类,嵌套接口),四种访问级别:

  • 私有的
  • 包级私有的
  • 受保护的
  • 公有的

公有的或受保护的成员是类的导出API的一部分。代表了对某个实现细节的公开承诺。

实例域决不能是公有的。

通过公有final静态域来暴露常量。

长度非零的数组总是可变的。类具有公有的静态final数组域或者返回这种域的访问方法,这几乎是错误的。

总结:

尽可能地降低可访问性。除了公有静态final域表示常量的特殊情形外,公有类都不应该包含静态域。并且要确保公有静态final域所引用的对象都是不可变的。

在公有类中使用访问方法而非公有域

//退化类
//没有封装
class Point{
    public double x;
    public double y;
}


//退化类应该被包含私有域和公有设值方法的类代替
class Point{
    //私有域
    private double x;
    private double y;
    
    public Point(double x,double y){
        this.x = x;
        this.y = y;
    }
    //公有的getter和setter方法
    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;
    }   
}

若类是包级私有的或是私有的嵌套类,直接暴露数据域并没有本质的错误。

使可变性最小化

不可变类只是其实例不能被修改的类,实例的信息在创建的时候就提供并且固定不变。如String类就是不可变类。

使类成为不可变类的几条原则:

  • 不提供修改状态属性的方法
  • 保证类不会被扩展
  • 使所有的域都是final的
  • 使所有的域都是私有的
  • 对于任何可变组件的互斥访问

不可变对象本质上是线程安全的,不要求同步。

不可变对象可以被自由地共享。对于频繁用到的值,提供公有的静态final常量。以重用现有的实例。

不可变对象为其他对象提供了大量的构件。

注:我觉得下面一段中译版翻译有误:

原文:[76页第5段]you don't worry about their values changing once they're in the map or set,
which would destory the map or set's invariants.

译文:一旦不可变对象进入到映射(map)或者集合(set)中,尽管这破坏了映射或者集合的不变性约束,但是也不用担心它们的值会发生变化。

译文翻译得很生硬,破坏映射或集合的不变性约束指的应该是改变映射或集合的值而不是不可变对象进入映射或集合中。

修改后的译文:若不可变对象进入映射或集合,不会破坏映射或集合的不变性约束(因为它们的值不会发生变化)。

不可变类的缺点:对于每个不同的值都需要一个单独的对象。

 多步操作,每步都会产生一个新的对象。除了最后的结果之外其他对象最终都会丢弃,这就会造成性能问题。

解决:

1>某个多步操作由基本类型提供

2>使用可变的配套类

常见的配套类有StringBuilder类,它是String类的配套类。

       //String
       String str = "";
       long stringStartTime = System.currentTimeMillis();
       for(int i = 0;i<1000000;i++){
        str+=i;
       } 
       long stringEndTime = System.currentTimeMillis();
       System.out.println("String:"+(stringEndTime-stringStartTime));

       //StringBuilder
       StringBuilder sb = new StringBuilder();
       long sbStartTime = System.currentTimeMillis();
       for(int i = 0;i<1000000;i++){
         sb.append(i);
       }
       long sbEndTime = System.currentTimeMillis();
       System.out.println("StringBuilder:"+(sbEndTime-sbStartTime));

运行结果:

上面的例子就是使用String类和使用StringBuilder类执行多步操作,可以看出使用StringBuilder比String类快很多很多。

我们使用Javap -c的命令来查看字节码:

和我一起学Effective Java之类和接口_第1张图片

可以看到str+=i;实质上还是调用的StringBuilder,那照理说应该两者所花费的时间一样,为什么在这里差别这么大呢。

个人理解是因为,每次运行到str+=i;都会新创建一个String对象。
下面通过Debug调试来证明我的猜测。

下面这是String对象每次添加数字到末尾的GIF动画,可看到每次str所指向的String对象的value值都不同。
和我一起学Effective Java之类和接口_第2张图片

而下面则是StringBuilder对象使用append方法添加数字的GIF动画,可看到它的值并没有变化。
和我一起学Effective Java之类和接口_第3张图片

确保类的不可变性,类本身不能被子类化:

  • 使类成为final的
  • 类所有的构造器私有的或包级私有的,添加公有静态工厂方法来代替公有构造器。

不可变类的缺点:在特定情况下存在潜在的性能问题。

未完待续...

你可能感兴趣的:(和我一起学Effective Java之类和接口)