Vector
一个古老的集合类,实现了一个动态数组,现在已经不常用Stack
栈ArrayList
顺序表LinkedList
链表+队列+双端队列PriorityQueue
优先级队列HashSet
集合(不重复)(哈希表实现)HashMap
哈希表TreeSet
有序集合(红黑树)TreeMap
有序键值对(红黑树)Java 中的泛型是通过在类、接口或方法的声明中使用尖括号 < >
来实现的。
public class Box<T> { // 类型参数也可以指定多个,用 ',' 分隔
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
Java 中泛型的类型参数不能直接是基本数据类型, 而应该是引用类型
Box<int> intBox = new Box<>(42); // 错误的
Box<Integer> intBox = new Box<>(42); // 正确的
Box<Integer> intBox = new Box<Integer>(42); // 后面的Integer可以写也可以省略
规范:类型形参一般使用一个大写字母表示,常用的名称有:
裸类型就是不指定类型实参
Box box = new box();
注意:一般不用裸类型,裸类型是 Java 为了兼容老版本
Java 泛型的原理是基于类型擦除(Type Erasure)的概念。在编译时,Java 编译器会擦除泛型类型的信息,将泛型代码转换为普通的非泛型代码。这样,泛型的类型信息只存在于编译期,而在运行时是不可见的。
Object
类型。例如,List
在运行时会被擦除为 List
。泛型的上界是指泛型类型参数的限制,用于指定该参数必须是某个特定类型或其子类型。在Java中,通过使用 extends 关键字来指定上界。上界限制了可以传递给泛型类型参数的类型范围。
public class Box<T extends Number> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public static void main(String[] args) {
// 使用泛型的上界,创建一个存储整数的盒子
Box<Integer> intBox = new Box<>(42);
// 使用泛型的上界,创建一个存储双精度浮点数的盒子
Box<Double> doubleBox = new Box<>(3.14);
// 下面的代码会导致编译错误,因为String不是Number的子类型
// Box stringBox = new Box<>("Hello");
}
}
一个泛型上界的典型运用
// 求数组中的最大值
class Alg<T extends Comparable<T>> { // 设置上界 Comparable
public T findMax(T[] array) {
T max = array[0];
for (int i = 1; i < array.length; ++i) {
if (max.compareTo(array[i]) < 0) { // 因为此处要对两个类型进行比较,所以必须是限定继承了Comparable接口
max = array[i];
}
}
return max;
}
}
对上面的 Alg
类,我们发现 findMax()
方法的调用不需要依赖对象,所以可以设置为静态方法。
但是静态方法因为不直接访问类的实例,也就无法获取类中的泛型信息,无法使用泛型类型参数 T
。
要解决这个问题,可以把静态方法设置为泛型方法,将泛型参数放在方法的返回类型之前:
class Alg {
public static <T extends Comparable<T>> T findMax(T[] array) {
T max = array[0];
for (int i = 1; i < array.length; ++i) {
if (max.compareTo(array[i]) < 0) {
max = array[i];
}
}
return max;
}
}
?
通配符: 表示未知类型,可以用在方法参数、方法返回类型、变量等地方。例如,在一个方法中接受一个未知类型的集合:
public static void printList(List<?> list) {
for (Object element : list) {
System.out.print(element + " ");
}
System.out.println();
}
通配符的上界
通配符是为了处理泛型协变问题而引入的,特别是 ? extends T
这种形式
比如我们有这样一个继承关系:
class Fruit {
// ...
}
class Apple extends Fruit {
// ...
}
class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
Apple 是 Fruit 的子类,那么我们可以认为 Box
是 Box
的子类,可以写出以下代码:
public class Test {
private static void fun(Box<Fruit> box) {
System.out.println(box.getValue());
}
public static void main(String[] args) {
Box<Apple> box = new Box<>(new Apple());
fun(box); // 将 Box 传给 Box,报错
}
}
解决方式:
public class Test {
private static void fun(Box<? extends Fruit> box) { // 加入通配符 ? extends Fruit ,正确
System.out.println(box.getValue());
}
public static void main(String[] args) {
Box<Apple> box = new Box<>(new Apple());
fun(box);
}
}
但是 fun 方法里是不能对 box 里的元素进行修改,因为 box 里面放的是 Fruit 及其子类,没有一个类型可以让 Fruit 及其子类 都能接收。
通配符的下界
使用 ? super T
通配符时,表示可以接受类型为 T
或 T
的超类型的对象。这主要用于对泛型集合进行写入操作,允许向集合中添加 T
类型及其子类型的元素。
<? super 下界>
<? super Integer> // 表示可以传入的实参的类型是 Integer 或者 Integer 的父类
例子:
class Food {
}
class Fruit extends Food {
// ...
}
class Apple extends Fruit {
// ...
}
class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public void setValue(T newValue) {
this.value = newValue;
}
public T getValue() {
return value;
}
}
public class Test {
static void fun(Box<? super Fruit> box) {
// box 内可以存放 Fruit 及其子类的对象
box.setValue(new Fruit()); // 行
// box.setValue(new Food()); // 不行
box.setValue(new Apple()); // 行
}
public static void main(String[] args) {
Box<Food> box = new Box<>(new Food());
// 类型参数 Food 是 Fruit 的超类,可以传参
fun(box);
}
}
因为 Box 里的元素是 Fruit 类型及其父类类型,那么这些类型一定可以接收 Fruit 的子类对象。而如果你读数据,读出来的是 Fruit 类型或其父类类型,你没有一个很好的类型去接收,如果使用向下转型又不安全。所以通配符的下界适合写数据,不适合读数据。
装箱:基本数据类型->包装类类型
int a = 10;
Integer b = a; // 自动装箱
Integer c = Integer.valueOf(a); // 手动装箱
拆箱:包装类类型->基本数据类型
Integer a = 20; // 自动装箱
int b = a; // 自动拆箱
double d1 = a; // 自动拆箱
double d2 = a.doubleValue(); // 手动拆箱
特殊案例:
public static void main(String[] args) {
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
System.out.println(a == b);
System.out.println(c == d);
}
/* 输出:
true
false
*/
为什么值都是 100 就相等,值都是 200 的却不相等了呢?
装箱底层其实就是调用的 valueOf()
,查看 valueOf()
源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high) // low 为 -128,high 为 127
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可以看到,当 i 在 -128 ~ 127 的时候就直接返回 cache 里面的对象了,如果超过了这个范围就会 new 一个新的对象。这就是导致上述问题的原因。
所以涉及引用类型比较大小,都应该使用对应的比较方法,而不是 ==
号,包装类也不例外。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList
实现了 RandomAccess, Cloneable, java.io.Serializable
接口,表明它是可随机访问的,可以 clone
的,支持序列化的。ArrayList
不是线程安全的ArrayList
底层是一段连续的空间,并且可以动态扩容, 是一个动态类型的顺序表方法 | 解释 |
---|---|
ArrayList() |
构造一个初始容量为10的空列表 |
ArrayList(int initialCapacity) |
构造一个初始容量为 initialCapacity 的空列表 |
ArrayList(Collection extends E> c) |
利用其他 Collection 构造 ArrayList |
问题1:调用无参构造后,ArrayList
的容量是多少?
答案:0
elementData
是 ArrayList
底层维护的用来存储元素的数组,在调用无参构造后,只是把 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
这个空数组的引用赋给了它,所以容量还是 0
在第一次调用 add 时:
结论:第一次调用 add 的时候,底层数组 elementData 的容量才变成了10,如果只是调用无参构造方法,容量是0.
问题2:扩容是几倍扩
查看扩容方法 grow
,其中 int newCapacity = oldCapacity + (oldCapacity >> 1);
可以得知的 1.5 倍扩容
使用指定容量构造
如果指定的容量 > 0,就直接给你开对应容量的数组;如果 = 0,就把空数组传过来;如果 < 0 ,则抛非法参数异常
方法 | 解释 |
---|---|
boolean add(E e) |
尾插,返回true |
void add(int index, E element) |
在指定位置插入,如果index越界会抛异常 |
boolean addAll(Collection extends E> c) |
尾插一个集合的元素,可能抛空指针异常 |
boolean addAll(int index, Collection extends E> c) |
指定位置的版本,可能抛越界或空指针异常 |
E remove(int index) |
删除指定位置的元素,返回被删除的元素 |
boolean remove(Object o) |
删除第一个出现的 o |
E get(int index) |
获取 index 位置的元素 |
E set(int index, E element) |
设置 index 下标的元素为 element |
void clear() |
清空 |
boolean contains(Object o) |
查看 o 是否在 ArrayList 中 |
int indexOf(Object o) |
返回第一个 o 所在位置下标 |
int lastIndexOf(Object o) |
返回最后一个 o 所在位置下标 |
List |
截取部分,返回的引用仍指向原顺序表 |
遍历
for循环,println(arrayList),foreach 都和普通数组一样。
迭代器
Iterator<Integer> it = arrayList.iterator(); // 获取迭代器对象, 也可以用listIterator()
while (it.hasNext()) { // 判断是否有下一个
System.out.println(it.next()); // 访问,并向后走一步
}
iterator()
是Iterable
接口定义的方法,返回的迭代器只能向前遍历,并且不支持在遍历过程中修改集合。listIterator()
是List
接口定义的方法,返回的迭代器可以向前和向后遍历,同时支持在遍历过程中对集合进行修改。public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
底层由链表实现
ArrayList
的大同小异方法 | 解释 |
---|---|
Stack() |
构造一个空的栈 |
E push(E item) |
入栈,并返回 item |
E pop() |
出栈,并返回出栈的元素 |
E peek() |
获取栈顶元素 |
int size() |
获取栈中有效元素的个数 |
boolean empty() |
检测栈是否为空 |
Queue 是个接口,所以你并不能 new 它,而是 new 它的子类 LinkedList
Queue<Integer> queue = new LinkedList<>();
方法 | 解释 |
---|---|
boolean offer(E e) |
入队 |
E poll() |
出队 |
E peek() |
获取队头元素 |
默认创建小堆,容量为11
创建大堆,需要传比较器:
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
如果是自定义类,也可以通过实现 Comparable 接口来指定比较规则
也可以用 lambda 表达式:
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>((o1, o2) -> o2 - o1);
优先级队列的扩容说明:
Map 是接口,K-V模型,使用时需要实例化它的子类 TreeMap 或 HashMap
Map.Entry
是 Map 内部实现的用来存放
方法 | 解释 |
---|---|
K getKey() |
返回 entry 中的 key |
V getValue() |
返回 entry 中的 value |
V setValue(V value) |
将 value 替换为指定的 value |
注意:Key 不能设置
方法 | 解释 |
---|---|
V get(Object key) |
返回 key 对应的 value |
V getOrDefault(Object key, V defaultValue) |
返回 key 对应的 value,如 key 不存在,返回默认值 |
V put(K key, V value) |
设置 key 对应的 value |
V remove(Object key) |
删除 key 对应的映射关系 |
Set |
返回所有 key 的不重复集合 |
Collection |
返回所有 value 的可重复集合 |
Set |
返回所有的 key-value 映射关系 |
boolean containsKey(Object key) |
判断是否包含 key |
boolean containsValue(Object value) |
判断是否包含 value |
Set 是接口,K 模型,使用时需要实例化它的子类 TreeSet 或 HashSet
方法 | 解释 |
---|---|
boolean add(E e) |
添加元素,重复元素不会被添加成功 |
void clear() |
清空 |
boolean contains(Object o) |
判断 o 是否在集合中 |
Iterator |
返回迭代器 |
boolean remove(Object o) |
删除 |
int size() |
返回元素个数 |
boolean isEmpty() |
判断是否为空 |
Object[] toArray() |
将Set转换为数组 |
boolean containsAll(Collection> c) |
判断集合 c 中的元素是否在Set中全部存在 |
boolean addAll(Collection extends E> c) |
将集合 c 中的元素添加到 Set 中,可以达到去重的效果 |
实现 Set
接口的常用类还有 LinkedHashSet
,是在 HashSet
的基础上维护了一个双向链表来记录元素的插入次序
TreeSet 底层是 TreeMap 实现,查看源码:
public TreeSet() {
this(new TreeMap<E,Object>()); // 因为是 K 模型,第二个类型参数给的是Object,是用来占位的
}
下面简单了解一下源码:
默认负载因子是0.75:
默认桶的初始大小为16:
只是初始化,桶的容量是 0,在第一次 put 的时候才会给哈希桶分配 16 的容量
桶的最大容量:
树化的条件1:链表的长度>=8
树化的条件2:桶的容量>=64
树退化的条件
哈希桶
计算哈希值的方式:
h >>> 16 位将高位移到低位,然后与原来的 h 异或,这样得到的 h ,低 16 位既有高位的信息又有低位的信息。
因为在 Java 的哈希表实现中,会使用二的幂次作为哈希表的大小,并使用位掩码进行索引计算。由于使用了二的幂次,高位的哈希值可能会在索引计算中失去一些信息,导致一些哈希冲突。为了减少这种冲突,采用了一种位传播的策略。
在 Java 中,哈希表的数组长度为2的幂。这是因为在使用二进制表示时,取模运算(%)可以被优化为位运算,即使用掩码进行操作,而不是昂贵的除法运算。这种优化可以提高性能。
例如,如果哈希表的长度为2^n(其中n是非负整数),那么对于任意正整数k,k % (2^n) 可以等效为 k & (2^n - 1)。