Java 面向对象

Java 是纯粹的面向对象编程语言,提供了类、接口和继承等特性。

一、理解面向对象

编程思想 面向过程、结构化和面向对象

二、Java 中的面向对象

1. 一切都是对象

在 Java 语言中,除了 8 个基本数据类型以外,一切都是对象。

对象具有状态,Java 中一个对象通过它的成员变量数值来描述它的状态;对象具有行为,Java 中一个对象通过它的方法来描述它的行为。对象把数据和对数据的操作封装成一个有机的整体,实现了数据和操作的结合。

Java 语言中使用 class 关键字定义类,在类中描述对象的属性及方法。定义了类以后,用 new 关键字来创建类的实例–对象。

2. 类与对象

类是具有相同属性和方法的一组对象的集合,对象的抽象化是类,类的具体化就是对象。

类是对一类事物的描述,是抽象的、概念上的定义;对象是实际存在的属于某一类的个体,也被称为实例。

3. 类与类

类与类之前有以下几种关系:

  • 无关

  • 一般-特殊:又称为继承关系,在 Java 中,通常用 子类 extends 父类 来表示。

    1
    2
    3
    4
    5
    6
    7
     >class 水果 {
    ···
    }

    class 香蕉 extends 水果 {
    ···
    }
  • 整体-部分:又称为组装关系。在 Java 中,通常在一个类里保存另一个对象的引用来实现这种组合关系。

    1
    2
    3
    4
    5
    6
    7
    8
     >class 球员 {
    ···
    }
    class team {
    球员 球员1;
    球员 球员2;
    球员 球员3;
    }

三、类与对象

1. 定义类

1
2
3
4
5
6
修饰符 class 类名{
零到多个初始化块;
零到多个构造器;
零到多个变量;
零到多个方法;
}

(1) 修饰符

修饰符可以省略,也可以是 public|final|abstract

(2) 初始化块

1
2
3
[static] {
···
}

用于初始化对象或类。

(3) 构造器

1
2
3
修饰符 类名(形参列表){
···
}
  • 修饰符可以省略,也可以是 public|protected|private
  • 构造器用于创建某一类的对象
  • 如果没有编写构造器,则系统会为该类设置一个默认的构造器

(4) 变量

1
修饰符 类型 变量名 [= 默认值];
  • 修饰符可以省略,也可以是 [public|protected|private] + [static] + [final]
  • 类型可以是基本类型,也可以是引用类型
  • 定义成员变量时可以为其指定一个默认值

(5) 方法

1
2
3
修饰符 返回值类型 方法名(形参列表){

}
  • 修饰符可以省略,也可以是 [public|protected|private] + [static] + [final|abstract]
  • 返回值类型可以是 void|基本类型|引用类型

2. 创建对象

创建对象的根本途径是构造器,通过 new 来调用某个类的构造器即可创建对象。

1
类名 对象名 = new 类名(实参列表);

3. 对象和引用

(1) 对象和引用

1
Person p = new Person();

在这行代码中,实际产生了两个对象:p 变量和 Person 对象。

p 变量存储于栈中,是一个引用变量,指向位于堆内存中的 Person 对象。

(2) 多个引用

因此,也可以使多个引用变量指向同一个对象,共同管理:

1
Person p2 = p;

(3) 垃圾回收

如果堆内存中的对象不被任何变量指向,则程序无法再访问它,因此它也变成了垃圾,Java 的垃圾回收器将会在适当的时候回收释放它。

如果希望系统回收某个对象,只需要切断对象的引用变量跟它的联系即可。

4. this

(1) 什么是 this

Java 提供 this 关键字,它表示对象本身。

(2) 示例 方法中访问类

1
2
3
4
5
6
7
8
9
10
11
12
public class dog{
// 定义一个"吠"方法
public void bark(){
System.out.println("汪!");
}
// 定义一个"狂吠"方法,该方法需要借助"吠"方法
public void barkbarkbark(){
for (int i = 0; i < 3; i++){
this.brak();
}
}
}

Java 允许省略 this.

  • 如果调用类成员,则使用类作为主调
  • 如果调用实例成员,则使用对象本身作为主调

类方法无法访问实例成员,因为没有明确的调用对象,无法使用 this 。

虽然 Java 允许使用对象调用类成员,但这是不应该的,应尽量避免这样做。不要用对象调用类成员

(3) 示例 构造函数

this 可以在构造函数中使用,用于表示正在构造的对象。

(4) 示例 同名变量

若方法中有局部变量与成员变量同名,此时便可以为成员变量加上 this. ,以防混淆。

四、方法

1. 方法的定义

1
2
3
4
修饰符 返回值类型 方法名(参数类型 参数名){
方法体
return 返回值;
}

2. 方法的调用

1
方法名(参数)

3. 方法不能独立

  • 方法不能独立定义,只能在类中定义
  • 方法不能独立存在,要么属于类,要么属于对象
  • 方法不能独立执行,要么由类调用,要么由对象调用

4. 参数传递

传递方式是:单向传递,值传递

需要注意的是:

  • 传递基本类型,将实参的值(也就是数据)拷贝给形参,方法中对变量的改动将不会影响原来的变量

  • 传递引用类型,将参数的值(也就是数据的地址)拷贝给形参,方法中对引用变量的改动同样不会影响原来的引用变量,但能通过引用变量对引用对象做出修改

    可以理解为:

    将地址传递进方法中,虽然对地址的修改仅在方法中有效,但却可以顺着指针对其指向的内存做修改。

5. 方法重载

方法重载是指:

  • 多个方法在同一个类中
  • 多个方法具有相同的方法名
  • 多个方法的参数不同(类型或数量不同)

Java 会根据传入参数的数量与类型,自动选择对应的方法并执行。

6. 形参个数可变

(1) 定义

Java 允许为方法指定数量不确定的形参。如果在定义方法时,在最后一个形参的类型后增加 ,则表示该形参可以接受多个参数值,多个参数值被当作数组传入。

1
2
3
修饰符 返回值类型 方法名(形参列表, 数据类型... 形参名){

}

(2) 调用

1
方法名(实参列表, 形参1, 形参2, ···, 形参n)

例如:

1
2
3
4
5
6
7
8
9
10
public static void test(int a, String... words){
System.out.println(a);
for (var tmp : words){
System.out.println(tmp);
}
}

public static void main(String[] args){
test(5, "Hello", "World", "!");
}

可以在调用函数时传入任意个数的参数值,在方法中,会将它们当作数组处理。

(3) 与数组的对比

  • 定义:

    形参个数可变的方法:

    1
    public static void test1(int a, String... words)

    含数组的方法:

    1
    public static void test2(int a, String[] words)
  • 调用:

    形参个数可变的方法:

    1
    test1(5, "Hello", "World", "!");

    含数组的方法:

    1
    test2(5, new String[]{"Hello", "World", "!"});
  • 个数可变的形参只能出现在形参表的末尾,且一个方法只能有一个;

    数组形参可以出现在任意位置,且可以有任意个。

五、构造器

1. 什么是构造器?

构造器是一种特殊的方法,用于创建实例时执行初始化,它是创建对象的重要途经。

即使是使用工厂模式、反射等方式创建对象,其本质依然是依赖于构造器

2. 默认构造器

  • 如果没有为类设置构造器,系统会自动为类设置一个无参数、无动作的构造器;

  • 如果为类设置了构造器,则系统不再提供默认的构造器

3. 自定义构造器

如果希望改变默认的初始化,便可以通过自定义构造器来实现:

1
2
3
4
5
6
7
8
9
public class 类名 {
数据类型 成员变量;
public 类名() {
this.成员变量 = 默认值;
}
public 类名(数据类型 形参) {
this.成员变量 = 形参;
}
}

4. 构造器重载

Java 允许同一个类中由多个构造器,只要多个构造器的参数不同(类型或数量不同)即可。

5. 构造器的代码复用

可以在构造器中使用 this.构造器(参数) 调用同一个类下的零一个构造器,从而实现代码复用。但有两个限定条件:

  • 构造器只能调用构造器,不能调用方法
  • 调用其他构造器的语句需要放在构造器中的开头位置

六、成员变量和局部变量

1. 成员变量和局部变量

(1) 成员变量

成员变量指在类中定义的变量,可以被分为:

  • 类变量:
    • 用 static 修饰
    • 属于类
    • 生存期和作用域与类相同
    • 能够通过类和实例访问(推荐通过类)
  • 实例变量:
    • 不用 static 修饰
    • 属于实例
    • 生存期和作用域与实例相同
    • 仅能通过实例访问

(2) 局部变量

局部变量指在方法中定义的变量,可以被分为:

  • 形参:生存期和作用域与方法相同
  • 方法局部变量:生存期和作用域从定义的位置到方法结束
  • 代码块局部变量:生存期和作用域从定义的位置到代码块结束

2. 内存中成员变量

  • 类变量只会有一份,所有的实例共用;

    实例变量每个实例各有一份,每个实例单独拥有一份。

  • 第一次创建实例时,系统分配类变量和实例变量的空间;

    此后再创建同类实例时,系统仅分配实例变量的空间。

3. 内存中的局部变量

局部变量无需系统垃圾回收,往往会随着方法或代码块的运行结束而结束。

4. 变量使用规则

如果仅就程序的运行结果来看,大部分时候都可以直接使用成员变量来解决问题,无须使用局部变量。但实际上这种方法有很多坏处:

  • 扩大了变量的生存时间,导致更大的内存开销
  • 扩大了变量的作用域,不利于程序的内聚性

如果有以下几种情形,应考虑使用成员变量:

  • 当某属性由一个类共有,且同类属性值相同时,用类变量
  • 当某属性由一个类共有,且各个实例有各自的属性值时,用实例变量
  • 当某个信息需要在类的多个方法之间进行共享时,应设为成员变量

七、初始化块

1. 初始化块

1
2
3
4
5
6
class 类名 {
[static] {
···
}
···
}

初始化块用于初始化对象或类,其代码中可以包含任意可执行语句。

2. 执行顺序

在 Java 创建一个对象后,

  • 系统会首先加载类(如果类此前没有被加载的话)

  • 然后为该对象的所有实例变量分配内存

  • 接着执行初始化顺序

    其中,初始化块声明变量时指定初始值的执行顺序与它们在代码中的顺序相同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class Test {
    int a = 6;
    {
    a = 5;
    }

    {
    b = 5;
    }
    int b = 6;
    void fun() {
    System.out.println(a);
    System.out.println(b);
    }

    public static void main(String[] args) {
    Test test = new Test();
    test.fun();
    }
    }

    a 的声明变量时指定的初始值实例初始化块之前,因此实例初始化块后执行,变量被先赋 6,再赋 5;

    b 的声明变量时指定的初始值实例初始化块之后,因此声明变量时指定的初始值后执行,变量被先赋 5,再赋 6;

2. 实例初始化块

不加修饰符的初始化块便是实例初始化块。

实例初始化块总是在构造器执行之前执行,可以使用实例初始化块来进行对象的初始化操作。

1
2
3
4
5
6
7
8
9
10
11
12
public class Test2 {
{
System.out.println("实例初始化块");
}
public Test2() {
System.out.println("构造器");
}

public static void main(String[] args) {
Test2 test2 = new Test2();
}
}

需要注意的是:

在编译 Java 文件之后,实例初始化块会被”还原”到构造器之中,并且位于构造器的最前面。

3. 类初始化块

使用 static 修饰的初始化块便是类初始化块。

类初始化块在类初始化阶段执行,用于对整个类进行初始化处理。

八、封装

1. 封装

封装是面向对象的三大特征之一。将成员变量隐藏在对象内部,不允许外部直接访问 ,仅能通过该类提供的方法来实现对成员变量的操作与访问。

通过封装,可以:

  • 隐藏类的实现细节
  • 限制不合理访问
  • 进行数据检查,从而保证对象信息的完整性
  • 便于修改,提升代码可维护性

2. 访问修饰符

Java 符号

3. 常用做法

  • 将变量用 private 修饰,以限制外界的访问

  • 如果变量不需要被外部使用,无需额外处理,仅在内部调用它即可

  • 如果变量需要被外部使用,提供 getXXX()setXXX() ,可以通过方法中的代码逻辑保护成员变量。

    使外界仅能得到大致的范围,而无法得到具体数据:

    1
    2
    3
    4
    5
    6
    7
    8
    public String getSalary(String name) {
    ···
    if (salary >= 5000) {
    return "high";
    }else {
    return "medium"
    }
    }

    阻止对数据的不当修改:

    1
    2
    3
    4
    5
    6
    7
    public void setAge(String name, int age) {
    ···
    if (age >= 150 || age < 0) {
    return;
    }
    ···
    }

九、继承

1. 继承的概念

继承就是子类继承父类的特征和行为,使得子类对象具有父类的变量与方法。

2. 继承的格式

1
2
3
4
5
6
class 父类 {

}
class 子类 extends 父类 {

}

3. 继承的特征

  • 继承后,类将会获得父类的(非 private)属性与方法,但不包括构造器

  • 每个类只能有一个直接父类

    • 类只能继承于一个父类
    • 类的父类可以继承于其它类,类会获得”父类的父类”的属性与方法
  • 子类可以重写父类的方法,覆盖父类的属性

  • 继承的使用提高了类之间的耦合性

  • 类的最上层父类是 Java.lang.Object

4. 方法重写

方法重载:名字相同但参数不同的多个函数

方法重写:在子类中重写父类的方法

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
···
void calls {
嗷呜
}
}
class Dog extends Animal {
···
void calls {
汪汪汪
}
}

方法重写,即在子类中重写父类的方法。需要遵守“两同两小一大”规则:

  • 方法名相同

  • 参数相同

  • 子类方法返回值类型应该更小或相等

  • 子类方法声明抛出的异常类应该更小或相等

  • 子类方法的访问权限应该更大或相等

类方法可以重写,但没有多态性。

5. 变量覆盖

(1) 什么是变量覆盖?

如果子类中定义了和父类成员变量同名的成员变量,则父类中的成员变量将会被覆盖。

(2) 实例变量

可以通过 super 访问被覆盖的实例变量。

(3) 类变量

如果父类的类变量被覆盖,并且希望访问它,可以通过 父类名.变量名 进行访问。

6. super

(1) 访问被重写的方法

可以在子类的方法中使用 super 调用父类中被重写的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal {
···
void calls {
嗷呜
}
}
class Dog extends Animal {
···
void calls {
汪汪汪
}
void fatherCalls {
super.calls();
}
}

直接使用 方法名() 将访问子类重写的方法,

在方法中使用 super.方法名() ,将可以调用父类的方法。

(2) 访问被覆盖的变量

可以在子类的方法中使用 super 调用父类中被覆盖的变量。

1
2
3
4
5
6
7
8
9
10
class 父类 {
int a;
}
class 子类 extends 父类 {
int a;
void fun () {
this.a; // 访问子类实例中的变量
super.a; // 访问父类中被覆盖的变量
}
}

7. 构造器

  • 子类不会获得父类的构造器,但在子类构造器中必然调用父类构造器进行初始化
  • 默认情况下,子类构造器会隐式调用父类的无参构造器
  • 如果父类没有无参构造器,或者希望执行父类的带参构造器,应该在子类构造器的开头使用 super(参数) 调用父类的构造器
  • 创建任何 Java 对象时,都会首先执行 Java.lang.Object 类的构造器,并按继承关系依次向下执行构造器

8. 注意点

继承带来高度复用的同时,也破坏了父类的封装性。当子类继承父类之后,子类可以任意访问父类的变量和方法,可以重写和覆盖父类的变量和方法。为了保证父类具有良好的封装性,不会被子类随意改变,应注意以下几点:

  • 尽量隐藏父类的变量

  • 尽量隐藏父类的方法

  • 不要在父类的构造器中调用会被子类重写的方法

    防止因为方法篡改之后,实例化子类时出现异常

此外,应当在必要时才派生子类,派生时应具备以下条件:

  • 需要增加变量而不是改变变量值
  • 需要增加方法或改变方法

十、组合

1. 什么是组合?

如果需要复用一个类,除了继承之外,还可以通过组合实现。

具体做法是,将 A 类的对象作为 B 类的成员,通过调用该对象来实现代码的复用。

2. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Head {
void speak() {
System.out.println("我会说话");
}
}
public class Man {
Head head = new Head();
void speak() {
head.speak();
}
}


public static void main(String[] args) {
Man man = new Man();
man.speak();
}

3. 继承与组合

  • 继承:表达的是一种”是”的关系,将原有的类进行改造,实现代码复用
  • 组合:表达的是一种”有”的关系,直接利用原有的类,实现代码复用

十一、多态

1. 编译时类型和运行时类型

Java 引用类型有两个类型:

  • 编译时类型:声明该变量时使用的类型

  • 运行时类型:实际赋给变量的对象类型

当编译时类型与运行时类型不一致时,就可能出现多态。

2. 多态的定义

相同类型的变量,在调用同一个方法时,呈现出多种不同的行为特征。

3. 多态的条件

  • 有继承/实现关系
  • 有方法重写
  • 引用类型变量的编译时类型为父类,运行时类型为子类

4. 只有实例方法才具有多态性

类方法和实例方法都可以重写,但类方法没有多态性。

在 Java 中,多态的实现原理是:变量属于父类,但根据自己实际指向的子类实例调用对应的方法。

对于实例方法,子类实例具有自己的实例方法,这个方法在子类中定义;

对于类方法,虽然子类也有自己的类方法,但因为变量属于父类,调用类方法时将会调用父类的类方法。

5. 多态调用方法

首先检查父类中是否有对应方法,如果有,则继续执行,如果没有,则编译错误;之后在子类中调用同名方法。

即,可以访问子类中与父类同名的方法。

引用变量在编译阶段只能调用其编译时类型具有的方法,当运行时执行其运行时类型所具有的方法。因此,多态只能调用父类中存在的方法。

6. 多态调用变量

首先检查父类中是否有对应变量,如果有,则取得该变量,如果没有,则编译错误。

即,无法访问子类中的变量。

7. 强制类型转换

如果希望调用子类中的“特有”方法或子类中的变量,可以通过强制类型转换来实现,并且为了保证代码的健壮性,可以使用 instanceof 进行预先判断。

1
2
3
4
5
6
7
if (对象 instanceof 子类) {
···
((子类)对象).子类方法
((子类)对象).子类变量
子类 temp = (子类)对象
···
}

8. 多态示例

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
// 父类Animal
public class Animal {
String name = "Animal";

void eat() {
System.out.println("动物什么都吃");
}
}

// 子类Dog
public class Dog extends Animal{
String name = "Dog";

@Override
void eat() {
System.out.println("狗吃骨头");
}
void call() {
System.out.println("汪汪汪");
}
}

// 子类Cat
public class Cat extends Animal{
String name = "Cat";

@Override
void eat() {
System.out.println("猫吃鱼");
}
}

// 测试类
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
Animal dog = new Dog();
Animal cat = new Cat();
// 非多态
System.out.println("---animal---");
System.out.println(animal.name);
animal.eat();
// 多态
System.out.println("---dog---");
System.out.println(dog.name);
dog.eat();
// dog.call(); // 因为父类中不存在call(),因此无法调用
((Dog)dog).call();
System.out.println("---cat---");
System.out.println(cat.name);
cat.eat();
}
}

十二、抽象类

1. 什么是抽象类?

抽象类只描述类型的基本特征与功能,具体如何实现由子类完成。

2. 抽象方法与抽象类

  • 抽象方法和抽象类都用 abstract 定义
  • 如果类中存在抽象方法,则类必须定义为抽象类
  • 抽象类无法被实例化
  • 没有抽象类方法、没有抽象变量、没有抽象构造器
  • abstract 不能和 final、static、private 同时使用

3. 语法

1
2
3
4
5
abstract class 类名 {
···
abstract 返回值类型 方法名();
···
}

4. 抽象类示例

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
// 抽象父类
public abstract class Animal {
private String name;
private int age;

public abstract void eat();

// get/set方法

// 构造方法

// 重写toString()
}

// 子类
public class Cat extends Animal{

public Cat() {
}

public Cat(String name, int age) {
super(name, age);
}

@Override
public void eat() {
System.out.println("猫吃鱼");
}
}

// 测试类
public class Test {
public static void main(String[] args) {
Animal cat = new Cat("喵喵", 1);
System.out.println(cat);
cat.eat();
}
}

十三、接口

1. 什么是接口?

在 Java 中,接口是一系列方法的声明,这些方法应该被类实现。

接口定义了规范,它不关心类的具体实现,只是单纯规定类必须提供某些方法。

2. 接口的特性

  • 无法被实例化

  • 如果类实现接口,则它必须实现接口描述的所有方法

    如果抽象类实现接口,则它无需实现接口所描述的方法,但它的子类也必须实现接口描述的所有方法

  • 可以包含方法,方法会被隐式指定且只能指定为 public abstract

    可以包含变量,变量会被隐式指定且只能指定为 public static final

  • 一个类可以实现多个接口

JDK 1.8 :

  • 接口中可以有类方法,类方法中可以有具体代码

  • 接口可以有”默认方法”,该方法中可以有具体代码,用 default 修饰

    默认方法可以看作是实例方法,只不过需要加上 default 修饰符

JDK 1.9:

  • 方法可以定义为 private,使得某些方法可以被隐藏

3. 接口中的成员

  • 公共类常量
  • 公共抽象方法
  • 类方法 [ JDK 1.8 ]
  • 默认方法 [ JDK 1.8 ]
  • 私有方法 [ JDK 1.9 ]

4. 接口与类的区别

  • 接口不能实例化

    类可以实例化

  • 接口中的所有方法必须是公共抽象方法,所有变量必须是公共类常量

    类并无限制

  • 接口没有构造方法

    类有构造方法

5. 接口与抽象类的区别

  • 接口中只能有抽象方法

    抽象类中可以有”具体方法”

  • 接口中的变量只能是公共类变量

    抽象类并无限制

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

    一个类可以实现多个接口

  • 接口没有构造方法

    抽象类有构造方法

  • 接口是对行为的抽象

    抽象类是对事物的抽象

  • 接口的含义:有什么能力

    抽象类的含义:是什么东西

  • 接口体现的是规范,规定了实现者需要向外界提供的服务

    抽象类体现的是模块化设计,建立模块后由子类进行完善

6. 声明接口

1
2
3
4
[public] interface 接口名 [extends 其他的接口名] {
// 变量
// 方法
}

接口能够继承其它接口

7. 使用接口

(1) 定义变量

接口可以用于声明引用类型变量,它必须指向实现了该接口的对象。

(2) 强制类型转换

如果一个类同时实现了多个接口,则它的编译时类型既可以是 A 接口类型,也可以是 B 接口类型。

如果希望把编译时类型为 A 接口变量的实例转换为 B 接口,可以使用强制类型转换。

实例如下:

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
// Fly接口
public interface Fly {
void fly();
}

// Run接口
public interface Run {
void run();
}

// 实现了两个接口的类
public class Duck implements Fly,Run{
@Override
public void fly() {
System.out.println("我可以飞");
}

@Override
public void run() {
System.out.println("我可以跑");
}
}

// 测试类
public class Test {
public static void main(String[] args) {
Fly duck = new Duck();
duck.fly();
((Run)duck).run();
}
}

(3) 调用常量

可以通过 接口名.常量名 调用接口中声明的公共类变量。

(4) 被其它类实现

1
2
3
class 类名 implements 接口名 {
// 实现接口的方法
}

8. 接口示例

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
60
// 接口Action
public interface Action {
void jump();
}

// 继承自Action的AnimalAction接口
public interface AnimalAction extends Action {
void stand();
}

// 抽象父类
public abstract class Animal {
private String name;
private int age;

public abstract void eat();

// get/set方法

// 构造方法

// 重写toString()
}

// 子类
public class Cat extends Animal implements AnimalAction{

public Cat() {
}

public Cat(String name, int age) {
super(name, age);
}

@Override
public void eat() {
System.out.println("猫吃鱼");
}

@Override
public void jump() {
System.out.println("跳跳跳");
}

@Override
public void stand() {
System.out.println("乖乖站着");
}
}

// 测试类
public class Test {
public static void main(String[] args) {
Cat cat = new Cat("喵喵", 1);
System.out.println(cat);
cat.eat();
cat.jump();
cat.stand();
}
}

注意:

之所以没有用 Animal cat = new Cat() 的原因是:

Cat 既继承了 Animal 类,又实现了 AnimalAction 接口,拥有最多的方法,而 Animal 类并没有实现对应接口,缺失了方法。

因此直接使用 Cat 类更为方便。

9. 函数式接口

函数式接口就是一个有且仅有一个抽象方法的接口,可以通过 @FunctionalInterface 进行注解。

1
2
3
4
5
@FunctionalInterface
interface MyInterface
{
void fun();
}

十四、内部类

1. 什么是内部类?

将一个类的定义放在另一个类的内部,就是内部类。

在 Java 中,内部类主要分为:实例内部类、类内部类、局部内部类、匿名内部类

2. 特性

(1) 作用

  • 提供更好的封装,可以将内部类隐藏在外部类中
  • 可以独立地继承类或实现接口,而不会与外部类相互影响
  • 匿名内部类可以用于创建仅需要使用一次的类

(2) 访问规则

  • 内部类可以访问外部类中的所有成员,因为内部类是外部类的成员
  • 外部类无法直接访问内部类中的成员,而是需要通过创建内部类的实例从而进行访问

3. 实例内部类

(1) 什么是实例内部类?

定义在类中,且不带 static 修饰符的类。

(2) 定义

1
2
3
4
class 外部类 {
class 内部类 {
}
}

(3) 访问

私有实例内部类

如果内部类是 private ,则内部类作为一个成员仅在外部类中才可以被访问。因此,应该在外部类中新建一个函数,实例化内部类并访问,其它类通过实例化外部类并执行该函数,实现间接访问内部类。

1
2
3
4
5
6
7
8
9
10
11
class 外部类 {
private class 内部类 {
变量/方法
}
public void fun() {
内部类 a = new 内部类();
···a.变量/方法···
}
}

new 外部类().fun();
非私有实例内部类

如果内部类不是 private ,则可以直接实例化并访问。

1
2
3
4
5
6
7
class 外部类 {
class 内部类 {
变量/方法
}
}

new 外部类().new 内部类().变量/方法;

(4) 注意

  • 成员内部类可以访问外部类的成员;

    而如果外部类希望访问成员内部类的成员,只能实例化内部类的方式来访问

  • 非静态的成员内部类不可以拥有静态成员

(5) 示例

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
public class Car {
private int wheelNum = 4;

private class Wheel {
void turn() {
for (int i = 1; i <= wheelNum; i++) {
System.out.println("第"+ i +"个轮子转呀转");
}
}
}

public void run() {
new Wheel().turn();
System.out.println("车开起来了");
}

public class Man {
void talk() {
System.out.println("给我加点油");
}
}

// get/set方法

// 构造方法
}

public class Test {
public static void main(String[] args) {
Car car = new Car();
car.run();

Car.Man man = new Car().new Man();
man.talk();
}
}

4. 类内部类

(1) 什么是类内部类?

用 static 修饰的成员内部类。

如果用 static 修饰内部类,则该内部类就属于外部类本身,而不属于外部类的某个对象。

(2) 定义

1
2
3
4
class 外部类 {
static class 内部类 {
}
}

(3) 访问

实例化类内部类
1
new 外部类.内部类()
访问类内部类的类成员
1
外部类.内部类.成员
访问类内部类的实例成员
1
new 外部类.内部类().成员

(4) 注意

  • 类内部类中可以包含类成员和实例成员,但是它们组合起来作为外部类的类成员

  • 类内部类只能访问外部类中的类成员

  • 外部类无法直接访问静态内部类的成员,但可以

    • 通过静态内部类的类名访问静态内部类的类成员 外部类.内部类.成员
    • 通过静态内部类的实例访问静态内部类的实例成员 new 外部类.内部类().成员

(5) 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Car {
static class Tank {
static void oil() {
System.out.println("正在加油");
}
void check() {
System.out.println("加满了");
}
}
}
public class Test {
public static void main(String[] args) {
Car.Tank.oil();
Car.Tank tank = new Car.Tank();
tank.check();
}
}

5. 局部内部类

(1) 什么是局部内部类?

即定义在方法中的类。

(2) 定义

1
2
3
4
5
6
7
class 类名 {
void 方法名 {
class 内部类名 {
···
}
}
}

(3) 注意

  • 只能在当前方法中被访问

  • 不带修饰符,因为它必定属于方法,且只在方法中才能被访问

  • 被局部内部类、匿名内部类访问的局部变量必须用 final 修饰

    在 Java 8 以前,必须用 final 修饰;

    在 Java 8 以后,可以不用 final 修饰,但变量不能被再次修改,即隐式设为常量

(4) 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Cat {
void eatFood() {
class Food {
String name;
int num;

@Override
public String toString() {
return num + "个" + name;
}

// 构造方法
}
Food food = new Food("鱼", 3);
System.out.println("猫吃了" + food);
}

public static void main(String[] args) {
Cat cat = new Cat();
cat.eatFood();
}
}

6. 匿名内部类

(1) 什么是匿名内部类?

匿名内部类,即没有名字的内部类,会在创建后立即消失,无法重复使用。

(2) 定义

1
2
3
new 父类名|接口名 {

}

(3) 注意

  • 匿名类可以继承父类或实现接口

  • 匿名类中可以重写方法、覆盖变量

  • 因为要创建实例,匿名类中不能包含抽象方法,即所有的抽象方法都应该在匿名类中重写

  • 被局部内部类、匿名内部类访问的局部变量必须用 final 修饰

    在 Java 8 以前,必须用 final 修饰;

    在 Java 8 以后,可以不用 final 修饰,但变量不能被再次修改,即隐式设为常量

(4) 示例

重写方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Cat {
public void eat() {
System.out.println("猫吃鱼");
}
}
public class Test {
public static void main(String[] args) {
// 直接调用实例的方法
new Cat().eat();
// 通过匿名内部类重写方法后,调用实例的方法
new Cat() {
public void eat() {
System.out.println("猫也吃猫粮");
}
}.eat();
}
}

被访问的局部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class Cat {
abstract void fun();
}
public class Test {
public static void main(String[] args) {
// 隐式定义为常量
int age = 8;
new Cat() {
public void fun() {
System.out.println(age);
}
}.fun();
}
}

十五、枚举类

1. 枚举

一些类的对象是有限且固定的,它们便被称为枚举。

例如:

季节类,有且仅有:春天、夏天、秋天、冬天

性别类,有且仅有:男性、女性

2. 枚举类

Java 通过枚举类来表达枚举关系。

1
2
3
enum 枚举类名 {
枚举实例1, 枚举实例2, ···, 枚举实例n;
}

3. 注意点

  • 枚举类可以实现接口,但必须实现接口描述的所有方法

    因为要创建枚举实例

  • 枚举类不能通过 new 创建实例,所有实例必须在枚举列开头列出

  • 枚举类不能派生子类

4. 常用方法

方法 说明
枚举类名.valueOf(枚举值”) 返回指定枚举类中的指定枚举
枚举1.compareTo(枚举2) 比较罗列枚举值时的顺序(若枚举 1 在枚举 2 之后,则返回整数···)
枚举.ordinal() 返回枚举值在枚举类中的索引值
枚举.toString() 返回枚举的名称
1
2
3
4
5
6
7
8
9
10
11
12
public enum Season {
SPRING, SUMMER, FALL, WINTER;
}


public static void main(String[] args) {
// Season i = Season.SPRING;
Season i = Season.valueOf("SPRING");
System.out.println(i.compareTo(Season.SUMMER));
System.out.println(i.ordinal());
System.out.println(i);
}

5. 类变量和类方法

可以在枚举类中定义类变量和类方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum Season {
SPRING, SUMMER, FALL, WINTER;
final static int seasonNum = 4;
static void sayHi() {
System.out.println("你好!我是季节类。");
}
}

public class Test {
public static void main(String[] args) {
System.out.println(Season.seasonNum);
Season.sayHi();
}
}

6. 成员变量

可以为枚举类定义成员变量,用于描述每个枚举的特有属性。

(1) 不好的做法

为枚举类添加成员变量,并在使用之前对每个枚举的成员变量进行赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum Season {
SPRING, SUMMER, FALL, WINTER;
String describe;
public String getDescribe() {
return describe;
}
public void setDescribe(String describe) {
this.describe = describe;
}
}
public class Test {
public static void main(String[] args) {
Season.SPRING.setDescribe("春天");
System.out.println(Season.SPRING.getDescribe());
}
}

但这种做法不够方便,也不够安全,原因是:

  • 在使用枚举之前需要人为设置属性值
  • 成员变量可能被人为篡改
  • 代码不够简洁

(2) 建议做法

  • 将成员变量用 priveta final 修饰,使得成员变量隐藏且不可变
  • 通过构造器为枚举中的成员变量传入初始值
  • 在列出枚举值时传入对应参数
1
2
3
4
5
6
7
8
9
enum 枚举类名 {
枚举实例1(参数列),
枚举实例2(参数列),
···,
枚举实例n(参数列);

// 隐藏且不可变的成员变量
// 构造方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public enum Season {
SPRING("春天"),
SUMMER("夏天"),
FALL("秋天"),
WINTER("冬天");
private final String describe;
public String getDescribe() {
return describe;
}
Season(String describe) {
this.describe = describe;
}
}
public class Test {
public static void main(String[] args) {
System.out.println(Season.SPRING.getDescribe());
System.out.println(Season.SUMMER.getDescribe());
System.out.println(Season.FALL.getDescribe());
System.out.println(Season.WINTER.getDescribe());
}
}

7. 成员方法

可以为枚举类定义成员方法,用于进行每个枚举的特有动作。

  • 定义抽象方法 / 实现接口
  • 在枚举值之后紧跟 { } ,在其中重写方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum 枚举类名 [implements 接口名]{
枚举实例1{
// 重写方法
},
枚举实例2{
// 重写方法
},
···,
枚举实例n{
// 重写方法
};

[抽象方法]
}
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
public enum Season {
SPRING{
public void sayHi() {
System.out.println("你好!我是春天。");
};
},
SUMMER{
public void sayHi() {
System.out.println("你好!我是夏天。");
};
},
FALL{
public void sayHi() {
System.out.println("你好!我是秋天。");
};
},
WINTER{
public void sayHi() {
System.out.println("你好!我是冬天。");
};
};

public abstract void sayHi();
}
public class Test {
public static void main(String[] args) {
Season.SPRING.sayHi();
Season.SUMMER.sayHi();
Season.FALL.sayHi();
Season.WINTER.sayHi();
}
}

参考