第八节:面向对象高级

记录现阶段感觉有用的知识点,查缺补漏。

继承

  • 关键字 extends。

  • Java 里的继承是 单继承。 一个子类 只能继承自 一个父类。

  • 子类的内存结构:

    类 都是在 堆 上分配内存空间的。 (创建子类对象时,都要先去看看有没有父类)

    只不过子类里有个 super( ) 的关键字,指向了父类的内存地址。

    也就是说,实现继承的手段,就是在子类的内存空间里,保存了父类的信息(内存地址)。

重写(Override)

  • 里氏替换原则:

    通俗点说:子类不能改变父类原有的功能,但是可以扩展父类的功能。

    1987年提出来的:

    Inheritance should ensure that

    any property proved about supertype objects also holds for subtype objects.

  • 重写需要满足:(可以用 @Override 注解让 编译器来检查)

    1. 子类方法 的访问权限 不能小于 父类方法 的访问权限。(可以访问子类方法,就一定能访问父类方法)
    2. 子类方法 的返回值类型 是 父类方法 的返回类值类型或其子类型。
    3. 子类方法 抛出的异常 是 父类方法抛出的异常或其子异常。
  • 方法的调用顺序:

    先去 当前类看有没有相应方法,再去父类看有没有继承过来的相应方法。

    如果还不行,就对参数进行类型转换,然后还是先看当前类,再看父类。

    public class OverrideTest2 {
         
    
        public static void main(String[] args) {
         
            A a = new A();
            B b = new B();
            C c = new C();
            D d = new D();
    
            // 当前就有的方法。
            a.show(a);  // A.show(A)
            a.show(c);  // A.show(C)
    
            // b的父类A中有相应方法。
            b.show(c);  // A.show(C)
    
            // 将b进行类型转换为其父类A。
            a.show(b);  // A.show(A)
    
            // 将d进行类型转换为其父类C。
            // b的父类A中有相应的方法。
            b.show(d);  // A.show(C)
            
            
            // 引用的还是 B对象, 和B对象调用的情况是一样的。
            A ba = new B();
            ba.show(c);  // A.show(C)
            ba.show(d);  // A.show(C)
            ba.show(a);  // B.show(A)
        }
        
    }
    
    // 继承关系: A <--- B <--- C <--- D
    class A{
         
        public void show(A obj) {
         
            System.out.println("A.show(A)");
        }
    
        public void show(C obj) {
         
            System.out.println("A.show(C)");
        }
    }
    
    class B extends A {
         
    
        @Override  // 重写了 A 类的方法。
        public void show(A obj) {
         
            System.out.println("B.show(A)");
        }
    }
    
    class C extends B {
         
    
    }
    
    class D extends C {
         
    
    }
    
    
  • 存在继承的情况下,代码块的初始化顺序:

    父类(静态变量、静态代码块)

    子类(静态变量、静态代码块)

    父类(实例变量、构造代码块)

    父类(构造函数)

    子类(实例变量、构造代码块)

    子类(构造函数)

虽然重写(Override)和重载(Overload)听着很像,但是搞明白他们代表什么,还是很好区分的。。。

final 关键字

  • 被 final 修饰过的变量,就是常量。

    对于基本类型, final 使其数值不能改变。

    对于引用类型, final 使其引用不能改变。但是引用指向的 对象里的数据是可以改变的。

  • public static final :

    就是他的名称一样: 全局的(不同包下可访问),静态的(类名可访问),常量(不可变)。

  • final 修饰的方法: 不能被子类重写。

  • final 修饰的类: 不能被继承。

抽象类

有时候我们描述事物的时候,不是把他定死的,也是一个很抽象的事物。

我们就可以用 抽象类 来表示这个事物。

  • 包含 抽象方法的类,都是抽象类。 用 abstract 关键字 进行声明。

  • 抽象方法没有实现体,只有声明,等着子类继承从而实现他。

    子类要实现 父类的所有抽象方法,否则子类仍然是 抽象类。

  • 可以有构造方法,但我们不能进行 new 实例化。继承的子类实例化时,JVM会用他的父类抽象类的构造方法

  • 抽象类因为要有 子类继承,才有存在意义。所以权限修饰符只能用 public(默认)、protected。

接口

接口是一种规范,是一种更加纯粹的抽象。接口里面只能有 全局常量、和抽象方法。

  • Java8 之前,接口不用有任何方法的实现; Java8 开始,接口里可以有默认方法的实现。

  • 用接口, 需要用 implements 关键字来实现。 可以有多个实现。

    实现接口的类,仍然要实现接口里所有没有实现的抽象方法。否则是抽象类。

  • 接口的字段,默认的就是 public static final;

    接口的方法,默认的就是 public abstract。

  • 接口之间是允许 多继承的。(继承的还是接口,不要去继承类去了。。。)

    就算继承的多个接口中相同的抽象方法也没关系,最后在 “该接口” 中,对该方法只有一种实现方案。

多态

可以简单的理解为 : 父类引用 指向 子类对象。 (向上转型)。

当然还有向下转型的,就是强转,不过这时候需要保证类型所属是正确的,否则就会抛 ClassCast异常。

比如

Animal a = new Bird();

Bird b = (Bird)a; // 这是可以的,类型是正确的。

Animal a2 = new Person();

Bird b2 = (Bird) a2; // 这样就不行了, 人类 不是 鸟类。 会抛 ClassCast 异常。

  • instanceof 关键字: 判断某个对象 是不是属于某种类型(或其父类型)。

重写 和 重载 也是属于多态的一种体现。(多态: 多种表现形态)

重写: 字父类中,方法的多态体现。

重载: 一个类中,方法的多态体现。

Object 类

是所有类的 根基类。相当于 对象中 “万物” 的概念。

多态表现:可以接收 任意数据类型的引用。

  • toString()

    默认返回是 类名@4554617c 形式。 @后面的数值是 散列码 的 无符号的十六进制表示形式。

    推荐重写toString( ), 返回的是人们可读的 字符串表示形式。

  • equals( )

    引用类型用 == 判断的是地址是否相等,基本数据类型用 == 判断的是 数值是否相等。

    如果要对 引用类型 用人们常理解的相等的方式来判断相不相等,就要重写 equals() 方法。

    equals( ) 方法需要满足5个特性:

    • 自反性。

      x.equals(x) 始终为true。

    • 对称性。

      x.equals(y) 和 y.equals(x) 始终相等。

    • 传递性。

      如果有 x.equals(y), y.equals(z) 都为 true

      则有 x.equals(z) 为 true。

    • 一致性。

      在不改变 x,y的情况下, 多次调用 x.equals(y) 始终一致。

    • 非空性。

      任何非空 x, 都有 x.equals(null) 始终为 false。

    重写 equals() 方法,一般都有一套固定的流程。

    判断是不是同一个对象引用,判断是不是null,再 类型转换 进行各个属性的判断。

        @Override
        public boolean equals(Object o) {
          
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Person person = (Person) o;
            return age == person.age &&
                    name.equals(person.name);
        }
    
  • hashCode( ) 方法

    一般 equals()方法 和 hashCode()方法都是配套使用的。

    如果 2个对象 等价(equals 为true),那么他们的 散列码(hash码),一定相等。

    但是如果2个对象的 hash码 相等,他们不一定等价。

    • 重写 hashCode , 可以用 Objects.hash( ) 方法,这里面用的是 Arrays.hashCode( ) 方法。

      这里面的实现原因,就不太清楚了,贴图吧:

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-19FrTRbE-1588245224177)(C:\Users\Administrator\Desktop\哈希均匀.png)]

  • clone()方法

    需要重写 clone() 的类,必须实现 Clonable 接口, 否则会抛 CloneNotSurport 异常。

    分为 深拷贝、浅拷贝。

    如果对象里的属性有 引用类型的数据,他们就有区别了。 (虽然有点绕,但就是这么个区别)

    浅拷贝: 拷贝对象, 和原始对象, 他们里面的引用属性 是 指向的同一个对象。

    深拷贝: 拷贝对象, 和原始对象, 他们里面的引用属性 是 指向 不同的对象。

    一般不会用 clone( ) 方法,既复杂又会抛异常。

    Effective Java 书上推荐用 拷贝构造函数、或者 拷贝工厂。

内部类

定义在 类里面 或者 方法里面 的类,就是内部类。

广义的分:

​ 定义在 类 里的:

  • 成员内部类

  • 静态内部类 (就是在 成员内部类加个 static 关键字修饰)

  • 匿名内部类 (局部内部类的限制,同样也限制了他)

    定义在 方法 里的:

  • 局部内部类

关于内部类的栗子:

(提出来说一点: JDK1.8以后,对于变量,默认都是写了final的,不去改变他那他就是final修饰的了)

public class Main {
   
    public static void main(String[] args) {
   

        // 成员内部类的访问
        Outer outer = new Outer();
        outer.setX(100);
        Outer.Inner inner = outer.new Inner();
        inner.say();
        System.out.println("================================");

        // 静态内部类的使用
        Outer.InnerStatic innerStatic = new Outer.InnerStatic();
        innerStatic.say();
        System.out.println("================================");

        // 局部内部类的访问
        outer.method();
        System.out.println("================================");


        /** * 匿名内部类 * 也算是 属于 局部内部类。 * 使用匿名内部类,必须通过 实现一个接口,或者继承一个类的形式来实现。 * * 注意: * 匿名内部类里, 不能有抽象的方法。 * 匿名内部类里, 不能有 static 的资源。 * 匿名内部类里, 只能访问外面的 final 型的局部变量。 * (因为在编译时,类都是单独编译成一个 .class 文件,如果里面有 访问了 非fianl 型的变量,不知道把他存在哪。。。) */
        // 匿名内部类的使用
        MyInterface myInterface = new MyInterface() {
   
            @Override
            public void methodByInterface() {
   
                System.out.println("匿名内部类。。。实现了一个接口");
            }
        };
        myInterface.methodByInterface();

        // JDK1.8 之后,对于 变量,是默认就有 final 关键字,如果不改变他,就认为他是一个 fianl 修饰的。
// int num = 666;
// num = 999;
        new MyClass(123) {
   
            // public static int aaa;
            public void methodByMyClass() {
   
                System.out.println("匿名内部类。。。继承了一个父类");
                // 不加这句话: num = 999; 就不报错。。。。
// System.out.println("匿名内部类。。。继承了一个父类" + num); 
            }
        }.methodByMyClass();

    }
}


class Outer{
   
    private int x;
    private static final String name = "Outer Class String";
 

    /** * 成员内部类。 * 需要依赖 外部类。 * 可访问外部内的一切资源。 如果重名了,就要 外部类.this.资源 的形式访问。 */
    public class Inner{
   
        private int x;
        private int y;


        public void say() {
   
            System.out.println("局部内部类的 : " + x + " ; " + y);
            System.out.println("外部类的: "+Outer.this.x);
        }

        public int getY() {
   
            return y;
        }

        public void setY(int y) {
   
            this.y = y;
        }
    }


    /** * 静态内部类 * 不依赖外部类, 只能用 外部类的 static 资源。 */
    static class InnerStatic{
   
        private int x;

        public void say(){
   
            System.out.println(x + " ; " + Outer.name);
        }
    }


    public void method() {
   
        /** * 局部内部类 * 就像方法里面的局部变量一样,是不能有 public、private 这些权限修饰符的。 */
        class LocalInner{
   
            int x;
            public void say() {
   
                System.out.println("局部内部类的方法");
            }
        }

        new LocalInner().say();
    }

    public int getX() {
   
        return x;
    }

    public void setX(int x) {
   
        this.x = x;
    }
}

包装类型

偷个懒,不久前就写过,现在直接放链接吧: https://blog.csdn.net/weixin_43201538/article/details/105419905

可变参数

JDk1.5 之后参数列表 支持可变参数。 就在类型后面加三个小数点就可以。。。

需要注意的是: 可变参数必须放在参数列表的最后一个位置。

(很容易想明白,放前面的话不知道什么时候结束,什么时候调用后面的 固定长度的参数)

递归

就是 自己调用自己调用。 所以要有终止条件,否则很容易 StackOverflowError。

在 二叉树的算法题中,递归的方式就很好用。。。