Java程序设计(2021春)——第三章类的重用笔记与思考
Java程序设计(2021春)——第三章类的重用笔记与思考
本章概览:
3.1 类的继承(概念与语法)
3.2 Object
类(在Java继承最顶层的类)
3.3 终结类和终结方法(只能拿来用,不可以继承)
3.4 抽象类(有一些方法暂时不能实现)
3.5 泛型
3.6 类的组合(继承是隶属关系,组合不是)
3.1.1&3.1.2 类继承的概念与语法
继承是一种以已有类为基础,生成设计新类的机制,是面向对象程序设计的重要特征。
类继承的概念
- 根据已有类来定义新类,新类拥有已有类的所有功能。
- Java中只支持类的单继承,每个子类只能有一个直接超类。
- 已有的作为基础的类叫做超类,也叫做基类或父类。
- 由超类产生的新类叫做子类(
subclass
),在面向对象概念中也称派生类, - 超类是所有子类的公共属性及方法的集合,子类则是超类的特殊化。
- 继承机制可以提高程序的抽象程度,提高代码的可重用性。
超类和子类
子类对象与超类对象存在“是一个...”(或“是一种...”)的关系,将在后续例题中介绍。
子类对象
从外部来看,他应该包括:
- 与超类相同的接口,即,子类应当有超类所有的对外接口对外服务。
- 可以具有更多的方法和数据成员。
其内包含着超类的所有变量和方法,但是子类的对象里面存储的只有从超类继承来的属性和自己本类所扩展新增的属性(类的方法只会存放在方法,不会在每个对象中存储,对象中进存储数据)。
继承的语法
/*类继承关系的说明*/
[ClassModifier]class ClassName extends SuperClassName{
//类体
}
类继承举例:
设有三个类:Person
,Employee
,Manager
,Person
是最广泛的概念,Employee
比Person
更具体一些,Manager
又是比泛泛的Employee
更具体的类。因此,可以让Employee
继承Person
,再让Manager
继承Employee
,用自然语言可以描述为Employee
是一种Person
,Manager
是一种Employee
,这其中便蕴含了类的继承关系设计的思想。
例:类继承
public class Person{
public String name;
public String getName(){
return name;
}
}
public class Employee extends Person{
public int employeeNumber;
public int getEmployeeNumber(){
return employeeNumber;
}
}
public class Manager extends Emploee{
public String responsibilities;
public String getResponsibilities(){
return responsibilities;
}
}
测试类
public class Exam4_2Test {
public static void main(String[] args) {
Employee li = new Employee();
li.name = "Li Ming";//从超类继承的属性
li.employeeNumber = 123456;//自己的属性
System.out.println(li.getName());//从超类继承的方法
System.out.println(li.getEmploeeNumber());//自己定义的方法
Manager he = new Manager();
he.name = "He Xia";//从超类的超类继承的属性
he.employeeNumber = 543469;//从超类继承的树属性
he.responsibilities = "Internet Project";//自己的属性
System.out.println(he.getName());//从间接超类继承的方法
System.out.println(he.getEmploeeNumber());//从直接超类继承的方法
System.out.println(he.getResponsibilities());//自己定义的方法
}
}
运行结果
Li Ming
123456
He Xia
543469
Internet Project
例:访问从超类继承的成员
public class classB {
public int a = 10;
private int b = 20;
protected int c = 30;
public int getB() {
return b;
}
}
public class classA extends classB {
public int d;
public void tryVariables() {
System.out.println(a);//允许
System.out.println(b);//不允许
System.out.println(getB());//允许
System.out.println(d);//允许
}
}
从上例可知,classA
中直接访问a
是可以的,但试图直接访问b
是不可以的,因为private
是私有属性,在继承类中不可以直接访问(可以查看我的《Java程序设计(2021春)——第二章笔记与思考》这篇blog中有具体介绍权限控制范围),此时可以查看超类中是否为该private
设置访问接口,此处有即为getB()
。
3.1.3 隐藏和覆盖
子类对从超类继承而来的属性和行为可以重新定义:定义重名的属性,则从超类继承过来的属性会被隐藏;如果声明一个和超类继承过来的方法圆形一模一样的方法,那么从超类继承的方法会被覆盖。
属性的隐藏
子类中声明了与超类中相同的成员变量名
- 从超类继承的变量将被隐藏,但仍存在。
- 子类拥有了两个相同名字的变量,一个继承自超类,另一个由自己声明。
- 当子类执行继承自超类的操作时,处理的是继承自超类的变量,而当子类执行它自己声明的方法时,所操作的是它自己声明的变量。
- 变量隐藏可以修改类型。
例:
class Parent {
Number aNumber;
}
class Child extends Parent {
Float aNumber;
}
通过这样的办法可以隐藏掉超类中的某些属性,从而定义我们自己的属性。
访问被隐藏的超类属性
- 调用从超类继承的方法,则操作的是从超类继承的属性。
- 本类中声明的方法使用
super.属性
访问从超类继承的属性。
public class A1 {
int x = 2;
public void setx(int i) {
x = i;
}
void printa() {
System.out.println(x);
}
}
public class B1 extends A1 {
int x = 100;
void printb() {
super.x = super.x + 10;//操作从超类继承而来的x
System.out.println("super.x=" + super.x + " x= " + x);
}
}
public class Exam4_4Test {
public static void main(String args[]) {
A1 a1 = new A1();
a1.setx(4);
a1.printa();
B1 b1 = new B1();
b1.printb();
b1.printa();
b1.setx(6);//将继承x的值设置为6
b1.printb();
b1.printa();
a1.printa();
}
}
输出:
4
super.x=12 x= 100
12
super.x=16 x= 100
16
4
例:访问超类静态属性
class A {
static int x = 2;//注意,虽然可以被所有子类对象成员访问,但是静态成员不被继承
public void setx(int i) {
x = i;
}
void printa() {
System.out.println(x);
}
}
class B extends A {
int x = 100;
void printb() {
super.x = super.x + 10;
System.out.println("super.x=" + super.x + " x= " + x);
}
}
public class Tester {
public static void main(String args[]) {
A a1 = new A();
a1.setx(4);
a1.printa();
B b1 = new B();
b1.printb();
b1.printa();
b1.setx(6);
b1.printb();
b1.printa();
a1.printa();
}
}
输出:
4
super.x=14 x= 100
14
super.x=16 x= 100
16
16
注意,静态成员不属于任何一个对象,不会有多个副本,所以在Tester
类中调用的方法对super.x
进行操作时,父类A
中的x
就是被改变了,从别的对象访问也是被改变的。
方法的覆盖
- 如果子类不需要使用从超类继承来的方法的功能,则可以声明自己的同名方法,称为方法覆盖。
- 覆盖方法的返回类型、方法名称、参数的个数以及类型必须和被覆盖的方法一模一样。
- 只需在方法名前面使用不同的类名或不同类的对象名即可区分覆盖方法和被覆盖方法。
- 覆盖方法的访问权限可以比被覆盖的宽松,但是不能更为严格。例如,方法在超类中是公有的,在子类中也必须是公有的,不能比这个更严格;方法在超类中是个保护的或私有的,则在子类中可以是公有的。
方法覆盖的应用场合
- 子类中实现与超类相同的功能,但采用不同的算法或公式。
- 在名字相同的方法中,要做比超类更多的事情。
- 在子类中需要取消从超类继承的方法。
方法覆盖的注意事项
必须覆盖的方法
- 派生类必须覆盖基类中的抽象的方法,否则派生类自身也成为抽象类。抽象类不能生成实例,只能当作超类用。(稍后会详细介绍抽象类)
不能覆盖的方法
- 基类中声明为
final
的终结方法。 - 基类中声明为
static
的静态方法。
调用被覆盖的方法
super.overriddenMethodName();
3.2 Object
类
- Java中
Object
类是所有类的直接或间接的超类,处在类层次的最高点。 - 包含了所有Java类的公共属性。
Object
类的主要方法
public final Class getClass()
:获取当前对象所属的类信息,返回Class
对象。public String toString()
:返回表示当前对象本身有关信息的字符串对象。public boolean equals(Object obj)
:比较两个对象引用是否指向同一对象,是则返回true
,否则返回false
。protected Object clone()
: 复制当前对象,并返回这个副本。public int hashCode()
: 返回该对象的哈希代码值。protected void finalize()throws Throwable
: 在对象被回收时执行,通常完成资源的释放工作。
相等和同一
- 相等(
equal
):两个对象具有相同的类型及相同的属性值。 - 同一(
identical
):两个引用变量指向的是同一个对象。 - 两个对象同一,则肯定相等。
- 两个对象相等,不一定同一。
- 比较运算符
==
判断的是这两个对象是否同一。
例:用==
判断两个引用是否同一
public class Tester1 {
public static void main(String[] args) {
BankAccount a = new BankAccount("Bob", 123456, 100.00f);
BankAccount b = new BankAccount("Bob", 123456, 100.00f);
if (a == b) {
System.out.println("YES");
} else {
System.out.println("NO");
}
}
}
输出:
NO
解析:判断连个引用是否相等,本质上是判断他们指向的对象是否相同。
equals
方法
Objct
类中的equals()
方法的定义如下:
public boolean equals(Object x){
return this == x;
}
例:equals
方法
public class EqualsTest {
public static void main(String[] args) {
BankAccount a = new BankAccount("Bob", 123456, 100.00f);
BankAccount b = new BankAccount("Bob", 123456, 100.00f);
if (a.equals(b)) {
System.out.println("YES");
} else {
System.out.println("NO");
}
}
}
输出:
NO
由上可知,equals()
方法的功能天然的是判断对象是否同一,而非判断两个对象是否相等。
因此,当我们想要判断两个对象是否相等而非对象是否相同时,需要自己写equals()
方法体覆盖Object
类中的equals()
方法。
例:覆盖equals()
方法(1)
在BankAccount
类中覆盖equals()
方法
public boolean equals(Object x) {
if (this.getClass() != x.getClass())
return false;//先判断同类
BankAccount b = (BankAccount) x;//强转未知类型的x(其实程序运行到这说明x的类型是符合要求的)到BankAccount类
return ((this.getOwnerName().equals(b.getOwnerName())) && (this.getAccountNumber() == b.getAccountNumber())&& (this.getBalance() == b.getBalance()));
}
例:覆盖equals()
方法2
public class Apple {
private String color;
private boolean ripe;
public Apple(String aColor, boolean isRipe) {
color = aColor;
ripe = isRipe;
}
public void setColor(String aColor) {
color = aColor;
}
public void setRipe(boolean isRipe) {
ripe = isRipe;
}
public String getColor() {
return color;
}
public boolean getRipe() {
return ripe;
}
public String toString() {
if (ripe)
return ("A ripe" + color + "apple");
else
return ("A not so ripe" + "apple");
}
public boolean equals(Object obj) {
if(obj instanceof Apple) {
Apple a = (Apple)obj;
return ( (color.equals(a.getColor())) && (ripe == a.getRipe()))
}
return false;
}
}
上述采用instanceof
来判obj
是否类型相符合。
hashCode()
方法
hashCode()
是一个返回对象散列码的方法,该方法实现的一般规定是:
- 在一个Java程序的一次执行过程中,如果对象”相等比较“所使用的信息没有被修改的话,同一对象执行
hashCode()
方法每次都应返回到同一个整数。在不同的执行中,对象的hashCode()
方法返回值不必一致。 - 如果依照
equals()
方法两个对象是相等的,则在这两个对象上调用hashCode()
方法应该返回同样的整数结果。 - 如果依据
equals()
方法两个对象不相等,并不要求在这两个对象上调用hashCode()
方法返回值不同。
只要实现得合理,Object
类定义的hashCode()
方法为不同对象返回不同的整数。一个典型的实现是,将对象的内部地址转换为整数返回,但是Java语言并不要求必须这样实现。
clone
方法
用于根据已存在的对象构造一个新的对象,即复制对象。
使用clone
方法复制对象
-
覆盖
clone
方法:在Object
类中被定义为protected
,所以需要覆盖为public
。 -
实现
Clonable
接口,赋予一个对象被克隆的能力(cloneability
)。不需要再额外去实现什么,只是一个标记表示可以克隆。例:如下表示允许
MyObject
类被克隆class MyObject implements Cloneable{ //... }
finalize
方法
- 在对象被垃圾回收器回收之前,系统自动调用对象的
finalize
方法,但是该方法不能够被显式地调用。一个对象不再被使用即随时有可能被回收,但是什么时刻回收、以什么次序回收并没有规定。 - 如果要覆盖
finalize
方法,覆盖方法的最后必须调用super.finalize
。
getClass
方法
getClass
是一个final
方法,返回一个Class
对象,用来代表对象所属的类。- 通过
Class
对象,可以查询类的各种信息:如名字、超类、实现接口的名字等。
例:
void PrintClassName(Object obj){
System.out.println("The Object's Class is "+obj.getClass().getName());
}
notify
,notifyAll
,wait
方法
final
方法,不能覆盖。- 这三个方法主要应用在多线程程序中。
3.3 终结类与终结方法
- 用
final
修饰的类和方法。 - 终结类不能被继承。
- 中介方法不能被子类覆盖。
例:终结类
声明ChessAlgorithm
类为final
类
final class ChessAlgorithm{...}
如果再写如下程序:
class BetterChessAlgorithm extends ChessAlgorithm{...}
编译器将显示一个错误
Chess.java:6:Can't subclass final classes:class ChessAlgorithm
例:终结方法
final
方法举例
class Parent{
public Parent(){}//构造方法
final int getPI(){
return Math.PI;//终结方法
}
}
说明:
getPI
是用final
修饰符声明的终结方法,不能在子类中对该方法进行覆盖,因而如下声明是错误的。
Class Child extends Parent{
public Child(){}
int getPI{
return 3.14;//错误:不允许覆盖超类中的终结方法
}
}
综上终结类是只能直接使用不能被继承的类,终结方法是只可以原样使用不可以被覆盖的方法。
3.4 抽象类
抽象类往往代表比较抽象的概念,在抽象类中通常要规定整个类家族各级子类都必须具有的属性和方法。
抽象类
- 类名前加修饰符
abstract
。 - 可包含常规类能包含的任何成员,包括非抽象方法。
- 也可包含抽象方法:用
abstract
修饰,只有方法原型,没有方法体的实现。 - 没有具体实例对象的类,不能使用
new
方法进行实例化,只能用作超类。 - 只有当子类实现了抽象类中的所有抽象方法,子类才不是抽象类,才能产生实例。
- 如果子类中仍有抽象方法未实现,则子类也只能是抽象类,那就只能寄希望于下级子类实现抽象方法以产生实例。
抽象类声明的语法形式
abstract class Number{
...
}
如果写:
new Number();
编译器将会显示错误。
抽象方法
抽象方法规定了一种行为的访问接口,通常是在抽象类中规定子类都必须要有这样的行为,但是行为的具体实现没有给出。
-
抽象方法声明的语法形式:
public abstract <returnType><methodName>(...)
-
仅有方法原型,而没有方法体。
-
抽象方法的具体实现由子类在它们各自的类声明中完成。
-
只有抽象类可以包含抽象方法,即,若打算在方法中写抽象方法,则须定义类为抽象类。
抽象方法的优点
- 可以同以用这种方式设计整个类家族的对外公共服务接口。
- 隐藏具体的细节信息,所有子类使用的都是相同的方法原型,期中包含了调用该方法时需要了解的全部信息。
- 强迫子类完成指定的行为,规定所有子类的标准行为。
例:抽象的绘图类和抽象方法
各种图形都需要实现绘图方法,可在它们的抽象超类中声明一个draw
抽象方法。
abstract class GraphicObject{
int x,y;
void moveTo(int newX,int newY){
...
}
abstract void draw();
}
然后在每一个子类中重写draw
方法,例如:
class Circle extends GraphicObject{
void draw(){
...
}
}
class Rectangle extends GraphicObject{
void draw(){
...
}
}
3.5 泛型
泛型的本质是将类型参数化。有泛型的类、泛型的方法和泛型的接口。
例:泛型类
class GeneralType <Type>{
Type object;
public GeneralType(Type object){
this.object = object;
}
public Type getObj(){
return object;
}
}
public class Test {
public static void main(String[] args) {
GeneralType<Integer> i = new GeneralType<Integer>(2);
GeneralType<Double> d = new GeneralType<Double>(0.33);
System.out.println("i.object = " + (Integer) i.getObj());
System.out.println("i.object = " + (Integer) d.getObj());//编译错误:Cannot cast from Double to Integer
}
}
例:泛型方法
class GeneralMethod {
<Type> void printClassName(Type object) {
System.out.println(object.getClass().getName());
}
}
public class Test {
public static void main(String[] args) {
GeneralMethod gm = new GeneralMethod();
gm.printClassName("hello");
gm.printClassName(3);
gm.printClassName(3.0f);
gm.printClassName(3.0);
}
}
例:使用通配符
会进一步加强程序的通用性
class GeneralType <Type>{
Type object;
public GeneralType(Type object){
this.object = object;
}
public Type getObj(){
return object;
}
}
class ShowType {
public void show(GeneralType<?> o) {
System.out.println(o.getObj().getClass().getName());
}
}
public class Test {
public static void main(String[] args) {
ShowType st = new ShowType();
GeneralType<Integer> i = new GeneralType<Integer>(2);
GeneralType<String> s = new GeneralType<String>("Hello");
st.show(i);
st.show(s);
}
}
输出
java.lang.Integer
java.lang.String
有限制的泛型
在参数Type
后使用extends
关键字并加上类名或接口名,表明参数所代表的类型必须是该类的子类或者实现了该接口。
注意,对于实现了某接口的有限制泛型,也是使用extends
关键字,而不是implements
关键字。
例:有限制的泛型
class GeneralType<Type extends Number> {
Type object;
public GeneralType(Type object) {
this.object = object;
}
public Type getObj() {
return object;
}
}
public class Test {
public static void main(String[] args) {
GeneralType<Integer> i = new GeneralType<Integer>(2);
//GeneralType<String> s = new GeneralType<String>("Hello");
//非法,T只能是Number或者Number的子类
}
}
3.6 类的组合
面向对象的程序是用软件来模拟现实世界中的对象,而现实世界中的对象往往是由部件组装而成的。
类的组合也是一种类的重用机制,表达的是包含关系,”有一个“的关系。
组合的语法
将以存在的类的对象放到新类即可
例如,可以说:厨房kitchen
中有一个炉子cooker
和一个冰箱refrigerator
。所以,可以简单地把对象myCooker
和myRefrigerator
放在类kitchen
中。
class Cooker{//类语句}
class Refrigerator{//类语句}
class Kitchen {
Cooker myCooker;
Refrigerator myRefrigerator;
}
例:组合举例——线段类
一条线段包含两个端点
public class Point {
private int x, y;// coordinate
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int GetX() {
return x;
}
public int GetY() {
return y;
}
}
class Line {
private Point p1, p2;
Line(Point a, Point b) {
p1 = new Point(a.GetX(), a.GetY());
p2 = new Point(b.GetX(), b.GetY());
}
public double Length() {
return Math.sqrt(Math.pow(p2.GetX() - p1.GetX(), 2) + Math.pow(p2.GetY() - p1.GetY(), 2));
}
}
继承和组合的异同
继承表达的是”是一个“”是一种“的从属关系;组合表达的是”有一个”的包含关系。在声明复杂的类的时候往往继承和组合都用得上。
例:组合与继承的结合
class Plate {// 声明盘子
public Plate(int i) {
System.out.println("Plate constructor");
}
}
class DinnerPlate extends Plate {// 声明餐盘为盘子的子类
public DinnerPlate(int i) {
super(i);
System.out.println("DinnerPlate constructor");
}
}
class Utensil {// 声明器具
Utensil(int i) {
System.out.println("Utensil constructor");
}
}
class Spoon extends Utensil {// 声明勺子为器具的子类
public Spoon(int i) {
super(i);
System.out.println("Spoon constructor");
}
}
class Fork extends Utensil {// 声明餐叉为器具的子类
public Fork(int i) {
super(i);
System.out.println("Fork constructor");
}
}
class Knife extends Utensil {// 声明餐刀为器具的子类
public Knife(int i) {
super(i);
System.out.println("Knife constructor");
}
}
class Custom {// 声明做某事的习惯
public Custom(int i) {
System.out.println("Custom constructor");
}
}
public class PlaceSetting extends Custom {// 餐桌的布置
Spoon sp;
Fork frk;
Knife kn;
DinnerPlate pl;
public PlaceSetting(int i) {
super(i + 1);
sp = new Spoon(i + 2);
frk = new Fork(i + 3);
kn = new Knife(i + 4);
pl = new DinnerPlate(i + 5);
System.out.println("PlaceSetting constructor");
}
public static void main(String[] args) {
PlaceSetting x = new PlaceSetting(9);
}
}
输出
Custom constructor
Utensil constructor
Spoon constructor
Utensil constructor
Fork constructor
Utensil constructor
Knife constructor
Plate constructor
DinnerPlate constructor
PlaceSetting constructor
由上可知,首先调用PlaceSetting
超类构造方法,然后构建对象成员:先调用Spoon
超类构造方法,Utensil
,同样的次序构造Fork
Knife
Plate
;调用DinnerPlate
超类Plate
构造方法再调用DinnerPlate
构造方法。
3.7 小结
-
介绍了Java语言类的重用机制,形式可以是继承或组合。继承表达了一种从属关系,“是一种”“是一个”;组合表达了一种包含关系,是一种部件组装的思想,两者都实现了类的重用,使我们可以在已有类的基础上设计新的类,提高了软件开发的效率,并且程序的可靠性和稳定性会更好。
-
Object
类的主要方法。Object
类是Java中所有类的直接或间接的超类,在整个类继承体系的最上端。因此,在Object
类中规定了所有类都必须具有的属性和行为。但从Object
类中直接继承的行为和功能不一定好用,需要我们自己去覆盖,如equals
等方法,但是终结方法、静态方法等不能覆盖。 -
终结类和终结方法的特点和语法。
-
抽象类和抽象方法的特点和语法。无法实现方法体,在超类中声明抽象方法。抽象方法的具体实现留给子类或子类的子类直至某一级子类