【JavaSE】抽象类和接口
【JavaSE】抽象类和接口
- 前言:补充知识 —— 利用类和对象,交换两个数字
- 一、抽象类
- 1.1 抽象类是什么?
- 1.2 抽象类特点
- 1.3 抽象类举例
- 1.4 抽象类作用
- 二、接口
- 2.1 接口是什么?
- 2.2 接口的特性
- 2.3 接口的使用规则
- 2.4 类可以实现多个接口
- 2.5 接口之间的继承
- 2.6 常用接口举例(Java自带的)
- 2.6.1 Comparable 接口 和 Comparator 接口(与 比较有关的接口)
- 2.6.2 Clonable 接口
- 2.7 ==扩展 —— 深拷贝和浅拷贝==
- 三、抽象类和接口的区别
- 四、Object 类
- 4.1 所有类的对象都可以使用Object的引用进行接收
- 4.2 toString()方法:用来获取对象信息
- 4.3 equals()方法:用来判断2个对象是否相等
- 4.4 hashcode()方法
前言:补充知识 —— 利用类和对象,交换两个数字
class Myvalue{public int val;
}
public class Test {public static void swap(Myvalue myvalue1,Myvalue myvalue2){int tmp = myvalue1.val;myvalue1.val = myvalue2.val;myvalue2.val = tmp;}public static void main(String[] args) {Myvalue myvalue1 = new Myvalue();myvalue1.val = 10;Myvalue myvalue2 = new Myvalue();myvalue2.val = 20;System.out.println("交换前:"+ myvalue1.val + " " + myvalue2.val);swap(myvalue1,myvalue2);System.out.println("交换后:"+ myvalue1.val + " " + myvalue2.val);}
}
一、抽象类
1.1 抽象类是什么?
抽象类定义:如果一个类不能描述一个具体的对象的时候,我们就把它称为对象类。
1.2 抽象类特点
1) 抽象类,一般使用关键字abstract 来修饰
2 )抽象类,不能通过new关键字来实例化对象
3 )抽象类中,如果一个方法 没有具体的实现,那么就把这个方法修改为abstract修饰。
4 )如果一个类有抽象方法,那么该类必须是抽象类;相反,如果一个类是抽象类,那么可以没有抽象方法。
5 )如果一个类继承了这个抽象类,那么必须重写这个抽象类当中的抽象方法。
6) 如果一个类 继承了这个抽象类,但是不想重写抽象类当中的方法,只能把当前这个类也设置为抽象类。 但是需要注意的是,这些抽象方法最终都必须被重写! 只要有其他的普通类继承这些抽象类,都会被重写。
7 )抽象类和普通类的区别
几乎没区别,抽象类和普通类都可以定义成员变量和构造方法。
(1)抽象类不能实例化,普通类可以实例化。
(2)只是抽象类可以定义抽象方法。
8 ) 抽象方法不能是 private 的,抽象方法不能被final和static修饰,因为抽象方法要被子类重写。
1.3 抽象类举例
画图案
1.4 抽象类作用
抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类,然后让子类重写抽象类中的抽象方法。
那么我们思考一个问题:如果是这样的话,普通的类也可以被继承 普通的方法也可以被重写, 为什么要用非得用抽象类和抽象方法呢?
确实如此, 但是使用抽象类相当于多了一重编译器的校验。
使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成。
那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的,但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题。
二、接口
2.1 接口是什么?
在现实生活中,接口的例子比比皆是,比如:笔记本上的USB口,电源插座等。
如上图所示,电脑的USB口上,可以插:U盘、鼠标、键盘…所有符合USB协议的设备。
通过上述例子可以看出:接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。
在Java中,接口可以看成是:多个类的公共规范,是一种引用数据类型。
2.2 接口的特性
1)接口使用 interface 关键字来定义
2)接口不能被实例化
3)接口中定义的成员变量,默认是public static final修饰的,不写的时候也是这样。
4)接口当中的方法,如果要有具体的实现,只能被 static 或者 default 修饰
5)接口当中的方法,如果没有具体的实现,那么就写为抽象方法,即 public abstract 修饰的方法,此时这个方法默认就是 public abstract 。
6)一个类 和 接口的关系:使用 implements 实现,
7)实现该接口以后,就要重写该接口当中的抽象方法
8)在main方法中调用接口IShape
输出结果为:
2.3 接口的使用规则
接口不能直接使用,必须要有一个"实现类"来"实现"该接口,实现接口中的所有抽象方法。
请实现笔记本电脑使用USB鼠标、USB键盘的例子
- USB接口:包含打开设备、关闭设备功能
- 笔记本类:包含开机功能、关机功能、使用USB设备功能
- 鼠标类:实现USB接口,并具备点击功能
- 键盘类:实现USB接口,并具备输入功能
// USB接口
// 鼠标类,实现USB接口
// 键盘类,实现USB接口
// 笔记本类:使用USB设备
// 测试类:
如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
2.4 类可以实现多个接口
- Animal 类
- Dog类
-
Bird类
-
IFlyable接口
- IRunnable接口
-
ISwimmable接口
问题1:能不能把刚刚写的 3个接口 改为类,把这些功能写到类里面?
答:不可以,因为Java只能单继承,不能多继承。(不能继承多个类,但是可以实现多个接口)
所以,接口的出现,可以实现多继承。
问题2:不定义IFlyable接口,在Bird类写fly()方法 与 定义IFlyable接口,Bird类实现接口,重写fly()方法的区别?(接口的好处是什么?)
答:如果有更多的类,不用每个类里单独实现fly()方法,可以实现代码复用。
(以新建的Duck类为例)
- TestDemo类
对于接口来说,有几个接口,参数就能换几次
- 新建Duck类
- TestDemo类中利用接口,实现多态
接口 和 定义方法 的区别:
有了接口之后, 让程序员忘记类型。类的使用者就不必关注具体类型,而只关注某个类是否具备某种能力。
2.5 接口之间的继承
接口间的继承相当于把多个接口合并在一起。
2.6 常用接口举例(Java自带的)
2.6.1 Comparable 接口 和 Comparator 接口(与 比较有关的接口)
- 实例 1:比较两个学生的成绩大小
通过大于号 小于号进行比较的时候,此时不能直接比较引用类型
编译器 要求:给出一个比较大小的规则(可以使用接口)
1)通过 Comparable接口 ,给出比较规则:Comparable 接口是 默认自定义接口
- Student 类
- 测试类Test
- 输出结果
2)Comparator 接口:需要自己定义比较器,对类的侵入性比较低
但是上面的比较,只能用年龄比较。如果需要按照成绩比较,就不能直接在重写的compareTo方法上更改!
因此我们使用Comparator 接口,自己定义比较器 进行比较
-
Student 类
-
AgeComparator 接口
-
ScoreComparator 接口
-
NameComparator 接口
-
测试类Test
-
输出结果
- 实例 2:对自定义类型数组,进行排序
(1) 对自定义类型数组,进行排序(利用Comparable 接口)
-
Student 类
-
Test 测试类
- 运行Test,输出结果:
- 注释掉,Comparable接口 和 重写的compareTo方法
- 再次运行Test,报错:类型转换异常
利用 Arrays.sort(students);对自定义类型进行排序,一定会涉及两个对象之间的比较,需要一些比较规则(比如上述中的 Comparable 接口)
这就是实现这些接口的重要性,尤其是自定义类型一定能比较(利用Comparable接口 或者 自定义比较器 )
(2) 对自定义类型数组,进行排序( 自己实现Arrays.sort() )
- 利用 默认的Comparable 接口
-
Student 类
-
Test 测试类
-
运行Test,输出结果:
学生按照年龄排序,谁小,谁在前。
那我们如果想实现:依然按照年龄排序,但 谁年龄大谁在前呢?
- 方式1:修改 自定义的bubbleSort() 逻辑,其余的不变
输出结果:
- 方式2:修改 重写的compareTo() 方法,其余的不变
输出结果:
但默认的Comparable 接口只能按照年龄比较大小,如果我们需要按照 分数、 姓名比较大小呢?
那么就需要用到我们之前自定义的Comparator 比较器了(ScoreComparator / NameComparator / )
- 利用 自定义的Comparator 比较器:按照分数比较
输出结果:
- Arrays.sort()在实现排序的时候,存在优先级
Arrays.sort()在实现排序的时候,存在优先级:
第一优先级,是根据当前类 实现的 comparable接口,进行排序。
但是,一旦传入了想要的比较方式,就按照传入的比较器 进行比较。
- 同理可得,根据 姓名进行比较大小
输出结果:
2.6.2 Clonable 接口
Java 中内置了一些很有用的接口,Clonable 就是其中之一。
Object 类中存在一个 clone 方法,调用这个方法可以创建一个对象的 “拷贝”。
但要想合法调用 clone方法, 必须要 先实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常。
1.Clonable 接口,实例1
- Person 类
-
Test 类 :将person1这个引用 所指的对象,利用clone方法 克隆一份(但报错)
-
之前学到过,所有的类 都默认继承 父类Object
被 protected 修饰:如果是不同包(clone方法在java.lang包,Test类在demo2包下),只能在子类当中访问(可以在Person类和Test类 访问父类的clone方法)。
上面的报错,是因为:
虽然两个类都继承于Object类(java.lang包下),但是克隆的时候,不是在Person (demo包下)中访问,而在Test类(demo包下)通过person1这个引用(是Person类下)在调用,所以无法调用到clone方法。
- 所以,需要在Person类中重写 clone方法。
间接访问:Person类中重写 clone方法
-
克隆的3个注意事项:
- protected 关键字:
- 向下转型
- 必须实现克隆接口
2.Clonable 接口,实例 2
- Person 类
- Test 类
输出结果:
2.7 扩展 —— 深拷贝和浅拷贝
1. 浅拷贝:Cloneable 拷贝出的对象是一份 “浅拷贝”
- Person 类:新增加成员变量m
- Test 类中,修改 person2.m.money = 99.5
发现修改 person2.m.money = 99.5,把 person1.m.money 也变成了 99.5。
结论:
通过clone,我们只是拷贝了Person对象。但是Person对象中的Money对象,并没有拷贝。
通过person2这个引用修改了m的值后,person1这个引用访问m的时候,值也发生了改变。这里就是发生了浅拷贝。
那么如何实现深拷贝呢?
2. 深拷贝
-
Person 类
-
Test 类
输出结果:
3. 深拷贝与浅拷贝 图解
- 浅拷贝图解
- 深拷贝图解
- 深拷贝 代码解析:
三、抽象类和接口的区别
抽象类和接口都是 Java 中多态的常见使用方式.。
区别 | 抽象类 | 接口 | |
---|---|---|---|
(1) | 结构组成 | 抽象类 + 抽象方法 | 抽象方法 + 全局常量 |
(2) | 权限 | 不限 | public |
(3) | 子类使用 | 使用extends关键字继承抽象类 | 使用implements关键字实现接口 |
(4) | 关系 | 一个抽象类可以实现若干个接口 | 接口不能继承抽象类,但是接口可以使用extends关键字继承多个父借口 |
(5) | 子类限制 | 一个子类只能继承一个抽象类 | 一个子类可以实现多个接口 |
具体解析:
// 抽象类
abstract class Animal {protected String name; // 普通字段public static final int LEGS = 4; // 静态常量
}// 接口
interface Runnable {int MAX_SPEED = 100; // 默认 public static final// String name; // 错误!不能有普通字段
}
// 抽象类
abstract class Animal {public void eat() { // 普通方法System.out.println("Eating...");}public abstract void makeSound(); // 抽象方法
}// 接口(Java8前)
interface Runnable {void run(); // 默认 public abstract// void stop() {} // 错误!不能有实现
}
// 抽象类和接口都不能直接实例化
Animal a = new Animal(); // 错误!
Runnable r = new Runnable(); // 错误!// 只能通过子类/实现类实例化
Animal cat = new Cat(); // 假设Cat继承Animal
Runnable rabbit = new Rabbit(); // 假设Rabbit实现Runnable
举例:
interface Walkable {default void walk() { // 默认方法System.out.println("Walking...");}static void printInfo() { // 静态方法System.out.println("Walkable interface");}
}
四、Object 类
Object是Java默认提供的一个类。
Java里面除了Object类,所有的类都是存在继承关系的,默认会继承Object父类。
即所有类的对象都可以使用Object的引用进行接收。
4.1 所有类的对象都可以使用Object的引用进行接收
Object 类,实例1:使用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);}
}//执行结果:
Person@1b6d3586
Student@4554617c
所以在开发之中,Object类是参数的最高统一类型。
但是Object类也存在有定义好的一些方法。
4.2 toString()方法:用来获取对象信息
如果要打印对象中的内容,可以直接重写Object类中的toString()方法。
// Object类中的toString()方法实现:
public String toString() {return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
4.3 equals()方法:用来判断2个对象是否相等
在Java中,进行比较时:( 区别 == 与equals )
a.如果“==”左右两侧是基本类型变量,比较的是变量中值是否相同
b.如果“==”左右两侧是引用类型变量,比较的是引用变量地址是否相同
c.如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的:
区别于comparable接口 和 comparator接口:
4.4 hashcode()方法
- 回忆刚刚的toString方法的源码:
public String toString() {return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
我们看到了hashCode()这个方法,他帮我算了一个具体的对象位置,这里面涉及数据结构,可以说它是个内存地址。然后调用Integer.toHexString()方法,将这个地址以16进制输出。
- hashcode方法源码:
public native int hashCode();
该方法是一个native方法,底层是由C/C++代码写的。
我们认为两个名字相同,年龄相同的对象,将存储在同一个位置,如果不重写hashcode()方法,我们可以来看示例代码,可以发现两个对象的hash值不一样:
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("zhangsan", 20) ;Person per2 = new Person("zhangsan", 20) ;System.out.println(per1.hashCode());System.out.println(per2.hashCode());}
}
//执行结果
460141958
1163157884
- 像重写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() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。