面向对象程序设计概述
面向对象程序设计(OOP)。面向对象更加适用于解决规模较大的问题,在OOP中,不必关心对象的具体实现,只要能满足用户的需求即可。面向对象的思想即物以类聚,分类的思维模式,思考问题首先会解决问题需要哪些分类,然后对这些分类进行单独思考
面向对象编程的本质就是:以类的方式组织代码,以对象的组织(封装)数据
面向对象三大特性
封装
首先,一个优秀的程序需要做到“高内聚,低耦合”。高内聚指的即类的内部数据操作细节需要自己完成,不允许外部干扰;低耦合即是仅暴露少量的方法给外部进行使用
封装的优点
- 可以改变内部结构,除了该类的方法之外,不会影响其他代码
- 更改器方法可以执行错误检查,增加系统的可维护性
私有方法
在Java中,为了实现一个私有的方法,只用需要将关键字public改成private。只要是私有方法,类的设计者就可以确定它不会被其他的类操作调用,可以删除。但是如果是公有的,就不能删掉,因为有可能其他的代码会依赖这个方法。
受保护方法
有些时候,我们希望父类中的某些方法允许被子类访问,或者允许子类的方法访问父类的某个域。为此我们需要将这些方法或域声明为protected。
在实际应用中,要谨慎使用protected属性。由于其他的程序员可以由这个受保护类再派生出新类,并访问其中的受保护域。在这种情况下,如果需要对这个类的实现进行修改,就必须通知所有使用这个类的程序员。这违背了OOP提倡的数据封装原则。
这种方法最好的实例就是Object类中的clone方法
以下是四种方法修饰符的对比:
内部类 | 本包 | 子类 | 外部类 | |
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
final实例
可以将实例域定义为final。构建对象时必须对这样的域进行初始化,并且在后面的操作中,不能对其进行修改。
继承
关键字extends表示继承,表明正在构造的新类派生于一个已存在的类。已存在的类被称为超类、基类、父类;新类被称为子类、派生类。子类拥有比父类更加丰富的方法。例如,human(人)和superman(超人)这2个类。超人拥有人的一切特征,还可以在这基础上拓展他独有的能力。所以是超人继承了人。
覆盖方法
超人继承了人的所有特性,但是人的一些特性总会和超人不符合,因此超人会在这个方法中进行修改和完善。这在Java中就被称作覆盖方法
public Superman(String name, String nature, int year, int month, int day) {
super(name, nature, year, month, day);
bonus = 0;
}
这里的关键字super是调用父类Human中含有name,nature,year,month和day参数的构造器的简写形式
我们可以通过super实现对父类构造器的调用。调用构造器的语句必须是子类构造器的第一条语句。如果子类构造器没用显示的调用父类的构造器,则将自动调用父类默认的构造器(没有参数)。如果父类没有无参构造器,而子类又没有显示的调用父类的其他构造器,则会编译报错
关键字this的两个用途:
- 引用隐式参数
- 调用该类的其他构造器
相对应的super也有两个用途:
- 调用父类方法
- 调用父类的构造器
继承并不局限于一个层次。在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链 通常,一个祖先类可以拥有多个子孙继承链。例如:
阻止继承:final类和方法
不允许拓展的类被称为final类。如果在定义类的时候使用了final修饰符就表明这个类是final类。声明格式如下:
public final class BusDriver extends Driver{
...
}
final类中的所有方法自动的成为final方法
类中的特定方法也可以被声明为final。如果这样,子类就无法覆盖这个方法。将类或方法声明为final的主要目的是:确保他们不会在子类中改变语义
如果一个方法没有被覆盖并且很短,编译器就可以对其进行优化处理,这个过程被称为内联
多态
"is-a"规则:它表明子类的每个对象也是超类的对象。这个规则的另一个表述法是置换法则。它表明程序中出现父类对象的任何地方都可以用子类对象置换。例如可以将一个子类对象置换给父类。
Human h;
h = new Human(. . .);
h - new Superman(. . .);
在JAVA中,对象变量是多态的。一个Human类既可以引用一个Human类的对象也可以引用Human类任何一个子类的对象
理解方法调用
假设要调用x.f(args),隐式参数x声明为类C的一个对象
- 编译器查看对象的声明类型和方法名。有可能存在多个名字为f,但参数类型不一样的方法。编译器将会一一列举所有C类中名为f的方法和其父类中访问属性为public且名为f的方法(父类中的私有方法不可以访问)
- 编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就采用这个方法。这个过程被称作重载解析
- 如果是private方法、static方法、final方法或者构造器,那么编译器将会准确地知道应该调用哪个方法,我们将这种调用方式称作静态绑定。与之相对应的是,调用方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定
- 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法
每次调用方法都要进行搜索,时间开销相当大。因此虚拟机预先为每一个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。这样,在真正调用方法的时候,虚拟机仅仅去查找这个表就可以了
动态绑定的一个重要特性:无需对现存的代码进行修改,就可以对程序进行拓展
对象构造
重载
有些类有多个构造器。如果多个方法有相同的名字,不同的参数,这种特征叫做重载。编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相印的方法。如果编译器找不到匹配的参数,就会产生编译时错误(这个过程被称作重载解析)。例如:
indexOf(int)
indexOf(int, int)
indexOf(String)
indexOf(String, int)
默认域初始化
如果在构造器中没有显式地给域赋予初始值,那么就会被自动的赋为默认值:数值为0、布尔值为false、对象引用为null。(一般不会这样做)
如果不明确地对域进行初始化,就会影响代码的可读性
无参数的构造器
很多类都会包含一个无参的构造函数,对象由无参数构造函数创建时,其状态会设置为适当的默认值。例如:
public Employee0 {
name = "";
salary = 0;
hireDay = LocalDate,now();
}
如果在编写一个类时没有编写构造器,那么系统会提供一个无参构造器。这个构造器将所有的实例域设置为默认值。如果类中至少提供了一种构造器,但是没有提供无参数的构造器,则在构造对象时使用无参构造则会被视作不合法
显示域初始值
通过重载类的构造器方法,可以采用多种形式设置类的实例域的初始状态。确保不管怎样调用构造器,每个实例域都可以被设置成一个有意义的初值
强制类型转换
进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能
如果试图在继承链上进行向下的类型转换,并且“谎报”了有关对象包含的内容,运行程序的时候,Java运行时系统将会报告这个错误,并产生一个ClassCastException异常。如果没有捕获到这个异常程序将会终止。
因此,应该养成一个良好的设计习惯,在进行类型转换之前,先使用instanceof操作符查看一下是否可以成功转换。
- 只能在继承层次内进行类型转换
- 在将父类转换成子类之前,应该使用instanceof进行检查
- 在一般情况下,应该尽量少用类型转换和instanceof运算符
文档注释
JDK包含的一个很有用的工具叫javadoc,由源文件生成的一个HTML文档。
javadoc从下面几个特性来抽取信息:
- 包
- 公有类与接口
- 公有的和受保护的构造器方法
- 公有的和受保护的域
应该为以上几个部分编写注释。注释放置在所描述的特性前。以/**开始,*/结束。
类设计技巧
1.一定要保证数据私有
绝对不要破坏封装性。当数据保持私有的时候,它们的表示形式的变化不会对类的使用者产生影响,即使出现bug也易于检测
2.一定要对数据进行初始化
Java不会对局部变量进行初始化,但是会对对象的实例域进行初始化。最好不要依赖于系统的默认值,而是应该显式的初始化所有数据
3.不要在类中使用过多的数据类型
用其他的类代替多个相关基本类型的使用。这样会使类更加易于理解且易于修改
4.类名和方法名要能体现它们的职责
命名类名的良好习惯是采用一个名词;前面有形容词修饰的名词或动名词
5.优先使用不可变的类
更改对象的问题在于,如果多个线程试图同时更新一个对象,就会发生并发改变。其结果无法预料。如果类是不可变的,就可以安全地在多个线程间共享对象
Object类:所有类的父类
Object类是JAVA中所有类的始祖,在JAVA中每个类都是由它拓展而来的。但是并不需要写extend继承
- 在JAVA中,只有基本类型不是对象,例如,数值、字符和布尔类型的值都不是对象。
- 所有的数组类型,不管是对象数组还是基本类型的数组都拓展了Object类
equals方法
Object类中的equals方法用于检验一个对象是否等于另一个对象。在Object类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的
Java要求equals具有下面这些特性:
- 自反性:对于任何非空引用x,x.equals(x)应该返回true
- 对称性:对于任何引用x、y,当且仅当y.equals(x)返回true;x.equals(y)也应该返回true
- 传递性:对于任何引用x、y、z,如果x.equals(y)返回true;y.eqyals(z)返回true;那么x.equals(z)也应该返回true
- 一致性:如果x,y引用的对象没有发生变化,反复调用x.equals(y)应该返回相同的结果
- 对于任意非空引用x,x.equals(null)都应该返回false
hashCode方法
散列码(hashCode)是由对象导出的一个整型值。散列码是没有规律的。由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址
如果重新定义equals方法就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中。hashCode方法返回的是一个整数类型(负数也可)。如果x.equals(y),那么x.hashCode()和y.hashCode()必须具有相同的值
toString方法
toString方法,它用于返回表示对象值的字符串。绝大多数(但不是全部)的toString方法都遵循这样的格式:类的名字+一对方括号括起来的域值
最好通过调用getClass().getName()获取类名的字符串,而不要将类名硬加到toString方法中
public String toStringO
{
return getClassO.getNameO
+ "[name=" + name
+ ",salary:" + salary
+ ",hireDay=" + hireDay
+ "]";
}
如果父类使用了getClass().getName(),那么子类只需要调用super.toString()方法就可以了
泛型数组列表
在实际应用中,一旦确定了数组的大小,那么后面就很难改变了。解决这个问题最简单的方式就是使用Java中一个被称为ArrayList的类。它在添加或删除元素时,具有自动调节数组容量的功能。ArrayList是一个采用类型参数的泛型类。示例:
ArrayList<Human> list = new ArrayList<Human>();
在JavaSE 7 中,可以省去右边的类型参数。
- add方法可以将元素添加到数组列表中
- size方法将返回数组列表中包含的实际元素数目
- 一旦确定了数组列表大小不再变化,可以用trimToSize方法。这个方法可以将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器会回收多余的存储空间
虽然数组列表可以自动拓展容量,但相对应的它也增加了访问元素语法的复杂程度。使用get和set方法来实现访问和改变数组元素的操作。例如要设置第i个元素
list.set(i,first)
//等价于对数组a的元素赋值
a[i] = first;
对数组实施插入和删除元素的操作其效率比较低。对于小型数组来说,这一点不必担心。但是如果数组存储的元素数比较多,又经常需要在中间位置插入、删除元素,就应该考虑使用链表
对象包装器与自动装箱
有的时候,需要将基本类型转换为对象。比如想要定义一个整型数组列表,但是尖括号中的类型参数不允许是基础类型。也就是说,这里就必须用到对象包装器。例如:
ArrayList<Integer> list = new ArrayList<>();
Integer类就对应着基本类型int。这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Charater、Void和Boolean(前6个类派生于公共的父类Number)。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装中的值。同时,包装器类是final,因此不能定义他们的子类
list.add(3);
自动转换为
list.add(Integer.valueOf(3));
上述变换被称为自动装箱。
相反的,将一个Integer对象赋予给int值时,将会进行自动拆箱
枚举类
public enuni Size { SMALL , MEDIUM, LARGE, EXTRAJARGE };
这个声明定义的类型是一个类,它有4个实例,在此尽量不要构造新对象。因此,在比较两个枚举类型的值时,不要调用equals,而是直接使用"=="就可以了。
如果有需要的话,可以在枚举类型中添加一些构造器、方法和域。
所有的枚举类型都是Enum类的子类。它们继承了这个类的很多方法。其中最有用的是toString方法,这个方法能够返回枚举常量名。例如:Size.SMALL.toString()将返回字符串”SMALL“
每个枚举类都有一个静态的values方法,它将返回一个包含全部枚举值的数组。
反射
反射库提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵Java代码的程序。
能够分析类能力的程序称为反射。反射的功能很强大。
- 在运行时分析类的能力
- 在运行时查看对象
- 实际通用的数组操作代码
- 利用Methods对象
Class类
在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这些信息会跟踪每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。保存这些信息的类就叫做Class类。
Object类中的getClass()方法将会返回一个Class类型的实例。
还可以调用静态方法forName获得类名对应的Class对象。如果类名保存在字符串中,并可在运行时改变,就可以使用这个方法。这个方法只有在className是类名或接口名的时候才能够执行,否则就会抛出一个checkedexception(已检查异常)。无论什么时候用这个方法,都应该提供一个异常处理器。
一个Class对象实际上是表示的一种类型,而这个类型未必是一个类。例如,int不是类,但int.class是一个Class类型的对象。
最常用的Class方法是getName。这个方法将会返回类的名字
利用反射分析类的能力
反射最重要的内容就是检查类的结构。
在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。三个类都有getName方法,用来返回项目名称。Filed类有一个getType方法,用来返回描述域所属类型的Class对象
继承的设计技巧
- 将公共操作和域放在父类
- 不要使用受保护的域
- 使用继承实现"is-a"关系
- 除非所有继承的方法都有意义,否则不要使用继承
- 在覆盖方法时不要改变预期行为
- 使用多态而不是类型信息
- 不要过多的使用反射
参考文献 : 《JAVA核心技术 卷一》