Java面向对象进阶
目录
一、不可变对象
1. 定义
2. 特性
3. 实现原则
4. 优点
5. 缺点
6. 常见的不可变对象
二、继承
1. 子类对父类成员的继承情况
2. 虚方法和虚方法表
(1)定义
(2)特点
3. 成员方法的调用规则
4. 方法重写
三、构造方法
1. 访问特点
2. 调用规则
3. 构造方法重载
(1)特点
(2)使用场景
(3)与 super() 的区别
四、带有继承结构的标准 JavaBean 类
1. 基本规范
2. 示例代码
五、多态
1. 多态调用成员的特点
(1)调用成员变量的特点
(2)调用成员方法的特点
(3)总结
(4)对比表格
2. 多态的优点和缺点
(1)多态的优点
(2)多态的缺点
3. instanceof的使用
(1)instanceof的作用
(2)instanceof的优缺点
四、final关键字
五、权限修饰符分类
1. public(公共)
2. protected(受保护)
3. default(默认/包私有)
4. private(私有)
5. 总结
六、代码块
1. 局部代码块
2. 构造代码块(构造块)
3. 静态代码块
4. 同步代码块
5. 普通代码块
6. 代码块的特点与使用场景
一、不可变对象
1. 定义
Java中的不可变对象(Immutable Object)是指一旦创建后,其状态和属性在整个生命周期内都无法被修改的对象。由于对象状态不可变,多个线程同时访问时无需额外的同步机制,从而天然具备线程安全性;此外,开发者无需担心对象状态在运行过程中被意外修改,大大简化了代码逻辑和数据管理流程。因此,不可变对象在并发编程场景中被广泛应用,成为提升程序稳定性和性能的重要设计工具。
2. 特性
-
状态不可变:一旦创建,对象的状态(包括基本数据类型和引用类型)不能被改变。
-
线程安全:由于状态不可变,多个线程可以安全地共享一个不可变对象,无需额外的同步机制。
-
可预测性:不可变对象的行为是可预测的,因为其状态不会在运行时发生变化。
3. 实现原则
为了创建一个不可变对象,通常需要遵循以下规则:
-
声明为
final
:将类声明为final
,防止其他类继承并修改状态。 -
私有字段:所有字段都应声明为
private
,以防止外部直接访问和修改。 -
构造函数初始化:通过构造函数初始化所有字段,并确保构造过程不会抛出异常。
-
不提供修改方法:不提供任何修改字段值的方法(如
setter
方法),以避免外部修改。 -
深拷贝:如果包含可变对象作为字段,则需要在构造函数中创建深拷贝,以确保引用类型的字段也保持不变。
例如:
public final class ImmutablePerson {private final String name;private final int age;public ImmutablePerson(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public int getAge() {return age;}}
上述代码中,name
和age
字段被声明为final
,构造函数中直接初始化这些字段,并且没有提供修改字段的方法。
4. 优点
-
线程安全:不可变对象天生是线程安全的,因为其状态不会改变,因此无需同步机制即可安全共享。
-
易于管理和维护:由于状态不可变,开发者可以更容易地推理代码逻辑,避免因状态变化导致的错误。
-
减少内存开销:不可变对象可以被缓存和重用,例如Java中的
String
类通过缓存减少了内存使用。 -
提高代码安全性:不可变对象不会因为外部修改而引发问题,从而提高了代码的安全性。
5. 缺点
-
性能开销:每次修改不可变对象时都需要创建新的对象实例,这可能导致性能下降。
-
复杂性:对于包含大量字段或复杂逻辑的类,实现不可变性可能会增加设计和实现的复杂性。
6. 常见的不可变对象
Java平台库中提供了许多内置的不可变类,例如:
-
String
-
Integer
-
BigInteger
-
BigDecimal
-
Date
(JDK 8之后)
这些类的设计都遵循了不可变对象的原则,例如通过构造函数初始化字段并禁止修改。
二、继承
1. 子类对父类成员的继承情况
-
构造方法:在继承关系里,子类不能直接继承父类的构造方法。不过,子类可以利用
super()
来调用父类的构造方法,进而完成父类部分的初始化操作。 -
成员变量:子类能够继承父类的所有成员变量。但对于父类的私有成员变量,子类不能直接访问和修改,必须借助父类提供的
get()
和set()
方法来间接操作。 -
成员方法:子类可以继承父类中可被纳入虚方法表的成员方法(即非
private
、非static
、非final
的成员方法)。
2. 虚方法和虚方法表
(1)定义
虚方法:虚方法是一种在面向对象编程中具备重要特性的方法。在 C# 和 C++ 中,要借助 virtual
关键字来声明虚方法;而在 Java 里,默认只要方法不是 private
、static
或者 final
类型,就可当作虚方法。虚方法的一大关键特性是,它允许子类对其进行重写。与虚方法不同,非虚方法(像 private
、static
、final
方法)不允许被重写,也不会被列入虚方法表。因为这些方法具有特定的用途和调用规则,不会因为对象的实际类型而改变调用行为。
虚方法表:在 Java 中,每个类都会生成一张虚方法表。这张表涵盖了该类以及其所有父类的虚方法。当子类继承父类时,会继承父类的虚方法表,并且把自身定义的虚方法添加到表中。在调用虚方法时,程序会直接查询虚方法表,无需像普通方法调用那样逐级向上查找,从而显著提升了方法调用的效率。
(2)特点
-
虚方法是实现运行时多态的核心机制。运行时多态意味着可以使用基类的引用去调用派生类的方法,具体调用哪个类的方法会在程序运行时根据对象的实际类型动态决定。这种特性让代码更具灵活性和可扩展性,提高了代码的复用性和可维护性。
-
构造方法在面向对象编程里有其独特的地位,它既不能被继承,也不能被声明为虚方法。构造方法的主要作用是在创建对象时对对象进行初始化,每个类的构造方法都有其特定的职责和实现逻辑,不存在继承和重写的情况。
public class Animal {public void eat() {System.out.println("动物吃食物");} // 虚方法}public class Dog extends Animal {@Overridepublic void eat() {System.out.println("狗啃骨头");} // 重写虚方法}
3. 成员方法的调用规则
就近原则:子类调用方法时,会先在子类中查找,如果没有则逐级向上查找父类的方法。
this 和 super 关键字:
-
this
:从当前类的成员方法开始查找(就近原则)。 -
super
:直接调用父类的方法。
4. 方法重写
当父类的中的方法不能满足子类的需求时,子类可以通过方法重写重新定义从父类继承的方法。
-
子类重写的方法上需要加上
@Override
注解显式标记(编译时检查)。 -
如果子类重写了父类的方法,调用时会优先使用子类的方法。
-
子类重写方法的方法名和参数列表必须与父类一致,返回值类型必须小于或等于父类,且子类的访问权限不能比父类更严格(建议重写的方法尽量和父类保持一致)。
-
只有能被添加到虚方法表中的方法才能被重写,方法重写的本质是覆盖了虚方法表中的方法。
public class Animal {public void eat() {System.out.println("动物吃食物");}}public class Dog extends Animal {@Overridepublic void eat() {System.out.println("狗啃骨头");}public void printEat() {this.eat(); // 调用子类方法super.eat(); // 调用父类方法}}
三、构造方法
1. 访问特点
子类的所有构造方法默认会先访问父类的无参构造方法,随后才执行自身的构造逻辑。这是因为在子类进行初始化操作时,可能会用到父类中的数据。若父类尚未完成初始化,子类便无法正常使用这些数据。因此,在子类进行初始化之前,必须先调用父类的构造方法,以此来完成父类数据空间的初始化工作。
2. 调用规则
当父类存在无参构造器时,若子类的构造器没有显式调用 super()
,系统会自动默认调用 super()
。若要显式使用 super()
,则必须将其置于构造器的第一行。如果想调用父类的带参构造器,就一定要显式使用 super()
。
public class Test {// 主方法public static void main(String[] args) {// 创建一个Student对象s1Student s1 = new Student();// 创建一个Student对象s2,并传入姓名和年龄Student s2 = new Student("张三", 18);// 输出s2的姓名和年龄System.out.println(s2.name + " " + s2.age); // 张三 18}}public class Person {String name;int age;// 无参构造方法public Person() {System.out.println("父类的无参构造");}// 有参构造方法public Person(String name, int age) {this.name = name;this.age = age;}}public class Student extends Person {// 无参构造方法,调用父类的无参构造方法,并打印"子类的无参构造"public Student() {super();System.out.println("子类的无参构造");}// 带参数的构造方法,调用父类的带参数构造方法public Student(String name, int age) {super(name, age);}}
3. 构造方法重载
在 Java 中可以使用 this
关键字在同一个类的不同构造函数之间相互调用,以减少代码重复,这种方式称为构造函数重载(Constructor Chaining)。
(1)特点
-
使用
this(参数列表)
调用本类的另一个构造函数。 -
this()
必须放在构造函数的第一行,否则会编译错误。 -
不能递归调用(如
A()
调用A()
会导致无限循环)。 -
不能和
super()
同时使用(因为super()
也必须放在第一行)。
示例:
public class Person {private String name;private int age;// 无参构造public Person() {this("Null", 0); // 调用带参构造}// 带参构造public Person(String name, int age) {this.name = name;this.age = age;}}
Person()
调用 Person(String name, int age)
,避免重复初始化逻辑。
(2)使用场景
减少代码重复:多个构造函数共享初始化逻辑,避免重复代码。
提供默认值:无参构造提供默认值,带参构造允许自定义。
(3)与 super()
的区别
关键字 | 作用 | 是否必须第一行 | 能否递归 |
---|---|---|---|
this() | 调用本类的其他构造 | 必须 | 不能 |
super() | 调用父类的构造 | 必须 | 不能 |
四、带有继承结构的标准 JavaBean 类
JavaBean 是一种符合特定规范的 Java 类,通常用于封装数据。当 JavaBean 涉及继承结构时,需要遵循一些额外的规则。
1. 基本规范
-
所有属性私有化(private)
-
提供无参构造方法
-
提供 getter 和 setter 方法
-
实现
Serializable
接口(可选,用于序列化) -
重写
equals()
、hashCode()
和toString()
方法
2. 示例代码
父类 (Person.java)
public class Person {// 私有属性private String name;private int age;private String gender;// 无参构造public Person() {}// 全参构造public Person(String name, int age, String gender) {this.name = name;this.age = age;this.gender = gender;}// 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) {this.age = age;}public String getGender() {return gender;}public void setGender(String gender) {this.gender = gender;}public void eat() {System.out.println("吃饭");}}
子类 (Student.java)
public class Student extends Person {// 子类特有属性private String major;// 无参构造public Student() {}// 全参构造(包含父类和子类的属性)public Student(String name, int age, String gender, String major) {super(name, age, gender); // 调用父类构造方法this.major = major;}// getter 和 setter 方法public String getMajor() {return major;}public void setMajor(String major) {this.major = major;}@Overridepublic void eat() {System.out.println("在学校食堂吃饭");}}
关键点说明
-
构造方法:子类构造方法中使用
super()
调用父类构造方法,如果父类没有无参构造,子类必须显式调用父类的有参构造。 -
getter/setter:子类只需要为新增的属性提供 getter/setter,父类的属性通过继承的 getter/setter 访问。
五、多态
1. 多态调用成员的特点
(1)调用成员变量的特点
-
编译时:成员变量的访问在编译时依赖于引用变量的类型。也就是说,编译器会检查引用变量所属类中是否存在该成员变量,如果存在则编译通过,否则编译失败。例如,如果有一个父类
Fu
和一个子类Zi
,并且Fu
中有一个成员变量num
,而Zi
中没有定义num
,那么通过Fu
类型的引用访问num
是合法的,而通过Zi
类型的引用访问num
也是合法的,但如果通过Fu
类型的引用访问Zi
中特有的成员变量,则会导致编译错误。 -
运行时:成员变量的访问在运行时仍然依赖于引用变量的类型。即使实际对象是子类对象,通过父类引用访问成员变量时,访问的是父类中的成员变量。例如,如果有一个
Fu
类型的引用指向一个Zi
对象,通过该引用访问num
时,访问的是Fu
中的num
,而不是Zi
中的num
(如果存在的话)。
(2)调用成员方法的特点
-
编译时:成员方法的访问在编译时依赖于引用变量的类型。编译器会检查引用变量所属类中是否存在该方法,如果存在则编译通过,否则编译失败。例如,如果有一个父类
Fu
和一个子类Zi
,并且Fu
中有一个方法show()
,而Zi
中重写了show()
方法,那么通过Fu
类型的引用调用show()
是合法的,而通过Zi
类型的引用调用show()
也是合法的。 -
运行时:成员方法的访问在运行时依赖于实际对象的类型。即使引用变量是父类类型,如果实际对象是子类对象,并且子类重写了该方法,则调用的是子类重写的方法。例如,如果有一个
Fu
类型的引用指向一个Zi
对象,通过该引用调用show()
时,调用的是Zi
中的show()
方法,而不是Fu
中的show()
方法。
(3)总结
-
成员变量:编译和运行时都参考引用变量所属的类中的成员变量。
-
成员方法:编译时参考引用变量所属的类中的方法,运行时参考实际对象所属的类中的方法。
(4)对比表格
特性 | 成员变量 | 成员方法 |
---|---|---|
绑定方式 | 静态绑定(编译时决定) | 动态绑定(运行时决定) |
决定因素 | 引用变量的声明类型 | 实际创建的对象类型 |
是否有多态性 | 无 | 有 |
能否被重写 | 不能 | 能(非private/final/static) |
访问权限 | 遵循访问控制修饰符 | 遵循访问控制修饰符 |
2. 多态的优点和缺点
(1)多态的优点
-
提高代码的扩展性和通用性:多态使得代码能够以父类类型的形式接收子类对象,从而在运行时根据对象的实际类型调用其特有的方法或属性。例如,父类引用可以调用子类重写的方法,从而实现代码的灵活性和通用性。
-
减少代码冗余:使用多态性,可以减少代码中对不同类的重复处理,通过统一的接口或父类实现多种功能,从而简化代码结构。
-
提高代码的可维护性:多态性使得代码更加灵活,便于后期维护和扩展。例如,当需要添加新的子类时,只需扩展现有类,而无需修改已有代码。
-
解耦合:多态性通过父类引用调用子类方法,实现了代码的解耦合,使得程序的各个部分可以独立运行,降低了模块间的依赖性。
(2)多态的缺点
-
不能访问子类特有的方法:在多态性中,父类引用只能调用父类声明的方法,而无法直接调用子类中特有的方法。如果需要访问子类特有的方法,必须通过向下转型(强制类型转换)来实现。
-
可能导致代码复杂性增加:多态性虽然提高了代码的灵活性,但同时也增加了代码的复杂性,尤其是在需要频繁进行类型判断和转换时。例如,向下转型可能会引发运行时错误,如
ClassCastException
。 -
性能开销:多态性依赖于运行时类型检查,这会导致一定的性能开销。例如,Java虚拟机需要通过虚拟方法表(vtable)来实现多态性,这会增加额外的计算负担。
-
代码可读性降低:过度依赖多态性可能导致代码难以理解,尤其是当多层继承和多态嵌套时,代码的逻辑变得复杂,增加了维护难度。
3. instanceof
的使用
instanceof
关键字用于判断一个对象是否属于某个类或接口的实例,其基本语法为:object instanceof Class
,返回值为布尔值,表示对象是否为指定类的实例。
if (obj instanceof Cat) {Cat cat = (Cat) obj;cat.eat();} else if (obj instanceof Dog) {Dog dog = (Dog) obj;dog.eat();}
(1)instanceof
的作用
-
类型检查:
instanceof
用于判断对象是否为特定类的实例,从而避免直接强制类型转换可能引发的ClassCastException
。 -
向下转型的前置条件:在使用向下转型时,通常先使用
instanceof
检查对象是否为子类,以避免因类型不匹配而导致运行时错误。
(2)instanceof
的优缺点
-
优点:
-
简单易用:通过
instanceof
可以快速判断对象的类型,避免复杂的类型转换逻辑。 -
提高代码安全性:通过
instanceof
检查,可以避免因类型转换错误导致的运行时异常。
-
-
缺点:
-
代码耦合性增加:过度依赖
instanceof
可能导致代码过于依赖类型判断,降低代码的可读性和可维护性。 -
性能问题:
instanceof
操作会增加额外的运行时开销,尤其是在频繁调用的场景中,可能影响性能。
-
四、final
关键字
场景 | 作用 |
---|---|
final 修饰变量 | 被final 修饰的变量只能赋值一次(即常量)。基本数据类型:值不可变。引用数据类型:引用地址不可变,但对象内部属性可修改。 |
final 修饰方法 | 禁止子类重写final 的修饰方法(锁定实现),但允许重载。 |
final 修饰class | 禁止继承该类(所有成员方法隐式为final )。 |
五、权限修饰符分类
1. public
(公共)
-
作用范围:
public
修饰的成员可以在任何地方被访问,包括同一个包内的类、不同包中的类以及子类。 -
特点:具有最大的访问权限,通常用于公开接口或方法,以便外部代码可以方便地使用。
2. protected
(受保护)
-
作用范围:
protected
修饰的成员只能在同一个包内的类中访问,也可以被不同包中的子类访问。 -
特点:比
public
的访问权限小,但比default
和private
大,适用于需要在子类中共享数据或方法的场景。
3. default
(默认/包私有)
-
作用范围:
default
修饰的成员仅能在同一个包内被访问,不能被其他包中的类访问。 -
特点:没有显式声明修饰符时,默认权限即为
default
。它适用于希望限制成员访问范围到当前包内的场景。
4. private
(私有)
-
作用范围:
private
修饰的成员只能在定义它的类内部被访问,不能被同一包内的其他类或外部类访问。 -
特点:具有最小的访问权限,通常用于隐藏实现细节,保护类的内部状态。
5. 总结
修饰符 | 同类 | 同包 | 不同包子类 | 不同包非子类 |
---|---|---|---|---|
private | ✔ | ✖ | ✖ | ✖ |
default | ✔ | ✔ | ✖ | ✖ |
protected | ✔ | ✔ | ✔ | ✖ |
public | ✔ | ✔ | ✔ | ✔ |
六、代码块
Java中的代码块是通过花括号 {}
包围的一段代码,用于定义逻辑相关的语句。根据其位置和声明的不同,Java中的代码块可以分为以下几种类型:
1. 局部代码块
局部代码块是在方法内部定义的代码块,用于限定局部变量的作用域。它通常与 if
、while
等控制语句结合使用,以实现特定条件下的代码执行。局部代码块中的变量仅在该代码块内有效,有助于提高内存利用率。
2. 构造代码块(构造块)
构造代码块是定义在类的构造方法外部的代码块,用于在每个构造方法调用前执行。它的作用是将多个构造方法中重复的代码放在构造代码块里,减少重复代码,提高代码的可读性和重用性。构造代码块在创建对象时会自动执行,每个构造方法都会单独执行一次。
3. 静态代码块
静态代码块是使用 static
关键字修饰的代码块,用于初始化类的静态成员变量或执行类加载时需要的操作。静态代码块只会在类加载到JVM中时执行一次,因此常用于资源加载、初始化等场景。静态代码块优先于主方法执行,并且只能访问静态成员。
4. 同步代码块
同步代码块是在多线程环境下使用的代码块,通过加锁机制确保线程安全。同步代码块通常与 synchronized
关键字结合使用,用于控制多个线程对共享资源的访问。
5. 普通代码块
普通代码块是普通的初始化代码块,没有修饰符,主要用于构造方法中重复的逻辑操作。它在创建对象时与构造代码块类似,但不具有 static
特性。
6. 代码块的特点与使用场景
-
代码块是一种逻辑上的独立区域,不以分号结尾,而是以右花括号
}
结束。 -
代码块可以嵌套使用,但需要注意作用域和生命周期管理。
-
在继承关系中,子类的静态代码块会先于父类的静态代码块执行,而普通代码块则遵循构造方法的调用顺序。