接上节 JavaSE 基础语法 - 类和对象 - ④ 的内容。
26、Object 类简介
我们先来看一个简单的问题。
定义一个学生类:
publicclassStudent{Stringname;intage;}测试代码:
publicclassTest{publicstaticvoidmain(String[]args){Studentstu=newStudent();System.out.println(stu);}}运行结果:
Student@3b07d329大家有没有想过,Student 类里面明明什么方法都没有,为什么还能直接打印对象?
再比如:
Studentstu=newStudent();stu.toString();代码同样可以正常运行。
可是我们从来没有写过 toString() 方法。
那么这个 toString() 方法到底是从哪里来的?
解:所有 Java 类都会默认继承 Object 类。
26.1 什么是 Object 类
Object类位于:java.lang.Object它是 Java 中最顶层(根)的类。
也就是说:普通类都直接或间接继承 Object(数组类型也是 Object 的子类)。
例如:
我们定义:
publicclassStudent{}实际上编译器会自动理解成:
publicclassStudentextendsObject{}只是:
extendsObject被 Java 自动省略了。
因此 Student 类实际上拥有 Object 类中的所有非私有成员。
26.2 Object 在继承体系中的地位
Java 的继承关系可以理解为一棵树。
Object为树顶,其他的都是它的分支。
像 String 、 ArrayList 、 TreeSet 这种,最终都会继承 Object。
因此:Object 被称为所有类的父类(Root Class)。
26.3 为什么所有类都继承 Object?
为什么 Java 要设计一个 Object 类?
原因很简单:现实生活中,不同的对象虽然类型不同,但它们都有一些共同的行为。
例如:
学生:姓名、年龄 汽车:品牌、价格 手机:品牌、颜色虽然数据不同。
但是它们都可以:
- 比较
- 打印
- 获取类型
- 获取哈希值
因此:
Java 将这些所有对象都具有的公共能力,统一放到了Object类中。
这样所有类都自动拥有这些能力。
这也是继承的思想。
27、对象打印
在Object 类中定义了:
publicStringtoString()下面重点说一下:为什么打印对象会得到一串奇怪的内容。
27.1 为什么打印对象得到的是地址
来看下面代码。
publicclassStudent{Stringname="张三";}测试:
publicclassTest{publicstaticvoidmain(String[]args){Studentstu=newStudent();System.out.println(stu);}}输出:
Student@3b07d329很多认为:这是对象地址。其实严格来说,并不是对象地址。
而是:类名 + @ + 对象哈希值(十六进制)
为什么?下面继续分析。
System.out.println() 底层做了什么?
平常经常会写:
System.out.println(stu);实际上,println() 方法存在多个重载。
例如:
println(intx)println(doublex)println(charx)println(Objectobj)当传入对象时调用的是:
println(Objectobj)它的内部源码如下:
publicvoidprintln(Objectx){Strings=String.valueOf(x);if(getClass()==PrintStream.class){// need to apply String.valueOf again since first invocation// might return nullwriteln(String.valueOf(s));}else{synchronized(this){print(s);newLine();}}}继续进入:String.valueOf 的源码:
publicstaticStringvalueOf(Objectobj){return(obj==null)?"null":obj.toString();}看到这里就明白了。
也就是说:
System.out.println(stu);底层实际上等价于:
System.out.println(stu.toString());因此真正执行的是:toString()
27.2 Object 中的 toString()
Object 中默认的:toString() 到底返回什么?
源码如下:
publicStringtoString(){returngetClass().getName()+"@"+Integer.toHexString(hashCode());}它由三部分组成:类全限定名 + @ + 对象哈希值(16进制)
例如:
com.demo.Student@3b07d329如果类没有包,那么输出:
Student@3b07d329所以很多人误认为 3b07d329 是对象地址。实际上它只是对象的哈希值转换成十六进制后的结果。
注意:哈希值并不等于对象在内存中的地址,只是在默认实现中通常与对象身份相关。JVM 并没有规定 hashCode() 必须返回对象地址。
27.3 为什么要重写 toString()
默认输出 Student@3b07d329 这一串,对于我们来说几乎没有任何意义。
例如我们真正想看到的是:
Student{name='张三',age=18}或者:
姓名:张三 年龄:18因此在开发中,都会重写:toString(),让对象能够按照我们希望的格式输出。
重写 toString()
Student:
publicclassStudent{privateStringname;privateintage;publicStudent(Stringname,intage){this.name=name;this.age=age;}@OverridepublicStringtoString(){return"Student{name='"+name+"', age="+age+"}";}}测试:
Studentstu=newStudent("张三",18);System.out.println(stu);输出:
Student{name='张三',age=18}这样输出就比原来的更直观。
27.4 @Override 注解
在重写方法时,前面都会写:@Override
例如:
@OverridepublicStringtoString(){return"...";}那么,它有什么作用?
什么是注解?
注解(Annotation)可以理解为:写给编译器看的说明信息。
它不会直接改变程序的运行结果,但可以帮助编译器检查代码。
@Override 的作用
@Override 表示:当前方法是重写(Override)父类的方法。
例如:
@OverridepublicStringtoString(){return"Student{name='"+name+"', age="+age+"}";}编译器会检查:
- 父类中是否真的存在 toString() 方法;
- 方法名、参数列表、返回值类型是否符合重写规则。
如果写错了,例如:
@OverridepublicStringtostring(){return"Student";}由于方法名写成了 tostring(小写 s),编译器会立即报错,帮助我们及时发现问题。
如果没有 @Override:
publicStringtostring(){return"Student";}程序虽然能够编译通过,但这并不是重写,而是定义了一个新的方法。此时调用:
System.out.println(stu);仍然会执行 Object 类中的 toString(),输出依旧是:
Student@3b07d329因此:建议在所有重写方法前都加上 @Override 注解,它不仅能提高代码可读性,还能帮助编译器进行检查,减少低级错误。
28、对象数组
一个学生需要创建一个对象,那么多个学生就要创建多个对象。
如果现在班级只有一名学生,这样写完全没有问题。
但是,如果班级有50名学生呢。
难道要写:
Studentstu1=newStudent("张三",18);Studentstu2=newStudent("李四",19);Studentstu3=newStudent("王五",20);......Studentstu50=newStudent("赵六",18);很明显,这种方式既麻烦,又不利于后续管理。
那么,有没有一种方式能够统一管理多个 Student 对象?
答案就是:对象数组。
28.1 为什么需要对象数组
之前学习整数数组:
int[]nums={10,20,30,40,50};数组中存放的是:
1020304050那么,Student 能不能也放到数组中?
当然可以。例如:
Student[]students=newStudent[50];这样就可以保存 50 个 Student 对象。
因此:对象数组就是元素类型为引用数据类型(对象)的数组。
它最大的作用就是:统一管理多个对象。
例如:
学校:
Student[]↓ 张三 李四 王五 赵六以后学习 ArrayList 的时候,其实底层思想也是类似的。
28.2 对象数组的定义
对象数组定义方式与普通数组完全一致。
语法格式:
类名[]数组名;例如:
Student[]students;也可以:
Studentstudents[];但是推荐:
Student[]students;这样符合 Java 官方规范。
创建对象数组
使用new创建。
例如:
Student[]students=newStudent[5];表示创建一个长度为 5 的 Student 数组。
注意:这里创建的是数组对象。不是Student 对象。
这一点非常重要。
28.3 对象数组初始化
对象数组有三种初始化方式。
第一种:动态初始化
例如:
Student[]students=newStudent[3];运行后,数组有三个元素,每个元素都为 null。
那为什么不是三个 Student 对象,而是 null 。
原因:new Student[3] 只是创建了数组,没有创建 Student 对象。
因此数组中的每一个元素默认值都是null,因为 Student 属于引用类型。
引用类型默认值就是:null
第二种:静态初始化
例如:
Students1=newStudent("张三",18);Students2=newStudent("李四",20);Student[]students={s1,s2};或者:
Student[]students={newStudent("张三",18),newStudent("李四",20),newStudent("王五",19)};创建数组同时,创建对象。
第三种:先创建数组,再存放对象
例如:
Student[]students=newStudent[3];students[0]=newStudent("张三",18);students[1]=newStudent("李四",20);students[2]=newStudent("王五",19);这种方式是比较常用的。
28.4 对象数组访问
访问方式与普通数组完全一致。
例如:
System.out.println(students[0]);输出:
Student{name='张三',age=18}访问成员:
System.out.println(students[0].getName());输出:
张三修改对象:
students[0].setAge(20);再次打印:
System.out.println(students[0]);输出:
Student{name='张三',age=20}说明:数组保存的是对象引用。
28.5 对象数组遍历
遍历方式与普通数组一样。
例如:
for(inti=0;i<students.length;i++){System.out.println(students[i]);}增强 for:
for(Studentstu:students){System.out.println(stu);}输出:
Student{name='张三',age=18}Student{name='李四',age=20}Student{name='王五',age=19}28.6 JVM 内存分析
下面我们来分析一下对象数组在内存中到底是怎么存储的。
来看:
Student[]students=newStudent[3];JVM 内存:
可以看到,数组里面没有 Student,只有三个 null
继续:
students[0]=newStudent("张三",18);内存:
数组里面保存的是:Student对象地址,而不是对象本身。
继续:
students[1]=newStudent("李四",20);students[2]=newStudent("王五",19);最终:
因此:对象数组保存的是对象引用(地址),真正的对象依然存放在堆内存中。