Part1

一、面向对象的三大特征(下)

1.继承和组合

继承是实现类复用的重要手段,但继承有一个坏处:破坏封装。相比之下,组合也是实现类复用的重要手段,而采用组合实现类复用则能提供更好的封装性。组合不同于继承,不需要考虑子类和父类间的逻辑关系,只是单纯地实现代码的复用,因此更加地灵活,核心思想就是“把父类对象作为新类的属性,新类通过调用这个属性来获得父类的属性和方法”。

  1. class Teacher{
  2. public int age;
  3. public String name;
  4. public void rest(){
  5. System.out.println("睡觉");
  6. }
  7. }
  8. class Student{
  9. Teacher t = new Teacher();//此时t是Student类的一个属性
  10. public void rest(){
  11. t.rest();//通过t调用Teacher对象的rest方法实现复用
  12. }
  13. }
  14. public class Test {
  15. public static void main(String[] args) {
  16. Student s = new Student();
  17. s.t.age = 23;
  18. s.t.name = "sundegan";
  19. s.t.rest();
  20. System.out.println(s.t.age + s.t.name);
  21. }
  22. }

继承只能有一个父类,而组合可以有多个属性。需代码复用时,何时使用继承,何时使用组合?
当两者关系是“is-a”时,使用继承;当两者关系是“has—a”时,使用组合。
注:属性的属性只能放到方法里操作,不能直接在方法外部操作。

  1. class Teacher{
  2. public int age;
  3. public String name;
  4. public void rest(){
  5. System.out.println("睡觉");
  6. }
  7. }
  8. class Student{
  9. Teacher t = new Teacher();//此时t是Student类的一个属性
  10. //t.age = 23;//错误
  11. //t.name = "sundegan";//错误,属性的属性只能在方法里操作
  12. public void rest(){
  13. t.age = 23;//在方法里操作就无问题
  14. t.name = "sundegan";
  15. t.rest();//通过t调用Teacher对象的rest方法实现复用
  16. }
  17. }
  18. public class Test {
  19. public static void main(String[] args) {
  20. Student s = new Student();
  21. s.rest();
  22. System.out.println(s.t.age + s.t.name);
  23. }
  24. }
  25. //结果:
  26. //睡觉
  27. //23sundegan

2.多态

同一个方法调用,由于对象不同可能会有不同的行为。现实生活中的例子,同样是休息,张三是睡觉,李四是打游戏。

  • 多态的要点

1)多态是方法的多态,不是属性的多态(与属性无关)
2)多态存在的3个必要条件:继承,方法重写,父类引用指向子类对象。
3)父类引用指向子类对象后,用该引用调用子类重写的方法。

  • 多态的作用

可以理解为“一个接口,多种实现”,可以把不同子类对象都赋给父类引用,再把这个父类引用当作参数传入到方法中,可以屏蔽子类对象间的内部差异,可以在运行时传入任何的子类对象,就算父类增加新的子类,不用作出修改,方法依旧能够运行,这样可以写出通用的代码,降低耦合性。

  1. class Animal{
  2. public void shout(){
  3. System.out.println("叫了一声");
  4. }
  5. }
  6. class Dog extends Animal{
  7. public void shout(){
  8. System.out.println("汪汪汪");
  9. }
  10. }
  11. class Cat extends Animal{
  12. public void shout(){
  13. System.out.println("喵喵喵");
  14. }
  15. }
  16. public class Test {
  17. //根据传入的引用不一样,调用的方法也不一样,这里就形成了多态
  18. //任何子类对象都能传入到这个方法中执行
  19. static void animalCry(Animal a){
  20. a.shout();
  21. }
  22. //如果不用多态,则需一个个定义,
  23. //且父类如果扩展了一个子类,这里又需要把扩展的子类重新添加过来,非常麻烦,耦合性太大
  24. /*
  25. static void animalCry(Dog a){
  26. a.shout();
  27. }
  28. static void animalCry(Cat a){
  29. a.shout();
  30. }
  31. */
  32. public static void main(String[] args) {
  33. Animal dog = new Dog();//向上转型
  34. Animal cat = new Cat();//向上转型
  35. animalCry(dog);
  36. animalCry(cat);
  37. }
  38. }
  39. //结果:
  40. //汪汪汪
  41. //喵喵喵
  • 多态注意事项

1)与方法不同的是,实例变量不具备多态性,在通过引用访问实例变量时,指向子类对象的父类引用总是访问它编译时类型所定义的成员变量,即父类的成员变量,如果父类中不存在该变量则报错。

  1. class Animal{
  2. int age = 1;
  3. }
  4. class Dog extends Animal{
  5. int age = 2;
  6. }
  7. class Cat extends Animal{
  8. int age = 3;
  9. }
  10. public class Test {
  11. //尝试使用多态调用实例变量
  12. static void getAge(Animal a){
  13. System.out.println(a.age);
  14. }
  15. public static void main(String[] args) {
  16. Animal dog = new Dog();
  17. Animal cat = new Cat();
  18. getAge(dog);//总是输出父类中的实例变量,而不是Dog子类中的实例变量
  19. getAge(cat);//总是输出父类中的实例变量,而不是Dog子类中的实例变量
  20. }
  21. }
  22. //结果:
  23. //1
  24. //1

2)如果使用多态时,调用的不是子类重写的父类方法,就无多态。比如:父类中的方法,但子类中未对其进行重写,则在调用的时候,调用的是从父类继承得到的方法;假如调用的是子类中自己特有的方法,而父类中没有,则会报错,虽然引用实际指向的仍然是子类,但在编译时类型是父类引用,而父类中无此方法。

  1. class Animal{
  2. public void shout(){
  3. System.out.println("叫了一声");
  4. }
  5. }
  6. class Cat extends Animal{
  7. public void catchMouse(){
  8. System.out.println("猫抓老鼠");
  9. }
  10. public void shout(){
  11. System.out.println("喵喵喵");
  12. }
  13. }
  14. public class Test {
  15. public static void main(String[] args) {
  16. Animal a = new Cat();
  17. a.catchMouse();//报错,父类中无这个方法,在编译时找不到这个方法
  18. }
  19. }

引用变量在编译时,只能调用其编译时类型的方法,但运行时执行的是运行时类型的方法,因此在编写代码使用多态时,要注意只能调用父类中的方法(这个方法可以被不同的子类重写,此时形成多态)。

3.向上转型和向下转型

Java允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,这就是向上转型,由系统自动完成(对于父类引用变量,能通过is-a测试的对象都可以被赋值给该引用)。
引用变量在编译时,只能调用其编译时类型的方法,但运行时执行的是运行时类型的方法,如果需要让这个引用变量调用它运行时类型的方法,则需要把它强制转换成运行时类型,即向下转型

  1. class Animal{
  2. public void shout(){
  3. System.out.println("叫了一声");
  4. }
  5. }
  6. class Cat extends Animal{
  7. public void catchMouse(){
  8. System.out.println("猫抓老鼠");
  9. }
  10. public void shout(){
  11. System.out.println("喵喵喵");
  12. }
  13. }
  14. public class Test {
  15. public static void main(String[] args) {
  16. Animal a = new Cat();//自动向上转型
  17. //a.catchMouse();//报错,父类中无这个方法,在编译时找不到这个方法
  18. Cat p = (Cat) a;//强制类型转换,此时才可以调用cat对象特有的方法
  19. p.catchMouse();
  20. }
  21. }
  22. //猫抓老鼠
  • 这种强制类型转换不是万能的,当强制类型转换时需要注意:

1)基本类型之间的强制类型转换只能在数值类型之间进行,数值类型和布尔类不能进行转换。
2)引用类型之间的强制转换只能在具有继承关系的两个类型之间进行,如果两个没有任何继承关系的类型,则无法进行转换,在编译时会报错。
3)如果试图把一个父类引用转换成子类类型的引用,则这个父类引用所指向的对象必须实际上是子类实例才行,否则将在运行时引发ClassCastException异常。

  1. class Animal{
  2. public void shout(){
  3. System.out.println("叫了一声");
  4. }
  5. }
  6. class Dog extends Animal{
  7. public void shout(){
  8. System.out.println("汪汪汪");
  9. }
  10. }
  11. class Cat extends Animal{
  12. public void catchMouse(){
  13. System.out.println("猫抓老鼠");
  14. }
  15. public void shout(){
  16. System.out.println("喵喵喵");
  17. }
  18. }
  19. public class Test {
  20. public static void main(String[] args) {
  21. Animal a = new Cat();//自动向上转型
  22. //a.catchMouse();//报错,父类中无这个方法,在编译时找不到这个方法
  23. Cat p = (Cat) a;//强制类型转换,此时才可以调用Cat对象特有的方法
  24. p.catchMouse();
  25. Dog d = (Dog) a;//编译检查无问题,但运行时会有异常,因为a所指对象不是Dog类或其子类的实例
  26. }
  27. }
  • instanceof运算符

考虑到强制类型转换时可能出现运行异常,因此转型前应该通过instancof运算符来判断转型是否可以成功,从而避免ClassCastException异常,保证程序的健壮性。

  1. 引用类型变量 instanceof 类或接口

如果引用类型变量指向的对象是后面的类的实例或其子类的实例,则返回true,否则返回false。

  1. class Animal{
  2. public void shout(){
  3. System.out.println("叫了一声");
  4. }
  5. }
  6. class Dog extends Animal{
  7. public void shout(){
  8. System.out.println("汪汪汪");
  9. }
  10. }
  11. class Cat extends Animal{
  12. public void catchMouse(){
  13. System.out.println("猫抓老鼠");
  14. }
  15. public void shout(){
  16. System.out.println("喵喵喵");
  17. }
  18. }
  19. public class Test {
  20. public static void main(String[] args) {
  21. Animal a = new Cat();
  22. //判断是否可以强制类型转换
  23. if(a instanceof Cat){
  24. Cat p = (Cat) cat;
  25. }
  26. else if(a instanceof Dog){
  27. Dog d = (Dog) cat;
  28. }
  29. }
  30. }

3.初始化块

如果是简单赋值可以直接在定义变量时完成赋值,但某些情况下,在赋值时需要把运行相关程序得到的值赋值给这些变量,或者在赋值前进行判断再赋值,这种情况就可以使用初始化块。

  • 初始化块是类里可出现的第4种成员,类的所有成员有:成员变量、方法、构造器、初始化块。

    1. public class Test {
    2. public int a = 2;
    3. System.out.println(a);//错误,需要<标识符>
    4. public static void main(String[] args) {
    5. }
    6. }

    注:类的内部不能书写其他代码,只能由这四部分组成,比如这里打印代码直接写在类内部是错误的。

  • 普通初始化块(实例初始化块)

    1. public class Test {
    2. //初始化块
    3. {
    4. a = 1;
    5. }
    6. int a;
    7. public void print(){
    8. System.out.println(a);
    9. }
    10. public static void main(String[] args) {
    11. Test t = new Test();
    12. t.print();
    13. }
    14. }
    15. //结果:1

    Q1:为什么初始化块里赋值可以不用对a进行定义直接赋值,先赋值再定义?
    在类的内部,Field定义先执行,此时a已经在内存中被创建,再执行初始化块。所以,初始化块的位置不是固定的,既可以在变量定义之前,也可以在变量定义之后。代码执行顺序为:
    1)变量的声明在任意代码之前执行;
    2)静态初始化块、静态变量声明时的赋值语句合在一起执行,执行顺序与它们在代码中的书写顺序相同;
    3)实例初始化块、实例变量声明时的赋值语句、构造方法合在一起执行,前两者执行顺序与它们在代码中的书写顺序相同,构造方法最后执行;

    1. public class Test {
    2. {
    3. a = 1;
    4. }
    5. void print(){
    6. System.out.println(a);
    7. }
    8. public int a = 2;
    9. public static void main(String[] args) {
    10. new Test().get();
    11. }
    12. }
    13. //2

    这里是对a执行完所有的初始化过程后才进行访问,打印出a的值为2。也可以说明,在创建对象时,给对象的变量分配后内存后,所有的初始化过程全部执行完后才执行其余代码。

    1. {
    2. a = 1;
    3. }
    4. int a;
    5. int a;
    6. {
    7. a = 1;
    8. }
    9. 两者等价,字节码相同
    1. public class Test {
    2. //初始化块
    3. {
    4. a = 1;
    5. }
    6. int a = 2;//在定义时赋值,在初始化之后
    7. public void print(){
    8. System.out.println(a);
    9. }
    10. public static void main(String[] args) {
    11. Test t = new Test();
    12. t.print();
    13. }
    14. }
    15. //结果:2
    16. public class Test {
    17. int a = 2;//在定义时赋值,在初始化块之前
    18. //初始化块
    19. {
    20. a = 1;
    21. }
    22. public void print(){
    23. System.out.println(a);
    24. }
    25. public static void main(String[] args) {
    26. Test t = new Test();
    27. t.print();
    28. }
    29. }
    30. 结果:1

    Q2:为什么两次结果不一样?初始化块和定义时都进行赋值,一定是先执行初始化块吗?
    如果Field在初始化块之前,在定义时就会得到被赋予的值,然后再经过初始化块赋值,值被改变。
    如果Field在初始化块之后,先被定义,先初始化块赋值,再向下执行其他赋值操作。
    可以这么理解,不管在之前还是之后,定义和赋值是完全分离的,定义时赋值拆分成两步,此时的赋值操作也相当于一个“初始化块”,即实例初始化块、定义变量时指定的默认值都可认为是初始化代码,总是先执行定义,再按顺序执行所有的初始化块。

    1. public class Test {
    2. {
    3. System.out.println("第一个初始化块");
    4. }
    5. public Test(){
    6. System.out.println("无参构造器");
    7. }
    8. {
    9. System.out.println("第二个初始化块");
    10. }
    11. int a = 2;
    12. public static void main(String[] args) {
    13. new Test();
    14. }
    15. }
    16. //结果:
    17. //第一个初始化块
    18. //第二个初始化块
    19. //无参构造器

    因为初始化块没有名字,也就无标识,总是在创建Java对象时隐式执行,而且在构造器之前自动执行。
    注:Java允许定义多个初始化块,但这是没有意义的,因为初始化块总是在构造器之前全部执行,完全可以把多个初始化合并成一个,让程序更加简洁。

    1. public class Test {
    2. {
    3. a = 1;
    4. System.out.println(a);//错误:非法前向引用,a为右值引用
    5. }
    6. int a;
    7. public static void main(String[] args) {
    8. }
    9. }

    Q3:为什么这里不能对a访问,会提示非法向前引用?
    变量声明最先执行,此时变量a已经存在,按理说不应该出现“非法向前引用”一说。会出现非法向前引用的原因是,这是Java编译器强制进行的检查,Java在初始化成员变量过程中对成员变量的限制,如果成员变量满足以下四点,就必须在使用前对该成员变量先声明才能引用:
    1)成员变量所在类或接口(类名或接口名为C)是直接包含该成员变量的;
    2)该成员变量在C的静态成员、非静态成员初始化中,或者C的静态、非静态初始化块中;
    3)通过简单名称引用的变量,如这里直接通过a这个简单引用变量访问。
    4)右值引用,即不是在赋值语句的左边被赋值。
    简单来说就是,初始化块中只能访问定义在初始化块之前的变量,定义在它之后的变量,可以被赋值,但是不能被访问。

  • 实例初始化块和构造器

与构造器不同,实例初始化块是一段固定执行的代码,它不能接受参数,对同一个类的所有对象进行的初始化处理完全相同。因此,如果有一段初始化处理代码对所有对象都相同,且无须接受参数,就可以放到初始块中。实际上,实例初始化块是一个假象,使用javac编译后,类中的实例初始化块会消失:实例初始化块的内容会被“还原”到每个构造器中,且位于构造器的最前面。
image.png

  • 静态初始化块(类初始化块)

类初始化块将在类初始化阶段执行,用于对类变量进行初始化,而不是创建对象时执行,因此类初始化块总是在实例初始化块之前执行。

  1. public class Test {
  2. //静态初始化块
  3. static {
  4. a = 2;
  5. }
  6. static int a = 1;
  7. public static void main(String[] args) {
  8. System.out.println(new Test().a);
  9. }
  10. }

注:静态初始化块同样是类的静态成员,不能访问非静态成员(实例变量和实例方法)。

  • 初始化块往上追溯执行

如果是实例初始化块,最先执行的实例初始化块代码是Object类的实例初始化块,依次往下。如果是类初始化块,最先执行的是Object类的类初始化块,依次往下。