Java Core 第9版 读书笔记(OOP) Part1

用书中的例子,举一个简单的 Employee 类。

class Employee 
{
    // instance fields
    private String name ;
    private double salary ;
    private Data hireDay ;

    // constructor 
    public Employee( String n , int  year , int month , int day )
    {
        name = n ;
        salary = s ;
        GregorianCalendar calendar = new GregorianCalendat( year , month-1 , day );
        hireDay = calendar.getTime();
    } 
    public String getName ()
    {
        return name ;
    }
    //省略一些访问器和更改器
}

封装

  • 一个私有的数据域;
  • 一个公有的域访问器方法 ;
  • 一个公有的域更改器方法 ;

好处: 除了该类的方法外,不会影响其他代码。

域访问器

为什么要设置域访问器(getName,…)?
关键在于,事理上一个雇员在构造好后名字应该是作为只读域存在在对象中,使用private进行修饰,设置访问器来确保 name 域不会受到外界的破坏。

虽然 salary 不是只读域(raiseSalary可以修改),但是也只有仅仅几个方法可以在本类中修改,一旦出现了错误,只要调试这几个方法即可。不会使得错误出现在封装的类之外,更快的解决问题。

域更改器

其实访问器方法和更改器方法有可能需要做更多的工作。除了,对属性的直接外部访问进行封装,更改器方法还可以执行错误检查,然而直接对域进行赋值则不会处理。

public void setName() throws UnKonwNameException ;
{
    //....
}

注意 : 域访问器不要直接返回对象引用,要返回对象的克隆。Object.clone();

基于类的访问权限

class Employee 
{
    public boolean equals( Employee other )
    {
        return name.equals(other.name);
    }
}

一个方法可以访问所属类的所有对象的私有数据。

final 实例域

final 通常用来修饰基本类型域,或不可变类的域(类中的方法不会改变对象,如String)如用于可变类,则可能造成混乱。

tips : final 方法在继承中讲,有关动态绑定。

静态域和静态方法

静态域

类对于静态域,只有一个。而每一个对象,对于所有静态实例域都有一份自己的拷贝。

举一个应用

class Employee
{
    private static int nextId = 1 ;
    private int id ;

    private void setId()
    {
        this.id = Employee.nextId;
        Employee.nextId ++ ;
    }
}

静态常量

静态变量使用得比较少,但静态常量却使用的比较多,例如在 Math 中的PI 。.

public static final double PI = 3.14159826 ;

如果 static 关键字被省略,PI 就成了 Math 类的实例域,需要通过 Math 类的对象访问 PI ,并且每一个 Math 都有一有一份类静态域的拷贝。

另外:

public class System
{
    public static final PrintStream out = ... ;
}

由于每个类对象都可以对公有域进行修改,所以最好不要将域设计为 public 但是对于一个常量,经常性的调用 域访问器 (也不排错OwO),显得有些多此一举,而 final 关键字就解决了这个问题,实现所谓的公有常量域,无法被更改。

静态方法

不对对象进行操作的方法(在静态方法初始化时,还不存在实例呢,怎么操作…),也就是说,静态方法不能访问实例域。

几个设计技巧(封装)

  • 一定要保证数据私有。

最重要的,绝对不要破坏封装性。尽管需要编写访问器方法和更改器方法,但是尽量还是保持实例域的私有性,数据的表示形式可能会变,但它们的使用方式却不会经常发生变化。当数据保持私有时,它们的表示形式发生变化不会对类的使用者产生影响,即使出现bug也易于检测。

  • 一定要对数据初始化。

在创建实例时的执行步骤是先调用实例化父类,然后对字段进行初始赋值,再之使用构造器。而在方法中,直接进入栈中的方法帧,所有变量均为初始化,不可以直接使用。

  • 不要在类中使用过多的基本类型。

不要过多的使用基本类型,就是说用其他类代替多个相关的基本类型,会更加易于理解且易于修改。如下

private String street ;
private String city ;
private String state ;
//可以使用一个称为 Address 的新类替换
class Address
{
    private String street ;
    private String city ;
    private String state ;

    //getter and setter ...
}
  • 不是所有的域都需要独立的域访问器和更改器。

不要矫枉过正,要自己思考哦~

  • 将职责过多的类进行分解。

设计10个类,每个类只有一个方法。(OOP癌症患者)

继承

在 Employee 类中 ,经理的待遇和普通的雇员的待遇存在着一些差异,不过他们之间也存在着相同的地方,例如他们都领取薪水。但是经理的工资算法和普通员工可能有一定差别。这种情况就需要使用继承。可以定义一个新类 Manager ,以便提供经理的工资算法。这样在计算机科学中讲 Manager 是 Employee 的子集,存在着 “is-a” 的关系,每个经理都是一名雇员。

class Manager extends Employee
{
    private int Bonus ;
    public void setBouns ( int bonus )
    {
        this.Bonus = bonus
    }

    @Override 
    public int getSalary()
    {
        return super.getSalary + this.bonus ; 
    }
}

super 和 this 是相同概念,实际上,super不是一个对象的引用,他只是一个指示编译器调用超类方法的关键字。

多态

一个变量对象可以指示多种实际类型的现象称之为多态(polymorphism),在运行时能够自动选择调用哪个方法的现象称为动态绑定(dynamic binding)。

借用上面的例子,若是以 Employee 来声明雇员引用,则无论实例类型都调用 Employee 的getSalary() 算法。

置换法则

在任何可以使用超类的地方,可以使用子类,逆之不行。
优点:

Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
Employee[0] = boss ;

boss.getSalary() ;  // OK
staff[0].getSalary();   // ERROR

staff[0] 和 boss 引用的是同一个对象,但是编译器将 staff[0] 看成是 Employee 对象。

不能讲超类引用赋值给子类,因为Employee不一定是个Manager

Manager m = new Employee();     //ERROR

置换引发的问题:
子类数组的引用可以转换为超类数组的引用,而不需要强制转换。

Manager[] managers = new Manager[10];
Employee[] staff = managers ;   //OK

// 此时 managers 和 staff 引用了同一个数组。

staff[0] = new Employee();  //OK

managers[0].setBonus(1000); //ArrayStroeException 

动态绑定

与之相反的是,通过 final , static , private 修饰的,编译器可以准确的知道应该调用哪个方法,称之为静态绑定

而动态绑定则是,在运行过程中检查实例对象的具体类型决定调用哪个方法称之为动态绑定

例如如下:

class Pet 
{
    public static void say ( Pet obj  )
    {
        Pet.shout();
    }

    // 动物是泛指没有具体叫声,将方法置空;
    public void shout() { }
}

class Dog extends Pet 
{
    @Override 
    public void shout ()
    {
        System.out.println("Wang~");
    }
}

class Cat extends Pet
{
    @Override 
    public void shout ()
    {
        System.out.println("Miao~");
    }   
}

执行如下代码:

Pet p1 = new Dog();
p1.shout( p1 );

Pet p2 = new Cat();
p2.shout( p2 );

输出的结果是:

Wang~
Miao~

这里引入一部分对于JVM指令的研究和分析:

0:new               // class com/Dog
3:dup           
4:invokespecial     // Method com/Dog."<init>":()V
7:a_store_1
8:aload_1
9:invokevirtual     // Method com/Pet.shout:()V
12:return 

1 声明一个 com.Dog 将引用压入栈顶。
3 复制栈顶的引用,并将复制压入栈顶。
4 调用 com.Dog 的构造方法,对栈顶com.Dog进行初始化。
7 将栈顶引用存入本地引用变量 1 (Pet p)
8 获取本地引用变量 1 压入栈顶
9 invokevirtual 调用虚方法

具体实现在编译后交由虚拟机对虚方法进行处理,通过判断引用对象的实际类型决定调用的方法。

每次调用方法都要进行搜索,时间开销相当大,因此虚拟机为每个类创建了一个方法表,列出了其所有方法的签名和实际调用方法。这样一来,在整整调用方法时,虚拟机找这个表就行了。

在运行时,调用 p.shout 的解析过程为:
1. 虚拟机提取 p 的实际类型的方法表。
2. 虚拟机搜索定义 shout 签名的类。
3. 调用。

final阻止继承不允许子类重写他的方法,确保他们不会在子类中改变语义。事实上,在C++、C#中,默认方法都不具有多态性。
除非有足够多的理由使用多态,否则应将所有方法声明为 final

另外

动态绑定是多态实现的原理,提高了程序的可拓展性,在不改变代码的基础上实现对功能的增加。(开闭原则)

抽象类

看上面动态绑定举的例子,有一处非常奇怪也非常显眼的地方。Pet 的 shout 置空,且不存在所谓的 Pet 对象。

Pet 类更加通用,只将他作为派生其他类的基类,而不需要真正的实例,更具有通用性,更加抽象。

为什么要花费精力进行这么高层次的抽象呢?可以将子类的通用方法、域放置在继承关系较高层次的通用超类中。

例如 宠物会叫 , 但是 Pet 却只是抽象对叫声一无所知,当然可以让 Pet 的实例 shout 方法为空,然而更好的方法是 使用抽象方法,这样就完全不需要实现这个方法了。

我们称 没有具体实现的方法为 抽象方法,一个类拥有抽象方法,那么他就必须声明为抽象类。

public abstract Pet
{
    // 通用域
    private String name ;

    // 纯虚函数
    public abstract void shout () ;

    // getter .. and .. setter 

    // 通用方法  
}

因为抽象类不存在实例,所以抽象方法便称之为纯虚函数。

proteced

允许子类直接访问,父类的proteced域。

但是只允许访问当前对象父类的 proteced 域,不能访问其他对象的这个域。

一个小小的实例,Override Object 的 equals 方法。

众所周知,Object是所有类的超类,Object提供了一个这样的通用方法:

public void equlas ( Object obj )
{
    return (this == obj);
}

实现了两个对象进行比较,判断是否为同一个对象,而==只判断两个对象具有共同的引用时,他们一定相等。但是对于多数类,这样的比较毫无意义,通常要检测两个对象是否相等需要覆盖着个方法,例如判断两只宠物的相等性需要对实例域进行比较。

class Pet 
{
    public boolean equlas ( Object otherObj )
    {
        if ( this == obj ) return true ;
        if ( otherObj == null ) return false ;
        if ( this.getClass() != otherObj.getClass() ) return false ;
        Pet other = (Pet) otherObj ;
        return(this.name.equlas(other.name));
    }
}

继承设计技巧

  • 将公共操作和域放在超类。
  • 不要使用受保护的域。

一个类的子类的集合是无限大的,任何人都可以继承父类使用protected域,从而破坏封装。

  • 使用继承实现 “is - a” 关系。

使用继承很容易达到代码复用的目的,不要把无关的两个类强行划上 is - a。

  • 除非所有继承的方法都有意义,否则不要使用继承。

不要为了继承而继承。

  • 在覆盖方法时,不要改变预期的行为。

置换原则不仅用于语法,而且也可能应用于行为,不应该毫无缘由的改变行为的内涵。

  • 使用多态,而非类型信息。

if ( x is of type 1 )
action1(x)
else if ( x is of type 2 )
action2(x)
都应该考虑使用多态性。

欢迎交流,指出错误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值