类和接口

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

信息隐藏(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的命令来查看字节码:

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

个人理解是因为,每次运行到str+=i;都会新创建一个String对象。

下面通过Debug调试来证明我的猜测。

下面这是String对象每次添加数字到末尾的GIF动画,可看到每次str所指向的String对象的value值都不同。

而下面则是StringBuilder对象使用append方法添加数字的GIF动画,可看到它的值并没有变化。


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

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

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

复合优于继承

继承打破了封装性(子类依赖父类中特定功能的实现细节)

合理的使用继承的情况:

  • 在包内使用
  • 父类专门为继承为设计,并且有很好的文档说明

只有当子类真正是父类的子类型时,才适合用继承。

对于两个类A和B,只有两者之间存在"is-a"关系,类B才能拓展类A。

继承机制会把父类API中的所有缺陷传播到子类中,而复合允许设计新的API来隐藏这些缺陷。

复合(composition):不扩展现有的类,而是在新的类中增加一个私有域,引用现有类的一个实例。

转发(fowarding):新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回结果。

public class FowardSet<E> implements Set<E> {

    //引用现有类的实例
private final Set<E> set; public FowardSet(Set<E> set){
this.set = set;
} /*
*转发方法
*/
@Override
public int size() {
return set.size();
} @Override
public boolean isEmpty() {
return set.isEmpty();
} @Override
public boolean contains(Object o) {
return set.contains(o);
} @NotNull
@Override
public Iterator<E> iterator() {
return set.iterator();
} @NotNull
@Override
public Object[] toArray() {
return set.toArray();
} @NotNull
@Override
public <T> T[] toArray(T[] a) {
return set.toArray(a);
} @Override
public boolean add(E e) {
return set.add(e);
} @Override
public boolean remove(Object o) {
return set.remove(o);
} @Override
public boolean containsAll(Collection<?> c) {
return set.containsAll(c);
} @Override
public boolean addAll(Collection<? extends E> c) {
return set.addAll(c);
} @Override
public boolean retainAll(Collection<?> c) {
return set.retainAll(c);
} @Override
public boolean removeAll(Collection<?> c) {
return set.removeAll(c);
} @Override
public void clear() {
set.clear();
} @Override
public boolean equals(Object obj) {
return set.equals(obj);
} @Override
public String toString() {
return set.toString();
} @Override
public int hashCode() {
return set.hashCode();
}
} /*
* 包装类(wrapper class),采用装饰者模式
*/
public class InstrumentedSet<E> extends FowardSet<E> {
private int addCount=0; public InstrumentedSet(Set<E> set) {
super(set);
} @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;
}
}

上面的例子中,FowardingSet是转发类,也是被包装类,而InstrumentedSet是包装类,它采用的是装饰者模式,而不是委托模式。

装饰者模式

委托模式

为什么包装类不适合用在回调框架中?

包装类不适合用在回调框架(callback framework)中,会出现SELF问题

在回调框架中,对象把自身的引用传递给其他对象,用于后续的调用(回调)

SELF问题:被包装的对象并不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时却避开了外面的包装对象。

接口优于抽象类

两种机制用来定义允许多个实现的类型:接口和抽象类

区别:

  • 现有的类可以很容易被更新,以实现新的接口。
  • 接口是定义混合类型的理想选择。
  • 接口允许我们构造非层次结构的类型框架。

抽象的骨架实现类(skelletal implementation class):把接口和抽象类的优点结合起来。接口负责定义类型,骨架实现类接管所有与接口实现相关的工作。

Java Collections中为每个重要的集合接口都提供了一个骨架实现。如AbstractCollection,AbstractSet,AbstractList,AbstractMap。

设计得当,骨架实现可以让程序员很容易就提供我们自己的接口实现。

模拟多重继承:实现了接口的类可以把对于接口方法的调用转发到一个内部私有类的实例上

抽象类的演变比接口的演变容易多了:在后续的发行版本中,在抽象类中增加新的方法,始终可以增加具体的方法,它包含合理的默认实现。该抽象类的所有现有的实现都将提供这个新的方法。对于接口,这样做不行。

接口是定义允许多个实现的类型的最佳途径。当是否容易演变比灵活性和功能更为重要的时候,应该使用抽象类。

接口只用于定义类型

不应该使用接口来定义常量,可以使用枚举类型或者是不可实例化的工具类来定义常量。

/**
* 使用接口定义常量,不推荐使用
* 细节的实现,会被泄露到导出到API中
* 并且代表一种承诺,类为保证二进制兼容性,需要一直实现接口即使它不再需要使用这些常量了
*/
public interface MathConstants { static final double PI = 3.14159265; } /**
* 使用枚举类型定义常量
*/
public enum MathConstantsWithEnum {
PI(3.14159265);
private double value;
private MathConstantsWithEnum(double value){
this.value = value;
}
} /**
* 使用不可实例化的帮助类来定义常量
*/
public class MathConstantsUtil {
private MathConstantsUtil(){}
public static final double PI = 3.14159265;
} import static com.xyz.johntsai.effectivejava.MathConstantsUtil.*;
/**
* 静态导入机制,避免用类名来修饰常量名
*/
public class TestStaticImport {
public static double getArea(double r){
return PI*r*r;
}
}

类层次优于标签类

/**
* 标签类
* 缺点:过于冗长,易错,效率低下,不易拓展
*/
public class Figure {
enum Shape{ RECTANGLE,CIRCLE
} final Shape shape; //用于三角形
double width;
double length; //用于圆形
double radius; //三角形的构造器
Figure(double length,double width){
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
} Figure(double radius){
shape = Shape.CIRCLE;
this.radius =radius;
} double getArea(){
switch (shape){
case RECTANGLE:
return this.length*this.width;
case CIRCLE:
return Math.PI*this.radius*this.radius;
default:
throw new AssertionError();
}
}
}
/**
* 类层次代替标签类
*/ /**
*Figure类是类层次的根
*/
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 width;
final double length; Rectangle(int width,int length){
this.length = length;
this.width = width;
} @Override
double area() {
return width*length;
}
}

用函数对象表示策略

策略模式

Strategy pattern

        String [] array = {"a","aa"};
//匿名内部类
Arrays.sort(array, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length()-o2.length();
}
}); //JDK1.8 支持lambda
Arrays.sort(array,(s1,s2)->s1.length()-s2.length()); //多次重复使用 用私有的静态成员类实现,并通过静态final成员导出
Arrays.sort(array,Host.STRING_COMPARATOR); class Host{
private static class StrLengthComparator implements Comparator<String>,Serializable{ @Override
public int compare(String o1, String o2) {
return o1.length()-o2.length();
}
} public static final Comparator<String> STRING_COMPARATOR = new StrLengthComparator();
}

优先考虑静态成员类

什么时候使用嵌套类,局部类,匿名类和lambda表达式?

局部类:定义在代码块中(大括号里面有0个或多个语句),一般定义在方法内部

public class LocalClassExample {

    static String regularExpression = "[^0-9]";

    public static void validatePhoneNumber(String phoneNumber1,String phoneNumber2){

        final int length = 10;

        //局部类(Local class)
class PhoneNumber{
String formattedPhoneNumber = null;
PhoneNumber(String phoneNumber){
String currentNumber = phoneNumber.replaceAll(regularExpression,"");
formattedPhoneNumber = currentNumber.length()==length?currentNumber:null;
}
public String getNumber(){
return formattedPhoneNumber;
}
} PhoneNumber number1 = new PhoneNumber(phoneNumber1);
PhoneNumber number2 = new PhoneNumber(phoneNumber2); if(number1.getNumber()==null)
System.out.println("First number is invalid");
else
System.out.println("First number is"+number1.getNumber());
if(number2.getNumber()==null)
System.out.println("Second number is invalid");
else
System.out.println("Second number is"+number2.getNumber());
} public static void main(String[] args) {
validatePhoneNumber("1234567890","456-7890");
}
}

和我一起学Effective Java之类和接口的更多相关文章

  1. 和我一起学Effective Java之创建和销毁对象

    前言 主要学习创建和销毁对象: 1.何时以及如何创建对象 2.何时以及如何避免创建对象 3.如何确保它们能够适时地销毁 4.如何管理对象销毁之前必须进行的清理动作 正文 一.用静态工厂方法代替构造器 ...

  2. 和我一起学Effective Java之泛型

    泛型 不要在新代码中使用原始类型 泛型(generic):声明中具有一个或多个类型参数 原始类型(raw type):不带任何实际类型参数的泛型名称 格式: 类或接口的名称 < 对应于泛型形式类 ...

  3. <<Effective Java>>之Comparable接口的实现约定

    对于BigDecimal类在HashSet和TreeSet中 new BigDecimal("1.00") new BigDecimal("1.0") 在Has ...

  4. EFFECTIVE JAVA 类和接口

    第十六条:复合优先于继承 //这是一个不好的类---执行的结果 addCount = 4(addAll的实现依赖于HashSet的add方法,InstrumentHashSet方法重写了add方法有执 ...

  5. Effective Java 第三版——14.考虑实现Comparable接口

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  6. Effective Java 第三版—— 20. 接口优于抽象类

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  7. Effective Java 第三版——21. 为后代设计接口

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  8. Effective Java 第三版——22. 接口仅用来定义类型

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  9. Effective Java 第三版——38. 使用接口模拟可扩展的枚举

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

随机推荐

  1. unity DoTween使用

    先说插件获取,DoTween是一个开源的插件,它的代码托管在Github上[https://github.com/Demigiant/dotween].若只是单纯项目需要是可以去AssetStore获 ...

  2. 喵哈哈村的魔法考试 Round #20 (Div.2) 题解

    题解: A 喵哈哈村的跳棋比赛 题解:其实我们要理解题意就好了,画画图看看这个题意.x<y,那么就交换:x>y,那么x=x%y. 如果我们经过很多次,或者y<=0了,那么就会无限循环 ...

  3. oracle 列相减——(Oracle分析函数Lead(),Lag())

    lag和lead函数,用于取出数据的前n行的数据和后n行的数据,当然要和over(order by)一起组合 其实这2个函数的作用非常好理解,Lead()就是取当前顺序的下一条记录,相对Lag()就是 ...

  4. shutdown vs close

    shutdown 和 close关闭tcp连接的介绍网上有很多,主要区别如下: 1.调用close后,将中止通信.删除套接字.丢弃数据.但是,注意喽,但是,如果有多个进程共享一个套接字,close每被 ...

  5. Python:内置函数

    Python所有的内置函数     Built-in Functions     abs() divmod() input() open() staticmethod() all() enumerat ...

  6. AngualrJS中制作一个有关菜单的Directive

    通常我们这样写一个菜单: <ul> <li data-ng-class="{'active': highlight('/orders')}"> <a ...

  7. vue2.0实现倒计时的插件(时间戳 刷新 跳转 都不影响)

    我发现好多倒计时的插件,刷新都会变成从头再来,于是自己用vue2.0写了一个,测试通过,直接上代码 如下是组件代码: <template> <span :endTime=" ...

  8. Java全栈程序员之04:Ubuntu下安装MySQL、注册服务及Navcat

    在安装MySQL之前,我们先来介绍一下Ubuntu下软件的安装方式.首先回顾下,我们安装JDK,使用的是后缀名为tar.gz的安装包.安装idea,使用的是tar.gz:那有没有别的安装方式呢? 1. ...

  9. iOS:百度长语音识别具体的封装:识别、播放、进度刷新

    一.介绍 以前做过讯飞语音识别,比较简单,识别率很不错,但是它的识别时间是有限制的,最多60秒.可是有的时候我们需要更长的识别时间,例如朗诵古诗等功能.当然讯飞语音也是可以通过曲线救国来实现,就是每达 ...

  10. iOS开发-获取设备型号信息

    开发中有的时候查看设计统计数据,或者通过日志查看错误信息,这个时候我们就需要获取获取设备信息,看下关于设备有几种方法: NSLog(@"%@",[[UIDevice current ...