Java面向对象
面向对象:是一种程序设计的思想, 其基本思想是使用对象、类、封装、继承、多态等基本概念来进行程序设计。
三大特性:
封装:封装性就是尽可能的隐藏对象内部细节,只保留一些对外的接口使其与外部发生联系。用户无需关心对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
可以通过对类的成员设置一定的访问权限,实现类中成员的信息隐藏。

优点:1.实现了高内聚,低耦合;2.提高系统的可扩展性、可维护性
继承: 子类的对象拥有父类的全部属性与方法,称作子类对父类的继承。
Java中父类可以拥有多个子类,但是子类只能继承一个父类,称为单继承。继承实现了代码的复用。
Java中所有的类都是通过直接或间接地继承java.lang.Object类得到的。
子类不能继承父类中访问权限为private的成员变量和方法。
子类可以重写父类的方法,即命名与父类同名的成员变量。
多态:对象的多态性是指在父类中定义的属性或方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为。
Java中多态体现在两个方面:
编译时多态:编译时多态主要指方法的重载
运行时多态:指的是方法的重写:产生运行时多态有三个条件:(1)继承;(2)方法重写;(3)父类引用指向子类对象;
Java 和 C++ 的区别
- Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
- Java 通过虚拟机(JVM)从而实现跨平台特性,但是 C++ 依赖于特定的平台。
- Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
- Java 支持自动垃圾回收,而 C++ 需要手动回收。
- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
- Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
- Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
JDK/JRE/JVM
- JVM:是java 虚拟机,当我们运行一个程序时,JVM 负责将Java源文件编译成能被Java虚拟机执行的字节码文件。除此之外,JVM 提供了内存管理/ 垃圾回收和安全机制等。
- JRE:Java Runtime Environment,Java 运行环境的简称,为 Java 的运行提供了所需的环境。它是一个 JVM 程序,主要包括了 JVM 的标准实现和一些 Java 基本类库。
- JDK:Java Development Kit,Java 开发工具包,提供了 Java 的开发及运行环境。JDK 是 Java 开发的核心,集成了 JRE 以及一些其它的工具,比如编译 Java 源码的编译器 javac 等。
Java数据类型以及自动拆箱/装箱
Java语言支持的数据类型包括两种:
基本数据类型:
- byte:1字节(8位)
- short(2):2字节(16位)
- char(2):2字节(16位)
- int(4):4字节(32位)
- float(4):4字节(32位)
- long(8):8字节(64位)
- double(8):8字节(64位)
- Boolean(1):true、false,可以使用 1 位(bit) 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。
java采用Unicode编码,不论汉字、字母、数字,每个字符都占用2字节。
自动装箱是Java编译器在基本数据类型和对应的对象包装类型之间做的一个转化。反之就是自动拆箱。自动装箱就是Java编译器在基本数据类型和对应的对象包装类型间的转化,即int转化为Integer,自动拆箱是Integer调用其方法将其转化为int的过程。
1 | Integer x = 2; // 装箱 调用了 Integer.valueOf(2) |
Java容器
容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
Collection

注:List是有序可重复元素集合,Set是无序不可重复元素集合;
Set
- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
- HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
- LinkedHashSet:具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。
List
- ArrayList:基于动态数组实现,支持随机访问。
- Vector:和 ArrayList 类似,但它是线程安全的。
- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
Queue
- LinkedList:可以用它来实现双向队列。
- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。
Map

- TreeMap:基于红黑树实现。
- HashMap:基于哈希表实现。
- HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 HashTable 不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
迭代器模式

Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。
Java中的Iterator功能比较简单,并且只能单向移动:
(1) 使用方法iterator()要求容器返回一个Iterator。注意:iterator()方法是java.lang.Iterable接口的方法,被Collection继承。
(2) 使用hasNext()检查序列中是否还有元素。
(3) 使用next()获得序列中的下一个元素。
(4) 使用remove()将迭代器新返回的元素删除。
Iterator只能正向遍历集合,适用于获取移除元素。ListIterator继承自Iterator,专门针对List,可以从两个方向来遍历List,同时支持元素的修改(插入和删除)。从 JDK 1.5 之后可以使用 for each方法来遍历实现了 Iterable 接口的聚合对象。
源码分析
ArrayList
ArrayList 是基于数组实现的,所以支持快速随机访问。
数组的默认大小为 10。
1
private static final int DEFAULT_CAPACITY = 10;
扩容:
oldCapacity + (oldCapacity >> 1),为旧容量的 1.5 倍,扩容操作需要调用Arrays.copyOf()把原数组整个复制到新数组中,操作代价很高。删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的。
Vector
同步:它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。
与ArrayList比较:
(1)Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
(2)Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍。替代方案:可以使用
Collections.synchronizedList();得到一个线程安全的 ArrayList。1
2List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。
1
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList
读写分离:写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
写操作需要加锁,防止并发写入时导致写入数据丢失。
写操作结束之后需要把原始数组指向新的复制数组。优缺点:
优点:CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能。
缺点:- 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
- 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
适用场景:适合读多写少的应用场景,不适合内存敏感以及对实时性要求很高的场景。
LinkedList
ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。ArrayList 和 LinkedList 的区别可以归结为数组和链表的区别:
数组支持随机访问,但插入删除的代价很高,需要移动大量元素;
链表不支持随机访问,但插入删除只需要改变指针。
HashMap
- 内部包含了一个 Entry 类型的数组 table。Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶取模运算结果相同的 Entry。

拉链法
1
2
3
4HashMap<String, String> map = new HashMap<>();
map.put("K1", "V1");
map.put("K2", "V2");
map.put("K3", "V3");(1)新建一个 HashMap,默认大小为 16
(2)插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 113,使用除留余数法得到所在的桶下标 113%16=1。
(3)插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 120,使用除留余数法得到所在的桶下标 120%16=8。
(4)插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 120,使用除留余数法得到所在的桶下标 120%16=8,插在 <K2,V2> 前面。(注意:头插法)扩容
当需要扩容时,令 capacity 为原来的两倍。扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。
链表转红黑树
从 JDK 1.8 开始,一个桶存储的链表长度大于等于 8 时会将链表转换为红黑树。
与HashTable比较
(1)Hashtable 使用 synchronized 来进行同步。
(2)HashMap 可以插入键为 null 的 Entry。(HashMap 使用第 0 个桶存放键为 null 的键值对)
(3)HashMap 的迭代器是 fail-fast 迭代器。
(4)HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
ConcurrentHashMap
- ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。
- size操作:ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。
- JDK1.8后改动:
- JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发度与 Segment 数量相等。
- JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。
Java泛型
Java1.4或更早版本的开发背景的人都知道,在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。
Java并发
为什么要使用多线程
多线程并不一定能提升性能(甚至还会降低性能);多线程也不只是为了提升性能。多线程主要有以下的应用场景:
避免阻塞(异步调用)
单个线程中的程序,是顺序执行的。如果前面的操作发生了阻塞,那么就会影响到后面的操作。这时候可以采用多线程,我感觉就等于是异步调用。这样的例子有很多:eg.ajax调用,就是浏览器会启一个新的线程,不阻塞当前页面的正常操作。
避免CPU空转
以web请求为例,处理完一条请求,再处理下一条请求的话,CPU会存在大量的闲置时间。因为处理一条请求,经常涉及到RPC、数据库访问、磁盘IO等操作,这些操作的速度比CPU慢很多,而在等待这些响应的时候,CPU却不能去处理新的请求,因此都采用对每个请求创建新线程来响应的方式实现,这样在等待IO操作的等待时间里,就可以去继续处理其他请求,对并发的响应性就好了很多。
提升性能
在满足条件的前提下,多线程确实能提升性能。多核CPU才行
进程和线程
进程是执行着的应用程序,而线程是进程内部的一个执行序列。一个进程可以有多个线程。
- 地址空间和其它资源:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
- 进程是资源分配的最小单位,它使用独立的数据空间;线程是程序执行的最小单位,它共享进程的数据结构。
使用线程的三种方法
实现Runnable接口
1.创建:实现Runnable接口 + 重写run()
2.启动:创建实现类对象 + Thread对象 + start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class StartRun implements Runnable {
/**
* 线程入口点
*/
public void run() {
for(int i=0;i<10;i++) {
System.out.println("一边听歌");
}
}
public static void main(String[] args) {
new Thread(new StartRun()).start();
for(int i=0;i<10;i++) {
System.out.println("一边敲代码");
}
}
}继承Thread类
1.创建:继承Thread + 重写run()
2.启动:创建子类对象 + start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class StartThread extends Thread {
/**
* 线程入口点
*/
public void run() {
for(int i=0;i<10;i++) {
System.out.println("一边听歌");
}
}
public static void main(String[] args) {
//启动线程
//1.创建子类对象
StartThread st = new StartThread();
//2.调用start
st.start(); //交给cpu,形成两条路径,但start方法不保证立即运行,由cpu去调用
for(int i=0;i<10;i++) {
System.out.println("一边敲代码");
}
}
}实现Callable接口
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装,并且抛出异常。
1
2
3
4
5
6
7
8
9
10
11
12
13public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
}推荐实现接口:避免单继承的局限性,优先使用接口,方便共享资源。
基础线程机制
线程池:
装有线程的池子,我们可以把要执行的多线程交给线程池来处理,和连接池的概念一样。使用实现了Executor接口的ThreadPoolExecutor来创建线程池。通过维护一定数量的线程池来达到多个线程的复用。(为了减少在创建和销毁线程上所花的时间以及系统资源的开销)。
主要有三种 Executor:
- CachedThreadPool:一个任务创建一个线程;
- FixedThreadPool:所有任务只能使用固定大小的线程;
- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
守护线程(Daemon):
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
main() 属于非守护线程。
在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程。
sleep():
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。使线程停止运行一段时间,将处于阻塞状态。
如果调用了sleep方法之后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行!
sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
1
2
3
4
5try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}yield():
yield:礼让线程,暂停线程直接进入就绪状态,不是阻塞状态。
如果调用了yield方法之后,没有其他等待执行的线程,这个时候当前线程就会马上恢复执行!
synchronized和ReentrantLock
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
synchronized:
- 同步一个代码块:同一个对象
- 同步一个方法:同一个对象
- 同步一个类:两个线程调用该类的不同对象上的这种同步语句,也会进行同步
- 同步一个静态方法:作用于整个类
ReentrantLock:
- ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁
比较:
- 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
- 性能:新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
- 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock 可中断,而 synchronized 不行。
- 公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
- 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象
- 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
线程中的相互协作
join():
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,直到目标线程结束。
虽然 b 线程先启动,但是如果在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。
wait():
调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
注意:
- 使用 wait() 挂起期间,线程会释放锁,而sleep() 不会;
- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
线程状态

Java I/O
分类
Java 的 I/O 大概可以分成以下几类:
- 磁盘操作:File
- 字节操作:InputStream 和 OutputStream
- 字符操作:Reader 和 Writer
- 对象操作:Serializable
- 网络操作:Socket
- 新的输入/输出:NIO
磁盘操作
File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。
递归地列出一个目录下的所有 文件
1 | public void show(File dir){ |
字节操作
InputStream
Java I/O 使用了装饰者模式来实现。以 InputStream 为例,
InputStream 是抽象组件;
FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。

实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可
字符操作
编码与解码:编码就是把字符转换为字节,而解码是把字节重新组合成字符。
- GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
- UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
- UTF-16be 编码中,中文字符和英文字符都占 2 个字节;(Java:Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。)
Reader和Writer

- InputStreamReader 实现从字节流解码成字符流;(转换流)
OutputStreamWriter 实现字符流编码成为字节流。(转换流)
- 从JDK文档中可以知道FileOutputStream是OutputStream的直接子类,FileInputStream也是InputStream的直接子类,但是在字符流文件的两个操作类却有一些特殊,FileWriter并不直接是Writer的子类,而是OutputStreamWriter的子类,而FileReader也不直接是Reader的子类,而是InputStreamReader的子类,那么从这两个类的继承关系就可以清楚的发现,不管是是使用字节流还是字符流实际上最终都是以字节形式操作输出流的。
### 对象操作序列化:
- 序列化就是将一个对象转换成字节序列,方便存储和传输。
- 序列化:ObjectOutputStream.writeObject()
- 反序列化:ObjectInputStream.readObject()
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
Serializable:
- 序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。
transient:
java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
ArrayList 中存储数据的数组 elementData 是用 transient 修饰的
1 | private transient Object[] elementData; |
通过一些重写的方法,只序列化数组中有内容的那部分数据
网络操作
Java 中的网络支持:
- InetAddress:用于表示网络上的硬件资源,即 IP 地址;
- URL:统一资源定位符;
- Sockets:使用 TCP 协议实现网络通信;
- Datagram:使用 UDP 协议实现网络通信。
从URL读取字节数据:
1 | public static void main(String[] args) throws IOException { |
Sockets:
- ServerSocket:服务器端类
- Socket:客户端类
- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。

NIO
NIO即New IO,这个库是在JDK1.4 中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
流与块:
I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
面向流的 I/O 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。
面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
通道和缓冲区:
通道 Channel: 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
缓冲区:发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
缓冲区状态变量:
1.capacity:最大容量;
2.position:当前已经读写的字节数;
3.limit:还可以读写的字节数。
选择器与非阻塞I/O:
NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。

NIO 与 IO 主要区别:
- NIO 面向块,IO 面向流
- NIO 非阻塞,IO 阻塞