前言:继承和多态是面向对象开发的重要环节,使用得当可以让代码的的功能更加灵动、高效,同时还可以减少代码的冗余。
一、类的继承
1、什么是继承
例如,现在有这里有一些动物:鸡,鸭,猪,狗等,他们都属于一个动物类(Animal)。根据java类的基础特性我们可以知道,类可以抽象出来实例化成为一个个具体的对象,但是他们都有很多共同属性,例如:
静态属性: 都有重量、年龄等,你甚至可以给它取个名字来当做他的静态属性。
动态属性: 都需要进食,都可以行动等。
如果将这些属性都写进猪狗鸡鸭这些类的话,那么上面的这些属性难免会重复写上几次,造成代码的冗余,那么我们就可以将这些共有的特性抽取出来抽象成一个类,然后让这些普通类包含这个类的属性,这就叫继承
2、如何继承
1、关键字extends
代码如下:
class Animal{ public String name; public int age; public void eat(){ System.out.println(name+"正在吃饭"); } } class Dog extends Animal{ public void wangwang(){ System.out.println(name+"正在汪汪叫"); } public void eat(){ System.out.println(name+"正在吃狗粮"); } } class Bird extends Animal{ public String wing; public void fly(){ System.out.println(name + "正在飞"); } @Override public void eat(){ System.out.println(name+"正在吃鸟粮"); } }
这个被共用的Animal类称为父类,这些共用父类属性的被称为子类,其基本的思想就是子类基于某个父类进行扩展,得到一个新的子类。让这个子类也可以继承父类所具有的属性,这个子类同时也可以自己增加父类所不具备的属性,
子类对象无法调用父类中被private修饰的成员,如图:
这里显示 index 在父类Animal中被 private修饰,无法访问从而报错。对于成员方法同样如此。
子类只能调用父类中被public 或者 protected 修饰的成员变量或方法。
在子类中可以使用super来调用父类的方法或者成员,也可以通过super来进行父类的构造。
2、构造方法
如果提供了子类的构造方法,那么编译器默认不再提供不带参数的构造方法,如果自己提供了子类的构造方法,那么必须先要帮助父类进行构造:
class Animal{ public String name; public int age; // 父类构造方法 public Animal(String name, int age){ this.name = name ; this.age = age ; } } class Dog extends Animal { public int dog_spec1; public int dog_spec2; // 子类构造方法 public Dog(String name, int age, int dog_spec1, int dog_spec2) { super(name, age); // 在子类构造完成之前先帮助父类构造 // 父类构造完成 this.dog_spec1 = dog_spec1; this.dog_spec2 = dog_spec2; } // 子类构造完成 }
在继承的处理机制当中,党实例化一个子类对象时,父类对象也相应的被实例化,换句话说,在实例化子类对象的时候,java编译器会在子类的构造方法中自动调用父类的无参构造方法或者提供的构造方法。
3、重写
子类在继承父类后并不只是拥有了父类的属性,还可以对父类的属性进行拓展,即对父类的成员方法进行重写。
重写就是在子类中保留父类中方法的方法名,然后重写方法体里面的实现内容,更改成员方法的权限,在上面关键字extends 的代码例子中,dog和bird两个类都继承了Animal类然后都重写了Animal类中的eat方法,这里不再举例。
注意:当重写父类方法的时候,修改方法的权限只能从小范围到大范围的改变,例如,父类中的eat方法被public 修饰,那么子类重写的eat方法就不能被private和protected 修饰。
3、Object类
java中有一种比较特殊的类,Object类是所有类的父类,是java类中的最高层的类。在用户创建一个类的时候,除非这个类已经指定了要继承某一个类(java中一个类只能有一个父类),那么它就是默认java.lang,Object 类中继承Object类,(java中每一个类都源于java.lang.Object 类)。由于所有类,除了已经有父类的子类,都是Object的子类,所以在定义类的时候,extends Object 可以省略不写。
1、Obejct类中方法:
①clone()方法
创建并返回此对象的副本,一般表达式为x.clone!= x (意思是和原来的对象不是同一个对象)。想要克隆一个对象,首先这个对象的类要实现Cloneable接口,否则则会抛出CloneNotSupportedException异常。
在java中创建对象有两种方法,一种是我们所熟悉的关键字new,一种是Object类里面的clone()方法。
那么new是如何创建对象的过程是怎么样的? new关键字用于创建类的新实例对象。new首先会根据new的对象的类型来分配空间。分配完之后,再调用构造函数,为对象进行初始化,构造方法结束后,一个对象创建完毕,然后就会返回这个对象所在堆区的引用地址,在外面就可以使用这个类的引用来接收这个地址并对这个对象进行相关操作。
都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用源对象中对应的各个域,填充新对象的域, 填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。
而clone()方法,会在堆区生成一个和源对象类相同的对象,然后将源对象的内容填充到新的对象里,这个新对象存放在堆区的不同地方,然后返回这个新对象的引用地址,在外部可以使用所兼容的类的引用来接收这个地址,即x.clone!= x。
如何使用clone()方法?
使用clone来复制一个对象的时候需要实现Cloneable这个接口,由于clone()这个方法源自Object类,我们在idea中按住左ctrl然后用左键点击clone进去可以发现里面是一个没有方法体的方法
( 由翻译得知,这段绿色的文字建议我们重写这个clone方法)
如果需要复制某个对象,成生一个新的副本,则在这个对象的类实现了这个Cloneable接口之后,还需要对这个clone方法进行重写,在idea中,在类中点击鼠标左键,然后选择Generate快速生成clone方法的重写,如图:
throws为异常关键字,它用于方法体内部,并且抛出一个异常,党程序执行到throw语句时立即终止,他后面的语句将不会执行。如果不想他抛出异常,现阶段只用将throw 及后面的异常 复制粘贴到main方法的后面。
我们使用clone来克隆一个对象,但是由于clone方法返回的是一个object类,因此此时相当于将一个父类赋值给一个子类,就形成了向下转型,对于克隆的返回值还需要进行一个强制类型转化,将其转化为与接收对象相同的类类型:
代码如下:
以学生为例子,创建一个Student类,并实现Cloneable接口,然后重写clone方法。
class Student implements Cloneable{ String name; int age; int score; public Student(String name, int age, int score) { this.name = name; this.age = age; this.score = score; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } @Override public String toString() { return "Student"+ "name:"+name +" age: "+age + " score:"+score; } } public class Main { public static void main(String[] args) throws CloneNotSupportedException { Student student1 = new Student("zhangsan",18,100); Student student2 = (Student) student1.clone(); System.out.println(student1); System.out.println(student2); } }
打印结果如下:
深浅克隆:
我们在上面的Student的类外面定义一个另外一个类Money(可以认为money是每个人的刚需必须有而生成的一个组合类型),我们暂时忽略student类当中的其他成员变量,只写入一个Money类来组合,代码如下
class Money{ public int money = 10; } class Student implements Cloneable{ public Money m = new Money(); @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } @Override public String toString() { return "Student"+ " money:"+m; } } public class Main { public static void main(String[] args) throws CloneNotSupportedException { Student student1 = new Student(); Student student2 = (Student) student1.clone(); System.out.println(student1.m.money); System.out.println(student2.m.money); student1.m.money = 100; System.out.println(student1.m.money); System.out.println(student2.m.money); } }
解释如下:
我们给出的Money类中的有一个money成员,给出的默认值为10,然后将这个类和Student类进行组合,在mian方法中生成一个新的student1对象,并对其进行克隆,然后用一个student2接收这个引用地址,重写tostring方法后进行打印。后将student其中的Money类生成的m对象中的money改为100,然后再进行money的打印。结果如下:
对比结果可以发现,修改student1的money的值后,student2 的money值也被修改了。
这是为什么呢???
这是因为clone在赋值对象的时候,其中的Money类的对象m的引用也复制了,
即:student1.m == student2.m
修改student1 的m的对象的内容,就相当于修改了student2的m对象的内容;
为了印证这个猜想,我们对其hash值进行打印:结果发现相同
图解如下
这种克隆称为浅克隆,也叫作浅拷贝。
如何进行深克隆???
如果要进行深克隆,就需要继续重写clone方法。针对克隆的m是相同引用的问题,我们做出修改:在克隆出一个student对象后,将里面的Money类的m对象的引用进行修改,要么使用new来新建一个m对象,或者使用clone对当前对象的Money类类型的m对象进行再次克隆。
注意:为了实现对m的拷贝,m的Money类也需要实现Cloneable接口并重写clone方法。
protected Object clone() throws CloneNotSupportedException { Student student = (Student) super.clone(); //student.m = new Money(); //student.m.money = this.m.money student.m = (Money)m.clone(); // m的Money类型也需要接口和重写clone方法 return student; }
②equals方法
在java中我们经常使用 == 运算符来比较其左右两端的数据,其规则如下:
如果 == 两侧都是相同的基本数据类型,则比较其值是否相同,返回true或者fasle
如果 == 两侧是对象的引用,则比较其引用的地址是否相同,例如:
public class Test { public static void main(String[] args) throws CloneNotSupportedException { String str1 = new String("123"); String str2 = new String("123"); if ( str1 == str2 ){ System.out.println("True"); }else { System.out.println("False"); } / String str3 = "123"; String str4 = "123"; if (str3 == str4){ System.out.println("True"); }else { System.out.println("False"); } if (str1.equals(str2)){ System.out.println("True"); }else { System.out.println("False"); } } }
结果为:
我们在main函数里面新创建两个str1和str2对象,然后用 == 对其进行比较,因为他们是两个不同对象的引用,所以第一个输出位false,而str3和str4,两个都是指向的同一个内容“123”,在str3去创建的时候,“123”这个字符串被放入了堆区的内存池中,str3指向这个字符串,当str4去创建一个字符串对象的时候,程序会首先去内存池查看是否已经存在相同内容的字符串,如果存在,就将其指向已经存在的字符串,否则就会生成一个新的字符串对象》》:
而.equals方法则是比较两个引用的对象的实际内容是否相同,案例如上图代码,因为str1和str2都的对象的内容都是:字符串"123",所以第三个值为true。
但是由于在自定义类中使用equals方法比较两个同类的不同对象的时候,equals方法的默认实现是使用“==”,运算符来比较两个对象的引用地址,而不是比较对象内容,所以要想真正做到比较两个自定义类的对象的内容,还需要在自定义类中重写equals方法。
③getClass()方法
他会返回对象执行时的Class实例,然后使用此实例调用getName()方法取得实例对应的类的名称,语法如下:
getClass().getName();
使用getClass方法返回并用Class对象来接收这个类的实例,并以此调用他的信息,例如这个实例里面的方法等。
④toString()方法
功能是将一个对象以字符串的形式输出,他会返回一个String实例,通常toString方法在自定义类中需要重写,来完整地输出这个对象的字符串形式。案例如下:
import java.lang.Object; class Student{ String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return name + " " + age; } } public class Main { public static void main(String[] args) throws CloneNotSupportedException { Student student1 = new Student("zhangsan",18); System.out.println(student1); } }
输出结果为
实例化一个student1对象,在重写toString方法后,调用打印函数对其进行输出,结果如上图。
但是为什么这个打印函数会自动调用toString方法???
我们在idea中按住ctrl+鼠标左键单机println,结果如下:
发现接收类型使一个Object类的x,Object类是所有类的父类,这里发生向上转型,在synchronize里面打印s,于是我们再次按住ctrl加鼠标左键单机进入String s = 后面的valueOf查看Object类的x实例转化为String s 的原理,情况如下:
发现,这里调用了你传进来的Object类的实例obj的方法toString (),而toString()方法已经在我们的子类中被重写,此时发生了动态绑定,jvm会依次在Student,Object类中查找(顺着继承链)实现了该toSting方法的类,并调用。此处已经在Student类中实现。
同时这也是多态的思想,关于多态,后面会讲到。
java类和对象:继承、多态、接口、抽象类-2