Arrays、Collections、Objects,都是对应对象的工具类。好的工具类通用的写法特征:
- 构造器必须是私有的。这样的话,工具类就无法被 new 出来,因为工具类在使用的时候,无需初始化,直接使用即可,所以不会开放出构造器出来。
- 工具类的工具方法必须被 static、final 关键字修饰。这样的话就可以保证方法不可变、不能被重写,并且可以直接使用,非常方便。
我们需要注意的是,尽量不在工具方法中,对共享变量有做修改的操作访问(如果必须要做的话,必须加锁),因为会有线程安全的问题。除上述情况外,工具类方法本身是没有线程安全问题的,可以放心使用。
Arrays
Arrays
主要对数组Array
提供了一些高效的操作,比如说排序、查找、填充、拷贝、相等判断等等。
排序
Arrays.sort
主要用于排序,从下图可以看出,sort可接收所有类型数组。
sort 的性能高,是因为其使用了DualPivotQuickSort 双轴快速排序
。以sort(byte[] a)为例,看到其实是直接调用了DualPivotQuicksort.sort
方法。
对于双轴快速排序,可参考双轴快速排序-CSDN
public static void sort(byte[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1);
}
二分查找
使用Arrays.bin
的注意事项:
- 如果被搜索的数组是无序的,一定要先排序,否则二分搜索很有可能搜索不到。
- 搜索方法返回的是数组的下标值。如果搜索不到,返回的下标值就会是负数,这时我们需要判断一下正负。如果是负数,还从数组中获取数据的话,会报数组越界的错误。
二分查找的实现
// a:我们要搜索的数组,fromIndex:从那里开始搜索,默认是0;
// toIndex:搜索到何处停止,默认是数组大小
// key:我们需要搜索的元素
// c:比较器
private static <T> int binarySearch0(T[] a, int fromIndex, int toIndex,
T key, Comparator<? super T> c) {
// 如果比较器 c 是空的,直接使用 key 的 Comparable.compareTo 方法进行排序
// 假设 key 类型是 String 类型,String 默认实现了 Comparable 接口,就可以直接使用 compareTo 方法进行排序
if (c == null) {
// 这是另外一个方法,使用内部排序器进行比较的方法
return binarySearch0(a, fromIndex, toIndex, key);
}
int low = fromIndex;
int high = toIndex - 1;
// 开始位置小于结束位置,就会一直循环搜索
while (low <= high) {
// 假设 low =0,high =10,那么 mid 就是 5,所以说二分的意思主要在这里,每次都是取索引的中间值
// 这里涉及到二进制计算
int mid = (low + high) >>> 1;
T midVal = a[mid];
// 比较数组中间值和给定的值的大小关系
int cmp = c.compare(midVal, key);
// 如果数组中间值小于给定的值,说明我们要找的值在中间值的右边。我们将左边界移到中点往右
if (cmp < 0)
low = mid + 1;
// 我们要找的值在中间值的左边。我们将左边界移到中点往右
else if (cmp > 0)
high = mid - 1;
else
// 找到了
return mid; // key found
}
// 返回的值是负数,表示没有找到
return -(low + 1); // key not found.
}
拷贝
数组拷贝我们经常遇到,有时需要拷贝整个数组,有时需要拷贝部分。
比如 ArrayList 在 add(扩容) 或 remove(删除元素不是最后一个) 操作时,会进行一些拷贝。
拷贝整个数组我们可以使用 copyOf 方法,拷贝部分我们可以使用 copyOfRange 方法,以 copyOfRange 为例,看下底层源码的实现:Arrays 的拷贝方法,实际上底层调用的是 System.arraycopy 这个 native 方法。
// original 原始数组数据
// from 拷贝起点
// to 拷贝终点
public static char[] copyOfRange(char[] original, int from, int to) {
// 需要拷贝的长度
int newLength = to - from;
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
// 初始化新数组
char[] copy = new char[newLength];
// 调用 native 方法进行拷贝,参数的意思分别是:
// 被拷贝的数组、从数组那里开始、目标数组、从目的数组那里开始拷贝、拷贝的长度
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
// System.arraycopy方法签名
public static native void arraycopy(Object src,
int srcPos,
Object dest,
int destPos,
int length);
Collections
Collections也提供了二分查找、排序方法,sort 底层使用的就是 Arrays.sort 方法,binarySearch 底层是自己重写了二分查找算法,实现的逻辑和 Arrays 的二分查找算法完全一致。
求集合中最大、小值(max、min)
max 和 min 方法很相似,以下以max方法为例。max提供了如下两种重载。
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) {}
public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp) {}
两个方法的区别在于一个需要传外部排序器;一个虽然不需要传排序器,但是需要集合中的元素强制实现 Comparable 接口。从上述源码中,可以体会到:
- max 方法泛型 T 定义得非常巧妙,意思是泛型必须继承 Object 并且实现 Comparable 的接口。一般让我们来定义的话,我们可能会在方法里面去判断有无实现 Comparable 的接口,这种是在运行时才能知道结果。
而这里泛型直接定义了必须实现 Comparable 接口,在编译的时候就可通过错误告诉使用者,当前类没有实现 Comparable 接口
- 以上源码给我们提供了实现两种排序机制的优秀示例:自定义类实现 Comparable 接口、传入外部排序器。两种排序实现原理类似,但实现有所差别,我们在工作中如果需要些排序的工具类时,可以效仿。
不可变集合
得到不可变集合的方法,都以unmodifiable
为前缀。这类方法的意思是,我们会从原集合中,得到一个不可变的新集合。得到的新集合只能访问,无法修改,一旦修改,就会抛出异常。
新集合都是Collections中定义的私有类,如UnmodifiableCollection
。为什么在调用不可变集合的修改方法时会抛出错误呢?原因如下图,这些不可变的集合类,只开放了查询方法,在定义时即将修改性质的方法,抛出UnsupportedOperationException
。
同步/线程安全集合
得到不可变集合的方法,都以synchronized
为前缀。以 synchronizedCollection
为例,源码如下,所有的方法签名中,都使用了synchronized
来修饰,所以多线程对集合同时进行操作,是线程安全的。
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
private static final long serialVersionUID = 3053995032091335093L;
final Collection<E> c; // Backing Collection
final Object mutex; // Object on which to synchronize
SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
mutex = this;
}
SynchronizedCollection(Collection<E> c, Object mutex) {
this.c = Objects.requireNonNull(c);
this.mutex = Objects.requireNonNull(mutex);
}
public int size() {
synchronized (mutex) {return c.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return c.isEmpty();}
}
public boolean contains(Object o) {
synchronized (mutex) {return c.contains(o);}
}
public Object[] toArray() {
synchronized (mutex) {return c.toArray();}
}
public <T> T[] toArray(T[] a) {
synchronized (mutex) {return c.toArray(a);}
}
public Iterator<E> iterator() {
return c.iterator(); // Must be manually synched by user!
}
public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
public boolean remove(Object o) {
synchronized (mutex) {return c.remove(o);}
}
public boolean containsAll(Collection<?> coll) {
synchronized (mutex) {return c.containsAll(coll);}
}
public boolean addAll(Collection<? extends E> coll) {
synchronized (mutex) {return c.addAll(coll);}
}
public boolean removeAll(Collection<?> coll) {
synchronized (mutex) {return c.removeAll(coll);}
}
public boolean retainAll(Collection<?> coll) {
synchronized (mutex) {return c.retainAll(coll);}
}
public void clear() {
synchronized (mutex) {c.clear();}
}
public String toString() {
synchronized (mutex) {return c.toString();}
}
// Override default methods in Collection
@Override
public void forEach(Consumer<? super E> consumer) {
synchronized (mutex) {c.forEach(consumer);}
}
@Override
public boolean removeIf(Predicate<? super E> filter) {
synchronized (mutex) {return c.removeIf(filter);}
}
@Override
public Spliterator<E> spliterator() {
return c.spliterator(); // Must be manually synched by user!
}
@Override
public Stream<E> stream() {
return c.stream(); // Must be manually synched by user!
}
@Override
public Stream<E> parallelStream() {
return c.parallelStream(); // Must be manually synched by user!
}
private void writeObject(ObjectOutputStream s) throws IOException {
synchronized (mutex) {s.defaultWriteObject();}
}
}
Objects
对于 Objects,我们经常使用的就是两个场景,相等判断和判空。
相等判断
Objects 提供 equals 和 deepEquals 两个方法来进行相等判断。
equals:判断基本类型和自定义类
deepEquals :判断数组
从以下源码中,可以看出 Objects 对基本类型和复杂类型的对象,都有着比较细粒度的判断,可以放心使用。
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
public static boolean deepEquals(Object a, Object b) {
if (a == b)
return true;
else if (a == null || b == null)
return false;
else
// 实际是调用了 deepEquals0 方法
return Arrays.deepEquals0(a, b);
}
static boolean deepEquals0(Object e1, Object e2) {
assert e1 != null;
boolean eq;
if (e1 instanceof Object[] && e2 instanceof Object[])
eq = deepEquals ((Object[]) e1, (Object[]) e2);
else if (e1 instanceof byte[] && e2 instanceof byte[])
eq = equals((byte[]) e1, (byte[]) e2);
else if (e1 instanceof short[] && e2 instanceof short[])
eq = equals((short[]) e1, (short[]) e2);
else if (e1 instanceof int[] && e2 instanceof int[])
eq = equals((int[]) e1, (int[]) e2);
else if (e1 instanceof long[] && e2 instanceof long[])
eq = equals((long[]) e1, (long[]) e2);
else if (e1 instanceof char[] && e2 instanceof char[])
eq = equals((char[]) e1, (char[]) e2);
else if (e1 instanceof float[] && e2 instanceof float[])
eq = equals((float[]) e1, (float[]) e2);
else if (e1 instanceof double[] && e2 instanceof double[])
eq = equals((double[]) e1, (double[]) e2);
else if (e1 instanceof boolean[] && e2 instanceof boolean[])
eq = equals((boolean[]) e1, (boolean[]) e2);
else
eq = e1.equals(e2);
return eq;
}
为空判断
Objects 提供 isNull
和 requireNonNull
两个方法来进行为空判断。
requireNonNull 方法,传入的参数一旦为空,会直接抛出异常,我们需要在实际场景中进行选择。
public static boolean isNull(Object obj) {
return obj == null;
}
public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier) {
if (obj == null)
throw new NullPointerException(messageSupplier.get());
return obj;
}