🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
目录
- 1.抽象类与接口
- 1.1 抽象类
- 1.1.1抽象类的概念
- 1.1.2抽象类的语法
- 1.1.3抽象类的特性
- 1.1.4 抽象类的作用
- 1.2 接口
- 1.2.1接口的概念
- 1.2.2语法规则
- 1.2.3 接口的使用
- 1.2.4接口特性
- 1.2.5 实现多个接口
- 1.2.6 接口之间的继承
- 1.2.7 接口的经典使用案例
- 1.2.8抽象类与接口的区别
- 1.3 拷贝与Object类
- 1.3.1 Object类
- 1.3.1.1 概述
- 1.3.1.2 toString方法
- 1.3.1.3 对象比较equals方法
- 1.3.1.4 hashcode方法
- 1.3.2 Clonable接口与深拷贝浅拷贝
1.抽象类与接口
1.1 抽象类
1.1.1抽象类的概念
在面向对象的概念中,所有的对象都是通过类来描述的,但是有时候一些类并不可以清晰地刻画一个对象,没有包含足够的信息来描绘一个具体的对象的类,那么这样的类就叫做抽象类,比如说:有一个类,这个类是一个图形,但是仅凭借这些信息,你现在完全不会在纸上描绘出一个图形,因为你不知道这个图形是一个什么样的图形,图形包括好多,有正方形,三角形等等
从上面的信息,我们不难看出以下几点:
- 这些具体的图形与抽象的图形是继承关系
- 如果抽象图形中有一个画图形的方法,这个方法无法具体实现
- 子类中的画图形方法可以实现
在这个例子中,我们令抽象图形类为Shape,令画图形的方法为draw,则Shape类为抽象类,其中的draw方法不能实现任何的具体功能,是一个没有实际功能的方法,我们称这样的方法为抽象方法
1.1.2抽象类的语法
在Java中,如果已给类被abstract修饰,我们称这个类为抽象类,抽象类中被abstract修饰的方法称为抽象方法,抽象方法不用给出具体的实现,我们拿上面的Shape类来举例:
//抽象类被abstract修饰
public abstract class Shape {
protected double area;//抽象类中也可以包含普通成员变量
public abstract void draw();//抽象类的方法中必须也是abstract修饰
abstract void area();
public double getArea(){//普通成员方法
return area;
}
}
注意:抽象类也是类,内部也可以包含普通方法和普通成员变量,甚至可以是构造方法
抽象类与普通类的不同于相同
- 都可以包含普通成员变量与成员方法
- 抽象类比普通类多了抽象方法和抽象
- 抽象类不可以被实例化
1.1.3抽象类的特性
- 抽象类不可以直接实例化对象
- 抽象方法不可以是private,static,final修饰的,因为它必须被子类重写
- 抽象类必须被继承,并且在继承之后必须重写抽象方法,否则子类也需要用abstract修饰,即子类也是一个抽象类,类似于后面接口的继承,我们之后会聊到.
我们下面拿一个具体的例子来说明:
public abstract class Shape {
protected double area;//抽象类中也可以包含普通成员变量
public abstract void draw();//抽象类的方法中必须也是abstract修饰
abstract void area();
public double getArea(){//普通成员方法
return area;
}
}
public class Rect extends Shape {
private double wide;//宽
private double length;//长
private double area;//面积
public Rect(double wide, double length) {
this.wide = wide;
this.length = length;
}
public void area() {//重写父类area方法,计算面积
this.area = wide * length;
}
public void draw(){//重写父类draw方法,画图形
System.out.println("画一个矩形");
}
}
- 抽象类中不一定含有抽象方法,但是抽象方法一定包含在抽象类中
- 抽象类中可以有构造方法,供子类创建对象时,初始化父类成员变量,注意并不是用来实例化时调用,因为它就不可以被实例化
1.1.4 抽象类的作用
抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.
有些同学可能会说了, 普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?
确实如此. 但是使用抽象类相当于多了一重编译器的校验.
使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了, 使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.
很多语法存在的意义都是为了 “预防出错”, 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们.
充分利用编译器的校验, 在实际开发中是非常有意义的.
1.2 接口
1.2.1接口的概念
在我们现实生活中,我们到处可以见到各种各样的接口,比如:笔记本电脑上的USB接口
电源插座等
电脑上的USB接口可以插:键盘,鼠标,u盘等所有符合USB协议的设备
插座上可以插电源:电饭煲,电脑,电视机等所有符合用电规范的设备
通过上述例子可以看出:接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。
在Java中,接口可以看做:多个类公共的规范,是一种引用类型的数据
1.2.2语法规则
接口的定义格式与定义类的格式基本相同,将class关键字替换为interface关键字,就定义了一个接口
public interface Itest{
// 抽象方法
public abstract void method1(); // public abstract 是固定搭配,可以不写
public void method2();
abstract void method3();
void method4();
// 注意:在接口中上述写法都是抽象方法,跟推荐方式4,代码更简洁
}
注意:创建接口时,接口的名称一般用大写字母I开头
虽然以上代码中加上一些abstract,public等的一些修饰符不会出现语法错误,但是我们嫩为了保证代码的简洁性,我们建议接口中的方法前面不要加任何修饰符
1.2.3 接口的使用
接口不可以直接使用,必须有一个“实现类”来实现该接口,类与接口之间是implements的关系,这也是一个类实现一个接口的关键字,就和上面一个类继承一个抽象类道理是相同的,语法格式如下:
public class 类名 implements 接口名
注意:子类和父类是extends关系,接口和类是implements关系
下面我们来具体举一个例子,我们拿USB接口来说事:
//USB接口
public interface USB {
void openService();
void closeService();
}
public class Mouse implements USB{
@Override
public void openService() {
System.out.println("插入鼠标");
}
@Override
public void closeService() {
System.out.println("拔出鼠标");
}
public void click(){
System.out.println("点击鼠标");
}
}
public class KeyBoard implements USB{
@Override
public void openService() {
System.out.println("插入键盘");
}
@Override
public void closeService() {
System.out.println("拔出键盘");
}
public void inPut(){
System.out.println("打字~");
}
}
1.2.4接口特性
-
接口是一种引用类型,但是不可以被实例化
-
接口中每一个方法都是public修饰,即每一个方法都会被隐式地定义为public abstract,若添加其他修饰符,都会报错
-
接口中的方法是不可以在接口中实现的,只能由类来实现
-
重写接口的方法时,不可以使用比public修饰符权限更低的修饰符来重写方法,这和继承体系中的重写是一样的规则
-
接口中可以含有变量,但是接口中的变量会被隐式的定义为public abstract final修饰
-
接口中不可以含有任何的代码块和构造方法,但是抽象类可以有
-
接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class
-
如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
-
jdk8中:接口中还可以包含default方法
1.2.5 实现多个接口
在Java语言中,类和类之间是单继承关系,一个类只可以有一个父类,即Java中不支持多继承关系,但是一个类可以实现多个接口,下面通过Animal类来具体说明
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 Frog extends Animal implements IRunning, ISwimming {//可以实现多个接口
public Frog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在往前跳");
}
@Override
public void swim() {
System.out.println(this.name + "正在蹬腿游泳");
}
}
注意:在一个类实现多接口的时候,每一个接口的方法都必须被重写,我们在idea编译器中,可以使用Ctrl+i快速实现接口
再比如鸭子,既会跑,又会游泳,又会飞
class Duck extends Animal implements IRunning, ISwimming, IFlying {
public Duck(String name) {
super(name);
}
@Override
public void fly() {
System.out.println(this.name + "正在用翅膀飞");
}
@Override
public void run() {
System.out.println(this.name + "正在用两条腿跑");
}
@Override
public void swim() {
System.out.println(this.name + "正在漂在水上");
}
}
上面展示了Java中面相对象中最常见的一种用法:一个类继承一个父类,同时实现多个接口
继承表达的含义是is a,即一个类属于什么大类,接口则表示的是can xxx,即一个类具有什么样的特性或能力
这样设计的好处在哪里呢?时刻牢记多态的好处,它可以让程序员忘记类型,有了接口之后,就不必关心具体的类型,而是关注某个类是否具有某种能力
面试题:什么时候会发生多态
在不同类继承一个父类,并且重写了父类中的方法,并且通过父类引用创建的子类对象调用了重写的方法,此时会发生动态绑定并发生向上转型,此时就会发生多态
只要这个类的东西具有某种能力,就可以实现一个接口,比如实现上述接口的类不一定必须是动物类,还可以是其他类,比如机器人也会跑
class Robot implements IRunning {
private String name;
public Robot(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(this.name + "正在用轮子跑");
}
}
1.2.6 接口之间的继承
在Java中,类和类之间是单继承关系,一个类可以实现多个接口,接口与接口之间可以多继承,即接口可以达到多继承的目的
接口可以继承一个接口,达到复用的效果,使用extends关键字
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {
}
class Frog implements IAmphibious {
}
通过一个创建一个新接口IAmphibious,并继承了IRunning, ISwimming,来达到合并两个接口的目的,在Frog类中,还需要继续重写run方法和swim方法
1.2.7 接口的经典使用案例
- 给对象数组排序
如果我们有一个数组,这个数组中的数据类型是学生类
public class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", score=" + score +
'}';
}
}
public class Main {
public static void main(String[] args) {
Student[] student = new Student[]{
new Student("zhangsan",95),
new Student("lisi",89),
new Student("wangwu",88),
new Student("zhaoliu",98),
};
}
}
比如我们在这里要对这个数组中的学生按照成绩进行排序,数组中我们有sort方法,能否直接使用这个方法呢?
在这里我们可以看出,编译器在这个地方抛出了异常,为什么呢,是因为一个数组中的对象有两个成员,一个是姓名,一个是成绩,编译器并不知道你要通过哪个成员来比较对象,所以我们这里引入了Comparable接口,通过重写该接口中的compareTo方法来实现对象的比较
public class Student implements Comparable {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", score=" + score +
'}';
}
@Override
public int compareTo(Object o) {//重写compareTo方法
Student s = (Student) o;//object类强转为Student类
if (this.score > s.score){
return -1;
}else if (this.score<s.score){
return 1;
}else {
return 0;
}
}
}
在sort方法中,会自动调用重写的compareTo方法
然后比较当前对象和参数对象的大小关系(按分数来算).
如果当前对象应排在参数对象之前, 返回小于 0 的数字;
如果当前对象应排在参数对象之后, 返回大于 0 的数字;
如果当前对象和参数对象不分先后, 返回 0;
之后的运行就符合预期了
注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则.
在这里我们给出几种更加灵活的方法,利用比较器,即实现Compartor接口
public class nameCompare implements Comparator<Student> {//比较什么,尖括号里就写什么
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
}
public class sorceCompare implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
if (o1.score > o2.score){
return -1;
}else if (o1.score<o2.score){
return 1;
}else {
return 0;
}
}
}
public class Main {
public static void main(String[] args) {
Student[] student = new Student[]{
new Student("zhangsan",95),
new Student("lisi",89),
new Student("wangwu",88),
new Student("zhaoliu",98),
};
Arrays.sort(student);
System.out.println(Arrays.toString(student));
nameCompare comparator1 = new nameCompare();
sorceCompare comparator2 = new sorceCompare();
System.out.println(comparator1.compare(student[0],student[1]));
System.out.println(comparator2.compare(student[0],student[1]));
}
}
在这里我们可以看出,与Comparable接口不同的是,它不是在想要比较的类上直接添加,而是单独创建了一个类并创建对象,可以根据成绩比较,可以根据名字比较,返回的是一个整数,若我们想利用比较器对数组进行排序,可不可以实现呢,当然可以,我们在上述代码的基础上稍作改动
public class Main {
public static void main(String[] args) {
Student[] student = new Student[]{
new Student("zhangsan",95),
new Student("lisi",89),
new Student("wangwu",88),
new Student("zhaoliu",98),
};
Arrays.sort(student);
System.out.println(Arrays.toString(student));
nameCompare comparator1 = new nameCompare();
sorceCompare comparator2 = new sorceCompare();
System.out.println(comparator1.compare(student[0],student[1]));
System.out.println(comparator2.compare(student[0],student[1]));
Arrays.sort(student,comparator1)//传入比较器
System.out.println(Arrays.toString(student));
}
}
在数组类的sort方法中,我们传入了一个关于名字的比较器,是sort的一个重载方法,那么运行结果就会根据名字字母的先后顺序进行排序,运行结果如下
在这里我们给出重载sort方法的源码,方便大家理解
public static <T> void sort(T[] a, Comparator<? super T> c) {//在这里我们可以看到该方法有比较器的形式参数
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
1.2.8抽象类与接口的区别
抽象类和接口都是 Java 中多态的常见使用方式. 都需要重点掌握. 同时又要认清两者的区别(重要!!! 常见面试题).
核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法.
如之前写的 Animal 例子. 此处的 Animal 中包含一个 name 这样的属性, 这个属性在任何子类中都是存在的. 因此此处的 Animal 只能作为一个抽象类, 而不应该成为一个接口.
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
1.3 拷贝与Object类
1.3.1 Object类
1.3.1.1 概述
Object类是Java默认提供的一个类,在Java里面除了Object类,与所有的类都存在继承关系,他们都会默认继承Object类,即所有的对象都可以用Object类来接收,它可以向上转所有类型的对象
class Person{}
class Student{}
public class Test{
public static void main(String[] args){
func(new Person());
func(new Student());
}
public static void func(Object obj){
System.out.println(obj);
}
}
问题:
如果所有类的默认继承了Object类,那么一个类在手动继承了一个类,就相当于继承了两个类,那么为什么不报错呢其实并不是这样的原理,是手动继承的父类已经继承了Object类,子类再去手动继承父类就相当于多层继承,这种继承方式是允许的
在开发的过程中,Object类是最高统一的类型,是所有类型的父类,但是Object类也有一些定义好的方法,我么来通过jdk1.8帮助手册来查看:
现阶段我们只需要掌握用记号笔画注的几个方法,Object类的方法时需要全部掌握的,其他的方法我们在后面再慢慢接触
1.3.1.2 toString方法
如果要打印对象中的内容,可以直接重写Object类中的toString方法,之前我们用过好几次了
object中的toString:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
学生类中重写:
public class Student implements Comparable {
public String name;
public int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", score=" + score +
'}';
}
}
这是之前Student类的例子
重写方法可以用alt+insert来快速实现
1.3.1.3 对象比较equals方法
- 在Java中,如果“==”两边是基础类型,那么可以直接比较他们的数值是否相等
- 如果“==”两边是引用类型,那么比较的是他们的地址是否相同
- 如果我们要比较对象中的内容是否相同,我们则需要重写equals方法
Object中的equals方法:
public boolean equals(Object obj) {
return (this == obj);//使用引用中的地址直接进行比较,只看它们是不是同一个引用
}
public class Student {
public String name;
public int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", score=" + score +
'}';
}
@Override
public boolean equals(Object obj) {
if (obj == null){//传入null
return false;
} else if (this == obj) {//同一个引用
return true;
}
if (!(obj instanceof Student)){//若传入的对象不是学生类
return false;
}
Student stu = (Student) obj;//把obj对象向下转回来,以便比较
return stu.name.equals(this.name)&&stu.score == this.score;//String类重写了equals方法,可直接比较字符串是否相同
}
}
下面是String类中重写的equals方法:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
return (anObject instanceof String aString)
&& (!COMPACT_STRINGS || this.coder == aString.coder)
&& StringLatin1.equals(value, aString.value);
}
总结:比较引用内容是否相同,需要重写equals方法
1.3.1.4 hashcode方法
hashcode()这个方法,可以理解为它为你算出了一个对象的具体位置,目前可以理解为地址,其实这牵扯到了数据结构中的hash表,关键字和hashcode形成了某种映射关系,我们讲到后面再细说
hashcode的源码:
public native int hashCode();
这个方法时native方法,底层是c/c++代码实现的,所以我们看不到
我们还是以学生类来举例,若我们不重写hashcode方法
public class Main {
public static void main(String[] args) {
Student[] student = new Student[]{
new Student("zhangsan",95),
new Student("zhangsan",95)
};
System.out.println(student[0].hashCode());
System.out.println(student[1].hashCode());
}
}
在这里我们可以看到,这两个值是完全不同的两个值,虽然他们的成绩和名字是一样的
现在我们重写hashcode方法
public class Student{
public String name;
public int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public int hashCode() {
return Objects.hash(name, score);
}
}
public class Main {
public static void main(String[] args) {
Student[] student = new Student[]{
new Student("zhangsan",95),
new Student("zhangsan",95)
};
System.out.println(student[0].hashCode());
System.out.println(student[1].hashCode());
}
}
现在我们发现两个对象的hashcode值是一样的
在我们重写equals和hashcode方法的时候仍然可以通过alt+insert方法快速生成
1.3.2 Clonable接口与深拷贝浅拷贝
Java中内置了一些很有用的接口,上面的Comperable接口就是其中之一,接下来我们就介绍的Clonable接口也是其中之一
在object类中有一个clone方法,调用这个方法可以对对象进行拷贝,但是这个方法不可以直接使用,我们必须在被拷贝数据类型的后面实现Clonable接口,否者会抛出CloneNotSupportedException的异常
现在我们看一下Clonable接口的源码:
public interface Cloneable {
}
在这里我们惊奇地发现这个接口是一个空接口,这里我们需要解释以下,Clonable接口在这里只起到标记的作用,证明这个类是可以被拷贝的,我们称它为标记接口
接下来我们便可以用clone方法实现一个浅拷贝:
public class Money {
public double money = 99.9;
}
public class Student implements Cloneable{
public String name;
public int score;
public Money m;
public Student(String name, int score) {
this.name = name;
this.score = score;
this.m = new Money();//在该类中创建一个对象,否则为null
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", score=" + score +
", m=" + m.money +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {//在主函数中要对异常进行声明,否者在调用clone方法时会抛出之前的异常
Student s1 = new Student("zhangsan",99);
Student s2 = (Student) s1.clone();//clone一个学生出来
System.out.println(s1);
System.out.println(s2);
}
}
运行结果如下:
如今我们对s2的m进行修改:
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student("zhangsan",99);
Student s2 = (Student) s1.clone();
System.out.println(s1);
System.out.println(s2);
s2.m.money = 88.8;
System.out.println(s1);
System.out.println(s2);
}
}
运行结果如下:
我们发现,s1的m也被修改了,为什么呢,我们通过图来解释:
从上述的图中可以看出,m对象没有进行拷贝,两个引用都是指向了同一个对象,上述代码只是进行了浅拷贝,并没有对m进行拷贝,所以在修改s2的m时必定会把s1的m也修改掉,下面我们对s1进行深拷贝
public class Money implements Cloneable{
public double money = 99.9;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();//把Money类也设置为客克隆的类,并提供重写方法
}
}
public class Student implements Cloneable{
public String name;
public int score;
public Money m;
public Student(String name, int score) {
this.name = name;
this.score = score;
this.m = new Money();//在该类中创建一个对象,否则为null
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", score=" + score +
", m=" + m.money +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
Student tmp = (Student) super.clone();//创建一个临时变量,把studen拷贝过来
tmp.m = (Money) this.m.clone();//把临时变量中的m对象另外拷贝一份
return tmp;//返回临时变量
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student("zhangsan",99);
Student s2 = (Student) s1.clone();
System.out.println(s1);
System.out.println(s2);
s2.m.money = 88.8;
System.out.println(s1);
System.out.println(s2);
}
}
运行结果如下:
从这里我们可以看出,s1的m值没有被改变,接下来我们画图解释原理:
从上述图中我们可以看出,m也进行了靠北,所以我们便完成了深拷贝