类与对象进阶
包¶
- 区分相同名字的类
- 当类很多时,可以更好的管理类
- 控制访问范围
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(); 时:
- 对象数量:堆内存中**只有一个对象**(
sub),不是“大对象套小对象”。 - 加载过程 (洋葱模型):
- 先加载顶级父类 (
Object) 的属性。 - 再加载父类 (
Base) 的属性(包括private属性,它们真实存在于内存中,只是被封装了)。 - 最后加载子类 (
Sub) 的属性。 - 空间划分:这个对象内部划分了不同区域来存储父类和子类的属性,即使属性名相同(如
n1),在内存中也是独立存在的两个变量。
访问查找规则¶
当调用 sub.x 或 sub.method() 时,JVM 按照以下顺序查找:
- 子类 (Sub):先看自己有没有,且是否有权限访问?
- 有 -> 使用子类的。
- 父类 (Base):如果没有,去直接父类找。
- 有 -> 使用父类的。
- 上级父类... -> Object:依次向上查找。
- 报错:如果一直找到顶层都没有,则编译报错。
注意:如果是
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. 多态的具体体现¶
多态主要体现在两个方面:
- 方法的多态:
- 重载 (Overload):同一个方法名,输入参数不同,功能不同(编译时多态)。
- 重写 (Override):同一个方法名,子类覆盖父类,行为改变(运行时多态)。
- 对象的多态 (核心):
- 核心公式:
父类的引用 指向 子类的对象 - 语法:
Father obj = new Son(); - 编译类型:
=号左边(Father),决定了你**能调用哪些方法**。 - 运行类型:
=号右边(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 运行时的灵魂。
当调用对象方法的时候,该方法会和该对象的**内存地址(运行类型)**绑定。
- 调用方法时:JVM 会看这个对象真正的内存是啥(
new的是啥)。如果子类有这个方法,就执行子类的;如果没有,才去父类找。 - 调用属性时:**没有**动态绑定机制。哪里声明,就在哪里使用。
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 内置的类,如
String、Integer等,以及数组)都直接或间接继承自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):
- 一致性:如果两个对象通过
equals方法比较是相等的,那么它们的hashCode必须相同。 - 引用关系:如果两个引用指向同一个对象(如
aa3 = aa),哈希值肯定一样。 - 差异性:如果两个对象不相等,
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());
hashCode是为了提高哈希结构容器的效率。- 指向同一对象的引用,哈希值一定相同。
- 指向不同对象的引用,哈希值通常不同。
- 后续在集合章节中,如果需要,也会重写
hashCode。
这是为您补充整理的 toString 和 finalize 方法的简练笔记,保持了与之前一致的结构风格。
toString 方法¶
基本介绍:
- 默认行为:
Object类的默认实现返回 "全类名+@+哈希值的十六进制"。 - 源码逻辑:
getClass().getName() + "@" + Integer.toHexString(hashCode())(参考标准JDK行为)。 - 主要用途:子类往往重写此方法,用于返回对象的属性信息,以便查看和调试。
触发机制:
- 当直接输出一个对象时(例如
System.out.println(monster)),toString方法会被默认调用。
最佳实践:
- 在自定义类中重写该方法,打印出更有意义的字段信息(如 name, job, sal 等)。
finalize 方法¶
触发时机:
- 当对象被回收时,系统会自动调用该对象的
finalize方法。 - 销毁前奏:在销毁对象前,会先调用此方法,子类可以重写以进行资源释放操作。
回收条件:
- 当某个对象**没有任何引用**时,JVM 就认为该对象是一个垃圾对象,会使用垃圾回收机制来销毁它。
主动调用 GC:
- 垃圾回收机制主要由系统决定(有自己的 GC 算法),但也可以通过
System.gc()主动触发垃圾回收机制。
实际应用 (老韩提示):
- 实际开发中几乎不会运用
finalize。 - 学习它的目的更多是为了**应付面试**。