1. Java基础
1. Java基本语法
1. 数据类型
char不能被强制转换成string
注意:Java你的long类型一定要在数值后面加一个L,否则会被解析为整型,大小写都可,但是建议大写,小写容易和1搞混。
Folat需要加F
装箱与拆箱
装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
Integer i = 10; //自动装箱
Integer integer = Integer.valueOf(num);//手动装箱,这种方法更好,上面始终创建实例,而这种可能在未来进行优化,因为创建过程不固定
Integer i=new Integer(10);//未进行装箱,只是生成一个引用
int n1 = i.intValue();//手动拆箱
int n = i; //自动拆箱
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析
==比较时如果有一边是算数表达式则会自动进行拆箱,两边是包装器就比较引用类型
Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character(char的包装类),Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character 创建了数值在[0,127]范围的缓存数据,Boolean 直接返回 True Or False。如果超出对应范围仍然会去创建新的对象。
自动装箱拆箱只发生编译阶段,只为减少代码编写,其会影响执行效率。
所有的整数和浮点数的包装类型都继承自Number
,基本类型没有无符号形,但是包装器提供了相关方法。
System.out.println(Byte.toUnsignedInt(x)); // 255
System.out.println(Byte.toUnsignedInt(y)); // 127
2. 数据语法
3. 运算符
==和equals的区别
==:基本数据类型比较的是值,引用数据类型比较的是内存地址
equals:用于引用类型的值比较,比较之前需要进行重写(string默认已经被重写过)
Objects.equals(null,"SnailClimb");//防止空指针异常
hashCode与equals
hashCode用于获取哈希码,由于不同的对象会出现哈希碰撞得出相同的哈希码,这时须要用equals进一步判断是否真的相同。
hashCode没有覆盖之前不同的对象得出的哈希码一定不同,即使不同的对象他们的值一样,但是值一样的对象我们认为其为相同对象,所以需要重写hashCode
2. Java执行控制流程
- 条件语句
- if
- if … else
- if … else if
- switch
- 循环语句
- while
- do … while
- for
- 跳转语句
- break,跳出该循环体
- continue,推出此次循环,执行下一次循环。
3. 数组操作
3.1 数组初始化
int[] arrayA = new int[]{10, 20, 30};//静态数组标准分配
int[] arrayC = {30, 40, 50};// 静态数组省略式分配,注意省略不可以分两步骤
int[] arrayA = new int[5];//动态数组的标准分配
3.2 数组遍历
普通for循环
foreach
Arrays.toString(ns)
3.3 多维数组
获取行数: int rowLength = array.length;
获取列数: int colLength = array[0].length;
4. 面向对象
1. 对象创建
2. 属性和方法
3. 构造方法
一旦手动定义了一个构造方法,编译器就不再提供默认的构造器。构造函数只能在该对象中被调用一次
4. 方法重载
5. 方法重写
构造器不能被重写,重写规则如下:
- “两同”即方法名相同、形参列表相同;
- “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
- “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。
6. 初始化
1. 类的初始化:构造函数
2. 成员初始化
3. 构造器初始化
4. 初始化顺序
- 静态属性
- 静态方法块
- main方法
- 普通属性
- 普通方法快
- 构造函数
7. this和supper
this还可以和构造函数一起使用,注意只能放在第一行
super与this相似不过指向的父类,和构造函数配合也只能放在第一行。
注意类的初始化,如果父类没有初始化则会优先初始化父类
8. 访问控制权限
- 继承(extends)
- 多态
- 继承
- 重写父类方法
- 父类引用指向子类对象
- 组合:类中放其它几个类对象,就像是把其它类组合在一起了
- 代理
- 向上转型/向下转型
- 向上转型:父类引用,子类对象,自动完成
- 向下转型:子类引用,父类对象,需要强制转型
- static
- 修饰变量,即类变量,生命周期与类相同
- 静态方法:不能用this,不能调用非静态变量和方法
- 静态代码块:可用于类初始化
- final
9. 接口和抽象类
- 接口(interface):
- 修饰符只能是public和default
- implements实现接口
- 接口不能被实例化(new),所以不能有构造方法
- 必须实现所有方法,否则定义为抽象类
- 抽象类(abstract):
- 有抽象方法必须是抽象类
- 既可以有抽象方法也可以有具体方法
- 可以拥有构造方法
- 不能被实例化(即new)
10. 内部类
内部类拥有外部类的访问权
- 定义在方法中的类:局部内部类
- 定义在作用域内的类:成员内部类
- 作为静态属性的存在:静态内部类
- 实现了接口的匿名类:匿名内部类
11. 深拷贝与浅拷贝
浅拷贝:引用数据类型的拷贝结果是指向同一对象
深拷贝:引用数据类型的拷贝结果是生成的新对象
5. 异常处理
异常都继承自java.long中的Throwable类。Throwable有Exception和Error。
受检查异常
Java 代码在编译过程中,如果受检查异常没有被
catch
/throw
处理的话,就没办法通过编译 。不受检查异常(
RuntimeException
)
throws和throw:throws跟在方法声明后面。throw用在方法体内,表示抛出异常
try…catch…finally:finally只有以下情况才会不执行
- try中执行
System.exit(int)
- 程序所在的线程死亡。
- 关闭 CPU。
注意: 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。
多catch中由于匹配从上至下所以需要子类在前,否则就没意义
可以没有catch,但是不能单try
栈也可能发生OOM,当无法申请到足够的内存
6. 集合
1. Iterator接口
实现此接口的对象允许成为for-each循环的目标,即能够被迭代。
2. List
- ArrayList
- Vetor:线程安全,不推荐
- Stack
- LinkedList
ArrayList
// ArrayList传入0长度时,返回的对象
private static final Object[] EMPTY_ELEMENTDATA = {};
// 无参构造函数返回的对象
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}
// 真正存放元素的数组
transient Object[] elementData; // non-private to simplify nested class access
private int size;
扩容机制:
添加元素时才会进行动态扩容,如果为空初始化到DEFAULT_CAPACITY = 10
,否则扩容到原来的1.5倍。
int newCapacity = oldCapacity + (oldCapacity >> 1);
如果扩容之后仍然不够,这回将数组设置到需要的大小。扩容的最大值为Integer.MAX_VALUE
。设置完毕大小之后就会用Arrays.copyOf()
方法将数组给复制过来。该方法底层调用System.arrayCopy()
方法
elementData = Arrays.copyOf(elementData, newCapacity);
remove方法:
采用向前复制,并将最后一个元素置为null。
Vector
组成基本和ArrayList类似。
扩容机制:
构造函数允许传入一个capacityIncrement
作为扩容因子,如果没有传入每次容量就翻倍,反之传入的话容量会增加扩容因子大小。
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
3. Set
- HashSet:不保证迭代顺序,允许null元素
- TreeSet:基于TreeMap的NavgableSet实现。排序基于构造函数,和TreeMap相同
- LinkedHashSet:双向链表,链表定义了插入顺序,迭代次数不受容量控制。
4. Map
- HashMap
- LinkedHashMap:双向链表,线程不安全,允许null
- WeakHashMap:键可能被自动回收,无论其映射如何,支持null,不允许重复,经常被用作缓存
- ConcurrentHashMap
- EnumMap:效率非常高,空间占用也好,key为枚举尽量使用EnumMap
- TreeMap:红黑树,线程不安全。继承SortedMap,保证了顺序性,所以需要key实现Comparable接口,或者map传入Comparator实现匿名内部类
- Hashtable:
全表锁
来保持线程安全
HashMap
比较对象时使用equals,使用containsKey判断是否含有key。一个key-value键值对被称为Entry
。
//是hashMap的最小容量16,容量就是数组的大小也就是变量,transient Node<K,V>[] table。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大数量,该数组最大值为2^31一次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子,如果构造的时候不传则为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//一个位置里存放的节点转化成树的阈值,也就是8,比如数组里有一个node,这个
// node链表的长度达到该值才会转化为红黑树。
static final int TREEIFY_THRESHOLD = 8;
//当一个反树化的阈值,当这个node长度减少到该值就会从树转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//满足节点变成树的另一个条件,就是存放node的数组长度要达到64
static final int MIN_TREEIFY_CAPACITY = 64;
//具体存放数据的数组
transient Node<K,V>[] table;
//entrySet,一个存放k-v缓冲区
transient Set<Map.Entry<K,V>> entrySet;
//size是指hashMap中存放了多少个键值对,即实际容量
transient int size;
//对map的修改次数
transient int modCount;
//加载因子
final float loadFactor;
数据超过
容量*0.75(加载因子)
时,容器将会执行扩容,最小容量16
。
JDK1.7存储数据的方式是使用拉链法,即使用链表(头插法,尾插法需要指针移到尾部浪费性能)+数组
。JDK1.8及其之后链表的长度超过阈值则链表会转变成一颗红黑树
,但是在转换之前仍然有一个条件就是数组的长度必须不能小于64
,不是所有的链表都会同时转换,只有满足条件的链表才会转换。
转载因子对性能的影响:过大则意味着链表的长度越长,这回导致检索性能的降低,反之过短虽然性能增加了但是空间浪费严重。
扩容机制:
由于下标计算的特性所以集合的容量大小必须是2的倍数
。
每一次的扩容都是原容量的二倍
初始若为空,则只会在第一次put时才进行扩容。
除非容量为空,否则都是执行的先插入数据再进行扩容``(resize)`
下标计算(Hash地址计算):
由于数组下标最大长度为length - 1,所以计算出来的最终值不能大于length-1。只有使用length-1和其相与算出来的下标值才会满足条件。
i = (length - 1) & hash
ConcurrentHashMap
jdk1.7使用分段锁(Segment 数组 + HashEntry 数组 + 链表)
,1.8之后使用 Node 数组 + 链表 / 红黑树
。底层基于CAS+synchronized+volatile
实现的线程安全。【一个Segment包含多个HashEntry,一个HashEntry包含多个Entry】
volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性
正是由于volatile关键字,所以每次get()
方法无需加锁就取出的值都是最新的。
HashMap和Hashtable的区别
- 线程安全
- 效率
- null key支持:HashMap允许null key,但是Hashtable不允许
- 初始容量和扩容机制:Hashtable初始11,此后扩容2n+1,HashMap初始16,此后翻倍。Hashtable会使用指定的容量大小,但是HashMap总是为2的倍数。
- 底层数据结构:尽管开始都是数组+链表,但是HashMap在一定条件下会转换数据结构。
5. Queue
- PriorityQueue:不允许null,线程不安全,队首是优先级最高的元素,所以需要实现或传入比较器
- PriorityBlockingQueue:线程安全
- ArrayDeque
- 相关方法:
- add(),超出容量抛异常
- offer(),超出容量返回false
- remove(),空队列,抛异常
- poll(),空队列返回null,所以不要向里面放null元素,否则不知道是不是队列空了
- peek/element(),返回队首,不会出队,前者会抛异常
6. Collections
- Collections类:一个包装类,只包括静态方法和返回Collections
- 创建空集合
- 创建单元素集合
- 排序sort,传入集合需要可变
- shuffle,洗牌算法,重写随机打乱集合元素顺序
- unmodifiable,不可变集合,返回一个新的集合,最好将原先的即集合引用置空
- synchronized,使集合变得线程安全返回一个新集合
7. RandomAccess接口
一个标识符,表示其支持随机访问。
8. comparable和comparator接口
- comparable出自java.long,它有一个compareTo方法,用于集合内部,也叫做内部比较器
- comparator出自java.util,它有一个compare方法,用于集合外部,也叫做外部比较器
9. 集合迭代
Iterator对象有两个方法:boolean hasNext()
判断是否有下一个元素,E next()
返回下一个元素。
foreach迭代的本质就是使用Iterator,所以只要实现了Iterator接口都可以使用foreach
map的迭代方法:
for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println(key + " = " + value);
}
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
创建迭代器:
在自定义的集合内部创建一个内部类实现了迭代器接口,集合创建一个方法可以获取到该迭代器。迭代器先要重写hasNext和next方法
// 一个简单的实现
@Override
public boolean hasNext() {
// index减少到0说明迭代完了
return index > 0;
}
@Override
public T next() {
// 每迭代一次减少index
index--;
return ReverseList.this.list.get(index);
}
不支持直接访问的集合优先选择foreach方式进行遍历,因为在for中循环进行元素查找时性能非常低,比如链表结构使用get(i)都会首先遍历i之前的元素
10. List与Array的转换
List<String> list = List.of("apple", "pear", "banana");
Object[] array = list.toArray();
会丢失信息,不推荐。
List<Integer> list = List.of(12, 34, 56);
Integer[] array = list.toArray(new Integer[3]);
如果传入的数组不够大,那么List
内部会创建一个新的刚好够大的数组,填充后返回;如果传入的数组比List
元素还要多,那么填充完元素后,剩下的数组元素一律填充null
。
// 推荐使用
Integer[] array = list.toArray(new Integer[list.size()]);
集合判断相等使用的是equals方法
7. 泛型
- 泛型表示类,属性、方法参数泛型
- 泛型表示接口
- 泛型方法
- 泛型通配符
- 上界通配符:<? extends ClassType> 表示ClassType的所有子对象
- 下界通配符:<? super ClassType>表示ClassType的所有父对象
- 这些类型只能表示都,不能表示写
- 无限定通配符”?“
- ? 表示不确定的 java 类型
- T (type) 表示具体的一个 java 类型
- K V (key value) 分别代表 java 键值中的 Key Value
- E (element) 代表 Element
泛型本质就是使用Object,所以泛型不能是基本类型,因为基本类型不属于Object,所以泛型类的不同对象获取的Class也是相同的
泛型造成的覆写问题:
比如类中覆写equals(T t),由于底层会被擦拭成equals(Object t)而这与java自带的equals方法重复。
泛型可以被继承
不能实例化
T
类型,例如:new T()
可以声明带泛型的数组但是不能直接new出来,需要强制转型
Pair<String>[] ps = (Pair<String>[]) new Pair[2];
8. 反射
JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。
每加载一种class
,JVM就为其创建一个Class
类型的实例,并关联起来。注意:这里的Class
类型是一个名叫Class
的class
。它长这样:
public final class Class {
private Class() {}
}
这个Class实例是JVM内部创建的,如果我们查看JDK源码,可以发现Class类的构造方法是private,只有JVM能创建Class实例,我们自己的Java程序是无法创建Class实例的。
1. 创建类实例
//知道具体类
Class alunbarClass = TargetObject.class;
//传入类路径
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");
//通过对象实例
Employee e = new Employee();
Class alunbarClass2 = e.getClass();
//通过类加载器,传入类路径,通过类加载器获取Class对象不会进行初始化,意味着不进行包括初始化等一些列步骤,静态块和静态对象不会得到执行
class clazz = ClassLoader.LoadClass("cn.javaguide.TargetObject");
//基本内置类型的包装类都有一个TYPE属性
Calss cl = Integer.TYPE;
Java的类是动态加载的,只有当用到是才会加载该类,根据这个思路可以以包路径为判断条件来动态加载类
2. 创建对象
主要通过构造器来创建对象
通过Class实例进行获取Constructor
:
getConstructor(Class...)
:获取某个public
的Constructor
;getDeclaredConstructor(Class...)
:获取某个Constructor
;getConstructors()
:获取所有public
的Constructor
;getDeclaredConstructors()
:获取所有Constructor
。
private构造方法需要设置权限(setAccessible(true)
)才能进行对象的创建:
//本质调用无参构造器,没有public无参构造就会失败
Object object=c1.newInstance();
//通过构造器创建对象
c1.getConstructor(...).newInstance(...)
3. 方法使用
通过类实例获取该类的方法:返回Method对象
Method getMethod(name, Class...)
:获取某个public
的Method
(包括父类),第二个参数是由于方法可能重载。Method getDeclaredMethod(name, Class...)
:获取当前类的某个Method
(不包括父类,但是可以获取私有方法)Method[] getMethods()
:获取所有public
的Method
(包括父类)Method[] getDeclaredMethods()
:获取当前类的所有Method
(不包括父类)
//invoke的第一个参数为该方法所属对象,毕竟需要对象调用方法,类不行,如果调用静态方法那么传入对象为null,传入子类对象也遵循多态
method.invoke(object,...)
一个Method
对象包含该方法的所有信息:
getName()
:返回方法名称,例如:"getScore"
;getReturnType()
:返回方法返回值类型,也是一个Class实例,例如:String.class
;getParameterTypes()
:返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
;getModifiers()
:返回方法的修饰符,它是一个int
,不同的bit表示不同的含义。
4. 属性操作
通过类实例获取该类的属性:返回Field对象
Field getField(name)
:根据字段名获取某个public的field(包括父类)Field getDeclaredField(name)
:根据字段名获取当前类的某个field(不包括父类)Field[] getFields()
:获取所有public的field(包括父类)Field[] getDeclaredFields()
:获取当前类的所有field(不包括父类)
一个Field
对象包含该字段的所有信息:
get(object)
:返回字段值getName()
:返回字段名称,例如,"name"
;getType()
:返回字段类型,也是一个Class
实例,例如,String.class
;getModifiers()
:返回字段的修饰符,它是一个int
,不同的bit表示不同的含义。- Modifier.isFinal(m); //m是getModifiers获取到的值
5. 获取Interface
通过类实例调用getInterfaces()
可以返回该类实现的所有接口,但是不包括父类接口
。
使用instanceof
关键字判断对象类型,父类也会返回为真。
Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isNumber = n instanceof Number; // true
Class中的isAssignableFrom方法判断时,可以是否可以进行向上转型
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer
6. 获取Annotation:
可以根据注解位置获取相应的注解
Class.getAnnotation(Class)
Field.getAnnotation(Class)
Method.getAnnotation(Class)
Constructor.getAnnotation(Class)
注解本身就属于class,所以读取方法、字段和构造方法的Annotation
和Class类似。
读取方法参数注解会有点麻烦,getParameterAnnotations返回一个注解数组。
7. 反射优缺点
- 反射执行方法性能很低
- 如果关闭权限检测,速度会有所提升
- 如果经常使用反射可以关闭权限检测
同一类的不同对象反射获得的类都是一样的
对于数组,只要维度一样就是同一类
9. 枚举
- 枚举特性:编译器会自动添加toString,ordinal(声明顺序,不建议使用,选择通过构造函数设置顺序),vaules方法返回数组,可以静态导包像static,valueOf普通字符串形势返回变量。
- 枚举和普通类一样:除了可以快速定义常量,还能定义属性和方法
- EnumSet,EnumMap:两个枚举工具类,效率更高。
- 所有枚举值都是public static final
枚举的类型是一个单独的类型,相较于常量有优势,能够在编译期间发现某些错误
尽管枚举也是引用类型,但是由于每个常量在JVM只有唯一实例,所以可以使用==进行比较,需要变量类型
- 定义的
enum
类型总是继承自java.lang.Enum
,且无法被继承; - 只能定义出
enum
的实例,而无法通过new
操作符创建enum
的实例; - 定义的每个实例都是引用类型的唯一实例;
- 可以将
enum
类型用于switch
语句,case值不要变量类型 - 编译后的
enum
类和普通class
并没有任何区别。但是我们自己无法按定义普通class
那样来定义enum
,必须使用enum
关键字,这是Java语法规定的。
注意:枚举类的字段也可以是非final类型,即可以在运行期修改,但是不推荐这样做!
name()方法返回字符串和toString类似,但是toString可以被覆写。点出枚举属性返回的是toString方法的结果。
10. 文件与I/O
字节流:处理单位byte
字符流:处理单位char
数据源是文本最好使用字符流
Java标准库的包
java.io
提供了同步IO,输入输出会阻塞,而java.nio
则是异步IO。
输入输出是一个相对的概念,一般从程序出发,输入就是读取流到程序中,输出则是写入流到其他位置。
1. File对象
对象创建:
// 绝对路径
File f = new File("C:\\Windows\\notepad.exe");
// 相对路径,可以用.表示当前目录,..表示上级目录。
File f = new File("/usr/bin/javac");
注意Windows平台使用
\
作为路径分隔符,在Java字符串中需要用\\
表示一个\
。Linux平台使用/
作为路径分隔符。
File对象可以获取到File的路径
:
String getPath()
:返回构造方法传入的路径。String getAbsolutePath()
:返回绝对路径。String getCanonicalPath
:返回规范路径,对绝对路径的规范。
创建File对象时路径出错不会出现异常,只有操作时才会出现异常。调用isFile()
,判断该File
对象是否是一个已存在的文件,调用isDirectory()
,判断该File
对象是否是一个已存在的目录。
createNewFile
,创建文件createTempFile
,创建临时文件,程序停止时文件会删除。deleteOnExit
,虚拟机退出时删除文件list()
和listFiles()
列出目录下的文件和子目录名。listFiles()
可以传入一个FileFilter
,可以过滤不想要的文件和目录。
当File对象是一个目录时:
boolean mkdir()
:创建当前File对象表示的目录;boolean mkdirs()
:创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;boolean delete()
:删除当前File对象表示的目录,当前目录必须为空才能删除成功。
2. InputStream
此为抽象类,提供了一个重要方法read返回int(ASCII码),为空时返回-1,但是一次只能读取一个,可以配合利用StringBuilder可以拼成一个字符串。
int read(byte[] b)
:读取若干字节并填充到byte[]
数组,返回读取的字节数int read(byte[] b, int off, int len)
:指定byte[]
数组的偏移量和最大填充数
ByteArrayInputStream(byte[]),在内存中模拟一个字节流输入。
如果想为流加缓冲功能、计算签名、加密解密功能
graph TB
FileInputStream --> BufferedFileInputStream
FileInputStream --> DigestInputStream
FileInputStream --> CipherFileInputStream
FileInputStream --> BufferedDigestFileInputStream
FileInputStream --> BufferedCipherFileInputStream
3. OutputStream
提供write(int b)
方法,实际上只是将其写入到缓冲区中,如果要真正写到磁盘或网络中需要flush()
,缓冲区满了也会自动flush,对于网络传输不建议自动flush,因为一次性会发送大量数据。write(byte[])
一次写入多个数据。
ByteArrayOutputStream(),可以在内存中模拟一个OutputStream
,toByteArray()
方法输出到数组中
4. 操作ZIP
ZipInputStream
5. 读取classpath资源
Class
对象的getResourceAsStream()
可以从classpath中读取指定资源;classpath路径默认“/”
6. 序列化与反序列化
ObjectOutputStream(OutputStream o)
,它负责把一个Java对象写入一个字节流,写入的Object需要实现Serializable
接口
反序列化ObjectInputStream
,readObject方法可以直接返回一个对象。通过网络传输的对象如果电脑上没有该类则无法完成反序列化。由于对象生成无需构造方法所以有一定的安全隐患。
7. Reader
所有字符输入流的超类,提供read()
方法,读取的单位时char。
FileReader
创建时可以传入编码(JDK高版本),CharArrayReader
和StringReader
可以在内存中模拟一个字符流。
Reader
是基于InputStream
构造的:可以通过InputStreamReader
在指定编码的同时将任何InputStream
转换为Reader
。
8. Writer
FileWriter
实现了文件字符流输出;CharArrayWriter
和StringWriter
在内存中模拟一个字符流输出。
Writer
是基于OutputStream
构造的,可以通过OutputStreamWriter
将OutputStream
转换为Writer
,转换时需要指定编码。
9. PrintStream和PrintWriter
PrintStream
是一种能接收各种数据类型的输出,打印数据时比较方便:
System.out
是标准输出;System.err
是标准错误输出。
PrintWriter
是基于Writer
的输出。
10. FIles工具类
// 快速读取文件
byte[] data = Files.readAllBytes(Paths.get("/path/to/file.txt"));
// 快速写入文件
byte[] data = ...
Files.write(Paths.get("/path/to/file.txt"), data);
11. BIO、NIO、AIO
BIO(同步并阻塞):一个网络请求开启一个线程进行处理,若连接不做任何处理仍会有线程开销。
NIO(同步非阻塞):让一个线程维护一个多路复用器即Selector(选择器,Java中的一个对象)进行轮询连接在选择器上的网络请求,当某一个网络请求发出事件时,选择器选择该请求进行处理。一个选择器维护的网络请求有上限。
AIO :异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
11. 注解(Annotation,元数据)
1. 元注解
作用:注解其他注解
@Target:描述注解的使用范围,可以将解参数变为数组
{ ElementType.METHOD, ElementType.FIELD }
- 类或接口:
ElementType.TYPE
; - 字段:
ElementType.FIELD
; - 方法:
ElementType.METHOD
; - 构造方法:
ElementType.CONSTRUCTOR
; - 方法参数:
ElementType.PARAMETER
。
- 类或接口:
@Retention:表示需要在什么级别保存该注释信息,即在什么时间才有效,一般用RUNTIME,用于描述其生命周期
- 仅编译期:
RetentionPolicy.SOURCE
;在编译期就被丢掉了 - 仅class文件:
RetentionPolicy.CLASS
;仅保存在class文件中,它们不会被加载进JVM - 运行期:
RetentionPolicy.RUNTIME
;会被加载进JVM,并且在运行期可以被程序读取。 - 如果
@Retention
不存在,则该Annotation
默认为CLASS
。因为通常我们自定义的Annotation
都是RUNTIME
,所以,务必要加上@Retention(RetentionPolicy.RUNTIME)
- SOURCE- - > CLASS- - > RUNTIME
- 仅编译期:
@Repeatable
使用@Repeatable
这个元注解可以定义Annotation
是否可重复。
@Documented:说明该注解被包含在javadoc中
@Inherited:说明子类可以继承父类的该注解,注解仅针对
ElementType.TYPE
有效
2. 注解创建
@Retention
@Target(...)
public @interface name{
//注解的参数:参数类型+参数名;
//default ""表示可以不加参数,没有就必须赋值
Sring name() default "";
//如果默认值为-1代表不存在
int id() default -1;
}
//使用
{
//如果只有一个,则可以省略value,直接写”“
@name(value = "")
public void test(){}
}
// 判断@Report是否存在于Person类:
Person.class.isAnnotationPresent(Report.class);
12. 网络编程
1. TCP编程
// 服务端接受连接
// ServerSocket用来监听该端口。
ServerSocket ss = new ServerSocket(6666);
// Socket实例就是用来和连接的客户端通信。
Socket sock = ss.accept();
// 客户端发起连接
Socket sock = new Socket("localhost", 6666);
// 从socket中读取数据:
InputStream in = sock.getInputStream();
// 写入数据到网络中:
OutputStream out = sock.getOutputStream();
注意使用flush强制输出缓冲区到网络
2. UDP编程
不像TCP一样需要维护一个流,它只需要创建一个数据包。
// 服务端接受数据包
DatagramSocket ds = new DatagramSocket(6666); // 监听指定端口
// 准备一个缓冲区用户接收数据
byte[] buffer = new byte[1024];
// 创建一个数据包传入创建的缓冲区,用receive函数,设置该连接用户接受数据的数据包。
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);
// 客户端发送数据包,可以调用connect指定连接的服务器和端口
DatagramSocket ds=new DatagramSocket(666, InetAddress.getByName("localhost"));
// 意思是后续接收UDP包时,等待时间最多不会超过1秒,否则在没有收到UDP包时,客户端会无限等待下去。
ds.setSoTimeout(1000);
byte[] data = "Hello".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length);
ds.send(packet);
// 完成后断开连接,它只是清除了客户端DatagramSocket实例记录的远程服务器地址和端口号,这样,DatagramSocket实例就可以连接另一个服务器端。
ds.disconnect();
3. HTTP编程
==请求:==
HTTP请求的格式是固定的,它由HTTP Header和HTTP Body两部分构成。
第一行:请求方法 路径 HTTP版本。例如,GET / HTTP/1.1
表示使用GET
请求,路径是/
,版本是HTTP/1.1
。
后续每一行就一对Header: Value
,例如常见的:
- Host:表示请求的域名,因为一台服务器上可能有多个网站,因此有必要依靠Host来识别请求是发给哪个网站的;
- User-Agent:表示客户端自身标识信息,不同的浏览器有不同的标识,服务器依靠User-Agent判断客户端类型是IE还是Chrome,是Firefox还是一个Python爬虫;
- Accept:表示客户端能处理的HTTP响应格式,
*/*
表示任意格式,text/*
表示任意文本,image/png
表示PNG格式的图片; - Accept-Language:表示客户端接收的语言,多种语言按优先级排序,服务器依靠该字段给用户返回特定语言的网页版本。
POST
请求通常要设置Content-Type
表示Body的类型,Content-Length
表示Body的长度。
==响应:==
响应的第一行总是HTTP版本 响应代码 响应说明
,例如,HTTP/1.1 200 OK
表示版本是HTTP/1.1
,响应代码是200
,响应说明是OK
。HTTP有固定的响应代码:
- 1xx:表示一个提示性响应,例如101表示将切换协议,常见于WebSocket连接;
- 2xx:表示一个成功的响应,例如200表示成功,206表示只发送了部分内容;
- 3xx:表示一个重定向的响应,例如301表示永久重定向,303表示客户端应该按指定路径重新发送请求;
- 4xx:表示一个因为客户端问题导致的错误响应,例如400表示因为Content-Type等各种原因导致的无效请求,404表示指定的路径不存在;
- 5xx:表示一个因为服务器问题导致的错误响应,例如500表示服务器内部故障,503表示服务器暂时无法响应。
URL url = new URL("http://www.example.com/path/to/target?a=1&b=2");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setUseCaches(false);
conn.setConnectTimeout(5000); // 请求超时5秒
// 设置HTTP头:
conn.setRequestProperty("Accept", "*/*");
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; MSIE 11; Windows NT 5.1)");
// 连接并发送HTTP请求:
conn.connect();
// 判断HTTP响应是否200:
if (conn.getResponseCode() != 200) {
throw new RuntimeException("bad response");
}
// 获取所有响应Header:
Map<String, List<String>> map = conn.getHeaderFields();
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
// 获取响应内容:
InputStream input = conn.getInputStream();
4. RMI远程调用
Java的RMI远程调用是指,一个JVM中的代码可以通过网络实现远程调用另一个JVM的某个方法。RMI是Remote Method Invocation的缩写。
// 继承Remote接口,接口内部本身无任何方法,主要作为一个标识作用。
public interface RemoteTestService extends Remote {
// 创建的需要被远程调用的方法
LocalDateTime getLocalDateTime(String zoneId) throws RemoteException;
}
// 服务端将方法暴露到网络
// 实例化一个WorldClock:
RemoteTest remoteTest = new RemoteTestService();
// 将此服务转换为远程服务接口:
RemoteTest skeleton = (RemoteTest) UnicastRemoteObject.exportObject(RemoteTest, 0);
// 将RMI服务注册到1099端口:
Registry registry = LocateRegistry.createRegistry(1099);
// 注册此服务,服务名为"WorldClock":
registry.rebind("RemoteTest", skeleton);
// 客户端调用该方法
// 连接到服务器localhost,端口1099:
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// 查找名称为"WorldClock"的服务并强制转型为WorldClock接口:
RemoteTest remoteTest = (RemoteTest) registry.lookup("RemoteTest");
// 正常调用接口方法:
remoteTest.test();
两个设备之间实际上调用的时Registry内部动态生成的。
Java的RMI严重依赖序列化和反序列化,而这种情况下可能会造成严重的安全漏洞,因为Java的序列化和反序列化不但涉及到数据,还涉及到二进制的字节码,即使使用白名单机制也很难保证100%排除恶意构造的字节码。
13. Java核心类
1. 字符串和编码
字符串比较equals(),要忽略大小写比较,使用equalsIgnoreCase()方法。
contains():是否包含字串
使用trim()
方法可以移除字符串首尾空白字符。空白字符包括空格,\t
,\r
,\n
:
另一个strip()
方法也可以移除字符串首尾空白字符。它和trim()
不同的是,类似中文的空格字符\u3000
也会被移除:
String
还提供了isEmpty()
和isBlank()
来判断字符串是否为空和空白字符串:
字符串替换
String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
// 使用正则表达式
String s = "A,,B;C ,D";
s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"
分割字符串
String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}
2. StringBuilder
普通String类由于无法不可变所以容易产生一些垃圾影响效率,使用StringBuilder可变类可以提升效率(支持链式操作)
链式操作的关键是方法返回this
StringBuffer线程安全,效率降低,接口相同,现在没必要使用。
String(线程安全,因为不可变只能读,即final域下的对象都是线程安全)被final修饰过, StringBuilder (线程不安全,性能只比buffer高 10%~15% )与 StringBuffer (线程安全)都继承自 AbstractStringBuilder 类
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
3. StringJoiner
// 第一个分隔符,第二个起始符,第三个结束符,后两个默认为空
var sj = new StringJoiner(", ", "Hello ", "!");
String.join()的本质就是使用StringJoiner
4. JavaBean
也称实体类,boolean
字段比较特殊,它的读方法一般命名为isXyz()
:,主要用来传输数据
5. BigInteger
当范围超过long之后建议使用BigInteger,其内部使用一个int数组来模拟大数。和Integer
、Long
一样继承自Number
6. BigDecimal
表示一个任意大小且精度完全准确的浮点数。
- scale,获取小数位数,返回负数如-2,则表示这是整数且末尾有两个0,setScale设置精度(第一个为精度,第二个为超过精度规则)
- stripTrailingZeros,去掉尾0
- divideAndRemainder,返回一个数组第一个为商,第二个为余数,两个都是BigDecimal类型
尽管除法不会丢失精度,但是存在无法除尽的情况,所以仍然需要设置精度以及如何截断。
使用equals方法比较要求scale也相等,可以使用compareTo来比较
BigDecimal本质也是用BigInteger表示整数和scale表示小数,其也是从Number继承。
7. 时间相关类
System.currentTimeMillis()
,获取当前时间戳。
- 一套定义在
java.util
这个包里面,主要包括Date
、Calendar
和TimeZone
这几个类; - 一套新的API是在Java 8引入的,定义在
java.time
这个包里面,主要包括LocalDateTime
、ZonedDateTime
、ZoneId
等。
1. Date
// 获取当前时间:
Date date = new Date();
System.out.println(date.getYear() + 1900); // 必须加上1900
System.out.println(date.getMonth() + 1); // 0~11,必须加上1
System.out.println(date.getDate()); // 1~31,不能加1
// 转换为String:
System.out.println(date.toString());
// 转换为GMT时区:
System.out.println(date.toGMTString());
// 转换为本地时区:
System.out.println(date.toLocaleString());
SimpleDateFormat
对一个Date
进行转换。它用预定义的字符串表示格式化:
- yyyy:年
- MM:月
- dd: 日
- HH: 小时
- mm: 分钟
- ss: 秒
假设当前月份是9月:
M
:输出9
MM
:输出09
MMM
:输出Sep
MMMM
:输出September
2. Calendar
Calendar
可以用于获取并设置年、月、日、时、分、秒,它和Date
比,主要多了一个可以做简单的日期和时间运算的功能。
// 获取当前时间:
Calendar c = Calendar.getInstance();
int y = c.get(Calendar.YEAR);
int m = 1 + c.get(Calendar.MONTH);
int d = c.get(Calendar.DAY_OF_MONTH);
int w = c.get(Calendar.DAY_OF_WEEK); // 从星期日开始
int hh = c.get(Calendar.HOUR_OF_DAY);
int mm = c.get(Calendar.MINUTE);
int ss = c.get(Calendar.SECOND);
int ms = c.get(Calendar.MILLISECOND);
System.out.println(y + "-" + m + "-" + d + " " + w + " " + hh + ":" + mm + ":" + ss + "." + ms);
// 2021-6-21 2 10:44:36.219
操作Clendar之前需要clear()函数清楚所有字段,通过getTime可以转换成Time对象,然后就可以进行格式化了。
8. 其它工具类
- Math:数学计算
- Random:生成伪随机数
- SecureRandom:生成安全的随机数,强烈建议使用根据外部物理条件产生的随机数。
读取Properties
Properties
本质就是一个HashTable。Properties
读取配置文件,一共有三步:
- 创建
Properties
实例; - 调用
load()
读取文件; - 调用
getProperty()
获取配置。
String f = "setting.properties";
Properties props = new Properties();
props.load(new java.io.FileInputStream(f));
// 防止中文乱码
props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));
String filepath = props.getProperty("last_open_file");
// 第二个参数为读取为空时返回的默认值
String interval = props.getProperty("auto_save_interval", "120");
Properties可以多次load,后面的会覆盖前面的,根据此特性可以控制配置文件的先后。
写配置文件:
Properties props = new Properties();
props.setProperty("url", "http://www.liaoxuefeng.com");
props.setProperty("language", "Java");
props.store(new FileOutputStream("C:\\conf\\setting.properties"), "这是写入的properties注释");
2. JVM
1. 内存区域划分
JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代
JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代 。
JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
1. JVM栈(Java Virtual Machine Stacks)
JVM中每一个线程就会有一个独立的线程栈
与之匹配,栈中数据私有,每一个栈含有一个一个的栈帧
,每一个执行的方法都会形成一个栈帧,其包括局部变量、程序运行状态、方法返回值、方法出口
等信息。
2. 本地方法栈(Native Method Stacks)
执行native方法
,也有栈帧。
3. 程序计数器
唯一一个不会出现 OutOfMemoryError 的内存区域。随着线程创建而创建,死亡而死亡。记录着下一句代码的地址。
4. 方法区(Method Area)
属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码
等数据。
常量池(Runtime Constant Pool)
:用于存放编译期生成的各种字面量和符号引用。
5. 堆(heap)
堆是所有线程共享的,主要是存放对象实例
和数组
。处于物理上不连续
的内存空间,只要逻辑连续
即可。
栈是运行时单位,代表了处理逻辑。堆是存储的对象,代表了处理数据。对象的属性存储在堆中,对象的方法存放在栈中。
2. JVM类加载
类加载器(ClassLoader)
编译”.java
“文件后会生成”.class
“文件,该文件在开头会有特定的标识,类加载器负责将”.class
“文件加载进内存,并将这些内容转换成方法区中的运行时数据结构。类加载器只负责”.class
“文件的加载,具体是否执行由Execution Engine来决定。
不同类的加载可能也是由不同的类加载器进行加载:
BootStrap ClassLoader
:rt.jarExtension ClassLoader
: 加载扩展的jar包App ClassLoader
:指定的classpath下面的jar包Custom ClassLoader
:自定义的类加载器
究竟使用何种类加载器加载由双亲委派机制
决定:
如果我们需要加载一个类,他首先会递交给它的父类加载器,只有当父类加载器无法加载时才会返回给该类加载器加载。
这是一种隔离作用,避免我们的代码直接影响JDK的代码,比如我们自定义一个String类,将该类导入,去使用它,我们仍然会加载JDK提供的String。
类加载器加载文件步骤如下图所示:
1=>operation: 加载
2=>operation: 链接
2.1=>operation: 验证
2.2=>operation: 准备
2.3=>operation: 解析
3=>operation: 初始化
4=>operation: 使用
5=>operation: 卸载
1(right)->2(right)->3(right)->4(right)->5(right)
1. 加载
- 将class文件加载到内存
- 将静态数据结构转化成方法区中运行时的数据结构
- 在堆中生成一个代表这个类的 java.lang.Class对象作为数据访问的入口
2. 链接
1. 验证
确保加载的类符合 JVM 规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的事件,其实就是一个安全检查
2. 准备
为static变量在方法区中分配内存空间,设置变量的初始值,例如 static int a = 3 (注意:准备阶段只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量是对象初始化时赋值的)
3. 解析
虚拟机将常量池内的符号引用替换为直接引用的过程(符号引用比如我现在import java.util.ArrayList这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行)
3. 初始化
初始化其实就是执行类构造器方法的<clinit>()
的过程,而且要保证执行前父类的<clinit>()
方法执行完毕。这个方法由编译器收集,顺序执行所有类变量(static修饰的成员变量)显式初始化和静态代码块中语句。此时准备阶段时的那个 static int a
由默认初始化的0变成了显式初始化的3. 由于执行顺序缘故,初始化阶段类变量如果在静态代码块中又进行了更改,会覆盖类变量的显式初始化,最终值会为静态代码块中的赋值。
注意:字节码文件中初始化方法有两种,非静态资源初始化的
<init>
和静态资源初始化的<clinit>
,类构造器方法<clinit>()
不同于类的构造器,这些方法都是字节码文件中只能给JVM识别的特殊方法。
4. 卸载
GC将无用对象从内存中卸载
3. JVM对象创建
1=>operation: 类加载检查
2=>operation: 分配内存
3=>operation: 初始化零值
4=>operation: 设置对象头
5=>operation: 执行init方法
1(right)->2(right)->3(right)->4(right)->5(right)
对象内存布局

1. 对象头
markword:

类型指针:
通过此指针找到该对象的描述、内部的方法以及属性入口。
2. 实例数据
对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容
3. 对齐填充
Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,对象头正好8字节。
1. 类加载检查
虚拟机每遇到一条new指令,就检查常量池定位符号引用
2. 分配内存
分配内存主要有两种方式:
指针碰撞
原理:一边用过,一边没有过,中间有一个指针,新内存的分配只需要移动指针即可。
空闲列表
虚拟机维护了一个空闲列表
,分配内存时只需要修改空闲列表即可。
选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配并发问题:
- CAS+失败重试:CAS是乐观锁的一种实现方式。
- TLAB(Thread Local Allocation Buffer,即线程本地分配缓存区):为每一个线程预先在
Eden 区
分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。
3. 初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段(成员变量)在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4. 设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
5. 执行init方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init>
方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
对象的定位访问
1. 句柄

如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
2. 直接指针

如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
4. 垃圾回收
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)
.
1. 堆空间的基本结构

From区和To区是一个相对的区域,对象从From区到To区后,From区就变成了To区,相对的To区就变成了From区。
新生代分配比例,8:1:1
2. GC流程
- 对象在Eden区分配内存
- 在经历一次垃圾回收之后,如果对象仍然存活,对象进入From区,对象年龄变为1。
- 之后对象从From区进入To区,完成一次GC,对象年龄加1。
- 当对象年龄增加到15岁(默认)时,对象就会进入老年代。
空间分配担保机制:在发生Minor GC之前,虚拟机检查老年代的最大连续可用空间
是否大于所有新生代对象的总大小
。如果大于代表此次GC绝对安全
,若小于代表此次GC有一定风险
,虚拟机会检查HandlePromotionFailure
是否设置为True,若是,虚拟机会进行一次大胆尝试Minor GC
,设置为false,虚拟机为保险起见直接进行一次Full GC
。
大对象直接进入老年代,这样做的目的是为了避免在幸存者区复制导致效率降低。
3. GC分类
- Partial GC:
- Minor GC / Young GC:收集新生代对象,当Eden区满后触发
- Major GC / Old GC,收集老年代对象
- Mixed GC,收集整个新生代对象和部分老年代对象
- Full GC,收集整个堆,包括Yong Gen,Old Gen,Perm Gen(如果存在)。
在有些语境下Major GC 和 Full GC指代相同。
4. GC算法
1. 标记清除法(Mark—Sweep)
两次扫描空间,第一次标记活着的对象,第二次清除未标记的对象。
- 优点:不需要额外空间
- 缺点:两次扫描,严重浪费时间,会产生内存碎片
2. 标记压缩法(Mark-Compact)
对标记清除法的一种改进,主要解决内存碎片问题,在经历多次标记清除法之后进行一次压缩,尽管解决了内存碎片问题,但是压缩时由于会进行对象的移动,所以会有性能的丢失。
3. 复制算法(Copying)
开始时将空间分成一个对象面
和一个空闲面
。程序从对象面分配空间,当对象面满了之后进行一次扫描将活动的对像复制到空闲面。原对象面就会变成空闲面。对象面和空闲面在Java中有一个新的名字叫From区
和To区
。
4. 分代回收策略(Generational Collecting)
实际上有些对象存活率不同,对象大小不同。根据这种特性综合上述算法而产生这种分代回收策略。
新生代中由于对象存活时间短并且需要经常进行GC,所以采用复制算法。老年代中对象存活时间较长且不经常进行GC,所以采用标记复制或者标记压缩。
5. GC调优
1. GC调优原则
上线之前就应该将JVM参数设置到最优
- 减少创建对象的数量
- 减少使用全局变量和大对象
- 大部分是代码问题而非参数设置问题
2. GC调优目的
- 将转移到老年代的对象数量降低到最小
- 减少 GC 的执行时间
3. GC调优策略
- Full GC成本远高于Minor GC,因此尽可能将对象分配在新生代,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。
- 大对象直接进入老年代,-XX:PretenureSizeThreshold设置
- 合理设置进入老年代对象的年龄,-XX:MaxTenuringThreshold 设置对象进入老年代的年龄大小,减少老年代的内存占用,降低 full gc 发生的频率。
- 设置稳定的堆大小,堆大小设置有两个参数:
-Xms
初始化堆大小,-Xmx
最大堆大小。 - 不需要GC优化
- MinorGC 执行时间不到50ms;
- Minor GC 执行不频繁,约10秒一次;
- Full GC 执行时间不到1s;
- Full GC 执行频率不算频繁,不低于10分钟1次。
6. 对象死亡
在执行垃圾回收之前需要判断对象是否死亡。
1. 死亡判断算法
1. 引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
循环引用问题:由于两个对象相互引用导致引用计数器始终不为零导致无法被GC
2. 可达性分析算法
以GC Roots
为起点,从这些节点开始向下搜索,节点走过的路径被称为引用链
,如果GC Roots没有任何引用链时则代表对象是不可用的。
在Java中,可作为GC Root的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
在可达性分析算法中不可达的对象,并不是直接被回收,这时它们处于缓刑状态,至少需要进行两次标记才会确定该对象是否被回收。
如果发现不可达之后将执行第一次标记,标记之后紧接着会进行一次筛选,筛选的条件是是判断此对象是否有必要执行
finalize()
方法,此方法可以将此对象与GC Roots建立连接
。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
2. 引用
强引用(StrongReference):垃圾回收器绝不会回收它,宁愿程序终止
软引用(SoftReference):只有内存不够的时候才会被回收,被回收了就加入引用队列。
弱引用(WeakReference):垃圾回收器一旦发现就会回收,加入引用队列,但是垃圾回收器线程优先级很低,不一定会发现。
虚引用(PhantomReference):虚引用并不会决定对象的生命周期,任何时候都会被回收,且必须和引用队列联合使用
虚引用主要用来跟踪对象被垃圾回收的活动,观察引用队列是否有其虚引用
一般都用软引用,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
3. 方法区的GC
1. 常量池的GC
只要常量池中的常量没有任何对象引用,那么他就是废弃常量,如果这时发生内存回收的话而且有必要的话,它就会被系统清理出常量池了。
2. 无用类
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的
ClassLoader
已经被回收。 - 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
7. 垃圾收集器
1. CMS收集器(Concurrent Mark Sweep)
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字上我们就能看出这个收集器使用的标记清除算法
:
- 初始标记:Stop the world,即暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快。
- 并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象。无法保证实时性,可能遗漏。
- 重新标记:为了修正并发标记所遗漏的,时间比初始标记长,远比并发标记短。需要Stop the world
- 并发清除:开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
从上面可以知道该GC收集器有以下缺点:
- 对 CPU 资源敏感;
- 无法处理浮动垃圾;
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
2. G1收集器 (Garbage-First)
G1收集器是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
该收集器的特点:
- 并行与并发
- 分代收集
- 空间整合:标记整理
- 可预测的停顿
收集器运作步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
3. Java并发编程

1. 线程
1. 线程创建与开启
1. 继承Thread
创建Thread子类,重写run
方法,调用start
方法开启线程。
Thread thread = new MyThread();
thread.start();
本质上仍然是实现Runnable
2. 实现Runnable
编写线程实现Runnable接口,将线程放入Thread中。
Thread thread = new Thread(new MyThead());
thread.start();
3. 实现Callable
编写线程实现Callable接口,使用该类作为参数创建FutureTask对象
,再将FutureTask对象作为参数创建Thread对象。可以通过FutureTask的get方法获取线程结果。
FutureTask futureTask = new FutureTask<>(new MyThead());
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
相较于前面两种实现他有两个好处,一个是可以
返回对象
,另一个是可以做异常处理
尽管线程对象直接调用run()方法也能够顺利执行,但是实际上它并没有开启一个线程,只是单纯的作为一个普通方法执行。
2. 线程相关方法
1. 线程终止
尽管官方提供一些线程停止的相关方法,但是可以看多里面有许多都已经被废弃了。比如stop,destory
。
推荐线程自己停止。线程没有停止是因为线程内部被阻塞或者正在执行,比如有一个while循环,我们可以设置一个标志位(建议使用volatile修饰)
来结束循环,使得线程可以结束。
interrupt()
:线程不会立即中断,它只是修改一个标志位
为true标志该线程处于中断状态,通过isInterrupted方法返回标志位,具体的中断由用户处理
。如果用户处于wait,sleep等时,如果线程检测到处于中断状态,那么线程将会抛出异常InterruptedException
,抛出异常后线程将会进入实际的中断。
volatile
关键字解决了共享变量在线程间的可见性问题,使得获取的数据一定是最新的。
2. 线程暂停
- sleep是线程
Thread
提供的方法,调用sleep不会释放对象锁,由于没有释放对象锁,所以也不能调用里面的同步方法。 - wait是Object类中提供的方法,调用wait会释放对象锁,在调用
notify()
方法唤醒后会重新获取锁。使用this.notifyAll()
将唤醒所有当前正在this
锁等待的线程,相较于notify它更安全,可以防止因为考虑不周而只释放了一个锁,释放的锁也是随机的具有极大的不确定性。
2. yield(礼让)
线程停止,释放cpu资源,线程将处于就绪态,接下来线程的调度仍然由cpu选择,所以礼让不一定成功。
3. join(插队)
等待该线程终止,才执行之后的代码,但是并不会暂停之前开启的线程。
3. 线程调度算法
- 分时调度模型
- 抢占式调度模型
线程优先级1~10,数字越大,优先级越高
4. 线程分类
用户线程
守护线程
:后台记录操作日志,监控内存,垃圾回收等等。使用setDaemon()
(需要在star方法之前设置)方法将线程变成守护线程,守护线程在用户线程死亡后就会伴随一起死亡。
虚拟机必须确保用户线程执行完毕
虚拟机不用等待守护线程执行完毕
2. 线程池

1. Executor框架组成
- 任务(Runnable/Callable):线程池执行的任务需要实现
Runnable/Callable
接口 - 执行(Executor)
- 结果(Future):调用
submit()
方法时会返回一个FutureTask
对象
2. ThreadPoolExecutor
ThreadPoolExecutor的相关参数
:
- corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;
- maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数,最大数由workQueue任务队列的类型决定
- workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。它一般分为:
- 直接提交队列:没有容量,每执行一个插入就会阻塞,每一个删除就会被唤醒
- 有界任务队列:
- 无界任务队列:易导致OOM
- 优先任务队列几种;
- keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
- unit : keepAliveTime 参数的时间单位。
- threadFactory :executor 创建新线程的时候会用到。
- handler :饱和策略\拒绝策略:
- AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
- CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
- DiscardPolicy: 不处理新任务,直接丢弃掉。
- DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求

底层原理:
3. 几个常见对比
1. Runnable和Callable
如果任务不需要返回结果或抛出异常推荐使用Runnable 接口,这样代码看起来会更加简洁。
Executors工具类可以实现Runnable转成Callable。
2. execute和submit
- execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
- submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功 ,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后
立即返回
,这时候有可能任务没有执行完。
3. shutdown和shutdownNow
- shutdown() :关闭线程池,线程池的状态变为
SHUTDOWN
。线程池不再接受新任务了,但是队列里的任务得执行完毕。 - shutdownNow() :关闭线程池,线程的状态变为
STOP
。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
4. isTerminated和isShutdown
- isShutDown 当调用 shutdown() 方法后返回为 true。
- isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true。
3. 多线程同步
1. 锁
1. 锁分类
- 乐观锁,悲观锁(CAS):是否锁住同步资源。
- CAS算法,硬件级别,比较与交换,比较主内存的值
- ABA问题:A改了值,B给改了回来,于是线程感知不到第一次操作,解决方法,加个版本号。存在意义:阻塞时间小于自旋成功时间
- 读锁(共享锁),写锁(排它锁):能否共享同一把锁,只能读不能修改
- 自旋锁,自适应性自旋(根据上一次成功次数调整这次自旋次数,如果随着自旋次数增加几率越来越低,就进行降级),非自旋锁
- 无锁,偏向锁,轻量级锁,重量级锁
- 分布式锁
- 区间锁(分段锁) java.util.concurrent concurrentmap
- 重入锁,非重入锁:是否能获取同一把锁
- 公平锁(),非公平锁(先去判断是否可以获取到锁,不管等待队列是否有进程):竞争时是否排队
- 隐式锁(sys,基于jvm内置锁,无需在代码中手动添加),显式锁(需要手动编写)
2. synchronized和Reentrantlock
1. synchronized关键字
修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
synchronized void method() {
//业务代码
}
修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
synchronized void staic method() {
//业务代码
}
修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得当前class 的锁
synchronized(this) {
//业务代码
}
双重校验锁实现对象单例(线程安全)
public class Singleton {
// 使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
构造方法本身就属于线程安全的,不存在同步的构造方法一说。
2. synchronized底层原理
synchronized同步语句块
:在同步语句块的前后是monitorenter
和monitorexit
指令
当执行 monitorenter 指令时,线程试图获取锁也就是获取对象监视器monitor
的持有权。
在执行monitorenter
时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
在执行 monitorexit
指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized修饰方法
:编译后会给方法添加一个ACC_SYNCHRONIZED
访问标志,该方法来辨别一个方法是否为一个同步方法。