目录
1. Object类
1.1 什么是Object类?
1.2 Object中的常用成员方法
1. toString方法
2. equals方法
3. hashCode方法
1. Cloneable接口
#. clone方法
#. 浅拷贝与深拷贝
2. 比较接口
#. Comparable接口
#. Comparator接口
#. 优劣对比
2. 内部类
2. 静态内部类
3. 局部内部类
4. 匿名内部类
## 总结与补充
1. Object类
1.1 什么是Object类?
Object是Java默认提供的一个类。
Object
类是所有类的隐式父类,无论是否显式声明继承,所有类都直接或间接继承自Object
。
例如,使用Object接收所有类的对象:
//自定义类
class Person{}
class Student{}public class Test {public static void main(String[] args) {function(new Person());function(new Student());}public static void function(Object obj) { //用Object接收所有类型System.out.println(obj);}
}
其实我们点开println方法,可以发现它的形参变量是Object类型的:
1.2 Object中的常用成员方法
Object类中的方法有这么多:
虽然我们作为一个合格的程序员要学会这里面的所有方法,但目前我们有能力学会的只有toString方法、equals方法 和 hashCode方法这3个方法。其他方法涉及到垃圾回收机制、反射机制和线程机制。
1. toString方法
功能:将对象转换为字符串表示形式。
Object类中toString方法原型:
// Object类中的toString()方法实现:
public String toString() {return getClass().getName() + "@" + Integer.toHexString(hashCode());}
在《类和对象(2)—— 封装》中我就讲解过,println(对象名)本质上是调用了toString方法,只需要对toString方法进行重写,使其返回对象成员的信息,我们就能实现对象的打印。
例如,重写Person类的toString方法:
public String toString(){return "Person{" +"name="+name +", " +"age="+age +"}";}
}
【这里不多作赘述,详细请看《类和对象(2)—— 封装》】
2. equals方法
功能:用于比较两个对象的内容是否相等。
- 默认情况下,Object类的equals方法只是简单地比较两个对象的引用是否相同(即是否是同一个对象)。
- 在实际应用中,我们经常需要根据对象的实际内容来判断它们是否“相等”,因此通常会重写这个方法。
Object类中equals方法原型:
public boolean equals(Object obj) {return (this == obj);
}
- 可以看到,默认的equals方法是用“==”号来进行数值比较。如果我们用 “==”号 来比较两个引用类型,那么比较的是它们两个的地址,这样做肯定不符合我们的需求。
- 所以我们要使用equals()时,就必须在自定义类中的重写equals方法。
例如,重写Person类的equals方法:
public class Person {private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic boolean equals(Object obj) {if(obj == null){ //null无法比较return false;}if(this == obj){ //地址指向相同时,对象一定相同return true;}if(!(obj instanceof Person)){ //若obj不是Person或其子类,那么this与obj无法比较return false;}//经过上一个if语句的判断,obj可以安全向下转型Person person = (Person) obj;//需要两个成员变量都相等时,方可认为这两个是同一个对象return this.name.equals(person.name) && this.age == person.age;}
}public class Test {public static void main(String[] args) {Person person1 = new Person("小明",20);Person person2 = new Person("小明",20);if(person1 == person2){System.out.println("它们地址相同");}else if(person1.equals(person2)){System.out.println("他们是同一个对象");}}
}
【补充:String类内部已经有重写的equals方法,不需要我们自己重写,直接用就可以了】
从运行结果可以看出,虽然new出来的两个对象地址不同,但是按照我们自定义的比较方法(重写equals方法)来看,他们是同一个对象。
3. hashCode方法
功能:生成并返回一个哈希码值。
- 默认情况,Object中的hashCode方法会根据对象的地址生成哈希码。
- 在实际应用中,我们通常会根据对象中的一个或多个成员变量来生成哈希码。
例如,默认使用对象的地址生成:
public class Person {private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic boolean equals(Object obj) {……必须name和age都相同时,两个对象才相同…… //重写equals方法}
}public class Test {public static void main(String[] args) {Person person1 = new Person("小明",20);Person person2 = new Person("小明",20);if(person1 == person2){System.out.println("它们地址相同");}if(person1.equals(person2)){System.out.println("它们是同一个对象");}if(person1.hashCode() == person2.hashCode()){System.out.println("它们的哈希码相同");}}
}
这里person1和person2本是同一个对象,它们的地址不相同,但由于使用的是默认的hashCode方法,通过地址生成的哈希码是不一样的。
Object类中hashCode方法原型:
【native关键字修饰的方法没有方法体,它是由底层代码(用的是C或C++)进行实现,具体细节我们无法看到】
取模法得到哈希码:
这里简单解释一下哈希码的生成过程,一个最简单的生成方式——取模法。
计算机中的内存是连续排列的,相等于一个很大的数组。对象生成后肯定会有一个地址,我们用一个这个对象的地址 除以 内存(数组)的大小,得到的余数(模)就是我们的哈希值。
【哈希码的获得还可以通过 乘法与取模法结合、位运算法……】
一个地址对应一个哈希表,但是一个哈希码可以对应多个地址。
我们用哈希表(桶)来存储哈希码,简单来说哈希表结合了数组与链表。因为不管用什么方法得到的哈希码都会有概率复(比如4和14取余得到的哈希码都是4),于是我们把重复的部分用链表来连接起来。类似下图:
【当数据量大的时候,数组连接的不一定是链表,有可能是红黑树】
hash方法:
来源:
java.util.Objects
工具类中的静态方法(Java 7+引入)。目的:简化多字段哈希值的生成,替代手动计算哈希值的繁琐操作。
hash方法可以接受多个参数,自动组合它们的哈希值生成最终结果。参数是对象类中的成员变量。
例如,用一个字段生成哈希码:
public class Person {private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic int hashCode() {return Objects.hash(name); //用name生成哈希码}@Overridepublic boolean equals(Object obj) {……必须name和age都相同时,两个对象才相同…… //重写equals方法}
}public class Test {public static void main(String[] args) {Person person1 = new Person("小明",20); Person person2 = new Person("小明",18); //同名不同年龄if(person1 == person2){System.out.println("它们地址相同");}if(person1.equals(person2)){System.out.println("它们是同一个对象");}if(person1.hashCode() == person2.hashCode()){System.out.println("它们的哈希码相同");}}
}
这里的两个人名相同,但是年龄不同,所以他们不是同一个对象。但由于此时的哈希码是根据name生成的,所以会输出“它们的哈希码相同”。
【如果同时用name和age来生成哈希码( 例如return hash(age, name); ),它们的哈希值会不同。但请注意:当存储的数据量大的时候,哈希码仍然可能会重复,所以hashCode方法与equals方法必须同时重写。即——用所有字段生成哈希码时,如果两个对象的哈希码不同,则这两个对象一定不同;但如果两者哈希码相同,这两个对象不一定相同,要用equals进行检验。】
1.3 Object类与常见接口
1. Cloneable接口
#. clone方法
Object 类中存在一个 clone 方法,调用这个方法可以创建一个对象的 "拷贝"。但是要想合法调用 clone 方法,必须要先实现 Clonable 接口,否则就会抛出CloneNotSupportedException 异常。
Object中clone方法的原型:
- clone方法由native关键字修饰,无法看到底层结构。
- 默认的clone方法被protected修饰(只能被同一分支线上的继承类使用),因此子类必须覆盖(重写)为
public
才能被外部调用。- 返回一个克隆对象,类型是Object,接收时必须向下转型。
- 重写克隆方法时,要声明可以抛出CloneNotSupportedException异常。
- 使用clone方法的方法也要声明可以抛出CloneNotSupportedException异常。(包括main方法)
Clonable接口的原型:
反例1:实现了Clonable接口,但在main方法中没有声明可以抛出CloneNotSupportedException异常
public class Person implements Cloneable{ //要实现Cloneable接口private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}@Override //重写方法要声明可以抛出异常protected Object clone() throws CloneNotSupportedException {return super.clone();}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +'}';}
}public class Test { //error:main方法没有声明可抛出异常public static void main(String[] args) {Person p1 = new Person("小明",18);Person p2 = (Person) p1.clone();System.out.println(p2);}
}
结果:
反例2:都有抛出异常和重写方法,但是没有实现Clonable接口
public class Person { //error:没有实现Cloneable接口private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}@Override //重写方法要声明可以抛出异常protected Object clone() throws CloneNotSupportedException {return super.clone();}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +'}';}
}public class Test { //main方法要声明可抛出异常public static void main(String[] args) throws CloneNotSupportedException{Person p1 = new Person("小明",18);Person p2 = (Person) p1.clone();System.out.println(p2);}
}
结果:
正确做法:既要实现Clonable接口,也要在出现clone方法的方法声明可以抛出异常
public class Person implements Clonable{ //要实现Cloneable接口private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}@Override //重写方法要声明可以抛出异常protected Object clone() throws CloneNotSupportedException {return super.clone();}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +'}';}
}public class Test { //main方法要声明可抛出异常public static void main(String[] args) throws CloneNotSupportedException{Person p1 = new Person("小明",18);Person p2 = (Person) p1.clone();System.out.println(p2);}
}
为什么想要使用clone方法这么麻烦?
因为有些数据我们不希望被拷贝,而且默认的clone方法是浅拷贝,容易引发问题。
在 Java 早期版本中,设计者需要为对象克隆提供一种标准机制,但面临两个核心矛盾:
-
灵活性:允许开发者自定义对象的克隆行为。
-
安全性:避免所有对象默认支持克隆(某些对象克隆可能导致不可预测的副作用,如线程不安全或破坏不可变性)。
最终,Java 选择了标记接口这一种折中方案。将Cloneable
设计成一个空接口,并把clone()
方法分离在 Object
类中。这种分离导致以下问题:
#. 浅拷贝与深拷贝
浅拷贝:
定义:
创建一个新对象,复制原始对象的所有字段:
基本类型字段:直接复制值(如
int
、boolean
等)。引用类型字段:仅复制引用地址(新旧对象共享同一块内存中的引用对象)。
特点:
修改新对象中的基本类型字段不会影响原对象。
修改新对象中的引用类型字段(如修改对象的属性)会影响原对象,因为它们指向同一内存地址。
默认的clone方法是浅拷贝。
例如:
class Money{int money = 1000;
}public class Person implements Cloneable{String name;Money m;public Person(String name) {this.name = name;this.m = new Money();}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}void printPerson(){System.out.println("姓名:"+name+",工资:"+m.money);}
}public class Test {public static void main(String[] args) throws CloneNotSupportedException {Person person1 = new Person("小明");person1.printPerson();Person person2 = (Person) person1.clone(); //person2是person1的浅拷贝person2.printPerson();System.out.println("============修改后=============");person2.m.money = 200; //只修改person2中的money值person1.printPerson();person2.printPerson();}
}
这里我们只修改person2中的m.money值,person1的m.money值也受到影响,所以说默认的clone方法是浅拷贝。下面是图例:
深拷贝:
定义:
创建一个新对象,覆写递归复制原始对象及其所有引用对象:
基本类型字段:直接复制值。
引用类型字段:创建新的对象实例,并复制其内容(新旧对象完全独立)。
特点:
修改新对象中的任何字段(基本类型或引用类型)均不会影响原对象。
clone方法的深拷贝化:
- 每个 内层引用对应的类 都必须重写clone方法
- 在外层对象的重写方法中,先用 默认的克隆方法( super.clone()浅拷贝 ) 构造出拷贝框架,然后让每个引用变量的轮流调用自己的克隆方法。
例如:
class Money implements Cloneable{int money = 1000;@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}
}public class Person implements Cloneable{String name;Money m;public Person(String name) {this.name = name;this.m = new Money();}@Overrideprotected Object clone() throws CloneNotSupportedException {Person tmpPerson = (Person) super.clone(); //克隆外层对象tmpPerson.m = (Money) this.m.clone(); //克隆内层引用return tmpPerson;}void printPerson(){System.out.println("姓名:"+name+",工资:"+m.money);}
}public class Test {public static void main(String[] args) throws CloneNotSupportedException {Person person1 = new Person("小明");person1.printPerson();Person person2 = (Person) person1.clone();person2.printPerson();System.out.println("============修改后=============");person2.m.money = 200; //只改person2中的money值person1.printPerson();person2.printPerson();}
}
深拷贝后,person1和person2的内容完全分离。
2. 比较接口
#. Comparable接口
在实际应用中,我们是会让两个对象进行比较大小的(比较的是对象中的某一项属性)。Comparable接口用于规范对象之间的比较方式,当我们实现了Comparable接口,就可以通过对象名调用的方式进行对象之间的比较。
Comparable接口的原型:
public interface Comparable<T> {public int compareTo(T o); }
compareTo方法的返回值类型是int,一般来说:(默认的返回值规则)
- 返回值是正数,当前对象 > 传入对象;
- 如果返回值是负数,当前对象 > 传入对象;
- 如果返回的是0,当前对象 = 传入对象;
- 【因为封装类(如Integer类)中的compareTo方法是自然升序的,所以才有默认返回值规则】
Comparable接口中只有一个compareTo方法,重写compareTo方法就可以让对象具有可比较性。
【<T>是泛型,为了不让泛型的内容影响到我们的理解,这里我用通用基类Object类来代替演示(反正泛型会先用Object类接收,再转换为指定类型)】
例如:
public class Student implements Comparable {private String name;private int score;public Student(String name, int score) {this.name = name;this.score = score;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", score=" + score +'}' ;}@Overridepublic int compareTo(Object o) { //重写compareTo方法,实现Comparable接口Student s = (Student) o;return this.score - s.score;}
}public class Test {public static void main(String[] args) {Student student1 = new Student("小明",78);Student student2 = new Student("小军",83);Student student3 = new Student("小亮",63);Student[] students = {student1, student2, student3};System.out.println(Arrays.toString(students));Arrays.sort(students);System.out.println("排序后:");System.out.println(Arrays.toString(students));}
}
反例 —— 如果我们没有实现Comparable接口,直接使用sort方法对students数组进行排序,那么会报错:
原因是sort方法会需要你给他一个比较的规则,这个比较的规则就是compareTo方法:
【黄色的是compareTo和Comparable】
#. Comparator接口
如果不实现Comparable接口(即不重写CompareTo方法),且需要比较的话,我们还有一种方法,那就是使用Comparator接口。
Comparator接口的部分原型:
public interface Comparator<T> {int compare(T o1, T o2);boolean equals(Object obj); }
- 【这个只是接口原型的一部分,Comparator接口中还有很多默认方法,抽象方法只有compare方法和equals方法】
- 我们只需重写compare方法就算是实现了Comparator接口。因为所以类都继承自Object类,Object类中有默认的equals方法,相当于Object类帮我们重写了。(equals方法原型可参考上文的“1.2.2 equals方法”)
- compare方法完全没有默认比较规则,实现时既可以采用升序,也可以采用降序。
- 使用Comparator比较器时,要比较的类中必须要有getter方法,以保证比较器可以获得对应字段并比较。
使用Comparator比较器时,要有一个类实现该接口,然后向排序方法中传入实现类的实例。
【为了不让泛型的内容影响到我们的理解,我们还是用Object类来完成重写】
我们知道使用sort的前提是要告诉它一个比较规则,既然要比较的类中没有告诉它,那么我们单独传一个比较规则的实例(比较器)给它:
//要比较的类
public class Student {private String name;private int score;public Student(String name, int score) {this.name = name;this.score = score;}public int getScore() { //使用比较器时必须要有getter方法return score;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", score=" + score +'}';}
}//实现构造器的类
public class ScoreComparator implements Comparator {@Overridepublic int compare(Object o1, Object o2) {Student s1 = (Student) o1;Student s2 = (Student) o2;//这里采用降序return s2.getScore() - s1.getScore(); //字段被private修饰,通过getter方法获得字段}
}public class Test {public static void main(String[] args) {Student student1 = new Student("小明",78);Student student2 = new Student("小军",83);Student student3 = new Student("小亮",63);Student[] students = {student1, student2, student3};System.out.println(Arrays.toString(students));ScoreComparator scoreComparator = new ScoreComparator();Arrays.sort(students, scoreComparator); //传入比较规则的实例System.out.println("排序后:");System.out.println(Arrays.toString(students));}
}
方法调用图如下:
#. 优劣对比
Comparable 接口
定义:通过实现
Comparable
接口,类可以定义其对象的自然排序规则(如String
的字典序、Integer
的数值大小)。优点:
内聚性强:排序逻辑与类本身绑定,符合面向对象的封装原则。
简洁性:直接调用
sort(对象数组)
即可排序,无需额外参数。默认支持:Java 集合框架(如
TreeSet
、TreeMap
)自动使用Comparable
排序。缺点:
单一排序规则:每个类只能定义一种自然排序( 只能针对一个字段 ),无法灵活切换。
有侵入性:需修改类源码,无法为第三方库的类添加自然排序。若排序规则需变更,必须修改比较类的内部代码。
Comparator 接口
定义:通过实现
Comparator
接口,可定义多种外部排序策略,独立于类本身。优点:
灵活性:支持多字段组合排序策略(如按年龄、姓名、工资排序)。
无侵入性:无需修改比较类的源码,适合第三方库或不可修改的类。
动态配置:运行时切换排序规则(如用户选择排序方式)。
缺点:
代码分散:排序逻辑分布在多个
Comparator
实现中,可能降低内聚性。额外实例化:需为每个排序策略创建实例(可通过单例或缓存优化)。
冗余代码:若多个类需相似排序逻辑,需重复实现
Comparator
。
总结:
Comparable
是“我是可排序的”:内聚性强,但灵活性低。
Comparator
是“我可以帮你排序”:灵活性高,但逻辑分散。二者互补:
Comparable
定义默认排序,Comparator
扩展多策略排序。
注意区分构造器与比较器。构造器是类的构造方法;而比较器的本质是Comparator接口,它的具体实现是一个类的实例。
2. 内部类
在外部类中,内部类定义位置与外部类成员所处的位置相同,因此称为内部类。而内部类又可以分为4种。
1. 成员内部类(实例内部类)
定义:定义在外部类中的类,且没有
static
修饰符。也称作实例内部类。
特点:成员内部类可以访问外部类的所有成员(包括私有成员)。
成员内部类的实例化:
情况1:在外部类中实例化
情况2:在方法中实例化
例1:情况1
public class Outer {private int a = 111;class Inner{ //成员内部类public void printOuterMember(){System.out.println(a);}}Inner innerClass = new Inner(); //在外部类中直接实例化内部类
}public class Test {public static void main(String[] args) {Outer outer = new Outer();outer.innerClass.printOuterMember(); }
}
例2 :通过“ 外部类类型的引用变量.new 内部类构造方法 ”的方法创建内部类对象
public class Outer {private int a = 111;class Inner{ //成员内部类public void printOuterMember(){System.out.println(a);}}
}public class Test {public static void main(String[] args) {Outer outer = new Outer();Outer.Inner inner = outer.new Inner(); inner.printOuterMember();}
}
2. 静态内部类
定义:被
static
修饰的成员内部类。
特点:
例如:
public class Outer {//静态内部类static class Inner{ int a = 11; //静态内部类的实例变量static int b = 22; //静态内部类的静态变量}
}public class Test {public static void main(String[] args) {System.out.println(Outer.Inner.b); //可以通过类名访问System.out.println(Outer.Inner.a); //报错:不可以通过类名访问
}
结果:
可以看到,通过类名访问的方式可以访问到静态变量b,但不能访问到成员变量a,说明静态内部类中是可以存在实例变量的。
静态内部类的实例化:
情况1:在外部类中实例化
情况2:在方法中实例化
例1:对应情况1
public class Outer {private static int a = 222; //私有的静态变量static class Inner{ //静态内部类String name = "静态内部类";public void printOuterMember(){System.out.println(a);}}Inner innerClass = new Inner(); //在外部类中创建内部类的实例
}public class Test {public static void main(String[] args) {Outer outer = new Outer();System.out.println(outer.innerClass.name); //外部类实例.内部类实例.内部类成员outer.innerClass.printOuterMember();}
}
例2:对应情况2
public class Outer {private static int a = 222; //私有的静态变量static class Inner{ //静态内部类String name = "静态内部类";public void printOuterMember(){System.out.println(a);}}
}public class Test {public static void main(String[] args) {Outer.Inner inner = new Outer.Inner(); //在方法中创建内部类实例System.out.println(inner.name); //内部类实例.内部类成员inner.printOuterMember();}
}
3. 局部内部类
定义:定义在 方法体 或 代码块(包括静态代码块和实例代码块) 中的类。局部内部类的作用域仅限于所在方法或代码块。
特点:
局部内部类的实例化:
- 只能在定义它的方法体或代码块内部实例化。
- 直接通过内部类的类名创建。
例如:
public class Outer {//私有的静态变量private int a = 333; //method是外部类的成员方法void method(){final double b = 100;int c = 10; //c是隐式final变量,它的值一直都是10class Inner {public void print() {System.out.println("我是方法体中的局部内部类");System.out.println(a); //访问外部类成员System.out.println(b); //访问方法体中的final常量System.out.println(c); //访问方法体中的隐式final变量}}//局部内部类的实例化Inner inner2 = new Inner(); inner2.print();}
}public class Test {public static void main(String[] args) {Outer outer = new Outer();outer.method();}
}
4. 匿名内部类
特点:
匿名内部类的实例化:在定义时直接实例化。
interface Adding{int add(int a, int b);
}public class Test {public static void main(String[] args) {Adding adding = new Adding() { //匿名内部类的实例由引用变量adding接收@Overridepublic int add(int a, int b) {return a+b;}};System.out.println(adding.add(123,456));}
}
## 总结与补充
顶级类(外部类)只可以声明为 public 或 default默认,但对于成员内部类 和 静态内部类来说4种声明都可以。
例如:
不同内部类的修饰符支持如下:
本期分享完毕,感谢大家的支持Thanks♪(・ω・)ノ