前言
前面我们已经学了继承,在继承中我们会发现,子类重写了父类的方法,最终使用的子类的方法,而父类中方法的方法体没啥用,那么是否能把它剩略呢?另一个问题就是类不能多继承,子类的功能是不方便的扩展与维护的。这两个问题都会随着本章所讲解的抽象类与接口所回答,接下来就让我们带着问题进入本章的阅读。
博客主页:KC老衲爱尼姑的博客主页
共勉:talk is cheap, show me the code
作者是爪哇岛的新手,水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
🌍1.1抽象类
🌍什么是抽象类?
在java中类是用来描述对象的信息,当一个类没有包含足够的信息去描述一个具体的对象,那么这样的类被称为抽象类。比如
圆形,正方形,三角形都属于图形,因此和图形的关系是继承关系。而图形类中虽然有draw()方法,但是图形类不是一个具体的形状,所以无法去画一个具体的图形。又因为图形类本身就是通过对圆形,正方形,三角形等图形进一步抽象而来的类,导致其方法是无法具体实现,所以可以将该类称之为抽象类。
🌍1.2为什么要使用抽象类?
先来看一段代码
public class Shape {
public void draw(){
System.out.println("画一个图形");
}
}
public class Circular extends Shape{
public void draw(){
System.out.println("画一个圆形");
}
}
public class Square extends Shape{
public void draw(){
System.out.println("画一个正方形");
}
}
public class Triangle extends Shape{
public void draw(){
System.out.println("画一个三角形");
}
}
public class Text {
public static void main(String[] args) {
Shape c = new Circular();
c.draw();
Shape t = new Triangle();
t.draw();
Shape s = new Square();
s.draw();
}
}
圆形,正方形,三角形都重写了父类Shape中的draw()方法,最终执行的是子类中的方法,而父类中draw();方法体中的内容一点用都没有。此时就可以将该方法定义为抽象方法,而包含抽象方法得到类则定义为抽象类。
🌍1.3抽象类的语法
在java中,如果一个类被abstract修饰则称之为抽象类,抽象类中的倍abstract修饰的方法被称为抽象方法,而抽象方法是可以不写具体的方法体。
代码示例
public abstract class Shape {
public int area;
private double height;
public static final int width =90;
public Shape(int area, double height) {
this.area = area;
this.height = height;
}
public abstract void draw();
public void print(){
System.out.println("我是一个实例方法");
}
public static void method(){
System.out.println("我是一个静态方法");
}
public int getArea() {
return area;
}
public void setArea(int area) {
this.area = area;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public static int getWidth() {
return width;
}
}
抽象类与普通类最大的区别就是抽象类被abstract,以及该类中可以定义抽象方法。在其它方面基本与普通类差不多,如抽象类可以定义实例成员变量,可以提供构造器,可以定义实例方法与静态方法。
🌍1.4抽象类特性
1.抽象类不能直接实例化对象
Shape s=new Shape();
创建对象是通过一个具体的类,描述出一个具体的对象。而图形本身就是抽象出来的概念,将它实例出来的图形还是图形,那么图形是啥呢?而实例化对象是获得一个具体对象,所以抽象类不能直接实例化对象。
2.抽象方法不能是 private 修饰的
public abstract class Shape {
private abstract void draw();
}
private修饰的成员只能在本类中访问,而抽象方法被其它子类访问并重写的。
注意:抽象方法没有加访问限定符时,默认是public.
- 抽象方法不能被final和static修饰,因为抽象方法要被子类重写
public abstract class Shape {
public final abstract void draw();
}
public abstract class Shape {
public static abstract void draw();
}
final修饰方法都不能被重写,static修饰的方法也不能被继承。
4.抽象类必须被继承,并且继承后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用 abstract 修饰
代码示例
public abstract class Shape {
public abstract void draw();
public abstract void rotating();
}
public abstract class Triangle extends Shape{
public abstract void print();
}
public class LsoscelesTriangle extends Triangle {
@Override
public void draw() {
System.out.println("画一个等腰三角形");
}
@Override
public void print() {
System.out.println("我是一个等腰三角形");
}
@Override
public void rotating() {
System.out.println("等腰三角形旋转90度");
}
}
public class Text {
public static void main(String[] args) {
LsoscelesTriangle l=new LsoscelesTriangle();
l.draw();
l.print();
}
}
三角形定义为抽象类并继承图形类是可以不重写图形类中的draw();方法,而等腰三角形类继承了三角形类,所以它要重写图形类和三角形类中的所有的抽象方法。
5.抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
6.抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量
1.5🌍抽象类的使用总结与注意事项
- 抽象类可以理解成类的不完整设计图,是用来被子类继承的。
- 一个类如果继承了抽象类,那么这个类必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。
- 不能用abstract修饰变量、代码块、构造器。
- 有得有失: 得到了抽象方法,失去了创建对象的能力。
🌍1.6final和abstract是什么关系?
互斥关系
- abstract定义的抽象类作为模板让子类继承,final定义的类不能被继承。
- 抽象方法定义通用功能让子类重写,final定义的方法子类不能重写。
🌍2.1接口
🌍2.2什么是接口?
接口在我们现实中随处可见,比如笔记本电脑上的USB接口,它可以插U盘,鼠标,键盘,所有符合USB协议的设备,比如电源插座上的插孔,可以插符合插孔规范的电脑,电视机等设备,比如寝室们的锁孔,凡是符合这把锁的钥匙都能插,并且打开门。通过这些例子可以得出一个结论:接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。 在Java中,接口可以看成是:多个类的公共规范,是一种引用数据类型。
🌍2.3为什么要使用接口?
先来看一段代码
public abstract class Animal {
abstract void eat();
abstract void run();
abstract void Swimming();
abstract void fly();
}
public class Bird extends Animal{
@Override
void eat() {
System.out.println("鸟吃虫子");
}
@Override
void run() {
System.out.println("鸟用小爪子跳着走");
}
@Override
void Swimming() {
}
@Override
void fly() {
System.out.println("鸟用翅膀飞");
}
}
public class Dog extends Animal {
@Override
void eat() {
System.out.println("狗吃肉");
}
@Override
void run() {
System.out.println("狗用4条腿跑");
}
@Override
void Swimming() {
}
@Override
void fly() {
}
}
public class Fish extends Animal{
@Override
void eat() {
System.out.println("鱼吃微生物");
}
@Override
void run() {
}
@Override
void Swimming() {
System.out.println("鱼在水里游泳");
}
@Override
void fly() {
}
}
定义一个抽象的动物类,并提供抽象方法,然后让狗,鱼,鸟三个动物继承并重写该类中的方法后,就会产生一个问题,那就是狗是不会游泳的,是不会飞的,同理其它物种也是一样,这与实际不符合,此时就需要接口来解决和这个问题,将某些行为定义成接口,然后让子类实现接口即可。
接口语法规则
🌍接口的定义与特点
接口用关键字interface来定义
public interface 接口名 {
// 常量
// 抽象方法
}
代码示例
public interface Fly {
//public abstract void fly();
//public static final int a=90;
void fly();
int a=90;
}
在接口中默认的方法是抽象方法,变量默认是常量。所以修饰符可以直接省略。
注意
- 创建接口时, 接口的命名一般以大写字母 I 开头.
- 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性.
- 接口的命名一般使用 “形容词” 词性的单词
🌍2.4接口使用
接口自身就是过度抽象的类不能直接使用,必须要有一个"实现类"来"实现"该接口,实现接口中的所有抽象方法。
代码示例
public interface IUSB {
void openDevice();
void closeDevice();
}
public class KeyBoard implements IUSB{
@Override
public void openDevice() {
System.out.println("打开键盘");
}
@Override
public void closeDevice() {
System.out.println("关闭键盘");
}
public void inPut(){
System.out.println("键盘输入");
}
}
public class Mouse implements IUSB{
@Override
public void openDevice() {
System.out.println("使用鼠标");
}
@Override
public void closeDevice() {
System.out.println("不使用鼠标");
}
public void click(){
System.out.println("鼠标点击");
}
}
public class Computer {
public void powerOn(){
System.out.println("打开笔记本电脑");
}
public void powerOff(){
System.out.println("关闭笔记本电脑");
}
public void useDevice(IUSB usb){
usb.openDevice();
if(usb instanceof Mouse){
Mouse mouse = (Mouse)usb;
mouse.click();
}else if(usb instanceof KeyBoard){
KeyBoard keyBoard = (KeyBoard)usb;
keyBoard.inPut();
}
usb.closeDevice();
}
}
public class Text {
public static void main(String[] args) {
Computer computer = new Computer();
computer.powerOn();
// 使用鼠标设备
computer.useDevice(new Mouse());
// 使用键盘设备
computer.useDevice(new KeyBoard());
computer.powerOff();
}
}
运行结果
//使用鼠标
//鼠标点击
//不使用鼠标
//打开键盘
//键盘输入
//关闭键盘
//关闭笔记本电脑
🌍2.5接口特性
1.接口类型是一种引用类型,但是不能直接new接口的对象
接口是抽象类进一步的抽象,凡是抽象的都不能实例化对象
public class Text2 {
public static void main(String[] args) {
IUSB i=new IUSB();
}
}
2.接口中每一个方法都是public的抽象方法, 即接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)
public interface IUSB {
private abstract void openDevice();
private abstract void closeDevice();
}
3.接口中的方法是不能在接口中实现的,只能由实现接口的类来实现。
public interface USB {
void openDevice();
// 编译失败:因为接口中的方式默认为抽象方法
// Error:(5, 23) java: 接口抽象方法不能带有主体
void closeDevice(){
System.out.println("关闭USB设备");
}
}
接口中的方法是抽象方法是被实现类所所重写的,不能在接口中实现。
4.接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量
public interface IUSB {
//public static final int a=90;
int a=90;
}
5.接口中不能有静态代码块和构造方法
public interface USB {
// 编译失败
public USB(){
}
{
} // 编译失败
void openDevice();
void closeDevice();
}
因为接口是进一步的抽象,在接口里面所有定义的变量都默认是常量,而常量必须就地初始化,因此没有必要提供构造器或者代码块来初始化成员变量。
6.使用default关键字定义的方法是接口默认的方法
public interface IUSB {
default void print(){
System.out.println("我是USB接口");
}
}
7.重写接口中方法时,不能使用default访问权限修饰
public interface IUSB {
void openDevice();
void closeDevice();
}
public class KeyBoard implements IUSB{
@Override
default void openDevice() {
System.out.println("打开键盘");//编译报错
}
@Override
default void closeDevice() {
System.out.println("关闭键盘");//编译报错
}
public void inPut(){
System.out.println("键盘输入");
}
}
实现类中重写的方法的访问权限需大于等于接口中,而接口中方法的访问权限修饰符默认是public,所以实现类中所重写的方法必须用public修饰。
🌍2.6注意事项
- 接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class
- 如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
- dk8中:接口中还可以包含default方法。
实现多个接口
java中不能多继承只能单继承也就是不能同时拥有多了亲爹,但是java支持类有多个干爹,这个干爹就是接口。
代码示例
定义一个动物类
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
实现会飞,会游泳,会跑等是三个接口
public interface IFly {
void fly();
}
public interface IRunning {
void run();
}
public interface ISwimming {
void swim();
}
猫只能凭借4条腿跑
public class Cat extends Animal implements IRunning{
public Cat(String name) {
super(name);
}
@Override
public void run() {
System.out.println(name+"四条腿跑的快");
}
}
鸟是会飞的
public class Bird extends Animal implements IFly{
public Bird(String name) {
super(name);
}
@Override
public void fly() {
System.out.println(name+"用翅膀飞");
}
}
青蛙既能游泳又能跑
public class Frog extends Animal implements IRunning,ISwimming {
public Frog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(name+"跳着走");
}
@Override
public void swim() {
System.out.println(name+"游泳");
}
}
鸭子既能跑,又能游泳,更能飞。
public class Duck extends Animal implements IFly,IRunning,ISwimming{
public Duck(String name) {
super(name);
}
@Override
public void fly() {
System.out.println(name+"用鸭翅膀飞");
}
@Override
public void run() {
System.out.println(name+"用大脚趾跑");
}
@Override
public void swim() {
System.out.println(name+"游泳");
}
}
上述代码都是一个类继承一个类,同时实现多个接口。继承体现类与类之间的关系是is -a关系,接口是体现了事物xxx的特征。
比如猫是一只动物,具有跑的特征,青蛙是一只动物,既能跑,也能游泳。
有了接口后,类的实现者不必关心具体的类型,而只要关注类是否具备某种能力。
比如,现在实现一个吃东西的方法
public class Text {
public static void main(String[] args) {
Bird b = new Bird("小鸟");
Cat c = new Cat("小猫");
Frog f = new Frog("青蛙");
Duck d = new Duck("大黄鸭");
run(b);
run(c);
run(f);
run(d);
}
public static void run(IRunning run) {
run.run();
}
}
//运行结果
小鸟会用爪子跑
小猫四条腿跑的快
青蛙跳着走
大黄鸭用大脚趾跑
上述代码等价于
public class Text {
public static void main(String[] args) {
IRunning b = new Bird("小鸟");
IRunning c = new Cat("小猫");
IRunning f = new Frog("青蛙");
IRunning d = new Duck("大黄鸭");
b.run();
c.run();
f.run();
d.run();
}
}
//运行结果
小鸟会用爪子跑
小猫四条腿跑的快
青蛙跳着走
大黄鸭用大脚趾跑
实现接口可以看做特殊的继承,上述的代码就是多态的体现,父类引用指向子类对象,最终调用的是具体子类的方法。
接口间的继承
在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到 多继承的目的。
接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字.
public interface IRunning {
void run();
}
public interface ISwimming {
void swim();
}
public interface IAmphibious extends IRunning,ISwimming{
}
public class Frog2 implements IAmphibious {
@Override
public void run() {
System.out.println("青蛙跳着走");
}
@Override
public void swim() {
System.out.println("青蛙游泳");
}
}
通过接口继承创建了一个新的接口IAmphibious,而frog2实现了这个接口,就要重写该接口里面的swim()方法以及run()方法。
🌍2.7接口使用实例
🌍2.8给引用类型的数据排序
####🌍2.9 接口Comparable的使用
首先准备2个学生类。
public class Student {
private String name;
private int age;
private double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
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 double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
@Override
public String toString() {
return "Studnet{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
}
学生测试类
public class StudentText {
public static void main(String[] args) {
Student s=new Student("叶秋涵",19,100);
Student s2=new Student("叶子秋",15,99);
}
}
既然要对引用类型的数据进行排序,那么是否是运算符来比较大小呢?肯定是不可以的,对象与对象的比较,如同人与人之间的比较,总要具体到某一件事上才能比较,好比家人总是说你张三成绩比你好,这就是比较了2对象之间的成绩。所以不是拿对象那本身来互相比较,而是具体到对象的某一属性来比较。
为了指定到对象的某一属性来进行比价,我们让Student实现Comparable接口,并实现其中的compareTo接口。
public class Student implements Comparable<Student>{
private String name;
private int age;
private double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
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 double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
@Override
public String toString() {
return "Studnet{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
@Override
public int compareTo(Student o) {//按年龄比较
return this.age-o.age;
}
}
Studnet测试类
public class StudentText {
public static void main(String[] args) {
Student s=new Student("叶秋涵",11,100);
Student s2=new Student("叶子秋",15,99);
if(s.compareTo(s2)>0){
System.out.println("s>s2");
}else if(s.compareTo(s2)<0){
System.out.println("s<s2");
}else{
System.out.println("s==s2");
}
}
}
接口compareTo()的比较规则
- 如果认为左边数据大于右边数据则返回正整数
- 如果认为左边数据小于右边数据则返回负整数
- 如果认为左边数据等于右边数据则返回负整数
- 默认是升序,若要降序将比价的2个对象的参数互换即可。即o2-o1.
使用Arrays.sort();对引用类型的数组进行排序。
示例代码一
public class Student /*implements Comparable<Student>*/{
private String name;
private int age;
private double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
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 double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
@Override
public String toString() {
return "Studnet{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
// @Override
// public int compareTo(Student o) {
// return this.age-o.age;
// }
}
public class StudentText {
public static void main(String[] args) {
Student [] arr=new Student[3];
arr[0]=new Student("叶秋涵",11,100);
arr[1]=new Student("叶子秋",15,99);
arr[2]=new Student("老衲爱尼姑",13,39);
// if(arr[0].compareTo(arr[1])>0){
// System.out.println("s>s2");
// }else if(arr[0].compareTo(arr[1])<0){
// System.out.println("s<s2");
// }else{
// System.out.println("s==s2");
// }
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
}
代码无法编译过去,因为sort();会将传入的对象强制转换为Comparable,而此时的Student没有实现Comparable是无法强转的,同时sort()方法会自动调用compareTo()方法,也就是意味着对引用类型的数组进行排序也要事先指定排序规则。
Student实现了Comparable并重写了ompareTo(),最后成功对该数组排序。
[Studnet{name='叶秋涵', age=11, score=100.0}, Studnet{name='老衲爱尼姑', age=13, score=39.0}, Studnet{name='叶子秋', age=15, score=99.0}]
🌍2.9 自定义比较器( Comparator)
如果我们需要按分数对学生排序,那么按照上述的代码,我们还需修改compareTo()中的排序规则,某一天又需要对年龄比较又要修改回来,这样就非常麻烦此时需要另一个接口Comparator(),来解决此问题。
自定义年龄比较器
ublic class AgeComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge()-o2.getAge();
}
}
自定义分数比较器
class ScoreComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return Double.compare(o1.getScore(), o2.getScore());
}
}
自定义姓名比较器
public class NameComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.getName().compareTo(o2.getName());
}
}
补充字符串的排序比较
使用字符串的compareTo();接口来比较。
int compareTo(String anotherString) ,虽然返回值是Int,但是实际比较的两个字符串的ascii码值。返回的值(正数、负数、0)有三种情况:
- 如果第一个字符和参数的第一个字符不等,结束比较,返回与第一个字符的ASCII码差值;
- 如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较,直到不同字符返回差值;
- 如果两个字符串为子串关系,则返回两个字符串的长度差值。
使用比较器对学生数组进行排序
public class Text {
public static void main(String[] args) {
Student [] arr=new Student[3];
arr[0]=new Student("叶秋涵",11,100);
arr[1]=new Student("子秋",15,99);
arr[2]=new Student("老衲爱尼姑",13,39);
System.out.println("按年龄排序");
AgeComparator ageComparator=new AgeComparator();
Arrays.sort(arr,ageComparator);
System.out.println(Arrays.toString(arr));
System.out.println("按分数排序");
ScoreComparator scores=new ScoreComparator();
Arrays.sort(arr,scores);
System.out.println(Arrays.toString(arr));
System.out.println("按字典序排序");
NameComparator name=new NameComparator();
Arrays.sort(arr,name);
System.out.println(Arrays.toString(arr));
}
}
运行结果
按年龄排序
[Studnet{name='叶秋涵', age=11, score=100.0}, Studnet{name='老衲爱尼姑', age=13, score=39.0}, Studnet{name='子秋', age=15, score=99.0}]
按分数排序
[Studnet{name='老衲爱尼姑', age=13, score=39.0}, Studnet{name='子秋', age=15, score=99.0}, Studnet{name='叶秋涵', age=11, score=100.0}]
按字典序排序
[Studnet{name='叶秋涵', age=11, score=100.0}, Studnet{name='子秋', age=15, score=99.0}, Studnet{name='老衲爱尼姑', age=13, score=39.0}]
🌍3.0Clonable 接口和深拷贝
java 中内置了一些很有用的接口, Clonable 就是其中之一. Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 “拷贝”. 但是要想合法调用 clone 方法, 必须要 先实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常。要拷贝的类实现了Clonable();该接口是个空接口,也就是接口里面没有任何的抽象方法,实现该接口只是为了标记要拷贝的对象,以便能成功拷贝。
🌍3.1Clonable的使用
public class Person implements Cloneable{
public int age;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
'}';
}
public static void main(String[] args) throws Exception {
Person p=new Person();
p.age=90;
System.out.println(p);
Person p2=(Person) p.clone();
System.out.println(p2.age);
System.out.println("============");
p2.age=99;
System.out.println(p2.age);
}
}
运行结果
Person{age=90}
90
============
99
上述代码内存解析
clone方法克隆了一份对象p并返回一个Object的对象,由于该方法的返回值为Object,所以需将其强转为Person在赋值给Person。当我们通过p2引用修改age可以知道不会修改引用p所指向对象中的age,因为这是两份内存,是互不干扰的。
🌍3.2浅拷贝
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向 原来的对象.换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象.
浅拷贝代码示例
public class Person implements Cloneable{
public int age;
Wallet w=new Wallet();
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
'}';
}
}
public class Wallet {
double money = 99;
}
public class WalletText {
public static void main(String[] args) throws CloneNotSupportedException {
Person p = new Person();
Person p1 = (Person) p.clone();
System.out.println(p1.w.money);
System.out.println("修改后");
p1.w.money=12;
System.out.println(p1.w.money);
}
}
运行结果
//99.0
//修改后
//12.0
浅拷贝内存图
🌍3.3深拷贝
被复制对象的所有变量都含有与原来的对象相同的值.而那些引用其他对象的变量将指向被 复制过的新对象.而不再是原有的那些被引用的对象.换言之.深拷贝把要复制的对象所引用的对象都 复制了一遍
深拷贝代码示例
public class Wallet implements Cloneable{
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
double money = 99;
@Override
public String toString() {
return "Wallet{" +
"money=" + money +
'}';
}
}
public class Person implements Cloneable {
public int age;
public Wallet w = new Wallet();
@Override
protected Object clone() throws CloneNotSupportedException {
Person tmp = (Person) super.clone();
tmp.w = (Wallet) this.w.clone();
return tmp;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", w=" + w +
'}';
}
}
public class WalletText2 {
public static void main(String[] args) throws CloneNotSupportedException {
Person p=new Person();
p.age=90;
System.out.println(p);
System.out.println("修改后");
Person p1=(Person) p.clone();
p1.age=90;
p1.w.money=900;
System.out.println(p1);
}
}
运行结果
Person{age=90, w=Wallet{money=99.0}}
修改后
Person{age=90, w=Wallet{money=900.0}}
深拷贝内存图
为了完成深拷贝,只需将引用p所指的对象先拷贝,后将p对象的w引用所指向的对象再拷贝一份,最后修改p1中引用w的地址即可。为了完成这个过程需要拷贝Wallet,故该类也需实现Cloneable并重写clone()方法,还需一个中间变量tmp去修改p1中引用w的地址。
🌍3.4抽象类和接口的区别
核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中 不能包含普通方法, 子类必须重写所有的抽象方法.
普通区别
- 从组成来说,抽象类是由普通类加抽象方法组成,而接口是抽象方法+全局常量。
- 从访问权限来说,抽象类可以使用各种权限修饰符,而接口只能用public
- 从子类角度来说,子类使用extends继承抽象类,对于实现接口则是用implements
- 从类与类之间的关系来说,一个抽象类可以实现多个接口,但是接口不能继承抽象类,但能使用extends关键字继承多个接口。
- 从子类角度来说,一个子类只能继承一个抽象类,但是能实现多个接口。
🌍4.1Object类
Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父 类。即所有类的对象都可以使用Object的引用进行接收。
代码示例使用Object接收其它类的引用
public class Text {
public static void main(String[] args) {
fun(new Person());
fun(new Wallet());
}
private static void fun(Object o) {
System.out.println(o);
}
}
运行结果
clonable.Person@1b6d3586
clonable.Wallet@4554617c
🌍4.1获取对象信息
如果要打印对象中的内容,可以直接重写Object类中的toString()方法,不重写打印的地址。
// Object类中的toString()方法实现:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
只需用idea重写toString()即可,打印对象的信息。
🌍4.2equals()
在Java中,= =进行比较时:
- 如果== 左右两侧是基本类型变量,比较的是变量中值是否相同
- .如果==左右两侧是引用类型变量,比较的是引用变量地址是否相同
- .如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的:
代码示例
没有重写equals
public static void main(String[] args) {
Person p=new Person();
p.age=90;
Person p1=new Person();
p1.age=90;
System.out.println(p.age == p1.age);
System.out.println(p == p1);
System.out.println(p.equals(p1));
}
运行结果
true
false
false
重写了equals
public static void main(String[] args) {
Person p=new Person();
p.name="叶秋涵";
Person p1=new Person();
p1.name="叶秋涵";
System.out.println(p == p1);
System.out.println(p.name.equals(p1.name));//比较对象的属性
}
运行结果
false
true
🌍4.3hashcode方法
回忆刚刚的toString方法的源码:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
调用hashCode();返回哈希值,然后通过Integer.toHexString以16进制打印出来。
hashcode方法源码:
public native int hashCode();
该方法是一个native方法,底层是由C/C++代码写的。我们看不到。 我们认为两个名字相同,年龄相同的对象,将存储在同一个位置,如果不重写hashcode()方法,我们可以来看示例 代码:
没有重写hashCode()l;
public class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class Text {
public static void main(String[] args) {
Person p=new Person("阿茂",19);
Person p2=new Person("阿茂",19);
System.out.println(p.hashCode());
System.out.println(p2.hashCode());
}
}
运行结果
460141958
1163157884
注意事项:两个对象的hash值不一样。
重写hashCode();后
public 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 Text {
public static void main(String[] args) {
Person p=new Person("阿茂",19);
Person p2=new Person("阿茂",19);
System.out.println(p.hashCode());
System.out.println(p2.hashCode());
}
}
运行结果
38003601
38003601
注意事项:哈希值一样。
结论:
1、hashcode方法用来确定对象在内存中存储的位置是否相同
2、事实上hashCode() 在哈希表中才有用,在其它情况下没用。在哈希表中hashCode() 的作用是获取对象的哈希值,进而确定该对象在哈希表中的位置。
最后的话
各位看官如果觉得文章写得不错,点赞评论关注走一波!谢谢啦!