Ⅰ. 抽象类 -- abstract
一、概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
使用抽象类相当于多了一层编译器的检验 !
二、抽象类的语法
在 Java 中,一个类如果被** abstract ** 修饰则称为抽象类 ,抽象类中被 abstract修饰的方法 称为抽象方法 ,并且抽象方法不用给出具体的实现体。
说白了抽象类就是在多态的基础上,不让抽象类实例化出对象,并且重写的接口也不能有方法实现,然后强制性让子类去重写接口才能实例化子类对象,但同样也是可以用抽象类的引用指向子类的空间来达到多态的效果,形成一个更高层次的抽象感觉!
【抽象类和普通类的区别】
-
「抽象类 」不能实例化对象,而「普通类 」可以。
-
「抽象类 」中可以包含非抽象类方法和抽象方法,而「普通类 」只能包含非抽象类方法。
-
「抽象类 」不加访问限定符时候默认为
default,和「普通类 」是一样的。
abstract class Base {
**// 可以包含静态和非静态成员变量、方法,以及构造方法**
public int a = 10;
static public int b = 20;
public Base() {
System.out.println("Base()"); // 构造方法
}
public void func() {
System.out.println("func()"); // 非静态方法
}
public static void func2() {
System.out.println("static func2()"); // 静态方法
}
**// 抽象方法:(不能有函数体)**
abstract protected void eat();
}
class Derived extends Base {
public Derived() {
System.out.println("Derived()");
}
// 重写抽象类函数,其返回值可以比抽象类函数的返回值权限高一些
@Override
public void eat() {
System.out.println("Derived eat()");
}
}
public class test1 {
public static void main(String[] args) {
Base tmp = new Derived();
System.out.println(tmp.a + Base.b);
tmp.eat(); // 访问的是子类重写的方法
tmp.func();
Base.func2();
}
}
// 执行结果:
Base()
Derived()
30
Derived eat()
func()
static func2()三、抽象类的特性
-
「抽象类 」不能直接实例化对象 。
-
「抽象类 」也是类,可以包含普通方法和属性 、静态方法和属性 ,甚至构造方法 。
-
「抽象方法」不能被 **
private、**static、final** 修饰** 。(这和多态规则是类似的) -
抽象方法和非抽象方法的默认的访问限定符为
default。(但不能显式写default) -
子类继承「抽象类」之后要重写父类中的抽象方法,否则子类也必须得是抽象类才行 。
Ⅱ. 接口 -- interface
接口是公共的行为规范标准 ,大家在实现时,只要符合规范标准就可以通用,不需要在乎类型 。在 Java 中,接口可以看成是多个类的公共规范,是一种引用数据类型 。
接口的定义格式与定义类的格式基本相同,将 ** class 关键字换成 ** interface** 关键字** ,就定义了一个接口 。
【接口类的特性和规定】
-
接口不能直接实例化出对象 。
-
接口的命名一般以大写字母
I开头。 -
阿里编码规范中约定,接口中的方法和属性不要加任何修饰符号 ,保持代码的简洁性。
-
接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是
.class。 -
和抽象类一样,如果子类没有实现接口中的所有的抽象方法,则该子类必须设置为抽象类,否则会报错!
-
成员变量 :
-
默认被
public static final修饰,所以必须初始化 。 -
不能被
public外的访问限定符修饰。(这里说的不准确,具体可以看下面的总结)
-
-
成员方法 :
-
默认被
public abstract修饰 ,所以可以简洁的写出接口。(也就是只能为public abstract,如果是其他修饰符都会报错) -
不能被
public外的访问限定符修饰。(这里说的不准确,具体可以看下面的总结) -
不能存在 「
构造方法」 和 「静态代码块」 。 -
接口可以有「
静态方法」,但是该静态方法必须要有方法体。 -
一般接口没有方法体,但是在
JDK 1.8中是可以有的,需要显式使用default修饰该方法。
-
interface IUSB {
// 成员变量:
public int a; // ❌必须初始化
public int b = 10; // ✅
protected int c = 20; // ❌不能被public外的访问限定符修饰
int d = 30; // ✅推荐这种写法
private int e = 40; // ❌不能被public外的访问限定符修饰
public static int f = 50; // ✅
public static final int g = 60; // ✅
// 成员方法:
public abstract void method(); // ✅public abstract是固定搭配,可以不写
void method1(); // ✅推荐这种写法
private void method2(); // ❌不能被public外的访问限定符修饰
protected void method3(); // ❌不能被public外的访问限定符修饰
public static void method4() {
// ✅静态方法必须有方法体
}
default public void method5() {
// ✅普通方法有方法体的话,必须使用default修饰该方法
}
public IUSB {} // ❌不能存在构造方法
}一、接口的使用 -- implements
接口不能直接使用,必须要有一个子类来 "实现" 该接口,实现接口中的所有抽象方法。(这跟前面所说的抽象类是类似的)
这里要用到 implements** 关键字来表示实现该接口** ,注意不是继承关系!
interface IUSB {
void method();
}
class mouse implements IUSB {
// 对接口的方法进行重写
@Override
public void method() {
System.out.println("mouse重写接口类的方法");
}
}下面我们就用一个文件一个类或者接口的编程方式,写一个 USB 接口,然后用鼠标类和键盘类来实现该接口的方法,接着用电脑类来向上转型,达到屏蔽底层实现的效果,代码如下所示:
package JavaSE.interfaceTest.USB;
// IUSB.java
public interface IUSB {
void openService(); // 打开USB的服务接口
void closeService(); // 关闭USB的服务接口
}
// Mouse.java
public class Mouse implements IUSB{
@Override
public void openService() {
System.out.println("打开mouse的接口服务");
}
@Override
public void closeService() {
System.out.println("关闭mouse的接口服务");
}
// 鼠标自己包含的方法
public void click() {
System.out.println("点击鼠标");
}
}
// KeyBoard.java
public class KeyBoard implements IUSB{
@Override
public void openService() {
System.out.println("打开keyBoard的服务");
}
@Override
public void closeService() {
System.out.println("关闭keyBoard的服务");
}
// 键盘自己包含的方法
public void input() {
System.out.println("键盘输入内容……");
}
}
// Computer.java
public class Computer {
public void usbMap(IUSB usb) {
// 调用usb的启动服务接口,此时不关心是哪个具体的子类实现的
usb.openService();
// 根据具体类型,进行向下转型调用子类独有的方法
if(usb instanceof Mouse) {
Mouse m = (Mouse)usb;
m.click();
} else if(usb instanceof KeyBoard) {
KeyBoard k = (KeyBoard)usb;
k.input();
}
// 调用usb的关闭服务接口,此时不关心是哪个具体的子类实现的
usb.closeService();
}
}
///////////////////////////////////////////////////////////////////////////
// Main.java
public class Main {
public static void main(String[] args) {
Computer cptr = new Computer();
cptr.usbMap(new Mouse());
System.out.println("-----------------");
cptr.usbMap(new KeyBoard());
}
}
二、接口与抽象类的区别
| 接口 | 抽象类 | |
|---|---|---|
| 关键字使用 | interface 和 implements |
abstract 和 extends |
| 谁可使用 | 任意类型均可实现接口 | 只能由子类重写方法 |
| 构造方法 | 没有构造方法 | 有构造方法 |
| 普通方法有无定义和实现 | 只有定义,没有方法的实现 | 既有定义,又可以有实现 |
| 类型拓展 | 一个类可实现多个接口 | 不支持多继承 |
| 成员变量 |
只能被 public static final 修饰,并且必须赋初值,不能被修改 |
与普通类的成员变量一样 |
| 成员方法 | 所有的成员方法都是 public abstract 修饰的 |
抽象方法被 abstract 修饰,其不能被 private、static、final、synchronized 和 native 等修饰,并且不能有方法体 |
| 静态代码块 | 不支持 | 可以使用 |
因为上面表格空间有限,这里补充一下访问限定符的总结:
-
「方法」的访问控制符 :
-
普通类方法 :
- 四个访问控制符都可以使用
-
抽象类方法 :
-
JDK 1.8以前:抽象类的方法默认访问权限为protected(即可以是public和protected) -
JDK 1.8:**抽象类的方法默认访问权限为 **default(即可以是public和protected或者不写)
-
-
接口方法 :
-
JDK 1.8以前:接口中的方法默认,也必须是public的(即只能用public) -
JDK 1.8:接口中的方法默认 **public的,也可以是 **default** 的** (即可以是public和default) -
JDK 1.9:接口中的方法可以是private的(即可以是public和default和private)
-
-
-
「变量」的访问控制符 :
-
普通类变量 :
- 四个访问控制符都可以使用
-
抽象类变量 :
- 四个访问控制符都可以使用
-
接口变量 :
- 接口变量默认且仅为
public static final
- 接口变量默认且仅为
-
三、一个类实现多个接口
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 + "正在蹬腿游泳");
}
}这样设计有什么好处呢?时刻牢记多态的好处,让程序猿忘记类型。有了接口之后,类的使用者就不必关注具体类型,而只关注某个类是否具备某种能力。
简单地说,就是当继承一个类之后,该子类还要其它的行为规范,那么就可以实现不同的行为规范来实现,而这些行为规范可能不属于当前父类所应该拥有的,比如说不是所有动物都会跑或者飞 !
此外需要注意的是继承需要放在实现接口之前,不能颠倒顺序 !
四、接口之间的继承
在 Java 中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承 。即:用接口可以达到类似多继承的目的,**使用的是继承的关键字 ** extends。如下所示:
interface A {
void funcA();
}
interface B {
void funcB();
}
interface C extends A, B { // C接口继承了A和B的接口
void funcC();
}
// Test类必须实现三个接口
class Test implements C{
@Override
public void funcA() {
System.out.println("重写A");
}
@Override
public void funcB() {
System.out.println("重写B");
}
@Override
public void funcC() {
System.out.println("重写C");
}
}五、常用接口
① Comparable
这个接口用来进行比较大小,通常用于自定义类型数组的排序!
在类中实现 Comparable<类型>接口 之后,重写 compareTo()方法 即可,如下所示:
class Student **implements Comparable<Student>** { // 要实现Comparable接口!!!
public int age;
public int score;
public String name;
public Student(int age, int score, String name) {
this.age = age;
this.score = score;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", score=" + score +
", name='" + name + '\'' +
'}';
}
**@Override**
** public int compareTo(Student o) {**
** return this.age - o.age; // 重写该接口,比较年龄大小进行返回!!!**
** }**
}
public class Test {
public static void main(String[] args) {
Student[] s = new Student[]{
new Student(18, 100, "liren"),
new Student(24, 86, "yt"),
new Student(16, 95, "lt")
};
**Arrays.sort(s);**
System.out.println(Arrays.toString(s));
}
}
// 结果:
[Student{age=16, score=95, name='lt'}, Student{age=18, score=100, name='liren'}, Student{age=24, score=86, name='yt'}]这里需要注意就是字符串的比较需要调用字符串本身实现的 ** compareTo(),而不是 ** equals(),因为 equals() 函数返回值是布尔类型,不能用做比较大小,只能比较相不相同,并且 sort() 源码也要求的是返回整型来进行比较~
compareTo()的规则是这样的:
返回 负数 → 当前对象
<参数对象 → 当前对象排在前面返回 零 → 两个对象相等 → 顺序不变
返回 正数 → 当前对象
>参数对象 → 当前对象排在后面
② Comparator
虽然上面通过实现 Comparable 接口我们实现了自定义类型的排序,但它存在一个问题:不灵活 。
因为对 compareTo() 函数进行重写,只能对其中一种类型进行排序,相当于写死了,如果我们在一个程序中需要对 Student 类进行不同字段的排序,很显然这种操作的局限性是很大的,办不到这种需求!
所以我们就要引入一个新的接口:Comparator,通常称之为「比较器 」。
Comparator 的使用是类似的,首先要实现 Comparator接口 ,然后重写 compare()方法 !
class Student {
public int age;
public int score;
public String name;
public Student(int age, int score, String name) {
this.age = age;
this.score = score;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", score=" + score +
", name='" + name + '\'' +
'}';
}
}
// 年龄比较器
public class AgeComp **implements Comparator<Student>** {
**@Override**
** public int compare(Student o1, Student o2) {**
** return o1.age - o2.age;**
** }**
}
// 分数比较器
public class ScoreComp **implements Comparator<Student>** {
**@Override**
** public int compare(Student o1, Student o2) {**
** return o1.score - o2.score;**
** }**
}
public class Test {
public static void main(String[] args) {
Student s1 = new Student("zhangsan", 18, 60);
Student s2 = new Student("lisi", 15, 99);
Student[] s = {s1, s2};
*// 使用年龄对比排序(注释部分是类似源码原理,调用compare(a, b)后返回整型与0比较)*
* *AgeComp ac = new AgeComp();
*/*if(ac.compare(s1, s2) > 0)*
* System.out.println("zhangsan大!");*
* else*
* System.out.println("lisi大!");*/*
* *Arrays.*sort*(s, ac);
System.*out*.println(Arrays.*toString*(s));
*// 使用分数对比排序*
* *ScoreComp sc = new ScoreComp();
Arrays.*sort*(s, sc);
System.*out*.println(Arrays.*toString*(s));
}
}③ Cloneable && 深浅拷贝
Clonable 用于拷贝对象 ,虽然用的少,但是面试经常问**!**

Object 类中存在一个 clone() 方法,调用这个方法可以创建一个对象的 "拷贝"。其声明如下所示:
protected native Object clone() throws CloneNotSupportedException;想合法调用 clone 方法,假设当前要拷贝的类为 Person 类型,则有以下规则:
-
让
Person实现Cloneable接口 (相当于做一个标记,告诉编译器这个类对象可以被克隆,尽管该接口是空接口) -
让
Person重写clone()方法 (因为Object中的clone()方法是protected修饰的,非Person类中要调用拷贝方法的话必须调用Person类中提供的clone(),不然会有访问权限问题) -
使用时候需要向下转型 (因为
Person类重写clone()方法后返回值仍然是Object,所以在使用clone()的时候需要强制转型为Person类型才能接收对象)
class Money {
public double money = 13.14;
}
public class Person implements Cloneable { // 实现Cloneable接口
public String name;
Money m = new Money();
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", m=" + m.money +
'}';
}
// 重写拷贝方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}注意事项:如果非 Person 类需要用到 Person 类的 clone() 方法,但没有实现 Cloneable 接口,则必须在使用了 clone() 方法的方法中处理 CloneNotSupportedException 异常,如下图所示:

但是这里就涉及到深浅拷贝的问题了,因为 Money 也是一个自定义类型,其对象也是开辟在堆上的,但是因为重写的 clone() 接口中调用的是 Object.clone(),其只做了将当前对象的资源原封不动拷贝到另一个对象中,所以 p1 和 p2 中的 Money 对象指的是同一个内存空间的对象,这就是浅拷贝 !如下面代码所示:
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person();
Person p2 = (Person)p1.clone();
System.out.println(p1.toString());
System.out.println(p2.toString());
System.out.println("==================");
p1.m.money = 20.1;
System.out.println(p1.toString());
System.out.println(p2.toString());
}
}
// 运行结果:
Person{name='null', m=13.14}
Person{name='null', m=13.14}
==================
Person{name='null', m=20.1}
Person{name='null', m=20.1}可以看到我们修改了 p1 的 money,p2 也跟着变了,这在逻辑上是不成立的,所以我们得用深拷贝来解决问题 !
首先我们也要给 Money 类实现 Cloneable 接口以及重写 clone() 方法:
public class Money implements Cloneable {
public double money = 13.14;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}然后修改一下 Person 类的 clone() 方法,借助 this 引用完成 Money 类的拷贝:
public class Person implements Cloneable { // 实现Cloneable接口
...
@Override
protected Object clone() throws CloneNotSupportedException {
Person p = (Person)super.clone(); // 先进行Person的拷贝
p.m = (Money)this.m.clone(); // 此时this指向p1,拷贝一份给p.m,才是深拷贝
return p;
}
}
// 运行结果:
Person{name='null', m=13.14}
Person{name='null', m=13.14}
==================
Person{name='null', m=20.1}
Person{name='null', m=13.14}
Ⅲ. Object 类
Object 是 Java 默认提供的一个类,被定义在 Java.lang 包中,这个包默认会自动导入的,如下所示:

在 Java 中 Object** 类是所有类的父类** ,也就是说所有类都默认会继承 Object。即所有类的对象都可以使用 Object 的引用进行接收。
所以我们要学习 Object 类中的所有方法,其实也不多,就是如下图所示的几个,也有很多是我们接触过的:

比如我们之前学过了 toString()、clone(),下面我们来学习一下 equalsI() 以及 hashCode() 方法,剩下的方法则会在后面的学习过程不断的接触!
一、对象比较的方法 -- equals()
在 Java 中,当用 == 进行比较时:
-
如果
==左右两侧是基本类型变量 ,比较的是变量中值 是否相同。 -
如果
==两侧是引用类型变量 ,比较的是引用变量地址 是否相同。
所以如果要比较对象中内容,必须重写 Object中的 equals()方法 ,因为 equals() 方法默认也是按照地址比较的,如下所示:
// Object类中的equals方法
public boolean equals(Object obj) {
return (this == obj); // 使用引用中的地址直接来进行比较
}下面还是用 Person 类为例子,重写 Person 类中的 equals() 方法,再运行看效果:
class Person {
public String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if(obj == null)
return false;
if(obj == this)
return true;
if(!(obj instanceof Person))
return false;
Person p = (Person)obj; // 需要向下转型!!!
return this.name.equals(p.name);
}
}
public class Test {
public static void main(String[] args) {
Person p1 = new Person("liren");
Person p2 = new Person("liren");
System.out.println(p1.equals(p2));
}
}
// 运行结果:
true二、获取对象内存位置的方法 -- hashCode()
主要应用场景:
-
哈希表数据结构中
-
HashMap、HashSet、Hashtable等集合类使用hashCode()来确定对象的存储位置 -
这些集合首先调用
hashCode()计算哈希值,然后根据哈希值决定对象存储在哪个桶中
-
-
对象比较优化
-
在比较两个对象是否相等时,可以先比较
hashCode() -
如果哈希值不同,可以立即确定对象不相等,避免昂贵的 **
equals()** 比较
-
-
作为对象的唯一标识
-
在某些情况下,
hashCode()可以作为对象的唯一标识符使用 -
但要注意哈希冲突的可能性
-
下面我们有个需求:我们认为两个名字、年龄相同的对象,应该是同一个对象才对,也就是在内存位置上应该是相同才对,但是如果不重写 hashCode() 方法,我们得到的两个对象的内存位置是不同的,如下所示:
class Person {
public int age;
public String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
}
public class Test {
public static void main(String[] args) {
Person p1 = new Person(18, "liren");
Person p2 = new Person(18, "liren");
System.out.println(p1.hashCode());
System.out.println(p2.hashCode());
System.out.println(p1 == p2);
}
}
// 运行结果:
1735600054
21685669
false可以看到两个对象虽然属性相同,但是内存空间是不同的,在本质上不是同一个对象!
但是如果我们重写 ** Person 类中的 ** hashCode() 方法的话就不一样了,这里我们需要借助 ** Objects 类中的 ** hash()** 方法,根据 ** Person** 类的属性来生成哈希值** ,那么相同的属性就能得到相同的哈希值,最后通过 hashCode() 拿到的就是相同的内存位置,表示它们是同一个对象!所以最后的代码如下所示:
import java.util.Objects; // 需要导入util包
class Person {
public int age;
public String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public int hashCode() {
return Objects.hash(age, name); // 通过hash()根据属性生成哈希值
}
}
public class Test {
public static void main(String[] args) {
Person p1 = new Person(18, "liren");
Person p2 = new Person(18, "liren");
System.out.println(p1.hashCode());
System.out.println(p2.hashCode());
System.out.println(p1 == p2);
}
}
// 运行结果:
102982637
102982637
false注意事项:虽然我们重写了 hashCode() 方法让其具有相同属性的对象得到相同的哈希值,来让我们觉得它们是同一个对象,但是它们在内存上实际的地址还是不同的 ,这可以通过上面代码中的 p1 == p2 来判断:无论上面的哈希值是否相同,都不能作为判断是否为同一个对象的标准,因为这个哈希值只是根据属性得到的,而不是通过内存地址得到的 !