数据类型
基本数据类型
Java的基本类型有: char(2字节), byte(1字节), short(2字节), int(4字节), long(8字节), float(4字节), double(8字节), boolean(-),
Java没有bit类型, 但可以使用BitSet
类代替.
- byte: 1字节, 范围-128~127
- short: 2字节, 范围-32768~32767, 为什么最小是-32768 ?
- int/long:
100L
表示long类型, 0x/0/0b前缀分别表示16/8/2进制- 如果
long l = 3600 * 24 * 30 * 1000
,1000后面不加L,右边会按int计算并产生溢出
- 如果
- float/double: 3.14F表示float类型, 3.14和3.14D都表示double
- char: 单引号, ‘\u2122’或’A’
浮点数的比较
浮点数(基本类型)之间的等值判断,基本数据类型不能用==来比较,浮点数包装数据类型不能用 equals 来判断。
// 反例1: |
BitSet
BitSet bits = new BitSet(16); // 初始大小对性能的影响 |
包装器
包装器API
基本类型对应包装器为Character, Byte, Short, Integer, Long, Float, Double, 包装器与基本类型互转:
Integer ii = Integer.valueOf(1); |
Double类的一些方法:
Double.compareTo(Double)
: 大于小于直接比较, =的判断是把double转成一个LongBit? Native方法, 需要看一下浮点数的内存isNaN()
返回true表示不是正常数字, 比如除以0, 负数的平方根. 代码里如何得到一个NaN?
装箱拆箱的实现
▶ 何时发生装箱/拆箱:
- 什么是自动装箱: int → Integer, 实际调用
Integer.valueOf(int)
- 什么时候发生自动装箱:
- 创建对象:
Integer i = 3
- 方法参数传递:
void method(Integer i)
- 创建对象:
- 什么是自动拆箱: Integer → int, 实际调用
integer.intValue()
- 什么时候发生自动拆箱:
- 加法:
integer1 + integer2
, 先拆箱转换为int … - 需要注意的是
if (integer3 == integer1 + integer2)
, 首先右边1和2拆箱为int, 变成if (integer3 == int)
, 这时不是发生(int→integer)装箱, 而是继续拆箱, 最终比较的是if (int == int)
- 加法:
▶ Integer/Long自动装箱 valueOf(x)
的实现
- Integer/Long的
valueOf(i)
使用了享元模式, 在static
代码块中预先创建了范围-128~127
的对象, 缓存在Cache里; - 当调用
valueOf(i)
的时候,先判断i的范围是否是-128~127,如果是则直接从cache里返回对象,减少类的创建; - 下面创建Integer的效率, 前者可能更高:
Integer i = 3
,Integer i = new Integer(3)
; - Float/Double的
valueOf(f)
没有使用享元模式;
▶ 代码example
Long l1 = Long.valueOf(128); |
输出: false, true, false, true
慎用Long.equals()
以下代码会输出false:
System.out.println(new Long(1).equals(1)); |
原因是,Long.equals(Object)
,进入equals是会对整形参数1进行一次装箱,i被包装成Integer(1)
,
和其他类的equals行为一样,Long.equals(Integer(1))
会先判断输入参数的类型if (obj instanceof Long)
,这里就返回false了。
所以用Long的正确条例是,Long
的方法传参数都用明确的long型:new Long(1L)
, longObj.equals(1L)
。
BigInteger, BigDecimal
Java还提供了两个用于大数运算的类: BigInteger
(任意大整数)和BigDecimal
(任意大小的带小数点的数字). 常用方法: add()
, subtract()
, multiply()
, divide()
BigInteger big1 = new BigInteger("99"); |
BigDecimal 的等值比较应使用 compareTo()方法,而不是 equals()方法。 说明:equals()方法会比较值和精度(1.0 与 1.00 返回结果为 false),而 compareTo()则会忽略精度。
数组
- Java中数组本质上也是对象, 拥有所有Object的方法, 不同于int/double等基本类型.
- Java对象在内存里前几个字节是”对象头”, 非数组对象的的对象头占用2字节, 数组对象的对象头占用3字节, 多的1字节用来存储对象长度
- 数组可以通过属性
length
获取长度, 遍历数组:for(int i = 0; i < array.length; i++)
- 数组创建后会记住元素类型和大小, 所以:
A[]
类型的数组可以强转换为Object[]
, 但不能反过来执行;- 用
new A[1]
方式创建的数组, 只能向内存储A
类型或者A的派生类
的对象, 试图存入其他类型对象会抛ArrayStoreException; - 数组创建后不再能改变长度;
▶ 数组如果作为形参 or 返回值, 可以使用Object
, 而不是用Object[]
:
// 反射方式创建新数组 |
▶ 数组与list互转:
//list -> array |
Arrays
Java核心类库有两个Arrays类:
java.lang.reflect.Array
: 提供了数组的反射相关方法;java.utils.Arrays
: 类似Collections类, 提供了merge/sort等方法
示例代码: 用反射创建数组, 拷贝数组:
// java.lang.reflect.Array创建数组 |
java.util.Arrays
java.util.Arrays
包含了许多处理数组的实用方法:
asList
: 将一个数组(变长参数的语法糖实现就是数组)转变成一个List(确切的来说是ArrayList
),注意这个List是定长的,企图添加或者删除数据都会报错(java.lang.UnsupportedOperationException
).List<Integer> list = Arrays.asList(3,4,2,1,5,7,6);
// 下面这种用法是错误的:
int a[] = new int[]{1,2,5,4,6,8,7,9};
List list = Arrays.asList(a);sort
: 对数组进行排序。适合byte,char,double,float,int,long,short等基本类型,还有Object类型(实现了Comparable接口),如果提供了比较器Comparator也可以适用于泛型。void sort(Object[] a); // 需要类实现Comparable接口
void sort(T[] a, Comparator<? super T> c); // 带比较器binarySearch
: 通过二分查找法对已排序(譬如经过Arrays.sort排序,且按照升序进行排序。如果数组没有经过排序,那么检索结果未知)的数组进行查找。适合byte,char,double,float,int,long,short等基本类型,还有Object类型和泛型copyOf
: 数组拷贝,并返回新数组,底层采用System.arrayCopy(native方法)实现。copyOfRange
: 数组拷贝,指定一定的范围,String str2[] = Arrays.copyOfRange(arr,1,3)
;equals
和deepEquals
:- equals:判断两个数组的每一个对应的元素是否equals
- deepEquals:主要针对一个数组中的元素还是数组的情况
toString
和deepToString
: 参考equals
和deepEquals
hashCode
和deepHashCode
:- hashCode:计算一个数组的hashCode. 每个元素的
element.hashCode()
都要参与计算
- hashCode:计算一个数组的hashCode. 每个元素的
fill
: 给数组赋值。填充数组。Arrays.fill(intArr, 1);
Java.lang.reflect.Array
施工中
枚举
- Java在SE5中才添加了emum特性, 在定义一个enum时会自动创建
toString()
和value()
方法(均是static方法), enum还支持类似Objec的私有属性,和构造;- enum 类型不支持
public
和protected
修饰符的构造方法, 因此构造函数一定要是private
或friendly
的. 也正因为如此, 所以枚举对象是无法在程序中通过直接调用其构造方法来初始化的. - 枚举可以出现在switch语句中, 若要判断两个枚举类型常量的值是否相等, 使用
==
, 或equals()
都可以. 前者更好因为可以可以判断null的情况 - 比较两个枚举类型常量的值的大小要使用
compareTo()
方法.
- enum 类型不支持
// 一个基本的枚举: |
运算符
- 赋值: 类实例的赋值操作
a=b
实际是把b这个”对象引用”指向了a的指向的对象, 如果b原来的对象的引用数为0, 在一定条件下会被JVM销毁. - 对于基本数据类型,
==
判断的是值, 而不是”是否指向同一个引用”; - 用
==
比较Object, 如果a和b是否指向的是同一块内存则为true - 判断两个字符串的内容是否相同不能用
if(str1==str2)
, 要用str1.equals(str2)
方法. - 大部分jdk中的类实现了
Object.equals(Object)
这个方法(判断两值是否相等), 但是对于某些自定义的类要留意其equals
方法, 因为Object.equals
默认行为是比较引用的this==obj
;
hashCode和equals更多参考: (五)面向对象
左右结合
Java中赋值=
, 单目运算++
等, 条件运算符?:
是右结合, 其他都是左结合,
比如x=y=z
, 相当于x=(y=z)
位移运算
- 左移<< : 丢弃最高位(符号位同样丢弃), 0补最低位. 当byte和short左移时, 自动升级为int型.
- 数学意义: 左移n位相等于乘以2^n
- 右移>> : 高位补充符号位, 正数右移补充0, 负数右移补充1, 当byte和short右移时, 自动升级为int型.
- 数学意义: 右移n位相当于除以2^n
- 无符号右移>>> : 无论正负, 高位补充0
- 无符号右移只是对32位和64位的值有意义
关于补码/反码参考脚注1
java.lang.Math
- abs:
return v>0?v:-v;
- sqrt: native
- pow: native
控制流程和语句
- Java的
if
,for
,while
,do-while
,if...else if
和C++完全一样, 此外Java还多了foreach:for(int i : integerArray) {...}
switch
语句支持String类型和enum
类型
方法
- Java的参数传递为
值传递
. 也就是说, 当我们传递一个参数时, 方法内将获得该参数的一个拷贝. - 基本类型(int/char等)的参数传递, 方法内获得是一个拷贝. Java方法对变量的修改不会影响到原变量.
- 对象类型作为形参传递, 函数内获得一个引用的拷贝.
- Java不能实现C/C++中的swap功能
对象类型都是通过引用拷贝(跟C++中引用不同)传参, 通过该引用能够更改其指向的对象内部值, 如果只是更改该引用值, 仅对函数内部可见, 函数外部的实参依然没有改变;
Swap
Java对普通类型的变量 or 引用类型的变量, 都无法简单通过=
赋值实现 Swap,
折中的做法有: 使用数组, 作为成员变量
public static void swap1(int[] data, int a, int b) { |
变参函数
Java也支持变参函数:
void foo(String[] args) { //第一种形式 |
面向对象
Object的一些默认方法
Object obj = new Object(); |
equals方法
Object的equals方法默认是比较引用地址. equals方法的特点:
- 自反性: a.eq(a)==true
- 对称性: if a.eq(b)==true, then b.eq(a)==true
- 传递性: a->b, b->c, a->c
所以伪码如下:
if super.equals==false false |
hashCode方法
hashCode()
返回int类型, 返回值可以看成是对象的”消息摘要”
比较equals和hashCode
- 如果重新了equals方法, 就必须重写hashCode方法, 以便可以将对象插入到HashMap中(摘自Java核心技术卷1, 为什么?)
- 如果两个对象equals, 那么hashCode一定相同, 如果两个对象hashCode相同, 但不一定equals, 为什么?
- equals要依次比较每个属性的值, hashCode是对”需要比较的属性”求散列, 所以如果哈希方法不够好出现碰撞, hashCode相同但是每个属性不equals
- 因为HashMap插入时用Key的hashCode作为数组的下标, 所以hashCode返回必须是正int
- 好的hashCode方法应该对”需要比较的每个属性”充分散列
clone
Object.clone默认是浅拷贝;
Cloneable接口
Cloneable
和Serializable
一样都是标记型接口,它们内部都没有方法和属性,implements Cloneable
表示该对象能被克隆,能使用Object.clone()
方法。
如果没有implements Cloneable
的类调用Object.clone()
方法就会抛出 CloneNotSupportedException
Example:
public class Example implements Cloneable { |
Example类的 clone()
默认调用了 Object.clone()
, 这是一个Native方法, 默认是 浅克隆(shallow clone)
浅拷贝(浅克隆)复制出来的对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。
深拷贝(深克隆)复制出来的所有变量都含有与原来的对象相同的值,那些引用其他对象的变量将指向复制出来的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。
如何实现 deep clone:
clone方法里要对每个引用类型的成员都调用一次 clone()
, 例子:
class Car implements Cloneable { |
使用Serializable实现深克隆(deep clone):
略
构造和销毁
构造器(constructor):
Java的构造器实际上是一个static函数, 因为在没有实例化之前就可以调用构造, 但是一般来说, static方法里不能使用this
关键字, 因为this
的含义是指向类实例本身的一个引用(C++的this是指向类实例自身的指针), 但是构造器这个特殊的static方法里却可以使用this
关键字.
继承和构造顺序
派生类被实例化时, 总是先调用super()
, 即基类的默认构造方法. 在派生类的构造函数中, 也可以使用super(args...)
调用指定的基类构造方法.
Class A { |
默认构造方法
如果一个类没有定义任何构造方法, 那么编译器会为这个类自动生成一个不带参数的默认构造方法
,
销毁
- Java允许在类中定义一个
finalize()
方法, 这个方法里可以做什么? JVM何时调用这个方法? - Efftive Java中提到
finalize()
方法可用作”守卫方法”, 比如socket在这里做最后的关闭检查:
protect void finalize() { |
this关键字
- 在调用Java的方法时, 会隐式的将”指向自身的引用”作为方法的第一个参数
function(this, param)
, C++的this是”指向类实例自身”的指针; - static方法的第一个参数则是null.
访问控制权限
- 没有任何权限修饰, 默认是包内可见, friendly的;
- 访问权限 public > protected > friendly > private
- protected: 包可见, 子类可见;
- friendly: 包可见, 子类不可见 (没有这个关键字, 什么都不加默认是friendly);
- private: 只有同一类型可见;
继承
多重继承
- Java不支持多重继承class, 但支持多重继承interface. 思考一个问题:
“有两个类B1和B2继承自A. 假设B1和B2都继承了A的方法并各自进行了覆盖, 编写了自己的实现. 假设C通过多重继承继承了B1和B2, 那么C应该同时继承B1和B2的重载方法, 那么它应该继承哪个的呢?是B1的还是B2的呢?”
C++中经常会掉入这个陷阱, 虽然它也提出了替代的方法来解决这个问题. 我们在Java中就不会出现这个问题. 就算两个接口拥有同样的方法, 实现的类只会有一个方法, 这个方法由实现的类编写. 动态的加载类会让多重继承的实现变得困难.
因为在C++没有Interface, 在C++中使用”虚拟继承”解决上面的问题:
- B和C去虚拟继承A:
class B : public virtual A
,- D多重继承A和B:
class D : public B1, public B2
;
抽象类和接口
- 含有抽象方法(abstract function)的类是抽象类(abstract class).
- 任何子类都必须实现抽象类的抽象方法, 或者自身也声明为抽象类;
抽象类public abstract class A
, 和接口的异同:
- 抽象类和接口都能有自己的属性成员, 不同的是接口中的成员属性都是static和final的, 因此比较合适的做法是在interface里放置一些常量.
- 抽象类里还可以定义自己的方法实现, 并能被派生类继承, 但接口不能含有任何方法实现.
Java和C++实现多态的对比
C++ | Java |
---|---|
virtual func | 普通方法 |
virtual f()=0 | abstract func() |
abstract class | interface |
多态(polymorphism)
多态的含义就是一个方法多种实现, 分静态和动态, 在同一个类中实现多态是通过函数重载
-Overload, 在继承中实现多态是通过运行时绑定
.
- 在Java的继承中, 除了static和final方法(private也是final的)之外, 其他的方法都是
运行时绑定
的, - 类的属性成员并不在多态的讨论范围内, “多态”仅仅指方法的多态. 比如基类和派生类都有field属性, 那么在派生类实例中, 将包含两个field, 通过
基类.field
也只能访问基类的field, 因为 属性没有多态. - 类的构造方法不具备多态性, 因为类的构造器默认是static属性的, 对比C++的构造也不具备多态性(C++通过虚函数实现), 原因是构造期间尚未生成虚函数表.
- 在派生类中, 覆写(Override)基类的私有方法不会编译报错, 但不会照期望的执行, 结论就是: 只有非private方法才可以被派生类覆写(Override).
- Override和Overload都可以看成是多态性的表现, 前者是基类和派生类之间的多态, 后者是一个类内部的多态表现. // 疑似C++理论
final, static 关键字
final
Java中的final
关键字和C++中的const
关键字一样, 都表示不可改变.
final关键字可以修饰:
- 成员: 表示常量, 也可以在final成员定义时不给初值, 在构造方法里赋初值;
- 形参: 表示这个参数引用指向的内容不能被改变.
- 方法: 表示这个方法不能在派生类中被”覆写”(Override), 但可以被继承使用. 类中所有private方法都被隐式的声明为final的.
- 类: 表示这个类不能被继承, final类中所有的方法也被隐式声明为final的, 设计类时候, 如果这个类不需要有子类, 类的实现细节不允许改变, 并且确信这个类不会载被扩展, 那么就设计为final类. final和abstract这两个关键字是反相关的, final类就不可能是abstract的
- C++的const类成员和Java的final类属性: 在C/Java的类中, 都支持
public final int ee = 1
这样的声明+赋初值的方式, 也支持先声明再初值的方式(这种情况下, 都需要在构造函数里初值). 这样的设计的好处是可以做到一个类中final域在不同的对象有不同的值.
private final List Loans = new ArrayList(); |
下面总结了一些使用final关键字的好处:
- final关键字提高了性能, JVM和Java应用都会缓存final变量.
- final变量可以安全的在多线程环境下进行共享, 而不需要额外的同步开销.
- 使用final关键字, JVM会对方法/变量及类进行优化.
摘自《Java编程思想》第四版第143页:
“使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。“
static
使用static块初始化final的Map:public class Test {
private static final Map<Integer, String> myMap;
static {
Map<Integer, String> aMap = ....;
aMap.put(1, "one");
aMap.put(2, "two");
myMap = Collections.unmodifiableMap(aMap);
}
}
回顾C++的const
可以修饰函数(修饰返回值or修饰形参or修饰类的函数成员),
const int ptr; // ptr指向的内容无法修改
int const ptr; // 指针ptr本身的值无法被修改
修饰形参: void func(const int *ptr);
修饰返回值: const &aaa func(void); //
修饰类的函数成员: void func(int, int) const; // 函数内不能修改类成员的值
接口
@TODO
内部类
一般内部类
- 外部类不一定有内部类实例, 但内部类一定有对应的外部类,
- 内部类的成员不能是static, 也不能有static代码块(但内部类可以是static的, 嵌套类)
- 外部类和内部类可以 互相访问 所有成员(包括private);
- 外部类可以访问内部类的一切成员, 无论这个内部类是public还是private的, 无论内部类的成员是public还是private的, 外部类通过
内部类实例.成员名
访问内部类的成员; - 内部类可以访问外部类的一切成员, 包括外部类的private成员, 访问方式是
外部类类名.this.func()
, 或者也可以”直接调用”外部类的成员.
- 外部类可以访问内部类的一切成员, 无论这个内部类是public还是private的, 无论内部类的成员是public还是private的, 外部类通过
- 在编译成功后, 会出现这样两个class文件:
Outer.class
和Outer$Inner.class
;
定义一个内部类:
public class Outter { |
外部类如何访问内部类
- 内部类访问外部类属性:
println(OutterClass.this.propertyName);
- 外部类访问内部类属性:
println(inner.propertyName)
// 必须先创建内部类实例inner - 在拥有外部类对象之前, 是不可能创建内部类对象的, 换句话说, 其他人只能通过外部类对象才能访问内部类:
Outter.Inner in = new Outter.Inner(); // ERROR! 要先创建外部类 |
其他类如何访问内部类
- public的内部类 的public成员是包可见;
- public的内部类 的private成员包不可见, 仅对外部类可见;
- 当Inner是private时, 其他类不能通过
Outter.Inner in = out.getInner()
或者Outter.Inner in = out.new Inner
的方式创建Inner对象, 因为Inner类就是private的;
但是, 如果private的Inner继承自一个Base类, 这个Base类又是包可见(Public)的, 那么可以通过Base base = out.getInner()
的方式创建内部类对象, 换句话说, 这个Base是内部类的一个对外接口, 只能通过这个对外接口访问private的内部类;
以上参考: 探讨Java内部类的可见性; @Ref
内部类的必要性?
- Java不允许多重继承, 使用内部类可以”继承”外部类的方法, 并且内部类可以独立的继承自另一个抽象类或者接口.
- 把实现细节放在内部类, 相当于是对外隐藏细节, 封装.
- 事件监听大量用到匿名内部类.
- 使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现, 所以无论外围类是否已经继承了某个(接口的)实现, 对于内部类都没有影响[Think in Java]
局部内部类 & 匿名类
- 匿名类首先要有一个Interface or 基类;
- 匿名类没有名字, 也没有构造方法, 没有访问修饰符;
- 匿名类可以访问外部的变量, 但是创建匿名类的方法参数是final的;
定义一个匿名类:
/* 匿名类要有一个接口或基类 */ |
UI中大量使用的事件callback:
button.setOnClickListener(new OnClickListener() { |
嵌套内部类
- static的内部类被称为嵌套类, 嵌套内部类不需要由外部类创建, 也就没有隐藏的外部类引用
- 不能调用非static的外部类成员, 也不能访问
Outter.this.property
; - 外部类初始化的时候, 不会触发嵌套内部类的初始化.
静态内部类的初始化的时机( 初始化时会执行static代码块, 初始化static成员变量, JVM会把这些操作放在一个叫
clint
的方法中执行 ):
字符串
- String是一个特殊的类, 不需要构造函数就可以创建实例
String s = "hello world"
; - String的
char[]
是final static的, 只有一份拷贝.一旦String被创建, 字符串的内容就不可改变了 // Question: 当new一个String时, 是如何判断字符串池里是否已经有相同字符串的? - 字符串的比较不能使用
==
:==
仍然比较的是引用, 而应该使用String.equals()
String一些方法和实现
bool contains(String str)
: 判断参数s是否被包含在字符串中,并返回一个布尔类型的值int indexOf(String str, int fromIndex)
:String substring(int beginIndex, int endIndex)
: 该方法从beginIndex位置起,从当前字符串中取出到endIndex-1位置的字符作为一个新的字符串返回。int compareTo(String anotherString)
: 该方法是对字符串内容按字典顺序进行大小比较,通过返回的整数值指明当前字符串与参数字符串的大小关系。若当前对象比参数大则返回正整数,反之返回负整数,相等返回0。boolean equals(Object anotherObject)
: 比较当前字符串和参数字符串,在两个字符串相等的时候返回true,否则返回false。- 比较引用是否相等
- 要比较的对象是否
instanceof String
- 比较数组的长度 & 依次比较每个char
String concat(String str)
: 将参数中的字符串str连接到当前字符串的后面, 生成一个新字符串返回String replace(char oldChar, char newChar)
: 用字符newChar替换当前字符串中所有的oldChar字符,并返回一个新的字符串。String replaceAll(String regex, String replacement)
: 该方法用字符replacement的内容替换当前字符串中遇到的所有和字符串regex相匹配的子串,应将新的字符串返回。
String 不可被继承
public final class String implements java.io.Serializable, Comparable<String>, CharSequence |
比较StringBuffer
- StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。两者的
char []
不是final的, 可以修改; - StringBuffer线程安全, 所有方法都是synchronized的;
/* 比较 `String concat(String)`, `+`, 以及 StringBuffer 效率*/ |
String,char,byte的互转
- String是由
char[]
存储数据, char是unicode, 用16bit(2字节)的数值表示一个char:char c = '\u554a';
- String和char都可以用
\u0000
这种方式初始化. - byte是字节, String/char转为byte[]时, 不能确定byte[]的长度, 视转换用哪种编码(GBK/UTF-8)而定.
String string = "\u0048\u0069"; // Unicode对应的字符串是"Hi" |
String str = "嘿H1"; |
unicode编码只指定了编码值, gbk和utf8定义了如何存储编码值.
- 一个char存储的是16位的unicode, 范围0~0xFFFF(65535), 超过这个范围的汉字, 比如”𩄀”, 要用两个char也就是4字节表示.
- 如果unicode用gbk编码, 一个中文3字节, 一个英文1字节;
- 如果unicode用utf-8编码, 中文2字节, 英文一字节;
- 所以上面的输出分别是3, 5, 6, 3;
常用类
String & StringBuffer
见 字符串
包装类
见 数据类型
Math类
@TODO
日期类
使用 DateTimeFormatter 替换 SimpleDateFormat: 你真的会使用SimpleDateFormat吗? - 知乎
反射和RTTI
- RTTI: Run-Time Type Indentification, 运行时类型识别. 并非Java体系中的概念, 来自Thinking in C++
- Reflection(反射): 允许在程序运行期间探知并分析类对象的结构.
Class类和Class对象
- 每个类实例都有一个相对应的”Class对象”, 所以类实例在进行向上转型时不会丢失原有的类型信息, 这个Class对象的类型就是”Class类”, 位于
java.lang.Class
;T.class
: 获取类型T的Class对象, 基本类型int
也可以通过int.class
获取, 虽然int等基本类型不是类, 但是也可以Class cl = int.class;
t.getClass()
: 返回的也是Class对象,getClass()
是Object类的方法;- 可以用
==
判断class对象是否相等:if(a.getClass() == A.class)
;
- JVM通过”Class对象”创建”类对象”, 然后通过”类对象”创建类实例:
- 加载, 加载器(Class Loader)从磁盘上找到并加载.class文件, 加载.class文件可以看成是加载字节码, 并创建Class对象;
- 链接, 分为三个步骤: 验证字节码(语法层面), 为static分配空间但不初始化(基本类型置为0,引用置为null), 解析这个类对其他类的引用;
- 初始化, 首先初始化该类的超类, 然后是static成员和static块, 最后才是构造器执行(构造器也可以看成是static方法).
用反射创建类
// 方式1: |
使用反射API分析类
Class, Constructor(构造方法), Field(属性), Method(方法), Modifier(作用域)
Class cl = Class.forName("orj.xxx.ClassName"); |
Class类的方法列表
Class<?> forName(String className)
Class<?> forName(String name, boolean initialize, ClassLoader loader)
T newInstance()
:boolean isInstance(Object)
: Native方法, 注意区别instanceof
二元操作符boolean isArray()
: 是否是数组, Native方法Class<?> getComponentType()
: 返回Class类型, 返回的Class是数组元素的类型, 示例代码:String[].class.getComponentType()
Method getMethod(String name, Class<?>... parameterTypes)
: 返回指定方法名和形参的方法- 以下用来获取构造器/方法/属性的列表:
Constructor[] getDeclaredConstructors()
Method[] getDeclaredMethods()
Field[] getDeclaredFields()
数组和反射
java.lang.reflect.Array
类提供了数组的反射方法, 注意区分java.util.Arrays
用反射创建数组
// Array.newInstance 创建数组 |
用反射分析数组
// Class.getComponentType 获取数组元素类型 |
reflect.Array类的方法列表
Object newInstance(Class<?> componentType, int length)
- reflect.Array并没有探测数组元素类型, 和数组长度的方法:(
Class
类提供了一个:array.getClass().getComponentType().toString());
int Array.getLength(Object arr)
: 返回值是int, 数组大小最大只能是int ?- …
安全的类型转换
- 向上转型:
List<Object> list = new ArrayList<Object>()
; - 向下转型:
ChildA child = (obj instanceof ChildA ? (ChildA)obj : null);
instanceof关键字用于判断一个引用类型变量所指向的对象是否是一个类(或接口、抽象类、父类)的实例。
异常处理
图-Java异常类的层次结构:
Error & Exception
在 Java 语言规范中,所有异常都是 Throwable 类或者其子类的实例。Throwable 有两大直接子类。第一个是 Error,涵盖程序不应捕获的异常。当程序触发 Error 时,它的执行状态已经无法恢复,需要中止线程甚至是中止虚拟机。第二子类则是 Exception,涵盖程序可能需要捕获并且处理的异常。
- Error 是程序无法处理的, 内存不足或JVM的错误, 比如
OutOfMemoryError
,ThreadDeath
- Exception 可由程序处理, 又分为”CheckedException”(受捡异常, 上图粉红色), 和”UncheckedException”(不受检异常, 上图蓝色)
- 前者是程序需要捕获并处理的异常(比如打开文件错误, 网络超时等待), 需要throws-try-catch语句显式的捕获;
- 后者是代码错误, 比如数组越界, 这种不需要明确throws, 如果throws了也不强制代码必须catch, 其实Error也能算是不受检异常;
继承关系
Throwable |
try-catch
如果该异常被 catch 代码块捕获,finally 代码块则在 catch 代码块之后运行。
在某些不幸的情况下,catch 代码块也触发了异常,那么 finally 代码块同样会运行,并会抛出 catch 代码块触发的异常。
在某些极端不幸的情况下,finally 代码块也触发了异常,那么只好中断当前 finally 代码块的执行,并往外抛异常。
try语句中的return
// x返回多少? 会打印出什么? |
- 如果
try{return}
, 但finally没有return语句(比如上面的代码), 仍旧会执行finally
块, try中的”返回值”被保存在局部变量中, jsr指令跳到finally块执行, 然后返回之前保存在局部变量的返回值. - 如果上面代码finally也有return, 执行顺序是: try里的++x, finally里的++x, finally里的return
- 如果try和Catch都有return, 返回catch块的return
- 如果try和finally块都有return, try中
return表达式
仍会执行, 但不会返回try块的返回值, 而是执行finally块的return.
getMessage vs toString
如代码所示,e.toString()
获取的信息包括异常类型和异常详细消息,而e.getMessage()
只是获取了异常的详细消息字符串,
所以推荐在Catch中使用e.toString()
常见异常及解释
断言
- 表达式
assert 表达式:错误消息
比如assert x>y : "断言失败!"
- 如何开启关闭断言? 单点为某个类开启断言?
java -ea Xxx
,java -ea:MyClass Xxx
集合
集合类继承关系
Java核心类库提供了两大类容器, Collection(集合)和Map, 其中Collection接口又派生出List, Queue, Set三种接口:
容器顶层接口Collection/Map以及主要实现类 & 继承关系:java.util.Collection [I]
java.util.List [I]
ArrayList
LinkedList
Vector
Stack
java.util.Queue [I]
LinkedList
PriorityQueue
java.util.Deque [I]
LinkedList
java.util.Set [I]
TreeSet
HashSet
LinkedHashSet
java.util.Map [I]
TreeMap
HashMap
LinkedHashMap
Collection接口
Collection接口方法:
add()
: ArrayList和LinkedList都是append to endremove(Object)
: 遍历整个并equals判断是否相等, 然后删除contains(Object)
: 都是O(N)遍历containsAll(Collection<?> c):
不是测试是否包含连续的集合, 比如String.indexOf那样size()
:toArray()
: 生成数组iterator()
: 返回迭代器Iterator, 它具有next()方法, 用于每次返回一个元素, 直到循环器中元素穷尽: - 从Obj继承的
equals()
,hashCode()
List
List接口常用方法:
add(int index, E element)
:
Inserts the specified element at the specified position in this list (optional operation).addAll(Collection<? extends E> c)
:
Appends all of the elements in the specified collection to the end of this list, in the order that they are returned by the specified collection’s iterator (optional operation).contains(Object o)
:
Returns true if this list contains the specified element.containsAll(Collection<?> c)
:
Returns true if this list contains all of the elements of the specified collection.retainAll(Collection<?> c)
:
Retains only the elements in this list that are contained in the specified collection (optional operation).sort(Comparator<? super E> c)
:
Sorts this list according to the order induced by the specified Comparator.subList(int fromIndex, int toIndex)
:
Returns a view of the portion of this list between the specified fromIndex, inclusive, and toIndex, exclusive.
ArrayList
ArrayList内部是Object[]
数组实现, 数组初始大小10, 每次扩展原大小的两倍, 随机访问性能好, 插入/删除代价较大, iterator是整数封装.
ArrayList实现了List接口:
iterator()
,listIterator()
,listIterator(index)
add(E)
,add(index,E)
,addAll(Collection)
remove(E)
,remove(index)
,removeAll(Collection)
set(index,E)
sort(Comparator<? super E> c)
: 实际调用了Arrays.sort()
subList(start,end)
: 返回的并不是ArrayList ,而是ArrayList的一个视图, 对于SubList的所有操作最终会反映到原列表上。retainAll(Collection)
保留ArrayList中和Collection中共有的元素(但会改变ArrayList, 没有在Collection中的元素会从ArrayList里删除)Object[] toArray()
: 对该方法返回的数组, 进行操作(增删改查)都不会影响原集合的数据(ArrayList中elementData)- 使用工具类
Arrays
的asList()
方法把数组转换成集合后, 不能使用该集合的add
/remove
/clear
方法会, 否则抛出UnsupportedOperationException
异常。说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。
扩容
ArrayList()
创建的数组大小是0, 第一次add()的时候会把数组扩容到DEFAULT_CAPACITY
, 也就是10,
每次调用add()
的时候都会检查一下添加后的数组大小是否比当前的数组大, 如果是则扩大到 1.5倍原数组的大小. int newCapacity = oldCapacity + (oldCapacity >> 1);
// 为什么是x1.5倍?
所以ArrayList(默认构造函数)每次扩容的大小是: 0, 10, 15, 22, 33, 49 …
如果在构造ArrayList时就指定了初始大小为N, 则扩容大小是, N, 1.5N …
LinkedList
链表实现, 随机访问性能差, 插入/删除较快, iterator是引用封装.
LinkedList同时实现了List, Deque接口:
add(E)
,add(index,E)
,addAll(Collection)
addFirst(E)
,addLast(E)
,offerFirst(E)
,offerLast(E)
… 所有Deque接口的方法
Vector
类似ArrayList, Object[]数组实现, 包括的方法参考ArrayList, synchronized同步 @弃用
Stack
push()
入栈, pop()
弹出栈顶部元素, peek()
获取栈顶但不弹出顶部元素,
Stack实际就是对Vector包装了一层, 所以也是synchronized同步
Queue & Deque
Queue接口
offer
,add
: 添加元素到队列尾部.
当队列满时, offer返回false, add抛出异常.poll
,remove
: 返回队列头部的元素, 并移除出这个元素.
当队列为空时, poll返回false, remove抛出异常.peek
,element
: 返回队列头部的元素但不移除它.
当队列空时, peek返回false, element抛出异常.
Deque接口
offerFirst
,offerLast
: 添加元素到队列, 失败返falseaddFirst
,addLast
: 添加元素到队列, 失败抛异常pollFirst
,poolLast
: 返回并移出元素, 失败返falseremoveFirst
,removeLast
: 返回并移出元素, 失败抛异常peekFirst
,peekLast
: 返回但不移出, 失败返falseelementFirst
,elementLast
: 返回但不移出, 失败抛异常
实现类
LinkedList
: 双向链表, 同时实现了Deque和Queue接口, 它是唯一一个允许放入null的Queue;ArrayDeque
: 以循环数组实现的双向Queue,默认初始大小是16,每次扩容double。
普通数组只能快速在末尾添加元素,为了支持FIFO,从数组头快速取出元素,就需要使用循环数组:有指向队头/队尾两个下标值.
从队列取出元素时,表示队头下标值++;
向队列插入元素时,如果已到数组空间的末尾,则将元素循环赋值到数组0位置。
如果队尾的下标追上队头,说明数组所有空间已用完,进行双倍的数组扩容。
带优先级的队列 :
PriorityQueue
: 用二叉堆实现的优先级队列。出队列的顺序不是按照FIFO的顺序, 而是按照插入元素来排序。插入的元素必须实现Comparable, 或者在PriorityQueue构造器传入Comparator,
优先级队列是无界的,但是有一个内部容量,控制着用于存储队列元素的数组大小。它通常至少等于队列的大小。随着不断向优先级队列添加元素,其容量会自动增加。无需指定容量增加策略的细节。
优先级队列不允许 null 元素。
线程安全的阻塞/非阻塞队列, 详见线程安全的队列 :
- 阻塞:
ArrayBlockingQueue
,LinkedBlockingQueue
,LinkedBlockingDeque
; - 非阻塞:
ConcurrentLinkedQueue
/ConcurrentLinkedDeque
;
Set
Set是不能包含重复的元素的集合, Set接口常用方法:
add(E e)
addAll(Collection<? extends E> c)
contains(Object o)
containsAll(Collection<?> c)
retainAll(Collection<?> c)
toArray()
HashSet
HashSet 是一个没有重复元素的集合. 元素并没有以某种特定顺序来存放,
HashSet内部实现是使用了HashMap的transient HashMap<E,Object> map
, add(E)
方法实际调用的是hashMap.put(e,PRESENT)
LinkedHashSet
LinkedHashSet 可以按照插入顺序对元素进行遍历.
LinkedHashSet 继承了 HashSet, 内部是基于 LinkedHashMap 来实现的. 可以在LinkedHashSet构造器看出来:
HashSet(int initialCapacity, float loadFactor, boolean dummy) { |
TreeSet
TreeSet是基于TreeMap实现的.
TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序. 这取决于使用的构造方法.
TreeSet的add、remove 和 contains方法的时间复杂度是O(logn).
class Item implements Comparable<Item> { |
Iterator: 迭代器
- Iterator接口的方法
hasNext
: 返回true或falsenext
: 迭代器后移一次之后, 回迭代器前面的元素remove
: 删除上次next()返回的, 所以新创建迭代器之后, 必须先next一次才能remove. 一次remove之前必须有一次next, 不能连续调用remove;add
: Iterator接口没有add, 但ArrayList和LinkedList的内部Itr都实现了add. 在当前迭代器之前插入. 如果创建了迭代器后立刻add, 则是插入到首位.
- ArrayList的Iterator:
- 属性
int cursor
和int lastRet
分别用来记录”下次next方法要返回的元素位置” 和”上次next方法返回的”, 初始值分别是0和-1; - 创建迭代器:
- 方法1: ArrayList.iterator()
- 方法2: ArrayList.listIterator(), 返回的迭代器有
add(Ele)
方法用于插入新元素;
- 属性
How to iterate collection:
// 1 |
集合泛型算法
Collection & Collections & Arrays:
java.util.Collection<E>
是一个泛型接口;java.util.Collections
是一个集合工具类, 提供一些操作集合的通用方法;java.utils.Arrays
是一个集合工具类, 提供操作数组的通用方法, 例如merge, sort等;java.lang.reflect.Array
类提供了数组的反射方法;
图-Collection类 vs Collections类:
排序操作(主要针对List接口相关)
reverse(List list)
:反转指定List集合中元素的顺序rotate(List list, int distance)
:将所有元素向右移位指定长度, 如果distance等于size那么结果不变shuffle(List list)
:对List中的元素进行随机排序(洗牌)sort(List list)
:对List里的元素根据自然升序排序sort(List list, Comparator c)
:自定义比较器进行排序swap(List list, int i, int j)
:将指定List集合中i处元素和j出元素进行交换
如果要使用Collections.sort, 则要求集合内存放的类型必须实现Comparable接口
查找和替换(主要针对Collection接口相关)
binarySearch(List list, Object key)
:使用二分搜索法, 以获得指定对象在List中的索引, 前提是集合已经排序fill(List list, Object obj)
:使用指定对象填充frequency(Collection Object o)
:返回指定集合中指定对象出现的次数max(Collection coll)
:返回最大元素max(Collection coll, Comparator comp)
:根据自定义比较器, 返回最大元素min(Collection coll)
:返回最小元素min(Collection coll, Comparator comp)
:根据自定义比较器, 返回最小元素replaceAll(List list, Object old, Object new)
:替换
Map接口
Map不是继承Collection接口, 也没有继承Iterable接口, Map接口提供的方法:
put(k,v)
,get(k)
,containsKey(k)
,containsValue(v)
remove(k)
,replace(k,v1,v2)
HashMap
- HashMap 是一个散列表, 它存储的内容是键值对(key-value)映射.
- HashMap 继承于AbstractMap, 实现了 Map、Cloneable、java.io.Serializable接口.
- HashMap 的实现不是同步的, 这意味着它不是线程安全的. 它的 key、value都可以为null. 此外, HashMap中的映射不是有序的.
HashMap 的实例有两个参数影响其性能: “初始容量” 和 “加载因子”. 容量 是哈希表中桶的数量, 初始容量 只是哈希表在创建时的容量.
加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度.
当哈希表中的条目数超出了加载因子与当前容量的乘积时, 则要对该哈希表进行 rehash 操作(即重建内部数据结构), 从而哈希表将具有大约两倍的桶数.
通常, 默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷.
在设置初始容量时应该考虑到映射中所需的条目数及其加载因子, 以便最大限度地减少 rehash 操作次数. 如果一个Map的初始容量大于”最大条目数”乘以加载因子, 则不会发生 rehash 操作.
内部实现
HashMap几个重要成员:
Node[] table; // 桶 |
put(Key, Val)
函数大致的实现为:
- 计算
Key
的hashCode, 创建新的Node对象new Node(hash, key, value, null)
// node存储了hashCode, Key, Val - 然后再计算
Key
在桶里的index (index等于table.length-1 & hash
); - 如果没碰撞(table[index] == null), 把node直接放到table数组里:
table[index]=node
; - 如果碰撞了(table[index] != null), 则判断
table[i]
的首个元素的key是否hashCode相同 && key equals 为真;- 是, 是则覆盖掉旧的value;
- 否, 插入到
table[i]
的链表里, 所以链表里保存是”Key的hashCode相同, 但Key不equal的元素”;
- 如果碰撞导致链表过长(大于等于
TREEIFY_THRESHOLD
, 8), 就把这条链表转换成红黑树; 如果map内的元素总数超过
table.length x loadFactor
, 就要resize(扩容)上面提到了
table[index]
在哈希冲突时候, 会把 table[index] 处理成链表, 当链表过长的时候, 链表的遍历性能是O(n), 很差, 所以
当链表长度>=8时, 转成查找效率更高的红黑树;
get(k)
函数的实现: 这里省略了部分步骤, 只看当 table[i]
是链表 or 红黑树的情况:
遍历链表or树, 判断Key是否equal, 如果是, 返回该节点;
扩容
table[]数组double, 把所有元素re-hash放到扩容后的table[]中.
在HashMap中, 哈希桶数组table的长度length大小必须为2的n次方(一定是合数), 这是一种非常规的设计, 常规的设计是把桶的大小设计为素数. 相对来说素数导致冲突的概率要小于合数,
HashMap采用这种非常规设计, 主要是为了在取模和扩容时做优化, 同时为了减少冲突, HashMap定位哈希桶索引位置时, 也加入了高位参与运算的过程.
@Ref: Java 8系列之重新认识HashMap -
为什么是0.75?
从前面可知, 新添加进来的 Key-Value, 通过key.hashCode
计算地址存放, 发现当前位置已经有元素, 则称为元素的碰撞, 需要重新计算或者其他方式放置该元素.
HashMap为了避免碰撞采取的优化策略, 简单的说, 原本可以放100个数据的空间, 当放到80个的时候, 根据经验, 接下去冲突的可能性会更加高. 因此就自动增加空间来减小冲突可能性.
数组大小与碰撞几率服从泊松分布, 根据经验在0.75处几率最小.
Set视图
获取HashMap的Set视图: Set<Map.Entry<K, V>> entrySet()
, 返回类型是EntrySet extends AbstractSet<Map.Entry<K,V>>
, EntrySet的方法:
- size(), 直接返回HashMap的size
- forEach(Consumer<? super Map.Entry<K,V>> action)
LinkedHashMap
- LinkedHashMap 继承自 HashMap, 它能保证遍历元素时, 输出的顺序和输入时的顺序相同.
- LinkedHashMap 不仅实现HashMap的开散列哈希表(数组+链表), 还维护着一个运行于所有键值对的双向链接列表. 此列表定义了迭代的顺序, 该迭代顺序包括插入顺序和访问顺序两种, 默认是插入顺序;可以通过设置 accessOrder为 true, 把迭代顺序设置为访问顺序.
- LinkedHashMap 重写了父类的 HashMap 的get方法: 在调用父类的 getEntry() 方法取得查找的元素之后, 再判断排序模式 accessOrder是否为true, 如果是, 那么就把最新访问的元素添加到双向链表的表头, 并从原来的位置删除(可以用来实现LRU). 因为链表的插入和删除操作都是常量级的时间复杂度, 所以不会带来性能损失.
- LinkedHashMap 在保留 HashMap 的查找效率的同时, 保持元素输出的顺序和输入时的顺序相同, 并提供了元素的LRU访问.
参考: LinkedHashMap内部实现 @Ref
LinkedHashMap & HashMap代码比较
- LinkedHashMap 继承自 HashMap;
- HashMap的桶数组
HashMap.Node<K,V> table[]
, HashMap.Node<K,V>继承自Map.Entry<K,V>; - LinkedHashMap的
Entry
继承自HashMap.Node<K,V>
, (与HashMap.Node
相比)增加了before/after两个引用做双向链表
TreeMap
TreeMap 是一个有序的key-value集合, TreeMap 根据Key的自然顺序进行排序, 或者根据TreeMap构造器提供的 Comparator进行排序.
内部是基于红黑树(Red-Black tree)的 NavigableMap实现.
TreeMap的基本操作 containsKey
、get
、put
和 remove
的时间复杂度是 log(n).
- TreeMap 是一个有序的key-value集合, 它是通过红黑树实现的.
- TreeMap 继承于AbstractMap, 所以它是一个Map, 即一个key-value集合.
- TreeMap 实现了NavigableMap接口,
descendingKeySet()
方法返回一个与原顺序相反的值的一个Set集合, 其实是指向同一块内存区域, 在该视图上的任何修改都会影响到原始的数据. - TreeMap 实现了Cloneable接口, 意味着它能被克隆.
- TreeMap 实现了java.io.Serializable接口, 意味着它支持序列化.
实现
TreeMap的特点: 1 插入的元素可以按Key的自然顺序遍历, 2 像HashMap一样近似O(1)的查找复杂度;
二叉堆满足1, 但是不满足2,
红黑树的中序遍历可以满足1, 同时红黑树的查找复杂度(参考BST)是O(logN)
put
: 如果存在的话,old value被替换;如果不存在的话,则新添一个节点,然后对做红黑树的平衡操作。get
: log(n)- 顺序遍历: 中序遍历
为什么采用红黑树
排序二叉树虽然可以快速检索, 但在最坏的情况下: 如果插入的节点集本身就是有序的(比如由小到大排列, 或是由大到小排列),
那么最后得到的排序二叉树将变成链表: 所有节点只有左节点(如果插入节点集本身是大到小排列);或所有节点只有右节点(如果插入节点集本身是小到大排列).
在这种情况下, 排序二叉树就变成了普通链表, 其检索效率就会很差.
为了改变排序二叉树存在的不足, Rudolf Bayer 与 1972 年发明了另一种改进后的排序二叉树: 红黑树, 他将这种排序二叉树称为”对称二叉 B 树”, 而红黑树这个名字则由 Leo J. Guibas 和 Robert Sedgewick 于 1978 年首次提出.
参考: 教你透彻了解红黑树 @Ref
HashTable
- HashTable的方法都是采用了
synchronized
同步. - 高并发场景下不推荐使用HashTable, 应该使用
java.util.concurrent.ConcurrentHashMap
替代.
WeakHashMap
这种Map通常用在数据缓存中.它将键存储在WeakReference中, 就是说, 如果没有强引用指向键对象的话, 这些键就可以被垃圾回收线程回收
→ Advanced Java Tutorial#WeakHashMap
关于Map.Entry
HashMap有一个该类型的属性: transient Set<Map.Entry<K,V>> entrySet;
,
该属性在调用public Set<Map.Entry<K,V>> entrySet()
方法内被初始化:
public Set<Map.Entry<K,V>> entrySet() { |
EntrySet
类是HashMap的一个static内部类, 定义了forEach
方法:public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e);
}
}
}
How to iterate map
// 1: |
泛型
泛型类和泛型方法
泛型类
public class Pair<T> { |
泛型方法
public Class ArrayAlg { |
泛型的类型限定
public static <T extends Comparable> T min(T[] a) { |
如果T需要多个类型限定: <T extends Comparable & Serializable>
类型擦除
- JVM没有”泛型类”这种类型, java代码被编译后生成的字节代码, 这个过程中所有的泛型类型要被替换, 原则:
- 有类型限定的, 替换为第一个限定类型,
T extends Comparable & Serializable
被替换为Comparable
- 无类型限定, 替换为Object,
T
被替换为Object
- 有类型限定的, 替换为第一个限定类型,
对类型查询的影响
1. instanceof
Pair<String> s = new Pair<String>(); // 擦除后为 Pair<Object> s |
2. getClass()
Pair<String> s = new Pair<String>(); |
不能创建泛型类数组
// 试图创建泛型类型的数组会在编译期报错: |
原因是数组一旦创建会记住元素的类型, 当试图向数组中存储不同的类型时会报错, Pair<String>[]
这样声明的泛型数组, 擦除后变为Pair<Object>[]
.
不能实例化泛型
不能使用像new T()
, new T[N]
, T.class
这样的表达式.
通配符<?>
无限定通配符
public static boolean Foo(List<?> list) { |
List<?>
表示持有某种特定类型的List,但是不知道具体是哪种类型。那么我们可以向其中添加对象吗?当然不可以,因为并不知道实际是哪种类型,所以不能添加任何类型,这是不安全的。
上界通配符
? extends ClassType
表示ClassType的任何子类
先看一段代码:List<? extends Fruit> list = new ArrayList<Apple>();
// Compile Error: can’t add any type of object:
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
// 只能向list里添加null
list.add(null);
// get是可以编译通过的
list.get(0);
做了泛型的向上转型 (List<? extends Fruit> flist = new ArrayList<Apple>()
),那么我们也就失去了向这个List
添加任何对象的能力,即使是Object
也不行。
那么上界通配符有什么用呢?
public class GenericTest { |
List<? extends Fruit> list
表示一个List, 里面存储的类型是Fruit
的派生类, 从list里get出来的类型至少是Fruit
, 或者Fruit
的派生类, 可以调用Fruit
类的方法.
传递给GenericTest.func()
的参数可以是List<Apple>
, 也可以是List<Lemon>
,
上界通配符<? extends Base>
, 可以调用基类Base
里定义的方法, 也可以get, 但是不可以set
下界通配符
? super Integer
表示Integer的超类, 只能用于setter
.
void setFirst(Pair<? super Integer>); |
限定符和泛型的一些问题…
泛型中无界通配符<?>
和<T>
的区别?
<T>
用在类或方法的定义里:public class ArrayList<T>
<?>
通配符用在”调用”的地方, 通配符是拿来使用定义好的泛型的, 可以使用?
的一般满足:- 方法定义里只使用Object的方法,跟
?
类型无关; - 使用中不依赖于泛型, 最典型的是
Class<?> ...
- 方法定义里只使用Object的方法,跟
- 无限定通配符表示匹配任意类。
ArrayList<?>
和ArrayList<Object>
看上去有点类似,但实际却不一样。ArrayList<?>
是任意ArrayList<T>
的超类;List<Apple>
是List<? extends Fruit>
的子类(假设Apple继承自Fruit)ArrayList<Object>
并不是ArrayList<T>
的超类;
代理
什么是代理模式:
用户代码不直接调用某些功能类的方法, 而是通过代调类作为”中间层”去调用”被代理类”. 所有调用都会被代理类拦截, 我们可以利用代理类的这个特性, 在代理类里增加额外的执行代码.
使用代理可以给我们带来如下好处: 用户代码(调用者)和功能类(被调用者)解耦, 第二个好处是通过代理层可以加入一些通用的代码.
Java代理模式的实现主要有: 静态代理, JDK动态代理, Cglib动态代理.
JDK动态代理
如何使用JDK的动态代理:
// 1 接口 |
通过proxy调用YourClass
实现自接口YourInterface
的所有方法, 都会调用到YourHandler
的invoke方法,
在invoke方法里可以很方便的做一些前置和后置处理(访问控制、远程通信、日志、缓存等), 在invoke里再通过反射调用实际类YourClass
的方法.
动态代理的优点是, 当YourInterface
的实现类有很多的时候, 比如有YourClassA, YourClassB…
通过代理调用这些实现类的方法(必须是实现YourInterface里的方法), 都会由代理调用到InvocationHandler.invoke()
,
如果用静态代理, 那么代理类(实现了YourInterface接口)必须为YourInterface的每一个方法都增加单独的代码.
参考: Java 动态代理作用是什么? - 知乎 @Ref
实现原理
在调用Proxy.newProxyInstance()
之后,
又调用了ProxyGenerator.generateProxyClass()
方法生成最终代理类的字节码, 并通过ClassLoader把字节码转化成对象.
在最终代理类里实现了我们的Interface定义的所有方法, 在这些方法内部, 都通过反射调用了InvocationHandler
接口实现类的invoke()
方法
包
sun.misc.ProxyGenerator
提供了一个功能, 可以生成YourInterface的实现类的字节码:byte[] data = ProxyGenerator.generateProxyClass(name,new Class[]{YourInterface.class});
CGLIB代理
CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB是一个好的选择。
与JDK动态代理不同的是, 使用CGLIB即使被代理类没有实现任何接口也可以实现动态代理功能。但是不能对final修饰的类进行代理。
JDK动态代理通过反射类Proxy和InvocationHandler回调接口实现,要求委托类必须实现一个接口,只能对该类接口中定义的方法实现代理,这在实际编程中有一定的局限性。
CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。
ASM是一个Java字节码操控框架。通过分析被代理类的class文件, 在内存中创建被代理类的增强子类, 它能被用来动态生成类或者增强既有类的功能。
ASM可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。
脚本语言例如Groovy和BeanShell,也是使用ASM来生成java的字节码。当然不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。
下面通过一个例子看看使用CGLib如何实现动态代理:
定义业务逻辑:
public class UserServiceImpl { |
实现MethodInterceptor接口,定义方法的拦截器:
public class YourMethodInterceptor implements MethodInterceptor { |
利用Enhancer
类生成UserServiceImpl
的代理类:
Enhancer enhancer = new Enhancer(); |
Enhancer
是CGLib的字节码增强类, 可以生成类的字节码(UserServiceImpl
的子类),
其作用类似sun.misc.ProxyGenerator
, 区别是Enhancer
不需要被代理类实现接口, 而ProxyGenerator
要求被代理类必须实现接口
以上参考:
@Ref 说说 cglib 动态代理
Spring AOP与代理
Spring AOP中的一些注解 & 概念:
@Aspect: PointCut + Advice
@PointCut: 切点, 在哪里切入
@Advice: 切入的行为(在切点之前还是之后, 或者环绕切点), 以及做什么
Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP中的动态代理主要有两种方式,JDK动态代理 和 CGLIB动态代理。
- 如果目标类(被切的类)有统一的实现接口,Spring AOP使用JDK动态代理,
- 如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。
因此如果某个类被标记为final,并且没有实现接口,那么它是无法被动态代理的,也就无法当做切点(CutPoint)
I/O
概述
Java 的 I/O 操作类在包 java.io 下,大概有将近 80 个类,但是这些类大概可以分成四组,分别是:
- 基于字节操作的 I/O 流接口:
InputStream
和OutputStream
- 基于字符操作的 I/O 流接口:
Writer
和Reader
- 基于磁盘操作的 I/O 文件接口:
File
参考: 深入分析 Java I/O 的工作机制 @Ref
流
按照流操作对象的类型是字节还是字符, 分为字节流和字符流
- 字节流的父类是
InputStream
/OutputStream
, 读写单个字节/字节数组, - 字符流的父类是
Reader
/Writer
用于读写被编码(GBK/UTF8)的字符串, 读写Char/Char数组;
按照功能分为节点流(node stream)和过滤流(filter stream, 或者叫装饰流)
- 节点流用来处理从基本位置获取字节(文件, 内存, 管道),
FileInputStream
,ByteArrayInputStream
,PipedInputStream
, 这些类提供基本的读写方法; - 过滤流用于包装节点流, 提供了新的方法, 可以更方便的读写高级类型的数据(类序列化, 压缩文件, Java基本类型)
ObjectInputStream
,ZipInputStream
,DataInputStream
.
节点流 & 过滤流
图-外层的DataInputStream(过滤流)提供了额外的方法:
字节流 & 字符流
字符流相关类以及继承关系:
字符流 |
字节流相关类以及继承关系:
字节流 |
字节流 常用类和方法
InputStream
/OutputStream
提供基本的字符/字符数组读写InputStream.available()
: 返回可读的字节数read()
,read(byte[])
: 阻塞的, read返回读取的一个字节(int)write(int b)
,write(byte[])
: 阻塞的close()
FileInputStream
/FileOutputStream
ByteArrayInputStream
/ByteArrayOutputStream
: 包含一个内部缓冲区(字节数组), 该缓冲区包含从流中读取的字节PipedInputStream
/PipedOutputStream
同上BufferedInputStream
/BufferedOutputStream
: 为另一个流提供缓冲ObjectInputStream
/ObjectOutputStream
Object readObject()
void writeObject(Object)
字符流 常用类和方法
- Reader: 提供对char,char[],String类型数据的基本操作
read()
: 返回字符的Unicode编码(0-65535,双字节范围), 到达流末尾返回-1;read(char[])
: 读取字符到数组并返回已读取的字符个数;skip(long n)
: 跳过n个charmark(int limit)
: 为流的当前位置增加标记, 下次调用reset可以返回这个标记, 如果调用mark()后读取字符数超过limit, 下次调用reset会失败.reset()
:close()
:
- InputStreamReader
getEncoding()
: 获取输入流的编码ready()
: 如果有数据可读, 返回true
- FileReader: 继承自 InputStreamReader
- 构造器:
FileReader(String)
,FileReader(java.io.File)
- 构造器:
- BufferedReader:
readLine()
: 读取一行并返回字符串(不包括换行符), 如果流已经读尽则返回null
- Scanner: 不是继承自Reader
- Writer : 提供对char,char[],String类型数据的基本操作
write(char c)
,write(char[])
,write(String)
append(char)
,append(CharSequence)
flush()
: 让缓冲区的内容立刻写入close()
:
- PrintWriter:
文件
本章主要介绍文件操作类: java.io.File
和 java.io.RandomAccessFile
java.io.File
File 是“文件”和“目录路径名”的抽象表示形式。File 直接继承于Object,实现了Serializable接口和Comparable接口。
实现Serializable接口,意味着File对象支持序列化操作。
实现Comparable接口,意味着File对象之间可以比较大小;File能直接被存储在有序集合(如TreeSet、TreeMap中)。
public class FileTest { |
java.io.RandomAccessFile
java.io.RandomAccessFile
是随机访问文件(包括读/写)的类。它支持对文件随机访问的读取和写入,即我们可以从指定的位置读取/写入文件数据。
需要注意的是,RandomAccessFile 虽然属于java.io包,但它不是InputStream或者OutputStream的子类;
它也不同于FileInputStream和FileOutputStream。 FileInputStream 只能对文件进行读操作,而FileOutputStream 只能对文件进行写操作;
RandomAccessFile 同时支持文件的读和写,并且它支持随机访问。
RandomAccessFile
大部分功能被JDK1.4中NIO的内存映射文件替代了
RandomAccessFile raf = new RandomAccessFile(args[0], "r"); |
try-with-resources
旧风格的I/O操作的异常捕获:
private static void printFile() throws IOException { |
上面代码中可能会抛出异常. try语句块中有3个地方能抛出异常, finally语句块中有一个地方会能出异常.
不论try语句块中是否有异常抛出, finally语句块始终会被执行.这意味着, 不论try语句块中发生什么, InputStream 都会被关闭, 或者说都会试图被关闭.如果关闭失败, InputStream’s close()方法也可能会抛出异常.
Q: 假设try语句块抛出一个异常, 然后finally语句块被执行.同样假设finally语句块也抛出了一个异常.那么哪个异常会根据调用栈往外传播?
A: 即使try语句块中抛出的异常与异常传播更相关, 最终还是finally语句块中抛出的异常会根据调用栈向外传播.
在JDK7中, try-with-resources 风格的IO异常捕获:
try-with-resources语句会确保在try语句结束时关闭所有资源. 实现了java.lang.AutoCloseable
或java.io.Closeable
的对象都可以做为在try()
代码块内打开的资源, 并且可以在退出try()
语句块时被自动关闭.
// try()代码块内打开多个资源: |
当try-with-resources结构中抛出一个异常, 同时资源调用close方法时也抛出一个异常, try-with-resources结构中抛出的异常会向外传播, 而资源被关闭时抛出的异常被抑制了. 这与旧风格代码的例子相反.
API Example
字节流 API Example
/* 基本字节流 InputStream/OutputStream 接口测试: */ |
字符流 API Example
/* PrintWriter and Scanner */ |
序列化
@Q 序列化机制是怎样的?
①如何定义一个可序列化的类?
②serialVersionUID属性的作用和生成方式?
③哪些字段不会被序列化?
④ArrayList类的序列化是如何实现的?
⑤如何自定义序列化?
⑥反序列化的调用过程?
- 实现
Serializable
接口的类可以被序列化, 通过ObjectOutputStream
和ObjectInputStream
对象进行序列化及反序列化 - 通过
ObjectOutputStream
反序列化的时候, 会比较 serialVersionUID是否相同, 如果不同会抛异常InvalidClassException
, 建议: 在创建可序列化的类的时候指定一个serialVersionUID, 并且在可兼容升级的时候不要修改serialVersionUID, 除非是不兼容的版本. 如果没有定义serialVersionUID, 在反序列化的时候, ObjectOutputStream会自动生成一个(根据类名,接口名,属性名, 以及描述符等生成一个64位的哈希数字) - static 和 transient关键字修饰的属性不会被序列化
- ArrayList的实现里, 把
elementData[]
声明为 transient, 同时也实现了writeObject(ObjectOutputStream)
和readObject(ObjectInputStream)
, 在这两个方法里实现自定义序列化, 目的是为了避免elementData[]
中大量空元素被序列化, 减少序列化字节占用. - 如何自定义序列化实现: 类实现自己的
writeObject(ObjectOutputStream)
和readObject(ObjectInputStream)
- 反序列化调用过程(伪码):
ObjectInputStream.readObject(Object)
ObjectStreamClass.initNonProxy // 检查是否实现 Serializable接口, serialVersionUID是相等
调用类的readObject()
@Ref 你真的以为你了解Java的序列化了吗?
@Ref 为什么阿里巴巴要求程序员谨慎修改serialVersionUID 字段的值 - 后端 - 掘金
序列化/反序列化API示例代码:
class User implements java.io.Serializable { |
多线程
线程6种状态
- New, 创建Thread实例之后;
- Runnable, 执行
thread.start()
之后; - Blocked, 线程试图获取ReentrantLock失败, 或进入synchronize代码块, 或调用Block IO;
- Waiting, 调用
object.wait()
或thread.join()
之后;- 调用
object.wait()
,condition.await()
方法都会产生WAITING状态; - 调用
thread.join()
后, 调用者会Waiting一直到thread线程退出;
- 调用
- Time-Waitting, 调用
Object.wait(long)
或thread.join(long)
,lock.tryLock(long)
时; - Terminated: 线程的
run()
方法正常退出或者run()
方法抛出未捕获异常时;
上面的状态来自Oracle JDK 8
java.lang.Thread.State
, 并不等同于unix下的原生线程状态,
Thread.State (Java Platform SE 8 )
图-线程6种状态的转换:
线程控制API
start
// 第一种Runnable接口 |
interrupt
- 调用
t.interrupt()
方法时, 线程t
会收到中断信号, Java并没有要求线程一定响应中断. 线程应该根据情况决定是否响应中断, 循环调用t.isInterrupted()
可以检测线程的中断标志位. - 如果线程内调用了
sleep()
或者wait()
方法让线程进入等待状态, 当调用t.interrupt()
, 线程会抛出InterruptException
, 如果你的线程里调用了可能抛出该异常的阻塞方法, 那么就不必每次调用isInterrupt()
检测中断状态了, 在catch里捕获该异常即可. - 如果线程已经被中断的情况下再调用
sleep()
,sleep()
方法会清除中断状态并且抛出上述异常, 并不会进入sleep状态, 所以线程循环中有sleep()
的也不必用isInterrupt
检查中断状态 - 可抛出中断异常的: 线程内调用
wait()
, 或者调用thread.join()
和thread.sleep()
Thread t = new Thread(new Runnable() {
public void run() {
try {
while(!Thread.currentThread().isInterrupted() /* && */) {
Thread.sleep(5000); // 如果有sleep, 上面的isInterrupted不必要
}
} catch (InterruptedException e) {}
}
});
t.start(); // sub-thread now is "runnnable"
t.interrupt(); // main thread interrupt sub-thread
join
执行thread.join()
的线程会进入waiting状态, 直到thread
线程终止或自然退出, 继续执行后面的代码
MyThread thread = new MyThread(); |
sleep
执行thread.sleep(m)
的线程会进入timed_waitting状态m毫秒(注意, 并没有sleep这种状态),Thread.sleep()
与线程调度器交互,它将当前线程设置为等待一段时间的状态。一旦等待时间结束,线程状态就会被改为可运行(runnable),并开始等待CPU来执行后续的任务。因此,当前线程的实际休眠时间取决于线程调度器,而线程调度器则是由操作系统来进行管理的。
比较thread.sleep(long millis)
和object.wait()
:
- 对于
sleep()
方法,我们首先要知道该方法是属于Thread类中的。而wait()
方法,则是属于Object类中的。 sleep()
方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是cpu对线程的监控状态依然保持者,当指定的时间到了又会自动恢复runnable。- 在调用
sleep()
方法后, 线程进入timed_waiting状态, 并且线程不会释放对象锁。 - 当调用线程里调用
obj.wait()
方法的时候,线程进入等待此对象的等待队列,放弃对象锁并进入waiting状态,只有针对此对象调用notify()
方法后, 线程才会从对象锁的等待队列中被取出。
sleep和wait的区别?
Thread.sleep()
方法是一个静态方法,作用在当前线程上;obj.wait()
方法是一个实例方法,并且只能在其他线程调用本实例的obj.notify()
方法时被唤醒。- 调用
wait()
方法时,线程在等待的时候会释放掉它所获得的monitor,但是调用Thread.sleep()
方法时,线程在等待的时候仍然会持有monitor或者锁。 - Java中的
wait()
方法应在同步代码块中调用(已经获得了对象锁的情况下, 调用对象.wait()会放弃锁) - 使用
sleep()
方法时,被暂停的线程(是处于timed_wait状态?) 在被唤醒之后会立即进入就绪态(Runnable state) - 从
wait()
方法被唤醒的时候(通常是其他线程调用了obj.notify()
),被暂停的线程要首先获得锁,然后再进入Runnable。 - 如果你需要暂定线程一段特定的时间就使用
sleep()
方法,如果你想要实现线程间通信就使用wait()
方法。
如何终止线程?
几个问题:
- 被调用了
sleep()
的线程(timed_waiting状态)可以被interrupt()
抛出异常吗? - 调用了
thread.join()
的线程(waiting状态)可以被interrupt()
抛出异常吗? - 调用了
object.wait()
的线程(waiting状态)可以被interrupt()
抛出异常吗? - 调用阻塞IO方法被阻塞住的线程可以被
interrupt()
抛出异常吗? - 试图抢占锁(synchronized或ReentrantLock)但失败的线程(blocked状态)可以被
interrupt()
抛出异常吗?
答案: 可以, 可以, 可以, 否, 否
所以 只有处于waiting或timed_waiting状态的线程才可以抛出InterruptException
异常被中断, block状态的线程不可以;
如何终止waiting或timed_waiting状态的线程呢? 有两种方式:
- 用Volatile的标志位控制线程的循环逻辑;
- 使用interrupt()方法中断当前线程, 线程的循环里应该try-catch
InterruptException
但是对于进入blocked状态的线程, 是无法被interrupt()
中断的, 所以可能的做法是: 关闭阻塞的资源
class IOBlocked implements Runnable { |
唤醒线程
如何唤醒sleep或者wait的线程?
- 对于因为IO阻塞而进入的blocked状态的线程, 没有办法唤醒;
- 对于因为调用
waiting()
,t.sleep()
,t.join()
而进入waiting或timed_waiting状态的线程, 调用t.interrupt()
可以让上面的”阻塞操作”抛出InterruptedException
来打到”唤醒”的效果;
被弃用的方法
Thread类不再推荐被使用的方法: ~yield,stop,suspend,resume~
yield
yield方法会临时暂停当前正在执行的线程,来让有同样优先级的正在等待的线程有机会执行。
如果没有正在等待的线程,或者所有正在等待的线程的优先级都比较低,那么该线程会继续运行。
执行了yield方法的线程什么时候会继续运行由线程调度器来决定。
yield方法不保证当前的线程会暂停或者停止,但是可以保证当前线程在调用yield方法时会放弃CPU。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。
因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。
但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
stop
该方法天生是不安全的。使用thread.stop()停止一个线程,导致释放(解锁)所有该线程已经锁定的监视器(因沿堆栈向上传播的未检查ERROR
ThreadDeath
而解锁)。 // → 非受检异常
如果之前受这些监视器保护的任何对象处于不一致状态,则不一致状态的对象(受损对象)将对其他线程可见,这可能导致任意的行为。
ThreadDeath是java.lang.Error,不是java.lang.Exception。不受检异常意味着我们不会有意识在代码里写Try-Catch去处理异常, 比如在finally里释放锁
上面的意思是:
线程当前可能持有一个监视器(或锁),执行 thread.stop()
将会产生一个 ThreadDeath 错误(不受检ERROR),线程向上抛出错误,导致监视器被解锁。
可能导致的问题: 以银行转账的例子,如果在”减扣A余额, 增加B余额”的过程中, 线程被stop, 将产生业务数据的不一致.
建议 用interrupt替代stop, 在线程中循环检测thread.isInterrupted()
或者 捕获InterruptException
然后由业务代码进行收尾处理.
ThreadDeath 和 InterruptException 的区别是:
前者不受检, 意味着业务代码没有机会捕获并处理, 会向上层堆栈抛出错误, 线程状态变为 “Terminated”;
后者是受检异常, 可以被捕获并由业务代码处理;
suspend & resume
- 当某个线程的suspend()方法被调用时,该线程会被挂起。如果该线程占有了锁,则它不会释放锁。线程在挂起的状态下还持有锁,这导致其他线程将不能访问该资源直到目标线程恢复工作。
- 线程的
resume()方法
会恢复 因suspend()
方法挂起的线程,使之重新能够获得CPU执行。
建议使用 object.wait
和 object.notify
方法代替 suspend
& resume
线程属性
优先级
- java中线程优先级范围
MIN_PRIORITY
~MAX_PRIORITY
(其值1~10),NORMAL_PRIORITY
(其值=5); - 线程默认情况下继承父线程的优先级;
daemon
thread.setDaemon(true); |
当JVM还存在一个非守护线程, JVM就不会退出, 当存活的线程仅剩下守护线程时, JVM才会退出.
守护线程最典型的应用就是GC
异常处理器
thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { |
J.U.C
参考:
J.U.C: Java并发包,即java.util.concurrent包,是JDK的核心工具包,是JDK1.5之后,由 Doug Lea实现并引入。
整个java.util.concurrent包,按照功能可以大致划分如下:
- locks 锁框架: ReentrantLock, ReentrantReadWriteLock, LockSupport, StampedLock, Condition
- atomic 原子类框架: AtomicInteger, AtomicLong, AtomicXXXFieldUpdater, LongAdder
- sync 同步器框架: CountDownLatch, CyclicBarrier, Semaphore, Exchanger, Phaser
- collections 集合框架:
- executors 执行器框架: ThreadPoolExecutor, Fork/Join
ReentrantLock和条件对象
可重入锁: ReentrantLock
ReentrantLock是”可重入锁”: 一个线程已经持有锁的情况下, 重复对该锁进行lock()
操作, 能立刻获得锁且不会被阻塞.
ReentrantLock reentrantLock = new ReentrantLock(); |
ReentrantLock的构造函数ReentrantLock(boolean fair)
可以返回公平锁(true)和非公平锁(false).
公平锁(Fair):加锁前检查是否有排队等待的线程,如果队列非空先进入队列,获取锁的顺序同调用lock的顺序一致;
非公平锁(Nonfair):加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待;
因为队列的存在和线程调度的机制, 公平锁的吞吐量更低, 所以 ReentrantLock()默认构造是 非公平锁
锁的实现
图-ReentrantLock-AQS UML:
ReentrantLock
中有一个 Sync 类型的成员, 根据调用不同的构造方法, sync 被初始化为NonFairSync
(非公平锁, 默认) 或者FairSync
(公平锁),这两种Sync都继承自AbstractQueuedSynchronizer
, 简称AQS,是java.util.concurrent的核心,CountDownLatch
、FutureTask
、Semaphore
、ReentrantLock
等都有一个内部类是这个抽象类的子类。ReentrantLock中
所有涉及对AQS的访问都要经过Sync。
AbstractQueuedSynchronizer
有几个重要的成员变量:
1 int类型的计数器state
;
2 等待线程的队列(由head和tail指针表示的双向队列);
3 从AbstractOwnableSynchronizer
继承来的exclusiveOwnerThread
(Thread类型);
计数器是volatile修饰的, 作用是记录锁被重入的次数, 初值是0, 重入一次+1, 释放一次-1, 计数器为0表示没有线程持有该锁, 是free的;
尝试CAS修改计数器失败的线程, 会被放入队列尾部;exclusiveOwnerThread
用来记录当前该锁被哪个线程占用(但不是volatile的, 此处有疑问)
AbstractQueuedSynchronizer抽象类提供的主要的属性和方法:
public abstract class AbstractQueuedSynchronizer { |
lock()
ReentrantLock.lock()调用栈如下(以 NonFairSync为例):
ReentrantLock.lock() |
过程大致如下:
- 对计数器
CAS(0,1)
操作, (CAS(0,1)
意即为如果计数器等于期望值0则设置为1) - CAS成功, 成功获取到该锁, 并把exclusiveOwnerThread置为当前线程引用地址,
lock()
成功返回; - CAS不成功, 表明已经有线程持有该锁, 且exclusiveOwnerThread不等于当前线程, 创建当前线程的
AQS.Node
对象, 并插入AQS的队尾, 并调用LockSupport.park()
使当前Thread进入Blocked
state表示重入成功获取到锁的计数, 每次成功重入+1,
如果尝试获取锁的线程不等于已持有锁的线程, 计数不用+1, 把这个线程加入AQS队列, 并且使这个线程进入Blocked状态
unlock()
ReentrantLock.unlock()调用栈如下:
ReentrantLock.unlock() |
- 只允许已经持有锁的线程调用
unlock()
, 否则unlock()
会抛出 IllegalMonitorStateException异常 - 已经持有锁的线程, 每次调用
unlock()
计数器都会-1, 直到计数器等于0, 这时候表示锁全部被解开了, 再从AQS的队列取出第一个节点, 把这个节点对应的线程设置为Runnable
测锁与超时
ReentrantLock lock = new ReentrantLock(); |
读写锁
- 如果一个数据结构只有很少线程修改其值, 但是有很多线程读取, 这种数据结构非常适合用读写锁
ReentrantReadWriteLock
- writeLock一旦被持有, 排除其他的写锁和读锁;
- readLock一旦被持有, 排斥写锁, 但不排斥其他的读锁;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); |
条件对象: Condition
实例代码:
ReentrantLock reentrantLock = new ReentrantLock(); |
条件对象的实现
ReentrantLock.newCondition()
返回一个ConditionObject
对象, 是AbstractQueuedSynchronizer的一个内部类;
一个锁可以创建多个ConditionObject
对象;ConditionObject
的实现也是一个队列, firstWaiter和lastWaiter记录了队列的头和尾
- 一个线程调用
condition.await()
之后进入waiting状态并进入该condition的队列中, 处于waiting的线程无法改变自身状态, 只能等待其他线程调用condition.signalAll()
; - 一个线程调用
condition.signalAll()
之后, 所有在此condition等待队列中的其他线程被移出, 这些线程重新设置runnable状态, 这些线程从await()
阻塞调用里返回;
Synchronized和对象锁
本节主要介绍对同步关键字(Synchronized), 以及对象锁.
Synchronized
(1) synchronized方法:
public class Bank { |
(2) synchronized块
Object object = new Object(); |
- 每个类对象都有从Object继承的”对象锁”, synchronized方法利用这个对象锁保护方法内的代码片段.
- 对于同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前对象的Class对象。
- 对于同步方法块,锁是Synchonized括号里配置的对象。
同步方法 和 同步代码块 都是用了进入/退出Monitor对象来实现的, 但两者的实现细节不一样
实现
synchronized
- 在synchronized代码块前后增加的
monitorenter
和monitorexist
两个JVM字节码指令,指令的参数是this引用。 - synchronized关键字起到的作用是设置一个独占访问临界区,在进入这个临界区前要先获取对应的监视器锁,任何Java对象都可以成为监视器锁,声明在静态方法上时监视器锁是当前类的Class对象,实例方法上是当前实例。
- synchronized提供了原子性、可见性和防止重排序的保证。
- JMM中定义监视器锁的释放操作happen-before与后续的同一个监视器锁获取操作。再结合程序顺序规则就可以形成内存传递可见性保证。
下面以一段Java代码为例:
public class TestSynchronize { |
javap查看inc()
方法的实现:private void inc();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter // monitor 1
4: aload_0
5: dup
6: getfield #2 // Field count:I
9: iconst_1
10: iadd
11: putfield #2 // Field count:I
14: aload_1
15: monitorexit // monitor 2
16: goto 24
19: astore_2
20: aload_1
21: monitorexit // monitor 3
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
LineNumberTable:
line 14: 0
line 15: 4
在synchronized代码块前后增加的monitorenter
和monitorexist
两个JVM字节码指令,指令的参数是this引用。
hotspot中对于 monitor_enter
和 monitor_exit
的C++代码是:
void LIRGenerator::monitor_enter(LIR_Opr object, LIR_Opr lock, LIR_Opr hdr, LIR_Opr scratch, int monitor_no, CodeEmitInfo* info_for_exception, CodeEmitInfo* info) { |
锁的升级
Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,
所以在Java SE1.6里对象锁
一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态 和 重量级锁状态,
它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。
轻量级锁适合追求响应时间,同步块执行速度非常快的情况
重量级锁追求吞吐量,适合同步块执行速度较长的代码。
随着锁的升级, Java对象头里的Mark Word
存储的内容也会变化。
回顾 Java对象的内存结构: 对象有32+32个字节的”对象头”, 其中第一个32字节是”Mark Word”, 存储了hashCode, 锁信息, 以及分代信息等, 结构如下:
无锁
无锁状态下, 对象Mark Word 锁标志也是01(同偏向锁一样)
偏向锁
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁。
偏向锁只是比较,没有使用CAS操作,也没有自旋,所以在没有多线程竞争的情况下,加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。
偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。
轻量级锁(自旋)
轻量级锁是指 当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),
JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。
然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。
如果成功,当前线程获得锁,
如果失败,表示其他线程竞争锁,当前线程便尝试使用 自旋来获取锁。
因为轻量锁用到了CAS,第一次CAS失败会进入自旋,自旋虽然会消耗CPU,但不会切换线程状态,自旋较适用于锁使用者保持锁时间比较短的情况
重量级锁
重量级锁通过对象内部的监视器(monitor对象)实现的,其中monitor对象的本质是依赖于底层操作系统的Mutex Lock
实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
重量级锁不使用自旋,不会消耗CPU。但会让线程进入阻塞状态让出CPU,增加了线程切换的代价。
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
// 参考自 https://blog.csdn.net/javazejian/article/details/72828483 |
Monitor对象
上面提到重量锁的实现是利用了Monitor对象,
编译器在把java文件编译为字节码的后, 会在synchronized
代码块前后插入monitorenter
和monitorexit
指令,monitorenter
指令是在编译后插入到同步代码块的开始位置,而monitorexit
是插入到方法结束处和异常处, JVM要保证每个monitorenter
必须有对应的monitorexit
与之配对。
“任何Java对象都有一个 monitor对象 与之关联,当且一个 monitor对象 被持有后,它将处于锁定状态。”
这两个指令是通过monitor对象实现的(有关monitorenter
和monitorexit
指令的实现在JVM的InterpreterRuntime.cpp文件),
monitor对象可以看成是JVM进程里的C++对象。每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
Monitor模型中, 有三个重要的属性, 可以想象成三个不同的房间:Special Room, Entry Set, Wait Set
如果一个顾客想要进入这个特殊的房间(Special Room),他首先需要在走廊(Entry Set)排队等待。调度器将基于某个标准(比如 FIFO)来选择排队的客户进入房间。
如果,因为某些原因,该客户客户暂时因为其他事情无法脱身(线程被挂起),那么他将被送到另外一间专门用来等待的房间(Wait Set),这个房间的可以可以在稍后再次进入那件特殊的房间。
图-Monitor模型:
Object.wait()和notify()
- The Owner: 指向持有Monitor对象的线程, 同一时刻只允许一个线程持有;
- Entry Set: 尝试持有Monitor的线程都会先进入这个队列, 如果线程获取到了Monitor对象, 线程会从Entry Set队列删除, Owner同时会指向这个线程, 这个对列里的线程再次获取锁从而进入The Owner区;
- Wait Set: 调用了
object.wait()
的线程从Owner区进入Wait Set, 等待被唤醒, 如上图中的③, 注意必须拥有锁的线程才能调object.wait()
;如果调用
object.notify()
和object.notifyAll()
, 线程会进入Entry Set队列或者自旋获取Owner ?
比较ReentrantLock和synchronized
- ReentrantLock可以”可中断的”获取锁
void lockInterruptibly() throws InterruptedException
- ReentrantLock可以尝试非阻塞地获取锁
boolean tryLock()
- ReentrantLock可以超时获取锁,通过
tryLock(timeout, unit)
- ReentrantLock可以实现公平锁,通过
new ReentrantLock(true)
实现 - ReentrantLock对象可以同时绑定多个Condition对象,只需要多次调用
newCondition()
方法即可。而在synchronized中只能使用一个对象的wait()
,notify()
,notifyAll()
- Condition对应的方法是
await()
,signal()
,signalAll()
, Object对应的方法wait()
,notify()
,notifyAll()
- ReentrantLock的实现是 AQS, synchronized实现模型是 Monitor
Volatile关键字
volatile关键字特性:
- 多CPU环境的可见性: 多CPU的环境下, CPU有可能从寄存器或Cache里直接取值, 这种情况下运行在不同CPU上的线程获取的值可能不同, volitile变量可以保证每次更新都改变到主存, 每次读取都从主存中读取.
- volatile可以作为一种开销较低的免锁机制(某些情况下).
- volatile变量的”复合操作”(对变量的写操作依赖当前值)不具备原子性.
- volatile 的
long
,double
的读写不保证有原子性.
volatile不适用的情况
- 用于计数器(请使用
AomicInteger
), 虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。 - “依赖当前值”的写操作, 比如
i=i+1
- 非原子操作,
i++
,i=!i
都不是原子操作
比如以下代码是有问题的:
private volatile i = 0; |
volatile适用的情况
- 作为简单的状态标志,
vol_variable = 1
和vol_variable = 0
这种操作是原子的, 对volatile变量的赋值也对其他线程立刻可见; - 保证只有一个线程写, 其他线程只能读;
CAS
一些概念:
▷原子操作:
▷乐观锁 & 悲观锁:
- 悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,拿到锁之后才可以修改临界区数据;
- 乐观锁(Optimistic Locking): 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。上面提到的乐观锁的概念中其实已经阐述了他的具体实现细节:主要就是两个步骤:数据更新和冲突检测。其实现方式有一种比较典型的就是 Compare and Swap(CAS)。
▷CAS:
CAS(Compare And Swap):比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B并返回True,否则就什么都不做并返回False。比较+替换是一个原子操作。
Unsafe提供的CAS
上面提到ReentrantLock.lock()
的实现是通过AQS, AQS的 CAS方法实现如下:public abstract class AbstractQueuedSynchronizer {
private static final Unsafe unsafe = Unsafe.getUnsafe()
private static final long stateOffset;
static {
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
}
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
}
AQS 使用 unsafe包提供的CAS (Native方法), 然后通过JNI 调用到了Hotspot的 Unsafe.cpp中,
C++代码最终调用的是Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value)
,
cmpxchg()调用了汇编 CMPXCHG
指令,具体汇编指令可以查看Intel手册 CMPXCHG
CAS的ABA问题
ABA问题: 线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来避免,详见 [AtomicStampedReference]
Atomic
原子变量是一种更好的”Volatile”. – 《Java并发编程实战》
java提供了几个类用于原子操作:
- 原子更新基本类型:AtomicBoolean,AtomicInteger, AtomicLong.
- 原子更新数组:AtomicIntegerArray,AtomicLongArray, AtomicReferenceArray.
- 原子更新引用类型:AtomicReference, AtomicStampedReference, AtomicMarkableReference.
- 原子更新字段类型:AtomicReferenceFieldUpdater, AtomicIntegerFieldUpdater, AtomicLongFieldUpdater.
AtomicInteger atomicInteger = new AtomicInteger(); |
大多数方法都是调用sun.misc.Unsafe
里的方法实现的, sun.misc.Unsafe
只提供三种CAS方法: compareAndSwapObject
, compareAndSwapInt
和compareAndSwapLong
解决ABA问题
Java中的 AtomicStampedReference
参考 用AtomicStampedReference解决ABA问题 @Ref
ThreadLocal
ThreadLocal是一个为线程提供线程局部变量的工具类。为线程提供一个线程私有的变量副本,这样多个线程都可以随意更改自己线程局部的变量,不会影响到其他线程。不过需要注意的是,ThreadLocal提供的只是一个浅拷贝,如果变量是一个引用类型,那么就要考虑它内部的状态是否会被改变,想要解决这个问题可以通过重写ThreadLocal的initialValue()
函数来自己实现深拷贝,建议在使用ThreadLocal时一开始就重写该函数。
首次调用threadLocal.get()
方法时会调用initialValue()
赋一个初始值。
例子: 1.8之前提供的SimpleDateFormat
不是线程安全的, 下面的代码用ThreadLocal 解决这个问题:
private final static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() { |
注: JDK1.8的 DateTimeFormatter是线程安全的.
实现
我们需要知道Thread类中有一个threadLocals变量:
public class Thread { |
ThreadLocal
中含有一个叫做ThreadLocalMap
的内部类,该类为一个采用线性探测法实现的HashMap
。
这个 HashMap 的 Entry继承了WeakReference: Entry(ThreadLocal,Object)
,它的 key为ThreadLocal
对象, value是缓存的本地对象,
从 ThreadLocal中 get值的时候,首先通过 Thread.currentThread
得到当前线程,然后拿到这个线程的 ThreadLocalMap,取得Entry中的value值。
下面是ThreadLocalMap
代码片段:
static class ThreadLocalMap { |
ThreadLocal
中只有三个成员变量,这三个变量都是与ThreadLocalMap
的hash策略相关的:
/** |
唯一的实例变量threadLocalHashCode
是用来进行寻址的hashcode,它由函数 nextHashCode()
生成,该函数简单地通过一个增量HASH_INCREMENT来生成hashcode。至于为什么这个增量为0x61c88647,主要是因为ThreadLocalMap的初始大小为16,每次扩容都会为原来的2倍,这样它的容量永远为2的n次方,该增量选为0x61c88647也是为了尽可能均匀地分布,减少碰撞冲突。
ThreadLocal
部分参考自: 聊一聊Spring中的线程安全性 | SylvanasSun’s Blog @Ref
InheritableThreadLocal
如果在父线程中创建 ThreadLocal,会发现父线程设置的值在子线程中无法获取,JDK中有InheritableThreadLocal解决此问题。
public class SubThreadUsage { |
实现
InheritableThreadLocal 继承了 ThreadLocal
在 Thread 构造方法调用的 init() 中,可看见如果 parent.inheritableThreadLocals不为空,则 ThreadLocal.createInheritedMap()拷贝 ThreadLocalMap,注意这里的拷贝是浅拷贝。子线程如果修改了继承自父线程的ThreadLocal,其他的子线程也可能会看到这个改变。
Executor线程池
线程池相关类和方法:
- ExecutorService: Java线程池的接口, 提供了如下方法:
void execute(Runnable command)
执行Ruannable类型的任务Future<?> submit(Runnable task)
可用来提交Callable或Runnable任务,并返回代表此任务的Future对象Future<T> submit(Callable<T> task)
: 同上void shutdown()
: 关闭线程池,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。最终调用了每个线程的interrupt()
void shutdownNow()
: 关闭线程池, 中断正在处理任务的线程,也不处理阻塞队列中已保存的任务。最终调用了每个线程的interrupt()
boolean isShutdown()
- ThreadPoolExecutor: 实现了ExecutorService接口, 通用线程池
- ScheduledExecutorService: ExecutorService的实现类, 用执行定时任务
ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit)
: 执行定时任务
- Executors: 线程池的工厂类, 用于创建线程池
ExecutorService newCachedThreadPool()
: 创建一个可缓存线程池,队列容量固定是1(可以认为没有队列),线程数会一直增长(如果没有空闲线程),如果线程空闲超过60s会被回收;ExecutorService newFixedThreadPool(int nThreads)
: 创建一个定长线程池,超出的线程会进入等待队列,队列是无限大的;ExecutorService newSingleThreadExecutor()
: 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
: 创建一个定长线程池,支持定时及周期性任务执行。
示例代码:
public static void tpoolTest() { |
线程池的shutdown
或者shutdownNow
方法来关闭线程池。原理是遍历线程池的工作线程,然后逐个调用线程的interrupt
方法来中断线程,所以无法响应中断的任务可能永远无法停止。
两者的区别:shutdown
方法将执行平缓的关闭过程:不在接收新的任务,同时等待已提交的任务执行完成,包括哪些还未开始执行的任务。shutdownNow
方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。
线程池的实现
构造方法
工厂类Executors
包装了对ThreadPoolExecutor
构造方法的调用, 隐藏了很多创建线程池的细节, 所以在并发严格的情况下, 最好的方式还是直接调用ThreadPoolExecutor
构造方法创建线程池.
ThreadPoolExecutor的构造函数:
public class ThreadPoolExecutor extends AbstractExecutorService { |
构造器中各个参数的含义:
- corePoolSize: (线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到线程数大于 corePoolSize 时就不再创建。如果调用了线程池的
prestartAllCoreThreads()
方法,线程池会提前创建并启动所有基本线程。 - workQueue: 一个阻塞队列,用来存储等待执行的任务。当线程数已经大于corePoolSize时, 再向线程池添加任务,会把任务放入该队列中。阻塞队列有以下几种选择:
LinkedBlockingQueue
:一个基于链表结构的 无界阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue
。
静态工厂方法Executors.newFixedThreadPool()
使用了这个队列。SynchronousQueue
:一个不存储元素的 有界阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态(可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开。),吞吐量通常要高于LinkedBlockingQueue
,
静态工厂方法Executors.newCachedThreadPool
使用了这个队列。ArrayBlockingQueue
:基于数组结构的 有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。PriorityBlockingQueue
:一个具有优先级的 无限阻塞队列。
- maximumPoolSize: 线程池允许创建的最大线程数(池+队列里的线程数)。线程池新增任务时判断顺序
corePoolSize?
->workQueue?
->maximumPoolSize
- 如果是
LinkedBlockingQueue
这种 近似无界的队列,maximumPoolSize
没有效果; - 如果是
ArrayBlockingQueue
这种 有界阻塞队列,如果队列满了,并且已创建的线程数小于maximumPoolSize
,则线程池会再创建新的线程执行任务,直到总线程数超过maximumPoolSize
。
- 如果是
- keepAliveTime: 工作线程空闲后,保持存活的时间。线程池会一直终止空闲超过keepAliveTime的线程,直到线程池中的线程数不超过
corePoolSize
。 - unit: keepAliveTime的单位
- handler: 当队列和线程池都满了(
maximumPoolSize
),说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。- AbortPolicy:直接抛出异常。
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
再回过来看Executors提供的几种工厂方法:
newCachedThreadPool()
: corePoolSize为0, maximumPoolSize为INT.Max, 队列使用SynchronousQueue不存储线程, 所以有新任务提交时, 如果没有空闲的线程, 则继续创建新的线程, 直到线程数达到INT.Max
. 空闲时间超过60s的线程会被回收;newFixedThreadPool(int nThreads)
: corePoolSize和maximumPoolSize都是nThreads, 意味着线程池大小从0会增长到coreSize, 队列是近似无界队列LinkedBlockingQueue, 可以一直接收新任务, keepAliveTime=0意味着不会回收空闲线程newSingleThreadExecutor()
: 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
所以,
使用newCachedThreadPool()
的问题在于, 如果没有控制好任务大小(所有线程一直在忙) 线程数会一直增长(maxPoolSize
是Integer.MAX_VALUE
). 只有线程空闲的时候才有机会减少线程数.
使用newFixedThreadPool()
的问题在于, 虽然工作线程数是固定的, 但是等待队列大小是Integer.MAX_VALUE
,
这两种线程池都有可能因为创建大量线程导致OOM.
工作线程
线程池创建线程时,会将工作线程封装成Worker类,Worker在执行完任务后,还会无限循环获取工作队列里的任务来执行。
我们可以从Worker的run方法里看到这点:
public void run() { |
优化线程池
线程池参数的解析和使用建议:
- 线程池大小:
- 如果是计算密集任务,一般设置为cpu核心数,
- 如果是IO密集任务一般设置为核心数2~3倍;// 理论值, 实际工程中远比这个大
- 默认情况下,核心工作线程值在初始的时候被创建,当新任务来到的时候被启动,但是我们可以通过重写
prestartCoreThread
或prestartCoreThreads
方法来改变这种行为。
通常场景我们可以在应用启动的时候来WarmUp核心线程,从而达到任务过来能够立马执行的结果,使得初始任务处理的时间得到一定优化。 - 合理的拒绝策略: @TODO
- 队列的选择:
- 无界队列:
- 使用无界队列如
LinkedBlockingQueue
没有指定最大容量的时候,将会引起当核心线程都在忙的时候,新的任务被放在队列上。
因此,永远不会有大于corePoolSize
的线程被创建,因此maximumPoolSize
参数将失效。
这种策略比较适合所有的任务都不相互依赖,独立执行。如Web服务器中,每个线程独立处理请求。
但是当任务处理速度小于任务进入速度的时候会引起队列的无限膨胀。 - 先级不同的任务可以使用优先级队列
PriorityBlockingQueue
来处理。它可以让优先级高的任务先得到执行,
需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
- 使用无界队列如
- 有界队列:有界队列如
ArrayBlockingQueue
帮助限制资源的消耗,但是不容易控制。
队列长度和maximumPoolSize
这两个值会相互影响,
使用 大的队列 和 小maximumPoolSize
会降低CPU占用、操作系统资源、上下文切换的消耗,但是会降低吞吐量,如果任务被频繁的阻塞如IO线程,系统其实可以调度更多的线程。
使用 小的队列 通常需要大maximumPoolSize
,从而使得CPU更忙一些,但是又会增加降低吞吐量的线程调度的消耗。
总结一下:是IO密集型可以考虑 多些线程+小的队列 来平衡CPU的使用,CPU密集型可以考虑 少些线程+大的队列 减少线程调度的消耗。
- 无界队列:
线程池的监控:
通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用
- taskCount:线程池需要执行的任务数量。
- completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。
- largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
- getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不+ getActiveCount:获取活动的线程数。
通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute
,afterExecute
和terminated
方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。如:
参考:
- @Ref 聊聊并发(三)——JAVA线程池的分析和使用
- @Ref 如何合理地估算线程池大小? | 并发编程网 – ifeve.com
- @Ref java线程池大小为何会大多被设置成CPU核心数+1? - 知乎
Future
Callable
接口类似于Runnable,但是Runnable不会返回结果,并且无法抛出返回结果的异常,Callable
功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future
拿到。
如果主线程发起IO操作并轮询等待返回结果,这种很适合用Callable/Future。
Callable
有些类似Runnable
, 它们都是接口, 前者需要实现V call()
, 后者需要实现void run()
;- 需要用
FutureTask
包装一下Callable
,FutureTask
提供了get()
方法, 可以获取执行结果; - 创建
Thread
实例, 通过构造器Thread(FutureTask)
, 这里实际还是调用的Thread(Runnable)
,FutureTask
接口继承自Runnable
; - Future是
ExecutorService.submit(Callable)
返回的类型; - 实际上
FutureTask
实现了Future
接口, 通过FutureTask
和Future
类型引用都可以调用get()
,cancel()
,isDone()
,isCancelled()
等方法;
public class FutureAndFutureTaskExample { |
问题: FutureTask.cancel()
和 Thread.interrupt()
有什么区别?
通过查看cancel()
的源码发现, 实际cancel()
最终还是调用了Thread.interrupt()
, 所以, FutureTask.cancel()
也无法真正停止异步任务,
如果真的需要任务可以被终止/取消, 那么就需要在Runnable
或Callable
的主循环里捕捉InterruptException异常.
ListenableFuture(Guava)
Guava的 Listenable Future对 Future做了改进,支持注册一个任务执行结束后回调函数。
// 创建一个 ListenableFuture |
CompletableFuture(Java8)
本节参考:
Java8的CompletableFuture参考了Guava的ListenableFuture的思路,CompletableFuture能够将回调放到与任务不同的线程中执行,也能将回调作为继续执行的同步函数,在与任务相同的线程中执行。
CompletableFuture弥补了Future模式的缺点。在异步的任务完成后,需要用其结果继续操作时,无需等待。可以直接通过thenAccept、thenApply、thenCompose等方式将前面异步处理的结果交给另外一个异步事件处理线程来处理。
与Guava ListenableFuture相比,CompletableFuture不仅可以在任务完成时注册回调通知,而且可以指定任意线程,实现了真正的异步非阻塞。
▶ 创建一个CompletableFuture:public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
runAsync方法不支持返回值/supplyAsync可以支持返回值
没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。
▶ 使用 thenApply 串行任务:public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
当一个线程依赖另一个线程时,可以使用 thenApply 方法来把这两个线程串行化。
T:上一个任务返回结果的类型
U:当前任务的返回值类型
▶使用 thenAccept 消费处理结果:public CompletionStage<Void> thenAccept(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor);
▶ 使用 thenCombine 合并任务:public <U,V> CompletionStage<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn,Executor executor);
thenCombine 会把 两个 CompletionStage 的任务都执行完成后,把两个任务的结果一块交给 thenCombine 来处理。
▶ 使用 thenCompose 流水化处理任务:public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn);
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) ;
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor) ;
thenCompose 方法允许你对两个 CompletionStage 进行流水线操作,第一个操作完成时,将其结果作为参数传递给第二个操作。
▶ 代码示例1: thenApply/whenComplete/exceptionally
public static void example() throws Exception { |
▶ anyOf / allOfpublic static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs);
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs);
anyOf: 当任意一个CompletableFuture完成后, 创建一个完成的CompletableFuture
allOf: 当所有的阶段完成后, 创建一个完成的CompletableFuture
public static CompletableFuture<List> example() { |
Fork/Join框架
Java在JDK 7之后加入了并行计算的框架Fork/Join,可以解决我们系统中大数据计算的性能问题。Fork/Join采用的是分治法,Fork是将一个大任务拆分成若干个子任务,子任务分别去计算,而Join是获取到子任务的计算结果,然后合并,这个是递归的过程。子任务被分配到不同的核上执行时,效率最高。
Fork/Join框架的核心是ForkJoinPool
(类似ExecuteService
会给线程池中的线程分发任务,不同之处在于它使用了工作窃取算法,所谓工作窃取,是采用分治法的思想,将一个大任务拆分为若干互不依赖的子任务,把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务。同时,为了最大限度地提高并行处理能力,采用了工作窃取算法来运行任务,也就是说当某个线程处理完自己工作队列中的任务后,尝试当其他线程的工作队列中窃取一个任务来执行,直到所有任务处理完毕。
ForkJoinTask
是一个抽象类,有两个实现子类,RecursiveTask
(有返回值)和RecursiveAction
(无返回结果),我们自己定义任务时,只需选择这两个类继承即可。
继承RecursiveTask
和RecursiveAction
类必须实现compute()
方法,在这个方法里要实现递归控制条件。
compute()
的实现通常为:
if (任务足够小){ |
下面是一个计算数组之和的Fork/Join例子:
public class CJForkJoinTask extends RecursiveTask<Integer> |
线程安全的集合
旧的线程安全的集合: 任何集合类都可以通过使用同步包装器变成线程安全的:
List<E> synchArrayList = Collections.synchronizedList(new ArrayList<E>()); |
java.util.concurrent包提供了线程安全的集合, 继承关系如下:
阻塞队列 |
线程安全的队列
Name | 是否阻塞 | 是否有界 | 队列长度 | 内部实现 |
---|---|---|---|---|
ArrayBlockingQueue | 阻塞 | 有界 | 构造器指定 | 循环数组,FIFO |
LinkedBlockingQueue | 阻塞 | 有界 | 构造器指定, 默认Int.Max | 链表,FIFO |
LinkedBlockingDeque | 阻塞 | 有界 | 构造器指定, 默认Int.Max | 双向链表,FIFO |
SynchronousQueue | 阻塞 | 有界 | 1 | |
PriorityBlockingQueue | 阻塞 | 无界 | 构造器指定, 默认11, 无限扩容 | 二叉堆 |
DelayQueue | 阻塞 | 无界 | 初始empty, 无限扩容 | |
ConcurrentLinkedQueue | 非阻塞 | 无界 | 初始empty, 无限扩容 | 单向链表 |
ConcurrentLinkedDeque | 非阻塞 | 无界 | 初始empty, 无限扩容 | 双向链表 |
线程安全的队列可以分为 阻塞队列 , 非阻塞队列, 按照是否可无限扩容分为 有界队列 , 无界队列 :
- 阻塞队列是指: 当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。
阻塞队列一般是用锁(例如BlockingQueue
)来实现,
阻塞队列继承自接口BlockingQueue
, 常用的有:ArrayBlockingQueue
,LinkedBlockingQueue
,PriorityBlockingQueue
,LinkedBlockingDeque
; - 非阻塞队列是指:
非阻塞队列一般是用CAS
实现的”Lock-Free”方法,
非阻塞队列主要有:ConcurrentLinkedQueue
,ConcurrentLinkedDeque
; - 有界/无界: 无界队列可以无限扩容
阻塞队列
阻塞队列一般使用condition实现消费者和生产者的”通讯”。
比如当生产者往满的队列里添加元素时会阻塞住,当消费者消费了队列中的元素后,会通过condition通知生产者当前队列可用。
BlockingQueue接口方法有put/take:
阻塞方法:
- put(E o):将元素添加到此队列尾,如果队列满将一直阻塞,可以响应中断。
- take():检索并移除此队列的头部,如果队列为空则一直阻塞,可以响应中断。
不阻塞且抛异常的方法:
- add(E o):将元素添加到此队列中,如果队列已满不会阻塞,直接抛出 IllegalStateException
- remove(): 移除队列头部的元素,如果队列为空不会阻塞,直接抛出 IllegalStateException
不阻塞且带返回值的方法:
- offer(E o): 将元素添加到队列,不阻塞,成功返回true,失败返回false;
- offer(E o, long timeout, TimeUnit unit): 带等待时间的offer方法,如果队列已满,将等待指定的时间;
- poll(long timeout, TimeUnit unit): 返回队列的头部并移除,如果队列为空,则等待指定等待的时间。如果取不到返回null;
其他方法:
- drainTo(Collection<? super E> c): 移除此队列中所有可用的元素,并将它们添加到给定 collection 中。
- drainTo(Collection<? super E> c,int maxElements): 最多从此队列中移除给定数量的可用元素,并将这些元素添加到给定 collection 中
- remainingCapacity(): 返回在无阻塞的理想情况下(不存在内存或资源约束)此队列能接受的元素数量;如果没有内部限制,则返回 Integer.MAX_VALUE。
ArrayBlockingQueue
- ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序
- ArrayBlockingQueue内部由 一个公共的读写锁,两个Condition(notFull、notEmpty) 管理队列满或空时的阻塞状态。
因为在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue。 - 构造器
ArrayBlockingQueue(int)
都要指定数组初始大小,并且大小不再扩展。 - 默认情况下ArrayBlockingQueue不保证访问者公平的访问队列,所谓“公平访问队列”是指:当队列可用时,可以按照阻塞的先后顺序访问队列。即:
先阻塞的生产者线程,可以先往队列里插入元素;
先阻塞的消费者线程,可以先从队列里获取元素。
我们可以使用以下代码创建一个“公平的”阻塞队列:ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
LinkedBlockingQueue
- LinkedBlockingQueue是链表实现的“有界”的阻塞队列。构造函数可以指定最大长度,队列的默认和最大长度为
Integer.MAX_VALUE
- 内部基于链表实现,由两个锁(takeLock与putLock),以及 两个Condition(notFull、notEmpty) 管理队列满或空时的阻塞状态。
由于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
LinkedBlockingDeque
- LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。构造函数可以指定最大长度,队列的默认和最大长度为
Integer.MAX_VALUE
- 相比其他的阻塞单向队列,LinkedBlockingDeque多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法
SynchronousQueue
SynchronousQueue是无界的,是一种无缓冲的等待队列,
但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加,可以认为SynchronousQueue是一个缓存值为1的阻塞队列,
但是 isEmpty()方法永远返回是true,remainingCapacity() 方法永远返回是0,remove()和removeAll() 方法永远返回是false,iterator()方法永远返回空,peek()方法永远返回null。
声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
DelayQueue
- DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。
队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将DelayQueue运用在以下应用场景:- 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
- 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。
- 队列中的Delayed必须实现compareTo来指定元素的顺序。
PriorityBlockingQueue
- PriorityBlockingQueue是一个支持优先级的无界队列。默认情况下元素采取自然顺序排列,也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列。
- 内部基于二叉堆。使用一把公共的读写锁。虽然实现了BlockingQueue接口,其实没有任何阻塞队列的特征,空间不够时会自动扩容。
- 构造器:
PriorityBlockingQueue()
:默认数组初始大小11,不指定Comparator,存入的对象需要实现Comparable
接口;PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator)
;
非阻塞队列
ConcurrentLinkedQueue
- ConcurrentLinkedQueue是一个基于链接节点的无边界的线程安全队列,它采用FIFO原则对元素进行排序。采用“wait-free”算法(即CAS)来实现的。
- ConcurrentLinkedQueue的结构是单向链表和head/tail两个指针,因为入队时需要修改队尾元素的next指针,以及修改tail指向新入队的元素两个CAS动作无法原子,所以需要的特殊的算法,见:
Java 理论与实践: 非阻塞算法简介
ConcurrentLinkedDeque
- ConcurrentLinkedDeque是一种基于双向链表的无界链表。
- 与大多数集合类型不同,其size方法不是一个常量操作。因为链表的异步性质,确定当前元素的数量需要遍历所有的元素,所以如果在遍历期间有其他线程修改了这个集合,size方法就可能会报告不准确的结果。
- 批量的操作:包括添加、删除或检查多个元素,比如addAll()、removeIf()或者removeIf() 或forEach()方法,这个类型并不保证以原子方式执行。由此可见如果想保证原子访问,不得使用批量操作的方法。
Set
ConcurrentSkipListSet
ConcurrentSkipListSet的实现非常简单,其内部引用了一个ConcurrentSkipListMap对象,所有API方法都是调用了ConcurrentSkipListMap。
ConcurrentSkipListSet和TreeSet,它们虽然都是有序的集。但是:
第一,它们的线程安全机制不同,TreeSet是非线程安全的,而ConcurrentSkipListSet是线程安全的;
第二,ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,而TreeSet是通过TreeMap实现的;
Map
ConcurrentHashMap
- 数据分段存储,每个段有一个写锁,当一个线程占用某个段的锁时,其他段也可以正常访问,有效分散了阻塞的概率,而且没有读锁;
- 没有读锁是因为put/remove动作是个原子动作(比如put是一个对数组元素/Entry 指针的赋值操作),读操作不会看到一个更新动作的中间状态;
- 每次扩容为原来容量的2倍,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容;
- 在获取size操作的时候,不是直接把所有segment的count相加就可以可到整个ConcurrentHashMap大小,也不是在统计size的时候把所有的segment的put、remove、clean方法全部锁住,这种方法太低效。
在累加count操作过程中,之前累加过的count发生变化的几率非常小,所有ConcurrentHashMap的做法是先尝试2(RETRIES_BEFORE_LOCK)次通过不锁住Segment的方式统计各个Segment大小,如果统计的过程中,容器的count发生了变化,再采用加锁的方式来统计所有的Segment的大小。 - putIfAbsent(k,v):当k已经存在时返回已存在的v。
ConcurrentSkipListMap
- JDK6新增的并发优化的SortedMap,以SkipList实现。SkipList是红黑树的一种简化替代方案,是个流行的有序集合算法。Concurrent包选用它是因为它支持基于CAS的无锁算法,而红黑树则没有好的无锁算法。
- ConcurrentSkipListMap 的key是有序的;
- 与ConcurrentHashMap相比,ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap 的存取时间是log(n),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出优势。
- 它的size()比较特殊,需要遍历所有元素;
Deprecated: Vector & HashTable
Vector和HashTable已经被弃用,取而代之的是ArrayList和HashMap,如果要使用线程安全的容器,可以用Collections转换:
List<E> syncList = Collections.synchronzedList(new ArrayList<E>()); |
计数器CountDownLatch
CountDownLatch是在java1.5被引入的,跟它一起被引入的并发工具类还有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue,它们都存在于java.util.concurrent包下。
CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。
CountDownLatch提供了类似计数器的同步手段, 构造器和主要方法:
public CountDownLatch(int count) { }; //参数count为计数值 |
Example:
public class Test { |
其他参考: 什么时候使用CountDownLatch - ImportNew @Ref
信号量Semaphore
Semaphore翻译成字面意思为 “信号量”,Semaphore可以控同时访问的任务个数,通过 acquire() 获取一个许可,如果没有就等待; release() 释放一个许可。
构造器和主要方法:
//参数permits表示许可数目,即同时可以允许多少线程进行访问 |
线程间交换数据的Exchanger
@TODO
网络编程
Server/Client的Socket API介绍.
Server
ServerSocket server = new ServerSocket(9090); |
Client
/* 方式1 */ |
半关闭
- Socket.shutdownOutput():
- Socket.shutdownInput():
- boolean isOutputShutdown():
- boolean isInputShutdown():
Socket socket = new Socket(host, port); |
可中断套接字
当连接到一个套接字时,当前线程将会被阻塞直到建立连接或产生超时为止。java.nio包提供的一个特性——SocketChannel类,与上面的Socket不同,SocketChannel是可以中断的
如果发生中断, 下面的操作不会阻塞, 而是抛出异常
SocketChannel channel = SocketChannel.open(); |
NIO
从BIO到NIO
BIO 即阻塞 I/O,不管是磁盘 I/O 还是网络 I/O,数据在写入 OutputStream 或者从 InputStream 读取时都有可能会阻塞。
一旦有线程阻塞将会失去 CPU 的使用权,这在当前的大规模访问量和有性能要求情况下是不能接受的。
虽然当前的网络 I/O 有一些解决办法,如一个客户端一个处理线程,出现阻塞时只是一个线程阻塞而不会影响其它线程工作,还有为了减少系统线程的开销,采用线程池的办法来减少线程创建和回收的成本,但是有一些使用场景仍然是无法解决的。
Java NIO是java 1.4之后新出的一套IO接口,这里的的新是相对于原有标准的Java IO和Java Networking接口。NIO提供了一种完全不同的操作方式。
NIO(Non-blocking I/O)是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。
NIO包介绍
Java Non-blocking I/O主要有三大核心部分:Channel
(通道),Buffer
(缓冲区), Selector
;
除此之外,Java NIO还包括了新的文件/目录的操作: Path
和Files
。
- java.nio.channels 包:
java.nio.channels.ServerSocketChannel
java.nio.channels.SocketChannel
java.nio.channels.FileChannel
java.nio.channels.SocketChannel.Selector
类- java.nio.Buff 接口:
java.nio.ByteBuffer
: 最基本的字符buff, 从Channel
(ServerSocketChannel, FileChannel等)读取出的内容放在ByteBuffer
里, 或者通过Channel.write
把ByteBuffer内容写入Channel;java.nio.MappedByteBuffer
: FileChannel通道打开的文件映射到内存, 通过MappedByteBuffer
来操作;
- java.nio.file 包:
java.nio.file.Path
: Path的实例指代一个目录或文件java.nio.file.Paths
: Path的工厂类, 用于获取Path实例java.nio.file.Files
: 提供对Path
的操作
▶ BIO和NIO的对比变化如下:
- (1) BIO流 vs NIO管道:
- Java BIO的各种流的读写都是阻塞操作。这意味着一个线程一旦调用了read(),write()方法但系统缓冲区没数据可读,那么该线程会进入阻塞状态(Blocked)。
- NIO读写都是非阻塞的, NIO基于Channel(管道)和Buffer(缓冲区)进行操作:数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Channel可以是文件也可以是Socket;
- (2) NIO里新增了Selector,用于监听多个Channel的事件,当Channel产生可读写事件后, 用ByteBuffer读取数据。
Selector允许一个单一线程监听多个Channel输入。我们可以注册多个Channel到Selector上,然后然后用一个线程来挑出一个处于可读或者可写状态的Channel。Selector机制使得单线程管理过个Channel变得容易。 - (3) NIO的提供了
Path
和Files
来取代io包中的File
,Path
的实例指代一个目录或文件,Files
则提供了对目录或文件的基本操作(exists, copy, move, delete)
NIO ByteBuffer
ByteBuffer的属性、方法:
- 属性 capacity >= limit >= position >= mark
capacity
: 指定数组大小, Buffer创建后就不可改变;limit
: 最大可以读的到位置, 初始值等于capacity, 调用flip()方法后limit=positionposition
: 当前的读写位置, 初始值0, 每次写入一个字节position+1, 每次读都是从position++位置读一个字节mark
: 初始值-1, 备忘位置, 参加mark()/reset()方法
ByteBuffer.allocate(int)
: 创建buff并初始化大小put(byte)
,put(byte[])
: 向buff存储数据get()
, 返回position位置的一个byteflip()
, 向Buffer写完数据开始读数据前要调用一次, 把position的值赋给limit, 然后position=0, 然后可以调用get()从position读出字节;rewind()
, position=0, mark=-1, 不改变limit的值, 可以再读一遍[0~limit]的字节mark()
: mark=position, 调用mark()来记录当前positionreset()
: position=mark, 调用reset()让position置为mark的值, 一次reset()对应一次mark()clear()
: limit,position,mark置为初始值;compact()
: 清除未读的数据, 将所有未读的数据拷贝到buffer起始处equals()
: 比较两个buff剩余未读的字节数, 比较剩余的每一个字节compareTo()
: ..
ByteBuffer内部是由一个数组实现的, 所以capacity理论最大值受
MAX_Integer
和-Xmx
限制
NIO Channel
@TODO
NIO Selector
@TODO
Files & Path
示例代码:
public class NioPathAndFiles { |
NIO网络读写
API说明:
- 服务端:
ServerSocketChannel.open()
: 创建一个server socket channel实例, 相当于传统Socket的ServerSocket
ServerSocketChannel.socket().bind(SocketAddress local)
: 绑定端口ServerSocketChannel.configureBlocking(false)
: 把server socket channel设置为 非阻塞 的情况下,accept()/read()/write()
会立刻返回;ServerSocketChannel.accept()
: 阻塞, 并在有客户端成功连接时返回一个SocketChannel
实例ServerSocketChannel.register(Selector, EVENT)
: 为server channel注册监听的事件
- Selector:
Selector.open()
: 创建一个selector实例Selector.select()
: 开始监听并阻塞
- 客户端:
SocketChannel.configureBlocking(false)
: 把socket channel设置为非阻塞, 读写会立刻返回SocketChannel.write(ByteBuffer)
: 写方法SocketChannel.read(ByteBuffer)
: 读方法, 返回值是读取的字节数
用NIO API实现简单的Socket Server(用Selector
实现多路复用, 用Channel.configureBlocking(false)
设置为非阻塞I/O):
ByteBuffer echoBuffer = ByteBuffer.allocate(1024); |
总结: NIO 的Socket 多路复用如下:
- 创建服务端 socketChannel
- 创建 Selector
- 服务端 socketChannel 在 Selector上注册 ACCEPT事件
- While循环
- selector.select() 阻塞, 如果 Selector上有事件发生, 退出阻塞
- selector取出所有事件集合, 并遍历
- 如果有 ACCEPT 事件, 服务端 socketChannel去accept这个请求, 创建 客户端 socketChannel, 并在Selector上注册该 channel的 READ事件
- 如果有 READ 事件, 读对应的 客户端 socketChannel
与传统Socket比较
从上面的代码可以看到,
- 传统的Java Socket(BIO, 阻塞IO), 等同于
java.net + java.io
, 使用的”Socket句柄”是java.net.ServerSocket
(服务端socket)和java.net.Socket
(客户端socket), 通过Socket
获取InputStream/OutpubtStream进行读/写. - NIO Socket使用的”socket句柄”是
java.nio.channels
包下面的ServerSocketChannel
和SocketChannel
, SocketChannel的读写是通过java.nio.ByteBuffer
- 前者IO方法是阻塞的, 后者IO方法是非阻塞 // ?
多线程-BIO缺陷
- 线程的创建和销毁成本很高
- 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千…
- 线程的切换成本是很高
- 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,高并发下会使系统负载压力过大
BIO(阻塞IO)模型,之所以需要多线程,是因为在进行I/O操作的时候,一是没有办法知道到底能不能写、能不能读,只能阻塞等待。
NIO的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机会:如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件事记下来,记录的方式通常是在Selector上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写。
NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。
单线程处理I/O的效率确实非常高,没有线程切换,只是拼命的读、写、选择事件。
以上参考: Java NIO浅析 - @Ref
NIO大文件读写
大文件读写几种方案:
- 传统IO读取方式:
- 字节方式读取: FileInputStream VS BufferedInputStream
- 字符方式读取: BufferedReader
- NIO读取:
- FileChannel + ByteBuffer
- MappedByteBuffer(内存映射)
测试结论参考: JAVA NIO(六):读取10G的文件其实很容易 - CSDN博客 @Ref
传统NIO读取:
java.io.RandomAccessFile
提供了文件随机读写,
下面的代码是使用nio中的FileChannel
和ByteBuffer
从RandomAccessFile
中读取:
RandomAccessFile randomAccessFile = new RandomAccessFile(new File(filePath), "r"); |
使用内存映射:
nio.FileChannel还提供了内存映射的方式读取文件:
RandomAccessFile randomAccessFile = new RandomAccessFile(new File(filePath), "r"); |
内存映射读取的优劣
- 内存映射方式的读取速度更快
- read()是系统调用, 首先将文件从硬盘拷贝到内核空间的一个缓冲区, 再将这些数据拷贝到用户空间, 实际上进行了两次数据拷贝.
- map()也是系统调用, 但没有进行数据拷贝, 当缺页中断发生时, 直接将文件从硬盘拷贝到用户空间, 只进行了一次数据拷贝.
- MappedByteBuffer使用虚拟内存, 因此分配(map)的内存大小不受JVM的-Xmx参数限制, 但是也是有大小限制的;
- 如果当文件超出1.5G限制时, 可以通过position参数重新
map(mode, position, size)
文件后面的内容; - MappedByteBuffer在处理大文件时的确性能很高, 但也存在一些问题, 如内存占用/文件关闭不确定, 被其打开的文件只有在垃圾回收的才会被关闭, 而且这个时间点是不确定的。javadoc中也提到:”A mapped byte buffer and the file mapping that it represents remain* valid until the buffer itself is garbage-collected.”
参考: 深入浅出 MappedByteBuffer v
可以响应线程中断的Channel
本章参考
注解
本章包括: 注解的定义和使用, JavaSE的标准注解和元注解.
使用注解
// Class annotation |
定义一个注解
所有注解都隐式的继承自java.lang.annotation.Annotation
// Controller: |
如何获取带有特定注解(这里用@YourAnnotation注解为例)的类, 使用了org.reflections.Reflections工具类:Reflections reflections = new Reflections("org.test");
Set<Class<?>> classes = reflections.getTypesAnnotatedWith(YourAnnotation.class);
标准注解
JavaSE在java.lang.annotation和javax.annotation包定义了大量注解, 其中4个是元注解, 用于定义一般注解 &描述注解的行为属性.
- @Deprecated: 所有场合,包 & 类 & 方法 & 属性
- @SuppressWarnings: 类 & 方法 & 属性, 阻止某种警告信息,
@SuppressWarnings(value={"unchecked","deprecation"})
- @Override: 只有方法
- @Resources: ?
- @Resource: 可以写在属性上, 和setter方法上, 默认按照名称进行装配
- @PostConstruct 方法, 指明该方法在构造器之后立刻被调用
- @PreDestory 方法, 指明该方法在类被销毁前调用
元注解
- @Target:
@Target(ElementType.TYPE)
// 类, 接口, 枚举, 注解@Target(ElementType.METHOD)
//方法@Target(ElementType.PARAMETER)
//方法参数@Target(ElementType.FIELD)
//字段
- @Retention:
@Retention(RetentionPolicy.SOURCE)
//注解仅存在于源码中, 在class字节码文件中不包含@Retention(RetentionPolicy.CLASS)
// 默认的保留策略, 注解会在class字节码文件中存在, 但运行时无法获得,@Retention(RetentionPolicy.RUNTIME)
// 注解会在class字节码文件中存在, 在运行时可以通过反射获取到
- @Document: 说明该注解将被包含在javadoc中
- @Inherited: 一般在定义注解时使用, 说明这个子类可以继承父类中的这个注解
安全
java.security 包提供了消息摘要/消息签名等算法.
将长度不固定的消息(message)作为输入参数,运行特定的Hash函数,生成固定长度的输出,这个输出就是Hash,也称为这个消息的消息摘要(Message Digest)
消息签名可以看成是在密钥加密的基础上的消息摘要, 消息摘要和消息签名的作用:
- 数据完整性检查
- 数据校验, 是否在传递过程中被篡改
施工中…
消息摘要(Message Digest)
有如下几种消息摘要:
- MD5, 任何消息都压缩为16字节(128位)的摘要(指纹), 不推荐使用MD5的原因是?
- SHA1(属于SHA一代), 任何消息都压缩为20字节(160位)的摘要, 所以SHA-1共有最多2^120个摘要;
- SHA256(属于SHA二代), 32字节(256位);
- SHA512
- MAC(或者HMAC算法), 在散列基础上增加了密钥;
- BCrypt: 根据Blowfish加密算法所设计的密码散列函数
// MD5 & SHA |
消息签名(Message Signature)
- DSA(数字签名)/RSA(公钥/私钥), 例如DSA是利用了对数值巨大的数字进行因数分解的困难性.
对称加密
- DES
- AES取代DES
- Blowfish: 对称密钥区块加密算法
/***** DES *****/ |
blowfish & bcrypt
Encryption with BlowFish in Java - Stack Overflow
// blowfish |
BCrypt是基于Blowfish加密算法所设计的密码散列函数, 代码jBCrypt - strong password hashing for Java
// bcrypt |
非对称加密
- RSA: @TODO
Native Method
实现一个Native方法:
声明java native method:
public class CJNativeInterfaceDemo {
public native String input(String prompt);
static {
System.loadLibrary("./libJniTest.so");
}
public static void main(String[] args) {
CJNativeInterfaceDemo jniDemo = new CJNativeInterfaceDemo();
jniDemo.input("JNI Test");
}
}生成c++头文件
javac CJNativeInterfaceDemo.java
生成.class文件javah -jni CJNativeInterfaceDemo
生成.h文件
实现C++函数并编译成动态库
gcc -I/usr/lib/jvm/java-7-openjdk-i386/include/ CJNativeInterfaceDemo.c -shared -o libJniTest.so
Java 8
Lambda
“Lambda 表达式”(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。
闭包的概念: 可以把闭包简单理解成”定义在一个函数内部的函数体”,并且在内部函数体中能访问在外部函数中定义的变量
lambda的语法为: expression
= (variable) -> action
, 例如
Runnable r = () -> { log.info"HelloWorld";}
int sum = (x,y) -> x+y;
- 等号的右边即是一个lambda表达式
Lambda表达式要点总结
- lambda表达式可以用于以下几个情况:
- 有单个抽象方法的类, 比如一个方法接收Runnable、Comparable或者 Callable 接口,都有单个抽象方法,可以传入lambda表达式;
- 使用了 @FunctionalInterface 注释的函数式接口,比如
java.util.function
包下面的Predicate、Function、Consumer 或 Supplier, BinaryOperator- 例如ArrayList的
forEach(Consumer<E> action)
方法的形参是Consumer类型, 可以接受一个lambda表达式做实参; - 例如Collection的stream()返回一个Stream, Stream类的
filter()
,map()
的形参分别是Predicate和Function;
- 例如ArrayList的
- lambda表达式内可以使用方法引用,仅当该方法不修改lambda表达式提供的参数。例如
list.forEach(System.out::println)
- Lambda表达式在Java中又称为闭包或匿名函数
- Lambda方法在编译器内部被翻译成私有方法,并派发 invokedynamic 字节码指令来进行调用。使用 javap -p 或 javap -c -v 命令来看一看lambda表达式生成的字节码。大致应该长这样:
private static java.lang.Object lambda$0(java.lang.String);
- lambda内部可以使用静态、非静态和局部变量,这称为lambda内的变量捕获。
- lambda表达式有个限制,那就是只能引用 final 或 final 局部变量,这就是说不能在lambda内部修改定义在域外的变量,读取是可以的但不能修改。
int factor = 2;
primes.forEach(element -> { System.out.println(factor*element); });
创建匿名类
例1:
new Thread(new Runnable() { |
例2: 你们最讨厌的Comparator接口
Comparator<Score> byName = new Comparator<Score>() { |
表达式迭代 forEach
List list = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API"); |
map() & reduce()
// 为每个订单的价格加上12$的税, 并求和 |
函数式接口
什么是函数式接口? 简单说就是只拥有一个抽象方法的接口,如Runnable
Function功能型函数式接口
类 java.util.function.Function<T,R>
相当于仅含有一个方法的接口类, 这个方法接收一个参数T, 返回类型R.
在Java8中, 这种接口类可以用一个lambda表达式来表示.
Function只有一个方法apply, 该方法接收一个参数并返回一个值:
Function<Integer, Integer> func = x -> x*2;
Integer ii = func.apply(100);
除了👆上面这种形式, 在Java8中还增加了::
, 称为”方法引用操作符”, 对象::方法
将返回一个函数接口(function interface),
我们可以使用它来引用类的方法. 例如:
class MyMath{
public double square(double num){
return Math.pow(num , 2);
}
}
MyMath myMath = new MyMath();
Function<Double, Double> square = myMath::square; // 声明一个函数式接口实例, 相当于把square方法抽取出来, 增加给这个实例
double ans = square.apply(23.0);
注意被
::
引用的方法需要符合“函数式接口” (一个输入参数一个返回值)
Predicate断言型函数式接口
类 java.util.function.Predicate<T>
相当于一个”接收一个输入参数T, 返回boolean的lambda表达式”类型 :
Predicate<Integer> pred = x -> x>5;
boolean ret = pred.test();
使用::
方法引用操作符:
Set<String> set = new HashSet<>();
set.addAll(Arrays.asList("one","two","three"));
Predicate<String> pred = set::contains;
boolean exists = pred.test("one");
Predicate.test()的更多例子:
List languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp"); |
Predicate.and(), or(), xor()的例子:
List languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp"); |
Consumer消费型函数式接口
@TODO
Supplier供给型函数式接口
@TODO
Stream API
stream并不是某种数据结构,它只是数据源的一种视图。这里的数据源可以是一个数组,Java容器或I/O channel等。正因如此要得到一个stream通常不会手动创建,而是调用对应的工具方法,比如:
- 调用
Collection.stream()
或者Collection.parallelStream()
方法 - 调用
Arrays.stream(T[] array)
方法 - Map类容器无法直接用
stream()
, 但可以使用map.entrySet().stream()
获得流
常见的stream接口继承关系如图:
流(Stream)的特性
大部分情况下Stream是容器调用Collection.stream()
方法得到的,但Stream和Collections有以下不同:
- 无存储。stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
- 为函数式编程而生。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。
- 惰式执行。stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
- 可消费性。stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。
中间操作 & 结束操作
对stream的操作分为为两类,中间操作(intermediate operations)和结束操作(terminal operations),二者特点是:
- 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新stream,仅此而已。
- 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数。计算完成之后stream就会失效。
下表汇总了Stream接口的部分常见方法:
operator | function |
---|---|
中间操作 | concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered() |
结束操作 | allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray() |
中间操作
filter
filter(): 函数原型为Stream<T> filter(Predicate<? super T> predicate)
,作用是返回一个只包含满足predicate条件元素的Stream。
predicate 可以看成是返回boolean的lambda表达式
下面例子中, filter方法接收一个predicate
类型的参数:
// 保留长度等于3的字符串
Stream<String> stream = Stream.of("Java", "Scala", "C++", "Haskell", "Lisp");
stream.filter(str -> str.length()==3)
.forEach(str -> System.out.println(str));
下面例子中, filter接收的参数是list2::contains
, 被引用的方法(这里的contain方法)需符合“predicate”原型:
// 求list1和list2的交集
List<T> intersect = list1.stream()
.filter(list2::contains)
.collect(Collectors.toList());
distinct
distinct(): 函数原型为Stream<T> distinct()
,作用是返回一个去除重复元素之后的Stream。
stream.distinct()
.forEach(str -> System.out.println(str));
limit & skip
limit(n)/skip(n): limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素
sort
sorted(): 排序函数有两个,一个是用自然顺序排序,一个是使用自定义比较器排序,函数原型分别为Stream<T> sorted()
和Stream<T> sorted(Comparator<? super T> comparator)
。
stream().sorted((x, y) -> x-y ).collect(Collectors.toList()); |
map
map(): 对当前Stream所有元素执行mapper操作, 返回新的Stream
stream.map(str -> str.toUpperCase())
.forEach(str -> System.out.println(str));
flatMap
flatMap(): “摊平”
// 把List<Int> 摊平成 Int
Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
stream.flatMap(list -> list.stream())
.forEach(i -> System.out.println(i));
结束操作
结束操作包括collect, reduce, forEach等, 分别用于聚合和遍历.
forEach
forEach是结束操作, 会立刻执行, 执行结束后Stream失效.
方法定义为void forEach(Consumer<? super E> action)
,作用是对容器中的每个元素执行action指定的动作,也就是对元素进行遍历。
通常我们在使用forEach时, 也会用来做合并操作。
使用Stream.forEach()迭代
Stream<String> stream = Stream.of("Java", "Scala", "C++", "Haskell", "Lisp");
stream.forEach(str -> System.out.println(str));在forEach中进行合并:
// Combine map1 and map2
// Map.merge()用于相同k的合并
Map<String,Integer> mergedMap = new HashMap(map1);
map2.forEach((k,v) -> mergedMap.merge(k,v, Integer::Sum));
reduce()
规约操作(reduction operation)又被称作折叠操作(fold),是通过某个连接动作将所有元素汇总成一个汇总结果的过程。元素求和、求最大值或最小值、求出元素总个数、将所有元素转换成一个列表或集合,都属于规约操作。
Stream类库有两个通用的规约操作reduce()
和collect()
,也有一些为简化书写而设计的专用规约操作,比如sum()
、max()
、min()
、count()
等。
其原型为:Optional<T> reduce(BinaryOperator<T> accumulator)
reduce()
最常用的场景就是从一组值中生成一个值,reduce()
的方法定义有三种重写形式:
Optional<T> reduce(BinaryOperator<T> accumulator)
: 返回的类型Optional
表示(一个)值的容器,使用它可以避免null值的麻烦。// 找出最长的单词
Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
System.out.println(longest.get());T reduce(T identity, BinaryOperator<T> accumulator)
:int[] array = {23,43,56,97,32};
// 求所有元素的和:
Integer sum = Arrays.stream(array).reduce(0, (a, b) -> a+b);
// 等价于:
Integer sum = Arrays.stream(array).reduce(0, Integer::sum);<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
:
它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。// 求单词长度之和
Stream<String> stream = Stream.of("Java", "Scala", "C++", "Haskell", "Lisp");
Integer lengthSum = stream.reduce(0, // 初始值
(sum, str) -> sum+str.length(), // 累加器
(a, b) -> a+b); // 部分和拼接器,并行执行时才会用到更多
reduce()
的例子:// 求最小值, 有起始值
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 无起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 字符串连接,有起始值, concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);
// 字符串连接,有起始值, 有filter操作, concat = "ace"
String concat = Stream.of("a", "B", "c", "D", "e", "F").
filter(x -> x.compareTo("Z") > 0).
reduce("", String::concat);
collect()
Stream.collect()
方法和类Collectors
一起使用, 常用于把一个Stream的结果收集进容器里,
考虑一下将一个Stream转换成一个容器(或者Map)需要做哪些工作?我们至少需要两样东西:
- 目标容器是什么?是ArrayList还是HashSet,或者是个TreeMap。
- 新元素如何添加到容器中?是List.add()还是Map.put()。
- 如果并行的进行规约,还需要告诉collect() 多个部分结果如何合并成一个。
结合以上分析,collect()方法定义为<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
三个参数依次对应上述三条分析。不过每次调用collect()都要传入这三个参数太麻烦,收集器Collectors就是对这三个参数的简单封装,所以collect()的另一定义为<R,A> R collect(Collector<? super T,A,R> collector)
。
一些例子:
将Stream规约成List
Stream<String> stream = Stream.of("Java", "Scala", "C++", "Haskell", "Lisp");
List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
List<String> list = stream.collect(Collectors.toList());// 方式2
System.out.println(list);将Stream转换成List 或Set
Stream<String> stream = Stream.of("Java", "Scala", "C++", "Haskell", "Lisp");
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());Stream转换成map & map排序:
// Stream转换成map:
// Function.identity()返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。
Map<Integer, String> map = stream.collect(Collectors.toMap(Function.identity(), String::length));
// map排序 & 取TopN:
// 对Entry的流进行排序, 然后生成有序的LinkedHashMap:
Map<String ,Long> sortedMap = unsortedMap.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(topN)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(oldValue, newValue) -> oldValue,
LinkedHashMap::new));合并Map1, Map2
Map<String, Integer> mx = Stream.of(m1, m2)
.map(Map::entrySet) // converts each map into an entry set
.flatMap(Collection::stream) // converts each set into an entry stream, then
// "concatenates" it in place of the original set
.collect(
Collectors.toMap( // collects into a map
Map.Entry::getKey, // where each entry is based
Map.Entry::getValue, // on the entries in the stream
Integer::max // such that if a value already exist for
// a given key, the max of the old
// and new value is taken
)
);拼接字符串
Stream<String> stream = Stream.of("Java", "Scala", "C++", "Haskell", "Lisp");
String mergedString = stream.filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));上述代码能够满足大部分需求,但由于返回结果是接口类型,我们并不知道类库实际选择的容器类型是什么,有时候我们可能会想要人为指定容器的实际类型,这个需求可通过
Collectors.toCollection(Supplier<C> collectionFactory)
方法完成。// 使用toCollection()指定规约容器的类型
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)
Stream的底层实现
- stream(): Stream只是一个接口,并没有操作的缺省实现。最主要的实现是ReferencePipeline,而它的一些具体实现又是由AbstractPipeline完成的
- parrallelStream(): 底层使用的是ForkJoinPool, 比较适合使用计算密集型并且没有I/O的任务
新的Data & Time
@TODO
参考: Java 8 新特性概述 @Ref
Java 9
参考: Java 9 新特性概述 @Ref
Java 10
参考: Java 10 新特性介绍 @Ref
附录:JDK常用类
java.lang📦继承关系图
java.util📦继承关系图
附录:补码,反码
反码: 正数的反码是本身, 负数的反码=符号位不变, 其他位取反
补码: 正数的补码是本身, 负数的补码=符号位不变, 其他位取反, 再加1
看几组补码-真值: “1111 1111”=-1, “1000 0010”=-126, “1000 001”=-127, “1000 0000”=-128
不要用计算补码的方式去”算”-128的补码, 1000 0000 是定义的.
参考:
- @Ref 原码, 反码, 补码 详解
- @Ref 原码、反码和补码
附录:运算符
/
整数除法 15/2 = 7%
取余, 或者叫取模
C++里的函数
mod(a, b)
: 等同于a%b
, 取余floor(a)
: 返回小于等于a的整数,floor(2.5)=2
,floor(-3.5)=-3
ceil(a)
: 返回大于等于a的整数,ceil(2.5)=3
位运算符(java)
&
符号位|
符号位~
符号位^
符号位<<
左移: 丢弃最高位(符号位同样丢弃), 0补最低位. 当byte和short左移时, 自动升级为int型.- 数学意义: 左移n位相等于乘以2^n
>>
右移: 高位补充符号位, 正数右移补充0, 负数右移补充1, 当byte和short右移时, 自动升级为int型.- 数学意义: 右移n位相当于除以2^n
>>>
无符号右移: 无论正负, 高位补充0- 无符号右移运算符>>> 只是对32位和64位的值有意义
参考
- 《Java核心技术 卷 I 第九版》 @Ref