抽象类
什么是抽象类?
定义:[拥有抽象方法的类或者说采用 abstract 关键字修饰的类]{.red}
// 定义的抽象类
public abstract class AbstractClass
{
// 定义的抽象方法
public abstract void abstractMethod();
}特点:
[抽象类中并非所有方法都是抽象的]{.red}:
- [每个抽象类可以同时拥有具体实现方法和抽象方法]{.blue}
- [每个继承抽象类的子类必须实现所有的抽象方法]{.blue}
[抽象方法的访问权限不可以是私有的、也不可以是静态或者不可变的]{.red}:这几种情况都会导致子类无法重写或者说实现抽象方法
[抽象类中可以存在任意访问权限的成员变量]{.red}:拥有成员变量也就意味着拥有构造方法
[不可以利用 new 关键字直接创建抽象类实例]{.red}
// 这种创建方式是错误的
AbstractClass object = new AbstractClass();
// 只能够将抽象类的引用指向具体的实现类来创建:SubClass 继承实现了抽象类
AbstractClass object = new SubClass();
为什么需要提供抽象类?为什么不直接提供一个实现了方法的父类呢?
可以将不同的类中拥有相同的变量和方法抽取出来成为抽象类的属性,任何类需要拥有这些属性时只需要继承抽象类就行,[便于代码的继续进行扩展]{.blue}
设计系统架构时很难得知最终如何去实现这些功能,抽象类就能很好地帮助系统的架构设计,不需要提供任何实现,仅考虑需要提供什么,有利于程序架构设计以及之后的重构
普通父类中的所有方法都必须实现,之前提到的在设计系统架构时是很难得知最终如何实现这个方法
:::info
总结:抽象类除了拥有抽象方法之外,其余和普通的类没有什么区别
:::
接口
概述
定义:[没有具体实现和成员变量,而只是描述类应该完成的需求,没有具体的实现]{.red}
特点:
[接口中所有方法都没有具体实现,都是公共的抽象方法]{.red}:实现类必须实现所有抽象方法
Java 8
新特性中接口也可以拥有实现的方法[接口和接口之间可以继承,类可以实现多个接口]{.red}
接口继承接口采用的实现
extends
关键字而不是implements
[接口中默认所有变量都是公共的静态常量]{.red}
不能有成员变量,也不能有非公共的变量(
protected
、private
都不可以修饰接口中的变量)接口不能采用
new
创建对象仅可以创建接口引用,指向各个实现接口的类[接口中可以嵌套定义接口,类中也可以嵌套定义接口,接口中也可以嵌套定义内部类(默认为静态内部类)]{.blue}
interface Walk{ // 接口的定义
void walk();
}
interface Run extends Walk{ // 接口继承接口
void run();
}
class Animal implements Run, Comparator<Animal>{ // 类实现接口,多接口
public void walk(){
}
public void run(){
}
}
/*初始化接口*/
List<String> list = new ArrayList<>();
新特性:(
Java 8
后新增加的特性)[接口中可以存在静态方法]{.red}
通常仍然将这些方法放入伴生的工具类中(
Collection & Collections
前者仍然只是提供抽象方法,后者提供具体方法)接口中可以存在默认的实现方法:采用
default
关键字修饰;实现类可以重写默认方法可以不重写注:这两个新特性实际上是有违接口的设计初衷
default int defaultMethod(){
return -1;
}
细节:
+++ 接口中定义一个默认实现方法,抽象类中也定义一个具有相同名称的实现方法,那么实现接口同时继承抽象类的类会选择哪个方法呢?
[解决方式:抽象类中的方法将会覆盖其余接口中所有的同名方法,父类优先级最高]{.red}
+++
+++ 两个接口同时定义一个具有相同名称的默认实现方法,那么同时实现两个接口的实现类会选择哪个方法呢?
[解决方式:编译器无法识别实现类到底继承哪个方法,需要实现类 重写该方法 从而覆盖两个接口的默认实现]{.red}
+++
+++ 一个接口中定义了一个默认的实现方法,另一个接口定义了一个同名的抽象方法,那么同时实现两个接口的类会继承默认的实现方法吗?
[解决方式:编译器依然直接报错,仍然需要重写,
Java
强调一致性,这种二义性依然需要程序员处理]{.red}+++
+++ 两个接口同时定义了一个具有相同名称的抽象方法,那么同时实现两个接口的实现类不会有任何问题,只需要实现就行了
+++
+++ 抽象类实现接口,子类继承抽象类
- 抽象类可以选择实现接口中的方法,也可以选择不实现交给继承的子类实现
- 子类必须实现没有被抽象类实现的方法,也可以选择覆写抽象类实现的方法
+++
常用接口
比较接口
Comparable<T>
:该接口仅提供一个方法定义:[实现该接口的类的实例对象是可以相互进行比较的]{.blue}
规则:[泛型接口中传入的类必须是实现该接口的类]{.red}
细节:(1)
java.util
包下的方法 (2) 函数式接口/*接口定义*/
public interface Comparable<T>{
public int compareTo(T o);
}
/*例子*/
class Person implements Comparable<Person>{
private int age;
... // 提供构造方法初始化成员变量
public int compareTo(Person person){
// 根据对象的年龄进行比较
return this.age > person.age ? 1 : (this.age == person.age ? 0 : -1);
}
}
public static void main(String[] args){
List<MyClass> list = new LinkedList<>();
Collections.addAll(list, new MyClass(23),
new MyClass(12),
new MyClass(98),
new MyClass(20));
// Collections 工具类就可以直接对这种可比较的对象进行排序
Collections.sort(list);
}
/*Collections sort 方法源码*/
// 如果链表中存放的对象本身就是可比较的那么就不需要传入比较器来比较对象了
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
Comparator<T>
:[该接口仅需要重写一个方法]{.red},其余方法都是静态的或者默认实现的定义:[实现该接口的类是可以去比较其他类的]{.blue},也就是说实现了该接口的类就是 [比较器]{.red}
要求:[泛型接口中传入的类可以不是实现该接口的类,可以是任何需要比较的类]{.red}
细节:(1)
java.lang
包下的方法 (2) 函数式接口/*接口定义*/
public interface Comparator<T> {
int compare(T o1, T o2);
}
/*例子*/
// 被比较的类不需要实现任何接口
class Person{
public int age;
... // 提供构造方法初始化成员变量
}
/*比较器:传入的类型就是需要比较的类*/
class PersonComaprator implements Comparator<Person>{
int compare(Person person1, Person person2){
return person1.age > person2.age ? 1 : (person1.age == person2.age ? 0 : -1);
}
}
public static void main(String[] args){
List<MyClass> list = new LinkedList<>();
Collections.addAll(list, new MyClass(23),
new MyClass(12),
new MyClass(98),
new MyClass(20));
// Collections 工具类就可以直接对这种可比较的对象进行排序
Collections.sort(list, new PersonComparator());
}
/*Collections sort 方法源码*/
// 如果链表中存放的对象本身是不可比较的,那么就需要传入实现了 Comparator 接口的比较器来对链表元素进行比较
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
克隆接口
:::info
参考博客:Java提高篇——对象克隆(复制)
:::
什么是克隆?
定义:简单来说就是 [复制]{.blue},将一个变量的值赋给另一个变量
基本数据类型的克隆:[原变量发生变化不会影响到克隆变量的值,两者在栈中具有独立的空间]{.red}
public static void main(String[] args) throws CloneNotSupportedException
{
// 基本数据类型的克隆
int number = 10;
int cloneNumber = number;
// 修改原变量的值
number = 20;
// 查看两个的变量的值是否相同
System.out.println(number + "\t" + cloneNumber);
}引用类型的克隆:
思考:引用对象是怎么进行克隆的呢?和基本数据类型相同吗?
public static void main(String[] args) throws CloneNotSupportedException
{
// 引用类型的克隆
Student student1 = new Student(12, "冬坂五百里");
// 尝试用基本数据类型的克隆方法来完成引用类型的克隆
Student student2 = student1;
// 修改第二个对象的年龄
student2.age = 24;
// 查看两个对象的年龄是否相同
System.out.println(student1.age);
System.out.println(student2.age);
}结果:[在我们修改了第二个对象的年龄之后发现两者的年龄居然相同,两者并没有在堆空间独立存在]{.green}
分析:这种方式只是将栈空间中的两个引用指向了堆空间中的同一个对象而已,并没有为克隆变量开辟新的堆空间
利用 System.identityHashCode(); 方法可以计算对象在堆内存中的地址,结果一定是一样的
结论:显然这种克隆并不是我们真正想要的克隆方式,我们想要的显然是两个对象是完全独立的,相互不受影响的
如何实现克隆?
前提:[经过前面的分析可以明确的是,克隆这种技术主要针对的是引用类型的对象,毕竟基础数据类型完全可以直接复制]{.red}
核心:
[实现 Cloneable 接口,覆写 Object 类提供的 clone 方法后调用即可]{.red}
Cloneable 接口是 [标志性]{.red} 接口,没有定义任何方法,实现该接口仅仅是标志当前对象可以被克隆
源码
[克隆的对象和原对象在堆中具有相互独立的空间,两者不会相互影响]{.red}
克隆的对象类型和原对象的类型相同,并不强制
clone 方法返回的类型是 Object,可以根据自己的需要强制转换,所以可能出现和原对象类型不相同的情况
克隆对象和原对象内容是相同的,前提是必须重写
equals
方法
/**
* 源码中提到的注意事项
* x.clone() != x will be true
* x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.
* x.clone().equals(x) will be true, this is not an absolute requirement.
*/
protected native Object clone() throws CloneNotSupportedException;
浅克隆与深克隆:
场景:类中不仅包含基本数据类型的成员变量,同时包含引用类型的成员变量
浅克隆:对象被复制时,仅复制基本数据类型的成员变量和引用类型的引用,[并不会复制引用类型拥有的成员变量]{.red}
注:意味着只要修改引用类型拥有的成员变量就会导致克隆对象和原对象一起发生改变
深克隆:对象被复制时,不仅复制基本数据类型的成员变量,[并且将引用类型拥有的成员变量一起复制]{.red}
:::info
简单了解区别之后,再来看这两者具体是怎么实现的,具体的演示也在后面
:::
浅克隆
特点:
- [Object 类默认提供的克隆方式就是浅克隆]{.red}
- [不会复制引用类型拥有的成员变量,仅会复制引用类型的引用]{.red}
实现:
// 克隆的对象必须实现 Cloneable 接口并且重写 clone 方法
class Student implements Cloneable{
// 为了方便赋值就设为公共的属性
public int age;
public String name;
public Student(int age, String name){
this.age = age;
this.name = name;
}
protected Object clone() throws CloneNotSupportedException
{
// Object 类中默认的克隆方式就是浅拷贝:不需要重写任何代码
return super.clone();
}
}
// 测试
public static void main(String[] args) throws CloneNotSupportedException{
// 浅克隆: 本质是创建新的对象并且将旧对象的值赋给新对象
Student old_student = new Student(12, "冬坂五百里");
Student new_student = (Student) old_student.clone();
// 修改克隆对象的年龄
new_student.age = 24;
// 测试两者的年龄是否相同:结果是显然不相同的
System.out.println(old_student.age);
System.out.println(new_student.age);
}
深克隆
手动实现:
特点:
- [会将引用类型拥有的成员变量一起复制]{.red}
- 引用类型的成员变量也必须实现 Cloneable 接口并且重写 clone 方法
实现:
class Address implements Cloneable{
public String address;
public Address(String address)
{
this.address = address;
}
protected Object clone() throws CloneNotSupportedException
{
return super.clone();
}
}
// 克隆的对象必须实现 Cloneable 接口并且重写 clone 方法
class Student implements Cloneable{
public int age;
public String name;
public Address address;
public Student(int age, String name, Address address){
this.age = age;
this.name = name;
this.address = address;
}
protected Object clone() throws CloneNotSupportedException
{
Student student = (Student) super.clone();
// 调用引用类型的成员变量后调用其相应的克隆方法: 手动克隆
student.address = (Address) address.clone();
return student;
}
}
/**
错误测试:更改的是引用类型拥有成员变量而不是引用类型本身
new_student.address = new Address("中国")
这样修改就相当于修改了成员变量的引用,无论你是浅克隆还是深克隆,两个对象的地址肯定都是不一样的
**/
public static void main(String[] args) throws CloneNotSupportedException{
// 手动实现深克隆:在克隆方法中继续调用引用类型的克隆方法就是手动实现
Student old_student = new Student(12, "冬坂五百里");
Address address = new Address("日本");
old_student.address = address;
Student new_student = (Student) old_student.clone();
// 更改克隆对象的引用类型成员变量拥有的成员变量的值
address.address = "中国";
// 测试两个对象的地址是否相同:结果肯定是不同的
System.out.println(old_student.address);
System.out.println(new_student.address);
}
序列化实现
场景:
- 如果类中的引用类型的成员变量也拥有引用类型的成员变量,或者类中存在多个引用类型的成员变量
- 前者就需要在克隆方法中不断地嵌套调用克隆方法,后者则是需要同时调用多个克隆方法
- 这种情况被称作 [多层克隆]{.red},显然手动实现克隆是非常麻烦的
特点:
- [序列化实现深克隆就可以解决多层克隆这种情况]{.red}
- [每个引用类型或者说类都需要实现序列化接口]{.red}
实现
class Student implements Serializable{
...
}
class Address implements Serializable{
}
class Teacher implements Serializable
{
private int age;
private String name;
private List<Student> students;
private Address address;
// 省略不重要的代码
...
protected Object clone()
{
// 序列化流
ObjectOutputStream oos = null;
// 反序列化流
ObjectInputStream ois = null;
// 克隆对象
Teacher teacher = null;
try
{
ByteArrayOutputStream bao = new ByteArrayOutputStream();
// 将对象写入字节数组输出流中
oos = new ObjectOutputStream(bao);
oos.writeObject(this);
// 从哪里读入数据: 从字节数组输出流中读取数据
ois = new ObjectInputStream(new ByteArrayInputStream(bao.toByteArray()));
teacher = (Teacher) ois.readObject();
}
catch (IOException | ClassNotFoundException e)
{
e.printStackTrace();
}
return teacher;
}
}
public static void main(String[] args)
{
// 序列化实现深克隆
List<Student> students = new LinkedList<>();
Collections.addAll(students,new Student(12, "学生1号"),
new Student(13, "学生2号"),
new Student(14, "学生2号"));
Teacher teacher = new Teacher(24, "冬坂五百里", students,new Address("日本"));
Teacher clone_teacher = (Teacher) teacher.clone();
// 修改克隆对象
clone_teacher.getAddress().setAddress("中国");
clone_teacher.getStudents().remove(2);
// 测试两个对象的集合内容是否相同、以及地址是否相同
teacher.getStudents().forEach(System.out::println);
clone_teacher.getStudents().forEach(System.out::println);
System.out.println(teacher.getAddress());
System.out.println(clone_teacher.getAddress());
}
为什么要使用克隆?为什么不直接创建新对象后挨个赋值呢?
- 理由①:某些情况我们需要暂时保存当前对象的状态,确保我们在修改当前对象后依然能够得知其原有的状态
- 理由②:
clone
方法的默认实现是本地的,[效率显然会优于循环赋值]{.red}clone
能够简化循环赋值的过程,[代码更加简洁]{.red}
细节
- 字符串对象实际上也是引用类型的对象,但是由于字符串对象具有 [不可变的特性]{.red},
- 修改字符串内容实际上修改的是引用指向的内容,而不是在原有的基础上进行修改
- 所以在浅克隆中修改字符串对象不会导致克隆对象和原对象一起发生,原对象的引用依然指向原来的字符串内容,克隆对象的引用已经指向新的字符串内容了
接口 vs 抽象类
接口 | 抽象类 | |
---|---|---|
继承 & 实现 | 可以实现多个接口 | 仅可以继承一个抽象类 |
方法 | 所有方法都是公共的抽象方法 | 既有抽象方法也有普通方法([抽象方法默认包可见性]{.red}) |
成员变量 | 仅拥有静态常量 | 任意类型成员变量 |
耦合度 | 低 | 高 |