面向对象——类与对象、封装性、构造方法、this关键性、代码块、static关键字、继承、final关键字、抽象类和接口、多态、内部类

面向对象的思想

面向对象是一种符合人类思维习惯的编程思想。现实生活中存在各种形态不同的事物,这些事物之间存在着各种各样的联系。在程序中使用对象映射现实中的事物,使用对象的关系描述现实当中事物之间的联系,这种思想就是面向对象
提到面向对象,自然会想到面向过程,面向过程就是通过分析得出解决问题所需的步骤,然后用函数把这些步骤逐一实现,使用的时候依次调用函数就可以了。面向对象则是把构成问题的事物按照一定规则划分为多个独立的对象,然后通过调用对象的方法来解决问题。当然,一个应用程序会包含多个对象,通过多个对象的相互配合实现应用程序的功能,这样当应用程序功能发生变动时,只需要修改个别的对象就可以了,从而使代码维护起来更加方便。面向对象的特性主要可以概括为封装性、继承性和多态性,接下来对这3种特性进行简单介绍。

封装性

封装是面向对象的核心思想,它有两层含义,第一层含义是指把对象的属性和行为看成是一个密不可分的整体,将这两者“封装”在一起(即封装在对象中);另外一层含义指“信息隐藏”,将不想让外界知道的信息隐藏起来。

继承性

继承性主要描述的是类与类之间的关系,通过继承,可以在原有类的基础上,对原有类的功能进行扩展。继承不仅增强了代码的复用性、提高了开发效率,还大大降低了程序产生错误的可能性,为程序的维护以及扩展提供了便利。
继承是子类继承父类的属性和方法,并在子类中添加特有的属性和方法,此时,子类拥有了父类的属性和方法,同时也满足了个性需求。

多态性

多态性是指在一个类中定义的属性和方法被其他类继承后,它们可以具有不同的数据类型或表现出不同的行为,这使得同一个属性和方法在不同的类中具有不同的语义。多态的特性使程序更抽象、便捷,有助于开发人员设计程序时分组协同开发。多态性与重写密不可分。

类与对象

在面向对象技术中,为了做到让程序对事物的描述与事物在现实中的形态保持一致,提出了两个概念,即类和对象。在 Java 程序中类和对象是最基本、最重要的单元。类表示某类群体的一些基本特征抽象,对象表示一个个具体的事物。
类表示的是共性的特征,对象表示的是个性的特征。

例如,在现实生活中,学生这个群体就可以表示为一个类,而某个具体的学生就可以称为对象。一个具体的学生有自己的姓名和年龄等信息,这些信息在面向对象的概念中称为属性,学生可以看书和打篮球,看书和打篮球这些行为在类中就称为方法。类与对象的关系如下图所示。

在这里插入图片描述
在上图中,学生可以看作一个类,小明、李华、大军都是学生类的对象。类用于描述多个对象的共同特征,它是对象的模板。对象用于描述现实中的个体,它是类的实例(特例)。对象是根据类创建的,一个类可以对应多个对象

  • 类:抽象,共同
  • 对象:具体,个性

类的对象

在面向对象的思想中最核心的就是对象,创建对象的前提是需要定义一个类,类是 Java 中一个重要的引用数据类型,也是组成 Java 程序的基本要素,所有的 Java 程序都是基于类的。
类是对象的抽象,用于描述一组对象的共同特征和行为。类中可以定义成员变量和成员方法,其中,成员变量用于描述对象的特征,成员变量也被称作对象的属性;成员方法用于描述对象的行为,可简称为方法。
类的定义格式如下:

class 类名 {
	成员变量;
    成员方法;
}
  • 成员变量 = 成员属性 = 属性
  • 变量 = 局部变量
  • 成员方法 = 方法

根据上述格式定义一个学生类,成员变量包括姓名(name)、年龄(age)、性别(sex);成员方法包括读书 read()。学生类定义的示例代码如下所示。

class Student {
    String name;    	// 声明String类型的变量name
    int age;        	// 声明int类型的变量age
    String  sex;    	// 声明String类型的变量sex
	// 定义 read () 方法
	void read() {  
		System.out.println("大家好,我是" + name + ", 我在看书!");
	}
}

以上代码中定义了一个学生类。其中,Student 是类名,name、age、sex 是成员变量,read() 是成员方法,在成员方法 read() 中可以直接访问成员变量 name。

留心:局部变量与成员变量的不同
在 Java 中,定义在类中的变量被称为成员变量,定义在方法中的变量被称为局部变量。如果在某一个方法中定义的局部变量与成员变量同名,这种情况是允许的,此时,在方法中通过变量名访问到的是局部变量,而并非成员变量。请阅读下面的示例代码:

class Student {
	int age = 30;		// 类中定义的变量被称作成员变量
	void read() {  
	     int age = 50;  // 方法内部定义的变量被称作局部变量
	     System.out.println("大家好,我" + age + "岁了, 我在看书!");
	}
}

上述代码中,在 student 类的 read ()方法中有一条打印语句,打印了变量 age,此时打印的是局部变量 age,也就是说当有另外一个程序调用 read() 方法时,输出的 age 值为50,而不是30。

对象的创建与使用

上一节定义了一个 Student 类,要想使用一个类则必须要创建该类的对象,类仅仅是提供了一个模板,一般情况下,类不支持直接调用。在 Java 程序中可以使用 new 关键字创建对象,使用 new 关键字创建对象的具体格式如下:

类名 对象名称 = null;
对象名称 = new 类名();

上述格式中,创建对象分为声明对象和实例化对象两步,也可以直接通过下面的方式创建对象,具体格式如下:

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

例如,创建 Student 类的实例对象,示例代码如下:

Student student = new Student();

创建实例对象 = 实例化 = 给对象分配内存空间并对属性进行初始化
了解对象的创建之后,就可以使用类创建对象,示例代码如下。

class Student01 {
	String name;       							// 声明姓名属性
	void read() {  
	      System.out.println("大家好,我是" + name + ",我在看书!");
	}
}
public class Test {
    public static void main(String[] args) {  
	      Student01 student = new Student01();		//创建并实例化对象
	}
}

上述代码在 main() 方法中实例化了一个 Student 对象,对象名称为 student。使用 new 关键字创建的对象是在堆内存分配空间。student 对象的内存分配如下图所示。

在这里插入图片描述
创建对象后,可以使用对象访问类中的某个属性或方法,对象属性和方法的访问通过“.”运算符实现,具体格式如下。

对象名称.属性名
对象名称.方法名

下面通过一个案例学习对象属性的访问和方法的访问。具体代码如下所示。

class Student02 {
    String name;       			// 声明姓名属性
    void read() {  
    	System.out.println("大家好,我是" + name);
    }
}
public class Example01 {
    public static void main(String[] args) {
        Student02 student1 = new Student02();		// 创建第一个Student对象
        Student02 student2 = new Student02(); 		// 创建第二个Student对象
        student1.name = "小明";					// 为student1对象的name属性赋值
        student1.read();						// 调用对象的方法
        student2.name = "李华";
        student2.read();
    }
}

student1 对象和 student2 对象在调用 read() 方法时,打印的 name 值不相同。这是因为 student1 对象和 student2 对象在系统内存中是两个完全独立的个体,它们分别拥有各自的 name 属性,对 student1 对象的 name 属性进行赋值并不会影响到 student2 对象 name 属性的值。

在这里插入图片描述

由图可知,程序分别实例化了两个 student 对象 student1 和 student2,student1 和 student2 分别指向各自的堆内存空间,互不干扰。

对象的引用传递

类属于引用数据类型,引用数据类型就是指内存空间可以同时被多个栈内存引用。下面通过一个案例详细讲解对象的引用传递,具体代码如下所示。

class Student02 {
    String name;	// 声明姓名属性
    int age;		// 声明年龄属性
    void read() {  
    	System.out.println("大家好,我是"+name+",年龄"+age);
    }
}
public class Example02 {
    public static void main(String[] args) {
        Student02 student1 = new Student02();	//创建student1对象并实例化
        Student02 student2 = null;			//创建student对象,但不对其进行实例化
        student2 = student1;		//sstudent1给student2分配空间使用权
        student1.name = "小明";		//为student1对象的name属性赋值
        student1.age = 20;
        student2.age = 50;
        student1.read();			//调用对象的方法
        student2.read();
    }
}

student1 对象和 student2 对象输出的内容是一致的,这是因为 student2 对象获得了 student1 对象的堆内存空间的使用权。第14行代码对 student1 对象的 age 属性赋值之后,第15行代码通过 student2 对象对 age 属性值进行了修改。实际上所谓的引用传递,就是将一个堆内存空间的使用权给多个栈内存空间使用,每个栈内存空间都可以修改堆内存空间的内容。

  • student1 对象和 student2 对象引用传递的内存分配如下图所示。

在这里插入图片描述
在这里插入图片描述

  • 第一步声明对象 student1 和 student2,并使用 new 创建 Student 对象赋值给 student1,使用 new 创建对象时会开辟一个堆内存空间,对象 student1 指向开辟的堆内存地址 0x001;
  • 第二步通过对象 student1 给对象 student2 分配内存空间使用权,对象 student2 指向堆内存地址 0x001;
  • 第三步由于对象 student1 指向堆内存地址 0x001,所以对象 student1 修改属性值时,就是修改堆内存中对象的值,堆内存中 name 的值修改为“小明”,age 的值修改为20;
  • 第四步与第三步类似,对象 student2 也指向堆内存地址 0x001,堆内存中 age 的值修改为50,最终结果对象 student1 的age属性值也是50。

注意:一个栈内存空间只能指向一个堆内存空间,如果想要再指向其他堆内存空间,就必须先断开已有的指向才能分配新的指向。
关键字:空指针,内存管理,垃圾回收机制。

访问控制权限

在 Java 中,针对类、成员方法和属性,Java 提供了4种访问控制权限,分别是 private、default、protected 和 public。下面通过一张图将这4种访问控制权限按级别由小到大依次列出,如下图所示。
在这里插入图片描述
(1)private:private属于私有访问权限,用于修饰类的属性和方法,也可以修饰内部类。类的成员一旦使用了 private 关键字修饰,则该成员只能在本类中进行访问。
(2)default:default 属于默认访问权限,如果一个类中的属性或方法没有任何的访问权限声明,则该属性或方法就是默认的访问权限,默认的访问权限可以被本包中的其他类访问,但是不能被其他包的类访问。
(3)protected:protected属于受保护的访问权限。如果一个类中的成员使用了protected 访问权限,则只能被本包及不同包的子类访问。
(4)public:public 属于公共访问权限。如果一个类中的成员使用了 public 访问权限,则该成员可以在所有类中被访问,不管是否在同一包中。

下面通过一张表总结上述访问控制权限,如下表所示。

在这里插入图片描述
下面通过一段代码演示4种访问控制权限修饰符的用法,示例代码如下。

public class Test {
    public int aa;			// aa可以被所有的类访问
    protected boolean bb;	// bb可以被所有子类以及本包的类访问
    void cc() {				// 默认访问权限,能在本包范围内问
        System.out.println("包访问权限");
    }
    // private权限的内部类,即这是私有的内部类,只能在本类中访问
    private class InnerClass {
    }
}

注意:外部类的访问权限只能是 public 或 default,所以 Test 类只能使用 public 修饰或者不写修饰符。局部变量是没有访问权限控制的,因为局部变量只在其所在的作用域内起作用,不可能被其他类访问到,如果在程序中这样编写代码,编译器会报错。

  • 错误示例代码如下所示。
public class Test {
    void cc() {					// 默认访问权限,能在本包范围内使用
    	public int aa;			// 错误,局部变量没有访问权限控制
    	protected boolean bb;	// 错误,局部变量没有访问权限控制
        	System.out.println("包访问权限");
    }
    // private权限的内部类,即这是私有的内部类,只能在本类使用
    private class InnerClass {
    }
}

小提示:Java 程序的文件名
如果一个 Java 源文件中定义的所有类都没有使用 public 修饰,那么这个 Java 源文件的文件名可以是一切合法的文件名;如果一个源文件中定义了一个 public 修饰的类,那么这个源文件的文件名必须与 public 修饰的类名相同,且一个源文件中只能有0个或1个 public 类。

封装性

为什么要封装

在 Java 面向对象的思想中,封装是指一种将类的实现细节包装、隐藏起来的方法。封装可以被认为是一个保护屏障,防止本类的代码和数据被外部类定义的代码随机访问(属性),通过访问修饰符(public),向外界提供可以访问的方法(我们平时定义的普通类当中,属性一般私有 private,方法一般公有 public。下面通过一个例子具体讲解什么是封装,具体代码如下所示。

class Student03{
    String name;	// 声明姓名属性
    int age;		// 声明年龄属性
    void read() {  
    	System.out.println("大家好,我是"+name+",年龄"+age);
    }
}
public class Example03 {
    public static void main(String[] args) {
        Student03 student = new Student03();	// 创建学生对象
        student.name = "张三";				// 为对象的name属性赋值
        student.age = -18;					// 为对象的age属性赋值
        student.read();						// 调用对象的方法
    }
}

从上述代码中看,第12行代码将 age(年龄)属性赋值为-18岁,这在程序中是不会有任何问题的,因为 int 的值可以取负数。但在现实中,-18明显是一个不合理的年龄值。为了避免这种错误的发生,在设计 Student 类时,应该对成员变量的访问作出一些限定(这个限定就是属性私有),不允许外界随意访问,这就需要实现类的封装。

如何实现封装

类的封装是指将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息(属性),而是通过该类提供的方法实现对内部信息(属性)的操作访问。封装的具体实现过程是,在定义一个类时,将类中的属性私有化,即使用 private 关键字修饰类的属性,私有属性只能在它所在的类中被访问。如果外界想要直接访问私有属性(对象名.属性名),是绝对不可能的,所以需要提供一些使用 public 修饰的公有方法,外界可以通过public 方法来访问 private 属性,其中包括用于获取属性值的getXxx()方法(也称为getter方法)和设置属性值的setXxx()方法(也称为setter方法)

修改之前的案例,使用 private 关键字修饰 name 属性和 age 属性以及其对应的 getter/setter 方法,演示如何实现类的封装,只展示新增代码。

public class Student {
    private String name;
    private int age;
    
    public String getName() {
    	return name;
    }
    public void setName(String name) {
    	this.name = name;
    }
    public int getAge() {
    	return age;
    }
    public void setAge(int age) {
        if(age < 0 || age > 120){
        	System.out.println("您输入的年龄有误!");
        } else {
        	this.age = age;
        }
	}
}

public 方法有两个作用:
(1)提供对外的接口,使外界能够访问 private 属性。
(2)在 public 方法中可以添加控制逻辑,限制对 private 属性的访问。

构造方法

前面我们学习了面向对象的重要思想,而封装是面向对象的核心思想,掌握封装对于学习 Java 面向对象的内容十分重要。从前面所学的知识可以发现,实例化一个对象后,如果要为这个对象中的属性赋值,则必须通过直接访问对象的属性(严禁,所以才将属性私有化)或调用 setter 方法才可以,如果需要在实例化对象时为这个对象的属性赋值,可以通过构造方法即构造器实现。构造方法是类的一个特殊成员方法,在类实例化对象时自动调用。本节将对定义构造方法、构造方法的重载进行详细地讲解。

  • 实例化 = 创建对象 = new 一个对象 = 实例化一个对象
  • 只有实例化时,才为对象分配内存(堆内存)

定义构造方法

构造方法是一个特殊的成员方法,在定义时,有以下几点需要注意。
(1)构造方法的名称必须与类名一致(即首字母大写)。
(2)构造方法名称前不能有任何返回值类型的声明。
(3)不能在构造方法中使用 return 返回一个值,但可以单独写 return 语句作为方法的结束。可以有 return 这个动作,但是不能有值。在构造器中添加 return 语句,符合语法规则,但不是必要的,甚至有点多余的,不建议添加 return。
下面通过一个案例演示构造方法的定义,具体代码如下所示。

class Student{
    public Student() {  
    	System.out.println("调用了构造方法");
    }
}
public class Example05 {
    public static void main(String[] args) {
        System.out.println("声明对象...");
        Student student = null;         //声明对象
        System.out.println("实例化对象...");
        student = new Student();     	//实例化对象
    }
}
  • 当调用关键字 new 实例化对象时,程序调用了 Student 类的无参构造方法。

一个类中除了定义无参的构造方法外,还可以定义有参的构造方法,通过有参的构造方法可以实现对属性的赋值。通过下面一个案例演示有参构造方法的定义与调用。

class Student{
    private String name;
    private int age;
    public Student(String n, int a) {
        name = n;
        age = a;
        System.out.println("调用了有参构造");
    }
    public void read(){
    	System.out.println("我是: " + name + ", 年龄: " + age);
    }
}
public class Example06 {
    public static void main(String[] args) {
        Student student = new Student("张三",18); // 实例化Student对象
        student.read();
    }
}

思考

  • 针对上面的案例 Example06,不传递参数,直接 new 一个对象Student06 student = new Student06();,行不行?为什么?
  • 答:不行。因为带参构造器覆盖了无参构造器。

Java 为每一个类提供了一个无参构造器(不会体现在代码中),在 new 对象时,Java 会调用这个无参构造器。
注意:在 Example05 中定义的无参构造器,与 Java 提供的无参构造器同名,所以默认的无参构造器被用户写的无参构造器覆盖了,当 new 对象时,不会再调用默认无参构造器了,而是会调用用户自己的无参构造器。
(1)如果用户自定义的类中没有无参构造器,则调用 Java 提供的默认无参构造器.
(2)如果用户自定义的类中有无参构造器,则调用用户自己的无参构造器。
(3)如果用户自定义的类中只有带参构造器,则要格外注意,new 对象时一定要携带参数:

  • new 对象时携带参数(要求参数个数与参数类型与构造器保持一致),则程序正常运行,此时会使用传递的参数实例化一个对象。
  • new 对象时不携带参数,则程序报错,因为用户自定义的带参构造器覆盖了 Java 提供的默认无参构造器,所以该类中没有无参构造器了,所以如果自定义了带参构造器,则建议同时添加一个无参构造器,这是一个非常好的习惯。

思考:如果自定义的带参构造器覆盖了 Java 提供的默认无参构造器,同时要求实例化对象时不传递参数,该如何修改代码?

  • 答:添加一个无参构造器。

构造方法的重载

与普通方法一样,构造方法也可以重载,在一个类中可以定义多个构造方法,但是需要每个构造方法的参数类型或参数个数不同。在创建对象时,可以通过调用不同的构造方法为不同的属性赋值。

下面通过一个案例学习构造方法的重载,具体代码如下所示。

class Student{
    private String name;
    private int age;
    public Student() {}
    public Student(String n) {
        name = n;
        System.out.println("调用了一个参数的构造方法");
    }
    public Student(String n, int a) {
        name = n;
        age = a;
        System.out.println("调用了两个参数的构造方法");
    }
    public void read(){
         System.out.println("我是:"+name+",年龄:"+age);
    }
}
public class Example07 {
    public static void main(String[] args) {
        Student student1 = new Student("张三");
        Student student2 = new Student("张三", 18);	// 实例化Student对象
        student1.read();
        student2.read();
    }
}

Java 会根据传递的参数类型和参数个数,来自动调用相应的构造器。

默认构造方法

在 Java 中的每个类都至少有一个构造方法,如果在一个类中没有定义构造方法,系统会自动为这个类创建一个默认的构造方法,这个默认的构造方法没有参数,方法体中没有任何代码,所以 Java 中默认的构造方法在程序运行时什么也不做。

下面程序中Student类的两种写法,效果是完全一样的。

  • 第一种写法:
class Student {
}
  • 第二种写法:
class Student {
   public Student(){
   }
}

对于第一种写法,类中虽然没有声明构造方法,但仍然可以用 new Student() 语句创建 Student 类的实例对象,在实例化对象时调用默认构造方法。

由于系统提供的默认构造方法往往不能满足需求,因此,通常需要程序员自己在类中定义构造方法,一旦为类定义了构造方法,系统就不再提供默认的构造方法了,具体代码如下所示。

class Student {
    int age;
    public Student(int n) {
    	age = n;
    }
}

上面的 Student 类中定义了一个有参构造方法,这时系统就不再提供默认的构造方法。

注意:构造方法通常使用 public 进行修饰。

this关键字

Java 开发中,当成员变量与局部变量发生重名问题时,需要使用到 this 关键字分辨成员变量与局部变量。Java 中的 this 关键字语法比较灵活,其作用主要有以下3个:
(1)使用 this 关键字调用本类中的属性。
(2)使用 this 关键字调用成员方法。
(3)使用 this 关键字调用构造方法。

使用this关键字调用本类中的属性

在之前的案例中,Student 类中的成员变量 age 表示年龄,而构造方法中表示年龄的参数是 a,这样的程序可读性很差。这时需要对一个类中表示年龄的变量进行统一命名,例如都声明为 age。但是这样做会导致成员变量和局部变量的命名冲突。下面通过一个案例进行验证。

class Student {
    private String name;
    private int age;
    // 定义构造方法
    public Student(String name, int age) {
        name = name;
        age = age;
    }
    public String read(){
        return "我是:"+name+",年龄:"+age;
    }
}
public class Example09 {
    public static void main(String[] args) {
        Student student = new Student("张三", 18);
        System.out.println(student.read());
    }
}

运行结果显示,student 对象姓名为 null,年龄为0,这表明构造方法中的赋值并没有成功,这是因为构造方法参数名称与对象成员变量名称相同,编译器无法确定哪个名称是当前对象的属性。
为了解决这个问题,Java 提供了关键字 this 指代当前对象,通过 this 可以访问当前对象的成员。修改上面的案例,使用 this 关键字指定当前对象属性,具体代码如下所示。

class Student {
    private String name;
    private int age;
    public Student(String name,int age) {// 定义构造方法
        this.name = name;
        this.age = age;
    }
    public String read(){
        return "我是:"+name+",年龄:"+age;
    }
}
public class Example10 {
    public static void main(String[] args) {
        Student student = new Student("张三", 18);
        System.out.println(student.read());
    }
}

修改后成功调用构造方法完成了 student 对象的初始化。这是因为在构造方法之中,使用 this 关键字明确标识出了类中的两个属性“this.name”“this.age”,在进行赋值操作时不会产生歧义。

使用 this 关键字调用成员方法

可以通过this关键字调用成员方法,具体示例代码如下

class Student {
    public void openMouth() {
        ...
    }
    public void read() {
        this.openMouth();
    }
}

上述代码中,在read()方法中使用 this 关键字调用了 openMouth() 方法。需要注意的是,此处的 this 关键字也可以省略不写

使用 this 关键字调用构造方法

构造方法是在实例化对象时被 Java 虚拟机自动调用,在程序中不能像调用其他成员方法一样调用构造方法,但可以在一个构造方法中使用“this(参数1,参数2…)”的形式调用其他的构造方法。
下面通过一个案例演示使用 this 关键字调用构造方法,具体代码如下所示。

class Student {
    private String name;
    private int age;
    public Student () {
        System.out.println("调用了无参的构造方法");
    }
    public Student (String name,int age) {
        this();                  // 调用无参的构造方法
        this.name = name;
        this.age = age;
    }
    public String read(){
        return "我是:"+name+",年龄:"+age;
    }
}

public class Example11 { 
    public static void main(String[] args) {
        Student student = new Student("张三",18);    // 实例化 Student对象
        System.out.println(student.read());
    }
}

使用 this 调用类的构造方法时,应注意以下三点。
(1)只能在构造方法中使用 this 调用其他的构造方法,不能在成员方法中通过 this 调用构造方法。
(2)在构造方法中,使用 this 调用其他构造方法的语句必须位于第一行,且只能出现一次。
(3)不能在一个类的两个构造方法中使用 this 互相调用。

class Student {
	public Student () {
        this("张三");	// 调用有参构造方法
		System.out.println("无参的构造方法被调用了。");
	}
	public Student (String name) {
		this();			// 调用无参构造方法
		System.out.println("有参的构造方法被调用了。");
	}
}

思考:this()必须放在构造方法的第一句,在成员方法中不能用this()调用构造方法的原因:

  • 在构造函数中,如果你不指定构造器之间的调用关系,那么编译器会给你加上super();目的是在初始化当前对象时,先保证了父类对象先初始化。所以,你指定了构造函数间的调用,那么this()必须在第一行,以保证在执行任何动作前,对象已经完成了初始化。构造函数只能被构造函数调用,因为对象只会初始化一次

  • 解答这个问题,首先要明确一下构造方法和普通成员方法的区别。类似this()和super()这样的方法被称为构造方法,顾名思义,他的作用就是在JVM堆中构建出一个指定类型的对象,如果你调用了两个这种形式的方法,岂不是代表着构建出了两个对象。同理,为了避免构建出两个对象这种问题的出现,Java在编译时对这种情况做了强校验,用户不能再同一个方法内调用多次this()或super(),同时为了避免对对象本身进行操作时,对象本身还未构建成功(也就找不到对应对象),所以对this()或super()的调用只能在构造方法中的第一行实现,防止异常。在普通的成员方法中,如果调用super()或者this(),你是想要重新创建一个对象吗?抱歉Java为了保证自身对象的合理性,不允许你做这样的操作。

总结

  • this()和super()为构造方法,作用是在JVM堆中构建出一个对象。因此避免多次创建对象,同一个方法内只能调用一次this()或super()。同时为了避免操作对象时对象还未构建成功,需要this()和super()的调用在第一行实现【以此来创建对象】,防止异常。

代码块

代码块,简单来讲,就是用{}括号括起来的一段代码,根据位置及声明关键字的不同,代码块可以分为4种:普通代码块、构造块、静态代码块、同步代码块。本节将针对普通代码块和构造块进行讲解。静态代码块将在下一节的 static 关键字中进行讲解,同步代码块将在多线程部分进行讲解。

普通代码块

普通代码块就是直接在方法或是语句中定义的代码块,具体示例如下。

public class Example12 { 
	public static void main(String[] args) {
        {
            int age = 18;
            System.out.println("这是普通代码块。age:"+age);
        } 
        int age = 30;
        System.out.println("age:"+age);
	}
}

在上述代码中,每一对“{}”括起来的代码都称为一个代码块。Example12 是一个大的代码块,在 Example12 代码块中包含了 main() 方法代码块,在 main() 方法中又定义了一个局部代码块,局部代码块对 main() 方法进行了“分隔”,起到了限定作用域的作用。
上述代码中的局部代码块中定义了变量 agemain() 方法代码块中也定义了变量 age,但由于两个变量处在不同的代码块,作用域不同,因此并不相互影响

构造块

构造代码块是直接在类中定义的代码块。下面通过一个案例演示构造代码块的使用,具体代码如下所示。

class Student{
    String name;    		//成员属性
    {
    	System.out.println("我是构造代码块");       //与构造方法同级
    }
    //构造方法
    public Student(){
    	System.out.println("我是Student类的构造方法");
    }
}
public class Example13  {
    public static void main(String[] args) {
        Student student1 = new Student();
        Student student2 = new Student();
    }
}

(1)在实例化 Student 类对象student1、student2时,构造块的执行顺序大于构造方法(这里和构造块写在前面还是后面没有关系)。
(2)每当实例化一个 Student 类对象,都会在执行构造方法之前执行构造块。

static关键字

在 Java 中,定义了一个 static 关键字,它用于修饰类的成员,如成员变量、成员方法以及代码块等,被 static 修饰的成员具备一些特殊属性。

静态属性

如果在 Java 程序中使用 static 修饰属性,则该属性称为静态属性(也称全局属性),静态属性可以使用类名直接访问(即不需要 new 对象,该属性也有内存空间),也可以通过对象名访问,但是一般情况,建议通过类名访问,因为通过类名访问,一眼就能看出这是 static 属性,通过对象名访问,与普通属性的访问方式没有区别,容易混淆。访问格式如下。

类名.属性名

学习静态属性之前,先来看一个案例,,具体代码如下所示。

class Student {
    String name;	// 定义name属性
    int age;		// 定义age属性
    String school = "A大学";	// 定义school属性
    public Student(String name,int age){
        this.name = name;
        this.age = age;
    }
    public void info(){
        System.out.println("姓名:" + this.name+",年龄:" +this. age+",学校:" + school);  
    }
}
public class Example14 {
    public static void main(String[] args) {
    Student student1 = new Student("张三",18);    // 创建学生对象
    Student student2 = new Student("李四",19);
    Student student3 = new Student("王五",20);
    student1.info();
    student2.info();
    student3.info();
    //修改stu1对象的school的值
    student1.school = "B大学";
    System.out.println("修改student1学生对象的学生信息为B大学后");
    student1.info();
    student2.info();
    student3.info();
    }
}

张三的学校信息由 A 大学修改为了 B 大学,而李四和王五的大学信息没有变化,表明非静态属性(普通属性)是对象所有,改变当前对象的属性值,不影响其他对象的属性值。
下面,考虑一种情况:假设 A 大学改名成了 B 大学,而此时 Student 类已经产生了10万个学生对象,那么意味着,如果要修改这些学生对象的学校信息,则要把这10万个对象中的学校属性全部修改,共修改10万遍,这样肯定是非常麻烦的。
为了解决上述问题,可以使用 static 关键字修饰 school 属性,将其变为公共属性。这样,school 属性只会分配一块内存空间,被 Student 类的所有对象共享(1.所有的对象都可以访问;2.共享属性只有唯一的一个值,即只有一个块内存空间),只要某个对象进行了一次修改,全部学生对象的 school 属性值都会发生变化。

  • 修改案例一,使用 static 关键字修饰 school 属性,具体代码如下所示。
class Student {
    String name;	// 声明name属性
    int age;		// 声明age属性
    static String school = "A大学";	// 定义school属性
    public Student(String name,int age){
        this.name = name;
        this.age = age;
    }
    public void info(){
        System.out.println("姓名:" + this.name+",年龄:" +this. age+",学校:" + school);  
    }
}

public class Example15 {
    public static void main(String[] args) {
        Student stu1 = new Student("张三",18);	// 创建学生对象
        Student stu2 = new Student("李四",19);
        Student stu3 = new Student("王五",20);
        stu1.info();
        stu2.info();
        stu3.info();
        stu1.school = "B大学";
        stu1.info();
        stu2.info();
        stu3.info();
    }
}

在这里插入图片描述
在这里插入图片描述

  • Java 要为 static 属性提供内存空间,与是否 new 对象无关,即 static 属性是属于类的,不属于某一个对象,是所有对象共享的。

小提示:static 不能修饰局部变量

static 关键字只能修饰成员变量,不能修饰局部变量,否则编译器会报错。例如,下面的代码是非法的。

public class Student {
    public void study() {
        // 这行代码是非法的,编译器会报错
        static int num = 10;	
    }
}

思考:static 为什么不能修饰局部变量?

  • 理解一:假如,用static修饰一个变量,jvm就理解为你需要延长该字段的生命周期,既然是延长,就直接申明为全局变量就行了三,所以报错修饰局部变量,类在,static在;
  • 理解二:假如可以用static修饰局部变量,就表示此变量在所在类被jvm加载的时候就初始化加载了。但是方法和对象相关的,而方法只有在被调用的时候才有意义,你在加载类的时候还没有调用方法,就把方法里的局部变量加载了,这就显的没有必要意义了。所以没必要修饰局部变量。

此时可能就会有异议了:为什么static可以修饰一个方法呢?

  • 可以这样理解:一个方法被static修饰,即表示和所在类被jvm同时加载。类被加载完成的同时,这个方法就已经准备好了,所以以后我们在调用被static修饰的方法的时候,就直接用加载好的类名直接调用已经加载好的方法就可以了。当然你也可以申明一个该类的对象来调用此方法。

静态方法

如果想要使用类中的成员方法,就需要先将这个类实例化(new)。而在实际开发时,开发人员有时希望在不创建对象的情况下,通过类名就可以直接调用某个方法,这时就需要使用静态方法,要实现静态方法只需要在成员方法前加上 static 关键字。
同静态变量一样,静态方法也可以通过类名和对象访问,具体如下所示。

类名.方法
实例对象名.方法

下面通过一个案例学习静态方法的使用,具体代码如下所示。

class Student16 {
    private String name;	// 声明name属性
    private int age;		// 声明age属性
    private static String school = "A大学";	// 定义school属性
    public static String getSchool() {                 
   	 return school;
    }
    public static void setSchool(String s) {
    	school = s;
    }

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

    public void info(){
        System.out.println("姓名:" + this.name+",年龄:" +this. age+",学校:" + school);
    }
}
class Example16 {
    public static void main(String[] args) {
        Student16 student1 = new Student16("张三",18);      // 创建学生对象stu1
        Student16 student2 = new Student16("李四",19);      // 创建学生对象stu2
        Student16 student3 = new Student16("王五",20);      // 创建学生对象stu3
        System.out.println("----修改前----");
        student1.info();
        student2.info();
        student3.info();
        System.out.println("----修改后----");
        Student.setSchool("B大学");	 //为静态属性school重新赋值
        student1.info();
        student2.info();
        student3.info();
    }
}

注意:静态方法只能访问静态成员,因为非静态成员需要先创建对象才能访问,即随着对象的创建,非静态成员才会分配内存。而静态方法在被调用时可以不创建任何对象。
思考题:在 static 方法中,为什么不能访问普通属性?
关于静态方法与静态属性的访问问题

静态代码块

在 Java 类中,用 static 关键字修饰的代码块称为静态代码块。当类被加载时,静态代码块就会执行,由于类只加载一次,所以静态代码块只执行一次。在程序中,通常使用静态代码块对类的成员变量进行初始化。
下面通过一个案例学习静态代码块的使用,具体代码如下所示。

class Student{
    String name;    		//成员属性
    {
    	System.out.println("我是构造代码块");
    }
    static {
    	System.out.println("我是静态代码块");
    }
    public Student(){      		//构造方法
    	System.out.println("我是Student类的构造方法");
    }
}

class Example17{
    public static void main(String[] args) {
        Student stu1 = new Student();
        Student stu2 = new Student();
        Student stu3 = new Student();
    }
}

在这里插入图片描述

继承

继承的概念

在程序中,继承描述的是事物之间的所属关系,通过继承可以使多种事物之间形成一种关系体系。

在 Java 中,类的继承是指在一个现有类的基础上构建一个新的类,构建出来的新类被称作子类,现有类被称作父类。子类会自动继承父类的属性和方法,使得子类具有父类的特征和行为。

  • 思考题:子类中无法直接访问父类的 private 属性,为什么还要说子类继承了父类的属性?
  • 回答:虽然子类无法直接访问父类的 private 属性,但是父类中的 private 属性一定会有相应的 setter / getter 方法,而且这些方法一定是 public,子类就可以通过这些 public 方法来访问相应的 private 属性。

子类继承了父类的 private 属性,但是无法直接访问,必须通过继承的父类中相应的 setter / getter 方法才可以访问 private 属性
在 Java 程序中,如果想声明一个类继承另一个类,需要使用 extends 关键字,其语法格式如下所示。

class 父类{
	……
}
class 子类 extends 父类{
	…… 
}

下面通过一个案例学习子类是如何继承父类的,具体代码如下所示。

// 定义Animal类
class Animal01 {
    private String name;	// 声明name属性
    private int age;		// 声明age属性
    public final String COLOR = "黑色";

    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;
    }
}

// 定义Dog类继承Animal类
class Dog01 extends Animal01 {
    //此处不写任何代码
}

// 定义测试类
public class Example01 {
    public static void main(String[] args) {
        Dog01 dog = new Dog01();	// 创建一个Dog类的对象
        dog.setName("牧羊犬");	// 此时调用的是父类Animal中的setter方法
        dog.setAge(3);			// 此时调用的是父类Animal中的setter方法
        System.out.println("名称:"+dog.getName()+",年龄:"+dog.getAge()
                +",颜色:"+dog.COLOR);
    }
}

子类除了可以继承父类的属性和方法,也可以定义自己的属性和方法。修改上述案例,在子类 Dog 中增加属性 color 和相应的 getter 和 setter 方法,具体代码如下所示。

// 定义Animal类
class Animal02 {
	private String name;	// 声明name属性
    private int age;		// 声明age属性
    public final String COLOR = "白色";
    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;
    }
}
// 定义Dog类继承Animal类
class Dog02 extends Animal02 {
    private String color;        // 声明color属性
    public String getColor() {
    	return color;
    }
    public void setColor(String color) {
    	this.color = color;
    }
}

// 定义测试类
public class Example02 {
    public static void main(String[] args) {
        Dog02 dog = new Dog02();     // 创建并实例化dog对象
        dog.setName("牧羊犬");    // 此时访问的是父类Animal中的setter方法
        dog.setAge(3);            // 此时访问的是父类Animal中的setter方法
        dog.setColor("黑色");    // 此时访问的是Dog类中的setter方法
        System.out.println("名称:"+dog.getName()+",年龄:"+dog.getAge()+", "+
        "颜色:"+dog.getColor());
    }
}
  • 能否通过 animal 对象访问 Dog 类中的 color 属性? - 不能

思考题:为什么会存在子类?子类存在的意义是什么?

  • :父类原有功能已经不能满足需求,需要进行扩充,子类在父类基础上,对功能做了扩充。

思考题:为什么不在父类上直接进行扩充?为什么要使用子类来扩充?

  • 注意:子类虽然可以通过继承访问父类的成员属性和成员方法,但不是所有的父类属性和方法都可以被子类访问。子类只能访问父类中 public 和 protected 修饰的属性和方法,父类中被默认修饰符 default 和 private 修饰的属性和方法不能被子类访问。

在一个项目中,会有很多的实体类,此时,可以把实体类中相同的属性提取出来,形成一个父类,其他想要使用这些属性的类,来继承该父类,减少代码量,想要添加新的公共属性时,直接在父类中添加即可,减少出错的概率。

在类的继承中,需要注意一些问题,具体如下
(1)在 Java 中,类只支持单继承,不允许多继承。也就是说一个类只能有一个直接父类。例如下面这种情况是不合法的。

class A{}
class B{}
class C extends A,B{}	// C 类不可以同时继承A类和B类

(2)多个类可以继承同一个父类,例如下面这种情况是允许的。

class A{}
class B extends A{}   //类B继承类A
class C extends A{}   //类C继承类A

(3)在 Java 中,多层继承也是可以的,即一个类的父类可以再继承另外的父类。例如,C 类继承自 B 类,而 B 类又可以继承自类 A,这时,C 类也可称作 A 类的子类。例如下面这种情况是允许的。

class A{}
class B extends A{}   // 类B继承类A,类B是类A的子类
class C extends B{}   // 类C继承类B,类C是类B的子类,同时也是类A的子类

(4)在 Java 中,子类和父类是一种相对概念,一个类可以是某个类的父类,也可以是另一个类的子类。例如,在第(3)种情况中,B 类是 A 类的子类,同时又是 C 类的父类。

方法的重写

在继承关系中,子类会自动继承父类中定义的方法,但有时在子类中需要对继承的方法进行一些修改,即对父类的方法进行重写。在子类中重写的方法需要和父类被重写的方法具有相同的方法名、参数列表以及返回值类型(即只有方法体不一样,其他都一样)
下面通过一个案例讲解方法的重写,具体代码如下所示。

// 定义Animal类
class Animal03 {		
    //定义动物叫的方法		
    void shout() {			  
    	System.out.println("动物发出叫声");
    }
}
// 定义Dog类继承Animal类
class Dog03 extends Animal03 {    
    //重写父类Animal中的shout()方法
    void shout() {			 
    	System.out.println("汪汪汪……");  
    }
}

// 定义测试类
public class Example03 {	
    public static void main(String[] args) {
        Dog03 dog = new Dog03(); // 创建Dog类的实例对象
        dog.shout();      // 调用Dog类重写的shout()方法
    }
}

脚下留心:子类重写父类方法时的访问权限
子类重写父类方法时,不能使用比父类中被重写的方法更严格的访问权限。例如,父类中的方法是 public 权限,子类的方法就不能是 private 权限。如果子类在重写父类方法时定义的权限缩小,则在编译时将出现错误。
下面我们对案例一进行修改,修改后的代码如下所示。

// 定义Animal类
class Animal04 {		
    //定义动物叫的方法		
    public void shout() {			  
    	System.out.println("动物发出叫声");
    }
}
// 定义Dog类继承Animal类
class Dog04 extends Animal04 {    
    //重写父类Animal中的shout()方法
    private void shout() {			 
    	System.out.println("汪汪汪……");  
    }
}

// 定义测试类
public class Example04 {	
    public static void main(String[] args) {
        Dog04 dog = new Dog04(); // 创建Dog类的实例对象
        dog.shout();           // 调用Dog类重写的shout()方法
    }
}

上述代码运行时会报错,编译文件报错“com.itheima.Dog中的shout()无法覆盖com.itheima.Anima的shout()”,这是因为子类重写父类方法时,不能使用比父类中被重写的方法更严格的访问权限

super关键字

当子类重写父类的方法后,子类对象将无法访问父类中被子类重写过的方法。为了解决这个问题,Java 提供了 super 关键字,使用 super 关键字可以在子类中访问父类的非私有方法、非私有属性以及构造方法。下面详细讲解 super 关键字的具体用法。
(1)使用 super 关键字访问或调用父类的非私属性或非私有方法,具体格式如下。

super.属性
super.方法(参数1,参数2)

下面通过一个案例学习使用 super 关键字访问父类的成员变量和成员方法。

// 定义 Animal 类
class Animal05 {
    String name = "牧羊犬";
    // 定义动物叫的方法
    void shout() {
        System.out.println("动物发出叫声");
    }
}
// 定义Dog类继承Animal类
class Dog05 extends Animal05 {    
    // 重写父类Animal中的shout()方法,扩大了访问权限
    public void shout() {			 
        super.shout();	// 调用父类中的shout()方法
        System.out.println("汪汪汪……");  
    }
    public void printName(){
       	System.out.println("名字:" + super.name);	// 访问父类中的name属性
    }
}

// 定义测试类
public class Example05 {	
    public static void main(String[] args) {
        Dog05 dog = new Dog05();	// 创建Dog类的对象
        dog.shout();			// 调用Dog类重写的shout()方法
        dog.printName();		// 调用Dog类中的printName()方法
    }
}

(2)使用 super 关键字调用父类中指定的构造方法,具体格式如下。

super(参数1,参数2)

下面通过一个案例学习如何使用 super 关键字调用父类的构造方法,具体代码如下所示。

// 定义Animal类
class Animal06 {
    private String name;
    private int age;
    public Animal(String name, int age) {	// Animal类有参构造方法
        this.name = name;
        this.age = age;
    }
    
    public String info() {
    	return "名称:"+this.getName()+",年龄:"+this.getAge();
    }
}
// 定义Dog类继承Animal类
class Dog06 extends Animal06 {
    private String color;
    public Dog(String name, int age, String color) {
        super(name, age);    //通过super关键字调用Animal类有两个参数的构造方法
        this.setColor(color);
    }
    //省略属性color的getter/setter方法
    // 重写父类的info()方法
    public String info() {
    	return super.info()+",颜色:"+this.getColor();  // 扩充父类中的方法
    }
}
// 定义测试类
public class Example06 {
    public static void main(String[] args) {
        Dog06 dog = new Dog06("牧羊犬",3,"黑色");             // 创建Dog类的对象
        System.out.println(dog.info());
    }
}

由运行结果可知,控制台打印了“名称:牧羊犬,年龄:3,颜色:黑色”,说明子类 Dog 使用 super() 成功调用了父类中有两个参数的构造方法,并传递了参数 name 和 age 的值,其中,name 的值为牧羊犬,age 的值为3。
注意:通过 super() 调用父类构造方法的代码必须位于子类构造方法的第一行,并且只能出现一次。

super 与 this 的区别

super 与 this 关键字的作用非常相似,都可以调用构造方法、方法和属性,但是两者之间还是有区别的,super与this的区别如下表所示。
在这里插入图片描述
需要注意的是,this 和 super 不可以同时出现,因为使用 this 和 super 调用构造方法的代码都要求必须放在构造方法的首行。
一定要区分开“重写”和“重载”

final关键字

在默认情况下,所有的成员变量和成员方法都可以被子类重写。如果父类的成员不希望被子类重写,可以在声明父类的成员时使用 final 关关键字修饰。final 有“最终”、“不可更改”的含义。在 Java 中,可以使用 final 关键字修饰类、属性、方法。在使用 final 关键字时需 要注意以下几点:
(1)使用 final 关键字修饰的类不能有子类。
(2)使用 final 关键字修饰的方法不能被子类重写
(3)使用 final 关键字修饰的变量是常量,常量不可修改。
本节针对 final 关键字的用法逐一进行讲解。

final 关键字修饰类

Java 中使用 final 关键字修饰的类不可以被继承,也就是不能派生子类。下面通过一个案例进行验证,具体代码如下所示。

// 使用final关键字修饰Animal类
final class Animal07 {
}
// Dog类继承Animal类
class Dog07 extends Animal07 {
}
// 定义测试类
public class Example07 {
    public static void main(String[] args) {
    	Dog07 dog = new Dog07(); // 创建Dog类的对象
    }
}

编译器报“无法从最终 Animal 进行继承”错误,说明 Dog 类不能继承使用 final 修饰的 Animal 类。由此可见,被 final 关键字修饰的类不能被其他类继承。

继承 = 派生

final 关键字修饰方法

当一个类的方法被 final 关键字修饰后,该类的子类将不能重写该方法。下面通过一个案例进行验证,具体代码如下所示。

// 定义Animal类
class Animal08 {
    // 使用final关键字修饰shout()方法
    public final void shout() {}
}
// 定义Dog类继承Animal类
class Dog08 extends Animal08 {
    // 重写Animal类的shout()方法
    public void shout() {}
}
// 定义测试类 
public class Example08 {
    public static void main(String[] args) {
    	Dog08 dog = new Dog08(); // 创建Dog类的对象
    }
}

使用 final 关键字修饰父类 Animal 中的 shout() 方法,在子类 Dog 类中重写 shout() 方法时,编译报“Dog中的 shout() 无法覆盖 Animal 中的 shout() 被覆盖的方法为 final”错误。这是因为 Animal 类的 shout() 方法被 final 关键字修饰,而子类不能对 final 关键字修饰的方法进行重写。

final 关键字修饰变量

Java 中被 final 修饰的变量称为常量,常量只能在声明时被赋值一次,在后面的程序中,常量的值不能被改变。如果再次对 final 修饰的常量赋值,则程序会在编译时报错。下面通过一个案例进行验证,具体代码如下所示。

public class Example09 {
    public static void main(String[] args) {
        final int AGE = 18;		// 使用final关键字修饰的变量AGE第一次可以被赋值
        AGE = 20;				// 再次被赋值会报错
    }
}

程序编译时报错“无法为最终变量 AGE 分配值”,这是因为使用 final 定义的常量本身不可被修改。
注意:在使用 final 声明常量时,常量的名称要求全部使用字母大写
如果一个程序中的常量使用 public static final 声明,则此常量将成为全局常量,如下面代码所示。
public static final String NAME = "哈士奇";

抽象类和接口

抽象类

定义一个类时,常常需要定义一些成员方法用于描述类的行为特征,但有时这些方法的实现方式是无法确定的。例如,前面定义的 Animal 类中的 shout() 方法用于描述动物的叫声,但是不同的动物,叫声也不相同,因此在 shout() 方法中无法准确描述动物的叫声。
针对上面描述的情况,Java 提供了抽象方法来满足这种需求。抽象方法是使用 abstract 关键字修饰的成员方法,抽象方法在定义时不需要实现方法体(不能有方法体,即不能有花括号)。抽象方法的语法格式如下。

abstract 返回值类型 方法名称(参数列表); 

当一个类包含了抽象方法,该类就是抽象类。抽象类和抽象方法一样,必须使用 abstract 关键字进行修饰。抽象类的语法格式如下。

abstract class 抽象类名称{
    属性;
    // 普通方法
    访问权限 返回值类型 方法名称(参数){
        return [返回值];
    }
    // 抽象方法,无方法体
    访问权限 abstract 返回值类型 抽象方法名称(参数);
}

从上面抽象类的语法格式中可以发现,抽象类的定义比普通类多了一个或多个抽象方法,其他地方与普通类的组成基本相同。抽象类的定义规则如下。
(1)包含抽象方法(无论是自定义的,还是继承的)的类必须是抽象类。
(2)声明抽象类和抽象方法时都要使用 abstract 关键字修饰。
(3)抽象方法只需声明而不需要实现(无法实例化一个抽象类)。
(4)如果非抽象类继承了抽象类,那么该类必须实现抽象类中的全部抽象方法。
注意:抽象类的作用就是等着被继承,由子类来实现抽象方法。子类可以只实现父类中的部分抽象方法,但此时要求子类必须也必须声明为抽象类。

下面通过一个案例学习抽象类的使用,具体代码如下所示。

// 定义抽象类Animal
abstract class Animal10 { 
    // 定义抽象方法shout()
    abstract void shout(); 
}
    // 定义Dog类继承抽象类Animal
class Dog10 extends Animal10 {
    // 重写抽象方法shout()
    void shout() {
    	System.out.println("汪汪...");
	}
}

// 定义测试类
public class Example10 {
    public static void main(String[] args) {
        Dog10 dog = new Dog10();	// 创建Dog类的对象
        dog.shout();			// 通过dog对象调用shout()方法
    }
}

控制台打印了“汪汪…”,说明 dog 对象调用了 Dog 类中实现的父类 Animal 的抽象方法 shout()

接口

接口是一种用来定义程序的协议,它用于描述类或结构的一组相关行为。接口是由抽象类衍生出来的一个概念,并由此产生了一种编程方式,可以称这种编程方式为面向接口编程。面向接口编程就是将程序的业务逻辑进行分离,以接口的形式去对接不同的业务模块。接口中不实现任何业务逻辑,业务逻辑由接口的实现类(子类)来完成(接口用来定义标准,如方法名、返回值类型以及请求参数,在思考接口时,尽量使接口具有普适性)。当业务需求变更时,只需要修改实现类中的业务逻辑,而不需要修改接口中的内容,以减少需求变更对系统产生的影响。

  • 下面通过现实生活中的的例子来类比面向接口编程。例如,鼠标、U盘等外部设备通过 USB 来连接计算机,即插即用,非常灵活。如果需要更换与计算机进行连接的外部设备,只需要拔掉当前 USB 上的设备,把新的设备插入即可,这就是面向接口编程的思想。

在 Java 中,使用接口的目的是为了克服单继承的限制,因为一个类只能继承一个父类,而一个类可以同时实现多个父接口。在 JDK8 之前,接口是由全局常量和抽象方法组成的。JDK8 对接口进行了重新定义,接口中除了抽象方法外,还可以定义默认方法和静态方法,默认方法使用 default 关键字修饰,静态方法使用 static 关键字修饰,且这两种方法都允许有方法体。
接口使用 interface 关键字声明,语法格式如下:

[public] interface 接口名 [implements 接口1, 接口2, ...] {
	[public] [static] [final] 数据类型 常量名 = 常量;
	[public] [abstract] 返回值的数据类型 方法名(参数列表);
	[public] static 返回值的数据类型 方法名(参数列表){}
	[public] default 返回值的数据类型 方法名(参数列表){}
}

上述语法格式中的“implements 接口1, 接口2, ...”表示一个接口可以有多个父接口,父接口之间使用逗号分隔。接口中的变量默认使用 public static final 进行修饰,即全局常量。接口中定义的抽象方法默认使用 public abstract 进行修饰,所以,接口中的方法没有方法体。

  • 注意:在很多的 Java 程序中,经常看到编写接口中的方法时省略了 public ,有很多读者认为它的访问权限是 default,这实际上是错误的。不管写不写访问权限,接口中方法的访问权限永远是 public。

思考题:为什么接口中的方法的访问权限永远都是 public?为什么要这样设计?
java中接口的方法为什么必须是public

接口本身不能直接实例化,接口中的抽象方法和默认方法只能通过接口实现类的实例对象进行调用。实现类通过 implements 关键字实现接口,并且实现类必须重写接口中所有的抽象方法。需要注意的是,一个类可以同时实现多个接口,实现多个接口时,多个接口名需要使用英文逗号(,)分隔。

定义接口的实现类,语法格式如下。

修饰符 class 类名 implements 接口1, 接口2,...{
    ...
}

下面通过一个案例学习接口的使用,代码如下所示。

// 定义接口 Animal 
interface Animal11 {
	int ID = 1;				// 定义全局常量,编号 
	String NAME ="牧羊犬";	// 定义全局常量,名称 
	void shout();			// 定义抽象方法 shout() 
	public void info();		// 定义抽象方法 info() 
	static int getID(){		// 定义静态方法 getID(),用于返回 ID 值 
		return Animal11.ID;
    }
}
// 定义接口 Action
interface Action11{
    public void eat();		// 定义抽象方法 eat() 
}

// 定义 Dog 类实现 Animal 接口和 Action 接口 
class Dog11 implements Animal11, Action11 {
    //重写Action接口中的抽象方法 eat() 
    public void eat() {
        System.out.println("喜欢吃骨头");
    }
    
    //重写Animal接口中的抽象方法 shout() 
    public void shout() {
        System.out.println("汪汪……");
    }
    
    //重写Animal接口中的抽象方法info() 
    public void info() {
        System.out.println("名称:"+NAME);
    }
}
//定义测试类
class Example11{
    public static void main(String[] args){
        System.out.println("编号"+Animal.getID());
        Dog dog = new Dog(); //创建Dog类的实例对象 
        dog.info();		//调用Dog类中重写的info()方法 
        dog.shout();	//调用Dog类中重写的shout()方法 
        dog.eat();		//调用Dog类中重写的eat)方法 
    }
}
  1. 定义一个 Animal 接口,在 Animal 接口中定义了全局常量 ID 和全局常量 NAME、抽象方法 shout()、info() 和静态方法 getID()。
  2. 定义一个 Action 接口,在 Action 接口中定义了一个抽象方法 eat(),用于输出信息“喜欢吃骨头”。
  3. 定义一个 Dog 类,Dog 类通过 implements 关键字实现了 Animal 接口和 Action 接口,并重写了这两个接口中的抽象方法。
  4. 使用 Animal 接口名直接访问了 Animal 接口中的静态方法getID(),输出编号信息。
  5. 创建 Dog 类的对象 dog,并通过 dog 对象调用了本类实现的 Animal 接口和 Action 接口中的 info() 方法、shout() 方法,以及本类新增的 eat() 方法。

注意:接口的实现类,必须实现接口中的所有抽象方法,否则程序编译报错。
如果只实现了部分抽象方法,则该子类必须为抽象类(没有实际意义)

修饰符 class 类名 extends 父类名 implements 接口1, 接口2, ... {
    ...
}

下面演示一个类既可以实现接口又可以继承抽象类的情况。

//定义接口Animal
interface Animal12 {
    public String NAME = "牧羊犬";	//定全局常量,名称
    public void shout();			//定义抽象方法shout()
    public void info();				//定义抽象方法info() 
}

// 定义抽象类 Action
abstract class Action12 {
	public abstract void eat();		//定义抽象方法 eat()
}
//定义Dog类继承Action抽象类,并实现Animal接口 
class Dog12 extends Action12 implements Animal12 {
    //重写Action抽象类中的抽象方法eat() 
    public void eat() {
    	System.out.println("喜欢吃骨头");
    }

    //重写Animal接口中的抽象方法 shout() 
    public void shout() {
    	System.out.println("汪汪……");
    }

    //重写Animal接口中的抽象方法 info() 
    public void info() {
    	System.out.println("名称:"+NAME);
    }
}

// 定义测试类
public class Example12 {
    public static void main(String[] args){
        Dog12 dog=new Dog12();	// 创建 Dog 类的实例对象 
        dog.info();			// 调用 Dog 类中重写的 info() 方法 
        dog.shout();		// 调用 Dog 类中重写的 shout() 方法 
        dog.eat();			// 调用 Dog 类中重写的 eat() 方法
    }
}
  1. 定义一个Animal接口,Animal接口中声明了全局常量NAME(名称)、抽象方法shout()和抽象方法info()。
  2. 定义一个抽象类Action,抽象类Action中定义了一个抽象方法eat()。
  3. 定义一个Dog类,Dog类通过extends关键字继承了Action抽象类,同时通过implements实现了Animal接口。Dog类重写了Animal接口和Action抽象类中的所有抽象方法,包括shout()方法、info()方法和eat()方法。
  4. 创建一个Dog类对象dog,通过对象dog分别调用info()、shout()和eat()方法。

在 Java 中,接口是不允许继承抽象类的,但是允许接口继承接口,并且一个接口可以同时继承多个接口。下面通过一个案例讲解接口的继承。

// 定义接口 Animal 
interface Animal13 {
    public string NAME = "牧羊犬";
    public void info();		// 定义抽象方法 info()
}
// 定义 Color 接口
interface Color13 {
	public void black();	// 定义抽象方法 black() 
}

//定义Action接口,它同时继承Animal接口和Color接口 
interface Action13 extends Animal13, Color13 {
	public void shout();	// 定义抽象方法 shout()
}

//定义Dog类实现Action接口 
class Dog13 implements Action13 {
	//重写Animal接口中的抽象方法info() 
    public void info(){
		System.out.println("名称:"+NAME);
    }

    //重写Color接口中的抽象方法black() 
    public void black(){
    	System.out.printin("黑色");
    }
    //重写Action接口中的抽象方法 shout() 
    public void shout() {
        System.out.println("汪汪……");
    }
}

//定义测试类
class Example13{
    public static void main(String[] args){
        Dog13 dog=new Dog13();	//创建Dog类的dog对象 
        dog.info();			//调用Dog类中重写的info()方法 
        dog.shout();		//调用Dog类中重写的shout()方法 
        dog.black();		//调用Dog类中重写的black()方法
    }
}
  1. 定义一个Animal接口,Animal接口中声明了全局常量NAME(名称)、抽象方法info()。
  2. 定义一个Color接口,Color接口中定义了一个抽象方法black()。
  3. 定义一个接口Action并继承接口Animal和接口Color,这样接口Action中就同时拥有Animal接口中的info()方法、NAME属性和Color接口中的black()方法以及本类中的shout()方法。
  4. 定义一个Dog类并实现了Action接口,这样Dog类就必须同时重写Animal接口、中的抽象方法info()、Color接口中的抽象方法black()和Action接口中的抽象方法shout()。
  5. 创建一个Dog类的对象dog,通过对象dog调用Dog类中定义的shout()方法以及Dog类中实现自Action接口的info()方法和eat()方法。

多态

多态概述

多态是面向对象思想中的一个非常重要的概念,在 Java 中,多态是指不同类的对象在调用同一个方法时表现出的多种不同行为。例如,要实现一个动物叫声的方法,由于每种动物的叫声是不同的,因此可以在方法中接收一个动物类型的参数,当传入猫类对象时就发出猫类的叫声,传入犬类对象时就发出犬类的叫声。在同一个方法中,这种由于参数类型不同而导致执行效果不同的现象就是多态
Java 中多态主要有以下两种形式。
(1)方法的重载。
(2)对象的多态(方法的重写)。

下面以对象的多态为例,通过一个案例演示 Java 程序中的多态,具体代码下所示。

// 定义抽象类Animal
abstract class Animal {  
	abstract void shout();	// 定义抽象shout()方法
}
// 定义Cat类继承Animal抽象类
class Cat extends Animal {
    // 实现shout()方法
    public void shout() {
    	System.out.println("喵喵……");
    }
}
// 定义Dog类继承Animal抽象类
class Dog extends Animal {
    // 实现shout()方法
    public void shout() {
    	System.out.println("汪汪……");
    }
} 
// 定义测试类
public class Example14 {
    public static void main(String[] args) {
        Animal an1 = new Cat();	// 创建Cat对象,使用Animal类型的变量an1引用
        Animal an2 = new Dog(); // 创建Dog对象,使用Animal类型的变量an2引用
        an1.shout();        
        an2.shout();        
    }
}

对象类型的转换

对象类型转换主要分为以下两种情况:
(1)向上转型:子类对象 → 父类对象。
(2)向下转型:父类对象 → 子类对象。

对象向上转型

对象向上转型,父类对象可以调用子类重写父类的方法,但无法调用子类特有的方法,这样当需要新添功能时,只需要新增一个子类,在子类中对父类的功能进行扩展,而不用更改父类的代码,保证了程序的安全性。对于向上转型,程序会自动完成,对象向上转型格式如下所示。

父类类型 父类对象 = 子类实例;

下面通过一个案例介绍如何进行对象的向上转型操作,具体代码下所示。

// 定义Anmal类
class Animal {
    public void shout(){
    	System.out.println("喵喵……"); 
    }     
}
// 定义Dog类
class Dog extends Animal {
    // 重写shout()方法
    public void shout() {
    	System.out.println("汪汪……"); 
    }
    public void eat() {
    	System.out.println("吃骨头……"); 
    }
}

// 定义测试类
public class Example15 {
    public static void main(String[] args) {
        Dog dog = new Dog();	// 创建Dog对象
        Animal an = dog;		// 向上转型
        an.shout();
    }
}

虽然程序中使用父类对象 an 调用了 shout() 方法,但实际上调用的是被子类重写过的 shout() 方法。也就是说,如果对象发生了向上转型后,调用的方法一定是被子类重写过的方法。
注意:父类 Animal 的对象 an 是无法调用 Dog 类中的 eat() 方法的,因为eat()方法只在子类中定义,而没有在父类中定义。

对象向下转型

除了向上转型,对象还可以向下转型。向下转型一般是为了重新获得因为向上转型而丢失的子类特性。对象在进行的向下转型前,必须先进行向上转型,否则将出现对象转换异常。
向下转型时,必须指明要转型的子类类型。对象向下转型格式如下所示。

父类类型 父类对象 = 子类实例;
子类类型 子类对象 = (子类)父类对象;

下面通过一个案例介绍对象进行向下转型,具体代码下所示。

// 定义 Animal 类
class Animal16 {
    public void shout(){
    	System.out.println("喵喵……"); 
    }     
}

// 定义Dog类
class Dog16 extends Animal16 {
    // 重写shout()方法
    public void shout() {
    	System.out.println("汪汪……"); 
    }
    public void eat() {
    	System.out.println("吃骨头……"); 
    }
}

// 定义测试类
public class Example16 {
    public static void main(String[] args) {
        Animal16 an =  new Dog16();	// 此时发生了向上转型,子类→父类
        Dog16 dog = (Dog16)an;		// 此时发生了向下转型
        dog.shout();
        dog.eat();
    }
}

注意:在向下转型时,不能直接将父类实例强制转换为子类实例,否则程序会报错。
例如,将案例二中的第22~23行代码修改为下面一行代码,则程序报错。
Dog dog = (Dog)new Animal(); // 编译错误

instanceof 关键字

Java 中可以使用 instanceof 关键字判断一个对象是否是某个类(或接口)的实例,语法格式如下所示。

对象  instanceof  类(或接口)

上述语法格式中,如果“对象”是指定的类的实例对象,则返回 true,否则返回 false。下面通过一个案例演示instanceof关键字的用法,具体代码下所示。

// 定义Animal类
class Animal17 {
    public void shout(){ 
    	System.out.println("动物叫……"); 
    }    
}
 
// 定义Dog类
class Dog17 extends Animal17 {
    // 重写shout()方法
    public void shout() {
    	System.out.println("汪汪……"); 
    }
    public void eat() {
    	System.out.println("吃骨头……"); 
    }
}
// 定义测试类
public class Example17 {
    public static void main(String[] args) {
        Animal17 a1 = new Dog17();         // 通过向上转型实例化Animal对象
        System.out.println("Animal a1 = new Dog():"+(a1 instanceof Animal17));
        System.out.println("Animal a1 = new Dog():"+(a1 instanceof Dog17));
        Animal17 a2 = new Animal17();     // 实例化Animal对象
        System.out.println("Animal a2 = new Animal():"+(a2 instanceof Animal17));
        System.out.println("Animal a2 = new Animal():"+(a2 instanceof Dog17));
    }
}

Object类

Java 提供了一个 Object 类,它是所有类的父类,每个类都直接或间接继承了 Object 类,因此 Object 类通常被称为超类。当定义一个类时,如果没有使用 extends 关键字为这个类显式地指定父类,那么该类会默认继承Object类
无论类是否使用了 extends 关键字,所有类都有父类 Object,而且这个父类是默认继承的。
Object 类中定义了一些常用方法,具体如下所示。

在这里插入图片描述
上表列举了 Object 类的常用方法,下面以 toString() 方法进行讲解,toString() 方法常用于将对象的基本信息以字符串的形式返回。下面通过一个案例演示 Object 类中 toString() 方法的使用,具体代码下所示。

// 定义Animal类
class Animal18 {  
    // 定义动物叫的方法 
    void shout() {	
    	System.out.println("动物叫!");
    }
}
// 定义测试类
public class Example18 {
    public static void main(String[] args)  {
        Animal animal = new Animal();			// 创建Animal类对象
        System.out.println(animal.toString());	// 调用toString()方法并打印
    }
}

在实际开发中,通常情况下不会直接调用 Object 类中的方法,因为 Object 类中的方法并不能适用于所有的子类,这时就需要对 Object 类中的方法进行重写,以符合实际开发需求。下面通过重写 Object 类的 toString() 方法进行演示。修改案例一,在Animal类中重写 toString() 方法,具体代码下所示。

// 定义Animal类
class Animal19 {  
    //重写Object类的toString()方法 
    public String toString() {	
    	return "动物叫!";
    }
}
// 定义测试类
public class Example19 {
    public static void main(String[] args)  {
        Animal animal = new Animal();			// 创建Animal类对象
        System.out.println(animal.toString());	// 调用toString()方法并打印
    }
}

内部类

在 Java 中,允许在一个类的内部定义类,这样的类称作内部类,内部类所在的类称作外部类。在实际开发中,根据内部类的位置、修饰符和定义方式的不同,内部类可分为4种,分别是成员内部类、局部内部类、静态内部类、匿名内部交类。本节对这4种形式的内部类进行讲解。

成员内部类

在一个类中除了可以定义成员变量、成员方法,还可以定义类,这样的类被称作成员内部类。成员内部类可以访问外部类的所有成员,无论外部类的成员是何种访问权限。如果想通过外部类访问内部类,则需要通过外部类创建内部类对象,创建内部类对象的具体语法格式如下:

外部类名 外部类对象 = new 外部类名();
外部类名.内部类名 内部类对象 = 外部类对象.new 内部类名();

下面通过一个案例学习如何定义成员内部类以及如何在外部类中访问内部类,具体代码如下所示。

class Outer {
    int m = 0; 	// 定义类的成员变量
    //外部类方法test1()
    void test1() {
    	System.out.println("外部类成员方法test1()");
	}
    // 下面的代码定义了一个成员内部类Inner
    class Inner {
        int n = 1;
        void show1() {
            // 在成员内部类的方法中访问外部类的成员变量m
            System.out.println("外部成员变量m = " + m);
            // 在成员内部类的方法中访问外部类的成员方法test1()
            test1(); 
        }
        void show2() {
        	System.out.println("内部成员方法show2()");
        }
	}
    //外部类方法test2()
    void test2() {						
        Inner inner = new Inner();		//实例化内部类对象inner
        System.out.println("内部成员变量n = " + inner.n);	//访问内部类变量和方法
        inner.show2();
    }
}
public class Example20 {
    public static void main(String[] args) {
        Outer outer = new Outer();			//实例化外部类对象outer
        Outer.Inner inner = outer.new Inner();	//实例化内部类对象inner
        inner.show1();	//在内部类中访问外部类的成员变量m和成员方法test1()
        outer.test2();	//在内部类中访问内部类的成员变量n和成员方法show2()
    }
}

想要实例化一个内部类,必须先实例化一个外部类,通过外部类对象,才可以实例化内部类。

局部内部类

局部内部类,也称为方法内部类,是指定义在某个局部范围中的类,它和局部变量都是在方法中定义的,有效范围只限于方法内部。
在局部内部类中,局部内部类可以访问外部类的所有成员变量和成员方法,而在外部类中无法直接访问局部内部类中的变量和方法。如果要在外部类中访问局部内部类的成员,只能在局部内部类的所属方法中创建局部内部类的对象,通过对象访问局部内部类的变量和方法。
下面通过一个案例,讲解局部内部类的定义以及如何访问局部内部类,具体代码如下所示。

class Outer {
    int m = 0;	// 定义类的成员变量
    //定义一个成员方法test1()
    void test1() {
    	System.out.println("外部类成员方法test1()");
    }
    void test2() {
        //定义一个局部内部类,在局部内部类中访问外部类变量和方法
        class Inner {
            int n = 1;
            void show() {
                System.out.println("外部成员变量m = " + m);
                test1();
        	}
    	}
        //访问局部内部类中的变量和方法
        Inner inner = new Inner();
        System.out.println("局部内部类变量n = " + inner.n);
        inner.show();
    }
}
public class Example21 {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.test2();   //通过外部类对象outer调用创建了局部内部类的方法test2()
    }
}

静态内部类

静态内部类,就是使用 static 关键字修饰的成员内部类。与成员内部类相比,在形式上,静态内部类只是在内部类前增加了 static 关键字,但在功能上,静态内部类只能访问外部类的静态成员,通过外部类访问静态内部类成员时,因为程序已经提前在静态常量区分配好了内存,所以即使静态内部类没有加载,依然可以通过外部类直接创建一个静态内部类对象。
创建静态内部类对象的基本语法格式如下。

外部类名.静态内部类名 变量名 = new 外部类名.静态内部类名();

下面通过一个案例学习静态内部类的定义和使用,具体代码如下所示。

class Outer {
    static int m = 0; // 定义类的静态变量
    // 下面的代码定义了一个静态内部类
    static class Inner {
        int n = 1;
        void show() {
            // 在静态内部类的方法中访问外部类的静态变量m
            System.out.println("外部类静态变量m = " + m);
        }
    }
}
public class Example22 {
    // 实例化静态内部类对象时,不需要通过外部类对象
    // 但是对于普通的内部类对象,实例化时,需要通过外部类对象,
    // 即先要实例化外部类对象,才可以实例化内部类对象
    public static void main(String[] args) {
        Outer.Inner inner = new Outer.Inner();
        inner.show();
    }
}

匿名内部类

在 Java 中调用某个方法时,如果该方法的参数是接口类型,那么在传参时,除了可以传入一个接口实现类,还可以传入实现接口的匿名内部类作为参数,在匿名内部类中实现接口方法。匿名内部类就是没有名称的内部类,定义匿名内部类时,其类体作为new语句的一部分。定义匿名内部类的基本语法格式如下所示。

new 继承的父类或实现的接口名(){
   //匿名内部类的类体
}

下面通过一个案例学习匿名内部类的定义和使用,具体代码如下所示。

interface Animal{			//定义接口Animal
	void shout();				//定义抽象方法shout()
}
public class Example23{
    public static void main(String[] args){
        String name = "小花";
        animalShout(
            new Animal(){	//调用animalShout()方法,参数为匿名内部类
                @Override
                public void shout() {
                	System.out.println(name+"喵喵...");
                }
            }
        );
    }
    public static void animalShout(Animal an){	//该方法参数为Animal接口类型
        an.shout();
    }
}

注意:在JDK 8之前,局部变量前必须加 final 修饰符,否则程序编译时报错。在案例中的匿名内部类中访问了局部变量 name,而局部变量 name 并没有使用 final 修饰符修饰,程序也没有报错。这是因为JDK 8的新增特性,允许在局部内部类、匿名内部类中访问非 final 修饰的局部变量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宇宙超级无敌霸王龙捏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值