汕头网站建设方案优化,flash个人音乐网站模板源码,深圳产品设计招聘信息,如何修改网站标题文章目录 java 继承3.1 根父类Object3.2 方法重写3.3 继承案例#xff1a;图形类继承体系3.4 继承的细节3.4.1 构造方法3.4.2 重名与静态绑定3.4.3 重载和重写3.4.4 父子类型转换3.4.5 继承访问权限protected3.4.6 可见性重写3.4.7 防止继承final 3.5 继承是把双刃剑3.5.1 继承… 文章目录 java 继承3.1 根父类Object3.2 方法重写3.3 继承案例图形类继承体系3.4 继承的细节3.4.1 构造方法3.4.2 重名与静态绑定3.4.3 重载和重写3.4.4 父子类型转换3.4.5 继承访问权限protected3.4.6 可见性重写3.4.7 防止继承final 3.5 继承是把双刃剑3.5.1 继承破坏封装性3.5.2 继承没有反映is-a关系 java 继承
本文为书籍《Java编程的逻辑》1和《剑指Java核心原理与应用实践》2阅读笔记
我们知道java中有类和对象的概念当我们研究类与类之间的关系时会发现类与类之间有一种继承的关系。比如动物类Animal和狗类DogAnimal是父类Dog是子类。父类也叫基类子类也叫派生类。父类、子类是相对的一个类B可能是类A的子类但又是类C的父类。之所以叫继承是因为子类继承了父类的属性和行为父类有的属性和行为会继承给子类。但子类也可以增加子类特有的属性和行为某些父类有的行为子类的实现方式可能与父类也不完全一样。使用继承一方面可以复用代码公共的属性和行为可以放到父类中而子类只需要关注子类特有的就可以了另一方面不同子类的对象可以更为方便地被统一处理。
3.1 根父类Object
在Java中即使没有声明父类也有一个隐含的父类这个父类叫Object。Object没有定义属性但定义了一些方法如下图所示。 上图中定义了一些方法Object中的方法我们先学习toString()方法toString()方法的目的是返回一个对象的文本描述这个方法可以直接被所有类使用toString代码如下 public String toString() {return this.getClass().getName() Integer.toHexString(this.hashCode());}现在有一个Point point对象可以这样使用toString方法
System.out.println(point.toString());输出类似这样com.ieening.Point2e5c649
结合toString和输出结果我们知道之前是类名之后是该对象哈希值的十六进制表示。为什么这么设计呢写类名是可以理解的表示对象的类型而写哈希值则是不得已的因为Object类并不知道具体对象的属性不知道怎么用文本描述但又需要区分不同对象只能是写一个哈希值。
但子类是可以知道自己的属性和值的显然Object的toString不适合子类为解决该问题java允许子类重写父类继承的方法以适合自己的实际和需求。所谓重写就是定义和父类一样的方法并重新实现。
3.2 方法重写
设计一个Point类并重写toString方法。
class Point {private int x;private int y;Point() {this(0, 0);}Point(int x, int y) {this.x x;this.y y;}public int getX() {return x;}public void setX(int x) {this.x x;}public int getY() {return y;}public void setY(int y) {this.y y;}public double distance() {return Math.sqrt(x * x y * y);}Overridepublic String toString() {return Point [x x , y y ];}}
toString方法前面有一个Override这表示toString这个方法是重写的父类的方法重写后的方法返回Point的x和y坐标的值。重写后将调用子类的实现。比如如下代码的输出就变成了Point [x3, y4]。 public static void main(String[] args) {Point point new Point(3, 4);System.out.println(point);}3.3 继承案例图形类继承体系
接下来我们以一些图形处理中的例子来进一步解释。先来看一些图形的例子如下图所示 这都是一些基本的图形图形有直线、正方形、三角形、圆形等图形有不同的颜色。接下来我们定义以下类来说明关于继承的一些概念和使用
父类Shape表示图形。类Circle表示圆。类Line表示直线。类ArrowLine表示带箭头的直线。图形管理者类ShapeManager它负责管理画板上的所有图形对象并负责绘制。
1、Shape
package com.ieening.learninheritshape;public class Shape {private static final String DEFAULT_COLOR black;private String color;public Shape() {this(DEFAULT_COLOR);}public Shape(String color) {this.color color;}public String getColor() {return color;}public void setColor(String color) {this.color color;}public void draw() {System.out.println(draw shape);}
}
上面代码非常简单实例变量color表示颜色draw方法表示绘制可以看到并没有实现实际的绘制代码主要是演示继承关系。
2、圆
圆Circle继承自Shape但包括了额外的属性中心点和半径以及额外的方法area用于计算面积另外重写了draw方法代码如下所示。
package com.ieening.learninheritshape;public class Circle extends Shape { // 注释1// 中心点private Point center;// 半径private double r;public Circle(Point center, double r) {this.center center;this.r r;}Overridepublic void draw() {System.out.println(draw circle at center.toString() with r r , using color : getColor()); // 注释2、注释3}public double area() {return Math.PI * r * r;}Overridepublic String toString() {return Circle [center center , r r ];}public static void main(String[] args) {Point center new Point(3, 4);// 创建圆赋值给circleCircle circle new Circle(center, 2);// 调用draw方法会执行Circle的draw方法circle.draw();// 输出圆面积System.out.println(circle.area());}
}
程序输出
12.566370614359172
draw circle at Point [x3, y4] with r 2.0, using color : black // 注释注释1Java使用extends关键字表示继承关系一个类最多只能有一个父类注释2子类不能直接访问父类的私有属性和方法。比如在Circle中不能直接访问Shape的私有实例变量color而是调用共有方法getColor注释3除了私有的外子类继承了父类的其他属性和方法。比如在Circle的draw方法中可以直接调用getColor方法
这里比较奇怪的是color是什么时候赋值的在new的过程中父类的构造方法也会执行且会优先于子类执行。在这个例子中父类Shape的默认构造方法会在子类Circle的构造方法之前执行。
3、直线
线Line继承自Shape但有两个点以及一个获取长度的方法并重写了draw方法代码如下所示。
package com.ieening.learninheritshape;public class Line extends Shape {private Point start;public Point getStart() {return start;}public void setStart(Point start) {this.start start;}private Point end;public Point getEnd() {return end;}public void setEnd(Point end) {this.end end;}public Line(Point start, Point end, String color) {super(color);this.start start;this.end end;}public double length() {return start.distance(end);}Overridepublic void draw() {System.out.println(draw line from start.toString() to end.toString() , using color super.getColor());}public static void main(String[] args) {Line line new Line(new Point(0, 0), new Point(1, 1), red);System.out.println(line.length());line.draw();}
}
运行结果
1.0
draw line from Point [x0, y0] to Point [x1, y1], using color red这里我们要说明的是super这个关键字super用于指代父类可用于调用父类构造方法访问父类方法和变量。
在Line构造方法中super(color)表示调用父类的带color参数的构造方法。调用父类构造方法时super必须放在第一行。在draw方法中super.getColor()表示调用父类的getColor方法当然不写super.也是可以的因为这个方法子类没有同名的没有歧义当有歧义的时候通过super.可以明确表示调用父类的方法。super同样可以引用父类非私有的变量。可以看出super的使用与this有点像但super和this是不同的this引用一个对象是实实在在存在的可以作为函数参数可以作为返回值但super只是一个关键字不能作为参数和返回值它只是用于告诉编译器访问父类的相关变量和方法。
4、带箭头直线
带箭头直线ArrowLine继承自Line但多了两个属性分别表示两端是否有箭头也重写了draw方法代码如下所示
package com.ieening.learninheritshape;public class ArrowLine extends Line {private boolean startArrow;public boolean isStartArrow() {return startArrow;}public void setStartArrow(boolean startArrow) {this.startArrow startArrow;}private boolean endArrow;public boolean isEndArrow() {return endArrow;}public void setEndArrow(boolean endArrow) {this.endArrow endArrow;}public ArrowLine(Point start, Point end, String color, boolean startArrow, boolean endArrow) {super(start, end, color);this.startArrow startArrow;this.endArrow endArrow;}Overridepublic void draw() {if (startArrow) {System.out.print(draw start arrow, );}if (endArrow) {System.out.print(draw end arrow);}super.draw();}public static void main(String[] args) {ArrowLine arrowLine new ArrowLine(new Point(0, 0), new Point(2, 2), blue, true, true);arrowLine.draw();}
}
运行结果
draw start arrow, draw end arrowdraw line from Point [x0, y0] to Point [x2, y2], using color blueArrowLine继承自Line而Line继承自Shape, ArrowLine的对象也有Shape的属性和方法。注意draw方法的最后一行super.draw()表示调用父类的draw()方法这时候不带super.是不行的因为当前的方法也叫draw()。
5、图形管理器
使用继承的一个好处是可以统一处理不同子类型的对象。比如我们来看一个图形管理者类它负责管理画板上的所有图形对象并负责绘制在绘制代码中只需要将每个对象当作Shape并调用draw方法就可以了系统会自动执行子类的draw方法。代码如下所示。
package com.ieening.learninheritshape;public class ShapeManager {private static final int MAX_NUM 100;private Shape[] shapes new Shape[MAX_NUM];private int shapeNum 0;public void addShape(Shape shape) {if (shapeNum MAX_NUM) {shapes[shapeNum] shape;}}public void draw() {for (int i 0; i shapeNum; i) {shapes[i].draw();}}public static void main(String[] args) {ShapeManager shapeManager new ShapeManager();shapeManager.addShape(new Circle(new Point(4, 4), 3));shapeManager.addShape(new Line(new Point(2, 3), new Point(3, 4), green));shapeManager.addShape(new ArrowLine(new Point(1, 2), new Point(5, 5), black, false, true));shapeManager.draw();}
}
运行结果如下
draw circle at Point [x4, y4] with r 3.0, using color : black
draw line from Point [x2, y3] to Point [x3, y4], using color green
draw end arrowdraw line from Point [x1, y2] to Point [x5, y5], using color blackShapeManager使用一个数组保存所有的shape在draw方法中调用每个shape的draw方法。ShapeManager并不知道每个shape具体的类型也不关心但可以调用到子类的draw方法。如上面代码main方法中使用ShapeManager的一个例子新建了三个shape分别是一个圆、直线和带箭头的线然后加到了shapeManager中然后调用manager的draw方法。需要说明的是在addShape方法中参数Shape shape声明的类型是Shape而实际的类型则分别是Circle、Line和ArrowLine。子类对象赋值给父类引用变量这叫**向上转型**转型就是转换类型向上转型就是转换为父类类型。
变量shape可以引用任何Shape子类类型的对象这叫多态即一种类型的变量可引用多种实际类型对象。这样对于变量shape它就有两个类型类型Shape我们称之为shape的静态类型类型Circle/Line/ArrowLine我们称之为shape的动态类型。在ShapeManager的draw方法中shapes[i].draw()调用的是其对应动态类型的draw方法这称之为方法的动态绑定。
为什么要有多态和动态绑定呢创建对象的代码ShapeManager以外的代码和操作对象的代码ShapeManager本身的代码经常不在一起操作对象的代码往往只需要知道对象是某种父类型也往往只需要知道它是某种父类型就可以了。可以说多态和动态绑定是计算机程序的一种重要思维方式使得操作对象的程序不需要关注对象的实际类型从而可以统一处理不同对象但又能实现每个对象的特有行为。
3.4 继承的细节
3.4.1 构造方法
我们已经知道子类可以通过super调用父类的构造方法如果子类没有通过super调用则会自动调动父类的默认构造方法那如果父类没有默认构造方法呢如下所示
package com.ieening;public class Base {private String member;public Base(String member) {this.member member;}
}
这个类只有一个带参数的构造方法没有默认构造方法。这个时候它的任何子类都必须在构造方法中通过super调用Base的带参数构造方法如下所示否则Java会提示编译错误Implicit super constructor Base() is undefined for default constructor. Must define an explicit constructorJava(134217868)。
另外需要注意的是如果在父类构造方法中调用了可被重写的方法则可能会出现意想不到的结果。我们来看个例子下面是基类和子类代码
package com.ieening;public class Base {private String member;public Base(String member) {this.member member;}public Base() {test();}public void test() {}
}/*** Child*/
class Child extends Base {private int a 123;Overridepublic void test() {System.out.println(a);}public static void main(String[] args) {Child child new Child();child.test();}
}运行结果
0
123构造方法调用了test方法。子类Child有一个实例变量a初始赋值为123重写了test()方法输出a的值。输出结果中第一次输出为0第二次输出为123。第一行为什么是0呢第一次输出是在new过程中输出的在new过程中首先是初始化父类父类构造方法调用test()方法test()方法被子类重写了就会调用子类的test()方法子类方法访问子类实例变量a而这个时候子类的实例变量的赋值语句和构造方法还没有执行所以输出的是其默认值0。
像这样在父类构造方法中调用可被子类重写的方法是一种很不好的实践容易引起混淆应该只调用private的方法。
3.4.2 重名与静态绑定
子类可以重写父类非private的方法当调用的时候会动态绑定执行子类的方法。那实例变量、静态方法和静态变量呢它们可以重名吗如果重名访问的是哪一个呢重名是可以的重名后实际上有两个变量或方法。private变量和方法只能在类内访问访问的也永远是当前类的即在子类中访问的是子类的在父类中访问的是父类的它们只是碰巧名字一样而已没有任何关系。public变量和方法则要看如何访问它。在类内访问的是当前类的但子类可以通过super.明确指定访问父类的。在类外则要看访问变量的静态类型静态类型是父类则访问父类的变量和方法静态类型是子类则访问的是子类的变量和方法。
public class Base {public static String s static_base;public String m base;public static void staticTest(){System.out.println(base static: s);}
}base类中定义了一个public静态变量s一个public实例变量m一个静态方法staticTest。子类Child中定义了和父类重名的变量和方法。对于一个子类对象它就有了两份变量和方法在子类内部访问的时候访问的是子类的或者说子类变量和方法隐藏了父类对应的变量和方法。
public class Child extends Base {public static String s child_base;public String m child;public static void staticTest(){System.out.println(child static: s);}
}观察下面外部访问代码创建了一个子类对象然后将对象分别赋值给了子类引用变量c和父类引用变量b然后通过b和c分别引用变量和方法。程序输出为
public static void main(String[] args) {Child c new Child();Base b c;System.out.println(b.s);System.out.println(b.m);b.staticTest();System.out.println(c.s);System.out.println(c.m);c.staticTest();
}运行结果如下
static_base
base
base static: static_base
child_base
child
child static: child_base当通过b静态类型Base访问时访问的是Base的变量和方法当通过c静态类型Child访问时访问的是Child的变量和方法这称之为静态绑定即访问绑定到变量的静态类型。静态绑定在程序编译阶段即可决定而动态绑定则要等到程序运行时。实例变量、静态变量、静态方法、private方法都是静态绑定的。
3.4.3 重载和重写
重载是指方法名称相同但参数签名不同参数个数、类型或顺序不同重写是指子类重写与父类相同参数签名的方法。对一个函数调用而言可能有多个匹配的方法有时候选择哪一个并不是那么明显。总体的规律就是
当有多个重名函数的时候在决定要调用哪个函数的过程中首先是按照参数类型进行匹配的换句话说寻找在所有重载版本中最匹配的当没有最匹配时看变量的动态类型进行动态绑定
来看个例子这是基类代码
public class Base {public int sum(int a, int b){System.out.println(base_int_int);return ab;}
}代码中定义了方法sum下面是子类代码
public class Child extends Base {public long sum(long a, long b){System.out.println(child_long_long);return ab;}
}以下是调用的代码
public static void main(String[] args){Child c new Child();int a 2;int b 3;c.sum(a, b);
}Child和Base都定义了sum方法这里调用的是哪个sum方法呢子类的sum方法参数类型虽然不完全匹配但是是兼容的父类的sum方法参数类型是完全匹配的。程序输出为
0
base_int_int父类类型完全匹配的方法被调用了。如果父类代码改成下面这样呢
public class Base {public long sum(int a, long b){System.out.println(base_int_long);return ab;}
}父类方法类型也不完全匹配了。程序输出为
0
base_int_long调用的还是父类的方法。父类和子类的两个方法的类型都不完全匹配为什么调用父类的呢因为父类的更匹配一些。现在修改一下子类代码更改为
public class Child extends Base {public long sum(int a, long b){System.out.println(child_int_long);return ab;}
}程序输出变为了
0
child_int_long终于调用了子类的方法。
3.4.4 父子类型转换
之前我们说过子类型的对象可以赋值给父类型的引用变量这叫向上转型向上转型不会出现问题那父类型的变量可以赋值给子类型的变量吗或者说可以向下转型吗语法上可以进行强制类型转换但不一定能转换成功。我们以前面的例子来看
Base b new Child();
Child c (Child)b;Child c (Child) b就是将变量b的类型强制转换为Child并赋值为c这是没有问题的因为b的动态类型就是Child但下面的代码是不行的
Base b new Base();
Child c (Child) b;语法上Java不会报错但运行时会抛出错误错误为类型转换异常。一个父类的变量能不能转换为一个子类的变量取决于这个父类变量的动态类型即引用的对象类型是不是这个子类或这个子类的子类。给定一个父类的变量能不能知道它到底是不是某个子类的对象从而安全地进行类型转换呢答案是可以通过instanceof关键字看下面代码
public boolean canCast(Base b){return b instanceof Child;
}这个函数返回Base类型变量是否可以转换为Child类型instanceof前面是变量后面是类返回值是boolean值表示变量引用的对象是不是该类或其子类的对象。
3.4.5 继承访问权限protected
变量和函数有public/private修饰符public表示外部可以访问private表示只能内部使用还有一种可见性介于中间的修饰符protected表示虽然不能被外部任意访问但可被子类访问。另外protected还表示可被同一个包中的其他类访问不管其他类是不是该类的子类。我们来看个例子这是基类代码
public class Base {protected int currentStep;protected void step1(){}protected void step2(){}public void action(){this.currentStep 1;step1();this.currentStep 2;step2();}
}action表示对外提供的行为内部有两个步骤step1()和step2()使用currentStep变量表示当前进行到了哪个步骤step1()、step2()和currentStep是protected的子类一般不重写action而只重写step1和step2同时子类可以直接访问currentStep查看进行到了哪一步。子类的代码是
public class Child extends Base {protected void step1(){System.out.println(child step this.currentStep);}protected void step2(){System.out.println(child step this.currentStep);}
}使用Child的代码是
public static void main(String[] args){Child c new Child();c.action();
}输出为
child step 1
child step 2基类定义了表示对外行为的方法action并定义了可以被子类重写的两个步骤step1()和step2()以及被子类查看的变量currentStep子类通过重写protected方法step1()和step2()来修改对外的行为。
这种思路和设计是一种设计模式称之为模板方法。action方法就是一个模板方法它定义了实现的模板而具体实现则由子类提供。模板方法在很多框架中有广泛的应用这是使用protected的一种常见场景。
3.4.6 可见性重写
重写方法时一般并不会修改方法的可见性。但我们还是要说明一点重写时子类方法不能降低父类方法的可见性。不能降低是指父类如果是public则子类也必须是public父类如果是protected子类可以是protected也可以是public即子类可以升级父类方法的可见性但不能降低。
为什么要这样规定呢继承反映的是is-a的关系即子类对象也属于父类子类必须支持父类所有对外的行为将可见性降低就会减少子类对外的行为从而破坏is-a的关系但子类可以增加父类的行为所以提升可见性是没有问题的。
3.4.7 防止继承final
有的时候我们不希望父类方法被子类重写有的时候甚至不希望类被继承可以通过final关键字实现。final关键字可以修饰变量而这是final的另一种用法。一个Java类默认情况下都是可以被继承的但加了final关键字之后就不能被继承了如下所示
public final class Base {//主体代码
}一个非final的类其中的public/protected实例方法默认情况下都是可以被重写的但加了final关键字后就不能被重写了如下所示
public class Base {public final void test(){System.out.println(不能被重写);}
}3.5 继承是把双刃剑
一方面继承是非常强大的另一方面继承的破坏力也是很强的。继承为什么会有破坏力呢主要是因为继承可能破坏封装而封装可以说是程序设计的第一原则另外继承可能没有反映出is-a关系。
3.5.1 继承破坏封装性
什么是封装呢封装就是隐藏实现细节提供简化接口。使用者只需要关注怎么用而不需要关注内部是怎么实现的。实现细节可以随时修改而不影响使用者。函数是封装类也是封装。通过封装才能在更高的层次上考虑和解决问题。可以说封装是程序设计的第一原则没有封装代码之间会到处存在着实现细节的依赖则构建和维护复杂的程序是难以想象的。继承可能破坏封装是因为子类和父类之间可能存在着实现细节的依赖。子类在继承父类的时候往往不得不关注父类的实现细节而父类在修改其内部实现的时候如果不考虑子类也往往会影响到子类。子类和父类之间是细节依赖子类扩展父类仅仅知道父类能做什么是不够的还需要知道父类是怎么做的而父类的实现细节也不能随意修改否则可能影响子类。更具体地说子类需要知道父类的可重写方法之间的依赖关系。
3.5.2 继承没有反映is-a关系
继承关系是设计用来反映is-a关系的子类是父类的一种子类对象也属于父类父类的属性和行为也适用于子类。就像橙子是水果一样水果有的属性和行为橙子也必然都有。但现实中设计完全符合is-a关系的继承关系是困难的。比如绝大部分鸟都会飞可能就想给鸟类增加一个方法fly()表示飞但有一些鸟就不会飞比如企鹅。在is-a关系中重写方法时子类不应该改变父类预期的行为但是这是没有办法约束的。还是以鸟为例你可能给父类增加了fly()方法对企鹅你可能想企鹅不会飞但可以走和游泳就在企鹅的fly()方法中实现了有关走或游泳的逻辑。继承是应该被当作is-a关系使用的但是Java并没有办法约束父类有的属性和行为子类并不一定都适用子类还可以重写方法实现与父类预期完全不一样的行为。但对于通过父类引用操作子类对象的程序而言它是把对象当作父类对象来看待的期望对象符合父类中声明的属性和行为。如果不符合结果是什么呢混乱。 马俊昌.Java编程的逻辑[M].北京:机械工业出版社,2018. ↩︎ 尚硅谷教育.剑指Java核心原理与应用实践[M].北京:电子工业出版社,2023. ↩︎