泛型是计算机编程中一种重要的思维方式,它将程序算法与数据类型相分离,使得同一套程序算法能够应用于各种数据类型,并且可以保证类型安全,提高可读性。
通俗来说,泛型就是类型参数化,即通过参数的形式传入类型,将代码与具体的数据类型解绑,同一套代码可用于多种数据类型。
x1/* 泛型类 Pair,拥有两个泛型 U 和 V */
2public class Pair<U, V> {
3 U first;
4 V second;
5
6 public Pair(U first, V second) {
7 this.first = first;
8 this.second = second;
9 }
10
11 public U getFirst() {
12 return first;
13 }
14
15 public V getSecond() {
16 return second;
17 }
18
19 public static void main(String[] args) {
20 // 泛型类实例化01 传入泛型参数 <String, String> => <U,T>
21 Pair<String, String> pair01 = new Pair<String, String>("老马", "说编程");
22
23 // 泛型类实例化02 传入泛型参数 <String, Integer> => <U,T>
24 Pair<String, Integer> pair02 = new Pair<String, Integer>("老马", 100);
25
26 // 使用泛型类对象, 无需强制类型转换
27 String first = pair01.getFirst();
28 String second = pair01.getSecond();
29
30 // 提示:JDK7+版本,new后面的类型参数可以省略,会自动推断
31 Pair<String, Integer> pair02_ = new Pair<>("老马", 100);
32 }
33}
34
泛型的本质是类型擦除(这对后面理解泛型非常重要)。在编译过程中,所有的泛型都将会被替换为Object类(或其上界类),并在合适的位置插入必要的强制类型转换,虚拟机只能执行这种转换后的非泛型代码。
271// 编译后:泛型被擦除,泛型类型被替换为Object(或其上界类)
2public class ErasedPair {
3 Object first;
4 Object second;
5
6 public ErasedPair(Object first, Object second) {
7 this.first = first;
8 this.second = second;
9 }
10
11 public Object getFirst() {
12 return first;
13 }
14
15 public Object getSecond() {
16 return second;
17 }
18
19 public static void main(String[] args) {
20 ErasedPair pair01 = new ErasedPair("老马", 100);
21
22 // 编译后:在使用处插入强制类型转换
23 String first = (String) pair01.getFirst(); // "老马"
24 Integer second = (Integer) pair01.getSecond(); // 100
25 }
26}
27
前面得知,泛型类最后依旧会被转换为非泛型类,那么我们使用泛型类有什么好处呢? 主要有两点:
避免强制类型转换,做到在编译期进行类型安全检查,防止类型转换异常(ClassCastException)。
精简代码,增强代码的健壮性和可维护性。
泛型根据定义的位置不同,分为泛型类、泛型接口、泛型方法三类。
601// 1. 泛型类:在类名后声明泛型,后面将在实例化泛型类时具体化泛型(E)
2public class DynamicArray<E> {
3 private static final int DEFAULT_CAPACITY = 10;
4 private int size;
5 private Object[] elementData;
6
7 public DynamicArray() {
8 this.elementData = new Object[DEFAULT_CAPACITY];
9 }
10
11 // 动态扩容
12 private void ensureCapacity(int minCapacity) {
13 int oldCapacity = elementData.length;
14 if (oldCapacity >= minCapacity) {
15 return;
16 }
17 int newCapacity = oldCapacity * 2;
18 if (newCapacity < minCapacity) {
19 newCapacity = minCapacity;
20 }
21 elementData = Arrays.copyOf(elementData, newCapacity); // 拷贝到newCapacity长度的新数组
22 }
23
24 public void add(E e) {
25 ensureCapacity(size + 1);
26 elementData[size++] = e;
27 }
28
29 public E get(int index) {
30 return (E) elementData[index];
31 }
32
33 public int size() {
34 return size;
35 }
36
37 public E set(int index, E element) {
38 E oldValue = get(index);
39 elementData[index] = element;
40 return oldValue;
41 }
42
43 public static void main(String[] args) {
44 // 2. 在实例化泛型类时具体化泛型(E)
45 // 2.1 具体化为具体类型,此时具体化为Double类型
46 DynamicArray<Double> doubleDynamicArray = new DynamicArray<>();
47 doubleDynamicArray.add(1.23);
48
49 // 2.2 具体化为具体类型中的Object类型(不推荐这样使用,因为后面使用时需要强制类型转换,违背了泛型的本意)
50 DynamicArray<Object> objectDynamicArray = new DynamicArray<>();
51 DynamicArray defaultDynamicArray = new DynamicArray(); // 具体化过程<xxx>可以省略,默认为<Object>
52 defaultDynamicArray.add("老马");
53 defaultDynamicArray.add(100);
54
55 // 2.3 具体化为其它泛型类,此时具体化为Pair<String, Integer>类型(其中Pair的泛型U,V分别具体化为String, Integer类型)
56 DynamicArray<Pair<String, Integer>> pairDynamicArray = new DynamicArray<>();
57 pairDynamicArray.add(new Pair<>("老马", 100));
58 }
59}
60
491// 1. 泛型接口:在接口名后声明泛型,后续将在实现接口时具体化泛型(T)
2public interface Comparable<T> {
3 int compareTo(T o);
4}
5
6// 2. 在实现接口时具体化泛型(T)
7// 2.1 具体化为具体类型,此处为Integer
8public class MyInteger implements Comparable<Integer> {
9 private Integer num;
10
11
12 public int compareTo(Integer o) { // 可以与Integer类型的其它对象比较
13 return this.num - o;
14 }
15}
16
17// 2.2 具体化为具体类型,此处为Object
18public class MyObject implements Comparable<Object> {
19 private Object num;
20
21
22 public int compareTo(Object o) { // 可以与Object类型的其它对象比较
23 return this.num.hashCode() - o.hashCode();
24 }
25}
26
27// 2.3 具体化为新声明的泛型(泛型传递),此时具体化为子类新声明的(TT)
28// 注意:这里为了区分Comparable的泛型名称T,取名为TT;实际上,新声明的泛型也可以叫T;
29public class MyInfo<TT> implements Comparable<TT> {
30 private TT info;
31
32 public MyInfo(TT info) {
33 this.info = info;
34 }
35
36
37 public int compareTo(TT o) {
38 return info.hashCode() - o.hashCode(); // 可以与TT类型的其它对象比较
39 }
40
41 public static void main(String[] args) {
42 // 2.3 在实例化子类时,将具体化子类的泛型TT,此时TT具体化为String类型
43 // 进而,将已知的TT类型(String)传递给Comparable接口的泛型T(泛型传递)
44 MyInfo<String> stringMyInfo = new MyInfo<>("hyx");
45 int compare = stringMyInfo.compareTo("hyx2");
46
47 }
48}
49
251// 1. 泛型方法:在返回值之前声明泛型
2public static <T> int indexOf(T[] arr, T elm) {
3 for (int i = 0; i < arr.length; i++) {
4 if (arr[i].equals(elm)) {
5 return i;
6 }
7 }
8 return -1;
9}
10
11// 扩展:多个泛型参数的泛型方法
12public static <U, V> Pair<U, V> makePair(U first, V second) {
13 Pair<U, V> pair = new Pair<>(first, second);
14 return pair;
15}
16
17public static void main(String[] args) {
18 // 2. 在使用方法时具体化泛型(T)
19 int indexOf01 = indexOf(new Integer[]{1, 3, 5}, 10); // 此时具体化为具体类型Integer
20 int indexOf02 = indexOf(new String[]{"hello", "老马", "编程"}, "老马"); // 此时具体化为具体类型String
21
22 // 注意:和泛型类/泛型接口不同,泛型方法在使用时一般并不需要特意指定泛型的具体类型,它会根据实参自动推断
23 Pair<Integer, String> pair = makePair(1, "老马");
24}
25
注意:
如果方法为静态方法,那么将不能够使用类上声明的泛型(静态变量同理),因为他们是类级别共享的。
一个方法是不是泛型的,与它所在的类是不是泛型没有任何关系,可以使用类上的泛型,也可以新定义方法的泛型。
虽然泛型在编译时被擦除为Object或上界类,但是在运行时,泛型引用的对象是实际的不同具体类型,并且可以获取和使用该类型信息。
311public class MyWrapper<T> {
2 private T data;
3
4 public MyWrapper(T data) {
5 this.data = data;
6 }
7
8 public Class<?> getDataType() {
9 // 具体化类型的Class信息
10 return data.getClass();
11 }
12
13 public T create() throws IllegalAccessException, InstantiationException {
14 return (T) getDataType().newInstance();
15 }
16
17 public static void main(String[] args) throws InstantiationException, IllegalAccessException {
18 // 1. 使用不同的类型具体化泛型
19 MyWrapper<String> strWrapper = new MyWrapper<>("abc");
20 MyWrapper<Date> dateWrapper = new MyWrapper<>(new Date());
21
22 // 2. 获取具体化类型的Class信息
23 System.out.println(strWrapper.getDataType()); // class java.lang.String
24 System.out.println(dateWrapper.getDataType()); // class java.lang.Integer
25
26 // 3. 使用“具体化类型的Class信息”创建“具体化类型对象”
27 Date date = dateWrapper.create();
28 System.out.println(date); // Wed Feb 08 16:53:14 CST 2023
29 }
30}
31
在上述案例中,声明泛型时未做任何额外的限制,因此在泛型具体化时,可以使用任意类型,这种未被限制的泛型称为无限定泛型。也正是因为在具体化时没有限制类型的取值范围,因此无限定泛型在使用时将会受到一些限制。
例如,在通过泛型引用E e
操作指向的对象时,由于E可能是任意类型,因此只能调用任意类型的根类Object的属性或方法。
101public void method01(E e) {
2 // 1. 只能调用任意类型的根类Object的属性或方法
3 e.hashCode(); // Object的方法,OK
4 //e.childClassMethod(); // 非Object方法,ERR
5
6 // 2. 只能赋值给Object或同类型引用E
7 Object o = e;
8 E ee = e;
9 //Number n = e; // Error: 不兼容的类型: E无法转换为java.lang.Number
10}
为了减弱上述限制,我们可以在声明泛型时进一步限定泛型可具体化的类型范围,要求其必须继承某类或实现某个接口,这样就可以在保证类型安全的前提下使用该类(接口)的一系列方法了,这种被限制可具体化类型范围的泛型称为限定泛型。
上文提到的Pair<U,V>
类,对其进行扩展,限定泛型可具体化的类型必须是Number或其子类,格式为:<泛型名 extends 上界类名>
。
191// 1. 泛型类NumberPair,声明了泛型U,V,并限定必须为Number或其子类
2public class NumberPair<U extends Number, V extends Number> extends Pair<U, V> {
3 public NumberPair(U first, V second) {
4 super(first, second);
5 }
6
7 // 2. 泛型进行限定后,就可以使用上界类的一系列属性和方法了
8 public double sum() {
9 return getFirst().doubleValue() + getSecond().doubleValue(); // 使用Number的doubleValue()方法
10 }
11
12 public static void main(String[] args) {
13 // 3. 在实例化时,泛型的具体化类型必须为Number或其子类
14 // NumberPair<String, Long> pair = new NumberPair<String, Long>(1, 2L); // Type parameter 'java.lang.String' is not within its bound; should extend 'java.lang.Number'
15 NumberPair<Integer, Long> pair = new NumberPair<>(1, 2L); // Integer, Long都是Number的子类
16 double sum = pair.sum();
17 }
18}
19
注意:
对于限定泛型,在进行类型擦除时,将转换为它的上界类。
上界接口可以存在多个,如:
T extends Base & Comparable & Serializable
,其中Base为上界类,其它为上界接口。
上界类也可以是一个带泛型的泛型类,那么在声明限定泛型时,必须对上界类的泛型进行具体化:
661import java.lang.Comparable;
2
3public class MainTest {
4 // 1. 上界类的泛型具体化为某个具体类型(由于该具体类型难以和原限定泛型T有交互,因此使用极少)
5 // 调用要求:限定泛型T的具体化类型必须实现Comparable<String>接口
6 public static <T extends Comparable<String>> T maxInString(T[] arr) {
7 T max = arr[0];
8 for (int i = 1; i < arr.length; i++) {
9 if (arr[i].compareTo(max.toString()) > 0) { // compareTo的形参类型为String
10 max = arr[i];
11 }
12 }
13 return max;
14 }
15
16 // 2. 上界类的泛型具体化为原限定类型T(递归类型限制,常用)
17 // 调用要求:限定泛型T的具体化类型必须实现Comparable<T>接口
18 public static <T extends Comparable<T>> T maxInT(T[] arr) {
19 T max = arr[0];
20 for (int i = 1; i < arr.length; i++) {
21 if (arr[i].compareTo(max) > 0) { // compareTo的形参类型为T
22 max = arr[i];
23 }
24 }
25 return max;
26 }
27
28 // 3.上界类的泛型具体化为Object(使用极少)
29 // 调用要求:限定泛型T的具体化类型必须实现Comparable<Object>接口
30 public static <T extends Comparable<Object>> T maxInObject(T[] arr) {
31 T max = arr[0];
32 for (int i = 1; i < arr.length; i++) {
33 if (arr[i].compareTo(max) > 0) { // compareTo的形参类型为Object
34 max = arr[i];
35 }
36 }
37 return max;
38 }
39
40 // 4. 上界类的泛型具体化类型省略(这里不是默认为Object类型,而是为?类型,后文将会讲解)
41 // 调用要求:限定泛型T的具体化类型必须实现Comparable<?>接口,?表示任意类型
42 public static <T extends Comparable> T maxInAnyone(T[] arr) {
43 T max = arr[0];
44 for (int i = 1; i < arr.length; i++) {
45 if (arr[i].compareTo(max) > 0) { // compareTo的形参类型为Object
46 max = arr[i];
47 }
48 }
49 return max;
50 }
51
52 public static void main(String[] args) {
53 // 定义一个String类型的数组,String实现了java.lang.Comparable<String>接口
54 String[] stringArr = {"a", "b", "c"};
55
56 // 可以使用maxInString、maxInT、maxInAnyone方法
57 maxInString(stringArr);
58 maxInT(stringArr);
59 maxInAnyone(stringArr);
60
61 // 不允许使用maxInObject方法,因为String并未实现Comparable<Object>接口
62 // maxInObject(stringArr);
63 }
64}
65
66
注意:
在实例化泛型类时,如果泛型的具体类型省略,将默认为Object类型,但是在具体化上界类时,并非如此。
上界类还可以是已声明的其它泛型,当该泛型被具体化时,才会确定上界类的具体类型。如上述的DynamicArray的addAll()方法:
131// 1. 上界类的类型为类上声明的泛型E,即限定泛型T的上界为E
2public <T extends E> void addAll(DynamicArray<T> c) {
3 for (int i = 0; i < c.size; i++) {
4 add(c.get(i)); // 解释:由于T是E或其子类,因此可以调用add(E e)方法
5 }
6}
7
8public static void main(String[] args) {
9 // 通过 T extends E 使DynamicArray<Number>容器允许添加DynamicArray<Integer>容器中的对象
10 DynamicArray<Number> numberArr = new DynamicArray<>();
11 numberArr.addAll(new DynamicArray<Integer>());
12}
13
如果不使用T extends E
将会怎样?即addAll()方法定义如下所示,可以看到,将会出现编译错误。
141// 直接使用类上的泛型E
2public void addAll2(DynamicArray<E> c) { // 只能接收DynamicArray<E>类型
3 for (int i = 0; i < c.size; i++) {
4 add(c.get(i));
5 }
6}
7
8public static void main(String[] args) {
9 DynamicArray<Number> numberArr = new DynamicArray<>();
10
11 // err 提示需要一个DynamicArray <Number>类型,但是提供了一个DynamicArray <Integer>类型
12 numberArr.addAll2(new DynamicArray<Integer>());
13}
14
为什么会出现编译错误呢?我们分析下,如果DynamicArray<Integer>能给DynamicArray<Number>赋值将会怎么样?
71// 如果上述DynamicArray<Integer>类型可以赋值给DynamicArray<Number>类型
2DynamicArray<Integer> integerArray = new DynamicArray<>();
3DynamicArray<Number> numberArray = integerArray; // 假设成立
4
5// 由于numberArray的泛型的具体类型为Number,那么将可以通过add(E e)方法添加Double类型的数据到之前的integerArray之中!
6numberArray.add(new Double(1.2)); // 如果上述假设成立那么将会出现该非法操作
7
注意:
在add()方法中,形参为E,类型擦除后转换为Number,可以传入Number及Integer等子类;
但是在addAll()方法中,形参为DynamicArray<E>,类型擦除后为DynamicArray<Number>,而DynamicArray<Integer>是不允许传给DynamicArray<Number>的,否则将会出现上述隐患;
在泛型具体化时(而非声明时),支持一些通配符的使用,它可以通配多种具体类型,但同时也带来了一些限制,下面将会详细介绍。
通用泛型通配符用于在具体化泛型时通配所有的具体类型,它简化了泛型的声明和使用,格式为:?
。
101// 在具体化类泛型E时,使用?通配所有的具体类型
2public static int indexOf(DynamicArray<?> arr, Object elm) {
3 for (int i = 0; i < arr.size(); i++) {
4 if (arr.get(i).equals(elm)) { // 由于arr.get(i)的具体类型是?,因此只能调用Object对应的方法
5 return i;
6 }
7 }
8 return -1;
9}
10
相应的,由于具体类型未知,因此在使用被通配的泛型对象时,也有一些限制。
81// 1. 只能使用Object类型作为引用
2Object o = arr.get(0);
3
4// 2. 只能调用任意类型的根类Object的属性和方法
5arr.get(0).hashCode();
6
7// 3. 不能当作任何对象的引用,即不能被赋值
8arr.get(0) = new Object() // err
为减弱上述限制,根据不同的使用场景,提供了两种特定通配范围的泛型通配符:子类型泛型通配符和超类型泛型通配符。
子类型泛型通配符对通配的具体类型范围做出了一些限制,用于通配ParentClass其子类,格式为:? extends ParentClass
。
171// 在具体化类泛型E时,通配E及其子类
2public void addAll(DynamicArray<? extends E> c) {
3 for (int i = 0; i < c.size; i++) {
4 add(c.get(i));
5 }
6}
7
8public static void main(String[] args) {
9 // Number容器添加Integer容器的所有数据
10 DynamicArray<Number> numbers = new DynamicArray<>();
11 DynamicArray<Integer> ints = new DynamicArray<>();
12 ints.add(100);
13 numbers.addAll(ints); // 这里类泛型E为Number,由于addAll方法在实例化E时通配了所有Number的子类,因此可以正确传参
14}
15
16// adAll方法的泛型具体化为什么不是<E>:虽然可以调用add(E e),但是采用严格匹配,Number容器无法添加Integer容器数据
17// adAll方法的泛型具体化为什么不是<?>:虽然可以传参进来,但是add(E e)无法调用,因为?不一定是E或其子类
在得知子类型泛型通配符只通配某个类及其子类后,那么就可以确定它的上界类了,上界类确定后就可以使用上界类的属性和方法,并且可以赋值给上界类。(注意:该例中上界类为E,同样是一个未知类型,因此没有其它额外的方法可以调用,同样也只能够赋值给E)
超类型泛型通配符和子类型泛型通配符相反,它用于通配ChildClass及其父类,格式为:? super ChildClass
。
191// 在具体化类泛型E时,通配E及其父类
2public void copyTo(DynamicArray<? super E> dest) {
3 for (int i = 0; i < size; i++) {
4 dest.add(get(i)); // get(i)的类型为E;dest容器的类型为E或其父类;
5 }
6}
7
8public static void main(String[] args) {
9 // Integer容器数据拷贝到另外的Number容器中
10 DynamicArray<Integer> ints2 = new DynamicArray<Integer>();
11 ints.add(100);
12 ints.add(34);
13 DynamicArray<Number> numbers2 = new DynamicArray<Number>();
14 ints.copyTo(numbers);
15}
16
17// copyTo方法的泛型具体化为什么不是<E>:虽然可以调用add(E e),但是采用严格匹配,Number容器数据无法添加到Integer容器
18// copyTo方法的泛型具体化为什么不是<?>:虽然可以传参进来,但是add(E e)无法调用,因为?不一定是E或其父类
19// copyTo方法的泛型具体化为什么不是<? extends E>:add(E e)无法调用,虽然?一定是E或其子类,但不一定是E或其父类
在得知超类型泛型通配符只通配某个类及其父类后,那么就可以确定它的下界类了,下界类确定后就可以使用下界类作为引用。
注意:关于限定泛型、子类型通配符、超类型通配符的赋值兼容
91// 限定泛型<T extends Number>,表示限定具体化类型只能是Number或其子类
2
3// 通配Number及其子类,由于Integer是Number的子类,所以可以赋值
4DynamicArray<? extends Number> numberArray = new DynamicArray<Integer>();
5
6// 通配Number及其父类,由于Integer不是Number的父类,所以不可以赋值
7DynamicArray<? super Number> numberArray = new DynamicArray<Integer>(); // err
8DynamicArray<? super Number> numberArray2 = new DynamicArray<Object>(); // OK,Object是Number的父类,可以通配
9
再来看另外一个关于超类型通配符的使用场景:
601// 普通类Base继承了Comparable<Base>
2class Base implements Comparable<Base> {
3 private int sortOrder;
4
5 public Base(int sortOrder) {
6 this.sortOrder = sortOrder;
7 }
8
9
10 public int compareTo(Base o) {
11 if (sortOrder < o.sortOrder) {
12 return -1;
13 } else if (sortOrder > o.sortOrder) {
14 return 1;
15 } else {
16 return 0;
17 }
18 }
19}
20
21// Child继承了Base,相当于间接继承了Comparable<Base>,因此继承了int compareTo(Base o)方法
22class Child extends Base {
23 public Child(int sortOrder) {
24 super(sortOrder);
25 }
26
27 // max方法声明方式1
28 public static <T extends Comparable<T>> T max(DynamicArray<T> arr) {
29 if (arr == null || arr.size() == 0) {
30 return null;
31 }
32
33 T max = arr.get(0);
34 for (int i = 1; i < arr.size(); i++) {
35 if (arr.get(i).compareTo(max) > 0) {
36 max = arr.get(i);
37 }
38 }
39
40 return max;
41 }
42
43 // max方法声明方式2:引入超类型通配符
44 public static <T extends Comparable<? super T>> T max(DynamicArray<T> arr) {
45 // ...
46 }
47
48 // 如下为测试代码
49 public static void main(String[] args) {
50 DynamicArray<Child> childs = new DynamicArray<Child>();
51 childs.add(new Child(20));
52 childs.add(new Child(80));
53
54 // 调用max,其中T为Child
55 // 方式1:要求T(Child)必须实现Comparable<Child>接口,但是Child实现的是Comparable<Base>接口,错误!
56 // 方式2:要求T(Child)必须实现Comparable<? super Child>接口,其中Comparable的泛型可以通配Child及其父类,自然也可以是base,OK!
57 Child maxChild = max(childs);
58 }
59}
60
限定泛型在声明泛型时使用,泛型通配符在具体化泛型时使用,它们的使用时机不同。
泛型通配符形式和限定泛型往往配合使用,如下面的swap()方法。
191// 1. 通配所有类型,用户阅读和使用更加方便
2// 注意:由于arr使用了通用泛型通配符,未知类型(arr.get(j))不能给set()方法的第二个形参(也是未知类型的引用)赋值
3public static void swap(DynamicArray<?> arr, int i, int j) {
4 swapInternal(arr, i, j); // 内部调用泛型方法
5}
6
7// 2. 私有的泛型方法,内部能够使用set()方法
8private static <T> void swapInternal(DynamicArray<T> arr, int i, int j) {
9 T tmp = arr.get(i);
10 arr.set(i, arr.get(j));
11 arr.set(j, tmp);
12}
13
14// 3. 其它综合使用案例
15public static <T extends Comparable<? super T>> void sort(List<T> list)
16public static <T> void sort(List<T> list, Comparator<? super T> c)
17public static <T> void copy(List<? super T> dest, List<? extends T> src)
18public static <T> T max(Collection<? extends T> coll,Comparator<? super T> comp)
19
通常子类型泛型通配符可以使用限定泛型来实现,但是方法返回值依赖于泛型等特殊情况除外。
341// 拷贝容器数据,方式1:
2public static <D, S extends D> void copy(DynamicArray<D> dest, DynamicArray<S> src) {
3 for (int i = 0; i < src.size(); i++) {
4 dest.add(src.get(i));
5 }
6}
7// 拷贝容器数据,方式2:
8public static <D> void copy(DynamicArray<D> dest, DynamicArray<? extends D> src) {
9 for (int i = 0; i < src.size(); i++) {
10 dest.add(src.get(i));
11 }
12}
13
14// 添加容器数据,方式1:
15public <T extends E> void addAll(DynamicArray<T> c){}
16// 添加容器数据,方式2:
17public void addAll(DynamicArray<? extends E> c){}
18
19// indexOf,方式1:
20public static <T> int indexOf(DynamicArray<T> arr, Object elm)
21// indexOf,方式2:
22public static int indexOf(DynamicArray<?> arr, Object elm)
23
24// 返回值依赖于泛型:返回类型无法用通配符替代
25public static <T extends Comparable<T>> T max(DynamicArray<T> arr) {
26 T max = arr.get(0);
27 for (int i = 1; i < arr.size(); i++) {
28 if (arr.get(i).compareTo(max) > 0) {
29 max = arr.get(i);
30 }
31 }
32 return max;
33}
34
前面提到,Java中的泛型是通过类型擦除来实现的,所有的泛型在编译时都会被替换为Object或上界类,运行时Java虚拟机不知道泛型这回事,这带来了很多局限性,其中有的部分是比较容易理解的,有的则是非常违反直觉的 。
泛型的具体化类型不能是基本类型,应该使用它的包装类。
31// 不能使用int去具体化泛型,因为int没有根类Object
2Pair<int> intPair = new Pair<int>(1,100);
3
不能通过泛型直接创建对象,需要传入泛型对应的类型信息,通过反射创建。
191// 1. 由于类型擦除,使用new T()创建的对象也只能当Object使用,并且容易引起使用者误解,所以Java干脆禁止
2T elm = new T(); // err
3
4// 同样,泛型数组也是不允许创建的(容器的元素类型一般为Object而非泛型E与此相关)
5T[] arr = new T[10]; // err 不能创建泛型数组
6
7
8// 2. 如果希望创建具体化类型的对象,则可传入具体化类型的Class对象,根据反射创建
9public static <T> T create(Class<T> type) {
10 try {
11 return type.newInstance(); // 反射
12 } catch (Exception e) {
13 return null;
14 }
15}
16
17// 创建Date对象
18Date date = Pair.create(Date.class);
19
提示:
实际上,可以参考第二节中,通过泛型引用的对象获取Class信息,进而创建对象。
泛型在编译时将会被擦除为Object(或上界类),不同的具体化类型只是在编译时自动插入了不同的强制类型转换,本质上还是同一个类。
由于是同一个类,因此它们的类型信息完全一致,并且类上所具有的静态资源也是共享的。
251// 1. 不同具体化类型的两个Pair对象
2Pair<String, String> pair01 = new Pair<String, String>("老马", "说编程");
3Pair<String, Integer> pair02 = new Pair<String, Integer>("老马", 100);
4
5// 类型信息是完全一致的
6System.out.println(Pair.class); // class com.huangyuanxin.notes.javabase.generics.Pair
7System.out.println(Pair.class == pair01.getClass()); // true
8System.out.println(pair01.getClass() == pair02.getClass()); // true
9System.out.println(Pair<?, ?>.class); // err
10System.out.println(Pair<String, Integer>.class); // err
11
12// 类型也可以使用instanceof关键字进行判断
13if (pair01 instanceof Pair){} // OK
14if (pair01 instanceof Pair<?, ?>){} // OK,特殊情形
15if (pair01 instanceof Pair<Integer>){} // err
16
17
18// 2. 定义如下静态变量
19public static String STATIC_NAME = "PAIR";
20
21// 打印静态变量,发现也是共享的
22Pair.STATIC_NAME = "STATIC_NAME_MODIFY";
23System.out.println(Pair.STATIC_NAME); // STATIC_NAME_MODIFY
24System.out.println(pair01.STATIC_NAME); // STATIC_NAME_MODIFY
25
注意:
内部的first/second编译时都被擦除为Object类型,但是运行时分别指向不同的具体化类型对象。
泛型类的泛型不能用于静态变量或静态方法,应为静态方法单独声明泛型,而静态变量不允许为泛型。
121// 声明泛型T,仅适用于成员变量和成员方法
2public class Singleton<T> {
3 private static T instance; // err 静态变量不能为泛型
4
5 public synchronized static T getInstance() { // err 不能用于静态方法
6 if (instance == null) {
7 //创建实例
8 }
9 return instance;
10 }
11}
12
241// 1. 案例一:如下,Base实现了Comparable<Base>接口
2class Base implements Comparable<Base>{}
3
4// Child继承Base,间接实现了Comparable<Base>接口,OK!
5class Child extends Base{}
6
7// 如果Child想自定义compareTo()方法,发现不能实现Comparable<Child>接口
8class Child extends Base implements Comparable<Child>{} // err,重复实现了
9
10// 此时只能重写Base中的方法来自定义Child中的该方法
11
12public int compareTo(Base o) {
13 if (!(o instanceof Child)) {
14 throw new IllegalArgumentException();
15 }
16 Child c = (Child) o;
17 return 0;
18}
19
20
21// 2. 案例二:看起来很像方法重载,但是由于类型擦除后都是Object,因此也是不允许的
22public static void test(DynamicArray<Integer> intArr){}
23public static void test(DynamicArray<String> strArr){}
24
如下创建泛型数组的代码是禁止的:
41Pair<Object, Integer>[] options = new Pair<Object, Integer>[]{
2 new Pair("1元", 7), new Pair("2元", 2), new Pair("10元", 1)
3};
4
因为数组是Java直接支持的概念,它知道数组元素的实际类型,在类型不对时可快速触发运行时异常,因此编译时允许赋值给父类数组。
51// 创建普通类型数组
2Integer[] ints = new Integer[10];
3Number[] numbers = ints; // 子类数组赋值给父类数组 ok
4numbers[0] = 1.2; // 类型不对,触发运行时异常:java.lang.ArrayStoreException: java.lang.Double
5
但是如果允许创建泛型数组,如下:
51// 创建泛型数组
2Pair<Object, Integer>[] options = new Pair<Object, Integer>[3];
3Object[] objs = options; // 子类数组赋值给父类数组
4objs[0] = new Pair<Double, String>(12.34, "hello"); // 隐患代码
5
由于Pair<Double, String>和Pair<Object, Integer>的类型都是Pair,因此第二行赋值时即不会编译报错,也不会立即触发运行时异常,埋下了隐患,因此Java禁止创建泛型数组。
如果我们非要创建泛型类型的数组,可以使用原始类型来创建,这样可以跳过编译检查,但是问题还是存在的。
71Pair[] options = new Pair[]{
2 new Pair<String, Integer>("1元", 7),
3 new Pair<String, Integer>("2元", 2),
4 new Pair<String, Integer>("10元", 1),
5 new Pair<String, String>("10元", "1") // 注意:数组元素类型不一致。
6};
7
最好的解决办法是,使用泛型容器来代替泛型数组:
61DynamicArray<Pair<String, Integer>> options = new DynamicArray<>();
2options.add(new Pair<String, Integer>("1元", 7));
3options.add(new Pair<String, Integer>("2元", 2));
4options.add(new Pair<String, Integer>("10元", 1));
5options.add(new Pair<String, String>("10元", "1")); // err 不兼容的类型在编译时报错
6
有时候我们希望将泛型容器直接转化为一个泛型数组,如下:
51DynamicArray<Integer> ints = new DynamicArray<Integer>();
2ints.add(100);
3ints.add(34);
4Integer[] arr = ints.toArray(); // 泛型容器 -> 泛型数组
5
实现toArray()方法时,一般是先创建一个泛型数组,然后拷贝数据再返回该数组。
由于前面已经提到,直接创建泛型数组是行不通的:E[] arr = new E[size]; // err
,因此,可能会想到如下两种方式:
111//
2public E[] toArray() {
3 Object[] copy = new Object[size]; // 创建Object数组
4 System.arraycopy(elementData, 0, copy, 0, size);
5 return (E[]) copy; // 强转报错
6}
7
8public E[] toArray() {
9 return (E[])Arrays.copyOf(elementData, size); // copyOf返回Object数组,强转报错
10}
11
虽然这两者方式没有编译错误,但是在运行时都会抛出如下异常:
11java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
要想实现上述需求,必须知道数组元素的类型信息,才能创建泛型数组,可以修改实现如下:
101// 接收数组元素的类型信息
2public E[] toArray(Class<E> type) {
3 Object copy = Array.newInstance(type, size); // 创建type类型的数组对象
4 System.arraycopy(elementData, 0, copy, 0, size);
5 return (E[]) copy;
6}
7
8// 使用时传入类型信息
9Integer[] arr = ints.toArray(Integer.class);
10
提示:
实际上,可以通过一些运行时类型信息来获取元素的类型信息,从而传入Array.newInstance创建数组,可以对比第二节相关案例。
21elementData.getClass().getComponentType()
2elementData[0].getClass()
容器类主要分为集合类容器(Collection)和映射类容器(Map)。集合类容器包括列表(List)、队列(Queue)、集合(Set)三大类,其中队列又衍生出双端队列(Deque),它们都是容器类的超级接口,并且一般都定义了对应的抽象类。
在日常开发中,我们一般使用上述接口或抽象类的具体子类,常用的容器如下:
容器 | 容器类 | 说明 |
---|---|---|
数组列表 | ArrayList | 基于数组实现的列表 |
链式列表 | LinkedList | 基于链表实现的列表,也可作为链式双端队列 |
数组双端队列 | ArrayDeque | 基于循环数组实现的双端队列 |
链式双端队列 | LinkedList | 基于链表实现的双端队列,也可作为链式列表 |
优先级队列 | PriorityQueue | 基于堆实现的单端队列,元素可以按优先级出列 |
哈希集合 | HashSet | 基于哈希表+链表(或红黑树)实现的无序集合 |
带链的哈希集合 | LinkedHashSet | 继承自HashSet,在其基础上通过额外的链来维护插入有序 |
树状集合 | TreeSet | 基于红黑树实现的规则有序集合 |
枚举集合 | EnumSet | 基于数组实现的高效集合,只适用于枚举类型元素 |
哈希映射 | HashMap | 基于哈希表+链表(或红黑树)实现的无序映射 |
带链的哈希映射 | LinkedHashMap | 继承自HashMap,在其基础上通过额外的链来维护存取有序 |
树状映射 | TreeMap | 基于红黑树实现的规则有序映射 |
枚举映射 | EnumMap | 基于位向量实现的高效映射,只适用于枚举元素 |
注意:
容器一般会继承对应的抽象类及直接实现对应的超级接口,如ArrayList继承了AbstractList,并且还直接实现了List接口。
但是也有些例外,如ArrayDeque没有对应的AbstractDeque,EnumSet和EnumMap没有直接实现对应的Set和Map接口等。
Iterable<T>
接口表示“可迭代的”,它提供了获取迭代器(Iterator<E>)的方法,通过迭代器可以进行遍历操作,并且支持ForEach语法。
ListIterator<E>
扩展了Iterator接口,增加了一些向前遍历、添加元素、修改元素、返回索引位置等方法。
171// Iterable<T>
2Iterator<T> iterator() // 返回Iterator对象
3default void forEach(Consumer<? super T> action)
4
5// Iterator<E>
6boolean hasNext() // 判断是否还有元素未访问
7E next() // 返回下一个元素
8default void remove() // 删除最后返回的元素
9
10// ListIterator<E>
11boolean hasPrevious()
12E previous()
13int nextIndex()
14int previousIndex()
15void set(E e)
16void add(E e)
17
提示
只要对象实现了Iterable接口,就可以使用foreach语法,编译器会转换为调用Iterable和Iterator接口的方法。
Collection<E>
表示单列集合,只定义了基本的增删改查和遍历等方法,没有定义元素间的顺序或位置,也没有规定是否有重复元素。
171// Collection<E>
2int size()
3boolean isEmpty()
4boolean contains(Object o)
5Object[] toArray()
6<T> T[] toArray(T[] a)
7boolean add(E e)
8boolean remove(Object o)
9boolean containsAll(Collection<?> c)
10boolean addAll(Collection<? extends E> c)
11boolean removeAll(Collection<?> c)
12default boolean removeIf(Predicate<? super E> filter)
13boolean retainAll(Collection<?> c) // 交集
14void clear()
15default Stream<E> stream()
16default Stream<E> parallelStream()
17
注意:
Collection的add方法默认为抛出UnsupportedOperationException异常。
List<E>
是 Collection<E> 的子接口,表示有顺序和位置的集合,增加了根据索引位置进行操作的方法。
141// List<E>
2boolean addAll(int index, Collection<? extends E> c)
3default void replaceAll(UnaryOperator<E> operator)
4default void sort(Comparator<? super E> c)
5E get(int index)
6E set(int index, E element)
7void add(int index, E element)
8E remove(int index)
9int indexOf(Object o)
10int lastIndexOf(Object o)
11ListIterator<E> listIterator()
12ListIterator<E> listIterator(int index)
13List<E> subList(int fromIndex, int toIndex)
14
Queue<E>
是Collection<E>的子接口,表示先进先出的队列,在尾部添加,从头部查看或删除。
Deque<E>
是Queue<E>的子接口,表示更为通用的双端队列,有明确的在头或尾进行查看、添加和删除的方法。
261// Queue<T>
2boolean offer(E e)
3E remove()
4E poll()
5E element()
6E peek()
7
8// Deque<E>
9void addFirst(E e)
10void addLast(E e)
11boolean offerFirst(E e)
12boolean offerLast(E e)
13E removeFirst()
14E removeLast()
15E pollFirst()
16E pollLast()
17E getFirst()
18E getLast()
19E peekFirst()
20E peekLast()
21boolean removeFirstOccurrence(Object o)
22boolean removeLastOccurrence(Object o)
23void push(E e)
24E pop()
25Iterator<E> descendingIterator()
26
Set<E>
是Collection<E>的子接口,它没有增加新的方法,但保证不含重复元素。SortedSet<E>和NavigableSet<E>在Set的基础上进行了扩充,方便实现TreeSet子类。
231// Set<E> 不含重复元素
2
3// SortedSet<E> 不含重复元素且有序
4Comparator<? super E> comparator()
5SortedSet<E> subSet(E fromElement, E toElement)
6SortedSet<E> headSet(E toElement)
7SortedSet<E> tailSet(E fromElement)
8E first()
9E last()
10
11// NavigableSet<E> // 不含重复元素且有序且可导航的
12E lower(E e)
13E floor(E e)
14E ceiling(E e)
15E higher(E e)
16E pollFirst()
17E pollLast()
18NavigableSet<E> descendingSet();
19Iterator<E> descendingIterator()
20NavigableSet<E> subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive)
21NavigableSet<E> headSet(E toElement, boolean inclusive)
22NavigableSet<E> tailSet(E fromElement, boolean inclusive)
23
Map<K,V>
表示键值对集合(映射),它的元素为Entry<K,V>
类型,经常根据键进行操作。SortedMapMap<K,V>和NavigableMapMap<K,V>在Map的基础上进行了扩充,方便实现TreeMap子类。
621// Entry<K,V>
2K getKey()
3V getValue()
4V setValue(V value)
5public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey()
6public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue()
7public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp)
8public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp)
9
10// Map<K,V>
11int size()
12boolean isEmpty()
13boolean containsKey(Object key)
14boolean containsValue(Object value)
15V get(Object key)
16V put(K key, V value)
17V remove(Object key)
18void putAll(Map<? extends K, ? extends V> m)
19void clear()
20Set<K> keySet() // key视图
21Collection<V> values() // value视图
22Set<Map.Entry<K, V>> entrySet() // entry视图
23default V getOrDefault(Object key, V defaultValue)
24default void forEach(BiConsumer<? super K, ? super V> action)
25default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
26default V putIfAbsent(K key, V value)
27default boolean remove(Object key, Object value)
28default boolean replace(K key, V oldValue, V newValue)
29default V replace(K key, V value)
30default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
31default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
32default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
33default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)
34
35// SortedMap<K,V>
36Comparator<? super K> comparator()
37SortedMap<K,V> subMap(K fromKey, K toKey)
38SortedMap<K,V> headMap(K toKey)
39SortedMap<K,V> tailMap(K fromKey)
40K firstKey()
41K lastKey()
42
43// NavigableMap<K,V>
44Map.Entry<K,V> lowerEntry(K key)
45K lowerKey(K key)
46Map.Entry<K,V> floorEntry(K key)
47K floorKey(K key)
48Map.Entry<K,V> ceilingEntry(K key)
49K ceilingKey(K key)
50Map.Entry<K,V> higherEntry(K key)
51K higherKey(K key)
52Map.Entry<K,V> firstEntry()
53Map.Entry<K,V> lastEntry()
54Map.Entry<K,V> pollFirstEntry()
55Map.Entry<K,V> pollLastEntry()
56NavigableMap<K,V> descendingMap()
57NavigableSet<K> navigableKeySet()
58NavigableSet<K> descendingKeySet()
59NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive)
60NavigableMap<K,V> headMap(K toKey, boolean inclusive)
61NavigableMap<K,V> tailMap(K fromKey, boolean inclusive)
62
不同类型的容器有不同的适用场景,如数组类容器适合随机访问,链式容器适合头尾存取,堆类型容器适合TopN问题,树状容器适合元素按规则排序的场景等,应该根据使用场景选用合适的容器。
除了Hashtable
、Vector
和Stack
外,我们本章介绍的各种容器类都是线程不安全的。如需多线程操作同一个容器,可以使用Collections
工具类提供的synchronizedXXX方法对容器对象进行同步,或者使用专门的线程安全容器类。
容器类提供的迭代器都有一个特点,会在迭代时检测容器的结构性变化(通过modCount来实现),如通过容器引用去添加或删除元素等,将会抛出ConcurrentModificationException
。如确实需要增删元素,可以通过迭代器的add和remove方法操作。
ArrayList<E>
是List<E>的子类,基于数组实现,它的随机访问效率很高,但从中间插入和删除元素需要移动元素,效率比较低。
501// 构造方法
2ArrayList()
3ArrayList(Collection<? extends E> c) // 构造后调用addAll添加所有元素
4ArrayList(int initialCapacity) // 指定初始容量
5
6// 基本增删改查
7int size() // 列表长度,即元素个数
8boolean isEmpty() // 列表是否为空
9boolean contains(Object o) // 是否包含指定元素,依据是equals方法的返回值
10boolean add(E e) // 添加元素到末尾
11void add(int index, E element) // 在指定位置处添加元素(index为0表示插入最前面,index为size()表示插到最后面
12boolean addAll(Collection<? extends E> c) // 添加多个元素
13boolean addAll(int index, Collection<? extends E> c) // 指定位置添加多个元素
14E get(int index) // 访问指定位置的元素
15E set(int index, E element) // 修改指定位置的元素内容
16E remove(int index) // 删除指定位置的元素,返回值为被删对象
17boolean remove(Object o) // 按值删除,删除从0索引开始比较遇到的第一个相等元素(参数可以为null)
18boolean removeAll(Collection<?> c) // 删除多个元素
19boolean removeIf(Predicate<? super E> filter) // 按条件删除
20void replaceAll(UnaryOperator<E> operator) // 操作所有元素,如将元素全部转为大写:strList01.replaceAll(e -> e.toUpperCase())
21boolean retainAll(Collection<?> c) // 只保留参数容器中的元素,即取两集合交集
22void clear() // 清空列表,即删除所有元素
23int indexOf(Object o) // 查找元素,如果找到,返回索引位置,否则返回-1
24int lastIndexOf(Object o) // 从后往前找
25
26// 遍历
27Iterator<E> iterator() // 普通后向迭代器
28ListIterator<E> listIterator() // 列表迭代器(支持双向)
29ListIterator<E> listIterator(int index) // 指定迭代器开始位置,默认为0,表示从头开始遍历,可以指定为size(),配合hasPrevious()从后遍历
30Spliterator<E> spliterator()
31void forEach(Consumer<? super E> action)
32
33// 排序
34void sort(Comparator<? super E> c)
35
36// 转换
37String toString() // 返回字符串形式,如[1, 2, 3]、[a, b, c]
38Object[] toArray() // 返回Object数组
39<T> T[] toArray(T[] arr) // 返回对应类型的数组,如果参数数组容量足以容纳所有元素,就使用该数组,否则就新建一个数组(如果数组类型不对,将会抛出ArrayStoreException)
40List<E> subList(int fromIndex, int toIndex) // 返回一个子列表(SubList),并引用原列表元素(会直接影响原列表)
41Arrays.asList(new Integer[]{1, 2, 3} // 数组转换为只读List,支持可变参数形式:Arrays.asList(1, 2, 3)
42List<Integer> list = new ArrayList<Integer>(Arrays.asList(arr)) // 数组转换为完整List
43
44// 动态分配
45void ensureCapacity(int minCapacity) // 确保数组的大小至少为minCapacity,如果不够,会进行扩展
46void trimToSize() // 重新分配一个数组,大小刚好为实际内容的长度。调用这个方法可以节省数组占用的空间。
47
48// 其它方法
49Object clone()
50
注意:
基于索引操作的方法,在操作节点前都会检查索引是否越界,如果越界将会抛出IndexOutOfBoundsException。
基于索引操作的插入类方法,当索引为0时,插入到头部,索引为size()时,插入到尾部;
基于索引操作的删除和查看方法,索引范围必须为0~size()-1;
ArrayList内部使用数组elementData来存储元素,默认长度为10,长度会随着元素个数的变化动态分配(1.5倍),一般会有一些预留的空间,由另外一个整数size来记录实际的元素个数。
21transient Object[] elementData; // 存储元素的数组
2private int size; // 元素实际个数
由于迭代器内部会维护一些索引位置相关的数据,因此要求在迭代过程中,容器不能发生结构性变化,否则这些索引位置就失效了,就会抛出ConcurrentModificationException
。所谓结构性变化,就是添加和删除元素等,只是修改元素内容不算结构性变化。
71public static void remove(ArrayList<Integer> list) {
2 for (Integer a : list) { // 迭代
3 if (a <= 100) {
4 list.remove(a); // 结构性变化
5 }
6 }
7}
如何避免异常呢?可以使用迭代器的remove方法,或直接通过list.removeIf来实现相同功能。
151// 使用迭代器的remove方法
2public static void remove(ArrayList<Integer> list) {
3 Iterator<Integer> it = list.iterator();
4 while (it.hasNext()) {
5 if (it.next() <= 100) {
6 it.remove(); // 使用迭代器it的remove()方法
7 }
8 }
9}
10
11// List的按条件删除方法(remove的特殊场景,add并没有对应方法)
12public static void remove(ArrayList<Integer> list) {
13 list.removeIf(a -> a <= 100);
14}
15
为什么上面可以使用迭代器的remove方法来删除呢?这涉及到迭代器的实现原理,它内部维护了三个成员变量:
31int cursor; // 下一个要返回的元素位置
2int lastRet = -1; // 最后一个返回的索引位置,如果没有,为 -1
3int expectedModCount = modCount; // 期望的修改次数,初始化为外部类当前的修改次数modCount
当外部类调用add、remove等影响结构性的方法时,modCount都会自增,而每次迭代器操作的时候都会检查expectedModCount是否与外部类的modCount相同,这样就能检测出结构性变化。
如果使用迭代器的remove方法,它在调用ArrayList的remove方法时,可以同步更新内部的cursor、lastRet和expectedModCount的值,因此可以正确删除。不过,需要注意的是,调用迭代器的remove方法前必须先调用next,否则会抛出IllegalStateException。
91// 删除所有元素
2public static void removeAll(ArrayList<Integer> list) {
3 Iterator<Integer> it = list.iterator();
4 while (it.hasNext()) {
5 it.next(); // 在调用remove()前必须调用next()方法
6 it.remove();
7 }
8}
9
注意:
迭代器是一种关注点分离的思想,将数据的实际组织方式与数据的迭代遍历相分离,是一种常见的设计模式。
迭代器语法更加简洁,并且对于部分容器,性能更加高效,推荐优先使用。
LinkedList<E>
是List<E>的间接子类,基于链表实现,随机访问效率比较低,但增删元素只需要调整邻近节点的链接。此外,它还继承了Deque\<E\>
接口,可以用作双端队列、先进先出队列、栈等。
531// 构造方法
2LinkedList() // 无初始容量
3LinkedList(Collection<? extends E> c)
4
5// 用作链式列表
6int size() // 列表长度,即元素个数
7boolean isEmpty() // 列表是否为空
8boolean contains(Object o) // 是否包含指定元素,依据是equals方法的返回值
9boolean add(E e) // 添加元素到末尾
10void add(int index, E element) // 在指定位置处添加元素(index为0表示插入最前面,index为size()表示插到最后面
11boolean addAll(Collection<? extends E> c) // 添加多个元素
12boolean addAll(int index, Collection<? extends E> c) // 在指定位置添加多个元素
13E get(int index) // 访问指定位置的元素
14E set(int index, E element) // 修改指定位置的元素内容
15E remove(int index) // 删除指定位置的元素,返回值为被删对象
16boolean remove(Object o) // 从头部开始比较,移除第一个值为参数o的元素,值可以为null
17void clear() // 清空列表,即删除所有元素
18int indexOf(Object o) // 查找元素,如果找到,返回索引位置,否则返回-1
19int lastIndexOf(Object o) // 从后往前找
20
21// 用作先进先出队列(尾进头出)
22boolean add(E e) boolean offer(E e)
23E remove() E poll()
24E element() E peek()
25
26// 用作双端队列
27void addFirst(E e) boolean offerFirst(E e)
28void addLast(E e) boolean offerLast(E e)
29E removeFirst() E pollFirst()
30E removeLast() E pollLast()
31E getFirst() E peekFirst()
32E getLast() E peekLast()
33boolean removeFirstOccurrence(Object o) // 头部开始比较,移除第一个值为参数o的元素,值可以为null
34boolean removeLastOccurrence(Object o) // 从尾部开始比较,移除第一个值为参数o的元素,值可以为null
35
36// 用作栈(头部为栈顶)
37void push(E e)
38E pop()
39E peek()
40
41// 遍历相关方法
42Iterator<E> descendingIterator() // 反向迭代器
43ListIterator<E> listIterator(int index) // 列表迭代器(支持双向迭代和指定迭代开始位置)
44Spliterator<E> spliterator()
45
46// 转换方法
47String toString() // 返回字符串形式,如[1, 2, 3]、[a, b, c]
48Object[] toArray() // 返回Object数组
49<T> T[] toArray(T[] a) // 返回对应类型的数组,如果参数数组长度足以容纳所有元素,就使用该数组,否则就新建一个数组(如果数组类型不对,将会抛出ArrayStoreException)
50
51// 其它方法
52Object clone()
53
注意:
栈/队列是双端队列的特殊情况,它们的方法都可以使用双端队列的方法替代,不过使用不同的名称和方法,概念上更为清晰。
offer/poll/peek开头的方法在已满或为空时返回false或null(虽然LinkedList没有”已满“的概念,但其它队列/栈可能会有)。
add/remove/get和push/pop/element开头的方法在已满或为空时会抛出IllegalStateException或NoSuchElementException。
LinkedList是一个双端链表,每个元素(节点)在内存中单独存放,元素之间通过前驱指针和后继指针进行链接。
121// LinkedList的元素节点
2private static class Node<E> {
3 E item; // 元素值
4 Node<E> next; // 前驱指针
5 Node<E> prev; // 后继指针
6
7 Node(Node<E> prev, E element, Node<E> next) {
8 this.item = element;
9 this.next = next;
10 this.prev = prev;
11 }
12}
而LinkedList内部只需保存一个头指针和一个尾指针即可,分别指向第一个节点和最后一个节点,通过指针寻址操作,关联所有元素,构成逻辑上的双端链表。
31transient Node<E> first; // 头指针
2transient Node<E> last; // 尾指针
3transient int size = 0; // 元素个数
ArrayDeque<E>
是Deque<E>的子类,基于循环数组实现,它可以用作双端队列、先进先出队列、栈等。和链式双端队列相比,从两端操作的效率会更高一些,但是不支持索引操作,并且在中间插入和删除很慢。
构造方法如下,其它常用方法和LinkedList中介绍的类似,不再赘述。
31ArrayDeque() // 默认容量为16,每次扩容为之前的2倍
2ArrayDeque(int numElements) // 指定队列初始容量,一般为2^n-1(如numElements为31,则实际初始容量为32,如numElements为32,实际初始容量为64)。
3ArrayDeque(Collection<? extends E> c)
下面重点看下ArrayDeque的循环数组是如何实现的,ArrayDeque内部主要有如下实例变量:
31private transient E[] elements; // 存储元素的数组
2private transient int head; // 头指针
3private transient int tail; // 尾指针
通过引入头指针和尾指针使物理上的简单数组(从头到尾)变为了一个逻辑上循环的数组,避免了在头尾操作时的移动。头尾有四种分布:
head=tail:队列为空(即size=0,由于数组长度为最大容量+1,因此不会是已满情形)。
head<tail:队列无循环,元素索引从head~tail-1。
0=tail<head:队列也无循环,但是处于数组最右端,元素索引从head~elements.length-1。
0<tail<head:队列会形成循环,元素索引分为两段,第一段从head~elements.length-1,第二段从0~tail-1。
队列的长度始终可以通过(tail - head) & (elements.length - 1)算出。而在添加新元素时,如在尾部添加,则tail = (tail + 1) & (elements.length - 1),如在头部添加,则head = ( head-1 ) & ( elements.length-1 ),如果出现head==tail,则表示容器已满,需要将容量扩为之前的2倍。
注意:
ArrayDeque中,有效元素不允许为null,contains等方法在内部遍历时也将null视为结尾。
通过位与运算,可以有效提高计算下标的效率,并且可以确保索引不会越界,这在循环数组中的应用非常常见。
LinkedList<E>
还继承了Deque<E>接口,可以用作双端队列、先进先出队列、栈等,在链式列表章节已有介绍。
PriorityQueue<E>
是Queue<E>的子类,表示优先级队列,基于堆实现的。常见的应用场景有“求前K个最大的元素”、“求实时中值”等。
351// 构造方法(元素需实现Comparable接口或构造时传入Comparator)
2PriorityQueue() // 默认容量为11,小顶堆
3PriorityQueue(int initialCapacity)
4PriorityQueue(Comparator<? super E> comparator)
5PriorityQueue(int initialCapacity, Comparator<? super E> comparator)
6PriorityQueue(Collection<? extends E> c)
7PriorityQueue(PriorityQueue<? extends E> c)
8PriorityQueue(SortedSet<? extends E> c)
9
10// 基本增删改查
11int size()
12boolean isEmpty()
13boolean contains(Object o)
14boolean containsAll(Collection<?> c)
15boolean add(E e) boolean offer(E e) // 添加到合适位置
16boolean addAll(Collection<? extends E> c) // 添加多个元素
17boolean remove(Object o) E poll() // 取出头部元素(头部元素一定是最小/最大的那个,连续取出时的顺序是有序的)
18boolean removeAll(Collection<?> c) // 删除多个元素
19boolean removeIf(Predicate<? super E> filter) // 按条件删除
20boolean retainAll(Collection<?> c) // 只保留参数容器中的元素,即取两集合交集
21E element() E peek()
22void clear()
23
24// 迭代
25Iterator<E> iterator()
26final Spliterator<E> spliterator() // 特殊迭代器
27
28// 转换
29String toString() // 返回原始数组的字符串形式,是无序的,如[a, s, c, t]
30Object[] toArray() // 注意是原始数组,是无序的
31<T> T[] toArray(T[] a) // 注意是原始数组,是无序的
32
33// 其它方法
34Comparator<? super E> comparator() // 返回内部比较器
35
优先级队列基于堆实现,而堆是一颗完全二叉树,在从左到右并分层进行编号后,可以直接计算出任意节点的父节点和左右子节点的编号,如编号为i
的节点,其父节点编号为i/2
,左右子节点的编号分别为2\*i
和2\*i+1
,可以将这个编号作为数组的索引,将每个节点按编号存储在一个连续的数组中,不仅节省空间,而且访问效率非常高。
但在插入和删除(即将尾部元素覆盖头部元素)元素时,需要进行向上调整(siftup)或向下调整(siftdown)来维持堆的性质,效率都为Olog2N。
41private transient Object[] queue; // 存储元素的数组
2private int size = 0;
3private final Comparator<? super E> comparator; // 比较器,在元素实现了Comparable接口时可以为null
4private transient int modCount = 0; // 修改次数
注意:
堆分为小顶堆和大顶堆,大顶堆指每个元素不大于其父元素,根节点就是最大节点,元素之间可以重复,小顶堆与之类似。
HashMap<K,V>
是Map<K,V>的子接口,基于哈希表实现(哈希表+链表/红黑树),要求元素的键(key)重写hashCode和equals方法,操作效率很高,但元素间没有顺序。
421// 构造方法
2HashMap() // 默认初始容量为16(必须为2^n次方),负载因子为0.75f
3HashMap(int initialCapacity)
4HashMap(int initialCapacity, float loadFactor)
5HashMap(Map<? extends K, ? extends V> m)
6
7// 基本增删改查
8int size()
9boolean isEmpty()
10boolean containsKey(Object key)
11boolean containsValue(Object value)
12V get(Object key)
13V getOrDefault(Object key, V defaultValue) // get,key不存在则取默认值
14V put(K key, V value) // 会覆盖旧元素
15V putIfAbsent(K key, V value) // 不覆盖旧元素
16void putAll(Map<? extends K, ? extends V> m)
17V remove(Object key)
18boolean remove(Object key, Object value)
19void clear()
20V replace(K key, V value)
21boolean replace(K key, V oldValue, V newValue)
22void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
23V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) // put计算后的新值并返回;如果新值为null,则进行删除该key并返回null
24V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) // 仅在key不存在时compute,key存在直接返回旧value
25V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) // 仅在key存在时compute,key不存在直接返回null
26V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) // 归并旧值和新值
27
28// 遍历
29// map.keySet().Xxx
30void forEach(BiConsumer<? super K, ? super V> action)
31
32// 特殊视图
33Set<K> keySet()
34Collection<V> values()
35Set<Map.Entry<K,V>> entrySet()
36
37// 转换
38String toString() // 字符串形式如:{a=97, b=98, c=99}
39
40// 其它方法
41Object clone()
42
HashMap内部有一个Node类型的数组table,称为哈希表(哈希桶),每个元素(table[i])指向一个单向链表(或红黑树)。
131transient Node<K,V>[] table; // 哈希表(哈希桶),默认为空数组{}
2transient int size; // 元素的实际个数
3int threshold; // 扩容阈值(一般等于容量*负载因子),当szie>threshold时,扩容为之前的两倍
4final float loadFactor; // 负载因子
5static final int TREEIFY_THRESHOLD = 8; // 链表树化的阈值
6static final int UNTREEIFY_THRESHOLD = 6; // 树退化为链表的阈值
7
8static class Node<K,V> implements Map.Entry<K,V> {
9 final int hash;
10 final K key;
11 V value;
12 Node<K,V> next;
13}
当put新元素时,先计算key对应的hash值,再通过取余( h%(length-1),可优化为h&(length-1) )得到数组中的索引位置buketIndex,然后将value存放在该位置或该位置指向的链表(或红黑树)中。
LinkedHashMap<K,V>
继承自HashMap<K,V>,在其哈希表+链表(或红黑树)的基础上额外添加了一条用于维护元素顺序的双向链表,这个链表可以按插入顺序排序,也可以按访问顺序排序。
构造方法如下,其它方法和HashMap类似,但是get/put等方法内部会额外维护一个插入或访问顺序,同时遍历时按照该顺序进行。
71// 构造方法
2LinkedHashMap()
3LinkedHashMap(int initialCapacity) // initialCapacity-初始容量
4LinkedHashMap(int initialCapacity, float loadFactor) // loadFactor-负载因子,当szie>initialCapacity*loadFactor时进行哈希表的扩容
5LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) // accessOrder-是否按访问有序
6LinkedHashMap(Map<? extends K, ? extends V> m)
7
提示:
如果键本来就是有序的,使用LinkedHashMap比TreeMap效率更高。
LinkedHashMap是HashMap的子类,内部增加了如下实例变量:
41final boolean accessOrder; // 是否按访问有序
2transient LinkedHashMap.Entry<K,V> head; // 双向链表的头部
3transient LinkedHashMap.Entry<K,V> tail; // 双向链表的尾部
4
其中Entry继承了HashMap.Node,增加了两个变量before和after, 分别指向前驱节点和后继节点。
71static class Entry<K,V> extends HashMap.Node<K,V> {
2 Entry<K,V> before, after;
3 Entry(int hash, K key, V value, Node<K,V> next) {
4 super(hash, key, value, next);
5 }
6}
7
当处于“插入有序”模式时,哈希表新增元素的同时,也会添加到链表的末尾。当处于“访问有序”模式时,无论是插入、修改或访问,都会将该节点移到链表的末尾。
231// 最近最少使用缓存
2public class LRUCache<K, V> extends LinkedHashMap<K, V> {
3 private int maxEntries;
4
5 public LRUCache(int maxEntries) {
6 super(16, 0.75f, true);
7 this.maxEntries = maxEntries;
8 }
9
10
11 protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
12 return size() > maxEntries;
13 }
14
15 public static void main(String[] args) {
16 LRUCache<String, String> lruCache = new LRUCache<>(2);
17 lruCache.put("str01", "str01");
18 lruCache.put("str02", "str02");
19 lruCache.put("str03", "str03");
20 System.out.println(lruCache); // {str02=str02, str03=str03}
21 }
22}
23
TreeMap<K,V>
是Map<K,V>的间接子接口,基于排序二叉树(红黑树)实现,要求键(key)实现Comparable<E>接口,或者创建TreeSet时提供一个Comparator<E>对象,其操作效率稍低,但键(key)可以按比较有序。
构造方法如下,其它方法和HashMap类似。此外,还有一些继承自SortedMap和NavigableMap的方法,由于使用较少,请查阅API文档。
61// 构造方法
2TreeMap()
3TreeMap(Comparator<? super K> comparator)
4TreeMap(Map<? extends K, ? extends V> m)
5TreeMap(SortedMap<K, ? extends V> m)
6
注意:
TreeMap使用键的比较结果(而非equals)对键进行排重,即使键实际上不同,但只要比较结果相同,就会被认为相同。
TreeMap是基于红黑树实现的,主要成员变量如下:
31private transient Entry<K,V> root = null; // 红黑树的根节点
2private transient int size = 0; // 当前节点的个数
3private final Comparator<? super K> comparator; // Key的比较器(优先使用),用于比较Key的大小和判断Key是否相等
EnumMap<K,V>
是Map<K,V>的子接口,使用比哈希表效率更高的静态数组实现,但是要求元素必须为枚举类型。
构造方法如下,需要通过枚举类的Class信息进行构造,同时key必须为枚举类型。
51// 构造方法
2EnumMap(Class<K> keyType) // 使用枚举类型构造
3EnumMap(EnumMap<K, ? extends V> m)
4EnumMap(Map<K, ? extends V> m)
5
下面是一个简单的使用示例:
141// 枚举类
2public enum Size {
3 SMALL, MEDIUM, LARGE
4}
5
6// 使用示例:
7public static void main(String[] args) {
8 EnumMap<Size, String> enumMap = new EnumMap<>(Size.class);
9 enumMap.put(Size.MEDIUM, "中");
10 enumMap.put(Size.SMALL, "小");
11 enumMap.put(Size.LARGE, "大");
12 System.out.println(enumMap); // {SMALL=小, MEDIUM=中, LARGE=大} 按枚举定义顺序排序
13}
14
注意:
EnumMap是有顺序的,为枚举元素定义的顺序。
当put的值为null时,将会被替换为
EnumMap.NULL
存储,而值为真正的null表示该key不存在。上述两种场景在get时都会返回null,但是在遍历时,不存在的key将会被跳过,如:{SMALL=null, MEDIUM=中}。
虽然使用普通的HashMap可以实现相同的功能,但是使用EnumMap更加简洁安全和高效。
EnumMap内部有两个长度相等的静态数组,一个表示所有可能的键, 一个表示对应的值,值为 null 表示没有该键值对,键都有一个对应的索引,根据索引可直接访问和操作其键和值,效率很高。
41private final Class<K> keyType; // 枚举类型信息
2private transient K[] keyUniverse; // keys,初始化为所有可能的键
3private transient Object[] vals; // 键对应的值,为null表示key不存在,为EnumMap.NULL表示值为null
4private transient int size = 0; // 元素个数
HashSet<E>
是Set<E>的子接口,基于HashMap<E,Object>实现,因此同样要求元素的键(key)重写hashCode和equals方法, 特性也基本类似,如访问效率高,元素间没有顺序等;
271// 构造方法(常用于去重、保存特殊值、集合运算等场景)
2HashSet()
3HashSet(int initialCapacity)
4HashSet(int initialCapacity, float loadFactor)
5HashSet(Collection<? extends E> c)
6
7// 基本增删改查
8int size()
9boolean isEmpty()
10boolean contains(Object o)
11boolean add(E e)
12boolean addAll(Collection<? extends E> c)
13boolean remove(Object o)
14boolean removeIf(Predicate<? super E> filter)
15boolean removeAll(Collection<?> c)
16boolean retainAll(Collection<?> c)
17void clear();
18
19// 遍历
20Iterator<E> iterator()
21Spliterator<E> spliterator()
22
23// 转换
24String toString()
25Object[] toArray()
26<T> T[] toArray(T[] a)
27
HashSet的内部有一个HashMap,操作基本都是委托其完成的。
21private transient HashMap<E,Object> map; // 内部的HashMap
2private static final Object PRESENT = new Object(); // 值都用new Object()填充
LinkedHashSet<E>
继承自HashSet<E>,基于LinkedHashMap<K,V>实现,默认支持插入有序,不支持访问有序。
构造方法如下,其它常用方法和HashSet的使用类似,但add等方法内部会额外维护一个插入顺序,同时遍历时按照该顺序进行。
61// 构造方法
2LinkedHashSet()
3LinkedHashSet(int initialCapacity)
4LinkedHashSet(int initialCapacity, float loadFactor)
5LinkedHashSet(Collection<? extends E> c)
6
LinkedHashSet继承自HashSet,构造时内部的map被初始化为LinkedHashMap,因此支持按插入有序:
81// HashSet内部的map对象
2private transient HashMap<E,Object> map;
3
4// 在构造LinkedHashSet时被初始化为LinkedHashMap
5HashSet(int initialCapacity, float loadFactor, boolean dummy) {
6 map = new LinkedHashMap<>(initialCapacity, loadFactor); // 默认为按插入有序
7}
8
TreeSet<E>
是Set<E>的间接子接口,基于TreeMap<E,Object>实现,, 特性也基本类似,同样也要求元素的键实现Comparable<E>接口,或者创建TreeMap时提供一个Comparator<E>对象。
构造方法如下,其它常用方法和HashSet中介绍的类似,不再赘述。此外,有一些继承自SortedSet和NavigableSet的方法,由于使用较少,请查阅API文档。
51// 构造方法
2TreeSet()
3TreeSet(Comparator<? super E> comparator)
4TreeSet(Collection<? extends E> c)
5TreeSet(SortedSet<E> s)
TreeSet的内部有一个NavigableMap,操作基本都是委托其完成的。
21private transient NavigableMap<E,Object> m; // TreeMap或传入的NavigableMap子类
2private static final Object PRESENT = new Object(); // 值都用new Object()填充
EnumSet<E>
是Set<E>的子接口,基于位向量实现,效率非常高,但是元素要求必须为枚举类型。
构造函数如下,其它方法和HashSet使用类似。
141// 工厂方法
2public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) // 初始集合不包括任何元素
3< E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) // 初始集合包括指定枚举类型的所有枚举值
4< E extends Enum<E>> EnumSet<E> range(E from, E to) // 初始集合包括枚举值中指定范围的元素
5< E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s) // 初始集合包括指定集合的补集
6< E extends Enum<E>> EnumSet<E> of(E e) // 初始集合包括参数中的所有元素
7< E extends Enum<E>> EnumSet<E> of(E e1, E e2)
8< E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3)
9< E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4)
10< E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5)
11< E extends Enum<E>> EnumSet<E> of(E first, E... rest)
12< E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s) // 初始集合包括参数容器中的所有元素
13< E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c)
14
一个简单的使用示例如下:
111enum Day {
2 MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
3}
4
5public static void main(String[] args) {
6 Set<Day> weekend = EnumSet.noneOf(Day.class);
7 weekend.add(Day.SATURDAY);
8 weekend.add(Day.SUNDAY);
9 System.out.println(weekend); // [SATURDAY, SUNDAY]
10}
11
EnumSet与之前介绍的Set实现类不同,它内部没有用对应的Map类EnumMap,而是使用了一种极为高效的位向量方式。
位向量就是用一个位表示一个元素的状态(是否存在),用一组位表示一个集合的状态。如前面的枚举类型Day,它有7个枚举值,可以用一个字节的低7位表示,最高位补0,当对应元素存在时,则置为1,否则为0。
当枚举类型的枚举值个数<=64时,将创建RegularEnumSet
实现类,内部采用64位的long类型存储元素是否存在的信息。否则将创建JumboEnumSet
实现类,采用long类型的数组存储,并用size记录元素的个数。
131// EnumSet共有成员
2final Class<E> elementType; // 枚举类型
3final Enum<?>[] universe; // 枚举类的所有枚举值
4
5// RegularEnumSet
6private long elements = 0L;
7public int size() {
8 return Long.bitCount(elements);
9}
10
11// JumboEnumSet
12private long elements[];
13private int size = 0;
在进行一些增删改查时,基本都是使用位操作来进行的,因此效率非常高,部分操作如下:
161// 添加元素(置1)
2elements |= (1L << ((Enum)e).ordinal()) // 其中(1L << ((Enum)e).ordinal())表示将元素 e 对应的位设为 1
3
4// 删除元素(置0)
5elements &= ~(1L << ((Enum)e).ordinal()) // ~(1L << ((Enum)e).ordinal())表示将元素 e 对应的位设为了 0
6
7// 是否包含
8(elements & (1L << ((Enum)e).ordinal())) != 0
9
10// 取补集
11elements = ~elements // 按位取反,相当于就是取补集
12elements &= -1L >>> -universe.length // 移除高位多余的1
13
14// JumboEnumSet:需定位数组中要操作的long类型
15e.ordinal() >>> 6 // 等效于除以64,得到操作第n个long类型
16
扩展:取补集时为什么要移除高位多余的1?
因为elements是64位的,当前枚举类可能没有用那么多位,取反后高位部分都变为了1,因此需将超出universe.length的部分设为0。
在移动位数为负数的情况下,上述代码相当于:elements &= -1L >>> (64-universe.length)。如universe.length 为 7,则 -1L>>> ( 64-7 ) 就是二进制的 1111111,与 elements 相与,就会将超出universe.length部分的高 57 位都变为0。
Collections
工具类以静态方法的方式提供了很多通用算法和功能。
针对容器接口的通用操作,这是面向接口编程的一种体现,是接口的典型用法。
561// 1. 查找和替换
2// 1.1 二分查找(前提是List中的元素是从小到大排序的,如果是从大到小排序,则需要传递一个逆序Comparator对象)
3// 如果List实现了RandomAccess接口或size<5000,则使用indexedBinarySearch,否则使用iteratorBinarySearch
4static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) // 要求元素实现Comparable接口,下同
5static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c) // 要求提供Comparator,下同
6
7// 1.2 最大值和最小值
8static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
9static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)
10static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll)
11static <T> T min(Collection<? extends T> coll, Comparator<? super T> comp)
12
13// 1.3 元素出现的次数(参数o可以为null)
14static int frequency(Collection<?> c, Object o)
15
16// 1.4 子列表位置(找到返回索引位置,未找到返回-1)
17static int indexOfSubList(List<?> source, List<?> target)
18static int lastIndexOfSubList(List<?> source, List<?> target)
19
20// 1.5 查看两个集合是否不相交(true-不相交,false-有交集)
21static boolean disjoint(Collection<?> c1, Collection<?> c2)
22
23// 1.6 替换所有oldVal为newVal(如果发生了替换,返回值为true,否则为 false)
24static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)
25
26
27// 2. 排序和调整顺序
28// 2.1 列表排序
29static <T extends Comparable<? super T>> void sort(List<T> list)
30static <T> void sort(List<T> list, Comparator<? super T> c)
31
32// 2.2 元素交换
33static void swap(List<?> list, int i, int j)
34
35// 2.3 元素翻转
36static void reverse(List<?> list)
37
38// 2.4 洗牌(遍历列表,依次将当前位置元素与剩余未处理元素中的随机一个进行交换)
39static void shuffle(List<?> list)
40static void shuffle(List<?> list, Random rnd) // rnd-随机数生成类
41
42// 2.5 循环移位
43static void rotate(List<?> list, int distance) // distance-正数表示右移,负数表示左移
44Collections.rotate(list.subList(1, 5), 2) // 支持子列表形式,如[8, 5, 3, 6, 2, 19, 21] -> [8, 6, 2, 5, 3, 19, 21]
45
46
47// 3. 添加和修改
48// 3.1 批量添加
49static <T> boolean addAll(Collection<? super T> c, T... elements)
50
51// 3.2 批量填充固定值
52static <T> void fill(List<? super T> list, T obj)
53
54// 3.3 批量复制(将src列表中的每个元素复制到dest列表的对应位置处,覆盖dest中原来的值,dest的列表长度不能小于src,dest中超过src长度部分的元素不受影响)
55static <T> void copy(List<? super T> dest, List<? extends T> src)
56
目的是为了使更多类型的数据更为方便和安全地参与到容器类协作体系中。
581// 1. 适配器:将其他类型的数据转换为容器接口对象
2// 1.1 空容器(是一个静态不可变对象,不支持修改操作,可以节省创建新对象的内存和时间开销,经常用作方法返回值,等效于Java9中的List.of()方法)
3static <T> Enumeration<T> emptyEnumeration()
4static <T> Iterator<T> emptyIterator()
5static final <T> List<T> emptyList()
6static <T> ListIterator<T> emptyListIterator()
7static final <K,V> Map<K,V> emptyMap()
8static final <K,V> NavigableMap<K,V> emptyNavigableMap()
9static <E> NavigableSet<E> emptyNavigableSet()
10static final <T> Set<T> emptySet()
11static final <K,V> SortedMap<K,V> emptySortedMap()
12static <E> SortedSet<E> emptySortedSet()
13
14// 1.2 单对象容器(将一个单独的对象转换为一个标准的容器接口对象,也是不可变对象,不支持修改操作)
15// 经常用于构建方法参数或返回值,如list.removeAll(Collections.singleton("b"))表示删除list中的所有值为"b"的元素
16// 注:list.remove("b")只会删除第一个"b"元素
17static <T> Set<T> singleton(T o) // SingletonSet,等效于Java9中的Set.of("b")
18static <T> List<T> singletonList(T o) // List.of("b")
19static <K,V> Map<K,V> singletonMap(K key, V value) // Map.of("b")
20
21// 1.3 容器转换
22static <E> Set<E> newSetFromMap(Map<E, Boolean> map) // Map->Set
23static <T> Queue<T> asLifoQueue(Deque<T> deque) // Deque->后进先出队列
24static <T> List<T> nCopies(int n, T o) // 返回包含n个相同对象的List接口
25
26
27// 2. 装饰器:修饰一个给定容器接口对象,增加某种性质
28// 2.1 写安全(使容器对象变为只读的,写入会抛出UnsupportedOperationException 异常)
29static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c)
30static <T> List<T> unmodifiableList(List<? extends T> list)
31static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m)
32static <K,V> NavigableMap<K,V> unmodifiableNavigableMap(NavigableMap<K, ? extends V> m)
33static <T> NavigableSet<T> unmodifiableNavigableSet(NavigableSet<T> s)
34static <T> Set<T> unmodifiableSet(Set<? extends T> s)
35static <K,V> SortedMap<K,V> unmodifiableSortedMap(SortedMap<K, ? extends V> m)
36static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<T> s)
37
38// 2.2 类型安全(指确保容器中不会保存错误类型的对象)
39static <E> Collection<E> checkedCollection(Collection<E> c, Class<E> type)
40static <E> List<E> checkedList(List<E> list, Class<E> type)
41static <K, V> Map<K, V> checkedMap(Map<K, V> m, Class<K> keyType, Class<V> valueType)
42static <K,V> NavigableMap<K,V> checkedNavigableMap(NavigableMap<K, V> m, Class<K> keyType, Class<V> valueType)
43static <E> NavigableSet<E> checkedNavigableSet(NavigableSet<E> s, Class<E> type)
44static <E> Queue<E> checkedQueue(Queue<E> queue, Class<E> type)
45static <E> Set<E> checkedSet(Set<E> s, Class<E> type)
46static <K,V> SortedMap<K,V> checkedSortedMap(SortedMap<K, V> m, Class<K> keyType, Class<V> valueType)
47static <E> SortedSet<E> checkedSortedSet(SortedSet<E> s, Class<E> type)
48
49// 2.3 线程安全
50static <T> Collection<T> synchronizedCollection(Collection<T> c)
51static <T> List<T> synchronizedList(List<T> list)
52static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
53static <K,V> NavigableMap<K,V> synchronizedNavigableMap(NavigableMap<K,V> m)
54static <T> NavigableSet<T> synchronizedNavigableSet(NavigableSet<T> s)
55static <T> Set<T> synchronizedSet(Set<T> s)
56static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
57static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)
58
注意:为什么使用了泛型后还会有类型安全问题呢? 因为Java是通过擦除来实现泛型的,类型参数是可选的,并且JDK5前的老代码都没有泛型。
111// 类型安全检查前
2List list = new ArrayList<Integer>();
3list.add("hello"); // 不报错,但是有隐患
4List<Integer> list2 = list;
5System.out.println(list2.get(0).intValue()); // ClassCastException: java.lang.String cannot be cast to java.lang.Integer
6
7// 类型安全检查后
8List list = new ArrayList<Integer>();
9list = Collections.checkedList(list, Integer.class);
10list.add("hello"); // ClassCastException
11
51// 1. 比较器
2Collections.reverseOrder()
3Collections.reverseOrder(String.CASE_INSENSITIVE_ORDER)
4
5
异常指程序运行过程中出现的错误,以java.lang.Throwable
为根,Java定义了非常多的异常:
Throwable:是所有异常的基类。它有两个主要子类:java.lang.Error
和java.lang.Exception
。
Error:表示系统错误或资源耗尽。如图中列出的虚拟机错误(VirtualMacheError)
及其子类内存溢出错误(OutOfMemory-Error)
和栈溢出错误(StackOverflowError)
等。该类异常由Java系统自己使用,应用程序不应抛出和处理。
Exception:表示应用程序错误。如图中的IOException (输入输出I/O异常)
、RuntimeException (运行时异常)
、SQLException (数据库SQL异常)
等。应用程序也可以通过继承Exception或其子类创建自定义异常。
特殊的,Exception有一个子类叫做RuntimeException,实际含义表示未受检异常(unchecked exception),相对而言,Exception的其它子类称为受检异常(checked exception)。 未受检异常不要求程序对可能抛出的异常进行处理,使用更加方便。
应用程序可以通过继承Exception或其子类创建自定义异常。特别的,如果继承的是RuntimeException,那么创建的将会是未受检异常。
181// 继承自RuntimeException的未受检异常
2public class BizException extends RuntimeException /*Exception*/ {
3 public BizException() {
4 }
5
6 public BizException(String message) {
7 super(message);
8 }
9
10 public BizException(Throwable cause) {
11 super(cause); // cause是异常的底层原因,可以用于构建异常链
12 }
13
14 public BizException(String message, Throwable cause) {
15 super(message, cause);
16 }
17}
18
throw
用来抛出一个异常对象,并将这个异常对象传递到调用者处,并结束当前方法的执行。
181public static void main(String[] args) {
2 String[] arr = {"a", "b", "c"};
3
4 // 查找数组元素
5 String element = getElement(arr, 3);
6 System.out.println("element = " + element);
7}
8
9public static String getElement(String[] arr, int index) {
10 // 判断索引是否越界
11 if (index < 0 || index > arr.length - 1) {
12 // 越界,创建异常对象并抛出
13 throw new ArrayIndexOutOfBoundsException("哥们,下标越界了~~~");
14 }
15
16 return arr[index];
17}
18
异常抛出后,会沿着方法栈往调用者传递,我们可以对其进行捕捉和处理。
91public static void main(String[] args) {
2 try {
3 int anInt = Integer.parseInt("a");
4 } catch (NumberFormatException exception) {
5 // 处理异常
6 exception.printStackTrace(); // java.lang.NumberFormatException: For input string: "a"
7 }
8}
9
异常捕捉后,可以获取异常相关的信息,如下:
151// 异常描述信息
2String message = exception.getMessage(); // For input string: "a"
3
4// 异常的底层原因
5Throwable cause = exception.getCause(); // null或其它异常
6
7// 异常的字符串形式(不常用)
8String toString = exception.toString(); // java.lang.NumberFormatException: For input string: "a"
9
10// 异常的堆栈信息
11StackTraceElement[] stackTrace = exception.getStackTrace();
12
13// 打印堆栈信息到标准错误流
14exception.printStackTrace(); // java.lang.NumberFormatException: For input string: "a" ...
15
如需捕捉多个异常,则可以按照如下格式书写,注意越明确的类型应越先捕捉。
121try {
2 // 业务代码
3} catch (ArrayIndexOutOfBoundsException | StringIndexOutOfBoundsException exception) {
4 System.out.println("数组索引越界异常|字符串索引越界异常");
5} catch (IndexOutOfBoundsException exception) {
6 System.out.println("索引越界异常");
7} catch (RuntimeException exception) {
8 System.out.println("运行时异常");
9} catch (Exception exception) {
10 System.out.println("异常");
11}
12
注意:
如果异常一直未被捕捉,最后会被Java虚拟机处理,默认行为是打印堆栈信息,然后退出线程。
对于受检异常,如果未在当前方法进行捕捉,则必须通过throws
关键字在方法上进行声明,提醒调用者处理异常。
71public static void main(String[] args) throws ParseException {
2 DateFormat dateTimeInstance = DateFormat.getDateTimeInstance();
3
4 // parse方法可能抛出受检异常ParseException(已在方法进行声明)
5 Date date = dateTimeInstance.parse("2022-12-21 15:23:32");
6}
7
注意:
子类方法不能声明或抛出父类方法中未声明的异常。
你可以声明抛出异常,但实际并不抛出,这一般用在在父类方法,方便子类进行扩展。
try后面还可以跟finally语句,finally内的代码不管有无异常发生,都会执行,一般用于释放资源,如数据库连接、文件流等。
111public static void main(String[] args) throws ParseException {
2 DateFormat dateTimeInstance = DateFormat.getDateTimeInstance();
3
4 try {
5 Date date = dateTimeInstance.parse("2022-12-21 15:23:32");
6 } finally {
7 // 虚拟机正常运行时,该代码一定会被执行
8 System.out.println("---finally---");
9 }
10}
11
注意:
如果程序被突然终止(宕机、断电等)或在try/catch中调用了退出JVM相关的方法,则finally代码块不会被执行。
如果某些资源即使在程序退出后也不能自动释放,则不能依赖finally代码块,如持久化存储的业务标记。
如果finally代码块中有return语句或抛出异常,则会覆盖try代码块中的返回结果,应避免该情况。
try-with-resources语句配合java.lang.AutoCloseable
接口,可以实现资源的自动关闭(基于finally代码块实现)。
331// 传统写法
2public static void useResource() throws Exception {
3 //创建资源
4 AutoCloseable r = new FileInputStream("hello");
5
6 try {
7 //使用资源
8
9 } finally {
10 // 释放资源
11 r.close();
12 }
13}
14
15// try-with-resources写法(资源可以定义多个,以分号分隔)
16public static void useResource() throws Exception {
17 // 创建资源(退出try后可自动调用close()方法释放资源)
18 try (AutoCloseable r = new FileInputStream("hello")) {
19 // 使用资源
20 }
21}
22
23// try-with-resources写法(引入方式,创建新的变量保存)
24public static void useResource() throws Exception {
25 final Resource resource1 = new Resource("resource1");
26 Resource resource2 = new Resource("resource2");
27
28 // 引入方式
29 try (Resource r1 = resource1; Resource r2 = resource2) {
30 // 使用资源
31 }
32}
33
在Java 9之前,资源必须声明和初始化在try语句块内,Java 9去除了这个限制,资源可以在try语句外被声明和初始化,但必须是final的或者是事实上final的(即虽然没有声明为final但也没有被重新赋值)。
101// try-with-resources写法(引入方式, JDK9+简写)
2public static void useResource() throws Exception {
3 final Resource resource1 = new Resource("resource1");
4 Resource resource2 = new Resource("resource2");
5
6 // 引入方式
7 try (resource1; resource2) {
8 // 使用资源
9 }
10}
在catch代码块中可重新抛出异常,异常可以是原来的,也可以是新建的,并且可以关联原来的异常形成异常链。
71try {
2 // 业务代码
3} catch (NumberFormatException exception) {
4 System.out.println("not valid number");
5 throw new BizException("输入格式不正确", exception);
6}
7
上述案例中,捕捉到NumberFormatException异常后,转化为统一的BizException重新抛出,并将exception作为cause传递给了新建的BizException,这样就形成了一个异常链,捕获到BizException的代码可以通过getCause()得到底层的NumberFormatException。
某些Java的异常类并没有定义带cause的构造方法,但可以通过Throwable的Throwable initCause(Throwable cause)
方法来设置cause,但是必须注意,该方法只能被调用一次。
如果在try或者catch语句内有return语句,则return语句执行后的结果先会缓存,待finally语句执行结束后才返回(但是该值不能被改变)。
151public class Demo {
2 public static void main(String[] args) throws ParseException {
3 System.out.println(test()); // 返回值是0,而不是2
4 }
5
6 public static int test() {
7 int ret = 0;
8 try {
9 return ret; // 返回值
10 } finally {
11 ret = 2; // 返回值已确定,无法修改
12 }
13 }
14}
15
如果在finally中也有return语句呢? 那么try和catch内的return会丢失,实际会返回finally中的返回值。finally中有return不仅会覆盖try和catch内的返回值,还会掩盖try和catch内的异常,就像异常没有发生一样。
161public class Demo {
2 public static void main(String[] args) throws ParseException {
3 System.out.println(test()); // 正常返回2,而非抛异常
4 }
5
6 public static int test() {
7 int ret = 0;
8 try {
9 int a = 1 / 0; // ArithmeticException
10 return ret;
11 } finally {
12 return 2; // 实际返回
13 }
14 }
15}
16
同理,如果finally代码块中抛出了异常,则原返回值或异常也将会被掩盖。
151public class Demo {
2 public static void main(String[] args) throws ParseException {
3 System.out.println(test()); // 抛RuntimeException异常而非ArithmeticException
4 }
5
6 public static int test() {
7 try {
8 int a = 1 / 0; // ArithmeticException
9 return a;
10 } finally {
11 throw new RuntimeException("hello"); // RuntimeException
12 }
13 }
14}
15
因此,应该尽量避免在finally中使用return语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理。
文件:文件是操作系统对磁盘数据的抽象,方便用户进行数据管理。
文件存储:文件在磁盘上以二进制形式进行存储,根据解读方式的不同,可分为UTF-8文本文件、JPG图片文件、MP4视频文件、ZIP压缩文件等多种类型,一般以后缀名进行标识。
文本文件:如果文件能以某种编码(UTF-8、GBK等)映射为可读的字符形式,那么该类文件称为文本文件。文本文件具有换行的概念,在Windows系统中使用\r\n
这2个字节表示换行符(Linux为\n
,MAC系统为\r
)。
注意:
在Windows系统中,文件名是大小写不敏感的,即同目录下的a.txt和A.txt是同一个文件。
文件IO比较慢,且需经过内核态和用户态的两次复制,因此文件操作时一般按块进行,并设置一定大小的缓冲区。
java.io.File 类封装了操作系统和文件系统的差异,提供了统一的文件和目录API。它可以表示文件,也可以表示目录,构造方法如下:
91// 1. 构造方法。创建一个表示文件或目录的不可变对象。文件或目录可以存在,也可以不存在,并不会实际打开文件。
2public File(String pathname) // pathname-完整路径[+文件名](可以是相对路径,也可以是绝对路径)
3public File(String parent, String child) // parent-父目录 child-表示子目录[+文件名]
4public File(File parent, String child)
5
6// 2. 获取构造参数
7public String getPath() // 构造File对象时的原始路径和文件名
8public String getName() // 仅文件或目录名称
9
File 类中有 4 个静态变量, 表示路径或目录的分隔符:
91// 1. 文件路径分隔符(Linux为正斜杠/,windows为反斜杠\)
2public static final String separator
3public static final char separatorChar
4
5// 2. 多个文件路径中的分隔符,如环境变量PATH中的分隔符,Java类路径变量classpath中的分隔符(Linux为:,windows为;)
6public static final String pathSeparator
7public static final char pathSeparatorChar
8
9// 3. 特别提示:特定于平台的换行符可以使用System.lineSeparator()获取,或使用BufferedReader.newLine()输出
271// 1. 父目录
2public String getParent()
3public File getParentFile()
4
5// 2. 绝对路径
6public boolean isAbsolute() // 判断File中的路径是否是绝对路径
7public String getAbsolutePath() // 完整的绝对路径名
8public File getAbsoluteFile()
9
10// 3. 简洁的绝对路径(去掉路径中的"."或".."以及跟踪软链接等)
11public String getCanonicalPath() throws IOException
12public File getCanonicalFile() throws IOException
13
14// 4. 文件或目录是否存在
15public boolean exists()
16
17// 5. 是否为文件或目录
18public boolean isDirectory()
19public boolean isFile()
20
21// 6. 文件长度(字节数),对目录没有意义
22public long length()
23
24// 7. 文件修改时间
25public long lastModified() // 最后修改时间,从纪元时开始的毫秒数
26public boolean setLastModified(long time) // 设置最后修改时间,返回是否修改成功
27
注意, File 对象没有返回创建时间的方法 , 因为创建时间不是一个公共概念 , Linux/Unix 就没有创建时间的概念。
File 类中与安全和权限相关的主要方法有:
161public boolean isHidden() // 是否为隐藏文件(Linux中文件无隐藏属性,以.开头的文件表示隐藏文件)
2public boolean canExecute() // 是否可执行
3public boolean canRead() // 是否可读
4public boolean canWrite() // 是否可写
5public boolean setReadOnly() // 设置文件为只读文件,修改成功返回true,否则返回false,下亦同
6
7// 修改文件读权限(ownerOnly表示是否仅对文件拥有者生效,如果false,等效于chmod a+r )
8public boolean setReadable(boolean readable, boolean ownerOnly)
9public boolean setReadable(boolean readable) // ownerOnly默认为true,等效chmod o+r
10// 修改文件写权限
11public boolean setWritable(boolean writable, boolean ownerOnly)
12public boolean setWritable(boolean writable) // chmod o+w
13// 修改文件可执行权限
14public boolean setExecutable(boolean executable, boolean ownerOnly)
15public boolean setExecutable(boolean executable) // chmod o+x
16
当 File 对象代表文件时,主要操作有创建 、 删除 、 重命名等。
161// 1. 实际创建文件。创建成功返回true,否则返回false。如果文件已存在,则不会重新创建。
2public boolean createNewFile() throws IOException
3
4// 2. 创建临时文件(静态方法)
5// 只可以指定临时文件的前缀、后缀和目录,而完整得路径名是由系统生成的,具有唯一性
6// prefix-前缀,至少3个字符 suffix-后缀名,默认为.tmp directory-临时文件所在目录,为null或不指定则为系统默认目录
7public static File createTempFile(String prefix, String suffix) throws IOException
8public static File createTempFile(String prefix, String suffix, File directory) throws IOException
9
10// 3. 删除文件
11public boolean delete() // 删除文件或空目录,删除成功返回true,否则返回false(注意:删除非空目录将失败,返回false)
12public void deleteOnExit() // 在 Java 虚拟机正常退出的时候进行删除
13
14// 4. 重命名文件
15public boolean renameTo(File dest) // 重命名成功返回true
16
当 File 对象代表目录时,可以执行目录相关的操作,如创建、遍历等。
191// 创建目录。成功返回true , 失败返回false,如果目录已存在,返回值是false。
2public boolean mkdir() // 中间目录要求存在,否则返回false,类似于:mkdir p-dir/a1
3public boolean mkdirs() // 创建中间目录,类似于:mkdir -p p-dir/a1
4
5// 列举目录下的所有文件和目录(. .. 除外),但不进行递归
6public String]] list()
7public String]] list(FilenameFilter filter) // FilenameFilter 传入目录和文件名进行过滤,返回true表示可列举
8public File]] listFiles()
9public File[] listFiles(FileFilter filter) // FileFilter 传入完整文件名进行过滤,返回true表示可列举
10public File[] listFiles(FilenameFilter filter)
11
12public interface FileFilter {
13 boolean accept(File pathname);
14}
15
16public interface FilenameFilter {
17 boolean accept(File dir, String name);
18}
19
831// 列出当前目录下的所有扩展名为 .txt 的文件
2public static void main(String[] args) {
3 File f = new File(".");
4 // 列举目录下的文件或目录并执行过滤
5 File[] files = f.listFiles(new FilenameFilter() {
6
7 public boolean accept(File dir, String name) {
8 if (name.endsWith(".txt")) {
9 return true; // 仅列举txt文件
10 }
11 return false;
12 }
13 });
14}
15
16// 递归遍历:列举目录下的所有文件(不包括目录)
17public static List<File> listAllFiles(final File directory) {
18 ArrayList<File> files = new ArrayList<>();
19
20 if (directory.isFile()) {
21 // 文件,直接处理
22 files.add(directory);
23 } else {
24 // 目录,列举后循环处理
25 for (File file : directory.listFiles()) {
26 if (file.isFile()) {
27 // 子文件,直接处理
28 files.add(directory);
29 } else {
30 // 子目录,递归
31 files.addAll(listAllFiles(file));
32 }
33 }
34 }
35
36 return files;
37}
38
39
40// 递归遍历:计算一个目录下的所有文件的大小
41public static long sizeOfDirectory(final File directory, String suffix) {
42 long size = 0;
43 if (directory.isFile()) {
44 // 文件,直接处理
45 if (directory.getName().endsWith(suffix)) {
46 size = directory.length();
47 }
48 } else {
49 // 目录,列举后循环处理
50 for (File file : directory.listFiles()) {
51 if (file.isFile()) {
52 // 子文件,直接处理
53 if (file.getName().endsWith(suffix)) {
54 size += file.length();
55 }
56 } else {
57 // 子目录,递归
58 size += sizeOfDirectory(file, suffix);
59 }
60 }
61 }
62 return size;
63}
64
65// 递归遍历:删除非空目录
66public static void deleteRecursively(final File file) throws IOException {
67 if (file.isFile()) {
68 // 文件,直接处理
69 if (!file.delete()) {
70 throw new IOException("Failed to delete " + file.getCanonicalPath());
71 }
72 } else if (file.isDirectory()) {
73 // 目录,列举后循环处理每个子文件或子目录
74 for (File child : file.listFiles()) {
75 deleteRecursively(child);
76 }
77
78 // 再处理当前目录
79 if (!file.delete()) {
80 throw new IOException("Failed to delete " + file.getCanonicalPath());
81 }
82 }
83}
在Java中,将文件及其它输入输出设备抽象为流,并构建了基于流的相关协作体系,默认情况下,流为字节形式,称为字节流。
InputStream/OutputStream(抽象类)表示最顶层的字节输入流和字节输出流,其中定义了它们的一些共性方法:
321---------------------- InputStream --------------------------
2// 1. 读1个字节
3// 如果有数据,则返回0~255;如果无数据,则阻塞直到数据到来或流关闭或出现异常;如果读到结尾,则返回-1。
4public abstract int read() throws lOException
5
6// 2. 读n个字节
7// 最多读n个字节到字节数组中,返回值为实际读入的字节个数。如果流中无数据则会阻塞;如果刚开始读取时已到流结尾,则返回-1;
8public int read(byte b[]) throws IOException
9public int read(byte b[], int off, int len) throws lOException // off-起始索引,len-读取长度
10
11// 3. 关闭流
12public void close() throws lOException // 一般在finlly代码块中调用,并且对close()抛出的异常进行忽略
13
14// 4. 高级方法(跳读/重读)
15public long skip(long n) throws IOException // 尽力跳过输入流中n个字节,返回实际跳过的字节数
16public int available() throws IOException // 返回下一次不需要阻塞就能读取到的大概字节个数,InpuStream默认为0
17public synchronized void mark(int readlimit) // 标记流,此后可以使用reset()方法回到该位置,readlimit为最大可回退的字节数
18public boolean markSupported() // 判断是否支持mark()方法,FileInpuStream不直接支持,BufferedInput-Stream/ByteArrayInputStream可以支持
19public synchronized void reset() throws IOException // 回退流到mark()方法标记处
20
21---------------------- OutputStream --------------------------
22// 1. 写1个字节(取int参数的低8位)
23public abstract void write(int b) throws IOException
24
25// 2. 写n个字节
26public void write(byte b[]) throws IOException
27public void write(byte b[], int off, int len) throws IOException // off-起始索引,len-写入长度
28
29// 3. 刷新流/关闭流
30public void flush() throws IOException // 将缓冲而未实际写的数据进行实际写入(应用->操作系统)
31public void close() throws IOException // 先调用flush方法,再释放流占用的系统资源。一般也要求在finlly代码块调用。
32
FileInputStream/FileOutputStream继承自InputStream/OutputStream, 表示文件输入流和文件输出流,即输入输出目的地为文件。
221---------------------- FileInputStream --------------------------
2// 1. 构造方法。打开文件并构造FileInputStream
3// 如果文件不存在,则报错:FileNotFoundException: D:\video.txt (系统找不到指定的文件。)
4// 如果指定的文件是一个已存在的目录,则报错:FileNotFoundException: D:\video (拒绝访问。)
5// 如果当前用户没有读权限,会抛出异常SecurityException(RuntimeException)
6public FileInputStream(String name) throws FileNotFoundException
7public FileInputStream(File file) throws FileNotFoundException
8
9
10---------------------- FileOutputStream --------------------------
11
12// 1. 构造方法。打开文件并构造FileOutputStream
13// 如果文件存在,则进行覆盖;如果文件不存在,则创建新文件。
14// 如果指定的文件是一个已存在的目录,或者由于其他原因不能打开文件,会抛出异常FileNotFoundException(IOException)
15// 如果当前用户没有写权限,会抛出异常SecurityException(RuntimeException)
16public FileOutputStream(File file, boolean append) throws FileNotFoundException // append-是否追加方式打开
17public FileOutputStream(String name) throws FileNotFoundException
18
19// 2. 高级方法
20public final FileDescriptor getFD() // 获取文件描述符(保存与操作系统相关的一些文件内存结构,同时提供了sync()方法)
21public native void sync() throws SyncFailedException // 刷新缓存区(操作系统->硬盘)
22
下面是一些按字节读写文件的示例:
461// 批量写文件示例
2public static void main(String[] args) throws FileNotFoundException {
3 // 1. 打开文件
4 OutputStream output = new FileOutputStream("hello.txt"); // FileNotFoundException
5
6 // 2. 写字节数据
7 try {
8 String data = "hello, 黄原鑫";
9 byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
10 output.write(bytes);
11 } catch (IOException e) {
12 e.printStackTrace();
13 } finally {
14
15 // 3. 关闭文件
16 try {
17 output.close();
18 } catch (IOException e) {
19 e.printStackTrace();
20 }
21 }
22}
23
24// 批量读文件示例
25public static void main(String[] args) throws FileNotFoundException {
26 // 1. 打开文件
27 InputStream input = new FileInputStream("hello.txt"); // FileNotFoundException
28
29 // 2. 按字节读取数据
30 try {
31 byte[] buf = new byte[1024];
32 int bytesRead = input.read(buf); // 最多读1024字节
33 String data = new String(buf, 0, bytesRead, StandardCharsets.UTF_8);
34 System.out.println(data);
35 } catch (IOException e) {
36 e.printStackTrace();
37 } finally {
38
39 // 3. 关闭文件
40 try {
41 input.close();
42 } catch (IOException e) {
43 e.printStackTrace();
44 }
45 }
46}
ByteArrayInputStream/ByteArrayOutputStream也继承自InputStream/OutputStream, 表示字节数组输入流和字节数组输出流,即输入输出目的地为字节数组。
221---------------------- ByteArrayInputStream --------------------------
2// 1. 构造方法。将字节数组包装为一个字节输入流,是一种适配器模式。
3// 字节数组输入流的所有数据都在内存,支持mark/reset重复读取
4public ByteArrayInputStream(byte buf[])
5public ByteArrayInputStream(byte buf[], int offset, int length) // offset-起始索引 length-使用的长度
6
7---------------------- ByteArrayOutputStream --------------------------
8// 1. 构造方法。内部为一个字节数组,数组长度根据流输出的内容动态扩展。
9public ByteArrayOutputStream()
10public ByteArrayOutputStream(int size) // size-初始尺寸大小,默认为32,不足时进行指数扩展
11
12// 2. 转换为字节数组或字符串
13public synchronized byte[] toByteArray()
14public synchronized String toString() // 使用系统默认编码
15public synchronized String toString(String charsetName) // 使用指定编码
16
17// 3. 写到另一个OutputStream
18public synchronized void writeTo(OutputStream out) throws IOException
19
20// 4. 其它方法
21public synchronized int size() // 当前写入的字节个数
22public synchronized void reset() // 重置字节个数为0
下面示例将从文件输入流读取数据到字节数组输出流,然后转化为字符串输出。
361public static void main(String[] args) throws FileNotFoundException {
2 // 1. 打开输入流(FileInputStream)
3 InputStream input = new FileInputStream("hello.txt");
4 ByteArrayOutputStream output = null;
5
6 try {
7 // 2. 打开输出流(ByteArrayOutputStream)
8 output = new ByteArrayOutputStream();
9
10 // 3. 每次最多读取1024字节,读到结尾时(-1)退出
11 byte[] buf = new byte[1024];
12 int bytesRead = 0;
13 while ((bytesRead = input.read(buf)) != -1) {
14 // 4. 写到输出流
15 output.write(buf, 0, bytesRead); // 由于最后一次读取可能<1024字节,因此需指定本次读取的字节长度
16 }
17
18 // 5. 字节数组输出流转换为字符串
19 String data = output.toString("UTF-8");
20 System.out.println(data);
21 } catch (IOException e) {
22 e.printStackTrace();
23 } finally {
24 // 6. 关闭输出流和输入流
25 try {
26 output.close();
27 } catch (IOException ioException) {
28 ioException.printStackTrace();
29 }
30 try {
31 input.close();
32 } catch (IOException ioException) {
33 ioException.printStackTrace();
34 }
35 }
36}
DataInputStream/DataOutputStream是装饰类基类FilterInputStream/FilterOutputStream
的子类,并且实现了DataInput/DataOutput
接口,可以以各种基本类型和字符串读取或写入数据。
191---------------------- DataInputStream --------------------------
2// 1. 构造方法。装饰一个InputStream(接口+组合)
3public DataInputStream(InputStream in)
4
5// 2. 读数据
6boolean readBoolean() throws lOException;
7int readInt() throws IOException;
8String readUTF() throws IOException;
9
10
11---------------------- DataOutputStream --------------------------
12// 1. 构造方法。装饰一个OutputStream(接口+组合)
13public DataOutputStream(OutputStream out)
14
15// 2. 写数据
16void writeBoolean(boolean v) throws IOException; // 写入1个字节,如果值为true,则写入1,否则0
17void writeInt(int v) throws IOException; // 写入4个字节,最高位字节先写入,最低位最后写入
18void writeUTF(String s) throws IOException; // 将字符串的UTF-8编码字节写入
19
下面是一个使用DataInputStream/DataOutputStream装饰FileInputStream/FileOutputStream后,用来序列化对象的使用示例:
601public class Student {
2 String name;
3 int age;
4 double score;
5
6 // 无参构造、全参构造、Getter/Setter、toString
7
8 public static void main(String[] args) throws IOException {
9 List<Student> students = Arrays.asList(new Student[]{
10 new Student("张三", 18, 80.9d),
11 new Student("李四", 17, 67.5d)
12 });
13
14 writeStudents(students);
15
16 List<Student> readStudents = readStudents();
17 System.out.println(readStudents);
18 }
19
20 public static void writeStudents(List<Student> students) throws IOException {
21 // 1. 装饰FileOutputStream
22 DataOutputStream output = new DataOutputStream(new FileOutputStream("students.dat"));
23
24 try {
25 // 2. 写数据
26 output.writeInt(students.size());
27 for (Student s : students) {
28 output.writeUTF(s.getName());
29 output.writeInt(s.getAge());
30 output.writeDouble(s.getScore());
31 }
32 } finally {
33 // 3. 关闭流
34 output.close();
35 }
36 }
37
38 public static List<Student> readStudents() throws IOException {
39 // 1. 装饰FileInputStream
40 DataInputStream input = new DataInputStream(new FileInputStream("students.dat"));
41
42 try {
43 // 2. 读数据
44 int size = input.readInt();
45 List<Student> students = new ArrayList<>(size);
46 for (int i = 0; i < size; i++) {
47 Student s = new Student();
48 s.setName(input.readUTF());
49 s.setAge(input.readInt());
50 s.setScore(input.readDouble());
51 students.add(s);
52 }
53 return students;
54 } finally {
55 // 3. 关闭流
56 input.close();
57 }
58 }
59}
60
ObjectInputStream/ObjectOutputStream继承自InputStream/OutputStream,并实现了ObjectInput/ObjectOutput
接口,可以读取和写入实现了java.io.Serializable
接口的对象。
141---------------------- ObjectInputStream --------------------------
2// 1. 构造方法
3public ObjectInputStream(InputStream in) throws IOException
4
5// 2. 读数据:字节、整型、字符串、对象等
6// 如果反序列化的数据没有匹配的类信息,将会报ClassNotFoundException
7public final Object readObject() throws IOException, ClassNotFoundException
8
9---------------------- ObjectOutputStream --------------------------
10// 1. 构造方法
11public ObjectOutputStream(OutputStream out) throws IOException
12
13// 2. 写数据:字节、整型、字符串、对象等
14public final void writeObject(Object obj) throws IOException
下面是一个使用ObjectInputStream/ObjectOutputStream来写入和读取对象的示例:
531public class Student implements Serializable {
2 String name;
3 int age;
4 double score;
5
6 public static void main(String[] args) throws IOException, ClassNotFoundException {
7 List<Student> students = Arrays.asList(new Student[]{
8 new Student("张三", 18, 80.9d),
9 new Student("李四", 17, 67.5d)
10 });
11
12 writeStudents(students);
13
14 List<Student> readStudents = readStudents();
15 System.out.println(readStudents);
16 }
17
18 public static void writeStudents(List<Student> students) throws IOException {
19 // 1. 对象输出流
20 ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("students.dat")));
21
22 try {
23 // 2. 写数据
24 out.writeInt(students.size());
25 for (Student s : students) {
26 out.writeObject(s); // 一次写一个对象
27 }
28 } finally {
29 // 3. 关闭流
30 out.close();
31 }
32 }
33
34 public static List<Student> readStudents() throws IOException, ClassNotFoundException {
35 // 1. 对象输入流
36 ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream("students.dat")));
37
38 try {
39 // 2. 读数据
40 int size = in.readInt();
41 List<Student> list = new ArrayList<>(size);
42 for (int i = 0; i < size; i++) {
43 list.add((Student) in.readObject()); // 一次读一个对象
44 }
45 return list;
46 } finally {
47 // 3. 关闭流
48 in.close();
49 }
50 }
51
52}
53
实际上,List以及之前介绍的String、Date、Double、Map等, 都实现了Serializable接口,上述示例可以再次简化:
201 public static void writeStudentList(List<Student> students) throws IOException {
2 ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("students.dat")));
3 try {
4 // 直接序列化Student数组
5 out.writeObject(students);
6 } finally {
7 out.close();
8 }
9 }
10
11 public static List<Student> readStudentList() throws IOException, ClassNotFoundException {
12 ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream("students.dat")));
13 try {
14 // 反序列化Student数组
15 return (List<Student>) in.readObject();
16 } finally {
17 in.close();
18 }
19 }
20
扩展:
ObjectInput/ObjectOutput是DataInput/DataOutput的子接口,增加了Object readObject()和void writeObject(Object obj)方法。
BufferedInputStream/BufferedOutputStream也是装饰类基类FilterInputStream/FilterOutputStream
的子类,它提供了对流进行缓冲的作用,提升操作流的性能。
131---------------------- BufferedInputStream --------------------------
2// 1. 构造方法。装饰一个InputStream,并且支持其mark/reset方法
3public BufferedInputStream(InputStream in)
4public BufferedInputStream(InputStream in, int size) // size-缓冲区大小,默认为8192
5
6---------------------- BufferedOutputStream --------------------------
7// 1. 构造方法。装饰一个OutputStream
8public BufferedOutputStream(OutputStream out)
9public BufferedOutputStream(OutputStream out, int size) // size-缓冲区大小,默认为8192
10
11// 2. 刷新流
12public synchronized void flush() throws IOException // 将缓冲区的内容写到包装的流中,并调用包装流的flush()方法
13
在使用FileInputStream/FileOutputStream时,应该几乎总是在它的外面包上对应的缓冲类,如下所示:
81// 使用BufferedInputStream/BufferedOutputStream装饰FileInputStream/FileOutputStream,使其具有缓冲功能
2Inputstream input = new BufferedInputStream(new FileInputStream("hello.txt"));
3Outputstream output = new BufferedOutputStream(new FileOutputStream("hello.txt"));
4
5// 被BufferedInputStream/BufferedOutputStream装饰后的流依然可以被其它装饰类装饰
6DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("students.dat")));
7DataInputStream input = new DataInputStream(new BufferedInputStream(new FileInputStream("students.dat")));
8
如果需要对文件进行随机读写或重复读,可以使用RandomAccessFile,它一个更接近于操作系统API的封装类。
411// 1. 构造方法
2// mode取值有四种,分别为 r-只读、rw-读写、rwd-读写并同步刷新文件内容、rws-读写并同步刷新文件内容及元数据)
3public RandomAccessFile(String name, String mode) throws FileNotFoundException
4public RandomAccessFile(File file, String mode) throws FileNotFoundException
5
6// 2. 操作文件指针
7// RandomAccessFile内部有一个文件指针,指向当前读写的位置,各种read/write操作都会自动更新该指针
8public native long getFilePointer() throws IOException; //获取当前文件指针
9public native void seek(long pos) throws IOException; //更改当前文件指针到pos
10public int skipBytes(int n) throws IOException // 文件指针位移n
11public native long length() throws IOException; // 文件字节数
12public native void setLength(long newLength) throws IOException; // 修改文件长度(文件将会截断或扩展,扩展内容未定义)
13
14// 3. 读方法
15public int read() throws IOException //读一个字节,取最低八位,0到255
16public int read(byte b[]) throws IOException
17public int read(byte b[], int off, int len) throws IOException
18public final double readDouble() throws IOException
19public final int readInt() throws IOException
20public final String readUTF() throws IOException
21
22// 4. 读满字节数组
23// 读够期望的长度,如果到了文件结尾也没读够,它们会抛出EOFException异常
24public final void readFully(byte b[]) throws IOException // 读满字节数组
25public final void readFully(byte b[], int off, int len) throws IOException
26
27// 5. 写方法
28public void write(int b) throws IOException
29public final void writeInt(int v) throws IOException
30public void write(byte b[]) throws IOException
31public void write(byte b[], int off, int len) throws IOException
32public final void writeUTF(String str) throws IOException
33
34// 6. 关闭文件
35public void close() throws IOException
36
37// 7. 有问题的方法
38// RandomAccessFile没有编码的概念,假定一个字节代表一个字符,无法处理多字节字符(中文),应避免使用
39public final void writeBytes(String s) throws IOException // 写字符串(要求输入字符串无多字节字符)
40public final String readLine() throws IOException // 读一行(要求文件无多字节字符)
41
注意:
虽然RandomAccessFile有类似于读写字节流的方法,但大多是实现DataInput/DataOutput接口而来,并不是InputStream/OutputStream的子类。
如果需要处理大型文件或在不同应用程序之间共享数据,可以使用MappedByteBuffer,它是文件映射到内存的字节数组,操作该字节数组即可操作文件,大多数操作系统都支持该机制,称为内存映射文件。
内存映射文件基于FileInputStream/FileOutputStream或RandomAccessFile,它们都有一个获取FileChannel
方法,而FileChannel可以将文件映射到内存,映射完成后,文件就可以关闭了,后续对文件的读写可以通过MappedByteBuffer完成。
91// 1. 获取FileChannel
2public FileChannel getChannel()
3
4// 2. 映射文件到内存
5// mode-映射模式(MapMode.READ_ONLY-只读、MapMode.READ_WRITE-读写、MapMode.PRIVATE-私有模式,更改不反映到文件,也不被其他程序看到 )
6// position-映射起始位置 size-映射长度,如果映射的区域超过了现有文件的范围,则文件会自动扩展,扩展出的区域字节内容为0
7public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException
8
9
注意:
映射模式受限于文件打开的方式,若是输入流或写模式打开文件,则不能设置为READ_WRITE映射模式。
内存映射文件仅在发生实际读写时,才会将要读写的部分按页映射到内存。数据读写完毕后,由操作系统进行同步,只要操作系统不崩溃,一定可以同步到磁盘上,即使应用程序已经退出。
在该种方式下,程序直接访问内核内存空间,仅需一次数据拷贝过程,比普通文件读写的性能更高。
内存映射文件也有局限性,比如,它不太适合处理小文件,它是按页分配内存的,对于小文件,会浪费空间,另外,映射文件要消耗一定的操作系统资源,初始化比较慢。
MappedByteBuffer代表内存中的字节数组,是 ByteBuffer(Buffer) 的子类,它可以简单理解为一个字节数组包装类,这个字节数组的长度是不可变的,在内存映射文件中,这个长度由map方法中的参数size决定。
231// 1. 获取和修改读写位置指针
2public final int position() // 获取当前读写位置
3public final Buffer position(int newPosition) // 修改当前读写位置
4
5// 2. 读写数据(这些方法在读写后,都会自动增加position)
6public abstract byte get(); // 从当前位置获取一个字节
7public ByteBuffer get(byte[] dst) // 从当前位置拷贝dst.length长度的字节到dst
8public abstract int getInt(); // 从当前位置读取一个int
9public abstract double getDouble(); // 从当前位置读取一个double
10public final ByteBuffer put(byte[] src) // 将字节数组src写入当前位置
11public abstract ByteBuffer putLong(long value); // 将long类型的value写入当前位置
12
13// 3. 指定位置读写数据(这些方法在读写时,不会改变当前读写位置position)
14public abstract int getInt(int index); // 从index处读取一个int
15public abstract double getDouble(int index); //从index处读取一个double
16public abstract ByteBuffer putDouble(int index, double value); // 在index处写入一个double
17public abstract ByteBuffer putLong(int index, long value); // 在index处写入一个long
18
19// 4. 与文件内容同步相关的方法
20public final boolean isLoaded() // 检查文件内容是否真实加载到了内存,这个值是一个参考值,不一定精确
21public final MappedByteBuffer load() // 尽量将文件内容加载到内存
22public final MappedByteBuffer force() // 将对内存的修改强制同步到硬盘上
23
内存映射文件的另一个重要特点是,它可以被多个不同的应用程序共享,多个程序可以映射同一个文件,映射到同一块内存区域,一个程序对内存的修改,可以让其他程序也看到,这使得它特别适合用于不同应用程序之间的通信。
381/**
2 * 拷贝字节流。类似于JDK9中的InputStream.transferTo()方法
3 */
4public static void copy(InputStream input, OutputStream output) throws IOException {
5 byte[] buf = new byte[4096];
6 int bytesRead = 0;
7 while ((bytesRead = input.read(buf)) != -1) {
8 output.write(buf, 0, bytesRead);
9 }
10}
11
12/**
13 * 将文件读入字节数组
14 */
15public static byte[] readFileToByteArray(String fileName) throws IOException {
16 InputStream input = new FileInputStream(fileName);
17 ByteArrayOutputStream output = new ByteArrayOutputStream();
18 try {
19 copy(input, output);
20 return output.toByteArray();
21 } finally {
22 input.close();
23 }
24}
25
26/**
27 * 将字节数组写入文件
28 *
29 */
30public static void writeByteArrayToFile(String fileName, byte[] data) throws IOException {
31 OutputStream output = new FileOutputStream(fileName);
32 try {
33 output.write(data);
34 } finally {
35 output.close();
36 }
37}
38
字符流指以字符为单位读取和解读流中的字节,一个字符可能包含多个字节,这取决解读时使用的字符编码。
注意:对于增补字符集,一个完整的字符内容可能需要两个字符(char)来表示。
Reader/Writer(抽象类)表示最顶层的字符输入流和字符输出流,其中定义了它们的一些共性方法:
321---------------------- Reader --------------------------
2// 1. 读1个字符(0~65535)
3public int read() throws lOException
4
5// 2. 读N个字符
6public int read(char cbuf[]) throws lOException
7abstract public int read(char cbuf[], int off, int len) throws IOException
8
9// 3. 关闭流
10abstract public void close() throws IOException
11
12// 4. 高级方法
13public long skip(long n) throws IOException // 跳过n个字符
14public boolean ready() throws IOException // 返回下一次不需要阻塞就能读取到的大概字符个数。类似于字节流的有available()方法
15
16
17---------------------- Writer --------------------------
18// 1. 写1个字符
19public void write(int c)
20
21// 2. 写n个字符
22public void write(char cbuf[])
23abstract public void write(char cbuf[], int off, int len) throws IOException
24
25// 3. 写字符串
26public void write(String str) throws IOException
27
28// 4. 刷新/关闭流
29abstract public void close() throws IOException;
30abstract public void flush() throws IOException;
31
32
InputStreamReader/OutputStreamWriter是适配器类,继承自Reader/Writer,能将字节流(InputStream/OutputStream)转换为字符流(Reader/Writer)。
101---------------------- InputStreamReader --------------------------
2// 1. 构造方法。将字节输入流适配为字符输入流
3public InputStreamReader(InputStream in) // 使用系统默认编码:Charset.defaultCharset()
4public InputStreamReader(InputStream in, String charsetName) // charsetName-字符编码
5
6---------------------- OutputStreamWriter --------------------------
7// 1. 构造方法。将字节输出流适配为字符输出流
8public OutputStreamWriter(OutputStream out) // 使用系统默认编码:Charset.defaultCharset()
9public OutputStreamWriter(OutputStream out, String charsetName) // charsetName-字符编码
10
下面是一个将字节流适配为字符流并进行字符读写的示例:
291public static void main(String[] args) throws IOException {
2 // 1. 将字节输出流适配为GB2312编码的字符输出流
3 Writer writer = new OutputStreamWriter(new FileOutputStream("hello.txt"), "GB2312");
4
5 try {
6 // 2. 按字符写数据
7 String str = "hello, 123,老马";
8 writer.write(str);
9 } finally {
10
11 // 3. 关闭流
12 writer.close();
13 }
14
15 // 1. 将字节输入流适配为GB2312编码的字符输入流
16 Reader reader = new InputStreamReader(new FileInputStream("hello.txt"), "GB2312");
17 try {
18
19 // 2. 按字符读数据
20 char[] cbuf = new char[1024]; // 最大读1024字节
21 int charsRead = reader.read(cbuf);
22 System.out.println(new String(cbuf, 0, charsRead));
23 } finally {
24
25 // 3. 关闭流
26 reader.close();
27 }
28}
29
FileReader/FileWriter继承自Reader/Writer, 表示文件字符输入流和文件字符输出流,即输入输出目的地为文件。
111---------------------- FileReader --------------------------
2// 1. 构造方法
3public FileReader(File file) throws FileNotFoundException
4public FileReader(String fileName) throws FileNotFoundException
5
6
7---------------------- FileWriter --------------------------
8// 1. 构造方法
9public FileWriter(File file) throws IOException
10public FileWriter(String fileName, boolean append) throws IOException // append-是否追加方式打开
11
注意:
FileReader和FileWriter以及下面介绍的几种字符流操作类,都不能直接指定编码类型,只能使用默认编码。
如需指定字符流的编码类型,可以使用适配器InputStreamReader/OutputStreamWriter将字节流转换为指定编码的字节流。
CharArrayReader/CharArrayWriter也继承自Reader/Writer, 表示字符数组输入流和字符数组输出流,即输入输出目的地为字符数组。
141---------------------- CharArrayReader --------------------------
2// 1. 构造方法
3public CharArrayReader(char buf[])
4public CharArrayReader(char buf[], int offset, int length)
5
6---------------------- CharArrayWriter --------------------------
7// 1. 构造方法
8public CharArrayWriter()
9public CharArrayWriter(int initialSize)
10
11// 2. 转换为字符数组或字符串
12public char[] toCharArray()
13public String toString()
14
下面是一个从文件字符流中读数据到字节数组输出流的示例:
251public static void main(String[] args) throws IOException {
2 // 1. 将文件字节流适配为GB2312编码的字符流
3 Reader reader = new InputStreamReader(new FileInputStream("hello.txt"), "GB2312");
4 CharArrayWriter writer = null;
5
6 try {
7 // 2. 创建默认编码的字符数组输出流
8 writer = new CharArrayWriter();
9
10 // 3. 读取输入流数据写到输出流
11 char[] cbuf = new char[1024];
12 int charsRead = 0;
13 while ((charsRead = reader.read(cbuf)) != -1) {
14 writer.write(cbuf, 0, charsRead);
15 }
16
17 // 4. 将字符数组输出流转换为字符串
18 System.out.println(writer.toString());
19 } finally {
20
21 // 5. 关闭流
22 reader.close();
23 writer.close();
24 }
25}
StringReader/StringWriter也继承自Reader/Writer, 表示字符串输入流和字符串输出流,即输入输出目的地为字符串。它与CharArrayReader/CharArrayWriter类似,只是输入源为String,输出目标为StringBuffer。实际上,String和StringBuffer内部是由char数组组成的,所以它们本质上是一样的。
141---------------------- StringReader --------------------------
2// 1. 构造方法
3public CharArrayReader(char buf[])
4public CharArrayReader(char buf[], int offset, int length)
5
6---------------------- StringWriter --------------------------
7// 1. 构造方法
8public StringWriter()
9public StringWriter(int initialSize)
10
11// 2. 其它方法
12public String toString() // 转换为字符串
13public StringBuffer getBuffer() // 获取内部的StringBuffer
14
BufferedReader/BufferedWriter是装饰类,直接继承自Reader/Writer,提供缓冲以及按行读写的功能。
171---------------------- BufferedReader --------------------------
2// 1. 构造方法。装饰一个Reader,使其具有缓冲和按行读写的功能
3public BufferedReader(Reader in)
4public BufferedReader(Reader in, int sz) // sz-缓冲区大小,默认为8192
5
6// 2. 特殊方法
7public String readLine() throws IOException // 读入1行字符(不包括换行符),当读到流结尾时,返回null
8
9
10---------------------- BufferedWriter --------------------------
11 // 1. 构造方法。装饰一个Writer,使其具有缓冲和按行读写的功能
12public BufferedWriter(Writer out)
13public BufferedWriter(Writer out, int sz) // sz-缓冲区大小,默认为8192
14
15// 2. 特殊方法
16public void newLine() throws IOException // 输出平台特定的换行符,来自于line.separator属性
17
注意:
通过
System.lineSeparator()
也可以获取平台特定的换行符。FileReader/FileWriter是没有缓冲的,也不能按行读写, 因此一般应该在它们的外面包上对应的缓冲类。
下面是一个带缓冲的文件字符流的读写示例:
671public class Student {
2 String name;
3 int age;
4 double score;
5
6 // 无参构造、全参构造、Getter/Setter、toString
7
8 public static void main(String[] args) throws IOException {
9 List<Student> students = Arrays.asList(new Student[]{
10 new Student("张三", 18, 80.9d),
11 new Student("李四", 17, 67.5d)
12 });
13
14 writeStudents(students);
15
16 List<Student> readStudents = readStudents();
17 System.out.println(readStudents);
18 }
19
20
21 public static void writeStudents(List<Student> students) throws IOException {
22 BufferedWriter writer = null;
23 try {
24 // 1. 装饰FileWriter,使其具有缓冲和按行读写的功能
25 writer = new BufferedWriter(new FileWriter("students.txt"));
26
27 // 2. 写字符流
28 for (Student s : students) {
29 writer.write(s.getName() + "," + s.getAge() + "," + s.getScore());
30 writer.newLine(); // 写换行符
31 }
32 } finally {
33 // 3. 关闭流
34 if (writer != null) {
35 writer.close();
36 }
37 }
38 }
39
40 public static List<Student> readStudents() throws IOException {
41 List<Student> students = new ArrayList<>();
42 BufferedReader reader = null;
43 try {
44 // 1. 装饰FileReader,使其具有缓冲和按行读写的功能
45 reader = new BufferedReader(new FileReader("students.txt"));
46
47 // 2. 按行读取字符流
48 String line = reader.readLine();
49 while (line != null) {
50 String[] fields = line.split(",");
51 Student s = new Student();
52 s.setName(fields[0]);
53 s.setAge(Integer.parseInt(fields[1]));
54 s.setScore(Double.parseDouble(fields[2]));
55 students.add(s);
56 line = reader.readLine();
57 }
58
59 return students;
60 } finally {
61 // 3. 关闭流
62 if (reader != null) {
63 reader.close();
64 }
65 }
66 }
67}
PrintWriter继承自Writer,是一个非常方便的类,可以直接指定文件名/File/OutputStream/Writer等作为构造参数,还可以指定编码类型,支持自动缓冲,可以自动将多种类型转换为字符串,在输出到文件时 ,可以优先选择该类。
211// 1. 构造函数
2public PrintWriter(String fileName) throws FileNotFoundException
3public PrintWriter(String fileName, String csn) throws FileNotFoundException, UnsupportedEncodingException
4public PrintWriter(File file) throws FileNotFoundException
5public PrintWriter(File file, String csn) throws FileNotFoundException, UnsupportedEncodingException
6public PrintWriter(OutputStream out)
7public PrintWriter(OutputStream out, boolean autoFlush) // autoFlush-是否在调用println/printf/format方法的时候同步缓冲区,默认为false
8public PrintWriter(Writer out)
9public PrintWriter(Writer out, boolean autoFlush)
10
11// 2. 输出数据
12public void print(int i)
13public void print(Object obj)
14public void println() // 输出换行符
15public void println(int x)
16public void println(Object x)
17
18// 3. 类似C语言中的格式化输出
19public PrintWriter printf(String format, Object ... args)
20PrintWriter writer = writer.format("%.2f", 123.456f); // 保留小数点后2位
21
注意:
如果以Writer为参数的构造方法,则PrintWriter就不会包装BufferedWriter了,其它类型参数则会。
下面是使用PrintWriter改造上面写学生信息的示例:
131public static void writeStudents2(List<Student> students) throws IOException {
2 // 1. 创建PrintWriter
3 PrintWriter writer = new PrintWriter("students.txt");
4 try {
5 // 2. 格式化输出到流
6 for (Student s : students) {
7 writer.println(s.getName() + "," + s.getAge() + "," + s.getScore());
8 }
9 } finally {
10 // 3. 关闭流
11 writer.close();
12 }
13}
PrintStream继承自FilterOutputStream,属于字节流,但其功能与PrintWriter非常的相似。一些差异点如下:
PrintStream不支持Writer作为构造方法参数。
PrintStream在碰到换行符'\n'时自动刷新缓冲区。
write(int b)方法的实现不同,正常的字符流PrintWriter写一个字符(2个低字节),而PrintStream只写一个字节。
Scanner是一个单独的类,它是一个简单的文本扫描器,能够从流中提取基本类型和字符串。
221// 1. 构造方法
2public Scanner(String source)
3public Scanner(Path source)
4public Scanner(Path source, String charsetName)
5public Scanner(File source) throws FileNotFoundException
6public Scanner(File source, String charsetName) throws FileNotFoundException
7public Scanner(InputStream source)
8public Scanner(InputStream source, String charsetName)
9public Scanner(Readable source)
10public Scanner(ReadableByteChannel source)
11public Scanner(ReadableByteChannel source, String charsetName)
12
13// 2. 设置分隔符(Scanner工作时需要一个分隔符来将不同数据区分开来,默认是使用空白符)
14public Scanner useDelimiter(Pattern pattern)
15public Scanner useDelimiter(String pattern)
16
17// 3. 扫描数据
18public String next()
19public String nextLine()
20public int nextInt()
21public float nextFloat()
22
使用Scanner改造上面解析每行学生信息的示例如下:
271public static List<Student> readStudents2() throws IOException {
2 List<Student> students = new ArrayList<>();
3
4 // 1. 装饰FileReader
5 BufferedReader reader = new BufferedReader(new FileReader("students.txt"));
6 try {
7 // 2. 按行读取字符流
8 String line = reader.readLine();
9 while (line != null) {
10 Student s = new Student();
11
12 // 使用扫描器扫描每行数据,分隔符为,
13 Scanner scanner = new Scanner(line).useDelimiter(",");
14 s.setName(scanner.next());
15 s.setAge(scanner.nextInt());
16 s.setScore(scanner.nextDouble());
17 students.add(s);
18
19 line = reader.readLine();
20 }
21
22 return students;
23 } finally {
24 // 3. 关闭流
25 reader.close();
26 }
27}
操作系统在启动时通常会打开三个标准流:
System.in
:标准输入流(InputStream),一般指键盘,可以和Scanner配合使用,从键盘输入数据。
System.out
:标准输出流(PrintStream),一般指控制台,输出提示信息。
System.err
:标准错误流(PrintStream),一般也是控制台,输出错误信息,如使用e.printStackTrace()打印异常信息。
71// 从标准输入流(键盘)提取数据
2Scanner in = new Scanner(System.in);
3int num = in.nextInt();
4
5// 输出到标准输出流(控制台)
6System.out.println(num)
7
标准流可以重定向,如将标准输入流重定向到文件,从文件中接受输入,或将标准输出流(错误流)重定向到文件,将输出写到文件。
211public static void main(String[] args) throws UnsupportedEncodingException, FileNotFoundException {
2 // 重定向标准输入流到字节数组
3 System.setIn(new ByteArrayInputStream("hello".getBytes("UTF-8")));
4
5 // 重定向标准输出流和标准错误流到文件
6 System.setOut(new PrintStream("out.txt"));
7 System.setErr(new PrintStream("err.txt"));
8
9 try {
10 // 从标准输入流(字节数组)扫描数据
11 Scanner in = new Scanner(System.in);
12
13 // 标准输出流输出数据(到文件)
14 System.out.println(in.nextLine());
15 System.out.println(in.nextLine()); // java.util.NoSuchElementException: No line found
16 } catch (Exception e) {
17 // 标准错误流输出数据(到文件)
18 System.err.println(e.getMessage()); // No line found
19 }
20}
21
标准输入输出流也是操作系统的重要协作机制,命令从标准输入接受参数,处理结果写到标准输出,这个标准输出可以连接到下一个命令作为标准输入,构成管道式的处理链条。
31# 查找一个日志文件access.log中127.0.0.1出现的行数
2cat access.log | grep 127.0.0.1 | wc -l
3
821/**
2* 拷贝字符流(Reader->Writer)
3*/
4public static void copy(final Reader input, final Writer output) throws IOException {
5 char[] buf = new char[4096];
6 int charsRead = 0;
7 while ((charsRead = input.read(buf)) != -1) {
8 output.write(buf, 0, charsRead);
9 }
10}
11
12
13/**
14* 读文件到字符串
15*/
16public static String readFileToString(final String fileName, final String encoding) throws IOException {
17 BufferedReader reader = null;
18 try {
19 reader = new BufferedReader(new InputStreamReader(new FileInputStream(fileName), encoding));
20 StringWriter writer = new StringWriter();
21 copy(reader, writer);
22 return writer.toString();
23 } finally {
24 if (reader != null) {
25 reader.close();
26 }
27 }
28}
29
30/**
31* 将字符串写入文件
32*/
33public static void writeStringToFile(final String fileName, final String data, final String encoding) throws IOException {
34 Writer writer = null;
35 try {
36 writer = new OutputStreamWriter(new FileOutputStream(fileName), encoding);
37 writer.write(data);
38 } finally {
39 if (writer != null) {
40 writer.close();
41 }
42 }
43}
44
45/**
46* 按行将多行数据写到文件
47*/
48public static void writeLines(final String fileName, final String encoding, final Collection<?> lines) throws IOException {
49 PrintWriter writer = null;
50 try {
51 writer = new PrintWriter(fileName, encoding);
52 for (Object line : lines) {
53 writer.println(line);
54 }
55 } finally {
56 if (writer != null) {
57 writer.close();
58 }
59 }
60}
61
62/**
63* 按行读取文件到List
64*/
65public static List<String> readLines(final String fileName, final String encoding) throws IOException {
66 BufferedReader reader = null;
67 try {
68 reader = new BufferedReader(new InputStreamReader(new FileInputStream(fileName), encoding));
69 List<String> list = new ArrayList<>();
70 String line = reader.readLine();
71 while (line != null) {
72 list.add(line);
73 line = reader.readLine();
74 }
75 return list;
76 } finally {
77 if (reader != null) {
78 reader.close();
79 }
80 }
81}
82
Properties文件一般用于配置程序的属性参数,每一行表示一个属性,属性是以等号(=)或冒号(:)分隔的键值对,如下例所示:
61# config.properties
2db.host = 192.168.10.100
3db.port : 3306
4db.username = zhangsan
5db.password = mima1234
6
Java中有一个专门的类Properties
来处理该类属性文件,它会自动忽略文件中的空行和注释行(#或!开头)以及分隔符前后的空格。
141// 1. 构造方法
2public Properties()
3public Properties(Properties defaults)
4
5// 2. 从流中加载属性
6public synchronized void load(InputStream inStream)
7
8// 3. 获取属性值
9public String getProperty(String key)
10public String getProperty(String key, String defaultValue) // 没有找到配置的值,则返回默认值
11public Set<String> stringPropertyNames() // 所有属性
12
13// 4. 设置属性值
14public Object setProperty(String key, String value)
一个使用Properties加载属性文件并获取属性的示例如下:
111public static void main(String[] args) throws IOException {
2 // 构造和加载属性
3 Properties prop = new Properties();
4 prop.load(new FileInputStream("config.properties"));
5
6 // 获取属性和使用
7 String host = prop.getProperty("db.host");
8 int port = Integer.valueOf(prop.getProperty("db.port", "3306"));
9 System.out.println(host + " " + port); // 192.168.10.100 3306
10}
11
值得注意的是,Properties不能直接处理中文,在配置文件中,所有非ASCII字符需要使用Unicode编码,如name=老马
需替换为name=\u8001\u9A6c
。如果你使用IDE进行编辑,或许它会帮你自动转换,不过,你也可以使用JDK命令:native2ascii -encoding UTF-8 native.properties ascii.properties
进行转换。
CSV(Comma-Separated Values)文件一般用于表示表格类型的数据,每一行表示一条记录,记录包含多个字段,字段之间用逗号、制表符、冒号、分号等分隔。
21张三,18,80.9
2李四,17,67.5
如果字段内容包含分隔符或换行符等特殊字符,主要有两种方式处理:
方式一:使用特殊符号如双引号(")将字段内容括起来,如果字段内容有",则用两个"表示。
方式二:使用转义字符如反斜杠()对特殊字符进行转义,如果字段内容有\,则用两个\表示,如hello\, world \\ abc'n"老马"
。
CSV文件需要处理转义字符、空格、null值以及注释等复杂情形,可以采用Apache Commons CSV库来解析CSV文件,导入依赖如下:
71<!-- https://commons.apache.org/proper/commons-csv/index.html -->
2<dependency>
3 <groupId>org.apache.commons</groupId>
4 <artifactId>commons-csv</artifactId>
5 <version>1.9.0</version>
6</dependency>
7
解析CSV文件主要依赖CSVFormat
类,有一些预定义的格式,如CSVFormat.DEFAULT
和CSVFormat.RFC4180
等,也可以通过如下一些方法自定义CSVFormat对象。
251// 构造函数(静态方法)
2public static CSVFormat newFormat(final char delimiter) // delimiter-字段之间的分隔符
3
4// 自定义方法
5public CSVFormat withDelimiter(final char delimiter) // 定义分隔符
6public CSVFormat withQuote(final char quoteChar) // 定义引号符
7public CSVFormat withEscape(final char escape) // 定义转义符
8public CSVFormat withNullString(final String nullString) // 定义值为null的对象对应的字符串值
9public CSVFormat withRecordSeparator(final char recordSeparator) // 定义记录之间的分隔符
10public CSVFormat withIgnoreSurroundingSpaces(final boolean ignoreSurroundingSpaces) // 定义是否忽略字段之间的空白
11
12// 解析CSV字符流
13public CSVParser parse(final Reader in) throws IOException // 解析CSV字符流,返回CSVParser对象
14
15// 使用 CSVParser 获取记录信息
16public long getRecordNumber() // 记录数
17public Iterator<CSVRecord> iterator() // 记录的迭代器(意味着CSVParser对象本身可以通过FOR-EACH遍历)
18public List<CSVRecord> getRecords() throws IOException // 返回所有记录
19
20// 使用 CSVRecord 获取字段信息
21public int size() // 字段个数
22public String get(final int i) // 根据字段列索引获取值,索引从0开始
23public String get(final String name) // 根据列名获取值
24public Iterator<String> iterator() // 字段的迭代器(意味着CSVRecord对象本身可以通过FOR-EACH遍历)
25
写CSV文件,可以使用 CSVPrinter
类,它有许多打印相关的方法:
71// 构造方法
2public CSVPrinter(final Appendable appendable, final CSVFormat format) throws IOException appendable-可以使用Writer format-可以使用CSVFormat.DEFAULT
3
4// 写CSV文件
5public void printRecord(final Object... values) throws IOException // 输出一条记录,参数可变,每个参数是一个字段值
6public void printRecord(final Iterable<?> values) throws IOException // 输出一条记录
7
下面是一个读取和写入CSV文件的示例:
551public static void main(String[] args) throws IOException {
2 // 自定义CSVFormat解析器
3 // 字段之间用逗号分隔,使用双引号作为引号符,使用反斜杠作为转义符,将字符串N/A视为NULL值,并忽略字段之间的空格
4 CSVFormat format = CSVFormat.newFormat(',').withQuote('"').withEscape('\\').withNullString("N/A").withIgnoreSurroundingSpaces(true);
5
6 // 读CSV文件
7 Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream("student.csv"), Charset.forName("GBK")));
8 try {
9 // 解析CSV文件,并遍历CSVParser(实现了Iterable接口)
10 for (CSVRecord record : format.parse(reader)) {
11 int fieldNum = record.size(); // 字段数
12 // 遍历所有字段
13 for (int i = 0; i < fieldNum; i++) {
14 System.out.print(record.get(i) + "|"); // 打印字段
15 }
16 System.out.println("\n---------------------------");
17 }
18 } finally {
19 reader.close();
20 }
21
22 // 写CSV文件
23 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("student-out.csv")));
24 CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT);
25 csvPrinter.printRecord("张三", 12, '男');
26 csvPrinter.printRecord("李四", 23, '女');
27 csvPrinter.close();
28}
29
30测试文件内容如下:
31张三,15
32李四,17
33错误空值,NULL
34正确空值,N/A
35方式一,"hello, world \ abc
36""老马""",12.3
37方式二,hello\, world \\ abc\n"老马",12.3
38
39
40测试输出如下:
41张三|15|
42---------------------------
43李四|17|
44---------------------------
45错误空值|NULL|
46---------------------------
47正确空值|null|
48---------------------------
49方式一|hello, world \ abc
50"老马"|12.3|
51---------------------------
52方式二|hello, world \ abc
53"老马"|12.3|
54---------------------------
55
Excel是广泛使用的表格文档格式,通常使用POI类库来进行处理,主要的类如下:
类名 | 说明 |
---|---|
Workbook | Excel文件(接口),HSSFWork-book和XSSFWorkbook实现类分别表示.xls文件和.xlsx文件 |
Sheet | 工作表 |
Row | 数据行 |
Cell | 单元格 |
使用POI类库前先导入对应的依赖如下:
131<!-- HSSFWorkbook -->
2<dependency>
3 <groupId>org.apache.poi</groupId>
4 <artifactId>poi</artifactId>
5 <version>3.17</version>
6</dependency>
7<!-- XSSFWorkbook -->
8<dependency>
9 <groupId>org.apache.poi</groupId>
10 <artifactId>poi-ooxml</artifactId>
11 <version>3.17</version>
12</dependency>
13
下面是一个简单的Excel文件读取和写入示例:
611public class Student {
2 String name;
3 int age;
4 double score;
5
6 // 全参构造/Getter/Setter/ToString()
7
8 public static void main(String[] args) throws Exception {
9 List<Student> students = Arrays.asList(new Student[]{
10 new Student("张三", 18, 80.9d),
11 new Student("李四", 17, 67.5d)
12 });
13
14 writeToExcel(students, "student.xlsx");
15 List<Student> studentList = readFromExcel("student.xlsx");
16 System.out.println(studentList);
17 }
18
19
20 /**
21 * 保存Student列表到Excel
22 */
23 public static void writeToExcel(List<Student> list, String file) throws IOException {
24 // 构建Excel文件
25 // Workbook wb = new HSSFWorkbook(); // xls文件
26 Workbook wb = new XSSFWorkbook(); // xlsx文件
27 Sheet sheet = wb.createSheet(); // 工作表
28 for (int i = 0; i < list.size(); i++) {
29 Student student = list.get(i);
30 Row row = sheet.createRow(i); // 行
31 row.createCell(0).setCellValue(student.getName()); // 单元格
32 row.createCell(1).setCellValue(student.getAge());
33 row.createCell(2).setCellValue(student.getScore());
34 }
35
36 // 写入到文件
37 OutputStream out = new FileOutputStream(file);
38 wb.write(out);
39 out.close();
40 wb.close();
41 }
42
43 /**
44 * 读Excel文件
45 */
46 public static List<Student> readFromExcel(String file) throws Exception {
47 Workbook wb = WorkbookFactory.create(new File(file));
48 List<Student> list = new ArrayList<>();
49 for (Sheet sheet : wb) { // 遍历所有工作表
50 for (Row row : sheet) { // 遍历所有行
51 String name = row.getCell(0).getStringCellValue(); // 取单元格值
52 int age = (int) row.getCell(1).getNumericCellValue();
53 double score = row.getCell(2).getNumericCellValue();
54 list.add(new Student(name, age, score));
55 }
56 }
57 wb.close();
58 return list;
59 }
60}
61
Jsoup是一种常用的HTML分析器,Maven依赖如下:
61<dependency>
2 <groupId>org.jsoup</groupId>
3 <artifactId>jsoup</artifactId>
4 <version>1.10.2</version>
5</dependency>
6
下面是使用Jsoup解析URL的示例:
171public static void main(String[] args) throws Exception {
2 // 解析本地HTML文件
3 // Document doc = Jsoup.parse(new File("articles.html"), "UTF-8");
4
5 // 从URL解析
6 String url = "http://www.cnblogs.com/swiftma/p/5631311.html";
7 Document doc = Jsoup.connect(url).get();
8
9 // 使用”CSS选择器“选择元素并遍历
10 Elements elements = doc.select("#cnblogs_post_body p a");
11 for (Element e : elements) {
12 String title = e.text(); // 获取标签体
13 String href = e.attr("href"); // href属性
14 System.out.println(title + ", " + href);
15 }
16}
17
Java内置了gzip和zip两种压缩格式的支持,其中gzip只能压缩一个文件,而zip文件中可以包含多个文件。
压缩和解压gzip文件使用GZIPOutputStream
和GZIPInputStream
装饰类,它们分别继承自DeflaterOutputStream(FilterOutputStream)和InflaterInputStream(FilterInputStream)。
651public static void main(String[] args) throws Exception {
2 gzip("D:\\mybatis-sql.log");
3 gunzip("D:\\mybatis-sql.log.gz", "D:\\mybatis-sql2.log");
4}
5
6/**
7 * gzip压缩
8 */
9public static void gzip(String fileName) throws IOException {
10 String gzipFileName = fileName + ".gz";
11 InputStream in = null;
12 OutputStream out = null;
13
14 try {
15 // 正常的输入流、被GZIPOutputStream包装的输出流
16 in = new BufferedInputStream(new FileInputStream(fileName));
17 out = new GZIPOutputStream(new BufferedOutputStream(new FileOutputStream(gzipFileName)));
18
19 // 拷贝字节流
20 copy(in, out);
21 } finally {
22 if (out != null) {
23 out.close();
24 }
25 if (in != null) {
26 in.close();
27 }
28 }
29}
30
31/**
32 * gzip解压
33 */
34public static void gunzip(String gzipFileName, String unzipFileName) throws IOException {
35 InputStream in = null;
36 OutputStream out = null;
37
38 try {
39 // 被GZIPInputStream包装的输入流、正常的输出流
40 in = new GZIPInputStream(new BufferedInputStream(new FileInputStream(gzipFileName)));
41 out = new BufferedOutputStream(new FileOutputStream(unzipFileName));
42
43 // 拷贝字节流
44 copy(in, out);
45 } finally {
46 if (out != null) {
47 out.close();
48 }
49 if (in != null) {
50 in.close();
51 }
52 }
53}
54
55/**
56 * 拷贝字节流
57 */
58public static void copy(InputStream input, OutputStream output) throws IOException {
59 byte[] buf = new byte[4096];
60 int bytesRead = 0;
61 while ((bytesRead = input.read(buf)) != -1) {
62 output.write(buf, 0, bytesRead);
63 }
64}
65
压缩和解压zip文件使用ZIPOutputStream
和ZIPInputStream
装饰类,也继承自DeflaterOutputStream(FilterOutputStream)和InflaterInputStream(FilterInputStream),但是使用起来稍微复杂些。
1181public static void main(String[] args) throws Exception {
2 zip(new File("D:\\var\\log\\fs\\FS-FMS\\8088"), new File("D:\\var\\log\\fs\\FS-FMS\\8888.zip"));
3 unzip(new File("D:\\var\\log\\fs\\FS-FMS\\8888.zip"), "D:\\var\\log\\fs\\FS-FMS\\out");
4}
5
6/**
7 * zip压缩目录
8 */
9public static void zip(File inFile, File zipFile) throws IOException {
10 // 被ZipOutputStream装饰的输出流(zip文件)
11 ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFile)));
12
13 try {
14 if (!inFile.exists()) {
15 throw new FileNotFoundException(inFile.getAbsolutePath());
16 }
17
18 // 获取父目录
19 inFile = inFile.getCanonicalFile();
20 String rootPath = inFile.getParent(); // 父目录,用于计算每个文件的相对路径
21 if (!rootPath.endsWith(File.separator)) {
22 rootPath += File.separator;
23 }
24
25 // 添加根目录到压缩文件
26 addFileToZipOut(inFile, out, rootPath);
27 } finally {
28 out.close();
29 }
30}
31
32/**
33 * 添加当前文件(目录)到压缩文件
34 */
35private static void addFileToZipOut(File file, ZipOutputStream out, String rootPath) throws IOException {
36 // 获取相对路径
37 String relativePath = file.getCanonicalPath().substring(rootPath.length());
38
39 if (file.isFile()) {
40 // 在写入每一个文件前,必须要先调用该方法,表示准备写入一个压缩条目ZipEntry
41 out.putNextEntry(new ZipEntry(relativePath)); // 每个压缩条目有个名称,这个名称是压缩文件的相对路径
42
43 // 拷贝当前文件到压缩文件
44 InputStream in = new BufferedInputStream(new FileInputStream(file));
45 try {
46 copy(in, out);
47 } finally {
48 in.close();
49 }
50 } else {
51 // 在写入每一个目录前,也必须要先调用该方法,表示准备写入一个压缩条目ZipEntry
52 // 注意:这里不能用File.separator,因为解压时写死的以/结尾作为目录,而windows平台是\
53 out.putNextEntry(new ZipEntry(relativePath + "/")); // 每个压缩条目有个名称,这个名称是压缩文件的相对路径,如果名称以字符'/'结尾,表示目录
54
55 // 循环递归处理每个文件
56 for (File f : file.listFiles()) {
57 addFileToZipOut(f, out, rootPath);
58 }
59 }
60}
61
62public static void unzip(File zipFile, String destDir) throws IOException {
63 // 被 ZipInputStream 装饰的输入流(zip文件)
64 ZipInputStream zin = new ZipInputStream(new BufferedInputStream(new FileInputStream(zipFile)));
65
66 // 目标目录必须以目录分隔符结尾
67 if (!destDir.endsWith(File.separator)) {
68 destDir += File.separator;
69 }
70
71 try {
72 // 循环处理每个压缩条目
73 ZipEntry entry = zin.getNextEntry();
74 while (entry != null) {
75 extractZipEntry(entry, zin, destDir);
76 entry = zin.getNextEntry();
77 }
78 } finally {
79 zin.close();
80 }
81}
82
83/**
84 * 提取ZIP压缩条目
85 */
86private static void extractZipEntry(ZipEntry entry, ZipInputStream zin, String destDir) throws IOException {
87 if (!entry.isDirectory()) {
88 // 创建文件的父目录
89 File parent = new File(destDir + entry.getName()).getParentFile();
90 if (!parent.exists()) {
91 parent.mkdirs();
92 }
93
94 // 目标文件
95 OutputStream entryOut = new BufferedOutputStream(new FileOutputStream(destDir + entry.getName()));
96 try {
97 // 拷贝字节流
98 copy(zin, entryOut);
99 } finally {
100 entryOut.close();
101 }
102 } else {
103 // 创建目录及其父目录
104 new File(destDir + entry.getName()).mkdirs();
105 }
106}
107
108/**
109 * 拷贝字节流
110 */
111public static void copy(InputStream input, OutputStream output) throws IOException {
112 byte[] buf = new byte[4096];
113 int bytesRead = 0;
114 while ((bytesRead = input.read(buf)) != -1) {
115 output.write(buf, 0, bytesRead);
116 }
117}
118
序列化就是将对象转化为字符流/字节流, 反序列化就是将字符流/字节流转化为对象,主要有两个用途:一个是对象持久化;另一个是跨网络的数据交换和远程过程调用。
在标准JDK中,通过ObjectInputStream
和`ObjectOutputStream
流提供了基于java.io.Serializable
接口的序列化机制。 它有很多优点,使用简单,可自动处理对象引用和循环引用,也可以方便地进行定制,处理版本问题等,但它也有一些重要的局限性:
序列化格式是一种私有格式,是一种 Java 特有的技术,不能被其他语言识别,不能实现跨语言的数据交换 。
序列化字节中保存了很多描述信息,使得序列化格式比较大,并且是二进制的,不方便查看和修改。
使用反射分析遍历对象结构,性能比较低 (ASM)。
由于这些局限性,在跨语言的数据交换格式中,经常采用XML或JSON格式,它们清晰易读,各种语言基本都支持,缺点是性能和序列化大小。在性能和序列化大小敏感的领域,往往会采用更为精简高效的二进制方式,如ProtoBuf
、Thrift、MessagePack等 。
注意:
如果尝试序列化未实现Serializable接口的对象,那么将会抛出
java.io.NotSerializableException
。如果 a、b 两个对象都引用同一个对象 c ,序列化后c 只会保存一份 , 并且反序列化后依然指向相同对象。
如果 a 、 b 两个对象有循环引用,即 a 引用了 b , 而 b 也引用了 a,反序列化后依然 可以保持引用关系。
默认的序列化机制将对象中的所有字段保存和恢复,但某些字段信息,如对象的创建时间,默认hashcode()返回值等并不需要保存,我们可以将字段声明为 transient
,则默认的序列化机制将会忽略它。如 LinkedList 中的这些字段:
31transient int size = 0;
2transient Node<E> first;
3transient Node<E> last;
之后,我们可以在类中定义 writeObject/readObject 方法来自己保存该字段。
61// 自定义序列化(注意:方法声明要一致)
2private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException
3
4// 自定义反序列化(注意:方法声明要一致)
5private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException
6
如 LinkedList 的序列化和反序列化代码如下:
251// 序列化
2private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
3 // 调用默认的序列化机制,保存所有非 transient 的字段,以及一些元数据描述等隐藏信息
4 s.defaultWriteObject();
5
6 // 写元素个数
7 s.writeInt(size);
8
9 // 循环写每个元素
10 for (Node<E> x = first; x != null; x = x.next)
11 s.writeObject(x.item);
12}
13
14// 反序列化
15private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
16 // 调用默认的序列化机制
17 s.defaultReadObject();
18
19 // 读元素个数
20 int size = s.readInt();
21
22 // 循环读入每个元素
23 for (int i = 0; i < size; i++)
24 linkLast((E) s.readObject());
25}
默认情况下,Java根据类中一系列的信息自动生成一个版本号, 如果类的定义发生了变化 , 版本号就会变化,如果反序列化时的版本号不一致,则会抛出java.io.InvalidClassException
。
我们可以手动在类中添加如下静态变量来标识类的版本,而非由Java自动生成,以便更好地控制序列化的版本和节省性能。
31// 手动指定当前类的序列化版本
2private static final long serialVersionUID = 1L;
3
如果版本号一致,但实际的字段不匹配,Java 会分情况自动进行处理 , 以尽量保持兼容性。
字段删掉了: 即流中有该字段, 而类定义中没有, 该字段会被忽略 ;
新增了字段: 即类定义中有, 而流中没有, 该字段会被设为默认值 ;
字段类型变了: 对于同名的字段, 类型变了, 会抛出 InvalidClassException。
注解就是给程序添加一些信息,用字符@开头,这些信息用于修饰它后面紧挨着的其他代码元素,比如类、接口、字段、方法、方法中的参数、构造方法等,注解可以被编译器、程序运行时、和其他工具使用,用于增强或修改程序行为等。
注解本质上就是一个接口,该接口默认继承Annotation
接口,我们使用javap将生成的注解class文件反编译后,可以看到如下内容:
101// Annotation接口
2public interface Annotation {
3 boolean equals(Object obj);
4 int hashCode();
5 String toString();
6 Class<? extends Annotation> annotationType(); // 返回真正的注解类型
7}
8
9// MyAnno接口继承自Annotation
10public interface MyAnno extends java.lang.annotation.Annotation {}
元注解是一种用于修饰注解的注解,常用的元注解如下:
@Target表示注解的目标,取值为一个或多个ElementType
枚举值。如果没有声明@Target,默认为适用于所有类型。
121public enum ElementType {
2 TYPE, // 类,接口、注解、枚举
3 FIELD, // 字段、枚举常量
4 METHOD, // 方法
5 PARAMETER, // 方法中的参数
6 CONSTRUCTOR, // 构造方法
7 LOCAL_VARIABLE, // 本地变量
8 ANNOTATION_TYPE, // 注解类型
9 PACKAGE, // 包
10 TYPE_PARAMETER, // 类型参数
11 TYPE_USE
12}
@Retention表示注解信息保留到什么时候,取值为一个RetentionPolicy
枚举值。如果没有声明@Retention,默认为CLASS。
51public enum RetentionPolicy {
2 SOURCE, // 只在源代码中保留,编译器将代码编译为字节码文件后就会丢掉
3 CLASS, // 保留到字节码文件中,但Java虚拟机将class文件加载到内存时不一定会在内存中保留
4 RUNTIME // 一直保留到运行时
5}
@Inherited表示注解将会被子类继承。如下示例中,Child类并没有直接声明Test注解,但依然检测其存在。
281public class InheritDemo {
2 // 定义一个Anno01注解,并添加@Inherited元注解
3
4 RetentionPolicy.RUNTIME) (
5 static @interface Anno01 {
6 }
7
8 // 定义一个Anno02注解,不添加@Inherited元注解
9 RetentionPolicy.RUNTIME) (
10 static @interface Anno02 {
11 }
12
13 // Base类同时添加@Anno01和@Anno02注解
14
15
16 static class Base {
17 }
18
19 // Child类继承Base类,预期应继承父类中添加了@Inherited的注解
20 static class Child extends Base {
21 }
22
23 // 验证
24 public static void main(String[] args) {
25 System.out.println(Child.class.isAnnotationPresent(Anno01.class)); // true
26 System.out.println(Child.class.isAnnotationPresent(Anno02.class)); // false
27 }
28}
@Documented表示将注解信息包含到Javadoc中。
@Repeatable表示可以在同一个地方多次应用该注解。
181
2RetentionPolicy.RUNTIME) (
3ElementType.METHOD) (
4Select.List.class) // 配置可重复使用,并指定对应的List注解 (
5public @interface Select {
6 String[] value();
7 String databaseId() default "";
8 boolean affectData() default false;
9
10 // 对应的List注解
11
12 RetentionPolicy.RUNTIME) (
13 ElementType.METHOD) (
14 @interface List {
15 Select[] value();
16 }
17
18}
可修饰接口,用于检查被标注的接口是否为函数式接口(只有一个抽象方法的接口)。
51
2RetentionPolicy.RUNTIME) (
3ElementType.TYPE) (
4public @interface FunctionalInterface {
5}
可修饰方法,表示该方法是“重写”方法,可以减少编程错误(如父类方法名修改后,若子类方法名忘记修改,存在注解时将报错)。
41ElementType.METHOD) (
2RetentionPolicy.SOURCE) (
3public @interface Override {
4}
可修饰类、方法、字段、参数等,表示对应的代码已经过时了,程序员不应该使用它,不过,它是一种警告,而不是强制性的。
51
2RetentionPolicy.RUNTIME) (
3value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) (
4public @interface Deprecated {
5}
可修饰类或方法等,用于压制Java的编译警告,通过必填参数设置压制的类型。
51TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) ({
2RetentionPolicy.SOURCE) (
3public @interface SuppressWarnings {
4 String[] value();
5}
注解的定义和接口类似,格式如下:
91// 完整格式
2元注解
3修饰符 @interface 注解名称{
4 参数列表;
5}
6
7// 最简示例
8public @interface Anno01 {
9}
注解本质上就是一个接口,定义注解参数即在接口中定义抽象方法,其中方法名表示参数名,返回值类型表示参数的类型。
171public @interface Anno02 {
2 int value(); // 基本类型
3
4 String name() default "张三"; // String类型,并指定默认值
5
6 Class<?> cls(); // Class类型
7
8 Size size(); // 枚举类型
9
10 Anno01 anno01(); // 注解类型
11
12 String[] strs(); // 数组类型
13}
14
15enum Size {
16 SMALL, MEDIUM, LARGE
17}
注意:
参数的类型必须为如下类型:基本类型(不包括包装类型)、String、Class、枚举、注解,以及这些类型的数组。
参数可以通过default关键字指定默认值,默认值必须为一个常量,不能为null。
如提供了参数,但未指定默认值,则必须在使用注解时提供具体的值(不能为null)。
查看元注解@Target的参数值,明确注解可使用的位置,然后在目标位置添加注解并填充参数。
51"deprecation","unused"}) ({
2public static void main(String[] args) {
3 Date date = new Date(2017, 4, 12);
4 int year = date.getYear(); // 方法过时警告
5}
当只有一个参数,且名称为value时,提供参数值时可以省略"value="。
11"deprecation","unused"}) ({
数组赋值时,值使用{}包裹,如果数组中只有一个值,则{}可以省略。
11"unchecked") (
注解只是对程序的标识,创建注解后,我们应同时提供处理这些标识的其它代码,以使添加的注解生效。我们主要考虑@Retention为RetentionPolicy.RUNTIME的注解,利用反射机制在运行时进行查看和利用这些信息。
51public Annotation[] getAnnotations() // 获取所有的注解
2public Annotation[] getDeclaredAnnotations() // 获取所有本元素上直接声明的注解,忽略inherited来的
3public <A extends Annotation> A getAnnotation(Class<A> annotationClass) // 获取指定类型的注解,没有返回null
4public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) // 判断是否有指定类型的注解
5public Annotation[][] getParameterAnnotations() // 获取参数的注解(仅适用Method和Contructor)
一个简单的示例如下:
471public class MethodAnnotations {
2 // 定义QueryParam注解
3 ElementType.PARAMETER) (
4 RetentionPolicy.RUNTIME) (
5 static @interface QueryParam {
6 String value();
7 }
8
9 // 定义DefaultValue注解
10 ElementType.PARAMETER) (
11 RetentionPolicy.RUNTIME) (
12 static @interface DefaultValue {
13 String value() default "";
14 }
15
16 // 定义hello方法,参数使用了注解
17 public void hello( ("action") String action,
18 "sort") ("asc") String sort){ (
19 // ...
20 }
21
22 // 解析注解
23 public static void main(String[] args) throws Exception {
24 // 获取Class->Method->ParameterAnnotations
25 Class<?> cls = MethodAnnotations.class;
26 Method method = cls.getMethod("hello", new Class[]{String.class, String.class});
27 Annotation[][] annts = method.getParameterAnnotations();
28
29 // 遍历二维数组,解析注解
30 for(int i=0; i<annts.length; i++){
31 System.out.println("annotations for paramter " + (i+1));
32 Annotation[] anntArr = annts[i];
33 // 处理第i个参数
34 for(Annotation annt : anntArr){
35 if(annt instanceof QueryParam){
36 // 处理第i个参数的QueryParam注解
37 QueryParam qp = (QueryParam)annt;
38 System.out.println(qp.annotationType().getSimpleName()+":"+ qp.value());
39 }else if(annt instanceof DefaultValue){
40 // 处理第i个参数的DefaultValue注解
41 DefaultValue dv = (DefaultValue)annt;
42 System.out.println(dv.annotationType().getSimpleName()+":"+ dv.value());
43 }
44 }
45 }
46 }
47}
911// @Label用于定制输出字段的名称
2RUNTIME) (
3FIELD) (
4public @interface Label {
5 String value() default "";
6}
7
8// @Format用于定义日期类型的输出格式
9RUNTIME) (
10FIELD) (
11public @interface Format {
12 String pattern() default "yyyy-MM-dd HH:mm:ss";
13 String timezone() default "GMT+8";
14}
15
16// 序列化
17public static String format(Object obj) {
18 StringBuilder sb = new StringBuilder();
19
20 try {
21 // 获取Class
22 Class<?> cls = obj.getClass();
23
24 // 获取当前类所有字段
25 for (Field f : cls.getDeclaredFields()) {
26 if (!f.isAccessible()) {
27 f.setAccessible(true);
28 }
29 // 解析属性名称 优先使用@Label注解配置的名称
30 Label label = f.getAnnotation(Label.class);
31 String name = label != null ? label.value() : f.getName();
32 // 获取属性值
33 Object value = f.get(obj);
34 if (value != null && f.getType() == Date.class) {
35 // 解析日期字段
36 value = formatDate(f, value);
37 }
38 sb.append(name + ":" + value + "\n");
39 }
40 return sb.toString();
41 } catch (IllegalAccessException e) {
42 throw new RuntimeException(e);
43 }
44}
45
46// 解析日期字段
47private static Object formatDate(Field f, Object value) {
48 // 获取@Format注解配置的日期格式
49 Format format = f.getAnnotation(Format.class);
50 if (format != null) {
51 // 序列化为指定格式
52 SimpleDateFormat sdf = new SimpleDateFormat(format.pattern());
53 sdf.setTimeZone(TimeZone.getTimeZone(format.timezone()));
54 return sdf.format(value);
55 }
56 return value;
57}
58
59// 目标类
60static class Student {
61 "姓名") (
62 String name;
63
64 "出生日期") (
65 pattern="yyyy/MM/dd") (
66 Date born;
67
68 "分数") (
69 double score;
70
71 public Student() {
72 }
73
74 public Student(String name, Date born, Double score) {
75 super();
76 this.name = name;
77 this.born = born;
78 this.score = score;
79 }
80
81
82 public String toString() {
83 return "Student [name=" + name + ", born=" + born + ", score=" + score + "]";
84 }
85}
86
87// 测试
88SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
89Student zhangsan = new Student("张三", sdf.parse("1990-12-12"), 80.9d);
90System.out.println(SimpleFormatter.format(zhangsan)); // 姓名:张三 出生日期:1990/12/12 分数:80.9
91
501// 修饰类中字段,表达依赖关系
2RUNTIME) (
3FIELD) (
4public @interface SimpleInject {
5}
6
7// 目标类
8public class ServiceA {
9 // 使用注解注入
10
11 ServiceB b;
12
13 public void callB(){
14 b.action();
15 }
16}
17
18public class ServiceB {
19 public void action(){
20 System.out.println("I'm B");
21 }
22}
23
24// 从容器获取对象并处理依赖关系
25public static <T> T getInstance(Class<T> cls) {
26 try {
27 // 创建对象
28 T obj = cls.newInstance();
29
30 // 遍历所有字段
31 Field[] fields = cls.getDeclaredFields();
32 for (Field f : fields) {
33 // 如果被SimpleInject注解,则处理依赖关系
34 if (f.isAnnotationPresent(SimpleInject.class)) {
35 if (!f.isAccessible()) {
36 f.setAccessible(true);
37 }
38 Class<?> fieldCls = f.getType();
39 f.set(obj, getInstance(fieldCls)); // 依赖注入(这里暂不考虑循环依赖问题)
40 }
41 }
42 return obj;
43 } catch (Exception e) {
44 throw new RuntimeException(e);
45 }
46}
47
48// 测试
49ServiceA a = SimpleContainer.getInstance(ServiceA.class);
50a.callB();
611// 修饰类,表示类型是单例
2RUNTIME) (
3TYPE) (
4public @interface SimpleSingleton {
5}
6
7// 目标类
8
9public class ServiceB {
10 public void action(){
11 System.out.println("I'm B");
12 }
13}
14
15// 单例对象缓存
16private static Map<Class<?>, Object> instances = new ConcurrentHashMap<>();
17
18// 从容器获取对象并处理依赖关系(支持单例)
19public static <T> T getInstance(Class<T> cls) {
20 try {
21 // 判断是否为单例,如果不是,则创建新实例返回
22 boolean singleton = cls.isAnnotationPresent(SimpleSingleton.class);
23 if (!singleton) {
24 return createInstance(cls);
25 }
26
27 // 是单例,尝试从缓存获取对象
28 Object obj = instances.get(cls);
29 if (obj != null) {
30 return (T) obj;
31 }
32
33 // 缓存没有,则创建单例对象,并存入缓存
34 synchronized (cls) {
35 obj = instances.get(cls);
36 if (obj == null) {
37 obj = createInstance(cls);
38 instances.put(cls, obj);
39 }
40 }
41 return (T) obj;
42 } catch (Exception e) {
43 throw new RuntimeException(e);
44 }
45}
46
47// 创建对象,和第一版的getInstance类似
48private static <T> T createInstance(Class<T> cls) throws Exception {
49 T obj = cls.newInstance();
50 Field[] fields = cls.getDeclaredFields();
51 for (Field f : fields) {
52 if (f.isAnnotationPresent(SimpleInject.class)) {
53 if (!f.isAccessible()) {
54 f.setAccessible(true);
55 }
56 Class<?> fieldCls = f.getType();
57 f.set(obj, getInstance(fieldCls));
58 }
59 }
60 return obj;
61}
841// Check注解
2RetentionPolicy.RUNTIME) (
3ElementType.METHOD) (
4public @interface Check {
5}
6
7// 目标类
8public class Calculator {
9 //加法
10
11 public void add(){
12 String str = null;
13 str.toString();
14 System.out.println("1 + 0 =" + (1 + 0));
15 }
16 //减法
17
18 public void sub(){
19 System.out.println("1 - 0 =" + (1 - 0));
20 }
21 //乘法
22
23 public void mul(){
24 System.out.println("1 * 0 =" + (1 * 0));
25 }
26 //除法
27
28 public void div(){
29 System.out.println("1 / 0 =" + (1 / 0));
30 }
31
32 public void show(){
33 System.out.println("永无bug...");
34 }
35}
36
37/**
38 * 简单的测试框架
39 * 执行@Check修饰的方法并记录异常
40 */
41public class TestCheck {
42
43 public static void main(String[] args) throws IOException {
44 //1.创建计算器对象
45 Calculator c = new Calculator();
46 //2.获取字节码文件对象
47 Class cls = c.getClass();
48 //3.获取所有方法
49 Method[] methods = cls.getMethods();
50
51 int number = 0;//出现异常的次数
52 BufferedWriter bw = new BufferedWriter(new FileWriter("bug.txt"));
53
54 for (Method method : methods) {
55 //4.判断方法上是否有Check注解
56 if(method.isAnnotationPresent(Check.class)){
57 //5.有,执行
58 try {
59 method.invoke(c);
60 } catch (Exception e) {
61 //6.捕获异常
62
63 //记录到文件中
64 number ++;
65
66 bw.write(method.getName()+ " 方法出异常了");
67 bw.newLine();
68 bw.write("异常的名称:" + e.getCause().getClass().getSimpleName());
69 bw.newLine();
70 bw.write("异常的原因:"+e.getCause().getMessage());
71 bw.newLine();
72 bw.write("--------------------------");
73 bw.newLine();
74
75 }
76 }
77 }
78
79 bw.write("本次测试一共出现 "+number+" 次异常");
80 bw.flush();
81 bw.close();
82 }
83}
84
一般来说,在操作某个数据的时候,我们都是知道并且依赖于数据的类型的,并且编译器也是根据其类型,进行代码的检查和编译。如:
根据类型使用new创建对象。
根据类型定义变量,类型可能是基本类型、类、接口或数组。
将特定类型的对象传递给方法。
根据类型访问对象的属性,调用对象的方法等。
但是反射不一样,它是在运行时(而非编译时)动态获取类型的信息,如接口信息、成员信息、方法信息、构造方法信息等。这些信息使用Class<T>
类进行封装,获取Class类后就可以创建对象、访问和修改成员、调用方法等。
在Java中,每个已加载的类在内存都有一份类信息,使用Class类进行封装,每个对象都有指向它所属类信息的引用。获取方法如下:
211// 1. 全类名方式
2// 如果类尚在硬盘中,未读入内存,则使用全类名将字节码文件加载进内存,返回Class对象
3// 这种方式多用于配置文件,将类名定义在配置文件中,然后读取文件,加载类。
4try {
5 Class<?> hashMapClass = Class.forName("java.util.HashMap");
6} catch (ClassNotFoundException exception) {
7 exception.printStackTrace();
8}
9
10// 2. 类名方式
11// 如果类已通过ClassLoader加载完成,但没有该类的具体对象,可以用类名获取该类的Class对象,这种方式一般用于参数的传递。
12Class<Date> dateClass = Date.class;
13Class<Comparable> comparableClass = Comparable.class; // 接口
14Class<Month> monthClass = Month.class; // 枚举
15
16// 3. 对象方式
17// 如果有具体的对象,直接引用Object类中定义的 final native Class<?> getClass() 方法获取:
18Class<?> objClass = new Object().getClass(); // 普通对象
19Class<Map.Entry> entryClass = Map.Entry.class; // 内部类对象 interface java.util.Map$Entry
20Class<? extends Comparable> comparableClass = ((Comparable<String>) o -> 0).getClass(); // lambda对象 class com.huangyuanxin.notes.javabase.Test01$$Lambda$1/804564176
21
特殊的,基本类型没有getClass()方法,但也都有对应的Class对象,Class的类型参数为对应的包装类型:
21Class<Integer> intClass = int.class; // int 泛型具体化类型为Integer
2Class<? extends Integer> integerClass = new Integer(0).getClass(); // class java.lang.Integer
void作为特殊的返回类型,也有对应的Class:
11Class<Void> voidClass = void.class; // void
对于数组,每种类型及每个维度都有对应数组类型的Class对象:
41Class<? extends String[]> arrClass1 = new String[10].getClass(); // class [Ljava.lang.String;
2Class<? extends String[][]> arrClass2 = new String[10][20].getClass(); // class [[Ljava.lang.String;
3Class<? extends int[]> arrClass3 = new int[10].getClass(); // class [I
4Class<? extends int[][]> arrClass4 = new int[3][2].getClass(); // class [[I
有了Class对象后,我们就可以了解到关于类型的很多信息,并基于这些信息采取一些行动,下面会分组进行介绍。
注意:
同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。
Class对象代表的类型既可以是普通的类,也可以是内部类,还可以是基本类型、数组等,可以通过以下方法进行区分 :
81public native boolean isArray() // 是否是数组
2public native boolean isPrimitive() // 是否是基本类型
3public native boolean isInterface() // 是否是接口
4public boolean isEnum() // 是否是枚举
5public boolean isAnnotation() // 是否是注解
6public boolean isAnonymousClass() // 是否是匿名内部类
7public boolean isMemberClass() // 是否是成员类,成员类定义在方法外,不是匿名类
8public boolean isLocalClass() // 是否是本地类,本地类定义在方法内,不是匿名类
反射虽然是灵活的,但一般情况下,并不是我们优先建议的,主要原因是:
反射更容易出现运行时错误。使用显式的类和接口,编译器能帮我们做类型检查,减少错误,但使用反射,类型是运行时才知道的,编译器无能为力。
反射的性能要低一些。在访问字段、调用方法前,反射先要查找对应的Field/Method,性能要慢一些。
简单的说,如果能用接口实现同样的灵活性,就不要使用反射。
另外,反射也不是万能的,有些信息无法通过反射获取,如类字段的顺序,方法的参数名称(需要手动在编译时开启 -parameters 参数)等,有些信息即使反射获得后也不能使用,如Unsafe.getUnsafe()方法,在业务代码中是不能调用的。
Class有如下方法,可以获取与类名称有关的信息:
191// 类的名称
2public String getName() // 包名.类名(常用)
3public String getSimpleName() // 不带包名的类名
4public String getCanonicalName() // 返回的名字更为友好
5public Package getPackage() // 包信息 package java.util, Java Platform API Specification, version 1.8
6
7// 类上的修饰符
8public native int getModifiers() // 获取修饰符,返回值可通过Modifier类进行解读
9
10// 父类和接口
11public native Class<? super T> getSuperclass() // 获取父类,如果当前为Object,则父类为 null
12public native Class<?>[] getInterfaces() // 对于类,为自己声明实现的所有接口;对于接口,为直接扩展的接口,不包括通过父类继承的
13
14// 类的注解
15public Annotation[] getDeclaredAnnotations() // 自己声明的注解
16public Annotation[] getAnnotations() // 所有的注解,包括继承得到的
17public <A extends Annotation> A getAnnotation(Class<A> annotationclass) // 获取指定类型的注解,包括继承得到
18public boolean isAnnotationPresent(Class<? extends Annotation> annotationclass) // 检查指定类型的注解,包括继承得到
19
类名称之间的不同可参考如下表格:
关于数组类型getName()返回值的说明:
格式为:
数组维度+数据类型
,其中数组维度用[
表示,有几个[表示是几维数组。数据类型可以是基本类型,有:boolean(Z), byte(B), char(C), short(S), int(I), long(J), float(F), double(D)。
数据类型也可以是引用类型(类或接口等),用
L+全类名+;
表示。
类中定义的静态变量和实例变量都被称为字段,字段信息用Field
类封装,可通过Class类的如下方法获取:
41public Field[] getFields() // 返回所有的public字段,包括其父类的,如果没有字段,返回空数组
2public Field[] getDeclaredFields() // 返回本类声明的所有字段,包括非public的,但不包括父类的
3public Field getField(String name) // 返回本类或父类中指定名称的public字段,找不到抛出异常NoSuchFieldException
4public Field getDeclaredField(String name) // 返回本类中声明的指定名称的字段,找不到抛出异常NoSuchFieldException
获取Field类对象后,即可通过其方法获取字段信息及修改字段内容。
201// 获取字段信息
2public String getName() //获取字段的名称
3public Class<?> getType() //返回字段的类型
4public int getModifiers() //返回字段的修饰符
5public boolean isAccessible() //判断当前程序是否有该字段的访问权限
6
7// 读写字段
8public void setAccessible(boolean flag) //设为true表示忽略Java的访问检查机制,以允许读写非public的字段
9public Object get(Object obj) //获取指定对象obj中该字段的值
10public void set(Object obj, Object value) //将指定对象obj中该字段的值设为value
11
12//以基本类型操作字段
13public void setBoolean(Object obj, boolean z)
14public boolean getBoolean(Object obj)
15public void setDouble(Object obj, double d)
16public double getDouble(Object obj)
17
18//获取字段的注解信息
19public Annotation[] getDeclaredAnnotations()
20public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
注意:
对于静态变量,get/set方法的obj参数直接传null即可。
private字段不允许直接调用get/set方法,需要先setAccessible(true)关闭Java的检查机制,否则会抛IllegalAccessException。
如果字段值为基本类型,get/set会自动在基本类型与对应的包装类型间进行转换。
类中定义的静态方法和实例方法都被称为方法,用Method
类封装,可通过Class类的如下方法获取:
51public Method[] getMethods() //返回所有的public方法,包括其父类的,如果没有方法,返回空数组
2public Method[] getDeclaredMethods() //返回本类声明的所有方法,包括非public的,但不包括父类的
3public Method getMethod(String name, Class<?>... parameterTypes) //返回本类或父类中指定名称和参数类型的public方法,找不到抛出异常NoSuchMethodException
4public Method getDeclaredMethod(String name, Class<?>... parameterTypes) //返回本类中声明的指定名称和参数类型的方法,找不到抛出异常NoSuchMethodException
5
获取Method对象后,即可通过其方法获取方法信息及调用方法等。
161// 获取方法信息
2public String getName() // 获取方法的名称
3public int getModifiers() // 获取方法的修饰符(返回值可通过Modifier类进行解读)
4public Class<?>[] getParameterTypes() // 获取方法的参数类型列表
5public Class<?> getReturnType() // 获取方法的返回值类型
6public Class<?>[] getExceptionTypes() // 获取方法声明抛出的异常类型列表
7
8// invoke
9public void setAccessible(boolean flag) // 设为true表示忽略Java的访问检查机制,以允许调用非public的方法
10public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException // 在指定对象obj上调用Method代表的方法,传递的参数列表为args
11
12// 获取注解信息
13public Annotation[] getDeclaredAnnotations() // 获取方法的所有注解信息
14public <T extends Annotation> T getAnnotation(Class<T> annotationClass) // 获取方法的指定注解信息
15public Annotation[][] getParameterAnnotations() // 获取方法参数的注解信息
16
关于invoke方法的使用有如下几点注意事项:
对于静态方法,invoke时obj参数直接传null即可。
invoke方法的参数args可以为null,也可以为一个空数组,返回值被包装为Object类型。
如果目标方法调用抛出异常,将会被包装为InvocationTargetException重新抛出,可以通过getCause方法得到原异常。
下面是一个使用invoke调用静态方法的示例:
71public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
2 Class<?> integerClass = Integer.class;
3
4 Method method = integerClass.getMethod("parseInt", new Class[]{String.class}); // args可以是Class数组
5 System.out.println(method.invoke(null, "123")); // 调用静态方法parseInt,传递参数123
6}
7
获取修饰符时,得到的是一个int类型,可通过Modifier类的如下方法进行解析:
231class Student {
2 public static final int MAX_NAME_LEN = 255;
3}
4
5public static void main(String[] args) throws NoSuchFieldException {
6 Field field = Student.class.getField("MAX_NAME_LEN");
7
8 int mod = field.getModifiers();
9 System.out.println(Modifier.toString(mod)); // public static final
10
11 System.out.println("isPublic: " + Modifier.isPublic(mod)); // isPublic: true
12 System.out.println("isProtected: " + Modifier.isProtected(mod)); // isProtected: false
13 System.out.println("isPrivate: " + Modifier.isPrivate(mod)); // isPrivate: false
14 System.out.println("isStatic: " + Modifier.isStatic(mod)); // isStatic: true
15 System.out.println("isFinal: " + Modifier.isFinal(mod)); // isFinal: true
16 System.out.println("isVolatile: " + Modifier.isVolatile(mod)); // isVolatile: false
17 System.out.println("isAbstract: " + Modifier.isAbstract(mod)); // isAbstract: false
18 System.out.println("isInterface: " + Modifier.isInterface(mod)); // isInterface: false
19 System.out.println("isNative: " + Modifier.isNative(mod)); // isNative: false
20 System.out.println("isStrict: " + Modifier.isStrict(mod)); // isStrict: false
21 System.out.println("isSynchronized: " + Modifier.isSynchronized(mod)); // isSynchronized: false
22 System.out.println("isTransient: " + Modifier.isTransient(mod)); // isTransient: false
23}
获取类信息(Class)后,可以使用其获取构造器和创建对象:
191// 直接调用无参构造创建对象(要求类定义了无参构造方法)
2public T newInstance() throws InstantiationException, IllegalAccessException
3
4// 获取构造器(Constructor)
5public Constructor<?>[] getConstructors() //获取所有的public构造器,返回值可能为长度为0的空数组
6public Constructor<?>[] getDeclaredConstructors() //获取所有的构造器,包括非public的
7public Constructor<T> getConstructor(Class<?>... parameterTypes) //获取指定参数类型的public构造器,没找到抛出异常NoSuchMethodException
8public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) //获取指定参数类型的构造器,包括非public的,没找到抛出异常NoSuchMethodException
9
10// 通过构造器(Constructor)创建对象
11public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
12
13// 构造器(Constructor)的其它方法
14public Class<?>[] getParameterTypes() // 获取构造器参数的类型信息
15public int getModifiers() // 构造器的修饰符,返回值可通过Modifier类进行解读
16public Annotation[] getDeclaredAnnotations() // 构造器的注解信息
17public <T extends Annotation> T getAnnotation(Class<T> annotationClass) // 构造器的指定注解信息
18public Annotation[][] getParameterAnnotations() // 构造器中参数的注解信息
19
前面介绍过,instanceof关键字可以用来判断引用指向的实际对象类型,但是instanceof后面的类型是在代码中确定的,如果要检查的类型是动态的,可以使用Class类的isInstance
方法,效果是一样的:
151public static void main(String[] args) throws ClassNotFoundException {
2 ArrayList<String> list = new ArrayList<>();
3
4 // 1. 通过instanceof关键字判断
5 if (list instanceof ArrayList) {
6 System.out.println("array list");
7 }
8
9 // 2. 通过Class的isInstance方法判断
10 Class<?> listClass = Class.forName("java.util.ArrayList");
11 if (listClass.isInstance(list)) {
12 System.out.println("array list");
13 }
14}
15
isInstance判断的是对象和类之间的关系,Class还有一个方法isAssignableFrom
可以判断类与类之间的关系:
81// 检查参数类型cls能否赋给当前Class类型的变量
2public native boolean isAssignableFrom(Class<?> cls);
3
4// 示例
5Object.class.isAssignableFrom(String.class) // true
6String.class.isAssignableFrom(String.class) // true
7List.class.isAssignableFrom(ArrayList.class) // true
8
在程序中也往往需要进行强制类型转换,而强制转换到的类型要在写代码时就知道的,如果是动态的,可以封装为如下toType方法:
151// 封装toType方法
2public static <T> T toType(Object obj, Class<T> cls) {
3 // 通过Class的 T cast(Object obj) 方法转换
4 return cls.cast(obj);
5}
6
7public static void main(String[] args) {
8 List<String> list = new ArrayList<>();
9
10 // 1. 通过强制类型转换
11 ArrayList arrList01 = (ArrayList) list;
12
13 // 2. 传入Class对象,转换为对应类型
14 ArrayList arrList02 = toType(list, ArrayList.class);
15}
Class有三个重载静态方法,可以根据类名加载类:
41public static Class<?> forName(String className) // Class.forName(className, true, currentLoader) 使用加载当前类的ClassLoader加载,加载后执行初始化代码
2public static Class<?> forName(String name, boolean initialize, ClassLoader loader) // loader-类加载器 initialize-加载后是否执行类的初始化代码(如static代码块)
3public static Class<?> forName(Module module, String name) // 加载指定模块中指定名称的类(JDK9)。 当找不到类的时候,它不会抛出异常,而是返回 null,它也不会执行类的初始化
4
其中className与Class.getName()的返回值是一致,如加载String类型的一维数组使用[java.lang.String;
。
21Class cls = Class.forName("[Ljava.lang.String;");
2System.out.println(cls == String[].class);
需要注意的是,基本类型不支持forName方法:
101Class.forName("int"); // ClassNotFoundException
2
3// 基本类型应作特殊处理
4public static Class<?> forName(String className) throws ClassNotFoundException {
5 if("int".equals(className)){
6 return int.class;
7 }
8 // 其它基本类型省略
9 return Class.forName(className);
10}
对于数组类型的Class,有一个专门的方法,可以获取它的元素类型:
61// 获取数组的元素类型
2public native Class<?> getComponentType()
3
4// 示例
5String[] arr = new String[]{};
6System.out.println(arr.getClass().getComponentType()); // class java.lang.String
另外,java.lang.reflect包中专门提供了一个针对数组反射操作的类Array
,以便于统一处理多种类型的数组,主要方法有:
201// 创建数组
2public static Object newInstance(Class<?> componentType, int length) // 创建指定元素类型和指定长度的数组
3public static Object newInstance(Class<?> componentType, int... dimensions) // 创建指定类型的多维数组
4
5// 获取数组长度
6public static native int getLength(Object array) // 返回数组的长度
7
8// 获取和设置元素值
9public static native Object get(Object array, int index) // 获取数组array指定的索引位置index处的值
10public static native void set(Object array, int index, Object value) // 修改数组array指定的索引位置index处的值为value
11
12// 以各种基本类型操作数组元素
13public static native double getDouble(Object array, int index)
14public static native void setDouble(Object array, int index, double d)
15public static native void setLong(Object array, int index, long l)
16public static native long getLong(Object array, int index)
17
18// 示例
19int[] intArr = (int[])Array.newInstance(int.class, 10);
20String[] strArr = (String[])Array.newInstance(String.class, 10);
对于枚举类型的Class,有一个专门方法 , 可以获取所有的枚举常量:
121// 获取所有枚举常量
2public T[] getEnumConstants()
3
4// 示例
5enum Size {
6 SMALL, MEDIUM, LARGE
7}
8public static void main(String[] args) {
9 Class<Size> sizeClass = Size.class;
10 Size[] enumConstants = sizeClass.getEnumConstants();
11 System.out.println(Arrays.toString(enumConstants)); // [SMALL, MEDIUM, LARGE]
12}
对于内部类类型的Class,也有一些特殊的方法:
51public Class<?>[] getClasses() // 获取所有的public的内部类和接口,包括从父类继承得到的
2public Class<?>[] getDeclaredClasses() // 获取自己声明的所有的内部类和接口
3public Class<?> getDeclaringClass() // 如果当前Class为内部类,获取声明该类的最外部的Class对象
4public Class<?> getEnclosingClass() // 如果当前Class为内部类,获取直接包含该类的类
5public Method getEnclosingMethod() // 如果当前Class为本地类或匿名内部类,返回包含它的方法
虽然泛型在运行时会被擦除,但在类信息Class中仍然有关于泛型的一些信息,可以通过反射获取。
131// Class
2public TypeVariable<Class<T>>[] getTypeParameters() // 获取类的泛型参数信息
3
4// Field
5public Type getGenericType() // 获取泛型类型
6
7// Method
8public Type getGenericReturnType() // 返回值的泛型类型
9public Type[] getGenericParameterTypes() // 参数的泛型类型列表
10public Type[] getGenericExceptionTypes() // 异常的泛型类型列表
11
12// Constructor
13public Type[] getGenericParameterTypes() // 获取构造器参数的泛型类型列表
其中Type
是一个接口,Class实现了Type,Type的其他子接口还有:
TypeVariable:类型参数,可以有上界,比如:T extends Number。
ParameterizedType:参数化的类型,有原始类型和具体的类型参数,比如:List<String>。
WildcardType:通配符类型,比如:?、? extends Number、? super Integer。
一个简单的使用示例如下:
491public class GenericDemo {
2 // 定义了一个泛型类,它有两个泛型参数U和V,其中U必须是Comparable<U>或其子类
3 static class GenericTest<U extends Comparable<U>, V> {
4 // 泛型引用
5 U u;
6 V v;
7
8 // 泛型类型为String的List
9 List<String> list;
10
11 // 定义了一个泛型方法,它复用了类上的泛型U作为返回值,同时有一个List参数,List的泛型具体化类型为“Number或其子类”
12 public U test(List<? extends Number> numbers) {
13 return null;
14 }
15 }
16
17 public static void main(String[] args) throws Exception {
18 // 获取类的Class信息
19 Class<?> cls = GenericTest.class;
20
21 // 获取类的泛型参数信息 -> <U extends Comparable<U>, V>
22 for (TypeVariable t : cls.getTypeParameters()) {
23 System.out.println(t.getName() + " extends " + Arrays.toString(t.getBounds())); // U extends [java.lang.Comparable<U>] V extends [class java.lang.Object]
24 }
25
26 // 获取字段的泛型类型01
27 // u的类型信息为TypeVariable
28 Field fu = cls.getDeclaredField("u");
29 System.out.println(fu.getGenericType()); // U
30
31 // 获取字段的泛型类型02
32 // list的类型信息为ParameterizedType
33 Field flist = cls.getDeclaredField("list");
34 Type listType = flist.getGenericType();
35 if (listType instanceof ParameterizedType) { // 字段list是参数化类型
36 ParameterizedType pType = (ParameterizedType) listType;
37 // raw type: interface java.util.List, type arguments:[class java.lang.String]
38 System.out.println("raw type: " + pType.getRawType() + ", type arguments:" + Arrays.toString(pType.getActualTypeArguments()));
39 }
40
41 // 获取方法参数的泛型类型01
42 // numbers的类型信息为WildcardType
43 Method m = cls.getMethod("test", List.class);
44 for (Type t : m.getGenericParameterTypes()) {
45 System.out.println(t); // java.util.List<? extends java.lang.Number>
46 }
47 }
48}
49
51public Annotation[] getAnnotations() // 获取所有的注解
2public Annotation[] getDeclaredAnnotations() // 获取所有本元素上直接声明的注解,忽略inherited来的
3public <A extends Annotation> A getAnnotation(Class<A> annotationClass) // 获取指定类型的注解,没有返回null
4public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) // 判断是否有指定类型的注解
5public Annotation[][] getParameterAnnotations() // 获取参数的注解(仅适用Method和Contructor)
如下示例使用反射实现一个简单的通用序列化/反序列化类SimpleMapper:
1471public class SimpleMapperDemo {
2 public static void main(String[] args) {
3 Student student = new Student("张三", 18, 89d);
4
5 // 序列化
6 String studentStr = SimpleMapper.toString(student); // demo01.SimpleMapperDemo$Student\nname=张三\nage=18\nscore=89.0
7
8 // 反序列化
9 Student studentNew = (Student) SimpleMapper.fromString(studentStr); // Student [name=张三, age=18, score=89.0]
10 }
11
12 static class Student {
13 String name;
14 int age;
15 Double score;
16
17 public Student() {
18 }
19
20 public Student(String name, int age, Double score) {
21 super();
22 this.name = name;
23 this.age = age;
24 this.score = score;
25 }
26
27
28 public String toString() {
29 return "Student [name=" + name + ", age=" + age + ", score=" + score + "]";
30 }
31 }
32}
33
34
35/**
36 * 简单序列化器。
37 * 支持最简单的类,即有默认构造方法,成员类型只有基本类型、包装类或String。
38 * 序列化的格式也很简单,第一行为类的名称,后面每行表示一个字段,用字符'='分隔,表示字段名称和字符串形式的值。
39 *
40 * @Author: huangyuanxin
41 * @Date: 2023/3/3
42 */
43public class SimpleMapper {
44 /**
45 * 将对象obj转换为字符串
46 */
47 public static String toString(Object obj) {
48 StringBuilder sb = new StringBuilder();
49
50 try {
51 // 获取Class信息
52 Class<?> cls = obj.getClass();
53
54 // 获取类名
55 sb.append(cls.getName() + "\n");
56
57 // 获取当前类定义的字段
58 for (Field f : cls.getDeclaredFields()) {
59 // 跳过权限检查
60 if (!f.isAccessible()) {
61 f.setAccessible(true);
62 }
63 // 获取字段名和字段值
64 sb.append(f.getName() + "=" + f.get(obj).toString() + "\n");
65 }
66
67 return sb.toString();
68 } catch (IllegalAccessException e) {
69 throw new RuntimeException(e);
70 }
71 }
72
73 /**
74 * 将字符串转换为对象
75 */
76 public static Object fromString(String str) {
77 try {
78 // 拆分
79 String[] lines = str.split("\n");
80 if (lines.length < 1) {
81 throw new IllegalArgumentException(str);
82 }
83
84 // 加载或获取Class信息
85 Class<?> cls = Class.forName(lines[0]);
86
87 // 创建目标对象
88 Object obj = cls.newInstance();
89
90 // 反序列化
91 for (int i = 1; i < lines.length; i++) {
92 // 拆分键值对
93 String[] fv = lines[i].split("=");
94 if (fv.length != 2) {
95 throw new IllegalArgumentException(lines[i]);
96 }
97
98 // 根据字段名获取字段的反射信息
99 Field f = cls.getDeclaredField(fv[0]);
100 if (!f.isAccessible()) {
101 f.setAccessible(true);
102 }
103
104 // 使用反射设置字段值
105 setFieldValue(f, obj, fv[1]);
106 }
107
108 return obj;
109 } catch (Exception e) {
110 throw new RuntimeException(e);
111 }
112 }
113
114 /**
115 * 设置字段值。先根据字段的类型,将字符串形式的值转换为了对应类型的值。
116 */
117 private static void setFieldValue(Field f, Object obj, String value) throws Exception {
118 // 获取字段类型
119 Class<?> type = f.getType();
120
121 // 转换为对应类型进行设置
122 if (type == int.class) {
123 f.setInt(obj, Integer.parseInt(value));
124 } else if (type == byte.class) {
125 f.setByte(obj, Byte.parseByte(value));
126 } else if (type == short.class) {
127 f.setShort(obj, Short.parseShort(value));
128 } else if (type == long.class) {
129 f.setLong(obj, Long.parseLong(value));
130 } else if (type == float.class) {
131 f.setFloat(obj, Float.parseFloat(value));
132 } else if (type == double.class) {
133 f.setDouble(obj, Double.parseDouble(value));
134 } else if (type == char.class) {
135 f.setChar(obj, value.charAt(0));
136 } else if (type == boolean.class) {
137 f.setBoolean(obj, Boolean.parseBoolean(value));
138 } else if (type == String.class) {
139 f.set(obj, value);
140 } else {
141 // 假定其它类型都有一个String类型参数的构造器
142 Constructor<?> ctor = type.getConstructor(String.class);
143 f.set(obj, ctor.newInstance(value));
144 }
145 }
146}
147
在Java的底层源码中,存在一个sun.misc.Unsafe
类,也可以用于创建对象。
101// Unsafe类
2public final class Unsafe {
3 private static final Unsafe theUnsafe; // 单例实例
4
5 // 私有的构造方法
6 private Unsafe() {
7 }
8
9 // ....
10}
由于它的构造方法是私有的,也没有暴露外部对象,因此只能通过反射来获取,示例如下:
161public class UnsafeDemo {
2 public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
3 // 获取theUnsafe
4 Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
5 theUnsafeField.setAccessible(true);
6 Unsafe theUnsafe = (Unsafe) theUnsafeField.get(null);
7
8 // 创建对象
9 UnsafeDemo demo = (UnsafeDemo)theUnsafe.allocateInstance(UnsafeDemo.class);
10 demo.sayHello();
11 }
12
13 public void sayHello(){
14 System.out.println("hello, Unsafe.");
15 }
16}
必须注意的是,他直接调用的底层C++代码,跳过了Java的对象管理和内存管理以及垃圾回收等机制,不会调用Java类的构造方法(可能突破单例模式限制),并且可能造成内存泄漏,因此请谨慎使用。
注意:
不能在业务代码中调用Unsafe.getUnsafe(),将会抛出SecurityException,因为该方法被
@CallerSensitive
注解。
接口常作为方法的形参来传递代码,如Collections.sort方法的Comparator类型参数:
11public static <T> void sort(List<T> list, Comparator<? super T> c)
它真实需要的不是一个Comparator对象,而是在对象的int compare(T o1, T o2)
方法中包含的大小比较逻辑。
但由于无法直接传递代码,因此只能传递一个具有该功能的对象。在Java 8之前,最简洁的方式是使用匿名内部类构建一个对象:
71Arrays.sort(files, new Comparator<File>() {
2
3
4 public int compare(File f1, File f2) {
5 return f1.getName().compareTo(f2.getName());
6 }
7});
在Java8,引入了Lambda表达式,它是一种紧凑的代码传递方式,传递代码不再有实现接口的模板代码,而是直接给出了方法的实现代码,变得更为直观。
11Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
注意:
虽然Lambda表达式非常简洁,但它只支持函数式接口,其它的接口类型还需使用匿名内部类。
Lambda表达式由->
分隔为两部分,前面()
内是方法的参数列表,后面{}
内是方法的实现代码。其中参数列表由函数式接口的抽象方法决定,必须保证参数的类型和顺序完全一致,但参数名称不做要求。
31File[] files = file.listFiles((File dir, String name) -> {
2 return name.endsWith(".log");
3});
编译器会尽可能的对Lambda表达式进行推断,以简化其书写:
对于参数列表,参数的类型可以省略。特殊的,如果只有一个参数,则()
也可以省略。
对于方法实现,支持使用表达式表示,省略{}
、return
和;
。
91// 0个参数
2executor.submit(()->System.out.println("hello")); // 方法实现直接用表达式表示
3
4// 1个参数(可省略小括号)
5File[] files = f.listFiles(path -> path.getName().endsWith(".txt"));
6
7// 2个参数
8File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));
9
注意:
如需在Lambda表达式中访问局部变量,则该变量必须是final的,或等效final的(因为该变量是通过构造参数直接传入)。
Lambda表达式不是匿名内部类的语法糖,它是基于invokedynamic指令实现的,并不会生成很多类。
281// 赋值给函数式接口
2Comparator<String> comparator = (o1, o2) -> o1.length() - o2.length();
3
4// 传递一个Lambda表达式
5private static void startThread(Runnable task) {
6 new Thread(task).start();
7}
8public static void main(String[] args) {
9 startThread(() ‐> System.out.println("线程任务执行!"));
10}
11
12// 返回一个Lambda表达式
13public static Comparator<String> newComparator() {
14 return (a, b) ‐> b.length() ‐ a.length();
15}
16
17// 同时传递和返回
18public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor)
19{
20 Objects.requireNonNull(keyExtractor);
21 return (Comparator<T> & Serializable)(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
22}
23
24// 调用sort
25Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
26Arrays.sort(files, Comparator.comparing(File::getName));
27
28
函数式接口指只有一个抽象方法的接口,一般使用@FunctionalInterface
进行注解(非强制,与@Override注解类似)。
41
2public interface Runnable {
3 public abstract void run();
4}
Java 8 预定义了大量的函数式接口,用于常见类型的代码传递,这些函数定义在java.util.function
包下,主要的有:
对于基本类型boolean/int/long/double,为避免装箱和拆箱,Java 8 提供了一些专门的函数。比如,int相关的主要函数有:
为便于举例,我们定义一个简单的学生类Student,以及一个Student列表:
161// Student类
2static class Student {
3 String name;
4 double score;
5
6 public Student(String name, double score) {
7 this.name = name;
8 this.score = score;
9 }
10}
11
12// Student列表
13List<Student> students = Arrays.asList(new Student[] {
14 new Student("zhangsan", 89d),
15 new Student("lisi", 89d),
16 new Student("wangwu", 98d) });
基础使用
131// 列表过滤(支持不同的 列表类型、过滤条件 )
2public static <E> List<E> filter(List<E> list, Predicate<E> pred) {
3 List<E> retList = new ArrayList<>();
4 for (E e : list) {
5 if (pred.test(e)) {
6 retList.add(e);
7 }
8 }
9 return retList;
10}
11
12// 过滤90分以上的
13students = filter(students, t -> t.getScore() > 90);
and/or/negate
Predicate接口提供了and/or/negate三个方法用于组合其它Predicate。
241// and 逻辑与操作
2default Predicate<T> and(Predicate<? super T> other) {
3 Objects.requireNonNull(other);
4 return (t) -> test(t) && other.test(t);
5}
6
7// or 逻辑或操作
8default Predicate<T> or(Predicate<? super T> other) {
9 Objects.requireNonNull(other);
10 return (t) -> test(t) || other.test(t);
11}
12
13// negate 取反操作
14default Predicate<T> negate() {
15 return (t) -> !test(t);
16}
17
18// 示例
19Predicate<String> predicate01 = s -> s.startsWith("a");
20Predicate<String> predicate02 = s -> s.endsWith("c");
21System.out.println(predicate01.and(predicate02).test("abc")); // true
22System.out.println(predicate01.or(predicate02).test("abc")); // true
23System.out.println(predicate01.negate().test("abc")); // false
24System.out.println(predicate01.and(predicate02).negate().test("abc")); // false
基础使用
151// 列表转换(支持不同的 列表类型、源类型、目的类型 )
2public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) {
3 List<R> retList = new ArrayList<>(list.size());
4 for (T e : list) {
5 retList.add(mapper.apply(e));
6 }
7 return retList;
8}
9
10// Student -> String
11List<String> names = map(students, t -> t.getName());
12
13// Student -> Student
14students = map(students, t -> new Student(t.getName().toUpperCase(), t.getScore()));
15
andThen与compose
Function提供了andThen/compose分别用于后置/前置组合其它Function。
181// andThen 后置组合:将T->R和R->V组合为T->V类型Function
2default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
3 Objects.requireNonNull(after);
4 return (T t) ‐> after.apply(apply(t));
5}
6
7// compose 前置组合:将T->R和V->T组合为V->R类型Function
8default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
9 Objects.requireNonNull(before);
10 return (V v) -> apply(before.apply(v));
11}
12
13// 示例
14Function<String, Double> one = s -> Double.parseDouble(s);
15Function<Double, Integer> two = d -> d.intValue();
16System.out.println(one.andThen(two).apply("12.34")); // 12
17System.out.println(two.compose(one).apply("12.34")); // 12
18
注意:
组合后,前面函数式接口的输出必须兼容后面函数式接口的输入,如one.compose(two)是错误的,Integer不能赋值给String。
基础使用
91// 列表消费(支持不同的 列表类型、源类型)
2public static <E> void foreach(List<E> list, Consumer<E> consumer) {
3 for (E e : list) {
4 consumer.accept(e);
5 }
6}
7
8// 将Student中的Name转为大写
9foreach(students, t -> t.setName(t.getName().toUpperCase()));
andThen
Consumer提供了andThen用于后置组合其它Consumer。
111// andThen 后置组合:先消费后再转给after消费
2default Consumer<T> andThen(Consumer<? super T> after) {
3 Objects.requireNonNull(after);
4 return (T t) -> { accept(t); after.accept(t); };
5}
6
7// 示例
8Consumer<String> consumer01 = s -> System.out.println(s.toLowerCase());
9Consumer<String> consumer02 = s -> System.out.println(s.toUpperCase());
10consumer01.andThen(consumer02).accept("Hello"); // hello \n HELLO
11
方法引用是Lambda表达式的进一步简化。前面说到,Lambda表达式用于传递一段代码,如果这段代码在其它地方已经存在,则可以通过类名/变量名::方法名
的格式直接引用。
可以通过类名引用静态方法,要求被引用的方法和抽象方法的形参列表及返回值完全一致。
81// Student类的静态方法(无参且返回String)
2public static String getCollegeName() {
3 return "Laoma School";
4}
5
6// 通过类名引用静态方法
7// 函数式接口Supplier<String>的抽象方法String T get()也是无参且返回String
8Supplier<String> s = Student::getCollegeName; // 等效于() -> Student.getCollegeName
也可以通过类名引用构造方法,和引用静态方法要求相同,即被引用的方法和抽象方法的形参列表及返回值完全一致。
101// Student类的构造方法(两个类参数String和double,无返回值)
2public Student(String name, double score) {
3 this.name = name;
4 this.score = score;
5}
6
7// 通过类名引用构造方法
8// BiFunction<String, Double, Student> 的 Student apply(String t, Double u) 方法也是两个参数String和double,无返回值
9BiFunction<String, Double, Student> bf = Student::new; // 等效于(name, score) -> new Student(name, score)
10
可以通过类名引用实例方法,但由于实例方法必须通过实例变量调用,因此只能引用抽象方法第一个形参类型中的实例方法,并且剩余形参列表和返回值也要求和引用方法完全一致。
101// Student类的实例方法
2public String getName() {
3 return name;
4}
5
6// 通过类名引用实例方法
7// Function<Student, String> 的 String apply(Student t) 方法第一个参数为Student t,用作实例变量
8// 剩余参数为无参,返回值为String和Student类的实例方法getName()一致
9Function<Student, String> func = Student::getName; // 第一个参数为Student t,等效于 t -> t.getName()
10
在运行时,抽象方法第一个参数不作为引用方法的参数传入,而用于调用该引用方法。
通过变量名引用它的任意实例方法,它将通过该变量进行调用。
81// 变量
2Student student = new Student("张三", 87.6);
3
4// 通过变量名引用其实例方法
5Supplier<String> s = student::getName; // 等效于() -> student.getName()
6Consumer<String> consumer = student::setName; // 等效于 (name) -> student.setName(name)
7Function<Student, String> func = student::getName; // 错误,已有实例变量student,第一个参数将传入引用方法getName,但引用方法并没有参数
8
注意:
这个变量名也可以是
super
或者this
。
流式编程通常是对集合数据进行处理,让集合中的对象像水流一样流动,分别进行去重、过滤、映射等操作,就像生产线一样。
对此,Java 8 引入了一套新的类库,位于包java.util.stream
下,称之为Stream API
。它有如下一些特征:
内部迭代:无需手工迭代集合中的元素,将迭代过程交给并行化机制,不仅代码可读性强,而且能利用多核处理器的优势。
声明式编程:基于提供的处理函数,声明要做什么,而非怎么做。
延迟计算:流是懒加载的,可以将流看作“延迟列表”,只在绝对必要时才计算。
无限流:由于延迟计算,因此流可以无限大。如 limit(n) 或 findFirst() 之类的短路操作允许在有限时间内完成无限流上的计算。
不修改源:如从集合中获得流然后对其进行过滤,将产生一个没有过滤元素的新流,而不是从源集合中删除元素。
非存储:流不是存储元素的数据结构,它通过计算操作的管道传递来自源(如数据结构、数组、生成器函数或I/O通道)的元素。
Stream API的主要操作定义在Stream
接口中,他类似于一个功能更加丰富的迭代器,可以通过Collection接口(JKD8+)的默认方法获取:
91// 返回一个顺序流
2default Stream<E> stream() {
3 return StreamSupport.stream(spliterator(), false);
4}
5
6// 返回一个并行流
7default Stream<E> parallelStream() {
8 return StreamSupport.stream(spliterator(), true);
9}
注意:
顺序流采用单线程处理,并行流并行处理,线程个数一般与系统的CPU核数一样,以充分利用CPU的计算能力。
并行流的实现基于Java 7引入的
fork/join
框架,处理由fork和join两个阶段组成,fork就是将要处理的数据拆分为小块,多线程按小块进行并行计算,join就是将小块的计算结果进行合并。
Arrays有一些stream方法,可以将数组或子数组转换为流,比如:
91// 获取流
2public static IntStream stream(int[] array)
3public static DoubleStream stream(double[] array, int startInclusive, int endExclusive)
4public static <T> Stream<T> stream(T[] array)
5
6// 示例:输出当前目录下所有普通文件的名字
7File[] files = new File(".").listFiles();
8Arrays.stream(files).filter(File::isFile).map(File::getName).forEach(System.out::println);
9
Stream有一些静态方法,可以构建流:
151//返回一个空流
2public static<T> Stream<T> empty()
3
4//返回只包含一个元素t的流
5public static<T> Stream<T> of(T t)
6
7//返回包含多个元素values的流
8public static<T> Stream<T> of(T... values)
9
10//通过Supplier生成流,流的元素个数是无限的
11public static<T> Stream<T> generate(Supplier<T> s)
12
13//同样生成无限流,第一个元素为seed,第二个为f(seed),第三个为f(f(seed)),依次类推
14public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
15
一些简单示例如下:
51// 输出10个随机数
2Stream.generate(()->Math.random()).limit(10).forEach(System.out::println);
3
4// 输出100个递增的奇数
5Stream.iterate(1, t->t+2).limit(100).forEach(System.out::println);
如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :
71// 合并流
2static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
3
4// 示例:
5Stream<String> streamA = Stream.of("张无忌");
6Stream<String> streamB = Stream.of("张翠山");
7Stream<String> result = Stream.concat(streamA, streamB);
中间操作(intermediate operation) 不触发实际的执行,用于构建流水线,返回的是Stream对象。
过滤不符合条件的元素。
111public class StreamDemo {
2 private static List<Student> students = Arrays.asList(new Student[]{
3 new Student("zhangsan", 89d),
4 new Student("lisi", 89d),
5 new Student("wangwu", 98d)});
6
7 public static void main(String[] args) {
8 // 返回学生列表中90分以上的
9 List<Student> list01 = students.stream().filter(student -> student.getScore() > 90).collect(Collectors.toList());
10 }
11}
将元素转换为其它类型。
61// 根据学生列表返回名称列表
2List<String> list02 = students.stream().map(Student::getName).collect(Collectors.toList());
3
4// 返回90分以上的学生名称列表
5List<String> list03 = students.stream().filter(student -> student.getScore() > 90).map(Student::getName).collect(Collectors.toList());
6System.out.println("list03 = " + list03);
map函数接受的参数是一个Function<T, R>
,为避免装箱/拆箱,提高性能,Stream还有如下返回基本类型特定流的方法:
31DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)
2IntStream mapToInt(ToIntFunction<? super T> mapper)
3LongStream mapToLong(ToLongFunction<? super T> mapper)
DoubleStream/IntStream/LongStream是基本类型特定的流,有一些专门的更为高效的方法。比如,求学生列表的分数总和,代码可以为:
11double sum = students.stream().mapToDouble(Student::getScore).sum();
过滤重复的元素,只留下其中一个,是否重复是根据equals方法来比较的。
31// 返回字符串列表中长度小于3的字符串、转换为小写、只保留唯一的
2List<String> strList = Arrays.asList(new String[]{"abc", "def", "hello", "Abc"});
3List<String> list04 = strList.stream().filter(s -> s.length() <= 3).map(String::toLowerCase).distinct().collect(Collectors.toList()); // [abc, def]
虽然都是中间操作,但distinct与filter/map是不同的,filter/map都是无状态的,对于流中的每一个元素,它的处理都是独立的,处理后即交给流水线中的下一个操作。
但distinct不同,它是有状态的,在处理过程中,它需要在内部记录之前出现过的元素,如果已经出现过,即重复元素,它就会过滤掉,不传递给流水线中的下一个操作。
对于顺序流,内部实现时,distinct操作会使用HashSet记录出现过的元素,如果流是有顺序的,需要保留顺序,会使用LinkedHashSet。
对流中的元素进行排序,要求元素实现Comparable接口或传入一个Comparator。
91// API
2Stream<T> sorted() // 要求实现Comparable接口
3Stream<T> sorted(Comparator<? super T> comparator) // 使用传入的comparator进行比较
4
5// 示例:过滤得到90分以上的学生,然后按分数从高到低排序,分数一样的,按名称排序
6List<Student> list05 = students.stream().filter(t -> t.getScore() > 90)
7 .sorted(Comparator.comparing(Student::getScore).reversed().thenComparing(Student::getName)) // 排序
8 .collect(Collectors.toList());
9
与distinct一样,sorted也是一个有状态的中间操作,在处理过程中,需要在内部记录出现过的元素,与distinct不同的是,每碰到流中的一个元素,distinct都能立即做出处理,要么过滤,要么马上传递给下一个操作,但sorted不能,它需要先排序,为了排序,它需要先在内部数组中保存碰到的每一个元素,到流结尾时,再对数组排序,然后再将排序后的元素逐个传递给流水线中的下一个操作。
skip跳过流中的n个元素,如果流中元素不足n个,返回一个空流。limit限制流的长度为maxSize,用它们组合可以截取第n+1 ~ n+maxSize的元素。
71// API
2Stream<T> skip(long n)
3Stream<T> limit(long maxSize)
4
5// 将学生列表按照分数排序,返回第3名到第5名
6List<Student> list06 = students.stream().sorted(Comparator.comparing(Student::getScore).reversed())
7 .skip(2).limit(3).collect(Collectors.toList());
skip和limit都是有状态的中间操作。对前n个元素,skip的操作就是过滤,对后面的元素,skip就是传递给流水线中的下一个操作。
limit的一个特点是,它不需要处理流中的所有元素,只要处理的元素个数达到maxSize,后面的元素就不需要处理了,这种可以提前结束的操作被称为短路操作。
peek主要目的是支持调试,可以使用该方法观察在流水线中流转的元素。
71// API
2Stream<T> peek(Consumer<? super T> action) // 返回的流与之前的流是一样的,没有变化,但它提供了一个Consumer,会将流中的每一个元素传给该Consumer
3
4// 进打印流的中间状态元素
5List<String> list07 = students.stream().filter(t->t.getScore()>90)
6 .peek(System.out::println).map(Student::getName).collect(Collectors.toList());
7
接受一个函数mapper,对流中的每一个元素,mapper会将该元素转换为一个流Stream,然后把新生成流的每一个元素传递给下一个操作,完成了一个1到n的映射。
91// API
2<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
3
4// 示例
5List<String> lines = Arrays.asList(new String[]{"hello abc", "老马 编程"});
6List<String> words = lines.stream().flatMap(line -> Arrays.stream(line.split("\\s+"))) // 将元素(一行字符串)按空白符分隔为了一个流(多个单词)
7 .collect(Collectors.toList());
8System.out.println(words); // [hello, abc, 老马, 编程]
9
相应的,针对基本类型,flatMap还有如下类似方法:
31DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper)
2IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper)
3LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper)
终端操作(terminal operation) 触发实际执行,返回具体结果。
返回流中的最大值/最小值,值的注意的是,它的返回值类型是Optional<T>
,而不是T,表示可能返回null(在流中不含任何元素的情况下)。
71// API
2Optional<T> max(Comparator<? super T> comparator)
3Optional<T> min(Comparator<? super T> comparator)
4
5// 示例:返回分数最高的学生
6Student student = students.stream().max(Comparator.comparing(Student::getScore).reversed()).get(); // 这里students必须不为空,否则会抛空指针异常
7
返回流中元素的个数。
21// 统计大于90分的学生个数
2long above90Count = students.stream().filter(t -> t.getScore() > 90).count();
接受一个谓词Predicate,返回一个boolean值,用于判定流中的元素是否满足一定的条件,它们的区别是:
allMatch: 只有在流中所有元素都满足条件的情况下才返回true。
anyMatch: 只要流中有一个元素满足条件就返回true。
noneMatch: 只有流中所有元素都不满足条件才返回true。
如果流为空,这几个函数的返回值都是true。
21// 判断是不是所有学生都及格了(不小于60分)
2boolean allPass = students.stream().allMatch(t -> t.getScore() >= 60);
这几个操作都是短路操作,都不一定需要处理所有元素就能得出结果,比如,对于allMatch,只要有一个元素不满足条件,就能返回false。
返回类型都是Optional,如果流为空,返回Optional.empty()。findFirst返回第一个元素,而findAny返回任一元素,它们都是短路操作。
101// API
2Optional<T> findFirst()
3Optional<T> findAny()
4
5// 随便找一个不及格的学生
6Optional<Student> student02 = students.stream().filter(t -> t.getScore() < 60).findAny();
7if (student02.isPresent()) {
8 // 不及格的学生....
9}
10
接受一个Consumer,对流中的每一个元素,传递元素给Consumer,区别在于,在并行流中,forEach不保证处理的顺序,而forEachOrdered会保证按照流中元素的出现顺序进行处理。
61// API
2void forEach(Consumer<? super T> action)
3void forEachOrdered(Consumer<? super T> action)
4
5// 逐行打印大于90分的学生
6students.stream().filter(t -> t.getScore() > 90).forEach(System.out::println);
将流转换为数组。
121// API
2Object[] toArray() // 返回的数组类型为Object[]
3<A> A[] toArray(IntFunction<A[]> generator) // 传递一个类型为IntFunction的generator,返回正确类型的数组。
4
5// IntFunction:将int转换为R类型,这里的int是流的元素个数,R类型是A[],即元素的数组类型
6public interface IntFunction<R> {
7 R apply(int value);
8}
9
10// 示例:获取90分以上的学生数组
11Student[] above90Arr = students.stream().filter(t -> t.getScore() > 90).toArray(Student[]::new); // Student[]::new就是一个类型为IntFunction<Student[]>的generator
12
代表归约或折叠,即将流中的元素归约为一个值,完成n到1的映射。它有三个重载形式,使用它们可以实现max/min/count等函数:
31Optional<T> reduce(BinaryOperator<T> accumulator); // 将第一个元素与第二个元素进行双元操作,并返回元素类型;将前一次的返回值与下一个元素再进行双元操作,依次循环
2T reduce(T identity, BinaryOperator<T> accumulator); // 传入初始值identity,与第一个元素进行双元操作,后续类似
3<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner); // 初始值类型和返回值类型自定义
第一个基本等同于调用:
151boolean foundAny = false;
2T result = null;
3
4for (T element : this stream) {
5 // 挑选第一个元素作为初始值
6 if (!foundAny) {
7 foundAny = true;
8 result = element;
9 }
10 else
11 // 后续元素进行规约操作
12 result = accumulator.apply(result, element);
13}
14
15return foundAny ? Optional.of(result) : Optional.empty(); // 空流时返回Optional.empty()
比如,使用reduce求分数最高的学生(max),代码可以为:
71Student topStudent = students.stream().reduce((accu, t) -> {
2 if (accu.getScore() >= t.getScore()) {
3 return accu;
4 } else {
5 return t;
6 }
7}).get();
第二个reduce函数多了一个identity参数,表示初始值,它基本等同于调用:
81// 初始值
2T result = identity;
3
4// 规约操作
5for (T element : this stream)
6 result = accumulator.apply(result, element)
7
8return result;
第一个和第二个reduce的返回类型只能是流中元素的类型,而第三个更为通用,它的归约类型可以自定义。
另外,它多了一个combiner参数,combiner用在并行流中,用于合并子线程的结果,对于顺序流,它基本等同于调用:
81// 初始值(注意类型为U,而非T)
2U result = identity;
3
4// 规约操作
5for (T element : this stream)
6 result = accumulator.apply(result, element)
7
8return result;
注意与第二个reduce函数相区分,它的结果类型不是T,而是U。
21// 使用reduce函数计算学生分数的和(Student...->Double)
2double sumScore = students.stream().reduce(0d, (sum, t) -> sum += t.getScore(), (sum1, sum2) -> sum1 += sum2);
以上,可以看出,reduce虽然更为通用,但比较费解,难以使用,一般情况,应该优先使用其他函数。
在之前的代码中,如过滤得到90分以上的学生列表:
21List<Student> above90List = students.stream().filter(t->t.getScore()>90)
2 .collect(Collectors.toList()); // Stream -> List<Student>
最后的collect方法是如何将Stream转换为List<Student>的呢?先看下collect方法相关的定义:
111// 收集方法
2<R, A> R collect(Collector<? super T, A, R> collector)
3
4// 收集器
5public interface Collector<T, A, R> {
6 Supplier<A> supplier(); // 工厂方法
7 BiConsumer<A, T> accumulator(); // 累加器
8 BinaryOperator<A> combiner(); // 只在并行流中有用,用于合并部分结果
9 Function<A, R> finisher(); // 调整和类型转换
10 Set<Characteristics> characteristics(); // 用于标示收集器的特征,Collector接口的调用者可以利用这些特征进行一些优化,有三个值:CONCURRENT, UNORDERED和IDENTITY_FINISH。
11}
对于顺序流,collect内部与这些接口方法的交互大概是这样的:
101// 1. 首先调用工厂方法supplier创建一个存放处理状态的容器container,类型为A
2A container = collector.supplier().get();
3
4// 2. 然后对流中的每一个元素t,调用累加器accumulator,参数为累计状态container和当前元素t
5for (T t : data)
6 collector.accumulator().accept(container, t);
7
8// 3. 最后调用finisher对累计状态container进行可能的调整,类型转换(A转换为R),并返回结果
9return collector.finisher().apply(container);
10
Collectors.toList()具体是什么呢?看下代码:
131public static <T> Collector<T, ?, List<T>> toList() {
2
3 // 返回CollectorImpl对象:Collectors内部的一个私有类
4 return new CollectorImpl<>(
5 (Supplier<List<T>>) ArrayList::new, // supplier => ArrayList::new,即创建一个ArrayList作为容器
6 List::add, // accumulator => List::add,即将碰到的每一个元素加到列表中
7 (left, right) -> {
8 left.addAll(right);
9 return left;
10 }, // combiner => 合并结果
11 CH_ID); // CH_ID => 一个静态变量,只有一个特征IDENTITY_FINISH,表示finisher没有什么事情可以做,就是把累计状态container直接返回
12}
13
也就是说,collect(Collectors.toList())背后的伪代码如下所示:
91// 创建容器
2List<T> container = new ArrayList<>();
3
4// 收集元素
5for (T t : data)
6 container.add(t);
7
8// 返回
9return container;
单列容器
91// 1. toList(ArrayList)
2List<Student> toList = students.stream().filter(t -> t.getScore() > 90).collect(Collectors.toList());
3
4// 2. toSet(HashSet)
5Set<Student> toSet = students.stream().filter(t -> t.getScore() > 90).collect(Collectors.toSet());
6
7// 3. toCollection(自定义单列容器)
8Collection<Student> toCollection = students.stream().filter(t -> t.getScore() > 90)
9 .collect(Collectors.toCollection(LinkedHashSet::new)); // LinkedHashSet::new,收集的元素去重且有序
双列容器
141// 1. toMap(HashMap)
2Map<String, Double> nameMapScore = students.stream().collect(Collectors.toMap(Student::getName, Student::getScore)); // name->score
3Map<String, Student> nameMapThis01 = students.stream().collect(Collectors.toMap(Student::getName, t -> t)); // name -> this
4Map<String, Student> nameMapThis02 = students.stream().collect(Collectors.toMap(Student::getName, Function.identity())); // name -> this,其中Function.identity()与t->t等效
5
6// 注意:如果key出现重复元素,不是覆盖,而是直接报错!
7Map<String, Integer> strLenMapErr = Stream.of("abc", "hello", "abc").collect(Collectors.toMap(Function.identity(), t -> t.length())); // java.lang.IllegalStateException: Duplicate key 3
8Map<String, Integer> strLenMap = Stream.of("abc", "hello", "abc").collect(Collectors.toMap(Function.identity(), t -> t.length(), (oldValue, value) -> value)); // 传入mergeFunction处理重复元素
9
10// 2. toMap(自定义双列容器)
11Map<String, Double> linkednameMapScore = students.stream().collect(Collectors.toMap(Student::getName, Student::getScore, (oldValue, value) -> value, LinkedHashMap::new)); // LinkedHashMap::new
12
13// 3. toConcurrentMap(ConcurrentHashMap)
14Map<String, Double> nameMapScoreByParallel = students.parallelStream().collect(Collectors.toConcurrentMap(Student::getName, Student::getScore));
除了将元素流收集到容器中,另一个常见的操作是收集为一个字符串。
81// 字符串收集器
2public static Collector<CharSequence, ?, String> joining() // 按逗号分隔
3public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) // 按delimiter分隔
4public static Collector<CharSequence, ?, String> joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix) // 添加前后缀,元素按delimiter分隔
5
6// 示例
7String strCollect = Stream.of("abc", "老马", "hello").collect(Collectors.joining(",", "[", "]")); // [abc,老马,hello]
8String strCollect = Stream.of("abc", "老马", "hello").filter(t -> false).collect(Collectors.joining(",", "[", "]")); // []
分组类似于SQL语句中的group by子句,它将元素流中的每个元素进行分组,然后针对分组进行处理和收集。
最基本的分组收集器及其示例如下:
191// classifier:分组器,类型为Function<T,K>,其中T为元素类型,K为分组值类型
2// 所有分组值一样的元素会被归为同一个组,放到一个列表中,所以返回值类型是Map<K, List<T>>
3public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier)
4
5// 示例
6public class StreamDemo2 {
7 static List<Student> students = Arrays.asList(new Student[]{
8 new Student("zhangsan", "1", 91d),
9 new Student("lisi", "2", 89d),
10 new Student("wangwu", "1", 50d),
11 new Student("zhaoliu", "2", 78d),
12 new Student("sunqi", "1", 59d)});
13
14 public static void main(String[] args) {
15 // 按Grade进行分组,默认收集到HashMap<分组值,ArrayList<T>>中
16 Map<String, List<Student>> groups = students.stream().collect(Collectors.groupingBy(Student::getGrade));
17 System.out.println("groups = " + groups); // groups = {1=[Student [name=zhangsan, score=91.0], Student [name=wangwu, score=50.0], Student [name=sunqi, score=59.0]], 2=[Student [name=lisi, score=89.0], Student [name=zhaoliu, score=78.0]]}
18 }
19}
跟踪groupingBy的源代码如下:
131// 1> groupingBy入口,传入一个分组器classifier
2public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier) {
3 return groupingBy(classifier, toList()); // 调用重载的groupingBy方法,传递toList收集器
4}
5
6// 2> 重载的groupingBy方法,传入的toList收集器作为”下游收集器“,下游收集器负责收集同一个分组内元素的结果
7public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream) {
8 return groupingBy(classifier, HashMap::new, downstream); // 再调用重载的groupingBy方法,多传递HashMap::new方法引用
9}
10
11// 3> 重载的groupingBy方法,传入的HashMap::new作为Map的工厂方法mapFactory
12public static <T, K, D, A, M extends Map<K, D>> Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier, Supplier<M> mapFactory, Collector<? super T, A, D> downstream)
13
对最后一个重载的groupingBy方法返回的收集器,其收集元素的基本过程和伪代码为:
251//先创建一个存放结果的Map
2Map map = mapFactory.get();
3
4for (T t : data) {
5 // 对每一个元素,先分组
6 K key = classifier.apply(t);
7
8 // 找存放分组结果的容器,如果没有,让下游收集器创建,并放到Map中
9 A container = map.get(key);
10 if (container == null) {
11 container = downstream.supplier().get();
12 map.put(key, container);
13 }
14
15 // 将元素交给下游收集器(即分组收集器)收集
16 downstream.accumulator().accept(container, t);
17}
18
19// 调用分组收集器的finisher方法,转换结果
20for (Map.Entry entry : map.entrySet()) {
21 entry.setValue(downstream.finisher().apply(entry.getValue()));
22}
23
24return map;
25
在groupingBy函数中,默认的Map工厂方法为HashMap::new,下游收集器为toList,它们都可以修改,实现更强大的功能。
通过修改Map工厂方法和下游收集器,在分组后可以进行一系列的自定义操作。
修改Map工厂方法:如将默认的HashMap::new替换为LinkedHashMap::new,可以实现分组有序。
修改下游收集器:如将默认的toList()替换为counting(),可以实现分组计数。
下面java.util.stream.Collectors
包中提供的一些常用下游收集器:
201// 计数
2public static <T> Collector<T, ?, Long> counting()
3
4// 计算最大值
5// 注意:分组收集器的结果是Optional<T>,而不是T
6public static <T> Collector<T, ?, Optional<T>> maxBy(Comparator<? super T> comparator)
7
8// 计算最小值
9// 注意:分组收集器的结果是Optional<T>,而不是T
10public static <T> Collector<T, ?, Optional<T>> minBy(Comparator<? super T> comparator)
11
12// 求平均值,int和long也有类似方法
13public static <T> Collector<T, ?, Double> averagingDouble(ToDoubleFunction<? super T> mapper)
14
15// 求和,long和double也有类似方法
16public static <T> Collector<T, ?, Integer> summingInt(ToIntFunction<? super T> mapper)
17
18// 求多种汇总信息,int和double也有类似方法
19// LongSummaryStatistics包括个数、最大值、最小值、和、平均值等多种信息
20public static <T> Collector<T, ?, LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper)
下面是一些示例:
271// 统计每个年级的学生个数 => counting()
2// gradeCountMap = {1=3, 2=2}
3Map<String, Long> gradeCountMap = students.stream().collect(groupingBy(Student::getGrade, counting()));
4
5// 统计一个单词流中每个单词的个数,按出现顺序排序 => counting()、LinkedHashMap::new
6// wordCountMap = {hello=2, world=1, abc=1}
7Map<String, Long> wordCountMap = Stream.of("hello", "world", "abc", "hello").collect(
8 groupingBy(Function.identity(), LinkedHashMap::new, counting()));
9
10// 获取每个年级分数最高的一个学生 => maxBy
11// topStudentMap = {1=Optional[Student [name=zhangsan, score=91.0]], 2=Optional[Student [name=lisi, score=89.0]]}
12Map<String, Optional<Student>> topStudentMap = students.stream().collect(
13 groupingBy(Student::getGrade, maxBy(Comparator.comparing(Student::getScore))));
14
15// 获取每个年级分数最高的一个学生,并对其调用Optional::get方法 => collectingAndThen
16// topStudentMap2 = {1=Student [name=zhangsan, score=91.0], 2=Student [name=lisi, score=89.0]}
17Map<String, Student> topStudentMap2 = students.stream().collect(
18 groupingBy(Student::getGrade, // 分组器
19 collectingAndThen(
20 maxBy(Comparator.comparing(Student::getScore)), // 下游收集器
21 Optional::get))); // 下游收集器后置处理
22
23// 按年级统计学生分数信息
24// gradeScoreStat = {1=DoubleSummaryStatistics{count=3, sum=200.000000, min=50.000000, average=66.666667, max=91.000000}, 2=DoubleSummaryStatistics{count=2, sum=167.000000, min=78.000000, average=83.500000, max=89.000000}}
25Map<String, DoubleSummaryStatistics> gradeScoreStat = students.stream().collect(
26 groupingBy(Student::getGrade, summarizingDouble(Student::getScore)));
27
注意:
存在更为通用的名为reducing的归约收集器,由于比较复杂且少用,暂不介绍。
在分组后,直接交给下游收集器处理的一般为元素本身,可通过mapping方法为下游收集器组合一个前置Function,在下游收集前,对传入的元素进行映射转换等一系列处理。
91// 组合下游收集器的前置Function
2public static <T, U, A, R> Collector<T, ?, R>
3 mapping(Function<? super T, ? extends U> mapper, Collector<? super U, A, R> downstream) // mapper-前置处理Function
4
5// 示例:按年级分组,得到学生名称列表
6// {1=[zhangsan, wangwu, sunqi], 2=[lisi, zhaoliu]}
7Map<String, List<String>> gradeNameMap = students.stream().collect(
8 groupingBy(Student::getGrade,
9 mapping(Student::getName, toList()))); // 先将Student映射为Student.Name,在传给toList收集器
相应的,也可以通过collectingAndThen方法为下游收集器组合一个后置Function,在下游收集完成后,在分组内进行排序(sort)、过滤(filter)、限制返回元素(skip/limit)等一系列操作。
111// 组合下游收集器的后置Function
2public static<T,A,R,RR> Collector<T,A,RR>
3 collectingAndThen(Collector<T,A,R> downstream, Function<R,RR> finisher) { // finisher-后置处理Function
4
5 // 主要代码,其它代码省略
6 return new CollectorImpl<>(downstream.supplier(),
7 downstream.accumulator(),
8 downstream.combiner(),
9 downstream.finisher().andThen(finisher), // 后置处理
10 characteristics);
11}
组内排序
151// 通用组合方法,收集完后在组内进行排序(当然,也可以先排序后分组,效果一样)
2public static <T> Collector<T, ?, List<T>> collectingAndSort(Collector<T, ?, List<T>> downstream,
3 Comparator<? super T> comparator) {
4 return Collectors.collectingAndThen(downstream, (r) -> {
5 r.sort(comparator);
6 return r;
7 });
8}
9
10// 示例:按年级分组,分组内学生按照分数由高到低进行排序
11Map<String, List<Student>> gradeStudentMap =
12 students.stream().collect(
13 groupingBy(Student::getGrade, // 按年级分组
14 collectingAndSort(toList(), // 组合toList下游收集器
15 Comparator.comparing(Student::getScore).reversed()))); // 和用于分组内排序的Function
组内过滤
131// 通用组合方法,收集完后在组内进行过滤(当然,也可以先过滤后分组,效果一样)
2public static <T> Collector<T, ?, List<T>> collectingAndFilter( Collector<T, ?, List<T>> downstream,
3 Predicate<T> predicate) {
4 return Collectors.collectingAndThen(downstream, (r) -> {
5 return r.stream().filter(predicate).collect(Collectors.toList());
6 });
7}
8
9// 示例:按年级分组,分组后,每个分组只保留不及格的学生(低于60分)
10Map<String, List<Student>> gradeStudentMap =
11 students.stream().collect(
12 groupingBy(Student::getGrade,
13 collectingAndFilter(toList(), t->t.getScore()<60)));
组内特定区间
121// 通用组合方法,收集完后只返回特定区间元素
2public static <T> Collector<T, ?, List<T>> collectingAndSkipLimit(Collector<T, ?, List<T>> downstream, long skip, long limit) {
3 return Collectors.collectingAndThen(downstream, (r) -> {
4 return r.stream().skip(skip).limit(limit).collect(Collectors.toList());
5 });
6}
7
8// 示例:按年级分组,分组后,每个分组只保留前两名的学生
9Map<String, List<Student>> gradeStudentMap = students.stream()
10 .sorted(Comparator.comparing(Student::getScore).reversed()) // 排序
11 .collect(groupingBy(Student::getGrade, // 按年级分组
12 collectingAndSkipLimit(toList(), 0, 2))); // 只保留组内前2个元素
分组的一个特殊情况是分区,就是将流按true/false分为两个组,Collectors有专门的分区函数:
61// 分区,下游收集器为toList()
2public static <T> Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate)
3
4// 分区,指定一个下游收集器
5public static <T, D, A> Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
6 Collector<? super T, A, D> downstream)
下面是一些简单示例:
61// 将学生按照是否及格(大于等于60分)分为两组
2Map<Boolean, List<Student>> byPass = students.stream().collect(partitioningBy(t->t.getScore()>=60));
3
4// 按是否及格分组后,计算每个分组的平均分
5Map<Boolean, Double> avgScoreMap = students.stream().collect(partitioningBy(t->t.getScore()>=60,
6 averagingDouble(Student::getScore)));
groupingBy和partitioningBy都可以接受一个下游收集器,而下游收集器又可以是分组或分区。
41// 按年级对学生分组,分组后,再按照是否及格对学生进行分区
2Map<String, Map<Boolean, List<Student>>> multiGroup = students.stream().collect(
3 groupingBy(Student::getGrade, // 按年级分组
4 partitioningBy(t -> t.getScore() >= 60))); // 再分区