
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) 2. 「变量」的访问控制符:
public static finalclass 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 是 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));
}
}
// 运行结果:
truehashCode()主要应用场景:
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 来判断:无论上面的哈希值是否相同,都不能作为判断是否为同一个对象的标准,因为这个哈希值只是根据属性得到的,而不是通过内存地址得到的!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。