继承
类、超类、子类
通过extends
关键字来实现类的继承
class Manager extends Employee
{
...//Add some new methods and new fields
}
此时,Manager
为Employee
的子类,Employee
为Manager
的超类Manager
会继承Employee
的所有方法和域
重写类方法、访问超类的私有域
重新定义一个方法,如果与超类的某个方法重名,那么该方法会覆盖掉超类的方法
但是,子类并不能访问超类的私有域
解决方法:
通过超类的方法来访问超类的私有域,
如果子类重写了超类的方法,那么可以通过关键字super
来访问超类的方法
如:
class Manager extends Employee
{
public double getSalary() //超类中也有一个getSalary方法
{
double baseSalary = super.getSalary(); //通过super关键字来访问超类的方法
return baseSalary + bonus;
}
}
C++中,访问超类是用::
操作符的形式
子类的构造器
子类的构造器第一行必须调用超类的构造器对对象进行构造
如:
public Manager(String n, double s, int year, int month, int day)
{
super(n, s, year, month, day); //借助super关键字
bonus = 0; //而外的域初始化
}
如果没有显式地调用超类的构造器,那么将自动调用超类默认的(没有参数)的构造器;
如果超类没有默认的构造器,也没有显示地调用超类的构造器,那么java编译器将会报错
在C++的构造函数中,使用初始化列表语法来调用超类的构造函数
对象的引用变量
如果将对象的引用变量声明为某个类的引用变量,那么它除了可以引用这个类的对象之外,还可以引用它的子类的对象
如:
已知Manager
是Employee
的子类,Employee e;
既可以引用Employee
的对象,也可以引用Manager
类的对象
这种一个对象变量可以指示多种实际类型的现象称为多态(polymorphism)
在运行时能自动选择调用哪个方法的现象称为动态绑定(dynamic binding)
继承层次
由一个类派生出来的所有类的集合称为继承层次(inheritance hierarchy)
从某个特定的类到其祖先的路径称为继承链(inheritance chain)
Java不支持多继承
多态
如前所述,某个类的引用变量可以引用该类的子类的实例,称对象变量是多态的
但是,不能通过这个引用变量访问子类的方法和域
如:
Manager boss = new Manager(...);
Employee e = boss; //Manager是Employee的子类
boss.setBonus(5000); //setBonus()是Manager类特有的方法,Employee没有该方法
e.setBonus(5000); //ERROR!!!! Employee没有这个方法,不能调用;此时可以利用强制类型转换解决
e.getBonus(); //OK,Employee有这个方法,但是Manager重写了这个方法,由于动态绑定,此处会自动调用Manager的方法
注意:不能将一个超类的引用赋给子类变量(如:Manager boss = e;
)
动态绑定、对象方法调用的过程
- 编译器查看对象的声明类型和方法名
编译器会一一列出对象所在类的所有该方法名(包括超类的public方法)的方法(此处还没有检查参数类型) - 编译器查看调用方法时提供的参数类型
编译器会挑出类型匹配的所有方法(不一定要完全匹配,兼容即可)- 方法名和参数类型作为一个方法的签名(唯一证明),而返回类型不是
因此覆盖方法时应保证返回类型是兼容的
- 方法名和参数类型作为一个方法的签名(唯一证明),而返回类型不是
- 如果是
private
、static
、final
方法
编译器则可以准确地知道调用哪一个方法,这种调用方式称为静态绑定 - 程序运行时采用动态绑定调用方法,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法
如果每次调用方法都进行一次搜索,那么时间开销太大了
因此,虚拟机会预先为每个类创建一个方法表(method table),调用方法时虚拟机直接在该表中查找对应的方法
- 动态绑定的特性:无需对现存代码进行修改,就可以对程序进行扩展。
假如为一个类创建子类,那么子类的实例无需对该类进行重新编译即可调用该类的方法
注意:覆盖一个方法时,子类方法的可见性不得低于超类方法的可见性;特别是当超类方法是public时,子类方法必须是public
阻止继承:final类和方法
- final类:不允许扩展(即不允许利用该类定义子类)的类
类中的方法会自动设置为final,而域不会 - final方法:不允许覆盖(重写)的方法
- 如果类中某个方法很简短、被频繁调用且没有真正进行覆盖
那么即时编译器会将其进行内联处理以提高程序效率
如果虚拟机又加载了一个子类,子类对这个方法进行了覆盖,那么编译器会自动取消内联
强制类型转换
- 与内置类型的强制转换形式一致
- 只能在继承称此内进行类型转换
- 子类转换成超类是允许的
正如超类的引用变量能直接引用子类 超类转换成子类不一定允许
如果出错,会引发一个异常,如果没有被捕获,那么出现编译错误
通常需要借助instranceof
运算符来判断该对象是否能成为目标子类的实例
如:(Manager
是Employee
的子类)if( staff[1] instanceof Manager ) { boss = (Manager) staff[1]; }
注意:尽可能避免强制类型转换和instanceof
的使用
在C++中,如果强制转换失败将得到的是null对象,而不会产生异常或是出现编译错误
抽象类
用abstract
修饰符定义抽象类和抽象方法
- 抽象方法相当于“占位”的作用,表明这个类存在这个方法,但类的定义中不给出该方法的具体定义,而在其子类中进行具体的定义
- 包含抽象方法的类必须定义为抽象类,不包含抽象方法的类也可以定义为抽象类
- 抽象类不允许直接产生实例,但其具体子类可以
- 可以定义一个抽象类的引用变量,用它来引用其子类的实例
如:
abstract class Person //包含抽象方法,必须定义为抽象类
{
private String name;
public Person(String n)
{
name = n;
}
public abstract String getDescription(); //抽象方法
public String getName()
{
return name;
}
}
C++中,在方法定义的尾部加上=0
来表示一个纯虚函数(抽象方法);包含抽象方法的类自动作为抽象类
抽象类的作用:
假设Student
和Employee
为Person
的子类Student
和Emloyee
中都定义了getDescription
方法
变量Student s = new Student
和Employee e = new Employee
都可以调用这个方法
但是Person p = s
或Person p = e
则不行(因为Person
类不具有这个方法)
如果给Person
类添加一个getDescription
抽象方法,那么变量p
就能调用这个方法(即时编译器会自动确定调用哪一个类的方法)
受保护访问
使用protected
的实例域对所有子类及同一个包的所有其他类都可见
该实例域可以被子类的方法访问
假设p域为A类的protected域,B类和C类为A类的子类
但B类中的方法只能访问B对象的p域,而不能访问C类的p域
注意:该修饰符应当谨慎使用;一定程度上破坏了oop的数据封装原则
C++中,保护域仅对子类可见,这一点上Java的protected安全性比较低
控制可见性的访问修饰符归纳
private
:仅对本类可见(常用于实例域)public
:对所有类可见(常用于方法)protected
:对本包和所有子类可见(常用于特殊的实例域)- 默认:本包可见(尽量避免)
Object:所有类的超类
每个类均由Object类扩展而来,但无需显式的书写extends Object
可以创建一个Object类的引用变量来引用任何类型的对象
所有类都拥有一些通用方法,这些方法定义在Object类中
C++中没有所有类的根类,但每个指针都可以转换成`void `指针*
equals方法
该方法不是标准库提供的方法,需要自己编写
检查两个对象是否相等(状态相同)
形式:objectA.equals(objectB)
,返回布尔值
一般实现机制:
- 检查两个对象是否为同一个引用
- 检查参数里的对象是否为null
- 检查两个对象是否为同一个类
- 检查两个对象的实例域是否完全相等
如果在子类中定义equals方法,那么它必须显调用超类的equals方法
class Manager extends Employee
{
...
public boolean equals(Object otherObject)
{
//检查是否属于同一个类
if( !super.equals(otherObject) )
return false;
Manager other = (Manager) otherObject;
//检查实例域是否完全相等
return bonus == other.bonus;
}
}
相等测试与继承
- Java语言规范要求equals方法具有以下特性
- 自反性:
x.equals(x)
应该返回true - 对称性:若
x.equals(y)
为true,则y.equals(x)
也应该为true - 传递性:若
x.equals(y)
和y.equals(z)
为true,则x.equals(z)
也应该为true - 一致性:若x和y引用的对象不变,那么反复调用
x.equals(y)
应该返回相同的结果 - 对于任意非空引用x,
x.equals(null)
应该返回false
- 自反性:
- 编写equals方法的建议
- 显式参数命名为
otherObject
,类型为Object
(否则无法覆盖Object.equals()
),之后将其转换为一个叫做other
的变量 - 检测
this
和otherObject
是否引用同一个对象
if(this == otherObject) return true;
- 检测
otherObject
是否为null
if(otherObject == null) return false;
- 检测
this
与otherObject
是否属于同一个类- 如果equals在每个子类中语义不同:
if(getClass() != otherObject.getClass()) return false;
- 如果语义统一
if(!(otherObject instanceof ClassName)) return false;
- 如果equals在每个子类中语义不同:
- 将
otherObject
转换成相应的类的类型变量
ClassName other = (ClassName) otherObject;
- 用
==
比较所需的基本类型域,用equals
比较所需的对象域
如果都匹配,返回true,否则返回false - 如果在子类中重定义equals,则必须调用
suer.equals(other)
- 显式参数命名为
- 对于数组类型的域,可以使用静态的
Arrays.equals
方法检测两个数组是否相等
甚至可以用于对象数组,它将自动调用对应类的equals
方法 - 可以使用
@Override
对覆盖超类的方法进行标记
当尝试定义一个新的方法(即不小心出现错误)的时候,编译器会报错
如:@Override public boolean equals(Object otherObject)
hashCode方法
散列码是由对象导出的一个整型值,是没有规律的
一般两个不同的对象的散列码不同
Object.hashCode()
导出的是对象的存储地址- 自定义类可以自己重写
hashCode
方法 String.hashCode()
的散列码是由字符串内容计算得到hashCode()
与equals()
的结果应统一
即当x.equals(y)
返回true时,x.hashCode()
与y.hashCode()
的结果应该一致Objects.hashCode(Object a)
(注意是Objects不是Object)
当a为null时返回0,否则返回a.hashCode()
散列码的计算应合理组合实例域的散列码,以便能够均匀地产生散列码
也可以直接借助Objects.hash
来生成散列码
如利用实例域x
、y
、z
组合生成散列码:public int hashCode() { return Objects.hash(x, y, z); }
toString方法
用于返回表示对象值的字符串
如Point.toString()
返回java.awt.Point[x=10,y=20]
通常toString方法都以类名+域值的形式返回
- 标准库中大多数类都提供了
toString
方法 - 自定义类最好也能提供这个方法,以便后期调试
- 当一个对象与字符串相加时,系统会自动调用toString方法
如"abcd" + x
相当于"abcd" + x.toString()
- 当用
System.out.println(x)
打印对象x时,也会自动调用toString方法
即System.out.println(x.toString())
Object.toString()
返回对象的类名与散列码
如System.out.println(System.out)
返回java.io.PrintStream@2f6684
- 但是,数组直接继承了object类的toString方法
也就是说
int[] luckNumbers = {2, 3, 5};
String s = "" + luckNumbers;
得到的s将会是I@1a46e30
(I表示整型数组)- 此时我们可以调用静态方法
Arrays.toString()
来打印
如String s = "" + Arrays.toString(luckNumbers);
- 如果要打印多维数组,那么应调用
Arrays.deepToString
方法
- 此时我们可以调用静态方法
一般的
toString
方法的定义方式public String toString() { return getClass().getName() //获取类名 + "[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]"; }
泛型数组列表
Java允许定义数组时用变量来确定数组大小
而ArrayList
类又提供了一个动态数组,称为数组列表ArrayList
类是一个采用类型参数(type parameter)的泛型类(generic class)
- 数组列表管理着对象引用的一个内部数组
当内部数组占满后如果再往其中添加内容,数组列表会自动创建一个更大的数组,并把数据拷贝到新的数组上
//创建数组列表时需要指明对象的类 ArrayList<Employee> staff = new ArrayList<Employee>(); //等号右边可以不指明对象类型 ArrayList<Employee> staff = new ArrayList<>(); //可以对数组列表调用“add”方法添加对象 staff.add(new Employee(...)); //如果能提前估计对象的熟练,可以在填充数组前,调用“ensureCapacity”方法指定内部数组的大小,当超过这个大小时会自动创建一个更大的数组(这是与普通数组的区别) staff.ensureCapacity(100); //也可以在初始化时直接指定内部数组的大小 ArrayList<Employee> staff = new ArrayList<>(100); //调用“size”方法返回内部数组的实际大小 //与普通数组的“length”方法类似 staff.size(); //当确认数组大小不再发生变化时,可以调用“trimToSize”回收多余空间 //如果大小固定后再次添加元素,那么将会移动存储块 staff.trimToSize();
在老版本的Java中,程序员采用Vector类实现动态数组,但ArrayList类更加有效
在C++中,利用vector模板来实现动态数组。C++中重载了[]运算符,Java中必须显式调用;C++中向量是值传递的,即a=b会拷贝一个新的给a,Java中ArrayList类是引用的传递,即a和b引用同一个数组列表
访问数组列表元素
staff.set(i, harry); //类似于数组的staff[i] = harry;
Employee e = staff.get(i); //类似于数组的e = staff[i];
staff.add(i, e); //把e插入到i位置,i~(size()-1)的元素后移
Employee e = staff.remove(i); //移除i位置的元素并返回,类似python的pop
staff.toArray(a); //把数组列表staff的内容填入普通数组a
for(Employee e : staff) //可以利用for each遍历数组列表
{
...
}
类型化与原始数组列表的兼容性
P186~187,此处并没有看懂!
对象包装器与自动装箱
有时候需要将基本类型转换成对象使用
这些类称为包装器(wrapper)
- 常见对象包装器类
- Interger
- Long
- Float
- Double
- Short
- Byte
- …以上六个对象包装器类又派生于Number类
- Charactrer
- Void
- Boolean
- 对象包装器类是不可变的
- 对象包装器类是不可扩展的(final类)
- 常用于构造数组列表
数组列表是不支持基本类型数据的
此时可以借助对象包装器类实现
如ArrayList<Interger> list = new ArrayList<>();
- 为数组列表添加修改元素时会自动装箱(autoboxing)
如:list.add(3)
会自动替换成list.add(Interger.valueOf(3))
- 表达式中绝大多数的对象包装器能够自动装箱和拆箱
如:
int n = list.get(i);
等价于int n = list.get(i).intValue();
Interger n = 3;
、n++
等也会自动装箱和拆箱 - 但是对于
==
运算符,对象包装器不会自动拆箱
如:a、b为某对象包装器的实例
if(a==b) ...
判断的是a和b是否为同一个引用
if(a.equals(b)) ...
才能实现两个值的比较 - 对象包装器类不能实现方法的传递的参数可变
因为对象包装器类是不可变的
可以借助org.omg.CORBA
包中定义的持有者类型(holder)
如:IntHolder、BooleanHolder等
持有者类型都包含一个公有值域value
!!!可以直接访问这个域
常见方法——
//java.lang.Interger,其他对象包装类的方法类似
int intValue() //以int的形式返回对象的值
static String toString(int i) //以String对象的形式返回i的十进制表示
static String toString(int i, int radix) //同上,但以radix进制表示
static int parseInt(String s) //以十进制将s转换为整型
static int parseInt(String s, int radix) //同上,以radix进制
static Interger valueOf(String s) //以十进制将s转换成Interger类的实例
static Interger valueOf(String s, int radix) //同上,以radix进制
//java.text.NumberFormat
Number parse(String s) //将s转换成数值并返回
参数数量可变的方法
在参数类型之后加上三个句点.
即表示可接受任意数量的该类型参数
如printf
方法的定义public PrintStream printf(String fmt, Obejct... args){}
剩下的这些参数会依次放入args数组中
枚举类
- 由
enum
关键字定义的枚举类型实际上是由枚举类实现的 - 构造器值在构造枚举常量时被调用
- 所有的枚举类型都是Enum类的子类
在枚举类型中可以添加一些构造器、方法、域
例如:public enum Size { SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL"); private String abbreviation; private Size(String abbreviation) { this.abbreviation = abbreviation; } public String getAbbreviation() { return abbreviation; } }
常用方法——
//java.lang.Enum<E>
static Enum valueOf(Class enumClass, String name) //返回指定类、指定名字的枚举常量
static toString() //枚举常量名
int ordinal() //返回枚举常量在enum声明中的位置(从0开始)
int compareTo(E other) //如果枚举常量在other前,返回负值;如果this==other,返回0;如果在other后,返回正值
反射
能够分析类能力的程序称为反射(reflective)
- 反射机制用来——
- 在运行中分析类的能力
- 在运行中查看对象
- 实现通用的数组操作代码
- 利用Method对象(类似C++中的函数指针)
反射机制通常用于工具构造,而不是应用程序设计
此节暂时跳过
P192-212
继承设计的技巧
- 将公共操作和域放在超类
- 不要使用受保护域
protected机制并不能很好的保护域
第一,子类集合是无限制的,任何人都能派生一个子类并编写代码来直接访问受保护域,破坏了封装性;
第二,Java中,同一个包中所有类都可以访问受保护域
受保护域一般用于指示那些不提供一般用途,而应在子类中重新定义的方法 - 只使用继承来实现”is a“关系
不滥用继承 - 除非所有继承的方法都有意义,否则不要使用继承
- 在覆盖方法时,不要改变预期的行为
即不要偏离最初设计的想法 - 使用多态,而非类型信息
如if(x...)...else if(x...)...
(用来判断x的类型,来执行相应的动作)应放在同一个方法里 - 不要过多地使用反射
反射在编译时很难发现错误