类的基本概念
- pubilc: 可以修饰类、类方法、类变量、实例变量、实例方法。构造方法,表示可被外部访问
- private:可以修饰类、类方法、类变量、实例变量、实例方法。构造方法,表示不可以被外部访问,只能在类内部被使用
- static:修饰类变量和类方法,它也可以修饰内部类
- this:表示当前实例,可以用于调用其他构造方法,访问实例变量,访问实例方法
- final:修饰类变量、实例变量,表示只能被赋值一次,也可以修饰实例方法和局部变量
类的继承
基本概念
- 每个类有且只有一个父类,没有声明父类的,其父类为Object,子类继承了父类非private的属性和方法,可以增加自己的属性和方法,以及重写父类的方法实现。
- new过程中,父类先进行初始化,子类可以通过super调用父类相应的构造方法,没有使用super的情况下,调用父类的默认构造方法。如果父类没有默认的构造方法,则必须指定
- 子类变量和方法与父类重名的情况下,可通过super强制访问父类的变量和方法
- 子类对象可以赋值给父类引用变量,这叫多态;实际执行调用的是子类实现,这叫动态绑定。
这里对多态、动态绑定给出一个示例进行说明。
这里以图形为例,所有图形(Shape)都有一个表示颜色的属性,有一个表示绘制的方法
public class Shape {
private String color;
private static final String DEFAULT_COLOR = "black";
public Shape() {
this(DEFAULT_COLOR);
}
public Shape(String color) {
this.color = color;
}
public void draw() {
System.out.println("draw Shape");
}
// 省略color的getter/setter
}
再来一个圆(Circle)继承自Shape,但它有额外的属性:半径
public class Circle extends Shape{
// 半径
private double r;
@Override
public void draw() {
System.out.println("draw circle, the r is " +
this.r + ",using color is: " + getColor());
}
}
再来一条直线(Line)继承自Shape,它有额外的属性:长度
public class Line extends Shape{
private int length;
public Line(int length, String color) {
super(color);
this.length = length;
}
@Override
public void draw() {
System.out.println("Draw line, the color is " + getColor());
}
}
最后就进行测试,我们新建一个Circle和一个Line,将它们放入一个Shape的集合。然后执行draw方法
public static void main(String[] args) {
List<Shape> shapes = new ArrayList<>();
shapes.add(new Line(998, "RED"));
shapes.add(new Circle());
for(Shape shape: shapes) {
shape.draw();
}
}
最后的输出是
Draw line, the color is RED
draw circle, the r is 0.0,using color is: black
在shapes.add中,参数是Shape,而实际的类型则分别是Circle和Line。子类对象赋值给父类引用变量,这叫向上转型。
变量shape可以引用任何Shape子类类型的对象,这叫多态,即一种类型的变量,可引用多重实际类型对象。这样,对于变量shape,它就有两种类型:类型Shape,我们称之为静态类型,类型Circle/Line,我们称之为动态类型。
shape.draw()调用的是其对应的动态类型的draw方法,这称之为方法的动态绑定
继承的细节
构造方法
前面说到:子类可以通过super调用父类相应的构造方法,没有使用super的情况下,调用父类的默认构造方法。如果父类没有默认的构造方法,则必须指定。
这里举例说明一下
public class Base {
private String name;
public Base(String name) {
this.name = name;
}
}
父类只有一个带参数的构造方法,没有默认构造方法。
它的子类都必须在构造方法中通过super调用Base的带参构造方法,否则会报错:
静态绑定
当子类和父类中有重名的变量或方法。
private变量和方法只能在类内访问,访问也永远是当前类的,即:在子类中访问的是子类的;在父类中访问的是父类的。
public变量和方法,则要看如何访问它。
1. 在类内,访问的是当前类的,但子类可以通过super.明确指定访问父类的。
2. 在类外,则要看访问变量的静态类型:静态类型是父类,则访问父类的变量和方法;静态类型是子类,则访问的是子类的变量和方法
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 = "static_child";
public String m = "child";
public static void staticTest() {
System.out.println("child static " + s);
}
}
public static void main(String[] args) {
Child child = new Child();
Base b = child;
System.out.println(b.m);
System.out.println(b.s);
b.staticTest();
System.out.println(child.m);
System.out.println(child.s);
child.staticTest();
}
输出
base
static_base
base static static_base
child
static_child
child static static_child
当通过b(静态类型Base)访问时,访问的是Base的变量和方法,当通过child(静态类型Child)访问时,访问的是Child的变量和方法,这称之为静态绑定,即访问绑定到变量的静态类型。
重载和重写
重载是指方法名称相同但参数签名不同(参数个数、类型或顺序不同)
重写是指子类重写与父类相同的参数签名的方法
当有多个重名函数的时候,在决定要调用哪个函数的过程中,首先是按照参数类型进行匹配的,换句话说,寻找在所有重载版本中最匹配的,然后才看变量的动态类型,进行动态绑定。
父子类型转换
子类型的对象可以赋值给父类型的引用变量,这是向上转型。
那么父类型的变量可以赋值给子类型的变量吗? 或者说可以向下转型吗?
Base b = new Child();
// 将变量b的类型强制转换为Child并赋值为c
Child c = (Child)b;
因为b的动态类型就是Child,所以上面那样是不会出错的。
但是,
Base b = new Base();
Child c = (Child)b;
语法上不会有错,但是运行时会抛出错误,错误为类型转换异常。
一个父类的变量能不能转换为一个子类的变量,取决于这个父类变量的动态类型(即引用的对象类型)是不是这个子类或者这个子类的子类
给定一个父类的变量能不能知道她是否可以安全的向下转型呢?
if(b instanceof Child) {
Child c = (Child)b;
}
继承访问权限protected
protected表示虽然不能被外部任意访问,但可被子类访问,还表示可以被同一包中的其他类访问,不管其他类是不是该类的子类。
可见性重写
重写方法时,一般并不会修改方法的可见性。
但是,重写时,子类方法不能降低父类方法的可见性。
父类如果是public,则子类也必须是public;父类如果是protected,子类可以是protected,也可以是public。 即子类可以升级父类方法的可见性但不能降低。
why?
继承反映的是“is-a”的关系,即子类对象也属于父类,子类必须支持父类所有对外的行为,将可见性降低就会减少子类对外的行为,从而破坏“is-a”的关系。
继承实现的基本原理
类加载过程
所谓类的加载是指将类的相关信息加载到内存。在Java中,类是动态加载的,当第一次使用这个雷电 时候才会加载,加载一个类时,会查看其父类是否已经加载,如果没有,则会加载其父类。
一个类的信息主要包括以下部分:
- 类变量 (静态变量)
- 类初始化代码
- 定义静态变量时的赋值语句
- 静态初始化代码块
- 类方法
- 实例变量
- 实例初始化代码
- 定义实例变量时的赋值语句
- 实例初始化代码块
- 构造方法
- 实例方法
- 父类信息引用
为什么说继承是把双刃剑
什么是封装呢? 封装就是隐藏实现细节,提供简化接口,使用者只需要关注怎么用,不需要关注内部是怎么实现的。
封装是如何被破坏的
这里直接看代码的示例来说明
public class Base {
private static final int MAX_NUM = 1000;
private int[] arr = new int[MAX_NUM];
private int count;
// 添加单个数字
public void add(int num) {
if(count < MAX_NUM) {
arr[count++] = num;
}
}
// 添加一串数组
public void addAll(int[] nums) {
for(int num : nums) {
// 内部通过add,添加单个数字方法实现
add(num);
}
}
}
public class Child extends Base{
// 添加过的数字的总和
private long sum;
// 添加一个数字,并计算和
@Override
public void add(int num) {
super.add(num);
sum+=num;
}
// 添加一个数字,并计算和
@Override
public void addAll(int[] nums) {
super.addAll(nums);
for (int i = 0; i < nums.length; i++) {
sum+=nums[i];
}
}
// 得到当前数字的和
public long getSum() {
return sum;
}
}
public static void main(String[] args) {
Child child = new Child();
child.addAll(new int[]{1,2,3});
System.out.println(child.getSum());
}
输出结果
12
其实期望得到的结果应该是6。但是由于子类的addAll方法调用了父类的addAll,而父类的addAll方法是通过add来添加的,由于动态绑定,子类的add方法被执行了。所以每个数字被统计了两次。
可以看出,如果子类不知道父类方法的实现细节,它就不能正确地进行扩展了。
所以仅仅知道父类能做什么是不够的,还需要知道父类是怎么做的,而父类的实现细节也不能随意修改,否则可能影响子类。
当然,对于这个示例,可以通过编码的方法来避免,但是这只是一个举例,说明继承可能会破坏封装。