# 第二章 面向对象编程

# 2.1 类与对象的关系

(1)类就是数据类型,比如 Cat

(2)对象就是一个类的具体的实例

(3)从猫类到猫对象的说法:创建一个对象、实例化一个对象、把类实例化

(4)总结:类是抽象的、概念的,代表一类事物,比如人类、猫类,即它是数据类型;对象是具体的、实际的,代表一个具体事物,即是实例;类是对象的模板,对象是类的一个个体,对应的是一个实例

# 2.2 属性

# 2.2.1 基本介绍

属性的叫法:成员变量 = 属性 = 字段

属性是类的一个组成部分,一般是基本数据类型,也可以是引用类型(对象、数组)

# 2.2.2 注意事项和细节

属性如果不赋值,有默认值,规则和数组一致。

# 2.3 对象

# 2.3.1 创建对象

  • 先声明再创建
Cat cat; //声明对象 cat
cat = new Cat(); //创建
1
2
  • 直接创建
Cat cat = new Cat();
1

# 2.3.2 Java对象分配机制

栈:一般存放基本数据类型、局部变量

堆:存放对象(Cat cat,数组等)

方法区:常量池,类加载信息

示意图:

Java 对象创建过程:

//解读 Cat cat = new Cat();
//创建了一个 Cat 类的对象,并把该对象的引用赋值给变量 cat
Cat cat = new Cat();
cat.name = "小白";
cat.age = 12;
cat.color = "白色";
1
2
3
4
5
6
  1. 先加载 Cat 类信息【属性和方法信息只会加载一次】
  2. 在堆中分配空间,然后对属性进行默认初始化
  3. 把地址赋给 cat,cat 就指向对象
  4. 对属性进行指定的初始化,比如 cat.name = "小白"

# 2.4 方法

# 2.4.1 方法入门

class Person {
    String name;
    int age;
    
    //方法
    //public 表示方法是公开的
    //void 表示方法没有返回值
    //speak() speak 是方法名,() 是形参列表
    //{} 是方法体,可以写要执行的代码
    public void speak() {
        System.out.println("我是人")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 当程序执行到方法时,就会开辟一个独立的空间【栈空间】
  2. 当方法执行完毕,或者执行到 return 语句时,就会返回,返回到调用该方法的地方,返回后继续执行方法后面的代码
  3. 当 main 方法【栈】执行完毕,整个程序退出

# 2.4.2 重点

​ 方法体里不能再定义方法

​ 方法调用说明:同一个类中的方法调用,直接调用即可。跨类中的方法,可以调用的话需要通过对象名调用,比如 对象名.方法名(参数)。

​ 在内存中,当程序执行到方法时,就会在栈里新开辟一个独属于方法的空间。

# 2.4.3 方法传参机制

# 2.4.3.1 基本数据类型的传参机制

基本数据类型传递的是值(值拷贝),形参的任何改变不影响实参

//基本数据类型的传参机制
public class 方法传参机制 {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        A obj = new A();
        obj.swap(a, b);
        System.out.println("main方法的 a = " + a + " b = " + b); //main方法的 a = 1 b = 2
    }
}

class A {
    public void swap(int a, int b) {
        int tmp;
        tmp = a;
        a = b;
        b = tmp;
        System.out.println("swap方法的 a = " + a + " b = " + b); //swap方法的 a = 2 b = 1
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

图解:

# 2.4.3.2 引用数据类型的传参机制

引用类型传递的是地址(传递的也是值,但是值是地址),可以通过形参影响实参

public class 方法传参机制_引用数据类型 {
    public static void main(String[] args) {
        int arr[] = {1, 2, 3};
        B obj = new B();
        obj.test1(arr);
        System.out.println("main的数组");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " "); //100 2 3
        }
        System.out.println("");
    }
}

class B {
    public void test1(int[] arr) {
        arr[0] = 100;
        System.out.println("test1的数组");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " "); //100 2 3
        }
        System.out.println("");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

(3)易错

public class 方法传参机制_易错 {
    public static void main(String[] args) {
        Person person = new Person();
        person.name = "jack";
        person.age = 18;
        C c = new C();
        c.test2(person);
        System.out.println("main方法中name = " + person.name + " age = " + person.age); 
        //main方法中name = jack age = 180
    }
}

class Person {
    public String name;
    public int age;
}

class C {
    public void test2(Person person) {
        person.age = 180;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

在此基础上,如果 test2 方法中让 person = null 的话,将会输出什么?

将会输出 name = jack age = 18

public class 方法传参机制_易错 {
    public static void main(String[] args) {
        Person person = new Person();
        person.name = "jack";
        person.age = 18;
        C c = new C();
        c.test2(person);
        System.out.println("main方法中name = " + person.name + " age = " + person.age); 
        //main方法中name = jack age = 18
    }
}

class Person {
    public String name;
    public int age;
}

class C {
    public void test2(Person person) {
        person = null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

那么如果 test2 方法中让 person = new Person() 的话将会输出什么?

将会输出 name = jack age = 18

public class 方法传参机制_易错 {
    public static void main(String[] args) {
        Person person = new Person();
        person.name = "jack";
        person.age = 18;
        C c = new C();
        c.test2(person);
        System.out.println("main方法中name = " + person.name + " age = " + person.age);
        //main方法中name = jack age = 18
    }
}

class Person {
    public String name;
    public int age;
}

class C {
    public void test2(Person person) {
        person = new Person();
        person.name = "tom";
        person.age = 20;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 2.4.4 重载

Java 中允许同一个类中有多个同名的方法存在,但要求形参列表不一致

方法名:必须相同

形参列表:必须不同

返回类型:无要求

# 2.4.5 可变参数

Java 允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法,就可以通过可变参数实现

class Method {
    
    public int sum(int n1, int n2) {
        return n1 + n2;
    }
    
    public int sum(int n1, int n2, int n3) {
        return n1 + n2 + n3;
    }
    
    public int sum(int n1, int n2, int n3, int  n4) {
        return n1 + n2 + n3 + n4;
    }
    
    //可以简写成
    
    //int... 表示接收的是可变参数,类型是 int,即可以接收多个 int
    //使用可变参数时,可以当做使用数组来使用,即 nums 可以当做数组
    public int sum(int... nums) {
        int res = 0;
        for (int i = 0; i < nums.length; i++){
            res += nums[i];
        }
        return res;
    }
    
}
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

注意:

  1. 可变参数的实参可以为 0 个或任意多个
  2. 可变参数的实参可以为数组
  3. 可变参数的本质就是数组
  4. 可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数在最后
  5. 一个形参列表中只能出现一个可变参数

# 2.4.6 作用域

  1. 在 Java 编程中,主要的变量就是属性【成员变量】和局部变量
  2. 局部变量一般是指在成员方法中定义的变量
  3. 全局变量也就是属性,作用域为整个类体
  4. 局部变量也就是除了属性之外的其它变量,作用域为定义它的代码块中
  5. 全局变量可以不赋值而直接使用,因为有默认值;局部变量必须赋值后才能使用,因为没有默认值
  6. 属性和局部变量可以重名,访问时遵循就近原则
  7. 在用一个作用域中,比如在同一个成员方法中,局部变量不能重名
  8. 属性生命周期较长,伴随着对象的创建而创建,伴随着对象的销毁而销毁;局部变量生命周期较短,伴随着它的代码块的执行而创建,伴随着代码块的结束而销毁,即在一次方法调用过程中
  9. 属性/全局变量:可以被本类使用或其它类使用【通过对象调用】
  10. 局部变量:只能在本类中对应的方法中使用
  11. 属性/全局变量可以加修饰符,局部变量不可以加修饰符

# 2.4.7 构造器/构造方法

前面我们在创建对象时,都是先把一个对象创建好后,再给它的属性赋值,如果需要在创建对象的时候就直接指定这个对象的属性值,可以使用构造器

语法:

[修饰符] 方法名(形参列表) {
    方法体;
}
1
2
3

注意:

  1. 构造器的修饰符可以默认,也可以是 public、protected、private

  2. 构造器没有返回值

  3. 构造器方法名和类名字必须一样

  4. 参数列表和成员方法的规则一致

  5. 在创建对象时,系统会自动的调用该类的构造器完成对对象的初始化

  6. 一个类可以定义多个不同的构造器,即构造器重载

  7. 如果没有定义构造器,系统会自动给类生成一个默认的无参构造器,一旦定义了自己的构造器,默认的构造器就覆盖了,如果要使用默认的无参构造器,就要显式的定义一下。

# 2.4.8 对象创建的流程

流程分析:

  1. 加载 Person 类信息(Person.class),只会加载一次

  2. 在堆中分配空间(地址)

  3. 完成对象初始化:

    (1)默认初始化:age = 0, name = null

    (2)显式初始化:age = 90, name = null

​ (3)构造器初始化:age = 20, name = 小明

  1. 把对象在堆中的地址返回给 p( p 是对象名,也可以理解为是对象的引用)

# 2.4.9 this关键字

Java 虚拟机会给每个对象分配 this 代表当前对象

this 本质:

this 使用细节:

​ (1)this关键字可以用来访问本类的属性、方法、构造器

​ (2)this用于区分当前类的属性和局部变量

​ (3)访问成员方法的语法:this.方法名(参数列表)

​ (4)访问构造器语法:this(参数列表),注意:只能在构造器中使用(即只能在构造器中访问另一个构造器,必须放在第一条语句)

​ (5)this不能在类定义的外部使用,只能在类定义的方法中使用

# 2.5 包

我们引入一个包的主要目的是要使用该包下的类

比如 import java.util.Scanner; 就是引入 util 包下的一个类 Scanner

# 2.6 访问修饰符

Java 提供四种访问控制修饰符号,用于控制方法和属性的访问权限

  1. 公开级别:用 public 修饰,对外公开
  2. 受保护级别:用 protected 修饰,对子类和同一个包中的类公开
  3. 默认级别:没有修饰符号,向同一个包的类公开
  4. 私有级别:用 private 修饰,只有类本身可以访问,不对外公开
访问级别 访问控制修饰符 同类 同包 子类 不同包
公开 public
受保护 protected ×
默认 没有修饰符 × ×
私有 private × × ×

注意:

  1. 修饰符可以用来修饰类中的属性,成员方法以及类
  2. 只有默认的和 public 才能修饰类

# 2.7 封装

封装就是把抽象出的属性和方法封装在一起,数据被保护在内部,程序的其它部分只有通过被授权访问的方法来对数据进行操作

# 2.7.1 封装的实现步骤

  1. 将属性进行私有化 private,让外部不能直接修改属性
  2. 提供一个公共的【public】set 方法,用于对属性判断并赋值
public void setXxx(类型 参数名) { //Xxx 表示某个属性名
    //加入数据验证的业务逻辑
    属性 = 参数名;
}
1
2
3
4
  1. 提供一个公共的【public】get 方法,用于获取属性的值
public 数据类型 getXxx() {
    return xxx;
}
1
2
3

# 2.8 继承

# 2.8.1 为什么需要继承

有两个类,一个是 Pupil 类【小学生类】,一个是 Graduate 类【大学毕业生类】,两个类的属性和方法有很多是相同的该怎么办?

这里可以使用继承,把两个类中的相同的属性和方法提取出来封装成一个 Student 父类,让两个类继承父类,这样这两个类就可以使用相同的属性和方法而不用重复定义了

# 2.8.2 继承基本介绍

继承可以解决代码复用,当多个类存在相同的属性和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过 extends 来声明继承父类即可

当子类继承父类后,子类就会自动拥有父类定义的属性和方法

# 2.8.3 继承深入

  1. 子类继承了父类所有的属性和方法,非私有的属性和方法可以在子类直接访问,但是私有属性和方法不能再子类直接访问,要通过父类提供的公共的方法去访问。

  2. 子类必须调用父类的构造器,完成父类的初始化。当创建子类对象时,不管使用的子类的哪个构造器,默认情况下一定会去调用父类的无参构造器(因为子类的构造器默认有个super() 方法,用来调用父类的无参构造器),如果父类没有无参构造器,则必须在子类的构造器中用 super 去指定使用父类的哪个构造器完成对父类的初始化工作。

  3. 如果希望指定去调用父类的某个构造器,则显式的调用一下:super(参数列表)

  4. super 在使用时,必须放在构造器第一行,super 只能在构造器中使用

  5. super 和 this 都只能放在构造器第一行,因此两个方法不能共存在一个构造器中

  6. Java 所有类都是 Object 类的子类

  7. 父类构造器的调用不限于直接父类,将一直往上追溯直到 Object 类【顶级父类】

  8. 子类最多只能继承一个父类【直接继承】,即 Java 中是单继承机制

# 2.8.4 继承的内存布局图

当使用 Son 的属性或者方法时,会按照查找关系来返回信息:

  1. 首先看子类是否有该属性或者方法

  2. 如果子类有该属性或者方法,并且可以访问,则返回信息

  3. 如果子类没有该属性或者方法,就看父类有没有该属性或者方法,如果父类有该属性或者方法,并且可以访问,则返回信息,如果有但是不能访问则报错

  4. 如果父类没有就继续找上级父类,直到Object,没有找到则提示方法不存在

# 2.8.5 super关键字

super 代表父类的引用,用于访问父类的属性、方法、构造器,不能访问父类 private 的属性或方法

# 2.8.5.1 super 好处

  1. 调用父类的构造器的好处:分工明确,父类属性由父类初始化,子类的属性由子类初始化
  2. 当子类中有和父类中的成员【属性或方法】重名时,为了访问父类的成员,必须通过 super,如果没有重名,使用 super 或 this 访问是一样的效果
  3. super 的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用。super 去访问爷爷类的成员,如果多个上级类中都有同名的成员,使用 super 访问遵循就近原则

# 2.8.6 方法重写

  1. 方法重写就是子类有一个方法和父类的某个方法的名称、参数、返回类型一样【这里返回类型子类和父类的一样或者子类的返回类型是父类返回类型的子类】,那么我们就说子类的这个方法重写了父类的方法
  2. 子类方法不能缩小父类方法的访问权限

# 2.8.6.1 重载与重写

名称 发生范围 方法名 形参列表 返回类型 修饰符
重载 本类 必须一样 类型,个数或者顺序至少有一个不同 无要求 无要求
重写 父子类 必须一样 相同 子类返回类型和父类的返回类型一致或者是父类的返回类型的子类型 子类方法不能缩小父类方法的访问范围

# 2.9 多态

# 2.9.1 引入

多态指方法或对象具有多种形态,是面向对象的第三大特征,多态是建立在封装和继承基础之上的

当一个方法的形参需要传入子类的类型,但是如果有好多的子类,就要把这个方法写好多遍用来传入不同的子类类型,所以引入多态就可以只写一遍这个方法,形参中传入父类的类型,因为父类的类型可以接受不同的子类的类型。举例:

public class 引入 {
    public static void main(String[] args) {
        Dog dog = new Dog("大黄", 18);
        Cat cat = new Cat("小花", 20);
        Master master = new Master();
        master.feedDog(dog);
        master.feedCat(cat);
        master.feed(dog);
        master.feed(cat);
    }
}

class Animals {
    public String name;
    public int age;
    public Animals(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

class Dog extends Animals{
    public Dog(String name, int age) {
        super(name, age);
        this.name = name;
        this.age = age;
    }
    public void cry() {
        System.out.println("狗叫");
    }
}

class Cat extends Animals{
    public Cat(String name, int age) {
        super(name, age);
        this.name = name;
        this.age = age;
    }
    public void cry() {
        System.out.println("猫叫");
    }
}

class Master {
    //这里如果要写给动物们喂食,要获取动物们的信息
    //如果要给狗喂食,feed()方法的形参就得写成Dog dog用来接收Dog类对象,这样就只能给Dog类喂食,不能给Cat类喂食
    public void feedDog(Dog dog) {
        System.out.println(dog.name);
    }

    //如果要给猫喂食,feed()方法的形参就得写成Cat cat用来接收cat类对象
    public void feedCat(Cat cat) {
        System.out.println(cat.name);
    }
    
    //如果动物很多,食物很多... 那么就导致会有很多的 feed 方法,不利于管理和维护

    //简单点,就是写一个feed方法,feed方法里的形参可以接受不同动物类的形参
    //例如,一个feed方法的形参既可以接收Dog类的对象,也可以接收Cat类对象
    public void feed(Animals animals) {
        System.out.println(animals.name);
    }
}
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
61
62
63

# 2.9.2 多态的具体体现

  1. 一个对象的编译类型和运行类型可以不一致

  2. 编译类型在定义对象时就确定了,不能改变

  3. 运行类型是可以变化的

  4. 编译类型看定义时 = 号的左边,运行类型看 = 号的右边

//编译类型是父类的引用可以接收其子类的对象
Animal animal = new Dog(); //animal 的编译类型是 Animal,运行类型是 Dog

animal = new Cat(); //animal 的运行类型变成了 Cat,但是编译类型仍然是 Animal
1
2
3
4
public class PolyObject {
    public static void main(String[] args) {
        //animal 的编译类型是 Animal,运行类型是 Dog
        Animal animal = new Dog();
        //当执行到该行时,animal 运行类型是 Dog,所以 cry 就是 Dog 的 cry
        animal.cry(); //小狗汪汪叫
        
        //animal 编译类型是 Animal,运行类型是 Cat
        animal = new Cat();
        animal.cry(); //小猫喵喵叫
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 2.9.3 向上转型

前提:两个对象(类)存在继承关系

本质:父类的引用指向了子类的对象

语法:父类类型 引用名 = new 子类类型();

特点:

  1. 父类类型的引用可以调用父类中的所有成员【需要遵守访问权限】
  2. 不能调用子类中的特有成员【特有成员指子类里有和父类里不相同的属性或方法,即子类自己的属性或方法】。因为在编译阶段,能调用哪些成员是由编译类型来决定的
  3. 父类类型的引用在调用子类非特有的属性(即子类和父类共同有的属性)时,要看编译类型,编译类型是什么就调用什么的属性
  4. 父类类型的引用在调用子类非特有的方法(即子类和父类共同有的方法,即子类重写了父类的方法)时,看运行类型,调用规则与前面的方法调用规则一致,即先从子类找,子类没有就调用父类的。
  5. 如果想要调用子类的特有的成员,就要用到向下转型
// 父类
class Animal {
    public String name = "动物";
    public int age = 20;
    public String hobby = "睡觉";
    
    public void sleep() {
        System.out.println("睡");
    }
    
    public void eat() {
        System.out.println("吃");
    }
}

// 子类
class Cat extends Animal {
    public String name = "小猫";
    public int age = 10;
    public String color = "黄色";
    
    public void sleep() {
        System.out.println("小猫睡");
    }
    
    public void catchMouse() {
        System.out.println("小猫抓老鼠");
    }
}

public class PolyDetail {
    public static void main(String[] args) {
        // 向上转型:父类的引用指向了子类的对象
        Animal animal = new Cat();

        // 可以调用父类中的所有成员,在调用 sleep 方法时,先在子类找,子类没有就去父类找,跟前面学习的一样
        animal.sleep(); // 小猫睡


        //即使是子类中没有的成员(属性和方法)也可以调用,因为遵守继承规则
        animal.eat(); // 吃
        System.out.println(animal.hobby); //睡觉


        animal.catchMouse(); // 报错,不能调用子类的特有的成员
        System.out.println(animal.color); //报错,不能调用子类的特有的成员


        //父类类型的引用在调用子类非特有的属性(即子类和父类共同有的属性,即子类重写了父类的属性)时,要看编译类型,编译类型是什么就调用什么的属性
        System.out.println(animal.name); //动物
        Cat cat = new Cat();
        System.out.println(cat.name); //小猫
    }
}
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

# 2.9.4 向下转型

  1. 语法:子类类型 新引用名 = (子类类型) 父类引用;

  2. 只能强转父类的引用,不能强转父类的对象

  3. 要求父类的引用必须指向的是目标类型的对象

  4. 向下转型后,可以调用子类类型的所有成员

//animal.catchMouse(); // 报错,不能调用子类的特有的成员
        //System.out.println(animal.color); //报错,不能调用子类的特有的成员

        Cat cat = (Cat) animal; 
        cat.catchMouse(); // 小猫抓老鼠
        System.out.println(cat.color); //黄色
1
2
3
4
5
6

# 2.9.5 关于属性重写问题

属性没有重写之说,属性的值看编译类型

public class 属性重写 {
    public static void main(String[] args) {
        Father1 son = new Son1();
        System.out.println(son.count); //10
    }
}

class Father1 {
    int count = 10;
}

class Son1 extends Father1{
    int count = 20;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

调用属性看编译类型,调用方法看运行类型

# 2.9.6 动态绑定机制

  1. 当调用对象方法时:不管是在父类里调用方法还是在子类里调用方法,都要先从运行类型的类中找有没有同名的这个方法,有就调用运行类型的类里的,没有才去父类找。

  2. 当使用对象属性时:没用动态绑定机制,相应的类里声明了就用相应的属性

public class 动态绑定机制 {
    public static void main(String[] args) {
        A b = new B();
        System.out.println(b.sum()); //30
        System.out.println(b.sum1()); //20
    }
}

class A {
    public int i = 10;
    public int sum() {
        return getI() + 10; //30 此处的getI()会调用子类的getI()方法,向上转型调用方法时,看运行类型
    }
    public int sum1() {
        return i + 10; //20
    }
    public int getI() {
        return i; //10
    }
}

class B extends A {
    public int i = 20;
    public int getI() {
        return i; //20
    }
}
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

# 2.9.7 多态数组

多态数组指数组的定义类型为父类类型,里面保存的实际元素类型为子类类型

public class PolymorphicAarrays {
    public static void main(String[] args) {
        Person[] persons = new Person[5];
        
        persons[0] = new Person("jack", 20);
        persons[1] = new Student("student1", 18, 100);
        persons[2] = new Student("student2", 20, 90);
        persons[3] = new Teacher("teacher1", 35, 1000);
        persons[4] = new Teacher("teacher2", 40, 2000);

        for (int i = 0; i < persons.length; i++) {
            System.out.println(persons[i].say());
            
            //想要调用特有的方法
            if (persons[i] instanceof Student) { //判断运行类型
                Student student = (Student) persons[i];
                student.study();
            } else if (persons[i] instanceof Teacher) {
                Teacher teacher = (Teacher) persons[i];
                teacher.teach();
            } else if (persons[i] instanceof Person) {
                
            } else {
                System.out.println("类型错误");
            }
        }
    }
}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String say() {
        return name + "\t" + age;
    }
}

class Teacher extends Person {
    private double salary;

    public Teacher(String name, int age, double salary) {
        super(name, age);
        this.salary = salary;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    @Override
    public String say() {
        return "老师 " + super.say() + " salary = " + salary;
    }

    //特有方法
    public void teach() {
        System.out.println("老师 " + getName() + " 正在上课");
    }
}

class Student extends Person {
    private double score;

    public Student(String name, int age, double score) {
        super(name, age);
        this.score = score;
    }

    public double getScore() {
        return score;
    }

    public void setScore(double score) {
        this.score = score;
    }

    @Override
    public String say() {
        return "学生 " + super.say() + " score = " + score;
    }

    //特有方法
    public void study() {
        System.out.println("学生 " + getName() + " 正在学习");
    }
}
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

# 2.10 Object 类

# 2.10.1 == 和 equals 的对比

== 是一个比较运算符,它既可以判断基本类型又可以判断引用类型:

  • 如果判断基本类型,判断的是值是否相等
  • 如果判断引用类型,判断的是地址是否相等,即判断是不是同一个对象
class A {
    
}

public class Equals {
    public static void main(String[] args) {
        A a = new A();
        A b = a;
        A c = b;
        System.out.println(a == c); // true
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

equals 是 Object 类中的方法,只能判断引用类型,默认判断的是地址是否相等,但是 Object 的子类中往往重写该方法,用于判断内容是否相等,比如:Integer,String 重写 equals 方法

//Object 的 equals
public boolean equals(Object obj) {
    return (this == obj);
}

//Integer 重写 equals 方法
public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
Integer integer1 = new Integer(1000);
Integer integer2 = new Integer(1000);
System.out.println(integer1 == integer2); //false 对象不同,地址不同,因为 new 了两次,两个不一样
System.out.println(integer1.equals(integer2)); //true Integer 重写了 equals 方法,使其判断内容是否相等,都是 1000 所以相等
1
2
3
4

# 2.10.2 hashCode 方法

hashCode 方法用于返回该对象的哈希码值,支持此方法是为了提高哈希表的性能

有两个引用,如果指向的是同一个对象,则哈希值是一样的

有两个引用,如果指向的是不同对象,则哈希值是不一样的

# 2.10.3 toString 方法

toString 方法默认返回 全类名 + @ + 哈希值的十六进制,子类往往重写 toString 方法,用于返回对象的属性信息

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
1
2
3

重写 toString 方法,打印对象或拼接对象时,都会自动调用该对象的 toString 形式

当直接输出一个对象时,toString 方法会被默认的调用

# 2.10.4 finalize 方法

  1. 当对象被回收时,系统自动调用该对象的 finalize 方法,子类可以重写该方法做一些释放资源的操作
  2. 什么时候被回收:当某个对象没有任何引用时,则 JVM 就认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用 finalize 方法
  3. 垃圾回收机制的调用,是由系统来决定【即有自己的 GC 算法】,也可以通过 System.gc() 主动触发垃圾回收机制

# 2.11 类变量和类方法【静态变量和静态方法】

# 2.11.1 类变量引入

问题:有一群小孩在玩堆雪人,不时有新的小孩加入,如何知道现在共有多少人在玩?

class Child {
    private String name;
    //定义一个变量 count,是一个类变量,该变量最大的特点就是会被 Child 类的所有的对象实例共享
    public static int count = 0;
    public Child(String name) {
        this.name = name;
    }
    public void join() {
        System.out.println(name + " 加入了游戏");
    }
}

public class ChildGame {
    public static void main(String[] args) {
        Child child = new Child("小孩1");
        child.join();
        child.count++;
        
        //类变量可以通过类名来访问
        System.out.println("共有 " + Child.count + " 小孩加入了游戏");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2.11.2 类变量

类变量也叫静态变量/静态属性,是该类的所有对象共享的变量,任何一个该类的对象去访问它时,取到的都是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量。

语法:访问修饰符 static 数据类型 变量名;

访问:类名.类变量名对象名.类变量名

类变量是在类加载时就初始化了,也就是说,即使没有创建对象,只要类加载了就可以使用类变量

类变量的生命周期是随类的加载开始,随着类消亡而销毁

# 2.11.3 类方法

类方法也叫静态方法

语法:访问修饰符 static 数据返回类型 方法名() {}

调用:类名.类方法名对象名.类方法名

class Stu {
    private String name; //普通成员
    //定义一个静态变量,来累计学生的学费
    private static double fee = 0;
    
    public Stu(String name) {
        this.name = name;
    }
    
    //1. 当方法使用了 static 修饰后,该方法就是静态方法
    //2. 静态方法就可以访问静态属性/变量
    public static void payFee(double fee) {
        Stu.fee += fee; //累积
    }
    
    public static void showFee() {
        System.out.println("总学费有: " + Stu.fee);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在实际开发中,会将一些通用的方法设计成静态方法,这样不需要创建对象就可以使用了

//如果我们希望不创建实例也可以调用某个方法,可以把方法做成静态方法
System.out.println("9 开平方的结果是 = " + Math.sqrt(9));
1
2

注意:

  1. 类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区,类方法中无 this 的参数,普通方法中隐含着 this 的参数
  2. 类方法中不允许使用和对象有关的关键字,比如 this 或 super
  3. 类方法中只能访问静态变量或静态方法
  4. 普通成员方法既可以访问普通变量和方法,也可以访问静态变量和方法,即静态方法只能访问静态的成员;非静态的方法可以访问静态成员和非静态成员【遵守访问权限】

# 2.12 代码块

# 2.12.1 基本介绍

代码块又称为初始化块,属于类中的成员,是类的一部分,类似于方法,将逻辑语句封装在方法体中,通过 {} 包围起来,但和方法不同,没有方法名、没有返回、没有参数,只有方法体,而且不用通过对象或类显式调用,而是加载类时或创建对象时隐式调用

语法:

//修饰符可选,要写的话只能写 static
[修饰符] {
    代码
};
1
2
3
4

代码块相当于另外一种形式的构造器,可以做初始化的操作,代码块调用的顺序要优先于构造器

# 2.12.2 代码块细节

  1. static 代码块也叫静态代码块,作用就是对类进行初始化,而且它随着类的加载而执行,并且只会执行一次。如果是普通代码块每创建一个对象就执行
  2. 类什么时候被加载
  • 创建对象实例时【new 对象时】
  • 创建子类对象实例,父类也会被加载
  • 使用类的静态成员时
  1. 普通的代码块在创建对象实例时会被隐式的调用,被创建一次就会调用一次,如果只是使用类的静态成员时普通代码块并不会执行
  2. static 代码块是类加载时执行,只会执行一次
  3. 创建一个对象时,一个类的调用顺序是:

(1)调用静态代码块和静态属性初始化,静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态变量初始化,则按它们定义的顺序调用

(2)调用普通代码块和普通属性的初始化,普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和多个普通属性初始化,则按定义的顺序调用

(3)调用构造方法

  1. 构造器的最前面其实隐含了 super() 和调用普通代码块,静态相关的代码块、属性初始化在类加载时就执行完毕,因此是优先于构造器和普通代码块执行的
class A {
    public A() { //构造器
        //这里有隐藏的执行
        (1) super();
        (2) 调用普通代码块
        (3) 其它
        System.out.println("ok");
    }
}
1
2
3
4
5
6
7
8
9
class AAA {
    {
        System.out.pringln("AAA 的普通代码块");
    }
    
    public AAA() {
        System.out.println("AAA() 构造器被调用");
    }
}

class BBB extends AAA {
    {
        System.out.pringln("BBB 的普通代码块");
    }
    
    public BBB() {
        System.out.println("BBB() 构造器被调用");
    }
}

public class CodeBlock {
    public static void main(String[] args) {
        BBB bbb = new BBB();
        //进入 BBB 类的构造器,BBB 的构造器里隐藏了 super(),执行 super(),进入父类 AAA 的构造器
        //在父类的构造器中,也隐藏了 super(),执行 super(),执行到 Object 类,因为 Object 类中没有东西,所以不输出,然后回到父类 AAA 的构造器,执行隐藏的普通代码块,输出 "AAA 的普通代码块",然后输出 "AAA() 构造器被调用",此时父类初始化完毕,回到子类 BBB
        //BBB 构造器中隐藏的 super() 执行完了,执行隐藏的普通代码块,输出 "BBB 的普通代码块",然后输出 "BBB() 构造器被调用"
    }
}
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
  1. 创建一个子类对象时【继承关系】,它们的静态代码块、静态属性初始化、普通代码块、普通属性初始化、构造方法的调用顺序如下:

类加载相关的:

(1)父类的静态代码块和静态属性(优先级一致,按顺序执行)

(2)子类的静态代码块和静态属性(优先级一致,按顺序执行)

对象的创建:

(3)父类的普通代码块的普通属性初始化(优先级一致,按顺序执行)

(4)父类的构造方法里的语句

(5)子类的普通代码块的普通属性初始化(优先级一致,按顺序执行)

(6)子类的构造方法里的语句

  1. 静态代码块只能直接调用静态成员,普通代码块可以调用任意成员

# 2.13 关键字 final

# 2.13.1 基本介绍

final 可以修饰类、属性、方法和局部变量,在某些情况下可能有以下需求就会使用到 final:

  1. 当不希望类被继承时,可以用 final 修饰
  2. 当不希望父类的某个方法被子类重写时,可以用 final 关键字修饰
  3. 当不希望类的某个属性的值被修改,可以用 final 修饰
  4. 当不希望某个局部变量被修改,可以使用 final 修饰

# 2.13.2 使用细节

  1. final 修饰的属性又叫常量
  2. final 修饰的属性在定义时,必须赋初值,并且以后不能再修改,赋值可以在如下位置之一:

(1)定义时

(2)在构造器中

(3)在代码块中

  1. 如果 final 修饰的属性是静态的,则初始化的位置只能是:

(1)定义时

(2)在静态代码块

(3)不能在构造器中赋值

  1. final 类不能继承,但是可以实例化对象

  2. 如果类不是 final 类,但是含有 final 方法,则该方法虽然不能重写,但是可以被继承

  3. 一般来说,如果一个类已经是 final 类了,就没有必要再将方法修饰成 final 方法

  4. final 不能修饰构造器

  5. final 和 static 往往搭配使用,效率更高,底层编译器做了优化处理

  6. 包装类 Integer、Double、Float、Boolean 等都是 final

# 2.14 抽象类

# 2.14.1 抽象类引入

当父类的某些方法需要声明但是又不确定如何实现时,可以将其声明为抽象方法,那么这个类就是抽象类

class Animal {
    private String name;
    private int age;
    public Animal(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
    
    public void eat() {
        System.out.println("这是一个动物,但是不知道吃什么")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

将 eat() 方法抽象

abstract class Animal {
    private String name;
    private int age;
    public Animal(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
    
    //抽象方法
    //所谓的抽象方法就是没有实现的方法,所谓的没有实现的方法就是指没有方法体
    //当一个类中存在抽象方法时,需要将该类声明为 abstract 类
    public abstract void eat();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 2.14.2 基本介绍

当父类的一些方法不能确定时,可以用 abstract 关键字来修饰该方法,有 abstract 方法,类就要用 abstract 修饰

抽象类的价值更多作用是在于设计,是设计者设计好后让子类继承并实现抽象类

# 2.14.3 抽象类深入

  1. 抽象类不能被实例化
  2. 抽象类不一定要包含 abstract 方法,也就是说,抽象类可以没有 abstract 方法
  3. 一旦类包含了 abstract 方法,则这个类必须声明为 abstract
  4. abstract 只能修饰类和方法,不能修饰属性和其它的
  5. 抽象类可以包含任意成员
  6. 如果一个类继承了抽象类就必须实现抽象类里的所有抽象方法,除非它自己也声明为 abstract 类
  7. 抽象方法不能使用 private、final 和 static 来修饰,因为这些关键字都是和重写违背的

# 2.15 接口

# 2.15.1 接口引入

USB 插槽就是现实中的接口,我们可以把手机、相机、U 盘都插在 USB 插槽上,而不用担心哪个插槽是专门插哪个的,原因是做 USB 插槽的厂家和做各种设备的厂家都遵守了统一的规定,包括尺寸、排线等

# 2.15.2 基本介绍

接口就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候再根据具体情况把这些方法写出来

语法:

interface 接口名 {
    //属性
    
    //方法【可以是抽象方法、默认方法、静态方法】
}

class 类名 implements 接口 {
    自己的属性;
    自己的方法;
    必须实现的接口的抽象方法
}
1
2
3
4
5
6
7
8
9
10
11

在 JDK7 前,接口里的所有方法都没有方法体,即都是抽象方法

在 JDK8 后接口可以有静态方法、默认方法,即接口中可以有方法的具体实现

# 2.15.3 接口深入

  1. 接口不能被实例化
  2. 接口中所有的方法都是 public 方法,接口中的抽象方法可以不用 abstract 修饰
void aaa(); //实际上是 abstract void aaa();
1
  1. 一个普通类实现接口,就必须将该接口的所有方法都实现
  2. 抽象类实现接口,可以不用实现接口的方法
  3. 一个类同时可以实现多个接口
  4. 接口中的属性只能是 final 的,而且是 public static final 修饰符
int a = 1; //实际上是 public static final int a = 1; 必须初始化
1
  1. 接口中的属性的访问形式:接口名.属性名
  2. 一个接口不能继承其它的类,但是可以继承多个别的接口
  3. 接口的修饰符只能是 public 和默认,这点和类的修饰符是一样的

# 2.15.4 接口 VS 继承

当有一个小猴子对象时,小猴子继承父类的会爬树的功能,如果小猴子想飞,就去实现鸟的接口,想游泳就实现鱼的接口

当子类继承了父类就自动的拥有父类的功能,如果子类需要扩展功能,可以通过实现接口的方式扩展,可以理解实现接口是对 Java 单继承机制的一种补充

继承的价值主要在于:解决代码的复用性和可维护性

接口的价值主要在于:设计好各种规范【方法】,让其它类去实现这些方法,即更加的灵活

接口比继承更加灵活,继承是满足 is-a 的关系,而接口只需满足 like-a 的关系

接口在一定程度上实现代码解耦【接口规范性 + 动态绑定机制】

# 2.15.5 接口的多态特性

在前面的 USB 接口案例中,UsbInterface usb 既可以接收手机对象,又可以接收相机对象,就体现了接口多态【接口引用可以指向实现了接口的类的对象】

接口存在多态传递的现象

interface IH {}

interface IG extends IH {}

class Teacher implements IG {}

public class Test {
    public static void main(String[] args) {
        IG ig = new Teacher();
        //如果 IG 继承了 IH 接口,而 Teacher 类实现了 IG 接口,那么实际上就相当于 Teacher 类也实现了 IH 接口,这就是所谓的接口多态传递现象
        IH ih = new Teacher;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2.16 内部类

# 2.16.1 基本介绍

一个类的内部又完整的嵌套了另一个类结构,被嵌套的类称为内部类,嵌套其它类的类称为外部类,是类的第五大成员,内部类最大的特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系

class Outer{ //外部类
    
    class Inner { //内部类
        
    }
}

class Other { //外部其它类
    
}
1
2
3
4
5
6
7
8
9
10

类的五大成员:属性、方法、构造器、代码块、内部类

# 2.16.2 内部类分类

定义在外部类的局部位置上【比如定义在外部类的方法中】:

  • 局部内部类【有类名】
  • 匿名内部类【没有类名】

定义在外部类的成员位置上:

  • 成员内部类【没用 static 修饰】
  • 静态内部类【使用 static 修饰】

# 2.16.3 局部内部类

  1. 局部内部类是定义在外部类的局部位置,比如方法中,并且有类名,相当于局部变量
  2. 可以直接访问外部类的所有成员,包含私有的
  3. 不能添加访问修饰符,因为它的地位就是一个局部变量,局部变量是不能使用修饰符的,但是可以使用 final 修饰,因为局部变量也可以使用 final
  4. 作用域:仅仅在定义它的方法或代码块中
  5. 局部内部类访问外部类的成员的方式:直接访问
  6. 外部类访问局部内部类的成员的方式:创建对象访问且必须在作用域内
  7. 外部其它类不能访问局部内部类【因为局部内部类的地位是一个局部变量】
  8. 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员可以使用 外部类名.this.成员 访问
class Outer { //外部类
    private int n1 = 100; //外部类的成员属性
    
    private void m2() { //外部类的成员方法
        System.out.println("Outer m2()");
    }
    
    public void m1() { //外部类的成员方法
        //局部内部类是定义在外部类的局部位置,通常在方法
        //不能添加访问修饰符,但是可以使用 final 修饰
        class Inner { //局部内部类,本质仍然是一个类,相当于局部变量
            private int n1 = 800; //内部类的成员属性
            
            public void f1() {
                //可以直接访问外部类的所有成员,包含私有的
                //Outer.this 本质就是外部类的对象,即哪个对象调用了 m1 方法,Outer.this 就是哪个对象,主函数里是 outer 调用的 m1 方法,所以这里的 Outer.this 指 outer
                System.out.println("n1 = " + n1 + " 外部的 n1 = " + Outer.this.n1);
                
                m2();
            }
        }
        
        //Inner 本质还是一个类,所有能被继承,当加上 final 修饰,Inner 就不能被继承了
        class Inner02 extends Inner {}
        
        Inner inner = new Inner();
        inner.f1();
    }
}

public class Test {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.m1();
    }
}
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

# 2.16.4 匿名内部类

# 2.16.4.1 匿名内部类 - 基于接口

  1. 本质是类

  2. 内部类

  3. 该类没有名字

  4. 同时还是一个对象

  5. 匿名内部类是定义在外部类的局部位置,比如方法中,并且没有类名

  6. 基本语法:

new 类或接口(参数列表) {
  类体  
};
1
2
3
# 2.16.4.1.1 传统方法实现接口
interface IA {
    public void cry();
}

class Tiger implements IA {
    public void cry() {
        System.out.println("虎啸");
    }
}

class Outer {
    private int n1 = 10; //属性
    
    public void method() { //方法
        //这里想使用 IA 接口,并创建对象
        //传统方法是写一个类,实现该接口并创建对象
        IA tiger = new Tiger();
        tiger.cry();
    }
}

public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.method();
    }
}
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
# 2.16.4.1.2 使用匿名内部类实现接口
interface IA {
    public void cry();
}

class Tiger implements IA {
    public void cry() {
        System.out.println("虎啸");
    }
}

class Outer {
    private int n1 = 10; //属性
    
    public void method() { //方法
        //这里想使用 IA 接口,并创建对象
        //Tiger 类只是使用一次,后面再不使用,可以使用匿名内部类来简化开发,还有就是如果有不同的对象想使用接口,都要定义一个类去实现接口,很繁琐,所以可以使用 匿名内部类实现接口
        IA tiger = new IA() {
            public void cry() {
                System.out.println("虎啸");
            }
        };
        //tiger 的编译类型是 IA
        //tiger 的运行类型是匿名内部类
        tiger.cry();
        
        //其实底层会给匿名内部类分配类名,这里的是 Outer$1
        //JDK 底层在创建匿名内部类 Outer$1 时会马上就创建 Outer$1 实例,并且把地址返回给 tiger
        //匿名内部类使用一次后就不能再使用
        /*
        	看源码
        	class Outer$1 implements IA {
        		public void cry() {
        			System.out.println("虎啸");
        		}
        	}
        */
    }
}

public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.method();
    }
}
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

# 2.16.4.2 匿名内部类 - 基于类

class Father {
    public Father(String name) { //构造器
    }
    
    public void test(){ //方法
        
    }
}

class Outer { //外部类
    private int n1 = 10; //属性
    
    public void method() { //方法
        
        //基于类的匿名内部类
        //如果没有大括号就是创建一个 Father 对象实例,有大括号是创建一个匿名内部类
        //有大括号:father 的编译类型是 Father,father 的运行类型是 Outer$2
        Father father = new Father("jack") {
            public void test() {
                System.out.println("匿名内部类重写了 test 方法");
            }
        };
        
        father.test();
    }
}

public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.method();
    }
}
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

# 2.16.4.3 匿名内部类 - 基于抽象类

abstract class Animal { //抽象类
    abstract void eat();
}

class Outer { //外部类
    private int n1 = 10; //属性
    
    public void method() { //方法
        //基于抽象类的匿名内部类,如果是基于抽象类的话,一定要实现抽象类的方法
        Animal animal = new Animal() {
            void eat() {
                System.out.println("小狗吃骨头");
            }
        };
        animal.eat();
    }
}

public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.method();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 2.16.4.4 匿名内部类细节

  1. 匿名内部类的语法比较奇特,因为匿名内部类既是一个类的定义,同时它本身也是一个对象,因此从语法上看,它既有定义类的特征,也有创建对象的特征
  2. 可以直接访问外部类的所有成员,包含私有的
  3. 不能添加访问修饰符,因为它的地位就是一个局部变量
  4. 作用域:仅仅在定义它的方法或代码块中
  5. 匿名内部类访问外部类成员的方式是直接访问
  6. 外部其它类不能访问匿名内部类,因为匿名内部类是一个局部变量
  7. 如果外部类和内部类的成员重名时,内部类访问的话,默认遵循就近原则,如果想访问外部类的成员,则可以使用 外部类名.this.成员 去访问

# 2.16.4.5 匿名内部类实践

当做实参直接传递,简洁高效

传统方法:

interface IL {
    void show();
}

//传统方法:需要实现接口,然后 new 出对象实例后把实例传给形参
class Picture implements IL {
    public void show() {
        System.out.println("这是好电影")
    }
}

public class Test {
    public static void main(String[] args) {
        Picture picture = new Picture();
        f1(picture);
    }
    
    public static void f1(IL il) {
        il.show();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

使用匿名内部类:

interface IL {
    void show();
}

public class Test {
    public static void main(String[] args) {
        //匿名内部类当做实参直接传递,这时就不需要写实现接口的类,并且不用硬编码
        f1(new IL() {
           public void show() {
               System.out.println("这是好电影")
           } 
        });
    }
    
    public static void f1(IL il) {
        il.show();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

练习:

  1. 有一个铃声接口 Bell,里面有个 ring 方法
  2. 有一个手机类 Cellphone,具有闹钟功能 alarmclock,参数是 Bell 类型
  3. 通过匿名内部类作为参数测试手机类的闹钟功能:分别打印起床了和上课了
interface Bell {
    void ring();
}

class Cellphone {
    public void alarmclock(Bell bell) {
        bell.ring();
    }

}


public class Test {
    public static void main(String[] args) {
        Cellphone cellphone = new Cellphone();
        cellphone.alarmclock(new Bell() {
            @Override
            public void ring() {
                System.out.println("起床了");
            }
        });

        cellphone.alarmclock(new Bell() {
            @Override
            public void ring() {
                System.out.println("上课了");
            }
        });
    }
}
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

# 2.16.5 成员内部类

  1. 成员内部类是定义在外部类的成员位置,并且没有 static 修饰
  2. 可以直接访问外部类的所有成员,包含私有的
  3. 可以添加任意访问修饰符【public、protected、默认、private】,因为它的地位就是一个成员
  4. 成员内部类访问外部类成员的访问方式是:直接访问
  5. 外部类访问成员内部类的访问方式是:创建对象再访问
  6. 外部其它类访问成员内部类的访问方式是:看代码示例
  7. 如果外部类和内部类的成员重名时,内部类访问的话默认遵循就近原则,如果想访问外部类的成员则可以使用 外部类名.this.成员 进行访问
class Outer {
    private String name = "小明"; //外部类的成员属性

    class Inner { //成员内部类
        private String name = "小红"; //内部类的属性
        public void sleep() { //内部类的方法
            //内部类访问外部类的成员:直接访问
            //如果外部类的成员和内部类的成员重名,内部类在访问时遵循就近原则,如果想访问到外部类的成员,可以用 外部类名.this.成员 进行访问
            System.out.println(name + "睡觉" + " " + Outer.this.name + "也在睡觉");
        }

        public void eat() { //内部类的方法
            System.out.println("吃饭");
        }
    }

    public void invokeInner() { //外部类的成员方法
        //外部类访问内部类的成员的方式:创建对象再访问
        Inner inner = new Inner();
        inner.eat();
    }
}

class OtherOuter { //外部其它类
    public void otherOuterInvokeInner() {
        //外部其它类访问成员内部类的方式:Outer.Inner inner = new Outer().new Inner(); 先创建外部类再创建内部类
        Outer.Inner inner = new Outer().new Inner();
        inner.sleep();
        inner.eat();
    }
}

public class Test {
    public static void main(String[] args) {
        OtherOuter otherOuter = new OtherOuter();
        otherOuter.otherOuterInvokeInner();

        Outer outer = new Outer();
        outer.invokeInner();
    }
}
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

# 2.16.6 静态内部类

  1. 静态内部类是定义在外部类的成员位置,并且有 static 修饰
  2. 可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员
  3. 可以添加任意访问修饰符【public、protected、默认、private】,因为它的地位就是一个成员
  4. 静态内部类访问外部类的静态成员的访问方式:直接访问所有静态成员
  5. 外部类访问静态内部类的访问方式:创建对象再访问
  6. 外部其它类访问静态内部类:看代码示例
//简写
public class InnerDemo {
    public static void main(String[] args) {
        Inner inner = new Outer.Inner();
        inner.say();
    }
}
1
2
3
4
5
6
7
  1. 如果外部类和静态内部类的成员重名时,静态内部类访问时默认遵循就近原则,如果想要访问外部类的成员,则可以使用 外部类名.成员 去访问

# 2.17 易混淆

问题:Java 中引用类型的传递传的是地址,比如形参和实参,当传递的是引用类型时,传递的是地址,但是有个问题就是,形参接收到的是实参自己的地址,还是接收到的是实参所指向的地址

最近更新: 3/24/2025, 2:07:09 PM
第二章 面向对象编程

开发笔记   |