在某些情况下,类也确实只是函数的容器,但类更多表示的是自定义数据类型
Math类的常用函数(静态方法)
类方法(静态方法):static修饰,直接通过类名进行调用,不需要创建实例
实例方法:通过实例调用,或者创建对象后进行对象调用
public:表示这些函数是公开的,可以在任何地方被外部调用
private:私有的,这个函数只能在同一个类内被别的函数调用,而不能被外部的类调用
通过private封装和隐藏内部实现细节,避免被误操作,是计算机程序的一种基本思维方式
Arrays类的一些函数
一个数据类型就主要由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{}是静态初始化代码块:静态初始化代码块在类加载的时候执行,这是在任何对象创建之前,且只执行一次
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; }
构造方法有一些特殊的地方:
构造方法:
public:可以修饰类、类方法、类变量、实例变量、实例方法、构造方法,表示可被外部访问。
private:可以修饰类、类方法、类变量、实例变量、实例方法、构造方法,表示不可以被外部访问,只能在类内部被使用。
static:修饰类变量和类方法,它也可以修饰内部类。
this:表示当前实例,可以用于调用其他构造方法,访问实例变量,访问实例方法。
final:修饰类变量、实例变量,表示只能被赋值一次,也可以修饰实例方法和局部变量。
每个类封装其内部细节,对外提供高层次的功能,使其他类在更高层次上考虑和解决问题,是程序设计的一种基本思维方式。
类中实例变量的类型可以是当前定义的类型,两个类之间可以互相引用
带完整包名的类名称为其完全限定名,比如String类的完全限定名为java.lang.String
Java API中所有的类和接口都位于包Java或javax下,Java是标准包,javax是扩展包
使用package声明其包名
包名和文件目录结构必须匹配
为避免命名冲突,Java中命名包名的一个惯例是使用域名作为前缀,因为域名是唯一的,一般按照域名的反序来定义包名,比如,域名是apache.org,包名就以org.apache开头。
如果代码需要公开给其他人用,最好有一个域名以确保唯一性,如果只是内部使用,则确保内部没有其他代码使用该包名即可。
使用有两种方式:一种是通过类的完全限定名;另外一种是将用到的类引入当前类(import …)。
做import操作时,可以一次将某个包下的所有类引入,语法是使用. *
在一个类内,对其他类的引用必须是唯一确定的,不能有重名的类,如果有,则通过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变量 } }
静态导入不应过度使用,否则难以区分访问的是哪个类的代码。
打包方式:
首先到编译后的java class文件根目录,运行
jar -cvf <包名>.jar <最上层包名>
如果Hello.class位于E:\bin\shuo\laoma\Hello.class,则可以到目录 E:\bin下,然后运行:
jar -cvf <hello>.jar <shuo>
hello.jar就是jar包,jar包其实就是一个压缩文件,可以使用解压缩工具打开。
使用jar包:加入类路径(classpath)
完全限定名
,确定的方式是根据import语句和classpath
完全限定名
寻找并加载类,寻找的方式就是在类路径中寻找
Java 9中,清晰地引入了模块的概念,JDK和JRE都按模块化进行了重构,传统的组织机制依然是支持的,但新的应用可以使用模块
父类=基类
子类=派生类
在Java中,所有类都有一个父类Object
Object没有定义属性,但定义了一些方法
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方法,这称之为方法的动态绑定。
多态和动态绑定是计算机程序的一种重要思维方式,使得操作对象的程序不需要关注对象的实际类型,从而可以统一处理不同对象,但又能实现每个对象的特有行为
//父类 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方法,都是静态绑定的。
重载:是指方法名称相同但参数签名不同(参数个数、类型或顺序不同)
重写:是指子类重写与父类相同参数签名的方法
当有多个重名函数的时候,在决定要调用哪个函数的过程中,首先是按照参数类型进行匹配的,换句话说,
寻找在所有重载版本中最匹配的,(都一样先匹配子类的)
然后才看变量的动态类型,进行动态绑定
public表示外部可以访问,private表示只能内部使用
还有一种可见性介于中间的修饰符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方法就是一个模板方法,它定义了实现的模板,而具体实现则由子类提供
重写时,子类方法不能降低父类方法的可见性。不能降低是指,父类如果是public,则子类也必须是public,父类如果是protected,子类可以是protected,也可以是public,即子类可以升级父类方法的可见性但不能降低
原因:继承反映的是“is-a”的关系,即子类对象也属于父类,子类必须支持父类所有对外的行为,将可见性降低就会减少子类对外的行为,从而破坏“is-a”的关系,但子类可以增加父类的行为,所以提升可见性是没有问题的。
一个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)执行实例初始化代码。
实例初始化代码的执行从父类开始,再执行子类的。但在任何类执行初始化代码之前,所有实例变量都已设置完默认值。
创建对象执行的优先原则:
每个对象除了保存类的实例变量之外,还保存着实际类信息的引用。
Child c = new Child();会将新创建的Child对象引用赋给变量c,
而Base b = c;会让b也引用这个Child对象。
创建和赋值后,内存布局如图
引用型变量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。
寻找要执行的实例方法的时候,是从对象的实际类型信息开始查找的,找不到的时候,再查找父类类型信息。
方法调用优先次序:
我们来看b.action(),这句代码的输出和c.action()是一样的,这称为动态绑定,而动态绑定实现的机制就是根据对象的实际类型查找要执行的方法,子类型中找不到的时候再查找父类。这里,因为b和c指向相同的对象,所以执行结果是一样的
如果继承的层次比较深,要调用的方法位于比较上层的父类,则调用的效率是比较低的,因为每次调用都要进行很多次查找。大多数系统使用一种称为虚方法表的方法来优化调用的效率。
虚方法表,就是在类加载的时候为每个类创建一个表,记录该类的对象所有动态绑定的方法(包括父类的方法)及其地址,但一个方法只有一条记录,子类重写了父类方法后只会保留子类的。对于本例来说,Child和Base的虚方法表如图
对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添加1、2、3,期望的输出是1+2+3=6,实际输出为12! 为什么是12呢?查看代码不难看出,同一个数字被汇总了两次。 子类的addAll方法首先调用了父类的add-All方法,而父类的addAll方法通过add方法添加,由于动态绑定,子类的add方法会执行,子类的add也会做汇总操作。
子类和父类之间是细节依赖,子类扩展父类,仅仅知道父类能做什么是不够的,还需要知道父类是怎么做的,而父类的实现细节也不能随意修改,否则可能影响子类。
子类需要知道父类的可重写方法之间的依赖关系,具体到上例中,就是add和addAll方法之间的关系,而且这个依赖关系,父类不能随意改变。但即使这个依赖关系不变,封装还是可能被破坏
父类不能随意增加公开方法,因为给父类增加就是给所有子类增加,而子类可能必须要重写该方法才能确保方法的正确性。总结一下:对于子类而言,通过继承实现是没有安全保障的,因为父类修改内部实现细节,它的功能就可能会被破坏;而对于基类而言,让子类继承和重写方法,就可能丧失随意修改内部实现的自由。
比如,绝大部分鸟都会飞,可能就想给鸟类增加一个方法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修饰符;
❑ 写文档,说明可重写方法的实现机制,为子类提供指导,告诉子类应该如何重写;
❑ 在基类修改可能影响子类时,写修改说明。