目录
- 1 重载
- 1.1 重载和重写注意点
- 1.2 重载概念
- 1.3 重载问题
- 1.3.1 重载中null和有形参
- 1.3.2 重载中string和stringbuffer
- 1.3.3 重载中无参和不定长参数
- 2 重写
- 2.1 重写概念
- 2.2 重写问题
- 2.2.1 属性,静态属性,静态方法不能被重写
- 2.2.2 子类为什么不能继承父类静态
- 2.2.3 子类重写父类注意事项
- 2.2.4 子类继承父类时问题
- 3 初始化问题
- 3.1 初始化顺序
- 3.2 初始化条件
- 3.2.1 可以初始化本类条件
- 3.2.2 不能初始化子类条件
- 3.3 父类构造函数分析
- 3.3.1 父类缺省构造函数
- 3.3.2 保护父类构造函数
- 3.4 特例分析
- 3.4.1 特例一:父子初始化分析
- 3.4.2 特例二:静态属性包含实例化分析
- 3.4.3 特例三:子类加载但不初始化
1 重载
1.1 重载和重写注意点
重载和重写的关键点:
-
private
: 一个私有的java方法是不能被重写的,因为它对子类压根就不可见 -
final
:重载一个final
的方法是可以的,但是不能重写它,因此父类如果将方法声明为final
的就可保证所有子类的调用此方法时调用的都是父类的方法。
final:如果两个方法有同样的参数列表,而其中一个的参数被声明为final
的这种情况下这两个方法完全一样,因此不可重载。编译都通不过,因为这两个方法被视为完全一样。 -
static
:可以重载一个静态的Java
方法但是不能重写静态的Java
方法,因为静态方法在方法区中只有一个。 -
static
:重载是关于对象(实例)和继承而言的。一个声明为静态的方法属于整个类(对于这个的所有对象都是一样的)。因此重写它没有任何意义。
1.2 重载概念
重载向类中添加更多的属性、方法,把它变得看起来更庞大作为重载的象征。事实上正确的方式是,当我们使用重载时外部看起来我们的类会变得更加紧凑
重载:方法名必须相同,参数类型必须不同
,包括但不限于一项:参数数目,参数类型,参数顺序,与方法的返回值无关,与权限修饰符无关
1.3 重载问题
我们先看两段代码:
public classTest2 {
public static void main(String[] args) {
f1(null);
f2();
}
public static void f1(String s) {
System.out.println("执行哪个方法?我是String");
}
public static void f1(Object o) {
System.out.println("执行哪个方法?我是Object");
}
public static void f2(){
System.out.println("执行哪个方法?我是无参数");
}
public static void f2(String...strings){
System.out.println("执行哪个方法?我是不定长参数");
}
}
1.3.1 重载中null和有形参
我们在调用f1(null)
,理论上调用两个方法都是可以运行的,但是jvm
肯定不能两个方法都运行,只能选择其中的一个,它会选择哪个?jvm
会选择 String
参数的方法,因为根据方法重载中准确性的原则,从层次上看Object
处在更上层,String
是从Object
继承过来的,调用String
将更准确。
1.3.2 重载中string和stringbuffer
如果我再加一个方法
public static void f1(StringBuffer s) {
System.out.println("执行哪个方法?我是String");
}
这时再调用f1(null);
就不能通过编译,为什么呢?由于StringBuffer
和String
并没有继承上的关系,因此编译器感觉StringBuffer
和String
作为参数的方法都很准确,它就不知道该执行哪个方法了,会出现编译错误,违反了重载中唯一性的原则
1.3.3 重载中无参和不定长参数
而我们在调用f2();
方法时,jvm
又会执行哪个?答案是无参数的。其实不定长参数在编译器编译之后它会将这个f2(String...strings)
编译成参数为数组的方法,我们可以通过代码证明:
Class clazz=Test2.class;
Method[]methods=clazz.getDeclaredMethods();
for (Method method :methods) {
System.out.println(method);
}
结果为:public static void Test2.f2(java.lang.String[])
所以直接调用不传参数调用f2()
自然就是调用无参数方法最正确。而如果没有f2(){......};
这个方法我们调用f2()
依然可以运行,这是因为不定长参数支持无参数
,但是这里的支持无参数其实是编译器自动帮我们填充一个new String[]{""}
的形式调用,所以,本质上来讲,就是一个以数组为参数的调用方法。并且如果我们定义一个void f2(String[] s)
的方法会提示重复
2 重写
2.1 重写概念
Overriding
重写
当继承一个类的时候,根据父类方法的访问修饰符 子类可以获得所有protect
以上的父类方法。但是父类的方法的具体行为可能在子类中并不适合,因此我们需要根据子类对于这个方法的需求重写继承自父类的这个方法。重写后原来的旧方法对于这个子类会完全废弃。
重写
:要求两同两小一大
原则
方法名相同,参数类型相同,子类返回类型小于等于父类方法返回类型, 子类抛出异常小于等于父类方法抛出异常, 子类访问权限大于等于父类方法访问权限。注意
:这里的返回类型必须要在有继承关系
的前提下比较
2.2 重写问题
2.2.1 属性,静态属性,静态方法不能被重写
java
中静态属性和和静态方法可以被继承,但是没有被重写(overwrite
)而是被隐藏。静态方法和属性
是属于类的,调用的时候直接通过类名.方法名
完成的,不需继承机制就可以调用如果子类里面定义了静态方法和属性,那么这时候父类的静态方法或属性称之为隐藏
,如果想要调用父类的静态方法和属性,直接通过父类名.方法名
或变量名完成,至于是否继承一说,子类是有继承静态方法和属性,但是跟实例方法和属性不太一样,存在隐藏
的这种情况。
多态之所以能够实现是依赖于 继承
接口和 重写
、重载
(继承和重写最为关键)。有了继承和重写就可以实现 父类的引用可以指向不同子类的对象
。
因此,多态存在的三个必要条件:继承,重写,父类引用指向子类对象:Parent p = new Child()
重写的功能是:重写
后子类的优先级要高于父类的优先级,但是隐藏
是没有这个优先级之分的。
注意:
静态属性
、静态方法
和非静态的属性
都可以被 继承
和 隐藏
而不能够被重写
,因此不能实现多态,不能实现父类的引用可以指向不同子类的对象。非静态的方法可以被继承和重写,因此可以实现多态
public class A//父类
{
public static String str = "静态属性";
public String name = "非静态属性";
public static void sing()
{
System.out.println("静态方法");
}
public void run()
{
System.out.println("非静态方法");
}
}
public class B extends A //子类B
{
public static String str = "B该改写后的静态属性";
public String name ="B改写后的非静态属性";
public static void sing()
{
System.out.println("B改写后的静态方法");
}
}
public class C extends A //子类C继承A中的所有属性和方法
{
}
public class Test//测试类
{
public static void main(String[] args)
{
C c = new C();
System.out.println(c.name);
System.out.println(c.str);
输出的结果都是父类中的非静态属性、静态属性和静态方法,推出静态属性和静态方法可以被继承
c.sing();
A c1 = new C();
System.out.println(c1.name);
System.out.println(c1.str);
结果同上,输出的结果都是父类中的非静态属性、静态属性和静态方法,推出静态属性和静态方法可以被继承
c1.sing();
B b = new B();
System.out.println(b.name);
System.out.println(b.str);
结果都是子类的非静态属性,静态属性和静态方法,这里和非静态属性和非静态类的继承相同
b.sing();
A b1 = new B();
结果是父类的静态属性,说明静态属性不可以被重写,不能实现多态
System.out.println(b1.str);
结果是父类的非静态属性,说明非静态属性不可以被重写,不能实现多态
System.out.println(b1.name);
结果都是父类的静态方法,说明静态方法不可以被重写,不能实现多态
b1.sing();
}
}
2.2.2 子类为什么不能继承父类静态
重写
只能适用于实例方法
.不能用于静态方法 .对于静态方法,只能隐藏
(形式上被重写了,但是不符合的多态的特性),重写
是用来实现多态性的,只有实例方法是可以实现多态,而静态方法无法实现多态。例如:
Employee man = new Manager();
man.test();
实例化的这个对象中,声明的man
变量是Employee
类的,变量名存在栈
中,而内存堆中为对象申请的空间却是按照Manager
类来的,就是Employee
类型的man
变量的指针指向了一个Manager
类的对象。如果对这个man
调用方法,调用的是谁的?如果是非静态方法,编译时编译器以为是要调用Employee
类的,可是实际运行时,解释器就从堆上开工了,实际上是从Manager
类的那个对象上走的,所以调用的方法实际上是Manager
类的方法。有这种结果关键在于man
实际上指向了Manager
类对象。
现在用man
来调用静态方法,实际上此时是Employee
类在调用静态方法,Employee
类本身肯定不会指向Manager
类的对象,那么最终调用的是Employee
类的方法。
由此,只能说形式上静态方法的却可以被重写,实际上达不到重写的效果,从多态的角度可以认为子类实际上是写了一个新方法,从这个角度上说静态方法无法被重写。那么也就证明了重写和覆盖就是一回事
2.2.3 子类重写父类注意事项
子类重写父类注意事项:
- 重写方法不能比被重写方法限制有更严格的访问级别
不能有更严格权限但是可以更广泛比如父类方法是包访问权限,子类的重写方法是public访问权限 比如:Object类有个toString()方法,开始重写这个方法的时候我们总容易忘记public修饰符,编译器当然不会放过任何教训我们 的机会。出错的原因就是:没有加任何访问修饰符的方法具有包访问权限,包访问权限比public当然要严格了,所以编译器会报错的 - 参数列表必须与被重写方法的相同
重写
有个孪生的弟弟叫重载
。如果子类方法的参数与父类对应的方法不同,那么就是你认错人了,那是重载,不是重写 - 返回类型必须与被重写方法的返回类型相同
父类方法A:void eat(){}
子类方法B:int eat(){}
两者虽然参数相同,可是返回类型不同,所以不是重写
父类方法A:int eat(){}
子类方法B:long eat(){}
返回类型虽然兼容父类,但是不同就是不同,所以不是重写 - 不能重写被标识为
final
的方法 - 重写方法不能抛出新的异常或者比被重写方法声明的检查异常更广的检查异常。但是可以抛出更少,更有限或者不抛出异常
子类抛出的异常类型不能比父类抛出的异常类型更宽泛!
对于这句话你还少了一个条件应该是子类重写父类方法不能抛出比父类更宽的异常类型的把 - 如果一个方法不能被继承,则不能重写它,如果父类方法是
private
,但是子类重写了个public
相同方法,那么它不是重写也不是重载,属于子类的全新的方法
2.2.4 子类继承父类时问题
如下代码:
父类代码:
public class SuperClass {
private int mSuperX;
public SuperClass() {
setX(99);
}
public void setX(int x) {
mSuperX = x;
}
}
子类代码
public class SubClass extends SuperClass {
private int mSubX = 1;
public SubClass() {}
@Override
public void setX(int x) {
super.setX(x);
mSubX = x;
System.out.println("SubX is assigned " + x);
}
public void printX() {
System.out.println("SubX = " + mSubX);
}
}
使用mSubX
来跟踪mSuperX
最后在main里调用:
public class Main {
public static void main(String[] args) {
SubClass sc = new SubClass();
sc.printX();
}}
理想中的结果是:
SubX is assigned 99
SubX = 99
实际结果:
SubX is assigned 99
SubX = 1
结果分析:
我们都知道Java
是面向对象的语言, 面向对象三大特性之一多态性. 假如父类构造方法中调用了某个方法, 这个方法恰好被子类重写了, 会发生什么?
根据多态性, 实际被调用的是子类的方法
, 这个没错. 再考虑有继承时, 初始化的顺序. 如果是new
一个子类, 那么初始化顺序是:父类static成员 -> 子类static成员 -> 父类普通成员初始化和初始化块 -> 父类构造方法 -> 子类普通成员初始化和初始化块 -> 子类构造方法
父类构造方法中调用了一次setX
, 此时mSubX
中已经是我们要跟踪的值, 但之后子类普通成员初始化将mSubX
又初始化了一遍, 覆盖了前面我们跟踪的值, 自然得到的值就是错的
3 初始化问题
3.1 初始化顺序
先说下java
对象初始化步骤:
-
本类
:静态变量->静态初始化块->变量->初始化块->构造函数 -
继承类
:父类静态变量->父类静态初始化块->子类静态变量->子类静态初始化块->父类变量->父类初始化块->父类构造函数->子类变量->子类初始化块->子类构造函数
注意
:有多个静态变量或代码块则按顺序
加载,可参考特例二分析
3.2 初始化条件
3.2.1 可以初始化本类条件
需要对类进行初始化:
- 使用
new
关键字、反射、clone
、反序列化机制实例化对象时; - 读取或设置类静态字段的时候(但被
final
修饰的字段,在编译器时就被放入常量池的静态字段除外static final
); - 调用类静态方法的时候;
- 使用反射
Class.forName("全限定类名")
对类进行反射调用的时候,该类需要初始化; - 初始化一个类的时候,有父类,先初始化父类(注:1. 接口除外,父接口在调用的时候才会被初始化;2.子类引用父类静态字段,只会引发父类初始化);
- 被标明为启动类的类(即包含
main()
方法的类)要初始化; - 当使用
JDK1.7
的动态语言支持时,如果一个java.invoke.MethodHandle
实例最后的解析结果REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
3.2.2 不能初始化子类条件
不会初始化子类的:
- 调用的是父类的static方法或者字段
只会触发子类的加载、父类初始化,不会导致子类初始化
可参考特例三分析 - 调用的是父类的final方法或者字段
- 通过数组来引用,不会触发此类初始化
3.3 父类构造函数分析
3.3.1 父类缺省构造函数
缺省构造函数的问题:假如base
类是父类,derived
类是子类,首先要说明的是由于先有父类后有子类,所以生成子类之前要首先有父类。class
是由class
的构造函数constructor
产生的,每一个class
都有构造函数,如果你在编写自己的class
时没有编写任何构造函数,那么编译器为你自动产生一个缺省default
构造函数。这个default构造函数实质是空的,其中不包含任何代码。但是一牵扯到继承,它的问题就出现了。
如果父类base class
只有缺省构造函数,也就是编译器自动为你产生的。而子类中也只有缺省构造函数,那么不会产生任何问题,因为当你试图产生一个子类的实例时,首先要执行子类的构造函数,但是由于子类继承父类,所以子类的缺省构造函数自动调用父类的缺省构造函数。先产生父类的实例,然后再产生子类的实例。
class base{
}
class derived extends base{
public static void main(String[] args){
derived d=new derived();
}
}
下面我自己显式地加上了缺省构造函数:
class base{
base(){
System.out.println("base constructor");
}
}
class derived extends base{
derived(){
System.out.println("derived constructor");
}
public static void main(String[] args){
derived d=new derived();
}
}
执行结果如下:说明了先产生 base class
然后是derived class
base constructor
derived constructor
我要说明的问题出在如果base class
有多个constructor
而derived class
也有多个constructor
,这时子类中的构造函数缺省调用那个父类的构造函数呢?答案是调用父类的缺省构造函数。但是不是编译器自动为你生成的那个缺省构造函数而是你自己显式地写出来的缺省构造函数。
class base{
base(){
System.out.println("base constructor");
}
base(int i){
System.out.println("base constructor int i");
}
}
class derived extends base{
derived(){
System.out.println("derived constructor");
}
derived(int i){
System.out.println("derived constructor int i");
}
public static void main(String[] args){
derived d=new derived();
derived t=new derived(9);
}
}
运行结果
base constructor
derived constructor
base constructor
derived constructor int i
如果将base
类的构造函数注释掉,则出错。
class base{
// base(){
// System.out.println("base constructor");
// }
base(int i){
System.out.println("base constructor int i");
}
}
class derived extends base{
derived(){
System.out.println("derived constructor");
}
derived(int i){
System.out.println("derived constructor int i");
}
public static void main(String[] args){
derived d=new derived();
derived t=new derived(9);
}
}
运行结果
derived.java:10: cannot resolve symbol
symbol : constructor base ()
location: class base
derived(){
^
derived.java:13: cannot resolve symbol
symbol : constructor base ()
location: class base
derived(int i){
2 errors
说明子类中的构造函数找不到显式写出的父类中的缺省构造函数,所以出错。
3.3.2 保护父类构造函数
那么如果你不想子类的构造函数调用你显式写出的父类中的缺省构造函数怎么办呢?
如下例:
class base{
// base(){
// System.out.println("base constructor");
// }
base(int i){
System.out.println("base constructor int i");
}
}
class derived extends base{
derived(){
super(8);
System.out.println("derived constructor");
}
derived(int i){
super(i);
System.out.println("derived constructor int i");
}
public static void main(String[] args){
derived d=new derived();
derived t=new derived(9);
}
}
运行结果
base constructor int i
derived constructor
base constructor int i
derived constructor int i
super(i)
表示父类的构造函数base(i)
请大家注意
一个是super(i)
一个是super(8)
结论:子类如果有多个构造函数的时候,父类要么没有构造函数,让编译器自动产生,那么在执行子类构造函数之前先执行编译器自动产生的父类的缺省构造函数;要么至少要有一个显式的缺省构造函数可以让子类的构造函数调用
3.4 特例分析
3.4.1 特例一:父子初始化分析
public class BaseDemo {
private String baseName = "base";
public BaseDemo() {
callName();
}
public void callName() {
System.out.println(baseName);
}
static class Sub extends BaseDemo {
private String baseName = "sub";
public void callName() {
System.out.println(baseName);
}
}
public static void main(String[] args) {
BaseDemo b = new Sub();
}
}
输出结果:
null
以上例子输出结果为null
,仔细想想对象初始化顺序就可以明白,父类构造器执行的时候,调用了子类的重载方法,然而子类的类字段还在刚初始化的阶段,刚完成内存布局,只能输出null
3.4.2 特例二:静态属性包含实例化分析
静态变量或代码块只初始化一次,有多个静态变量或代码块则按顺序加载
public class InitDemo {
public static InitDemo i1=new InitDemo();
public static InitDemo i2=new InitDemo();
{
System.out.println("构造块");
}
static{
System.out.println("静态块");
}
public static void main(String[] args) {
new InitDemo();
}
}
输出结果:
构造块
构造块
静态块
构造块
原因分析:
开始时JVM
加载InitDemo.class
,对所有的静态成员进行声明,i1
和i2
被初始化为默认值为null
,又因i1
和i2
需要被显示初始化,所以对i1进行显示初始化:初始化代码块->构造函数,此时不调用static代码块,因为在开始时已经对static部分进行了初始化,虽然只对static变量进行了初始化,但在初始化i1时也不会再执行static块,因为JVM认为这是第二次加载类InitDemo了,所以static会在i1初始化时被忽略掉,所以直接初始化非static部分,也就是构造部分接着构造函数
i1初始化完成后接着对i2进行初始化,过程同i1
i1和i2初始化后就完成了所有static变量初始化,开始按顺序
执行static块部分
static变量和代码块初始化顺序是由位置决定的
这个例子把static块放在了静态变量前面
public class InitDemo {
static{
System.out.println("静态块");
}
public static InitDemo i1=new InitDemo();
public static InitDemo i2=new InitDemo();
{
System.out.println("构造块");
}
public static void main(String[] args) {
new InitDemo();
}
}
输出结果:
静态块
构造块
构造块
构造块
3.4.3 特例三:子类加载但不初始化
父类代码
public class ParentDemo {
public static int abc=123;
static {
System.out.println("Parent is init 。。。。");
}
}
子类代码
public class SubDemo extends ParentDemo{
static {
System.out.println("Sub is init 。。。。");
}
}
测试类代码
public class TestDemo {
public static void main(String[] args) {
System.out.println(SubDemo.abc);
}
}
执行结果
Parent is init 。。。。
123
此处调用的是父类的static方法或者字段,只会触发子类的加载、父类初始化,不会导致子类初始化