Part1
一、面向对象的三大特征(下)
1.继承和组合
继承是实现类复用的重要手段,但继承有一个坏处:破坏封装。相比之下,组合也是实现类复用的重要手段,而采用组合实现类复用则能提供更好的封装性。组合不同于继承,不需要考虑子类和父类间的逻辑关系,只是单纯地实现代码的复用,因此更加地灵活,核心思想就是“把父类对象作为新类的属性,新类通过调用这个属性来获得父类的属性和方法”。
class Teacher{public int age;public String name;public void rest(){System.out.println("睡觉");}}class Student{Teacher t = new Teacher();//此时t是Student类的一个属性public void rest(){t.rest();//通过t调用Teacher对象的rest方法实现复用}}public class Test {public static void main(String[] args) {Student s = new Student();s.t.age = 23;s.t.name = "sundegan";s.t.rest();System.out.println(s.t.age + s.t.name);}}
继承只能有一个父类,而组合可以有多个属性。需代码复用时,何时使用继承,何时使用组合?
当两者关系是“is-a”时,使用继承;当两者关系是“has—a”时,使用组合。
注:属性的属性只能放到方法里操作,不能直接在方法外部操作。
class Teacher{public int age;public String name;public void rest(){System.out.println("睡觉");}}class Student{Teacher t = new Teacher();//此时t是Student类的一个属性//t.age = 23;//错误//t.name = "sundegan";//错误,属性的属性只能在方法里操作public void rest(){t.age = 23;//在方法里操作就无问题t.name = "sundegan";t.rest();//通过t调用Teacher对象的rest方法实现复用}}public class Test {public static void main(String[] args) {Student s = new Student();s.rest();System.out.println(s.t.age + s.t.name);}}//结果://睡觉//23sundegan
2.多态
同一个方法调用,由于对象不同可能会有不同的行为。现实生活中的例子,同样是休息,张三是睡觉,李四是打游戏。
- 多态的要点
1)多态是方法的多态,不是属性的多态(与属性无关)
2)多态存在的3个必要条件:继承,方法重写,父类引用指向子类对象。
3)父类引用指向子类对象后,用该引用调用子类重写的方法。
- 多态的作用
可以理解为“一个接口,多种实现”,可以把不同子类对象都赋给父类引用,再把这个父类引用当作参数传入到方法中,可以屏蔽子类对象间的内部差异,可以在运行时传入任何的子类对象,就算父类增加新的子类,不用作出修改,方法依旧能够运行,这样可以写出通用的代码,降低耦合性。
class Animal{public void shout(){System.out.println("叫了一声");}}class Dog extends Animal{public void shout(){System.out.println("汪汪汪");}}class Cat extends Animal{public void shout(){System.out.println("喵喵喵");}}public class Test {//根据传入的引用不一样,调用的方法也不一样,这里就形成了多态//任何子类对象都能传入到这个方法中执行static void animalCry(Animal a){a.shout();}//如果不用多态,则需一个个定义,//且父类如果扩展了一个子类,这里又需要把扩展的子类重新添加过来,非常麻烦,耦合性太大/*static void animalCry(Dog a){a.shout();}static void animalCry(Cat a){a.shout();}*/public static void main(String[] args) {Animal dog = new Dog();//向上转型Animal cat = new Cat();//向上转型animalCry(dog);animalCry(cat);}}//结果://汪汪汪//喵喵喵
- 多态注意事项
1)与方法不同的是,实例变量不具备多态性,在通过引用访问实例变量时,指向子类对象的父类引用总是访问它编译时类型所定义的成员变量,即父类的成员变量,如果父类中不存在该变量则报错。
class Animal{int age = 1;}class Dog extends Animal{int age = 2;}class Cat extends Animal{int age = 3;}public class Test {//尝试使用多态调用实例变量static void getAge(Animal a){System.out.println(a.age);}public static void main(String[] args) {Animal dog = new Dog();Animal cat = new Cat();getAge(dog);//总是输出父类中的实例变量,而不是Dog子类中的实例变量getAge(cat);//总是输出父类中的实例变量,而不是Dog子类中的实例变量}}//结果://1//1
2)如果使用多态时,调用的不是子类重写的父类方法,就无多态。比如:父类中的方法,但子类中未对其进行重写,则在调用的时候,调用的是从父类继承得到的方法;假如调用的是子类中自己特有的方法,而父类中没有,则会报错,虽然引用实际指向的仍然是子类,但在编译时类型是父类引用,而父类中无此方法。
class Animal{public void shout(){System.out.println("叫了一声");}}class Cat extends Animal{public void catchMouse(){System.out.println("猫抓老鼠");}public void shout(){System.out.println("喵喵喵");}}public class Test {public static void main(String[] args) {Animal a = new Cat();a.catchMouse();//报错,父类中无这个方法,在编译时找不到这个方法}}
引用变量在编译时,只能调用其编译时类型的方法,但运行时执行的是运行时类型的方法,因此在编写代码使用多态时,要注意只能调用父类中的方法(这个方法可以被不同的子类重写,此时形成多态)。
3.向上转型和向下转型
Java允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,这就是向上转型,由系统自动完成(对于父类引用变量,能通过is-a测试的对象都可以被赋值给该引用)。
引用变量在编译时,只能调用其编译时类型的方法,但运行时执行的是运行时类型的方法,如果需要让这个引用变量调用它运行时类型的方法,则需要把它强制转换成运行时类型,即向下转型。
class Animal{public void shout(){System.out.println("叫了一声");}}class Cat extends Animal{public void catchMouse(){System.out.println("猫抓老鼠");}public void shout(){System.out.println("喵喵喵");}}public class Test {public static void main(String[] args) {Animal a = new Cat();//自动向上转型//a.catchMouse();//报错,父类中无这个方法,在编译时找不到这个方法Cat p = (Cat) a;//强制类型转换,此时才可以调用cat对象特有的方法p.catchMouse();}}//猫抓老鼠
- 这种强制类型转换不是万能的,当强制类型转换时需要注意:
1)基本类型之间的强制类型转换只能在数值类型之间进行,数值类型和布尔类不能进行转换。
2)引用类型之间的强制转换只能在具有继承关系的两个类型之间进行,如果两个没有任何继承关系的类型,则无法进行转换,在编译时会报错。
3)如果试图把一个父类引用转换成子类类型的引用,则这个父类引用所指向的对象必须实际上是子类实例才行,否则将在运行时引发ClassCastException异常。
class Animal{public void shout(){System.out.println("叫了一声");}}class Dog extends Animal{public void shout(){System.out.println("汪汪汪");}}class Cat extends Animal{public void catchMouse(){System.out.println("猫抓老鼠");}public void shout(){System.out.println("喵喵喵");}}public class Test {public static void main(String[] args) {Animal a = new Cat();//自动向上转型//a.catchMouse();//报错,父类中无这个方法,在编译时找不到这个方法Cat p = (Cat) a;//强制类型转换,此时才可以调用Cat对象特有的方法p.catchMouse();Dog d = (Dog) a;//编译检查无问题,但运行时会有异常,因为a所指对象不是Dog类或其子类的实例}}
- instanceof运算符
考虑到强制类型转换时可能出现运行异常,因此转型前应该通过instancof运算符来判断转型是否可以成功,从而避免ClassCastException异常,保证程序的健壮性。
引用类型变量 instanceof 类或接口
如果引用类型变量指向的对象是后面的类的实例或其子类的实例,则返回true,否则返回false。
class Animal{public void shout(){System.out.println("叫了一声");}}class Dog extends Animal{public void shout(){System.out.println("汪汪汪");}}class Cat extends Animal{public void catchMouse(){System.out.println("猫抓老鼠");}public void shout(){System.out.println("喵喵喵");}}public class Test {public static void main(String[] args) {Animal a = new Cat();//判断是否可以强制类型转换if(a instanceof Cat){Cat p = (Cat) cat;}else if(a instanceof Dog){Dog d = (Dog) cat;}}}
3.初始化块
如果是简单赋值可以直接在定义变量时完成赋值,但某些情况下,在赋值时需要把运行相关程序得到的值赋值给这些变量,或者在赋值前进行判断再赋值,这种情况就可以使用初始化块。
初始化块是类里可出现的第4种成员,类的所有成员有:成员变量、方法、构造器、初始化块。
public class Test {public int a = 2;System.out.println(a);//错误,需要<标识符>public static void main(String[] args) {}}
注:类的内部不能书写其他代码,只能由这四部分组成,比如这里打印代码直接写在类内部是错误的。
普通初始化块(实例初始化块)
public class Test {//初始化块{a = 1;}int a;public void print(){System.out.println(a);}public static void main(String[] args) {Test t = new Test();t.print();}}//结果:1
Q1:为什么初始化块里赋值可以不用对a进行定义直接赋值,先赋值再定义?
在类的内部,Field定义先执行,此时a已经在内存中被创建,再执行初始化块。所以,初始化块的位置不是固定的,既可以在变量定义之前,也可以在变量定义之后。代码执行顺序为:
1)变量的声明在任意代码之前执行;
2)静态初始化块、静态变量声明时的赋值语句合在一起执行,执行顺序与它们在代码中的书写顺序相同;
3)实例初始化块、实例变量声明时的赋值语句、构造方法合在一起执行,前两者执行顺序与它们在代码中的书写顺序相同,构造方法最后执行;public class Test {{a = 1;}void print(){System.out.println(a);}public int a = 2;public static void main(String[] args) {new Test().get();}}//2
这里是对a执行完所有的初始化过程后才进行访问,打印出a的值为2。也可以说明,在创建对象时,给对象的变量分配后内存后,所有的初始化过程全部执行完后才执行其余代码。
{a = 1;}int a;和int a;{a = 1;}两者等价,字节码相同
public class Test {//初始化块{a = 1;}int a = 2;//在定义时赋值,在初始化之后public void print(){System.out.println(a);}public static void main(String[] args) {Test t = new Test();t.print();}}//结果:2public class Test {int a = 2;//在定义时赋值,在初始化块之前//初始化块{a = 1;}public void print(){System.out.println(a);}public static void main(String[] args) {Test t = new Test();t.print();}}结果:1
Q2:为什么两次结果不一样?初始化块和定义时都进行赋值,一定是先执行初始化块吗?
如果Field在初始化块之前,在定义时就会得到被赋予的值,然后再经过初始化块赋值,值被改变。
如果Field在初始化块之后,先被定义,先初始化块赋值,再向下执行其他赋值操作。
可以这么理解,不管在之前还是之后,定义和赋值是完全分离的,定义时赋值拆分成两步,此时的赋值操作也相当于一个“初始化块”,即实例初始化块、定义变量时指定的默认值都可认为是初始化代码,总是先执行定义,再按顺序执行所有的初始化块。public class Test {{System.out.println("第一个初始化块");}public Test(){System.out.println("无参构造器");}{System.out.println("第二个初始化块");}int a = 2;public static void main(String[] args) {new Test();}}//结果://第一个初始化块//第二个初始化块//无参构造器
因为初始化块没有名字,也就无标识,总是在创建Java对象时隐式执行,而且在构造器之前自动执行。
注:Java允许定义多个初始化块,但这是没有意义的,因为初始化块总是在构造器之前全部执行,完全可以把多个初始化合并成一个,让程序更加简洁。public class Test {{a = 1;System.out.println(a);//错误:非法前向引用,a为右值引用}int a;public static void main(String[] args) {}}
Q3:为什么这里不能对a访问,会提示非法向前引用?
变量声明最先执行,此时变量a已经存在,按理说不应该出现“非法向前引用”一说。会出现非法向前引用的原因是,这是Java编译器强制进行的检查,Java在初始化成员变量过程中对成员变量的限制,如果成员变量满足以下四点,就必须在使用前对该成员变量先声明才能引用:
1)成员变量所在类或接口(类名或接口名为C)是直接包含该成员变量的;
2)该成员变量在C的静态成员、非静态成员初始化中,或者C的静态、非静态初始化块中;
3)通过简单名称引用的变量,如这里直接通过a这个简单引用变量访问。
4)右值引用,即不是在赋值语句的左边被赋值。
简单来说就是,初始化块中只能访问定义在初始化块之前的变量,定义在它之后的变量,可以被赋值,但是不能被访问。实例初始化块和构造器
与构造器不同,实例初始化块是一段固定执行的代码,它不能接受参数,对同一个类的所有对象进行的初始化处理完全相同。因此,如果有一段初始化处理代码对所有对象都相同,且无须接受参数,就可以放到初始块中。实际上,实例初始化块是一个假象,使用javac编译后,类中的实例初始化块会消失:实例初始化块的内容会被“还原”到每个构造器中,且位于构造器的最前面。
- 静态初始化块(类初始化块)
类初始化块将在类初始化阶段执行,用于对类变量进行初始化,而不是创建对象时执行,因此类初始化块总是在实例初始化块之前执行。
public class Test {//静态初始化块static {a = 2;}static int a = 1;public static void main(String[] args) {System.out.println(new Test().a);}}
注:静态初始化块同样是类的静态成员,不能访问非静态成员(实例变量和实例方法)。
- 初始化块往上追溯执行
如果是实例初始化块,最先执行的实例初始化块代码是Object类的实例初始化块,依次往下。如果是类初始化块,最先执行的是Object类的类初始化块,依次往下。
