概述
sun 斯坦福大学网络 美国太阳微系统公司
闭源时代, 商业软件公司必备的四大要素: 操作系统 数据库 中间件 编程语言
- MicroSoft: windows SQLserver iis .net
- Oracle: unix Oracle beaweblogic java
开源时代, nginx, Apache基金会, mysql, redis, python, openjdk
c++– 为了简化C++ 主要体现在对象生命周期的自动管理和单继承
java对多线程的支持也比较到位
java的可移植性
跨平台 字节码
JDK
JRE
JVM
以hotspot为例
java文件 -》 class文件 -》 类加载器 classloader
整个jvm中所有线程共享的: 方法区(不同的实现方式: jdk7之前为永久带 jdk8之后为元数据区) 堆内存
每个线程所独有的: 线程栈(虚拟机栈, 线程中java方法的栈) 本地方法栈 程序计数器(cpu的工作就是取值执行, 记录当前执行到的位置)
特定平台实现的执行载体: 执行引擎 本地方法库接口 本地方法库
线程状态
java的线程实现依赖于所处的平台; 例如在linux系统中就是直接调用pthread库; 因此线程的调度不受java控制
JDK中 Thread.State 枚举类定义了 6 种线程状态
新建(New) 线程被创建之后尚未执行的状态
可运行(Runnable) 可运行状态可以被细分为就绪态和运行态; 就绪态是指线程运行所需要的资源(io, 锁)等都已经备齐, 但是由于操作系统尚未调度到该线程(内核级线程 native 方法, 不受java控制), 因此仍处于就绪队列中等待执行; 而运行态就是当前线程成功拿到CPU时间片, 正在执行; 处于可运行状态的线程使用yield方法主动让出时间片之后, 线程状态不变
阻塞(Blocked) 线程等待锁时会进入该状态; 例如线程A在同步
终止(Terminated) 完全终止运行 生命周期结束; 使用stop退出线程可能会导致线程数据不一致(例如: 线程中存在同步代码块, 同步块在执行过程中, 外部线程调用该线程的stop方法, 线程非正常退出); 建议使用interrupt方法退出处于阻塞态的线程, sleep会被中断并抛出异常; 也可以使用标志位, 控制循环退出, 让线程执行完毕, 自动退出 /**
- Thread state for a thread blocked waiting for a monitor lock.
- A thread in the blocked state is waiting for a monitor lock
- to enter a synchronized block/method or
- reenter a synchronized block/method after calling
- {@link Object#wait() Object.wait}. */ BLOCKED, /**
- Thread state for a waiting thread.
- A thread is in the waiting state due to calling one of the
- following methods:
-
- {@link Object#wait() Object.wait} with no timeout
- {@link #join() Thread.join} with no timeout
- {@link LockSupport#park() LockSupport.park}
- </ul> *
-
A thread in the waiting state is waiting for another thread to
- perform a particular action. *
- For example, a thread that has called Object.wait()
- on an object is waiting for another thread to call
- Object.notify() or Object.notifyAll() on
- that object. A thread that has called Thread.join()
- is waiting for a specified thread to terminate. */ WAITING, /**
- Thread state for a waiting thread with a specified waiting time.
- A thread is in the timed waiting state due to calling one of
- the following methods with a specified positive waiting time:
-
- {@link #sleep Thread.sleep}
- {@link Object#wait(long) Object.wait} with timeout
- {@link #join(long) Thread.join} with timeout
- {@link LockSupport#parkNanos LockSupport.parkNanos}
- {@link LockSupport#parkUntil LockSupport.parkUntil}
- </ul>
*/ TIMED_WAITING,
指令重排与内存屏障
哈佛架构: L1缓存是指令和数据分离的, 吞吐量高
普林斯顿架构: 也就是冯诺依曼架构, L2 L3 主内存都是指令和数据一起存放的, 总线压力大
外部存储设备 -> 内存 -> CPU
在中间添加缓存可以显著提升处理效率; 例如内存和外部存储设备之间添加缓存中间件; 现代cpu自带的, 单核独占的L1和L2缓存, 与多核共享的L3缓存; 降低了处理速度差距所带来的等待成本
缓存一致性: MESI协议的实现, 多个cpu读取同样的数据到cache中, 运算之后, 将哪个副本写入内存?
- 修改态 Modified: 此cache已被修改过(脏数据) 内容与主内存中的数据不同 数据为此cache专有
- 专有态 Exclusive: 此cache内容与主内存中的数据相同 但是是当前cache所专有的
- 共享态 Shared: 此cache内容与主内存中的数据相同 同时也出现在其他cache中
- 无效态 Invalid: 此cache内容无效 为空
单个CPU(核心)对缓存中的数据修改之后, 需要通知其他cpu; CPU不仅需要控制自己的读写操作, 还需要监听其他CPU发出的通知, 需要保证最终一致性
指令重排: CPU的性能优化方法之一; CPU在写缓存时, 其他CPU可能也在写, 那就需要等待, 因此为了提高并发效率, CPU会暂时先延后当前需要等待的指令的执行顺序, 先执行后续命令, 除非命令之间有依赖; 一般来讲, 读缓存命令的优先级要高于写缓存命令, 更容易触发指令重排; 但是重排也是要按照一定原则的, 需要遵循as-if-serial语义, 即不能改变原始程序的执行结果; 编译器, runtime和cpu在指令重排或优化时都需要遵守该语义, 因此存在依赖关系的操作会被谨慎处理; 对于分支语句, CPU会进行分支预测优化, 即先蒙一个值,
高速缓存可能存在伪共享问题, 缓存失效; 避免伪共享; 内置Contended避免伪共享
此外, 由于缓存可能是脏的, 不同核心/cpu所看到的同一块内存数据可能是不同的; 而指令重排可能并不符合你的预期; 处理器提供了内存屏障(Memory Barrier)
- 写内存屏障: 在写入指令之后after 插入存储屏障(Store Barrier), 让后续指令暂时不可见, 确保写入数据经由cache更新到内存之后才释放屏障; 避免指令重排
- 读内存屏障: 在读取指令之前before 插入读取屏障(Load Barrier), 强制让当前cache中的数据失效, 从主内存中更新数据之后再从cache中读取; 保证一致性
线程通信
文件共享: 以文件作为中间载体, 多个线程之间使用文件作为缓冲区; 网络共享也是类似的概念; Linux中各种资源都可以被当做文件处理
变量共享: 信号量 锁 消息队列
以生产者-消费者模型为例, 阻塞与唤醒, 一般使用阻塞队列实现;
古老版本中可以使用suspend挂起线程, 然后使用resume唤醒线程, 但是由于suspend不会像wait一样释放当前线程所占有的锁, 因此容易造成死锁; 而且suspend方法必须要在resume方法之前调用, 否则该线程无法被唤醒, 这对方法在jdk 1.2 版本后被弃用了
取而代之的是wait和notify机制, 使用方法类似, 但是这对方法只能有同一对象锁的持有者线程调用, 也就是必须写在同步块里面, 否则会抛出IllegalMonitorStateException异常; 机制详解; 这对方法对调用的顺序也有要求, 否则同样会陷入无法唤醒的状态
而在JDK5引入了JUC库之后, LockSupport类中提供了park和unpark方法; 实现基于01信号量/容量为1的信号量; 或者是我们熟悉的mutex互斥锁; LockSupport.park()会让当前线程挂起; 这对方法的好处在于不用考虑调用的次数与顺序; 但是由于park不会像wait一样自动释放锁, 仍然会出现造成死锁的风险
伪唤醒: wait和park最好不要写在if语句的block中; 检查状态最好使用while循环; 这是底层实现的问题
线程封闭
线程开放是指线程之间对数据的共享, 借助线程通信手段实现数据的交互
而与之相对的线程封闭就是指将数据封闭在各自的线程之中, 不需要同步; 通过ThreadLocal和局部变量实现线程数据的封闭
通过消除竞争条件, ThreadLocal在并发模式下是绝对安全的; 创建变量之后会自动在每个线程中生成独立的副本, 每个副本之间相互独立, 互不影响; 每个线程对该变量进行修改只会影响本线程
1
ThreadLocal<T> value = new ThreadLocal<T>();
JVM会为每个线程维护ThreadLocal的副本
而局部变量则更为简单, 因为每个线程都有独立的虚拟机栈, 当局部变量被调用时会被push到当前线程的栈帧中, 其他线程自然无法访问; 这就是栈封闭
线程池
核心思想是资源复用, 另一个好处是能够控制并发数量
创建时间 + 销毁时间 > 任务执行时间; 这种情况下使用线程是不合算的
此外, 尽管线程本身比进程来说占用的资源要少很多, 但是切换的成本仍然是不可忽略的, 进程切换会刷新CPU缓存, 线程不会, 但是仍要刷新寄存器的值; 线程调度也需要成本
- 线程池管理器: 负责管理线程池的生命周期, 负责向线程池中添加新任务
- 工作线程
- 任务接口
- 任务队列
接口: Executor, ExecutorService, ScheduledExecutorService 实现类: ThreadPoolExecutor, ScheduledThreadPoolExecutor
- newFixedThreadPool(int nThreads): 固定大小, 任务队列容量无界的线程池, 核心线程数=最大线程数
- newCachedThreadPool(): 任务队列容量无界的缓冲线程池, 其中任务队列为同步队列, 线程保持时间为60秒; 核心线程数=0, 最大线程数=Integer.MAX_VALUE; 适用于耗时小的异步任务
- newSingleThreadExecutor(): 保证同时只有一个任务执行, 执行顺序严格按照添加顺序
- newScheduledThreadPool(int corePoolSize): 定时任务线程池, 核心数量=corePoolSize, 最大线程数=Integer.MAX_VALUE
先判断核心线程数量 再判断工作队列: 工作队列无界则最大线程数量无效, 因为工作队列可以一直往里塞新任务 最后判断最大线程数量 都不满足就拒绝 reject
计划任务与延时队列DelayedWorkQueue
- scheduleAtFixedRate 如果任务的执行时间超过预设的等待时间, 则下一个任务会在当前任务执行后立即执行
- scheduleWithFixedDelay 不管任务执行时间多久, 执行完一个任务之后都要按照设置的等待时间等待
线程池的终止: 线程池被终止之后不会接受新的任务, 会拒绝; 以下两种终止API的实现方式不同
- shutdown: 优雅地终止, 队列中残存的任务会被执行
- shutdownNow: 尝试立即终止, 队列中的任务会被销毁并返回队列中任务的数量, 正在运行的线程会被interrupt
内存模型
java为了保证并发编程在多平台上的一致性表现, 设计了内存模型规范
一个jvm本质是一个进程, 在此基础上运行java的字节码; 单独一个jvm无法支持多进程模型, 这其实是一种简化;
在单个jvm进程中, 多个线程共享的内存区域为堆和方法区(永久带, 元数据区)
此外, jvm为每个线程独立地维护线程栈, 本地方法栈和程序计数器
原子性
并发数据争用的解决方案: 临界区争抢, 消息队列
临界区: 多个线程视图抢占进入的区域(代码段)
竞态条件: 临界区内的特殊条件, 例如多个线程读写冲突的变量; 如果一段代码是线程安全的(栈封闭, 局部变量), 则它不可能包含静态条件;
也就是说, 如果一个资源的生命周期能够被单个线程所管理, 且该资源不会被其他线程所调用, 那么该资源就是线程安全的; 或者说, 只有写操作才会导致多线程数据不安全, 因此只读的不可变对象也不会有线程安全问题
非原子性操作例如i++就很容易发生同步读取数据的问题, 线程t1读取值, 并进行处理, 线程t2在t1还没有写回数据时就再次读取值, 读到了过期的脏数据; 这个类似于数据库中事务的脏读, 但又不完全一致; 而volatile关键字只能保证每条语句的可见性, 但在i++中, 其实执行了三条语句, 我们需要将这三条语句视为一个不可拆分的整体; 这就是所谓的原子性
原子操作: 原子操作可以是一个步骤; 也可以是多个步骤, 但是需要保证其执行顺序不可以被打乱, 也不可以被切割而执行其中的一部分, 也就是不可中断性; 原子操作是原子性的核心特征
为了保证竞态条件能够被按照期望的结果执行, 一般需要通过循环CAS或锁(synchronized, ReentrantLock)等方式将其转变为原子操作
CAS(Compare and swap): 比较和交换策略, 是硬件级别的同步原语; 简单来说就是CPU提供了一种根据原始数据版本判断当前线程修改数据时是否有其他线程同时在修改; 例如CAS流程: i的初始值为0, 线程A通过CAS访问i会维护(V:i的内存地址, E:i的原始值, T:处理后的目标值)结构, 在处理完毕得到T之后需要更新V的数据, 在更新前, 需要判断当前V的数据与E是否相同, 相同则更新为T, 否则说明i的数据在线程A的处理过程中被其他线程所修改, 因此需要更新E并重新处理得到T2, 再次循环上述更新步骤, 直到更新完成. 可以看出, CAS本质上是一种乐观并发处理策略(乐观锁), 假定不发生冲突, 在更新数据时判断数据是否发生冲突, 处理方式类似于版本号机制, 我们看一个例子来加深理解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
volatile int i = 0;
static Unsafe unsafe;
private static long valueOffset;
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
valueOffset = unsafe.objectFieldOffset(Demo.class.getDeclaredField("i"));
int current;
do {
current = unsafe.getIntVolatile(this, valueOffset);
} while (!unsafe.compareAndSwapInt(this, valueOffset, current, current + 1));
// 更新失败说明被其他线程影响了, 需要重做业务
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
1
2
3
4
5
6
7
8
9
static inline bool
compareAndSwap (volatile jint *addr, jint old, jint new_val)
{
jboolean result = false;
spinlock lock; // 自旋锁
if ((result = (*addr == old)))
*addr = new_val;
return result;
}
在sun.misc.Unsafe类中提供了大量与CAS有关的本地方法接口, 详见闭源魔法类; 但是这种方式过于麻烦, 一般我们直接使用JUC中提供的原子操作封装类型来实现, 包括但不限于基本数据类型, 数组, 引用, 字段
1
2
3
4
5
AtomicInteger i = new AtomicInteger(0);
public void add() {
i.incrementAndGet();
}
在java8之后提供了专门用于计数(DoubleAdder, LongAdder)和用于更新(类似于Python中的高阶函数reduce)的API(DoubleAccumulator, LongAccumulator); 适用于频繁更新且读取次数相对较少的高并发场景, 关键在于参考了分布式处理思想
LongAdder的实现相比于AtomicInteger要更加巧妙. 由于专门被用于处理加法操作, LongAdder会为每个线程维护一个单独的数据区块, 用于记录每个线程对原数据的增量, 最终要获得累加结果时只需要将这些区块的增量加和即可(调用sum方法).
1
2
3
4
5
6
7
8
LongAdder longAdder = new LongAdder();
Thread {
longAdder.increment();
// longAdder.add(x);
}
longAdder.sum();
LongAdder的这种实现方式基于分布式思想, LongAdder提供一个cell数组. 每个线程只需要维护自己所对应的单元格, 这样做能够有效避免数据读写冲突, 在最终读取结果时只需要将cell中的所有单元格所保存的增量相加即可, 因此速度: LongAdder 千万级别(线程越多性能越好) > AtomicInteger 千万级别 > synchronized 百万级别
但是CAS策略也有一定的缺陷:
- JDK中的JUC中的实现的CAS策略API都是基于自旋策略, 尽管这适用于短时间内频繁获取和释放锁的场景, 但是这种消耗时间片的做法在CAS循环体的执行时间较长的情况下会造成非常严重的CPU资源浪费, 例如等待大IO时, 本来阻塞的BIO会让线程主动放弃时间片进入阻塞态, 但是本质上类似于非阻塞NIO的自旋实现方式会以轮询的方式将宝贵的时间片用于等待IO结果
- CAS的另一个问题是比较和交换策略只适用于更新单个变量, 对多个变量的处理则较为麻烦
- ABA问题,即CAS第一次读取变量时为A,期间被其他线程改为B,然后又被改为A,最后CAS进行验证时发现与第一次读取的数据版本相同,这就导致CAS主观认为数据没有被修改。尽管这并不影响最终的修改结果,但是在某些安全性要求较高的场景下可能会导致意想不到的问题。另一种更危险的情况是当以地址作为CAS判断标准时,变量A的地址X因为GC等原因被替换为新的变量B,而CAS更新时发现地址X仍然存在数据就使用变更后的A替换了B,导致B数据丢失。
JUC中提供了AtomicStampedReference类给数据添加时间戳标签,但是由于ABA问题几乎不会影响最终的执行结果,导致这个类很少被使用。
可见性
内存模型决定了在程序执行的每个timing时可以读取的值, 或者说, 内存模型规定了变量的可见性
jvm的jit优化有时会自动进行指令重排与常亮折叠, 这会导致一些违背程序设计逻辑的结果
1
2
3
4
5
6
7
8
9
10
public static boolean flag = true;
Thread1{
while (flag){
do some thing
}
}
this.flag = false; // 正常应该终止
这可能是因为CPU缓存没有及时更新而导致的数据延迟; 但缓存延迟不会让结果一直错误, 而jit的指令重排则可能造成更严重的后果. 但是如果启动jvm时添加了-server选项, 那么jvm会进行jit优化, 由于while每次循环都要访问一个变量, jit就将指令重排为
1
2
3
4
5
if (flag){
while (true){
do some thing
}
}
这样, 在某个线程修改flag会导致其他线程不可见
对于同步的规则定义: 同步意味着以下代码严格保证可见性; 从表现上来看就是保证了代码的执行顺序, 但是实际可能并非如此, 乱序的代码只要保证了可见性也可以达到同步的效果
- 监视器m的解锁与所有后续操作对于m的加锁同步
- 对volatile变量v的写入与所有其他线程后续对v的读同步
- 启动线程的操作与线程中的第一个操作同步
- 对于每个属性写入默认值(0, false, null)与每个线程对其进行的操作同步
- 线程T1的最后操作与线程T2发现线程T1已经结束同步(isAlive, join判断线程是否终结)
- 如果线程T1中断了T2(通过抛出InterruptedException异常), 那么线程T1的中断操作(Thread.interrupt)与其他所有线程发现T2倍中断了同步(Thread.isInterrupted)
先行发生原则(Happens-before): 用于强调两个有冲突的动作之间的顺序, 以及定义数据争用的发生时机; 先行发生原则是一种内存模型规范, 每种JVM自行实现, 在符合内存模型的JVM上执行的java程序的并发过程是可以预测的, 至少能够保证可见性原则
volatile功能: 禁止缓存, 禁止重排序 final功能:
字分裂(word tearing): 字节被多个线程同时修改, 导致字节出现数据错误; 现代处理器几乎都支持以byte为单位处理, 因此很难出现这个问题
double和long的特殊处理: 类似字分裂问题, 如果处理器一次只能操作32位, 那么以64位存储的double和long则可能出现分裂问题, 即一个线程修改前32位, 另一个线程修改后32位; jvm保证通过volatile修饰的变量不会出现分裂问题
有序性
基本语法
代码规范
java13 新添加的yield关键字 可能是为了实现协程
保留字: 截止java14仍未被使用的关键字, 但在自定义变量名时需要注意避开. 目前只有goto和const
标识符: 也就是类名、变量名、方法名、接口名、包名等; 满足正则[a-Z_$][a-z0-9_$]*; 不能是关键字或保留字; 不能有空格; 不建议以下划线或美元符号开头
数据类型
基本数据类型: 为了方便,
- 整数类型: 一个字节=八个比特位; byte(8位=8个bit) short(16位) int(32位) long(64位); 注意, 没有无符号类型; java提供了统一的内存模型, 因此上述类型的位数与平台无关, 这要跟c++做区分
- 浮点类型: float(32位) double(64位)
- 字符型: char(16位)
- 布尔型: boolean
自动类型提升: 低位数类型与高位数类型运算时会自动进行类型提升, 但最终的结果必须用高位数类型接收, 或者用强制类型转换将结果转换为其他类型, 但要注意处理溢出; 特别的, 当byte,char,short三种类型两两做运算时, 结果会自动转换为int类型; 注意: 整数默认为int类型, 浮点数默认为double类型
1
2
3
4
5
6
byte b = 2;
b = b + 1; // 编译报错 因为数字 1 默认为 int 类型
b += 1; // 自操作会执行隐式强转
float f = b + 2.13; // 编译报错 因为浮点数默认为 double 类型
float f = b + 2.13f; // 可以直接声明为 float 类型
引用数据类型: 万物皆对象, 栈里存变量名 指向堆中的某块内存
- 类: 典型的如字符串类型, 高精度类型BigInteger, BigDecimal
- 接口:
- 数组:
1
2
3
4
5
6
7
8
9
10
11
12
String str0 = "abc";
try {
Field chr = String.class.getDeclaredField("value");
chr.setAccessible(true);
char[] data = (char[]) chr.get(str0);
data[1] = 'd';
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
System.out.println(str0);
String的用法, StringBuilder和StringBuffer
语法细节
if/else的匹配按照就近原则, 这一点要和python语言区分, 因为Python是严格按照格式判断代码块的
1
2
3
4
5
6
7
8
9
int x = 3, y = 1;
if (x > 2)
if (y > 4) // 下两个代码块都匹配这个 if
System.out.println("不会输出这行");
else if (y > 2) // 匹配最近的 if
System.out.println("也不会输出这行");
else // 匹配最近的 if
System.out.println("会输出这行");
在一定区间内取随机整数的通用方法
1
int rand = (int)(Math.random() * (end - start + 1) + start);
++a是先自加后操作,a++是先操作后自加, 操作包括但不限于赋值、打印、参与运算等等,
位运算 16进制转换
跳出多重循环 break+标签; break默认只退出最近的包裹循环, 需要跳出多重循环需要使用标签; continue也有类似的用法, 通过添加标签可以结束指定循环结构的当次循环
1
switch-case 可以使用的变量类型只有6种 byte short char int String(Java 7开始) enum(Java 5开始); 尤其要记住不支持布尔型boolean和长整型long和浮点数float以及double
switch一般被用于区间匹配: 判断当前月份所属的季节, 判断当前日期为全年的第几天
数组是对象, 栈中存储数组的首地址, 堆中每个数组下标对应的内存存储的是实际元素的引用(地址), 这意味着寻找元素需要二次寻址;
为了方便数组的使用, jdk提供了java.util.Arrays工具类, 默认使用DualPivotQuicksort类的sort方法来排序, 内部实现包括双轴快速排序,还使用了TimSort、插入排序、成对插入排序、3-way快速排序.
1
2
3
4
5
6
7
8
9
int[] x = {5, 1, 3, 2, 8};
Arrays.sort(x);
System.out.println(Arrays.binarySearch(x, 2));
char[] y = new char[5];
Arrays.fill(y, 'a');
System.out.println(Arrays.toString(x));
System.out.println(Arrays.toString(y));
基本数据类型:比较的是==两边值是否相等 引用数据类型:比较的是==两边内存地址是否相等
基本数据类型在放到hashmap等结构中时会自动装箱为对象类型
简而言之, java默认equals相等的两个对象hashcode也相同, 而hashcode相同的两个对象equals却不一定相同; 因为在object类中, equals比较的是地址, hashcode是将地址传入本地方法并返回int类型;
Arrays重写了equals和hashcode方法, 但数组对象本身却没有, 要注意区别; 对引用数据类型来说, equals默认等于==, 都是对对象的地址作比较, 也就是判断两个对象是否指向了同一段内存地址; 而String这样的类重写了equals, 比较的是值是否相同abc.equals(abc); 为了防止在hash结构中出现迷惑的冲突现象, 重写equals方法之后必须重写hashcode方法, 因为abc和abc两个字符串可能是不同的String对象,
面向对象
面向对象是为了提高程序可读性, 提供更清晰的结构以及更高的开发效率; 代价是性能会随着包装层数的增加而被严重剥削
类是抽象的概念, 而对象是一个实例
Field 字段 成员变量; 带有getter和setter方法的字段一般被称为属性property
Method 函数 成员方法
方法的修饰符: static public final
类中成员的修饰符: public protected default(默认, 包内可访问, 子类不可访问) private
JDK 5 之后支持变长参数; 但要注意处理空值; 同一个函数最多只能有一个可变形参
形参: 函数定义的参数
实参: 实际传递给函数的参数
java中 方法传递参数只有值传递一种形式 即传递原有数据的副本; 基本数据类型就直接传递值, 不能实现传递修改, 因此swap函数的实现只能依靠数组; 引用数据类型就传递对应地址的值(传的值就是地址, 也就是栈上引用的复制), 因此可以实现传递修改; 牢记 stack -> heap
匿名对象: 没有显式指定名称, 野对象, 可以当做参数被传递, 一次性使用
封装
又称为信息隐藏,是将事物的属性和行为归到一个类中,以方便使用,同时避免干扰。封装将事物相关的数据保护起来,只有通过给定的安全接口才可以对事物中的数据进行操作。保证了信息安全,提供了统一调用
为什么封装 隐藏内部实现的复杂性, 暴露该暴露的, 隐藏该隐藏的
实现封装的关键就是对类中元素访问权限的限制 最小权限原则
Java修饰符 public > protected > default > private
为什么不能用private修饰Java外部类? 因为如果使用private修饰Java外部类,那么这个类不能创建实例,这个类的属性和方法不能被访问,那么创建这个类毫无意义,所以不能使用private修饰Java外部类。
为什么不能用protected修饰Java外部类? 举个栗子,如果类A用protected修饰,与类A不同包的类B想要访问类A的话,类B就必须是继承类A的(或者说类B必须为类A的子类),但是类B继承类A的前提又是类B可以访问到类A,仔细想想会发现这里是冲突的,其实这就说明了为什么不能用protected来修饰外部类。再说,protected是用来表示在继承关系中的访问权限的,在同一个包和子类中都可以访问,因为继承就是为了拥有父类的属性和方法,所以protected是用于修饰类的方法和属性的,也就是说,我想要这个类的属性和方法可以被任何子类继承,我就用protected。我想要这个类的属性和方法不能被任何子类继承,我就用private。同理,我想要这个类被继承,我就用abstract。我不想这个类被继承,我就用final。所以,用protected修饰类有什么意义呢?关键点还是在于第一句话,protected是用来表示在继承关系中的访问权限的!
封装的思想并不是面向对象专属的, 操作系统中的系统调用函数就是对系统所提供的复杂功能的封装
典型的: private字段通过public的get和set方法实现有限制的访问, 保护了内部字段
抽象
将一类事物的公共特性提前出来,封装在一个抽象类中。
继承
个性对共性的属性和行为的接受,同时保留个性的属性和行为。通过共性对个性进行归类,同时又不失个性。
不存在多继承
C3继承算法
多态
对不同的事物发出同一个消息,表现出不同的行为。多态通常建立在继承上,即不同的事物是有同一个基类的,子类对父类的方法进行了不同的实现,表现即为多态。
编译时多态(即方法重载)、运行时多态(运行时根据实例决定调用哪个方法,通常的多态指的就是运行时多态)
利用多态实现动态绑定技术
条件:1.有继承;2.有重写;3.父类引用指向子类 特点:1.该引用只能调用父类中有的方法;2.态连接、动态调用);3.变量不能被重写(覆盖),重写只针对方法。 类型转换: 1.向上类型转换:子类转换为父类。不需要显示指出 2.为父类的引用指向子类。 不需要重启服务器就可以实现扩展。 优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)
方法的重载(Overload): 在同一个类中的同名方法 但是 参数数量或对应位置的类型不同; 例如Arrays中提供的sort和binarySearch方法, 都重载各种数据类型; 注意 与修饰符(权限 返回值)以及抛出的异常无关;
1
2
3
4
void show(int x, double y){} // 原始
void show(double x, int y){} // Yes
int show(int x, double y){ return 0; } // No
方法的重写(Override): 子类重写父类的方法 方法名 参数类型 参数个数 返回类型 都必须相同; 访问修饰符一定要大于等于被重写方法的访问修饰符(public>protected>default>private)
混淆
throw/throws overload/override final/finally/finalize collection/collections String/StringBuilder/StringBuffer
sleep() wait() 抽象类和接口