1、什么是面向对象

1.1、面向过程

“面向过程”(Procedure Oriented)是一种以过程为中心的编程思想。即:分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用即可。

1.2、面向对象

Java的核心思想就是:面向对象编程(OOP,Object Oriented Programming)。

面向对象编程的本质就是:以类的方式组织代码,以对象的方式封装数据。

  • 类是对对象的抽象;
  • 对象是类具体的实例。

2、方法回顾和加深

2.1、方法的定义

(1)修饰符

(2)返回类型

(3)break和return的区别

  • break用来跳出循环或switch语句。
  • return用来结束方法。即:退出该函数的执行,返回到函数的调用处。

(4)方法名:驼峰命名、间名知意。

(5)参数列表:固定参数、可变参数。

(6)异常抛出

2.2、方法的调用

(1)静态方法:类名.方法名

(2)非静态方法:对象.方法名

(3)形参和实参:形式参数、实际参数

(4)值传递和引用传递:Java中都是值传递。(注意与C++的区别)

(5)this关键字

3、类与对象的创建

3.1、类与对象的关系

  • 类是一种抽象的数据类型。它是对某一类事物的整体定义(描述),但并不能代表某一个具体的事物。
  • 对象是抽象概念(类)的具体实例。

3.2、对象的创建

  • 使用new关键字创建对象。
1
类名 对象名 = new 类名();

**注:**IDEA中“Ctrl+Alt+V”可以快速实现表达式的自动补全。

自动补齐快捷键

3.3、对象的初始化

3.3.1、初始化步骤

对象的初始化步骤主要包括:

  1. 当一个对象被创建之后,虚拟机会为其分配内存
  2. 在为这些实例变量分配内存的同时,这些实例变量也会被默认初始化
  3. 如果有父类会先调用父类的构造函数
  4. 再执行该类自己的构造函数。

3.3.2、构造函数

构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值。

构造函数的主要特点:

  • 构造函数的命名必须和类名完全相同。
  • 构造函数没有返回值,也不能用void来修饰。
  • 构造函数一般定义为public方法,当一个类只定义了私有的构造函数,将无法通过new关键字来创建其对象。
  • 构造函数不能被直接调用,必须通过new关键字在创建对象时才会自动调用。
  • 当一个类没有定义任何构造函数时,编译器会为其自动生成一个默认的构造函数。
  • 默认构造函数是不带参数的。
  • 一个类可以有多个构造函数 ,即构造函数的重载。
  • 一个类一旦定义了有参构造,无参构造必须显示定义(函数体可以为空)。

:IDEA中“Alt+Insert”可以快速生成构造函数。

构造函数快捷键

3.4、内存简析

这里仅通过一个简单的例子对对象创建和初始化时的内存进行简略地分析(真正地内存结构要比这里复杂的多)。

  1. 创建一个Pet类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Pet {

    String name;
    int age;

    public void shot(){
    System.out.println("叫了一声");
    }
    }
  2. 实例化对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Application {

    public static void main(String[] args) {

    //在主函数中调用Pet类
    Pet dog = new Pet();
    dog.name = "旺财";
    dog.age = 1;
    dog.shot();
    }
    }

上述代码在执行过程中内存的管理过程如下图所示:

内存简析

程序在运算时:

  1. 首先会在方法区加载Application类,及其相关信息(如:main方法和常量“旺财”等)。
  2. 执行main方法(入栈)。
  3. 加载Pet类,及其相关信息(如:成员变量、成员函数和常量“叫了一声”等)。
  4. 在堆中新建一个对象,并对其进行默认初始化;与此同时在栈中生成一个变量名称为dog的引用,指向该对象。
  5. 通过“对象名.属性 = 属性值”修改对象的属性值。
  6. 该对象调用方法区的shot()函数。

注:

  • 类的静态成员会在类加载时在静态方法区同步加载;
  • 类的静态成员可供类的所有对象调用。
  • 方法区是一种特殊的堆。

3.5、对象的引用

对象是通过引用来操作成员变量和成员函数的。

  • 引用其实就是一个对象的别名,就是指向对象的内存地址。

    Java在访问对象的时候,不会直接访问对象在内存中的数据,而是通过引用去访问。引用也是一种数据类型,我们可以把它想象为类似 C++ 语言中指针的东西,它指示了对象在内存中的地址——只不过我们不能够观察到这个地址究竟是什么。

  • 引用也是一种数据类型。

    这种类型即不是我们平时所说的基本数据类型也不是类的实例(对象)。

  • 引用(型常量或变量)是存在栈中的,它们所指向的对象是存放在堆中的。

4、面向对象的三大特性

4.1、封装

  • 该露的露,该藏的藏。

  • 追求“高内聚,低耦合”。

    高内聚:即类的内部数据只允许在类内部使用,不允许外部干涉。

    低耦合:仅暴露少量的方法(接口)供外部使用。

  • 数据的隐藏(封装)。

    通常,我们应禁止外部直接访问类的成员变量,而应通过操作接口来访问。

    即:成员变量私有化(private),通过get、set方法来获取和设置成员变量的值。

**注:**IDEA中可以使用“Alt+Insert”快捷键快速生成get和set方法。

Alt+Insert

此外,我们常在set方法中对输入参数的安全(合法)性进行验证。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person{

int age;//年龄

public void setAge(int age) {
//对输入参数的安全(合法)性进行验证
if (age > 0 && age < 200){
this.age = age;
System.out.println("年龄设置为:" + age);
}else {
System.out.println("输入年龄不合法!");
}
}
}

4.2、继承

4.2.1、关于继承

1
2
3
class 子类 extends 父类 {

}
  • 子类(派生类)继承父类(基类)的关键字:extends(扩展)。

  • Java中的类只有单继承,没有多继承(注意与C++的区别)。

    即:一个儿子只能有一个父亲,但一个父亲可以有多个儿子。

  • 继承的本质是对某一批类的抽象,从而实现对现实世界更好地建模。

  • 继承是类和类之间的一种关系。

    除此之外,类和类之间的关系还有依赖、组合、聚合等。

4.2.2、继承的特性

  • 子类拥有父类非 private 的属性、方法。
  • 子类是不继承父类的构造函数的,它只是隐式或显式地调用父类的构造函数。
  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  • 被final修饰的类(或类的成员)不能被继承。
  • 类可以用自己的方式实现父类的方法(重写)。
  • 缺点:提高了类之间的耦合度。

4.2.3、初识Object类

  • Object类是所有类的基类。
  • 所有的类都直接或间接继承于Object类。
  • 当一个类没有显示地继承自某个类,则默认继承object类。
  • Object类在 java.lang 包中,所以不需要 import。

**注:**IDEA中可以使用“Ctrl+H”快捷键查看类的层次结构(Hierarchy),即:类的继承关系。

Hierarchy

4.2.4、super关键字

我们可以将super与this进行对比。

(1)引用类的成员

  • super关键字:父类对象的引用。我们可以通过super关键字来访问父类的成员(变量或函数)。
  • this关键字:本类对象的引用。我们可以通过super关键字来访问本类的成员(变量或函数)。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Animal {
String food = "面包";
void eat() {
System.out.println("animal eat" + this.food);
}
}

class Dog extends Animal {
String food = "骨头";
void eat() {
System.out.println("dog eat" + super.food + "和" + this.food); // this 调用本类的属性;super 调用父类的属性
}
void eatTest() {
this.eat(); // this 调用本类的方法
super.eat(); // super 调用父类方法
}
}

public class Test {
public static void main(String[] args) {
Animal a = new Animal();
a.eat();
Dog d = new Dog();
d.eatTest();
}
}

输出结果为:

animal eat面包
dog eat面包和骨头
animal eat面包

(2)引用构造函数

在类的构造函数中,我们:

  • 既可以分别使用super()方法和super(参数)方法分别调用父类的无参构造函数和有参构造函数;
  • 也可以分别使用this()方法和this(参数)方法分别调用本类的无参构造函数和有参构造函数。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.atangbiji;

public class Test {

public static void main(String[] args) {
Student student1 = new Student();
System.out.println("#######################");
Student student2 = new Student("Atang");
System.out.println("#######################");
Student student3 = new Student("Atang",18);
}
}

class Person{
public Person() {
System.out.println("父类无参构造函数执行!");
}
public Person(String name) {
this();//显示调用
System.out.println("父类有参构造函数执行!" + name);
}
public Person(String name,int age) {
this(name);//显示调用
System.out.println("父类有参构造函数执行!" + age);
}
}

class Student extends Person{
public Student() {
//默认隐藏了:super();
System.out.println("子类无参构造函数执行!");
}
public Student(String name) {
//默认隐藏了:super();
System.out.println("子类有参构造函数执行!" + name);
}
public Student(String name,int age) {
super(name);//显示调用
System.out.println("子类有参构造函数执行!" + age);
}
}

运行结果为:

父类无参构造函数执行!
子类无参构造函数执行!
#######################
父类无参构造函数执行!
子类有参构造函数执行!Atang
#######################
父类无参构造函数执行!
父类有参构造函数执行!Atang
子类有参构造函数执行!18

注:

  • 子类无参和有参构造函数在执行前默认都会先调用父类的无参构造函数。

    这是因为子类构造函数的第一行默认都隐藏了一句代码:

    1
    super();
  • 我们可以在构造函数中显示地调用super()、super(参数)、this()和this(参数)方法。

    即:显示地调用父类无参、父类有参、本类无参和本类有参构造函数。

  • 一个构造函数中不能同时调用super和this方法。

  • 一个构造函数中super()、super(参数)、this()和this(参数)方法至多只能调用一次。

  • 必须在构造函数的第一行调用super()、super(参数)、this()或this(参数)方法。

  • super必须在继承条件下才可以使用,this在没有继承的情况下也可以使用。

4.2.5、方法重写

(1)基本概念

类中如果创建了一个与父类中名称相同、参数列表相同、返回值类型相同的方法,只是方法体中的实现不同,以实现不同于父类的功能,这种方式被称为方法重写(override)

注:

  • 重写的前提是类有继承关系。
  • 方法重写指的是子类重写父类的方法
  • 重写的方法可以使用 @Override 注解来标识。
  • 构造方法不能被重写。
  • 静态(static)方法不能被重写。(因为静态方法属于类,不属于对象)
  • 被final修饰的方法不能被重写。(因为被final修饰的方法不能被继承)
  • 私有(private)方法不能被重写。(因为类的private方法会隐式地被指定为final方法)
  • 注意与重载(Overload)的区别。

(2)特点:

  • 子类重写的方法必须要和父类方法具有相同的方法名参数列表返回值类型

  • 访问权限不能比父类中被重写方法的访问权限更低(public>protected>default>private)。

    即:重写后方法的访问权限可以扩大,但不能缩小。

  • 重写后方法抛出异常的范围与父类方法抛出异常的范围相比,可以缩小,但不能扩大。

    即:重写后方法抛出的异常只能比父类少,不能比父类多。

**注:**IDEA中可以使用“Alt+Insert”快捷键快速实现方法的重写。

方法重写

(3)应用场景

当父类中的方法无法满足子类需求或子类具有特有功能的时候,需要方法重写。

4.3、多态

4.3.1、什么是多态

(1)说官话

多态性是面向对象编程的又一个重要特征,它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。

对面向对象来说,多态分为编译时多态运行时多态。其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的方法。通过编译之后会变成两个不同的方法,在运行时谈不上多态。而运行时多态是动态的,主要是指方法的重写,它是通过动态绑定来实现的,也就是大家通常所说的多态性。

我们这里所说的多态,主要指的是运行时多态。这种多态通过动态绑定(dynamic binding)技术来实现,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。也就是说,只有程序运行起来,你才知道调用的是哪个子类的方法。 这种多态通过函数的重写以及向上(父类)转型来实现。

(2)说人话

所谓多态,其实就是:我们可以让父类的引用指向子类的对象(又称“向上转型”),并使用该父类的引用调用重写的方法。即:

1
父类类型 变量名 = new 子类类型();

因此,对于一个对象而言:它的实际类型是确定的,但指向该对象的引用类型却是不确定的。我们可以既可以用本类的引用指向本类的对象;也可以用父类的引用指向子类的对象。

类似于:有朋友邀请你的家人参加他们的婚礼,请帖上写的是你父亲的名字(父类的引用),而实际却是让你或你的姊妹(子类的对象)去参加的婚礼(调用的方法)。

4.3.2、如何实现多态

  • 多态指的是方法的多态,属性是没有多态的。
  • ★多态存在的3个条件:
    • 必须有继承关系。(继承)
    • 父类的引用指向子类的对象。 (即:向上转型 )
    • 子类重写父类的方法 。(方法重写)
  • 当使用多态的方式调用方法时,程序会首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
  • 当存在多态(父类引用调用重载方法)时,父类引用会调用子类重写后的方法。
  • 多态的本质就是将子类对象赋值给父类变量,在运行时期会表现出具体的子类特征(调用子类的重写方法)。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.atangbiji;

public class Application {
public static void main(String[] args) {
Student s1 = new Student();//用本类的引用指向本类的对象
Person s2 = new Student(); //用父类的引用指向子类的对象
Object s3 = new Student(); //用父类的引用指向子类的对象

s1.run();//非多态(本类对象的引用直接调用本类方法)
s2.run();//多态(父类对象的引用调用子类重写的方法)
//s3.run();//因为Object类中没有run方法,所以s3不能直接调用
((Student) s3).run();//非多态(可以将父类型强制转换为子类型后,再调用run方法)
}
}

class Person{
public void run(){
System.out.println("父类run()方法被调用!");
}
}

class Student extends Person{
@Override //重写
public void run() {
System.out.println("子类run()方法被调用!");
}
}

运行结果:

子类run()方法被调用!
子类run()方法被调用!
子类run()方法被调用!

该示例程序中:只有s2在调用run()方法时会出现多态现象。因为父类(Person)本身存在run()方法,而子类(Student)中重写了父类的run()方法,所以当父类的引用(s2)指向子类的对象(new Student()),并使用该父类的引用调用run()方法时,程序在编译阶段并不知道该调用父类中的run()方法还是子类中的run()方法,只有在运行阶段才知道该调用的是父类中的run()方法还是子类中的run()方法。我们称这种在运行阶段才能确定该调用哪个类中方法的现象为“多态”。

由此可见:当存在多态(父类引用调用重载方法)时,父类引用会调用子类重写后的方法。

4.3.3、多态的优点

下面通过一个例子来分析多态性有哪些优点,以及我们为什么要使用多态。

多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.atangbiji;

public class Application {
public static void main(String[] args) {

Shape s = new Shape();//用本类的引用指向本类的对象
Shape s1 = new Circle();//用父类的引用指向子类的对象
Shape s2 = new Triangle();//用父类的引用指向子类的对象
Shape s3 = new Square();//用父类的引用指向子类的对象

s.draw();//非多态(本类对象的引用直接调用本类方法)
System.out.println("####################");

s1.draw();//多态(父类对象的引用调用子类重写的方法)
s2.draw();//多态(父类对象的引用调用子类重写的方法)
s3.draw();//多态(父类对象的引用调用子类重写的方法)
}
}

class Shape {
void draw() {
System.out.println("父类中计算对象面积的方法,没有实际意义,需要在子类中重写。");
}
}

class Circle extends Shape {
void draw() {
System.out.println("绘制圆形");
}
}

class Triangle extends Shape {
void draw() {
System.out.println("绘制三角形");
}
}

class Square extends Shape {
void draw() {
System.out.println("绘制矩形");
}
}

运行结果:

父类中计算对象面积的方法,没有实际意义,需要在子类中重写。
####################
绘制圆形
绘制三角形
绘制矩形

通过上述示例,我们可以发现多态主要具有如下优点:

  1. 多态对现有代码具有可替换性。例如:多态让我们只需要通过Shape类对象的引用调用draw()方法,便可实现绘制任何几何图形(如圆形、三角形、矩形等)的功能,并轻松完成各功能间的相互替换。
  2. 多态对现有代码具有可扩充性。即:增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了绘制圆形、三角形以及矩形的多态基础上,很容易添加绘制多边形的多态性。
  3. 多态让代码具有接口性。多态是父类通过方法签名,向子类提供了一个共同接口,由子类重写该方法来实现的。

5、instanceof

1
对象名 instanceof 类名
  • 它是 Java 的一个二元操作符。
  • 作用:测试左边的对象是否是右边的类的实例。
  • 返回值类型:boolean型。
    • 若该对象是该类或其子类的一个实例,则返回 true。
    • 若 该对象不是该类或其子类的一个实例,则返回 false。
    • 或该对象为null,则返回 false。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.atangbiji;

import java.util.ArrayList;

public class Application {
public static void main(String[] args) {
Object obj = new String();//多态

System.out.println(obj instanceof String);
System.out.println(obj instanceof Object);
System.out.println(obj instanceof ArrayList);
}
}

运行结果:

true
true
false

6、引用类型转换

引用类型转换的前提:有继承关系。

(1)向上转型:父类的引用指向子类的实例。

  • 低(子类)转高(父类):自动类型转换、安全,可能丢失自己本来的一些方法。
1
父类类型  变量名 = new 子类类型();

注:多态就是引用类型向上转型。

**(2)向下转型:**子类的引用不能指向父类的实例。

  • 高(父类)转低(子类):强制类型转换、不安全。
1
子类类型 变量名 = (子类类型) 父类变量名;

**注:**向下转型一旦没有注意,就会出现类型转换异常:ClassCastException。**所以在转型之前应该先使用instanceof运算符判断一下是否能进行强制转换。**如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
// 向上转型
Animal a = new Cat(); // 多态
a.eat(); // 调用的是 Cat 的 eat

// 向下转型
if (a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse(); // 调用的是 Cat 的 catchMouse
} else if (a instanceof Dog){
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse
}
}

7、Static详解

7.1、静态成员

  • 静态成员(变量/方法)为类的所有实例所共享。
  • 既可以使用对象调用静态成员,也可以使用类名调用静态成员。
  • 非静态方法可以直接调用静态成员,而静态方法只能调用静态成员。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.atangbiji;

public class Application {
public static void main(String[] args) {
Student s1 = new Student();

System.out.println(s1.name);//使用对象调用非静态变量
System.out.println(s1.age);//使用对象调用静态变量
System.out.println(Student.age);//使用类名调用静态变量

s1.Read();//使用对象调用非静态函数
s1.Write();//使用对象调用静态函数
Student.Write();//使用类名调用静态函数
}
}
class Student{
public String name;//非静态变量
public static int age;//静态变量

public void Read(){
System.out.println("非静态成员函数被调用");
}
public static void Write(){
System.out.println("静态成员函数被调用");
}
}

运行结果:

null
0
0
非静态成员函数被调用
静态成员函数被调用
静态成员函数被调用

7.2、静态代码块

(1)静态代码块和匿名代码块

1
2
3
4
5
6
7
8
9
class 类名{
{
//匿名代码块
}

static {
//静态代码块
}
}

(2)特点

  • 匿名代码块在构造函数之前执行,静态代码块在匿名代码块之前执行。

  • 静态代码只在第一次加载时执行一次。

  • 每次创建对象时都会先执行匿名代码块,再执行构造函数。

    因此,匿名代码块常用来赋初值。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.atangbiji;

public class Application {
public static void main(String[] args) {
Student s1 = new Student();
System.out.println("###################");
Student s2 = new Student();
}
}
class Student{
{
//匿名代码块
System.out.println("匿名代码块已执行!");
}

static {
//静态代码块
System.out.println("静态代码块已执行!");
}

public Student() {
System.out.println("构造函数已执行!");
}
}

执行结果:

静态代码块已执行!
匿名代码块已执行!
构造函数已执行!
###################
匿名代码块已执行!
构造函数已执行!

7.3、静态导入包

在Java 5中,import语句得到了增强,当你想调用类的静态成员时,可以使用静态导入的方式将其导入。

如:我们采用如下两种方法调用Math类中的静态成员。

(1)不使用静态导入包

1
2
3
4
5
6
7
8
9
package com.atangbiji;

public class Application {
public static void main(String[] args) {
System.out.println(Math.abs(-1));
System.out.println(Math.PI);
System.out.println(Math.max(1,2));
}
}

注: java.lang包是提供利用java编程语言进行程序设计的基础类,在项目中使用的时候不需要import。

运行结果:

1
3.141592653589793
2

(2)使用静态导入包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atangbiji;

//静态导入包
import static java.lang.Math.abs;//静态导入Math类的静态函数abs()
import static java.lang.Math.PI;//静态导入Math类的静态属性PI
import static java.lang.Math.*;//静态导入所以成员

public class Application {
public static void main(String[] args) {
System.out.println(abs(-1));
System.out.println(PI);
System.out.println(max(1,2));
}
}

运行结果:

1
3.141592653589793
2

8、抽象类与抽象方法

抽象类往往用来表征对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。

比如,在一个图形编辑软件的分析设计过程中,就会发现问题领域存在着圆、三角形这样一些具体概念。它们是不同的,但是它们又都属于形状这样一个概念。形状这个概念在问题领域并不是直接存在的,它就是一个抽象概念。而正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能够实例化的。

8.1、抽象类

被abstract关键字修饰的类叫做抽象类

  • 抽象类不能实例化(即:不能直接使用new关键字创建该类的对象),只能靠子类去实现它。

    **注:**抽象类没有对象,却有孩子。

  • 抽象类中既可以有抽象方法,也可以有普通方法

  • 抽象类必须被继承,才能被使用

  • 一个类只能继承一个抽象类。

8.2、抽象方法

被abstract关键字修饰的方法叫做抽象方法

  • 抽象方法,只有方法名,没有方法体
  • 构造方法和静态方法不能是抽象方法。
  • 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
  • 抽象类的子类必须给出抽象类中的抽象方法的具体实现(即:抽象类的子类必须重写抽象类中的抽象方法),除非该子类也是抽象类。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.atangbiji;

public class Application {
public static void main(String[] args) {
Person person = new Person();
person.say();
person.doSomething();
}
}

//抽象类
abstract class Animal{
public Animal() {
System.out.println("抽象类的构造函数已执行!");
}
public abstract void doSomething();//抽象方法(只有方法名,没有方法体)
public void say(){
System.out.println("抽象类中的非抽象方法已执行!");
}
}

//抽象类的非抽象子类
class Person extends Animal{
//抽象类的非抽象子类必须重写抽象类中的抽象方法
@Override
public void doSomething() {
System.out.println("抽象类的非抽象子类重写了抽象类中的抽象方法。");
}
}

运行结果:

抽象类的构造函数已执行!
抽象类中的非抽象方法已执行!
抽象类的非抽象子类重写了抽象类中的抽象方法。

9、接口的定义与实现

9.1、什么是接口(Interface)

接口(Interface)就是一种规范,它定义的是一组规则。就像一个国家的法律一样,一旦制定好后大家必须遵照执行。

9.2、接口的特点

  • 在JAVA编程语言中接口是一种抽象类型,它是抽象方法的集合
  • 接口并不是类,但编写接口的方式和类很相似。
  • 接口通常以interface来声明。
  • 接口不能被实例化,但是可以被实现(implements)
  • 接口不是被类继承了,而是要被类实现
  • 一个实现接口的类,必须实现(重写)接口内所描述的所有方法,否则就必须声明为抽象类。
  • 接口没有构造方法
  • 接口中所有的方法必须是抽象方法
  • 接口中每一个方法是隐式抽象的。即:
    • 接口中的方法都是公有的。
    • 接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
    • 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
  • 接口中的变量是公共的不能被继承的静态变量。即:
    • 接口中可以含有变量。
    • 但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
  • 一个接口能继承(extends)另一个接口,且接口支持多继承
    • 在Java中,类的多继承是不合法,但接口允许多继承。

9.3、接口与抽象类的区别

  • 抽象类中的方法可以有方法体,而接口中的方法都没有方法体。
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。

9.4、接口的定义与实现

接口的声明语法格式如下:

1
2
3
4
[修饰符] interface 接口名称 [extends 其他的接口名] {
// 声明变量(公共的不能被继承的静态变量)
// 抽象方法(只有方法名,没有方法体)
}

接口实现类使用implements关键字实现接口。

1
2
3
[修饰符] class 类名 implements 接口名称1[, 接口名称2, 接口名称3, ...]{
//重写接口内所描述的所有方法
}

注:Java中一个类只能继承一个类,但一个类可以同时实现多个接口

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.atangbiji;

public class Application {
public static void main(String[] args) {
//使用接口的实现类
UserServiceImpl user = new UserServiceImpl();
user.add("ATang");
user.delete("ATang");
user.update("ATang");
user.query("ATang");
user.time();
}
}
//定义接口
interface UserService{
//声明变量(接口中的变量会被隐式的指定为:public static final,所以必须赋初值)
int age = 0;
//抽象方法(接口中的方法会被隐式的指定为:public abstract)
void add(String name);
void delete(String name);
void update(String name);
void query(String name);
}
interface TimeService{
//抽象方法
void time();
}

//实现接口(接口都必须用一个类来实现,一个类可以同时实现多个接口)
class UserServiceImpl implements UserService,TimeService{//

//接口实现类必须重写(实现)接口内所描述的所有方法
//重写UserService接口中的方法
@Override
public void add(String name) {
System.out.println("新增用户!");
}

@Override
public void delete(String name) {
System.out.println("删除用户!");
}

@Override
public void update(String name) {
System.out.println("修改用户!");
}

@Override
public void query(String name) {
System.out.println("查询用户!");
}

//重写TimeService接口中的方法
@Override
public void time() {
System.out.println("开始计时……");
}
}

运行结果:

新增用户!
删除用户!
修改用户!
查询用户!
开始计时……

9.5、接口的继承

一个接口能继承(extends)另一个接口,这和类之间的继承比较相似。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 文件名: Sports.java
public interface Sports
{
public void setHomeTeam(String name);
public void setVisitingTeam(String name);
}

// 文件名: Football.java
public interface Football extends Sports
{
public void homeTeamScored(int points);
public void visitingTeamScored(int points);
public void endOfQuarter(int quarter);
}

Football接口自己声明了3个方法,从Sports接口继承了2个方法,这样,实现Football接口的类需要实现5个方法。

9.6、接口的多继承

Java中类是不允许多继承的,但接口允许多继承(即:一个子类只能继承一个父类,但一个子接口可以继承多个父接口)。

1
2
3
4
修饰符 interface 子接口 extends 父接口1, 父接口2,……{
// 声明变量(公共的不能被继承的静态变量)
// 抽象方法(只有方法名,没有方法体)
}

9.7、标记接口

没有任何方法和属性的接口被称为标记接口

如:java.awt.event 包中的 MouseListener 接口继承的 java.util.EventListener 接口定义如下:

1
2
3
package java.util;
public interface EventListener
{}

标记接口作用:简单形象的说就是给某个对象打个标(盖个戳),使对象拥有某个或某些特权。它仅仅表明它的类属于一个特定的类型,供其他代码来测试允许做一些事情。

10、内部类

**内部类就是在一个类(或方法)的内部再定义一个类。**如:在A类中定义一个B类,那么B类就是A类的内部类,A类就是B类的外部类。

10.1、成员内部类

成员内部类:在外部类的成员位置定义的内部类。一般定义格式为:

1
2
3
4
5
class A{
class B{

}
}
  • 内部类对象必须通过外部类对象来创建
  • 成员内部类可以无条件访问外部类的属性和方法(包括private成员和静态成员)。
  • 外部类想要访问内部类属性或方法时,必须要创建一个内部类对象,然后通过该对象访问内部类的属性或方法。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.atangbiji;

public class Application {
public static void main(String[] args) {
Outer outer = new Outer();//新建一个外部类对象
Outer.Inner inner = outer.new Inner();//通过外部类对象创建内部类对象

outer.out();
inner.in();
System.out.println("################");
outer.getInner();
inner.getID();
}
}

//外部类
class Outer{
private int id = 10;
public void out(){
System.out.println("外部类方法执行!");
}
//外部类访问内部类属性和方法
public void getInner(){
Inner inner = new Inner();
System.out.println(inner.name);
inner.in();
}
//内部类
class Inner{
private String name = "ATang";
public void in(){
System.out.println("内部类方法执行!");
}
//内部类访问外部类属性和方法
public void getID(){
System.out.println(id);
out();
}
}
}

执行结果为:

外部类方法执行!
内部类方法执行!
################
ATang
内部类方法执行!
10
外部类方法执行!

注:当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象。即默认情况下访问的是成员内部类的成员。若要访问外部类的同名成员,则需要使用下面的方式进行访问:

1
2
外部类.this.成员变量
外部类.this.成员方法

附:成员内部类的访问权限

成员内部类前可加如下四种访问修饰符:

  • public:所有类可访问。
  • private:仅外部类可访问。
  • protected:同包下或继承类可访问。
  • default:同包下可访问。

10.2、静态内部类

一般定义格式为:

1
2
3
4
5
class A {
static class B {

}
}
  • 静态内部类和成员内部类相比多了一个static修饰符。
  • 与类的静态成员变量一样,静态内部类是不依赖于外部类的。
  • 静态内部类不能调用外部类的非静态变量与方法。
  • 静态内部类对象不必通过外部类对象来创建。

注:

静态内部类对象的创建一般是:外部类.内部类 类名 = new 外部类.内部类();
成员内部类对象的创建一般是:外部类.内部类 类名 = 外部类对象名.new 内部类();

10.3、局部内部类

局部内部类:定义在一个类的成员函数(或作用域)中的类。它在实际开发中用的并不多,并不常见,了解即可。

一般定义格式为:

1
2
3
4
5
6
7
class A{
public void f(){
class B{

}
}
}
  • 局部内部类类似于方法中的局部变量,是不能被 public、protected、private 和 static 修饰的。
  • 局部内部类的访问仅限于它所在的成员函数(或作用域)内。
  • 局部内部类的实例只能在它所在的成员函数(或作用域)中创建,我们无法在外部创建局部内部类的实例对象。因为局部内部类是定义在方法中的,而方法是需要所在类的对象去调用。
  • 局部内部类只能访问它所在的成员函数(或作用域)中声明的final类型的变量。

10.4、匿名内部类(重点)

匿名内部类:没有名字的内部类,必须在创建时使用 new 语句来声明。其语法形式如下:

1
2
3
new <类名或接口名>() {

};

10.4.1、为什么引入匿名内部类?

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Application {
public static void main(String[] args) {
Person p = new Child();
p.eat();

UserServiceImpl u = new UserServiceImpl();
u.setName();
}
}

//父类
class Person {
public void eat(){
System.out.println("父类成员函数执行!");
}
}
//子类
class Child extends Person {
public void eat() {
System.out.println("子类成员函数执行!");
}
}

//接口
interface UserService{
public void setName();
}
//接口实现类
class UserServiceImpl implements UserService{
@Override
public void setName() {
System.out.println("接口实现类成员函数执行!");
}
}

运行结果:

子类成员函数执行!
接口实现类成员函数执行!

如代码所示,我们用Child类继承了Person类,然后实现了Child的一个实例,并将其向上转型为Person类的引用;用UserServiceImpl类实现了UserService接口。需要注意的是:如果此处的Child类和UserServiceImpl类只使用一次,那么将其编写为独立的一个类岂不是很麻烦?因此,我们便引入了匿名内部类。

结论:当一个类继承了一个父类或者实现了一个(或多个)接口且该类只被使用一次时,使用匿名内部类便可以省略一个类的书写,进而简化代码。

10.4.2、本质

匿名内部类的本质:是一个继承了一个父类但没有名字的子类,或者是实现了一个(或多个)接口但没有名字的实现类。

10.4.3、两种使用方式

  1. 继承一个类,重写其方法。
  2. 实现一个接口(可以是多个),实现其方法。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.atangbiji;

public class Application {
public static void main(String[] args) {
//一般类的匿名对象调用其成员方法
new People().eat();
//匿名内部类实现方式一:(等价于继承了一个父类的子类)
People p = new People() {
@Override
public void eat() {
System.out.println("继承一个类,重写其方法。");
}
};
p.eat();
//匿名内部类实现方式二:(等价于一个或多个接口的实现类)
UserService u = new UserService() {
@Override
public void setName() {
System.out.println("实现一个接口(可以是多个),实现其方法。");
}
};
u.setName();
}
}

//父类
class People{
public void eat(){
System.out.println("父类成员函数已执行!");
}
}

//接口
interface UserService{
public void setName();
}

运行结果:

父类成员函数已执行!
继承一个类,重写其方法。
实现一个接口(可以是多个),实现其方法。

如代码所示,我们直接将子类(或接口实现类)中的方法在大括号中实现了,这样便可以省略一个类的书写。

**结论:**由上面的例子可以看出,对于一个父类或一个接口,其子类(或实现类)中的方法都可以使用匿名内部类来实现。

注:

  • 匿名内部类在创建的同时会返回该子类或接口实现类的对象。
  • 正因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码编写。
  • 匿名内部类是唯一没有构造函数的内部类。
  • 匿名内部类只能访问外部类的final变量。
  • 匿名内部类是平时我们编写代码时使用最多的内部类。

附:内部类的好处

  1. 完善了Java多继承机制,由于每一个内部类都可以独立的继承接口或类,所以无论外部类是否继承或实现了某个类或接口,对于内部类没有影响。
  2. 方便写事件驱动程序。

(本讲完,系列博文持续更新中…… )

阿汤笔迹微信公众平台

关注**“阿汤笔迹”** 微信公众号,获取更多学习笔记。
原文地址:http://www.atangbiji.com/2020/12/20/oop
博主最新文章在个人博客 http://www.atangbiji.com/ 发布。