Java 面向对象



类和对象

概念

面向对象和面向过程都是解决问题的一种思路。

  • 面向过程

    • 是一种以过程为中心的编程思想,实现功能的每一步都是自己实现的。面向过程编程最易被初学者接受,其往往用一长段代码来实现指定功能,尽量忽略面向对象的复杂语法,即面向过程是“强调做什么,而不是以什么形式去做”。
    • 开发过程的思路是将数据与方法按照执行的逻辑顺序组织在一起,数据与方法分开考虑,也就是拿数据做操作。
  • 面向对象

    • 是一种以对象为中心的编程思想,通过指挥对象实现具体的功能,强调“必须通过对象的形式来做事情”。
    • 将数据(成员变量)与功能(成员方法)绑定到一起,进行封装,以增强代码的模块化和重用性,这样能够减少重复代码的编写过程,提高开发效率。
    • 总结:以类的方式组装代码,以对象的方式封装数据

面向对象编程的两个重要概念:类和对象

    • 类是具有相同属性和行为的事物的统称(或统称为抽象)。
    • 类是抽象的,在使用的时候通常会找到这个类的一个具体的存在来使用。
    • 一个类可以找到多个对象。
  • 对象

    • 某一个具体事物的存在,在现实世界中可以是看得见摸得着的。
    • 可以直接使用。
    • 类和对象之间的关系:类就是创建对象的模板。

类和对象之间的关系:类就是创建对象的模板。

image

类的定义

类由属性和行为两部分组成:

  • 属性:在类中通过成员变量来体现(类中方法外的变量)。
  • 行为:在类中通过成员方法来体现(去掉 static 关键字即可)。

类的定义步骤:

  1. 定义类;
  2. 编写类的成员变量;
  3. 编写类的成员方法。
public class Student {

    // 成员变量(属性)
    String name;
    int age;

    // 成员方法(行为)
    public void study(){  // 注意:没有 static 关键字
        System.out.println("学习");
    }
}

对象的创建和使用

创建对象的格式:

  • 类名 对象名 = new 类名();

调用成员的格式:

  • 对象名.成员变量
  • 对象名.成员方法();
public class TestStudent {

    public static void main(String[] args) {
        // 类名 对象名 = new 类名();
        Student stu = new Student();
        // 对象名.变量名
        // 初始化的默认值
        System.out.println(stu.name);  // null
        System.out.println(stu.age);   // 0

        stu.name = "张三";
        stu.age = 23;

        System.out.println(stu.name);  // 张三
        System.out.println(stu.age);   // 23

        // 调用:对象名.方法名();
        stu.study();
        System.out.println(stu);  // 打印全类名,即:包名+类名
    }
}

对象内存

Java 虚拟机要运行程序,必须要对内存进行空间的分配和管理。

Java 中的内存分配:

image

单个对象的内存

image

多个对象的内存

image

总结:多个对象在堆内存中,都有不同的内存划分,成员变量存储在各自的内存区域中,而成员方法则多个对象共用一份。

多个对象指向相同的内存

image

总结:当多个对象的引用指向同一个内存空间,变量所记录的地址值是一样的。此时只要有任何一个对象修改了内存中的数据,随后无论使用哪一个对象进行数据获取,都是修改后的数据。

查看对象内存地址

引入 maven 依赖:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

代码:

import org.openjdk.jol.vm.VM;

VM.current().addressOf(Object o);  // 获取内存地址

成员变量和局部变量

  1. 类中位置不同

    • 成员变量:位于方法外部。
    • 局部变量:位于方法内部。
  2. 内存中位置不同

    • 成员变量:位于堆内存。
    • 局部变量:位于栈内存。
  3. 生命周期不同

    • 成员变量:随着对象的存在而存在,随着对象的消失而消失。
    • 局部变量:随着方法的调用而存在,随着方法的调用完毕而消失。
  4. 初始化值不同

    • 成员变量:有默认初始值。
    • 局部变量:没有默认初始化值,必须先定义和赋值才能使用。

封装

封装思想

封装的概述:

  • 封装是面向对象三大特征之一(封装、继承、多态)。
  • 封装是指将对象的属性和行为进行包装,不需要让外界知道具体的实现细节。

封装的原则:

  • 将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。
  • 实现方式:将成员变量修饰为 private,并提供对应的 getXxx()/setXxx() 方法。

封装的好处:

  • 通过方法来控制成员变量的操作,提高了代码的安全性。
  • 用方法将代码进行封装,提高了代码的复用性。

private 关键字

概述:private 是一个权限修饰符,可以用来修饰成员(成员变量、成员方法)。

特点 : 被 private 修饰的成员,只能在本类进行访问。而针对 private 修饰的成员变量,如果需要被其他类使用,则可以提供相应的方法:

  • 提供get变量名()方法,用于获取成员变量的值,方法用 public 修饰。

  • 提供set变量名(参数)方法,用于设置成员变量的值,方法用 public 修饰。

示例:

public class Student {

    private String name;
    private int age;

    // 提供修改name属性的封装方法
    public void setName(String a){
        name = a;
    }

    public void getName(){
        System.out.println("姓名:"+name);
    }

    // 提供修改age属性的封装方法
    public void setAge(int a){
        if(age<0 || age>120) {
            System.out.println("年龄【"+age+"】设置有误");
        } else {
            age = a;
        }
    }

    public void getAge(){
        System.out.println("年龄:"+age);
    }

    public void personalFile(){
        System.out.println("姓名:"+name+","+"年龄:"+age);
    }
}


// 学生测试类
class StudentTest{
    public static void main(String[] args) {
        Student student = new Student();
        // student.age = 12;  此行会报错
        student.setAge(12);
        student.getAge();  // 12
        student.setName("小明");
        student.personalFile();  // 姓名:小明,年龄:12
    }
}

this 关键字

this 关键字代表了当前调用方法的引用,即哪个对象调用的方法,this 就代表哪一个对象

Java 的 this 只能用于方法体中。当一个对象创建后,Java 虚拟机就会给这个对象分配一个引用自身的指针,这个指针的名字就是 this。因此,this 只能在类种的非静态方法中使用,静态方法和静态代码块中不能出现 this,并且 this 只能和特定的对象关联,而不和类关联,即同一个类的不同对象有不同的 this。

作用:

  1. 如果存在成员变量与局部变量同名时,在方法内部默认访问的是局部变量,那么可以通过 this 关键字访问成员变量。

    • 方法的形参如果与成员变量同名,不带 this 修饰的变量指的是形参,而不是成员变量。
    • 方法的形参没有与成员变量同名,不带 this 修饰的变量指的是成员变量(编译器会自动在该变量的前面添加 this 关键字)。
  2. 用于在构造方法中引用满足指定参数类型的构造方法,只能引用一个构造方法且必须位于开始的位置。

注意:

  1. this 关键字调用其他构造函数时,this 关键字必须要是构造函数中的第一句语句。
  2. this 关键字在构造函数中不能出现相互调用的情况,因为是个死循环。

示例:

// 经典用法
class Student{

    int id;
    String name;

    public Student(int id, String name){
        this.id = id;  // 局部变量的id给成员变量的id赋值
        this.name = name;
    }

     public Student(String name){
        this.name = name;
     }
}
// 以上示例的两个 this.name = name 可简便处理(尽量优化重复代码),即作用 2
class Student{

    int id;
    String name;

    public Student(int id, String name){
        this(name);  // 调用其他构造方法,且必须放在第一句
        // this();  // 无参的构造函数被调用
        this.id = id;  // 局部变量的id给成员变量的id赋值
        System.out.println("两个参数的构造函数被调用了");
    }

     public Student(String name){
          this.name = name;  // 已在双参的构造函数中调用
          System.out.println("一个参数的构造函数被调用了");
     }

     public Student(){
          System.out.println("无参的构造函数被调用了");   // 已在双参的构造函数中调用
     }
}

this 内存原理

image

image


构造方法

格式:

  • 方法名必须与类名一致。
  • 没有返回值类型,不能使用 void 进行修饰。
  • 没有返回值(不能有 retrun 带回结果数据)。

执行时机:

  • 创建对象的时候调用,每创建一次对象,就会执行一次构造方法。
  • 不能手动调用构造方法。

构造方法的创建:

  • 如果没有定义构造方法,系统将给出一个默认的无参数构造方法(Java 编译器添加的无参构造函数的权限修饰符与类的权限修饰符一致);如果定义了构造方法,系统将不再提供默认的构造方法。建议:无论是否使用,都手动书写无参数构造方法,和带参数构造方法。
  • 构造方法不会被继承。
  • 在构造方法中才能调用重载的构造方法,语法为this.(实参列表),且必须为第一行,后面可以继续有代码。
  • 在调用重载的构造方法时,不可以使用成员变量。因为从语义上讲,这个对象还没被初始化完成。

示例:标准类的代码编写和使用

package com.demo;


// 学生类: 封装数据
public class Student {
    
    private String name;
    private int age;

    public Student() {
    }

    public Student(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 void show(){
        System.out.println(name + "..." + age);
    }
}


// 学生测试类
class TestStudent {
    
    public static void main(String[] args) {
        // 1. 无参数构造方法创建对象,通过 setXxx 方法给成员变量进行赋值
        Student stu1 = new Student();
        stu1.setName("张三");
        stu1.setAge(23);
        stu1.show();

        // 2. 通过带参数构造方法, 直接给成员变量赋值
        Student stu2 = new Student("李四", 24);
        stu2.show();
    }
}

static 关键字

static 是静态的意思,可以修饰成员方法或成员变量。

static 修饰的特点:

  • 被类的所有对象共享(是我们判断是否使用静态关键字的条件)。
  • 随着类的加载而加载,优先于对象存在(对象需要类被加载后,才能创建)。
    • 静态方法与非静态方法的字节码文件是同时存在内存中的;只是静态的成员变量是优先于对象存在的。
  • 静态成员变量只会在数据共享区中维护一份,而非静态成员变量的数据会在每个对象中都维护一份。
  • 可以通过类名调用(推荐:方便且省内存),也可以通过对象名调用。
  • 什么时候用 static 修饰方法:如果一个方法没有直接访问到非静态的成员时,那么就可以使用 static 修饰,一般用于工具类型的方法。

示例:

class Student{
    String name;
    static String country = "中国";

    public Student(String name){
        this.name = name;
    }
}

public class Test{
    public static void main(String[] args) {
        Student s1 = new Student("张三");
        Student s2 = new Student("李四");
        s1.country = "小日本";  // 修改了数据共享区的country值,并且数据只有一份
        System.out.println("姓名:"+s1.name+"国籍:"+s1.country);  // 因此此时country是"小日本"
        System.out.println("姓名:"+s2.name+"国籍:"+s2.country);  // 因此此时country是"小日本"
    }
}

static 注意事项:

  1. 静态方法可以调用类名或者对象进行调用;而非静态方法只能通过对象进行调用。
  2. 静态方法可以直接访问静态的成员,不能直接访问非静态的成员(变量与方法);但可以在静态方法里创建对象来访问非静态的数据。
  3. 非静态方法可以直接访问静态与非静态的成员。
  4. 静态方法不能出现 this 或者 super 关键字(this 与 super 都是指引用的对象空间,即非静态变量)。

使用案例:统计一个类被使用了多少次创建对象,该类对外显示被创建的次数

class Caculate{

    String name;
    static int count;

    {
        count++;  // 无论调用哪个构造函数,都会先执行构造代码块
    }

    public Caculate(String name){
        this.name = name;
    }

    public Caculate(){

    }

    public void showCount(){
        System.out.println("创建了"+count+"次对象");
    }
}


public class Demo {

    public static void main(String[] args) {
        Caculate a = new Caculate();
        Caculate b = new Caculate("xiha");
        Caculate c = new Caculate();
        c.showCount();  // 创建了3次对象
    }

}

代码块

在 Java 中,使用 { } 括起来的代码被称为代码块。


局部代码块

  • 位置: 方法中定义。
  • 作用: 限定变量的生命周期,及早释放,提高内存利用率。

示例代码:

public class Test {

    public static void main(String[] args) {
        {
            int a = 10;
            System.out.println(a);
        }
       // System.out.println(a);  找不到变量a
    }
}

构造代码块

  • 位置: 类中方法外定义。
  • 特点:优先于构造方法执行。
  • 作用: 将多个构造方法中相同的代码,抽取到构造代码块中,提高代码的复用性。

示例代码:

public class Test {

    public static void main(String[] args) {
        Student stu1 = new Student();  // 先打印“好好学习”
        Student stu2 = new Student(10);  // 先打印“好好学习”
    }
}

class Student {

    {
        System.out.println("好好学习");
    }

    public Student(){
        System.out.println("无参构造方法");
    }

    public Student(int a){
        System.out.println("带参构造方法");
    }

}

静态代码块

  • 位置: 类中方法外定义。
  • 特点: 通过 static 关键字修饰,随着类的加载而加载,并且只执行一次。
  • 作用: 在类加载的时候做一些数据初始化的操作。
public class Test {

    public static void main(String[] args) {
        Student stu1 = new Student();  // 会打印“好好学习”
        Student stu2 = new Student(10);  // 不会打印“好好学习”
    }
}

class Student {

    static {
        System.out.println("好好学习");
    }

    public Student(){
        System.out.println("无参构造方法");
    }

    public Student(int a){
        System.out.println("带参构造方法");
    }
}

初始化

成员初始化

Java 会尽量保证每个成员变量在使用前都会获得初始化。

  • 默认值的初始化:
类型 初始值
boolean false
char /u0000
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d
引用类型 null
  • 指定值的初始化:
int a = 11  // 指定 a 的初始化值不是 0,而是 11

初始化顺序

以下代码的执行结果反映了各种属性/方法的初始化顺序:

  1. 静态属性
  2. 静态代码块/静态变量(二者的优先级按执行顺序)
  3. 普通属性
  4. 普通代码块
  5. 构造函数
public class LifeCycle {

    // 静态属性
    private static String staticField = getStaticField();

    // 静态代码块
    static {
        System.out.println(staticField);
        System.out.println("静态代码块初始化");
    }

    // 普通属性
    private String field = getField();

    // 普通代码块
    {
        System.out.println(field);
    }

    // 构造方法
    public LifeCycle() {
        System.out.println("构造方法初始化");
    }

    public static String getStaticField() {
        String statiFiled = "Static Field Initial";
        return statiFiled;
    }

    public static String getField() {
        String filed = "Field Initial";
        return filed;
    }

    // 主函数
    public static void main(String[] argc) {
        new LifeCycle();
    }

}

继承

继承介绍

继承的概念

继承是面向对象的三大特征之一,可以使得子类具有父类的属性和方法,还可以在子类中重新定义以及追加属性和方法。

实现继承的格式:继承通过 extends 实现

  • 格式:class 子类 extends 父类 { }
  • 举例:class Dog extends Animal

继承的好处:继承可以让类与类之间产生父子关系,即子类可以使用父类的非私有成员。

示例:

// 父类
public class Father {
    public void show() {
        System.out.println("show方法被调用");
    }
}

// 子类
public class Son extends Father {
    public void method() {
        System.out.println("method方法被调用");
    }
}

public class Test {
    public static void main(String[] args) {
        //创建对象,调用方法
        Father f = new Father();
        f.show();

        Son s = new Son();
        s.method();
        s.show();
    }
}

继承的好处和弊端

继承的好处:

  • 提高了代码的复用性(多个类相同的成员可以放到同一个父类中)。
  • 提高了代码的维护性(如果方法的代码需要修改,修改一处即可)。

继承的弊端:

  • 继承让类与类之间产生了关系,类的耦合性增强了。当父类发生变化时,子类的实现也不得不跟着变化,因此削弱了子类的独立性。

继承的应用场景:

  • 使用继承,需要考虑类与类之间是否存在 is..a 的关系,不能盲目使用继承。
  • is..a 的关系,即表示谁是谁的一种。例如:老师和学生是人的一种,那人就是父类,学生和老师就是子类。

Java 中类继承的特点

  1. 只支持单继承,不支持多继承
    • 错误范例:class A extends B, C
  2. 支持多层继承

多层继承示例:

public class GrandFather {

    public void drink() {
        System.out.println("爷爷爱喝酒");
    }

}

public class Father extends GrandFather {

    public void smoke() {
        System.out.println("爸爸爱抽烟");
    }

}

public class Son extends Father {
	// 此时,Son类中就同时拥有drink方法以及smoke方法
}

继承中的成员访问特点

继承中的成员变量访问特点

在子类方法中访问一个变量,采用的是就近原则。

  1. 子类局部范围找;
  2. 子类成员范围找;
  3. 父类成员范围找;
  4. 如果都没有就报错(不考虑多层父类)。

示例代码:

class Father {
    int num = 10;
}

class Son extends Father {
    int num = 20;
    public void show(){
        int num = 30;
        System.out.println(num);
    }
}

public class Test {
    public static void main(String[] args) {
        Son s = new Son();
        s.show();  // 输出show方法中的局部变量:30
    }
}

继承中的成员方法访问特点

通过子类对象访问一个方法,也是采用就近原则:

  1. 子类成员范围找;
  2. 父类成员范围找;
  3. 如果都没有就报错(不考虑多级父类)。

super 关键字

this 和 super 关键字

  • this:代表本类对象的引用。
  • super:代表父类对象的引用。

作用:

  1. 子父类存在同名的成员时,在子类中默认是访问子类的成员,可以通过 super 关键字指定访问父类的成员。
  2. 创建子类对象时,默认会先调用父类无参的构造函数,可以通过 super 关键字调用指定的父类构造方法。

super 调用父类构造方法时的注意事项:

  1. 如果在子类的构造方法上没有指定调用父类的构造方法,那么 Java 编译器会在子类的构造方法上自动添加 super()。
  2. 必须是子类构造函数中的第一个语句。
  3. super 与 this 不能同时共存于同一个构造函数中,因为两个语句都必须位于第一句。

super 与 this 的区别:

  1. 代表的事物不一致:
    • super 关键字代表的是父类空间的引用。
    • this 关键字代表的是本类对象的引用。
  2. 使用前提不一致:
    • super 必须要有继承关系才能使用。
    • this 不需要存在继承关系也可使用。
  3. 调用成员的区别:
    • super 是调用父类的成员。
    • this 是调用本类的成员。

继承中的构造方法访问特点

注意:子类中所有的构造方法默认都会访问父类中的无参构造方法

  • 子类会继承父类中的数据,可能还会使用父类的数据。所以,子类在初始化之前,一定要先完成父类数据的初始化。原因在于,每一个子类构造方法的第一条语句默认都是:super()。

问题:如果父类中没有无参构造方法,只有带参构造方法,该怎么办呢?

  • 方法一:通过使用 super(...) 关键字显式地调用父类的带参构造方法。
  • 方法二:子类通过 this(...) 调用本类的其他构造方法,本类其他构造方法再通过 super(...) 手动调用父类的带参构造方法。

super 内存图

对象在堆内存中,会单独存在一块 super 区域,用来存放父类的数据。

image


方法重写

方法重写的概念:子类出现了和父类中一模一样的方法声明(方法名和参数列表均一样)。

方法重写的应用场景:当子类即需要父类的功能,又需要有自己特有的功能时,可以重写父类中的方法,这样即沿袭了父类的功能,又定义了子类特有的功能。

Override 注解:用来检测当前的方法是否是重写的方法,起到【校验】的作用。

方法重写的注意事项:

  1. 私有方法不能被重写(父类私有成员子类是不能继承的)。
  2. 子类方法的访问权限不能更低,即子类访问权限修饰符不能严于父类(public > 默认 > 私有)。
  3. 静态方法不能被重写。如果子类也有同名静态方法,则并不是重写的父类的方法。
  4. 构造方法不能被重写。

示例:

public class Father {
    private void show() {
        System.out.println("Father中show()方法被调用");
    }

    void method() {
        System.out.println("Father中method()方法被调用");
    }
}

public class Son extends Father {

    /* 编译【出错】,子类不能重写父类私有的方法*/
    @Override
    private void show() {
        System.out.println("Son中show()方法被调用");
    }
   
    /* 编译【出错】,子类重写父类方法的时候,访问权限需要大于等于父类 */
    @Override
    private void method() {
        System.out.println("Son中method()方法被调用");
    }

    /* 编译【通过】,子类重写父类方法的时候,访问权限需要大于等于父类 */
    @Override
    public void method() {
        System.out.println("Son中method()方法被调用");
    }
}

抽象类和抽象方法

特点

抽象类的概述:

我们在描述一类事物的时候,发现这种事物确实存在着某种行为,但是这种行为目前并不是具体的,那么我们可以抽取这种行为的声明,但是不去实现该种行为,这时候这种行为称为抽象的行为,我们就需要使用抽象类。

抽象方法指一些只有方法声明,而没有具体方法体的方法。抽象方法一般存在于抽象类或接口中。

抽象类的好处:

声明抽象类的唯一目的是将来对该类进行扩充,即强制要求子类一定要实现指定的方法(一个方法只要有大括号,就是具体的实现)。

抽象类的特点:

抽象类和抽象方法必须使用 abstract 关键字修饰:

// 抽象类的定义
public abstract class 类名 {}

// 抽象方法的定义
public abstract void eat();

抽象类的使用细节:

  1. 如果一个方法没有方法体(即大括号),那么该方法必须要使用 abstract 修饰,把该函数修饰成抽象的方法。
  2. 如果一个类出现了抽象方法,那么该类也必须使用 abstract 修饰。
  3. 如果一个非抽象类继承了抽象类,那么必须要把抽象类的所有抽象方法全部实现。
  4. 抽象类可以存在非抽象方法。
  5. 抽象类可以不存在抽象方法(虽然语法支持不存在,但一般会存在抽象方法)。
  6. 抽象类是不能创建对象的。原因:因为抽象类中是存在抽象方法的,如果抽象类能创建对象,那么用抽象的对象来调用抽象方法是没有意义的。
  7. 抽象类是存在构造函数的,用来给子类创建对象时初始化父类的属性。

abstract 不能与以下关键字共同修饰一个方法:

  1. private(导致子类无法调用而无法重写抽象方法)
  2. static(导致可用类名创建对象)
  3. final(导致子类无法重写抽象方法)

抽象类的示例:

  • 案例需求:

    • 定义猫类(Cat)和狗类(Dog)
    • 猫类成员方法:eat(猫吃鱼)drink(喝水…)
    • 狗类成员方法:eat(狗吃肉)drink(喝水…)
  • 实现步骤:

    1. 猫类和狗类中存在共性内容,应向上抽取出一个动物类(Animal)。
    2. 父类 Animal 中,无法将 eat 方法具体实现描述清楚,所以定义为抽象方法。
    3. 抽象方法需要存活在抽象类中,应将 Animal 定义为抽象类。
    4. 让 Cat 和 Dog 分别继承 Animal,重写 eat 方法。
    5. 测试类中创建 Cat 和 Dog 对象,调用方法测试。
  • 代码实现:
// 动物类
public abstract class Animal {

    public Animal() {
    }

    public void drink(){
        System.out.println("喝水");
    }

    public abstract void eat();
}

// 猫类
public class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
}

// 狗类
public class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("狗吃肉");
    }
}

// 测试类
public static void main(String[] args) {
        Dog d = new Dog();
        d.eat();
        d.drink();

        Cat c = new Cat();
        c.drink();
        c.eat();

        // Animal a = new Animal();  报错:抽象类不能实例化
        // a.eat();
    }

模板设计模式

设计模式:

  • 设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
  • 使用设计模式是为了代码可重用、让代码更容易被他人理解、保证代码可靠性、程序的重用性。

模板设计模式:

  • 例如抽象类就可以看做成一个模板,模板中不能明确的东西定义成抽象方法。
  • 让使用模板的类(抽象类的子类)去重写抽象方法实现具体需求。

模板设计模式的优势:

  • 模板已经定义了通用结构,使用者只需要关心自己需要实现的功能即可。

示例代码:

  • 模板类:
/*
    作文模板类
 */
public abstract class CompositionTemplate {

    public final void write(){
        System.out.println("<<我的爸爸>>");
        body();
        System.out.println("啊~ 这就是我的爸爸");
    }

    public abstract void body();
}
  • 实现类 A:
public class Tom extends CompositionTemplate {

    @Override
    public void body() {
        System.out.println("那是一个秋天, 风儿那么缠绵,记忆中, " +
                "那天爸爸骑车接我放学回家,我的脚卡在了自行车链当中,爸爸蹬不动,他就站起来蹬...");
    }
}
  • 实现类 B:
public class Tony extends CompositionTemplate {
    @Override
    public void body() {

    }

    /*public void write(){

    }*/
}
  • 测试类:
public class Test {
    public static void main(String[] args) {
        Tom t = new Tom();
        t.write();
    }
}

final 关键字

final 表示最终的意思,可以修饰类、成员方法和成员变量。

  • final 修饰类:该类不能被继承(不能有子类,但可以有父类),且 final 类中的所有成员方法都会被隐式指定为 final 方法。
  • final 修饰方法:该方法不能被重写。
  • final 修饰变量:表明该变量是一个常量,不能再次赋值。
    • 变量是基本类型:不能改变的是值。
    • 变量是引用类型:不能改变的是地址值,但地址里面的内容是可以改变的。

示例:

public static void main(String[] args){
    final Student s = new Student(23);
  	s = new Student(24);  // 错误:不能改变s的地址值
 	s.setAge(24);  // 正确
}

接口

接口的概述

接口相当于就是对外的一种约定和标准。这里拿操作系统举例子,为什么会有操作系统?就是为了屏蔽软件的复杂性和硬件的简单性之间的差异,为软件提供统一的标准。

Java 接口存在的意义:

  1. 定义约束规范:接口的本质是契约
  2. 程序解耦:约束和实现分离
  3. 用来做功能的扩展

接口的特点:

  • 接口用关键字 interface 修饰:public interface 接口名 {}

  • 类实现接口用 implements 表示:public class 类名 implements 接口名 {}

  • 接口不能实例化,只可以创建接口的实现类对象。

接口的成员特点:

  • 成员变量:只能是常量,默认修饰符:public static final(即使少写修饰符,编译器也会自动添加)。

  • 构造方法:没有,因为成员变量已经是固定的,方法又是抽象的而不能调用,因此构造方法没意义。

  • 成员方法:只能是抽象方法,默认修饰符:public abstract

  • 修饰符:接口中只能使用两种访问修饰符,一种是public,它对整个项目可见;一种是default缺省值,它只具有包访问权限。

类和接口的关系:

  • 类与类的关系:继承关系。只能单继承,但可以多层继承。

  • 接口与接口的关系:继承关系。可以单继承,也可以多继承。

  • 类与接口的关系:实现关系。可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口。

疑问:Java 为什么不支持多继承类,而支持多实现接口?

  1. 若直接继承多个类,且多个父类有同名的方法时,不知调用哪个;
  2. 支持多实现接口是因为即使多个接口有同名的方法,但在类中实现后,是调用在类中已经具体实现的方法。

接口的组成

  • 常量:public static final

  • 抽象方法:public abstract

  • 默认方法(Java 8)

  • 静态方法(Java 8)

  • 私有方法(Java 9)


接口的默认方法

// 定义格式
public default 返回值类型 方法名(参数列表) {
}

作用:解决接口升级的问题

  • 以前,当创建了一个接口,并且已经被大量的类实现时,如果需要再扩充这个接口的功能加新的方法,就会导致所有实现类需要重写这个方法。
  • 而如果在接口中已实现了默认方法,就不会有这个问题(不覆盖的话就执行默认方法)。所以从 JDK8 开始就新加了接口的默认方法,便于接口的扩展。

接口中默认方法的规则:

  • public 可以省略,default 不能省略。
  • 接口中既可以定义抽象方法,又可以定义默认方法,默认方法不是抽象方法。
  • 实现类可以直接调用接口中的默认方法,即继承了接口中的默认方法。
  • 默认方法不是抽象方法,所以不强制被重写;但也可以被重写,重写的时候去掉 default 关键字。
  • 如果实现了多个接口,多个接口中存在相同的默认方法声明,实现类就必须对该默认方法进行重写。

接口的静态方法

// 定义格式
public static 返回值类型 方法名(参数列表) {

}

注意事项:

  • 静态方法只能通过接口名调用,不能通过实现类名或者对象名调用。
  • public 可以省略,static 不能省略。

接口的私有方法

私有方法的产生原因:

  • Java 9 中新增了带方法体的私有方法,这其实在 Java 8 中就埋下了伏笔:Java 8 允许在接口中定义带方法体的默认方法和静态方法。
  • 这样可能就会引发一个问题:当两个默认方法或者静态方法中包含一段相同的代码实现时,程序必然考虑将这段实现代码抽取成一个共性方法,而这个共性方法是不需要让别人使用的,因此要用私有给隐藏起来,这就是 Java 9 增加私有方法的必然性。

定义格式:

// 格式一
private 返回值类型 方法名(参数列表) { 
}

// 格式二
private static 返回值类型 方法名(参数列表) {
}

注意事项:

  • 默认方法可以调用私有的静态方法和非静态方法。
  • 静态方法只能调用私有的静态方法。

接口与抽象类的异同

相同点

  1. 都可以被继承。
  2. 都不能直接实例化。
  3. 都可以包含抽象方法。
  4. 派生类必须实现未实现的方法。

不同点

  1. 接口支持多继承;抽象类不支持多继承。
  2. 一个类只能继承一个抽象类,而一个类可以实现多个接口。
  3. 接口中的成员变量只能是常量(public static final 类型);抽象类中的成员变量可以是各种类型。
  4. 接口只能定义抽象方法;抽象类既可以定义抽象方法,也可以定义实现方法。
  5. 接口中不能含有静态代码块以及静态方法;抽象类则可以有静态代码块和静态方法。

多态

多态的概述

多态

  • 指同一种类型的对象执行同一个方法时可以表现出不同的行为特征。

  • 多态把子类对象主观地看作是其父类型的对象,因此其父类型就可以是很多种类型。

使用前提:必须存在继承或者实现的关系。

使用方式

  • 父类的引用变量指向子类的对象
  • 接口的引用变量指向了接口实现类的对象

使用结果

  • 子父类存在同名静态方法时,调用的是父类的方法(看引用类型)。
  • 子父类存在同名实例方法重写方法时,调用的是子类的方法(看对象类型)。
  • 如果想调用子类的独有方法,则需要进行强制类型转换。
package com.annotation;

class Person {

    public static void name() {
        System.out.println("人的名字");
    }
    public void age() {
        System.out.println("人的年龄");
    }
    public void eat() {
        System.out.println("人在吃饭");
    }
    public void job() {
        System.out.println("人的工作");
    }
}

class Student extends Person {

    public static void name() {
        System.out.println("学生的名字");
    }
    public void age() {
        System.out.println("学生的年龄");
    }
    @Override
    public void eat() {
        System.out.println("人在吃饭");
    }

    // 子类独有方法
    public void score() {
        System.out.println("学生的分数");
    }
}

// 测试类
public class ErrorDemo {
    public static void main(String[] args) {
        Student s = new Student();
        Person p = new Student();
        // 同名静态方法
        s.name();  // 学生的名字
        p.name();  // 人的名字
        // 同名成员方法
        s.age();  // 学生的年龄
        p.age();  // 学生的年龄
        // 重写方法
        s.eat();  // 人在吃饭
        p.eat();  // 人在吃饭
        // 子类独有方法
        ((Student) p).score();  // 学生的分数
        // 父类独有方法
        p.job();  // 人的工作
    }
}

多态的好处:提高了程序的扩展性

  1. 多态用于形参类型的时候,可以接收更多类型的数据。
  2. 多态用于返回值类型的时候,可以返回更多类型的数据。

代码示例:

abstract class MyShape {

    public abstract void getArea();

    public abstract void getLength();
}

class Circle extends MyShape {

    public static final double PI = 3.14;

    double r;

    public Circle(double r) {
        this.r = r;
    }

    public void getArea(){
        System.out.println("圆形的面积:"+PI*r*r);
    }

    public void getLength() {
        System.out.println("圆形的周长:"+PI*2*r);
    }
}

class Rect extends MyShape {

    int width;
    int height;

    public Rect(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public void getArea() {
        System.out.println("矩形的面积:"+width*height);
    }

    public void getLength() {
        System.out.println("矩形的周长:"+2*(width+height));
    }
}

/*
即使后来增加了梯形的需求,也无需改动其余代码,直接创建对象即可,即提高代码的拓展性。
class 梯形 extends MyShape{
           ……
           ……
}
*/

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

        // 需求1 调用
        Circle c = new Circle(4.0);
        print(c);  // 即 MyShape s = new Circle(4.0);
        Rect r = new Rect(3, 4);
        print(r);
        // 梯形 t = new 梯形(3, 4);
        //print(t);

        // 需求2 调用
        MyShape s = getShape(1);  // 调用了使用多态的方法,定义的变量类型要与返回值类型一致
        s.getArea();
        s.getLength();
    }

    // 需求1:定义一个函数可以接收任意类型的图形对象,并且打印图形面积与周长
    public static void print(MyShape s){
        s.getArea();
        s.getLength();
    }

    // 需求2:定义一个函数可以返回任意类型的图形对象
    public static MyShape getShape(int i){
        if(i==0){
            return new Circle(4.0);
        }else{
            return new Rect(3, 4);
        }
    }
}

JAVA 编译器:编译看左边,运行不一定看右边

  • 编译看左边:编译时会检查引用变量所属的类是否具备指定的成员,如果不具备则马上编译报错。
  • 运行不一定看右边:如果子类中没有覆盖指定的方法,就去父类里找,父类里没有,就去父类的父类找,只要能让一个引用指向这个对象,就说明这个对象肯定是这个类型或者其子类的一个实例(否则赋值会报 ClassCastException),总归有父类兜底。

instanceof 与 多态转型

  • 向上转型:父类引用指向子类对象

  • 向下转型:子类型 对象名 = (子类型) 父类引用

代码示例:

class Father {
    public void show(){
        System.out.println("Father..show...");
    }
}

class Son extends Father {
    @Override
    public void show() {
        System.out.println("Son..show...");
    }

    public void method(){
        System.out.println("我是子类特有的方法, method");
    }
}

public class Test {
    public static void main(String[] args) {
        // 1. 向上转型:父类引用指向子类对象
        Father f = new Son();
        f.show();
        
        // 多态不能调用子类特有的成员
        // f.method();  // 此行报错
        // 解决方式一: 直接创建子类对象
        // 解决方式二:向下转型

        // 2. 向下转型:父类类型转换回子类类型
        Son s = (Son) f;
        s.method();  // "我是子类特有的方法, method"
    }
}

多态转型存在的风险和解决方案:

  • 风险:如果被转的引用类型变量,对应的实际类型和目标类型不是同一种类型,那么在转换的时候就会出现 ClassCastException。

  • 解决方案:先使用 instanceof 关键字判断类型。

  • 使用方法:变量名 instanceof 类型

  • 通俗理解:判断 instanceof 关键字左边的变量是否属于右边的类型,返回 boolean 类型的结果。

  • 判断规则:x instanceof y

    • 如果 x、y 属于父子或子父关系(包括多层),则编译通过。
    • 如果 x 指向的对象是 y 的类型或其子类,则返回 true 。
    • 如果 x 指向的对象的类型高于 y 的类型,则返回 false 。

示例:

abstract class Animal {
    public abstract void eat();
}

class Dog extends Animal {

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

    public void watchHome(){
        System.out.println("看家");
    }
}

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

public class Test {
    public static void main(String[] args) {
        useAnimal(new Dog());
        useAnimal(new Cat());
    }

    public static void useAnimal(Animal a){
        // Animal a = new Dog();
        // Animal a = new Cat();
        // a.eat();
        // a.watchHome();  // 无法通用:因为 Cat 没有 watchHome 方法,会报 ClassCastException 类型转换异常

        // 解决方案:先判断 a 变量的类型是否是 Dog
        if(a instanceof Dog){
            // 向下转型
            Dog dog = (Dog) a;
            dog.eat();
            dog.watchHome();
        } else {
            a.eat();
        }
    }
}

内部类

内部类的概念:在一个类中定义一个类。例如,在一个 A 类的内部定义了一个 B 类,那么 B 类就被称为内部类。

内部类的 class 文件外部类$内部类.class,其好处:便于区分该 class 文件是属于哪个外部类的。

内部类的基本使用

内部类的定义格式:

// 格式
class 外部类名 {
	修饰符 class 内部类名{

	}
}

// 示例
class Outer {
    public class Inner {

    }
}

内部类的访问特点:

  • 内部类可以直接访问外部类的成员,包括私有。
  • 外部类要访问内部类的成员,必须先创建内部类的对象。

示例:

public class Outer {
    private int num = 10;

	public class Inner {
        public void show() {
            System.out.println(num);
        }
    }

    public void method() {
        Inner i = new Inner();
        i.show();
    }
}

成员内部类

成员内部类的定义位置:跟成员变量是一个位置。

应用场景:事物 A 里存在另外一个事物 B,而事物 B 需要经常访问事物 A 的成员,那么就使用成员内部类。

外部访问成员内部类的格式外部类名.内部类名 对象名 = 外部类对象.内部类对象;

// 示例
Outer.Inner oi = new Outer().new Inner();

私有成员内部类:

将一个类设计为内部类的目的,大多数都是不想让外界去访问,所以内部类的定义应该私有化。私有化之后,再提供一个可以让外界调用的方法,由该方法内部创建内部类对象并调用。

静态成员内部类:

  • 静态成员内部类的访问格式:外部类名.内部类名 对象名 = new 外部类名.内部类名();

  • 静态成员内部类中的静态方法:外部类名.内部类名.方法名();

综合示例:

class Outer {

    int x = 100;

    // 成员内部类
    static class Inner {

        static int x = 10;

        static public void print(){
            System.out.println("x="+Outer.this.x);  // 输出外部类的同名成员变量
        }
    }
}

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

        //常规访问方法
        Outer.Inner i = new Outer().new Inner();
        i.print();

        //使用类名访问静态内部类的静态成员
        System.out.println(Outer.Inner.x);  // 静态成员变量
        Outer.Inner.print();  //静态成员函数

        //创建静态内部类的对象
        Outer.Inner i = new Outer.Inner();
        i.print();
    }
}

注意事项:

  1. 如果外部类与内部类存在同名的成员变量,那么在内部类中默认访问的是内部类的成员变量(可以通过外部类.this.成员变量名指定访问外部类的成员)。
  2. 私有的成员内部类只能在外部类提供一个方法创建内部类的对象进行访问,不能在其他类创建对象。
  3. 成员内部类一旦出现静态成员,那么该内部类也必须使用 static 修饰(原因:静态的成员数据是不需要对象才能访问的)。

局部内部类

局部内部类的定义位置:局部内部类是在方法中定义的类。

局部内部类的使用方式

  • 局部内部类,外界是无法直接使用的,需要在方法内部创建对象并使用。
  • 该类可以直接访问外部类的成员,也可以访问方法内的局部变量。

注意:

  • 如果局部内部类访问一个局部变量,那么该局部变量需加上 final 修饰。

  • 原因:方法执行完毕后,局部变量消失,而内部类对象直到垃圾回收才消失,即内部类对象生命周期比局部变量长,给人感觉局部变量的生命周期被延长了,所以要访问局部变量的复制品。

示例代码:

class Outer {

    int x = 100;

    public void print(){

        // 局部变量
        final int x = 10;  // 加上final后能给局部内部类访问
        
        // 局部内部类
        class Inner {

            public void getInner(){
                System.out.println("我是局部内部类..");
            }
        }

        Inner i = new Inner();
        i.getInner();
    }
}

class Test {
    public static void main(String[] args) {
        Outer o = new Outer();
        o.print();
    }
}

匿名内部类

概念:没有类名的类。匿名内部类只是没有类名,其他的一概成员都是具备的。

好处:简化书写。

使用前提:必须存在继承或者实现关系才能使用。

应用场景:匿名内部类一般用于实参。

定义格式new 已存在的类名() { 重写方法 }new 已存在的接口名() { 重写方法 }

new Inter(){
  @Override
  public void method(){}
}

匿名内部类的本质:是一个继承了该类或者实现了该接口的子类匿名对象。

匿名内部类的细节:匿名内部类可以通过多态的形式接收。

Inter i = new Inter(){
  @Override
    public void method(){
        
    }
}

示例:继承关系下的匿名内部类

abstract class Animal{

    public abstract Animal run();   //返回Animal类型

    public abstract void sleep();
}

class Outer{

    //需求:在方法内部定义一个类继承Animal类,然后同时调用run与sleep方法。
    public void print(){
        /*
        //局部内部类的做法:
        class Dog extends Animal{

            public void run(){
                System.out.println("斑点狗在跑..");
            }

            public void sleep(){
                System.out.println("斑点狗在睡觉..");
            }

            //若出现独有的方法,只能使用局部内部类的访问方法
            public void bite(){
                System.out.println("斑点狗在咬咬..");
            }
        }
        Dog d = new Dog();
        d.run();
        d.sleep();
        */

        /*
        //匿名内部类的做法:
        //匿名内部类与Animal是继承的关系;目前创建的是子类的对象。
        //方法一:
        Animal a = new Animal(){    //多态

            //实现父类的两个抽象方法
            public void run(){
                System.out.println("斑点狗在跑..");
            }
            public void sleep(){
                System.out.println("斑点狗在睡觉..");
            }

            /*若出现独有的方法,只能使用局部内部类的访问方法。
              因为匿名内部类没有类名,无法创建内部类的对象来进行强制类型转换。
            public void bite(){
                 System.out.println("斑点狗在咬咬..");
            }*/ 
        };
        a.run();
        a.sleep();
        */

        //方法二:
        new Animal(){   

            //实现父类的两个抽象方法
            public Animal run(){
                System.out.println("斑点狗在跑..");
                return this;  //返回目前调用的对象
            }
            public void sleep(){
                System.out.println("斑点狗在睡觉..");
            }

            /*若出现独有的方法,只能使用局部内部类的访问方法
            public void bite(){
                System.out.println("斑点狗在咬咬..");
            }
            */
        }.run().sleep();   //即this.sleep();

   }
}

class Test{
    public static void main(String[] args) {
        Outer o = new Outer();
        o.print();
    }
}

示例:实现关系下的匿名内部类

interface Dao{

    public void add();
}

class Outer{

    public void print(){
        //创建一个匿名内部类对象
        new Dao(){

            public void add(){
                System.out.println("添加成功");
            }
        }.add();
    }
}

class Test{
    public static void main(String[] args) {
        Outer o = new Outer();
        o.print();               
    }
}

匿名内部类在开发中的使用:

当发现某个方法需要使用到某个接口或抽象类的子类对象,我们就可以传递一个匿名内部类过去,来简化传统的代码。

interface Dao{

    public void add();
}

/*
class Outer{
    匿名内部类直接在实参创建即可。
}
*/

class Test{
    public static void main(String[] args) {
        test(new Dao(){

            public void add(){
                 System.out.println("添加成功");
            }
        });
    }

    public static void test(Dao d){  // 形参是一个接口类型,只能传接口的实现类
         d.add();
    }
}

posted @ 2021-09-20 21:40  Juno3550  阅读(233)  评论(0编辑  收藏  举报