3.1子类和父类
继承
在Java中,你可以看见这样一种操作:一个类可以获得另一个类的属性(比如方法,字段等)。这样的机制在java中被叫做继承。
相应的,加入A类继承了B类中的方法和字段,那么我们称A类(也就是继承其他类属性的类)为子类(Subclass),而称B类(被继承的类)为父类(Superclass)。
extends关键字
对于继承,java定义类extends这个关键字,用于表达两个类之间的继承关系:
class Superclass{
//......
}
class Subclass extends Super calss{
//......
}
由上图所示,extends关键字一般放在子类后面,声明其继承的父类是谁。
在完成这一系列操作后,子类中必然包含父类中的所有成员(包括private修饰的)
tips:这里就要解释一下:如上所述,子类必然继承父类的所有成员,只不过对于private修饰的对象,子类无法直接访问和看见,但是可以通过调用父类本身的方法来获取和调用这些对象。
protected以及包访问权限
protected关键字与private作用相当,唯一的区别在于子类可以访问父类中被protected修饰的成员(直接访问,不需要通过父类方法)。
除了public,private和protected之外,Java还有一种默认访问权限——包访问权限。他的作用我们之前提过,也就是在同一个包(package)下的类可以直接互相访问彼此之间除了private修饰的所哟其他成员。
重写(覆写)Overriding
有些时候我们会发现,父类中的方法无法很好的满足子类想要实现的某些功能,我们需要一个新的方法来覆盖掉从父类中继承而来的方法:
public class Manager extends Employee{
...
public double getSalary(){
double baseSalary = super.getSalary();
return basSalary + bonus;
}
...
}
我们假定父类Employee中含有一个方法getSalary(),那么上述代码中子类Manager中对getSalary方法的改写行为就叫做重写(注意,不要和Day2中提到的重载混淆)。
在子类中使用关键字super+任意父类方法名便可以调用父类中的该方法。类似的,在子类中使用this关键字可以强调使用的是自身的方法(便于区分)。
子类构造器
子类继承了所有父类内部的成员(实例字段,方法)。构造器不作为成员,故它们无法被子类继承。但是,父类构造器可以被子类所调用。
每当创建一个子类时,java都会自动在子类中创建一个默认构造器,而在这个构造器中,自动调用了父类构造器。
就像这样:
class Manager extends Employee{
Manager(){
super();
...
}
}
继承的注意事项
- 一个类可以被多个类所继承,也就是说一个父类可能会有一个或多个子类
- 一个类只能继承一个父类,也就是说一个子类只能对应一个父类
- 已经是子类的类可以被其他类所继承
和C/C++语言不一样,java不支持一个子类拥有多个父类(多重继承)
多态
我们可以像这样创建一个子类对象赋值给父类变量:
Employee e;
e = new Employee(...);
e = new Manager(...);
在java中,对象变量是多态的(多态:暂时理解为多种表现形态就行)
父类变量可以被赋值父类对象和任意子类对象,而子类变量不能赋值父类对象
可以看出来,在继承中对象的赋值是向下的。
动态绑定
我们假定父类Employee中存在以下几个方法:
- getName()
- getSalary()
- getHireDay()
- rasieSalary(double)
与此同时,子类Manager中重写了 getSalary方法并且新增了一个setBounds(double)方法。
那么此时,当子类对象和父类对象分别调用这些方法的时候,我们会发现结果如下表所示:
调用方法 | Employee | Manager |
getName() | Employee.getName() | Employee.getName() |
getSalary() | Employee.getSalary() | Manager.getSalary() |
getHireDay() | Employee.getHireDay() | Employee.getHireDay() |
raiseSalary(double) | Employee.raiseSalary(double) | Employee.raiseSalary(double) |
setBonus(double) | 报错,因为没有该方法 | Manager.setBonus(double) |
发现了吗,java会根据子类是否重写父类方法来选择调用父类方法还是子类方法。这被称之为动态绑定。这使得原代码得到了延伸,而编码者可以省去很多修改的时间。
继承阻碍:final类和方法
正如小标题所说的那样:
被final关键字修饰的类无法被继承;被final修饰的方法无法被覆写(但是可以被继承)
强制转换
正如基本类型之间的强制转换那样,引用类型之间也可以通过强制转换的方法来改变数据的类型,当然,也会造成一定的数据损耗或丢失就是了。
让我们用一段实例代码来回顾一下刚才的所学:
//定义一个父类Calculation,有求和求差两方法
class Calculation{
int z;
//求和
public void add(int x,int y) {
z = x + y;
System.out.println("两数和是:"+z);
}
//求差
public final void subtract(int x,int y) {
z = x - y;
System.out.println("两数差是:"+z);
}
}
//子类Try,继承了父类calculation,重写了求和方法并新增了求积方法
public class Try extends Calculation{
//求积方法
public void multiply(int x,int y) {
z = x - y;
System.out.println("两数乘积是:"+z);
}
//重写求和
@Override
public void add(int x,int y) {
z = x + y;
System.out.println("我不告诉你");
}
//报错,因为final修饰不能重写
/*public void subtract(int x,int y) {
z = x - y;
System.out.println("我不听"+z);
}*/
//应用代码
public static void main(String[] args) {
int a = 20,b = 10;
//多态,分别定义子类父类两者的对象
Calculation demo = new Try();
Calculation demo2 = new Calculation();
demo.add(a,b);
demo.subtract(a,b);
demo.multiply(a,b);
System.out.println("-------------------分割--------------------");
demo2.add(a,b);
demo2.subtract(a,b);
//报错,因为父类对象无法调用子类新定义的方法
/*demo2.multiply(a,b);*/
}
}
最后输出结果如下:
我不告诉你
两数差是:10
两数乘积是:10
-----------------分割--------------------------
两数和是:30
两数差是:10
3.2抽象类
就好像静态方法那样。首先,抽象类顾名思义,是对由abstract关键字修饰的类的统称。他的用处是:
抽象类中可以编写和存放抽象方法,而一般类不行。
那么问题又来了,什么是抽象方法?
抽象方法
抽象方法是位于抽象类中的,被abstract关键字所修饰的,没有方法体的方法。如下所示:
public abstract class Person{
private String name;
public Person(String name){
this.name = name;
}
public abstract String getDescription(); //抽象方法
public String getName(){
return name;
}
}
像图中getDescription方法这样,在abstract类中,被abstract修饰且方法内没有具体代码的方法,就被叫做抽象方法(再次强调,要三者同时满足,缺一不可)。
关于抽象类
- 与一般的类不同,抽象类不可以创建实例对象,也就是说,用new来创建抽象类的对象是不合法的操作。
- 如果有一个类继承了抽象方法,那么这个子类就必须继承并实现这个抽象方法(补全方法体)
3.3Object——始祖类
正如他的名字那样,Object类是java中所有类的始祖:所有的类都继承了Object类。但是,在编写代码的过程中我们从来不会写某个类extends Obect。
重写equals方法(详)
在Day1中我们提到,==通常用于比较两组基本类型数据大小是否相同,但不能用在引用类型上。
究其原因。一般来说基本类型(如int,double)的初始化都是用=后面接一个数据直接进行赋值。这样的初始化方式我们称为静态初始化。静态初始化的数据在一开始都被放在一个名为“静态数据区”的地方,然后以此时的位置为存储地址,完成对该变量的赋值。而在这时,后面以同样的方式初始化的变量在被赋值的时候,虚拟机会查询一次静态数据区内的数据是否与即将存储的数据有相同的值。如果有,虚拟机将不会把新输入的值开辟一个新的位置,而是直接使用这个原有的,相同的数据地址给这个新的变量赋值。
也就是说,在进行一系列的操作后,通过静态初始化方法赋值出来的,拥有同样内部值的量个不同变量所指向的是同一个位置的数据。
这也就是为什么==是一个比较两者存储地址的符号能够完成比较两者内容是否相同的工作。
(像String s ="sss";这样赋值出来的引用数据类型String也会被==比较出true就是这样来的)
(之后会配图)
在Object类中实现的equals方法被用于确定两个对象的引用是否相同。
普通的equals方法就是单纯的比较两者的引用地址:
boolean equals(Object obj){
return (this==obj);
}
然而,当两个对象具有相同的状态时,例如,如果两个雇员具有相同的姓名,工资和雇佣日期(实际指的是同一个员工),如果想让程序认为他们是相等的,那么就需要重写equals方法。
以下是重写equals方法的示例代码:
boolean equlas(Object otherObject) {
if(this==otherObject) { //如果两者的存储地址相同,返回true
return true;
}
if(otherObject==null) { //如果对比对象为空,直接返回false
return false;
}
if(getClass()!=otherObject.getClass()) { //getClass()返回一个类的类型?
return false;
}
/*
* 类名1 other = (类名1)otherObject;
* return 实例字段1.equals(other.实例字段1)&& (以此类推....)
*
* 这一段句代码是逐个比较每一个字段的值是否相等
*/
}
下面那段注释位置根据不同类所拥有的,需要比较的不同字段而更改。
重写toString方法
另外一个重要的方法是toString方法。作用是将需要的数据转化为String类型,让他能够以文字形式输出。由于其常用性,我们也需要对其进行重写。
普通的toString方法是返回类名或者引用类型的引用地址。
当我们打印这个对象的引用时,实际上是默认在调用toString方法
以下两句等价:
System.out.println(p);
System.out.println(p.toString());
以下是重写toString代码:
public String toString() {
return "一段你想返回的具体字符串";
}
3.4泛型数组列表
当我们int[] a;创建一个静态数组时,很容易发现他的缺点:我们无法动态的改变数组大小.
这时候就绪哟用到泛型数组列表:
ArrayList<Employee> staff = new ArrayList<Employee>();
staff.add(...);
staff.remove(...);
如上所示,我们就可以通过利用ArrayLIst中自带的方法来完成添加或者删除数据等操作。
下面列举一些ArrayList常用的方法:
- add() //添加
- remove() //删除