数组
// 多维数组中构成矩阵的每个向量都可以具有任意长度,也称为粗糙数组
// 只有第一维必须需要指定长度(如果不指定其他维度长度,相当于仍然为一维数组,且所有元素都是null)
// 多维数组从第二维开始都是对象引用,可以为null
Random random = new Random(47);
int[][][] ints = new int[random.nextInt(7)][][];
// 元素为List<String>的数组
List<String>[] ls;
// 数组是协变的;集合不是
String[] strings = new String[10];
Object[] objects = strings;
// List<String> strings = new ArrayList<>();
// List<Object> objects = objects;
- 数组与其他种类容器之间的区别有三个方面:效率、类型、保存基本数据类型
- 数组是一种效率最高的存储和随机访问对象引用序列的方式
- 线性序列 ==> 随机访问非常快
- 大小(长度)被固定,且在其生命周期内不可变
ArrayList
虽然支持弹性扩容,但这种弹性需要开销,因此ArrayList
的效率比数组低很多- 数组标识符其实只是一个引用,指向在堆中创建的一个真实对象,这个(数组)对象用以保存指向其他对象的引用
- 对象数组保存的是引用;基本类型数组直接保存基本类型的值
char
数组的默认值是'\u0000' 0
(存疑:IDEA中拷贝出来的;JDK11.11)- 多维数组中构成矩阵的每个向量都可以具有任意长度,也称为
粗糙数组
- 数组必须知道它们所持有的确切类型,以强制保证类型安全
- 优先使用泛型方法而不是泛型类
容器深入研究
此图为Java编程思想第四版插图(可能与JDK8+版本实现有所不同;阅读此书籍时,本地JDK为11.11)
-
JDK11.11+
中:TreeMap<K,V> implements NavigableMap<K,V>
&
NavigableMap<K,V> extends SortedMap<K,V>
-
Set
LinkedHashSet
维护的是保持了插入顺序
的链接列表TreeSet
是SortedSet
的唯一实现(按照元素的比较函数
对元素排序)- 没有重新定义
hashCode()
的类放置到任何散列中都可能会产生重复(建议同时实现equals()
与hashCode()
) HashSet
的性能基本上总是比TreeSet
好,特别是在添加和查询
元素时。TreeSet
存在的唯一原因就是它可以维持元素的排序状态
。因为其内部结构支持排序,用TreeSet
迭代通常比HashSet
快
-
Queue
是队列,Deque
是双端队列Queue
是一种常用的数据结构,可以将队列看做是一种特殊的线性表,该结构遵循的先进先出
原则
(LinkedList
实现了Queue
接口,因为LinkedList
进行插入、删除操作效率较高)Deque
是Queue
的一个子接口;该队列两端
的元素既能入队(offer)也能出队(poll)
(如果将Deque
限制为只能从一端入队和出队,则可实现栈的数据结构)
(对于栈
而言,有入栈(push)和出栈(pop),遵循先进后出
原则)LinkedList<E> implements Deque<E>
&Deque<E> extends Queue<E>
Deque
不如Queue
常用
-
Map
Map
(映射表)的性能
是一个很重要的问题;HashMap
中使用了散列码
来取代键的缓慢搜索散列
是映射中存储元素时最常用的方式LinkedHashMap
可以在构造器中设定是否使用最近最少使用 LRU
算法- 使用
LRU
,没有被访问过的元素会出现在队列的前面(适用于需要定期清理元素场景) - 默认为按照
插入顺序
- 默认情况下以
插入顺序
或LRU
序循环时都以插入顺序进行遍历;访问到一定数量的元素后,LRU
模式获取元素的顺序才会发生改变 - 使用自定义的类作为键时(如
Map
、Set
),必须同时重载hashCode()
和equals()
(hashCode()
并不需要总是能够返回唯一的标识符,但equals()
方法必须严格判断两个对象是否相同) Map
接口entrySet()
的恰当实现应该在Map
中提供视图,而不是副本,并且这个视图允许对原始映射表进行修改
-
equals()
方法实现必须满足以下条件- 自反性。对任意
x
,x.equals(x)
一定返回true
- 对称性。对任意
x
和y
,如果y.equals(x)
返回true
,则x.equals(y)
也返回true
- 传递性。对任意
x y z
,如果有x.equals(y)
返回true
,y.equals(z)
返回true
,则x.equals(z)
一定返回true
- 一致性。对任意
x y
,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)
多少次,返回的结果应该保持一致,要么一直是true
,要么一直是false
- 对任何不是
null
的x
,x.equals(null)
一定返回false
- 自反性。对任意
-
散列的价值在于速度
HashMap
保存数据机制:- 散列将键保存在某处
- 存储一组元素最快的数据结构是
数组
,因此使用它来表示键的信息
- (数组容量不可调整)数组并不保存键本身。而是通过键对象生成一个数字,并将其作为数组的下标,这个数字就是
散列码
,由定义在Object
中且可覆盖的hashCode() 散列函数
方法生成 - 为解决数组容量固定的问题,不同的键可以产生相同的下标,即可能会产生
冲突
。因此数组多大就不重要了,任何键总能在数组中找到自己的位置 - 生成桶的下标前,
hashCode()
还需要进一步处理,所以散列码的生成范围并不重要,只要是int
即可 - 好的
hashCode()
应该产生分布均匀的散列码(分布不均可能导致某些区域负载过重)
散列码不必是独一无二的(应该更关注生成速度,而不是唯一性),但是通过
hashCode()
和equals()
必须能够完全确定对象的身份 -
快速报错
fast-fail
:是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast
事件;只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生- 集合修改次数参数
modCount
- 所有涉及修改集合中元素个数的操作,都会改变
modCount
的值 - 迭代器
Iterator
持有参数expectedModCount
,默认expectedModCount = modCount
- 遍历过程中若
expectedModCount != modCount
,抛出异常ConcurrentModificationException
,即产生fast-fail
- 集合修改次数参数
-
持有引用
- 当存在可能会耗尽大量内存的大对象时,这些类显得特别有用
- 三个继承自
Reference
的类:SoftReference
、WeakReference
、PhantomReference
。当垃圾回收器正在考察的对象只能通过某个Reference
对象才“可获得”时,这三个类为垃圾回收器提供了不同级别的间接性指示 - 如果希望一个对象能够被访问,同时希望能够允许垃圾回收器释放它,就可以使用
Reference
对象
(可以继续使用该对象,当内存消耗殆尽时又允许回收此对象)
条件:一定不能有普通的引用指向该对象(普通引用指没有经Reference对象包装过的引用) SoftReference
、WeakReference
、PhantomReference
由强到弱排列,对应不级别的“可获得性”
Java I/O 系统
类结构图:
// 视图缓冲器
// 真实定义 ByteBuffer
ByteBuffer bb = ByteBuffer.wrap(new byte[]{0, 0, 0, 0, 0, 0, 0, 'a'});
bb.rewind();
// 获取其他视图
CharBuffer cb = bb.asCharBuffer();
FloatBuffer fb = bb.asFloatBuffer();
IntBuffer ib = bb.asIntBuffer();
LongBuffer lb = bb.asLongBuffer();
ShortBuffer sb = bb.asShortBuffer();
DoubleBuffer db = bb.asDoubleBuffer();
-
Java I/O
设计中大量使用了装饰器
设计模式 -
RandomAccessFile
适用于由大小已知的记录组成的文件。与InputStream
、OutputStream
无任何关系,是一个完全独立的类,拥有和别的I/O
类型本质不同的行为(可以在一个文件内向前或向后移动),直接从Object
派生而来 -
管道流用于任务之间的通信
-
缓冲往往能显著的增加
I/O
操作的性能 -
旧的
I/O
包已经使用NIO
重新实现过(即便不显示的使用NIO
编写代码,也能从中受益)
(速度的提升来自于所使用的结构更接近于操作系统执行I/O
的方式:通道和缓冲器
)我们可以把它想象成一个煤矿,通道是一个包含煤层(数据)的矿藏,而缓冲器则是派送到矿藏的卡车。卡车满载而归,我们再从卡车上获得煤炭。
即不直接与通道交互,只和缓冲器交互,并把缓冲器再派送到通道。
通道要么从缓冲器获得数据,要么向缓冲器发送数据 -
缓冲器容纳的是普通的字节,为了把它们转换成字符,我们要么在输入它们时进行
编码
,要么在将其从缓冲器输出时对它们进行解码
-
视图缓冲器
可以让我们通过某个特定的基本数据类型的视窗查看其底层的ByteBuffer
(ByteBuffer
依然是实际存储数据的地方,对视图的任何修改都会映射为对ByteBuffer
的操作)
各种不同的视图数据显示的方式也不相同:
-
字节存放次序
big endian
:高位优先。将最重要的字节存放在地址最低的存储器单元little endian
:低位优先。将最重要的字节存放在地址最高的存储器单元ByteBuffer
默认是以big endian 高位优先
的形式存储数据的(网络传输时也多以高位优先)ByteOrder.BIG_ENDIAN
&ByteOrder.LITTLE_ENDIAN
-
ByteBuffer
是将数据移进移出通道的唯一方式,并且只能创建一个独立的基本类型缓冲器,或者使用as
方法从ByteBuffer
中获得
NIO
类关系(Java编程思想第四版)
-
文件锁
-
Java的对象序列化将那些实现了
Serializable
接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象 -
对象序列化主要为了支持两种特性:
- Java的远程方法调用(RMI),使存活于其他计算机上的对象使用起来就像存活于本机上一样(需要通过序列化传出参数及返回值)
- 对Java Beans来说序列化也是必须的
-
对象序列化是基于字节的
-
Serializable & Externalizable 区别
Serializable
对象完全以存储的二进制位为基础来构造,不调用构造器Externalizable
对象,默认只会调用源对象的默认无参构造(必须存在)进行(反)序列化【使用方法writeExternal()
&&readExternal()
进行定制】
-
对于实现了
Serializable
接口的类,使用transient
逐字段关闭(反)序列化使用
transient
修饰的字段并非能完全关闭(反)序列化 -
Externalizable
的替代方法实现
Serializable
接口,并“添加”方法writeObject()
与readObject()
;(反)序列化会自动调用(必须具有准确的方法签名)
【对象ObjectOutputStream
||ObjectInputStream
的对应方法执行调用】
方法定义是 private 的,不是接口方法却可以被正常调用
枚举
// 一个简单的 Reflection.java 反编译结果
// 1、类是final的;2、实例是static final的;3、新增了一个static方法
Compiled from "Reflection.java"
final class cn.yangcx.enum19.Explore extends java.lang.Enum<cn.yangcx.enum19.Explore> {
public static final cn.yangcx.enum19.Explore HERE;
public static final cn.yangcx.enum19.Explore THERE;
public static cn.yangcx.enum19.Explore[] values();
public static cn.yangcx.enum19.Explore valueOf(java.lang.String);
static {};
}
// 枚举子类化
public interface Food{
enum Appetizer implements Food{;}
enum MainCourse implements Food{;}
enum Dessert implements Food{;}
enum Coffee implements Food{;}
}
// enum编写方法
public enum ConstantSpecificMethod {
// 枚举对象实现方法
DATE_TIME {
String getInfo() {
return DateFormat.getDateInstance().format(new Date());
}
};
// 定义方法(普通方法或抽象方法都行)
abstract String getInfo();
}
-
所有的枚举类都继承自
java.lang.Enum
-
ordinal()
方法每个enum
声明时的次序,从0
开始 -
编译器自动提供
equals()
和hashCode()
方法 -
Enum
实现了java.lang.Comparable
接口 -
枚举中任意方法定义必须在
enum
实例之后 -
枚举类的构造器是否为
private
关系不大(只能在enum
定义的内部通过构造器创建实例) -
如果在
switch case
中调用return
,编译器可能会警告没有default
(确实没有default
的情况)
与是否使用枚举类型无关 -
编译器自动为创建的枚举类新增两个静态方法:
values()
和valueOf()
(java.lang.Enum
的valueOf()
方法有两个参数) -
反编译后,枚举类及
enum
实例都是final
的;且多了一个static
方法 -
对于
enum
而言,实现接口是使其子类化的唯一办法 -
enum
要求其成员必须是唯一的 -
EnumSet
性能非常好(可能是将一个long
值作为比特向量;具体原理及作用也搞不清楚) -
EnumMap
内部由数组实现,所以速度飞快enum
实例的定义顺序决定了其在EnumSet
和EnumMap
中的顺序
EnumMap
允许开发者修改值对象 -
存在多种
enum
,且他们之间存在互操作的情况下,可以使用EnumMap
实现多路分发(multiple dispatching) -
Java
的enum
允许开发者为每个enum
实例编写方法;表驱动的代码(table-driven code)
注解
// 注解定义
// 如果希望此注解应用于所有的 ElementType,可以省去 @Target(很少见)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
// 元素不是必须的
String description() default "no description";
// 元素不能由不确定的值;不能以 null 作为其值(如果需要,可以使用 -1 或空字符串替代)
// String desc() default null;
}
- 注解定义
@Target
:此注解应用于什么地方(METHOD
、FIELD
。。。)
(如果希望此注解应用于所有的ElementType
,可以省去 @Target(很少见))@Retention
:此注解在哪一个级别可用(SOURCE
源代码、CLASS
类文件、RUNTIME
运行时。。。)@Documented
:此注解将包含在Javadoc
中@Inherited
:允许子类继承父类中的注解
- 没有元素的注解被称为
标记注解
- 注解可以嵌套
- 元素不能有不确定的值;也不能以
null
作为其值 - 使用多个注解时,同一个注解不能重复使用
- 注解不支持继承(不能使用
extends
语法)
并发
// 第一个线程设置为守护线程并启动任务中的线程
Thread thread = new Thread(new Daemon());
thread.setDaemon(true);
thread.start();
// 子线程 Daemon.java
private final Thread[] threads = new Thread[10];
@Override
public void run() {
for (int i = 0; i < threads.length; i++) {
//调用当前线程的线程是一个守护线程,导致当前线程也是守护线程
threads[i] = new Thread(new DaemonSpawn());
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
// 这里全部都是 isDaemon = true
print("t[" + i + "].isDaemon() = " + threads[i].isDaemon() + ",");
}
}
// 无法从线程中捕获异常
public class ExceptionThread implements Runnable {
@Override
public void run() {
// 直接抛出异常
throw new RuntimeException();
}
public static void main(String[] args) {
try {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
} catch (RuntimeException ue) {
// todo 这一句不会执行
System.out.println("Exception has been handled!");
}
}
}
// 捕获线程抛出异常
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
// todo 实现方法,处理异常
}
// 设置未捕获异常处理器
thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
// OR 设置默认异常处理器
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
// CountDownLatch
// 计数
final CountDownLatch countDownLatch = new CountDownLatch(2);
// 阻塞等待其他线程完成;计数归零后自动唤醒继续执行
countDownLatch.await();
// 计数 -1
countDownLatch.countDown();
// CyclicBarrier
// 屏障个数;barrierAction-屏障满足条件后优先执行的方法
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, null);
// 到达屏障后阻塞等待;满足条件后优先执行barrierAction(如果存在),再继续执行后续方法
cyclicBarrier.await();
// Semaphore
// fair – true if this semaphore will guarantee first-in first-out granting of permits under contention, else false
Semaphore semaphore = new Semaphore(size, true);
// 请求许可(阻塞等待)
available.acquire();
// 返还许可
available.release();
- 实现线程的两种方式
myThread extends Thread
myRunnable implements Runnable
- 通过多线程机制,子任务中的每一个都将由执行线程来驱动;其底层机制是
切分CPU时间
(CPU将轮流给每个任务分配其占用时间) Executor
是Java SE5/6
中启动任务的优选方法- 在任何线程池中,现有线程在可能的情况下,都会被自动复用
- 如果希望从任务在完成时能够返回一个值,可以实现
Callable
接口 - 调度器倾向于让优先级越高的线程先执行(优先级越低的线程也会执行,只是执行的频率较低)
(绝大多数情况下,所有线程都应该按照默认的优先级运行。不建议操作线程优先级) - 尽管
JDK
有 10 个优先级,但它与多数操作系统都不能映射的很好 Thread.yield()
可以建议
将CPU时间片从一个线程转移到另一个线程(不保证一定生效)
yield()
与sleep()
:线程并不会释放锁
wait()
:线程会释放锁- 通过线程
Thread0
(此线程为守护线程)启动多个子线程ThreadN
(不显式设置为守护线程),那么子线程也会自动的成为守护线程 - 使用原生的
Thread
并设置为守护线程
时,任务中的finally
语句块不会执行
(非守护线程时,finally
语句块会正常执行 ==>main
主线程结束后,系统不会以一种优雅的方式退出守护线程) Thread
类自身并不执行任何操作,它只是驱动赋予它的任务- 一个线程可以在其他线程之上调用
join()
方法,其效果是等待一段时间直到第二个线程结束才继续执行
例:如果在线程t1
上执行t2.join()
,线程t1
将被挂起,直到线程t2
结束才恢复 - 无法从线程中捕获异常(使用
try-catch
不能捕获,需要使用其他方式实现) - 捕获从线程抛出的异常:
Thread.UncaughtExceptionHandler
原子操作
是不能被线程调度机制中断的操作,一旦开始,一定可以在切换到其他线程之前完成操作
(依赖于原子性是很棘手且很危险的,问题:原子操作是否需要进行同步控制?)volatile
确保了域在应用中的跨线程的可视性;同步synchronized
也会导致向主存中刷新
(如果一个域完全由synchronized
方法或语句块来防护,不必将其设置为volatile
的)volatile
会移除编译器所有关于读取和写入操作的优化- 相比于使用
volatile
关键字(使用起来需要考虑的更周全),使用锁更安全(synchronized
或Lock
对象) synchronized
不属于方法签名- 建议使用同步代码块(
synchronized
修饰方法内的部分代码)而不对整个方法进行同步 - 为使用相同变量的每个不同的线程创建不同的存储,即线程本地存储,可由
java.lang.ThreadLocal
实现 - 线程状态及转换
- 线程中断
- 在
run()
中使用return
- 调用
Thread.interrupted()
(新的concurrent
库阻止了对此方法的直接调用) - 调用
ExecutorService.shutDown() || ExecutorService.shutDownNow()
关闭所有任务 - 调用
ExecutorService.sumit()
方法返回对象Future<?>.cancel(boolean mayInterruptIfRunning)
关闭单个任务
- 在
- 能够中断对
sleep()
的调用,但无法中断试图获取synchronized
锁或任何试图执行I/O
操作的线程
(I/O
操作可以通过关闭底层资源进行中断,不是必需操作) - 只能在同步控制方法或同步控制块(
synchronized
)里调用wait()
、notify()
和notifyAll()
- 使用
notify()
而不是notifyAll()
是一种优化(只会唤醒等待这个锁的相关任务) Lock && Condition
:每个对lock()
的调用必需使用try-catch
子句保证锁的正常释放(lock()
在try
之前执行);任务在调用await()
、singal()
或singalAll()
之前必需先获取锁
(Lock
和Condition
对象只有在更困难的多线程问题中才是必需的)CountDownLatch
:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes
- 用给定的计数
N
初始化CountDownLatch
- 使用
countDown()
使计数归零之前,await()
方法会一直阻塞。归零之后,释放所有等待的线程,await()
的所有后续调用都将立即返回 - 计数无法被重置
- 一个线程(或者多个), 等待另外
N
个线程完成某个事情之后才能执行
- 用给定的计数
CyclicBarrier
字面意思为可循环使用(Cyclic)的屏障(Barrier)。让一组线程到达一个同步点后再一起继续运行,在其中任意一个线程未达到同步点,其他到达的线程均会被阻塞A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point
- (构造函数
CyclicBarrier(int parties, Runnable barrierAction)
也可以实现所有await()
满足后优先执行barrierAction
)
- (构造函数
CountDownLatch
与CyclicBarrier
对比- 对于
CountDownLatch
来说,重点是“一个或多个线程等待”,而其他N个线程在完成“某件事情”后可以终止也可以等待; - 对于
CyclicBarrier
,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待 CountDownLatch
是计数器,线程完成一个记录一个,只不过计数不是递增而是递减CyclicBarrier
更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行
CountDownLatch
简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()
方法发出通知后,当前线程才可以继续执行CyclicBarrier
是所有线程都进行等待,直到所有线程都准备好进入await()
方法之后,所有线程同时开始执行!
(构造函数CyclicBarrier(int parties, Runnable barrierAction)
也可以实现所有await()
满足后优先执行barrierAction
)CountDownLatch
的计数器只能使用一次。而CyclicBarrier
的计数器可以使用reset()
方法重置。
(所以CyclicBarrier
能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次)CyclicBarrier
还提供其他有用的方法
(比如getNumberWaiting
方法可以获得CyclicBarrier
阻塞的线程数量。isBroken
方法用来知道阻塞的线程是否被中断)
- 对于
- Semaphore:计数信号量,允许
n
个任务同时访问某个资源 java.util.concurrent.Exchanger
类可用于两个线程之间交换信息可简单地将
Exchanger
对象理解为一个包含两个格子的容器,通过exchange
方法可以向两个格子中填充信息。
当两个格子中均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换Exchanger
类仅可用作两个线程的信息交换- 超过两个线程调用同一个
exchanger
对象时,得到的结果是随机的(并且只有两个线程能交换成功,未配对的线程会被阻塞,永久等待)
Synchronized
和Lock
对比- 使用
Lock
通常会比使用synchronized
高效的多,而且synchronized
的开销变化范围更大,而Lock
相对更一致 synchronized
的代码可读性更高- 更安全的做法是以更加传统的互斥方式入手,只有在对性能有明确需求时再替换为
Lock
- 使用
- 免锁容器:
CopyOnWriteArrayList
、CopyOnWriteArraySet
(CopyOnWriteArraySet
底层使用CopyOnWriteArrayList
承载数据);写入操作会导致创建整个底层数组的副本ConcurrentHashMap
、ConcurrentLinkedQueue
允许并发读取和写入;写入操作只会导致容器中的部分内容(分段)发生复制
ReadWriteLock
对向数据结构相对不频繁的写入,但是有多个任务经常读取这个数据结构的这类情况进行了优化- 可以拥有多个读取者,只要他们都不试图写入即可
- 如果写锁被其他线程持有,那么任何读取者都不能访问,直到写锁被释放
参考资料: