目录
一、继承
1、定义:
在Java语言中,两个不同类可能会有部分重叠的属性和方法,因此为了减少代码的重复率,Java提出了继承的概念以提高代码的复用率。
继承:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特 性的基础上进行扩展,增加新功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用。
2、用法:
Java语言中,类的继承关系,需要使用关键字extends
public class 子类 extends 父类 {
}
例:
class person{
public String name;
public int age;
public void eat(){
System.out.println("吃饭!!!");
}
public void sleep(){
System.out.println("休息!!!");
}
}
class student extends person{
public void learning{
System.out.println("上课!!!")
}
}
class teacher extends person{
public void teaching{
System.out.println("教学!!!")
}
}
public class test {
public static void main(String[] args) {
student s1 = new student();
// student类中并没有定义任何成员变量,name和age属性是从父类person继承下来的
System.out.println(s1.name);
System.out.println(s1.age);
// s1访问的eat()和sleep()方法也是从person中继承下来的
s1.eat();
s1.sleep();
}
}
注:
- 子类会将父类中的成员变量或者成员方法继承到子类中了
- 子类继承父类后,必须新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承
3、使用从父类继承的成员
在继承体系中,子类将父类中的方法和字段继承下来了,那在子类中能否直接访问父类中继承下来的成员呢?
(1)、子类使用从父类继承的成员变量
1. 子类和父类不存在同名成员变量
public class A {
int a;
int b;
}
public class B extends A{
int c;
public void func(){
a = 10; // 访问从父类中继承下来的a
b = 20; // 访问从父类中继承下来的b
c = 30; // 父类没有c,访问子类的c
}
}
2. 子类和父类存在同名成员变量
public class A {
int a;
int b;
}
public class B extends A{
int a; // 与父类中成员a同名,且类型相同
public void method(){
a = 1; // 访问子类的a!!!
b = 2; // 子类没有b,访问的是从父类继承下来的b
//c = 3; // 编译失败,因为父类和子类都没有成员变量c
}
}
在子类方法中或通过子类对象访问成员时:
- 如果访问的成员变量子类中有,优先访问自己的成员变量;若子类中无,则访问父类继承下来的;如果父类也没有定义,则编译报错。
- 成员变量访问遵循就近原则,如果子类和父类中存在同名成员变量,则优先访问子类的。
(2)、子类使用从父类继承的成员方法
1. 子类和父类不存在同名成员方法
public class A {
public void func1(){
System.out.println("A中的func1方法");
}
}
public class B extends A{
public void func2(){
System.out.println("B中的func2方法");
}
public void func3(){
func2(); // 访问子类的方法
func1(); // 访问父类的方法
// func4(); // 父类和子类都没有,编译失败
}
}
2. 子类和父类存在同名的成员方法
public class A {
public void func1(){
System.out.println("A中的func1()");
}
public void func2(){
System.out.println("A中的func2()");
}
}
public class B extends A{
public void func1(int a) {
System.out.println("B中的func1(int)");
}
public void func2(){
System.out.println("B中的func2()");
}
public void func3(){
func1(); // 没有传参,访问父类中的func1()
func1(20); // 传递int参数,访问子类中的func1(int)
//方法重载,注意不是重写!!!
func2(); // 访问的是子类中的func2()
}
}
注:
- 成员方法没有同名时,优先在子类中寻找,若子类中没有则再到父类中找,如果父类中也没有则报错。
- 成员方法存在同名时,则使用子类中的方法。
- 若父类和子类同名方法的参数不同,根据调用方法传递的参数选择方法,注意这种情况是重载而非重写。
4、super
(1)、定义:
在上述的代码中,子类和父类中存在同名的成员变量和方法,调用同名的变量和方法时只能使用子类中的成员,但是想要明确调用父类中的成员就需要使用super关键字。
public class A {
int a;
int b;
}
public class B extends A{
int a; // 与父类中成员变量同名且类型相同
char b; // 与父类中成员变量同名但类型不同
public void func1(){
// 对于同名的成员变量,直接访问时,访问的都是子类的
this.a = 100;
this.b = 101;
// 注意使用this是引用当前类的对象,而想要访问父类的成员变量时,需要使用super关键字
super.a = 200;
super.b = 201;
}
}
注:super只能在非静态方法中使用
5、子类构造方法
(1)、定义
构造子类对象时,先要调用父类的构造方法,将从父类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整 。
public class A {
public A(){
System.out.println("A()");
}
}
public class B extends A{
public B(){
super();
// 用户没有写构造方法时,编译器会自动添加无参构造方法,而且super()必须是子类构造方法中第一
条语句。
System.out.println("B()");
}
}
public class Test {
public static void main(String[] args) {
B b = new B();
}
}
结果:
// A()
// B()
注:
- 若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构造方法
- 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败。
- 在子类构造方法中,super()调用父类构造时,必须是子类构造函数中第一条语句。
- super()只能在子类构造方法中出现一次,并且不能和this同时出现
6、执行顺序
(1)、没有继承关系时
class A {
public int m;
public int n;
public A(String m, int n) {
this.m = m;
this.n = n;
System.out.println("构造方法执行");
}
{
System.out.println("实例代码块执行");
}
static {
System.out.println("静态代码块执行");
}
}
public class test {
public static void main(String[] args) {
A a1 = new A(1,2);
System.out.println("------------------------");
A a2 = new A(3,4);
}
}
执行结果:
静态代码块
实例代码块
构造方法
------------------------
实例代码块
构造方法
注:
- 静态代码块先执行,并且只执行一次,在类加载阶段执行
- 当有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行
(2)、有继承关系时
class A {
public int m;
public int n;
public A(int m, int n) {
this.m = m;
this.n = n;
System.out.println("A的构造方法执行");
}
{
System.out.println("A的实例代码块执行");
}
static {
System.out.println("A的静态代码块执行");
}
}
class B extends A{
public B(int m,int n) {
super(m,n);
System.out.println("B的构造方法执行");
}
{
System.out.println("B的实例代码块执行");
}
static {
System.out.println("B的静态代码块执行");
}
}
public class test {
public static void main(String[] args) {
B b1 = new B(1,2);
System.out.println("--------------------");
B b2 = new B(3,4);
}
}
执行结果:
A的静态代码块执行
B的静态代码块执行
A的实例代码块执行
A的构造方法执行
B的实例代码块执行
B的构造方法执行
---------------------
A的实例代码块执行
A的构造方法执行
B的实例代码块执行
B的构造方法执行
注:
- 父类静态代码块优先于子类静态代码块执行,且是最早执行
- 父类实例代码块和父类构造方法执行
- 子类的实例代码块和子类构造方法接着再执行
- 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
7、继承方式
(1)、单继承
(2)、多继承
(3)、不同类继承同一个类
注:Java不支持多继承,并且一般不出现超过三层的继承关系。继承层次太多, 就需要考虑对代码进行重构了。如果想从语法上进行限制继承, 可以使用 final 关键字。
8、final
final关键可以用来修饰变量、成员方法以及类。
(1)、final修饰变量或字段
此处表示常量即不能修改
final int a = 10;
a = 20; // 编译出错
(2)、修饰方法
此方法不能被重写
(3)、修饰类
此类不能被继承
final public class Animal {
}
public class Bird extends Animal {
}
// 编译出错Error:(3, 27) java: 无法从最终com.bit.Animal进行继
9、继承与组合
(1)、定义
组合和继承类似,既是一种表达类之间关系的方式,也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如 extends 这样的关键字),仅仅是将一个类的实例作为另外一个类的字段。继承表示对象之间是is-a的关系,组合表示对象之间是has-a的关系
例:
// 轮胎类
class Tire{
}
// 发动机类
class Engine{
}
// 车载系统类
class VehicleSystem{
}
class Car{
private Tire tire; // 可以复用轮胎中的属性和方法
private Engine engine; // 可以复用发动机中的属性和方法
private VehicleSystem vs; // 可以复用车载系统中的属性和方法
}
class Benz extend Car{
// 将汽车中包含的:轮胎、发送机、车载系统全部继承下来
}
二、多态
1、定义
多态为不同的对象去完成同一个行为时,会产生出不同的状态。总的来说就是同一件事情,发生在不同对象身上,就会产生不同的结果。
2、使用
在Java中要实现多态,必须要满足如下几个条件,缺一不可:
- 必须在继承体系下
- 子类必须要对父类中方法进行重写
- 通过父类的引用调用重写的方法
在代码运行时,当传递不同类对象时,会调用对应类中的方法
例:
public class A {
int m;
public Animal(int m){
this.m = m;
}
public void func(){
System.out.println("呵呵呵!!!");
}
}
public class B extends A{
public B(int m){
super(m);
}
public void func(){
System.out.println("哈哈哈!!");
}
}
public class C extends A {
public C(int m){
super(m);
}
public void func(){
System.out.println("嘿嘿嘿!");
}
}
public class test {
public static void func1(A a){
a.func);
}
public static void main(String[] args) {
B b = new B(1);
C c = new C(2);
func1(b);
func1(c);
}
}
运行结果:
哈哈哈!!
嘿嘿嘿!
3、重写
(1)、定义
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
(2)、规则
- 子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致被重写的方法返回值类型可以不同,但是必须是具有父子关系的
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected
- 父类被static、private修饰的方法、构造方法都不能被重写。
- 重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
(3)、绑定
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
4、向上转型和向下转型
(1)、向上转型
定义:实际就是创建一个子类对象,将其当成父类对象来使用,从小范围向大范围的转换。
父类类型 对象名 = new 子类类型()
优点:让代码实现更简单灵活。
缺点:不能调用到子类特有的方法。
(2)、向下转型
定义:将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
注:向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为true,则可以安全转换。
5、多态优缺点
(1)、优点
1. 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
圈复杂度是一种描述一段代码复杂程度的方式。一段代码如果平铺直叙,那么就比较简单容易理解。而如果有很多的条件分支或者循环语句,就认为理解起来更复杂。因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数,这个个数就称为 "圈复杂度"。如果一个方法的圈复杂度太高,就需要考虑重构。不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .
例:
public static void drawShapes() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单
public static void drawShapes() {
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
2. 可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("△");
}
}
对于类的调用者来说(drawShapes方法),只要创建一个新类的实例就可以了,改动成本很低。
而对于不用多态的情况,就要把 drawShapes 中的 if - else 进行一定的修改,改动成本更高。
(2)、缺点
- 代码的运行效率降低。
- 属性没有多态性,当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
- 构造方法没有多态性
6、避免在构造方法中调用重写的方法
用尽量简单的方式使对象进入可工作状态,不要在构造器中调用方法(如果这个方法被子类重写,就会触发动态绑定,但是此时子类对象还没构造完成),可能会出现一些隐藏的但是又极难发现的问题。
例:
class B {
public B() {
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
public void func() {
System.out.println("D.func() " + num);
}
}
public class test {
public static void main(String[] args) {
D d = new D();
}
}
// 执行结果:D.func() 0
构造 D 对象的同时,会调用 B 的构造方法。B 的构造方法中调用了 func 方法,此时会触发动态绑定,会调用到 D 中的 func。此时 D 对象自身还没有构造,此时 num 处在未初始化的状态,值为 0。如果具备多态性,num的值应该是1。所以在构造函数内,尽量避免使用实例方法,除了final和private方法。