4.1 子类与父类
上面我们学会了类,也明白了类编写的规则,体会到了面向对象的一个特性:封装(数据的封装),接下来我们学习第二个特性:继承。什么是继承呢?看下面例子你就明白了。
描述:编写三个类:鱼类,鲨鱼类,鲸鱼类。
普通方式:
-
鱼类:
class Fish{ float weight; //鱼体重 float longth; //鱼长度 //获取体重 public float getWeight(){ return this.weight; } //获取长度 public float getLongth(){ return this.longth; } // 打印鱼类 public void speak(){ System.out.println("Fish class"); } }
-
鲨鱼类:
class Shark{ float weight; //鱼体重 float longth; //鱼长度 //获取体重 public float getWeight(){ return this.weight; } //获取长度 public float getLongth(){ return this.longth; } // 打印鲨鱼类 public void speak(){ System.out.println("Shark class"); } }
-
鲸鱼类:
class Whale{ float weight; //鱼体重 float longth; //鱼长度 //获取体重 public float getWeight(){ return this.weight; } //获取长度 public float getLongth(){ return this.longth; } // 打印鲸鱼类 public void speak(){ System.out.println("Whale class"); } }
-
观察上面三个类,很容易就发现,鲨鱼类、鲸鱼类与鱼类,有很多地方是一样的:体重、长度、获取体重、获取长度。不一样的是:打印方法speak。所以我们可以发现,上面编写三个类的方式就会显得很冗余,而且从现实角度来说,鲨鱼和鲸鱼都属于鱼。那么我们可以想:既然已经编写了鱼类,鲸鱼类和鲨鱼类可不可以直接继承编写好的鱼类:属性:weight、longth;方法:getWeight、getLongth。然后这两个类再各自写不一样的方法:speak 呢?答案是可以的,这就是类的特性:继承,让我们来看看如何利用继承来实现这三个类吧。
使用继承编写方式:
-
鱼类:
class Fish{ float weight; //鱼体重 float longth; //鱼长度 //获取体重 public float getWeight(){ return this.weight; } //获取长度 public float getLongth(){ return this.longth; } // 打印鱼类 public void speak(){ System.out.println("Fish class"); } }
-
鲨鱼类:
class Shark extends Fish{ public void speak(){ System.out.println("Shark class"); } }
-
鲸鱼类:
class Whale extends Fish{ public void speak(){ System.out.println("Whale class"); } }
-
从上面可以看出,使用了继承之后,鲨鱼类和鲸鱼类编写量就少了很多,却和普通方式编写方式都实现了这三个类。在这里面,鲨鱼和鲸鱼继承了鱼类,鲨鱼和鲸鱼就叫做鱼类的子类,鱼类叫做鲨鱼和鲸鱼的父类,子类需要使用extends关键字去继承父类。
-
子类继承父类格式:
-
class 子类 extends 父类 {}
class Whale extends Fish{ public void speak(){ System.out.println("Whale class"); } }
-
-
类的树形结构:
- Object是所有类的祖先类,任何类都是Object类的子孙类,若一个类声明时没有使用extends关键词,默认是Object的子孙类。class A 等同于 class A extends Object。
- 每个类(除了Object)有且仅有一个父类,一个类可以有零个或多个子类。
5.2 继承性
从上面我们了解了继承,那么子类去继承父类的时候,是完全继承父类的属性和方法呢?还是仅仅只有一部分呢?所以继承得有他的继承规则,这就是继承性
- 子类和父类在同一包中,子类继承父类不是private的成员变量和方法。继承后的成员变量和方法,访问权限不变。
- 子类和父类不再同一包中,子类只继承父类public和protected员变量和方法。
- protected的进一步说明:
- 如果D类再D本身中创建了一个对象,这个对象总是可以通过“.”运算符访问继承的或自己定义的protected变量和protected方法。
- 如果在另一个类中,如在Other类中用D类创建了一个对象d,那么d对象通过".”运算符去访问protected变量和protected方法时,就得遵守以下两条规则:
- 对于D类自己声明的protected变量或方法,如果Ohter类和D类在同一包中,d对象可以访问。
- 对于D类继承的protected变量或方法,需要追溯到这些protected变量或方法所在的"祖先"类,例如A类,只要A类和Ohter类在同一包中,d对象就能访问继承的protected变量或方法
5.3 子类与对象
上面我们已经学了继承的规则,那么继承如何实现的呢?当用子类的构造方法创建了一个对象,不仅子类中声明的成员变量分配了空间,父类的成员变量也都分配了内存空间,但仅仅只将子类继承的那部分成员变量分配给子类对象。父类的private成员变量也分配了空间,但是没有分配给子类对象。而如果子类和父类不在同一个包中,父类的友好成员变量也不会分配给子类对象。
那么在子类创建了对象之后,其父类也分配了内存空间,父类的private成员变量不会分配给这个对象,我们没有使用父类创建对象,所以private成员变量也不属于父类的某个对象,那么private成员变量似乎浪费了空间对吧。实际情况并非如此,因为子类中有些方法是从父类继承的,这部分方法可以操作这部分未继承的变量。
案例:
class People {
private int averHeight = 166;
public int getAverHeight(){
return averHeight;
}
}
class ChinaPeople extends People{
int height;
public void setHeight(int h){
height=h;
}
public int getHeight(){
return height;
}
}
public class Example5_2 {
public static void main(String[] args) {
ChinaPeople zhangSan = new ChinaPeople();
System.out.println("子类未继承的averHeight: "+zhangSan.getAverHeight());
zhangSan.setHeight(160);
System.out.println("子类对象的实例变量height: "+zhangSan.getHeight());
}
}
5. 4 instanceof运算符
instanceof 运算符是一个双目运算符,用来判断数据的数据类型
左边的操作元是对象,后面的操作元是类。
当左边的操作元是右边的类或者子类创建的对象时,instanceof运算结果为true,否则为false。
- 使用格式: 对象 instanceof 类
案例:
class Example{
public static void main (String[] args){
String s1 = new String("123");
System.out.println(s1 instanceof String);
}
}
5.5 成员变量的隐藏和方法重写
-
成员变量的隐藏
什么是成员变量的隐藏呢?
我们在写子类的时候,如果在子类中声明了一个成员变量名字和继承的成员变量名字相同,那么子类就会隐藏所继承的成员变量。
特点:
- 子类仍然可以调用从父类继承过来的方法操作被隐藏的成员变量。
- 子类对象和子类自定义的方法去访问与父类同名的成员变量指的是访问的子类自己声明的成员变量。
-
方法重写
什么是方法的重写?
如果我们在子类中定义了一个方法,这个方法和从父类中继承的某个方法,类型、返回值、方法名、参数完全相同,这就叫做方法重写。
-
重写的目的:可以隐藏继承的方法,把父类的状态和行为改变为自身的状态和行为。
-
如果父类f()可以被继承,子类就有权利重写f(),一旦重写,就会隐藏掉父类的f(),子类对象调用的f()一定调用的重写f(),如果子类没有重写f(),调用f()就是调用父类的f()。
-
重写方法可以操作继承的成员变量,调用继承的方法,也可以操作子类新声明的成员变量和调用新定义的方法,但无法操作被子类隐藏的成员变量和方法(想操作的话得用super关键字,后面会提到)
案例:
package 子类与继承.重写方法; class University{ void enterRule(double math,double english,double chinese){ double total = math+english+chinese; if(total>=180){ System.out.println(total+"分数达到大学录取线"); } else{ System.out.println(total+"分数未达到大学录取分数线"); } } } class ImportantUniversity extends University{ void enterRule(double math,double english,double chinese){ double total = math+english+chinese; if(total>=220){ System.out.println(total+"分数达到重点大学录取线"); } else{ System.out.println(total+"分数未达到重点大学录取分数线"); } } } public class Example { public static void main(String[] args) { double math=62,english=76.5,chinese=67; ImportantUniversity univer=new ImportantUniversity(); univer.enterRule(math,english,chinese); //调用重写的方法 } }
注意:子类重写方法时,不能降低访问权限,但是可以升高访问权限(访问权限降序排序:public protected 友好的 private)
-
5.6 super关键字
之前我们提到,通过子类创建对象的时候,父类的成本变量和方法都被分配了内存,但是子类只继承了一步,有一部分没有继承,被隐藏了,子类对象不能去访问隐藏的成员变量和调用隐藏的方法,似乎这部分浪费了内存空间。那么我们如何访问这这部被隐藏的变量和方法呢?
在java里,子类一旦隐藏了继承的成员变量和方法,子类对象就不再拥有这些变量和方法,这些变量和方法就归位super关键字所有,当我们需要访问的时候,就可以通过super关键字去访问。
- 比如在子类中访问被隐藏的成员变量x,就可以通过 super.x 去访问。
- 比如在子类中调用被隐藏的方法f(),就可以通过super.f()去调用。
案例:
class A{
float a;
float f(){
this.a = 5.0f;
System.out.println("this setA function belong to class A ");
return this.a;
}
}
class B extends A{
float a;
float f(){
float c;
super.a=a;
c= super.f();
return c/a;
}
float g(){
float c;
c= super.f();
return c;
}
}
public class superExample {
public static void main(String[] args) {
B objecta= new B();
objecta.a=100;
float resultone = objecta.f();
float resulttwo = objecta.g();
System.out.println(resultone);
System.out.println(resulttwo);
}
}
-
使用super关键字调用父类构造方法
子类是不继承父类的构造方法的,当用子类构造方法创建子类对象的时候,子类构造方法总是优先用super关键字调用父类某个构造方法,如果没有明显写出是哪个构造方法,默认调用父类默认构造方法,用:super()调用。
- 调用默认构造方法:super ();
- 调用有参构造方法:super(参数1,参数2···)
案例:
class Father{
public String name;
Father(){
this.name="fault";
}
Father(String name){
this.name=name;
}
}
class Son1 extends Father{
Son(){
super()
System.out.println(super.name);
}
}
class Son2 extends Father{
Son(){
super("Alex")
System.out.println(super.name);
}
}
public class Example{
public static void main (String[] args){
Son1 son1 = new Son1(); // 输出 falut
Son2 son2 = new Son2(); // 输出 Alex
}
}
5.7 final关键字
final可修饰类、成员变量和方法中的局部变量
-
final类
在class关键字前面加上final,这个类就成为了final类,final类不允许被继承。
为什么要有final类呢?
有时候是处于安全性的考虑。例如java.lang包中的String类,对编译器和解释器的正常运行有很重要的作用,不允许用户扩展String类,所以将String类写为了final类。
-
final方法
当用final修饰父类中的方法,那么子类就不允许重写这个方法(乖乖地给爹继承,不可以做任何篡改)。
-
final变量
当成员变量或者局部变量使用final修饰,这个变量就变为了常量,不允许再对其值进行修改。
因为后面不允许修改值,所以在用final声明常量时必须指定值。
案例:
final class A{
final double PI=3.1415926; //PI就是一个常量
public double getArea(final double r){
// r=r+1; // 非法操作,r是final变量,不允许修改值。
return PI*r*r;
}
public final void speak(){
System.out.println("您好,how's everything here?");
}
}
//class B extends A{ //A是final类,不允许继承。
//
//}
public class Example {
public static void main(String[] args) {
A a = new A();
System.out.println("area: "+ a.getArea(3.13d));
a.speak();
}
}
5.8 对象的上转型对象
什么是对象的上转型对象呢?
回答这个问题之前,我们可以看看这个例子:
狗是动物,猫也是动物,可以说狗类是动物类的子类,猫类也是动物类的子类。
猫发出的叫声是“喵~~~”
狗发出的叫声是“汪汪汪~”
我们编写动物类的时候,可以编写一个speak()方法表示动物的发出叫声行为。
然后编写猫类、狗类的时候各自重写speak()方法表示猫的叫声,狗的叫声。
-
代码实现:
class Animal{ public void speak(){ System.out.println("动物~"); } } class Cat extends Animal{ public void speak(){ System.out.println("喵~~~"); } } class Dog extends Animal{ public void speak(){ System.out.println("汪汪汪~"); } } public class Example { public static void main(String[] args) { Animal animal = new Animal(); Cat cat = new Cat(); Dog dog = new Dog(); animal.speak(); cat.speak(); dog.speak(); } }
运行效果:
上面代码通过创建Animal类对象animal、Cat类对象cat和Dog类对象dog,然后各自调用自身speak方法实现发出三种叫声的行为了,但是这样子似乎有丢丢麻烦,这时候,咱们本节的主题:对象的上转型对象就登场了,接下来看看如何实现吧。
public class Example {
public static void main(String[] args) {
Animal animal; // 声明父类animal对象。
animal = new Cat(); // 创建子类Cat对象,并赋值给父类animal对象。
animal.speak(); // 调用speak()方法
animal = new Dog(); // 创建子类Dog对象,并赋值给父类animal对象。
animal.speak(); // 调用speak()方法
}
}
运行效果:
可以看出,这样子也实现发出不同子类的叫声。
在这里,我们声明了一个父类Animal对象animal,然后用子类Cat创建了对象并把这个对象的引用放到animal中,这时候用animal调用speak(),发出的叫声是“喵~~~”,而不是“动物~”,这时候,我们称animal对象是Cat类对象的上转型对象。后面又用子类Dog创建了对象并把引用放到animal中,调用speak(),发出的叫声是“汪汪汪~“,称animal是Dog类对象的上转型对象。(tips:这其实就是面对对象编程另一特点:多态。)
上转型对象的实体是由子类创建的,所以它失去原对象的一些特点,拥有以下特点:
特点:
- 上转型对象不能操作子类新增的成员变量,不能调用子类新增的方法。(失去部分属性和行为)
- 上转型对象可以访问子类继承或隐藏的成员变量,也可以调用子类继承或重写的实例方法。
注意:
- 不要将父类创建的对象和子类对象的上转型对象混淆。
- 可以将对象的上转型对象强制转换到一个子类对象,这时,该子类对象又具备了子类所有属性和功能。
- 不可以将父类创建的对象的引用赋值给子类声明的变量。
- 如果子类重写了父类的静态方法,子类对象的上转型对象不能调用子类重写的静态方法,只能调用父类的静态方法。
5.9 多态
什么是多态呢?
当一个类有多个子类,这些子类都重写了父类的某个方法时,把子类创建的对象的引用放到一个父类的方法时,就得到了该对象的上转型对象,当用这个上转型对象调用这个方法时,就可能得到了多种形态。像我们上面举例子的Animal类、Dog类和Cat类,当我们使用Dog类的上转型对象animal调用speak时,发出的是“汪汪汪~”,而把Cat类对象的引用放到animal上,又去调用speak时,发出的是“喵~~~”,这就是多态!!!
官方点解释:多态性就是指父类的某个方法被其子类重写时,可以各自产生自己的功能行为。
5.10 abstract类和abstract方法
abstract类–>抽象类
abstract方法–>抽象方法
-
abstract类
用abstract修饰的类就成为abstract类。
如:
abstract class A{ .... }
-
abstract方法
用abstract修饰的方法称为abstract方法。
如:
abstract int min(int x,int y);
abstract方法只能声明,不可以实现,就是不能写方法体,方法体由子类去重写。
特点:
-
abstract类可以有abstract方法(非abstract类不允许有abstract方法),也可以有非abstract方法。如:
abstract class A{ abstract int min (int x,int y); int max(int x,int y){ return x>y?x:y; } }
-
abstract类不能用new运算符创建该类对象。
某个非抽象类如果是抽象类的子类,就必须重写父类的抽象方法。
可以看出,抽象类是需要被继承的,所有不能用final修饰抽象类。
-
abstract类的子类
Abstract类的非abstract子类必须重写父类的abstract方法,重写的时候,去掉abstract修饰,如class B作为上面的abstract class A的子类:
class B extends A{ int min (int x,int y){ // 去掉abstract修饰,给出方法体。 return x>y?y:x; } }
如果一个类abstract类是abstract类的子类,它可以重写父类的abstract方法,也可以继承父类的abstract方法。
-
abstract类的对象作为上转型对象
abstract类不能用new运算符创建对象,但是我们可以声明一个abstract类的对象,然后把其子类的对象的引用放在上面,这个abstract类的对象就成为了上转型对象,就可以利用上转型对象的特性调用子类重写的方法。
-
理解abstract类
abstract类的语法并不复杂,很好掌握。
但更重要的是要理解abstract类的意义。
- abstract类抽象出重要的行为标准。行为标准用抽象方法表示。即抽象类封装了子类必须要有的行为标准。
- 抽象类声明的对象可以成为子类的上转型对象,调用子类重写的方法。体现了子类根据抽象类里的行为标准给出的具体行为。
案例:抽象类GrilFriend给出找女朋友的标准:会说话和会煮饭。具体说什么话和煮什么在子类中实现。
//抽象类GrilFriend给出找女朋友的标准:会说话speak和会煮饭cooking
abstract class GirlFriend{
public abstract void speak();
public abstract void cooking();
}
// 继承并重写
class ChineseGrilFriend extends GirlFriend{
public void speak(){
System.out.println("你好");
}
public void cooking(){
System.out.println("西红柿炒鸡蛋");
}
}
// 继承并重写
class AmericanGrilFriend extends GirlFriend{
public void speak(){
System.out.println("Hello");
}
public void cooking(){
System.out.println("roast beef");
}
}
class Boy{
GirlFriend friend;
void setGrilFriend(GirlFriend f){
this.friend = f;
}
void showGrilFriend(){
// 父类GirlFriend的对象friend是子类的上转型对象,调用重写的方法。
this.friend.speak();
this.friend.cooking();
}
}
public class Example {
public static void main(String[] args) {
Boy boy = new Boy();
System.out.println("中国女朋友:");
ChineseGrilFriend chineseGrilFriend = new ChineseGrilFriend();
boy.setGrilFriend(chineseGrilFriend);
boy.showGrilFriend();
System.out.println("美国女朋友:");
AmericanGrilFriend americanGrilFriend = new AmericanGrilFriend();
boy.setGrilFriend(americanGrilFriend);
boy.showGrilFriend();
}
}
5. 11 面向抽象编程
在设计程序时,经常会使用abstract类。
因为abstract类只关心操作,不关心这些操作具体的实现细节。这样可以使程序的设计者主要精力放在程序的设计上,不必拘泥于细节的实现(将细节的实现交给子类的设计者)
使用多态进行程序设计的核心技术之一是使用上转型对象,即将abstract类声明的对象作为其子类对象的上转型对象,这个上转型对象就可以调用子类重写的方法。
那么什么是面向抽象编程呢?
所谓面向抽象编程,指当我们设计某种重要的类的时候,不让该类面向具体的类,而是面向抽象类。
案例:
需求:设计一个柱形类,这个柱形底部是圆,求出柱形的体积。
-
不使用面向抽象编程方法实现:
可以先设计一个圆类,再设计一个柱形类。
//圆类 class Circle { double r; Circle(double r){ this.r=r; } public double getArea(){ return 3.14*r*r; } } //设计柱类 class Pillar{ Circle bottom; double height; Pillar(Circle bottom,double h){ this.height = h; this.bottom = bottom; } public double getVolume(){ return bottom.getArea()*height; } } public class Example{ public static void main(String [] args){ Circle bottom = new Circle(5.4); Pillar pillar = new Pillar(bottom,10.5); System.out.println("体积:"+pillar.getVolume()); } }
分析:这样子确实实现了需求,但是实际上,需求可能会变化,比如我们现在又想计算一个以矩形为底的柱体的体积,可以看出,上面设计的的Pillar类无法创建出这样的柱体,除非重写Pillar类。所以我们是否有一种方式,就是不重写Pillar类就可以创建以各种几何体为底的柱体呢?这就得用到面向抽象编程思想了。
我们发现,不管柱体以什么几何体为底,都有计算面积的行为,那么我们可以用一个抽象类封装下这个行为标准,需要以什么几何体为底的柱体,我们就定义这个几何体类来继承抽象类,然后在柱类Pillar中使用这个抽象类声明的对象作为各子类的上转型对象,就可以去调用各子类重写的计算面积的方法,就可以实现不改Pillar类就能创建处各种柱体了。
-
使用面向抽象编程方法实现:
// 抽象底部类 abstract class Geometry{ public abstract double getArea(); } // 需要以矩形为底的柱体,就定义一个矩形类 class Rectangle extends Geometry{ double a,b; Rectangle(double a,double b){ this.a=a; this.b=b; } public double getArea(){ return a*b; } } // 需要以圆为底的柱体,就定义一个圆类 class Circle extends Geometry{ double r; Circle(double r){ this.r=r; } public double getArea() { return 3.14*r*r; } } // 柱类 class Pillar{ // 抽象类声明对象作为传进来的子类的上转型对象。 Geometry bottom; double height; Pillar(Geometry bottom,double height){ this.bottom=bottom; this.height=height; } public double getVolume(){ if(bottom==null){ System.out.println("没有底,无法计算面积"); return -1; } return bottom.getArea()*height; } } public class Example { public static void main(String[] args) { Pillar pillar; Geometry bottom = null; pillar = new Pillar(bottom,100); System.out.println("体积:"+pillar.getVolume()); bottom = new Rectangle(12,22); pillar = new Pillar(bottom,58); System.out.println("体积: "+pillar.getVolume()); bottom = new Circle(10); pillar = new Pillar(bottom,58); System.out.println("体积: "+pillar.getVolume()); } }
通过面向抽象来设计Pillar类,使得Pillar类不再依赖具体类,因此每当系统增加一个Geometry的子类时,例如增加一个Triangle子类,就不需要修改Pillar类的任何代码就可以使用Pillar创建处具有三角形底的柱体。
总结:面向抽象编程的目的是为了应对需求的变化,将某个类中经常因需求变化而需要改代码的部分从该类中分离出去。面向抽象编程的核心是让类中每种可能的变化对应的交给抽象类的子类去负责,从而让该类的设计者不去关心具体实现,避免所设计的类依赖于具体的实现。面向抽象编程使设计的类容易应对用户需求的变化。
5.12 开-闭原则
所谓开闭原则,就是让设计的系统对扩展开放,对修改关闭。即当系统中增加新的模块时,不需要修改现有的模块。