Java 面向对象 -- Java 语言的封装、继承、多态、内部类和 Object 类

CSDN 2024-06-17 15:35:04 阅读 98

大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 007 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。

在我们对 Java 语言进行基本介绍之后,本篇文章将带领大家深入了解 Java 的面向对象编程(OOP)概念。我们将探讨封装、继承和多态这三大核心概念,以及内部类的使用和 Object 类的重要性。通过对这些内容的学习,读者将能够更好地设计和实现复杂的 Java 应用程序。

最后在前言的末尾我补充一下,如果这篇文章,对大家有所帮助或收获一定的乐趣和想法,那么非常欢迎大家能够,点赞、评论、收藏、订阅。这些也将是我持续更新的最大动力。


文章目录

1、面向对象的概念1.1、面向对象和面向过程的区别1.2、面向对象的一般步骤 2、关于封装2.1、封装的概念2.2、访问限定符2.3、封装的优点 3、Java 内部类3.1、成员内部类(Member Inner Class)3.2、静态内部类(Static Nested Class)3.3、局部内部类(Local Inner Class)3.4、匿名内部类(Anonymous Inner Class)(涉及接口) 4、关于继承4.1、什么是继承4.2、子类和超类4.2.1、子类4.2.2、超类 4.3、`this` 和 `super` 关键字4.4、子类中方法的重写 5、`Object` 类5.1、`Object` 类的重要方法5.2、`Object` 类的使用示例 6、特性:多态6.1、多态概述6.2、注意事项6.3、编译时多态与方法签名6.4、运行时多态6.5、向上转型6.6、向下转型6.7、`instanceof` 关键字


1、面向对象的概念

1.1、面向对象和面向过程的区别

面向过程和面向对象是两种不同的编程范式,它们各有优缺点。

面向过程:将问题分解成步骤,然后按照步骤实现函数,执行时依次调用函数。数据和对数据的操作是分离的。面向过程的优点是性能比面向对象高,因为不需要进行对象的实例化。但是,面向过程的代码不易于维护、复用和扩展;

面向对象:将问题分解成对象,描述事物在解决问题的步骤中的行为。对象与属性和行为是关联的。面向对象的优点是具有封装、继承、多态的特性,因此代码易于维护、复用和扩展,可以设计出低耦合的系统。但是,由于需要实例化对象,因此面向对象的性能比面向过程低。

在 Java 中,面向对象的三大特性是:

封装:封装是将对象的状态(属性)和行为(方法)包装在一起的过程。封装可以隐藏对象的内部实现细节,只暴露出需要的信息。这样可以保护对象的内部状态,防止外部直接访问对象的内部数据;

继承:继承是从已有的类派生出新的类的过程。新的类(子类)可以继承父类的属性和方法,并可以添加新的属性和方法,也可以重写父类的方法。继承可以提高代码的复用性,使得子类可以拥有父类的所有功能;

多态:多态是指允许一个接口使用多种实际类型的能力。多态可以使得代码更加灵活和可扩展。在 Java 中,多态主要体现在接口的多实现和类的多重继承。

1.2、面向对象的一般步骤

面向对象编程的一般步骤如下:

提炼问题领域中的对象:这一步主要是通过分析问题,找出其中涉及的名词,这些名词通常就是我们需要创建的对象;

对象描述:这一步是对找出的对象进行描述,明确这些对象应该具备哪些属性和行为。这些属性和行为在编程中通常体现为对象的变量和方法;

对象实例化:这一步是通过new关键字创建对象的实例。每个对象实例都有自己的属性和行为;

调用对象功能:这一步是通过对象实例调用其功能,也就是执行对象的方法。

以上就是面向对象编程的一般步骤。


2、关于封装

2.1、封装的概念

封装的基本思想是将对象的属性和行为隐藏起来,只对外暴露必要的接口。这样可以保护对象的内部状态,防止外部代码随意修改对象的属性,保证对象的完整性和一致性。

举个例子,计算机是一种复杂的设备,但对用户来说,只需通过一些简单的操作,如开关机、键盘输入、鼠标操作等,就可以与计算机交互。计算机内部的复杂硬件和工作原理被封装在一个壳子里,用户无需了解这些细节,只需使用提供的接口进行操作。

在 Java 中,封装就是将数据和操作数据的方法结合在一起,隐藏对象的属性和实现细节,只对外提供必要的接口进行交互。

2.2、访问限定符

Java 通过类和访问权限来实现封装。类可以将数据和操作数据的方法结合在一起,而访问权限控制方法和字段的可见性和访问范围。Java 提供了四种访问限定符:publicprotecteddefault(默认)和 private

public:公共的,可以被任何其他类或对象访问;protected:受保护的,只能被相同包或其子类中的类或对象访问;default(默认):没有显式指定访问限定符时,默认为包级私有,只能在同一个包内访问;private:私有的,只能在定义该成员的类内部访问。

访问权限修饰符是一种强制机制,用来保证封装性,防止外部代码破坏对象的内部状态。

image-20240130114024442

2.3、封装的优点

封装具有以下优点:

安全性:通过封装,可以隐藏对象的内部实现细节,防止外部代码随意访问和修改对象的属性,提高系统的安全性;

可读性和可维护性:封装使得对象的属性只能通过特定的方法访问和修改,修改逻辑集中在这些方法中,提高代码的可读性和可维护性;

易用性:封装可以对外部隐藏不必要的细节,只暴露必要的接口,使得对象的使用更加简单和直观,减少调用者的迷惑和错误。

下面是一个示例,展示了如何在 Java 中实现封装:

public class Person { // 私有属性 private String name; private int age; // 公共构造方法 public Person(String name, int age) { this.name = name; this.age = age; } // 公共的 getter 和 setter 方法 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) { this.age = age; } else { System.out.println("年龄必须是正数"); } } // 其他公共方法 public void displayInfo() { System.out.println("Name: " + name + ", Age: " + age); } public static void main(String[] args) { // 创建对象并使用公共方法访问私有属性 Person person = new Person("Alice", 25); person.displayInfo(); person.setAge(30); person.displayInfo(); }}

在这个例子中,Person 类的属性 nameage 被封装为私有,只能通过公共的 gettersetter 方法访问和修改。同时,setter 方法中还添加了逻辑检查,确保年龄是正数,从而保证了对象的状态一致性和安全性。

通过以上示例,可以看到封装不仅保护了对象的内部状态,还提供了对外统一的访问接口,提高了代码的安全性、可读性和可维护性。


3、Java 内部类

Java 内部类是定义在另一个类内部的类。内部类主要用于封装逻辑和行为,使代码更易读、易维护,并且可以更好地实现封装。Java 提供了几种不同类型的内部类,每种都有其特定的用途和特性。

内部类有以下几个主要用途:

封装:内部类可以访问外部类的所有成员(包括私有成员),因此,我们可以使用内部类来隐藏复杂的实现细节,提供简单的接口;

增强封装性和可读性:内部类可以将相关的类组织在一起,这样可以使代码更易于阅读和维护;

支持多重继承:Java 不支持多重继承,但我们可以使用内部类来模拟多重继承;

实现回调:内部类常常用于实现回调。在 GUI 编程和多线程编程中,我们经常需要在某个特定的时间点执行某个特定的任务,这时我们就可以使用内部类。

内部类是一种高级特性,它可以使我们的代码更加整洁、灵活和易于维护。我们可以将内部类分为四种:成员内部类、静态内部类、方法内部类和匿名内部类。

3.1、成员内部类(Member Inner Class)

成员内部类是定义在另一个类的成员位置的类。成员内部类可以访问外部类的所有成员,包括私有成员。

示例:

public class OuterClass { private String outerField = "Outer field"; /** 成员内部类 */ public class InnerClass { public void display() { System.out.println("Outer field: " + outerField); } } public static void main(String[] args) { OuterClass outer = new OuterClass(); OuterClass.InnerClass inner = outer.new InnerClass(); inner.display(); }}

在这个例子中,InnerClassOuterClass 的成员内部类,可以访问 OuterClassouterField

3.2、静态内部类(Static Nested Class)

静态内部类是使用 static 修饰的内部类。静态内部类不能访问外部类的非静态成员,但可以访问外部类的静态成员。

示例:

public class OuterClass { private static String staticOuterField = "Static Outer field"; /** 静态内部类 */ public static class StaticInnerClass { public void display() { System.out.println("Static Outer field: " + staticOuterField); } } public static void main(String[] args) { OuterClass.StaticInnerClass staticInner = new OuterClass.StaticInnerClass(); staticInner.display(); }}

在这个例子中,StaticInnerClassOuterClass 的静态内部类,可以访问 OuterClass 的静态成员 staticOuterField

3.3、局部内部类(Local Inner Class)

局部内部类是在方法或代码块内部定义的类。局部内部类只能在其定义的范围内使用,通常用于实现一些临时性的功能。

示例:

public class OuterClass { public void outerMethod() { final String localVar = "Local variable"; /** 局部内部类 */ class LocalInnerClass { public void display() { System.out.println("Local variable: " + localVar); } } LocalInnerClass localInner = new LocalInnerClass(); localInner.display(); } public static void main(String[] args) { OuterClass outer = new OuterClass(); outer.outerMethod(); }}

在这个例子中,LocalInnerClass 是定义在 outerMethod 方法内部的局部内部类,可以访问方法的局部变量 localVar

3.4、匿名内部类(Anonymous Inner Class)(涉及接口)

匿名内部类是一种没有名字的内部类,它通常用于只需要使用一次的场合。

匿名内部类通常用于以下两种类型的场合:

实现接口:匿名内部类可以在定义一个类的同时实现一个接口。例如,我们可以在创建一个线程时使用匿名内部类来实现 Runnable 接口;

继承类:匿名内部类可以在定义一个类的同时继承一个类。例如,我们可以在创建一个图形界面的按钮时使用匿名内部类来继承 ActionListener 类。

匿名内部类的语法格式如下:

new 父类名或接口名() { // 方法重写 @Override public void method() { // 执行语句 }}

匿名内部类是一种简洁的语法,它可以让我们的代码更加简洁和易于阅读。但是,由于匿名内部类没有名字,所以它只能在定义的地方使用,不能在其他地方引用,这限制了它的使用范围。

示例:

public class OuterClass { public void createThread() { /** 匿名内部类 */ Thread thread = new Thread() { public void run() { System.out.println("Anonymous Inner Class Thread running"); } }; thread.start(); } public static void main(String[] args) { OuterClass outer = new OuterClass(); outer.createThread(); }}

在这个例子中,匿名内部类继承了 Thread 类并重写了 run 方法。


4、关于继承

4.1、什么是继承

继承(Inheritance): 从一个已知的类中派生出一个新的类,新类可以拥有已知类的行为和属性,并且可以通过覆盖/重写来增强已知类的能力,子类共性的方法或者属性直接采用父类的,而不需要自己定义,只需要扩展自己的个性化。

继承的本质在于抽象。类是对对象的抽象,继承是对某一批类的抽象,从而实现对现实世界更好的建模。

继承的优点:提高代码的复用性、提供了多态的前提、为多态做铺垫等

继承的初始化顺序:

父类的静态代码块 -> 子类的静态代码块 -> 父类的构造代码块 -> 父类的无参构造方法 -> 子类的构造代码块 -> 子类的无参构造方法

4.2、子类和超类
4.2.1、子类

子类是继承自另一个类的类。子类不仅继承了父类的所有属性和方法,还可以添加自己的属性和方法,或者重写父类的方法以提供特定的实现。子类通过使用 extends 关键字来实现继承。

例如,定义一个 Dog 类作为 Animal 类的子类:

public class Dog extends Animal { String breed; @Override public void makeSound() { System.out.println("Dog barks"); } public void fetch() { System.out.println("Dog fetches the ball"); }}

在这个例子中,Dog 类继承了 Animal 类的所有属性和方法,并添加了一个新的属性 breed 以及一个新的方法 fetch()。同时,它重写了 makeSound() 方法,以提供狗特有的实现。

4.2.2、超类

超类是被其他类继承的类。超类提供了子类可以重用的通用属性和方法。在上面的例子中,Animal 类就是 Dog 类的超类。

4.3、thissuper 关键字

thissuper 是 Java 中的两个特殊关键字,它们在处理类和对象时非常有用。

this 关键字:this 是一个引用变量,它指向当前对象。在实例方法或构造函数中,this 通常用于引用当前对象的变量或方法。当类的成员变量与局部变量重名时,我们可以使用 this 来区分它们。此外,this 还可以用于在一个构造函数中调用另一个构造函数;

super 关键字:super 是一个引用变量,它指向当前对象的父类。我们可以使用 super 来访问父类的变量和方法。当子类需要调用父类的构造函数或者需要访问父类的方法时,我们可以使用 super。此外,如果子类重写了父类的方法,我们也可以通过 super 来调用父类的原始方法。

4.4、子类中方法的重写

在子类中,可以重写(override)父类的方法,以提供特定于子类的实现。重写方法时,方法名、返回类型和参数列表必须与父类方法一致。使用 @Override 注解可以帮助编译器检查方法是否正确重写。

方法重写的规则如下:

方法名和参数列表必须相同:只有当子类方法的方法名和参数列表与父类方法完全相同时,才能被视为重写;返回值类型: 如果方法的返回值是基本数据类型,那么子类重写的方法的返回值类型必须与父类相同;如果方法的返回值是引用数据类型,那么子类重写的方法的返回值类型可以是父类方法返回值类型的子类型(协变返回类型); 访问权限:子类重写的方法的访问权限不能小于父类方法的访问权限。例如,父类的方法是 protected,子类的重写方法可以是 protectedpublic,但不能是 private;异常:子类重写的方法抛出的异常类型必须是父类方法抛出的异常类型或其子类型。final 方法:被 final 修饰的方法不能被重写。private 方法:被 private 修饰的方法不能被重写。静态方法:静态方法不能被重写,如果子类中定义了与父类中静态方法相同的方法,那么这个方法不是重写父类的方法,而是隐藏了父类的方法。

以下是一个示例,展示了继承和方法重写的应用:

class Parent { public String name = "Parent"; // 父类方法 public void showMessage() { System.out.println("This is the parent class."); } // 父类静态方法 public static void staticMethod() { System.out.println("Static method in parent class."); } // final 方法 public final void finalMethod() { System.out.println("Final method in parent class."); }}class Child extends Parent { public String name = "Child"; // 重写父类方法 @Override public void showMessage() { System.out.println("This is the child class."); } // 子类静态方法,隐藏父类静态方法 public static void staticMethod() { System.out.println("Static method in child class."); } // 尝试重写 final 方法会导致编译错误 // public void finalMethod() { // System.out.println("Cannot override final method."); // } public void displayNames() { System.out.println("Name in child class: " + name); System.out.println("Name in parent class: " + super.name); }}public class Main { public static void main(String[] args) { Child child = new Child(); child.showMessage(); // 输出 "This is the child class." child.displayNames(); // 输出子类和父类的 name 属性 child.finalMethod(); // 输出 "Final method in parent class." // 调用静态方法 Parent.staticMethod(); // 输出 "Static method in parent class." Child.staticMethod(); // 输出 "Static method in child class." // 父类引用指向子类对象 Parent parent = new Child(); parent.showMessage(); // 输出 "This is the child class." parent.staticMethod(); // 输出 "Static method in parent class." }}

代码解析:

成员变量调用:Child 类中定义了一个与 Parent 类同名的成员变量 name。在 displayNames 方法中,使用 super.name 访问父类的 name 变量;方法调用和重写: 子类重写了父类的 showMessage 方法。调用 child.showMessage() 时,输出 “This is the child class.”;静态方法 staticMethod 在子类中定义后隐藏了父类的静态方法。调用 Parent.staticMethod()Child.staticMethod() 时,分别输出不同的信息; final 方法:子类无法重写父类的 finalMethod 方法,尝试重写会导致编译错误。


5、Object

Object 类是所有类的超类:在 Java 中,所有的类都直接或间接继承自 Object 类。Object 类是 Java 类层次结构的根类,它提供了一些通用的方法,这些方法可以被所有类使用或重写。

5.1、Object 类的重要方法

以下是 Object 类提供的一些重要方法:

equals(Object obj):用于比较两个对象是否相等。默认实现是比较对象的引用地址,但可以在子类中重写这个方法来比较对象的内容。

@Overridepublic boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } MyClass myClass = (MyClass) obj; return this.field.equals(myClass.field);} hashCode():返回对象的哈希码。哈希码用于哈希表,如 HashMap。如果重写了 equals 方法,也应该重写 hashCode 方法,以确保相等的对象具有相等的哈希码。

@Overridepublic int hashCode() { return Objects.hash(field);} toString():返回对象的字符串表示。默认实现返回对象的类名和哈希码,可以在子类中重写这个方法以提供更有意义的字符串表示。

@Overridepublic String toString() { return "MyClass{" + "field='" + field + '\'' + '}';} clone():创建并返回当前对象的副本。需要实现 Cloneable 接口,并重写 clone 方法

@Overrideprotected Object clone() throws CloneNotSupportedException { return super.clone();} finalize()对象被垃圾回收器回收之前调用的方法。通常不需要重写这个方法,依赖于垃圾回收器来管理资源。

@Overrideprotected void finalize() throws Throwable { super.finalize();} getClass():返回对象的运行时类。

Class<?> clazz = obj.getClass(); notify()notifyAll()wait():用于线程间通信。这些方法与对象的监视器相关联,用于协调线程间的协作。

synchronized (obj) { obj.notify();}

5.2、Object 类的使用示例

以下是一个示例,展示了如何使用 Object 类的这些方法:

import java.util.Objects;public class MyClass { private String field; public MyClass(String field) { this.field = field; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } MyClass myClass = (MyClass) obj; return Objects.equals(field, myClass.field); } @Override public int hashCode() { return Objects.hash(field); } @Override public String toString() { return "MyClass{" + "field='" + field + '\'' + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public static void main(String[] args) throws CloneNotSupportedException { MyClass obj1 = new MyClass("value"); MyClass obj2 = new MyClass("value"); System.out.println("Equals: " + obj1.equals(obj2)); // 输出 true System.out.println("HashCode obj1: " + obj1.hashCode()); // 输出 obj1 的哈希码 System.out.println("HashCode obj2: " + obj2.hashCode()); // 输出 obj2 的哈希码 System.out.println("ToString: " + obj1.toString()); // 输出 obj1 的字符串表示 MyClass obj3 = (MyClass) obj1.clone(); System.out.println("Clone: " + obj3.toString()); // 输出 obj3 的字符串表示 synchronized (obj1) { obj1.notify(); } }}


6、特性:多态

6.1、多态概述

多态是面向对象编程中的一个重要概念,主要有以下几个特点和优点:

多种形态:多态是指一个对象可以有多种形态。在 Java 中,一个子类的对象既可以作为子类的引用,也可以作为父类的引用。这就是多态的体现,也就是父类引用变量可以指向子类对象。

多态的定义格式:在 Java 中,多态可以通过以下三种方式来定义:

普通类:父类 变量名 = new 子类();抽象类:抽象类 变量名 = new 抽象类子类();接口:接口 变量名 = new 接口实现类();

优点:多态可以提高代码的可维护性和扩展性。通过多态,我们可以编写出更加通用的代码,这些代码对于不同的对象可以有不同的行为。

6.2、注意事项

在 Java 中使用多态时,需要注意以下几点:

多态的类型:Java 中的多态主要是运行时多态,也就是在运行时才确定具体调用哪个对象的方法

继承或实现关系:多态必须在有继承或实现关系的类之间才能实现。也就是说,一个父类的引用可以指向它的子类对象,或者一个接口的引用可以指向它的实现类对象;

方法重写:在多态中,同一个父类的方法可能会被不同的子类重写。当调用这个方法时,实际调用的是各个子类重写后的方法;

成员访问:在多态中,成员变量的访问遵循“编译看左边,运行看左边”的原则。也就是说,编译时参考的是引用变量的类型(左边),运行时调用的也是引用变量类型对应的方法,如果子类中没有重写这个方法,那么就调用父类的方法。

6.3、编译时多态与方法签名

编译时多态,也被称为方法重载(Overload),是指在同一个类中定义多个同名但参数列表不同的方法。编译器会根据方法的签名(方法名和参数列表)来确定具体调用哪个方法。

示例:

public class OverloadExample { public void print() { System.out.println("No parameters"); } public void print(String s) { System.out.println("String parameter: " + s); } public static void main(String[] args) { OverloadExample example = new OverloadExample(); example.print(); // 调用无参数的方法 example.print("Hello"); // 调用有一个字符串参数的方法 }}

6.4、运行时多态

运行时多态,也被称为动态绑定或方法重写(Override),是指在运行时才确定具体调用哪个对象的方法。

示例:

class Animal { public void makeSound() { System.out.println("Animal makes a sound"); }}class Dog extends Animal { @Override public void makeSound() { System.out.println("Dog barks"); }}class Cat extends Animal { @Override public void makeSound() { System.out.println("Cat meows"); }}public class PolymorphismExample { public static void main(String[] args) { Animal myAnimal = new Dog(); // 向上转型 myAnimal.makeSound(); // 输出 "Dog barks" myAnimal = new Cat(); // 向上转型 myAnimal.makeSound(); // 输出 "Cat meows" }}

6.5、向上转型

向上转型是 Java 中多态的一种表现形式,将子类对象赋值给父类引用,这个过程也被称为隐式转型或自动转型。

示例:

Animal myAnimal = new Dog(); // Dog 对象被赋值给 Animal 类型的引用myAnimal.makeSound(); // 调用 Dog 的 makeSound 方法

6.6、向下转型

向下转型是将已经向上转型的父类引用转换为子类引用,这个过程需要使用强制类型转换。注意,直接创建的父类对象是无法向下转型的。

示例:

Animal myAnimal = new Dog(); // 向上转型Dog myDog = (Dog) myAnimal; // 向下转型myDog.makeSound(); // 调用 Dog 的 makeSound 方法

需要注意的是,在进行向下转型时,可能会出现 ClassCastException 类型转换异常。因此,在进行向下转型之前,必须进行类型判断,可以使用 instanceof 关键字来判断。

6.7、instanceof 关键字

instanceof 是 Java 中的一个关键字,主要用于判断一个对象是否是某个类的实例,或者一个引用变量是否是某个类型的引用。

示例:

if (myAnimal instanceof Dog) { Dog myDog = (Dog) myAnimal; myDog.makeSound();}

在这个例子中,只有当 myAnimalDog 的实例时,才会进行向下转型,避免了 ClassCastException 异常。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。