目录
一、抽象类
二、接口
三、Object类
3.1 toString()方法
3.2 hashcode()方法
3.3 equals()方法
四、常用接口
4.1 Comparable接口(比较)
4.2 Comparator接口(比较)
4.3 Cloneable接口(拷贝)
4.4 浅拷贝与深拷贝
一、抽象类
在Java中,一个类如果被 abstract 修饰称为抽象类,抽象类中被 abstract 修饰的方法称为抽象方法,抽象方法不用给出具体的实现体。
public class TestDemo { public static void main(String[] args){ Circle c = new Circle(); c.setR(5); c.getArea(); Squre s = new Squre(); s.setLength(10); s.getArea(); }}//抽象类abstract class Shape{ private int size; //抽象方法 abstract public void getArea();}class Circle extends Shape{ private int r; public int getR() { return r; } public void setR(int r) { this.r = r; } //重写抽象方法 @Override public void getArea() { double area = r*r*r*4.0/3; System.out.println("此圆形的面积是: "+area); }}class Squre extends Shape{ private int length; //重写抽象方法 @Override public void getArea() { double area = length*length; System.out.println("此正方形的面积是: "+area); } public int getLength() { return length; } public void setLength(int length) { this.length = length; }}
抽象类的特性
抽象类中使用abstract修饰类和抽象方法,这个方法没有具体的实现,抽象类中可以包含普通类所能包含的成员,抽象类所存在的最大意义就是被继承。
抽象类方法不能是私有的,如果一个普通类继承了抽象类,那么必须重写抽象类中的抽象方法,不能被static和final修饰,因为抽象方法要被子类继承。
抽象类中不一定包含抽象方法,但是包含抽象方法的一定是抽象类,抽象类之间的相互继承不需要重写抽象方法。
二、接口
接口的定义
接口的定义格式与定义类的格式基本相同,将class关键字换成 interface 关键字,就定义了一个接口。
接口的使用
//接口的定义interface USB { void openDevice(); void closeDevice();}//实现接口class Mouse implements USB { @Override public void openDevice() { System.out.println("打开鼠标"); } @Override public void closeDevice() { System.out.println("关闭鼠标"); } public void click(){ System.out.println("鼠标点击"); }}//实现接口class KeyBoard implements USB{//实现接口中的抽象类 @Override public void openDevice() { System.out.println("打开键盘"); } @Override public void closeDevice() { System.out.println("关闭键盘"); } public void inPut(){ System.out.println("键盘输入"); }}
注意事项
❗ 接口不能够直接使用,必须有一个类来实现接口,并实现接口中的所有抽象方法
❗ 子类和父类之间是extends 继承关系,类与接口之间是 implements 实现关系
接口中每一个方法都是public的抽象方法, 即接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错。
❗ 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现 ,JDK1.8 开始允许有可以实现的方法,但这个方法只能是default 修饰的,类在实现该接口时,不需要重写该默认方法。
具体作用: 当我们进行业务扩展时,需要在接口中新增方法。如果新增的这个方法写成普通方法的话,那么需要在该接口所有的实现类中都重写这个方法。如果新增的方法定义为default类型,就不需要在所有的实现类中全部重写该default方法,哪个实现类需要新增该方法,就在哪个类中进行实现
重写接口中方法时,不能使用default访问权限修饰
接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量
接口中不能有静态代码块和构造方法
接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class
如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
实现多个接口
一个类实现多个接口
interface USB { void openDevice(); void closeDevice();}interface ULine{ void lineInsert();}class Mouse implements USB,ULine{ @Override public void openDevice() { System.out.println("打开鼠标"); } @Override public void closeDevice() { System.out.println("关闭鼠标"); } @Override public void lineInsert() { System.out.println("插入鼠标线"); }}
一个类实现多个接口时,每个接口中的抽象方法都要实现,否则类必须设置为抽象类
一个类继承一个父类,同时实现多个接口
public class TestDemo3 { public static void main(String[] args) { Duck duck = new Duck("yaya"); walk(duck); Brid brid = new Brid("gugu"); walk(brid); } public static void walk(IRunning running) { System.out.println("去散步"); running.run(); }} class Animal { protected String name; public Animal(String name) { this.name = name; }}interface IFlying { void fly();}interface IRunning { void run();}interface ISwimming { void swim();}class Duck extends Animal implements IFlying,IRunning,ISwimming{ public Duck(String name) { super(name); } @Override public void fly() { System.out.println("飞飞飞"); } @Override public void run() { System.out.println("鸭子嘎嘎跑"); } @Override public void swim() { System.out.println("游游游"); }}class Brid extends Animal implements IRunning,ISwimming,IFlying{ public Brid(String name) { super(name); } @Override public void fly() { System.out.println("鸟儿飞"); } @Override public void run() { System.out.println("鸟儿跑"); } @Override public void swim() { System.out.println("鸟儿游"); }}
接口中的多态
public class TestDemo3 { public static void main(String[] args) { Duck duck = new Duck("yaya"); walk(duck); Brid brid = new Brid("gugu"); walk(brid); } public static void walk(IRunning running) { System.out.println("去散步"); running.run(); }}
有了接口之后, 类的使用者就不必关注具体类型,而只关注某个类是否具备某种能力.
接口之间的继承
一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到多继承的目的。接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字
interface IRing { void run();} interface ISing { void swim();} interface IAmphibious extends IRunning, ISwimming {}class Frog implements IAmphibious { @Override public void run() { System.out.println("跑啊跑"); } @Override public void swim() { System.out.println("游啊游"); }}
接口间的继承相当于把多个接口合并在一起.
✅抽象类和接口的区别??
区别 | 抽象类(abstract) | 接口(interface) | |
---|---|---|---|
1 | 结构组成 | 普通类+抽象方法 | 抽象方法+全局变量 |
2 | 权限 | 各种权限 | public |
3 | 子类使用 | 使用extends关键字继承抽象类 | 使用implements关键字实现接口 |
4 | 关系 | 一个抽象类可以实现若干接口 | 接口不能继承抽象类,但是可以使用extends关键字继承多个接口 |
5 | 子类限制 | 一个子类只能继承一个抽象类 | 一个子类可以实现多个接口 |
三、Object类
Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父类。即所有类的对象都可以使用Object的引用进行接收。
public class TestDemo5 { public static void main(String[] args) { function(new Person()); function(new Student()); } public static void function(Object obj){ System.out.println(obj); }}class Person{ private int age; private String name;}class Student{ private int grade; private String sno;}
Object类中提供的一些默认方法
3.1 toString()方法
//Object类中的toString()方法实现:public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode());}
toString()方法一般需要通过重写之后进行使用。
3.2 hashcode()方法
返回对象的hash代码值
源码:
public native int hashCode();
重写hashCode() 方法
class Per{ public String name; public int age; public Per(String name, int age) { this.name = name; this.age = age; } @Override public int hashCode() { return Objects.hash(name, age); }}public class TestDemo6 { public static void main(String[] args) { Per per1 = new Per("gaobo",20); Per per2 = new Per("gaobo", 20); System.out.println(per1.hashCode()); /* 注意事项:哈希值一样。 结论: 1、hashcode方法用来确定对象在内存中存储的位置是否相同 2、事实上hashCode() 在散列表中才有用,在其它情况下没用。 在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。 */ System.out.println(per2.hashCode()); }}
3.3 equals()方法
比较的是地址
// Object类中的equals方法public boolean equals(Object obj){ return (this == obj); // 使用引用中的地址直接来进行比较 }
✅如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的
重写equals()方法
@Override public boolean equals(Object obj) { //判断是否为空 if (obj == null) { return false ; } if(this == obj) { return true ; } // 不是Person类对象 if (!(obj instanceof Per)) { return false ; } Per per = (Per) obj ; // 向下转型,比较属性值 return this.name.equals(per.name) && this.age==per.age ; } /* @Override public boolean equals(Object obj) { Per per = (Per)obj; //String类调用的是自身的equals, // s1跟s2两者比较的规则则是按照String类重写后的equals方法来比较, //很显然,String类的比较规则是按照值来比较的,因此结果会输出true。 if(this.name.equals(per.name)&&this.age == per.age){ return true; } return false; }}*/
编译器自动生成重写的hashcode()和equals()方法
@Overridepublic boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Per per = (Per) o; return age == per.age && Objects.equals(name, per.name); } @Override public int hashCode() { return Objects.hash(name, age); }
在object类中,hashcode()方法是本地方法,返回的是对象的地址值,而object类中的equals()方法比较的也是两个对象的地址值,如果equals()相等,说明两个对象地址值也相等,当然hashcode()也就相等了.**但是hashcode() 相同时,equals()不一定相同**
✅✅重写equals方法时,也必须重写hashcode()方法吗?
答:必须,hashCode 和 equals 两个方法是用来协同判断两个对象是否相等的,采用这种方式的原因是可以提高程序插入和查询的速度,当重写
equals
方法后有必要将hashCode
方法也重写,这样做才能保证不违背hashCode
方法中“相同对象必须有相同哈希值”的约定。
✅✅ == 和 equals 的区别是什么?
答:
对于基本类型和引用类型 == 的作用效果是不同的,如下所示:
基本类型:比较的是值是否相同;
引用类型:比较的是引用是否相同
String x = "string";String y = "string";String z = new String("string");System.out.println(x==y); // trueSystem.out.println(x==z); // falseSystem.out.println(x.equals(y)); // trueSystem.out.println(x.equals(z)); // true
对于equals() 方法,根据源码可以得知 : equals() 的本质上就是true
public boolean equals(Object obj) { return (this == obj);}
所以equals()方法 默认情况下是引用比较,只是很多类重写了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。
四、常用接口
4.1 Comparable接口(比较)
❓ 在学习数组时,Arrays类中的sort方法可以对对象数组进行排序 , 那下面的对象数组能不能用Arrays.sort排序呢?
class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; }}public class test4 { public static void main(String[] args) { Student[] students = new Student[] { new Student("zhangsan", 13), new Student("lisi", 23), new Student("able", 17), }; Arrays.sort(students); System.out.println(Arrays.toString(students)); }}
此时编译器并不知道到底是按姓名还是年龄进行排序,当sort方法对对象所属的类进行排序时,对象所属的类必须实现Comparable接口,通过参考文档可知,Comparable接口中仅有一个抽象方法。
那么我们就可以实现Comparable接口,并实现和重写compareTo方法
class Student implements Comparable{ public int age; public String name; public Student(int age, String name) { this.age = age; this.name = name; } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } //重写compareTo方法 @Override public int compareTo(Student o) { if (this.age - o.age > 0) return 1; else if (this.age - o.age 0) { System.out.println("student > student1"); } else { System.out.println("student < student1"); } }}
此时可以得到按年龄进行排序的结果:
我们知道在Arrays.sort(students); 中是传了一个学生对象数组,在调用Arrays对对象数组排序的时候,其实就调用了我们的Comparable接口中的compareTo方法对数组的每个元素进行了排序和比较,在Java中对于引用数据类型的比较或者排序,一般都要用到使用Comparable接口中的compareTo() 方法
按姓名排序时,重写的compareTo方法
@Override public int compareTo(Student o) { // this.代表对当前对象的引用,o.代表对参数对的引用 if (this.name.compareTo(o.name) > 0)//String类中重写了compareTo方法,可直接使用 return 1; else if (this.name.compareTo(o.name) < 0) return -1; else return 0; } //如果当前对象应排在参数对象之前, 返回小于 0 的数字; //如果当前对象应排在参数对象之后, 返回大于 0 的数字; //如果当前对象和参数对象不分先后, 返回 0;
缺点:一旦重写了comparable()方法,那么就只能对一种参数类型进行比较,把方法写死了,此时就需要使用Comparator接口 ❗
4.2 Comparator接口(比较)
这里是Arrays.sort中只有一个参数的方法
当实现Comparator接口时,可以使用两个参数重载的方法实现排序,包含一个比较器类型的参数
首先通过参考文档了解Comparator接口,我们需要重写的是compare()方法
所以就像Comparable 接口一样,我们只要实现了Comparator接口,并重写Comparator里的compare方法就可以实现对学生对象数组的排序
比如我们上面的年龄比较就可以写成这样
class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "[" + this.name + ":" + this.age + "]"; }} // 实现Comparator接口中的compare方法class AgeComparator implements Comparator { // 年龄比较器 @Override public int compare(Student o1, Student o2) { return o1.age - o2.age; // 反正返回的也是数字,当o1.age>o2.age时返回大于零的数,即o1对象排在o2对象的后面,升序排列,我们之前用Comparable接口时也可以这样简写 }}public class test4 { public static void main(String[] args) { Student[] students = new Student[]{ new Student("zhangsan", 13), new Student("lisi", 23), new Student("able", 17), }; AgeComparator ageComparator = new AgeComparator(); Arrays.sort(students, ageComparator); // 用类Arrays.sort对students数组进行排序,这里传了两个参数(学生对象和所对应的年龄比较器) System.out.println(Arrays.toString(students)); }}
同样,当我们按照姓名进行排序时,也可以使用此接口
class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "[" + this.name + ":" + this.age + "]"; }}class NameComparator implements Comparator { // 姓名比较器 // 实现Comparator接口中的compare方法 @Override public int compare(Student o1, Student o2) { return o1.name.compareTo(o2.name); // 因为name是String类型,也是一个引用类型,也要用到compareTo方法,此时的compareTo方法是String类里重写的方法 }}public class test4 { public static void main(String[] args) { Student[] students = new Student[]{ new Student("zhangsan", 13), new Student("lisi", 23), new Student("able", 17), }; NameComparator nameComparator = new NameComparator(); Arrays.sort(students, nameComparator); System.out.println(Arrays.toString(students)); }}
Comparable接口和Comparator接口都是Java中用来比较和排序引用类型数据的接口,要实现比较,就需要实现他们所各种对应的compareTo方法或者compare方法。
✅Comparator使用起来更加灵活,所以我更倾向于使用比较器:Comparator
4.3 Cloneable接口(拷贝)
对象在内存当中的存储
class Student { public int age = 15; @Override public String toString() { return "Student{" + "id=" + id + '}'; }}public class test3 { public static void main(String[] args) { Student student1 = new Student(); System.out.println(student1); }}
此时如果在堆内存中对student1对象拷贝一份,如果使用
Student student2 = student1;
这只是我们在栈上重新定义了一个引用变量student2,并指向了堆上的student1对象,并没有对我们的student1实现拷贝,改变student2.age会影响student.age 的值。
所以我们需要重写Object类中的clone方法进行克隆,在使用clone方法之前,需要实现Cloneable接口
由源码和参考文档可知,Cloneable是一个空接口即标记接口,如果有其他类继承该接口,说明该类的对象是可以被克隆的。
要克隆的这个对象的类必须实现 Cloneable 接口
类中重写 Object 的 clone() 方法
处理重写clone方法时的异常情况
clone方法需要进行强转(比较特殊,先记住就好)
class Student implements Cloneable{ public int age = 10; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } @Override public String toString() { return "Person{" + "age=" + age + '}'; }}public class Demo2 { public static void main(String[] args) throws CloneNotSupportedException{ Student student = new Student(); Student student2 = (Student)student.clone(); //返回值为Object需要进行强制类型转换 System.out.println(student.age); System.out.println(student2.age); student2.age = 18; System.out.println(student.age); System.out.println(student2.age); }}
此时在内存当中就是这样,student1和student2 中的两个age是相互独立的,student2的age发生改变不会影响student1 的内容。此时我们就成功实现了对象的拷贝
4.4 浅拷贝与深拷贝
浅拷贝
✅根据上边Cloneable接口使用介绍我们已经详细了解了,此时我们提出了一个问题,如果在Student类当中再定义一个引用类型,那么又该如何拷贝呢?
class Teacher{ int number = 20;}class Student implements Cloneable{ public int age = 10; Teacher teacher = new Teacher(); @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } @Override public String toString() { return "Person{" + "age=" + age + '}'; }}public class Demo2 { public static void main(String[] args) throws CloneNotSupportedException{ Student student = new Student(); Student student2 = (Student)student.clone(); //返回值为Object需要进行强制类型转换 System.out.println(student.teacher.number); System.out.println(student2.teacher.number); student.teacher.number = 100; System.out.println(student.teacher.number); System.out.println(student2.teacher.number); }}
此时,student 中teacher的改变也引起了 student2中地址的改变,此种拷贝就好像只拷贝了student.teacher.number 的地址,并未重新复制一块内存出来,此种拷贝就叫做浅拷贝
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝
刚刚我们通过实现Cloneable接口、重写clone方法对Student类实现了拷贝,那么同理我们也可以用这样的办法对Teacher类对象进行拷贝.
class Teacher implements Cloneable{ int number = 20; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}class Student implements Cloneable{ public int age = 10; public Teacher teacher = new Teacher(); @Override protected Object clone() throws CloneNotSupportedException { // 此时我们在进行 “(Student) student.clone();” 操作, // 我们在堆上对student克隆拷贝出来一个新对象,并让引用变量tmp指向新对象 Student tmp = (Student) super.clone(); // 用this.teacher.clone()对引用变量teacher所指向的Teacher类对象进行克隆 tmp.teacher = (Teacher) this.teacher.clone(); return tmp; } @Override public String toString() { return "Person{" + "age=" + age + '}'; }}public class Demo2 { public static void main(String[] args) throws CloneNotSupportedException{ Student student = new Student(); // 此时的student.clone返回Student类对象的引用tmp,student2 就指向了原来tmp所指向的对象 Student student2 = (Student)student.clone(); System.out.println(student.teacher.number); System.out.println(student2.teacher.number); student.teacher.number = 100; System.out.println(student.teacher.number); System.out.println(student2.teacher.number); }}
此时的内存结构图为:
上面的拷贝就把引用变量teacher所指向的Teacher类的对象也在堆中拷贝了一份,这就是深拷贝, 深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
深拷贝:创建一个新对象,然后将当前对象的各种成员属性复制到该新对象,无论该成员属性是值类型的还是引用类型,都复制独立的一份,引用类型也会复制该引用类型所指向的对象。