目录
一:抽象类
1.1抽象类的概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
例如,这里有三个类,🐕类和🐱类分别继承Animal,并重写Animal中的bark()方法。对于Animal类而言,由于其并不会指向一个具体的动物,所以其中的bark()方法无需进行实现。即这个类中没有包含足够的信息来描绘一个具体的对象。这样的Animal类就是抽象类。
1.2抽象类的语法
在Java中,一个类如果被abstract 修饰称为抽象类,抽象类中被abstract 修饰的方法称为抽象方法,抽象方法不用给出具体的实现。
1.3抽象类的特性
1.抽象类不能被实例化;(这个我们容易理解,抽象类的存在,本就不用于指定具体的对象)
2.抽象类中的成员变量和成员方法都与普通的类一样;(抽象类也是类)
3.抽象类存在的最大意义,就是被继承;
4.抽象类也可以发生向上转型,进一步发生多态;
5.当一个普通类,继承这个抽象类,这个普通类必须重写抽象类中的所有抽象方法;
6.当一个抽象类B继承了抽象类A,那么抽象类B可以不重写抽象类A当中的抽象方法;
7.当一个普通的类C继承了'6'中的抽象类B,就必须重写所有的抽象方法;
8.final不能修饰抽象方法和抽象类;(因为final修饰的不能重写,abstract修饰的必须重写)
9.抽象方法不能是private的;
10.抽象类当中不一定有抽象方法,但如果这个方法是抽象方法,那么这个类一定是抽象类。
二:接口
2.1接口概念
接口是一种行为的规范和标准。
2.2接口的特点
1.接口使用关键字interface来修饰;
2.接口当中的成员方法,只能是抽象方法,所有的方法,默认都是public abstract类型;
3.接口当中的成员变量,默认是public static final类型;
4.如果要实现接口当中的方法,需要用default来修饰;
5.接口当中的静态方法,可以有具体的实现;
//1.接口使用关键字interface来修饰
interface IShape{
//2.接口当中的成员方法,只能是抽象方法,所有的方法,默认都是public abstract类型;
// public abstract void func()相当于void func()
public abstract void func();
//3.成员变量,默认是public static final类型;
public static final int a = 10;
//4.如果要实现接口当中的方法,需要用default来修饰;
default void func1(){
System.out.println("默认方法!");
}
//5.接口当中的静态方法,可以有具体的实现。
public static void staticFunc(){
System.out.println("静态方法!");
}
}
6.接口不能进行实例化;
7.一个普通类可以通过implements来实现一个接口 .
具体实例如下:
这是一个画图的示例代码,父类为IShape类,子类则重写父类中的draw()方法,绘制不同的、具体的图形。
interface IShape {
void draw();
}
class Cycle implements IShape {
@Override
public void draw() {
System.out.println("●");
}
}
class Rect implements IShape {
@Override
public void draw() {
System.out.println("♦");
}
}
class Triangle implements IShape {
@Override
public void draw() {
System.out.println("△");
}
}
public class TestDemo1 {
public static void drawMap(IShape shape) {
shape.draw();
}
public static void main(String[] args) {
Cycle cycle = new Cycle();
Rect rect = new Rect();
Triangle triangle = new Triangle();
drawMap(cycle);
drawMap(rect);
drawMap(triangle);
}
}
运行结果如下:
8.一个类,可以继承抽象类,同时实现多个接口,每个接口之间使用逗号隔开。具体实例如下:
package Test1;
class Animal {
public String name;
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();
}
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+" 正在游!");
}
}
public class TestDemo1 {
public static void run(IRunning iRunning) {
iRunning.run();
}
public static void swim(ISwimming iSwimming) {
iSwimming.swim();
}
public static void fly(IFlying iFlying) {
iFlying.fly();
}
public static void main(String[] args) {
Duck duck = new Duck("唐老鸭");
duck.fly();
duck.run();
duck.swim();
System.out.println("=============");
fly(duck);
run(duck);
swim(duck);
}
}
运行结果如下:
具体分析:
注意:
- IDEA中可以使用ctrl+i快速实现接口。
- IDEA中可以使用alt+回车快速实现接口。
2.3接口的优势
有了接口之后, 类的使用者就不必关注具体类型,而只关注某个类是否具备某种能力即可。
例如, 现在实现一个方法, 叫 "跑步"。
public static void walk(IRunning running) {
System.out.println("跑步");
running.run();
}
在这个 walk 方法内部, 我们并不关注到底是哪种动物, 只要参数是会跑的, 就行。
Cat cat = new Cat("洗洗");
walk(cat);
Frog frog = new Frog("乔治");
walk(frog);
// 执行结果
跑步
洗洗正在用四条腿跑
跑步
乔治正在往前跳
甚至参数可以不是 "动物", 只要会跑!
class Robot implements IRunning {
private String name;
public Robot(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(this.name + "正在用轮子跑");
}
}
Robot robot = new Robot("阿尔法狗");
run(robot);
// 执行结果
阿尔法狗正在用轮子跑
2.4接口间的继承
我们只需知道两点,第一,接口间的继承表示的是一个接口拓展了另外一个接口的功能;第二,接口之间的继承用的是extends。
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
// 两栖动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {
}
class Frog implements IAmphibious {
...
}
当然,你拓展了接口,自然要重写所有的抽象方法。
2.5接口使用实例
2.5.1给对象数组排序
我们首先回忆一下如何对整型数组进行排序:
public class TestDemo2 {
public static void main(String[] args) {
int[] array = {1,21,4,15,6,17};
Arrays.sort(array);
System.out.println(Arrays.toString(array));
}
}
运行结果如下:
显然,对整型数组排序,直接调用Arrays.sort方法即可。
如果我们照猫画虎,对如下的学生数组进行排序,是否也可以直接调用Arrays.sort方法进行排序呢?代码实例及运行结果如下:
package Test1;
import java.util.Arrays;
class Student {
public String name;
public int age;
public double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
}
public class TestDemo2 {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("zhagnsan",98,58.9);
students[1] = new Student("lisi",38,98.9);
students[2] = new Student("aboluo",78,88.9);
Arrays.sort(students);
System.out.println("排序后的结果为:");
System.out.println(Arrays.toString(students));
}
}
不好意思,抛出了异常;这是为何呢?原因是我们没有给出排序的依据,是根据姓名、年龄、还是分数呢?
解决方案是,让我们的 Student 类实现 Comparable 接口, 并实现其中的 compareTo 方法。对于Comparable接口,我们需要了解:
基于这种认识,我们需要在Student类当中重写Comparable接口中的抽象方法:
具体代码如下:
package Test1;
import java.util.Arrays;
class Student implements Comparable<Student>{
public String name;
public int age;
public double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
@Override
public int compareTo(Student o) {
//return this.name.compareTo(o.name);
//return o.age-this.age;
return (int)(this.score-o.score);
}
}
public class TestDemo2 {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("zhagnsan",98,58.9);
students[1] = new Student("lisi",38,98.9);
students[2] = new Student("aboluo",78,88.9);
Arrays.sort(students);
System.out.println("按年龄排序后的结果为:");
System.out.println(Arrays.toString(students));
}
}
运行结果如下:
这样的代码还有很大的缺陷,即对于比较的标准,在类中做了具体的规定。在这里我们通过年龄进行排序,一旦我们有一天需要根据分数进行排序,就需要对Student类中的代码进行修改,很不方便。一种明智的解决方案是通过比较器进行比较。
具体代码如下:
package Test1;
import java.util.Arrays;
import java.util.Comparator;
class Student {
public String name;
public int age;
public double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
}
//年龄比较器
class AgeComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
//姓名比较器
class StringComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
}
//分数比较器
class ScoreComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return (int)(o1.score-o2.score);
}
}
public class TestDemo3 {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("zhagnsan",98,58.9);
students[1] = new Student("lisi",38,98.9);
students[2] = new Student("aboluo",78,88.9);
ScoreComparator scoreComparator = new ScoreComparator();
Arrays.sort(students,scoreComparator);
System.out.println("按分数排序后的结果为:");
System.out.println(Arrays.toString(students));
}
}
运行结果如下:
分析:
Q:为什么不重写Comparator接口中的equals等方法呢?
![](https://i-blog.csdnimg.cn/blog_migrate/a8ea6ea41de0abba44d6692cfbe792c2.png)
A:如果一个类既有继承又有实现,并且接口中的方法与父类接口中的方法相同,那么该类不用重写接口中的方法也可以。这就要说起java的祖宗Object了,Object是所有java类的父类,定义了一些方法,其中就有equals方法,让我们来看一下。所以为什么不用重写呢?因为父类已经有该方法且是具体的方法。
2.5.2Cloneable 接口和深浅拷贝
5.2.1浅拷贝
浅拷贝是会将对象的每个属性进行依次复制,但是当对象的属性值是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。我们先来看一个例子:
首先,我们创建一个Person类,实例化一个person1对象,并尝试进行拷贝。
显然代码出现了错误,此时不能直接进行拷贝。那么拷贝的要求是什么呢?一个对象可以被克隆,必须满足两个条件:
具体步骤如下:
S1:在Person类中实现Cloneable接口。
S2:重写Cloneable接口中的clone()方法。将鼠标放在空白处,按下ctrl+O,选择clone():Object方法,点击OK。
这里的意思,即调用父类Object类中的clone()方法。
S3.将person.clone()强转为Person,此为最特殊的一步。
三步完成,大局已定,直接运行即可,具体代码如下:
package Test;
class Person implements Cloneable{
public int id;
public Person(int id) {
this.id = id;
}
@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(1234);
Person person2 = (Person)person1.clone();
System.out.println(person2);
}
}
运行结果如下:
分析:
我们现在讲了半天,其实这个Person类中只有一个简单的成员变量,并没有引用类型。我们再回顾浅拷贝的定义,是会将对象的每个属性进行依次复制,但是当对象的属性值是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。所以现在,我会在Person类中添加一个引用类型的对象,我们来看看会发生什么。
package Test;
class Money{
public double money = 19.9;
}
class Person implements Cloneable{
public int id;
public Money m = new Money();
public Person(int id) {
this.id = id;
}
@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(1234);
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值,同样发生了变化。 即当对象的属性值是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。这种拷贝,我们称之为浅拷贝。
分析:
5.2.2深拷贝
深拷贝复制变量值,对于引用数据,则递归至基本类型后,再复制。深拷贝后的对象与原来的对象是完全隔离的,互不影响,对一个对象的修改并不会影响另一个对象。
对于我们上面的例子,如何实现深拷贝呢?简单,既然调用clone()方法能够实现克隆,那我再克隆一份person1所指向的m所指向的money,然后让person2所指向的m指向这份新的money,不就好了吗?具体步骤如下:
S1:在Money类中实现Cloneable接口,并重写Cloneable接口中的clone()方法。
S2: 在Person类中的clone()方法也要发生相应的改变。在“浅拷贝”中,我们直接return super.clone()即可,而在“深拷贝”中,需要对引用进行拷贝。
分析:
此时,当我使用person2对象改变money值后,再通过person1对象访问money值,不再发生变化。具体代码如下:
package Test;
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;
public Money m = new Money();
public Person(int id) {
this.id = id;
}
@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;
}
}
public class TestDemo {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person(1234);
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);
}
}
运行结果如下:
注意:
1.实现深拷贝,是从代码层次上进行的,不是说某个方法是深拷贝,是从代码的实现来看的;
2.要达到深拷贝,如果对象中有引用,这个对象的引用所指的对象也要进行拷贝;
3.拷贝完成之后,通过这个引用修改其指向的数据,原来的不会发生改变。
补充:抽象类和接口的区别
核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法。
三:Object类
Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父类。即所有类的对象都可以使用Object的引用进行接收。
整个Object类中的方法如下图所示:
在本课中,我会主要介绍toString()方法,equals()方法,hashcode()方法。
3.1toString()方法
如果要打印对象中的内容,可以直接重写Object类中的toString()方法。具体实例如下:
package Test;
class Player{
public int age;
String name;
public int salary;
public Player(int age, String name, int salary) {
this.age = age;
this.name = name;
this.salary = salary;
}
@Override
public String toString() {
return "Player{" +
"age=" + age +
", name='" + name + '\'' +
", salary=" + salary +
'}';
}
}
public class TestDemo3 {
public static void main(String[] args) {
Player p1 = new Player(37,"勒布朗詹姆斯",37800000);
System.out.println(p1);
}
}
运行结果如下:
3.2equal()方法
作用:用于比较两个对象中内容是否相同。首先我们要了解,什么情况下认为两个对象中内容相同?
如何利用编译器生成equals()方法?
S1:
S2:
S3:直接按默认设置next,最后finish即可。
分析:
具体代码如下:
package Test;
import java.util.Objects;
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 TestDemo4 {
public static void main(String[] args) {
Student student1 = new Student("gaobo",18);
Student student2 = new Student("gaobo",18);
System.out.println(student1.equals(student2));
}
}
运行结果如下:
3.3hashCode()方法
hashCode()这个方法用于计算一个具体的对象位置,我们可以初步理解为计算了内存地址所在。
内容完全一样的两个对象,我们认为它们应该放在同一个地址,所以这时需要重写hashCode()方法以达到我们的目的。具体代码如下:
package Test;
import java.util.Objects;
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 TestDemo4 {
public static void main(String[] args) {
Student student1 = new Student("gaobo",18);
Student student2 = new Student("gaobo",18);
System.out.println("对象位置:");
System.out.println(student1.hashCode());
System.out.println(student2.hashCode());
}
}
运行结果如下:
显然,结果确实如我们所预想的那样。
结论:
1、hashcode方法用来确定对象在内存中存储的位置是否相同 ;
2、事实上hashCode() 在散列表中才有用,在其它情况下没什么用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
最后说一点,在之前已经分析了Object可以接收任意的对象,因为Object是所有类的父类,但是Obejct并不局限于此,它可以接收所有数据类型,包括:类、数组、接口。不过坦白讲,这些内容并不会经常用到,也没必要去纠结。
本课内容完!