Java知识
值传递和引用传递
- 实际上只有值传递,传递值的副本
- 引用传递是传递地址作为值的副本,所以修改成员会影响原来的对象
- 基本类型值传递;引用类型(对象,数组等)引用传递
int 和 Integer
int 是基本数据类型,直接计算
Integer 是 Integer 对象,用各种方法Integer.parseInt(String s)将字符串转为 int(基本类型)Integer.max(int a, int b)返回较大值Integer.toString(a)转换成字符串
- List
等泛型集合里面只能放引用类型(对象类型),不能放基本类型 - 因为 Java 泛型编译后会变成 Object 类型,而 Object 不能接受基本类型
- 历史原因,Java 设计之初严格区分基本类型和引用类型,泛型是后期才引入的,为了兼容已有的类型系统,选择只支持引用类型
解决办法:用基本类型的包装类就行(Integer,Character, Boolean,Double)
1 | |
Java 类中的静态变量和静态方法
在 Java 中,静态变量和静态方法是与类本身关联的
而不是与类的实例关联。它们在内存中只存在一份,可以被类的所有实例共享
Java 里面的引用类型
Java 的所有非基本类型都是 引用类型(对象通过引用访问)
Collection 是 List,Set,Queue 等接口的根接口;Map 是 HashMap 和 TreeMap 等类实现的根接口
| Java 类型 | C++ 对应类型 | JAVA 功能描述 |
|---|---|---|
String |
std::string |
不可变对象(immutable),通过引用操作 |
int[] |
原生数组或std::vector |
数组是对象,带长度属性(arr.length),自动内存管理 |
List<T> (如 ArrayList) |
std::vector<T> |
List是接口,ArrayList是动态数组实现 |
Map<K,V> (如 HashMap) |
std::unordered_map<K,V> |
HashMap基于哈希表,无序 |
Set<T> (如 HashSet) |
std::unordered_set<T> |
HashSet基于哈希表,无序且唯一 |
自定义类(class Person) |
class Person |
对象通过引用访问(类似智能指针) |
Java 引用类型的使用
字符串:String name = "Alice";
动态数组:List<Integer> numbers = new ArrayList<>();
哈希表:Map<String, Integer> map = new HashMap<>();
自定义类:Person person = new Person("Alice", 18);
集合的遍历方式
for 循环
增强 for 循环(for-each 循环)
for (int i : numbers) {}for (var i : numbers) {}// 也可以用 var 相当于 C++的 autoIterator 迭代器
1
2
3
4Iterator<Integer> iterator = numbers.iterator();
while (iterator.hasNext()) {
int i = iterator.next();
}ListIterator 列表迭代器
是迭代器的子类,可以双向访问列表,在迭代中修改元素forEach 方法
list.forEach(element -> System.out.println(element));Stream API
1
2
3list.stream()
.filter(s -> s.length() > 5)
.forEach(System.out::println);
接口的使用
面向接口编程
先定义接口,然后实现接口,调用的时候使用接口接收对象
接口可以实现 java 的多态,接口不是“父类”,但你可以用接口类型的变量来引用实现了该接口的类的对象。这是 Java 多态的一种表现形式。
因为 Java 支持 “向上转型”(Upcasting):
当一个类 实现了某个接口,那么它的对象就可以被 自动视为该接口类型的实例。
接口本身不能被实例化(不能 new 接口),
但实现了该接口的类的对象,可以被当作该接口类型的引用使用。
List<String> list = new ArrayList<>();这里发生了什么?
new ArrayList<>() 创建了一个 具体的对象(实例)。
这个对象的实际类型是 ArrayList。
但由于 ArrayList implements List,Java 允许你用 List 类型的引用去“看”这个对象。
这叫 向上转型(Upcasting) —— 对象从“具体类型”被视作“更通用的接口类型”。
Spring 里面依赖注入的时候就发生了向上转型,所以能调用实现类的方法
首先 Spring 扫描到 @Service,创建一个 UserServiceImpl 的实例。
把这个实例注册为 UserService 类型的 Bean(因为 UserServiceImpl implements UserService)。
@Autowired
private UserService userService; // ← 接口类型!
// Spring 自动给字段赋值
this.userService = applicationContext.getBean(UserService.class);
// 内部等价于:
UserService userService = new UserServiceImpl(); // ← 多态:接口引用指向实现类对象
1. 提高代码灵活性
1 | |
相比于直接使用具体实现类:
1 | |
如果后续需要更换实现,所有使用该变量的地方都可能需要调整。
2. 符合设计原则
- 里氏替换原则:任何基类(接口)出现的地方,子类(实现类)都可以替换
- 依赖倒置原则:高层模块(业务代码)不依赖低层模块(具体实现),都依赖于抽象(接口)
3. 促进团队协作与测试
- 团队协作:前端开发只需查看接口文档了解可用方法,无需等待后端具体实现完成即可开始编写调用逻辑
- 单元测试:可以轻松使用”模拟对象(Mock)”替代真实的数据库操作等复杂依赖,而 Mock 通常基于接口实现
接口复用代码
Java 只有单继承(extends ),但可以实现(implements) 多个接口
从 Java 8 开始接口可以有:
- 默认方法(default):提供默认实现
- 静态方法(static):工具方法
- 私有方法(private):默认辅助方法
用接口复用代码的例子
1 | |
lambda 表达式
Java 8 引入了 lambda 表达式
简化了匿名内部类的写法
(a, b) -> a + b
(a, b) -> {a + b; return a + b;}// 多条语句要用{}
stream 的 API
适合集合对象的操作,如过滤,映射,排序,聚合等
问题场景:从一个列表中筛选出所有长度大于 3 的字符串,并收集到一个新的列表中。
没有 Stream 啲做法:
1 | |
使用 Stream API 的做法:
1 | |
originalList.stream():创建一个流,该流包含原始列表中的所有元素。.filter(s -> s.length() > 3):对流进行过滤,只保留长度大于 3 的字符串。.collect(Collectors.toList()):将过滤后的结果收集到一个新的列表中。
其他案例:字符串统一排序
map(s -> s.toUpperCase()):将每个字符串转换为大写。.sorted():对字符串进行排序。.collect(Collectors.joining(", ")):将排序后的字符串连接成一个字符串,并使用逗号(,)作为分隔符。
计算数字列表的和
numbers.stream():创建一个流,该流包含原始列表中的所有数字。.mapToInt(Integer::intValue):将数字映射为 int 类型。.sum():计算数字的和。
cpu 密集的运算用 Stream 并行
I/O 密集的运算用 Stream 串行
终端操作
终端操作是流管道(stream pipeline)中的最后一个操作,是触发流管道执行的操作。
一旦调用,流就被 消费(consumed),不能再被使用。
与中间操作(如 filter, map)不同,终端操作 不返回 Stream,而是返回
所有 Java stream API 的终端操作
| 类别 | 方法 | 功能说明 | 返回类型 | 是否短路 |
|---|---|---|---|---|
| 遍历 / 副作用 | forEach(action) |
对每个元素执行操作(并行流中无序) | void |
否 |
forEachOrdered(action) |
按照流的原始顺序执行操作(即使并行流也保序) | void |
否 | |
| 收集结果 | collect(Collectors.toList())(典型用法) |
将流元素收集到 List(也可 toSet(), joining() 等) |
R(如 List<T>) |
否 |
toArray() |
转为 Object[] 数组 |
Object[] |
否 | |
toArray(String[]::new)(示例) |
转为指定类型数组(如 String[]) |
T[] |
否 | |
| 归约 | reduce(初始值, 累加器) |
从初始值开始,依次合并元素(如字符串拼接、求和) | T |
否 |
reduce(累加器) |
无初始值归约,流为空时返回 Optional.empty() |
Optional<T> |
否 | |
| 查找与匹配 | findFirst() |
返回第一个元素(适合有序流) | Optional<T> |
✅ 是 |
findAny() |
返回任意一个元素(适合并行流,更快) | Optional<T> |
✅ 是 | |
anyMatch(pred) |
是否存在至少一个元素满足条件 | boolean |
✅ 是 | |
allMatch(pred) |
是否所有元素都满足条件 | boolean |
✅ 是(遇到 false 即停) | |
noneMatch(pred) |
是否没有元素满足条件 | boolean |
✅ 是(遇到 true 即停) | |
min(comparator) |
返回最小元素(按比较器) | Optional<T> |
否 | |
max(comparator) |
返回最大元素 | Optional<T> |
否 | |
| 计数 | count() |
返回流中元素总数 | long |
否 |
中间操作
| 类别 | 方法 | 功能说明 | 返回类型 |
|---|---|---|---|
| 过滤 | filter(Predicate<? super T> predicate) |
保留满足条件的元素 | Stream<T> |
| 映射 / 转换 | map(Function<? super T, ? extends R> mapper) |
将每个元素转换为另一种类型 | Stream<R> |
flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) |
将每个元素映射为一个流,然后扁平化合并为一个流 | Stream<R> |
|
mapToInt(ToIntFunction<? super T> mapper) |
转为 IntStream(用于基本类型优化) |
IntStream |
|
mapToLong(...) / mapToDouble(...) |
类似,分别转为 LongStream / DoubleStream |
LongStream / DoubleStream |
|
| 排序 | sorted() |
按自然顺序排序(要求元素实现 Comparable) |
Stream<T> |
sorted(Comparator<? super T> comparator) |
按指定比较器排序 | Stream<T> |
|
| 去重 | distinct() |
去除重复元素(基于 equals()) |
Stream<T> |
| 截取 | limit(long maxSize) |
保留前 maxSize 个元素 |
Stream<T> |
skip(long n) |
跳过前 n 个元素 | Stream<T> |
|
| 查看 / 调试 | peek(Consumer<? super T> action) |
对每个元素执行操作(常用于调试),不影响流本身 | Stream<T> |
| 并行 / 串行控制 | parallel() |
将流转换为并行流 | Stream<T> |
sequential() |
将流转换为顺序流 | Stream<T> |
|
unordered() |
忽略流的顺序性(可能提升并行性能) | Stream<T> |
责任链模式
一个请求需要多个处理逻辑时的灵活做法
比如各种校验逻辑
先定义一个抽象处理者类
1 | |
然后每个校验逻辑都继承这个类,比如登录逻辑
1 | |
然后再写其他节点,最后根据需要动态添加节点
1 | |
这样一来,发起方只需要调用第一个节点,不用关心后面有多少校验步骤
如果某个接口不需要某个校验就直接去掉这个节点就行,非常灵活
List 实现线程安全的方式
不同步的后果
如果两个线程同时执行到第 1 步,它们会读到相同的值(比如都是 100)。然后各自加 1,最后都写回 101。结果应该是 102,但因为两个线程覆盖了彼此的写入,最终只增加了 1。
synchronized 同步方式
使用 synchronized 关键字修饰方法
读写都会加锁,适合读写均衡的场景
1 | |
CopyOnWrite 同步方式
读无锁,写会复制新的数组,适合读多写少的场景
1 | |
Map 的遍历
for-each 循环和 entrySet()方法for (Map.Entry<K, V> entry : map.entrySet()) { entry.getKey(); entry.getValue(); }
for-each 循环和 keySet()方法for (String key : map.keySet()) { key; map.get(key); }
用迭代器遍历
1 | |
用 lambda 表达式和 forEach()方法遍历map.forEach((key, value) -> { key; value; });
使用 stream 流,还可以进行过滤和映射等map.entrySet().stream().forEach(entry -> { entry.getKey(); entry.getValue(); });
Map 实现线程安全
- HashMap 线程不安全,效率高一点
- ConcurrentHashMap 是 java 中一个线程安全的哈希表实现类,适合多线程并发地读写操作,实现原理是分段锁和 CAS(Compare And Swap 比较并替换),将整个哈希表分成了多个 Segment(段),每段都有自己独立的锁,读不需要锁,写锁定对应的 Segment 大大提高并发性能
- HashTable 基本被淘汰了,他线程安全,通过将内部方法都加 synchronized 关键字,读写都加锁,同一时刻只能有一个线程访问,效率较低
Java 的线程安全
- volatile 关键字修饰成员变量,静态变量,可以确保变量可见性
即当一个线程修改了 volatile 变量的值,其他线程能够立即看到这个修改 - synchronized 关键字修饰实例方法,静态方法,代码块,可以确保原子性和有序性
提供同步机制,确保同一时刻只有一个线程能执行被 synchronized 修饰的代码块或方法,从而保证操作的原子性和有序性。
concurrent 是并发的意思
juc 就是 java.util.concurrent 包
线程池相关
ThreadPoolExecutor: 最核心的线程池类Executors: 线程池工厂类
并发集合类
ConcurrentHashMap: 线程安全的哈希表,采用分段锁机制CopyOnWriteArrayList: 线程安全的列表,适合读多写少场景
同步工具类
CountDownLatch: 倒计时锁存器,用于等待多个线程完成各自任务后再继续执行CyclicBarrier: 循环屏障,让一组线程互相等待直到都到达屏障点,可重复使用Semaphore: 信号量,用于控制同时访问特定资源的线程数量
原子类
AtomicInteger: 原子整数类,提供对整数类型的原子操作AtomicReference: 原子引用类,提供对对象引用进行原子操作
Thread.sleep() vs Object.wait()区别
| 特性 | Thread.sleep() |
Object.wait() |
|---|---|---|
| 所属类 | Thread 类(静态方法) |
Object 类(实例方法) |
| 锁释放 | 不释放锁 | 会释放锁 |
| 使用前提 | 任意位置调用 | 必须在同步块内(持有锁) |
| 唤醒机制 | 超时自动恢复 | notify() / notifyAll() 或超时 |
| 设计用途 | 暂停线程执行,不涉及锁协作 | 线程间协调,释放锁让其他线程工作 |
notify()是唤醒一个进程,被唤醒的进程如果结束时没调用 notify()方法,那其他线程就没人唤醒了
notifyAll()是唤醒所有进程,然后他们竞争一个锁,一个幸运儿获得锁
synchronized 简单易用,自动加锁释放锁
ReentrantLock 更复杂,有很多高级功能
CopyOnWriteArrayList
这是一个线程安全的 ArrayList 实现,适用于读多写少的场景
这个原理是:读不加锁,写的时候加锁,写会复制一个新的数组,修改新数组
所以其他线程读的时候,要么是读新的快照,要么是旧的快照(都是线程安全的)
不会出现读到撕裂值和中间状态
最终一致性
不要求“立刻看到最新数据”,但保证“迟早会看到”。
只有允许最终一致的场景,才适合使用 CopyOnWrite(COW)这类机制。
count++ 是读-改-写的复合操作,需要原子性
int temp = count; // 读
temp = temp + 1; // 改
count = temp; // 写
count 这种是要求读最新数据的所以 CopyOnWrite 不适用
一个 COW 的应用场景:事件监听器列表(Event Listener List)
场景描述:
在很多框架中(如 GUI 编程、Spring 事件机制、Netty、日志系统等),都有这样的需求:
- 系统运行过程中,偶尔会有组件注册或注销事件监听器(写操作很少);
- 但频繁会触发事件,需要遍历所有监听器并通知它们(读操作极多);
- 遍历时不能因为有人动态增删监听器而崩溃(比如抛 ConcurrentModificationException);
- 允许“本次事件通知不包含刚刚注册的监听器”(即可以接受短暂的旧快照)。
事件监听器就是在这里就就是用户登录的时候会触发的事件,每个用户登录都会读这个,所以读非常多,但是写很少,而且可以允许登录的时候用旧快照,因为旧快照也是合法的,也就是会少触发个事件,大体不影响,以后还是会用新快照(最终一致性)
一些其他应用场景:系统配置缓存,白名单/黑名单
你提供的关于 Java 乐观锁实现方式的描述是正确的,但确实不够规范和清晰。以下是规范化后的表述:
Java 中实现乐观锁的方式
1. CAS (Compare and Swap) 操作
CAS 是乐观锁的核心机制
Java 提供了
java.util.concurrent.atomic包,包含各种原子类(如AtomicInteger、AtomicLong等)这些原子类利用 CAS 操作实现线程安全的原子操作,是乐观锁的基础实现方式
CAS 操作本身是一次性的原子比较和交换,但使用中通常结合循环来实现自旋等待
CAS 有 ABA 的问题,就是如果读的时候是 A,修改时还是 A,那 CAS 就会认为变量没被修改过开始更新,但可能 A 是被改成 B 后又被改回 A 了
2. 版本号控制
- 在数据中增加版本号字段,用于记录数据的更新版本
- 每次更新数据时递增版本号
- 更新时比较当前版本号与获取时的版本号,若一致则更新成功,否则更新失败
3. 时间戳机制
- 使用时间戳记录数据的最后更新时间
- 更新数据时比较时间戳
- 若当前时间戳与数据记录的时间戳不一致,则说明数据已被其他线程更新,本次更新失败
这些方式都体现了乐观锁的核心思想:假设数据冲突较少,先进行操作,而在提交时检查是否有冲突发生。