面向对象编程(Object-Oriented Programming,OOP)是一种软件开发的方法论和编程范式,它将现实世界中的实体抽象为对象,通过对象之间的交互来实现程序的设计和开发。
在面向对象编程中,程序的核心思想是将系统看作是一组相互作用的对象集合。每个对象都具有特定的属性(数据)和行为(方法),对象之间通过消息传递进行交互,以实现系统的功能。
HotSpot Java虚拟机的架构图如下。其中我们主要关心的是运行时数据区部分(Runtime Data Area)。
堆(Heap) :此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
栈(Stack) :是指虚拟机栈。虚拟机栈用于存储局部变量等。局部变量表存放了编译期可知长度的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,是对象在堆内存的首地址)。 方法执行完,自动释放。
方法区(Method Area) :用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
说明:
- 堆:凡是new出来的结构(对象、数组)都放在堆空间中。
- 对象的属性存放在堆空间中。
- 创建一个类的多个对象(比如p1、p2),则每个对象都拥有当前类的一套"副本"(即属性)。当通过一个对象修改其属性时,不会影响其它对象此属性的值。
- 当声明一个新的变量使用现有的对象进行赋值时(比如p3 = p1),此时并没有在堆空间中创建新的对象。而是两个变量共同指向了堆空间中同一个对象。当通过一个对象修改属性时,会影响另外一个对象对此属性的调用。
内存区域 | 描述 |
---|---|
方法区(Method Area) | 存储类的结构信息、静态变量、常量等数据的区域,也称为永久代(Permanent Generation)(旧版本)。 |
堆(Heap) | 存储对象实例的区域,包括新生代和老年代。 |
栈(Stack) | 线程的栈空间,存储线程执行方法时的局部变量、方法参数、方法调用和返回等信息。 |
本地方法栈(Native Method Stack) | 用于执行本地方法的栈空间,存储本地方法的局部变量和操作。 |
PC寄存器(Program Counter Register) | 存储当前线程执行的字节码指令地址。 |
堆外内存(Off-Heap) | 在Java堆之外分配的内存,用于特殊需求的数据存储,如直接内存(Direct Memory)。 |
对象名中存储的是什么呢?
答:对象地址。类、数组都是引用数据类型,引用数据类型的变量中存储的是对象的地址,或者说指向堆中对象的首地址。
需要注意的是,局部变量在使用之前必须先进行初始化,而成员变量有默认值,如果没有显式初始化,将具有其对应类型的默认值。
对比点 | 成员变量(实例变量) | 局部变量 |
---|---|---|
声明位置和方式 | 在类中,方法外 | 在方法体{}中或方法的形参列表、代码块中 |
存储位置 | 堆 | 栈 |
生命周期 | 随着对象的生命周期存在 | 随着方法调用的生命周期存在 |
作用域 | 可通过对象访问,本类中直接调用,其他类中使用"对象.实例变量" | 限定在其声明的方法体或代码块中,出了作用域就不能使用 |
修饰符 | public、protected、private、final、volatile、transient等 | final |
默认值 | 有默认值 | 没有默认值,需要手动初始化。形参由实参初始化。 |
权限修饰符的可见性是按照包(package)来划分的,同一包中的类可以互相访问默认(default)和protected级别的成员,而private级别的成员仅对本类可见。public级别的成员对所有类可见,无论是否在同一包中。
以下是Java中权限修饰符对成员变量和方法的修饰的表格展示:
权限修饰符 | 类内部 | 同一包内 | 子类 | 其他包 |
---|---|---|---|---|
public | ✔ | ✔ | ✔ | ✔ |
protected | ✔ | ✔ | ✔ | ✘ |
默认(无修饰符) | ✔ | ✔ | ✘ | ✘ |
private | ✔ | ✘ | ✘ | ✘ |
需要注意的是:
以下是对每个权限修饰符的说明:
权限修饰符是通过编译器和Java虚拟机(JVM)共同实现的。编译器在编译源代码时会根据权限修饰符的规则进行检查和限制,然后将相关信息写入编译后的字节码文件中。JVM在执行字节码时会根据字节码中的权限修饰符信息来进行访问控制。
在JVM执行字节码时,会使用访问控制规则来实现权限修饰符的功能。JVM会根据权限修饰符来判断是否允许访问某个类的字段或方法。如果访问违反了权限修饰符的规则,JVM会抛出相应的访问权限异常(如IllegalAccessError
)。
权限修饰符只提供了源代码级别的访问控制,而在运行时,通过反射等手段仍然可以绕过权限修饰符来访问和修改对象的字段和方法。
绕过方式 | 描述 |
---|---|
反射(Reflection) | 通过java.lang.reflect 包中的类和方法,在运行时获取和操作类的字段、方法和构造函数,绕过权限修饰符的限制。 |
反序列化(Deserialization) | 在对象被反序列化时,私有字段和方法的访问修饰符被忽略,可以绕过权限修饰符的限制访问和修改对象的状态。 |
内部类访问外部类的私有成员 | 内部类可以访问外部类的私有成员,包括私有字段和私有方法。通过在外部类的内部类中调用外部类的私有成员,可以绕过权限修饰符的限制。 |
继承关系 | 子类可以访问父类的受保护(protected)成员,即使子类和父类不在同一包中。这也可以看作是一种绕过权限修饰符的方式。 |
其他方式 | 还有其他一些特殊情况或技术可以绕过权限修饰符,例如使用本地方法(native methods)、使用特定的编译器选项或编译工具等。这些方式具体取决于环境和上下文。 |
需要注意的是,静态变量和静态方法属于类级别的,而不是实例级别的。静态变量在内存中只有一份拷贝,被所有的对象共享。静态方法可以通过类名直接调用,无需创建对象。
静态成员变量和方法的主要特点是它们与类的实例无关,可以直接通过类名访问,且对所有的对象都是共享的。
静态修饰符 | 成员变量(静态变量) | 方法(静态方法) |
---|---|---|
定义方式 | 在类中,方法外定义。 | 在类中定义,使用关键字static 修饰。 |
存储位置 | 存储在静态存储区,即方法区(类的静态区域)。 | 存储在静态存储区,即方法区(类的静态区域)。 |
初始化时机 | 类加载时进行初始化,仅初始化一次。 | 类加载时进行初始化,仅初始化一次。 |
初始化顺序 | 静态变量按照定义的顺序进行初始化。 | 静态方法可以随时调用,但在使用前需要确保类已加载和静态变量已初始化。 |
访问方式 | 可通过类名直接访问,也可以通过对象访问。 | 可通过类名直接调用,也可以通过对象调用。 |
对象依赖性 | 静态变量不依赖于特定的对象,对所有对象共享。 | 静态方法不依赖于特定的对象,不可直接访问实例变量和实例方法,只能访问静态变量和静态方法。 |
类变量和实例变量区别 | 所有对象共享相同的静态变量。 | 每个对象都有自己的实例变量,各个对象之间的实例变量相互独立。 |
生命周期 | **从类加载到类卸载期间存在。**静态字段的生命周期与类的生命周期一致,即使没有创建类的实例,静态字段仍然存在。 | **从类加载到类卸载期间存在。**静态字段的生命周期与类的生命周期一致,即使没有创建类的实例,静态字段仍然存在。 |
可见性 | 可以使用不同的访问修饰符(public、protected、default、private)限制访问。 | 可以使用不同的访问修饰符(public、protected、default、private)限制访问。 |
静态上下文 | 在静态上下文中,只能直接引用其他静态成员变量和静态方法。 | 在静态上下文中,只能直接引用其他静态成员变量和静态方法。 |
使用静态修饰符的成员仍然可以通过一些方式被绕过或修改,**这可能导致不可预测的行为或安全问题。**下面是一些可能影响静态成员的情况:
情况 | 描述 |
---|---|
反射(Reflection) | 通过java.lang.reflect 包中的类和方法,在运行时获取和操作类的静态字段和静态方法,即使它们被声明为私有(private)或受保护(protected)。此操作可以绕过静态修饰符的限制。 |
类加载器(ClassLoader) | 通过自定义类加载器加载和修改类的字节码,包括静态字段和静态方法。这可以用于修改静态成员的值或替换静态方法的实现。 |
多线程访问(Multithreading) | 当多个线程同时访问静态成员时,需要考虑线程安全性。如果没有适当的同步机制,可能会导致竞态条件或数据不一致的问题。 |
其他情况(例如安全漏洞或编程错误) | 在某些情况下,可能会发现静态成员的安全漏洞或编程错误,导致不可预测的行为发生。这可能包括未经授权的修改、访问或共享静态成员的敏感数据等。请谨慎处理静态成员的使用。 |
代码块类型 | 定义位置 | 执行时机 | 主要用途 |
---|---|---|---|
普通代码块 | 方法内部、构造方法内部、类内部(不在方法内) | 调用所在方法或创建对象时执行 | 限制变量的作用范围、执行一段特定的代码逻辑 |
静态代码块 | 类内部,使用static 关键字定义 |
类加载时执行,仅初始化一次 | 类的静态成员变量的初始化、执行一段需要全局初始化的代码逻辑 |
同步代码块 | 使用synchronized 关键字包围的代码块 |
线程获取锁时执行 | 控制多线程并发访问的同步性,避免竞态条件 |
实例初始化块 | 类内部,不带任何关键字直接定义 | 创建对象时执行,每次创建对象都会执行 | 初始化实例成员变量的共同代码部分,提供给多个构造方法共享的初始化逻辑 |
静态初始化块 | 类内部,使用static 关键字和{} 包围的代码块 |
类加载时执行,仅初始化一次 | 初始化静态成员变量的共同代码部分,提供给多个静态成员初始化的共享逻辑 |
潜在问题 | 描述 |
---|---|
变量作用域混淆 | 在代码块中声明的变量具有局部作用域,可能与外部作用域中的变量同名,导致变量作用域混淆和意外行为。 |
隐式对象创建 | 在代码块中创建对象时,需要注意对象的生命周期和资源管理,避免资源泄漏或内存泄漏的问题。 |
死锁风险 | 如果代码块涉及多个线程,需要注意同步和锁的使用,避免死锁的发生,确保程序的正常执行。 |
代码块嵌套过深 | 过多嵌套的代码块降低代码的可读性和可维护性,建议保持代码块简洁和扁平化的结构。 |
变量生命周期过长 | 代码块中声明的变量生命周期过长会占用过多的内存资源,及时释放不再需要的变量以避免内存消耗。 |
逻辑错误 | 在代码块中编写逻辑时,需要仔细考虑条件和循环的边界条件,以及代码块内部的执行顺序,确保程序的正确行为。 |
可变参数(Varargs) | 描述 |
---|---|
参数的顺序和类型 | 可变参数在参数列表中必须是最后一个参数,且每个方法或函数只能有一个可变参数。如果方法或函数有多个参数,可变参数之前的参数类型和顺序都必须明确指定。 |
与重载方法的关系 | 当方法或函数同时存在可变参数和重载(Overload)的情况时,编译器会根据参数的数量和类型选择最匹配的方法或函数进行调用。如果没有精确匹配的方法或函数,编译器会尝试进行自动类型转换或抛出编译错误。 |
与泛型的结合 | 可变参数可以与泛型一起使用。在定义可变参数时,可以指定泛型的类型。例如,可以声明一个可变参数为List,其中T是泛型类型参数。这样,在调用方法或函数时,可以传递不同类型的List参数给可变参数。 |
方法传递引用类型参数时,复制的是参数的引用值(即内存地址),而不是实际对象本身。传递的引用值指向堆内存中的对象。因此,在方法内部,可以通过参数的引用值访问和修改对象的状态。但是,如果在方法内部重新分配引用或将引用指向新的对象,原始参数的引用值不会受到影响。
类型 | 示例 |
---|---|
自定义类 | Person , Car , Book |
标准库中的类型 | String , ArrayList , HashMap |
数组类型 | int[] , String[] , Person[] |
接口类型 | Runnable , Comparator , List |
泛型类型 | List , Map , Optional |
特征 | 重载 (Overloading) | 重写 (Overriding) |
---|---|---|
定义 | 在同一个类中,使用相同的方法名,但参数列表不同。 | 子类中覆盖父类的方法,方法签名(名称、参数列表)必须相同。 |
发生位置 | 同一个类中的不同方法之间。 | 子类中重写父类的方法。 |
关联关系 | 方法名相同,参数列表不同。 | 子类和父类之间存在继承关系。 |
编译时多态性 | 是 | 是 |
运行时多态性 | 否 | 是 |
方法签名 | 方法名相同,参数列表不同。 | 方法名和参数列表完全相同。 |
返回类型 | 可以相同也可以不同。 | 必须相同。 |
访问修饰符 | 可以相同也可以不同。 | 可以相同也可以更宽松(子类可以扩大访问权限)。 |
异常抛出 | 可以相同也可以不同。 | 可以相同也可以更具体(子类可以抛出更少的异常)。 |
静态/实例方法 | 可以是静态方法或实例方法。 | 可以是实例方法(不能是静态方法)。 |
决定调用的方法 | 编译时根据方法的参数列表静态绑定。 | 运行时根据对象的实际类型动态绑定。 |
用途 | 提供相似功能但参数不同的方法。 | 扩展和修改从父类继承的方法的行为。 |
封装是面向对象编程的一个重要概念,**它指的是将数据和方法封装在类中,并通过访问权限修饰符来控制对这些数据和方法的访问。**通过封装,我们可以隐藏类的内部实现细节,只暴露必要的接口给外部使用。
**封装的核心思想是使用访问权限修饰符(如private、public、protected)来改变变量和方法的引用范围。**使用private关键字修饰的变量和方法只能在类的内部访问和调用,外部代码无法直接访问和修改它们。如果外部代码需要访问和修改private变量,可以提供相应的公开的get和set方法,通过这些方法来间接操作private变量。
封装提供了以下优点:
封装在软件设计中通常与高内聚和低耦合的原则密切相关
构造器(Constructor)是一种特殊类型的方法,用于创建和初始化对象。
凡是类,都有构造器
在Java中,构造器与类同名,没有返回类型,并且在使用new
关键字实例化对象时被调用。构造器在对象创建时被自动调用,用于执行对象的初始化操作。
以下是构造器的关键细节:
命名与定义:构造器的名称必须与类名完全相同。
void
,也不使用static
关键字进行修饰。构造器定义在类的内部,可以有多个构造器,通过参数列表的不同来区分。对象实例化:使用new
关键字调用构造器来创建对象实例。
初始化操作:构造器用于执行对象的初始化操作。
默认构造器:如果没有显式定义构造器,Java会自动提供一个无参的默认构造器。
重载构造器:可以定义多个构造器,它们具有相同的名称但参数列表不同,被称为构造器的重载。
构造器涉及到一些难题,以下是一些常见的问题:
构造器和普通方法的区别:
主要区别在于构造器用于对象的初始化,没有返回类型,而普通方法用于对象的操作和行为,有返回类型。
命名和调用方式:构造器的名称必须与类名完全相同,而普通方法可以有任意合法的方法名。构造器在使用new
关键字实例化对象时自动调用,而普通方法需要通过对象引用调用。
调用方式和时机:构造器在对象创建时被自动调用,用于完成对象的初始化。普通方法可以在对象创建后的任意时刻被调用,用于执行特定的功能或操作。
构造器的重载和继承:
super
关键字,同时也可以定义自己的构造器来满足特定需求。默认构造器的作用和限制:
默认构造器用于提供一个无参的初始化方式,但如果显式定义了其他构造器,系统不会再提供默认构造器。
默认构造器只能执行默认的初始化操作,无法实现自定义的初始化逻辑。
构造器的异常处理:
构造器链和this()
关键字:
this()
关键字用于在构造器内部调用同一类的其他构造器。this:引用当前对象的实例或调用当前对象的构造方法。
super:引用父类的成员变量、成员方法或调用父类的构造方法
super
关键字访问成员时,不仅限于直接父类(基类,超类);如果在爷爷类和当前类中存在同名成员,也可以使用 super
访问爷爷类的成员。此外,如果多个基类中都有同名的成员,使用 super
访问时会遵循就近原则。
super
关键字可以访问爷爷类 A 的同名成员。通过 super
关键字,可以在 C 类中访问 A 类的成员。特点 | this |
super |
---|---|---|
用途 | 引用当前对象的实例或调用当前对象的构造方法 | 引用父类的成员变量、成员方法或调用父类的构造方法 |
表示方式 | this 关键字 |
super 关键字 |
实例方法中的使用 | 可以使用this 关键字引用当前对象的实例 |
可以使用super 关键字引用父类的成员变量、成员方法或调用父类的构造方法 |
构造方法中的使用 | 可以使用this 关键字调用同类中的其他构造方法 |
可以使用super 关键字调用父类的构造方法 |
参数传递 | 可以使用this 关键字将当前对象的引用作为参数传递给其他方法 |
可以使用super 关键字将当前对象的引用作为参数传递给父类的构造方法 |
方法重写时的使用 | 可以使用this 关键字调用当前类中的被重写方法 |
可以使用super 关键字调用父类中的被重写方法 |
字段隐藏 | 可以使用this 关键字引用当前类中的字段 |
可以使用super 关键字引用父类中被子类字段隐藏的同名字段 |
内部类中的使用 | 可以使用this 关键字引用当前内部类的实例 |
可以使用super 关键字引用外部类中的成员变量、成员方法或调用外部类的构造方法 |
类B,称为子类、派生类(derived class)、SubClass
类A,称为父类、超类、基类(base class)、SuperClass
通过继承,子类可以获得父类的属性和方法,并可以在子类中添加新的属性和方法。
继承要求子类和父类之间满足"是一个"(is-a)的关系,即子类可以被看作是父类的一种特殊类型。通过继承,子类可以重用父类的代码,并且可以通过覆盖(重写)父类的方法来实现自己特有的行为。
@Override
注解进行标记。super()
关键字来调用父类的构造方法。Object
类是Java中的根类,所有类都直接或间接继承自Object
类。final
关键字可以用于类或方法,表示禁止继承或重写。继承的出现减少了代码冗余,提高了代码的复用性。
继承的出现,更有利于功能的扩展。
继承的出现让类与类之间产生了is-a
的关系,为多态的使用提供了前提。
is-a
的关系。可见,父类更通用、更一般,子类更具体。instanceof
运算符在实际编程中常用于类型检查和对象引用的安全转换。
作用:检查一个对象是否是某个类或其子类的实例
它通过使用对象的元数据信息来进行类型检查,会遍历对象的元数据信息,沿着继承链向上查找,比较对象的元数据信息与目标类的元数据信息是否匹配。
它可以帮助您在运行时确定对象的类型,以便进行相应的操作或处理。
需要注意的是,instanceof
运算符只能用于引用类型的对象,不能用于基本数据类型。
语法形式为:
object instanceof Class
指的是同一个方法或操作在不同的对象上可以表现出不同的行为。
核心思想是通过使用抽象和动态绑定来实现代码的灵活性和扩展性。
多态形式 | 描述 |
---|---|
继承多态(子类多态)(对象多态) | 通过继承关系实现的多态性。父类的引用可以指向子类的对象,通过父类的引用调用相同的方法时,具体执行的是子类中的方法。 |
接口多态 | 多个类可以实现同一个接口,通过接口类型的引用调用方法时,根据实际对象的类型来动态调用对应的方法。(使用接口创建对象实例) |
编译时多态(方法重载) | 在编译时根据方法的参数类型或个数确定调用哪个方法。编译器会根据方法的签名进行静态绑定,选择合适的方法进行调用。 |
运行时多态(动态绑定) | 在运行时根据对象的实际类型确定调用哪个方法。通过继承和方法重写的机制,可以在运行时根据对象的实际类型来调用相应的方法,实现动态绑定。(父类类型的引用来指向子类的对象)实际执行的是子类中重写的方法,而不是父类中的方法。 |
参数多态(泛型多态) | 通过泛型编程实现的多态性。使用泛型可以编写适用于多种类型的代码,提高代码的重用性和灵活性。参数多态允许在编译时不指定具体的类型,而在使用时根据实际的类型进行类型检查和类型推断。 |
运算符多态(运算符重载) | 通过定义类的成员函数或全局函数来重新定义运算符的行为。不同的操作数类型可以触发不同的运算符重载函数,实现多态性。 |
异常处理多态 | 不同类型的异常可以被捕获和处理,提供不同的错误处理逻辑。 |
构造器多态 | 通过使用不同的构造器来创建对象,根据构造器的参数类型和个数来确定具体的构造逻辑。 |
数组多态 | 通过数组的多态性,可以存储不同类型的对象,并通过数组的引用来访问不同类型的元素。 |
匿名内部类多态 | 通过使用匿名内部类,可以在创建对象的同时定义和实现一个接口或抽象类的方法。这种方式可以在需要临时实现某个接口或抽象类的场景下,实现多态性。 |
方法参数多态和方法返回类型多态 | 方法的参数类型可以使用父类类型,从而接受不同子类的对象作为参数,实现多态性。方法内部可以根据实际传入的对象类型来执行不同的逻辑。**方法的返回类型可以是父类类型,但实际返回的是子类对象。**这样可以使方法返回不同类型的对象,实现多态性。 |
编译时,看左边;运行时,看右边
编译时多态(Compile-time Polymorphism)和运行时多态(Runtime Polymorphism)是Java中的两种不同类型的多态性。
编译时多态(也称为静态多态):
运行时多态(也称为动态多态):
特征 | 编译时多态 (Compile-time Polymorphism) | 运行时多态 (Runtime Polymorphism) |
---|---|---|
定义 | 在编译时根据方法的声明信息确定要调用的具体方法。 | 在运行时根据对象的实际类型确定要调用的具体方法。 |
绑定时机 | 编译阶段 | 运行阶段 |
方法调用决定 | 根据方法的参数列表静态绑定。 | 根据对象的实际类型动态绑定。 |
方法重载 | 是 | 是 |
方法重写 | 不涉及方法重写,仅根据方法签名和参数列表选择方法。 | 通过子类重写父类的方法,调用时根据对象的实际类型选择方法。 |
发生时间 | 编译时 | 运行时 |
实现机制 | 静态绑定 | 动态绑定 |
关键特征 | 方法重载 | 方法重写 |
适用场景 | 在编译时根据方法的参数列表选择适当的方法。 | 在运行时根据对象的实际类型选择适当的方法。 |
示例 | 方法重载(多个同名方法,参数不同) | 方法重写(子类重写父类的方法) |
它允许以父类类型的引用来引用子类对象,并根据实际运行时的对象类型来调用相应的方法。
编译时,看左边;运行时,看右边。
注意使用前提:
**使用父类做方法的形参,是多态使用最多的场合。**即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。
开闭原则:
它允许通过父类引用调用子类对象的方法,实现动态绑定(Dynamic Binding)和运行时多态性(Runtime Polymorphism)。
当使用父类引用变量引用子类对象时,如果通过该引用变量调用的方法是虚方法,并且子类重写了该方法,那么在运行时会根据子类的实际类型来确定要执行的方法。
通过虚表和虚表指针的机制,虚方法调用实现了多态性和动态绑定。它使得程序能够根据对象的实际类型来确定要执行的方法,提供了灵活性和可扩展性。
静态链接(或早期绑定)
动态链接(或晚期绑定)