Java基础:面向对象(二)
文章目录
- Java基础:面向对象(二)
- 1. 面向对象编程思想
- 2. 类与对象
- 2.1 类
- 2.1.1 类的定义
- 2.1.2 成员变量
- 2.1.3 局部变量
- 2.2 对象
- 2.2.1 对象的定义
- 2.2.2 对象的使用
- 2.2.3 对象创建的原理
- 3. 封装
- 4. 构造方法
- 5. 对象作为方法的参数或返回类型
- 6. 对象数组
- 6.1 声明数组变量
- 6.2 创建数组对象
- 7. static关键字
- 7.1 static可以修饰属性
- 7.2 static可以修饰方法
- 7.3 static可以修饰代码块
- 7.4 单例模式
- 7.4.1 饿汉式单例模式
- 7.4.2 懒汉式单例模式
- 8. 使用访问修饰符实现封装
- 8.1 包的概念
- 8.2 导包操作
- 8.3 静态导入
- 9. 代码封装
- 10. this关键字
- 11. 继承关系
- 11.1 继承的概念
- 11.2 继承类型
- 11.3 继承的特性
- 11.4 子类实例化过程
- 11.5 属性的隐藏
- 11.6 方法重写(方法覆盖)
- 11.7 抽象方法和抽象类
- 11.8 final关键字的用法
- 12. 多态
- 12.1 多态的概念
- 12.2 向上转型(上溯造型)
- 12.3 向下转型(下溯造型)
- 12.4 里式代换原则
- 12.5 instanceof运算符
- 13. 组合关系
- 14. 接口
- 14.1 接口的概念
- 14.2 语法格式
- 14.3 接口的应用
- 14.3.1 接口的多态
- 14.3.2 接口与抽象类
- 14.3.3 简单工厂模式
- 14.3.4 工厂方法模式
- 14.3.5 缺省适配器模式
- 15. 内部类
- 15.1 简介
- 15.2 分类
- 15.2.1 成员内部类(普通内部类)
- 15.2.2 静态内部类
- 15.2.3 局部内部类
- 15.2.4 匿名内部类
1. 面向对象编程思想
- 面向对象分析(Object Oriented Analysis,OOA):面向对象分析是软件开发过程的第一步,它主要关注于对问题域的理解和分析。
- 面向对象设计(Object Oriented Design,OOD):面向对象设计是在面向对象分析的基础上进行的,它主要关注于如何将分析阶段得到的需求转化为具体的设计。
- 面向对象编程(Object-Oriented Programming,OOP):面向对象编程是面向对象软件开发的实现阶段,它主要关注于使用面向对象编程语言来实现设计阶段的成果。面向对象编程的主要特性包括封装性、继承性、多态性,这些特性有助于提高代码的可重用性、可维护性和可扩展性。
2. 类与对象
2.1 类
2.1.1 类的定义
在Java中,类(Class)是对象(Object)的模板,它定义了一类对象的属性和行为。类的定义格式如下:
[访问修饰符] class 类名{成员变量(属性);成员方法(行为);内部类;代码块;
}
2.1.2 成员变量
成员变量(包括实例变量和静态变量)是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。Java中类的成员变量都有默认值。例如,整数类型为0
,浮点数类型为0.0
,布尔类型为false
,字符类型为'\u0000'
,引用类型为null
。
2.1.3 局部变量
在方法体、方法参数或者代码块中定义的变量被称为局部变量。局部变量没有默认值,声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
❓问:成员变量有默认值,局部变量没有默认值,产生的原因?
✅答:成员变量和局部变量在默认值行为上的差异主要源于它们在生命周期、存储位置、作用域和初始化机制上的不同。以下是具体的原因:
- 生命周期
成员变量:随着类的实例化或类的加载而被创建,并在类的实例或类本身被销毁时销毁。
局部变量:在方法、构造函数或初始化块执行时创建,并在执行完毕后销毁。- 存储位置
成员变量:实例变量存储在堆内存中(随着对象的创建而分配),静态变量存储在方法区(也称为静态存储区或类变量区)。
局部变量:存储在栈内存中,每个方法执行时都会在栈内存中创建一个栈帧,局部变量就存储在这个栈帧中。- 作用域
成员变量:可以在类的任何方法中访问(对于实例变量)或在类的外部通过类的实例或类名(对于静态变量)访问。
局部变量:只能在声明它们的方法、构造函数或初始化块内部访问。- 初始化
成员变量:如果没有显式地初始化,Java在创建类的实例或JVM(Java虚拟机)类加载时,会自动为成员变量赋予默认值。
局部变量:使用前必须显式初始化,否则编译时会出现错误。
2.2 对象
2.2.1 对象的定义
对象是类的一个实例,有状态和行为。类和对象的关系,就是抽象和具体的关系。创建对象的基本语法格式如下:
类名 对象名 = new 类名();
2.2.2 对象的使用
调用对象的属性和方法,如果是在当前类中,可以直接访问属性和方法。如果在其他类中,通过点.
运算符来访问其属性和方法。例如,如果有一个名为Person
的类,需要创建per1
、per2
、per3
三个对象,使用set
和get
方法对属性进行赋值和取值操作。代码示例如下:
/* Test.java */
public class Test {public static void main(String[] args) {Person per1 = new Person();Person per2 = new Person();Person per3 = new Person("王麻子", 28);per1.setName("张三");per1.setAge(10);per2.setName("李四");per2.setAge(18);per1.init();System.out.println("姓名:" + per2.getName());System.out.println("年龄:" + per2.getAge());per3.init();}
}
/* Person.java */
public class Person {private String name;private int age;public Person() {}public Person(String name, int age) {this.name = name;this.age = age;}public void init() {System.out.println("姓名:" + name);System.out.println("年龄:" + 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;}
}
🆚🆚运行结果:
姓名:张三
年龄:10
姓名:李四
年龄:18
姓名:王麻子
年龄:28
2.2.3 对象创建的原理
- 在Java虚拟机(JVM)的内存模型中,程序计数器、Java栈、堆、本地方法栈和方法区是五个重要的组成部分。
- 当使用
new
关键字创建一个对象时,同时操作了栈和堆,在栈内存中保存对象的引用(对象的首地址),在堆内存中保存对象的属性。 - 编写
.java
文件(源文件)的时候,文件存储在硬盘上,这个文件只是包含了类的定义和源代码,但它并没有被JVM加载到内存中执行。当编译这个.java
文件时,Java编译器(如javac
命令)会检查源代码的语法和语义。编译成功后,它会生成一个或多个.class
文件(类文件)。这些类文件包含了JVM执行程序所需的字节码(Byte-code
)和其他信息。当运行(如java
命令)包含main
方法的类时,JVM会加载这个类文件到内存中。加载过程中,JVM会执行以下步骤:- 加载:JVM通过类加载器(ClassLoader)查找并加载指定的类文件到内存中。
- 链接:
- 验证:确保被加载的类的正确性和安全性。
- 准备:为类的静态变量分配内存,并将其初始化为默认值。
- 解析:把类中的符号引用转换为直接引用。
- 初始化:为类的静态变量赋予正确的初始值。
- 操作对象都是通过引用完成,一旦引用出栈,没有被引用的对象就变成了垃圾,JVM的垃圾回收器(Garbage Collector,GC)会负责回收其占用的内存空间。
3. 封装
- 定义:封装是将类的某些信息隐藏在类的内部,不允许外部程序直接访问,而是通过该类提供的方法来对隐藏的信息进行操作和访问。具体来说,封装通过定义私有变量(使用
private
作为访问修饰符)和公有方法(公共的getter
和setter
方法)来实现,私有变量只能在类的内部访问,而公有方法可以在类的外部访问和调用。
- 作用:数据隐藏,提高代码的可维护性,实现接口和抽象。降低耦合性,实现高内聚、低耦合。
4. 构造方法
在Java中,构造方法(也称为构造函数或构造器)是一种特殊的方法,用于在创建对象时初始化该对象的状态。构造方法的基本语法格式如下:
public class ClassName { // 类的成员变量 // ... // 构造函数 public ClassName() { // 构造函数的实现代码 // 这里可以初始化类的成员变量等 } // 如果有需要,可以定义带参数的构造函数 public ClassName(parameterType1 parameter1, parameterType2 parameter2, ...) { // 构造函数的实现代码 // 这里可以使用传入的参数来初始化类的成员变量等 } // 类的其他方法 // ...
}
- 名称:构造方法的名称必须与类名完全相同。这是Java语言规定的一个特性,以便在创建对象时能够自动调用相应的构造方法。
- 返回类型:构造方法没有返回类型,也不允许使用
void
关键字声明返回类型。这是因为构造方法的主要目的是初始化对象,而不是返回任何值。 - 访问修饰符:构造方法可以使用访问修饰符(如
public
、protected
、private
)来限制其可见性和可访问性。通常,构造方法被声明为public
,以便可以从类的外部创建对象。但是,在某些情况下,你可能希望将构造方法声明为protected
或private
,以实现特定的设计目标(如单例模式、工厂方法等)。 - 参数列表:构造方法可以具有任意数量和类型的参数。这些参数在创建对象时通过
new
关键字传递给构造方法,以便在初始化对象时使用。通过提供不同参数的构造方法,可以实现对象的多样化初始化。 - 默认构造方法:在Java中,每个类至少有一个构造方法。如果你没有显式地定义任何构造方法,那么Java编译器会自动为你生成一个默认的无参构造方法。如果你定义了至少一个构造方法,默认的无参构造方法可能不会被自动生成。
- 构造方法的调用:如果是在其他类中,通过
new
的方式来调用构造方法;如果是在自己的类中,可以在构造方法之间进行互相调用。使用this([参数])
,必须写在构造方法中的第一行。 - 继承:子类不会继承父类的构造方法,但子类构造方法可以通过
super()
关键字调用父类的构造方法。
5. 对象作为方法的参数或返回类型
创建一个类,然后创建该类的对象,并将这些对象作为参数传递给方法或作为方法的返回值。代码示例如下:
public class Person { private String name; private int age; // 构造方法、getter和setter方法... // 示例方法,接受Person对象作为参数 public void greet(Person otherPerson) { System.out.println(this.name + " says hello to " + otherPerson.getName()); } // 示例方法,返回Person对象 public Person getOlderPerson(Person person) { if (this.age > person.getAge()) { return this; } else { return person; } }
}
6. 对象数组
对象数组是一种可以存储多个对象的数组。每个数组元素都可以是一个对象的引用,指向一个实际的对象。
6.1 声明数组变量
声明一个数组变量,指定数组将要存储的对象的类型,并使用方括号[]
表示它是一个数组。
Student[] students; // 声明一个Student类型的数组变量
6.2 创建数组对象
使用new
关键字和类的名称来创建数组对象,并指定数组的大小(即它可以存储的元素数量)。
students = new Student[3]; // 创建一个可以存储3个Student对象的数组。
或者,可以在声明变量的同时创建数组对象:
Student[] students = new Student[3]; // 类名[] 数组 = new 类名[数组长度];
代码示例如下:
// Test.java
public class Test {public static void main(String[] args) {Student student1 = new Student("小明", "男");Student student2 = new Student("小红", "女");Student student3 = new Student("小丽", "女");Teacher teacher = new Teacher();Student[] students = new Student[3];students[0] = student1;students[1] = student2;students[2] = student3;teacher.sign(students);}
}
// Teacher.java
public class Teacher {public void sign(Student[] students) {for (Student student : students) {String gender = student.getGender();if (gender.equals("男")) {System.out.println(student.getName() + "到");} else {System.out.println(student.getName() + "有");}}}
}
// Student.java
// ...
🆚🆚运行结果:
小明到
小红有
小丽有
7. static关键字
7.1 static可以修饰属性
- 静态成员变量(类成员变量):使用
static
修饰的,作用范围在整个类的所有对象上。所有对象共享这个变量,在内存中只有一个副本。访问通过类名.变量名
访问。在类加载时被初始化,存储在堆里。 - 实例成员变量:没有
static
修饰,作用范围在某个实例对象上。在创建对象时被初始化,存在多个副本。各个对象不互相影响。通过实例对象名.变量名
访问。
7.2 static可以修饰方法
- 静态方法(类方法):使用
static
修饰,不能直接使用实例变量,只能直接使用静态变量。只能直接调用静态方法,不能直接调用实例方法。通过类名.方法名()
访问。不允许使用this
关键字(代表对象)。 - 实例方法:没有
static
修饰,既可以使用静态变量,又可以使用实例变量。既可以直接访问实例方法,又可以直接访问静态方法。 通过实例对象名.方法名()
访问。
代码示例如下:
public class Car {// 静态成员变量public static int count = 0;// 实例成员变量private String licenseNumber;private String brand;// 构造方法public Car(String licenseNumber, String brand) {this.licenseNumber = licenseNumber;this.brand = brand;// 每次创建Car对象时,都更新静态变量countcount++;}// 静态方法,用于返回当前Car对象的数量public static int getCurrentNumberOfCars() {return count;}// 实例方法,用于显示Car对象的详细信息public void displayDetails() {System.out.println("第" + count + "辆车的车牌号是:" + licenseNumber + ",品牌是:" + brand);}public static void main(String[] args) {// 可以通过类名直接访问静态成员变量System.out.println("最初车的数量:" + Car.count);// 创建Car对象,通过对象引用访问实例成员变量、实例方法Car car1 = new Car("A12345", "问界M9");System.out.println("第" + count + "辆车的车牌号是:" + car1.licenseNumber + ",品牌是:" + car1.brand);Car car2 = new Car("B67890", "小米SU7");car2.displayDetails();// 可以通过类名访问静态方法,获取Car对象的数量System.out.println("目前车的数量:" + Car.getCurrentNumberOfCars());}
}
🆚🆚运行结果:
最初车的数量:0
第1辆车的车牌号是:A12345,品牌是:问界M9
第2辆车的车牌号是:B67890,品牌是:小米SU7
目前车的数量:2
7.3 static可以修饰代码块
执行顺序和优先级:静态代码块>
构造代码块>
构造方法。
- 静态代码块:类加载的时候执行,并且只执行一次,优先于各种代码块和构造方法,最开始执行的。
- 构造代码块:在创建对象时被调用,每创建一次对象,都会执行一次构造代码块。 执行顺序,优先于构造方法执行。一个类如果有多个构造方法时,每通过构造方法,创建一个对象,代码块都会被执行一次。但是对于构造方法来讲,只执行某一个构造方法(根据参数执行)。
代码示例如下:
public class TestInitialization {public TestInitialization() {System.out.println("Constructor executed.");}{System.out.println("Instance block executed.");}static {System.out.println("Static block executed.");}public static void main(String[] args) {TestInitialization obj1 = new TestInitialization();TestInitialization obj2 = new TestInitialization();}
}
🆚🆚运行结果:
Static block executed.
Instance block executed.
Constructor executed.
Instance block executed.
Constructor executed.
7.4 单例模式
单例模式(Singleton Pattern)是一种创建型设计模式,确保一个类只能创建一个实例对象,自行实例化,并向整个系统提供这个对象。
7.4.1 饿汉式单例模式
饿汉式单例模式(Eager Initialization Singleton)是一种在类加载时就立即创建单例实例的设计模式。这种模式相比于懒汉式单例模式(Lazy Initialization Singleton),它的优势在于其线程安全性,因为JVM在加载类时会对静态变量进行初始化,并且这个初始化过程是线程安全的。
// 饿汉式单例模式
public class Singleton {private Singleton() { // 私有构造函数,防止外部通过new创建实例}private static final Singleton single = new Singleton(); // 提供一个当前类的私有静态成员变量public static Singleton getInstance() { // 提供一个公有的静态方法,返回成员变量return single;}
}
7.4.2 懒汉式单例模式
非线程安全的懒汉式单例模式是最基本的懒汉式单例模式实现,但在多线程环境下是不安全的。
// 非线程安全的懒汉式单例模式
public class Singleton2 {private Singleton2() {}private static Singleton2 singleton;public static Singleton2 getInstance() {if (singleton == null)singleton = new Singleton2();return singleton;}
}
为了保证懒汉式单例模式的线程安全,通常有以下3种方法:
- 线程安全的懒汉式单例模式:使用
synchronized
关键字来保证线程安全,但这种方式在性能上较差,因为每次调用getInstance()
方法时都需要进行同步; - 双重检查锁定(Double-Check Locking,DCL)的懒汉式单例模式:DCL模式通过两次检查实例是否为
null
来避免不必要的同步,是线程安全且性能较好的实现方式。为了避免指令重排导致的问题,需要将instance
声明为volatile
;
// 双重检查锁定的懒汉式单例模式
public class Singleton3 {private volatile static Singleton3 instance;private Singleton3() {}public static Singleton3 getInstance() {if (instance == null) { // 第一次检查 synchronized (Singleton3.class) {if (instance == null) { // 第二次检查 instance = new Singleton3();}}}return instance;}
}
- 静态内部类的懒汉式单例模式:利用静态内部类的特性来实现懒汉式单例模式,这种方式既保证了线程安全,又避免了同步带来的性能开销。
// 静态内部类的懒汉式单例模式
public class Singleton4 {private Singleton4() {}// 静态内部类 private static class Singleton4Holder {private static final Singleton4 INSTANCE = new Singleton4();}// 提供全局访问点 public static Singleton4 getInstance() {return Singleton4Holder.INSTANCE;}
}
8. 使用访问修饰符实现封装
8.1 包的概念
包(Package)是Java中用于组织类的命名空间。它可以把相关的类、接口、枚举和注解组合在一起,形成一个逻辑上的单元。包的主要目的是为了解决类的命名冲突和进行更好的代码组织。
以下是一些在Java类库中比较常用的包:
包名 | 说明 |
---|---|
java.lang | java语言包,不需要进行导包,就可以使用的 |
java.util | java实用工具包 |
java.io | java输入输出流包 |
java.sql | 操作数据库相关的包 |
java.net | java网络包 |
java.text | 处理文本、日期、时间、数字、消息的类和接口 |
java.swing | 图形用户界面 |
包的用途:
- 避免命名冲突;
- 可以按功能将类进行组合;
- 可以保护类、数据和方法。
📌注:
package
打包语句必须在代码的第一条语句。前面只能有空白和注释;- 使用
package
时,会在磁盘上生成对应的目录;- 每个源文件只能有一条打包语句。
8.2 导包操作
在同一个包中,类之间互相访问,可以不需导包操作;如果类不在同一个包下面,需要使用import
进行导包操作。
import
可以出现多次。- 声明在
package
之后,在class
之前。 - 导包可以使用通配符
*
。
8.3 静态导入
静态导入(Static Import)是一种导入静态成员(例如静态方法、静态字段或静态枚举常量)的方式,而无需通过类名来引用它们。要使用静态导入,需要使用import static
关键字。代码示例如下:
import static com.coder.test.MathUtils.add;
9. 代码封装
代码封装通常指的是使用不同的访问修饰符(Access Modifiers)来控制类、成员变量(属性)和方法的可见性和可访问性。Java提供了四种访问修饰符:
private | default | protected | public | |
---|---|---|---|---|
同类 | T | T | T | T |
同包 | F | T | T | T |
子类 | F | F | T | T |
通用性 | F | F | F | T |
- 属性和方法的访问修饰符
public
:公有的,最高的访问权限,所有类都可以访问,不管是否同包。protected
:受保护的,可以被同包的类访问,以及不同包中的子类访问。default
(无修饰符,也称为包级私有或默认访问权限):不使用访问修饰符关键字来声明,表示默认,当前类和同包可以访问。private
:只能在当前类中访问,不对外公开。
- 类的访问修饰符
public
:所有类都可以访问。default
:只能当前包中访问。
10. this关键字
this.属性名
:用于引用当前对象的该属性。this.方法名()
:用于调用当前对象的该方法。this([参数])
:用于调用当前类的另一个构造器,这称为构造器的重载调用或构造器链。必须是构造器(构造方法)中的第一个语句,且只能调用一次。this
:用于返回当前对象的引用。
11. 继承关系
11.1 继承的概念
继承是一种机制,它允许一个类(称为子类、派生类或扩展类)继承另一个类(称为父类、超类或基类)的属性和方法。子类可以继承父类的所有非私有成员(即public
、protected
和default
),并且可以选择性地覆盖(@Override
)父类的方法,或者添加新的属性和方法。继承关系通常被称为is-a
关系。继承的语法格式如下:
class 父类 {
}class 子类 extends 父类 {
}
11.2 继承类型
Java主要支持的是单一继承(Single Inheritance),即一个类只能直接继承自一个父类。
11.3 继承的特性
Java继承具有以下几个主要特性:
- 单一继承:Java只支持单继承,即一个类只能有一个直接的父类。虽然直接继承是单一的,但可以通过多层继承来间接地继承多个类的特性。
- 继承属性和方法:子类可以继承父类的非私有(
public
、protected
)属性和方法。子类可以访问和操作这些继承来的属性和方法。 - 方法重写(Override):子类可以提供一个与父类方法签名相同(方法名、参数列表和返回类型相同)但实现不同的方法。当通过子类对象调用该方法时,将执行子类中的方法实现,而不是父类中的方法。
- 构造方法不被继承:子类不继承父类的构造方法。但子类可以通过调用父类的构造方法来初始化从父类继承的属性。这通常通过
super()
关键字在子类构造方法中完成。 - 访问权限:子类可以访问父类的公有(
public
)和保护(protected
)成员,但不能直接访问父类的私有(private
)成员,必须通过getter
或setter
方式进行访问。 - 多态性:继承是实现多态性的基础之一。多态性允许在运行时根据对象的实际类型来调用相应的方法。例如,父类引用可以指向子类对象,并调用子类重写的方法。
- super关键字:子类可以通过
super
关键字来引用父类的属性和方法。这允许子类在需要时访问父类的实现或数据。 - 初始化顺序:在子类实例化时,首先执行父类的静态代码块,然后执行子类的静态代码块。接着,当创建子类对象时,会先调用父类的构造方法(如果有通过
super()
显式调用或者隐式调用),然后执行子类的构造方法中的代码。 - 子类可以添加新的属性和方法:子类可以拥有父类所没有的属性和方法,这扩展了父类的功能。
- final类和final方法:如果一个类被声明为
final
,则它不能被继承。同样,如果一个方法被声明为final
,则它不能在子类中被重写。
11.4 子类实例化过程
子类实例化过程:加载类(包括父类和子类)——>
初始化静态变量和静态初始化块——>
分配内存——>
调用父类构造函数——>
初始化父类实例变量和实例初始化块——>
执行父类构造函数体——>
初始化子类实例变量和实例初始化块——>
执行子类构造函数体——>
对象创建完成。
❓问:在子类创建对象时,是否也创建了父类的对象?
✅答:不会创建父类对象,只是创建了父类空间,并进行了初始化操作。
📌注:
- 如果父类有无参的构造方法,子类
super()
可以省略;- 如果父类中没有无参的构造方法,子类
super(参数)
不可以省略;- 如果使用
super()
显式地调用父类的构造方法,要求必须写在子类构造方法中的第一行;- 子类的构造方法中,不能同时出现
super()
和this()
。
11.5 属性的隐藏
- 在父类中定义一个属性,在子类中定义了一个同名的属性,在子类中访问,会隐藏父类的属性。
- 在子类中,有两个同名属性,一个是继承自父类的,使用
super
属性名访问,一个是子类自己的,使用this
属性名访问。 - 在方法中,如果访问到属性,继承自父类的方法,使用的是父类的属性,子类自己的方法,使用的是子类属性。
代码示例如下:
public class TestPro {public static void main(String[] args) {Son son = new Son();son.showName();son.testName();}
}
class Father {String name = "父类的属性";public void testName() {System.out.println("3. " + name);}
}
class Son extends Father {String name = "子类的属性";public void showName() {System.out.println("1. " + name);System.out.println("2. " + super.name + "\t" + this.name);}
}
🆚🆚运行结果:
1. 子类的属性
2. 父类的属性 子类的属性
3. 父类的属性
- 如果父类中有一个静态变量,则被所有子类所共享,其中一个子类修改了值,其他子类访问也是修改后的值。如果子类自己写了一个同名的静态变量,则子类中访问的是自己的变量,不再是父类变量。
代码示例如下:
public class TestPro2 {public static void main(String[] args) {Son son = new Son();son.showName(); // 打印父类的静态变量Son2 son2 = new Son2();son2.setName(); // 修改静态变量的值son.showName();}
}
class Father {static String name = "父类的属性";public void testName() {System.out.println("3. " + name);}
}
class Son extends Father {// static String name = "子类的属性";public void showName() {System.out.println("4. " + name);}
}
class Son2 extends Father {public void setName() {name = "新子类的属性";}
}
🆚🆚运行结果:
4. 父类的属性
4. 新子类的属性
11.6 方法重写(方法覆盖)
方法重写(Override)是面向对象编程的一个重要概念,它发生在子类与父类之间。方法重写的使用有以下要求:
- 方法签名相同:方法名、参数列表必须相同;
- 返回类型相同或子类:返回类型必须相同,或者是其子类;
- 访问权限不能更严格:子类方法的访问权限(
public
、protected
、default
、private
)不能低于父类中被重写的方法的访问权限; - 不能重写final方法:如果父类中的方法被声明为
final
,那么它不能被重写; - 不能重写static方法:如果父类中的方法是
static
的,那么子类中的同名同参数列表的方法并不是覆盖父类方法,而是隐藏了父类方法,因为static
方法是与类关联的,而不是与对象关联的,所以它们不参与多态; - 使用@Override注解:在Java中,建议在使用重写时加上
@Override
注解。这样,如果子类的方法不符合重写的要求(比如方法签名不匹配),编译器会报错; - 抛出异常不能更广泛:子类方法抛出的异常类型必须是父类方法抛出的异常类型的子集或者相同,子类方法不能抛出比父类方法更多的异常,但可以选择不抛出异常。
📌注:
- 使用方法重写的场景:子类和父类的方法要完成相同功能,但采用不同的算法或公式。
- 必须要重写的方法:抽象方法(由
abstract
修饰)必须在子类中被重写。- 不能重写的方法:构造方法(constructor)、私有方法(由
private
修饰)、最终方法(由final
修饰)、静态方法(由static
修饰)。
11.7 抽象方法和抽象类
抽象(Abstraction)是实现设计和实现相分离的一种关键手段。这种分离是面向对象编程(OOP)的基本原则之一,它使得软件设计更加模块化、可维护和可扩展。
- 抽象方法:使用
abstract
关键字修饰,只有方法的声明(方法头),没有方法的实现(方法体)。不能是静态方法。 - 抽象类:使用
abstract
关键字修饰,可以有抽象方法,也可以有普通方法,也可以有构造方法,也可以没有抽象方法。如果一个类中有抽象方法,则这个类必须是抽象类。抽象类不能实例化。
11.8 final关键字的用法
final
可以用来修饰类、方法、变量(包括实例变量、静态变量和局部变量)和常量表达式。以下是final
关键字的用法:
- 定义终结类(最终类)
- 当一个类定义为
final
时,就表明不能继承这个类,表示不能产生子类。 final
类里面的所有方法,都是隐式的final
,不会产生子类,也不会覆盖方法,所以不需要在final
类中为方法加final
关键字。
- 当一个类定义为
- 定义终结方法
- 不能被子类重写。所有
private
方法都是隐式的final
方法,在private
方法中,不需要使用final
关键字修饰。 final
方法可以被重载(同名,参数列表不同)。
- 不能被子类重写。所有
代码示例如下:
private int test1() {return 0;
}
//...
public final void test2() {//...
}
public final void test2(int x) {//...
}
- 定义常量
- 修饰引用类型:无法更改引用所指向的对象地址,但引用的对象属性可以修改。
- 修饰基本数据类型:无论是编译期常量还是非编译期常量,一旦它们被初始化,它们的值就不能再被修改。
- static final:
static final
的作用主要是用来声明全局常量,这些常量属于类并且其值不能被改变。这样的常量通常在类加载时初始化,并且可以在整个应用程序中通过类名来访问。
代码示例如下:
public class Test {// 编译期常量final int[] a = {10, 20, 30, 40}; // 引用类型final int i = 10; // 基本数据类型// 非编译期常量Random random = new Random();final int j = random.nextInt(10);public static void main(String[] args) {final Student student = new Student(); // 引用类型student.setName("张三");student.setGender("男");student.setName("李四");System.out.println(student);Test t = new Test();System.out.println(t.i);System.out.println(t.j);}
}
class Student {//...@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", gender='" + gender + '\'' +'}';}
}
🆚🆚运行结果:
Student{name='李四', gender='男'}
10
6
📌注:
final
不可以和abstract
放在一起对方法和类进行修饰。
12. 多态
12.1 多态的概念
在Java中,多态(Polymorphism)是面向对象编程的三大特性之一(封装、继承和多态),它允许我们以统一的方式处理不同类型的对象。以下是多态的必要条件和优点:
- 多态的必要条件
- 继承关系:多态需要存在继承关系。
- 方法重写:子类必须重写父类的方法,以实现自己的特有行为。
- 父类引用指向子类对象:在程序中使用父类类型的引用变量指向子类对象,即将子类对象赋给父类引用变量。
- 父类引用调用重写方法:通过父类引用变量调用子类对象中重写的方法。
- 运行时绑定:通过动态绑定机制,根据对象的实际类型来调用相应的方法。这是实现多态的关键机制之一。
- 多态的优点
- 简化代码。
- 面向抽象编程,不面向具体编程(依赖倒转原则)。
- 易于扩展,增强代码可读性。
12.2 向上转型(上溯造型)
向上转型(Upcasting)是指将子类的引用赋值给父类类型的变量。由于子类继承了父类的所有属性和方法(除了私有属性和方法),因此父类类型的变量可以引用子类对象。这种转换是自动的,不需要显式地进行类型转换。语法格式如下:
父类类型 对象名 = new 子类类型();
12.3 向下转型(下溯造型)
向下转型(Downcasting)是指将父类类型的引用转换为子类类型的变量。由于父类引用可能指向的是子类对象,也可能是其他子类对象或父类对象本身,因此这种转换是不安全的,因为它可能会导致ClassCastException
。为了进行向下转型,我们需要显式地使用类型转换操作符,并且最好使用instanceof
关键字进行检查,以确保转换是安全的。语法格式如下:
子类类型 对象名 = (子类类型)父类引用;
代码示例如下:
class Animal {void eat() {System.out.println("所有的动物都要吃东西");}
}
class Dog extends Animal {@Overridevoid eat() {System.out.println("小狗爱吃骨头");}
}
class Cat extends Animal {@Overridevoid eat() {System.out.println("小猫爱吃鱼");}
}
public class Main {public static void main(String[] args) {// 向上转型Animal animal1 = new Dog();animal1.eat();Animal animal2 = new Cat();// 向下转型if (animal2 instanceof Animal) { // 使用instanceof关键字进行检查Cat cat = (Cat) animal2;cat.eat();}}
}
🆚🆚运行结果:
小狗爱吃骨头
小猫爱吃鱼
12.4 里式代换原则
里式代换原则(Liskov Substitution Principle,LSP)是面向对象设计的基本原则之一。这个原则强调了在程序设计中,子类应当能够替换其父类并出现在父类能够出现的任何地方,而不会改变程序的行为。里式代换原则要求:
- 子类必须完全实现父类的方法;
- 父类出现的地方,子类一定可以出现;
- 子类出现的地方,父类不一定可以出现。
12.5 instanceof运算符
判断一个对象是否属于某个类(或其父类),返回boolean
类型的值,如果属于就返回true
,否则返回false
。
object instanceof Class
其中:object
是要测试的对象,Class
是要检查的类名。
13. 组合关系
组合(Composition)关系是一种表示“部分-整体”关系的设计概念,它表示一个对象(组合对象)包含另一个对象(被组合对象)作为其实例变量(字段、属性)。这种关系也称为has-a
关系,它表示一个对象有另一个对象作为其一部分。代码示例如下:
class Engine {// Engine类的属性和方法private String type;public Engine(String type) {this.type = type;}public String getType() {return type;}
}
class Car {// Car类包含Engine类的实例变量,表示组合关系private Engine engine;public Car(Engine engine) {this.engine = engine;}public void start() {// 使用被组合对象的方法System.out.println("汽车开始启动" + engine.getType() + "引擎");}
}
public class Main {public static void main(String[] args) {Engine engine = new Engine("V8");Car car = new Car(engine);car.start();}
}
🆚🆚运行结果:
汽车开始启动V8引擎
📌注:合成聚合复用原则(Composite/Aggregate Reuse Principle,CARP)是面向对象设计中的一个重要原则,它强调在复用代码时,应该优先使用对象的组合/聚合关系,而不是通过继承关系来达到复用的目的。
14. 接口
14.1 接口的概念
接口(Interface)是一个完全抽象的类,它不能被实例化,但可以被类实现(Implement)。实现关系通常被称为like-a
关系。接口主要用于定义一组方法,这些方法被称为接口方法,这些方法不提供具体的实现,而是由实现接口的类来提供。
14.2 语法格式
接口的语法格式如下:
[修饰符] interface 接口名 [extends 接口名[, 接口名...]] { // 常量(字段) [public] [static] [final] 数据类型 常量名 = 值; // 抽象方法 [public] [abstract] 返回类型 方法名(参数列表); // 默认方法(从JDK 1.8开始) [public] default 返回类型 方法名(参数列表) { // 方法体 } // 静态方法(从JDK 1.8开始) [public] static 返回类型 方法名(参数列表) { // 方法体 } // 私有方法(从Java 9开始) // 私有静态方法 private static 返回类型 方法名(参数列表) { // 方法体 } // 私有实例方法(从Java 9开始) private 返回类型 方法名(参数列表) { // 方法体 }
}
类的语法格式如下:
[修饰符] class 类名 [implements 接口名[, 接口名...]] { // 成员变量(字段) [修饰符] 数据类型 变量名; // 构造方法 [修饰符] 类名(参数列表) { // 构造方法体 } // 方法 [修饰符] 返回类型 方法名(参数列表) { // 方法体 } // 初始化块(可选) { // 初始化代码 } // 内部类、接口等(可选)
}
其中:
- 接口的定义:
[访问修饰符] interface 接口名{}
。 - 接口中的数据成员(变量)默认都是
public static final
的,也就是公有的静态常量。 - 接口中的方法默认是
public abstract
的,因此可以省略这些修饰符。 - 从Java 8(JDK 1.8)开始,接口中可以包含带有具体实现的方法,称为默认方法,使用
default
来标记。 - 从Java 8开始,接口中可以包含静态方法,使用
static
来标记,并且不能是抽象的。 - 从Java 9开始,接口中可以包含私有方法(包括私有静态方法和私有实例方法),这些私有方法只能在接口内部使用,并且不能被实现类访问。
- 接口不是类,没有构造方法,不能实例化。
- 接口可以实现多继承。
- 一个接口,可以被多个类实现。
- 类和接口的关系:
class 类名 implements 接口名{}
。 - 一个类可以实现多个接口:
class 类名 implements 接口1[,接口2...]{}
。 - 一个类可以在继承一个父类的同时,实现一个或多个接口:
class 类名 extends 父类 implements 接口1[,接口2...]{}
。
14.3 接口的应用
14.3.1 接口的多态
接口(Interface)是实现多态性(Polymorphism)的一种重要方式。接口的多态实现方式主要依赖于这三个关键概念:接口定义,类实现接口,接口引用指向实现类的对象。代码示例如下:
/* 接口定义 */
interface Shape { // 图形void draw();void calculateArea();
}
/* 类实现接口 */
class Circle implements Shape { // 圆@Overridepublic void draw() {System.out.println("画个圆");}@Overridepublic void calculateArea() {// 实现计算圆面积的代码}
}
class Rectangle implements Shape { // 矩形@Overridepublic void draw() {System.out.println("画个矩形");}@Overridepublic void calculateArea() {// 实现计算矩形面积的代码}
}
public class Main {public static void main(String[] args) {/* 接口引用指向实现类的对象 */Shape shape1 = new Circle(); // Circle 是 Shape 的一个实现Shape shape2 = new Rectangle(); // Rectangle 也是 Shape 的一个实现// 调用 draw 方法shape1.draw();shape2.draw();// 调用接口中定义的其他方法shape1.calculateArea();shape2.calculateArea();}
}
🆚🆚运行结果:
画个圆
画个矩形
14.3.2 接口与抽象类
接口(Interface)和抽象类(Abstract Class)是两种用于实现多态性和代码重用的重要机制。接口与抽象类的主要区别:
- 目的:接口主要用于定义对象的行为规范,而抽象类则用于定义对象的共同属性和行为的模板。
- 方法:接口只包含抽象方法,而抽象类可以包含抽象方法和非抽象方法。
- 继承与实现:类通过
extends
关键字继承抽象类,通过implements
关键字实现接口。 - 多重继承:一个类可以实现多个接口,但只能继承一个抽象类(直接继承)。
- 字段:接口中的字段都是常量(隐式
static
和final
),而抽象类可以包含各种类型的字段。
14.3.3 简单工厂模式
简单工厂模式(Simple Factory Pattern)是属于创建型模式,又叫做静态工厂方法(Static Factory Method)模式,它由一个工厂类根据传入的参数来决定创建哪一种产品类的实例。简单工厂模式是最简单的工厂模式,但它不是一种设计模式,因为它没有遵循开闭原则(Open-Closed Principle),即扩展开放,修改关闭。
代码示例如下:
// 定义一个产品接口
interface Product {void use();
}
// 实现产品接口的具体产品类
class ProductA implements Product {@Overridepublic void use() {System.out.println("使用产品A");}
}
class ProductB implements Product {@Overridepublic void use() {System.out.println("使用产品B");}
}
// 工厂类,负责创建产品对象
class SimpleFactory {// 静态方法,根据传入的参数创建对应的产品对象public static Product createProduct(String type) {if ("A".equals(type)) {return new ProductA();} else if ("B".equals(type)) {return new ProductB();} else {return null; // 或者抛出异常}}
}
// 客户端代码
public class Client {public static void main(String[] args) {// 使用工厂类创建产品对象Product productA = SimpleFactory.createProduct("A");if (productA != null) {productA.use(); }Product productB = SimpleFactory.createProduct("B");if (productB != null) {productB.use();}// 试图创建一个不存在的产品类型Product productC = SimpleFactory.createProduct("C");if (productC == null) {System.out.println("无法创建产品C");}}
}
🆚🆚运行结果:
使用产品A
使用产品B
无法创建产品C
14.3.4 工厂方法模式
工厂方法模式(Factory Method Pattern)定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建的任务交给它的子类来完成。这样做既保证了核心类的稳定,又使得子类可以根据自身需要来灵活创建对象。
代码示例如下:
// 产品接口
interface Product {void use();
}
// 具体产品类A
class ProductA implements Product {@Overridepublic void use() {System.out.println("使用产品A");}
}
// 具体产品类B
class ProductB implements Product {@Overridepublic void use() {System.out.println("使用产品B");}
}
// 工厂接口
interface Factory {Product createProduct();
}
// 具体工厂类A
class FactoryA implements Factory {@Overridepublic Product createProduct() {return new ProductA();}
}
// 具体工厂类B
class FactoryB implements Factory {@Overridepublic Product createProduct() {return new ProductB();}
}
// 客户端代码
public class Client {public static void main(String[] args) {Factory factoryA = new FactoryA();Product productA = factoryA.createProduct();productA.use();Factory factoryB = new FactoryB();Product productB = factoryB.createProduct();productB.use();}
}
🆚🆚运行结果:
使用产品A
使用产品B
14.3.5 缺省适配器模式
缺省适配器模式(Default Adapter Pattern)是适配器模式(Adapter Pattern)的一个特殊形式,是一种结构型设计模式,用于处理接口中方法数量过多,或者大部分方法都有默认实现的情况。缺省适配器模式通过提供一个实现了目标接口的缺省适配器类,该类为接口中的每个方法都提供了默认实现,从而允许子类继承该适配器类时只需要关注自己感兴趣的方法,而不需要实现接口中的所有方法。
代码示例如下:
// 目标接口
interface MyInterface {void method1();void method2();void method3();// ... 可能还有其他方法
}
// 缺省适配器类,实现了MyInterface接口并提供默认实现
class DefaultAdapter implements MyInterface {@Overridepublic void method1() {System.out.println("Default implementation of method1");}@Overridepublic void method2() {System.out.println("Default implementation of method2");}@Overridepublic void method3() {System.out.println("Default implementation of method3");}// ... 其他方法的默认实现
}
// 具体实现类,继承自缺省适配器并覆盖部分方法
class MySpecificImplementation extends DefaultAdapter {@Overridepublic void method1() {// 覆盖默认实现System.out.println("Specific implementation of method1");}// method2 和 method3 使用默认实现
}
// 客户端代码
public class Main {public static void main(String[] args) {MyInterface myImpl = new MySpecificImplementation();myImpl.method1();myImpl.method2();myImpl.method3();}
}
🆚🆚运行结果:
Specific implementation of method1
Default implementation of method2
Default implementation of method3
15. 内部类
15.1 简介
- 在一个类中,定义另一个类,称作内部类(Inner Class)。
- 内部类可以访问外部类中的属性和方法,不需要创建外部类的对象。
- 外部类要访问内部类的属性民和方法,需要创建内部类的对象.
- 如果内部类有和外部类同名变量和方法,则内部类的变量和方法优先级更高。
- 外部类,修饰符
public
、default
。 - 内部类,修饰符
public
、protected
、default
、private
。
15.2 分类
15.2.1 成员内部类(普通内部类)
成员内部类(Member Inner Classes)可以直接访问外部类的变量和方法(包括私有和静态修饰的),不能使用static
声明变量和方法的。编译后生成的class
文件命名为 “外部类名$内部类名”。不能直接new
一个内部类对象,必须使用外部类对象来创建内部类对象。
代码示例如下:
class Test {public static void main(String[] args) {OuterClass outer = new OuterClass();OuterClass.MemberInner MemberInner = outer.new MemberInner();MemberInner.test();}
}
class OuterClass {private int x = 10;private static int y = 20;class MemberInner {int x = 30;public void test() {System.out.println(OuterClass.this.x + x + y);}}
}
🆚🆚运行结果:
60
📌注:如果外部类和内部类具有相同名称的成员变量或方法,内部类可以直接访问内部类的成员变量和方法,如果内部类访问外部类的成员变量或方法时,需要
this
关键字。
15.2.2 静态内部类
静态内部类(Static Inner Classes)使用static
修饰的内部类,不能直接访问外部类的非静态成员变量和方法。如果要访问外部类的实例变量和方法,需要创建外部类的实例对象。可以创建静态变量和方法。静态内部类创建对象时,不需要创建外部类的对象,直接可以创建内部类的对象。代码示例如下:
代码示例如下:
class Test1 {public static void main(String[] args) {OuterStatic.test1(); // 类名.方法名()OuterStatic.InnerStatic innerStatic = new OuterStatic.InnerStatic(); // 创建内部类的对象innerStatic.test(); // 实例对象名.方法名()OuterStatic.InnerStatic.test1();}
}
class OuterStatic {private int x = 10;public static void test1() { // 静态方法System.out.println("OuterStatic");}static class InnerStatic {private static int y = 20; // 静态变量public static void test1() { // 静态方法System.out.println("InnerStatic");}public void test() { // 实例方法test1();OuterStatic outer = new OuterStatic();System.out.println(outer.x);}}
}
🆚🆚运行结果:
OuterStatic
InnerStatic
10
InnerStatic
15.2.3 局部内部类
局部内部类(Local Inner Classes)定义在外部类的方法或代码块中。可以直接访问外部类的所有成员变量和方法(包括私有的、静态的),不能使用访问修饰符(地位相当于局部变量),可以使用final
修饰,防止其他类去继承。作用范围就在当前的方法或代码块中。
代码示例如下:
class Test2 {public static void main(String[] args) {OuterLocal outerLocal = new OuterLocal();outerLocal.outerMethod();}
}
class OuterLocal {public void outerMethod() {final int localVar = 10; // 保证数据运行的一致性,使用final来修饰class InnerLocal {public void display() {System.out.println("Accessing localVar: " + localVar);}}InnerLocal inner = new InnerLocal();inner.display();}
}
🆚🆚运行结果:
Accessing localVar: 10
15.2.4 匿名内部类
匿名内部类(Anonymous Inner Classes)是没有类名的内部类,通常用于继承一个类或实现一个接口,并且只需要使用一次。常用于GUI编程中的事件监听器、线程等。匿名内部类不能定义构造函数。语法格式如下:
new <类或接口> <类的主体>
代码示例如下:
例1:
class Test3 {public static void main(String[] args) {AddInterface inter = new AddInterface() {@Overridepublic void add(int x, int y) {System.out.println(x + y);}};OuterAnony anony = new OuterAnony();anony.testAdd(inter, 10, 20);}
}
class OuterAnony {public void testAdd(AddInterface addInterface, int x, int y) {addInterface.add(x, y);}
}
interface AddInterface {void add(int x, int y);
}
例2:
class Test3 {public static void main(String[] args) {OuterAnony anony = new OuterAnony();anony.testAdd(new AddInterface() {@Overridepublic void add(int x, int y) {System.out.println(x + y);}}, 10, 20);}
}
例3:
class Test3 {public static void main(String[] args) {OuterAnony anony = new OuterAnony();anony.testAdd((x, y) -> System.out.println(x + y), 10, 20); // Lambda表达式}
}
🆚🆚运行结果:
30
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
以上内容是关于Java面向对象的基础知识,希望对初学者或再次学习者有所帮助,基础打扎实,不怕风吹雨打!如果以上内容有错误或者内容不全,望大家提出!我也会继续写好每一篇博文!
👍👍👍
待续未完
🙊🙊🙊
欢迎观看和提问!!!
👏👏👏
下一篇:Java基础:异常(三)