Java编程的逻辑笔记——面向对象(类的基础+类的继承)

面向对象

类的基础

类的基本概念

在某些情况下,类也确实只是函数的容器,但类更多表示的是自定义数据类型

函数容器

Math类的常用函数(静态方法)

image-20210606113412345

类方法(静态方法):static修饰,直接通过类名进行调用,不需要创建实例

实例方法:通过实例调用,或者创建对象后进行对象调用

public:表示这些函数是公开的,可以在任何地方被外部调用

private:私有的,这个函数只能在同一个类内被别的函数调用,而不能被外部的类调用

通过private封装和隐藏内部实现细节,避免被误操作,是计算机程序的一种基本思维方式

Arrays类的一些函数

image-20210606114058976

自定义数据类型

一个数据类型就主要由4部分组成:

❑ 类型本身具有的属性,通过类变量体现

❑ 类型本身可以进行的操作,通过类方法体现

❑ 类型实例具有的属性,通过实例变量体现

❑ 类型实例可以进行的操作,通过实例方法体现

术语别名与解释
成员变量类变量和实例变量,就是类的成员
静态变量或静态成员变量类变量,static修饰的变量
成员方法类方法和实例方法,都是类的成员
静态方法类方法,static修饰的方法
类变量

类型本身具有的属性通过类变量体现,经常用于表示一个类型中的常量

  • 与类方法一样,类变量可以直接通过类名访问,如Math.PI

  • final在修饰变量的时候表示常量,即变量赋值后就不能再修改了,使用final可以避免误操作

  • 表示类变量的时候,static修饰符是必需的,但public和final都不是必需的

实例变量和实例方法

实例,字面意思就是一个实际的例子

术语解释
实例方法具体的实例可以进行的操作
实例变量具体的实例所具有的属性

实例方法和类方法的区别:

  • 实例方法既能访问实例变量,也能访问类变量,既可以调用实例方法,也可以调用类方法
  • 类方法只能访问类变量,不能访问实例变量,可以调用其他的类方法,不能调用实例方法
使用第一个类
public static void main(String[] args) {
     Point p = new Point();
     p.x = 2;
     p.y = 3;
     System.out.println(p.distance());
}

分析:

 Point p = new Point();
//可以分为两部分
1 Point p ;
//Point p声明了一个变量,这个变量叫p,是Point类型的。
//声明变量本身只会分配存放位置的内存空间,这块空间还没有指向任何实际内容。
//因为这种变量和数组变量本身不存储数据,而只是存储实际内容的位置,它们也都称为引用类型的变量。
2 p = new Point();
//创建了一个实例或对象,然后赋值给了Point类型的变量p,它至少做了两件事:
//1)分配内存,以存储新对象的数据,对象数据包括这个对象的属性,具体包括其实例变量x和y。
//2)给实例变量设置默认值,int类型默认值为0。

默认值:

  • 数值类型变量的默认值是0

  • boolean是false,

  • char是“\u0000”

  • 引用类型变量都是null

  • null是一个特殊的值,表示不指向任何对象,这些默认值可以修改。

一般而言,不应该将实例变量声明为public,而只应该通过对象的方法对实例变量进行操作

这也是为了减少误操作,直接访问变量没有办法进行参数检查和控制,而通过方法修改,可以在方法中进行检查

变量默认值

如果希望修改这个默认值,可以在定义变量的同时就赋值,或者将代码放入初始化代码块中,代码块用{}包围

如:

int x = 1;
int y;
{
     y = 2;
}

静态变量也可以这样初始化:

static int STATIC_ONE = 1;
static int STATIC_TWO;
static
{
 STATIC_TWO = 2;
}

static{}是静态初始化代码块:静态初始化代码块在类加载的时候执行,这是在任何对象创建之前,且只执行一次

private变量

Point类定义——实例变量定义为private

class Point {
     private int x;
     private int y;
     public void setX(int x) {
     	 this.x = x;
     }
     public void setY(int y) {
    	 this.y = y;
     }
     public int getX() {
    	 return x;
     }
     public int getY() {
    	 return y;
     }
     public double distance() {
    	 return Math.sqrt(x * x + y * y);
     }
}

this表示当前实例,在语句this.x=x;中,this.x表示实例变量x,而右边的x表示方法参数中的x。

前面我们提到,在实例方法中,有一个隐含的参数,这个参数就是this,没有歧义的情况下,可以直接访问实例变量,在这个例子中,两个变量名都叫x,则需要通过加上this来消除歧义

set/get方法的意义

实际上,Java编译器一般也会将对这几个方法的调用转换为直接访问实例变量,而避免函数调用的开销。

但在很多情况下,通过函数调用可以封装内部数据避免误操作,我们一般还是不将成员变量定义为public。

构造方法
public Point(){
 	this(0,0);//调用Point(0, 0)构造函数
    //这个this调用必须放在第一行,这个规定也是为了避免误操作。
    //构造方法是用于初始化对象的,如果要调用别的构造方法,先调别的,然后根据情况自己再做调整,而如果自己先初始化了一部分,再调别的,自己的修改可能就被覆盖了
}
public Point(int x, int y){
     this.x = x;
     this.y = y;
}

构造方法有一些特殊的地方:

  1. 名称是固定的,与类名相同,靠这个用户和Java系统就都能容易地知道哪些是构造方法
  2. 没有返回值,也不能有返回值,构造方法隐含的返回值就是实例本身

构造方法:

  • 默认构造方法:在没有定义任何构造方法的时候,Java认为用户不需要,所以就生成一个空的以被new过程调用;定义了构造方法的时候,Java认为用户知道自己在干什么,认为用户是有意不想要不带参数的构造方法,所以不会自动生成。
  • 私有构造方法,适应场景
    • 不能创建类的实例,类只能被静态访问,如Math和Arrays类,它们的构造方法就是私有的
    • 能创建类的实例,但只能被类的静态方法调用。如单例:在这种场景中,对象是通过静态方法获取的,而静态方法调用私有构造方法创建一个对象,如果对象已经创建过了,就重用这个对象
    • 只是用来被其他多个构造方法调用,用于减少重复代码
类和对象的生命周期
  1. 当第一次通过new创建一个类的对象时,或者直接通过类名访问类变量和类方法时,Java会将类加载进内存,为这个类分配一块空间,这个空间会包括类的定义、它的变量和方法信息,同时还有类的静态变量,并对静态变量赋初始值
  2. 类加载进内存后,一般不会释放,直到程序结束。一般情况下,类只会加载一次,所以静态变量在内存中只有一份。
  3. 当通过new创建一个对象的时候,对象产生,在内存中,会存储这个对象的实例变量值,每做new操作一次,就会产生一个对象,就会有一份独立的实例变量。
  4. 每个对象除了保存实例变量的值外,可以理解为还保存着对应类型即类的地址,这样,通过对象能知道它的类,访问到类的变量和方法代码。
  5. 对象和数组一样,有两块内存,保存地址的部分分配在栈中,而保存实际内容的部分分配在堆中。栈中的内存是自动管理的,函数调用入栈就会分配,而出栈就会释放。
  6. 堆中的内存是被垃圾回收机制管理的,当没有活跃变量指向对象的时候,对应的堆空间就可能被释放,具体释放时间是Java虚拟机自己决定的。活跃变量就是已加载的类的类变量,以及栈中所有的变量。
小结
  1. public:可以修饰类、类方法、类变量、实例变量、实例方法、构造方法,表示可被外部访问。

  2. private:可以修饰类、类方法、类变量、实例变量、实例方法、构造方法,表示不可以被外部访问,只能在类内部被使用。

  3. static:修饰类变量和类方法,它也可以修饰内部类。

  4. this:表示当前实例,可以用于调用其他构造方法,访问实例变量,访问实例方法。

  5. final:修饰类变量、实例变量,表示只能被赋值一次,也可以修饰实例方法和局部变量。

类的组合

每个类封装其内部细节,对外提供高层次的功能,使其他类在更高层次上考虑和解决问题,是程序设计的一种基本思维方式。

类中实例变量的类型可以是当前定义的类型,两个类之间可以互相引用

代码的组织机制

包的概念

带完整包名的类名称为其完全限定名,比如String类的完全限定名为java.lang.String

Java API中所有的类和接口都位于包Java或javax下,Java是标准包,javax是扩展包

声明类所在的包

使用package声明其包名

  • 包名和文件目录结构必须匹配

  • 为避免命名冲突,Java中命名包名的一个惯例是使用域名作为前缀,因为域名是唯一的,一般按照域名的反序来定义包名,比如,域名是apache.org,包名就以org.apache开头。

  • 如果代码需要公开给其他人用,最好有一个域名以确保唯一性,如果只是内部使用,则确保内部没有其他代码使用该包名即可。

通过包使用类

使用有两种方式:一种是通过类的完全限定名;另外一种是将用到的类引入当前类(import …)。

  • 做import操作时,可以一次将某个包下的所有类引入,语法是使用. *

    • 需要注意的是,这个引入不能递归,它只会引入java.util包下的直接类,而不会引入java.util下嵌套包内的类,比如,不会引入包java.util.zip下面的类。试图嵌套引入的形式也是无效的,如import java.util..
  • 在一个类内,对其他类的引用必须是唯一确定的,不能有重名的类,如果有,则通过import只能引入其中的一个类,其他同名的类则必须要使用完全限定名

  • 有一种特殊类型的导入,称为静态导入,它有一个static关键字,可以直接导入类的公开静态方法和成员

    • import java.util.Arrays;
      import static java.util.Arrays.*;//静态引入Arrays中的所有静态方法
      import static java.lang.System.out; //导入静态变量out
      public class Hello {
       public static void main(String[] args) {
           int[] arr = new int[]{1,4,2,3};
           sort(arr); //可直接使用Arrays中的sort方法
           out.println(Arrays.toString(arr)); //可直接使用out变量
       }
      }
      
    • 静态导入不应过度使用,否则难以区分访问的是哪个类的代码。

包范围可见性
  • 如果什么修饰符都不写,它的可见性范围就是同一个包内,同一个包内的其他类可以访问,而其他包内的类则不可以访问
    • 同一个包指的是同一个直接包,子包下的类并不能访问
  • protected可见性包括包可见性,也就是说,声明为protected不仅表明子类可以访问,还表明同一个包内的其他类可以访问,即使这些类不是子类也可以
  • 可见性范围从小到大是:private < 默认(包) < protected <public
jar包

打包方式:

  1. 首先到编译后的java class文件根目录,运行

    jar -cvf <包名>.jar <最上层包名>

  2. 如果Hello.class位于E:\bin\shuo\laoma\Hello.class,则可以到目录 E:\bin下,然后运行:

    jar -cvf <hello>.jar <shuo>

    hello.jar就是jar包,jar包其实就是一个压缩文件,可以使用解压缩工具打开。

  3. 使用jar包:加入类路径(classpath)

程序的编译与链接
  • 编译:将源代码文件变成扩展名是.class的一种字节码,由javac命令完成
  • 链接:在运行时动态执行,.class文件不能直接运行,运行的是Java虚拟机,所谓链接就是根据引用到的类加载相应的字节码并执行
  • Java编译和运行时,都需要以参数指定一个classpath,即类路径。类路径可以有多个,对于直接的class文件,路径是class文件的根目录
  • 对于jar包,路径是jar包的完整名称(包括路径和jar包名)
  • 编译时,Java编译器会确定引用的每个类的完全限定名,确定的方式是根据import语句和classpath
    • 如果导入的是完全限定类名,则可以直接比较并确定
    • 如果是模糊导入(import带.*),则根据classpath找对应父包,再在父包下寻找是否有对应的类
    • 如果多个模糊导入的包下都有同样的类名,则Java会提示编译错误,此时应该明确指定导入哪个类
  • 运行时,会根据类的完全限定名寻找并加载类,寻找的方式就是在类路径中寻找
    • 如果是class文件的根目录,则直接查看是否有对应的子目录及文件
    • 如果是jar文件,则首先在内存中解压文件,然后再查看是否有对应的类
小结

Java 9中,清晰地引入了模块的概念,JDK和JRE都按模块化进行了重构,传统的组织机制依然是支持的,但新的应用可以使用模块

  • 一个应用可由多个模块组成,一个模块可由多个包组成
  • 模块之间可以有一定的依赖关系,一个模块可以导出包给其他模块用,可以提供服务给其他模块用,也可以使用其他模块提供的包,调用其他模块提供的服务。对于复杂的应用,模块化有很多好处,比如更强的封装、更为可靠的配置、更为松散的耦合、更动态灵活等

类的继承

父类=基类

子类=派生类

基本概念

在Java中,所有类都有一个父类Object

根父类Object

Object没有定义属性,但定义了一些方法

image-20210607093731574

Point p = new Point(2,3);
System.out.println(p.toString());
//输出
Point@76f9aa66
//源码
public String toString() {
 	return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
//分析:
1.getClass().getName() 返回当前对象的类名,hashCode()返回一个对象的哈希值
2.整数默认情况下,通常是对象的内存地址值,Integer.toHexString(hashCode())返回这个哈希值的十六进制表示。

子类可以重写父类的方法,以反映自己的不同实现。所谓重写,就是定义和父类一样的方法,并重新实现。

public class Point {
     private int x;
     private int y;
     public Point(int x, int y) {
     this.x = x;
     this.y = y;
 	}
     public double distance(Point point){
        return Math.sqrt(Math.pow(this.x-point.getX(),2)
                +Math.pow(this.y-point.getY(), 2));
     }
     public int getX() {
        return x;
     }
     public int getY() {
         return y;
     }
     @Override
     public String toString() {
        return "("+x+","+y+")";
     }
}

Point p = new Point(2,3);
System.out.println(p.toString())//输出(2,3)

toString()方法前面有一个@Override,这表示toString()这个方法是重写的父类的方法

继承演示
//父类,图形类
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");
     }
}
//子类-圆
public class Circle extends Shape {
     //中心点
     private Point center;
     //半径
     private double r;
     public Circle(Point center, double r) {
         this.center = center;
         this.r = r;
     }
     @Override
     public void draw() {
         System.out.println("draw circle at " +center.toString()+" with r "+r
         +", using color : "+getColor());
     }
     public double area(){
         return Math.PI*r*r;
     }
}
//子类-直线
public class Line extends Shape {
     private Point start;
     private Point end;
     public Line(Point start, Point end, String color) {
         super(color);
         //super(color)表示调用父类的带color参数的构造方法。调用父类构造方法时,super必须放在第一行。
         this.start = start;
         this.end = end;
     }
     public double length(){
         return start.distance(end);
     }
     public Point getStart() {
         return start;
     }
     public Point getEnd() {
         return end;
     }
     @Override
     public void draw() {
         System.out.println("draw line from "
                 + start.toString()+" to "+end.toString()
                 + ",using color "+super.getColor());
         //super.getColor()表示调用父类的getColor方法,
         //当然不写super.也是可以的,因为这个方法子类没有同名的,没有歧义,
         //当有歧义的时候,通过super.可以明确表示调用父类的方法
     }
}
//带箭头直线
public class ArrowLine extends Line {
     private boolean startArrow;
     private boolean endArrow;
     public ArrowLine(Point start, Point end, String color,
             boolean startArrow, boolean endArrow) {
             super(start, end, color);
             this.startArrow = startArrow;
             this.endArrow = endArrow;
     }
     @Override
     public void draw() {
         super.draw();
         //super.draw()表示调用父类的draw()方法,这时候不带super.是不行的,因为当前的方法也叫draw()
         if(startArrow){
         	System.out.println("draw start arrow");
         }
         if(endArrow){
         	System.out.println("draw end arrow");
         }
     }
}
//图形管理器
public class ShapeManager {
     private static final int MAX_NUM = 100;
     private Shape[] shapes = new Shape[MAX_NUM];
     private int shapeNum = 0;
    //,在addShape方法中,参数Shape shape,声明的类型是Shape,
    //而实际的类型则分别是Circle、Line和ArrowLine
     public void addShape(Shape shape){
         if(shapeNum<MAX_NUM){
         	shapes[shapeNum++] = shape;
         }
     }
    //ShapeManager使用一个数组保存所有的shape,在draw方法中调用每个shape的draw方法。
    //ShapeManager并不知道每个shape具体的类型,也不关心,但可以调用到子类的draw方法。
     public void draw(){
         for(int i=0; i<shapeNum; i++){
         	shapes[i].draw();
         }
     }
}

//图形管理器调用
public static void main(String[] args) {
     ShapeManager manager = new ShapeManager();
     //子类对象赋值给父类引用变量,这叫向上转型,转型就是转换类型,向上转型就是转换为父类类型。
     manager.addShape(new Circle(new Point(4,4),3));
     manager.addShape(new Line(new Point(2,3),
                               new Point(3,4),"green"));
     manager.addShape(new ArrowLine(new Point(1,2),
                                    new Point(5,5),"black",false,true));
     manager.draw();
}

1)Java使用extends关键字表示继承关系,一个类最多只能有一个父类;

2)子类不能直接访问父类的私有属性和方法。比如,在Circle中,不能直接访问Shape的私有实例变量color;

3)除了私有的外,子类继承了父类的其他属性和方法。比如,在Circle的draw方法中,可以直接调用getColor()方法。

类执行顺序

public static void main(String[] args) {
     Point center = new Point(2,3);
     //创建园,赋值给circle
     Circle circle = new Circle(center,2);
     //调用draw方法,会执行Circle的draw方法
     circle.draw();
     //输出园的面积
     System.out.println(circle.area());
}

//输出结果
draw circle at (2,3) with r 2.0, using color : black
12.566370614359172

在new的过程中,父类的构造方法也会执行,且会优先于子类执行。在这个例子中,父类Shape的默认构造方法会在子类Circle的构造方法之前执行。color在父类的构造函数被赋值。

  • 直线

super关键字:可用于调用父类构造方法,访问父类方法和变量

1)在Line构造方法中,super(color)表示调用父类的带color参数的构造方法。调用父类构造方法时,super必须放在第一行。

2)在draw方法中,super.getColor()表示调用父类的getColor方法,当然不写super.也是可以的,因为这个方法子类没有同名的,没有歧义,当有歧义的时候,通过super.可以明确表示调用父类的方法。

3)super同样可以引用父类非私有的变量。可以看出,super的使用与this有点像,但super和this是不同的,this引用一个对象,是实实在在存在的,可以作为函数参数,可以作为返回值,但super只是一个关键字不能作为参数和返回值,它只是用于告诉编译器访问父类的相关变量和方法

  • 带直线的箭头

ArrowLine继承自Line,而Line继承自Shape, ArrowLine的对象也有Shape的属性和方法。

  • 图形管理器

使用继承的一个好处是可以统一处理不同子类型的对象

在绘制代码中,只需要将每个对象当作Shape并调用draw方法就可以了,系统会自动执行子类的draw方法

向上转型:子类对象赋值给父类引用变量,这叫向上转型,转型就是转换类型,向上转型就是转换为父类类型

多态:变量shape可以引用任何Shape子类类型的对象,这叫多态,即一种类型的变量,可引用多种实际类型对象

静态类型:类型Shape,父类

动态类型:类型Circle/Line/ArrowLine,我们称之为shape的动态类型,shape的子类

动态绑定:shapes[i].draw()调用的是其对应动态类型的draw方法,这称之为方法的动态绑定。


多态和动态绑定是计算机程序的一种重要思维方式,使得操作对象的程序不需要关注对象的实际类型,从而可以统一处理不同对象,但又能实现每个对象的特有行为

小结
  1. 每个类有且只有一个父类,没有声明父类的,其父类为Object,子类继承了父类非private的属性和方法,可以增加自己的属性和方法,以及重写父类的方法实现。
  2. new过程中,父类先进行初始化,可通过super调用父类相应的构造方法,没有使用super的情况下,调用父类的默认构造方法
  3. 子类变量和方法与父类重名的情况下,可通过super强制访问父类的变量和方法
  4. 子类对象可以赋值给父类引用变量,这叫多态;实际执行调用的是子类实现,这叫动态绑定。

继承的细节

//父类
public class Base {
     public Base(){
     	test();
     }
     public void test(){
     }
}
//子类
public class Child extends Base {
     private int a = 123;
     public Child(){
     }
     public void test(){
     	System.out.println(a);
     }
}
//调用
public static void main(String[] args){
     Child c = new Child();//先初始化父类,父类初始化调用test(),而test被子类重写,调用子类的test,a还没有被赋值,输出0
     c.test();//调用子类test,输出123
}
//输出结果
0
123

分析:第一次输出是在new过程中输出的,在new过程中,首先是初始化父类,父类构造方法调用test()方法,test()方法被子类重写了,就会调用子类的test()方法,子类方法访问子类实例变量a,而这个时候子类的实例变量的赋值语句和构造方法还没有执行,所以输出的是其默认值0。

在父类构造方法中调用可被子类重写的方法,是一种不好的实践,容易引起混淆,应该只调用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);
     }
}
//子类代码
//子类定义了和父类重名的变量和方法。对于一个子类对象,它就有了两份变量和方法,在子类内部访问的时候,访问的是子类的,或者说,子类变量和方法隐藏了父类对应的变量和方法
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);
     }
}
//调用
//静态变量和静态方法一般通过类名直接访问,但也可以通过类的对象访问
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方法,都是静态绑定的。

重载和重写

重载:是指方法名称相同但参数签名不同参数个数、类型或顺序不同

重写:是指子类重写与父类相同参数签名的方法

当有多个重名函数的时候,在决定要调用哪个函数的过程中,首先是按照参数类型进行匹配的,换句话说,

  • 寻找在所有重载版本中最匹配的,(都一样先匹配子类的)

  • 然后才看变量的动态类型,进行动态绑定

父与子类型的转换
  • 一个父类的变量能不能转换为一个子类的变量,取决于这个父类变量的动态类型(即引用的对象类型)是不是这个子类或这个子类的子类。
  • 给定一个父类的变量能不能知道它到底是不是某个子类的对象,从而安全地进行类型转换呢?答案是可以,通过instanceof关键字
  • instanceof前面是变量,后面是类,返回值是boolean值,表示变量引用的对象是不是该类或其子类的对象。
继承访问权限protected

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();
    }
}
//子类
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);
    }
}
//使用子类
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方法就是一个模板方法,它定义了实现的模板,而具体实现则由子类提供

可见性重写

重写时,子类方法不能降低父类方法的可见性。不能降低是指,父类如果是public,则子类也必须是public,父类如果是protected,子类可以是protected,也可以是public,即子类可以升级父类方法的可见性但不能降低

原因:继承反映的是“is-a”的关系,即子类对象也属于父类,子类必须支持父类所有对外的行为,将可见性降低就会减少子类对外的行为,从而破坏“is-a”的关系,但子类可以增加父类的行为,所以提升可见性是没有问题的。

防止继承final

一个Java类,默认情况下都是可以被继承的,但加了final关键字之后就不能被继承了

    public final class Base {
        //主体代码
    }

一个非final的类,其中的public/protected实例方法默认情况下都是可以被重写的,但加了final关键字后就不能被重写了

    public class Base {
        public final void test(){
            System.out.println("不能被重写");
        }
    }

继承实现的原理

    public class Base {
        public static int s;
        private int a;
        static {
            System.out.println("基类静态代码块, s: "+s);
            s = 1;
        }
        {
            System.out.println("基类实例代码块, a: "+a);
            a = 1;
        }
        public Base(){
            System.out.println("基类构造方法, a: "+a);
            a = 2;
        }
        protected void step(){
            System.out.println("base s: " + s +", a: "+a);
        }
        public void action(){
            System.out.println("start");
            step();
            System.out.println("end");
        }
    }

Base包括一个静态变量s,一个实例变量a,一段静态初始化代码块,一段实例初始化代码块,一个构造方法,两个方法step和action。

    public class Child extends Base {
        public static int s;
        private int a;
        static {
            System.out.println("子类静态代码块, s: "+s);
            s = 10;
        }
        {
            System.out.println("子类实例代码块, a: "+a);
              a = 10;
          }
          public Child(){
              System.out.println("子类构造方法, a: "+a);
              a = 20;
          }
          protected void step(){
              System.out.println("child s: " + s +", a: "+a);
          }
      }

Child继承了Base,也定义了和基类同名的静态变量s和实例变量a,静态初始化代码块,实例初始化代码块,构造方法,重写了方法step

    public static void main(String[] args) {
        System.out.println("---- new Child()");
        Child c = new Child();
        System.out.println("\n---- c.action()");
        c.action();
        Base b = c;
        System.out.println("\n---- b.action()");
        b.action();
        System.out.println("\n---- b.s: " + b.s);
        System.out.println("\n---- c.s: " + c.s);
    }
	//执行结果    
	---- new Child()
    基类静态代码块, s: 0
    子类静态代码块, s: 0
    基类实例代码块, a: 0
    基类构造方法, a: 1
    子类实例代码块, a: 0
    子类构造方法, a: 10
    ---- c.action()
    start
    child s: 10, a: 20
    end
    ---- b.action()
    start
    child s: 10, a: 20
    end
    ---- b.s: 1
    ---- c.s: 10
类加载的过程

所谓类的加载是指将类的相关信息加载到内存。在Java中,类是动态加载的,当第一次使用这个类的时候才会加载,加载一个类时,会查看其父类是否已加载,如果没有,则会加载其父类

1)一个类的信息主要包括以下部分:

❑ 类变量(静态变量);

❑ 类初始化代码;

❑ 类方法(静态方法);

❑ 实例变量;

❑ 实例初始化代码;

❑ 实例方法;

❑ 父类信息引用。

2)类初始化代码包括:

❑ 定义静态变量时的赋值语句;

❑ 静态初始化代码块。

3)实例初始化代码包括:

❑ 定义实例变量时的赋值语句;

❑ 实例初始化代码块;

❑ 构造方法。

4)类加载过程包括:

❑ 分配内存保存类的信息;

❑ 给类变量赋默认值;

❑ 加载父类;

❑ 设置父子关系;

❑ 执行类初始化代码。

类初始化代码,是先执行父类的,再执行子类的

父类执行时,子类静态变量的值也是有的,是默认值

内存分为栈和堆,栈存放函数的局部变量,而堆存放动态分配的对象,

还有一个内存区,存放类的信息,这个区在Java中称为方法区

​ 类信息内存布局

类信息内存布局

class_init()来表示类初始化代码,用instance_init()表示实例初始化代码,实例初始化代码包括了实例初始化代码块和构造方法

对象创建的过程

创建对象过程包括:

1)分配内存

分配的内存包括本类和所有父类的实例变量,但不包括任何静态变量

2)对所有实例变量赋默认值

3)执行实例初始化代码

实例初始化代码的执行从父类开始,再执行子类的。但在任何类执行初始化代码之前,所有实例变量都已设置完默认值。


创建对象执行的优先原则:

  1. 静态 > 非静态,基类 > 子类
  2. 基类 > 子类
  3. 实例代码块 > 构造方法

每个对象除了保存类的实例变量之外,还保存着实际类信息的引用。

Child c = new Child();会将新创建的Child对象引用赋给变量c,

而Base b = c;会让b也引用这个Child对象。

创建和赋值后,内存布局如图

image-20210607155256615

引用型变量c和b分配在栈中,它们指向相同的堆中的Child对象。Child对象存储着方法区中Child类型的地址,还有Base中的实例变量a和Child中的实例变量a。

方法调用的过程

c.action()执行过程:

1)查看c的对象类型,找到Child类型,在Child类型中找action方法,发现没有,到父类中寻找

2)在父类Base中找到了方法action,开始执行action方法

3)action先输出了start,然后发现需要调用step()方法,就从Child类型开始寻找step()方法

4)在Child类型中找到了step()方法,执行Child中的step()方法,执行完后返回action方法

5)继续执行action方法,输出end。

寻找要执行的实例方法的时候,是从对象的实际类型信息开始查找的,找不到的时候,再查找父类类型信息。


方法调用优先次序:

  1. 自己的方法
  2. 没有则找父类对应方法

我们来看b.action(),这句代码的输出和c.action()是一样的,这称为动态绑定,而动态绑定实现的机制就是根据对象的实际类型查找要执行的方法,子类型中找不到的时候再查找父类。这里,因为b和c指向相同的对象,所以执行结果是一样的

如果继承的层次比较深,要调用的方法位于比较上层的父类,则调用的效率是比较低的,因为每次调用都要进行很多次查找。大多数系统使用一种称为虚方法表的方法来优化调用的效率。

虚方法表,就是在类加载的时候为每个类创建一个表,记录该类的对象所有动态绑定的方法(包括父类的方法)及其地址,但一个方法只有一条记录,子类重写了父类方法后只会保留子类的。对于本例来说,Child和Base的虚方法表如图

image-20210607161323218

对Child类型来说,action方法指向Base中的代码,toString方法指向Object中的代码,而step()指向本类中的代码。当通过对象动态绑定方法的时候,只需要查找这个表就可以了,而不需要挨个查找每个父类。

变量访问的过程

对变量的访问是静态绑定的,无论是类变量还是实例变量。代码中演示的是类变量:b.s和c.s,通过对象访问类变量,系统会转换为直接访问类变量Base.s和Child.s。

例子中的实例变量都是private的,不能直接访问;如果是public的,则b.a访问的是对象中Base类定义的实例变量a,而c.a访问的是对象中Child类定义的实例变量a。

继承是把双刃剑

继承破坏封装

封装就是隐藏实现细节,提供简化接口

继承可能破坏封装是因为子类和父类之间可能存在着实现细节的依赖。子类在继承父类的时候,往往不得不关注父类的实现细节,而父类在修改其内部实现的时候,如果不考虑子类,也往往会影响到子类。

封装如何被破坏
public class Base {
    private static final int MAX_NUM = 1000;
    private int[] arr = new int[MAX_NUM];
    private int count;
    public void add(int number){
        if(count<MAX_NUM){
            arr[count++] = number;
        }
    }
    public void addAll(int[] numbers){
        for(int num : numbers){
            add(num);
        }
    }
}
public class Child extends Base {
    private long sum;
    @Override
    public void add(int number) {
        super.add(number);
        sum+=number;
    }
    @Override
    public void addAll(int[] numbers) {
        super.addAll(numbers);
        for(int i=0; i<numbers.length; i++){
            sum+=numbers[i];
        }
    }
    public long getSum() {
        return sum;
    }
}
//调用
public static void main(String[] args) {
    Child c = new Child();
    c.addAll(new int[]{1,2,3});
    System.out.println(c.getSum());
}
//结果:12

使用addAll添加123,期望的输出是1+2+3=6,实际输出为12!
    为什么是12呢?查看代码不难看出,同一个数字被汇总了两次。
    子类的addAll方法首先调用了父类的add-All方法,而父类的addAll方法通过add方法添加,由于动态绑定,子类的add方法会执行,子类的add也会做汇总操作。

子类和父类之间是细节依赖,子类扩展父类,仅仅知道父类能做什么是不够的,还需要知道父类是怎么做的,而父类的实现细节也不能随意修改,否则可能影响子类。

子类需要知道父类的可重写方法之间的依赖关系,具体到上例中,就是add和addAll方法之间的关系,而且这个依赖关系,父类不能随意改变。但即使这个依赖关系不变,封装还是可能被破坏

父类不能随意增加公开方法,因为给父类增加就是给所有子类增加,而子类可能必须要重写该方法才能确保方法的正确性。总结一下:对于子类而言,通过继承实现是没有安全保障的,因为父类修改内部实现细节,它的功能就可能会被破坏;而对于基类而言,让子类继承和重写方法,就可能丧失随意修改内部实现的自由。

继承没有反映is-a关系

比如,绝大部分鸟都会飞,可能就想给鸟类增加一个方法fly()表示飞,但有一些鸟就不会飞,比如企鹅。在is-a关系中,重写方法时,子类不应该改变父类预期的行为,但是这是没有办法约束的。还是以鸟为例,你可能给父类增加了fly()方法,对企鹅,你可能想,企鹅不会飞,但可以走和游泳,就在企鹅的fly()方法中,实现了有关走或游泳的逻辑。

继承是应该被当作is-a关系使用的,但是,Java并没有办法约束,父类有的属性和行为,子类并不一定都适用,子类还可以重写方法,实现与父类预期完全不一样的行为。

但对于通过父类引用操作子类对象的程序而言,它是把对象当作父类对象来看待的,期望对象符合父类中声明的属性和行为。如果不符合,结果是什么呢?混乱。

如何应对继承的双面性
避免使用继承

❑ 使用final关键字;

❑ 优先使用组合而非继承;

❑ 使用接口。

  • 使用final关键字

    给方法加final修饰符,父类就保留了随意修改这个方法内部实现的自由,使用这个方法的程序也可以确保其行为是符合父类声明的。给类加final修饰符,父类就保留了随意修改这个类实现的自由,使用者也可以放心地使用它,而不用担心一个父类引用的变量,实际指向的却是一个完全不符合预期行为的子类对象。

    给类加final修饰符,父类就保留了随意修改这个类实现的自由,使用者也可以放心地使用它,而不用担心一个父类引用的变量,实际指向的却是一个完全不符合预期行为的子类对象

  • 优先使用组合而非继承

    使用组合可以抵挡父类变化对子类的影响,从而保护子类,应该优先使用组合

    public class Child {
        private Base base;
        private long sum;
        public Child(){
            base = new Base();
        }
        public void add(int number) {
            base.add(number);
            sum+=number;
        }
        public void addAll(int[] numbers) {
            base.addAll(numbers);
            for(int i=0; i<numbers.length; i++){
                sum+=numbers[i];
            }
        }
        public long getSum() {
            return sum;
        }
    }

这样,子类就不需要关注基类是如何实现的了,基类修改实现细节,增加公开方法,也不会影响到子类了。但组合的问题是,子类对象不能当作基类对象来统一处理了。

正确使用继承

使用继承大概主要有三种场景:

1)基类是别人写的,我们写子类;

2)我们写基类,别人可能写子类;

3)基类、子类都是我们写的。

  • 基类是别人写的,我们写子类

基类主要是Java API、其他框架或类库中的类,在这种情况下,我们主要通过扩展基类,实现自定义行为,这种情况下需要注意的是:

❑ 重写方法不要改变预期的行为;

❑ 阅读文档说明,理解可重写方法的实现机制,尤其是方法之间的依赖关系;

❑ 在基类修改的情况下,阅读其修改说明,相应修改子类。

  • 我们写基类,别人可能写子类;

需要注意的是:

❑ 使用继承反映真正的is-a关系,只将真正公共的部分放到基类;

❑ 对不希望被重写的公开方法添加final修饰符;

❑ 写文档,说明可重写方法的实现机制,为子类提供指导,告诉子类应该如何重写;

❑ 在基类修改可能影响子类时,写修改说明。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值