目录
1.抽象类
1.1抽象类的概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的
如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。 比如
在打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由 Shape的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类(abstract class).
1.2抽象类语法
//抽象类
abstract class Shape {
public abstract void draw();//抽象方法
}
注:抽象类也是类,内部可以包含普通方法和属性,甚至构造方法
1.3抽象类特性
我们总结一下抽象类的特性,我把它总结为15点
1.4抽象类的作用
使用抽象类相当于多了一重编译器的校验
使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了, 使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题
2.接口
2.1接口的概念
接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。
在Java中,接口可以看成是:多个类的公共规范,是一种引用数据类型。
2.2语法
接口的定义格式与定义类的格式基本相同,将class关键字换成 interface 关键字,就定义了一个接口。
public interface 接口名称{
// 抽象方法
public abstract void method1(); // public abstract 是固定搭配,可以不写
public void method2();
abstract void method3();
void method4();
// 注意:在接口中上述写法都是抽象方法,跟推荐方式4,代码更简洁
}
提示:
1. 创建接口时, 接口的命名一般以大写字母 I 开头.
2. 接口的命名一般使用 "形容词" 词性的单词.
3. 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性.
2.3接口使用
接口不能直接使用,必须要有一个"实现类"来"实现"该接口,实现接口中的所有抽象方法
public class 类名称 implements 接口名称{
// ...
}
注意:子类和父类之间是extends 继承关系,类与接口之间是 implements 实现关系。
2.4接口特性
总结:
2.5实现多个接口
class Animal {
public String name;
public int age;
public Animal(String name){
this.name = name;
}
public void eat(){
System.out.println(this.name+"吃饭饭");
}
}
interface IFlying {
void fly();
};
interface IRunning {
void run();
};
interface ISwimming {
void swim();
};
//狗继承了Animal,实现了这两个功能接口
class Dog extends Animal implements IRunning,ISwimming{
public Dog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name+"正在跑");
}
@Override
public void swim() {
System.out.println(this.name+"正在游泳");
}
}
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.
这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型,而只关注某个类是否具备某种能力
再实现一个walk方法
class Animal {
public String name;
public int age;
public Animal(String name){
this.name = name;
}
public void eat(){
System.out.println(this.name+"吃饭饭");
}
}
interface IFlying {
void fly();
};
interface IRunning {
void run();
};
interface ISwimming {
void swim();
};
//狗继承了Animal,实现了这两个功能接口
class Dog extends Animal implements IRunning,ISwimming{
public Dog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name+"正在跑");
}
@Override
public void swim() {
System.out.println(this.name+"正在游泳");
}
}
public class Test2 {
public static void walk(IRunning iRunning){
iRunning.run();
}
public static void main(String[] args) {
IRunning iRunning = new Dog("土豆");
walk(new Dog("崽崽"));
}
}
2.6接口之间的继承
interface A{
void funcA();
}
interface B{
void funcB();
}
//代表C 扩展了 A 和 B 的功能
interface C extends A,B{
void funcC();
}
class AA implements C{
@Override
public void funcC() {
System.out.println("funcC()");
}
@Override
public void funcA() {
}
@Override
public void funcB() {
}
}
2.7接口运用实例
Comparable 接口
让我们的 Student 类实现 Comparable 接口, 并实现其中的 compareTo 方法
在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象.
- 然后比较当前对象和参数对象的大小关系(按分数来算).
- 如果当前对象应排在参数对象之前, 返回小于 0 的数字;
- 如果当前对象应排在参数对象之后, 返回大于 0 的数字;
- 如果当前对象和参数对象不分先后, 返回 0;
- 再次执行程序, 结果就符合预期
【注意事项】: 对于 sort 方法来说, 需要传入的数组的每个对象都是 "可比较" 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则
在比较name时我们有这样的问题,在compareTo方法内部又调用了compareTo方法,这不是构成了递归了吗?
这里我们尤其需要注意,这里的两个compareTo方法完全不是同一个!!!
comparator接口
【升级版】(比较器)可以分别实现不同的比较
Cloneable接口
我们通过内存示意图分析一下克隆的过程
浅拷贝和深拷贝
下面我们在Person类的内部再定义一个引用变量
class Money implements Cloneable{
public double money = 19.9;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person implements Cloneable{
public int id = 1234;
public Money m = new Money();
@Override
public String toString() {
return "Person{" +
"id='" + id + '\'' +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class TestDemo {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person();
Person person2 = (Person)person1.clone();
System.out.println(person1.m.money);
System.out.println(person2.m.money);
System.out.println("=========================");
person2.m.money = 99.99;
System.out.println(person1.m.money);
System.out.println(person2.m.money);
}
}
我们运行一下这个程序
看一下输出的结果
我们通过person2的引用将money的值改变后,person1的money的值也改变了,也就是说
- 拷贝过程中我们只是拷贝了Person对象。但是Person对象中的Money对象并没有拷贝。
- 通过person2这个引用修改了m的值后,person1这个引用访问m的时候,值也发生了改变。
这里就是发生了浅拷贝。
我们用一张图帮助理解浅拷贝
那么如何实现深拷贝呢?
我们先大概猜测一下深拷贝是如何拷贝的
我们怎么实现呢?
需要把money也实现一下Cloneable接口
class Money implements Cloneable{
public double money = 19.9;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person implements Cloneable{
public int id = 1234;
public Money m = new Money();
@Override
public String toString() {
return "Person{" +
"id='" + id + '\'' +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
Person tmp = (Person) super.clone();
tmp.m = (Money) this.m.clone();
return tmp;
//return super.clone();
}
}
public class TestDemo {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person();
Person person2 = (Person)person1.clone();
System.out.println(person1.m.money);
System.out.println(person2.m.money);
System.out.println("=========================");
person2.m.money = 99.99;
System.out.println(person1.m.money);
System.out.println(person2.m.money);
}
}
我们用一张图分析一下这个代码
我们再运行一下程序
我们可以看出,此时person2的money改变后,person1的money值并没有改变
所以,可以判断,深拷贝成功
我们总结一下:
要达到深拷贝,每个对象中,如果也有引用,那么这个对象的引用 所指的对象,也要进行克隆
拷贝完成后,通过这个引用修改其指向的数据,原来的数据不会发生改变
2.8 抽象类和接口的区别
核心区别:
- 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写)
- 而接口中不能包含普通方法, 子类必须重写所有的抽象方法
抽象类存在的意义是为了让编译器更好的校验, 像 Animal 这样的类我们并不会直接使用, 而是使用它的子类.
万一不小心创建了 Animal 的实例, 编译器会及时提醒我们
3.Object类
Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父类。即所有类的对象都可以使用Object的引用进行接收。
范例:使用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类也存在有定义好的一些方法。如下
对于整个Object类中的方法需要实现全部掌握。
本小节当中,我们主要来熟悉这几个方法:toString()方法,equals()方法,hashcode()方法
3.1获取对象信息
如果要打印对象中的内容,可以直接重写Object类中的toString()方法,之前已经讲过了,此处不再累赘
3.2 对象比较equals方法
在Java中,==进行比较时:
- 如果==左右两侧是基本类型变量,比较的是变量中值是否相同
- 如果==左右两侧是引用类型变量,比较的是引用变量地址是否相同
- 如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的
直接这样用equals结果是false
我们需要重写equals方法
完整代码
class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
}
public class TestDemo2 {
public static void main(String[] args) {
Student student1 = new Student("xiaoming",18);
Student student2 = new Student("xiaoming",18);
System.out.println(student1.equals(student2));
}
}
结论:比较对象中内容是否相同的时候,一定要重写equals方法
3.3 hashcode方法
回忆刚刚的toString方法的源码:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
我们看到了hashCode()这个方法,他帮我算了一个具体的对象存在位置,这里面涉及数据结构,但是我们还没学数据结构,没法讲述,所以我们只能说它是个内存地址。然后调用Integer.toHexString()方法,将这个地址以16进制输出。
hashcode方法源码:
public native int hashCode();
该方法是一个native方法,底层是由C/C++代码写的。我们看不到。
我们认为两个名字相同,年龄相同的对象,将存储在同一个位置,如果不重写hashcode()方法,我们可以来看示例
class Student {
public String name;
public 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 TestDemo2 {
public static void main(String[] args) {
Student student1 = new Student("xiaoming",18);
Student student2 = new Student("xiaoming",18);
System.out.println("========对象的位置========");
System.out.println(student1.hashCode());
System.out.println(student2.hashCode());
}
}
注意事项:此时两个对象的hash值不一样。
像重写equals方法一样,我们也可以重写hashcode()方法。此时我们再来看看
class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class TestDemo2 {
public static void main(String[] args) {
Student student1 = new Student("xiaoming",18);
Student student2 = new Student("xiaoming",18);
System.out.println("========对象的位置========");
System.out.println(student1.hashCode());
System.out.println(student2.hashCode());
}
}
注意事项:此时哈希值一样。
结论:
- hashcode方法用来确定对象在内存中存储的位置是否相同
- 事实上hashCode() 在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
3.4 接收引用数据类型
在之前已经分析了Object可以接收任意的对象,因为Object是所有类的父类,但是Obejct并不局限于此,它可以接收所有数据类型,包括:类、数组、接口。
范例:使用Object来接受数组对象
public static void main(String[] args) {
// Object接收数组对象,向上转型
Object obj = new int[]{1,2,3,4,5,6} ;
// 向下转型,需要强转
int[] data = (int[]) obj ;
for(int i :data){
System.out.println(i+"、");
}
}
而Object可以接收接口是Java中的强制要求,因为接口本身不能继承任何类。
范例:使用Object接收接口对象
interface IMessage {
public void getMessage() ;
}
class MessageImpl implements IMessage {
@Override
public String toString() {
return "I am small biter" ;
}
public void getMessage() {
System.out.println("欢迎你");
}
}
public class Test {
public static void main(String[] args) {
IMessage msg = new MessageImpl() ; // 子类向父接口转型
Object obj = msg ; // 接口向Obejct转型Object真正达到了参数的统一,如果一个类希望接收所有的数据类型,就是用Object完成,在Java中,泛型就是底
层就是通过Object来实现的。
System.out.println(obj);
IMessage temp = (IMessage) obj ; // 强制类型转换
temp.getMessage();
}
}
Object真正达到了参数的统一,如果一个类希望接收所有的数据类型,就是用Object完成,在Java中,泛型就是底层就是通过Object来实现的。