Java中的一些概念
# Java中的一些概念
# 三大特性
Java的三大特性 面向对象三大特性:封装、继承、多态。 关于封装,可能脑子里立刻就会想到getter 和 setter。在上学时或者一些教材中也说了,类的属性如果要访问,不能直接 Object.field通过这种方式。
而是要——使用访问器方法(getter 和 setter)的主要好处之一就是封装性,它可以隐藏类的内部实现细节。通过封装,可以保护类的内部状态,防止外部代码直接修改这些状态
,从而确保类的行为符合设计意图。
但实际开发中,对于类中的 getter 和 setter,往往都是直接赋值跟获取值,似乎跟 Object.field这种方式没什么差别,导致部分人忘记——可以保护类的内部状态,防止外部代码直接修改这些状态
,这段话的含义。那接下来就用一个例子来展示下:
# 例子:银行账户类
假设我们有一个 BankAccount
类,它有三个字段:accountNumber
(账号)、balance
(余额)和 ownerName
(账户持有人姓名)。我们希望限制对这些字段的直接访问,并在设置和获取这些字段时添加一些逻辑。
# 不使用访问器的方法
如果我们不使用访问器方法,而是直接暴露字段,那么代码可能是这样的:
public class BankAccount {
public String accountNumber;
public double balance;
public String ownerName;
public BankAccount(String accountNumber, String ownerName) {
this.accountNumber = accountNumber;
this.ownerName = ownerName;
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount("123456789", "Alice");
account.balance = -100; // 这样直接修改 balance 是不安全的
System.out.println(account.balance); // 输出 -100
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在这个例子中,balance
字段可以被任意修改,即使修改后的值不符合实际(例如,余额不能为负数)。这样直接暴露字段会导致潜在的问题。
# 使用访问器方法
如果我们使用访问器方法(getter 和 setter),那么可以更好地控制对这些字段的访问,并在必要时添加验证逻辑:
public class BankAccount {
private String accountNumber;
private double balance;
private String ownerName;
public BankAccount(String accountNumber, String ownerName) {
this.accountNumber = accountNumber;
this.ownerName = ownerName;
}
public String getAccountNumber() {
return accountNumber;
}
public double getBalance() {
return balance;
}
public String getOwnerName() {
return ownerName;
}
public void setBalance(double balance) {
if (balance < 0) {
throw new IllegalArgumentException("Balance cannot be negative.");
}
this.balance = balance;
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount("123456789", "Alice");
try {
account.setBalance(-100); // 抛出异常
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
System.out.println(account.getBalance()); // 输出 0 或者默认初始值
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 封装性的具体体现
在这个例子中,使用访问器方法带来的封装性体现在:
- 隐藏内部实现细节:外部代码只能通过
getBalance()
和setBalance()
方法访问balance
字段,而不能直接修改它。这样,如果将来需要更改balance
字段的实现方式(例如,添加额外的状态或逻辑),外部代码不需要做出任何改动。 - 验证逻辑:在
setBalance()
方法中,我们可以添加逻辑来验证传入的值是否合法。例如,在上面的例子中,我们检查余额是否为负数,如果是,则抛出异常。这样可以确保账户的余额始终处于合法状态。 - 日志记录和监控:在访问器方法中可以轻松添加日志记录或性能监控的逻辑。例如,在
setBalance()
方法中记录每次余额变更的日志。 - 扩展性:如果未来需要在设置余额时执行额外的操作(如发送通知、记录交易历史等),只需在
setBalance()
方法中添加相应的逻辑即可,而无需修改使用该方法的代码。
通过使用访问器方法,我们可以更好地控制类的状态,并使代码更加健壮和易于维护。
# 一个根对象Object
是否所有的类最终都继承自单一的基类。或者说为什么需要一个Object类,让它是所有类的父类。
在Java中(事实上还包括除C++以外的所有OOP语言),答案是yes,这个终极基类的名字就是Object。事实证明,单根继承结构带来了很多好处。
C++没有唯一的一个父类,这是因为它要适应C模型。
而其它完全的面向对象语言,则不需要,它们有唯一的父类。
单根继承结构使垃圾回收器的实现变得容易得多,而垃圾回收器正是Java相对C++的重要改进之一。由于所有对象都保证具有其类型信息,因此不会因无法确定对象的类型而陷入僵局。这对于系统级操作(如异常处理)显得尤其重要,并且给编程带来了更大的灵活性。
可能还有其他方面需要考虑。比如,Java的反射机制
,当需要动态处理类信息时,所有的类都可以通过Object类
的方法来进行操作,比如getClass()方法返回对象的运行时类,这为反射提供了基础。并不是说,多个根节点就无法实现反射,比如:C++是多个根节点,但因为是多个根节点,所以每个类的方法可能不一致,无法保证每个对象都有类似的方法,因此反射的实现会更复杂,需要额外的机制来追踪类型信息。
此外,多继承还会导致一些问题,比如菱形继承问题,也就是所谓的“钻石问题”。这时候子类如果继承自两个有相同方法的父类,编译器就会不知道应该调用哪个方法,导致歧义。Java为了避免这种复杂性,可能决定采用单继承,但通过接口来实现多继承的部分功能。
# wait跟sleep方法
说到Object,那在问一个问题,为什么sleep()被定义在Thread类,而wait()则被定义在Object方法中。
sleep()是Thread类的一个静态方法,而wait()则是Object类的方法。但是为什么会这样设计呢?
可能的原因包括:sleep()是线程相关的操作,用于让当前线程暂停执行,所以放在Thread类里。而wait()用于线程间的协作,涉及到对象锁的释放,所以属于Object类的方法,这样所有对象都可以作为锁和条件变量来使用。
这也符合Java的监视器机制,每个对象都有一个内部锁和等待队列,所以wait()和notify()需要定义在Object里,让任何对象都能作为同步的基础。
如果不这样设计会有什么问题?
如果wait()放在Thread类里,那么每个线程可能需要管理自己的等待队列,这样会增加复杂性。而放在Object里,锁和等待队列由对象自己管理,更符合对象监视器的设计模式。而sleep()只是让线程休眠,不涉及锁,所以作为线程自身的控制方法放在Thread类里更合理。
# 对象与引用
在Java中,一切都被视为对象,因此可采用单一固定的语法。尽管一切都看作对象,但操纵的标识符实际上是对象的一个“引用”(reference)。可以将这一情形想像成用遥控器(引用)来操纵电视机(对象)。只要握住这个遥控器,就能保持与电视机的连接。当有人想改变频道或者减小音量时,实际操控的是遥控器(引用),再由遥控器来调控电视机(对象)。如果想在房间里四处走走,同时仍能调控电视机,那么只需携带遥控器(引用)而不是电视机(对象)。
此外,即使没有电视机,遥控器亦可独立存在。也就是说,你拥有一个引用,并不一定需要有一个对象与它关联。因此,如果想操纵一个词或句子,则可以创建一个String引用:
String s;
以上代码,创建的只是一个引用,并不是对象。
- 声明引用变量:
String s;
的作用是在栈内存中声明一个类型为String
的引用变量s
。此时:- 栈内存:分配一个固定大小的空间(通常4字节或8字节,取决于JVM),用于存储指向对象的地址。
- 堆内存:没有创建任何对象,
s
的值为null
(未指向任何对象)。
- 对象是否创建:只有通过
new
关键字、字面量赋值或反射等方式才会在堆内存中创建对象。
# 对象存储与堆栈
Java程序中对象创建后存在堆还是栈,以及堆和栈各自存储什么,为什么这样设计?
在Java中,对象实例本身存储在堆(Heap)内存中,而栈(Stack)内存则用于存储基本数据类型变量和对象引用(即指向堆内存的指针)。这种设计是Java内存管理的核心机制,具体原因和细节如下:
# 堆内存(Heap)
- 存储内容:
- 所有对象实例(通过
new
关键字创建的对象)。 - 数组(无论是基本类型数组还是对象数组)。
- 静态变量(存储在堆的方法区中)。
- 所有对象实例(通过
- 特点:
- 生命周期不确定:对象由垃圾回收器(GC)自动回收。
- 线程共享:
所有线程共享堆内存,需考虑线程安全问题
。 - 空间大但访问慢:堆内存容量远大于栈,但访问速度较慢。
# 栈内存(Stack)
- 存储内容:
- 基本数据类型变量(如
int
、double
等)。 - 对象引用(即指向堆内存中对象的指针)。
- 方法调用的上下文(如局部变量表、操作数栈、方法返回地址等)。
- 基本数据类型变量(如
- 特点:
- 生命周期确定:随方法调用结束自动释放(栈帧弹出)。
- 线程私有:
每个线程有自己的栈内存,无需同步
。 - 空间小但访问快:栈内存容量有限(默认几MB),但访问速度快。
简单总结下就是,Java中的对象通常是在堆上分配的,而栈一般用于存放基本类型变量和对象的引用。至于栈,栈的访问速度快,但空间有限,生命周期与方法调用相关;而堆可以动态分配更大的空间,但管理起来更复杂,有垃圾回收机制。
为什么Java这样设计?这涉及到Java的自动内存管理和垃圾回收机制的优势。
# 1.堆内存的用途与设计原因
- 动态分配与灵活生命周期:
- 对象的大小和生命周期通常在运行时才能确定,堆内存支持动态分配(如
new
关键字)。 - 对象可能被多个线程或方法共享,堆内存的全局性支持这种需求。
- 对象的大小和生命周期通常在运行时才能确定,堆内存支持动态分配(如
- 垃圾回收(GC)机制:
- 堆内存的自动垃圾回收简化了内存管理,避免手动释放内存的错误(如C/C++的内存泄漏)。
Java提供了被称为“垃圾回收器”的机制,它可以自动发现对象何时不再被使用,并继而销毁它。垃圾回收器非常有用,因为它减少了所必须考虑的议题和必须编写的代码。
Java的垃圾回收器被设计用来处理内存释放问题(尽管它不包括清理对象的其他方面)。垃圾回收器“知道”对象何时不再被使用,并自动释放对象占用的内存。这一点同所有对象都是继承自单根基类Object以及只能以一种方式创建对象(在堆上创建)这两个特性结合起来,使得用Java编程的过程较之用C++编程要简单得多,所要做出的决策和要克服的障碍也要少得多。
# 2.栈内存的用途与设计原因
- 高效管理方法调用:
- 栈内存的“先进后出”特性天然适合管理方法调用(如递归调用、局部变量隔离)。
- 方法执行完毕后,栈帧自动弹出,释放内存,效率极高。
- 线程安全性:
- 栈内存是线程私有的,不同线程的方法调用互不干扰,无需同步。
以上只是简单的介绍,若要更高效的编写程序还需进一步了解。
# 存储知识扩展
程序运行时,会有各种对象或者数据使用,那它们存储在哪里呢。有五个不同的地方可以存储数据:
- 寄存器。这是最快的存储区,因为它位于不同于其他存储区的地方——处理器内部。但是寄存器的数量极其有限,所以寄存器根据需求进行分配。你不能直接控制。寄存器是中央处理器(CPU)内部的一个存储内容的部件。它们是CPU中最小、最快的存储区域,主要用于暂存数据和指令,以便CPU执行计算和逻辑操作时能够快速访问。寄存器的数量和功能因不同的CPU架构而异
- 栈。位于通用RAM(Random Access Memory,随机访问存储器,通常就是指我们常说的内存器)中,但通过堆栈指针可以从处理器那里获得直接支持。堆栈指针若向下移动,则分配新的内存;若向上移动,则释放那些内存。这是一种快速有效的分配存储方法,仅次于寄存器。
- 堆。一种通用的内存池(也位于RAM区),用于存放所有的Java对象。堆不同于堆栈的好处是:编译器不需要知道存储的数据在堆里存活多长时间。因此,在堆里分配存储有很大的灵活性。当需要一个对象时,只需用new写一行简单的代码,当执行这行代码时,会自动在堆里进行存储分配。当然,为这种灵活性必须要付出相应的代价:用堆进行存储分配和清理可能比用栈进行存储分配需要更多的时间
- 常量存储。常量值通常直接存放在程序代码内部,这样做是安全的,因为它们永远不会被改变。有时,在嵌入式系统中,常量本身会和其他部分隔离开,所以在这种情况下,可以选择将其存放在ROM(只读存储器)中。
ROM(Read-Only Memory,只读存储器)是一种类型的存储器,其特点是数据一旦写入后通常只能读取而不能修改。这种特性使得 ROM 非常适合用于存储那些不需要改变的信息,比如计算机的固件或启动引导程序。
- 非RAM存储。如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序没有运行时也可以存在。其中两个基本的例子是流对象和持久化对象。在流对象中,对象转化成字节流,通常被发送给另一台机器。在“持久化对象”中,对象被存放于磁盘上,因此,即使程序终止,它们仍可以保持自己的状态。
# 基本数据的存储
对象存储在堆中,但Java中还有8种基本类型的数据,它们不是对象,它们又存在哪里呢?
以下是8种基本数据类型:
- char boolean byte
- short int long
- float double
一个字节 byte等于8位,一位可以存储一个,0或1。
这8种基本数据类型,存储位置取决于它们的上下文。
- 局部变量(在方法内部)存储在栈中。
- 成员变量(作为对象的一部分)存储在堆中。
- 静态变量存储在方法区(堆的一部分)。
静态变量(
static
修饰)存储在方法区(JDK 8之前称为永久代,JDK 8后改为元空间,属于堆的逻辑部分),
# 扩展
一段来自《Java编程思想》中的话
OOP(面向对象)和Java也许并不适合所有的人。重要的是要正确评估自己的需求,并决定Java是否能够最好地满足这些需求,还是使用其他编程系统(包括你当前正在使用的)才是更好的选择。如果知道自己的需求在可预见的未来会变得非常特殊化,并且Java可能不能满足你的具体限制,那么就应该去考察其他的选择(我特别推荐读者看看Python,参见www.Python.org)。即使最终仍旧选择Java作为编程语言,至少也要理解还有哪些选项可供选择,并且对为什么选择这个方向要有清楚的认识。