跳转至

类与对象进阶

  • 区分相同名字的类
  • 当类很多时,可以更好的管理类
  • 控制访问范围
package com.hspedu;
// 关键字表示打包 包名

import java.util.Scanner;// 只引入一个类Scanner
import java.util.*;//将java.util包所有都引入

“包”本质上是创建不同文件夹/目录来保存类文件

命名规则:

  • 只能包含数字、字母、下划线、小圆点
  • 不能用数字开头,不能是关键字或保留字

TIPS:

  • package的作用是声明当前类所在的包,需要放在类最上面,一个类最多只有一个package

访问修饰符

  • 公开级别:用 public 修饰,对外公开
  • 受保护级别:用 protected 修饰,对子类和同一个包中的类公开
  • 默认级别:没有修饰符号,向同一个包的类公开.
  • 私有级别:用 private 修饰,只有类本身可以访问,不对外公开.

TIPS:

  • 修饰符可以修饰类中的属性、成员方法、类本身
  • 只有默认的和public才能修饰类?

继承

class 子类 extends 父类{
}
  • 子类就会自动拥有父类定义的属性和方法
  • 父类又叫超类,基类。
  • 子类又叫派生类。

TIPS:

  • 子类继承了所有的属性和方法,非私有的属性和方法可以在子类直接访问, 但是私有属性和方法不能在子类直接访 问,要通过父类提供公共的方法去访问
  • 子类必须调用父类的构造器, 完成父类的初始化
  • 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须**在子类的构造器中用 super 去指定**使用父类的哪个构造器完成对父类的初始化工作,否则,编译不会通过
  • 如果希望指定去调用父类的某个构造器,则显式的调用一下 : super(参数列表)
  • super 在使用时,必须放在构造器第一行(super 只能在构造器中使用)
  • super() 和 this() 都只能放在构造器第一行,因此这两个方法不能共存在一个构造器
  • java 所有类都是 Object 类的子类, Object 是所有类的基类.
  • 父类构造器的调用不限于直接父类!将一直往上追溯直到 Object 类(顶级父类)
  • 子类最多只能继承一个父类(指直接继承),即 java 中是单继承机制。
  • 不能滥用继承,子类和父类之间必须满足 is-a 的逻辑关系

Super

维度 this (当前对象) super (父类空间)
本质 指向**当前对象本身**的引用 不是对象引用,只是指向**父类特征**的标识
访问属性 this.n1 (先查本类,无则向上找) super.n1 (跳过本类,**直接从父类**开始找)
访问方法 this.method() (先查本类) super.method() (直接查父类,常用于重写扩展)
构造器 this(...) (调用**本类**其他构造器) super(...) (调用**父类**构造器)
限制 必须在构造器**第一行** 必须在构造器**第一行**
冲突 互斥:一个构造器中不能同时出现 this(...)super(...) 同左

A. 构造器接力 (必须)

子类初始化前,必须先完成父类的初始化

  • 默认行为:若子类构造器未显式调用 super,编译器会自动插入 super() (父类无参构造)。
  • 显式调用:若需调用父类有参构造,必须手动写 super(参数)
  • 原则:父类构造器一直追溯到 Object 类。

B. 方法重写扩展 (常用)

子类重写父类方法时,想保留父类原有逻辑:

public void method() {
    super.method(); // 1. 先复用父类逻辑
    // 2. 再编写子类独有代码
}

C. 属性/方法去重 (特殊)

当子类和父类有**同名**成员(Shadowing)时,用 super 强制访问父类成员。

super案例

// TopBase.java
package com.hspedu.extend_;

public class TopBase { // 父类是 Object

    public TopBase() {
        // super(); Object 的无参构造器
        System.out.println("构造器 TopBase() 被调用..."); // 1
    }
}
// Base.java
package com.hspedu.extend_;

public class Base extends TopBase { // 父类
    // 4 个属性
    public int n1 = 100;
    protected int n2 = 200;
    int n3 = 300;
    private int n4 = 400;

    public Base() { // 无参构造器
        System.out.println("父类 Base()构造器被调用....");
    }

    public Base(String name, int age) { // 有参构造器
        // 默认 super()
        System.out.println("父类 Base(String name, int age)构造器被调用....");
    }

    public Base(String name) { // 有参构造器
        System.out.println("父类 Base(String name)构造器被调用....");
    }

    // 父类提供一个 public 的方法,返回了 n4
    public int getN4() {
        return n4;
    }

    public void test100() {
        System.out.println("test100");
    }

    protected void test200() {
        System.out.println("test200");
    }

    void test300() {
        System.out.println("test300");
    }

    private void test400() {
        System.out.println("test400");
    }

    // call
    public void callTest400() {
        test400();
    }
}
// Sub.java
package com.hspedu.extend_;

// 输入 ctrl + H 可以看到类的继承关系
public class Sub extends Base { // 子类

    public Sub(String name, int age) {
        // 1. 老师要调用父类的无参构造器,如下或者 什么都不写,默认就是调用 super()
        // super();//父类的无参构造器
        // 2. 老师要调用父类的 Base(String name) 构造器
        // super("hsp");
        // 3. 老师要调用父类的 Base(String name, int age) 构造器
        super("king", 20);

        // 细节: super 在使用时,必须放在构造器第一行
        // 细节: super() 和 this() 都只能放在构造器第一行,因此这两个方法不能共存在一个构造器
        // this() 不能再使用了
        System.out.println("子类 Sub(String name, int age)构造器被调用....");
    }

    public Sub() { // 无参构造器
        // super(); //默认调用父类的无参构造器
        super("smith", 10);
        System.out.println("子类 Sub()构造器被调用....");
    }

    // 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器
    public Sub(String name) {
        super("tom", 30);
        // do nothing...
        System.out.println("子类 Sub(String name)构造器被调用....");
    }

    public void sayOk() { // 子类方法
        // 非私有的属性和方法可以在子类直接访问
        // 但是私有属性和方法不能在子类直接访问
        System.out.println(n1 + " " + n2 + " " + n3);

        test100();
        test200();
        test300();
        // test400();错误
        // 要通过父类提供公共的方法去访问
        System.out.println("n4=" + getN4());
        callTest400(); //
    }
}
// ExtendsDetail.java
package com.hspedu.extend_;

public class ExtendsDetail {

    public static void main(String[] args) {
        // System.out.println("===第 1 个对象====");
        // Sub sub = new Sub(); //创建了子类对象 sub
        // System.out.println("===第 2 个对象====");
        // Sub sub2 = new Sub("jack"); //创建了子类对象 sub2
        System.out.println("===第 3 对象====");
        Sub sub3 = new Sub("king", 10); // 创建了子类对象 sub3

        // sub3.sayOk(); // 注意:原代码中可能是 sub.sayOk(),这里根据 sub3 对象应该调 sub3
    }
}

继承的本质

当执行 Sub sub = new Sub(); 时:

  1. 对象数量:堆内存中**只有一个对象**(sub),不是“大对象套小对象”。
  2. 加载过程 (洋葱模型)
  3. 先加载顶级父类 (Object) 的属性。
  4. 再加载父类 (Base) 的属性(包括 private 属性,它们真实存在于内存中,只是被封装了)。
  5. 最后加载子类 (Sub) 的属性。
  6. 空间划分:这个对象内部划分了不同区域来存储父类和子类的属性,即使属性名相同(如 n1),在内存中也是独立存在的两个变量。

访问查找规则

当调用 sub.xsub.method() 时,JVM 按照以下顺序查找:

  1. 子类 (Sub):先看自己有没有,且是否有权限访问?
  2. 有 -> 使用子类的。
  3. 父类 (Base):如果没有,去直接父类找。
  4. 有 -> 使用父类的。
  5. 上级父类... -> Object:依次向上查找。
  6. 报错:如果一直找到顶层都没有,则编译报错。

注意:如果是 super.x,则直接从**第 2 步**(直接父类)开始查找。

方法重写/覆盖

  • 子类的方法的形参列表、方法名称,要和父亲方法的形参列表、方法名称完全一样

  • 子类方法的返回类型和父类方法的返回类型一样,或者是父类返回类型的子类。

比如 父类 返回类型是Object,子类方法的返回类型是 String

  • 子类方法不能缩小父类的访问权限:public > protected > 默认 > private
名称 发生范围 方法名 形参列表 返回类型 修饰符
重载(overload) 本类 必须一样 类型,个数或者顺序至少有一个不同 无要求 无要求
重写(override) 父子类 必须一样 相同 子类重写的方法,返回的类型和父类返回的类型一致,或者是其子类 子类方法不能缩小父类方法的访问范围

TIPS:

  • 建议写上 @Override,编译器会自动帮你检查格式是否正确。
  • 以下三种情况**不能**重写:
  • private 方法 (私有无法继承)
  • static 方法 (静态属于类,不属于对象)
  • final 方法 (最终方法,禁止修改)
class Father {
    protected Object test() { return null; }
}

class Son extends Father {
    @Override // 校验注解
    public String test() { // 权限变大(public),返回类型变小(String)
        return "Override";
    }
}

多态 (Polymorphism)

多态是继 封装继承 之后,面向对象的第三大特征。

1. 什么是多态?

通俗理解:同一个行为(方法),作用在不同的对象上,产生不同的效果。

  • 比如“叫声”这个行为:狗是“汪汪”,猫是“喵喵”。
  • 比如“吃饭”这个行为:中国人用筷子,西方人用刀叉。

技术定义:方法或对象具有多种形态。多态建立在封装和继承的基础之上。

2. 多态的具体体现

多态主要体现在两个方面:

  1. 方法的多态
  2. 重载 (Overload):同一个方法名,输入参数不同,功能不同(编译时多态)。
  3. 重写 (Override):同一个方法名,子类覆盖父类,行为改变(运行时多态)。
  4. 对象的多态 (核心)
  5. 核心公式父类的引用 指向 子类的对象
  6. 语法Father obj = new Son();
  7. 编译类型= 号左边(Father),决定了你**能调用哪些方法**。
  8. 运行类型= 号右边(Son),决定了**真正执行的是哪个子类的方法**。

3. 多态快速入门案例

// 父类
class Animal {
    public void cry() {
        System.out.println("动物在叫...");
    }
}

// 子类 Dog
class Dog extends Animal {
    @Override // 重写
    public void cry() {
        System.out.println("小狗汪汪叫");
    }
}

// 子类 Cat
class Cat extends Animal {
    @Override // 重写
    public void cry() {
        System.out.println("小猫喵喵叫");
    }
}

public class TestPoly {
    public static void main(String[] args) {
        // 体验多态:父类的引用变量 a
        Animal a = new Dog(); // a 的编译类型是 Animal,运行类型是 Dog
        a.cry(); // 执行的是 Dog 的 cry -> "小狗汪汪叫"

        a = new Cat(); // a 的运行类型变成了 Cat
        a.cry(); // 执行的是 Cat 的 cry -> "小猫喵喵叫"
    }
}

4. 多态的核心细节 (非常重要)

这里是多态最容易晕、也是考试和面试最多的地方,请记住 “向上转型”“向下转型”

A. 向上转型 (Upcasting)

  • 本质:父类的引用指向子类的对象。
  • 语法父类类型 引用名 = new 子类类型();
  • 特点
  • 可以调用父类中的所有成员(需遵守访问权限)。
  • 不能调用子类中特有成员(因为编译类型是父类,编译器不认识子类的新方法)。
  • 最终运行效果看子类的具体实现(即:如果子类重写了,就调子类的)。

B. 向下转型 (Downcasting)

  • 本质:把指向子类对象的父类引用,转回子类类型(类似强转)。

  • 为什么要用?:为了调用子类**特有**的方法(这是向上转型做不到的)。

  • 语法子类类型 引用名 = (子类类型) 父类引用;

class Animal {
    void eat() { System.out.println("吃东西"); }
}

class Dog extends Animal {
    void eat() { System.out.println("狗吃骨头"); }
    void watchDoor() { System.out.println("狗看门"); } // 【特有方法】
}

public class Test {
    public static void main(String[] args) {
        // 1. 向上转型:把狗当成普通动物
        Animal a = new Dog();
        a.eat(); // 可以调(因为动物都会吃)

        // a.watchDoor(); // ❌ 报错!Java说:我只知道它是动物,动物不一定会看门。

        // 2. 向下转型:强行把这个动物还原成狗
        Dog d = (Dog) a; 
        d.watchDoor(); // ✅ 成功!现在 Java 确认它是狗了。
    }
}
  • 风险与解决:如果强转的类型不对(比如把“猫”强转成“狗”),会报 ClassCastException

  • 解决方案:使用 instanceof 判断。

if (a instanceof Dog) {
    Dog d = (Dog) a;
    d.watchDoor(); // 调用狗特有的方法
}

C. 属性没有多态!(大坑)

这是很多人的盲区:多态只针对方法,不针对属性。

  • 规则:属性的值看**编译类型**(左边)。
  • 方法:看**运行类型**(右边)。
class Base { int n = 10; }
class Sub extends Base { int n = 20; }

Base b = new Sub();
System.out.println(b.n); // 输出 10!(看左边 Base)
// 只有方法调用才会去看右边 Sub

5. Java 的动态绑定机制 (Dynamic Binding)

这是多态实现的底层原理,理解了这个,就理解了 Java 运行时的灵魂。

当调用对象方法的时候,该方法会和该对象的**内存地址(运行类型)**绑定。

  1. 调用方法时:JVM 会看这个对象真正的内存是啥(new 的是啥)。如果子类有这个方法,就执行子类的;如果没有,才去父类找。
  2. 调用属性时:**没有**动态绑定机制。哪里声明,就在哪里使用。

6. 多态的应用场景

学会多态后,你的代码会变得非常灵活(解耦)。

A. 多态数组

数组定义为父类类型,里面保存各种子类对象。

Person[] people = new Person[2];
people[0] = new Student("小明");
people[1] = new Teacher("老王");
// 遍历时,统一调用 people[i].say(),会自动根据身份不同输出不同内容

B. 多态参数

定义方法时,形参写父类类型,实参可以传任何子类对象。

// 只需要写这一个方法,就能喂食所有的动物
public void feed(Animal animal, Food food) {
    animal.eat(food);
}

// 调用
feed(new Dog(), new Bone());
feed(new Cat(), new Fish());

Object类

万物之源: 在 Java 中,所有类(包括你自定义的类、Java 内置的类,如 StringInteger 等,以及数组)都直接或间接继承自 Object 类。

隐式继承: 即使你在定义一个类时没有使用 extends 关键字,编译器也会自动让它继承 Object 类。

equals方法

  • ==是一个比较运算符

  • 既可以判断基本类型,也可以判断引用类型

  • 判断基本类型:判断**值**是否相等
  • 判断引用类型:判断地址是否相等,即判断是不是同一个对象

  • equal是Object类中的方法,只能判断引用类型

但是子类中往往重写这个方法,用于判断内容是否相等

package com.hspedu.object_;

public class Equals01 {

    public static void main(String[] args) {
        A a = new A();
        A b = a;
        A c = b;

        System.out.println(a == c);//true
        System.out.println(b == c);//true

        B bObj = a;
        System.out.println(bObj == c);//true

        int num1 = 10;
        double num2 = 10.0;
        System.out.println(num1 == num2);//基本数据类型,判断值是否相等

        //equals 方法,源码怎么查看.
        //把光标放在 equals 方法,直接输入 ctrl+b
        //如果你使用不了. 自己配置. 即可使用.

        /*
        //带大家看看 Jdk 的源码 String 类的 equals 方法
        //把 Object 的 equals 方法重写了,变成了比较两个字符串值是否相同
        public boolean equals(Object anObject) {
            if (this == anObject) {//如果是同一个对象
                return true;//返回 true
            }
            if (anObject instanceof String) {//判断类型
                String anotherString = (String)anObject;//向下转型
                int n = value.length;
                if (n == anotherString.value.length) {//如果长度相同
                    char v1[] = value;
                    char v2[] = anotherString.value;
                    int i = 0;
                    while (n-- != 0) {//然后一个一个的比较字符
                        if (v1[i] != v2[i])
                            return false;
                        i++;
                    }
                    return true;//如果两个字符串的所有字符都相等,则返回 true
                }
            }
            return false;//如果比较的不是字符串,则直接返回 false
        }
        */

        "hello".equals("abc");

        //看看 Object 类的 equals 是
        /*
        //即 Object 的 equals 方法默认就是比较对象地址是否相同
        //也就是判断两个对象是不是同一个对象.
        public boolean equals(Object obj) {
            return (this == obj);
        }
        */

        /*
        //从源码可以看到 Integer 也重写了 Object 的 equals 方法,
        //变成了判断两个值是否相同
        public boolean equals(Object obj) {
            if (obj instanceof Integer) {
                return value == ((Integer)obj).intValue();
            }
            return false;
        }
        */

        Integer integer1 = new Integer(1000);
        Integer integer2 = new Integer(1000);
        System.out.println(integer1 == integer2);//false
        System.out.println(integer1.equals(integer2));//true

        String str1 = new String("hspedu");
        String str2 = new String("hspedu");
        System.out.println(str1 == str2);//false
        System.out.println(str1.equals(str2));//true

    }
}

class B {}
class A extends B {}

equals重写

实例:判断二个Person对象的内容是否相等,如果二个对象的各个属性值都一样,则返回true

package com.hspedu.object_;

public class EqualsExercise01 {
    public static void main(String[] args) {
        Person person1 = new Person("jack", 10, '男');
        Person person2 = new Person("jack", 20, '男');

        System.out.println(person1.equals(person2));//假
    }
}

//判断两个 Person 对象的内容是否相等,
//如果两个 Person 对象的各个属性值都一样,则返回 true,反之 false
class Person { //extends Object
    private String name;
    private int age;
    private char gender;

    //重写 Object 的 equals 方法
    public boolean equals(Object obj) {
        //判断如果比较的两个对象是同一个对象,则直接返回 true
        if(this == obj) {
            return true;
        }
        //类型判断
        if(obj instanceof Person) {//是 Person,我们才比较
            //进行 向下转型,因为我需要得到 obj 的 各个属性
            Person p = (Person)obj;
            return this.name.equals(p.name) && this.age == p.age && this.gender == p.gender;
        }
        //如果不是 Person ,则直接返回 false
        return false;

    }

    public Person(String name, int age, char gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public char getGender() {
        return gender;
    }

    public void setGender(char gender) {
        this.gender = gender;
    }
}

hashCode方法

定义与作用:

  • 返回该对象的哈希码值。
  • 主要用于提高哈希表(如 java.util.Hashtable, HashMap 等)的性能。

核心协定 (Contract):

  1. 一致性:如果两个对象通过 equals 方法比较是相等的,那么它们的 hashCode 必须相同。
  2. 引用关系:如果两个引用指向同一个对象(如 aa3 = aa),哈希值肯定一样。
  3. 差异性:如果两个对象不相等,hashCode 不一定不同(允许碰撞),但不同的哈希值能提高哈希表性能。

实现细节:

  • 默认实现:Object 类的默认 hashCode 通常是将对象的**内部地址**转换成一个整数。
  • 注意:哈希值主要根据地址号来的,但不能完全等价于物理地址。
AA aa = new AA();
AA aa2 = new AA(); 
// aa 和 aa2 是不同对象,通常 hashCode 不同
AA aa3 = aa;       
// aa3 指向 aa,hashCode 必然相同
System.out.println("aa.hashCode()=" + aa.hashCode());
System.out.println("aa2.hashCode()=" + aa2.hashCode());
System.out.println("aa3.hashCode()=" + aa3.hashCode());
  1. hashCode 是为了提高哈希结构容器的效率。
  2. 指向同一对象的引用,哈希值一定相同。
  3. 指向不同对象的引用,哈希值通常不同。
  4. 后续在集合章节中,如果需要,也会重写 hashCode

这是为您补充整理的 toStringfinalize 方法的简练笔记,保持了与之前一致的结构风格。

toString 方法

基本介绍:

  • 默认行为Object 类的默认实现返回 "全类名+@+哈希值的十六进制"。
  • 源码逻辑:getClass().getName() + "@" + Integer.toHexString(hashCode())(参考标准JDK行为)。
  • 主要用途:子类往往重写此方法,用于返回对象的属性信息,以便查看和调试。

触发机制:

  • 当直接输出一个对象时(例如 System.out.println(monster)),toString 方法会被默认调用。

最佳实践:

  • 在自定义类中重写该方法,打印出更有意义的字段信息(如 name, job, sal 等)。

finalize 方法

触发时机:

  • 当对象被回收时,系统会自动调用该对象的 finalize 方法。
  • 销毁前奏:在销毁对象前,会先调用此方法,子类可以重写以进行资源释放操作。

回收条件:

  • 当某个对象**没有任何引用**时,JVM 就认为该对象是一个垃圾对象,会使用垃圾回收机制来销毁它。

主动调用 GC:

  • 垃圾回收机制主要由系统决定(有自己的 GC 算法),但也可以通过 System.gc() 主动触发垃圾回收机制。

实际应用 (老韩提示):

  • 实际开发中几乎不会运用 finalize
  • 学习它的目的更多是为了**应付面试**。