目录
1. 抽象类
1.1 抽象类概念
1.2 抽象类语法
1.3 抽象类需要注意的点
1.4 抽象类的作用
2. 接口
2.1 接口的概念
2.2 语法规则
2.3 接口使用
2.4 接口特性
2.5 实现多个接口
2.6 接口间的继承
2.7 接口使用实例
2.8 Clonable接口,浅拷贝和深拷贝
2.9 抽象类和接口的区别
3. Object类
3.1 获取对象信息
3.2 对象比较equals方法
3.3 hashcode方法
1. 抽象类
1.1 抽象类概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果 一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。 比如:
abstract class Shape {
public abstract void draw();//抽象方法
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("🟢");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("🖼️");
}
}
public class Test2 {
public static void drawMap(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
drawMap(new Rect());
drawMap(new Cycle());
}
}
在打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由 Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为抽象类(abstract class).
1.2 抽象类语法
在Java中,一个类如果被 abstract 修饰称为抽象类,抽象类中被 abstract 修饰的方法称为抽象方法,抽象方法不用 给出具体的实现体。
// 抽象类:被abstract修饰的类
public abstract class Shape {
// 抽象方法:被abstract修饰的方法,没有方法体
abstract public void draw();
abstract void calcArea();
// 抽象类也是类,也可以增加普通方法和属性
public double getArea(){
return area;
}
protected double area; // 面积
}
注意:抽象类也是类,内部可以包含普通方法和属性,甚至构造方法
1.3 抽象类需要注意的点
1. 抽象类和抽象方法都是使用abstract修饰的
2. 抽象类不能实例化,但是普通类可以
3. 抽象类中不一定包含抽象方法,但包含抽象方法的类一定是抽象类
4. 抽象类中可以定义成员变量和成员方法
5. 当一个普通类继承了抽象类,那么普通类中一定要重写抽象类中的抽象方法
6. 抽象类存在的最大的意义就是被继承
7. 当一个抽象类A继承了抽象类B,此时抽象类A不需要重写抽象类B中的抽象方法,但是当一个普通类C继承了抽象类A,此时就要重写所有的抽象方法
8. 抽象方法不能被final,static,private修饰,因为抽象方法要被子类重写
9. final关键字与abstract关键字不可能同时作用在一个方法或者类上
10. 抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量
1.4 抽象类的作用
抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.
有些同学可能会说了, 普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?
确实如此. 但是使用抽象类相当于多了一重编译器的校验. 使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类 了, 使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.
很多语法存在的意义都是为了 "预防出错", 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们. 充分利用编译器的校验, 在实际开发中是非常有意义的.
2. 接口
2.1 接口的概念
接口其实就是一种公共的行为规范标准,可以算抽象类的进一步抽象,大家在实现时,只要符合这个标准的,就都可以用这个接口.
在Java中,接口可以看成是:多个类的公共规范,是一种引用数据类型。
2.2 语法规则
接口的定义格式与定义类的格式基本相同,将class关键字换成 interface 关键字,就定义了一个接口。
public interface 接口名称{
// 抽象方法
public abstract void method1(); // public abstract 是固定搭配,可以不写
public void method2();
abstract void method3();
void method4();
// 注意:在接口中上述写法都是抽象方法,跟推荐方式4,代码更简洁
}
提示:
1. 创建接口时, 接口的命名一般以大写字母 I 开头.
2. 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性
2.3 接口使用
接口不能直接使用,必须要有一个"实现类"来"实现"该接口,实现接口中的所有抽象方法。
public class 类名称 implements 接口名称{
// ...
}
注意:子类和父类之间是 extends 继承关系,类与接口之间是 implements 实现关系。
2.4 接口特性
1. 接口中每一个方法都是public的抽象方法, 即接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)
public interface IShape {
// Error:(4, 18) java: 此处不允许使用修饰符private
private void darw();
void draw1();
}
2. 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现
public interface IShape {
void darw();
// 编译失败:因为接口中的方式默认为抽象方法
// Error:(5, 23) java: 接口抽象方法不能带有主体
void draw1() {
System.out.println("画图形!");
}
}
3. 但是在接口中的方法有两个特例
3.1 在接口中,静态方法可以有具体的实现
interface IShape {
// int age3 = 1;
void draw();
public static void draw1() {
System.out.println("1234");
}
}
3.2 被default关键字修饰的方法可以有具体的实现(从1.8开始Java引入了这个特性)
interface IShape {
// int age3 = 1;
void draw();
default void draw4() {
System.out.println("1234");
}
public static void draw1() {
System.out.println("1234");
}
}
4. 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量
interface IShape {
int age3 = 1;
void draw();
}
public class Test {
public static void main(String[] args) {
System.out.println(IShape.age3); // 可以直接通过接口名访问,说明是静态的
IShape.age3 = 3; // 编译报错:Error:(12, 12) java: 无法为最终变量brand分配值
// 说明age3具有final属性
}
}
5. 接口类型是一种引用类型,但是不能直接new接口的对象
public class Test {
public static void main(String[] args) {
IShape shape = new IShape();
}
}
// Error:(10, 19) java: day20210915.USB是抽象的; 无法实例化
6. 当一个类实现了一个接口后,这个类必须重写这个接口当中的抽象方法
7. 当接口中,存在default方法,可以选择重写,也可以选择不重写,具体看需求
interface IShape {
void draw();
default void draw4() {
System.out.println("1234");
}
}
class Cycle implements IShape {
@Override
public void draw() {
System.out.println("🟢");
}
@Override
public void draw4() {
System.out.println("我觉得父接口的这个默认方法不好,我自己重写!");
}
}
8. 不管是接口还是抽象类,它们仍是可以发生向上转型.
(接口)
interface IShape {
void draw();
/*default void draw4() {
System.out.println("1234");
}*/
}
class Cycle implements IShape {
@Override
public void draw() {
System.out.println("🟢");
}
/*@Override
public void draw4() {
System.out.println("我觉得父接口的这个默认方法不好,我自己重写!");
}*/
}
class Rect implements IShape {
@Override
public void draw() {
System.out.println("🖼️");
}
}
public class Test {
public static void drawMap(IShape shape) {
shape.draw();
}
public static void main(String[] args) {
IShape iShape = new Cycle();
IShape iShape1 = new Rect();
drawMap(iShape);
drawMap(iShape1);
}
}
(抽象类)
abstract class Shape {
public abstract void draw();//抽象方法
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("🟢");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("🖼️");
}
}
public class Test {
public static void drawMap(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
Shape shape = new Cycle();
Shape shape1 = new Rect();
drawMap(shape);
drawMap(shape1);
}
}
9. 子类重写方法的时候,这个方法一定要是public修饰的
interface IShape {
void draw();
}
class Rect implements IShape {
// 编译报错,重写IShape中draw方法时,不能使用默认修饰符
// 必须要是public
@Override
void draw() {
System.out.println("🖼️");
}
}
10. 接口中不能有代码块和构造方法
interface IShape {
void draw();
// 编译失败
{
}
// 编译失败
public IShape() {
}
}
11. 一个类如果不想实现接口中的方法,这个类可以被定义为抽象类
12. 接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class
2.5 实现多个接口
一个类实现多个接口,可以解决Java中多继承的问题
语法是一定要先继承类在实现接口
interface IFlying {
void fly();
}
interface ISwimming {
void swim();
}
interface IRunning {
void run();
}
abstract class Animal {
private String name;
private int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
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 abstract void eat();
}
class Dog extends Animal implements IRunning, ISwimming {
public Dog(String name, int age) {
super(name, age);
}
@Override
public void swim() {
System.out.println(this.getName() + "正在狗刨!");
}
@Override
public void run() {
System.out.println(this.getName() + "正在跑步!");
}
@Override
public void eat() {
System.out.println(this.getName() + "正在吃狗粮!");
}
}
class Bird extends Animal implements IFlying {
public Bird(String name, int age) {
super(name, age);
}
@Override
public void fly() {
System.out.println(this.getName() + "正在用翅膀飞!");
}
@Override
public void eat() {
System.out.println(this.getName() + "正在吃鸟量!");
}
}
class Robot implements IRunning {
@Override
public void run() {
System.out.println("机器人在跑!");
}
}
public class Test {
public static void test1(Animal animal) {
animal.eat();
}
public static void testFly(IFlying iFlying) {
iFlying.fly();
}
public static void testRun(IRunning running) {
running.run();
}
public static void testSwim(ISwimming iSwimming) {
iSwimming.swim();
}
public static void main(String[] args) {
testFly(new Bird("小鸟", 2));
testRun(new Dog("小狗", 1));
testSwim(new Dog("小狗", 1));
testRun(new Robot());
}
public static void main1(String[] args) {
test1(new Dog("小狗", 1));
test1(new Bird("小鸟", 2));
}
}
通过上述代码,我们可能会有如下疑惑
为什么不能将三个接口都写成在Animal类的方法呢?
因为有的动物不会飞
为什么不能把三个接口写成三个类呢?
因为Java当中,只支持单继承.
继承表达的含义是 is - a 语义, 而接口表达的含义是具有 xxx 特性.
时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力.比如在上述代码中,不需要是动物,机器人也可以跑.
2.6 接口间的继承
接口和接口之间也是存在关系的,用extends关键字来关联,此时认为这个意思是扩展接口功能的意思
interface A {
void testA();
}
interface B {
void testB();
}
//扩展功能!
interface C extends A,B {
void testC();
}
class D implements C {
@Override
public void testA() {
}
@Override
public void testB() {
}
@Override
public void testC() {
}
}
可以看到在D类当中要重写A,B接口中的抽象方法.
接口间的继承相当于把多个接口合并在一起.
2.7 接口使用实例
给对象数组排序
class Student {
public String name;
public int age;
public double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
}
public class Test {
public static void main(String[] args) {
Student[] array = new Student[3];
array[0] = new Student("zhangsan",89,39.9);
array[1] = new Student("lisi",69,59.9);
array[2] = new Student("wangwu",39,68.4);
System.out.println("排序前:" + Arrays.toString(array));
Arrays.sort(array);
System.out.println("排序后:" + Arrays.toString(array));
}
}
我们知道Arrays类中的sort这个方法是可以给数组进行排序的,但是给自定义类型排序时则不行了.
我们可以点进ComparableTimeSort错误,去看源代码.
可以看出数组的内容被强转成了Comparable类型,那Comparable又是什么呢?是一个接口,这个接口当中还有一个compareTo方法,那么我们的学生对象怎么能和这个Comparable接口产生联系呢?
用学生类实现这个接口即可,然后再重写接口中的compareTo方法就可以达到给对象数组排序的效果了(代码是按照年龄排序的).
class Student implements Comparable<Student>{
public String name;
public int age;
public double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
@Override
public int compareTo(Student o) {
/*if(this.age - o.age > 0) {
return 1;
}else if(this.age == o.age) {
return 0;
}else {
return -1;
}*/
return this.age - o.age;
}
}
public class Test {
public static void main(String[] args) {
Student[] array = new Student[3];
array[0] = new Student("zhangsan",89,39.9);
array[1] = new Student("lisi",69,59.9);
array[2] = new Student("wangwu",39,68.4);
System.out.println("排序前:" + Arrays.toString(array));
Arrays.sort(array);
System.out.println("排序后:" + Arrays.toString(array));
}
}
为了进一步加深对接口的理解, 我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序)
class Student implements Comparable<Student>{
public String name;
public int age;
public double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
@Override
public int compareTo(Student o) {
/*if(this.age - o.age > 0) {
return 1;
}else if(this.age == o.age) {
return 0;
}else {
return -1;
}*/
return this.age - o.age;
}
}
public class Test {
public static void main(String[] args) {
Student[] array = new Student[3];
array[0] = new Student("zhangsan",89,39.9);
array[1] = new Student("lisi",69,59.9);
array[2] = new Student("wangwu",39,68.4);
System.out.println("排序前:" + Arrays.toString(array));
//Arrays.sort(array);
bubbleSort(array);
System.out.println("排序后:" + Arrays.toString(array));
}
public static void bubbleSort(Comparable[] comparables) {
for (int i = 0; i < comparables.length-1; i++) {
for (int j = 0; j < comparables.length-1-i; j++) {
if(comparables[j].compareTo(comparables[j+1]) > 0){
Comparable tmp = comparables[j];
comparables[j] = comparables[j+1];
comparables[j+1] = tmp;
}
}
}
}
}
但是我们会发现 Comparable接口中的compareTo方法被写死了,如果我不按照年龄来排序,那该怎么办呢?我现在来按照成绩排序,还有一个Comparator接口,这个接口更加的灵活.
class Student {
public String name;
public int age;
public double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
}
class AgeComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
class ScoreComparator implements Comparator<Student> {
public int compare(Student o1, Student o2) {
return (int)(o1.score - o2.score);
}
}
public class Test {
public static void main(String[] args) {
Student student1 = new Student("zhangsan",19,30.9);
Student student2 = new Student("lisi",9,80.9);
AgeComparator ageComparator = new AgeComparator();
int ret = ageComparator.compare(student1,student2);
System.out.println(ret);
System.out.println("=====================");
ScoreComparator scoreComparator = new ScoreComparator();
int ret2 = scoreComparator.compare(student1,student2);
System.out.println(ret2);
}
}
2.8 Clonable接口,浅拷贝和深拷贝
class Student {
public int age;
public Student(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args) {
Student student1 = new Student(10);
Student student2 = (Student) student1.clone(); // 编译报错
}
}
这个clone方法student1这个引用没有访问权限
class Student implements Cloneable{
public int age;
public Student(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
public static void main(String[] args) {
Student student1 = new Student(10);
Student student2 = (Student) student1.clone(); // 编译失败
}
}
此时,出现了异常,我们只需要处理这个异常就ok了
这个Cloneable接口,里面什么都没有,它的作用是什么呢?
这个接口被叫做标记接口,只要实现了该接口,就说明当前的类是可以被克隆的.
class Student implements Cloneable{
public int age;
public Student(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException{
Student student1 = new Student(10);
Student student2 = (Student) student1.clone();
System.out.println(student1);
System.out.println(student2);
}
}
此时证明克隆成功了.
接下来我们在Student类中新增一个对象
class Money {
public double money;
}
class Student implements Cloneable{
public int age;
public Money m = new Money();
public Student(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
public static void main(String[] args) {
Student student1 = new Student(10);
student1.m.money = 19.9;
Student student2 = (Student) student1.clone(); // 此时还是编译报错 还按照上面的步骤
// 就可以解决
}
}
改正后的代码
class Money implements Cloneable{
public double money;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Student implements Cloneable{
public int age;
public Money m = new Money();
public Student(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException{
Student student1 = new Student(10);
student1.m.money = 19.9;
Student student2 = (Student) student1.clone();
System.out.println(student1.m.money);
System.out.println(student2.m.money);
}
}
class Money implements Cloneable{
public double money;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Student implements Cloneable{
public int age;
public Money m = new Money();
public Student(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException{
Student student1 = new Student(10);
student1.m.money = 19.9;
Student student2 = (Student) student1.clone();
System.out.println(student1.m.money);
System.out.println(student2.m.money);
System.out.println("=================");
student1.m.money = 29.9;
System.out.println(student1.m.money);
System.out.println(student2.m.money);
}
}
为什么我只动改了student1引用指向的对象中的属性,怎么student2引用指向的对象中的属性也跟着改了呢?
首先,我们的代码,先是将student1引用指向的对象拷贝一份,student2这个引用指向了这个拷贝的对象,但是在new Student()对象中,我们还有一个m引用指向的对象没有被拷贝,此时,m引用在student2这个引用指向的对象中,m引用里面的地址值没有改变,所以对student1引用指向的对象中的money属性改动,student2中的也没有变.这种没有彻底将所有对象拷贝的方式就叫做浅拷贝
那么,深拷贝就是将所有对象都拷贝了.
把上面的代码改成深拷贝
只要将上面Student类中的clone方法内容重写就行了.
2.9 抽象类和接口的区别
1. 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中 不能包含普通方法, 子类必须重写所有的抽象方法.
2. 使用extends关键字继承抽象类,使用关键字implements来实现接口
3. 一个子类只能继承一个抽象类,但是一个子类可以实现多个接口
4. 一个抽象类可以实现多个接口,但是接口不能继承抽象类,接口只能继承接口.
3. Object类
Object是Java默认提供的一个类。Java里面除了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) {
System.out.println(obj);
}
}
所以在开发之中,Object类是参数的最高统一类型。但是Object类也存在有定义好的一些方法。如下:
3.1 获取对象信息
如果要打印对象中的内容,可以直接重写Object类中的toString()方法,之前已经讲过了,此处不再累赘。
// Object类中的toString()方法实现:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
3.2 对象比较equals方法
在Java中,==进行比较时:
a.如果==左右两侧是基本类型变量,比较的是变量中值是否相同
b.如果==左右两侧是引用类型变量,比较的是引用变量地址是否相同
c.如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的:
// Object类中的equals方法
public boolean equals(Object obj) {
return (this == obj); // 使用引用中的地址直接来进行比较
}
class Person{
private String name ;
private int age ;
public Person(String name, int age) {
this.age = age ;
this.name = name ;
}
}
public class Test {
public static void main(String[] args) {
Person p1 = new Person("gaobo", 20) ;
Person p2 = new Person("gaobo", 20) ;
int a = 10;
int b = 10;
System.out.println(a == b); // 输出true
System.out.println(p1 == p2); // 输出false
System.out.println(p1.equals(p2)); // 输出false
}
}
Person类重写equals方法后,然后比较:
class Person{
...
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false ;
}
if(this == obj) {
return true ;
}
// 不是Person类对象
if (!(obj instanceof Person)) {
return false ;
}
Person person = (Person) obj ; // 向下转型,比较属性值
return this.name.equals(person.name) && this.age==person.age ;
}
}
结论:比较对象中内容是否相同的时候,一定要重写equals方法。
3.3 hashcode方法
我们看到了hashCode()这个方法,他帮我算了一个具体的对象位置,
该方法是一个native方法,底层是由C/C++代码写的。我们看不到。 我们认为两个名字相同,年龄相同的对象,将存储在同一个位置,如果不重写hashcode()方法,我们可以来看示例 代码:
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class TestDemo4 {
public static void main(String[] args) {
Person per1 = new Person("gaobo", 20) ;
Person per2 = new Person("gaobo", 20) ;
System.out.println(per1.hashCode());
System.out.println(per2.hashCode());
}
}
//执行结果
460141958
1163157884
注意事项:两个对象的hash值不一样.
像重写equals方法一样,我们也可以重写hashcode()方法。此时我们再来看看。
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class TestDemo4 {
public static void main(String[] args) {
Person per1 = new Person("gaobo", 20) ;
Person per2 = new Person("gaobo", 20) ;
System.out.println(per1.hashCode());
System.out.println(per2.hashCode());
}
}
//执行结果
460141958
460141958
注意事项:哈希值一样。
结论:
1、hashcode方法用来确定对象在内存中存储的位置是否相同
2、事实上hashCode() 在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的 散列码,进而确定该对象在散列表中的位置。