大家好,我是栗筝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 提供了四种访问限定符:public
、protected
、default
(默认)和 private
。
- public:公共的,可以被任何其他类或对象访问;
- protected:受保护的,只能被相同包或其子类中的类或对象访问;
- default(默认):没有显式指定访问限定符时,默认为包级私有,只能在同一个包内访问;
- private:私有的,只能在定义该成员的类内部访问。
访问权限修饰符是一种强制机制,用来保证封装性,防止外部代码破坏对象的内部状态。
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
类的属性 name
和 age
被封装为私有,只能通过公共的 getter
和 setter
方法访问和修改。同时,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();}
}
在这个例子中,InnerClass
是 OuterClass
的成员内部类,可以访问 OuterClass
的 outerField
。
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();}
}
在这个例子中,StaticInnerClass
是 OuterClass
的静态内部类,可以访问 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 父类名或接口名() {// 方法重写@Overridepublic 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;@Overridepublic 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、this
和 super
关键字
this
和 super
是 Java 中的两个特殊关键字,它们在处理类和对象时非常有用。
-
this
关键字:this
是一个引用变量,它指向当前对象。在实例方法或构造函数中,this
通常用于引用当前对象的变量或方法。当类的成员变量与局部变量重名时,我们可以使用this
来区分它们。此外,this
还可以用于在一个构造函数中调用另一个构造函数; -
super
关键字:super
是一个引用变量,它指向当前对象的父类。我们可以使用super
来访问父类的变量和方法。当子类需要调用父类的构造函数或者需要访问父类的方法时,我们可以使用super
。此外,如果子类重写了父类的方法,我们也可以通过super
来调用父类的原始方法。
4.4、子类中方法的重写
在子类中,可以重写(override)父类的方法,以提供特定于子类的实现。重写方法时,方法名、返回类型和参数列表必须与父类方法一致。使用 @Override
注解可以帮助编译器检查方法是否正确重写。
方法重写的规则如下:
- 方法名和参数列表必须相同:只有当子类方法的方法名和参数列表与父类方法完全相同时,才能被视为重写;
- 返回值类型:
- 如果方法的返回值是基本数据类型,那么子类重写的方法的返回值类型必须与父类相同;
- 如果方法的返回值是引用数据类型,那么子类重写的方法的返回值类型可以是父类方法返回值类型的子类型(协变返回类型);
- 访问权限:子类重写的方法的访问权限不能小于父类方法的访问权限。例如,父类的方法是
protected
,子类的重写方法可以是protected
或public
,但不能是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";// 重写父类方法@Overridepublic 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)
:用于比较两个对象是否相等。默认实现是比较对象的引用地址,但可以在子类中重写这个方法来比较对象的内容。
@Override
public 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
方法,以确保相等的对象具有相等的哈希码。
@Override
public int hashCode() {return Objects.hash(field);
}
toString()
:返回对象的字符串表示。默认实现返回对象的类名和哈希码,可以在子类中重写这个方法以提供更有意义的字符串表示。
@Override
public String toString() {return "MyClass{" +"field='" + field + '\'' +'}';
}
clone()
:创建并返回当前对象的副本。需要实现Cloneable
接口,并重写clone
方法。
@Override
protected Object clone() throws CloneNotSupportedException {return super.clone();
}
finalize()
:对象被垃圾回收器回收之前调用的方法。通常不需要重写这个方法,依赖于垃圾回收器来管理资源。
@Override
protected 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;}@Overridepublic 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);}@Overridepublic int hashCode() {return Objects.hash(field);}@Overridepublic String toString() {return "MyClass{" +"field='" + field + '\'' +'}';}@Overrideprotected 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)); // 输出 trueSystem.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 {@Overridepublic void makeSound() {System.out.println("Dog barks");}
}class Cat extends Animal {@Overridepublic 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();
}
在这个例子中,只有当 myAnimal
是 Dog
的实例时,才会进行向下转型,避免了 ClassCastException
异常。